@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
|
-
|
|
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>
|
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|