frigate/web/src/routes/Camera_V2.jsx

263 lines
7.8 KiB
React
Raw Normal View History

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';
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';
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();
const videoRef = useRef();
2022-01-13 02:10:59 +03:00
2022-01-13 01:53:50 +03:00
const { data: config } = useConfig();
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);
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);
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 = (
<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']}
id='bbox'
2022-01-13 01:53:50 +03:00
onChange={handleSetOption}
label='Bounding box'
labelPosition='after'
2022-01-13 01:53:50 +03:00
/>
<Switch
checked={options['timestamp']}
id='timestamp'
2022-01-13 01:53:50 +03:00
onChange={handleSetOption}
label='Timestamp'
labelPosition='after'
2022-01-13 01:53:50 +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']}
id='motion'
2022-01-13 01:53:50 +03:00
onChange={handleSetOption}
label='Motion boxes'
labelPosition='after'
2022-01-13 01:53:50 +03:00
/>
<Switch
checked={options['regions']}
id='regions'
2022-01-13 01:53:50 +03:00
onChange={handleSetOption}
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
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
);
break;
case 'history':
if (currentEvent) {
renderPlayer = (
2022-01-13 02:10:59 +03:00
<video
ref={videoRef}
onTimeUpdate={handleTimeUpdate}
onPause={handlePaused}
2022-01-13 02:10:59 +03:00
onClick={handleVideoTouch}
poster={`${apiHost}/api/events/${currentEvent.id}/snapshot.jpg`}
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>
);
}
break;
case 'debug':
renderPlayer = (
<Fragment>
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
</div>
{optionContent}
</Fragment>
2022-01-13 02:10:59 +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 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) => {
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 (
<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 02:10:59 +03:00
<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'>
2022-01-13 02:10:59 +03:00
{(playerType === 'live' || playerType === 'debug') && (
2022-01-13 01:53:50 +03:00
<Fragment>
<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>
<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}
className='mb-2'
2022-01-13 02:10:59 +03:00
/>
)}
{renderPlayer}
2022-01-13 01:53:50 +03:00
</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>
)}
2022-01-13 01:53:50 +03:00
</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' />
2022-01-13 01:53:50 +03:00
</Tabs>
</div>
</div>
</div>
);
}