@independo/capacitor-voice-recorder 8.1.0-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.
Files changed (76) hide show
  1. package/README.md +40 -30
  2. package/android/build.gradle +44 -1
  3. package/android/src/main/java/app/independo/capacitorvoicerecorder/VoiceRecorder.java +146 -0
  4. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/PermissionChecker.java +8 -0
  5. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecordDataMapper.java +32 -0
  6. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java +39 -0
  7. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderPlatform.java +25 -0
  8. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/CurrentRecordingStatus.java +9 -0
  9. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ErrorCodes.java +19 -0
  10. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/Messages.java +2 -1
  11. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/RecordData.java +15 -1
  12. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/RecordOptions.java +4 -0
  13. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ResponseFormat.java +18 -0
  14. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/ResponseGenerator.java +7 -1
  15. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/platform}/CustomMediaRecorder.java +33 -2
  16. package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/DefaultRecorderPlatform.java +86 -0
  17. package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/NotSupportedOsVersion.java +4 -0
  18. package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderService.java +144 -0
  19. package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderServiceException.java +23 -0
  20. package/dist/esm/adapters/VoiceRecorderWebAdapter.d.ts +23 -0
  21. package/dist/esm/adapters/VoiceRecorderWebAdapter.js +41 -0
  22. package/dist/esm/adapters/VoiceRecorderWebAdapter.js.map +1 -0
  23. package/dist/esm/core/error-codes.d.ts +4 -0
  24. package/dist/esm/core/error-codes.js +21 -0
  25. package/dist/esm/core/error-codes.js.map +1 -0
  26. package/dist/esm/core/recording-contract.d.ts +3 -0
  27. package/dist/esm/core/recording-contract.js +15 -0
  28. package/dist/esm/core/recording-contract.js.map +1 -0
  29. package/dist/esm/core/response-format.d.ts +8 -0
  30. package/dist/esm/core/response-format.js +17 -0
  31. package/dist/esm/core/response-format.js.map +1 -0
  32. package/dist/esm/platform/web/VoiceRecorderImpl.d.ts +45 -0
  33. package/dist/esm/{VoiceRecorderImpl.js → platform/web/VoiceRecorderImpl.js} +20 -2
  34. package/dist/esm/platform/web/VoiceRecorderImpl.js.map +1 -0
  35. package/dist/esm/platform/web/get-blob-duration.js.map +1 -0
  36. package/dist/esm/{predefined-web-responses.d.ts → platform/web/predefined-web-responses.d.ts} +12 -1
  37. package/dist/esm/{predefined-web-responses.js → platform/web/predefined-web-responses.js} +11 -0
  38. package/dist/esm/platform/web/predefined-web-responses.js.map +1 -0
  39. package/dist/esm/service/VoiceRecorderService.d.ts +47 -0
  40. package/dist/esm/service/VoiceRecorderService.js +60 -0
  41. package/dist/esm/service/VoiceRecorderService.js.map +1 -0
  42. package/dist/esm/web.d.ts +12 -1
  43. package/dist/esm/web.js +26 -12
  44. package/dist/esm/web.js.map +1 -1
  45. package/dist/plugin.cjs.js +200 -9
  46. package/dist/plugin.cjs.js.map +1 -1
  47. package/dist/plugin.js +200 -9
  48. package/dist/plugin.js.map +1 -1
  49. package/ios/Sources/VoiceRecorder/Adapters/DefaultRecorderPlatform.swift +33 -0
  50. package/ios/Sources/VoiceRecorder/Adapters/RecordDataMapper.swift +38 -0
  51. package/ios/Sources/VoiceRecorder/Adapters/RecorderAdapter.swift +24 -0
  52. package/ios/Sources/VoiceRecorder/Adapters/RecorderPlatform.swift +11 -0
  53. package/ios/Sources/VoiceRecorder/Bridge/VoiceRecorder.swift +172 -0
  54. package/ios/Sources/VoiceRecorder/{CurrentRecordingStatus.swift → Core/CurrentRecordingStatus.swift} +1 -0
  55. package/ios/Sources/VoiceRecorder/Core/ErrorCodes.swift +16 -0
  56. package/ios/Sources/VoiceRecorder/{Messages.swift → Core/Messages.swift} +1 -0
  57. package/ios/Sources/VoiceRecorder/{RecordData.swift → Core/RecordData.swift} +6 -0
  58. package/ios/Sources/VoiceRecorder/Core/RecordOptions.swift +11 -0
  59. package/ios/Sources/VoiceRecorder/Core/ResponseFormat.swift +22 -0
  60. package/ios/Sources/VoiceRecorder/{ResponseGenerator.swift → Core/ResponseGenerator.swift} +6 -0
  61. package/ios/Sources/VoiceRecorder/{CustomMediaRecorder.swift → Platform/CustomMediaRecorder.swift} +25 -1
  62. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderService.swift +128 -0
  63. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderServiceError.swift +14 -0
  64. package/package.json +10 -4
  65. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +0 -8
  66. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/NotSupportedOsVersion.java +0 -3
  67. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/RecordOptions.java +0 -3
  68. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +0 -205
  69. package/dist/esm/VoiceRecorderImpl.d.ts +0 -27
  70. package/dist/esm/VoiceRecorderImpl.js.map +0 -1
  71. package/dist/esm/helper/get-blob-duration.js.map +0 -1
  72. package/dist/esm/predefined-web-responses.js.map +0 -1
  73. package/ios/Sources/VoiceRecorder/RecordOptions.swift +0 -8
  74. package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +0 -170
  75. /package/dist/esm/{helper → platform/web}/get-blob-duration.d.ts +0 -0
  76. /package/dist/esm/{helper → platform/web}/get-blob-duration.js +0 -0
@@ -1,4 +1,4 @@
1
- package com.tchvu3.capacitorvoicerecorder;
1
+ package app.independo.capacitorvoicerecorder.platform;
2
2
 
3
3
  import android.content.Context;
4
4
  import android.media.AudioAttributes;
@@ -7,21 +7,34 @@ import android.media.AudioManager;
7
7
  import android.media.MediaRecorder;
8
8
  import android.os.Build;
9
9
  import android.os.Environment;
10
+ import app.independo.capacitorvoicerecorder.adapters.RecorderAdapter;
11
+ import app.independo.capacitorvoicerecorder.core.CurrentRecordingStatus;
12
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
10
13
  import java.io.File;
11
14
  import java.io.IOException;
12
15
  import java.util.regex.Matcher;
13
16
  import java.util.regex.Pattern;
14
17
 
15
- public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListener {
18
+ /** MediaRecorder wrapper that manages audio focus and interruptions. */
19
+ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListener, RecorderAdapter {
16
20
 
21
+ /** Android context for file paths and system services. */
17
22
  private final Context context;
23
+ /** Recording options passed from the service layer. */
18
24
  private final RecordOptions options;
25
+ /** Active MediaRecorder instance for the session. */
19
26
  private MediaRecorder mediaRecorder;
27
+ /** Output file for the current recording session. */
20
28
  private File outputFile;
29
+ /** Current session status tracked locally. */
21
30
  private CurrentRecordingStatus currentRecordingStatus = CurrentRecordingStatus.NONE;
31
+ /** Audio manager for focus changes. */
22
32
  private AudioManager audioManager;
33
+ /** Focus request for Android O and above. */
23
34
  private AudioFocusRequest audioFocusRequest;
35
+ /** Callback invoked when an interruption begins. */
24
36
  private Runnable onInterruptionBegan;
37
+ /** Callback invoked when an interruption ends. */
25
38
  private Runnable onInterruptionEnded;
26
39
 
27
40
  public CustomMediaRecorder(Context context, RecordOptions options) throws IOException {
@@ -31,14 +44,17 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
31
44
  generateMediaRecorder();
32
45
  }
33
46
 
47
+ /** Sets the callback for interruption begin events. */
34
48
  public void setOnInterruptionBegan(Runnable callback) {
35
49
  this.onInterruptionBegan = callback;
36
50
  }
37
51
 
52
+ /** Sets the callback for interruption end events. */
38
53
  public void setOnInterruptionEnded(Runnable callback) {
39
54
  this.onInterruptionEnded = callback;
40
55
  }
41
56
 
57
+ /** Configures the MediaRecorder with audio settings. */
42
58
  private void generateMediaRecorder() throws IOException {
43
59
  mediaRecorder = new MediaRecorder();
44
60
  mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
@@ -50,6 +66,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
50
66
  mediaRecorder.prepare();
51
67
  }
52
68
 
69
+ /** Picks a directory and allocates the output file for this session. */
53
70
  private void setRecorderOutputFile() throws IOException {
54
71
  File outputDir = context.getCacheDir();
55
72
 
@@ -79,6 +96,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
79
96
  mediaRecorder.setOutputFile(outputFile.getAbsolutePath());
80
97
  }
81
98
 
99
+ /** Maps directory strings to Android file locations. */
82
100
  private File getDirectory(String directory) {
83
101
  return switch (directory) {
84
102
  case "DOCUMENTS" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
@@ -90,12 +108,14 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
90
108
  };
91
109
  }
92
110
 
111
+ /** Starts recording and requests audio focus. */
93
112
  public void startRecording() {
94
113
  requestAudioFocus();
95
114
  mediaRecorder.start();
96
115
  currentRecordingStatus = CurrentRecordingStatus.RECORDING;
97
116
  }
98
117
 
118
+ /** Stops recording and releases audio resources. */
99
119
  public void stopRecording() {
100
120
  if (mediaRecorder == null) {
101
121
  abandonAudioFocus();
@@ -118,14 +138,17 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
118
138
  }
119
139
  }
120
140
 
141
+ /** Returns the output file for the current session. */
121
142
  public File getOutputFile() {
122
143
  return outputFile;
123
144
  }
124
145
 
146
+ /** Returns the options provided at start time. */
125
147
  public RecordOptions getRecordOptions() {
126
148
  return options;
127
149
  }
128
150
 
151
+ /** Pauses recording when supported by the OS version. */
129
152
  public boolean pauseRecording() throws NotSupportedOsVersion {
130
153
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
131
154
  throw new NotSupportedOsVersion();
@@ -140,6 +163,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
140
163
  }
141
164
  }
142
165
 
166
+ /** Resumes a paused or interrupted recording session. */
143
167
  public boolean resumeRecording() throws NotSupportedOsVersion {
144
168
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
145
169
  throw new NotSupportedOsVersion();
@@ -155,18 +179,22 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
155
179
  }
156
180
  }
157
181
 
182
+ /** Returns the current recording status. */
158
183
  public CurrentRecordingStatus getCurrentStatus() {
159
184
  return currentRecordingStatus;
160
185
  }
161
186
 
187
+ /** Deletes the output file from disk. */
162
188
  public boolean deleteOutputFile() {
163
189
  return outputFile.delete();
164
190
  }
165
191
 
192
+ /** Simple capability check used for device validation. */
166
193
  public static boolean canPhoneCreateMediaRecorder(Context context) {
167
194
  return true;
168
195
  }
169
196
 
197
+ /** Attempts to record a short sample to validate permission and hardware. */
170
198
  private static boolean canPhoneCreateMediaRecorderWhileHavingPermission(Context context) {
171
199
  CustomMediaRecorder tempMediaRecorder = null;
172
200
  try {
@@ -181,6 +209,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
181
209
  }
182
210
  }
183
211
 
212
+ /** Requests audio focus for the recording session. */
184
213
  private void requestAudioFocus() {
185
214
  if (audioManager == null) {
186
215
  return;
@@ -203,6 +232,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
203
232
  }
204
233
  }
205
234
 
235
+ /** Releases audio focus when recording completes. */
206
236
  private void abandonAudioFocus() {
207
237
  if (audioManager == null) {
208
238
  return;
@@ -216,6 +246,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
216
246
  }
217
247
  }
218
248
 
249
+ /** Handles audio focus changes as recording interruptions. */
219
250
  @Override
220
251
  public void onAudioFocusChange(int focusChange) {
221
252
  switch (focusChange) {
@@ -0,0 +1,86 @@
1
+ package app.independo.capacitorvoicerecorder.platform;
2
+
3
+ import android.content.Context;
4
+ import android.media.AudioManager;
5
+ import android.media.MediaPlayer;
6
+ import android.net.Uri;
7
+ import android.util.Base64;
8
+ import app.independo.capacitorvoicerecorder.adapters.RecorderAdapter;
9
+ import app.independo.capacitorvoicerecorder.adapters.RecorderPlatform;
10
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
11
+ import java.io.BufferedInputStream;
12
+ import java.io.ByteArrayOutputStream;
13
+ import java.io.File;
14
+ import java.io.FileInputStream;
15
+ import java.io.IOException;
16
+
17
+ /** Default Android platform adapter for recording and file IO. */
18
+ public class DefaultRecorderPlatform implements RecorderPlatform {
19
+
20
+ /** Android context used to access system services. */
21
+ private final Context context;
22
+
23
+ public DefaultRecorderPlatform(Context context) {
24
+ this.context = context;
25
+ }
26
+
27
+ /** Returns whether the device can create a MediaRecorder instance. */
28
+ @Override
29
+ public boolean canDeviceVoiceRecord() {
30
+ return CustomMediaRecorder.canPhoneCreateMediaRecorder(context);
31
+ }
32
+
33
+ /** Returns true when the system audio mode indicates another recorder. */
34
+ @Override
35
+ public boolean isMicrophoneOccupied() {
36
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
37
+ if (audioManager == null) return true;
38
+ return audioManager.getMode() != AudioManager.MODE_NORMAL;
39
+ }
40
+
41
+ /** Creates the recorder adapter for the provided options. */
42
+ @Override
43
+ public RecorderAdapter createRecorder(RecordOptions options) throws Exception {
44
+ return new CustomMediaRecorder(context, options);
45
+ }
46
+
47
+ /** Reads the recorded file as base64, returning null on failure. */
48
+ @Override
49
+ public String readFileAsBase64(File recordedFile) {
50
+ try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(recordedFile))) {
51
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
52
+ byte[] buffer = new byte[8192];
53
+ int bytesRead;
54
+ while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
55
+ outputStream.write(buffer, 0, bytesRead);
56
+ }
57
+ return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT);
58
+ } catch (IOException exp) {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** Returns the file duration in milliseconds, or -1 on failure. */
64
+ @Override
65
+ public int getDurationMs(File recordedFile) {
66
+ MediaPlayer mediaPlayer = null;
67
+ try {
68
+ mediaPlayer = new MediaPlayer();
69
+ mediaPlayer.setDataSource(recordedFile.getAbsolutePath());
70
+ mediaPlayer.prepare();
71
+ return mediaPlayer.getDuration();
72
+ } catch (Exception ignore) {
73
+ return -1;
74
+ } finally {
75
+ if (mediaPlayer != null) {
76
+ mediaPlayer.release();
77
+ }
78
+ }
79
+ }
80
+
81
+ /** Returns a file:// URI for the given recording. */
82
+ @Override
83
+ public String toUri(File recordedFile) {
84
+ return Uri.fromFile(recordedFile).toString();
85
+ }
86
+ }
@@ -0,0 +1,4 @@
1
+ package app.independo.capacitorvoicerecorder.platform;
2
+
3
+ /** Signals that a feature is not supported on the current OS version. */
4
+ public class NotSupportedOsVersion extends Exception {}
@@ -0,0 +1,144 @@
1
+ package app.independo.capacitorvoicerecorder.service;
2
+
3
+ import app.independo.capacitorvoicerecorder.adapters.PermissionChecker;
4
+ import app.independo.capacitorvoicerecorder.adapters.RecorderAdapter;
5
+ import app.independo.capacitorvoicerecorder.adapters.RecorderPlatform;
6
+ import app.independo.capacitorvoicerecorder.core.CurrentRecordingStatus;
7
+ import app.independo.capacitorvoicerecorder.core.ErrorCodes;
8
+ import app.independo.capacitorvoicerecorder.core.RecordData;
9
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
10
+ import app.independo.capacitorvoicerecorder.platform.NotSupportedOsVersion;
11
+ import java.io.File;
12
+
13
+ /** Service layer that orchestrates recording operations. */
14
+ public class VoiceRecorderService {
15
+
16
+ /** Platform adapter that owns file and recorder creation. */
17
+ private final RecorderPlatform platform;
18
+ /** Permission checker injected from the bridge layer. */
19
+ private final PermissionChecker permissionChecker;
20
+ /** Current recorder instance for an active session. */
21
+ private RecorderAdapter recorder;
22
+
23
+ public VoiceRecorderService(RecorderPlatform platform, PermissionChecker permissionChecker) {
24
+ this.platform = platform;
25
+ this.permissionChecker = permissionChecker;
26
+ }
27
+
28
+ /** Returns whether the device can record audio. */
29
+ public boolean canDeviceVoiceRecord() {
30
+ return platform.canDeviceVoiceRecord();
31
+ }
32
+
33
+ /** Returns whether the app has microphone permission. */
34
+ public boolean hasAudioRecordingPermission() {
35
+ return permissionChecker.hasAudioPermission();
36
+ }
37
+
38
+ /** Starts a recording session or throws a service exception. */
39
+ public void startRecording(
40
+ RecordOptions options,
41
+ Runnable onInterruptionBegan,
42
+ Runnable onInterruptionEnded
43
+ ) throws VoiceRecorderServiceException {
44
+ if (!platform.canDeviceVoiceRecord()) {
45
+ throw new VoiceRecorderServiceException(ErrorCodes.DEVICE_CANNOT_VOICE_RECORD);
46
+ }
47
+
48
+ if (!permissionChecker.hasAudioPermission()) {
49
+ throw new VoiceRecorderServiceException(ErrorCodes.MISSING_PERMISSION);
50
+ }
51
+
52
+ if (platform.isMicrophoneOccupied()) {
53
+ throw new VoiceRecorderServiceException(ErrorCodes.MICROPHONE_BEING_USED);
54
+ }
55
+
56
+ if (recorder != null) {
57
+ throw new VoiceRecorderServiceException(ErrorCodes.ALREADY_RECORDING);
58
+ }
59
+
60
+ try {
61
+ recorder = platform.createRecorder(options);
62
+ recorder.setOnInterruptionBegan(onInterruptionBegan);
63
+ recorder.setOnInterruptionEnded(onInterruptionEnded);
64
+ recorder.startRecording();
65
+ } catch (Exception exp) {
66
+ recorder = null;
67
+ throw new VoiceRecorderServiceException(ErrorCodes.FAILED_TO_RECORD, exp);
68
+ }
69
+ }
70
+
71
+ /** Stops the active recording session and returns the payload. */
72
+ public RecordData stopRecording() throws VoiceRecorderServiceException {
73
+ if (recorder == null) {
74
+ throw new VoiceRecorderServiceException(ErrorCodes.RECORDING_HAS_NOT_STARTED);
75
+ }
76
+
77
+ RecordOptions options = recorder.getRecordOptions();
78
+
79
+ try {
80
+ recorder.stopRecording();
81
+ File recordedFile = recorder.getOutputFile();
82
+ if (recordedFile == null) {
83
+ throw new VoiceRecorderServiceException(ErrorCodes.FAILED_TO_FETCH_RECORDING);
84
+ }
85
+
86
+ String recordDataBase64 = null;
87
+ String uri = null;
88
+ if (options.directory() != null) {
89
+ uri = platform.toUri(recordedFile);
90
+ } else {
91
+ recordDataBase64 = platform.readFileAsBase64(recordedFile);
92
+ }
93
+
94
+ int duration = platform.getDurationMs(recordedFile);
95
+ RecordData recordData = new RecordData(recordDataBase64, duration, "audio/aac", uri);
96
+ if ((recordDataBase64 == null && uri == null) || recordData.getMsDuration() < 0) {
97
+ throw new VoiceRecorderServiceException(ErrorCodes.EMPTY_RECORDING);
98
+ }
99
+
100
+ return recordData;
101
+ } catch (VoiceRecorderServiceException exp) {
102
+ throw exp;
103
+ } catch (Exception exp) {
104
+ throw new VoiceRecorderServiceException(ErrorCodes.FAILED_TO_FETCH_RECORDING, exp);
105
+ } finally {
106
+ if (options.directory() == null && recorder != null) {
107
+ recorder.deleteOutputFile();
108
+ }
109
+ recorder = null;
110
+ }
111
+ }
112
+
113
+ /** Pauses the active recording session. */
114
+ public boolean pauseRecording() throws VoiceRecorderServiceException {
115
+ if (recorder == null) {
116
+ throw new VoiceRecorderServiceException(ErrorCodes.RECORDING_HAS_NOT_STARTED);
117
+ }
118
+ try {
119
+ return recorder.pauseRecording();
120
+ } catch (NotSupportedOsVersion exception) {
121
+ throw new VoiceRecorderServiceException(ErrorCodes.NOT_SUPPORTED_OS_VERSION, exception);
122
+ }
123
+ }
124
+
125
+ /** Resumes a paused recording session. */
126
+ public boolean resumeRecording() throws VoiceRecorderServiceException {
127
+ if (recorder == null) {
128
+ throw new VoiceRecorderServiceException(ErrorCodes.RECORDING_HAS_NOT_STARTED);
129
+ }
130
+ try {
131
+ return recorder.resumeRecording();
132
+ } catch (NotSupportedOsVersion exception) {
133
+ throw new VoiceRecorderServiceException(ErrorCodes.NOT_SUPPORTED_OS_VERSION, exception);
134
+ }
135
+ }
136
+
137
+ /** Returns the current recording status. */
138
+ public CurrentRecordingStatus getCurrentStatus() {
139
+ if (recorder == null) {
140
+ return CurrentRecordingStatus.NONE;
141
+ }
142
+ return recorder.getCurrentStatus();
143
+ }
144
+ }
@@ -0,0 +1,23 @@
1
+ package app.independo.capacitorvoicerecorder.service;
2
+
3
+ /** Exception wrapper that carries a canonical error code. */
4
+ public class VoiceRecorderServiceException extends Exception {
5
+
6
+ /** Canonical error code mapped to plugin responses. */
7
+ private final String code;
8
+
9
+ public VoiceRecorderServiceException(String code) {
10
+ super(code);
11
+ this.code = code;
12
+ }
13
+
14
+ public VoiceRecorderServiceException(String code, Exception cause) {
15
+ super(code, cause);
16
+ this.code = code;
17
+ }
18
+
19
+ /** Returns the canonical error code for this failure. */
20
+ public String getCode() {
21
+ return code;
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../definitions';
2
+ import type { VoiceRecorderPlatform } from '../service/VoiceRecorderService';
3
+ /** Web adapter that delegates to the browser-specific implementation. */
4
+ export declare class VoiceRecorderWebAdapter implements VoiceRecorderPlatform {
5
+ /** Browser implementation that talks to MediaRecorder APIs. */
6
+ private readonly voiceRecorderImpl;
7
+ /** Checks whether the browser can record audio. */
8
+ canDeviceVoiceRecord(): Promise<GenericResponse>;
9
+ /** Returns whether the browser has microphone permission. */
10
+ hasAudioRecordingPermission(): Promise<GenericResponse>;
11
+ /** Requests microphone permission through the browser. */
12
+ requestAudioRecordingPermission(): Promise<GenericResponse>;
13
+ /** Starts a recording session using MediaRecorder. */
14
+ startRecording(options?: RecordingOptions): Promise<GenericResponse>;
15
+ /** Stops the recording session and returns the payload. */
16
+ stopRecording(): Promise<RecordingData>;
17
+ /** Pauses the recording session when supported. */
18
+ pauseRecording(): Promise<GenericResponse>;
19
+ /** Resumes a paused recording session when supported. */
20
+ resumeRecording(): Promise<GenericResponse>;
21
+ /** Returns the current recording state. */
22
+ getCurrentStatus(): Promise<CurrentRecordingStatus>;
23
+ }
@@ -0,0 +1,41 @@
1
+ import { VoiceRecorderImpl } from '../platform/web/VoiceRecorderImpl';
2
+ /** Web adapter that delegates to the browser-specific implementation. */
3
+ export class VoiceRecorderWebAdapter {
4
+ constructor() {
5
+ /** Browser implementation that talks to MediaRecorder APIs. */
6
+ this.voiceRecorderImpl = new VoiceRecorderImpl();
7
+ }
8
+ /** Checks whether the browser can record audio. */
9
+ canDeviceVoiceRecord() {
10
+ return VoiceRecorderImpl.canDeviceVoiceRecord();
11
+ }
12
+ /** Returns whether the browser has microphone permission. */
13
+ hasAudioRecordingPermission() {
14
+ return VoiceRecorderImpl.hasAudioRecordingPermission();
15
+ }
16
+ /** Requests microphone permission through the browser. */
17
+ requestAudioRecordingPermission() {
18
+ return VoiceRecorderImpl.requestAudioRecordingPermission();
19
+ }
20
+ /** Starts a recording session using MediaRecorder. */
21
+ startRecording(options) {
22
+ return this.voiceRecorderImpl.startRecording(options);
23
+ }
24
+ /** Stops the recording session and returns the payload. */
25
+ stopRecording() {
26
+ return this.voiceRecorderImpl.stopRecording();
27
+ }
28
+ /** Pauses the recording session when supported. */
29
+ pauseRecording() {
30
+ return this.voiceRecorderImpl.pauseRecording();
31
+ }
32
+ /** Resumes a paused recording session when supported. */
33
+ resumeRecording() {
34
+ return this.voiceRecorderImpl.resumeRecording();
35
+ }
36
+ /** Returns the current recording state. */
37
+ getCurrentStatus() {
38
+ return this.voiceRecorderImpl.getCurrentStatus();
39
+ }
40
+ }
41
+ //# sourceMappingURL=VoiceRecorderWebAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceRecorderWebAdapter.js","sourceRoot":"","sources":["../../../src/adapters/VoiceRecorderWebAdapter.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtE,yEAAyE;AACzE,MAAM,OAAO,uBAAuB;IAApC;QACI,+DAA+D;QAC9C,sBAAiB,GAAG,IAAI,iBAAiB,EAAE,CAAC;IAyCjE,CAAC;IAvCG,mDAAmD;IAC5C,oBAAoB;QACvB,OAAO,iBAAiB,CAAC,oBAAoB,EAAE,CAAC;IACpD,CAAC;IAED,6DAA6D;IACtD,2BAA2B;QAC9B,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;IAC3D,CAAC;IAED,0DAA0D;IACnD,+BAA+B;QAClC,OAAO,iBAAiB,CAAC,+BAA+B,EAAE,CAAC;IAC/D,CAAC;IAED,sDAAsD;IAC/C,cAAc,CAAC,OAA0B;QAC5C,OAAO,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IAED,2DAA2D;IACpD,aAAa;QAChB,OAAO,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IAED,mDAAmD;IAC5C,cAAc;QACjB,OAAO,IAAI,CAAC,iBAAiB,CAAC,cAAc,EAAE,CAAC;IACnD,CAAC;IAED,yDAAyD;IAClD,eAAe;QAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,CAAC;IACpD,CAAC;IAED,2CAA2C;IACpC,gBAAgB;QACnB,OAAO,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;IACrD,CAAC;CACJ","sourcesContent":["import type {\n CurrentRecordingStatus,\n GenericResponse,\n RecordingData,\n RecordingOptions,\n} from '../definitions';\nimport { VoiceRecorderImpl } from '../platform/web/VoiceRecorderImpl';\nimport type { VoiceRecorderPlatform } from '../service/VoiceRecorderService';\n\n/** Web adapter that delegates to the browser-specific implementation. */\nexport class VoiceRecorderWebAdapter implements VoiceRecorderPlatform {\n /** Browser implementation that talks to MediaRecorder APIs. */\n private readonly voiceRecorderImpl = new VoiceRecorderImpl();\n\n /** Checks whether the browser can record audio. */\n public canDeviceVoiceRecord(): Promise<GenericResponse> {\n return VoiceRecorderImpl.canDeviceVoiceRecord();\n }\n\n /** Returns whether the browser has microphone permission. */\n public hasAudioRecordingPermission(): Promise<GenericResponse> {\n return VoiceRecorderImpl.hasAudioRecordingPermission();\n }\n\n /** Requests microphone permission through the browser. */\n public requestAudioRecordingPermission(): Promise<GenericResponse> {\n return VoiceRecorderImpl.requestAudioRecordingPermission();\n }\n\n /** Starts a recording session using MediaRecorder. */\n public startRecording(options?: RecordingOptions): Promise<GenericResponse> {\n return this.voiceRecorderImpl.startRecording(options);\n }\n\n /** Stops the recording session and returns the payload. */\n public stopRecording(): Promise<RecordingData> {\n return this.voiceRecorderImpl.stopRecording();\n }\n\n /** Pauses the recording session when supported. */\n public pauseRecording(): Promise<GenericResponse> {\n return this.voiceRecorderImpl.pauseRecording();\n }\n\n /** Resumes a paused recording session when supported. */\n public resumeRecording(): Promise<GenericResponse> {\n return this.voiceRecorderImpl.resumeRecording();\n }\n\n /** Returns the current recording state. */\n public getCurrentStatus(): Promise<CurrentRecordingStatus> {\n return this.voiceRecorderImpl.getCurrentStatus();\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ /** Normalizes legacy error messages into canonical error codes. */
2
+ export declare const toCanonicalErrorCode: (legacyMessage: string) => string;
3
+ /** Adds a canonical `code` field to Error-like objects when possible. */
4
+ export declare const attachCanonicalErrorCode: (error: unknown) => void;
@@ -0,0 +1,21 @@
1
+ /** Maps legacy error messages to canonical error codes. */
2
+ const legacyToCanonical = {
3
+ CANNOT_RECORD_ON_THIS_PHONE: 'DEVICE_CANNOT_VOICE_RECORD',
4
+ };
5
+ /** Normalizes legacy error messages into canonical error codes. */
6
+ export const toCanonicalErrorCode = (legacyMessage) => {
7
+ var _a;
8
+ return (_a = legacyToCanonical[legacyMessage]) !== null && _a !== void 0 ? _a : legacyMessage;
9
+ };
10
+ /** Adds a canonical `code` field to Error-like objects when possible. */
11
+ export const attachCanonicalErrorCode = (error) => {
12
+ if (!error || typeof error !== 'object') {
13
+ return;
14
+ }
15
+ const messageValue = error.message;
16
+ if (typeof messageValue !== 'string') {
17
+ return;
18
+ }
19
+ error.code = toCanonicalErrorCode(messageValue);
20
+ };
21
+ //# sourceMappingURL=error-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../../../src/core/error-codes.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,iBAAiB,GAA2B;IAC9C,2BAA2B,EAAE,4BAA4B;CAC5D,CAAC;AAEF,mEAAmE;AACnE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,aAAqB,EAAU,EAAE;;IAClE,OAAO,MAAA,iBAAiB,CAAC,aAAa,CAAC,mCAAI,aAAa,CAAC;AAC7D,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,KAAc,EAAQ,EAAE;IAC7D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO;IACX,CAAC;IACD,MAAM,YAAY,GAAI,KAA+B,CAAC,OAAO,CAAC;IAC9D,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;QACnC,OAAO;IACX,CAAC;IACA,KAA2B,CAAC,IAAI,GAAG,oBAAoB,CAAC,YAAY,CAAC,CAAC;AAC3E,CAAC,CAAC","sourcesContent":["/** Maps legacy error messages to canonical error codes. */\nconst legacyToCanonical: Record<string, string> = {\n CANNOT_RECORD_ON_THIS_PHONE: 'DEVICE_CANNOT_VOICE_RECORD',\n};\n\n/** Normalizes legacy error messages into canonical error codes. */\nexport const toCanonicalErrorCode = (legacyMessage: string): string => {\n return legacyToCanonical[legacyMessage] ?? legacyMessage;\n};\n\n/** Adds a canonical `code` field to Error-like objects when possible. */\nexport const attachCanonicalErrorCode = (error: unknown): void => {\n if (!error || typeof error !== 'object') {\n return;\n }\n const messageValue = (error as { message?: unknown }).message;\n if (typeof messageValue !== 'string') {\n return;\n }\n (error as { code?: string }).code = toCanonicalErrorCode(messageValue);\n};\n"]}
@@ -0,0 +1,3 @@
1
+ import type { RecordingData } from '../definitions';
2
+ /** Normalizes recording payloads into a stable contract shape. */
3
+ export declare const normalizeRecordingData: (data: RecordingData) => RecordingData;
@@ -0,0 +1,15 @@
1
+ /** Normalizes recording payloads into a stable contract shape. */
2
+ export const normalizeRecordingData = (data) => {
3
+ const { recordDataBase64, uri, msDuration, mimeType } = data.value;
4
+ const normalizedValue = { msDuration, mimeType };
5
+ const trimmedUri = typeof uri === 'string' && uri.length > 0 ? uri : undefined;
6
+ const trimmedBase64 = typeof recordDataBase64 === 'string' && recordDataBase64.length > 0 ? recordDataBase64 : undefined;
7
+ if (trimmedUri) {
8
+ normalizedValue.uri = trimmedUri;
9
+ }
10
+ else if (trimmedBase64) {
11
+ normalizedValue.recordDataBase64 = trimmedBase64;
12
+ }
13
+ return { value: normalizedValue };
14
+ };
15
+ //# sourceMappingURL=recording-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording-contract.js","sourceRoot":"","sources":["../../../src/core/recording-contract.ts"],"names":[],"mappings":"AAEA,kEAAkE;AAClE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,IAAmB,EAAiB,EAAE;IACzE,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;IACnE,MAAM,eAAe,GAA4B,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IAC1E,MAAM,UAAU,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/E,MAAM,aAAa,GAAG,OAAO,gBAAgB,KAAK,QAAQ,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC;IAEzH,IAAI,UAAU,EAAE,CAAC;QACb,eAAe,CAAC,GAAG,GAAG,UAAU,CAAC;IACrC,CAAC;SAAM,IAAI,aAAa,EAAE,CAAC;QACvB,eAAe,CAAC,gBAAgB,GAAG,aAAa,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,eAAe,EAAmB,CAAC;AACvD,CAAC,CAAC","sourcesContent":["import type { RecordingData } from '../definitions';\n\n/** Normalizes recording payloads into a stable contract shape. */\nexport const normalizeRecordingData = (data: RecordingData): RecordingData => {\n const { recordDataBase64, uri, msDuration, mimeType } = data.value;\n const normalizedValue: Record<string, unknown> = { msDuration, mimeType };\n const trimmedUri = typeof uri === 'string' && uri.length > 0 ? uri : undefined;\n const trimmedBase64 = typeof recordDataBase64 === 'string' && recordDataBase64.length > 0 ? recordDataBase64 : undefined;\n\n if (trimmedUri) {\n normalizedValue.uri = trimmedUri;\n } else if (trimmedBase64) {\n normalizedValue.recordDataBase64 = trimmedBase64;\n }\n\n return { value: normalizedValue } as RecordingData;\n};\n"]}
@@ -0,0 +1,8 @@
1
+ /** Supported response shapes for plugin results. */
2
+ export type ResponseFormat = 'legacy' | 'normalized';
3
+ /** Default response shape when no config is provided. */
4
+ export declare const DEFAULT_RESPONSE_FORMAT: ResponseFormat;
5
+ /** Parses a user-provided response format into a supported value. */
6
+ export declare const resolveResponseFormat: (value: unknown) => ResponseFormat;
7
+ /** Reads the response format from a Capacitor plugin config object. */
8
+ export declare const getResponseFormatFromConfig: (config: unknown) => ResponseFormat;
@@ -0,0 +1,17 @@
1
+ /** Default response shape when no config is provided. */
2
+ export const DEFAULT_RESPONSE_FORMAT = 'legacy';
3
+ /** Parses a user-provided response format into a supported value. */
4
+ export const resolveResponseFormat = (value) => {
5
+ if (typeof value === 'string' && value.toLowerCase() === 'normalized') {
6
+ return 'normalized';
7
+ }
8
+ return DEFAULT_RESPONSE_FORMAT;
9
+ };
10
+ /** Reads the response format from a Capacitor plugin config object. */
11
+ export const getResponseFormatFromConfig = (config) => {
12
+ if (config && typeof config === 'object' && 'responseFormat' in config) {
13
+ return resolveResponseFormat(config.responseFormat);
14
+ }
15
+ return DEFAULT_RESPONSE_FORMAT;
16
+ };
17
+ //# sourceMappingURL=response-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-format.js","sourceRoot":"","sources":["../../../src/core/response-format.ts"],"names":[],"mappings":"AAGA,yDAAyD;AACzD,MAAM,CAAC,MAAM,uBAAuB,GAAmB,QAAQ,CAAC;AAEhE,qEAAqE;AACrE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAc,EAAkB,EAAE;IACpE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC;QACpE,OAAO,YAAY,CAAC;IACxB,CAAC;IACD,OAAO,uBAAuB,CAAC;AACnC,CAAC,CAAC;AAEF,uEAAuE;AACvE,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,MAAe,EAAkB,EAAE;IAC3E,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,gBAAgB,IAAI,MAAM,EAAE,CAAC;QACrE,OAAO,qBAAqB,CAAE,MAAuC,CAAC,cAAc,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,uBAAuB,CAAC;AACnC,CAAC,CAAC","sourcesContent":["/** Supported response shapes for plugin results. */\nexport type ResponseFormat = 'legacy' | 'normalized';\n\n/** Default response shape when no config is provided. */\nexport const DEFAULT_RESPONSE_FORMAT: ResponseFormat = 'legacy';\n\n/** Parses a user-provided response format into a supported value. */\nexport const resolveResponseFormat = (value: unknown): ResponseFormat => {\n if (typeof value === 'string' && value.toLowerCase() === 'normalized') {\n return 'normalized';\n }\n return DEFAULT_RESPONSE_FORMAT;\n};\n\n/** Reads the response format from a Capacitor plugin config object. */\nexport const getResponseFormatFromConfig = (config: unknown): ResponseFormat => {\n if (config && typeof config === 'object' && 'responseFormat' in config) {\n return resolveResponseFormat((config as { responseFormat?: unknown }).responseFormat);\n }\n return DEFAULT_RESPONSE_FORMAT;\n};\n"]}
@@ -0,0 +1,45 @@
1
+ import type { CurrentRecordingStatus, GenericResponse, RecordingData, RecordingOptions } from '../../definitions';
2
+ /** Preferred MIME types to probe in order of fallback. */
3
+ declare const POSSIBLE_MIME_TYPES: {
4
+ 'audio/aac': string;
5
+ 'audio/webm;codecs=opus': string;
6
+ 'audio/mp4': string;
7
+ 'audio/webm': string;
8
+ 'audio/ogg;codecs=opus': string;
9
+ };
10
+ /** Browser implementation backed by MediaRecorder and Capacitor Filesystem. */
11
+ export declare class VoiceRecorderImpl {
12
+ /** Active MediaRecorder instance, if recording. */
13
+ private mediaRecorder;
14
+ /** Collected data chunks from MediaRecorder. */
15
+ private chunks;
16
+ /** Promise resolved when the recorder stops and payload is ready. */
17
+ private pendingResult;
18
+ /** Returns whether the browser can start a recording session. */
19
+ static canDeviceVoiceRecord(): Promise<GenericResponse>;
20
+ /** Starts a recording session using MediaRecorder. */
21
+ startRecording(options?: RecordingOptions): Promise<GenericResponse>;
22
+ /** Stops the current recording and resolves the pending payload. */
23
+ stopRecording(): Promise<RecordingData>;
24
+ /** Returns whether the browser has microphone permission. */
25
+ static hasAudioRecordingPermission(): Promise<GenericResponse>;
26
+ /** Requests microphone permission from the browser. */
27
+ static requestAudioRecordingPermission(): Promise<GenericResponse>;
28
+ /** Pauses the recording session when supported. */
29
+ pauseRecording(): Promise<GenericResponse>;
30
+ /** Resumes a paused recording session when supported. */
31
+ resumeRecording(): Promise<GenericResponse>;
32
+ /** Returns the current recording status from MediaRecorder. */
33
+ getCurrentStatus(): Promise<CurrentRecordingStatus>;
34
+ /** Returns the first supported MIME type, if any. */
35
+ static getSupportedMimeType<T extends keyof typeof POSSIBLE_MIME_TYPES>(): T | null;
36
+ /** Initializes MediaRecorder and wires up handlers. */
37
+ private onSuccessfullyStartedRecording;
38
+ /** Handles failures from getUserMedia. */
39
+ private onFailedToStartRecording;
40
+ /** Converts a Blob payload into a base64 string. */
41
+ private static blobToBase64;
42
+ /** Resets state for the next recording attempt. */
43
+ private prepareInstanceForNextOperation;
44
+ }
45
+ export {};