mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-24 17:27:02 +00:00
d723c14d84
On Chrome and Windows 10, when the camera or microphone is already in use by another application, the browser rejects `getUserMedia`. Without dedicated handling, users just saw their camera or microphone not turning on, without any explanation. Detect this specific failure and surface a clear message to the user, so they understand another application is holding the device and know what to do about it.
152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
import { getPosthog } from './utils'
|
|
|
|
export const captureEvent = (
|
|
event: string,
|
|
props?: Record<string, unknown>
|
|
) => {
|
|
void getPosthog()
|
|
.then((ph) => {
|
|
ph.capture(event, props)
|
|
})
|
|
.catch(() => {
|
|
/* telemetry must never break the app */
|
|
})
|
|
if (import.meta.env.DEV) {
|
|
console.warn(`[telemetry] ${event}`, props)
|
|
}
|
|
}
|
|
|
|
export type LogCode =
|
|
// media
|
|
| 'join_preview_failure'
|
|
| 'room_media_failure'
|
|
| 'livekit_room_error'
|
|
| 'device_switch_failure'
|
|
| 'permission_poll_failure'
|
|
| 'media_devices_error_event'
|
|
// non-media families
|
|
| 'participant_mute_api_failure'
|
|
| 'permissions_api_failure'
|
|
| 'effects_processor_failure'
|
|
| 'clipboard_failure'
|
|
| 'fullscreen_failure'
|
|
| 'publish_sources_failure'
|
|
| 'disconnect_failure'
|
|
| 'generic_failure'
|
|
|
|
export const reportError = (
|
|
logCode: LogCode,
|
|
error: unknown,
|
|
extraInfo: Record<string, unknown> = {}
|
|
): void => {
|
|
const e = error instanceof Error ? error : new Error(String(error))
|
|
void getPosthog()
|
|
.then((ph) => {
|
|
ph.captureException(e, {
|
|
log_code: logCode,
|
|
error_name: e.name,
|
|
error_message: e.message,
|
|
...extraInfo,
|
|
})
|
|
})
|
|
.catch(() => {})
|
|
if (import.meta.env.DEV) {
|
|
console.warn(`[${logCode}]`, e, extraInfo)
|
|
}
|
|
}
|
|
|
|
export interface DeviceSnapshot {
|
|
cam_count: number
|
|
mic_count: number
|
|
out_count: number
|
|
labels_visible: boolean
|
|
saved_cam_present: boolean | null
|
|
saved_mic_present: boolean | null
|
|
saved_video_device_id_set: boolean
|
|
saved_audio_device_id_set: boolean
|
|
audio_enabled: boolean | null
|
|
video_enabled: boolean | null
|
|
cam_permission: PermissionState | 'unknown'
|
|
mic_permission: PermissionState | 'unknown'
|
|
}
|
|
|
|
/** Reads the persisted LiveKit user choices without importing the store. */
|
|
const readPersistedChoices = (): {
|
|
videoDeviceId?: string
|
|
audioDeviceId?: string
|
|
videoEnabled?: boolean
|
|
audioEnabled?: boolean
|
|
} => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('lk-user-choices') ?? '{}')
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
const queryPermission = async (
|
|
name: 'camera' | 'microphone'
|
|
): Promise<PermissionState | 'unknown'> => {
|
|
try {
|
|
const status = await navigator.permissions.query({
|
|
name: name as PermissionName,
|
|
})
|
|
return status.state
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
export const deviceSnapshot = async (): Promise<DeviceSnapshot> => {
|
|
const choices = readPersistedChoices()
|
|
let devices: MediaDeviceInfo[] = []
|
|
try {
|
|
devices = await navigator.mediaDevices.enumerateDevices()
|
|
} catch {
|
|
/* snapshot stays partial */
|
|
}
|
|
const ofKind = (k: MediaDeviceKind) => devices.filter((d) => d.kind === k)
|
|
const present = (k: MediaDeviceKind, id?: string) =>
|
|
id ? ofKind(k).some((d) => d.deviceId === id) : null
|
|
|
|
const [cam_permission, mic_permission] = await Promise.all([
|
|
queryPermission('camera'),
|
|
queryPermission('microphone'),
|
|
])
|
|
|
|
return {
|
|
cam_count: ofKind('videoinput').length,
|
|
mic_count: ofKind('audioinput').length,
|
|
out_count: ofKind('audiooutput').length,
|
|
labels_visible: devices.some((d) => !!d.label),
|
|
saved_cam_present: present('videoinput', choices.videoDeviceId),
|
|
saved_mic_present: present('audioinput', choices.audioDeviceId),
|
|
saved_video_device_id_set: !!choices.videoDeviceId,
|
|
saved_audio_device_id_set: !!choices.audioDeviceId,
|
|
audio_enabled: choices.audioEnabled ?? null,
|
|
video_enabled: choices.videoEnabled ?? null,
|
|
cam_permission,
|
|
mic_permission,
|
|
}
|
|
}
|
|
|
|
export const captureMediaEvent = async (
|
|
event:
|
|
| 'media-device-error'
|
|
| 'media-acquisition'
|
|
| 'media-device-topology'
|
|
| 'media-device-success'
|
|
| 'device-not-found'
|
|
| 'device-in-use'
|
|
| 'permissions-denied'
|
|
| 'screen-share-permission-denied'
|
|
| 'silent-mic-detected'
|
|
| 'silent-mic-analyser-unavailable'
|
|
| 'silent-mic-recovered'
|
|
| 'visit-room'
|
|
| 'connection-event',
|
|
props: Record<string, unknown>
|
|
) => {
|
|
captureEvent(event, { ...props, ...(await deviceSnapshot()) })
|
|
}
|