From e97eab9b5efe15df09a3b2e72d6122f1e22b4db5 Mon Sep 17 00:00:00 2001 From: kaelvar Date: Mon, 14 Sep 2026 14:25:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20apply=20the=20saved=20?= =?UTF-8?q?reception=20resolution=20when=20joining=20a=20meeting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `VideoResolutionSubscription` applied the saved reception resolution on `RoomEvent.TrackPublished`. livekit-client does not raise that event for cameras that were already sending when the local participant joined, so a user who had chosen Low definition still received High definition from everyone already in the meeting, and Low definition only from whoever joined after them. Nothing in the UI showed the discrepancy: the setting kept displaying Low definition. Apply the preference to the publications we already know about when the effect runs, and keep listening on `TrackPublished` — which stays the earliest point to cap a camera that starts after us — plus `TrackSubscribed`, which is the first event raised for the cameras that were already sending. That initial pass also covers a change of preference mid-call, which `VideoTab.updateExistingRemoteVideoQuality` was doing separately. Removed, it is now the same code path for joining and for changing the setting. The three entry points overlap on purpose; the `publication.videoQuality` guard makes the repeats free. It reads as High definition when nothing was ever requested, so the default case costs no signal round trip either. Fixes #1606. --- CHANGELOG.md | 1 + .../VideoResolutionSubscription.tsx | 45 +++++++++++-------- .../settings/components/tabs/VideoTab.tsx | 22 +-------- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f5c6e2..0967aebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to - 🐛(helm) render periodSeconds and failureThreshold on probes - 🐛(backend) report the app release to Sentry instead of "NA" - 🐛(frontend) play the waiting room notification sound on every arrival +- 🐛(frontend) apply the saved reception resolution when joining a meeting #1714 - 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422 - 🔒️(backend) enforce display name setting on rename API - 🔒️(backend) reject inactive users in resource server backend diff --git a/src/frontend/src/features/rooms/livekit/components/VideoResolutionSubscription.tsx b/src/frontend/src/features/rooms/livekit/components/VideoResolutionSubscription.tsx index b045140a..d1d907a0 100644 --- a/src/frontend/src/features/rooms/livekit/components/VideoResolutionSubscription.tsx +++ b/src/frontend/src/features/rooms/livekit/components/VideoResolutionSubscription.tsx @@ -1,17 +1,16 @@ import { useEffect } from 'react' import { useRoomContext } from '@livekit/components-react' import { - type RemoteParticipant, + type RemoteTrack, type RemoteTrackPublication, RoomEvent, Track, - VideoQuality, } from 'livekit-client' import { useSnapshot } from 'valtio' import { userChoicesStore } from '@/stores/userChoices' /** - * Sets initial video quality for new participants as they join. + * Applies the saved reception quality to every remote camera. * LiveKit doesn't allow handling video quality preferences at the room level. */ export const VideoResolutionSubscription = () => { @@ -19,30 +18,40 @@ export const VideoResolutionSubscription = () => { const room = useRoomContext() useEffect(() => { - if (!room) return + if (!room || videoSubscribeQuality === undefined) return - const handleTrackPublished = ( - publication: RemoteTrackPublication, - _participant: RemoteParticipant - ) => { - // By default, the maximum quality is set to high + const applyQuality = (publication: RemoteTrackPublication) => { if ( - videoSubscribeQuality === undefined || - videoSubscribeQuality === VideoQuality.HIGH - ) - return - - if ( - publication.kind === Track.Kind.Video && - publication.source !== Track.Source.ScreenShare + publication.kind !== Track.Kind.Video || + publication.source === Track.Source.ScreenShare || + publication.videoQuality === videoSubscribeQuality ) { - publication.setVideoQuality(videoSubscribeQuality) + return } + publication.setVideoQuality(videoSubscribeQuality) } + // Cameras we are already receiving: those published before this effect ran, + // and all of them again whenever the preference changes mid-call. + room.remoteParticipants.forEach((participant) => + participant.videoTrackPublications.forEach(applyQuality) + ) + + const handleTrackPublished = (publication: RemoteTrackPublication) => + applyQuality(publication) + + // TrackPublished is not raised for cameras that were already sending when we + // joined, but it is the earliest point for the ones that start after us. + const handleTrackSubscribed = ( + _track: RemoteTrack, + publication: RemoteTrackPublication + ) => applyQuality(publication) + room.on(RoomEvent.TrackPublished, handleTrackPublished) + room.on(RoomEvent.TrackSubscribed, handleTrackSubscribed) return () => { room.off(RoomEvent.TrackPublished, handleTrackPublished) + room.off(RoomEvent.TrackSubscribed, handleTrackSubscribed) } }, [room, videoSubscribeQuality]) diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx index 7b307031..39f24411 100644 --- a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -34,7 +34,7 @@ const EMPTY_PROPS = {} export const VideoTab = ({ id }: VideoTabProps) => { const { t } = useTranslation('settings', { keyPrefix: 'video' }) const room = useRoomContext() - const { localParticipant, remoteParticipants } = room + const { localParticipant } = room const { videoDeviceId, @@ -88,22 +88,6 @@ export const VideoTab = ({ id }: VideoTabProps) => { }) } - /** - * Updates video quality for all existing remote video tracks when user preference changes. - * LiveKit doesn't support setting video quality preferences at the room level for remote participants, - * so this function applies the selected quality to all existing remote video tracks. - * Hook useVideoResolutionSubscription updates quality preferences of new participants joining. - */ - const updateExistingRemoteVideoQuality = (selectedQuality: VideoQuality) => { - remoteParticipants.forEach((participant) => { - participant.videoTrackPublications.forEach((publication) => { - if (publication.videoQuality !== selectedQuality) { - publication.setVideoQuality(selectedQuality) - } - }) - }) - } - useEffect(() => { let videoTrack: LocalVideoTrack | null = null @@ -231,9 +215,7 @@ export const VideoTab = ({ id }: VideoTabProps) => { selectedKey={videoSubscribeQuality?.toString()} onSelectionChange={(key) => { if (key == undefined) return - const selectedQuality = Number(String(key)) - saveVideoSubscribeQuality(selectedQuality) - updateExistingRemoteVideoQuality(selectedQuality) + saveVideoSubscribeQuality(Number(String(key))) }} style={{ width: '100%',