diff --git a/web/src/utils/historyUtil.ts b/web/src/utils/historyUtil.ts index 14c6a8b34..5d1467cce 100644 --- a/web/src/utils/historyUtil.ts +++ b/web/src/utils/historyUtil.ts @@ -125,7 +125,6 @@ export function getTimelineHoursForDay( const data: TimelinePlayback[] = []; const startDay = new Date(timestamp * 1000); startDay.setHours(23, 59, 59, 999); - const dayEnd = startDay.getTime() / 1000; startDay.setHours(0, 0, 0, 0); const startTimestamp = startDay.getTime() / 1000; let start = startDay.getTime() / 1000; diff --git a/web/src/views/history/DesktopTimelineView.tsx b/web/src/views/history/DesktopTimelineView.tsx index 9b875a6a6..559ac9928 100644 --- a/web/src/views/history/DesktopTimelineView.tsx +++ b/web/src/views/history/DesktopTimelineView.tsx @@ -30,12 +30,11 @@ export default function DesktopTimelineView({ config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config] ); + const controllerRef = useRef(undefined); - - const [selectedPlayback, setSelectedPlayback] = useState(initialPlayback); - const initialScrollRef = useRef(null); + const [selectedPlayback, setSelectedPlayback] = useState(initialPlayback); const [timelineTime, setTimelineTime] = useState(0); // handle scrolling to initial timeline item @@ -73,6 +72,7 @@ export default function DesktopTimelineView({ ], { revalidateOnFocus: false } ); + const timelineGraphData = useMemo(() => { if (!activity) { return {}; diff --git a/web/src/views/history/MobileTimelineView.tsx b/web/src/views/history/MobileTimelineView.tsx index 7577336d1..f8a7d8a71 100644 --- a/web/src/views/history/MobileTimelineView.tsx +++ b/web/src/views/history/MobileTimelineView.tsx @@ -1,6 +1,3 @@ -import { useApiHost } from "@/api"; -import TimelineEventOverlay from "@/components/overlay/TimelineDataOverlay"; -import VideoPlayer from "@/components/player/VideoPlayer"; import ActivityScrubber, { ScrubberItem, } from "@/components/scrubber/ActivityScrubber"; @@ -11,9 +8,11 @@ import { getTimelineIcon, } from "@/utils/timelineUtil"; import { renderToStaticMarkup } from "react-dom/server"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import useSWR from "swr"; -import Player from "video.js/dist/types/player"; +import DynamicVideoPlayer, { + DynamicVideoController, +} from "@/components/player/DynamicVideoPlayer"; type MobileTimelineViewProps = { playback: TimelinePlayback; @@ -22,34 +21,9 @@ type MobileTimelineViewProps = { export default function MobileTimelineView({ playback, }: MobileTimelineViewProps) { - const apiHost = useApiHost(); const { data: config } = useSWR("config"); - const timezone = useMemo( - () => - config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, - [config] - ); - const playerRef = useRef(undefined); - const previewRef = useRef(undefined); - - const [scrubbing, setScrubbing] = useState(false); - const [focusedItem, setFocusedItem] = useState( - undefined - ); - - const [seeking, setSeeking] = useState(false); - const [timeToSeek, setTimeToSeek] = useState(undefined); - - const annotationOffset = useMemo(() => { - if (!config) { - return 0; - } - - return ( - (config.cameras[playback.camera]?.detect?.annotation_offset || 0) / 1000 - ); - }, [config]); + const controllerRef = useRef(undefined); const [timelineTime, setTimelineTime] = useState( playback.timelineItems.length > 0 @@ -68,165 +42,32 @@ export default function MobileTimelineView({ { revalidateOnFocus: false } ); - const playbackUri = useMemo(() => { - if (!playback) { - return ""; - } - - const date = new Date(playback.range.start * 1000); - return `${apiHost}vod/${date.getFullYear()}-${ - date.getMonth() + 1 - }/${date.getDate()}/${date.getHours()}/${ - playback.camera - }/${timezone.replaceAll("/", ",")}/master.m3u8`; - }, [playback]); - - const onSelectItem = useCallback( - (timeline: Timeline | undefined) => { - if (timeline) { - setFocusedItem(timeline); - const selected = timeline.timestamp; - playerRef.current?.pause(); - - let seekSeconds = 0; - (recordings || []).every((segment) => { - // if the next segment is past the desired time, stop calculating - if (segment.start_time > selected) { - return false; - } - - if (segment.end_time < selected) { - seekSeconds += segment.end_time - segment.start_time; - return true; - } - - seekSeconds += - segment.end_time - - segment.start_time - - (segment.end_time - selected); - return true; - }); - playerRef.current?.currentTime(seekSeconds); - } else { - setFocusedItem(undefined); - } - }, - [annotationOffset, recordings, playerRef] - ); - - const onScrubTime = useCallback( - (data: { time: Date }) => { - if (!playback.relevantPreview) { - return; - } - - if (playerRef.current?.paused() == false) { - setScrubbing(true); - playerRef.current?.pause(); - } - - const seekTimestamp = data.time.getTime() / 1000; - const seekTime = seekTimestamp - playback.relevantPreview.start; - setTimelineTime(seekTimestamp); - setTimeToSeek(Math.round(seekTime)); - }, - [scrubbing, playerRef, playback] - ); - - const onStopScrubbing = useCallback( - (data: { time: Date }) => { - const playbackTime = data.time.getTime() / 1000; - playerRef.current?.currentTime(playbackTime - playback.range.start); - setScrubbing(false); - playerRef.current?.play(); - }, - [playback, playerRef] - ); - - // handle seeking to next frame when seek is finished - useEffect(() => { - if (seeking) { - return; - } - - if (timeToSeek && timeToSeek != previewRef.current?.currentTime()) { - setSeeking(true); - previewRef.current?.currentTime(timeToSeek); - } - }, [timeToSeek, seeking]); - if (!config || !recordings) { return ; } return (
- <> -
- { - playerRef.current = player; - player.currentTime(timelineTime - playback.range.start); - player.on("playing", () => { - onSelectItem(undefined); - }); - }} - onDispose={() => { - playerRef.current = undefined; - }} - > - {config && focusedItem ? ( - - ) : undefined} - -
- {playback.relevantPreview && ( -
- { - previewRef.current = player; - player.pause(); - player.on("seeked", () => setSeeking(false)); - }} - onDispose={() => { - previewRef.current = undefined; - }} - /> -
- )} - + { + controllerRef.current = controller; + controllerRef.current.onPlayerTimeUpdate((timestamp: number) => { + setTimelineTime(timestamp); + }); + + if (playback.timelineItems.length > 0) { + controllerRef.current?.seekToTimestamp( + playback.timelineItems[0].timestamp, + true + ); + } + }} + />
{playback != undefined && ( { + controllerRef.current?.scrubToTimestamp( + data.time.getTime() / 1000 + ); + setTimelineTime(data.time.getTime() / 1000); + }} + timechangedHandler={(data) => { + controllerRef.current?.seekToTimestamp( + data.time.getTime() / 1000, + true + ); + }} selectHandler={(data) => { if (data.items.length > 0) { const selected = parseFloat(data.items[0].split("-")[0]); - onSelectItem( - playback.timelineItems.find( - (timeline) => timeline.timestamp == selected - ) + const timeline = playback.timelineItems.find( + (timeline) => timeline.timestamp == selected ); + + if (timeline) { + controllerRef.current?.seekToTimelineItem(timeline); + } } }} /> diff --git a/web/vite.config.ts b/web/vite.config.ts index 5d5bf8207..a97dbd014 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -12,24 +12,24 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://192.168.50.106:5000', + target: 'http://localhost:5000', ws: true, }, '/vod': { - target: 'http://192.168.50.106:5000' + target: 'http://localhost:5000' }, '/clips': { - target: 'http://192.168.50.106:5000' + target: 'http://localhost:5000' }, '/exports': { - target: 'http://192.168.50.106:5000' + target: 'http://localhost:5000' }, '/ws': { - target: 'ws://192.168.50.106:5000', + target: 'ws://localhost:5000', ws: true, }, '/live': { - target: 'ws://192.168.50.106:5000', + target: 'ws://localhost:5000', changeOrigin: true, ws: true, },