import { useEffect, useRef, useState } from 'react'; import { RoomEvent, Track } from 'livekit-client'; import type { Room, RemoteTrack, RemoteTrackPublication, RemoteParticipant } from 'livekit-client'; import type { Pod } from '@podman/shared'; import { Avatar } from './Avatar.js'; import { startBeat, type BeatHandle } from '../lib/beat.js'; interface PInfo { id: string; name: string; isLocal: boolean; speaking: boolean; } function snapshot(room: Room, fallbackName: string): PInfo[] { const lp = room.localParticipant; const local: PInfo = { id: lp.identity, name: lp.name || fallbackName, isLocal: true, speaking: lp.isSpeaking, }; const remotes = Array.from(room.remoteParticipants.values()).map((p) => ({ id: p.identity, name: p.name || p.identity, isLocal: false, speaking: p.isSpeaking, })); return [local, ...remotes]; } export function PodView({ team, me, room, devMode, onLeave, }: { team: Pod; me: string; room: Room | null; devMode: boolean; onLeave: () => void; }) { const [participants, setParticipants] = useState([]); const [sharing, setSharing] = useState(false); const [playingBeat, setPlayingBeat] = useState(false); const [note, setNote] = useState(null); const audioRef = useRef(null); const beatRef = useRef(null); const screenTrackRef = useRef(null); // Subscribe to live room state: participants, active speakers, remote audio. useEffect(() => { if (!room) return; const refresh = () => setParticipants(snapshot(room, me)); refresh(); const onAudio = (track: RemoteTrack, _pub: RemoteTrackPublication, _p: RemoteParticipant) => { if (track.kind === Track.Kind.Audio && audioRef.current) { audioRef.current.appendChild(track.attach()); } }; const onAudioGone = (track: RemoteTrack) => track.detach().forEach((el) => el.remove()); room .on(RoomEvent.ParticipantConnected, refresh) .on(RoomEvent.ParticipantDisconnected, refresh) .on(RoomEvent.ActiveSpeakersChanged, refresh) .on(RoomEvent.TrackSubscribed, onAudio) .on(RoomEvent.TrackUnsubscribed, onAudioGone); return () => { room .off(RoomEvent.ParticipantConnected, refresh) .off(RoomEvent.ParticipantDisconnected, refresh) .off(RoomEvent.ActiveSpeakersChanged, refresh) .off(RoomEvent.TrackSubscribed, onAudio) .off(RoomEvent.TrackUnsubscribed, onAudioGone); }; }, [room, me]); // Stop the beat if we leave/unmount. useEffect(() => { return () => { beatRef.current?.stop(); beatRef.current = null; }; }, []); async function toggleBeat() { if (!room) return; setNote(null); try { if (playingBeat) { if (beatRef.current) await room.localParticipant.unpublishTrack(beatRef.current.track); beatRef.current?.stop(); beatRef.current = null; setPlayingBeat(false); } else { await room.startAudio().catch(() => {}); const handle = startBeat(); beatRef.current = handle; await room.localParticipant.publishTrack(handle.track, { name: 'podman-beat' }); setPlayingBeat(true); } } catch (e) { setNote(`beat failed: ${(e as Error).message}`); } } async function toggleScreen() { if (!room) return; setNote(null); try { if (sharing) { if (screenTrackRef.current) await room.localParticipant.unpublishTrack(screenTrackRef.current); screenTrackRef.current?.stop(); screenTrackRef.current = null; setSharing(false); return; } if (!window.isSecureContext || !navigator.mediaDevices?.getDisplayMedia) { setNote('screen capture needs HTTPS (secure context)'); return; } const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); const track = stream.getVideoTracks()[0]; if (!track) return; track.onended = () => { screenTrackRef.current = null; setSharing(false); }; await room.localParticipant.publishTrack(track, { source: Track.Source.ScreenShare }); screenTrackRef.current = track; setSharing(true); } catch (e) { setNote(`screen share cancelled: ${(e as Error).message}`); } } const liveCount = participants.length; return (

{team.name}

{team.repo}

{devMode && (

DEV MODE — LiveKit not configured, so this is a local-only mock (no real room).

)} {/* Connectivity test controls */}
{playingBeat ? '🔊 broadcasting beat to the pod' : 'press “Play beat” — everyone should hear it'}
{note &&

{note}

}
{/* Live participants */}

In the room now ({liveCount})

{liveCount === 0 ? (

Connecting…

) : (
{participants.map((p) => (

{p.name}

{p.isLocal ? 'you' : 'connected'} {p.speaking ? ' · 🔊' : ''}

))}
)}

Pod roster: {team.members.join(', ') || '—'}

{/* PodMan panel */}
{/* hidden sink for remote audio elements */}
); }