2022-01-13 08:09:39 +03:00
|
|
|
import { h, Fragment, render } from 'preact';
|
2022-01-13 01:53:50 +03:00
|
|
|
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';
|
2022-01-13 08:09:39 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
2022-01-13 02:10:59 +03:00
|
|
|
import { useApiHost, useConfig, useEvents } from '../api';
|
2022-01-13 01:53:50 +03:00
|
|
|
import { Tabs, TextTab } from '../components/Tabs';
|
|
|
|
|
import Timeline from '../components/Timeline';
|
|
|
|
|
import { LiveChip } from '../components/LiveChip';
|
|
|
|
|
import { HistoryHeader } from './HistoryHeader';
|
2022-01-13 02:10:59 +03:00
|
|
|
import { longToDate } from '../utils/dateUtil';
|
|
|
|
|
import { useSearchString } from '../hooks/useSearchString';
|
2022-01-13 08:09:39 +03:00
|
|
|
import { Previous } from '../icons/Previous';
|
|
|
|
|
import { Play } from '../icons/Play';
|
|
|
|
|
import { Next } from '../icons/Next';
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const emptyObject = Object.freeze({});
|
|
|
|
|
|
|
|
|
|
export default function Camera({ camera }) {
|
|
|
|
|
const apiHost = useApiHost();
|
2022-01-13 08:09:39 +03:00
|
|
|
const videoRef = useRef();
|
2022-01-13 02:10:59 +03:00
|
|
|
|
2022-01-13 01:53:50 +03:00
|
|
|
const { data: config } = useConfig();
|
2022-01-13 08:09:39 +03:00
|
|
|
|
|
|
|
|
const beginningOfDay = new Date().setHours(0, 0, 0) / 1000;
|
|
|
|
|
const { searchString } = useSearchString(200, `camera=${camera}&after=${beginningOfDay}`);
|
2022-01-13 02:10:59 +03:00
|
|
|
const { data: events } = useEvents(searchString);
|
2022-01-13 08:09:39 +03:00
|
|
|
const [timelineEvents, setTimelineEvents] = useState();
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const [hideBanner, setHideBanner] = useState(false);
|
2022-01-13 02:10:59 +03:00
|
|
|
const [playerType, setPlayerType] = useState('live');
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const cameraConfig = config?.cameras[camera];
|
2022-01-13 02:10:59 +03:00
|
|
|
const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height));
|
2022-01-13 01:53:50 +03:00
|
|
|
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
|
|
|
|
|
2022-01-13 08:09:39 +03:00
|
|
|
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]);
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const handleSetOption = useCallback(
|
|
|
|
|
(id, value) => {
|
|
|
|
|
const newOptions = { ...options, [id]: value };
|
|
|
|
|
setOptions(newOptions);
|
|
|
|
|
},
|
|
|
|
|
[options, setOptions]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const searchParams = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
new URLSearchParams(
|
|
|
|
|
Object.keys(options).reduce((memo, key) => {
|
|
|
|
|
memo.push([key, options[key] === true ? '1' : '0']);
|
|
|
|
|
return memo;
|
|
|
|
|
}, [])
|
|
|
|
|
),
|
|
|
|
|
[options]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const optionContent = (
|
2022-01-13 08:09:39 +03:00
|
|
|
<div className='grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4'>
|
2022-01-13 01:53:50 +03:00
|
|
|
<Switch
|
|
|
|
|
checked={options['bbox']}
|
2022-01-13 08:09:39 +03:00
|
|
|
id='bbox'
|
2022-01-13 01:53:50 +03:00
|
|
|
onChange={handleSetOption}
|
2022-01-13 08:09:39 +03:00
|
|
|
label='Bounding box'
|
|
|
|
|
labelPosition='after'
|
2022-01-13 01:53:50 +03:00
|
|
|
/>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={options['timestamp']}
|
2022-01-13 08:09:39 +03:00
|
|
|
id='timestamp'
|
2022-01-13 01:53:50 +03:00
|
|
|
onChange={handleSetOption}
|
2022-01-13 08:09:39 +03:00
|
|
|
label='Timestamp'
|
|
|
|
|
labelPosition='after'
|
2022-01-13 01:53:50 +03:00
|
|
|
/>
|
2022-01-13 08:09:39 +03:00
|
|
|
<Switch checked={options['zones']} id='zones' onChange={handleSetOption} label='Zones' labelPosition='after' />
|
|
|
|
|
<Switch checked={options['mask']} id='mask' onChange={handleSetOption} label='Masks' labelPosition='after' />
|
2022-01-13 01:53:50 +03:00
|
|
|
<Switch
|
|
|
|
|
checked={options['motion']}
|
2022-01-13 08:09:39 +03:00
|
|
|
id='motion'
|
2022-01-13 01:53:50 +03:00
|
|
|
onChange={handleSetOption}
|
2022-01-13 08:09:39 +03:00
|
|
|
label='Motion boxes'
|
|
|
|
|
labelPosition='after'
|
2022-01-13 01:53:50 +03:00
|
|
|
/>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={options['regions']}
|
2022-01-13 08:09:39 +03:00
|
|
|
id='regions'
|
2022-01-13 01:53:50 +03:00
|
|
|
onChange={handleSetOption}
|
2022-01-13 08:09:39 +03:00
|
|
|
label='Regions'
|
|
|
|
|
labelPosition='after'
|
2022-01-13 01:53:50 +03:00
|
|
|
/>
|
|
|
|
|
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
|
|
|
|
</div>
|
2022-01-13 02:10:59 +03:00
|
|
|
);
|
2022-01-13 01:53:50 +03:00
|
|
|
|
2022-01-13 08:09:39 +03:00
|
|
|
let renderPlayer;
|
|
|
|
|
|
|
|
|
|
switch (playerType) {
|
|
|
|
|
case 'live':
|
|
|
|
|
renderPlayer = (
|
|
|
|
|
<Fragment>
|
|
|
|
|
<div>
|
|
|
|
|
<JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
|
|
|
|
|
</div>
|
|
|
|
|
</Fragment>
|
2022-01-13 01:53:50 +03:00
|
|
|
);
|
2022-01-13 08:09:39 +03:00
|
|
|
break;
|
|
|
|
|
case 'history':
|
|
|
|
|
if (currentEvent) {
|
|
|
|
|
renderPlayer = (
|
2022-01-13 02:10:59 +03:00
|
|
|
<video
|
2022-01-13 08:09:39 +03:00
|
|
|
ref={videoRef}
|
|
|
|
|
onTimeUpdate={handleTimeUpdate}
|
|
|
|
|
onPause={handlePaused}
|
2022-01-13 02:10:59 +03:00
|
|
|
onClick={handleVideoTouch}
|
|
|
|
|
poster={`${apiHost}/api/events/${currentEvent.id}/snapshot.jpg`}
|
2022-01-13 08:09:39 +03:00
|
|
|
preload='none'
|
2022-01-13 02:10:59 +03:00
|
|
|
playsInline
|
|
|
|
|
controls
|
|
|
|
|
>
|
|
|
|
|
<source
|
|
|
|
|
src={`${apiHost}/api/${camera}/start/${currentEvent.startTime}/end/${currentEvent.endTime}/clip.mp4`}
|
|
|
|
|
/>
|
|
|
|
|
</video>
|
2022-01-13 08:09:39 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'debug':
|
|
|
|
|
renderPlayer = (
|
|
|
|
|
<Fragment>
|
|
|
|
|
<div>
|
|
|
|
|
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
|
|
|
|
|
</div>
|
|
|
|
|
{optionContent}
|
|
|
|
|
</Fragment>
|
2022-01-13 02:10:59 +03:00
|
|
|
);
|
2022-01-13 08:09:39 +03:00
|
|
|
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);
|
2022-01-13 01:53:50 +03:00
|
|
|
}
|
2022-01-13 08:09:39 +03:00
|
|
|
};
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const handleVideoTouch = () => {
|
|
|
|
|
setHideBanner(true);
|
2022-01-13 02:10:59 +03:00
|
|
|
};
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const handleTabChange = (index) => {
|
|
|
|
|
if (index === 0) {
|
2022-01-13 02:10:59 +03:00
|
|
|
setPlayerType('history');
|
|
|
|
|
} else if (index === 1) {
|
|
|
|
|
setPlayerType('live');
|
2022-01-13 01:53:50 +03:00
|
|
|
} else if (index === 2) {
|
2022-01-13 02:10:59 +03:00
|
|
|
setPlayerType('debug');
|
2022-01-13 01:53:50 +03:00
|
|
|
}
|
2022-01-13 02:10:59 +03:00
|
|
|
};
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
const handleTimelineChange = (event) => {
|
2022-01-13 08:09:39 +03:00
|
|
|
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);
|
2022-01-13 02:10:59 +03:00
|
|
|
};
|
2022-01-13 01:53:50 +03:00
|
|
|
|
|
|
|
|
return (
|
2022-01-13 08:09:39 +03:00
|
|
|
<div className='flex bg-gray-900 w-full h-full justify-center'>
|
|
|
|
|
<div className='relative max-w-screen-md flex-grow w-full'>
|
2022-01-13 08:12:07 +03:00
|
|
|
<div className='absolute top-0 text-white w-full'>
|
|
|
|
|
<div className='flex pt-4 pl-4 items-center w-full h-16 z10'>
|
2022-01-13 02:10:59 +03:00
|
|
|
{(playerType === 'live' || playerType === 'debug') && (
|
2022-01-13 01:53:50 +03:00
|
|
|
<Fragment>
|
2022-01-13 08:09:39 +03:00
|
|
|
<Heading size='xl' className='mr-2'>
|
2022-01-13 02:10:59 +03:00
|
|
|
{camera}
|
|
|
|
|
</Heading>
|
2022-01-13 01:53:50 +03:00
|
|
|
<LiveChip />
|
|
|
|
|
</Fragment>
|
2022-01-13 02:10:59 +03:00
|
|
|
)}
|
2022-01-13 01:53:50 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2022-01-13 08:09:39 +03:00
|
|
|
<div className='flex flex-col justify-center h-full'>
|
|
|
|
|
<div className='relative'>
|
2022-01-13 02:10:59 +03:00
|
|
|
{currentEvent && (
|
|
|
|
|
<HistoryHeader
|
|
|
|
|
camera={camera}
|
|
|
|
|
date={longToDate(currentEvent.start_time)}
|
|
|
|
|
objectLabel={currentEvent.label}
|
2022-01-13 08:09:39 +03:00
|
|
|
className='mb-2'
|
2022-01-13 02:10:59 +03:00
|
|
|
/>
|
|
|
|
|
)}
|
2022-01-13 08:09:39 +03:00
|
|
|
{renderPlayer}
|
2022-01-13 01:53:50 +03:00
|
|
|
</div>
|
|
|
|
|
|
2022-01-13 08:09:39 +03:00
|
|
|
{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>
|
|
|
|
|
)}
|
2022-01-13 01:53:50 +03:00
|
|
|
</div>
|
|
|
|
|
|
2022-01-13 08:09:39 +03:00
|
|
|
<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' />
|
2022-01-13 01:53:50 +03:00
|
|
|
</Tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|