mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 12:15:25 +03:00
Break out live page
This commit is contained in:
parent
f6a4c2a7b3
commit
acb148547a
@ -1,5 +1,5 @@
|
|||||||
import WebRtcPlayer from "./WebRTCPlayer";
|
import WebRtcPlayer from "./WebRTCPlayer";
|
||||||
import { CameraConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage";
|
import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage";
|
||||||
import ActivityIndicator from "../ui/activity-indicator";
|
import ActivityIndicator from "../ui/activity-indicator";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
@ -11,27 +11,58 @@ import { Label } from "../ui/label";
|
|||||||
import { usePersistence } from "@/hooks/use-persistence";
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import MSEPlayer from "./MsePlayer";
|
import MSEPlayer from "./MsePlayer";
|
||||||
import JSMpegPlayer from "./JSMpegPlayer";
|
import JSMpegPlayer from "./JSMpegPlayer";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
type LivePlayerProps = {
|
type LivePlayerProps = {
|
||||||
|
className?: string;
|
||||||
cameraConfig: CameraConfig;
|
cameraConfig: CameraConfig;
|
||||||
liveMode: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Options = { [key: string]: boolean };
|
type Options = { [key: string]: boolean };
|
||||||
|
|
||||||
export default function LivePlayer({
|
export default function LivePlayer({
|
||||||
|
className,
|
||||||
cameraConfig,
|
cameraConfig,
|
||||||
liveMode,
|
|
||||||
}: LivePlayerProps) {
|
}: LivePlayerProps) {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
|
||||||
const [options, setOptions] = usePersistence(
|
const [options, setOptions] = usePersistence(
|
||||||
`${cameraConfig.name}-feed`,
|
`${cameraConfig?.name}-feed`,
|
||||||
emptyObject
|
emptyObject
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const restreamEnabled = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
cameraConfig &&
|
||||||
|
Object.keys(config.go2rtc.streams || {}).includes(
|
||||||
|
cameraConfig.live.stream_name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [config, cameraConfig]);
|
||||||
|
|
||||||
|
const defaultLiveMode = useMemo(() => {
|
||||||
|
if (cameraConfig) {
|
||||||
|
if (restreamEnabled) {
|
||||||
|
return cameraConfig.ui.live_mode || config?.ui.live_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "jsmpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [cameraConfig, restreamEnabled]);
|
||||||
|
const [liveMode, setLiveMode, sourceIsLoaded] = usePersistence(
|
||||||
|
`${cameraConfig.name}-source`,
|
||||||
|
defaultLiveMode
|
||||||
|
);
|
||||||
|
|
||||||
const handleSetOption = useCallback(
|
const handleSetOption = useCallback(
|
||||||
(id: string, value: boolean) => {
|
(id: string, value: boolean) => {
|
||||||
const newOptions = { ...options, [id]: value };
|
const newOptions = { ...options, [id]: value };
|
||||||
@ -56,10 +87,17 @@ export default function LivePlayer({
|
|||||||
setShowSettings(!showSettings);
|
setShowSettings(!showSettings);
|
||||||
}, [showSettings, setShowSettings]);
|
}, [showSettings, setShowSettings]);
|
||||||
|
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
if (liveMode == "webrtc") {
|
if (liveMode == "webrtc") {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl">
|
<div className="max-w-5xl">
|
||||||
<WebRtcPlayer camera={cameraConfig.live.stream_name} />
|
<WebRtcPlayer
|
||||||
|
className={className}
|
||||||
|
camera={cameraConfig.live.stream_name}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (liveMode == "mse") {
|
} else if (liveMode == "mse") {
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { baseUrl } from "@/api/baseUrl";
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
type WebRtcPlayerProps = {
|
type WebRtcPlayerProps = {
|
||||||
|
className?: string;
|
||||||
camera: string;
|
camera: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebRtcPlayer({
|
export default function WebRtcPlayer({
|
||||||
|
className,
|
||||||
camera,
|
camera,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -149,6 +151,7 @@ export default function WebRtcPlayer({
|
|||||||
<div>
|
<div>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
|
className={className}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
controls
|
controls
|
||||||
|
|||||||
@ -1,161 +1,80 @@
|
|||||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { usePersistence } from "@/hooks/use-persistence";
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { Event as FrigateEvent } from "@/types/event";
|
||||||
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { LuStar } from "react-icons/lu";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
function Live() {
|
function Live() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const { camera: openedCamera } = useParams();
|
|
||||||
|
|
||||||
const [camera, setCamera] = useState<string>(
|
// recent events
|
||||||
openedCamera ?? (config?.birdseye.enabled ? "birdseye" : "Select A Camera")
|
|
||||||
);
|
const now = new Date();
|
||||||
const cameraConfig = useMemo(() => {
|
now.setHours(now.getHours() - 4, 0, 0, 0);
|
||||||
return camera == "birdseye" ? undefined : config?.cameras[camera];
|
const recentTimestamp = now.getTime() / 1000;
|
||||||
}, [camera, config]);
|
const { data: events, mutate: updateEvents } = useSWR<FrigateEvent[]>([
|
||||||
const sortedCameras = useMemo(() => {
|
"events",
|
||||||
|
{ limit: 10, after: recentTimestamp },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// camera live views
|
||||||
|
|
||||||
|
const enabledCameras = useMemo<CameraConfig[]>(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(config.cameras).sort(
|
return Object.values(config.cameras);
|
||||||
(aConf, bConf) => aConf.ui.order - bConf.ui.order
|
|
||||||
);
|
|
||||||
}, [config]);
|
}, [config]);
|
||||||
const restreamEnabled = useMemo(() => {
|
|
||||||
if (!config) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (camera == "birdseye") {
|
|
||||||
return config.birdseye.restream;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
cameraConfig &&
|
<>
|
||||||
Object.keys(config.go2rtc.streams || {}).includes(
|
{events && events.length > 0 && (
|
||||||
cameraConfig.live.stream_name
|
<ScrollArea>
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [config, cameraConfig]);
|
|
||||||
const defaultLiveMode = useMemo(() => {
|
|
||||||
if (cameraConfig) {
|
|
||||||
if (restreamEnabled) {
|
|
||||||
return cameraConfig.ui.live_mode || config?.ui.live_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "jsmpeg";
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, [cameraConfig, restreamEnabled]);
|
|
||||||
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
|
|
||||||
`${camera}-source`,
|
|
||||||
camera == "birdseye" ? "jsmpeg" : defaultLiveMode
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className=" w-full">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Heading as="h2">Live</Heading>
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="mx-1">
|
{events.map((event) => {
|
||||||
<DropdownMenu>
|
return (
|
||||||
<DropdownMenuTrigger asChild>
|
<div
|
||||||
<Button className="capitalize" variant="outline">
|
className="relative rounded min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center mr-4"
|
||||||
{camera?.replaceAll("_", " ") || "Select A Camera"}
|
style={{
|
||||||
</Button>
|
backgroundImage: `url(${baseUrl}api/events/${event.id}/thumbnail.jpg)`,
|
||||||
</DropdownMenuTrigger>
|
}}
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
value={camera}
|
|
||||||
onValueChange={setCamera}
|
|
||||||
>
|
>
|
||||||
{config?.birdseye.enabled && (
|
<LuStar
|
||||||
<DropdownMenuRadioItem value="birdseye">
|
className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer"
|
||||||
Birdseye
|
//onClick={(e: Event) => onSave(e)}
|
||||||
</DropdownMenuRadioItem>
|
fill={event.retain_indefinitely ? "currentColor" : "none"}
|
||||||
)}
|
|
||||||
{sortedCameras.map((item) => (
|
|
||||||
<DropdownMenuRadioItem
|
|
||||||
className="capitalize"
|
|
||||||
key={item.name}
|
|
||||||
value={item.name}
|
|
||||||
>
|
|
||||||
{item.name.replaceAll("_", " ")}
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
<div className="mx-1">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button className="capitalize" variant="outline">
|
|
||||||
{viewSource || defaultLiveMode || "Select A Live Mode"}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuLabel>Select A Live Mode</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
value={`${viewSource}`}
|
|
||||||
onValueChange={setViewSource}
|
|
||||||
>
|
|
||||||
{restreamEnabled && (
|
|
||||||
<DropdownMenuRadioItem value="webrtc">
|
|
||||||
Webrtc
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
)}
|
|
||||||
{restreamEnabled && (
|
|
||||||
<DropdownMenuRadioItem value="mse">
|
|
||||||
MSE
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuRadioItem value="jsmpeg">
|
|
||||||
Jsmpeg
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
{camera != "birdseye" && (
|
|
||||||
<DropdownMenuRadioItem value="debug">
|
|
||||||
Debug
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{config && camera == "birdseye" && sourceIsLoaded && (
|
|
||||||
<BirdseyeLivePlayer
|
|
||||||
birdseyeConfig={config?.birdseye}
|
|
||||||
liveMode={`${viewSource ?? defaultLiveMode}`}
|
|
||||||
/>
|
/>
|
||||||
|
{event.end_time ? null : (
|
||||||
|
<div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl">
|
||||||
|
In progress
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{cameraConfig && sourceIsLoaded && (
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||||
|
{enabledCameras.map((camera) => {
|
||||||
|
return (
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
liveMode={`${viewSource ?? defaultLiveMode}`}
|
className=" rounded-2xl overflow-hidden"
|
||||||
cameraConfig={cameraConfig}
|
cameraConfig={camera}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,24 +12,24 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5000',
|
target: 'http://192.168.50.106:5000',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
'/vod': {
|
'/vod': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/clips': {
|
'/clips': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/exports': {
|
'/exports': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://localhost:5000',
|
target: 'ws://192.168.50.106:5000',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
'/live': {
|
'/live': {
|
||||||
target: 'ws://localhost:5000',
|
target: 'ws://192.168.50.106:5000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user