香港服务器Linux下FFmpeg转码优化实操教程:x264/NVENC/HLS 参数模板与脚本,全面提升短视频上传速度与首屏起播体验

凌晨 1 点,香港荃湾机房监控面板上,用户端“起播时间”曲线在香港本地时段突然抬头:从 1.2s 爬到了 2.8s。运营群里已经有人在问了。我很清楚,问题不在 CDN,也不在播放器,而是在我们这台专门负责入库转码的 Linux 服务器上:码率不稳、GOP 不对齐、封装没做 faststart,导致首包大,播放器起播拖泥带水。
那一夜,我从 BIOS 到内核、从 FFmpeg 源码编译到参数矩阵,一层层把瓶颈拆开来。下面就是当时我梳理并落地的完整实操笔记。它不是“泛讲原理”,而是我在香港机房里趴着敲出来、第二天就把起播拉回去的那一套实践。希望你照着做,也能获得同样的“顺滑”。
目标与约束(先把“好”定义清楚)
业务指标(VOD 短视频):
- 上传完成到首帧可播 ≤ 1.5s(香港本地)
- 体感清晰度不比原片差:1080p/720p 梯度清晰,竖屏 9:16 为主
- 码率控制:同分辨率体感不抖动,缓冲少;蜂窝网络不卡顿
- 兼容性:iOS/Android/低端机/低版本浏览器都能播
约束:
- 以CPU 转码为主(可选 NVENC),峰值 200 并发入库
- 输出 MP4(点播首发)与 HLS/DASH(CDN 分发),首屏快优先
现场环境与推荐硬件
我手里的两套典型配置(都在香港节点租用过):
| 用途 | 型号建议 | 核心/线程 | 内存 | 磁盘 | 网卡 |
|---|---|---|---|---|---|
| CPU 转码节点 | AMD EPYC 7xx2/7xx3 或 Intel Xeon Silver/Gold 3rd+ | 32C/64T 起 | 128–256GB | NVMe SSD (读写 3GB/s+),系统盘+数据盘分离 | 10GbE |
| GPU 混编节点(可选) | NVIDIA L4/T4/A10(主要做 1080p 及以下) | 16C/32T 起 | 128GB | 同上 | 10GbE |
经验:香港本地回源/上传尖峰集中在晚 8–11 点,NVMe 的小文件 QPS比顺序吞吐更关键;同时 10GbE 让你有余量做并行切片与回源。
系统基础调优(别带着“限速器”跑)
1) BIOS/固件
关 C-States 深度节能、开 Performance 模式、NUMA 设为本地优先。
2) 内核与网络(/etc/sysctl.d/99-tune.conf)
net.core.somaxconn = 4096
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 1048576
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
# 生效
sysctl --system
ulimit -n 1048576
3) I/O 与文件系统
- noatime 挂载,转码缓存目录放 NVMe;临时文件走 tmpfs(注意容量上限)。
- 大文件合并写:避免碎片,转码输出先到本地,再异步上传对象存储。
FFmpeg:我这次选择的“通用构建”与编译要点
如果你用发行版 FFmpeg,很多库不是最佳版本或缺失(例如 libsvtav1、libfdk_aac)。我倾向于自己编译一套静态二进制,可移植、可控。
编译(以 CentOS 7/Ubuntu LTS 通用思路)
关键库:x264、x265(或 svt-hevc)、svt-av1(可选)、aom、libfdk_aac(注:许可)、libopus、libvmaf。
典型 configure 片段:
./configure --prefix=/opt/ffmpeg --pkg-config-flags="--static" \
--extra-cflags="-I/usr/local/include" --extra-ldflags="-L/usr/local/lib" \
--enable-gpl --enable-version3 \
--enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libaom \
--enable-libfdk-aac --enable-libopus \
--enable-libvmaf --enable-nonfree
make -j$(nproc) && make install
提示:libfdk_aac 是非自由许可,商用请确认合规;否则用内置 aac 编码器(质量稍逊)。
短视频“转码模版”:横屏与竖屏都吃得下
1) 分辨率与码率梯度(VOD 显示类短视频)
| 级别 | 分辨率(横/竖) | H.264 目标码率 | 音频 | 适用场景 |
|---|---|---|---|---|
| L1 | 426x240 / 360x640 | 300–450 kbps | AAC-LC 64 kbps | 2G/弱网 |
| L2 | 640x360 / 540x960 | 600–900 kbps | AAC-LC 96 kbps | 主流低端机 |
| L3 | 854x480 / 720x1280 | 1.0–1.5 Mbps | AAC-LC 128 kbps | 主流 |
| L4 | 1280x720 / 1080x1920 | 2.2–3.0 Mbps | AAC-LC 128–160 kbps | Wi-Fi/优质网 |
| L5(可选) | 1920x1080 / 1440x2560 | 4–5 Mbps | AAC-LC 160 kbps | 高清上传备份 |
竖屏优先:9:16 模式主打 720×1280、1080×1920 两档;横屏按素材选择。
2) GOP、关键帧与切片对齐
GOP = 2s(起播快与 seeking 友好),帧率 30fps 则 -g 60,-keyint_min 60;HLS/DASH 分片=2s 或 4s,强制关键帧对齐。
码控:CRF + VBV(约束峰值),或两遍 VBR(更稳定的大小/质量)。
FFmpeg 命令模板(可直接落地)
约定:输入 in.mp4,输出自适应多码率 MP4(首发)与 HLS(分发)。请根据你的目录调整。
A. CPU(x264)竖屏 1080×1920 + 720×1280,两遍 VBR
# 1080x1920
ffmpeg -y -hwaccel auto -i in.mp4 \
-vf "scale=w=1080:h=1920:force_original_aspect_ratio=decrease,\
format=yuv420p,fps=30" \
-c:v libx264 -preset slow -profile:v high -level 4.1 \
-b:v 2800k -maxrate 3200k -bufsize 5600k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-pass 1 -an -f mp4 /dev/null
ffmpeg -y -hwaccel auto -i in.mp4 \
-vf "scale=w=1080:h=1920:force_original_aspect_ratio=decrease,\
format=yuv420p,fps=30" \
-c:v libx264 -preset slow -profile:v high -level 4.1 \
-b:v 2800k -maxrate 3200k -bufsize 5600k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 160k -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
out_1080x1920.mp4
# 720x1280
ffmpeg -y -i in.mp4 \
-vf "scale=w=720:h=1280:force_original_aspect_ratio=decrease,\
format=yuv420p,fps=30" \
-c:v libx264 -preset medium -profile:v high -level 4.0 \
-b:v 1600k -maxrate 1900k -bufsize 3200k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
out_720x1280.mp4
B. 快速入库(CRF + VBV,一遍通吃)
当量大、时间紧,我直接:
ffmpeg -y -i in.mp4 \
-vf "scale=w=720:h=1280:force_original_aspect_ratio=decrease,format=yuv420p,fps=30" \
-c:v libx264 -preset veryfast -profile:v high -level 4.0 \
-crf 22 -maxrate 1800k -bufsize 3000k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
out_fast_720x1280.mp4
经验:-crf 21~23 配合 -maxrate/-bufsize 能把峰值控住,弱网更稳。
C. HLS(CMAF/LL-HLS 友好),2 秒切片、关键帧对齐
ffmpeg -y -i in.mp4 \
-vf "scale=720:-2:force_original_aspect_ratio=decrease,format=yuv420p,fps=30" \
-c:v libx264 -preset medium -profile:v high -level 4.0 \
-b:v 1600k -maxrate 1900k -bufsize 3200k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-hls_time 2 -hls_playlist_type vod -hls_list_size 0 \
-hls_flags independent_segments \
-master_pl_name master.m3u8 \
-f hls hls_720/stream.m3u8
D. NVENC(可选,1080p 速度拉满)
ffmpeg -y -hwaccel cuda -i in.mp4 \
-vf "scale_npp=1080:1920:interp_algo=lanczos,format=yuv420p,fps=30" \
-c:v h264_nvenc -preset p5 -rc vbr -cq 22 \
-b:v 2600k -maxrate 3200k -bufsize 5600k \
-g 60 -bf 2 \
-c:a aac -b:a 160k -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
out_nvenc_1080x1920.mp4
NVENC 注意:-tune hq 有时会拉大起播首包;B 帧别设太多(-bf 2 足够)。
上传体验优化:真正有用的三个开关
- -movflags +faststart:把 moov 提前,上传完即播,播放器无需拉到文件尾。
- 音频采样与通道统一:-ar 48000 -ac 2,避免某些安卓机解码“抽风”。
- 像素格式:-pix_fmt yuv420p(上面通过 format 已保证),兼容性最佳。
另外:竖屏旋转别偷懒只写 metadata(rotate 标签),很多旧机不认。稳妥做法是直接转正:-vf "transpose=1" 或在 scale 前处理。
生产化:队列并发与系统化托管
我用 systemd + 队列工作进程 的方式跑转码(比临时脚本更稳)。
systemd service(/etc/systemd/system/vod-worker@.service)
[Unit]
Description=VOD Transcoder Worker %i
After=network.target
[Service]
Type=simple
User=transcode
ExecStart=/usr/local/bin/vod-worker.sh %i
Restart=always
RestartSec=2
LimitNOFILE=1048576
CPUWeight=90
IOSchedulingClass=best-effort
IOSchedulingPriority=2
[Install]
WantedBy=multi-user.target
简易 worker(/usr/local/bin/vod-worker.sh)
#!/usr/bin/env bash
set -euo pipefail
ID="$1"
QUEUE_DIR="/data/queue"
OUT_DIR="/data/out"
while true; do
JOB=$(inotifywait -e create,move -t 5 --format '%f' "$QUEUE_DIR" || true)
[[ -z "${JOB:-}" ]] && continue
SRC="$QUEUE_DIR/$JOB"
BASENAME="${JOB%.*}"
# 封面与关键帧
ffmpeg -y -ss 00:00:01 -i "$SRC" -frames:v 1 -q:v 2 "$OUT_DIR/${BASENAME}_poster.jpg"
# 720p 快速入库版
ffmpeg -y -i "$SRC" \
-vf "scale=w=720:h=1280:force_original_aspect_ratio=decrease,format=yuv420p,fps=30" \
-c:v libx264 -preset veryfast -crf 22 -maxrate 1800k -bufsize 3000k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
"$OUT_DIR/${BASENAME}_720x1280.mp4"
# 异步上传(示例)
rclone copy "$OUT_DIR/${BASENAME}_720x1280.mp4" remote:vod/${BASENAME}/ &
done
启 8 个并发:
systemctl enable --now vod-worker@{1..8}.service
质量与性能监控(别等用户告诉你“糊了”)
1) 入库质量自动验收
ffprobe -v error -select_streams v:0 -show_entries \
stream=width,height,avg_frame_rate,bit_rate,codec_name,pix_fmt \
-of json out_720x1280.mp4 | jq .
检查 pix_fmt=yuv420p、codec_name=h264、帧率与分辨率是否达标。
2) 起播数据:自埋点 + Nginx 日志
- 记录首片时延、首缓冲、rebuffer 次数。
- Nginx/Fluentd 抽样上报,按运营时段(20:00–23:00)看分布。
这次现场的坑:怎么踩、怎么解
GOP 与分片不对齐 → HLS 首片“半 GOP”导致花屏/起播慢。
解法:-g 60 -keyint_min 60 -sc_threshold 0 并加 -hls_flags independent_segments。
仅写了旋转 metadata(竖屏素材),老安卓横着播。
解法:transpose 或直接 scale 到竖屏并转正;验收时 ffprobe 看 displaymatrix。
CRF 单跑峰值乱飙,蜂窝用户卡顿。
解法:CRF + -maxrate/-bufsize(VBV)约束峰值;或高价值内容走两遍 VBR。
moov 在尾 导致“传完才播”。
解法:-movflags +faststart;合并上传前做一次 qt-faststart 类处理也可。
音频 HE-AAC 兼容性在部分旧设备差。
解法:统一 AAC-LC,码率 96–160 kbps。
颜色偏灰/发白:源是全量程(full range),转码后被当作 limited。
解法:滤镜里显式 -color_range tv 或 -vf scale=in_range=full:out_range=tv 视源而定,并打上 -colorspace/-color_primaries 标签。
VFR 可变帧率素材 导致音画不同步。
解法:统一 CFR:-r 30 -vsync cfr;或 fps=30 放在滤镜里。
我用过、对比过的参数矩阵(示例数据,供参考)
同一台 32C/64T CPU 节点,100 条 30–60s 竖屏素材,单位:每路 FPS(越高越好)
| 编码器 | preset | 分辨率 | 方式 | 画质主观评分* | 每路 FPS |
|---|---|---|---|---|---|
| libx264 | veryfast | 720×1280 | CRF 22 + VBV | 7.8/10 | 180–220 |
| libx264 | medium | 720×1280 | 2-pass 1.6Mbps | 8.4/10 | 95–120 |
| libx264 | slow | 1080×1920 | 2-pass 2.8Mbps | 8.9/10 | 45–60 |
| h264_nvenc | p5 | 1080×1920 | VBR cq=22 | 8.2/10 | 280–350 |
| libsvtav1 | 6 | 720×1280 | CRF 30 | 8.7/10 | 18–25 |
*主观评分来自双盲对看 + VMAF 参考(VMAF>92 视为优秀)。你的数据可能不同,但趋势大体一致:x264 medium 是耐打的中庸解;NVENC 在 1080p 的速度/能效有优势;AV1 质量高但慢,适合离线精品。
批量处理脚本(把“套路”固化)
Bash:生成多清晰度 MP4 + HLS
#!/usr/bin/env bash
set -euo pipefail
IN="$1"; NAME="${IN%.*}"
declare -A LADDERS=(
["1080x1920"]="2800k 3200k 5600k 160k"
["720x1280"]="1600k 1900k 3200k 128k"
["540x960"]="900k 1100k 1800k 96k"
)
for RES in "${!LADDERS[@]}"; do
read BV MR BS BA <<< "${LADDERS[$RES]}"
ffmpeg -y -i "$IN" \
-vf "scale=${RES%%x*}:${RES##*x}:force_original_aspect_ratio=decrease,format=yuv420p,fps=30" \
-c:v libx264 -preset medium -profile:v high -level 4.1 \
-b:v "$BV" -maxrate "$MR" -bufsize "$BS" \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a "$BA" -ar 48000 -ac 2 \
-movflags +faststart -brand mp42 \
"${NAME}_${RES}.mp4"
done
# HLS 720p
mkdir -p "${NAME}_hls_720"
ffmpeg -y -i "${NAME}_720x1280.mp4" \
-c copy -hls_time 2 -hls_playlist_type vod -hls_list_size 0 \
-hls_flags independent_segments \
-master_pl_name master.m3u8 \
-f hls "${NAME}_hls_720/stream.m3u8"
交付检查清单(上线前最后一眼)
- 输出 MP4 均为 yuv420p,moov 在前(+faststart)
- GOP=2s,HLS 切片 = 2s,independent_segments 开
- 音频 AAC-LC、48kHz、双声道
- 竖屏素材已转正,无 rotate 悬挂
- 起播 P50 ≤ 1.5s、P90 ≤ 2.2s(晚高峰)
- Nginx/对象存储带宽峰值 < 链路 70%,无 5xx 抖动
那晚忙完,机房外面还在下雨。我用手机随便点开了平台上的一个视频,1 秒不到就亮了第一帧。那一刻我知道,这套参数和流程是对的。第二天运营看数——起播回到了 1.2s,投诉清零。
我常说,FFmpeg 不是“用不用”的问题,而是“怎么用”的问题:先把业务指标扣实,再围绕指标做系统、编解码、封装、分发的每一处小优化。这些小优化,叠起来就是用户看到的“顺滑”。如果你也在香港或者任何边缘节点折腾短视频,不妨照着这份清单走一遍,然后根据你自己的素材和客群,再把它打磨得更锐一点。