Browse Source

Merge pull request #2026 from blakeblackshear/recording_fix

0.9.2
Blake Blackshear 3 năm trước cách đây
mục cha
commit
25bb515afc

+ 1 - 1
Makefile

@@ -3,7 +3,7 @@ default_target: amd64_frigate
 COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
 
 version:
-	echo "VERSION='0.9.1-$(COMMIT_HASH)'" > frigate/version.py
+	echo "VERSION='0.9.2-$(COMMIT_HASH)'" > frigate/version.py
 
 web:
 	docker build --tag frigate-web --file docker/Dockerfile.web web/

+ 2 - 2
docs/docs/configuration/camera_specific.md

@@ -15,8 +15,8 @@ Note that mjpeg cameras require encoding the video into h264 for recording, and
 
 ```yaml
 output_args:
-  record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
-  rtmp: -c:v libx264 -an -f flv
+  record: -f segment -segment_time 10 -segment_format ts -reset_timestamps 1 -strftime 1 -c:v libx264
+  rtmp: -c:v libx264 -f flv
 ```
 
 ### RTMP Cameras

+ 1 - 1
docs/docs/configuration/index.md

@@ -140,7 +140,7 @@ ffmpeg:
     # Optional: output args for detect streams (default: shown below)
     detect: -f rawvideo -pix_fmt yuv420p
     # Optional: output args for record streams (default: shown below)
-    record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
+    record: -f segment -segment_time 10 -segment_format ts -reset_timestamps 1 -strftime 1 -c copy
     # Optional: output args for rtmp streams (default: shown below)
     rtmp: -c copy -f flv
 

+ 1 - 1
docs/docs/faqs.md

@@ -13,7 +13,7 @@ A solid green image means that frigate has not received any frames from ffmpeg.
 
 ### How can I get sound or audio in my recordings?
 
-By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#ffmpeg).
+The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
 
 ### My mjpeg stream or snapshots look green and crazy
 

+ 22 - 0
docs/docs/hardware.md

@@ -44,3 +44,25 @@ The USB version is compatible with the widest variety of hardware and does not r
 The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai
 
 A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed.
+
+### What does Frigate use the CPU for and what does it use the Coral for? (ELI5 Version)
+
+This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
+
+CPU Usage: I am a CPU, Mendel is a Google Coral
+
+My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it.
+
+Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score!
+
+_What happens when I increase the resolution of my camera?_
+
+However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby.
+
+Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it.
+
+### Do hwaccel args help if I am using a Coral?
+
+YES! The Coral does not help with decoding video streams.
+
+Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://blog.video.ibm.com/streaming-video-tips/keyframes-interframe-video-compression/). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work.

+ 17 - 37
docs/package-lock.json

@@ -2469,31 +2469,11 @@
       "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM="
     },
     "ansi-align": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
-      "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+      "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
       "requires": {
-        "string-width": "^3.0.0"
-      },
-      "dependencies": {
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
+        "string-width": "^4.1.0"
       }
     },
     "ansi-colors": {
@@ -2902,15 +2882,15 @@
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
     },
     "boxen": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz",
-      "integrity": "sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+      "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
       "requires": {
         "ansi-align": "^3.0.0",
         "camelcase": "^6.2.0",
         "chalk": "^4.1.0",
         "cli-boxes": "^2.2.1",
-        "string-width": "^4.2.0",
+        "string-width": "^4.2.2",
         "type-fest": "^0.20.2",
         "widest-line": "^3.1.0",
         "wrap-ansi": "^7.0.0"
@@ -6826,9 +6806,9 @@
       "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
     },
     "nth-check": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
-      "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz",
+      "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==",
       "requires": {
         "boolbase": "^1.0.0"
       }
@@ -7650,9 +7630,9 @@
       "integrity": "sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg=="
     },
     "prismjs": {
-      "version": "1.24.1",
-      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz",
-      "integrity": "sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow=="
+      "version": "1.25.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
+      "integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
     },
     "process-nextick-args": {
       "version": "2.0.1",
@@ -9397,9 +9377,9 @@
       },
       "dependencies": {
         "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+          "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
         }
       }
     },

+ 2 - 2
docs/package.json

@@ -12,8 +12,8 @@
     "clear": "docusaurus clear"
   },
   "dependencies": {
-    "@docusaurus/core": "^2.0.0-beta.ff31de0ff",
-    "@docusaurus/preset-classic": "^2.0.0-beta.ff31de0ff",
+    "@docusaurus/core": "^2.0.0-beta.6",
+    "@docusaurus/preset-classic": "^2.0.0-beta.6",
     "@mdx-js/react": "^1.6.21",
     "clsx": "^1.1.1",
     "raw-loader": "^4.0.2",

+ 14 - 25
frigate/app.py

@@ -12,6 +12,7 @@ import yaml
 from peewee_migrate import Router
 from playhouse.sqlite_ext import SqliteExtDatabase
 from playhouse.sqliteq import SqliteQueueDatabase
+from pydantic import ValidationError
 
 from frigate.config import DetectorTypeEnum, FrigateConfig
 from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
@@ -85,29 +86,6 @@ class FrigateApp:
                 "frame_queue": mp.Queue(maxsize=2),
             }
 
-    def check_config(self):
-        for name, camera in self.config.cameras.items():
-            assigned_roles = list(
-                set([r for i in camera.ffmpeg.inputs for r in i.roles])
-            )
-            if not camera.record.enabled and "record" in assigned_roles:
-                logger.warning(
-                    f"Camera {name} has record assigned to an input, but record is not enabled."
-                )
-            elif camera.record.enabled and not "record" in assigned_roles:
-                logger.warning(
-                    f"Camera {name} has record enabled, but record is not assigned to an input."
-                )
-
-            if not camera.rtmp.enabled and "rtmp" in assigned_roles:
-                logger.warning(
-                    f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled."
-                )
-            elif camera.rtmp.enabled and not "rtmp" in assigned_roles:
-                logger.warning(
-                    f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
-                )
-
     def set_log_levels(self):
         logging.getLogger().setLevel(self.config.logger.default.value.upper())
         for log, level in self.config.logger.logs.items():
@@ -320,12 +298,23 @@ class FrigateApp:
             try:
                 self.init_config()
             except Exception as e:
-                print(f"Error parsing config: {e}")
+                print("*************************************************************")
+                print("*************************************************************")
+                print("***    Your config file is not valid!                     ***")
+                print("***    Please check the docs at                           ***")
+                print("***    https://docs.frigate.video/configuration/index     ***")
+                print("*************************************************************")
+                print("*************************************************************")
+                print("***    Config Validation Errors                           ***")
+                print("*************************************************************")
+                print(e)
+                print("*************************************************************")
+                print("***    End Config Validation Errors                       ***")
+                print("*************************************************************")
                 self.log_process.terminate()
                 sys.exit(1)
             self.set_environment_vars()
             self.ensure_dirs()
-            self.check_config()
             self.set_log_levels()
             self.init_queues()
             self.init_database()

+ 31 - 3
frigate/config.py

@@ -298,14 +298,13 @@ RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
     "-segment_time",
     "10",
     "-segment_format",
-    "mp4",
+    "ts",
     "-reset_timestamps",
     "1",
     "-strftime",
     "1",
     "-c",
     "copy",
-    "-an",
 ]
 
 
@@ -505,6 +504,10 @@ class CameraConfig(FrigateBaseModel):
                 for idx, (name, z) in enumerate(config["zones"].items())
             }
 
+        # add roles to the input if there is only one
+        if len(config["ffmpeg"]["inputs"]) == 1:
+            config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
+
         super().__init__(**config)
 
     @property
@@ -560,9 +563,17 @@ class CameraConfig(FrigateBaseModel):
                 if isinstance(self.ffmpeg.output_args.record, list)
                 else self.ffmpeg.output_args.record.split(" ")
             )
+
+            # backwards compatibility check for segment_format change from mp4 to ts
+            record_args = (
+                " ".join(record_args)
+                .replace("-segment_format mp4", "-segment_format ts")
+                .split(" ")
+            )
+
             ffmpeg_output_args = (
                 record_args
-                + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
+                + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.ts"]
                 + ffmpeg_output_args
             )
 
@@ -799,6 +810,23 @@ class FrigateConfig(FrigateBaseModel):
                 raise ValueError("Zones cannot share names with cameras")
         return v
 
+    @validator("cameras")
+    def ensure_cameras_are_not_missing_roles(cls, v: Dict[str, CameraConfig]):
+        for name, camera in v.items():
+            assigned_roles = list(
+                set([r for i in camera.ffmpeg.inputs for r in i.roles])
+            )
+            if camera.record.enabled and not "record" in assigned_roles:
+                raise ValueError(
+                    f"Camera {name} has record enabled, but record is not assigned to an input."
+                )
+
+            if camera.rtmp.enabled and not "rtmp" in assigned_roles:
+                raise ValueError(
+                    f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
+                )
+        return v
+
     @classmethod
     def parse_file(cls, config_file):
         with open(config_file) as f:

+ 32 - 8
frigate/record.py

@@ -48,9 +48,7 @@ class RecordingMaintainer(threading.Thread):
         recordings = [
             d
             for d in os.listdir(CACHE_DIR)
-            if os.path.isfile(os.path.join(CACHE_DIR, d))
-            and d.endswith(".mp4")
-            and not d.startswith("clip_")
+            if os.path.isfile(os.path.join(CACHE_DIR, d)) and d.endswith(".ts")
         ]
 
         files_in_use = []
@@ -113,9 +111,30 @@ class RecordingMaintainer(threading.Thread):
             file_name = f"{start_time.strftime('%M.%S.mp4')}"
             file_path = os.path.join(directory, file_name)
 
-            # copy then delete is required when recordings are stored on some network drives
-            shutil.copyfile(cache_path, file_path)
-            os.remove(cache_path)
+            ffmpeg_cmd = [
+                "ffmpeg",
+                "-y",
+                "-i",
+                cache_path,
+                "-c",
+                "copy",
+                "-movflags",
+                "+faststart",
+                file_path,
+            ]
+
+            p = sp.run(
+                ffmpeg_cmd,
+                encoding="ascii",
+                capture_output=True,
+            )
+
+            Path(cache_path).unlink(missing_ok=True)
+
+            if p.returncode != 0:
+                logger.error(f"Unable to convert {cache_path} to {file_path}")
+                logger.error(p.stderr)
+                continue
 
             rand_id = "".join(
                 random.choices(string.ascii_lowercase + string.digits, k=6)
@@ -203,7 +222,11 @@ class RecordingCleanup(threading.Thread):
             events: Event = (
                 Event.select()
                 .where(
-                    Event.camera == camera, Event.end_time < expire_date, Event.has_clip
+                    Event.camera == camera,
+                    # need to ensure segments for all events starting
+                    # before the expire date are included
+                    Event.start_time < expire_date,
+                    Event.has_clip,
                 )
                 .order_by(Event.start_time)
                 .objects()
@@ -271,7 +294,8 @@ class RecordingCleanup(threading.Thread):
                 Recordings.select().order_by(Recordings.start_time.desc()).get()
             )
 
-            oldest_timestamp = oldest_recording.start_time
+            p = Path(oldest_recording.path)
+            oldest_timestamp = p.stat().st_mtime - 1
         except DoesNotExist:
             oldest_timestamp = datetime.datetime.now().timestamp()
 

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 211 - 317
web/package-lock.json


+ 1 - 1
web/package.json

@@ -14,7 +14,7 @@
     "@cycjimmy/jsmpeg-player": "^5.0.1",
     "date-fns": "^2.21.3",
     "idb-keyval": "^5.0.2",
-    "immer": "^8.0.1",
+    "immer": "^9.0.6",
     "preact": "^10.5.9",
     "preact-async-route": "^2.2.1",
     "preact-router": "^3.2.1",

+ 2 - 2
web/src/components/JSMpegPlayer.jsx

@@ -3,7 +3,7 @@ import { baseUrl } from '../api/baseUrl';
 import { useRef, useEffect } from 'preact/hooks';
 import JSMpeg from '@cycjimmy/jsmpeg-player';
 
-export default function JSMpegPlayer({ camera }) {
+export default function JSMpegPlayer({ camera, width, height }) {
   const playerRef = useRef();
   const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`
 
@@ -32,6 +32,6 @@ export default function JSMpegPlayer({ camera }) {
   }, [url]);
 
   return (
-    <div ref={playerRef} class="jsmpeg" />
+    <div ref={playerRef} class="jsmpeg" style={`max-height: ${height}px; max-width: ${width}px`} />
   );
 }

+ 2 - 1
web/src/routes/Camera.jsx

@@ -21,6 +21,7 @@ export default function Camera({ camera }) {
   const [viewMode, setViewMode] = useState('live');
 
   const cameraConfig = config?.cameras[camera];
+  const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height))
   const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
 
   const handleSetOption = useCallback(
@@ -87,7 +88,7 @@ export default function Camera({ camera }) {
     player = (
       <Fragment>
         <div>
-          <JSMpegPlayer camera={camera} />
+          <JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
         </div>
       </Fragment>
     );

+ 1 - 1
web/src/routes/__tests__/Camera.test.jsx

@@ -13,7 +13,7 @@ describe('Camera Route', () => {
     mockSetOptions = jest.fn();
     mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]);
     jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
-      data: { cameras: { front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } } } },
+      data: { cameras: { front: { name: 'front', detect: {width: 1280, height: 720}, live: {height: 720}, objects: { track: ['taco', 'cat', 'dog'] } } } },
     }));
     jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
     jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => {

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác