wip refactor effects configuration

- add a button to explicitely enable camera when a room
display selected option even if camera is disabled, and suggest to the user to enable its
camera to allow her configure the effects

- remove useless useSyncAfterDelay
issue with rendering when enabling a processor, livekit is
working on it, it's a bad approach to fix it with a deteministic delay
it doesn't work and complexify the code

I thought about listening to some html event however, we should fix the root cause instead
This commit is contained in:
lebaudantoine
2025-07-28 18:30:51 +02:00
parent 3e94ed0a8b
commit f847f07c24
6 changed files with 85 additions and 70 deletions
@@ -8,13 +8,11 @@ import {
BackgroundOptions,
} from '../blur'
import { css } from '@/styled-system/css'
import { Text, P, ToggleButton, H } from '@/primitives'
import { Text, P, ToggleButton, H, Button } from '@/primitives'
import { styled } from '@/styled-system/jsx'
import { BlurOn } from '@/components/icons/BlurOn'
import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import { RiProhibited2Line } from '@remixicon/react'
import { FunnyEffects } from './FunnyEffects'
import { useHasFunnyEffectsAccess } from '../../hooks/useHasFunnyEffectsAccess'
@@ -50,10 +48,17 @@ export const EffectsConfiguration = ({
const videoRef = useRef<HTMLVideoElement>(null)
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const { toggle, enabled } = useTrackToggle({ source: Track.Source.Camera })
const [processorPending, setProcessorPending] = useState(false)
const processorPendingReveal = useSyncAfterDelay(processorPending)
const hasFunnyEffectsAccess = useHasFunnyEffectsAccess()
// Note: videoTrack.getProcessor() will return undefined during a transition
// but our selectedProcessor state maintains the intended value
const [selectedProcessor, setSelectedProcessor] = useState(
videoTrack?.getProcessor()
)
useEffect(() => {
const videoElement = videoRef.current
if (!videoElement) return
@@ -69,6 +74,7 @@ export const EffectsConfiguration = ({
const clearEffect = async () => {
await videoTrack.stopProcessor()
setSelectedProcessor(undefined)
onSubmit?.(undefined)
}
@@ -92,7 +98,6 @@ export const EffectsConfiguration = ({
await toggle(true, {
processor: newProcessorTmp,
})
setTimeout(() => setProcessorPending(false))
return
}
@@ -100,9 +105,10 @@ export const EffectsConfiguration = ({
await toggle(true)
}
const processor = getProcessor()
const processor = videoTrack?.getProcessor() as BackgroundProcessorInterface
try {
if (isSelected(type, options)) {
setSelectedProcessor(undefined)
// Stop processor.
await clearEffect()
} else if (!processor || processor.serialize().type !== type) {
@@ -118,28 +124,27 @@ export const EffectsConfiguration = ({
if (!BackgroundProcessorFactory.hasModernApiSupport()) {
await videoTrack.stopProcessor()
}
await videoTrack.setProcessor(newProcessor)
setSelectedProcessor(newProcessor)
onSubmit?.(newProcessor)
} else {
// Update processor.
processor?.update(options)
// We want to trigger onSubmit when options changes so the parent component is aware of it.
onSubmit?.(processor)
setSelectedProcessor(processor)
}
} catch (error) {
console.error('Error applying effect:', error)
} finally {
// Without setTimeout the DOM is not refreshing when updating the options.
setTimeout(() => setProcessorPending(false))
setProcessorPending(false)
}
}
const getProcessor = () => {
return videoTrack?.getProcessor() as BackgroundProcessorInterface
}
const isSelected = (type: ProcessorType, options: BackgroundOptions) => {
const processor = getProcessor()
if (!selectedProcessor) return false
const processor = selectedProcessor as BackgroundProcessorInterface
const processorSerialized = processor?.serialize()
return (
!!processor &&
@@ -165,7 +170,7 @@ export const EffectsConfiguration = ({
display: 'flex',
gap: '1.5rem',
flexDirection: 'column',
md: {
lg: {
flexDirection: 'row',
overflow: 'hidden',
},
@@ -175,31 +180,65 @@ export const EffectsConfiguration = ({
<div
className={css({
width: '100%',
aspectRatio: 16 / 9,
position: 'relative',
})}
>
{videoTrack && !videoTrack.isMuted ? (
<video
ref={videoRef}
width="100%"
muted
className={css(
layout === 'vertical'
? {
height: '175px',
width: '100%',
}
: {
minHeight: '175px',
maxWidth: '600px',
width: '100%',
objectFit: 'cover',
sm: {
aspectRatio: 16 / 9,
},
lg: {
maxWidth: '100%',
},
}
)}
style={{
transform: 'rotateY(180deg)',
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
borderRadius: '8px',
}}
/>
) : (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
backgroundColor: 'black',
justifyContent: 'center',
flexDirection: 'column',
borderRadius: '8px',
}}
className={css(
layout === 'vertical'
? {
height: '175px',
width: '100%',
}
: {
minHeight: '175px',
maxWidth: '600px',
width: '100%',
objectFit: 'cover',
sm: {
aspectRatio: 16 / 9,
},
lg: {
maxWidth: '100%',
},
}
)}
>
<P
style={{
@@ -211,17 +250,19 @@ export const EffectsConfiguration = ({
>
{t('activateCamera')}
</P>
</div>
)}
{processorPendingReveal && (
<div
className={css({
position: 'absolute',
right: '8px',
bottom: '8px',
})}
>
<Loader />
<Button
size="sm"
variant="tertiary"
onPress={async () => await toggle()}
aria-label={t('activateButton')}
className={css({
width: 'fit-content',
marginX: 'auto',
marginTop: '1rem',
})}
>
{t('activateButton')}
</Button>
</div>
)}
</div>
@@ -242,7 +283,7 @@ export const EffectsConfiguration = ({
{hasFunnyEffectsAccess && (
<FunnyEffects
videoTrack={videoTrack}
isPending={processorPendingReveal}
isPending={processorPending}
onPending={setProcessorPending}
/>
)}
@@ -270,8 +311,8 @@ export const EffectsConfiguration = ({
onPress={async () => {
await clearEffect()
}}
isSelected={!getProcessor()}
isDisabled={processorPendingReveal}
isSelected={!selectedProcessor}
isDisabled={processorPending || videoTrack?.isMuted}
>
<RiProhibited2Line />
</ToggleButton>
@@ -283,7 +324,7 @@ export const EffectsConfiguration = ({
tooltip={tooltipLabel(ProcessorType.BLUR, {
blurRadius: BlurRadius.LIGHT,
})}
isDisabled={processorPendingReveal}
isDisabled={processorPending || videoTrack?.isMuted}
onChange={async () =>
await toggleEffect(ProcessorType.BLUR, {
blurRadius: BlurRadius.LIGHT,
@@ -304,7 +345,7 @@ export const EffectsConfiguration = ({
tooltip={tooltipLabel(ProcessorType.BLUR, {
blurRadius: BlurRadius.NORMAL,
})}
isDisabled={processorPendingReveal}
isDisabled={processorPending || videoTrack?.isMuted}
onChange={async () =>
await toggleEffect(ProcessorType.BLUR, {
blurRadius: BlurRadius.NORMAL,
@@ -352,7 +393,7 @@ export const EffectsConfiguration = ({
tooltip={tooltipLabel(ProcessorType.VIRTUAL, {
imagePath,
})}
isDisabled={processorPendingReveal}
isDisabled={processorPending || videoTrack?.isMuted}
onChange={async () =>
await toggleEffect(ProcessorType.VIRTUAL, {
imagePath,
@@ -1,30 +0,0 @@
import { useEffect, useRef, useState } from 'react'
/**
* If value stays truthy for more than waitFor ms, syncValue takes the value of value.
* @param value
* @param waitFor
* @returns
*/
export function useSyncAfterDelay<T>(value: T, waitFor: number = 300) {
const valueRef = useRef(value)
const timeoutRef = useRef<NodeJS.Timeout>()
const [syncValue, setSyncValue] = useState<T>()
useEffect(() => {
valueRef.current = value
if (value) {
if (!timeoutRef.current) {
timeoutRef.current = setTimeout(() => {
setSyncValue(valueRef.current)
timeoutRef.current = undefined
}, waitFor)
}
} else {
setSyncValue(value)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value])
return syncValue
}
+2 -1
View File
@@ -154,7 +154,8 @@
}
},
"effects": {
"activateCamera": "Ihre Kamera ist deaktiviert. Wählen Sie eine Option, um sie zu aktivieren.",
"activateCamera": "Kamera ist aus.",
"activateButton": "Kamera einschalten",
"notAvailable": "Videoeffekte werden bald in Ihrem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit können Sie Google Chrome für die beste Leistung oder Firefox verwenden :(",
"heading": "Unschärfe",
"clear": "Effekt deaktivieren",
+2 -1
View File
@@ -154,7 +154,8 @@
}
},
"effects": {
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
"activateCamera": "Your camera is disabled.",
"activateButton": "Turn on camera",
"notAvailable": "Video effects will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome for best performance or Firefox :(",
"heading": "Blur",
"clear": "Disable effect",
+2 -1
View File
@@ -154,7 +154,8 @@
}
},
"effects": {
"activateCamera": "Votre caméra est désactivée. Choisissez une option pour l'activer.",
"activateCamera": "Caméra désactivée.",
"activateButton": "Activer la caméra",
"notAvailable": "Les effets vidéo seront bientôt disponibles sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome pour de meilleures performances ou Firefox :(",
"heading": "Flou",
"clear": "Désactiver l'effect",
+2 -1
View File
@@ -154,7 +154,8 @@
}
},
"effects": {
"activateCamera": "Uw camera is uitgeschakeld. Kies een optie om deze in te schakelen.",
"activateCamera": "Camera is uit.",
"activateButton": "Camera inschakelen",
"notAvailable": "Video-effecten zijn binnenkort beschikbaar in uw browser. We werken eraan! In de tussentijd kunt u Google Chrome gebruiken voor de beste prestaties of Firefox :(",
"heading": "Vervaging",
"clear": "Effect uitschakelen",