@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Service layer that orchestrates recording operations.
|
|
4
|
+
final class VoiceRecorderService {
|
|
5
|
+
/// Platform adapter for device and file operations.
|
|
6
|
+
private let platform: RecorderPlatform
|
|
7
|
+
/// Closure used to check microphone permission.
|
|
8
|
+
private let permissionChecker: () -> Bool
|
|
9
|
+
/// Factory for creating recorder adapters.
|
|
10
|
+
private let recorderFactory: () -> RecorderAdapter
|
|
11
|
+
/// Active recorder instance for the current session.
|
|
12
|
+
private var recorder: RecorderAdapter?
|
|
13
|
+
|
|
14
|
+
init(
|
|
15
|
+
platform: RecorderPlatform,
|
|
16
|
+
permissionChecker: @escaping () -> Bool,
|
|
17
|
+
recorderFactory: @escaping () -> RecorderAdapter = { CustomMediaRecorder() }
|
|
18
|
+
) {
|
|
19
|
+
self.platform = platform
|
|
20
|
+
self.permissionChecker = permissionChecker
|
|
21
|
+
self.recorderFactory = recorderFactory
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Returns whether the device can record audio.
|
|
25
|
+
func canDeviceVoiceRecord() -> Bool {
|
|
26
|
+
return platform.canDeviceVoiceRecord()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Returns whether the app has microphone permission.
|
|
30
|
+
func hasAudioRecordingPermission() -> Bool {
|
|
31
|
+
return permissionChecker()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Starts a recording session or throws a service error.
|
|
35
|
+
func startRecording(
|
|
36
|
+
options: RecordOptions?,
|
|
37
|
+
onInterruptionBegan: @escaping () -> Void,
|
|
38
|
+
onInterruptionEnded: @escaping () -> Void
|
|
39
|
+
) throws {
|
|
40
|
+
if !platform.canDeviceVoiceRecord() {
|
|
41
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.deviceCannotVoiceRecord)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if !permissionChecker() {
|
|
45
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.missingPermission)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if recorder != nil {
|
|
49
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.alreadyRecording)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let nextRecorder = recorderFactory()
|
|
53
|
+
nextRecorder.onInterruptionBegan = onInterruptionBegan
|
|
54
|
+
nextRecorder.onInterruptionEnded = onInterruptionEnded
|
|
55
|
+
let started = nextRecorder.startRecording(recordOptions: options)
|
|
56
|
+
if !started {
|
|
57
|
+
recorder = nil
|
|
58
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.deviceCannotVoiceRecord)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
recorder = nextRecorder
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Stops recording and returns the payload asynchronously.
|
|
65
|
+
func stopRecording(completion: @escaping (Result<RecordData, VoiceRecorderServiceError>) -> Void) {
|
|
66
|
+
guard let recorder = recorder else {
|
|
67
|
+
completion(.failure(VoiceRecorderServiceError(code: ErrorCodes.recordingHasNotStarted)))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
recorder.stopRecording { [weak self] stopSuccess in
|
|
72
|
+
guard let self = self else {
|
|
73
|
+
completion(.failure(VoiceRecorderServiceError(code: ErrorCodes.failedToFetchRecording)))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if !stopSuccess {
|
|
78
|
+
self.recorder = nil
|
|
79
|
+
completion(.failure(VoiceRecorderServiceError(code: ErrorCodes.failedToMergeRecording)))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let audioFileUrl = recorder.getOutputFile()
|
|
84
|
+
let fileExtension = audioFileUrl.pathExtension.lowercased()
|
|
85
|
+
let mimeType = fileExtension == "m4a" ? "audio/mp4" : "audio/aac"
|
|
86
|
+
let sendDataAsBase64 = recorder.options?.directory == nil
|
|
87
|
+
let recordDataBase64 = sendDataAsBase64 ? self.platform.readFileAsBase64(audioFileUrl) : nil
|
|
88
|
+
let uri = sendDataAsBase64 ? nil : audioFileUrl.path
|
|
89
|
+
let recordData = RecordData(
|
|
90
|
+
recordDataBase64: recordDataBase64,
|
|
91
|
+
mimeType: mimeType,
|
|
92
|
+
msDuration: self.platform.getDurationMs(audioFileUrl),
|
|
93
|
+
uri: uri
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self.recorder = nil
|
|
97
|
+
if (sendDataAsBase64 && recordData.recordDataBase64 == nil) || recordData.msDuration < 0 {
|
|
98
|
+
completion(.failure(VoiceRecorderServiceError(code: ErrorCodes.emptyRecording)))
|
|
99
|
+
} else {
|
|
100
|
+
completion(.success(recordData))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Pauses the active recording session.
|
|
106
|
+
func pauseRecording() throws -> Bool {
|
|
107
|
+
guard let recorder = recorder else {
|
|
108
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.recordingHasNotStarted)
|
|
109
|
+
}
|
|
110
|
+
return recorder.pauseRecording()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Resumes a paused recording session.
|
|
114
|
+
func resumeRecording() throws -> Bool {
|
|
115
|
+
guard let recorder = recorder else {
|
|
116
|
+
throw VoiceRecorderServiceError(code: ErrorCodes.recordingHasNotStarted)
|
|
117
|
+
}
|
|
118
|
+
return recorder.resumeRecording()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Returns the current recording status.
|
|
122
|
+
func getCurrentStatus() -> CurrentRecordingStatus {
|
|
123
|
+
guard let recorder = recorder else {
|
|
124
|
+
return .NONE
|
|
125
|
+
}
|
|
126
|
+
return recorder.getCurrentStatus()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Error wrapper that carries a canonical error code.
|
|
4
|
+
struct VoiceRecorderServiceError: Error {
|
|
5
|
+
/// Canonical error code for bridge mapping.
|
|
6
|
+
let code: String
|
|
7
|
+
/// Underlying error, when available.
|
|
8
|
+
let underlyingError: Error?
|
|
9
|
+
|
|
10
|
+
init(code: String, underlyingError: Error? = nil) {
|
|
11
|
+
self.code = code
|
|
12
|
+
self.underlyingError = underlyingError
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/independo-gmbh/capacitor-voice-recorder.git"
|
|
14
14
|
},
|
|
15
15
|
"description": "Capacitor plugin for voice recording",
|
|
16
|
-
"version": "8.0
|
|
16
|
+
"version": "8.1.0-dev.2",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@capacitor/android": "^8.0.0",
|
|
19
19
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
@@ -60,12 +60,18 @@
|
|
|
60
60
|
"prepublishOnly": "npm run build",
|
|
61
61
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
62
62
|
"verify:android": "node scripts/verify-android.js",
|
|
63
|
+
"verify:ios": "xcodebuild -scheme IndependoCapacitorVoiceRecorder -destination generic/platform=iOS",
|
|
64
|
+
"verify:web": "npm run build",
|
|
63
65
|
"watch": "tsc --watch",
|
|
64
|
-
"test": "
|
|
66
|
+
"test": "npm run test:web && npm run test:android && npm run test:ios",
|
|
67
|
+
"test:web": "jest",
|
|
68
|
+
"test:web:coverage": "jest --coverage --coverageReporters=lcov --coverageReporters=text-summary",
|
|
69
|
+
"test:android": "node scripts/verify-android.js testDebugUnitTest",
|
|
70
|
+
"test:android:coverage": "node scripts/verify-android.js testDebugUnitTest jacocoTestReport",
|
|
71
|
+
"test:ios": "node scripts/test-ios.js",
|
|
72
|
+
"test:ios:coverage": "node scripts/test-ios.js --coverage --result-bundle-path coverage/ios.xcresult && node scripts/xccov-to-cobertura.js --xcresult coverage/ios.xcresult --output coverage/ios-cobertura.xml",
|
|
65
73
|
"eslint": "eslint . --ext ts",
|
|
66
|
-
"verify:ios": "xcodebuild -scheme IndependoCapacitorVoiceRecorder -destination generic/platform=iOS",
|
|
67
74
|
"clean": "rm -rf dist",
|
|
68
|
-
"verify:web": "npm run build",
|
|
69
75
|
"swiftlint": "node-swiftlint",
|
|
70
76
|
"docgen": "docgen --api VoiceRecorderPlugin --output-readme README.md --output-json dist/docs.json",
|
|
71
77
|
"fmt": "npm run eslint -- --fix"
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
package com.tchvu3.capacitorvoicerecorder;
|
|
2
|
-
|
|
3
|
-
import android.content.Context;
|
|
4
|
-
import android.media.MediaRecorder;
|
|
5
|
-
import android.os.Build;
|
|
6
|
-
import android.os.Environment;
|
|
7
|
-
import java.io.File;
|
|
8
|
-
import java.io.IOException;
|
|
9
|
-
import java.util.regex.Matcher;
|
|
10
|
-
import java.util.regex.Pattern;
|
|
11
|
-
|
|
12
|
-
public class CustomMediaRecorder {
|
|
13
|
-
|
|
14
|
-
private final Context context;
|
|
15
|
-
private final RecordOptions options;
|
|
16
|
-
private MediaRecorder mediaRecorder;
|
|
17
|
-
private File outputFile;
|
|
18
|
-
private CurrentRecordingStatus currentRecordingStatus = CurrentRecordingStatus.NONE;
|
|
19
|
-
|
|
20
|
-
public CustomMediaRecorder(Context context, RecordOptions options) throws IOException {
|
|
21
|
-
this.context = context;
|
|
22
|
-
this.options = options;
|
|
23
|
-
generateMediaRecorder();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private void generateMediaRecorder() throws IOException {
|
|
27
|
-
mediaRecorder = new MediaRecorder();
|
|
28
|
-
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
|
29
|
-
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
|
|
30
|
-
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
|
31
|
-
mediaRecorder.setAudioEncodingBitRate(96000);
|
|
32
|
-
mediaRecorder.setAudioSamplingRate(44100);
|
|
33
|
-
setRecorderOutputFile();
|
|
34
|
-
mediaRecorder.prepare();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private void setRecorderOutputFile() throws IOException {
|
|
38
|
-
File outputDir = context.getCacheDir();
|
|
39
|
-
|
|
40
|
-
String directory = options.directory();
|
|
41
|
-
String subDirectory = options.subDirectory();
|
|
42
|
-
|
|
43
|
-
if (directory != null) {
|
|
44
|
-
outputDir = this.getDirectory(directory);
|
|
45
|
-
if (subDirectory != null) {
|
|
46
|
-
Pattern pattern = Pattern.compile("^/?(.+[^/])/?$");
|
|
47
|
-
Matcher matcher = pattern.matcher(subDirectory);
|
|
48
|
-
if (matcher.matches()) {
|
|
49
|
-
outputDir = new File(outputDir, matcher.group(1));
|
|
50
|
-
if (!outputDir.exists()) {
|
|
51
|
-
outputDir.mkdirs();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
outputFile = File.createTempFile(String.format("recording-%d", System.currentTimeMillis()), ".aac", outputDir);
|
|
58
|
-
|
|
59
|
-
if (directory == null) {
|
|
60
|
-
outputFile.deleteOnExit();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
mediaRecorder.setOutputFile(outputFile.getAbsolutePath());
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private File getDirectory(String directory) {
|
|
67
|
-
return switch (directory) {
|
|
68
|
-
case "DOCUMENTS" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
|
|
69
|
-
case "DATA", "LIBRARY" -> context.getFilesDir();
|
|
70
|
-
case "CACHE" -> context.getCacheDir();
|
|
71
|
-
case "EXTERNAL" -> context.getExternalFilesDir(null);
|
|
72
|
-
case "EXTERNAL_STORAGE" -> Environment.getExternalStorageDirectory();
|
|
73
|
-
default -> null;
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public void startRecording() {
|
|
78
|
-
mediaRecorder.start();
|
|
79
|
-
currentRecordingStatus = CurrentRecordingStatus.RECORDING;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public void stopRecording() {
|
|
83
|
-
mediaRecorder.stop();
|
|
84
|
-
mediaRecorder.release();
|
|
85
|
-
currentRecordingStatus = CurrentRecordingStatus.NONE;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public File getOutputFile() {
|
|
89
|
-
return outputFile;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
public RecordOptions getRecordOptions() {
|
|
93
|
-
return options;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
public boolean pauseRecording() throws NotSupportedOsVersion {
|
|
97
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
98
|
-
throw new NotSupportedOsVersion();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (currentRecordingStatus == CurrentRecordingStatus.RECORDING) {
|
|
102
|
-
mediaRecorder.pause();
|
|
103
|
-
currentRecordingStatus = CurrentRecordingStatus.PAUSED;
|
|
104
|
-
return true;
|
|
105
|
-
} else {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public boolean resumeRecording() throws NotSupportedOsVersion {
|
|
111
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
112
|
-
throw new NotSupportedOsVersion();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (currentRecordingStatus == CurrentRecordingStatus.PAUSED) {
|
|
116
|
-
mediaRecorder.resume();
|
|
117
|
-
currentRecordingStatus = CurrentRecordingStatus.RECORDING;
|
|
118
|
-
return true;
|
|
119
|
-
} else {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
public CurrentRecordingStatus getCurrentStatus() {
|
|
125
|
-
return currentRecordingStatus;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
public boolean deleteOutputFile() {
|
|
129
|
-
return outputFile.delete();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
public static boolean canPhoneCreateMediaRecorder(Context context) {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private static boolean canPhoneCreateMediaRecorderWhileHavingPermission(Context context) {
|
|
137
|
-
CustomMediaRecorder tempMediaRecorder = null;
|
|
138
|
-
try {
|
|
139
|
-
tempMediaRecorder = new CustomMediaRecorder(context, new RecordOptions(null, null));
|
|
140
|
-
tempMediaRecorder.startRecording();
|
|
141
|
-
tempMediaRecorder.stopRecording();
|
|
142
|
-
return true;
|
|
143
|
-
} catch (Exception exp) {
|
|
144
|
-
return exp.getMessage().startsWith("stop failed");
|
|
145
|
-
} finally {
|
|
146
|
-
if (tempMediaRecorder != null) tempMediaRecorder.deleteOutputFile();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
package com.tchvu3.capacitorvoicerecorder;
|
|
2
|
-
|
|
3
|
-
import android.Manifest;
|
|
4
|
-
import android.content.Context;
|
|
5
|
-
import android.media.AudioManager;
|
|
6
|
-
import android.media.MediaPlayer;
|
|
7
|
-
import android.net.Uri;
|
|
8
|
-
import android.util.Base64;
|
|
9
|
-
import com.getcapacitor.PermissionState;
|
|
10
|
-
import com.getcapacitor.Plugin;
|
|
11
|
-
import com.getcapacitor.PluginCall;
|
|
12
|
-
import com.getcapacitor.PluginMethod;
|
|
13
|
-
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
14
|
-
import com.getcapacitor.annotation.Permission;
|
|
15
|
-
import com.getcapacitor.annotation.PermissionCallback;
|
|
16
|
-
import java.io.BufferedInputStream;
|
|
17
|
-
import java.io.File;
|
|
18
|
-
import java.io.FileInputStream;
|
|
19
|
-
import java.io.IOException;
|
|
20
|
-
|
|
21
|
-
@CapacitorPlugin(
|
|
22
|
-
name = "VoiceRecorder",
|
|
23
|
-
permissions = { @Permission(alias = VoiceRecorder.RECORD_AUDIO_ALIAS, strings = { Manifest.permission.RECORD_AUDIO }) }
|
|
24
|
-
)
|
|
25
|
-
public class VoiceRecorder extends Plugin {
|
|
26
|
-
|
|
27
|
-
static final String RECORD_AUDIO_ALIAS = "voice recording";
|
|
28
|
-
private CustomMediaRecorder mediaRecorder;
|
|
29
|
-
|
|
30
|
-
@PluginMethod
|
|
31
|
-
public void canDeviceVoiceRecord(PluginCall call) {
|
|
32
|
-
if (CustomMediaRecorder.canPhoneCreateMediaRecorder(getContext())) {
|
|
33
|
-
call.resolve(ResponseGenerator.successResponse());
|
|
34
|
-
} else {
|
|
35
|
-
call.resolve(ResponseGenerator.failResponse());
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@PluginMethod
|
|
40
|
-
public void requestAudioRecordingPermission(PluginCall call) {
|
|
41
|
-
if (doesUserGaveAudioRecordingPermission()) {
|
|
42
|
-
call.resolve(ResponseGenerator.successResponse());
|
|
43
|
-
} else {
|
|
44
|
-
requestPermissionForAlias(RECORD_AUDIO_ALIAS, call, "recordAudioPermissionCallback");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@PermissionCallback
|
|
49
|
-
private void recordAudioPermissionCallback(PluginCall call) {
|
|
50
|
-
this.hasAudioRecordingPermission(call);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@PluginMethod
|
|
54
|
-
public void hasAudioRecordingPermission(PluginCall call) {
|
|
55
|
-
call.resolve(ResponseGenerator.fromBoolean(doesUserGaveAudioRecordingPermission()));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@PluginMethod
|
|
59
|
-
public void startRecording(PluginCall call) {
|
|
60
|
-
if (!CustomMediaRecorder.canPhoneCreateMediaRecorder(getContext())) {
|
|
61
|
-
call.reject(Messages.CANNOT_RECORD_ON_THIS_PHONE);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!doesUserGaveAudioRecordingPermission()) {
|
|
66
|
-
call.reject(Messages.MISSING_PERMISSION);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (this.isMicrophoneOccupied()) {
|
|
71
|
-
call.reject(Messages.MICROPHONE_BEING_USED);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (mediaRecorder != null) {
|
|
76
|
-
call.reject(Messages.ALREADY_RECORDING);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
String directory = call.getString("directory");
|
|
82
|
-
String subDirectory = call.getString("subDirectory");
|
|
83
|
-
RecordOptions options = new RecordOptions(directory, subDirectory);
|
|
84
|
-
mediaRecorder = new CustomMediaRecorder(getContext(), options);
|
|
85
|
-
mediaRecorder.startRecording();
|
|
86
|
-
call.resolve(ResponseGenerator.successResponse());
|
|
87
|
-
} catch (Exception exp) {
|
|
88
|
-
call.reject(Messages.FAILED_TO_RECORD, exp);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
@PluginMethod
|
|
93
|
-
public void stopRecording(PluginCall call) {
|
|
94
|
-
if (mediaRecorder == null) {
|
|
95
|
-
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
mediaRecorder.stopRecording();
|
|
101
|
-
File recordedFile = mediaRecorder.getOutputFile();
|
|
102
|
-
RecordOptions options = mediaRecorder.getRecordOptions();
|
|
103
|
-
|
|
104
|
-
String recordDataBase64 = null;
|
|
105
|
-
String uri = null;
|
|
106
|
-
if (options.directory() != null) {
|
|
107
|
-
uri = Uri.fromFile(recordedFile).toString();
|
|
108
|
-
} else {
|
|
109
|
-
recordDataBase64 = readRecordedFileAsBase64(recordedFile);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
RecordData recordData = new RecordData(
|
|
113
|
-
recordDataBase64,
|
|
114
|
-
getMsDurationOfAudioFile(recordedFile.getAbsolutePath()),
|
|
115
|
-
"audio/aac",
|
|
116
|
-
uri
|
|
117
|
-
);
|
|
118
|
-
if ((recordDataBase64 == null && uri == null) || recordData.getMsDuration() < 0) {
|
|
119
|
-
call.reject(Messages.EMPTY_RECORDING);
|
|
120
|
-
} else {
|
|
121
|
-
call.resolve(ResponseGenerator.dataResponse(recordData.toJSObject()));
|
|
122
|
-
}
|
|
123
|
-
} catch (Exception exp) {
|
|
124
|
-
call.reject(Messages.FAILED_TO_FETCH_RECORDING, exp);
|
|
125
|
-
} finally {
|
|
126
|
-
RecordOptions options = mediaRecorder.getRecordOptions();
|
|
127
|
-
if (options.directory() == null) {
|
|
128
|
-
mediaRecorder.deleteOutputFile();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
mediaRecorder = null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@PluginMethod
|
|
136
|
-
public void pauseRecording(PluginCall call) {
|
|
137
|
-
if (mediaRecorder == null) {
|
|
138
|
-
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
call.resolve(ResponseGenerator.fromBoolean(mediaRecorder.pauseRecording()));
|
|
143
|
-
} catch (NotSupportedOsVersion exception) {
|
|
144
|
-
call.reject(Messages.NOT_SUPPORTED_OS_VERSION);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@PluginMethod
|
|
149
|
-
public void resumeRecording(PluginCall call) {
|
|
150
|
-
if (mediaRecorder == null) {
|
|
151
|
-
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
call.resolve(ResponseGenerator.fromBoolean(mediaRecorder.resumeRecording()));
|
|
156
|
-
} catch (NotSupportedOsVersion exception) {
|
|
157
|
-
call.reject(Messages.NOT_SUPPORTED_OS_VERSION);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@PluginMethod
|
|
162
|
-
public void getCurrentStatus(PluginCall call) {
|
|
163
|
-
if (mediaRecorder == null) {
|
|
164
|
-
call.resolve(ResponseGenerator.statusResponse(CurrentRecordingStatus.NONE));
|
|
165
|
-
} else {
|
|
166
|
-
call.resolve(ResponseGenerator.statusResponse(mediaRecorder.getCurrentStatus()));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private boolean doesUserGaveAudioRecordingPermission() {
|
|
171
|
-
return getPermissionState(VoiceRecorder.RECORD_AUDIO_ALIAS).equals(PermissionState.GRANTED);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private String readRecordedFileAsBase64(File recordedFile) {
|
|
175
|
-
BufferedInputStream bufferedInputStream;
|
|
176
|
-
byte[] bArray = new byte[(int) recordedFile.length()];
|
|
177
|
-
try {
|
|
178
|
-
bufferedInputStream = new BufferedInputStream(new FileInputStream(recordedFile));
|
|
179
|
-
bufferedInputStream.read(bArray);
|
|
180
|
-
bufferedInputStream.close();
|
|
181
|
-
} catch (IOException exp) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
return Base64.encodeToString(bArray, Base64.DEFAULT);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private int getMsDurationOfAudioFile(String recordedFilePath) {
|
|
188
|
-
try {
|
|
189
|
-
MediaPlayer mediaPlayer = new MediaPlayer();
|
|
190
|
-
mediaPlayer.setDataSource(recordedFilePath);
|
|
191
|
-
mediaPlayer.prepare();
|
|
192
|
-
return mediaPlayer.getDuration();
|
|
193
|
-
} catch (Exception ignore) {
|
|
194
|
-
return -1;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private boolean isMicrophoneOccupied() {
|
|
199
|
-
AudioManager audioManager = (AudioManager) this.getContext().getSystemService(Context.AUDIO_SERVICE);
|
|
200
|
-
if (audioManager == null) return true;
|
|
201
|
-
return audioManager.getMode() != AudioManager.MODE_NORMAL;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from './definitions';
|
|
2
|
-
declare const POSSIBLE_MIME_TYPES: {
|
|
3
|
-
'audio/aac': string;
|
|
4
|
-
'audio/webm;codecs=opus': string;
|
|
5
|
-
'audio/mp4': string;
|
|
6
|
-
'audio/webm': string;
|
|
7
|
-
'audio/ogg;codecs=opus': string;
|
|
8
|
-
};
|
|
9
|
-
export declare class VoiceRecorderImpl {
|
|
10
|
-
private mediaRecorder;
|
|
11
|
-
private chunks;
|
|
12
|
-
private pendingResult;
|
|
13
|
-
static canDeviceVoiceRecord(): Promise<GenericResponse>;
|
|
14
|
-
startRecording(options?: RecordingOptions): Promise<GenericResponse>;
|
|
15
|
-
stopRecording(): Promise<RecordingData>;
|
|
16
|
-
static hasAudioRecordingPermission(): Promise<GenericResponse>;
|
|
17
|
-
static requestAudioRecordingPermission(): Promise<GenericResponse>;
|
|
18
|
-
pauseRecording(): Promise<GenericResponse>;
|
|
19
|
-
resumeRecording(): Promise<GenericResponse>;
|
|
20
|
-
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
21
|
-
static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(): T | null;
|
|
22
|
-
private onSuccessfullyStartedRecording;
|
|
23
|
-
private onFailedToStartRecording;
|
|
24
|
-
private static blobToBase64;
|
|
25
|
-
private prepareInstanceForNextOperation;
|
|
26
|
-
}
|
|
27
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceRecorderImpl.js","sourceRoot":"","sources":["../../src/VoiceRecorderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import {Filesystem} from '@capacitor/filesystem';\nimport write_blob from 'capacitor-blob-writer';\n\nimport type {\n Base64String,\n CurrentRecordingStatus,\n GenericResponse,\n RecordingData,\n RecordingOptions,\n} from './definitions';\nimport getBlobDuration from \"./helper/get-blob-duration\";\nimport {\n alreadyRecordingError,\n couldNotQueryPermissionStatusError,\n deviceCannotVoiceRecordError,\n emptyRecordingError,\n failedToFetchRecordingError,\n failedToRecordError,\n failureResponse,\n missingPermissionError,\n recordingHasNotStartedError,\n successResponse,\n} from './predefined-web-responses';\n\n// these mime types will be checked one by one in order until one of them is found to be supported by the current browser\nconst POSSIBLE_MIME_TYPES = {\n 'audio/aac': '.aac',\n 'audio/webm;codecs=opus': '.ogg',\n 'audio/mp4': '.mp3',\n 'audio/webm': '.ogg',\n 'audio/ogg;codecs=opus': '.ogg',\n};\n\nconst neverResolvingPromise = (): Promise<any> => new Promise(() => undefined);\n\nexport class VoiceRecorderImpl {\n private mediaRecorder: MediaRecorder | null = null;\n private chunks: any[] = [];\n private pendingResult: Promise<RecordingData> = neverResolvingPromise();\n\n public static async canDeviceVoiceRecord(): Promise<GenericResponse> {\n if (navigator?.mediaDevices?.getUserMedia == null || VoiceRecorderImpl.getSupportedMimeType() == null) {\n return failureResponse();\n } else {\n return successResponse();\n }\n }\n\n public async startRecording(options?: RecordingOptions): Promise<GenericResponse> {\n if (this.mediaRecorder != null) {\n throw alreadyRecordingError();\n }\n const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord();\n if (!deviceCanRecord.value) {\n throw deviceCannotVoiceRecordError();\n }\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => successResponse());\n if (!havingPermission.value) {\n throw missingPermissionError();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then((stream) => this.onSuccessfullyStartedRecording(stream, options))\n .catch(this.onFailedToStartRecording.bind(this));\n }\n\n public async stopRecording(): Promise<RecordingData> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n }\n try {\n this.mediaRecorder.stop();\n this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());\n return this.pendingResult;\n } catch (ignore) {\n throw failedToFetchRecordingError();\n } finally {\n this.prepareInstanceForNextOperation();\n }\n }\n\n public static async hasAudioRecordingPermission(): Promise<GenericResponse> {\n // Safari does not support navigator.permissions.query\n if (!navigator.permissions.query) {\n if (navigator.mediaDevices !== undefined) {\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n }\n return navigator.permissions\n .query({name: 'microphone' as any})\n .then((result) => ({value: result.state === 'granted'}))\n .catch(() => {\n throw couldNotQueryPermissionStatusError();\n });\n }\n\n public static async requestAudioRecordingPermission(): Promise<GenericResponse> {\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => failureResponse());\n if (havingPermission.value) {\n return successResponse();\n }\n\n return navigator.mediaDevices\n .getUserMedia({audio: true})\n .then(() => successResponse())\n .catch(() => failureResponse());\n }\n\n public pauseRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'recording') {\n this.mediaRecorder.pause();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n public resumeRecording(): Promise<GenericResponse> {\n if (this.mediaRecorder == null) {\n throw recordingHasNotStartedError();\n } else if (this.mediaRecorder.state === 'paused') {\n this.mediaRecorder.resume();\n return Promise.resolve(successResponse());\n } else {\n return Promise.resolve(failureResponse());\n }\n }\n\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\n if (this.mediaRecorder == null) {\n return Promise.resolve({status: 'NONE'});\n } else if (this.mediaRecorder.state === 'recording') {\n return Promise.resolve({status: 'RECORDING'});\n } else if (this.mediaRecorder.state === 'paused') {\n return Promise.resolve({status: 'PAUSED'});\n } else {\n return Promise.resolve({status: 'NONE'});\n }\n }\n\n public static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(): T | null {\n if (MediaRecorder?.isTypeSupported == null) return null;\n\n const foundSupportedType = Object.keys(POSSIBLE_MIME_TYPES).find((type) => MediaRecorder.isTypeSupported(type)) as\n | T\n | undefined;\n\n return foundSupportedType ?? null;\n }\n\n private onSuccessfullyStartedRecording(stream: MediaStream, options?: RecordingOptions): GenericResponse {\n this.pendingResult = new Promise((resolve, reject) => {\n this.mediaRecorder = new MediaRecorder(stream);\n this.mediaRecorder.onerror = () => {\n this.prepareInstanceForNextOperation();\n reject(failedToRecordError());\n };\n this.mediaRecorder.onstop = async () => {\n const mimeType = VoiceRecorderImpl.getSupportedMimeType();\n if (mimeType == null) {\n this.prepareInstanceForNextOperation();\n reject(failedToFetchRecordingError());\n return;\n }\n const blobVoiceRecording = new Blob(this.chunks, {type: mimeType});\n if (blobVoiceRecording.size <= 0) {\n this.prepareInstanceForNextOperation();\n reject(emptyRecordingError());\n return;\n }\n\n let uri: string | undefined = undefined;\n let recordDataBase64 = '';\n if (options?.directory) {\n const subDirectory = options.subDirectory?.match(/^\\/?(.+[^/])\\/?$/)?.[1] ?? '';\n const path = `${subDirectory}/recording-${new Date().getTime()}${POSSIBLE_MIME_TYPES[mimeType]}`;\n\n await write_blob({\n blob: blobVoiceRecording,\n directory: options.directory,\n fast_mode: true,\n path,\n recursive: true,\n });\n\n ({uri} = await Filesystem.getUri({directory: options.directory, path}));\n } else {\n recordDataBase64 = await VoiceRecorderImpl.blobToBase64(blobVoiceRecording);\n }\n\n const recordingDuration = await getBlobDuration(blobVoiceRecording);\n this.prepareInstanceForNextOperation();\n resolve({value: {recordDataBase64, mimeType, msDuration: recordingDuration * 1000, uri}});\n };\n this.mediaRecorder.ondataavailable = (event: any) => this.chunks.push(event.data);\n this.mediaRecorder.start();\n });\n return successResponse();\n }\n\n private onFailedToStartRecording(): GenericResponse {\n this.prepareInstanceForNextOperation();\n throw failedToRecordError();\n }\n\n private static blobToBase64(blob: Blob): Promise<Base64String> {\n return new Promise((resolve) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n const recordingResult = String(reader.result);\n const splitResult = recordingResult.split('base64,');\n const toResolve = splitResult.length > 1 ? splitResult[1] : recordingResult;\n resolve(toResolve.trim());\n };\n reader.readAsDataURL(blob);\n });\n }\n\n private prepareInstanceForNextOperation(): void {\n if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {\n try {\n this.mediaRecorder.stop();\n } catch (ignore) {\n console.warn('Failed to stop recording during cleanup');\n }\n }\n this.pendingResult = neverResolvingPromise();\n this.mediaRecorder = null;\n this.chunks = [];\n }\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-blob-duration.js","sourceRoot":"","sources":["../../../src/helper/get-blob-duration.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,IAAmB;IACvD,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,CAAC,WAAW;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpE,MAAM,SAAS,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtD,WAAW,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,GAAG,EAAE;YAChD,2EAA2E;YAC3E,IAAI,WAAW,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACpC,WAAW,CAAC,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC;gBAClD,WAAW,CAAC,YAAY,GAAG,GAAG,EAAE;oBAC5B,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC;oBAChC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;oBAC9B,WAAW,CAAC,WAAW,GAAG,CAAC,CAAC;gBAChC,CAAC,CAAC;YACN,CAAC;iBAAM,CAAC;gBACJ,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAClC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,WAAW,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE;YAC5B,MAAM,KAAK,GAAI,KAAoB,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;YACjF,MAAM,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC,CAAC;IACN,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAE9E,OAAO,SAAS,CAAC;AACrB,CAAC","sourcesContent":["/**\n * @param {Blob | string} blob\n * @returns {Promise<number>} Blob duration in seconds.\n */\nexport default function getBlobDuration(blob: Blob | string): Promise<number> {\n const tempVideoEl = document.createElement('video');\n if (!tempVideoEl) throw new Error('Failed to create video element');\n const durationP = new Promise<number>((resolve, reject) => {\n tempVideoEl.addEventListener('loadedmetadata', () => {\n // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=642012\n if (tempVideoEl.duration === Infinity) {\n tempVideoEl.currentTime = Number.MAX_SAFE_INTEGER;\n tempVideoEl.ontimeupdate = () => {\n tempVideoEl.ontimeupdate = null;\n resolve(tempVideoEl.duration);\n tempVideoEl.currentTime = 0;\n };\n } else {\n resolve(tempVideoEl.duration);\n }\n });\n\n tempVideoEl.onerror = (event) => {\n const error = (event as ErrorEvent).error || new Error('Unknown error occurred');\n reject(error);\n };\n });\n\n tempVideoEl.src = typeof blob === 'string' ? blob : URL.createObjectURL(blob);\n\n return durationP;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"predefined-web-responses.js","sourceRoot":"","sources":["../../src/predefined-web-responses.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,GAAoB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,GAAoB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;AACnF,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AACxF,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;AACjG,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAE7E,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;AAC/F,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;AAE/F,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAAU,EAAE,CAAC,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC","sourcesContent":["import type { GenericResponse } from './definitions';\n\nexport const successResponse = (): GenericResponse => ({ value: true });\nexport const failureResponse = (): GenericResponse => ({ value: false });\n\nexport const missingPermissionError = (): Error => new Error('MISSING_PERMISSION');\nexport const alreadyRecordingError = (): Error => new Error('ALREADY_RECORDING');\nexport const microphoneBeingUsedError = (): Error => new Error('MICROPHONE_BEING_USED');\nexport const deviceCannotVoiceRecordError = (): Error => new Error('DEVICE_CANNOT_VOICE_RECORD');\nexport const failedToRecordError = (): Error => new Error('FAILED_TO_RECORD');\nexport const emptyRecordingError = (): Error => new Error('EMPTY_RECORDING');\n\nexport const recordingHasNotStartedError = (): Error => new Error('RECORDING_HAS_NOT_STARTED');\nexport const failedToFetchRecordingError = (): Error => new Error('FAILED_TO_FETCH_RECORDING');\n\nexport const couldNotQueryPermissionStatusError = (): Error => new Error('COULD_NOT_QUERY_PERMISSION_STATUS');\n"]}
|