refactor(worker): 合并渲染和TS封装任务为单一处理流程

- 将 RENDER_SEGMENT_VIDEO 和 PACKAGE_SEGMENT_TS 任务类型合并为 RENDER_SEGMENT_TS
- 移除独立的 PackageSegmentTsHandler,将其功能集成到 RenderSegmentTsHandler 中
- 更新任务执行器中的 GPU 资源分配配置
- 修改单元测试以适配新的任务类型名称
- 在 TaskType 枚举中保留历史任务类型的兼容性标记
- 更新常量定义和默认功能配置中的任务类型引用
- 添加视频精确裁剪和 TS 封装功能到渲染处理器中
This commit is contained in:
2026-02-11 14:30:24 +08:00
parent c2ece02ecf
commit 9dd5b6237d
11 changed files with 230 additions and 365 deletions

View File

@@ -10,10 +10,9 @@ SOFTWARE_VERSION = '2.0.0'
# 支持的任务类型 # 支持的任务类型
TASK_TYPES = ( TASK_TYPES = (
'RENDER_SEGMENT_VIDEO', 'RENDER_SEGMENT_TS',
'COMPOSE_TRANSITION', 'COMPOSE_TRANSITION',
'PREPARE_JOB_AUDIO', 'PREPARE_JOB_AUDIO',
'PACKAGE_SEGMENT_TS',
'FINALIZE_MP4', 'FINALIZE_MP4',
) )

View File

@@ -17,9 +17,8 @@ logger = logging.getLogger(__name__)
# 默认支持的任务类型 # 默认支持的任务类型
DEFAULT_CAPABILITIES = [ DEFAULT_CAPABILITIES = [
"RENDER_SEGMENT_VIDEO", "RENDER_SEGMENT_TS",
"PREPARE_JOB_AUDIO", "PREPARE_JOB_AUDIO",
"PACKAGE_SEGMENT_TS",
"FINALIZE_MP4" "FINALIZE_MP4"
] ]

View File

@@ -20,12 +20,15 @@ IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif'}
class TaskType(Enum): class TaskType(Enum):
"""任务类型枚举""" """任务类型枚举"""
RENDER_SEGMENT_VIDEO = "RENDER_SEGMENT_VIDEO" # 渲染视频片段 RENDER_SEGMENT_TS = "RENDER_SEGMENT_TS" # 渲染+封装 TS(合并原 RENDER_SEGMENT_VIDEO + PACKAGE_SEGMENT_TS)
COMPOSE_TRANSITION = "COMPOSE_TRANSITION" # 合成转场效果 COMPOSE_TRANSITION = "COMPOSE_TRANSITION" # 合成转场效果
PREPARE_JOB_AUDIO = "PREPARE_JOB_AUDIO" # 生成全局音频 PREPARE_JOB_AUDIO = "PREPARE_JOB_AUDIO" # 生成全局音频
PACKAGE_SEGMENT_TS = "PACKAGE_SEGMENT_TS" # 封装 TS 分片
FINALIZE_MP4 = "FINALIZE_MP4" # 产出最终 MP4 FINALIZE_MP4 = "FINALIZE_MP4" # 产出最终 MP4
# Deprecated: 历史任务类型,保留枚举值供兼容
RENDER_SEGMENT_VIDEO = "RENDER_SEGMENT_VIDEO"
PACKAGE_SEGMENT_TS = "PACKAGE_SEGMENT_TS"
# 支持的转场类型(对应 FFmpeg xfade 参数) # 支持的转场类型(对应 FFmpeg xfade 参数)
TRANSITION_TYPES = { TRANSITION_TYPES = {

View File

@@ -6,17 +6,15 @@
""" """
from handlers.base import BaseHandler from handlers.base import BaseHandler
from handlers.render_video import RenderSegmentVideoHandler from handlers.render_video import RenderSegmentTsHandler
from handlers.compose_transition import ComposeTransitionHandler from handlers.compose_transition import ComposeTransitionHandler
from handlers.prepare_audio import PrepareJobAudioHandler from handlers.prepare_audio import PrepareJobAudioHandler
from handlers.package_ts import PackageSegmentTsHandler
from handlers.finalize_mp4 import FinalizeMp4Handler from handlers.finalize_mp4 import FinalizeMp4Handler
__all__ = [ __all__ = [
'BaseHandler', 'BaseHandler',
'RenderSegmentVideoHandler', 'RenderSegmentTsHandler',
'ComposeTransitionHandler', 'ComposeTransitionHandler',
'PrepareJobAudioHandler', 'PrepareJobAudioHandler',
'PackageSegmentTsHandler',
'FinalizeMp4Handler', 'FinalizeMp4Handler',
] ]

View File

@@ -1,318 +0,0 @@
# -*- coding: utf-8 -*-
"""
TS 分片封装处理器
处理 PACKAGE_SEGMENT_TS 任务,将视频片段和对应时间区间的音频封装为 TS 分片。
支持转场相关的 overlap 裁剪和转场分片封装。
"""
import os
import logging
from typing import List, Optional
from handlers.base import BaseHandler, VIDEO_ENCODE_ARGS
from domain.task import Task, TaskType
from domain.result import TaskResult, ErrorCode
logger = logging.getLogger(__name__)
class PackageSegmentTsHandler(BaseHandler):
"""
TS 分片封装处理器
职责:
- 下载视频片段
- 下载全局音频
- 截取对应时间区间的音频
- 封装为 TS 分片
- 上传 TS 产物
关键约束:
- TS 必须包含音视频同轨
- 使用 output_ts_offset 保证时间戳连续
- 输出 extinfDurationSec 供 m3u8 使用
转场相关:
- 普通片段 TS:需要裁剪掉 overlap 区域(已被转场分片使用)
- 转场分片 TS:直接封装转场视频产物,无需裁剪
- 无转场时:走原有逻辑,不做裁剪
精确裁剪:
- 当需要裁剪 overlap 区域时,必须使用重编码方式(-vf trim)才能精确切割
- 使用 -c copy 只能从关键帧切割,会导致不精确
"""
def get_supported_type(self) -> TaskType:
return TaskType.PACKAGE_SEGMENT_TS
def handle(self, task: Task) -> TaskResult:
"""处理 TS 封装任务"""
work_dir = self.create_work_dir(task.task_id)
try:
# 解析参数
video_url = task.get_video_url()
audio_url = task.get_audio_url()
start_time_ms = task.get_start_time_ms()
duration_ms = task.get_duration_ms()
output_spec = task.get_output_spec()
# 转场相关参数
is_transition_segment = task.is_transition_segment()
trim_head = task.should_trim_head()
trim_tail = task.should_trim_tail()
trim_head_ms = task.get_trim_head_ms()
trim_tail_ms = task.get_trim_tail_ms()
if not video_url:
return TaskResult.fail(
ErrorCode.E_SPEC_INVALID,
"Missing videoUrl"
)
if not audio_url:
return TaskResult.fail(
ErrorCode.E_SPEC_INVALID,
"Missing audioUrl"
)
# 计算时间参数
start_sec = start_time_ms / 1000.0
duration_sec = duration_ms / 1000.0
# 1. 并行下载视频片段与全局音频
video_file = os.path.join(work_dir, 'video.mp4')
audio_file = os.path.join(work_dir, 'audio.aac')
download_results = self.download_files_parallel([
{
'key': 'video',
'url': video_url,
'dest': video_file,
'required': True
},
{
'key': 'audio',
'url': audio_url,
'dest': audio_file,
'required': True
}
])
video_result = download_results.get('video')
if not video_result or not video_result['success']:
return TaskResult.fail(
ErrorCode.E_INPUT_UNAVAILABLE,
f"Failed to download video: {video_url}"
)
audio_result = download_results.get('audio')
if not audio_result or not audio_result['success']:
return TaskResult.fail(
ErrorCode.E_INPUT_UNAVAILABLE,
f"Failed to download audio: {audio_url}"
)
# 2. 判断是否需要精确裁剪视频
needs_video_trim = not is_transition_segment and (
(trim_head and trim_head_ms > 0) or
(trim_tail and trim_tail_ms > 0)
)
# 3. 如果需要裁剪,先重编码裁剪视频
processed_video_file = video_file
if needs_video_trim:
processed_video_file = os.path.join(work_dir, 'trimmed_video.mp4')
trim_cmd = self._build_trim_command(
video_file=video_file,
output_file=processed_video_file,
trim_head_ms=trim_head_ms if trim_head else 0,
trim_tail_ms=trim_tail_ms if trim_tail else 0,
output_spec=output_spec
)
logger.info(f"[task:{task.task_id}] Trimming video: head={trim_head_ms}ms, tail={trim_tail_ms}ms")
if not self.run_ffmpeg(trim_cmd, task.task_id):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"Video trim failed"
)
if not self.ensure_file_exists(processed_video_file, min_size=1024):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"Trimmed video file is missing or too small"
)
# 4. 构建 TS 封装命令
output_file = os.path.join(work_dir, 'segment.ts')
cmd = self._build_package_command(
video_file=processed_video_file,
audio_file=audio_file,
output_file=output_file,
start_sec=start_sec,
duration_sec=duration_sec
)
# 5. 执行 FFmpeg
if not self.run_ffmpeg(cmd, task.task_id):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"TS packaging failed"
)
# 6. 验证输出文件
if not self.ensure_file_exists(output_file, min_size=1024):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"TS output file is missing or too small"
)
# 8. 获取实际时长(用于 EXTINF)
actual_duration = self.probe_duration(output_file)
extinf_duration = actual_duration if actual_duration else duration_sec
# 9. 上传产物
ts_url = self.upload_file(task.task_id, 'ts', output_file)
if not ts_url:
return TaskResult.fail(
ErrorCode.E_UPLOAD_FAILED,
"Failed to upload TS"
)
return TaskResult.ok({
'tsUrl': ts_url,
'extinfDurationSec': extinf_duration
})
except Exception as e:
logger.error(f"[task:{task.task_id}] Unexpected error: {e}", exc_info=True)
return TaskResult.fail(ErrorCode.E_UNKNOWN, str(e))
finally:
self.cleanup_work_dir(work_dir)
def _build_trim_command(
self,
video_file: str,
output_file: str,
trim_head_ms: int,
trim_tail_ms: int,
output_spec
) -> List[str]:
"""
构建视频精确裁剪命令(重编码方式)
使用 trim 滤镜进行精确帧级裁剪,而非 -ss/-t 参数的关键帧裁剪。
Args:
video_file: 输入视频路径
output_file: 输出视频路径
trim_head_ms: 头部裁剪时长(毫秒)
trim_tail_ms: 尾部裁剪时长(毫秒)
output_spec: 输出规格
Returns:
FFmpeg 命令参数列表
"""
# 获取原视频时长
original_duration = self.probe_duration(video_file)
if not original_duration:
original_duration = 10.0 # 默认值,避免除零
trim_head_sec = trim_head_ms / 1000.0
trim_tail_sec = trim_tail_ms / 1000.0
# 计算裁剪后的起止时间
start_time = trim_head_sec
end_time = original_duration - trim_tail_sec
# 构建 trim 滤镜
vf_filter = f"trim=start={start_time}:end={end_time},setpts=PTS-STARTPTS"
cmd = [
'ffmpeg', '-y', '-hide_banner',
'-i', video_file,
'-vf', vf_filter,
]
# 编码参数
cmd.extend(VIDEO_ENCODE_ARGS)
# 帧率
fps = output_spec.fps
cmd.extend(['-r', str(fps)])
# 计算输出视频帧数,动态调整 GOP
output_duration_sec = end_time - start_time
total_frames = int(output_duration_sec * fps)
# 动态 GOP:短视频使用较小的 GOP
if total_frames <= 1:
gop_size = 1
elif total_frames < fps:
gop_size = total_frames
else:
gop_size = fps # 每秒一个关键帧
cmd.extend(['-g', str(gop_size)])
cmd.extend(['-keyint_min', str(min(gop_size, fps // 2 or 1))])
# 强制第一帧为关键帧
cmd.extend(['-force_key_frames', 'expr:eq(n,0)'])
# 无音频(音频单独处理)
cmd.append('-an')
cmd.append(output_file)
return cmd
def _build_package_command(
self,
video_file: str,
audio_file: str,
output_file: str,
start_sec: float,
duration_sec: float
) -> List[str]:
"""
构建 TS 封装命令
将视频和对应时间区间的音频封装为 TS 分片。
视频使用 copy 模式(已经过精确裁剪或无需裁剪)。
Args:
video_file: 视频文件路径(已处理)
audio_file: 音频文件路径
output_file: 输出文件路径
start_sec: 音频开始时间(秒)
duration_sec: 音频时长(秒)
Returns:
FFmpeg 命令参数列表
"""
cmd = [
'ffmpeg', '-y', '-hide_banner',
# 视频输入
'-i', video_file,
# 音频输入(从 start_sec 开始截取 duration_sec)
'-ss', str(start_sec),
'-t', str(duration_sec),
'-i', audio_file,
# 映射流
'-map', '0:v:0', # 使用第一个输入的视频流
'-map', '1:a:0', # 使用第二个输入的音频流
# 复制编码(视频已处理,无需重编码)
'-c:v', 'copy',
'-c:a', 'copy',
# 关键:时间戳偏移,保证整体连续
'-output_ts_offset', str(start_sec),
# 复用参数
'-muxdelay', '0',
'-muxpreload', '0',
# 输出格式
'-f', 'mpegts',
output_file
]
return cmd

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
视频片段渲染处理器 渲染+TS封装处理器
处理 RENDER_SEGMENT_VIDEO 任务,将原素材渲染为符合输出规格的视频片段 处理 RENDER_SEGMENT_TS 任务,将原素材渲染为视频并封装为 TS 分片
支持转场 overlap 区域的帧冻结生成。 支持转场 overlap 区域的帧冻结生成和精确裁剪
""" """
import os import os
@@ -11,7 +11,7 @@ import logging
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
from handlers.base import BaseHandler from handlers.base import BaseHandler, VIDEO_ENCODE_ARGS
from domain.task import Task, TaskType, RenderSpec, OutputSpec, Effect, IMAGE_EXTENSIONS from domain.task import Task, TaskType, RenderSpec, OutputSpec, Effect, IMAGE_EXTENSIONS
from domain.result import TaskResult, ErrorCode from domain.result import TaskResult, ErrorCode
@@ -26,21 +26,24 @@ def _get_extension_from_url(url: str) -> str:
return ext.lower() if ext else '' return ext.lower() if ext else ''
class RenderSegmentVideoHandler(BaseHandler): class RenderSegmentTsHandler(BaseHandler):
""" """
视频片段渲染处理器 渲染+TS封装处理器
职责: 职责:
- 下载素材文件 - 下载素材文件
- 下载 LUT 文件(如有) - 下载 LUT 文件(如有)
- 下载叠加层(如有) - 下载叠加层(如有)
- 下载音频(如有)
- 构建 FFmpeg 渲染命令 - 构建 FFmpeg 渲染命令
- 执行渲染(支持帧冻结生成 overlap 区域) - 执行渲染(支持帧冻结生成 overlap 区域)
- 裁剪 overlap 区域(如需要)
- 封装为 TS 分片
- 上传产物 - 上传产物
""" """
def get_supported_type(self) -> TaskType: def get_supported_type(self) -> TaskType:
return TaskType.RENDER_SEGMENT_VIDEO return TaskType.RENDER_SEGMENT_TS
def handle(self, task: Task) -> TaskResult: def handle(self, task: Task) -> TaskResult:
"""处理视频渲染任务""" """处理视频渲染任务"""
@@ -85,7 +88,10 @@ class RenderSegmentVideoHandler(BaseHandler):
else: else:
input_file = os.path.join(work_dir, 'input.mp4') input_file = os.path.join(work_dir, 'input.mp4')
# 2. 构建并行下载任务(主素材 + 可选 LUT + 可选叠加层) # 2. 构建并行下载任务(主素材 + 可选 LUT + 可选叠加层 + 可选音频
audio_url = task.get_audio_url()
audio_file = None
lut_file = os.path.join(work_dir, 'lut.cube') if render_spec.lut_url else None lut_file = os.path.join(work_dir, 'lut.cube') if render_spec.lut_url else None
overlay_file = None overlay_file = None
if render_spec.overlay_url: if render_spec.overlay_url:
@@ -121,6 +127,14 @@ class RenderSegmentVideoHandler(BaseHandler):
'dest': overlay_file, 'dest': overlay_file,
'required': False 'required': False
}) })
if audio_url:
audio_file = os.path.join(work_dir, 'audio.aac')
download_jobs.append({
'key': 'audio',
'url': audio_url,
'dest': audio_file,
'required': True
})
download_results = self.download_files_parallel(download_jobs) download_results = self.download_files_parallel(download_jobs)
material_result = download_results.get('material') material_result = download_results.get('material')
@@ -142,6 +156,14 @@ class RenderSegmentVideoHandler(BaseHandler):
logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it") logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it")
overlay_file = None overlay_file = None
if audio_url:
audio_dl = download_results.get('audio')
if not audio_dl or not audio_dl['success']:
return TaskResult.fail(
ErrorCode.E_INPUT_UNAVAILABLE,
f"Failed to download audio: {audio_url}"
)
# 3. 图片素材转换为视频 # 3. 图片素材转换为视频
if is_image: if is_image:
video_input_file = os.path.join(work_dir, 'input_video.mp4') video_input_file = os.path.join(work_dir, 'input_video.mp4')
@@ -219,27 +241,82 @@ class RenderSegmentVideoHandler(BaseHandler):
"Output file is missing or too small" "Output file is missing or too small"
) )
# 9. 获取实际时长 # 9. Overlap 裁剪(仅非转场分片、且有需要裁剪的 overlap 时)
actual_duration = self.probe_duration(output_file) is_transition_seg = task.is_transition_segment()
actual_duration_ms = int(actual_duration * 1000) if actual_duration else duration_ms trim_head = task.should_trim_head()
trim_tail = task.should_trim_tail()
trim_head_ms = task.get_trim_head_ms()
trim_tail_ms = task.get_trim_tail_ms()
needs_video_trim = not is_transition_seg and (
(trim_head and trim_head_ms > 0) or
(trim_tail and trim_tail_ms > 0)
)
# 10. 上传产物 processed_video = output_file
video_url = self.upload_file(task.task_id, 'video', output_file) if needs_video_trim:
if not video_url: processed_video = os.path.join(work_dir, 'trimmed_video.mp4')
return TaskResult.fail( trim_cmd = self._build_trim_command(
ErrorCode.E_UPLOAD_FAILED, video_file=output_file,
"Failed to upload video" output_file=processed_video,
trim_head_ms=trim_head_ms if trim_head else 0,
trim_tail_ms=trim_tail_ms if trim_tail else 0,
output_spec=output_spec
) )
# 11. 构建结果(包含 overlap 信息) logger.info(f"[task:{task.task_id}] Trimming video: head={trim_head_ms}ms, tail={trim_tail_ms}ms")
result_data = {
'videoUrl': video_url,
'actualDurationMs': actual_duration_ms,
'overlapHeadMs': overlap_head_ms,
'overlapTailMs': overlap_tail_ms
}
return TaskResult.ok(result_data) if not self.run_ffmpeg(trim_cmd, task.task_id):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"Video trim failed"
)
if not self.ensure_file_exists(processed_video, min_size=1024):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"Trimmed video file is missing or too small"
)
# 10. 封装 TS
start_time_ms = task.get_start_time_ms()
start_sec = start_time_ms / 1000.0
duration_sec = duration_ms / 1000.0
ts_output = os.path.join(work_dir, 'segment.ts')
ts_cmd = self._build_ts_package_command(
video_file=processed_video,
audio_file=audio_file,
output_file=ts_output,
start_sec=start_sec,
duration_sec=duration_sec
)
if not self.run_ffmpeg(ts_cmd, task.task_id):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"TS packaging failed"
)
if not self.ensure_file_exists(ts_output, min_size=1024):
return TaskResult.fail(
ErrorCode.E_FFMPEG_FAILED,
"TS output file is missing or too small"
)
# 11. 获取 EXTINF 时长 + 上传 TS
actual_duration = self.probe_duration(ts_output)
extinf_duration = actual_duration if actual_duration else duration_sec
ts_url = self.upload_file(task.task_id, 'ts', ts_output)
if not ts_url:
return TaskResult.fail(
ErrorCode.E_UPLOAD_FAILED,
"Failed to upload TS"
)
return TaskResult.ok({
'tsUrl': ts_url,
'extinfDurationSec': extinf_duration
})
except Exception as e: except Exception as e:
logger.error(f"[task:{task.task_id}] Unexpected error: {e}", exc_info=True) logger.error(f"[task:{task.task_id}] Unexpected error: {e}", exc_info=True)
@@ -350,6 +427,116 @@ class RenderSegmentVideoHandler(BaseHandler):
logger.info(f"[task:{task_id}] Converting image to video: {actual_duration_sec:.2f}s at {fps}fps") logger.info(f"[task:{task_id}] Converting image to video: {actual_duration_sec:.2f}s at {fps}fps")
return self.run_ffmpeg(cmd, task_id) return self.run_ffmpeg(cmd, task_id)
def _build_trim_command(
self,
video_file: str,
output_file: str,
trim_head_ms: int,
trim_tail_ms: int,
output_spec
) -> List[str]:
"""
构建视频精确裁剪命令(重编码方式)
使用 trim 滤镜进行精确帧级裁剪,而非 -ss/-t 参数的关键帧裁剪。
Args:
video_file: 输入视频路径
output_file: 输出视频路径
trim_head_ms: 头部裁剪时长(毫秒)
trim_tail_ms: 尾部裁剪时长(毫秒)
output_spec: 输出规格
Returns:
FFmpeg 命令参数列表
"""
original_duration = self.probe_duration(video_file)
if not original_duration:
original_duration = 10.0
trim_head_sec = trim_head_ms / 1000.0
trim_tail_sec = trim_tail_ms / 1000.0
start_time = trim_head_sec
end_time = original_duration - trim_tail_sec
vf_filter = f"trim=start={start_time}:end={end_time},setpts=PTS-STARTPTS"
cmd = [
'ffmpeg', '-y', '-hide_banner',
'-i', video_file,
'-vf', vf_filter,
]
cmd.extend(VIDEO_ENCODE_ARGS)
fps = output_spec.fps
cmd.extend(['-r', str(fps)])
output_duration_sec = end_time - start_time
total_frames = int(output_duration_sec * fps)
if total_frames <= 1:
gop_size = 1
elif total_frames < fps:
gop_size = total_frames
else:
gop_size = fps
cmd.extend(['-g', str(gop_size)])
cmd.extend(['-keyint_min', str(min(gop_size, fps // 2 or 1))])
cmd.extend(['-force_key_frames', 'expr:eq(n,0)'])
cmd.append('-an')
cmd.append(output_file)
return cmd
def _build_ts_package_command(
self,
video_file: str,
audio_file: Optional[str],
output_file: str,
start_sec: float,
duration_sec: float
) -> List[str]:
"""
构建 TS 封装命令
将视频和对应时间区间的音频封装为 TS 分片。
视频使用 copy 模式(已经过渲染/裁剪)。
支持无音频模式(video-only TS)。
Args:
video_file: 视频文件路径(已处理)
audio_file: 音频文件路径(可选,None 时生成 video-only TS)
output_file: 输出文件路径
start_sec: 音频开始时间(秒)
duration_sec: 音频时长(秒)
Returns:
FFmpeg 命令参数列表
"""
cmd = [
'ffmpeg', '-y', '-hide_banner',
'-i', video_file,
]
if audio_file:
cmd.extend(['-ss', str(start_sec), '-t', str(duration_sec), '-i', audio_file])
cmd.extend(['-map', '0:v:0', '-map', '1:a:0', '-c:v', 'copy', '-c:a', 'copy'])
else:
cmd.extend(['-c:v', 'copy'])
cmd.extend([
'-output_ts_offset', str(start_sec),
'-muxdelay', '0',
'-muxpreload', '0',
'-f', 'mpegts',
output_file
])
return cmd
def _build_command( def _build_command(
self, self,
input_file: str, input_file: str,

View File

@@ -4,9 +4,8 @@
RenderWorker v2 入口 RenderWorker v2 入口
支持 v2 API 协议的渲染 Worker,处理以下任务类型: 支持 v2 API 协议的渲染 Worker,处理以下任务类型:
- RENDER_SEGMENT_VIDEO: 渲染视频片段 - RENDER_SEGMENT_TS: 渲染视频片段并封装为 TS
- PREPARE_JOB_AUDIO: 生成全局音频 - PREPARE_JOB_AUDIO: 生成全局音频
- PACKAGE_SEGMENT_TS: 封装 TS 分片
- FINALIZE_MP4: 产出最终 MP4 - FINALIZE_MP4: 产出最终 MP4
使用方法: 使用方法:

View File

@@ -14,7 +14,7 @@ from domain.task import Task, TaskType
# 需要 GPU 加速的任务类型 # 需要 GPU 加速的任务类型
GPU_REQUIRED_TASK_TYPES = { GPU_REQUIRED_TASK_TYPES = {
TaskType.RENDER_SEGMENT_VIDEO, TaskType.RENDER_SEGMENT_TS,
TaskType.COMPOSE_TRANSITION, TaskType.COMPOSE_TRANSITION,
} }
from domain.config import WorkerConfig from domain.config import WorkerConfig
@@ -85,17 +85,15 @@ class TaskExecutor:
def _register_handlers(self): def _register_handlers(self):
"""注册所有任务处理器""" """注册所有任务处理器"""
# 延迟导入以避免循环依赖 # 延迟导入以避免循环依赖
from handlers.render_video import RenderSegmentVideoHandler from handlers.render_video import RenderSegmentTsHandler
from handlers.compose_transition import ComposeTransitionHandler from handlers.compose_transition import ComposeTransitionHandler
from handlers.prepare_audio import PrepareJobAudioHandler from handlers.prepare_audio import PrepareJobAudioHandler
from handlers.package_ts import PackageSegmentTsHandler
from handlers.finalize_mp4 import FinalizeMp4Handler from handlers.finalize_mp4 import FinalizeMp4Handler
handlers = [ handlers = [
RenderSegmentVideoHandler(self.config, self.api_client), RenderSegmentTsHandler(self.config, self.api_client),
ComposeTransitionHandler(self.config, self.api_client), ComposeTransitionHandler(self.config, self.api_client),
PrepareJobAudioHandler(self.config, self.api_client), PrepareJobAudioHandler(self.config, self.api_client),
PackageSegmentTsHandler(self.config, self.api_client),
FinalizeMp4Handler(self.config, self.api_client), FinalizeMp4Handler(self.config, self.api_client),
] ]

View File

@@ -21,7 +21,7 @@ class _DummyHandler(BaseHandler):
return TaskResult.ok({}) return TaskResult.ok({})
def get_supported_type(self): def get_supported_type(self):
return TaskType.RENDER_SEGMENT_VIDEO return TaskType.RENDER_SEGMENT_TS
def _create_handler(tmp_path): def _create_handler(tmp_path):

View File

@@ -4,7 +4,7 @@ import pytest
from domain.config import WorkerConfig from domain.config import WorkerConfig
from domain.task import Effect, OutputSpec, RenderSpec from domain.task import Effect, OutputSpec, RenderSpec
from handlers.render_video import RenderSegmentVideoHandler from handlers.render_video import RenderSegmentTsHandler
class _DummyApiClient: class _DummyApiClient:
@@ -20,7 +20,7 @@ def _create_handler(tmp_path):
cache_enabled=False, cache_enabled=False,
cache_dir=str(tmp_path / 'cache') cache_dir=str(tmp_path / 'cache')
) )
return RenderSegmentVideoHandler(config, _DummyApiClient()) return RenderSegmentTsHandler(config, _DummyApiClient())
def test_get_zoom_params_with_valid_values(): def test_get_zoom_params_with_valid_values():

View File

@@ -7,7 +7,7 @@ import util.tracing as tracing_module
def _create_task_stub(): def _create_task_stub():
task_type = SimpleNamespace(value="RENDER_SEGMENT_VIDEO") task_type = SimpleNamespace(value="RENDER_SEGMENT_TS")
return SimpleNamespace( return SimpleNamespace(
task_id="task-1001", task_id="task-1001",
task_type=task_type, task_type=task_type,
@@ -28,7 +28,7 @@ def test_task_trace_scope_sets_and_resets_context(monkeypatch):
context = tracing.get_current_task_context() context = tracing.get_current_task_context()
assert context is not None assert context is not None
assert context.task_id == "task-1001" assert context.task_id == "task-1001"
assert context.task_type == "RENDER_SEGMENT_VIDEO" assert context.task_type == "RENDER_SEGMENT_TS"
assert context.job_id == "job-2002" assert context.job_id == "job-2002"
assert context.segment_id == "seg-3003" assert context.segment_id == "seg-3003"