@siteed/expo-audio-studio 2.12.3 → 2.13.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 +5 -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/AudioDeviceManager.js +225 -40
- package/build/cjs/AudioDeviceManager.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 +52 -8
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/AudioDeviceManager.js +225 -40
- package/build/esm/AudioDeviceManager.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 +53 -9
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioDeviceManager.d.ts +78 -2
- package/build/types/AudioDeviceManager.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/ios/AudioDeviceManager.swift +21 -9
- package/ios/ExpoAudioStreamModule.swift +33 -1
- package/package.json +8 -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/AudioDeviceManager.ts +286 -59
- package/src/hooks/useAudioDevices.ts +39 -6
- package/src/useAudioRecorder.tsx +102 -9
|
@@ -50,6 +50,26 @@ 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
|
+
* type: "deviceConnected" | "deviceDisconnected",
|
|
60
|
+
* deviceId: string
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* Recording Interruption Events (recordingInterruptedEvent):
|
|
64
|
+
* {
|
|
65
|
+
* reason: "userPaused" | "userResumed" | "audioFocusLoss" | "audioFocusGain" |
|
|
66
|
+
* "deviceFallback" | "deviceSwitchFailed" | "phoneCall" | "phoneCallEnded",
|
|
67
|
+
* isPaused: boolean,
|
|
68
|
+
* timestamp: number
|
|
69
|
+
* }
|
|
70
|
+
*
|
|
71
|
+
* NOTE: Device events use "type" field, interruption events use "reason" field.
|
|
72
|
+
* This is intentional to distinguish between different event categories.
|
|
53
73
|
*/
|
|
54
74
|
export class AudioDeviceManager {
|
|
55
75
|
private eventEmitter: InstanceType<typeof EventEmitter>
|
|
@@ -57,59 +77,76 @@ export class AudioDeviceManager {
|
|
|
57
77
|
private availableDevices: AudioDevice[] = []
|
|
58
78
|
private deviceChangeListeners: Set<(devices: AudioDevice[]) => void> =
|
|
59
79
|
new Set()
|
|
60
|
-
private
|
|
80
|
+
private webDeviceChangeHandler?: () => void
|
|
61
81
|
private lastRefreshTime: number = 0
|
|
62
82
|
private refreshInProgress: boolean = false
|
|
63
83
|
private refreshDebounceMs: number = 500 // Minimum 500ms between refreshes
|
|
64
84
|
private logger?: ConsoleLike
|
|
65
85
|
|
|
86
|
+
// Track temporarily disconnected devices
|
|
87
|
+
private temporarilyDisconnectedDevices: Set<string> = new Set()
|
|
88
|
+
private disconnectionTimeouts: Map<string, NodeJS.Timeout> = new Map()
|
|
89
|
+
private readonly DISCONNECTION_TIMEOUT_MS = 5000 // 5 seconds
|
|
90
|
+
|
|
66
91
|
constructor(options?: { logger?: ConsoleLike }) {
|
|
67
92
|
this.eventEmitter = new EventEmitter(ExpoAudioStreamModule)
|
|
68
93
|
this.logger = options?.logger
|
|
69
94
|
|
|
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
|
-
)
|
|
95
|
+
// Set up device event listeners for all platforms immediately
|
|
96
|
+
this.setupDeviceEventListeners()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set up device event listeners for the current platform
|
|
101
|
+
*/
|
|
102
|
+
private setupDeviceEventListeners(): void {
|
|
103
|
+
if (Platform.OS === 'web') {
|
|
104
|
+
this.setupWebDeviceChangeListener()
|
|
105
|
+
} else {
|
|
106
|
+
this.setupNativeDeviceEventListener()
|
|
110
107
|
}
|
|
111
108
|
}
|
|
112
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Set up native device event listener for iOS/Android
|
|
112
|
+
*/
|
|
113
|
+
private setupNativeDeviceEventListener(): void {
|
|
114
|
+
// Store the last event type to avoid duplicates
|
|
115
|
+
let lastEventType: string | null = null
|
|
116
|
+
let lastEventTime = 0
|
|
117
|
+
|
|
118
|
+
this.eventEmitter.addListener('deviceChangedEvent', (event: any) => {
|
|
119
|
+
// Skip processing duplicate events that occur too close together
|
|
120
|
+
const now = Date.now()
|
|
121
|
+
const isSimilarEvent =
|
|
122
|
+
lastEventType === event.type &&
|
|
123
|
+
now - lastEventTime < this.refreshDebounceMs
|
|
124
|
+
|
|
125
|
+
if (isSimilarEvent) {
|
|
126
|
+
this.logger?.debug(
|
|
127
|
+
`Skipping similar device event (${event.type}) received too soon`
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update the last event tracking
|
|
133
|
+
lastEventType = event.type
|
|
134
|
+
lastEventTime = now
|
|
135
|
+
|
|
136
|
+
// Only refresh on meaningful events
|
|
137
|
+
if (
|
|
138
|
+
event.type === 'deviceConnected' ||
|
|
139
|
+
event.type === 'deviceDisconnected' ||
|
|
140
|
+
event.type === 'routeChanged'
|
|
141
|
+
) {
|
|
142
|
+
this.logger?.debug(`Processing device event: ${event.type}`)
|
|
143
|
+
// Force refresh for device events to ensure fresh data
|
|
144
|
+
this.forceRefreshDevices()
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
this.logger?.debug('Native device event listener set up')
|
|
148
|
+
}
|
|
149
|
+
|
|
113
150
|
/**
|
|
114
151
|
* Initialize the device manager with a logger
|
|
115
152
|
* @param logger A logger instance that implements the ConsoleLike interface
|
|
@@ -128,6 +165,36 @@ export class AudioDeviceManager {
|
|
|
128
165
|
this.logger = logger
|
|
129
166
|
}
|
|
130
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Initialize or reinitialize device detection
|
|
170
|
+
* Useful for restarting device detection if initial setup failed
|
|
171
|
+
*/
|
|
172
|
+
initializeDeviceDetection(): void {
|
|
173
|
+
this.logger?.debug('Initializing device detection...')
|
|
174
|
+
|
|
175
|
+
// Clean up existing listeners first
|
|
176
|
+
if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
|
|
177
|
+
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
|
|
178
|
+
navigator.mediaDevices.removeEventListener(
|
|
179
|
+
'devicechange',
|
|
180
|
+
this.webDeviceChangeHandler
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
this.webDeviceChangeHandler = undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Re-setup device event listeners
|
|
187
|
+
this.setupDeviceEventListeners()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get the current logger instance
|
|
192
|
+
* @returns The logger instance or undefined if not set
|
|
193
|
+
*/
|
|
194
|
+
getLogger(): ConsoleLike | undefined {
|
|
195
|
+
return this.logger
|
|
196
|
+
}
|
|
197
|
+
|
|
131
198
|
/**
|
|
132
199
|
* Get all available audio input devices
|
|
133
200
|
* @param options Optional settings to force refresh the device list. Can include a refresh flag.
|
|
@@ -278,6 +345,152 @@ export class AudioDeviceManager {
|
|
|
278
345
|
}
|
|
279
346
|
}
|
|
280
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Mark a device as temporarily disconnected (for UI filtering)
|
|
350
|
+
* @param deviceId The ID of the device that was disconnected
|
|
351
|
+
* @param notify Whether to notify listeners immediately (default: true)
|
|
352
|
+
*/
|
|
353
|
+
markDeviceAsDisconnected(deviceId: string, notify: boolean = true): void {
|
|
354
|
+
this.logger?.debug(
|
|
355
|
+
`Marking device ${deviceId} as temporarily disconnected`
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
// Clear any existing timeout for this device
|
|
359
|
+
const existingTimeout = this.disconnectionTimeouts.get(deviceId)
|
|
360
|
+
if (existingTimeout) {
|
|
361
|
+
clearTimeout(existingTimeout)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Add to disconnected set
|
|
365
|
+
this.temporarilyDisconnectedDevices.add(deviceId)
|
|
366
|
+
|
|
367
|
+
// Set timeout to remove from disconnected set
|
|
368
|
+
const timeout = setTimeout(() => {
|
|
369
|
+
this.logger?.debug(
|
|
370
|
+
`Reconnection timeout expired for device ${deviceId}`
|
|
371
|
+
)
|
|
372
|
+
this.temporarilyDisconnectedDevices.delete(deviceId)
|
|
373
|
+
this.disconnectionTimeouts.delete(deviceId)
|
|
374
|
+
// Refresh devices to show the device again if it's still available
|
|
375
|
+
this.forceRefreshDevices()
|
|
376
|
+
}, this.DISCONNECTION_TIMEOUT_MS)
|
|
377
|
+
|
|
378
|
+
this.disconnectionTimeouts.set(deviceId, timeout)
|
|
379
|
+
|
|
380
|
+
// Only notify listeners if requested
|
|
381
|
+
if (notify) {
|
|
382
|
+
this.notifyListeners()
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Mark a device as reconnected (remove from disconnected set)
|
|
388
|
+
* @param deviceId The ID of the device that was reconnected
|
|
389
|
+
*/
|
|
390
|
+
markDeviceAsReconnected(deviceId: string): void {
|
|
391
|
+
this.logger?.debug(`Marking device ${deviceId} as reconnected`)
|
|
392
|
+
|
|
393
|
+
// Clear timeout and remove from disconnected set
|
|
394
|
+
const timeout = this.disconnectionTimeouts.get(deviceId)
|
|
395
|
+
if (timeout) {
|
|
396
|
+
clearTimeout(timeout)
|
|
397
|
+
this.disconnectionTimeouts.delete(deviceId)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.temporarilyDisconnectedDevices.delete(deviceId)
|
|
401
|
+
|
|
402
|
+
// Notify listeners with updated device list
|
|
403
|
+
this.notifyListeners()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get filtered device list (excluding temporarily disconnected devices)
|
|
408
|
+
* @returns Array of available devices excluding temporarily disconnected ones
|
|
409
|
+
*/
|
|
410
|
+
private getFilteredDevices(): AudioDevice[] {
|
|
411
|
+
if (this.temporarilyDisconnectedDevices.size === 0) {
|
|
412
|
+
return [...this.availableDevices]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const filtered = this.availableDevices.filter(
|
|
416
|
+
(device) => !this.temporarilyDisconnectedDevices.has(device.id)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
this.logger?.debug(
|
|
420
|
+
`Filtered ${this.availableDevices.length - filtered.length} temporarily disconnected devices. ` +
|
|
421
|
+
`Showing ${filtered.length} devices.`
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return filtered
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the raw device list (including temporarily disconnected devices)
|
|
429
|
+
* @returns Array of all available devices from native layer
|
|
430
|
+
*/
|
|
431
|
+
getRawDevices(): AudioDevice[] {
|
|
432
|
+
return [...this.availableDevices]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the IDs of temporarily disconnected devices
|
|
437
|
+
* @returns Set of device IDs that are temporarily hidden from UI
|
|
438
|
+
*/
|
|
439
|
+
getTemporarilyDisconnectedDeviceIds(): ReadonlySet<string> {
|
|
440
|
+
return new Set(this.temporarilyDisconnectedDevices)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Clean up timeouts and listeners (useful for testing or cleanup)
|
|
445
|
+
*/
|
|
446
|
+
cleanup(): void {
|
|
447
|
+
// Clear all disconnection timeouts
|
|
448
|
+
this.disconnectionTimeouts.forEach((timeout) => clearTimeout(timeout))
|
|
449
|
+
this.disconnectionTimeouts.clear()
|
|
450
|
+
this.temporarilyDisconnectedDevices.clear()
|
|
451
|
+
|
|
452
|
+
// Clear device change listeners
|
|
453
|
+
this.deviceChangeListeners.clear()
|
|
454
|
+
|
|
455
|
+
// Clean up web device listener
|
|
456
|
+
if (Platform.OS === 'web' && this.webDeviceChangeHandler) {
|
|
457
|
+
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
|
|
458
|
+
navigator.mediaDevices.removeEventListener(
|
|
459
|
+
'devicechange',
|
|
460
|
+
this.webDeviceChangeHandler
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
this.webDeviceChangeHandler = undefined
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.logger?.debug('AudioDeviceManager cleanup completed')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Force refresh devices without debouncing (for device events)
|
|
471
|
+
* @returns Promise resolving to the updated device list (AudioDevice[])
|
|
472
|
+
*/
|
|
473
|
+
async forceRefreshDevices(): Promise<AudioDevice[]> {
|
|
474
|
+
this.logger?.debug('Force refreshing devices (bypassing debounce)...')
|
|
475
|
+
this.refreshInProgress = true
|
|
476
|
+
try {
|
|
477
|
+
// Force fetch the latest devices from native layer
|
|
478
|
+
const devices = await this.getAvailableDevices({ refresh: true })
|
|
479
|
+
// Update internal state
|
|
480
|
+
this.availableDevices = devices
|
|
481
|
+
// Notify listeners with fresh data
|
|
482
|
+
this.notifyListeners()
|
|
483
|
+
this.lastRefreshTime = Date.now()
|
|
484
|
+
return devices
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.logger?.error('Error during forceRefreshDevices:', error)
|
|
487
|
+
return this.availableDevices
|
|
488
|
+
} finally {
|
|
489
|
+
this.refreshInProgress = false
|
|
490
|
+
this.logger?.debug('Force refresh finished.')
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
281
494
|
/**
|
|
282
495
|
* Refresh the list of available devices with debouncing and notify listeners.
|
|
283
496
|
* @returns Promise resolving to the updated device list (AudioDevice[])
|
|
@@ -385,7 +598,6 @@ export class AudioDeviceManager {
|
|
|
385
598
|
finalDevices = [DEFAULT_DEVICE]
|
|
386
599
|
}
|
|
387
600
|
|
|
388
|
-
this.setupWebDeviceChangeListener()
|
|
389
601
|
this.availableDevices = finalDevices // Update internal state
|
|
390
602
|
return finalDevices
|
|
391
603
|
} catch (error) {
|
|
@@ -421,27 +633,39 @@ export class AudioDeviceManager {
|
|
|
421
633
|
/**
|
|
422
634
|
* Setup listener for device changes in web environment
|
|
423
635
|
*/
|
|
424
|
-
private setupWebDeviceChangeListener() {
|
|
636
|
+
private setupWebDeviceChangeListener(): void {
|
|
425
637
|
if (
|
|
426
638
|
typeof navigator === 'undefined' ||
|
|
427
639
|
!navigator.mediaDevices ||
|
|
428
|
-
this.
|
|
640
|
+
this.webDeviceChangeHandler // Avoid adding multiple listeners
|
|
429
641
|
) {
|
|
642
|
+
this.logger?.debug(
|
|
643
|
+
'Web device change listener not available or already set up'
|
|
644
|
+
)
|
|
430
645
|
return
|
|
431
646
|
}
|
|
432
647
|
|
|
433
|
-
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
648
|
+
try {
|
|
649
|
+
this.webDeviceChangeHandler = () => {
|
|
650
|
+
this.logger?.debug(
|
|
651
|
+
'Web device change detected, refreshing device list'
|
|
652
|
+
)
|
|
653
|
+
// Force refresh to get immediate updates
|
|
654
|
+
this.forceRefreshDevices()
|
|
655
|
+
}
|
|
438
656
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
657
|
+
navigator.mediaDevices.addEventListener(
|
|
658
|
+
'devicechange',
|
|
659
|
+
this.webDeviceChangeHandler
|
|
660
|
+
)
|
|
661
|
+
this.logger?.debug('Web device change listener successfully set up')
|
|
662
|
+
} catch (error) {
|
|
663
|
+
this.logger?.warn(
|
|
664
|
+
'Failed to set up web device change listener:',
|
|
665
|
+
error
|
|
666
|
+
)
|
|
667
|
+
this.webDeviceChangeHandler = undefined
|
|
668
|
+
}
|
|
445
669
|
}
|
|
446
670
|
|
|
447
671
|
/**
|
|
@@ -549,12 +773,15 @@ export class AudioDeviceManager {
|
|
|
549
773
|
/**
|
|
550
774
|
* Notify all registered listeners about device changes.
|
|
551
775
|
*/
|
|
552
|
-
|
|
553
|
-
// Pass a copy of the
|
|
554
|
-
const devicesCopy =
|
|
776
|
+
notifyListeners(): void {
|
|
777
|
+
// Pass a copy of the filtered devices array to listeners
|
|
778
|
+
const devicesCopy = this.getFilteredDevices()
|
|
779
|
+
|
|
555
780
|
this.logger?.debug(
|
|
556
|
-
`Notifying ${this.deviceChangeListeners.size} listeners with ${devicesCopy.length} devices
|
|
781
|
+
`Notifying ${this.deviceChangeListeners.size} listeners with ${devicesCopy.length} devices ` +
|
|
782
|
+
`(${this.temporarilyDisconnectedDevices.size} temporarily hidden)`
|
|
557
783
|
)
|
|
784
|
+
|
|
558
785
|
this.deviceChangeListeners.forEach((listener) => {
|
|
559
786
|
try {
|
|
560
787
|
listener(devicesCopy)
|
|
@@ -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
|
}
|