daily-examples/custom/shared/components/Tile/Video.js

169 lines
4.9 KiB
JavaScript

import React, { useMemo, forwardRef, memo, useEffect, useState } from 'react';
import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { isScreenId } from '@custom/shared/contexts/participantsState';
import Bowser from 'bowser';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { shallowEqualObjects } from 'shallow-equal';
import { useDeepCompareMemo } from 'use-deep-compare';
export const Video = memo(
forwardRef(({ fit = 'contain', participantId, videoTrack, ...rest }, videoEl) => {
const { callObject } = useCallState();
const { isMobile } = useUIState();
const isLocalCam = useMemo(() => {
const localParticipant = callObject.participants()?.local;
return participantId === localParticipant.session_id && !isScreenId(participantId);
}, [callObject, participantId]);
const [isMirrored, setIsMirrored] = useState(isLocalCam);
/**
* Considered as playable video:
* - local cam feed
* - any screen share
* - remote cam feed that is subscribed and reported as playable
*/
const isPlayable = useDeepCompareMemo(
() => isLocalCam || isScreenId(participantId),
[isLocalCam, isScreenId(participantId)]
);
/**
* Memo: Chrome >= 92?
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=1232649
*/
const isChrome92 = useMemo(() => {
const { browser, platform, os } = Bowser.parse(navigator.userAgent);
return (
browser.name === 'Chrome' &&
parseInt(browser.version, 10) >= 92 &&
(platform.type === 'desktop' || os.name === 'Android')
);
}, []);
/**
* Determine if video needs to be mirrored.
*/
useEffect(() => {
if (!videoTrack) return;
const videoTrackSettings = videoTrack.getSettings();
const isUsersFrontCamera =
'facingMode' in videoTrackSettings
? isLocalCam && videoTrackSettings.facingMode === 'user'
: isLocalCam;
// only apply mirror effect to user facing camera
if (isMirrored !== isUsersFrontCamera) {
setIsMirrored(isUsersFrontCamera);
}
}, [isMirrored, isLocalCam, videoTrack]);
/**
* Handle canplay & picture-in-picture events.
*/
useEffect(() => {
const video = videoEl.current;
if (!video) return;
const handleCanPlay = () => {
if (!video.paused) return;
video.play();
};
const handleEnterPIP = () => {
video.style.transform = 'scale(1)';
};
const handleLeavePIP = () => {
video.style.transform = '';
setTimeout(() => {
if (video.paused) video.play();
}, 100);
};
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('enterpictureinpicture', handleEnterPIP);
video.addEventListener('leavepictureinpicture', handleLeavePIP);
return () => {
video.removeEventListener('canplay', handleCanPlay);
video.removeEventListener('enterpictureinpicture', handleEnterPIP);
video.removeEventListener('leavepictureinpicture', handleLeavePIP);
};
}, [isChrome92, videoEl]);
/**
* Update srcObject.
*/
useEffect(() => {
const video = videoEl.current;
if (!video || !videoTrack) return;
video.srcObject = new MediaStream([videoTrack]);
if (isChrome92) video.load();
return () => {
// clean up when unmounted
video.srcObject = null;
if (isChrome92) video.load();
};
}, [isChrome92, participantId, videoEl, videoTrack, videoTrack?.id]);
return (
<>
<video
className={classNames(fit, {
isMirrored,
isMobile,
playable: isPlayable && videoTrack?.enabled,
})}
autoPlay
muted
playsInline
ref={videoEl}
{...props}
/>
<style jsx>{`
video {
opacity: 0;
}
video.playable {
opacity: 1;
}
video.isMirrored {
transform: scale(-1, 1);
}
video.isMobile {
border-radius: 4px;
display: block;
height: 100%;
position: relative;
width: 100%;
}
video:not(.isMobile) {
height: calc(100% + 4px);
left: -2px;
object-position: center;
position: absolute;
top: -2px;
width: calc(100% + 4px);
}
video.contain {
object-fit: contain;
}
video.cover {
object-fit: cover;
}
`}</style>
</>
);
}),
(p, n) => shallowEqualObjects(p, n)
);
Video.displayName = 'Video';
Video.propTypes = {
videoTrack: PropTypes.any,
mirrored: PropTypes.bool,
participantId: PropTypes.string,
};
export default Video;