Skip to content

Commit 26b7fe4

Browse files
authored
fix: sanitize platform specific props to avoid crash (#728)
Fixes #727 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Prevented crashes by ignoring platform-incompatible audio settings, improving stability on iOS and Android. - More reliable recorder startup through platform-aware audio handling. - Improved error handling for asynchronous recording initialization. - Documentation - Added platform-aware audio configuration guide with dedicated iOS, Android, and cross-platform sections, examples, and notes on applicable properties. - Chores - Updated development linting tooling for TypeScript and tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 4f29ef7 commit 26b7fe4

File tree

14 files changed

+699
-466
lines changed

14 files changed

+699
-466
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,16 +424,26 @@ export function Player() {
424424
425425
### Audio Configuration
426426

427+
The library automatically detects the platform and applies the appropriate settings. Use platform-specific properties (with `IOS` or `Android` suffixes) for fine-grained control, or use common properties for cross-platform consistency.
428+
429+
#### iOS Configuration
430+
427431
```typescript
428432
const audioSet: AudioSet = {
429-
// iOS Settings
433+
// iOS-specific settings
430434
AVSampleRateKeyIOS: 44100,
431435
AVFormatIDKeyIOS: AVEncodingOption.aac,
432436
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
433437
AVNumberOfChannelsKeyIOS: 2,
434438
AVModeIOS: 'measurement', // Available options: 'gameChatAudio', 'measurement', 'moviePlayback', 'spokenAudio', 'videoChat', 'videoRecording', 'voiceChat', 'voicePrompt'
439+
};
440+
```
441+
442+
#### Android Configuration
435443

436-
// Android Settings
444+
```typescript
445+
const audioSet: AudioSet = {
446+
// Android-specific settings
437447
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
438448
AudioSourceAndroid: AudioSourceAndroidType.MIC,
439449
// Common audio settings (apply on Android as well)
@@ -442,7 +452,22 @@ const audioSet: AudioSet = {
442452
AudioEncodingBitRate: 128000,
443453
AudioChannels: 1,
444454
};
455+
```
456+
457+
#### Cross-Platform Configuration
458+
459+
For consistent settings across platforms, use common properties that work on both iOS and Android:
445460

461+
```typescript
462+
const audioSet: AudioSet = {
463+
// Common settings automatically applied to the appropriate platform
464+
AudioSamplingRate: 44100,
465+
AudioEncodingBitRate: 128000,
466+
AudioChannels: 1,
467+
};
468+
```
469+
470+
```typescript
446471
const meteringEnabled = true; // Enable audio metering
447472

448473
const uri = await Sound.startRecorder(

android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ class HybridSound : HybridSoundSpec() {
9595
// For audio metering
9696
this.meteringEnabled = enableMetering ?: false
9797

98+
// Sanitize audioSets to ignore iOS-specific fields on Android to prevent crashes
99+
val sanitizedAudioSets = audioSets?.copy(
100+
AVEncoderAudioQualityKeyIOS = null,
101+
AVModeIOS = null,
102+
AVEncodingOptionIOS = null,
103+
AVFormatIDKeyIOS = null,
104+
AVNumberOfChannelsKeyIOS = null,
105+
AVLinearPCMBitDepthKeyIOS = null,
106+
AVLinearPCMIsBigEndianKeyIOS = null,
107+
AVLinearPCMIsFloatKeyIOS = null,
108+
AVLinearPCMIsNonInterleavedIOS = null,
109+
AVSampleRateKeyIOS = null
110+
)
111+
98112
// Return immediately and process in background
99113
CoroutineScope(Dispatchers.IO).launch {
100114
try {
@@ -115,7 +129,7 @@ class HybridSound : HybridSoundSpec() {
115129
MediaRecorder()
116130
}.apply {
117131
// Set audio source
118-
val audioSource = when (audioSets?.AudioSourceAndroid) {
132+
val audioSource = when (sanitizedAudioSets?.AudioSourceAndroid) {
119133
AudioSourceAndroidType.DEFAULT -> MediaRecorder.AudioSource.DEFAULT
120134
AudioSourceAndroidType.MIC -> MediaRecorder.AudioSource.MIC
121135
AudioSourceAndroidType.VOICE_UPLINK -> MediaRecorder.AudioSource.VOICE_UPLINK
@@ -141,7 +155,7 @@ class HybridSound : HybridSoundSpec() {
141155
setAudioSource(audioSource)
142156

143157
// Set output format
144-
val outputFormat = when (audioSets?.OutputFormatAndroid) {
158+
val outputFormat = when (sanitizedAudioSets?.OutputFormatAndroid) {
145159
OutputFormatAndroidType.DEFAULT -> MediaRecorder.OutputFormat.DEFAULT
146160
OutputFormatAndroidType.THREE_GPP -> MediaRecorder.OutputFormat.THREE_GPP
147161
OutputFormatAndroidType.MPEG_4 -> MediaRecorder.OutputFormat.MPEG_4
@@ -169,7 +183,7 @@ class HybridSound : HybridSoundSpec() {
169183
setOutputFormat(outputFormat)
170184

171185
// Set audio encoder
172-
val audioEncoder = when (audioSets?.AudioEncoderAndroid) {
186+
val audioEncoder = when (sanitizedAudioSets?.AudioEncoderAndroid) {
173187
AudioEncoderAndroidType.DEFAULT -> MediaRecorder.AudioEncoder.DEFAULT
174188
AudioEncoderAndroidType.AMR_NB -> MediaRecorder.AudioEncoder.AMR_NB
175189
AudioEncoderAndroidType.AMR_WB -> MediaRecorder.AudioEncoder.AMR_WB
@@ -195,7 +209,7 @@ class HybridSound : HybridSoundSpec() {
195209

196210
// Apply sane defaults based on AudioQuality when explicit values are missing
197211
// Default to HIGH if not provided
198-
val audioQuality = audioSets?.AudioQuality ?: AudioQualityType.HIGH
212+
val audioQuality = sanitizedAudioSets?.AudioQuality ?: AudioQualityType.HIGH
199213

200214
// Define quality presets to avoid repetition
201215
data class QualitySettings(val samplingRate: Int, val channels: Int, val bitrate: Int)
@@ -207,13 +221,13 @@ class HybridSound : HybridSoundSpec() {
207221
val defaults = presets[audioQuality]
208222

209223
// Apply settings with explicit overrides taking precedence
210-
val samplingRate = audioSets?.AudioSamplingRate?.toInt() ?: defaults?.samplingRate
224+
val samplingRate = sanitizedAudioSets?.AudioSamplingRate?.toInt() ?: defaults?.samplingRate
211225
samplingRate?.let { setAudioSamplingRate(it) }
212226

213-
val channels = audioSets?.AudioChannels?.toInt() ?: defaults?.channels
227+
val channels = sanitizedAudioSets?.AudioChannels?.toInt() ?: defaults?.channels
214228
channels?.let { setAudioChannels(it) }
215229

216-
val bitrate = audioSets?.AudioEncodingBitRate?.toInt() ?: defaults?.bitrate
230+
val bitrate = sanitizedAudioSets?.AudioEncodingBitRate?.toInt() ?: defaults?.bitrate
217231
bitrate?.let { setAudioEncodingBitRate(it) }
218232

219233
// Set output file

example/ios/Podfile.lock

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PODS:
3737
- ReactCommon/turbomodule/core
3838
- SocketRocket
3939
- Yoga
40-
- NitroSound (0.2.6):
40+
- NitroSound (0.2.7):
4141
- boost
4242
- DoubleConversion
4343
- fast_float
@@ -2702,75 +2702,75 @@ SPEC CHECKSUMS:
27022702
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
27032703
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
27042704
hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394
2705-
NitroModules: d9c969e83c30ec1e7efc95e0ae58c21db1585c14
2706-
NitroSound: 746745fd04657304045259e5d0e8dddb812e92db
2705+
NitroModules: 7d693306799405ca141ef5c24efc0936f20a09c0
2706+
NitroSound: c038fdf1477ce0949cf2d613ae32002493f8988d
27072707
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
27082708
RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585
27092709
RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23
27102710
RCTTypeSafety: 4aefa8328ab1f86da273f08517f1f6b343f6c2cc
27112711
React: 2073376f47c71b7e9a0af7535986a77522ce1049
27122712
React-callinvoker: 751b6f2c83347a0486391c3f266f291f0f53b27e
2713-
React-Core: 7195661f0b48e7ea46c3360ccb575288a20c932c
2714-
React-CoreModules: 14f0054ab46000dd3b816d6528af3bd600d82073
2715-
React-cxxreact: 7f602425c63096c398dac13cd7a300efd7c281ae
2713+
React-Core: dff5d29973349b11dd6631c9498456d75f846d5e
2714+
React-CoreModules: c0ae04452e4c5d30e06f8e94692a49107657f537
2715+
React-cxxreact: 376fd672c95dfb64ad5cc246e6a1e9edb78dec4c
27162716
React-debug: 7b56a0a7da432353287d2eedac727903e35278f5
2717-
React-defaultsnativemodule: 695d8a0b40f735edb3c4031e0f049e567fdac47a
2718-
React-domnativemodule: 6d66c1f61f277d008d98cae650ce2c025b89d3b9
2719-
React-Fabric: 997d4115d688f483cb409a1290171bff3c93dab4
2720-
React-FabricComponents: 8167e5e363ca3a3fe394d8afee355e4072bea1db
2721-
React-FabricImage: f8f9f2c97657116702acc670e3f4357bc842bed3
2722-
React-featureflags: dfb4d0d527d55dd968231370f6832b9197ee653d
2723-
React-featureflagsnativemodule: c63cfd8fe95cd98f12ebb37daa919c4544810a45
2724-
React-graphics: fd795f1c2a1133a08dde31725b20949edd545dca
2725-
React-hermes: 0a167bbb02c242664745e82154578c64e90a88e5
2726-
React-idlecallbacksnativemodule: 1798c6aa33ddc7c2e9fa3c3d67729728639889e9
2727-
React-ImageManager: c498ee6945dffacc82bfa175aa3264212f27c70b
2728-
React-jserrorhandler: 216951fea62fc26c600f4c96f0dc4fd53d1e7a9b
2729-
React-jsi: 9c27d27d3007b73c702ad3fd5a6166557c741020
2730-
React-jsiexecutor: 2b24f4ed4026344a27f717bf947a434cbbeeff7a
2731-
React-jsinspector: 02394b059c48805780f7d977366317a24168d00e
2732-
React-jsinspectorcdp: f4b6d5c5c9db05ef44d082716714f90cfeed96bb
2733-
React-jsinspectornetwork: e7c77d01b5f0664e24c0bec1aea27d5e3d7fb746
2734-
React-jsinspectortracing: aaa96a4e53abb88dc6d47da3b5744c710652fef9
2735-
React-jsitooling: 226e5f4147c7b6f1ae1954a8406ffa713f3da828
2736-
React-jsitracing: 8a2fbeaa9c53c3f0b23904ccffefc890eae48d71
2737-
React-logger: 1767babce2d28c3251039ce05556714a2c8c6ded
2738-
React-Mapbuffer: 33f678ee25b6c0ee2b01b1ecec08e3e02424cefe
2739-
React-microtasksnativemodule: 44b44a4d3cd6ffb85d928abf741acdc26722de2e
2740-
react-native-slider: 30cea7008de785564de2f4fd064f2deb38614a4a
2741-
react-native-video: f0566c7e82ba660f656c3269436f297a3f6a02d9
2742-
React-NativeModulesApple: b5d18bc109c45c9a1c6b71664991b5cc3adc4e48
2717+
React-defaultsnativemodule: 393b81aaa6211408f50a6ef00a277847256dd881
2718+
React-domnativemodule: 5fb5829baa7a7a0f217019cbad1eb226d94f7062
2719+
React-Fabric: a17c4ae35503673b57b91c2d1388429e7cbee452
2720+
React-FabricComponents: a76572ddeba78ebe4ec58615291e9db4a55cd46a
2721+
React-FabricImage: d806eb2695d7ef355ec28d1a21f5a14ac26b1cae
2722+
React-featureflags: 1690ec3c453920b6308e23a4e24eb9c3632f9c75
2723+
React-featureflagsnativemodule: 7b7e8483fc671c5a33aefd699b7c7a3c0bdfdfec
2724+
React-graphics: ea146ee799dc816524a3a0922fc7be0b5a52dcc1
2725+
React-hermes: fcbdc45ecf38259fe3b12642bd0757c52270a107
2726+
React-idlecallbacksnativemodule: a353f9162eaa7ad787e68aba9f52a1cfa8154098
2727+
React-ImageManager: ec5cf55ce9cc81719eb5f1f51d23d04db851c86c
2728+
React-jserrorhandler: 594c593f3d60f527be081e2cace7710c2bd9f524
2729+
React-jsi: 59ec3190dd364cca86a58869e7755477d2468948
2730+
React-jsiexecutor: b87d78a2e8dd7a6f56e9cdac038da45de98c944f
2731+
React-jsinspector: b9204adf1af622c98e78af96ec1bca615c2ce2bd
2732+
React-jsinspectorcdp: 4a356fa69e412d35d3a38c44d4a6cc555c5931e8
2733+
React-jsinspectornetwork: 7820056773178f321cbf18689e1ffcd38276a878
2734+
React-jsinspectortracing: b341c5ef6e031a33e0bd462d67fd397e8e9cd612
2735+
React-jsitooling: 401655e05cb966b0081225c5201d90734a567cb9
2736+
React-jsitracing: 67eff6dea0cb58a1e7bd8b49243012d88c0f511e
2737+
React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48
2738+
React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696
2739+
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
2740+
react-native-slider: 663776e5683e257de8df8091abc2d93ff6ec67db
2741+
react-native-video: a509f299d71020addc12bff98bc094c142d3f09c
2742+
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
27432743
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
2744-
React-perflogger: a03d913e3205b00aee4128082abe42fd45ce0c98
2745-
React-performancetimeline: 9b5986cc15afafb9bf246d7dd55bdd138df94451
2744+
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
2745+
React-performancetimeline: 9041c53efa07f537164dcfe7670a36642352f4c2
27462746
React-RCTActionSheet: 42195ae666e6d79b4af2346770f765b7c29435b9
2747-
React-RCTAnimation: 5c10527683128c56ff2c09297fb080f7c35bd293
2748-
React-RCTAppDelegate: c616bd5b0d12f0b21dfacee9cd2d512c6df013aa
2749-
React-RCTBlob: 6e3757bdd7dce6fd9788c0dd675fd6b6c432db9d
2750-
React-RCTFabric: e8f3b9da97477710bf0904a62eb5b5209c964694
2751-
React-RCTFBReactNativeSpec: c042f8d60d44ad9e2c722da89323c0bdab7a37af
2752-
React-RCTImage: a3482fe1ae562d1bab08b42d4670a7c9a21813cd
2753-
React-RCTLinking: d82b9adb141aef9d2b38d446b837ae7017ab60aa
2754-
React-RCTNetwork: fa9350dd99354c5695964f589bd4790bdd4f6a85
2755-
React-RCTRuntime: be99a38cd23388c08921d8969c82a1997a11ec90
2756-
React-RCTSettings: b7f4a03f44dba1d3a4dc6770843547b203ca9129
2757-
React-RCTText: 91dc597a5f6b27fd1048bb287c41ea05eeca9333
2758-
React-RCTVibration: 27b09ddf74bddfa30a58d20e48f885ea6ed6c9d9
2747+
React-RCTAnimation: fa103ccc3503b1ed8dedca7e62e7823937748843
2748+
React-RCTAppDelegate: 665d4baf19424cef08276e9ac0d8771eec4519f9
2749+
React-RCTBlob: 0fa9530c255644db095f2c4fd8d89738d9d9ecc0
2750+
React-RCTFabric: 1fcd8af6e25f92532f56b4ba092e58662c14d156
2751+
React-RCTFBReactNativeSpec: db171247585774f9f0a30f75109cc51568686213
2752+
React-RCTImage: ba824e61ce2e920a239a65d130b83c3a1d426dff
2753+
React-RCTLinking: d2dc199c37e71e6f505d9eca3e5c33be930014d4
2754+
React-RCTNetwork: 87137d4b9bd77e5068f854dd5c1f30d4b072faf6
2755+
React-RCTRuntime: 137fafaa808a8b7e76a510e8be45f9f827899daa
2756+
React-RCTSettings: 71f5c7fd7b5f4e725a4e2114a4b4373d0e46048f
2757+
React-RCTText: b94d4699b49285bee22b8ebf768924d607eccee3
2758+
React-RCTVibration: 6e3993c4f6c36a3899059f9a9ead560ddaf5a7d7
27592759
React-rendererconsistency: b4785e5ed837dc7c242bbc5fdd464b33ef5bfae7
2760-
React-renderercss: cef3f26df2ddec558ce3c0790fc574b4fb62ce67
2761-
React-rendererdebug: e68433ae67738caeb672a6c8cc993e9276b298a9
2762-
React-RuntimeApple: dc1d4709bf847bc695dbe6e8aaf3e22ef25aef02
2763-
React-RuntimeCore: ca3473c8b6578693fa3bad4d44240098d49d6723
2764-
React-runtimeexecutor: 0db3ca0b09cd72489cef3a3729349b3c2cf13320
2765-
React-RuntimeHermes: f92cabaf97ef2546a74360eddfc1c74a34cb9ff8
2766-
React-runtimescheduler: 06aea75069e0d556a75d258bfc89eb0ebd5d557e
2767-
React-timing: 1a90df9a04d8e7fd165ff7fa0918b9595c776373
2768-
React-utils: 92115441fb55ce01ded4abfb5e9336a74cd93e9c
2769-
ReactAppDependencyProvider: b20fba6c3d091a393925890009999472c8f94d95
2770-
ReactCodegen: 58dc2eb138a27145826ad7d5568610159dfcadee
2771-
ReactCommon: 00df7b9f859c9d02181844255bb89a8bca544374
2760+
React-renderercss: e6fb0ba387b389c595ffa86b8b628716d31f58dc
2761+
React-rendererdebug: 60a03de5c7ea59bf2d39791eb43c4c0f5d8b24e3
2762+
React-RuntimeApple: 3df6788cd9b938bb8cb28298d80b5fbd98a4d852
2763+
React-RuntimeCore: fad8adb4172c414c00ff6980250caf35601a0f5d
2764+
React-runtimeexecutor: d2db7e72d97751855ea0bf5273d2ac84e5ea390c
2765+
React-RuntimeHermes: 04faa4cf9a285136a6d73738787fe36020170613
2766+
React-runtimescheduler: f6a1c9555e7131b4a8b64cce01489ad0405f6e8d
2767+
React-timing: 1e6a8acb66e2b7ac9d418956617fd1fdb19322fd
2768+
React-utils: 52bbb03f130319ef82e4c3bc7a85eaacdb1fec87
2769+
ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3
2770+
ReactCodegen: 1d05923ad119796be9db37830d5e5dc76586aa00
2771+
ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6
27722772
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2773-
Yoga: 922d794dce2af9c437f864bf4093abfa7a131adb
2773+
Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d
27742774

27752775
PODFILE CHECKSUM: a9a2b8cbd155d2f049cd8e258791ebb1adcc04e2
27762776

ios/Sound.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ final class HybridSound: HybridSoundSpec_base, HybridSoundSpec_protocol {
2727

2828
public func startRecorder(uri: String?, audioSets: AudioSet?, meteringEnabled: Bool?) throws -> Promise<String> {
2929
let promise = Promise<String>()
30-
30+
31+
// Sanitize audioSets to ignore Android-specific fields on iOS to prevent crashes
32+
let sanitizedAudioSets = audioSets.map { original in
33+
var sanitized = original
34+
sanitized.AudioSourceAndroid = nil
35+
sanitized.OutputFormatAndroid = nil
36+
sanitized.AudioEncoderAndroid = nil
37+
return sanitized
38+
}
39+
3140
// Return immediately to prevent UI blocking
3241
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
3342
guard let self = self else {
@@ -40,7 +49,7 @@ final class HybridSound: HybridSoundSpec_base, HybridSoundSpec_protocol {
4049
self.recordingSession = AVAudioSession.sharedInstance()
4150

4251
// Apply AVModeIOS if provided
43-
let sessionMode = audioSets?.AVModeIOS.map(self.getAudioSessionMode) ?? .default
52+
let sessionMode = sanitizedAudioSets?.AVModeIOS.map(self.getAudioSessionMode) ?? .default
4453

4554
try self.recordingSession?.setCategory(.playAndRecord,
4655
mode: sessionMode,
@@ -56,7 +65,7 @@ final class HybridSound: HybridSoundSpec_base, HybridSoundSpec_protocol {
5665
if allowed {
5766
// Continue in background
5867
DispatchQueue.global(qos: .userInitiated).async {
59-
self.setupAndStartRecording(uri: uri, audioSets: audioSets, meteringEnabled: meteringEnabled, promise: promise)
68+
self.setupAndStartRecording(uri: uri, audioSets: sanitizedAudioSets, meteringEnabled: meteringEnabled, promise: promise)
6069
}
6170
} else {
6271
promise.reject(withError: RuntimeError.error(withMessage: "Recording permission denied. Please enable microphone access in Settings."))

0 commit comments

Comments
 (0)