> 技术文档 > 【音视频】RTP解包H264 代码实现_android 通用rtp解码成h264

【音视频】RTP解包H264 代码实现_android 通用rtp解码成h264


一、概述

之前实现了RTP封包H264,这里实现手动解包H264,并且将解包后的H264保存为本地文件,保存后的H264文件可以直接播放

【音视频】RTP解包H264 代码实现_android 通用rtp解码成h264

通过ffplay直接可以播放

ffplay out.h264

【音视频】RTP解包H264 代码实现_android 通用rtp解码成h264

我们这里的解包,是做的模拟,并没有监听UDP端口,然后接受RTP包,而是直接从发送端拷贝一份RTP包过来,因此我们这里并没有做UDP的乱序排序,以及丢包检测等,后续如果要做网络接收RTP报文,这些都是要优化的。

二、实现流程

读取文件
  • 这一步和之前的封包是一样的,不做讲解
FILE *bits = open_bitstream_file(\"1.h264\");//打开264文件,并将文件指针赋给bits,在此修改文件名实现打开别的264文件。if(!bits){printf(\"open file failed\\n\");return -1;}FILE *out_file = fopen(\"out.h264\", \"wb\");if(!out_file){printf(\"open out_file failed\\n\");return -1;}

open_bitstream_file函数如下

FILE *open_bitstream_file (char *filename){ FILE *bits = NULL; if (NULL == (bits=fopen(filename, \"rb\"))) { printf(\"open file error\\n\"); } return bits;}
rtp_h264_test_t结构体
  • rtp_h264_test_t这个结构体,我们现在关注的是解包部分
  • decoder_h264 为H264的解码器,主要是读取对应的H264的NAL结构
  • out_file主要是输出的h264的文件指针,用于保存RTP包中的H264内容,写到本地
struct rtp_h264_test_t{ int payload; // payload type const char* encoding; // 音频、视频的格式,比如H264, 该工程只支持264 int fd; struct sockaddr_in addr; size_t addr_size; char *in_file_name; // H264文件名 FILE* in_file;  // H264裸流文件 float frame_rate;  // 帧率 void* encoder_h264; // 封装 char *out_file_name; FILE *out_file; void* decoder_h264; // 解封装 uint8_t sps[40]; int sps_len; uint8_t pps[10]; int pps_len; int got_sps_pps;};
rtp_payload_t结构体
  • rtp_payload_t结构体主要是存储一些解包时的回调函数,把解包后的H264数据保存到本地中
struct rtp_payload_t // 用来封装回调函数的结构体{ void* (*alloc)(void* param, int bytes); void (*free)(void* param, void *packet); /// @return 0-ok, other-error 拿到一帧完整的数据 int (*packet)(void* param, const void *packet, int bytes, uint32_t timestamp, int flags);};
  • 注册这三个回调函数
struct rtp_payload_t handler_rtp_decode_h264;handler_rtp_decode_h264.alloc = rtp_alloc;handler_rtp_decode_h264.free = rtp_free;handler_rtp_decode_h264.packet = rtp_decode_packet;
  • 这里主要看看packet回调函数,了解它是怎么保存的
  • 这里绑定为rtp_decode_packet函数,是对H264和AAC通用的结构,我们这里主要看H264
  • 可以看到,我们对读取到RTP数据,提取对应的H264裸数据,因为RTP传输的H264数据都是裸数据,包括NAL头部和h264的裸数据,因此我们要手动添加startcode
  • 这里如果是FU-A分片发送的H264,在这里已经是组装好的了,因此这个回调函数处理的就是将完整的NAL单元加上startcode,然后写入文件而已
  • 至于组装NALU分片的话,不是这个回调函数实现的,具体看后面讲解
static int rtp_decode_packet(void* param, const void *packet, int bytes, uint32_t timestamp, int flags){ static const uint8_t start_code[4] = { 0, 0, 0, 1 }; struct rtp_h264_test_t* ctx = (struct rtp_h264_test_t*)param; static uint8_t buffer[2 * 1024 * 1024]; assert(bytes + 4 < sizeof(buffer)); assert(0 == flags); size_t size = 0; if (0 == strcmp(\"H264\", ctx->encoding) || 0 == strcmp(\"H265\", ctx->encoding)) { memcpy(buffer, start_code, sizeof(start_code)); size += sizeof(start_code); } else if (0 == strcasecmp(\"mpeg4-generic\", ctx->encoding)) { int len = bytes + 7; uint8_t profile = 2; uint8_t sampling_frequency_index = 4; uint8_t channel_configuration = 2; buffer[0] = 0xFF; /* 12-syncword */ buffer[1] = 0xF0 /* 12-syncword */ | (0 << 3)/*1-ID*/ | (0x00 << 2) /*2-layer*/ | 0x01 /*1-protection_absent*/; buffer[2] = ((profile - 1) << 6) | ((sampling_frequency_index & 0x0F) << 2) | ((channel_configuration >> 2) & 0x01); buffer[3] = ((channel_configuration & 0x03) << 6) | ((len >> 11) & 0x03); /*0-original_copy*/ /*0-home*/ /*0-copyright_identification_bit*/ /*0-copyright_identification_start*/ buffer[4] = (uint8_t)(len >> 3); buffer[5] = ((len & 0x07) << 5) | 0x1F; buffer[6] = 0xFC | ((len / 1024) & 0x03); size = 7; } memcpy(buffer + size, packet, bytes); size += bytes; printf(\"nalu get -> bytes:%d, timestamp:%u\\n\", size, timestamp); // TODO: // check media file fwrite(buffer, 1, size, ctx->out_file);}
rtp_payload_decode_input函数
int rtp_payload_decode_input(void* decoder, const void* packet, int bytes){ struct rtp_payload_delegate_t* ctx; ctx = (struct rtp_payload_delegate_t*)decoder; return ctx->decoder->input(ctx->packer, packet, bytes);}
  • 这个函数主要是调用代理类的解码器,然后进行解码操作的,具体看下面的rtp_payload_delegate_t结构体
struct rtp_payload_delegate_t  // 代理结构体{ struct rtp_payload_encode_t* encoder; struct rtp_payload_decode_t* decoder; void* packer;};
  • 因为我们是做的模拟解包,所以直接在编码后直接将编码好的RTP解包(不通过网络传输)
// 拿到一帧RTP序列化后的数据static int rtp_encode_packet(void* param, const void *packet, int bytes, uint32_t timestamp, int flags){ struct rtp_h264_test_t* ctx = (struct rtp_h264_test_t*)param; int ret = 0; ret = sendto( ctx->fd, (void*)packet, bytes, 0, (struct sockaddr*)&ctx->addr, ctx->addr_size); if (ret == -1) { #ifdef WIN32 printf(\"sendto failed: %d\\n\", WSAGetLastError()); #else perror(\"sendto failed\"); #endif return -1; } uint8_t *nalu = (uint8_t *)packet; printf(\"rtp send packet -> nalu_type:%d,0x%02x,0x%02x, bytes:%d, timestamp:%u\\n\",  nalu[12]&0x1f, nalu[12], nalu[13], bytes, timestamp); ret = rtp_payload_decode_input(ctx->decoder_h264, packet, bytes); // 解封装 return 0;}
rtp_payload_decode_t结构体
struct rtp_payload_decode_t{ void* (*create)(struct rtp_payload_t *handler, void* param); void (*destroy)(void* packer); /// RTP packet to PS/H.264 Elementary Stream /// @param[in] decoder RTP packet unpackers /// @param[in] packet RTP packet /// @param[in] bytes RTP packet length in bytes /// @param[in] time stream UTC time /// @return 1-packet handled, 0-packet discard, <0-failed int (*input)(void* decoder, const void* packet, int bytes);};
  • 这个结构体才是处理解包的具体函数,负责处理RTP包的,然后解包成H264包,给刚才的回调函数使用的
  • 具体看的是input回调函数,绑定的回调函数是rtp_h264_unpack_input函数
struct rtp_payload_decode_t *rtp_h264_decode(){ static struct rtp_payload_decode_t unpacker = { rtp_h264_unpack_create, rtp_h264_unpack_destroy, rtp_h264_unpack_input, }; return &unpacker;}
rtp_h264_unpack_input函数
  • 这个函数是解包的重点,下面看一下它的实现
static int rtp_h264_unpack_input(void* p, const void* packet, int bytes){ int r; uint8_t nalt; struct rtp_packet_t pkt; struct rtp_decode_h264_t *unpacker; unpacker = (struct rtp_decode_h264_t *)p; // 反序列化 if(!unpacker || 0 != rtp_packet_deserialize(&pkt, packet, bytes) || pkt.payloadlen < 1) return -EINVAL; if (-1 == unpacker->flags) { unpacker->flags = 0; unpacker->seq = (uint16_t)(pkt.rtp.seq - 1); // disable packet lost } if ((uint16_t)pkt.rtp.seq != (uint16_t)(unpacker->seq + 1)) { unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST; unpacker->size = 0; // discard previous packets } unpacker->seq = (uint16_t)pkt.rtp.seq; nalt = ((unsigned char *)pkt.payload)[0]; switch(nalt & 0x1F) { case 0: // reserved case 31: // reserved // 这里最好是报错 然后返回错误值 assert(0); return 0; // packet discard case 24: // STAP-A return rtp_h264_unpack_stap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 0); case 25: // STAP-B return rtp_h264_unpack_stap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 1); case 26: // MTAP16 return rtp_h264_unpack_mtap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 2); case 27: // MTAP24 return rtp_h264_unpack_mtap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 3); case 28: // FU-A return rtp_h264_unpack_fu(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 0); case 29: // FU-B return rtp_h264_unpack_fu(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 1); default: // 1-23 NAL unit r = unpacker->handler.packet(unpacker->cbparam, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, unpacker->flags); unpacker->flags = 0; unpacker->size = 0; return 0 == r ? 1 : r; // packet handled }}
  • 我们做的就是先读取出这个RTP报文的头部,反序列化出来
unpacker = (struct rtp_decode_h264_t *)p;// 反序列化if(!unpacker || 0 != rtp_packet_deserialize(&pkt, packet, bytes) || pkt.payloadlen < 1)return -EINVAL;
  • 反序列化函数rtp_packet_deserialize如下:
int rtp_packet_deserialize(struct rtp_packet_t *pkt, const void* data, int bytes){ uint32_t i, v; int hdrlen; const uint8_t *ptr; if (bytes < RTP_FIXED_HEADER) // RFC3550 5.1 RTP Fixed Header Fields(p12) return -1; ptr = (const unsigned char *)data; memset(pkt, 0, sizeof(struct rtp_packet_t)); // pkt header v = nbo_r32(ptr); pkt->rtp.v = RTP_V(v); pkt->rtp.p = RTP_P(v); pkt->rtp.x = RTP_X(v); pkt->rtp.cc = RTP_CC(v); pkt->rtp.m = RTP_M(v); pkt->rtp.pt = RTP_PT(v); pkt->rtp.seq = RTP_SEQ(v); pkt->rtp.timestamp = nbo_r32(ptr + 4); pkt->rtp.ssrc = nbo_r32(ptr + 8); assert(RTP_VERSION == pkt->rtp.v); // 调试的时候用 hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4; // 解析带csrc时的总长度 if (RTP_VERSION != pkt->rtp.v || bytes < hdrlen + (pkt->rtp.x ? 4 : 0) + (pkt->rtp.p ? 1 : 0)) return -1; // 报错 // pkt contributing source for (i = 0; i < pkt->rtp.cc; i++) { pkt->csrc[i] = nbo_r32(ptr + 12 + i * 4); } assert(bytes >= hdrlen); pkt->payload = (uint8_t*)ptr + hdrlen; // 跳过头部 拿到payload pkt->payloadlen = bytes - hdrlen;  // payload长度 // pkt header extension if (1 == pkt->rtp.x) { const uint8_t *rtpext = ptr + hdrlen; assert(pkt->payloadlen >= 4); pkt->extension = rtpext + 4; pkt->reserved = nbo_r16(rtpext); pkt->extlen = nbo_r16(rtpext + 2) * 4; if (pkt->extlen + 4 > pkt->payloadlen) { assert(0); return -1; } else { pkt->payload = rtpext + pkt->extlen + 4; pkt->payloadlen -= pkt->extlen + 4; } } // padding if (1 == pkt->rtp.p) { uint8_t padding = ptr[bytes - 1]; if (pkt->payloadlen < padding) { assert(0); return -1; } else { pkt->payloadlen -= padding; } } return 0;}

回顾一下RTP的报文格式:

  • 这是它的报文头部

在这里插入图片描述

  • 那么我们就可以根据这个解包出RTP报文的头部了,这个是固定的
  • 如果有扩展位,我们还需要解析扩展字节

详细的解析如下

一、整体流程概述

  1. 参数校验:确保输入数据长度不小于 RTP 固定头部长度(12 字节)。
  2. 解析固定头部:提取版本号、填充标志、扩展标志、CSRC 计数器等核心字段。
  3. 处理 CSRC 列表:根据 CC 值解析 0-15 个贡献源标识符。
  4. 处理头部扩展(如果存在):解析扩展头的长度和内容。
  5. 处理填充数据(如果存在):移除填充字节,计算真实负载长度。
  6. 定位负载位置:设置负载指针和长度。

二、关键数据结构与协议字段

RTP 固定头部格式(RFC 3550):
 0  1  2  3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|V=2|P|X| CC |M| PT | sequence number |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| timestamp |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|  synchronization source (SSRC) identifier |+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+| contributing source (CSRC) identifiers || .... |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • V:版本号(固定为 2)
  • P:填充标志(是否包含填充字节)
  • X:扩展标志(是否包含头部扩展)
  • CC:CSRC 计数器(0-15 个贡献源)
  • M:标记位(由应用层定义)
  • PT:负载类型(如 H.264=96,AAC=97 等)
  • Sequence Number:序列号(用于排序和丢包检测)
  • Timestamp:时间戳(用于同步)
  • SSRC:同步源标识符(标识媒体流源)

三、代码详细解析

1. 参数校验与初始化
if (bytes < RTP_FIXED_HEADER) // RTP_FIXED_HEADER = 12 return -1;ptr = (const unsigned char *)data;memset(pkt, 0, sizeof(struct rtp_packet_t));
  • 检查数据长度是否至少包含 RTP 固定头部(12 字节)。
  • 初始化输出结构体pkt
2. 解析固定头部字段
v = nbo_r32(ptr); // 读取前4字节(网络字节序转主机序)pkt->rtp.v = RTP_V(v); // 版本号 (2 bits)pkt->rtp.p = RTP_P(v); // 填充标志 (1 bit)pkt->rtp.x = RTP_X(v); // 扩展标志 (1 bit)pkt->rtp.cc = RTP_CC(v); // CSRC计数器 (4 bits)pkt->rtp.m = RTP_M(v); // 标记位 (1 bit)pkt->rtp.pt = RTP_PT(v); // 负载类型 (7 bits)pkt->rtp.seq = RTP_SEQ(v); // 序列号 (16 bits)pkt->rtp.timestamp = nbo_r32(ptr + 4); // 时间戳pkt->rtp.ssrc = nbo_r32(ptr + 8); // SSRC
  • 使用位运算提取各字段(例如,RTP_V(v)可能是(v >> 30) & 0x3)。
  • 验证版本号是否为 2(RTP 标准版本)。
3. 计算头部长度并校验
hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4; // 基础长度 + CSRC列表长度if (RTP_VERSION != pkt->rtp.v || bytes < hdrlen + ...) return -1;
  • 每个 CSRC 占 4 字节,总头部长度为12 + CC*4
  • 检查数据长度是否足够包含完整头部。
4. 解析 CSRC 列表
for (i = 0; i < pkt->rtp.cc; i++){ pkt->csrc[i] = nbo_r32(ptr + 12 + i * 4);}
  • CSRC 列表紧跟在固定头部之后,每个 CSRC 为 32 位(4 字节)。
5. 定位负载位置
pkt->payload = (uint8_t*)ptr + hdrlen;pkt->payloadlen = bytes - hdrlen;
  • 初始负载位置为头部之后的字节。
  • 初始负载长度为总长度减去头部长度。
6. 处理头部扩展(如果存在)
if (1 == pkt->rtp.x){ const uint8_t *rtpext = ptr + hdrlen; pkt->extension = rtpext + 4; pkt->reserved = nbo_r16(rtpext); // 保留字段 pkt->extlen = nbo_r16(rtpext + 2) * 4; // 扩展长度(以32位为单位) pkt->payload = rtpext + pkt->extlen + 4; pkt->payloadlen -= pkt->extlen + 4;}
  • 扩展头格式:
    • 16 位保留字段
    • 16 位扩展长度(表示 32 位字的数量,需乘以 4 得到字节数)
    • 扩展内容
7. 处理填充数据(如果存在)
if (1 == pkt->rtp.p){ uint8_t padding = ptr[bytes - 1]; pkt->payloadlen -= padding;}
  • 填充字节的最后一个字节表示填充长度(包括自身)。
  • 从负载长度中减去填充字节数。

然后我们需要根据发送的RTP报文的类型,来进行对应的H264解析,比如SPS、PPS可能就是直接发送一整个RTP包出去,而I帧可能就是通过FU-A的方式分片发送,如题类型参考下面:

Nalu_Type NALU 内容描述 备注 0 未指定 - 1 非 IDR 图像编码的 slice(如 P、B 帧) 普通帧数据,非关键帧(I 帧不一定是 IDR 帧,IDR 一定是 I 帧) 2 编码 slice 数据划分 A 传输片中重要信息(如片头、宏块预测模式,较少使用) 3 编码 slice 数据划分 B 仅传输残差(较少使用) 4 编码 slice 数据划分 C 仅传输残差中的 AC 系数(较少使用) 5 IDR 图像中的编码 slice(IDR 帧) 关键帧,解码器遇到 IDR 帧会重置参考帧列表 6 SEI 补充增强信息单元 存储私有数据(如时间戳、水印、HDR 元数据) 7 SPS 序列参数集 全局编码参数(分辨率、Profile、帧率等,解码器初始化必需) 8 PPS 图像参数集 帧级编码参数(熵编码模式、去块滤波等,与 SPS 配合使用) 9 接入单元定界符 - 10 序列结束 标记序列结束(较少使用) 11 码流结束 标记码流结束(较少使用) 12 填充数据 用于对齐字节(如确保 RTP 包长度为 4 字节整数倍) 24 STAP-A(单一时间聚合包模式) 单个 RTP 包传输多个同时间戳的 NALU(编码时间相同) 25 STAP-B(单一时间聚合包模式,含 DON) 同 STAP-A,多一个解码顺序号(DON) 26 MTAP 16(多个时间聚合包模式) 单个 RTP 包传输多个不同时间戳的 NALU(编码时间可不同,时间戳精度 16 位) 27 MTAP 24(多个时间聚合包模式) 同 MTAP 16,时间戳精度 24 位 28 FU-A(分片模式,首片 / 中间片) 当 NALU 过大时,分片传输(首片 S=1,中间片 S=0/E=0,尾片 E=1) 29 FU-B(分片模式,首片 / 中间片,含 DON) 同 FU-A,多 DON 字段 30-31 未指定,保留 -

代码实现如下:

nalt = ((unsigned char *)pkt.payload)[0];switch(nalt & 0x1F){case 0: // reservedcase 31: // reserved// 这里最好是报错 然后返回错误值assert(0);return 0; // packet discardcase 24: // STAP-Areturn rtp_h264_unpack_stap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 0);case 25: // STAP-Breturn rtp_h264_unpack_stap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 1);case 26: // MTAP16return rtp_h264_unpack_mtap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 2);case 27: // MTAP24return rtp_h264_unpack_mtap(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 3);case 28: // FU-Areturn rtp_h264_unpack_fu(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 0);case 29: // FU-Breturn rtp_h264_unpack_fu(unpacker, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, 1);default: // 1-23 NAL unitr = unpacker->handler.packet(unpacker->cbparam, (const uint8_t*)pkt.payload, pkt.payloadlen, pkt.rtp.timestamp, unpacker->flags);unpacker->flags = 0;unpacker->size = 0;return 0 == r ? 1 : r; // packet handled}
  • 如果是1-23的NALU类型,那么我们直接调用之前说的回调函数,加入startcode之后,然后保存H264即可,这里NALU的头部和H264的数据直接从报文就可以读出了

  • 如果是FU-A类型的,我们需要进行NALU的组包,看rtp_h264_unpack_fu函数,如下

static int rtp_h264_unpack_fu(struct rtp_decode_h264_t *unpacker, const uint8_t* ptr, int bytes, uint32_t timestamp, int fu_b){ int r, n; uint8_t fuheader; //uint16_t don; r = 0; n = fu_b ? 4 : 2; // payload前面有几个字节是供fu_a或fu_b使用, fu_a只有2个字节; fu_b是4个字节 if (bytes < n || unpacker->size + bytes - n > RTP_PAYLOAD_MAX_SIZE) { assert(0); return -EINVAL; // error } if (unpacker->size + bytes - n + 1 /*NALU*/ > unpacker->capacity) { void* p = NULL; int size = unpacker->size + bytes + 1; size += size / 4 > 128000 ? size / 4 : 128000; p = realloc(unpacker->ptr, size); if (!p) { // set packet lost flag unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST; unpacker->size = 0; return -ENOMEM; // error } unpacker->ptr = (uint8_t*)p; unpacker->capacity = size; } fuheader = ptr[1]; //don = nbo_r16(ptr + 2); if (FU_START(fuheader)) {#if 0 if (unpacker->size > 0) { unpacker->flags |= RTP_PAYLOAD_FLAG_PACKET_CORRUPT; unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, unpacker->timestamp, unpacker->flags); unpacker->flags = 0; unpacker->size = 0; // reset }#endif unpacker->size = 1; // NAL unit type byte unpacker->ptr[0] = (ptr[0]/*indicator*/ & 0xE0) | (fuheader & 0x1F); assert(H264_NAL(unpacker->ptr[0]) > 0 && H264_NAL(unpacker->ptr[0]) < 24); } else { if (0 == unpacker->size) { unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST; return 0; // packet discard } assert(unpacker->size > 0); } unpacker->timestamp = timestamp; if (bytes > n) { assert(unpacker->capacity >= unpacker->size + bytes - n); memmove(unpacker->ptr + unpacker->size, ptr + n, bytes - n); unpacker->size += bytes - n; } if(FU_END(fuheader)) { if(unpacker->size > 0) // 多次传入数据后等到FU_END的时候难道一个完整的nalu r = unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, timestamp, unpacker->flags); unpacker->flags = 0; unpacker->size = 0; // reset } return 0 == r ? 1 : r; // packet handled}

回顾FU-A的RTP报文格式

  • 前面的RTP报文都是一样的,只是负载的部分头部不是直接的NALU头部,而是使用FU indication + FU-header两部分
  • 整体结构变成了FU indication + FU-header + H264的NALU分片裸数据,这三部分组成
  • 具体的负载头部报文格式如下

FU indication

+---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|F|NRI| Type |+---------------+
  • 这⾥⾯的的F和NRI已经在NALU的Header解释清楚了,就是NALU头的前⾯三个bit位,后⾯的TYPE就是NALU的FU-A类型28,这样在RTP固定头后⾯第⼀字节的后⾯5bit提取出来就确认了该RTP包承载的不是⼀个完整的NALU,是其⼀部分。

  • 那么问题来了,⼀个NALU切分成多个RTP包传输,那么到底从哪⼉开始哪⼉结束呢?可能有⼈说RTP包固定头不是有mark标记么,注意区分那个是以帧图像的结束标记,这⾥要确定是NALU结束的标记,其次NALU的类型呢?那么就需要RTP固定12字节后⾯的Fu Header来进⾏区分。

FU header

+---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|S|E|R| Type |+---------------+

字段解释:

  • S: 1 bit 当设置成1,开始位指示分⽚NAL单元的开始。当跟随的FU荷载不是分⽚NAL单元荷载的开始,开始位设为0。
  • E: 1 bit 当设置成1, 结束位指示分⽚NAL单元的结束,即, 荷载的最后字节也是分⽚NAL单元的最后⼀个字节,当跟随的FU荷载不是分⽚NAL单元的最后分⽚,结束位设置为0。也就是说⼀个NALU切⽚时,第⼀个切⽚的SE是10,然后中间的切⽚是00,最后⼀个切⽚时11。
  • R: 1 bit保留位必须设置为0,接收者必须忽略该位。
  • Type: 5 bits此处的Type就是NALU头中的Type,取1-23的那个值,表示 NAL单元荷载类型定义

  • 我们假定RTP是按顺序发来的,并且不会丢包,因此我们直接判断FU Header的标志位
fuheader = ptr[1];
  • 如果标志位S为1,说明是一个NALU的开始
if (FU_START(fuheader)) {#if 0 if (unpacker->size > 0) { unpacker->flags |= RTP_PAYLOAD_FLAG_PACKET_CORRUPT; unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, unpacker->timestamp, unpacker->flags); unpacker->flags = 0; unpacker->size = 0; // reset }#endif unpacker->size = 1; // NAL unit type byte unpacker->ptr[0] = (ptr[0]/*indicator*/ & 0xE0) | (fuheader & 0x1F); assert(H264_NAL(unpacker->ptr[0]) > 0 && H264_NAL(unpacker->ptr[0]) < 24); }
  • 如果标志位E为1,说明是一个NALU的结束,这是我们组包结束,然后直接使用上层回调函数保存为H264到本地文件
if(FU_END(fuheader)) { if(unpacker->size > 0) // 多次传入数据后等到FU_END的时候难道一个完整的nalu r = unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, timestamp, unpacker->flags); unpacker->flags = 0; unpacker->size = 0; // reset }
  • 如果这两个标志位为0,那我们就直接组包就好,将数据拼到缓冲区后面
if (bytes > n){assert(unpacker->capacity >= unpacker->size + bytes - n);memmove(unpacker->ptr + unpacker->size, ptr + n, bytes - n);unpacker->size += bytes - n;}

更多资料:https://github.com/0voice