From 2e6518bf4b7092b10599b4cf2a2dcb4b6c23aa9d Mon Sep 17 00:00:00 2001 From: lizerk Date: Mon, 24 Aug 2026 14:05:03 +0300 Subject: [PATCH] Improve FFmpeg video thumbnail fallback handling --- backend/tasks/media.py | 61 +++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/backend/tasks/media.py b/backend/tasks/media.py index e917975e..7446c839 100644 --- a/backend/tasks/media.py +++ b/backend/tasks/media.py @@ -26,6 +26,8 @@ IMAGE_EXTENSIONS = { ".webp", } VIDEO_EXTENSIONS = { + ".3gp", + ".avi", ".m4v", ".mkv", ".mov", @@ -97,19 +99,17 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes: output_path = output_file.name filter_value = "scale=640:280:force_original_aspect_ratio=increase,crop=640:280" - command = [ - ffmpeg, - "-hide_banner", - "-loglevel", - "error", - "-ss", - "0.5", - "-i", - input_path, + command_prefix = [ffmpeg, "-hide_banner", "-loglevel", "error"] + command_suffix = [ + "-map", + "0:v:0", "-frames:v", "1", "-vf", filter_value, + "-an", + "-sn", + "-dn", "-c:v", "libwebp", "-quality", @@ -117,22 +117,33 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes: "-y", output_path, ] - result = subprocess.run( - command, - capture_output=True, - check=False, - timeout=30, - ) - if result.returncode != 0: - command[5] = "0" - result = subprocess.run( - command, - capture_output=True, - check=False, - timeout=30, - ) - if result.returncode != 0: - raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) + commands = [ + # Fast input seek works for ordinary MP4/WebM files. + [*command_prefix, "-ss", "0.5", "-i", input_path, *command_suffix], + # Accurate output seek handles fragmented MP4/MOV screen recordings + # whose first keyframe cannot be reached by input seeking. + [*command_prefix, "-i", input_path, "-ss", "0.5", *command_suffix], + # Some recordings start on a non-zero timeline. In that case use the + # first frame FFmpeg can decode instead of requiring a timestamp. + [*command_prefix, "-i", input_path, *command_suffix], + ] + last_error = "FFmpeg did not produce a video thumbnail." + for command in commands: + try: + result = subprocess.run( + command, + capture_output=True, + check=False, + timeout=30, + ) + except subprocess.TimeoutExpired as error: + last_error = str(error) + continue + if result.returncode == 0 and Path(output_path).stat().st_size > 0: + break + last_error = result.stderr.decode("utf-8", errors="replace") + else: + raise RuntimeError(last_error) return Path(output_path).read_bytes() finally: if temporary_input and input_path: