尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
H264 over RTP 打包实战:从 NALU 到 FU-A 分片与发送端实现
简介这份资源围绕RTP协议封装与发送H264视频流这一主题面向具备一定网络编程与音视频基础、希望打通编码封装到解码播放完整链路的开发者。包内共21个文件以264、h264视频样本、cpp与h源码、obj与pdb等编译中间产物、exe可执行程序及sdp会话描述文件为主压缩包约929KB体积轻量却覆盖了从NAL单元处理到VLC接收播放的关键环节。资源中附带的会话描述文件可直接供VLC读取以获取媒体类型、IP与端口等配置配套的解码处理模块则展示了NAL单元分割与RTP头封装的实现逻辑便于读者对照理解时间戳、序列号等字段在接收端重组播放中的作用。目前已有333人学习下载适合作为实时视频传输入门与联调排错的参考样例。1. 从一次花屏排查说起RTP 发 H264 到底难在哪去年帮一个做远程巡检的团队看问题现场画面每隔十几秒就糊一片日志里 RTP 包序号却是连续的。抓包一看发送端把一整帧 H264 塞进一个 RTP 包超过 MTU 直接被分片接收端重组失败解码器拿到半帧数据只能花屏。这个场景很典型H264 编码器吐出来的是 NALU 流RTP 是面向报文、有 MTU 上限的传输协议两者之间隔着一层必须自己写的打包逻辑。这份资源围绕的就是这层逻辑——怎么把 H264 的 NALU 按 RTP 规范切分、加头、打时间戳再通过 UDP 发出去。适合正在做音视频推流、监控回传、屏幕共享这类功能又不想一上来就搬整个 WebRTC 栈的开发者。下面按「协议怎么对应 → 代码怎么组织 → 怎么跑起来 → 怎么改」的顺序拆。2. 协议映射与工程结构NALU 到 RTP 包的完整链路2.1 先理清 H264 的 NALU 和 RTP 的对应关系H264 码流的基本单位是 NALU每个 NALU 前面通常带 4 字节起始码00 00 00 01或 3 字节00 00 01。NALU 头第一个字节里藏着nal_ref_idc和nal_unit_type后者决定这是 SPS、PPS、IDR 还是普通 P 帧切片。RTP 这边每个包有 12 字节固定头关键字段是序列号sequence number、时间戳timestamp、标记位marker和负载类型payload type。两者对接的核心规则有三条。第一小 NALU 直接塞进单个 RTP 包去掉起始码NALU 头原样保留。第二大 NALU 超过 MTU 时要拆成 FU-A 分片每个分片有自己的 FU indicator 和 FU header首片置 S 位、末片置 E 位。第三同一个视频帧的所有 RTP 包共享同一个时间戳帧最后一个包的 marker 置 1接收端靠这个判断帧边界。提示MTU 一般按 1500 字节算减去 IP 头 20 字节、UDP 头 8 字节、RTP 头 12 字节单个 RTP 负载实际可用约 1460 字节工程上常取 1400 留余量。2.2 工程目录怎么划分这类发送端项目结构不复杂但职责边界要清楚否则后面加音频、加 RTCP 会乱。常见做法是按「采集/读取 → 编码 → 打包 → 发送」四层拆h264_rtp_sender/ ├── src/ │ ├── main.c # 入口参数解析与主循环 │ ├── h264_reader.c # 从文件或编码器读 NALU │ ├── rtp_packet.c # RTP 头构造、FU-A 分片 │ ├── rtp_sender.c # UDP socket 封装与发送 │ └── timestamp.c # 90kHz 时钟与帧时间戳 ├── include/ │ └── rtp_packet.h # 对外结构体与函数声明 ├── test/ │ └── recv_dump.c # 简易接收端落盘验证 └── Makefileh264_reader只负责吐出一个完整的 NALU含类型和长度不关心怎么发rtp_packet是纯函数式的打包层输入 NALU 和帧序号输出一个或多个待发缓冲rtp_sender只管sendto。这样分层的好处是后面把文件源换成摄像头编码器只动h264_reader打包和发送逻辑一行不用改。2.3 RTP 头构造的关键字段RTP 固定头 12 字节逐字段填错一个就会导致接收端解析失败。下面这段是构造单包 RTP 头的典型写法/* 构造 12 字节 RTP 固定头返回写入长度 */ int build_rtp_header(uint8_t *buf, uint16_t seq, uint32_t ts, uint32_t ssrc, int marker, int payload_type) { buf[0] 0x80; /* V2, P0, X0, CC0 */ buf[1] (marker ? 0x80 : 0x00) | (payload_type 0x7F); buf[2] (seq 8) 0xFF; /* 序列号高字节网络序 */ buf[3] seq 0xFF; buf[4] (ts 24) 0xFF; /* 时间戳90kHz 时钟 */ buf[5] (ts 16) 0xFF; buf[6] (ts 8) 0xFF; buf[7] ts 0xFF; buf[8] (ssrc 24) 0xFF; /* 同步源标识会话内唯一 */ buf[9] (ssrc 16) 0xFF; buf[10] (ssrc 8) 0xFF; buf[11] ssrc 0xFF; return 12; }buf[0]固定0x80表示版本 2、无填充、无扩展、无 CSRCbuf[1]高位置 marker低 7 位是 payload typeH264 动态负载类型常用 96127 区间双方约定一致即可。序列号和时间戳都必须按大端写入这是新手最容易翻车的地方——本机小端直接 memcpy 过去接收端读出来就是乱序。ssrc用来区分同一地址上的多路流单路场景随便给个固定值也行但多路复用时必须唯一。2.4 FU-A 分片大 NALU 的拆包规则一个 NALU 超过约 1400 字节就得走 FU-A。FU-A 的负载结构是 1 字节 FU indicator 加 1 字节 FU header再接分片数据。indicator 的 type 字段固定为 28header 里 S、E、R 三位控制首末片低 5 位保留原始 NALU 类型/* 将一个大 NALU 拆成 FU-A 分片逐个发送 */ void send_fu_a(int sock, const uint8_t *nalu, int len, uint16_t *seq, uint32_t ts, uint32_t ssrc) { int max_payload 1400; int offset 1; /* 跳过原始 NALU 头 */ uint8_t nal_type nalu[0] 0x1F; uint8_t nri nalu[0] 0x60; while (offset len) { int chunk len - offset; if (chunk max_payload) chunk max_payload; int is_first (offset 1); int is_last (offset chunk len); uint8_t pkt[1500]; int hdr build_rtp_header(pkt, (*seq), ts, ssrc, is_last, 96); pkt[hdr] nri | 28; /* FU indicator: type28 */ pkt[hdr 1] (is_first ? 0x80 : 0) | (is_last ? 0x40 : 0) | nal_type; memcpy(pkt hdr 2, nalu offset, chunk); sendto(sock, pkt, hdr 2 chunk, 0, ...); offset chunk; } }offset从 1 开始因为原始 NALU 头已经被拆进 FU indicator 和 FU header不能再重复发送。nri保留原始 NALU 的参考级别接收端重组时要用。首片 S 位置 1、末片 E 位置 1中间片两位都是 0。marker 只在末片置 1表示这一帧结束。序列号每发一个分片自增一次时间戳整帧不变——这两条是接收端能正确重组的前提。3. 跑通发送端从编译到抓包验证3.1 编译与依赖这个发送端只依赖系统 socket 和标准 C 库没有第三方库Makefile里gcc -O2 -Wall编出可执行文件即可。如果h264_reader要接编码器才需要额外链接对应编码库文件源场景不需要。编译命令make clean make # 产物./h264_rtp_sender编译时留意-Wall的告警尤其是sendto返回值没处理、uint16_t序列号溢出这类编译器会提示隐式截断别忽略。3.2 准备测试码流手头没有现成 H264 文件的话用 ffmpeg 生成一段带 SPS/PPS 的裸流最省事# 生成 10 秒 640x480 的 H264 裸流关闭 B 帧便于时间戳对齐 ffmpeg -f lavfi -i testsrcsize640x480:rate25 -t 10 \ -c:v libx264 -profile:v baseline -x264-params keyint25 \ -bsf:v h264_mp4toannexb -f h264 test.h264-profile:v baseline去掉 B 帧时间戳单调递增排查时少一层干扰keyint25让每秒一个关键帧方便观察 SPS/PPS 和 IDR 的发送时机。裸流里每个 NALU 前带起始码h264_reader就是按起始码切分的。3.3 启动发送并抓包确认发送端启动参数一般包括目标地址、端口、码流文件和帧率./h264_rtp_sender -f test.h264 -h 192.168.1.50 -p 5004 -r 25-r 25告诉发送端按 25fps 的节奏推进时间戳时间戳增量是90000 / 25 3600。发出去之后最直接的验证是在接收侧抓包# 在接收机上抓 UDP 5004导出 pcap 后用分析工具看 tcpdump -i eth0 -w rtp.pcap udp port 5004打开 pcap 重点看三件事序列号是否连续无跳变、同一帧内时间戳是否一致、大帧是否出现 FU-A 的 S/E 配对。如果序列号在某个点突然回绕检查uint16_t自增有没有做溢出处理如果时间戳每包都在变说明时间戳被错误地按包递增而不是按帧递增。3.4 用简易接收端落盘验证抓包只能看头验证负载是否正确还得把 RTP 还原成 H264 文件再播放。项目里test/recv_dump.c就是干这个的收包、按序列号排序、去掉 RTP 头、FU-A 重组、补回起始码、写文件。跑通后./recv_dump -p 5004 -o out.h264 ffplay out.h264能正常播放且不花屏说明打包链路是通的。这一步是整个项目最值得先做的验证比盯着日志猜有效得多。4. 避坑与排查五个真实踩过的坑4.1 花屏但序列号连续现象接收端画面周期性花屏抓包看序列号连续、无丢包。原因单个 RTP 包负载超过 MTU被 IP 层分片接收端按 RTP 边界解析时拿到的是不完整 NALU。解决打包前判断 NALU 长度超过 1400 字节一律走 FU-A不要图省事直接塞。4.2 首帧黑屏或解码器报错现象播放器起播黑屏几秒或直接报 SPS/PPS 缺失。原因发送端从文件中间开始读或者关键帧前的 SPS/PPS 没跟着 IDR 一起发。解决每个 IDR 帧前强制重发 SPS 和 PPS接收端才能随时加入解码。这也是为什么测试码流要设keyint方便观察这个时机。4.3 时间戳跳变导致卡顿现象画面能播但每隔一阵卡一下。原因时间戳用了系统毫秒时钟没有换算成 90kHz或者按包递增而非按帧递增。解决统一用 90kHz 时钟每帧时间戳 帧序号 × (90000 / fps)整帧所有分片共享同一个值。4.4 序列号回绕后接收端乱序现象连续发几十分钟后接收端开始乱序重组。原因uint16_t序列号到 65535 后回绕到 0接收端排序逻辑没处理回绕。解决接收端比较序列号时用带符号差值判断新旧发送端自增本身没问题问题在接收侧的比较方式。4.5 多路流互相串包现象同一台机器发两路流到同一端口接收端画面错乱。原因两路流用了相同ssrc或相同端口接收端无法区分。解决每路流分配唯一ssrc端口也分开如果必须同端口接收端按ssrc分流。5. 二次开发把文件源换成实时编码器并加 RTCP 反馈5.1 替换数据源的改造点文件源换成实时编码器改动集中在h264_reader这一层。编码器回调里拿到的是带起始码的 NALU 缓冲接口保持「吐出一个完整 NALU 类型 是否关键帧」不变上层打包和发送完全不用动。我一般会在这个接口上加一个帧序号参数让时间戳模块直接按帧序号算避免依赖系统时钟导致抖动。/* 编码器回调适配把编码输出转成统一的 NALU 结构 */ typedef struct { const uint8_t *data; /* 含起始码的 NALU */ int len; int is_key; /* 是否 IDR决定要不要补发 SPS/PPS */ uint32_t frame_idx; /* 帧序号用于时间戳计算 */ } nalu_frame_t; int on_encoded(nalu_frame_t *f, void *ctx) { if (f-is_key) { send_sps_pps(ctx); /* 关键帧前补参数集 */ } return rtp_send_nalu(ctx, f-data, f-len, f-frame_idx * (90000 / g_fps)); }frame_idx由编码器侧维护g_fps是采集帧率两者相乘得到 90kHz 时间戳。这样即使编码器偶尔丢帧时间戳仍然按真实帧序推进接收端不会因为时间戳跳变而卡顿。5.2 加 RTCP 接收报告做码率自适应单向发送没有反馈网络一差就只能硬扛。加 RTCP 的 RR 报文后发送端能拿到丢包率和往返时延据此调编码码率。改造思路是开一个 RTCP 接收线程解析 RR 里的fraction lost字段超过阈值就通知编码器降码率。这部分不需要改 RTP 打包逻辑属于旁路增强。改造项改动文件验证方法实时编码源h264_reader.c接摄像头ffplay 实时播放关键帧补参数集rtp_packet.c中途加入接收端能正常起播RTCP 反馈新增 rtcp.c人为限速观察码率是否下降5.3 一个容易忽略的验证习惯改完打包逻辑后别只看「能播」要拿 pcap 逐帧核对 FU-A 的 S/E 配对和 marker 位置。我有次改分片大小把末片判断写成offset chunk len边界帧少发一个字节播放器偶发报错查了大半天。从那以后每次动分片逻辑我都强制走一遍「抓包 → 核对首末片 → 落盘播放」三步不省这一步。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Unity桌面精灵开发实战:透明窗口与鼠标穿透实现

Unity桌面精灵开发实战:透明窗口与鼠标穿透实现

简介:这份资源面向Unity初学者与独立开发者,聚焦在Windows平台用Unity实现类似QQ宠物的桌面透明精灵,重点解决无边界窗口、透明渲染与跳动动画等常见难点。压缩包共1267个文件,约41.45MB,以bin、info等工程与缓存文件为…

📅 2026/10/28 6:44:31
Agent-Reach 触达层设计:Python 构建 AI Agent 的 CLI 执行与安全实践

Agent-Reach 触达层设计:Python 构建 AI Agent 的 CLI 执行与安全实践

1. 从"Agent-Reach"这个名字说起:它到底想解决什么问题第一次看到 Agent-Reach 这个项目名,我的直觉是:这大概率是一个围绕 AI Agent 能力边界做文章的工具。Reach 这个词在工程语境里通常有两层含义,一层是"触达&…

📅 2026/10/28 6:39:30
从零实现Web消除游戏:核心逻辑与避坑指南

从零实现Web消除游戏:核心逻辑与避坑指南

消除类游戏是很多人入门前端开发时最想动手做、又最容易半途而废的一类项目。看起来规则简单——点两下、凑三个、消掉——但真正写起来,从棋盘数据结构、匹配检测、下落填充到连锁反应,每一块都有坑。我自己第一次写三消的时候,光"消除…

📅 2026/10/28 6:39:30
MORE NEWS

更多资讯

📰

refin-bin-0.14 实战指南:从解压到流水线集成

简介:这份资源是面向 Linux 与 macOS 用户的 UEFI 启动管理工具 rEFInd 的二进制发行包,适合需要在多系统环境中配置引导菜单、排查启动项或制作启动介质的进阶用户。压缩包共 61 个文件,约 3.22MB,以 23 个 efi 固件模块和 24 个…

📰

工业自动化中的MCPA2A技术:多设备协同通信架构解析

1. 项目背景解析:MCP&A2A的技术内涵MCP&A2A这个看似晦涩的缩写,实际上是现代工业自动化领域的一个关键技术组合。MCP代表"Multi-Channel Protocol"(多通道协议),而A2A则是"Agent to Agent"…

📰

GitHub作为协作操作系统:JavaScript工程的CI/CD与代码治理实战

简介:本资源是一份面向JavaScript初学者与Web开发入门者的GitHub平台实践入门包,聚焦代码托管、协作开发与开源项目参与的核心能力培养。压缩包共12个文件,含4个HTML页面(用于本地预览GitHub Pages效果)、2个JS脚本&am…

📰

Linux中断机制与核心数据结构深度解析

1. Linux中断机制全景解读中断处理是操作系统内核最核心的机制之一,就像人体神经系统的应激反应——当外部刺激出现时,CPU会立即暂停当前任务,转而去处理更紧急的事件。Linux内核通过精心设计的数据结构体系来管理这一复杂过程,主…

📰

模型团队自建算子仓库:从拒绝适配到掌握性能定义权

1. 从一条传闻说起:为什么“拒提案”和“建仓库”值得放在一起看去年圈子里流传过一个说法,某家做基座模型的团队,把一家硬件厂商递过来的联合优化提案给回绝了。理由不复杂:对方希望模型侧针对自家芯片做深度适配,而模…

📰

神经网络核心原理与工程实践指南

1. 人工神经网络的前世今生第一次接触神经网络是在2012年的ImageNet竞赛上,当时AlexNet以压倒性优势夺冠的场景至今记忆犹新。那是我第一次意识到,这个起源于1943年McCulloch-Pitts神经元模型的古老概念,经过几十年的蛰伏后终于迎来了爆发。神…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬