nosnia-audio-recorder 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,39 +72,109 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
72
72
 
73
73
  override fun startRecording(options: ReadableMap, promise: Promise) {
74
74
  try {
75
+ // Prevent concurrent recordings
75
76
  if (isRecording) {
76
77
  promise.reject("ALREADY_RECORDING", "Recording is already in progress")
77
78
  return
78
79
  }
79
80
 
80
- val filename = options.getString("filename") ?: generateFilename()
81
- val bitrate = options.getInt("bitrate").takeIf { it > 0 } ?: 128000
82
- val channels = options.getInt("channels").takeIf { it > 0 } ?: 1
83
- val sampleRate = options.getInt("sampleRate").takeIf { it > 0 } ?: 44100
81
+ // Validate and get filename
82
+ val filename = options.getString("filename")?.takeIf { it.isNotBlank() } ?: generateFilename()
83
+
84
+ // Validate and sanitize audio parameters with bounds checking
85
+ val bitrate = options.getInt("bitrate").let {
86
+ when {
87
+ it <= 0 -> 128000 // Default
88
+ it > 320000 -> 320000 // Max reasonable bitrate
89
+ else -> it
90
+ }
91
+ }
92
+
93
+ val channels = options.getInt("channels").let {
94
+ when {
95
+ it <= 0 -> 1 // Default to mono
96
+ it > 2 -> 2 // Max 2 channels (stereo)
97
+ else -> it
98
+ }
99
+ }
100
+
101
+ val sampleRate = options.getInt("sampleRate").let {
102
+ when {
103
+ it <= 0 -> 44100 // Default
104
+ it < 8000 -> 8000 // Min reasonable sample rate
105
+ it > 48000 -> 48000 // Max reasonable sample rate
106
+ else -> it
107
+ }
108
+ }
84
109
 
110
+ // Get and validate recording directory
85
111
  val recordingDir = getRecordingDirectory()
86
112
  if (!recordingDir.exists()) {
87
- recordingDir.mkdirs()
113
+ if (!recordingDir.mkdirs()) {
114
+ promise.reject("DIRECTORY_ERROR", "Failed to create recording directory")
115
+ return
116
+ }
117
+ }
118
+
119
+ // Validate directory is writable
120
+ if (!recordingDir.canWrite()) {
121
+ promise.reject("DIRECTORY_ERROR", "Recording directory is not writable")
122
+ return
123
+ }
124
+
125
+ // Create file path
126
+ val recordingFile = File(recordingDir, filename)
127
+ currentFilePath = recordingFile.absolutePath
128
+
129
+ if (currentFilePath == null || currentFilePath?.isEmpty() == true) {
130
+ promise.reject("PATH_ERROR", "Invalid file path")
131
+ return
88
132
  }
89
133
 
90
- currentFilePath = File(recordingDir, filename).absolutePath
134
+ // Clean up any existing file
135
+ try {
136
+ if (recordingFile.exists()) {
137
+ recordingFile.delete()
138
+ }
139
+ } catch (e: Exception) {
140
+ // Continue even if cleanup fails
141
+ }
91
142
 
143
+ // Create and configure media recorder
92
144
  mediaRecorder = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
93
145
  MediaRecorder(reactApplicationContext)
94
146
  } else {
95
147
  @Suppress("DEPRECATION")
96
148
  MediaRecorder()
97
- }).apply {
98
- setAudioSource(MediaRecorder.AudioSource.MIC)
99
- // Use MPEG output format for MP3-compatible encoding
100
- setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
101
- setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
102
- setAudioEncodingBitRate(bitrate)
103
- setAudioChannels(channels)
104
- setAudioSamplingRate(sampleRate)
105
- setOutputFile(currentFilePath)
106
- prepare()
107
- start()
149
+ })
150
+
151
+ if (mediaRecorder == null) {
152
+ promise.reject("INIT_RECORDER_ERROR", "Failed to create MediaRecorder instance")
153
+ return
154
+ }
155
+
156
+ try {
157
+ mediaRecorder?.apply {
158
+ setAudioSource(MediaRecorder.AudioSource.MIC)
159
+ setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
160
+ setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
161
+ setAudioEncodingBitRate(bitrate)
162
+ setAudioChannels(channels)
163
+ setAudioSamplingRate(sampleRate)
164
+ setOutputFile(currentFilePath)
165
+
166
+ // Prepare recorder
167
+ prepare()
168
+
169
+ // Start recording
170
+ start()
171
+ }
172
+ } catch (e: Exception) {
173
+ mediaRecorder?.release()
174
+ mediaRecorder = null
175
+ currentFilePath = null
176
+ promise.reject("START_RECORDING_ERROR", e.message ?: "Failed to start recording", e)
177
+ return
108
178
  }
109
179
 
110
180
  isRecording = true
@@ -115,75 +185,122 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
115
185
  mediaRecorder?.release()
116
186
  mediaRecorder = null
117
187
  isRecording = false
118
- promise.reject("START_RECORDING_ERROR", e.message, e)
188
+ promise.reject("START_RECORDING_ERROR", e.message ?: "Unknown error", e)
119
189
  }
120
190
  }
121
191
 
122
192
  override fun stopRecording(promise: Promise) {
123
193
  try {
124
- if (!isRecording && mediaRecorder == null) {
194
+ if (!isRecording || mediaRecorder == null) {
125
195
  promise.reject("NOT_RECORDING", "No recording in progress")
126
196
  return
127
197
  }
128
198
 
129
199
  stopProgressUpdates()
130
- mediaRecorder?.apply {
131
- stop()
132
- release()
200
+
201
+ try {
202
+ mediaRecorder?.apply {
203
+ if (isRecording) {
204
+ stop()
205
+ }
206
+ release()
207
+ }
208
+ } catch (e: Exception) {
209
+ // Try to release even if stop fails
210
+ try {
211
+ mediaRecorder?.release()
212
+ } catch (releaseError: Exception) {
213
+ // Ignore
214
+ }
133
215
  }
216
+
134
217
  mediaRecorder = null
135
218
  isRecording = false
136
219
  isPaused = false
137
220
 
221
+ // Validate file exists before returning
138
222
  val filePath = currentFilePath ?: ""
223
+ if (filePath.isNotEmpty()) {
224
+ try {
225
+ if (!File(filePath).exists()) {
226
+ promise.reject("FILE_NOT_FOUND", "Recording file was not created")
227
+ return
228
+ }
229
+ } catch (e: Exception) {
230
+ // Continue anyway, file might exist
231
+ }
232
+ }
233
+
139
234
  currentFilePath = null
140
235
  promise.resolve(filePath)
141
236
  } catch (e: Exception) {
142
237
  stopProgressUpdates()
143
- mediaRecorder?.release()
238
+ try {
239
+ mediaRecorder?.release()
240
+ } catch (releaseError: Exception) {
241
+ // Ignore
242
+ }
144
243
  mediaRecorder = null
145
244
  isRecording = false
146
- promise.reject("STOP_RECORDING_ERROR", e.message, e)
245
+ promise.reject("STOP_RECORDING_ERROR", e.message ?: "Failed to stop recording", e)
147
246
  }
148
247
  }
149
248
 
150
249
  override fun pauseRecording(promise: Promise) {
151
250
  try {
152
- if (!isRecording) {
251
+ if (!isRecording || mediaRecorder == null) {
153
252
  promise.reject("NOT_RECORDING", "No recording in progress")
154
253
  return
155
254
  }
156
255
 
256
+ if (isPaused) {
257
+ promise.reject("ALREADY_PAUSED", "Recording is already paused")
258
+ return
259
+ }
260
+
157
261
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
158
- mediaRecorder?.pause()
159
- isPaused = true
160
- pauseStartTime = System.currentTimeMillis()
161
- promise.resolve(null)
262
+ try {
263
+ mediaRecorder?.pause()
264
+ isPaused = true
265
+ pauseStartTime = System.currentTimeMillis()
266
+ promise.resolve(null)
267
+ } catch (e: Exception) {
268
+ promise.reject("PAUSE_ERROR", e.message ?: "Failed to pause recording", e)
269
+ }
162
270
  } else {
163
- promise.reject("NOT_SUPPORTED", "Pause is not supported on this API level")
271
+ promise.reject("PAUSE_NOT_SUPPORTED", "Pause is not supported on this Android version")
164
272
  }
165
273
  } catch (e: Exception) {
166
- promise.reject("PAUSE_ERROR", e.message, e)
274
+ promise.reject("PAUSE_ERROR", e.message ?: "Unknown error during pause", e)
167
275
  }
168
276
  }
169
277
 
170
278
  override fun resumeRecording(promise: Promise) {
171
279
  try {
172
- if (!isRecording || !isPaused) {
280
+ if (!isRecording || mediaRecorder == null) {
281
+ promise.reject("NOT_RECORDING", "No recording in progress")
282
+ return
283
+ }
284
+
285
+ if (!isPaused) {
173
286
  promise.reject("NOT_PAUSED", "Recording is not paused")
174
287
  return
175
288
  }
176
289
 
177
290
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
178
- mediaRecorder?.resume()
179
- isPaused = false
180
- pausedDuration += System.currentTimeMillis() - pauseStartTime
181
- promise.resolve(null)
291
+ try {
292
+ mediaRecorder?.resume()
293
+ isPaused = false
294
+ pausedDuration += System.currentTimeMillis() - pauseStartTime
295
+ promise.resolve(null)
296
+ } catch (e: Exception) {
297
+ promise.reject("RESUME_ERROR", e.message ?: "Failed to resume recording", e)
298
+ }
182
299
  } else {
183
- promise.reject("NOT_SUPPORTED", "Resume is not supported on this API level")
300
+ promise.reject("RESUME_NOT_SUPPORTED", "Resume is not supported on this Android version")
184
301
  }
185
302
  } catch (e: Exception) {
186
- promise.reject("RESUME_ERROR", e.message, e)
303
+ promise.reject("RESUME_ERROR", e.message ?: "Unknown error during resume", e)
187
304
  }
188
305
  }
189
306
 
@@ -191,46 +308,83 @@ class NosniaAudioRecorderModule(reactContext: ReactApplicationContext) :
191
308
  try {
192
309
  stopProgressUpdates()
193
310
 
194
- mediaRecorder?.apply {
311
+ // Safely stop recorder
312
+ try {
313
+ mediaRecorder?.apply {
314
+ if (isRecording || isPaused) {
315
+ stop()
316
+ }
317
+ release()
318
+ }
319
+ } catch (e: Exception) {
320
+ // Try to release even if stop fails
195
321
  try {
196
- stop()
197
- } catch (e: Exception) {
198
- // Might throw if already stopped
322
+ mediaRecorder?.release()
323
+ } catch (releaseError: Exception) {
324
+ // Ignore
199
325
  }
200
- release()
201
326
  }
327
+
202
328
  mediaRecorder = null
203
329
 
330
+ // Safely delete recording file
204
331
  currentFilePath?.let { filePath ->
205
- File(filePath).delete()
332
+ try {
333
+ val file = File(filePath)
334
+ if (file.exists()) {
335
+ file.delete()
336
+ }
337
+ } catch (e: Exception) {
338
+ // Log but don't fail - file cleanup is not critical
339
+ }
206
340
  }
341
+
207
342
  currentFilePath = null
208
343
  isRecording = false
209
344
  isPaused = false
345
+ pausedDuration = 0
346
+ pauseStartTime = 0
210
347
 
211
348
  promise.resolve(null)
212
349
  } catch (e: Exception) {
213
- promise.reject("CANCEL_ERROR", e.message, e)
350
+ mediaRecorder = null
351
+ isRecording = false
352
+ promise.reject("CANCEL_ERROR", e.message ?: "Failed to cancel recording", e)
214
353
  }
215
354
  }
216
355
 
217
356
  override fun getRecorderStatus(promise: Promise) {
218
357
  try {
219
358
  val currentTime = System.currentTimeMillis()
220
- val duration = if (isRecording) {
359
+
360
+ // Safely calculate duration
361
+ val duration = if (isRecording && mediaRecorder != null) {
221
362
  if (isPaused) {
222
- pauseStartTime - startTime - pausedDuration
363
+ // Paused: duration = pause time - start time - previous paused duration
364
+ val pausedDur = pauseStartTime - startTime - pausedDuration
365
+ if (pausedDur < 0) 0L else pausedDur
223
366
  } else {
224
- currentTime - startTime - pausedDuration
367
+ // Recording: duration = current time - start time - previous paused duration
368
+ val recordingDur = currentTime - startTime - pausedDuration
369
+ if (recordingDur < 0) 0L else recordingDur
225
370
  }
226
371
  } else {
227
- 0
372
+ 0L
228
373
  }
229
374
 
230
375
  val status = WritableNativeMap().apply {
231
376
  putBoolean("isRecording", isRecording)
232
377
  putDouble("duration", duration.toDouble())
233
- if (currentFilePath != null) {
378
+ if (!currentFilePath.isNullOrEmpty()) {
379
+ putString("currentFilePath", currentFilePath)
380
+ }
381
+ }
382
+
383
+ promise.resolve(status)
384
+ } catch (e: Exception) {
385
+ promise.reject("STATUS_ERROR", e.message ?: "Failed to get recorder status", e)
386
+ }
387
+ }
234
388
  putString("currentFilePath", currentFilePath)
235
389
  }
236
390
  }
@@ -97,60 +97,165 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
97
97
  resolve:(RCTPromiseResolveBlock)resolve
98
98
  reject:(RCTPromiseRejectBlock)reject {
99
99
  @try {
100
- if (_isRecording) {
100
+ // Safety check: options must not be nil
101
+ if (!options) {
102
+ reject(@"INVALID_OPTIONS", @"Options cannot be nil", nil);
103
+ return;
104
+ }
105
+
106
+ // Prevent concurrent recordings
107
+ if (_isRecording || _audioRecorder) {
101
108
  reject(@"ALREADY_RECORDING", @"Recording is already in progress", nil);
102
109
  return;
103
110
  }
104
111
 
112
+ // Validate and generate filename
105
113
  NSString *filename = options[@"filename"];
106
- if (!filename) {
114
+ if (!filename || ![filename isKindOfClass:[NSString class]] || filename.length == 0) {
107
115
  filename = [self generateFilename];
108
116
  }
109
117
 
118
+ // Get and validate recording directory
110
119
  NSString *recordingDir = [self getRecordingDirectory];
120
+ if (!recordingDir) {
121
+ reject(@"DIRECTORY_ERROR", @"Failed to get recording directory", nil);
122
+ return;
123
+ }
124
+
111
125
  NSString *filePath = [recordingDir stringByAppendingPathComponent:filename];
126
+ if (!filePath) {
127
+ reject(@"PATH_ERROR", @"Invalid file path", nil);
128
+ return;
129
+ }
130
+
131
+ // Clean up existing file to prevent conflicts
132
+ NSFileManager *fileManager = [NSFileManager defaultManager];
133
+ [fileManager removeItemAtPath:filePath error:nil];
134
+
112
135
  _recordingURL = [NSURL fileURLWithPath:filePath];
136
+ if (!_recordingURL) {
137
+ reject(@"URL_ERROR", @"Failed to create file URL", nil);
138
+ return;
139
+ }
140
+
141
+ // Validate and sanitize audio parameters
142
+ NSNumber *bitrate = options[@"bitrate"];
143
+ if (!bitrate || ![bitrate isKindOfClass:[NSNumber class]] || [bitrate integerValue] <= 0) {
144
+ bitrate = @(128000);
145
+ }
113
146
 
114
- NSNumber *bitrate = options[@"bitrate"] ?: @(128000);
115
- NSNumber *channels = options[@"channels"] ?: @(1);
116
- NSNumber *sampleRate = options[@"sampleRate"] ?: @(44100);
147
+ NSNumber *channels = options[@"channels"];
148
+ if (!channels || ![channels isKindOfClass:[NSNumber class]]) {
149
+ channels = @(1);
150
+ } else {
151
+ NSInteger chCount = [channels integerValue];
152
+ if (chCount <= 0 || chCount > 2) channels = @(1);
153
+ }
154
+
155
+ NSNumber *sampleRate = options[@"sampleRate"];
156
+ if (!sampleRate || ![sampleRate isKindOfClass:[NSNumber class]] || [sampleRate integerValue] <= 0) {
157
+ sampleRate = @(44100);
158
+ }
117
159
 
160
+ // Get audio session
118
161
  AVAudioSession *audioSession = [AVAudioSession sharedInstance];
119
- NSError *categoryError = nil;
120
- [audioSession setCategory:AVAudioSessionCategoryRecord
121
- error:&categoryError];
122
- if (categoryError) {
123
- reject(@"AUDIO_SESSION_ERROR", categoryError.description, categoryError);
162
+ if (!audioSession) {
163
+ reject(@"AUDIO_SESSION_ERROR", @"Failed to get audio session", nil);
124
164
  return;
125
165
  }
126
166
 
127
- NSDictionary *recordingSettings = @{
128
- AVFormatIDKey: @(kAudioFormatMPEG4AAC),
129
- AVSampleRateKey: sampleRate,
130
- AVNumberOfChannelsKey: channels,
131
- AVEncoderBitRateKey: bitrate,
132
- AVEncoderAudioQualityKey: @(AVAudioQualityMedium)
133
- };
167
+ // Set audio category with device compatibility options
168
+ NSError *categoryError = nil;
169
+ BOOL categorySuccess = [audioSession setCategory:AVAudioSessionCategoryRecord
170
+ withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker
171
+ error:&categoryError];
172
+ if (!categorySuccess || categoryError) {
173
+ NSString *msg = categoryError ? categoryError.description : @"Failed to set audio category";
174
+ reject(@"AUDIO_SESSION_ERROR", msg, categoryError);
175
+ return;
176
+ }
134
177
 
135
- NSError *recorderError = nil;
136
- _audioRecorder = [[AVAudioRecorder alloc] initWithURL:_recordingURL
137
- settings:recordingSettings
138
- error:&recorderError];
139
-
140
- if (recorderError) {
141
- reject(@"INIT_RECORDER_ERROR", recorderError.description, recorderError);
178
+ // Activate audio session
179
+ NSError *activateError = nil;
180
+ BOOL activateSuccess = [audioSession setActive:YES error:&activateError];
181
+ if (!activateSuccess || activateError) {
182
+ NSString *msg = activateError ? activateError.description : @"Failed to activate audio session";
183
+ reject(@"AUDIO_SESSION_ERROR", msg, activateError);
142
184
  return;
143
185
  }
144
186
 
145
- _audioRecorder.delegate = self;
146
- if (![_audioRecorder record]) {
147
- reject(@"START_RECORDING_ERROR", @"Failed to start recording", nil);
187
+ // Validate recording settings dictionary
188
+ if (!bitrate || !channels || !sampleRate) {
189
+ reject(@"INVALID_SETTINGS", @"Invalid recording settings", nil);
148
190
  return;
149
191
  }
150
192
 
151
- _isRecording = YES;
152
- [self startProgressTimer];
153
- resolve(nil);
193
+ // Create recording settings dictionary with safety checks
194
+ @try {
195
+ NSDictionary *recordingSettings = @{
196
+ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
197
+ AVSampleRateKey: sampleRate,
198
+ AVNumberOfChannelsKey: channels,
199
+ AVEncoderBitRateKey: bitrate,
200
+ AVEncoderAudioQualityKey: @(AVAudioQualityMedium)
201
+ };
202
+
203
+ // Validate settings dictionary created successfully
204
+ if (!recordingSettings || recordingSettings.count == 0) {
205
+ reject(@"SETTINGS_ERROR", @"Failed to create recording settings", nil);
206
+ return;
207
+ }
208
+
209
+ // Validate all required keys are present
210
+ if (!recordingSettings[AVFormatIDKey] || !recordingSettings[AVSampleRateKey] ||
211
+ !recordingSettings[AVNumberOfChannelsKey] || !recordingSettings[AVEncoderBitRateKey]) {
212
+ reject(@"SETTINGS_ERROR", @"Incomplete recording settings", nil);
213
+ return;
214
+ }
215
+
216
+ // Initialize recorder with error handling
217
+ NSError *recorderError = nil;
218
+ _audioRecorder = [[AVAudioRecorder alloc] initWithURL:_recordingURL
219
+ settings:recordingSettings
220
+ error:&recorderError];
221
+
222
+ if (recorderError) {
223
+ NSString *msg = recorderError.description ?: @"Unknown recorder initialization error";
224
+ reject(@"INIT_RECORDER_ERROR", msg, recorderError);
225
+ _audioRecorder = nil;
226
+ return;
227
+ }
228
+
229
+ // Validate recorder was created successfully
230
+ if (!_audioRecorder) {
231
+ reject(@"INIT_RECORDER_ERROR", @"AVAudioRecorder initialization returned nil", nil);
232
+ return;
233
+ }
234
+
235
+ // Set delegate and enable metering
236
+ _audioRecorder.delegate = self;
237
+ [_audioRecorder setMeteringEnabled:YES];
238
+
239
+ // Attempt to start recording
240
+ if (![_audioRecorder record]) {
241
+ NSString *errorMsg = @"Failed to start recording";
242
+ NSError *recErr = _audioRecorder.error;
243
+ if (recErr) {
244
+ errorMsg = [NSString stringWithFormat:@"%s (Error: %@)", errorMsg.UTF8String, recErr.description];
245
+ }
246
+ reject(@"START_RECORDING_ERROR", errorMsg, nil);
247
+ _audioRecorder = nil;
248
+ return;
249
+ }
250
+
251
+ _isRecording = YES;
252
+ [self startProgressTimer];
253
+ resolve(nil);
254
+ } @catch (NSException *innerException) {
255
+ reject(@"SETTINGS_ERROR", [NSString stringWithFormat:@"Exception creating settings: %@", innerException.reason], nil);
256
+ _audioRecorder = nil;
257
+ _isRecording = NO;
258
+ }
154
259
  } @catch (NSException *exception) {
155
260
  reject(@"START_RECORDING_ERROR", exception.reason, nil);
156
261
  }
@@ -164,14 +269,42 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
164
269
  return;
165
270
  }
166
271
 
272
+ // Stop timer and recorder
167
273
  [self stopProgressTimer];
168
- [_audioRecorder stop];
169
- NSString *filePath = [_recordingURL path];
274
+
275
+ // Safely stop the recorder
276
+ if (_audioRecorder && [_audioRecorder isRecording]) {
277
+ [_audioRecorder stop];
278
+ }
279
+
280
+ // Get file path before cleaning up recorder
281
+ NSString *filePath = nil;
282
+ if (_recordingURL) {
283
+ filePath = [_recordingURL path];
284
+ if (!filePath || filePath.length == 0) {
285
+ filePath = @"";
286
+ }
287
+ }
288
+
289
+ // Clean up recorder
290
+ _audioRecorder.delegate = nil;
170
291
  _audioRecorder = nil;
171
292
  _isRecording = NO;
293
+ _recordingURL = nil;
294
+
295
+ // Validate file exists before returning
296
+ if (filePath && filePath.length > 0) {
297
+ NSFileManager *fm = [NSFileManager defaultManager];
298
+ if ([fm fileExistsAtPath:filePath]) {
299
+ resolve(filePath);
300
+ return;
301
+ }
302
+ }
172
303
 
173
- resolve(filePath);
304
+ resolve(filePath ?: @"");
174
305
  } @catch (NSException *exception) {
306
+ _audioRecorder = nil;
307
+ _isRecording = NO;
175
308
  reject(@"STOP_RECORDING_ERROR", exception.reason, nil);
176
309
  }
177
310
  }
@@ -184,7 +317,17 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
184
317
  return;
185
318
  }
186
319
 
187
- [_audioRecorder pause];
320
+ // Check if recorder supports pause
321
+ if (![_audioRecorder respondsToSelector:@selector(pause)]) {
322
+ reject(@"PAUSE_NOT_SUPPORTED", @"Pause is not supported", nil);
323
+ return;
324
+ }
325
+
326
+ // Safely pause
327
+ if ([_audioRecorder isRecording]) {
328
+ [_audioRecorder pause];
329
+ }
330
+
188
331
  resolve(nil);
189
332
  } @catch (NSException *exception) {
190
333
  reject(@"PAUSE_ERROR", exception.reason, nil);
@@ -194,12 +337,29 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
194
337
  - (void)resumeRecording:(RCTPromiseResolveBlock)resolve
195
338
  reject:(RCTPromiseRejectBlock)reject {
196
339
  @try {
197
- if (!_isRecording || !_audioRecorder || [_audioRecorder isRecording]) {
340
+ if (!_isRecording || !_audioRecorder) {
341
+ reject(@"NOT_RECORDING", @"No recording in progress", nil);
342
+ return;
343
+ }
344
+
345
+ // Check if recorder is currently recording (not paused)
346
+ if ([_audioRecorder isRecording]) {
198
347
  reject(@"NOT_PAUSED", @"Recording is not paused", nil);
199
348
  return;
200
349
  }
201
350
 
202
- [_audioRecorder record];
351
+ // Check if recorder supports record method
352
+ if (![_audioRecorder respondsToSelector:@selector(record)]) {
353
+ reject(@"RECORD_NOT_SUPPORTED", @"Record is not supported", nil);
354
+ return;
355
+ }
356
+
357
+ // Resume recording
358
+ if (![_audioRecorder record]) {
359
+ reject(@"RESUME_ERROR", @"Failed to resume recording", nil);
360
+ return;
361
+ }
362
+
203
363
  resolve(nil);
204
364
  } @catch (NSException *exception) {
205
365
  reject(@"RESUME_ERROR", exception.reason, nil);
@@ -209,21 +369,41 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
209
369
  - (void)cancelRecording:(RCTPromiseResolveBlock)resolve
210
370
  reject:(RCTPromiseRejectBlock)reject {
211
371
  @try {
372
+ // Stop progress timer first
212
373
  [self stopProgressTimer];
213
374
 
375
+ // Safely stop recorder
214
376
  if (_audioRecorder) {
215
- [_audioRecorder stop];
377
+ if ([_audioRecorder isRecording] || [_audioRecorder isPaused]) {
378
+ [_audioRecorder stop];
379
+ }
380
+ _audioRecorder.delegate = nil;
216
381
  _audioRecorder = nil;
217
382
  }
218
383
 
384
+ // Clean up recording file
219
385
  if (_recordingURL) {
220
386
  NSFileManager *fileManager = [NSFileManager defaultManager];
221
- [fileManager removeItemAtURL:_recordingURL error:nil];
387
+ NSString *filePath = [_recordingURL path];
388
+ if (filePath && [fileManager fileExistsAtPath:filePath]) {
389
+ NSError *deleteError = nil;
390
+ [fileManager removeItemAtURL:_recordingURL error:&deleteError];
391
+ if (deleteError) {
392
+ // Log but don't fail - file cleanup is not critical
393
+ }
394
+ }
222
395
  _recordingURL = nil;
223
396
  }
224
397
 
225
398
  _isRecording = NO;
226
399
  resolve(nil);
400
+ } @catch (NSException *exception) {
401
+ _audioRecorder = nil;
402
+ _isRecording = NO;
403
+ reject(@"CANCEL_ERROR", exception.reason, nil);
404
+ }
405
+ }
406
+ resolve(nil);
227
407
  } @catch (NSException *exception) {
228
408
  reject(@"CANCEL_ERROR", exception.reason, nil);
229
409
  }
@@ -234,10 +414,23 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
234
414
  @try {
235
415
  NSMutableDictionary *status = [NSMutableDictionary dictionary];
236
416
  status[@"isRecording"] = @(_isRecording);
237
- status[@"duration"] = @(_audioRecorder.currentTime * 1000);
417
+
418
+ // Safely get duration
419
+ double duration = 0.0;
420
+ if (_audioRecorder) {
421
+ duration = _audioRecorder.currentTime;
422
+ if (duration < 0) duration = 0.0;
423
+ }
424
+ status[@"duration"] = @(duration * 1000);
425
+
426
+ // Safely get file path
238
427
  if (_recordingURL) {
239
- status[@"currentFilePath"] = [_recordingURL path];
428
+ NSString *filePath = [_recordingURL path];
429
+ if (filePath) {
430
+ status[@"currentFilePath"] = filePath;
431
+ }
240
432
  }
433
+
241
434
  resolve(status);
242
435
  } @catch (NSException *exception) {
243
436
  reject(@"STATUS_ERROR", exception.reason, nil);
@@ -272,14 +465,38 @@ RCT_EXPORT_MODULE(NosniaAudioRecorder)
272
465
 
273
466
  - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder
274
467
  successfully:(BOOL)flag {
275
- _isRecording = NO;
276
- [self stopProgressTimer];
468
+ @try {
469
+ if (!flag) {
470
+ // Recording failed - log but don't crash
471
+ }
472
+
473
+ _isRecording = NO;
474
+ [self stopProgressTimer];
475
+
476
+ // Clean up recorder reference
477
+ if (_audioRecorder == recorder) {
478
+ _audioRecorder = nil;
479
+ }
480
+ } @catch (NSException *exception) {
481
+ _audioRecorder = nil;
482
+ _isRecording = NO;
483
+ }
277
484
  }
278
485
 
279
486
  - (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder
280
487
  error:(NSError *)error {
281
- _isRecording = NO;
282
- [self stopProgressTimer];
488
+ @try {
489
+ _isRecording = NO;
490
+ [self stopProgressTimer];
491
+
492
+ // Clean up recorder reference
493
+ if (_audioRecorder == recorder) {
494
+ _audioRecorder = nil;
495
+ }
496
+ } @catch (NSException *exception) {
497
+ _audioRecorder = nil;
498
+ _isRecording = NO;
499
+ }
283
500
  }
284
501
 
285
502
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nosnia-audio-recorder",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "This is a modern audio recorder which actually works cross platform",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",