Configurable ffmpeg (#13722)

* Install multiple ffmpeg versions and add config to make it configurable

* Update docs

* Run ffprobe too

* Cleanup

* Apply config to go2rtc as well

* Fix ffmpeg bin

* Docs

* Restore path

* Cleanup env var

* Fix ffmpeg path for encoding

* Fix export

* Formatting
This commit is contained in:
Nicolas Mowen
2024-09-13 14:14:51 -06:00
committed by GitHub
parent 641f1244dd
commit 5ff476c6f9
23 changed files with 172 additions and 84 deletions

View File

@@ -414,7 +414,7 @@ def ffprobe():
output = []
for path in paths:
ffprobe = ffprobe_stream(path.strip())
ffprobe = ffprobe_stream(current_app.frigate_config.ffmpeg, path.strip())
output.append(
{
"return_code": ffprobe.returncode,

View File

@@ -17,6 +17,7 @@ from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name
from werkzeug.utils import secure_filename
from frigate.config import FrigateConfig
from frigate.const import (
CACHE_DIR,
CLIPS_DIR,
@@ -216,9 +217,10 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str, format: str):
height = request.args.get("height", type=int)
codec = "png" if format == "png" else "mjpeg"
config: FrigateConfig = current_app.frigate_config
image_data = get_image_from_recording(
recording.path, time_in_segment, codec, height
config.ffmpeg, recording.path, time_in_segment, codec, height
)
if not image_data:
@@ -273,9 +275,12 @@ def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str):
)
try:
config: FrigateConfig = current_app.frigate_config
recording: Recordings = recording_query.get()
time_in_segment = frame_time - recording.start_time
image_data = get_image_from_recording(recording.path, time_in_segment, "png")
image_data = get_image_from_recording(
config.ffmpeg, recording.path, time_in_segment, "png"
)
if not image_data:
return make_response(
@@ -474,9 +479,11 @@ def recording_clip(camera_name, start_ts, end_ts):
file_name = secure_filename(file_name)
path = os.path.join(CLIPS_DIR, f"cache/{file_name}")
config: FrigateConfig = current_app.frigate_config
if not os.path.exists(path):
ffmpeg_cmd = [
"ffmpeg",
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-y",
"-protocol_whitelist",
@@ -1141,8 +1148,9 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
diff = start_ts - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = current_app.frigate_config
ffmpeg_cmd = [
"ffmpeg",
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",
@@ -1206,9 +1214,10 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
last_file = selected_previews[-2]
selected_previews.append(last_file)
config: FrigateConfig = current_app.frigate_config
ffmpeg_cmd = [
"ffmpeg",
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",
@@ -1301,8 +1310,9 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=604800):
diff = start_ts - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = current_app.frigate_config
ffmpeg_cmd = [
"ffmpeg",
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",
@@ -1364,9 +1374,10 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=604800):
last_file = selected_previews[-2]
selected_previews.append(last_file)
config: FrigateConfig = current_app.frigate_config
ffmpeg_cmd = [
"ffmpeg",
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",

View File

@@ -374,7 +374,7 @@ class FrigateApp:
except PermissionError:
logger.error("Unable to write to /config to save export state")
migrate_exports(self.config.cameras.keys())
migrate_exports(self.config.ffmpeg, self.config.cameras.keys())
def init_external_event_processor(self) -> None:
self.external_event_processor = ExternalEventProcessor(self.config)

View File

@@ -866,6 +866,7 @@ class FfmpegOutputArgsConfig(FrigateBaseModel):
class FfmpegConfig(FrigateBaseModel):
path: str = Field(default="default", title="FFmpeg path")
global_args: Union[str, List[str]] = Field(
default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
)
@@ -884,6 +885,34 @@ class FfmpegConfig(FrigateBaseModel):
title="Time in seconds to wait before FFmpeg retries connecting to the camera.",
)
@property
def ffmpeg_path(self) -> str:
if self.path == "default":
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59:
return "/usr/lib/ffmpeg/7.0/bin/ffmpeg"
else:
return "ffmpeg"
elif self.path == "7.0":
return "/usr/lib/ffmpeg/7.0/bin/ffmpeg"
elif self.path == "5.0":
return "/usr/lib/ffmpeg/5.0/bin/ffmpeg"
else:
return f"{self.path}/bin/ffmpeg"
@property
def ffprobe_path(self) -> str:
if self.path == "default":
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59:
return "/usr/lib/ffmpeg/7.0/bin/ffprobe"
else:
return "ffprobe"
elif self.path == "7.0":
return "/usr/lib/ffmpeg/7.0/bin/ffprobe"
elif self.path == "5.0":
return "/usr/lib/ffmpeg/5.0/bin/ffprobe"
else:
return f"{self.path}/bin/ffprobe"
class CameraRoleEnum(str, Enum):
audio = "audio"
@@ -1194,7 +1223,7 @@ class CameraConfig(FrigateBaseModel):
)
cmd = (
["ffmpeg"]
[self.ffmpeg.ffmpeg_path]
+ global_args
+ hwaccel_args
+ input_args
@@ -1520,7 +1549,9 @@ class FrigateConfig(FrigateBaseModel):
if need_detect_dimensions or need_record_fourcc:
stream_info = {"width": 0, "height": 0, "fourcc": None}
try:
stream_info = stream_info_retriever.get_stream_info(input.path)
stream_info = stream_info_retriever.get_stream_info(
config.ffmpeg, input.path
)
except Exception:
logger.warn(
f"Error detecting stream parameters automatically for {input.path} Applying default values."

View File

@@ -50,7 +50,7 @@ def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> list[str]:
or get_ffmpeg_arg_list(ffmpeg.input_args)
)
return (
["ffmpeg", "-vn", "-threads", "1"]
[ffmpeg.ffmpeg_path, "-vn", "-threads", "1"]
+ input_args
+ ["-i"]
+ [ffmpeg_input.path]

View File

@@ -49,12 +49,12 @@ class LibvaGpuSelector:
FPS_VFR_PARAM = (
"-fps_mode vfr"
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59
else "-vsync 2"
)
TIMEOUT_PARAM = (
"-timeout"
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59
if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59
else "-stimeout"
)
@@ -111,17 +111,17 @@ PRESETS_HW_ACCEL_SCALE["preset-nvidia-h265"] = PRESETS_HW_ACCEL_SCALE[
]
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
"preset-rpi-64-h265": "ffmpeg -hide_banner {0} -c:v hevc_v4l2m2m {1}",
FFMPEG_HWACCEL_VAAPI: "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {1}",
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
FFMPEG_HWACCEL_NVIDIA: "ffmpeg -hide_banner {0} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {1}",
"preset-jetson-h264": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}",
"preset-jetson-h265": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}",
"preset-rk-h264": "ffmpeg -hide_banner {0} -c:v h264_rkmpp -profile:v high {1}",
"preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}",
"default": "ffmpeg -hide_banner {0} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {1}",
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}",
FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}",
"preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
"preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}",
"preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}",
"preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}",
"preset-rk-h264": "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}",
"preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v high {2}",
"default": "{0} -hide_banner {1} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {2}",
}
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = (
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA]
@@ -131,18 +131,18 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h265"] = (
)
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}",
"preset-rpi-64-h265": "ffmpeg -hide_banner {0} -c:v hevc_v4l2m2m -pix_fmt yuv420p {1}",
FFMPEG_HWACCEL_VAAPI: "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi {1}",
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
FFMPEG_HWACCEL_NVIDIA: "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v h264_nvenc {1}",
"preset-nvidia-h265": "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v hevc_nvenc {1}",
"preset-jetson-h264": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}",
"preset-jetson-h265": "ffmpeg -hide_banner {0} -c:v hevc_nvmpi -profile high {1}",
"preset-rk-h264": "ffmpeg -hide_banner {0} -c:v h264_rkmpp -profile:v high {1}",
"preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}",
"default": "ffmpeg -hide_banner {0} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {1}",
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}",
FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi {2}",
"preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
"preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc {2}",
"preset-nvidia-h265": "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v hevc_nvenc {2}",
"preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}",
"preset-jetson-h265": "{0} -hide_banner {1} -c:v hevc_nvmpi -profile high {2}",
"preset-rk-h264": "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}",
"preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v high {2}",
"default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {2}",
}
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = (
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA]
@@ -150,7 +150,7 @@ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = (
# encoding of previews is only done on CPU due to comparable encode times and better quality from libx264
PRESETS_HW_ACCEL_ENCODE_PREVIEW = {
"default": "ffmpeg -hide_banner {0} -c:v libx264 -profile:v baseline -preset:v ultrafast {1}",
"default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}",
}
@@ -197,7 +197,11 @@ class EncodeTypeEnum(str, Enum):
def parse_preset_hardware_acceleration_encode(
arg: Any, input: str, output: str, type: EncodeTypeEnum = EncodeTypeEnum.birdseye
ffmpeg_path: str,
arg: Any,
input: str,
output: str,
type: EncodeTypeEnum = EncodeTypeEnum.birdseye,
) -> str:
"""Return the correct scaling preset or default preset if none is set."""
if type == EncodeTypeEnum.birdseye:
@@ -215,6 +219,7 @@ def parse_preset_hardware_acceleration_encode(
arg = "default"
return arg_map.get(arg, arg_map["default"]).format(
ffmpeg_path,
input,
output,
_gpu_selector.get_selected_gpu(),

View File

@@ -15,7 +15,7 @@ import cv2
import numpy as np
from frigate.comms.config_updater import ConfigSubscriber
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig
from frigate.const import BASE_DIR, BIRDSEYE_PIPE
from frigate.util.image import (
SharedMemoryFrameManager,
@@ -112,7 +112,7 @@ class Canvas:
class FFMpegConverter(threading.Thread):
def __init__(
self,
camera: str,
ffmpeg: FfmpegConfig,
input_queue: queue.Queue,
stop_event: mp.Event,
in_width: int,
@@ -123,8 +123,8 @@ class FFMpegConverter(threading.Thread):
birdseye_rtsp: bool = False,
):
threading.Thread.__init__(self)
self.name = f"{camera}_output_converter"
self.camera = camera
self.name = "birdseye_output_converter"
self.camera = "birdseye"
self.input_queue = input_queue
self.stop_event = stop_event
self.bd_pipe = None
@@ -133,7 +133,7 @@ class FFMpegConverter(threading.Thread):
self.recreate_birdseye_pipe()
ffmpeg_cmd = [
"ffmpeg",
ffmpeg.ffmpeg_path,
"-threads",
"1",
"-f",
@@ -725,7 +725,7 @@ class Birdseye:
self.config = config
self.input = queue.Queue(maxsize=10)
self.converter = FFMpegConverter(
"birdseye",
config.ffmpeg,
self.input,
stop_event,
config.birdseye.width,

View File

@@ -6,7 +6,7 @@ import queue
import subprocess as sp
import threading
from frigate.config import CameraConfig
from frigate.config import CameraConfig, FfmpegConfig
logger = logging.getLogger(__name__)
@@ -15,6 +15,7 @@ class FFMpegConverter(threading.Thread):
def __init__(
self,
camera: str,
ffmpeg: FfmpegConfig,
input_queue: queue.Queue,
stop_event: mp.Event,
in_width: int,
@@ -30,7 +31,7 @@ class FFMpegConverter(threading.Thread):
self.stop_event = stop_event
ffmpeg_cmd = [
"ffmpeg",
ffmpeg.ffmpeg_path,
"-threads",
"1",
"-f",
@@ -142,6 +143,7 @@ class JsmpegCamera:
)
self.converter = FFMpegConverter(
config.name,
config.ffmpeg,
self.input,
stop_event,
config.frame_shape[1],

View File

@@ -78,6 +78,7 @@ class FFMpegConverter(threading.Thread):
# write a PREVIEW at fps and 1 key frame per clip
self.ffmpeg_cmd = parse_preset_hardware_acceleration_encode(
config.ffmpeg.ffmpeg_path,
config.ffmpeg.hwaccel_args,
input="-f concat -y -protocol_whitelist pipe,file -safe 0 -threads 1 -i /dev/stdin",
output=f"-threads 1 -g {PREVIEW_KEYFRAME_INTERVAL} -bf 0 -b:v {PREVIEW_QUALITY_BIT_RATES[self.config.record.preview.quality]} {FPS_VFR_PARAM} -movflags +faststart -pix_fmt yuv420p {self.path}",

View File

@@ -14,7 +14,7 @@ from typing import Optional
from peewee import DoesNotExist
from frigate.config import FrigateConfig
from frigate.config import FfmpegConfig, FrigateConfig
from frigate.const import (
CACHE_DIR,
CLIPS_DIR,
@@ -116,7 +116,7 @@ class RecordingExporter(threading.Thread):
minutes = int(diff / 60)
seconds = int(diff % 60)
ffmpeg_cmd = [
"ffmpeg",
self.config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",
@@ -230,11 +230,12 @@ class RecordingExporter(threading.Thread):
if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = (
f"ffmpeg -hide_banner {ffmpeg_input} -c copy -movflags +faststart {video_path}"
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart {video_path}"
).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.ffmpeg_path,
self.config.ffmpeg.hwaccel_args,
f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}",
f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}",
@@ -267,7 +268,7 @@ class RecordingExporter(threading.Thread):
logger.debug(f"Finished exporting {video_path}")
def migrate_exports(camera_names: list[str]):
def migrate_exports(ffmpeg: FfmpegConfig, camera_names: list[str]):
Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True)
exports = []
@@ -286,7 +287,7 @@ def migrate_exports(camera_names: list[str]):
) # use jpg because webp encoder can't get quality low enough
ffmpeg_cmd = [
"ffmpeg",
ffmpeg.ffmpeg_path,
"-hide_banner",
"-loglevel",
"warning",

View File

@@ -209,7 +209,9 @@ class RecordingMaintainer(threading.Thread):
if cache_path in self.end_time_cache:
end_time, duration = self.end_time_cache[cache_path]
else:
segment_info = await get_video_properties(cache_path, get_duration=True)
segment_info = await get_video_properties(
self.config.ffmpeg, cache_path, get_duration=True
)
if segment_info["duration"]:
duration = float(segment_info["duration"])
@@ -387,7 +389,7 @@ class RecordingMaintainer(threading.Thread):
# add faststart to kept segments to improve metadata reading
p = await asyncio.create_subprocess_exec(
"ffmpeg",
self.config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-y",
"-i",

View File

@@ -314,10 +314,10 @@ class StreamInfoRetriever:
def __init__(self) -> None:
self.stream_cache: dict[str, tuple[int, int]] = {}
def get_stream_info(self, path: str) -> str:
def get_stream_info(self, ffmpeg, path: str) -> str:
if path in self.stream_cache:
return self.stream_cache[path]
info = asyncio.run(get_video_properties(path))
info = asyncio.run(get_video_properties(ffmpeg, path))
self.stream_cache[path] = info
return info

View File

@@ -765,12 +765,16 @@ def add_mask(mask: str, mask_img: np.ndarray):
def get_image_from_recording(
file_path: str, relative_frame_time: float, codec: str, height: Optional[int] = None
ffmpeg, # Ffmpeg Config
file_path: str,
relative_frame_time: float,
codec: str,
height: Optional[int] = None,
) -> Optional[any]:
"""retrieve a frame from given time in recording file."""
ffmpeg_cmd = [
"ffmpeg",
ffmpeg.executable_path,
"-hide_banner",
"-loglevel",
"warning",

View File

@@ -378,11 +378,11 @@ def get_jetson_stats() -> dict[int, dict]:
return results
def ffprobe_stream(path: str) -> sp.CompletedProcess:
def ffprobe_stream(ffmpeg, path: str) -> sp.CompletedProcess:
"""Run ffprobe on stream."""
clean_path = escape_special_characters(path)
ffprobe_cmd = [
"ffprobe",
ffmpeg.ffprobe_path,
"-timeout",
"1000000",
"-print_format",
@@ -438,7 +438,9 @@ def auto_detect_hwaccel() -> str:
return ""
async def get_video_properties(url, get_duration=False) -> dict[str, any]:
async def get_video_properties(
ffmpeg, url: str, get_duration: bool = False
) -> dict[str, any]:
async def calculate_duration(video: Optional[any]) -> float:
duration = None
@@ -453,7 +455,7 @@ async def get_video_properties(url, get_duration=False) -> dict[str, any]:
# if cv2 failed need to use ffprobe
if duration is None:
p = await asyncio.create_subprocess_exec(
"ffprobe",
ffmpeg.ffprobe_path,
"-v",
"error",
"-show_entries",