@siteed/expo-audio-stream 1.11.6 → 1.12.1
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/CHANGELOG.md +8 -7
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +37 -18
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +32 -2
- package/build/ExpoAudioStream.types.d.ts +3 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +17 -4
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +4 -0
- package/build/WebRecorder.web.js.map +1 -1
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +7 -10
- package/build/useAudioRecorder.js.map +1 -1
- package/ios/AudioStreamManager.swift +130 -69
- package/ios/AudioStreamManagerDelegate.swift +1 -0
- package/ios/ExpoAudioStreamModule.swift +21 -3
- package/ios/RecordingSettings.swift +37 -1
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +5 -0
- package/src/ExpoAudioStream.web.ts +23 -5
- package/src/WebRecorder.web.ts +4 -0
- package/src/useAudioRecorder.tsx +7 -12
|
@@ -126,7 +126,6 @@ class AudioStreamManager: NSObject {
|
|
|
126
126
|
Logger.debug("Audio session interruption began")
|
|
127
127
|
pauseRecording()
|
|
128
128
|
|
|
129
|
-
// Notify about the interruption
|
|
130
129
|
delegate?.audioStreamManager(
|
|
131
130
|
self,
|
|
132
131
|
didReceiveInterruption: [
|
|
@@ -139,31 +138,24 @@ class AudioStreamManager: NSObject {
|
|
|
139
138
|
Logger.debug("Audio session interruption ended")
|
|
140
139
|
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
141
140
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
|
|
142
|
+
// Check if we should auto-resume and the recording wasn't manually paused
|
|
143
|
+
if autoResumeAfterInterruption && !wasSuspended {
|
|
144
|
+
// Add a slight delay to ensure the audio session is fully ready
|
|
145
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
146
|
+
guard let self = self else { return }
|
|
147
|
+
self.resumeRecording()
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
// Notify about the interruption
|
|
148
|
-
delegate?.audioStreamManager(
|
|
149
|
-
self,
|
|
150
|
-
didReceiveInterruption: [
|
|
151
|
-
"type": "ended",
|
|
152
|
-
"wasSuspended": wasSuspended,
|
|
153
|
-
"shouldResume": true
|
|
154
|
-
]
|
|
155
|
-
)
|
|
156
|
-
} else {
|
|
157
|
-
// Notify about the interruption without resume option
|
|
158
|
-
delegate?.audioStreamManager(
|
|
159
|
-
self,
|
|
160
|
-
didReceiveInterruption: [
|
|
161
|
-
"type": "ended",
|
|
162
|
-
"wasSuspended": wasSuspended,
|
|
163
|
-
"shouldResume": false
|
|
164
|
-
]
|
|
165
|
-
)
|
|
166
149
|
}
|
|
150
|
+
|
|
151
|
+
delegate?.audioStreamManager(
|
|
152
|
+
self,
|
|
153
|
+
didReceiveInterruption: [
|
|
154
|
+
"type": "ended",
|
|
155
|
+
"wasSuspended": wasSuspended,
|
|
156
|
+
"shouldResume": options.contains(.shouldResume)
|
|
157
|
+
]
|
|
158
|
+
)
|
|
167
159
|
}
|
|
168
160
|
@unknown default:
|
|
169
161
|
break
|
|
@@ -380,11 +372,51 @@ class AudioStreamManager: NSObject {
|
|
|
380
372
|
|
|
381
373
|
/// Creates a new recording file.
|
|
382
374
|
/// - Returns: The URL of the newly created recording file, or nil if creation failed.
|
|
383
|
-
private func createRecordingFile() -> URL? {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
375
|
+
private func createRecordingFile(isCompressed: Bool = false) -> URL? {
|
|
376
|
+
// Add debug logging
|
|
377
|
+
Logger.debug("Creating recording file - settings filename: \(recordingSettings?.filename ?? "nil")")
|
|
378
|
+
|
|
379
|
+
// Get base directory - use default if no custom directory provided
|
|
380
|
+
let baseDirectory: URL
|
|
381
|
+
if let customDir = recordingSettings?.outputDirectory {
|
|
382
|
+
baseDirectory = URL(fileURLWithPath: customDir)
|
|
383
|
+
Logger.debug("Using custom directory: \(customDir)")
|
|
384
|
+
} else {
|
|
385
|
+
// Use existing default behavior
|
|
386
|
+
baseDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
387
|
+
Logger.debug("Using default directory: \(baseDirectory.path)")
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Use custom filename if provided, otherwise generate UUID
|
|
391
|
+
let baseFilename = recordingSettings?.filename ?? UUID().uuidString
|
|
392
|
+
Logger.debug("Using base filename: \(baseFilename)")
|
|
393
|
+
|
|
394
|
+
// Remove any existing extension from the filename
|
|
395
|
+
let filenameWithoutExtension = baseFilename.replacingOccurrences(
|
|
396
|
+
of: "\\.[^\\.]+$",
|
|
397
|
+
with: "",
|
|
398
|
+
options: .regularExpression
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// Choose extension based on whether this is a compressed file
|
|
402
|
+
let fileExtension: String
|
|
403
|
+
if isCompressed {
|
|
404
|
+
fileExtension = recordingSettings?.compressedFormat.lowercased() ?? "aac"
|
|
405
|
+
} else {
|
|
406
|
+
fileExtension = "wav"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let fullFilename = "\(filenameWithoutExtension).\(fileExtension)"
|
|
410
|
+
Logger.debug("Full filename: \(fullFilename)")
|
|
411
|
+
|
|
412
|
+
let fileURL = baseDirectory.appendingPathComponent(fullFilename)
|
|
413
|
+
Logger.debug("Final file URL: \(fileURL.path)")
|
|
414
|
+
|
|
415
|
+
// Check if file already exists
|
|
416
|
+
if fileManager.fileExists(atPath: fileURL.path) {
|
|
417
|
+
Logger.debug("File already exists at: \(fileURL.path)")
|
|
418
|
+
return nil
|
|
419
|
+
}
|
|
388
420
|
|
|
389
421
|
if !fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil) {
|
|
390
422
|
Logger.debug("Failed to create file at: \(fileURL.path)")
|
|
@@ -479,6 +511,20 @@ class AudioStreamManager: NSObject {
|
|
|
479
511
|
/// - intervalMilliseconds: The interval in milliseconds for emitting audio data.
|
|
480
512
|
/// - Returns: A StartRecordingResult object if recording starts successfully, or nil otherwise.
|
|
481
513
|
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
|
|
514
|
+
// Check for active call first
|
|
515
|
+
let callCenter = CXCallObserver()
|
|
516
|
+
if callCenter.calls.contains(where: { $0.hasEnded == false }) {
|
|
517
|
+
Logger.debug("Cannot start recording during an active call")
|
|
518
|
+
delegate?.audioStreamManager(self, didFailWithError: "Cannot start recording during an active call")
|
|
519
|
+
return nil
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Store settings first before doing anything else
|
|
523
|
+
recordingSettings = settings
|
|
524
|
+
|
|
525
|
+
// Add debug logging to verify settings
|
|
526
|
+
Logger.debug("Starting recording with settings - filename: \(settings.filename ?? "nil"), directory: \(settings.outputDirectory ?? "nil")")
|
|
527
|
+
|
|
482
528
|
// Update auto-resume preference from settings
|
|
483
529
|
autoResumeAfterInterruption = settings.autoResumeAfterInterruption
|
|
484
530
|
|
|
@@ -606,37 +652,26 @@ class AudioStreamManager: NSObject {
|
|
|
606
652
|
|
|
607
653
|
Logger.debug("Initializing compressed recording with settings: \(compressedSettings)")
|
|
608
654
|
|
|
609
|
-
|
|
610
|
-
|
|
655
|
+
// Use createRecordingFile for consistency in file handling
|
|
656
|
+
compressedFileURL = createRecordingFile(isCompressed: true)
|
|
611
657
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
compressedFileURL = tempDirectory.appendingPathComponent(recordingUUID.uuidString)
|
|
615
|
-
.appendingPathExtension(settings.compressedFormat)
|
|
658
|
+
if let url = compressedFileURL {
|
|
659
|
+
Logger.debug("Using compressed file URL: \(url.path)")
|
|
616
660
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
Logger.debug("Failed to create empty file at: \(url.path)")
|
|
623
|
-
}
|
|
661
|
+
// Initialize recorder
|
|
662
|
+
compressedRecorder = try AVAudioRecorder(url: url, settings: compressedSettings)
|
|
663
|
+
if let recorder = compressedRecorder {
|
|
664
|
+
let prepared = recorder.prepareToRecord()
|
|
665
|
+
Logger.debug("Recorder prepared: \(prepared)")
|
|
624
666
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
Logger.debug("Recorder current time: \(recorder.currentTime)")
|
|
635
|
-
|
|
636
|
-
compressedFormat = settings.compressedFormat
|
|
637
|
-
compressedBitRate = settings.compressedBitRate
|
|
638
|
-
Logger.debug("Compressed recording initialized - Format: \(compressedFormat), Bitrate: \(compressedBitRate)")
|
|
639
|
-
}
|
|
667
|
+
let started = recorder.record()
|
|
668
|
+
Logger.debug("Recorder started: \(started)")
|
|
669
|
+
|
|
670
|
+
Logger.debug("Recorder current time: \(recorder.currentTime)")
|
|
671
|
+
|
|
672
|
+
compressedFormat = settings.compressedFormat
|
|
673
|
+
compressedBitRate = settings.compressedBitRate
|
|
674
|
+
Logger.debug("Compressed recording initialized - Format: \(compressedFormat), Bitrate: \(compressedBitRate)")
|
|
640
675
|
}
|
|
641
676
|
}
|
|
642
677
|
} catch {
|
|
@@ -700,18 +735,27 @@ class AudioStreamManager: NSObject {
|
|
|
700
735
|
isPaused = false
|
|
701
736
|
Logger.debug("Debug: Recording started successfully.")
|
|
702
737
|
|
|
738
|
+
var compression = compressedRecorder != nil ? CompressedRecordingInfo(
|
|
739
|
+
compressedFileUri: compressedFileURL?.absoluteString ?? "",
|
|
740
|
+
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
741
|
+
bitrate: compressedBitRate,
|
|
742
|
+
format: compressedFormat
|
|
743
|
+
) : nil
|
|
744
|
+
|
|
745
|
+
// Get the size separately since it's not part of the initializer
|
|
746
|
+
if let compressedPath = compressedFileURL?.path,
|
|
747
|
+
let attributes = try? FileManager.default.attributesOfItem(atPath: compressedPath),
|
|
748
|
+
let fileSize = attributes[.size] as? Int64 {
|
|
749
|
+
compression?.size = fileSize
|
|
750
|
+
}
|
|
751
|
+
|
|
703
752
|
return StartRecordingResult(
|
|
704
753
|
fileUri: recordingFileURL!.path,
|
|
705
754
|
mimeType: mimeType,
|
|
706
755
|
channels: settings.numberOfChannels,
|
|
707
756
|
bitDepth: settings.bitDepth,
|
|
708
757
|
sampleRate: settings.sampleRate,
|
|
709
|
-
compression:
|
|
710
|
-
fileUri: compressedFileURL!.absoluteString,
|
|
711
|
-
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
712
|
-
bitrate: compressedBitRate,
|
|
713
|
-
format: compressedFormat
|
|
714
|
-
) : nil
|
|
758
|
+
compression: compression
|
|
715
759
|
)
|
|
716
760
|
|
|
717
761
|
} catch {
|
|
@@ -792,6 +836,14 @@ class AudioStreamManager: NSObject {
|
|
|
792
836
|
|
|
793
837
|
/// Resumes the current audio recording.
|
|
794
838
|
func resumeRecording() {
|
|
839
|
+
// Check for active call first
|
|
840
|
+
let callCenter = CXCallObserver()
|
|
841
|
+
if callCenter.calls.contains(where: { $0.hasEnded == false }) {
|
|
842
|
+
Logger.debug("Cannot resume recording during an active call")
|
|
843
|
+
delegate?.audioStreamManager(self, didFailWithError: "Cannot resume recording during an active call")
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
|
|
795
847
|
guard isRecording && isPaused else { return }
|
|
796
848
|
|
|
797
849
|
lastValidDuration = nil // Clear the stored duration when resuming
|
|
@@ -920,6 +972,20 @@ class AudioStreamManager: NSObject {
|
|
|
920
972
|
// Update the WAV header with the correct file size
|
|
921
973
|
updateWavHeader(fileURL: fileURL, totalDataSize: fileSize - 44)
|
|
922
974
|
|
|
975
|
+
var compression = compressedRecorder != nil ? CompressedRecordingInfo(
|
|
976
|
+
compressedFileUri: compressedFileURL?.absoluteString ?? "",
|
|
977
|
+
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
978
|
+
bitrate: compressedBitRate,
|
|
979
|
+
format: compressedFormat
|
|
980
|
+
) : nil
|
|
981
|
+
|
|
982
|
+
// Get the size separately since it's not part of the initializer
|
|
983
|
+
if let compressedPath = compressedFileURL?.path,
|
|
984
|
+
let attributes = try? FileManager.default.attributesOfItem(atPath: compressedPath),
|
|
985
|
+
let fileSize = attributes[.size] as? Int64 {
|
|
986
|
+
compression?.size = fileSize
|
|
987
|
+
}
|
|
988
|
+
|
|
923
989
|
let result = RecordingResult(
|
|
924
990
|
fileUri: fileURL.absoluteString,
|
|
925
991
|
filename: fileURL.lastPathComponent,
|
|
@@ -929,12 +995,7 @@ class AudioStreamManager: NSObject {
|
|
|
929
995
|
channels: settings.numberOfChannels,
|
|
930
996
|
bitDepth: settings.bitDepth,
|
|
931
997
|
sampleRate: settings.sampleRate,
|
|
932
|
-
compression:
|
|
933
|
-
fileUri: compressedFileURL!.absoluteString,
|
|
934
|
-
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
935
|
-
bitrate: compressedBitRate,
|
|
936
|
-
format: compressedFormat
|
|
937
|
-
) : nil
|
|
998
|
+
compression: compression
|
|
938
999
|
)
|
|
939
1000
|
|
|
940
1001
|
// Cleanup
|
|
@@ -12,4 +12,5 @@ protocol AudioStreamManagerDelegate: AnyObject {
|
|
|
12
12
|
func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date)
|
|
13
13
|
func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool)
|
|
14
14
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any])
|
|
15
|
+
func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String)
|
|
15
16
|
}
|
|
@@ -172,7 +172,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
172
172
|
// Add compression info if available
|
|
173
173
|
if let compression = result.compression {
|
|
174
174
|
resultDict["compression"] = [
|
|
175
|
-
"
|
|
175
|
+
"compressedFileUri": compression.compressedFileUri,
|
|
176
176
|
"mimeType": compression.mimeType,
|
|
177
177
|
"bitrate": compression.bitrate,
|
|
178
178
|
"format": compression.format
|
|
@@ -213,8 +213,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
213
213
|
/// - promise: A promise to resolve with the recording result or reject with an error.
|
|
214
214
|
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
215
215
|
if let recordingResult = self.streamManager.stopRecording() {
|
|
216
|
-
|
|
217
|
-
let resultDict: [String: Any] = [
|
|
216
|
+
var resultDict: [String: Any] = [
|
|
218
217
|
"fileUri": recordingResult.fileUri,
|
|
219
218
|
"filename": recordingResult.filename,
|
|
220
219
|
"durationMs": recordingResult.duration,
|
|
@@ -224,6 +223,18 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
224
223
|
"sampleRate": recordingResult.sampleRate,
|
|
225
224
|
"mimeType": recordingResult.mimeType,
|
|
226
225
|
]
|
|
226
|
+
|
|
227
|
+
// Add compression info if available
|
|
228
|
+
if let compression = recordingResult.compression {
|
|
229
|
+
resultDict["compression"] = [
|
|
230
|
+
"compressedFileUri": compression.compressedFileUri,
|
|
231
|
+
"mimeType": compression.mimeType,
|
|
232
|
+
"bitrate": compression.bitrate,
|
|
233
|
+
"format": compression.format,
|
|
234
|
+
"size": compression.size
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
227
238
|
promise.resolve(resultDict)
|
|
228
239
|
} else {
|
|
229
240
|
promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
|
|
@@ -459,4 +470,11 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
459
470
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
|
|
460
471
|
sendEvent(recordingInterruptedEvent, info)
|
|
461
472
|
}
|
|
473
|
+
|
|
474
|
+
func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
|
|
475
|
+
// Send error event to JavaScript
|
|
476
|
+
sendEvent("error", [
|
|
477
|
+
"message": error
|
|
478
|
+
])
|
|
479
|
+
}
|
|
462
480
|
}
|
|
@@ -18,10 +18,11 @@ struct IOSNotificationConfig {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
struct CompressedRecordingInfo {
|
|
21
|
-
var
|
|
21
|
+
var compressedFileUri: String
|
|
22
22
|
var mimeType: String
|
|
23
23
|
var bitrate: Int
|
|
24
24
|
var format: String
|
|
25
|
+
var size: Int64 = 0 // Add size with default value
|
|
25
26
|
|
|
26
27
|
static func validate(format: String, bitrate: Int) -> Result<(String, Int), Error> {
|
|
27
28
|
// Validate format
|
|
@@ -59,6 +60,7 @@ struct IOSConfig {
|
|
|
59
60
|
enum RecordingError: Error {
|
|
60
61
|
case unsupportedFormat(String)
|
|
61
62
|
case invalidBitrate(Int)
|
|
63
|
+
case invalidOutputDirectory(String)
|
|
62
64
|
|
|
63
65
|
var localizedDescription: String {
|
|
64
66
|
switch self {
|
|
@@ -66,6 +68,8 @@ enum RecordingError: Error {
|
|
|
66
68
|
return "Unsupported compression format: \(format). iOS only supports AAC."
|
|
67
69
|
case .invalidBitrate(let bitrate):
|
|
68
70
|
return "Invalid bitrate: \(bitrate). Must be between 8000 and 960000 bps."
|
|
71
|
+
case .invalidOutputDirectory(let directory):
|
|
72
|
+
return "Invalid output directory: \(directory). Directory does not exist, is not a directory, or is not writable."
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -100,6 +104,10 @@ struct RecordingSettings {
|
|
|
100
104
|
|
|
101
105
|
let autoResumeAfterInterruption: Bool
|
|
102
106
|
|
|
107
|
+
// Make these optional with nil default values
|
|
108
|
+
var outputDirectory: String? = nil
|
|
109
|
+
var filename: String? = nil
|
|
110
|
+
|
|
103
111
|
static func fromDictionary(_ dict: [String: Any]) -> Result<RecordingSettings, Error> {
|
|
104
112
|
// Extract compression settings
|
|
105
113
|
let compression = dict["compression"] as? [String: Any]
|
|
@@ -222,6 +230,34 @@ struct RecordingSettings {
|
|
|
222
230
|
settings.notification = notificationConfig
|
|
223
231
|
}
|
|
224
232
|
|
|
233
|
+
// Parse output settings (they remain nil if not provided)
|
|
234
|
+
if let directory = dict["outputDirectory"] as? String {
|
|
235
|
+
// Only validate if a custom directory is provided
|
|
236
|
+
let fileManager = FileManager.default
|
|
237
|
+
var isDirectory: ObjCBool = false
|
|
238
|
+
|
|
239
|
+
// Clean up the directory path by removing file:// protocol if present
|
|
240
|
+
let cleanDirectory = directory.replacingOccurrences(of: "file://", with: "")
|
|
241
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
242
|
+
.replacingOccurrences(of: "//", with: "/")
|
|
243
|
+
|
|
244
|
+
if !fileManager.fileExists(atPath: cleanDirectory, isDirectory: &isDirectory) {
|
|
245
|
+
return .failure(RecordingError.invalidOutputDirectory("Directory does not exist: \(cleanDirectory)"))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if !isDirectory.boolValue {
|
|
249
|
+
return .failure(RecordingError.invalidOutputDirectory("Path is not a directory: \(cleanDirectory)"))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if !fileManager.isWritableFile(atPath: cleanDirectory) {
|
|
253
|
+
return .failure(RecordingError.invalidOutputDirectory("Directory is not writable: \(cleanDirectory)"))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
settings.outputDirectory = cleanDirectory
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
settings.filename = dict["filename"] as? String
|
|
260
|
+
|
|
225
261
|
return .success(settings)
|
|
226
262
|
}
|
|
227
263
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ export interface CompressionInfo {
|
|
|
11
11
|
mimeType: string
|
|
12
12
|
bitrate: number
|
|
13
13
|
format: string
|
|
14
|
+
compressedFileUri?: string
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface AudioStreamStatus {
|
|
@@ -188,6 +189,10 @@ export interface RecordingConfig {
|
|
|
188
189
|
|
|
189
190
|
// Optional callback to handle recording interruptions
|
|
190
191
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
192
|
+
|
|
193
|
+
// Optional output configuration
|
|
194
|
+
outputDirectory?: string // If not provided, uses default app directory
|
|
195
|
+
filename?: string // If not provided, uses UUID
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
export interface NotificationConfig {
|
|
@@ -50,7 +50,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
50
50
|
lastEmittedTime: number
|
|
51
51
|
lastEmittedCompressionSize: number
|
|
52
52
|
streamUuid: string | null
|
|
53
|
-
extension: 'webm' | 'wav' = 'wav' // Default extension is '
|
|
53
|
+
extension: 'webm' | 'wav' = 'wav' // Default extension is 'wav'
|
|
54
54
|
recordingConfig?: RecordingConfig
|
|
55
55
|
bitDepth: BitDepth // Bit depth of the audio
|
|
56
56
|
audioWorkletUrl: string
|
|
@@ -108,7 +108,9 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Start recording with options
|
|
111
|
-
async startRecording(
|
|
111
|
+
async startRecording(
|
|
112
|
+
recordingConfig: RecordingConfig = {}
|
|
113
|
+
): Promise<StartRecordingResult> {
|
|
112
114
|
if (this.isRecording) {
|
|
113
115
|
throw new Error('Recording is already in progress')
|
|
114
116
|
}
|
|
@@ -170,7 +172,15 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
170
172
|
this.lastEmittedSize = 0
|
|
171
173
|
this.lastEmittedTime = 0
|
|
172
174
|
this.lastEmittedCompressionSize = 0
|
|
173
|
-
|
|
175
|
+
|
|
176
|
+
// Use custom filename if provided, otherwise fallback to timestamp
|
|
177
|
+
if (recordingConfig.filename) {
|
|
178
|
+
// Remove any existing extension from the filename
|
|
179
|
+
this.streamUuid = recordingConfig.filename.replace(/\.[^/.]+$/, '')
|
|
180
|
+
} else {
|
|
181
|
+
this.streamUuid = Date.now().toString()
|
|
182
|
+
}
|
|
183
|
+
|
|
174
184
|
const fileUri = `${this.streamUuid}.${this.extension}`
|
|
175
185
|
const streamConfig: StartRecordingResult = {
|
|
176
186
|
fileUri,
|
|
@@ -266,9 +276,11 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
266
276
|
}
|
|
267
277
|
)
|
|
268
278
|
|
|
269
|
-
|
|
279
|
+
// Use the stored streamUuid (which contains our custom filename) for the final filename
|
|
280
|
+
const filename = `${this.streamUuid}.${this.extension}`
|
|
281
|
+
const result: AudioRecording = {
|
|
270
282
|
fileUri,
|
|
271
|
-
filename
|
|
283
|
+
filename, // This will now use our custom filename
|
|
272
284
|
bitDepth: this.bitDepth,
|
|
273
285
|
channels: this.recordingConfig?.channels ?? 1,
|
|
274
286
|
sampleRate: this.recordingConfig?.sampleRate ?? 44100,
|
|
@@ -277,6 +289,11 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
277
289
|
mimeType,
|
|
278
290
|
compression,
|
|
279
291
|
}
|
|
292
|
+
|
|
293
|
+
// Reset after creating the result
|
|
294
|
+
this.streamUuid = null
|
|
295
|
+
|
|
296
|
+
return result
|
|
280
297
|
} catch (error) {
|
|
281
298
|
this.logger?.error('[Stop] Error stopping recording:', error)
|
|
282
299
|
throw error
|
|
@@ -325,6 +342,7 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
|
|
|
325
342
|
format: this.recordingConfig.compression.format ?? 'opus',
|
|
326
343
|
bitrate:
|
|
327
344
|
this.recordingConfig.compression.bitrate ?? 128000,
|
|
345
|
+
compressedFileUri: `${this.streamUuid}.webm`,
|
|
328
346
|
}
|
|
329
347
|
: undefined,
|
|
330
348
|
}
|
package/src/WebRecorder.web.ts
CHANGED
|
@@ -351,6 +351,10 @@ export class WebRecorder {
|
|
|
351
351
|
return { pcmData: new Float32Array() }
|
|
352
352
|
} finally {
|
|
353
353
|
this.cleanup()
|
|
354
|
+
// Reset the chunks array
|
|
355
|
+
this.compressedChunks = []
|
|
356
|
+
this.compressedSize = 0
|
|
357
|
+
this.pendingCompressedChunk = null
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
|
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -378,18 +378,13 @@ export function useAudioRecorder({
|
|
|
378
378
|
)
|
|
379
379
|
|
|
380
380
|
// Check and update recording state
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
isRecording: status.isRecording,
|
|
389
|
-
isPaused: status.isPaused,
|
|
390
|
-
},
|
|
391
|
-
})
|
|
392
|
-
}
|
|
381
|
+
dispatch({
|
|
382
|
+
type: 'UPDATE_RECORDING_STATE',
|
|
383
|
+
payload: {
|
|
384
|
+
isRecording: status.isRecording,
|
|
385
|
+
isPaused: status.isPaused,
|
|
386
|
+
},
|
|
387
|
+
})
|
|
393
388
|
|
|
394
389
|
// Check and update recording progress
|
|
395
390
|
if (
|