代码实战 · 2026 年 11 月 11 日

用 FFmpeg 批量压缩视频:一条命令省下 80% 空间

手机视频、录屏和素材归档占满硬盘时,FFmpeg 的 CRF、preset 与缩放参数比“随便压一下”重要得多。给出可直接运行的批量脚本、目标体积算法和验证方法。

硬盘告警时,占空间的往往不是代码,而是几十个 GB 的录屏和手机视频。这些文件多数是 4K、高码率、只在一台设备上看过几次。把它们转成 1080p 的 H.264,通常能省下 70% 到 90% 的空间,画质损失在普通显示器上很难察觉。

但「压了就行」是个陷阱:参数选错会把文字录屏压成一团糊,或者让文件比原来还大。下面这套流程先讲清单个文件,再给出可以后台跑的批量脚本。

先看清源文件,再决定目标#

bash
ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,width,height,r_frame_rate,bit_rate \
  -show_entries format=duration,size,bit_rate \
  -of default=noprint_wrappers=1 input.mp4

重点看四个值:分辨率、帧率、视频码率、总时长。一个 4K 30fps、40 Mbps 的十分钟录屏,和一个 1080p 60fps、8 Mbps 的游戏录像,压缩策略完全不同。

源素材 建议 预期体积
4K 手机视频,主要在大屏看 降到 1080p,CRF 22–24 常见省 70–90%
1080p 录屏,含大量文字 保持分辨率,CRF 20–23,避免过度降噪 省 30–60%
已压过的 1080p 网络视频 不建议二次压缩 可能只省 10–30%,且更糊
需要剪辑的中间素材 用 ProRes / 高码率 H.264,不压 不适用

「省 80%」不是承诺,而是量级:源文件码率越高、分辨率越大,下降空间越大;已经压过的文件再压,收益会迅速变小。

单文件:一条可复用的命令#

bash
ffmpeg -hide_banner -i input.mp4 \
  -map 0:v:0 -map 0:a:0? \
  -c:v libx264 -crf 23 -preset medium \
  -vf "scale='min(1920,iw)':-2" \
  -c:a aac -b:a 128k -ac 2 \
  -movflags +faststart \
  -map_metadata 0 \
  output.mp4

逐项解释:

  • -crf 23 是 H.264 的恒定质量模式,18 接近视觉无损,23 是默认平衡点,28 开始明显变糊。录屏文字多,建议 20–22;普通画面 23–25。
  • -preset medium 决定编码速度与压缩率。slow 能再小 5%–10%,但耗时明显增加;批量任务用 medium 更稳。
  • scale='min(1920,iw)':-2 只在宽度超过 1920 时缩小,高度按比例取偶数。不要写死 1920:1080,那会拉伸非 16:9 的画面。
  • -movflags +faststart 把索引移到文件头,网页播放不用先下完整个文件。
  • -map_metadata 0 保留拍摄时间、GPS 等元数据;如果不想要,改成 -map_metadata -1。
  • -map 0:a:0? 的可选标记 ? 表示没有音频也不报错,适合批量处理。

如果源文件是 HDR(HLG / PQ),直接转 SDR 会灰蒙蒙。需要先做色调映射,例如 zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p,再接 scale。这条链路计算量大,建议单独处理,不要混在普通批量脚本里。

批量脚本:跳过已完成、失败不中断#

把下面保存为 compress.sh,放在素材目录的上一级:

bash
#!/usr/bin/env bash
set -uo pipefail

src="${1:?usage: compress.sh <source-dir> [output-dir]}"
src="${src%/}"
out="${2:-${src}/compressed}"
mkdir -p "$out"

find "$src" -type f \
  \( -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' -o -iname '*.avi' \) \
  -print0 |
while IFS= read -r -d '' file; do
  rel="${file#"$src"/}"
  target="$out/${rel%.*}.mp4"
  mkdir -p "$(dirname "$target")"

  if [ -s "$target" ]; then
    echo "skip  $rel"
    continue
  fi

  echo "==>   $rel"
  tmp="${target%.mp4}.part.mp4"
  if ffmpeg -hide_banner -loglevel error -nostdin -y -i "$file" \
      -map 0:v:0 -map 0:a:0? \
      -c:v libx264 -crf 23 -preset medium \
      -vf "scale='min(1920,iw)':-2" \
      -c:a aac -b:a 128k -ac 2 \
      -movflags +faststart -map_metadata 0 \
      "$tmp"; then
    mv "$tmp" "$target"
    printf 'done  %s  %s -> %s\n' "$rel" \
      "$(du -h "$file" | cut -f1)" "$(du -h "$target" | cut -f1)"
  else
    echo "FAIL  $rel"
    rm -f "$tmp"
  fi
done

几个工程细节:

  • set -uo pipefail 不启用 -e,单个文件失败不会中断整批;失败时删除半成品,下次还能重跑。
  • 先写 .part.mp4 再 mv,避免中断后留下一个看似完成、实际无法播放的文件。
  • -nostdin 很重要。ffmpeg 默认会抢终端输入,放进 while read 循环后可能吞掉后续文件名。
  • 输出目录保持相对路径,嵌套目录不会冲突。
  • 已有非空目标就跳过,天然支持断点续跑。

要并行,可以把 find 的输出交给 xargs -0 -P 2 -I{},但机械硬盘或网络盘上并行往往更慢,先测再开。

想精确控制体积:两遍编码#

CRF 控制质量,不控制体积。如果目标是「每个文件不超过 200 MB」,先算目标码率:

bash
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$file")
# 目标 200MB,音频 128kbps;输出 kbps
kbps=$(awk -v mb=200 -v d="$dur" 'BEGIN { printf "%d", mb*8192/d - 128 }')
echo "duration=$dur target_video=${kbps}k"

然后两遍编码:

bash
ffmpeg -y -i "$file" -c:v libx264 -b:v "${kbps}k" -preset medium \
  -pass 1 -passlogfile /tmp/ffpass -an -f null /dev/null
ffmpeg -y -i "$file" -c:v libx264 -b:v "${kbps}k" -preset medium \
  -pass 2 -passlogfile /tmp/ffpass \
  -c:a aac -b:a 128k -movflags +faststart "$target"
rm -f /tmp/ffpass*

两遍编码耗时接近翻倍,但体积可控。码率给得太低时,两遍也救不回画质;200 MB 的十分钟 1080p 大约对应 2.5 Mbps,文字录屏会明显吃力,这种素材应该优先保分辨率、降帧率,而不是硬压码率。

硬件编码与更省的编码器#

bash
# NVIDIA GPU(先确认 ffmpeg -encoders | grep nvenc)
ffmpeg -i input.mp4 -c:v h264_nvenc -preset p5 -cq 23 -b:v 0 \
  -c:a copy output.mp4

# Apple Silicon / macOS
ffmpeg -i input.mp4 -c:v hevc_videotoolbox -q:v 65 -tag:v hvc1 \
  -c:a copy output.mp4

硬件编码快,但同码率下画质通常不如 libx264,而且不同驱动的 -cq 语义不完全一致。归档用途优先软件编码;批量转码、时间敏感的场景再用 GPU。

H.265(libx265)能比 H.264 再省 20%–40%,但兼容性和解码开销更差。给 Apple 设备播放要加 -tag:v hvc1;网页端不要默认用 H.265。AV1(libsvtav1)压缩率更好,但编码慢,老设备不支持。

压完必须验证#

bash
# 容器、时长、码率
ffprobe -v error -show_entries format=duration,size,bit_rate \
  -of default=nw=1 output.mp4

# 抽三帧看画面(0%、50%、90% 处)
for p in 0.1 0.5 0.9; do
  ffmpeg -hide_banner -loglevel error -ss "$p" -i output.mp4 \
    -frames:v 1 -y "/tmp/check-${p}.jpg"
done

如果是同一分辨率的对比,可以用 SSIM 做客观参考:

bash
ffmpeg -i original.mp4 -i output.mp4 \
  -lavfi "[0:v][1:v]ssim" -f null - 2>&1 | tail -3

注意:缩放过分辨率就不能直接比 SSIM,需要先把输出缩放回原尺寸,或者改用 VMAF 并接受它的局限。指标只是参考,文字、皮肤、暗部这些关键画面还是要人眼看。

结论#

  • 先用 ffprobe 看清源文件,再决定分辨率、CRF 和是否值得压;已压过的文件二次压缩收益很小。
  • 普通归档用 libx264 -crf 23 -preset medium,文字录屏把 CRF 降到 20–22,缩放用 min(1920,iw):-2 避免拉伸。
  • 批量脚本要跳过已完成、失败删半成品、用 -nostdin,并且先写临时文件再原子重命名。
  • 需要固定体积时用目标码率 + 两遍编码;硬件编码适合赶时间,不适合追求同码率画质。
  • 压完至少抽帧检查,保留原文件直到确认无误。

完

#FFmpeg#视频#Shell#自动化
回到顶部 ↑