Improve FFmpeg video thumbnail fallback handling

This commit is contained in:
lizerk 2026-08-24 14:05:03 +03:00
parent 26fa3a5301
commit 2e6518bf4b

View File

@ -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: