You've already forked FrameTour-RenderWorker
feat(重构): 实现新的渲染服务架构
- 新增 RenderTask
This commit is contained in:
237
services/render_service.py
Normal file
237
services/render_service.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Union
|
||||
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
|
||||
from entity.render_task import RenderTask
|
||||
from entity.ffmpeg_command_builder import FFmpegCommandBuilder
|
||||
from util.exceptions import RenderError, FFmpegError
|
||||
from util.ffmpeg import probe_video_info, fade_out_audio, handle_ffmpeg_output, subprocess_args
|
||||
from telemetry import get_tracer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _convert_ffmpeg_task_to_render_task(ffmpeg_task):
|
||||
"""将旧的FfmpegTask转换为新的RenderTask"""
|
||||
from entity.render_task import RenderTask, TaskType
|
||||
|
||||
# 获取输入文件
|
||||
input_files = []
|
||||
for inp in ffmpeg_task.input_file:
|
||||
if hasattr(inp, 'get_output_file'):
|
||||
input_files.append(inp.get_output_file())
|
||||
else:
|
||||
input_files.append(str(inp))
|
||||
|
||||
# 确定任务类型
|
||||
task_type = TaskType.COPY
|
||||
if ffmpeg_task.task_type == 'concat':
|
||||
task_type = TaskType.CONCAT
|
||||
elif ffmpeg_task.task_type == 'encode':
|
||||
task_type = TaskType.ENCODE
|
||||
|
||||
# 创建新任务
|
||||
render_task = RenderTask(
|
||||
input_files=input_files,
|
||||
output_file=ffmpeg_task.output_file,
|
||||
task_type=task_type,
|
||||
resolution=ffmpeg_task.resolution,
|
||||
frame_rate=ffmpeg_task.frame_rate,
|
||||
annexb=ffmpeg_task.annexb,
|
||||
center_cut=ffmpeg_task.center_cut,
|
||||
zoom_cut=ffmpeg_task.zoom_cut,
|
||||
ext_data=getattr(ffmpeg_task, 'ext_data', {})
|
||||
)
|
||||
|
||||
# 复制各种资源
|
||||
render_task.effects = getattr(ffmpeg_task, 'effects', [])
|
||||
render_task.luts = getattr(ffmpeg_task, 'luts', [])
|
||||
render_task.audios = getattr(ffmpeg_task, 'audios', [])
|
||||
render_task.overlays = getattr(ffmpeg_task, 'overlays', [])
|
||||
render_task.subtitles = getattr(ffmpeg_task, 'subtitles', [])
|
||||
|
||||
return render_task
|
||||
|
||||
class RenderService(ABC):
|
||||
"""渲染服务抽象接口"""
|
||||
|
||||
@abstractmethod
|
||||
def render(self, task: Union[RenderTask, 'FfmpegTask']) -> bool:
|
||||
"""
|
||||
执行渲染任务
|
||||
|
||||
Args:
|
||||
task: 渲染任务
|
||||
|
||||
Returns:
|
||||
bool: 渲染是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
||||
"""
|
||||
获取视频信息
|
||||
|
||||
Args:
|
||||
file_path: 视频文件路径
|
||||
|
||||
Returns:
|
||||
tuple: (width, height, duration)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fade_out_audio(self, file_path: str, duration: float, fade_seconds: float = 2.0) -> str:
|
||||
"""
|
||||
音频淡出处理
|
||||
|
||||
Args:
|
||||
file_path: 音频文件路径
|
||||
duration: 音频总时长
|
||||
fade_seconds: 淡出时长
|
||||
|
||||
Returns:
|
||||
str: 处理后的文件路径
|
||||
"""
|
||||
pass
|
||||
|
||||
class DefaultRenderService(RenderService):
|
||||
"""默认渲染服务实现"""
|
||||
|
||||
def render(self, task: Union[RenderTask, 'FfmpegTask']) -> bool:
|
||||
"""执行渲染任务"""
|
||||
# 兼容旧的FfmpegTask
|
||||
if hasattr(task, 'get_ffmpeg_args'): # 这是FfmpegTask
|
||||
# 使用旧的方式执行
|
||||
return self._render_legacy_ffmpeg_task(task)
|
||||
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("render_task") as span:
|
||||
try:
|
||||
# 验证任务
|
||||
task.validate()
|
||||
span.set_attribute("task.type", task.task_type.value)
|
||||
span.set_attribute("task.input_files", len(task.input_files))
|
||||
span.set_attribute("task.output_file", task.output_file)
|
||||
|
||||
# 检查是否需要处理
|
||||
if not task.need_processing():
|
||||
if len(task.input_files) == 1:
|
||||
task.output_file = task.input_files[0]
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return True
|
||||
|
||||
# 构建FFmpeg命令
|
||||
builder = FFmpegCommandBuilder(task)
|
||||
ffmpeg_args = builder.build_command()
|
||||
|
||||
if not ffmpeg_args:
|
||||
# 不需要处理,直接返回
|
||||
if len(task.input_files) == 1:
|
||||
task.output_file = task.input_files[0]
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return True
|
||||
|
||||
# 执行FFmpeg命令
|
||||
return self._execute_ffmpeg(ffmpeg_args, span)
|
||||
|
||||
except Exception as e:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
logger.error(f"Render failed: {e}", exc_info=True)
|
||||
raise RenderError(f"Render failed: {e}") from e
|
||||
|
||||
def _execute_ffmpeg(self, args: list[str], span) -> bool:
|
||||
"""执行FFmpeg命令"""
|
||||
span.set_attribute("ffmpeg.args", " ".join(args))
|
||||
logger.info("Executing FFmpeg: %s", " ".join(args))
|
||||
|
||||
try:
|
||||
# 执行FFmpeg进程
|
||||
process = subprocess.run(
|
||||
["ffmpeg", "-progress", "-", "-loglevel", "error"] + args[1:],
|
||||
stderr=subprocess.PIPE,
|
||||
**subprocess_args(True)
|
||||
)
|
||||
|
||||
span.set_attribute("ffmpeg.return_code", process.returncode)
|
||||
|
||||
# 处理输出
|
||||
if process.stdout:
|
||||
output = handle_ffmpeg_output(process.stdout)
|
||||
span.set_attribute("ffmpeg.output", output)
|
||||
logger.info("FFmpeg output: %s", output)
|
||||
|
||||
# 检查返回码
|
||||
if process.returncode != 0:
|
||||
error_msg = process.stderr.decode() if process.stderr else "Unknown error"
|
||||
span.set_attribute("ffmpeg.error", error_msg)
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
logger.error("FFmpeg failed with return code %d: %s", process.returncode, error_msg)
|
||||
raise FFmpegError(
|
||||
f"FFmpeg execution failed",
|
||||
command=args,
|
||||
return_code=process.returncode,
|
||||
stderr=error_msg
|
||||
)
|
||||
|
||||
# 检查输出文件
|
||||
output_file = args[-1] # 输出文件总是最后一个参数
|
||||
if not os.path.exists(output_file):
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
raise RenderError(f"Output file not created: {output_file}")
|
||||
|
||||
# 检查文件大小
|
||||
file_size = os.path.getsize(output_file)
|
||||
span.set_attribute("output.file_size", file_size)
|
||||
|
||||
if file_size < 4096: # 文件过小
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
raise RenderError(f"Output file too small: {file_size} bytes")
|
||||
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
logger.info("FFmpeg execution completed successfully")
|
||||
return True
|
||||
|
||||
except subprocess.SubprocessError as e:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
logger.error("Subprocess error: %s", e)
|
||||
raise FFmpegError(f"Subprocess error: {e}") from e
|
||||
|
||||
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
||||
"""获取视频信息"""
|
||||
return probe_video_info(file_path)
|
||||
|
||||
def fade_out_audio(self, file_path: str, duration: float, fade_seconds: float = 2.0) -> str:
|
||||
"""音频淡出处理"""
|
||||
return fade_out_audio(file_path, duration, fade_seconds)
|
||||
|
||||
def _render_legacy_ffmpeg_task(self, ffmpeg_task) -> bool:
|
||||
"""兼容处理旧的FfmpegTask"""
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("render_legacy_ffmpeg_task") as span:
|
||||
try:
|
||||
# 处理依赖任务
|
||||
for sub_task in ffmpeg_task.analyze_input_render_tasks():
|
||||
if not self.render(sub_task):
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
return False
|
||||
|
||||
# 获取FFmpeg参数
|
||||
ffmpeg_args = ffmpeg_task.get_ffmpeg_args()
|
||||
|
||||
if not ffmpeg_args:
|
||||
# 不需要处理,直接返回
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return True
|
||||
|
||||
# 执行FFmpeg命令
|
||||
return self._execute_ffmpeg(ffmpeg_args, span)
|
||||
|
||||
except Exception as e:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
logger.error(f"Legacy FFmpeg task render failed: {e}", exc_info=True)
|
||||
raise RenderError(f"Legacy render failed: {e}") from e
|
Reference in New Issue
Block a user