fix(voice): handle browser autoplay block — flush pending audio on interaction

This commit is contained in:
2026-07-09 13:02:40 -04:00
parent 67a5a54244
commit 90bddc65d4
2 changed files with 31 additions and 6 deletions
+30 -5
View File
@@ -1,8 +1,27 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { useVoiceStore } from '../stores/voice.ts';
import { Track, RoomEvent } from 'livekit-client';
import type { Participant, TrackPublication, Room } from 'livekit-client';
// ponytail: global set of audio elements blocked by autoplay policy
const pendingAudio = new Set<HTMLAudioElement>();
function flushPendingAudio() {
for (const el of pendingAudio) {
el.play().catch(() => {});
pendingAudio.delete(el);
}
}
// ponytail: one-time document listener to flush on any user interaction
let listenerAttached = false;
function ensureInteractionListener() {
if (listenerAttached) return;
listenerAttached = true;
document.addEventListener('click', flushPendingAudio, { once: false });
document.addEventListener('keydown', flushPendingAudio, { once: false });
}
function RemoteAudioTrack({ participant, room }: { participant: Participant; room: Room }) {
const audioRef = useRef<HTMLAudioElement>(null);
@@ -12,28 +31,34 @@ function RemoteAudioTrack({ participant, room }: { participant: Participant; roo
let pub: TrackPublication | undefined;
const tryPlay = () => {
el.play().catch(() => {
pendingAudio.add(el);
ensureInteractionListener();
});
};
const attachIfReady = () => {
pub = participant.getTrackPublication(Track.Source.Microphone);
if (pub?.track && el) {
pub.track.attach(el);
el.play().catch(() => {});
tryPlay();
}
};
// ponytail: check immediately — track may already be subscribed
attachIfReady();
// ponytail: also listen for delayed subscription
const handleSubscribed = (track: Track, _pub: TrackPublication, p: Participant) => {
if (p.identity === participant.identity && _pub.source === Track.Source.Microphone) {
track.attach(el);
el.play().catch(() => {});
tryPlay();
}
};
room.on(RoomEvent.TrackSubscribed, handleSubscribed);
return () => {
if (pub?.track) pub.track.detach(el);
pendingAudio.delete(el);
room.off(RoomEvent.TrackSubscribed, handleSubscribed);
};
}, [participant, room]);