speechSynthesis vous donne un son que vous entendez et que vous ne pouvez pas envoyer. Il n’y a aucun MediaStreamTrack au bout, donc une réponse traduite prononcée par le navigateur n’entrera jamais dans un sender WebRTC. Je l’ai découvert en construisant une salle d’appel 1:1 avec traduction en direct, et la sortie consiste à ne plus s’en servir pour ce qui doit quitter la machine.
Ce que l’API renvoie réellement
La Web Speech API parle par le chemin audio de la plateforme, pas par quelque chose que la page possède. Vous obtenez une utterance, des événements et du son dans les haut-parleurs. Ce que vous n’obtenez jamais, c’est un nœud, un flux ou une piste. Ce n’est pas un oubli d’un navigateur : aucune capture n’est spécifiée, donc il n’y a rien à détecter et rien qui arrivera dans une version ultérieure.
Cela compte dès que le son a une destination autre que les haut-parleurs locaux. Un sous-titre passe. Une voix que la personne à l’autre bout doit entendre, non, parce que WebRTC n’envoie que ce qui se trouve sur une piste.
La voie qui marche
Utilisez un synthétiseur qui renvoie un fichier. J’utilise melotts de Workers AI, qui répond avec des octets audio plutôt qu’avec la promesse de faire du bruit. Ensuite le chemin est du Web Audio ordinaire : décodez les octets, connectez le buffer source à un MediaStreamDestination, et prenez la piste de cette destination.
// Synthesise to a file, decode through Web Audio, and replace the
// microphone track on the sender. speechSynthesis never enters this path.
const buf = await audioCtx.decodeAudioData(await res.arrayBuffer());
const dest = audioCtx.createMediaStreamDestination();
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(dest);
src.start();
const sender = pc.getSenders().find((s) => s.track?.kind === 'audio');
await sender.replaceTrack(dest.stream.getAudioTracks()[0]);replaceTrack est l’appel qui compte. Il échange l’audio sortant sans renégocier, donc l’autre côté entend la voix traduite au lieu du micro, sans glare, sans nouvelle offre et sans coupure.
La conséquence dont personne ne prévient
Comme la voix traduite remplace le micro sur le sender, tout ce qui reconstruit la RTCPeerConnection doit reconstruire aussi le canal vocal. Une reconnexion qui ne restaure que le peer remet le micro brut sur le fil, et l’autre côté entend soudain la voix non traduite en pleine phrase.
Cette panne est silencieuse du côté qui la provoque. Vous vous entendez normalement, l’appel reste connecté, et rien ne journalise d’erreur. Je ne la rattrape aujourd’hui que parce qu’un test navigateur recharge l’autre côté en pleine communication et échoue si le micro revient.
Ce que je regarderais en premier
- Si le son doit quitter la machine, écartez speechSynthesis dès la conception, pas après le prototype.
- Gardez une seule fonction qui construit la piste audio sortante, et appelez-la depuis chaque chemin qui crée une connexion.
- Écrivez le test qui recharge un côté en pleine communication. C’est la seule chose qui attrape la régression, car le symptôme est inaudible pour celui qui l’introduit.