capacitor-voice-recorder-wav-stereo 7.0.0
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/CapacitorVoiceRecorderWavStereo.podspec +17 -0
- package/README.md +213 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +7 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CustomMediaRecorder.java +187 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/Messages.java +15 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/NotSupportedOsVersion.java +4 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/RecordData.java +51 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/ResponseGenerator.java +37 -0
- package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +217 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +174 -0
- package/dist/esm/VoiceRecorderImpl.d.ts +19 -0
- package/dist/esm/VoiceRecorderImpl.js +172 -0
- package/dist/esm/VoiceRecorderImpl.js.map +1 -0
- package/dist/esm/definitions.d.ts +24 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/predefined-web-responses.d.ts +12 -0
- package/dist/esm/predefined-web-responses.js +12 -0
- package/dist/esm/predefined-web-responses.js.map +1 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.js +33 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +228 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +230 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/CurrentRecordingStatus.swift +9 -0
- package/ios/Plugin/CustomMediaRecorder.swift +82 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/Messages.swift +15 -0
- package/ios/Plugin/RecordData.swift +17 -0
- package/ios/Plugin/ResponseGenerator.swift +28 -0
- package/ios/Plugin/VoiceRecorder.swift +130 -0
- package/ios/Plugin/VoiceRecorderPlugin.h +10 -0
- package/ios/Plugin/VoiceRecorderPlugin.m +15 -0
- package/package.json +92 -0
|
@@ -0,0 +1,217 @@
|
|
|
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.util.Base64;
|
|
8
|
+
|
|
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
|
+
|
|
17
|
+
import java.io.BufferedInputStream;
|
|
18
|
+
import java.io.File;
|
|
19
|
+
import java.io.FileInputStream;
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.nio.ByteBuffer;
|
|
22
|
+
import java.nio.ByteOrder;
|
|
23
|
+
|
|
24
|
+
@CapacitorPlugin(
|
|
25
|
+
name = "VoiceRecorder",
|
|
26
|
+
permissions = {@Permission(alias = VoiceRecorder.RECORD_AUDIO_ALIAS, strings = {Manifest.permission.RECORD_AUDIO})}
|
|
27
|
+
)
|
|
28
|
+
public class VoiceRecorder extends Plugin {
|
|
29
|
+
|
|
30
|
+
static final String RECORD_AUDIO_ALIAS = "voice recording";
|
|
31
|
+
private CustomMediaRecorder mediaRecorder;
|
|
32
|
+
|
|
33
|
+
@PluginMethod()
|
|
34
|
+
public void canDeviceVoiceRecord(PluginCall call) {
|
|
35
|
+
if (CustomMediaRecorder.canPhoneCreateMediaRecorder(getContext())) {
|
|
36
|
+
call.resolve(ResponseGenerator.successResponse());
|
|
37
|
+
} else {
|
|
38
|
+
call.resolve(ResponseGenerator.failResponse());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@PluginMethod()
|
|
43
|
+
public void requestAudioRecordingPermission(PluginCall call) {
|
|
44
|
+
if (doesUserGaveAudioRecordingPermission()) {
|
|
45
|
+
call.resolve(ResponseGenerator.successResponse());
|
|
46
|
+
} else {
|
|
47
|
+
requestPermissionForAlias(RECORD_AUDIO_ALIAS, call, "recordAudioPermissionCallback");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@PermissionCallback
|
|
52
|
+
private void recordAudioPermissionCallback(PluginCall call) {
|
|
53
|
+
this.hasAudioRecordingPermission(call);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@PluginMethod()
|
|
57
|
+
public void hasAudioRecordingPermission(PluginCall call) {
|
|
58
|
+
call.resolve(ResponseGenerator.fromBoolean(doesUserGaveAudioRecordingPermission()));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@PluginMethod()
|
|
62
|
+
public void startRecording(PluginCall call) {
|
|
63
|
+
if (!CustomMediaRecorder.canPhoneCreateMediaRecorder(getContext())) {
|
|
64
|
+
call.reject(Messages.CANNOT_RECORD_ON_THIS_PHONE);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!doesUserGaveAudioRecordingPermission()) {
|
|
69
|
+
call.reject(Messages.MISSING_PERMISSION);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.isMicrophoneOccupied()) {
|
|
74
|
+
call.reject(Messages.MICROPHONE_BEING_USED);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mediaRecorder != null) {
|
|
79
|
+
call.reject(Messages.ALREADY_RECORDING);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
mediaRecorder = new CustomMediaRecorder(getContext());
|
|
85
|
+
mediaRecorder.initializeAudioRecord();
|
|
86
|
+
mediaRecorder.startRecording();
|
|
87
|
+
call.resolve(ResponseGenerator.successResponse());
|
|
88
|
+
} catch (IOException exp) {
|
|
89
|
+
call.reject(Messages.FAILED_TO_RECORD, exp);
|
|
90
|
+
} catch (Exception exp) {
|
|
91
|
+
call.reject(Messages.FAILED_TO_RECORD, exp);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@PluginMethod()
|
|
96
|
+
public void stopRecording(PluginCall call) {
|
|
97
|
+
if (mediaRecorder == null) {
|
|
98
|
+
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
mediaRecorder.stopRecording();
|
|
104
|
+
File recordedFile = mediaRecorder.getOutputFile();
|
|
105
|
+
RecordData recordData = new RecordData(
|
|
106
|
+
readWavFileAsBase64(recordedFile),
|
|
107
|
+
getWavFileDuration(recordedFile),
|
|
108
|
+
"audio/wav"
|
|
109
|
+
);
|
|
110
|
+
if (recordData.getRecordDataBase64() == null || recordData.getMsDuration() < 0) {
|
|
111
|
+
call.reject(Messages.EMPTY_RECORDING);
|
|
112
|
+
} else {
|
|
113
|
+
call.resolve(ResponseGenerator.dataResponse(recordData.toJSObject()));
|
|
114
|
+
}
|
|
115
|
+
} catch (Exception exp) {
|
|
116
|
+
call.reject(Messages.FAILED_TO_FETCH_RECORDING, exp);
|
|
117
|
+
} finally {
|
|
118
|
+
mediaRecorder.deleteOutputFile();
|
|
119
|
+
mediaRecorder = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private String readWavFileAsBase64(File wavFile) throws IOException {
|
|
124
|
+
byte[] bytes = new byte[(int) wavFile.length()];
|
|
125
|
+
try (FileInputStream fis = new FileInputStream(wavFile)) {
|
|
126
|
+
fis.read(bytes);
|
|
127
|
+
}
|
|
128
|
+
return Base64.encodeToString(bytes, Base64.DEFAULT);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private int getWavFileDuration(File wavFile) throws IOException {
|
|
132
|
+
try (FileInputStream fis = new FileInputStream(wavFile)) {
|
|
133
|
+
byte[] header = new byte[44]; // WAVヘッダーは通常44バイト
|
|
134
|
+
fis.read(header);
|
|
135
|
+
|
|
136
|
+
// サンプルレートを取得(ヘッダーの24-27バイト目)
|
|
137
|
+
int sampleRate = ByteBuffer.wrap(header, 24, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
|
138
|
+
|
|
139
|
+
// データサイズを取得(ヘッダーの40-43バイト目)
|
|
140
|
+
int dataSize = ByteBuffer.wrap(header, 40, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
|
141
|
+
|
|
142
|
+
// 時間(ミリ秒)を計算
|
|
143
|
+
return (int) ((dataSize / (sampleRate * 2 * 2)) * 1000); // 2チャンネル、16ビットを想定
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@PluginMethod()
|
|
148
|
+
public void pauseRecording(PluginCall call) {
|
|
149
|
+
if (mediaRecorder == null) {
|
|
150
|
+
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
call.resolve(ResponseGenerator.fromBoolean(mediaRecorder.pauseRecording()));
|
|
155
|
+
} catch (NotSupportedOsVersion exception) {
|
|
156
|
+
call.reject(Messages.NOT_SUPPORTED_OS_VERSION);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@PluginMethod()
|
|
161
|
+
public void resumeRecording(PluginCall call) {
|
|
162
|
+
if (mediaRecorder == null) {
|
|
163
|
+
call.reject(Messages.RECORDING_HAS_NOT_STARTED);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
call.resolve(ResponseGenerator.fromBoolean(mediaRecorder.resumeRecording()));
|
|
168
|
+
} catch (NotSupportedOsVersion exception) {
|
|
169
|
+
call.reject(Messages.NOT_SUPPORTED_OS_VERSION);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@PluginMethod()
|
|
174
|
+
public void getCurrentStatus(PluginCall call) {
|
|
175
|
+
if (mediaRecorder == null) {
|
|
176
|
+
call.resolve(ResponseGenerator.statusResponse(CurrentRecordingStatus.NONE));
|
|
177
|
+
} else {
|
|
178
|
+
call.resolve(ResponseGenerator.statusResponse(mediaRecorder.getCurrentStatus()));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private boolean doesUserGaveAudioRecordingPermission() {
|
|
183
|
+
return getPermissionState(VoiceRecorder.RECORD_AUDIO_ALIAS).equals(PermissionState.GRANTED);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private String readRecordedFileAsBase64(File recordedFile) {
|
|
187
|
+
BufferedInputStream bufferedInputStream;
|
|
188
|
+
byte[] bArray = new byte[(int) recordedFile.length()];
|
|
189
|
+
try {
|
|
190
|
+
bufferedInputStream = new BufferedInputStream(new FileInputStream(recordedFile));
|
|
191
|
+
bufferedInputStream.read(bArray);
|
|
192
|
+
bufferedInputStream.close();
|
|
193
|
+
} catch (IOException exp) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return Base64.encodeToString(bArray, Base64.DEFAULT);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private int getMsDurationOfAudioFile(String recordedFilePath) {
|
|
200
|
+
try {
|
|
201
|
+
MediaPlayer mediaPlayer = new MediaPlayer();
|
|
202
|
+
mediaPlayer.setDataSource(recordedFilePath);
|
|
203
|
+
mediaPlayer.prepare();
|
|
204
|
+
return mediaPlayer.getDuration();
|
|
205
|
+
} catch (Exception ignore) {
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private boolean isMicrophoneOccupied() {
|
|
211
|
+
AudioManager audioManager = (AudioManager) this.getContext().getSystemService(Context.AUDIO_SERVICE);
|
|
212
|
+
if (audioManager == null)
|
|
213
|
+
return true;
|
|
214
|
+
return audioManager.getMode() != AudioManager.MODE_NORMAL;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
}
|
|
File without changes
|
package/dist/docs.json
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api": {
|
|
3
|
+
"name": "VoiceRecorderPlugin",
|
|
4
|
+
"slug": "voicerecorderplugin",
|
|
5
|
+
"docs": "",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"methods": [
|
|
8
|
+
{
|
|
9
|
+
"name": "canDeviceVoiceRecord",
|
|
10
|
+
"signature": "() => Promise<GenericResponse>",
|
|
11
|
+
"parameters": [],
|
|
12
|
+
"returns": "Promise<GenericResponse>",
|
|
13
|
+
"tags": [],
|
|
14
|
+
"docs": "",
|
|
15
|
+
"complexTypes": [
|
|
16
|
+
"GenericResponse"
|
|
17
|
+
],
|
|
18
|
+
"slug": "candevicevoicerecord"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "requestAudioRecordingPermission",
|
|
22
|
+
"signature": "() => Promise<GenericResponse>",
|
|
23
|
+
"parameters": [],
|
|
24
|
+
"returns": "Promise<GenericResponse>",
|
|
25
|
+
"tags": [],
|
|
26
|
+
"docs": "",
|
|
27
|
+
"complexTypes": [
|
|
28
|
+
"GenericResponse"
|
|
29
|
+
],
|
|
30
|
+
"slug": "requestaudiorecordingpermission"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "hasAudioRecordingPermission",
|
|
34
|
+
"signature": "() => Promise<GenericResponse>",
|
|
35
|
+
"parameters": [],
|
|
36
|
+
"returns": "Promise<GenericResponse>",
|
|
37
|
+
"tags": [],
|
|
38
|
+
"docs": "",
|
|
39
|
+
"complexTypes": [
|
|
40
|
+
"GenericResponse"
|
|
41
|
+
],
|
|
42
|
+
"slug": "hasaudiorecordingpermission"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "startRecording",
|
|
46
|
+
"signature": "() => Promise<GenericResponse>",
|
|
47
|
+
"parameters": [],
|
|
48
|
+
"returns": "Promise<GenericResponse>",
|
|
49
|
+
"tags": [],
|
|
50
|
+
"docs": "",
|
|
51
|
+
"complexTypes": [
|
|
52
|
+
"GenericResponse"
|
|
53
|
+
],
|
|
54
|
+
"slug": "startrecording"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "stopRecording",
|
|
58
|
+
"signature": "() => Promise<RecordingData>",
|
|
59
|
+
"parameters": [],
|
|
60
|
+
"returns": "Promise<RecordingData>",
|
|
61
|
+
"tags": [],
|
|
62
|
+
"docs": "",
|
|
63
|
+
"complexTypes": [
|
|
64
|
+
"RecordingData"
|
|
65
|
+
],
|
|
66
|
+
"slug": "stoprecording"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "pauseRecording",
|
|
70
|
+
"signature": "() => Promise<GenericResponse>",
|
|
71
|
+
"parameters": [],
|
|
72
|
+
"returns": "Promise<GenericResponse>",
|
|
73
|
+
"tags": [],
|
|
74
|
+
"docs": "",
|
|
75
|
+
"complexTypes": [
|
|
76
|
+
"GenericResponse"
|
|
77
|
+
],
|
|
78
|
+
"slug": "pauserecording"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "resumeRecording",
|
|
82
|
+
"signature": "() => Promise<GenericResponse>",
|
|
83
|
+
"parameters": [],
|
|
84
|
+
"returns": "Promise<GenericResponse>",
|
|
85
|
+
"tags": [],
|
|
86
|
+
"docs": "",
|
|
87
|
+
"complexTypes": [
|
|
88
|
+
"GenericResponse"
|
|
89
|
+
],
|
|
90
|
+
"slug": "resumerecording"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "getCurrentStatus",
|
|
94
|
+
"signature": "() => Promise<CurrentRecordingStatus>",
|
|
95
|
+
"parameters": [],
|
|
96
|
+
"returns": "Promise<CurrentRecordingStatus>",
|
|
97
|
+
"tags": [],
|
|
98
|
+
"docs": "",
|
|
99
|
+
"complexTypes": [
|
|
100
|
+
"CurrentRecordingStatus"
|
|
101
|
+
],
|
|
102
|
+
"slug": "getcurrentstatus"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"properties": []
|
|
106
|
+
},
|
|
107
|
+
"interfaces": [
|
|
108
|
+
{
|
|
109
|
+
"name": "GenericResponse",
|
|
110
|
+
"slug": "genericresponse",
|
|
111
|
+
"docs": "",
|
|
112
|
+
"tags": [],
|
|
113
|
+
"methods": [],
|
|
114
|
+
"properties": [
|
|
115
|
+
{
|
|
116
|
+
"name": "value",
|
|
117
|
+
"tags": [],
|
|
118
|
+
"docs": "",
|
|
119
|
+
"complexTypes": [],
|
|
120
|
+
"type": "boolean"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "RecordingData",
|
|
126
|
+
"slug": "recordingdata",
|
|
127
|
+
"docs": "",
|
|
128
|
+
"tags": [],
|
|
129
|
+
"methods": [],
|
|
130
|
+
"properties": [
|
|
131
|
+
{
|
|
132
|
+
"name": "value",
|
|
133
|
+
"tags": [],
|
|
134
|
+
"docs": "",
|
|
135
|
+
"complexTypes": [
|
|
136
|
+
"Base64String"
|
|
137
|
+
],
|
|
138
|
+
"type": "{ recordDataBase64: string; msDuration: number; mimeType: string; }"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "CurrentRecordingStatus",
|
|
144
|
+
"slug": "currentrecordingstatus",
|
|
145
|
+
"docs": "",
|
|
146
|
+
"tags": [],
|
|
147
|
+
"methods": [],
|
|
148
|
+
"properties": [
|
|
149
|
+
{
|
|
150
|
+
"name": "status",
|
|
151
|
+
"tags": [],
|
|
152
|
+
"docs": "",
|
|
153
|
+
"complexTypes": [],
|
|
154
|
+
"type": "'RECORDING' | 'PAUSED' | 'NONE'"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"enums": [],
|
|
160
|
+
"typeAliases": [
|
|
161
|
+
{
|
|
162
|
+
"name": "Base64String",
|
|
163
|
+
"slug": "base64string",
|
|
164
|
+
"docs": "",
|
|
165
|
+
"types": [
|
|
166
|
+
{
|
|
167
|
+
"text": "string",
|
|
168
|
+
"complexTypes": []
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
"pluginConfigs": []
|
|
174
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CurrentRecordingStatus, GenericResponse, RecordingData } from './definitions';
|
|
2
|
+
export declare class VoiceRecorderImpl {
|
|
3
|
+
private mediaRecorder;
|
|
4
|
+
private chunks;
|
|
5
|
+
private pendingResult;
|
|
6
|
+
static canDeviceVoiceRecord(): Promise<GenericResponse>;
|
|
7
|
+
startRecording(): Promise<GenericResponse>;
|
|
8
|
+
stopRecording(): Promise<RecordingData>;
|
|
9
|
+
static hasAudioRecordingPermission(): Promise<GenericResponse>;
|
|
10
|
+
static requestAudioRecordingPermission(): Promise<GenericResponse>;
|
|
11
|
+
pauseRecording(): Promise<GenericResponse>;
|
|
12
|
+
resumeRecording(): Promise<GenericResponse>;
|
|
13
|
+
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
14
|
+
static getSupportedMimeType(): string | null;
|
|
15
|
+
private onSuccessfullyStartedRecording;
|
|
16
|
+
private onFailedToStartRecording;
|
|
17
|
+
private static blobToBase64;
|
|
18
|
+
private prepareInstanceForNextOperation;
|
|
19
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import getBlobDuration from 'get-blob-duration';
|
|
2
|
+
import { alreadyRecordingError, couldNotQueryPermissionStatusError, deviceCannotVoiceRecordError, emptyRecordingError, failedToFetchRecordingError, failedToRecordError, failureResponse, missingPermissionError, recordingHasNotStartedError, successResponse, } from './predefined-web-responses';
|
|
3
|
+
// these mime types will be checked one by one in order until one of them is found to be supported by the current browser
|
|
4
|
+
const possibleMimeTypes = ['audio/mp4', 'audio/webm;codecs=opus', 'audio/aac', 'audio/webm', 'audio/ogg;codecs=opus'];
|
|
5
|
+
const neverResolvingPromise = () => new Promise(() => undefined);
|
|
6
|
+
export class VoiceRecorderImpl {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.mediaRecorder = null;
|
|
9
|
+
this.chunks = [];
|
|
10
|
+
this.pendingResult = neverResolvingPromise();
|
|
11
|
+
}
|
|
12
|
+
static async canDeviceVoiceRecord() {
|
|
13
|
+
var _a;
|
|
14
|
+
if (((_a = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia) == null || VoiceRecorderImpl.getSupportedMimeType() == null) {
|
|
15
|
+
return failureResponse();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
return successResponse();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async startRecording() {
|
|
22
|
+
if (this.mediaRecorder != null) {
|
|
23
|
+
throw alreadyRecordingError();
|
|
24
|
+
}
|
|
25
|
+
const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord();
|
|
26
|
+
if (!deviceCanRecord.value) {
|
|
27
|
+
throw deviceCannotVoiceRecordError();
|
|
28
|
+
}
|
|
29
|
+
const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => successResponse());
|
|
30
|
+
if (!havingPermission.value) {
|
|
31
|
+
throw missingPermissionError();
|
|
32
|
+
}
|
|
33
|
+
return navigator.mediaDevices.getUserMedia({ audio: true })
|
|
34
|
+
.then(this.onSuccessfullyStartedRecording.bind(this))
|
|
35
|
+
.catch(this.onFailedToStartRecording.bind(this));
|
|
36
|
+
}
|
|
37
|
+
async stopRecording() {
|
|
38
|
+
if (this.mediaRecorder == null) {
|
|
39
|
+
throw recordingHasNotStartedError();
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
this.mediaRecorder.stop();
|
|
43
|
+
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
|
44
|
+
return this.pendingResult;
|
|
45
|
+
}
|
|
46
|
+
catch (ignore) {
|
|
47
|
+
throw failedToFetchRecordingError();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
this.prepareInstanceForNextOperation();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
static async hasAudioRecordingPermission() {
|
|
54
|
+
return navigator.permissions.query({ name: 'microphone' })
|
|
55
|
+
.then(result => ({ value: result.state === 'granted' }))
|
|
56
|
+
.catch(() => {
|
|
57
|
+
throw couldNotQueryPermissionStatusError();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
static async requestAudioRecordingPermission() {
|
|
61
|
+
const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => failureResponse());
|
|
62
|
+
if (havingPermission.value) {
|
|
63
|
+
return successResponse();
|
|
64
|
+
}
|
|
65
|
+
return navigator.mediaDevices.getUserMedia({ audio: true })
|
|
66
|
+
.then(() => successResponse())
|
|
67
|
+
.catch(() => failureResponse());
|
|
68
|
+
}
|
|
69
|
+
pauseRecording() {
|
|
70
|
+
if (this.mediaRecorder == null) {
|
|
71
|
+
throw recordingHasNotStartedError();
|
|
72
|
+
}
|
|
73
|
+
else if (this.mediaRecorder.state === 'recording') {
|
|
74
|
+
this.mediaRecorder.pause();
|
|
75
|
+
return Promise.resolve(successResponse());
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return Promise.resolve(failureResponse());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
resumeRecording() {
|
|
82
|
+
if (this.mediaRecorder == null) {
|
|
83
|
+
throw recordingHasNotStartedError();
|
|
84
|
+
}
|
|
85
|
+
else if (this.mediaRecorder.state === 'paused') {
|
|
86
|
+
this.mediaRecorder.resume();
|
|
87
|
+
return Promise.resolve(successResponse());
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return Promise.resolve(failureResponse());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getCurrentStatus() {
|
|
94
|
+
if (this.mediaRecorder == null) {
|
|
95
|
+
return Promise.resolve({ status: 'NONE' });
|
|
96
|
+
}
|
|
97
|
+
else if (this.mediaRecorder.state === 'recording') {
|
|
98
|
+
return Promise.resolve({ status: 'RECORDING' });
|
|
99
|
+
}
|
|
100
|
+
else if (this.mediaRecorder.state === 'paused') {
|
|
101
|
+
return Promise.resolve({ status: 'PAUSED' });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return Promise.resolve({ status: 'NONE' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
static getSupportedMimeType() {
|
|
108
|
+
if ((MediaRecorder === null || MediaRecorder === void 0 ? void 0 : MediaRecorder.isTypeSupported) == null)
|
|
109
|
+
return null;
|
|
110
|
+
const foundSupportedType = possibleMimeTypes.find(type => MediaRecorder.isTypeSupported(type));
|
|
111
|
+
return foundSupportedType !== null && foundSupportedType !== void 0 ? foundSupportedType : null;
|
|
112
|
+
}
|
|
113
|
+
onSuccessfullyStartedRecording(stream) {
|
|
114
|
+
this.pendingResult = new Promise((resolve, reject) => {
|
|
115
|
+
this.mediaRecorder = new MediaRecorder(stream);
|
|
116
|
+
this.mediaRecorder.onerror = () => {
|
|
117
|
+
this.prepareInstanceForNextOperation();
|
|
118
|
+
reject(failedToRecordError());
|
|
119
|
+
};
|
|
120
|
+
this.mediaRecorder.onstop = async () => {
|
|
121
|
+
const mimeType = VoiceRecorderImpl.getSupportedMimeType();
|
|
122
|
+
if (mimeType == null) {
|
|
123
|
+
this.prepareInstanceForNextOperation();
|
|
124
|
+
reject(failedToFetchRecordingError());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const blobVoiceRecording = new Blob(this.chunks, { 'type': mimeType });
|
|
128
|
+
if (blobVoiceRecording.size <= 0) {
|
|
129
|
+
this.prepareInstanceForNextOperation();
|
|
130
|
+
reject(emptyRecordingError());
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const recordDataBase64 = await VoiceRecorderImpl.blobToBase64(blobVoiceRecording);
|
|
134
|
+
const recordingDuration = await getBlobDuration(blobVoiceRecording);
|
|
135
|
+
this.prepareInstanceForNextOperation();
|
|
136
|
+
resolve({ value: { recordDataBase64, mimeType, msDuration: recordingDuration * 1000 } });
|
|
137
|
+
};
|
|
138
|
+
this.mediaRecorder.ondataavailable = (event) => this.chunks.push(event.data);
|
|
139
|
+
this.mediaRecorder.start();
|
|
140
|
+
});
|
|
141
|
+
return successResponse();
|
|
142
|
+
}
|
|
143
|
+
onFailedToStartRecording() {
|
|
144
|
+
this.prepareInstanceForNextOperation();
|
|
145
|
+
throw failedToRecordError();
|
|
146
|
+
}
|
|
147
|
+
static blobToBase64(blob) {
|
|
148
|
+
return new Promise(resolve => {
|
|
149
|
+
const reader = new FileReader();
|
|
150
|
+
reader.onloadend = () => {
|
|
151
|
+
const recordingResult = String(reader.result);
|
|
152
|
+
const splitResult = recordingResult.split('base64,');
|
|
153
|
+
const toResolve = (splitResult.length > 1) ? splitResult[1] : recordingResult;
|
|
154
|
+
resolve(toResolve.trim());
|
|
155
|
+
};
|
|
156
|
+
reader.readAsDataURL(blob);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
prepareInstanceForNextOperation() {
|
|
160
|
+
if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {
|
|
161
|
+
try {
|
|
162
|
+
this.mediaRecorder.stop();
|
|
163
|
+
}
|
|
164
|
+
catch (ignore) {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.pendingResult = neverResolvingPromise();
|
|
168
|
+
this.mediaRecorder = null;
|
|
169
|
+
this.chunks = [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=VoiceRecorderImpl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceRecorderImpl.js","sourceRoot":"","sources":["../../src/VoiceRecorderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import getBlobDuration from 'get-blob-duration';\r\n\r\nimport type {Base64String, CurrentRecordingStatus, GenericResponse, RecordingData} from './definitions';\r\nimport {\r\n alreadyRecordingError,\r\n couldNotQueryPermissionStatusError,\r\n deviceCannotVoiceRecordError,\r\n emptyRecordingError,\r\n failedToFetchRecordingError,\r\n failedToRecordError,\r\n failureResponse,\r\n missingPermissionError,\r\n recordingHasNotStartedError,\r\n successResponse,\r\n} from './predefined-web-responses';\r\n\r\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\r\nconst possibleMimeTypes = ['audio/mp4', 'audio/webm;codecs=opus', 'audio/aac', 'audio/webm', 'audio/ogg;codecs=opus'];\r\nconst neverResolvingPromise = (): Promise<any> => new Promise(() => undefined);\r\n\r\nexport class VoiceRecorderImpl {\r\n\r\n private mediaRecorder: MediaRecorder | null = null;\r\n private chunks: any[] = [];\r\n private pendingResult: Promise<RecordingData> = neverResolvingPromise();\r\n\r\n public static async canDeviceVoiceRecord(): Promise<GenericResponse> {\r\n if (navigator?.mediaDevices?.getUserMedia == null || VoiceRecorderImpl.getSupportedMimeType() == null) {\r\n return failureResponse();\r\n } else {\r\n return successResponse();\r\n }\r\n }\r\n\r\n public async startRecording(): Promise<GenericResponse> {\r\n if (this.mediaRecorder != null) {\r\n throw alreadyRecordingError();\r\n }\r\n const deviceCanRecord = await VoiceRecorderImpl.canDeviceVoiceRecord();\r\n if (!deviceCanRecord.value) {\r\n throw deviceCannotVoiceRecordError();\r\n }\r\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => successResponse());\r\n if (!havingPermission.value) {\r\n throw missingPermissionError();\r\n }\r\n\r\n return navigator.mediaDevices.getUserMedia({audio: true})\r\n .then(this.onSuccessfullyStartedRecording.bind(this))\r\n .catch(this.onFailedToStartRecording.bind(this));\r\n }\r\n\r\n public async stopRecording(): Promise<RecordingData> {\r\n if (this.mediaRecorder == null) {\r\n throw recordingHasNotStartedError();\r\n }\r\n try {\r\n this.mediaRecorder.stop();\r\n this.mediaRecorder.stream.getTracks().forEach(track => track.stop());\r\n return this.pendingResult;\r\n } catch (ignore) {\r\n throw failedToFetchRecordingError();\r\n } finally {\r\n this.prepareInstanceForNextOperation();\r\n }\r\n }\r\n\r\n public static async hasAudioRecordingPermission(): Promise<GenericResponse> {\r\n return navigator.permissions.query({name: 'microphone' as any})\r\n .then(result => ({value: result.state === 'granted'}))\r\n .catch(() => {\r\n throw couldNotQueryPermissionStatusError();\r\n });\r\n }\r\n\r\n public static async requestAudioRecordingPermission(): Promise<GenericResponse> {\r\n const havingPermission = await VoiceRecorderImpl.hasAudioRecordingPermission().catch(() => failureResponse());\r\n if (havingPermission.value) {\r\n return successResponse();\r\n }\r\n\r\n return navigator.mediaDevices.getUserMedia({audio: true})\r\n .then(() => successResponse())\r\n .catch(() => failureResponse());\r\n }\r\n\r\n public pauseRecording(): Promise<GenericResponse> {\r\n if (this.mediaRecorder == null) {\r\n throw recordingHasNotStartedError();\r\n } else if (this.mediaRecorder.state === 'recording') {\r\n this.mediaRecorder.pause();\r\n return Promise.resolve(successResponse());\r\n } else {\r\n return Promise.resolve(failureResponse());\r\n }\r\n }\r\n\r\n public resumeRecording(): Promise<GenericResponse> {\r\n if (this.mediaRecorder == null) {\r\n throw recordingHasNotStartedError();\r\n } else if (this.mediaRecorder.state === 'paused') {\r\n this.mediaRecorder.resume();\r\n return Promise.resolve(successResponse());\r\n } else {\r\n return Promise.resolve(failureResponse());\r\n }\r\n }\r\n\r\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\r\n if (this.mediaRecorder == null) {\r\n return Promise.resolve({status: 'NONE'});\r\n } else if (this.mediaRecorder.state === 'recording') {\r\n return Promise.resolve({status: 'RECORDING'});\r\n } else if (this.mediaRecorder.state === 'paused') {\r\n return Promise.resolve({status: 'PAUSED'});\r\n } else {\r\n return Promise.resolve({status: 'NONE'});\r\n }\r\n }\r\n\r\n public static getSupportedMimeType(): string | null {\r\n if (MediaRecorder?.isTypeSupported == null) return null;\r\n const foundSupportedType = possibleMimeTypes.find(type => MediaRecorder.isTypeSupported(type));\r\n return foundSupportedType ?? null;\r\n }\r\n\r\n private onSuccessfullyStartedRecording(stream: MediaStream): GenericResponse {\r\n this.pendingResult = new Promise((resolve, reject) => {\r\n this.mediaRecorder = new MediaRecorder(stream);\r\n this.mediaRecorder.onerror = () => {\r\n this.prepareInstanceForNextOperation();\r\n reject(failedToRecordError());\r\n };\r\n this.mediaRecorder.onstop = async () => {\r\n const mimeType = VoiceRecorderImpl.getSupportedMimeType();\r\n if (mimeType == null) {\r\n this.prepareInstanceForNextOperation();\r\n reject(failedToFetchRecordingError());\r\n return;\r\n }\r\n const blobVoiceRecording = new Blob(this.chunks, {'type': mimeType});\r\n if (blobVoiceRecording.size <= 0) {\r\n this.prepareInstanceForNextOperation();\r\n reject(emptyRecordingError());\r\n return;\r\n }\r\n const recordDataBase64 = await VoiceRecorderImpl.blobToBase64(blobVoiceRecording);\r\n const recordingDuration = await getBlobDuration(blobVoiceRecording);\r\n this.prepareInstanceForNextOperation();\r\n resolve({value: {recordDataBase64, mimeType, msDuration: recordingDuration * 1000}});\r\n };\r\n this.mediaRecorder.ondataavailable = (event: any) => this.chunks.push(event.data);\r\n this.mediaRecorder.start();\r\n });\r\n return successResponse();\r\n }\r\n\r\n private onFailedToStartRecording(): GenericResponse {\r\n this.prepareInstanceForNextOperation();\r\n throw failedToRecordError();\r\n }\r\n\r\n private static blobToBase64(blob: Blob): Promise<Base64String> {\r\n return new Promise(resolve => {\r\n const reader = new FileReader();\r\n reader.onloadend = () => {\r\n const recordingResult = String(reader.result);\r\n const splitResult = recordingResult.split('base64,');\r\n const toResolve = (splitResult.length > 1) ? splitResult[1] : recordingResult;\r\n resolve(toResolve.trim());\r\n };\r\n reader.readAsDataURL(blob);\r\n });\r\n }\r\n\r\n private prepareInstanceForNextOperation(): void {\r\n if (this.mediaRecorder != null && this.mediaRecorder.state === 'recording') {\r\n try {\r\n this.mediaRecorder.stop();\r\n } catch (ignore) {\r\n }\r\n }\r\n this.pendingResult = neverResolvingPromise();\r\n this.mediaRecorder = null;\r\n this.chunks = [];\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Base64String = string;
|
|
2
|
+
export interface RecordingData {
|
|
3
|
+
value: {
|
|
4
|
+
recordDataBase64: Base64String;
|
|
5
|
+
msDuration: number;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface GenericResponse {
|
|
10
|
+
value: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CurrentRecordingStatus {
|
|
13
|
+
status: 'RECORDING' | 'PAUSED' | 'NONE';
|
|
14
|
+
}
|
|
15
|
+
export interface VoiceRecorderPlugin {
|
|
16
|
+
canDeviceVoiceRecord(): Promise<GenericResponse>;
|
|
17
|
+
requestAudioRecordingPermission(): Promise<GenericResponse>;
|
|
18
|
+
hasAudioRecordingPermission(): Promise<GenericResponse>;
|
|
19
|
+
startRecording(): Promise<GenericResponse>;
|
|
20
|
+
stopRecording(): Promise<RecordingData>;
|
|
21
|
+
pauseRecording(): Promise<GenericResponse>;
|
|
22
|
+
resumeRecording(): Promise<GenericResponse>;
|
|
23
|
+
getCurrentStatus(): Promise<CurrentRecordingStatus>;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type Base64String = string\r\n\r\nexport interface RecordingData {\r\n value: {\r\n recordDataBase64: Base64String\r\n msDuration: number\r\n mimeType: string\r\n };\r\n}\r\n\r\nexport interface GenericResponse {\r\n value: boolean;\r\n}\r\n\r\nexport interface CurrentRecordingStatus {\r\n status: 'RECORDING' | 'PAUSED' | 'NONE';\r\n}\r\n\r\nexport interface VoiceRecorderPlugin {\r\n canDeviceVoiceRecord (): Promise<GenericResponse>;\r\n\r\n requestAudioRecordingPermission (): Promise<GenericResponse>;\r\n\r\n hasAudioRecordingPermission (): Promise<GenericResponse>;\r\n\r\n startRecording (): Promise<GenericResponse>;\r\n\r\n stopRecording (): Promise<RecordingData>;\r\n\r\n pauseRecording (): Promise<GenericResponse>;\r\n\r\n resumeRecording (): Promise<GenericResponse>;\r\n\r\n getCurrentStatus (): Promise<CurrentRecordingStatus>;\r\n\r\n}\r\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
const VoiceRecorder = registerPlugin('VoiceRecorder', {
|
|
3
|
+
web: () => import('./web').then(m => new m.VoiceRecorderWeb()),
|
|
4
|
+
});
|
|
5
|
+
export * from './definitions';
|
|
6
|
+
export { VoiceRecorder };
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,aAAa,GAAG,cAAc,CAAsB,eAAe,EAAE;IACzE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;CAC/D,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\r\n\r\nimport type { VoiceRecorderPlugin } from './definitions';\r\n\r\nconst VoiceRecorder = registerPlugin<VoiceRecorderPlugin>('VoiceRecorder', {\r\n web: () => import('./web').then(m => new m.VoiceRecorderWeb()),\r\n});\r\n\r\nexport * from './definitions';\r\nexport { VoiceRecorder };\r\n"]}
|