Make jsmpeg players fully responsive (#11567)

* make jsmpeg canvas responsive

* make birdseye responsive too
This commit is contained in:
Josh Hawkins 2024-05-27 17:18:04 -05:00 committed by GitHub
parent 5900a2a4ba
commit c1330704cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 142 additions and 76 deletions

View File

@ -5,12 +5,14 @@ import JSMpegPlayer from "./JSMpegPlayer";
import MSEPlayer from "./MsePlayer"; import MSEPlayer from "./MsePlayer";
import { LivePlayerMode } from "@/types/live"; import { LivePlayerMode } from "@/types/live";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import React from "react";
type LivePlayerProps = { type LivePlayerProps = {
className?: string; className?: string;
birdseyeConfig: BirdseyeConfig; birdseyeConfig: BirdseyeConfig;
liveMode: LivePlayerMode; liveMode: LivePlayerMode;
onClick?: () => void; onClick?: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function BirdseyeLivePlayer({ export default function BirdseyeLivePlayer({
@ -18,6 +20,7 @@ export default function BirdseyeLivePlayer({
birdseyeConfig, birdseyeConfig,
liveMode, liveMode,
onClick, onClick,
containerRef,
}: LivePlayerProps) { }: LivePlayerProps) {
let player; let player;
if (liveMode == "webrtc") { if (liveMode == "webrtc") {
@ -50,6 +53,7 @@ export default function BirdseyeLivePlayer({
camera="birdseye" camera="birdseye"
width={birdseyeConfig.width} width={birdseyeConfig.width}
height={birdseyeConfig.height} height={birdseyeConfig.height}
containerRef={containerRef}
/> />
); );
} else { } else {

View File

@ -1,13 +1,15 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { useResizeObserver } from "@/hooks/resize-observer";
// @ts-expect-error we know this doesn't have types // @ts-expect-error we know this doesn't have types
import JSMpeg from "@cycjimmy/jsmpeg-player"; import JSMpeg from "@cycjimmy/jsmpeg-player";
import { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
type JSMpegPlayerProps = { type JSMpegPlayerProps = {
className?: string; className?: string;
camera: string; camera: string;
width: number; width: number;
height: number; height: number;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function JSMpegPlayer({ export default function JSMpegPlayer({
@ -15,10 +17,57 @@ export default function JSMpegPlayer({
width, width,
height, height,
className, className,
containerRef,
}: JSMpegPlayerProps) { }: JSMpegPlayerProps) {
const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`; const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`;
const playerRef = useRef<HTMLDivElement | null>(null); const playerRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const internalContainerRef = useRef<HTMLDivElement | null>(null);
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef ?? internalContainerRef);
const stretch = true;
const aspectRatio = width / height;
const fitAspect = useMemo(
() => containerWidth / containerHeight,
[containerWidth, containerHeight],
);
const scaledHeight = useMemo(() => {
if (containerRef?.current && width && height) {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(
Math.min(containerHeight, containerRef.current?.clientHeight),
)
: aspectRatio > fitAspect
? Math.floor(containerWidth / aspectRatio)
: Math.floor(containerWidth / aspectRatio) / 1.5;
const finalHeight = stretch
? scaledHeight
: Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
}
}, [
aspectRatio,
containerWidth,
containerHeight,
fitAspect,
height,
width,
stretch,
containerRef,
]);
const scaledWidth = useMemo(() => {
if (aspectRatio && scaledHeight) {
return Math.ceil(scaledHeight * aspectRatio);
}
}, [scaledHeight, aspectRatio]);
useEffect(() => { useEffect(() => {
if (!playerRef.current) { if (!playerRef.current) {
@ -28,7 +77,7 @@ export default function JSMpegPlayer({
const video = new JSMpeg.VideoElement( const video = new JSMpeg.VideoElement(
playerRef.current, playerRef.current,
url, url,
{}, { canvas: "#video-canvas" },
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }, { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 },
); );
@ -44,12 +93,16 @@ export default function JSMpegPlayer({
}, [url]); }, [url]);
return ( return (
<div className={className} ref={containerRef}> <div className={className} ref={internalContainerRef}>
<div <div ref={playerRef} className="jsmpeg">
ref={playerRef} <canvas
className="jsmpeg h-full" id="video-canvas"
style={{ aspectRatio: width / height }} style={{
/> width: scaledWidth ?? width,
height: scaledHeight ?? height,
}}
></canvas>
</div>
</div> </div>
); );
} }

View File

@ -28,6 +28,7 @@ type LivePlayerProps = {
pip?: boolean; pip?: boolean;
onClick?: () => void; onClick?: () => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>; setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function LivePlayer({ export default function LivePlayer({
@ -43,6 +44,7 @@ export default function LivePlayer({
pip, pip,
onClick, onClick,
setFullResolution, setFullResolution,
containerRef,
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
@ -138,10 +140,11 @@ export default function LivePlayer({
if (cameraActive || !showStillWithoutActivity) { if (cameraActive || !showStillWithoutActivity) {
player = ( player = (
<JSMpegPlayer <JSMpegPlayer
className="flex size-full justify-center overflow-hidden rounded-lg md:rounded-2xl" className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl"
camera={cameraConfig.live.stream_name} camera={cameraConfig.live.stream_name}
width={cameraConfig.detect.width} width={cameraConfig.detect.width}
height={cameraConfig.detect.height} height={cameraConfig.detect.height}
containerRef={containerRef}
/> />
); );
} else { } else {
@ -156,9 +159,7 @@ export default function LivePlayer({
ref={cameraRef} ref={cameraRef}
data-camera={cameraConfig.name} data-camera={cameraConfig.name}
className={cn( className={cn(
"relative flex justify-center", "relative flex w-full cursor-pointer justify-center outline",
liveMode === "jsmpeg" ? "size-full" : "w-full",
"cursor-pointer outline",
activeTracking activeTracking
? "outline-3 rounded-lg shadow-severity_alert outline-severity_alert md:rounded-2xl" ? "outline-3 rounded-lg shadow-severity_alert outline-severity_alert md:rounded-2xl"
: "outline-0 outline-background", : "outline-0 outline-background",

View File

@ -23,6 +23,7 @@ export default function LiveBirdseyeView() {
const navigate = useNavigate(); const navigate = useNavigate();
const { isPortrait } = useMobileOrientation(); const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null); const mainRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [{ width: windowWidth, height: windowHeight }] = const [{ width: windowWidth, height: windowHeight }] =
useResizeObserver(window); useResizeObserver(window);
@ -75,7 +76,7 @@ export default function LiveBirdseyeView() {
return "absolute inset-y-2 left-[50%] -translate-x-[50%]"; return "absolute inset-y-2 left-[50%] -translate-x-[50%]";
} }
} else { } else {
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; return "absolute top-0 bottom-0 left-[50%] -translate-x-[50%]";
} }
}, [cameraAspectRatio, fullscreen, isPortrait]); }, [cameraAspectRatio, fullscreen, isPortrait]);
@ -159,6 +160,7 @@ export default function LiveBirdseyeView() {
</div> </div>
</TooltipProvider> </TooltipProvider>
</div> </div>
<div id="player-container" className="size-full" ref={containerRef}>
<TransformComponent <TransformComponent
wrapperStyle={{ wrapperStyle={{
width: "100%", width: "100%",
@ -177,13 +179,15 @@ export default function LiveBirdseyeView() {
}} }}
> >
<BirdseyeLivePlayer <BirdseyeLivePlayer
className="h-full" className={`${fullscreen ? "*:rounded-none" : ""}`}
birdseyeConfig={config.birdseye} birdseyeConfig={config.birdseye}
liveMode={preferredLiveMode} liveMode={preferredLiveMode}
containerRef={containerRef}
/> />
</div> </div>
</TransformComponent> </TransformComponent>
</div> </div>
</div>
</TransformWrapper> </TransformWrapper>
); );
} }

View File

@ -85,6 +85,7 @@ export default function LiveCameraView({
const navigate = useNavigate(); const navigate = useNavigate();
const { isPortrait } = useMobileOrientation(); const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null); const mainRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [{ width: windowWidth, height: windowHeight }] = const [{ width: windowWidth, height: windowHeight }] =
useResizeObserver(window); useResizeObserver(window);
@ -389,6 +390,7 @@ export default function LiveCameraView({
</div> </div>
</TooltipProvider> </TooltipProvider>
</div> </div>
<div id="player-container" className="size-full" ref={containerRef}>
<TransformComponent <TransformComponent
wrapperStyle={{ wrapperStyle={{
width: "100%", width: "100%",
@ -411,7 +413,7 @@ export default function LiveCameraView({
> >
<LivePlayer <LivePlayer
key={camera.name} key={camera.name}
className={`m-1 ${fullscreen ? "*:rounded-none" : ""}`} className={`${fullscreen ? "*:rounded-none" : ""}`}
windowVisible windowVisible
showStillWithoutActivity={false} showStillWithoutActivity={false}
cameraConfig={camera} cameraConfig={camera}
@ -421,6 +423,7 @@ export default function LiveCameraView({
preferredLiveMode={preferredLiveMode} preferredLiveMode={preferredLiveMode}
pip={pip} pip={pip}
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
containerRef={containerRef}
/> />
</div> </div>
{camera.onvif.host != "" && ( {camera.onvif.host != "" && (
@ -432,6 +435,7 @@ export default function LiveCameraView({
)} )}
</TransformComponent> </TransformComponent>
</div> </div>
</div>
</TransformWrapper> </TransformWrapper>
); );
} }