@independo/capacitor-voice-recorder 8.0.2-dev.1 → 8.1.0-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 +130 -32
- package/android/build.gradle +44 -1
- package/android/src/main/java/app/independo/capacitorvoicerecorder/VoiceRecorder.java +146 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/PermissionChecker.java +8 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecordDataMapper.java +32 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java +39 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderPlatform.java +25 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/core/CurrentRecordingStatus.java +9 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ErrorCodes.java +19 -0
- package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/Messages.java +2 -1
- package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/RecordData.java +15 -1
- package/android/src/main/java/app/independo/capacitorvoicerecorder/core/RecordOptions.java +4 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ResponseFormat.java +18 -0
- package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/ResponseGenerator.java +7 -1
- package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/CustomMediaRecorder.java +281 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/DefaultRecorderPlatform.java +86 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/NotSupportedOsVersion.java +4 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderService.java +144 -0
- package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderServiceException.java +23 -0
- package/dist/docs.json +145 -5
- package/dist/esm/adapters/VoiceRecorderWebAdapter.d.ts +23 -0
- package/dist/esm/adapters/VoiceRecorderWebAdapter.js +41 -0
- package/dist/esm/adapters/VoiceRecorderWebAdapter.js.map +1 -0
- package/dist/esm/core/error-codes.d.ts +4 -0
- package/dist/esm/core/error-codes.js +21 -0
- package/dist/esm/core/error-codes.js.map +1 -0
- package/dist/esm/core/recording-contract.d.ts +3 -0
- package/dist/esm/core/recording-contract.js +15 -0
- package/dist/esm/core/recording-contract.js.map +1 -0
- package/dist/esm/core/response-format.d.ts +8 -0
- package/dist/esm/core/response-format.js +17 -0
- package/dist/esm/core/response-format.js.map +1 -0
- package/dist/esm/definitions.d.ts +36 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/platform/web/VoiceRecorderImpl.d.ts +45 -0
- package/dist/esm/{VoiceRecorderImpl.js → platform/web/VoiceRecorderImpl.js} +20 -2
- package/dist/esm/platform/web/VoiceRecorderImpl.js.map +1 -0
- package/dist/esm/platform/web/get-blob-duration.js.map +1 -0
- package/dist/esm/{predefined-web-responses.d.ts → platform/web/predefined-web-responses.d.ts} +12 -1
- package/dist/esm/{predefined-web-responses.js → platform/web/predefined-web-responses.js} +11 -0
- package/dist/esm/platform/web/predefined-web-responses.js.map +1 -0
- package/dist/esm/service/VoiceRecorderService.d.ts +47 -0
- package/dist/esm/service/VoiceRecorderService.js +60 -0
- package/dist/esm/service/VoiceRecorderService.js.map +1 -0
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +26 -12
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +200 -9
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +200 -9
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/VoiceRecorder/Adapters/DefaultRecorderPlatform.swift +33 -0
- package/ios/Sources/VoiceRecorder/Adapters/RecordDataMapper.swift +38 -0
- package/ios/Sources/VoiceRecorder/Adapters/RecorderAdapter.swift +24 -0
- package/ios/Sources/VoiceRecorder/Adapters/RecorderPlatform.swift +11 -0
- package/ios/Sources/VoiceRecorder/Bridge/VoiceRecorder.swift +172 -0
- package/ios/Sources/VoiceRecorder/{CurrentRecordingStatus.swift → Core/CurrentRecordingStatus.swift} +2 -0
- package/ios/Sources/VoiceRecorder/Core/ErrorCodes.swift +16 -0
- package/ios/Sources/VoiceRecorder/{Messages.swift → Core/Messages.swift} +2 -0
- package/ios/Sources/VoiceRecorder/{RecordData.swift → Core/RecordData.swift} +6 -0
- package/ios/Sources/VoiceRecorder/Core/RecordOptions.swift +11 -0
- package/ios/Sources/VoiceRecorder/Core/ResponseFormat.swift +22 -0
- package/ios/Sources/VoiceRecorder/{ResponseGenerator.swift → Core/ResponseGenerator.swift} +6 -0
- package/ios/Sources/VoiceRecorder/Platform/CustomMediaRecorder.swift +359 -0
- package/ios/Sources/VoiceRecorder/Service/VoiceRecorderService.swift +128 -0
- package/ios/Sources/VoiceRecorder/Service/VoiceRecorderServiceError.swift +14 -0
- package/package.json +10 -4
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +0 -7
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CustomMediaRecorder.java +0 -149
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/NotSupportedOsVersion.java +0 -3
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/RecordOptions.java +0 -3
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +0 -203
- package/dist/esm/VoiceRecorderImpl.d.ts +0 -27
- package/dist/esm/VoiceRecorderImpl.js.map +0 -1
- package/dist/esm/helper/get-blob-duration.js.map +0 -1
- package/dist/esm/predefined-web-responses.js.map +0 -1
- package/ios/Sources/VoiceRecorder/CustomMediaRecorder.swift +0 -113
- package/ios/Sources/VoiceRecorder/RecordOptions.swift +0 -8
- package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +0 -147
- /package/dist/esm/{helper → platform/web}/get-blob-duration.d.ts +0 -0
- /package/dist/esm/{helper → platform/web}/get-blob-duration.js +0 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<br>
|
|
12
12
|
<a href="https://www.npmjs.com/package/@independo/capacitor-voice-recorder"><img src="https://img.shields.io/npm/dw/@independo/capacitor-voice-recorder" alt="" role="presentation" /></a>
|
|
13
13
|
<a href="https://www.npmjs.com/package/@independo/capacitor-voice-recorder"><img src="https://img.shields.io/npm/v/@independo/capacitor-voice-recorder" alt="" role="presentation" /></a>
|
|
14
|
+
<a href="https://codecov.io/gh/independo-gmbh/capacitor-voice-recorder/branch/main"><img src="https://codecov.io/gh/independo-gmbh/capacitor-voice-recorder/branch/main/graph/badge.svg" alt="Coverage Badge: main" /></a>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
@@ -24,8 +25,8 @@ npx cap sync
|
|
|
24
25
|
|
|
25
26
|
- Capacitor 8+
|
|
26
27
|
- iOS 15+
|
|
27
|
-
- Android minSdk 24
|
|
28
|
-
|
|
28
|
+
- Android minSdk 24+; builds require Java 21 (recommended). `npm run verify:android` requires a Java version supported
|
|
29
|
+
by the bundled Gradle wrapper (currently Java 21–24, with Java 21 recommended).
|
|
29
30
|
|
|
30
31
|
## iOS Package Manager Support
|
|
31
32
|
|
|
@@ -41,7 +42,7 @@ This plugin supports both CocoaPods and Swift Package Manager (SPM) on iOS.
|
|
|
41
42
|
Add the following to your `AndroidManifest.xml`:
|
|
42
43
|
|
|
43
44
|
```xml
|
|
44
|
-
<uses-permission android:name="android.permission.RECORD_AUDIO"
|
|
45
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
### Using with iOS
|
|
@@ -49,7 +50,6 @@ Add the following to your `AndroidManifest.xml`:
|
|
|
49
50
|
Add the following to your `Info.plist`:
|
|
50
51
|
|
|
51
52
|
```xml
|
|
52
|
-
|
|
53
53
|
<key>NSMicrophoneUsageDescription</key>
|
|
54
54
|
<string>This app uses the microphone to record audio.</string>
|
|
55
55
|
```
|
|
@@ -60,7 +60,8 @@ The `@independo/capacitor-voice-recorder` plugin allows you to record audio on A
|
|
|
60
60
|
|
|
61
61
|
## API
|
|
62
62
|
|
|
63
|
-
Below is an index of all available methods. Run `npm run docgen` after updating any JSDoc comments to refresh this
|
|
63
|
+
Below is an index of all available methods. Run `npm run docgen` after updating any JSDoc comments to refresh this
|
|
64
|
+
section.
|
|
64
65
|
|
|
65
66
|
<docgen-index>
|
|
66
67
|
|
|
@@ -72,6 +73,9 @@ Below is an index of all available methods. Run `npm run docgen` after updating
|
|
|
72
73
|
* [`pauseRecording()`](#pauserecording)
|
|
73
74
|
* [`resumeRecording()`](#resumerecording)
|
|
74
75
|
* [`getCurrentStatus()`](#getcurrentstatus)
|
|
76
|
+
* [`addListener('voiceRecordingInterrupted', ...)`](#addlistenervoicerecordinginterrupted-)
|
|
77
|
+
* [`addListener('voiceRecordingInterruptionEnded', ...)`](#addlistenervoicerecordinginterruptionended-)
|
|
78
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
75
79
|
* [Interfaces](#interfaces)
|
|
76
80
|
* [Type Aliases](#type-aliases)
|
|
77
81
|
* [Enums](#enums)
|
|
@@ -162,6 +166,7 @@ Will stop the recording that has been previously started.
|
|
|
162
166
|
If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.
|
|
163
167
|
If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.
|
|
164
168
|
In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.
|
|
169
|
+
On iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.
|
|
165
170
|
In case of success, the promise resolves to <a href="#recordingdata">RecordingData</a> containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.
|
|
166
171
|
|
|
167
172
|
**Returns:** <code>Promise<<a href="#recordingdata">RecordingData</a>></code>
|
|
@@ -191,7 +196,7 @@ On certain mobile OS versions, this function is not supported and will reject wi
|
|
|
191
196
|
resumeRecording() => Promise<GenericResponse>
|
|
192
197
|
```
|
|
193
198
|
|
|
194
|
-
Resumes a paused audio recording.
|
|
199
|
+
Resumes a paused or interrupted audio recording.
|
|
195
200
|
If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.
|
|
196
201
|
On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.
|
|
197
202
|
On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.
|
|
@@ -212,12 +217,62 @@ Will resolve with one of the following values:
|
|
|
212
217
|
`{ status: "NONE" }` if the plugin is idle and waiting to start a new recording.
|
|
213
218
|
`{ status: "RECORDING" }` if the plugin is in the middle of recording.
|
|
214
219
|
`{ status: "PAUSED" }` if the recording is paused.
|
|
220
|
+
`{ status: "INTERRUPTED" }` if the recording was paused due to a system interruption.
|
|
215
221
|
|
|
216
222
|
**Returns:** <code>Promise<<a href="#currentrecordingstatus">CurrentRecordingStatus</a>></code>
|
|
217
223
|
|
|
218
224
|
--------------------
|
|
219
225
|
|
|
220
226
|
|
|
227
|
+
### addListener('voiceRecordingInterrupted', ...)
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
addListener(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void) => Promise<PluginListenerHandle>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).
|
|
234
|
+
Available on iOS and Android only.
|
|
235
|
+
|
|
236
|
+
| Param | Type | Description |
|
|
237
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
238
|
+
| **`eventName`** | <code>'voiceRecordingInterrupted'</code> | The name of the event to listen for. |
|
|
239
|
+
| **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptedevent">VoiceRecordingInterruptedEvent</a>) => void</code> | The callback function to invoke when the event occurs. |
|
|
240
|
+
|
|
241
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
242
|
+
|
|
243
|
+
--------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
### addListener('voiceRecordingInterruptionEnded', ...)
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
addListener(eventName: 'voiceRecordingInterruptionEnded', listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void) => Promise<PluginListenerHandle>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Listen for audio recording interruption end events.
|
|
253
|
+
Available on iOS and Android only.
|
|
254
|
+
|
|
255
|
+
| Param | Type | Description |
|
|
256
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
257
|
+
| **`eventName`** | <code>'voiceRecordingInterruptionEnded'</code> | The name of the event to listen for. |
|
|
258
|
+
| **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptionendedevent">VoiceRecordingInterruptionEndedEvent</a>) => void</code> | The callback function to invoke when the event occurs. |
|
|
259
|
+
|
|
260
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
261
|
+
|
|
262
|
+
--------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
### removeAllListeners()
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
removeAllListeners() => Promise<void>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Remove all listeners for this plugin.
|
|
272
|
+
|
|
273
|
+
--------------------
|
|
274
|
+
|
|
275
|
+
|
|
221
276
|
### Interfaces
|
|
222
277
|
|
|
223
278
|
|
|
@@ -253,9 +308,16 @@ Interface representing the data of a recording.
|
|
|
253
308
|
|
|
254
309
|
Interface representing the current status of the voice recorder.
|
|
255
310
|
|
|
256
|
-
| Prop | Type
|
|
257
|
-
| ------------ |
|
|
258
|
-
| **`status`** | <code>'NONE' \| 'RECORDING' \| 'PAUSED'</code> | The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'. |
|
|
311
|
+
| Prop | Type | Description |
|
|
312
|
+
| ------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
313
|
+
| **`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'. |
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
#### PluginListenerHandle
|
|
317
|
+
|
|
318
|
+
| Prop | Type |
|
|
319
|
+
| ------------ | ----------------------------------------- |
|
|
320
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
259
321
|
|
|
260
322
|
|
|
261
323
|
### Type Aliases
|
|
@@ -268,6 +330,27 @@ Represents a Base64 encoded string.
|
|
|
268
330
|
<code>string</code>
|
|
269
331
|
|
|
270
332
|
|
|
333
|
+
#### VoiceRecordingInterruptedEvent
|
|
334
|
+
|
|
335
|
+
Event payload for voiceRecordingInterrupted event (empty - no data).
|
|
336
|
+
|
|
337
|
+
<code><a href="#record">Record</a><string, never></code>
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
#### Record
|
|
341
|
+
|
|
342
|
+
Construct a type with a set of properties K of type T
|
|
343
|
+
|
|
344
|
+
<code>{
|
|
271
345
|
[P in K]: T;
|
|
272
346
|
}</code>
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
#### VoiceRecordingInterruptionEndedEvent
|
|
350
|
+
|
|
351
|
+
Event payload for voiceRecordingInterruptionEnded event (empty - no data).
|
|
352
|
+
|
|
353
|
+
<code><a href="#record">Record</a><string, never></code>
|
|
354
|
+
|
|
355
|
+
|
|
273
356
|
### Enums
|
|
274
357
|
|
|
275
358
|
|
|
@@ -287,32 +370,46 @@ Represents a Base64 encoded string.
|
|
|
287
370
|
|
|
288
371
|
</docgen-api>
|
|
289
372
|
|
|
373
|
+
## Audio interruption handling
|
|
374
|
+
|
|
375
|
+
On iOS and Android, the plugin listens for system audio interruptions (phone calls, other apps taking audio focus). When
|
|
376
|
+
an interruption begins, the recording is paused, the status becomes `INTERRUPTED`, and the `voiceRecordingInterrupted`
|
|
377
|
+
event fires. When the interruption ends, the `voiceRecordingInterruptionEnded` event fires, and the status stays
|
|
378
|
+
`INTERRUPTED` until you call `resumeRecording()` or `stopRecording()`. Web does not provide interruption handling.
|
|
379
|
+
|
|
380
|
+
If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type
|
|
381
|
+
`audio/mp4`. Recordings without interruptions remain AAC with MIME type `audio/aac`.
|
|
290
382
|
|
|
291
|
-
## Format and
|
|
383
|
+
## Format and MIME type
|
|
292
384
|
|
|
293
|
-
The plugin
|
|
294
|
-
|
|
295
|
-
on android and ios the mime type will be `audio/aac`, while on chrome and firefox it
|
|
296
|
-
will be `audio/webm;codecs=opus` and on safari it will be `audio/mp4`.
|
|
297
|
-
note that these 3 browsers has been tested on. the plugin should still work on
|
|
298
|
-
other browsers, as there is a list of mime types that the plugin checks against the
|
|
299
|
-
user's browser.
|
|
385
|
+
The plugin returns the recording in one of several possible formats. The actual MIME type depends on the platform and
|
|
386
|
+
browser capabilities.
|
|
300
387
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
388
|
+
- Android: `audio/aac`
|
|
389
|
+
- iOS: `audio/aac` (or `audio/mp4` when interrupted recordings are merged)
|
|
390
|
+
- Web: first supported type from `audio/aac`, `audio/webm;codecs=opus`, `audio/ogg;codecs=opus`, `audio/webm`,
|
|
391
|
+
`audio/mp4`
|
|
392
|
+
|
|
393
|
+
Because not all devices and browsers support the same formats, recordings may not be playable everywhere. If you need
|
|
394
|
+
consistent playback across targets, convert recordings to a single format outside this plugin. The plugin focuses on
|
|
395
|
+
recording only and does not perform format conversion.
|
|
305
396
|
|
|
306
397
|
## Playback
|
|
307
398
|
|
|
308
|
-
To play
|
|
399
|
+
To play a recording, prefer `uri` when available. On native platforms, pass it through
|
|
400
|
+
`Capacitor.convertFileSrc` before using it in the web view.
|
|
309
401
|
|
|
310
402
|
```typescript
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
403
|
+
import {Capacitor} from '@capacitor/core';
|
|
404
|
+
|
|
405
|
+
const {recordDataBase64, mimeType, uri} = result.value;
|
|
406
|
+
const source = uri
|
|
407
|
+
? Capacitor.convertFileSrc(uri)
|
|
408
|
+
: `data:${mimeType};base64,${recordDataBase64}`;
|
|
409
|
+
|
|
410
|
+
const audioRef = new Audio(source);
|
|
411
|
+
audioRef.oncanplaythrough = () => audioRef.play();
|
|
412
|
+
audioRef.load();
|
|
316
413
|
```
|
|
317
414
|
|
|
318
415
|
## Compatibility
|
|
@@ -326,9 +423,10 @@ Versioning follows Capacitor versioning. Major versions of the plugin are compat
|
|
|
326
423
|
| 7.* | 7 |
|
|
327
424
|
| 8.* | 8 |
|
|
328
425
|
|
|
329
|
-
##
|
|
426
|
+
## Origins and credit
|
|
330
427
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
428
|
+
This project started as a fork of [
|
|
429
|
+
`tchvu3/capacitor-voice-recorder`](https://github.com/tchvu3/capacitor-voice-recorder).
|
|
430
|
+
Thanks to Avihu Harush for the original implementation and community groundwork. Since then, the plugin has been
|
|
431
|
+
re-architected for improved performance, reliability, and testability (service/adapters split, contract tests, and a
|
|
432
|
+
normalized response path). The codebase now diverges substantially, which is why this repo left the fork network.
|
package/android/build.gradle
CHANGED
|
@@ -16,9 +16,10 @@ buildscript {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
apply plugin: 'com.android.library'
|
|
19
|
+
apply plugin: 'jacoco'
|
|
19
20
|
|
|
20
21
|
android {
|
|
21
|
-
namespace = "
|
|
22
|
+
namespace = "app.independo.capacitorvoicerecorder"
|
|
22
23
|
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
23
24
|
defaultConfig {
|
|
24
25
|
minSdk = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
@@ -33,6 +34,14 @@ android {
|
|
|
33
34
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
testOptions {
|
|
38
|
+
unitTests.all {
|
|
39
|
+
jacoco {
|
|
40
|
+
includeNoLocationClasses = true
|
|
41
|
+
excludes = ['jdk.internal.*']
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
lint {
|
|
37
46
|
abortOnError = false
|
|
38
47
|
}
|
|
@@ -42,6 +51,10 @@ android {
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
jacoco {
|
|
55
|
+
toolVersion = '0.8.11'
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
repositories {
|
|
46
59
|
google()
|
|
47
60
|
mavenCentral()
|
|
@@ -53,6 +66,36 @@ dependencies {
|
|
|
53
66
|
implementation project(':capacitor-android')
|
|
54
67
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
55
68
|
testImplementation "junit:junit:$junitVersion"
|
|
69
|
+
testImplementation "org.json:json:20240303"
|
|
56
70
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
57
71
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
58
72
|
}
|
|
73
|
+
|
|
74
|
+
tasks.register('jacocoTestReport', JacocoReport) {
|
|
75
|
+
dependsOn tasks.named('testDebugUnitTest')
|
|
76
|
+
|
|
77
|
+
def fileFilter = [
|
|
78
|
+
'**/R.class',
|
|
79
|
+
'**/R$*.class',
|
|
80
|
+
'**/BuildConfig.*',
|
|
81
|
+
'**/Manifest*.*',
|
|
82
|
+
'**/*Test*.*',
|
|
83
|
+
'android/**/*.*',
|
|
84
|
+
]
|
|
85
|
+
def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes", excludes: fileFilter)
|
|
86
|
+
def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
|
|
87
|
+
classDirectories.setFrom(files([javaClasses, kotlinClasses]))
|
|
88
|
+
sourceDirectories.setFrom(files(['src/main/java', 'src/main/kotlin']))
|
|
89
|
+
executionData.setFrom(
|
|
90
|
+
fileTree(dir: buildDir, includes: [
|
|
91
|
+
'jacoco/testDebugUnitTest.exec',
|
|
92
|
+
'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec',
|
|
93
|
+
]),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
reports {
|
|
97
|
+
xml.required = true
|
|
98
|
+
html.required = true
|
|
99
|
+
csv.required = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import app.independo.capacitorvoicerecorder.adapters.PermissionChecker;
|
|
5
|
+
import app.independo.capacitorvoicerecorder.adapters.RecordDataMapper;
|
|
6
|
+
import app.independo.capacitorvoicerecorder.adapters.RecorderPlatform;
|
|
7
|
+
import app.independo.capacitorvoicerecorder.core.ErrorCodes;
|
|
8
|
+
import app.independo.capacitorvoicerecorder.core.Messages;
|
|
9
|
+
import app.independo.capacitorvoicerecorder.core.RecordData;
|
|
10
|
+
import app.independo.capacitorvoicerecorder.core.RecordOptions;
|
|
11
|
+
import app.independo.capacitorvoicerecorder.core.ResponseFormat;
|
|
12
|
+
import app.independo.capacitorvoicerecorder.core.ResponseGenerator;
|
|
13
|
+
import app.independo.capacitorvoicerecorder.platform.DefaultRecorderPlatform;
|
|
14
|
+
import app.independo.capacitorvoicerecorder.service.VoiceRecorderService;
|
|
15
|
+
import app.independo.capacitorvoicerecorder.service.VoiceRecorderServiceException;
|
|
16
|
+
import com.getcapacitor.PermissionState;
|
|
17
|
+
import com.getcapacitor.Plugin;
|
|
18
|
+
import com.getcapacitor.PluginCall;
|
|
19
|
+
import com.getcapacitor.PluginMethod;
|
|
20
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
21
|
+
import com.getcapacitor.annotation.Permission;
|
|
22
|
+
import com.getcapacitor.annotation.PermissionCallback;
|
|
23
|
+
|
|
24
|
+
@CapacitorPlugin(
|
|
25
|
+
name = "VoiceRecorder",
|
|
26
|
+
permissions = { @Permission(alias = VoiceRecorder.RECORD_AUDIO_ALIAS, strings = { Manifest.permission.RECORD_AUDIO }) }
|
|
27
|
+
)
|
|
28
|
+
/** Capacitor bridge for the VoiceRecorder plugin. */
|
|
29
|
+
public class VoiceRecorder extends Plugin {
|
|
30
|
+
|
|
31
|
+
/** Permission alias used by the Capacitor permission API. */
|
|
32
|
+
static final String RECORD_AUDIO_ALIAS = "voice recording";
|
|
33
|
+
/** Service layer that owns recording flows and validation. */
|
|
34
|
+
private VoiceRecorderService service;
|
|
35
|
+
/** Response format derived from plugin configuration. */
|
|
36
|
+
private ResponseFormat responseFormat;
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
public void load() {
|
|
40
|
+
super.load();
|
|
41
|
+
responseFormat = ResponseFormat.fromConfig(getConfig());
|
|
42
|
+
RecorderPlatform platform = new DefaultRecorderPlatform(getContext());
|
|
43
|
+
PermissionChecker permissionChecker = this::doesUserGaveAudioRecordingPermission;
|
|
44
|
+
service = new VoiceRecorderService(platform, permissionChecker);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Checks whether the device can record audio. */
|
|
48
|
+
@PluginMethod
|
|
49
|
+
public void canDeviceVoiceRecord(PluginCall call) {
|
|
50
|
+
call.resolve(ResponseGenerator.fromBoolean(service.canDeviceVoiceRecord()));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Requests microphone permission or returns success if already granted. */
|
|
54
|
+
@PluginMethod
|
|
55
|
+
public void requestAudioRecordingPermission(PluginCall call) {
|
|
56
|
+
if (service.hasAudioRecordingPermission()) {
|
|
57
|
+
call.resolve(ResponseGenerator.successResponse());
|
|
58
|
+
} else {
|
|
59
|
+
requestPermissionForAlias(RECORD_AUDIO_ALIAS, call, "recordAudioPermissionCallback");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Forwards permission results back to the JS call. */
|
|
64
|
+
@PermissionCallback
|
|
65
|
+
private void recordAudioPermissionCallback(PluginCall call) {
|
|
66
|
+
this.hasAudioRecordingPermission(call);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Returns whether the app has microphone permission. */
|
|
70
|
+
@PluginMethod
|
|
71
|
+
public void hasAudioRecordingPermission(PluginCall call) {
|
|
72
|
+
call.resolve(ResponseGenerator.fromBoolean(service.hasAudioRecordingPermission()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Starts a recording session. */
|
|
76
|
+
@PluginMethod
|
|
77
|
+
public void startRecording(PluginCall call) {
|
|
78
|
+
try {
|
|
79
|
+
String directory = call.getString("directory");
|
|
80
|
+
String subDirectory = call.getString("subDirectory");
|
|
81
|
+
RecordOptions options = new RecordOptions(directory, subDirectory);
|
|
82
|
+
service.startRecording(
|
|
83
|
+
options,
|
|
84
|
+
() -> notifyListeners("voiceRecordingInterrupted", null),
|
|
85
|
+
() -> notifyListeners("voiceRecordingInterruptionEnded", null)
|
|
86
|
+
);
|
|
87
|
+
call.resolve(ResponseGenerator.successResponse());
|
|
88
|
+
} catch (VoiceRecorderServiceException exp) {
|
|
89
|
+
call.reject(toLegacyMessage(exp.getCode()), exp.getCode(), exp);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Stops recording and returns the recording payload. */
|
|
94
|
+
@PluginMethod
|
|
95
|
+
public void stopRecording(PluginCall call) {
|
|
96
|
+
try {
|
|
97
|
+
RecordData recordData = service.stopRecording();
|
|
98
|
+
if (responseFormat == ResponseFormat.NORMALIZED) {
|
|
99
|
+
call.resolve(ResponseGenerator.dataResponse(RecordDataMapper.toNormalizedJSObject(recordData)));
|
|
100
|
+
} else {
|
|
101
|
+
call.resolve(ResponseGenerator.dataResponse(RecordDataMapper.toLegacyJSObject(recordData)));
|
|
102
|
+
}
|
|
103
|
+
} catch (VoiceRecorderServiceException exp) {
|
|
104
|
+
call.reject(toLegacyMessage(exp.getCode()), exp.getCode(), exp);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Pauses an active recording session if supported. */
|
|
109
|
+
@PluginMethod
|
|
110
|
+
public void pauseRecording(PluginCall call) {
|
|
111
|
+
try {
|
|
112
|
+
call.resolve(ResponseGenerator.fromBoolean(service.pauseRecording()));
|
|
113
|
+
} catch (VoiceRecorderServiceException exception) {
|
|
114
|
+
call.reject(toLegacyMessage(exception.getCode()), exception.getCode(), exception);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Resumes a paused recording session if supported. */
|
|
119
|
+
@PluginMethod
|
|
120
|
+
public void resumeRecording(PluginCall call) {
|
|
121
|
+
try {
|
|
122
|
+
call.resolve(ResponseGenerator.fromBoolean(service.resumeRecording()));
|
|
123
|
+
} catch (VoiceRecorderServiceException exception) {
|
|
124
|
+
call.reject(toLegacyMessage(exception.getCode()), exception.getCode(), exception);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Returns the current recording status. */
|
|
129
|
+
@PluginMethod
|
|
130
|
+
public void getCurrentStatus(PluginCall call) {
|
|
131
|
+
call.resolve(ResponseGenerator.statusResponse(service.getCurrentStatus()));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Checks whether the app has the RECORD_AUDIO permission. */
|
|
135
|
+
private boolean doesUserGaveAudioRecordingPermission() {
|
|
136
|
+
return getPermissionState(VoiceRecorder.RECORD_AUDIO_ALIAS).equals(PermissionState.GRANTED);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Maps canonical error codes back to legacy error messages. */
|
|
140
|
+
private String toLegacyMessage(String canonicalCode) {
|
|
141
|
+
if (ErrorCodes.DEVICE_CANNOT_VOICE_RECORD.equals(canonicalCode)) {
|
|
142
|
+
return Messages.CANNOT_RECORD_ON_THIS_PHONE;
|
|
143
|
+
}
|
|
144
|
+
return canonicalCode;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/PermissionChecker.java
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.adapters;
|
|
2
|
+
|
|
3
|
+
/** Functional interface used to check microphone permission. */
|
|
4
|
+
@FunctionalInterface
|
|
5
|
+
public interface PermissionChecker {
|
|
6
|
+
/** Returns whether the app currently has microphone permission. */
|
|
7
|
+
boolean hasAudioPermission();
|
|
8
|
+
}
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecordDataMapper.java
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.adapters;
|
|
2
|
+
|
|
3
|
+
import app.independo.capacitorvoicerecorder.core.RecordData;
|
|
4
|
+
import com.getcapacitor.JSObject;
|
|
5
|
+
|
|
6
|
+
/** Maps internal record data into legacy or normalized JS payloads. */
|
|
7
|
+
public final class RecordDataMapper {
|
|
8
|
+
|
|
9
|
+
private RecordDataMapper() {}
|
|
10
|
+
|
|
11
|
+
/** Converts record data to the legacy payload shape. */
|
|
12
|
+
public static JSObject toLegacyJSObject(RecordData recordData) {
|
|
13
|
+
return recordData.toJSObject();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Converts record data to the normalized payload shape. */
|
|
17
|
+
public static JSObject toNormalizedJSObject(RecordData recordData) {
|
|
18
|
+
JSObject normalized = new JSObject();
|
|
19
|
+
normalized.put("msDuration", recordData.getMsDuration());
|
|
20
|
+
normalized.put("mimeType", recordData.getMimeType());
|
|
21
|
+
|
|
22
|
+
String uri = recordData.getUri();
|
|
23
|
+
String recordDataBase64 = recordData.getRecordDataBase64();
|
|
24
|
+
if (uri != null && !uri.isEmpty()) {
|
|
25
|
+
normalized.put("uri", uri);
|
|
26
|
+
} else if (recordDataBase64 != null && !recordDataBase64.isEmpty()) {
|
|
27
|
+
normalized.put("recordDataBase64", recordDataBase64);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.adapters;
|
|
2
|
+
|
|
3
|
+
import app.independo.capacitorvoicerecorder.core.CurrentRecordingStatus;
|
|
4
|
+
import app.independo.capacitorvoicerecorder.core.RecordOptions;
|
|
5
|
+
import app.independo.capacitorvoicerecorder.platform.NotSupportedOsVersion;
|
|
6
|
+
import java.io.File;
|
|
7
|
+
|
|
8
|
+
/** Recorder abstraction used by the service layer. */
|
|
9
|
+
public interface RecorderAdapter {
|
|
10
|
+
/** Sets a callback invoked when interruptions begin. */
|
|
11
|
+
void setOnInterruptionBegan(Runnable callback);
|
|
12
|
+
|
|
13
|
+
/** Sets a callback invoked when interruptions end. */
|
|
14
|
+
void setOnInterruptionEnded(Runnable callback);
|
|
15
|
+
|
|
16
|
+
/** Starts recording audio. */
|
|
17
|
+
void startRecording();
|
|
18
|
+
|
|
19
|
+
/** Stops recording audio. */
|
|
20
|
+
void stopRecording();
|
|
21
|
+
|
|
22
|
+
/** Pauses recording if supported. */
|
|
23
|
+
boolean pauseRecording() throws NotSupportedOsVersion;
|
|
24
|
+
|
|
25
|
+
/** Resumes recording if supported. */
|
|
26
|
+
boolean resumeRecording() throws NotSupportedOsVersion;
|
|
27
|
+
|
|
28
|
+
/** Returns the current recording status. */
|
|
29
|
+
CurrentRecordingStatus getCurrentStatus();
|
|
30
|
+
|
|
31
|
+
/** Returns the output file for the recording. */
|
|
32
|
+
File getOutputFile();
|
|
33
|
+
|
|
34
|
+
/** Returns the options used to start recording. */
|
|
35
|
+
RecordOptions getRecordOptions();
|
|
36
|
+
|
|
37
|
+
/** Deletes the output file from disk. */
|
|
38
|
+
boolean deleteOutputFile();
|
|
39
|
+
}
|
package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderPlatform.java
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.adapters;
|
|
2
|
+
|
|
3
|
+
import app.independo.capacitorvoicerecorder.core.RecordOptions;
|
|
4
|
+
import java.io.File;
|
|
5
|
+
|
|
6
|
+
/** Platform abstraction for device and file operations. */
|
|
7
|
+
public interface RecorderPlatform {
|
|
8
|
+
/** Returns whether the device can record audio. */
|
|
9
|
+
boolean canDeviceVoiceRecord();
|
|
10
|
+
|
|
11
|
+
/** Returns true when the microphone is in use elsewhere. */
|
|
12
|
+
boolean isMicrophoneOccupied();
|
|
13
|
+
|
|
14
|
+
/** Creates a recorder instance for the given options. */
|
|
15
|
+
RecorderAdapter createRecorder(RecordOptions options) throws Exception;
|
|
16
|
+
|
|
17
|
+
/** Reads the recording file as base64, or null on failure. */
|
|
18
|
+
String readFileAsBase64(File recordedFile);
|
|
19
|
+
|
|
20
|
+
/** Returns the recording duration in milliseconds. */
|
|
21
|
+
int getDurationMs(File recordedFile);
|
|
22
|
+
|
|
23
|
+
/** Returns the URI for the recording file. */
|
|
24
|
+
String toUri(File recordedFile);
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.core;
|
|
2
|
+
|
|
3
|
+
/** Canonical error codes returned by the plugin. */
|
|
4
|
+
public final class ErrorCodes {
|
|
5
|
+
|
|
6
|
+
public static final String MISSING_PERMISSION = "MISSING_PERMISSION";
|
|
7
|
+
public static final String ALREADY_RECORDING = "ALREADY_RECORDING";
|
|
8
|
+
public static final String MICROPHONE_BEING_USED = "MICROPHONE_BEING_USED";
|
|
9
|
+
public static final String DEVICE_CANNOT_VOICE_RECORD = "DEVICE_CANNOT_VOICE_RECORD";
|
|
10
|
+
public static final String FAILED_TO_RECORD = "FAILED_TO_RECORD";
|
|
11
|
+
public static final String EMPTY_RECORDING = "EMPTY_RECORDING";
|
|
12
|
+
public static final String RECORDING_HAS_NOT_STARTED = "RECORDING_HAS_NOT_STARTED";
|
|
13
|
+
public static final String FAILED_TO_FETCH_RECORDING = "FAILED_TO_FETCH_RECORDING";
|
|
14
|
+
public static final String FAILED_TO_MERGE_RECORDING = "FAILED_TO_MERGE_RECORDING";
|
|
15
|
+
public static final String NOT_SUPPORTED_OS_VERSION = "NOT_SUPPORTED_OS_VERSION";
|
|
16
|
+
public static final String COULD_NOT_QUERY_PERMISSION_STATUS = "COULD_NOT_QUERY_PERMISSION_STATUS";
|
|
17
|
+
|
|
18
|
+
private ErrorCodes() {}
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
package
|
|
1
|
+
package app.independo.capacitorvoicerecorder.core;
|
|
2
2
|
|
|
3
|
+
/** Legacy error messages preserved for backward compatibility. */
|
|
3
4
|
public abstract class Messages {
|
|
4
5
|
|
|
5
6
|
public static final String MISSING_PERMISSION = "MISSING_PERMISSION";
|