RTP流合成MP4:H264与AAC封装实战

发布时间:2026/10/16 9:42:45

RTP流合成MP4:H264与AAC封装实战 简介这份资源面向音视频开发初学者与流媒体协议实践者聚焦于将RTP承载的H264视频与AAC音频数据重新封装为可播放的MP4文件这一典型工程问题。包内共31个文件约197KB以17个C/C头文件与2个源文件构成核心代码框架辅以Makefile、project.xml等构建配置以及sdp会话描述、cmmb2rtp脚本和一份test.mp4样例覆盖从RTP收包、音视频流解析到MP4封装的完整链路。代码中涉及rtp、isma、mp4v2、track等模块划分便于读者理解流媒体解复用与容器写入的协作方式。目前已有486人学习下载适合希望动手实践RTP解包与MP4合成、并借助样例文件快速验证流程的开发者参考。1. 从裸流到可播放文件RTP 里的 H264 和 AAC 到底怎么落成 MP4抓过包的人都有体会Wireshark 里 RTP 流跑得好好的H264 的 NAL 单元、AAC 的 AU 都在可一旦想把它存成能拖进度条、能丢进播放器的 MP4就发现事情没那么简单。裸流是「时间线上的碎片」MP4 是「带索引的容器」中间隔着时间戳换算、帧边界重组、SPS/PPS 注入、ADTS 头剥离这一整套活。这份资源要解决的正是把 RTP 承载的 H264 视频和 AAC 音频按时间戳对齐后合成一个标准 MP4 文件。它适合做流媒体录制、监控回放、音视频存档的工程师也适合想搞懂「为什么我合成的 MP4 只有第一帧能播」的排错党。下面按「原理立住 → 动手复现 → 坑在哪」的顺序拆开讲。2. 拆开 RTP 载荷H264 的 NAL 与 AAC 的 AU 怎么取出来2.1 先分清 RTP 包里的三种东西RTP 本身只是个传输壳真正要处理的是它载荷里的编码数据。H264 在 RTP 里通常有两种打包方式单一 NAL 单元模式一个 RTP 包就是一个完整 NAL和分片模式FU-A一个 NAL 被拆到多个 RTP 包。AAC 则一般走 mpeg4-generic 载荷一个 RTP 包可能装一个或多个 AAC 的 AUAccess Unit每个 AU 前面可能带 ADTS 头也可能不带——这取决于 SDP 里的config参数。判断依据在 SDP 里。H264 看sprop-parameter-sets它给出 SPS 和 PPS 的 Base64AAC 看config它给出 AudioSpecificConfig。这两个东西是后面写 MP4 的必需材料抓包时如果 SDP 丢了基本就得靠从码流里重新找 SPS/PPS 来救。常见做法是先把 RTP 包按 SSRC 分组再按 sequence number 排序最后按 timestamp 归并成帧。这里有个容易翻车的点RTP 的 timestamp 是采样率下的计数值不是毫秒。H264 的时钟频率通常是 90000AAC 常见是 44100 或 48000。换算成秒要除以对应频率不能混用。2.2 用 Python 把 RTP 载荷还原成 Annex-B 码流下面这段代码演示从 RTP 包列表里提取 H264 的 NAL 单元并拼成 Annex-B 格式带 00 00 00 01 起始码。实际项目里 RTP 包可能来自 pcap 解析或 socket 接收这里用列表模拟。import struct def parse_rtp_header(pkt): # RTP 头固定 12 字节含 CSRC 时更长这里按无扩展处理 b0, b1 pkt[0], pkt[1] version b0 6 padding (b0 5) 1 extension (b0 4) 1 csrc_count b0 0x0F marker (b1 7) 1 payload_type b1 0x7F seq struct.unpack(H, pkt[2:4])[0] timestamp struct.unpack(I, pkt[4:8])[0] ssrc struct.unpack(I, pkt[8:12])[0] header_len 12 csrc_count * 4 if extension: ext_len struct.unpack(H, pkt[header_len2:header_len4])[0] header_len 4 ext_len * 4 return { version: version, marker: marker, pt: payload_type, seq: seq, ts: timestamp, ssrc: ssrc, payload: pkt[header_len:] } def h264_rtp_to_annexb(pkts): # 按 seq 排序FU-A 需要重组 pkts sorted(pkts, keylambda p: parse_rtp_header(p)[seq]) out bytearray() fu_buffer None for p in pkts: h parse_rtp_header(p) if h[pt] ! 96: # 假设 H264 动态载荷类型为 96 continue nal h[payload] nal_type nal[0] 0x1F if nal_type 28: # FU-A 分片 start (nal[1] 7) 1 end (nal[1] 6) 1 orig_type nal[1] 0x1F if start: fu_buffer bytearray() # 重建 NAL 头F 和 NRI 来自 FU indicatortype 用原始类型 fu_buffer.append((nal[0] 0xE0) | orig_type) if fu_buffer is not None: fu_buffer.extend(nal[2:]) if end and fu_buffer is not None: out.extend(b\x00\x00\x00\x01) out.extend(fu_buffer) fu_buffer None elif nal_type in (1, 5, 7, 8): # 单包 NAL out.extend(b\x00\x00\x00\x01) out.extend(nal) return bytes(out)逻辑说明parse_rtp_header负责剥掉 RTP 头注意 CSRC 和扩展头会让头变长很多简易实现只减 12 字节遇到带扩展的流就错位。h264_rtp_to_annexb先按 sequence number 排序再处理 FU-A 分片起始分片重建 NAL 头中间分片只取载荷结束分片收尾。参数上载荷类型 96 是常见动态值实际要以 SDP 里的m行为准起始码用 4 字节00 00 00 01兼容性比 3 字节好。AAC 侧的处理思路类似但更简单如果 SDP 里config存在且 RTP 载荷不带 ADTS 头就直接把每个 AU 按长度拼起来如果带 ADTS 头需要先剥掉 7 字节头再交给 MP4 封装。判断方法看 SDP 的sizelength、indexlength等参数或者直接观察载荷前两字节是不是0xFF 0xF1。3. 时间戳对齐与 MP4 封装从裸流到可拖拽文件3.1 为什么不能直接把裸流塞进 MP4MP4 是 ISO BMFF 容器核心是 moov box 里的 track、sample tablestts、stsc、stsz、stco。每个 sample 对应一帧视频或一个 AAC 帧必须给出解码时间戳DTS、时长和文件偏移。裸流只有 RTP 的 32 位 timestamp而且音视频两条流的时间基准不同直接写会导致播放器要么只播第一帧要么音画不同步。正确做法是先分别把 H264 和 AAC 整理成「帧列表」每帧记录相对时间秒和字节数据再以最早的时间戳为基准做归一化最后用封装库写入 MP4。归一化时要注意 RTP timestamp 会回绕32 位溢出跨回绕点要加偏移量否则时间会突然倒退。3.2 用 PyAV 或 ffmpeg 做封装如果不想手写 MP4 box最稳的路子是借助 ffmpeg 的 libavformat。PyAV 是它的 Python 绑定能直接喂 packet。下面示例把上一步得到的 Annex-B 码流和 AAC 帧写成 MP4。import av from av import Packet def write_mp4(h264_annexb, aac_frames, out_path, fps25): container av.open(out_path, modew) vstream container.add_stream(h264, ratefps) vstream.width 1920 vstream.height 1080 vstream.pix_fmt yuv420p astream container.add_stream(aac, rate44100) astream.layout stereo # 视频按起始码切分 NAL每帧一个 packet # 实际项目里应按 AUD 或时间戳切帧这里简化演示 nals h264_annexb.split(b\x00\x00\x00\x01) for i, nal in enumerate(nals): if not nal: continue pkt Packet(nal, streamvstream) pkt.pts i pkt.dts i pkt.duration 1 container.mux(pkt) # 音频每个 AU 一个 packet for i, au in enumerate(aac_frames): pkt Packet(au, streamastream) pkt.pts i pkt.dts i pkt.duration 1024 # AAC 一帧 1024 采样 container.mux(pkt) container.close()逻辑说明add_stream时视频要指定分辨率和像素格式否则封装出的 moov 缺参数播放器解不了。视频 packet 的 pts/dts 这里用帧序号简化真实场景要按 RTP timestamp 换算成 90kHz 时基再乘系数。音频 duration 固定 1024 是 AAC-LC 的帧长如果采样率是 48000时间基也要相应调整。参数上ratefps影响 stts 里的时长写错会导致播放速度异常。如果环境里没有 PyAV用 ffmpeg 命令行也能救急先把 H264 存成.h264AAC 存成.aac再ffmpeg -i video.h264 -i audio.aac -c copy out.mp4。但这种方式对时间戳的还原依赖 ffmpeg 的探测遇到 RTP 时间戳跳变容易翻车适合快速验证。3.3 关键参数对照表参数H264 常见值AAC 常见值写 MP4 时的作用RTP 时钟频率9000044100 / 48000换算真实时间时间基1/900001/采样率stts 时长单位帧时长1/fps1024/采样率sample duration起始码00 00 00 01无Annex-B 分隔配置数据SPS/PPSAudioSpecificConfigavcC / esds box这张表是排错时的第一参照音画不同步先查时钟频率播放器报「moov 缺失」先查配置数据有没有写进 extradata。4. 避坑与排查合成 MP4 时最容易翻车的五件事4.1 现象MP4 能打开但只有第一帧后面全黑原因H264 的 SPS/PPS 没有写进 avcC box或者写成了 Annex-B 格式。MP4 要求 SPS/PPS 以 AVCDecoderConfigurationRecord 结构存在 extradata 里而不是混在码流中。解决封装前把 SPS/PPS 单独提取出来通过vstream.codec_context.extradata设置或者用 ffmpeg 的-bsf:v h264_mp4toannexb反向确认。PyAV 里可以在 add_stream 后手动赋值 extradata。4.2 现象音画不同步越播越偏原因音视频用了各自的 RTP 起始时间戳做零点没有统一基准。或者 AAC 的采样率写成了 44100实际流是 48000。解决取两条流最早时间戳的较小值作为全局零点所有 pts 减去它。采样率从 SDP 的rtpmap行读别猜。4.3 现象播放到某个时间点突然卡死或时间倒退原因RTP timestamp 32 位回绕。90000Hz 下约 13 小时回绕一次长录制必现。解决检测 timestamp 比上一个值小很多时累加一个 2^32 的偏移量。代码里维护wrap_offset每次回绕加一次。4.4 现象AAC 有杂音或直接解不出原因RTP 载荷带了 ADTS 头但封装时又按裸 AU 处理导致每个帧多出 7 字节垃圾。或者反过来该带 ADTS 的流被剥了头。解决看 SDP 里有没有config参数。有 config 通常表示裸 AU需要自己构造 AudioSpecificConfig没有 config 则载荷可能带 ADTS剥掉 7 字节再处理。4.5 现象ffmpeg 报「non-existing PPS 0 referenced」原因码流中间丢了 SPS/PPS或者 FU-A 重组时 NAL 头重建错误导致 PPS id 对不上。解决在关键帧前强制插入缓存的 SPS/PPS检查 FU-A 重组时(nal[0] 0xE0) | orig_type这一步NRI 位不能丢。5. 进阶技巧用 ffprobe 验证与长录制分片合成完别急着交付先跑一遍验证。ffprobe -v error -show_streams -show_format out.mp4看三个东西视频流的nb_frames是否和预期帧数一致音频流的sample_rate是否匹配format 里的duration是否和录制时长吻合。如果nb_frames是 0 或 N/A说明 sample table 没写对播放器拖进度条会失效。长录制场景比如几小时监控存档建议分片写 MP4每片 10 到 30 分钟。原因是 moov box 在文件末尾一次性写几小时的文件中途崩溃整个文件就废了。分片后每片独立可播代价是片与片之间有几帧重叠。实现上可以在封装层维护一个帧计数达到阈值就 close 当前 container 再开新的文件名带序号。另一个实用技巧是处理 B 帧。如果 H264 流里有 B 帧DTS 和 PTS 不再相等写 MP4 时必须分别计算。判断方法看 SPS 里的profile_idcHigh profile 通常有 B 帧。这时候 pts 按显示顺序排dts 按解码顺序排stts 表要按 dts 递增写。很多简易实现直接令 dtspts遇到 B 帧就花屏。我自己的习惯是每次合成完先 ffprobe 看帧数和时长再用播放器拖到中间、末尾各播 10 秒确认没有花屏和音画漂移最后才归档。这套流程帮我挡掉过好几次「文件能打开但内容不对」的返工。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/16 9:37:40

Spring Data JDBC 实战:轻量级聚合映射与 CRUD 全解析

1. Spring Data JDBC 到底解决了什么问题Spring Data JDBC 这个名字,很多做 Java 后端的同事第一反应是:这不就是 Spring Data JPA 的简写吗?还真不是。它是 Spring Data 家族里一个独立的成员,定位非常明确——在 JDBC 之上提供轻…

2026/10/16 9:37:40

QEMU与Multiboot实战:从零构建可启动内核全流程

简介:启动是操作系统学习的第一道门槛。这套Ustc OS课程Lab1资料提供了一套完整的QEMUMultiboot启动实验方案,内含实验报告与源代码。资源共10个文件,PDF报告讲解Multiboot头部设计与启动流程,汇编源码和链接脚本定义头部结构与内…

2026/10/16 9:37:40

Multiboot启动实验:从GRUB引导到C语言内核的完整流程

简介:面向刚开始接触操作系统实验的学生,这份资源以QEMU与Multiboot协议为切入点,完整演示如何在汇编中编写multiboot header:直接写VGA显存输出“helloworld”,借助串口输出“HELLOWORLD”,并将Ubuntu与主…

2026/10/16 11:04:41

BUUCTF PWN进阶:格式化字符串、栈迁移与ret2csu实战解析

刷 BUUCTF 的 PWN 题,从 41 到 45 这几道题给我的感觉是:前 40 题还在练手速,栈溢出让你把ret地址改了基本就能拿 shell;到了这个阶段,题目开始正经考“你懂不懂底层到底发生了什么”。格式化字符串怎么泄露地址、栈空…

2026/10/16 11:04:41

CLion源码编译OpenCV:构建可调试可追踪的开发闭环

1. 这不是“配环境”,而是构建一个可调试、可追踪、可复现的OpenCV开发闭环很多人点开这篇标题,第一反应是:“哦,又一个CLion配OpenCV的教程”。但我要先泼一盆冷水:如果你只是想把cv::imread跑起来,那大可…

2026/10/16 11:04:41

操作系统隔离与系统调用全链路:从用户态到内核态的穿越

6.S081这门操作系统课程讲到第三讲,终于开始触及最核心的部分:OS organization和system calls。前面几讲我们在用户态这边打转,fork、exec、wait这些系统调用用得很熟,但很少认真想过一个问题——你随手调一个write(),…

2026/10/16 11:04:41

多智能体协作新范式:DeepSeek Harness集成Codex与Claude Code实战

一眼看到这次更新的标题,我先是愣了一下——把 Codex、Claude Code 直接做成官方插件,这不再是"自己改造工具链"的玩法,而是把多个编程代理放进同一个调度框架里当子智能体用。我前阵子刚把一批并行任务从单代理切到多代理协作&…

2026/10/16 11:04:41

Netty带宽饱和下的连接处理:背压、水线与实战调优

刚经历了一次让人印象深刻的线上事故:某推送网关在业务高峰期整体带宽吃到接近上限,单个连接的数据还能正常处理,但新连接进来之后要么握手阶段超时,要么连进来以后迟迟拿不到响应。更诡异的是,已经建立的长连接里&…

2026/10/16 10:54:16

ConvLSTM实战:伦敦空气质量时空预测全流程解析

1. 问题定义与模型选型:为什么偏偏是ConvLSTM上一篇我们完成了伦敦地区空气质量数据的获取和预处理。拿到了20多个监测站点的逐小时污染物浓度记录,做了缺失值插补,也看了基本的时间分布规律。但数据准备好了并不代表能直接进入训练&#xff…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑