🐛(frontend) apply the saved reception resolution when joining a meeting

`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.
This commit is contained in:
kaelvar
2026-09-14 14:25:21 +02:00
committed by aleb_the_flash
parent 436b3dc9df
commit e97eab9b5e
3 changed files with 30 additions and 38 deletions
+1
View File
@@ -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
@@ -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])
@@ -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%',