@siteed/audio-studio 3.2.1-beta.0 → 3.2.1-beta.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/README.md +30 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +130 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1 -0
- package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
- package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/AudioStudio.web.js +125 -13
- package/build/cjs/AudioStudio.web.js.map +1 -1
- package/build/cjs/AudioStudioModule.js +6 -1
- package/build/cjs/AudioStudioModule.js.map +1 -1
- package/build/cjs/events.js +4 -0
- package/build/cjs/events.js.map +1 -1
- package/build/cjs/index.js +3 -1
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +139 -4
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/AudioStudio.types.js.map +1 -1
- package/build/esm/AudioStudio.web.js +125 -13
- package/build/esm/AudioStudio.web.js.map +1 -1
- package/build/esm/AudioStudioModule.js +6 -1
- package/build/esm/AudioStudioModule.js.map +1 -1
- package/build/esm/events.js +3 -0
- package/build/esm/events.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/useAudioRecorder.js +140 -5
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioStudio.types.d.ts +44 -1
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/AudioStudio.web.d.ts +17 -1
- package/build/types/AudioStudio.web.d.ts.map +1 -1
- package/build/types/AudioStudioModule.d.ts.map +1 -1
- package/build/types/events.d.ts +2 -1
- package/build/types/events.d.ts.map +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts +2 -0
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +103 -9
- package/ios/AudioStreamManagerDelegate.swift +1 -0
- package/ios/AudioStudioModule.swift +6 -0
- package/ios/RecordingSettings.swift +48 -43
- package/package.json +1 -1
- package/src/AudioStudio.types.ts +48 -1
- package/src/AudioStudio.web.ts +152 -13
- package/src/AudioStudioModule.ts +6 -1
- package/src/events.ts +13 -1
- package/src/index.ts +1 -0
- package/src/useAudioRecorder.tsx +182 -2
|
@@ -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.
|
|
3
|
+
"version": "3.2.1-beta.1",
|
|
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",
|
package/src/AudioStudio.types.ts
CHANGED
|
@@ -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,19 @@ 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
|
+
|
|
193
210
|
export interface AudioSessionConfig {
|
|
194
211
|
/**
|
|
195
212
|
* Audio session category that defines the audio behavior
|
|
@@ -484,6 +501,30 @@ export interface RecordingConfig {
|
|
|
484
501
|
/** Optional callback to handle recording interruptions */
|
|
485
502
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
486
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Maximum cumulative active recording duration, in milliseconds.
|
|
506
|
+
*
|
|
507
|
+
* Paused time does not count. Set to undefined, 0, or a negative value to disable.
|
|
508
|
+
*/
|
|
509
|
+
maxDurationMs?: number
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Stop recording automatically when maxDurationMs is reached.
|
|
513
|
+
*
|
|
514
|
+
* Defaults to false. The MaxDurationReached event is emitted before the stop request.
|
|
515
|
+
* The automatic stop result is not returned to onMaxDurationReached; use the
|
|
516
|
+
* event and stream callbacks for immediate UI updates.
|
|
517
|
+
*/
|
|
518
|
+
autoStopOnMaxDuration?: boolean
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Optional callback invoked when maxDurationMs is reached.
|
|
522
|
+
*
|
|
523
|
+
* If autoStopOnMaxDuration is true, this callback is invoked before the
|
|
524
|
+
* recorder finishes stopping. The final stop result is not passed here.
|
|
525
|
+
*/
|
|
526
|
+
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
527
|
+
|
|
487
528
|
/** Optional directory path where output files will be saved */
|
|
488
529
|
outputDirectory?: string // If not provided, uses default app directory
|
|
489
530
|
/** Optional filename for the recording (uses UUID if not provided) */
|
|
@@ -710,10 +751,16 @@ export interface UseAudioRecorderState {
|
|
|
710
751
|
size: number // Size in bytes of the recorded audio
|
|
711
752
|
/** Information about compression if enabled */
|
|
712
753
|
compression?: CompressionInfo
|
|
754
|
+
/** Configured maximum active recording duration in milliseconds, if enabled */
|
|
755
|
+
maxDurationMs?: number
|
|
756
|
+
/** Whether the current recording session has reached the configured maximum duration */
|
|
757
|
+
maxDurationReached?: boolean
|
|
713
758
|
/** Analysis data for the recording if processing was enabled */
|
|
714
759
|
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
715
760
|
/** Optional callback to handle recording interruptions */
|
|
716
761
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
762
|
+
/** Optional callback invoked when maxDurationMs is reached */
|
|
763
|
+
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
717
764
|
}
|
|
718
765
|
|
|
719
766
|
/**
|
package/src/AudioStudio.web.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface AudioStudioWebProps {
|
|
|
52
52
|
audioWorkletUrl: string
|
|
53
53
|
featuresExtratorUrl: string
|
|
54
54
|
maxBufferSize?: number // Maximum number of chunks to keep in memory
|
|
55
|
+
emitEvent?: (eventName: string, params: unknown) => void
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export class AudioStudioWeb extends LegacyEventEmitter {
|
|
@@ -80,12 +81,19 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
80
81
|
totalCompressedSize: number = 0
|
|
81
82
|
private readonly maxBufferSize: number
|
|
82
83
|
private eventCallback?: (event: AudioStreamEvent) => void
|
|
84
|
+
private readonly moduleEventEmitter?: (eventName: string, params: unknown) => void
|
|
85
|
+
private maxDurationTimer?: ReturnType<typeof setTimeout>
|
|
86
|
+
private maxDurationTargetMs = 0
|
|
87
|
+
private maxDurationAccumulatedActiveMs = 0
|
|
88
|
+
private maxDurationSegmentStartMs = 0
|
|
89
|
+
private maxDurationReached = false
|
|
83
90
|
|
|
84
91
|
constructor({
|
|
85
92
|
audioWorkletUrl,
|
|
86
93
|
featuresExtratorUrl,
|
|
87
94
|
logger,
|
|
88
95
|
maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
|
|
96
|
+
emitEvent,
|
|
89
97
|
}: AudioStudioWebProps) {
|
|
90
98
|
const mockNativeModule = {
|
|
91
99
|
addListener: () => {},
|
|
@@ -114,6 +122,125 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
114
122
|
this.audioWorkletUrl = audioWorkletUrl
|
|
115
123
|
this.featuresExtratorUrl = featuresExtratorUrl
|
|
116
124
|
this.maxBufferSize = maxBufferSize
|
|
125
|
+
this.moduleEventEmitter = emitEvent
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private emitModuleEvent(eventName: string, params: unknown) {
|
|
129
|
+
this.emit(eventName, params)
|
|
130
|
+
this.moduleEventEmitter?.(eventName, params)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private resetMaxDurationState(preserveReached = false) {
|
|
134
|
+
if (this.maxDurationTimer) {
|
|
135
|
+
clearTimeout(this.maxDurationTimer)
|
|
136
|
+
this.maxDurationTimer = undefined
|
|
137
|
+
}
|
|
138
|
+
this.maxDurationSegmentStartMs = 0
|
|
139
|
+
if (!preserveReached || !this.maxDurationReached) {
|
|
140
|
+
this.maxDurationTargetMs = 0
|
|
141
|
+
this.maxDurationAccumulatedActiveMs = 0
|
|
142
|
+
this.maxDurationReached = false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private getMaxDurationActiveMs(now = performance.now()) {
|
|
147
|
+
if (this.maxDurationSegmentStartMs <= 0) {
|
|
148
|
+
return this.maxDurationAccumulatedActiveMs
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
this.maxDurationAccumulatedActiveMs +
|
|
152
|
+
(now - this.maxDurationSegmentStartMs)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private scheduleMaxDurationTimer() {
|
|
157
|
+
if (
|
|
158
|
+
this.maxDurationTargetMs <= 0 ||
|
|
159
|
+
this.maxDurationReached ||
|
|
160
|
+
!this.isRecording ||
|
|
161
|
+
this.isPaused
|
|
162
|
+
) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.maxDurationTimer) {
|
|
167
|
+
clearTimeout(this.maxDurationTimer)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const remainingMs = Math.max(
|
|
171
|
+
0,
|
|
172
|
+
this.maxDurationTargetMs - this.getMaxDurationActiveMs()
|
|
173
|
+
)
|
|
174
|
+
this.maxDurationTimer = setTimeout(() => {
|
|
175
|
+
this.maxDurationTimer = undefined
|
|
176
|
+
this.emitMaxDurationReached()
|
|
177
|
+
}, remainingMs)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private startMaxDurationTimer(recordingConfig: RecordingConfig) {
|
|
181
|
+
this.resetMaxDurationState()
|
|
182
|
+
const targetMs = Number(recordingConfig.maxDurationMs ?? 0)
|
|
183
|
+
if (!Number.isFinite(targetMs) || targetMs <= 0) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.maxDurationTargetMs = targetMs
|
|
188
|
+
this.maxDurationSegmentStartMs = performance.now()
|
|
189
|
+
this.scheduleMaxDurationTimer()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private pauseMaxDurationTimer() {
|
|
193
|
+
if (this.maxDurationTimer) {
|
|
194
|
+
clearTimeout(this.maxDurationTimer)
|
|
195
|
+
this.maxDurationTimer = undefined
|
|
196
|
+
}
|
|
197
|
+
if (this.maxDurationSegmentStartMs > 0) {
|
|
198
|
+
this.maxDurationAccumulatedActiveMs = this.getMaxDurationActiveMs()
|
|
199
|
+
this.maxDurationSegmentStartMs = 0
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private resumeMaxDurationTimer() {
|
|
204
|
+
if (this.maxDurationTargetMs <= 0 || this.maxDurationReached) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
this.maxDurationSegmentStartMs = performance.now()
|
|
208
|
+
this.scheduleMaxDurationTimer()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private emitMaxDurationReached() {
|
|
212
|
+
if (this.maxDurationTargetMs <= 0 || this.maxDurationReached) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const durationMs = Math.round(this.getMaxDurationActiveMs())
|
|
217
|
+
this.maxDurationReached = true
|
|
218
|
+
const autoStopped = !!this.recordingConfig?.autoStopOnMaxDuration
|
|
219
|
+
this.emitModuleEvent('MaxDurationReached', {
|
|
220
|
+
durationMs,
|
|
221
|
+
maxDurationMs: this.maxDurationTargetMs,
|
|
222
|
+
overrunMs: Math.max(0, durationMs - this.maxDurationTargetMs),
|
|
223
|
+
streamUuid: this.streamUuid ?? undefined,
|
|
224
|
+
autoStopped,
|
|
225
|
+
})
|
|
226
|
+
if (autoStopped) {
|
|
227
|
+
this.stopRecording().catch((error) => {
|
|
228
|
+
this.logger?.error(
|
|
229
|
+
'Error auto-stopping on max duration:',
|
|
230
|
+
error
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private flushExpiredMaxDuration() {
|
|
237
|
+
if (
|
|
238
|
+
this.maxDurationTargetMs > 0 &&
|
|
239
|
+
!this.maxDurationReached &&
|
|
240
|
+
this.getMaxDurationActiveMs() >= this.maxDurationTargetMs
|
|
241
|
+
) {
|
|
242
|
+
this.emitMaxDurationReached()
|
|
243
|
+
}
|
|
117
244
|
}
|
|
118
245
|
|
|
119
246
|
// Utility to handle user media stream
|
|
@@ -275,6 +402,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
275
402
|
this.currentInterval = recordingConfig.interval ?? 1000
|
|
276
403
|
this.currentIntervalAnalysis = recordingConfig.intervalAnalysis ?? 500
|
|
277
404
|
this.lastEmittedAnalysisTime = Date.now()
|
|
405
|
+
this.startMaxDurationTimer(recordingConfig)
|
|
278
406
|
|
|
279
407
|
// Use custom filename if provided, otherwise fallback to timestamp
|
|
280
408
|
if (recordingConfig.filename) {
|
|
@@ -321,11 +449,11 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
321
449
|
// Update local state if the interruption should pause recording
|
|
322
450
|
if (event.isPaused) {
|
|
323
451
|
this.isPaused = true
|
|
452
|
+
this.pausedTime = Date.now()
|
|
453
|
+
this.pauseMaxDurationTimer()
|
|
324
454
|
|
|
325
455
|
// If this is a device disconnection, handle according to behavior setting
|
|
326
456
|
if (event.reason === 'deviceDisconnected') {
|
|
327
|
-
this.pausedTime = Date.now()
|
|
328
|
-
|
|
329
457
|
// Check if we should try fallback to another device
|
|
330
458
|
if (
|
|
331
459
|
this.recordingConfig?.deviceDisconnectionBehavior ===
|
|
@@ -339,7 +467,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
339
467
|
this.handleDeviceFallback().catch((error) => {
|
|
340
468
|
// If fallback fails, emit warning
|
|
341
469
|
this.logger?.error('Device fallback failed:', error)
|
|
342
|
-
this.
|
|
470
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
343
471
|
reason: 'deviceSwitchFailed',
|
|
344
472
|
isPaused: true,
|
|
345
473
|
timestamp: Date.now(),
|
|
@@ -352,15 +480,15 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
352
480
|
this.logger?.warn(
|
|
353
481
|
'Device disconnected - recording paused automatically'
|
|
354
482
|
)
|
|
355
|
-
this.
|
|
483
|
+
this.emitModuleEvent('onRecordingInterrupted', event)
|
|
356
484
|
}
|
|
357
485
|
} else {
|
|
358
486
|
// For other interruption types, just emit the event
|
|
359
|
-
this.
|
|
487
|
+
this.emitModuleEvent('onRecordingInterrupted', event)
|
|
360
488
|
}
|
|
361
489
|
} else {
|
|
362
490
|
// If not causing a pause, just forward the event
|
|
363
|
-
this.
|
|
491
|
+
this.emitModuleEvent('onRecordingInterrupted', event)
|
|
364
492
|
}
|
|
365
493
|
}
|
|
366
494
|
|
|
@@ -390,7 +518,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
390
518
|
private customRecorderAnalysisCallback(
|
|
391
519
|
audioAnalysisData: AudioAnalysis
|
|
392
520
|
): void {
|
|
393
|
-
this.
|
|
521
|
+
this.emitModuleEvent('AudioAnalysis', audioAnalysisData)
|
|
394
522
|
}
|
|
395
523
|
|
|
396
524
|
// Get recording duration
|
|
@@ -425,6 +553,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
425
553
|
} else {
|
|
426
554
|
this.currentDurationMs += chunkDurationMs
|
|
427
555
|
}
|
|
556
|
+
this.flushExpiredMaxDuration()
|
|
428
557
|
|
|
429
558
|
const audioEventPayload: AudioEventPayload = {
|
|
430
559
|
fileUri,
|
|
@@ -445,7 +574,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
445
574
|
: undefined,
|
|
446
575
|
}
|
|
447
576
|
|
|
448
|
-
this.
|
|
577
|
+
this.emitModuleEvent('AudioData', audioEventPayload)
|
|
449
578
|
}
|
|
450
579
|
|
|
451
580
|
// Stop recording
|
|
@@ -457,6 +586,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
457
586
|
this.logger?.debug('Starting stop process')
|
|
458
587
|
|
|
459
588
|
try {
|
|
589
|
+
this.pauseMaxDurationTimer()
|
|
460
590
|
const { compressedBlob, uncompressedBlob } =
|
|
461
591
|
await this.customRecorder.stop()
|
|
462
592
|
|
|
@@ -540,6 +670,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
540
670
|
this.totalCompressedSize = 0
|
|
541
671
|
this.lastEmittedCompressionSize = 0
|
|
542
672
|
this.audioChunks = []
|
|
673
|
+
this.resetMaxDurationState(true)
|
|
543
674
|
|
|
544
675
|
return result
|
|
545
676
|
} catch (error) {
|
|
@@ -565,11 +696,13 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
565
696
|
}
|
|
566
697
|
this.isPaused = true
|
|
567
698
|
this.pausedTime = Date.now()
|
|
699
|
+
this.pauseMaxDurationTimer()
|
|
568
700
|
} catch (error) {
|
|
569
701
|
this.logger?.error('Error in pauseRecording', error)
|
|
570
702
|
// Even if the pause operation failed, make sure our state is consistent
|
|
571
703
|
this.isPaused = true
|
|
572
704
|
this.pausedTime = Date.now()
|
|
705
|
+
this.pauseMaxDurationTimer()
|
|
573
706
|
}
|
|
574
707
|
}
|
|
575
708
|
|
|
@@ -607,8 +740,9 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
607
740
|
const pauseDuration = Date.now() - this.pausedTime
|
|
608
741
|
this.recordingStartTime += pauseDuration
|
|
609
742
|
this.pausedTime = 0
|
|
743
|
+
this.resumeMaxDurationTimer()
|
|
610
744
|
|
|
611
|
-
this.
|
|
745
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
612
746
|
reason: 'userResumed',
|
|
613
747
|
isPaused: false,
|
|
614
748
|
timestamp: Date.now(),
|
|
@@ -616,7 +750,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
616
750
|
} catch (error) {
|
|
617
751
|
this.logger?.error('Resume failed:', error)
|
|
618
752
|
// Fallback to emitting a general failure if resume fails unexpectedly
|
|
619
|
-
this.
|
|
753
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
620
754
|
reason: 'resumeFailed', // Use a more specific reason
|
|
621
755
|
isPaused: true, // Remain paused if resume fails
|
|
622
756
|
timestamp: Date.now(),
|
|
@@ -628,6 +762,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
628
762
|
|
|
629
763
|
// Get current status
|
|
630
764
|
status() {
|
|
765
|
+
this.flushExpiredMaxDuration()
|
|
631
766
|
const durationMs = this.getRecordingDuration()
|
|
632
767
|
|
|
633
768
|
const status: AudioStreamStatus = {
|
|
@@ -651,6 +786,9 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
651
786
|
compressedFileUri: `${this.streamUuid}.webm`,
|
|
652
787
|
}
|
|
653
788
|
: undefined,
|
|
789
|
+
maxDurationMs:
|
|
790
|
+
this.maxDurationTargetMs > 0 ? this.maxDurationTargetMs : undefined,
|
|
791
|
+
maxDurationReached: this.maxDurationReached,
|
|
654
792
|
}
|
|
655
793
|
return status
|
|
656
794
|
}
|
|
@@ -708,7 +846,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
708
846
|
// Try to get a fallback device
|
|
709
847
|
const fallbackDeviceInfo = await this.getFallbackDevice()
|
|
710
848
|
if (!fallbackDeviceInfo) {
|
|
711
|
-
this.
|
|
849
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
712
850
|
reason: 'deviceSwitchFailed',
|
|
713
851
|
isPaused: true,
|
|
714
852
|
timestamp: Date.now(),
|
|
@@ -774,6 +912,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
774
912
|
// Update recording state
|
|
775
913
|
this.isPaused = false
|
|
776
914
|
this.recordingStartTime = Date.now()
|
|
915
|
+
this.resumeMaxDurationTimer()
|
|
777
916
|
|
|
778
917
|
// Restore size counters to maintain continuity
|
|
779
918
|
this.currentSize = previousTotalSize
|
|
@@ -795,7 +934,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
795
934
|
error
|
|
796
935
|
)
|
|
797
936
|
this.isPaused = true
|
|
798
|
-
this.
|
|
937
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
799
938
|
reason: 'deviceSwitchFailed',
|
|
800
939
|
isPaused: true,
|
|
801
940
|
timestamp: Date.now(),
|
|
@@ -807,7 +946,7 @@ export class AudioStudioWeb extends LegacyEventEmitter {
|
|
|
807
946
|
} catch (error) {
|
|
808
947
|
this.logger?.error('Failed to use fallback device', error)
|
|
809
948
|
this.isPaused = true
|
|
810
|
-
this.
|
|
949
|
+
this.emitModuleEvent('onRecordingInterrupted', {
|
|
811
950
|
reason: 'deviceSwitchFailed',
|
|
812
951
|
isPaused: true,
|
|
813
952
|
timestamp: Date.now(),
|