webrtc之rtp协议(三): 封装VP8

本文介绍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 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论