This document describes the comprehensive device resilience system implemented for the Cantina WebRTC application. The system handles device disconnections/reconnections (like AirPods), OS default device changes, and device persistence across sessions.
WebRTC applications face several device-related challenges:
File: react/src/common/redux/interfaces.ts
export interface DevicePreference {
isDefault: boolean // User selected OS default
preferredLabel?: string // Device name for recovery
lastSeenId?: string // Last known device ID
}
Purpose: Store user intent separately from current device state. This allows recovery based on what the user wanted, not just what's currently selected.
File: react/src/common/redux/features/device/deviceSlice.ts
Key additions:
cameraPreference
, microphonePreference
, speakerPreference
- Store user preferencesdefaultCameraActualId
, defaultMicrophoneActualId
, defaultSpeakerActualId
- Track actual device when "Default" is selectedBehavior:
File: react/src/common/services/devicePreferenceManager.ts
Core service that handles device recovery logic:
async recoverDevice(
deviceType: 'camera' | 'microphone' | 'speaker',
currentId: string,
preference?: DevicePreference
): Promise<DeviceRecoveryResult>
Recovery Priority:
Key Features:
File: react/src/common/redux/features/device/deviceMonitorSaga.ts
Saga that continuously monitors for device changes:
function* handleDeviceChange(devices: MediaDeviceInfo[]): SagaIterator {
// 2-second delay for device stabilization (phone wake)
yield delay(2000)
// Validate each device type
// Recover if needed
// Update Redux state
// Notify user of changes
}
Features:
File: react/src/common/redux/features/calls/callSaga.ts
Enhanced unmute actions to revalidate devices:
case restoreLocalAudioTrack.type: {
// Revalidate microphone before unmuting
const validatedDevice = yield call(
checkMicrophone,
devices.microphoneId,
devices.microphoneLabel,
microphonePreference
)
// Update if device changed
if (validatedDevice && validatedDevice.deviceId !== devices.microphoneId) {
yield put(deviceActions.microphoneChanged({
deviceId: validatedDevice.deviceId,
label: validatedDevice.label
}))
// Update constraints with new device
}
}
Purpose: Ensures device is still valid when user unmutes, preventing errors.
File: react/src/common/services/checkDevices.ts
Added preference support to device validation:
export const checkMicrophone = async (
microphoneId: string,
microphoneLabel?: string,
preference?: DevicePreference
): Promise<DeviceInfo | null>
Behavior:
For each device type:
Get current selection and preference
const deviceByLabel = devices.find(d =>
d.label === preference.preferredLabel &&
d.deviceId !== MediaDevice.Default &&
d.deviceId !== MediaDevice.None
)
This finds devices by name when IDs change.
if (cameraId === MediaDevice.Default) {
const actualDevice = cameras.find(d =>
d.deviceId !== MediaDevice.Default &&
d.deviceId !== MediaDevice.None
)
// Track actual device for change detection
}
yield delay(2000) // Wait for devices to stabilize
Prevents errors when devices are still initializing.
if (result.recovered) {
yield call(toastInfo, `Camera switched to: ${result.deviceLabel}`)
} else if (result.fallbackUsed) {
yield call(toastInfo, `Camera disconnected. Using default.`)
}
Keeps users informed of automatic changes.
To implement similar resilience:
This approach works for any WebRTC application or system dealing with dynamic hardware devices.