✨(frontend) introduce blurring effects for Chromium-based users
Implemented using MediaPipe, which is not supported on Firefox due to limitations outlined in issue #38 of the track-processors-js repo. We decided to release this first version exclusively for Chrome to quickly offer a solution to users. Future iterations will address broader compatibility. An informational message will be added to notify users that the feature is experimental. For now, a text label will be used in place of an icon.
This commit is contained in:
committed by
aleb_the_flash
parent
756be17cc7
commit
607a5fc99d
@@ -1,24 +1,72 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useLocalParticipant } from '@livekit/components-react'
|
import { useLocalParticipant } from '@livekit/components-react'
|
||||||
import { LocalVideoTrack } from 'livekit-client'
|
import { LocalVideoTrack } from 'livekit-client'
|
||||||
import { Div, P } from '@/primitives'
|
import { Text, P, ToggleButton } from '@/primitives'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { HStack, VStack } from '@/styled-system/jsx'
|
||||||
|
import {
|
||||||
|
BackgroundBlur,
|
||||||
|
BackgroundOptions,
|
||||||
|
ProcessorWrapper,
|
||||||
|
BackgroundTransformer,
|
||||||
|
} from '@livekit/track-processors'
|
||||||
|
|
||||||
|
enum BlurRadius {
|
||||||
|
NONE = 0,
|
||||||
|
LIGHT = 5,
|
||||||
|
NORMAL = 10,
|
||||||
|
}
|
||||||
|
|
||||||
export const Effects = () => {
|
export const Effects = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
|
||||||
const { isCameraEnabled, cameraTrack } = useLocalParticipant()
|
const { isCameraEnabled, cameraTrack, localParticipant } =
|
||||||
|
useLocalParticipant()
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const [processorPending, setProcessorPending] = useState(false)
|
||||||
|
|
||||||
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
|
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
|
||||||
|
|
||||||
|
const getProcessor = () => {
|
||||||
|
return localCameraTrack?.getProcessor() as ProcessorWrapper<BackgroundOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBlurRadius = (): BlurRadius => {
|
||||||
|
const processor = getProcessor()
|
||||||
|
return (
|
||||||
|
(processor?.transformer as BackgroundTransformer)?.options?.blurRadius ||
|
||||||
|
BlurRadius.NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBlur = async (blurRadius: number) => {
|
||||||
|
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true)
|
||||||
|
if (!localCameraTrack) return
|
||||||
|
|
||||||
|
setProcessorPending(true)
|
||||||
|
|
||||||
|
const processor = getProcessor()
|
||||||
|
const currentBlurRadius = getBlurRadius()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (blurRadius == currentBlurRadius && processor) {
|
||||||
|
await localCameraTrack.stopProcessor()
|
||||||
|
} else if (!processor) {
|
||||||
|
await localCameraTrack.setProcessor(BackgroundBlur(blurRadius))
|
||||||
|
} else {
|
||||||
|
await processor?.updateTransformerOptions({ blurRadius })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying blur:', error)
|
||||||
|
} finally {
|
||||||
|
setProcessorPending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const videoElement = videoRef.current
|
const videoElement = videoRef.current
|
||||||
|
if (!videoElement) return
|
||||||
|
|
||||||
const attachVideoTrack = async () => {
|
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement)
|
||||||
if (!videoElement) return
|
|
||||||
localCameraTrack?.attach(videoElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
attachVideoTrack()
|
attachVideoTrack()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -27,8 +75,16 @@ export const Effects = () => {
|
|||||||
}
|
}
|
||||||
}, [localCameraTrack, isCameraEnabled])
|
}, [localCameraTrack, isCameraEnabled])
|
||||||
|
|
||||||
|
const isSelected = (blurRadius: BlurRadius) => {
|
||||||
|
return isCameraEnabled && getBlurRadius() == blurRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipLabel = (blurRadius: BlurRadius) => {
|
||||||
|
return t(`blur.${isSelected(blurRadius) ? 'clear' : 'apply'}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Div padding="0 1.5rem">
|
<VStack padding="0 1.5rem">
|
||||||
{localCameraTrack && isCameraEnabled ? (
|
{localCameraTrack && isCameraEnabled ? (
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
@@ -63,6 +119,34 @@ export const Effects = () => {
|
|||||||
</P>
|
</P>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Div>
|
{ProcessorWrapper.isSupported ? (
|
||||||
|
<HStack>
|
||||||
|
<ToggleButton
|
||||||
|
size={'sm'}
|
||||||
|
legacyStyle
|
||||||
|
aria-label={tooltipLabel(BlurRadius.LIGHT)}
|
||||||
|
tooltip={tooltipLabel(BlurRadius.LIGHT)}
|
||||||
|
isDisabled={processorPending}
|
||||||
|
onPress={async () => await toggleBlur(BlurRadius.LIGHT)}
|
||||||
|
isSelected={isSelected(BlurRadius.LIGHT)}
|
||||||
|
>
|
||||||
|
{t('blur.light')}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
size={'sm'}
|
||||||
|
legacyStyle
|
||||||
|
aria-label={tooltipLabel(BlurRadius.NORMAL)}
|
||||||
|
tooltip={tooltipLabel(BlurRadius.NORMAL)}
|
||||||
|
isDisabled={processorPending}
|
||||||
|
onPress={async () => await toggleBlur(BlurRadius.NORMAL)}
|
||||||
|
isSelected={isSelected(BlurRadius.NORMAL)}
|
||||||
|
>
|
||||||
|
{t('blur.normal')}
|
||||||
|
</ToggleButton>
|
||||||
|
</HStack>
|
||||||
|
) : (
|
||||||
|
<Text variant="sm">{t('notAvailable')}</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"activateCamera": ""
|
"activateCamera": "",
|
||||||
|
"notAvailable": "",
|
||||||
|
"blur": {
|
||||||
|
"light": "",
|
||||||
|
"normal": "",
|
||||||
|
"apply": "",
|
||||||
|
"clear": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sidePanel": {
|
"sidePanel": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|||||||
@@ -72,7 +72,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"activateCamera": "Camera is disabled"
|
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
|
||||||
|
"notAvailable": "The blur effect will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome :(",
|
||||||
|
"blur": {
|
||||||
|
"light": "Light blur",
|
||||||
|
"normal": "Blur",
|
||||||
|
"apply": "Enable blur",
|
||||||
|
"clear": "Disable blur"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sidePanel": {
|
"sidePanel": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|||||||
@@ -72,7 +72,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"activateCamera": "Votre camera est désactivée"
|
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
|
||||||
|
"notAvailable": "L'effet de flou sera bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome :(",
|
||||||
|
"blur": {
|
||||||
|
"light": "Léger flou",
|
||||||
|
"normal": "Flou",
|
||||||
|
"apply": "Appliquer le flou",
|
||||||
|
"clear": "Désactiver le flou"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sidePanel": {
|
"sidePanel": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|||||||
Reference in New Issue
Block a user