资讯详情

资讯详情

RTP打包H264实战:从NALU分割到FU-A分片与RTSP推流

简介RTP封装H264是实时视频传输的关键技术。压缩包提供了一套基于VC6的完整示例演示如何从H264码流中分割NAL单元构造RTP数据包并通过网络发送最终由VLC播放器接收解码涵盖协议解析、数据封装、传输配置和验证环节适合流媒体开发初学者与网络编程人员用作学习参考。包内共21个文件大小约929KB包含C源码.cpp/.h、可直接运行的exe程序、sdp会话描述文件、测试视频流.264/.h264、调试符号.pdb/.obj以及VC6工程文件.dsp/.dsw便于对照代码理解实现。已有325人学习该资源。通过实际操作可重点掌握NAL单元与RTP报文格式的对应关系、RTP时间戳与序列号的处理方式以及目的IP和端口号在VLC接收端配置中的一致性要求w.sdp文件为启动会话提供了配置模板NALDecoder则展示了从H264到RTP的核心封装逻辑是快速搭建实验环境、排查收发异常的可复用资料。1. 项目概述为什么我要用RTP发送H264做音视频底层开发的人迟早都会撞上“把H264裸流封装成RTP包”这件事。我之前在调试一个嵌入式设备上的实时监控推流模块时摄像头输出的是标准的H264 Annex-B字节流而播放端通过RTSP拉流中间必须要经过一层RTP打包。踩完一轮坑之后把整个思路、代码细节、抓包验证过程整理出来希望能帮到正在做类似工作的朋友。先说清楚这个东西是什么RTPReal-time Transport Protocol是互联网上传输实时音视频数据的标准协议而H264是目前最主流的视频编码格式。H264编码器输出的原始数据叫裸流elementary stream它不能直接扔到网络上因为接收端需要知道每一帧从哪里开始、需不需要分片、时间戳是多少。RTP协议干的事情就是把这些元信息补上把裸流切成适合网络传输的RTP包再交给UDP发出去。这个方案能解决什么问题最核心的是让H264视频能够在IP网络上“实时”传输并且接收端能正确重组、解码、播放。如果你在做一个网络摄像头、视频会议终端、或者任何需要低延迟视频传输的系统都绕不开这条链路。适合谁参考主要是刚接触音视频传输的嵌入式开发工程师、C/C后端开发以及对FFmpeg底层封装好奇的读者。2. 整体设计思路先分清楚H264的三种打包模式H264数据不是简单一刀切就能塞进RTP包的。RFC 6184H264 over RTP的规范里定义了三种打包方式单一NALU打包Single NAL Unit、聚合打包STAP-A、分片打包FU-A。绝大多数场景下我们主要用单一打包和FU-A分片。为什么不能总用单一打包因为RTP的承载协议UDP理论上单包最大可以到65535字节但实际上以太网MTU最大传输单元是1500字节扣掉IP头20字节和UDP头8字节留给RTP的载荷通常只有1450字节左右。如果H264编码器输出一个大I帧一个NALU可能超过几千甚至上万字节硬塞进一个UDP包里大概率会在IP层被分片而IP分片在公网环境下极易丢包一旦丢了任何一个分片整个UDP包就废了接收端根本拼不回来。所以设计思路很清晰NALU长度小于等于MTU阈值比如1400字节用单一NALU模式一包发一个完整NALU。NALU长度大于MTU阈值用FU-A分片模式把一个NALU切成多个RTP包每个包大小控制在1400字节以内。聚合包模式STAP-A一般用于把多个小的NALU例如SPS、PPS合并到一个RTP包里发送减少包数量降低网络开销。但在实际开发中我更推荐直接把SPS和PPS放在RTSP的DESCRIBE响应里或者通过RTP的带内传输与关键帧一起发一次处理。这样逻辑更简单踩坑更少。再说说RTP头的构造。RTP头固定12字节关键字段包括V版本号固定为2。P填充位一般置0。X扩展位一般置0。CCCSRC计数一般为0因为没有混合源。M标记位对H264来说一帧的最后一个RTP包置1其余为0。PT载荷类型H264通常用96或97这是动态范围96-127需要与接收端协商一致。sequence number序列号每个RTP包递增接收端用来检测丢包和排序。timestamp时间戳以90000Hz为时钟频率每发送一帧数据时间戳递增为“90000 / 帧率”。SSRC随机生成一个32位ID标识这个RTP流。有了这些前置知识代码写起来就有了骨架。3. 核心细节解析NALU头、FU-A分片和SPS/PPS的处理3.1 NALU头怎么解析和修改H264裸流中的每个NALU以起始码开头。Annex-B格式的起始码是00 00 00 01或者00 00 01。起始码之后紧跟的1字节是NALU头。NALU头这1字节有3个部分forbidden_zero_bitbit7必须为0。nal_ref_idcbit6-5表示参考级别0表示非参考帧如B帧非0表示参考帧如I帧、P帧。nal_unit_typebit4-0NALU类型1表示非IDR的切片5表示IDR关键帧7表示SPS8表示PPS。类型5IDR和类型1P帧是我们最常打交道的。在RTP单一打包模式下这个NALU头直接作为RTP载荷的第一个字节不需要改。但FU-A模式要动一个小手脚原来的NALU头需要拆成两部分——FU indicator1字节和FU header1字节放到RTP载荷最前面然后后面跟上NALU去掉第一个字节后的剩余数据。拆分的规则如下FU indicator nal_ref_idc 5 | 2828是FU-A的类型值。FU header 起始码后的NALU类型 | 如果是分片开始加0x80 | 如果是分片结束加0x40。举个例子假设NALU头的原始值是0x65二进制01100101说明这是IDR帧类型5nal_ref_idc3。分片时FU indicator 3 5| 28 0x7C。FU header起始分片 5 | 0x80 0x85。FU header中间分片 5。FU header结束分片 5 | 0x40 0x45。我见过不少人犯迷糊直接在FU indicator里写了0x60 | 28把类型5也塞进去了。记住类型信息只在FU header里保存FU indicator里的低5位永远是28。3.2 FU-A分片的完整流程分片的核心算法其实很简单把NALU从第2字节开始按最大载荷长度切成若干块每块前面加上FU indicator和FU header。具体步骤计算NALU总长度。如果总长度小于阈值走单一NALU打包否则进入分片流程。计算分片数 总长度 - 1 最大载荷 - 1 / 最大载荷。依次取出每片数据第一片置Start位最后一片置End位中间片均不置位。这里有个容易踩的性能坑如果每次打包都重新malloc一个缓冲区在高帧率场景下会频繁触发内存分配导致性能抖动。我的做法是定义一个大数组比如uint8_t rtpBuf[1500]每次打包都在这个栈数组里拼包拼好之后直接调用发送接口函数返回前数据已经全部拷走栈上内存自生自灭完全避免堆分配。还有一个细节部分解码器对FU-A的End位特别敏感如果End包没发或者置位错误这一整帧会被丢弃。所以发送逻辑里要严格保证一个NALU的所有FU-A包必须连续发出中间不能插入其他NALU的数据。3.3 SPS/PPS为什么重要如何随RTP发送SPS序列参数集和PPS图像参数集是解码器初始化的关键参数包含分辨率、帧率、编码档次等信息。如果接收端没有收到SPS/PPS后续的视频流再完整也解不出画面。在RTSP推流场景下SPS/PPS通常通过两种途径传递带外传输在RTSP DESCRIBE响应的sdp属性里通过sprop-parameter-sets字段传给客户端。带内传输在IDR帧前把SPS和PPS各自打包成RTP包发出去。为了兼容性和抗丢包能力我一般两种都做。带内发送时SPS和PPS各成一个RTP包载荷类型同样是96时间戳与后续IDR帧的时间戳保持一致这样接收端可以把它当作视频流的一部分按序处理。如果你看到SPS/PPS字段里的profile-level-id不对解码端可能直接拒绝解码。所以sprop-parameter-sets里的Base64字符串必须和实际编码器输出的SPS/PPS字节完全对应。4. 实操过程C语言实现的最小RTP打包器下面给出一个可以在Linux/Windows直接编译的最小实现框架。核心函数有三个初始化RTP头、发送单一NALU、发送FU-A分片。#include stdint.h #include string.h #include arpa/inet.h #define RTP_HEADER_LEN 12 #define MAX_RTP_PAYLOAD 1400 typedef struct { uint8_t rtpBuf[1500]; uint16_t seq; uint32_t ssrc; uint8_t pt; uint32_t timestamp; } RtpPackCtx; static void rtp_fill_header(RtpPackCtx *ctx, int payload_len, int mark) { uint8_t *p ctx-rtpBuf; p[0] 0x80; // V2, P0, X0, CC0 p[1] (mark ? 0x80 : 0) | (ctx-pt 0x7F); p[2] (ctx-seq 8) 0xFF; p[3] ctx-seq 0xFF; p[4] (ctx-timestamp 24) 0xFF; p[5] (ctx-timestamp 16) 0xFF; p[6] (ctx-timestamp 8) 0xFF; p[7] ctx-timestamp 0xFF; memcpy(p 8, ctx-ssrc, 4); ctx-seq; } void rtp_send_nalu(RtpPackCtx *ctx, const uint8_t *nalu, uint32_t len, int (*send_packet)(uint8_t *, int)) { uint8_t nal_header nalu[0]; if (len MAX_RTP_PAYLOAD 1) { // 单一NALU rtp_fill_header(ctx, len, 1); memcpy(ctx-rtpBuf RTP_HEADER_LEN, nalu, len); send_packet(ctx-rtpBuf, RTP_HEADER_LEN len); } else { // FU-A分片 uint32_t offset 1; // 跳过NALU头 uint32_t remain len - 1; int start 1; while (remain 0) { uint32_t piece remain MAX_RTP_PAYLOAD ? MAX_RTP_PAYLOAD : remain; int mark (piece remain) ? 1 : 0; rtp_fill_header(ctx, piece 2, mark); uint8_t *p ctx-rtpBuf RTP_HEADER_LEN; // FU indicator p[0] (nal_header 0x60) | 28; // FU header p[1] (nal_header 0x1F) | (start ? 0x80 : 0) | (mark ? 0x40 : 0); memcpy(p 2, nalu offset, piece); send_packet(ctx-rtpBuf, RTP_HEADER_LEN piece 2); offset piece; remain - piece; start 0; } } }这段代码里最需要注意的地方是mark位的设置。在单一NALU模式下一个包就是一整帧所以mark直接置1。在FU-A模式下只有最后一片的mark置1其余为0。有些接收端不看mark也能工作但规范的播放器比如VLC、FFmpeg的rtp depay都会参考mark来确定帧边界mark错了会导致花屏或卡顿。时间戳递增规则是每发送一帧视频时间戳增加90000 / fps。如果帧率是25fps那么每帧的时间戳增量就是3600。注意不是每包增加因为同一帧的多个FU-A包时间戳必须相同。5. 常见问题与排查技巧实录5.1 接收端花屏、马赛克但能出画面这类问题的根源多数是数据不完整或顺序错乱。先用抓包工具Wireshark过滤RTP流检查三件事每个RTP包的sequence是否连续。如果中间有大段跳号说明发生了丢包。本地回环测试丢包概率低如果走公网需要确认UDP缓冲区是否过小。同一帧内所有FU-A分片的时间戳是否一致。如果时间戳在分片之间跳变播放器会认为是不同帧的数据画面必然撕裂。FU-A的Start和End位是否成对出现。一个NALU的第一个包必须有Start最后一个包必须有End。如果丢了其中一个这个NALU是无法重组的。我以前遇到过一个诡异现象每解码几个帧就出现一次绿屏闪烁。后来抓包发现是发送端在打包IDR帧时把SPS/PPS也塞进了同一个RTP流里但PT类型被误设成了96号的动态类型而接收端sdp里声明的PT也是96按理说没问题。最后排查到时间戳上SPS/PPS包的时间戳比IDR帧的时间戳多加了3600导致播放器把它们当成了独立帧解码器状态被搞乱。修复方式很简单SPS/PPS的时间戳必须与IDR帧完全一致。5.2 发送端卡顿CPU占用过高很多人在做裸流封装时喜欢对每个RTP包都做一次sendto系统调用。如果帧率25fps、每帧10个分片那每秒就有250次sendto。系统调用开销在嵌入式设备上不可忽视。我的优化思路是“先组大包再一次性发送”。具体来说如果一个NALU的分片数不多可以把它所有FU-A分片依次填到同一个发送缓冲区里然后一次sendto把整个缓冲区发出去。但这种方式对接收端不友好因为一个UDP包里包含多个RTP包时接收端必须手动解析会增加复杂度。更稳妥的方式是启用UDP的GSOGeneric Segmentation Offload让内核把要发送的多段数据合并成一个UDP报文减少系统调用次数。不过这个特性在不同平台上的支持度不一样嵌入式Linux内核需要确认配置。如果不想引入GSO那就尽量在应用层做好内存复用避免每发一个包都重新计算、重新拷贝。实测下来纯C实现、栈缓冲复用、避免锁竞争在单核1GHz的ARM处理器上1080p 25fps的视频流封装加发送CPU占用能控制在15%以内。5.3 播放器首帧出画慢甚至黑屏黑屏或长时间不出画90%是SPS/PPS没送到。排查顺序是确认RTSP DESCRIBE响应里的sprop-parameter-sets是否正确。确认带内SPS/PPS是否在每个IDR帧之前都发送了。有些推流端只在编码器刚启动时发送一次SPS/PPS但如果接收端是在推流中途开始播放它就没机会收到SPS/PPS自然黑屏。确认SPS/PPS包的mark位和PT是否正确。我的经验是每次检测到IDR帧时强制在IDR帧的RTP包之前插入SPS和PPS包。这样对任何时刻接入的播放器都友好。代价只是一点点带宽完全值得。5.4 时间戳基准不一致导致音视频不同步如果你同时推音频和视频RTP时间戳的基准必须统一。音频的时钟频率一般是8000或48000Hz视频是90000Hz。接收端在RTSP的RTP-Info头里会拿到初始时间戳如果视频流中途时间戳回跳或跳变太大会导致音视频不同步。我踩过的坑是视频编码器输出的PTS是基于系统时钟微秒的我直接把微秒值除以11.111当作RTP时间戳结果因为浮点误差导致每秒钟会漂移几十毫秒长时间播放后音画不同步越来越明显。正确做法是用单调递增的计数器维护RTP时间戳每发送一帧视频固定加上90000 / fps。不要试图从PTS反算编码器的PTS主要用于B帧重排序和RTP时间戳的映射关系不值得在发送端引入额外复杂度。6. 工具选型VLC、FFmpeg和Wireshark如何配合验证写完代码后验证是最关键的一步。我习惯用FFmpeg和VLC做双端验证。先用自己的发送程序把RTP包送到本地UDP端口然后用VLC打开vlc rtp://127.0.0.1:5004如果VLC能正常出画说明RTP打包基本正确。但VLC的容错能力比较强有时候小问题它也能忍所以还要用FFmpeg做严格验证。通过sdp文件接收ffmpeg -protocol_whitelist file,udp,rtp -f sdp -i test.sdp -f null -FFmpeg的rtp depayloader对包的规范性要求比较高如果它报错或者解出的图像有异常说明还有隐藏问题。Wireshark主要用于定位细节问题。过滤表达式rtp可以直接看到每个RTP包的头部信息。如果看到sequence乱序或者时间戳跳变先检查自己的发送逻辑再检查是不是UDP接收缓冲区溢出导致丢包。可以用netstat -su查看系统的UDP丢包统计如果RcvbufErrors在增长就说明接收缓冲区太小需要增大rmem_max和rmem_default。6.1 sdp文件怎么写本地调试时sdp文件可以手工写成这样v0 o- 0 0 IN IP4 127.0.0.1 sH264 Test cIN IP4 127.0.0.1 t0 0 mvideo 5004 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1; profile-level-id42C01E; sprop-parameter-setsZ0LAHtkBkFqg,aMuMsg其中packetization-mode1表示支持非交错模式即支持FU-Asprop-parameter-sets里的Base64字符串就是SPS和PPS用逗号分隔。如果发送端不做RTSP直接用RTP推流接收端就靠这个sdp文件来初始化所以里面的信息必须和RTP包里实际发送的内容完全一致。6.2 FFmpeg如何模拟接收端如果你的发送程序还没有实现RTSP只是裸RTP推流FFmpeg可以这样直接收ffmpeg -protocol_whitelist file,udp,rtp -f sdp -i test.sdp -c:v copy output.h264如果推流正确output.h264就是标准的H264裸流可以用ffprobe查看分辨率、帧率等信息进一步确认参数是否正确。7. 完整可运行的测试代码最后给出一段可以直接在Linux上编译运行的测试代码它读取一个H264文件把每一帧发到指定端口。#include stdio.h #include stdlib.h #include stdint.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/time.h #define RTP_HEADER_LEN 12 #define MAX_RTP_PAYLOAD 1400 static uint16_t seq 0; static uint32_t ssrc 0x12345678; static uint32_t timestamp 0; static int udp_fd; static struct sockaddr_in dest_addr; static int send_rtp(uint8_t *buf, int len) { return sendto(udp_fd, buf, len, 0, (struct sockaddr *)dest_addr, sizeof(dest_addr)); } static void fill_rtp_header(uint8_t *buf, int payload_len, int mark, uint8_t pt) { buf[0] 0x80; buf[1] (mark ? 0x80 : 0) | (pt 0x7F); buf[2] (seq 8) 0xFF; buf[3] seq 0xFF; buf[4] (timestamp 24) 0xFF; buf[5] (timestamp 16) 0xFF; buf[6] (timestamp 8) 0xFF; buf[7] timestamp 0xFF; buf[8] (ssrc 24) 0xFF; buf[9] (ssrc 16) 0xFF; buf[10] (ssrc 8) 0xFF; buf[11] ssrc 0xFF; seq; } static void send_h264_nal(uint8_t *nal, uint32_t len, uint8_t pt) { uint8_t buf[1500]; uint8_t nal_header nal[0]; if (len MAX_RTP_PAYLOAD 1) { fill_rtp_header(buf, len, 1, pt); memcpy(buf RTP_HEADER_LEN, nal, len); send_rtp(buf, RTP_HEADER_LEN len); } else { uint32_t offset 1; uint32_t remain len - 1; int start 1; while (remain 0) { uint32_t piece remain MAX_RTP_PAYLOAD ? MAX_RTP_PAYLOAD : remain; int mark (piece remain) ? 1 : 0; fill_rtp_header(buf, piece 2, mark, pt); buf[RTP_HEADER_LEN] (nal_header 0x60) | 28; buf[RTP_HEADER_LEN 1] (nal_header 0x1F) | (start ? 0x80 : 0) | (mark ? 0x40 : 0); memcpy(buf RTP_HEADER_LEN 2, nal offset, piece); send_rtp(buf, RTP_HEADER_LEN piece 2); offset piece; remain - piece; start 0; } } } int main(int argc, char *argv[]) { if (argc 4) { fprintf(stderr, Usage: %s input.h264 ip port\n, argv[0]); return -1; } FILE *fp fopen(argv[1], rb); if (!fp) { perror(fopen); return -1; } udp_fd socket(AF_INET, SOCK_DGRAM, 0); dest_addr.sin_family AF_INET; dest_addr.sin_port htons(atoi(argv[3])); inet_pton(AF_INET, argv[2], dest_addr.sin_addr); uint8_t *buf malloc(1024 * 1024); size_t len; while ((len fread(buf, 1, 4, fp)) 4) { if (memcmp(buf, \x00\x00\x00\x01, 4) 0) { // 读取一个NALU size_t nal_len 4; uint8_t c; while (fread(c, 1, 1, fp) 1) { if (nal_len 3 buf[nal_len-3] 0 buf[nal_len-2] 0 buf[nal_len-1] 1) { fseek(fp, -3, SEEK_CUR); nal_len - 3; break; } else if (nal_len 4 buf[nal_len-4] 0 buf[nal_len-3] 0 buf[nal_len-2] 0 buf[nal_len-1] 1) { fseek(fp, -4, SEEK_CUR); nal_len - 4; break; } else { if (nal_len 1024 * 1024) { buf[nal_len] c; } } } uint8_t nal_type buf[4] 0x1F; if (nal_type 5) { timestamp 3600; } else if (nal_type 1) { timestamp 3600; } send_h264_nal(buf 4, nal_len - 4, 96); } } free(buf); fclose(fp); close(udp_fd); return 0; }这个测试代码读取Annex-B格式的H264文件每读取到一个NALU就调用发送函数帧率固定按25fps的时间戳递增。注意里面的时间戳递增逻辑放在nal_type 5 || nal_type 1的判断里这样SPS/PPS类型的NALU不会改变时间戳保证了SPS/PPS包与后续IDR帧的时间戳一致。编译运行gcc -o rtp_send rtp_send.c ./rtp_send test.h264 127.0.0.1 5004再用VLC播放rtp://127.0.0.1:5004就能看到画面。8. 实操中容易忽略的几个坑第一NALU和帧不能画等号。一个视频帧可能由多个NALU组成特别是编码器开了多slice模式时。如果只看nal_type为1或5就认为是一帧而实际上同一帧还有其他NALU发送端必须把这个帧的所有NALU都发完并且在最后一个NALU的RTP包上设置mark位。简单处理方式是强制编码器关闭多slice我通常把x264的slice-max-size设成0保证一帧只有一个slice。第二字节序问题。RTP头的sequence和timestamp在网络字节序里是大端但发送端的内存表示可能是小端。fill_rtp_header里我用手动移位而不是htons就是为了避免平台差异。SSRC字段我在拷贝时直接用了内存拷贝因为它在网络传输中本身就是无符号32位整数不需要转换。如果你用结构体强转要特别注意对齐和字节序问题。第三UDP发送缓冲区过小。如果发送端码率较高或者系统负载大UDP发送缓冲区满了之后sendto会返回EAGAIN。如果你不处理这个错误直接丢弃数据接收端就会感受到丢包。我的处理方式是稍微调大发送缓冲区int sendbuf 1024 * 1024; setsockopt(udp_fd, SOL_SOCKET, SO_SNDBUF, sendbuf, sizeof(sendbuf));同时把socket设为非阻塞加上EAGAIN重试或丢帧策略。实时流媒体场景下丢一帧比延迟堆积造成长时间卡顿更好。但丢帧也要有策略优先丢非参考帧P帧保留IDR帧否则接收端可能一直等参考帧回来反而更卡。9. 扩展怎么让这个发送器支持RTSP推流裸RTP发送只是最底层的能力实际情况中你大概率还需要配合RTSP协议让专业的播放器能通过rtsp://地址播放。RTSP其实不传数据它只负责协商协商完成后媒体数据还是通过RTP传送。最简单的做法是把FFmpeg的RTSP server模块拉进来。但如果你想自己实现一个轻量级的RTSP服务核心要点就四个步骤处理OPTIONS、DESCRIBE、SETUP、PLAY四个方法。DESCRIBE响应返回sdp包含视频流的编码参数、PT类型、sprop-parameter-sets。SETUP响应里要带上服务端选择的RTP端口和RTCP端口。PLAY响应之后服务端开始向客户端指定的IP和端口发送RTP包。其中最容易出错的是端口分配。标准RTSP要求RTP和RTCP使用连续的UDP端口RTP用偶数端口RTCP用奇数端口。有些客户端比如VLC对这个要求很严格如果RTCP端口不对它虽然能出画面但是会频繁打印RTCP超时日志。还有RTCP的SR包会携带当前RTP时间戳和对应的NTP时间用于音视频同步。如果你不做RTCPVLC通常也能播放但FFmpeg在有些情况下会等待RTCP包直到超时导致播放延迟增大。所以做RTSP推流时RTCP必须实现至少定期发SR包。我自己在实际项目中是把RTP打包部分写成独立模块RTSP协商和会话管理放在上层两者通过一个简单接口衔接。这样不管是做RTSP推流还是SRT、WebRTC通过SDP协商后走RTP底层的RTP打包逻辑都可以复用。10. 性能调优与最终建议最后聊点性能方面的心得。RTP打包是一个纯CPU和内存操作优化重点在于减少不必要的数据拷贝。我的最终实现中摄像头采集到H264数据后直接通过回调函数把指针和长度传给RTP打包器。打包器不复制整个NALU只有在FU-A分片时才把分片数据逐片拷入RTP缓冲区否则直接用原始指针构造RTP包。这样在1080p 30fps、平均码率8Mbps的场景下打包耗时可以忽略不计。另外sendto的调用频率要控制好。1080p 30fps的大I帧可能需要20个FU-A分片也就是20次sendto。这个频率对现代CPU来说没问题但如果你在ESP32这类MCU上做就要考虑减少系统调用次数或换用LwIP的raw API。调试时建议先用本地回环把链路走通再用真实网络抓包验证。不要一上来就直接对着真实设备调那样问题定位会很痛苦。把RTP发送H264这件事吃透之后你会发现RTSP推流、SRT推流、甚至自研传输协议都不再是难事。底层核心的“H264切包成RTP”逻辑是通用的剩下的只是换不同的控制协议而已。根据我个人经验有一个小技巧很值得分享写RTP打包器时不要只为了“能跑”而写建议把RTP包的内容打印出来和Wireshark抓包结果逐字节对比一次。这个过程能帮你发现大量书本上没写过的细节比如字节序、mark位、时间戳边界问题。我一开始也是凭经验写后来发现逐字节对比一次之后整个协议栈的很多概念都串联起来了后面调试其他流媒体协议也顺手很多。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →