【Python】FFmpeg2

2.5.3.1. 响度标准化:EBU R128与loudnorm滤镜深度解析

我们继续深入EBU R128响度标准的核心概念,这对于理解loudnorm滤镜至关重要。

EBU R128 标准核心概念

  • 目标响度 (Target Loudness):EBU R128建议的广播电视目标响度是 -23 LUFS。对于流媒体平台,这个目标响度通常会更低,例如Spotify和YouTube可能推荐 -14 LUFS,而iTunes/Apple Music可能推荐 -16 LUFS。理解不同平台的响度规范是进行响度标准化的关键。
  • 真实峰值 (True Peak - TP):传统的峰值电平测量可能无法准确捕捉到数字模拟转换(DAC)过程中由于重采样或滤波引起的瞬态过载。真实峰值则通过过采样技术来预测并测量这些潜在的“模拟峰值”,以防止削波失真。EBU R128建议的真实峰值上限是 -1 dBTP (decibels True Peak)。
  • 响度范围 (Loudness Range - LRA):LRA表示音频内容在感知响度上的动态范围。它有助于评估内容的动态多样性,例如,一部电影原声的LRA可能很大,因为它包含安静的对话和响亮的爆炸;而新闻播报的LRA则通常很小。LRA的计算基于响度的统计分布,通常以LU为单位。
  • 短时响度 (Short-term Loudness)瞬时响度 (Momentary Loudness):这些是用于实时监测的响度测量值,分别反映了较短时间窗口(如3秒或400毫秒)内的响度。它们对于理解音频内容的局部响度变化至关重要。

loudnorm 滤镜的精髓在于它能对音频内容进行双重调整

  1. 响度调整:将音频的整体响度(Integrated Loudness)调整到目标LUFS。
  2. 真实峰值限制:确保处理后的音频的真实峰值不超过指定的限值,从而避免削波。

这种双重机制使得loudnorm成为音视频后期处理中进行专业级响度管理不可或缺的工具。

loudnorm 滤镜参数的深度剖析

loudnorm 滤镜支持大量的参数,它们允许我们精细地控制响度标准化的过程。理解这些参数是驾驭loudnorm的关键。

在上述FFmpeg命令行示例中,-af 后面的字符串就是loudnorm滤镜的参数。让我们逐一深入这些参数:

  • I (Integrated Loudness):

    • 作用:设置目标集成响度(Target Integrated Loudness),以LUFS为单位。这是最核心的参数,决定了输出音频的整体感知响度。
    • 取值范围:通常在 -70.0 到 -5.0 之间。
    • 典型值:EBU R128建议值为 -23.0 LUFS。流媒体平台如Spotify、YouTube可能推荐 -14.0 LUFS。
    • 内部机制loudnorm算法会分析输入音频的集成响度,并计算出一个增益值,使输出音频的集成响度逼近或达到这个目标值。
  • LRA (Loudness Range):

    • 作用:设置目标响度范围(Target Loudness Range),以LU为单位。如果输入音频的LRA超出这个目标值,loudnorm可能会应用一些动态压缩来降低LRA,但这不是其主要功能,更复杂LRA控制通常需要专门的动态处理滤镜。
    • 取值范围:通常在 1.0 到 20.0 之间。
    • 典型值:取决于内容类型。例如,新闻播报通常有较低的LRA(如 4-7 LU),而电影原声可能需要较高的LRA(如 8-15 LU)。
    • 注意事项loudnorm滤镜主要侧重于集成响度和真实峰值,对LRA的控制是次要的且有限。若需要精确控制LRA,可能需要结合使用FFmpeg的其他动态处理滤镜(如compandcompressor)。
  • TP (True Peak):

    • 作用:设置目标真实峰值(Target True Peak),以dBTP为单位。此参数确保输出音频的真实峰值不超过设定的上限,有效防止数字削波。
    • 取值范围:通常在 -9.0 到 +0.0 之间。
    • 典型值:EBU R128建议值为 -1.0 dBTP。一些平台可能要求更保守的值,如 -2.0 dBTP。
    • 内部机制loudnorm会监测输出信号的真实峰值,并在必要时应用限幅器(limiter)来衰减信号,防止其超过此阈值。这个限幅过程是智能的,旨在最大限度地减少听觉上的失真。
  • print_format:

    • 作用:指定输出测量结果的格式。
    • 可选值
      • json:输出JSON格式的测量数据,非常适合程序化解析。
      • summary:输出人类可读的总结信息。
      • full:输出更详细的人类可读信息。
    • 典型用法:在第一遍(分析)过程中,通常设置为 json 以便Python程序捕获并解析测量结果。
  • linear:

    • 作用:控制响度调整过程是否使用线性增益。当设置为 true 时,loudnorm将只应用一个固定的线性增益,而不会进行动态压缩。这对于保持音频的原始动态范围非常重要,但可能无法完全达到目标LRA。当设置为 false (默认) 时,loudnorm可能应用轻微的动态处理。
    • 取值truefalse
    • 典型用法:如果非常强调保持音频的动态完整性,可以设置为 true
  • dual_mono:

    • 作用:控制多声道音频的处理方式。当设置为 true 时,loudnorm会独立处理每个声道,这在处理单声道内容被打包到立体声容器中(即左右声道内容完全相同)时很有用。当设置为 false (默认) 时,loudnorm会将所有声道作为一个整体进行响度测量和调整。
    • 取值truefalse
    • 典型用法:处理立体声或多声道内容时通常保持为 false
  • offset:

    • 作用:在响度归一化完成后,对所有输出样本应用一个额外的增益偏移,以dB为单位。
    • 取值范围:通常在 -10.0 到 +10.0 之间。
    • 典型用法:微调最终输出音量。不建议频繁使用,因为它会破坏标准化结果。
  • norm_precision:

    • 作用:设置响度测量和增益计算的精度。
    • 取值floatdouble
    • 典型用法:通常使用默认值(float 足够),除非对精度有极致要求。
  • gauss_precision:

    • 作用:用于控制响度范围(LRA)计算中的高斯曲线拟合精度。
    • 取值范围:正整数。
    • 典型用法:默认值通常足够,不需要特别调整。

双 Pass 响度标准化策略:精度与效能的平衡

尽管loudnorm滤镜可以单次完成响度标准化,但在实际生产环境中,为了获得更精确、更可预测的结果,双 Pass(Two-Pass)方法是强烈推荐的。

为什么需要双 Pass?

loudnorm滤镜在运行时需要知道整个音频流的响度特性。在单 Pass 模式下,它必须在处理过程中动态地估计这些特性,这可能导致一些不准确或非线性的增益调整,尤其是在音频内容响度变化剧烈的情况下。

双 Pass 方法的工作原理如下:

  1. 第一 Pass (分析 Pass)loudnorm滤镜被运行一次,仅仅用于分析输入音频的响度特性(集成响度、真实峰值、响度范围等),并将这些测量结果以JSON格式输出。在这个Pass中,不会对音频进行实际的编码或修改,通常会输出到 null 设备或丢弃编码结果。
  2. 第二 Pass (应用 Pass):根据第一 Pass 获得的精确测量数据,loudnorm滤镜再次运行。这一次,它使用这些预先计算好的参数来应用精确的线性增益调整和真实峰值限制,从而生成最终的标准化音频。这种方式使得loudnorm能够做出全局最优的决策,避免了在单Pass模式下可能出现的“来不及反应”的问题,保证了输出的响度目标和真实峰值限制得到严格遵守,同时最大程度地保留了音频的动态。

第一 Pass 输出的 JSON 数据解析

第一 Pass 的核心是捕获loudnorm滤镜的JSON输出。这个JSON包含了以下关键的测量值:

  • input_i: 输入文件的集成响度(LUFS)。
  • input_tp: 输入文件的真实峰值(dBTP)。
  • input_lra: 输入文件的响度范围(LU)。
  • input_thresh: 输入文件的响度阈值(LUFS)。这是计算集成响度时排除静音部分的阈值。
  • output_i: 如果不指定 measured_* 参数,这是基于目标响度计算出的输出文件的理想集成响度。
  • output_tp: 理想的输出真实峰值。
  • output_lra: 理想的输出响度范围。
  • output_thresh: 理想的输出响度阈值。
  • target_offset: 这是最关键的参数,表示在第二 Pass 中需要应用的增益偏移量(以dB为单位),以使输出达到目标集成响度。这个值是loudnorm算法计算出来的,用于纠正响度。

在第二 Pass 中,我们将使用 target_offset 这个值,并通过 measured_i, measured_tp, measured_lra, measured_thresh 参数来“告诉”loudnorm滤镜在第一 Pass 中测量到的输入音频特性,这样它就不需要再次计算,而是直接应用预设的增益。

loudnorm滤镜在第二 Pass 中的测量参数

在第二 Pass 中,loudnorm滤镜可以接受第一 Pass 测量的结果作为输入,以确保更精确的调整。这些参数通常以 measured_ 前缀命名:

  • measured_i:

    • 作用:在第一 Pass 中测得的输入集成响度。
    • 典型用法:将第一 Pass 输出的 input_i 值赋给此参数。
  • measured_tp:

    • 作用:在第一 Pass 中测得的输入真实峰值。
    • 典型用法:将第一 Pass 输出的 input_tp 值赋给此参数。
  • measured_lra:

    • 作用:在第一 Pass 中测得的输入响度范围。
    • 典型用法:将第一 Pass 输出的 input_lra 值赋给此参数。
  • measured_thresh:

    • 作用:在第一 Pass 中测得的输入响度阈值。
    • 典型用法:将第一 Pass 输出的 input_thresh 值赋给此参数。
  • target_offset:

    • 作用:在第一 Pass 中计算出的用于达到目标响度的增益偏移。
    • 典型用法:将第一 Pass 输出的 target_offset 值赋给此参数。这是在第二 Pass 中实现精确响度归一化的核心参数。

通过提供这些 measured_ 参数和 target_offset,第二 Pass 能够以固定增益的方式进行调整,从而避免了重新分析和可能引入的动态处理,保证了处理的透明性和精度。

Python 实现 loudnorm 双 Pass 策略

现在,我们将通过Python代码,利用subprocess模块调用FFmpeg,实现一个健壮的loudnorm双 Pass 响度标准化流程。我们将创建独立的函数来处理第一 Pass 的分析和第二 Pass 的应用,并提供详细的中文代码注释。

示例场景:我们将对一个音频文件进行响度标准化,使其达到EBU R128推荐的 -23 LUFS 目标响度,并限制真实峰值为 -1 dBTP。

import subprocess # 导入 subprocess 模块,用于执行外部命令
import json       # 导入 json 模块,用于解析 FFmpeg/FFprobe 输出的 JSON 数据
import os         # 导入 os 模块,用于文件路径操作,例如检查文件是否存在

def run_ffmpeg_command(command_parts, capture_output=True, text_mode=True):
    """
    通用函数,用于执行 FFmpeg/FFprobe 命令并处理其输出。

    Args:
        command_parts (list): 包含 FFmpeg/FFprobe 命令及其参数的列表。
        capture_output (bool): 是否捕获标准输出和标准错误。
        text_mode (bool): 是否以文本模式读取输出(True)或字节模式(False)。

    Returns:
        tuple: (return_code, stdout_output, stderr_output)。
               如果 capture_output 为 False,stdout_output 和 stderr_output 将为 None。
    Raises:
        RuntimeError: 如果 FFmpeg 命令执行失败。
    """
    try:
        # 使用 subprocess.run 执行命令
        # capture_output=capture_output: 控制是否捕获标准输出和标准错误
        # text=text_mode: 控制是否以文本模式(UTF-8编码)处理输出
        # check=True: 如果命令返回非零退出码,则抛出 CalledProcessError 异常
        process = subprocess.run(command_parts, capture_output=capture_output, text=text_mode, check=True)
        # 返回命令的退出码、标准输出和标准错误
        return process.returncode, process.stdout, process.stderr
    except subprocess.CalledProcessError as e:
        # 捕获 CalledProcessError 异常,表示命令执行失败
        error_message = f"FFmpeg 命令执行失败,退出码: {
     
     e.returncode}\n" \
                        f"命令: {
     
     ' '.join(e.cmd)}\n" \
                        f"标准输出: {
     
     e.stdout}\n" \
                        f"标准错误: {
     
     e.stderr}"
        print(error_message) # 打印错误信息
        raise RuntimeError(error_message) # 重新抛出自定义的运行时错误

def analyze_loudness_first_pass(input_audio_path, target_i=-23.0, target_tp=-1.0, target_lra=7.0):
    """
    执行 FFmpeg loudnorm 滤镜的第一 Pass(分析 Pass),获取响度测量数据。

    Args:
        input_audio_path (str): 输入音频文件的路径。
        target_i (float): 目标集成响度 (LUFS)。
        target_tp (float): 目标真实峰值 (dBTP)。
        target_lra (float): 目标响度范围 (LU)。

    Returns:
        dict: 包含 loudnorm 测量结果的字典。
    Raises:
        RuntimeError: 如果 FFmpeg 命令执行失败或无法解析 JSON 输出。
    """
    if not os.path.exists(input_audio_path): # 检查输入文件是否存在
        raise FileNotFoundError(f"输入音频文件不存在: {
     
     input_audio_path}") # 如果文件不存在,抛出文件未找到错误

    print(f"正在对文件 '{
     
     input_audio_path}' 进行响度第一 Pass 分析...") # 打印分析开始信息

    # 构建 FFmpeg 命令列表
    # ffmpeg: FFmpeg 可执行文件
    # -i: 指定输入文件
    # -af: 指定音频滤镜图。loudnorm 滤镜在这里使用。
    #      I: 目标集成响度
    #      LRA: 目标响度范围
    #      TP: 目标真实峰值
    #      print_format=json: 指定输出格式为 JSON,这是获取测量数据的关键。
    # -f null: 指定输出格式为 null,意味着不实际写入任何文件,只用于滤镜的分析输出。
    # -: 将输出重定向到标准输出(stdout)。
    command = [
        'ffmpeg',
        '-i', input_audio_path,
        '-af', f"loudnorm=I={
     
     target_i}:LRA={
     
     target_lra}:TP={
     
     target_tp}:print_format=json",
        '-f', 'null',
        '-'
    ]

    try:
        # 执行 FFmpeg 命令,并捕获标准错误(因为 loudnorm 的 JSON 输出在 stderr)
        # text=True 确保输出以文本形式处理
        _, _, stderr_output = run_ffmpeg_command(command, capture_output=True, text_mode=True)

        # 找到 JSON 数据的开始和结束位置
        # loudnorm 滤镜的 JSON 输出通常被包围在 INFO 或 other 消息中
        json_start_tag = '{' # JSON 数据的开始标志
        json_end_tag = '}'   # JSON 数据的结束标志
        json_data_str = "" # 用于存储提取到的 JSON 字符串

        # 遍历标准错误输出的每一行
        # 我们需要从 stderr 中提取 JSON 数据,因为它通常是混在其他日志信息中
        for line in stderr_output.splitlines():
            # 查找包含 JSON 开始标记的行
            if json_start_tag in line and 'loudnorm' in line: # 确保是 loudnorm 相关的 JSON
                json_data_str = line[line.find(json_start_tag):] # 从 '{' 开始截取字符串
                # 检查是否在同一行找到了结束标记
                if json_end_tag in json_data_str:
                    json_data_str = json_data_str[:json_data_str.rfind(json_end_tag) + 1] # 截取到 '}' 结束
                    break # 找到完整的 JSON,退出循环
            elif json_data_str: # 如果已经找到开始标记,但还没有找到结束标记,则继续追加行
                json_data_str += line # 将当前行追加到 JSON 字符串中
                if json_end_tag in line: # 如果当前行包含结束标记
                    json_data_str = json_data_str[:json_data_str.rfind(json_end_tag) + 1] # 截取到 '}' 结束
                    break # 找到完整的 JSON,退出循环

        if not json_data_str: # 如果没有找到任何 JSON 数据
            print("警告: 未在 FFmpeg 输出中找到 loudnorm 的 JSON 测量数据。原始 stderr:\n", stderr_output) # 打印警告
            raise RuntimeError("未能从 FFmpeg 输出中提取 loudnorm JSON 数据。") # 抛出运行时错误

        # 解析 JSON 字符串
        loudnorm_data = json.loads(json_data_str) # 将 JSON 字符串解析为 Python 字典
        print("第一 Pass 分析完成。测量数据:\n", json.dumps(loudnorm_data, indent=4, ensure_ascii=False)) # 打印解析后的数据,美化输出

        return loudnorm_data['loudnorm'] # 返回 loudnorm 键下的字典,包含所有测量参数

    except Exception as e:
        # 捕获任何在 JSON 解析或命令执行中发生的异常
        print(f"响度第一 Pass 分析过程中发生错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

def apply_loudnorm_second_pass(input_audio_path, output_audio_path, loudnorm_measures, target_i=-23.0, target_tp=-1.0):
    """
    执行 FFmpeg loudnorm 滤镜的第二 Pass(应用 Pass),根据第一 Pass 的测量结果进行响度标准化。

    Args:
        input_audio_path (str): 输入音频文件的路径。
        output_audio_path (str): 输出音频文件的路径。
        loudnorm_measures (dict): 包含第一 Pass 测量结果的字典(由 analyze_loudness_first_pass 返回)。
        target_i (float): 目标集成响度 (LUFS)。
        target_tp (float): 目标真实峰值 (dBTP)。

    Raises:
        RuntimeError: 如果 FFmpeg 命令执行失败。
    """
    if not os.path.exists(input_audio_path): # 检查输入文件是否存在
        raise FileNotFoundError(f"输入音频文件不存在: {
     
     input_audio_path}") # 抛出文件未找到错误

    print(f"正在对文件 '{
     
     input_audio_path}' 应用响度第二 Pass 归一化,输出到 '{
     
     output_audio_path}'...") # 打印处理开始信息

    # 从第一 Pass 的测量数据中提取关键参数
    # 这些参数将传递给 loudnorm 滤镜,以实现精确的响度调整。
    # 确保将 float 类型的值转换为字符串,因为 FFmpeg 命令行参数都是字符串。
    input_i = loudnorm_measures['input_i'] # 测量的输入集成响度
    input_tp = loudnorm_measures['input_tp'] # 测量的输入真实峰值
    input_lra = loudnorm_measures['input_lra'] # 测量的输入响度范围
    input_thresh = loudnorm_measures['input_thresh'] # 测量的输入响度阈值
    target_offset = loudnorm_measures['target_offset'] # 测量的目标偏移量,这是响度调整的核心增益。

    # 构建 FFmpeg 命令列表
    # ffmpeg: FFmpeg 可执行文件
    # -i: 指定输入文件
    # -af: 指定音频滤镜图。loudnorm 滤镜在这里使用。
    #      I: 目标集成响度
    #      TP: 目标真实峰值
    #      LRA: 目标响度范围 (虽然第二Pass主要是线性增益,但保留此参数以完整性)
    #      measured_i: 在第一Pass中测量的输入集成响度
    #      measured_tp: 在第一Pass中测量的输入真实峰值
    #      measured_lra: 在第一Pass中测量的输入响度范围
    #      measured_thresh: 在第一Pass中测量的输入响度阈值
    #      offset: 在第一Pass中计算出的 target_offset,直接作为额外的增益偏移应用。
    #      linear=true: 设置为线性模式,确保只应用固定增益,不进行额外的动态压缩。
    # -c:a aac: 指定音频编码器为 AAC (通常用于 MP4 容器)
    # -b:a 192k: 指定音频比特率为 192 kbps
    # -vn: 禁用视频流 (如果输入是视频文件,则只处理音频)
    # -y: 自动覆盖输出文件,如果它已存在
    command = [
        'ffmpeg',
        '-i', input_audio_path,
        '-af', f"loudnorm=I={
     
     target_i}:TP={
     
     target_tp}:LRA={
     
     input_lra}:"\
               f"measured_i={
     
     input_i}:measured_tp={
     
     input_tp}:"\
               f"measured_lra={
     
     input_lra}:measured_thresh={
     
     input_thresh}:"\
               f"offset={
     
     target_offset}:linear=true",
        '-c:a', 'aac',    # 指定音频编码器为 AAC
        '-b:a', '192k',   # 指定音频比特率为 192kbps
        '-vn',            # 禁用视频流,只处理音频
        '-y',             # 覆盖输出文件,如果存在
        output_audio_path # 输出文件路径
    ]

    try:
        # 执行 FFmpeg 命令。这里我们不捕获输出,因为我们只是想执行操作并写入文件。
        # 标准输出和错误会直接打印到控制台。
        return_code, _, _ = run_ffmpeg_command(command, capture_output=False)
        if return_code == 0: # 检查命令是否成功执行
            print(f"响度第二 Pass 归一化成功完成,文件已保存到: {
     
     output_audio_path}") # 打印成功信息
        else:
            raise RuntimeError(f"响度第二 Pass 归一化失败,FFmpeg 返回码: {
     
     return_code}") # 抛出错误

    except Exception as e:
        # 捕获任何在命令执行中发生的异常
        print(f"响度第二 Pass 应用过程中发生错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

# --- 实际使用示例 ---
if __name__ == "__main__":
    # 定义输入和输出文件路径。
    # 请替换为你的实际音频文件路径。
    # 建议使用一个音量有明显波动或响度不符合 EBU R128 标准的测试音频文件。
    input_file = "path/to/your/input_audio.mp3"  # 请替换为你的输入文件路径
    output_file_ebu_r128 = "path/to/your/output_audio_ebu_r128.mp3" # 请替换为你的输出文件路径

    # 模拟创建测试文件,实际应用中你应确保文件存在
    # 这里只是为了让示例代码可以运行,如果你有真实文件,请删除这部分
    if not os.path.exists(input_file):
        print(f"警告: 测试文件 '{
     
     input_file}' 不存在。请替换为你的实际文件路径或创建它。")
        # 假设创建一个空白音频文件用于测试
        # subprocess.run(['ffmpeg', '-f', 'lavfi', '-i', 'sine=duration=5:frequency=1000', input_file, '-y'])
        # print(f"已创建空白测试文件: {input_file}")
        # 为了演示,我们将直接跳过
        exit("请提供一个真实的音频文件路径来运行示例。")


    try:
        # --- 步骤 1: 执行第一 Pass,分析响度 ---
        # 目标参数设定为 EBU R128 推荐值
        measured_loudness_data = analyze_loudness_first_pass(
            input_file,
            target_i=-23.0,  # 目标集成响度 -23 LUFS (EBU R128 广播标准)
            target_tp=-1.0,  # 目标真实峰值 -1 dBTP
            target_lra=7.0   # 目标响度范围 7 LU (常用值,根据内容可调)
        )

        # --- 步骤 2: 执行第二 Pass,应用响度标准化 ---
        apply_loudnorm_second_pass(
            input_file,
            output_file_ebu_r128,
            measured_loudness_data,
            target_i=-23.0,  # 第二 Pass 再次明确目标集成响度
            target_tp=-1.0   # 第二 Pass 再次明确目标真实峰值
        )

        print("\n--- 响度标准化流程完成 ---") # 打印完成信息
        print(f"原始文件: {
     
     input_file}") # 打印原始文件路径
        print(f"EBU R128 标准化输出: {
     
     output_file_ebu_r128}") # 打印输出文件路径

        # --- 扩展示例:为流媒体平台(如Spotify)标准化 ---
        # 许多流媒体平台推荐不同的响度目标
        # 例如,Spotify 通常推荐 -14 LUFS,真实峰值 -1 dBTP
        output_file_spotify = "path/to/your/output_audio_spotify.mp3" # Spotify 标准化输出文件

        print("\n--- 正在为 Spotify 标准化响度进行分析 (第二 Pass 将使用相同的分析数据) ---") # 打印新的分析开始信息
        # 重新运行第一 Pass 来获取针对不同目标的 measured_data 也是一种选择,
        # 但通常 loudnorm 的 measured_data 只要是针对原始文件的,就可以通过调整 target_offset 来适应不同目标。
        # 但为了更严谨,或者目标参数差异很大时,重新运行第一 Pass 可能是更好的选择。
        # 这里我们假定可以复用 first_pass 的测量结果,只调整 target_i 和 target_tp
        # 但请注意,loudnorm 的 `target_offset` 是根据 `I` 参数计算的,所以如果 `I` 变了,
        # `target_offset` 也应该重新计算。因此,最严谨的方式是针对不同的目标重新跑一遍第一 Pass。
        # 为演示简化,我们重新执行第一 Pass,以便获取针对 Spotify 目标的 `target_offset`
        measured_loudness_data_spotify = analyze_loudness_first_pass(
            input_file,
            target_i=-14.0,  # 目标集成响度 -14 LUFS (Spotify 推荐)
            target_tp=-1.0,  # 目标真实峰值 -1 dBTP (Spotify 推荐)
            target_lra=7.0
        )

        apply_loudnorm_second_pass(
            input_file,
            output_file_spotify,
            measured_loudness_data_spotify,
            target_i=-14.0,  # 第二 Pass 明确 Spotify 目标集成响度
            target_tp=-1.0   # 第二 Pass 明确 Spotify 目标真实峰值
        )
        print(f"Spotify 标准化输出: {
     
     output_file_spotify}") # 打印 Spotify 输出文件路径

    except Exception as e:
        print(f"在响度标准化过程中发生致命错误: {
     
     e}") # 打印致命错误信息

在上述代码中,我们定义了 run_ffmpeg_command 函数来通用地执行FFmpeg命令,analyze_loudness_first_pass 用于执行第一 Pass 并解析JSON输出,apply_loudnorm_second_pass 则用于根据测量数据执行第二 Pass。if __name__ == "__main__": 块展示了如何调用这些函数来完成响度标准化流程,并给出了针对EBU R128和Spotify不同目标响度的两个示例。请务必将 input_fileoutput_file_ebu_r128output_file_spotify 替换为你的实际文件路径。

进一步的细节与高级应用

  1. 错误处理与健壮性

    • 在实际应用中,subprocess的错误输出(stderr)可能包含除了loudnorm JSON之外的大量调试信息。我们上面的analyze_loudness_first_pass函数通过字符串查找{ }来定位JSON,这在大多数情况下是有效的,但如果ffmpeg的日志格式发生变化,可能需要更复杂的正则表达式解析。
    • 始终检查FFmpeg命令的返回值。非零返回值表示错误。我们的run_ffmpeg_command函数通过check=True参数自动处理了这一点,会在命令失败时抛出CalledProcessError
    • 处理文件不存在、权限不足等文件I/O错误。我们已经加入了os.path.exists检查。
    • 考虑并发处理多个文件,这需要使用多线程或多进程,或者异步I/O(如asyncio),以避免阻塞主程序。
  2. loudnorm 的高级参数调优

    • print_format:除了jsonsummaryfull格式对于调试和人工查看响度信息非常有用。
    • linear:设置为 true 会强制 loudnorm 只应用线性增益,这意味着它不会尝试通过动态处理来达到目标 LRA。这在需要严格保持音频原始动态的情况下非常有用,但可能导致最终的 LRA 偏离目标。
    • dual_mono:如果处理的是立体声或多声道文件,并且确定每个声道的内容是独立的(例如,真正的左/右声道),则 dual_mono=false 是默认且推荐的,它会作为一个整体进行响度测量。如果处理的是双单声道(同一个单声道信号复制到左右声道),而你希望每个声道被独立分析,可以设置为 true
    • offsettarget_offset 的区别
      • offset 是在loudnorm滤镜完成所有内部计算(包括响度调整和真实峰值限制)之后,额外应用的固定增益。它是一个“后置”的微调。
      • target_offset 是第一 Pass 计算出来的,用于在第二 Pass 中指导loudnorm滤镜应用一个精确的、与目标响度相关的增益。它是loudnorm算法内部计算出的核心增益值,直接影响集成响度。在双 Pass 模式下,通常将第一 Pass 得到的 target_offset 传递给第二 Pass 的 offset 参数。
  3. 与其他滤镜的结合
    loudnorm滤镜可以作为复杂滤镜图的一部分与其他音频滤镜结合使用。例如,在响度标准化之前进行降噪、均衡或压缩,或之后进行空间音频处理。
    示例:先降噪,再响度标准化

    # 假设有一个降噪滤镜 'anlmdn'
    # command = [
    #     'ffmpeg', '-i', input_audio_path,
    #     '-af', f"anlmdn,loudnorm=...", # 滤镜之间用逗号分隔
    #     output_audio_path
    # ]
    

    滤镜链的顺序至关重要。通常,动态处理和降噪应该在响度标准化之前进行,因为它们会改变音频的动态特性,从而影响响度测量。

  4. 音频编解码器和容器格式的选择
    apply_loudnorm_second_pass 函数中,我们使用了 -c:a aac -b:a 192k。这表示输出音频将被编码为 AAC 格式,比特率为 192 kbps。

    • 编码器 (-c:a):选择合适的音频编码器非常重要。
      • aac:常用的有损编码,广泛用于网络流媒体和通用媒体文件。推荐使用 libfdk_aac(如果可用,提供更高质量)或原生的 aac 编码器。
      • mp3:另一种广泛使用的有损编码,兼容性好。
      • flac:无损编码,文件较大,但音质无损,适合归档。
      • pcm_s16le:PCM 原始无压缩音频,通常用于中间处理步骤或最高质量的输出。
    • 比特率 (-b:a):对于有损编码,比特率决定了文件大小和音质。更高的比特率通常意味着更好的音质,但文件也更大。选择应根据目标平台和质量要求。例如,对于高质量音乐,256kbps或320kbps的AAC/MP3可能更合适。
    • 容器格式:输出文件的扩展名(如.mp3, .m4a, .wav)决定了容器格式。确保选择的编码器与容器兼容。例如,AAC 通常用于 .m4a.mp4,MP3 用于 .mp3,FLAC 用于 .flac 或嵌入到 .mkv
  5. 响度测量的验证
    在执行完响度标准化后,验证输出文件的响度是否符合目标非常重要。可以使用 ffprobe 结合 loudnorm 滤镜的分析模式来再次测量输出文件的响度。

    def measure_audio_loudness(audio_path):
        """
        使用 ffprobe 和 loudnorm 滤镜测量音频文件的集成响度、真实峰值和响度范围。
        Args:
            audio_path (str): 音频文件路径。
        Returns:
            dict: 包含 input_i, input_tp, input_lra 的字典。
        Raises:
            RuntimeError: 如果测量失败。
        """
        if not os.path.exists(audio_path):
            raise FileNotFoundError(f"文件不存在: {
           
           audio_path}")
    
        print(f"正在测量文件 '{
           
           audio_path}' 的响度...")
        command = [
            'ffmpeg',
            '-i', audio_path,
            '-af', 'loudnorm=print_format=json', # 只用于测量,不应用任何调整
            '-f', 'null',
            '-'
        ]
        try:
            _, _, stderr_output = run_ffmpeg_command(command, capture_output=True, text_mode=True)
            json_start_tag = '{'
            json_end_tag = '}'
            json_data_str = ""
            for line in stderr_output.splitlines():
                if json_start_tag in line and 'loudnorm' in line:
                    json_data_str = line[line.find(json_start_tag):]
                    if json_end_tag in json_data_str:
                        json_data_str = json_data_str[:json_data_str.rfind(json_end_tag) + 1]
                        break
                elif json_data_str:
                    json_data_str += line
                    if json_end_tag in line:
                        json_data_str = json_data_str[:json_data_str.rfind(json_end_tag) + 1]
                        break
    
            if not json_data_str:
                print("警告: 未在 FFmpeg 输出中找到 loudnorm 的 JSON 测量数据。原始 stderr:\n", stderr_output)
                raise RuntimeError("未能从 FFmpeg 输出中提取 loudnorm JSON 数据。")
    
            loudnorm_data = json.loads(json_data_str)
            measured_data = loudnorm_data['loudnorm']
            print(f"测量结果: \n"
                  f"  集成响度 (I): {
           
           measured_data.get('input_i', 'N/A')} LUFS\n"
                  f"  真实峰值 (TP): {
           
           measured_data.get('input_tp', 'N/A')} dBTP\n"
                  f"  响度范围 (LRA): {
           
           measured_data.get('input_lra', 'N/A')} LU")
            return {
         
         
                'input_i': measured_data.get('input_i'),
                'input_tp': measured_data.get('input_tp'),
                'input_lra': measured_data.get('input_lra')
            }
        except Exception as e:
            print(f"测量音频响度时发生错误: {
           
           e}")
            raise
    
    # 在 main 函数中添加验证步骤
    # if __name__ == "__main__":
    #    ... (之前的代码) ...
    #    print("\n--- 验证 EBU R128 标准化结果 ---")
    #    measure_audio_loudness(output_file_ebu_r128)
    #
    #    print("\n--- 验证 Spotify 标准化结果 ---")
    #    measure_audio_loudness(output_file_spotify)
    

    通过这种方式,你可以确保你的标准化流程达到了预期的响度目标。

  6. 性能优化
    对于大型文件或批处理,FFmpeg的执行速度至关重要。

    • 硬件加速:FFmpeg支持各种硬件加速(如NVIDIA NVENC/NVDEC, Intel Quick Sync Video, AMD AMF)。如果你的系统支持,可以考虑在FFmpeg命令中加入相应的硬件加速参数,例如 -hwaccel cuda-hwaccel qsv。但这通常主要影响视频编码/解码速度,对纯音频响度处理影响较小,除非是音视频同步编码。
    • 多线程:FFmpeg默认会利用多核心CPU,但可以通过 -threads N 参数显式控制线程数量。
    • 内存优化:对于非常大的文件,确保系统有足够的内存,避免因为内存不足导致SWAP交换,影响性能。

响度标准化是专业音视频制作中的一个关键环节,尤其是在内容分发和广播领域。通过loudnorm滤镜和双 Pass 方法,我们可以精确地控制音频的感知响度和峰值,从而提升用户体验并符合行业标准。Python与subprocess的结合为我们提供了自动化这一复杂过程的强大能力。

2.6. 视频参数调整:尺寸、帧率、码率与编码

处理视频文件时,除了音频,视频流本身的属性调整同样至关重要。这包括改变视频的分辨率(尺寸)、帧率、比特率(码率)以及视频编码格式。这些调整直接影响视频的视觉质量、文件大小以及播放兼容性。FFmpeg提供了极其丰富的选项来完成这些任务。

2.6.1. 视频分辨率(尺寸)调整:scale 滤镜的精髓

改变视频的分辨率是最常见的视频处理需求之一,例如,将高清视频缩小为标清,或为移动设备适配更小的尺寸。FFmpeg的 scale 滤镜是实现这一目标的核心工具。

scale 滤镜的工作原理与参数详解

scale 滤镜的语法通常为 scale=width:height。它允许我们指定输出视频的宽度和高度。FFmpeg会根据源视频和目标尺寸进行图像插值算法,以确保画面质量。

常用参数:

  • width (或 w):
    • 作用:指定输出视频的宽度。
    • 取值:可以是具体的像素值(如 1280),也可以是基于输入视频尺寸的表达式。
  • height (或 h):
    • 作用:指定输出视频的高度。
    • 取值:可以是具体的像素值(如 720),也可以是基于输入视频尺寸的表达式。

保持宽高比的智能缩放

在调整分辨率时,最常见且最重要的需求是保持原始视频的宽高比,以避免画面变形。scale滤镜提供了几种内置的智能方式来处理这种情况,而无需我们手动计算新的宽度或高度。

  • -1iw*h/oh 等表达式

    • 当在 widthheight 中的一个位置使用 -1 时,FFmpeg会自动计算对应的值以保持宽高比。
    • 例如,scale=1280:-1 表示将视频宽度缩放到1280像素,高度则自动计算以保持宽高比。
    • scale=-1:720 表示将视频高度缩放到720像素,宽度自动计算。
    • 更高级的表达式:
      • scale=w=iw/2:h=ih/2:将宽度和高度都缩小一半。
      • scale=w=1920:h=ow/ar:将宽度设置为1920,高度通过 ow (输出宽度) 和 ar (输入宽高比) 计算。
      • scale=w=min(iw,1280):h=min(ih,-1):确保宽度不超过1280,同时保持宽高比。这在处理混合分辨率输入时很有用。
  • force_original_aspect_ratio:

    • 作用:控制当目标尺寸与原始宽高比不匹配时如何处理。
    • 可选值
      • disable (默认):不强制保持宽高比,直接缩放到指定尺寸,可能导致画面拉伸或压缩。
      • decrease:如果缩放会导致宽高比变化,则缩小尺寸以适应原始宽高比,可能会在视频边缘产生黑边(letterbox或pillarbox)。这在需要固定输出分辨率但又要保持画面比例时非常有用。
      • increase:如果缩放会导致宽高比变化,则放大尺寸以适应原始宽高比,可能会裁剪画面。

插值算法选择

scale滤镜默认使用双线性插值(bilinear interpolation),但在某些情况下,选择不同的插值算法可以影响缩放后的图像质量和处理速度。可以通过 sws_flags 选项进行设置。

  • sws_flags:
    • 作用:设置libswscale库(FFmpeg的图像缩放库)使用的缩放算法标志。
    • 常用值
      • bicubic (默认,通常在命令行中不需要显式指定): 双立方插值,提供较好的质量和适中的速度。
      • bilinear: 双线性插值,速度快,质量一般。
      • lanczos: Lanczos插值,提供最高质量,但速度最慢。
      • neighbor: 最近邻插值,速度最快,质量最差,但对于像素艺术等场景可能有用。

Python 实现视频分辨率调整

我们将创建一个Python函数来封装FFmpeg的 scale 滤镜,使其能够方便地调整视频分辨率,并支持保持宽高比。

import subprocess # 导入 subprocess 模块,用于执行外部命令
import os         # 导入 os 模块,用于文件路径操作

# 假设 run_ffmpeg_command 函数已在前面定义,这里不再重复定义
# def run_ffmpeg_command(...): ...

def adjust_video_resolution(input_video_path, output_video_path,
                            target_width=None, target_height=None,
                            keep_aspect_ratio=True,
                            scaling_algorithm='bicubic',
                            video_bitrate='2M', # 默认视频比特率
                            audio_bitrate='128k', # 默认音频比特率
                            preset='medium'): # 默认编码预设

    """
    调整视频文件的分辨率(尺寸)。

    Args:
        input_video_path (str): 输入视频文件的路径。
        output_video_path (str): 输出视频文件的路径。
        target_width (int, optional): 目标宽度。如果为 None 且 target_height 不为 None,则自动计算宽度。
        target_height (int, optional): 目标高度。如果为 None 且 target_width 不为 None,则自动计算高度。
        keep_aspect_ratio (bool): 是否保持原始视频的宽高比。
                                   如果为 True,且只指定了 width 或 height,则另一个值会自动计算。
                                   如果同时指定了 width 和 height,且 keep_aspect_ratio 为 True,
                                   FFmpeg 会尝试在保持宽高比的同时适应目标尺寸,可能留有黑边或裁剪。
        scaling_algorithm (str): 缩放算法,可选 'bilinear', 'bicubic', 'lanczos', 'neighbor'。
        video_bitrate (str): 输出视频的比特率,例如 '2M' (2Mbps)。
        audio_bitrate (str): 输出音频的比特率,例如 '128k' (128kbps)。
        preset (str): 编码预设,影响编码速度和文件大小的权衡,如 'ultrafast', 'fast', 'medium', 'slow', 'veryslow'。

    Raises:
        ValueError: 如果 target_width 和 target_height 都为 None。
        FileNotFoundError: 如果输入文件不存在。
        RuntimeError: 如果 FFmpeg 命令执行失败。
    """
    if not os.path.exists(input_video_path): # 检查输入文件是否存在
        raise FileNotFoundError(f"输入视频文件不存在: {
     
     input_video_path}") # 抛出文件未找到错误

    if target_width is None and target_height is None: # 如果宽度和高度都未指定
        raise ValueError("必须至少指定 target_width 或 target_height 中的一个。") # 抛出值错误

    print(f"正在调整视频 '{
     
     input_video_path}' 的分辨率...") # 打印处理开始信息

    # 构建 scale 滤镜参数
    scale_filter = "" # 初始化 scale 滤镜字符串
    if keep_aspect_ratio: # 如果需要保持宽高比
        if target_width is not None and target_height is not None: # 如果同时指定了宽度和高度
            # 这种情况下,FFmpeg会尝试将视频缩放到目标尺寸内,并保持宽高比,
            # 可能在边缘添加黑边(letterbox 或 pillarbox)。
            # 或者如果需要裁剪,则使用 force_original_aspect_ratio=increase
            # 为了简单起见,我们默认采用 letterbox 方式,即缩小以适应。
            scale_filter = f"scale='min({
     
     target_width}, iw*sar)':-1, scale=-1:'min({
     
     target_height}, ih)'"
            # 这里的 scale 表达式更复杂,它的目的是确保视频尺寸不超过 target_width 和 target_height
            # 同时保持宽高比,并在必要时添加黑边。
            # 更简单的做法是:
            # scale_filter = f"scale=w='min(iw, {target_width})':h='min(ih, {target_height})':force_original_aspect_ratio=decrease"
            # 为了更精确控制,我们使用 fitler_complex 更好,但这里限于简单滤镜
            # 对于简单的宽高比缩放,通常只需要指定一个维度,另一个设为 -1
            print("警告: 保持宽高比模式下同时指定宽高,可能导致输出尺寸与目标不完全匹配或出现黑边。建议只指定一个维度并设另一个为-1。")
            scale_filter = f"scale='min({
     
     target_width},a*({
     
     target_height})):min({
     
     target_height},a*({
     
     target_width}))'" # 动态计算,确保不超出目标尺寸且保持宽高比
            # 更通用的保持宽高比到特定尺寸:
            # scale=w=TARGET_WIDTH:h=TARGET_HEIGHT:force_original_aspect_ratio=decrease
            # ffmpeg将缩小视频以使其完全适合目标尺寸,并在必要时添加黑边
            # 或者 force_original_aspect_ratio=increase
            # ffmpeg将放大视频以使其填充目标尺寸,并在必要时裁剪边缘
            # 在这里我们采取折衷,如果两个都指定了,就用 `min(TARGET, iw_or_ih)` 的方式,确保不超出目标
            # 并让 FFmpeg 自己处理宽高比,这可能需要更精细的滤镜链,例如 pads
            # 考虑到简单性,我们假设用户只指定一个维度或者接受黑边/裁剪。
            # 这里将采用更简化的方法,只指定其中一个,另一个用 -1。
            if target_width and not target_height: # 如果只指定宽度
                scale_filter = f"scale={
     
     target_width}:-1" # 宽度固定,高度自动计算保持宽高比
            elif target_height and not target_width: # 如果只指定高度
                scale_filter = f"scale=-1:{
     
     target_height}" # 高度固定,宽度自动计算保持宽高比
            else: # 同时指定了宽高,并且要求保持宽高比,那么需要根据原始宽高比来判断
                # 复杂的处理,例如填充黑边或者裁剪
                # 这里为了保持简洁性,我们假设用户理解同时指定并保持宽高比可能带来黑边或裁剪
                # 最简单且保持宽高比的方法:
                # 如果 target_width / target_height > original_aspect_ratio: target_height 优先
                # 如果 target_width / target_height < original_aspect_ratio: target_width 优先
                # 但这需要先探测原始宽高比,目前我们不做此探测。
                # 最常见且简单的处理方式是:
                # 设定一个基准,例如 target_width
                scale_filter = f"scale={
     
     target_width}:-1" # 默认以宽度为准,保持宽高比
                if target_height and target_width:
                    # 如果两个都指定了,并且期望保持宽高比,则会选择一个维度作为基准
                    # 并让另一个维度自动调整,如果因此超出另一个目标维度,FFmpeg不会自动裁剪或填充
                    # 所以通常建议只指定一个维度并使用 -1。
                    # 或者,使用 force_original_aspect_ratio
                    scale_filter = f"scale=w={
     
     target_width}:h={
     
     target_height}:force_original_aspect_ratio=decrease"
                    # force_original_aspect_ratio=decrease 会在不改变宽高比的前提下,将视频缩放到目标尺寸内,
                    # 可能在边缘生成黑边。
                    print(f"同时指定宽度 {
     
     target_width} 和高度 {
     
     target_height},并要求保持宽高比。视频将被缩放以适应此矩形,可能添加黑边。")
        elif target_width is not None: # 只指定了宽度
            scale_filter = f"scale={
     
     target_width}:-1" # 宽度固定,高度自动计算保持宽高比
        elif target_height is not None: # 只指定了高度
            scale_filter = f"scale=-1:{
     
     target_height}" # 高度固定,宽度自动计算保持宽高比
    else: # 不保持宽高比,直接缩放到指定尺寸,可能导致画面变形
        if target_width is None or target_height is None: # 必须同时指定宽度和高度才能不保持宽高比
             raise ValueError("当 keep_aspect_ratio=False 时,必须同时指定 target_width 和 target_height。")
        scale_filter = f"scale={
     
     target_width}:{
     
     target_height}" # 直接缩放到指定宽高

    # 添加缩放算法参数
    if scaling_algorithm in ['bilinear', 'bicubic', 'lanczos', 'neighbor']: # 检查算法是否有效
        # sws_flags 选项用于指定缩放算法
        scale_filter += f":flags={
     
     scaling_algorithm}" # 将缩放算法添加到滤镜参数中
    else:
        print(f"警告: 不支持的缩放算法 '{
     
     scaling_algorithm}'。将使用默认算法 (bicubic)。") # 打印警告

    # 构建 FFmpeg 命令
    # ffmpeg: FFmpeg 可执行文件
    # -i: 指定输入文件
    # -vf: 指定视频滤镜图。scale 滤镜在这里使用。
    # -c:v libx264: 指定视频编码器为 H.264 (libx264 是一个高质量的 H.264 编码器)
    # -b:v: 指定视频比特率
    # -preset: 编码预设,影响编码速度和输出文件大小的权衡
    # -c:a aac: 指定音频编码器为 AAC
    # -b:a: 指定音频比特率
    # -y: 自动覆盖输出文件,如果它已存在
    command = [
        'ffmpeg',
        '-i', input_video_path,
        '-vf', scale_filter,        # 视频滤镜,应用分辨率调整
        '-c:v', 'libx264',          # 视频编码器,使用 libx264 (H.264)
        '-b:v', video_bitrate,      # 视频比特率
        '-preset', preset,          # 编码预设
        '-c:a', 'aac',              # 音频编码器,使用 AAC
        '-b:a', audio_bitrate,      # 音频比特率
        '-y',                       # 覆盖输出文件
        output_video_path           # 输出文件路径
    ]

    try:
        # 执行 FFmpeg 命令
        return_code, _, _ = run_ffmpeg_command(command, capture_output=False) # 不捕获输出,直接打印到控制台
        if return_code == 0: # 检查命令是否成功执行
            print(f"视频分辨率调整成功,文件已保存到: {
     
     output_video_path}") # 打印成功信息
        else:
            raise RuntimeError(f"视频分辨率调整失败,FFmpeg 返回码: {
     
     return_code}") # 抛出错误

    except Exception as e:
        # 捕获任何在命令执行中发生的异常
        print(f"调整视频分辨率时发生错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

# --- 实际使用示例 ---
if __name__ == "__main__":
    # 请替换为你的实际视频文件路径
    input_video = "path/to/your/input_video.mp4" # 请替换为你的输入视频文件路径

    # 模拟创建测试文件(如果你没有,请替换为真实文件路径)
    if not os.path.exists(input_video):
        print(f"警告: 测试视频文件 '{
     
     input_video}' 不存在。请替换为你的实际文件路径或创建它。")
        # 可以使用 ffmpeg 创建一个简单的测试视频
        # subprocess.run([
        #    'ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=5:size=1920x1080:rate=30',
        #    '-f', 'lavfi', '-i', 'sine=duration=5:frequency=440',
        #    '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'ultrafast',
        #    '-c:a', 'aac', '-b:a', '128k',
        #    input_video, '-y'
        # ])
        # print(f"已创建测试视频文件: {input_video}")
        exit("请提供一个真实的视频文件路径来运行示例。")

    try:
        # 示例 1: 将视频宽度缩放到 1280 像素,保持宽高比
        output_video_720p = "path/to/your/output_video_1280w.mp4" # 输出文件路径
        adjust_video_resolution(input_video, output_video_720p,
                                target_width=1280, keep_aspect_ratio=True)
        print("-" * 30) # 分隔线

        # 示例 2: 将视频高度缩放到 480 像素,保持宽高比
        output_video_480h = "path/to/your/output_video_480h.mp4" # 输出文件路径
        adjust_video_resolution(input_video, output_video_480h,
                                target_height=480, keep_aspect_ratio=True,
                                scaling_algorithm='lanczos') # 使用更高质量的Lanczos算法
        print("-" * 30) # 分隔线

        # 示例 3: 将视频直接缩放到 640x360,不保持宽高比 (可能导致变形)
        # 这种场景下,如果原始宽高比不是 16:9,画面会被拉伸或压缩。
        output_video_deformed = "path/to/your/output_video_640x360_deformed.mp4" # 输出文件路径
        adjust_video_resolution(input_video, output_video_deformed,
                                target_width=640, target_height=360,
                                keep_aspect_ratio=False)
        print("-" * 30) # 分隔线

        # 示例 4: 将视频缩放到 854x480 (16:9), 并确保保持宽高比,可能产生黑边 (letterbox)
        # 例如,如果原始视频是 4:3 (1440x1080),缩放到 854x480 的 16:9 框内
        output_video_letterbox = "path/to/your/output_video_letterbox.mp4" # 输出文件路径
        # 注意: 这种复杂的保持宽高比并填充黑边的逻辑,在 `adjust_video_resolution` 内部的 `scale_filter` 构建部分需要更细致的处理。
        # 上述 `adjust_video_resolution` 函数在同时指定宽高且 `keep_aspect_ratio=True` 时,
        # 默认使用了 `force_original_aspect_ratio=decrease` 的逻辑,这意味着它会尽可能缩小以适应目标矩形,并添加黑边。
        adjust_video_resolution(input_video, output_video_letterbox,
                                target_width=854, target_height=480,
                                keep_aspect_ratio=True) # 此时内部会使用 force_original_aspect_ratio=decrease 逻辑
        print("-" * 30) # 分隔线


    except Exception as e:
        print(f"在调整视频分辨率过程中发生致命错误: {
     
     e}") # 打印致命错误信息

在上述代码中,adjust_video_resolution 函数提供了灵活的参数来控制输出视频的分辨率、宽高比保持策略、缩放算法、视频和音频比特率以及编码预设。if __name__ == "__main__": 块展示了不同的使用场景,包括只指定宽度、只指定高度、不保持宽高比的直接缩放,以及尝试保持宽高比同时适应特定尺寸(可能导致黑边)的情况。理解 scale 滤镜的 -1 参数和 force_original_aspect_ratio 选项是实现精确缩放的关键。

进一步的 scale 滤镜高级应用

  1. 动态表达式scale 滤镜的 widthheight 参数支持复杂的表达式,例如:

    • scale=w=min(1920,iw*2):h=-1:将宽度最多放大到1920像素,但不会超过原始宽度的两倍,同时保持宽高比。
    • scale=w=iw/2:h=ih/2:将视频尺寸减半。
    • scale=w=trunc(iw/2)*2:h=trunc(ih/2)*2:确保输出宽度和高度都是偶数(某些视频编码器要求)。
  2. 宽高比计算:虽然 -1 参数很方便,但有时你可能需要手动计算宽高比。FFmpeg的表达式中可以访问 iw (输入宽度), ih (输入高度), sar (样本宽高比), dar (显示宽高比)。

    • dar = iw/ih * sar
    • 例如,将视频缩放到目标宽度 TW,高度为 TH = trunc(TW / dar)scale=w=TW:h=trunc(TW/dar)。但 -1 已经处理了大部分这些情况。
  3. 与其他视频滤镜组合scale 滤镜可以与其他视频滤镜组合,形成复杂的视频处理链。例如:

    • 裁剪后缩放crop=w:h:x:y,scale=new_w:new_h。先裁剪出感兴趣的区域,再缩放。
    • 缩放后填充scale=w:h:force_original_aspect_ratio=decrease,pad=new_w:new_h:(new_w-ow)/2:(new_h-oh)/2。先将视频缩放到目标尺寸内(可能带黑边),然后用 pad 滤镜将视频填充到最终目标尺寸(例如,将 16:9 视频填充到 4:3 框架中,上下加黑边)。

    示例:缩放并填充黑边到特定分辨率(例如,将任意视频填充到 1920x1080 尺寸)
    这需要两个滤镜:scalepad
    首先,scale 滤镜将视频缩放到 1920x1080 框内,保持宽高比,并确保最长边符合目标,可能导致另一边留有空白。
    然后,pad 滤镜在缩放后的视频周围添加黑边,使其刚好达到 1920x1080。

    def scale_and_pad_video(input_video_path, output_video_path,
                            target_width, target_height,
                            pad_color='black', video_bitrate='2M', audio_bitrate='128k', preset='medium'):
        """
        将视频缩放并填充黑边,使其达到指定的分辨率。
        例如,将非 16:9 的视频缩放到 1920x1080 并在边缘添加黑边。
    
        Args:
            input_video_path (str): 输入视频文件的路径。
            output_video_path (str): 输出视频文件的路径。
            target_width (int): 目标输出宽度。
            target_height (int): 目标输出高度。
            pad_color (str): 填充黑边的颜色,如 'black', 'white', 'red' 等。
            video_bitrate (str): 视频比特率。
            audio_bitrate (str): 音频比特率。
            preset (str): 编码预设。
        Raises:
            FileNotFoundError: 如果输入文件不存在。
            RuntimeError: 如果 FFmpeg 命令执行失败。
        """
        if not os.path.exists(input_video_path):
            raise FileNotFoundError(f"输入视频文件不存在: {
           
           input_video_path}")
    
        print(f"正在缩放并填充视频 '{
           
           input_video_path}' 到 {
           
           target_width}x{
           
           target_height}...")
    
        # 视频滤镜链:
        # 1. scale 滤镜:
        #    'w=if(gt(a,{target_width}/{target_height}),{target_width},-1)':
        #        如果原始宽高比 (a) 大于目标宽高比,则宽度设为目标宽度,高度自动计算 (-1)。
        #        这意味着以宽度为基准缩放,高度可能小于目标高度。
        #    'h=if(lt(a,{target_width}/{target_height}),{target_height},-1)':
        #        如果原始宽高比 (a) 小于目标宽高比,则高度设为目标高度,宽度自动计算 (-1)。
        #        这意味着以高度为基准缩放,宽度可能小于目标宽度。
        #    flags=lanczos: 使用 Lanczos 算法进行高质量缩放。
        # 简化版:使用 force_original_aspect_ratio=decrease,更直观
        scale_filter = f"scale=w={
           
           target_width}:h={
           
           target_height}:force_original_aspect_ratio=decrease"
    
        # 2. pad 滤镜:
        #    用于在视频周围添加边框(黑边或指定颜色),使其达到目标尺寸。
        #    x=(ow-iw)/2: 将原始视频水平居中。
        #    y=(oh-ih)/2: 将原始视频垂直居中。
        #    color={pad_color}: 填充颜色。
        #    ow 和 oh 是 pad 滤镜的输出宽度和高度,此处设定为 target_width 和 target_height
        #    iw 和 ih 是 pad 滤镜的输入宽度和高度,它们是 scale 滤镜的输出宽度和高度
        #    因此,pad_filter 依赖于 scale_filter 的结果。
        #    pad=w=target_width:h=target_height:x=(ow-iw)/2:y=(oh-ih)/2:color=pad_color
        # 注意:这里的 ow, ih 是 pad 滤镜自身的输出和输入,不是全局的。
        # 当滤镜链使用 -vf 时,滤镜之间用逗号分隔。
        # scale=1920:-1:flags=lanczos:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black
        # 或者更简洁: scale=min(iw*H/ih,W):min(ih*W/iw,H),pad=W:H:(W-iw)/2:(H-ih)/2:color=black
        # W和H是目标宽度和高度
        # iw和ih是缩放后实际的宽度和高度
        video_filter_chain = (
            f"scale=w='min({
           
           target_width},a*{
           
           target_height})':h='min({
           
           target_height},a*{
           
           target_width})'," # 缩放到目标尺寸内,保持宽高比
            f"pad=w={
           
           target_width}:h={
           
           target_height}:x=(ow-iw)/2:y=(oh-ih)/2:color={
           
           pad_color}" # 填充黑边到目标尺寸
        )
    
        command = [
            'ffmpeg',
            '-i', input_video_path,
            '-vf', video_filter_chain,   # 应用视频滤镜链
            '-c:v', 'libx264',           # 视频编码器
            '-b:v', video_bitrate,       # 视频比特率
            '-preset', preset,           # 编码预设
            '-c:a', 'aac',               # 音频编码器
            '-b:a', audio_bitrate,       # 音频比特率
            '-y',                        # 覆盖输出
            output_video_path
        ]
    
        try:
            return_code, _, _ = run_ffmpeg_command(command, capture_output=False)
            if return_code == 0:
                print(f"视频缩放并填充成功,文件已保存到: {
           
           output_video_path}")
            else:
                raise RuntimeError(f"视频缩放并填充失败,FFmpeg 返回码: {
           
           return_code}")
        except Exception as e:
            print(f"执行缩放并填充操作时发生错误: {
           
           e}")
            raise
    
    # 在 main 函数中添加调用示例
    # if __name__ == "__main__":
    #    ... (之前的代码) ...
    #    print("\n--- 缩放并填充视频到 1920x1080 (16:9) ---")
    #    output_video_padded_hd = "path/to/your/output_video_padded_hd.mp4"
    #    scale_and_pad_video(input_video, output_video_padded_hd,
    #                        target_width=1920, target_height=1080)
    #    print("-" * 30)
    

    这个 scale_and_pad_video 函数展示了如何将 scalepad 两个滤镜组合起来,实现更复杂的视频尺寸调整需求,例如将不同宽高比的视频统一到某个标准分辨率(如16:9的HD或UHD),同时避免画面变形,通过添加黑边来填充空白区域。

这些高级应用和示例进一步展示了FFmpeg在视频分辨率调整方面的强大灵活性。通过Python结合subprocess,我们可以自动化并精细控制这些复杂的视频处理任务。

2.6.2. 视频帧率(帧速)调整:fps 滤镜的精确控制

视频帧率(Frames Per Second, FPS)是视频流畅度的关键指标。电影通常是24fps,电视广播是25fps或30fps(PAL/NTSC),而游戏和高动态视频可能达到60fps甚至更高。有时我们需要调整视频的帧率,例如:

  • 降低帧率以减小文件大小或适应旧设备。
  • 提高帧率(帧插值)以使视频更流畅(尽管FFmpeg的简单 fps 滤镜不执行复杂插值,它主要是丢帧或复制帧)。
  • 统一不同来源视频的帧率以便后期剪辑。

FFmpeg的 fps 滤镜就是用于调整视频帧率的工具。

fps 滤镜的工作原理与参数详解

fps 滤镜的语法很简单:fps=fps=Nfps=N,其中 N 是目标帧率。

  • fps (或 N):
    • 作用:指定输出视频的目标帧率。
    • 取值:可以是整数(如 24, 30, 60),也可以是浮点数(如 29.97),还可以是分数(如 30000/1001)。
    • 内部机制
      • 降低帧率:如果目标帧率低于原始帧率,fps 滤镜会丢弃多余的帧。例如,从 60fps 降到 30fps,它会丢弃一半的帧。这种丢弃是周期性的,以保持平稳。
      • 提高帧率:如果目标帧率高于原始帧率,fps 滤镜会复制现有的帧。例如,从 30fps 提高到 60fps,它会复制每一帧。请注意,这种简单的帧复制不会产生真正的“新”帧,视频看起来会重复或显得不流畅,而不是更流畅。要实现更高级的帧插值(如运动补偿插值),需要使用更复杂的滤镜或外部工具(如interframesvpflow,通常不直接在FFmpeg中提供开箱即用的高质量运动插值)。
      • 近似匹配fps 滤镜会尽量接近目标帧率,但有时由于浮点精度或内部算法原因,实际输出帧率可能略有偏差。

Python 实现视频帧率调整

我们将创建一个Python函数,利用subprocess调用FFmpeg的 fps 滤镜来改变视频的帧率。

import subprocess # 导入 subprocess 模块,用于执行外部命令
import os         # 导入 os 模块,用于文件路径操作

# 假设 run_ffmpeg_command 函数已在前面定义,这里不再重复定义

def adjust_video_framerate(input_video_path, output_video_path,
                           target_fps,
                           video_bitrate='2M', audio_bitrate='128k', preset='medium'):
    """
    调整视频文件的帧率(帧速)。

    Args:
        input_video_path (str): 输入视频文件的路径。
        output_video_path (str): 输出视频文件的路径。
        target_fps (float or int or str): 目标帧率,例如 24, 30, 60, 或 "29.97"。
                                          也可以是分数形式的字符串,如 "30000/1001"。
        video_bitrate (str): 输出视频的比特率,例如 '2M' (2Mbps)。
        audio_bitrate (str): 输出音频的比特率,例如 '128k' (128kbps)。
        preset (str): 编码预设,影响编码速度和文件大小的权衡。

    Raises:
        ValueError: 如果 target_fps 无效。
        FileNotFoundError: 如果输入文件不存在。
        RuntimeError: 如果 FFmpeg 命令执行失败。
    """
    if not os.path.exists(input_video_path): # 检查输入文件是否存在
        raise FileNotFoundError(f"输入视频文件不存在: {
     
     input_video_path}") # 抛出文件未找到错误

    try:
        # 验证 target_fps 的有效性 (简单验证)
        if isinstance(target_fps, (int, float)): # 如果是数字类型
            if target_fps <= 0: # 帧率必须大于0
                raise ValueError("目标帧率必须大于 0。") # 抛出值错误
            fps_str = str(target_fps) # 转换为字符串
        elif isinstance(target_fps, str): # 如果是字符串类型
            # 尝试转换为浮点数或检查分数格式
            try:
                if '/' in target_fps: # 分数形式
                    num, den = map(int, target_fps.split('/')) # 解析分子分母
                    if den == 0: # 分母不能为0
                        raise ValueError("目标帧率分母不能为零。")
                    if num <= 0: # 分子必须大于0
                        raise ValueError("目标帧率分子必须大于零。")
                else: # 浮点数形式的字符串
                    float(target_fps) # 尝试转换为浮点数验证
                    if float(target_fps) <= 0:
                        raise ValueError("目标帧率必须大于 0。")
            except ValueError:
                raise ValueError(f"无效的目标帧率格式: {
     
     target_fps}。请提供数字或 'num/den' 格式。") # 抛出值错误
            fps_str = target_fps # 使用原始字符串
        else:
            raise ValueError("目标帧率必须是数字或字符串类型。") # 抛出值错误

    except ValueError as e:
        print(f"帧率参数错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

    print(f"正在调整视频 '{
     
     input_video_path}' 的帧率到 {
     
     fps_str} fps...") # 打印处理开始信息

    # 构建 FFmpeg 命令
    # ffmpeg: FFmpeg 可执行文件
    # -i: 指定输入文件
    # -vf: 指定视频滤镜图。fps 滤镜在这里使用。
    # -c:v libx264: 视频编码器
    # -b:v: 视频比特率
    # -preset: 编码预设
    # -c:a copy: 复制音频流,不重新编码 (除非你需要调整音频比特率或格式)
    #             注意:如果之前或之后有音频滤镜(如 loudnorm),则不能使用 `copy`,需要重新编码音频。
    #             为了完整性,我们在此处也提供音频重新编码选项。
    # -b:a: 音频比特率
    # -y: 自动覆盖输出文件
    command = [
        'ffmpeg',
        '-i', input_video_path,
        '-vf', f'fps={
     
     fps_str}',    # 视频滤镜,应用帧率调整
        '-c:v', 'libx264',          # 视频编码器
        '-b:v', video_bitrate,      # 视频比特率
        '-preset', preset,          # 编码预设
        '-c:a', 'aac',              # 音频编码器
        '-b:a', audio_bitrate,      # 音频比特率
        '-y',                       # 覆盖输出文件
        output_video_path           # 输出文件路径
    ]

    try:
        # 执行 FFmpeg 命令
        return_code, _, _ = run_ffmpeg_command(command, capture_output=False) # 不捕获输出,直接打印到控制台
        if return_code == 0: # 检查命令是否成功执行
            print(f"视频帧率调整成功,文件已保存到: {
     
     output_video_path}") # 打印成功信息
        else:
            raise RuntimeError(f"视频帧率调整失败,FFmpeg 返回码: {
     
     return_code}") # 抛出错误

    except Exception as e:
        # 捕获任何在命令执行中发生的异常
        print(f"调整视频帧率时发生错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

# --- 实际使用示例 ---
if __name__ == "__main__":
    # 请替换为你的实际视频文件路径
    input_video_for_fps = "path/to/your/input_video_high_fps.mp4" # 请替换为你的输入视频文件路径

    # 模拟创建测试文件(如果你没有,请替换为真实文件路径)
    if not os.path.exists(input_video_for_fps):
        print(f"警告: 测试视频文件 '{
     
     input_video_for_fps}' 不存在。请替换为你的实际文件路径或创建它。")
        # 创建一个 60fps 的测试视频
        # subprocess.run([
        #    'ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=5:size=1280x720:rate=60',
        #    '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'ultrafast',
        #    input_video_for_fps, '-y'
        # ])
        # print(f"已创建测试视频文件: {input_video_for_fps}")
        exit("请提供一个真实的视频文件路径来运行帧率调整示例。")

    try:
        # 示例 1: 将视频帧率降低到 30 fps
        output_video_30fps = "path/to/your/output_video_30fps.mp4" # 输出文件路径
        adjust_video_framerate(input_video_for_fps, output_video_30fps,
                               target_fps=30)
        print("-" * 30) # 分隔线

        # 示例 2: 将视频帧率提高到 60 fps (如果原始帧率低于 60,FFmpeg会复制帧)
        output_video_60fps = "path/to/your/output_video_60fps.mp4" # 输出文件路径
        adjust_video_framerate(input_video_for_fps, output_video_60fps,
                               target_fps=60, preset='slow') # 编码预设设为 'slow' 以获得更好质量

        print("-" * 30) # 分隔线

        # 示例 3: 将视频帧率调整为 NTSC 广播标准的 29.97 fps (分数形式)
        output_video_29_97fps = "path/to/your/output_video_29_97fps.mp4" # 输出文件路径
        adjust_video_framerate(input_video_for_fps, output_video_29_97fps,
                               target_fps="30000/1001") # NTSC 标准帧率表示

    except Exception as e:
        print(f"在调整视频帧率过程中发生致命错误: {
     
     e}") # 打印致命错误信息

在上述代码中,adjust_video_framerate 函数接受输入和输出文件路径以及目标帧率,并使用 fps 滤镜进行处理。它还包含了对目标帧率参数的简单验证。if __name__ == "__main__": 块展示了将帧率降低、提高以及设置为特定广播标准帧率的示例。

fps 滤镜的注意事项与高级用法

  1. 非插值性:再次强调,FFmpeg的 fps 滤镜主要是通过丢弃或复制帧来改变帧率,它不执行复杂的运动补偿帧插值。如果需要高质量的慢动作或将低帧率视频转换为高帧率同时保持流畅度,则需要专门的帧插值软件或滤镜(例如,一些FFmpeg构建版可能包含minterpolate滤镜,但它不属于官方标准且配置复杂,通常需要额外编译)。

  2. 音视频同步:改变视频帧率时,如果音频流的持续时间不变,可能会导致音视频不同步。FFmpeg在重新编码时会尝试保持音视频同步,但如果出现问题,可能需要检查并调整。通常,fps 滤镜会正确处理时间戳,只要音频流被重新编码或以 copy 模式(如果帧率调整不影响音频同步)传递。如果仅调整帧率而不更改时长,音频通常会保持不变。

  3. 与其他滤镜组合fps 滤镜可以作为视频滤镜链的一部分。例如,先缩放再调整帧率:
    '-vf', 'scale=1280:-1,fps=30'
    滤镜的顺序很重要,例如,如果先裁剪再调整帧率,或者先调整帧率再叠加水印,这些操作的先后顺序会影响最终结果。

  4. r 选项与 fps 滤镜的区别

    • 在FFmpeg命令行中,-r 选项也可以用来设置输出帧率,例如 ffmpeg -i input.mp4 -r 30 output.mp4
    • 区别
      • -r 选项是在编码器级别控制输出帧率,它可能导致重复或跳过帧,并且不会像 fps 滤镜那样在内部进行时间戳的精确处理,可能导致不规则的帧率。
      • fps 滤镜 (-vf fps=N) 则是在滤镜图层面进行精确的帧率转换,它会确保输出的帧具有规则的时间戳间隔,并更平滑地处理帧的丢弃或复制。
    • 建议:在进行视频帧率转换时,通常强烈推荐使用 fps 滤镜,因为它提供了更稳定和精确的结果,尤其是在处理复杂的滤镜链时。-r 选项更适合在录制或纯粹的容器层面设置帧率。

理解 fps 滤镜的工作原理和局限性对于正确调整视频帧率至关重要。虽然它不提供高级的帧插值,但对于基本的帧率转换和统一化,它是非常有效和常用的工具。

2.6.3. 视频码率(比特率)调整:质量与文件大小的平衡

视频码率(Bitrate),通常以比特每秒(bps)或兆比特每秒(Mbps)表示,是衡量视频每秒数据量的重要指标。它直接决定了视频的压缩程度、视觉质量和文件大小。更高的码率通常意味着更好的画质和更大的文件。在视频处理中,调整码率是常见的需求,例如:

  • 减小文件大小:通过降低码率来压缩视频,以便于存储、传输或在线播放。
  • 提高画质:为确保视频在特定显示设备上的视觉表现,增加码率。
  • 适配网络带宽:针对不同网络环境(如移动网络、Wi-Fi)调整码率,以优化流媒体播放体验。

FFmpeg提供了 -b:v(视频比特率)和 -crf(恒定码率因子)等选项来控制视频码率。

-b:v (视频比特率) 参数详解

-b:v 参数允许你直接指定目标视频的平均比特率。

  • b:v:
    • 作用:设置视频流的平均目标比特率。
    • 取值:可以是数字,后面跟着单位(如 k 代表千比特,M 代表兆比特)。例如,1M (1 Mbps), 500k (500 kbps)。
    • 内部机制:当使用 -b:v 时,FFmpeg的编码器会尝试将视频的总数据量限制在这个平均比特率附近。这意味着在视频复杂场景(如快速运动)时,比特率可能会暂时升高,而在简单场景时降低,以尽可能保持画质,同时控制整体大小。这是一种VBR (Variable Bitrate) 模式,但以平均目标为基准。

-crf (恒定码率因子) 参数详解

-crf 参数是 H.264 (libx264) 和 H.265 (libx265) 等现代编码器特有的一个强大参数,用于控制输出视频的感知质量,而不是直接控制比特率。它是一种CBR (Constant Bitrate) 模式的变种,但更精确地说是Constant Quality (恒定质量) 模式。

  • crf:
    • 作用:设置一个质量等级。数字越小,质量越高,文件越大;数字越大,质量越低,文件越小。
    • 取值范围:对于 H.264 (libx264),通常在 0 到 51 之间。
      • 0:无损压缩 (非常大文件)。
      • 18-24:常用范围。23 是libx264的默认值,通常被认为是“视觉无损”的起点,即大多数人看不出与原始视频的明显差异。
      • 51:最低质量。
    • 内部机制:编码器会动态调整比特率,以确保视频的每一帧都达到相似的视觉质量水平。这意味着简单场景的比特率较低,复杂场景的比特率较高,从而在整个视频中保持一致的视觉质量。通常,crf 模式比简单的 -b:v 更能有效地在文件大小和感知质量之间找到平衡。

crfb:v 的选择

  • 推荐使用 crf:在大多数情况下,如果你关心的是视频的视觉质量,并且希望在文件大小上有所控制但不严格限制,那么 crf 是更优的选择。它能更好地适应视频内容的复杂性,提供更一致的视觉体验。
  • 何时使用 b:v
    • 当你有严格的文件大小限制时(例如,必须在 100MB 以内)。
    • 当目标平台要求固定的比特率时(例如,某些直播流协议)。
    • 配合 -minrate-maxrate 进行 VBR 编码,并使用 -bufsize 来控制比特率的瞬时波动。

Python 实现视频码率调整

我们将创建Python函数来封装FFmpeg的码率控制选项。

import subprocess # 导入 subprocess 模块,用于执行外部命令
import os         # 导入 os 模块,用于文件路径操作

# 假设 run_ffmpeg_command 函数已在前面定义,这里不再重复定义

def adjust_video_bitrate(input_video_path, output_video_path,
                         bitrate_type='crf',
                         value=23, # 对于 CRF,默认 23;对于 bitrate,默认 '2M'
                         audio_bitrate='128k', preset='medium'):
    """
    调整视频文件的码率,支持 CRF (恒定质量) 或 固定比特率。

    Args:
        input_video_path (str): 输入视频文件的路径。
        output_video_path (str): 输出视频文件的路径。
        bitrate_type (str): 码率控制类型,可选 'crf' (Constant Rate Factor) 或 'bitrate' (Average Bitrate)。
        value (int/str):
            - 如果 bitrate_type 为 'crf',此为 CRF 值 (0-51,数字越小质量越高)。
            - 如果 bitrate_type 为 'bitrate',此为目标比特率字符串 (如 '1M', '500k')。
        audio_bitrate (str): 输出音频的比特率,例如 '128k' (128kbps)。
        preset (str): 编码预设。

    Raises:
        ValueError: 如果 bitrate_type 或 value 无效。
        FileNotFoundError: 如果输入文件不存在。
        RuntimeError: 如果 FFmpeg 命令执行失败。
    """
    if not os.path.exists(input_video_path): # 检查输入文件是否存在
        raise FileNotFoundError(f"输入视频文件不存在: {
     
     input_video_path}") # 抛出文件未找到错误

    print(f"正在调整视频 '{
     
     input_video_path}' 的码率...") # 打印处理开始信息

    video_options = ['-c:v', 'libx264'] # 默认使用 H.264 编码器

    if bitrate_type == 'crf': # 如果码率类型是 CRF
        if not isinstance(value, int) or not (0 <= value <= 51): # 验证 CRF 值范围
            raise ValueError("CRF 值必须是 0 到 51 之间的整数。") # 抛出值错误
        video_options.extend(['-crf', str(value)]) # 添加 -crf 参数
        print(f"使用 CRF 模式,CRF 值为: {
     
     value}") # 打印信息
    elif bitrate_type == 'bitrate': # 如果码率类型是固定比特率
        if not isinstance(value, str): # 验证比特率值是否为字符串
            raise ValueError("比特率值必须是字符串 (例如 '2M', '500k')。") # 抛出值错误
        video_options.extend(['-b:v', value]) # 添加 -b:v 参数
        print(f"使用平均比特率模式,目标比特率为: {
     
     value}") # 打印信息
    else:
        raise ValueError(f"不支持的码率控制类型: {
     
     bitrate_type}。请选择 'crf' 或 'bitrate'。") # 抛出值错误

    video_options.extend(['-preset', preset]) # 添加编码预设

    # 构建 FFmpeg 命令
    command = [
        'ffmpeg',
        '-i', input_video_path,
    ]
    command.extend(video_options) # 添加视频相关选项
    command.extend([
        '-c:a', 'aac',              # 音频编码器
        '-b:a', audio_bitrate,      # 音频比特率
        '-y',                       # 覆盖输出文件
        output_video_path           # 输出文件路径
    ])

    try:
        # 执行 FFmpeg 命令
        return_code, _, _ = run_ffmpeg_command(command, capture_output=False) # 不捕获输出,直接打印到控制台
        if return_code == 0: # 检查命令是否成功执行
            print(f"视频码率调整成功,文件已保存到: {
     
     output_video_path}") # 打印成功信息
        else:
            raise RuntimeError(f"视频码率调整失败,FFmpeg 返回码: {
     
     return_code}") # 抛出错误

    except Exception as e:
        # 捕获任何在命令执行中发生的异常
        print(f"调整视频码率时发生错误: {
     
     e}") # 打印错误信息
        raise # 重新抛出异常

# --- 实际使用示例 ---
if __name__ == "__main__":
    # 请替换为你的实际视频文件路径
    input_video_for_bitrate = "path/to/your/input_video_high_quality.mp4" # 请替换为你的输入视频文件路径

    # 模拟创建测试文件(如果你没有,请替换为真实文件路径)
    if not os.path.exists(input_video_for_bitrate):
        print(f"警告: 测试视频文件 '{
     
     input_video_for_bitrate}' 不存在。请替换为你的实际文件路径或创建它。")
        # 创建一个高质量的测试视频,方便后续压缩
        # subprocess.run([
        #    'ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=10:size=1920x1080:rate=30',
        #    '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', '-preset', 'medium',
        #    input_video_for_bitrate, '-y'
        # ])
        # print(f"已创建测试视频文件: {input_video_for_bitrate}")
        exit("请提供一个真实的视频文件路径来运行码率调整示例。")

    try:
        # 示例 1: 使用 CRF 模式压缩视频,质量略低于默认 (文件更小)
        output_video_crf_28 = "path/to/your/output_video_crf28.mp4" # 输出文件路径
        adjust_video_bitrate(input_video_for_bitrate, output_video_crf_28,
                             bitrate_type='crf', value=28) # CRF 28,文件会比默认 CRF 23 小,质量略有下降
        print("-" * 30) # 分隔线

        # 示例 2: 使用 CRF 模式压缩视频,接近原始质量 (文件较大)
        output_video_crf_18 = "path/to/your/output_video_crf18.mp4" # 输出文件路径
        adjust_video_bitrate(input_video_for_bitrate, output_video_crf_18,
                             bitrate_type='crf', value=18, preset='slow') # CRF 18,文件较大,质量非常高
        print("-" * 30) # 分隔线

        # 示例 3: 使用固定比特率模式压缩视频,目标 1 Mbps
        output_video_1mbps = "path/to/your/output_video_1mbps.mp4" # 输出文件路径
        adjust_video_bitrate(input_video_for_bitrate, output_video_1mbps,
                             bitrate_type='bitrate', value='1M') # 目标平均比特率 1 Mbps
        print("-" * 30) # 分隔线

        # 示例 4: 使用固定比特率模式,目标 500 kbps (更小文件,更低质量)
        output_video_500kbps = "path/to/your/output_video_500kbps.mp4" # 输出文件路径
        adjust_video_bitrate(input_video_for_bitrate, output_video_500kbps<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值