ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP8.5怎么用ffmpeg提取视频里的字幕轨道

PHP8.5怎么用ffmpeg提取视频里的字幕轨道 前言视频站、在线教育、字幕组工具链里「把 MKV 里的字幕扒出来」是个高频需求。很多人第一次写的时候是这么干的shell_exec(ffmpeg -i $file -map 0:s:0 out.srt)本地测试通过上线后遇到三种症状换一个文件就报Stream map 0:s:0 matches no streams导出成功但.srt打开是乱码批量处理几十个文件后有进程卡死不动CPU 却是 0。根因有三个。第一「字幕轨道」在 ffmpeg 里是分类型的文本字幕SubRip、ASS可以转成.srt而 DVD/PGS 这类位图字幕根本没法直接转文本必须 OCR。不看编码类型就无脑加-c:s srt结果就是报错或空文件。第二0:s:0里的s是字幕流序号不是文件的绝对流序号两者混用必错。第三shell_exec拼字符串既容易被文件名注入也拿不到退出码和标准错误出错时你什么都看不见。本文用一段完整可运行的代码讲清三件事用ffprobe列出所有字幕轨道及其编码类型、按类型选择正确的导出方式、用proc_open的数组形式安全地调用外部命令并拿到退出码。示例代码需要PHP 7.4 及以上proc_open支持数组形式的命令文中的语法按PHP 8.1写PHP 8.5 同样适用——这个话题本身和 PHP 版本关系不大真正需要对齐的是 ffmpeg 自身的版本。一、先分清文本字幕和位图字幕这是整件事里最容易出错的分界线。字幕流按存储方式分两类类型典型codec_name本质能否直接转成.srt文本字幕subrip、ass、ssa、webvtt、mov_text存的是字符和时间轴能-c:s srt位图字幕hdmv_pgs_subtitle、dvd_subtitle、dvb_subtitle存的是每帧的图片不能只能原样导出隐藏字幕eia_608、eia_708北美电视字幕流视情况通常要转码位图字幕转文本需要 OCR光学字符识别ffmpeg 本身不做这件事。硬来的结果只有两种直接报编码器不支持的错或者导出一个 0 字节的文件而ffmpeg的退出码是 0 —— 后者最坑因为它看起来「成功了」。所以流程必须是两步先探测再决定怎么导。二、用 ffprobe 列出所有字幕轨道ffprobe是 ffmpeg 套件自带的探测工具-of json让它输出 JSONPHP 解析起来最省事。探测字幕流的关键参数是-select_streams sffprobe -v error -select_streams s \ -show_entries streamindex,codec_name:stream_tagslanguage,title \ -of json input.mkv输出的结构大致是这样index是文件里的绝对流序号后面映射时要用它{ streams: [ {index: 2, codec_name: subrip, tags: {language: chi, title: 简体中文}}, {index: 3, codec_name: ass, tags: {language: eng, title: English}}, {index: 4, codec_name: hdmv_pgs_subtitle, tags: {language: jpn}} ] }注意这里的index可能是 2、3、4 —— 因为 0、1 通常是视频和音频。而-map 0:s:0指的是「第 0 条字幕流」也就是index为 2 的那条。绝对序号和字幕序号是两套编号混用是必踩的坑。三、用 proc_open 安全地调用命令不要用shell_exec(ffmpeg -i $file ...)拼字符串。proc_open从PHP 7.4起支持把命令传成数组PHP 会自己做参数转义完全不经过 shell文件名里带空格、引号、;、$()都无所谓。?php declare(strict_types1); // srt_extract.php —— 需要 PHP 7.4proc_open 数组形式示例按 PHP 8.1 写 /** 判断某个 ffmpeg 字幕编码是不是文本型能转成 SRT */ const TEXT_SUBTITLE_CODECS [subrip, srt, ass, ssa, webvtt, mov_text, text]; /** * 运行外部命令返回 [退出码, 标准输出, 标准错误]。 * * 命令以数组传入proc_open 自行处理参数转义不经过 shell天然免疫命令注入。 * 注意这里是「先把 stdout 读完再读 stderr」的简单写法 * 只有当子进程往 stderr 写出超过管道缓冲区通常 64KB的内容时才可能死锁 * ffprobe/ffmpeg 在 -v error 下不会出现这种情况。 */ function run(array $cmd): array { $proc proc_open($cmd, [ 0 [pipe, r], 1 [pipe, w], 2 [pipe, w], ], $pipes); if (!is_resource($proc)) { throw new RuntimeException(无法启动进程 . implode( , $cmd)); } fclose($pipes[0]); // 立刻关掉 stdin子进程不会等输入 $stdout (string) stream_get_contents($pipes[1]); $stderr (string) stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); return [proc_close($proc), $stdout, $stderr]; } /** * 列出文件里的全部字幕轨道。 * * return listarray{index:int, codec:string, language:string, title:string, text:bool} */ function listSubtitleTracks(string $file): array { if (!is_file($file)) { throw new RuntimeException(文件不存在{$file}); } [$code, $out, $err] run([ ffprobe, -v, error, -select_streams, s, -show_entries, streamindex,codec_name:stream_tagslanguage,title, -of, json, $file, ]); if ($code ! 0) { throw new RuntimeException(ffprobe 退出码 {$code}{$err}); } $data json_decode($out, true, 512, JSON_THROW_ON_ERROR); $tracks []; foreach ($data[streams] ?? [] as $stream) { $codec (string) ($stream[codec_name] ?? ); $tracks[] [ index (int) $stream[index], codec $codec, language (string) ($stream[tags][language] ?? und), title (string) ($stream[tags][title] ?? ), text in_array($codec, TEXT_SUBTITLE_CODECS, true), ]; } return $tracks; } /** * 导出单条字幕。 * * $absoluteIndex 是 ffprobe 给出的绝对流序号 * $convert 为 true 时转成 SRT仅文本字幕可用false 时原样复制。 */ function extractSubtitle(string $file, int $absoluteIndex, string $outFile, bool $convert): void { [$code, , $err] run([ ffmpeg, -nostdin, -y, -v, error, -i, $file, -map, 0: . $absoluteIndex, -c:s, $convert ? srt : copy, $outFile, ]); if ($code ! 0) { throw new RuntimeException(ffmpeg 退出码 {$code}{$err}); } // 退出码为 0 不代表真有内容必须再确认一次文件大小 $size is_file($outFile) ? (int) filesize($outFile) : 0; if ($size 0) { throw new RuntimeException(导出结果为空文件{$outFile}可能是位图字幕请检查编码类型); } } // ---------------- 主流程 ---------------- $input $argv[1] ?? ; if ($input ) { exit(用法: php srt_extract.php 视频文件\n); } try { $tracks listSubtitleTracks($input); } catch (Throwable $e) { exit(探测失败 . $e-getMessage() . \n); } if ($tracks []) { exit(这个文件里没有字幕轨道\n); } $base pathinfo($input, PATHINFO_FILENAME); $dir dirname($input); printf(共发现 %d 条字幕轨道\n\n, count($tracks)); printf(%-6s %-22s %-8s %-6s %s\n, 序号, 编码, 语言, 可转文本, 标题); foreach ($tracks as $i $t) { printf( %-6d %-22s %-8s %-6s %s\n, $t[index], $t[codec], $t[language], $t[text] ? 是 : 否, $t[title] ); } echo \n开始导出……\n; foreach ($tracks as $i $t) { $suffix $t[language] . . . $i; try { if ($t[text]) { $out {$dir}/{$base}.{$suffix}.srt; extractSubtitle($input, $t[index], $out, true); } else { // 位图字幕不能转文本只能原样导出扩展名用 .sup / .sub $out {$dir}/{$base}.{$suffix}. . ($t[codec] dvd_subtitle ? sub : sup); extractSubtitle($input, $t[index], $out, false); } printf( 第 %d 条 - %s%d 字节\n, $t[index], basename($out), (int) filesize($out)); } catch (Throwable $e) { printf( 第 %d 条 导出失败%s\n, $t[index], $e-getMessage()); } } echo 完成\n;用法php srt_extract.php demo.mkv输出会是先一张轨道清单再逐条给出导出结果共发现 3 条字幕轨道 序号 编码 语言 可转文本 标题 2 subrip chi 是 简体中文 3 ass eng 是 English 4 hdmv_pgs_subtitle jpn 否 开始导出…… 第 2 条 - demo.chi.0.srt18342 字节 第 3 条 - demo.eng.1.srt21107 字节 第 4 条 - demo.jpn.2.sup4823104 字节第 3 条原本是 ASS导出成.srt时 ffmpeg 会做格式转换样式信息字体、位置、特效会丢失这是格式本身的限制不是参数问题。需要保留样式就改导.ass并用-c:s copy。常见坑点1. 用shell_exec拼字符串调用 ffmpeg❌shell_exec(ffmpeg -i {$file} -map 0:s:0 out.srt)—— 文件名里只要有一个空格或分号命令就被截断或注入而且失败时你只能拿到null退出码和错误信息全丢了。 ✅ 用proc_open的数组形式PHP 7.4或至少用escapeshellarg()包住每个参数并检查$returnVar。2. 对位图字幕用-c:s srt❌ 导出 PGS 字幕时加-c:s srt—— ffmpeg 报Subtitle encoding currently only possible from text to text或者产出一个 0 字节文件却返回退出码 0。 ✅ 先看codec_name文本字幕才转 SRT位图字幕一律-c:s copy扩展名用.sup或.sub。要转文本得另外接 OCR 工具链。3. 把0:s:0当成绝对流序号❌ 文件里字幕流在index2、3、4却写-map 0:0结果把视频流当字幕处理。 ✅-map 0:s:N是「第 N 条字幕流」-map 0:index是绝对序号。稳妥做法是始终用 ffprobe 给出的绝对index加-map 0:index。4. 用-c:s copy却把文件命名成.srt❌ 源是 ASS加-c:s copy导出成.srt—— 文件内容还是 ASS 文本播放器按 SRT 解析时间轴和样式全乱。 ✅ 容器格式要和解码器匹配想转 SRT 就用-c:s srt想原样保留样式就导.ass并-c:s copy。5. 只看退出码不看输出文件❌if ($code 0) { echo 成功; }—— 空文件也是「成功」。 ✅ 退出码为 0 之后再检查filesize($outFile)是否大于 0两条都过才算成功。6. 忘了-nostdin批量处理时子进程吃掉父进程的输入❌ 在循环里连续调用 ffmpeg不写-nostdin。在某些环境下 ffmpeg 会尝试从标准输入读取键盘命令表现为进程挂住、CPU 占用为 0。 ✅ 所有非交互调用都加上-nostdin已经读到 EOF 的管道同理并加-y避免交互式覆盖确认。7. 把带字体附件的 MKV 转成 ASS 时丢了字体❌ 只想导出字幕用-map 0:s:1MKV 里内嵌的字体附件没跟着走字幕里的自定义字体在别处渲染不出来。 ✅ 需要连字体一起导出时把附件流也映射上-map 0:s:1 -map 0:t??表示没有附件也不报错并配-c:t copy。8.proc_open读管道的顺序造成死锁❌ 先stream_get_contents($pipes[1])读完 stdout再读 stderr而子进程往 stderr 写了超过管道缓冲区的内容就会卡住。 ✅-v error把 stderr 压到最小输出量确实可能很大的场景改用stream_select()同时读两个管道或把 stderr 重定向到临时文件。总结环节做法要点探测ffprobe -select_streams s -of json拿绝对index和codec_name分类文本 vs 位图位图不能转 SRT只能原样导出调用proc_open数组形式不经 shell免注入映射-map 0:绝对序号别把s:N和绝对序号混用导出-c:s srt或-c:s copy容器扩展名要匹配校验退出码 文件大小空文件也是「成功」用 ffmpeg 处理字幕难点从来不在命令本身而在先探测再决策这条纪律。把「列出轨道 → 判断编码类型 → 选择导出方式 → 双重校验」固定成流程再配上proc_open的数组调用这套脚本就能稳定跑在各种来源的视频上。
返回列表