简介:面向嵌入式系统开发者与流媒体技术爱好者,该资源包围绕MPEG-TS视频在FFmpeg下的传输与分段处理展开。MPEG-TS作为数字电视广播、IPTV及网络流媒体中常用的容器格式,通过固定大小的数据包承载音视频信息,能够较好适应复杂网络环境;作者在嵌入式ARM平台上交叉编译通过,并用C语言实现了将TS流分割为符合HLS、DASH协议要求的短片段,为资源受限设备上的实时视频传输提供了可落地的参考。压缩包共3个文件,包括C语言源码、Makefile自动化构建脚本与COPYING许可证说明,整体仅10KB,结构紧凑,便于阅读和二次移植。已有1554人学习。借助该资源,可快速掌握FFmpeg在嵌入式环境下的编译配置要点,深入理解TS分段处理的实现逻辑,为监控系统、车载娱乐及自定义流媒体服务开发打下基础。
1. mpeg-ts 格式视频ffmpeg传输:直播推流绕不开的 188 字节封装和传输通路
做视频传输的人迟早会撞上 mpeg-ts 格式视频ffmpeg传输这个组合。MPEG-TS 把音视频切成 188 字节一包的流式封装,自带时间戳和节目表,丢几个包不至于整体黑屏;ffmpeg 则是把这路流打包、推送到 UDP、TCP 或 HTTP 上最顺手的工具。这篇笔记只讲三件事:TS 流里实际在跑什么、用什么命令把视频推出去、推出去之后哪些参数最容易翻车。适合正在做直播推流、局域网视频分发,或者被播放器黑屏花屏折磨的人。新手能照着命令跑通一条链路,熟手能对着参数表排查,值不值得投入,看完你应该已经有判断。
2. MPEG-TS 传输的底层逻辑:188 字节包里装了什么,为什么它能扛抖动
传输和封装不是一回事。MP4 面向本地存储,元数据可能散落在文件各处,网络流拉到一半解析不出来;MPEG-TS 是为实时传输设计的,把音视频切成固定 188 字节的小包,每个包都能独立对齐、独立丢弃,不需要等整个文件到位。这个差异决定了直播、广电、IP 分发这些场景都选 TS 而不是 MP4。理解这一点,后面调参数时才不会把 MP4 的思维带进 TS 链路。
2.1 四字节头里的信息:同步字节、PID 与连续计数器
每个 TS 包固定 188 字节,前 4 字节是头。第一个字节永远是 0x47,接收端抓到一个 0x47 后连续校验几个包,就能锁定包边界,即使中间丢了包也能快速重新同步。接着是 13 比特的 PID,告诉接收端这个包属于哪一路流:PID 0 是 PAT(节目关联表),PAT 里写出 PMT 的 PID,PMT 再写明视频、音频分别在哪几个 PID 上。播放器先解 PAT,再解 PMT,才能真正拿到音视频数据,这也是后面黑屏排查里第一个要查的对象。
头里还有一个 4 比特的 continuity counter,从 0 到 15 循环递增。接收端发现计数器不连续,就知道中间丢了 TS 包,能不能恢复画面取决于丢的是不是关键帧所在的分片。固定包长加连续计数,就是 TS 抗丢包的核心设计:它不求不丢包,只求丢包后能快速恢复边界,并且不会因为一个包坏掉而丢掉整段节目。
想亲眼看一个 TS 流里的节目结构,用 ffprobe 直接读:
ffprobe -show_programs -show_streams -of json input.ts这段命令把 PAT/PMT 解析结果和每条流的编码信息、PID、time_base 一次性打出来。实际排查时我一般先看 program 里有没有完整的音视频流,再看每条 stream 的 id(PID)和 codec_name。如果这里就缺流,后面播放器黑屏大概率是转封装阶段的问题,不用一开始怀疑播放端。
2.2 PTS/DTS 与 90kHz 时基:ffmpeg 转 TS 时时间戳怎么换算
TS 的时间体系建立在 27MHz 的系统时钟上,节目时钟参考 PCR 用它;而 PTS/DTS 是 33 比特、90kHz 刻度,也就是 1/90000 秒的精度。为什么是 90kHz?它和 27MHz 正好是 300 倍关系,一个 PCR 域里能放 300 个 90k 时钟周期,换算整数化不容易有精度损失。ffmpeg 内部每个 stream 有自己的 time_base,比如 H.264 常见 1/90000,AAC 在不同封装下可能是 1/1000 或 1/90000。进 mpegts muxer 时,所有时间戳统一换算到 90kHz,这一步由封装器自动完成。
这里最容易翻车的不是换算本身,而是输入流的时间戳就是坏的:录屏工具录出来的素材 PTS 跳变、dts 倒序,转封装时会原样带进 TS。时间戳处理里有两个容易混淆的操作:-fflags +genpts 表示输入没有可信 PTS 时按帧率重建;-copyts 则是把源时间戳原样保留,不按输出起始点归零。直播推流我一般不加 -copyts,让 muxer 从起点重新排列时间戳,播放器切台、重连时更容易对齐;只有录制归档需要保留原始绝对时间戳的场景才用 -copyts。
TS 特有参数里值得记住的是 GOP 长度。-g 会直接影响播放器丢包后的恢复速度。TS 的随机接入点是 IDR 帧,播放器持续丢包时,在下一个 IDR 之前画面只能处于等待状态。把 -g 设成 50(大约 2 秒一个关键帧)是推流场景的常见起点;画面内容基本静止时可以加到 100 省码率,但丢包概率高的链路宁可短一点。
2.3 传输承载怎么选:UDP 组播、TCP 长连与 HTTP 拉流
TS 只定义封装,不定义传输。相同的一路 TS 流可以用 UDP、TCP、HTTP 三种方式送出去,选型的核心是丢包策略和延迟预算。UDP 没有重传,丢包直接体现在画面上,但延迟能做到几十毫秒,局域网组播推流是典型用法。TCP 有重传,丢包不丢数据,代价是延迟会随重传增大,适合跨网、对完整性敏感的链路。HTTP 拉流更接近点播语义,客户端按需连接,但 ffmpeg 自身做 HTTP server 的能力有限,生产里更多是推给流媒体服务器再转 HLS 分发,那是另一个话题。
我维护某跨平台系统的传输链路时,局域网设备间用 UDP 组播,跨网用 TCP,公网对延迟不敏感的回看场景才考虑 HTTP。三者延迟预期分别是毫秒级、数百毫秒、秒级起。如果你的播放器同时支持多种输入,先用 ffplay 把各承载实测一遍,看卡顿和延迟再定方案,别只看协议文档。
| 承载方式 | 丢包处理 | 典型延迟 | 适合场景 |
|---|---|---|---|
| UDP 组播 | 无重传,丢包即花屏,靠下个 IDR 恢复 | 几十毫秒 | 局域网直播分发、多接收端 |
| TCP 长连 | 重传,延迟随丢包率增大 | 数百毫秒 | 跨网、Wi-Fi、完整性优先 |
| HTTP 拉流 | 重传,按需连接 | 秒级 | 点播、回看、公网分发 |
3. 用 ffmpeg 做 TS 传输的最小命令:转封装、UDP 推流与 TCP 拉流
原理落到命令就三件事:把素材变成 TS、把 TS 推到网络、把网络上的 TS 接住。下面这套命令是模拟项目X 里反复用的基底,参数少到能背下来,但每个参数都有理由。你能看到 -c copy 快在哪、-re 为什么不能省、pkt_size=1316 为什么比默认值可靠。
3.1 本地文件转 TS 封装:-c copy 与 Annex B 转换
ffmpeg -i input.mp4 -c copy -f mpegts -bsf:v h264_mp4toannexb output.ts逻辑说明:-c copy不做转码,只改封装,速度快、画质无损;-f mpegts指定输出封装格式;-bsf:v h264_mp4toannexb把 H.264 从 MP4 的 AVCC 长度前缀结构转成 TS 要求的 Annex B 起始码结构。新版 ffmpeg 在-c copy到 mpegts 时会自动插入这个 bitstream filter,日志里会出现 Auto-inserting bitstream filter 字样;老版本不会自动处理,显式加上-bsf:v才能保证所有版本行为一致。H.265 对应的是h265_mp4toannexb,AAC 音频不需要类似转换。
如果素材本身有 B 帧,拷贝封装后 PTS 顺序和 DTS 顺序不一致是正常的,TS 里允许这种重排,播放器按 PTS 显示。真正要检查的是读出后的 dts 是否单调递增:
ffprobe -show_packets -select_streams v -show_entries packet=pts,dts -of csv output.ts | head -20从输出里看 dts 有没有回退。如果回退严重,说明源素材时间戳是坏的,直接-c copy会把坏戳带进 TS,这时候先给输入加-fflags +genpts重建时间戳再转。
3.2 UDP 组播推流:-re 与 pkt_size=1316 的真实意义
ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -b:v 2M \ -maxrate 2M -bufsize 4M -g 50 -c:a aac -b:a 128k \ -f mpegts "udp://239.0.0.1:1234?pkt_size=1316"逻辑说明:-re让 ffmpeg 按源文件的真实帧率读取输入,否则它会以最快速度把整段推完,播放器看到的是一阵狂轰后断流。pkt_size=1316是 TS over UDP 的推荐包大小,1316 正好是 7 个 TS 包(7×188),加上 UDP 和 IP 头仍在标准 MTU 1500 之内,IP 层不会分片,避免分片重组失败导致的整包丢弃。
组播地址用 239.0.0.0/4 段,接收端必须在同一网段;有些网络设备需要 IGMP 配置,局域网直连交换机通常默认透传组播。编码参数上,-preset veryfast适合推流机 CPU 有限的场景;-bufsize 4M配合-maxrate 2M约束码率不要瞬间冲高,TS 传输中码率尖峰过高会撑爆接收端缓冲区。
提示:
pkt_size是 UDP 协议的 URL 参数,必须写在udp://地址的?后面,写到-f mpegts后面会被当作 muxer 参数直接报错。
3.3 拉流侧与 TCP 推流:把 TS 从 UDP 换到可靠通道
接收端最小命令:
ffplay -fflags nobuffer -i udp://239.0.0.1:1234-fflags nobuffer让播放器不做大缓冲,延迟降到最低;局域网内花屏明显时把 nobuffer 去掉,让播放器多缓存一点。要顺便落盘,就把输出换成文件:
ffmpeg -i udp://239.0.0.1:1234 -c copy -f mpegts record.tsUDP 换到 TCP 推流的场景,ffmpeg 支持 TCP 监听模式:
ffmpeg -re -i input.mp4 -c copy -f mpegts \ "tcp://0.0.0.0:8080?listen=1" -loglevel info逻辑说明:listen=1让 ffmpeg 作为服务端监听一个端口,等客户端连进来后再开始推流;接收端用tcp://本机IP:8080去拉。TCP 有重传保护,适合 Wi-Fi 或跨网这种丢包率高的链路。注意 TCP 推流只能服务一个客户端,多路分发还是要回到 UDP 组播或者流媒体服务器,这是 TCP 长连方案的边界,别指望一个监听端口带一堆接收端。
注意:推流脚本里把 loglevel 固定下来。默认 info 够用,排查断流时改成 debug 能看到 muxer 的每包输出,但平时别开,日志量太大反而掩盖问题。
4. MPEG-TS 传输避坑指南:时间戳跳变、PID 漂移与黑屏花屏排查
传输链路翻车极少只有一个原因,但反复遇到的集中在下面四类。每一条按现象、原因、解决的顺序写,你对照现场现象直接跳到你命中那一条就行。
4.1 花屏:MP4 拷贝成 TS 后 H.264 结构不兼容
现象:input.mp4 -c copy -f mpegts出来的文件在电脑播放器上正常,换到某个嵌入式播放器就花屏,或者起播就是黑屏。
原因:MP4 里的 H.264 用 AVCC 长度前缀表示 NALU,TS 里要求 Annex B 起始码。新版 ffmpeg 会自动插入 h264_mp4toannexb,但嵌入式播放器对输入码流要求更严格,只要转换链路里有一环没完成转换就会花屏。老版本 ffmpeg 或者脚本里-bsf:v参数位置写错,转换会被静默跳过,这是最隐蔽的坑。
解决:转封装命令显式加-bsf:v h264_mp4toannexb,不依赖自动插入;转完后用 2.1 节的 ffprobe 命令确认码流结构正常。如果播放端对 H.264 profile 也挑剔,顺手用-profile:v baseline做一次硬转码,牺牲一点画质换兼容性。
4.2 黑屏:PAT/PMT 变化或 PID 漂移
现象:推流进程第一次起播正常,重启后播放器一直黑屏;手动拖进度条能恢复,自动播放恢复不了。
原因:mpegts muxer 在每次 ffmpeg 进程重启时重新分配 PID,视频常落在 256、音频 257,但不会保证不变;PAT/PMT 也会重建。播放器第一次启动时解析到旧 PID,进程重启后还守着旧 PID 等数据,TS 层没有重新触发 PAT 扫描,于是黑屏。这类问题不在 TS 数据本身,而在播放器的节目表状态机。
解决:播放端在对应 PID 上连续 N 秒收不到包时,强制重新解析 PAT/PMT;推流端保持节目号、音视频顺序和编码参数不变,让 PMT 内容和之前尽量一致,降低播放器重建节目的成本。你说它是玄学也行,但每次都是同一套机制。
4.3 音画不同步:时间戳被污染没重建
现象:直播 20 分钟后音画差距超过 500ms;录制的 TS 文件反复拖进度条后同步恢复。
原因:输入源是录屏或拼接素材,PTS 在拼接处跳变;-c copy模式把坏戳原样带进 TS,播放器按跳变后的 PTS 显示,误差积累成明显不同步。拖进度条能恢复,是因为播放器在 seek 时做了一次重同步,掩盖了问题。
解决:推流命令加-fflags +genpts;如果源是拼接素材,先单独跑一遍 ffprobe 检测 dts 跳变,跳变超过 1 秒就不要贪快用 copy,直接走一次转码重新生成时间戳。音画不同步是最像玄学的一类问题,排查时一定要先确认时间戳,而不是去调播放器的缓冲。
4.4 UDP 推流在局域网也花屏:包太小与注入太快
现象:千兆局域网环境,UDP 推流依然马赛克,接收端 CPU 不高,网络也没断。
原因:默认 TS 推流按 188 字节一个 UDP 包发送,2Mbps 码率一秒就是 1300 多个包,接收端 socket 缓冲区瞬间被灌满,丢包从缓冲区溢出开始。另一个常见原因是漏了-re,ffmpeg 把文件里几分钟的内容几十秒推完,接收端根本处理不过来,丢包率直接飙升。
解决:推流端加pkt_size=1316,接收端加大缓冲区并允许溢出后继续运行:
ffmpeg -i "udp://239.0.0.1:1234?buffer_size=1M&overrun_nonfatal=1" -c copy -f mpegts out.tsoverrun_nonfatal=1让接收端在缓冲区溢出时丢掉来不及处理的包,而不是终止进程;buffer_size给到 1M 提供足够暂存。这一对参数是 UDP 传输链路里最常用的容错组合,比在播放器里调缓冲有效得多。
5. 从单路到多路:TS 复用、tee 分流与双进程分发的组合玩法
单路推流跑通后,下一步一般是多路复用和同时录制。TS 天生支持多节目,一路 188 字节流里可以同时装多路视频和多路音频,ffmpeg 对应有一套组合技巧。这个能力在直播场景很实用:一路组播端口送出去,接收端按节目号选择自己想要的那一路,网络层不用为每路视频单独开端口。
5.1 一个节目挂多路音视频:map 与 -program
ffmpeg -i main.mp4 -i audio.m4a -map 0:v -map 0:a -map 1:a \ -c:v copy -c:a aac -f mpegts -program program_num=1:st=0:st=1:st=2 multi.ts逻辑说明:-map 0:v选主视频,-map 0:a选主文件里的原音轨,-map 1:a再引入第二个文件的音轨。把三条流塞进 program 1,接收端在 PMT 里能看到三个 stream,播放器可以选择音轨,适合多语言直播。这里-program是较新 ffmpeg 的能力,老版本只支持默认单节目,多路流堆在同一个节目里,播放器按 PID 区分一样能选轨,只是节目表结构没有分得那么清楚。
多路视频共用一路 TS 也是常见需求,但要注意:参与复用的视频必须参数接近,尤其是分辨率和帧率差太多时,播放器切换节目会明显卡顿。复用本身不转码,参数差异只能由上游统一。
5.2 一路码流同时推流与落盘:tee muxer
ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast \ -b:v 2M -g 50 -c:a aac -f tee \ "[f=mpegts]udp://239.0.0.1:1234?pkt_size=1316|[f=mpegts]record.ts"逻辑说明:tee 把编码后的同一路码流分发到多个输出,输出之间用|分隔。左边推 UDP,右边落盘,不用跑两个 ffmpeg 进程,CPU 省一半。-f tee后面的方括号里是每个输出各自的封装和地址配置,落盘文件那路如果磁盘慢,可以加-use_fifo 1开启异步写盘,避免磁盘抖动拖住推流。
tee 的边界要讲清楚:所有输出拿到的是同一份编码结果,做不到“推流走 1Mbps、落盘存 5Mbps”。遇到这种需求,要么开两个进程分别编码,要么先存高码率,推流端再从录制文件转一份低码率出来,没有第三条捷径。
5.3 不同目标不同码率:双进程与中间文件
ffmpeg -re -i input.mp4 -c:v libx264 -b:v 1M -f mpegts udp://239.0.0.1:1234?pkt_size=1316 & ffmpeg -re -i input.mp4 -c:v libx264 -b:v 5M -f mpegts record.ts & wait两个进程后台并行,各自解码一次原始文件,码率互不影响。代价是 CPU 翻倍。CPU 不够时改成中间文件方案:先转一次高码率 TS 落地,再让两个进程都从这个中间文件-c copy取流,一个推一个存:
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -preset medium -f mpegts mid.ts ffmpeg -re -i mid.ts -c copy -f mpegts "udp://239.0.0.1:1234?pkt_size=1316" & ffmpeg -re -i mid.ts -c copy -f mpegts record.ts & wait中间文件选 TS 而不是 MP4,喂给-c copy时不用再担心 Annex B 转换问题,少一层坑。双进程还有一个隐藏注意点:两个进程同时读同一个输入文件时,如果源文件在推流过程中被切割或覆盖,两路会同时翻车。我一般给源文件加只读权限,或者干脆用独立的输入副本,这个习惯救过我好几次。
6. 推流之后别急着走:验证 TS 链路的三道自查命令
推流完成不等于传输没问题。我每次搭完一条 mpeg-ts 传输链路,都跑三道自查,全过才敢交给下游。这三道命令都不改链路本身,只是观测,但能过滤掉大部分封装层面的隐患。
第一道,错误烟测。让 ffmpeg 拉流 30 秒,丢弃输出,只留错误日志:
ffmpeg -v error -i udp://239.0.0.1:1234 -t 30 -f null -30 秒内没有任何错误输出,说明封装基本健康;如果反复出现 Packet corrupt 或 cur_dts is invalid,说明上游时间戳或 PID 结构有问题,直接回到第 4 章对应条目查。
第二道,节目与包层面检查:
ffprobe -show_programs -show_streams -of json udp://239.0.0.1:1234重点看 program 数量、每个 stream 的 codec_type 和 id,和推流端脚本里预期的 PID 对照。这里有个血泪经验:推流脚本开头一定要写参数注释,GOP、码率、pkt_size、是否-re,一行都不能省。没有这张参数卡片,链路出问题时你连自己刚才推的是什么配置都想不起来。
第三道,连续性抽样。拉一段包的 PTS/DTS 和关键帧标记:
ffprobe -show_packets -select_streams v -show_entries packet=pts,dts,flags -of csv udp://239.0.0.1:1234 | head -50看 dts 是否单调、关键帧 flags 是否标 K。这两项过了,再配第一道的零报错,这条 TS 链路我才会放心交接。我的习惯是三道命令写进同一个验证脚本,每次改完推流参数就重跑一遍,比靠眼睛盯播放器窗口可靠得多。希望帮到你。
本文还有配套的精品资源,点击获取