Add jsmpeg support to new webUI and make birdseye default for live page (#8995)

* Add jsmpeg and make birdseye default for live view

* Fix jsmpeg

* Fix
This commit is contained in:
Nicolas Mowen 2023-12-20 07:34:27 -07:00 committed by Blake Blackshear
parent 1a27c7db29
commit 2236ae5d3b
5 changed files with 164 additions and 19 deletions

View File

@ -0,0 +1,36 @@
import WebRtcPlayer from "./WebRTCPlayer";
import { BirdseyeConfig } from "@/types/frigateConfig";
import ActivityIndicator from "../ui/activity-indicator";
import JSMpegPlayer from "./JSMpegPlayer";
type LivePlayerProps = {
birdseyeConfig: BirdseyeConfig;
liveMode: string;
};
export default function BirdseyeLivePlayer({
birdseyeConfig,
liveMode,
}: LivePlayerProps) {
if (liveMode == "webrtc") {
return (
<div className="max-w-5xl">
<WebRtcPlayer camera="birdseye" />
</div>
);
} else if (liveMode == "mse") {
return <div className="max-w-5xl">Not yet implemented</div>;
} else if (liveMode == "jsmpeg") {
return (
<div className={`max-w-[${birdseyeConfig.width}px]`}>
<JSMpegPlayer
camera="birdseye"
width={birdseyeConfig.width}
height={birdseyeConfig.height}
/>
</div>
);
} else {
<ActivityIndicator />;
}
}

View File

@ -0,0 +1,94 @@
import { baseUrl } from "@/api/baseUrl";
import { useResizeObserver } from "@/hooks/resize-observer";
// @ts-ignore we know this doesn't have types
import JSMpeg from "@cycjimmy/jsmpeg-player";
import { useEffect, useMemo, useRef } from "react";
type JSMpegPlayerProps = {
camera: string;
width: number;
height: number;
};
export default function JSMpegPlayer({
camera,
width,
height,
}: JSMpegPlayerProps) {
const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`;
const playerRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [{ width: containerWidth }] = useResizeObserver(containerRef);
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
// https://github.com/blakeblackshear/frigate/issues/1657
let scrollBarWidth = 0;
if (window.innerWidth && document.body.offsetWidth) {
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
}
const availableWidth = scrollBarWidth
? containerWidth + scrollBarWidth
: containerWidth;
const aspectRatio = width / height;
const scaledHeight = useMemo(() => {
const scaledHeight = Math.floor(availableWidth / aspectRatio);
const finalHeight = Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
return 100;
}, [availableWidth, aspectRatio, height]);
const scaledWidth = useMemo(
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
[scaledHeight, aspectRatio, scrollBarWidth]
);
useEffect(() => {
if (!playerRef.current) {
return;
}
console.log("player ref exists and creating video");
const video = new JSMpeg.VideoElement(
playerRef.current,
url,
{},
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
);
const fullscreen = () => {
if (video.els.canvas.webkitRequestFullScreen) {
video.els.canvas.webkitRequestFullScreen();
} else {
video.els.canvas.mozRequestFullScreen();
}
};
video.els.canvas.addEventListener("click", fullscreen);
return () => {
if (playerRef.current) {
try {
video.destroy();
} catch (e) {}
playerRef.current = null;
}
};
}, [url]);
return (
<div ref={containerRef}>
<div
ref={playerRef}
className={`jsmpeg`}
style={{
height: `${scaledHeight}px`,
width: `${scaledWidth}px`,
}}
/>
</div>
);
}

View File

@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import { usePersistence } from "@/hooks/use-persistence";
import JSMpegPlayer from "./JSMpegPlayer";
const emptyObject = Object.freeze({});
@ -66,7 +67,11 @@ export default function LivePlayer({
} else if (liveMode == "jsmpeg") {
return (
<div className={`max-w-[${cameraConfig.detect.width}px]`}>
Not Yet Implemented
<JSMpegPlayer
camera={cameraConfig.name}
width={cameraConfig.detect.width}
height={cameraConfig.detect.height}
/>
</div>
);
} else if (liveMode == "debug") {

View File

@ -1,3 +1,4 @@
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
import LivePlayer from "@/components/player/LivePlayer";
import { Button } from "@/components/ui/button";
import {
@ -21,18 +22,19 @@ function Live() {
const { camera: openedCamera } = useParams();
const [camera, setCamera] = useState<string>(
openedCamera ?? "Select A Camera"
openedCamera ?? (config?.birdseye.enabled ? "birdseye" : "Select A Camera")
);
const cameraConfig = useMemo(() => {
return config?.cameras[camera];
return camera == "birdseye" ? undefined : config?.cameras[camera];
}, [camera, config]);
const sortedCameras = useMemo(() => {
if (!config) {
return [];
}
return Object.values(config.cameras)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
return Object.values(config.cameras).sort(
(aConf, bConf) => aConf.ui.order - bConf.ui.order
);
}, [config]);
const restreamEnabled = useMemo(() => {
return (
@ -56,7 +58,7 @@ function Live() {
}, [cameraConfig, restreamEnabled]);
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
`${camera}-source`,
defaultLiveMode
camera == "birdseye" ? "jsmpeg" : defaultLiveMode
);
return (
@ -74,7 +76,7 @@ function Live() {
<DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={camera} onValueChange={setCamera}>
{(sortedCameras).map((item) => (
{sortedCameras.map((item) => (
<DropdownMenuRadioItem
className="capitalize"
key={item.name}
@ -114,6 +116,12 @@ function Live() {
</DropdownMenu>
</div>
</div>
{config && camera == "birdseye" && sourceIsLoaded && (
<BirdseyeLivePlayer
birdseyeConfig={config?.birdseye}
liveMode={`${viewSource ?? defaultLiveMode}`}
/>
)}
{cameraConfig && sourceIsLoaded && (
<LivePlayer
liveMode={`${viewSource ?? defaultLiveMode}`}

View File

@ -1,8 +1,8 @@
export interface UiConfig {
timezone?: string;
time_format?: 'browser' | '12hour' | '24hour';
date_style?: 'full' | 'long' | 'medium' | 'short';
time_style?: 'full' | 'long' | 'medium' | 'short';
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
live_mode?: string;
use_experimental?: boolean;
@ -10,6 +10,15 @@ export interface UiConfig {
order: number;
}
export interface BirdseyeConfig {
enabled: boolean;
height: number;
mode: "objects" | "continuous" | "motion";
quality: number;
restream: boolean;
width: number;
}
export interface CameraConfig {
audio: {
enabled: boolean;
@ -23,7 +32,7 @@ export interface CameraConfig {
best_image_timeout: number;
birdseye: {
enabled: boolean;
mode: "objects";
mode: "objects" | "continuous" | "motion";
order: number;
};
detect: {
@ -204,14 +213,7 @@ export interface FrigateConfig {
num_threads: number;
};
birdseye: {
enabled: boolean;
height: number;
mode: "objects";
quality: number;
restream: boolean;
width: number;
};
birdseye: BirdseyeConfig;
cameras: {
[cameraName: string]: CameraConfig;