diff --git a/frigate/http.py b/frigate/http.py index f3e97f944..fe5ae5ec8 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -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() diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index cd37eda0c..8ddc1ea7d 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -27,13 +27,17 @@ export default function Sidebar() { ) : null } - + {({ matches }) => matches ? ( {cameras.map((camera) => ( - + ))} diff --git a/web/src/components/EventCard.jsx b/web/src/components/EventCard.jsx index 549b5deb7..8a3a3f0a2 100644 --- a/web/src/components/EventCard.jsx +++ b/web/src/components/EventCard.jsx @@ -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 (
@@ -20,18 +21,11 @@ export default function EventCard({ camera, event }) {
{(event.top_score * 100).toFixed(1)}%
- {format(start, 'HH:mm:ss')} - {format(end, 'HH:mm:ss')} + {format(start, 'HH:mm:ss')} ({format(duration, 'mm:ss')})
-
-
- {event.zones.map((zone) => ( -
{zone}
- ))} -
-
); diff --git a/web/src/components/RecordingPlaylist.jsx b/web/src/components/RecordingPlaylist.jsx index 8aaae08e2..491bdbe31 100644 --- a/web/src/components/RecordingPlaylist.jsx +++ b/web/src/components/RecordingPlaylist.jsx @@ -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) => (
- - {item.hour}:00 - + {recording.date === selectedDate && item.hour === selectedHour ? ( + {item.hour}:00 + ) : ( + + {item.hour}:00 + + )} {item.events.length} Events
{item.events.map((event) => ( - + ))}
))} diff --git a/web/src/components/VideoPlayer.jsx b/web/src/components/VideoPlayer.jsx index 2bfc63219..0dc2f2d60 100644 --- a/web/src/components/VideoPlayer.jsx +++ b/web/src/components/VideoPlayer.jsx @@ -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 ( +//
+//
+// ); +// } + 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; diff --git a/web/src/routes/Recording.jsx b/web/src/routes/Recording.jsx index 75f71fef9..9626bb4cd 100644 --- a/web/src/routes/Recording.jsx +++ b/web/src/routes/Recording.jsx @@ -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; + }} > - + );