@capgo/capacitor-audio-recorder 8.1.0 → 8.2.1

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.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @capgo/capacitor-audio-recorder
2
- <a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
2
+ <a href="https://capgo.app/"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-audio-recorder" alt="Capgo - Instant updates for Capacitor" /></a>
3
3
 
4
4
  <div align="center">
5
5
  <h2><a href="https://capgo.app/?ref=plugin_audio_recorder"> ➡️ Get Instant updates for your App with Capgo</a></h2>
@@ -43,6 +43,22 @@ npm install @capgo/capacitor-audio-recorder
43
43
  npx cap sync
44
44
  ```
45
45
 
46
+ ## Background recording
47
+
48
+ The plugin does not automatically put your app into a background-safe mode — you need to configure each platform so the process is allowed to keep running while the screen is locked.
49
+
50
+ - **iOS**: Enable the *Background Modes → Audio* capability in Xcode (or add `UIBackgroundModes` with an `audio` entry in `Info.plist`). With that flag enabled, `startRecording` will continue while the device is locked because the plugin already uses an `AVAudioSession` category that supports background capture.
51
+ - **Android**: Recording continues as long as the app process stays alive. For hour-long sessions you should move the recording work into a foreground service with an ongoing notification to prevent the OS from stopping the process. Add the required permissions, e.g.:
52
+
53
+ ```xml
54
+ <!-- android/app/src/main/AndroidManifest.xml -->
55
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
56
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
57
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
58
+ ```
59
+
60
+ Then start a foreground service before calling `startRecording` (or trigger the recording inside that service). The plugin itself does not create the service for you, so you can use your preferred foreground-service implementation or a background-task helper plugin to start/stop it alongside the recording UI.
61
+
46
62
  ## API
47
63
 
48
64
  <docgen-index>
@@ -24,7 +24,7 @@ import java.util.Locale;
24
24
  )
25
25
  public class CapacitorAudioRecorderPlugin extends com.getcapacitor.Plugin {
26
26
 
27
- private final String pluginVersion = "8.1.0";
27
+ private final String pluginVersion = "8.2.1";
28
28
 
29
29
  private enum RecordingStatus {
30
30
  INACTIVE,
@@ -4,7 +4,7 @@ import Foundation
4
4
 
5
5
  @objc(CapacitorAudioRecorderPlugin)
6
6
  public class CapacitorAudioRecorderPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioRecorderDelegate {
7
- private let pluginVersion: String = "8.1.0"
7
+ private let pluginVersion: String = "8.2.1"
8
8
  public let identifier = "CapacitorAudioRecorderPlugin"
9
9
  public let jsName = "CapacitorAudioRecorder"
10
10
  public let pluginMethods: [CAPPluginMethod] = [
@@ -35,6 +35,10 @@ public class CapacitorAudioRecorderPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioR
35
35
  private var pauseStartDate: Date?
36
36
  private var accumulatedPauseDuration: TimeInterval = 0
37
37
  private var shouldEmitStoppedEvent = true
38
+ // AVAudioSession interruption observer. Active for the lifetime of a
39
+ // recording so phone calls / Siri / alarms can pause cleanly without
40
+ // losing the partially-recorded file.
41
+ private var interruptionObserver: NSObjectProtocol?
38
42
 
39
43
  // MARK: - Plugin methods
40
44
 
@@ -80,16 +84,52 @@ public class CapacitorAudioRecorderPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioR
80
84
  return
81
85
  }
82
86
 
87
+ // Re-activate the AVAudioSession before calling record(). After an
88
+ // iOS interruption (Siri / call / alarm), the session is deactivated
89
+ // and won't auto-reactivate when the user taps resume. Without this,
90
+ // record() silently returns false, no audio is captured, and the
91
+ // recorder eventually fires audioRecorderDidFinishRecording(false)
92
+ // which destroys the file via resetRecorder(deleteFile: true).
93
+ do {
94
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
95
+ } catch {
96
+ CAPLog.print("CapacitorAudioRecorderPlugin", "Failed to reactivate audio session on resume: \(error.localizedDescription)")
97
+ call.reject("Failed to reactivate audio session.", nil, error)
98
+ return
99
+ }
100
+
83
101
  if let pauseStartDate {
84
102
  accumulatedPauseDuration += Date().timeIntervalSince(pauseStartDate)
85
103
  }
86
- recorder.record()
104
+ let didStart = recorder.record()
105
+ if !didStart {
106
+ CAPLog.print("CapacitorAudioRecorderPlugin", "AVAudioRecorder.record() returned false on resume")
107
+ call.reject("Failed to resume recording.")
108
+ return
109
+ }
87
110
  status = .recording
88
111
  pauseStartDate = nil
89
112
  call.resolve()
90
113
  }
91
114
 
92
115
  @objc func stopRecording(_ call: CAPPluginCall) {
116
+ // Recovery path: an interruption-driven recorder failure may have
117
+ // already torn down the AVAudioRecorder via the delegate callback,
118
+ // but the partial file is preserved on disk. Return that file so
119
+ // the caller can finalize what was captured before the failure
120
+ // instead of getting "no active recording" + losing everything.
121
+ if audioRecorder == nil, let url = currentFileURL {
122
+ let result: [String: Any] = [
123
+ "duration": 0,
124
+ "uri": url.absoluteString
125
+ ]
126
+ currentFileURL = nil
127
+ unregisterInterruptionObserver()
128
+ notifyListeners("recordingStopped", data: result)
129
+ call.resolve(result)
130
+ return
131
+ }
132
+
93
133
  guard let recorder = audioRecorder, status != .inactive else {
94
134
  call.reject("No active recording to stop.")
95
135
  return
@@ -177,10 +217,27 @@ public class CapacitorAudioRecorderPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioR
177
217
  "uri": uri
178
218
  ]
179
219
  notifyListeners("recordingStopped", data: result)
220
+ resetRecorder(deleteFile: false)
180
221
  } else {
181
- notifyListeners("recordingError", data: ["message": "Recording finished unsuccessfully."])
222
+ // Recording terminated unexpectedly (most often: an audio session
223
+ // interruption + a record() call that didn't actually resume).
224
+ // Preserve the partial file on disk and KEEP currentFileURL alive
225
+ // so a subsequent stopRecording call can recover what was captured
226
+ // before the failure. Reset everything else so the plugin's state
227
+ // doesn't lie about an active recording.
228
+ let uri = currentFileURL?.absoluteString ?? ""
229
+ notifyListeners("recordingError", data: [
230
+ "message": "Recording finished unsuccessfully.",
231
+ "uri": uri
232
+ ])
233
+ unregisterInterruptionObserver()
234
+ self.audioRecorder = nil
235
+ // intentionally NOT clearing currentFileURL — it's the recovery breadcrumb
236
+ status = .inactive
237
+ recordingStartDate = nil
238
+ pauseStartDate = nil
239
+ accumulatedPauseDuration = 0
182
240
  }
183
- resetRecorder(deleteFile: !flag)
184
241
  }
185
242
 
186
243
  // MARK: - Helpers
@@ -234,9 +291,87 @@ public class CapacitorAudioRecorderPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioR
234
291
  accumulatedPauseDuration = 0
235
292
  pauseStartDate = nil
236
293
  shouldEmitStoppedEvent = true
294
+
295
+ registerInterruptionObserver()
296
+ }
297
+
298
+ private func registerInterruptionObserver() {
299
+ unregisterInterruptionObserver()
300
+ interruptionObserver = NotificationCenter.default.addObserver(
301
+ forName: AVAudioSession.interruptionNotification,
302
+ object: audioSession,
303
+ queue: .main
304
+ ) { [weak self] notification in
305
+ self?.handleInterruption(notification: notification)
306
+ }
307
+ }
308
+
309
+ private func unregisterInterruptionObserver() {
310
+ if let observer = interruptionObserver {
311
+ NotificationCenter.default.removeObserver(observer)
312
+ interruptionObserver = nil
313
+ }
314
+ }
315
+
316
+ private func handleInterruption(notification: Notification) {
317
+ guard let userInfo = notification.userInfo,
318
+ let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
319
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
320
+ return
321
+ }
322
+
323
+ switch type {
324
+ case .began:
325
+ // iOS is interrupting (call, Siri, alarm, Bluetooth disconnect).
326
+ // Pause the recorder synchronously inside the notification handler
327
+ // so AVAudioRecorder finalizes pending writes before the session
328
+ // is deactivated. Without this, the recorder hits
329
+ // audioRecorderDidFinishRecording(success: false) and the file
330
+ // is destroyed.
331
+ guard let recorder = audioRecorder, status == .recording else { return }
332
+ recorder.pause()
333
+ pauseStartDate = Date()
334
+ status = .paused
335
+ notifyListeners("recordingInterruptionBegan", data: [:])
336
+
337
+ case .ended:
338
+ // Interruption ended. iOS hints whether we should resume via the
339
+ // shouldResume option. If yes, re-activate the session and record;
340
+ // if no, stay paused and let the user resume manually.
341
+ guard let recorder = audioRecorder, status == .paused else {
342
+ notifyListeners("recordingInterruptionEnded", data: ["shouldResume": false])
343
+ return
344
+ }
345
+ let shouldResume: Bool = {
346
+ guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
347
+ return false
348
+ }
349
+ let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
350
+ return options.contains(.shouldResume)
351
+ }()
352
+
353
+ if shouldResume {
354
+ do {
355
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
356
+ if let pauseStart = pauseStartDate {
357
+ accumulatedPauseDuration += Date().timeIntervalSince(pauseStart)
358
+ }
359
+ recorder.record()
360
+ status = .recording
361
+ pauseStartDate = nil
362
+ } catch {
363
+ CAPLog.print("CapacitorAudioRecorderPlugin", "Failed to resume after interruption: \(error.localizedDescription)")
364
+ }
365
+ }
366
+ notifyListeners("recordingInterruptionEnded", data: ["shouldResume": shouldResume])
367
+
368
+ @unknown default:
369
+ return
370
+ }
237
371
  }
238
372
 
239
373
  private func resetRecorder(deleteFile: Bool) {
374
+ unregisterInterruptionObserver()
240
375
  if deleteFile, let url = currentFileURL {
241
376
  try? FileManager.default.removeItem(at: url)
242
377
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-audio-recorder",
3
- "version": "8.1.0",
3
+ "version": "8.2.1",
4
4
  "description": "Record audio on iOS, Android, and Web with Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",