Sfoglia il codice sorgente

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

Jason Hunter 3 anni fa
parent
commit
9822d614e2

+ 57 - 6
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)] = []
-
-    events = Event.select().where(Event.camera == camera_name)
+        dates[date][search.group(3)] = {"delay": delay, "events": []}
+
+    # 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()

+ 6 - 2
web/src/Sidebar.jsx

@@ -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>

+ 5 - 11
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 (
     <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>
   );

+ 9 - 5
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) => (
           <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>
         ))}

+ 27 - 3
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 (
+//     <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;

+ 5 - 2
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;
+        }}
       >
-        <RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} />
+        <RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} selectedHour={hour} />
       </VideoPlayer>
     </div>
   );