@independo/capacitor-voice-recorder 8.2.12-dev.1 → 8.3.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 +31 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/VoiceRecorder.java +6 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java +3 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/CustomMediaRecorder.java +24 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderService.java +8 -0
- package/dist/docs.json +33 -0
- package/dist/esm/adapters/VoiceRecorderWebAdapter.d.ts +3 -1
- package/dist/esm/adapters/VoiceRecorderWebAdapter.js +4 -0
- package/dist/esm/adapters/VoiceRecorderWebAdapter.js.map +1 -1
- package/dist/esm/definitions.d.ts +23 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/platform/web/VoiceRecorderImpl.d.ts +15 -1
- package/dist/esm/platform/web/VoiceRecorderImpl.js +68 -0
- package/dist/esm/platform/web/VoiceRecorderImpl.js.map +1 -1
- package/dist/esm/service/VoiceRecorderService.d.ts +5 -1
- package/dist/esm/service/VoiceRecorderService.js +4 -0
- package/dist/esm/service/VoiceRecorderService.js.map +1 -1
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +80 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +80 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/VoiceRecorder/Adapters/RecorderAdapter.swift +2 -0
- package/ios/Sources/VoiceRecorder/Bridge/VoiceRecorder.swift +6 -0
- package/ios/Sources/VoiceRecorder/Platform/CustomMediaRecorder.swift +27 -0
- package/ios/Sources/VoiceRecorder/Service/VoiceRecorderService.swift +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ section.
|
|
|
106
106
|
* [`pauseRecording()`](#pauserecording)
|
|
107
107
|
* [`resumeRecording()`](#resumerecording)
|
|
108
108
|
* [`getCurrentStatus()`](#getcurrentstatus)
|
|
109
|
+
* [`getCurrentAmplitude()`](#getcurrentamplitude)
|
|
109
110
|
* [`addListener('voiceRecordingInterrupted', ...)`](#addlistenervoicerecordinginterrupted-)
|
|
110
111
|
* [`addListener('voiceRecordingInterruptionEnded', ...)`](#addlistenervoicerecordinginterruptionended-)
|
|
111
112
|
* [`removeAllListeners()`](#removealllisteners)
|
|
@@ -257,6 +258,27 @@ Will resolve with one of the following values:
|
|
|
257
258
|
--------------------
|
|
258
259
|
|
|
259
260
|
|
|
261
|
+
### getCurrentAmplitude()
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
getCurrentAmplitude() => Promise<CurrentAmplitude>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Gets the current input amplitude.
|
|
268
|
+
|
|
269
|
+
Returns `{ value: 0 }` when no recording is active. The value is normalized
|
|
270
|
+
to the `[0, 1]` range, but the underlying signal source differs by platform,
|
|
271
|
+
so consumers may need a platform-specific scaling curve for exact parity.
|
|
272
|
+
|
|
273
|
+
Intended for UI-rate polling. A `60-100ms` interval is a reasonable starting
|
|
274
|
+
point for meters or waveforms; avoid calling it in a tight loop because each
|
|
275
|
+
call crosses the JavaScript/native bridge.
|
|
276
|
+
|
|
277
|
+
**Returns:** <code>Promise<<a href="#currentamplitude">CurrentAmplitude</a>></code>
|
|
278
|
+
|
|
279
|
+
--------------------
|
|
280
|
+
|
|
281
|
+
|
|
260
282
|
### addListener('voiceRecordingInterrupted', ...)
|
|
261
283
|
|
|
262
284
|
```typescript
|
|
@@ -347,6 +369,15 @@ Interface representing the current status of the voice recorder.
|
|
|
347
369
|
| **`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'. |
|
|
348
370
|
|
|
349
371
|
|
|
372
|
+
#### CurrentAmplitude
|
|
373
|
+
|
|
374
|
+
Interface representing the current input amplitude.
|
|
375
|
+
|
|
376
|
+
| Prop | Type | Description |
|
|
377
|
+
| ----------- | ------------------- | ------------------------------------------------------------- |
|
|
378
|
+
| **`value`** | <code>number</code> | The current input amplitude normalized to the `[0, 1]` range. |
|
|
379
|
+
|
|
380
|
+
|
|
350
381
|
#### PluginListenerHandle
|
|
351
382
|
|
|
352
383
|
| Prop | Type |
|
|
@@ -131,6 +131,12 @@ public class VoiceRecorder extends Plugin {
|
|
|
131
131
|
call.resolve(ResponseGenerator.statusResponse(service.getCurrentStatus()));
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** Returns the current input amplitude. */
|
|
135
|
+
@PluginMethod
|
|
136
|
+
public void getCurrentAmplitude(PluginCall call) {
|
|
137
|
+
call.resolve(ResponseGenerator.dataResponse(service.getCurrentAmplitude()));
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
/** Checks whether the app has the RECORD_AUDIO permission. */
|
|
135
141
|
private boolean doesUserGaveAudioRecordingPermission() {
|
|
136
142
|
return getPermissionState(VoiceRecorder.RECORD_AUDIO_ALIAS).equals(PermissionState.GRANTED);
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java
CHANGED
|
@@ -28,6 +28,9 @@ public interface RecorderAdapter {
|
|
|
28
28
|
/** Returns the current recording status. */
|
|
29
29
|
CurrentRecordingStatus getCurrentStatus();
|
|
30
30
|
|
|
31
|
+
/** Returns the current input amplitude normalized to [0, 1]. */
|
|
32
|
+
double getCurrentAmplitude();
|
|
33
|
+
|
|
31
34
|
/** Returns the output file for the recording. */
|
|
32
35
|
File getOutputFile();
|
|
33
36
|
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/CustomMediaRecorder.java
CHANGED
|
@@ -18,6 +18,9 @@ import java.util.regex.Pattern;
|
|
|
18
18
|
/** MediaRecorder wrapper that manages audio focus and interruptions. */
|
|
19
19
|
public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListener, RecorderAdapter {
|
|
20
20
|
|
|
21
|
+
/** Maximum amplitude value reported by Android MediaRecorder.getMaxAmplitude(). */
|
|
22
|
+
private static final double MAX_MEDIA_RECORDER_AMPLITUDE = 32767.0;
|
|
23
|
+
|
|
21
24
|
interface MediaRecorderFactory {
|
|
22
25
|
MediaRecorder create();
|
|
23
26
|
}
|
|
@@ -307,6 +310,19 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
|
|
|
307
310
|
return currentRecordingStatus;
|
|
308
311
|
}
|
|
309
312
|
|
|
313
|
+
/** Returns the current input amplitude normalized to [0, 1]. */
|
|
314
|
+
public double getCurrentAmplitude() {
|
|
315
|
+
if (currentRecordingStatus != CurrentRecordingStatus.RECORDING || mediaRecorder == null) {
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
return clampAmplitude(mediaRecorder.getMaxAmplitude() / MAX_MEDIA_RECORDER_AMPLITUDE);
|
|
321
|
+
} catch (RuntimeException ignore) {
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
310
326
|
/** Deletes the output file from disk. */
|
|
311
327
|
public boolean deleteOutputFile() {
|
|
312
328
|
return outputFile.delete();
|
|
@@ -360,6 +376,14 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
|
|
|
360
376
|
}
|
|
361
377
|
}
|
|
362
378
|
|
|
379
|
+
/** Clamps platform-specific amplitude calculations into the public range. */
|
|
380
|
+
private static double clampAmplitude(double value) {
|
|
381
|
+
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
return Math.min(1, Math.max(0, value));
|
|
385
|
+
}
|
|
386
|
+
|
|
363
387
|
/** Handles audio focus changes as recording interruptions. */
|
|
364
388
|
@Override
|
|
365
389
|
public void onAudioFocusChange(int focusChange) {
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderService.java
CHANGED
|
@@ -141,4 +141,12 @@ public class VoiceRecorderService {
|
|
|
141
141
|
}
|
|
142
142
|
return recorder.getCurrentStatus();
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/** Returns the current input amplitude normalized to [0, 1]. */
|
|
146
|
+
public double getCurrentAmplitude() {
|
|
147
|
+
if (recorder == null) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
return recorder.getCurrentAmplitude();
|
|
151
|
+
}
|
|
144
152
|
}
|
package/dist/docs.json
CHANGED
|
@@ -184,6 +184,23 @@
|
|
|
184
184
|
],
|
|
185
185
|
"slug": "getcurrentstatus"
|
|
186
186
|
},
|
|
187
|
+
{
|
|
188
|
+
"name": "getCurrentAmplitude",
|
|
189
|
+
"signature": "() => Promise<CurrentAmplitude>",
|
|
190
|
+
"parameters": [],
|
|
191
|
+
"returns": "Promise<CurrentAmplitude>",
|
|
192
|
+
"tags": [
|
|
193
|
+
{
|
|
194
|
+
"name": "returns",
|
|
195
|
+
"text": "A promise that resolves to a CurrentAmplitude."
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
"docs": "Gets the current input amplitude.\n\nReturns `{ value: 0 }` when no recording is active. The value is normalized\nto the `[0, 1]` range, but the underlying signal source differs by platform,\nso consumers may need a platform-specific scaling curve for exact parity.\n\nIntended for UI-rate polling. A `60-100ms` interval is a reasonable starting\npoint for meters or waveforms; avoid calling it in a tight loop because each\ncall crosses the JavaScript/native bridge.",
|
|
199
|
+
"complexTypes": [
|
|
200
|
+
"CurrentAmplitude"
|
|
201
|
+
],
|
|
202
|
+
"slug": "getcurrentamplitude"
|
|
203
|
+
},
|
|
187
204
|
{
|
|
188
205
|
"name": "addListener",
|
|
189
206
|
"signature": "(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void) => Promise<PluginListenerHandle>",
|
|
@@ -359,6 +376,22 @@
|
|
|
359
376
|
}
|
|
360
377
|
]
|
|
361
378
|
},
|
|
379
|
+
{
|
|
380
|
+
"name": "CurrentAmplitude",
|
|
381
|
+
"slug": "currentamplitude",
|
|
382
|
+
"docs": "Interface representing the current input amplitude.",
|
|
383
|
+
"tags": [],
|
|
384
|
+
"methods": [],
|
|
385
|
+
"properties": [
|
|
386
|
+
{
|
|
387
|
+
"name": "value",
|
|
388
|
+
"tags": [],
|
|
389
|
+
"docs": "The current input amplitude normalized to the `[0, 1]` range.",
|
|
390
|
+
"complexTypes": [],
|
|
391
|
+
"type": "number"
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
},
|
|
362
395
|
{
|
|
363
396
|
"name": "PluginListenerHandle",
|
|
364
397
|
"slug": "pluginlistenerhandle",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../definitions';
|
|
1
|
+
import type { CurrentAmplitude, CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../definitions';
|
|
2
2
|
import type { VoiceRecorderPlatform } from '../service/VoiceRecorderService';
|
|
3
3
|
/** Web adapter that delegates to the browser-specific implementation. */
|
|
4
4
|
export declare class VoiceRecorderWebAdapter implements VoiceRecorderPlatform {
|
|
@@ -20,4 +20,6 @@ export declare class VoiceRecorderWebAdapter implements VoiceRecorderPlatform {
|
|
|
20
20
|
resumeRecording(): Promise<GenericResponse>;
|
|
21
21
|
/** Returns the current recording state. */
|
|
22
22
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
23
|
+
/** Returns the current input amplitude. */
|
|
24
|
+
getCurrentAmplitude(): Promise<CurrentAmplitude>;
|
|
23
25
|
}
|
|
@@ -37,5 +37,9 @@ export class VoiceRecorderWebAdapter {
|
|
|
37
37
|
getCurrentStatus() {
|
|
38
38
|
return this.voiceRecorderImpl.getCurrentStatus();
|
|
39
39
|
}
|
|
40
|
+
/** Returns the current input amplitude. */
|
|
41
|
+
getCurrentAmplitude() {
|
|
42
|
+
return this.voiceRecorderImpl.getCurrentAmplitude();
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
//# sourceMappingURL=VoiceRecorderWebAdapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceRecorderWebAdapter.js","sourceRoot":"","sources":["../../../src/adapters/VoiceRecorderWebAdapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VoiceRecorderWebAdapter.js","sourceRoot":"","sources":["../../../src/adapters/VoiceRecorderWebAdapter.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtE,yEAAyE;AACzE,MAAM,OAAO,uBAAuB;IAApC;QACI,+DAA+D;QAC9C,sBAAiB,GAAG,IAAI,iBAAiB,EAAE,CAAC;IA8CjE,CAAC;IA5CG,mDAAmD;IAC5C,oBAAoB;QACvB,OAAO,iBAAiB,CAAC,oBAAoB,EAAE,CAAC;IACpD,CAAC;IAED,6DAA6D;IACtD,2BAA2B;QAC9B,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;IAC3D,CAAC;IAED,0DAA0D;IACnD,+BAA+B;QAClC,OAAO,iBAAiB,CAAC,+BAA+B,EAAE,CAAC;IAC/D,CAAC;IAED,sDAAsD;IAC/C,cAAc,CAAC,OAA0B;QAC5C,OAAO,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IAED,2DAA2D;IACpD,aAAa;QAChB,OAAO,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IAED,mDAAmD;IAC5C,cAAc;QACjB,OAAO,IAAI,CAAC,iBAAiB,CAAC,cAAc,EAAE,CAAC;IACnD,CAAC;IAED,yDAAyD;IAClD,eAAe;QAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,CAAC;IACpD,CAAC;IAED,2CAA2C;IACpC,gBAAgB;QACnB,OAAO,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;IACrD,CAAC;IAED,2CAA2C;IACpC,mBAAmB;QACtB,OAAO,IAAI,CAAC,iBAAiB,CAAC,mBAAmB,EAAE,CAAC;IACxD,CAAC;CACJ","sourcesContent":["import type {\n CurrentAmplitude,\n CurrentRecordingStatus,\n GenericResponse,\n RecordingData,\n RecordingOptions,\n} from '../definitions';\nimport { VoiceRecorderImpl } from '../platform/web/VoiceRecorderImpl';\nimport type { VoiceRecorderPlatform } from '../service/VoiceRecorderService';\n\n/** Web adapter that delegates to the browser-specific implementation. */\nexport class VoiceRecorderWebAdapter implements VoiceRecorderPlatform {\n /** Browser implementation that talks to MediaRecorder APIs. */\n private readonly voiceRecorderImpl = new VoiceRecorderImpl();\n\n /** Checks whether the browser can record audio. */\n public canDeviceVoiceRecord(): Promise<GenericResponse> {\n return VoiceRecorderImpl.canDeviceVoiceRecord();\n }\n\n /** Returns whether the browser has microphone permission. */\n public hasAudioRecordingPermission(): Promise<GenericResponse> {\n return VoiceRecorderImpl.hasAudioRecordingPermission();\n }\n\n /** Requests microphone permission through the browser. */\n public requestAudioRecordingPermission(): Promise<GenericResponse> {\n return VoiceRecorderImpl.requestAudioRecordingPermission();\n }\n\n /** Starts a recording session using MediaRecorder. */\n public startRecording(options?: RecordingOptions): Promise<GenericResponse> {\n return this.voiceRecorderImpl.startRecording(options);\n }\n\n /** Stops the recording session and returns the payload. */\n public stopRecording(): Promise<RecordingData> {\n return this.voiceRecorderImpl.stopRecording();\n }\n\n /** Pauses the recording session when supported. */\n public pauseRecording(): Promise<GenericResponse> {\n return this.voiceRecorderImpl.pauseRecording();\n }\n\n /** Resumes a paused recording session when supported. */\n public resumeRecording(): Promise<GenericResponse> {\n return this.voiceRecorderImpl.resumeRecording();\n }\n\n /** Returns the current recording state. */\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\n return this.voiceRecorderImpl.getCurrentStatus();\n }\n\n /** Returns the current input amplitude. */\n public getCurrentAmplitude(): Promise<CurrentAmplitude> {\n return this.voiceRecorderImpl.getCurrentAmplitude();\n }\n}\n"]}
|
|
@@ -79,6 +79,15 @@ export interface CurrentRecordingStatus {
|
|
|
79
79
|
*/
|
|
80
80
|
status: 'RECORDING' | 'PAUSED' | 'INTERRUPTED' | 'NONE';
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Interface representing the current input amplitude.
|
|
84
|
+
*/
|
|
85
|
+
export interface CurrentAmplitude {
|
|
86
|
+
/**
|
|
87
|
+
* The current input amplitude normalized to the `[0, 1]` range.
|
|
88
|
+
*/
|
|
89
|
+
value: number;
|
|
90
|
+
}
|
|
82
91
|
/**
|
|
83
92
|
* Event payload for voiceRecordingInterrupted event (empty - no data).
|
|
84
93
|
*/
|
|
@@ -169,6 +178,20 @@ export interface VoiceRecorderPlugin {
|
|
|
169
178
|
* @throws Error if the status cannot be fetched.
|
|
170
179
|
*/
|
|
171
180
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
181
|
+
/**
|
|
182
|
+
* Gets the current input amplitude.
|
|
183
|
+
*
|
|
184
|
+
* Returns `{ value: 0 }` when no recording is active. The value is normalized
|
|
185
|
+
* to the `[0, 1]` range, but the underlying signal source differs by platform,
|
|
186
|
+
* so consumers may need a platform-specific scaling curve for exact parity.
|
|
187
|
+
*
|
|
188
|
+
* Intended for UI-rate polling. A `60-100ms` interval is a reasonable starting
|
|
189
|
+
* point for meters or waveforms; avoid calling it in a tight loop because each
|
|
190
|
+
* call crosses the JavaScript/native bridge.
|
|
191
|
+
*
|
|
192
|
+
* @returns A promise that resolves to a CurrentAmplitude.
|
|
193
|
+
*/
|
|
194
|
+
getCurrentAmplitude(): Promise<CurrentAmplitude>;
|
|
172
195
|
/**
|
|
173
196
|
* Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).
|
|
174
197
|
* Available on iOS and Android only.
|
|
@@ -1 +1 @@
|
|
|
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 * Whether the web implementation should require the selected recording MIME type\n * to also be playable by the browser's native HTML `<audio>` element.\n *\n * Defaults to `true` on web to reduce cases where `MediaRecorder` reports support\n * for a format but the recorded file cannot be played back in the same browser\n * (observed on some Safari/iOS/WKWebView combinations).\n *\n * Native platforms ignore this option.\n */\n requirePlaybackSupport?: boolean;\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 * The recorded file extension / format without a leading dot (for example: `m4a`, `aac`, `mp3`, `webm`).\n */\n fileExtension: 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"]}
|
|
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 * Whether the web implementation should require the selected recording MIME type\n * to also be playable by the browser's native HTML `<audio>` element.\n *\n * Defaults to `true` on web to reduce cases where `MediaRecorder` reports support\n * for a format but the recorded file cannot be played back in the same browser\n * (observed on some Safari/iOS/WKWebView combinations).\n *\n * Native platforms ignore this option.\n */\n requirePlaybackSupport?: boolean;\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 * The recorded file extension / format without a leading dot (for example: `m4a`, `aac`, `mp3`, `webm`).\n */\n fileExtension: 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 * Interface representing the current input amplitude.\n */\nexport interface CurrentAmplitude {\n /**\n * The current input amplitude normalized to the `[0, 1]` range.\n */\n value: number;\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 * Gets the current input amplitude.\n *\n * Returns `{ value: 0 }` when no recording is active. The value is normalized\n * to the `[0, 1]` range, but the underlying signal source differs by platform,\n * so consumers may need a platform-specific scaling curve for exact parity.\n *\n * Intended for UI-rate polling. A `60-100ms` interval is a reasonable starting\n * point for meters or waveforms; avoid calling it in a tight loop because each\n * call crosses the JavaScript/native bridge.\n *\n * @returns A promise that resolves to a CurrentAmplitude.\n */\n getCurrentAmplitude(): Promise<CurrentAmplitude>;\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"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../../definitions';
|
|
1
|
+
import type { CurrentAmplitude, CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../../definitions';
|
|
2
2
|
/**
|
|
3
3
|
* Ordered MIME types to probe for audio recording via `MediaRecorder.isTypeSupported()`.
|
|
4
4
|
*
|
|
@@ -37,6 +37,14 @@ export declare class VoiceRecorderImpl {
|
|
|
37
37
|
private static readonly DEFAULT_REQUIRE_PLAYBACK_SUPPORT;
|
|
38
38
|
/** Active MediaRecorder instance, if recording. */
|
|
39
39
|
private mediaRecorder;
|
|
40
|
+
/** AudioContext used for live amplitude metering. */
|
|
41
|
+
private audioContext;
|
|
42
|
+
/** Web Audio analyser used to sample the active input stream. */
|
|
43
|
+
private amplitudeAnalyser;
|
|
44
|
+
/** Source node that keeps the input stream connected to the analyser. */
|
|
45
|
+
private amplitudeSource;
|
|
46
|
+
/** Reusable buffer for analyser time-domain samples. */
|
|
47
|
+
private amplitudeSamples;
|
|
40
48
|
/** Collected data chunks from MediaRecorder. */
|
|
41
49
|
private chunks;
|
|
42
50
|
/** Promise resolved when the recorder stops and payload is ready. */
|
|
@@ -72,6 +80,8 @@ export declare class VoiceRecorderImpl {
|
|
|
72
80
|
resumeRecording(): Promise<GenericResponse>;
|
|
73
81
|
/** Returns the current recording status from MediaRecorder. */
|
|
74
82
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
83
|
+
/** Returns the current input amplitude normalized to the [0, 1] range. */
|
|
84
|
+
getCurrentAmplitude(): Promise<CurrentAmplitude>;
|
|
75
85
|
/**
|
|
76
86
|
* Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current
|
|
77
87
|
* environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,
|
|
@@ -113,6 +123,10 @@ export declare class VoiceRecorderImpl {
|
|
|
113
123
|
private onFailedToStartRecording;
|
|
114
124
|
/** Converts a Blob payload into a base64 string. */
|
|
115
125
|
private static blobToBase64;
|
|
126
|
+
/** Connects the recording stream to a lightweight Web Audio analyser for amplitude polling. */
|
|
127
|
+
private startAmplitudeMeter;
|
|
128
|
+
/** Clamps platform-specific amplitude calculations into the public range. */
|
|
129
|
+
private static clampAmplitude;
|
|
116
130
|
/** Resets state for the next recording attempt. */
|
|
117
131
|
private prepareInstanceForNextOperation;
|
|
118
132
|
}
|
|
@@ -55,6 +55,14 @@ export class VoiceRecorderImpl {
|
|
|
55
55
|
constructor() {
|
|
56
56
|
/** Active MediaRecorder instance, if recording. */
|
|
57
57
|
this.mediaRecorder = null;
|
|
58
|
+
/** AudioContext used for live amplitude metering. */
|
|
59
|
+
this.audioContext = null;
|
|
60
|
+
/** Web Audio analyser used to sample the active input stream. */
|
|
61
|
+
this.amplitudeAnalyser = null;
|
|
62
|
+
/** Source node that keeps the input stream connected to the analyser. */
|
|
63
|
+
this.amplitudeSource = null;
|
|
64
|
+
/** Reusable buffer for analyser time-domain samples. */
|
|
65
|
+
this.amplitudeSamples = null;
|
|
58
66
|
/** Collected data chunks from MediaRecorder. */
|
|
59
67
|
this.chunks = [];
|
|
60
68
|
/** Promise resolved when the recorder stops and payload is ready. */
|
|
@@ -195,6 +203,22 @@ export class VoiceRecorderImpl {
|
|
|
195
203
|
return Promise.resolve({ status: 'NONE' });
|
|
196
204
|
}
|
|
197
205
|
}
|
|
206
|
+
/** Returns the current input amplitude normalized to the [0, 1] range. */
|
|
207
|
+
getCurrentAmplitude() {
|
|
208
|
+
var _a;
|
|
209
|
+
if (((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) !== 'recording' || this.amplitudeAnalyser == null || this.amplitudeSamples == null) {
|
|
210
|
+
return Promise.resolve({ value: 0 });
|
|
211
|
+
}
|
|
212
|
+
this.amplitudeAnalyser.getByteTimeDomainData(this.amplitudeSamples);
|
|
213
|
+
let sumOfSquares = 0;
|
|
214
|
+
for (const sample of this.amplitudeSamples) {
|
|
215
|
+
const centered = (sample - 128) / 128;
|
|
216
|
+
sumOfSquares += centered * centered;
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve({
|
|
219
|
+
value: VoiceRecorderImpl.clampAmplitude(Math.sqrt(sumOfSquares / this.amplitudeSamples.length)),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
198
222
|
/**
|
|
199
223
|
* Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current
|
|
200
224
|
* environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,
|
|
@@ -272,6 +296,7 @@ export class VoiceRecorderImpl {
|
|
|
272
296
|
return;
|
|
273
297
|
}
|
|
274
298
|
this.mediaRecorder = new MediaRecorder(stream, { mimeType });
|
|
299
|
+
this.startAmplitudeMeter(stream);
|
|
275
300
|
this.mediaRecorder.onerror = () => {
|
|
276
301
|
this.prepareInstanceForNextOperation();
|
|
277
302
|
reject(failedToRecordError());
|
|
@@ -338,6 +363,34 @@ export class VoiceRecorderImpl {
|
|
|
338
363
|
reader.readAsDataURL(blob);
|
|
339
364
|
});
|
|
340
365
|
}
|
|
366
|
+
/** Connects the recording stream to a lightweight Web Audio analyser for amplitude polling. */
|
|
367
|
+
startAmplitudeMeter(stream) {
|
|
368
|
+
var _a;
|
|
369
|
+
try {
|
|
370
|
+
const AudioContextConstructor = (_a = window.AudioContext) !== null && _a !== void 0 ? _a : window.webkitAudioContext;
|
|
371
|
+
if (AudioContextConstructor == null) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.audioContext = new AudioContextConstructor();
|
|
375
|
+
this.amplitudeSource = this.audioContext.createMediaStreamSource(stream);
|
|
376
|
+
this.amplitudeAnalyser = this.audioContext.createAnalyser();
|
|
377
|
+
this.amplitudeAnalyser.fftSize = 2048;
|
|
378
|
+
this.amplitudeSamples = new Uint8Array(this.amplitudeAnalyser.fftSize);
|
|
379
|
+
this.amplitudeSource.connect(this.amplitudeAnalyser);
|
|
380
|
+
}
|
|
381
|
+
catch (ignore) {
|
|
382
|
+
this.audioContext = null;
|
|
383
|
+
this.amplitudeAnalyser = null;
|
|
384
|
+
this.amplitudeSource = null;
|
|
385
|
+
this.amplitudeSamples = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** Clamps platform-specific amplitude calculations into the public range. */
|
|
389
|
+
static clampAmplitude(value) {
|
|
390
|
+
if (!Number.isFinite(value))
|
|
391
|
+
return 0;
|
|
392
|
+
return Math.min(1, Math.max(0, value));
|
|
393
|
+
}
|
|
341
394
|
/** Resets state for the next recording attempt. */
|
|
342
395
|
prepareInstanceForNextOperation() {
|
|
343
396
|
if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {
|
|
@@ -348,6 +401,21 @@ export class VoiceRecorderImpl {
|
|
|
348
401
|
console.warn('Failed to stop recording during cleanup');
|
|
349
402
|
}
|
|
350
403
|
}
|
|
404
|
+
if (this.amplitudeSource != null) {
|
|
405
|
+
try {
|
|
406
|
+
this.amplitudeSource.disconnect();
|
|
407
|
+
}
|
|
408
|
+
catch (ignore) {
|
|
409
|
+
// The node may already be disconnected during recorder cleanup.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (this.audioContext != null && this.audioContext.state !== 'closed') {
|
|
413
|
+
void this.audioContext.close().catch(() => undefined);
|
|
414
|
+
}
|
|
415
|
+
this.audioContext = null;
|
|
416
|
+
this.amplitudeAnalyser = null;
|
|
417
|
+
this.amplitudeSource = null;
|
|
418
|
+
this.amplitudeSamples = null;
|
|
351
419
|
this.pendingResult = neverResolvingPromise();
|
|
352
420
|
this.mediaRecorder = null;
|
|
353
421
|
this.chunks = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceRecorderImpl.js","sourceRoot":"","sources":["../../../../src/platform/web/VoiceRecorderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import {Filesystem} from '@capacitor/filesystem';\nimport write_blob from 'capacitor-blob-writer';\n\nimport type {\n Base64String,\n CurrentRecordingStatus,\n GenericResponse,\n RecordingData,\n RecordingOptions,\n} from '../../definitions';\n\nimport getBlobDuration from './get-blob-duration';\nimport {\n alreadyRecordingError,\n couldNotQueryPermissionStatusError,\n deviceCannotVoiceRecordError,\n emptyRecordingError,\n failedToFetchRecordingError,\n failedToRecordError,\n failureResponse,\n missingPermissionError,\n recordingHasNotStartedError,\n successResponse,\n} from './predefined-web-responses';\n\n/**\n * Ordered MIME types to probe for audio recording via `MediaRecorder.isTypeSupported()`.\n *\n * ⚠️ The order is intentional and MUST remain stable unless you also update the\n * selection policy in code and test on Safari/iOS + WebViews.\n *\n * ✅ What this list is used for\n * - Selecting a `mimeType` for `new MediaRecorder(stream, { mimeType })`.\n *\n * ❌ What this list does NOT guarantee\n * - It does NOT guarantee that the recorded output will be playable via the\n * HTML `<audio>` element in the same browser.\n *\n * Real-world caveat (important):\n * - We have observed cases where `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')`\n * returned `true`, the recorder produced a Blob, but `<audio>` could not play it.\n * This can happen due to container/codec playback support differences, platform\n * quirks (especially Safari/iOS / WKWebView), or incomplete WebM playback support.\n *\n * Current selection behavior in this implementation:\n * - By default, MIME selection treats recorder support and playback support as separate\n * capabilities and probes both:\n * - Recorder capability: `MediaRecorder.isTypeSupported(type)`\n * - Playback capability: `audio.canPlayType(type)`\n * - This default can be disabled via `RecordingOptions.requirePlaybackSupport = false`\n * to fall back to recorder-only probing.\n *\n * Keeping legacy keys:\n * - Some entries are kept even if they overlap (e.g. `audio/mp4` and explicit codec),\n * to maximize compatibility across differing browser implementations.\n */\nconst POSSIBLE_MIME_TYPES: Record<string, string> = {\n // ✅ Most universal\n 'audio/mp4;codecs=\"mp4a.40.2\"': '.m4a', // AAC in MP4 (explicit codec helps detection)\n 'audio/mp4': '.m4a', // (legacy key kept; broad support)\n 'audio/aac': '.aac', // (legacy key kept; less common in the wild)\n 'audio/mpeg': '.mp3', // MP3 (universal)\n 'audio/wav': '.wav', // WAV (universal, big files)\n\n // ✅ Modern high-quality (very widely supported, but slightly less “universal” than MP3/AAC)\n 'audio/webm;codecs=\"opus\"': '.webm', // Opus in WebM (explicit codec helps detection)\n 'audio/webm;codecs=opus': '.webm', // (legacy key kept)\n 'audio/webm': '.webm', // (legacy key kept; container-only, codec-dependent)\n\n // ⚠️ Least universal (Safari/iOS historically the limiting factor)\n 'audio/ogg;codecs=opus': '.ogg', // (legacy key kept)\n 'audio/ogg;codecs=vorbis': '.ogg', // Ogg Vorbis (weakest mainstream support)\n};\n\n/** Creates a promise that never resolves. */\nconst neverResolvingPromise = (): Promise<any> => new Promise(() => undefined);\n\n/** Browser implementation backed by MediaRecorder and Capacitor Filesystem. */\nexport class VoiceRecorderImpl {\n /** Default behavior for web MIME selection: require recorder + playback support. */\n private static readonly DEFAULT_REQUIRE_PLAYBACK_SUPPORT = true;\n /** Active MediaRecorder instance, if recording. */\n private mediaRecorder: MediaRecorder | null = null;\n /** Collected data chunks from MediaRecorder. */\n private chunks: any[] = [];\n /** Promise resolved when the recorder stops and payload is ready. */\n private pendingResult: Promise<RecordingData> = neverResolvingPromise();\n\n /**\n * Returns whether the browser can start a recording session.\n *\n * On web this checks:\n * - `navigator.mediaDevices.getUserMedia`\n * - at least one supported recording MIME type using {@link getSupportedMimeType}\n *\n * The optional `requirePlaybackSupport` flag is forwarded to MIME selection and defaults\n * to `true` when omitted.\n */\n public static async canDeviceVoiceRecord(\n options?: Pick<RecordingOptions, 'requirePlaybackSupport'>,\n ): Promise<GenericResponse> {\n if (\n navigator?.mediaDevices?.getUserMedia == null ||\n VoiceRecorderImpl.getSupportedMimeType({\n requirePlaybackSupport: options?.requirePlaybackSupport,\n }) == null\n ) {\n return failureResponse();\n } else {\n return successResponse();\n }\n }\n\n /**\n * Starts a recording session using `MediaRecorder`.\n *\n * The selected MIME type is resolved once at start time (using the optional\n * `requirePlaybackSupport` flag from `RecordingOptions`) and reused for the final Blob\n * and file extension to keep the recording payload internally consistent.\n */\n public async startRecording(options?: RecordingOptions): Promise<GenericResponse> {\n if (this.mediaRecorder != null) {\n throw alreadyRecordingError();\n }\n const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord(options);\n if (!deviceCanRecord.value) {\n throw deviceCannotVoiceRecordError();\n }\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => successResponse());\n if (!havingPermission.value) {\n throw missingPermissionError();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then((stream) => this.onSuccessfullyStartedRecording(stream, options))\n .catch(this.onFailedToStartRecording.bind(this));\n }\n\n /** Stops the current recording and resolves the pending payload. */\n public async stopRecording(): Promise<RecordingData> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n }\n try {\n this.mediaRecorder.stop();\n this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());\n return this.pendingResult;\n } catch (ignore) {\n throw failedToFetchRecordingError();\n } finally {\n this.prepareInstanceForNextOperation();\n }\n }\n\n /** Returns whether the browser has microphone permission. */\n public static async hasAudioRecordingPermission(): Promise<GenericResponse> {\n // Safari does not support navigator.permissions.query\n if (!navigator.permissions.query) {\n if (navigator.mediaDevices !== undefined) {\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n }\n return navigator.permissions\n .query({name: 'microphone' as any})\n .then((result) => ({value: result.state === 'granted'}))\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n\n /** Requests microphone permission from the browser. */\n public static async requestAudioRecordingPermission(): Promise<GenericResponse> {\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => failureResponse());\n if (havingPermission.value) {\n return successResponse();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => failureResponse());\n }\n\n /** Pauses the recording session when supported. */\n public pauseRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'recording') {\n this.mediaRecorder.pause();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n /** Resumes a paused recording session when supported. */\n public resumeRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'paused') {\n this.mediaRecorder.resume();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n /** Returns the current recording status from MediaRecorder. */\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\n if (this.mediaRecorder == null) {\n return Promise.resolve({status: 'NONE'});\n } else if (this.mediaRecorder.state === 'recording') {\n return Promise.resolve({status: 'RECORDING'});\n } else if (this.mediaRecorder.state === 'paused') {\n return Promise.resolve({status: 'PAUSED'});\n } else {\n return Promise.resolve({status: 'NONE'});\n }\n }\n\n /**\n * Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current\n * environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,\n * optionally requiring native HTML `<audio>` playback support too.\n *\n * The search order is the iteration order of {@link POSSIBLE_MIME_TYPES}.\n *\n * @typeParam T - A MIME type string that exists as a key in {@link POSSIBLE_MIME_TYPES}.\n *\n * @returns The first supported MIME type for `MediaRecorder`, or `null` if:\n * - `MediaRecorder` is unavailable, or\n * - no configured MIME types are supported.\n *\n * ⚠️ Important: `MediaRecorder` support ≠ `<audio>` playback support\n *\n * Some browsers/platforms can claim support for recording a format (notably WebM/Opus)\n * but still fail to play the resulting Blob through the native HTML audio pipeline.\n * This mismatch is especially likely on Safari/iOS / WKWebView variants, so the default\n * behavior also probes `HTMLAudioElement.canPlayType(type)` when available.\n *\n * Selection policy when playback probing is enabled:\n * - keep the global priority order from {@link POSSIBLE_MIME_TYPES}\n * - among recordable types, prefer the first `\"probably\"` playable candidate\n * - otherwise return the first `\"maybe\"` playable candidate\n * - treat `\"\"` as not playable\n *\n * Note: The <audio> element is never attached to the DOM, so it won't appear to users or assistive tech.\n *\n * Fallback behavior:\n * - If `document` / `audio.canPlayType` is unavailable (e.g. SSR-like environments),\n * this falls back to record-only probing.\n */\n public static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(\n options?: { requirePlaybackSupport?: boolean },\n ): T | null {\n if (MediaRecorder?.isTypeSupported == null) return null;\n\n const orderedTypes = Object.keys(POSSIBLE_MIME_TYPES) as T[];\n const recordSupportedTypes = orderedTypes.filter((type) => MediaRecorder.isTypeSupported(type));\n if (recordSupportedTypes.length === 0) return null;\n\n const requirePlaybackSupport =\n options?.requirePlaybackSupport ?? VoiceRecorderImpl.DEFAULT_REQUIRE_PLAYBACK_SUPPORT;\n if (!requirePlaybackSupport) {\n return recordSupportedTypes[0] ?? null;\n }\n\n if (typeof document === 'undefined' || typeof document.createElement !== 'function') {\n return recordSupportedTypes[0] ?? null;\n }\n\n const audioElement = document.createElement('audio') as Partial<HTMLAudioElement>;\n if (typeof audioElement.canPlayType !== 'function') {\n return recordSupportedTypes[0] ?? null;\n }\n\n let firstProbably: T | null = null;\n let firstMaybe: T | null = null;\n\n for (const type of recordSupportedTypes) {\n const playbackSupport = audioElement.canPlayType(type);\n if (playbackSupport === 'probably') {\n firstProbably = type;\n break;\n }\n if (playbackSupport === 'maybe' && firstMaybe == null) {\n firstMaybe = type;\n }\n }\n\n return firstProbably ?? firstMaybe ?? null;\n }\n\n /** Initializes MediaRecorder and wires up handlers. */\n private onSuccessfullyStartedRecording(stream: MediaStream, options?: RecordingOptions): GenericResponse {\n this.pendingResult = new Promise((resolve, reject) => {\n const mimeType = VoiceRecorderImpl.getSupportedMimeType({\n requirePlaybackSupport: options?.requirePlaybackSupport,\n });\n if (mimeType == null) {\n this.prepareInstanceForNextOperation();\n reject(failedToRecordError());\n return;\n }\n\n this.mediaRecorder = new MediaRecorder(stream, {mimeType});\n this.mediaRecorder.onerror = () => {\n this.prepareInstanceForNextOperation();\n reject(failedToRecordError());\n };\n this.mediaRecorder.onstop = async () => {\n const mt = this.mediaRecorder?.mimeType ?? mimeType;\n const blobVoiceRecording = new Blob(this.chunks, {type: mt});\n if (blobVoiceRecording.size <= 0) {\n this.prepareInstanceForNextOperation();\n reject(emptyRecordingError());\n return;\n }\n\n let uri: string | undefined = undefined;\n let recordDataBase64 = '';\n const fileExtension = (POSSIBLE_MIME_TYPES[mimeType] ?? '').replace(/^\\./, '');\n if (options?.directory) {\n const subDirectory = options.subDirectory?.match(/^\\/?(.+[^/])\\/?$/)?.[1] ?? '';\n const path = `${subDirectory}/recording-${new Date().getTime()}${POSSIBLE_MIME_TYPES[mt]}`;\n\n await write_blob({\n blob: blobVoiceRecording,\n directory: options.directory,\n fast_mode: true,\n path,\n recursive: true,\n });\n\n ({uri} = await Filesystem.getUri({directory: options.directory, path}));\n } else {\n recordDataBase64 = await VoiceRecorderImpl.blobToBase64(blobVoiceRecording);\n }\n\n const recordingDuration = await getBlobDuration(blobVoiceRecording);\n this.prepareInstanceForNextOperation();\n resolve({\n value: {\n recordDataBase64,\n mimeType: mt,\n fileExtension,\n msDuration: recordingDuration * 1000,\n uri\n }\n });\n };\n this.mediaRecorder.ondataavailable = (event: any) => this.chunks.push(event.data);\n this.mediaRecorder.start();\n });\n return successResponse();\n }\n\n /** Handles failures from getUserMedia. */\n private onFailedToStartRecording(): GenericResponse {\n this.prepareInstanceForNextOperation();\n throw failedToRecordError();\n }\n\n /** Converts a Blob payload into a base64 string. */\n private static blobToBase64(blob: Blob): Promise<Base64String> {\n return new Promise((resolve) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const recordingResult = String(reader.result);\n const splitResult = recordingResult.split('base64,');\n const toResolve = splitResult.length > 1 ? splitResult[1] : recordingResult;\n resolve(toResolve.trim());\n };\n reader.readAsDataURL(blob);\n });\n }\n\n /** Resets state for the next recording attempt. */\n private prepareInstanceForNextOperation(): void {\n if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {\n try {\n this.mediaRecorder.stop();\n } catch (ignore) {\n console.warn('Failed to stop recording during cleanup');\n }\n }\n this.pendingResult = neverResolvingPromise();\n this.mediaRecorder = null;\n this.chunks = [];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"VoiceRecorderImpl.js","sourceRoot":"","sources":["../../../../src/platform/web/VoiceRecorderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import {Filesystem} from '@capacitor/filesystem';\nimport write_blob from 'capacitor-blob-writer';\n\nimport type {\n Base64String,\n CurrentAmplitude,\n CurrentRecordingStatus,\n GenericResponse,\n RecordingData,\n RecordingOptions,\n} from '../../definitions';\n\nimport getBlobDuration from './get-blob-duration';\nimport {\n alreadyRecordingError,\n couldNotQueryPermissionStatusError,\n deviceCannotVoiceRecordError,\n emptyRecordingError,\n failedToFetchRecordingError,\n failedToRecordError,\n failureResponse,\n missingPermissionError,\n recordingHasNotStartedError,\n successResponse,\n} from './predefined-web-responses';\n\n/**\n * Ordered MIME types to probe for audio recording via `MediaRecorder.isTypeSupported()`.\n *\n * ⚠️ The order is intentional and MUST remain stable unless you also update the\n * selection policy in code and test on Safari/iOS + WebViews.\n *\n * ✅ What this list is used for\n * - Selecting a `mimeType` for `new MediaRecorder(stream, { mimeType })`.\n *\n * ❌ What this list does NOT guarantee\n * - It does NOT guarantee that the recorded output will be playable via the\n * HTML `<audio>` element in the same browser.\n *\n * Real-world caveat (important):\n * - We have observed cases where `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')`\n * returned `true`, the recorder produced a Blob, but `<audio>` could not play it.\n * This can happen due to container/codec playback support differences, platform\n * quirks (especially Safari/iOS / WKWebView), or incomplete WebM playback support.\n *\n * Current selection behavior in this implementation:\n * - By default, MIME selection treats recorder support and playback support as separate\n * capabilities and probes both:\n * - Recorder capability: `MediaRecorder.isTypeSupported(type)`\n * - Playback capability: `audio.canPlayType(type)`\n * - This default can be disabled via `RecordingOptions.requirePlaybackSupport = false`\n * to fall back to recorder-only probing.\n *\n * Keeping legacy keys:\n * - Some entries are kept even if they overlap (e.g. `audio/mp4` and explicit codec),\n * to maximize compatibility across differing browser implementations.\n */\nconst POSSIBLE_MIME_TYPES: Record<string, string> = {\n // ✅ Most universal\n 'audio/mp4;codecs=\"mp4a.40.2\"': '.m4a', // AAC in MP4 (explicit codec helps detection)\n 'audio/mp4': '.m4a', // (legacy key kept; broad support)\n 'audio/aac': '.aac', // (legacy key kept; less common in the wild)\n 'audio/mpeg': '.mp3', // MP3 (universal)\n 'audio/wav': '.wav', // WAV (universal, big files)\n\n // ✅ Modern high-quality (very widely supported, but slightly less “universal” than MP3/AAC)\n 'audio/webm;codecs=\"opus\"': '.webm', // Opus in WebM (explicit codec helps detection)\n 'audio/webm;codecs=opus': '.webm', // (legacy key kept)\n 'audio/webm': '.webm', // (legacy key kept; container-only, codec-dependent)\n\n // ⚠️ Least universal (Safari/iOS historically the limiting factor)\n 'audio/ogg;codecs=opus': '.ogg', // (legacy key kept)\n 'audio/ogg;codecs=vorbis': '.ogg', // Ogg Vorbis (weakest mainstream support)\n};\n\n/** Creates a promise that never resolves. */\nconst neverResolvingPromise = (): Promise<any> => new Promise(() => undefined);\n\n/** Browser implementation backed by MediaRecorder and Capacitor Filesystem. */\nexport class VoiceRecorderImpl {\n /** Default behavior for web MIME selection: require recorder + playback support. */\n private static readonly DEFAULT_REQUIRE_PLAYBACK_SUPPORT = true;\n /** Active MediaRecorder instance, if recording. */\n private mediaRecorder: MediaRecorder | null = null;\n /** AudioContext used for live amplitude metering. */\n private audioContext: AudioContext | null = null;\n /** Web Audio analyser used to sample the active input stream. */\n private amplitudeAnalyser: AnalyserNode | null = null;\n /** Source node that keeps the input stream connected to the analyser. */\n private amplitudeSource: MediaStreamAudioSourceNode | null = null;\n /** Reusable buffer for analyser time-domain samples. */\n private amplitudeSamples: Uint8Array<ArrayBuffer> | null = null;\n /** Collected data chunks from MediaRecorder. */\n private chunks: any[] = [];\n /** Promise resolved when the recorder stops and payload is ready. */\n private pendingResult: Promise<RecordingData> = neverResolvingPromise();\n\n /**\n * Returns whether the browser can start a recording session.\n *\n * On web this checks:\n * - `navigator.mediaDevices.getUserMedia`\n * - at least one supported recording MIME type using {@link getSupportedMimeType}\n *\n * The optional `requirePlaybackSupport` flag is forwarded to MIME selection and defaults\n * to `true` when omitted.\n */\n public static async canDeviceVoiceRecord(\n options?: Pick<RecordingOptions, 'requirePlaybackSupport'>,\n ): Promise<GenericResponse> {\n if (\n navigator?.mediaDevices?.getUserMedia == null ||\n VoiceRecorderImpl.getSupportedMimeType({\n requirePlaybackSupport: options?.requirePlaybackSupport,\n }) == null\n ) {\n return failureResponse();\n } else {\n return successResponse();\n }\n }\n\n /**\n * Starts a recording session using `MediaRecorder`.\n *\n * The selected MIME type is resolved once at start time (using the optional\n * `requirePlaybackSupport` flag from `RecordingOptions`) and reused for the final Blob\n * and file extension to keep the recording payload internally consistent.\n */\n public async startRecording(options?: RecordingOptions): Promise<GenericResponse> {\n if (this.mediaRecorder != null) {\n throw alreadyRecordingError();\n }\n const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord(options);\n if (!deviceCanRecord.value) {\n throw deviceCannotVoiceRecordError();\n }\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => successResponse());\n if (!havingPermission.value) {\n throw missingPermissionError();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then((stream) => this.onSuccessfullyStartedRecording(stream, options))\n .catch(this.onFailedToStartRecording.bind(this));\n }\n\n /** Stops the current recording and resolves the pending payload. */\n public async stopRecording(): Promise<RecordingData> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n }\n try {\n this.mediaRecorder.stop();\n this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());\n return this.pendingResult;\n } catch (ignore) {\n throw failedToFetchRecordingError();\n } finally {\n this.prepareInstanceForNextOperation();\n }\n }\n\n /** Returns whether the browser has microphone permission. */\n public static async hasAudioRecordingPermission(): Promise<GenericResponse> {\n // Safari does not support navigator.permissions.query\n if (!navigator.permissions.query) {\n if (navigator.mediaDevices !== undefined) {\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n }\n return navigator.permissions\n .query({name: 'microphone' as any})\n .then((result) => ({value: result.state === 'granted'}))\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n\n /** Requests microphone permission from the browser. */\n public static async requestAudioRecordingPermission(): Promise<GenericResponse> {\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => failureResponse());\n if (havingPermission.value) {\n return successResponse();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => failureResponse());\n }\n\n /** Pauses the recording session when supported. */\n public pauseRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'recording') {\n this.mediaRecorder.pause();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n /** Resumes a paused recording session when supported. */\n public resumeRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'paused') {\n this.mediaRecorder.resume();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n /** Returns the current recording status from MediaRecorder. */\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\n if (this.mediaRecorder == null) {\n return Promise.resolve({status: 'NONE'});\n } else if (this.mediaRecorder.state === 'recording') {\n return Promise.resolve({status: 'RECORDING'});\n } else if (this.mediaRecorder.state === 'paused') {\n return Promise.resolve({status: 'PAUSED'});\n } else {\n return Promise.resolve({status: 'NONE'});\n }\n }\n\n /** Returns the current input amplitude normalized to the [0, 1] range. */\n public getCurrentAmplitude(): Promise<CurrentAmplitude> {\n if (this.mediaRecorder?.state !== 'recording' || this.amplitudeAnalyser == null || this.amplitudeSamples == null) {\n return Promise.resolve({value: 0});\n }\n\n this.amplitudeAnalyser.getByteTimeDomainData(this.amplitudeSamples);\n let sumOfSquares = 0;\n for (const sample of this.amplitudeSamples) {\n const centered = (sample - 128) / 128;\n sumOfSquares += centered * centered;\n }\n\n return Promise.resolve({\n value: VoiceRecorderImpl.clampAmplitude(Math.sqrt(sumOfSquares / this.amplitudeSamples.length)),\n });\n }\n\n /**\n * Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current\n * environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,\n * optionally requiring native HTML `<audio>` playback support too.\n *\n * The search order is the iteration order of {@link POSSIBLE_MIME_TYPES}.\n *\n * @typeParam T - A MIME type string that exists as a key in {@link POSSIBLE_MIME_TYPES}.\n *\n * @returns The first supported MIME type for `MediaRecorder`, or `null` if:\n * - `MediaRecorder` is unavailable, or\n * - no configured MIME types are supported.\n *\n * ⚠️ Important: `MediaRecorder` support ≠ `<audio>` playback support\n *\n * Some browsers/platforms can claim support for recording a format (notably WebM/Opus)\n * but still fail to play the resulting Blob through the native HTML audio pipeline.\n * This mismatch is especially likely on Safari/iOS / WKWebView variants, so the default\n * behavior also probes `HTMLAudioElement.canPlayType(type)` when available.\n *\n * Selection policy when playback probing is enabled:\n * - keep the global priority order from {@link POSSIBLE_MIME_TYPES}\n * - among recordable types, prefer the first `\"probably\"` playable candidate\n * - otherwise return the first `\"maybe\"` playable candidate\n * - treat `\"\"` as not playable\n *\n * Note: The <audio> element is never attached to the DOM, so it won't appear to users or assistive tech.\n *\n * Fallback behavior:\n * - If `document` / `audio.canPlayType` is unavailable (e.g. SSR-like environments),\n * this falls back to record-only probing.\n */\n public static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(\n options?: { requirePlaybackSupport?: boolean },\n ): T | null {\n if (MediaRecorder?.isTypeSupported == null) return null;\n\n const orderedTypes = Object.keys(POSSIBLE_MIME_TYPES) as T[];\n const recordSupportedTypes = orderedTypes.filter((type) => MediaRecorder.isTypeSupported(type));\n if (recordSupportedTypes.length === 0) return null;\n\n const requirePlaybackSupport =\n options?.requirePlaybackSupport ?? VoiceRecorderImpl.DEFAULT_REQUIRE_PLAYBACK_SUPPORT;\n if (!requirePlaybackSupport) {\n return recordSupportedTypes[0] ?? null;\n }\n\n if (typeof document === 'undefined' || typeof document.createElement !== 'function') {\n return recordSupportedTypes[0] ?? null;\n }\n\n const audioElement = document.createElement('audio') as Partial<HTMLAudioElement>;\n if (typeof audioElement.canPlayType !== 'function') {\n return recordSupportedTypes[0] ?? null;\n }\n\n let firstProbably: T | null = null;\n let firstMaybe: T | null = null;\n\n for (const type of recordSupportedTypes) {\n const playbackSupport = audioElement.canPlayType(type);\n if (playbackSupport === 'probably') {\n firstProbably = type;\n break;\n }\n if (playbackSupport === 'maybe' && firstMaybe == null) {\n firstMaybe = type;\n }\n }\n\n return firstProbably ?? firstMaybe ?? null;\n }\n\n /** Initializes MediaRecorder and wires up handlers. */\n private onSuccessfullyStartedRecording(stream: MediaStream, options?: RecordingOptions): GenericResponse {\n this.pendingResult = new Promise((resolve, reject) => {\n const mimeType = VoiceRecorderImpl.getSupportedMimeType({\n requirePlaybackSupport: options?.requirePlaybackSupport,\n });\n if (mimeType == null) {\n this.prepareInstanceForNextOperation();\n reject(failedToRecordError());\n return;\n }\n\n this.mediaRecorder = new MediaRecorder(stream, {mimeType});\n this.startAmplitudeMeter(stream);\n this.mediaRecorder.onerror = () => {\n this.prepareInstanceForNextOperation();\n reject(failedToRecordError());\n };\n this.mediaRecorder.onstop = async () => {\n const mt = this.mediaRecorder?.mimeType ?? mimeType;\n const blobVoiceRecording = new Blob(this.chunks, {type: mt});\n if (blobVoiceRecording.size <= 0) {\n this.prepareInstanceForNextOperation();\n reject(emptyRecordingError());\n return;\n }\n\n let uri: string | undefined = undefined;\n let recordDataBase64 = '';\n const fileExtension = (POSSIBLE_MIME_TYPES[mimeType] ?? '').replace(/^\\./, '');\n if (options?.directory) {\n const subDirectory = options.subDirectory?.match(/^\\/?(.+[^/])\\/?$/)?.[1] ?? '';\n const path = `${subDirectory}/recording-${new Date().getTime()}${POSSIBLE_MIME_TYPES[mt]}`;\n\n await write_blob({\n blob: blobVoiceRecording,\n directory: options.directory,\n fast_mode: true,\n path,\n recursive: true,\n });\n\n ({uri} = await Filesystem.getUri({directory: options.directory, path}));\n } else {\n recordDataBase64 = await VoiceRecorderImpl.blobToBase64(blobVoiceRecording);\n }\n\n const recordingDuration = await getBlobDuration(blobVoiceRecording);\n this.prepareInstanceForNextOperation();\n resolve({\n value: {\n recordDataBase64,\n mimeType: mt,\n fileExtension,\n msDuration: recordingDuration * 1000,\n uri\n }\n });\n };\n this.mediaRecorder.ondataavailable = (event: any) => this.chunks.push(event.data);\n this.mediaRecorder.start();\n });\n return successResponse();\n }\n\n /** Handles failures from getUserMedia. */\n private onFailedToStartRecording(): GenericResponse {\n this.prepareInstanceForNextOperation();\n throw failedToRecordError();\n }\n\n /** Converts a Blob payload into a base64 string. */\n private static blobToBase64(blob: Blob): Promise<Base64String> {\n return new Promise((resolve) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const recordingResult = String(reader.result);\n const splitResult = recordingResult.split('base64,');\n const toResolve = splitResult.length > 1 ? splitResult[1] : recordingResult;\n resolve(toResolve.trim());\n };\n reader.readAsDataURL(blob);\n });\n }\n\n /** Connects the recording stream to a lightweight Web Audio analyser for amplitude polling. */\n private startAmplitudeMeter(stream: MediaStream): void {\n try {\n const AudioContextConstructor =\n window.AudioContext ?? (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (AudioContextConstructor == null) {\n return;\n }\n\n this.audioContext = new AudioContextConstructor();\n this.amplitudeSource = this.audioContext.createMediaStreamSource(stream);\n this.amplitudeAnalyser = this.audioContext.createAnalyser();\n this.amplitudeAnalyser.fftSize = 2048;\n this.amplitudeSamples = new Uint8Array(this.amplitudeAnalyser.fftSize);\n this.amplitudeSource.connect(this.amplitudeAnalyser);\n } catch (ignore) {\n this.audioContext = null;\n this.amplitudeAnalyser = null;\n this.amplitudeSource = null;\n this.amplitudeSamples = null;\n }\n }\n\n /** Clamps platform-specific amplitude calculations into the public range. */\n private static clampAmplitude(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(1, Math.max(0, value));\n }\n\n /** Resets state for the next recording attempt. */\n private prepareInstanceForNextOperation(): void {\n if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {\n try {\n this.mediaRecorder.stop();\n } catch (ignore) {\n console.warn('Failed to stop recording during cleanup');\n }\n }\n if (this.amplitudeSource != null) {\n try {\n this.amplitudeSource.disconnect();\n } catch (ignore) {\n // The node may already be disconnected during recorder cleanup.\n }\n }\n if (this.audioContext != null && this.audioContext.state !== 'closed') {\n void this.audioContext.close().catch(() => undefined);\n }\n this.audioContext = null;\n this.amplitudeAnalyser = null;\n this.amplitudeSource = null;\n this.amplitudeSamples = null;\n this.pendingResult = neverResolvingPromise();\n this.mediaRecorder = null;\n this.chunks = [];\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResponseFormat } from '../core/response-format';
|
|
2
|
-
import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../definitions';
|
|
2
|
+
import type { CurrentAmplitude, CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../definitions';
|
|
3
3
|
/** Platform abstraction used by the service layer. */
|
|
4
4
|
export interface VoiceRecorderPlatform {
|
|
5
5
|
/** Checks whether the device can record audio. */
|
|
@@ -18,6 +18,8 @@ export interface VoiceRecorderPlatform {
|
|
|
18
18
|
resumeRecording(): Promise<GenericResponse>;
|
|
19
19
|
/** Returns the current recording state. */
|
|
20
20
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
21
|
+
/** Returns the current input amplitude. */
|
|
22
|
+
getCurrentAmplitude(): Promise<CurrentAmplitude>;
|
|
21
23
|
}
|
|
22
24
|
/** Orchestrates platform calls and normalizes responses when requested. */
|
|
23
25
|
export declare class VoiceRecorderService {
|
|
@@ -42,6 +44,8 @@ export declare class VoiceRecorderService {
|
|
|
42
44
|
resumeRecording(): Promise<GenericResponse>;
|
|
43
45
|
/** Returns the current recording state. */
|
|
44
46
|
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
47
|
+
/** Returns the current input amplitude. */
|
|
48
|
+
getCurrentAmplitude(): Promise<CurrentAmplitude>;
|
|
45
49
|
/** Wraps calls to apply canonical error codes when requested. */
|
|
46
50
|
private execute;
|
|
47
51
|
}
|