feat: moved events to camera and added logic to track video progress

This commit is contained in:
JohnMark Sill 2022-01-12 23:09:39 -06:00
parent 2f408c7ea8
commit e57c15cc37
2 changed files with 175 additions and 106 deletions

View File

@ -1,12 +1,8 @@
import { Fragment, h } from 'preact';
import { h } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Next } from '../icons/Next';
import { Play } from '../icons/Play';
import { Previous } from '../icons/Previous';
import { TextTab } from './Tabs';
import { longToDate } from '../utils/dateUtil';
export default function Timeline({ events, onChange }) {
export default function Timeline({ events, offset, currentIndex, onChange }) {
const timelineContainerRef = useRef(undefined);
const [timeline, setTimeline] = useState([]);
@ -18,14 +14,13 @@ export default function Timeline({ events, onChange }) {
useEffect(() => {
if (events && timelineOffset) {
const filteredEvents = [...events].reverse().filter((e) => e.end_time !== undefined);
const firstEvent = events[events.length - 1];
const firstEvent = events[0];
if (firstEvent) {
setMarkerTime(longToDate(firstEvent.start_time));
}
const firstEventTime = longToDate(firstEvent.start_time);
const eventsMap = filteredEvents.map((e, i) => {
const eventsMap = events.map((e, i) => {
const startTime = longToDate(e.start_time);
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
const seconds = Math.round(Math.abs(endTime - startTime) / 1000);
@ -53,6 +48,32 @@ export default function Timeline({ events, onChange }) {
}
}, [events, timelineOffset]);
useEffect(() => {
if (currentEvent && offset >= 0) {
setScrollActive(false);
timelineContainerRef.current.scroll({
left: currentEvent.positionX + offset - timelineOffset,
behavior: 'smooth',
});
} else {
setScrollActive(true);
}
}, [offset]);
useEffect(() => {
if (currentIndex !== undefined && currentIndex !== currentEvent.index) {
const event = timeline[currentIndex];
setCurrentEvent({
...event,
id: event.id,
index: currentIndex,
startTime: event.start_time,
endTime: event.end_time,
});
timelineContainerRef.current.scroll({left: event.positionX - timelineOffset, behavior: "smooth"})
}
}, [currentIndex]);
const checkMarkerForEvent = (markerTime) => {
if (!scrollActive) {
setScrollActive(true);
@ -133,57 +154,24 @@ export default function Timeline({ events, onChange }) {
}
}, [timeline]);
const setNextCurrentEvent = function (offset) {
setScrollActive(false);
setCurrentEvent((currentEvent) => {
const index = currentEvent.index + offset;
const nextEvent = timeline[index];
const positionX = nextEvent.positionX - timelineOffset;
timelineContainerRef.current.scrollLeft = positionX;
return {
...nextEvent,
id: nextEvent.id,
index,
startTime: nextEvent.start_time,
endTime: nextEvent.end_time,
};
});
};
const handlePrevious = function () {
setNextCurrentEvent(-1);
};
const handleNext = function () {
setNextCurrentEvent(1);
};
return (
<Fragment>
<div className="relative flex-grow-1">
<div className="absolute left-0 top-0 h-full w-full" style={{ textAlign: 'center' }}>
<div className="h-full" style={{ margin: '0 auto', textAlign: 'center' }}>
<span className="z-20 text-white">{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}</span>
<div
className="z-20 h-full absolute"
style={{
left: 'calc(100% / 2)',
height: 'calc(100% - 24px)',
borderRight: '2px solid rgba(252, 211, 77)',
}}
></div>
</div>
</div>
<div ref={timelineContainerRef} className="overflow-x-auto" onScroll={handleScroll}>
<RenderTimeline />
<div className="relative flex-grow-1">
<div className="absolute left-0 top-0 h-full w-full" style={{ textAlign: 'center' }}>
<div className="h-full" style={{ margin: '0 auto', textAlign: 'center' }}>
<span className="z-20 text-white">{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}</span>
<div
className="z-20 h-full absolute"
style={{
left: 'calc(100% / 2)',
height: 'calc(100% - 24px)',
borderRight: '2px solid rgba(252, 211, 77)',
}}
></div>
</div>
</div>
<div className="flex self-center">
<TextTab onClick={handlePrevious} text={<Previous />} />
<TextTab text={<Play />} />
<TextTab onClick={handleNext} text={<Next />} />
<div ref={timelineContainerRef} className="overflow-x-auto hide-scroll" onScroll={handleScroll}>
<RenderTimeline />
</div>
</Fragment>
</div>
);
}

View File

@ -1,11 +1,11 @@
import { h, Fragment } from 'preact';
import { h, Fragment, render } from 'preact';
import AutoUpdatingCameraImage from '../components/AutoUpdatingCameraImage';
import JSMpegPlayer from '../components/JSMpegPlayer';
import Heading from '../components/Heading';
import Link from '../components/Link';
import Switch from '../components/Switch';
import { usePersistence } from '../context';
import { useCallback, useMemo, useState } from 'preact/hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useApiHost, useConfig, useEvents } from '../api';
import { Tabs, TextTab } from '../components/Tabs';
import Timeline from '../components/Timeline';
@ -13,15 +13,22 @@ import { LiveChip } from '../components/LiveChip';
import { HistoryHeader } from './HistoryHeader';
import { longToDate } from '../utils/dateUtil';
import { useSearchString } from '../hooks/useSearchString';
import { Previous } from '../icons/Previous';
import { Play } from '../icons/Play';
import { Next } from '../icons/Next';
const emptyObject = Object.freeze({});
export default function Camera({ camera }) {
const apiHost = useApiHost();
const videoRef = useRef();
const { data: config } = useConfig();
const { searchString } = useSearchString(25, `camera=${camera}`);
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');
@ -30,7 +37,15 @@ export default function Camera({ camera }) {
const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height));
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
const [currentEvent, setCurrentEvent] = useState(undefined);
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(
(id, value) => {
@ -52,58 +67,63 @@ export default function Camera({ camera }) {
);
const optionContent = (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className='grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'>
<Switch
checked={options['bbox']}
id="bbox"
id='bbox'
onChange={handleSetOption}
label="Bounding box"
labelPosition="after"
label='Bounding box'
labelPosition='after'
/>
<Switch
checked={options['timestamp']}
id="timestamp"
id='timestamp'
onChange={handleSetOption}
label="Timestamp"
labelPosition="after"
label='Timestamp'
labelPosition='after'
/>
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Masks" labelPosition="after" />
<Switch checked={options['zones']} id='zones' onChange={handleSetOption} label='Zones' labelPosition='after' />
<Switch checked={options['mask']} id='mask' onChange={handleSetOption} label='Masks' labelPosition='after' />
<Switch
checked={options['motion']}
id="motion"
id='motion'
onChange={handleSetOption}
label="Motion boxes"
labelPosition="after"
label='Motion boxes'
labelPosition='after'
/>
<Switch
checked={options['regions']}
id="regions"
id='regions'
onChange={handleSetOption}
label="Regions"
labelPosition="after"
label='Regions'
labelPosition='after'
/>
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</div>
);
const RenderPlayer = useCallback(() => {
if (playerType === 'live') {
return <JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />;
} else if (playerType === 'debug') {
return (
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} className="w-full" />
{/* {optionContent} */}
</div>
let renderPlayer;
switch (playerType) {
case 'live':
renderPlayer = (
<Fragment>
<div>
<JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
</div>
</Fragment>
);
} else if (playerType === 'history') {
return (
currentEvent && (
break;
case 'history':
if (currentEvent) {
renderPlayer = (
<video
ref={videoRef}
onTimeUpdate={handleTimeUpdate}
onPause={handlePaused}
onClick={handleVideoTouch}
poster={`${apiHost}/api/events/${currentEvent.id}/snapshot.jpg`}
preload="none"
preload='none'
playsInline
controls
>
@ -111,10 +131,31 @@ export default function Camera({ camera }) {
src={`${apiHost}/api/${camera}/start/${currentEvent.startTime}/end/${currentEvent.endTime}/clip.mp4`}
/>
</video>
)
);
}
break;
case 'debug':
renderPlayer = (
<Fragment>
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
</div>
{optionContent}
</Fragment>
);
break;
default:
break;
}
const handleTimeUpdate = () => {
const timestamp = Math.round(videoRef.current.currentTime);
const offset = Math.round(timestamp);
const triggerStateChange = offset !== timelineOffset;
if (triggerStateChange) {
setTimelineOffset(offset);
}
}, [playerType, currentEvent]);
};
const handleVideoTouch = () => {
setHideBanner(true);
@ -131,19 +172,38 @@ export default function Camera({ camera }) {
};
const handleTimelineChange = (event) => {
setCurrentEvent(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 (
<div className="flex bg-black w-full h-full justify-center">
<div className="relative max-w-screen-md flex-grow w-full">
<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={`absolute top-0 text-white w-full transition-opacity duration-300 ${hideBanner && 'opacity-0'}`}
>
<div className="flex pt-4 pl-4 items-center bg-gradient-to-b from-black to-transparent w-full h-16 z10">
<div className='flex pt-4 pl-4 items-center bg-gradient-to-b from-black to-transparent w-full h-16 z10'>
{(playerType === 'live' || playerType === 'debug') && (
<Fragment>
<Heading size="xl" className="mr-2">
<Heading size='xl' className='mr-2'>
{camera}
</Heading>
<LiveChip />
@ -152,27 +212,48 @@ export default function Camera({ camera }) {
</div>
</div>
<div className="flex flex-col justify-center h-full">
<div className="relative">
<div className='flex flex-col justify-center h-full'>
<div className='relative'>
{currentEvent && (
<HistoryHeader
camera={camera}
date={longToDate(currentEvent.start_time)}
objectLabel={currentEvent.label}
className="mb-2"
className='mb-2'
/>
)}
<RenderPlayer />
{renderPlayer}
</div>
{playerType === 'history' && <Timeline events={events} onChange={handleTimelineChange} />}
{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">
<Tabs selectedIndex={1} onChange={handleTabChange} className="justify">
<TextTab text="History" />
<TextTab text="Live" />
<TextTab text="Debug" />
<div className='absolute flex justify-center bottom-8 w-full'>
<Tabs selectedIndex={1} onChange={handleTabChange} className='justify'>
<TextTab text='History' />
<TextTab text='Live' />
<TextTab text='Debug' />
</Tabs>
</div>
</div>