From 1ac17785214aae261e940aa773a33e647835c436 Mon Sep 17 00:00:00 2001 From: kaelvar Date: Tue, 1 Sep 2026 22:09:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20keep=20the=20sending?= =?UTF-8?q?=20resolution=20picked=20while=20the=20camera=20is=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `handleVideoResolutionChange` did all of its work inside `if (videoTrack)`, including `saveVideoPublishResolution`. With the camera off there is no camera publication, so choosing a resolution did nothing at all: it was neither applied nor recorded, while the selector went on showing the value the user had just picked. Turning the camera back on then published at the old resolution, and so did the next session. Found on a self-hosted instance: a user set the sending resolution with the camera off, turned it back on, and the publisher kept sending 720p. Nothing in the UI suggested the choice had been dropped. Persist the choice first and unconditionally, then restart the track only when there is one to restart. Persisting alone is not enough within a session. `roomOptions` is only read by `new Room(...)`, so a store update never reaches a room that is already built. Sync the VideoDeviceControl with the userChoiesStore resolution, as we did for the device id and the processor configuration. The early return is the honest shape here: with no live track there is nothing to await, and the defaults above already cover what happens next. --- CHANGELOG.md | 4 +++ .../controls/Device/VideoDeviceControl.tsx | 8 ++++-- .../settings/components/tabs/VideoTab.tsx | 26 ++++++++++--------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fcee43..0b9e9bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Fixed + +- 🐛(frontend) keep the sending resolution picked while the camera is off #1667 + ## [1.30.0] - 2026-09-01 ### Added diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx index 62f2f294..3a533385 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react' import { Button, Popover } from '@/primitives' import { RiArrowUpSLine, RiImageCircleAiFill } from '@remixicon/react' -import { Track, type VideoCaptureOptions } from 'livekit-client' +import { Track, type VideoCaptureOptions, VideoPresets } from 'livekit-client' import { ToggleDevice } from './ToggleDevice' import { css } from '@/styled-system/css' @@ -57,7 +57,8 @@ export const VideoDeviceControl = ({ }: VideoDeviceControlProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) - const { videoDeviceId, processorConfig } = useSnapshot(userChoicesStore) + const { videoDeviceId, processorConfig, videoPublishResolution } = + useSnapshot(userChoicesStore) const onChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => @@ -97,6 +98,9 @@ export const VideoDeviceControl = ({ await toggle(!trackProps.enabled, { processor: processor, + ...(videoPublishResolution && { + resolution: VideoPresets[videoPublishResolution].resolution, + }), } as VideoCaptureOptions) } diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx index ccf145b6..6ae71bbc 100644 --- a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -32,7 +32,8 @@ const EMPTY_PROPS = {} export const VideoTab = ({ id }: VideoTabProps) => { const { t } = useTranslation('settings', { keyPrefix: 'video' }) - const { localParticipant, remoteParticipants } = useRoomContext() + const room = useRoomContext() + const { localParticipant, remoteParticipants } = room const { videoDeviceId, @@ -70,19 +71,20 @@ export const VideoTab = ({ id }: VideoTabProps) => { } const handleVideoResolutionChange = async (key: 'h720' | 'h360' | 'h180') => { - const videoPublication = localParticipant.getTrackPublication( + saveVideoPublishResolution(key) + const videoTrack = localParticipant.getTrackPublication( Track.Source.Camera - ) - const videoTrack = videoPublication?.track - if (videoTrack) { - saveVideoPublishResolution(key) - await videoTrack.restartTrack({ - resolution: VideoPresets[key].resolution, - deviceId: { exact: videoDeviceId }, - processor: - BackgroundProcessorFactory.fromProcessorConfig(processorConfig), - }) + )?.track + if (!videoTrack) { + return } + + await videoTrack.restartTrack({ + resolution: VideoPresets[key].resolution, + deviceId: { exact: videoDeviceId }, + processor: + BackgroundProcessorFactory.fromProcessorConfig(processorConfig), + }) } /**