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

View File

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

View File

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

View File

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

View File

@ -85,6 +85,7 @@ export default function LiveCameraView({
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [{ width: windowWidth, height: windowHeight }] =
useResizeObserver(window);
@ -389,48 +390,51 @@ export default function LiveCameraView({
</div>
</TooltipProvider>
</div>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
}}
contentStyle={{
position: "relative",
width: "100%",
height: "100%",
padding: "8px",
}}
>
<div
className={`flex flex-col items-center justify-center ${growClassName}`}
ref={clickOverlayRef}
onClick={handleOverlayClick}
style={{
aspectRatio: aspectRatio,
<div id="player-container" className="size-full" ref={containerRef}>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
}}
contentStyle={{
position: "relative",
width: "100%",
height: "100%",
padding: "8px",
}}
>
<LivePlayer
key={camera.name}
className={`m-1 ${fullscreen ? "*:rounded-none" : ""}`}
windowVisible
showStillWithoutActivity={false}
cameraConfig={camera}
playAudio={audio}
micEnabled={mic}
iOSCompatFullScreen={isIOS}
preferredLiveMode={preferredLiveMode}
pip={pip}
setFullResolution={setFullResolution}
/>
</div>
{camera.onvif.host != "" && (
<PtzControlPanel
camera={camera.name}
clickOverlay={clickOverlay}
setClickOverlay={setClickOverlay}
/>
)}
</TransformComponent>
<div
className={`flex flex-col items-center justify-center ${growClassName}`}
ref={clickOverlayRef}
onClick={handleOverlayClick}
style={{
aspectRatio: aspectRatio,
}}
>
<LivePlayer
key={camera.name}
className={`${fullscreen ? "*:rounded-none" : ""}`}
windowVisible
showStillWithoutActivity={false}
cameraConfig={camera}
playAudio={audio}
micEnabled={mic}
iOSCompatFullScreen={isIOS}
preferredLiveMode={preferredLiveMode}
pip={pip}
setFullResolution={setFullResolution}
containerRef={containerRef}
/>
</div>
{camera.onvif.host != "" && (
<PtzControlPanel
camera={camera.name}
clickOverlay={clickOverlay}
setClickOverlay={setClickOverlay}
/>
)}
</TransformComponent>
</div>
</div>
</TransformWrapper>
);