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 的变化原理是什么
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容