// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { isVideoTransformDevice, VideoInputDevice } from 'amazon-chime-sdk-js'; import isEqual from 'lodash.isequal'; import React, { ReactNode, useEffect, useState } from 'react'; import { useBackgroundBlur } from '../../../providers/BackgroundBlurProvider'; import { useVideoInputs } from '../../../providers/DevicesProvider'; import { useLocalVideo } from '../../../providers/LocalVideoProvider'; import { useLogger } from '../../../providers/LoggerProvider'; import { useMeetingManager } from '../../../providers/MeetingProvider'; import { DeviceType } from '../../../types'; import { isOptionActive } from '../../../utils/device-utils'; import useMemoCompare from '../../../utils/use-memo-compare'; import { ControlBarButton } from '../../ui/ControlBar/ControlBarButton'; import { Camera, Spinner } from '../../ui/icons'; import PopOverItem from '../../ui/PopOver/PopOverItem'; import PopOverSeparator from '../../ui/PopOver/PopOverSeparator'; import { BaseSdkProps } from '../Base'; interface Props extends BaseSdkProps { /** The label that will be shown for video input control, it defaults to `Video`. */ label?: string; /** The label that will be shown for the background blur button. */ backgroundBlurLabel?: string; } export const VideoInputBackgroundBlurControl: React.FC< React.PropsWithChildren<Props> > = ({ label = 'Video', backgroundBlurLabel = 'Enable Background Blur', ...rest }) => { const logger = useLogger(); const meetingManager = useMeetingManager(); const { devices, selectedDevice } = useVideoInputs(); const { isVideoEnabled, toggleVideo } = useLocalVideo(); const { isBackgroundBlurSupported, createBackgroundBlurDevice } = useBackgroundBlur(); const [isLoading, setIsLoading] = useState(false); const [ dropdownWithVideoTransformOptions, setDropdownWithVideoTransformOptions, ] = useState<ReactNode[] | null>(null); const videoDevices: DeviceType[] = useMemoCompare( devices, (prev: DeviceType[], next: DeviceType[]): boolean => isEqual(prev, next) ); const toggleBackgroundBlur = async (): Promise<void> => { if (isLoading || !selectedDevice) { return; } try { setIsLoading(true); let current: VideoInputDevice; if (!isVideoTransformDevice(selectedDevice)) { // Enable video transform on the non-transformed device current = await createBackgroundBlurDevice(selectedDevice); logger.info( `Video filter turned on - selecting video transform device: ${JSON.stringify( current )}` ); } else { // switch back to the inner device current = await selectedDevice.intrinsicDevice(); logger.info( `Video filter was turned off - selecting inner device: ${JSON.stringify( current )}` ); } // If we're currently using a video transform device, and a non-video transform device is selected // then the video transform device will be stopped automatically await meetingManager.startVideoInputDevice(current); } catch (error) { logger.error('Failed to toggle Background Blur'); } finally { setIsLoading(false); } }; useEffect(() => { const handleClick = async (deviceId: string): Promise<void> => { try { // If background blur is on, then re-use the same video transform pipeline, but replace the inner device // If background blur is not on, then do a normal video selection let newDevice: VideoInputDevice = deviceId; if (isVideoTransformDevice(selectedDevice) && !isLoading) { setIsLoading(true); if ('chooseNewInnerDevice' in selectedDevice) { // @ts-ignore newDevice = selectedDevice.chooseNewInnerDevice(deviceId); } else { logger.error('Transform device cannot choose new inner device'); } } if (isVideoEnabled) { await meetingManager.startVideoInputDevice(newDevice); } else { meetingManager.selectVideoInputDevice(newDevice); } } catch (error) { logger.error('Failed to select video input device'); } finally { setIsLoading(false); } }; const getDropdownWithVideoTransformOptions = async (): Promise<void> => { const deviceOptions: ReactNode[] = await Promise.all( videoDevices.map(async (device) => ( <PopOverItem key={device.deviceId} checked={await isOptionActive(selectedDevice, device.deviceId)} onClick={async (): Promise<void> => handleClick(device.deviceId)} > <span>{device.label}</span> </PopOverItem> )) ); if (isBackgroundBlurSupported) { const videoTransformOptions: ReactNode = ( <PopOverItem key="videoinput" checked={isVideoTransformDevice(selectedDevice)} disabled={isLoading} onClick={toggleBackgroundBlur} > <> {isLoading && <Spinner width="1.5rem" height="1.5rem" />} {backgroundBlurLabel} </> </PopOverItem> ); deviceOptions.push(<PopOverSeparator key="separator" />); deviceOptions.push(videoTransformOptions); } setDropdownWithVideoTransformOptions(deviceOptions); }; getDropdownWithVideoTransformOptions(); }, [ createBackgroundBlurDevice, meetingManager, meetingManager.startVideoInputDevice, videoDevices, isLoading, isVideoEnabled, selectedDevice, isBackgroundBlurSupported, ]); return ( <ControlBarButton icon={<Camera disabled={!isVideoEnabled} />} onClick={toggleVideo} label={label} {...rest} > {dropdownWithVideoTransformOptions} </ControlBarButton> ); }; export default VideoInputBackgroundBlurControl;