Files
dumpsterChat/web/src/components/AudioRenderers.tsx
T

101 lines
3.0 KiB
TypeScript

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<HTMLAudioElement>();
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<HTMLAudioElement>(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 <audio ref={audioRef} autoPlay />;
}
export function AudioRenderers() {
const room = useVoiceStore((s) => s._room);
// ponytail: use a counter that increments on every participant change
// zustand's Object.is check on the participants array wasn't triggering re-renders
const [, setTick] = useState(0);
useEffect(() => {
if (!room) return;
const bump = () => setTick((t) => t + 1);
room.on(RoomEvent.ParticipantConnected, bump);
room.on(RoomEvent.ParticipantDisconnected, bump);
room.on(RoomEvent.TrackSubscribed, bump);
room.on(RoomEvent.TrackUnsubscribed, bump);
return () => {
room.off(RoomEvent.ParticipantConnected, bump);
room.off(RoomEvent.ParticipantDisconnected, bump);
room.off(RoomEvent.TrackSubscribed, bump);
room.off(RoomEvent.TrackUnsubscribed, bump);
};
}, [room]);
if (!room) return null;
const remoteParticipants = Array.from(room.remoteParticipants.values());
return (
<div className="hidden">
{remoteParticipants.map((p) => (
<RemoteAudioTrack key={p.identity} participant={p} room={room} />
))}
</div>
);
}