mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 17:25:22 +03:00
chore: moved timeline viewer to new component
This commit is contained in:
parent
5d3ef02ef7
commit
8e5ce8aad7
117
web/src/components/HistoryViewer.jsx
Normal file
117
web/src/components/HistoryViewer.jsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { Fragment, h } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useApiHost, useEvents } from '../api';
|
||||||
|
import { useSearchString } from '../hooks/useSearchString';
|
||||||
|
import { Next } from '../icons/Next';
|
||||||
|
import { Play } from '../icons/Play';
|
||||||
|
import { Previous } from '../icons/Previous';
|
||||||
|
import { HistoryHeader } from '../routes/HistoryHeader';
|
||||||
|
import { longToDate } from '../utils/dateUtil';
|
||||||
|
import Timeline from './Timeline';
|
||||||
|
|
||||||
|
export default function HistoryViewer({ camera }) {
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const videoRef = useRef();
|
||||||
|
|
||||||
|
const beginningOfDay = new Date().setHours(0, 0, 0) / 1000;
|
||||||
|
const { searchString } = useSearchString(200, `camera=${camera}&after=${beginningOfDay}`);
|
||||||
|
const { data: events } = useEvents(searchString);
|
||||||
|
const [timelineEvents, setTimelineEvents] = useState();
|
||||||
|
|
||||||
|
const [currentEvent, setCurrentEvent] = useState();
|
||||||
|
const [currentEventIndex, setCurrentEventIndex] = useState();
|
||||||
|
const [timelineOffset, setTimelineOffset] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (events) {
|
||||||
|
setTimelineEvents([...events].reverse().filter((e) => e.end_time !== undefined));
|
||||||
|
}
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
const timestamp = Math.round(videoRef.current.currentTime);
|
||||||
|
const offset = Math.round(timestamp);
|
||||||
|
const triggerStateChange = offset !== timelineOffset;
|
||||||
|
if (triggerStateChange) {
|
||||||
|
setTimelineOffset(offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimelineChange = (event) => {
|
||||||
|
console.log({ event });
|
||||||
|
if (event !== undefined) {
|
||||||
|
setCurrentEvent(event);
|
||||||
|
setCurrentEventIndex(event.index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVideoTouch = () => {
|
||||||
|
setHideBanner(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = function () {
|
||||||
|
videoRef.current.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaused = () => {
|
||||||
|
setTimelineOffset(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevious = function () {
|
||||||
|
setCurrentEventIndex((index) => index - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = function () {
|
||||||
|
setCurrentEventIndex((index) => index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{currentEvent && (
|
||||||
|
<Fragment>
|
||||||
|
<div className='relative flex flex-col'>
|
||||||
|
<HistoryHeader
|
||||||
|
camera={camera}
|
||||||
|
date={longToDate(currentEvent.start_time)}
|
||||||
|
objectLabel={currentEvent.label}
|
||||||
|
className='mb-2'
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
onPause={handlePaused}
|
||||||
|
onClick={handleVideoTouch}
|
||||||
|
poster={`${apiHost}/api/events/${currentEvent.id}/snapshot.jpg`}
|
||||||
|
preload='none'
|
||||||
|
playsInline
|
||||||
|
controls
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src={`${apiHost}/api/${camera}/start/${currentEvent.start_time}/end/${currentEvent.end_time}/clip.mp4`}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Timeline
|
||||||
|
events={timelineEvents}
|
||||||
|
offset={timelineOffset}
|
||||||
|
currentIndex={currentEventIndex}
|
||||||
|
onChange={handleTimelineChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex self-center'>
|
||||||
|
<button onClick={handlePrevious}>
|
||||||
|
<Previous />
|
||||||
|
</button>
|
||||||
|
<button onClick={handlePlay}>
|
||||||
|
<Play />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleNext}>
|
||||||
|
<Next />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const firstEventTime = longToDate(firstEvent.start_time);
|
const firstEventTime = longToDate(firstEvent.start_time);
|
||||||
const eventsMap = events.map((e, i) => {
|
const timelineEvents = events.map((e, i) => {
|
||||||
const startTime = longToDate(e.start_time);
|
const startTime = longToDate(e.start_time);
|
||||||
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
|
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
|
||||||
const seconds = Math.round(Math.abs(endTime - startTime) / 1000);
|
const seconds = Math.round(Math.abs(endTime - startTime) / 1000);
|
||||||
@ -35,16 +35,15 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const recentEvent = eventsMap[eventsMap.length - 1];
|
const firstTimelineEvent = timelineEvents[0];
|
||||||
const event = {
|
setCurrentEvent({
|
||||||
...recentEvent,
|
...firstTimelineEvent,
|
||||||
id: recentEvent.id,
|
id: firstTimelineEvent.id,
|
||||||
index: eventsMap.length - 1,
|
index: 0,
|
||||||
startTime: recentEvent.start_time,
|
startTime: firstTimelineEvent.start_time,
|
||||||
endTime: recentEvent.end_time,
|
endTime: firstTimelineEvent.end_time,
|
||||||
};
|
});
|
||||||
setCurrentEvent(event);
|
setTimeline(timelineEvents);
|
||||||
setTimeline(eventsMap);
|
|
||||||
}
|
}
|
||||||
}, [events, timelineOffset]);
|
}, [events, timelineOffset]);
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
startTime: event.start_time,
|
startTime: event.start_time,
|
||||||
endTime: event.end_time,
|
endTime: event.end_time,
|
||||||
});
|
});
|
||||||
timelineContainerRef.current.scroll({left: event.positionX - timelineOffset, behavior: "smooth"})
|
timelineContainerRef.current.scroll({ left: event.positionX - timelineOffset, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}, [currentIndex]);
|
}, [currentIndex]);
|
||||||
|
|
||||||
@ -128,7 +127,7 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
const timelineLength = timelineOffset + lastEvent.positionX + lastEvent.width;
|
const timelineLength = timelineOffset + lastEvent.positionX + lastEvent.width;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex items-center h-20"
|
className='relative flex items-center h-20'
|
||||||
style={{
|
style={{
|
||||||
width: `${timelineLength}px`,
|
width: `${timelineLength}px`,
|
||||||
background: "url('/marker.png')",
|
background: "url('/marker.png')",
|
||||||
@ -141,7 +140,7 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={e.id}
|
key={e.id}
|
||||||
className="absolute z-10 rounded-full bg-blue-300 h-2"
|
className='absolute z-10 rounded-full bg-blue-300 h-2'
|
||||||
style={{
|
style={{
|
||||||
left: `${e.positionX}px`,
|
left: `${e.positionX}px`,
|
||||||
width: `${e.seconds}px`,
|
width: `${e.seconds}px`,
|
||||||
@ -155,12 +154,12 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
}, [timeline]);
|
}, [timeline]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-grow-1">
|
<div className='relative flex-grow-1'>
|
||||||
<div className="absolute left-0 top-0 h-full w-full" style={{ textAlign: 'center' }}>
|
<div className='absolute left-0 top-0 h-full w-full' style={{ textAlign: 'center' }}>
|
||||||
<div className="h-full" style={{ margin: '0 auto', textAlign: 'center' }}>
|
<div className='h-full' style={{ margin: '0 auto', textAlign: 'center' }}>
|
||||||
<span className="z-20 text-white">{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}</span>
|
<span className='z-20 text-white'>{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}</span>
|
||||||
<div
|
<div
|
||||||
className="z-20 h-full absolute"
|
className='z-20 h-full absolute'
|
||||||
style={{
|
style={{
|
||||||
left: 'calc(100% / 2)',
|
left: 'calc(100% / 2)',
|
||||||
height: 'calc(100% - 24px)',
|
height: 'calc(100% - 24px)',
|
||||||
@ -169,7 +168,7 @@ export default function Timeline({ events, offset, currentIndex, onChange }) {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={timelineContainerRef} className="overflow-x-auto hide-scroll" onScroll={handleScroll}>
|
<div ref={timelineContainerRef} className='overflow-x-auto hide-scroll' onScroll={handleScroll}>
|
||||||
<RenderTimeline />
|
<RenderTimeline />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,37 +16,19 @@ import { useSearchString } from '../hooks/useSearchString';
|
|||||||
import { Previous } from '../icons/Previous';
|
import { Previous } from '../icons/Previous';
|
||||||
import { Play } from '../icons/Play';
|
import { Play } from '../icons/Play';
|
||||||
import { Next } from '../icons/Next';
|
import { Next } from '../icons/Next';
|
||||||
|
import HistoryViewer from '../components/HistoryViewer';
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
export default function Camera({ camera }) {
|
export default function Camera({ camera }) {
|
||||||
const apiHost = useApiHost();
|
|
||||||
const videoRef = useRef();
|
|
||||||
|
|
||||||
const { data: config } = useConfig();
|
const { data: config } = useConfig();
|
||||||
|
|
||||||
const beginningOfDay = new Date().setHours(0, 0, 0) / 1000;
|
|
||||||
const { searchString } = useSearchString(200, `camera=${camera}&after=${beginningOfDay}`);
|
|
||||||
const { data: events } = useEvents(searchString);
|
|
||||||
const [timelineEvents, setTimelineEvents] = useState();
|
|
||||||
|
|
||||||
const [hideBanner, setHideBanner] = useState(false);
|
|
||||||
const [playerType, setPlayerType] = useState('live');
|
const [playerType, setPlayerType] = useState('live');
|
||||||
|
|
||||||
const cameraConfig = config?.cameras[camera];
|
const cameraConfig = config?.cameras[camera];
|
||||||
const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height));
|
const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height));
|
||||||
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
||||||
|
|
||||||
const [currentEvent, setCurrentEvent] = useState();
|
|
||||||
const [currentEventIndex, setCurrentEventIndex] = useState();
|
|
||||||
const [timelineOffset, setTimelineOffset] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (events) {
|
|
||||||
setTimelineEvents([...events].reverse().filter((e) => e.end_time !== undefined));
|
|
||||||
}
|
|
||||||
}, [events]);
|
|
||||||
|
|
||||||
const handleSetOption = useCallback(
|
const handleSetOption = useCallback(
|
||||||
(id, value) => {
|
(id, value) => {
|
||||||
const newOptions = { ...options, [id]: value };
|
const newOptions = { ...options, [id]: value };
|
||||||
@ -115,31 +97,12 @@ export default function Camera({ camera }) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'history':
|
case 'history':
|
||||||
if (currentEvent) {
|
renderPlayer = <HistoryViewer camera={camera} />;
|
||||||
renderPlayer = (
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
onTimeUpdate={handleTimeUpdate}
|
|
||||||
onPause={handlePaused}
|
|
||||||
onClick={handleVideoTouch}
|
|
||||||
poster={`${apiHost}/api/events/${currentEvent.id}/snapshot.jpg`}
|
|
||||||
preload='none'
|
|
||||||
playsInline
|
|
||||||
controls
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src={`${apiHost}/api/${camera}/start/${currentEvent.startTime}/end/${currentEvent.endTime}/clip.mp4`}
|
|
||||||
/>
|
|
||||||
</video>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'debug':
|
case 'debug':
|
||||||
renderPlayer = (
|
renderPlayer = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div>
|
|
||||||
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
|
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
|
||||||
</div>
|
|
||||||
{optionContent}
|
{optionContent}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -148,19 +111,6 @@ export default function Camera({ camera }) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
const timestamp = Math.round(videoRef.current.currentTime);
|
|
||||||
const offset = Math.round(timestamp);
|
|
||||||
const triggerStateChange = offset !== timelineOffset;
|
|
||||||
if (triggerStateChange) {
|
|
||||||
setTimelineOffset(offset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoTouch = () => {
|
|
||||||
setHideBanner(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (index) => {
|
const handleTabChange = (index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
setPlayerType('history');
|
setPlayerType('history');
|
||||||
@ -171,29 +121,6 @@ export default function Camera({ camera }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimelineChange = (event) => {
|
|
||||||
if (event !== undefined) {
|
|
||||||
setCurrentEvent(event);
|
|
||||||
setCurrentEventIndex(event.index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlay = function () {
|
|
||||||
videoRef.current.play();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaused = () => {
|
|
||||||
setTimelineOffset(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevious = function () {
|
|
||||||
setCurrentEventIndex((index) => index - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = function () {
|
|
||||||
setCurrentEventIndex((index) => index + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex bg-gray-900 w-full h-full justify-center'>
|
<div className='flex bg-gray-900 w-full h-full justify-center'>
|
||||||
<div className='relative max-w-screen-md flex-grow w-full'>
|
<div className='relative max-w-screen-md flex-grow w-full'>
|
||||||
@ -210,42 +137,7 @@ export default function Camera({ camera }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col justify-center h-full'>
|
<div className='flex flex-col justify-center h-full'>{renderPlayer}</div>
|
||||||
<div className='relative'>
|
|
||||||
{currentEvent && (
|
|
||||||
<HistoryHeader
|
|
||||||
camera={camera}
|
|
||||||
date={longToDate(currentEvent.start_time)}
|
|
||||||
objectLabel={currentEvent.label}
|
|
||||||
className='mb-2'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderPlayer}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{playerType === 'history' && (
|
|
||||||
<Fragment>
|
|
||||||
<Timeline
|
|
||||||
events={timelineEvents}
|
|
||||||
offset={timelineOffset}
|
|
||||||
currentIndex={currentEventIndex}
|
|
||||||
onChange={handleTimelineChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex self-center'>
|
|
||||||
<button onClick={handlePrevious}>
|
|
||||||
<Previous />
|
|
||||||
</button>
|
|
||||||
<button onClick={handlePlay}>
|
|
||||||
<Play />
|
|
||||||
</button>
|
|
||||||
<button onClick={handleNext}>
|
|
||||||
<Next />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='absolute flex justify-center bottom-8 w-full'>
|
<div className='absolute flex justify-center bottom-8 w-full'>
|
||||||
<Tabs selectedIndex={1} onChange={handleTabChange} className='justify'>
|
<Tabs selectedIndex={1} onChange={handleTabChange} className='justify'>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user