CreaRack-SL

Panel de ajustes de dispositivos en MeetingRoom (cám/mic/altavoz)

Panel de ajustes de dispositivos en MeetingRoom (cám/mic/altavoz)

Contexto y motivación

Edu reportó que tras el primer deploy WebRTC, Windows elegía automáticamente la cámara del smartphone conectado vía Phone Link en lugar de la webcam física. Además, no existía ningún botón de configuración en la interfaz para corregirlo manualmente.

El commit 1b8a61e (s79 · 2026-05-22) introduce un panel completo de selección de dispositivos audiovisuales que resuelve este problema y añade persistencia entre sesiones.


Funcionalidades añadidas

Botón «Ajustes» en la barra de controles

Se añade un botón tools-btn junto a «Compartir pantalla» y «Colgar» que activa/desactiva el panel overlay de ajustes.

<button type="button" onClick={() => setSettingsOpen((s) => !s)} className="tools-btn">
  {settingsOpen ? 'Cerrar ajustes' : 'Ajustes'}
</button>

Panel overlay con tres dropdowns

El panel se renderiza como position: absolute anclado a la esquina inferior-derecha del modal (right: 8, bottom: 56). Contiene tres selectores:

SelectorlocalStorage keyConstraint aplicado
Cámara (videoinput)meeting:camIdvideo: { deviceId: { exact } }
Micrófono (audioinput)meeting:micIdaudio: { deviceId: { exact } }
Altavoz (audiooutput)meeting:speakerIdHTMLVideoElement.setSinkId()

Flujo técnico

1. Inicialización con persistencia

Al montar el componente, los tres estados se inicializan leyendo localStorage:

const [selectedCamId, setSelectedCamId] = useState<string>(
  () => localStorage.getItem('meeting:camId') || '',
);

Si hay un ID guardado, getUserMedia arranca directamente con ese deviceId: { exact }. Si el dispositivo ya no existe (p.ej. USB desconectado), hay un fallback automático a getUserMedia({ video: true, audio: true }) sin constraint:

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 });
}

2. Enumeración de dispositivos

refreshDevices() llama a navigator.mediaDevices.enumerateDevices() para poblar las listas de cámaras, micrófonos y altavoces. Se invoca:

  • Justo después de getUserMedia exitoso (las labels solo están pobladas tras obtener permisos).
  • Cada vez que el sistema operativo emite el evento devicechange (USB conectado/desconectado durante la reunión).

El listener se registra con addEventListener('devicechange', refreshDevices) y se limpia en el cleanup del useEffect.

3. Cambio de cámara en vivo (applyCameraChange)

getUserMedia({ video: { deviceId: { exact: id } }, audio: false })
  → nuevo VideoTrack
  → RTCRtpSender.replaceTrack() en cada peer (sin colgar)
  → removeTrack / addTrack en originalStreamRef
  → setLocalStream() para refrescar el preview local
  → localStorage.setItem('meeting:camId', id)

Si screenSharing está activo, el preview local no se actualiza (la pantalla sigue como fuente visual de la transmisión).

4. Cambio de micrófono en vivo (applyMicChange)

Mismo patrón que la cámara pero con audioinput:

getUserMedia({ audio: { deviceId: { exact: id } }, video: false })
  → nuevo AudioTrack
  → RTCRtpSender.replaceTrack() en cada peer
  → removeTrack / addTrack en originalStreamRef
  → localStorage.setItem('meeting:micId', id)

5. Cambio de altavoz (applySpeakerChange)

A diferencia de cámara/mic, el altavoz no requiere un nuevo stream: se aplica setSinkId(deviceId) sobre todos los elementos <video> del DOM. Esto incluye los tiles remotos.

const videos = document.querySelectorAll<HTMLVideoElement>('video');
for (const v of Array.from(videos)) {
  if (typeof v.setSinkId === 'function') {
    await v.setSinkId(deviceId);
  }
}

Compatibilidad: setSinkId solo está soportado en Chromium (Chrome, Edge). Firefox y Safari ignoran la selección y usan el dispositivo de salida del sistema. La UI muestra una nota informativa al respecto.


Estado del componente (nuevos useState)

EstadoTipoPropósito
settingsOpenbooleanVisibilidad del panel overlay
camerasMediaDeviceInfo[]Lista de cámaras disponibles
microphonesMediaDeviceInfo[]Lista de micrófonos disponibles
speakersMediaDeviceInfo[]Lista de altavoces disponibles
selectedCamIdstringID persistido / seleccionado de cámara
selectedMicIdstringID persistido / seleccionado de micrófono
selectedSpeakerIdstringID persistido / seleccionado de altavoz

Casos de borde cubiertos

  • Dispositivo desconectado entre sesiones: fallback silencioso a default, sin error visible al usuario.
  • Phone Link / cámaras virtuales: el selector muestra todas las videoinput enumeradas; el usuario puede elegir explícitamente su webcam física.
  • Dispositivos reconectados en sesión activa: devicechange event refresca los dropdowns automáticamente.
  • Screen sharing activo al cambiar cámara: replaceTrack actualiza el track enviado a peers, pero el preview local mantiene la pantalla compartida.
  • Firefox / Safari y altavoz: la UI avisa que setSinkId no está disponible; el resto de funcionalidad (cámara/mic) sigue operativa.

Notas de implementación

  • La función refreshDevices está definida dentro del useEffect que gestiona la sesión WebRTC, lo que garantiza acceso al closure mounted para evitar actualizaciones de estado tras desmontar.
  • Los tres useCallback (applyCameraChange, applyMicChange, applySpeakerChange) son callbacks estables que solo referencian refs y estado gestionado, minimizando re-renders.
  • El panel overlay usa variables CSS del design system (--bg-raised, --border, --radius, --fg-dim) para coherencia visual con el resto del modal.

Véase también

  • [[entity—meeting—component—meeting-room]]
  • [[feature—workspace—webrtc-peer-to-peer]]