fix: screenshare rendering, voice panel layout, SW cache bust with git SHA

- VideoGrid: add screen share track display with proper sizing
- participantToVoice: detect ScreenShare + ScreenShareAudio tracks
- VoicePanel: flex layout, no scroll, tiles dynamically fill space
- VoiceChannel: show screen/camera icons in participant list
- vite.config: inject git SHA into SW CACHE_VERSION on build
This commit is contained in:
2026-07-06 17:49:17 +00:00
parent cb4df31f43
commit c9a213ab8a
6 changed files with 101 additions and 80 deletions
+52 -39
View File
@@ -3,18 +3,16 @@ import { Track } from 'livekit-client';
import type { Participant, TrackPublication, Room } from 'livekit-client';
import { useVoiceStore } from '../stores/voice.ts';
function VideoTile({ participant, isLocal, room }: { participant: Participant; isLocal: boolean; room: Room }) {
function VideoTile({ participant, isLocal, room, source }: { participant: Participant; isLocal: boolean; room: Room; source: Track.Source }) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const el = videoRef.current;
if (!el) return;
let pub: TrackPublication | undefined;
const attachTrack = () => {
pub = participant.getTrackPublication(Track.Source.Camera);
pub = participant.getTrackPublication(source);
if (pub?.track && el) {
pub.track.attach(el);
el.muted = isLocal;
@@ -23,16 +21,13 @@ function VideoTile({ participant, isLocal, room }: { participant: Participant; i
};
const detachTrack = () => {
if (pub?.track && el) {
pub.track.detach(el);
}
if (pub?.track && el) pub.track.detach(el);
};
attachTrack();
// Listen for track subscriptions
const handleSubscribed = (track: Track, publication: TrackPublication, trackParticipant: Participant) => {
if (trackParticipant.identity === participant.identity && publication.source === Track.Source.Camera) {
if (trackParticipant.identity === participant.identity && publication.source === source) {
track.attach(el);
el.muted = isLocal;
el.play().catch(() => {});
@@ -40,25 +35,25 @@ function VideoTile({ participant, isLocal, room }: { participant: Participant; i
};
room.on('trackSubscribed' as any, handleSubscribed);
return () => {
detachTrack();
room.off('trackSubscribed' as any, handleSubscribed);
};
}, [participant, isLocal, room]);
}, [participant, isLocal, room, source]);
const username = participant.name || participant.identity;
const isScreen = source === Track.Source.ScreenShare;
return (
<div className="relative bg-gb-bg rounded-sm overflow-hidden border border-gb-bg-t aspect-video flex items-center justify-center">
<div className={`relative bg-gb-bg rounded-sm overflow-hidden border ${isScreen ? 'border-gb-aqua' : 'border-gb-bg-t'} w-full h-full flex items-center justify-center`}>
<video
ref={videoRef}
className="absolute inset-0 w-full h-full object-cover"
className={`${isScreen ? 'w-full h-full' : 'absolute inset-0 w-full h-full'} object-contain`}
autoPlay
playsInline
/>
<div className="absolute bottom-1 left-1 text-xxs text-gb-fg-s bg-gb-bg/70 px-1 py-0.5 rounded-sm">
{username}{isLocal ? ' (you)' : ''}
{isScreen ? '🖥 ' : ''}{username}{isLocal ? ' (you)' : ''}
</div>
</div>
);
@@ -70,42 +65,60 @@ export function VideoGrid() {
if (!room) return null;
const hasCamParticipants = voiceParticipants.filter((p) => p.hasVideo);
if (hasCamParticipants.length === 0) 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 (
<div className="px-3 py-1.5 border-b border-gb-bg-t">
<div className="text-xxs text-gb-fg-f mb-1">
video [{hasCamParticipants.length}]
</div>
<div
className={`grid gap-1 ${
hasCamParticipants.length === 1
? 'grid-cols-1'
: hasCamParticipants.length <= 4
? 'grid-cols-2'
: 'grid-cols-3'
}`}
>
{hasCamParticipants.map((vp) => {
const p = getLiveKitParticipant(vp.identity);
if (!p) return null;
return (
<div className="flex-1 min-h-0 flex flex-col px-2 py-1 gap-1">
{/* Screen shares */}
{hasScreen && screenParticipants.map((vp) => {
const p = getLiveKitParticipant(vp.identity);
if (!p) return null;
return (
<div key={`screen-${p.identity}`} className="flex-1 min-h-0">
<VideoTile
key={p.identity}
participant={p}
isLocal={p.identity === room.localParticipant.identity}
room={room}
source={Track.Source.ScreenShare}
/>
);
})}
</div>
</div>
);
})}
{/* Camera grid */}
{camParticipants.length > 0 && (
<div
className={`${hasScreen ? 'shrink-0 h-[25%]' : 'flex-1'} grid gap-1`}
style={{ gridTemplateColumns: `repeat(${camCols}, 1fr)` }}
>
{camParticipants.map((vp) => {
const p = getLiveKitParticipant(vp.identity);
if (!p) return null;
return (
<div key={p.identity} className="min-h-0">
<VideoTile
participant={p}
isLocal={p.identity === room.localParticipant.identity}
room={room}
source={Track.Source.Camera}
/>
</div>
);
})}
</div>
)}
</div>
);
}