ENESFRPT

Profundidad en todo. Superficialidad en nada.

srcObject es estado del DOM, no una prop

Tu propio cuadro sale en negro y la otra persona te ve perfectamente. Esa asimetría es todo el diagnóstico: la pista se negocia y se envía, así que la cámara funciona y la conexión funciona. Lo que ha fallado es local, y es que srcObject es estado del DOM y no una prop, así que React no lo restaura nunca.

Por qué falla en unas entradas y en otras no

Una pantalla de llamada tiene fases. Esperando aprobación, conectando, conectado, terminada, reconectando. Varias de ellas devuelven pronto sin renderizar los elementos de vídeo. Cada uno de esos retornos destruye el elemento, y la fase siguiente monta un nodo nuevo con srcObject a null.

Así que asignar el stream una sola vez, dentro de la promesa de getUserMedia, funciona justo cuando resulta que existe un elemento de vídeo en ese instante. Falla siempre que la cámara se abre en una pantalla que no tiene ninguno: el invitado esperando a que el anfitrión le deje entrar, y cada reentrada desde la pantalla de fin o de conexión perdida. No salta nada, porque asignar sobre una ref que es null no es un error, es una operación vacía.

Por qué parece un problema de cámara y no lo es

El stream se guarda igualmente. Las pistas están vivas, se añaden a la conexión y se envían. Por eso el otro lado no se entera, y por eso el primer instinto, revisar permisos y dispositivos, no encuentra nada. El fallo está entero en los últimos centímetros, entre un MediaStream que existe y un elemento de vídeo al que nunca se lo dijeron.

Enlaza en las dos direcciones

Hay dos sucesos y ningún orden garantizado: llega el stream y se monta el elemento. Atiende a los dos. Una callback ref recoge el stream actual cada vez que monta un nodo, y un setter empuja el stream al nodo actual cada vez que llega. El que ocurra segundo hace el enlace.

// Bound in both directions, so whichever arrives second does the work.
const el = useRef<HTMLVideoElement | null>(null);
const want = useRef<MediaStream | null>(null);

const paint = useCallback((node: HTMLVideoElement | null) => {
  if (!node || node.srcObject === want.current) return;
  node.srcObject = want.current;
  if (want.current) void node.play().catch(() => undefined);
}, []);

// Callback ref: pulls, when a new element mounts.
const videoRef = useCallback((node) => { el.current = node; paint(node); }, [paint]);
// Setter: pushes, when the stream arrives.
const showPreview = useCallback((s) => { want.current = s; paint(el.current); }, [paint]);

La comprobación de igualdad importa. Sin ella, cada re-render reasigna srcObject, y reasignar reinicia el elemento, lo que produce un parpadeo visible en una vista previa que ya funcionaba.

Probarlo para que siga arreglado

Comprobar que srcObject no es null no basta. Un stream con las pistas paradas sigue enlazado y no pinta nada, así que la comprobación pasa sobre un cuadro negro. Espera fotogramas descodificados: videoWidth por encima de cero, el elemento sin pausar, y al menos una pista todavía en estado live.

  • No vuelvas a un if (ref.current) pelado. Es correcto en la ruta que estás probando y falso en las que no.
  • Limpia el stream guardado en el cleanup, o se pintará uno viejo en el siguiente montaje.
  • Prueba la entrada que tiene pantalla de espera, no solo la entrada directa. La directa es la ruta que ya funcionaba.
  • speechSynthesis no se puede capturar

    La Web Speech API no te da un MediaStreamTrack, así que una traducción hablada nunca entra en un sender de WebRTC. Esta es la vía que sí funciona.

  • Translator.create necesita un clic real

    La Translator API de Chrome exige activación transitoria siempre que falte descargar el paquete de idioma, que es todo perfil nuevo. Precaliéntala en el clic.

¿Tienes algo entre manos que tiene que aguantar?

Si estás en algún punto entre una demo y un sistema del que depende gente de verdad, esa es justo la parte que hago.