You've already forked FrameTour-RenderWorker
237 lines
8.8 KiB
Python
237 lines
8.8 KiB
Python
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 |