mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 09:15:22 +03:00
gstreamer improvements
This commit is contained in:
parent
ab659ff18c
commit
2c62c7f93b
@ -402,6 +402,10 @@ class CameraGStreamerInput(CameraInput):
|
|||||||
default=[],
|
default=[],
|
||||||
title="Set the camera source format. Default is: ['video/x-raw,format=(string)NV12', 'videoconvert', 'videoscale']",
|
title="Set the camera source format. Default is: ['video/x-raw,format=(string)NV12', 'videoconvert', 'videoscale']",
|
||||||
)
|
)
|
||||||
|
raw_pipeline: List[str] = Field(
|
||||||
|
default=[],
|
||||||
|
title="Override full pipeline. The pipeline should start with the arguments after the `gst-launch-1.0`, `-q`",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CameraInputValidator:
|
class CameraInputValidator:
|
||||||
@ -595,7 +599,11 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for input in self.gstreamer.inputs:
|
for input in self.gstreamer.inputs:
|
||||||
caps = gst_discover(input.path, ["width", "height", "video codec"])
|
caps = (
|
||||||
|
None
|
||||||
|
if len(self.gstreamer.decoder_pipeline) > 0
|
||||||
|
else gst_discover(input.path, ["width", "height", "video codec"])
|
||||||
|
)
|
||||||
gst_cmd = self._get_gstreamer_cmd(self.gstreamer, input, caps)
|
gst_cmd = self._get_gstreamer_cmd(self.gstreamer, input, caps)
|
||||||
if gst_cmd is None:
|
if gst_cmd is None:
|
||||||
continue
|
continue
|
||||||
@ -607,33 +615,37 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
self,
|
self,
|
||||||
base_config: GstreamerConfig,
|
base_config: GstreamerConfig,
|
||||||
gstreamer_input: CameraGStreamerInput,
|
gstreamer_input: CameraGStreamerInput,
|
||||||
caps: Dict,
|
caps: Optional[Dict],
|
||||||
):
|
):
|
||||||
if CameraRoleEnum.rtmp.value in gstreamer_input.roles:
|
if CameraRoleEnum.rtmp.value in gstreamer_input.roles:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{CameraRoleEnum.rtmp.value} role does not supported for the GStreamer integration"
|
f"{CameraRoleEnum.rtmp.value} role does not supported for the GStreamer integration"
|
||||||
)
|
)
|
||||||
|
if len(gstreamer_input.raw_pipeline) > 0:
|
||||||
|
logger.warn("You are using raw pipeline for `%s` camera", self.name)
|
||||||
|
pipeline_args = [
|
||||||
|
f"{item} !".split(" ")
|
||||||
|
for item in gstreamer_input.raw_pipeline
|
||||||
|
if len(item) > 0
|
||||||
|
]
|
||||||
|
pipeline_args = [item for sublist in pipeline_args for item in sublist]
|
||||||
|
return ["gst-launch-1.0", "-q", *pipeline_args][:-1]
|
||||||
|
|
||||||
builder = GstreamerBuilder(
|
builder = GstreamerBuilder(
|
||||||
gstreamer_input.path, self.detect.width, self.detect.height, self.name
|
gstreamer_input.path, self.detect.width, self.detect.height, self.name
|
||||||
)
|
)
|
||||||
if caps is None or len(caps) == 0:
|
|
||||||
logger.warn("gsreamer was not able to detect the input stream format")
|
|
||||||
return builder.build_with_test_source()
|
|
||||||
|
|
||||||
decoder_pipeline = (
|
decoder_pipeline = (
|
||||||
gstreamer_input.decoder_pipeline
|
gstreamer_input.decoder_pipeline
|
||||||
if gstreamer_input.decoder_pipeline is not None
|
if len(gstreamer_input.decoder_pipeline) > 0
|
||||||
else base_config.decoder_pipeline
|
else base_config.decoder_pipeline
|
||||||
)
|
)
|
||||||
decoder_pipeline = [part for part in decoder_pipeline if part != ""]
|
decoder_pipeline = [part for part in decoder_pipeline if part != ""]
|
||||||
builder = builder.with_decoder_pipeline(
|
builder = builder.with_decoder_pipeline(decoder_pipeline, caps)
|
||||||
decoder_pipeline, codec=caps.get("video codec")
|
|
||||||
)
|
|
||||||
|
|
||||||
source_format_pipeline = (
|
source_format_pipeline = (
|
||||||
gstreamer_input.source_format_pipeline
|
gstreamer_input.source_format_pipeline
|
||||||
if gstreamer_input.source_format_pipeline is not None
|
if len(gstreamer_input.source_format_pipeline) > 0
|
||||||
else base_config.source_format_pipeline
|
else base_config.source_format_pipeline
|
||||||
)
|
)
|
||||||
source_format_pipeline = [part for part in source_format_pipeline if part != ""]
|
source_format_pipeline = [part for part in source_format_pipeline if part != ""]
|
||||||
|
|||||||
@ -9,6 +9,8 @@ from frigate.const import (
|
|||||||
RECORD_SEGMENT_TIME_SECONDS,
|
RECORD_SEGMENT_TIME_SECONDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
VIDEO_CODEC_CAP_NAME = "video codec"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -124,12 +126,24 @@ CODECS = {
|
|||||||
|
|
||||||
class GstreamerBuilder:
|
class GstreamerBuilder:
|
||||||
def __init__(self, uri, width, height, name, format="I420"):
|
def __init__(self, uri, width, height, name, format="I420"):
|
||||||
self.uri = uri
|
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.name = name
|
self.name = name
|
||||||
self.video_format = f"video/x-raw,width=(int){width},height=(int){height},format=(string){format}"
|
self.video_format = f"video/x-raw,width=(int){width},height=(int){height},format=(string){format}"
|
||||||
self.input_pipeline = [f'rtspsrc location="{uri}" latency=0']
|
|
||||||
|
is_rtsp = "rtsp://" in uri
|
||||||
|
is_rtmp = "rtmp://" in uri
|
||||||
|
if is_rtsp:
|
||||||
|
self.input_pipeline = [f'rtspsrc location="{uri}" latency=0 do-timestamp=true']
|
||||||
|
elif is_rtmp:
|
||||||
|
self.input_pipeline = [f'rtmpsrc location="{uri}"']
|
||||||
|
else:
|
||||||
|
logger.warn(
|
||||||
|
"An input url does not start with rtsp:// or rtmp:// for camera %s. Assuming full input pipeline supplied.",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
self.input_pipeline = [uri]
|
||||||
|
|
||||||
self.destination_format_pipeline = [self.video_format, "videoconvert"]
|
self.destination_format_pipeline = [self.video_format, "videoconvert"]
|
||||||
self.decoder_pipeline = None
|
self.decoder_pipeline = None
|
||||||
|
|
||||||
@ -140,11 +154,16 @@ class GstreamerBuilder:
|
|||||||
]
|
]
|
||||||
return self._build_launch_command(pipeline)
|
return self._build_launch_command(pipeline)
|
||||||
|
|
||||||
def with_decoder_pipeline(self, decoder_pipeline, codec):
|
def with_decoder_pipeline(self, decoder_pipeline, caps):
|
||||||
if decoder_pipeline is not None and len(decoder_pipeline) > 0:
|
if decoder_pipeline is not None and len(decoder_pipeline) > 0:
|
||||||
self.decoder_pipeline = decoder_pipeline
|
self.decoder_pipeline = decoder_pipeline
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
if caps is None or len(caps) == 0 or VIDEO_CODEC_CAP_NAME not in caps:
|
||||||
|
logger.warn("gsreamer was not able to detect the input stream format")
|
||||||
|
self.decoder_pipeline = None
|
||||||
|
return self
|
||||||
|
codec = caps.get(VIDEO_CODEC_CAP_NAME)
|
||||||
self.decoder_pipeline = autodetect_decoder_pipeline(codec)
|
self.decoder_pipeline = autodetect_decoder_pipeline(codec)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -162,11 +181,16 @@ class GstreamerBuilder:
|
|||||||
logger.warn("gsreamer was not able to auto detect the decoder pipeline.")
|
logger.warn("gsreamer was not able to auto detect the decoder pipeline.")
|
||||||
return self.build_with_test_source()
|
return self.build_with_test_source()
|
||||||
|
|
||||||
|
# remove unnecessary video conversion for the record-only input
|
||||||
|
src_dst_format_pipeline = (
|
||||||
|
["videoconvert", "videoscale"]
|
||||||
|
if use_record and not use_detect
|
||||||
|
else [*self.source_format_pipeline, *self.destination_format_pipeline]
|
||||||
|
)
|
||||||
pipeline = [
|
pipeline = [
|
||||||
*self.input_pipeline,
|
*self.input_pipeline,
|
||||||
*self.decoder_pipeline,
|
*self.decoder_pipeline,
|
||||||
*self.source_format_pipeline,
|
*src_dst_format_pipeline,
|
||||||
*self.destination_format_pipeline,
|
|
||||||
]
|
]
|
||||||
return self._build_launch_command(pipeline, use_detect, use_record)
|
return self._build_launch_command(pipeline, use_detect, use_record)
|
||||||
|
|
||||||
@ -179,8 +203,8 @@ class GstreamerBuilder:
|
|||||||
|
|
||||||
record_mux = (
|
record_mux = (
|
||||||
[
|
[
|
||||||
"queue2",
|
"queue",
|
||||||
"x264enc key-int-max=10",
|
"omxh264enc",
|
||||||
"h264parse",
|
"h264parse",
|
||||||
f"splitmuxsink async-handling=true location={os.path.join(CACHE_DIR, self.name)}{GSTREAMER_RECORD_SUFFIX}-%05d.mp4 max-size-time={RECORD_SEGMENT_TIME_SECONDS*1000000000}",
|
f"splitmuxsink async-handling=true location={os.path.join(CACHE_DIR, self.name)}{GSTREAMER_RECORD_SUFFIX}-%05d.mp4 max-size-time={RECORD_SEGMENT_TIME_SECONDS*1000000000}",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -184,17 +184,17 @@ class TestGstTools(TestCase):
|
|||||||
|
|
||||||
class TestGstreamerBuilder(TestCase):
|
class TestGstreamerBuilder(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.builder = GstreamerBuilder("uri://", 320, 240, "cam_name")
|
self.builder = GstreamerBuilder("rtsp://", 320, 240, "cam_name")
|
||||||
|
|
||||||
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
||||||
def test_manual_decoder_and_cource(self, mock_autodetect_pipeline):
|
def test_manual_decoder_and_cource(self, mock_autodetect_pipeline):
|
||||||
builder = self.builder.with_decoder_pipeline(["a", "b", "c"], codec="H.264")
|
builder = self.builder.with_decoder_pipeline(["a", "b", "c"], caps=None)
|
||||||
builder = builder.with_source_format_pipeline(["d", "e", "f"])
|
builder = builder.with_source_format_pipeline(["d", "e", "f"])
|
||||||
assert builder.build(use_detect=True, use_record=False) == [
|
assert builder.build(use_detect=True, use_record=False) == [
|
||||||
"gst-launch-1.0",
|
"gst-launch-1.0",
|
||||||
"-q",
|
"-q",
|
||||||
"rtspsrc",
|
"rtspsrc",
|
||||||
'location="uri://"',
|
'location="rtsp://"',
|
||||||
"latency=0",
|
"latency=0",
|
||||||
"!",
|
"!",
|
||||||
"a",
|
"a",
|
||||||
@ -220,13 +220,13 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
||||||
def test_autodetect_codecs_success(self, mock_pipeline):
|
def test_autodetect_codecs_success(self, mock_pipeline):
|
||||||
mock_pipeline.return_value = ["rtph264depay", "h264parse", "omxh264dec"]
|
mock_pipeline.return_value = ["rtph264depay", "h264parse", "omxh264dec"]
|
||||||
builder = self.builder.with_decoder_pipeline([], codec="H.264")
|
builder = self.builder.with_decoder_pipeline([], caps={"video codec": "H.264"})
|
||||||
builder = builder.with_source_format_pipeline([])
|
builder = builder.with_source_format_pipeline([])
|
||||||
assert builder.build(use_detect=True, use_record=False) == [
|
assert builder.build(use_detect=True, use_record=False) == [
|
||||||
"gst-launch-1.0",
|
"gst-launch-1.0",
|
||||||
"-q",
|
"-q",
|
||||||
"rtspsrc",
|
"rtspsrc",
|
||||||
'location="uri://"',
|
'location="rtsp://"',
|
||||||
"latency=0",
|
"latency=0",
|
||||||
"!",
|
"!",
|
||||||
"rtph264depay",
|
"rtph264depay",
|
||||||
@ -251,7 +251,7 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
"gst-launch-1.0",
|
"gst-launch-1.0",
|
||||||
"-q",
|
"-q",
|
||||||
"rtspsrc",
|
"rtspsrc",
|
||||||
'location="uri://"',
|
'location="rtsp://"',
|
||||||
"latency=0",
|
"latency=0",
|
||||||
"!",
|
"!",
|
||||||
"rtph264depay",
|
"rtph264depay",
|
||||||
@ -276,10 +276,9 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
"fdsink",
|
"fdsink",
|
||||||
"t.",
|
"t.",
|
||||||
"!",
|
"!",
|
||||||
"queue2",
|
"queue",
|
||||||
"!",
|
"!",
|
||||||
"x264enc",
|
"omxh264enc",
|
||||||
"key-int-max=10",
|
|
||||||
"!",
|
"!",
|
||||||
"h264parse",
|
"h264parse",
|
||||||
"!",
|
"!",
|
||||||
@ -292,7 +291,7 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
"gst-launch-1.0",
|
"gst-launch-1.0",
|
||||||
"-q",
|
"-q",
|
||||||
"rtspsrc",
|
"rtspsrc",
|
||||||
'location="uri://"',
|
'location="rtsp://"',
|
||||||
"latency=0",
|
"latency=0",
|
||||||
"!",
|
"!",
|
||||||
"rtph264depay",
|
"rtph264depay",
|
||||||
@ -301,20 +300,9 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
"!",
|
"!",
|
||||||
"omxh264dec",
|
"omxh264dec",
|
||||||
"!",
|
"!",
|
||||||
"video/x-raw,format=(string)NV12",
|
"queue",
|
||||||
"!",
|
"!",
|
||||||
"videoconvert",
|
"omxh264enc",
|
||||||
"!",
|
|
||||||
"videoscale",
|
|
||||||
"!",
|
|
||||||
"video/x-raw,width=(int)320,height=(int)240,format=(string)I420",
|
|
||||||
"!",
|
|
||||||
"videoconvert",
|
|
||||||
"!",
|
|
||||||
"queue2",
|
|
||||||
"!",
|
|
||||||
"x264enc",
|
|
||||||
"key-int-max=10",
|
|
||||||
"!",
|
"!",
|
||||||
"h264parse",
|
"h264parse",
|
||||||
"!",
|
"!",
|
||||||
@ -327,7 +315,7 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
||||||
def test_autodetect_codecs_failure(self, mock_pipeline):
|
def test_autodetect_codecs_failure(self, mock_pipeline):
|
||||||
mock_pipeline.return_value = None
|
mock_pipeline.return_value = None
|
||||||
builder = self.builder.with_decoder_pipeline([], codec="H.264")
|
builder = self.builder.with_decoder_pipeline([], caps={"video codec": "H.264"})
|
||||||
builder = builder.with_source_format_pipeline([])
|
builder = builder.with_source_format_pipeline([])
|
||||||
assert builder.build(use_detect=True, use_record=False) == [
|
assert builder.build(use_detect=True, use_record=False) == [
|
||||||
"gst-launch-1.0",
|
"gst-launch-1.0",
|
||||||
@ -340,6 +328,53 @@ class TestGstreamerBuilder(TestCase):
|
|||||||
"fdsink",
|
"fdsink",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
||||||
|
def test_rtmp_source(self, mock_autodetect_pipeline):
|
||||||
|
self.builder = GstreamerBuilder("rtmp://", 320, 240, "cam_name")
|
||||||
|
builder = self.builder.with_decoder_pipeline(["a"], caps=None)
|
||||||
|
builder = builder.with_source_format_pipeline(["d"])
|
||||||
|
assert builder.build(use_detect=True, use_record=False) == [
|
||||||
|
"gst-launch-1.0",
|
||||||
|
"-q",
|
||||||
|
"rtmpsrc",
|
||||||
|
'location="rtmp://"',
|
||||||
|
"!",
|
||||||
|
"a",
|
||||||
|
"!",
|
||||||
|
"d",
|
||||||
|
"!",
|
||||||
|
"video/x-raw,width=(int)320,height=(int)240,format=(string)I420",
|
||||||
|
"!",
|
||||||
|
"videoconvert",
|
||||||
|
"!",
|
||||||
|
"fdsink",
|
||||||
|
]
|
||||||
|
mock_autodetect_pipeline.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("frigate.gstreamer.autodetect_decoder_pipeline")
|
||||||
|
def test_custom_source(self, mock_autodetect_pipeline):
|
||||||
|
self.builder = GstreamerBuilder("videotestsrc is-live=true pattern=snow", 320, 240, "cam_name")
|
||||||
|
builder = self.builder.with_decoder_pipeline(["a"], caps=None)
|
||||||
|
builder = builder.with_source_format_pipeline(["d"])
|
||||||
|
assert builder.build(use_detect=True, use_record=False) == [
|
||||||
|
"gst-launch-1.0",
|
||||||
|
"-q",
|
||||||
|
"videotestsrc",
|
||||||
|
"is-live=true",
|
||||||
|
"pattern=snow",
|
||||||
|
"!",
|
||||||
|
"a",
|
||||||
|
"!",
|
||||||
|
"d",
|
||||||
|
"!",
|
||||||
|
"video/x-raw,width=(int)320,height=(int)240,format=(string)I420",
|
||||||
|
"!",
|
||||||
|
"videoconvert",
|
||||||
|
"!",
|
||||||
|
"fdsink",
|
||||||
|
]
|
||||||
|
mock_autodetect_pipeline.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main(verbosity=2)
|
main(verbosity=2)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user