☰
PHP7.4怎么用ffmpeg把视频转成av1格式
2026/10/18 2:36:23 网站建设 项目流程

前言


AV1(AOMedia Video 1)是免版税的开放视频编码,同画质下比 H.264 体积小很多,放在“用户上传视频、后台统一压缩存储”的场景里很有吸引力。于是很多 PHP 项目会接到这样一个需求:上传的视频在后台转成 AV1 再入库。


真做起来,症状通常很有代表性:



  • ffmpeg直接退出,日志只有一句Unknown encoder 'libaom-av1';

  • 换成libsvtav1还是找不到,但网上的教程都说“直接加-c:v libsvtav1就行”;

  • 转一个 10 分钟的视频,CPU 跑满几个小时,PHP 脚本早就超时了;

  • 转是转出来了,.mp4文件在部分播放器里只有声音没画面;

  • 明明写了“质量参数”,出来的文件大小和预期差了一个量级。


核心原因有两个:AV1 编码器全是 ffmpeg 的外部依赖库,ffmpeg -encoders里没有就是没有,跟 PHP 一点关系都没有;以及 AV1 的编码参数体系跟 x264 习惯完全不同,把-preset veryfast、-crf 30那套直接搬过来,要么报错,要么语义完全变了。


本文以 PHP 7.4 为运行环境,讲清楚三件事:怎么确认系统里到底有哪个 AV1 编码器、AV1 该怎么选容器和参数、以及 PHP 侧该怎样组织这条又长又重的任务。需要提醒的是,PHP 7.4 的官方安全支持已于 2022 年 11 月结束,新项目请直接使用 8.x;本文代码只用了 7.4 已有的语法(箭头函数、类型属性、??=),在 8.x 上同样能跑。


一、AV1 编码器来自哪里:全是外部库


ffmpeg 本身不实现 AV1 编码,它是通过外部库来提供编码器的,常见的三个是:




编码器名来源库特点参数风格



libsvtav1SVT-AV1(Intel 与 Netflix 主导)并行化程度高,工程上最实用-preset 0..13,数字越大越快

libaom-av1libaom(AOMedia 官方参考实现)质量打磨充分,编码很慢-cpu-used 0..8,配合-crf/-b:v

librav1erav1e(Rust 实现)生态较小,速度中等-speed、-qp



第一个必须建立的认知是:这些名字必须出现在ffmpeg -encoders的输出里才能用。发行版打包的 ffmpeg 通常会带上 aom 和 SVT-AV1,但从某个源码构建的静态 ffmpeg、或者某些精简镜像里的 ffmpeg 很可能只有解码器(libdav1d)而没有编码器。判断方法就是在部署机上跑一次:


ffmpeg -hide_banner -encoders 2>/dev/null | grep -i av1

用 PHP 做体检就是把这条命令交给proc_open()的数组形式执行,再逐行匹配编码器名字(完整实现见第四节的Av1Transcoder::encoders())。这里有一个 PHP 7.4 的细节:str_contains()是 PHP 8.0 才加入的,7.4 上匹配子串必须用strpos($line, $name) !== false,否则会直接Fatal error。


每个编码器支持的参数并不完全一致,用ffmpeg -h encoder=libsvtav1可以打印出它接受的选项列表,遇到“参数不识别”的报错时先看这份输出,比搜索更快。


二、容器怎么选:WebM 还是 MP4


AV1 既可以放进 WebM,也可以放进 MP4,但两者的搭配习惯不同:




目标容器视频编码推荐音频编码兼容性说明



.webmAV1libopus历史上就是 WebM 的组合,浏览器支持最一致

.mp4AV1(av01)AAC需要较新的 muxer 与播放器,老播放器可能只有声无画

.mkvAV1任意归档场景最省心,但浏览器不直接播



两条实践结论:需要给浏览器直接播放的,优先.webm+libopus;需要进入既有 MP4 流程的,就用.mp4+ AAC,但要接受部分老客户端播放不了的事实。不要把 Opus 塞进 MP4 再把扩展名写成.mp4交付——容器与编码的匹配关系一旦错位,排查成本极高。


三、参数:最容易被 x264 习惯带偏的地方


AV1 的质量控制主要有两种思路:恒定质量(CRF/QP)和恒定码率。绝大多数压缩场景应该用恒定质量。


最容易踩的三个点:


第一个,libaom-av1里-crf必须配合-b:v 0。在 libaom 中,只要码率上限不为 0,-crf就退化成“带码率上限的 VBR”,最终产物并不是你想要的恒定质量结果。写成-crf 30 -b:v 0才是真正的恒定质量模式。


第二个,SVT-AV1 的-preset方向和 x264 相反。x264 的-preset从ultrafast到veryslow,名字越“快”越快;SVT-AV1 用的是 0 到 13 的数字,数字越小越慢、压缩率越高,数字越大越快。把-preset veryfast交给libsvtav1只会得到参数错误。常用区间是 4 到 8,越往小走耗时增长越陡。


第三个,AV1 只管视频。-c:v libsvtav1之后音频必须单独指定,否则会走默认编码器,产物可能比预期大很多或者不被容器接受。


另外,CRF 的取值范围是 0 到 63(不是 x264 的 0 到 51),越大画质越低、体积越小。具体取多少要看素材,建议先用一小段样片在 28、32、38 三个值上各转一次,肉眼确认后再批量跑——这比盲信任何一篇“最佳参数”文章都可靠。


四、完整可运行示例


下面这个脚本做了四件事:探测可用的 AV1 编码器、按编码器生成正确的参数、带进度解析地执行、用ffprobe校验产物真的是 AV1。


php transcode_av1.php input.mp4 output.webm

<?php
declare(strict_types=1);
// 运行环境:PHP 7.4+,ffmpeg 需带 libsvtav1 或 libaom-av1

final class Av1Transcoder
{
/** @var string */
private $ffmpeg;
/** @var string */
private $ffprobe;

public function __construct(string $ffmpeg = 'ffmpeg', string $ffprobe = 'ffprobe')
{
$this->ffmpeg = $ffmpeg;
$this->ffprobe = $ffprobe;
}

/** @return string[] */
public function encoders(): array
{
$r = $this->run([$this->ffmpeg, '-hide_banner', '-encoders'], 30);
$priority = ['libsvtav1' => 0, 'libaom-av1' => 1, 'librav1e' => 2];
$found = [];
foreach (explode("\n", $r['stdout']) as $line) {
foreach ($priority as $name => $rank) {
if (strpos($line, $name) !== false) {
$found[$name] = $rank;
}
}
}
asort($found);
return array_keys($found);
}

/**
* 按编码器生成不同的质量参数;$crf 为 0-63,越大画质越低
* @return string[]
*/
public function buildArgs(string $input, string $output, string $encoder, int $crf = 32): array
{
$args = [$this->ffmpeg, '-hide_banner', '-nostdin', '-y', '-i', $input, '-c:v', $encoder];

if ($encoder === 'libsvtav1') {
// preset 数字越大越快,6 是一个偏快的中间档
$quality = ['-preset', '6', '-crf', (string) $crf];
} elseif ($encoder === 'libaom-av1') {
// -b:v 0 必须写,否则 -crf 会退化成带上限的 VBR
$quality = ['-crf', (string) $crf, '-b:v', '0', '-cpu-used', '4', '-row-mt', '1'];
} elseif ($encoder === 'librav1e') {
$quality = ['-qp', (string) $crf, '-speed', '6'];
} else {
throw new InvalidArgumentException('不支持的编码器: ' . $encoder);
}
$args = array_merge($args, $quality);

$ext = strtolower(pathinfo($output, PATHINFO_EXTENSION));
$audio = ($ext === 'mp4')
? ['-c:a', 'aac', '-b:a', '128k']
: ['-c:a', 'libopus', '-b:a', '96k'];
$args = array_merge($args, $audio);

if ($ext === 'mp4') {
// 让 moov box 前置,便于边下边播
$args = array_merge($args, ['-movflags', '+faststart']);
}

return array_merge($args, ['-progress', 'pipe:1', '-nostats', $output]);
}

/**
* @param string[] $args
* @param callable|null $onProgress 形如 function (array $p): void
* @return array{code:int,stdout:string,stderr:string}
*/
public function run(array $args, int $timeoutSeconds = 7200, ?callable $onProgress = null): array
{
$proc = proc_open($args, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($proc)) {
throw new RuntimeException('proc_open 失败');
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

$stdout = '';
$stderr = '';
$pending = '';
$progress = [];
$deadline = microtime(true) + $timeoutSeconds;

while (true) {
if (microtime(true) > $deadline) {
proc_terminate($proc, 9);
throw new RuntimeException('转码超时(' . $timeoutSeconds . 's),已强制结束');
}
$read = [];
if (!feof($pipes[1])) { $read[] = $pipes[1]; }
if (!feof($pipes[2])) { $read[] = $pipes[2]; }
if ($read === []) { break; }

$w = [];
$e = [];
$ready = @stream_select($read, $w, $e, 1);
if ($ready === false) { break; }
if ($ready === 0) { continue; }

foreach ($read as $stream) {
$chunk = fread($stream, 65536);
if ($chunk === false || $chunk === '') { continue; }
if ($stream === $pipes[2]) {
$stderr .= $chunk;
continue;
}
$stdout .= $chunk;
if ($onProgress === null) { continue; }
// 进度是 key=value 行,以 progress= 结尾一组,攒够一组再回调
$pending .= $chunk;
while (($pos = strpos($pending, "\n")) !== false) {
$line = substr($pending, 0, $pos);
$pending = substr($pending, $pos + 1);
$eq = strpos($line, '=');
if ($eq === false) { continue; }
$progress[substr($line, 0, $eq)] = trim(substr($line, $eq + 1));
if (strpos($line, 'progress=') === 0) {
$onProgress($progress);
$progress = [];
}
}
}
}

fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);

return ['code' => $code, 'stdout' => $stdout, 'stderr' => $stderr];
}

public function codecName(string $file): string
{
$r = $this->run([
$this->ffprobe, '-v', 'quiet', '-print_format', 'json',
'-select_streams', 'v:0', '-show_streams', $file,
], 30);
$data = json_decode($r['stdout'], true);
return (string) ($data['streams'][0]['codec_name'] ?? '');
}
}

// ---- 使用 ----
$input = $argv[1] ?? null;
$output = $argv[2] ?? null;
if ($input === null || $output === null) {
fwrite(STDERR, "用法: php transcode_av1.php <输入> <输出(.webm|.mp4)>\n");
exit(1);
}

try {
$tr = new Av1Transcoder();
$encoders = $tr->encoders();
if ($encoders === []) {
throw new RuntimeException('本机 ffmpeg 没有 AV1 编码器');
}
$encoder = $encoders[0]; // 优先 libsvtav1
$args = $tr->buildArgs($input, $output, $encoder, 32);

$last = 0.0;
$result = $tr->run($args, 7200, function (array $p) use (&$last) {
// out_time_us 单位是微秒
$sec = ((int) ($p['out_time_us'] ?? 0)) / 1000000;
if ($sec - $last >= 5) { // 每 5 秒打印一次,避免刷屏
$last = $sec;
printf("已编码 %.1f 秒\n", $sec);
}
});

if ($result['code'] !== 0) {
throw new RuntimeException('ffmpeg 退出码 ' . $result['code'] . ': '
. substr(trim($result['stderr']), -500));
}
printf("完成: %s (编码器 %s, 视频编码 %s)\n", $output, $encoder, $tr->codecName($output));
} catch (Throwable $e) {
fwrite(STDERR, '失败: ' . $e->getMessage() . "\n");
exit(2);
}

运行输出形如:


已编码 5.0 秒
已编码 10.0 秒
完成: output.webm (编码器 libsvtav1, 视频编码 av1)

最后一步的codecName()校验很关键:它会告诉你产物里的视频流编码名确实是av1,而不是 ffmpeg 悄悄选了个默认编码器。


常见坑点


1. 把 “没有编码器” 当成 PHP 的问题


❌ 反复检查 PHP 代码、重装 PHP 扩展 ✅ffmpeg -hide_banner -encoders看libsvtav1/libaom-av1是否在列表里;不在就只能换 ffmpeg 构建


2. libaom 的-crf没配-b:v 0


❌-c:v libaom-av1 -crf 30✅-c:v libaom-av1 -crf 30 -b:v 0,才是恒定质量模式


3. 把 x264 的 preset 名字给 SVT-AV1


❌-c:v libsvtav1 -preset veryslow✅-c:v libsvtav1 -preset 6;记住数字越大越快,方向与 x264 相反


4. 容器和音频编码不匹配


❌ 输出.webm却用 AAC,或者输出.mp4却用 Opus ✅ WebM 配libopus,MP4 配aac;不确定就用.mkv归档


5. 转码放在同步请求里


❌ 在 HTTP 请求里直接跑几十分钟的转码,指望set_time_limit(0)兜住 ✅ 交给队列/后台进程,PHP 只负责投递与查询进度;否则 fpm worker 会被占满


6. 不限制线程数


❌ 容器里 ffmpeg 按宿主机核心数开线程,把同机其他服务一起拖慢 ✅ 用-threads或限制容器 CPU 配额,给业务进程留出余量


7. 只看退出码不校验产物


❌if ($code === 0) 成功入库✅ 用ffprobe确认codec_name === 'av1',再检查时长和文件大小是否合理


8. 依赖“音视频一定有”的假设


❌ 直接按有音轨的参数拼命令,遇到纯视频素材报错或产出异常 ✅ 先探测流信息,无音轨时加-an,避免 muxer 报错


总结




环节做法关键点



环境确认ffmpeg -encoders查编码器AV1 编码器全是外部库,缺失就换构建

编码器选择优先libsvtav1,备选libaom-av1两者参数体系完全不同

质量控制-crf恒定质量,libaom 需-b:v 0CRF 范围 0-63

速度档位SVT-AV1 用-preset 0..13数字越大越快,与 x264 相反

容器匹配WebM+Opus / MP4+AAC别把不兼容的组合塞进同一容器

任务组织队列后台执行 + 进度解析不要占用同步 HTTP 请求

结果校验ffprobe校验codec_name退出码为 0 不等于产物正确



把视频转 AV1 这件事,PHP 侧真正需要负责的只有三件事:确认环境里到底有哪个编码器、按该编码器的规则生成参数、以及把这条长任务放到不阻塞请求的地方去跑。编码质量与速度的取舍属于 ffmpeg 的领域,用小段样片先做参数对比,比在代码里反复猜测要靠谱得多。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询