mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-04 13:47:37 +02: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,
|
request,
|
||||||
)
|
)
|
||||||
from flask_sockets import Sockets
|
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 playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
from frigate.const import CLIPS_DIR, RECORD_DIR
|
from frigate.const import CLIPS_DIR, RECORD_DIR
|
||||||
@ -462,15 +462,54 @@ def recordings(camera_name):
|
|||||||
|
|
||||||
dates = OrderedDict()
|
dates = OrderedDict()
|
||||||
for path in files:
|
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)
|
search = re.search(r".+/(\d{4}[-]\d{2})/(\d{2})/(\d{2}).+", path)
|
||||||
if not search:
|
if not search:
|
||||||
continue
|
continue
|
||||||
date = f"{search.group(1)}-{search.group(2)}"
|
date = f"{search.group(1)}-{search.group(2)}"
|
||||||
if date not in dates:
|
if date not in dates:
|
||||||
dates[date] = OrderedDict()
|
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
|
e: Event
|
||||||
for e in events:
|
for e in events:
|
||||||
@ -478,14 +517,26 @@ def recordings(camera_name):
|
|||||||
key = date.strftime("%Y-%m-%d")
|
key = date.strftime("%Y-%m-%d")
|
||||||
hour = date.strftime("%H")
|
hour = date.strftime("%H")
|
||||||
if key in dates and hour in dates[key]:
|
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(
|
return jsonify(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"date": date,
|
"date": date,
|
||||||
"recordings": [
|
"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()
|
for date, hours in dates.items()
|
||||||
|
@ -27,13 +27,17 @@ export default function Sidebar() {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</Match>
|
</Match>
|
||||||
<Match path="/recordings/:camera/:date?/:hour?">
|
<Match path="/recordings/:camera/:date?/:hour?/:seconds?">
|
||||||
{({ matches }) =>
|
{({ matches }) =>
|
||||||
matches ? (
|
matches ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Separator />
|
<Separator />
|
||||||
{cameras.map((camera) => (
|
{cameras.map((camera) => (
|
||||||
<Destination href={`/recordings/${camera}`} text={camera} />
|
<Destination
|
||||||
|
path={`/recordings/${camera}/:date?/:hour?/:seconds?`}
|
||||||
|
href={`/recordings/${camera}`}
|
||||||
|
text={camera}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<Separator />
|
<Separator />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { h } from 'preact';
|
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 Link from '../components/Link';
|
||||||
import { useApiHost } from '../api';
|
import { useApiHost } from '../api';
|
||||||
|
|
||||||
export default function EventCard({ camera, event }) {
|
export default function EventCard({ camera, event, delay }) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const start = fromUnixTime(event.start_time);
|
const start = fromUnixTime(event.start_time);
|
||||||
const end = fromUnixTime(event.end_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 (
|
return (
|
||||||
<Link className="" href={`/recordings/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
|
<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">
|
<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-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">
|
<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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import Link from '../components/Link';
|
|||||||
import Menu from '../icons/Menu';
|
import Menu from '../icons/Menu';
|
||||||
import MenuOpen from '../icons/MenuOpen';
|
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 [active, setActive] = useState(true);
|
||||||
const toggle = () => setActive(!active);
|
const toggle = () => setActive(!active);
|
||||||
|
|
||||||
@ -19,13 +19,17 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
|||||||
{recording.recordings.map((item) => (
|
{recording.recordings.map((item) => (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
|
<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">
|
{recording.date === selectedDate && item.hour === selectedHour ? (
|
||||||
{item.hour}:00
|
<span className="text-green-500">{item.hour}:00</span>
|
||||||
</Link>
|
) : (
|
||||||
|
<Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
|
||||||
|
{item.hour}:00
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<span className="float-right">{item.events.length} Events</span>
|
<span className="float-right">{item.events.length} Events</span>
|
||||||
</div>
|
</div>
|
||||||
{item.events.map((event) => (
|
{item.events.map((event) => (
|
||||||
<EventCard camera={camera} event={event} />
|
<EventCard camera={camera} event={event} delay={item.delay} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import 'videojs-playlist';
|
import 'videojs-playlist';
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
@ -8,6 +9,27 @@ const defaultOptions = {
|
|||||||
fluid: true,
|
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 {
|
export default class VideoPlayer extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { options, onReady = () => {} } = this.props;
|
const { options, onReady = () => {} } = this.props;
|
||||||
@ -21,14 +43,16 @@ export default class VideoPlayer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
const { onDispose = () => {} } = this.props;
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
this.player.dispose();
|
this.player.dispose();
|
||||||
|
onDispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
// shouldComponentUpdate() {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { style, children } = this.props;
|
const { style, children } = this.props;
|
||||||
|
@ -44,7 +44,7 @@ export default function Recording({ camera, date, hour, seconds }) {
|
|||||||
|
|
||||||
const selectedHour = hours.indexOf(hour);
|
const selectedHour = hours.indexOf(hour);
|
||||||
|
|
||||||
if (this.player !== undefined) {
|
if (this.player) {
|
||||||
this.player.playlist([]);
|
this.player.playlist([]);
|
||||||
this.player.playlist(playlist);
|
this.player.playlist(playlist);
|
||||||
this.player.playlist.autoadvance(0);
|
this.player.playlist.autoadvance(0);
|
||||||
@ -74,8 +74,11 @@ export default function Recording({ camera, date, hour, seconds }) {
|
|||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onDispose={() => {
|
||||||
|
this.player = null;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} />
|
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} selectedHour={hour} />
|
||||||
</VideoPlayer>
|
</VideoPlayer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user