简介这份源码资源面向音视频开发初学者与进阶工程师聚焦于使用FFmpeg API将内存中的H264裸流封装为FLV格式并通过RTMP协议完成直播推流这一典型场景。资源包内仅含1个C语言源文件压缩包约3KB体量轻巧却覆盖了从解码、封装到网络推流的完整链路适合作为理解FFmpeg流媒体处理流程的入门参考。源码中涉及av_register_all、avformat_network_init等初始化调用H264 NAL单元解析FLV Tag结构转换以及av_interleaved_write_frame持续推流与资源释放等关键环节能帮助读者建立对内存IO、时间戳同步和RTMP连接管理的直观认识。目前已有964人学习下载对于想深入实时音视频应用、提升网络编程与多媒体编码技术深度的开发者而言是一份便于快速上手和二次调试的实践素材。1. 从一份 H264 转 RTMP 源码说起直播推流链路里最该先啃透的那块骨头如果你手头正好有一份 H264 转 RTMP 的源码包想把它跑起来、或者想搞清楚直播推流到底是怎么把一帧画面送到服务器上的那这篇拆解笔记就是写给你的。这个项目干的事情很纯粹把本地 H264 裸流或 MP4 文件通过 FFmpeg 封装成 FLV 格式再走 RTMP 协议推到流媒体服务器。它适合两类人——一类是刚接触音视频、想找一个能跑通的最小推流 Demo 来建立体感的新手另一类是做直播客户端、想搞清楚 FLV Tag 怎么切、时间戳怎么打、RTMP 握手和 Chunk 怎么发的熟手。整个链路不长但每一环都有坑下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆开讲。2. 项目定位与技术选型为什么是 FFmpeg FLV RTMP 这套组合2.1 这个项目到底解决了什么问题直播推流这条链路往大了说可以拆成采集、编码、封装、传输四段。这份源码不碰采集和编码它假设你手里已经有 H264 数据了——可能是一个.h264裸流文件也可能是一个 MP4 文件里读出来的视频包。它要解决的是后两段把 H264 按 FLV 的格式封装成 Tag再通过 RTMP 协议一包一包发到服务器。为什么是 FLV 而不是 MP4因为 RTMP 协议原生承载的就是 FLV Tag。FLV 的结构非常简单一个 Header 加一串 Tag每个 Tag 要么是 Audio、要么是 Video、要么是 Script Data。RTMP 在传输层把 FLV Tag 再切成 Chunk 发出去接收端拼回 Tag 就能还原出流。这套设计在低延迟场景下非常高效所以直播推流至今仍然是 FLV over RTMP 的天下。为什么用 FFmpeg因为封装这件事看起来简单实际上要处理时间戳基准、SPS/PPS 的 AVCDecoderConfigurationRecord 封装、AAC 的 AudioSpecificConfig 等等。FFmpeg 的libavformat已经把 FLV 封装器写好了你只需要把 H264 包喂给av_interleaved_write_frame它帮你处理一切。自己从零写 FLV 封装不是不行但没必要除非你要做极致的定制。2.2 核心依赖与版本选择这个项目依赖的库不多但版本搭配有讲究。常见做法是依赖作用版本建议FFmpeg封装 FLV、推 RTMP4.x 或 5.x 均可4.4 最稳librtmpRTMP 协议实现通常由 FFmpeg 内置无需单独装zlibFLV 某些场景下的压缩系统自带即可FFmpeg 在 4.x 到 5.x 之间有几个 API 有变动比如av_register_all在 4.x 之后就不再需要了avcodec_register_all也一样。如果你拿到的源码里还有这两个调用编译时可能会报 deprecated 警告但一般不影响运行。真正需要注意的是av_interleaved_write_frame的返回值处理以及AVPacket的pts/dts设置这两个地方出错会导致推流花屏或者服务器直接断连。提示如果你用的是 FFmpeg 5.xav_register_all已经被移除源码里如果有这行需要注释掉。4.x 则保留也无妨。2.3 整体数据流从 H264 到 RTMP 包整个数据流可以画成一条直线H264 裸流/MP4 → av_read_frame 读包 → 设置 pts/dts → av_interleaved_write_frame → FLV Tag → RTMP Chunk → 服务器关键点在于av_interleaved_write_frame这个函数。它名字里的 “interleaved” 意思是音视频交错写入但如果你只推视频它其实只做封装和发送。它会根据你设置的AVFormatContext的oformat来决定封装格式这里我们设成flv。然后它会调用 FLV 的write_packet回调把 H264 包转成 FLV Video Tag再通过 RTMP 协议发出去。时间戳是这条链路上最容易翻车的地方。FLV 的时间戳单位是毫秒而 H264 的pts通常是基于time_base的。你需要用av_rescale_q把pts从流的time_base转到毫秒。如果转错了服务器收到的流要么快进要么慢放甚至直接拒绝。3. 跑起来从编译到推流成功的完整步骤3.1 环境准备与编译先确认系统里有没有 FFmpeg 的开发库。Linux 下用包管理器装最省事# Ubuntu/Debian sudo apt-get install libavformat-dev libavcodec-dev libavutil-dev # CentOS/RHEL sudo yum install ffmpeg-devel如果你要自己编译 FFmpeg记得开--enable-librtmp否则推 RTMP 时会报 “Protocol not found”。编译源码包里的代码通常就是一个Makefile或者CMakeLists.txt。如果是 Makefile直接make就行如果是 CMake标准流程mkdir build cd build cmake .. make编译时如果报undefined reference to av_register_all说明你用的 FFmpeg 版本太新把源码里那行删掉或者用宏包起来。3.2 推流地址与参数配置推流地址的格式是rtmp://服务器地址:端口/应用名/流名。比如rtmp://127.0.0.1:1935/live/test。这个地址通常写在源码的某个宏或者配置文件里找到它改成你自己的。除了地址还有几个参数需要关注帧率影响时间戳的步进。如果源码里写死 25fps但你实际流是 30fps推上去会变慢。分辨率不影响推流本身但影响服务器端的转码策略。码率如果源码里有码率控制注意别设得太大否则网络跟不上会丢包。常见做法是把这些参数放到一个结构体里从命令行读取。如果源码里是硬编码的建议先改成可配置的方便调试。3.3 启动服务器与验证推流本地测试最方便的是用nginx-rtmp-module或者SRS。这里以 nginx-rtmp 为例配置文件里加一段rtmp { server { listen 1935; application live { live on; record off; } } }启动 nginx 后运行你的推流程序。如果一切正常你会看到程序打印出推流的包数和时间戳。用ffplay拉流验证ffplay rtmp://127.0.0.1:1935/live/test如果 ffplay 能播出来说明推流成功。如果黑屏或者报错先看推流端的日志再看服务器端的日志。常见问题是时间戳从 0 开始但服务器要求从某个值开始或者 SPS/PPS 没有正确封装进 FLV 的 AVCDecoderConfigurationRecord。注意有些服务器要求第一个视频 Tag 必须是 AVC sequence header也就是包含 SPS/PPS 的那个包。FFmpeg 的 FLV 封装器会自动处理但如果你手动改过AVPacket的flags可能会破坏这个逻辑。4. 核心模块拆解读包、封装、发送三段式4.1 读包与时间戳处理读包这一步如果输入是 MP4用av_read_frame循环读如果是 H264 裸流需要自己解析 NALU 或者用h264_mp4toannexb做转换。下面是一段典型的读包代码AVPacket pkt; av_init_packet(pkt); pkt.data NULL; pkt.size 0; while (av_read_frame(fmt_ctx, pkt) 0) { if (pkt.stream_index video_stream_index) { // 将 pts 从流的 time_base 转为毫秒 int64_t pts_ms av_rescale_q(pkt.pts, video_stream-time_base, (AVRational){1, 1000}); int64_t dts_ms av_rescale_q(pkt.dts, video_stream-time_base, (AVRational){1, 1000}); // 设置到 packet 上供 FLV 封装器使用 pkt.pts pts_ms; pkt.dts dts_ms; // 写入输出上下文内部会封装成 FLV Tag 并发送 int ret av_interleaved_write_frame(out_fmt_ctx, pkt); if (ret 0) { fprintf(stderr, Error writing frame: %s\n, av_err2str(ret)); break; } } av_packet_unref(pkt); }这段代码的关键在av_rescale_q。video_stream-time_base通常是{1, 90000}或者{1, 1000}取决于容器。MP4 里常见的是{1, 90000}而 FLV 要求毫秒所以必须转。如果不转时间戳会差 90 倍推上去直接飞起。另一个点是pkt.pts和pkt.dts的区别。H264 有 B 帧时pts和dts不一样。FLV 封装器需要正确的dts来排序pts来显示。如果你只设了pts没设dtsB 帧场景下会花屏。4.2 FLV 封装与 AVCDecoderConfigurationRecordFFmpeg 的 FLV 封装器在写第一个视频包之前会先写一个 AVC sequence header。这个 header 的内容就是AVCDecoderConfigurationRecord里面包含 SPS 和 PPS。如果你是从 MP4 读的流extradata里已经有这些信息如果是裸流需要自己构造。构造AVCDecoderConfigurationRecord的代码大概长这样// 假设 sps 和 pps 已经拿到 int sps_len ...; int pps_len ...; // 计算 extradata 大小5 字节头 sps 1 2 pps int extradata_size 5 sps_len 1 2 pps_len; uint8_t *extradata av_mallocz(extradata_size AV_INPUT_BUFFER_PADDING_SIZE); extradata[0] 1; // configurationVersion extradata[1] sps[1]; // AVCProfileIndication extradata[2] sps[2]; // profile_compatibility extradata[3] sps[3]; // AVCLevelIndication extradata[4] 0xff; // lengthSizeMinusOne 3 extradata[5] 0xe1; // numOfSequenceParameterSets 1 extradata[6] (sps_len 8) 0xff; extradata[7] sps_len 0xff; memcpy(extradata[8], sps, sps_len); int offset 8 sps_len; extradata[offset] 1; // numOfPictureParameterSets extradata[offset 1] (pps_len 8) 0xff; extradata[offset 2] pps_len 0xff; memcpy(extradata[offset 3], pps, pps_len); // 设置到 codecpar 的 extradata video_stream-codecpar-extradata extradata; video_stream-codecpar-extradata_size extradata_size;这段代码看起来繁琐但每一步都有意义。lengthSizeMinusOne设为 3 表示 NALU 长度用 4 字节表示这是 FLV 的标准。如果设错了服务器解析 NALU 时会错位。提示如果你用 FFmpeg 的avformat_write_header之前就设好了extradata封装器会自动写 sequence header。如果没设它会等到第一个视频包才写可能导致服务器端首帧黑屏。4.3 RTMP 发送与 Chunk 分片RTMP 发送这一步FFmpeg 的av_interleaved_write_frame内部会调用rtmp_write。RTMP 协议把 FLV Tag 切成 Chunk每个 Chunk 有一个 header包含csid、timestamp、msg_length、msg_type等信息。默认的 Chunk Size 是 128 字节但握手之后可以协商更大的值比如 4096这样能减少 header 开销。如果你发现推流延迟很大可以检查一下 Chunk Size 有没有协商。FFmpeg 默认会发Set Chunk Size消息但有些服务器不支持太大的值。常见做法是设成 4096兼顾效率和兼容性。另一个影响延迟的是av_interleaved_write_frame的缓冲行为。它内部有一个AVIOContext的缓冲区默认 32KB。如果包很小可能会攒一会儿才发。要降低延迟可以调小这个缓冲区或者用avio_flush强制刷新。// 设置输出上下文的缓冲区大小 out_fmt_ctx-pb-buffer_size 4096;这行代码要在avio_open之后、avformat_write_header之前设置。设太小会增加系统调用次数设太大增加延迟4096 是一个折中。5. 避坑与排查推流失败、花屏、延迟高的常见原因5.1 推流直接失败报 “Protocol not found”现象程序启动后立刻退出日志里写着Protocol not found或者rtmp not found。原因FFmpeg 编译时没有开--enable-librtmp或者系统里的 FFmpeg 库是精简版不带 RTMP 协议。解决用ffmpeg -protocols | grep rtmp检查。如果没有输出需要重新编译 FFmpeg 或者换一个带 RTMP 的版本。Linux 下也可以装libavformat-dev的完整版通常包含 RTMP。5.2 推流成功但播放花屏现象ffplay 能连上但画面是绿的或者马赛克。原因最常见的是pts/dts设置错误或者 SPS/PPS 没有正确封装。B 帧场景下如果dts没设对解码器无法正确排序。解决先确认av_rescale_q的目标time_base是{1, 1000}。然后检查extradata是否在avformat_write_header之前设置。如果是从 MP4 读的流extradata通常已经有了不需要手动构造。如果是裸流按 4.2 节的代码构造。5.3 推流延迟越来越高现象刚开始延迟正常跑几分钟后延迟累积到几秒甚至十几秒。原因时间戳没有递增或者av_interleaved_write_frame的缓冲区没有及时刷新。也可能是服务器端的缓冲策略。解决检查pts是否单调递增。如果输入是循环读同一个文件时间戳会重置导致服务器认为流倒退了。需要在每次循环时加上一个偏移量。另外调小AVIOContext的buffer_size并在每次写包后调用avio_flush。5.4 服务器拒绝连接报 “Handshake failed”现象TCP 连接建立后RTMP 握手失败。原因推流地址的端口或者应用名不对或者服务器要求特定的tcUrl。解决用ffmpeg -i input.mp4 -f flv rtmp://...测试同样的地址。如果 ffmpeg 能推说明地址没问题是你的代码里AVFormatContext的filename或者oformat设错了。检查avformat_alloc_output_context2的第二个参数是不是flv。5.5 编译时报 “deprecated” 警告现象一堆av_register_all、avcodec_register_all的警告。原因FFmpeg 4.x 之后这些函数被标记为废弃5.x 直接移除。解决4.x 下可以忽略或者用#if LIBAVFORMAT_VERSION_INT AV_VERSION_INT(58, 9, 100)包起来。5.x 下直接删掉。这些函数在新版里已经不需要调用了FFmpeg 会自动初始化。6. 二次开发从能跑到好用几个改造方向6.1 支持 AAC 音频推流现在的源码只推视频但实际直播场景通常需要音视频一起推。改造的思路是找到音频流用同样的方式读包、设时间戳、写包。关键点是 AAC 的extradata需要封装成AudioSpecificConfigFFmpeg 的 FLV 封装器会自动处理你只需要确保codecpar-extradata有值。// 音频包处理和视频类似 if (pkt.stream_index audio_stream_index) { int64_t pts_ms av_rescale_q(pkt.pts, audio_stream-time_base, (AVRational){1, 1000}); pkt.pts pts_ms; pkt.dts pts_ms; // 音频通常没有 B 帧pts 等于 dts av_interleaved_write_frame(out_fmt_ctx, pkt); }音频的时间戳通常和视频独立但服务器会按时间戳交错播放。如果音频时间戳和视频差太多会出现音画不同步。常见做法是以音频为基准视频时间戳向音频对齐。6.2 增加断线重连推流过程中网络抖动是常态没有重连机制的话一断就得重启程序。改造方法是在av_interleaved_write_frame返回错误时关闭当前的AVFormatContext重新avformat_alloc_output_context2和avio_open然后从最后一个关键帧开始推。if (ret 0) { // 保存最后一个关键帧的位置 // 关闭并重新打开输出上下文 av_write_trailer(out_fmt_ctx); avio_closep(out_fmt_ctx-pb); avformat_free_context(out_fmt_ctx); // 重新初始化 avformat_alloc_output_context2(out_fmt_ctx, NULL, flv, push_url); avio_open(out_fmt_ctx-pb, push_url, AVIO_FLAG_WRITE); avformat_write_header(out_fmt_ctx, NULL); // 从关键帧重新推 }重连时要注意服务器可能要求从 AVC sequence header 重新开始所以最好把 SPS/PPS 也重新发一遍。6.3 性能优化减少内存拷贝当前的代码里av_read_frame读出来的AVPacket会经过av_interleaved_write_frame内部拷贝一次。如果码率很高这个拷贝会成为瓶颈。优化方法是直接用avio_write写 FLV Tag跳过 FFmpeg 的封装层。但这需要自己实现 FLV 封装工作量不小。一个折中的办法是调大AVIOContext的缓冲区减少系统调用次数。另外如果输入是文件可以用av_read_frame的AVPacket直接引用文件映射的内存避免拷贝。但这需要改 FFmpeg 的源码不太现实。我一般会先测一下推流时的 CPU 占用如果av_interleaved_write_frame占了大头再考虑优化。多数场景下网络带宽才是瓶颈不是内存拷贝。6.4 验证改造效果的方法改完之后怎么验证最直接的是用ffplay拉流看画面和声音。但ffplay的缓冲比较大看不出延迟。要测延迟可以用ffmpeg拉流并记录时间戳ffmpeg -i rtmp://127.0.0.1:1935/live/test -f null - 21 | grep time对比推流端和拉流端的time差值就是端到端延迟。如果延迟超过 2 秒检查 Chunk Size 和缓冲区设置。另一个验证点是看服务器端的日志。nginx-rtmp 会记录每个连接的推流时长和字节数。如果字节数在涨但客户端没画面说明封装有问题。从那以后我每次改完推流代码都会先用ffprobe检查 FLV 的 Tag 结构确认 sequence header 在第一个视频 Tag 之前再上ffplay验证。这个习惯帮我省了很多来回调试的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?