mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 20:25:26 +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",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.8.0",
|
||||
"video.js": "^8.6.1",
|
||||
"videojs-playlist": "^5.1.0",
|
||||
"video.js": "^8.10.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
@ -8187,18 +8186,6 @@
|
||||
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz",
|
||||
"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": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"hls.js": "^1.5.7",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.0.3",
|
||||
"lucide-react": "^0.294.0",
|
||||
@ -65,8 +66,6 @@
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.8.0",
|
||||
"video.js": "^8.6.1",
|
||||
"videojs-playlist": "^5.1.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@ -2,20 +2,23 @@ import Providers from "@/context/providers";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
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 Statusbar from "./components/Statusbar";
|
||||
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() {
|
||||
return (
|
||||
@ -30,19 +33,21 @@ function App() {
|
||||
id="pageRoot"
|
||||
className={`absolute top-2 right-0 overflow-hidden ${isMobile ? "left-0 bottom-16" : "left-16 bottom-8"}`}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Live />} />
|
||||
<Route path="/events" element={<Events />} />
|
||||
<Route path="/export" element={<Export />} />
|
||||
<Route path="/storage" element={<Storage />} />
|
||||
<Route path="/plus" element={<SubmitPlus />} />
|
||||
<Route path="/system" element={<System />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/config" element={<ConfigEditor />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="/playground" element={<UIPlayground />} />
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
</Routes>
|
||||
<Suspense>
|
||||
<Routes>
|
||||
<Route path="/" element={<Live />} />
|
||||
<Route path="/events" element={<Events />} />
|
||||
<Route path="/export" element={<Export />} />
|
||||
<Route path="/storage" element={<Storage />} />
|
||||
<Route path="/plus" element={<SubmitPlus />} />
|
||||
<Route path="/system" element={<System />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/config" element={<ConfigEditor />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="/playground" element={<UIPlayground />} />
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, ReactElement } from "react";
|
||||
import videojs from "video.js";
|
||||
import "videojs-playlist";
|
||||
import "video.js/dist/video-js.css";
|
||||
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 VideoPlayer from "./VideoPlayer";
|
||||
import VideoPlayer from "../VideoPlayer";
|
||||
import Player from "video.js/dist/types/player";
|
||||
import TimelineEventOverlay from "../overlay/TimelineDataOverlay";
|
||||
import TimelineEventOverlay from "../../overlay/TimelineDataOverlay";
|
||||
import { useApiHost } from "@/api";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { Recording } from "@/types/record";
|
||||
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 { LuPause, LuPlay } from "react-icons/lu";
|
||||
import {
|
||||
@ -18,10 +17,9 @@ import {
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
} from "../../ui/dropdown-menu";
|
||||
import { MdForward10, MdReplay10 } from "react-icons/md";
|
||||
|
||||
type PlayerMode = "playback" | "scrubbing";
|
||||
import { DynamicVideoController } from "./DynamicVideoController";
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
player: Player | null;
|
||||
show: boolean;
|
||||
@ -1,9 +1,7 @@
|
||||
import DynamicVideoPlayer, {
|
||||
DynamicVideoController,
|
||||
} from "@/components/player/DynamicVideoPlayer";
|
||||
import PreviewPlayer, {
|
||||
PreviewController,
|
||||
} from "@/components/player/PreviewPlayer";
|
||||
import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController";
|
||||
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -14,15 +12,27 @@ import {
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Preview } from "@/types/preview";
|
||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
const DynamicVideoPlayer = React.lazy(
|
||||
() => import("@/components/player/dynamic/DynamicVideoPlayer"),
|
||||
);
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
|
||||
type DesktopRecordingViewProps = {
|
||||
@ -216,23 +226,25 @@ export function DesktopRecordingView({
|
||||
key={mainCamera}
|
||||
className="w-[82%] flex justify-center items mb-5"
|
||||
>
|
||||
<DynamicVideoPlayer
|
||||
className={`w-full ${grow}`}
|
||||
camera={mainCamera}
|
||||
timeRange={currentTimeRange}
|
||||
cameraPreviews={allPreviews ?? []}
|
||||
startTime={playbackStart}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
controller.onPlayerTimeUpdate((timestamp: number) => {
|
||||
setPlayerTime(timestamp);
|
||||
setCurrentTime(timestamp);
|
||||
Object.values(previewRefs.current ?? {}).forEach((prev) =>
|
||||
prev.scrubToTimestamp(Math.floor(timestamp)),
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Skeleton className={`w-full ${grow}`} />}>
|
||||
<DynamicVideoPlayer
|
||||
className={`w-full ${grow}`}
|
||||
camera={mainCamera}
|
||||
timeRange={currentTimeRange}
|
||||
cameraPreviews={allPreviews ?? []}
|
||||
startTime={playbackStart}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
controller.onPlayerTimeUpdate((timestamp: number) => {
|
||||
setPlayerTime(timestamp);
|
||||
setCurrentTime(timestamp);
|
||||
Object.values(previewRefs.current ?? {}).forEach((prev) =>
|
||||
prev.scrubToTimestamp(Math.floor(timestamp)),
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="w-full flex justify-center gap-2 overflow-x-auto">
|
||||
{allCameras.map((cam) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user