@siteed/audio-studio 3.2.1-beta.0 → 3.2.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +41 -1
  3. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +130 -0
  4. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1 -0
  5. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
  6. package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
  7. package/build/cjs/AudioStudio.types.js.map +1 -1
  8. package/build/cjs/AudioStudio.web.js +125 -13
  9. package/build/cjs/AudioStudio.web.js.map +1 -1
  10. package/build/cjs/AudioStudioModule.js +6 -1
  11. package/build/cjs/AudioStudioModule.js.map +1 -1
  12. package/build/cjs/events.js +4 -0
  13. package/build/cjs/events.js.map +1 -1
  14. package/build/cjs/index.js +3 -1
  15. package/build/cjs/index.js.map +1 -1
  16. package/build/cjs/useAudioRecorder.js +187 -30
  17. package/build/cjs/useAudioRecorder.js.map +1 -1
  18. package/build/cjs/utils/nativeRecordingOptions.js +13 -0
  19. package/build/cjs/utils/nativeRecordingOptions.js.map +1 -0
  20. package/build/cjs/utils/nativeRecordingOptions.test.js +30 -0
  21. package/build/cjs/utils/nativeRecordingOptions.test.js.map +1 -0
  22. package/build/esm/AudioStudio.types.js.map +1 -1
  23. package/build/esm/AudioStudio.web.js +125 -13
  24. package/build/esm/AudioStudio.web.js.map +1 -1
  25. package/build/esm/AudioStudioModule.js +6 -1
  26. package/build/esm/AudioStudioModule.js.map +1 -1
  27. package/build/esm/events.js +3 -0
  28. package/build/esm/events.js.map +1 -1
  29. package/build/esm/index.js +1 -0
  30. package/build/esm/index.js.map +1 -1
  31. package/build/esm/useAudioRecorder.js +188 -31
  32. package/build/esm/useAudioRecorder.js.map +1 -1
  33. package/build/esm/utils/nativeRecordingOptions.js +10 -0
  34. package/build/esm/utils/nativeRecordingOptions.js.map +1 -0
  35. package/build/esm/utils/nativeRecordingOptions.test.js +28 -0
  36. package/build/esm/utils/nativeRecordingOptions.test.js.map +1 -0
  37. package/build/types/AudioStudio.types.d.ts +58 -1
  38. package/build/types/AudioStudio.types.d.ts.map +1 -1
  39. package/build/types/AudioStudio.web.d.ts +17 -1
  40. package/build/types/AudioStudio.web.d.ts.map +1 -1
  41. package/build/types/AudioStudioModule.d.ts.map +1 -1
  42. package/build/types/events.d.ts +2 -1
  43. package/build/types/events.d.ts.map +1 -1
  44. package/build/types/index.d.ts +1 -0
  45. package/build/types/index.d.ts.map +1 -1
  46. package/build/types/useAudioRecorder.d.ts +4 -1
  47. package/build/types/useAudioRecorder.d.ts.map +1 -1
  48. package/build/types/utils/nativeRecordingOptions.d.ts +28 -0
  49. package/build/types/utils/nativeRecordingOptions.d.ts.map +1 -0
  50. package/build/types/utils/nativeRecordingOptions.test.d.ts +2 -0
  51. package/build/types/utils/nativeRecordingOptions.test.d.ts.map +1 -0
  52. package/ios/AudioStreamManager.swift +103 -9
  53. package/ios/AudioStreamManagerDelegate.swift +1 -0
  54. package/ios/AudioStudioModule.swift +6 -0
  55. package/ios/RecordingSettings.swift +48 -43
  56. package/package.json +1 -1
  57. package/src/AudioStudio.types.ts +70 -1
  58. package/src/AudioStudio.web.ts +152 -13
  59. package/src/AudioStudioModule.ts +6 -1
  60. package/src/events.ts +13 -1
  61. package/src/index.ts +1 -0
  62. package/src/useAudioRecorder.tsx +260 -45
  63. package/src/utils/nativeRecordingOptions.test.ts +29 -0
  64. package/src/utils/nativeRecordingOptions.ts +20 -0
@@ -0,0 +1,28 @@
1
+ import { RecordingConfig } from '../AudioStudio.types';
2
+ export declare function createNativeRecordingOptions(recordingOptions: RecordingConfig): {
3
+ sampleRate?: import("..").SampleRate;
4
+ channels?: 1 | 2;
5
+ encoding?: import("..").EncodingType;
6
+ interval?: number;
7
+ intervalAnalysis?: number;
8
+ keepAwake?: boolean;
9
+ showNotification?: boolean;
10
+ showWaveformInNotification?: boolean;
11
+ notification?: import("..").NotificationConfig;
12
+ enableProcessing?: boolean;
13
+ ios?: import("..").IOSConfig;
14
+ android?: import("..").AndroidConfig;
15
+ web?: import("..").WebConfig;
16
+ segmentDurationMs?: number;
17
+ features?: import("..").AudioFeaturesOptions;
18
+ output?: import("..").OutputConfig;
19
+ autoResumeAfterInterruption?: boolean;
20
+ maxDurationMs?: number;
21
+ outputDirectory?: string;
22
+ filename?: string;
23
+ deviceId?: string;
24
+ deviceDisconnectionBehavior?: import("..").DeviceDisconnectionBehaviorType;
25
+ bufferDurationSeconds?: number;
26
+ streamFormat?: "float32" | "raw";
27
+ };
28
+ //# sourceMappingURL=nativeRecordingOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeRecordingOptions.d.ts","sourceRoot":"","sources":["../../../src/utils/nativeRecordingOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAGtD,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;EAgB7E"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=nativeRecordingOptions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeRecordingOptions.test.d.ts","sourceRoot":"","sources":["../../../src/utils/nativeRecordingOptions.test.ts"],"names":[],"mappings":""}
@@ -11,6 +11,7 @@ import Accelerate
11
11
  import UIKit
12
12
  import MediaPlayer
13
13
  import UserNotifications
14
+ import QuartzCore
14
15
 
15
16
  // Constants
16
17
  internal let WAV_HEADER_SIZE: Int64 = 44 // Standard WAV header is 44 bytes
@@ -48,6 +49,11 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
48
49
  private var startTime: Date?
49
50
  private var totalPausedDuration: TimeInterval = 0 // Track total paused time
50
51
  private var currentPauseStart: Date? // Track current pause start
52
+ private var maxDurationTimer: DispatchSourceTimer?
53
+ private var maxDurationTargetMs: Int64 = 0
54
+ private var maxDurationAccumulatedActiveMs: Double = 0
55
+ private var maxDurationSegmentStart: CFTimeInterval?
56
+ private var maxDurationReached: Bool = false
51
57
  var isRecording = false
52
58
  var isPaused = false
53
59
  var isPrepared = false // Add this new state flag
@@ -690,37 +696,39 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
690
696
  /// Gets the current status of the recording.
691
697
  /// - Returns: A dictionary containing the recording status information.
692
698
  func getStatus() -> [String: Any] {
693
- guard let settings = recordingSettings else {
694
- print("Recording settings are not available.")
695
- return [:]
696
- }
697
-
698
699
  let durationInSeconds = currentRecordingDuration()
699
700
  let durationInMilliseconds = Int(durationInSeconds * 1000)
701
+ let settings = recordingSettings
700
702
 
701
703
  var status: [String: Any] = [
702
704
  "durationMs": durationInMilliseconds,
703
705
  "isRecording": isRecording,
704
706
  "isPaused": isPaused,
705
707
  "mimeType": mimeType,
706
- "size": totalDataSize
708
+ "size": totalDataSize,
709
+ "maxDurationReached": maxDurationReached
707
710
  ]
711
+
712
+ let statusMaxDurationMs = settings?.maxDurationMs ?? maxDurationTargetMs
713
+ if statusMaxDurationMs > 0 {
714
+ status["maxDurationMs"] = statusMaxDurationMs
715
+ }
708
716
 
709
717
  // Safely handle optional interval values
710
- if let interval = settings.interval {
718
+ if let interval = settings?.interval {
711
719
  status["interval"] = interval
712
720
  } else {
713
721
  status["interval"] = 1000 // Default value
714
722
  }
715
723
 
716
- if let intervalAnalysis = settings.intervalAnalysis {
724
+ if let intervalAnalysis = settings?.intervalAnalysis {
717
725
  status["intervalAnalysis"] = intervalAnalysis
718
726
  } else {
719
727
  status["intervalAnalysis"] = 500 // Default value
720
728
  }
721
729
 
722
730
  // Add compression info if enabled
723
- if settings.output.compressed.enabled,
731
+ if settings?.output.compressed.enabled == true,
724
732
  let compressedURL = compressedFileURL,
725
733
  FileManager.default.fileExists(atPath: compressedURL.path) {
726
734
  do {
@@ -744,6 +752,86 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
744
752
  return status
745
753
  }
746
754
 
755
+ private func maxDurationActiveMs(now: CFTimeInterval = CACurrentMediaTime()) -> Double {
756
+ if let segmentStart = maxDurationSegmentStart {
757
+ return maxDurationAccumulatedActiveMs + ((now - segmentStart) * 1000)
758
+ }
759
+ return maxDurationAccumulatedActiveMs
760
+ }
761
+
762
+ private func startMaxDurationTimer(settings: RecordingSettings) {
763
+ cancelMaxDurationTimer()
764
+ maxDurationReached = false
765
+ maxDurationTargetMs = settings.maxDurationMs
766
+ maxDurationAccumulatedActiveMs = 0
767
+ maxDurationSegmentStart = nil
768
+ guard settings.maxDurationMs > 0 else { return }
769
+
770
+ maxDurationSegmentStart = CACurrentMediaTime()
771
+ scheduleMaxDurationTimer()
772
+ }
773
+
774
+ private func scheduleMaxDurationTimer() {
775
+ guard maxDurationTargetMs > 0, !maxDurationReached, isRecording, !isPaused else {
776
+ return
777
+ }
778
+
779
+ maxDurationTimer?.cancel()
780
+ let remainingMs = max(0, Double(maxDurationTargetMs) - maxDurationActiveMs())
781
+ let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
782
+ timer.schedule(deadline: .now() + .milliseconds(Int(remainingMs.rounded(.up))))
783
+ timer.setEventHandler { [weak self] in
784
+ self?.maxDurationTimer = nil
785
+ self?.emitMaxDurationReached()
786
+ }
787
+ maxDurationTimer = timer
788
+ timer.resume()
789
+ }
790
+
791
+ private func pauseMaxDurationTimer() {
792
+ maxDurationTimer?.cancel()
793
+ maxDurationTimer = nil
794
+ if maxDurationSegmentStart != nil {
795
+ maxDurationAccumulatedActiveMs = maxDurationActiveMs()
796
+ maxDurationSegmentStart = nil
797
+ }
798
+ }
799
+
800
+ private func resumeMaxDurationTimer() {
801
+ guard maxDurationTargetMs > 0, !maxDurationReached else { return }
802
+ maxDurationSegmentStart = CACurrentMediaTime()
803
+ scheduleMaxDurationTimer()
804
+ }
805
+
806
+ private func cancelMaxDurationTimer() {
807
+ maxDurationTimer?.cancel()
808
+ maxDurationTimer = nil
809
+ maxDurationSegmentStart = nil
810
+ if !maxDurationReached {
811
+ maxDurationTargetMs = 0
812
+ maxDurationAccumulatedActiveMs = 0
813
+ }
814
+ }
815
+
816
+ private func emitMaxDurationReached() {
817
+ guard maxDurationTargetMs > 0, !maxDurationReached else { return }
818
+ guard isRecording, !isPaused else { return }
819
+
820
+ let durationMs = Int64(maxDurationActiveMs().rounded())
821
+ maxDurationReached = true
822
+ let shouldAutoStop = recordingSettings?.autoStopOnMaxDuration == true
823
+ delegate?.audioStreamManager(self, didReachMaxDuration: [
824
+ "durationMs": durationMs,
825
+ "maxDurationMs": maxDurationTargetMs,
826
+ "overrunMs": max(Int64(0), durationMs - maxDurationTargetMs),
827
+ "streamUuid": recordingUUID ?? "",
828
+ "autoStopped": shouldAutoStop
829
+ ])
830
+ if shouldAutoStop {
831
+ _ = stopRecording()
832
+ }
833
+ }
834
+
747
835
  /// Builds compression info dictionary with incremental compressed data for streaming events.
748
836
  /// Mirrors the Android pattern (AudioRecorderManager.kt) for consistent cross-platform behavior.
749
837
  /// - Returns: A dictionary containing compression metadata and base64-encoded data chunk, or nil if compression is disabled.
@@ -1179,6 +1267,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1179
1267
  isRecording = true
1180
1268
  isPaused = false
1181
1269
  pausedBySystemInterruption = false
1270
+ startMaxDurationTimer(settings: settings)
1182
1271
 
1183
1272
  // Start the audio engine
1184
1273
  try audioEngine.start()
@@ -1220,6 +1309,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1220
1309
  } catch {
1221
1310
  Logger.debug("Error starting audio engine: \(error.localizedDescription)")
1222
1311
  isRecording = false
1312
+ cancelMaxDurationTimer()
1223
1313
  cleanupPreparation()
1224
1314
  return nil
1225
1315
  }
@@ -1231,6 +1321,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1231
1321
  guard isPrepared && !isRecording else { return }
1232
1322
 
1233
1323
  Logger.debug("Cleaning up prepared resources that weren't used")
1324
+ cancelMaxDurationTimer()
1234
1325
 
1235
1326
  // Remove input tap
1236
1327
  audioEngine.inputNode.removeTap(onBus: 0)
@@ -1296,6 +1387,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1296
1387
 
1297
1388
  // Store when we paused
1298
1389
  currentPauseStart = Date()
1390
+ pauseMaxDurationTimer()
1299
1391
 
1300
1392
  // Update state
1301
1393
  isPaused = true
@@ -1357,6 +1449,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1357
1449
 
1358
1450
  // Clear the stored valid duration
1359
1451
  lastValidDuration = nil
1452
+ resumeMaxDurationTimer()
1360
1453
 
1361
1454
  // Reset emission timers to ensure emission starts immediately after resume
1362
1455
  lastEmissionTime = Date()
@@ -1843,6 +1936,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
1843
1936
 
1844
1937
  // Set stopping flag to prevent race conditions with background/foreground transitions
1845
1938
  stopping = true
1939
+ cancelMaxDurationTimer()
1846
1940
 
1847
1941
  Logger.debug("Stopping recording...")
1848
1942
 
@@ -12,5 +12,6 @@ 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, didReachMaxDuration info: [String: Any])
15
16
  func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String)
16
17
  }
@@ -6,6 +6,7 @@ import AVFoundation
6
6
  private let audioDataEvent: String = "AudioData"
7
7
  private let audioAnalysisEvent: String = "AudioAnalysis"
8
8
  private let recordingInterruptedEvent: String = "onRecordingInterrupted"
9
+ private let maxDurationReachedEvent: String = "MaxDurationReached"
9
10
  private let deviceChangedEvent: String = "deviceChangedEvent"
10
11
  private let trimProgressEvent: String = "TrimProgress"
11
12
  private let errorEvent: String = "error"
@@ -48,6 +49,7 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
48
49
  audioDataEvent,
49
50
  audioAnalysisEvent,
50
51
  recordingInterruptedEvent,
52
+ maxDurationReachedEvent,
51
53
  deviceChangedEvent,
52
54
  trimProgressEvent,
53
55
  errorEvent,
@@ -1122,6 +1124,10 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
1122
1124
  "timestamp": Date().timeIntervalSince1970 * 1000
1123
1125
  ])
1124
1126
  }
1127
+
1128
+ func audioStreamManager(_ manager: AudioStreamManager, didReachMaxDuration info: [String: Any]) {
1129
+ sendEvent(maxDurationReachedEvent, info)
1130
+ }
1125
1131
 
1126
1132
  func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
1127
1133
  Logger.debug("AudioStudioModule", "Delegate: didPauseRecording")
@@ -22,13 +22,13 @@ struct OutputSettings {
22
22
  var enabled: Bool = true
23
23
  var format: String = "wav" // Currently only "wav" is supported
24
24
  }
25
-
25
+
26
26
  struct CompressedOutput {
27
27
  var enabled: Bool = false
28
28
  var format: String = "aac" // "aac" or "opus" (opus falls back to aac on iOS)
29
29
  var bitrate: Int = 128000
30
30
  }
31
-
31
+
32
32
  var primary: PrimaryOutput = PrimaryOutput()
33
33
  var compressed: CompressedOutput = CompressedOutput()
34
34
  }
@@ -39,13 +39,13 @@ struct CompressedRecordingInfo {
39
39
  var bitrate: Int
40
40
  var format: String
41
41
  var size: Int64 = 0 // Add size with default value
42
-
42
+
43
43
  static func validate(format: String, bitrate: Int) -> Result<(String, Int), Error> {
44
44
  // Validate format
45
45
  guard ["aac", "opus"].contains(format.lowercased()) else {
46
46
  return .failure(RecordingError.unsupportedFormat(format))
47
47
  }
48
-
48
+
49
49
  // Adjust bitrate based on format
50
50
  let adjustedBitrate: Int
51
51
  if format.lowercased() == "aac" {
@@ -57,7 +57,7 @@ struct CompressedRecordingInfo {
57
57
  // Typical Opus voice bitrates: 8-24 kbps, music: 32-128 kbps
58
58
  adjustedBitrate = min(max(bitrate, 8000), 320000)
59
59
  }
60
-
60
+
61
61
  return .success((format, adjustedBitrate))
62
62
  }
63
63
  }
@@ -77,7 +77,7 @@ enum RecordingError: Error {
77
77
  case unsupportedFormat(String)
78
78
  case invalidBitrate(Int)
79
79
  case invalidOutputDirectory(String)
80
-
80
+
81
81
  var localizedDescription: String {
82
82
  switch self {
83
83
  case .unsupportedFormat(let format):
@@ -98,56 +98,58 @@ struct RecordingSettings {
98
98
  var bitDepth: Int = 16
99
99
  var interval: Int?
100
100
  var intervalAnalysis: Int?
101
-
101
+
102
102
  // Feature flags
103
103
  var keepAwake: Bool = true
104
104
  var showNotification: Bool = false
105
105
  var enableProcessing: Bool = false
106
-
106
+
107
107
  // Remove pointsPerSecond and algorithm
108
108
  var featureOptions: [String: Bool]? = ["rms": true, "zcr": true]
109
-
109
+
110
110
  // iOS-specific configuration
111
111
  var ios: IOSConfig?
112
-
112
+
113
113
  // Notification configuration
114
114
  var notification: NotificationConfig?
115
-
115
+
116
116
  // Output configuration
117
117
  var output: OutputSettings = OutputSettings()
118
-
118
+
119
119
  let autoResumeAfterInterruption: Bool
120
-
120
+
121
121
  var outputDirectory: String? = nil
122
122
  var filename: String? = nil
123
-
123
+
124
124
  // Update default to 100ms
125
125
  var segmentDurationMs: Int = 100 // Default 100ms segments
126
-
126
+
127
127
  // Add these new properties
128
128
  var deviceId: String?
129
129
  var deviceDisconnectionBehavior: DeviceDisconnectionBehavior = .FALLBACK
130
130
  var bufferDurationSeconds: Double?
131
131
  var streamFormat: String = "raw"
132
-
132
+ var maxDurationMs: Int64 = 0
133
+ var autoStopOnMaxDuration: Bool = false
134
+
133
135
  static func fromDictionary(_ dict: [String: Any]) -> Result<RecordingSettings, Error> {
134
136
  // Parse output configuration
135
137
  var outputSettings = OutputSettings()
136
-
138
+
137
139
  if let outputDict = dict["output"] as? [String: Any] {
138
140
  // Parse primary output settings
139
141
  if let primaryDict = outputDict["primary"] as? [String: Any] {
140
142
  outputSettings.primary.enabled = primaryDict["enabled"] as? Bool ?? true
141
143
  outputSettings.primary.format = primaryDict["format"] as? String ?? "wav"
142
144
  }
143
-
145
+
144
146
  // Parse compressed output settings
145
147
  if let compressedDict = outputDict["compressed"] as? [String: Any] {
146
148
  outputSettings.compressed.enabled = compressedDict["enabled"] as? Bool ?? false
147
149
  let format = (compressedDict["format"] as? String)?.lowercased() ?? "aac"
148
150
  outputSettings.compressed.format = format
149
151
  outputSettings.compressed.bitrate = compressedDict["bitrate"] as? Int ?? 128000
150
-
152
+
151
153
  // Validate compression settings if enabled
152
154
  if outputSettings.compressed.enabled {
153
155
  if case .failure(let error) = CompressedRecordingInfo.validate(
@@ -159,40 +161,43 @@ struct RecordingSettings {
159
161
  }
160
162
  }
161
163
  }
162
-
164
+
163
165
  // Add extraction of new properties
164
166
  let deviceId = dict["deviceId"] as? String
165
167
  let deviceDisconnectionBehaviorStr = dict["deviceDisconnectionBehavior"] as? String
166
-
168
+
167
169
  // Create settings
168
170
  var settings = RecordingSettings(
169
171
  sampleRate: dict["sampleRate"] as? Double ?? 44100.0,
170
172
  desiredSampleRate: dict["desiredSampleRate"] as? Double ?? 44100.0,
171
173
  autoResumeAfterInterruption: dict["autoResumeAfterInterruption"] as? Bool ?? false
172
174
  )
173
-
175
+
174
176
  settings.output = outputSettings
175
-
177
+
176
178
  // Parse core settings
177
179
  settings.numberOfChannels = dict["channels"] as? Int ?? 1
178
180
  settings.bitDepth = dict["bitDepth"] as? Int ?? 16
179
181
  settings.interval = dict["interval"] as? Int
180
182
  settings.intervalAnalysis = dict["intervalAnalysis"] as? Int
181
-
183
+ if let maxDurationNumber = dict["maxDurationMs"] as? NSNumber {
184
+ settings.maxDurationMs = maxDurationNumber.int64Value
185
+ }
186
+ settings.autoStopOnMaxDuration = dict["autoStopOnMaxDuration"] as? Bool ?? false
182
187
  // Parse feature flags
183
188
  settings.keepAwake = dict["keepAwake"] as? Bool ?? true
184
189
  settings.showNotification = dict["showNotification"] as? Bool ?? false
185
190
  settings.enableProcessing = dict["enableProcessing"] as? Bool ?? false
186
-
191
+
187
192
  settings.featureOptions = dict["features"] as? [String: Bool]
188
-
193
+
189
194
  // Update segmentDurationMs parsing
190
195
  settings.segmentDurationMs = dict["segmentDurationMs"] as? Int ?? 100
191
-
196
+
192
197
  // Parse iOS-specific config
193
198
  if let iosDict = dict["ios"] as? [String: Any],
194
199
  let audioSessionDict = iosDict["audioSession"] as? [String: Any] {
195
-
200
+
196
201
  // Map category
197
202
  let category: AVAudioSession.Category
198
203
  if let categoryStr = audioSessionDict["category"] as? String {
@@ -208,7 +213,7 @@ struct RecordingSettings {
208
213
  } else {
209
214
  category = .record
210
215
  }
211
-
216
+
212
217
  // Map mode
213
218
  let mode: AVAudioSession.Mode
214
219
  if let modeStr = audioSessionDict["mode"] as? String {
@@ -226,7 +231,7 @@ struct RecordingSettings {
226
231
  } else {
227
232
  mode = .default
228
233
  }
229
-
234
+
230
235
  // Map category options
231
236
  var categoryOptions: AVAudioSession.CategoryOptions = []
232
237
  if let optionsArray = audioSessionDict["categoryOptions"] as? [String] {
@@ -243,63 +248,63 @@ struct RecordingSettings {
243
248
  }
244
249
  }
245
250
  }
246
-
251
+
247
252
  settings.ios = IOSConfig(audioSession: IOSAudioSessionConfig(
248
253
  category: category,
249
254
  mode: mode,
250
255
  categoryOptions: categoryOptions
251
256
  ))
252
257
  }
253
-
258
+
254
259
  // Parse notification config
255
260
  if let notificationDict = dict["notification"] as? [String: Any] {
256
261
  var notificationConfig = NotificationConfig()
257
262
  notificationConfig.title = notificationDict["title"] as? String
258
263
  notificationConfig.text = notificationDict["text"] as? String
259
264
  notificationConfig.icon = notificationDict["icon"] as? String
260
-
265
+
261
266
  // Parse iOS-specific notification config
262
267
  if let iosNotificationDict = notificationDict["ios"] as? [String: Any] {
263
268
  notificationConfig.ios = IOSNotificationConfig(
264
269
  categoryIdentifier: iosNotificationDict["categoryIdentifier"] as? String
265
270
  )
266
271
  }
267
-
272
+
268
273
  settings.notification = notificationConfig
269
274
  }
270
-
275
+
271
276
  // Parse output settings (they remain nil if not provided)
272
277
  if let directory = dict["outputDirectory"] as? String {
273
278
  // Only validate if a custom directory is provided
274
279
  let fileManager = FileManager.default
275
280
  var isDirectory: ObjCBool = false
276
-
281
+
277
282
  // Clean up the directory path by removing file:// protocol if present
278
283
  let cleanDirectory = directory.replacingOccurrences(of: "file://", with: "")
279
284
  .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
280
285
  .replacingOccurrences(of: "//", with: "/")
281
-
286
+
282
287
  if !fileManager.fileExists(atPath: cleanDirectory, isDirectory: &isDirectory) {
283
288
  return .failure(RecordingError.invalidOutputDirectory("Directory does not exist: \(cleanDirectory)"))
284
289
  }
285
-
290
+
286
291
  if !isDirectory.boolValue {
287
292
  return .failure(RecordingError.invalidOutputDirectory("Path is not a directory: \(cleanDirectory)"))
288
293
  }
289
-
294
+
290
295
  if !fileManager.isWritableFile(atPath: cleanDirectory) {
291
296
  return .failure(RecordingError.invalidOutputDirectory("Directory is not writable: \(cleanDirectory)"))
292
297
  }
293
-
298
+
294
299
  settings.outputDirectory = cleanDirectory
295
300
  }
296
-
301
+
297
302
  settings.filename = dict["filename"] as? String
298
-
303
+
299
304
  // Set new properties
300
305
  settings.deviceId = deviceId
301
306
  settings.deviceDisconnectionBehavior = DeviceDisconnectionBehavior(rawValue: deviceDisconnectionBehaviorStr ?? "fallback") ?? .FALLBACK
302
-
307
+
303
308
  if let bufferDuration = dict["bufferDurationSeconds"] as? Double {
304
309
  settings.bufferDurationSeconds = bufferDuration
305
310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/audio-studio",
3
- "version": "3.2.1-beta.0",
3
+ "version": "3.2.1-beta.2",
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
  "type": "commonjs",
@@ -4,7 +4,7 @@ import {
4
4
  AudioFeaturesOptions,
5
5
  DecodingConfig,
6
6
  } from './AudioAnalysis/AudioAnalysis.types'
7
- import { AudioAnalysisEvent } from './events'
7
+ import type { AudioAnalysisEvent } from './events'
8
8
 
9
9
  export interface CompressionInfo {
10
10
  /** Size of the compressed audio data in bytes */
@@ -36,6 +36,10 @@ export interface AudioStreamStatus {
36
36
  mimeType: string
37
37
  /** Information about audio compression if enabled */
38
38
  compression?: CompressionInfo
39
+ /** Configured maximum active recording duration in milliseconds, if enabled */
40
+ maxDurationMs?: number
41
+ /** Whether the current recording session has reached the configured maximum duration */
42
+ maxDurationReached?: boolean
39
43
  }
40
44
 
41
45
  interface AudioDataEventBase {
@@ -190,6 +194,21 @@ export interface StartRecordingResult {
190
194
  }
191
195
  }
192
196
 
197
+ export interface MaxDurationReachedEvent {
198
+ /** Active recording duration that triggered the event, in milliseconds */
199
+ durationMs: number
200
+ /** Configured active recording duration limit, in milliseconds */
201
+ maxDurationMs: number
202
+ /** Amount by which timer delivery exceeded the limit, in milliseconds */
203
+ overrunMs: number
204
+ /** Active stream identifier when available */
205
+ streamUuid?: string
206
+ /** Whether the recorder was configured to stop automatically after this event */
207
+ autoStopped: boolean
208
+ }
209
+
210
+ export type RecordingStopReason = 'manual' | 'maxDuration'
211
+
193
212
  export interface AudioSessionConfig {
194
213
  /**
195
214
  * Audio session category that defines the audio behavior
@@ -484,6 +503,43 @@ export interface RecordingConfig {
484
503
  /** Optional callback to handle recording interruptions */
485
504
  onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
486
505
 
506
+ /**
507
+ * Maximum cumulative active recording duration, in milliseconds.
508
+ *
509
+ * Paused time does not count. Set to undefined, 0, or a negative value to disable.
510
+ */
511
+ maxDurationMs?: number
512
+
513
+ /**
514
+ * Stop recording automatically when maxDurationMs is reached.
515
+ *
516
+ * Defaults to false. When used with `useAudioRecorder`, the
517
+ * MaxDurationReached event is emitted immediately, then the hook stops the
518
+ * recorder and exposes the final result through `onRecordingStopped`.
519
+ */
520
+ autoStopOnMaxDuration?: boolean
521
+
522
+ /**
523
+ * Optional callback invoked when maxDurationMs is reached.
524
+ *
525
+ * This remains an immediate threshold callback. If
526
+ * autoStopOnMaxDuration is true, use `onRecordingStopped` for the full
527
+ * recording result after stop completes.
528
+ */
529
+ onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
530
+
531
+ /**
532
+ * Optional callback invoked after a recording has fully stopped and the
533
+ * final `AudioRecording` result is available.
534
+ *
535
+ * The reason is `manual` when stopped through `stopRecording()` and
536
+ * `maxDuration` when stopped by `autoStopOnMaxDuration`.
537
+ */
538
+ onRecordingStopped?: (
539
+ recording: AudioRecording,
540
+ reason: RecordingStopReason
541
+ ) => void | Promise<void>
542
+
487
543
  /** Optional directory path where output files will be saved */
488
544
  outputDirectory?: string // If not provided, uses default app directory
489
545
  /** Optional filename for the recording (uses UUID if not provided) */
@@ -710,10 +766,23 @@ export interface UseAudioRecorderState {
710
766
  size: number // Size in bytes of the recorded audio
711
767
  /** Information about compression if enabled */
712
768
  compression?: CompressionInfo
769
+ /** Configured maximum active recording duration in milliseconds, if enabled */
770
+ maxDurationMs?: number
771
+ /** Whether the current recording session has reached the configured maximum duration */
772
+ maxDurationReached?: boolean
713
773
  /** Analysis data for the recording if processing was enabled */
714
774
  analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
775
+ /** Reason associated with the last completed recording */
776
+ lastRecordingReason?: RecordingStopReason
715
777
  /** Optional callback to handle recording interruptions */
716
778
  onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
779
+ /** Optional callback invoked when maxDurationMs is reached */
780
+ onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
781
+ /** Optional callback invoked when a recording fully stops */
782
+ onRecordingStopped?: (
783
+ recording: AudioRecording,
784
+ reason: RecordingStopReason
785
+ ) => void | Promise<void>
717
786
  }
718
787
 
719
788
  /**