chore: moved timeline viewer to new component

This commit is contained in:
JohnMark Sill 2022-01-12 23:44:25 -06:00
parent 5d3ef02ef7
commit 8e5ce8aad7
3 changed files with 140 additions and 132 deletions

View 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>
);
}

View File

@ -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>

View File

@ -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'>