From c57524f174fd80bc43fc8ab3b963b0c25083a79f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 4 Feb 2026 17:59:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(video):=20=E6=B7=BB=E5=8A=A0=E6=BA=90?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=97=B6=E9=95=BF=E6=A3=80=E6=B5=8B=E5=92=8C?= =?UTF-8?q?=E5=B8=A7=E5=86=BB=E7=BB=93=E8=A1=A5=E8=B6=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 探测源视频实际时长并计算变速后的有效时长 - 检测源视频时长不足的情况并记录警告日志 - 计算时长短缺并自动冻结最后一帧进行补足 - 更新 FFmpeg 命令构建逻辑以支持时长补足 - 合并转场 overlap 冻结和时长不足冻结的处理 - 添加必要的参数传递以支持时长检测功能 --- handlers/render_video.py | 117 +++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 22 deletions(-) diff --git a/handlers/render_video.py b/handlers/render_video.py index 638b932..f277730 100644 --- a/handlers/render_video.py +++ b/handlers/render_video.py @@ -135,13 +135,36 @@ class RenderSegmentVideoHandler(BaseHandler): logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it") overlay_file = None - # 6. 计算 overlap 时长(用于转场帧冻结) + # 6. 探测源视频时长(仅对视频素材) + # 用于检测时长不足并通过冻结最后一帧补足 + source_duration_sec = None + if not is_image: + source_duration = self.probe_duration(input_file) + if source_duration: + source_duration_sec = source_duration + speed = float(render_spec.speed) if render_spec.speed else 1.0 + if speed > 0: + # 计算变速后的有效时长 + effective_duration_sec = source_duration_sec / speed + required_duration_sec = duration_ms / 1000.0 + + # 如果源视频时长不足,记录日志 + if effective_duration_sec < required_duration_sec: + shortage_sec = required_duration_sec - effective_duration_sec + logger.warning( + f"[task:{task.task_id}] Source video duration insufficient: " + f"effective={effective_duration_sec:.2f}s (speed={speed}), " + f"required={required_duration_sec:.2f}s, " + f"will freeze last frame for {shortage_sec:.2f}s" + ) + + # 7. 计算 overlap 时长(用于转场帧冻结) # 头部 overlap: 来自前一片段的出场转场 overlap_head_ms = task.get_overlap_head_ms() # 尾部 overlap: 当前片段的出场转场 overlap_tail_ms = task.get_overlap_tail_ms_v2() - # 7. 构建 FFmpeg 命令 + # 8. 构建 FFmpeg 命令 output_file = os.path.join(work_dir, 'output.mp4') cmd = self._build_command( input_file=input_file, @@ -152,28 +175,29 @@ class RenderSegmentVideoHandler(BaseHandler): lut_file=lut_file, overlay_file=overlay_file, overlap_head_ms=overlap_head_ms, - overlap_tail_ms=overlap_tail_ms + overlap_tail_ms=overlap_tail_ms, + source_duration_sec=source_duration_sec ) - # 8. 执行 FFmpeg + # 9. 执行 FFmpeg if not self.run_ffmpeg(cmd, task.task_id): return TaskResult.fail( ErrorCode.E_FFMPEG_FAILED, "FFmpeg rendering failed" ) - # 9. 验证输出文件 + # 10. 验证输出文件 if not self.ensure_file_exists(output_file, min_size=4096): return TaskResult.fail( ErrorCode.E_FFMPEG_FAILED, "Output file is missing or too small" ) - # 10. 获取实际时长 + # 11. 获取实际时长 actual_duration = self.probe_duration(output_file) actual_duration_ms = int(actual_duration * 1000) if actual_duration else duration_ms - # 11. 上传产物 + # 12. 上传产物 video_url = self.upload_file(task.task_id, 'video', output_file) if not video_url: return TaskResult.fail( @@ -181,7 +205,7 @@ class RenderSegmentVideoHandler(BaseHandler): "Failed to upload video" ) - # 12. 构建结果(包含 overlap 信息) + # 13. 构建结果(包含 overlap 信息) result_data = { 'videoUrl': video_url, 'actualDurationMs': actual_duration_ms, @@ -310,7 +334,8 @@ class RenderSegmentVideoHandler(BaseHandler): lut_file: Optional[str] = None, overlay_file: Optional[str] = None, overlap_head_ms: int = 0, - overlap_tail_ms: int = 0 + overlap_tail_ms: int = 0, + source_duration_sec: Optional[float] = None ) -> List[str]: """ 构建 FFmpeg 渲染命令 @@ -325,6 +350,7 @@ class RenderSegmentVideoHandler(BaseHandler): overlay_file: 叠加层文件路径(可选) overlap_head_ms: 头部 overlap 时长(毫秒) overlap_tail_ms: 尾部 overlap 时长(毫秒) + source_duration_sec: 源视频实际时长(秒),用于检测时长不足 Returns: FFmpeg 命令参数列表 @@ -347,10 +373,12 @@ class RenderSegmentVideoHandler(BaseHandler): filters = self._build_video_filters( render_spec=render_spec, output_spec=output_spec, + duration_ms=duration_ms, lut_file=lut_file, overlay_file=overlay_file, overlap_head_ms=overlap_head_ms, - overlap_tail_ms=overlap_tail_ms + overlap_tail_ms=overlap_tail_ms, + source_duration_sec=source_duration_sec ) # 应用滤镜 @@ -403,10 +431,12 @@ class RenderSegmentVideoHandler(BaseHandler): self, render_spec: RenderSpec, output_spec: OutputSpec, + duration_ms: int, lut_file: Optional[str] = None, overlay_file: Optional[str] = None, overlap_head_ms: int = 0, - overlap_tail_ms: int = 0 + overlap_tail_ms: int = 0, + source_duration_sec: Optional[float] = None ) -> str: """ 构建视频滤镜链 @@ -414,10 +444,12 @@ class RenderSegmentVideoHandler(BaseHandler): Args: render_spec: 渲染规格 output_spec: 输出规格 + duration_ms: 目标时长(毫秒) lut_file: LUT 文件路径 overlay_file: 叠加层文件路径(支持图片 png/jpg 和视频 mov) overlap_head_ms: 头部 overlap 时长(毫秒) overlap_tail_ms: 尾部 overlap 时长(毫秒) + source_duration_sec: 源视频实际时长(秒),用于检测时长不足 Returns: 滤镜字符串 @@ -496,20 +528,39 @@ class RenderSegmentVideoHandler(BaseHandler): is_video_overlay=is_video_overlay, overlap_head_ms=overlap_head_ms, overlap_tail_ms=overlap_tail_ms, - use_hwdownload=bool(hwaccel_prefix) + use_hwdownload=bool(hwaccel_prefix), + duration_ms=duration_ms, + render_spec=render_spec, + source_duration_sec=source_duration_sec ) - # 6. 帧冻结(tpad)- 用于转场 overlap 区域 + # 6. 帧冻结(tpad)- 用于转场 overlap 区域和时长不足补足 # 注意:tpad 必须在缩放之后应用 tpad_parts = [] + + # 计算是否需要额外的尾部冻结(源视频时长不足) + extra_tail_freeze_sec = 0.0 + if source_duration_sec is not None: + speed = float(render_spec.speed) if render_spec.speed else 1.0 + if speed > 0: + # 计算变速后的有效时长 + effective_duration_sec = source_duration_sec / speed + required_duration_sec = duration_ms / 1000.0 + + # 如果源视频时长不足,需要冻结最后一帧来补足 + if effective_duration_sec < required_duration_sec: + extra_tail_freeze_sec = required_duration_sec - effective_duration_sec + if overlap_head_ms > 0: # 头部冻结:将第一帧冻结指定时长 head_duration_sec = overlap_head_ms / 1000.0 tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}") - if overlap_tail_ms > 0: - # 尾部冻结:将最后一帧冻结指定时长 - tail_duration_sec = overlap_tail_ms / 1000.0 - tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}") + + # 尾部冻结:合并 overlap 和时长不足的冻结 + total_tail_freeze_sec = (overlap_tail_ms / 1000.0) + extra_tail_freeze_sec + if total_tail_freeze_sec > 0: + # 将最后一帧冻结指定时长 + tpad_parts.append(f"stop_mode=clone:stop_duration={total_tail_freeze_sec}") if tpad_parts: filters.append(f"tpad={':'.join(tpad_parts)}") @@ -539,7 +590,10 @@ class RenderSegmentVideoHandler(BaseHandler): is_video_overlay: bool = False, overlap_head_ms: int = 0, overlap_tail_ms: int = 0, - use_hwdownload: bool = False + use_hwdownload: bool = False, + duration_ms: int = 0, + render_spec: Optional[RenderSpec] = None, + source_duration_sec: Optional[float] = None ) -> str: """ 构建包含特效的 filter_complex 滤镜图 @@ -557,6 +611,9 @@ class RenderSegmentVideoHandler(BaseHandler): overlap_head_ms: 头部 overlap 时长 overlap_tail_ms: 尾部 overlap 时长 use_hwdownload: 是否使用了硬件加速解码(已在 base_filters 中包含 hwdownload) + duration_ms: 目标时长(毫秒) + render_spec: 渲染规格(用于获取变速参数) + source_duration_sec: 源视频实际时长(秒),用于检测时长不足 Returns: filter_complex 格式的滤镜字符串 @@ -615,14 +672,30 @@ class RenderSegmentVideoHandler(BaseHandler): current_output = effect_output effect_idx += 1 - # 帧冻结(tpad)- 用于转场 overlap 区域 + # 帧冻结(tpad)- 用于转场 overlap 区域和时长不足补足 tpad_parts = [] + + # 计算是否需要额外的尾部冻结(源视频时长不足) + extra_tail_freeze_sec = 0.0 + if source_duration_sec is not None and render_spec is not None and duration_ms > 0: + speed = float(render_spec.speed) if render_spec.speed else 1.0 + if speed > 0: + # 计算变速后的有效时长 + effective_duration_sec = source_duration_sec / speed + required_duration_sec = duration_ms / 1000.0 + + # 如果源视频时长不足,需要冻结最后一帧来补足 + if effective_duration_sec < required_duration_sec: + extra_tail_freeze_sec = required_duration_sec - effective_duration_sec + if overlap_head_ms > 0: head_duration_sec = overlap_head_ms / 1000.0 tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}") - if overlap_tail_ms > 0: - tail_duration_sec = overlap_tail_ms / 1000.0 - tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}") + + # 尾部冻结:合并 overlap 和时长不足的冻结 + total_tail_freeze_sec = (overlap_tail_ms / 1000.0) + extra_tail_freeze_sec + if total_tail_freeze_sec > 0: + tpad_parts.append(f"stop_mode=clone:stop_duration={total_tail_freeze_sec}") if tpad_parts: tpad_output = '[v_tpad]'