From 87144cd5729de900bcc3ef26ef429e46598fa485 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 28 Nov 2022 20:48:11 -0700 Subject: [PATCH] FEAT: Support for ffmpeg presets (#3840) * Add hwaccel presets * Use hwaccel presets * Add input arg presets * Use input arg presets * Make util to clean up redundant code * Add support for output arg presets * Add tests * Update camera specific to use presets * Update hwaccel to use presets * Format files and fix tests * Rewrite tests to test record correctly * Move presets from string to list to avoid manually separating into a list * Add mjpeg cuvid decoder preset * Fix tests * Fix comment --- docs/docs/configuration/camera_specific.md | 56 +--- .../configuration/hardware_acceleration.md | 14 +- frigate/config.py | 49 ++-- frigate/ffmpeg_presets.py | 277 ++++++++++++++++++ frigate/test/test_ffmpeg_presets.py | 152 ++++++++++ frigate/util.py | 7 +- 6 files changed, 480 insertions(+), 75 deletions(-) create mode 100644 frigate/ffmpeg_presets.py create mode 100644 frigate/test/test_ffmpeg_presets.py diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index c1b57658c..6b110a991 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -8,15 +8,15 @@ title: Camera Specific Configurations The input and output parameters need to be adjusted for MJPEG cameras ```yaml -input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -use_wallclock_as_timestamps 1 -c:v mjpeg +input_args: preset-http-mjpeg-generic ``` Note that mjpeg cameras require encoding the video into h264 for recording, and rtmp roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. ```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: preset-record-mjpeg + rtmp: preset-rtmp-mjpeg ``` ## JPEG Stream Cameras @@ -24,25 +24,7 @@ output_args: Cameras using a live changing jpeg image will need input parameters as below ```yaml -input_args: -- -r -- 5 # << enter FPS here -- -stream_loop -- -1 -- -f -- image2 -- -avoid_negative_ts -- make_zero -- -fflags -- nobuffer -- -flags -- low_delay -- -strict -- experimental -- -fflags -- +genpts+discardcorrupt -- -use_wallclock_as_timestamps -- 1 +input_args: preset-http-jpeg-generic ``` Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras) @@ -53,7 +35,7 @@ The input parameters need to be adjusted for RTMP cameras ```yaml ffmpeg: - input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv + input_args: preset-rtmp-generic ``` ## UDP Only Cameras @@ -62,7 +44,7 @@ If your cameras do not support TCP connections for RTSP, you can use UDP. ```yaml ffmpeg: - input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1 + input_args: preset-rtsp-udp ``` ## Model/vendor specific setup @@ -77,7 +59,7 @@ cameras: output_args: record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -tag:v hvc1 -bsf:v hevc_mp4toannexb -c:a aac rtmp: -c:v copy -c:a aac -f flv - + inputs: - path: rtsp://user:password@camera-ip:554/H264/ch1/main/av_stream # <----- Update for your camera roles: @@ -99,7 +81,7 @@ You will need to remove `nobuffer` flag for Blue Iris RTSP cameras ```yaml ffmpeg: - input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1 + input_args: preset-rtsp-blue-iris ``` ### Reolink 410/520 (possibly others) @@ -112,21 +94,7 @@ According to [this discussion](https://github.com/blakeblackshear/frigate/issues cameras: reolink: ffmpeg: - input_args: - - -avoid_negative_ts - - make_zero - - -fflags - - +genpts+discardcorrupt - - -flags - - low_delay - - -strict - - experimental - - -analyzeduration - - 1000M - - -probesize - - 1000M - - -rw_timeout - - "5000000" + input_args: preset-http-reolink inputs: - path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password roles: @@ -148,6 +116,6 @@ In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate ```yaml ffmpeg: output_args: - record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -ar 44100 -c:a aac - rtmp: -c:v copy -f flv -ar 44100 -c:a aac -``` \ No newline at end of file + record: preset-record-ubiquiti + rtmp: preset-rtmp-ubiquiti +``` diff --git a/docs/docs/configuration/hardware_acceleration.md b/docs/docs/configuration/hardware_acceleration.md index 67df7ca60..7a6780a72 100644 --- a/docs/docs/configuration/hardware_acceleration.md +++ b/docs/docs/configuration/hardware_acceleration.md @@ -12,22 +12,22 @@ Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config ```yaml ffmpeg: - hwaccel_args: -c:v h264_v4l2m2m + hwaccel_args: preset-rpi-64-h264 ``` ### Intel-based CPUs (<10th Generation) via Quicksync ```yaml ffmpeg: - hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p + hwaccel_args: preset-intel-vaapi ``` -**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the frigate.yml for HA OS users](advanced.md#environment_vars). +**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the frigate.yml for HA OS users](advanced.md#environment_vars). ### Intel-based CPUs (>=10th Generation) via Quicksync ```yaml ffmpeg: - hwaccel_args: -c:v h264_qsv + hwaccel_args: preset-intel-qsv-h264 ``` ### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver @@ -36,7 +36,7 @@ ffmpeg: ```yaml ffmpeg: - hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p + hwaccel_args: preset-amd-vaapi ``` ### NVIDIA GPU @@ -79,11 +79,11 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9) ``` -For example, for H264 video, you'll select `h264_cuvid`. +For example, for H264 video, you'll select `preset-nvidia-h264`. ```yaml ffmpeg: - hwaccel_args: -c:v h264_cuvid + hwaccel_args: preset-nvidia-h264 ``` If everything is working correctly, you should see a significant improvement in performance. diff --git a/frigate/config.py b/frigate/config.py index a2f7f3e63..ca6ead667 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -21,10 +21,17 @@ from frigate.const import ( from frigate.util import ( create_mask, deep_merge, + get_ffmpeg_arg_list, escape_special_characters, load_config_with_no_duplicates, load_labels, ) +from frigate.ffmpeg_presets import ( + parse_preset_hardware_acceleration, + parse_preset_input, + parse_preset_output_record, + parse_preset_output_rtmp, +) logger = logging.getLogger(__name__) @@ -646,11 +653,8 @@ class CameraConfig(FrigateBaseModel): def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): ffmpeg_output_args = [] if "detect" in ffmpeg_input.roles: - detect_args = ( - self.ffmpeg.output_args.detect - if isinstance(self.ffmpeg.output_args.detect, list) - else self.ffmpeg.output_args.detect.split(" ") - ) + detect_args = get_ffmpeg_arg_list(self.ffmpeg.output_args.detect) + ffmpeg_output_args = ( [ "-r", @@ -663,19 +667,18 @@ class CameraConfig(FrigateBaseModel): + ["pipe:"] ) if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled: - rtmp_args = ( - self.ffmpeg.output_args.rtmp - if isinstance(self.ffmpeg.output_args.rtmp, list) - else self.ffmpeg.output_args.rtmp.split(" ") + rtmp_args = get_ffmpeg_arg_list( + parse_preset_output_rtmp(self.ffmpeg.output_args.rtmp) + or self.ffmpeg.output_args.rtmp ) + 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: - record_args = ( - self.ffmpeg.output_args.record - if isinstance(self.ffmpeg.output_args.record, list) - else self.ffmpeg.output_args.record.split(" ") + record_args = get_ffmpeg_arg_list( + parse_preset_output_record(self.ffmpeg.output_args.record) + or self.ffmpeg.output_args.record ) ffmpeg_output_args = ( @@ -688,18 +691,18 @@ class CameraConfig(FrigateBaseModel): if len(ffmpeg_output_args) == 0: return None - global_args = ffmpeg_input.global_args or self.ffmpeg.global_args - hwaccel_args = ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args - input_args = ffmpeg_input.input_args or self.ffmpeg.input_args - - global_args = ( - global_args if isinstance(global_args, list) else global_args.split(" ") + global_args = get_ffmpeg_arg_list( + ffmpeg_input.global_args or self.ffmpeg.global_args ) - hwaccel_args = ( - hwaccel_args if isinstance(hwaccel_args, list) else hwaccel_args.split(" ") + hwaccel_args = get_ffmpeg_arg_list( + ffmpeg_input.hwaccel_args + or parse_preset_hardware_acceleration(self.ffmpeg.hwaccel_args) + or self.ffmpeg.hwaccel_args ) - input_args = ( - input_args if isinstance(input_args, list) else input_args.split(" ") + input_args = get_ffmpeg_arg_list( + ffmpeg_input.input_args + or parse_preset_input(self.ffmpeg.input_args, self.detect.fps) + or self.ffmpeg.input_args ) cmd = ( diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py new file mode 100644 index 000000000..3e7f0ef54 --- /dev/null +++ b/frigate/ffmpeg_presets.py @@ -0,0 +1,277 @@ +"""Handles inserting and maintaining ffmpeg presets.""" + +from typing import Any + + +PRESETS_HW_ACCEL = { + "preset-rpi-32-h264": ["-c:v", "h264_v4l2m2m"], + "preset-rpi-64-h264": ["-c:v", "h264_v4l2m2m"], + "preset-intel-vaapi": [ + "-hwaccel", + "vaapi", + "-hwaccel_device", + "/dev/dri/renderD128", + "-hwaccel_output_format", + "yuv420p", + ], + "preset-intel-qsv-h264": ["-c:v", "h264_qsv"], + "preset-intel-qsv-h265": ["-c:v", "hevc_qsv"], + "preset-amd-vaapi": [ + "-hwaccel", + "vaapi", + "-hwaccel_device", + "/dev/dri/renderD128", + "-hwaccel_output_format", + "yuv420p", + ], + "preset-nvidia-h264": ["-c:v", "h264_cuvid"], + "preset-nvidia-h265": ["-c:v", "hevc_cuvid"], + "preset-nvidia-mjpeg": ["-c:v", "mjpeg_cuvid"], +} + + +def parse_preset_hardware_acceleration(arg: Any) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + return PRESETS_HW_ACCEL.get(arg, None) + + +PRESETS_INPUT = { + "preset-http-jpeg-generic": [ + "-r", + "{}", + "-stream_loop", + "-1", + "-f", + "image2", + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-mjpeg-generic": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-reolink": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-flags", + "low_delay", + "-strict", + "experimental", + "-analyzeduration", + "1000M", + "-probesize", + "1000M", + "-rw_timeout", + "5000000", + ], + "preset-rtmp-generic": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rw_timeout", + "5000000", + "-use_wallclock_as_timestamps", + "1", + "-f", + "live_flv", + ], + "preset-rtsp-generic": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + "-timeout", + "5000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-udp": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "udp", + "-timeout", + "5000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-blue-iris": [ + "-avoid_negative_ts", + "make_zero", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + "-timeout", + "5000000", + "-use_wallclock_as_timestamps", + "1", + ], +} + + +def parse_preset_input(arg: Any, detect_fps: int) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + if arg == "preset-jpeg-generic": + return PRESETS_INPUT[arg].format(f"{detect_fps}") + + return PRESETS_INPUT.get(arg, None) + + +PRESETS_RECORD_OUTPUT = { + "preset-record-generic": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + "-an", + ], + "preset-record-generic-audio": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-c:a", + "aac", + ], + "preset-record-mjpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-jpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-ubiquiti": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-ar", + "44100", + "-c:a", + "aac", + ], +} + + +def parse_preset_output_record(arg: Any) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + return PRESETS_RECORD_OUTPUT.get(arg, None) + + +PRESETS_RTMP_OUTPUT = { + "preset-rtmp-generic": ["-c", "copy", "-f", "flv"], + "preset-rtmp-mjpeg": ["-c:v", "libx264", "-an", "-f", "flv"], + "preset-rtmp-jpeg": ["-c:v", "libx264", "-an", "-f", "flv"], + "preset-rtmp-ubiquiti": [ + "-c:v", + "copy", + "-f", + "flv", + "-ar", + "44100", + "-c:a", + "aac", + ], +} + + +def parse_preset_output_rtmp(arg: Any) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + return PRESETS_RTMP_OUTPUT.get(arg, None) diff --git a/frigate/test/test_ffmpeg_presets.py b/frigate/test/test_ffmpeg_presets.py new file mode 100644 index 000000000..42e380fab --- /dev/null +++ b/frigate/test/test_ffmpeg_presets.py @@ -0,0 +1,152 @@ +import unittest +from frigate.config import FrigateConfig +from frigate.ffmpeg_presets import parse_preset_input + + +class TestFfmpegPresets(unittest.TestCase): + def setUp(self): + self.default_ffmpeg = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect", "rtmp"], + } + ], + "output_args": { + "detect": "-f rawvideo -pix_fmt yuv420p", + "record": "-f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an", + "rtmp": "-c copy -f flv", + }, + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "record": { + "enabled": True, + }, + "rtmp": { + "enabled": True, + }, + "name": "back", + } + }, + } + + def test_default_ffmpeg(self): + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert self.default_ffmpeg == frigate_config.dict(exclude_unset=True) + + def test_ffmpeg_hwaccel_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ + "hwaccel_args" + ] = "preset-rpi-64-h264" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "preset-rpi-64-h264" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v h264_v4l2m2m" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_hwaccel_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ + "hwaccel_args" + ] = "-other-hwaccel args" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "-other-hwaccel args" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_default_ffmpeg_input_arg_preset(self): + frigate_config = FrigateConfig(**self.default_ffmpeg) + + self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ + "input_args" + ] = "preset-rtsp-generic" + frigate_preset_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + frigate_preset_config.cameras["back"].create_ffmpeg_cmds() + assert ( + frigate_preset_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + == frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + ) + + def test_ffmpeg_input_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ + "input_args" + ] = "preset-rtmp-generic" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "preset-rtmp-generic" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert (" ".join(parse_preset_input("preset-rtmp-generic", 5))) in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_input_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = "-some inputs" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "-some inputs" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ + "record" + ] = "preset-record-generic-audio" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "preset-record-generic-audio" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v copy -c:a aac" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ + "record" + ] = "-some output" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "-some output" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_rtmp_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ + "rtmp" + ] = "preset-rtmp-jpeg" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "preset-rtmp-jpeg" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v libx264" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_rtmp_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ + "rtmp" + ] = "-some output" + frigate_config = FrigateConfig(**self.default_ffmpeg) + frigate_config.cameras["back"].create_ffmpeg_cmds() + assert "-some output" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/frigate/util.py b/frigate/util.py index 0a5ef6a39..c74a4b685 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -13,7 +13,7 @@ from abc import ABC, abstractmethod from collections import Counter from collections.abc import Mapping from multiprocessing import shared_memory -from typing import AnyStr +from typing import Any, AnyStr import cv2 import numpy as np @@ -886,6 +886,11 @@ def vainfo_hwaccel() -> sp.CompletedProcess: return sp.run(ffprobe_cmd, capture_output=True) +def get_ffmpeg_arg_list(arg: Any) -> list: + """Use arg if list or convert to list format.""" + return arg if isinstance(arg, list) else arg.split(" ") + + class FrameManager(ABC): @abstractmethod def create(self, name, size) -> AnyStr: