@independo/capacitor-voice-recorder 8.1.8 → 8.1.9-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,8 +21,8 @@ The `@independo/capacitor-voice-recorder` plugin allows you to record audio on A
21
21
  ## Installation
22
22
 
23
23
  ```
24
- npm install --save @independo/capacitor-voice-recorder
25
- npx cap sync
24
+ pnpm add @independo/capacitor-voice-recorder
25
+ pnpm exec cap sync
26
26
  ```
27
27
 
28
28
  ### Configuration
@@ -48,7 +48,7 @@ Add the following to your `Info.plist`:
48
48
 
49
49
  - Capacitor 8+
50
50
  - iOS 15+
51
- - Android minSdk 24+; builds require Java 21 (recommended). `npm run verify:android` requires a Java version supported
51
+ - Android minSdk 24+; builds require Java 21 (recommended). `pnpm verify:android` requires a Java version supported
52
52
  by the bundled Gradle wrapper (currently Java 21–24, with Java 21 recommended).
53
53
 
54
54
  ### Compatibility
@@ -93,7 +93,7 @@ export const stopRecording = async () => {
93
93
 
94
94
  ## API
95
95
 
96
- Below is an index of all available methods. Run `npm run docgen` after updating any JSDoc comments to refresh this
96
+ Below is an index of all available methods. Run `pnpm docgen` after updating any JSDoc comments to refresh this
97
97
  section.
98
98
 
99
99
  <docgen-index>
@@ -322,10 +322,11 @@ Interface representing a generic response with a boolean value.
322
322
 
323
323
  Can be used to specify options for the recording.
324
324
 
325
- | Prop | Type | Description |
326
- | ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
327
- | **`directory`** | <code><a href="#directory">Directory</a></code> | The capacitor filesystem directory where the recording should be saved. If not specified, the recording will be stored in a base64 string and returned in the <a href="#recordingdata">`RecordingData`</a> object. |
328
- | **`subDirectory`** | <code>string</code> | An optional subdirectory in the specified directory where the recording should be saved. |
325
+ | Prop | Type | Description |
326
+ | ---------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
327
+ | **`directory`** | <code><a href="#directory">Directory</a></code> | The capacitor filesystem directory where the recording should be saved. If not specified, the recording will be stored in a base64 string and returned in the <a href="#recordingdata">`RecordingData`</a> object. |
328
+ | **`subDirectory`** | <code>string</code> | An optional subdirectory in the specified directory where the recording should be saved. |
329
+ | **`requirePlaybackSupport`** | <code>boolean</code> | Whether the web implementation should require the selected recording MIME type to also be playable by the browser's native HTML `&lt;audio&gt;` element. Defaults to `true` on web to reduce cases where `MediaRecorder` reports support for a format but the recorded file cannot be played back in the same browser (observed on some Safari/iOS/WKWebView combinations). Native platforms ignore this option. |
329
330
 
330
331
 
331
332
  #### RecordingData
@@ -412,8 +413,8 @@ an interruption begins, the recording is paused, the status becomes `INTERRUPTED
412
413
  event fires. When the interruption ends, the `voiceRecordingInterruptionEnded` event fires, and the status stays
413
414
  `INTERRUPTED` until you call `resumeRecording()` or `stopRecording()`. Web does not provide interruption handling.
414
415
 
415
- If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type
416
- `audio/mp4`. Recordings without interruptions remain AAC with MIME type `audio/aac`.
416
+ If interruptions occur on iOS, recordings are segmented and merged when you stop. iOS recordings are normalized to an
417
+ M4A container with MIME type `audio/mp4` for consistent output across interrupted and non-interrupted sessions.
417
418
 
418
419
  ### Web constraints
419
420
 
@@ -422,6 +423,9 @@ If interruptions occur on iOS, recordings are segmented and merged when you stop
422
423
  - The Permissions API is not consistently supported; `hasAudioRecordingPermission()` can reject with
423
424
  `COULD_NOT_QUERY_PERMISSION_STATUS`. In that case, use `requestAudioRecordingPermission()` or `startRecording()` and
424
425
  handle errors.
426
+ - By default, the web implementation picks a MIME type that is supported for both recording (`MediaRecorder`) and
427
+ playback (`<audio>`). You can disable the playback probe with `RecordingOptions.requirePlaybackSupport = false` if
428
+ you prefer recorder-only MIME selection.
425
429
 
426
430
  ## Recording options and storage
427
431
 
@@ -447,9 +451,9 @@ The plugin returns the recording in one of several possible formats. The actual
447
451
  browser capabilities.
448
452
 
449
453
  - Android: `audio/aac`
450
- - iOS: `audio/aac` (or `audio/mp4` when interrupted recordings are merged)
451
- - Web: first supported type from `audio/aac`, `audio/webm;codecs=opus`, `audio/ogg;codecs=opus`, `audio/webm`,
452
- `audio/mp4`
454
+ - iOS: `audio/mp4` (M4A container)
455
+ - Web: first supported MIME type from the plugin's ordered list, with a default preference for formats that are
456
+ reported as playable by the browser `<audio>` element (in addition to `MediaRecorder` support)
453
457
 
454
458
  Because not all devices and browsers support the same formats, recordings may not be playable everywhere. If you need
455
459
  consistent playback across targets, convert recordings to a single format outside this plugin. The plugin focuses on
package/dist/docs.json CHANGED
@@ -315,6 +315,13 @@
315
315
  "docs": "An optional subdirectory in the specified directory where the recording should be saved.",
316
316
  "complexTypes": [],
317
317
  "type": "string | undefined"
318
+ },
319
+ {
320
+ "name": "requirePlaybackSupport",
321
+ "tags": [],
322
+ "docs": "Whether the web implementation should require the selected recording MIME type\nto also be playable by the browser's native HTML `<audio>` element.\n\nDefaults to `true` on web to reduce cases where `MediaRecorder` reports support\nfor a format but the recorded file cannot be played back in the same browser\n(observed on some Safari/iOS/WKWebView combinations).\n\nNative platforms ignore this option.",
323
+ "complexTypes": [],
324
+ "type": "boolean | undefined"
318
325
  }
319
326
  ]
320
327
  },
@@ -19,6 +19,17 @@ export interface RecordingOptions {
19
19
  * An optional subdirectory in the specified directory where the recording should be saved.
20
20
  */
21
21
  subDirectory?: string;
22
+ /**
23
+ * Whether the web implementation should require the selected recording MIME type
24
+ * to also be playable by the browser's native HTML `<audio>` element.
25
+ *
26
+ * Defaults to `true` on web to reduce cases where `MediaRecorder` reports support
27
+ * for a format but the recorded file cannot be played back in the same browser
28
+ * (observed on some Safari/iOS/WKWebView combinations).
29
+ *
30
+ * Native platforms ignore this option.
31
+ */
32
+ requirePlaybackSupport?: boolean;
22
33
  }
23
34
  /**
24
35
  * Interface representing the data of a recording.
@@ -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/**\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"]}
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 /**\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,23 +1,64 @@
1
1
  import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../../definitions';
2
- /** Preferred MIME types to probe in order of fallback. */
3
- declare const POSSIBLE_MIME_TYPES: {
4
- 'audio/aac': string;
5
- 'audio/webm;codecs=opus': string;
6
- 'audio/mp4': string;
7
- 'audio/webm': string;
8
- 'audio/ogg;codecs=opus': string;
9
- };
2
+ /**
3
+ * Ordered MIME types to probe for audio recording via `MediaRecorder.isTypeSupported()`.
4
+ *
5
+ * ⚠️ The order is intentional and MUST remain stable unless you also update the
6
+ * selection policy in code and test on Safari/iOS + WebViews.
7
+ *
8
+ * ✅ What this list is used for
9
+ * - Selecting a `mimeType` for `new MediaRecorder(stream, { mimeType })`.
10
+ *
11
+ * ❌ What this list does NOT guarantee
12
+ * - It does NOT guarantee that the recorded output will be playable via the
13
+ * HTML `<audio>` element in the same browser.
14
+ *
15
+ * Real-world caveat (important):
16
+ * - We have observed cases where `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')`
17
+ * returned `true`, the recorder produced a Blob, but `<audio>` could not play it.
18
+ * This can happen due to container/codec playback support differences, platform
19
+ * quirks (especially Safari/iOS / WKWebView), or incomplete WebM playback support.
20
+ *
21
+ * Current selection behavior in this implementation:
22
+ * - By default, MIME selection treats recorder support and playback support as separate
23
+ * capabilities and probes both:
24
+ * - Recorder capability: `MediaRecorder.isTypeSupported(type)`
25
+ * - Playback capability: `audio.canPlayType(type)`
26
+ * - This default can be disabled via `RecordingOptions.requirePlaybackSupport = false`
27
+ * to fall back to recorder-only probing.
28
+ *
29
+ * Keeping legacy keys:
30
+ * - Some entries are kept even if they overlap (e.g. `audio/mp4` and explicit codec),
31
+ * to maximize compatibility across differing browser implementations.
32
+ */
33
+ declare const POSSIBLE_MIME_TYPES: Record<string, string>;
10
34
  /** Browser implementation backed by MediaRecorder and Capacitor Filesystem. */
11
35
  export declare class VoiceRecorderImpl {
36
+ /** Default behavior for web MIME selection: require recorder + playback support. */
37
+ private static readonly DEFAULT_REQUIRE_PLAYBACK_SUPPORT;
12
38
  /** Active MediaRecorder instance, if recording. */
13
39
  private mediaRecorder;
14
40
  /** Collected data chunks from MediaRecorder. */
15
41
  private chunks;
16
42
  /** Promise resolved when the recorder stops and payload is ready. */
17
43
  private pendingResult;
18
- /** Returns whether the browser can start a recording session. */
19
- static canDeviceVoiceRecord(): Promise<GenericResponse>;
20
- /** Starts a recording session using MediaRecorder. */
44
+ /**
45
+ * Returns whether the browser can start a recording session.
46
+ *
47
+ * On web this checks:
48
+ * - `navigator.mediaDevices.getUserMedia`
49
+ * - at least one supported recording MIME type using {@link getSupportedMimeType}
50
+ *
51
+ * The optional `requirePlaybackSupport` flag is forwarded to MIME selection and defaults
52
+ * to `true` when omitted.
53
+ */
54
+ static canDeviceVoiceRecord(options?: Pick<RecordingOptions, 'requirePlaybackSupport'>): Promise<GenericResponse>;
55
+ /**
56
+ * Starts a recording session using `MediaRecorder`.
57
+ *
58
+ * The selected MIME type is resolved once at start time (using the optional
59
+ * `requirePlaybackSupport` flag from `RecordingOptions`) and reused for the final Blob
60
+ * and file extension to keep the recording payload internally consistent.
61
+ */
21
62
  startRecording(options?: RecordingOptions): Promise<GenericResponse>;
22
63
  /** Stops the current recording and resolves the pending payload. */
23
64
  stopRecording(): Promise<RecordingData>;
@@ -31,8 +72,41 @@ export declare class VoiceRecorderImpl {
31
72
  resumeRecording(): Promise<GenericResponse>;
32
73
  /** Returns the current recording status from MediaRecorder. */
33
74
  getCurrentStatus(): Promise<CurrentRecordingStatus>;
34
- /** Returns the first supported MIME type, if any. */
35
- static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(): T | null;
75
+ /**
76
+ * Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current
77
+ * environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,
78
+ * optionally requiring native HTML `<audio>` playback support too.
79
+ *
80
+ * The search order is the iteration order of {@link POSSIBLE_MIME_TYPES}.
81
+ *
82
+ * @typeParam T - A MIME type string that exists as a key in {@link POSSIBLE_MIME_TYPES}.
83
+ *
84
+ * @returns The first supported MIME type for `MediaRecorder`, or `null` if:
85
+ * - `MediaRecorder` is unavailable, or
86
+ * - no configured MIME types are supported.
87
+ *
88
+ * ⚠️ Important: `MediaRecorder` support ≠ `<audio>` playback support
89
+ *
90
+ * Some browsers/platforms can claim support for recording a format (notably WebM/Opus)
91
+ * but still fail to play the resulting Blob through the native HTML audio pipeline.
92
+ * This mismatch is especially likely on Safari/iOS / WKWebView variants, so the default
93
+ * behavior also probes `HTMLAudioElement.canPlayType(type)` when available.
94
+ *
95
+ * Selection policy when playback probing is enabled:
96
+ * - keep the global priority order from {@link POSSIBLE_MIME_TYPES}
97
+ * - among recordable types, prefer the first `"probably"` playable candidate
98
+ * - otherwise return the first `"maybe"` playable candidate
99
+ * - treat `""` as not playable
100
+ *
101
+ * Note: The <audio> element is never attached to the DOM, so it won't appear to users or assistive tech.
102
+ *
103
+ * Fallback behavior:
104
+ * - If `document` / `audio.canPlayType` is unavailable (e.g. SSR-like environments),
105
+ * this falls back to record-only probing.
106
+ */
107
+ static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(options?: {
108
+ requirePlaybackSupport?: boolean;
109
+ }): T | null;
36
110
  /** Initializes MediaRecorder and wires up handlers. */
37
111
  private onSuccessfullyStartedRecording;
38
112
  /** Handles failures from getUserMedia. */
@@ -2,13 +2,51 @@ import { Filesystem } from '@capacitor/filesystem';
2
2
  import write_blob from 'capacitor-blob-writer';
3
3
  import getBlobDuration from './get-blob-duration';
4
4
  import { alreadyRecordingError, couldNotQueryPermissionStatusError, deviceCannotVoiceRecordError, emptyRecordingError, failedToFetchRecordingError, failedToRecordError, failureResponse, missingPermissionError, recordingHasNotStartedError, successResponse, } from './predefined-web-responses';
5
- /** Preferred MIME types to probe in order of fallback. */
5
+ /**
6
+ * Ordered MIME types to probe for audio recording via `MediaRecorder.isTypeSupported()`.
7
+ *
8
+ * ⚠️ The order is intentional and MUST remain stable unless you also update the
9
+ * selection policy in code and test on Safari/iOS + WebViews.
10
+ *
11
+ * ✅ What this list is used for
12
+ * - Selecting a `mimeType` for `new MediaRecorder(stream, { mimeType })`.
13
+ *
14
+ * ❌ What this list does NOT guarantee
15
+ * - It does NOT guarantee that the recorded output will be playable via the
16
+ * HTML `<audio>` element in the same browser.
17
+ *
18
+ * Real-world caveat (important):
19
+ * - We have observed cases where `MediaRecorder.isTypeSupported('audio/webm;codecs=opus')`
20
+ * returned `true`, the recorder produced a Blob, but `<audio>` could not play it.
21
+ * This can happen due to container/codec playback support differences, platform
22
+ * quirks (especially Safari/iOS / WKWebView), or incomplete WebM playback support.
23
+ *
24
+ * Current selection behavior in this implementation:
25
+ * - By default, MIME selection treats recorder support and playback support as separate
26
+ * capabilities and probes both:
27
+ * - Recorder capability: `MediaRecorder.isTypeSupported(type)`
28
+ * - Playback capability: `audio.canPlayType(type)`
29
+ * - This default can be disabled via `RecordingOptions.requirePlaybackSupport = false`
30
+ * to fall back to recorder-only probing.
31
+ *
32
+ * Keeping legacy keys:
33
+ * - Some entries are kept even if they overlap (e.g. `audio/mp4` and explicit codec),
34
+ * to maximize compatibility across differing browser implementations.
35
+ */
6
36
  const POSSIBLE_MIME_TYPES = {
7
- 'audio/aac': '.aac',
8
- 'audio/webm;codecs=opus': '.ogg',
9
- 'audio/mp4': '.mp3',
10
- 'audio/webm': '.ogg',
11
- 'audio/ogg;codecs=opus': '.ogg',
37
+ // ✅ Most universal
38
+ 'audio/mp4;codecs="mp4a.40.2"': '.m4a', // AAC in MP4 (explicit codec helps detection)
39
+ 'audio/mp4': '.m4a', // (legacy key kept; broad support)
40
+ 'audio/aac': '.aac', // (legacy key kept; less common in the wild)
41
+ 'audio/mpeg': '.mp3', // MP3 (universal)
42
+ 'audio/wav': '.wav', // WAV (universal, big files)
43
+ // ✅ Modern high-quality (very widely supported, but slightly less “universal” than MP3/AAC)
44
+ 'audio/webm;codecs="opus"': '.webm', // Opus in WebM (explicit codec helps detection)
45
+ 'audio/webm;codecs=opus': '.webm', // (legacy key kept)
46
+ 'audio/webm': '.webm', // (legacy key kept; container-only, codec-dependent)
47
+ // ⚠️ Least universal (Safari/iOS historically the limiting factor)
48
+ 'audio/ogg;codecs=opus': '.ogg', // (legacy key kept)
49
+ 'audio/ogg;codecs=vorbis': '.ogg', // Ogg Vorbis (weakest mainstream support)
12
50
  };
13
51
  /** Creates a promise that never resolves. */
14
52
  const neverResolvingPromise = () => new Promise(() => undefined);
@@ -22,22 +60,40 @@ export class VoiceRecorderImpl {
22
60
  /** Promise resolved when the recorder stops and payload is ready. */
23
61
  this.pendingResult = neverResolvingPromise();
24
62
  }
25
- /** Returns whether the browser can start a recording session. */
26
- static async canDeviceVoiceRecord() {
63
+ /**
64
+ * Returns whether the browser can start a recording session.
65
+ *
66
+ * On web this checks:
67
+ * - `navigator.mediaDevices.getUserMedia`
68
+ * - at least one supported recording MIME type using {@link getSupportedMimeType}
69
+ *
70
+ * The optional `requirePlaybackSupport` flag is forwarded to MIME selection and defaults
71
+ * to `true` when omitted.
72
+ */
73
+ static async canDeviceVoiceRecord(options) {
27
74
  var _a;
28
- if (((_a = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia) == null || VoiceRecorderImpl.getSupportedMimeType() == null) {
75
+ if (((_a = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia) == null ||
76
+ VoiceRecorderImpl.getSupportedMimeType({
77
+ requirePlaybackSupport: options === null || options === void 0 ? void 0 : options.requirePlaybackSupport,
78
+ }) == null) {
29
79
  return failureResponse();
30
80
  }
31
81
  else {
32
82
  return successResponse();
33
83
  }
34
84
  }
35
- /** Starts a recording session using MediaRecorder. */
85
+ /**
86
+ * Starts a recording session using `MediaRecorder`.
87
+ *
88
+ * The selected MIME type is resolved once at start time (using the optional
89
+ * `requirePlaybackSupport` flag from `RecordingOptions`) and reused for the final Blob
90
+ * and file extension to keep the recording payload internally consistent.
91
+ */
36
92
  async startRecording(options) {
37
93
  if (this.mediaRecorder != null) {
38
94
  throw alreadyRecordingError();
39
95
  }
40
- const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord();
96
+ const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord(options);
41
97
  if (!deviceCanRecord.value) {
42
98
  throw deviceCannotVoiceRecordError();
43
99
  }
@@ -139,30 +195,91 @@ export class VoiceRecorderImpl {
139
195
  return Promise.resolve({ status: 'NONE' });
140
196
  }
141
197
  }
142
- /** Returns the first supported MIME type, if any. */
143
- static getSupportedMimeType() {
198
+ /**
199
+ * Returns the first MIME type (key of {@link POSSIBLE_MIME_TYPES}) that the current
200
+ * environment reports as supported for recording via `MediaRecorder.isTypeSupported()`,
201
+ * optionally requiring native HTML `<audio>` playback support too.
202
+ *
203
+ * The search order is the iteration order of {@link POSSIBLE_MIME_TYPES}.
204
+ *
205
+ * @typeParam T - A MIME type string that exists as a key in {@link POSSIBLE_MIME_TYPES}.
206
+ *
207
+ * @returns The first supported MIME type for `MediaRecorder`, or `null` if:
208
+ * - `MediaRecorder` is unavailable, or
209
+ * - no configured MIME types are supported.
210
+ *
211
+ * ⚠️ Important: `MediaRecorder` support ≠ `<audio>` playback support
212
+ *
213
+ * Some browsers/platforms can claim support for recording a format (notably WebM/Opus)
214
+ * but still fail to play the resulting Blob through the native HTML audio pipeline.
215
+ * This mismatch is especially likely on Safari/iOS / WKWebView variants, so the default
216
+ * behavior also probes `HTMLAudioElement.canPlayType(type)` when available.
217
+ *
218
+ * Selection policy when playback probing is enabled:
219
+ * - keep the global priority order from {@link POSSIBLE_MIME_TYPES}
220
+ * - among recordable types, prefer the first `"probably"` playable candidate
221
+ * - otherwise return the first `"maybe"` playable candidate
222
+ * - treat `""` as not playable
223
+ *
224
+ * Note: The <audio> element is never attached to the DOM, so it won't appear to users or assistive tech.
225
+ *
226
+ * Fallback behavior:
227
+ * - If `document` / `audio.canPlayType` is unavailable (e.g. SSR-like environments),
228
+ * this falls back to record-only probing.
229
+ */
230
+ static getSupportedMimeType(options) {
231
+ var _a, _b, _c, _d, _e;
144
232
  if ((MediaRecorder === null || MediaRecorder === void 0 ? void 0 : MediaRecorder.isTypeSupported) == null)
145
233
  return null;
146
- const foundSupportedType = Object.keys(POSSIBLE_MIME_TYPES).find((type) => MediaRecorder.isTypeSupported(type));
147
- return foundSupportedType !== null && foundSupportedType !== void 0 ? foundSupportedType : null;
234
+ const orderedTypes = Object.keys(POSSIBLE_MIME_TYPES);
235
+ const recordSupportedTypes = orderedTypes.filter((type) => MediaRecorder.isTypeSupported(type));
236
+ if (recordSupportedTypes.length === 0)
237
+ return null;
238
+ const requirePlaybackSupport = (_a = options === null || options === void 0 ? void 0 : options.requirePlaybackSupport) !== null && _a !== void 0 ? _a : VoiceRecorderImpl.DEFAULT_REQUIRE_PLAYBACK_SUPPORT;
239
+ if (!requirePlaybackSupport) {
240
+ return (_b = recordSupportedTypes[0]) !== null && _b !== void 0 ? _b : null;
241
+ }
242
+ if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
243
+ return (_c = recordSupportedTypes[0]) !== null && _c !== void 0 ? _c : null;
244
+ }
245
+ const audioElement = document.createElement('audio');
246
+ if (typeof audioElement.canPlayType !== 'function') {
247
+ return (_d = recordSupportedTypes[0]) !== null && _d !== void 0 ? _d : null;
248
+ }
249
+ let firstProbably = null;
250
+ let firstMaybe = null;
251
+ for (const type of recordSupportedTypes) {
252
+ const playbackSupport = audioElement.canPlayType(type);
253
+ if (playbackSupport === 'probably') {
254
+ firstProbably = type;
255
+ break;
256
+ }
257
+ if (playbackSupport === 'maybe' && firstMaybe == null) {
258
+ firstMaybe = type;
259
+ }
260
+ }
261
+ return (_e = firstProbably !== null && firstProbably !== void 0 ? firstProbably : firstMaybe) !== null && _e !== void 0 ? _e : null;
148
262
  }
149
263
  /** Initializes MediaRecorder and wires up handlers. */
150
264
  onSuccessfullyStartedRecording(stream, options) {
151
265
  this.pendingResult = new Promise((resolve, reject) => {
152
- this.mediaRecorder = new MediaRecorder(stream);
266
+ const mimeType = VoiceRecorderImpl.getSupportedMimeType({
267
+ requirePlaybackSupport: options === null || options === void 0 ? void 0 : options.requirePlaybackSupport,
268
+ });
269
+ if (mimeType == null) {
270
+ this.prepareInstanceForNextOperation();
271
+ reject(failedToRecordError());
272
+ return;
273
+ }
274
+ this.mediaRecorder = new MediaRecorder(stream, { mimeType });
153
275
  this.mediaRecorder.onerror = () => {
154
276
  this.prepareInstanceForNextOperation();
155
277
  reject(failedToRecordError());
156
278
  };
157
279
  this.mediaRecorder.onstop = async () => {
158
- var _a, _b, _c;
159
- const mimeType = VoiceRecorderImpl.getSupportedMimeType();
160
- if (mimeType == null) {
161
- this.prepareInstanceForNextOperation();
162
- reject(failedToFetchRecordingError());
163
- return;
164
- }
165
- const blobVoiceRecording = new Blob(this.chunks, { type: mimeType });
280
+ var _a, _b, _c, _d, _e;
281
+ const mt = (_b = (_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.mimeType) !== null && _b !== void 0 ? _b : mimeType;
282
+ const blobVoiceRecording = new Blob(this.chunks, { type: mt });
166
283
  if (blobVoiceRecording.size <= 0) {
167
284
  this.prepareInstanceForNextOperation();
168
285
  reject(emptyRecordingError());
@@ -171,8 +288,8 @@ export class VoiceRecorderImpl {
171
288
  let uri = undefined;
172
289
  let recordDataBase64 = '';
173
290
  if (options === null || options === void 0 ? void 0 : options.directory) {
174
- const subDirectory = (_c = (_b = (_a = options.subDirectory) === null || _a === void 0 ? void 0 : _a.match(/^\/?(.+[^/])\/?$/)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : '';
175
- const path = `${subDirectory}/recording-${new Date().getTime()}${POSSIBLE_MIME_TYPES[mimeType]}`;
291
+ const subDirectory = (_e = (_d = (_c = options.subDirectory) === null || _c === void 0 ? void 0 : _c.match(/^\/?(.+[^/])\/?$/)) === null || _d === void 0 ? void 0 : _d[1]) !== null && _e !== void 0 ? _e : '';
292
+ const path = `${subDirectory}/recording-${new Date().getTime()}${POSSIBLE_MIME_TYPES[mt]}`;
176
293
  await write_blob({
177
294
  blob: blobVoiceRecording,
178
295
  directory: options.directory,
@@ -187,7 +304,14 @@ export class VoiceRecorderImpl {
187
304
  }
188
305
  const recordingDuration = await getBlobDuration(blobVoiceRecording);
189
306
  this.prepareInstanceForNextOperation();
190
- resolve({ value: { recordDataBase64, mimeType, msDuration: recordingDuration * 1000, uri } });
307
+ resolve({
308
+ value: {
309
+ recordDataBase64,
310
+ mimeType: mt,
311
+ msDuration: recordingDuration * 1000,
312
+ uri
313
+ }
314
+ });
191
315
  };
192
316
  this.mediaRecorder.ondataavailable = (event) => this.chunks.push(event.data);
193
317
  this.mediaRecorder.start();
@@ -227,4 +351,6 @@ export class VoiceRecorderImpl {
227
351
  this.chunks = [];
228
352
  }
229
353
  }
354
+ /** Default behavior for web MIME selection: require recorder + playback support. */
355
+ VoiceRecorderImpl.DEFAULT_REQUIRE_PLAYBACK_SUPPORT = true;
230
356
  //# sourceMappingURL=VoiceRecorderImpl.js.map