import { useEffect, useRef } from 'react'; import { Track } from 'livekit-client'; import type { Participant, TrackPublication, Room } from 'livekit-client'; import { useVoiceStore } from '../stores/voice.ts'; function VideoTile({ participant, isLocal, room, source }: { participant: Participant; isLocal: boolean; room: Room; source: Track.Source }) { const videoRef = useRef(null); useEffect(() => { const el = videoRef.current; if (!el) return; let pub: TrackPublication | undefined; const attachTrack = () => { pub = participant.getTrackPublication(source); if (pub?.track && el) { pub.track.attach(el); el.muted = isLocal; el.play().catch(() => {}); } }; const detachTrack = () => { if (pub?.track && el) pub.track.detach(el); }; attachTrack(); const handleSubscribed = (track: Track, publication: TrackPublication, trackParticipant: Participant) => { if (trackParticipant.identity === participant.identity && publication.source === source) { track.attach(el); el.muted = isLocal; el.play().catch(() => {}); } }; room.on('trackSubscribed' as any, handleSubscribed); return () => { detachTrack(); room.off('trackSubscribed' as any, handleSubscribed); }; }, [participant, isLocal, room, source]); const username = participant.name || participant.identity; const isScreen = source === Track.Source.ScreenShare; return (
); } export function VideoGrid() { const room = useVoiceStore((s) => s._room); const voiceParticipants = useVoiceStore((s) => s.participants); if (!room) return null; const getLiveKitParticipant = (identity: string) => { if (identity === room.localParticipant.identity) return room.localParticipant; return room.remoteParticipants.get(identity); }; const camParticipants = voiceParticipants.filter((p) => p.hasVideo); const screenParticipants = voiceParticipants.filter((p) => p.isScreenSharing); if (camParticipants.length === 0 && screenParticipants.length === 0) return null; const hasScreen = screenParticipants.length > 0; // ponytail: grid layout for cameras; screen share gets the remaining space const camCols = camParticipants.length <= 1 ? 1 : camParticipants.length <= 4 ? 2 : 3; return (
{/* Screen shares */} {hasScreen && screenParticipants.map((vp) => { const p = getLiveKitParticipant(vp.identity); if (!p) return null; return (
); })} {/* Camera grid */} {camParticipants.length > 0 && (
{camParticipants.map((vp) => { const p = getLiveKitParticipant(vp.identity); if (!p) return null; return (
); })}
)}
); }