You've already forked FrameTour-RenderWorker
feat(render): 实现渲染系统v2核心架构
- 添加v2支持的任务类型常量定义 - 更新软件版本至0.0.9 - 定义v2统一音视频编码参数 - 实现系统信息工具get_sys_info_v2方法 - 新增get_capabilities和_get_gpu_info功能 - 创建core模块及TaskHandler抽象基类 - 添加渲染系统设计文档包括集群架构、v2 PRD和Worker PRD - 实现任务处理器抽象基类及接口规范
This commit is contained in:
175
handlers/package_ts.py
Normal file
175
handlers/package_ts.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TS 分片封装处理器
|
||||
|
||||
处理 PACKAGE_SEGMENT_TS 任务,将视频片段和对应时间区间的音频封装为 TS 分片。
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from handlers.base import BaseHandler
|
||||
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 使用
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
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')
|
||||
if not self.download_file(video_url, video_file):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_INPUT_UNAVAILABLE,
|
||||
f"Failed to download video: {video_url}"
|
||||
)
|
||||
|
||||
# 2. 下载全局音频
|
||||
audio_file = os.path.join(work_dir, 'audio.aac')
|
||||
if not self.download_file(audio_url, audio_file):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_INPUT_UNAVAILABLE,
|
||||
f"Failed to download audio: {audio_url}"
|
||||
)
|
||||
|
||||
# 3. 构建 TS 封装命令
|
||||
output_file = os.path.join(work_dir, 'segment.ts')
|
||||
cmd = self._build_command(
|
||||
video_file=video_file,
|
||||
audio_file=audio_file,
|
||||
output_file=output_file,
|
||||
start_sec=start_sec,
|
||||
duration_sec=duration_sec
|
||||
)
|
||||
|
||||
# 4. 执行 FFmpeg
|
||||
if not self.run_ffmpeg(cmd, task.task_id):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_FFMPEG_FAILED,
|
||||
"TS packaging failed"
|
||||
)
|
||||
|
||||
# 5. 验证输出文件
|
||||
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"
|
||||
)
|
||||
|
||||
# 6. 获取实际时长(用于 EXTINF)
|
||||
actual_duration = self.probe_duration(output_file)
|
||||
extinf_duration = actual_duration if actual_duration else duration_sec
|
||||
|
||||
# 7. 上传产物
|
||||
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_command(
|
||||
self,
|
||||
video_file: str,
|
||||
audio_file: str,
|
||||
output_file: str,
|
||||
start_sec: float,
|
||||
duration_sec: float
|
||||
) -> List[str]:
|
||||
"""
|
||||
构建 TS 封装命令
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user