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