ARTICLE DETAIL

资讯详情

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

FFmpeg SEI嵌入实战:RTMP推流携带自定义数据的完整指南

FFmpeg SEI嵌入实战:RTMP推流携带自定义数据的完整指南 搞直播的同学应该都遇到过这种需求推流的过程中想在视频流里塞点“私货”比如题目ID、时间戳、比分、弹幕指令、抽奖事件甚至是端到端的业务信令。表面上看RTMP有metadata可以用真到了线上才发现metadata限制多、丢数据、跟画面不同步折腾得人很崩溃。后面我转向了SEISupplemental Enhancement Information补充增强信息配合FFmpeg在RTMP推流里嵌自定义数据才算把这类需求稳定落地。这篇文章我就把从原理到实操的完整路线写清楚包括命令行快速验证、C API动态注入、拉流端提取以及在真实链路里容易踩的坑适合正在做直播互动、低延迟数据同步、视频打点这类功能的朋友参考。1. 为什么是SEIRTMP推流里塞自定义数据的几条路子1.1 SEI到底是什么SEI是H.264/H.265视频编码标准里定义的一种数据载体。视频编码后输出的是一串NALUNetwork Abstraction Layer Unit其中大部分NALU是真正的图像数据IDR、P帧、B帧等而SEI属于“附加说明信息”专门用来携带与视频内容相关的辅助数据解码器可以选择性解析不影响画面本身。以H.264举例SEI NALU的nal_unit_type是6H.265里则分为Prefix SEI类型39和Suffix SEI类型40。SEI内部有payloadType和payloadSize字段其中payloadType5对应的是user_data_unregistered非注册用户数据格式是“16字节UUID 自定义数据”。这个类型是FFmpeg和各种编解码器支持最完整的一种也是我们在RTMP推流中嵌入自定义数据时最常用的载体。SEI最大的特点是它和视频帧共享同一条传输通道。也就是说SEI数据封装在视频编码流内部跟着视频帧一起打包、一起推流、一起到达播放端天然具备帧级同步能力。对于“我在第100帧画面出现的时候同时告诉客户端一个数据”这种需求SEI几乎是唯一能做到“零额外延迟、严格帧同步”的方案。1.2 对比FLV metadata、旁路信令和SEIRTMP推流中携带自定义数据的方案主要有三类FLV metadata、旁路信令通道、SEI。这里我直接用表格对比一下大家看得更清楚。对比项FLV metadata旁路信令WebSocket/HTTPSEI同步精度与视频帧无关容易漂移完全独立延迟波动大与视频帧严格同步实现复杂度低FFmpeg可直接写高需额外部署信令服务中需要编解码侧配合数据大小受限且部分播放器敏感几乎不受限理论不受限实践建议1KB以内兼容性很多播放器忽略无视频链路兼容问题需要解码器/播放器支持延迟较低但无法精确到帧取决于网络无法帧同步零额外延迟随帧到达典型场景码率、分辨率等静态信息弹幕、连麦指令答题、打点、实时数据同步从表中能看出来metadata适合放“不经常变化且不要求帧同步”的信息旁路信令适合放“允许一定延迟且数据量较大”的业务信息而SEI适合的是“必须跟某一帧画面严格对应”的自定义数据。实际项目中三者往往互相补充但如果核心诉求是“高效嵌入、帧级同步”SEI是最优解。1.3 哪些场景适合用SEI我总结了几类最典型的落地场景大家可以对照自己的需求判断直播答题/互动当主播发起答题时在视频帧中嵌入题目ID和选项数据客户端播放到该帧时立即弹出答题界面体验上完全无延迟。实时比分/数据打点体育赛事直播中把比分、球员数据、文字链等塞进SEI播放器渲染字幕时能和镜头切换精确配合。广告监播/内容审核在视频流里埋入时间戳或内容ID排障时能精准定位具体帧便于追责和排查。低延迟数据同步云游戏、远程协作场景中在帧内携带操作序列号保证端到端同步。本地录制标记推流的同时本地落盘SEI里写入录制节点信息后期剪辑时方便按标记切片段。2. 动手前的准备版本、推流链路和数据格式设计2.1 FFmpeg版本与推流链路规划先说版本。如果你打算用命令行快速验证建议使用FFmpeg 5.1及以上版本因为从这一版本开始提供了专门的sei滤镜可以很方便地在编码前插入SEI。如果你打算走C API方式在AVFrame上挂SEI数据那么FFmpeg 4.0以上版本都支持。我自己常用的组合是FFmpeg 6.0以上版本配合libx264/libx265编码器稳定性实测没问题。推流链路建议从最简单的开始验证本地视频文件 - FFmpeg - RTMP服务器 - 拉流端ffprobe/ffplay。RTMP服务器可以用Nginx-RTMP、SRS或其他常见流媒体服务器本机起一个就行。演示地址我用通用的占位写法大家替换成自己服务器的地址即可。rtmp://your-rtmp-server/live/stream链路规划时有一个容易被忽略的点SEI能否保留取决于中间的转封装和转码环节。如果是纯转封装比如RTMP转HLSSEI通常会保留一旦经过转码服务如云转码、AI审核、低码率压缩SEI很可能被编码器丢弃。所以正式使用前一定要确认从推流端到播放端整条链路中没有“会重编码并丢弃SEI”的环节。2.2 自定义数据格式设计SEI是二进制通道理论上你想塞什么字节都行但实际项目中如果没有统一格式后面解析会非常痛苦。我强烈建议在SEI payload中采用“版本号 业务JSON 扩展字段”的结构。举个例子[1字节版本号] [2字节消息类型] [4字节时间戳] [N字节JSON数据]其中JSON部分可以灵活扩展比如{ msg: quiz_start, question_id: 10086, expire_ts: 1710000000 }另外user_data_unregistered类型需要16字节UUID。UUID的作用是标识这个SEI的“业务归属”拉流端看到这个UUID才知道“这是我关心的数据按我的协议解析”。FFmpeg官方示例里常用全零UUID但真实项目中建议用uuidgen生成一个固定值避免和其他业务数据混淆。关于数据大小我建议把单条SEI负载控制在1KB以内。SEI本身允许更大的数据但RTMP推流是实时传输负载太大会导致视频帧膨胀增加网络拥塞和解码延迟。如果业务数据超过1KB优先考虑压缩比如zlib或者改成旁路信令通道。2.3 验证基线先用FFmpeg确认SEI能被保留在写复杂代码之前先用最简单的方式验证“RTMP链路是否保留SEI”这一步能省去后面很多排查时间。用FFmpeg命令行往视频里插入一条静态SEI然后拉流查看。ffmpeg -re -i input.mp4 -vf seitypeunregistered:datahello_sei -c:v libx264 -preset veryfast -tune zerolatency -c:a copy -f flv rtmp://your-rtmp-server/live/stream这条命令做了三件事读取本地视频、在编码前用sei滤镜插入一条unregistered类型SEI、通过FLV封装推送到RTMP服务器。等推流跑起来后再用ffprobe拉流检查ffprobe -v error -select_streams v -show_frames -show_data rtmp://your-rtmp-server/live/stream如果SEI保留正常输出帧数据里能看到SEI相关的NALU信息如果看不到就要检查推流端版本、编码器参数和服务器配置了。这个基线测试只需要几十秒却能直接暴露“链路不支持SEI”的大问题。3. 核心实现把自定义数据高效嵌入SEI3.1 方案一命令行sei滤镜快速验证命令行方式适合快速验证、联调测试和临时需求核心是FFmpeg的sei滤镜。这个滤镜可以插入unregistered类型的SEI数据可以通过data参数传原始字符串也可以用hex参数传十六进制编码的二进制数据。# 字符串方式适合可读文本 ffmpeg -re -i input.mp4 -vf seitypeunregistered:datahello_sei -c:v libx264 -preset veryfast -tune zerolatency -c:a copy -f flv rtmp://your-rtmp-server/live/stream # 十六进制方式适合二进制业务数据 ffmpeg -re -i input.mp4 -vf seitypeunregistered:hex7b226d7367223a2268656c6c6f227d -c:v libx264 -preset veryfast -tune zerolatency -c:a copy -f flv rtmp://your-rtmp-server/live/streamhex参数后面跟的是一串十六进制字符串比如7b226d7367223a2268656c6c6f227d解码后就是{msg:hello}。这种方式适合验证“二进制数据能否完整穿透链路”。命令行方案的优点是零代码、见效快缺点也很明显SEI内容是静态写死的无法根据业务状态动态生成。比如你想在直播过程中根据后台发来的题目动态嵌SEI命令行就搞不定了。此外sei滤镜默认是逐帧插入的如果只希望在关键帧插入命令行实现起来也比较别扭。所以命令行方案适合“验证链路”和“固定数据压测”不适合生产动态数据。3.2 方案二C API在编码前挂side data推荐动态场景生产环境里要动态、高效地嵌SEI我推荐走C API在AVFrame上挂AV_FRAME_DATA_SEI_UNREGISTERED类型的side datalibx264/libx265编码器会自动把它编码成SEI NALU输出。这种方式的好处是数据在内存里动态拼好随帧提交编码器无缝编码完全不依赖外部进程。核心代码思路如下#include libavutil/frame.h // 假设frame是已经准备好的AVFramedata和data_len是业务数据 size_t sei_size 16 data_len; // 16字节UUID 业务数据 AVFrameSideData *sd av_frame_new_side_data(frame, AV_FRAME_DATA_SEI_UNREGISTERED, sei_size); if (!sd) { // 处理分配失败 } // 前16字节写固定UUID uint8_t uuid[16] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; memcpy(sd-data, uuid, 16); // 后面写入真正的业务数据 memcpy(sd-data 16, data, data_len); // 正常送入编码器 ret avcodec_send_frame(enc_ctx, frame);这段代码的核心点在于av_frame_new_side_data负责分配并关联SEI数据编码器在编码时会检查AVFrame的side data列表如果发现AV_FRAME_DATA_SEI_UNREGISTERED类型的数据就把它写入编码后的视频流中。你不需要手动拼NALU也不用管H.264还是H.265编解码器都帮你处理了。我惯用的做法是在推流线程里维护一个“待发送SEI队列”。业务模块比如答题服务、数据打点服务把数据写入队列推流线程每编码一帧之前检查队列里是否有新数据有就挂到当前帧上没有就正常编码。这样可以精确控制“哪一帧携带哪条数据”同时避免每一帧都塞无用数据导致码率膨胀。关于帧类型如果业务要求“只在关键帧嵌SEI”可以在提交前检查帧类型// 只处理关键帧 if (frame-pict_type AV_PICTURE_TYPE_I) { // 挂side data }实际测试中libx264会在关键帧的码流中输出SEI NALU位于SPS/PPS之后、图像数据之前播放器解码到关键帧时能立刻拿到SEI。这种方案比命令行灵活太多推荐作为生产首选。3.3 方案三自定义滤镜或修改编码器可选如果不想写完整的C程序又想实现动态数据嵌入还有一个折中思路写一个自定义FFmpeg滤镜。滤镜读取外部输入比如本地socket、管道、消息队列把收到的数据动态挂到每帧的side data上然后继续走编码流程。这种方案的优点是能复用FFmpeg命令行框架缺点是滤镜代码的调试和维护成本不低适合有“既要动态数据、又要低开发量”需求的朋友。至于修改libx264源码、通过x264的SEI API注入数据属于更底层的做法一般只有需要极致控制SEI位置和格式时才会考虑。常规项目不推荐毕竟FFmpeg官方已经支持side data方式没必要自己维护一套编解码器补丁。三种方案的选择逻辑很清晰快速验证用命令行生产动态数据用C API挂side data团队有能力且需要定制行为再考虑自定义滤镜或改编码器。4. 拉流端怎么把SEI取出来4.1 命令行验证trace_headers和ffprobe数据推上去了拉流端怎么确认SEI在不在、内容对不对最直接的方法是使用trace_headersbitstream filter。在FFmpeg中用-bsf:v trace_headers可以将码流中的NALU信息打印出来包括SEI的payloadType和内容摘要。ffmpeg -i rtmp://your-rtmp-server/live/stream -c:v copy -bsf:v trace_headers -f null -输出中重点看NALU类型为6的单元对应的就是H.264 SEI。如果SEI的payloadType是5user_data_unregistered里面会显示UUID和业务数据。看到这些信息就说明SEI已经从推流端完整走到了拉流端。如果不想看这么细用ffprobe也能做基础验证ffprobe -v error -select_streams v -show_frames -show_data rtmp://your-rtmp-server/live/streamffprobe会把视频帧的AVPacket数据打印出来SEI NALU的起始码比如00 00 00 01 06会在数据里出现。查一下有没有这个起始码基本能确认SEI是否存在。缺点是不太直观数据量大时不好定位建议作为辅助手段。4.2 解码端C API提取SEI拉流端如果是自研播放器或处理程序可以通过解码后的AVFrame提取SEI。解码器在解析到SEI NALU时会自动把它放到AVFrame的side data里类型同样是AV_FRAME_DATA_SEI_UNREGISTERED。AVFrameSideData *sd av_frame_get_side_data(frame, AV_FRAME_DATA_SEI_UNREGISTERED); if (sd sd-size 16) { uint8_t *uuid sd-data; // 前16字节是UUID uint8_t *payload sd-data 16; // 后面是业务数据 size_t payload_len sd-size - 16; // 根据UUID判断是不是自己关心的数据再按自定义协议解析payload }和推流端一样解码端的核心也是AV_FRAME_DATA_SEI_UNREGISTERED这个side data类型。只要解码器比如FFmpeg的h264解码器支持SEI解析你就能在拿到每一帧的同时拿到这一帧携带的业务数据帧级同步在这里自然完成。值得补充的是在音频和视频时间戳对齐方面SEI数据是跟着视频帧走的所以如果需要把SEI和音频播放进度关联需要额外做音视频时间戳映射。具体做法可以按视频帧的pts换算成统一的播放时钟再和音频pts对比。4.3 播放器兼容性提醒SEI在播放器层面的兼容性是这几年咨询我最多的问题。先说结论FFmpeg系的播放器ffplay、VLC、ijkplayer等都能正确解析SEIAndroid的MediaCodec、iOS的VideoToolbox底层也会解析SEI但给不给上层透传要看应用层怎么封装浏览器MSE场景比较特殊浏览器解码视频后不会主动把SEI传给JavaScript而且部分场景下MSE会把SEI NALU过滤掉。如果你的播放端是Web最稳妥的做法有两种一种是用WASM解析原始FLV/TS流在解封装阶段把Sei NALU抠出来另一种是服务端剥离SEI后通过WebSocket/HTTP推送客户端收到数据和视频帧后按时间戳自行对齐。我在实际项目中更推荐后者因为服务端剥离SEI实现简单、稳定性高Web端只需要处理普通的信令逻辑。5. 实战踩坑记录与优化建议5.1 常见问题速查表排障时先看这个表能定位大部分问题。问题现象可能原因解决办法推流成功后拉流看不到SEI中间环节转码丢弃SEI排查转码服务改用纯转封装链路SEI数据到达但解析乱码UUID或协议版本不匹配统一UUID和消息格式增加版本号视频延迟明显增高SEI负载过大或每帧都插限制负载大小按关键帧/低频发送播放器黑屏或花屏手动拼SEI NALU长度错误用AVFrame side data避免手写NALU部分播放器拿不到SEI播放器不解析/不透传SEI改用服务端剥离信令下发的方案编码器报错或警告SEI side data分配失败检查内存管理确认编码器版本支持5.2 我实际踩过的几个坑第一个坑是负载过大。早期做一个直播答题项目产品经理要求把题目图片的URL、选项、文案全部塞进SEI一条数据到了5KB。结果推流端码率飙升弱网环境下视频卡顿明显很多播放器解码压力也变大。后来做了两层处理一是只传题目ID客户端根据ID去拉详情二是SEI负载超过1KB时自动降级为旁路信令。这个经验后来一直沿用SEI里只放强同步要求的最小必要信息其余数据走带外通道。第二个坑是每帧都塞SEI导致I帧过大。刚开始实现时为了省事直接在每一帧上挂side data结果I帧大小暴涨关键帧传输耗时增加播放器缓冲时间变长。后来改成“事件驱动”模式只有业务状态变化时才在下一帧挂SEI并且优先挂在关键帧上。这样既保证了及时性又不会持续增加码率。第三个坑和服务器有关。有一次线上链路是“推流端 - CDN转码 - 播放端”结果SEI在CDN转码后被全部丢弃客户端拿不到任何业务数据。当时踩了这个坑才意识到SEI能不能穿透链路必须在方案设计阶段就和CDN供应商确认。如果链路里有“必须保留SEI”的硬性要求要么选择不转码的节点要么找支持SEI透传的转码服务。第四个坑是手动构造SEI NALU时长度字段写错。H.264的SEI长度可以采用1字节、2字节或4字节扩展表示负载超过255字节时就需要扩展长度字段。有些开发者图省事直接手写NALU长度字段一旦写错轻则播放器解析错误重则花屏。我后来统一改用AVFrame side data方式长度和封装都由库内部处理再没出过这类问题。5.3 性能与稳定性建议结合几个线上项目的经验我总结了几条SEI嵌入的稳定性建议控制发送频率。SEI数据没有强实时要求时建议只在关键帧发送频率控制在每2到4秒一条避免不必要的码率开销。给SEI协议增加版本号。后续业务升级时旧客户端可以根据版本号忽略无法解析的数据不会因为格式变化崩溃。数据要幂等。播放端可能丢失一帧、也可能重复处理同一帧业务处理逻辑要保证重复收到SEI不会产生副作用。做好降级容灾。SEI偶尔丢失是正常现象比如网络抖动、播放器跳帧。客户端必须保证“没有SEI也能正常播放”SEI只是增强体验不能让播放能力依赖它。上线前全链路压测。分别在推流端、RTMP服务器、CDN、播放端四个节点抓包确认SEI存在性和完整性避免端到端问题暴露在线上。最后再分享一点个人的实践经验做这类“在视频流里塞业务数据”的功能最有价值的一件事就是先用最小链路把风险点摸清。我第一次做SEI嵌入时花了一整天在纠结命令行参数和滤镜用法后来发现真正要命的问题反而不是怎么塞数据而是中间链路会不会丢。现在我的工作流程固定是本地单机验证SEI生成和解析再穿透RTMP服务器验证最后才上CDN直播环境做压测。整个过程里命令行方案是最快的验证工具C API方案是最可靠的生产手段这两个组合我推荐大家都掌握。如果你们也在做类似直播互动、数据打点、端到端同步的需求希望这篇文章能帮你们少踩几个坑。
返回列表