@independo/capacitor-voice-recorder 8.1.0-dev.2 → 8.1.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Independo GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -67,6 +67,7 @@ dependencies {
67
67
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
68
68
  testImplementation "junit:junit:$junitVersion"
69
69
  testImplementation "org.json:json:20240303"
70
+ testImplementation "org.mockito:mockito-core:5.21.0"
70
71
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
71
72
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
72
73
  }
@@ -82,7 +83,7 @@ tasks.register('jacocoTestReport', JacocoReport) {
82
83
  '**/*Test*.*',
83
84
  'android/**/*.*',
84
85
  ]
85
- def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes", excludes: fileFilter)
86
+ def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug", excludes: fileFilter)
86
87
  def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
87
88
  classDirectories.setFrom(files([javaClasses, kotlinClasses]))
88
89
  sourceDirectories.setFrom(files(['src/main/java', 'src/main/kotlin']))
@@ -18,10 +18,109 @@ import java.util.regex.Pattern;
18
18
  /** MediaRecorder wrapper that manages audio focus and interruptions. */
19
19
  public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListener, RecorderAdapter {
20
20
 
21
+ interface MediaRecorderFactory {
22
+ MediaRecorder create();
23
+ }
24
+
25
+ interface AudioManagerProvider {
26
+ AudioManager getAudioManager(Context context);
27
+ }
28
+
29
+ interface DirectoryProvider {
30
+ File getDocumentsDirectory();
31
+
32
+ File getFilesDir(Context context);
33
+
34
+ File getCacheDir(Context context);
35
+
36
+ File getExternalFilesDir(Context context);
37
+
38
+ File getExternalStorageDirectory();
39
+ }
40
+
41
+ interface SdkIntProvider {
42
+ int getSdkInt();
43
+ }
44
+
45
+ interface AudioFocusRequestFactory {
46
+ AudioFocusRequest create(AudioManager.OnAudioFocusChangeListener listener);
47
+ }
48
+
49
+ private static final class DefaultMediaRecorderFactory implements MediaRecorderFactory {
50
+ @Override
51
+ public MediaRecorder create() {
52
+ return new MediaRecorder();
53
+ }
54
+ }
55
+
56
+ private static final class DefaultAudioManagerProvider implements AudioManagerProvider {
57
+ @Override
58
+ public AudioManager getAudioManager(Context context) {
59
+ return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
60
+ }
61
+ }
62
+
63
+ private static final class DefaultDirectoryProvider implements DirectoryProvider {
64
+ @Override
65
+ public File getDocumentsDirectory() {
66
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
67
+ }
68
+
69
+ @Override
70
+ public File getFilesDir(Context context) {
71
+ return context.getFilesDir();
72
+ }
73
+
74
+ @Override
75
+ public File getCacheDir(Context context) {
76
+ return context.getCacheDir();
77
+ }
78
+
79
+ @Override
80
+ public File getExternalFilesDir(Context context) {
81
+ return context.getExternalFilesDir(null);
82
+ }
83
+
84
+ @Override
85
+ public File getExternalStorageDirectory() {
86
+ return Environment.getExternalStorageDirectory();
87
+ }
88
+ }
89
+
90
+ private static final class DefaultSdkIntProvider implements SdkIntProvider {
91
+ @Override
92
+ public int getSdkInt() {
93
+ return Build.VERSION.SDK_INT;
94
+ }
95
+ }
96
+
97
+ private static final class DefaultAudioFocusRequestFactory implements AudioFocusRequestFactory {
98
+ @Override
99
+ public AudioFocusRequest create(AudioManager.OnAudioFocusChangeListener listener) {
100
+ AudioAttributes audioAttributes = new AudioAttributes.Builder()
101
+ .setUsage(AudioAttributes.USAGE_MEDIA)
102
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
103
+ .build();
104
+
105
+ return new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
106
+ .setAudioAttributes(audioAttributes)
107
+ .setOnAudioFocusChangeListener(listener)
108
+ .build();
109
+ }
110
+ }
111
+
21
112
  /** Android context for file paths and system services. */
22
113
  private final Context context;
23
114
  /** Recording options passed from the service layer. */
24
115
  private final RecordOptions options;
116
+ /** Factory for MediaRecorder instances. */
117
+ private final MediaRecorderFactory mediaRecorderFactory;
118
+ /** Directory provider for file output. */
119
+ private final DirectoryProvider directoryProvider;
120
+ /** SDK version provider for API gating. */
121
+ private final SdkIntProvider sdkIntProvider;
122
+ /** Audio focus request factory for O and above. */
123
+ private final AudioFocusRequestFactory audioFocusRequestFactory;
25
124
  /** Active MediaRecorder instance for the session. */
26
125
  private MediaRecorder mediaRecorder;
27
126
  /** Output file for the current recording session. */
@@ -38,9 +137,33 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
38
137
  private Runnable onInterruptionEnded;
39
138
 
40
139
  public CustomMediaRecorder(Context context, RecordOptions options) throws IOException {
140
+ this(
141
+ context,
142
+ options,
143
+ new DefaultMediaRecorderFactory(),
144
+ new DefaultAudioManagerProvider(),
145
+ new DefaultDirectoryProvider(),
146
+ new DefaultSdkIntProvider(),
147
+ new DefaultAudioFocusRequestFactory()
148
+ );
149
+ }
150
+
151
+ CustomMediaRecorder(
152
+ Context context,
153
+ RecordOptions options,
154
+ MediaRecorderFactory mediaRecorderFactory,
155
+ AudioManagerProvider audioManagerProvider,
156
+ DirectoryProvider directoryProvider,
157
+ SdkIntProvider sdkIntProvider,
158
+ AudioFocusRequestFactory audioFocusRequestFactory
159
+ ) throws IOException {
41
160
  this.context = context;
42
161
  this.options = options;
43
- this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
162
+ this.mediaRecorderFactory = mediaRecorderFactory;
163
+ this.directoryProvider = directoryProvider;
164
+ this.sdkIntProvider = sdkIntProvider;
165
+ this.audioFocusRequestFactory = audioFocusRequestFactory;
166
+ this.audioManager = audioManagerProvider.getAudioManager(context);
44
167
  generateMediaRecorder();
45
168
  }
46
169
 
@@ -56,7 +179,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
56
179
 
57
180
  /** Configures the MediaRecorder with audio settings. */
58
181
  private void generateMediaRecorder() throws IOException {
59
- mediaRecorder = new MediaRecorder();
182
+ mediaRecorder = mediaRecorderFactory.create();
60
183
  mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
61
184
  mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
62
185
  mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
@@ -68,7 +191,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
68
191
 
69
192
  /** Picks a directory and allocates the output file for this session. */
70
193
  private void setRecorderOutputFile() throws IOException {
71
- File outputDir = context.getCacheDir();
194
+ File outputDir = directoryProvider.getCacheDir(context);
72
195
 
73
196
  String directory = options.directory();
74
197
  String subDirectory = options.subDirectory();
@@ -99,11 +222,11 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
99
222
  /** Maps directory strings to Android file locations. */
100
223
  private File getDirectory(String directory) {
101
224
  return switch (directory) {
102
- case "DOCUMENTS" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
103
- case "DATA", "LIBRARY" -> context.getFilesDir();
104
- case "CACHE" -> context.getCacheDir();
105
- case "EXTERNAL" -> context.getExternalFilesDir(null);
106
- case "EXTERNAL_STORAGE" -> Environment.getExternalStorageDirectory();
225
+ case "DOCUMENTS" -> directoryProvider.getDocumentsDirectory();
226
+ case "DATA", "LIBRARY" -> directoryProvider.getFilesDir(context);
227
+ case "CACHE" -> directoryProvider.getCacheDir(context);
228
+ case "EXTERNAL" -> directoryProvider.getExternalFilesDir(context);
229
+ case "EXTERNAL_STORAGE" -> directoryProvider.getExternalStorageDirectory();
107
230
  default -> null;
108
231
  };
109
232
  }
@@ -150,7 +273,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
150
273
 
151
274
  /** Pauses recording when supported by the OS version. */
152
275
  public boolean pauseRecording() throws NotSupportedOsVersion {
153
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
276
+ if (sdkIntProvider.getSdkInt() < Build.VERSION_CODES.N) {
154
277
  throw new NotSupportedOsVersion();
155
278
  }
156
279
 
@@ -165,7 +288,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
165
288
 
166
289
  /** Resumes a paused or interrupted recording session. */
167
290
  public boolean resumeRecording() throws NotSupportedOsVersion {
168
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
291
+ if (sdkIntProvider.getSdkInt() < Build.VERSION_CODES.N) {
169
292
  throw new NotSupportedOsVersion();
170
293
  }
171
294
 
@@ -215,17 +338,8 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
215
338
  return;
216
339
  }
217
340
 
218
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
219
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
220
- .setUsage(AudioAttributes.USAGE_MEDIA)
221
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
222
- .build();
223
-
224
- audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
225
- .setAudioAttributes(audioAttributes)
226
- .setOnAudioFocusChangeListener(this)
227
- .build();
228
-
341
+ if (sdkIntProvider.getSdkInt() >= Build.VERSION_CODES.O) {
342
+ audioFocusRequest = audioFocusRequestFactory.create(this);
229
343
  audioManager.requestAudioFocus(audioFocusRequest);
230
344
  } else {
231
345
  audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
@@ -238,7 +352,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
238
352
  return;
239
353
  }
240
354
 
241
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && audioFocusRequest != null) {
355
+ if (sdkIntProvider.getSdkInt() >= Build.VERSION_CODES.O && audioFocusRequest != null) {
242
356
  audioManager.abandonAudioFocusRequest(audioFocusRequest);
243
357
  audioFocusRequest = null;
244
358
  } else {
@@ -256,7 +370,7 @@ public class CustomMediaRecorder implements AudioManager.OnAudioFocusChangeListe
256
370
  // For voice recording, ducking still degrades captured audio, so treat all loss types as interruptions.
257
371
  if (currentRecordingStatus == CurrentRecordingStatus.RECORDING) {
258
372
  try {
259
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
373
+ if (sdkIntProvider.getSdkInt() >= Build.VERSION_CODES.N) {
260
374
  mediaRecorder.pause();
261
375
  currentRecordingStatus = CurrentRecordingStatus.INTERRUPTED;
262
376
  if (onInterruptionBegan != null) {
@@ -17,11 +17,77 @@ import java.io.IOException;
17
17
  /** Default Android platform adapter for recording and file IO. */
18
18
  public class DefaultRecorderPlatform implements RecorderPlatform {
19
19
 
20
+ interface RecorderFactory {
21
+ RecorderAdapter create(Context context, RecordOptions options) throws Exception;
22
+ }
23
+
24
+ interface MediaPlayerFactory {
25
+ MediaPlayer create();
26
+ }
27
+
28
+ interface UriConverter {
29
+ String toUri(File recordedFile);
30
+ }
31
+
32
+ interface Base64Encoder {
33
+ String encode(byte[] data);
34
+ }
35
+
36
+ private static final class DefaultRecorderFactory implements RecorderFactory {
37
+ @Override
38
+ public RecorderAdapter create(Context context, RecordOptions options) throws Exception {
39
+ return new CustomMediaRecorder(context, options);
40
+ }
41
+ }
42
+
43
+ private static final class DefaultMediaPlayerFactory implements MediaPlayerFactory {
44
+ @Override
45
+ public MediaPlayer create() {
46
+ return new MediaPlayer();
47
+ }
48
+ }
49
+
50
+ private static final class DefaultUriConverter implements UriConverter {
51
+ @Override
52
+ public String toUri(File recordedFile) {
53
+ return Uri.fromFile(recordedFile).toString();
54
+ }
55
+ }
56
+
57
+ private static final class DefaultBase64Encoder implements Base64Encoder {
58
+ @Override
59
+ public String encode(byte[] data) {
60
+ return Base64.encodeToString(data, Base64.DEFAULT);
61
+ }
62
+ }
63
+
20
64
  /** Android context used to access system services. */
21
65
  private final Context context;
66
+ /** Recorder factory for platform creation. */
67
+ private final RecorderFactory recorderFactory;
68
+ /** Media player factory for duration lookup. */
69
+ private final MediaPlayerFactory mediaPlayerFactory;
70
+ /** Uri converter for response payloads. */
71
+ private final UriConverter uriConverter;
72
+ /** Base64 encoder for file payloads. */
73
+ private final Base64Encoder base64Encoder;
22
74
 
23
75
  public DefaultRecorderPlatform(Context context) {
76
+ this(context, new DefaultRecorderFactory(), new DefaultMediaPlayerFactory(), new DefaultUriConverter(), new DefaultBase64Encoder());
77
+ }
78
+
79
+ DefaultRecorderPlatform(
80
+ Context context,
81
+ RecorderFactory recorderFactory,
82
+ MediaPlayerFactory mediaPlayerFactory,
83
+ UriConverter uriConverter,
84
+ Base64Encoder base64Encoder
85
+ ) {
24
86
  this.context = context;
87
+ this.recorderFactory = recorderFactory;
88
+ this.mediaPlayerFactory = mediaPlayerFactory;
89
+ this.uriConverter = uriConverter;
90
+ this.base64Encoder = base64Encoder;
25
91
  }
26
92
 
27
93
  /** Returns whether the device can create a MediaRecorder instance. */
@@ -41,7 +107,7 @@ public class DefaultRecorderPlatform implements RecorderPlatform {
41
107
  /** Creates the recorder adapter for the provided options. */
42
108
  @Override
43
109
  public RecorderAdapter createRecorder(RecordOptions options) throws Exception {
44
- return new CustomMediaRecorder(context, options);
110
+ return recorderFactory.create(context, options);
45
111
  }
46
112
 
47
113
  /** Reads the recorded file as base64, returning null on failure. */
@@ -54,7 +120,7 @@ public class DefaultRecorderPlatform implements RecorderPlatform {
54
120
  while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
55
121
  outputStream.write(buffer, 0, bytesRead);
56
122
  }
57
- return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT);
123
+ return base64Encoder.encode(outputStream.toByteArray());
58
124
  } catch (IOException exp) {
59
125
  return null;
60
126
  }
@@ -65,7 +131,7 @@ public class DefaultRecorderPlatform implements RecorderPlatform {
65
131
  public int getDurationMs(File recordedFile) {
66
132
  MediaPlayer mediaPlayer = null;
67
133
  try {
68
- mediaPlayer = new MediaPlayer();
134
+ mediaPlayer = mediaPlayerFactory.create();
69
135
  mediaPlayer.setDataSource(recordedFile.getAbsolutePath());
70
136
  mediaPlayer.prepare();
71
137
  return mediaPlayer.getDuration();
@@ -81,6 +147,6 @@ public class DefaultRecorderPlatform implements RecorderPlatform {
81
147
  /** Returns a file:// URI for the given recording. */
82
148
  @Override
83
149
  public String toUri(File recordedFile) {
84
- return Uri.fromFile(recordedFile).toString();
150
+ return uriConverter.toUri(recordedFile);
85
151
  }
86
152
  }
@@ -2,6 +2,9 @@ import Foundation
2
2
  import AVFoundation
3
3
  import Capacitor
4
4
 
5
+ typealias PermissionRequester = (@escaping (Bool) -> Void) -> Void
6
+ typealias PermissionStatusProvider = () -> AVAudioSession.RecordPermission
7
+
5
8
  /// Capacitor bridge for the VoiceRecorder plugin.
6
9
  @objc(VoiceRecorder)
7
10
  public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
@@ -25,6 +28,14 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
25
28
  private var service: VoiceRecorderService?
26
29
  /// Response format derived from plugin configuration.
27
30
  private var responseFormat: ResponseFormat = .legacy
31
+ /// Permission requester used by the bridge.
32
+ private var permissionRequester: PermissionRequester = { completion in
33
+ AVAudioSession.sharedInstance().requestRecordPermission(completion)
34
+ }
35
+ /// Permission status provider used by the bridge.
36
+ private var permissionStatusProvider: PermissionStatusProvider = {
37
+ AVAudioSession.sharedInstance().recordPermission
38
+ }
28
39
 
29
40
  /// Initializes dependencies after the plugin loads.
30
41
  public override func load() {
@@ -38,6 +49,24 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
38
49
  )
39
50
  }
40
51
 
52
+ func configureForTesting(
53
+ service: VoiceRecorderService?,
54
+ responseFormat: ResponseFormat? = nil,
55
+ permissionRequester: PermissionRequester? = nil,
56
+ permissionStatusProvider: PermissionStatusProvider? = nil
57
+ ) {
58
+ self.service = service
59
+ if let responseFormat = responseFormat {
60
+ self.responseFormat = responseFormat
61
+ }
62
+ if let permissionRequester = permissionRequester {
63
+ self.permissionRequester = permissionRequester
64
+ }
65
+ if let permissionStatusProvider = permissionStatusProvider {
66
+ self.permissionStatusProvider = permissionStatusProvider
67
+ }
68
+ }
69
+
41
70
  /// Returns whether the device can record audio.
42
71
  @objc func canDeviceVoiceRecord(_ call: CAPPluginCall) {
43
72
  let canRecord = service?.canDeviceVoiceRecord() ?? false
@@ -46,7 +75,7 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
46
75
 
47
76
  /// Requests microphone permission from the user.
48
77
  @objc func requestAudioRecordingPermission(_ call: CAPPluginCall) {
49
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
78
+ permissionRequester { granted in
50
79
  if granted {
51
80
  call.resolve(ResponseGenerator.successResponse())
52
81
  } else {
@@ -159,7 +188,7 @@ public class VoiceRecorder: CAPPlugin, CAPBridgedPlugin {
159
188
 
160
189
  /// Returns whether AVAudioSession reports granted permission.
161
190
  func doesUserGaveAudioRecordingPermission() -> Bool {
162
- return AVAudioSession.sharedInstance().recordPermission == AVAudioSession.RecordPermission.granted
191
+ return permissionStatusProvider() == AVAudioSession.RecordPermission.granted
163
192
  }
164
193
 
165
194
  /// Maps canonical error codes back to legacy messages.
@@ -1,15 +1,36 @@
1
1
  import Foundation
2
2
  import AVFoundation
3
3
 
4
+ protocol AudioSessionProtocol: AnyObject {
5
+ var category: AVAudioSession.Category { get }
6
+ func setCategory(_ category: AVAudioSession.Category) throws
7
+ func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
8
+ }
9
+
10
+ protocol AudioRecorderProtocol: AnyObject {
11
+ @discardableResult
12
+ func record() -> Bool
13
+ func stop()
14
+ func pause()
15
+ }
16
+
17
+ typealias AudioRecorderFactory = (_ url: URL, _ settings: [String: Any]) throws -> AudioRecorderProtocol
18
+
19
+ extension AVAudioSession: AudioSessionProtocol {}
20
+ extension AVAudioRecorder: AudioRecorderProtocol {}
21
+
4
22
  /// AVAudioRecorder wrapper that supports interruptions and segment merging.
5
23
  class CustomMediaRecorder: RecorderAdapter {
6
24
 
25
+ private let audioSessionProvider: () -> AudioSessionProtocol
26
+ private let audioRecorderFactory: AudioRecorderFactory
27
+
7
28
  /// Options provided by the service layer.
8
29
  public var options: RecordOptions?
9
30
  /// Active audio session for recording.
10
- private var recordingSession: AVAudioSession!
31
+ private var recordingSession: AudioSessionProtocol!
11
32
  /// Active recorder instance for the current segment.
12
- private var audioRecorder: AVAudioRecorder!
33
+ private var audioRecorder: AudioRecorderProtocol!
13
34
  /// Base file path for the merged recording.
14
35
  private var baseAudioFilePath: URL!
15
36
  /// List of segment files created during interruptions.
@@ -26,13 +47,23 @@ class CustomMediaRecorder: RecorderAdapter {
26
47
  var onInterruptionEnded: (() -> Void)?
27
48
 
28
49
  /// Recorder settings used for all segments.
29
- private let settings = [
50
+ private let settings: [String: Any] = [
30
51
  AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
31
52
  AVSampleRateKey: 44100,
32
53
  AVNumberOfChannelsKey: 1,
33
54
  AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
34
55
  ]
35
56
 
57
+ init(
58
+ audioSessionProvider: @escaping () -> AudioSessionProtocol = { AVAudioSession.sharedInstance() },
59
+ audioRecorderFactory: @escaping AudioRecorderFactory = { url, settings in
60
+ return try AVAudioRecorder(url: url, settings: settings)
61
+ }
62
+ ) {
63
+ self.audioSessionProvider = audioSessionProvider
64
+ self.audioRecorderFactory = audioRecorderFactory
65
+ }
66
+
36
67
  /// Resolves the directory where audio files should be saved.
37
68
  private func getDirectoryToSaveAudioFile() -> URL {
38
69
  if options?.directory != nil,
@@ -60,13 +91,13 @@ class CustomMediaRecorder: RecorderAdapter {
60
91
  public func startRecording(recordOptions: RecordOptions?) -> Bool {
61
92
  do {
62
93
  options = recordOptions
63
- recordingSession = AVAudioSession.sharedInstance()
94
+ recordingSession = audioSessionProvider()
64
95
  originalRecordingSessionCategory = recordingSession.category
65
96
  try recordingSession.setCategory(AVAudioSession.Category.playAndRecord)
66
- try recordingSession.setActive(true)
97
+ try recordingSession.setActive(true, options: [])
67
98
  baseAudioFilePath = getDirectoryToSaveAudioFile().appendingPathComponent("recording-\(Int(Date().timeIntervalSince1970 * 1000)).aac")
68
99
  audioFileSegments = [baseAudioFilePath]
69
- audioRecorder = try AVAudioRecorder(url: baseAudioFilePath, settings: settings)
100
+ audioRecorder = try audioRecorderFactory(baseAudioFilePath, settings)
70
101
  setupInterruptionHandling()
71
102
  audioRecorder.record()
72
103
  status = CurrentRecordingStatus.RECORDING
@@ -88,7 +119,7 @@ class CustomMediaRecorder: RecorderAdapter {
88
119
  }
89
120
 
90
121
  do {
91
- try self.recordingSession.setActive(false)
122
+ try self.recordingSession.setActive(false, options: [])
92
123
  try self.recordingSession.setCategory(self.originalRecordingSessionCategory)
93
124
  } catch {
94
125
  }
@@ -151,13 +182,13 @@ class CustomMediaRecorder: RecorderAdapter {
151
182
  if(status == CurrentRecordingStatus.PAUSED || status == CurrentRecordingStatus.INTERRUPTED) {
152
183
  let wasInterrupted = status == CurrentRecordingStatus.INTERRUPTED
153
184
  do {
154
- try recordingSession.setActive(true)
185
+ try recordingSession.setActive(true, options: [])
155
186
  if status == CurrentRecordingStatus.INTERRUPTED {
156
187
  let directory = getDirectoryToSaveAudioFile()
157
188
  let timestamp = Int(Date().timeIntervalSince1970 * 1000)
158
189
  let segmentNumber = audioFileSegments.count
159
190
  let segmentPath = directory.appendingPathComponent("recording-\(timestamp)-segment-\(segmentNumber).aac")
160
- audioRecorder = try AVAudioRecorder(url: segmentPath, settings: settings)
191
+ audioRecorder = try audioRecorderFactory(segmentPath, settings)
161
192
  audioFileSegments.append(segmentPath)
162
193
  }
163
194
  audioRecorder.record()
@@ -165,7 +196,7 @@ class CustomMediaRecorder: RecorderAdapter {
165
196
  return true
166
197
  } catch {
167
198
  if wasInterrupted {
168
- try? recordingSession.setActive(false)
199
+ try? recordingSession.setActive(false, options: [])
169
200
  }
170
201
  return false
171
202
  }
@@ -183,7 +214,7 @@ class CustomMediaRecorder: RecorderAdapter {
183
214
  private func setupInterruptionHandling() {
184
215
  interruptionObserver = NotificationCenter.default.addObserver(
185
216
  forName: AVAudioSession.interruptionNotification,
186
- object: AVAudioSession.sharedInstance(),
217
+ object: recordingSession,
187
218
  queue: .main
188
219
  ) { [weak self] notification in
189
220
  self?.handleInterruption(notification: notification)
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.1.0-dev.2",
16
+ "version": "8.1.0-dev.4",
17
17
  "devDependencies": {
18
18
  "@capacitor/android": "^8.0.0",
19
19
  "conventional-changelog-conventionalcommits": "^9.1.0",