ENESFRPT

Profundidade em tudo. Superficialidade em nada.

srcObject é estado do DOM, não uma prop

O teu quadrado fica preto e a outra pessoa vê-te perfeitamente. Essa assimetria é todo o diagnóstico: a faixa é negociada e enviada, por isso a câmara funciona e a ligação funciona. O que falhou é local, e é que o srcObject é estado do DOM e não uma prop, por isso o React nunca o repõe.

Porque falha nuns caminhos de entrada e noutros não

Um ecrã de chamada tem fases. À espera de aprovação, a ligar, ligado, terminada, a reconectar. Várias delas devolvem cedo sem renderizar os elementos de vídeo. Cada um desses retornos destrói o elemento, e a fase seguinte monta um nó novo com o srcObject a null.

Por isso ligar o stream uma única vez, dentro da promessa do getUserMedia, funciona exatamente quando por acaso existe um elemento de vídeo nesse instante. Falha sempre que a câmara abre num ecrã que não tem nenhum: o convidado à espera de que o anfitrião o deixe entrar, e todas as reentradas a partir do ecrã de fim ou de ligação perdida. Nada rebenta, porque atribuir a uma ref que é null não é um erro, é uma operação vazia.

Porque parece um problema de câmara e não é

O stream fica guardado de qualquer forma. As faixas estão vivas, são adicionadas à ligação e são enviadas. É por isso que o outro lado não dá por nada, e é por isso que o primeiro instinto, ver permissões e dispositivos, não encontra nada. O erro está todo nos últimos centímetros, entre um MediaStream que existe e um elemento de vídeo a quem nunca o disseram.

Liga nos dois sentidos

Há dois acontecimentos e nenhuma ordem garantida: o stream chega, e o elemento monta. Trata dos dois. Uma callback ref puxa o stream atual sempre que um nó monta, e um setter empurra o stream para o nó atual sempre que ele chega. O que acontecer em segundo lugar faz a ligação.

// 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]);

A verificação de igualdade importa. Sem ela, cada re-render volta a atribuir o srcObject, e voltar a atribuir reinicia o elemento, o que produz uma cintilação visível numa pré-visualização que já estava a funcionar.

Testar para que continue corrigido

Verificar que o srcObject não é null não chega. Um stream com as faixas paradas continua ligado e não pinta nada, por isso a verificação passa sobre um quadrado preto. Espera por fotogramas descodificados: videoWidth acima de zero, o elemento sem pausa, e pelo menos uma faixa ainda em estado live.

  • Não voltes a um if (ref.current) simples. É correto no caminho que estás a testar e errado nos que não estás.
  • Limpa o stream guardado no cleanup, ou um antigo vai ser pintado na montagem seguinte.
  • Testa a entrada que passa por um ecrã de espera, não só a entrada direta. A direta é o caminho que já funcionava.

Tens uma coisa em mãos que tem de aguentar?

Se estás algures entre uma demo e um sistema de que depende gente a sério, essa é precisamente a parte que eu faço.