@independo/capacitor-voice-recorder 8.0.2-dev.1 → 8.1.0-dev.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 +92 -4
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +1 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CustomMediaRecorder.java +106 -5
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +2 -0
- package/dist/docs.json +145 -5
- package/dist/esm/definitions.d.ts +36 -3
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/VoiceRecorder/CurrentRecordingStatus.swift +1 -0
- package/ios/Sources/VoiceRecorder/CustomMediaRecorder.swift +242 -20
- package/ios/Sources/VoiceRecorder/Messages.swift +1 -0
- package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +44 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,9 @@ Below is an index of all available methods. Run `npm run docgen` after updating
|
|
|
72
72
|
* [`pauseRecording()`](#pauserecording)
|
|
73
73
|
* [`resumeRecording()`](#resumerecording)
|
|
74
74
|
* [`getCurrentStatus()`](#getcurrentstatus)
|
|
75
|
+
* [`addListener('voiceRecordingInterrupted', ...)`](#addlistenervoicerecordinginterrupted-)
|
|
76
|
+
* [`addListener('voiceRecordingInterruptionEnded', ...)`](#addlistenervoicerecordinginterruptionended-)
|
|
77
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
75
78
|
* [Interfaces](#interfaces)
|
|
76
79
|
* [Type Aliases](#type-aliases)
|
|
77
80
|
* [Enums](#enums)
|
|
@@ -162,6 +165,7 @@ Will stop the recording that has been previously started.
|
|
|
162
165
|
If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.
|
|
163
166
|
If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.
|
|
164
167
|
In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.
|
|
168
|
+
On iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.
|
|
165
169
|
In case of success, the promise resolves to <a href="#recordingdata">RecordingData</a> containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.
|
|
166
170
|
|
|
167
171
|
**Returns:** <code>Promise<<a href="#recordingdata">RecordingData</a>></code>
|
|
@@ -191,7 +195,7 @@ On certain mobile OS versions, this function is not supported and will reject wi
|
|
|
191
195
|
resumeRecording() => Promise<GenericResponse>
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
Resumes a paused audio recording.
|
|
198
|
+
Resumes a paused or interrupted audio recording.
|
|
195
199
|
If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.
|
|
196
200
|
On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.
|
|
197
201
|
On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.
|
|
@@ -212,12 +216,62 @@ Will resolve with one of the following values:
|
|
|
212
216
|
`{ status: "NONE" }` if the plugin is idle and waiting to start a new recording.
|
|
213
217
|
`{ status: "RECORDING" }` if the plugin is in the middle of recording.
|
|
214
218
|
`{ status: "PAUSED" }` if the recording is paused.
|
|
219
|
+
`{ status: "INTERRUPTED" }` if the recording was paused due to a system interruption.
|
|
215
220
|
|
|
216
221
|
**Returns:** <code>Promise<<a href="#currentrecordingstatus">CurrentRecordingStatus</a>></code>
|
|
217
222
|
|
|
218
223
|
--------------------
|
|
219
224
|
|
|
220
225
|
|
|
226
|
+
### addListener('voiceRecordingInterrupted', ...)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
addListener(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void) => Promise<PluginListenerHandle>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).
|
|
233
|
+
Available on iOS and Android only.
|
|
234
|
+
|
|
235
|
+
| Param | Type | Description |
|
|
236
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
237
|
+
| **`eventName`** | <code>'voiceRecordingInterrupted'</code> | The name of the event to listen for. |
|
|
238
|
+
| **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptedevent">VoiceRecordingInterruptedEvent</a>) => void</code> | The callback function to invoke when the event occurs. |
|
|
239
|
+
|
|
240
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
241
|
+
|
|
242
|
+
--------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
### addListener('voiceRecordingInterruptionEnded', ...)
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
addListener(eventName: 'voiceRecordingInterruptionEnded', listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void) => Promise<PluginListenerHandle>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Listen for audio recording interruption end events.
|
|
252
|
+
Available on iOS and Android only.
|
|
253
|
+
|
|
254
|
+
| Param | Type | Description |
|
|
255
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
256
|
+
| **`eventName`** | <code>'voiceRecordingInterruptionEnded'</code> | The name of the event to listen for. |
|
|
257
|
+
| **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptionendedevent">VoiceRecordingInterruptionEndedEvent</a>) => void</code> | The callback function to invoke when the event occurs. |
|
|
258
|
+
|
|
259
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
260
|
+
|
|
261
|
+
--------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
### removeAllListeners()
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
removeAllListeners() => Promise<void>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Remove all listeners for this plugin.
|
|
271
|
+
|
|
272
|
+
--------------------
|
|
273
|
+
|
|
274
|
+
|
|
221
275
|
### Interfaces
|
|
222
276
|
|
|
223
277
|
|
|
@@ -253,9 +307,16 @@ Interface representing the data of a recording.
|
|
|
253
307
|
|
|
254
308
|
Interface representing the current status of the voice recorder.
|
|
255
309
|
|
|
256
|
-
| Prop | Type
|
|
257
|
-
| ------------ |
|
|
258
|
-
| **`status`** | <code>'NONE' \| 'RECORDING' \| 'PAUSED'</code> | The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'. |
|
|
310
|
+
| Prop | Type | Description |
|
|
311
|
+
| ------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
312
|
+
| **`status`** | <code>'NONE' \| 'RECORDING' \| 'PAUSED' \| 'INTERRUPTED'</code> | The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'INTERRUPTED', 'NONE'. |
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
#### PluginListenerHandle
|
|
316
|
+
|
|
317
|
+
| Prop | Type |
|
|
318
|
+
| ------------ | ----------------------------------------- |
|
|
319
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
259
320
|
|
|
260
321
|
|
|
261
322
|
### Type Aliases
|
|
@@ -268,6 +329,27 @@ Represents a Base64 encoded string.
|
|
|
268
329
|
<code>string</code>
|
|
269
330
|
|
|
270
331
|
|
|
332
|
+
#### VoiceRecordingInterruptedEvent
|
|
333
|
+
|
|
334
|
+
Event payload for voiceRecordingInterrupted event (empty - no data).
|
|
335
|
+
|
|
336
|
+
<code><a href="#record">Record</a><string, never></code>
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
#### Record
|
|
340
|
+
|
|
341
|
+
Construct a type with a set of properties K of type T
|
|
342
|
+
|
|
343
|
+
<code>{
|
|
271
344
|
[P in K]: T;
|
|
272
345
|
}</code>
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
#### VoiceRecordingInterruptionEndedEvent
|
|
349
|
+
|
|
350
|
+
Event payload for voiceRecordingInterruptionEnded event (empty - no data).
|
|
351
|
+
|
|
352
|
+
<code><a href="#record">Record</a><string, never></code>
|
|
353
|
+
|
|
354
|
+
|
|
273
355
|
### Enums
|
|
274
356
|
|
|
275
357
|
|
|
@@ -287,6 +369,12 @@ Represents a Base64 encoded string.
|
|
|
287
369
|
|
|
288
370
|
</docgen-api>
|
|
289
371
|
|
|
372
|
+
## Audio interruption handling
|
|
373
|
+
|
|
374
|
+
On iOS and Android, the plugin listens for system audio interruptions (phone calls, other apps taking audio focus). When an interruption begins, the recording is paused, the status becomes `INTERRUPTED`, and the `voiceRecordingInterrupted` event fires. When the interruption ends, the `voiceRecordingInterruptionEnded` event fires, and the status stays `INTERRUPTED` until you call `resumeRecording()` or `stopRecording()`. Web does not provide interruption handling.
|
|
375
|
+
|
|
376
|
+
If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type `audio/mp4`. Recordings without interruptions remain AAC with MIME type `audio/aac`.
|
|
377
|
+
|
|
290
378
|
|
|
291
379
|
## Format and Mime type
|
|
292
380
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
package com.tchvu3.capacitorvoicerecorder;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.media.AudioAttributes;
|
|
5
|
+
import android.media.AudioFocusRequest;
|
|
6
|
+
import android.media.AudioManager;
|
|
4
7
|
import android.media.MediaRecorder;
|
|
5
8
|
import android.os.Build;
|
|
6
9
|
import android.os.Environment;
|
|
@@ -9,20 +12,33 @@ import java.io.IOException;
|
|
|
9
12
|
import java.util.regex.Matcher;
|
|
10
13
|
import java.util.regex.Pattern;
|
|
11
14
|
|
|
12
|
-
public class CustomMediaRecorder {
|
|
15
|
+
public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListener {
|
|
13
16
|
|
|
14
17
|
private final Context context;
|
|
15
18
|
private final RecordOptions options;
|
|
16
19
|
private MediaRecorder mediaRecorder;
|
|
17
20
|
private File outputFile;
|
|
18
21
|
private CurrentRecordingStatus currentRecordingStatus = CurrentRecordingStatus.NONE;
|
|
22
|
+
private AudioManager audioManager;
|
|
23
|
+
private AudioFocusRequest audioFocusRequest;
|
|
24
|
+
private Runnable onInterruptionBegan;
|
|
25
|
+
private Runnable onInterruptionEnded;
|
|
19
26
|
|
|
20
27
|
public CustomMediaRecorder(Context context, RecordOptions options) throws IOException {
|
|
21
28
|
this.context = context;
|
|
22
29
|
this.options = options;
|
|
30
|
+
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
|
23
31
|
generateMediaRecorder();
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
public void setOnInterruptionBegan(Runnable callback) {
|
|
35
|
+
this.onInterruptionBegan = callback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public void setOnInterruptionEnded(Runnable callback) {
|
|
39
|
+
this.onInterruptionEnded = callback;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
private void generateMediaRecorder() throws IOException {
|
|
27
43
|
mediaRecorder = new MediaRecorder();
|
|
28
44
|
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
|
@@ -75,14 +91,31 @@ public class CustomMediaRecorder {
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
public void startRecording() {
|
|
94
|
+
requestAudioFocus();
|
|
78
95
|
mediaRecorder.start();
|
|
79
96
|
currentRecordingStatus = CurrentRecordingStatus.RECORDING;
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
public void stopRecording() {
|
|
83
|
-
mediaRecorder
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
if (mediaRecorder == null) {
|
|
101
|
+
abandonAudioFocus();
|
|
102
|
+
currentRecordingStatus = CurrentRecordingStatus.NONE;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (currentRecordingStatus == CurrentRecordingStatus.RECORDING
|
|
108
|
+
|| currentRecordingStatus == CurrentRecordingStatus.PAUSED
|
|
109
|
+
|| currentRecordingStatus == CurrentRecordingStatus.INTERRUPTED) {
|
|
110
|
+
mediaRecorder.stop();
|
|
111
|
+
}
|
|
112
|
+
} catch (IllegalStateException ignore) {
|
|
113
|
+
} finally {
|
|
114
|
+
mediaRecorder.release();
|
|
115
|
+
mediaRecorder = null;
|
|
116
|
+
abandonAudioFocus();
|
|
117
|
+
currentRecordingStatus = CurrentRecordingStatus.NONE;
|
|
118
|
+
}
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
public File getOutputFile() {
|
|
@@ -112,7 +145,8 @@ public class CustomMediaRecorder {
|
|
|
112
145
|
throw new NotSupportedOsVersion();
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
if (currentRecordingStatus == CurrentRecordingStatus.PAUSED) {
|
|
148
|
+
if (currentRecordingStatus == CurrentRecordingStatus.PAUSED || currentRecordingStatus == CurrentRecordingStatus.INTERRUPTED) {
|
|
149
|
+
requestAudioFocus();
|
|
116
150
|
mediaRecorder.resume();
|
|
117
151
|
currentRecordingStatus = CurrentRecordingStatus.RECORDING;
|
|
118
152
|
return true;
|
|
@@ -146,4 +180,71 @@ public class CustomMediaRecorder {
|
|
|
146
180
|
if (tempMediaRecorder != null) tempMediaRecorder.deleteOutputFile();
|
|
147
181
|
}
|
|
148
182
|
}
|
|
183
|
+
|
|
184
|
+
private void requestAudioFocus() {
|
|
185
|
+
if (audioManager == null) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
190
|
+
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
|
191
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
192
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
193
|
+
.build();
|
|
194
|
+
|
|
195
|
+
audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
|
196
|
+
.setAudioAttributes(audioAttributes)
|
|
197
|
+
.setOnAudioFocusChangeListener(this)
|
|
198
|
+
.build();
|
|
199
|
+
|
|
200
|
+
audioManager.requestAudioFocus(audioFocusRequest);
|
|
201
|
+
} else {
|
|
202
|
+
audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private void abandonAudioFocus() {
|
|
207
|
+
if (audioManager == null) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && audioFocusRequest != null) {
|
|
212
|
+
audioManager.abandonAudioFocusRequest(audioFocusRequest);
|
|
213
|
+
audioFocusRequest = null;
|
|
214
|
+
} else {
|
|
215
|
+
audioManager.abandonAudioFocus(this);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Override
|
|
220
|
+
public void onAudioFocusChange(int focusChange) {
|
|
221
|
+
switch (focusChange) {
|
|
222
|
+
case AudioManager.AUDIOFOCUS_LOSS:
|
|
223
|
+
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
|
224
|
+
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
|
225
|
+
// For voice recording, ducking still degrades captured audio, so treat all loss types as interruptions.
|
|
226
|
+
if (currentRecordingStatus == CurrentRecordingStatus.RECORDING) {
|
|
227
|
+
try {
|
|
228
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
229
|
+
mediaRecorder.pause();
|
|
230
|
+
currentRecordingStatus = CurrentRecordingStatus.INTERRUPTED;
|
|
231
|
+
if (onInterruptionBegan != null) {
|
|
232
|
+
onInterruptionBegan.run();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (Exception ignore) {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case AudioManager.AUDIOFOCUS_GAIN:
|
|
240
|
+
if (currentRecordingStatus == CurrentRecordingStatus.INTERRUPTED) {
|
|
241
|
+
if (onInterruptionEnded != null) {
|
|
242
|
+
onInterruptionEnded.run();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
149
250
|
}
|
|
@@ -82,6 +82,8 @@ public class VoiceRecorder extends Plugin {
|
|
|
82
82
|
String subDirectory = call.getString("subDirectory");
|
|
83
83
|
RecordOptions options = new RecordOptions(directory, subDirectory);
|
|
84
84
|
mediaRecorder = new CustomMediaRecorder(getContext(), options);
|
|
85
|
+
mediaRecorder.setOnInterruptionBegan(() -> notifyListeners("voiceRecordingInterrupted", null));
|
|
86
|
+
mediaRecorder.setOnInterruptionEnded(() -> notifyListeners("voiceRecordingInterruptionEnded", null));
|
|
85
87
|
mediaRecorder.startRecording();
|
|
86
88
|
call.resolve(ResponseGenerator.successResponse());
|
|
87
89
|
} catch (Exception exp) {
|
package/dist/docs.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"text": "Error with one of the specified error codes if the recording cannot be stopped."
|
|
116
116
|
}
|
|
117
117
|
],
|
|
118
|
-
"docs": "Stops audio recording.\nWill stop the recording that has been previously started.\nIf the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.\nIf the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.\nIn a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.\nIn case of success, the promise resolves to RecordingData containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.",
|
|
118
|
+
"docs": "Stops audio recording.\nWill stop the recording that has been previously started.\nIf the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.\nIf the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.\nIn a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.\nOn iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.\nIn case of success, the promise resolves to RecordingData containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.",
|
|
119
119
|
"complexTypes": [
|
|
120
120
|
"RecordingData"
|
|
121
121
|
],
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"text": "Error with one of the specified error codes if the recording cannot be resumed."
|
|
158
158
|
}
|
|
159
159
|
],
|
|
160
|
-
"docs": "Resumes a paused audio recording.\nIf the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\nOn success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.\nOn certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.",
|
|
160
|
+
"docs": "Resumes a paused or interrupted audio recording.\nIf the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\nOn success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.\nOn certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.",
|
|
161
161
|
"complexTypes": [
|
|
162
162
|
"GenericResponse"
|
|
163
163
|
],
|
|
@@ -178,11 +178,95 @@
|
|
|
178
178
|
"text": "Error if the status cannot be fetched."
|
|
179
179
|
}
|
|
180
180
|
],
|
|
181
|
-
"docs": "Gets the current status of the voice recorder.\nWill resolve with one of the following values:\n`{ status: \"NONE\" }` if the plugin is idle and waiting to start a new recording.\n`{ status: \"RECORDING\" }` if the plugin is in the middle of recording.\n`{ status: \"PAUSED\" }` if the recording is paused.",
|
|
181
|
+
"docs": "Gets the current status of the voice recorder.\nWill resolve with one of the following values:\n`{ status: \"NONE\" }` if the plugin is idle and waiting to start a new recording.\n`{ status: \"RECORDING\" }` if the plugin is in the middle of recording.\n`{ status: \"PAUSED\" }` if the recording is paused.\n`{ status: \"INTERRUPTED\" }` if the recording was paused due to a system interruption.",
|
|
182
182
|
"complexTypes": [
|
|
183
183
|
"CurrentRecordingStatus"
|
|
184
184
|
],
|
|
185
185
|
"slug": "getcurrentstatus"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "addListener",
|
|
189
|
+
"signature": "(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void) => Promise<PluginListenerHandle>",
|
|
190
|
+
"parameters": [
|
|
191
|
+
{
|
|
192
|
+
"name": "eventName",
|
|
193
|
+
"docs": "The name of the event to listen for.",
|
|
194
|
+
"type": "'voiceRecordingInterrupted'"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "listenerFunc",
|
|
198
|
+
"docs": "The callback function to invoke when the event occurs.",
|
|
199
|
+
"type": "(event: VoiceRecordingInterruptedEvent) => void"
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
203
|
+
"tags": [
|
|
204
|
+
{
|
|
205
|
+
"name": "param",
|
|
206
|
+
"text": "eventName The name of the event to listen for."
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "param",
|
|
210
|
+
"text": "listenerFunc The callback function to invoke when the event occurs."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"name": "returns",
|
|
214
|
+
"text": "A promise that resolves to a PluginListenerHandle."
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
"docs": "Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).\nAvailable on iOS and Android only.",
|
|
218
|
+
"complexTypes": [
|
|
219
|
+
"PluginListenerHandle",
|
|
220
|
+
"VoiceRecordingInterruptedEvent"
|
|
221
|
+
],
|
|
222
|
+
"slug": "addlistenervoicerecordinginterrupted-"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "addListener",
|
|
226
|
+
"signature": "(eventName: 'voiceRecordingInterruptionEnded', listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void) => Promise<PluginListenerHandle>",
|
|
227
|
+
"parameters": [
|
|
228
|
+
{
|
|
229
|
+
"name": "eventName",
|
|
230
|
+
"docs": "The name of the event to listen for.",
|
|
231
|
+
"type": "'voiceRecordingInterruptionEnded'"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "listenerFunc",
|
|
235
|
+
"docs": "The callback function to invoke when the event occurs.",
|
|
236
|
+
"type": "(event: VoiceRecordingInterruptionEndedEvent) => void"
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
240
|
+
"tags": [
|
|
241
|
+
{
|
|
242
|
+
"name": "param",
|
|
243
|
+
"text": "eventName The name of the event to listen for."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "param",
|
|
247
|
+
"text": "listenerFunc The callback function to invoke when the event occurs."
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "returns",
|
|
251
|
+
"text": "A promise that resolves to a PluginListenerHandle."
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
"docs": "Listen for audio recording interruption end events.\nAvailable on iOS and Android only.",
|
|
255
|
+
"complexTypes": [
|
|
256
|
+
"PluginListenerHandle",
|
|
257
|
+
"VoiceRecordingInterruptionEndedEvent"
|
|
258
|
+
],
|
|
259
|
+
"slug": "addlistenervoicerecordinginterruptionended-"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"name": "removeAllListeners",
|
|
263
|
+
"signature": "() => Promise<void>",
|
|
264
|
+
"parameters": [],
|
|
265
|
+
"returns": "Promise<void>",
|
|
266
|
+
"tags": [],
|
|
267
|
+
"docs": "Remove all listeners for this plugin.",
|
|
268
|
+
"complexTypes": [],
|
|
269
|
+
"slug": "removealllisteners"
|
|
186
270
|
}
|
|
187
271
|
],
|
|
188
272
|
"properties": []
|
|
@@ -262,9 +346,25 @@
|
|
|
262
346
|
{
|
|
263
347
|
"name": "status",
|
|
264
348
|
"tags": [],
|
|
265
|
-
"docs": "The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'.",
|
|
349
|
+
"docs": "The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'INTERRUPTED', 'NONE'.",
|
|
266
350
|
"complexTypes": [],
|
|
267
|
-
"type": "'NONE' | 'RECORDING' | 'PAUSED'"
|
|
351
|
+
"type": "'NONE' | 'RECORDING' | 'PAUSED' | 'INTERRUPTED'"
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "PluginListenerHandle",
|
|
357
|
+
"slug": "pluginlistenerhandle",
|
|
358
|
+
"docs": "",
|
|
359
|
+
"tags": [],
|
|
360
|
+
"methods": [],
|
|
361
|
+
"properties": [
|
|
362
|
+
{
|
|
363
|
+
"name": "remove",
|
|
364
|
+
"tags": [],
|
|
365
|
+
"docs": "",
|
|
366
|
+
"complexTypes": [],
|
|
367
|
+
"type": "() => Promise<void>"
|
|
268
368
|
}
|
|
269
369
|
]
|
|
270
370
|
}
|
|
@@ -387,6 +487,46 @@
|
|
|
387
487
|
"complexTypes": []
|
|
388
488
|
}
|
|
389
489
|
]
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"name": "VoiceRecordingInterruptedEvent",
|
|
493
|
+
"slug": "voicerecordinginterruptedevent",
|
|
494
|
+
"docs": "Event payload for voiceRecordingInterrupted event (empty - no data).",
|
|
495
|
+
"types": [
|
|
496
|
+
{
|
|
497
|
+
"text": "Record<string, never>",
|
|
498
|
+
"complexTypes": [
|
|
499
|
+
"Record"
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"name": "Record",
|
|
506
|
+
"slug": "record",
|
|
507
|
+
"docs": "Construct a type with a set of properties K of type T",
|
|
508
|
+
"types": [
|
|
509
|
+
{
|
|
510
|
+
"text": "{\r\n [P in K]: T;\r\n}",
|
|
511
|
+
"complexTypes": [
|
|
512
|
+
"K",
|
|
513
|
+
"T"
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "VoiceRecordingInterruptionEndedEvent",
|
|
520
|
+
"slug": "voicerecordinginterruptionendedevent",
|
|
521
|
+
"docs": "Event payload for voiceRecordingInterruptionEnded event (empty - no data).",
|
|
522
|
+
"types": [
|
|
523
|
+
{
|
|
524
|
+
"text": "Record<string, never>",
|
|
525
|
+
"complexTypes": [
|
|
526
|
+
"Record"
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
]
|
|
390
530
|
}
|
|
391
531
|
],
|
|
392
532
|
"pluginConfigs": []
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from "@capacitor/core";
|
|
1
2
|
import type { Directory } from "@capacitor/filesystem";
|
|
2
3
|
/**
|
|
3
4
|
* Represents a Base64 encoded string.
|
|
@@ -59,10 +60,18 @@ export interface GenericResponse {
|
|
|
59
60
|
*/
|
|
60
61
|
export interface CurrentRecordingStatus {
|
|
61
62
|
/**
|
|
62
|
-
* The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'.
|
|
63
|
+
* The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'INTERRUPTED', 'NONE'.
|
|
63
64
|
*/
|
|
64
|
-
status: 'RECORDING' | 'PAUSED' | 'NONE';
|
|
65
|
+
status: 'RECORDING' | 'PAUSED' | 'INTERRUPTED' | 'NONE';
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Event payload for voiceRecordingInterrupted event (empty - no data).
|
|
69
|
+
*/
|
|
70
|
+
export type VoiceRecordingInterruptedEvent = Record<string, never>;
|
|
71
|
+
/**
|
|
72
|
+
* Event payload for voiceRecordingInterruptionEnded event (empty - no data).
|
|
73
|
+
*/
|
|
74
|
+
export type VoiceRecordingInterruptionEndedEvent = Record<string, never>;
|
|
66
75
|
/**
|
|
67
76
|
* Interface for the VoiceRecorderPlugin which provides methods to record audio.
|
|
68
77
|
*/
|
|
@@ -110,6 +119,7 @@ export interface VoiceRecorderPlugin {
|
|
|
110
119
|
* If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.
|
|
111
120
|
* If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.
|
|
112
121
|
* In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.
|
|
122
|
+
* On iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.
|
|
113
123
|
* In case of success, the promise resolves to RecordingData containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.
|
|
114
124
|
* @returns A promise that resolves to RecordingData.
|
|
115
125
|
* @throws Error with one of the specified error codes if the recording cannot be stopped.
|
|
@@ -125,7 +135,7 @@ export interface VoiceRecorderPlugin {
|
|
|
125
135
|
*/
|
|
126
136
|
pauseRecording(): Promise<GenericResponse>;
|
|
127
137
|
/**
|
|
128
|
-
* Resumes a paused audio recording.
|
|
138
|
+
* Resumes a paused or interrupted audio recording.
|
|
129
139
|
* If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.
|
|
130
140
|
* On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.
|
|
131
141
|
* On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.
|
|
@@ -139,8 +149,31 @@ export interface VoiceRecorderPlugin {
|
|
|
139
149
|
* `{ status: "NONE" }` if the plugin is idle and waiting to start a new recording.
|
|
140
150
|
* `{ status: "RECORDING" }` if the plugin is in the middle of recording.
|
|
141
151
|
* `{ status: "PAUSED" }` if the recording is paused.
|
|
152
|
+
* `{ status: "INTERRUPTED" }` if the recording was paused due to a system interruption.
|
|
142
153
|
* @returns A promise that resolves to a CurrentRecordingStatus.
|
|
143
154
|
* @throws Error if the status cannot be fetched.
|
|
144
155
|
*/
|
|
145
156
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
157
|
+
/**
|
|
158
|
+
* Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).
|
|
159
|
+
* Available on iOS and Android only.
|
|
160
|
+
*
|
|
161
|
+
* @param eventName The name of the event to listen for.
|
|
162
|
+
* @param listenerFunc The callback function to invoke when the event occurs.
|
|
163
|
+
* @returns A promise that resolves to a PluginListenerHandle.
|
|
164
|
+
*/
|
|
165
|
+
addListener(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void): Promise<PluginListenerHandle>;
|
|
166
|
+
/**
|
|
167
|
+
* Listen for audio recording interruption end events.
|
|
168
|
+
* Available on iOS and Android only.
|
|
169
|
+
*
|
|
170
|
+
* @param eventName The name of the event to listen for.
|
|
171
|
+
* @param listenerFunc The callback function to invoke when the event occurs.
|
|
172
|
+
* @returns A promise that resolves to a PluginListenerHandle.
|
|
173
|
+
*/
|
|
174
|
+
addListener(eventName: 'voiceRecordingInterruptionEnded', listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void): Promise<PluginListenerHandle>;
|
|
175
|
+
/**
|
|
176
|
+
* Remove all listeners for this plugin.
|
|
177
|
+
*/
|
|
178
|
+
removeAllListeners(): Promise<void>;
|
|
146
179
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type {Directory} from \"@capacitor/filesystem\";\n\n/**\n * Represents a Base64 encoded string.\n */\nexport type Base64String = string;\n\n/**\n * Can be used to specify options for the recording.\n */\nexport interface RecordingOptions {\n /**\n * The capacitor filesystem directory where the recording should be saved.\n *\n * If not specified, the recording will be stored in a base64 string and returned in the `RecordingData` object.\n * @see RecordingData\n */\n directory?: Directory;\n\n /**\n * An optional subdirectory in the specified directory where the recording should be saved.\n */\n subDirectory?: string;\n}\n\n/**\n * Interface representing the data of a recording.\n */\nexport interface RecordingData {\n /**\n * The value containing the recording details.\n */\n value: {\n /**\n * The recorded data as a Base64 encoded string.\n */\n recordDataBase64: Base64String;\n /**\n * The duration of the recording in milliseconds.\n */\n msDuration: number;\n /**\n * The MIME type of the recorded file.\n */\n mimeType: string;\n\n /**\n * The URI of the recording file.\n */\n uri?: string;\n };\n}\n\n/**\n * Interface representing a generic response with a boolean value.\n */\nexport interface GenericResponse {\n /**\n * The result of the operation as a boolean value.\n */\n value: boolean;\n}\n\n/**\n * Interface representing the current status of the voice recorder.\n */\nexport interface CurrentRecordingStatus {\n /**\n * The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'.\n */\n status: 'RECORDING' | 'PAUSED' | 'NONE';\n}\n\n/**\n * Interface for the VoiceRecorderPlugin which provides methods to record audio.\n */\nexport interface VoiceRecorderPlugin {\n /**\n * Checks if the current device can record audio.\n * On mobile, this function will always resolve to `{ value: true }`.\n * In a browser, it will resolve to `{ value: true }` or `{ value: false }` based on the browser's ability to record.\n * This method does not take into account the permission status, only if the browser itself is capable of recording at all.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with code \"COULD_NOT_QUERY_PERMISSION_STATUS\" if the device cannot query the permission status.\n */\n canDeviceVoiceRecord(): Promise<GenericResponse>;\n\n /**\n * Requests audio recording permission from the user.\n * If the permission has already been provided, the promise will resolve with `{ value: true }`.\n * Otherwise, the promise will resolve to `{ value: true }` or `{ value: false }` based on the user's response.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error if the permission request fails.\n */\n requestAudioRecordingPermission(): Promise<GenericResponse>;\n\n /**\n * Checks if audio recording permission has been granted.\n * Will resolve to `{ value: true }` or `{ value: false }` based on the status of the permission.\n * The web implementation of this plugin uses the Permissions API, which is not widespread.\n * If the status of the permission cannot be checked, the promise will reject with `COULD_NOT_QUERY_PERMISSION_STATUS`.\n * In that case, use `requestAudioRecordingPermission` or `startRecording` and capture any exception that is thrown.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with code \"COULD_NOT_QUERY_PERMISSION_STATUS\" if the device cannot query the permission status.\n */\n hasAudioRecordingPermission(): Promise<GenericResponse>;\n\n /**\n * Starts audio recording.\n * On success, the promise will resolve to { value: true }.\n * On error, the promise will reject with one of the following error codes:\n * \"MISSING_PERMISSION\", \"ALREADY_RECORDING\", \"MICROPHONE_BEING_USED\", \"DEVICE_CANNOT_VOICE_RECORD\", or \"FAILED_TO_RECORD\".\n * @param options The options for the recording.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be started.\n */\n startRecording(options?: RecordingOptions): Promise<GenericResponse>;\n\n /**\n * Stops audio recording.\n * Will stop the recording that has been previously started.\n * If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.\n * If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.\n * In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.\n * In case of success, the promise resolves to RecordingData containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.\n * @returns A promise that resolves to RecordingData.\n * @throws Error with one of the specified error codes if the recording cannot be stopped.\n */\n stopRecording(): Promise<RecordingData>;\n\n /**\n * Pauses the ongoing audio recording.\n * If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\n * On success, the promise will resolve to { value: true } if the pause was successful or { value: false } if the recording is already paused.\n * On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be paused.\n */\n pauseRecording(): Promise<GenericResponse>;\n\n /**\n * Resumes a paused audio recording.\n * If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\n * On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.\n * On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be resumed.\n */\n resumeRecording(): Promise<GenericResponse>;\n\n /**\n * Gets the current status of the voice recorder.\n * Will resolve with one of the following values:\n * `{ status: \"NONE\" }` if the plugin is idle and waiting to start a new recording.\n * `{ status: \"RECORDING\" }` if the plugin is in the middle of recording.\n * `{ status: \"PAUSED\" }` if the recording is paused.\n * @returns A promise that resolves to a CurrentRecordingStatus.\n * @throws Error if the status cannot be fetched.\n */\n getCurrentStatus(): Promise<CurrentRecordingStatus>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type {PluginListenerHandle} from \"@capacitor/core\";\nimport type {Directory} from \"@capacitor/filesystem\";\n\n/**\n * Represents a Base64 encoded string.\n */\nexport type Base64String = string;\n\n/**\n * Can be used to specify options for the recording.\n */\nexport interface RecordingOptions {\n /**\n * The capacitor filesystem directory where the recording should be saved.\n *\n * If not specified, the recording will be stored in a base64 string and returned in the `RecordingData` object.\n * @see RecordingData\n */\n directory?: Directory;\n\n /**\n * An optional subdirectory in the specified directory where the recording should be saved.\n */\n subDirectory?: string;\n}\n\n/**\n * Interface representing the data of a recording.\n */\nexport interface RecordingData {\n /**\n * The value containing the recording details.\n */\n value: {\n /**\n * The recorded data as a Base64 encoded string.\n */\n recordDataBase64: Base64String;\n /**\n * The duration of the recording in milliseconds.\n */\n msDuration: number;\n /**\n * The MIME type of the recorded file.\n */\n mimeType: string;\n\n /**\n * The URI of the recording file.\n */\n uri?: string;\n };\n}\n\n/**\n * Interface representing a generic response with a boolean value.\n */\nexport interface GenericResponse {\n /**\n * The result of the operation as a boolean value.\n */\n value: boolean;\n}\n\n/**\n * Interface representing the current status of the voice recorder.\n */\nexport interface CurrentRecordingStatus {\n /**\n * The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'INTERRUPTED', 'NONE'.\n */\n status: 'RECORDING' | 'PAUSED' | 'INTERRUPTED' | 'NONE';\n}\n\n/**\n * Event payload for voiceRecordingInterrupted event (empty - no data).\n */\nexport type VoiceRecordingInterruptedEvent = Record<string, never>;\n\n/**\n * Event payload for voiceRecordingInterruptionEnded event (empty - no data).\n */\nexport type VoiceRecordingInterruptionEndedEvent = Record<string, never>;\n\n/**\n * Interface for the VoiceRecorderPlugin which provides methods to record audio.\n */\nexport interface VoiceRecorderPlugin {\n /**\n * Checks if the current device can record audio.\n * On mobile, this function will always resolve to `{ value: true }`.\n * In a browser, it will resolve to `{ value: true }` or `{ value: false }` based on the browser's ability to record.\n * This method does not take into account the permission status, only if the browser itself is capable of recording at all.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with code \"COULD_NOT_QUERY_PERMISSION_STATUS\" if the device cannot query the permission status.\n */\n canDeviceVoiceRecord(): Promise<GenericResponse>;\n\n /**\n * Requests audio recording permission from the user.\n * If the permission has already been provided, the promise will resolve with `{ value: true }`.\n * Otherwise, the promise will resolve to `{ value: true }` or `{ value: false }` based on the user's response.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error if the permission request fails.\n */\n requestAudioRecordingPermission(): Promise<GenericResponse>;\n\n /**\n * Checks if audio recording permission has been granted.\n * Will resolve to `{ value: true }` or `{ value: false }` based on the status of the permission.\n * The web implementation of this plugin uses the Permissions API, which is not widespread.\n * If the status of the permission cannot be checked, the promise will reject with `COULD_NOT_QUERY_PERMISSION_STATUS`.\n * In that case, use `requestAudioRecordingPermission` or `startRecording` and capture any exception that is thrown.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with code \"COULD_NOT_QUERY_PERMISSION_STATUS\" if the device cannot query the permission status.\n */\n hasAudioRecordingPermission(): Promise<GenericResponse>;\n\n /**\n * Starts audio recording.\n * On success, the promise will resolve to { value: true }.\n * On error, the promise will reject with one of the following error codes:\n * \"MISSING_PERMISSION\", \"ALREADY_RECORDING\", \"MICROPHONE_BEING_USED\", \"DEVICE_CANNOT_VOICE_RECORD\", or \"FAILED_TO_RECORD\".\n * @param options The options for the recording.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be started.\n */\n startRecording(options?: RecordingOptions): Promise<GenericResponse>;\n\n /**\n * Stops audio recording.\n * Will stop the recording that has been previously started.\n * If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.\n * If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.\n * In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.\n * On iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.\n * In case of success, the promise resolves to RecordingData containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.\n * @returns A promise that resolves to RecordingData.\n * @throws Error with one of the specified error codes if the recording cannot be stopped.\n */\n stopRecording(): Promise<RecordingData>;\n\n /**\n * Pauses the ongoing audio recording.\n * If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\n * On success, the promise will resolve to { value: true } if the pause was successful or { value: false } if the recording is already paused.\n * On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be paused.\n */\n pauseRecording(): Promise<GenericResponse>;\n\n /**\n * Resumes a paused or interrupted audio recording.\n * If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.\n * On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.\n * On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.\n * @returns A promise that resolves to a GenericResponse.\n * @throws Error with one of the specified error codes if the recording cannot be resumed.\n */\n resumeRecording(): Promise<GenericResponse>;\n\n /**\n * Gets the current status of the voice recorder.\n * Will resolve with one of the following values:\n * `{ status: \"NONE\" }` if the plugin is idle and waiting to start a new recording.\n * `{ status: \"RECORDING\" }` if the plugin is in the middle of recording.\n * `{ status: \"PAUSED\" }` if the recording is paused.\n * `{ status: \"INTERRUPTED\" }` if the recording was paused due to a system interruption.\n * @returns A promise that resolves to a CurrentRecordingStatus.\n * @throws Error if the status cannot be fetched.\n */\n getCurrentStatus(): Promise<CurrentRecordingStatus>;\n\n /**\n * Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).\n * Available on iOS and Android only.\n *\n * @param eventName The name of the event to listen for.\n * @param listenerFunc The callback function to invoke when the event occurs.\n * @returns A promise that resolves to a PluginListenerHandle.\n */\n addListener(\n eventName: 'voiceRecordingInterrupted',\n listenerFunc: (event: VoiceRecordingInterruptedEvent) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for audio recording interruption end events.\n * Available on iOS and Android only.\n *\n * @param eventName The name of the event to listen for.\n * @param listenerFunc The callback function to invoke when the event occurs.\n * @returns A promise that resolves to a PluginListenerHandle.\n */\n addListener(\n eventName: 'voiceRecordingInterruptionEnded',\n listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Remove all listeners for this plugin.\n */\n removeAllListeners(): Promise<void>;\n}\n"]}
|
|
@@ -6,9 +6,13 @@ class CustomMediaRecorder {
|
|
|
6
6
|
public var options: RecordOptions?
|
|
7
7
|
private var recordingSession: AVAudioSession!
|
|
8
8
|
private var audioRecorder: AVAudioRecorder!
|
|
9
|
-
private var
|
|
9
|
+
private var baseAudioFilePath: URL!
|
|
10
|
+
private var audioFileSegments: [URL] = []
|
|
10
11
|
private var originalRecordingSessionCategory: AVAudioSession.Category!
|
|
11
12
|
private var status = CurrentRecordingStatus.NONE
|
|
13
|
+
private var interruptionObserver: NSObjectProtocol?
|
|
14
|
+
var onInterruptionBegan: (() -> Void)?
|
|
15
|
+
var onInterruptionEnded: (() -> Void)?
|
|
12
16
|
|
|
13
17
|
private let settings = [
|
|
14
18
|
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
|
@@ -46,8 +50,10 @@ class CustomMediaRecorder {
|
|
|
46
50
|
originalRecordingSessionCategory = recordingSession.category
|
|
47
51
|
try recordingSession.setCategory(AVAudioSession.Category.playAndRecord)
|
|
48
52
|
try recordingSession.setActive(true)
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
baseAudioFilePath = getDirectoryToSaveAudioFile().appendingPathComponent("recording-\(Int(Date().timeIntervalSince1970 * 1000)).aac")
|
|
54
|
+
audioFileSegments = [baseAudioFilePath]
|
|
55
|
+
audioRecorder = try AVAudioRecorder(url: baseAudioFilePath, settings: settings)
|
|
56
|
+
setupInterruptionHandling()
|
|
51
57
|
audioRecorder.record()
|
|
52
58
|
status = CurrentRecordingStatus.RECORDING
|
|
53
59
|
return true
|
|
@@ -56,20 +62,46 @@ class CustomMediaRecorder {
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
public func stopRecording() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
public func stopRecording(completion: @escaping (Bool) -> Void) {
|
|
66
|
+
removeInterruptionHandling()
|
|
67
|
+
audioRecorder.stop()
|
|
68
|
+
|
|
69
|
+
let finalizeStop: (Bool) -> Void = { [weak self] success in
|
|
70
|
+
guard let self = self else {
|
|
71
|
+
completion(false)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
do {
|
|
76
|
+
try self.recordingSession.setActive(false)
|
|
77
|
+
try self.recordingSession.setCategory(self.originalRecordingSessionCategory)
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
self.originalRecordingSessionCategory = nil
|
|
82
|
+
self.audioRecorder = nil
|
|
83
|
+
self.recordingSession = nil
|
|
84
|
+
self.status = CurrentRecordingStatus.NONE
|
|
85
|
+
completion(success)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if audioFileSegments.count > 1 {
|
|
89
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
90
|
+
guard let self = self else {
|
|
91
|
+
completion(false)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
self.mergeAudioSegments { success in
|
|
95
|
+
finalizeStop(success)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
finalizeStop(true)
|
|
100
|
+
}
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
public func getOutputFile() -> URL {
|
|
72
|
-
return
|
|
104
|
+
return baseAudioFilePath
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
public func getDirectory(directory: String?) -> FileManager.SearchPathDirectory? {
|
|
@@ -97,17 +129,207 @@ class CustomMediaRecorder {
|
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
public func resumeRecording() -> Bool {
|
|
100
|
-
if(status == CurrentRecordingStatus.PAUSED) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
if(status == CurrentRecordingStatus.PAUSED || status == CurrentRecordingStatus.INTERRUPTED) {
|
|
133
|
+
let wasInterrupted = status == CurrentRecordingStatus.INTERRUPTED
|
|
134
|
+
do {
|
|
135
|
+
try recordingSession.setActive(true)
|
|
136
|
+
if status == CurrentRecordingStatus.INTERRUPTED {
|
|
137
|
+
let directory = getDirectoryToSaveAudioFile()
|
|
138
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
139
|
+
let segmentNumber = audioFileSegments.count
|
|
140
|
+
let segmentPath = directory.appendingPathComponent("recording-\(timestamp)-segment-\(segmentNumber).aac")
|
|
141
|
+
audioRecorder = try AVAudioRecorder(url: segmentPath, settings: settings)
|
|
142
|
+
audioFileSegments.append(segmentPath)
|
|
143
|
+
}
|
|
144
|
+
audioRecorder.record()
|
|
145
|
+
status = CurrentRecordingStatus.RECORDING
|
|
146
|
+
return true
|
|
147
|
+
} catch {
|
|
148
|
+
if wasInterrupted {
|
|
149
|
+
try? recordingSession.setActive(false)
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
106
153
|
}
|
|
154
|
+
|
|
155
|
+
return false
|
|
107
156
|
}
|
|
108
157
|
|
|
109
158
|
public func getCurrentStatus() -> CurrentRecordingStatus {
|
|
110
159
|
return status
|
|
111
160
|
}
|
|
112
161
|
|
|
162
|
+
private func setupInterruptionHandling() {
|
|
163
|
+
interruptionObserver = NotificationCenter.default.addObserver(
|
|
164
|
+
forName: AVAudioSession.interruptionNotification,
|
|
165
|
+
object: AVAudioSession.sharedInstance(),
|
|
166
|
+
queue: .main
|
|
167
|
+
) { [weak self] notification in
|
|
168
|
+
self?.handleInterruption(notification: notification)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func removeInterruptionHandling() {
|
|
173
|
+
if let observer = interruptionObserver {
|
|
174
|
+
NotificationCenter.default.removeObserver(observer)
|
|
175
|
+
interruptionObserver = nil
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func handleInterruption(notification: Notification) {
|
|
180
|
+
guard let userInfo = notification.userInfo,
|
|
181
|
+
let interruptionTypeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
182
|
+
let interruptionType = AVAudioSession.InterruptionType(rawValue: interruptionTypeValue) else {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
switch interruptionType {
|
|
187
|
+
case .began:
|
|
188
|
+
if status == CurrentRecordingStatus.RECORDING {
|
|
189
|
+
audioRecorder.stop()
|
|
190
|
+
status = CurrentRecordingStatus.INTERRUPTED
|
|
191
|
+
onInterruptionBegan?()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case .ended:
|
|
195
|
+
if status == CurrentRecordingStatus.INTERRUPTED {
|
|
196
|
+
onInterruptionEnded?()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@unknown default:
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func mergeAudioSegments(completion: @escaping (Bool) -> Void) {
|
|
205
|
+
if audioFileSegments.count <= 1 {
|
|
206
|
+
completion(true)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let basePathWithoutExtension = baseAudioFilePath.deletingPathExtension()
|
|
211
|
+
let mergedFilePath = basePathWithoutExtension.appendingPathExtension("m4a")
|
|
212
|
+
let segmentURLs = audioFileSegments
|
|
213
|
+
let keys = ["tracks", "duration"]
|
|
214
|
+
let dispatchGroup = DispatchGroup()
|
|
215
|
+
let syncQueue = DispatchQueue(label: "CustomMediaRecorder.assetSyncQueue")
|
|
216
|
+
var loadedAssets = Array<AVURLAsset?>(repeating: nil, count: segmentURLs.count)
|
|
217
|
+
var loadFailed = false
|
|
218
|
+
|
|
219
|
+
for (index, segmentURL) in segmentURLs.enumerated() {
|
|
220
|
+
let asset = AVURLAsset(url: segmentURL)
|
|
221
|
+
dispatchGroup.enter()
|
|
222
|
+
asset.loadValuesAsynchronously(forKeys: keys) {
|
|
223
|
+
var assetIsValid = true
|
|
224
|
+
for key in keys {
|
|
225
|
+
var error: NSError?
|
|
226
|
+
if asset.statusOfValue(forKey: key, error: &error) != .loaded {
|
|
227
|
+
assetIsValid = false
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
syncQueue.async {
|
|
232
|
+
if assetIsValid {
|
|
233
|
+
loadedAssets[index] = asset
|
|
234
|
+
} else {
|
|
235
|
+
loadFailed = true
|
|
236
|
+
}
|
|
237
|
+
dispatchGroup.leave()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dispatchGroup.notify(queue: DispatchQueue.global(qos: .userInitiated)) { [weak self] in
|
|
243
|
+
guard let self = self else {
|
|
244
|
+
completion(false)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
var assets: [AVURLAsset] = []
|
|
249
|
+
var didFail = false
|
|
250
|
+
syncQueue.sync {
|
|
251
|
+
if loadFailed || loadedAssets.contains(where: { $0 == nil }) {
|
|
252
|
+
didFail = true
|
|
253
|
+
} else {
|
|
254
|
+
assets = loadedAssets.compactMap { $0 }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if didFail || assets.count != segmentURLs.count {
|
|
259
|
+
completion(false)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let composition = AVMutableComposition()
|
|
264
|
+
guard let compositionAudioTrack = composition.addMutableTrack(
|
|
265
|
+
withMediaType: .audio,
|
|
266
|
+
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
267
|
+
) else {
|
|
268
|
+
completion(false)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var insertTime = CMTime.zero
|
|
273
|
+
|
|
274
|
+
for asset in assets {
|
|
275
|
+
guard let assetTrack = asset.tracks(withMediaType: .audio).first else {
|
|
276
|
+
completion(false)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
do {
|
|
281
|
+
let timeRange = CMTimeRange(start: .zero, duration: asset.duration)
|
|
282
|
+
try compositionAudioTrack.insertTimeRange(timeRange, of: assetTrack, at: insertTime)
|
|
283
|
+
insertTime = CMTimeAdd(insertTime, asset.duration)
|
|
284
|
+
} catch {
|
|
285
|
+
completion(false)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A) else {
|
|
291
|
+
completion(false)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let tempDirectory = self.getDirectoryToSaveAudioFile()
|
|
296
|
+
let tempPath = tempDirectory.appendingPathComponent("temp-merged-\(Int(Date().timeIntervalSince1970 * 1000)).m4a")
|
|
297
|
+
|
|
298
|
+
exportSession.outputURL = tempPath
|
|
299
|
+
exportSession.outputFileType = .m4a
|
|
300
|
+
|
|
301
|
+
exportSession.exportAsynchronously {
|
|
302
|
+
guard exportSession.status == .completed else {
|
|
303
|
+
completion(false)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if !FileManager.default.fileExists(atPath: tempPath.path) {
|
|
308
|
+
completion(false)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
do {
|
|
313
|
+
if FileManager.default.fileExists(atPath: mergedFilePath.path) {
|
|
314
|
+
try FileManager.default.removeItem(at: mergedFilePath)
|
|
315
|
+
}
|
|
316
|
+
try FileManager.default.moveItem(at: tempPath, to: mergedFilePath)
|
|
317
|
+
|
|
318
|
+
for segmentURL in self.audioFileSegments {
|
|
319
|
+
if segmentURL != mergedFilePath && FileManager.default.fileExists(atPath: segmentURL.path) {
|
|
320
|
+
try? FileManager.default.removeItem(at: segmentURL)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
self.baseAudioFilePath = mergedFilePath
|
|
324
|
+
completion(true)
|
|
325
|
+
} catch {
|
|
326
|
+
if FileManager.default.fileExists(atPath: tempPath.path) {
|
|
327
|
+
try? FileManager.default.removeItem(at: tempPath)
|
|
328
|
+
}
|
|
329
|
+
completion(false)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
113
335
|
}
|
|
@@ -7,6 +7,7 @@ struct Messages {
|
|
|
7
7
|
static let FAILED_TO_RECORD = "FAILED_TO_RECORD"
|
|
8
8
|
static let RECORDING_HAS_NOT_STARTED = "RECORDING_HAS_NOT_STARTED"
|
|
9
9
|
static let FAILED_TO_FETCH_RECORDING = "FAILED_TO_FETCH_RECORDING"
|
|
10
|
+
static let FAILED_TO_MERGE_RECORDING = "FAILED_TO_MERGE_RECORDING"
|
|
10
11
|
static let EMPTY_RECORDING = "EMPTY_RECORDING"
|
|
11
12
|
static let ALREADY_RECORDING = "ALREADY_RECORDING"
|
|
12
13
|
static let MICROPHONE_BEING_USED = "MICROPHONE_BEING_USED"
|
|
@@ -54,6 +54,14 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
|
|
|
54
54
|
return
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
customMediaRecorder?.onInterruptionBegan = { [weak self] in
|
|
58
|
+
self?.notifyListeners("voiceRecordingInterrupted", data: [:])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
customMediaRecorder?.onInterruptionEnded = { [weak self] in
|
|
62
|
+
self?.notifyListeners("voiceRecordingInterruptionEnded", data: [:])
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
let directory: String? = call.getString("directory")
|
|
58
66
|
let subDirectory: String? = call.getString("subDirectory")
|
|
59
67
|
let recordOptions = RecordOptions(directory: directory, subDirectory: subDirectory)
|
|
@@ -71,27 +79,42 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
|
|
|
71
79
|
return
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
customMediaRecorder?.stopRecording
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
customMediaRecorder?.stopRecording { [weak self] stopSuccess in
|
|
83
|
+
DispatchQueue.main.async {
|
|
84
|
+
guard let self = self else {
|
|
85
|
+
call.reject(Messages.FAILED_TO_FETCH_RECORDING)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if !stopSuccess {
|
|
90
|
+
self.customMediaRecorder = nil
|
|
91
|
+
call.reject(Messages.FAILED_TO_MERGE_RECORDING)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let audioFileUrl = self.customMediaRecorder?.getOutputFile()
|
|
96
|
+
if(audioFileUrl == nil) {
|
|
97
|
+
self.customMediaRecorder = nil
|
|
98
|
+
call.reject(Messages.FAILED_TO_FETCH_RECORDING)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let fileExtension = audioFileUrl!.pathExtension.lowercased()
|
|
103
|
+
let mimeType = fileExtension == "m4a" ? "audio/mp4" : "audio/aac"
|
|
104
|
+
let sendDataAsBase64 = self.customMediaRecorder?.options?.directory == nil
|
|
105
|
+
let recordData = RecordData(
|
|
106
|
+
recordDataBase64: sendDataAsBase64 ? self.readFileAsBase64(audioFileUrl) : nil,
|
|
107
|
+
mimeType: mimeType,
|
|
108
|
+
msDuration: self.getMsDurationOfAudioFile(audioFileUrl),
|
|
109
|
+
uri: sendDataAsBase64 ? nil : audioFileUrl!.path
|
|
110
|
+
)
|
|
111
|
+
self.customMediaRecorder = nil
|
|
112
|
+
if (sendDataAsBase64 && recordData.recordDataBase64 == nil) || recordData.msDuration < 0 {
|
|
113
|
+
call.reject(Messages.EMPTY_RECORDING)
|
|
114
|
+
} else {
|
|
115
|
+
call.resolve(ResponseGenerator.dataResponse(recordData.toDictionary()))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/independo-gmbh/capacitor-voice-recorder.git"
|
|
14
14
|
},
|
|
15
15
|
"description": "Capacitor plugin for voice recording",
|
|
16
|
-
"version": "8.0
|
|
16
|
+
"version": "8.1.0-dev.1",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@capacitor/android": "^8.0.0",
|
|
19
19
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|