@siteed/expo-audio-studio 2.12.3 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/android/build.gradle +11 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
- package/app.plugin.js +3 -1
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioDeviceManager.js +229 -40
- package/build/cjs/AudioDeviceManager.js.map +1 -1
- package/build/cjs/WebRecorder.web.js +1 -0
- package/build/cjs/WebRecorder.web.js.map +1 -1
- package/build/cjs/hooks/useAudioDevices.js +30 -5
- package/build/cjs/hooks/useAudioDevices.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +53 -8
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/workers/InlineFeaturesExtractor.web.js +8 -2
- package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioDeviceManager.js +229 -40
- package/build/esm/AudioDeviceManager.js.map +1 -1
- package/build/esm/WebRecorder.web.js +1 -0
- package/build/esm/WebRecorder.web.js.map +1 -1
- package/build/esm/hooks/useAudioDevices.js +31 -6
- package/build/esm/hooks/useAudioDevices.js.map +1 -1
- package/build/esm/useAudioRecorder.js +54 -9
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/workers/InlineFeaturesExtractor.web.js +8 -2
- package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +1 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioDeviceManager.d.ts +82 -2
- package/build/types/AudioDeviceManager.d.ts.map +1 -1
- package/build/types/WebRecorder.web.d.ts.map +1 -1
- package/build/types/hooks/useAudioDevices.d.ts +1 -0
- package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/ios/AudioDeviceManager.swift +21 -9
- package/ios/ExpoAudioStreamModule.swift +33 -1
- package/package.json +7 -6
- package/plugin/build/index.cjs +194 -0
- package/plugin/build/index.d.cts +1 -0
- package/plugin/build/index.js +7 -6
- package/plugin/src/index.ts +8 -8
- package/src/AudioAnalysis/AudioAnalysis.types.ts +1 -0
- package/src/AudioDeviceManager.ts +290 -59
- package/src/WebRecorder.web.ts +1 -0
- package/src/hooks/useAudioDevices.ts +39 -6
- package/src/useAudioRecorder.tsx +103 -9
- package/src/workers/InlineFeaturesExtractor.web.tsx +8 -2
|
@@ -50,6 +50,30 @@ function mapRawDeviceToAudioDevice(rawDevice: any): AudioDevice {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Class that provides a cross-platform API for managing audio input devices
|
|
53
|
+
*
|
|
54
|
+
* EVENT API SPECIFICATION:
|
|
55
|
+
* ========================
|
|
56
|
+
*
|
|
57
|
+
* Device Events (deviceChangedEvent):
|
|
58
|
+
* ```
|
|
59
|
+
* {
|
|
60
|
+
* type: "deviceConnected" | "deviceDisconnected",
|
|
61
|
+
* deviceId: string
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Recording Interruption Events (recordingInterruptedEvent):
|
|
66
|
+
* ```
|
|
67
|
+
* {
|
|
68
|
+
* reason: "userPaused" | "userResumed" | "audioFocusLoss" | "audioFocusGain" |
|
|
69
|
+
* "deviceFallback" | "deviceSwitchFailed" | "phoneCall" | "phoneCallEnded",
|
|
70
|
+
* isPaused: boolean,
|
|
71
|
+
* timestamp: number
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* NOTE: Device events use "type" field, interruption events use "reason" field.
|
|
76
|
+
* This is intentional to distinguish between different event categories.
|
|
53
77
|
*/
|
|
54
78
|
export class AudioDeviceManager {
|
|
55
79
|
private eventEmitter: InstanceType<typeof EventEmitter>
|
|
@@ -57,59 +81,76 @@ export class AudioDeviceManager {
|
|
|
57
81
|
private availableDevices: AudioDevice[] = []
|
|
58
82
|
private deviceChangeListeners: Set<(devices: AudioDevice[]) => void> =
|
|
59
83
|
new Set()
|
|
60
|
-
private
|
|
84
|
+
private webDeviceChangeHandler?: () => void
|
|
61
85
|
private lastRefreshTime: number = 0
|
|
62
86
|
private refreshInProgress: boolean = false
|
|
63
87
|
private refreshDebounceMs: number = 500 // Minimum 500ms between refreshes
|
|
64
88
|
private logger?: ConsoleLike
|
|
65
89
|
|
|
90
|
+
// Track temporarily disconnected devices
|
|
91
|
+
private temporarilyDisconnectedDevices: Set<string> = new Set()
|
|
92
|
+
private disconnectionTimeouts: Map<string, NodeJS.Timeout> = new Map()
|
|
93
|
+
private readonly DISCONNECTION_TIMEOUT_MS = 5000 // 5 seconds
|
|
94
|
+
|
|
66
95
|
constructor(options?: { logger?: ConsoleLike }) {
|
|
67
96
|
this.eventEmitter = new EventEmitter(ExpoAudioStreamModule)
|
|
68
97
|
this.logger = options?.logger
|
|
69
98
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
lastEventType === event.type &&
|
|
83
|
-
now - lastEventTime < this.refreshDebounceMs
|
|
84
|
-
|
|
85
|
-
if (isSimilarEvent) {
|
|
86
|
-
this.logger?.debug(
|
|
87
|
-
`Skipping similar device event (${event.type}) received too soon`
|
|
88
|
-
)
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Update the last event tracking
|
|
93
|
-
lastEventType = event.type
|
|
94
|
-
lastEventTime = now
|
|
95
|
-
|
|
96
|
-
// Only refresh on meaningful events
|
|
97
|
-
if (
|
|
98
|
-
event.type === 'deviceConnected' ||
|
|
99
|
-
event.type === 'deviceDisconnected' ||
|
|
100
|
-
event.type === 'routeChanged'
|
|
101
|
-
) {
|
|
102
|
-
this.logger?.debug(
|
|
103
|
-
`Processing device event: ${event.type}`
|
|
104
|
-
)
|
|
105
|
-
// Refresh devices and notify listeners regardless of the direct return value
|
|
106
|
-
this.refreshDevices()
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
)
|
|
99
|
+
// Set up device event listeners for all platforms immediately
|
|
100
|
+
this.setupDeviceEventListeners()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set up device event listeners for the current platform
|
|
105
|
+
*/
|
|
106
|
+
private setupDeviceEventListeners(): void {
|
|
107
|
+
if (Platform.OS === 'web') {
|
|
108
|
+
this.setupWebDeviceChangeListener()
|
|
109
|
+
} else {
|
|
110
|
+
this.setupNativeDeviceEventListener()
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Set up native device event listener for iOS/Android
|
|
116
|
+
*/
|
|
117
|
+
private setupNativeDeviceEventListener(): void {
|
|
118
|
+
// Store the last event type to avoid duplicates
|
|
119
|
+
let lastEventType: string | null = null
|
|
120
|
+
let lastEventTime = 0
|
|
121
|
+
|
|
122
|
+
this.eventEmitter.addListener('deviceChangedEvent', (event: any) => {
|
|
123
|
+
// Skip processing duplicate events that occur too close together
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
const isSimilarEvent =
|
|
126
|
+
lastEventType === event.type &&
|
|
127
|
+
now - lastEventTime < this.refreshDebounceMs
|
|
128
|
+
|
|
129
|
+
if (isSimilarEvent) {
|
|
130
|
+
this.logger?.debug(
|
|
131
|
+
`Skipping similar device event (${event.type}) received too soon`
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Update the last event tracking
|
|
137
|
+
lastEventType = event.type
|
|
138
|
+
lastEventTime = now
|
|
139
|
+
|
|
140
|
+
// Only refresh on meaningful events
|
|
141
|
+
if (
|
|
142
|
+
event.type === 'deviceConnected' ||
|
|
143
|
+
event.type === 'deviceDisconnected' ||
|
|
144
|
+
event.type === 'routeChanged'
|
|
145
|
+
) {
|
|
146
|
+
this.logger?.debug(`Processing device event: ${event.type}`)
|
|
147
|
+
// Force refresh for device events to ensure fresh data
|
|
148
|
+
this.forceRefreshDevices()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
this.logger?.debug('Native device event listener set up')
|
|
152
|
+
}
|
|
153
|
+
|
|
113
154
|
/**
|
|
114
155
|
* Initialize the device manager with a logger
|
|
115
156
|
* @param logger A logger instance that implements the ConsoleLike interface
|
|
@@ -128,6 +169,36 @@ export class AudioDeviceManager {
|
|
|
128
169
|
this.logger = logger
|
|
129
170
|
}
|
|
130
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Initialize or reinitialize device detection
|
|
174
|
+
* Useful for restarting device detection if initial setup failed
|
|
175
|
+
*/
|
|
176
|
+
initializeDeviceDetection(): void {
|
|
177
|
+
this.logger?.debug('Initializing device detection...')
|
|
178
|
+
|
|
179
|
+
// Clean up existing listeners first
|
|
180
|
+
if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
|
|
181
|
+
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
|
|
182
|
+
navigator.mediaDevices.removeEventListener(
|
|
183
|
+
'devicechange',
|
|
184
|
+
this.webDeviceChangeHandler
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
this.webDeviceChangeHandler = undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Re-setup device event listeners
|
|
191
|
+
this.setupDeviceEventListeners()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the current logger instance
|
|
196
|
+
* @returns The logger instance or undefined if not set
|
|
197
|
+
*/
|
|
198
|
+
getLogger(): ConsoleLike | undefined {
|
|
199
|
+
return this.logger
|
|
200
|
+
}
|
|
201
|
+
|
|
131
202
|
/**
|
|
132
203
|
* Get all available audio input devices
|
|
133
204
|
* @param options Optional settings to force refresh the device list. Can include a refresh flag.
|
|
@@ -278,6 +349,152 @@ export class AudioDeviceManager {
|
|
|
278
349
|
}
|
|
279
350
|
}
|
|
280
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Mark a device as temporarily disconnected (for UI filtering)
|
|
354
|
+
* @param deviceId The ID of the device that was disconnected
|
|
355
|
+
* @param notify Whether to notify listeners immediately (default: true)
|
|
356
|
+
*/
|
|
357
|
+
markDeviceAsDisconnected(deviceId: string, notify: boolean = true): void {
|
|
358
|
+
this.logger?.debug(
|
|
359
|
+
`Marking device ${deviceId} as temporarily disconnected`
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// Clear any existing timeout for this device
|
|
363
|
+
const existingTimeout = this.disconnectionTimeouts.get(deviceId)
|
|
364
|
+
if (existingTimeout) {
|
|
365
|
+
clearTimeout(existingTimeout)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Add to disconnected set
|
|
369
|
+
this.temporarilyDisconnectedDevices.add(deviceId)
|
|
370
|
+
|
|
371
|
+
// Set timeout to remove from disconnected set
|
|
372
|
+
const timeout = setTimeout(() => {
|
|
373
|
+
this.logger?.debug(
|
|
374
|
+
`Reconnection timeout expired for device ${deviceId}`
|
|
375
|
+
)
|
|
376
|
+
this.temporarilyDisconnectedDevices.delete(deviceId)
|
|
377
|
+
this.disconnectionTimeouts.delete(deviceId)
|
|
378
|
+
// Refresh devices to show the device again if it's still available
|
|
379
|
+
this.forceRefreshDevices()
|
|
380
|
+
}, this.DISCONNECTION_TIMEOUT_MS)
|
|
381
|
+
|
|
382
|
+
this.disconnectionTimeouts.set(deviceId, timeout)
|
|
383
|
+
|
|
384
|
+
// Only notify listeners if requested
|
|
385
|
+
if (notify) {
|
|
386
|
+
this.notifyListeners()
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Mark a device as reconnected (remove from disconnected set)
|
|
392
|
+
* @param deviceId The ID of the device that was reconnected
|
|
393
|
+
*/
|
|
394
|
+
markDeviceAsReconnected(deviceId: string): void {
|
|
395
|
+
this.logger?.debug(`Marking device ${deviceId} as reconnected`)
|
|
396
|
+
|
|
397
|
+
// Clear timeout and remove from disconnected set
|
|
398
|
+
const timeout = this.disconnectionTimeouts.get(deviceId)
|
|
399
|
+
if (timeout) {
|
|
400
|
+
clearTimeout(timeout)
|
|
401
|
+
this.disconnectionTimeouts.delete(deviceId)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.temporarilyDisconnectedDevices.delete(deviceId)
|
|
405
|
+
|
|
406
|
+
// Notify listeners with updated device list
|
|
407
|
+
this.notifyListeners()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get filtered device list (excluding temporarily disconnected devices)
|
|
412
|
+
* @returns Array of available devices excluding temporarily disconnected ones
|
|
413
|
+
*/
|
|
414
|
+
private getFilteredDevices(): AudioDevice[] {
|
|
415
|
+
if (this.temporarilyDisconnectedDevices.size === 0) {
|
|
416
|
+
return [...this.availableDevices]
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const filtered = this.availableDevices.filter(
|
|
420
|
+
(device) => !this.temporarilyDisconnectedDevices.has(device.id)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
this.logger?.debug(
|
|
424
|
+
`Filtered ${this.availableDevices.length - filtered.length} temporarily disconnected devices. ` +
|
|
425
|
+
`Showing ${filtered.length} devices.`
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return filtered
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get the raw device list (including temporarily disconnected devices)
|
|
433
|
+
* @returns Array of all available devices from native layer
|
|
434
|
+
*/
|
|
435
|
+
getRawDevices(): AudioDevice[] {
|
|
436
|
+
return [...this.availableDevices]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get the IDs of temporarily disconnected devices
|
|
441
|
+
* @returns Set of device IDs that are temporarily hidden from UI
|
|
442
|
+
*/
|
|
443
|
+
getTemporarilyDisconnectedDeviceIds(): ReadonlySet<string> {
|
|
444
|
+
return new Set(this.temporarilyDisconnectedDevices)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Clean up timeouts and listeners (useful for testing or cleanup)
|
|
449
|
+
*/
|
|
450
|
+
cleanup(): void {
|
|
451
|
+
// Clear all disconnection timeouts
|
|
452
|
+
this.disconnectionTimeouts.forEach((timeout) => clearTimeout(timeout))
|
|
453
|
+
this.disconnectionTimeouts.clear()
|
|
454
|
+
this.temporarilyDisconnectedDevices.clear()
|
|
455
|
+
|
|
456
|
+
// Clear device change listeners
|
|
457
|
+
this.deviceChangeListeners.clear()
|
|
458
|
+
|
|
459
|
+
// Clean up web device listener
|
|
460
|
+
if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
|
|
461
|
+
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
|
|
462
|
+
navigator.mediaDevices.removeEventListener(
|
|
463
|
+
'devicechange',
|
|
464
|
+
this.webDeviceChangeHandler
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
this.webDeviceChangeHandler = undefined
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.logger?.debug('AudioDeviceManager cleanup completed')
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Force refresh devices without debouncing (for device events)
|
|
475
|
+
* @returns Promise resolving to the updated device list (AudioDevice[])
|
|
476
|
+
*/
|
|
477
|
+
async forceRefreshDevices(): Promise<AudioDevice[]> {
|
|
478
|
+
this.logger?.debug('Force refreshing devices (bypassing debounce)...')
|
|
479
|
+
this.refreshInProgress = true
|
|
480
|
+
try {
|
|
481
|
+
// Force fetch the latest devices from native layer
|
|
482
|
+
const devices = await this.getAvailableDevices({ refresh: true })
|
|
483
|
+
// Update internal state
|
|
484
|
+
this.availableDevices = devices
|
|
485
|
+
// Notify listeners with fresh data
|
|
486
|
+
this.notifyListeners()
|
|
487
|
+
this.lastRefreshTime = Date.now()
|
|
488
|
+
return devices
|
|
489
|
+
} catch (error) {
|
|
490
|
+
this.logger?.error('Error during forceRefreshDevices:', error)
|
|
491
|
+
return this.availableDevices
|
|
492
|
+
} finally {
|
|
493
|
+
this.refreshInProgress = false
|
|
494
|
+
this.logger?.debug('Force refresh finished.')
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
281
498
|
/**
|
|
282
499
|
* Refresh the list of available devices with debouncing and notify listeners.
|
|
283
500
|
* @returns Promise resolving to the updated device list (AudioDevice[])
|
|
@@ -385,7 +602,6 @@ export class AudioDeviceManager {
|
|
|
385
602
|
finalDevices = [DEFAULT_DEVICE]
|
|
386
603
|
}
|
|
387
604
|
|
|
388
|
-
this.setupWebDeviceChangeListener()
|
|
389
605
|
this.availableDevices = finalDevices // Update internal state
|
|
390
606
|
return finalDevices
|
|
391
607
|
} catch (error) {
|
|
@@ -421,27 +637,39 @@ export class AudioDeviceManager {
|
|
|
421
637
|
/**
|
|
422
638
|
* Setup listener for device changes in web environment
|
|
423
639
|
*/
|
|
424
|
-
private setupWebDeviceChangeListener() {
|
|
640
|
+
private setupWebDeviceChangeListener(): void {
|
|
425
641
|
if (
|
|
426
642
|
typeof navigator === 'undefined' ||
|
|
427
643
|
!navigator.mediaDevices ||
|
|
428
|
-
this.
|
|
644
|
+
this.webDeviceChangeHandler // Avoid adding multiple listeners
|
|
429
645
|
) {
|
|
646
|
+
this.logger?.debug(
|
|
647
|
+
'Web device change listener not available or already set up'
|
|
648
|
+
)
|
|
430
649
|
return
|
|
431
650
|
}
|
|
432
651
|
|
|
433
|
-
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
652
|
+
try {
|
|
653
|
+
this.webDeviceChangeHandler = () => {
|
|
654
|
+
this.logger?.debug(
|
|
655
|
+
'Web device change detected, refreshing device list'
|
|
656
|
+
)
|
|
657
|
+
// Force refresh to get immediate updates
|
|
658
|
+
this.forceRefreshDevices()
|
|
659
|
+
}
|
|
438
660
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
661
|
+
navigator.mediaDevices.addEventListener(
|
|
662
|
+
'devicechange',
|
|
663
|
+
this.webDeviceChangeHandler
|
|
664
|
+
)
|
|
665
|
+
this.logger?.debug('Web device change listener successfully set up')
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.logger?.warn(
|
|
668
|
+
'Failed to set up web device change listener:',
|
|
669
|
+
error
|
|
670
|
+
)
|
|
671
|
+
this.webDeviceChangeHandler = undefined
|
|
672
|
+
}
|
|
445
673
|
}
|
|
446
674
|
|
|
447
675
|
/**
|
|
@@ -549,12 +777,15 @@ export class AudioDeviceManager {
|
|
|
549
777
|
/**
|
|
550
778
|
* Notify all registered listeners about device changes.
|
|
551
779
|
*/
|
|
552
|
-
|
|
553
|
-
// Pass a copy of the
|
|
554
|
-
const devicesCopy =
|
|
780
|
+
notifyListeners(): void {
|
|
781
|
+
// Pass a copy of the filtered devices array to listeners
|
|
782
|
+
const devicesCopy = this.getFilteredDevices()
|
|
783
|
+
|
|
555
784
|
this.logger?.debug(
|
|
556
|
-
`Notifying ${this.deviceChangeListeners.size} listeners with ${devicesCopy.length} devices
|
|
785
|
+
`Notifying ${this.deviceChangeListeners.size} listeners with ${devicesCopy.length} devices ` +
|
|
786
|
+
`(${this.temporarilyDisconnectedDevices.size} temporarily hidden)`
|
|
557
787
|
)
|
|
788
|
+
|
|
558
789
|
this.deviceChangeListeners.forEach((listener) => {
|
|
559
790
|
try {
|
|
560
791
|
listener(devicesCopy)
|
package/src/WebRecorder.web.ts
CHANGED
|
@@ -147,6 +147,7 @@ export class WebRecorder {
|
|
|
147
147
|
sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
|
|
148
148
|
segmentDurationMs:
|
|
149
149
|
this.config.segmentDurationMs ?? DEFAULT_SEGMENT_DURATION_MS, // Default to 100ms segments
|
|
150
|
+
extractionTimeMs: 0,
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
if (recordingConfig.enableProcessing) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useState, useId } from 'react'
|
|
2
2
|
|
|
3
3
|
import { audioDeviceManager } from '../AudioDeviceManager'
|
|
4
4
|
import { AudioDevice } from '../ExpoAudioStream.types'
|
|
@@ -12,6 +12,9 @@ export function useAudioDevices() {
|
|
|
12
12
|
const [loading, setLoading] = useState(true)
|
|
13
13
|
const [error, setError] = useState<Error | null>(null)
|
|
14
14
|
|
|
15
|
+
// Generate unique instance ID for debugging
|
|
16
|
+
const instanceId = useId().replace(/:/g, '').slice(0, 5)
|
|
17
|
+
|
|
15
18
|
// Load devices on mount
|
|
16
19
|
useEffect(() => {
|
|
17
20
|
let isMounted = true
|
|
@@ -30,7 +33,9 @@ export function useAudioDevices() {
|
|
|
30
33
|
const device = await audioDeviceManager.getCurrentDevice()
|
|
31
34
|
if (isMounted) setCurrentDevice(device)
|
|
32
35
|
} catch (err) {
|
|
33
|
-
|
|
36
|
+
audioDeviceManager
|
|
37
|
+
.getLogger()
|
|
38
|
+
?.error('Failed to load audio devices:', err)
|
|
34
39
|
if (isMounted)
|
|
35
40
|
setError(
|
|
36
41
|
err instanceof Error
|
|
@@ -47,6 +52,12 @@ export function useAudioDevices() {
|
|
|
47
52
|
// Set up device change listener
|
|
48
53
|
const removeListener = audioDeviceManager.addDeviceChangeListener(
|
|
49
54
|
(updatedDevices: AudioDevice[]) => {
|
|
55
|
+
audioDeviceManager
|
|
56
|
+
.getLogger()
|
|
57
|
+
?.debug(
|
|
58
|
+
`🎛️ useAudioDevices [${instanceId}] received device change. Count: ${updatedDevices.length}`
|
|
59
|
+
)
|
|
60
|
+
|
|
50
61
|
if (isMounted) {
|
|
51
62
|
setDevices(updatedDevices)
|
|
52
63
|
|
|
@@ -57,10 +68,17 @@ export function useAudioDevices() {
|
|
|
57
68
|
(d: AudioDevice) => d.id === currentDevice.id
|
|
58
69
|
)
|
|
59
70
|
) {
|
|
71
|
+
audioDeviceManager
|
|
72
|
+
.getLogger()
|
|
73
|
+
?.debug(
|
|
74
|
+
`🎛️ useAudioDevices [${instanceId}] Current device ${currentDevice.id} no longer available, updating`
|
|
75
|
+
)
|
|
60
76
|
audioDeviceManager
|
|
61
77
|
.getCurrentDevice()
|
|
62
78
|
.then((newDevice: AudioDevice | null) => {
|
|
63
|
-
if (isMounted)
|
|
79
|
+
if (isMounted) {
|
|
80
|
+
setCurrentDevice(newDevice)
|
|
81
|
+
}
|
|
64
82
|
})
|
|
65
83
|
}
|
|
66
84
|
}
|
|
@@ -94,7 +112,9 @@ export function useAudioDevices() {
|
|
|
94
112
|
|
|
95
113
|
return success
|
|
96
114
|
} catch (err) {
|
|
97
|
-
|
|
115
|
+
audioDeviceManager
|
|
116
|
+
.getLogger()
|
|
117
|
+
?.error('Failed to select audio device:', err)
|
|
98
118
|
setError(
|
|
99
119
|
err instanceof Error
|
|
100
120
|
? err
|
|
@@ -127,7 +147,9 @@ export function useAudioDevices() {
|
|
|
127
147
|
|
|
128
148
|
return success
|
|
129
149
|
} catch (err) {
|
|
130
|
-
|
|
150
|
+
audioDeviceManager
|
|
151
|
+
.getLogger()
|
|
152
|
+
?.error('Failed to reset to default audio device:', err)
|
|
131
153
|
setError(
|
|
132
154
|
err instanceof Error
|
|
133
155
|
? err
|
|
@@ -156,7 +178,9 @@ export function useAudioDevices() {
|
|
|
156
178
|
|
|
157
179
|
return updatedDevices
|
|
158
180
|
} catch (err) {
|
|
159
|
-
|
|
181
|
+
audioDeviceManager
|
|
182
|
+
.getLogger()
|
|
183
|
+
?.error('Failed to refresh audio devices:', err)
|
|
160
184
|
setError(
|
|
161
185
|
err instanceof Error
|
|
162
186
|
? err
|
|
@@ -168,6 +192,14 @@ export function useAudioDevices() {
|
|
|
168
192
|
}
|
|
169
193
|
}, [])
|
|
170
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Initialize device detection
|
|
197
|
+
* Useful for restarting device detection if it failed initially
|
|
198
|
+
*/
|
|
199
|
+
const initializeDeviceDetection = useCallback(() => {
|
|
200
|
+
audioDeviceManager.initializeDeviceDetection()
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
171
203
|
return {
|
|
172
204
|
devices,
|
|
173
205
|
currentDevice,
|
|
@@ -176,5 +208,6 @@ export function useAudioDevices() {
|
|
|
176
208
|
selectDevice,
|
|
177
209
|
resetToDefaultDevice,
|
|
178
210
|
refreshDevices,
|
|
211
|
+
initializeDeviceDetection,
|
|
179
212
|
}
|
|
180
213
|
}
|