diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 3b9f8f87c7..1b816be680 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -24,6 +24,8 @@ import { ContactName } from './conversation/ContactName'; import { useIntersectionObserver } from '../util/hooks'; import { MAX_FRAME_SIZE } from '../calling/constants'; +const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000; + type BasePropsType = { getFrameBuffer: () => ArrayBuffer; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; @@ -68,12 +70,24 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( videoAspectRatio, } = props.remoteParticipant; + const [hasReceivedVideoRecently, setHasReceivedVideoRecently] = useState( + false + ); const [isWide, setIsWide] = useState( videoAspectRatio ? videoAspectRatio >= 1 : true ); const [hasHover, setHover] = useState(false); const [showBlockInfo, setShowBlockInfo] = useState(false); + // We have some state (`hasReceivedVideoRecently`) and this ref. We can't have a + // single state value like `lastReceivedVideoAt` because (1) it won't automatically + // trigger a re-render after the video has become stale (2) it would cause a full + // re-render of the component for every frame, which is way too often. + // + // Alternatively, we could create a timeout that's reset every time we get a video + // frame (perhaps using a debounce function), but that becomes harder to clean up + // when the component unmounts. + const lastReceivedVideoAt = useRef(-Infinity); const remoteVideoRef = useRef(null); const canvasContextRef = useRef(null); @@ -85,12 +99,22 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( ? intersectionObserverEntry.isIntersecting : true; + const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible; + const hasVideoToShow = wantsToShowVideo && hasReceivedVideoRecently; + const videoFrameSource = useMemo( () => getGroupCallVideoFrameSource(demuxId), [getGroupCallVideoFrameSource, demuxId] ); const renderVideoFrame = useCallback(() => { + if ( + Date.now() - lastReceivedVideoAt.current > + MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES + ) { + setHasReceivedVideoRecently(false); + } + const canvasEl = remoteVideoRef.current; if (!canvasEl) { return; @@ -133,9 +157,18 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( 0 ); + lastReceivedVideoAt.current = Date.now(); + + setHasReceivedVideoRecently(true); setIsWide(frameWidth > frameHeight); }, [getFrameBuffer, videoFrameSource]); + useEffect(() => { + if (!hasRemoteVideo) { + setHasReceivedVideoRecently(false); + } + }, [hasRemoteVideo]); + useEffect(() => { if (!hasRemoteVideo || !isVisible) { return noop; @@ -198,7 +231,6 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( } const showHover = hasHover && !props.isInPip; - const canShowVideo = hasRemoteVideo && !isBlocked && isVisible; return ( <> @@ -254,10 +286,16 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( /> )} - {canShowVideo ? ( + {wantsToShowVideo && ( { remoteVideoRef.current = canvasEl; if (canvasEl) { @@ -271,7 +309,8 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( } }} /> - ) : ( + )} + {!hasVideoToShow && ( {isBlocked ? ( <> diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 3b4b94e25a..13dee5be6b 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13570,6 +13570,22 @@ "updated": "2020-11-17T23:29:38.698Z", "reasonDetail": "Doesn't touch the DOM." }, + { + "rule": "React-useRef", + "path": "ts/components/GroupCallRemoteParticipant.js", + "line": " const lastReceivedVideoAt = react_1.useRef(-Infinity);", + "reasonCategory": "usageTrusted", + "updated": "2021-06-17T20:46:02.342Z", + "reasonDetail": "Doesn't reference the DOM." + }, + { + "rule": "React-useRef", + "path": "ts/components/GroupCallRemoteParticipant.tsx", + "line": " const lastReceivedVideoAt = useRef(-Infinity);", + "reasonCategory": "usageTrusted", + "updated": "2021-06-17T20:46:02.342Z", + "reasonDetail": "Doesn't reference the DOM." + }, { "rule": "React-useRef", "path": "ts/components/GroupDescriptionInput.js",