Repository: blakeblackshear/frigate

Issue:
[Support]: rtmp can no longer be disabled globally
### Describe the problem you are having

rtmp can no longer be disabled globally in 0.9.2 (as documented [here](https://docs.frigate.video/configuration/index)) and now results in a validation error.

Quick fix is to explicitly disable rtmp on all cameras in the cameras section.

### Version

0.9.2-25bb515

### Frigate config file

```yaml
mqtt:
  host: mqtt

detectors:
  coral:
    type: edgetpu
    device: usb

ffmpeg:
  hwaccel_args: -c:v h264_v4l2m2m  # Raspberry Pi 3/4 (64-bit OS)

detect:
  width: 640
  height: 360
  fps: 3

# Optional: RTMP configuration
# NOTE: Can be overridden at the camera level
rtmp:
  # Optional: Enable the RTMP stream (default: True)
  enabled: False

cameras:
  Kitchen:
    ffmpeg:
      inputs:
        - path: rtsp://192.168.1.39/ch0_1.h264
          roles:
            - detect
        - path: rtsp://192.168.1.39/ch0_0.h264
          roles:
            - record
    objects:
      track:
        - person
```


### Relevant log output

```shell
[2021-10-20 13:29:01] frigate.app                    INFO    : Starting Frigate (0.9.2-25bb515),
*************************************************************,
***    Your config file is not valid!                     ***,
***    Please check the docs at                           ***,
***    https://docs.frigate.video/configuration/index     ***,
***    Config Validation Errors                           ***,
1 validation error for FrigateConfig,
  Camera Kitchen has rtmp enabled, but rtmp is not assigned to an input. (type=value_error),
cameras,
***    End Config Validation Errors                       ***,
[cmd] python3 exited 1,
[cont-finish.d] executing container finish scripts...,
[cont-finish.d] done.,
[s6-finish] waiting for services.,
[s6-finish] sending all processes the KILL signal and exiting.,
[s6-finish] sending all processes the TERM signal.
```


### FFprobe output from your camera

```shell
N/A
```


### Frigate stats

_No response_

### Operating system

Debian

### Install method

Docker Compose

### Coral version

USB

### Network connection

Wired

### Camera make and model

N/A

### Any other information that may be helpful

_No response_


Discussion:
added this to my config but still getting error

```
rtmp:
  enabled: False
```
> 
> 
> added this to my config but still getting error
> 
> ```
> rtmp:
>   enabled: False
> ```

Did you add it in to all of your cameras like this?

```
cameras:
  Kitchen:
    rtmp:
      enabled: False
    ffmpeg:
      inputs:
        - path: rtsp://192.168.1.39/ch0_1.h264
          roles:
            - detect
            - record
```
ah i see, the global settings isnt working and you have to add it to each camera. reading it again makes sense now. thanks

Resolving patch:
diff --git a/frigate/config.py b/frigate/config.py
--- a/frigate/config.py
+++ b/frigate/config.py
@@ -298,13 +298,14 @@ class BirdseyeConfig(FrigateBaseModel):
     "-segment_time",
     "10",
     "-segment_format",
-    "ts",
+    "mp4",
     "-reset_timestamps",
     "1",
     "-strftime",
     "1",
     "-c",
     "copy",
+    "-an",
 ]
 
 
@@ -564,16 +565,9 @@ def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
                 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.ts"]
+                + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
                 + ffmpeg_output_args
             )
 
@@ -800,6 +794,21 @@ def runtime_config(self) -> FrigateConfig:
 
             config.cameras[name] = camera_config
 
+            # check runtime config
+            for name, camera in config.cameras.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 config
 
     @validator("cameras")
@@ -810,23 +819,6 @@ def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig
                 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:
diff --git a/frigate/record.py b/frigate/record.py
--- a/frigate/record.py
+++ b/frigate/record.py
@@ -48,7 +48,9 @@ def move_files(self):
         recordings = [
             d
             for d in os.listdir(CACHE_DIR)
-            if os.path.isfile(os.path.join(CACHE_DIR, d)) and d.endswith(".ts")
+            if os.path.isfile(os.path.join(CACHE_DIR, d))
+            and d.endswith(".mp4")
+            and not d.startswith("clip_")
         ]
 
         files_in_use = []
@@ -111,30 +113,9 @@ def move_files(self):
             file_name = f"{start_time.strftime('%M.%S.mp4')}"
             file_path = os.path.join(directory, file_name)
 
-            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
+            # copy then delete is required when recordings are stored on some network drives
+            shutil.copyfile(cache_path, file_path)
+            os.remove(cache_path)
 
             rand_id = "".join(
                 random.choices(string.ascii_lowercase + string.digits, k=6)
@@ -150,8 +131,11 @@ def move_files(self):
 
     def run(self):
         # Check for new files every 5 seconds
-        while not self.stop_event.wait(5):
+        wait_time = 5
+        while not self.stop_event.wait(wait_time):
+            run_start = datetime.datetime.now().timestamp()
             self.move_files()
+            wait_time = max(0, 5 - (datetime.datetime.now().timestamp() - run_start))
 
         logger.info(f"Exiting recording maintenance...")
 
@@ -290,9 +274,7 @@ def expire_files(self):
 
         # find all the recordings older than the oldest recording in the db
         try:
-            oldest_recording = (
-                Recordings.select().order_by(Recordings.start_time.desc()).get()
-            )
+            oldest_recording = Recordings.select().order_by(Recordings.start_time).get()
 
             p = Path(oldest_recording.path)
             oldest_timestamp = p.stat().st_mtime - 1
@@ -301,7 +283,7 @@ def expire_files(self):
 
         logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
         process = sp.run(
-            ["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"],
+            ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
             capture_output=True,
             text=True,
         )


Test patch:
diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py
--- a/frigate/test/test_config.py
+++ b/frigate/test/test_config.py
@@ -702,7 +702,11 @@ def test_fails_on_invalid_role(self):
                         "inputs": [
                             {
                                 "path": "rtsp://10.0.0.1:554/video",
-                                "roles": ["detect", "clips"],
+                                "roles": ["detect"],
+                            },
+                            {
+                                "path": "rtsp://10.0.0.1:554/video2",
+                                "roles": ["clips"],
                             },
                         ]
                     },
@@ -717,6 +721,37 @@ def test_fails_on_invalid_role(self):
 
         self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
 
+    def test_fails_on_missing_role(self):
+
+        config = {
+            "mqtt": {"host": "mqtt"},
+            "cameras": {
+                "back": {
+                    "ffmpeg": {
+                        "inputs": [
+                            {
+                                "path": "rtsp://10.0.0.1:554/video",
+                                "roles": ["detect"],
+                            },
+                            {
+                                "path": "rtsp://10.0.0.1:554/video2",
+                                "roles": ["record"],
+                            },
+                        ]
+                    },
+                    "detect": {
+                        "height": 1080,
+                        "width": 1920,
+                        "fps": 5,
+                    },
+                    "rtmp": {"enabled": True},
+                }
+            },
+        }
+
+        frigate_config = FrigateConfig(**config)
+        self.assertRaises(ValueError, lambda: frigate_config.runtime_config)
+
     def test_global_detect(self):
 
         config = {
@@ -958,6 +993,34 @@ def test_global_rtmp_merge(self):
         runtime_config = frigate_config.runtime_config
         assert runtime_config.cameras["back"].rtmp.enabled
 
+    def test_global_rtmp_default(self):
+
+        config = {
+            "mqtt": {"host": "mqtt"},
+            "rtmp": {"enabled": False},
+            "cameras": {
+                "back": {
+                    "ffmpeg": {
+                        "inputs": [
+                            {
+                                "path": "rtsp://10.0.0.1:554/video",
+                                "roles": ["detect"],
+                            },
+                            {
+                                "path": "rtsp://10.0.0.1:554/video2",
+                                "roles": ["record"],
+                            },
+                        ]
+                    },
+                }
+            },
+        }
+        frigate_config = FrigateConfig(**config)
+        assert config == frigate_config.dict(exclude_unset=True)
+
+        runtime_config = frigate_config.runtime_config
+        assert not runtime_config.cameras["back"].rtmp.enabled
+
     def test_global_live(self):
 
         config = {