WebRTC中PacedSender工作原理和代码分析

摘抄一段 PacedSender 简介,下面的链接对该模块的工作原理做了详细的介绍,今天大致看了下这个模块的代码,记录一下

在estimator根据网络状态决策出新的通信码率(target bitrate),它会将这个码率设置到pacer当中,要求pacer按照新的码率来计算发包频率。因为在视频通信中,单帧视频可能有上百KB,如果是当视频帧被编码器编码出来后,就立即进行RTP打包发送,瞬时会发送大量的数据到网络上,可能会引起网络衰减和通信恶化。WebRTC引入pacer,pacer会根据estimator评估出来的码率,按照最小单位时间(5ms)做时间分片进行递进发送数据,避免瞬时对网络的冲击。pacer的目的就是让视频数据按照评估码率均匀的分布在各个时间片里发送, 所以在弱网的WiFi环境,pacer是个非常重要的关键步骤

via: http://livevideostack.com/portal.php?mod=view&aid=201

PacedSender 发送速率设置

GCC 估测的带宽只会通过 SetEstimatedBitrate 方法设置到 PacedSender 中, pacing_bitrate_kbps_PacedSender 发送媒体包的速率,为GCC估测带宽 乘以了固定系数 kDefaultPaceMultiplier(2.5)

void PacedSender::SetEstimatedBitrate(uint32_t bitrate_bps) {
  if (bitrate_bps == 0)
    LOG(LS_ERROR) << "PacedSender is not designed to handle 0 bitrate.";
  CriticalSectionScoped cs(critsect_.get());
  estimated_bitrate_bps_ = bitrate_bps;
  padding_budget_->set_target_rate_kbps(estimated_bitrate_bps_ / 1000 );

  // 更新 pacing 发送速率,为 estimated_bitrate_bps_/1000 * 2.5;
  pacing_bitrate_kbps_ =
      max(min_send_bitrate_kbps_, estimated_bitrate_bps_ / 1000) *
      kDefaultPaceMultiplier;
  alr_detector_->SetEstimatedBitrate(bitrate_bps);
}

PacedSender 包缓存队列

该方法由 rtp_sender 模块调用,将封装好的视频rtp包的元信息,如 ssrc, sequence_number等封装成Packet数据结构存储到队列中,并未缓存真正的媒体数据。发包时,PacedSender 会通过这些元信息,在rtp_sender中的缓存队列中找到对应的媒体包数据

// 将视频包元信息,instert到pacer中
void PacedSender::InsertPacket(RtpPacketSender::Priority priority,
                               uint32_t ssrc,
                               uint16_t sequence_number,
                               int64_t capture_time_ms,
                               size_t bytes,
                               bool retransmission) {
  CriticalSectionScoped cs(critsect_.get());
  DCHECK(estimated_bitrate_bps_ > 0)
        << "SetEstimatedBitrate must be called before InsertPacket.";

  int64_t now_ms = clock_->TimeInMilliseconds();
  prober_->OnIncomingPacket(bytes);

  if (capture_time_ms < 0)
    capture_time_ms = now_ms;

  // 封装 packet 包,放到list中 packets_
  packets_->Push(paced_sender::Packet(priority, ssrc, sequence_number,
                                      capture_time_ms, now_ms, bytes,
                                      retransmission, packet_counter_++));
}

发包线程

线程发包间隔 5ms 计算

PacedSender 发送线程是调用时间间隔为5ms,为什么是5ms, 通过模块中下面的方法计算的:

// 计算线程调用的时间间隔,即下面 process() 方法调用的时间间隔 !!!!
int64_t PacedSender::TimeUntilNextProcess() {
  CriticalSectionScoped cs(critsect_.get());
    //  如果正在探测
    if (prober_->IsProbing()) {
    int64_t ret = prober_->TimeUntilNextProbe(clock_->TimeInMilliseconds());
    if (ret > 0 || (ret == 0 && !probing_send_failure_))
      return ret;
  }
  // 当前时间 减去 上一次 process()方法调用的时间,得出时间间隔
  int64_t elapsed_time_us = clock_->TimeInMicroseconds() - time_last_update_us_;
  int64_t elapsed_time_ms = (elapsed_time_us + 500) / 1000;
    
  // kMinPacketLimitMs:5ms,即 process() 方法 5m 处理一次
  return std::max<int64_t>(kMinPacketLimitMs - elapsed_time_ms, 0);
}

kMinPacketLimitMs 的值为固定 5ms, elapsed_time_ms从代码中可以看出,为上一次调用 process到现在的时间间隔,两个的时间差即为线程要等待的时长,最大5ms,之后线程会再次调用process 方法发送媒体数据。

线程处理方法 process()

下面的 process() 方法中处理真正发包逻辑, 每次进入 process 都会调用 UpdateBudgetWithElapsedTime(elapsed_time_ms), 去更新(增加) media_budget_中的 bytes_remaining_ ,

bytes_remaining_ 为每次可以发送包的最大字节数:

发包量 = Δt x GCC反馈带宽 x 2.5

Δt 即elapsed_time_ms, 最大 5ms。 在下面的while 循环中,只要包队列不为空,就一直尝试用SendPacket去发包。

int32_t PacedSender::Process() {
  int64_t now_us = clock_->TimeInMicroseconds();
  CriticalSectionScoped cs(critsect_.get());
    // elapsed_time_ms: 上一次process和这次process 之间的时间间隔 elapsed_time_ms  ms
  int64_t elapsed_time_ms = (now_us - time_last_update_us_ + 500) / 1000;
  time_last_update_us_ = now_us;
  int target_bitrate_kbps = pacing_bitrate_kbps_; // 拿到最新的 pacer 发送速率
  if (!paused_ && elapsed_time_ms > 0) {
    // 缓存的所有的包的大小,按字节算
    size_t queue_size_bytes = packets_->SizeInBytes();
    if (queue_size_bytes > 0) {
      // Assuming equal size packets and input/output rate, the average packet
      // has avg_time_left_ms left to get queue_size_bytes out of the queue, if
      // time constraint shall be met. Determine bitrate needed for that.
      packets_->UpdateQueueTime(clock_->TimeInMilliseconds());
      int64_t avg_time_left_ms = std::max<int64_t>(
          1, kMaxQueueLengthMs - packets_->AverageQueueTimeMs()); // 这个地方应该是 已经缓存包时长 = 最大缓存时长 - 剩余队列时长
      int min_bitrate_needed_kbps =
          static_cast<int>(queue_size_bytes * 8 / avg_time_left_ms);
      if (min_bitrate_needed_kbps > target_bitrate_kbps)
        target_bitrate_kbps = min_bitrate_needed_kbps; // 如果计算出来的发送速率比上面传入的 目标发送码率大,则更新目标发送码率
    }
    //  media budget 更新发送速率
    media_budget_->set_target_rate_kbps(target_bitrate_kbps);
    // 最大时间间隔 kMaxIntervalTimeMs = 30 ms
    elapsed_time_ms = min(kMaxIntervalTimeMs, elapsed_time_ms);
    UpdateBudgetWithElapsedTime(elapsed_time_ms);
  }

  bool is_probing = prober_->IsProbing();
  PacedPacketInfo pacing_info;
  size_t bytes_sent = 0;
  size_t recommended_probe_size = 0;
  if (is_probing) {
    pacing_info = prober_->CurrentCluster();
    recommended_probe_size = prober_->RecommendedMinProbeSize();
  }
    
  // 队列不为空的情况下
  while (!packets_->Empty()) {
    // Since we need to release the lock in order to send, we first pop the
    // element from the priority queue but keep it in storage, so that we can
    // reinsert it if send fails.
    const paced_sender::Packet& packet = packets_->BeginPop();
    // 去除 队列中的包,并开始发送
    if (SendPacket(packet, pacing_info)) {
      // Send succeeded, remove it from the queue.
      // 发送成功,则记录发送的字节数,并从队列中将包清除
      bytes_sent += packet.bytes;
      packets_->FinalizePop(packet);
//        探测期间,或发送字节数超过探测的数值了,则跳出循环不再发送   这个地方和探测有关,是否有问题???
      if (is_probing && bytes_sent > recommended_probe_size)
        break;
    } else {
      // Send failed, put it back into the queue.
      packets_->CancelPop(packet);
      break;
    }
  }

  if (packets_->Empty() && !paused_) {
    // We can not send padding unless a normal packet has first been sent. If we
    // do, timestamps get messed up.
    if (packet_counter_ > 0) {
    // 计算是否应该发送 padding 包
    int padding_needed = static_cast<int>(is_probing ? (recommended_probe_size - bytes_sent) : padding_budget_->bytes_remaining());
        
        if (padding_needed > 0) {
          bytes_sent += SendPadding(padding_needed, pacing_info);
        }
    }
  }
    
  if (is_probing) {
    probing_send_failure_ = bytes_sent == 0;
    if (!probing_send_failure_)
      prober_->ProbeSent(clock_->TimeInMilliseconds(), bytes_sent);
  }
  alr_detector_->OnBytesSent(bytes_sent, now_us / 1000);

  return 0;
}

发包方法SendPacket()

该方法由上面的while发包循环调用,发包后会调用 UpdateBudgetWithBytesSent(packet.bytes)media_budget_ 减去packet.bytes长度的发包预算, 当发博包循环走几次之后,media_budget中的预算长度被消耗完,即 <= 0, 此时 media_budget_->bytes_remaining() 方法会做 max(0, bytes_remaining_) 处理,即返回0 ,而发包前会判断 media_budget_->bytes_remaining() == 0 ,满足条件就return false不发了。

bool PacedSender::SendPacket(const paced_sender::Packet& packet,
                             const PacedPacketInfo& pacing_info) {
    // 是否暂停发包
    if (paused_)
    return false;
    
    
    //  media  budget 剩余预算字节数为 0,停止发包
  if (media_budget_->bytes_remaining() == 0 &&
      pacing_info.probe_cluster_id == PacedPacketInfo::kNotAProbe) {
    return false;
  }

  critsect_->Enter();
  const bool success = packet_sender_->TimeToSendPacket(
      packet.ssrc, packet.sequence_number, packet.capture_time_ms,
      packet.retransmission, pacing_info);
  critsect_->Leave();

  if (success) {
    // TODO(holmer): High priority packets should only be accounted for if we
    // are allocating bandwidth for audio.
    if (packet.priority != kHighPriority) { // 包的优先级不为最高优先级,更新发送的字节数
      // Update media bytes sent.
      UpdateBudgetWithBytesSent(packet.bytes);
    }
  }

  return success;
}

SendPacket 方法最终会调用 rtp_sender中的方法,将ssrcsequence_number 等参数传递过去,rtp_sender通过这些值找到真正的视频媒体包,最终发送到到网络上。

media_budget简介

media_budget 是在 PacedSender中封装的一个类,全部代码如下,注释做了解释:

class IntervalBudget {
 public:
  explicit IntervalBudget(int initial_target_rate_kbps)
      : target_rate_kbps_(initial_target_rate_kbps),
        bytes_remaining_(0) {}

  void set_target_rate_kbps(int target_rate_kbps) {
    //更新发送速率
    target_rate_kbps_ = target_rate_kbps;
      bytes_remaining_ =
        max(-kWindowMs * target_rate_kbps_ / 8, bytes_remaining_);
  }

  void IncreaseBudget(int64_t delta_time_ms) {
    // 估计在 delta 时间, 在带宽为 target_rate_kbps 的情况可以发送出去多少字节
    int64_t bytes = target_rate_kbps_ * delta_time_ms / 8;
      
    if (bytes_remaining_ < 0) {
      // We overused last interval, compensate this interval.
      bytes_remaining_ = bytes_remaining_ + bytes;
    } else {
      // If we underused last interval we can't use it this interval.
      bytes_remaining_ = bytes;
    }
  }

  //更新实际发送的字节数, 从bytes_remaining_减去
  void UseBudget(size_t bytes) {
    bytes_remaining_ = max(bytes_remaining_ - static_cast<int>(bytes),
                                -kWindowMs * target_rate_kbps_ / 8);
  }
  // 几次发送循环后,发送的总字节数大于开始的 bytes_remaining_,bytes_remaining_ <= 0,改方法返回0
  size_t bytes_remaining() const {
    return static_cast<size_t>(max(0, bytes_remaining_));
  }

  int target_rate_kbps() const { return target_rate_kbps_; }

 private:
  static const int kWindowMs = 500; // window 500 ms

  int target_rate_kbps_;
  int bytes_remaining_;
};

PacedSender 原理总结

到这可以知道PacedSender工作原理了, 每次发包前会更新media_budget中预算bytes_remaining_ 的大小,而每次发送时间(<= 5ms)内最多发送 bytes_remaining_ 字节数,从而达到限制和平滑带宽的目的,PacedSenderpadding发送的原理和此类似。

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

推荐阅读更多精彩内容