按目标大小压缩视频:码率反推公式、H.264/H.265 实测与 FFmpeg 自动化

前言

需求很朴素:把任意一段视频压到指定的 MB 数以内,比如 Discord 免费用户的 10 MB、Gmail 附件的 25 MB。

这个需求看着简单,但 FFmpeg 并没有 --target-size 25MB 这样的参数。CRF 模式是恒定质量、可变码率,你事先不知道输出多大;ABR 模式虽然能指定码率,但需要你自己把"目标大小"换算成码率,还要处理音频占用、容器开销这些细节。

这篇文章把整个链路讲透:码率反推公式 → 单遍 vs 两遍编码 → H.264/H.265 实测对比 → Python 自动化封装,最后附一份可直接跑的完整脚本。

环境准备

  • Python 3.10+
  • FFmpeg 6.0+(需在 PATH 中,ffmpeg -version 可验证)
  • 无需第三方 Python 库(只用标准库 subprocess / json
bash 复制代码
# macOS
brew install ffmpeg
# Ubuntu
sudo apt install ffmpeg
# Windows (winget)
winget install Gyan.FFmpeg

一、码率反推:核心公式

文件大小与码率的关系是线性的:

复制代码
文件大小(bit) = 总码率(bps) × 时长(s)

换成实用单位(1 MB = 8 × 1024 × 1024 bit = 8388608 bit):

复制代码
总码率(kbps) = 目标大小(MB) × 8192 ÷ 时长(s)

但直接用这个值会超标,因为还有两块开销没算:

  1. 音频码率:AAC 立体声通常 128 kbps,需要从总码率里扣掉
  2. 容器封装开销:MP4 的 moov box、每个 sample 的索引项等,一般占 1-3%

所以实际可用的视频码率是:

python 复制代码
def calc_video_bitrate(target_mb: float, duration: float,
                       audio_kbps: int = 128,
                       overhead: float = 0.03) -> int:
    """根据目标文件大小反推视频码率(kbps)

    Args:
        target_mb: 目标文件大小,单位 MB
        duration: 视频时长,单位秒
        audio_kbps: 音频码率,单位 kbps
        overhead: 容器封装开销比例,默认 3%

    Returns:
        视频码率(kbps),最低不低于 100
    """
    total_kbps = target_mb * 8192 / duration      # 总预算
    total_kbps *= (1 - overhead)                   # 扣除容器开销
    video_kbps = total_kbps - audio_kbps           # 扣除音轨
    return max(int(video_kbps), 100)               # 兜底,防止负数

举个例子:把 252 秒的视频压到 25 MB。

python 复制代码
>>> calc_video_bitrate(25, 252)
660

即视频码率 660 kbps,音频 128 kbps,理论输出约 24.3 MB。

用 ffprobe 拿到时长

python 复制代码
import subprocess, json

def probe(path: str) -> dict:
    """用 ffprobe 读取视频元信息"""
    cmd = [
        "ffprobe", "-v", "error",
        "-print_format", "json",
        "-show_format", "-show_streams",
        path
    ]
    out = subprocess.run(cmd, capture_output=True, text=True, check=True)
    info = json.loads(out.stdout)

    video = next(s for s in info["streams"] if s["codec_type"] == "video")
    return {
        "duration": float(info["format"]["duration"]),
        "size_mb": int(info["format"]["size"]) / 1024 / 1024,
        "width": video["width"],
        "height": video["height"],
        "codec": video["codec_name"],
    }

二、单遍 ABR vs 两遍编码

指定了码率还不够,编码器怎么分配这些码率也影响命中精度。

单遍 ABR(Average Bitrate)

bash 复制代码
ffmpeg -i input.mp4 -c:v libx264 -b:v 660k -c:a aac -b:a 128k out.mp4

编码器边编边估算,开头的帧因为缺少全局信息,码率分配往往不准。实测偏差可能到 ±10%,运动剧烈的素材更差。

两遍编码(2-pass)

第一遍只做分析,把每帧的复杂度写进 log 文件;第二遍据此做全局最优分配。

bash 复制代码
# Pass 1:只分析,不输出(-an 关闭音频加速分析)
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 660k \
       -pass 1 -an -f null /dev/null

# Pass 2:实际编码
ffmpeg -i input.mp4 -c:v libx264 -b:v 660k \
       -pass 2 -c:a aac -b:a 128k out.mp4

Windows 下 /dev/null 要换成 NUL

代价是编码时间翻倍,收益是命中精度提升到 ±2% 左右,并且同码率下画质更好(复杂片段能分到更多码率)。

结论:需要严格卡住文件大小时,必须用两遍编码。

为什么不用 CRF

CRF(Constant Rate Factor)是恒定质量模式,画质一致性最好,但输出大小完全不可控:

bash 复制代码
ffmpeg -i input.mp4 -c:v libx264 -crf 23 out.mp4  # 输出多大?不知道

同样 CRF 23,一段 PPT 录屏可能输出 5 MB,一段滑雪视频可能输出 200 MB。CRF 适合"我要这个画质",ABR 两遍适合"我要这个大小",需求不同。

三、H.264 vs H.265 实测对比

用一段 1080p / 252 秒 / 原始 783 MB 的产品演示视频(含人物讲解 + 屏幕演示,运动量中等)测试,统一目标 25 MB,两遍编码。

测试命令:

bash 复制代码
# H.264 (libx264)
ffmpeg -i in.mp4 -c:v libx264 -preset medium -b:v 660k -pass 2 \
       -c:a aac -b:a 128k out_h264.mp4

# H.265 (libx265)
ffmpeg -i in.mp4 -c:v libx265 -preset medium -b:v 660k -x265-params pass=2 \
       -c:a aac -b:a 128k out_h265.mp4

实测结果(测试机:i7-12700 / 无硬件加速):

编码器 目标 实际输出 偏差 编码耗时 VMAF 分数
libx264 (1-pass) 25 MB 27.4 MB +9.6% 1m48s 78.2
libx264 (2-pass) 25 MB 25.3 MB +1.2% 3m22s 81.5
libx265 (2-pass) 25 MB 24.9 MB −0.4% 9m47s 87.1

VMAF 是 Netflix 开源的画质评估指标,满分 100,越高越接近原片。测量命令:

ffmpeg -i out.mp4 -i in.mp4 -lavfi libvmaf -f null -

几个可以确认的结论:

1. 同码率下 H.265 画质明显更好,VMAF 高出约 5.6 分。换个说法:要达到同样画质,H.265 大约只需要 H.264 六到七成的码率。这跟官方宣称的"节省 50%"有差距,实际收益取决于素材类型,静态画面收益更大。

2. H.265 编码耗时是 H.264 的近 3 倍 。这是纯软编的数据,如果启用硬件编码(hevc_nvenc / hevc_videotoolbox)能快一个数量级,但画质会掉一些。

3. 兼容性是 H.265 的最大问题 。Safari 和新版 Chrome 支持,但老设备、部分 Android 机型、微信内置浏览器仍有播放失败的风险。如果压出来是要发给不确定环境的人看,老老实实用 H.264。

四、完整脚本

把上面的逻辑封装成一个可直接用的命令行工具:

python 复制代码
#!/usr/bin/env python3
"""compress.py --- 按目标文件大小压缩视频

用法:
    python compress.py input.mp4 25            # 压到 25MB,H.264
    python compress.py input.mp4 10 --hevc     # 压到 10MB,H.265
    python compress.py input.mp4 25 --scale 720
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path

NULL_DEV = "NUL" if os.name == "nt" else "/dev/null"


def probe(path: str) -> dict:
    """读取视频元信息"""
    cmd = ["ffprobe", "-v", "error", "-print_format", "json",
           "-show_format", "-show_streams", path]
    out = subprocess.run(cmd, capture_output=True, text=True, check=True)
    info = json.loads(out.stdout)
    video = next(s for s in info["streams"] if s["codec_type"] == "video")
    return {
        "duration": float(info["format"]["duration"]),
        "size_mb": int(info["format"]["size"]) / 1024 / 1024,
        "width": video["width"],
        "height": video["height"],
    }


def calc_video_bitrate(target_mb: float, duration: float,
                       audio_kbps: int = 128, overhead: float = 0.03) -> int:
    """反推视频码率(kbps)"""
    total = target_mb * 8192 / duration * (1 - overhead)
    return max(int(total - audio_kbps), 100)


def suggest_scale(bitrate_kbps: int, src_height: int) -> int:
    """根据码率推荐合理的输出高度

    码率过低时必须同步降分辨率,否则每像素分到的数据太少会出现色块。
    """
    if bitrate_kbps < 800:
        target = 480
    elif bitrate_kbps < 2000:
        target = 720
    elif bitrate_kbps < 5000:
        target = 1080
    else:
        return src_height          # 码率充足,保持原分辨率
    return min(target, src_height)  # 不做放大


def compress(src: str, target_mb: float, hevc: bool = False,
             scale: int | None = None, audio_kbps: int = 128) -> str:
    meta = probe(src)
    vb = calc_video_bitrate(target_mb, meta["duration"], audio_kbps)
    height = scale or suggest_scale(vb, meta["height"])

    encoder = "libx265" if hevc else "libx264"
    dst = str(Path(src).with_name(f"{Path(src).stem}_{int(target_mb)}mb.mp4"))

    print(f"[info] 源文件 {meta['size_mb']:.1f}MB / {meta['duration']:.0f}s "
          f"/ {meta['width']}x{meta['height']}")
    print(f"[info] 目标 {target_mb}MB → 视频码率 {vb}kbps / 输出高度 {height}p")

    # 缩放滤镜:宽度自动取偶数(编码器要求)
    vf = f"scale=-2:{height}"

    # ---- Pass 1 ----
    p1 = ["ffmpeg", "-y", "-i", src, "-c:v", encoder, "-b:v", f"{vb}k",
          "-vf", vf, "-preset", "medium"]
    p1 += (["-x265-params", "pass=1"] if hevc else ["-pass", "1"])
    p1 += ["-an", "-f", "null", NULL_DEV]

    print("[1/2] 分析中...")
    subprocess.run(p1, check=True, stdout=subprocess.DEVNULL,
                   stderr=subprocess.DEVNULL)

    # ---- Pass 2 ----
    p2 = ["ffmpeg", "-y", "-i", src, "-c:v", encoder, "-b:v", f"{vb}k",
          "-vf", vf, "-preset", "medium"]
    p2 += (["-x265-params", "pass=2"] if hevc else ["-pass", "2"])
    p2 += ["-c:a", "aac", "-b:a", f"{audio_kbps}k",
           "-movflags", "+faststart", dst]

    print("[2/2] 编码中...")
    subprocess.run(p2, check=True, stdout=subprocess.DEVNULL,
                   stderr=subprocess.DEVNULL)

    # 清理两遍编码产生的 log
    for f in Path(".").glob("ffmpeg2pass*"):
        f.unlink(missing_ok=True)
    for f in Path(".").glob("x265_2pass*"):
        f.unlink(missing_ok=True)

    out_mb = Path(dst).stat().st_size / 1024 / 1024
    ratio = (1 - out_mb / meta["size_mb"]) * 100
    print(f"[done] {dst} --- {out_mb:.2f}MB (压缩 {ratio:.1f}%)")
    return dst


def main():
    ap = argparse.ArgumentParser(description="按目标大小压缩视频")
    ap.add_argument("input", help="输入视频路径")
    ap.add_argument("target", type=float, help="目标大小(MB)")
    ap.add_argument("--hevc", action="store_true", help="使用 H.265 编码")
    ap.add_argument("--scale", type=int, help="强制输出高度,如 720")
    ap.add_argument("--audio", type=int, default=128, help="音频码率(kbps)")
    args = ap.parse_args()

    if not Path(args.input).exists():
        sys.exit(f"文件不存在: {args.input}")

    try:
        compress(args.input, args.target, args.hevc, args.scale, args.audio)
    except subprocess.CalledProcessError as e:
        sys.exit(f"FFmpeg 执行失败: {e}")


if __name__ == "__main__":
    main()

运行效果

复制代码
$ python compress.py demo.mp4 25
[info] 源文件 783.2MB / 252s / 1920x1080
[info] 目标 25.0MB → 视频码率 660kbps / 输出高度 480p
[1/2] 分析中...
[2/2] 编码中...
[done] demo_25mb.mp4 --- 25.31MB (压缩 96.8%)

注意脚本自动把分辨率降到了 480p------因为 660 kbps 配 1080p 会严重色块化。如果你不接受这个降级,只能提高目标大小,这是物理约束,不是工具问题

五、什么时候不该自己写脚本

上面这套方案的适用前提是:你有 FFmpeg 环境、能跑 Python、且要批量处理

如果是下面这些情况,脚本反而是负担:

  • 偶尔压一两个文件:装 FFmpeg + 调参数的时间,比压缩本身长得多
  • 要给非技术同事用:你没法让运营同学跑命令行
  • 在没有开发环境的机器上(客户现场、iPad、临时借用的电脑)

这时候浏览器方案更实际。我平时应急会用 VideoCompress 这类在线工具,它的交互逻辑跟本文的思路一致------直接填目标 MB 数,服务端去做码率反推和编码,省掉了自己算的步骤;也提供高级模式手动调清晰度和画面尺寸。支持 40 多种输入格式,导出不带水印,另外附带裁剪、剪辑、转 MP3、视频转文字这些配套功能。

选型时要注意的边界,跟所有云端方案一样:

维度 本地 FFmpeg 在线工具
文件上限 有(VideoCompress 免费 1GB / 付费 10GB)
隐私 文件不出本机 需上传(其说明为完成后 24h 内自动删除)
批量自动化 强,可脚本化
环境依赖 需安装配置 浏览器即可
参数可控性 完全可控 受界面暴露的选项限制

涉密素材、需要接进 CI/CD 的流程,用本地方案;一次性任务、临时环境,用在线工具。没必要非此即彼。

六、几个工程实践上的坑

1. -movflags +faststart 别忘了加。 它把 moov box 移到文件开头,让视频能边下边播。不加的话,网页上播放要等整个文件下载完,体验差很多。

2. 别对已经压过的视频二次压缩。 有损编码是不可逆的,二次压缩是在已经丢失信息的基础上继续丢,画质衰减速度远快于体积缩减。管线设计上要保证始终从母版素材出发

3. 先剪辑再压缩。 文件大小和时长成正比。剪掉 30% 的无效片段,比在码率上死磕划算得多,而且不损失保留部分的画质。

4. 分辨率和码率必须匹配。 这是最常见的错误------保持 1080p 硬压到极低码率,结果满屏色块。宁可降到 720p 保住码率密度。

5. 两遍编码的 log 文件会污染工作目录。 ffmpeg2pass-0.logx265_2pass.log 这些要记得清理,并发跑多个任务时还要用 -passlogfile 指定不同路径,否则会互相覆盖。

总结

按目标大小压缩视频,核心就三步:

  1. ffprobe 拿时长 → 用 目标MB × 8192 ÷ 时长 − 音频码率 反推视频码率
  2. 根据码率同步下调分辨率,保证每像素码率密度
  3. 两遍 ABR 编码精确命中目标

编码器选择上,追求体积效率和画质用 H.265,追求兼容性和编码速度用 H.264。要严格卡大小就别用 CRF。

工具选择上没有唯一答案------批量、涉密、要自动化就用 FFmpeg;一次性、无环境、给非技术同事用就走在线工具。搞清楚约束条件比纠结工具重要。

参考资料

标签:视频压缩、FFmpeg、H.265、音视频开发、Python自动化

相关推荐
阿童木写作2 小时前
TikTok Shop视频翻译工具实战测评
人工智能·python·音视频
DogDaoDao2 小时前
【论文解读】系数能量引导的快速变换算法:Beyond VVC 编码器优化新思路
音视频·视频编解码·ecm·h266·vvc·变换编码·beyond vvc
美港探案3 小时前
MiniMax H3 炸场!视频大模型拼“真实生产”时代到来
音视频
AI创界者16 小时前
AIGC进阶】Sulphur-2 视频生成大模型离线实战:文生视频/图生视频本地一键部署整合包解压即用与调优指南
人工智能·aigc·音视频
ldsweet20 小时前
HarmonyOS NEXT 音频播放器开发:AVPlayer 封装、播放列表与后台播放实战
华为·音视频·harmonyos
林澈在路上20 小时前
2026 AI 写歌 APP 推荐:国产软件哪个好用实测
大数据·人工智能·aigc·音视频·音频
湿滑路面20 小时前
VideoPipe中集成多模态大模型做视频(图片)分析
人工智能·算法·音视频
数字会议深科技1 天前
自主音视频会议核心技术沉淀:两项发明专利赋能专业会务设备稳健运行
人工智能·音视频·音频·国际会议·会议解决方案·会议讨论系统·高端会议
2zcode1 天前
抑郁症多模态音视频与脑电信号数据集
音视频·情绪识别·抑郁症·心理健康分析