diff --git a/apps/worker/video_processing/thumbnail_generator.py b/apps/worker/video_processing/thumbnail_generator.py index 175a1f9a1..5429587cd 100755 --- a/apps/worker/video_processing/thumbnail_generator.py +++ b/apps/worker/video_processing/thumbnail_generator.py @@ -38,53 +38,36 @@ def extract_first_frame( """ from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg + _is_temp_output = False if output_path is None: tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) tmp.close() output_path = tmp.name - - # 计算抽帧时间点:取视频时长 * seek_ratio,最少 min_seek_seconds 秒 - try: - duration = probe_duration(video_path) - seek_time = max(min_seek_seconds, duration * seek_ratio) - except Exception: - # probe 失败时 fallback 到第1秒 - seek_time = min_seek_seconds - - # 格式化为 HH:MM:SS.xx - seek_str = _format_seek_time(seek_time) - - # -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用) - # -vframes 1 只取一帧 - # -q:v 2 jpeg 高质量 - scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease" - cmd = [ - FFMPEG_BIN, - "-y", - "-ss", - seek_str, - "-i", - video_path, - "-vframes", - "1", - "-vf", - scale_filter, - "-q:v", - "2", - output_path, - ] + _is_temp_output = True try: - run_ffmpeg(cmd, capture_output=True, timeout=timeout) - except Exception: - # 失败时退回到第0帧兜底 - cmd2 = [ + # 计算抽帧时间点:取视频时长 * seek_ratio,最少 min_seek_seconds 秒 + try: + duration = probe_duration(video_path) + seek_time = max(min_seek_seconds, duration * seek_ratio) + except Exception: + # probe 失败时 fallback 到第1秒 + seek_time = min_seek_seconds + + # 格式化为 HH:MM:SS.xx + seek_str = _format_seek_time(seek_time) + + # -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用) + # -vframes 1 只取一帧 + # -q:v 2 jpeg 高质量 + scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease" + cmd = [ FFMPEG_BIN, "-y", + "-ss", + seek_str, "-i", video_path, - "-ss", - "00:00:00", "-vframes", "1", "-vf", @@ -93,12 +76,40 @@ def extract_first_frame( "2", output_path, ] - run_ffmpeg(cmd2, capture_output=True, timeout=timeout) - if not Path(output_path).exists() or Path(output_path).stat().st_size == 0: - raise RuntimeError(f"Thumbnail generation failed: {output_path}") + try: + run_ffmpeg(cmd, capture_output=True, timeout=timeout) + except Exception: + # 失败时退回到第0帧兜底 + cmd2 = [ + FFMPEG_BIN, + "-y", + "-i", + video_path, + "-ss", + "00:00:00", + "-vframes", + "1", + "-vf", + scale_filter, + "-q:v", + "2", + output_path, + ] + run_ffmpeg(cmd2, capture_output=True, timeout=timeout) - return output_path + if not Path(output_path).exists() or Path(output_path).stat().st_size == 0: + raise RuntimeError(f"Thumbnail generation failed: {output_path}") + + return output_path + except Exception: + # 失败时清理自己创建的临时文件 + if _is_temp_output and output_path: + try: + Path(output_path).unlink(missing_ok=True) + except Exception: + pass + raise def _format_seek_time(seconds: float) -> str: