WebRTC 之 RTX

Abstract WebRTC RTX 笔记
Authors Walter Fan
Category learning note
Status v1.0
Updated 2020-08-28
License CC-BY-NC-ND 4.0

什么是 RTX

RTX 就是重传 Retransmission, 将丢失的包重新由发送方传给接收方。

Webrtc 默认开启 RTX (重传),它一般采用不同的 SSRC 进行传输,即原始的 RTP 包和重传的 RTP 包的 SSRC 是不同的,这样不会干扰原始 RTP 包的度量。

RTX 包的 Payload 在 RFC4588 中有详细描述,一般 NACK 导致的重传包和 Bandwidth Probe 导致的探测包也可能走 RTX 通道。

为什么用 RTX

媒体流多使用 RTP 通过 UDP 进行传输,由于是不可靠传输,丢包是不可避免,也是可以容忍的,但是对于一些关键数据是不能丢失的,这时候就需要重传(RTX)。

在 WebRTC 中常用的 QoS 策略有

  1. 反馈:例如 PLI , NACK
  2. 冗余, 例如 FEC, RTX
  3. 调整:例如码率,分辨率及帧率的调整
  4. 缓存: 例如 Receive Adaptive Jitter Buffer, Send Buffer

这些措施一般需要结合基于拥塞控制(congestion control) 及带宽估计(bandwidth estimation)技术, 不同的网络条件下需要采用不同的措施。

FEC 用作丢包恢复需要占用更多的带宽,即使 5% 的丢包需要几乎一倍的带宽,在带宽有限的情况下可能会使情况更糟。

RTX 不会占用太多的带宽,接收方发送 NACK 指明哪些包丢失了,发送方通过单独的 RTP 流(不同的 SSRC)来重传丢失的包,但是 RTX 至少需要一个 RTT 来修复丢失的包。

音频流对于延迟很敏感,而且占用带宽不多,所以用 FEC 更好。WebRTC 默认并不为 audio 开启 RTX
视频流对于延迟没那么敏感,而且占用带宽很多,所以用 RTX 更好。

RTX 相关的信令

RTX 的信令层主要是由发送方通过 SDP 告知接收方我支持 RTX 特性,并且约定原始包和重传包之间的关系由什么方式指定。

现在常用的方式有三种

  1. APT - Associated Payload Type 关联荷载类型 - Chrome, Edge, Firefox, Safari 都支持
  2. RID/RRID - RTP Stream Id 和 Repaired RTP Stream Id - - Chrome, Edge, Safari 支持, Firefox 不支持
  3. SSRC Group - SSRC 分组 - Firefox 支持,其他三个现在优先用 rid/rrid

SDP Extensions

1) Associated Payload Type

在SDP 中可以指定 RTP 流所关联的 RTX 流的荷载类型 Associated Payload Type, 参照 RFC 4588, 期望在 SDP 中有如下属性

a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96;rtx-time=3000
for example

v=0
o=mascha 2980675221 2980675778 IN IP4 host.example.net
c=IN IP4 192.0.2.0
m=video 49170 RTP/AVPF 96 97
a=rtpmap:96 MP4V-ES/90000
a=rtcp-fb:96 nack
a=fmtp:96 profile-level-id=8;config=01010000012000884006682C2090A21F
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96;rtx-time=3000

2) RID and RRID

As RFC 8853, 约定 RTP 包中增加 rid 和 rrid 的扩展头

a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=simulcast...
a=rid:<rid-id> <direction> [pt=<fmt-list>;]<restriction>=<value>...
  • direction 可以是 send 或者 recv,pt 包含相关的 payload type, restriction 是指一些编码约束, 详情参见 RFC8851

3) SSRC-Group

还有一个方法就是 SSRC Group, 将相互之间有关联关系的媒体流的 SSRC 编配成一个个小组

1. FID SSRC-group for RTX

举例如下

a=ssrc:659652645 cname:Taj3/ieCnLbsUFoH
a=ssrc:659652645 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:659652645 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:659652645 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 cname:Taj3/ieCnLbsUFoH
a=ssrc:98148385 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:98148385 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc-group:FID 659652645 98148385

2. SIM SSRC-group for Simulcast

Simulcast 联播结合 RTX , 可做如下所示例中的分组

a=ssrc:659652645 cname:Taj3/ieCnLbsUFoH
a=ssrc:659652645 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:659652645 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:659652645 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 cname:Taj3/ieCnLbsUFoH
a=ssrc:98148385 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:98148385 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1982135572 cname:Taj3/ieCnLbsUFoH
a=ssrc:1982135572 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1982135572 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:1982135572 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:2523084908 cname:Taj3/ieCnLbsUFoH
a=ssrc:2523084908 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:2523084908 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:2523084908 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:3604909222 cname:Taj3/ieCnLbsUFoH
a=ssrc:3604909222 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:3604909222 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:3604909222 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1893605472 cname:Taj3/ieCnLbsUFoH
a=ssrc:1893605472 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1893605472 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:1893605472 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc-group:SIM 659652645 1982135572 3604909222
a=ssrc-group:FID 659652645 98148385
a=ssrc-group:FID 1982135572 2523084908
a=ssrc-group:FID 3604909222 1893605472

RTP 头扩展

根据 RFC8852: RTP Stream Identifier Source Description (SDES) 中的定义,RID 和 RRID 的扩展头格式如下

  • RtpStreamId 对每个 RTP stream 都是不同的(类似于 SSRC , 在RTP Session 中需要保持唯一性)
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|RtpStreamId=12 |     length    | RtpStreamId                 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • RepairedRtpStreamId 只会出现在 Repair RTP Streams 中, 指明它所修复的 RTP 流的 rid
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Repaired...=13 |     length    | RepairRtpStreamId           ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RTX 媒体包的格式

RFC4588 - "RTP Retransmission Payload Format" 中描述了 RTX RTP 包的格式。

  1. RTP 头中会包含上面所述的 rrid
  2. RTP 荷载中会有一个 OSN ,对应原始 RTP 包中的 sequence number
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         RTP Header                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            OSN                |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
|                  Original RTP Packet Payload                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

例如

  • SDP 中指定了 rid 的值 和扩展头的标识
a=rid:1 send
a=rid:2 send
a=rid:3 send
a=simulcast:send 1;2;3
 
 
a=extmap:8/sendrecv http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4/sendrecv urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:5/sendrecv urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:7/sendrecv urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
  • 原始的 RTP 包的格式如下
Real-Time Transport Protocol
[Stream setup by HEUR RTP (frame 62)]
10.. .... = Version: RFC 1889 Version (2)
..0. .... = Padding: False
...1 .... = Extension: True
.... 0000 = Contributing source identifiers count: 0
1... .... = Marker: True
Payload type: DynamicRTP-Type-97 (97)
Sequence number: 27303
[Extended sequence number: 92839]
Timestamp: 3417222624
Synchronization Source identifier: 0x9100cc9c (2432748700)
Defined by profile: Unknown (0xbede)
Extension length: 2
Header extensions
RFC 5285 Header Extension (One-Byte Header)
Identifier: 8
Length: 3
Extension Data: 6e8c4a
RFC 5285 Header Extension (One-Byte Header)
Identifier: 4
Length: 1
Extension Data: 30
RFC 5285 Header Extension (One-Byte Header)
Identifier: 5
Length: 1
Extension Data: 31
Payload: 9a2ba3655796f772c2c0159bd6570fb896b7f95142362c29381d926f75cf8c364f927912…
  • RTX RTP 包的格式如下
Real-Time Transport Protocol
[Stream setup by HEUR RTP (frame 62)]
10.. .... = Version: RFC 1889 Version (2)
..0. .... = Padding: False
...1 .... = Extension: True
.... 0000 = Contributing source identifiers count: 0
0... .... = Marker: False
Payload type: DynamicRTP-Type-124 (124)
Sequence number: 7863
[Extended sequence number: 73399]
Timestamp: 3417198504
Synchronization Source identifier: 0x58b41246 (1488196166)
Defined by profile: Unknown (0xbede)
Extension length: 2
Header extensions
RFC 5285 Header Extension (One-Byte Header)
Identifier: 8
Length: 3
Extension Data: 6e051f
RFC 5285 Header Extension (One-Byte Header)
Identifier: 4
Length: 1
Extension Data: 30
RFC 5285 Header Extension (One-Byte Header)
Identifier: 7
Length: 1
Extension Data: 31
Payload: 9d41d0efd4d67217f916c5854544005a847a64f0936f6620873be35ba26fb2ddfe465015…
 

WebRTC 是怎么实现 RTX 的

在 WebRTC 中,主要实现在两个方面

1)接收端生成 NACK:检查 Sequence Number , 如果发现有丢包,并且在合理范围之内,就会生成 NACK 包给发送方,要求重传。

NACK 包格式参见 RFC4585#page-34

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|    1    |       205     |          length               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            PID(SN)            |             BLP               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

BLP: 是指位掩码,bit 位为1 表示这个包丢失了
( bitmask of following lost packets 16bits, bit_i=1: lost )

在 SDP 中可以指定RTX 所支持的时长, 如果没有,那么 WebRTC 在发送端会维持一个所发送包的默认的长度(1000ms )

a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96;rtx-time=3000

可以 wireshark 或其命令行工具 tshark 看看抓的 RTCP 包

tshark -r test_wireshark.pcapng -2 -R "rtcp.rtpfb.fmt==1" -T json -j "rtcp"
[
  {
    "_index": "packets-2020-12-08",
    "_type": "doc",
    "_score": null,
    "_source": {
      "layers": {
        "frame": {
          "filtered": "frame"
        },
        "eth": {
          "filtered": "eth"
        },
        "ip": {
          "filtered": "ip"
        },
        "udp": {
          "filtered": "udp"
        },
        "rtcp": {
          "rtcp.version": "2",
          "rtcp.padding": "0",
          "rtcp.rc": "1",
          "rtcp.pt": "201",
          "rtcp.length": "7",
          "rtcp.senderssrc": "0x00000001",
          "Source 1": {
            "filtered": "Source 1"
          }
        },
        "rtcp": {
          "rtcp.version": "1",
          "rtcp.padding": "1",
          "rtcp.rtpfb.fmt": "1",
          "rtcp.pt": "205",
          "rtcp.length": "9509",
          "rtcp.senderssrc": "0x1b9be51d",
          "rtcp.mediassrc": "0xae6f4f97",
          "rtcp.rtpfb.nack_pid": "13307",
          "rtcp.rtpfb.nack_blp": "0x577f",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "45998",
          "rtcp.rtpfb.nack_blp": "0xec91",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "9740",
          "rtcp.rtpfb.nack_blp": "0x8e4c",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "33552",
          "rtcp.rtpfb.nack_blp": "0xcc19",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "10036",
          "rtcp.rtpfb.nack_blp": "0x88c8",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "10803",
          "rtcp.rtpfb.nack_blp": "0xaeac",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "19214",
          "rtcp.rtpfb.nack_blp": "0xc85f",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "49044",
          "rtcp.rtpfb.nack_blp": "0x8bf8",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "64847",
          "rtcp.rtpfb.nack_blp": "0x1c10",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "9258",
          "rtcp.rtpfb.nack_blp": "0xe14f",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "40446",
          "rtcp.rtpfb.nack_blp": "0x7a6c",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "15752",
          "rtcp.rtpfb.nack_blp": "0x95f2",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "26760",
          "rtcp.rtpfb.nack_blp": "0x1d02",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          },
          "rtcp.rtpfb.nack_pid": "32768",
          "rtcp.rtpfb.nack_blp": "0x484e",
          "rtcp.rtpfb.nack_blp_tree": {
            "filtered": "rtcp.rtpfb.nack_blp"
          }
        },
        "_ws.malformed": {
          "filtered": "_ws.malformed"
        }
      }
    }
  }
]

2) 发送端处理 NACK, 并发送 RTX 包

当收到 NACK 请求时

  • OnReceivedNack
void RTPSender::OnReceivedNack(
    const std::vector<uint16_t>& nack_sequence_numbers,
    int64_t avg_rtt) {
  packet_history_->SetRtt(TimeDelta::Millis(5 + avg_rtt));
  for (uint16_t seq_no : nack_sequence_numbers) {
    const int32_t bytes_sent = ReSendPacket(seq_no);
    if (bytes_sent < 0) {
      // Failed to send one Sequence number. Give up the rest in this nack.
      RTC_LOG(LS_WARNING) << "Failed resending RTP packet " << seq_no
                          << ", Discard rest of packets.";
      break;
    }
  }
}


  • 于是,从发送历史中找到 NACK 中指明的包,构建 RTX 包以重传
nt32_t RTPSender::ReSendPacket(uint16_t packet_id) {
  int32_t packet_size = 0;
  const bool rtx = (RtxStatus() & kRtxRetransmitted) > 0;

  std::unique_ptr<RtpPacketToSend> packet =
      packet_history_->GetPacketAndMarkAsPending(
          packet_id, [&](const RtpPacketToSend& stored_packet) {
            // Check if we're overusing retransmission bitrate.
            // TODO(sprang): Add histograms for nack success or failure
            // reasons.
            packet_size = stored_packet.size();
            std::unique_ptr<RtpPacketToSend> retransmit_packet;
            if (retransmission_rate_limiter_ &&
                !retransmission_rate_limiter_->TryUseRate(packet_size)) {
              return retransmit_packet;
            }
            if (rtx) {
              retransmit_packet = BuildRtxPacket(stored_packet);
            } else {
              retransmit_packet =
                  std::make_unique<RtpPacketToSend>(stored_packet);
            }
            if (retransmit_packet) {
              retransmit_packet->set_retransmitted_sequence_number(
                  stored_packet.SequenceNumber());
            }
            return retransmit_packet;
          });
  if (packet_size == 0) {
    // Packet not found or already queued for retransmission, ignore.
    RTC_DCHECK(!packet);
    return 0;
  }
  if (!packet) {
    // Packet was found, but lambda helper above chose not to create
    // `retransmit_packet` out of it.
    return -1;
  }
  packet->set_packet_type(RtpPacketMediaType::kRetransmission);
  packet->set_fec_protect_packet(false);
  std::vector<std::unique_ptr<RtpPacketToSend>> packets;
  packets.emplace_back(std::move(packet));
  paced_sender_->EnqueuePackets(std::move(packets));

  return packet_size;
}
  • 构建 RTX 包

std::unique_ptr<RtpPacketToSend> RTPSender::BuildRtxPacket(
    const RtpPacketToSend& packet) {
  std::unique_ptr<RtpPacketToSend> rtx_packet;

  // Add original RTP header.
  {
    MutexLock lock(&send_mutex_);
    if (!sending_media_)
      return nullptr;

    RTC_DCHECK(rtx_ssrc_);

    // Replace payload type.
    auto kv = rtx_payload_type_map_.find(packet.PayloadType());
    if (kv == rtx_payload_type_map_.end())
      return nullptr;

    rtx_packet = std::make_unique<RtpPacketToSend>(&rtp_header_extension_map_,
                                                   max_packet_size_);

    rtx_packet->SetPayloadType(kv->second);

    // Replace SSRC.
    rtx_packet->SetSsrc(*rtx_ssrc_);

    CopyHeaderAndExtensionsToRtxPacket(packet, rtx_packet.get());

    // RTX packets are sent on an SSRC different from the main media, so the
    // decision to attach MID and/or RRID header extensions is completely
    // separate from that of the main media SSRC.
    //
    // Note that RTX packets must used the RepairedRtpStreamId (RRID) header
    // extension instead of the RtpStreamId (RID) header extension even though
    // the payload is identical.
    if (always_send_mid_and_rid_ || !rtx_ssrc_has_acked_) {
      // These are no-ops if the corresponding header extension is not
      // registered.
      if (!mid_.empty()) {
        rtx_packet->SetExtension<RtpMid>(mid_);
      }
      if (!rid_.empty()) {
        rtx_packet->SetExtension<RepairedRtpStreamId>(rid_);
      }
    }
  }
  RTC_DCHECK(rtx_packet);

  uint8_t* rtx_payload =
      rtx_packet->AllocatePayload(packet.payload_size() + kRtxHeaderSize);
  if (rtx_payload == nullptr)
    return nullptr;

  // Add OSN (original sequence number).
  ByteWriter<uint16_t>::WriteBigEndian(rtx_payload, packet.SequenceNumber());

  // Add original payload data.
  auto payload = packet.payload();
  memcpy(rtx_payload + kRtxHeaderSize, payload.data(), payload.size());

  // Add original additional data.
  rtx_packet->set_additional_data(packet.additional_data());

  // Copy capture time so e.g. TransmissionOffset is correctly set.
  rtx_packet->set_capture_time(packet.capture_time());

  return rtx_packet;
}

static void CopyHeaderAndExtensionsToRtxPacket(const RtpPacketToSend& packet,
                                               RtpPacketToSend* rtx_packet) {
  // Set the relevant fixed packet headers. The following are not set:
  // * Payload type - it is replaced in rtx packets.
  // * Sequence number - RTX has a separate sequence numbering.
  // * SSRC - RTX stream has its own SSRC.
  rtx_packet->SetMarker(packet.Marker());
  rtx_packet->SetTimestamp(packet.Timestamp());

  // Set the variable fields in the packet header:
  // * CSRCs - must be set before header extensions.
  // * Header extensions - replace Rid header with RepairedRid header.
  const std::vector<uint32_t> csrcs = packet.Csrcs();
  rtx_packet->SetCsrcs(csrcs);
  for (int extension_num = kRtpExtensionNone + 1;
       extension_num < kRtpExtensionNumberOfExtensions; ++extension_num) {
    auto extension = static_cast<RTPExtensionType>(extension_num);

    // Stream ID header extensions (MID, RSID) are sent per-SSRC. Since RTX
    // operates on a different SSRC, the presence and values of these header
    // extensions should be determined separately and not blindly copied.
    if (extension == kRtpExtensionMid ||
        extension == kRtpExtensionRtpStreamId) {
      continue;
    }

    // Empty extensions should be supported, so not checking `source.empty()`.
    if (!packet.HasExtension(extension)) {
      continue;
    }

    rtc::ArrayView<const uint8_t> source = packet.FindExtension(extension);

    rtc::ArrayView<uint8_t> destination =
        rtx_packet->AllocateExtension(extension, source.size());

    // Could happen if any:
    // 1. Extension has 0 length.
    // 2. Extension is not registered in destination.
    // 3. Allocating extension in destination failed.
    if (destination.empty() || source.size() != destination.size()) {
      continue;
    }

    std::memcpy(destination.begin(), source.begin(), destination.size());
  }
}

3) 接收端收 RTX packet,重新构建 media packet , 找到对应的 media stream ,放入其接收缓冲

void RtxReceiveStream::OnRtpPacket(const RtpPacketReceived& rtx_packet) {
  RTC_DCHECK_RUN_ON(&packet_checker_);
  if (rtp_receive_statistics_) {
    rtp_receive_statistics_->OnRtpPacket(rtx_packet);
  }
  rtc::ArrayView<const uint8_t> payload = rtx_packet.payload();

  if (payload.size() < kRtxHeaderSize) {
    return;
  }

  auto it = associated_payload_types_.find(rtx_packet.PayloadType());
  if (it == associated_payload_types_.end()) {
    RTC_DLOG(LS_VERBOSE) << "Unknown payload type "
                         << static_cast<int>(rtx_packet.PayloadType())
                         << " on rtx ssrc " << rtx_packet.Ssrc();
    return;
  }
  RtpPacketReceived media_packet;
  media_packet.CopyHeaderFrom(rtx_packet);

  media_packet.SetSsrc(media_ssrc_);
  media_packet.SetSequenceNumber((payload[0] << 8) + payload[1]);
  media_packet.SetPayloadType(it->second);
  media_packet.set_recovered(true);
  media_packet.set_arrival_time(rtx_packet.arrival_time());

  // Skip the RTX header.
  rtc::ArrayView<const uint8_t> rtx_payload = payload.subview(kRtxHeaderSize);

  uint8_t* media_payload = media_packet.AllocatePayload(rtx_payload.size());
  RTC_DCHECK(media_payload != nullptr);

  memcpy(media_payload, rtx_payload.data(), rtx_payload.size());

  media_sink_->OnRtpPacket(media_packet);
}

存在问题

现在 WebRTC Library 对于 rrid(RepairedRtpStreamId) 的支持并不完善,仍然需要用 SSRC-Group 来指明 RTX stream 所使用的 SSRC , 然后才能进行丢包恢复,参见 Issue 10297: RTX does not work if SSRCs are not negotiated

另外一个问题 Issue 13896: Failure to re-send packet via RTX due to additional space required for MID/RRID header extension,在发送RTX 包时,如果包括的媒体包比较大(例如关键帧)时会失败,原因是没有分配足够的空间来放置 MID/RRID 头。

参考资料



本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容