mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01: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 { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import { usePersistence } from "@/hooks/use-persistence";
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
|
import JSMpegPlayer from "./JSMpegPlayer";
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
@ -66,7 +67,11 @@ export default function LivePlayer({
|
|||||||
} else if (liveMode == "jsmpeg") {
|
} else if (liveMode == "jsmpeg") {
|
||||||
return (
|
return (
|
||||||
<div className={`max-w-[${cameraConfig.detect.width}px]`}>
|
<div className={`max-w-[${cameraConfig.detect.width}px]`}>
|
||||||
Not Yet Implemented
|
<JSMpegPlayer
|
||||||
|
camera={cameraConfig.name}
|
||||||
|
width={cameraConfig.detect.width}
|
||||||
|
height={cameraConfig.detect.height}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (liveMode == "debug") {
|
} else if (liveMode == "debug") {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -21,18 +22,19 @@ function Live() {
|
|||||||
const { camera: openedCamera } = useParams();
|
const { camera: openedCamera } = useParams();
|
||||||
|
|
||||||
const [camera, setCamera] = useState<string>(
|
const [camera, setCamera] = useState<string>(
|
||||||
openedCamera ?? "Select A Camera"
|
openedCamera ?? (config?.birdseye.enabled ? "birdseye" : "Select A Camera")
|
||||||
);
|
);
|
||||||
const cameraConfig = useMemo(() => {
|
const cameraConfig = useMemo(() => {
|
||||||
return config?.cameras[camera];
|
return camera == "birdseye" ? undefined : config?.cameras[camera];
|
||||||
}, [camera, config]);
|
}, [camera, config]);
|
||||||
const sortedCameras = useMemo(() => {
|
const sortedCameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(config.cameras)
|
return Object.values(config.cameras).sort(
|
||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
(aConf, bConf) => aConf.ui.order - bConf.ui.order
|
||||||
|
);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
const restreamEnabled = useMemo(() => {
|
const restreamEnabled = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@ -56,7 +58,7 @@ function Live() {
|
|||||||
}, [cameraConfig, restreamEnabled]);
|
}, [cameraConfig, restreamEnabled]);
|
||||||
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
|
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
|
||||||
`${camera}-source`,
|
`${camera}-source`,
|
||||||
defaultLiveMode
|
camera == "birdseye" ? "jsmpeg" : defaultLiveMode
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,7 +76,7 @@ function Live() {
|
|||||||
<DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
|
<DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuRadioGroup value={camera} onValueChange={setCamera}>
|
<DropdownMenuRadioGroup value={camera} onValueChange={setCamera}>
|
||||||
{(sortedCameras).map((item) => (
|
{sortedCameras.map((item) => (
|
||||||
<DropdownMenuRadioItem
|
<DropdownMenuRadioItem
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
key={item.name}
|
key={item.name}
|
||||||
@ -114,6 +116,12 @@ function Live() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{config && camera == "birdseye" && sourceIsLoaded && (
|
||||||
|
<BirdseyeLivePlayer
|
||||||
|
birdseyeConfig={config?.birdseye}
|
||||||
|
liveMode={`${viewSource ?? defaultLiveMode}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{cameraConfig && sourceIsLoaded && (
|
{cameraConfig && sourceIsLoaded && (
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
liveMode={`${viewSource ?? defaultLiveMode}`}
|
liveMode={`${viewSource ?? defaultLiveMode}`}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
export interface UiConfig {
|
export interface UiConfig {
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
time_format?: 'browser' | '12hour' | '24hour';
|
time_format?: "browser" | "12hour" | "24hour";
|
||||||
date_style?: 'full' | 'long' | 'medium' | 'short';
|
date_style?: "full" | "long" | "medium" | "short";
|
||||||
time_style?: 'full' | 'long' | 'medium' | 'short';
|
time_style?: "full" | "long" | "medium" | "short";
|
||||||
strftime_fmt?: string;
|
strftime_fmt?: string;
|
||||||
live_mode?: string;
|
live_mode?: string;
|
||||||
use_experimental?: boolean;
|
use_experimental?: boolean;
|
||||||
@ -10,6 +10,15 @@ export interface UiConfig {
|
|||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BirdseyeConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
height: number;
|
||||||
|
mode: "objects" | "continuous" | "motion";
|
||||||
|
quality: number;
|
||||||
|
restream: boolean;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CameraConfig {
|
export interface CameraConfig {
|
||||||
audio: {
|
audio: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -23,7 +32,7 @@ export interface CameraConfig {
|
|||||||
best_image_timeout: number;
|
best_image_timeout: number;
|
||||||
birdseye: {
|
birdseye: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
mode: "objects";
|
mode: "objects" | "continuous" | "motion";
|
||||||
order: number;
|
order: number;
|
||||||
};
|
};
|
||||||
detect: {
|
detect: {
|
||||||
@ -204,14 +213,7 @@ export interface FrigateConfig {
|
|||||||
num_threads: number;
|
num_threads: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
birdseye: {
|
birdseye: BirdseyeConfig;
|
||||||
enabled: boolean;
|
|
||||||
height: number;
|
|
||||||
mode: "objects";
|
|
||||||
quality: number;
|
|
||||||
restream: boolean;
|
|
||||||
width: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
cameras: {
|
cameras: {
|
||||||
[cameraName: string]: CameraConfig;
|
[cameraName: string]: CameraConfig;
|
||||||
|
Loading…
Reference in New Issue
Block a user