资讯动态

FFmpeg 实战:m3u8 与 mp4 双向转换、HLS 切片与故障排查

发布时间:2026/9/30 5:17:27 来源:尧图企业网站定制
上周帮朋友处理一批录播文件目录里躺着两百多个.ts分片和一个index.m3u8他用普通播放器一个个点着看还行想剪一段做课程回放就彻底没辙了。这种场景我遇到过太多次——凡是走流媒体协议播出来的内容落到本地往往就是一串碎片加一个索引文件而真正好用的仍然是一个规规矩矩的 mp4。FFmpeg 就是干这个的它能把散装分片按索引拼回一个完整的 mp4也能反过来把一个规整的 mp4 切成 m3u8 索引加一堆 ts 分片交给播放器或网页去点播。这篇东西我不打算写成命令手册那种东西查文档就有。我想聊的是我自己在 mp4 与 m3u8 双向转换上踩过的坑为什么有的命令跑完只有声音没有画面为什么切片之后网页播放器一直转圈为什么明明-c copy几秒就完成的活儿非有人要重编码跑二十分钟。内容会覆盖 FFmpeg 的安装与自检、m3u8 转 mp4 的几种典型路径、mp4 切片成 HLS 的参数计算、加密流的处理、以及失败之后怎么定位。刚接触 FFmpeg 的人可以照着抄命令已经用了一段时间的人可以重点看参数取舍和排查思路那几节。前提说清楚所有操作只针对你自己录制的、或者你已经获得授权的素材别拿去做不该做的事。1. 先搞清楚 m3u8 和 mp4 到底差在哪1.1 一个是播放清单一个是完整容器很多人第一次接触 m3u8 会以为它是某种视频格式其实它就是个纯文本清单。你用记事本打开一个 m3u8看到的通常是这样的内容#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.000, seg_000.ts #EXTINF:10.000, seg_001.ts #EXT-X-ENDLIST#EXTINF后面跟的是这一段的时长下一行是分片文件名。播放器读这个清单按顺序把每个 ts 拉下来、解码、拼起来播放。所以 m3u8 本身不包含任何画面数据它只是一张目录表。mp4 则完全不同它是把视频轨、音频轨、字幕轨、时间戳索引全部封装在一个文件里的容器moov盒子里记着每一帧的位置和时长播放器打开它就知道整个影片的结构。这个根本差别决定了两者的使用位置mp4 适合存放和搬运一个文件走天下m3u8 适合分发可以按需加载、可以切码率、可以做直播。理解了这一点你就明白为什么转换本质上是拆表和装订两件事而不是像转码那样要重新计算画面。1.2 为什么流媒体非要拆成碎片有人会问既然 mp4 这么好用为什么还要费劲拆开核心原因有三个。第一是起播速度一个 2GB 的 mp4 如果moov盒子在文件末尾播放器必须把整个文件拉完才能开始播而 m3u8 只需要先拿到几百字节的清单和第一个分片一两秒就能出画面。第二是码率自适应同一个视频准备 1080p、720p、480p 三个版本的清单播放器根据当前网速自己切换网络抖动时降码率而不是卡住。第三是直播场景内容是一段段持续产生的根本不存在一个完整的文件可以封装。反过来说当内容已经躺在你硬盘上、不需要自适应、也不需要考虑起播延迟的时候碎片化就是纯粹的麻烦。你会遇到文件名带序号、目录散乱、播放器不支持直接打开清单、想做剪辑软件却识别不了这一堆东西。这就是 m3u8 转 mp4 这个方向的需求来源。1.3 两个方向的转换各自解决什么问题m3u8 → mp4是我用得最多的方向。典型场景是录播平台导出的一堆分片要归档需要合并成一个文件方便长期保存或者你手里的课程素材要丢进剪辑软件而剪辑软件只认标准容器。这个方向的核心诉求是无损、快速、音画对齐所以绝大多数情况应该走-c copy直接复制流不重编码。mp4 → m3u8则是分发方向。你自己拍的视频要放到网页上做点播希望用户点开就能播、拖动进度条秒响应、手机上不卡顿这时候切片成 HLS 是最省事的方案。这个方向的核心诉求是分片均匀、关键帧对齐、兼容性好参数选择比第一个方向讲究得多。两个方向我都建议先跑一遍最小命令看结果再决定要不要调参。很多人一上来就堆一大堆参数出了问题反而不知道是哪一个导致的。2. 把 FFmpeg 装到能跑起来2.1 Windows解压版、PATH 与版本区别Windows 上最省事的办法是用官方提供的编译版压缩包下载下来是个.7z比如名字里带essentials_build的那种。解压之后你会看到bin目录里有三个关键可执行文件ffmpeg.exe负责转换ffprobe.exe负责探测分析ffplay.exe是个轻量播放器用来快速验证。很多人只把ffmpeg.exe拖出来用后面想用ffprobe看流信息又得重新找建议整个目录一起放到一个固定路径比如D:\tools\ffmpeg。essentials_build和full_build的区别在于内置编码器的数量。前者只打包了最常见的那些H.264、H.265、AAC、MP3、VP9 等体积小后者把能塞的都塞进去了包括一些冷门格式和实验性编码器。做 mp4 与 m3u8 互转这件事essentials 完全够用我真没遇到过因为编码器不全而失败的案例。接下来是配置环境变量这一步是新手最容易卡住的地方。把D:\tools\ffmpeg\bin加到系统 PATH 里然后必须重新开一个命令行窗口老窗口读的还是旧的环境变量。我见过有人折腾半小时以为装失败了其实只是没重开 cmd。顺便提一句 32 位和 64 位的问题。如果你还在用 32 位系统得单独找 32 位的编译版64 位版本在上面跑不起来会直接报不是有效应用程序。现在还在用老系统的机器不少这个坑挺常见。2.2 Linux包管理器安装与带硬件加速的编译如果只是要跑转换命令sudo apt install ffmpeg就够了装完立刻能用。但如果你要处理大量视频、希望用显卡加速发行版仓库里的版本往往没开 NVENC这时候就得自己编译或者找第三方编译版。编译的核心是让 configure 阶段能找到 CUDA 相关头文件和库典型的配置长这样./configure \ --enable-nonfree \ --enable-cuda-nvcc \ --enable-nvdec --enable-nvenc --enable-cuvid \ --enable-ffnvcodec \ --enable-libnpp \ --extra-cflags-I/usr/local/cuda/include \ --extra-ldflags-L/usr/local/cuda/lib64 \ --enable-gpl --enable-libx264 --enable-libx265编译完之后用ffmpeg -hide_banner -encoders | grep nvenc检查有没有h264_nvenc和hevc_nvenc有就说明编译成功了。这里有个实际经验驱动版本和 CUDA 版本不匹配是编译失败的头号原因报错信息通常出现在checking for nvcc那一步遇到就先确认nvcc --version能不能正常输出。2.3 装完必做的三条自检命令装好之后别急着跑正式任务先做三个检查能把后面 80% 的命令跑不通问题提前排除。第一条看版本和构建配置ffmpeg -version输出第一行是版本号下面的configuration:里能看到是否启用了--enable-libx264之类的选项。这个信息很重要比如你写的命令里有-c:v libx264但你的构建里没有这个编码器就会报Unknown encoder libx264。第二条看某个编码器是否可用ffmpeg -hide_banner -encoders | grep -Ei 264|265|aac|nvenc第三条用 ffprobe 确认能读懂文件ffprobe -v error -show_format -show_streams -of json input.mp4这三条能跑通说明环境没问题后面出问题就是命令或素材本身的事了排查范围一下缩小一半。2.4 关于版本选择的一点个人看法FFmpeg 的版本迭代挺快新版对一些历史遗留问题做了自动处理。比如从 ts 里复制 AAC 音频到 mp4 时老版本必须手动加-bsf:a aac_adtstoasc新版本会自动帮你加。又比如 HLS 切片时 H.264 的 Annex-B 转换新版 hls 复用器也会自动处理。我的建议是生产环境用稳定的大版本别追最新。如果你在网上抄到的命令里有一堆手动加 bitstream filter 的写法用新版本跑也不会有问题那些 filter 通常会被忽略或者幂等执行。反过来用老版本去跑只写了两三个参数的新教程命令就可能失败。3. m3u8 转 mp4把碎片拼回完整文件3.1 本地分片目录的标准操作假设你有一个目录里面有index.m3u8和一堆seg_000.ts、seg_001.ts……最直接的命令是ffmpeg -allowed_extensions ALL -i index.m3u8 -c copy -movflags faststart output.mp4逐个拆解一下。-allowed_extensions ALL是因为 m3u8 里写的分片扩展名有时候不是.ts可能被改成.jpg、.png之类的伪装后缀默认情况下 ffmpeg 出于安全考虑会拒绝加载非标准扩展名。-c copy表示音视频轨直接复制不重新编码这是速度的关键——一小时视频几秒钟就能合并完。-movflags faststart会把moov盒子搬到文件头部这样输出的 mp4 起播很快也方便网页播放。注意如果你的 m3u8 里写的是绝对路径或者带域名的地址即使文件在本地ffmpeg 也会尝试走网络协议。这时候需要加上-protocol_whitelist file,http,https,tcp,tls,crypto否则会报协议不在白名单里。如果 m3u8 文件丢了只剩下一堆 ts 分片也能救。用一个文本文件列出所有分片就行file seg_000.ts file seg_001.ts file seg_002.ts然后ffmpeg -f concat -safe 0 -i filelist.txt -c copy -movflags faststart output.mp4-safe 0是为了允许文件路径中出现特殊字符缺了它会报不安全路径。生成这个列表不用手敲Linux 下ls seg_*.ts | sort | sed s/^/file /;s/$// filelist.txt一条命令搞定。注意一定要排序ls默认的字典序对seg_1.ts、seg_10.ts、seg_2.ts这种不带补零的命名会排错合并出来顺序全乱。3.2 直接喂网络地址如果分片还在服务器上直接给 ffmpeg 一个完整 URL 就行ffmpeg -headers User-Agent: Mozilla/5.0 -i https://example.com/hls/index.m3u8 \ -c copy -movflags faststart output.mp4几个实用参数值得记住。-headers用来附加 HTTP 请求头有些源会校验 Referer 或 UA不加就返回 403。-user_agent是-headers的简化写法只改 UA 的时候用它更短。-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5在网络不稳定时很有用断了会自动重连不至于跑了一半任务失败。-rw_timeout控制单次读超时单位是微秒设成-rw_timeout 15000000就是 15 秒。这个参数在处理响应慢的源时能避免无限等待。我一般会把它和-timeout搭配着设具体值根据源站响应速度调。还有一个经验网络源转本地文件时输出文件名别和输入同名同目录否则某些情况下可能出现读写冲突输出直接变成 0 字节。3.3-c copy还是重编码怎么判断这是新手最容易选错的地方。我的判断标准很简单按优先级来情况选择理由分片是 H.264 AAC目标就是普通 mp4-c copy无损、极快、CPU 几乎不动分片是 H.265播放设备不支持重编码为 H.264兼容性优先音画不同步、时间戳混乱先试-c copy加时间戳参数大部分情况不用重编码需要裁剪、缩放、加水印必须重编码滤镜链无法在 copy 模式下工作重编码的典型命令是把-c copy换成-c:v libx264 -preset medium -crf 20 -c:a aac -b:a 128k-crf是画质控制数值越小越清晰、文件越大18 到 23 是常用区间20 基本是肉眼无损。-preset控制编码速度从ultrafast到veryslow越慢压缩率越高。我实测一小时 1080p 素材medium大概需要 15 到 30 分钟取决于 CPU。有显卡的话把-c:v libx264换成-c:v h264_nvenc速度能快 5 到 10 倍但同码率下画质略差一点。我的用法是中间产物用 NVENC 图快最终交付用 libx264 图质量。3.4 遇到加密流怎么处理有些 m3u8 里会有这么一行#EXT-X-KEY:METHODAES-128,URIkey.key,IV0x...这说明分片被 AES-128 加密了需要密钥才能解。如果你手上有密钥文件在本地目录里放好直接跑-allowed_extensions ALL那条命令ffmpeg 会自动读取并解密。如果密钥是通过 HTTP 提供的需要在白名单里加上crypto和httpsffmpeg -protocol_whitelist file,http,https,tcp,tls,crypto -i index.m3u8 -c copy output.mp4常见的失败现象是Invalid data found when processing input或者解出来的文件全是噪音。第一个原因通常是密钥长度不对——AES-128 的密钥文件必须是正好 16 字节多一个换行符都会失败用xxd key.key看一下就很清楚。第二个原因可能是 IV 不匹配如果 m3u8 里指定了 IV就要用指定的那个不能自己随便生成。提示如果你不确定素材是否加密先用 ffprobe 跑一遍源地址能读出流信息说明没加密或者密钥可公开获取直接报错就要回头检查清单文件。3.5 一批目录的批处理写法真实工作里很少只转一个文件。假设你有case01到case20二十个目录每个里面都有一个 m3u8Linux 下一条循环就完事for d in case*/; do name$(basename $d) ffmpeg -hide_banner -loglevel error -allowed_extensions ALL \ -i ${d}index.m3u8 -c copy -movflags faststart out/${name}.mp4 done-loglevel error让输出只保留错误信息批量跑的时候屏幕不会被刷爆。-hide_banner去掉版本横幅。这两个参数在脚本里几乎是标配。Windows 下用 PowerShell 写循环也可以Get-ChildItem -Directory | ForEach-Object { $n $_.Name ffmpeg -allowed_extensions ALL -i $($_.FullName)\index.m3u8 -c copy -movflags faststart out\$n.mp4 }批量任务最怕的是跑到一半某个文件失败后面全乱。我的做法是每个文件单独输出一行状态失败就记下名字跳过跑完统一处理失败的几个比一路中断强得多。4. mp4 转 m3u8切片、加密与播放列表4.1 最小可用的切片命令从零开始做一个能播的 HLS 目录基础命令只有一行ffmpeg -i input.mp4 -c copy -bsf:v h264_mp4toannexb \ -f hls -hls_time 10 -hls_list_size 0 \ -hls_segment_filename out_%03d.ts \ out.m3u8跑完你会得到out.m3u8和out_000.ts、out_001.ts……每个分片大约 10 秒。%03d是序号格式会生成000、001这样的三位数字排序稳定。切片速度极快因为-c copy同样不重编码。-bsf:v h264_mp4toannexb这个 bitstream filter 要专门说一下。mp4 里存 H.264 用的是一种长度前缀的格式而 ts 里用的是另一种起始码的格式两者不通用。这个 filter 就是做转换的。新版 ffmpeg 的 hls 复用器会自动插入它但显式写上没坏处尤其是你在用一些较老的构建版本时。4.2 四个关键参数逐个拆参数选错是切片效果差的根源这四个必须搞清楚。-hls_time是目标分片时长单位秒。它只是目标实际分片长度取决于关键帧位置。因为 ts 只能在关键帧处切开如果原视频 GOP 是 4 秒你要 6 秒的分片实际会切成 4 秒或 8 秒。想要精确控制原视频必须用-g参数强制关键帧间隔。计算方法很简单关键帧间隔 帧率 × 目标分片时长。25fps 的视频要 6 秒分片-g 15030fps 要 4 秒分片-g 120。-hls_list_size是清单里保留多少个分片。默认值是 5也就是滚动窗口模式只保留最近 5 个分片在清单里——这是给直播用的。做点播必须设成0表示保留全部分片。我见过有人切完发现只有前几分钟能播后面全黑就是这个参数没改。-hls_segment_filename指定分片命名模板。用%d系列占位符也可以用%v表示码率变体编号做多码率时会用到。命名里尽量别用中文和空格某些服务器返回时会有编码问题。-hls_playlist_type vod明确标记清单是点播类型会在清单末尾加#EXT-X-ENDLIST。播放器看到这个标记就知道内容已经完整可以显示总时长。不加的话有些播放器会以为是直播流进度条不显示总长度。完整的点播切片命令ffmpeg -i input.mp4 -c copy -bsf:v h264_mp4toannexb \ -f hls -hls_time 6 -hls_list_size 0 \ -hls_playlist_type vod \ -hls_segment_filename seg_%04d.ts \ index.m3u84.3 ts 分片还是 fmp4 分片HLS 支持两种分片格式。传统的.ts是 MPEG-TS 流兼容性最好从老播放器到各种移动端都能播这也是目前用得最多的形式。另一种是-hls_segment_type fmp4生成的.m4s分片配合一个init.mp4初始化文件体积略小、切换码率时更平滑新设备支持良好但老设备可能识别不了。切换方式ffmpeg -i input.mp4 -c copy \ -f hls -hls_time 6 -hls_list_size 0 \ -hls_segment_type fmp4 \ -hls_fmp4_init_filename init.mp4 \ -hls_segment_filename seg_%04d.m4s \ index.m3u8我的选择原则面向大众用户的公开点播用 ts因为不知道对面是什么设备内部使用、目标设备明确的新项目用 fmp4。还有一种-hls_flags single_file模式把全部分片塞进一个大文件清单里用字节范围索引适合分片太多导致文件数爆炸的场景但服务器必须支持 Range 请求。4.4 切片加密如果内容需要一定的访问控制HLS 支持 AES-128 加密。第一步生成密钥和 IVopenssl rand 16 enc.key openssl rand -hex 16第二条命令输出的是 32 位十六进制字符串作为 IV 记下来。然后创建密钥信息文件enc.keyinfo格式是三行https://your-domain.com/key/enc.key /path/to/enc.key 0123456789abcdef0123456789abcdef第一行是播放器获取密钥的 URL第二行是服务端本地密钥文件路径第三行是 IV。切片时加上ffmpeg -i input.mp4 -c copy \ -f hls -hls_time 6 -hls_list_size 0 \ -hls_key_info_file enc.keyinfo \ -hls_segment_filename enc_%04d.ts \ index.m3u8生成的分片就是加密的了播放器必须先请求 key URL 拿到密钥才能解。实际部署时那个 key URL 通常挂在一个需要鉴权的接口上用临时令牌控制访问这才是加密真正起作用的部分——分片本身加密只防君子访问控制才是关键。另外要注意enc.key文件千万不要和分片放在同一个公开目录里等于把钥匙插在门上。4.5 多码率 master playlist单码率切片只能叫HLS 化真正发挥 HLS 优势的是多码率自适应。做法是在一次 ffmpeg 调用里生成多个分辨率的变体再输出一个 master 清单。ffmpeg -i input.mp4 \ -filter_complex [0:v]split2[v1][v2]; \ [v1]scalew1280:h720[v1out]; \ [v2]scalew854:h480[v2out] \ -map [v1out] -c:v:0 libx264 -b:v:0 2600k -maxrate:v:0 2800k -bufsize:v:0 4000k \ -map [v2out] -c:v:1 libx264 -b:v:1 1200k -maxrate:v:1 1300k -bufsize:v:1 2000k \ -map a:0 -map a:0 -c:a aac -b:a 128k -ac 2 \ -g 150 -keyint_min 150 -sc_threshold 0 \ -f hls -hls_time 6 -hls_playlist_type vod \ -hls_segment_filename v%v/seg_%04d.ts \ -master_pl_name master.m3u8 \ -var_stream_map v:0,a:0 v:1,a:1 \ v%v/index.m3u8这段命令里有几个点值得单独解释。split2把视频流复制成两路分别缩放到 720p 和 480p。-b:v是目标码率-maxrate是峰值上限-bufsize是缓冲区大小一般取maxrate的 1.5 倍左右三者配合可以让码率曲线更平滑。-g 150 -keyint_min 150 -sc_threshold 0是必须的它强制所有变体在相同时间点产生关键帧否则播放器切换码率时会出现花屏或者卡顿这是多码率切片最容易忽略的一点。-var_stream_map v:0,a:0 v:1,a:1把视频轨和音频轨配对成两个变体%v在文件名和目录里都会被替换成变体编号。跑完之后你会得到v0/、v1/两个目录加一个master.m3u8播放器读 master 自动选择合适码率。码率档位的搭配经验是相邻档位之间码率差 1.5 到 2 倍差太少切换没意义差太多视觉跳变明显。5. 三个真实场景的完整实操5.1 场景一录播分片合并加完整性校验这是我做得最多的一类活儿。手上一个目录index.m3u8加 200 多个 ts需求是合并成 mp4 并确认没问题。先看一眼清单确认分片数量grep -c \.ts index.m3u8 ls *.ts | wc -l两个数字对不上就说明有分片缺失合并出来的视频会有跳帧。这种情况先把缺的补上别硬合。然后合并ffmpeg -hide_banner -allowed_extensions ALL -i index.m3u8 \ -c copy -movflags faststart \ -loglevel warning \ output.mp4跑完之后一定要校验这一步不能省ffprobe -v error -show_entries formatduration,size -of defaultnw1 output.mp4对比一下清单里#EXTINF的总和差距在 1 秒以内算正常差得多就说明有分片没合进去。再快速播一遍开头、中间、结尾各 10 秒确认音画同步。我用的是ffplay -ss 00:10:00 -t 10 output.mp4直接跳到中间看一段。注意合并出来的文件容器时长和实际可播时长可能不一致尤其是最后一个分片不完整的时候。校验时以实际能播到最后为准不要只看 ffprobe 的数字。5.2 场景二在线地址一次成型有些内容只提供在线清单地址需要一次性拉下来存成 mp4。这时候关键在网络稳定性ffmpeg -hide_banner \ -user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) \ -headers Referer: https://example.com/player\r\n \ -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 \ -rw_timeout 20000000 \ -protocol_whitelist file,http,https,tcp,tls,crypto \ -i https://example.com/hls/index.m3u8 \ -c copy -movflags faststart output.mp4-headers里多个头用\r\n分隔末尾也要有。Referer 这个头在不少源上是必须的缺了直接 403。如果跑到一半断了ffmpeg 会按-reconnect_delay_max指定的最大间隔重试网络恢复后能接着拉。另一种做法是先分片下载再本地合并。aria2c这类工具支持多线程分段下载速度通常比 ffmpeg 单线程快适合源站带宽充足的情况。下载完之后按 3.1 节的办法用 concat 合并。缺点是分片文件名可能重复或者没有序号规律需要人工整理一下列表顺序。5.3 场景三自有视频做网页点播加服务器配置假设你拍了一段视频要放在自己的网页上点播。流程是先预处理成标准格式再切片最后配置服务器。预处理这一步很多人忽略但它决定后面顺不顺。原视频的编码格式、GOP 结构、时间戳都可能是乱的直接切片会得到长短不一的分片ffmpeg -i raw.mp4 \ -c:v libx264 -preset medium -crf 20 \ -profile:v high -level 4.1 -pix_fmt yuv420p \ -g 150 -keyint_min 150 -sc_threshold 0 \ -c:a aac -b:a 128k -ac 2 -ar 44100 \ -movflags faststart \ input.mp4-pix_fmt yuv420p是兼容性关键很多手机的相机输出 yuvj420p 或者 10bit 格式网页播放器解不了转成 yuv420p 就通吃了。-profile:v high -level 4.1是目前兼容性最好的组合从老手机到新浏览器都支持。然后切片ffmpeg -i input.mp4 -c copy -bsf:v h264_mp4toannexb \ -f hls -hls_time 6 -hls_list_size 0 \ -hls_playlist_type vod \ -hls_segment_filename hls/seg_%04d.ts \ hls/index.m3u8服务器这边有两个坑。第一是 MIME 类型.m3u8必须返回application/vnd.apple.mpegurl.ts必须是video/mp2t类型不对某些播放器会拒绝加载。nginx 里可以这样补location ~ \.m3u8$ { types { application/vnd.apple.mpegurl m3u8; } add_header Access-Control-Allow-Origin *; add_header Cache-Control no-cache; } location ~ \.ts$ { types { video/mp2t ts; } add_header Cache-Control max-age31536000; }第二是跨域如果你的播放页和视频文件不同域必须加 CORS 头否则 hls.js 拉分片会被浏览器拦掉。index.m3u8建议不缓存或者短缓存ts 分片可以设置很长的缓存时间因为它们的内容一旦生成就不会变。网页端播放用 hls.js 是最常见的方案Vue 或者 React 项目里都是引入这个库然后hls.loadSource(hls/index.m3u8)。Safari 因为原生支持 HLS不需要额外库但为了统一行为多数项目还是会走 hls.js 加降级判断。6. 转换失败的排查实录6.1 报错信息该怎么读FFmpeg 的报错信息其实很有信息量只是长得吓人。我总结了一个读法顺序。先看最后一行的具体错误。比如Invalid data found when processing input表示输入根本没法解析问题在源文件Unknown encoder libx264是构建里没这个编码器问题在环境Protocol not on whitelist crypto是白名单缺项问题在参数。再看中间的流信息。ffmpeg 会先打印一行Input #0, hls, from xxx后面跟着Stream #0:0: Video: h264 ...这样的描述。如果这一行里出现的是你意料之外的编码格式比如你以为是 H.264 结果是 MPEG-4 Part 2那后面-c copy到 mp4 可能就播不了。这个信息比报错本身更有用。最后看警告。带[hls 0x...]前缀的警告通常指向清单文件的问题比如分片缺失、时长标注不一致、序列号跳变。这些警告不一定导致失败但值得看一眼。6.2 音画不同步与时间戳问题这是最烦人的一类问题因为命令跑成功了输出文件却不对。常见原因有三种。第一种是分片的时间戳不连续。有些录制工具生成的分片每段都从 0 开始计时合并起来时间轴就重复了。处理办法是让 ffmpeg 重新生成时间戳ffmpeg -fflags genpts -i index.m3u8 -c copy output.mp4第二种是负时间戳。某些 ts 的第一帧时间戳是负值mp4 容器不接受ffmpeg 会报Non-monotonous DTS或者直接丢掉开头几帧。加这个参数处理-avoid_negative_ts make_zero第三种是音频采样率不一致导致的累积漂移。音频轨道如果有轻微的速度偏差一小时下来能差出好几秒。这种情况只能重编码音频让它按视频时间轴重新对齐-c:v copy -c:a aac -af aresampleasync1:first_pts0顺带说一句老教程里常见的-async 1参数在新版本里已经废弃了用aresample滤镜替代看到旧写法别照抄。6.3 常见问题速查表现象可能原因处理办法Unknown encoder libx264构建未启用该编码器换-c:v h264_nvenc或重新安装完整构建Protocol crypto not on whitelist缺少协议白名单加-protocol_whitelist file,http,https,tcp,tls,cryptoInvalid data found源文件损坏或加密密钥错误检查密钥是否正好 16 字节输出只有声音没有画面视频编码不被 mp4 支持改-c:v libx264重编码合并后顺序错乱文件名字典序不等于数字序命名补零或用自然排序切片后只有前几个分片能播-hls_list_size默认值 5改成-hls_list_size 0网页播放器一直转圈MIME 类型或 CORS 不对配置application/vnd.apple.mpegurl和跨域头码率切换时花屏各变体关键帧不对齐强制-g、-keyint_min、-sc_threshold 0进度条不显示总时长缺#EXT-X-ENDLIST加-hls_playlist_type vod网络源 403缺 Referer 或 User-Agent用-headers补齐输出文件 0 字节输入输出同路径冲突换输出目录或文件名6.4 几个不影响功能但很烦人的坑最后说几个能跑通但用着别扭的问题。mp4 损坏。有时候文件能识别但播到一半卡死通常是moov盒子或者索引表出了问题。可以先试试ffmpeg -i broken.mp4 -c copy fixed.mp4很多时候重新封装一遍就好了因为 ffmpeg 会重建索引。如果连 ffprobe 都读不出来那就得用十六进制工具去看文件头尾的原子结构找ftyp和moov的位置这种修复属于另一个话题了成功率也不高重要文件平时多做备份比事后修划算。分片数量太多。一个两小时的视频按 2 秒切片会产生 3600 个文件目录打开都要卡半天上传到对象存储也是几千个请求。这种时候要么把-hls_time调大到 6 到 10 秒要么用-hls_flags single_file把所有分片打进一个文件。文件命名带空格或中文。命令行里会被拆成多个参数脚本里更是灾难。切片的输出目录和文件名统一用英文加下划线这个问题就不存在了。校验不能只看大小。有人合并完看输出文件有 800MB 就以为成功了结果播到某个位置就断。老老实实用 ffprobe 看时长再挑几个时间点实际播一下比看文件大小靠谱得多。7. 用 Python 把重复劳动脚本化7.1 先探测再决策批量处理时最忌讳不管三七二十一直接跑命令因为素材情况千差万别。我的做法是先探测再根据探测结果决定参数。ffprobe 支持 JSON 输出用 Python 解析非常方便import json import subprocess def probe(path): cmd [ ffprobe, -v, error, -print_format, json, -show_format, -show_streams, path, ] result subprocess.run(cmd, capture_outputTrue, textTrue, checkTrue) return json.loads(result.stdout) info probe(index.m3u8) for s in info[streams]: print(s[codec_type], s.get(codec_name), s.get(width), s.get(height)) print(duration:, info[format].get(duration))拿到这些信息之后就能判断了如果是 h264 加 aac走-c copy如果是 hevc 而目标播放器不支持就安排重编码如果音频是 ac3转 aac 提高兼容性。这个判断逻辑写一次后面所有素材都能自动处理。7.2 批量合并的完整脚本下面这个脚本我用了很久处理目录结构为input/批次/index.m3u8的素材输出到output/批次.mp4已经合并过的自动跳过import subprocess from pathlib import Path SRC Path(input) DST Path(output) DST.mkdir(exist_okTrue) for case in sorted(p for p in SRC.iterdir() if p.is_dir()): target DST / f{case.name}.mp4 if target.exists() and target.stat().st_size 0: print(f[skip] {case.name}) continue cmd [ ffmpeg, -hide_banner, -loglevel, error, -allowed_extensions, ALL, -i, str(case / index.m3u8), -c, copy, -movflags, faststart, -y, str(target), ] print(f[run ] {case.name}) r subprocess.run(cmd, capture_outputTrue, textTrue) if r.returncode ! 0: print(f[fail] {case.name}: {r.stderr.strip()[-300:]})几个细节值得说一下。-y表示覆盖已有文件不加的话 ffmpeg 会卡在交互式询问上脚本就挂住了这是自动化里非常经典的一个坑。capture_outputTrue把输出收进变量只打印最后 300 个字符的错误信息屏幕上不会被刷屏。跳过逻辑用文件存在加大小大于 0 双重判断因为有时候中断会留下 0 字节文件。7.3 断点续跑与日志习惯批量跑几百个任务中途失败是常态。我的经验是每一行状态都写到日志文件里跑完之后 grep 一下就能找到失败的import logging logging.basicConfig( filenameconvert.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) logging.info(task start: %s, case.name)脚本重跑的时候因为有了跳过逻辑已经成功的不会重做失败的会自动重试这样反复跑几次基本能把全部处理完。比一次性跑完更好的一点是机器负载也低不会因为长时间满负载导致其他服务受影响。如果用asyncio并发跑多个转换任务记得控制并发数。FFmpeg 的-c copy模式本身资源占用很低并发 4 到 8 个没问题但如果是重编码每个进程都能吃满多核并发 2 个就已经让 CPU 跑满了再往上加只会互相拖慢。我的做法是根据是不是重编码来决定并发数copy 模式给 8重编码给 2。8. 最后分享几个我自己的使用习惯写到这里基本把两个方向的转换都覆盖了。我自己的几个固定习惯分享一下可能对你有用。一是任何命令先在小片段上试。拿-t 30截前 30 秒跑一遍确认输出没问题再处理完整文件能省掉大量等待时间。尤其是多码率切片这种命令又长又容易错的试跑一次能提前发现参数写错。二是输出文件永远另存。不管多简单的合并都写到新文件名里原素材一律不动。我见过太多人-y直接覆盖了源文件回头想重来发现素材没了。三是保留一份原始清单。转换完 m3u8 之后把index.m3u8和几个分片一起归档万一以后需要重新生成不同参数的版本不用再去重新下载。体积不大但能救急。四是参数写进脚本而不是记在脑子里。每个项目建一个.sh或者.py把用着顺手的参数固化下来下次直接改输入路径就行。我现在有十几个这样的脚本覆盖不同分辨率和不同平台的切片需求比每次查文档快得多。转换这件事本身没什么玄学核心就三件事分清楚是复制流还是重编码参数搞清楚每个是干什么的出问题知道从哪一行报错入手。剩下的都是熟练度问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑