资讯动态

RTP H264/AAC裸流封装MP4实战:从组帧到moov写入

发布时间:2026/10/16 3:24:21 来源:尧图企业网站定制
简介这份资源面向音视频开发初学者与流媒体协议实践者聚焦将RTP承载的H264视频与AAC音频数据重新封装为可播放MP4文件这一典型场景可用于直播录制、流媒体抓包还原、播放器调试等方向。压缩包共31个文件约197KB以17个C/C头文件与2个源文件构成核心逻辑辅以Makefile、project.xml等构建配置以及sdp会话描述、cmmb2rtp工具脚本和一份test.mp4样例整体呈现出一个结构清晰的rtp2mp4工程骨架。已有486人学习下载说明该方向具备一定关注度。读者可从中获取RTP解包、H264与AAC帧提取、MP4封装流程的完整代码参考理解头文件划分与模块职责并借助样例文件验证合成结果适合作为流媒体入门的练手项目或二次开发基础。1. 从一堆裸流到能播的 MP4这个项目到底解决什么问题如果你抓过 RTP 包大概经历过这种绝望Wireshark 里 H264 的 NALU 和 AAC 的 AU 一段段躺在那里时间戳、序列号、标记位全都有可就是没法直接拖进播放器。原因不复杂——RTP 是传输层协议它只负责把媒体切片送出去不负责告诉你「这一帧该在什么时间、以什么顺序、拼成什么容器」。这个项目干的事就是把 RTP 里的 H264 和 AAC 裸流按时间戳重新组装、补齐容器元信息最终合成一个标准 MP4 文件。它适合两类人一类是做流媒体抓包分析、需要把会话落盘成可回放文件的开发者另一类是想搞懂 MP4 封装moov、stbl、stsd、stts、stsc、stco 这一整套 box到底怎么和 H264/AAC 对应起来的人。说白了RTP 是「运货的车」MP4 是「带说明书的货架」这个项目就是那本说明书。2. 技术选型与整体架构为什么是「先解包、再组帧、后封装」2.1 三段式流水线设计这个项目的核心思路可以拆成三段RTP 解析层、媒体组帧层、MP4 封装层。RTP 解析层负责拆掉 RTP 头拿到 payload、时间戳timestamp、序列号seq、标记位marker媒体组帧层负责把 H264 的 FU-A/STAP-A 分片还原成完整 NALU把 AAC 的每个 RTP 包还原成一个 AUAccess UnitMP4 封装层负责把组好的帧按时间顺序写进 mdat同时生成 moov 里的各种索引表。为什么不用现成的 FFmpeg 一行命令搞定因为一旦你直接ffmpeg -i rtp://...中间发生了什么就成了黑匣子。这个项目的价值恰恰在于把「RTP 时间戳 → MP4 时间基」的换算、H264 的 SPS/PPS 提取、AAC 的 ADTS 头构造这些细节全部摊开。常见做法是H264 走 avcCAVCDecoderConfigurationRecord存 SPS/PPSAAC 走 esdsES Descriptor存 AudioSpecificConfig这两块配置信息决定了播放器能不能正确初始化解码器。2.2 关键数据结构设计组帧层需要维护几个状态当前正在拼接的 NALU 缓冲区、上一帧的时间戳、以及一个待写入的帧队列。下面是一个简化的帧结构定义用 Python 描述实际项目常用 C/C 或 Java但逻辑一致class MediaFrame: def __init__(self): self.pts 0 # 显示时间戳单位统一为 90kHz 或毫秒 self.dts 0 # 解码时间戳H264 有 B 帧时与 pts 不同 self.data b # 完整帧数据H264 为 NALUAAC 为 AU self.is_keyframe False # 是否关键帧写 stss 表要用 self.track_id 0 # 0 视频1 音频pts和dts的区分是 H264 封装的第一个坑如果流里有 B 帧解码顺序和显示顺序不一致MP4 的 sttsTime-to-Sample表必须用 DTS 来排cttsComposition Time-to-Sample表来记录 PTS 和 DTS 的差值。很多简易实现直接拿 RTP 时间戳当 PTS 又当 DTS遇到 B 帧就花屏或者音画不同步。2.3 时间基换算RTP 时钟和 MP4 时间基的对齐RTP 里 H264 的时钟频率通常是 90000HzAAC 通常是 44100Hz 或 48000Hz而 MP4 的 mvhd 里有一个统一的 timescale。常见做法是把视频轨的 timescale 设成 90000音频轨设成采样率这样 RTP 时间戳可以直接用不用做浮点换算避免累积误差。下面这段是时间戳归一化的核心逻辑def normalize_timestamp(rtp_ts, base_ts, clock_rate, mp4_timescale): # 计算相对于第一个包的时间偏移 delta (rtp_ts - base_ts) 0xFFFFFFFF # 处理 32 位回绕 # 换算到 MP4 时间基 return int(delta * mp4_timescale / clock_rate)参数说明rtp_ts是当前包的 RTP 时间戳base_ts是会话第一个包的时间戳clock_rate是时钟频率视频 90000音频看采样率mp4_timescale是目标时间基。 0xFFFFFFFF是为了处理 RTP 时间戳 32 位溢出回绕这个细节在长时间抓包里必现不处理就会出现时间戳突然跳变、播放器直接卡死。3. H264 与 AAC 组帧从 RTP payload 到完整帧3.1 H264 的 FU-A 分片重组H264 over RTP 最常见的打包方式是 FU-AFragmentation Unit一个大的 NALU 被切成多个 RTP 包每个包带一个 FU indicator 和 FU header。重组逻辑是遇到 FU-A 起始包Start 位为 1就开一个新缓冲区中间包追加数据结束包End 位为 1时把缓冲区里的数据前面补上真正的 NALU 头形成一个完整 NALU。def handle_fu_a(payload, fu_state): fu_indicator payload[0] fu_header payload[1] nal_type fu_indicator 0x1F start_bit (fu_header 7) 0x01 end_bit (fu_header 6) 0x01 original_nal_type fu_header 0x1F if start_bit: # 重建 NALU 头F(1) NRI(2) Type(5) nalu_header (fu_indicator 0xE0) | original_nal_type fu_state.buffer bytes([nalu_header]) payload[2:] elif end_bit: fu_state.buffer payload[2:] complete_nalu fu_state.buffer fu_state.buffer b return complete_nalu else: fu_state.buffer payload[2:] return None逻辑说明fu_indicator的高 3 位是 F 和 NRI低 5 位固定为 28FU-A 类型fu_header的高 2 位是 Start 和 End低 5 位才是原始 NALU 类型。重建 NALU 头时要把 fu_indicator 的 F/NRI 和 fu_header 的原始类型拼起来。这个拼接如果搞错解码器会直接报「invalid NALU type」。3.2 SPS/PPS 的提取与 avcC 构造H264 解码器初始化需要 SPSSequence Parameter Set和 PPSPicture Parameter Set。在 RTP 流里它们可能通过 STAP-A 打包也可能单独成包。提取出来后要构造成 avcC box 的内容def build_avcC(sps_list, pps_list): # configurationVersion 1 avcC bytes([0x01]) # AVCProfileIndication, profile_compatibility, AVCLevelIndication avcC sps_list[0][1:4] # lengthSizeMinusOne 3表示 4 字节长度前缀 avcC bytes([0xFF]) # numOfSequenceParameterSets avcC bytes([0xE0 | len(sps_list)]) for sps in sps_list: avcC len(sps).to_bytes(2, big) sps # numOfPictureParameterSets avcC bytes([len(pps_list)]) for pps in pps_list: avcC len(pps).to_bytes(2, big) pps return avcC参数说明lengthSizeMinusOne 3意味着每个 NALU 前面用 4 字节大端长度前缀这是 MP4 里 H264 的标准存法和 Annex-B 的 0x00000001 起始码不同。写 mdat 时每个 NALU 都要转成「4 字节长度 数据」的格式否则播放器找不到帧边界。3.3 AAC 的 AU 重组与 ADTS 头AAC over RTP 相对简单通常一个 RTP 包就是一个 AU但要注意 AU 头AU-headers-length的存在。有些打包方式会在 payload 前加 2 字节的 AU header记录每个 AU 的长度和索引。去掉这个头之后剩下的就是裸 AAC 数据。写 MP4 时AAC 不需要 ADTS 头那是流式传输用的但需要 AudioSpecificConfigdef build_audio_specific_config(audio_object_type, sample_rate_index, channel_config): # 5 bits object type, 4 bits sample rate index, 4 bits channel config config (audio_object_type 11) | (sample_rate_index 7) | (channel_config 3) return config.to_bytes(2, big)这个 2 字节的配置会放进 esds box 的 DecoderSpecificInfo 里。如果 sample_rate_index 填错播放器会按错误的采样率解码声音变调如果 channel_config 填错可能只有单声道或者直接静音。4. MP4 封装把帧写进正确的 box 里4.1 MP4 文件结构速览一个可播放的 MP4 至少包含ftyp文件类型、moov元信息、mdat媒体数据。moov 里每个 track 有 tkhd、mdiamdia 下有 mdhd、hdlr、minfminf 下有 stbl。stbl 是核心包含 stsd采样描述、stts时间戳表、stsc采样到 chunk 映射、stsz采样大小、stcochunk 偏移、stss关键帧表。写文件时通常先写 ftyp 和 mdat 占位最后回填 moov因为 moov 里很多偏移量依赖 mdat 的实际位置。4.2 stts 和 stsz 的生成stts 记录每个采样的时长stsz 记录每个采样的大小。对于固定帧率的视频stts 可以压缩成「N 个采样每个时长 delta」对于音频通常每个 AAC 帧是 1024 个采样点时长固定。下面是一个简化的 stts 生成逻辑def build_stts(frames): # frames 按 dts 排序 entries [] if not frames: return entries current_delta frames[1].dts - frames[0].dts if len(frames) 1 else 0 count 1 for i in range(1, len(frames)): delta frames[i].dts - frames[i-1].dts if delta current_delta: count 1 else: entries.append((count, current_delta)) current_delta delta count 1 entries.append((count, current_delta)) return entries逻辑说明stts 的每一项是sample_count, sample_delta连续相同 delta 的帧会被合并成一项。这个合并能显著减小 moov 体积尤其是固定帧率的流。如果每帧都写一项一个几小时的录像 moov 可能几百 KB影响起播速度。4.3 stco 与 mdat 偏移回填stco 记录每个 chunk 在文件中的绝对偏移。如果采用「先写 mdat 再写 moov」的顺序mdat 的位置在写之前就确定了stco 可以直接算。但如果采用「先写 moov 占位再回填」的方式就需要在 mdat 写完后 seek 回去修改。常见做法是先把所有帧的数据写进一个临时缓冲区统计总大小然后一次性写出 ftyp mdat 头 数据 moov这样所有偏移都能提前算出来避免文件 seek。def write_mp4(output_path, video_frames, audio_frames, avcC, audio_config): mdat_data b chunk_offsets [] for frame in video_frames audio_frames: chunk_offsets.append(len(mdat_data)) # H264 需要 4 字节长度前缀 if frame.track_id 0: mdat_data len(frame.data).to_bytes(4, big) frame.data else: mdat_data frame.data # 此处省略 moov 各 box 的序列化实际项目按 ISO/IEC 14496-12 逐字段写 with open(output_path, wb) as f: f.write(build_ftyp()) f.write(build_mdat_header(len(mdat_data))) f.write(mdat_data) f.write(build_moov(video_frames, audio_frames, chunk_offsets, avcC, audio_config))参数说明chunk_offsets记录每个采样在 mdat 内的相对偏移写 stco 时要加上 mdat 数据起始的绝对偏移。H264 帧加 4 字节长度前缀AAC 帧直接写裸数据。这个顺序保证了 moov 在文件末尾播放器需要先读 moov 再 seek 到 mdat网络播放时起播稍慢但本地播放没问题。5. 避坑与排查那些让我熬夜的 RTP 封装问题5.1 现象播放器能识别文件但黑屏原因SPS/PPS 没有正确写入 avcC或者 avcC 的 lengthSizeMinusOne 和 mdat 里的 NALU 长度前缀字节数不一致。解决用 mp4box 或 ffprobe 检查 avcC 内容确认 lengthSizeMinusOne 为 3 时mdat 里每个 NALU 前确实是 4 字节长度。我一般会写个脚本把 mdat 前几百字节 dump 出来人工核对第一个 NALU 的长度字段是否等于后续数据长度。5.2 现象音画不同步越播越偏原因视频和音频用了不同的时间基或者音频的 sample_rate_index 填错导致解码时长计算错误。解决统一用采样点作为时间单位视频轨 timescale 设 90000音频轨设采样率所有时间戳换算成整数。检查 esds 里的 samplingFrequencyIndex 是否和实际采样率匹配44100 对应 index 448000 对应 index 3。5.3 现象文件播到一半卡死原因RTP 时间戳 32 位回绕没有处理或者丢包导致 NALU 不完整但被当成完整帧写入。解决时间戳计算时用(current - base) 0xFFFFFFFF处理回绕组帧时检查 FU-A 的 Start 和 End 是否配对如果收到新的 Start 但上一个 FU 还没 End丢弃上一个不完整的缓冲区。丢包在真实网络里是常态宁可丢一帧也不能写半帧。5.4 现象moov 体积异常大原因stts 没有合并相同 delta或者 stsz 每个采样都写独立项。解决stts 按连续相同 delta 合并stsz 如果所有采样大小相同可以用固定值模式但视频帧大小通常不同只能逐项写。另外 stco 如果每个采样一个 chunk条目数等于帧数可以改成多个采样一个 chunk 来减少条目。5.5 现象AAC 有杂音或爆音原因AU header 没有正确剥离或者 AudioSpecificConfig 里的 object type 填错比如把 HE-AAC 当成 LC-AAC。解决先确认 RTP payload 前是否有 2 字节 AU-headers-length如果有按长度字段逐个剥离AudioSpecificConfig 的 object type 从 SDP 的 config 参数里解析不要硬编码。6. 二次开发从能跑到好用我踩过的那些坑这个项目跑通之后二次开发的空间主要在三个方向性能、健壮性、格式扩展。性能上最初的实现是每来一个 RTP 包就 memcpy 一次抓大流的时候 CPU 直接飙满。后来改成环形缓冲区加零拷贝组帧时只记录偏移不复制数据写 mdat 时一次性拼接吞吐量翻了将近三倍。健壮性上我加了一个「帧完整性校验」每个 NALU 重组完成后检查其长度是否和 FU-A 分片累计的长度一致不一致就丢弃并打日志。这个校验帮我抓到了好几次因为 UDP 丢包导致的半帧写入。格式扩展方面最常见的需求是支持 H265HEVC。H265 的 NALU 头和 H264 不同avcC 要换成 hvcCSPS/PPS/VPS 三组参数都要存。改造时不要试图复用 H264 的组帧代码单独写一个 H265 的 handler因为 FU 的头部结构完全不一样。另一个扩展方向是支持 fMP4分片 MP4把 moov 和 moof 分开写适合边录边播的场景。这时候 stbl 里的表要拆到每个 moof 的 traf 里复杂度上一个台阶建议先把普通 MP4 写稳再动。验证方法上我习惯用三层校验第一层用 ffprobe 看流信息是否正确帧数、时长、码率第二层用 mp4box -info 看 box 结构是否完整第三层实际播放拖动进度条看 seek 是否正常。seek 正常说明 stco 和 stss 写对了这一步能筛掉大部分索引表错误。还有一个血泪经验每次改完封装逻辑先用一段 10 秒的短流测试确认没问题再跑长流。长流跑一次几分钟短流几秒钟迭代效率差几十倍。从那以后我每次改 box 写入逻辑都强制先用短流走一遍 ffprobe 播放器 seek 测试确认无误再上长流。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑