gstreamer work in progress / cofr

This commit is contained in:
YS 2021-12-13 13:17:15 +03:00
parent bd8e23833c
commit 4a497204cb
7 changed files with 221 additions and 75 deletions

View File

@ -7,7 +7,7 @@ import sys
import threading import threading
from logging.handlers import QueueHandler from logging.handlers import QueueHandler
from typing import Dict, List from typing import Dict, List
import traceback
import yaml import yaml
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
@ -320,6 +320,7 @@ class FrigateApp:
print("*** Config Validation Errors ***") print("*** Config Validation Errors ***")
print("*************************************************************") print("*************************************************************")
print(e) print(e)
print(traceback.format_exc())
print("*************************************************************") print("*************************************************************")
print("*** End Config Validation Errors ***") print("*** End Config Validation Errors ***")
print("*************************************************************") print("*************************************************************")

View File

@ -9,7 +9,7 @@ from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import yaml import yaml
from pydantic import BaseModel, Extra, Field, validator from pydantic import BaseModel, Extra, Field, validator, root_validator
from pydantic.fields import PrivateAttr from pydantic.fields import PrivateAttr
from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
@ -22,7 +22,8 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
# German Style: # German Style:
# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S" # DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} FRIGATE_ENV_VARS = {k: v for k,
v in os.environ.items() if k.startswith("FRIGATE_")}
DEFAULT_TRACKED_OBJECTS = ["person"] DEFAULT_TRACKED_OBJECTS = ["person"]
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
@ -39,7 +40,8 @@ class DetectorTypeEnum(str, Enum):
class DetectorConfig(FrigateBaseModel): class DetectorConfig(FrigateBaseModel):
type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type") type: DetectorTypeEnum = Field(
default=DetectorTypeEnum.cpu, title="Detector Type")
device: str = Field(default="usb", title="Device Type") device: str = Field(default="usb", title="Device Type")
num_threads: int = Field(default=3, title="Number of detection threads") num_threads: int = Field(default=3, title="Number of detection threads")
@ -82,8 +84,10 @@ class RetainConfig(FrigateBaseModel):
class EventsConfig(FrigateBaseModel): class EventsConfig(FrigateBaseModel):
max_seconds: int = Field(default=300, title="Maximum event duration.") max_seconds: int = Field(default=300, title="Maximum event duration.")
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.") pre_capture: int = Field(
post_capture: int = Field(default=5, title="Seconds to retain after event ends.") default=5, title="Seconds to retain before event starts.")
post_capture: int = Field(
default=5, title="Seconds to retain after event ends.")
required_zones: List[str] = Field( required_zones: List[str] = Field(
default_factory=list, default_factory=list,
title="List of required zones to be entered in order to save the event.", title="List of required zones to be entered in order to save the event.",
@ -161,8 +165,10 @@ class RuntimeMotionConfig(MotionConfig):
class DetectConfig(FrigateBaseModel): class DetectConfig(FrigateBaseModel):
height: int = Field(default=720, title="Height of the stream for the detect role.") height: int = Field(
width: int = Field(default=1280, title="Width of the stream for the detect role.") default=720, title="Height of the stream for the detect role.")
width: int = Field(
default=1280, title="Width of the stream for the detect role.")
fps: int = Field( fps: int = Field(
default=5, title="Number of frames per second to process through detection." default=5, title="Number of frames per second to process through detection."
) )
@ -204,7 +210,8 @@ class RuntimeFilterConfig(FilterConfig):
config["raw_mask"] = mask config["raw_mask"] = mask
if mask is not None: if mask is not None:
config["mask"] = create_mask(config.get("frame_shape", (1, 1)), mask) config["mask"] = create_mask(
config.get("frame_shape", (1, 1)), mask)
super().__init__(**config) super().__init__(**config)
@ -251,19 +258,22 @@ class ZoneConfig(BaseModel):
if isinstance(coordinates, list): if isinstance(coordinates, list):
self._contour = np.array( self._contour = np.array(
[[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates] [[int(p.split(",")[0]), int(p.split(",")[1])]
for p in coordinates]
) )
elif isinstance(coordinates, str): elif isinstance(coordinates, str):
points = coordinates.split(",") points = coordinates.split(",")
self._contour = np.array( self._contour = np.array(
[[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)] [[int(points[i]), int(points[i + 1])]
for i in range(0, len(points), 2)]
) )
else: else:
self._contour = np.array([]) self._contour = np.array([])
class ObjectConfig(FrigateBaseModel): class ObjectConfig(FrigateBaseModel):
track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.") track: List[str] = Field(
default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.") filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.")
mask: Union[str, List[str]] = Field(default="", title="Object mask.") mask: Union[str, List[str]] = Field(default="", title="Object mask.")
@ -352,6 +362,11 @@ class FfmpegConfig(FrigateBaseModel):
) )
class GstreamerConfig(FrigateBaseModel):
pipeline: List[str] = Field(
default=[], title="GStreamer pipeline. Each pipeline will be splited by ! sign")
class CameraRoleEnum(str, Enum): class CameraRoleEnum(str, Enum):
record = "record" record = "record"
rtmp = "rtmp" rtmp = "rtmp"
@ -361,6 +376,9 @@ class CameraRoleEnum(str, Enum):
class CameraInput(FrigateBaseModel): class CameraInput(FrigateBaseModel):
path: str = Field(title="Camera input path.") path: str = Field(title="Camera input path.")
roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.") roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.")
class CameraFFmpegInput(CameraInput):
global_args: Union[str, List[str]] = Field( global_args: Union[str, List[str]] = Field(
default_factory=list, title="FFmpeg global arguments." default_factory=list, title="FFmpeg global arguments."
) )
@ -372,9 +390,12 @@ class CameraInput(FrigateBaseModel):
) )
class CameraFfmpegConfig(FfmpegConfig): class CameraGStreamerInput(CameraInput):
inputs: List[CameraInput] = Field(title="Camera inputs.") pipeline: List[str] = Field(
default=[], title="GStreamer pipeline. Each pipeline will be splited by ! sign")
class CameraInputValidator:
@validator("inputs") @validator("inputs")
def validate_roles(cls, v): def validate_roles(cls, v):
roles = [role for i in v for role in i.roles] roles = [role for i in v for role in i.roles]
@ -389,6 +410,15 @@ class CameraFfmpegConfig(FfmpegConfig):
return v return v
class CameraFfmpegConfig(FfmpegConfig, CameraInputValidator):
inputs: List[CameraFFmpegInput] = Field(title="Camera FFMpeg inputs.")
class CameraGStreamerConfig(GstreamerConfig, CameraInputValidator):
inputs: List[CameraGStreamerInput] = Field(
title="Camera GStreamer inputs.")
class SnapshotsConfig(FrigateBaseModel): class SnapshotsConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Snapshots enabled.") enabled: bool = Field(default=False, title="Snapshots enabled.")
clean_copy: bool = Field( clean_copy: bool = Field(
@ -400,7 +430,8 @@ class SnapshotsConfig(FrigateBaseModel):
bounding_box: bool = Field( bounding_box: bool = Field(
default=True, title="Add a bounding box overlay on the snapshot." default=True, title="Add a bounding box overlay on the snapshot."
) )
crop: bool = Field(default=False, title="Crop the snapshot to the detected object.") crop: bool = Field(
default=False, title="Crop the snapshot to the detected object.")
required_zones: List[str] = Field( required_zones: List[str] = Field(
default_factory=list, default_factory=list,
title="List of required zones to be entered in order to save a snapshot.", title="List of required zones to be entered in order to save a snapshot.",
@ -440,7 +471,8 @@ class TimestampStyleConfig(FrigateBaseModel):
default=TimestampPositionEnum.tl, title="Timestamp position." default=TimestampPositionEnum.tl, title="Timestamp position."
) )
format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.") format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.") color: ColorConfig = Field(
default_factory=ColorConfig, title="Timestamp color.")
thickness: int = Field(default=2, title="Timestamp thickness.") thickness: int = Field(default=2, title="Timestamp thickness.")
effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.") effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.")
@ -448,8 +480,10 @@ class TimestampStyleConfig(FrigateBaseModel):
class CameraMqttConfig(FrigateBaseModel): class CameraMqttConfig(FrigateBaseModel):
enabled: bool = Field(default=True, title="Send image over MQTT.") enabled: bool = Field(default=True, title="Send image over MQTT.")
timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.") timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.")
bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.") bounding_box: bool = Field(
crop: bool = Field(default=True, title="Crop MQTT image to detected object.") default=True, title="Add bounding box to MQTT image.")
crop: bool = Field(
default=True, title="Crop MQTT image to detected object.")
height: int = Field(default=270, title="MQTT image height.") height: int = Field(default=270, title="MQTT image height.")
required_zones: List[str] = Field( required_zones: List[str] = Field(
default_factory=list, default_factory=list,
@ -469,12 +503,16 @@ class RtmpConfig(FrigateBaseModel):
class CameraLiveConfig(FrigateBaseModel): class CameraLiveConfig(FrigateBaseModel):
height: int = Field(default=720, title="Live camera view height") height: int = Field(default=720, title="Live camera view height")
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality") quality: int = Field(default=8, ge=1, le=31,
title="Live camera view quality")
class CameraConfig(FrigateBaseModel): class CameraConfig(FrigateBaseModel):
name: Optional[str] = Field(title="Camera name.") name: Optional[str] = Field(title="Camera name.")
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") ffmpeg: Optional[CameraFfmpegConfig] = Field(
title="FFmpeg configuration for the camera.")
gstreamer: Optional[CameraGStreamerConfig] = Field(
title="GStreamer configuration for the camera.")
best_image_timeout: int = Field( best_image_timeout: int = Field(
default=60, default=60,
title="How long to wait for the image with the highest confidence score.", title="How long to wait for the image with the highest confidence score.",
@ -500,7 +538,8 @@ class CameraConfig(FrigateBaseModel):
objects: ObjectConfig = Field( objects: ObjectConfig = Field(
default_factory=ObjectConfig, title="Object configuration." default_factory=ObjectConfig, title="Object configuration."
) )
motion: Optional[MotionConfig] = Field(title="Motion detection configuration.") motion: Optional[MotionConfig] = Field(
title="Motion detection configuration.")
detect: DetectConfig = Field( detect: DetectConfig = Field(
default_factory=DetectConfig, title="Object detection configuration." default_factory=DetectConfig, title="Object detection configuration."
) )
@ -514,13 +553,16 @@ class CameraConfig(FrigateBaseModel):
if "zones" in config: if "zones" in config:
colors = plt.cm.get_cmap("tab10", len(config["zones"])) colors = plt.cm.get_cmap("tab10", len(config["zones"]))
config["zones"] = { config["zones"] = {
name: {**z, "color": tuple(round(255 * c) for c in colors(idx)[:3])} name: {**z, "color": tuple(round(255 * c)
for c in colors(idx)[:3])}
for idx, (name, z) in enumerate(config["zones"].items()) for idx, (name, z) in enumerate(config["zones"].items())
} }
# add roles to the input if there is only one # add roles to the input if there is only one
if "ffmpeg" in config:
if len(config["ffmpeg"]["inputs"]) == 1: if len(config["ffmpeg"]["inputs"]) == 1:
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"] config["ffmpeg"]["inputs"][0]["roles"] = [
"record", "rtmp", "detect"]
super().__init__(**config) super().__init__(**config)
@ -533,8 +575,21 @@ class CameraConfig(FrigateBaseModel):
return self.detect.height * 3 // 2, self.detect.width return self.detect.height * 3 // 2, self.detect.width
@property @property
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]: def decoder_cmds(self) -> List[Dict[str, List[str]]]:
decoder_cmds = []
if self.ffmpeg:
return self._ffmpeg_cmds return self._ffmpeg_cmds
else:
assert self.gstreamer
for gstreamer_input in self.gstreamer.inputs:
decoder_cmd = self._get_gstreamer_cmd(gstreamer_input)
if decoder_cmd is None:
continue
decoder_cmds.append(
{"roles": gstreamer_input.roles, "cmd": decoder_cmd})
return decoder_cmds
def create_ffmpeg_cmds(self): def create_ffmpeg_cmds(self):
if "_ffmpeg_cmds" in self: if "_ffmpeg_cmds" in self:
@ -548,7 +603,54 @@ class CameraConfig(FrigateBaseModel):
ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd}) ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
self._ffmpeg_cmds = ffmpeg_cmds self._ffmpeg_cmds = ffmpeg_cmds
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
def _get_gstreamer_cmd(self, gstreamer_input: CameraGStreamerInput):
assert list(
["detect"]) == gstreamer_input.roles, "only detect role is supported"
pipeline = [part for part in self.gstreamer.pipeline if part != ""]
video_format = f"video/x-raw,width=(int){self.detect.width},height=(int){self.detect.height},format=(string)I420"
if len(pipeline) == 0:
pipeline = [
#"videotestsrc pattern=0",
"rtspsrc location=\"rtsp://admin:123456@192.168.5.95:554/stream0\"",
"rtph265depay", "h265parse","omxh265dec",
"video/x-raw,format=(string)NV12",
"videoconvert","videoscale",
video_format,
"videoconvert"
# "videoscale",
# video_format,
# "videoconvert"
]
# pipeline = [
# #"videotestsrc pattern=0",
# "rtspsrc location=\"rtsp://admin:123456@192.168.5.180:554/cam/realmonitor?channel=0&subtype=0\"",
# "rtph264depay",
# "h264parse",
# "omxh264dec",
# "video/x-raw,format=(string)NV12",
# "videoconvert",
# "videoscale",
# video_format,
# "videoconvert"
# # "videoscale",
# # video_format,
# # "videoconvert"
# ]
pipeline_args = (
[f"{item} !".split(" ") for item in pipeline]
)
pipeline_args = [item for sublist in pipeline_args for item in sublist]
return [
"gst-launch-1.0",
"-q",
*pipeline_args,
"fdsink"
]
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraFFmpegInput):
ffmpeg_output_args = [] ffmpeg_output_args = []
if "detect" in ffmpeg_input.roles: if "detect" in ffmpeg_input.roles:
detect_args = ( detect_args = (
@ -574,7 +676,8 @@ class CameraConfig(FrigateBaseModel):
else self.ffmpeg.output_args.rtmp.split(" ") else self.ffmpeg.output_args.rtmp.split(" ")
) )
ffmpeg_output_args = ( ffmpeg_output_args = (
rtmp_args + [f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args rtmp_args +
[f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args
) )
if "record" in ffmpeg_input.roles and self.record.enabled: if "record" in ffmpeg_input.roles and self.record.enabled:
record_args = ( record_args = (
@ -598,13 +701,16 @@ class CameraConfig(FrigateBaseModel):
input_args = ffmpeg_input.input_args or self.ffmpeg.input_args input_args = ffmpeg_input.input_args or self.ffmpeg.input_args
global_args = ( global_args = (
global_args if isinstance(global_args, list) else global_args.split(" ") global_args if isinstance(
global_args, list) else global_args.split(" ")
) )
hwaccel_args = ( hwaccel_args = (
hwaccel_args if isinstance(hwaccel_args, list) else hwaccel_args.split(" ") hwaccel_args if isinstance(
hwaccel_args, list) else hwaccel_args.split(" ")
) )
input_args = ( input_args = (
input_args if isinstance(input_args, list) else input_args.split(" ") input_args if isinstance(
input_args, list) else input_args.split(" ")
) )
cmd = ( cmd = (
@ -618,6 +724,12 @@ class CameraConfig(FrigateBaseModel):
return [part for part in cmd if part != ""] return [part for part in cmd if part != ""]
@root_validator
def either_ffmpeg_or_gstreamer(cls, v):
if ("ffmpeg" not in v) and ("gstreamer" not in v):
raise ValueError('either ffmpeg or gstreamer should be set')
return v
class DatabaseConfig(FrigateBaseModel): class DatabaseConfig(FrigateBaseModel):
path: str = Field( path: str = Field(
@ -627,9 +739,12 @@ class DatabaseConfig(FrigateBaseModel):
class ModelConfig(FrigateBaseModel): class ModelConfig(FrigateBaseModel):
path: Optional[str] = Field(title="Custom Object detection model path.") path: Optional[str] = Field(title="Custom Object detection model path.")
labelmap_path: Optional[str] = Field(title="Label map for custom object detector.") labelmap_path: Optional[str] = Field(
width: int = Field(default=320, title="Object detection model input width.") title="Label map for custom object detector.")
height: int = Field(default=320, title="Object detection model input height.") width: int = Field(
default=320, title="Object detection model input width.")
height: int = Field(
default=320, title="Object detection model input height.")
labelmap: Dict[int, str] = Field( labelmap: Dict[int, str] = Field(
default_factory=dict, title="Labelmap customization." default_factory=dict, title="Labelmap customization."
) )
@ -656,7 +771,8 @@ class ModelConfig(FrigateBaseModel):
self._colormap = {} self._colormap = {}
for key, val in self._merged_labelmap.items(): for key, val in self._merged_labelmap.items():
self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3]) self._colormap[val] = tuple(int(round(255 * c))
for c in cmap(key)[:3])
class LogLevelEnum(str, Enum): class LogLevelEnum(str, Enum):
@ -688,7 +804,8 @@ class FrigateConfig(FrigateBaseModel):
default_factory=ModelConfig, title="Detection model configuration." default_factory=ModelConfig, title="Detection model configuration."
) )
detectors: Dict[str, DetectorConfig] = Field( detectors: Dict[str, DetectorConfig] = Field(
default={name: DetectorConfig(**d) for name, d in DEFAULT_DETECTORS.items()}, default={name: DetectorConfig(**d)
for name, d in DEFAULT_DETECTORS.items()},
title="Detector hardware configuration.", title="Detector hardware configuration.",
) )
logger: LoggerConfig = Field( logger: LoggerConfig = Field(
@ -734,7 +851,8 @@ class FrigateConfig(FrigateBaseModel):
# MQTT password substitution # MQTT password substitution
if config.mqtt.password: if config.mqtt.password:
config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS) config.mqtt.password = config.mqtt.password.format(
**FRIGATE_ENV_VARS)
# Global config to propegate down to camera level # Global config to propegate down to camera level
global_config = config.dict( global_config = config.dict(
@ -753,7 +871,8 @@ class FrigateConfig(FrigateBaseModel):
) )
for name, camera in config.cameras.items(): for name, camera in config.cameras.items():
merged_config = deep_merge(camera.dict(exclude_unset=True), global_config) merged_config = deep_merge(camera.dict(
exclude_unset=True), global_config)
camera_config: CameraConfig = CameraConfig.parse_obj( camera_config: CameraConfig = CameraConfig.parse_obj(
{"name": name, **merged_config} {"name": name, **merged_config}
) )
@ -769,6 +888,7 @@ class FrigateConfig(FrigateBaseModel):
camera_config.detect.stationary_interval = stationary_interval camera_config.detect.stationary_interval = stationary_interval
# FFMPEG input substitution # FFMPEG input substitution
if "ffmpeg" in camera_config:
for input in camera_config.ffmpeg.inputs: for input in camera_config.ffmpeg.inputs:
input.path = input.path.format(**FRIGATE_ENV_VARS) input.path = input.path.format(**FRIGATE_ENV_VARS)
@ -816,8 +936,9 @@ class FrigateConfig(FrigateBaseModel):
) )
# check runtime config # check runtime config
decoder_config = camera_config.ffmpeg if "ffmpeg" in camera_config else camera_config.gstreamer
assigned_roles = list( assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles]) set([r for i in decoder_config.inputs for r in i.roles])
) )
if camera_config.record.enabled and not "record" in assigned_roles: if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError( raise ValueError(

View File

@ -319,11 +319,11 @@ def events():
def config(): def config():
config = current_app.frigate_config.dict() config = current_app.frigate_config.dict()
# add in the ffmpeg_cmds # add in the decoder_cmds
for camera_name, camera in current_app.frigate_config.cameras.items(): for camera_name, camera in current_app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name] camera_dict = config["cameras"][camera_name]
camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds) camera_dict["decoder_cmds"] = copy.deepcopy(camera.decoder_cmds)
for cmd in camera_dict["ffmpeg_cmds"]: for cmd in camera_dict["decoder_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"]) cmd["cmd"] = " ".join(cmd["cmd"])
return jsonify(config) return jsonify(config)

View File

@ -9,6 +9,7 @@ import subprocess as sp
import threading import threading
from multiprocessing import shared_memory from multiprocessing import shared_memory
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
from frigate.log import LogPipe
import cv2 import cv2
import numpy as np import numpy as np
@ -32,21 +33,35 @@ class FFMpegConverter:
ffmpeg_cmd = f"ffmpeg -f rawvideo -pix_fmt yuv420p -video_size {in_width}x{in_height} -i pipe: -f mpegts -s {out_width}x{out_height} -codec:v mpeg1video -q {quality} -bf 0 pipe:".split( ffmpeg_cmd = f"ffmpeg -f rawvideo -pix_fmt yuv420p -video_size {in_width}x{in_height} -i pipe: -f mpegts -s {out_width}x{out_height} -codec:v mpeg1video -q {quality} -bf 0 pipe:".split(
" " " "
) )
# ffmpeg_cmd = f"gst-launch-1.0 fdsrc ! video/x-raw, width={in_width}, height={in_height}, format=I420 ! nvvideoconvert ! omxh264enc ! h264parse ! mpegtsmux ! fdsink".split(
# " "
# )
logger.error(f" ffmpeg_cmd >>>> {ffmpeg_cmd}")
self.logpipe = LogPipe(
"ffmpeg.output", logging.ERROR)
self.process = sp.Popen( self.process = sp.Popen(
ffmpeg_cmd, ffmpeg_cmd,
stdout=sp.PIPE, stdout=sp.PIPE,
stderr=sp.DEVNULL, stderr=self.logpipe,
stdin=sp.PIPE, stdin=sp.PIPE,
start_new_session=True, start_new_session=True,
) )
def write(self, b): def write(self, b):
try:
self.process.stdin.write(b) self.process.stdin.write(b)
except Exception:
self.logpipe.dump()
return False
def read(self, length): def read(self, length):
try: try:
return self.process.stdout.read1(length) return self.process.stdout.read1(length)
except ValueError: except ValueError:
self.logpipe.dump()
return False return False
def exit(self): def exit(self):
@ -408,8 +423,11 @@ def output_frames(config: FrigateConfig, video_output_queue):
if any( if any(
ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager
): ):
try:
# write to the converter for the camera if clients are listening to the specific camera # write to the converter for the camera if clients are listening to the specific camera
converters[camera].write(frame.tobytes()) converters[camera].write(frame.tobytes())
except Exception:
pass
# update birdseye if websockets are connected # update birdseye if websockets are connected
if config.birdseye.enabled and any( if config.birdseye.enabled and any(

View File

@ -247,7 +247,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "-rtsp_transport" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "-rtsp_transport" in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
def test_ffmpeg_params_global(self): def test_ffmpeg_params_global(self):
config = { config = {
@ -276,7 +276,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "-re" in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
def test_ffmpeg_params_camera(self): def test_ffmpeg_params_camera(self):
config = { config = {
@ -306,8 +306,8 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "-re" in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
assert "test" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "test" not in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
def test_ffmpeg_params_input(self): def test_ffmpeg_params_input(self):
config = { config = {
@ -341,10 +341,10 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "-re" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "-re" in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
assert "test" in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "test" in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
assert "test2" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "test2" not in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
assert "test3" not in runtime_config.cameras["back"].ffmpeg_cmds[0]["cmd"] assert "test3" not in runtime_config.cameras["back"].decoder_cmds[0]["cmd"]
def test_inherit_clips_retention(self): def test_inherit_clips_retention(self):
config = { config = {
@ -512,9 +512,9 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
ffmpeg_cmds = runtime_config.cameras["back"].ffmpeg_cmds decoder_cmds = runtime_config.cameras["back"].decoder_cmds
assert len(ffmpeg_cmds) == 1 assert len(decoder_cmds) == 1
assert not "clips" in ffmpeg_cmds[0]["roles"] assert not "clips" in decoder_cmds[0]["roles"]
def test_max_disappeared_default(self): def test_max_disappeared_default(self):
config = { config = {

View File

@ -101,14 +101,14 @@ def stop_ffmpeg(ffmpeg_process, logger):
def start_or_restart_ffmpeg( def start_or_restart_ffmpeg(
ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None decoder_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None
): ):
if ffmpeg_process is not None: if ffmpeg_process is not None:
stop_ffmpeg(ffmpeg_process, logger) stop_ffmpeg(ffmpeg_process, logger)
if frame_size is None: if frame_size is None:
process = sp.Popen( process = sp.Popen(
ffmpeg_cmd, decoder_cmd,
stdout=sp.DEVNULL, stdout=sp.DEVNULL,
stderr=logpipe, stderr=logpipe,
stdin=sp.DEVNULL, stdin=sp.DEVNULL,
@ -116,7 +116,7 @@ def start_or_restart_ffmpeg(
) )
else: else:
process = sp.Popen( process = sp.Popen(
ffmpeg_cmd, decoder_cmd,
stdout=sp.PIPE, stdout=sp.PIPE,
stderr=logpipe, stderr=logpipe,
stdin=sp.DEVNULL, stdin=sp.DEVNULL,
@ -187,7 +187,8 @@ class CameraWatchdog(threading.Thread):
self.config = config self.config = config
self.capture_thread = None self.capture_thread = None
self.ffmpeg_detect_process = None self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR) self.logpipe = LogPipe(
f"ffmpeg.{self.camera_name}.detect", logging.ERROR)
self.ffmpeg_other_processes = [] self.ffmpeg_other_processes = []
self.camera_fps = camera_fps self.camera_fps = camera_fps
self.ffmpeg_pid = ffmpeg_pid self.ffmpeg_pid = ffmpeg_pid
@ -199,7 +200,7 @@ class CameraWatchdog(threading.Thread):
def run(self): def run(self):
self.start_ffmpeg_detect() self.start_ffmpeg_detect()
for c in self.config.ffmpeg_cmds: for c in self.config.decoder_cmds:
if "detect" in c["roles"]: if "detect" in c["roles"]:
continue continue
logpipe = LogPipe( logpipe = LogPipe(
@ -225,7 +226,8 @@ class CameraWatchdog(threading.Thread):
self.logger.error( self.logger.error(
"The following ffmpeg logs include the last 100 lines prior to exit." "The following ffmpeg logs include the last 100 lines prior to exit."
) )
self.logger.error("You may have invalid args defined for this camera.") self.logger.error(
"You may have invalid args defined for this camera.")
self.logpipe.dump() self.logpipe.dump()
self.start_ffmpeg_detect() self.start_ffmpeg_detect()
elif now - self.capture_thread.current_frame.value > 20: elif now - self.capture_thread.current_frame.value > 20:
@ -234,7 +236,8 @@ class CameraWatchdog(threading.Thread):
) )
self.ffmpeg_detect_process.terminate() self.ffmpeg_detect_process.terminate()
try: try:
self.logger.info("Waiting for ffmpeg to exit gracefully...") self.logger.info(
"Waiting for ffmpeg to exit gracefully...")
self.ffmpeg_detect_process.communicate(timeout=30) self.ffmpeg_detect_process.communicate(timeout=30)
except sp.TimeoutExpired: except sp.TimeoutExpired:
self.logger.info("FFmpeg didnt exit. Force killing...") self.logger.info("FFmpeg didnt exit. Force killing...")
@ -257,11 +260,11 @@ class CameraWatchdog(threading.Thread):
self.logpipe.close() self.logpipe.close()
def start_ffmpeg_detect(self): def start_ffmpeg_detect(self):
ffmpeg_cmd = [ decoder_cmd = [
c["cmd"] for c in self.config.ffmpeg_cmds if "detect" in c["roles"] c["cmd"] for c in self.config.decoder_cmds if "detect" in c["roles"]
][0] ][0]
self.ffmpeg_detect_process = start_or_restart_ffmpeg( self.ffmpeg_detect_process = start_or_restart_ffmpeg(
ffmpeg_cmd, self.logger, self.logpipe, self.frame_size decoder_cmd, self.logger, self.logpipe, self.frame_size
) )
self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid
self.capture_thread = CameraCapture( self.capture_thread = CameraCapture(
@ -482,11 +485,13 @@ def process_frames(
current_frame_time.value = frame_time current_frame_time.value = frame_time
frame = frame_manager.get( frame = frame_manager.get(
f"{camera_name}{frame_time}", (frame_shape[0] * 3 // 2, frame_shape[1]) f"{camera_name}{frame_time}", (
frame_shape[0] * 3 // 2, frame_shape[1])
) )
if frame is None: if frame is None:
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.") logger.info(
f"{camera_name}: frame {frame_time} is not in memory store.")
continue continue
if not detection_enabled.value: if not detection_enabled.value:
@ -592,7 +597,8 @@ def process_frames(
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
for index in idxs: for index in idxs:
obj = group[index[0]] index = index if isinstance(index, np.int32) else index[0]
obj = group[index]
if clipped(obj, frame_shape): if clipped(obj, frame_shape):
box = obj[2] box = obj[2]
# calculate a new region that will hopefully get the entire object # calculate a new region that will hopefully get the entire object

View File

@ -67,8 +67,8 @@ class ProcessClip:
self.config = config self.config = config
self.camera_config = self.config.cameras["camera"] self.camera_config = self.config.cameras["camera"]
self.frame_shape = self.camera_config.frame_shape self.frame_shape = self.camera_config.frame_shape
self.ffmpeg_cmd = [ self.decoder_cmd = [
c["cmd"] for c in self.camera_config.ffmpeg_cmds if "detect" in c["roles"] c["cmd"] for c in self.camera_config.decoder_cmds if "detect" in c["roles"]
][0] ][0]
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
self.frame_queue = mp.Queue() self.frame_queue = mp.Queue()
@ -84,7 +84,7 @@ class ProcessClip:
* self.camera_config.frame_shape_yuv[1] * self.camera_config.frame_shape_yuv[1]
) )
ffmpeg_process = start_or_restart_ffmpeg( ffmpeg_process = start_or_restart_ffmpeg(
self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size self.decoder_cmd, logger, sp.DEVNULL, frame_size
) )
capture_frames( capture_frames(
ffmpeg_process, ffmpeg_process,