forked from Github/frigate
feat: Timeline UI (#2830)
This commit is contained in:
30
web/src/components/HistoryViewer/HistoryHeader.tsx
Normal file
30
web/src/components/HistoryViewer/HistoryHeader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { h } from 'preact';
|
||||
import Heading from '../Heading';
|
||||
import { TimelineEvent } from '../Timeline/Timeline';
|
||||
|
||||
interface HistoryHeaderProps {
|
||||
event: TimelineEvent;
|
||||
className?: string;
|
||||
}
|
||||
export const HistoryHeader = ({ event, className = '' }: HistoryHeaderProps) => {
|
||||
let title = 'No Event Found';
|
||||
let subtitle = <span>Event was not found at marker position.</span>;
|
||||
if (event) {
|
||||
const { startTime, endTime, label } = event;
|
||||
const thisMorning = new Date();
|
||||
thisMorning.setHours(0, 0, 0);
|
||||
const isToday = endTime.getTime() > thisMorning.getTime();
|
||||
title = label;
|
||||
subtitle = (
|
||||
<span>
|
||||
{isToday ? 'Today' : 'Yesterday'}, {startTime.toLocaleTimeString()} - {endTime.toLocaleTimeString()} ·
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`text-center ${className}`}>
|
||||
<Heading size='lg'>{title}</Heading>
|
||||
<div>{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
134
web/src/components/HistoryViewer/HistoryVideo.tsx
Normal file
134
web/src/components/HistoryViewer/HistoryVideo.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { h } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useApiHost } from '../../api';
|
||||
import { isNullOrUndefined } from '../../utils/objectUtils';
|
||||
|
||||
interface OnTimeUpdateEvent {
|
||||
timestamp: number;
|
||||
isPlaying: boolean;
|
||||
}
|
||||
|
||||
interface VideoProperties {
|
||||
posterUrl: string;
|
||||
videoUrl: string;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface HistoryVideoProps {
|
||||
id: string;
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
onTimeUpdate?: (event: OnTimeUpdateEvent) => void;
|
||||
onPause: () => void;
|
||||
onPlay: () => void;
|
||||
}
|
||||
|
||||
export const HistoryVideo = ({
|
||||
id,
|
||||
isPlaying: videoIsPlaying,
|
||||
currentTime,
|
||||
onTimeUpdate,
|
||||
onPause,
|
||||
onPlay,
|
||||
}: HistoryVideoProps) => {
|
||||
const apiHost = useApiHost();
|
||||
const videoRef = useRef<HTMLVideoElement>();
|
||||
const [videoHeight, setVideoHeight] = useState<number>(undefined);
|
||||
const [videoProperties, setVideoProperties] = useState<VideoProperties>(undefined);
|
||||
|
||||
const currentVideo = videoRef.current;
|
||||
if (currentVideo && !videoHeight) {
|
||||
const currentVideoHeight = currentVideo.offsetHeight;
|
||||
if (currentVideoHeight > 0) {
|
||||
setVideoHeight(currentVideoHeight);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const idExists = !isNullOrUndefined(id);
|
||||
if (idExists) {
|
||||
if (videoRef.current && !videoRef.current.paused) {
|
||||
videoRef.current = undefined;
|
||||
}
|
||||
|
||||
setVideoProperties({
|
||||
posterUrl: `${apiHost}/api/events/${id}/snapshot.jpg`,
|
||||
videoUrl: `${apiHost}/vod/event/${id}/index.m3u8`,
|
||||
height: videoHeight,
|
||||
});
|
||||
} else {
|
||||
setVideoProperties(undefined);
|
||||
}
|
||||
}, [id, videoHeight, videoRef, apiHost]);
|
||||
|
||||
useEffect(() => {
|
||||
const playVideo = (video: HTMLMediaElement) => video.play();
|
||||
|
||||
const attemptPlayVideo = (video: HTMLMediaElement) => {
|
||||
const videoHasNotLoaded = video.readyState <= 1;
|
||||
if (videoHasNotLoaded) {
|
||||
video.oncanplay = () => {
|
||||
playVideo(video);
|
||||
};
|
||||
video.load();
|
||||
} else {
|
||||
playVideo(video);
|
||||
}
|
||||
};
|
||||
|
||||
const video = videoRef.current;
|
||||
const videoExists = !isNullOrUndefined(video);
|
||||
if (videoExists) {
|
||||
if (videoIsPlaying) {
|
||||
attemptPlayVideo(video);
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
}, [videoIsPlaying, videoRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
const videoExists = !isNullOrUndefined(video);
|
||||
const hasSeeked = currentTime >= 0;
|
||||
if (videoExists && hasSeeked) {
|
||||
video.currentTime = currentTime;
|
||||
}
|
||||
}, [currentTime, videoRef]);
|
||||
|
||||
const onTimeUpdateHandler = useCallback(
|
||||
(event: Event) => {
|
||||
const target = event.target as HTMLMediaElement;
|
||||
const timeUpdateEvent = {
|
||||
isPlaying: videoIsPlaying,
|
||||
timestamp: target.currentTime,
|
||||
};
|
||||
|
||||
onTimeUpdate && onTimeUpdate(timeUpdateEvent);
|
||||
},
|
||||
[videoIsPlaying, onTimeUpdate]
|
||||
);
|
||||
|
||||
const videoPropertiesIsUndefined = isNullOrUndefined(videoProperties);
|
||||
if (videoPropertiesIsUndefined) {
|
||||
return <div style={{ height: `${videoHeight}px`, width: '100%' }} />;
|
||||
}
|
||||
|
||||
const { posterUrl, videoUrl, height } = videoProperties;
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
key={posterUrl}
|
||||
onTimeUpdate={onTimeUpdateHandler}
|
||||
onPause={onPause}
|
||||
onPlay={onPlay}
|
||||
poster={posterUrl}
|
||||
preload='metadata'
|
||||
controls
|
||||
style={height ? { minHeight: `${height}px` } : {}}
|
||||
playsInline
|
||||
>
|
||||
<source type='application/vnd.apple.mpegurl' src={videoUrl} />
|
||||
</video>
|
||||
);
|
||||
};
|
||||
79
web/src/components/HistoryViewer/HistoryViewer.tsx
Normal file
79
web/src/components/HistoryViewer/HistoryViewer.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Fragment, h } from 'preact';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { useEvents } from '../../api';
|
||||
import { useSearchString } from '../../hooks/useSearchString';
|
||||
import { getNowYesterdayInLong } from '../../utils/dateUtil';
|
||||
import Timeline from '../Timeline/Timeline';
|
||||
import { TimelineChangeEvent } from '../Timeline/TimelineChangeEvent';
|
||||
import { TimelineEvent } from '../Timeline/TimelineEvent';
|
||||
import { HistoryHeader } from './HistoryHeader';
|
||||
import { HistoryVideo } from './HistoryVideo';
|
||||
|
||||
export default function HistoryViewer({ camera }) {
|
||||
const { searchString } = useSearchString(500, `camera=${camera}&after=${getNowYesterdayInLong()}`);
|
||||
const { data: events } = useEvents(searchString);
|
||||
|
||||
const [timelineEvents, setTimelineEvents] = useState<TimelineEvent[]>(undefined);
|
||||
const [currentEvent, setCurrentEvent] = useState<TimelineEvent>(undefined);
|
||||
const [isPlaying, setIsPlaying] = useState(undefined);
|
||||
const [currentTime, setCurrentTime] = useState<number>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (events) {
|
||||
const filteredEvents = [...events].reverse().filter((e) => e.end_time !== undefined);
|
||||
setTimelineEvents(filteredEvents);
|
||||
}
|
||||
}, [events]);
|
||||
|
||||
const handleTimelineChange = useCallback(
|
||||
(event: TimelineChangeEvent) => {
|
||||
if (event.seekComplete) {
|
||||
setCurrentEvent(event.timelineEvent);
|
||||
|
||||
if (isPlaying && event.timelineEvent) {
|
||||
const eventTime = (event.markerTime.getTime() - event.timelineEvent.startTime.getTime()) / 1000;
|
||||
setCurrentTime(eventTime);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isPlaying]
|
||||
);
|
||||
|
||||
const onPlayHandler = () => {
|
||||
setIsPlaying(true);
|
||||
};
|
||||
|
||||
const onPausedHandler = () => {
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const onPlayPauseHandler = (isPlaying: boolean) => {
|
||||
setIsPlaying(isPlaying);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
<div className='relative flex flex-col'>
|
||||
<Fragment>
|
||||
<HistoryHeader event={currentEvent} className='mb-2' />
|
||||
<HistoryVideo
|
||||
id={currentEvent ? currentEvent.id : undefined}
|
||||
isPlaying={isPlaying}
|
||||
currentTime={currentTime}
|
||||
onPlay={onPlayHandler}
|
||||
onPause={onPausedHandler}
|
||||
/>
|
||||
</Fragment>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
<Timeline
|
||||
events={timelineEvents}
|
||||
isPlaying={isPlaying}
|
||||
onChange={handleTimelineChange}
|
||||
onPlayPause={onPlayPauseHandler}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user