WebRTC H264 拉流渲染灰屏问题总结(一)

1. 前言

前段时间在处理公司屏幕共享功能的时候遇到一个问题, 视频拉流渲染的时候偶尔会出现灰屏, 下面是个例子.

1-1

出现问题是有偶现的, 随机的, 但频率并不低, 严重的影响了观看的体验.

针对灰屏问题进行了一些调研, 最终解决了这个问题(目前没有复现), 通过解决这个问题还是发现了很多知识盲区和没掌握的细节问题, 特此做一个总结.

2. 概念同步

2.1 H264

2.1.1 SPS

Sequence Paramater Set - 序列参数集
SPS 中保存了一组编码视频序列 (Coded Video Sequence)的全局参数, 因此该类型保存的是和编码序列相关的参数

2.1.2 PPS

Picture Paramater Set - 图像参数集
该类型保存了整体图像相关的参数

2.1.3 IDR

Instantaneous Decoding Refresh - 即时解码刷新
IDR帧实质也是I帧, 使用帧内预测. IDR帧的作用是立即刷新,会导致 DPB(Decoded Picture Buffer参考帧列表) 清空,而 I帧不会. 所以 IDR帧承担了随机访问功能, 一个新的 IDR帧开始, 可以重新算一个新的 GOP 开始编码, 播放器永远可以从一个 IDR帧播放, 因为在它之后没有任何帧引用之前的帧.

如果一个视频中没有 IDR帧, 这个视频是不能随机访问的. 所有位于 IDR帧后的 B帧和 P帧都不能参考 IDR帧以前的帧, 而普通 I帧后的 B帧和 P帧仍然可以参考 I帧之前的其他帧. IDR帧阻断了误差的积累,而 I帧并没有阻断误差的积累.

2.1.3 GOP

Group of picture - 图像组, 通常指两个 I帧之间的帧数
一个 GOP 序列的第一个图像叫做 IDR 图像(立即刷新图像, IDR 图像都是 I 帧图像,但 I帧不一定都是 IDR帧

2.2 WebRTC 拉流逻辑

WebRTC 接收到媒体数据的 udp 包后, 会经过 packet_buffer, 这里负责组帧成完整帧的逻辑判断, 只有完整帧才会继续走下面的解码渲染逻辑.

3. 发现问题

3.1 问题分析

当看到渲染出现灰屏的时候首先怀疑是不是推流的问题, 但推流通常会因为码率过低而导致图片编码质量很低导致的模糊, 基本不会出现这种还有局部很清晰的情况, 所以从拉流一端继续排查.

拉流端可能出现这类问题无非两种问题: 数据错误, 数据丢失.

  • 数据错误
    当出现错误的数据大概率是因为程序 bug, 导致交给解码器的数据并不正确, 但通常这这会出现大面积色块的问题, 并不会出现类似灰屏这种问题.

  • 数据丢失
    组帧逻辑如果有 bug 会导致不完整的帧交给解码器, 出现异常情况.
    WebRTC 的组帧判断的逻辑还是比较健壮的, 应该不会出现丢部分数据的问题.

继续观察显现, 出现的灰屏的时长基本符合我们设置的 GOP 时长, 那么问题大概率出现在关键帧刷新的地方 ( 结合拉流逻辑里对 H264 判定 IDR 帧 ).

为了更好的控制码率, 我在 WebRTC 里集成了 x264 编码器, 和默认的 openh264 有很多参数配置还是有区别的, 然后对比了一下两个编码器关于关键帧的一些设置发现了一些问题, 下面具体针对问题展开.

3.2 对比编码器配置

3.2.1 OpenH264

typedef enum {
  CONSTANT_ID = 0,           ///< constant id in SPS/PPS
  INCREASING_ID = 0x01,      ///< SPS/PPS id increases at each IDR
  SPS_LISTING  = 0x02,       ///< using SPS in the existing list if possible
  SPS_LISTING_AND_PPS_INCREASING  = 0x03,
  SPS_PPS_LISTING  = 0x06,
} EParameterSetStrategy;
  // Reuse SPS id if possible. This helps to avoid reset of chromium HW decoder
  // on each key-frame.
  // Note that WebRTC resets encoder on resolution change which makes all
  // EParameterSetStrategy modes except INCREASING_ID (default) essentially
  // equivalent to CONSTANT_ID.
  encoder_params.eSpsPpsIdStrategy = SPS_LISTING;

OpenH264 的编码器对于Sps/Pps 的设置比较丰富, 具体使用上是对 Sps/Pps 采用尽量重用的方式.

3.2.2 x264

int b_repeat_headers;       /* put SPS/PPS before each keyframe */
param.b_repeat_headers      = 1;

因为我们有可能在推流过程中改变分辨率, 所以采用的是每个关键帧都需要携带 Sps/Pps 才能完成解码.

3.2.3 WebRTC 组帧逻辑

// modules/video_coding/packet_buffer.cc
...
// sps_pps_idr_is_h264_keyframe_ 开关, 默认是 false.
// 当缺失 Sps/Pps 的时候也有可能会被认为是 IDR帧. 
          if ((sps_pps_idr_is_h264_keyframe_ && has_h264_idr && has_h264_sps &&
               has_h264_pps) ||
              (!sps_pps_idr_is_h264_keyframe_ && has_h264_idr)) {
            is_h264_keyframe = true;
            // Store the resolution of key frame which is the packet with
            // smallest index and valid resolution; typically its IDR or SPS
            // packet; there may be packet preceeding this packet, IDR's
            // resolution will be applied to them.
            if (buffer_[start_index]->width() > 0 &&
                buffer_[start_index]->height() > 0) {
              idr_width = buffer_[start_index]->width();
              idr_height = buffer_[start_index]->height();
            }
          }
...

// 如果通过上面的逻辑判定不是关键帧才会判断是否存在丢包情况
// 假如一个 IDR帧的 Sps/Pps 包发生丢包, 在这样的逻辑下是有可能进行解码
// 因为缺少 Sps/Pps 信息, 解码器内部会以普通的 I帧进行处理, 不会清空 DPB(Decoded Picture Buffer参考帧列表)
        // If this is not a keyframe, make sure there are no gaps in the packet
        // sequence numbers up until this point.
        if (!is_h264_keyframe && missing_packets_.upper_bound(start_seq_num) !=
                                     missing_packets_.begin()) {
          return found_frames;
        }

到这里可以大概率的怀疑是因为这个逻辑导致的灰屏. 主要原因:

  1. x264 的 Sps/Pps 逻辑和 OpenH264 不同
  2. 拉流渲染的时候关键帧的 Sps/Pps 包发生丢包或者乱序, 组帧的地方正好符合组帧逻辑, 进行了解码.

下面针对这个猜测进行修改尝试.

4. 尝试解决

既然有了猜测, 那主要的修改就是在于如何打开这个开关.

// video\/rtp_video_stream_receiver2.cc
...
  if (codec_params.count(cricket::kH264FmtpSpsPpsIdrInKeyframe) ||
      field_trial::IsEnabled("WebRTC-SpsPpsIdrIsH264Keyframe")) {
    packet_buffer_.ForceSpsPpsIdrIsH264Keyframe();
  }

可以看到这里有两种打开方式:

  1. 通过 sdp 的的音频 fmtp 增加 sps-pps-idr-in-keyframe
  2. 通过 WebRTC 的全局控制开关

为了能更好的兼容各种情况, 我们采用在 sdp 里携带动态控制开关, 这样可以针对不同的推流选择性的开启这个功能.

经过线上的测试, 打开开关后确实没有再发现有灰屏的问题, 说明这个控制是有效的.

5. TODO

虽然看上去是修改了这个问题, 但其实还是靠猜测和一些无法100% 可控的验证手段, 有几个方面还可以继续展开调研, 可以放倒后面继续做.

  • 通过自己模拟丢包或者乱序去复现问题
  • 解码器针对丢失 Sps/Pps 后的处理逻辑
  • 编码器的设置是否也可以规避这个问题
  • 编码器的 Sps/Pps 的变化原理是什么

你可能感兴趣的:(WebRTC H264 拉流渲染灰屏问题总结(一))