diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 6c25e1b4c0f..8f88bdd18b7 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -38,134 +38,151 @@ interface NotInPipPropsType { type PropsType = BasePropsType & (InPipPropsType | NotInPipPropsType); -export const GroupCallRemoteParticipant: React.FC = props => { - const { - demuxId, - getGroupCallVideoFrameSource, - hasRemoteAudio, - hasRemoteVideo, - } = props; - - const [canvasStyles, setCanvasStyles] = useState({}); - - const remoteVideoRef = useRef(null); - const rafIdRef = useRef(null); - const frameBufferRef = useRef( - new ArrayBuffer(FRAME_BUFFER_SIZE) - ); - - const videoFrameSource = useMemo( - () => getGroupCallVideoFrameSource(demuxId), - [getGroupCallVideoFrameSource, demuxId] - ); - - const renderVideoFrame = useCallback(() => { - const canvasEl = remoteVideoRef.current; - if (!canvasEl) { - return; - } - - const context = canvasEl.getContext('2d'); - if (!context) { - return; - } +export const GroupCallRemoteParticipant: React.FC = React.memo( + props => { + const { + demuxId, + getGroupCallVideoFrameSource, + hasRemoteAudio, + hasRemoteVideo, + } = props; + + const [isWide, setIsWide] = useState(true); + + const remoteVideoRef = useRef(null); + const canvasContextRef = useRef(null); + const frameBufferRef = useRef( + new ArrayBuffer(FRAME_BUFFER_SIZE) + ); - const frameDimensions = videoFrameSource.receiveVideoFrame( - frameBufferRef.current + const videoFrameSource = useMemo( + () => getGroupCallVideoFrameSource(demuxId), + [getGroupCallVideoFrameSource, demuxId] ); - if (!frameDimensions) { - return; - } - const [frameWidth, frameHeight] = frameDimensions; - canvasEl.width = frameWidth; - canvasEl.height = frameHeight; + const renderVideoFrame = useCallback(() => { + const canvasEl = remoteVideoRef.current; + if (!canvasEl) { + return; + } + + const canvasContext = canvasContextRef.current; + if (!canvasContext) { + return; + } + + const frameDimensions = videoFrameSource.receiveVideoFrame( + frameBufferRef.current + ); + if (!frameDimensions) { + return; + } - context.putImageData( - new ImageData( - new Uint8ClampedArray( - frameBufferRef.current, - 0, - frameWidth * frameHeight * 4 + const [frameWidth, frameHeight] = frameDimensions; + if (frameWidth < 2 || frameHeight < 2) { + return; + } + + canvasEl.width = frameWidth; + canvasEl.height = frameHeight; + + canvasContext.putImageData( + new ImageData( + new Uint8ClampedArray( + frameBufferRef.current, + 0, + frameWidth * frameHeight * 4 + ), + frameWidth, + frameHeight ), - frameWidth, - frameHeight - ), - 0, - 0 - ); + 0, + 0 + ); + + setIsWide(frameWidth > frameHeight); + }, [videoFrameSource]); + + useEffect(() => { + if (!hasRemoteVideo) { + return noop; + } + + let rafId = requestAnimationFrame(tick); + + function tick() { + renderVideoFrame(); + rafId = requestAnimationFrame(tick); + } + + return () => { + cancelAnimationFrame(rafId); + }; + }, [hasRemoteVideo, renderVideoFrame, videoFrameSource]); + + let canvasStyles: CSSProperties; + let containerStyles: CSSProperties; // If our `width` and `height` props don't match the canvas's aspect ratio, we want to // fill the container. This can happen when RingRTC gives us an inaccurate // `videoAspectRatio`, or if the container is an unexpected size. - if (frameWidth > frameHeight) { - setCanvasStyles({ width: '100%' }); + if (isWide) { + canvasStyles = { width: '100%' }; } else { - setCanvasStyles({ height: '100%' }); + canvasStyles = { height: '100%' }; } - }, [videoFrameSource]); - useEffect(() => { - if (!hasRemoteVideo) { - return noop; + // TypeScript isn't smart enough to know that `isInPip` by itself disambiguates the + // types, so we have to use `props.isInPip` instead. + // eslint-disable-next-line react/destructuring-assignment + if (props.isInPip) { + containerStyles = canvasStyles; + } else { + const { top, left, width, height } = props; + + containerStyles = { + height, + left, + position: 'absolute', + top, + width, + }; } - const tick = () => { - renderVideoFrame(); - rafIdRef.current = requestAnimationFrame(tick); - }; - - rafIdRef.current = requestAnimationFrame(tick); - - return () => { - if (rafIdRef.current) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } - }; - }, [hasRemoteVideo, renderVideoFrame, videoFrameSource]); - - let containerStyles: CSSProperties; - - // TypeScript isn't smart enough to know that `isInPip` by itself disambiguates the - // types, so we have to use `props.isInPip` instead. - // eslint-disable-next-line react/destructuring-assignment - if (props.isInPip) { - containerStyles = canvasStyles; - } else { - const { top, left, width, height } = props; - - containerStyles = { - height, - left, - position: 'absolute', - top, - width, - }; + return ( +
+ {hasRemoteVideo ? ( + { + remoteVideoRef.current = canvasEl; + if (canvasEl) { + canvasContextRef.current = canvasEl.getContext('2d', { + alpha: false, + desynchronized: true, + storage: 'discardable', + } as CanvasRenderingContext2DSettings); + } else { + canvasContextRef.current = null; + } + }} + /> + ) : ( + + {/* TODO: Improve the styling here. See DESKTOP-894. */} + + + )} +
+ ); } - - return ( -
- {hasRemoteVideo ? ( - - ) : ( - - {/* TODO: Improve the styling here. See DESKTOP-894. */} - - - )} -
- ); -}; +); diff --git a/ts/components/GroupCallRemoteParticipants.tsx b/ts/components/GroupCallRemoteParticipants.tsx index bf0e96a3890..922ca325f78 100644 --- a/ts/components/GroupCallRemoteParticipants.tsx +++ b/ts/components/GroupCallRemoteParticipants.tsx @@ -162,7 +162,9 @@ export const GroupCallRemoteParticipants: React.FC = ({ ]); // 4. Lay out this arrangement on the screen. - const gridParticipantHeight = gridArrangement.scalar * MIN_RENDERED_HEIGHT; + const gridParticipantHeight = Math.floor( + gridArrangement.scalar * MIN_RENDERED_HEIGHT + ); const gridParticipantHeightWithMargin = gridParticipantHeight + PARTICIPANT_MARGIN; const gridTotalRowHeightWithMargin = @@ -181,12 +183,15 @@ export const GroupCallRemoteParticipants: React.FC = ({ const totalRowWidth = totalRowWidthWithoutMargins + PARTICIPANT_MARGIN * (remoteParticipantsInRow.length - 1); - const leftOffset = (containerDimensions.width - totalRowWidth) / 2; + const leftOffset = Math.floor( + (containerDimensions.width - totalRowWidth) / 2 + ); let rowWidthSoFar = 0; return remoteParticipantsInRow.map(remoteParticipant => { - const renderedWidth = - remoteParticipant.videoAspectRatio * gridParticipantHeight; + const renderedWidth = Math.floor( + remoteParticipant.videoAspectRatio * gridParticipantHeight + ); const left = rowWidthSoFar + leftOffset; rowWidthSoFar += renderedWidth + PARTICIPANT_MARGIN; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 2497c45f7dd..b6a68c67694 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -14579,10 +14579,10 @@ { "rule": "React-useRef", "path": "ts/components/GroupCallRemoteParticipant.js", - "line": " const rafIdRef = react_1.useRef(null);", + "line": " const canvasContextRef = react_1.useRef(null);", "lineNumber": 25, "reasonCategory": "usageTrusted", - "updated": "2020-11-17T16:24:25.480Z", + "updated": "2020-11-17T23:29:38.698Z", "reasonDetail": "Doesn't touch the DOM." }, {