mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-22 08:57:20 -06:00
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:
parent
1a27c7db29
commit
2236ae5d3b
36
web/src/components/player/BirdseyeLivePlayer.tsx
Normal file
36
web/src/components/player/BirdseyeLivePlayer.tsx
Normal 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 />;
|
||||
}
|
||||
}
|
94
web/src/components/player/JSMpegPlayer.tsx
Normal file
94
web/src/components/player/JSMpegPlayer.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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") {
|
||||
|
@ -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}`}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user