Volver a la wiki

Panel ajustes dispositivos cam/mic/altavoz en MeetingRoom

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:

DropdownFuentePersistencia
CámaraMediaDeviceInfo[] kind videoinputlocalStorage['meeting:camId']
MicrófonoMediaDeviceInfo[] kind audioinputlocalStorage['meeting:micId']
AltavozMediaDeviceInfo[] kind audiooutputlocalStorage['meeting:speakerId']

Cambio de dispositivo en vivo (sin colgar)

Cada cambio invoca una función apply*Change(deviceId) que:

  1. Abre un nuevo getUserMedia con deviceId: { exact: X }.
  2. Obtiene el nuevo track (video o audio).
  3. Reemplaza en todos los peers activos: RTCRtpSender.replaceTrack(newTrack) — sin necesidad de renegociación.
  4. Actualiza originalStreamRef (para pantalla compartida y cleanup correcto).
  5. Persiste el deviceId en localStorage.
// 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ónDescripción
setSinkId solo ChromiumFirefox y Safari ignoran la selección de altavoz
Screen share no rotaAl compartir pantalla, applyCameraChange no cambia el stream local (pantalla tiene prioridad visual) — se aplica igualmente al stream remoto
Labels vacíos sin permisosenumerateDevices devuelve labels vacíos hasta que getUserMedia ha sido llamado — por eso refreshDevices se llama tras obtener el stream

Test plan (post-deploy)

Véase también

Subir