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__) # 向后兼容层 - 处理旧的FfmpegTask对象 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( args, 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