You've already forked FrameTour-RenderWorker
feat(video): 添加视频转场功能支持
- 在 TASK_TYPES 中新增 COMPOSE_TRANSITION 类型 - 定义 TRANSITION_TYPES 常量支持多种转场效果 - 在 TaskType 枚举中添加 COMPOSE_TRANSITION - 创建 TransitionConfig 数据类处理转场配置 - 为 RenderSpec 添加 transition_in 和 transition_out 属性 - 在 Task 类中添加转场相关的方法 - 新增 ComposeTransitionHandler 处理转场合成任务 - 修改 PackageSegmentTsHandler 支持转场分片封装 - 修改 RenderSegmentVideoHandler 支持 overlap 区域生成 - 在 TaskExecutor 中注册转场处理器
This commit is contained in:
@@ -3,11 +3,12 @@
|
||||
视频片段渲染处理器
|
||||
|
||||
处理 RENDER_SEGMENT_VIDEO 任务,将原素材渲染为符合输出规格的视频片段。
|
||||
支持转场 overlap 区域的帧冻结生成。
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from handlers.base import BaseHandler, VIDEO_ENCODE_ARGS
|
||||
from domain.task import Task, TaskType, RenderSpec, OutputSpec
|
||||
@@ -25,7 +26,7 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
- 下载 LUT 文件(如有)
|
||||
- 下载叠加层(如有)
|
||||
- 构建 FFmpeg 渲染命令
|
||||
- 执行渲染
|
||||
- 执行渲染(支持帧冻结生成 overlap 区域)
|
||||
- 上传产物
|
||||
"""
|
||||
|
||||
@@ -77,7 +78,11 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it")
|
||||
overlay_file = None
|
||||
|
||||
# 4. 构建 FFmpeg 命令
|
||||
# 4. 计算 overlap 时长
|
||||
overlap_head_ms = render_spec.get_overlap_head_ms()
|
||||
overlap_tail_ms = render_spec.get_overlap_tail_ms()
|
||||
|
||||
# 5. 构建 FFmpeg 命令
|
||||
output_file = os.path.join(work_dir, 'output.mp4')
|
||||
cmd = self._build_command(
|
||||
input_file=input_file,
|
||||
@@ -86,28 +91,30 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
output_spec=output_spec,
|
||||
duration_ms=duration_ms,
|
||||
lut_file=lut_file,
|
||||
overlay_file=overlay_file
|
||||
overlay_file=overlay_file,
|
||||
overlap_head_ms=overlap_head_ms,
|
||||
overlap_tail_ms=overlap_tail_ms
|
||||
)
|
||||
|
||||
# 5. 执行 FFmpeg
|
||||
# 6. 执行 FFmpeg
|
||||
if not self.run_ffmpeg(cmd, task.task_id):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_FFMPEG_FAILED,
|
||||
"FFmpeg rendering failed"
|
||||
)
|
||||
|
||||
# 6. 验证输出文件
|
||||
# 7. 验证输出文件
|
||||
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"
|
||||
)
|
||||
|
||||
# 7. 获取实际时长
|
||||
# 8. 获取实际时长
|
||||
actual_duration = self.probe_duration(output_file)
|
||||
actual_duration_ms = int(actual_duration * 1000) if actual_duration else duration_ms
|
||||
|
||||
# 8. 上传产物
|
||||
# 9. 上传产物
|
||||
video_url = self.upload_file(task.task_id, 'video', output_file)
|
||||
if not video_url:
|
||||
return TaskResult.fail(
|
||||
@@ -115,10 +122,15 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
"Failed to upload video"
|
||||
)
|
||||
|
||||
return TaskResult.ok({
|
||||
# 10. 构建结果(包含 overlap 信息)
|
||||
result_data = {
|
||||
'videoUrl': video_url,
|
||||
'actualDurationMs': actual_duration_ms
|
||||
})
|
||||
'actualDurationMs': actual_duration_ms,
|
||||
'overlapHeadMs': overlap_head_ms,
|
||||
'overlapTailMs': overlap_tail_ms
|
||||
}
|
||||
|
||||
return TaskResult.ok(result_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[task:{task.task_id}] Unexpected error: {e}", exc_info=True)
|
||||
@@ -135,7 +147,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
output_spec: OutputSpec,
|
||||
duration_ms: int,
|
||||
lut_file: Optional[str] = None,
|
||||
overlay_file: Optional[str] = None
|
||||
overlay_file: Optional[str] = None,
|
||||
overlap_head_ms: int = 0,
|
||||
overlap_tail_ms: int = 0
|
||||
) -> List[str]:
|
||||
"""
|
||||
构建 FFmpeg 渲染命令
|
||||
@@ -148,6 +162,8 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
duration_ms: 目标时长(毫秒)
|
||||
lut_file: LUT 文件路径(可选)
|
||||
overlay_file: 叠加层文件路径(可选)
|
||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||
|
||||
Returns:
|
||||
FFmpeg 命令参数列表
|
||||
@@ -166,7 +182,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
render_spec=render_spec,
|
||||
output_spec=output_spec,
|
||||
lut_file=lut_file,
|
||||
has_overlay=overlay_file is not None
|
||||
has_overlay=overlay_file is not None,
|
||||
overlap_head_ms=overlap_head_ms,
|
||||
overlap_tail_ms=overlap_tail_ms
|
||||
)
|
||||
|
||||
# 应用滤镜
|
||||
@@ -188,8 +206,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
cmd.extend(['-g', str(gop_size)])
|
||||
cmd.extend(['-keyint_min', str(gop_size)])
|
||||
|
||||
# 时长
|
||||
duration_sec = duration_ms / 1000.0
|
||||
# 时长(包含 overlap 区域)
|
||||
total_duration_ms = duration_ms + overlap_head_ms + overlap_tail_ms
|
||||
duration_sec = total_duration_ms / 1000.0
|
||||
cmd.extend(['-t', str(duration_sec)])
|
||||
|
||||
# 无音频(视频片段不包含音频)
|
||||
@@ -205,7 +224,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
render_spec: RenderSpec,
|
||||
output_spec: OutputSpec,
|
||||
lut_file: Optional[str] = None,
|
||||
has_overlay: bool = False
|
||||
has_overlay: bool = False,
|
||||
overlap_head_ms: int = 0,
|
||||
overlap_tail_ms: int = 0
|
||||
) -> str:
|
||||
"""
|
||||
构建视频滤镜链
|
||||
@@ -215,6 +236,8 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
output_spec: 输出规格
|
||||
lut_file: LUT 文件路径
|
||||
has_overlay: 是否有叠加层
|
||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||
|
||||
Returns:
|
||||
滤镜字符串
|
||||
@@ -265,7 +288,22 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
)
|
||||
filters.append(scale_filter)
|
||||
|
||||
# 5. 构建最终滤镜
|
||||
# 5. 帧冻结(tpad)- 用于转场 overlap 区域
|
||||
# 注意:tpad 必须在缩放之后应用
|
||||
tpad_parts = []
|
||||
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}")
|
||||
|
||||
if tpad_parts:
|
||||
filters.append(f"tpad={':'.join(tpad_parts)}")
|
||||
|
||||
# 6. 构建最终滤镜
|
||||
if has_overlay:
|
||||
# 使用 filter_complex 格式
|
||||
base_filters = ','.join(filters) if filters else 'copy'
|
||||
|
||||
Reference in New Issue
Block a user