diff --git a/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts b/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts new file mode 100644 index 00000000..13c518e2 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useVideoResolutionSubscription.ts @@ -0,0 +1,51 @@ +import { useEffect } from 'react' +import { usePersistentUserChoices } from './usePersistentUserChoices' +import { useRoomContext } from '@livekit/components-react' +import { + RemoteParticipant, + RemoteTrackPublication, + RoomEvent, + Track, + VideoQuality, +} from 'livekit-client' + +/** + * This hook sets initial video quality for new participants as they join. + * LiveKit doesn't allow handling video quality preferences at the room level. + */ +export const useVideoResolutionSubscription = () => { + const { + userChoices: { videoSubscribeQuality }, + } = usePersistentUserChoices() + + const room = useRoomContext() + + useEffect(() => { + if (!room) return + const handleTrackPublished = ( + publication: RemoteTrackPublication, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _participant: RemoteParticipant + ) => { + // By default, the maximum quality is set to high + if ( + videoSubscribeQuality === undefined || + videoSubscribeQuality === VideoQuality.HIGH + ) + return + + if ( + publication.kind === Track.Kind.Video && + publication.source !== Track.Source.ScreenShare + ) { + publication.setVideoQuality(videoSubscribeQuality) + } + } + + room.on(RoomEvent.TrackPublished, handleTrackPublished) + + return () => { + room.off(RoomEvent.TrackPublished, handleTrackPublished) + } + }, [room, videoSubscribeQuality]) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 96d6b74a..a02b1528 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -32,6 +32,7 @@ import { RecordingStateToast } from '@/features/recording' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { useConnectionObserver } from '../hooks/useConnectionObserver' import { useNoiseReduction } from '../hooks/useNoiseReduction' +import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription' const LayoutWrapper = styled( 'div', @@ -77,6 +78,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { React.useRef(null) useConnectionObserver() + useVideoResolutionSubscription() const tracks = useTracks( [ diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx index db089f70..b301eef5 100644 --- a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -12,6 +12,7 @@ import { LocalVideoTrack, Track, VideoPresets, + VideoQuality, } from 'livekit-client' import { BackgroundProcessorFactory } from '@/features/rooms/livekit/components/blur' import { VideoResolution } from '@/stores/userChoices' @@ -61,12 +62,18 @@ type DeviceItems = Array<{ value: string; label: string }> export const VideoTab = ({ id }: VideoTabProps) => { const { t } = useTranslation('settings', { keyPrefix: 'video' }) - const { localParticipant } = useRoomContext() + const { localParticipant, remoteParticipants } = useRoomContext() const { - userChoices: { videoDeviceId, processorSerialized, videoPublishResolution }, + userChoices: { + videoDeviceId, + processorSerialized, + videoPublishResolution, + videoSubscribeQuality, + }, saveVideoInputDeviceId, saveVideoPublishResolution, + saveVideoSubscribeQuality, } = usePersistentUserChoices() const [videoElement, setVideoElement] = useState( null @@ -112,6 +119,22 @@ 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 @@ -237,6 +260,56 @@ export const VideoTab = ({ id }: VideoTabProps) => { }} /> + +
+ { + if (key == undefined) return + const selectedQuality = Number(String(key)) + saveVideoSubscribeQuality(selectedQuality) + updateExistingRemoteVideoQuality(selectedQuality) + }} + style={{ + width: '100%', + }} + /> +
+
+ ) } diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json index 4254bb59..35c7e0cc 100644 --- a/src/frontend/src/locales/de/settings.json +++ b/src/frontend/src/locales/de/settings.json @@ -43,12 +43,20 @@ "resolution": { "heading": "Auflösung", "publish": { - "label": "Wählen Sie Ihre maximale Sendeauflösung", + "label": "Wählen Sie Ihre maximale Sendeauflösung (max.)", "items": { "high": "Hohe Auflösung", "medium": "Standardauflösung", "low": "Niedrige Auflösung" } + }, + "subscribe": { + "label": "Wählen Sie Ihre Empfangsauflösung (max.)", + "items": { + "high": "Hohe Auflösung (automatisch)", + "medium": "Standardauflösung", + "low": "Niedrige Auflösung" + } } }, "permissionsRequired": "Berechtigungen erforderlich" diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index f06ddd8b..d86cd31c 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -43,12 +43,20 @@ "resolution": { "heading": "Resolution", "publish": { - "label": "Select your sending resolution (maximum)", + "label": "Select your sending resolution (max.)", "items": { "high": "High definition", "medium": "Standard definition", "low": "Low definition" } + }, + "subscribe": { + "label": "Select your reception resolution (max.)", + "items": { + "high": "High definition (automatic)", + "medium": "Standard definition", + "low": "Low definition" + } } }, "permissionsRequired": "Permissions required" diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json index c2bcb684..41a7968e 100644 --- a/src/frontend/src/locales/fr/settings.json +++ b/src/frontend/src/locales/fr/settings.json @@ -43,12 +43,20 @@ "resolution": { "heading": "Résolution", "publish": { - "label": "Sélectionner votre résolution d'envoi (maximum)", + "label": "Sélectionner votre résolution d'envoi (max.)", "items": { "high": "Haute définition", "medium": "Définition standard", "low": "Basse définition" } + }, + "subscribe": { + "label": "Sélectionner votre résolution de réception (max.)", + "items": { + "high": "Haute définition (automatique)", + "medium": "Définition standard", + "low": "Basse définition" + } } }, "permissionsRequired": "Autorisations nécessaires" diff --git a/src/frontend/src/locales/nl/settings.json b/src/frontend/src/locales/nl/settings.json index bd539b51..235382db 100644 --- a/src/frontend/src/locales/nl/settings.json +++ b/src/frontend/src/locales/nl/settings.json @@ -43,12 +43,20 @@ "resolution": { "heading": "Resolutie", "publish": { - "label": "Selecteer uw verzendresolutie (maximum)", + "label": "Selecteer uw verzendresolutie (max.)", "items": { "high": "Hoge definitie", "medium": "Standaarddefinitie", "low": "Lage definitie" } + }, + "subscribe": { + "label": "Selecteer uw ontvangstresolutie (max.)", + "items": { + "high": "Hoge definitie (automatisch)", + "medium": "Standaarddefinitie", + "low": "Lage definitie" + } } }, "permissionsRequired": "Machtigingen vereist"