You've already forked FrameTour-RenderWorker
feat(video): 添加源视频时长检测和帧冻结补足功能
- 探测源视频实际时长并计算变速后的有效时长 - 检测源视频时长不足的情况并记录警告日志 - 计算时长短缺并自动冻结最后一帧进行补足 - 更新 FFmpeg 命令构建逻辑以支持时长补足 - 合并转场 overlap 冻结和时长不足冻结的处理 - 添加必要的参数传递以支持时长检测功能
This commit is contained in:
@@ -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]'
|
||||
|
||||
Reference in New Issue
Block a user