fix videojs bug when switching cameras, support recording delay, fix navigation highlight

This commit is contained in:
Jason Hunter 2021-06-02 23:20:07 -04:00 committed by Blake Blackshear
parent ca20c735f7
commit 9822d614e2
6 changed files with 108 additions and 28 deletions

View File

@ -23,7 +23,7 @@ from flask import (
request,
)
from flask_sockets import Sockets
from peewee import SqliteDatabase, operator, fn, DoesNotExist
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value
from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR, RECORD_DIR
@ -462,15 +462,54 @@ def recordings(camera_name):
dates = OrderedDict()
for path in files:
first = glob.glob(f"{path}/00.*.mp4")
delay = 0
if len(first) > 0:
delay = int(first[0].strip(path).split(".")[1])
search = re.search(r".+/(\d{4}[-]\d{2})/(\d{2})/(\d{2}).+", path)
if not search:
continue
date = f"{search.group(1)}-{search.group(2)}"
if date not in dates:
dates[date] = OrderedDict()
dates[date][search.group(3)] = []
dates[date][search.group(3)] = {"delay": delay, "events": []}
events = Event.select().where(Event.camera == camera_name)
# Packing intervals to return all events with same label and overlapping times as one row.
# See: https://blogs.solidq.com/en/sqlserver/packing-intervals/
events = Event.raw(
"""WITH C1 AS
(
SELECT id, label, camera, top_score, start_time AS ts, +1 AS type, 1 AS sub
FROM event
WHERE camera = ?
UNION ALL
SELECT id, label, camera, top_score, end_time + 15 AS ts, -1 AS type, 0 AS sub
FROM event
WHERE camera = ?
),
C2 AS
(
SELECT C1.*,
SUM(type) OVER(PARTITION BY label ORDER BY ts, type DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) - sub AS cnt
FROM C1
),
C3 AS
(
SELECT id, label, camera, top_score, ts,
(ROW_NUMBER() OVER(PARTITION BY label ORDER BY ts) - 1) / 2 + 1
AS grpnum
FROM C2
WHERE cnt = 0
)
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
FROM C3
GROUP BY label, grpnum
ORDER BY start_time;""",
camera_name,
camera_name,
)
e: Event
for e in events:
@ -478,14 +517,26 @@ def recordings(camera_name):
key = date.strftime("%Y-%m-%d")
hour = date.strftime("%H")
if key in dates and hour in dates[key]:
dates[key][hour].append(model_to_dict(e, exclude=[Event.thumbnail]))
dates[key][hour]["events"].append(
model_to_dict(
e,
exclude=[
Event.false_positive,
Event.zones,
Event.thumbnail,
Event.has_clip,
Event.has_snapshot,
],
)
)
return jsonify(
[
{
"date": date,
"recordings": [
{"hour": hour, "events": events} for hour, events in hours.items()
{"hour": hour, "delay": value["delay"], "events": value["events"]}
for hour, value in hours.items()
],
}
for date, hours in dates.items()

View File

@ -27,13 +27,17 @@ export default function Sidebar() {
) : null
}
</Match>
<Match path="/recordings/:camera/:date?/:hour?">
<Match path="/recordings/:camera/:date?/:hour?/:seconds?">
{({ matches }) =>
matches ? (
<Fragment>
<Separator />
{cameras.map((camera) => (
<Destination href={`/recordings/${camera}`} text={camera} />
<Destination
path={`/recordings/${camera}/:date?/:hour?/:seconds?`}
href={`/recordings/${camera}`}
text={camera}
/>
))}
<Separator />
</Fragment>

View File

@ -1,13 +1,14 @@
import { h } from 'preact';
import { differenceInSeconds, fromUnixTime, format, startOfHour } from 'date-fns';
import { addSeconds, differenceInSeconds, fromUnixTime, format, startOfHour } from 'date-fns';
import Link from '../components/Link';
import { useApiHost } from '../api';
export default function EventCard({ camera, event }) {
export default function EventCard({ camera, event, delay }) {
const apiHost = useApiHost();
const start = fromUnixTime(event.start_time);
const end = fromUnixTime(event.end_time);
const seconds = Math.max(differenceInSeconds(start, startOfHour(start)) - 10, 0);
const duration = addSeconds(new Date(0), differenceInSeconds(end, start));
const seconds = Math.max(differenceInSeconds(start, startOfHour(start)) - delay - 10, 0);
return (
<Link className="" href={`/recordings/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
<div className="rounded-lg shadow-lg bg-gray-600 w-full flex flex-row flex-wrap p-3 antialiased mb-2">
@ -20,18 +21,11 @@ export default function EventCard({ camera, event }) {
<div className="text-lg text-white leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
<div className="text-xs md:text-normal text-gray-300 hover:text-gray-400 cursor-pointer">
<span className="border-b border-dashed border-gray-500 pb-1">
{format(start, 'HH:mm:ss')} - {format(end, 'HH:mm:ss')}
{format(start, 'HH:mm:ss')} ({format(duration, 'mm:ss')})
</span>
</div>
</div>
</div>
<div className="hidden md:block w-full text-right">
<div className="text-sm text-gray-300 hover:text-gray-400 cursor-pointer md:absolute pt-3 md:pt-0 bottom-0 right-0">
{event.zones.map((zone) => (
<div>{zone}</div>
))}
</div>
</div>
</div>
</Link>
);

View File

@ -7,7 +7,7 @@ import Link from '../components/Link';
import Menu from '../icons/Menu';
import MenuOpen from '../icons/MenuOpen';
export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
export default function RecordingPlaylist({ camera, recordings, selectedDate, selectedHour }) {
const [active, setActive] = useState(true);
const toggle = () => setActive(!active);
@ -19,13 +19,17 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
{recording.recordings.map((item) => (
<div className="mb-2">
<div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
<Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
{item.hour}:00
</Link>
{recording.date === selectedDate && item.hour === selectedHour ? (
<span className="text-green-500">{item.hour}:00</span>
) : (
<Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
{item.hour}:00
</Link>
)}
<span className="float-right">{item.events.length} Events</span>
</div>
{item.events.map((event) => (
<EventCard camera={camera} event={event} />
<EventCard camera={camera} event={event} delay={item.delay} />
))}
</div>
))}

View File

@ -1,4 +1,5 @@
import { h, Component } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import videojs from 'video.js';
import 'videojs-playlist';
import 'video.js/dist/video-js.css';
@ -8,6 +9,27 @@ const defaultOptions = {
fluid: true,
};
// export default function VideoPlayer({ children, options, onReady = () => {} }) {
// const playerRef = useRef(null);
// useEffect(() => {
// if (playerRef.current) {
// const player = videojs(playerRef.current, { ...defaultOptions, ...options }, () => {
// onReady(player);
// });
// return () => {
// player.dispose();
// };
// }
// }, [options, onReady]);
// return (
// <div data-vjs-player>
// <video ref={playerRef} className="video-js vjs-default-skin" controls playsInline />
// {children}
// </div>
// );
// }
export default class VideoPlayer extends Component {
componentDidMount() {
const { options, onReady = () => {} } = this.props;
@ -21,14 +43,16 @@ export default class VideoPlayer extends Component {
}
componentWillUnmount() {
const { onDispose = () => {} } = this.props;
if (this.player) {
this.player.dispose();
onDispose();
}
}
shouldComponentUpdate() {
return false;
}
// shouldComponentUpdate() {
// return false;
// }
render() {
const { style, children } = this.props;

View File

@ -44,7 +44,7 @@ export default function Recording({ camera, date, hour, seconds }) {
const selectedHour = hours.indexOf(hour);
if (this.player !== undefined) {
if (this.player) {
this.player.playlist([]);
this.player.playlist(playlist);
this.player.playlist.autoadvance(0);
@ -74,8 +74,11 @@ export default function Recording({ camera, date, hour, seconds }) {
this.player = player;
}
}}
onDispose={() => {
this.player = null;
}}
>
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} />
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} selectedHour={hour} />
</VideoPlayer>
</div>
);