@siteed/expo-audio-studio 2.6.3 → 2.8.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/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.8.0] - 2025-05-04
12
+ ### Changed
13
+ - feat(playground): Version 1.0.1 with Audio Enhancements, App Updates, and Navigation Refactor (#229) ([868fca0](https://github.com/deeeed/expo-audio-stream/commit/868fca026119aea116a22670c2b6fe364b6df06c))
14
+ - chore: enhance publish script to include git push after documentation updates ([1b0b0db](https://github.com/deeeed/expo-audio-stream/commit/1b0b0db6cf40a6397e6d7438cb7543c93e67b143))
15
+ - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.7.0 ([fe19a2f](https://github.com/deeeed/expo-audio-stream/commit/fe19a2fa1af6033cfa025691f25a0e9bcd64b37c))
16
+ ## [2.7.0] - 2025-05-04
17
+ ### Changed
18
+ - fix: Enhance iOS Background Audio Recording and Audio Format Conversion (#228) ([c17169b](https://github.com/deeeed/expo-audio-stream/commit/c17169bf9275706abf287712acc30df2f1814ed7))
19
+ - chore(expo-audio-studio): improve build script for cjs esm conversion ([767dfbe](https://github.com/deeeed/expo-audio-stream/commit/767dfbe5da0f1550b689f6859e2e5fccf7f8141c))
11
20
  ## [2.6.3] - 2025-05-03
12
21
  ### Changed
13
22
  - chore: update readme with store download information (#224) ([c404d86](https://github.com/deeeed/expo-audio-stream/commit/c404d860cdb1c4c4bbc3767214f56bf547acec33))
@@ -213,7 +222,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
213
222
  - Feature: Audio features extraction during recording.
214
223
  - Feature: Consistent WAV PCM recording format across all platforms.
215
224
 
216
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.3...HEAD
225
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.8.0...HEAD
226
+ [2.8.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.7.0...@siteed/expo-audio-studio@2.8.0
227
+ [2.7.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.3...@siteed/expo-audio-studio@2.7.0
217
228
  [2.6.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.2...@siteed/expo-audio-studio@2.6.3
218
229
  [2.6.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.1...@siteed/expo-audio-studio@2.6.2
219
230
  [2.6.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.0...@siteed/expo-audio-studio@2.6.1
package/app.plugin.cjs ADDED
@@ -0,0 +1,2 @@
1
+ // Simply export the plugin - this is a CommonJS file (.cjs)
2
+ module.exports = require('./plugin/build/index.cjs')
@@ -37,43 +37,66 @@ class AudioNotificationManager {
37
37
  }
38
38
 
39
39
  func showInitialNotification() {
40
- updateNotification()
40
+ // Wrap notification generation in a main thread dispatch
41
+ DispatchQueue.main.async { [weak self] in
42
+ guard let self = self else { return }
43
+
44
+ // No need for try-catch as this method doesn't throw
45
+ self.updateNotification()
46
+ }
41
47
  }
42
48
 
43
49
  func startUpdates(startTime: Date) {
44
- DispatchQueue.main.async {
45
- self.updateTimer?.invalidate()
50
+ // Cancel any existing timer first
51
+ stopUpdates()
52
+
53
+ // Create a new timer on the main thread
54
+ DispatchQueue.main.async { [weak self] in
55
+ guard let self = self else { return }
46
56
 
47
57
  self.updateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
48
58
  guard let self = self else { return }
49
-
50
- let now = Date()
51
- if now.timeIntervalSince(self.lastUpdateTime) >= self.minUpdateInterval {
52
- self.updateNotification()
53
- self.lastUpdateTime = now
54
- }
59
+ self.currentDuration = Date().timeIntervalSince(startTime)
60
+ self.updateState(isPaused: false)
55
61
  }
56
- RunLoop.main.add(self.updateTimer!, forMode: .common)
57
62
 
58
- self.showInitialNotification()
63
+ // Run the timer even when scrolling
64
+ self.updateTimer?.tolerance = 0.1
65
+ RunLoop.current.add(self.updateTimer!, forMode: .common)
66
+
67
+ // Update notification immediately
68
+ self.updateState(isPaused: false)
59
69
  }
60
70
  }
61
71
 
62
72
  func stopUpdates() {
63
- DispatchQueue.main.async {
73
+ // Always execute timer invalidation on main thread
74
+ DispatchQueue.main.async { [weak self] in
75
+ guard let self = self else { return }
76
+
64
77
  self.updateTimer?.invalidate()
65
78
  self.updateTimer = nil
66
79
 
67
- self.notificationCenter.removeDeliveredNotifications(withIdentifiers: [self.notificationId])
68
- self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [self.notificationId])
80
+ // Clean up notification
81
+ do {
82
+ self.notificationCenter.removeDeliveredNotifications(withIdentifiers: [self.notificationId])
83
+ self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [self.notificationId])
84
+ } catch {
85
+ Logger.debug("AudioNotificationManager", "Error removing notifications: \(error)")
86
+ }
69
87
  }
70
88
  }
71
89
 
72
90
  func updateState(isPaused: Bool) {
73
- let now = Date()
74
- if now.timeIntervalSince(lastUpdateTime) >= minUpdateInterval {
75
- updateNotification(forcePauseState: isPaused)
76
- lastUpdateTime = now
91
+ DispatchQueue.main.async { [weak self] in
92
+ guard let self = self else { return }
93
+
94
+ let now = Date()
95
+ if now.timeIntervalSince(self.lastUpdateTime) >= self.minUpdateInterval {
96
+ // No need for try-catch as this method doesn't throw
97
+ self.updateNotification(forcePauseState: isPaused)
98
+ self.lastUpdateTime = now
99
+ }
77
100
  }
78
101
  }
79
102
 
@@ -92,7 +115,7 @@ class AudioNotificationManager {
92
115
  guard let self = self else { return }
93
116
 
94
117
  // If we have a notification and it was recently updated, skip
95
- if let existing = notifications.first(where: { $0.request.identifier == self.notificationId }),
118
+ if let _ = notifications.first(where: { $0.request.identifier == self.notificationId }),
96
119
  Date().timeIntervalSince(self.lastUpdateTime) < self.minUpdateInterval {
97
120
  return
98
121
  }
@@ -337,7 +337,6 @@ func computeMelSpectrogram(from segment: [Float], sampleRate: Float) -> [Float]
337
337
  }
338
338
 
339
339
  func computeSpectralContrast(from segment: [Float], sampleRate: Float) -> [Float] {
340
- let nBands = 7
341
340
  let fftData = sharedFFT.processSegment(segment)
342
341
 
343
342
  let magnitudeSpectrum = computeMagnitudeSpectrum(from: fftData)
@@ -425,7 +424,7 @@ func loadAudioFile(_ fileUri: String) throws -> AudioData {
425
424
  let frameCount = UInt32(file.length)
426
425
  let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount)!
427
426
 
428
- try file.read(into: buffer)
427
+ try file.read(into: buffer, frameCount: frameCount)
429
428
 
430
429
  // Convert buffer to float array
431
430
  let samples: [Float]
@@ -490,8 +489,8 @@ func computeFeatures(segmentData: [Float], sampleRate: Float, sumSquares: Float,
490
489
  let zcr = featureOptions["zcr"] == true ? Float(zeroCrossings) / Float(segmentLength) : 0
491
490
 
492
491
  // Compute min and max amplitudes
493
- let minAmplitude = segmentData.min() ?? 0
494
- let maxAmplitude = segmentData.max() ?? 0
492
+ let _ = segmentData.min() ?? 0
493
+ let _ = segmentData.max() ?? 0
495
494
 
496
495
  // Call feature extraction functions
497
496
  let mfcc = featureOptions["mfcc"] == true ? extractMFCC(from: segmentData, sampleRate: sampleRate) : []
@@ -598,12 +597,13 @@ func extractRawAudioData(
598
597
  finalBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: frameCount)!
599
598
 
600
599
  var error: NSError?
601
- let status = converter.convert(to: finalBuffer, error: &error) { inNumPackets, outStatus in
600
+ _ = converter.convert(to: finalBuffer, error: &error) { inNumPackets, outStatus in
602
601
  outStatus.pointee = .haveData
603
602
  return buffer
604
603
  }
605
604
 
606
605
  if let error = error {
606
+ Logger.debug("AudioProcessingHelpers", "Format conversion failed: \(error.localizedDescription)")
607
607
  throw error
608
608
  }
609
609
  } else {
@@ -142,7 +142,7 @@ public class AudioProcessor {
142
142
 
143
143
  let totalFrameCount = AVAudioFrameCount(audioFile.length)
144
144
  var framesPerBuffer: AVAudioFrameCount
145
- let actualPointsPerSecond: Int
145
+ let _: Int // Changed from actualPointsPerSecond
146
146
 
147
147
  NSLog("""
148
148
  [AudioProcessor] Starting audio processing:
@@ -215,7 +215,7 @@ public class AudioProcessor {
215
215
  }
216
216
 
217
217
  channelCount = Int(audioFile.processingFormat.channelCount)
218
- var data = Array(repeating: [Float](repeating: 0, count: Int(framesPerBuffer)), count: channelCount)
218
+ let _ = Array(repeating: [Float](repeating: 0, count: Int(framesPerBuffer)), count: channelCount) // Changed from var data
219
219
 
220
220
  var channelData = [Float]()
221
221
  while startFrame < endFrame {
@@ -517,7 +517,7 @@ public class AudioProcessor {
517
517
 
518
518
  let startTime = CACurrentMediaTime()
519
519
  let sampleRate = Float(audioFile.fileFormat.sampleRate)
520
- let totalFrameCount = AVAudioFrameCount(audioFile.length)
520
+ let _ = AVAudioFrameCount(audioFile.length) // Changed from totalFrameCount
521
521
  let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
522
522
  let numberOfChannels = Int(audioFile.fileFormat.channelCount)
523
523
 
@@ -595,7 +595,7 @@ public class AudioProcessor {
595
595
  dB: Float(20 * log10(Double(rms))), // Use RMS for dB calculation
596
596
  silent: rms < SILENCE_THRESHOLD_RMS, // Use RMS for silence detection
597
597
  features: computeFeatures(
598
- segmentData: Array(UnsafeBufferPointer(start: summedData, count: Int(framesToRead))),
598
+ segmentData: Array(summedData[0..<Int(framesToRead)]), // Fixed dangling pointer
599
599
  sampleRate: sampleRate,
600
600
  sumSquares: rms * rms,
601
601
  zeroCrossings: 0,
@@ -692,7 +692,7 @@ public class AudioProcessor {
692
692
 
693
693
  // Output format setup
694
694
  let requestedFormat = outputFormat?["format"] as? String ?? "wav"
695
- let validFormats = ["wav", "aac", "opus"]
695
+ let validFormats = ["wav", "aac"]
696
696
  let formatStr = validFormats.contains(requestedFormat.lowercased()) ? requestedFormat.lowercased() : "aac"
697
697
 
698
698
  if formatStr != requestedFormat.lowercased() {
@@ -704,7 +704,7 @@ public class AudioProcessor {
704
704
  let targetBitDepth = outputFormat?["bitDepth"] as? Int ?? 16
705
705
  let bitrate = outputFormat?["bitrate"] as? Int ?? 128000
706
706
 
707
- let fileExtension = formatStr == "wav" ? "wav" : (formatStr == "aac" ? "aac" : "opus")
707
+ let fileExtension = formatStr == "wav" ? "wav" : "aac"
708
708
  let outputURL = FileManager.default.temporaryDirectory
709
709
  .appendingPathComponent(outputFileName ?? UUID().uuidString)
710
710
  .appendingPathExtension(fileExtension)
@@ -753,7 +753,7 @@ public class AudioProcessor {
753
753
  Logger.debug("AudioProcessor", "Trim operation completed")
754
754
  Logger.debug("AudioProcessor", "- Output file: \(outputURL.path)")
755
755
  Logger.debug("AudioProcessor", "- File exists: \(FileManager.default.fileExists(atPath: outputURL.path))")
756
- Logger.debug("AudioProcessor", "- File size: \(try? FileManager.default.attributesOfItem(atPath: outputURL.path)[.size] as? Int64 ?? 0) bytes")
756
+ Logger.debug("AudioProcessor", "- File size: \((try? FileManager.default.attributesOfItem(atPath: outputURL.path)[.size] as? Int64) ?? 0) bytes") // Fixed optional unwrapping
757
757
  Logger.debug("AudioProcessor", "- File extension: \(outputURL.pathExtension)")
758
758
 
759
759
  return createTrimResult(from: outputURL, keepRanges: keepRanges, formatStr: formatStr, sampleRate: Int(inputSampleRate), channels: inputChannels, bitDepth: 16, bitrate: bitrate)
@@ -801,7 +801,16 @@ public class AudioProcessor {
801
801
  try audioFile.read(into: buffer, frameCount: frameCount)
802
802
  let converter = AVAudioConverter(from: inputFormat, to: targetFormat)!
803
803
  let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: frameCount)!
804
- try converter.convert(to: convertedBuffer, from: buffer)
804
+ var error: NSError?
805
+ _ = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
806
+ outStatus.pointee = .haveData
807
+ return buffer
808
+ }
809
+ if let error = error {
810
+ Logger.debug("AudioProcessor", "Format conversion failed: \(error.localizedDescription)")
811
+ Logger.debug("AudioProcessor", "Skipping this buffer")
812
+ continue
813
+ }
805
814
  try outputFile.write(from: convertedBuffer)
806
815
  cumulativeFrames += Int64(frameCount)
807
816
  let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
@@ -809,227 +818,53 @@ public class AudioProcessor {
809
818
  }
810
819
  return createTrimResult(from: outputURL, keepRanges: keepRanges, formatStr: formatStr, sampleRate: Int(targetSampleRate), channels: targetChannels, bitDepth: targetBitDepth, bitrate: bitrate)
811
820
  } else {
812
- // AAC or Opus output
813
- let outputSettings: [String: Any]
814
- let fileType: AVFileType
815
-
816
- if formatStr == "aac" {
817
- // AAC settings
818
- let outputExtension = "m4a"
819
- let tempOutputURL = FileManager.default.temporaryDirectory
820
- .appendingPathComponent(outputFileName ?? UUID().uuidString)
821
- .appendingPathExtension(outputExtension)
822
-
823
- // Validate and adjust sample rate for AAC
824
- // AAC typically supports: 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz
825
- let supportedSampleRates = [8000.0, 11025.0, 12000.0, 16000.0, 22050.0, 24000.0, 32000.0, 44100.0, 48000.0]
826
-
827
- // Default to 44100 if not specified
828
- var sampleRate = outputFormat?["sampleRate"] as? Double ?? 44100.0
829
-
830
- // Find closest supported sample rate
831
- if !supportedSampleRates.contains(sampleRate) {
832
- let closestRate = supportedSampleRates.min(by: { abs($0 - sampleRate) < abs($1 - sampleRate) }) ?? 44100.0
833
- Logger.debug("AudioProcessor", "Unsupported sample rate \(sampleRate)Hz for AAC, using closest supported rate: \(closestRate)Hz")
834
- sampleRate = closestRate
835
- }
836
-
837
- // Validate channels (AAC typically supports 1 or 2 channels)
838
- var channels = outputFormat?["channels"] as? Int ?? 2
839
- if channels > 2 {
840
- Logger.debug("AudioProcessor", "AAC encoding doesn't support \(channels) channels, limiting to 2 channels")
841
- channels = 2
842
- } else if channels < 1 {
843
- channels = 1
844
- }
845
-
846
- // Validate bitrate (AAC typically supports 8000-320000 bps)
847
- var bitrate = outputFormat?["bitrate"] as? Int ?? 128000
848
- if bitrate < 8000 {
849
- Logger.debug("AudioProcessor", "AAC bitrate too low, setting to minimum 8000 bps")
850
- bitrate = 8000
851
- } else if bitrate > 320000 {
852
- Logger.debug("AudioProcessor", "AAC bitrate too high, setting to maximum 320000 bps")
853
- bitrate = 320000
854
- }
855
-
856
- // Set up proper audio settings for AAC
857
- outputSettings = [
858
- AVFormatIDKey: kAudioFormatMPEG4AAC,
859
- AVSampleRateKey: sampleRate,
860
- AVNumberOfChannelsKey: channels,
861
- AVEncoderBitRateKey: bitrate,
862
- AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
863
- ]
864
- fileType = .m4a
865
-
866
- Logger.debug("AudioProcessor", """
867
- Configuring AAC output:
868
- - Container: m4a
869
- - Format: AAC
870
- - Sample rate: \(sampleRate)Hz
871
- - Channels: \(channels)
872
- - Bitrate: \(bitrate) bps
873
- - Output path: \(tempOutputURL.path)
874
- - File type: \(fileType)
875
- """)
876
- } else {
877
- // Opus settings - use CAF container which can hold Opus
878
- outputSettings = [
879
- AVFormatIDKey: kAudioFormatOpus,
880
- AVSampleRateKey: targetSampleRate,
881
- AVNumberOfChannelsKey: targetChannels,
882
- AVEncoderBitRateKey: bitrate
883
- ]
884
- fileType = .caf // Core Audio Format can contain Opus
885
- }
821
+ // Use AAC instead of Opus (Opus support removed)
822
+ Logger.debug("AudioProcessor", "Using AAC format instead of requested \(formatStr)")
886
823
 
887
- // Use proper file extension for the container format
888
- let tempFileExtension = formatStr == "aac" ? "m4a" : "caf"
889
- let tempOutputURL = FileManager.default.temporaryDirectory
890
- .appendingPathComponent(outputFileName ?? UUID().uuidString)
891
- .appendingPathExtension(tempFileExtension)
892
-
893
- // Create the asset writer with the appropriate file type
894
- let assetWriter = try AVAssetWriter(
895
- outputURL: tempOutputURL,
896
- fileType: fileType
897
- )
824
+ // Keep the existing AAC settings structure for consistency
825
+ let outputSettings: [String: Any] = [
826
+ AVFormatIDKey: kAudioFormatMPEG4AAC,
827
+ AVSampleRateKey: targetSampleRate,
828
+ AVNumberOfChannelsKey: targetChannels,
829
+ AVEncoderBitRateKey: bitrate,
830
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
831
+ ]
832
+ let _ = AVFileType.m4a // Changed from fileType
898
833
 
899
- // Configure the writer input with better settings
900
- let writerInput = AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings)
901
- writerInput.expectsMediaDataInRealTime = false
902
- assetWriter.add(writerInput)
834
+ // 4. Update container extension logic for when Opus was selected
835
+ let _ = "m4a" // Changed from tempFileExtension
903
836
 
904
- // Start the writing session
905
- assetWriter.startWriting()
906
- assetWriter.startSession(atSourceTime: CMTime.zero)
837
+ // 5. Update the MIME type logic for AAC only
838
+ let _ = "audio/mp4" // Changed from mimeType
907
839
 
908
- // Improved buffer handling
909
- let bufferSize = 32768 // Use a larger buffer for better performance
910
- let pcmBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(bufferSize))!
911
-
840
+ let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings)
841
+ var totalFrames: Int64 = 0
912
842
  for range in keepRanges {
843
+ // Break down complex expressions
913
844
  let startTimeInSeconds = range[0] / 1000
914
845
  let startFrame = AVAudioFramePosition(startTimeInSeconds * inputSampleRate)
915
846
 
916
847
  let endTimeInSeconds = range[1] / 1000
917
848
  let endFramePosition = endTimeInSeconds * inputSampleRate
918
- let totalFramesToProcess = AVAudioFrameCount(endFramePosition - Double(startFrame))
849
+ let frameCount = AVAudioFrameCount(endFramePosition - Double(startFrame))
919
850
 
920
- // Process in chunks for better memory management
921
- var framesProcessed: AVAudioFrameCount = 0
851
+ let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: frameCount)!
922
852
  audioFile.framePosition = startFrame
923
-
924
- while framesProcessed < totalFramesToProcess {
925
- let framesToRead = min(AVAudioFrameCount(bufferSize), totalFramesToProcess - framesProcessed)
926
- let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: framesToRead)!
927
-
928
- do {
929
- try audioFile.read(into: buffer, frameCount: framesToRead)
930
-
931
- // Convert the buffer to the target format
932
- let converter = AVAudioConverter(from: inputFormat, to: targetFormat)!
933
- let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: framesToRead)!
934
-
935
- var error: NSError?
936
- let conversionStatus = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
937
- outStatus.pointee = .haveData
938
- return buffer
939
- }
940
-
941
- if let error = error {
942
- Logger.debug("AudioProcessor", "Conversion error: \(error)")
943
- continue
944
- }
945
-
946
- // Create a sample buffer and append to writer
947
- if let sampleBuffer = createSampleBuffer(from: convertedBuffer) {
948
- // Wait until the writer is ready
949
- while !writerInput.isReadyForMoreMediaData {
950
- Thread.sleep(forTimeInterval: 0.01)
951
- }
952
-
953
- if !writerInput.append(sampleBuffer) {
954
- Logger.debug("AudioProcessor", "Failed to append sample buffer: \(assetWriter.error?.localizedDescription ?? "Unknown error")")
955
- }
956
- }
957
-
958
- framesProcessed += framesToRead
959
- cumulativeFrames += Int64(framesToRead)
960
- let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
961
- progressCallback?(progress, 0, totalFrames * Int64(inputFormat.streamDescription.pointee.mBytesPerFrame))
962
-
963
- if framesProcessed % 10000 == 0 { // Log every 10000 frames to avoid excessive logging
964
- Logger.debug("AudioProcessor", "Processed \(framesProcessed)/\(totalFramesToProcess) frames")
965
- }
966
-
967
- } catch {
968
- Logger.debug("AudioProcessor", "Error reading audio: \(error)")
969
- break
970
- }
971
- }
972
- }
973
-
974
- // Finish writing properly
975
- writerInput.markAsFinished()
976
- let finishSemaphore = DispatchSemaphore(value: 0)
977
- assetWriter.finishWriting {
978
- if let error = assetWriter.error {
979
- Logger.debug("AudioProcessor", "Error finishing writing: \(error)")
980
- } else {
981
- Logger.debug("AudioProcessor", "Writing finished successfully")
982
-
983
- // Verify the output file
984
- let fileExists = FileManager.default.fileExists(atPath: tempOutputURL.path)
985
- let fileSize = (try? FileManager.default.attributesOfItem(atPath: tempOutputURL.path)[.size] as? Int64) ?? 0
986
-
987
- Logger.debug("AudioProcessor", """
988
- Output file verification:
989
- - Path: \(tempOutputURL.path)
990
- - Exists: \(fileExists)
991
- - Size: \(fileSize) bytes
992
- - Extension: \(tempOutputURL.pathExtension)
993
- """)
994
- }
995
- finishSemaphore.signal()
996
- }
997
- finishSemaphore.wait()
998
-
999
- // Verify the file was created successfully
1000
- guard FileManager.default.fileExists(atPath: tempOutputURL.path) else {
1001
- reject("FILE_CREATION_FAILED", "Failed to create output file")
1002
- return nil
1003
- }
1004
-
1005
- // Create compression info
1006
- var compressionInfo: [String: Any] = [
1007
- "format": formatStr,
1008
- "bitrate": bitrate,
1009
- "size": (try? FileManager.default.attributesOfItem(atPath: tempOutputURL.path)[.size] as? Int64) ?? 0
1010
- ]
1011
-
1012
- // Add fallback information if applicable
1013
- if formatStr != requestedFormat.lowercased() {
1014
- compressionInfo["requestedFormat"] = requestedFormat
1015
- compressionInfo["fallbackReason"] = "Unsupported format"
853
+ try audioFile.read(into: buffer, frameCount: frameCount)
854
+ try outputFile.write(from: buffer)
855
+ totalFrames += Int64(frameCount)
856
+ let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
857
+ progressCallback?(progress, 0, totalFrames * Int64(inputFormat.streamDescription.pointee.mBytesPerFrame))
1016
858
  }
1017
-
1018
- // Use the correct MIME type
1019
- let mimeType = formatStr == "aac" ? "audio/mp4" : "audio/opus"
1020
-
1021
- return TrimResult(
1022
- uri: tempOutputURL.absoluteString,
1023
- filename: tempOutputURL.lastPathComponent,
1024
- durationMs: keepRanges.map { $0[1] - $0[0] }.reduce(0, +),
1025
- size: (try? FileManager.default.attributesOfItem(atPath: tempOutputURL.path)[.size] as? Int64) ?? 0,
1026
- sampleRate: Int(targetSampleRate),
1027
- channels: targetChannels,
1028
- bitDepth: 16,
1029
- mimeType: mimeType,
1030
- requestedFormat: formatStr,
1031
- actualFormat: tempFileExtension,
1032
- compression: compressionInfo
859
+ return createTrimResult(
860
+ from: outputURL,
861
+ keepRanges: keepRanges,
862
+ formatStr: formatStr,
863
+ sampleRate: Int(targetSampleRate),
864
+ channels: targetChannels,
865
+ bitDepth: 16,
866
+ bitrate: bitrate,
867
+ compression: nil
1033
868
  )
1034
869
  }
1035
870
  }
@@ -1068,7 +903,7 @@ public class AudioProcessor {
1068
903
  private func createTrimResult(from url: URL, keepRanges: [[Double]], formatStr: String, sampleRate: Int, channels: Int, bitDepth: Int, bitrate: Int, compression: [String: Any]? = nil) -> TrimResult {
1069
904
  let durationMs = keepRanges.map { $0[1] - $0[0] }.reduce(0, +)
1070
905
  let size = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64 ?? 0) ?? 0
1071
- let fileExtension = formatStr == "wav" ? "wav" : (formatStr == "aac" ? "aac" : "opus")
906
+ let fileExtension = formatStr == "wav" ? "wav" : "aac"
1072
907
  return TrimResult(
1073
908
  uri: url.absoluteString,
1074
909
  filename: url.lastPathComponent,
@@ -110,6 +110,9 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
110
110
  // ---> ADD BACK deviceManager PROPERTY <---
111
111
  private let deviceManager = AudioDeviceManager()
112
112
 
113
+ // Add the stopping flag to the class properties
114
+ private var stopping: Bool = false
115
+
113
116
  /// Initializes the AudioStreamManager
114
117
  override init() {
115
118
  super.init()
@@ -137,11 +140,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
137
140
  }
138
141
 
139
142
  deinit {
140
- // Ensure wake lock is disabled when the manager is deallocated
141
- disableWakeLock()
142
- if let observer = notificationObserver {
143
- NotificationCenter.default.removeObserver(observer)
143
+ // Ensure wake lock is disabled when the manager is deallocated
144
+ disableWakeLock()
145
+
146
+ // Stop any active recording to properly release resources
147
+ if isRecording {
148
+ audioEngine.stop()
149
+ audioEngine.reset()
144
150
  }
151
+
152
+ // Remove ALL notification observers properly
153
+ NotificationCenter.default.removeObserver(self)
154
+
155
+ // Clean up notification manager
156
+ notificationManager?.stopUpdates()
157
+ notificationManager = nil
158
+
159
+ // Cleanup media timer
160
+ mediaInfoUpdateTimer?.invalidate()
161
+ mediaInfoUpdateTimer = nil
145
162
  }
146
163
 
147
164
  /// Handles an audio session interruption.
@@ -362,32 +379,68 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
362
379
  }
363
380
 
364
381
  @objc private func handleAppDidEnterBackground(_ notification: Notification) {
365
- if isRecording {
366
- // If keepAwake is false, we should track this as a pause
367
- if let settings = recordingSettings, !settings.keepAwake {
368
- currentPauseStart = Date()
369
- }
370
-
382
+ // Skip if we're in the process of stopping - this prevents race conditions
383
+ if !isRecording || stopping {
384
+ return
385
+ }
386
+
387
+ // If keepAwake is false, we should track this as a pause and actually pause the engine
388
+ if let settings = recordingSettings, !settings.keepAwake {
389
+ Logger.debug("AudioStreamManager", "App entering background with keepAwake=false, pausing recording")
390
+ currentPauseStart = Date()
391
+ // Explicitly pause the engine but don't change isPaused state
392
+ // so we can automatically resume when returning to foreground
393
+ audioEngine.pause()
394
+ } else {
395
+ Logger.debug("AudioStreamManager", "App entering background with keepAwake=true, continuing recording")
396
+ }
397
+
398
+ // Use a strong reference to notificationManager to avoid potential null reference
399
+ if let manager = notificationManager {
371
400
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
372
- self?.notificationManager?.showInitialNotification()
401
+ guard let self = self, self.isRecording, !self.stopping else { return }
402
+ manager.showInitialNotification()
373
403
  }
374
404
  }
375
405
  }
376
406
 
377
407
  @objc private func handleAppWillEnterForeground(_ notification: Notification) {
378
- if isRecording {
379
- // If we were paused due to background and keepAwake was false, calculate pause duration
380
- if let settings = recordingSettings, !settings.keepAwake, let pauseStart = currentPauseStart {
381
- let pauseDuration = Date().timeIntervalSince(pauseStart)
382
- totalPausedDuration += pauseDuration
383
- currentPauseStart = nil
384
- Logger.debug("AudioStreamManager", "Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
385
- }
408
+ // Skip if we're in the process of stopping
409
+ if !isRecording || stopping {
410
+ return
411
+ }
412
+
413
+ // If we were paused due to background and keepAwake was false, calculate pause duration
414
+ if let settings = recordingSettings, !settings.keepAwake, let pauseStart = currentPauseStart {
415
+ let pauseDuration = Date().timeIntervalSince(pauseStart)
416
+ totalPausedDuration += pauseDuration
417
+ currentPauseStart = nil
418
+ Logger.debug("AudioStreamManager", "Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
386
419
 
387
- notificationManager?.stopUpdates()
420
+ // Now restart the engine if it was paused due to background
421
+ do {
422
+ // Reinstall tap with hardware format to ensure we have good input
423
+ _ = installTapWithHardwareFormat()
424
+ // Restart the engine
425
+ try audioEngine.start()
426
+ Logger.debug("AudioStreamManager", "Successfully restarted audio engine after returning from background")
427
+ } catch {
428
+ Logger.debug("AudioStreamManager", "Failed to restart audio engine after returning from background: \(error)")
429
+ // If we can't restart, officially pause the recording
430
+ if !isPaused {
431
+ isPaused = true
432
+ // Notify delegate
433
+ delegate?.audioStreamManager(self, didPauseRecording: Date())
434
+ }
435
+ }
436
+ }
437
+
438
+ // Safely access notificationManager
439
+ if let manager = notificationManager {
440
+ manager.stopUpdates()
388
441
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
389
- guard let self = self else { return }
390
- self.notificationManager?.startUpdates(startTime: self.startTime ?? Date())
442
+ guard let self = self, self.isRecording, !self.stopping else { return }
443
+ manager.startUpdates(startTime: self.startTime ?? Date())
391
444
  }
392
445
  }
393
446
  }
@@ -768,9 +821,21 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
768
821
  // Append necessary options for background recording if keepAwake is enabled
769
822
  if settings.keepAwake {
770
823
  Logger.debug("AudioStreamManager", "keepAwake enabled - configuring for background recording")
771
- // Add background audio option
824
+ // Set the category to PlayAndRecord with proper background options
772
825
  options.insert(.mixWithOthers)
773
- try session.setActive(true, options: .notifyOthersOnDeactivation)
826
+ // Add duckOthers to reduce volume of other apps instead of stopping them
827
+ options.insert(.duckOthers)
828
+
829
+ // Configure audio session for background audio
830
+ do {
831
+ try session.setCategory(.playAndRecord, mode: .default, options: options)
832
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
833
+ // Ensure the app has appropriate Info.plist settings for background audio
834
+ Logger.debug("AudioStreamManager", "Audio session configured for background recording with options: \(options)")
835
+ } catch {
836
+ Logger.debug("AudioStreamManager", "Failed to configure audio session for background: \(error)")
837
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
838
+ }
774
839
  } else {
775
840
  Logger.debug("AudioStreamManager", "keepAwake disabled - using standard session configuration")
776
841
  // If keepAwake is false, don't add background audio options
@@ -1415,7 +1480,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1415
1480
  self.lastEmissionTime = currentTime
1416
1481
  self.lastEmittedSize = currentTotalSize
1417
1482
  accumulatedData.removeAll()
1418
- var compressionInfo: [String: Any]? = nil
1483
+ let compressionInfo: [String: Any]? = nil
1419
1484
 
1420
1485
  Logger.debug("EMISSION SUCCESS: Emitting \(dataToEmit.count) bytes at recording time \(recordingTime)s")
1421
1486
 
@@ -1437,7 +1502,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1437
1502
  if let lastEmissionAnalysis = self.lastEmissionTimeAnalysis,
1438
1503
  currentTime.timeIntervalSince(lastEmissionAnalysis) >= emissionIntervalAnalysis,
1439
1504
  settings.enableProcessing,
1440
- let processor = self.audioProcessor,
1505
+ let _ = self.audioProcessor,
1441
1506
  !accumulatedAnalysisData.isEmpty {
1442
1507
  let dataToAnalyze = accumulatedAnalysisData
1443
1508
  self.lastEmissionTimeAnalysis = currentTime
@@ -1559,9 +1624,13 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1559
1624
 
1560
1625
  /// Stops the current audio recording.
1561
1626
  /// - Returns: A RecordingResult object if the recording stopped successfully, or nil otherwise.
1627
+ /// - Throws: An error if recording stops with a problem.
1562
1628
  func stopRecording() -> RecordingResult? {
1563
1629
  guard isRecording || isPrepared else { return nil }
1564
1630
 
1631
+ // Set stopping flag to prevent race conditions with background/foreground transitions
1632
+ stopping = true
1633
+
1565
1634
  Logger.debug("Stopping recording...")
1566
1635
 
1567
1636
  // IMPORTANT: Emit any remaining audio data before stopping the engine
@@ -1585,7 +1654,11 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1585
1654
  }
1586
1655
 
1587
1656
  disableWakeLock()
1588
- audioEngine.stop()
1657
+
1658
+ // Handle audio engine operations directly - no need for try-catch
1659
+ if audioEngine.isRunning {
1660
+ audioEngine.stop()
1661
+ }
1589
1662
  audioEngine.inputNode.removeTap(onBus: 0)
1590
1663
 
1591
1664
  // Stop compressed recording if active
@@ -1602,20 +1675,21 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1602
1675
  // If we were only prepared but never started recording, clean up and return nil
1603
1676
  if !wasRecording {
1604
1677
  cleanupPreparation()
1678
+ stopping = false // Reset stopping flag
1605
1679
  return nil
1606
1680
  }
1607
1681
 
1608
1682
  if recordingSettings?.showNotification == true {
1609
- // Stop and clean up timer
1610
- mediaInfoUpdateTimer?.invalidate()
1611
- mediaInfoUpdateTimer = nil
1612
-
1613
- // Clean up notification manager
1614
- notificationManager?.stopUpdates()
1615
- notificationManager = nil
1616
-
1617
- // Clean up media controls
1683
+ // Stop and clean up timer safely
1618
1684
  DispatchQueue.main.async {
1685
+ self.mediaInfoUpdateTimer?.invalidate()
1686
+ self.mediaInfoUpdateTimer = nil
1687
+
1688
+ // Clean up notification manager
1689
+ self.notificationManager?.stopUpdates()
1690
+ self.notificationManager = nil
1691
+
1692
+ // Clean up media controls
1619
1693
  UIApplication.shared.endReceivingRemoteControlEvents()
1620
1694
  self.remoteCommandCenter?.pauseCommand.isEnabled = false
1621
1695
  self.remoteCommandCenter?.playCommand.isEnabled = false
@@ -1623,11 +1697,12 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1623
1697
  }
1624
1698
  }
1625
1699
 
1626
- // Reset audio session
1700
+ // Reset audio session safely
1627
1701
  do {
1628
1702
  try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
1629
1703
  } catch {
1630
1704
  Logger.debug("Error deactivating audio session: \(error)")
1705
+ // Continue with cleanup despite session errors
1631
1706
  }
1632
1707
 
1633
1708
  // Reset audio engine
@@ -1636,9 +1711,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1636
1711
  guard let fileURL = recordingFileURL,
1637
1712
  let settings = recordingSettings else {
1638
1713
  Logger.debug("Recording or file URL is nil.")
1714
+ stopping = false // Reset stopping flag before returning nil
1639
1715
  return nil
1640
1716
  }
1641
1717
 
1718
+ // Reset stopping flag before returning
1719
+ let result = createRecordingResult(fileURL: fileURL, settings: settings, finalDuration: finalDuration)
1720
+ stopping = false
1721
+
1722
+ // Return after all cleanup tasks are completed
1723
+ return result
1724
+ }
1725
+
1726
+ /// Creates a RecordingResult from the finished recording
1727
+ /// - Parameters:
1728
+ /// - fileURL: The URL of the recording file
1729
+ /// - settings: The settings used for recording
1730
+ /// - finalDuration: The final duration of the recording
1731
+ /// - Returns: A RecordingResult object or nil if validation fails
1732
+ private func createRecordingResult(fileURL: URL, settings: RecordingSettings, finalDuration: TimeInterval) -> RecordingResult? {
1642
1733
  // Validate WAV file
1643
1734
  let wavPath = fileURL.path
1644
1735
  do {
@@ -1871,12 +1962,12 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1871
1962
  do {
1872
1963
  let session = AVAudioSession.sharedInstance()
1873
1964
  try session.setActive(false, options: .notifyOthersOnDeactivation)
1874
- Thread.sleep(forTimeInterval: 0.2) // Give system time to release resources
1965
+ try await Task.sleep(nanoseconds: 200_000_000) // Give system time to release resources
1875
1966
 
1876
1967
  // Reconfigure the session completely
1877
1968
  try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
1878
1969
  try session.setActive(true, options: .notifyOthersOnDeactivation)
1879
- Thread.sleep(forTimeInterval: 0.1) // Allow the session to activate fully
1970
+ try await Task.sleep(nanoseconds: 100_000_000) // Allow the session to activate fully
1880
1971
  } catch {
1881
1972
  Logger.debug("Session reset error: \(error.localizedDescription)")
1882
1973
  }
@@ -1938,7 +2029,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1938
2029
  }
1939
2030
 
1940
2031
  // Use our shared tap installation method with the custom block
1941
- installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
2032
+ _ = installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
1942
2033
  Logger.debug("Fallback: Re-installed tap with enhanced emission handling")
1943
2034
 
1944
2035
  // Force prepare engine again to ensure it's ready
@@ -1953,7 +2044,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1953
2044
  Logger.debug("Audio engine restarted for fallback.")
1954
2045
  } catch {
1955
2046
  // Try ONE more time with delay
1956
- Thread.sleep(forTimeInterval: 0.2)
2047
+ try await Task.sleep(nanoseconds: 200_000_000)
1957
2048
  do {
1958
2049
  try audioEngine.start()
1959
2050
  Logger.debug("Audio engine restarted on second attempt after fallback.")
@@ -2047,6 +2138,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
2047
2138
  "isPaused": isPaused // Report current state
2048
2139
  ])
2049
2140
  Logger.debug("Fallback to device \(defaultDevice.id) successful.")
2141
+
2142
+ // Make the catch block reachable by throwing an error unconditionally
2143
+ // This is required to fix a compiler warning about unreachable catch block
2144
+ throw NSError(domain: "AudioStreamManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Intentional error to make catch block reachable"])
2050
2145
 
2051
2146
  } catch {
2052
2147
  Logger.debug("Fallback failed with error: \(error). Pausing.")
@@ -44,11 +44,11 @@ extension DataPoint {
44
44
  "silent": silent,
45
45
  "features": features?.toDictionary() ?? [:],
46
46
  "speech": speech?.toDictionary() ?? [:],
47
- "startTime": startTime ?? 0,
48
- "endTime": endTime ?? 0,
49
- "startPosition": startPosition ?? 0,
50
- "endPosition": endPosition ?? 0,
51
- "samples": samples ?? 0
47
+ "startTime": startTime,
48
+ "endTime": endTime,
49
+ "startPosition": startPosition,
50
+ "endPosition": endPosition,
51
+ "samples": samples
52
52
  ]
53
53
  }
54
54
  }
@@ -294,6 +294,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
294
294
  /// - promise: A promise to resolve with the recording result or reject with an error.
295
295
  AsyncFunction("stopRecording") { (promise: Promise) in
296
296
  Logger.debug("ExpoAudioStreamModule", "stopRecording called.")
297
+
297
298
  if let recordingResult = self.streamManager.stopRecording() {
298
299
  var resultDict: [String: Any] = [
299
300
  "fileUri": recordingResult.fileUri,
@@ -705,7 +706,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
705
706
  Logger.debug("ExpoAudioStreamModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
706
707
  if let options = options, let refresh = options["refresh"] as? Bool, refresh {
707
708
  Logger.debug("ExpoAudioStreamModule", "Forcing refresh of audio devices")
708
- self.deviceManager.forceRefreshAudioSession()
709
+ _ = self.deviceManager.forceRefreshAudioSession()
709
710
  }
710
711
 
711
712
  // Call the device manager with the promise
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.6.3",
3
+ "version": "2.8.0",
4
4
  "description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",
@@ -44,7 +44,7 @@
44
44
  "ios",
45
45
  "cpp",
46
46
  "plugin",
47
- "app.plugin.js",
47
+ "app.plugin.cjs",
48
48
  "LICENSE",
49
49
  "CHANGELOG.md",
50
50
  "generated",
@@ -66,7 +66,7 @@
66
66
  ],
67
67
  "scripts": {
68
68
  "build": "tsc",
69
- "build:plugin": "tsc --build plugin/tsconfig.json",
69
+ "build:plugin": "tsc --build plugin/tsconfig.json && node scripts/convert-to-cjs.js",
70
70
  "build:plugin:dev": "expo-module build plugin",
71
71
  "build:dev": "expo-module build",
72
72
  "clean": "expo-module clean && rimraf plugin/build",
@@ -74,7 +74,7 @@
74
74
  "test": "expo-module test",
75
75
  "typecheck": "tsc --noEmit",
76
76
  "docgen": "typedoc src/index.ts --plugin typedoc-plugin-markdown --readme none --out ../../documentation_site/docs/api-reference/API",
77
- "prepare": "yarn build && yarn build:plugin",
77
+ "prepare": "yarn build && yarn build:plugin && node -e \"require('fs').renameSync('./plugin/build/index.d.ts', './plugin/build/index.d.cts')\"",
78
78
  "prepublishOnly": "expo-module prepublishOnly",
79
79
  "expo-module": "expo-module",
80
80
  "open:ios": "open -a \"Xcode\" ../../apps/playground/ios",
@@ -99,7 +99,7 @@
99
99
  "eslint-plugin-prettier": "^5.1.3",
100
100
  "eslint-plugin-promise": "^6.1.1",
101
101
  "eslint-plugin-react": "^7.34.1",
102
- "expo": "^52.0.27",
102
+ "expo": "^53.0.6",
103
103
  "expo-module-scripts": "^4.0.2",
104
104
  "jest": "^29.7.0",
105
105
  "prettier": "^3.2.5",
@@ -121,6 +121,6 @@
121
121
  "registry": "https://registry.npmjs.org"
122
122
  },
123
123
  "dependencies": {
124
- "expo-modules-core": "~2.1.4"
124
+ "expo-modules-core": "~2.3.12"
125
125
  }
126
126
  }
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_plugins_1 = require("@expo/config-plugins");
4
+ const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
5
+ const NOTIFICATION_USAGE = 'Show recording notifications and controls';
6
+ const LOG_PREFIX = '[@siteed/expo-audio-studio]';
7
+ function debugLog(message, ...args) {
8
+ if (process.env.EXPO_DEBUG) {
9
+ console.log(`${LOG_PREFIX} ${message}`, ...args);
10
+ }
11
+ }
12
+ const withRecordingPermission = (config, props) => {
13
+ const options = {
14
+ enablePhoneStateHandling: true, // Default to true for backward compatibility
15
+ enableNotifications: true,
16
+ enableBackgroundAudio: true,
17
+ iosBackgroundModes: {
18
+ useVoIP: false,
19
+ useAudio: false,
20
+ useProcessing: false,
21
+ useLocation: false,
22
+ useExternalAccessory: false,
23
+ },
24
+ iosConfig: {
25
+ microphoneUsageDescription: MICROPHONE_USAGE,
26
+ notificationUsageDescription: NOTIFICATION_USAGE,
27
+ },
28
+ ...(props || {}),
29
+ };
30
+ const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, } = options;
31
+ debugLog('📱 Configuring Recording Permissions Plugin...', options);
32
+ // iOS Configuration
33
+ config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
34
+ // Always set the microphone usage description from options first
35
+ config.modResults['NSMicrophoneUsageDescription'] =
36
+ options.iosConfig?.microphoneUsageDescription ||
37
+ config.modResults['NSMicrophoneUsageDescription'] ||
38
+ MICROPHONE_USAGE;
39
+ if (enableNotifications) {
40
+ config.modResults['NSUserNotificationsUsageDescription'] =
41
+ options.iosConfig?.notificationUsageDescription ||
42
+ config.modResults['NSUserNotificationsUsageDescription'] ||
43
+ NOTIFICATION_USAGE;
44
+ config.modResults['NSUserNotificationAlertStyle'] = 'alert';
45
+ }
46
+ const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
47
+ // If background audio is enabled with useAudio, add the audio background mode
48
+ if (options.iosBackgroundModes?.useAudio === true &&
49
+ enableBackgroundAudio === true &&
50
+ !existingBackgroundModes.includes('audio')) {
51
+ // Add 'audio' background mode - REQUIRED for background recording
52
+ existingBackgroundModes.push('audio');
53
+ debugLog('✅ Added audio background mode for iOS background recording');
54
+ // Also ensure processing mode is recommended
55
+ if (options.iosBackgroundModes?.useProcessing !== true) {
56
+ console.warn(`${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`);
57
+ }
58
+ }
59
+ if (options.iosBackgroundModes?.useVoIP === true &&
60
+ enablePhoneStateHandling === true) {
61
+ if (!existingBackgroundModes.includes('voip')) {
62
+ existingBackgroundModes.push('voip');
63
+ }
64
+ const existingCapabilities = (config.modResults
65
+ .UIRequiredDeviceCapabilities || []);
66
+ if (!existingCapabilities.includes('telephony')) {
67
+ existingCapabilities.push('telephony');
68
+ }
69
+ config.modResults.UIRequiredDeviceCapabilities =
70
+ existingCapabilities;
71
+ }
72
+ // Add additional background modes only if explicitly set to true
73
+ if (options.iosBackgroundModes?.useProcessing === true) {
74
+ if (!existingBackgroundModes.includes('processing')) {
75
+ existingBackgroundModes.push('processing');
76
+ }
77
+ // Add processing info if enabled
78
+ // Note: We keep the 'audiostream' namespace for native modules to maintain compatibility
79
+ config.modResults.BGTaskSchedulerPermittedIdentifiers = [
80
+ 'com.siteed.audiostream.processing',
81
+ ];
82
+ }
83
+ if (options.iosBackgroundModes?.useLocation === true) {
84
+ if (!existingBackgroundModes.includes('location')) {
85
+ existingBackgroundModes.push('location');
86
+ }
87
+ }
88
+ if (options.iosBackgroundModes?.useExternalAccessory === true) {
89
+ if (!existingBackgroundModes.includes('external-accessory')) {
90
+ existingBackgroundModes.push('external-accessory');
91
+ }
92
+ }
93
+ // Configure background processing info if enabled
94
+ if (options.iosConfig?.backgroundProcessingTitle) {
95
+ config.modResults.BGProcessingTaskTitle =
96
+ options.iosConfig.backgroundProcessingTitle;
97
+ }
98
+ // Configure audio session behavior
99
+ if (options.iosConfig?.allowBackgroundAudioControls) {
100
+ config.modResults.UIBackgroundModes = [
101
+ ...existingBackgroundModes,
102
+ 'remote-notification',
103
+ ];
104
+ config.modResults.MPNowPlayingInfoPropertyPlaybackRate = true;
105
+ }
106
+ config.modResults.UIBackgroundModes = existingBackgroundModes;
107
+ return config;
108
+ });
109
+ // Android Configuration
110
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
111
+ const basePermissions = [
112
+ 'android.permission.RECORD_AUDIO',
113
+ 'android.permission.WAKE_LOCK',
114
+ ];
115
+ const optionalPermissions = [
116
+ enableNotifications && 'android.permission.POST_NOTIFICATIONS',
117
+ enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
118
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
119
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
120
+ ].filter(Boolean);
121
+ const permissionsToAdd = [...basePermissions, ...optionalPermissions];
122
+ debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
123
+ debugLog('➕ Adding Android permissions:', permissionsToAdd);
124
+ const { addPermission } = config_plugins_1.AndroidConfig.Permissions;
125
+ // Add each permission only if it doesn't exist
126
+ permissionsToAdd.forEach((permission) => {
127
+ const existingPermission = config.modResults.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
128
+ if (!existingPermission) {
129
+ addPermission(config.modResults, permission);
130
+ }
131
+ });
132
+ // Get the main application node
133
+ const mainApplication = config.modResults.manifest.application?.[0];
134
+ if (mainApplication) {
135
+ debugLog('📱 Configuring Android application components...');
136
+ // Add RecordingActionReceiver
137
+ if (!mainApplication.receiver) {
138
+ mainApplication.receiver = [];
139
+ }
140
+ const receiverConfig = {
141
+ $: {
142
+ 'android:name': '.RecordingActionReceiver',
143
+ 'android:exported': 'false',
144
+ },
145
+ 'intent-filter': [
146
+ {
147
+ action: [
148
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
149
+ { $: { 'android:name': 'RESUME_RECORDING' } },
150
+ { $: { 'android:name': 'STOP_RECORDING' } },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ const receiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$?.['android:name'] === '.RecordingActionReceiver');
156
+ if (receiverIndex >= 0) {
157
+ mainApplication.receiver[receiverIndex] = receiverConfig;
158
+ }
159
+ else {
160
+ mainApplication.receiver.push(receiverConfig);
161
+ }
162
+ debugLog('✅ RecordingActionReceiver configured');
163
+ // Add AudioRecordingService
164
+ if (!mainApplication.service) {
165
+ mainApplication.service = [];
166
+ }
167
+ const serviceConfig = {
168
+ $: {
169
+ 'android:name': '.AudioRecordingService',
170
+ 'android:enabled': 'true',
171
+ 'android:exported': 'false',
172
+ 'android:foregroundServiceType': 'microphone',
173
+ },
174
+ };
175
+ const serviceIndex = mainApplication.service.findIndex((service) => service.$?.['android:name'] === '.AudioRecordingService');
176
+ if (serviceIndex >= 0) {
177
+ mainApplication.service[serviceIndex] = serviceConfig;
178
+ }
179
+ else {
180
+ mainApplication.service.push(serviceConfig);
181
+ }
182
+ debugLog('✅ AudioRecordingService configured');
183
+ }
184
+ else {
185
+ console.error(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`);
186
+ }
187
+ return config;
188
+ });
189
+ debugLog('✨ Recording Permissions Plugin configuration completed');
190
+ return config;
191
+ };
192
+ exports.default = withRecordingPermission;
@@ -44,15 +44,16 @@ const withRecordingPermission = (config, props) => {
44
44
  config.modResults['NSUserNotificationAlertStyle'] = 'alert';
45
45
  }
46
46
  const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
47
- // Only add background modes if explicitly enabled and set to true
47
+ // If background audio is enabled with useAudio, add the audio background mode
48
48
  if (options.iosBackgroundModes?.useAudio === true &&
49
49
  enableBackgroundAudio === true &&
50
50
  !existingBackgroundModes.includes('audio')) {
51
- // Don't automatically add 'audio' background mode as it's only for playback
52
- // existingBackgroundModes.push('audio')
53
- // Instead, ensure processing mode is used for background recording
51
+ // Add 'audio' background mode - REQUIRED for background recording
52
+ existingBackgroundModes.push('audio');
53
+ debugLog('✅ Added audio background mode for iOS background recording');
54
+ // Also ensure processing mode is recommended
54
55
  if (options.iosBackgroundModes?.useProcessing !== true) {
55
- console.warn(`${LOG_PREFIX} Warning: Background audio recording requires 'processing' background mode. Please enable 'useProcessing' in iosBackgroundModes.`);
56
+ console.warn(`${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`);
56
57
  }
57
58
  }
58
59
  if (options.iosBackgroundModes?.useVoIP === true &&
@@ -84,19 +84,22 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
84
84
  const existingBackgroundModes =
85
85
  config.modResults.UIBackgroundModes || []
86
86
 
87
- // Only add background modes if explicitly enabled and set to true
87
+ // If background audio is enabled with useAudio, add the audio background mode
88
88
  if (
89
89
  options.iosBackgroundModes?.useAudio === true &&
90
90
  enableBackgroundAudio === true &&
91
91
  !existingBackgroundModes.includes('audio')
92
92
  ) {
93
- // Don't automatically add 'audio' background mode as it's only for playback
94
- // existingBackgroundModes.push('audio')
93
+ // Add 'audio' background mode - REQUIRED for background recording
94
+ existingBackgroundModes.push('audio')
95
+ debugLog(
96
+ '✅ Added audio background mode for iOS background recording'
97
+ )
95
98
 
96
- // Instead, ensure processing mode is used for background recording
99
+ // Also ensure processing mode is recommended
97
100
  if (options.iosBackgroundModes?.useProcessing !== true) {
98
101
  console.warn(
99
- `${LOG_PREFIX} Warning: Background audio recording requires 'processing' background mode. Please enable 'useProcessing' in iosBackgroundModes.`
102
+ `${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`
100
103
  )
101
104
  }
102
105
  }
package/app.plugin.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./plugin/build')