优化 ffmpeg 硬件加速兼容性

This commit is contained in:
linyq 2025-05-19 02:41:30 +08:00
parent 6356a140aa
commit 47cd4f145d
8 changed files with 596 additions and 393 deletions

View File

@ -13,6 +13,7 @@ from app.config import config
from app.models.exception import HttpException from app.models.exception import HttpException
from app.router import root_api_router from app.router import root_api_router
from app.utils import utils from app.utils import utils
from app.utils import ffmpeg_utils
def exception_handler(request: Request, e: HttpException): def exception_handler(request: Request, e: HttpException):
@ -80,3 +81,10 @@ def shutdown_event():
@app.on_event("startup") @app.on_event("startup")
def startup_event(): def startup_event():
logger.info("startup event") logger.info("startup event")
# 检测FFmpeg硬件加速
hwaccel_info = ffmpeg_utils.detect_hardware_acceleration()
if hwaccel_info["available"]:
logger.info(f"FFmpeg硬件加速检测结果: 可用 | 类型: {hwaccel_info['type']} | 编码器: {hwaccel_info['encoder']} | 独立显卡: {hwaccel_info['is_dedicated_gpu']} | 参数: {hwaccel_info['hwaccel_args']}")
else:
logger.warning(f"FFmpeg硬件加速不可用: {hwaccel_info['message']}, 将使用CPU软件编码")

View File

@ -5,7 +5,7 @@
@Project: NarratoAI @Project: NarratoAI
@File : clip_video @File : clip_video
@Author : 小林同学 @Author : 小林同学
@Date : 2025/5/6 下午6:14 @Date : 2025/5/6 下午6:14
''' '''
import os import os
@ -16,14 +16,16 @@ from loguru import logger
from typing import Dict, List, Optional from typing import Dict, List, Optional
from pathlib import Path from pathlib import Path
from app.utils import ffmpeg_utils
def parse_timestamp(timestamp: str) -> tuple: def parse_timestamp(timestamp: str) -> tuple:
""" """
解析时间戳字符串返回开始和结束时间 解析时间戳字符串返回开始和结束时间
Args: Args:
timestamp: 格式为'HH:MM:SS-HH:MM:SS''HH:MM:SS,sss-HH:MM:SS,sss'的时间戳字符串 timestamp: 格式为'HH:MM:SS-HH:MM:SS''HH:MM:SS,sss-HH:MM:SS,sss'的时间戳字符串
Returns: Returns:
tuple: (开始时间, 结束时间) 格式为'HH:MM:SS''HH:MM:SS,sss' tuple: (开始时间, 结束时间) 格式为'HH:MM:SS''HH:MM:SS,sss'
""" """
@ -34,37 +36,37 @@ def parse_timestamp(timestamp: str) -> tuple:
def calculate_end_time(start_time: str, duration: float, extra_seconds: float = 1.0) -> str: def calculate_end_time(start_time: str, duration: float, extra_seconds: float = 1.0) -> str:
""" """
根据开始时间和持续时间计算结束时间 根据开始时间和持续时间计算结束时间
Args: Args:
start_time: 开始时间格式为'HH:MM:SS''HH:MM:SS,sss'(带毫秒) start_time: 开始时间格式为'HH:MM:SS''HH:MM:SS,sss'(带毫秒)
duration: 持续时间单位为秒 duration: 持续时间单位为秒
extra_seconds: 额外添加的秒数默认为1秒 extra_seconds: 额外添加的秒数默认为1秒
Returns: Returns:
str: 计算后的结束时间格式与输入格式相同 str: 计算后的结束时间格式与输入格式相同
""" """
# 检查是否包含毫秒 # 检查是否包含毫秒
has_milliseconds = ',' in start_time has_milliseconds = ',' in start_time
milliseconds = 0 milliseconds = 0
if has_milliseconds: if has_milliseconds:
time_part, ms_part = start_time.split(',') time_part, ms_part = start_time.split(',')
h, m, s = map(int, time_part.split(':')) h, m, s = map(int, time_part.split(':'))
milliseconds = int(ms_part) milliseconds = int(ms_part)
else: else:
h, m, s = map(int, start_time.split(':')) h, m, s = map(int, start_time.split(':'))
# 转换为总毫秒数 # 转换为总毫秒数
total_milliseconds = ((h * 3600 + m * 60 + s) * 1000 + milliseconds + total_milliseconds = ((h * 3600 + m * 60 + s) * 1000 + milliseconds +
int((duration + extra_seconds) * 1000)) int((duration + extra_seconds) * 1000))
# 计算新的时、分、秒、毫秒 # 计算新的时、分、秒、毫秒
ms_new = total_milliseconds % 1000 ms_new = total_milliseconds % 1000
total_seconds = total_milliseconds // 1000 total_seconds = total_milliseconds // 1000
h_new = int(total_seconds // 3600) h_new = int(total_seconds // 3600)
m_new = int((total_seconds % 3600) // 60) m_new = int((total_seconds % 3600) // 60)
s_new = int(total_seconds % 60) s_new = int(total_seconds % 60)
# 返回与输入格式一致的时间字符串 # 返回与输入格式一致的时间字符串
if has_milliseconds: if has_milliseconds:
return f"{h_new:02d}:{m_new:02d}:{s_new:02d},{ms_new:03d}" return f"{h_new:02d}:{m_new:02d}:{s_new:02d},{ms_new:03d}"
@ -75,44 +77,12 @@ def calculate_end_time(start_time: str, duration: float, extra_seconds: float =
def check_hardware_acceleration() -> Optional[str]: def check_hardware_acceleration() -> Optional[str]:
""" """
检查系统支持的硬件加速选项 检查系统支持的硬件加速选项
Returns: Returns:
Optional[str]: 硬件加速参数如果不支持则返回None Optional[str]: 硬件加速参数如果不支持则返回None
""" """
# 检查NVIDIA GPU支持 # 使用集中式硬件加速检测
try: return ffmpeg_utils.get_ffmpeg_hwaccel_type()
nvidia_check = subprocess.run(
["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if nvidia_check.returncode == 0:
return "cuda"
except Exception:
pass
# 检查MacOS videotoolbox支持
try:
videotoolbox_check = subprocess.run(
["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if videotoolbox_check.returncode == 0:
return "videotoolbox"
except Exception:
pass
# 检查Intel Quick Sync支持
try:
qsv_check = subprocess.run(
["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if qsv_check.returncode == 0:
return "qsv"
except Exception:
pass
return None
def clip_video( def clip_video(
@ -123,13 +93,13 @@ def clip_video(
) -> Dict[str, str]: ) -> Dict[str, str]:
""" """
根据时间戳裁剪视频 根据时间戳裁剪视频
Args: Args:
video_origin_path: 原始视频的路径 video_origin_path: 原始视频的路径
tts_result: 包含时间戳和持续时间信息的列表 tts_result: 包含时间戳和持续时间信息的列表
output_dir: 输出目录路径默认为None时会自动生成 output_dir: 输出目录路径默认为None时会自动生成
task_id: 任务ID用于生成唯一的输出目录默认为None时会自动生成 task_id: 任务ID用于生成唯一的输出目录默认为None时会自动生成
Returns: Returns:
Dict[str, str]: 时间戳到裁剪后视频路径的映射 Dict[str, str]: 时间戳到裁剪后视频路径的映射
""" """
@ -152,12 +122,11 @@ def clip_video(
# 确保输出目录存在 # 确保输出目录存在
Path(output_dir).mkdir(parents=True, exist_ok=True) Path(output_dir).mkdir(parents=True, exist_ok=True)
# 检查硬件加速支持 # 获取硬件加速支持
hwaccel = check_hardware_acceleration() hwaccel = check_hardware_acceleration()
hwaccel_args = [] hwaccel_args = []
if hwaccel: if hwaccel:
hwaccel_args = ["-hwaccel", hwaccel] hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
logger.info(f"使用硬件加速: {hwaccel}")
# 存储裁剪结果 # 存储裁剪结果
result = {} result = {}
@ -170,7 +139,7 @@ def clip_video(
# 根据持续时间计算真正的结束时间加上1秒余量 # 根据持续时间计算真正的结束时间加上1秒余量
duration = item["duration"] duration = item["duration"]
calculated_end_time = calculate_end_time(start_time, duration) calculated_end_time = calculate_end_time(start_time, duration)
# 转换为FFmpeg兼容的时间格式逗号替换为点 # 转换为FFmpeg兼容的时间格式逗号替换为点
ffmpeg_start_time = start_time.replace(',', '.') ffmpeg_start_time = start_time.replace(',', '.')
ffmpeg_end_time = calculated_end_time.replace(',', '.') ffmpeg_end_time = calculated_end_time.replace(',', '.')

View File

@ -14,6 +14,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip
from app.config import config from app.config import config
from app.models.schema import VideoAspect, VideoConcatMode, MaterialInfo from app.models.schema import VideoAspect, VideoConcatMode, MaterialInfo
from app.utils import utils from app.utils import utils
from app.utils import ffmpeg_utils
requested_count = 0 requested_count = 0
@ -257,10 +258,10 @@ def time_to_seconds(time_str: str) -> float:
""" """
将时间字符串转换为秒数 将时间字符串转换为秒数
支持格式: 'HH:MM:SS,mmm' (::,毫秒) 支持格式: 'HH:MM:SS,mmm' (::,毫秒)
Args: Args:
time_str: 时间字符串, "00:00:20,100" time_str: 时间字符串, "00:00:20,100"
Returns: Returns:
float: 转换后的秒数(包含毫秒) float: 转换后的秒数(包含毫秒)
""" """
@ -282,7 +283,7 @@ def time_to_seconds(time_str: str) -> float:
raise ValueError("时间格式必须为 HH:MM:SS,mmm") raise ValueError("时间格式必须为 HH:MM:SS,mmm")
return seconds + ms return seconds + ms
except ValueError as e: except ValueError as e:
logger.error(f"时间格式错误: {time_str}") logger.error(f"时间格式错误: {time_str}")
raise ValueError(f"时间格式错误: 必须为 HH:MM:SS,mmm 格式") from e raise ValueError(f"时间格式错误: 必须为 HH:MM:SS,mmm 格式") from e
@ -291,10 +292,10 @@ def time_to_seconds(time_str: str) -> float:
def format_timestamp(seconds: float) -> str: def format_timestamp(seconds: float) -> str:
""" """
将秒数转换为可读的时间格式 (HH:MM:SS,mmm) 将秒数转换为可读的时间格式 (HH:MM:SS,mmm)
Args: Args:
seconds: 秒数(可包含毫秒) seconds: 秒数(可包含毫秒)
Returns: Returns:
str: 格式化的时间字符串, "00:00:20,100" str: 格式化的时间字符串, "00:00:20,100"
""" """
@ -303,57 +304,26 @@ def format_timestamp(seconds: float) -> str:
seconds_remain = seconds % 60 seconds_remain = seconds % 60
whole_seconds = int(seconds_remain) whole_seconds = int(seconds_remain)
milliseconds = int((seconds_remain - whole_seconds) * 1000) milliseconds = int((seconds_remain - whole_seconds) * 1000)
return f"{hours:02d}:{minutes:02d}:{whole_seconds:02d},{milliseconds:03d}" return f"{hours:02d}:{minutes:02d}:{whole_seconds:02d},{milliseconds:03d}"
def _detect_hardware_acceleration() -> Optional[str]: def _detect_hardware_acceleration() -> Optional[str]:
""" """
检测系统可用的硬件加速器 检测系统可用的硬件加速器
Returns: Returns:
Optional[str]: 硬件加速参数如果不支持则返回None Optional[str]: 硬件加速参数如果不支持则返回None
""" """
# 检查NVIDIA GPU支持 # 使用集中式硬件加速检测
try: hwaccel_type = ffmpeg_utils.get_ffmpeg_hwaccel_type()
nvidia_check = subprocess.run( return hwaccel_type
["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if nvidia_check.returncode == 0:
return "cuda"
except Exception:
pass
# 检查MacOS videotoolbox支持
try:
videotoolbox_check = subprocess.run(
["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if videotoolbox_check.returncode == 0:
return "videotoolbox"
except Exception:
pass
# 检查Intel Quick Sync支持
try:
qsv_check = subprocess.run(
["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False
)
if qsv_check.returncode == 0:
return "qsv"
except Exception:
pass
return None
def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> str: def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> str:
""" """
保存剪辑后的视频 保存剪辑后的视频
Args: Args:
timestamp: 需要裁剪的时间戳,格式为 'HH:MM:SS,mmm-HH:MM:SS,mmm' timestamp: 需要裁剪的时间戳,格式为 'HH:MM:SS,mmm-HH:MM:SS,mmm'
例如: '00:00:00,000-00:00:20,100' 例如: '00:00:00,000-00:00:20,100'
@ -374,7 +344,7 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st
# 解析时间戳 # 解析时间戳
start_str, end_str = timestamp.split('-') start_str, end_str = timestamp.split('-')
# 格式化输出文件名(使用连字符替代冒号和逗号) # 格式化输出文件名(使用连字符替代冒号和逗号)
safe_start_time = start_str.replace(':', '-').replace(',', '-') safe_start_time = start_str.replace(':', '-').replace(',', '-')
safe_end_time = end_str.replace(':', '-').replace(',', '-') safe_end_time = end_str.replace(':', '-').replace(',', '-')
@ -391,48 +361,47 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st
if not os.path.exists(origin_video): if not os.path.exists(origin_video):
logger.error(f"源视频文件不存在: {origin_video}") logger.error(f"源视频文件不存在: {origin_video}")
return '' return ''
# 获取视频总时长 # 获取视频总时长
try: try:
probe_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", probe_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", origin_video] "-of", "default=noprint_wrappers=1:nokey=1", origin_video]
total_duration = float(subprocess.check_output(probe_cmd).decode('utf-8').strip()) total_duration = float(subprocess.check_output(probe_cmd).decode('utf-8').strip())
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logger.error(f"获取视频时长失败: {str(e)}") logger.error(f"获取视频时长失败: {str(e)}")
return '' return ''
# 计算时间点 # 计算时间点
start = time_to_seconds(start_str) start = time_to_seconds(start_str)
end = time_to_seconds(end_str) end = time_to_seconds(end_str)
# 验证时间段 # 验证时间段
if start >= total_duration: if start >= total_duration:
logger.warning(f"起始时间 {format_timestamp(start)} ({start:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒)") logger.warning(f"起始时间 {format_timestamp(start)} ({start:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒)")
return '' return ''
if end > total_duration: if end > total_duration:
logger.warning(f"结束时间 {format_timestamp(end)} ({end:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒),将自动调整为视频结尾") logger.warning(f"结束时间 {format_timestamp(end)} ({end:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒),将自动调整为视频结尾")
end = total_duration end = total_duration
if end <= start: if end <= start:
logger.warning(f"结束时间 {format_timestamp(end)} 必须大于起始时间 {format_timestamp(start)}") logger.warning(f"结束时间 {format_timestamp(end)} 必须大于起始时间 {format_timestamp(start)}")
return '' return ''
# 计算剪辑时长 # 计算剪辑时长
duration = end - start duration = end - start
# logger.info(f"开始剪辑视频: {format_timestamp(start)} - {format_timestamp(end)},时长 {format_timestamp(duration)}") # logger.info(f"开始剪辑视频: {format_timestamp(start)} - {format_timestamp(end)},时长 {format_timestamp(duration)}")
# 检测可用的硬件加速选项 # 获取硬件加速选项
hwaccel = _detect_hardware_acceleration() hwaccel = _detect_hardware_acceleration()
hwaccel_args = [] hwaccel_args = []
if hwaccel: if hwaccel:
hwaccel_args = ["-hwaccel", hwaccel] hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
logger.info(f"使用硬件加速: {hwaccel}")
# 转换为FFmpeg兼容的时间格式逗号替换为点 # 转换为FFmpeg兼容的时间格式逗号替换为点
ffmpeg_start_time = start_str.replace(',', '.') ffmpeg_start_time = start_str.replace(',', '.')
ffmpeg_end_time = end_str.replace(',', '.') ffmpeg_end_time = end_str.replace(',', '.')
# 构建FFmpeg命令 # 构建FFmpeg命令
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "-y", *hwaccel_args, "ffmpeg", "-y", *hwaccel_args,
@ -444,36 +413,36 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st
"-strict", "experimental", "-strict", "experimental",
video_path video_path
] ]
# 执行FFmpeg命令 # 执行FFmpeg命令
# logger.info(f"裁剪视频片段: {timestamp} -> {ffmpeg_start_time}到{ffmpeg_end_time}") # logger.info(f"裁剪视频片段: {timestamp} -> {ffmpeg_start_time}到{ffmpeg_end_time}")
# logger.debug(f"执行命令: {' '.join(ffmpeg_cmd)}") # logger.debug(f"执行命令: {' '.join(ffmpeg_cmd)}")
process = subprocess.run( process = subprocess.run(
ffmpeg_cmd, ffmpeg_cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
check=False # 不抛出异常,我们会检查返回码 check=False # 不抛出异常,我们会检查返回码
) )
# 检查是否成功 # 检查是否成功
if process.returncode != 0: if process.returncode != 0:
logger.error(f"视频剪辑失败: {process.stderr}") logger.error(f"视频剪辑失败: {process.stderr}")
if os.path.exists(video_path): if os.path.exists(video_path):
os.remove(video_path) os.remove(video_path)
return '' return ''
# 验证生成的视频文件 # 验证生成的视频文件
if os.path.exists(video_path) and os.path.getsize(video_path) > 0: if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
# 检查视频是否可播放 # 检查视频是否可播放
probe_cmd = ["ffprobe", "-v", "error", video_path] probe_cmd = ["ffprobe", "-v", "error", video_path]
validate_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) validate_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if validate_result.returncode == 0: if validate_result.returncode == 0:
logger.info(f"视频剪辑成功: {video_path}") logger.info(f"视频剪辑成功: {video_path}")
return video_path return video_path
logger.error("视频文件验证失败") logger.error("视频文件验证失败")
if os.path.exists(video_path): if os.path.exists(video_path):
os.remove(video_path) os.remove(video_path)
@ -506,14 +475,14 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, pro
saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory) saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory)
if saved_video_path: if saved_video_path:
video_paths.update({index+1:saved_video_path}) video_paths.update({index+1:saved_video_path})
# 更新进度 # 更新进度
if progress_callback: if progress_callback:
progress_callback(index + 1, total_items) progress_callback(index + 1, total_items)
except Exception as e: except Exception as e:
logger.error(f"视频裁剪失败: {utils.to_json(item)} =>\n{str(traceback.format_exc())}") logger.error(f"视频裁剪失败: {utils.to_json(item)} =>\n{str(traceback.format_exc())}")
return {} return {}
logger.success(f"裁剪 {len(video_paths)} videos") logger.success(f"裁剪 {len(video_paths)} videos")
# logger.debug(json.dumps(video_paths, indent=4, ensure_ascii=False)) # logger.debug(json.dumps(video_paths, indent=4, ensure_ascii=False))
return video_paths return video_paths

View File

@ -5,7 +5,7 @@
@Project: NarratoAI @Project: NarratoAI
@File : merger_video @File : merger_video
@Author : 小林同学 @Author : 小林同学
@Date : 2025/5/6 下午7:38 @Date : 2025/5/6 下午7:38
''' '''
import os import os
@ -15,6 +15,8 @@ from enum import Enum
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from loguru import logger from loguru import logger
from app.utils import ffmpeg_utils
class VideoAspect(Enum): class VideoAspect(Enum):
"""视频宽高比枚举""" """视频宽高比枚举"""
@ -43,7 +45,7 @@ class VideoAspect(Enum):
def check_ffmpeg_installation() -> bool: def check_ffmpeg_installation() -> bool:
""" """
检查ffmpeg是否已安装 检查ffmpeg是否已安装
Returns: Returns:
bool: 如果安装则返回True否则返回False bool: 如果安装则返回True否则返回False
""" """
@ -58,88 +60,36 @@ def check_ffmpeg_installation() -> bool:
def get_hardware_acceleration_option() -> Optional[str]: def get_hardware_acceleration_option() -> Optional[str]:
""" """
根据系统环境选择合适的硬件加速选项 根据系统环境选择合适的硬件加速选项
Returns: Returns:
Optional[str]: 硬件加速参数如果不支持则返回None Optional[str]: 硬件加速参数如果不支持则返回None
""" """
try: # 使用集中式硬件加速检测
# 检测操作系统 return ffmpeg_utils.get_ffmpeg_hwaccel_type()
is_windows = os.name == 'nt'
# 检查NVIDIA GPU支持
nvidia_check = subprocess.run(
['ffmpeg', '-hide_banner', '-hwaccels'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
output = nvidia_check.stdout.lower()
# 首先尝试获取系统信息Windows系统使用更安全的检测方法
if is_windows:
try:
# 尝试检测显卡信息
gpu_info = subprocess.run(
['wmic', 'path', 'win32_VideoController', 'get', 'name'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False
)
gpu_info_output = gpu_info.stdout.lower()
# 检测是否为AMD显卡
if 'amd' in gpu_info_output or 'radeon' in gpu_info_output:
logger.info("检测到AMD显卡为避免兼容性问题将使用软件编码")
return None
# 检测是否为集成显卡
if 'intel' in gpu_info_output and ('hd graphics' in gpu_info_output or 'uhd graphics' in gpu_info_output):
# 在Windows上Intel集成显卡可能不稳定建议使用软件编码
logger.info("检测到Intel集成显卡为避免兼容性问题将使用软件编码")
return None
except Exception as e:
logger.warning(f"获取显卡信息失败: {str(e)},将谨慎处理硬件加速")
# 根据ffmpeg支持的硬件加速器决定使用哪种
if 'cuda' in output and not is_windows:
# 在非Windows系统上使用CUDA
return 'cuda'
elif 'nvenc' in output and not is_windows:
# 在非Windows系统上使用NVENC
return 'nvenc'
elif 'qsv' in output and not (is_windows and ('amd' in gpu_info_output if 'gpu_info_output' in locals() else False)):
# 只有在非AMD系统上使用QSV
return 'qsv'
elif 'videotoolbox' in output: # macOS
return 'videotoolbox'
elif 'vaapi' in output and not is_windows: # Linux VA-API
return 'vaapi'
else:
logger.info("没有找到支持的硬件加速器或系统不兼容,将使用软件编码")
return None
except Exception as e:
logger.warning(f"检测硬件加速器时出错: {str(e)},将使用软件编码")
return None
def check_video_has_audio(video_path: str) -> bool: def check_video_has_audio(video_path: str) -> bool:
""" """
检查视频是否包含音频流 检查视频是否包含音频流
Args: Args:
video_path: 视频文件路径 video_path: 视频文件路径
Returns: Returns:
bool: 如果视频包含音频流则返回True否则返回False bool: 如果视频包含音频流则返回True否则返回False
""" """
if not os.path.exists(video_path): if not os.path.exists(video_path):
logger.warning(f"视频文件不存在: {video_path}") logger.warning(f"视频文件不存在: {video_path}")
return False return False
probe_cmd = [ probe_cmd = [
'ffprobe', '-v', 'error', 'ffprobe', '-v', 'error',
'-select_streams', 'a:0', '-select_streams', 'a:0',
'-show_entries', 'stream=codec_type', '-show_entries', 'stream=codec_type',
'-of', 'csv=p=0', '-of', 'csv=p=0',
video_path video_path
] ]
try: try:
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
return result.stdout.strip() == 'audio' return result.stdout.strip() == 'audio'
@ -151,11 +101,11 @@ def check_video_has_audio(video_path: str) -> bool:
def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) -> str: def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) -> str:
""" """
创建ffmpeg合并所需的concat文件 创建ffmpeg合并所需的concat文件
Args: Args:
video_paths: 需要合并的视频文件路径列表 video_paths: 需要合并的视频文件路径列表
concat_file_path: concat文件的输出路径 concat_file_path: concat文件的输出路径
Returns: Returns:
str: concat文件的路径 str: concat文件的路径
""" """
@ -169,10 +119,10 @@ def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) ->
else: # Unix/Mac系统 else: # Unix/Mac系统
# 转义特殊字符 # 转义特殊字符
abs_path = abs_path.replace('\\', '\\\\').replace(':', '\\:') abs_path = abs_path.replace('\\', '\\\\').replace(':', '\\:')
# 处理路径中的单引号 (如果有) # 处理路径中的单引号 (如果有)
abs_path = abs_path.replace("'", "\\'") abs_path = abs_path.replace("'", "\\'")
f.write(f"file '{abs_path}'\n") f.write(f"file '{abs_path}'\n")
return concat_file_path return concat_file_path
@ -187,7 +137,7 @@ def process_single_video(
) -> str: ) -> str:
""" """
处理单个视频调整分辨率帧率等 处理单个视频调整分辨率帧率等
Args: Args:
input_path: 输入视频路径 input_path: 输入视频路径
output_path: 输出视频路径 output_path: 输出视频路径
@ -195,7 +145,7 @@ def process_single_video(
target_height: 目标高度 target_height: 目标高度
keep_audio: 是否保留音频 keep_audio: 是否保留音频
hwaccel: 硬件加速选项 hwaccel: 硬件加速选项
Returns: Returns:
str: 处理后的视频路径 str: 处理后的视频路径
""" """
@ -212,14 +162,14 @@ def process_single_video(
try: try:
# 对视频进行快速探测,检测其基本信息 # 对视频进行快速探测,检测其基本信息
probe_cmd = [ probe_cmd = [
'ffprobe', '-v', 'error', 'ffprobe', '-v', 'error',
'-select_streams', 'v:0', '-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height', '-show_entries', 'stream=codec_name,width,height',
'-of', 'csv=p=0', '-of', 'csv=p=0',
input_path input_path
] ]
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
# 如果探测成功,使用硬件加速;否则降级到软件编码 # 如果探测成功,使用硬件加速;否则降级到软件编码
if result.returncode != 0: if result.returncode != 0:
logger.warning(f"视频探测失败,为安全起见,禁用硬件加速: {result.stderr}") logger.warning(f"视频探测失败,为安全起见,禁用硬件加速: {result.stderr}")
@ -231,15 +181,9 @@ def process_single_video(
# 添加硬件加速参数(根据前面的安全检查可能已经被禁用) # 添加硬件加速参数(根据前面的安全检查可能已经被禁用)
if hwaccel: if hwaccel:
try: try:
if hwaccel == 'cuda' or hwaccel == 'nvenc': # 使用集中式硬件加速参数
command.extend(['-hwaccel', 'cuda']) hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
elif hwaccel == 'qsv': command.extend(hwaccel_args)
command.extend(['-hwaccel', 'qsv'])
elif hwaccel == 'videotoolbox':
command.extend(['-hwaccel', 'videotoolbox'])
elif hwaccel == 'vaapi':
command.extend(['-hwaccel', 'vaapi', '-vaapi_device', '/dev/dri/renderD128'])
logger.info(f"应用硬件加速: {hwaccel}")
except Exception as e: except Exception as e:
logger.warning(f"应用硬件加速参数时出错: {str(e)},将使用软件编码") logger.warning(f"应用硬件加速参数时出错: {str(e)},将使用软件编码")
# 重置命令,移除可能添加了一半的硬件加速参数 # 重置命令,移除可能添加了一半的硬件加速参数
@ -270,7 +214,7 @@ def process_single_video(
# 选择编码器 - 考虑到Windows和特定硬件的兼容性 # 选择编码器 - 考虑到Windows和特定硬件的兼容性
use_software_encoder = True use_software_encoder = True
if hwaccel: if hwaccel:
if hwaccel == 'cuda' or hwaccel == 'nvenc': if hwaccel == 'cuda' or hwaccel == 'nvenc':
try: try:
@ -289,7 +233,7 @@ def process_single_video(
elif hwaccel == 'vaapi' and not is_windows: # Linux VA-API elif hwaccel == 'vaapi' and not is_windows: # Linux VA-API
command.extend(['-c:v', 'h264_vaapi', '-profile', '100']) command.extend(['-c:v', 'h264_vaapi', '-profile', '100'])
use_software_encoder = False use_software_encoder = False
# 如果前面的条件未能应用硬件编码器,使用软件编码 # 如果前面的条件未能应用硬件编码器,使用软件编码
if use_software_encoder: if use_software_encoder:
logger.info("使用软件编码器(libx264)") logger.info("使用软件编码器(libx264)")
@ -315,14 +259,14 @@ def process_single_video(
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e) error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error(f"处理视频失败: {error_msg}") logger.error(f"处理视频失败: {error_msg}")
# 如果使用硬件加速失败,尝试使用软件编码 # 如果使用硬件加速失败,尝试使用软件编码
if hwaccel: if hwaccel:
logger.info("尝试使用软件编码作为备选方案") logger.info("尝试使用软件编码作为备选方案")
try: try:
# 构建新的命令,使用软件编码 # 构建新的命令,使用软件编码
fallback_cmd = ['ffmpeg', '-y', '-i', input_path] fallback_cmd = ['ffmpeg', '-y', '-i', input_path]
# 保持原有的音频设置 # 保持原有的音频设置
if not keep_audio: if not keep_audio:
fallback_cmd.extend(['-an']) fallback_cmd.extend(['-an'])
@ -332,7 +276,7 @@ def process_single_video(
fallback_cmd.extend(['-c:a', 'aac', '-b:a', '128k']) fallback_cmd.extend(['-c:a', 'aac', '-b:a', '128k'])
else: else:
fallback_cmd.extend(['-an']) fallback_cmd.extend(['-an'])
# 保持原有的视频过滤器 # 保持原有的视频过滤器
fallback_cmd.extend([ fallback_cmd.extend([
'-vf', f"{scale_filter},{pad_filter}", '-vf', f"{scale_filter},{pad_filter}",
@ -346,7 +290,7 @@ def process_single_video(
'-pix_fmt', 'yuv420p', '-pix_fmt', 'yuv420p',
output_path output_path
]) ])
logger.info(f"执行备选FFmpeg命令: {' '.join(fallback_cmd)}") logger.info(f"执行备选FFmpeg命令: {' '.join(fallback_cmd)}")
subprocess.run(fallback_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(fallback_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(f"使用软件编码成功处理视频: {output_path}") logger.info(f"使用软件编码成功处理视频: {output_path}")
@ -355,7 +299,7 @@ def process_single_video(
fallback_error_msg = fallback_error.stderr.decode() if fallback_error.stderr else str(fallback_error) fallback_error_msg = fallback_error.stderr.decode() if fallback_error.stderr else str(fallback_error)
logger.error(f"备选软件编码也失败: {fallback_error_msg}") logger.error(f"备选软件编码也失败: {fallback_error_msg}")
raise RuntimeError(f"无法处理视频 {input_path}: 硬件加速和软件编码都失败") raise RuntimeError(f"无法处理视频 {input_path}: 硬件加速和软件编码都失败")
# 如果不是硬件加速导致的问题,或者备选方案也失败了,抛出原始错误 # 如果不是硬件加速导致的问题,或者备选方案也失败了,抛出原始错误
raise RuntimeError(f"处理视频失败: {error_msg}") raise RuntimeError(f"处理视频失败: {error_msg}")
@ -409,7 +353,7 @@ def combine_clip_videos(
# 重组视频路径和原声设置为一个字典列表结构 # 重组视频路径和原声设置为一个字典列表结构
video_segments = [] video_segments = []
# 检查视频路径和原声设置列表长度是否匹配 # 检查视频路径和原声设置列表长度是否匹配
if len(video_paths) != len(video_ost_list): if len(video_paths) != len(video_ost_list):
logger.warning(f"视频路径列表({len(video_paths)})和原声设置列表({len(video_ost_list)})长度不匹配") logger.warning(f"视频路径列表({len(video_paths)})和原声设置列表({len(video_ost_list)})长度不匹配")
@ -417,16 +361,16 @@ def combine_clip_videos(
min_length = min(len(video_paths), len(video_ost_list)) min_length = min(len(video_paths), len(video_ost_list))
video_paths = video_paths[:min_length] video_paths = video_paths[:min_length]
video_ost_list = video_ost_list[:min_length] video_ost_list = video_ost_list[:min_length]
# 创建视频处理配置字典列表 # 创建视频处理配置字典列表
for i, (video_path, video_ost) in enumerate(zip(video_paths, video_ost_list)): for i, (video_path, video_ost) in enumerate(zip(video_paths, video_ost_list)):
if not os.path.exists(video_path): if not os.path.exists(video_path):
logger.warning(f"视频不存在,跳过: {video_path}") logger.warning(f"视频不存在,跳过: {video_path}")
continue continue
# 检查是否有音频流 # 检查是否有音频流
has_audio = check_video_has_audio(video_path) has_audio = check_video_has_audio(video_path)
# 构建视频片段配置 # 构建视频片段配置
segment = { segment = {
"index": i, "index": i,
@ -435,11 +379,11 @@ def combine_clip_videos(
"has_audio": has_audio, "has_audio": has_audio,
"keep_audio": video_ost > 0 and has_audio # 只有当ost>0且实际有音频时才保留 "keep_audio": video_ost > 0 and has_audio # 只有当ost>0且实际有音频时才保留
} }
# 记录日志 # 记录日志
if video_ost > 0 and not has_audio: if video_ost > 0 and not has_audio:
logger.warning(f"视频 {video_path} 设置为保留原声(ost={video_ost}),但该视频没有音频流") logger.warning(f"视频 {video_path} 设置为保留原声(ost={video_ost}),但该视频没有音频流")
video_segments.append(segment) video_segments.append(segment)
# 处理每个视频片段 # 处理每个视频片段
@ -495,20 +439,20 @@ def combine_clip_videos(
if not processed_videos: if not processed_videos:
raise ValueError("没有有效的视频片段可以合并") raise ValueError("没有有效的视频片段可以合并")
# 按原始索引排序处理后的视频 # 按原始索引排序处理后的视频
processed_videos.sort(key=lambda x: x["index"]) processed_videos.sort(key=lambda x: x["index"])
# 第二阶段:分步骤合并视频 - 避免复杂的filter_complex滤镜 # 第二阶段:分步骤合并视频 - 避免复杂的filter_complex滤镜
try: try:
# 1. 首先,将所有没有音频的视频或音频被禁用的视频合并到一个临时文件中 # 1. 首先,将所有没有音频的视频或音频被禁用的视频合并到一个临时文件中
video_paths_only = [video["path"] for video in processed_videos] video_paths_only = [video["path"] for video in processed_videos]
video_concat_path = os.path.join(temp_dir, "video_concat.mp4") video_concat_path = os.path.join(temp_dir, "video_concat.mp4")
# 创建concat文件用于合并视频流 # 创建concat文件用于合并视频流
concat_file = os.path.join(temp_dir, "concat_list.txt") concat_file = os.path.join(temp_dir, "concat_list.txt")
create_ffmpeg_concat_file(video_paths_only, concat_file) create_ffmpeg_concat_file(video_paths_only, concat_file)
# 合并所有视频流,但不包含音频 # 合并所有视频流,但不包含音频
concat_cmd = [ concat_cmd = [
'ffmpeg', '-y', 'ffmpeg', '-y',
@ -522,19 +466,19 @@ def combine_clip_videos(
'-threads', str(threads), '-threads', str(threads),
video_concat_path video_concat_path
] ]
subprocess.run(concat_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(concat_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("视频流合并完成") logger.info("视频流合并完成")
# 2. 提取并合并有音频的片段 # 2. 提取并合并有音频的片段
audio_segments = [video for video in processed_videos if video["keep_audio"]] audio_segments = [video for video in processed_videos if video["keep_audio"]]
if not audio_segments: if not audio_segments:
# 如果没有音频片段,直接使用无音频的合并视频作为最终结果 # 如果没有音频片段,直接使用无音频的合并视频作为最终结果
shutil.copy(video_concat_path, output_video_path) shutil.copy(video_concat_path, output_video_path)
logger.info("无音频视频合并完成") logger.info("无音频视频合并完成")
return output_video_path return output_video_path
# 创建音频中间文件 # 创建音频中间文件
audio_files = [] audio_files = []
for i, segment in enumerate(audio_segments): for i, segment in enumerate(audio_segments):
@ -554,11 +498,11 @@ def combine_clip_videos(
"path": audio_file "path": audio_file
}) })
logger.info(f"提取音频 {i+1}/{len(audio_segments)} 完成") logger.info(f"提取音频 {i+1}/{len(audio_segments)} 完成")
# 3. 计算每个音频片段的时间位置 # 3. 计算每个音频片段的时间位置
audio_timings = [] audio_timings = []
current_time = 0.0 current_time = 0.0
# 获取每个视频片段的时长 # 获取每个视频片段的时长
for i, video in enumerate(processed_videos): for i, video in enumerate(processed_videos):
duration_cmd = [ duration_cmd = [
@ -569,7 +513,7 @@ def combine_clip_videos(
] ]
result = subprocess.run(duration_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) result = subprocess.run(duration_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
duration = float(result.stdout.strip()) duration = float(result.stdout.strip())
# 如果当前片段需要保留音频,记录时间位置 # 如果当前片段需要保留音频,记录时间位置
if video["keep_audio"]: if video["keep_audio"]:
for audio in audio_files: for audio in audio_files:
@ -580,9 +524,9 @@ def combine_clip_videos(
"index": video["index"] "index": video["index"]
}) })
break break
current_time += duration current_time += duration
# 4. 创建静音音频轨道作为基础 # 4. 创建静音音频轨道作为基础
silence_audio = os.path.join(temp_dir, "silence.aac") silence_audio = os.path.join(temp_dir, "silence.aac")
create_silence_cmd = [ create_silence_cmd = [
@ -595,28 +539,28 @@ def combine_clip_videos(
silence_audio silence_audio
] ]
subprocess.run(create_silence_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(create_silence_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 5. 创建复杂滤镜命令以混合音频 # 5. 创建复杂滤镜命令以混合音频
filter_script = os.path.join(temp_dir, "filter_script.txt") filter_script = os.path.join(temp_dir, "filter_script.txt")
with open(filter_script, 'w') as f: with open(filter_script, 'w') as f:
f.write(f"[0:a]volume=0.0[silence];\n") # 首先静音背景轨道 f.write(f"[0:a]volume=0.0[silence];\n") # 首先静音背景轨道
# 添加每个音频文件 # 添加每个音频文件
for i, timing in enumerate(audio_timings): for i, timing in enumerate(audio_timings):
f.write(f"[{i+1}:a]adelay={int(timing['start']*1000)}|{int(timing['start']*1000)}[a{i}];\n") f.write(f"[{i+1}:a]adelay={int(timing['start']*1000)}|{int(timing['start']*1000)}[a{i}];\n")
# 混合所有音频 # 混合所有音频
mix_str = "[silence]" mix_str = "[silence]"
for i in range(len(audio_timings)): for i in range(len(audio_timings)):
mix_str += f"[a{i}]" mix_str += f"[a{i}]"
mix_str += f"amix=inputs={len(audio_timings)+1}:duration=longest[aout]" mix_str += f"amix=inputs={len(audio_timings)+1}:duration=longest[aout]"
f.write(mix_str) f.write(mix_str)
# 6. 构建音频合并命令 # 6. 构建音频合并命令
audio_inputs = ['-i', silence_audio] audio_inputs = ['-i', silence_audio]
for timing in audio_timings: for timing in audio_timings:
audio_inputs.extend(['-i', timing["file"]]) audio_inputs.extend(['-i', timing["file"]])
mixed_audio = os.path.join(temp_dir, "mixed_audio.aac") mixed_audio = os.path.join(temp_dir, "mixed_audio.aac")
audio_mix_cmd = [ audio_mix_cmd = [
'ffmpeg', '-y' 'ffmpeg', '-y'
@ -627,10 +571,10 @@ def combine_clip_videos(
'-b:a', '128k', '-b:a', '128k',
mixed_audio mixed_audio
] ]
subprocess.run(audio_mix_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(audio_mix_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("音频混合完成") logger.info("音频混合完成")
# 7. 将合并的视频和混合的音频组合在一起 # 7. 将合并的视频和混合的音频组合在一起
final_cmd = [ final_cmd = [
'ffmpeg', '-y', 'ffmpeg', '-y',
@ -643,22 +587,22 @@ def combine_clip_videos(
'-shortest', '-shortest',
output_video_path output_video_path
] ]
subprocess.run(final_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(final_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("视频最终合并完成") logger.info("视频最终合并完成")
return output_video_path return output_video_path
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logger.error(f"合并视频过程中出错: {e.stderr.decode() if e.stderr else str(e)}") logger.error(f"合并视频过程中出错: {e.stderr.decode() if e.stderr else str(e)}")
# 尝试备用合并方法 - 最简单的无音频合并 # 尝试备用合并方法 - 最简单的无音频合并
logger.info("尝试备用合并方法 - 无音频合并") logger.info("尝试备用合并方法 - 无音频合并")
try: try:
concat_file = os.path.join(temp_dir, "concat_list.txt") concat_file = os.path.join(temp_dir, "concat_list.txt")
video_paths_only = [video["path"] for video in processed_videos] video_paths_only = [video["path"] for video in processed_videos]
create_ffmpeg_concat_file(video_paths_only, concat_file) create_ffmpeg_concat_file(video_paths_only, concat_file)
backup_cmd = [ backup_cmd = [
'ffmpeg', '-y', 'ffmpeg', '-y',
'-f', 'concat', '-f', 'concat',
@ -668,14 +612,14 @@ def combine_clip_videos(
'-an', # 无音频 '-an', # 无音频
output_video_path output_video_path
] ]
subprocess.run(backup_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(backup_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.warning("使用备用方法(无音频)成功合并视频") logger.warning("使用备用方法(无音频)成功合并视频")
return output_video_path return output_video_path
except Exception as backup_error: except Exception as backup_error:
logger.error(f"备用合并方法也失败: {str(backup_error)}") logger.error(f"备用合并方法也失败: {str(backup_error)}")
raise RuntimeError(f"无法合并视频: {str(backup_error)}") raise RuntimeError(f"无法合并视频: {str(backup_error)}")
except Exception as e: except Exception as e:
logger.error(f"合并视频时出错: {str(e)}") logger.error(f"合并视频时出错: {str(e)}")
raise raise

419
app/utils/ffmpeg_utils.py Normal file
View File

@ -0,0 +1,419 @@
"""
FFmpeg 工具模块 - 提供 FFmpeg 相关的工具函数特别是硬件加速检测
"""
import os
import platform
import subprocess
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
}
def check_ffmpeg_installation() -> bool:
"""
检查ffmpeg是否已安装
Returns:
bool: 如果安装则返回True否则返回False
"""
try:
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_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
# 检测操作系统
system = platform.system().lower()
logger.debug(f"检测硬件加速 - 操作系统: {system}")
# 获取FFmpeg支持的硬件加速器列表
try:
hwaccels_cmd = subprocess.run(
['ffmpeg', '-hide_banner', '-hwaccels'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
supported_hwaccels = hwaccels_cmd.stdout.lower()
except Exception as e:
logger.error(f"获取FFmpeg硬件加速器列表失败: {str(e)}")
supported_hwaccels = ""
# 根据操作系统检测不同的硬件加速器
if system == 'darwin': # macOS
_detect_macos_acceleration(supported_hwaccels)
elif system == 'windows': # Windows
_detect_windows_acceleration(supported_hwaccels)
elif system == 'linux': # Linux
_detect_linux_acceleration(supported_hwaccels)
else:
logger.warning(f"不支持的操作系统: {system}")
_FFMPEG_HW_ACCEL_INFO["message"] = f"不支持的操作系统: {system}"
# 记录检测结果已经在启动时输出,这里不再重复输出
return _FFMPEG_HW_ACCEL_INFO
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系统的硬件加速
Args:
supported_hwaccels: FFmpeg支持的硬件加速器列表
"""
global _FFMPEG_HW_ACCEL_INFO
# 在Windows上首先检查显卡信息
gpu_info = _get_windows_gpu_info()
# 检查是否为AMD显卡
if 'amd' in gpu_info.lower() or 'radeon' in gpu_info.lower():
logger.info("检测到AMD显卡为避免兼容性问题将使用软件编码")
_FFMPEG_HW_ACCEL_INFO["message"] = "检测到AMD显卡为避免兼容性问题将使用软件编码"
return
# 检查是否为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
# 检测NVIDIA CUDA支持
if 'cuda' in supported_hwaccels and 'nvidia' in gpu_info.lower():
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)}")
# 检测Intel QSV支持如果是Intel显卡
if 'qsv' in supported_hwaccels and 'intel' in gpu_info.lower():
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"] = not is_intel_integrated
return
except Exception as e:
logger.debug(f"测试QSV失败: {str(e)}")
# 检测D3D11VA支持
if 'd3d11va' in supported_hwaccels:
try:
test_cmd = subprocess.run(
["ffmpeg", "-hwaccel", "d3d11va", "-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"] = "d3d11va"
_FFMPEG_HW_ACCEL_INFO["encoder"] = "h264" # D3D11VA只用于解码编码仍使用软件编码器
_FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "d3d11va"]
_FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated
return
except Exception as e:
logger.debug(f"测试D3D11VA失败: {str(e)}")
# 检测DXVA2支持
if 'dxva2' in supported_hwaccels:
try:
test_cmd = subprocess.run(
["ffmpeg", "-hwaccel", "dxva2", "-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"] = "dxva2"
_FFMPEG_HW_ACCEL_INFO["encoder"] = "h264" # DXVA2只用于解码编码仍使用软件编码器
_FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "dxva2"]
_FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated
return
except Exception as e:
logger.debug(f"测试DXVA2失败: {str(e)}")
_FFMPEG_HW_ACCEL_INFO["message"] = f"Windows系统未检测到可用的硬件加速显卡信息: {gpu_info}"
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:
gpu_info = subprocess.run(
['wmic', 'path', 'win32_VideoController', 'get', 'name'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False
)
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"]

View File

@ -19,6 +19,8 @@ from typing import List, Dict
from loguru import logger from loguru import logger
from tqdm import tqdm from tqdm import tqdm
from app.utils import ffmpeg_utils
class VideoProcessor: class VideoProcessor:
def __init__(self, video_path: str): def __init__(self, video_path: str):
@ -63,7 +65,7 @@ class VideoProcessor:
if '=' in line: if '=' in line:
key, value = line.split('=', 1) key, value = line.split('=', 1)
info[key] = value info[key] = value
# 处理帧率(可能是分数形式) # 处理帧率(可能是分数形式)
if 'r_frame_rate' in info: if 'r_frame_rate' in info:
try: try:
@ -71,9 +73,9 @@ class VideoProcessor:
info['fps'] = str(num / den) info['fps'] = str(num / den)
except ValueError: except ValueError:
info['fps'] = info.get('r_frame_rate', '25') info['fps'] = info.get('r_frame_rate', '25')
return info return info
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logger.error(f"获取视频信息失败: {e.stderr}") logger.error(f"获取视频信息失败: {e.stderr}")
return { return {
@ -83,7 +85,7 @@ class VideoProcessor:
'duration': '0' 'duration': '0'
} }
def extract_frames_by_interval(self, output_dir: str, interval_seconds: float = 5.0, def extract_frames_by_interval(self, output_dir: str, interval_seconds: float = 5.0,
use_hw_accel: bool = True) -> List[int]: use_hw_accel: bool = True) -> List[int]:
""" """
按指定时间间隔提取视频帧 按指定时间间隔提取视频帧
@ -98,57 +100,51 @@ class VideoProcessor:
""" """
if not os.path.exists(output_dir): if not os.path.exists(output_dir):
os.makedirs(output_dir) os.makedirs(output_dir)
# 计算起始时间和帧提取点 # 计算起始时间和帧提取点
start_time = 0 start_time = 0
end_time = self.duration end_time = self.duration
extraction_times = [] extraction_times = []
current_time = start_time current_time = start_time
while current_time < end_time: while current_time < end_time:
extraction_times.append(current_time) extraction_times.append(current_time)
current_time += interval_seconds current_time += interval_seconds
if not extraction_times: if not extraction_times:
logger.warning("未找到需要提取的帧") logger.warning("未找到需要提取的帧")
return [] return []
# 确定硬件加速器选项 # 确定硬件加速器选项
hw_accel = [] hw_accel = []
if use_hw_accel: if use_hw_accel and ffmpeg_utils.is_ffmpeg_hwaccel_available():
# 尝试检测可用的硬件加速器 hw_accel = ffmpeg_utils.get_ffmpeg_hwaccel_args()
hw_accel_options = self._detect_hw_accelerator()
if hw_accel_options:
hw_accel = hw_accel_options
logger.info(f"使用硬件加速: {' '.join(hw_accel)}")
else:
logger.warning("未检测到可用的硬件加速器,使用软件解码")
# 提取帧 # 提取帧
frame_numbers = [] frame_numbers = []
for i, timestamp in enumerate(tqdm(extraction_times, desc="提取视频帧")): for i, timestamp in enumerate(tqdm(extraction_times, desc="提取视频帧")):
frame_number = int(timestamp * self.fps) frame_number = int(timestamp * self.fps)
frame_numbers.append(frame_number) frame_numbers.append(frame_number)
# 格式化时间戳字符串 (HHMMSSmmm) # 格式化时间戳字符串 (HHMMSSmmm)
hours = int(timestamp // 3600) hours = int(timestamp // 3600)
minutes = int((timestamp % 3600) // 60) minutes = int((timestamp % 3600) // 60)
seconds = int(timestamp % 60) seconds = int(timestamp % 60)
milliseconds = int((timestamp % 1) * 1000) milliseconds = int((timestamp % 1) * 1000)
time_str = f"{hours:02d}{minutes:02d}{seconds:02d}{milliseconds:03d}" time_str = f"{hours:02d}{minutes:02d}{seconds:02d}{milliseconds:03d}"
output_path = os.path.join(output_dir, f"keyframe_{frame_number:06d}_{time_str}.jpg") output_path = os.path.join(output_dir, f"keyframe_{frame_number:06d}_{time_str}.jpg")
# 使用ffmpeg提取单帧 # 使用ffmpeg提取单帧
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
"-loglevel", "error", "-loglevel", "error",
] ]
# 添加硬件加速参数 # 添加硬件加速参数
cmd.extend(hw_accel) cmd.extend(hw_accel)
cmd.extend([ cmd.extend([
"-ss", str(timestamp), "-ss", str(timestamp),
"-i", self.video_path, "-i", self.video_path,
@ -157,12 +153,12 @@ class VideoProcessor:
"-y", "-y",
output_path output_path
]) ])
try: try:
subprocess.run(cmd, check=True, capture_output=True) subprocess.run(cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logger.warning(f"提取帧 {frame_number} 失败: {e.stderr}") logger.warning(f"提取帧 {frame_number} 失败: {e.stderr}")
logger.info(f"成功提取了 {len(frame_numbers)} 个视频帧") logger.info(f"成功提取了 {len(frame_numbers)} 个视频帧")
return frame_numbers return frame_numbers
@ -173,119 +169,9 @@ class VideoProcessor:
Returns: Returns:
List[str]: 硬件加速器ffmpeg命令参数 List[str]: 硬件加速器ffmpeg命令参数
""" """
# 检测操作系统 # 使用集中式硬件加速检测
import platform if ffmpeg_utils.is_ffmpeg_hwaccel_available():
system = platform.system().lower() return ffmpeg_utils.get_ffmpeg_hwaccel_args()
# 测试不同的硬件加速器
accelerators = []
if system == 'darwin': # macOS
# 测试 videotoolbox (Apple 硬件加速)
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "videotoolbox",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "videotoolbox"]
except subprocess.CalledProcessError:
pass
elif system == 'linux':
# 测试 VAAPI
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "vaapi",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "vaapi"]
except subprocess.CalledProcessError:
pass
# 尝试 CUDA
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "cuda",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "cuda"]
except subprocess.CalledProcessError:
pass
elif system == 'windows':
# 测试 CUDA
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "cuda",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "cuda"]
except subprocess.CalledProcessError:
pass
# 测试 D3D11VA
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "d3d11va",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "d3d11va"]
except subprocess.CalledProcessError:
pass
# 测试 DXVA2
test_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-hwaccel", "dxva2",
"-i", self.video_path,
"-t", "0.1",
"-f", "null",
"-"
]
try:
subprocess.run(test_cmd, capture_output=True, check=True)
return ["-hwaccel", "dxva2"]
except subprocess.CalledProcessError:
pass
# 如果没有找到可用的硬件加速器
return [] return []
def process_video_pipeline(self, def process_video_pipeline(self,
@ -294,7 +180,7 @@ class VideoProcessor:
use_hw_accel: bool = True) -> None: use_hw_accel: bool = True) -> None:
""" """
执行简化的视频处理流程直接从原视频按固定时间间隔提取帧 执行简化的视频处理流程直接从原视频按固定时间间隔提取帧
Args: Args:
output_dir: 输出目录 output_dir: 输出目录
interval_seconds: 帧提取间隔 interval_seconds: 帧提取间隔
@ -302,7 +188,7 @@ class VideoProcessor:
""" """
# 创建输出目录 # 创建输出目录
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
try: try:
# 直接从原视频提取关键帧 # 直接从原视频提取关键帧
logger.info(f"从视频间隔 {interval_seconds} 秒提取关键帧...") logger.info(f"从视频间隔 {interval_seconds} 秒提取关键帧...")
@ -311,7 +197,7 @@ class VideoProcessor:
interval_seconds=interval_seconds, interval_seconds=interval_seconds,
use_hw_accel=use_hw_accel use_hw_accel=use_hw_accel
) )
logger.info(f"处理完成!视频帧已保存在: {output_dir}") logger.info(f"处理完成!视频帧已保存在: {output_dir}")
except Exception as e: except Exception as e:
@ -324,16 +210,16 @@ if __name__ == "__main__":
import time import time
start_time = time.time() start_time = time.time()
# 使用示例 # 使用示例
processor = VideoProcessor("./resource/videos/test.mp4") processor = VideoProcessor("./resource/videos/test.mp4")
# 设置间隔为3秒提取帧 # 设置间隔为3秒提取帧
processor.process_video_pipeline( processor.process_video_pipeline(
output_dir="output", output_dir="output",
interval_seconds=3.0, interval_seconds=3.0,
use_hw_accel=True use_hw_accel=True
) )
end_time = time.time() end_time = time.time()
print(f"处理完成!总耗时: {end_time - start_time:.2f}") print(f"处理完成!总耗时: {end_time - start_time:.2f}")

View File

@ -1,5 +1,5 @@
[app] [app]
project_version="0.6.1" project_version="0.6.2"
# 支持视频理解的大模型提供商 # 支持视频理解的大模型提供商
# gemini (谷歌, 需要 VPN) # gemini (谷歌, 需要 VPN)
# siliconflow (硅基流动) # siliconflow (硅基流动)

View File

@ -7,6 +7,7 @@ from webui.components import basic_settings, video_settings, audio_settings, sub
review_settings, merge_settings, system_settings review_settings, merge_settings, system_settings
# from webui.utils import cache, file_utils # from webui.utils import cache, file_utils
from app.utils import utils from app.utils import utils
from app.utils import ffmpeg_utils
from app.models.schema import VideoClipParams, VideoAspect from app.models.schema import VideoClipParams, VideoAspect
@ -64,7 +65,7 @@ def init_log():
try: try:
for handler_id in logger._core.handlers: for handler_id in logger._core.handlers:
logger.remove(handler_id) logger.remove(handler_id)
# 重新添加带有高级过滤的处理器 # 重新添加带有高级过滤的处理器
def advanced_filter(record): def advanced_filter(record):
"""更复杂的过滤器,在应用启动后安全使用""" """更复杂的过滤器,在应用启动后安全使用"""
@ -74,7 +75,7 @@ def init_log():
"CUDA initialization" "CUDA initialization"
] ]
return not any(msg in record["message"] for msg in ignore_messages) return not any(msg in record["message"] for msg in ignore_messages)
logger.add( logger.add(
sys.stdout, sys.stdout,
level=_lvl, level=_lvl,
@ -91,7 +92,7 @@ def init_log():
colorize=True colorize=True
) )
logger.error(f"设置高级日志过滤器失败: {e}") logger.error(f"设置高级日志过滤器失败: {e}")
# 将高级过滤器设置放到启动主逻辑后 # 将高级过滤器设置放到启动主逻辑后
import threading import threading
threading.Timer(5.0, setup_advanced_filters).start() threading.Timer(5.0, setup_advanced_filters).start()
@ -192,7 +193,14 @@ def main():
"""主函数""" """主函数"""
init_log() init_log()
init_global_state() init_global_state()
# 检测FFmpeg硬件加速
hwaccel_info = ffmpeg_utils.detect_hardware_acceleration()
if hwaccel_info["available"]:
logger.info(f"FFmpeg硬件加速检测结果: 可用 | 类型: {hwaccel_info['type']} | 编码器: {hwaccel_info['encoder']} | 独立显卡: {hwaccel_info['is_dedicated_gpu']} | 参数: {hwaccel_info['hwaccel_args']}")
else:
logger.warning(f"FFmpeg硬件加速不可用: {hwaccel_info['message']}, 将使用CPU软件编码")
# 仅初始化基本资源避免过早地加载依赖PyTorch的资源 # 仅初始化基本资源避免过早地加载依赖PyTorch的资源
# 检查是否能分解utils.init_resources()为基本资源和高级资源(如依赖PyTorch的资源) # 检查是否能分解utils.init_resources()为基本资源和高级资源(如依赖PyTorch的资源)
try: try:
@ -218,15 +226,15 @@ def main():
audio_settings.render_audio_panel(tr) audio_settings.render_audio_panel(tr)
with panel[2]: with panel[2]:
subtitle_settings.render_subtitle_panel(tr) subtitle_settings.render_subtitle_panel(tr)
# 渲染视频审查面板 # 渲染视频审查面板
review_settings.render_review_panel(tr) review_settings.render_review_panel(tr)
# 放到最后渲染可能使用PyTorch的部分 # 放到最后渲染可能使用PyTorch的部分
# 渲染系统设置面板 # 渲染系统设置面板
with panel[2]: with panel[2]:
system_settings.render_system_panel(tr) system_settings.render_system_panel(tr)
# 放到最后渲染生成按钮和处理逻辑 # 放到最后渲染生成按钮和处理逻辑
render_generate_button() render_generate_button()