@independo/capacitor-voice-recorder 8.1.0-dev.1 → 8.1.0-dev.3
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/LICENSE +21 -0
- package/README.md +40 -30
- 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/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/platform}/CustomMediaRecorder.java +33 -2
- 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/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/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} +1 -0
- package/ios/Sources/VoiceRecorder/Core/ErrorCodes.swift +16 -0
- package/ios/Sources/VoiceRecorder/{Messages.swift → Core/Messages.swift} +1 -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/{CustomMediaRecorder.swift → Platform/CustomMediaRecorder.swift} +67 -12
- 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 -8
- 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 -205
- 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/RecordOptions.swift +0 -8
- package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +0 -170
- /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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Independo GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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
|
|
|
@@ -371,36 +372,44 @@ Event payload for voiceRecordingInterruptionEnded event (empty - no data).
|
|
|
371
372
|
|
|
372
373
|
## Audio interruption handling
|
|
373
374
|
|
|
374
|
-
On iOS and Android, the plugin listens for system audio interruptions (phone calls, other apps taking audio focus). When
|
|
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.
|
|
375
379
|
|
|
376
|
-
If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type
|
|
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`.
|
|
377
382
|
|
|
383
|
+
## Format and MIME type
|
|
378
384
|
|
|
379
|
-
|
|
385
|
+
The plugin returns the recording in one of several possible formats. The actual MIME type depends on the platform and
|
|
386
|
+
browser capabilities.
|
|
380
387
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
note that these 3 browsers has been tested on. the plugin should still work on
|
|
386
|
-
other browsers, as there is a list of mime types that the plugin checks against the
|
|
387
|
-
user's browser.
|
|
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`
|
|
388
392
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
as this plugin focuses on the recording aspect, it does not provide any conversion between formats.
|
|
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.
|
|
393
396
|
|
|
394
397
|
## Playback
|
|
395
398
|
|
|
396
|
-
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.
|
|
397
401
|
|
|
398
402
|
```typescript
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
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();
|
|
404
413
|
```
|
|
405
414
|
|
|
406
415
|
## Compatibility
|
|
@@ -414,9 +423,10 @@ Versioning follows Capacitor versioning. Major versions of the plugin are compat
|
|
|
414
423
|
| 7.* | 7 |
|
|
415
424
|
| 8.* | 8 |
|
|
416
425
|
|
|
417
|
-
##
|
|
426
|
+
## Origins and credit
|
|
418
427
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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", 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";
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
package
|
|
1
|
+
package app.independo.capacitorvoicerecorder.core;
|
|
2
2
|
|
|
3
3
|
import com.getcapacitor.JSObject;
|
|
4
4
|
|
|
5
|
+
/** Recording payload returned to the bridge layer. */
|
|
5
6
|
public class RecordData {
|
|
6
7
|
|
|
8
|
+
/** File URI when returning recordings by reference. */
|
|
7
9
|
private String uri;
|
|
10
|
+
/** Base64 payload for inline recording data. */
|
|
8
11
|
private String recordDataBase64;
|
|
12
|
+
/** MIME type of the audio payload. */
|
|
9
13
|
private String mimeType;
|
|
14
|
+
/** Recording duration in milliseconds. */
|
|
10
15
|
private int msDuration;
|
|
11
16
|
|
|
12
17
|
public RecordData() {}
|
|
@@ -18,6 +23,7 @@ public class RecordData {
|
|
|
18
23
|
this.uri = uri;
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
/** Returns the base64 payload, if present. */
|
|
21
27
|
public String getRecordDataBase64() {
|
|
22
28
|
return recordDataBase64;
|
|
23
29
|
}
|
|
@@ -26,6 +32,7 @@ public class RecordData {
|
|
|
26
32
|
this.recordDataBase64 = recordDataBase64;
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
/** Returns the recording duration in milliseconds. */
|
|
29
36
|
public int getMsDuration() {
|
|
30
37
|
return msDuration;
|
|
31
38
|
}
|
|
@@ -34,6 +41,7 @@ public class RecordData {
|
|
|
34
41
|
this.msDuration = msDuration;
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
/** Returns the MIME type of the recording. */
|
|
37
45
|
public String getMimeType() {
|
|
38
46
|
return mimeType;
|
|
39
47
|
}
|
|
@@ -42,6 +50,12 @@ public class RecordData {
|
|
|
42
50
|
this.mimeType = mimeType;
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/** Returns the file URI, if present. */
|
|
54
|
+
public String getUri() {
|
|
55
|
+
return uri;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Serializes the record data into the legacy JS payload shape. */
|
|
45
59
|
public JSObject toJSObject() {
|
|
46
60
|
JSObject toReturn = new JSObject();
|
|
47
61
|
toReturn.put("recordDataBase64", recordDataBase64);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package app.independo.capacitorvoicerecorder.core;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.PluginConfig;
|
|
4
|
+
|
|
5
|
+
/** Supported response payload shapes. */
|
|
6
|
+
public enum ResponseFormat {
|
|
7
|
+
LEGACY,
|
|
8
|
+
NORMALIZED;
|
|
9
|
+
|
|
10
|
+
/** Reads the response format from plugin configuration. */
|
|
11
|
+
public static ResponseFormat fromConfig(PluginConfig config) {
|
|
12
|
+
String value = config.getString("responseFormat", "legacy");
|
|
13
|
+
if ("normalized".equalsIgnoreCase(value)) {
|
|
14
|
+
return NORMALIZED;
|
|
15
|
+
}
|
|
16
|
+
return LEGACY;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,34 +1,40 @@
|
|
|
1
|
-
package
|
|
1
|
+
package app.independo.capacitorvoicerecorder.core;
|
|
2
2
|
|
|
3
3
|
import com.getcapacitor.JSObject;
|
|
4
4
|
|
|
5
|
+
/** Helper for building JS payloads in the legacy response shape. */
|
|
5
6
|
public class ResponseGenerator {
|
|
6
7
|
|
|
7
8
|
private static final String VALUE_RESPONSE_KEY = "value";
|
|
8
9
|
private static final String STATUS_RESPONSE_KEY = "status";
|
|
9
10
|
|
|
11
|
+
/** Wraps a boolean value into the response shape. */
|
|
10
12
|
public static JSObject fromBoolean(boolean value) {
|
|
11
13
|
return value ? successResponse() : failResponse();
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/** Returns a success response with value=true. */
|
|
14
17
|
public static JSObject successResponse() {
|
|
15
18
|
JSObject success = new JSObject();
|
|
16
19
|
success.put(VALUE_RESPONSE_KEY, true);
|
|
17
20
|
return success;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** Returns a failure response with value=false. */
|
|
20
24
|
public static JSObject failResponse() {
|
|
21
25
|
JSObject success = new JSObject();
|
|
22
26
|
success.put(VALUE_RESPONSE_KEY, false);
|
|
23
27
|
return success;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/** Wraps arbitrary data into the response shape. */
|
|
26
31
|
public static JSObject dataResponse(Object data) {
|
|
27
32
|
JSObject success = new JSObject();
|
|
28
33
|
success.put(VALUE_RESPONSE_KEY, data);
|
|
29
34
|
return success;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/** Wraps the recording status into the response shape. */
|
|
32
38
|
public static JSObject statusResponse(CurrentRecordingStatus status) {
|
|
33
39
|
JSObject success = new JSObject();
|
|
34
40
|
success.put(STATUS_RESPONSE_KEY, status.name());
|