融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践

本文由融云技术团队原创投稿,做者是融云WebRTC高级工程师苏道,转载请注明出处。php

一、引言

在一个典型的IM应用里,使用实时音视频聊天功能时,视频首帧的显示,是一项很重要的用户体验指标。html

本文主要经过对WebRTC接收端的音视频处理过程分析,来了解和优化视频首帧的显示时间,并进行了总结和分享。android

(本文同步发布于:http://www.52im.net/thread-3169-1-1.htmlweb

二、什么是WebRTC?

对于没接触过实时音视频技术的人来讲,老是看到别人在提WebRTC,那WebRTC是什么?咱们有必要简单介绍一下。算法

说到 WebRTC,咱们不得不提到 Gobal IP Solutions,简称 GIPS。这是一家 1990 年成立于瑞典斯德哥尔摩的 VoIP 软件开发商,提供了能够说是世界上最好的语音引擎。相关介绍详见《访谈WebRTC标准之父:WebRTC的过去、如今和将来》。小程序

Skype、腾讯 QQ、WebEx、Vidyo 等都使用了它的音频处理引擎,包含了受专利保护的回声消除算法,适应网络抖动和丢包的低延迟算法,以及先进的音频编解码器。微信小程序

Google 在 Gtalk 中也使用了 GIPS 的受权。Google 在 2011 年以6820万美圆收购了 GIPS,并将其源代码开源,加上在 2010 年收购的 On2 获取到的 VPx 系列视频编解码器(详见《即时通信音视频开发(十七):视频编码H.26四、VP8的前世此生》),WebRTC 开源项目应运而生,即 GIPS 音视频引擎 + 替换掉 H.264 的 VPx 视频编解码器。api

在此以后,Google 又将在 Gtalk 中用于 P2P 打洞的开源项目 libjingle 融合进了 WebRTC。目前 WebRTC 提供了包括 Web、iOS、Android、Mac、Windows、Linux 在内的全部平台支持。数组

(以上介绍,引用自《了不得的WebRTC:生态日趋完善,或将实时音视频技术白菜化》)缓存

虽然WebRTC的目标是实现跨平台的Web端实时音视频通信,但由于核心层代码的Native、高品质和内聚性,开发者很容易进行除Web平台外的移殖和应用。目前为止,WebRTC几乎是是业界能免费获得的惟一高品质实时音视频通信技术。

三、流程介绍

一个典型的实时音视频处理流程大概是这样:

  • 1)发送端采集音视频数据,经过编码器生成帧数据;
  • 2)这数据被打包成 RTP 包,经过 ICE 通道发送到接收端;
  • 3)接收端接收 RTP 包,取出 RTP payload,完成组帧的操做;
  • 4)以后音视频解码器解码帧数据,生成视频图像或音频 PCM 数据。

以下图所示:

本文所涉及的参数调整,谈论的部分位于上图中的第 4 步。

由于是接收端,因此会收到对方的 Offer 请求。先设置 SetRemoteDescription 再 SetLocalDescription。

以下图蓝色部分:

四、参数调整

4.1 视频参数调整

当收到 Signal 线程 SetRemoteDescription 后,会在 Worker 线程中建立 VideoReceiveStream 对象。具体流程为 SetRemoteDescription -> VideoChannel::SetRemoteContent_w 建立 WebRtcVideoReceiveStream。

WebRtcVideoReceiveStream 包含了一个 VideoReceiveStream 类型 stream_ 对象, 经过 webrtc::VideoReceiveStream* Call::CreateVideoReceiveStream 建立。

建立后当即启动 VideoReceiveStream 工做,即调用 Start() 方法。

此时 VideoReceiveStream 包含一个 RtpVideoStreamReceiver 对象准备开始处理 video RTP 包。

接收方建立 createAnswer 后经过 setLocalDescription 设置 local descritpion。 

对应会在 Worker 线程中 setLocalContent_w 方法中根据 SDP 设置 channel 的接收参数,最终会调用到 WebRtcVideoReceiveStream::SetRecvParameters。

WebRtcVideoReceiveStream::SetRecvParameters 实现以下:

void WebRtcVideoChannel::WebRtcVideoReceiveStream::SetRecvParameters(
    const ChangedRecvParameters& params) {
  bool video_needs_recreation = false;
  bool flexfec_needs_recreation = false;
  if(params.codec_settings) {
    ConfigureCodecs(*params.codec_settings);
    video_needs_recreation = true;
  }
  if(params.rtp_header_extensions) {
    config_.rtp.extensions = *params.rtp_header_extensions;
    flexfec_config_.rtp_header_extensions = *params.rtp_header_extensions;
    video_needs_recreation = true;
    flexfec_needs_recreation = true;
  }
  if(params.flexfec_payload_type) {
    ConfigureFlexfecCodec(*params.flexfec_payload_type);
    flexfec_needs_recreation = true;
  }
  if(flexfec_needs_recreation) {
    RTC_LOG(LS_INFO) << "MaybeRecreateWebRtcFlexfecStream (recv) because of "
                        "SetRecvParameters";
    MaybeRecreateWebRtcFlexfecStream();
  }
  if(video_needs_recreation) {
    RTC_LOG(LS_INFO)
        << "RecreateWebRtcVideoStream (recv) because of SetRecvParameters";
    RecreateWebRtcVideoStream();
  }
}

根据上面 SetRecvParameters 代码,若是 codec_settings 不为空、rtp_header_extensions 不为空、flexfec_payload_type 不为空都会重启 VideoReceiveStream。

video_needs_recreation 表示是否要重启 VideoReceiveStream。

重启过程为:把先前建立的释放掉,而后重建新的 VideoReceiveStream。

以 codec_settings 为例:初始 video codec 支持 H264 和 VP8。若对端只支持 H264,协商后的 codec 仅支持 H264。SetRecvParameters 中的 codec_settings 为 H264 不空。其实先后 VideoReceiveStream 的都有 H264 codec,没有必要重建 VideoReceiveStream。能够经过配置本地支持的 video codec 初始列表和 rtp extensions,从而生成的 local SDP 和 remote SDP 中影响接收参数部分调整一致,而且判断 codec_settings 是否相等。 若是不相等再 video_needs_recreation 为 true。

这样设置就会使 SetRecvParameters 避免触发重启 VideoReceiveStream 逻辑。 

在 debug 模式下,修改后,验证没有 “RecreateWebRtcVideoStream (recv) because of SetRecvParameters” 的打印, 便可证实没有 VideoReceiveStream 重启。

4.2 音频参数调整

和上面的视频调整相似,音频也会有由于 rtp extensions 不一致致使从新建立 AudioReceiveStream,也是释放先前的 AudioReceiveStream,再从新建立 AudioReceiveStream。

参考代码:

bool WebRtcVoiceMediaChannel::SetRecvParameters(
    const AudioRecvParameters& params) {
  TRACE_EVENT0("webrtc", "WebRtcVoiceMediaChannel::SetRecvParameters");
  RTC_DCHECK(worker_thread_checker_.CalledOnValidThread());
  RTC_LOG(LS_INFO) << "WebRtcVoiceMediaChannel::SetRecvParameters: "
                   << params.ToString();
  // TODO(pthatcher): Refactor this to be more clean now that we have
  // all the information at once.
  if(!SetRecvCodecs(params.codecs)) {
    return false;
  }
  if(!ValidateRtpExtensions(params.extensions)) {
    return false;
  }
  std::vector<webrtc::RtpExtension> filtered_extensions = FilterRtpExtensions(
      params.extensions, webrtc::RtpExtension::IsSupportedForAudio, false);
  if(recv_rtp_extensions_ != filtered_extensions) {
    recv_rtp_extensions_.swap(filtered_extensions);
    for(auto& it : recv_streams_) {
      it.second->SetRtpExtensionsAndRecreateStream(recv_rtp_extensions_);
    }
  }
  return true;
}

AudioReceiveStream 的构造方法会启动音频设备,即调用 AudioDeviceModule 的 StartPlayout。

AudioReceiveStream 的析构方法会中止音频设备,即调用 AudioDeviceModule 的 StopPlayout。

所以重启 AudioReceiveStream 会触发屡次 StartPlayout/StopPlayout。

经测试,这些没必要要的操做会致使进入视频会议的房间时,播放的音频有一小段间断的状况。

解决方法:一样是经过配置本地支持的 audio codec 初始列表和 rtp extensions,从而生成的 local SDP 和 remote SDP 中影响接收参数部分调整一致,避免 AudioReceiveStream 重启逻辑。

另外 audio codec 多为 WebRTC 内部实现,去掉一些不用的 Audio Codec,能够减少 WebRTC 对应的库文件。

4.3 音视频相互影响

WebRTC 内部有三个很是重要的线程:

  • 1)woker 线程;
  • 2)signal 线程;
  • 3)network 线程。

调用 PeerConnection 的 API 的调用会由 signal 线程进入到 worker 线程。

worker 线程内完成媒体数据的处理,network 线程处理网络相关的事务,channel.h 文件中有说明,以 _w 结尾的方法为 worker 线程的方法,signal 线程的到 worker 线程的调用是同步操做。

以下面代码中的 InvokerOnWorker 是同步操做,setLocalContent_w 和 setRemoteContent_w 是 worker 线程中的方法。

bool BaseChannel::SetLocalContent(const MediaContentDescription* content,
                                  SdpType type,
                                  std::string* error_desc) {
  TRACE_EVENT0("webrtc", "BaseChannel::SetLocalContent");
  returnI nvokeOnWorker<bool>(
      RTC_FROM_HERE,
      Bind(&BaseChannel::SetLocalContent_w, this, content, type, error_desc));
}
bool BaseChannel::SetRemoteContent(const MediaContentDescription* content,
                                   SdpType type,
                                   std::string* error_desc) {
  TRACE_EVENT0("webrtc", "BaseChannel::SetRemoteContent");
  return InvokeOnWorker<bool>(
      RTC_FROM_HERE,
      Bind(&BaseChannel::SetRemoteContent_w, this, content, type, error_desc));
}

setLocalDescription 和 setRemoteDescription 中的 SDP 信息都会经过 PeerConnection 的 PushdownMediaDescription 方法依次下发给 audio/video RtpTransceiver 设置 SDP 信息。

举例:执行 audio 的 SetRemoteContent_w 执行很长(好比音频 AudioDeviceModule 的 InitPlayout 执行耗时), 会影响后面的 video SetRemoteContent_w 的设置时间。

PushdownMediaDescription 代码:

RTCError PeerConnection::PushdownMediaDescription(
    SdpType type,
    cricket::ContentSource source) {
  const SessionDescriptionInterface* sdesc =
      (source == cricket::CS_LOCAL ? local_description()
                                   : remote_description());
  RTC_DCHECK(sdesc);
  // Push down the new SDP media section for each audio/video transceiver.
  for(const auto& transceiver : transceivers_) {
    const ContentInfo* content_info =
        FindMediaSectionForTransceiver(transceiver, sdesc);
    cricket::ChannelInterface* channel = transceiver->internal()->channel();
    if(!channel || !content_info || content_info->rejected) {
      continue;
    }
    const MediaContentDescription* content_desc =
        content_info->media_description();
    if(!content_desc) {
      continue;
    }
    std::string error;
    bool success = (source == cricket::CS_LOCAL)
                       ? channel->SetLocalContent(content_desc, type, &error)
                       : channel->SetRemoteContent(content_desc, type, &error);
    if(!success) {
      LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, error);
    }
  }
  ...
}

五、其余影响首帧显示的问题

5.1 Android图像宽高16字节对齐

AndroidVideoDecoder 是 WebRTC Android 平台上的视频硬解类。AndroidVideoDecoder 利用 MediaCodec API 完成对硬件解码器的调用。

MediaCodec 有已下解码相关的 API:

  • 1)dequeueInputBuffer:若大于 0,则是返回填充编码数据的缓冲区的索引,该操做为同步操做;
  • 2)getInputBuffer:填充编码数据的 ByteBuffer 数组,结合 dequeueInputBuffer 返回值,可获取一个可填充编码数据的 ByteBuffer;
  • 3)queueInputBuffer:应用将编码数据拷贝到 ByteBuffer 后,经过该方法告知 MediaCodec 已经填写的编码数据的缓冲区索引;
  • 4)dequeueOutputBuffer:若大于 0,则是返回填充解码数据的缓冲区的索引,该操做为同步操做;
  • 5)getOutputBuffer:填充解码数据的 ByteBuffer 数组,结合 dequeueOutputBuffer 返回值,可获取一个可填充解码数据的 ByteBuffer;
  • 6)releaseOutputBuffer:告诉编码器数据处理完成,释放 ByteBuffer 数据。

在实践当中发现,发送端发送的视频宽高须要 16 字节对齐,由于在某些 Android 手机上解码器须要 16 字节对齐。

大体的原理就是:Android 上视频解码先是把待解码的数据经过 queueInputBuffer 给到 MediaCodec。而后经过 dequeueOutputBuffer 反复查看是否有解完的视频帧。若非 16 字节对齐,dequeueOutputBuffer 会有一次MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED。而不是一上来就能成功解码一帧。

经测试发现:帧宽高非 16 字节对齐会比 16 字节对齐的慢 100 ms 左右。

5.2 服务器需转发关键帧请求

iOS 移动设备上,WebRTC App应用进入后台后,视频解码由 VTDecompressionSessionDecodeFrame 返回 kVTInvalidSessionErr,表示解码session 无效。从而会触发观看端的关键帧请求给服务器。

这里要求服务器必须转发接收端发来的关键帧请求给发送端。若服务器没有转发关键帧给发送端,接收端就会长时间没有能够渲染的图像,从而出现黑屏问题。

这种状况下只能等待发送端本身生成关键帧,发送个接收端,从而使黑屏的接收端恢复正常。

5.3 WebRTC内部的一些丢弃数据逻辑举例

Webrtc从接受报数据到、给到解码器之间的过程当中也会有不少验证数据的正确性。

举例1:

PacketBuffer 中记录着当前缓存的最小的序号 first_seq_num_(这个值也是会被更新的)。 当 PacketBuffer 中 InsertPacket 时候,若是即将要插入的 packet 的序号 seq_num 小于 first_seq_num,这个 packet 会被丢弃掉。若是所以持续丢弃 packet,就会有视频不显示或卡顿的状况。

举例2:

正常状况下 FrameBuffer 中帧的 picture id,时间戳都是一直正增加的。

若是 FrameBuffer 收到 picture_id 比最后解码帧的 picture id 小时,分两种状况:

  • 1)时间戳比最后解码帧的时间戳大,且是关键帧,就会保存下来。
  • 2)除状况 1 以外的帧都会丢弃掉。

代码以下:

auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();
 auto last_decoded_frame_timestamp =
     decoded_frames_history_.GetLastDecodedFrameTimestamp();
 if(last_decoded_frame && id <= *last_decoded_frame) {
   if(AheadOf(frame->Timestamp(), *last_decoded_frame_timestamp) &&
       frame->is_keyframe()) {
     // If this frame has a newer timestamp but an earlier picture id then we
     // assume there has been a jump in the picture id due to some encoder
     // reconfiguration or some other reason. Even though this is not according
     // to spec we can still continue to decode from this frame if it is a
     // keyframe.
     RTC_LOG(LS_WARNING)
         << "A jump in picture id was detected, clearing buffer.";
     ClearFramesAndHistory();
     last_continuous_picture_id = -1;
   } else{
     RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
                         << id.picture_id << ":"
                         << static_cast<int>(id.spatial_layer)
                         << ") inserted after frame ("
                         << last_decoded_frame->picture_id << ":"
                         << static_cast<int>(last_decoded_frame->spatial_layer)
                         << ") was handed off for decoding, dropping frame.";
     return last_continuous_picture_id;
   }
 }

所以为了能让收到了流顺利播放,发送端和中转的服务端须要确保视频帧的 picture_id, 时间戳正确性。

WebRTC 还有其余不少丢帧逻辑,若网络正常且有持续有接收数据,可是视频卡顿或黑屏无显示,多为流自己的问题。

六、本文小结

本文经过分析 WebRTC 音视频接收端的处理逻辑,列举了一些能够优化首帧显示的点,好比经过调整 local SDP 和 remote SDP 中与影响接收端处理的相关部分,从而避免 Audio/Video ReceiveStream 的重启。

另外列举了 Android 解码器对视频宽高的要求、服务端对关键帧请求处理、以及 WebRTC 代码内部的一些丢帧逻辑等多个方面对视频显示的影响。 这些点都提升了融云 SDK 视频首帧的显示时间,改善了用户体验。

因我的水平有限,文章内容或许存在必定的局限性,欢迎回复进行讨论。

附录1:融云分享的其它文章

融云技术分享:融云安卓端IM产品的网络链路保活技术实践
IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略
融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践》(* 本文)
即时通信云融云CTO的创业经验分享:技术创业,你真的准备好了?

附录2:更多实时音视频相关技术文章

[1] 开源实时音视频技术WebRTC的文章:
开源实时音视频技术WebRTC的现状
简述开源实时音视频技术WebRTC的优缺点
访谈WebRTC标准之父:WebRTC的过去、如今和将来
《[良心分享:WebRTC 零基础开发者教程(中文)[附件下载]]( http://www.52im.net/thread-26...
WebRTC实时音视频技术的总体架构介绍
新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的?
WebRTC实时音视频技术基础:基本架构和协议栈
浅谈开发实时视频直播平台的技术要点
《[[观点] WebRTC应该选择H.264视频编码的四大理由]( http://www.52im.net/thread-48...
基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些?
开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用
简述实时音视频聊天中端到端加密(E2EE)的工做原理
实时通讯RTC技术栈之:视频编解码
开源实时音视频技术WebRTC在Windows下的简明编译教程
网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填?
了不得的WebRTC:生态日趋完善,或将实时音视频技术白菜化
腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践
融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践
  更多同类文章 ……
[2] 实时音视频开发的其它精华资料:
即时通信音视频开发(一):视频编解码之理论概述
即时通信音视频开发(二):视频编解码之数字视频介绍
即时通信音视频开发(三):视频编解码之编码基础
即时通信音视频开发(四):视频编解码之预测技术介绍
即时通信音视频开发(五):认识主流视频编码技术H.264
即时通信音视频开发(六):如何开始音频编解码技术的学习
即时通信音视频开发(七):音频基础及编码原理入门
即时通信音视频开发(八):常见的实时语音通信编码标准
即时通信音视频开发(九):实时语音通信的回音及回音消除概述
即时通信音视频开发(十):实时语音通信的回音消除技术详解
即时通信音视频开发(十一):实时语音通信丢包补偿技术详解
即时通信音视频开发(十二):多人实时音视频聊天架构探讨
即时通信音视频开发(十三):实时视频编码H.264的特色与优点
即时通信音视频开发(十四):实时音视频数据传输协议介绍
即时通信音视频开发(十五):聊聊P2P与实时音视频的应用状况
即时通信音视频开发(十六):移动端实时音视频开发的几个建议
即时通信音视频开发(十七):视频编码H.26四、VP8的前世此生
即时通信音视频开发(十八):详解音频编解码的原理、演进和应用选型
即时通信音视频开发(十九):零基础,史上最通俗视频编码技术入门
实时语音聊天中的音频处理与编码压缩技术简述
网易视频云技术分享:音频处理与压缩技术快速入门
学习RFC3550:RTP/RTCP实时传输协议基础知识
基于RTMP数据传输协议的实时流媒体技术研究(论文全文)
声网架构师谈实时音视频云的实现难点(视频采访)
浅谈开发实时视频直播平台的技术要点
还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!
实现延迟低于500毫秒的1080P实时音视频直播的实践分享
移动端实时视频直播技术实践:如何作到实时秒开、流畅不卡
如何用最简单的方法测试你的实时音视频方案
技术揭秘:支持百万级粉丝互动的Facebook实时视频直播
简述实时音视频聊天中端到端加密(E2EE)的工做原理
移动端实时音视频直播技术详解(一):开篇
移动端实时音视频直播技术详解(二):采集
移动端实时音视频直播技术详解(三):处理
移动端实时音视频直播技术详解(四):编码和封装
移动端实时音视频直播技术详解(五):推流和传输
移动端实时音视频直播技术详解(六):延迟优化
理论联系实际:实现一个简单地基于HTML5的实时视频直播
IM实时音视频聊天时的回声消除技术详解
浅谈实时音视频直播中直接影响用户体验的几项关键技术指标
如何优化传输机制来实现实时音视频的超低延迟?
首次披露:快手是如何作到百万观众同场看直播仍能秒开且不卡顿的?
Android直播入门实践:动手搭建一套简单的直播系统
网易云信实时视频直播在TCP数据传输层的一些优化思路
实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器
P2P技术如何将实时视频直播带宽下降75%?
专访微信视频技术负责人:微信实时视频聊天技术的演进
腾讯音视频实验室:使用AI黑科技实现超低码率的高清实时视频聊天
微信团队分享:微信每日亿次实时音视频聊天背后的技术解密
近期大热的实时直播答题系统的实现思路与技术难点分享
福利贴:最全实时音视频开发要用到的开源工程汇总
七牛云技术分享:使用QUIC协议实现实时视频直播0卡顿!
实时音视频聊天中超低延迟架构的思考与技术实践
理解实时音视频聊天中的延时问题一篇就够
实时视频直播客户端技术盘点:Native、HTML五、WebRTC、微信小程序
写给小白的实时音视频技术入门提纲
微信多媒体团队访谈:音视频开发的学习、微信的音视频技术和挑战等
腾讯技术分享:微信小程序音视频技术背后的故事
微信多媒体团队梁俊斌访谈:聊一聊我所了解的音视频技术
新浪微博技术分享:微博短视频服务的优化实践之路
实时音频的混音在视频直播应用中的技术原理和实践总结
以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战
腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践
新浪微博技术分享:微博实时直播答题的百万高并发架构实践
技术干货:实时视频直播首屏耗时400ms内的优化实践
爱奇艺技术分享:轻松诙谐,讲解视频编解码技术的过去、如今和未来
零基础入门:实时音视频技术基础知识全面盘点
  更多同类文章 ……

本文已同步发布于“即时通信技术圈”公众号:

本文在公众号上的连接是:点此进入,原文连接是:http://www.52im.net/thread-3169-1-1.html