diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index ff73e047fa..d4d884987e 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -248,10 +248,18 @@ export default function LivePlayer({ // remounts them in the same render and the new player is hidden until it // plays. jsmpeg plays the camera, not the stream, and keeps playing const [renderedStream, setRenderedStream] = useState(streamName); + const [switchFrame, setSwitchFrame] = useState(); if (renderedStream !== streamName) { setRenderedStream(streamName); if (preferredLiveMode !== "jsmpeg") { setLiveReady(false); + + // the old video is still in the DOM until this render commits, so its + // last frame can cover the gap until the new stream plays + const frame = captureVideoFrame(overlayRef.current); + if (frame) { + setSwitchFrame(frame); + } } } @@ -263,6 +271,7 @@ export default function LivePlayer({ const playerIsPlaying = useCallback(() => { setLiveReady(true); + setSwitchFrame(undefined); }, []); // enabled states @@ -416,6 +425,12 @@ export default function LivePlayer({ )} {player} + {switchFrame && !liveReady && ( +
node?.replaceChildren(switchFrame)} + /> + )} {cameraEnabled && !offline && (!showStillWithoutActivity || isReEnabling) && @@ -582,3 +597,19 @@ export default function LivePlayer({
); } + +function captureVideoFrame( + container: HTMLElement | null, +): HTMLCanvasElement | undefined { + const video = container?.querySelector("video"); + if (!video || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { + return undefined; + } + + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.className = "size-full object-contain"; + canvas.getContext("2d")?.drawImage(video, 0, 0); + return canvas; +} diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 8252a6692e..0e107f8153 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -873,9 +873,12 @@ export default function LiveCameraView({ growClassName, )} ref={clickOverlayRef} - style={{ - aspectRatio: constrainedAspectRatio, - }} + style={ + { + aspectRatio: constrainedAspectRatio, + "--camera-aspect": cameraAspectRatio, + } as React.CSSProperties + } > {clickOverlay && overlaySize.width > 0 && (