♻️(front) refactor Effects

We need to have an agnostic component to apply effects on any video
track, enters EffectsConfiguration. It takes in input the video
track and outputs the processor applied. This is gonna be used in
order to use the same component on the pre join screen and the in
room customization side panel.
This commit is contained in:
Nathan Vasse
2025-01-20 17:59:53 +01:00
committed by NathanVss
parent 03796fcbb2
commit 9ebfc8ea29
3 changed files with 137 additions and 98 deletions

View File

@@ -8,9 +8,9 @@ import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList' import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Effects } from './Effects'
import { Chat } from '../prefabs/Chat' import { Chat } from '../prefabs/Chat'
import { Transcript } from './Transcript' import { Transcript } from './Transcript'
import { Effects } from './effects/Effects'
type StyledSidePanelProps = { type StyledSidePanelProps = {
title: string title: string

View File

@@ -0,0 +1,18 @@
import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { css } from '@/styled-system/css'
import { EffectsConfiguration } from './EffectsConfiguration'
export const Effects = () => {
const { cameraTrack } = useLocalParticipant()
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
return (
<div
className={css({
padding: '0 1.5rem',
})}
>
<EffectsConfiguration videoTrack={localCameraTrack} layout="vertical" />
</div>
)
}

View File

@@ -1,13 +1,21 @@
import { LocalVideoTrack, Track, TrackProcessor } from 'livekit-client'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { Text, P, ToggleButton, Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { import {
BackgroundBlurFactory, BackgroundBlurFactory,
BackgroundBlurProcessorInterface, BackgroundBlurProcessorInterface,
} from './blur/index' } from '../blur'
import { css } from '@/styled-system/css'
import { Text, P, ToggleButton, H } from '@/primitives'
import { HStack, styled } from '@/styled-system/jsx'
enum BlurRadius {
NONE = 0,
LIGHT = 5,
NORMAL = 10,
}
const isSupported = BackgroundBlurFactory.isSupported()
const Information = styled('div', { const Information = styled('div', {
base: { base: {
@@ -19,48 +27,45 @@ const Information = styled('div', {
}, },
}) })
enum BlurRadius { export const EffectsConfiguration = ({
NONE = 0, videoTrack,
LIGHT = 5, onSubmit,
NORMAL = 10, layout = 'horizontal',
} }: {
videoTrack: LocalVideoTrack
const isSupported = BackgroundBlurFactory.isSupported() onSubmit?: (processor?: TrackProcessor<Track.Kind.Video>) => void
layout?: 'vertical' | 'horizontal'
export const Effects = () => { }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const { isCameraEnabled, cameraTrack, localParticipant } =
useLocalParticipant()
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const [processorPending, setProcessorPending] = useState(false) const [processorPending, setProcessorPending] = useState(false)
const localCameraTrack = cameraTrack?.track as LocalVideoTrack useEffect(() => {
const videoElement = videoRef.current
if (!videoElement) return
const getProcessor = () => { const attachVideoTrack = async () => videoTrack?.attach(videoElement)
return localCameraTrack?.getProcessor() as BackgroundBlurProcessorInterface attachVideoTrack()
}
const getBlurRadius = (): BlurRadius => { return () => {
const processor = getProcessor() if (!videoElement) return
return processor?.options.blurRadius || BlurRadius.NONE videoTrack.detach(videoElement)
} }
}, [videoTrack, videoTrack?.isMuted])
const toggleBlur = async (blurRadius: number) => { const toggleBlur = async (blurRadius: number) => {
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true) if (!videoTrack) return
if (!localCameraTrack) return
setProcessorPending(true) setProcessorPending(true)
const processor = getProcessor() const processor = getProcessor()
const currentBlurRadius = getBlurRadius() const currentBlurRadius = getBlurRadius()
try { try {
if (blurRadius == currentBlurRadius && processor) { if (blurRadius == currentBlurRadius && processor) {
await localCameraTrack.stopProcessor() await videoTrack.stopProcessor()
onSubmit?.(undefined)
} else if (!processor) { } else if (!processor) {
await localCameraTrack.setProcessor( const newProcessor = BackgroundBlurFactory.getProcessor({ blurRadius })!
BackgroundBlurFactory.getProcessor({ blurRadius })! await videoTrack.setProcessor(newProcessor)
) onSubmit?.(newProcessor)
} else { } else {
processor?.update({ blurRadius }) processor?.update({ blurRadius })
} }
@@ -71,21 +76,17 @@ export const Effects = () => {
} }
} }
useEffect(() => { const getProcessor = () => {
const videoElement = videoRef.current return videoTrack?.getProcessor() as BackgroundBlurProcessorInterface
if (!videoElement) return }
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement) const getBlurRadius = (): BlurRadius => {
attachVideoTrack() const processor = getProcessor()
return processor?.options.blurRadius || BlurRadius.NONE
return () => { }
if (!videoElement) return
localCameraTrack.detach(videoElement)
}
}, [localCameraTrack, isCameraEnabled])
const isSelected = (blurRadius: BlurRadius) => { const isSelected = (blurRadius: BlurRadius) => {
return isCameraEnabled && getBlurRadius() == blurRadius return getBlurRadius() == blurRadius
} }
const tooltipLabel = (blurRadius: BlurRadius) => { const tooltipLabel = (blurRadius: BlurRadius) => {
@@ -93,55 +94,82 @@ export const Effects = () => {
} }
return ( return (
<VStack padding="0 1.5rem" overflowY="scroll"> <div
{localCameraTrack && isCameraEnabled ? ( className={css(
<video layout === 'vertical'
ref={videoRef} ? {
width="100%" display: 'flex',
muted flexDirection: 'column',
style={{ gap: '0.5rem',
transform: 'rotateY(180deg)', }
minHeight: '175px', : {
borderRadius: '8px', display: 'flex',
}} gap: '1.5rem',
/> flexDirection: 'column',
) : ( md: {
<div flexDirection: 'row',
style={{ },
width: '100%', }
height: '174px', )}
display: 'flex', >
backgroundColor: 'black', <div
justifyContent: 'center', className={css({
flexDirection: 'column', width: '100%',
}} aspectRatio: 16 / 9,
> })}
<P >
{videoTrack && !videoTrack.isMuted ? (
<video
ref={videoRef}
width="100%"
muted
style={{ style={{
color: 'white', transform: 'rotateY(180deg)',
textAlign: 'center', minHeight: '175px',
textWrap: 'balance', borderRadius: '8px',
marginBottom: 0, }}
/>
) : (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
backgroundColor: 'black',
justifyContent: 'center',
flexDirection: 'column',
}} }}
> >
{t('activateCamera')} <P
</P> style={{
</div> color: 'white',
)} textAlign: 'center',
<Div textWrap: 'balance',
alignItems={'left'} marginBottom: 0,
width={'100%'} }}
style={{ >
border: '1px solid #dadce0', {t('activateCamera')}
borderRadius: '8px', </P>
margin: '0 .625rem', </div>
padding: '0.5rem 1rem', )}
}} </div>
<div
className={css(
layout === 'horizontal'
? {
md: {
borderLeft: '1px solid #dadce0',
paddingLeft: '1.5rem',
},
}
: {}
)}
> >
<H <H
lvl={3} lvl={3}
style={{ style={{
marginBottom: '0.4rem', marginBottom: '0.4rem',
fontWeight: 'bold',
}} }}
> >
{t('heading')} {t('heading')}
@@ -173,16 +201,9 @@ export const Effects = () => {
<Text variant="sm">{t('notAvailable')}</Text> <Text variant="sm">{t('notAvailable')}</Text>
)} )}
<Information> <Information>
<Text <Text variant="sm"> {t('experimental')}</Text>
variant="sm"
style={{
textWrap: 'balance',
}}
>
{t('experimental')}
</Text>
</Information> </Information>
</Div> </div>
</VStack> </div>
) )
} }