一、jitter buffer 介绍
 二、jitter 估计
 三、buffer 处理 rtp 包逻辑
 四、接收和解码流程
 五、FrameBuffer 类介绍

/*
 ********************************************************************************
 一、jitter buffer 介绍

jitter buffer 抖动缓冲区
   当网络不稳定时(发生抖动),增加buffer的长度,多缓存一些数据,以应对将来可能发生的抖动。
   它对数据包丢失、乱序、延迟到达等情况进行处理,平滑的向解码模块输出数据包/帧,
 抵抗各种弱网情况对播放/渲染造成的影响,降低卡顿,提高用户体验。

视频从采集到渲染的流程如下:

采集 -> 编码 -> 分包 -> 发送 ----------------> 网络
 渲染 <- 解码 <- Jitter Buffer <- 组帧 <- 接收 <-|

********************************************************************************
 */

/*
 ********************************************************************************
 二、jitter 估计
 
 jitter 就是一种抖动。
 RTP数据包从源地址发送到目的地址,会发生不一样的延迟,这样的延迟变动就是 jitter
 jitter 会让音视频的播放不稳定,如音频的颤音、视频的忽快忽慢
 解决 jitter 的方法是增加延时,这个延时称为抖动延迟(jitter delay)

抖动延迟由网络延迟、解码延迟、渲染延迟构成。解码、渲染延迟比较稳定,网络抖动延迟是动态变化的。
 webrtc 认为网络抖动延迟由两部分构成:
 1、网络噪声带来的抖动延迟(网络排队延迟)
 2、传输大的视频帧(特别是关键帧)对网络造成冲击带来的抖动延迟
   (计算信道速率,根据信道速率计算大的视频帧对网络冲击带来的延迟)

webrtc 使用卡尔曼滤波(kalman) 估算网络排队延迟和信道速率

modules\video_coding\jitter_estimator.cc

1、更新 jitter 估计
 void VCMJitterEstimator::UpdateEstimate(
   int64_t frameDelayMS, uint32_t frameSizeBytes, bool incompleteFrame = false);
 
 其中 frameDelayMS 帧间延迟,指的是一帧数据因为分包和网络传输所造成的延时。
 frameSizeBytes 指当前数据帧大小, incompleteFrame 指是否为完整的帧。

1.1、噪声阈值计算
 VCMJitterEstimator::NoiseThreshold()
   double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;

1.2、计算 jitter 估计值
 VCMJitterEstimator::CalculateEstimate()
   double ret = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
 
 即
 jitterDelay = _theta[0] * (_maxFrameSize - _avgFrameSize)
               + _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset; 
 
 其中:
 _theta[0] 为信道传输速率的倒数
 _maxFrameSize 自会话开始以来所收到的最大帧大小
 _avgFrameSize 平均帧大小
 _noiseStdDevs 表示噪声系数,值为2.33
 _varNoise 表示噪声方差,默认值为4.0 EstimateRandomJitter()中会不断更新该值
 _noiseStdDevOffset 为噪声扣除常数,值为30.0

2、获取 jitter 估计
 返回以毫秒为单位的当前抖动估计,并在重传情况下添加一个RTT相关项
 int VCMJitterEstimator::GetJitterEstimate(
   double rttMultiplier, absl::optional<double> rttMultAddCapMs)
 其中: rttMultiplier 为RTT参数乘数

参考: http://www.ctiforum.com/news/guonei/512085.html

********************************************************************************
 */

/*
 ********************************************************************************
 三、buffer 处理 rtp 包逻辑

modules\video_coding\jitter_buffer.h

buffer 接收 rtp 包的处理逻辑主要使用到以下三个队列
 class VCMJitterBuffer {
   UnorderedFrameList free_frames_ RTC_GUARDED_BY(crit_sect_);
   FrameList decodable_frames_ RTC_GUARDED_BY(crit_sect_);
   FrameList incomplete_frames_ RTC_GUARDED_BY(crit_sect_);
 }
 free_frames_ 队列用于管理空的frame,弹出空的frame来存放rtp包,解码完成后的frame重置后再次存入该队列。
 incomplete_frames_ 队列用于存放尚未完整的frame,当frame完整时将其push到 decodable_frames_
 decodable_frames_ 队列用于存放完整的可以解码的frame
 
 1、第一次接收到一个rtp视频包,从 free_frames_ 队列中弹出一个空 frame 块,用来放置这个包。
    之后每次接收一个rtp包,根据时间戳在 incomplete_frames_ 和 decodable_frames_ 
    中寻找,看是否已经接收到过相同时间戳的包,如果找到则弹出该 frame 块。
    否则,从 free_frames_ 中弹出一个空 frame
 2、根据包的序列号,找到应该插入 frame 的位置将包插入,并更新 frame 的 state (frame中存放多个rtp包)
    其中 state 的状态为 
    enum VCMFrameBufferStateEnum {
      kStateEmpty,       // frame popped by the RTP receiver
      kStateIncomplete,  // frame that have one or more packet(s) stored
      kStateComplete,    // frame that have all packets
    };
 3、根据不同的 buffer_state 将 frame 帧 push 回到队列中。
    如果 buffer_state 为 kCompleteSession 并且 frame 已经在 decodable list (continuous 为 true)
    将 frame push 到 decodable_frames_ 队列 decodable_frames_.InsertFrame(frame);

如果 buffer_state 为 kCompleteSession 或 kIncomplete
    将 frame push 到 incomplete_frames_ 队列 incomplete_frames_.InsertFrame(frame);

如果 buffer_state 为 kNoError 或 kOutOfBoundsPacket 或 kDuplicatePacket
    将 frame push 到 frame_list 队列 frame_list->InsertFrame(frame);

VCMJitterBuffer::InsertPacket -> VCMJitterBuffer::FindAndInsertContinuousFramesWithState() 
    将 incomplete_frames_ 队列中的 frame push 到 decodable_frames_ 队列
 
 4、free_frames_ 队列初始化大小为 kStartNumberOfFrames = 6 (在构造函数 VCMJitterBuffer() 中初始化)
    如果 free_frames_ 队列为空时,增加队列大小,最大值为 kMaxNumberOfFrames = 300
    定期从 incomplete_frames_ 和 decodable_frames_ 队列中清除一些过时的frame 
    push 到 free_frames_ 队列 VCMJitterBuffer::RecycleFramesUntilKeyFrame()
 
 5、解码线程取出 frame 并解码完成后,将 frame 重置并 push 到 free_frames_ 队列
    VideoReceiver::Decode() -> VCMReceiver::ReleaseFrame() -> VCMJitterBuffer::ReleaseFrame()
    -> VCMJitterBuffer::RecycleFrameBuffer() -> frame->Reset()/free_frames_.push_back(frame)

********************************************************************************
 */

/*
 ********************************************************************************
 四、接收和解码流程

webrtc jitter buffer 被两个线程操作
 (1) 写线程负责组帧之后把数据写入 jitter buffer 里
 (2) 读线程负责从 jitter buffer 里读取数据然后解码

1、接收数据
 1.1、读线程 创建流程
 WebRtcVideoChannel::WebRtcVideoReceiveStream::WebRtcVideoReceiveStream()
 -> WebRtcVideoChannel::WebRtcVideoReceiveStream::RecreateWebRtcVideoStream()
    [调用 stream_ = call_->CreateVideoReceiveStream(std::move(config));
     调用 stream_->Start()
     其中 stream_ 类型为 webrtc::VideoReceiveStream*
    ]
    [Call::CreateVideoReceiveStream()
     VideoReceiveStream::Start()
    ]
 -> webrtc::VideoReceiveStream* Call::CreateVideoReceiveStream()
    [调用构造函数 
     VideoReceiveStream::VideoReceiveStream(module_process_thread_.get())
     其中 Call::Call() 设置为 module_process_thread_ 为 ModuleProcessThread 线程
    ]
    [Call::Create(const Call::Config& config)
        [调用 Create(config, Clock::GetRealTimeClock(),
                    ProcessThread::Create("ModuleProcessThread"),
                    ProcessThread::Create("PacerThread"));
         创建两个线程: ModuleProcessThread 和 PacerThread
        ]
     -> Call::Create()
        [调用 new internal::Call()]
     -> Call::Call()
        [其中设置 module_process_thread_ 为 ModuleProcessThread 线程]
    ]
    [VideoReceiveStream::VideoReceiveStream()
        [设置 rtp_stream_sync_(this) 即 rtp_stream_sync_ 为 VideoReceiveStream 类型
         设置 process_thread_ 为 ModuleProcessThread 线程
         调用 process_thread_->RegisterModule(&rtp_stream_sync_, RTC_FROM_HERE);
        ]
    ]
 -> VideoReceiveStream::Start()
 -> VideoReceiveStream::StartNextDecode()
    [将数据帧从 jitter buffer 中取出
     调用 frame_buffer_->NextFrame()
    ]

1.2、写线程 创建流程
 VideoReceiveStream::VideoReceiveStream()
    [创建 rtp_video_stream_receiver_(process_thread_)
     其中 rtp_video_stream_receiver_ 类型为 RtpVideoStreamReceiver
     process_thread_ 为 ModuleProcessThread 线程
    ]
 -> RtpVideoStreamReceiver::RtpVideoStreamReceiver()
    [设置 process_thread_ 为 ModuleProcessThread 线程]
 -> RtpVideoStreamReceiver::OnCompleteFrame() [从网络接收到RTP数据]
 -> VideoReceiveStream::OnCompleteFrame()
    [调用 frame_buffer_->InsertFrame()]
 -> FrameBuffer::InsertFrame() [将数据帧存放到 jitter buffer ]

2、解码流程 
 // 创建一个工作线程,用于解码数据帧
 // 从 frame_buffer_ 中获取数据帧进行解码
 VideoStreamDecoderImpl::VideoStreamDecoderImpl()
    [创建线程 PlatformThread 一个简单的工作线程
     decode_thread_(&DecodeLoop,
                    this,
                    "video_stream_decoder_decode_thread",
                    rtc::kHighestPriority)
     启动线程 decode_thread_.Start()
    ]
 -> VideoStreamDecoderImpl::DecodeLoop(void* ptr)
    [auto* vs_decoder = static_cast<VideoStreamDecoderImpl*>(ptr);
     启用循环 while (true)
     DecodeResult decode_result =
        vs_decoder->DecodeNextFrame(max_wait_time_ms, keyframe_required);
     如果 decode_result 为 kShutdown 则返回 
    ]
 -> VideoStreamDecoderImpl::DecodeNextFrame()
    [调用 frame_buffer_.NextFrame()
     VideoDecoder* decoder = GetDecoder(frame->PayloadType());
     decoder->Decode()
    ]
    [FrameBuffer::NextFrame()
     VideoStreamDecoderImpl::GetDecoder()
     VideoDecoder::Decode() 纯虚函数
     [视频编码方式(h264 vp8 vp9),下面以vp8为例]
    ]
 -> LibvpxVp8Decoder::Decode()

// 创建一个任务队列,在上面的工作线程中以异步的方式执行任务
 // 将 frame 存放到 frame_buffer_
 VideoStreamDecoderImpl::VideoStreamDecoderImpl()
    [创建任务队列 bookkeeping_queue_(task_queue_factory->CreateTaskQueue())]
 VideoStreamDecoderImpl::OnFrame()
    [如果 bookkeeping_queue_.IsCurrent() 为 false
     调用 bookkeeping_queue_.PostTask(
            std::make_unique<OnFrameTask>(std::move(frame), this));
     其中 OnFrameTask::Run() 调用 
     video_stream_decoder_->OnFrame() 即 VideoStreamDecoderImpl::OnFrame()
    ]
    [调用 frame_buffer_.InsertFrame(std::move(frame))]

********************************************************************************
 */

/*
 ********************************************************************************
 五、FrameBuffer 类介绍
 ********************************************************************************
 */

/**
 * jitter buffer实现 frame_buffer2.cc 
 * class FrameBuffer
 * 
*/

FrameBuffer(Clock* clock,
            VCMTiming* timing,
            VCMReceiveStatisticsCallback* stats_callback);

// Insert a frame into the frame buffer. Returns the picture id
// of the last continuous frame or -1 if there is no continuous frame.
int64_t InsertFrame(std::unique_ptr<EncodedFrame> frame);

// Get the next frame for decoding. Will return at latest after
// |max_wait_time_ms|.
//  - If a frame is available within |max_wait_time_ms| it will return
//    kFrameFound and set |frame_out| to the resulting frame.
//  - If no frame is available after |max_wait_time_ms| it will return
//    kTimeout.
//  - If the FrameBuffer is stopped then it will return kStopped.
ReturnReason NextFrame(int64_t max_wait_time_ms,
                       std::unique_ptr<EncodedFrame>* frame_out,
                       bool keyframe_required);
void NextFrame(
    int64_t max_wait_time_ms,
    bool keyframe_required,
    rtc::TaskQueue* callback_queue,
    std::function<void(std::unique_ptr<EncodedFrame>, ReturnReason)> handler);

// Tells the FrameBuffer which protection mode that is in use. Affects
// the frame timing. (影响帧定时)
void SetProtectionMode(VCMVideoProtection mode);

// Start the frame buffer, has no effect if the frame buffer is started.
// The frame buffer is started upon construction.
void Start();

// Stop the frame buffer, causing any sleeping thread in NextFrame to
// return immediately.
void Stop();

// Updates the RTT for jitter buffer estimation.
void UpdateRtt(int64_t rtt_ms);

// Clears the FrameBuffer, removing all the buffered frames.
void Clear();

class FrameBuffer {
 public:

private:
  
  FrameMap frames_;  // 只存储未解码的帧
  int64_t latest_return_time_ms_;  // 下次返回帧的时间
};

void FrameBuffer::NextFrame(
    int64_t max_wait_time_ms,
    bool keyframe_required,
    rtc::TaskQueue* callback_queue,
    std::function<void(std::unique_ptr<EncodedFrame>, ReturnReason)> handler) {
  // 
  int64_t latest_return_time_ms =
      clock_->TimeInMilliseconds() + max_wait_time_ms;

latest_return_time_ms_ = latest_return_time_ms;
  keyframe_required_ = keyframe_required;
  frame_handler_ = handler;
  callback_queue_ = callback_queue;
  StartWaitForNextFrameOnQueue();
}

void FrameBuffer::StartWaitForNextFrameOnQueue() {
  // 查找下一个待解码的帧存放到 frames_to_decode_ 并返回最大等待时间
  int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());

// 启动一个重复任务
  // 如果在等待时间内有可以解码的帧( frames_to_decode_ 不为空)
  // 则调用 frame_handler_ 处理帧
  callback_task_ = RepeatingTaskHandle::DelayedStart(
      callback_queue_->Get(), TimeDelta::ms(wait_ms), [this] {
        // 如果这个任务没有被取消,我们在等待时没有得到任何新的帧,
        // 则继续 delivery frame
        if (!frames_to_decode_.empty()) {
          // 还有 frame 继续 deliver
          frame_handler_(absl::WrapUnique(GetNextFrame()), kFrameFound);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else if (clock_->TimeInMilliseconds() >= latest_return_time_ms_) {
          // 超时,发送信号并停止重复任务
          frame_handler_(nullptr, kTimeout);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else {
          // 如果没有帧用于解码,并且还有时间
          // 这意味着在创建和执行此任务之间清除了帧缓冲区
          // 继续等待剩余时间
          int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
          return TimeDelta::ms(wait_ms);
        }
      });
}

/**
 * 从未解码的帧 frames_ 中查找 superframe 及其剩余帧,并将其存放到 frames_to_decode_
 * 计算帧的渲染时间戳和最大需要等待的时间
 * 返回等待时间
*/
int64_t FrameBuffer::FindNextFrame(int64_t now_ms) {
  // 根据下次返回帧的时间和当前时间计算等待时间
  int64_t wait_ms = latest_return_time_ms_ - now_ms;
  // 清理容器
  frames_to_decode_.clear();

// 遍历未解码的帧 frames_
  for (auto frame_it = frames_.begin();
       frame_it != frames_.end() && frame_it->first <= last_continuous_frame_;
       ++frame_it) {
    if (!frame_it->second.continuous ||
        frame_it->second.num_missing_decodable > 0) {
      continue;
    }

// 获取已编码的帧
    EncodedFrame* frame = frame_it->second.frame.get();

// 如果需要关键帧,但当前帧不是关键帧,则跳过
    if (keyframe_required_ && !frame->is_keyframe())
      continue;

// 只会返回 superframe 的所有部分
    // 因此如果不是 superframe 的开始,则跳过
    if (frame->inter_layer_predicted) {
      continue;
    }

// 收集同一 superframe 的所有剩余帧 
    std::vector<FrameMap::iterator> current_superframe;
    current_superframe.push_back(frame_it);

// 判断下一个帧是否和当前帧为同一 superframe
    // 如果是,则将其存放到 current_superframe
    bool last_layer_completed = frame_it->second.frame->is_last_spatial_layer;
    FrameMap::iterator next_frame_it = frame_it;

// 将同一 superframe 的所有帧存放到 current_superframe
    while (true) {
      ++next_frame_it;
      if (next_frame_it == frames_.end() ||
          next_frame_it->first.picture_id != frame->id.picture_id ||
          !next_frame_it->second.continuous) {
        break;
      }
      // Check if the next frame has some undecoded references other than
      // the previous frame in the same superframe.
      size_t num_allowed_undecoded_refs =
          (next_frame_it->second.frame->inter_layer_predicted) ? 1 : 0;
      if (next_frame_it->second.num_missing_decodable >
          num_allowed_undecoded_refs) {
        break;
      }
      // All frames in the superframe should have the same timestamp.
      if (frame->Timestamp() != next_frame_it->second.frame->Timestamp()) {
        RTC_LOG(LS_WARNING) << "Frames in a single superframe have different"
                               " timestamps. Skipping undecodable superframe.";
        break;
      }
      current_superframe.push_back(next_frame_it);
      last_layer_completed = next_frame_it->second.frame->is_last_spatial_layer;
    }
    
    // 检查当前的 superframe 是否完整 
    if (!last_layer_completed) {
      continue;
    }

// 将当前 superframe 及其剩下的所有帧传递给 frames_to_decode_
    frames_to_decode_ = std::move(current_superframe);

// 如果帧的渲染时间戳无效
    // 根据当前时间和帧的时间戳计算帧的渲染时间戳,并保存到帧信息中
    if (frame->RenderTime() == -1) {
      frame->SetRenderTime(timing_->RenderTimeMs(frame->Timestamp(), now_ms));
    }
    // 根据帧的渲染时间戳和当前时间计算最大需要等待的时间
    wait_ms = timing_->MaxWaitingTime(frame->RenderTime(), now_ms);

break;
  }

// 更新等待时间
  wait_ms = std::min<int64_t>(wait_ms, latest_return_time_ms_ - now_ms);
  wait_ms = std::max<int64_t>(wait_ms, 0);
  return wait_ms;
}

/**
 * 根据首帧的时间戳和帧的接收时间,计算帧的延迟
 * 根据帧的延迟和帧的大小,更新抖动估计
 * 
 * 设置 frames_to_decode_ 中帧的渲染时间为首帧的渲染时间
 * 返回待编码帧 frames_to_decode_
 * 
*/
EncodedFrame* FrameBuffer::GetNextFrame() {
  int64_t now_ms = clock_->TimeInMilliseconds();
  // TODO(ilnik): remove |frames_out| use frames_to_decode_ directly.
  std::vector<EncodedFrame*> frames_out;

bool superframe_delayed_by_retransmission = false;
  // 统计 superframe 大小,用于更新抖动
  size_t superframe_size = 0;
  // 获取 frames_to_decode_ 中第一个待解码的帧
  EncodedFrame* first_frame = frames_to_decode_[0]->second.frame.get();
  // 获取第一个待解码帧的渲染时间和接收时间
  int64_t render_time_ms = first_frame->RenderTime();
  int64_t receive_time_ms = first_frame->ReceivedTime();
  
  // 优雅地处理坏的RTP时间戳和渲染时间问题
  if (HasBadRenderTiming(*first_frame, now_ms)) {
    jitter_estimator_.Reset();
    timing_->Reset();
    render_time_ms = timing_->RenderTimeMs(first_frame->Timestamp(), now_ms);
  }

// 遍历 frames_to_decode_
  for (FrameMap::iterator& frame_it : frames_to_decode_) {
    // 获取待解码的帧
    EncodedFrame* frame = frame_it->second.frame.release();
    // 设置帧的渲染时间
    frame->SetRenderTime(render_time_ms);

superframe_delayed_by_retransmission |= frame->delayed_by_retransmission();
    // 计算接收时间
    receive_time_ms = std::max(receive_time_ms, frame->ReceivedTime());
    // 计算 superframe 帧大小
    superframe_size += frame->size();

PropagateDecodability(frame_it->second);
    decoded_frames_history_.InsertDecoded(frame_it->first, frame->Timestamp());

// Remove decoded frame and all undecoded frames before it.
    if (stats_callback_) {
      unsigned int dropped_frames = std::count_if(
          frames_.begin(), frame_it,
          [](const std::pair<const VideoLayerFrameId, FrameInfo>& frame) {
            return frame.second.frame != nullptr;
          });
      if (dropped_frames > 0) {
        stats_callback_->OnDroppedFrames(dropped_frames);
      }
    }

frames_.erase(frames_.begin(), ++frame_it);

// 将帧存放到 frames_out
    frames_out.push_back(frame);
  }

if (!superframe_delayed_by_retransmission) {
    int64_t frame_delay;

// 根据首帧的时间戳和接收时间 计算帧延迟并保存到 frame_delay
    if (inter_frame_delay_.CalculateDelay(first_frame->Timestamp(),
                                          &frame_delay, receive_time_ms)) {
      // 根据 帧延迟 和 superframe 帧大小 更新抖动估计
      jitter_estimator_.UpdateEstimate(frame_delay, superframe_size);
    }

float rtt_mult = protection_mode_ == kProtectionNackFEC ? 0.0 : 1.0;
    absl::optional<float> rtt_mult_add_cap_ms = absl::nullopt;
    if (rtt_mult_settings_.has_value()) {
      rtt_mult = rtt_mult_settings_->rtt_mult_setting;
      rtt_mult_add_cap_ms = rtt_mult_settings_->rtt_mult_add_cap_ms;
    }
    // 设置 jitter 延迟
    timing_->SetJitterDelay(
        jitter_estimator_.GetJitterEstimate(rtt_mult, rtt_mult_add_cap_ms));
    // 更新当前延迟
    timing_->UpdateCurrentDelay(render_time_ms, now_ms);
  } else {
    if (RttMultExperiment::RttMultEnabled() || add_rtt_to_playout_delay_)
      jitter_estimator_.FrameNacked();
  }

UpdateJitterDelay();
  UpdateTimingFrameInfo();

if (frames_out.size() == 1) {
    return frames_out[0];
  } else {
    return CombineAndDeleteFrames(frames_out);
  }
}

webrtc jitter buffer相关推荐

  1. java jitter buffer_android webrtc jitter buffer大小设置

    1. PeerConnectionClient.java 设置在如下接口: private void createPeerConnectionInternal(Context context,EglB ...

  2. 网易干货 | 浅析视频Jitter Buffer

    一. 什么是JitterBuffer Jitter Buffer也叫做抖动缓冲区,它是实时音视频里面的一个重要模块,它对数据包丢失.乱序.延迟到达等情况进行处理,平滑的向解码模块输出数据包/帧,抵抗各 ...

  3. rtcp 的jitter buffer

    //@[TOC](jitter buffer) jitter buffer rtcp xr jb 草案 觉得这篇draft写的挺好,适合入门阅读. zhangbin 20191128 翻译 jitte ...

  4. 音频传输之Jitter Buffer设计与实现

    在语音通信中Jitter Buffer(下面简称JB)是接收侧一个非常重要的模块,它是决定音质的重要因素之一.一方面它会把收到的乱序的语音包排好序放在buffer里正确的位置上,另一方面它把接收到的语 ...

  5. WebRTC内置debug工具,详细参数解读

    为了确保这篇文章所写内容尽可能的准确,我决定请来Philipp Hancke来作为此篇文章的共同作者. 当你想要找到你WebRTC产品中的问题时,webrtc-internals是一个非常棒的工具,因 ...

  6. webrtc fec

    转自:http://www.cnblogs.com/webrtc/p/7402570.html WebRTC::FEC [TOC] Tags: WebRTC FEC WebRTC中的 FEC 实现分为 ...

  7. WebRTC音频预处理单元APM的整体编译及使用

    正文 然而本次优化仍然没能用上整套VoE,因为VoE不仅仅包含音频预处理,它将音频编码模块.传输模块一并融入了引擎,而bill的项目需要使用既有的编码.传输层,因此使用整个VoE对我来说显得冗余且不可 ...

  8. WebRTC详解-zz

    1.WebRTC目的 WebRTC(Web Real-Time Communication)项目的最终目的主要是让Web开发者能够基于浏览器(Chrome\FireFox\...) 轻易快捷开发出丰富 ...

  9. 技术宝典 | 基于标准 WebRTC 低延迟直播的开源实践

    导读:2020年,新冠疫情爆发并席卷全球,对包括中国在内的全球经济造成了巨大的冲击,同时深刻影响了社会生活.在这一背景下,以消费市场上轰轰烈烈的直播电商为引爆点,直播行业再次掀起热潮.在中国企业数字化 ...

最新文章

  1. jquery 实现iframe 自适应高度
  2. 底层经典书籍-编译原理
  3. C++之error: cannot bind non-const lvalue reference of type ‘myString’ to an rvalue of type ‘myString
  4. Nginx/Apache发大招
  5. Visual Studio Code 1.42 发布
  6. CodeForces 489A SwapSort (选择排序法)
  7. python 进度条_2019年的代码都写完了吗?不如做个Python进度条看看还剩多少
  8. Gmail priority inbox帮助你减少工作量
  9. express不是内部或外部命令的解决方法
  10. 串行加法器 并行加法器 超前进位加法器
  11. 指标之王macd的计算公式和使用方法
  12. 6.13编一程序,将两个字符串连接起来,不要用strcat函数。
  13. 用python计算个人所得税计算器_教你使用Python实现新个税计算器
  14. 图说三极管的三个工作状态
  15. C语言正数和负数循环右移,左移,把某正数的第m位(从0开始)到n位取反
  16. 微信小程序校验网络连接是否正常,无网络时给出提示
  17. 企业项目开发中可遵循的时间管理守则-华为时间管理
  18. [转]数据库巡检报告模板
  19. 图片编辑的利器(介绍韩国免费图片工具PhotoScape)
  20. 基于MATLAB Simulink的三相AC DC整流后的直流电机转速开环控制仿真模型

热门文章

  1. 高精度减法(C语言实现)
  2. LeetCode刷题(43)~汉明距离【异或+布赖恩·克尼根算法】
  3. java startw_java-Jetty(9.4.12):上下文启动失败o.e.j.w.WebAp...
  4. 上海现有电视频道分配表
  5. anaconda安装pythonocc
  6. 二叉树的遍历(先序、中序、后序)
  7. Linux 系统Trouble shouting及修复FATAL: NO bootale medium found! System halted
  8. 软件测试的魅力何在?您为什么选择测试一行而不做开发?
  9. 【洛谷P5514】永夜的报应【模拟】
  10. SpringMVC基础三