(frontend) add video resolution selector for publishing control

Introduce select option allowing users to set maximum publishing
resolution that instantly changes video track resolution for other
participants.

Essential for low bandwidth networks and follows common patterns across
major videoconferencing solutions. Users can optimize their video
quality based on network conditions without leaving the call.
This commit is contained in:
lebaudantoine
2025-08-12 19:24:17 +02:00
committed by aleb_the_flash
parent fd90d0b830
commit 803c94a80c
5 changed files with 118 additions and 2 deletions

View File

@@ -7,7 +7,14 @@ import { HStack } from '@/styled-system/jsx'
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices' import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
import { ReactNode, useCallback, useEffect, useState } from 'react' import { ReactNode, useCallback, useEffect, useState } from 'react'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { createLocalVideoTrack, LocalVideoTrack } from 'livekit-client' import {
createLocalVideoTrack,
LocalVideoTrack,
Track,
VideoPresets,
} from 'livekit-client'
import { BackgroundProcessorFactory } from '@/features/rooms/livekit/components/blur'
import { VideoResolution } from '@/stores/userChoices'
type RowWrapperProps = { type RowWrapperProps = {
heading: string heading: string
@@ -57,8 +64,9 @@ export const VideoTab = ({ id }: VideoTabProps) => {
const { localParticipant } = useRoomContext() const { localParticipant } = useRoomContext()
const { const {
userChoices: { videoDeviceId }, userChoices: { videoDeviceId, processorSerialized, videoPublishResolution },
saveVideoInputDeviceId, saveVideoInputDeviceId,
saveVideoPublishResolution,
} = usePersistentUserChoices() } = usePersistentUserChoices()
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>( const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
null null
@@ -88,6 +96,22 @@ export const VideoTab = ({ id }: VideoTabProps) => {
isDisabled: true, isDisabled: true,
} }
const handleVideoResolutionChange = async (key: 'h720' | 'h360' | 'h180') => {
const videoPublication = 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.deserializeProcessor(processorSerialized),
})
}
}
useEffect(() => { useEffect(() => {
let videoTrack: LocalVideoTrack | null = null let videoTrack: LocalVideoTrack | null = null
@@ -165,6 +189,54 @@ export const VideoTab = ({ id }: VideoTabProps) => {
)} )}
</div> </div>
</RowWrapper> </RowWrapper>
<H lvl={2}>{t('video.resolution.heading')}</H>
<HStack
gap={0}
style={{
flexWrap: 'wrap',
}}
>
<div
style={{
flex: '1 1 215px',
minWidth: 0,
}}
>
<Field
type="select"
label={t('video.resolution.publish.label')}
items={[
{
value: 'h720',
label: `${t('video.resolution.publish.items.high')} (720p)`,
},
{
value: 'h360',
label: `${t('video.resolution.publish.items.medium')} (360p)`,
},
{
value: 'h180',
label: `${t('video.resolution.publish.items.low')} (180p)`,
},
]}
selectedKey={videoPublishResolution}
onSelectionChange={async (key) => {
await handleVideoResolutionChange(key as VideoResolution)
}}
style={{
width: '100%',
}}
/>
</div>
<div
style={{
width: '10rem',
justifyContent: 'center',
display: 'flex',
paddingLeft: '1.5rem',
}}
/>
</HStack>
</TabPanel> </TabPanel>
) )
} }

View File

@@ -40,6 +40,17 @@
"disabled": "Videovorschau deaktiviert" "disabled": "Videovorschau deaktiviert"
} }
}, },
"resolution": {
"heading": "Auflösung",
"publish": {
"label": "Wählen Sie Ihre maximale Sendeauflösung",
"items": {
"high": "Hohe Auflösung",
"medium": "Standardauflösung",
"low": "Niedrige Auflösung"
}
}
},
"permissionsRequired": "Berechtigungen erforderlich" "permissionsRequired": "Berechtigungen erforderlich"
}, },
"notifications": { "notifications": {

View File

@@ -40,6 +40,17 @@
"disabled": "Video preview disabled" "disabled": "Video preview disabled"
} }
}, },
"resolution": {
"heading": "Resolution",
"publish": {
"label": "Select your sending resolution (maximum)",
"items": {
"high": "High definition",
"medium": "Standard definition",
"low": "Low definition"
}
}
},
"permissionsRequired": "Permissions required" "permissionsRequired": "Permissions required"
}, },
"notifications": { "notifications": {

View File

@@ -40,6 +40,17 @@
"disabled": "Aperçu vidéo désactivé" "disabled": "Aperçu vidéo désactivé"
} }
}, },
"resolution": {
"heading": "Résolution",
"publish": {
"label": "Sélectionner votre résolution d'envoi (maximum)",
"items": {
"high": "Haute définition",
"medium": "Définition standard",
"low": "Basse définition"
}
}
},
"permissionsRequired": "Autorisations nécessaires" "permissionsRequired": "Autorisations nécessaires"
}, },
"notifications": { "notifications": {

View File

@@ -40,6 +40,17 @@
"disabled": "Videovoorbeeld uitgeschakeld" "disabled": "Videovoorbeeld uitgeschakeld"
} }
}, },
"resolution": {
"heading": "Resolutie",
"publish": {
"label": "Selecteer uw verzendresolutie (maximum)",
"items": {
"high": "Hoge definitie",
"medium": "Standaarddefinitie",
"low": "Lage definitie"
}
}
},
"permissionsRequired": "Machtigingen vereist" "permissionsRequired": "Machtigingen vereist"
}, },
"notifications": { "notifications": {