流媒体弱网优化之路(WebRTC)——jitterbuffer分析与优化

流媒体弱网优化之路(WebRTC)——jitterbuffer分析与优化

——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost

目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。

欢迎大家使用
——


一、jitterbuffer原理

  WebRTC中的Jitter Buffer是一个用于处理网络抖动(jitter)的缓冲区,它的作用是为音频或视频数据提供一个平滑的播放体验。当我们在网络上传输音频或视频数据时,由于网络传输延迟和不可预测的网络抖动,数据包很可能会以不同的速度到达接收端。这可能会导致数据包在接收端的播放速度不一致,从而影响音频或视频的质量和连续性。Jitter Buffer的原理是将接收到的数据包缓存到一个缓冲区中,在缓冲区中等待一段时间,以便其他数据包到达。然后,Jitter Buffer会按照正确的顺序将数据包发送到音频或视频播放器,以确保数据包按照正确的速率播放。

1.1 组包流程

  在详细介绍jitterbuffer前我们先把整体组包的流程解释一下:
  1.当接收到Rtp包后第一时间会进入到PacketBuffer中进行组帧,并对外提供FindFrame函数对外查找组成的帧。同时还会统计丢包的数据;(序号排序)
在这里插入图片描述
  2.当接收到Frame后需要RtpFrameReferenceFinder查找整体的帧参考关系,并且会根据数据整体的状态:kStash、kHandOff、kDrop几个状态确定是否丢弃该帧;(帧排序)
在这里插入图片描述
  3.上述的方式保证了Gop内部的数据帧之间的有序性,但每个Gop之间则没确定。而FrameBuffer则保证每个帧的Gop顺序的输出至解码器。(Gop排序)
在这里插入图片描述
  调用流程:

// src/video/rtp_video_stream_receiver2
RtpVideoStreamReceiver2::OnReceivedPayloadData;
//				|
PacketBuffer::InsertPacket;
//				|
PacketBuffer::FindFrames;
//				|
RtpVideoStreamReceiver2::OnInsertedPacket;
//				|
RtpVideoStreamReceiver2::OnAssembledFrame;
//				|
RtpFrameReferenceFinder::ManageFrame;
//				|
 VideoReceiveStream2::OnCompleteFrame; // video_receivestream2是rtp_video_stream_receiver2的回调complete_frame_callback_
//				|
FrameBuffer2::InsertFrame;

1.2 解码与渲染

  上诉取帧完成后,进入到解码流程。
  进入到VideoReceiver2进行解码,具体的流程如下:

FrameBuffer::StartWaitForNextFrameOnQueue;
//				|
VideoReceiveStream2::StartNextDecode;
//				|
VideoReceiveStream2::HandleEncodedFrame;
//				|
VideoReceiveStream2::DecodeAndMaybeDispatchEncodedFrame;
//				|
VideoReceiver2::Decode;
//				|
VCMGenericDecoder::Decode;
//				|
 H264DecoderImpl::Decode;
//				|
VCMDecodedFrameCallback::Decoded;
//				|
VideoStreamDecoder::FrameToRender;
//				|
IncomingVideoStream::OnFrame;
//				| ——  incoming_render_queue_.PostTask // 投入渲染队列
 

1.3 timing 与 jitter

  上述已经是整个数据包从接收到渲染的整体流程,但生产环境中我们整体的网络抖动会导致我们该流程中某一流程的阻塞就造成整个画面的卡顿。如果想要让画面更加的流畅我们必须提前感知到整个流程是否存在卡的可能,并且使用延迟换取流程,适当地增大整个渲染延迟。

1.3.1 jitter buffer

  关键参数:
  1.VCMInterFrameDelay:帧间延迟 = 前后帧接收时间差 - 前后帧发送时间差;
  2.VCMJitterEstimator:通过帧延迟计算出抖动值;
在这里插入图片描述

1.3.2 timing

  timing是解码完成后确定渲染时间戳的关键类,该类会结合jitterbuffer测算的jitter情况、解码消耗时间、渲染耗时等等计算得到一个可靠的渲染延迟。

获取函数意义
TargetDelayInternal()此函数返回目标延迟,目标延迟是希望获得的延迟,但并不是最终的延迟
RenderTimeInternal()此函数返回一个平滑后的延迟,由于延迟采样没有经过线性归一,因此波动会很大需要进行卡尔曼滤波
RequiredDecodeTime()此函数返回解码耗时,解码耗时

  current_delay_是最终使用的延迟值。

  // 渲染延迟 固定值10ms
  void set_render_delay(TimeDelta render_delay);

  // jitter延迟
  void SetJitterDelay(TimeDelta required_delay);

  // 最小播放延迟
  void set_min_playout_delay(TimeDelta min_playout_delay);
  
  // 最大播放延迟
  void set_max_playout_delay(TimeDelta max_playout_delay);

  // 直接更新当前延迟
  void UpdateCurrentDelay(uint32_t frame_timestamp);
  void UpdateCurrentDelay(Timestamp render_time, Timestamp actual_decode_time);

二、jitterbuffer的实现

  jitterbuffer插入数据实现。

VCMFrameBufferEnum VCMJitterBuffer::InsertPacket(const VCMPacket& packet,
                                                 bool* retransmitted) {
  MutexLock lock(&mutex_);

  ++num_packets_;
  // 待插入的packet更老
  // Does this packet belong to an old frame?
  if (last_decoded_state_.IsOldPacket(&packet)) {
    // Account only for media packets.
    if (packet.sizeBytes > 0) {
      // 累积老包个数
      num_consecutive_old_packets_++;
    }
    // 此包晚收到,并且属于同时间的一帧,更新last_decoded_state_->sequence_num_
    // Update last decoded sequence number if the packet arrived late and
    // belongs to a frame with a timestamp equal to the last decoded
    // timestamp.
    last_decoded_state_.UpdateOldPacket(&packet);
    DropPacketsFromNackList(last_decoded_state_.sequence_num());

    // Also see if this old packet made more incomplete frames continuous.
    FindAndInsertContinuousFramesWithState(last_decoded_state_);

    // 连续收到老包的个数太多,则flush jitter buffer
    if (num_consecutive_old_packets_ > kMaxConsecutiveOldPackets) {
      RTC_LOG(LS_WARNING)
          << num_consecutive_old_packets_
          << " consecutive old packets received. Flushing the jitter buffer.";
      Flush();
      return kFlushIndicator;
    }
    // 返回老包标示
    return kOldPacket;
  }

  // 收到了更新的包则重置此值
  num_consecutive_old_packets_ = 0;

  VCMFrameBuffer* frame;
  FrameList* frame_list;
  // 获取packet对应的帧,如果不存在则获取一个空帧
  const VCMFrameBufferEnum error = GetFrame(packet, &frame, &frame_list);
  if (error != kNoError)
    return error;

  // 记录当前包插入时间
  int64_t now_ms = clock_->TimeInMilliseconds();
  // 第一个包已经插入成功后,开始计算帧间延迟
  // We are keeping track of the first and latest seq numbers, and
  // the number of wraps to be able to calculate how many packets we expect.
  if (first_packet_since_reset_) {
    // Now it's time to start estimating jitter
    // reset the delay estimate.
    inter_frame_delay_.Reset(now_ms);
  }

  // 空包不计入jitter estimate
  // Empty packets may bias the jitter estimate (lacking size component),
  // therefore don't let empty packet trigger the following updates:
  if (packet.video_header.frame_type != VideoFrameType::kEmptyFrame) {
    // packet属于同一帧
    if (waiting_for_completion_.timestamp == packet.timestamp) {
      // This can get bad if we have a lot of duplicate packets,
      // we will then count some packet multiple times.
      waiting_for_completion_.frame_size += packet.sizeBytes;
      waiting_for_completion_.latest_packet_time = now_ms;
    } else if (waiting_for_completion_.latest_packet_time >= 0 &&
               waiting_for_completion_.latest_packet_time + 2000 <= now_ms) {
      // 如果当前包不属于正在等待完整的帧且等待完整帧超时了,重置waiting_for_completion_
      // A packet should never be more than two seconds late
      UpdateJitterEstimate(waiting_for_completion_, true);
      waiting_for_completion_.latest_packet_time = -1;
      waiting_for_completion_.frame_size = 0;
      waiting_for_completion_.timestamp = 0;
    }
  }

  // 保存帧的当前状态
  VCMFrameBufferStateEnum previous_state = frame->GetState();
  // Insert packet.
  FrameData frame_data;
  frame_data.rtt_ms = kDefaultRtt;
  frame_data.rolling_average_packets_per_frame = average_packets_per_frame_;
  // 插入packet到对应的frame
  VCMFrameBufferEnum buffer_state =
      frame->InsertPacket(packet, now_ms, frame_data);

  // 插入包未发生错误
  if (buffer_state > 0) {
    if (first_packet_since_reset_) {
      latest_received_sequence_number_ = packet.seqNum;
      first_packet_since_reset_ = false;
    } else {
      // 是重传包,增加帧nack计数
      if (IsPacketRetransmitted(packet)) {
        frame->IncrementNackCount();
      }
      // 如果更新nack列表失败且当前包不是关键帧,需要flush
      if (!UpdateNackList(packet.seqNum) &&
          packet.video_header.frame_type != VideoFrameType::kVideoFrameKey) {
        buffer_state = kFlushIndicator;
      }

      // 更新最新接收到的包序列号
      latest_received_sequence_number_ =
          LatestSequenceNumber(latest_received_sequence_number_, packet.seqNum);
    }
  }

  // frame是否连续(即本身已经连续或者跟decodable_frames_内某一帧比连续)
  // Is the frame already in the decodable list?
  bool continuous = IsContinuous(*frame);
  switch (buffer_state) {
    case kGeneralError:
    case kTimeStampError:
    case kSizeError: {
      // 发生错误回收frame
      RecycleFrameBuffer(frame);
      break;
    }
    case kCompleteSession: {
      // 只通知一次frame完整
      if (previous_state != kStateComplete) {
        if (continuous) {
          // 触发通知连续完整帧事件
          // Signal that we have a complete session.
          frame_event_->Set();
        }
      }

      *retransmitted = (frame->GetNackCount() > 0);
      if (continuous) {
        // 连续则插入到decodable_frames_
        decodable_frames_.InsertFrame(frame);
        FindAndInsertContinuousFrames(*frame);
      } else {
        // 不连续则插入未完成帧列表
        incomplete_frames_.InsertFrame(frame);
      }
      break;
    }
    case kIncomplete: {
      // 如果是空帧,更新空帧解码状态
      if (frame->GetState() == kStateEmpty &&
          last_decoded_state_.UpdateEmptyFrame(frame)) {
        // 回收空帧(比如padding帧)
        RecycleFrameBuffer(frame);
        return kNoError;
      } else {
        // 媒体帧则插入未完整列表
        incomplete_frames_.InsertFrame(frame);
      }
      break;
    }
    case kNoError:
    case kOutOfBoundsPacket:
    case kDuplicatePacket: {
      // Put back the frame where it came from.
      if (frame_list != NULL) {
        // 如果帧属于incomplete或者decodable列表,则放回到其中
        frame_list->InsertFrame(frame);
      } else {
        // 回收帧
        RecycleFrameBuffer(frame);
      }
      ++num_duplicated_packets_;
      break;
    }
    case kFlushIndicator:
      // 回收帧并请求flush
      RecycleFrameBuffer(frame);
      return kFlushIndicator;
    default:
      assert(false);
  }
  return buffer_state;
}

  其中后使用到jitter_estimate_是一个对jitter做估计的类。

// Updates the estimates with the new measurements.
void JitterEstimator::UpdateEstimate(TimeDelta frame_delay,
                                     DataSize frame_size) {
  if (frame_size.IsZero()) {
    return;
  }
  // Can't use DataSize since this can be negative.
  double delta_frame_bytes =
      frame_size.bytes() - prev_frame_size_.value_or(DataSize::Zero()).bytes();
  if (frame_size_count_ < kFsAccuStartupSamples) {
    frame_size_sum_ += frame_size;
    frame_size_count_++;
  } else if (frame_size_count_ == kFsAccuStartupSamples) {
    // Give the frame size filter.
    avg_frame_size_ = frame_size_sum_ / static_cast<double>(frame_size_count_);
    frame_size_count_++;
  }

  DataSize avg_frame_size = kPhi * avg_frame_size_ + (1 - kPhi) * frame_size;
  DataSize deviation_size = DataSize::Bytes(2 * sqrt(var_frame_size_));
  if (frame_size < avg_frame_size_ + deviation_size) {
    // Only update the average frame size if this sample wasn't a key frame.
    avg_frame_size_ = avg_frame_size;
  }

  double delta_bytes = frame_size.bytes() - avg_frame_size.bytes();
  var_frame_size_ = std::max(
      kPhi * var_frame_size_ + (1 - kPhi) * (delta_bytes * delta_bytes), 1.0);

  // Update max_frame_size_ estimate.
  max_frame_size_ = std::max(kPsi * max_frame_size_, frame_size);

  if (!prev_frame_size_) {
    prev_frame_size_ = frame_size;
    return;
  }
  prev_frame_size_ = frame_size;

  // Cap frame_delay based on the current time deviation noise.
  TimeDelta max_time_deviation =
      TimeDelta::Millis(time_deviation_upper_bound_ * sqrt(var_noise_) + 0.5);
  frame_delay.Clamp(-max_time_deviation, max_time_deviation);

  // Only update the Kalman filter if the sample is not considered an extreme
  // outlier. Even if it is an extreme outlier from a delay point of view, if
  // the frame size also is large the deviation is probably due to an incorrect
  // line slope.
  double deviation = DeviationFromExpectedDelay(frame_delay, delta_frame_bytes);

  if (fabs(deviation) < kNumStdDevDelayOutlier * sqrt(var_noise_) ||
      frame_size.bytes() >
          avg_frame_size_.bytes() +
              kNumStdDevFrameSizeOutlier * sqrt(var_frame_size_)) {
    // Update the variance of the deviation from the line given by the Kalman
    // filter.
    EstimateRandomJitter(deviation);
    // Prevent updating with frames which have been congested by a large frame,
    // and therefore arrives almost at the same time as that frame.
    // This can occur when we receive a large frame (key frame) which has been
    // delayed. The next frame is of normal size (delta frame), and thus deltaFS
    // will be << 0. This removes all frame samples which arrives after a key
    // frame.
    if (delta_frame_bytes > -0.25 * max_frame_size_.bytes()) {
      // Update the Kalman filter with the new data
      KalmanEstimateChannel(frame_delay, delta_frame_bytes);
    }
  } else {
    int nStdDev =
        (deviation >= 0) ? kNumStdDevDelayOutlier : -kNumStdDevDelayOutlier;
    EstimateRandomJitter(nStdDev * sqrt(var_noise_));
  }
  // Post process the total estimated jitter
  if (startup_count_ >= kStartupDelaySamples) {
    PostProcessEstimate();
  } else {
    startup_count_++;
  }
}

// 获取jitter estimate网络估计延迟时间
uint32_t VCMJitterBuffer::EstimatedJitterMs() {
  MutexLock lock(&mutex_);
  const double rtt_mult = 1.0f;
  return jitter_estimate_.GetJitterEstimate(rtt_mult, absl::nullopt);
}

  上面的JitterEstimator逻辑需要好好解释一下:
  造成的延迟抖动的情况分为:大帧接收慢、网络抖动等问题。
  1.大帧分片:一般720p的I帧可能会有30 ~ 50个rtp数据包,而一般的p帧只有 几个包,也就是jitter的变化会出现10倍的突变,因此而造成抖动;
  2.网络抖动:可知组帧需要多个rtp数据包,其中只要有一个数据包缺失我们就需要进行nack重传进行补包,中间会消耗至少一个rtt的时间进行修复,也会造成比较明显的抖动。因此我们可以看到,jitter计算中会参考rtt的数据:


// Returns the current filtered estimate if available,
// otherwise tries to calculate an estimate.
TimeDelta JitterEstimator::GetJitterEstimate(
    double rtt_multiplier,
    absl::optional<TimeDelta> rtt_mult_add_cap) {
  TimeDelta jitter = CalculateEstimate() + OPERATING_SYSTEM_JITTER;
  Timestamp now = clock_->CurrentTime();
  if (now - latest_nack_ > kNackCountTimeout)
    nack_count_ = 0;

  if (filter_jitter_estimate_ > jitter)
    jitter = filter_jitter_estimate_;
  if (nack_count_ >= kNackLimit) {
    if (rtt_mult_add_cap.has_value()) {
      jitter += std::min(rtt_filter_.Rtt() * rtt_multiplier,
                         rtt_mult_add_cap.value());
    } else {
      jitter += rtt_filter_.Rtt() * rtt_multiplier;
    }
  }

  if (enable_reduced_delay_) {
    static const Frequency kJitterScaleLowThreshold = Frequency::Hertz(5);
    static const Frequency kJitterScaleHighThreshold = Frequency::Hertz(10);
    Frequency fps = GetFrameRate();
    // Ignore jitter for very low fps streams.
    if (fps < kJitterScaleLowThreshold) {
      if (fps.IsZero()) {
        return std::max(TimeDelta::Zero(), jitter);
      }
      return TimeDelta::Zero();
    }

    // Semi-low frame rate; scale by factor linearly interpolated from 0.0 at
    // kJitterScaleLowThreshold to 1.0 at kJitterScaleHighThreshold.
    if (fps < kJitterScaleHighThreshold) {
      jitter = (1.0 / (kJitterScaleHighThreshold - kJitterScaleLowThreshold)) *
               (fps - kJitterScaleLowThreshold) * jitter;
    }
  }

  return std::max(TimeDelta::Zero(), jitter);
}

三、乱序反应与丢包jitter

  上面给大家详细介绍了jitterbuffer部分的实现,可知jitterbuffer在工作的过程中是依赖于rtt统计的,同时jitter的大帧延迟进行了卡尔曼滤波之后,同样会作用到我们的渲染延迟中。
  这样的实现在P2P的形式下工作会比较良好,但在当前国内部分厂商的应用场景下会出现一个无法完美解决的问题,同时jitterbuffer也不完美存在这些应用中的问题:
  1.高丢包剧烈变化的场景下卡顿会比较频繁的出现(例如:0 丢包 ~ 70 丢包);
  2.经过服务器中转时,上行丢包对下行jitter动态调整效果不佳。

3.1 突发高丢包卡顿原因

  我们可知整体的组包在于 packet buffer 部分,当数据包出现大面积丢包时,在packet buffer部分最明显的表现就是组成完整帧的能力变慢了,而后续 frame finder 的帧排序,frame buffer 的Gop排序都依赖于前面的数据包,在突变的过程中 jitterbuffer 会出现较明显的延迟增加不足,因此卡顿会非常明显。

  解决方式:根据乱序,固定增加render延迟;

3.2 上行丢包卡顿

  我们的jitterbuffer调整jitter延迟是依赖于rtt的,在P2P模式下,rtt就是两个端交互的真实值。但在带有sfu服务器的模式下,上行的rtt不会反馈到下行的rtt上,因此我们整体的jitter计算会出现偏低的情况。

  解决方式:统计端到端延迟,根据端到端延迟以及乱序度动态增加渲染延迟

四、总结

  本文介绍了jitterbuffer的一些实现逻辑和代码实现,并且针对jitterbuffer在sfu模式下使用的一些问题提出了一些新的解决方案。根据上述的解决方案进行调整,我们在70%随机丢包、无带宽限制、无延迟增加的情况下,实现了1.5s延迟且在弱网切换瞬间几乎无感知的播放平滑。也是使用了最传统的延迟换卡顿的策略,感谢大家阅读。

### 回答1: WebRTC抖动缓冲器是一种用于处理网络延迟和抖动的技术。它可以在音频和视频传输过程中缓存数据包,以确保数据包按照正确的顺序到达接收方,并且可以在需要时进行重传。抖动缓冲器可以帮助提高音频和视频的质量,减少延迟和丢包率,从而提高用户体验。 ### 回答2: WebRTC jitterbuffer是一种用于优化WebRTC音频和视频流的技术。在网络通信中,数据包的传输不可避免地会出现延迟,这可能导致音频和视频流的中断和卡顿。为了解决这个问题,WebRTC jitterbuffer使用一种缓冲机制,以平衡延迟和数据传输的完整性。 WebRTC jitterbuffer接收来自网络的音频和视频帧,并通过调整它们的播放时间来平衡延迟。如果网络不稳定或拥塞,WebRTC jitterbuffer会增加缓冲,以确保音频或视频流的连续性。当网络稳定时,WebRTC jitterbuffer会逐渐减少缓冲的大小,以减少延迟和改善实时反应性。 此外,WebRTC jitterbuffer还可根据特定的需求进行定制。例如,它可以根据网络质量和设备性能进行动态调整。它还可以通过将音频和视频流分别处理,来更好地优化总体性能。 总的来说,WebRTC jitterbuffer是一种有效的技术,可以帮助WebRTC用户克服网络延迟的问题,提供更好的音频和视频通信体验。随着WebRTC应用程序的广泛应用,WebRTC jitterbuffer的重要性也逐渐凸显。 ### 回答3: WebRTC JitterBuffer – 意指 WebRTC 的抖动缓冲器,是一种数据处理机制,主要用于解决因网络传输延迟不一致导致的数据抖动或突变问题。WebRTC 向来被视为一种适用于实时实时通信场景的音视频传输协议,依靠其先进的传输技术,WebRTC 能够将高品质的音视频数据进行实时传输,并保证在不同网络延迟下的稳定性和流畅性。 然而,由于公共互联网环境的不可预测性,很容易会遭遇到网络延迟、数据丢包等问题,从而导致用户在接收音视频时出现抖动的情况。这时候就需要使用 WebRTC JitterBuffer 来对数据进行处理,使其变得更加平滑,并对数据进行合理的延时处理。 WebRTC 抖动缓冲器的主要作用是针对网络抖动或突变对音视频数据进行一定的延迟处理,并从丢包数据中恢复丢失的数据。在 WebRTC 中,一般会采用混合适应性抖动缓冲器(Hybrid Adaptive Jitter Buffer)的机制来进行处理,具体详见以下的处理流程: 1.接收数据 - WebRTC 首先会收集音视频数据,并对数据进行封装和编码,然后通过 UDP 协议进行传输。 2.处理网络抖动 - 当数据经过网络时,会因为网络延迟、网络丢包、网络拥塞等原因造成数据的抖动现象。WebRTC JitterBuffer 就会在此时进行响应,对抖动数据进行中间处理,从而使数据流畅而不受到抖动的影响。 3.延迟处理 - 当 WebRTC JitterBuffer 处理完抖动数据后,还可以对数据进行一定的延迟处理。这是因为,数据在传输过程中,很容易收到网络情况的影响,从而导致数据的突变现象。延迟处理可以缓解这一问题,并使音视频数据更平滑地呈现在用户面前。 4.数据解码 - 最后,WebRTC JitterBuffer 将处理好的音视频数据进行解码,然后交给音频或视频播放器进行渲染和播放。 综上所述,WebRTC JitterBuffer 可以说是 WebRTC 传输协议中重要的一个环节,通过它可以解决音视频传输过程中的抖动或突变问题,并保障了音视频数据的流畅和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值