Votre propre vignette est noire et l’autre personne vous voit très bien. Cette asymétrie est tout le diagnostic : la piste est négociée et envoyée, donc la caméra marche et la connexion marche. Ce qui a échoué est local, et c’est que srcObject est un état du DOM et non une prop, donc React ne le restaure jamais.
Pourquoi cela échoue sur certaines entrées seulement
Un écran d’appel a des phases. En attente d’admission, connexion, connecté, terminé, reconnexion. Plusieurs d’entre elles retournent tôt sans rendre les éléments vidéo. Chacun de ces retours détruit l’élément, et la phase suivante monte un nœud neuf dont srcObject vaut null.
Lier le flux une seule fois, dans la promesse de getUserMedia, fonctionne donc exactement quand un élément vidéo existe à cet instant. Cela rate dès que la caméra s’ouvre sur un écran qui n’en a pas : l’invité qui attend d’être admis, et chaque retour depuis l’écran de fin ou de connexion perdue. Rien ne lève d’erreur, car affecter une ref nulle n’est pas une erreur, c’est une opération vide.
Pourquoi cela ressemble à un problème de caméra sans en être un
Le flux est stocké dans les deux cas. Les pistes sont vivantes, ajoutées à la connexion, et envoyées. C’est pourquoi l’autre côté n’est pas affecté, et pourquoi le premier réflexe, vérifier permissions et périphériques, ne trouve rien. Le bug tient entièrement dans les derniers centimètres, entre un MediaStream qui existe et un élément vidéo à qui on ne l’a jamais dit.
Lier dans les deux sens
Il y a deux événements et aucun ordre garanti : le flux arrive, et l’élément se monte. Traitez les deux. Une callback ref tire le flux courant quand un nœud se monte, et un setter pousse le flux vers le nœud courant quand il arrive. Celui qui survient en second fait la liaison.
// 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]);Le test d’égalité compte. Sans lui, chaque re-rendu réaffecte srcObject, et réaffecter redémarre l’élément, ce qui provoque un clignotement visible sur un aperçu qui fonctionnait déjà.
Le tester pour qu’il reste corrigé
Vérifier que srcObject n’est pas null ne suffit pas. Un flux dont les pistes sont arrêtées reste lié et ne peint rien, donc l’assertion passe sur un carré noir. Attendez des images décodées : videoWidth au-dessus de zéro, l’élément non en pause, et au moins une piste encore à l’état live.
- Ne revenez pas à un simple if (ref.current). Il est correct sur le chemin que vous testez et faux sur ceux que vous ne testez pas.
- Videz le flux stocké au nettoyage, sinon un flux périmé sera peint au montage suivant.
- Testez l’entrée qui passe par un écran d’attente, pas seulement l’entrée directe. La directe est le chemin qui marchait déjà.