Panel ajustes dispositivos cam/mic/altavoz en MeetingRoom
Contexto y motivación
Tras el primer despliegue de la funcionalidad de videollamadas (s79), Edu reportó que no existía botón de configuración y que Windows seleccionaba automáticamente la cámara del smartphone conectado vía Phone Link en lugar de la webcam física. Esta feature introduce un panel de ajustes completo para seleccionar y cambiar dispositivos de captura/reproducción en caliente.
PR: #62 — 2026-05-22 — @Esquembri
Funcionalidades añadidas
Botón “Ajustes” en la barra de controles
Añadido junto a “Compartir pantalla” y el botón de colgar. Toggle — alterna entre Ajustes / Cerrar ajustes.
Panel overlay de dispositivos
Panel flotante (position: absolute, bottom: 56px, right: 8px) que aparece sobre el grid de vídeo cuando settingsOpen === true. Contiene tres <select> con los dispositivos detectados:
| Dropdown | Fuente | Persistencia |
|---|---|---|
| Cámara | MediaDeviceInfo[] kind videoinput | localStorage['meeting:camId'] |
| Micrófono | MediaDeviceInfo[] kind audioinput | localStorage['meeting:micId'] |
| Altavoz | MediaDeviceInfo[] kind audiooutput | localStorage['meeting:speakerId'] |
Cambio de dispositivo en vivo (sin colgar)
Cada cambio invoca una función apply*Change(deviceId) que:
- Abre un nuevo
getUserMediacondeviceId: { exact: X }. - Obtiene el nuevo track (video o audio).
- Reemplaza en todos los peers activos:
RTCRtpSender.replaceTrack(newTrack)— sin necesidad de renegociación. - Actualiza
originalStreamRef(para pantalla compartida y cleanup correcto). - Persiste el
deviceIdenlocalStorage.
// Ejemplo simplificado de applyCameraChange
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } },
audio: false,
});
for (const peer of peersMapRef.current.values()) {
const sender = peer.pc.getSenders().find(s => s.track?.kind === 'video');
if (sender) await sender.replaceTrack(newStream.getVideoTracks()[0]);
}
localStorage.setItem('meeting:camId', deviceId);
Selección de altavoz (setSinkId)
const videos = document.querySelectorAll<HTMLVideoElement>('video');
for (const v of Array.from(videos)) {
if (typeof v.setSinkId === 'function') await v.setSinkId(deviceId);
}
⚠️ Solo funciona en Chrome y Edge. Firefox y Safari no implementan
setSinkId— la UI lo advierte explícitamente.
Fallback robusto al iniciar
Si el deviceId guardado en localStorage ya no existe (p.ej. cámara USB desconectada entre sesiones), getUserMedia falla con ConstraintNotSatisfiedError. El código captura esta excepción y reintenta sin constraint (device por defecto):
try {
stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraint, audio: audioConstraint });
} catch (constraintErr) {
console.warn('[meeting] device constraint failed, fallback default', constraintErr);
stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
}
Listener devicechange
navigator.mediaDevices.addEventListener('devicechange', refreshDevices);
// Cleanup en return del useEffect:
navigator.mediaDevices.removeEventListener('devicechange', refreshDevices);
Refresca automáticamente los tres arrays de dispositivos si el usuario conecta o desconecta hardware durante una reunión activa.
Flujo de inicialización con selección persistida
mount
└─ useEffect (init)
├─ Lee localStorage (camId, micId)
├─ getUserMedia({ video: { exact: camId }, audio: { exact: micId } })
│ └─ [fallback si constraint falla] getUserMedia({ video: true, audio: true })
├─ refreshDevices() → setCameras / setMicrophones / setSpeakers
└─ addEventListener('devicechange', refreshDevices)
Limitaciones conocidas
| Limitación | Descripción |
|---|---|
setSinkId solo Chromium | Firefox y Safari ignoran la selección de altavoz |
| Screen share no rota | Al compartir pantalla, applyCameraChange no cambia el stream local (pantalla tiene prioridad visual) — se aplica igualmente al stream remoto |
| Labels vacíos sin permisos | enumerateDevices devuelve labels vacíos hasta que getUserMedia ha sido llamado — por eso refreshDevices se llama tras obtener el stream |
Test plan (post-deploy)
- Abrir reunión → click “Ajustes” → aparece panel
- Cambiar cámara → imagen cambia en vivo sin reconexión
- Cambiar micrófono → audio sigue funcionando
- Cerrar reunión y reabrir → dropdown muestra último dispositivo seleccionado
- Desconectar cámara USB, abrir reunión → fallback a default sin error visible
- Conectar nuevo micrófono durante reunión → aparece en el dropdown automáticamente
Véase también
- [[entity—meeting—component—meeting-room]]