本文来分析webrtc打包h264 rtp包的代码,版本m98
一、RTP协议
1.1 RTP协议概述
- 实时传输协议(RTP)是一个网络协议,它允许在网络上进行实时的音频和视频数据传输。RTP协议主要用于解决多媒体数据的实时传输问题,特别是对延迟和数据丢失敏感的应用。
- RTP协议包括两个紧密相关的部分:RTP数据协议和RTP控制协议(RTCP)。RTP数据协议负责数据的传输,RTCP负责监控服务质量并提供同步和标识信息。
- RTP协议并不保证数据的可靠传输,因为在实时应用中,比起保证数据的完整性,降低延迟和抖动更为重要。因此,RTP通常运行在不可靠的传输协议(如UDP)之上。
1.2 RTP协议的主要特点
- 实时性:RTP协议能够处理实时数据的传输,包括对延迟敏感的通信,如视频会议和语音通话。
- 多播传输:RTP支持多播,可以将数据同时发送给多个接收者。
- 提供序列号:RTP为每个数据包提供序列号,以便接收者可以根据序列号重新组合数据。
- 提供时间戳:RTP为每个数据包提供时间戳,以便接收者可以同步播放数据。
- 提供有效载荷标识:RTP可以识别不同类型的有效载荷(如音频、视频等)。
1.3 RTP协议头结构
其中:
- V(版本号):这是一个2位的字段,用于标识RTP的版本。当前的版本是2。
- P(填充):如果该位被设置,那么RTP头部的末尾将包含一些填充字节,这些字节不包含在包的长度中。
- X(扩展):如果该位被设置,那么RTP头部将包含一个扩展头部。头部扩展可以用于存储信息,比如视频旋转角度。
- CC(CSRC计数):这是一个4位的字段,表示CSRC标识符的数量。
- M(标记):这是一个1位的字段,由具体的有效载荷格式定义其含义。例如,在音频应用中,如果M位被设置,那么这个包通常包含了一个时间段的开始。
- PT(有效载荷类型):这是一个7位的字段,用于标识RTP包中的有效载荷数据的类型。
- 序列号:这是一个16位的字段,用于标识发送者发送的RTP包的序列号。
- 时间戳:这是一个32位的字段,用于标识数据的采样实例。
- SSRC(同步源):这是一个32位的字段,用于标识RTP数据包的源。
- CSRC(贡献源):这是一个可选的字段,用于标识对于复合包或混合音频数据的贡献源,主要用于MCU 混流。
- Extension header(扩展头): 如果X被设置,RTP头的尾部会包含扩展头信息。
二、RTP打包H.264
2.1 H.264 NALU
H.264将视频数据划分为一系列的网络抽象层单元(NALU)。每个NALU都包含了一部分视频数据,并且具有自己的头部结构。每个NALU由一个NALU Header和RBSP组成。下图为NALU Header。
- F(禁止位):占用1位,用于指示NALU是否被禁止。
- NRI(重要性指示):占用2位,用于示指NAL的U重要性。较高的重要性值表示NALU包含了重要的视频数据。
- Type(类型):占用5位,用于指示NALU的类型。不同的类型对应不同的视频数据,如关键帧非、关键帧、SPS(序列参数集)等。常见的h.264naul 类型有:
0:未使用
1:非关键帧(P帧)
2:关键帧(I帧)
3:SPS(序列参数集)
4:PPS(图像参数集)
5:分片层(Slice Layer)
6:扩展NALU
7:扩展NALU
8:扩展NALU
9-23:保留
24-31:未使用
2.2 RTP载荷结构
RTP载荷分为三种不同的载荷结构:
-
单一NALU单元结构(Single NAL Unit Packet):包含一个单一的NALU,此时Type与H.264NALU保持一致。
在单一NALU打包的情况下,整个H.264 NALU被打包在一个RTP包中。这种方式适用于NALU的大小不超过RTP包的最大大小限制的情况。 -
组合封包结构(Aggregation Packet):包含多个NALU。
此时Type值应该按下表设置
webrtc中组包只支持STAP-A(Single-Time Aggregation Packet)结构。顾名思义,STAP-A是指组合成RTP包的所有NALU共享相同的时间戳。
下图为两个NALU组合成一个RTP包的完整RTP包结构示例:
-
分片结构(Fragmentation Unit):当一个H.264 NALU比较大时,为了网络传输,可以将一个NALU拆分到多个RTP中进行传输。webrtc支持FU-A的分片结构,如果所示:
FU Indicator结构与H.264 NALU头结构相同,如果所示。其中F、NRI与H.264 NALU一致,Type的值为28\29,分别对应FU-A和FU-B。
FU header结构如图所示:
其中:
S代表Start bit,如果设置为1,代表该RTP包为FU的第一个包;
E代表End bit,如果设置为1,代表该RTP包为FU的第最后一个包;
R代表Reserverd bit,保留位,必须设置为0;
Type与H.264 NALU头保持一致。
2.3 打包模式
RTP规定的打包模式有三种,分别为单一NALU模式(Single NAL unit mode)、非交替模式(Non-interleaved mode)和交替模式(Interleaved mode)。webrtc支持Single NAL unit mode和Non-interleaved mode。三种打包模式与RTP载荷结构的对应关系如图所示:
三、webrtc 打包h264流程
在webrtc源码阅读之视频采集、编码、发送中,我们分析到了RTPSenderVideo::SendVideo
,在这个函数中,会调用RtpPacketizer::Create
对视频数据进行RTP打包,并通过RtpPacketizerH264::NextPacket
将数据转换为真正的RTP包。
std::unique_ptr<RtpPacketizer> packetizer =
RtpPacketizer::Create(codec_type, payload, limits, video_header);
.......
if (!packetizer->NextPacket(packet.get()))
return false;
我们以视频编码格式为H.264进行分析。
RtpPacketizerH264::RtpPacketizerH264(rtc::ArrayView<const uint8_t> payload,
PayloadSizeLimits limits,
H264PacketizationMode packetization_mode)
: limits_(limits), num_packets_left_(0) {
// Guard against uninitialized memory in packetization_mode.
RTC_CHECK(packetization_mode == H264PacketizationMode::NonInterleaved ||
packetization_mode == H264PacketizationMode::SingleNalUnit);
//对H264打包时,需要去除H264码流中的StartCode,并以NALU为单位进行打包
//根据H264码流格式,通过StartCode,区分不同NALU,每个input_fragments_元素为一个H264 NALU
for (const auto& nalu :
H264::FindNaluIndices(payload.data(), payload.size())) {
input_fragments_.push_back(
payload.subview(nalu.payload_start_offset, nalu.payload_size));
}
if (!GeneratePackets(packetization_mode)) { //打包为RTP包
// If failed to generate all the packets, discard already generated
// packets in case the caller would ignore return value and still try to
// call NextPacket().
num_packets_left_ = 0;
while (!packets_.empty()) {
packets_.pop();
}
}
}
3.1 解析NALU
std::vector<NaluIndex> FindNaluIndices(const uint8_t* buffer,
size_t buffer_size) {
//H264的StratCode有两种:0x00 0x00 0x01或 0x00 0x00 0x00 0x01,跟据StartCode来区分不同NALU
std::vector<NaluIndex> sequences;
if (buffer_size < kNaluShortStartSequenceSize)
return sequences;
static_assert(kNaluShortStartSequenceSize >= 2,
"kNaluShortStartSequenceSize must be larger or equals to 2");
const size_t end = buffer_size - kNaluShortStartSequenceSize;
for (size_t i = 0; i < end;) {
if (buffer[i + 2] > 1) {
i += 3;
} else if (buffer[i + 2] == 1) {
if (buffer[i + 1] == 0 && buffer[i] == 0) {
// We found a start sequence, now check if it was a 3 of 4 byte one.
NaluIndex index = {i, i + 3, 0};
if (index.start_offset > 0 && buffer[index.start_offset - 1] == 0)
--index.start_offset;
// Update length of previous entry.
auto it = sequences.rbegin();
if (it != sequences.rend())
it->payload_size = index.start_offset - it->payload_start_offset;
sequences.push_back(index);
}
i += 3;
} else {
++i;
}
}
// Update length of last entry, if any.
auto it = sequences.rbegin();
if (it != sequences.rend())
it->payload_size = buffer_size - it->payload_start_offset;
return sequences;
}
H.264原始码流的每个NALU前都会有一个StartCode(起始码)。起始码有两种格式0x00 0x00 0x01或 0x00 0x00 0x00 0x01,按这个准则,找到数据中的起始码,并以起始码的位置分割,就找到了NALU的分界。
3.2 打包单一NALU
bool RtpPacketizerH264::PacketizeSingleNalu(size_t fragment_index) {
// Add a single NALU to the queue, no aggregation.
size_t payload_size_left = limits_.max_payload_len;
if (input_fragments_.size() == 1)
payload_size_left -= limits_.single_packet_reduction_len;
else if (fragment_index == 0)
payload_size_left -= limits_.first_packet_reduction_len;
else if (fragment_index + 1 == input_fragments_.size())
payload_size_left -= limits_.last_packet_reduction_len;
rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];
//比较可承载负载大小与NALU大小,若NALU太大则SingleNalu模式不能打包整个NALU
if (payload_size_left < fragment.size()) {
RTC_LOG(LS_ERROR) << "Failed to fit a fragment to packet in SingleNalu "
"packetization mode. Payload size left "
<< payload_size_left << ", fragment length "
<< fragment.size() << ", packet capacity "
<< limits_.max_payload_len;
return false;
}
RTC_CHECK_GT(fragment.size(), 0u);
packets_.push(PacketUnit(fragment, true /* first */, true /* last */,
false /* aggregated */, fragment[0]));
++num_packets_left_;
return true;
}
只有在强制选择SingleNalUnit模式时才会选择这种打包方式,webrtc一般选择的是Non-interleaved mode。
3.3 打包FU-A
bool RtpPacketizerH264::PacketizeFuA(size_t fragment_index) {
// Fragment payload into packets (FU-A).
rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];
PayloadSizeLimits limits = limits_;
// Leave room for the FU-A header.
limits.max_payload_len -= kFuAHeaderSize;
// Update single/first/last packet reductions unless it is single/first/last
// fragment.
if (input_fragments_.size() != 1) {
// if this fragment is put into a single packet, it might still be the
// first or the last packet in the whole sequence of packets.
if (fragment_index == input_fragments_.size() - 1) { //最后一包
limits.single_packet_reduction_len = limits_.last_packet_reduction_len;
} else if (fragment_index == 0) { //第一包
limits.single_packet_reduction_len = limits_.first_packet_reduction_len;
} else {
limits.single_packet_reduction_len = 0;
}
}
if (fragment_index != 0) //非第一包,设置第一包标志为0
limits.first_packet_reduction_len = 0;
if (fragment_index != input_fragments_.size() - 1) //非最后一包,设置最后一包标志位0
limits.last_packet_reduction_len = 0;
// Strip out the original header.
size_t payload_left = fragment.size() - kNalHeaderSize;
int offset = kNalHeaderSize;
//按FU-A分包的大小,计算每个FU的载荷大小
std::vector<int> payload_sizes = SplitAboutEqually(payload_left, limits);
if (payload_sizes.empty())
return false;
for (size_t i = 0; i < payload_sizes.size(); ++i) { //根据计算出来的FU包的载荷大小进行分包
int packet_length = payload_sizes[i];
RTC_CHECK_GT(packet_length, 0);
packets_.push(PacketUnit(fragment.subview(offset, packet_length),
/*first_fragment=*/i == 0,
/*last_fragment=*/i == payload_sizes.size() - 1,
false, fragment[0]));
offset += packet_length;
payload_left -= packet_length;
}
num_packets_left_ += payload_sizes.size();
RTC_CHECK_EQ(0, payload_left);
return true;
}
这是webrtc中h264最常见的RTP打包方式。根据FU-A的打包规则进行打包。
3.4 打包STAP-A
size_t RtpPacketizerH264::PacketizeStapA(size_t fragment_index) {
// Aggregate fragments into one packet (STAP-A).
size_t payload_size_left = limits_.max_payload_len;
if (input_fragments_.size() == 1)
payload_size_left -= limits_.single_packet_reduction_len;
else if (fragment_index == 0)
payload_size_left -= limits_.first_packet_reduction_len;
int aggregated_fragments = 0;
size_t fragment_headers_length = 0;
rtc::ArrayView<const uint8_t> fragment = input_fragments_[fragment_index];
RTC_CHECK_GE(payload_size_left, fragment.size());
++num_packets_left_;
//计算出stap-a中每个nalu所需要的空间
auto payload_size_needed = [&] {
size_t fragment_size = fragment.size() + fragment_headers_length;
if (input_fragments_.size() == 1) {
// Single fragment, single packet, payload_size_left already adjusted
// with limits_.single_packet_reduction_len.
return fragment_size;
}
if (fragment_index == input_fragments_.size() - 1) {
// Last fragment, so STAP-A might be the last packet.
return fragment_size + limits_.last_packet_reduction_len;
}
return fragment_size;
};
while (payload_size_left >= payload_size_needed()) {
RTC_CHECK_GT(fragment.size(), 0);
packets_.push(PacketUnit(fragment, aggregated_fragments == 0, false, true,
fragment[0]));
payload_size_left -= fragment.size();
payload_size_left -= fragment_headers_length;
fragment_headers_length = kLengthFieldSize;
// If we are going to try to aggregate more fragments into this packet
// we need to add the STAP-A NALU header and a length field for the first
// NALU of this packet.
if (aggregated_fragments == 0)
fragment_headers_length += kNalHeaderSize + kLengthFieldSize;
++aggregated_fragments;
// Next fragment.
++fragment_index;
if (fragment_index == input_fragments_.size())
break;
fragment = input_fragments_[fragment_index];
}
RTC_CHECK_GT(aggregated_fragments, 0);
packets_.back().last_fragment = true;
return fragment_index;
}
3.5 NextPacket
bool RtpPacketizerH264::NextPacket(RtpPacketToSend* rtp_packet) {
RTC_DCHECK(rtp_packet);
if (packets_.empty()) {
return false;
}
PacketUnit packet = packets_.front();
if (packet.first_fragment && packet.last_fragment) { //如果一个包既是第一包又是最后一包,则说明这是Single NAL unit包
// Single NAL unit packet.
size_t bytes_to_send = packet.source_fragment.size();
uint8_t* buffer = rtp_packet->AllocatePayload(bytes_to_send);
memcpy(buffer, packet.source_fragment.data(), bytes_to_send);
packets_.pop();
input_fragments_.pop_front();
} else if (packet.aggregated) { //组合包 stap-a
NextAggregatePacket(rtp_packet);
} else { //分片模式 fu-a
NextFragmentPacket(rtp_packet);
}
rtp_packet->SetMarker(packets_.empty()); //如果是最后一包,则置marker位
--num_packets_left_;
return true;
}
根据打包类型,调用不同函数。
- 如果是Single NAL unit packet,则按Single NALU规则直接打进RTP包里。
- 如果是组合包,则调用NextAggregatePacket进行封装,封装规则与2.2中组合封包结构(Aggregation Packet)一致。
- 如果是组合包,则调用NextFragmentPacket进行封装,封装规则与2.2中分片结构(Fragmentation Unit)一致。
- 如果是最后一包,会把RTP头的marker位置1,用来代表帧结束标志。
至此,webrtc将H.264打包位RTP包的过程已经分析完毕。
参考资料
RTP: A Transport Protocol for Real-Time Applications(RFC3550)
RTP Payload Format for H.264 Video(RFC6184)