From 93dcbb7591a36f607f9cc80cec7928be716b27f7 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 17 May 2026 20:46:13 +0100 Subject: [PATCH 1/3] fix: handle odd dims --- core/src/graphics/yuv_renderer.rs | 11 ++++++----- core/src/livekit/video.rs | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/graphics/yuv_renderer.rs b/core/src/graphics/yuv_renderer.rs index 3dbb849..c176273 100644 --- a/core/src/graphics/yuv_renderer.rs +++ b/core/src/graphics/yuv_renderer.rs @@ -115,7 +115,8 @@ impl YuvPipeline { // For R8Unorm textures, bytes_per_row = texture_width (1 byte per pixel). // So we align the texture width to 256. let y_tex_w = align_to(width, 256); - let uv_tex_w = align_to(width / 2, 256); + let uv_tex_w = align_to(width.div_ceil(2), 256); + let uv_h = height.div_ceil(2); let y_tex = device.create_texture(&wgpu::TextureDescriptor { label: Some("YUV Y texture"), @@ -136,7 +137,7 @@ impl YuvPipeline { label: Some("YUV U texture"), size: wgpu::Extent3d { width: uv_tex_w, - height: height / 2, + height: uv_h, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -151,7 +152,7 @@ impl YuvPipeline { label: Some("YUV V texture"), size: wgpu::Extent3d { width: uv_tex_w, - height: height / 2, + height: uv_h, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -241,8 +242,8 @@ impl YuvPipeline { }; let y_tex_w = align_to(frame.width, 256); - let uv_tex_w = align_to(frame.width / 2, 256); - let uv_h = frame.height / 2; + let uv_tex_w = align_to(frame.width.div_ceil(2), 256); + let uv_h = frame.height.div_ceil(2); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("YUV upload encoder"), diff --git a/core/src/livekit/video.rs b/core/src/livekit/video.rs index e9a2884..d52f764 100644 --- a/core/src/livekit/video.rs +++ b/core/src/livekit/video.rs @@ -40,7 +40,7 @@ impl VideoBuffer { // GPU-aligned strides (wgpu requires bytes_per_row multiple of 256) let y_stride = align_to(width, 256); - let uv_stride = align_to(width / 2, 256); + let uv_stride = align_to(width.div_ceil(2), 256); self.stride_y = y_stride; self.stride_u = uv_stride; self.stride_v = uv_stride; @@ -67,7 +67,7 @@ impl VideoBuffer { .copy_from_slice(&dy[src_start..src_start + width as usize]); } - let uv_w = (width / 2) as usize; + let uv_w = width.div_ceil(2) as usize; for row in 0..chroma_height as usize { let src_start = row * src_stride_u as usize; let dst_start = row * uv_stride as usize; From efa72e5a233f376f80d76f2233cc151a90689a2a Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 17 May 2026 20:46:45 +0100 Subject: [PATCH 2/3] fix: align webapp with updated app tracks lifetime --- web-app/src/pages/Room.tsx | 151 ++++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 46 deletions(-) diff --git a/web-app/src/pages/Room.tsx b/web-app/src/pages/Room.tsx index 03be557..5ad1e40 100644 --- a/web-app/src/pages/Room.tsx +++ b/web-app/src/pages/Room.tsx @@ -11,6 +11,7 @@ import { TrackReference, StartAudio, useDataChannel, + useIsMuted, } from "@livekit/components-react"; import "@livekit/components-styles"; import { HiMiniUser } from "react-icons/hi2"; @@ -259,20 +260,44 @@ function ParticipantsGrid() { }, [visibleCameraTracks, visibleAudioTracks, remoteParticipants, localParticipant]); // Get screen share tracks (including local) - don't filter by participant name for screen shares - const activeScreenShare = useMemo(() => { - // Screen shares might come from any participant, don't filter them + const screenShareTrack = useMemo(() => { return screenShareTracks.length > 0 ? screenShareTracks[0] : null; }, [screenShareTracks]); // Get the user's display name for local participant const localUserName = user ? `${user.first_name} ${user.last_name}` : "You"; - // If there's a screen share, show focus layout (screen share center, participants on side) - if (activeScreenShare) { + return ( + + ); +} + +function ParticipantsLayout({ + participants, + screenShareTrack, + localUserName, + localParticipant, +}: { + participants: Array<{ participant: Participant; cameraTrack?: TrackReference; isLocal: boolean }>; + screenShareTrack: TrackReference | null; + localUserName: string; + localParticipant: any; +}) { + const isScreenShareMuted = useIsMuted( + screenShareTrack ?? { participant: localParticipant, source: Track.Source.ScreenShare }, + ); + const hasActiveScreenShare = !!screenShareTrack && !isScreenShareMuted; + + if (hasActiveScreenShare) { const screenShareOwnerName = - activeScreenShare.participant.identity === localParticipant?.identity ? + screenShareTrack.participant.identity === localParticipant?.identity ? localUserName - : cleanParticipantName(activeScreenShare.participant.name || activeScreenShare.participant.identity); + : cleanParticipantName(screenShareTrack.participant.name || screenShareTrack.participant.identity); return (
@@ -290,7 +315,7 @@ function ParticipantsGrid() { ))} {/* Screen share takes focus with cursor overlay */} - +
); } @@ -499,8 +524,9 @@ function ParticipantCard({ const rawParticipantName = participant.name || participant.identity || "Unknown"; const participantName = isLocal ? localUserName : cleanParticipantName(rawParticipantName); - // Check if participant is muted - const isMicMuted = !participant.isMicrophoneEnabled; + // Use LiveKit hooks for reactive muted state + const isCameraMuted = useIsMuted(cameraTrack ?? { participant, source: Track.Source.Camera }); + const isMicMuted = useIsMuted({ participant, source: Track.Source.Microphone }); return (
- {cameraTrack ? + {cameraTrack && !isCameraMuted ? :
} {/* Audio playback for remote participants */} - {!isLocal && participant.audioTrackPublications.size > 0 && ( - - )} + {!isLocal && } {/* Participant metadata bar */}
@@ -541,6 +558,24 @@ function ParticipantCard({ ); } +function RemoteAudio({ participant }: { participant: Participant }) { + const isMicMuted = useIsMuted({ participant, source: Track.Source.Microphone }); + const audioPub = Array.from(participant.audioTrackPublications.values())[0]; + + if (!audioPub || isMicMuted) return null; + + return ( + + ); +} + function AudioButton({ hasAudioEnabled, setHasAudioEnabled, @@ -591,12 +626,18 @@ function AudioButton({ onChange={(e) => handleMicrophoneChange(e.target.value)} onClick={(e) => e.stopPropagation()} className={clsx( - "hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-xs p-0 border-0 shadow-none hover:shadow-xs text-xs", + "hover:outline-solid hover:outline-1 hover:outline-slate-300 focus:ring-0 focus-visible:ring-0 hover:bg-slate-200 size-4 rounded-xs p-0 border-0 shadow-none hover:shadow-xs text-[0px] w-4 appearance-none bg-no-repeat bg-right cursor-pointer block leading-none align-middle", { [Colors.mic.text]: hasAudioEnabled, [Colors.deactivatedIcon]: !hasAudioEnabled, }, )} + style={{ + backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`, + backgroundPosition: "right 2px center", + backgroundRepeat: "no-repeat", + backgroundSize: "12px", + }} > {microphoneDevices.map((device) => (