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