设为首页收藏本站

安徽论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 30956|回复: 0

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

[复制链接]

76

主题

0

回帖

240

积分

中级会员

Rank: 3Rank: 3

积分
240
发表于 2022-3-26 10:29:39 | 显示全部楼层 |阅读模式
网站内容均来自网络,本站只提供信息平台,如有侵权请联系删除,谢谢!
本文由融云技术团队原创投稿,作者是融云WebRTC高级工程师苏道,转载请注明出处。
1、引言

在一个典型的IM应用里,使用实时音视频聊天功能时,视频首帧的显示,是一项很重要的用户体验指标。
本文主要通过对WebRTC接收端的音视频处理过程分析,来了解和优化视频首帧的显示时间,并进行了总结和分享。
(本文同步发布于:http://www.52im.net/thread-3169-1-1.html
2、什么是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.264、VP8的前世今生》),WebRTC 开源项目应运而生,即 GIPS 音视频引擎 + 替换掉 H.264 的 VPx 视频编解码器。
在此之后,Google 又将在 Gtalk 中用于 P2P 打洞的开源项目 libjingle 融合进了 WebRTC。目前 WebRTC 提供了包括 Web、iOS、Android、Mac、Windows、Linux 在内的所有平台支持。
(以上介绍,引用自《了不起的WebRTC:生态日趋完善,或将实时音视频技术白菜化》)
虽然WebRTC的目标是实现跨平台的Web端实时音视频通讯,但因为核心层代码的Native、高品质和内聚性,开发者很容易进行除Web平台外的移殖和应用。目前为止,WebRTC几乎是是业界能免费得到的唯一高品质实时音视频通讯技术。
3、流程介绍

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


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

本文所涉及的参数调整,谈论的部分位于上图中的第 4 步。
因为是接收端,所以会收到对方的 Offer 请求。先设置 SetRemoteDescription 再 SetLocalDescription。
如下图蓝色部分: 

4、参数调整

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) 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、其他影响首帧显示的问题

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 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)
  
         

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
免责声明
1. 本论坛所提供的信息均来自网络,本网站只提供平台服务,所有账号发表的言论与本网站无关。
2. 其他单位或个人在使用、转载或引用本文时,必须事先获得该帖子作者和本人的同意。
3. 本帖部分内容转载自其他媒体,但并不代表本人赞同其观点和对其真实性负责。
4. 如有侵权,请立即联系,本网站将及时删除相关内容。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表