mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-31 00:18:55 +01:00
fix videojs bug when switching cameras, support recording delay, fix navigation highlight
This commit is contained in:
parent
ca20c735f7
commit
9822d614e2
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user