优化 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.router import root_api_router
from app.utils import utils
from app.utils import ffmpeg_utils
def exception_handler(request: Request, e: HttpException):
@ -80,3 +81,10 @@ def shutdown_event():
@app.on_event("startup")
def 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
@File : clip_video
@Author : 小林同学
@Date : 2025/5/6 下午6:14
@Date : 2025/5/6 下午6:14
'''
import os
@ -16,14 +16,16 @@ from loguru import logger
from typing import Dict, List, Optional
from pathlib import Path
from app.utils import ffmpeg_utils
def parse_timestamp(timestamp: str) -> tuple:
"""
解析时间戳字符串返回开始和结束时间
Args:
timestamp: 格式为'HH:MM:SS-HH:MM:SS''HH:MM:SS,sss-HH:MM:SS,sss'的时间戳字符串
Returns:
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:
"""
根据开始时间和持续时间计算结束时间
Args:
start_time: 开始时间格式为'HH:MM:SS''HH:MM:SS,sss'(带毫秒)
duration: 持续时间单位为秒
extra_seconds: 额外添加的秒数默认为1秒
Returns:
str: 计算后的结束时间格式与输入格式相同
"""
# 检查是否包含毫秒
has_milliseconds = ',' in start_time
milliseconds = 0
if has_milliseconds:
time_part, ms_part = start_time.split(',')
h, m, s = map(int, time_part.split(':'))
milliseconds = int(ms_part)
else:
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))
# 计算新的时、分、秒、毫秒
ms_new = total_milliseconds % 1000
total_seconds = total_milliseconds // 1000
h_new = int(total_seconds // 3600)
m_new = int((total_seconds % 3600) // 60)
s_new = int(total_seconds % 60)
# 返回与输入格式一致的时间字符串
if has_milliseconds:
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]:
"""
检查系统支持的硬件加速选项
Returns:
Optional[str]: 硬件加速参数如果不支持则返回None
"""
# 检查NVIDIA GPU支持
try:
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
# 使用集中式硬件加速检测
return ffmpeg_utils.get_ffmpeg_hwaccel_type()
def clip_video(
@ -123,13 +93,13 @@ def clip_video(
) -> Dict[str, str]:
"""
根据时间戳裁剪视频
Args:
video_origin_path: 原始视频的路径
tts_result: 包含时间戳和持续时间信息的列表
output_dir: 输出目录路径默认为None时会自动生成
task_id: 任务ID用于生成唯一的输出目录默认为None时会自动生成
Returns:
Dict[str, str]: 时间戳到裁剪后视频路径的映射
"""
@ -152,12 +122,11 @@ def clip_video(
# 确保输出目录存在
Path(output_dir).mkdir(parents=True, exist_ok=True)
# 检查硬件加速支持
# 获取硬件加速支持
hwaccel = check_hardware_acceleration()
hwaccel_args = []
if hwaccel:
hwaccel_args = ["-hwaccel", hwaccel]
logger.info(f"使用硬件加速: {hwaccel}")
hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
# 存储裁剪结果
result = {}
@ -170,7 +139,7 @@ def clip_video(
# 根据持续时间计算真正的结束时间加上1秒余量
duration = item["duration"]
calculated_end_time = calculate_end_time(start_time, duration)
# 转换为FFmpeg兼容的时间格式逗号替换为点
ffmpeg_start_time = start_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.models.schema import VideoAspect, VideoConcatMode, MaterialInfo
from app.utils import utils
from app.utils import ffmpeg_utils
requested_count = 0
@ -257,10 +258,10 @@ def time_to_seconds(time_str: str) -> float:
"""
将时间字符串转换为秒数
支持格式: 'HH:MM:SS,mmm' (::,毫秒)
Args:
time_str: 时间字符串, "00:00:20,100"
Returns:
float: 转换后的秒数(包含毫秒)
"""
@ -282,7 +283,7 @@ def time_to_seconds(time_str: str) -> float:
raise ValueError("时间格式必须为 HH:MM:SS,mmm")
return seconds + ms
except ValueError as e:
logger.error(f"时间格式错误: {time_str}")
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:
"""
将秒数转换为可读的时间格式 (HH:MM:SS,mmm)
Args:
seconds: 秒数(可包含毫秒)
Returns:
str: 格式化的时间字符串, "00:00:20,100"
"""
@ -303,57 +304,26 @@ def format_timestamp(seconds: float) -> str:
seconds_remain = seconds % 60
whole_seconds = int(seconds_remain)
milliseconds = int((seconds_remain - whole_seconds) * 1000)
return f"{hours:02d}:{minutes:02d}:{whole_seconds:02d},{milliseconds:03d}"
def _detect_hardware_acceleration() -> Optional[str]:
"""
检测系统可用的硬件加速器
Returns:
Optional[str]: 硬件加速参数如果不支持则返回None
"""
# 检查NVIDIA GPU支持
try:
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
# 使用集中式硬件加速检测
hwaccel_type = ffmpeg_utils.get_ffmpeg_hwaccel_type()
return hwaccel_type
def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> str:
"""
保存剪辑后的视频
Args:
timestamp: 需要裁剪的时间戳,格式为 'HH:MM:SS,mmm-HH:MM:SS,mmm'
例如: '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('-')
# 格式化输出文件名(使用连字符替代冒号和逗号)
safe_start_time = start_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):
logger.error(f"源视频文件不存在: {origin_video}")
return ''
# 获取视频总时长
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]
total_duration = float(subprocess.check_output(probe_cmd).decode('utf-8').strip())
except subprocess.CalledProcessError as e:
logger.error(f"获取视频时长失败: {str(e)}")
return ''
# 计算时间点
start = time_to_seconds(start_str)
end = time_to_seconds(end_str)
# 验证时间段
if start >= total_duration:
logger.warning(f"起始时间 {format_timestamp(start)} ({start:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒)")
return ''
if end > total_duration:
logger.warning(f"结束时间 {format_timestamp(end)} ({end:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒),将自动调整为视频结尾")
end = total_duration
if end <= start:
logger.warning(f"结束时间 {format_timestamp(end)} 必须大于起始时间 {format_timestamp(start)}")
return ''
# 计算剪辑时长
duration = end - start
# logger.info(f"开始剪辑视频: {format_timestamp(start)} - {format_timestamp(end)},时长 {format_timestamp(duration)}")
# 检测可用的硬件加速选项
# 获取硬件加速选项
hwaccel = _detect_hardware_acceleration()
hwaccel_args = []
if hwaccel:
hwaccel_args = ["-hwaccel", hwaccel]
logger.info(f"使用硬件加速: {hwaccel}")
hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
# 转换为FFmpeg兼容的时间格式逗号替换为点
ffmpeg_start_time = start_str.replace(',', '.')
ffmpeg_end_time = end_str.replace(',', '.')
# 构建FFmpeg命令
ffmpeg_cmd = [
"ffmpeg", "-y", *hwaccel_args,
@ -444,36 +413,36 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st
"-strict", "experimental",
video_path
]
# 执行FFmpeg命令
# logger.info(f"裁剪视频片段: {timestamp} -> {ffmpeg_start_time}到{ffmpeg_end_time}")
# logger.debug(f"执行命令: {' '.join(ffmpeg_cmd)}")
process = subprocess.run(
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False # 不抛出异常,我们会检查返回码
)
# 检查是否成功
if process.returncode != 0:
logger.error(f"视频剪辑失败: {process.stderr}")
if os.path.exists(video_path):
os.remove(video_path)
return ''
# 验证生成的视频文件
if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
# 检查视频是否可播放
probe_cmd = ["ffprobe", "-v", "error", video_path]
validate_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if validate_result.returncode == 0:
logger.info(f"视频剪辑成功: {video_path}")
return video_path
logger.error("视频文件验证失败")
if os.path.exists(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)
if saved_video_path:
video_paths.update({index+1:saved_video_path})
# 更新进度
if progress_callback:
progress_callback(index + 1, total_items)
except Exception as e:
logger.error(f"视频裁剪失败: {utils.to_json(item)} =>\n{str(traceback.format_exc())}")
return {}
logger.success(f"裁剪 {len(video_paths)} videos")
# logger.debug(json.dumps(video_paths, indent=4, ensure_ascii=False))
return video_paths

View File

@ -5,7 +5,7 @@
@Project: NarratoAI
@File : merger_video
@Author : 小林同学
@Date : 2025/5/6 下午7:38
@Date : 2025/5/6 下午7:38
'''
import os
@ -15,6 +15,8 @@ from enum import Enum
from typing import List, Optional, Tuple
from loguru import logger
from app.utils import ffmpeg_utils
class VideoAspect(Enum):
"""视频宽高比枚举"""
@ -43,7 +45,7 @@ class VideoAspect(Enum):
def check_ffmpeg_installation() -> bool:
"""
检查ffmpeg是否已安装
Returns:
bool: 如果安装则返回True否则返回False
"""
@ -58,88 +60,36 @@ def check_ffmpeg_installation() -> bool:
def get_hardware_acceleration_option() -> Optional[str]:
"""
根据系统环境选择合适的硬件加速选项
Returns:
Optional[str]: 硬件加速参数如果不支持则返回None
"""
try:
# 检测操作系统
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
# 使用集中式硬件加速检测
return ffmpeg_utils.get_ffmpeg_hwaccel_type()
def check_video_has_audio(video_path: str) -> bool:
"""
检查视频是否包含音频流
Args:
video_path: 视频文件路径
Returns:
bool: 如果视频包含音频流则返回True否则返回False
"""
if not os.path.exists(video_path):
logger.warning(f"视频文件不存在: {video_path}")
return False
probe_cmd = [
'ffprobe', '-v', 'error',
'-select_streams', 'a:0',
'-show_entries', 'stream=codec_type',
'-of', 'csv=p=0',
'ffprobe', '-v', 'error',
'-select_streams', 'a:0',
'-show_entries', 'stream=codec_type',
'-of', 'csv=p=0',
video_path
]
try:
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
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:
"""
创建ffmpeg合并所需的concat文件
Args:
video_paths: 需要合并的视频文件路径列表
concat_file_path: concat文件的输出路径
Returns:
str: concat文件的路径
"""
@ -169,10 +119,10 @@ def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) ->
else: # Unix/Mac系统
# 转义特殊字符
abs_path = abs_path.replace('\\', '\\\\').replace(':', '\\:')
# 处理路径中的单引号 (如果有)
abs_path = abs_path.replace("'", "\\'")
f.write(f"file '{abs_path}'\n")
return concat_file_path
@ -187,7 +137,7 @@ def process_single_video(
) -> str:
"""
处理单个视频调整分辨率帧率等
Args:
input_path: 输入视频路径
output_path: 输出视频路径
@ -195,7 +145,7 @@ def process_single_video(
target_height: 目标高度
keep_audio: 是否保留音频
hwaccel: 硬件加速选项
Returns:
str: 处理后的视频路径
"""
@ -212,14 +162,14 @@ def process_single_video(
try:
# 对视频进行快速探测,检测其基本信息
probe_cmd = [
'ffprobe', '-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height',
'-of', 'csv=p=0',
'ffprobe', '-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height',
'-of', 'csv=p=0',
input_path
]
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
# 如果探测成功,使用硬件加速;否则降级到软件编码
if result.returncode != 0:
logger.warning(f"视频探测失败,为安全起见,禁用硬件加速: {result.stderr}")
@ -231,15 +181,9 @@ def process_single_video(
# 添加硬件加速参数(根据前面的安全检查可能已经被禁用)
if hwaccel:
try:
if hwaccel == 'cuda' or hwaccel == 'nvenc':
command.extend(['-hwaccel', 'cuda'])
elif hwaccel == 'qsv':
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}")
# 使用集中式硬件加速参数
hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args()
command.extend(hwaccel_args)
except Exception as e:
logger.warning(f"应用硬件加速参数时出错: {str(e)},将使用软件编码")
# 重置命令,移除可能添加了一半的硬件加速参数
@ -270,7 +214,7 @@ def process_single_video(
# 选择编码器 - 考虑到Windows和特定硬件的兼容性
use_software_encoder = True
if hwaccel:
if hwaccel == 'cuda' or hwaccel == 'nvenc':
try:
@ -289,7 +233,7 @@ def process_single_video(
elif hwaccel == 'vaapi' and not is_windows: # Linux VA-API
command.extend(['-c:v', 'h264_vaapi', '-profile', '100'])
use_software_encoder = False
# 如果前面的条件未能应用硬件编码器,使用软件编码
if use_software_encoder:
logger.info("使用软件编码器(libx264)")
@ -315,14 +259,14 @@ def process_single_video(
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
logger.error(f"处理视频失败: {error_msg}")
# 如果使用硬件加速失败,尝试使用软件编码
if hwaccel:
logger.info("尝试使用软件编码作为备选方案")
try:
# 构建新的命令,使用软件编码
fallback_cmd = ['ffmpeg', '-y', '-i', input_path]
# 保持原有的音频设置
if not keep_audio:
fallback_cmd.extend(['-an'])
@ -332,7 +276,7 @@ def process_single_video(
fallback_cmd.extend(['-c:a', 'aac', '-b:a', '128k'])
else:
fallback_cmd.extend(['-an'])
# 保持原有的视频过滤器
fallback_cmd.extend([
'-vf', f"{scale_filter},{pad_filter}",
@ -346,7 +290,7 @@ def process_single_video(
'-pix_fmt', 'yuv420p',
output_path
])
logger.info(f"执行备选FFmpeg命令: {' '.join(fallback_cmd)}")
subprocess.run(fallback_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
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)
logger.error(f"备选软件编码也失败: {fallback_error_msg}")
raise RuntimeError(f"无法处理视频 {input_path}: 硬件加速和软件编码都失败")
# 如果不是硬件加速导致的问题,或者备选方案也失败了,抛出原始错误
raise RuntimeError(f"处理视频失败: {error_msg}")
@ -409,7 +353,7 @@ def combine_clip_videos(
# 重组视频路径和原声设置为一个字典列表结构
video_segments = []
# 检查视频路径和原声设置列表长度是否匹配
if 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))
video_paths = video_paths[:min_length]
video_ost_list = video_ost_list[:min_length]
# 创建视频处理配置字典列表
for i, (video_path, video_ost) in enumerate(zip(video_paths, video_ost_list)):
if not os.path.exists(video_path):
logger.warning(f"视频不存在,跳过: {video_path}")
continue
# 检查是否有音频流
has_audio = check_video_has_audio(video_path)
# 构建视频片段配置
segment = {
"index": i,
@ -435,11 +379,11 @@ def combine_clip_videos(
"has_audio": has_audio,
"keep_audio": video_ost > 0 and has_audio # 只有当ost>0且实际有音频时才保留
}
# 记录日志
if video_ost > 0 and not has_audio:
logger.warning(f"视频 {video_path} 设置为保留原声(ost={video_ost}),但该视频没有音频流")
video_segments.append(segment)
# 处理每个视频片段
@ -495,20 +439,20 @@ def combine_clip_videos(
if not processed_videos:
raise ValueError("没有有效的视频片段可以合并")
# 按原始索引排序处理后的视频
processed_videos.sort(key=lambda x: x["index"])
# 第二阶段:分步骤合并视频 - 避免复杂的filter_complex滤镜
try:
# 1. 首先,将所有没有音频的视频或音频被禁用的视频合并到一个临时文件中
video_paths_only = [video["path"] for video in processed_videos]
video_concat_path = os.path.join(temp_dir, "video_concat.mp4")
# 创建concat文件用于合并视频流
concat_file = os.path.join(temp_dir, "concat_list.txt")
create_ffmpeg_concat_file(video_paths_only, concat_file)
# 合并所有视频流,但不包含音频
concat_cmd = [
'ffmpeg', '-y',
@ -522,19 +466,19 @@ def combine_clip_videos(
'-threads', str(threads),
video_concat_path
]
subprocess.run(concat_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("视频流合并完成")
# 2. 提取并合并有音频的片段
audio_segments = [video for video in processed_videos if video["keep_audio"]]
if not audio_segments:
# 如果没有音频片段,直接使用无音频的合并视频作为最终结果
shutil.copy(video_concat_path, output_video_path)
logger.info("无音频视频合并完成")
return output_video_path
# 创建音频中间文件
audio_files = []
for i, segment in enumerate(audio_segments):
@ -554,11 +498,11 @@ def combine_clip_videos(
"path": audio_file
})
logger.info(f"提取音频 {i+1}/{len(audio_segments)} 完成")
# 3. 计算每个音频片段的时间位置
audio_timings = []
current_time = 0.0
# 获取每个视频片段的时长
for i, video in enumerate(processed_videos):
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)
duration = float(result.stdout.strip())
# 如果当前片段需要保留音频,记录时间位置
if video["keep_audio"]:
for audio in audio_files:
@ -580,9 +524,9 @@ def combine_clip_videos(
"index": video["index"]
})
break
current_time += duration
# 4. 创建静音音频轨道作为基础
silence_audio = os.path.join(temp_dir, "silence.aac")
create_silence_cmd = [
@ -595,28 +539,28 @@ def combine_clip_videos(
silence_audio
]
subprocess.run(create_silence_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 5. 创建复杂滤镜命令以混合音频
filter_script = os.path.join(temp_dir, "filter_script.txt")
with open(filter_script, 'w') as f:
f.write(f"[0:a]volume=0.0[silence];\n") # 首先静音背景轨道
# 添加每个音频文件
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")
# 混合所有音频
mix_str = "[silence]"
for i in range(len(audio_timings)):
mix_str += f"[a{i}]"
mix_str += f"amix=inputs={len(audio_timings)+1}:duration=longest[aout]"
f.write(mix_str)
# 6. 构建音频合并命令
audio_inputs = ['-i', silence_audio]
for timing in audio_timings:
audio_inputs.extend(['-i', timing["file"]])
mixed_audio = os.path.join(temp_dir, "mixed_audio.aac")
audio_mix_cmd = [
'ffmpeg', '-y'
@ -627,10 +571,10 @@ def combine_clip_videos(
'-b:a', '128k',
mixed_audio
]
subprocess.run(audio_mix_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("音频混合完成")
# 7. 将合并的视频和混合的音频组合在一起
final_cmd = [
'ffmpeg', '-y',
@ -643,22 +587,22 @@ def combine_clip_videos(
'-shortest',
output_video_path
]
subprocess.run(final_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("视频最终合并完成")
return output_video_path
except subprocess.CalledProcessError as e:
logger.error(f"合并视频过程中出错: {e.stderr.decode() if e.stderr else str(e)}")
# 尝试备用合并方法 - 最简单的无音频合并
logger.info("尝试备用合并方法 - 无音频合并")
try:
concat_file = os.path.join(temp_dir, "concat_list.txt")
video_paths_only = [video["path"] for video in processed_videos]
create_ffmpeg_concat_file(video_paths_only, concat_file)
backup_cmd = [
'ffmpeg', '-y',
'-f', 'concat',
@ -668,14 +612,14 @@ def combine_clip_videos(
'-an', # 无音频
output_video_path
]
subprocess.run(backup_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.warning("使用备用方法(无音频)成功合并视频")
return output_video_path
except Exception as backup_error:
logger.error(f"备用合并方法也失败: {str(backup_error)}")
raise RuntimeError(f"无法合并视频: {str(backup_error)}")
except Exception as e:
logger.error(f"合并视频时出错: {str(e)}")
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 tqdm import tqdm
from app.utils import ffmpeg_utils
class VideoProcessor:
def __init__(self, video_path: str):
@ -63,7 +65,7 @@ class VideoProcessor:
if '=' in line:
key, value = line.split('=', 1)
info[key] = value
# 处理帧率(可能是分数形式)
if 'r_frame_rate' in info:
try:
@ -71,9 +73,9 @@ class VideoProcessor:
info['fps'] = str(num / den)
except ValueError:
info['fps'] = info.get('r_frame_rate', '25')
return info
except subprocess.CalledProcessError as e:
logger.error(f"获取视频信息失败: {e.stderr}")
return {
@ -83,7 +85,7 @@ class VideoProcessor:
'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]:
"""
按指定时间间隔提取视频帧
@ -98,57 +100,51 @@ class VideoProcessor:
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 计算起始时间和帧提取点
start_time = 0
end_time = self.duration
extraction_times = []
current_time = start_time
while current_time < end_time:
extraction_times.append(current_time)
current_time += interval_seconds
if not extraction_times:
logger.warning("未找到需要提取的帧")
return []
# 确定硬件加速器选项
hw_accel = []
if use_hw_accel:
# 尝试检测可用的硬件加速器
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("未检测到可用的硬件加速器,使用软件解码")
if use_hw_accel and ffmpeg_utils.is_ffmpeg_hwaccel_available():
hw_accel = ffmpeg_utils.get_ffmpeg_hwaccel_args()
# 提取帧
frame_numbers = []
for i, timestamp in enumerate(tqdm(extraction_times, desc="提取视频帧")):
frame_number = int(timestamp * self.fps)
frame_numbers.append(frame_number)
# 格式化时间戳字符串 (HHMMSSmmm)
hours = int(timestamp // 3600)
minutes = int((timestamp % 3600) // 60)
seconds = int(timestamp % 60)
milliseconds = int((timestamp % 1) * 1000)
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")
# 使用ffmpeg提取单帧
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
]
# 添加硬件加速参数
cmd.extend(hw_accel)
cmd.extend([
"-ss", str(timestamp),
"-i", self.video_path,
@ -157,12 +153,12 @@ class VideoProcessor:
"-y",
output_path
])
try:
subprocess.run(cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
logger.warning(f"提取帧 {frame_number} 失败: {e.stderr}")
logger.info(f"成功提取了 {len(frame_numbers)} 个视频帧")
return frame_numbers
@ -173,119 +169,9 @@ class VideoProcessor:
Returns:
List[str]: 硬件加速器ffmpeg命令参数
"""
# 检测操作系统
import platform
system = platform.system().lower()
# 测试不同的硬件加速器
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
# 如果没有找到可用的硬件加速器
# 使用集中式硬件加速检测
if ffmpeg_utils.is_ffmpeg_hwaccel_available():
return ffmpeg_utils.get_ffmpeg_hwaccel_args()
return []
def process_video_pipeline(self,
@ -294,7 +180,7 @@ class VideoProcessor:
use_hw_accel: bool = True) -> None:
"""
执行简化的视频处理流程直接从原视频按固定时间间隔提取帧
Args:
output_dir: 输出目录
interval_seconds: 帧提取间隔
@ -302,7 +188,7 @@ class VideoProcessor:
"""
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
try:
# 直接从原视频提取关键帧
logger.info(f"从视频间隔 {interval_seconds} 秒提取关键帧...")
@ -311,7 +197,7 @@ class VideoProcessor:
interval_seconds=interval_seconds,
use_hw_accel=use_hw_accel
)
logger.info(f"处理完成!视频帧已保存在: {output_dir}")
except Exception as e:
@ -324,16 +210,16 @@ if __name__ == "__main__":
import time
start_time = time.time()
# 使用示例
processor = VideoProcessor("./resource/videos/test.mp4")
# 设置间隔为3秒提取帧
processor.process_video_pipeline(
output_dir="output",
interval_seconds=3.0,
use_hw_accel=True
)
end_time = time.time()
print(f"处理完成!总耗时: {end_time - start_time:.2f}")

View File

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

View File

@ -7,6 +7,7 @@ from webui.components import basic_settings, video_settings, audio_settings, sub
review_settings, merge_settings, system_settings
# from webui.utils import cache, file_utils
from app.utils import utils
from app.utils import ffmpeg_utils
from app.models.schema import VideoClipParams, VideoAspect
@ -64,7 +65,7 @@ def init_log():
try:
for handler_id in logger._core.handlers:
logger.remove(handler_id)
# 重新添加带有高级过滤的处理器
def advanced_filter(record):
"""更复杂的过滤器,在应用启动后安全使用"""
@ -74,7 +75,7 @@ def init_log():
"CUDA initialization"
]
return not any(msg in record["message"] for msg in ignore_messages)
logger.add(
sys.stdout,
level=_lvl,
@ -91,7 +92,7 @@ def init_log():
colorize=True
)
logger.error(f"设置高级日志过滤器失败: {e}")
# 将高级过滤器设置放到启动主逻辑后
import threading
threading.Timer(5.0, setup_advanced_filters).start()
@ -192,7 +193,14 @@ def main():
"""主函数"""
init_log()
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的资源
# 检查是否能分解utils.init_resources()为基本资源和高级资源(如依赖PyTorch的资源)
try:
@ -218,15 +226,15 @@ def main():
audio_settings.render_audio_panel(tr)
with panel[2]:
subtitle_settings.render_subtitle_panel(tr)
# 渲染视频审查面板
review_settings.render_review_panel(tr)
# 放到最后渲染可能使用PyTorch的部分
# 渲染系统设置面板
with panel[2]:
system_settings.render_system_panel(tr)
# 放到最后渲染生成按钮和处理逻辑
render_generate_button()