mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
Use dynamic imports to reduce initial load times
Remove videojs
This commit is contained in:
parent
0e8350ea7f
commit
664d88d25d
15
web/package-lock.json
generated
15
web/package-lock.json
generated
@ -60,8 +60,7 @@
|
|||||||
"tailwind-merge": "^2.1.0",
|
"tailwind-merge": "^2.1.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.8.0",
|
"vaul": "^0.8.0",
|
||||||
"video.js": "^8.6.1",
|
"video.js": "^8.10.0",
|
||||||
"videojs-playlist": "^5.1.0",
|
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
@ -8187,18 +8186,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz",
|
||||||
"integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w=="
|
"integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w=="
|
||||||
},
|
},
|
||||||
"node_modules/videojs-playlist": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-p5ohld6Kom9meYCcEVYj0JVS2MBL2XxMiU+IDB/xKpDOspFAHrERHrZEBoiJZc/mCfHixZBNgj1vWRgYsVVsrw==",
|
|
||||||
"dependencies": {
|
|
||||||
"global": "^4.3.2",
|
|
||||||
"video.js": "^6 || ^7 || ^8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/videojs-vtt.js": {
|
"node_modules/videojs-vtt.js": {
|
||||||
"version": "0.15.5",
|
"version": "0.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"hls.js": "^1.5.7",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
@ -65,8 +66,6 @@
|
|||||||
"tailwind-merge": "^2.1.0",
|
"tailwind-merge": "^2.1.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.8.0",
|
"vaul": "^0.8.0",
|
||||||
"video.js": "^8.6.1",
|
|
||||||
"videojs-playlist": "^5.1.0",
|
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,20 +2,23 @@ import Providers from "@/context/providers";
|
|||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import Wrapper from "@/components/Wrapper";
|
import Wrapper from "@/components/Wrapper";
|
||||||
import Sidebar from "@/components/navigation/Sidebar";
|
import Sidebar from "@/components/navigation/Sidebar";
|
||||||
import Live from "@/pages/Live";
|
|
||||||
import Export from "@/pages/Export";
|
|
||||||
import Storage from "@/pages/Storage";
|
|
||||||
import System from "@/pages/System";
|
|
||||||
import ConfigEditor from "@/pages/ConfigEditor";
|
|
||||||
import Logs from "@/pages/Logs";
|
|
||||||
import NoMatch from "@/pages/NoMatch";
|
|
||||||
import Settings from "@/pages/Settings";
|
|
||||||
import UIPlayground from "./pages/UIPlayground";
|
|
||||||
import Events from "./pages/Events";
|
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import Statusbar from "./components/Statusbar";
|
import Statusbar from "./components/Statusbar";
|
||||||
import Bottombar from "./components/navigation/Bottombar";
|
import Bottombar from "./components/navigation/Bottombar";
|
||||||
import SubmitPlus from "./pages/SubmitPlus";
|
import { Suspense, lazy } from "react";
|
||||||
|
|
||||||
|
const Live = lazy(() => import("@/pages/Live"));
|
||||||
|
const Events = lazy(() => import("@/pages/Events"));
|
||||||
|
const Export = lazy(() => import("@/pages/Export"));
|
||||||
|
const Storage = lazy(() => import("@/pages/Storage"));
|
||||||
|
const SubmitPlus = lazy(() => import("@/pages/SubmitPlus"));
|
||||||
|
const ConfigEditor = lazy(() => import("@/pages/ConfigEditor"));
|
||||||
|
const System = lazy(() => import("@/pages/System"));
|
||||||
|
const Settings = lazy(() => import("@/pages/Settings"));
|
||||||
|
const UIPlayground = lazy(() => import("@/pages/UIPlayground"));
|
||||||
|
const Logs = lazy(() => import("@/pages/Logs"));
|
||||||
|
const NoMatch = lazy(() => import("@/pages/NoMatch"));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -30,6 +33,7 @@ function App() {
|
|||||||
id="pageRoot"
|
id="pageRoot"
|
||||||
className={`absolute top-2 right-0 overflow-hidden ${isMobile ? "left-0 bottom-16" : "left-16 bottom-8"}`}
|
className={`absolute top-2 right-0 overflow-hidden ${isMobile ? "left-0 bottom-16" : "left-16 bottom-8"}`}
|
||||||
>
|
>
|
||||||
|
<Suspense>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Live />} />
|
<Route path="/" element={<Live />} />
|
||||||
<Route path="/events" element={<Events />} />
|
<Route path="/events" element={<Events />} />
|
||||||
@ -43,6 +47,7 @@ function App() {
|
|||||||
<Route path="/playground" element={<UIPlayground />} />
|
<Route path="/playground" element={<UIPlayground />} />
|
||||||
<Route path="*" element={<NoMatch />} />
|
<Route path="*" element={<NoMatch />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useRef, ReactElement } from "react";
|
import { useEffect, useRef, ReactElement } from "react";
|
||||||
import videojs from "video.js";
|
import videojs from "video.js";
|
||||||
import "videojs-playlist";
|
|
||||||
import "video.js/dist/video-js.css";
|
import "video.js/dist/video-js.css";
|
||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
|
|
||||||
|
|||||||
187
web/src/components/player/dynamic/DynamicVideoController.ts
Normal file
187
web/src/components/player/dynamic/DynamicVideoController.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import Player from "video.js/dist/types/player";
|
||||||
|
import { Recording } from "@/types/record";
|
||||||
|
import { DynamicPlayback } from "@/types/playback";
|
||||||
|
import { PreviewController } from "../PreviewPlayer";
|
||||||
|
|
||||||
|
type PlayerMode = "playback" | "scrubbing";
|
||||||
|
|
||||||
|
export class DynamicVideoController {
|
||||||
|
// main state
|
||||||
|
public camera = "";
|
||||||
|
private playerController: Player;
|
||||||
|
private previewController: PreviewController;
|
||||||
|
private setScrubbing: (isScrubbing: boolean) => void;
|
||||||
|
private setFocusedItem: (timeline: Timeline) => void;
|
||||||
|
private playerMode: PlayerMode = "playback";
|
||||||
|
|
||||||
|
// playback
|
||||||
|
private recordings: Recording[] = [];
|
||||||
|
private annotationOffset: number;
|
||||||
|
private timeToStart: number | undefined = undefined;
|
||||||
|
|
||||||
|
// listeners
|
||||||
|
private playerProgressListener: (() => void) | null = null;
|
||||||
|
private playerEndedListener: (() => void) | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
camera: string,
|
||||||
|
playerController: Player,
|
||||||
|
previewController: PreviewController,
|
||||||
|
annotationOffset: number,
|
||||||
|
defaultMode: PlayerMode,
|
||||||
|
setScrubbing: (isScrubbing: boolean) => void,
|
||||||
|
setFocusedItem: (timeline: Timeline) => void,
|
||||||
|
) {
|
||||||
|
this.camera = camera;
|
||||||
|
this.playerController = playerController;
|
||||||
|
this.previewController = previewController;
|
||||||
|
this.annotationOffset = annotationOffset;
|
||||||
|
this.playerMode = defaultMode;
|
||||||
|
this.setScrubbing = setScrubbing;
|
||||||
|
this.setFocusedItem = setFocusedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPlayback(newPlayback: DynamicPlayback) {
|
||||||
|
this.recordings = newPlayback.recordings;
|
||||||
|
this.playerController.src({
|
||||||
|
src: newPlayback.playbackUri,
|
||||||
|
type: "application/vnd.apple.mpegurl",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.timeToStart) {
|
||||||
|
this.seekToTimestamp(this.timeToStart);
|
||||||
|
this.timeToStart = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.playerController.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
seekToTimestamp(time: number, play: boolean = false) {
|
||||||
|
if (this.playerMode != "playback") {
|
||||||
|
this.playerMode = "playback";
|
||||||
|
this.setScrubbing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.recordings.length == 0 ||
|
||||||
|
time < this.recordings[0].start_time ||
|
||||||
|
time > this.recordings[this.recordings.length - 1].end_time
|
||||||
|
) {
|
||||||
|
this.timeToStart = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seekSeconds = 0;
|
||||||
|
(this.recordings || []).every((segment) => {
|
||||||
|
// if the next segment is past the desired time, stop calculating
|
||||||
|
if (segment.start_time > time) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.end_time < time) {
|
||||||
|
seekSeconds += segment.end_time - segment.start_time;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
seekSeconds +=
|
||||||
|
segment.end_time - segment.start_time - (segment.end_time - time);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (seekSeconds != 0) {
|
||||||
|
this.playerController.currentTime(seekSeconds);
|
||||||
|
|
||||||
|
if (play) {
|
||||||
|
this.playerController.play();
|
||||||
|
} else {
|
||||||
|
this.playerController.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seekToTimelineItem(timeline: Timeline) {
|
||||||
|
this.playerController.pause();
|
||||||
|
this.seekToTimestamp(timeline.timestamp + this.annotationOffset);
|
||||||
|
this.setFocusedItem(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProgress(playerTime: number): number {
|
||||||
|
// take a player time in seconds and convert to timestamp in timeline
|
||||||
|
let timestamp = 0;
|
||||||
|
let totalTime = 0;
|
||||||
|
(this.recordings || []).every((segment) => {
|
||||||
|
if (totalTime + segment.duration > playerTime) {
|
||||||
|
// segment is here
|
||||||
|
timestamp = segment.start_time + (playerTime - totalTime);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
totalTime += segment.duration;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlayerTimeUpdate(listener: ((timestamp: number) => void) | null) {
|
||||||
|
if (this.playerProgressListener) {
|
||||||
|
this.playerController.off("timeupdate", this.playerProgressListener);
|
||||||
|
this.playerProgressListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener) {
|
||||||
|
this.playerProgressListener = () => {
|
||||||
|
const progress = this.playerController.currentTime() || 0;
|
||||||
|
|
||||||
|
if (progress == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listener(this.getProgress(progress));
|
||||||
|
};
|
||||||
|
this.playerController.on("timeupdate", this.playerProgressListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClipChangedEvent(listener: ((dir: "forward") => void) | null) {
|
||||||
|
if (this.playerEndedListener) {
|
||||||
|
this.playerController.off("ended", this.playerEndedListener);
|
||||||
|
this.playerEndedListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener) {
|
||||||
|
this.playerEndedListener = () => listener("forward");
|
||||||
|
this.playerController.on("ended", this.playerEndedListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrubToTimestamp(time: number, saveIfNotReady: boolean = false) {
|
||||||
|
const scrubResult = this.previewController.scrubToTimestamp(time);
|
||||||
|
|
||||||
|
if (!scrubResult && saveIfNotReady) {
|
||||||
|
this.previewController.setNewPreviewStartTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrubResult && this.playerMode != "scrubbing") {
|
||||||
|
this.playerMode = "scrubbing";
|
||||||
|
this.playerController.pause();
|
||||||
|
this.setScrubbing(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRecordingAtTime(time: number): boolean {
|
||||||
|
if (!this.recordings || this.recordings.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.recordings.find(
|
||||||
|
(segment) => segment.start_time <= time && segment.end_time >= time,
|
||||||
|
) != undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default typeof DynamicVideoController;
|
||||||
@ -1,15 +1,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import VideoPlayer from "./VideoPlayer";
|
import VideoPlayer from "../VideoPlayer";
|
||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
import TimelineEventOverlay from "../overlay/TimelineDataOverlay";
|
import TimelineEventOverlay from "../../overlay/TimelineDataOverlay";
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { Recording } from "@/types/record";
|
import { Recording } from "@/types/record";
|
||||||
import { Preview } from "@/types/preview";
|
import { Preview } from "@/types/preview";
|
||||||
import { DynamicPlayback } from "@/types/playback";
|
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
|
||||||
import PreviewPlayer, { PreviewController } from "./PreviewPlayer";
|
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { LuPause, LuPlay } from "react-icons/lu";
|
import { LuPause, LuPlay } from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
@ -18,10 +17,9 @@ import {
|
|||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../../ui/dropdown-menu";
|
||||||
import { MdForward10, MdReplay10 } from "react-icons/md";
|
import { MdForward10, MdReplay10 } from "react-icons/md";
|
||||||
|
import { DynamicVideoController } from "./DynamicVideoController";
|
||||||
type PlayerMode = "playback" | "scrubbing";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically switches between video playback and scrubbing preview player.
|
* Dynamically switches between video playback and scrubbing preview player.
|
||||||
@ -296,185 +294,6 @@ export default function DynamicVideoPlayer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DynamicVideoController {
|
|
||||||
// main state
|
|
||||||
public camera = "";
|
|
||||||
private playerController: Player;
|
|
||||||
private previewController: PreviewController;
|
|
||||||
private setScrubbing: (isScrubbing: boolean) => void;
|
|
||||||
private setFocusedItem: (timeline: Timeline) => void;
|
|
||||||
private playerMode: PlayerMode = "playback";
|
|
||||||
|
|
||||||
// playback
|
|
||||||
private recordings: Recording[] = [];
|
|
||||||
private annotationOffset: number;
|
|
||||||
private timeToStart: number | undefined = undefined;
|
|
||||||
|
|
||||||
// listeners
|
|
||||||
private playerProgressListener: (() => void) | null = null;
|
|
||||||
private playerEndedListener: (() => void) | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
camera: string,
|
|
||||||
playerController: Player,
|
|
||||||
previewController: PreviewController,
|
|
||||||
annotationOffset: number,
|
|
||||||
defaultMode: PlayerMode,
|
|
||||||
setScrubbing: (isScrubbing: boolean) => void,
|
|
||||||
setFocusedItem: (timeline: Timeline) => void,
|
|
||||||
) {
|
|
||||||
this.camera = camera;
|
|
||||||
this.playerController = playerController;
|
|
||||||
this.previewController = previewController;
|
|
||||||
this.annotationOffset = annotationOffset;
|
|
||||||
this.playerMode = defaultMode;
|
|
||||||
this.setScrubbing = setScrubbing;
|
|
||||||
this.setFocusedItem = setFocusedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
newPlayback(newPlayback: DynamicPlayback) {
|
|
||||||
this.recordings = newPlayback.recordings;
|
|
||||||
this.playerController.src({
|
|
||||||
src: newPlayback.playbackUri,
|
|
||||||
type: "application/vnd.apple.mpegurl",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.timeToStart) {
|
|
||||||
this.seekToTimestamp(this.timeToStart);
|
|
||||||
this.timeToStart = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
this.playerController.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
seekToTimestamp(time: number, play: boolean = false) {
|
|
||||||
if (this.playerMode != "playback") {
|
|
||||||
this.playerMode = "playback";
|
|
||||||
this.setScrubbing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.recordings.length == 0 ||
|
|
||||||
time < this.recordings[0].start_time ||
|
|
||||||
time > this.recordings[this.recordings.length - 1].end_time
|
|
||||||
) {
|
|
||||||
this.timeToStart = time;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seekSeconds = 0;
|
|
||||||
(this.recordings || []).every((segment) => {
|
|
||||||
// if the next segment is past the desired time, stop calculating
|
|
||||||
if (segment.start_time > time) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.end_time < time) {
|
|
||||||
seekSeconds += segment.end_time - segment.start_time;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
seekSeconds +=
|
|
||||||
segment.end_time - segment.start_time - (segment.end_time - time);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seekSeconds != 0) {
|
|
||||||
this.playerController.currentTime(seekSeconds);
|
|
||||||
|
|
||||||
if (play) {
|
|
||||||
this.playerController.play();
|
|
||||||
} else {
|
|
||||||
this.playerController.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seekToTimelineItem(timeline: Timeline) {
|
|
||||||
this.playerController.pause();
|
|
||||||
this.seekToTimestamp(timeline.timestamp + this.annotationOffset);
|
|
||||||
this.setFocusedItem(timeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
getProgress(playerTime: number): number {
|
|
||||||
// take a player time in seconds and convert to timestamp in timeline
|
|
||||||
let timestamp = 0;
|
|
||||||
let totalTime = 0;
|
|
||||||
(this.recordings || []).every((segment) => {
|
|
||||||
if (totalTime + segment.duration > playerTime) {
|
|
||||||
// segment is here
|
|
||||||
timestamp = segment.start_time + (playerTime - totalTime);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
totalTime += segment.duration;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlayerTimeUpdate(listener: ((timestamp: number) => void) | null) {
|
|
||||||
if (this.playerProgressListener) {
|
|
||||||
this.playerController.off("timeupdate", this.playerProgressListener);
|
|
||||||
this.playerProgressListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listener) {
|
|
||||||
this.playerProgressListener = () => {
|
|
||||||
const progress = this.playerController.currentTime() || 0;
|
|
||||||
|
|
||||||
if (progress == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
listener(this.getProgress(progress));
|
|
||||||
};
|
|
||||||
this.playerController.on("timeupdate", this.playerProgressListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClipChangedEvent(listener: ((dir: "forward") => void) | null) {
|
|
||||||
if (this.playerEndedListener) {
|
|
||||||
this.playerController.off("ended", this.playerEndedListener);
|
|
||||||
this.playerEndedListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listener) {
|
|
||||||
this.playerEndedListener = () => listener("forward");
|
|
||||||
this.playerController.on("ended", this.playerEndedListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrubToTimestamp(time: number, saveIfNotReady: boolean = false) {
|
|
||||||
const scrubResult = this.previewController.scrubToTimestamp(time);
|
|
||||||
|
|
||||||
if (!scrubResult && saveIfNotReady) {
|
|
||||||
this.previewController.setNewPreviewStartTime(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrubResult && this.playerMode != "scrubbing") {
|
|
||||||
this.playerMode = "scrubbing";
|
|
||||||
this.playerController.pause();
|
|
||||||
this.setScrubbing(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRecordingAtTime(time: number): boolean {
|
|
||||||
if (!this.recordings || this.recordings.length == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.recordings.find(
|
|
||||||
(segment) => segment.start_time <= time && segment.end_time >= time,
|
|
||||||
) != undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlayerControlsProps = {
|
type PlayerControlsProps = {
|
||||||
player: Player | null;
|
player: Player | null;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import DynamicVideoPlayer, {
|
|
||||||
DynamicVideoController,
|
|
||||||
} from "@/components/player/DynamicVideoPlayer";
|
|
||||||
import PreviewPlayer, {
|
import PreviewPlayer, {
|
||||||
PreviewController,
|
PreviewController,
|
||||||
} from "@/components/player/PreviewPlayer";
|
} from "@/components/player/PreviewPlayer";
|
||||||
|
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
|
||||||
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||||
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -14,15 +12,27 @@ import {
|
|||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Preview } from "@/types/preview";
|
import { Preview } from "@/types/preview";
|
||||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, {
|
||||||
|
Suspense,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const DynamicVideoPlayer = React.lazy(
|
||||||
|
() => import("@/components/player/dynamic/DynamicVideoPlayer"),
|
||||||
|
);
|
||||||
|
|
||||||
const SEGMENT_DURATION = 30;
|
const SEGMENT_DURATION = 30;
|
||||||
|
|
||||||
type DesktopRecordingViewProps = {
|
type DesktopRecordingViewProps = {
|
||||||
@ -216,6 +226,7 @@ export function DesktopRecordingView({
|
|||||||
key={mainCamera}
|
key={mainCamera}
|
||||||
className="w-[82%] flex justify-center items mb-5"
|
className="w-[82%] flex justify-center items mb-5"
|
||||||
>
|
>
|
||||||
|
<Suspense fallback={<Skeleton className={`w-full ${grow}`} />}>
|
||||||
<DynamicVideoPlayer
|
<DynamicVideoPlayer
|
||||||
className={`w-full ${grow}`}
|
className={`w-full ${grow}`}
|
||||||
camera={mainCamera}
|
camera={mainCamera}
|
||||||
@ -233,6 +244,7 @@ export function DesktopRecordingView({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex justify-center gap-2 overflow-x-auto">
|
<div className="w-full flex justify-center gap-2 overflow-x-auto">
|
||||||
{allCameras.map((cam) => {
|
{allCameras.map((cam) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user