Spaces:
Running
Running
| """ | |
| FFmpeg 工具模块 - 提供 FFmpeg 相关的工具函数,特别是硬件加速检测 | |
| 优化多平台兼容性,支持渐进式降级和智能错误处理 | |
| """ | |
| import os | |
| import platform | |
| import subprocess | |
| import tempfile | |
| from typing import Dict, List, Optional, Tuple, Union | |
| from loguru import logger | |
| # 全局变量,存储检测到的硬件加速信息 | |
| _FFMPEG_HW_ACCEL_INFO = { | |
| "available": False, | |
| "type": None, | |
| "encoder": None, | |
| "hwaccel_args": [], | |
| "message": "", | |
| "is_dedicated_gpu": False, | |
| "fallback_available": False, # 是否有备用方案 | |
| "fallback_encoder": None, # 备用编码器 | |
| "platform": None, # 平台信息 | |
| "gpu_vendor": None, # GPU厂商 | |
| "tested_methods": [] # 已测试的方法 | |
| } | |
| # 硬件加速优先级配置(按平台和GPU类型) | |
| HWACCEL_PRIORITY = { | |
| "windows": { | |
| "nvidia": ["cuda", "nvenc", "d3d11va", "dxva2"], | |
| "amd": ["d3d11va", "dxva2", "amf"], # 不再完全禁用AMD | |
| "intel": ["qsv", "d3d11va", "dxva2"], | |
| "unknown": ["d3d11va", "dxva2"] | |
| }, | |
| "darwin": { | |
| "apple": ["videotoolbox"], | |
| "nvidia": ["cuda", "videotoolbox"], | |
| "amd": ["videotoolbox"], | |
| "intel": ["videotoolbox"], | |
| "unknown": ["videotoolbox"] | |
| }, | |
| "linux": { | |
| "nvidia": ["cuda", "nvenc", "vaapi"], | |
| "amd": ["vaapi", "amf"], | |
| "intel": ["qsv", "vaapi"], | |
| "unknown": ["vaapi"] | |
| } | |
| } | |
| # 编码器映射 | |
| ENCODER_MAPPING = { | |
| "cuda": "h264_nvenc", | |
| "nvenc": "h264_nvenc", | |
| "videotoolbox": "h264_videotoolbox", | |
| "qsv": "h264_qsv", | |
| "vaapi": "h264_vaapi", | |
| "amf": "h264_amf", | |
| "d3d11va": "libx264", # D3D11VA只用于解码 | |
| "dxva2": "libx264", # DXVA2只用于解码 | |
| "software": "libx264" | |
| } | |
| def get_null_input() -> str: | |
| """ | |
| 获取平台特定的空输入文件路径 | |
| Returns: | |
| str: 平台特定的空输入路径 | |
| """ | |
| system = platform.system().lower() | |
| if system == "windows": | |
| return "NUL" | |
| else: | |
| return "/dev/null" | |
| def create_test_video() -> str: | |
| """ | |
| 创建一个临时的测试视频文件,用于硬件加速测试 | |
| Returns: | |
| str: 临时测试视频文件路径 | |
| """ | |
| try: | |
| # 创建临时文件 | |
| temp_file = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) | |
| temp_path = temp_file.name | |
| temp_file.close() | |
| # 生成一个简单的测试视频(1秒,黑色画面) | |
| cmd = [ | |
| 'ffmpeg', '-y', '-f', 'lavfi', '-i', 'color=black:size=320x240:duration=1', | |
| '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-t', '1', temp_path | |
| ] | |
| subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) | |
| return temp_path | |
| except Exception as e: | |
| logger.debug(f"创建测试视频失败: {str(e)}") | |
| return get_null_input() | |
| def cleanup_test_video(path: str) -> None: | |
| """ | |
| 清理测试视频文件 | |
| Args: | |
| path: 测试视频文件路径 | |
| """ | |
| try: | |
| if path != get_null_input() and os.path.exists(path): | |
| os.unlink(path) | |
| except Exception as e: | |
| logger.debug(f"清理测试视频失败: {str(e)}") | |
| def check_ffmpeg_installation() -> bool: | |
| """ | |
| 检查ffmpeg是否已安装 | |
| Returns: | |
| bool: 如果安装则返回True,否则返回False | |
| """ | |
| try: | |
| # 在Windows系统上使用UTF-8编码 | |
| is_windows = os.name == 'nt' | |
| if is_windows: | |
| subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=True) | |
| else: | |
| subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) | |
| return True | |
| except (subprocess.SubprocessError, FileNotFoundError): | |
| logger.error("ffmpeg未安装或不在系统PATH中,请安装ffmpeg") | |
| return False | |
| def detect_gpu_vendor() -> str: | |
| """ | |
| 检测GPU厂商 | |
| Returns: | |
| str: GPU厂商 (nvidia, amd, intel, apple, unknown) | |
| """ | |
| system = platform.system().lower() | |
| try: | |
| if system == "windows": | |
| gpu_info = _get_windows_gpu_info().lower() | |
| if 'nvidia' in gpu_info or 'geforce' in gpu_info or 'quadro' in gpu_info: | |
| return "nvidia" | |
| elif 'amd' in gpu_info or 'radeon' in gpu_info: | |
| return "amd" | |
| elif 'intel' in gpu_info: | |
| return "intel" | |
| elif system == "darwin": | |
| # macOS上检查是否为Apple Silicon | |
| if platform.machine().lower() in ['arm64', 'aarch64']: | |
| return "apple" | |
| else: | |
| # Intel Mac,可能有独立显卡 | |
| gpu_info = _get_macos_gpu_info().lower() | |
| if 'nvidia' in gpu_info: | |
| return "nvidia" | |
| elif 'amd' in gpu_info or 'radeon' in gpu_info: | |
| return "amd" | |
| else: | |
| return "intel" | |
| elif system == "linux": | |
| gpu_info = _get_linux_gpu_info().lower() | |
| if 'nvidia' in gpu_info: | |
| return "nvidia" | |
| elif 'amd' in gpu_info or 'radeon' in gpu_info: | |
| return "amd" | |
| elif 'intel' in gpu_info: | |
| return "intel" | |
| except Exception as e: | |
| logger.debug(f"检测GPU厂商失败: {str(e)}") | |
| return "unknown" | |
| def test_hwaccel_method(method: str, test_input: str) -> bool: | |
| """ | |
| 测试特定的硬件加速方法 | |
| Args: | |
| method: 硬件加速方法名称 | |
| test_input: 测试输入文件路径 | |
| Returns: | |
| bool: 是否支持该方法 | |
| """ | |
| try: | |
| # 构建测试命令 | |
| cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error"] | |
| # 添加硬件加速参数 | |
| if method == "cuda": | |
| cmd.extend(["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"]) | |
| elif method == "nvenc": | |
| cmd.extend(["-hwaccel", "cuda"]) | |
| elif method == "videotoolbox": | |
| cmd.extend(["-hwaccel", "videotoolbox"]) | |
| elif method == "qsv": | |
| cmd.extend(["-hwaccel", "qsv"]) | |
| elif method == "vaapi": | |
| # 尝试找到VAAPI设备 | |
| render_device = _find_vaapi_device() | |
| if render_device: | |
| cmd.extend(["-hwaccel", "vaapi", "-vaapi_device", render_device]) | |
| else: | |
| cmd.extend(["-hwaccel", "vaapi"]) | |
| elif method == "d3d11va": | |
| cmd.extend(["-hwaccel", "d3d11va"]) | |
| elif method == "dxva2": | |
| cmd.extend(["-hwaccel", "dxva2"]) | |
| elif method == "amf": | |
| cmd.extend(["-hwaccel", "auto"]) # AMF通常通过auto检测 | |
| else: | |
| return False | |
| # 添加输入和输出 | |
| cmd.extend(["-i", test_input, "-f", "null", "-t", "0.1", "-"]) | |
| # 执行测试 | |
| result = subprocess.run( | |
| cmd, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| check=False, | |
| timeout=10 # 10秒超时 | |
| ) | |
| success = result.returncode == 0 | |
| if success: | |
| logger.debug(f"硬件加速方法 {method} 测试成功") | |
| else: | |
| logger.debug(f"硬件加速方法 {method} 测试失败: {result.stderr[:200]}") | |
| return success | |
| except subprocess.TimeoutExpired: | |
| logger.debug(f"硬件加速方法 {method} 测试超时") | |
| return False | |
| except Exception as e: | |
| logger.debug(f"硬件加速方法 {method} 测试异常: {str(e)}") | |
| return False | |
| def detect_hardware_acceleration() -> Dict[str, Union[bool, str, List[str], None]]: | |
| """ | |
| 检测系统可用的硬件加速器,使用渐进式检测和智能降级 | |
| Returns: | |
| Dict: 包含硬件加速信息的字典 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| # 如果已经检测过,直接返回结果 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is not None: | |
| return _FFMPEG_HW_ACCEL_INFO | |
| # 检查ffmpeg是否已安装 | |
| if not check_ffmpeg_installation(): | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "FFmpeg未安装或不在系统PATH中" | |
| return _FFMPEG_HW_ACCEL_INFO | |
| # 检测平台和GPU信息 | |
| system = platform.system().lower() | |
| gpu_vendor = detect_gpu_vendor() | |
| _FFMPEG_HW_ACCEL_INFO["platform"] = system | |
| _FFMPEG_HW_ACCEL_INFO["gpu_vendor"] = gpu_vendor | |
| logger.debug(f"检测硬件加速 - 平台: {system}, GPU厂商: {gpu_vendor}") | |
| # 获取FFmpeg支持的硬件加速器列表 | |
| try: | |
| hwaccels_cmd = subprocess.run( | |
| ['ffmpeg', '-hide_banner', '-hwaccels'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False | |
| ) | |
| supported_hwaccels = hwaccels_cmd.stdout.lower() if hwaccels_cmd.returncode == 0 else "" | |
| logger.debug(f"FFmpeg支持的硬件加速器: {supported_hwaccels}") | |
| except Exception as e: | |
| logger.warning(f"获取FFmpeg硬件加速器列表失败: {str(e)}") | |
| supported_hwaccels = "" | |
| # 创建测试输入 | |
| test_input = create_test_video() | |
| try: | |
| # 根据平台和GPU厂商获取优先级列表 | |
| priority_list = HWACCEL_PRIORITY.get(system, {}).get(gpu_vendor, []) | |
| if not priority_list: | |
| priority_list = HWACCEL_PRIORITY.get(system, {}).get("unknown", []) | |
| logger.debug(f"硬件加速测试优先级: {priority_list}") | |
| # 按优先级测试硬件加速方法 | |
| for method in priority_list: | |
| # 检查FFmpeg是否支持该方法 | |
| if method not in supported_hwaccels and method != "nvenc": # nvenc可能不在hwaccels列表中 | |
| logger.debug(f"跳过不支持的硬件加速方法: {method}") | |
| continue | |
| _FFMPEG_HW_ACCEL_INFO["tested_methods"].append(method) | |
| if test_hwaccel_method(method, test_input): | |
| # 找到可用的硬件加速方法 | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = method | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = ENCODER_MAPPING.get(method, "libx264") | |
| # 构建硬件加速参数 | |
| if method == "cuda": | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"] | |
| elif method == "nvenc": | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda"] | |
| elif method == "videotoolbox": | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "videotoolbox"] | |
| elif method == "qsv": | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "qsv"] | |
| elif method == "vaapi": | |
| render_device = _find_vaapi_device() | |
| if render_device: | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "vaapi", "-vaapi_device", render_device] | |
| else: | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "vaapi"] | |
| elif method in ["d3d11va", "dxva2"]: | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", method] | |
| elif method == "amf": | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "auto"] | |
| # 判断是否为独立GPU | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = gpu_vendor in ["nvidia", "amd"] or (gpu_vendor == "intel" and "arc" in _get_gpu_info().lower()) | |
| _FFMPEG_HW_ACCEL_INFO["message"] = f"使用 {method} 硬件加速 ({gpu_vendor} GPU)" | |
| logger.debug(f"硬件加速检测成功: {method} ({gpu_vendor})") | |
| break | |
| # 如果没有找到硬件加速,设置软件编码作为备用 | |
| if not _FFMPEG_HW_ACCEL_INFO["available"]: | |
| _FFMPEG_HW_ACCEL_INFO["fallback_available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["fallback_encoder"] = "libx264" | |
| _FFMPEG_HW_ACCEL_INFO["message"] = f"未找到可用的硬件加速,将使用软件编码 (平台: {system}, GPU: {gpu_vendor})" | |
| logger.debug("未检测到硬件加速,将使用软件编码") | |
| finally: | |
| # 清理测试文件 | |
| cleanup_test_video(test_input) | |
| return _FFMPEG_HW_ACCEL_INFO | |
| def _get_gpu_info() -> str: | |
| """ | |
| 获取GPU信息的统一接口 | |
| Returns: | |
| str: GPU信息字符串 | |
| """ | |
| system = platform.system().lower() | |
| if system == "windows": | |
| return _get_windows_gpu_info() | |
| elif system == "darwin": | |
| return _get_macos_gpu_info() | |
| elif system == "linux": | |
| return _get_linux_gpu_info() | |
| else: | |
| return "unknown" | |
| def _get_macos_gpu_info() -> str: | |
| """ | |
| 获取macOS系统的GPU信息 | |
| Returns: | |
| str: GPU信息字符串 | |
| """ | |
| try: | |
| # 使用system_profiler获取显卡信息 | |
| result = subprocess.run( | |
| ['system_profiler', 'SPDisplaysDataType'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False | |
| ) | |
| if result.returncode == 0: | |
| return result.stdout | |
| # 备用方法:检查是否为Apple Silicon | |
| if platform.machine().lower() in ['arm64', 'aarch64']: | |
| return "Apple Silicon GPU" | |
| else: | |
| return "Intel Mac GPU" | |
| except Exception as e: | |
| logger.debug(f"获取macOS GPU信息失败: {str(e)}") | |
| return "unknown" | |
| def _find_vaapi_device() -> Optional[str]: | |
| """ | |
| 查找可用的VAAPI设备 | |
| Returns: | |
| Optional[str]: VAAPI设备路径,如果没有找到则返回None | |
| """ | |
| try: | |
| # 常见的VAAPI设备路径 | |
| possible_devices = [ | |
| "/dev/dri/renderD128", | |
| "/dev/dri/renderD129", | |
| "/dev/dri/card0", | |
| "/dev/dri/card1" | |
| ] | |
| for device in possible_devices: | |
| if os.path.exists(device): | |
| # 测试设备是否可用 | |
| test_cmd = subprocess.run( | |
| ["ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-hwaccel", "vaapi", "-vaapi_device", device, | |
| "-f", "lavfi", "-i", "color=black:size=64x64:duration=0.1", | |
| "-f", "null", "-"], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False | |
| ) | |
| if test_cmd.returncode == 0: | |
| logger.debug(f"找到可用的VAAPI设备: {device}") | |
| return device | |
| logger.debug("未找到可用的VAAPI设备") | |
| return None | |
| except Exception as e: | |
| logger.debug(f"查找VAAPI设备失败: {str(e)}") | |
| return None | |
| def _detect_macos_acceleration(supported_hwaccels: str) -> None: | |
| """ | |
| 检测macOS系统的硬件加速 | |
| Args: | |
| supported_hwaccels: FFmpeg支持的硬件加速器列表 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| if 'videotoolbox' in supported_hwaccels: | |
| # 测试videotoolbox | |
| try: | |
| test_cmd = subprocess.run( | |
| ["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False | |
| ) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "videotoolbox" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_videotoolbox" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "videotoolbox"] | |
| # macOS的Metal GPU加速通常是集成GPU | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = False | |
| return | |
| except Exception as e: | |
| logger.debug(f"测试videotoolbox失败: {str(e)}") | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "macOS系统未检测到可用的videotoolbox硬件加速" | |
| def _detect_windows_acceleration(supported_hwaccels: str) -> None: | |
| """ | |
| 检测Windows系统的硬件加速 - 基于实际测试结果优化 | |
| 重要发现:CUDA硬件解码在视频裁剪场景下会导致滤镜链错误, | |
| 因此优先使用纯NVENC编码器方案,既保证性能又确保兼容性。 | |
| Args: | |
| supported_hwaccels: FFmpeg支持的硬件加速器列表 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| # 在Windows上,首先检查显卡信息 | |
| gpu_info = _get_windows_gpu_info() | |
| logger.debug(f"Windows GPU信息: {gpu_info}") | |
| # 检查是否为Intel集成显卡 | |
| is_intel_integrated = False | |
| if 'intel' in gpu_info.lower() and ('hd graphics' in gpu_info.lower() or 'uhd graphics' in gpu_info.lower()): | |
| logger.info("检测到Intel集成显卡") | |
| is_intel_integrated = True | |
| # 1. 优先检测NVIDIA硬件加速 - 基于实际测试的最佳方案 | |
| if 'nvidia' in gpu_info.lower() or 'geforce' in gpu_info.lower() or 'quadro' in gpu_info.lower(): | |
| logger.info("检测到NVIDIA显卡,开始测试硬件加速") | |
| # 检查NVENC编码器是否可用 | |
| try: | |
| encoders_cmd = subprocess.run( | |
| ["ffmpeg", "-hide_banner", "-encoders"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False | |
| ) | |
| has_nvenc = "h264_nvenc" in encoders_cmd.stdout.lower() | |
| logger.debug(f"NVENC编码器检测结果: {'可用' if has_nvenc else '不可用'}") | |
| if has_nvenc: | |
| # 优先方案:纯NVENC编码器(测试证明最兼容) | |
| logger.debug("测试纯NVENC编码器(推荐方案,避免滤镜链问题)") | |
| test_cmd = subprocess.run([ | |
| "ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-f", "lavfi", "-i", "testsrc=duration=0.1:size=640x480:rate=30", | |
| "-c:v", "h264_nvenc", "-preset", "medium", "-cq", "23", | |
| "-pix_fmt", "yuv420p", "-f", "null", "-" | |
| ], stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "nvenc" # 使用nvenc类型标识纯编码器 | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = [] # 不使用硬件解码参数 | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "纯NVENC编码器(最佳兼容性)" | |
| logger.info("✓ 纯NVENC编码器测试成功") | |
| return | |
| # 备用方案:如果需要的话,可以测试CUDA硬件解码(但不推荐用于视频裁剪) | |
| if 'cuda' in supported_hwaccels: | |
| logger.debug("测试CUDA硬件解码(仅用于非裁剪场景)") | |
| test_cmd = subprocess.run([ | |
| "ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-hwaccel", "cuda", "-hwaccel_output_format", "cuda", | |
| "-f", "lavfi", "-i", "testsrc=duration=0.1:size=640x480:rate=30", | |
| "-c:v", "h264_nvenc", "-preset", "medium", "-cq", "23", | |
| "-pix_fmt", "yuv420p", "-f", "null", "-" | |
| ], stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "cuda" # 保留cuda类型用于特殊场景 | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "CUDA+NVENC(限特殊场景使用)" | |
| _FFMPEG_HW_ACCEL_INFO["fallback_available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["fallback_encoder"] = "h264_nvenc" | |
| logger.info("✓ CUDA+NVENC硬件加速测试成功(备用方案)") | |
| return | |
| except Exception as e: | |
| logger.debug(f"NVIDIA硬件加速测试失败: {str(e)}") | |
| # 2. 检测AMD硬件加速 | |
| if 'amd' in gpu_info.lower() or 'radeon' in gpu_info.lower(): | |
| logger.info("检测到AMD显卡,开始测试硬件加速") | |
| # 检查AMF编码器是否可用 | |
| try: | |
| encoders_cmd = subprocess.run( | |
| ["ffmpeg", "-hide_banner", "-encoders"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False | |
| ) | |
| has_amf = "h264_amf" in encoders_cmd.stdout.lower() | |
| logger.debug(f"AMF编码器检测结果: {'可用' if has_amf else '不可用'}") | |
| if has_amf: | |
| # 测试AMF编码器 | |
| logger.debug("测试AMF编码器") | |
| test_cmd = subprocess.run([ | |
| "ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-f", "lavfi", "-i", "testsrc=duration=0.1:size=640x480:rate=30", | |
| "-c:v", "h264_amf", "-quality", "balanced", "-qp_i", "23", | |
| "-pix_fmt", "yuv420p", "-f", "null", "-" | |
| ], stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "amf" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_amf" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = [] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "AMD AMF编码器" | |
| logger.info("✓ AMD AMF编码器测试成功") | |
| return | |
| except Exception as e: | |
| logger.debug(f"AMD硬件加速测试失败: {str(e)}") | |
| # 3. 检测Intel硬件加速 | |
| if 'intel' in gpu_info.lower() and 'qsv' in supported_hwaccels: | |
| logger.info("检测到Intel显卡,开始测试硬件加速") | |
| try: | |
| encoders_cmd = subprocess.run( | |
| ["ffmpeg", "-hide_banner", "-encoders"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False | |
| ) | |
| has_qsv = "h264_qsv" in encoders_cmd.stdout.lower() | |
| logger.debug(f"QSV编码器检测结果: {'可用' if has_qsv else '不可用'}") | |
| if has_qsv: | |
| # 测试QSV编码器 | |
| logger.debug("测试QSV编码器") | |
| test_cmd = subprocess.run([ | |
| "ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-f", "lavfi", "-i", "testsrc=duration=0.1:size=640x480:rate=30", | |
| "-c:v", "h264_qsv", "-preset", "medium", "-global_quality", "23", | |
| "-pix_fmt", "yuv420p", "-f", "null", "-" | |
| ], stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "qsv" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_qsv" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = [] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "Intel QSV编码器" | |
| logger.info("✓ Intel QSV编码器测试成功") | |
| return | |
| except Exception as e: | |
| logger.debug(f"Intel硬件加速测试失败: {str(e)}") | |
| # 4. 如果没有硬件编码器,使用软件编码 | |
| logger.info("未检测到可用的硬件编码器,使用软件编码") | |
| _FFMPEG_HW_ACCEL_INFO["available"] = False | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "software" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "libx264" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = [] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = False | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "使用软件编码" | |
| def _detect_linux_acceleration(supported_hwaccels: str) -> None: | |
| """ | |
| 检测Linux系统的硬件加速 | |
| Args: | |
| supported_hwaccels: FFmpeg支持的硬件加速器列表 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| # 获取Linux显卡信息 | |
| gpu_info = _get_linux_gpu_info() | |
| is_nvidia = 'nvidia' in gpu_info.lower() | |
| is_intel = 'intel' in gpu_info.lower() | |
| is_amd = 'amd' in gpu_info.lower() or 'radeon' in gpu_info.lower() | |
| # 检测NVIDIA CUDA支持 | |
| if 'cuda' in supported_hwaccels and is_nvidia: | |
| try: | |
| test_cmd = subprocess.run( | |
| ["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False | |
| ) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "cuda" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda"] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True | |
| return | |
| except Exception as e: | |
| logger.debug(f"测试CUDA失败: {str(e)}") | |
| # 检测VAAPI支持 | |
| if 'vaapi' in supported_hwaccels: | |
| # 检查是否存在渲染设备 | |
| render_devices = ['/dev/dri/renderD128', '/dev/dri/renderD129'] | |
| render_device = None | |
| for device in render_devices: | |
| if os.path.exists(device): | |
| render_device = device | |
| break | |
| if render_device: | |
| try: | |
| test_cmd = subprocess.run( | |
| ["ffmpeg", "-hwaccel", "vaapi", "-vaapi_device", render_device, | |
| "-i", "/dev/null", "-f", "null", "-"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False | |
| ) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "vaapi" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_vaapi" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "vaapi", "-vaapi_device", render_device] | |
| # 根据显卡类型判断是否为独立显卡 | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = is_nvidia or (is_amd and not is_intel) | |
| return | |
| except Exception as e: | |
| logger.debug(f"测试VAAPI失败: {str(e)}") | |
| # 检测Intel QSV支持 | |
| if 'qsv' in supported_hwaccels and is_intel: | |
| try: | |
| test_cmd = subprocess.run( | |
| ["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"], | |
| stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False | |
| ) | |
| if test_cmd.returncode == 0: | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "qsv" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_qsv" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "qsv"] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = False # Intel QSV通常是集成GPU | |
| return | |
| except Exception as e: | |
| logger.debug(f"测试QSV失败: {str(e)}") | |
| _FFMPEG_HW_ACCEL_INFO["message"] = f"Linux系统未检测到可用的硬件加速,显卡信息: {gpu_info}" | |
| def _get_windows_gpu_info() -> str: | |
| """ | |
| 获取Windows系统的显卡信息 | |
| Returns: | |
| str: 显卡信息字符串 | |
| """ | |
| try: | |
| # 使用PowerShell获取更可靠的显卡信息,并使用UTF-8编码 | |
| gpu_info = subprocess.run( | |
| ['powershell', '-Command', "Get-WmiObject Win32_VideoController | Select-Object Name | Format-List"], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', text=True, check=False | |
| ) | |
| # 如果PowerShell失败,尝试使用wmic | |
| if not gpu_info.stdout.strip(): | |
| gpu_info = subprocess.run( | |
| ['wmic', 'path', 'win32_VideoController', 'get', 'name'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', text=True, check=False | |
| ) | |
| # 记录详细的显卡信息以便调试 | |
| logger.debug(f"Windows显卡信息: {gpu_info.stdout}") | |
| return gpu_info.stdout | |
| except Exception as e: | |
| logger.warning(f"获取Windows显卡信息失败: {str(e)}") | |
| return "Unknown GPU" | |
| def _get_linux_gpu_info() -> str: | |
| """ | |
| 获取Linux系统的显卡信息 | |
| Returns: | |
| str: 显卡信息字符串 | |
| """ | |
| try: | |
| # 尝试使用lspci命令 | |
| gpu_info = subprocess.run( | |
| ['lspci', '-v', '-nn', '|', 'grep', '-i', 'vga\\|display'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=False | |
| ) | |
| if gpu_info.stdout: | |
| return gpu_info.stdout | |
| # 如果lspci命令失败,尝试使用glxinfo | |
| gpu_info = subprocess.run( | |
| ['glxinfo', '|', 'grep', '-i', 'vendor\\|renderer'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=False | |
| ) | |
| if gpu_info.stdout: | |
| return gpu_info.stdout | |
| return "Unknown GPU" | |
| except Exception as e: | |
| logger.warning(f"获取Linux显卡信息失败: {str(e)}") | |
| return "Unknown GPU" | |
| def get_ffmpeg_hwaccel_args() -> List[str]: | |
| """ | |
| 获取FFmpeg硬件加速参数 | |
| Returns: | |
| List[str]: FFmpeg硬件加速参数列表 | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] | |
| def get_ffmpeg_hwaccel_type() -> Optional[str]: | |
| """ | |
| 获取FFmpeg硬件加速类型 | |
| Returns: | |
| Optional[str]: 硬件加速类型,如果不支持则返回None | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO["type"] if _FFMPEG_HW_ACCEL_INFO["available"] else None | |
| def get_ffmpeg_hwaccel_encoder() -> Optional[str]: | |
| """ | |
| 获取FFmpeg硬件加速编码器 | |
| Returns: | |
| Optional[str]: 硬件加速编码器,如果不支持则返回None | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO["encoder"] if _FFMPEG_HW_ACCEL_INFO["available"] else None | |
| def get_ffmpeg_hwaccel_info() -> Dict[str, Union[bool, str, List[str], None]]: | |
| """ | |
| 获取FFmpeg硬件加速信息 | |
| Returns: | |
| Dict: 包含硬件加速信息的字典 | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO | |
| def is_ffmpeg_hwaccel_available() -> bool: | |
| """ | |
| 检查是否有可用的FFmpeg硬件加速 | |
| Returns: | |
| bool: 如果有可用的硬件加速则返回True,否则返回False | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO["available"] | |
| def is_dedicated_gpu() -> bool: | |
| """ | |
| 检查是否使用独立显卡进行硬件加速 | |
| Returns: | |
| bool: 如果使用独立显卡则返回True,否则返回False | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| return _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] | |
| def get_optimal_ffmpeg_encoder() -> str: | |
| """ | |
| 获取最优的FFmpeg编码器 | |
| Returns: | |
| str: 编码器名称 | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| if _FFMPEG_HW_ACCEL_INFO["available"]: | |
| return _FFMPEG_HW_ACCEL_INFO["encoder"] | |
| elif _FFMPEG_HW_ACCEL_INFO["fallback_available"]: | |
| return _FFMPEG_HW_ACCEL_INFO["fallback_encoder"] | |
| else: | |
| return "libx264" # 默认软件编码器 | |
| def get_ffmpeg_command_with_hwaccel(input_path: str, output_path: str, **kwargs) -> List[str]: | |
| """ | |
| 生成带有硬件加速的FFmpeg命令 | |
| Args: | |
| input_path: 输入文件路径 | |
| output_path: 输出文件路径 | |
| **kwargs: 其他FFmpeg参数 | |
| Returns: | |
| List[str]: FFmpeg命令列表 | |
| """ | |
| # 如果还没有检测过,先进行检测 | |
| if _FFMPEG_HW_ACCEL_INFO["type"] is None: | |
| detect_hardware_acceleration() | |
| cmd = ["ffmpeg", "-y"] | |
| # 添加硬件加速参数 | |
| if _FFMPEG_HW_ACCEL_INFO["available"]: | |
| cmd.extend(_FFMPEG_HW_ACCEL_INFO["hwaccel_args"]) | |
| # 添加输入文件 | |
| cmd.extend(["-i", input_path]) | |
| # 添加编码器 | |
| encoder = get_optimal_ffmpeg_encoder() | |
| cmd.extend(["-c:v", encoder]) | |
| # 添加其他参数 | |
| for key, value in kwargs.items(): | |
| if key.startswith("_"): # 跳过内部参数 | |
| continue | |
| if isinstance(value, list): | |
| cmd.extend(value) | |
| else: | |
| cmd.extend([f"-{key}", str(value)]) | |
| # 添加输出文件 | |
| cmd.append(output_path) | |
| return cmd | |
| def test_ffmpeg_compatibility() -> Dict[str, any]: | |
| """ | |
| 测试FFmpeg兼容性并返回详细报告 | |
| Returns: | |
| Dict: 兼容性测试报告 | |
| """ | |
| report = { | |
| "ffmpeg_installed": False, | |
| "platform": platform.system().lower(), | |
| "gpu_vendor": "unknown", | |
| "hardware_acceleration": { | |
| "available": False, | |
| "type": None, | |
| "encoder": None, | |
| "tested_methods": [] | |
| }, | |
| "software_fallback": { | |
| "available": False, | |
| "encoder": "libx264" | |
| }, | |
| "recommendations": [] | |
| } | |
| # 检查FFmpeg安装 | |
| report["ffmpeg_installed"] = check_ffmpeg_installation() | |
| if not report["ffmpeg_installed"]: | |
| report["recommendations"].append("请安装FFmpeg并确保其在系统PATH中") | |
| return report | |
| # 检测硬件加速 | |
| hwaccel_info = detect_hardware_acceleration() | |
| report["gpu_vendor"] = hwaccel_info.get("gpu_vendor", "unknown") | |
| report["hardware_acceleration"]["available"] = hwaccel_info.get("available", False) | |
| report["hardware_acceleration"]["type"] = hwaccel_info.get("type") | |
| report["hardware_acceleration"]["encoder"] = hwaccel_info.get("encoder") | |
| report["hardware_acceleration"]["tested_methods"] = hwaccel_info.get("tested_methods", []) | |
| # 检查软件备用方案 | |
| report["software_fallback"]["available"] = hwaccel_info.get("fallback_available", True) | |
| report["software_fallback"]["encoder"] = hwaccel_info.get("fallback_encoder", "libx264") | |
| # 生成建议 | |
| if not report["hardware_acceleration"]["available"]: | |
| if report["gpu_vendor"] == "nvidia": | |
| report["recommendations"].append("建议安装NVIDIA驱动和CUDA工具包以启用硬件加速") | |
| elif report["gpu_vendor"] == "amd": | |
| report["recommendations"].append("AMD显卡硬件加速支持有限,建议使用软件编码") | |
| elif report["gpu_vendor"] == "intel": | |
| report["recommendations"].append("建议更新Intel显卡驱动以启用QSV硬件加速") | |
| else: | |
| report["recommendations"].append("未检测到支持的GPU,将使用软件编码") | |
| return report | |
| def force_software_encoding() -> None: | |
| """ | |
| 强制使用软件编码,禁用硬件加速 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| _FFMPEG_HW_ACCEL_INFO.update({ | |
| "available": False, | |
| "type": "software", | |
| "encoder": "libx264", | |
| "hwaccel_args": [], | |
| "message": "强制使用软件编码", | |
| "is_dedicated_gpu": False, | |
| "fallback_available": True, | |
| "fallback_encoder": "libx264" | |
| }) | |
| logger.info("已强制切换到软件编码模式") | |
| def reset_hwaccel_detection() -> None: | |
| """ | |
| 重置硬件加速检测结果,强制重新检测 | |
| 这在以下情况下很有用: | |
| 1. 驱动程序更新后 | |
| 2. 系统配置改变后 | |
| 3. 需要重新测试硬件加速时 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| logger.info("🔄 重置硬件加速检测,将重新检测...") | |
| _FFMPEG_HW_ACCEL_INFO = { | |
| "available": False, | |
| "type": None, | |
| "encoder": None, | |
| "hwaccel_args": [], | |
| "message": "", | |
| "is_dedicated_gpu": False, | |
| "fallback_available": False, | |
| "fallback_encoder": None, | |
| "platform": None, | |
| "gpu_vendor": None, | |
| "tested_methods": [] | |
| } | |
| def test_nvenc_directly() -> bool: | |
| """ | |
| 直接测试NVENC编码器是否可用(无硬件解码) | |
| Returns: | |
| bool: NVENC是否可用 | |
| """ | |
| try: | |
| logger.info("🧪 直接测试NVENC编码器...") | |
| # 测试纯NVENC编码器 | |
| test_cmd = subprocess.run([ | |
| "ffmpeg", "-hide_banner", "-loglevel", "error", | |
| "-f", "lavfi", "-i", "testsrc=duration=1:size=640x480:rate=30", | |
| "-c:v", "h264_nvenc", "-preset", "fast", "-profile:v", "main", | |
| "-pix_fmt", "yuv420p", "-t", "1", "-f", "null", "-" | |
| ], stderr=subprocess.PIPE, stdout=subprocess.PIPE, | |
| encoding='utf-8', text=True, check=False) | |
| if test_cmd.returncode == 0: | |
| logger.info("✅ NVENC编码器测试成功!") | |
| return True | |
| else: | |
| logger.warning(f"❌ NVENC编码器测试失败: {test_cmd.stderr}") | |
| return False | |
| except Exception as e: | |
| logger.error(f"NVENC测试异常: {str(e)}") | |
| return False | |
| def force_use_nvenc_pure() -> None: | |
| """ | |
| 强制使用纯NVENC编码器模式 | |
| 当自动检测失败但你确定NVENC可用时使用 | |
| """ | |
| global _FFMPEG_HW_ACCEL_INFO | |
| logger.info("🎯 强制启用纯NVENC编码器模式...") | |
| # 先测试NVENC是否真的可用 | |
| if test_nvenc_directly(): | |
| _FFMPEG_HW_ACCEL_INFO["available"] = True | |
| _FFMPEG_HW_ACCEL_INFO["type"] = "nvenc_pure" | |
| _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" | |
| _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = [] | |
| _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True | |
| _FFMPEG_HW_ACCEL_INFO["message"] = "强制启用纯NVENC编码器" | |
| logger.info("✅ 已强制启用纯NVENC编码器模式") | |
| else: | |
| logger.error("❌ NVENC编码器不可用,无法强制启用") | |
| def get_hwaccel_status() -> Dict[str, any]: | |
| """ | |
| 获取当前硬件加速状态的详细信息 | |
| Returns: | |
| Dict: 硬件加速状态信息 | |
| """ | |
| hwaccel_info = get_ffmpeg_hwaccel_info() | |
| status = { | |
| "available": hwaccel_info.get("available", False), | |
| "type": hwaccel_info.get("type", "software"), | |
| "encoder": hwaccel_info.get("encoder", "libx264"), | |
| "message": hwaccel_info.get("message", ""), | |
| "is_dedicated_gpu": hwaccel_info.get("is_dedicated_gpu", False), | |
| "platform": platform.system(), | |
| "gpu_vendor": detect_gpu_vendor(), | |
| "ffmpeg_available": check_ffmpeg_installation() | |
| } | |
| return status | |
| # 自动重置检测(在模块导入时执行) | |
| def _auto_reset_on_import(): | |
| """模块导入时自动重置硬件加速检测""" | |
| try: | |
| # 只在平台真正改变时才重置,而不是初始化时 | |
| current_platform = platform.system() | |
| cached_platform = _FFMPEG_HW_ACCEL_INFO.get("platform") | |
| # 只有当已经有缓存的平台信息,且平台改变了,才需要重置 | |
| if cached_platform is not None and cached_platform != current_platform: | |
| reset_hwaccel_detection() | |
| except Exception as e: | |
| logger.debug(f"自动重置检测失败: {str(e)}") | |
| # 执行自动重置 | |
| _auto_reset_on_import() | |