import { useEffect, useRef, useState } 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(); function flushPendingAudio() { for (const el of pendingAudio) { el.play().catch(() => {}); pendingAudio.delete(el); } } 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(null); useEffect(() => { const el = audioRef.current; if (!el) return; 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); tryPlay(); } }; attachIfReady(); const handleSubscribed = (track: Track, _pub: TrackPublication, p: Participant) => { if (p.identity === participant.identity && _pub.source === Track.Source.Microphone) { track.attach(el); tryPlay(); } }; room.on(RoomEvent.TrackSubscribed, handleSubscribed); return () => { if (pub?.track) pub.track.detach(el); pendingAudio.delete(el); room.off(RoomEvent.TrackSubscribed, handleSubscribed); }; }, [participant, room]); return