feat: Timeline UI (#2830)

This commit is contained in:
JohnMark Sill
2022-02-27 08:04:12 -06:00
committed by GitHub
parent 4004048add
commit 3e07d4eddb
54 changed files with 1950 additions and 50 deletions

View 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()} &middot;
</span>
);
}
return (
<div className={`text-center ${className}`}>
<Heading size='lg'>{title}</Heading>
<div>{subtitle}</div>
</div>
);
};

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

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