当前位置: 首页 > 工具软件 > VP8 > 使用案例 >

封装VP8格式的RTP包

夏宏旷
2023-12-01

封装VP8格式的RTP包

整体结构

*  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 |P|X| CC    |M|     PT      |       sequence number         |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |                          timestamp                            |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |           synchronization source (SSRC) identifier            |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |            contributing source (CSRC) identifiers             |
* |                             ……                                |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |                       header extension                        |
* |                             ……                                |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |           vp8 payload descriptor (integer #octets)            |
* :                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |                               : vp8 payload header(3 octets)  |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | vp8 pyld her  :                                               |
* +-+-+-+-+-+-+-+-+                                               |
* :                      octets 4..N  vp8 payload                 :
* |                                                               |
* |                             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |                             :      optional RTP padding       |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RTP包头前12(通常是12个,可能有多个CSRC。或者加了扩展位)个固定字节解释:

  • V – Version. RTP版本号
  • P – Padding. 是否填充,如果设置为允许填充的话,在包的末尾填充一个或多个字节,这些填充的字节不是有效负载的一部分。
  • X – Extension bit. 扩充位,如果设置为允许的话,固定头结构后面(即包的12个字节后面,有效负载的前面)紧跟着一个扩展头结构,该结构是已定义的一种格式。(可以自己设计,为了支持多个扩展头,RFC5285以defined by profile进行了扩展。分为One-Byte Header和Two-Byte Header:==https://datatracker.ietf.org/doc/rfc5285/
  • CSRC count (CC) 数据源的个数(即源的个数),如果只有一个源(SSRC)那么此时的值为0
  • M – Marker. 标识,每帧的最后一个包设置为1,表示这一帧结束了,可以开始解码。不然就得等下一帧来了,才知道上一帧结束了。
  • Payload type 有效负载,RTP数据的有效负载(不包括头12个字节),由具体的应用程序来确定负载的格式和意义。官方文档里有表格说明,该表格显示了格式代码和具体格式的对应关系,附加的格式代码可能不在RTP协议里定义。
  • Sequence number 数据包序号,发送的RTP数据包序号,接收端可用它来检查丢失的数据包和确定保存数据包次序。
  • Timestamp 时间戳, 纪录了RTP数据包中第一个字节的采样时间(不是真实时间),采样时间必须源自一个时间增量且允许同步和计算。
  • SSRC – 同步标识, 是一个随机数,在同一个RTP会话中只有一个同步标识。
  • CSRC – Contributing source identifiers list. 标识此数据包中包含的有效负载的来源。

Payload Descriptor

结构如下(左右两图结构一样,只有PictureID长度不同)

     0 1 2 3 4 5 6 7                           0 1 2 3 4 5 6 7 
    +-+-+-+-+-+-+-+-+                         +-+-+-+-+-+-+-+-+
I:  |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:  |   TLOPICIDX   |(OPTIONAL)               |   PictureID   | 
    +-+-+-+-+-+-+-+-+                         +-+-+-+-+-+-+-+-+
T/K:|TID|Y| KEYIDX  |(OPTIONAL)          L:   |   TLOPICIDX   |(OPTIONAL) 
    +-+-+-+-+-+-+-+-+                         +-+-+-+-+-+-+-+-+
                                         T/K: |TID|Y| KEYIDX  |(OPTIONAL) 
                                              +-+-+-+-+-+-+-+-+

必选

  • X:扩展控制位。如果为1,结构如图。如果为0,途中optional的行都没有。
  • R:保留位,必须设置为0.
  • N:不被参考的帧。1表示丢了这帧,不影响其他帧的解码。如果不清楚,设置0.
  • S:标识一个VP8 partition的开始。如果payload的第一个字节是一个新的VP8 partition的开始,则设为1.每帧的第一个包必须设置为1.
  • PID: Partition index.每帧的第一个partition必须是0.每个partition增加1,或者全保持为0.PID不超过7.如果多个包包含同一个PID,则除了第一个,其他的S位设置为0.

可选

X为1时,有以下部分:

  • I: PictureID 控制。如果设置为1,有图中I行。
  • L: TL0PICIDX 控制。如果设置为1,有图中L行。并且T必须设置为1.
  • T: TID 控制。如果设置为1,有T/K行。
  • K: KeyIDX 控制。如果设置为1,有T/K行。
  • RSV: 保留位。
  • M:0,PictureID长度7位(上左图);1,PictureID长度15位(上右图)。
  • PictureID:标识帧,随机值开始,每帧增加1。到达最大值后,回到0.(不要假设PictureID的长度保持不变)
  • TL0PICIDX:8 bits temporal level zero index. The field MUST be
    present if the L bit is equal to 1, and MUST NOT be present
    otherwise. TL0PICIDX is a running index for the temporal base
    layer frames, i.e., the frames with TID set to 0. If TID is
    larger than 0, TL0PICIDX indicates which base layer frame the
    current image depends on. TL0PICIDX MUST be incremented when TID
    is 0. The index SHOULD start on a random number, and MUST restart
    at 0 after reaching the maximum number 255.
  • TID:2bit,Temporal-layer index.如果T为0,必须被忽略。
  • Y:1layer sync bit.
  • KeyIDX:5bit,temporal key frame index.如果K为0,必须被忽略。开始于随机值,最大值31,达到最大值后变为0.关键帧如果改变了对后续解码很关键的参数信息,则+1,未改变参数的关键帧和非关键帧保持和前面的关键帧相同。如果收到一个P帧,而相同KeyIDX的关键帧没有收到,则不应该解码。如果不确定是否改变了关键信息,可以把关键帧都+1.或者把K设置为0。

从ffmpeg的代码上看,TL0PICIDX,TID,Y,KEYIDX都被忽略了,不过其实大部分编码也都不适用这几位。
==更多详情可阅读:https://tools.ietf.org/html/draft-ietf-payload-vp8-12#section-4.2==

Payload Header

是VP8压缩数据的未压缩数据部分。前3个字节是I帧P帧通用的。I帧后7个字节被认为是payload的其他部分。
payload header只存在于S位为1,且PID为0的包(144)。

      0 1 2 3 4 5 6 7
     +-+-+-+-+-+-+-+-+
     |Size0|H| VER |P|
     +-+-+-+-+-+-+-+-+
     |     Size1     |
     +-+-+-+-+-+-+-+-+
     |     Size2     |
     +-+-+-+-+-+-+-+-+
     | Bytes 4..N of |
     | VP8 payload   |
     :               :
     +-+-+-+-+-+-+-+-+
     | OPTIONAL RTP  |
     | padding       |
     :               :
     +-+-+-+-+-+-+-+-+
  • P:类型,VP8只有两种,keyframe及interframe,分别为0和1.RFC6386中有定义。(也就是说,如果payload Header第一个字节是偶数,则说明这帧是keyframe)
  • VER:版本,内容如下:
            +---------+-------------------------+-------------+
            | Version | Reconstruction Filter   | Loop Filter |
            +---------+-------------------------+-------------+
            | 0       | Bicubic                 | Normal      |
            |         |                         |             |
            | 1       | Bilinear                | Simple      |
            |         |                         |             |
            | 2       | Bilinear                | None        |
            |         |                         |             |
            | 3       | None                    | None        |
            |         |                         |             |
            | Other   | Reserved for future use |             |
            +---------+-------------------------+-------------+
  • H:显示位。0的时候,不显示,其实我觉得很奇怪,因为我这边抓到的报文,该位都是0,总不能都不显示吧,显然又是被解码器忽略了!
  • Size:首个partition的长度,19位,很奇怪的设定,计算方式是这样的1stPartitionSize = Size0 + 8 * Size1 + 2048 * Size2

VP8 payload

VP8编码数据

最后一个需要解析的是关于keyframe的了,只有是keyframe才带有keyframe header。这个头里面携带了图像的大小,以及起始的校验值0x9d012a(157 1 42)。rfc6386更是把代码直接贴出来了:头校验,不符合0x9d012a显然非VP8的keyframe了

==更多详情可阅读:http://elkpi.com/topics/2014/12/vp8-rtp-payload.html==

Optional RTP padding

RTP头的P为1才有这部分。

封包说明

  • RTP封包的时候,可以考虑VP8的partition边界,也可以不考虑。
  • 如果不考虑,则所有一个包可以包含多个partition,PID全为0.
  • 如果考虑,则一个包只能包含一个parition的片段,每个partition的包PID递增。

==参考资料:https://segmentfault.com/a/1190000023097934==

实践

解释下面这段VP8数据的RTP包:

[144 96 42 244 223 91 236 167 131 214 222 250 190 222 0 1 64 49 0 0 144 128 245 116 16 191 0 157 1 42 128 2 224 ... 226 72 169 244 51 91 89 71 39]
//【首先是RTP Header】 144[10010000]: V=2;P=0说明没有填充;X=1说明多了一个字节的扩展位;CC=0,说明没有CSRC。96[01100000] M=0说明这一帧还没结束,payloadType=96;42 244是序列号;223 91 236 167是时间戳;131 214 222 250是SSRC;190 222 0 1 64 49 0 0是扩展位(0xBE开头,是One-Byte Header)。
//【然后是payload Describetor】144[10010000]:X=1说明有扩展位;R必须为0;N通常为0;S=1说明是一个新的VP8 partition的开始。因为X=1说明128[10000000]是X的值,I=1说明有PictureID的信息;L=0、T=0、K=0说明这几行信息都没有;RSV保留为默认为0;245 116 :M=1说明PictureID有15位,后面15位都是PictureID。
//【然后是payload header】16[00010000]前三位为size0,就不去计算了,H=1说明要显示,作用其实也不大;VER=0;重要的是!P=0说明是KeyFrame,191 0分别是size1和size2;再后面三位是157 1 42可以验证前面P=0。
[144 96 42 245 223 91 236 167 131 214 222 250 190 222 0 1 64 49 0 0 128 128 245 116 248 225 248 55 217 186 2 31 214 ... 43 155 167 49 69 104 111 73 208 212 104 222 4 243 189 41 80 28 141 54 134 233 231 82 223 33]
//【首先是RTP Header】 144[10010000]: V=2;P=0说明没有填充;X=1说明多了一个字节的扩展位;CC=0,说明没有CSRC。96[01100000] M=0说明这一帧还没结束,payloadType=96;42 245是序列号;223 91 236 167是时间戳(因为是同一帧,所以和上一个包是一样的);131 214 222 250是SSRC;190 222 0 1 64 49 0 0是扩展位。
//【然后是payload Describetor】128[10000000]:X=1说明有扩展位;R必须为0;N通常为0;S=0说明不是一个新的VP8 partition的开始,而是接着上一个包的。因为X=1说明128[10000000]是X的值,I=1说明有PictureID的信息;L=0、T=0、K=0说明这几行信息都没有;RSV保留为默认为0;245 116 :M=1说明PictureID有15位,后面15位都是PictureID(也和上一个包相同)。
//【然后是payload数据】因为这是接着前一个包的中间的帧数据,所以没有header了,而是直接接着上一个尾巴的,详情看后面的Payload函数代码。
[144 224 42 246 223 91 236 167 131 214 222 250 190 222 0 1 64 49 0 0 128 128 245 116 18 152 229 126 72 231 2 118 76 213 252 86 88 89 ... 186 62 12 89 67 104 195 134 95 83 59 40 226 187 37 90 18 0]
//【首先是RTP Header】 144[10010000]: V=2;P=0说明没有填充;X=1说明多了一个字节的扩展位;CC=0,说明没有CSRC。224[11100000] M=1说明这一帧到这个包就结束了,payloadType=96;42 246是序列号;(后面都和前两个包一样)223 91 236 167是时间戳;131 214 222 250是SSRC;190 222 0 1 64 49 0 0是扩展位。
//【然后是payload Describetor】128[10000000]:X=1说明有扩展位;R必须为0;N通常为0;S=0说明不是一个新的VP8 partition的开始,而是接着上一个包的。因为X=1说明128[10000000]是X的值,I=1说明有PictureID的信息;L=0、T=0、K=0说明这几行信息都没有;RSV保留为默认为0;245 116 :M=1说明PictureID有15位,后面15位都是PictureID(也和上一个包相同)。
//【然后是payload数据】因为这是接着前一个包的最后的帧数据,也是直接接着上一个尾巴的,没有header。
[144 224 42 247 223 91 249 79 131 214 222 250 190 222 0 1 64 49 0 0 144 128 245 117 49 51 0 228 224 175 188 243 87 167 71 167 216 133 23 242 73 96 55 216 63 252 111 125 121 180 94 67 58 11  ...  32 76 173 48 194 242 52 71 141 116 185 88 68 47 245 145 39 0]
//【首先是RTP Header】 144[10010000]: V=2;P=0说明没有填充;X=1说明多了一个字节的扩展位;CC=0,说明没有CSRC。224[01100000] M=1说明这一帧已经结束(一个包就包含了整个帧的数据,因为是P帧所以比较小,前面是I帧所以比较大),payloadType=96;42 247是序列号;223 91 249 79是时间戳;131 214 222 250是SSRC;190 222 0 1 64 49 0 0是扩展位。
//【然后是payload Describetor】144[10010000]:X=1说明有扩展位;R必须为0;N通常为0;S=1说明是一个新的VP8 partition的开始。因为X=1说明128[10000000]是X的值,I=1说明有PictureID的信息;L=0、T=0、K=0说明这几行信息都没有;RSV保留为默认为0;245 117 :M=1说明PictureID有15位,后面15位都是PictureID。
//【然后是payload header】49[00110001]前三位为size0,就不去计算了,H=1说明要显示;VER=0;重要的是!P=1说明是interframe,51 0分别是size1和size2;再后面三位不是157 1 42可以验证前面P=1,而是真实的VP8 payload了。
// Payload fragments a VP8 packet across one or more byte arrays
vp8HeaderSize := 1 
//在这个封包方法中,只要了必选的VP8 payload Descriptor
func (p *VP8Payloader) Payload(mtu int, payload []byte) [][]byte {
  maxFragmentSize := mtu - vp8HeaderSize//这里mtu=1200-12 

  payloadData := payload
  payloadDataRemaining := len(payload)

  payloadDataIndex := 0
  var payloads [][]byte

  // Make sure the fragment/payload size is correct这个帧封完了
  if min(maxFragmentSize, payloadDataRemaining) <= 0 {
    return payloads
  }
  for payloadDataRemaining > 0 {
    currentFragmentSize := min(maxFragmentSize, payloadDataRemaining)
    out := make([]byte, vp8HeaderSize+currentFragmentSize)
    if payloadDataRemaining == len(payload) {
    //如果是一个帧的第一个包,那么S位等于1,否则默认为0
      out[0] = 0x10
    }

    copy(out[vp8HeaderSize:], payloadData[payloadDataIndex:payloadDataIndex+currentFragmentSize])
    //数据是直接分割vp8 payload的,然后统一加上了一个vp8 payload Descriptor。所以只有每帧的第一个包里面的payload包含了payload header。
    payloads = append(payloads, out)

    payloadDataRemaining -= currentFragmentSize
    payloadDataIndex += currentFragmentSize
  }

  return payloads
}
 类似资料: