
简介这份源码资源面向具备一定C语言与音视频基础的开发者聚焦于使用FFmpeg API将内存中的H264裸流封装为FLV格式并通过RTMP协议完成直播推流这一典型场景。资源包内仅含1个C源文件压缩包约3KB体量轻巧便于直接阅读与移植适合作为实时音视频推流模块的入门参考。源码围绕FFmpeg的libavformat与libavcodec展开涉及初始化网络库、解析H264 NAL单元、构建FLV Tag、创建RTMP连接、写入头部与持续推帧、收尾释放资源等关键环节能帮助读者理解内存级封装与推流的完整链路。目前已有964人学习下载对于想深入网络编程与多媒体编码技术、提升实时音视频应用开发能力的开发者而言是一份可直接对照调试的实践素材。1. 从一份 H264 转 RTMP 源码包说起为什么值得拆前阵子有个做直播客户端的朋友找我说他们要把本地摄像头采集的 H264 裸流推到流媒体服务器中间不想依赖庞大的 FFmpeg 命令行进程想直接在自己的程序里把 H264 封装成 FLV 再走 RTMP 发出去。他丢给我一份源码包名字很直白就叫 H264tortmp。我花了一个周末把它拆开跑通发现这东西虽然体量不大但把「H264 裸流 → FLV 封装 → RTMP 推流」这条链路讲得很完整适合拿来当学习 RTMP 协议和 FLV 封装的脚手架。它解决的核心问题就一个手里有一堆 NALU怎么把它们变成服务器认的 RTMP 流。适合谁适合已经懂点 socket、想搞清楚 RTMP 握手和 FLV Tag 结构、又不想一上来啃 FFmpeg 源码的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆给你看。2. 项目定位与技术选型它到底封装了哪几层2.1 这份源码解决的是哪一段链路很多人第一次接触推流脑子里是「采集 → 编码 → 推流」一整条。这份源码不碰采集也不碰编码它假设你已经拿到了 H264 裸流也就是一串以 00 00 00 01 或 00 00 01 开头的 NALU。它负责的是后半段把 NALU 按 FLV 的格式打成 Tag再通过 RTMP 协议发到服务器。所以它的输入是 H264 文件或内存里的 NALU 缓冲输出是发往 RTMP 服务端的字节流。理解这个边界很重要因为后面你改造的时候采集和编码那部分要自己接上去源码里没有。从工程角度看这条链路可以拆成三层。第一层是 H264 解析层负责从裸流里切出一个个 NALU识别 SPS、PPS、IDR 和普通帧。第二层是 FLV 封装层负责把 NALU 组装成 FLV 的 Video Tag包括 AVC 序列头AVCDecoderConfigurationRecord和普通的 NALU 包。第三层是 RTMP 协议层负责握手、连接、创建流、发送音视频数据。这三层在源码里通常是分开的文件改的时候可以一层一层单独调。2.2 为什么是 FLV 而不是直接发 H264这里有个新手常问的问题RTMP 不是能直接发 H264 吗为什么还要套一层 FLV原因是 RTMP 的消息体本身不定义视频编码格式它只负责传输真正描述「这段数据是 H264、分辨率多少、SPS/PPS 是什么」的是 FLV 的 Video Tag 规范。服务器收到 RTMP 消息后是按 FLV Tag 去解析的。所以 H264 要进 RTMP必须先变成 FLV Tag。这也是为什么源码里一定有一个 AVCDecoderConfigurationRecord 的构造过程它就是把 SPS 和 PPS 打包成 FLV 能识别的格式。FLV 的 Video Tag 结构大致是Tag Header11 字节 Tag Data。Tag Data 里第一个字节的高 4 位是帧类型1 表示关键帧2 表示普通帧低 4 位是编码 ID7 表示 AVC。接着一个字节是 AVCPacketType0 表示序列头1 表示 NALU2 表示序列结束。后面跟着 3 字节的 CompositionTime再往后才是真正的数据。序列头的数据就是 AVCDecoderConfigurationRecord普通帧的数据则是长度前缀 NALU 的形式。源码里这些字段的拼装就是核心工作量。2.3 技术栈与依赖判断从源码包的命名和常见实现看这份代码大概率是 C 或 C 写的依赖标准 socket 库可能用到一点平台相关的网络 API。它不太可能依赖 FFmpeg 的库因为如果依赖了 FFmpeg就没必要单独写 FLV 封装和 RTMP 握手了。常见做法是用 C 写一个 rtmp 客户端类内部维护 socket 连接和 RTMP 握手状态机再用一个 flv 封装类把 NALU 转成 Tag最后有一个 h264 解析类负责切 NALU。如果你拿到的是 C 版本可能会看到类名类似 RtmpClient、FlvMuxer、H264Parser 这样的划分。提示不同来源的源码包文件组织差异很大有的把所有逻辑塞在一个 cpp 里有的拆成多个文件。先看文件数量和 include 关系能快速判断它的分层。3. 跑起来从编译到推流成功的完整步骤3.1 环境准备与编译这份源码是纯网络和字节操作不依赖图形库所以编译门槛不高。Linux 下用 g 直接编Windows 下用 Visual Studio 建个控制台工程也行。常见做法是源码里会有一个 main 函数做测试入口读取一个本地 H264 文件然后推到一个 RTMP 地址。编译命令大概是这样# Linux 下编译假设源码文件是 main.cpp、rtmp.cpp、flv.cpp、h264.cpp g -o h264tortmp main.cpp rtmp.cpp flv.cpp h264.cpp -lstdc -lpthread如果报找不到 socket 相关函数检查是否包含了 sys/socket.h、netinet/in.h、arpa/inet.h 这几个头文件。Windows 下需要链接 ws2_32.lib并且在使用 socket 前调用 WSAStartup。这些平台差异是新手最容易卡住的地方源码里如果有条件编译宏通常已经处理了没有的话要自己补。编译通过后你会得到一个可执行文件。先别急着推流用./h264tortmp不带参数跑一下看它提示什么。很多测试入口会打印用法比如需要传入 H264 文件路径和 RTMP URL。如果它直接开始读文件并尝试连接说明参数是写死在代码里的需要你去改。3.2 准备一个可用的 RTMP 服务端推流得有接收端。常见做法是本地起一个流媒体服务器比如用开源的 SRS 或 nginx-rtmp。这里不展开部署细节只说验证思路服务器起来后会监听一个 RTMP 端口通常是 1935。你推流的 URL 格式一般是rtmp://127.0.0.1/live/stream其中 live 是应用名stream 是流名。服务器端要配置好对应的 application允许推流。如果服务器没配好客户端会卡在握手或 connect 阶段表现是连不上或者连上后马上断开。注意本地测试时防火墙可能拦 1935 端口Linux 下用ss -tlnp | grep 1935确认服务在监听Windows 下检查入站规则。3.3 源码里的推流主流程跑通的关键是看懂 main 里的调用顺序。典型流程是初始化 RTMP 客户端 → 连接服务器 → 发送握手 → 发送 connect 命令 → 创建流 → 读取 H264 文件 → 切 NALU → 构造 FLV Tag → 发送。下面这段伪代码是我从源码里提炼出来的主流程实际函数名可能不同但逻辑一致// 推流主流程示意 int main() { RtmpClient client; // 1. 连接 RTMP 服务器内部完成握手 if (!client.connect(127.0.0.1, 1935)) { printf(connect failed\n); return -1; } // 2. 发送 connect 命令交换能力信息 client.sendConnect(live); // 3. 创建流拿到 stream id client.createStream(); // 4. 打开 H264 文件循环读取 FILE* fp fopen(test.h264, rb); H264Parser parser; FlvMuxer muxer; while (!feof(fp)) { // 切出一个 NALU Nalu nalu parser.nextNalu(fp); if (nalu.empty()) continue; // 转成 FLV Tag序列头只发一次 FlvTag tag muxer.pack(nalu); // 通过 RTMP 发送 client.sendVideo(tag); } fclose(fp); client.close(); return 0; }这段代码里几个关键点connect 内部不只是 TCP 连接还包括 RTMP 的 C0/C1/C2 握手sendConnect 要构造 AMF0 编码的命令消息createStream 之后才能发视频数据。参数方面服务器地址和端口、应用名、流名通常需要按你的环境改。如果源码里这些是硬编码的找到对应字符串替换即可。3.4 验证推流是否成功推流端跑起来后怎么知道成功了两个办法。一是看服务器日志SRS 或 nginx-rtmp 会打印收到推流的客户端和流名。二是用播放器拉流比如用 ffplay 或 VLC 打开rtmp://127.0.0.1/live/stream能出画面就说明整条链路通了。如果播放器一直转圈先看服务器有没有收到数据再看收到的数据是不是合法的 FLV。常见问题是时间戳不对导致播放器卡住或者序列头没发导致解码器初始化失败。4. 核心模块拆解NALU 切分、FLV 封装与 RTMP 发送4.1 H264 NALU 切分与类型识别H264 裸流是一串连续的 NALU每个 NALU 前面有起始码。起始码有两种4 字节的 00 00 00 01 和 3 字节的 00 00 01。切分逻辑就是扫描起始码把两个起始码之间的数据作为一个 NALU。NALU 的第一个字节包含类型信息低 5 位是 nal_unit_type。常见类型5 是 IDR 帧关键帧1 是非 IDR 帧7 是 SPS8 是 PPS。源码里通常有一个函数做这件事// 从缓冲区中查找下一个 NALU 的起始位置 int findStartCode(const uint8_t* data, int size, int offset) { for (int i offset; i size - 3; i) { // 匹配 00 00 01 或 00 00 00 01 if (data[i] 0 data[i1] 0 data[i2] 1) { return i; } if (i size - 4 data[i] 0 data[i1] 0 data[i2] 0 data[i3] 1) { return i; } } return -1; }这个函数的参数是数据缓冲、总长度和起始偏移返回起始码的位置。实际使用时先找到第一个起始码再找下一个两者之间的数据就是 NALU 负载。注意起始码本身不属于 NALU 数据封装时要跳过。识别类型时取 NALU 第一个字节和 0x1F 做与运算。SPS 和 PPS 要单独保存因为构造 AVCDecoderConfigurationRecord 时要用到它们。4.2 FLV 封装AVCDecoderConfigurationRecord 的构造FLV 的 AVC 序列头是推流能否被解码的关键。它的结构是1 字节 configurationVersion固定 11 字节 AVCProfileIndication来自 SPS1 字节 profile_compatibility来自 SPS1 字节 AVCLevelIndication来自 SPS1 字节 lengthSizeMinusOne通常 0xFF 表示 4 字节长度前缀1 字节 numOfSPS通常 0xE1 表示 1 个 SPS2 字节 SPS 长度SPS 数据1 字节 numOfPPS2 字节 PPS 长度PPS 数据。源码里构造这段的代码大概长这样// 构造 AVCDecoderConfigurationRecord std::vectoruint8_t buildAvcConfig(const std::vectoruint8_t sps, const std::vectoruint8_t pps) { std::vectoruint8_t config; config.push_back(0x01); // configurationVersion config.push_back(sps[1]); // AVCProfileIndication config.push_back(sps[2]); // profile_compatibility config.push_back(sps[3]); // AVCLevelIndication config.push_back(0xFF); // lengthSizeMinusOne 3 config.push_back(0xE1); // numOfSPS 1 // SPS 长度大端 config.push_back((sps.size() 8) 0xFF); config.push_back(sps.size() 0xFF); config.insert(config.end(), sps.begin(), sps.end()); config.push_back(0x01); // numOfPPS 1 config.push_back((pps.size() 8) 0xFF); config.push_back(pps.size() 0xFF); config.insert(config.end(), pps.begin(), pps.end()); return config; }参数说明sps 和 pps 是去掉起始码后的原始数据。sps[1]、sps[2]、sps[3] 分别对应 profile、compatibility、level这是 H264 规范定义的。lengthSizeMinusOne 设为 3 表示后面 NALU 长度用 4 字节表示这是 FLV 的常见约定。这段配置只在推流开始时发一次之后发的都是普通 NALU 包。4.3 普通视频帧的 FLV Tag 封装普通帧的封装比序列头简单但要注意长度前缀。FLV 的 AVC NALU 包格式是1 字节帧类型和编码 ID关键帧是 0x17普通帧是 0x271 字节 AVCPacketType1 表示 NALU3 字节 CompositionTime通常 0然后是 NALU 数据。NALU 数据前面要加 4 字节的长度前缀这个长度是 NALU 本身的长度不含起始码。代码如下// 封装一个普通视频帧为 FLV Tag Data std::vectoruint8_t packVideoNalu(const std::vectoruint8_t nalu, bool isKeyFrame) { std::vectoruint8_t data; // 帧类型关键帧 0x17普通帧 0x27 data.push_back(isKeyFrame ? 0x17 : 0x27); data.push_back(0x01); // AVCPacketType NALU data.push_back(0x00); // CompositionTime 高字节 data.push_back(0x00); // 中字节 data.push_back(0x00); // 低字节 // 4 字节长度前缀大端 uint32_t len nalu.size(); data.push_back((len 24) 0xFF); data.push_back((len 16) 0xFF); data.push_back((len 8) 0xFF); data.push_back(len 0xFF); data.insert(data.end(), nalu.begin(), nalu.end()); return data; }这里 isKeyFrame 的判断依据是 NALU 类型是否为 5。CompositionTime 在无 B 帧的情况下填 0 即可如果有 B 帧需要根据 PTS 和 DTS 的差值来填。长度前缀用大端这是网络字节序的常见做法。封装好的 Tag Data 还要加上 FLV Tag Header 和 RTMP 消息头才能发出去。4.4 RTMP 握手与消息发送RTMP 握手是连接建立后的第一步。客户端发 C01 字节版本号通常 3和 C11536 字节随机数服务器回 S0、S1、S2客户端再发 C2。握手完成后才能发命令消息。命令消息用 AMF0 编码connect 命令包含 app 名、flashVer、tcUrl 等字段。发送视频数据时RTMP 消息头里的 type 是 9视频stream id 是 createStream 返回的 id。源码里发送函数通常长这样// 发送一个 RTMP 视频消息 bool sendVideoMessage(int streamId, const std::vectoruint8_t tagData, uint32_t timestamp) { // 构造 FLV Tag Header std::vectoruint8_t tag; tag.push_back(0x09); // TagType Video // DataSize 3 字节大端 uint32_t size tagData.size(); tag.push_back((size 16) 0xFF); tag.push_back((size 8) 0xFF); tag.push_back(size 0xFF); // Timestamp 3 字节大端 tag.push_back((timestamp 16) 0xFF); tag.push_back((timestamp 8) 0xFF); tag.push_back(timestamp 0xFF); tag.push_back(0x00); // TimestampExtended tag.push_back(0x00); // StreamID 低字节 tag.push_back(0x00); tag.push_back(0x00); tag.insert(tag.end(), tagData.begin(), tagData.end()); // 通过 socket 发送前面还要加 RTMP 消息头 return sendRaw(tag); }参数里 streamId 来自 createStream 的响应timestamp 是毫秒级时间戳必须递增否则播放器会认为流有问题。sendRaw 内部处理 TCP 分包和 RTMP chunk 分块。RTMP 的 chunk 大小默认 128 字节发送前通常要发一个 Set Chunk Size 消息把 chunk 调大减少分块开销。5. 避坑与排查推流失败时先看这几个地方5.1 握手失败或连接被拒现象是程序卡在 connect 或者直接返回失败。原因通常是服务器地址端口不对、服务器没启动、或者防火墙拦截。解决方法是先用 telnet 或 nc 测试端口通不通nc -zv 127.0.0.1 1935不通就先解决网络和服务问题。如果端口通但握手失败检查 C0 版本号是不是 3C1 的 1536 字节有没有发全。5.2 服务器收到流但播放器黑屏现象是服务器日志显示有推流但播放器拉流没画面。原因多半是 AVCDecoderConfigurationRecord 构造错误或者 SPS/PPS 没发。解决方法是抓包看第一个视频 Tag 的 AVCPacketType 是不是 0数据长度和内容是否符合预期。常见错误是 SPS 长度算错或者把起始码也塞进了 SPS 数据里。5.3 时间戳不递增导致播放卡顿现象是播放器能出画面但很快卡住或花屏。原因是发送的 timestamp 没有递增或者所有帧都用了同一个时间戳。解决方法是维护一个全局时间戳变量每发一帧按帧率增加比如 25 帧每秒就每次加 40 毫秒。注意 RTMP 的时间戳是 32 位回绕时要处理。5.4 NALU 切分把一帧切成多段现象是画面撕裂或解码报错。原因是起始码扫描逻辑有 bug把 00 00 01 误判成了数据的一部分。解决方法是确保扫描时从上一个 NALU 结束位置开始并且优先匹配 4 字节起始码。另外有些编码器会在 NALU 之间插入填充字节切分时要跳过。5.5 发送大帧时 TCP 缓冲区溢出现象是推流一段时间后程序阻塞或崩溃。原因是 IDR 帧通常很大一次性写入 socket 可能超过发送缓冲区。解决方法是把发送逻辑改成循环写直到所有字节写完或者用非阻塞 socket 加发送队列。这个坑在本地测试时不容易发现一上真实网络就暴露。6. 二次开发把这份源码接进自己的采集编码链路6.1 替换输入源从文件到内存 NALU源码默认从文件读 H264实际项目里你的 NALU 来自编码器回调。改造点是把文件读取循环换成编码器回调。常见做法是定义一个 NaluSource 接口文件读取和编码器回调各实现一份。编码器回调里通常一次给你一个完整帧的 NALU可能包含多个 NALU比如 SPS、PPS、IDR 在一起你需要按起始码再切一次。改造后的调用大概是这样// 编码器回调中直接推流 void onEncodedFrame(const uint8_t* data, int size, bool isKeyFrame) { // 按起始码切出 NALU std::vectorNalu nalus splitNalus(data, size); for (auto nalu : nalus) { if (nalu.type 7) { sps nalu.data; continue; } if (nalu.type 8) { pps nalu.data; continue; } // 第一个关键帧前先发序列头 if (!configSent !sps.empty() !pps.empty()) { sendAvcConfig(sps, pps); configSent true; } auto tag packVideoNalu(nalu.data, nalu.type 5); sendVideoMessage(streamId, tag, getNextTimestamp()); } }这里的关键是序列头只发一次且必须在第一个关键帧之前发。时间戳用一个单调递增的函数生成不要用系统时间直接取避免回绕和跳变。6.2 增加音频AAC 的 FLV 封装如果要推带声音的流需要增加 AAC 的封装。AAC 在 FLV 里的 TagType 是 8序列头是 AudioSpecificConfig普通帧直接跟 AAC 裸数据。改造思路和视频类似保存 AudioSpecificConfig在第一个音频帧前发序列头之后每帧加 1 字节的 AACPacketType。注意音视频时间戳要统一到同一个时钟基准否则会出现音画不同步。6.3 断线重连与状态管理生产环境必须处理断线。改造点是在 RtmpClient 里加一个状态机记录当前是握手、连接、推流还是断开。发送失败时不要直接退出而是进入重连流程关闭旧 socket重新握手重新 connect 和 createStream然后从下一个关键帧开始发。重连期间要丢弃非关键帧否则服务器解码器无法恢复。这个逻辑我一般会单独写一个 ReconnectManager避免和协议代码混在一起。6.4 验证改造是否成功的方法改完之后怎么验证我习惯分三步。第一步用本地文件推流确认服务器能收到并且播放器能播。第二步换成编码器实时数据观察时间戳是否连续、关键帧间隔是否正常。第三步模拟网络断开看重连逻辑是否在下一个关键帧恢复。每一步都用抓包工具看 RTMP 消息重点看视频 Tag 的 AVCPacketType 和时间戳字段。从那以后我每次改推流代码都强制走一遍「文件 → 实时 → 断线」这三步少一步都可能在上线后翻车。希望这份拆解能帮你把这份源码跑起来也帮到你把它接进自己的项目里。本文还有配套的精品资源点击获取