Go语言之pion/rtcp包源码分析

725 阅读10分钟

「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战

pion/rtcp包

rtcp是rtp的姐妹协议,pion/rtcp实现了rfc3550和rfc5506. rtcp采用的是out-of-band(oob,带外数据,和传输数据不是用的同一通道), rtcp提供了一些rtp会话的统计信息和控制信息.

rtcp的主要作用是提供qos的反馈, 例如: 已传输字节/包数量/丢包数/延时/rtt延时等. app可能会利用这些数据做控制服务参数,例如:限流/切换编码格式.

/*
Decoding RTCP packets:

  pkt, err := rtcp.Unmarshal(rtcpData)
  // ...

  switch p := pkt.(type) {
  case *rtcp.CompoundPacket:
    ...
  case *rtcp.PictureLossIndication:
    ...
  default:
    ...
  }

Encoding RTCP packets:

  pkt := &rtcp.PictureLossIndication{
    SenderSSRC: senderSSRC,
    MediaSSRC: mediaSSRC
  }
  pliData, err := pkt.Marshal()
  // ...

*/

上面是一个rtcp包的序列化和反序列化的例子,后面统称rtcp的打包和解包过程.

rtcp包每个都很小,所以在传输时总是多个rtcp聚合到一起,然后传输.

pion/rtcp支持的rtcp包类型

rtcp包的类型有很多,目前pion/rtcp实现并支持的rtcp类型在200-206之间

  • 200 sr
  • 201 rr
  • 202 sdes
  • 203 bye
  • 204 app
  • 205 rtpfb
  • 206 psfb

其中rtpfb是传输相关的反馈,psfb是负载相关的反馈.

其中psfb还包含以下具体的消息类型:

  • PLI,解码器会告知编码器,已经丢掉了数量不确定的视频编码数据
    • 此时发送者会重新发送一个关键帧
  • SLI,解码器告知编码器,丢失了几个宏块
    • 此时发送者会重新发送一个关键帧
  • FIR,同样是请求关键帧的消息
  • REMB, 拥塞控制算法,接收端预估最大比特率

rtpfb还包含以下具体的消息类型:

  • TLN, nack,负反馈,这个反馈是告知发送者丢了一些rtp包
  • RRR, 快速同步请求,多用于mcu
  • TCC, 也被称为twcc,发送端预估带宽

rtcp.Header

rtcp包不管类型,都有一个共同的头,4字节

type Header struct {
  Padding bool
  Count uint8
  Type PacketType
  Length uint16
}

分别对应rtcp头中的几个信息,其中Count是5bit,随Type的不同而有不同的意思.

func (h Header) Marshal() ([]byte, error) {
  /*
   *  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|    RC   |   PT=SR=200   |             length            |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   */
  rawPacket := make([]byte, headerLength)

  rawPacket[0] |= rtpVersion << versionShift

  if h.Padding {
    rawPacket[0] |= 1 << paddingShift
  }

  if h.Count > 31 {
    return nil, errInvalidHeader
  }
  rawPacket[0] |= h.Count << countShift

  rawPacket[1] = uint8(h.Type)

  binary.BigEndian.PutUint16(rawPacket[2:], h.Length)

  return rawPacket, nil
}

序列化里的内容比较简单.

func (h *Header) Unmarshal(rawPacket []byte) error {
  if len(rawPacket) < headerLength {
    return errPacketTooShort
  }

  version := rawPacket[0] >> versionShift & versionMask
  if version != rtpVersion {
    return errBadVersion
  }

  h.Padding = (rawPacket[0] >> paddingShift & paddingMask) > 0
  h.Count = rawPacket[0] >> countShift & countMask

  h.Type = PacketType(rawPacket[1])

  h.Length = binary.BigEndian.Uint16(rawPacket[2:])

  return nil
}

反序列化同样非常简单暴力.

rtcp.Packet

rtcp包

type Packet interface {
  DestinationSSRC() []uint32

  Marshal() ([]byte, error)
  Unmarshal(rawPacket []byte) error
}

序列化和反序列化就是rtcp Packet对象和字节数组[]byte的转换. DestinationSSRC方法返回rtcp包相关的ssrc.

func Unmarshal(rawData []byte) ([]Packet, error) {
  var packets []Packet
  for len(rawData) != 0 {
    p, processed, err := unmarshal(rawData)
    if err != nil {
      return nil, err
    }

    packets = append(packets, p)
    rawData = rawData[processed:]
  }

  switch len(packets) {
  // Empty packet
  case 0:
    return nil, errInvalidHeader
  // Multiple Packets
  default:
    return packets, nil
  }
}

反序列化,是通过unmarshal一个个将rtcp.Packet解出来的.

func unmarshal(rawData []byte) (
  packet Packet, bytesprocessed int, err error) {

  var h Header

  // 将解前4个字节,这是rtcp的公共头
  err = h.Unmarshal(rawData)
  if err != nil {
    return nil, 0, err
  }

  bytesprocessed = int(h.Length+1) * 4
  if bytesprocessed > len(rawData) {
    return nil, 0, errPacketTooShort
  }
  inPacket := rawData[:bytesprocessed]

  // 拿到数据后,根据包类型构建Packet
  switch h.Type {
  case TypeSenderReport:
    packet = new(SenderReport)

  case TypeReceiverReport:
    packet = new(ReceiverReport)

  case TypeSourceDescription:
    packet = new(SourceDescription)

  case TypeGoodbye:
    packet = new(Goodbye)

  // 可以看到在rtpfb中,是通过Count来区分TLN/RRR/TCC
  case TypeTransportSpecificFeedback:
    switch h.Count {
    case FormatTLN:
      packet = new(TransportLayerNack)
    case FormatRRR:
      packet = new(RapidResynchronizationRequest)
    case FormatTCC:
      packet = new(TransportLayerCC)
    default:
      packet = new(RawPacket)
    }

  // 可以看到在psfb中,是通过Count来区分/PLI/SLI/REMB/FIR
  // REMB和TCC同属于拥塞控制算法,一个在负载反馈一个在传输反馈
  case TypePayloadSpecificFeedback:
    switch h.Count {
    case FormatPLI:
      packet = new(PictureLossIndication)
    case FormatSLI:
      packet = new(SliceLossIndication)
    case FormatREMB:
      packet = new(ReceiverEstimatedMaximumBitrate)
    case FormatFIR:
      packet = new(FullIntraRequest)
    default:
      packet = new(RawPacket)
    }

  default:
    // 最终还支持原始rtcp包
    packet = new(RawPacket)
  }

  // 这步是rtcp包数据进行反序列化
  err = packet.Unmarshal(inPacket)

  return packet, bytesprocessed, err
}

在unmarsharl功能数中,每次处理一个rtcp包.

来看一下将多个rtcp Packet序列化到一个字符数组里:

func Marshal(packets []Packet) ([]byte, error) {
  out := make([]byte, 0)
  for _, p := range packets {
    data, err := p.Marshal()
    if err != nil {
      return nil, err
    }
    out = append(out, data...)
  }
  return out, nil
}

可以看到此处的序列化,直接使用rtcp.Packet的序列化. 回头看一下反序列化时,用字符数组来反序列化成Packet时,用的是一个完整的rtcp包数据, 所以此处在做序列化时,同样是用一个完整的rtcp包数据.

总结一下: pion/rtcp包第一个暴露的就是Packet包,和基于Packet的两个功能性函数: 将多个Packet序列化为字符数组;将字符数组反序列为多个Packet.

剩下的就是各个rtcp类型对Packet接口的实现.

RawPacket

原始包,就是未解析的rtcp包. 这里的未解析是只未分析出rtcp包类型或具体的消息类型,头信息还是有的.

type RawPacket []byte

var _ Packet = (*RawPacket)(nil)

func (r RawPacket) Marshal() ([]byte, error) {
  return r, nil
}

func (r *RawPacket) Unmarshal(b []byte) error {
  if len(b) < (headerLength) {
    return errPacketTooShort
  }
  *r = b

  var h Header
  return h.Unmarshal(b)
}

func (r RawPacket) Header() Header {
  var h Header
  if err := h.Unmarshal(r); err != nil {
    return Header{}
  }
  return h
}

func (r *RawPacket) DestinationSSRC() []uint32 {
  return []uint32{}
}

对于原始rtcp,处理还是蛮简单的,

func (r RawPacket) String() string {
  out := fmt.Sprintf("RawPacket: %v", ([]byte)(r))
  return out
}

最后还添加了一个打印支持.

SenderReport

先弄明白sr和rr的区别:

  • sr/rr都是用于反馈接收质量的
  • sr比rr多20个字节的数据,这20个字节包含了此参与者的信息
  • 何时发sr,何时发rr
    • 如果上次发送report之后又发送了rtp数据,则发sr
    • 如果在一个report周期内,又发送了rtp数据,则发sr
    • 其他情况:在一个report周期内,没有发送rtp数据,则发rr
  • rr是发给其他发送rtp数据的参与者:我收到了多少包,丢了多少,时间...
  • sr是发给其他rtp参与者:我收到了多少,...,还包含我发送了多少,时间...

SenderReport的类型如下:

type SenderReport struct {
  SSRC uint32
  NTPTime uint64
  RTPTime uint32
  PacketCount uint32
  OctetCount uint32
  Reports []ReceptionReport
  ProfileExtensions []byte
}

对照rfc3550 6.4.1节的结构描述来看,依次是:

  • 4字节的ssrc
  • 8字节的ntp时间戳
  • 4字节的rtp时间戳
  • 4字节的发送rtp总包数
  • 4字节的发送的payload总长度
  • 多个报告块 report block
  • profile-specific扩展

在头中,Count表示报告块的个数,PT是200.

func (r SenderReport) Marshal() ([]byte, error) {

  rawPacket := make([]byte, r.len())
  packetBody := rawPacket[headerLength:]

  binary.BigEndian.PutUint32(packetBody[srSSRCOffset:], r.SSRC)
  binary.BigEndian.PutUint64(packetBody[srNTPOffset:], r.NTPTime)
  binary.BigEndian.PutUint32(packetBody[srRTPOffset:], r.RTPTime)
  binary.BigEndian.PutUint32(packetBody[srPacketCountOffset:], r.PacketCount)
  binary.BigEndian.PutUint32(packetBody[srOctetCountOffset:], r.OctetCount)

  // sr的整个头是24字节
  offset := srHeaderLength
  for _, rp := range r.Reports {
    data, err := rp.Marshal()
    if err != nil {
      return nil, err
    }
    copy(packetBody[offset:], data)

    // 每个报告块的长度都是24字节
    offset += receptionReportLength
  }

  // Count只有5bit,不能超过31个报告块
  if len(r.Reports) > countMax {
    return nil, errTooManyReports
  }

  copy(packetBody[offset:], r.ProfileExtensions)

  // 最后补上4字节的公共头
  hData, err := r.Header().Marshal()
  if err != nil {
    return nil, err
  }
  copy(rawPacket, hData)

  return rawPacket, nil
}

序列化是将SenderReport的数据组成字符数组,相对简单.

func (r *SenderReport) Unmarshal(rawPacket []byte) error {

  // 4字节的rtcp公共头 + 24字节的sr头
  if len(rawPacket) < (headerLength + srHeaderLength) {
    return errPacketTooShort
  }

  var h Header
  if err := h.Unmarshal(rawPacket); err != nil {
    return err
  }

  if h.Type != TypeSenderReport {
    return errWrongType
  }

  packetBody := rawPacket[headerLength:]

  r.SSRC = binary.BigEndian.Uint32(packetBody[srSSRCOffset:])
  r.NTPTime = binary.BigEndian.Uint64(packetBody[srNTPOffset:])
  r.RTPTime = binary.BigEndian.Uint32(packetBody[srRTPOffset:])
  r.PacketCount = binary.BigEndian.Uint32(packetBody[srPacketCountOffset:])
  r.OctetCount = binary.BigEndian.Uint32(packetBody[srOctetCountOffset:])

  offset := srReportOffset
  for i := 0; i < int(h.Count); i++ {
    rrEnd := offset + receptionReportLength
    if rrEnd > len(packetBody) {
      return errPacketTooShort
    }
    rrBody := packetBody[offset : offset+receptionReportLength]
    offset = rrEnd

    var rr ReceptionReport
    if err := rr.Unmarshal(rrBody); err != nil {
      return err
    }
    r.Reports = append(r.Reports, rr)
  }

  // 剩下的丢给profile 扩展
  if offset < len(packetBody) {
    r.ProfileExtensions = packetBody[offset:]
  }

  // 最后还做了一次report块的校验
  if uint8(len(r.Reports)) != h.Count {
    return errInvalidHeader
  }

  return nil
}

和序列化一样,在反序列化中都没有对报告块做分析.

func (r *SenderReport) DestinationSSRC() []uint32 {
  out := make([]uint32, len(r.Reports)+1)
  for i, v := range r.Reports {
    out[i] = v.SSRC
  }
  out[len(r.Reports)] = r.SSRC
  return out
}

DestinationSSRC的目的是获取报告块中的ssrc.

func (r SenderReport) String() string {
  out := fmt.Sprintf("SenderReport from %x\n", r.SSRC)
  out += fmt.Sprintf("\tNTPTime:\t%d\n", r.NTPTime)
  out += fmt.Sprintf("\tRTPTIme:\t%d\n", r.RTPTime)
  out += fmt.Sprintf("\tPacketCount:\t%d\n", r.PacketCount)
  out += fmt.Sprintf("\tOctetCount:\t%d\n", r.OctetCount)

  out += "\tSSRC    \tLost\tLastSequence\n"
  for _, i := range r.Reports {
    out += fmt.Sprintf("\t%x\t%d/%d\t%d\n",
      i.SSRC, i.FractionLost, i.TotalLost, i.LastSequenceNumber)
  }
  out += fmt.Sprintf("\tProfile Extension Data: %v\n", r.ProfileExtensions)
  return out
}

打印也支持.

ReceptionReport

报告块,这个是用于告诉推流者,本端收流的质量信息.

这个报告是基于ssrc的.

type ReceptionReport struct {
  SSRC uint32
  FractionLost uint8
  TotalLost uint32
  LastSequenceNumber uint32
  Jitter uint32
  LastSenderReport uint32
  Delay uint32
}

按rfc3550 6.4.1说明,这个报告块的长度是24字节:

  • SSRC 4字节,指明是针对哪个源的接收报告
  • FractionLost, 1字节,从上次发送sr/rr包到现在,rtp丢包率,小数表示
  • TotalLost, 3字节,rtp包总的丢包数量
  • LastSequenceNumber, 4字节,低16位是接收的最大rtp序号,高16位存rtp序号重置次数
  • Jitter, 4字节,rtp包到达间隔时间的一个方差估计,也称抖动
  • LastSenderPeport, 4字节,从源收到的最新sr包中的ntp时间戳中的32位,如果没收到sr,则为0
  • Delay, 4字节,单位1/2的16次方秒,是从源收到sr包到发送此包的时间,如果源没有发sr,则为0

这个报告块包含了很多信息,具体如何使用,就看上层业务如何调度,我们先看报告块的方法:

func (r ReceptionReport) Marshal() ([]byte, error) {
  rawPacket := make([]byte, receptionReportLength)

  binary.BigEndian.PutUint32(rawPacket, r.SSRC)

  rawPacket[fractionLostOffset] = r.FractionLost

  if r.TotalLost >= (1 << 25) {
    return nil, errInvalidTotalLost
  }

  // 高字节保存在低地址,大端模式
  tlBytes := rawPacket[totalLostOffset:]
  tlBytes[0] = byte(r.TotalLost >> 16)
  tlBytes[1] = byte(r.TotalLost >> 8)
  tlBytes[2] = byte(r.TotalLost)

  binary.BigEndian.PutUint32(rawPacket[lastSeqOffset:], r.LastSequenceNumber)
  binary.BigEndian.PutUint32(rawPacket[jitterOffset:], r.Jitter)
  binary.BigEndian.PutUint32(rawPacket[lastSROffset:], r.LastSenderReport)
  binary.BigEndian.PutUint32(rawPacket[delayOffset:], r.Delay)

  return rawPacket, nil
}

func (r *ReceptionReport) Unmarshal(rawPacket []byte) error {
  if len(rawPacket) < receptionReportLength {
    return errPacketTooShort
  }

  r.SSRC = binary.BigEndian.Uint32(rawPacket)
  r.FractionLost = rawPacket[fractionLostOffset]

  tlBytes := rawPacket[totalLostOffset:]
  r.TotalLost = uint32(tlBytes[2]) | uint32(tlBytes[1])<<8 | uint32(tlBytes[0])<<16

  r.LastSequenceNumber = binary.BigEndian.Uint32(rawPacket[lastSeqOffset:])
  r.Jitter = binary.BigEndian.Uint32(rawPacket[jitterOffset:])
  r.LastSenderReport = binary.BigEndian.Uint32(rawPacket[lastSROffset:])
  r.Delay = binary.BigEndian.Uint32(rawPacket[delayOffset:])

  return nil
}

因为有rfc标准,所以报告块的序列化和反序列化都很简单,难的是如何在应用层去使用这些信息.

ReceiverReport

作为流的接收者,发送给发流者的反馈信息. rr作为sr的一个简写部分,大部分数据和结构都是类似的.

作为开源作者,从来不会在两个相同的地方使用同一个招式,而是各种秀.

因为sr和rr的部分代码逻辑类似,下面只挑一些不同的来说.

在序列化时,考虑到了最后的扩展有可能不满4字节的情况:

for (len(pe) & 0x3) != 0 {
  pe = append(pe, 0)
}

在反序列化时倒没考虑填充的问题,因为反序列化时无法考虑这种情况.

SourceDescription

sdes类型的rtcp包,源描述包.

下面是rtcp包组合中的几条约束:

  • 只要带宽允许,sr/rr应该经常发,每个发送周期,都必须报告报告包
  • 每个组合包,都应该包含sdes包,更具体一点,是应该包含sdes包中的cname
    • 新接收者可以通过cname来识别ssrc,并进行流同步
  • rtcp组合包的长度是受限于mtu的,需要注意组合包数量

sdes类型的rtcp包除了公共头,剩下的就是一个个chunk块, chunk块里包含ssrc/csrc信息和具体的sdes信息,所以也称为3层结构. 公共头里的Count就指明了chunk块的数量.

/*
 *         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
 *        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * header |V=2|P|    SC   |  PT=SDES=202  |             length            |
 *        +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
 * chunk  |                          SSRC/CSRC_1                          |
 *   1    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *        |                           SDES items                          |
 *        |                              ...                              |
 *        +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
 * chunk  |                          SSRC/CSRC_2                          |
 *   2    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *        |                           SDES items                          |
 *        |                              ...                              |
 *        +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
 */

ssrc/csrc每个都占4个字节,下面来说下sdes items:

sdes items描述的条目有很多:

  • cname 规范名字
  • Nmae 用户名
  • Email 邮件
  • Phone 电话
  • Location 地址
  • Tool app或工具名
  • Note 提示
  • Private 隐私信息

她们都有一个共同的开头:

/*
 *   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
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |    CNAME=1    |     length    | user and domain name        ...
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 */

再看看处理过程:

type SourceDescription struct {
  Chunks []SourceDescriptionChunk
}

SourceDescription实现了Packet接口,在序列化和反序列化时, 针对chunk块,使用的是SourceDescriptionChunk.

type SourceDescriptionChunk struct {
  Source uint32
  Items  []SourceDescriptionItem
}

在SourceDescriptionChunk的序列化和反序列化中,使用到了SourceDescriptionItem:

type SourceDescriptionItem struct {
  Type SDESType
  Text string
}

在序列化时,chunk块设置长度时有特殊处理,这是rfc上规定的:

func (s SourceDescriptionChunk) len() int {
  len := sdesSourceLen
  for _, it := range s.Items {
    len += it.len()
  }

  // 这步是让每个chunk块以0x00结尾
  len += sdesTypeLen

  // 下面这个是填充,以和4字节对齐
  len += getPadding(len)

  return len
}

下面从头到位分析以下序列化的流程:

func (s SourceDescription) Marshal() ([]byte, error) {

  // 计算rtcp包的总长度
  rawPacket := make([]byte, s.len())
  packetBody := rawPacket[headerLength:]

  chunkOffset := 0
  for _, c := range s.Chunks {
    data, err := c.Marshal()
    if err != nil {
      return nil, err
    }
    copy(packetBody[chunkOffset:], data)
    chunkOffset += len(data)
  }

  if len(s.Chunks) > countMax {
    return nil, errTooManyChunks
  }

  hData, err := s.Header().Marshal()
  if err != nil {
    return nil, err
  }
  copy(rawPacket, hData)

  return rawPacket, nil
}

接下来看每个chunk块的序列化:

func (s SourceDescriptionChunk) Marshal() ([]byte, error) {

  rawPacket := make([]byte, sdesSourceLen)
  binary.BigEndian.PutUint32(rawPacket, s.Source)

  for _, it := range s.Items {
    data, err := it.Marshal()
    if err != nil {
      return nil, err
    }
    rawPacket = append(rawPacket, data...)
  }

  // 这儿就是具体在chunk后添加空字节0x00的逻辑
  rawPacket = append(rawPacket, uint8(SDESEnd))
  rawPacket = append(rawPacket, make([]byte, getPadding(len(rawPacket)))...)

  return rawPacket, nil
}

chunk里分ssrc/csrc和item,item的序列化如下:

func (s SourceDescriptionItem) Marshal() ([]byte, error) {
  if s.Type == SDESEnd {
    return nil, errSDESMissingType
  }

  rawPacket := make([]byte, sdesTypeLen+sdesOctetCountLen)

  // item设置type
  rawPacket[sdesTypeOffset] = uint8(s.Type)

  txtBytes := []byte(s.Text)
  octetCount := len(txtBytes)
  if octetCount > sdesMaxOctetCount {
    return nil, errSDESTextTooLong
  }

  // item设置length
  rawPacket[sdesOctetCountOffset] = uint8(octetCount)

  // item设置内容
  rawPacket = append(rawPacket, txtBytes...)

  return rawPacket, nil
}

sdes类型的rtcp的反序列化也是类似的.

func (s *SourceDescription) DestinationSSRC() []uint32 {
  out := make([]uint32, len(s.Chunks))
  for i, v := range s.Chunks {
    out[i] = v.Source
  }
  return out
}

sdes的DestinationSSRC是获取chunk的数量.

Goodbye

bye类型的rtcp包,用于表明某些源不再活跃.

/*
 *        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|    SC   |   PT=BYE=203  |             length            |
 *       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *       |                           SSRC/CSRC                           |
 *       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *       :                              ...                              :
 *       +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
 * (opt) |     length    |               reason for leaving            ...
 *       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 */

不再活跃的源可以是多个ssrc或csrc,bye包中还可以包含可选的原因. 最后离开的原因的处理和sdes中的chunk填充处理是一致的.

type Goodbye struct {
  Sources []uint32
  Reason string
}

func (g Goodbye) Marshal() ([]byte, error) {

  // 计算整个bye包的长度
  rawPacket := make([]byte, g.len())
  packetBody := rawPacket[headerLength:]

  if len(g.Sources) > countMax {
    return nil, errTooManySources
  }

  // 拷贝ssrc
  for i, s := range g.Sources {
    binary.BigEndian.PutUint32(packetBody[i*ssrcLength:], s)
  }

  // 复制原因
  if g.Reason != "" {
    reason := []byte(g.Reason)

    if len(reason) > sdesMaxOctetCount {
      return nil, errReasonTooLong
    }

    reasonOffset := len(g.Sources) * ssrcLength
    packetBody[reasonOffset] = uint8(len(reason))
    copy(packetBody[reasonOffset+1:], reason)
  }

  // 设置公共头
  hData, err := g.Header().Marshal()
  if err != nil {
    return nil, err
  }
  copy(rawPacket, hData)

  return rawPacket, nil
}

反序列化也是类似.

TransportLayerNack

rtpfb中的tln,传输丢包.

现在rtp传输层的nack反馈只有一种通用的nack. 她的作用就是通知丢了一个或多个rtp包.

一旦底层的传输协议提供了类似的"向发送端反馈信息"的机制, 则不应该使用通用nack.

rtcp中的反馈包包含3个层级的:

  • 传输层反馈
  • 负载层反馈
  • app层反馈

反馈包的统一格式如下:

// 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|  FMT    |    PT         |           length              |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |                     SSRC of packet sender                     |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |                      SSRC of media source                     |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |              feedback control information(FCI)                |

和通用的rtcp头相比,通用头的Count用于表示一些ssrc或chunk或报告包的数量, 在反馈包中,FMT用于表示PT下的细类. 对于tln,PT=205,FMT=15.

ssrc of packet sender:发送此包的原始ssrc. ssrc of media source:这个nack包所关联的哪些ssrc,就是这些ssrc的rtp包丢了. FCI,4字节,决定了哪些信息.

// 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
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |        PID                  |       BLP                       |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

上面就是通用nack的fci.

PID,2字节,是丢失的一个rtp包的序号. BLP,是后续丢的数据包的掩码位,是基于PID来计算的.

可以看出,每次反馈某个rtp包及后面16个包的情况,如果之后第二个包丢失,就将第二位设置为1. 注意:设置为0并不代表对应的rtp已经收到了,只表明在本次反馈中,没有将第x位标记为丢失.

type PacketBitmap uint16

type NackPair struct {
  PacketID uint16
  LostPackets PacketBitmap
}

type TransportLayerNack struct {
  SenderSSRC uint32
  MediaSSRC uint32
  Nacks []NackPair
}

对应tln的类型,一个tln是可以带多个fci的. rfc还规定了反馈消息的长度是n+2,n为nack数量(一个nack就是一个fci), 在代码中的体现是n+2不能超过255,具体的依据还没找到,估计在rfc的哪个角落.

func (p TransportLayerNack) Marshal() ([]byte, error) {
  if len(p.Nacks)+tlnLength > math.MaxUint8 {
    return nil, errTooManyReports
  }

  rawPacket := make([]byte, nackOffset+(len(p.Nacks)*4))
  binary.BigEndian.PutUint32(rawPacket, p.SenderSSRC)
  binary.BigEndian.PutUint32(rawPacket[4:], p.MediaSSRC)
  for i := 0; i < len(p.Nacks); i++ {
    binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i):], p.Nacks[i].PacketID)
    binary.BigEndian.PutUint16(rawPacket[nackOffset+(4*i)+2:], uint16(p.Nacks[i].LostPackets))
  }
  h := p.Header()
  hData, err := h.Marshal()
  if err != nil {
    return nil, err
  }

  return append(hData, rawPacket...), nil
}

tln的结构简单,所以序列化也简单.

这里单独对NackPair提供了几个方法:

func (n *NackPair) Range(f func(seqno uint16) bool) {
  more := f(n.PacketID)
  if !more {
    return
  }

  b := n.LostPackets
  for i := uint16(0); b != 0; i++ {
    if (b & (1 << i)) != 0 {
      b &^= (1 << i)
      more = f(n.PacketID + i + 1)
      if !more {
        return
      }
    }
  }
}

单独看这个方法,没什么头绪,要结合下面的方法一起看:

func (n *NackPair) PacketList() []uint16 {
  out := make([]uint16, 0, 17)
  n.Range(func(seqno uint16) bool {
    out = append(out, seqno)
    return true
  })
  return out
}

用闭包来获取一个[]uint16,猜测是获取rtp序号. 结合Range来看,是获取丢包序号,最长不超过17个.

func NackPairsFromSequenceNumbers(
  sequenceNumbers []uint16) (pairs []NackPair) {

  if len(sequenceNumbers) == 0 {
    return []NackPair{}
  }

  // 第一个nack数据
  nackPair := &NackPair{PacketID: sequenceNumbers[0]}
  for i := 1; i < len(sequenceNumbers); i++ {
    m := sequenceNumbers[i]

    // 如果pid超过了当前nack的pid+16, 则新建一个nack包
    if m-nackPair.PacketID > 16 {
      pairs = append(pairs, *nackPair)
      nackPair = &NackPair{PacketID: m}
      continue
    }

    // 当前迭代的rtp序号可添加到nack中
    nackPair.LostPackets |= 1 << (m - nackPair.PacketID - 1)
  }

  // 处理最后一个nack,添加到nack队列
  pairs = append(pairs, *nackPair)
  return
}

看明白后,这个生成nack队列的功能函数非常精巧.简单就是美.

这里提供的都是一些nack底层的操作,上层会依据这些信息和操作来组合更加复杂的逻辑.

RapidResynchronizationRequest

快速同步请求反馈,也是rtp传输层的包.接收者会通知编码器:有一个或多个图片的数据丢失了.

rfc上说到:媒体接收者无法同步某些媒体流时,发送一个rrr给媒体发送者, 希望媒体发送者尽快发送一个sr包来.

type RapidResynchronizationRequest struct {
  SenderSSRC uint32
  MediaSSRC uint32
}

rrr没有fci.

func (p RapidResynchronizationRequest) Marshal() ([]byte, error) {
  rawPacket := make([]byte, p.len())
  packetBody := rawPacket[headerLength:]

  binary.BigEndian.PutUint32(packetBody, p.SenderSSRC)
  binary.BigEndian.PutUint32(packetBody[rrrMediaOffset:], p.MediaSSRC)

  hData, err := p.Header().Marshal()
  if err != nil {
    return nil, err
  }
  copy(rawPacket, hData)

  return rawPacket, nil
}

rrr的长度是4字节的公共头,8字节的反馈头.

其他部分和其他类型rtcp的类似,就不赘述了.

TransportLayerCC

tcc,也被称为twcc,和remb称为带宽自适应的两大算法.

twcc的两大优势:

  • 基于rtp包,而不是媒体流,更加适合拥塞控制
  • 可以进行更早的丢包检测和恢复

如果要使用twcc,第一需要扩展rtp头,第二需要在sdp中告知"启用了twcc", 第三就是rtcp的支持.

因为pion/rtp支持了tcc,但在pion的其他项目中没有使用到,所以暂不分析rtcp的tcc.

PictureLossIndication

pli,负载层的反馈.

type PictureLossIndication struct {
  SenderSSRC uint32
  MediaSSRC uint32
}

func (p PictureLossIndication) Marshal() ([]byte, error) {
  rawPacket := make([]byte, p.len())
  packetBody := rawPacket[headerLength:]

  binary.BigEndian.PutUint32(packetBody, p.SenderSSRC)
  binary.BigEndian.PutUint32(packetBody[4:], p.MediaSSRC)

  h := Header{
    Count:  FormatPLI,
    Type:   TypePayloadSpecificFeedback,
    Length: pliLength,
  }
  hData, err := h.Marshal()
  if err != nil {
    return nil, err
  }
  copy(rawPacket, hData)

  return rawPacket, nil
}

这种通知型的反馈,是不需要带FCI的,就如RRR一样. pli,12字节,绝大部分逻辑都和rrr类似.

SliceLossIndication

如果将一个图片分层多个小块,从左到右,从上到下,依次编号1-N, sli就是反馈丢了哪些块,pli比较暴力,直接是整个丢了.

sli的fci如下:

// 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
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |        First          |       Number            |  PictureID  |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

说明:

  • First, 13位,第一个丢失的块
  • Number, 13位,总共丢了多少块
  • PictureID,图片的低6位id,和编码格式相关

对应的类型如下:

type SLIEntry struct {
  First uint16
  Number uint16
  Picture uint8
}

type SliceLossIndication struct {
  SenderSSRC uint32
  MediaSSRC uint32
  SLI []SLIEntry
}

在序列化和反序列化中,和其他类型的rtcp包类似,只是将常用的copy改为了append, 小改动.

FullIntraRequest

fir,也是请求关键帧的一种请求,和pli类似,但pli用于从错误中恢复, 而fir的应用场景是新参与者进入,会发送fir,合流,也会用到fir, 如果是从错误中恢复,不会使用fir,而是使用pli.

// 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
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |                     SSRC                                      |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |  seq nr.     |               reserved                         |

seq nr.是命令序号,1字节,可有多个fci.

type FIREntry struct {
  SSRC           uint32
  SequenceNumber uint8
}

type FullIntraRequest struct {
  SenderSSRC uint32
  MediaSSRC  uint32
  FIR []FIREntry
}

序列化和反序列化就不贴了,逻辑和其他包类似.

ReceiverEstimatedMaximumBitrate

拥塞控制算法.

remb,和twcc一个是发送端的算法,一个是接收端的算法,两者的效果都是类似的.

remb就是估计一个会话的总可用带宽.

remb包,是向多个媒体发送方通知:我的总估计可用带宽. 发送方发送的带宽不能超过估计的总带宽,而且需要对改变发送带宽有快速的响应.

除了rtcp包,还需要在sdp中告知:启用了remb.

/*
    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| FMT=15  |   PT=206      |             length            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                  SSRC of packet sender                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                  SSRC of media source                         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Unique identifier 'R' 'E' 'M' 'B'                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Num SSRC     | BR Exp    |  BR Mantissa                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |   SSRC feedback                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  ...                                                          |
*/

下面是对应的类型:

type ReceiverEstimatedMaximumBitrate struct {
  SenderSSRC uint32
  Bitrate uint64
  SSRCs []uint32
}

func (p ReceiverEstimatedMaximumBitrate) Marshal() (buf []byte, err error) {
  // rebm的长度是20字节+4*ssrc数
  buf = make([]byte, p.MarshalSize())

  n, err := p.MarshalTo(buf)
  if err != nil {
    return nil, err
  }

  if n != len(buf) {
    return nil, errWrongMarshalSize
  }

  return buf, nil
}

具体的序列化放在MarshalTo上面:

func (p ReceiverEstimatedMaximumBitrate) MarshalTo(
  buf []byte) (n int, err error) {

  size := p.MarshalSize()
  if len(buf) < size {
    return 0, errPacketTooShort
  }

  buf[0] = 143 // v=2, p=0, fmt=15
  buf[1] = 206

  length := uint16((p.MarshalSize() / 4) - 1)
  binary.BigEndian.PutUint16(buf[2:4], length)

  binary.BigEndian.PutUint32(buf[4:8], p.SenderSSRC)
  binary.BigEndian.PutUint32(buf[8:12], 0) // always zero

  buf[12] = 'R'
  buf[13] = 'E'
  buf[14] = 'M'
  buf[15] = 'B'

  buf[16] = byte(len(p.SSRCs))

  // math/bits.LeadingZeros64是获取前面为0的个数
  // shift最终是比特率的位数
  shift := uint(64 - bits.LeadingZeros64(p.Bitrate))

  // 此处用到了指数和尾数的概念
  // 103020 = 1.0302 * 10的5次方
  // 那么1.0302就是尾数,5就是指数
  var mantissa uint
  var exp uint

  // 以2为底,用24位来存储比特率
  if shift <= 18 {
    mantissa = uint(p.Bitrate)
    exp = 0
  } else {
    mantissa = uint(p.Bitrate >> (shift - 18))
    exp = shift - 18
  }

  buf[17] = byte((exp << 2) | (mantissa >> 16))
  buf[18] = byte(mantissa >> 8)
  buf[19] = byte(mantissa)

  n = 20
  for _, ssrc := range p.SSRCs {
    binary.BigEndian.PutUint32(buf[n:n+4], ssrc)
    n += 4
  }

  return n, nil
}

整个序列化里还是用到了不少新东西的.

最后在打印中,体现了不少好玩的单位.

组合包 CompoundPacket

rtcp包都非常小,所以很多都是聚合在一起发送,发送也有一些规则:

  • 组合包的第一个包一定要是报告包sr或rr
    • 即使没有数据,也要发送一个空的rr包
    • 即使只有一个bye包要组合,也要添加一个空的rr包
  • 每个组合包都应该包含一个含有cname条目的sdes包

类型就是[]Packet

type CompoundPacket []Packet
var _ Packet = (*CompoundPacket)(nil) // assert is a Packet

func (c CompoundPacket) Marshal() ([]byte, error) {
  if err := c.Validate(); err != nil {
    return nil, err
  }

  p := []Packet(c)
  return Marshal(p)
}

组合包的检查我们最后再看,我们分析rtcp时, 第一分析的对象是Packet和基于Packet数组的序列化和反序列化,正好在这里用上了.

func (c *CompoundPacket) Unmarshal(rawData []byte) error {
  out := make(CompoundPacket, 0)
  for len(rawData) != 0 {
    p, processed, err := unmarshal(rawData)
    if err != nil {
      return err
    }

    out = append(out, p)
    rawData = rawData[processed:]
  }
  *c = out

  if err := c.Validate(); err != nil {
    return err
  }

  return nil
}

现在分析最后一个,rfc规定的组合包校验流程:

func (c CompoundPacket) Validate() error {
  if len(c) == 0 {
    return errEmptyCompound
  }

  // 组合包第一个包必须是报告包(sr/rr)
  switch c[0].(type) {
  case *SenderReport, *ReceiverReport:
    // ok
  default:
    return errBadFirstPacket
  }

  // 必须包含一个sdes.cname包
  // 而且sdes.cname前面只能有rr包(如果第一个是sr包的情况除外).
  for _, pkt := range c[1:] {
    switch p := pkt.(type) {
    case *ReceiverReport:
      continue

    case *SourceDescription:
      var hasCNAME bool
      for _, c := range p.Chunks {
        for _, it := range c.Items {
          if it.Type == SDESCNAME {
            hasCNAME = true
          }
        }
      }

      if !hasCNAME {
        return errMissingCNAME
      }

      return nil

    default:
      return errPacketBeforeCNAME
    }
  }

  return errMissingCNAME
}

到此,pion/rtcp包分析完了,除了tcc没详细去分析,其他包都已经过了一遍.