本文介绍Rtp如何封装vp8遍码,webrtc中,常用编码格式vp8,协议规范在rfc7741中。本文内容:
- rtp如何封装vp8
- 代码实现
作者:音视频小话
原文:https://mp.weixin.qq.com/s/C6JDE4M78FT0LBNK4dXdAg
1. Rtp封装vp8
rtp封装vp8,对比H264,比较简单。没有H264那种3个封装方式,只需要关注M标志位,vp8 payload descriptor头,和VP8 Payload Header。总体的封装如下:
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 |
| .... |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| VP8 payload descriptor (integer #octets) |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| : VP8 payload header (3 octets) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| VP8 pyld hdr : |
+-+-+-+-+-+-+-+-+ |
: Octets 4..N of VP8 payload :
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| : OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
如上在Rtp Header后,紧跟着Vp8 payload descriptor,其后又跟着vp8 payload Header。
- Rtp Header中M bit位:必须为每个编码帧的最后一个数据包设置,以符合视频格式中 M 位的正常使用。这使解码器能够完成图片解码,否则可能需要等待下一个数据包才能明确知道,该帧已完成。一句话,M bit位表示是否为最后一个包,如果遇到报文带有M=1,表示可以组包上送了。
- Vp8 payload descriptor:这个就是主要是vp8帧的信息。
- VP8 Payload Header:内有关键信息,该帧是否为关键帧。应用举例,该报文是否为keyframe。为什么需要确认是否为keyframe?因为如果出现丢包,需要有等待下一个keyframe流程,否则继续上送非keyframe帧,会出现解码错误,播放渲染会出现花屏。等到新的keyframe来后,再上送。
1.1 Vp8 payload descriptor
RTP Header之后的第一个八位字节是 VP8 有效负载描述符。
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|X|R|N|S|R| PID | (REQUIRED) |X|R|N|S|R| PID | (REQUIRED)
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
X: |I|L|T|K| RSV | (OPTIONAL) X: |I|L|T|K| RSV | (OPTIONAL)
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
I: |M| PictureID | (OPTIONAL) I: |M| PictureID | (OPTIONAL)
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
L: | TL0PICIDX | (OPTIONAL) | PictureID |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
T/K: |TID|Y| KEYIDX | (OPTIONAL) L: | TL0PICIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
T/K: |TID|Y| KEYIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
Vp8 description解释vp8帧的关键信息。
第一个字节是必须的,包含X, R, N, S, R, PID标志。
X:存在扩展控制位。设置为 1 时,必须在强制的第一个八位字节后立即提供扩展八位字节。如果该位为零,则必须省略所有可选字段。注意:不要将此 X 位与 RTP 标头中的 X 位混淆。
R:为将来使用而保留的位。必须设置为 0,并且必须被接收器忽略。没啥用
N:非参考帧。设置为 1 时,可以丢弃该帧而不会影响任何其他未来或过去的帧。如果帧的参考状态未知,则应将此位设置为 0,以避免丢弃需要参考的帧。
S:VP8 分区的开始。当 RTP 数据包的第一个有效载荷八位字节是新 VP8 分区的开头时,应将其设置为 1,否则不得为 1。对于每个编码帧的第一个数据包,S 位必须设置为 1。
PID:分区索引。表示数据包的第一个有效载荷八位字节属于哪个 VP8 分区。第一个 VP8 分区(包含模式和运动矢量)必须标记为 PID = 0。对于每个后续分区,PID 应增加 1,但对于所有数据包,PID 可以保持在 0。PID 不能大于 7。如果编码帧中有多个数据包包含相同的 PID,则除具有该 PID 的第一个数据包之外,不得为任何数据包设置 S 位。
1.1.1 X使能后Option字段
X Option内容有:I, L, T, K, RSV字段
+-+-+-+-+-+-+-+-+
X: |I|L|T|K| RSV | (OPTIONAL)
+-+-+-+-+-+-+-+-+
详情:I:PictureID 存在。设置为 1 时,PictureID 必须存在于扩展位字段之后,并按如下方式指定。否则,PictureID 不得存在。
L:TL0PICIDX 存在。设置为 1 时,TL0PICIDX 必须存在并按如下方式指定,并且 T 位必须设置为 1。否则,TL0PICIDX 不得存在。
T:TID 存在。设置为 1 时,必须存在 TID/Y/KEYIDX 八位字节。八位字节的 TID|Y 部分必须按如下方式指定。如果 K(如下)设置为 1 但 T 设置为 0,则必须存在 TID/Y/KEYIDX 八位字节,但必须忽略 TID 字段。如果 T 和 K 均未设置为 1,则不得存在 TID/Y/KEYIDX 八位字节。
K:KEYIDX 存在。设置为 1 时,必须存在 TID/Y/KEYIDX 八位字节。八位字节的 KEYIDX 部分必须按如下方式指定。如果 T(上文)设置为 1 但 K 设置为 0,则必须存在 TID/Y/KEYIDX 八位字节,但必须忽略 KEYIDX 字段。如果 T 和 K 均未设置为 1,则不得存在 TID/Y/KEYIDX 八位字节。
RSV:为将来使用而保留的位。必须设置为 0 并且必须被接收方忽略。
1.1.2 I使能后Option字段
I Option主要有两部分: M,PictureID
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
I: |M| PictureID | (OPTIONAL) I: |M| PictureID | (OPTIONAL)
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| PictureID |
+-+-+-+-+-+-+-+-+
具体如下:
M:第一个八位字节的最高有效位是扩展标志。如果设置了 M,则 PictureID 字段的其余部分必须包含 15 位,否则必须包含 7 位。注意:不要将此 M 位与 RTP 标头中的 M 位混淆。
PictureID:7 位或 15 位(图 2 中分别显示为左侧和右侧),不包括 M 位。这是帧的运行索引,可以从随机值开始,对于每个后续帧,必须增加 1,并且在达到最大 ID(所有位都设置)后必须回绕到 0。PictureID 的 7 位或 15 位从最高有效位到最低有效位依次排列,从 M 位后的第一位开始。发送方选择 7 位或 15 位索引并相应地设置 M 位。接收方不得假设 PictureID 中的位数在整个会话期间保持不变。发送一个所有位都设置为 1 的 7 位 PictureID 后,发送方可以将 PictureID 回绕到 0 或扩展到 15 位并继续递增。
1.1.3 L使能后Option字段(TL0PICIDX)
L Option字段主要信息:TL0PICIDX
+-+-+-+-+-+-+-+-+
L: | TL0PICIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
具体如下:
TL0PICIDX:8 位时间零级索引。TL0PICIDX 是时间基础层帧(即 TID 设置为 0 的帧)的运行索引。如果 TID 大于 0,则 TL0PICIDX 指示当前图像依赖于哪个基础层帧。当 TID 为 0 时,TL0PICIDX 必须递增。索引可以从随机值开始,并且在达到最大数字 255 后必须回绕到 0。TL0PICIDX 的使用取决于 TID 的存在。因此,建议在 TL0PICIDX 存在时使用 TID。
1.1.4 TID/Y/KEYIDX使能后扩展
T或K使能后,TID/Y/KEYIDX字段有具体值。
+-+-+-+-+-+-+-+-+
T/K: |TID|Y| KEYIDX |
+-+-+-+-+-+-+-+-+
具体值含义:
TID:2 位时间层索引。当 T 位设置为 0 时,接收方必须忽略 TID 字段。TID 字段指示数据包代表哪个时间层。最低层,即基础层,必须将 TID 设置为 0。较高层应根据其在层层次结构中的位置增加 TID。
Y:1 层同步位。如果当前帧仅依赖于 TL0PICIDX 等于当前帧的基层 (TID = 0) 帧,则应将 Y 位设置为 1。如果当前帧依赖于除 TL0PICIDX 等于当前帧的基层 (TID = 0) 帧之外的任何其他帧,则必须将 Y 位设置为 0。此外,如果当前帧之后的任何帧依赖于比 TL0PICIDX 等于当前帧的基层帧更早的非基层帧,则必须将 Y 位设置为 0。如果在 T 位等于 0 时设置了 Y 位,则当前帧必须仅依赖于过去的基层 (TID=0) 关键帧,如 KEYIDX 字段的变化所示。此外,此帧不得依赖于自上次更改 KEYIDX 字段以来已更新的三个编解码器缓冲区(由 [RFC6386] 定义)中的任何一个。
KEYIDX:5 位临时关键帧索引。当 K 位设置为 0 时,接收方必须忽略 KEYIDX 字段。KEYIDX 字段是关键帧的运行索引。KEYIDX 可以从随机值开始,在达到最大数字 31 后必须返回 0。使用时,KEYIDX 应该同时出现在关键字。发送方必须增加关键帧的 KEYIDX,这些关键帧传达了对后续帧解释至关重要的参数更新,并且应该保持不包含这些关键更新的关键帧的 KEYIDX 不变。如果存在 KEYIDX,接收方在上一次 KEYIDX 返回后未收到并解码具有相同 KEYIDX 的关键帧,则不应解码帧间。也就是没有对应的keyframe,无法正确解码。
提示:执行 VP8 流拼接的实现必须确保在拼接过程中遵守 TL0PICIDX 和 KEYIDX 递增规则。这可能需要拼接后重写 TL0PICIDX 和 KEYIDX 的值。
1.2 VP8 Payload Header
在VP8 Payload Descriptor后,紧跟着vp8 payload header。
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|Size0|H| VER |P|
+-+-+-+-+-+-+-+-+
| Size1 |
+-+-+-+-+-+-+-+-+
| Size2 |
+-+-+-+-+-+-+-+-+
| Octets 4..N of|
| VP8 payload |
: :
+-+-+-+-+-+-+-+-+
| OPTIONAL RTP |
| padding |
: :
+-+-+-+-+-+-+-+-+
P:逆关键帧标志。设置为 0 时,当前帧为关键帧。设置为 1 时,非关键帧。其他的字段暂时用不上。
2. 代码实现
借用开源cpp_media_server来举例,
cpp media server是基于c++17开发的webrtc会议服务sfu.
支持跨平台(linux/mac),支持./build.sh一键编译。
地址: https://github.com/runner365/cpp_media_server
vp8组装的实现文件:https://github.com/runner365/cpp_media_server/blob/v1.1/src/net/webrtc/pack_handle_vp8.cpphttps://github.com/runner365/cpp_media_server/blob/v1.1/src/net/webrtc/pack_handle_vp8.hpp
在文件pack_handle_vp8.cpp中,具体见下面的代码注释:
void pack_handle_vp8::input_rtp_packet(std::shared_ptr<rtp_packet_info> pkt_ptr) {
//本函数通过分析:
//1)rtp header
//2) vp8 payload descriptor
//3) vp8 payload header
//实现组帧上送
ptr = (const uint8_t *)(pkt_ptr->pkt->get_payload());
pend = ptr + pkt_ptr->pkt->get_payload_length();
/*
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|X|R|N|S|R| PID | (REQUIRED)
+-+-+-+-+-+-+-+-+
*/
// VP8 payload descriptor
extended_control_bits = ptr[0] & 0x80; //X
start_of_vp8_partition = ptr[0] & 0x10; //S
ptr++;
/*
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|X|R|N|S|R| PID | (REQUIRED)
+-+-+-+-+-+-+-+-+
X: |I|L|T|K| RSV | (OPTIONAL)
+-+-+-+-+-+-+-+-+
I: |M| PictureID | (OPTIONAL)
+-+-+-+-+-+-+-+-+
| PictureID |
+-+-+-+-+-+-+-+-+
L: | TL0PICIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
T/K:|TID|Y| KEYIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
*/
//如果存在X extension
if (extended_control_bits && ptr < pend) {
uint8_t pictureid_present;
uint8_t tl0picidx_present;
uint8_t tid_present;
uint8_t keyidx_present;
//I OPTIONAL
pictureid_present = ptr[0] & 0x80;
//L OPTIONAL
tl0picidx_present = ptr[0] & 0x40;
//T OPTIONAL
tid_present = ptr[0] & 0x20;
//K OPTIONAL
keyidx_present = ptr[0] & 0x10;
ptr++;
//如果I OPTIONAL存在
if (pictureid_present && ptr < pend) {
uint16_t picture_id;
//获取PictureID
picture_id = ptr[0] & 0x7F;
if ((ptr[0] & 0x80) && ptr + 1 < pend) {
picture_id = (picture_id << 8) | ptr[1];
ptr++;
}
ptr++;
}
//如果L OPTIONAL存在
if (tl0picidx_present && ptr < pend) {
// ignore temporal level zero index
ptr++;
}
//如果T OPTIONAL或者K OPTIONAL存在
if ((tid_present || keyidx_present) && ptr < pend) {
// ignore KEYIDX
ptr++;
}
}
// VP8 payload header (3 octets)
// 如果是新的一个vp8帧开始,查看vp8 payload header,
// 看看是否为keyframe
if (start_of_vp8_partition) {
//当新的一个vp8帧开始
/*
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|Size0|H| VER |P|
+-+-+-+-+-+-+-+-+
| Size1 |
+-+-+-+-+-+-+-+-+
| Size2 |
+-+-+-+-+-+-+-+-+
*/
// P: 关键帧逆使能,0位关键帧,1为非关键帧
if ((ptr[0] & 0x01) == 0) {// PID == 0 mean keyframe
log_infof("vp8 get keyframe");
key_frame = true;
}
//当新vp8帧来了,上送上一个vp8帧给业务
output_packet();
}
//如果是关键帧,明确其是关键帧
if (key_frame) {
buffer_pkt_ptr_->is_key_frame_ = true;
}
buffer_pkt_ptr_->dts_ = pkt_ptr->pkt->get_timestamp();
buffer_pkt_ptr_->pts_ = pkt_ptr->pkt->get_timestamp();
buffer_pkt_ptr_->av_type_ = MEDIA_VIDEO_TYPE;
buffer_pkt_ptr_->codec_type_ = MEDIA_CODEC_VP8;
buffer_pkt_ptr_->fmt_type_ = MEDIA_FORMAT_RAW;
buffer_pkt_ptr_->buffer_ptr_->append_data((const char*)ptr, pend - ptr);
if (pkt_ptr->pkt->get_marker()) {
output_packet();
}
3. 总结
本文介绍Rtp如何封装vp8编码格式,并举例如何组装Rtp。vp8的rtp,只需要关注:
- Rtp报文中的M标志位:表示改vp8帧是否结束
- VP8 Payload Descriptor:vp8帧关键信息
- VP8 Payload Header:该帧是否为关键帧
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。