资讯详情

8K HDR!|为 Chromium 实现 HEVC 硬解 - 原理/实测指南

HEVC 的现状

背景简介

什么是 HEVC ?简单来说就是比较 H264 支持压缩效率更高的现代视频编码格式 8K,支持 HDR,支持广色域,支持最高 16bit 颜色深度最高 YUV444 总之,色彩抽样是用来取代现有的 H264 更高效、更现代的视频编码格式,并得到了各种硬件的广泛支持。

然而,由于版权和技术派别,浏览器并不支持这种格式,特别是目前市场份额最高的格式 Chrome,一月初看到一个 B 站用户吐槽 HEVC 解码性能/发热新闻(谢谢 B 站在 HEVC WASM 探索解码方案),考虑到这也是困扰行业很长时间的问题,大量依赖 HEVC 的 Web 项目被迫产生各种项目 Workaround 方案,但效果不是最理想的,最好是帮助 Chromium 实现一下 HEVC 硬解吧。

本文简述了 Web 解码方案了解码方案的现状 Chromium 浏览器实现 & 完善硬解过程中遇到的问题和实现原理,并在文章末尾添加测试结果。预编译版本供参考,希望解决 FrontEnd 苦 HEVC 问题已久。

也可提前下载 Chrome Canary(https://www.google.com/chrome/canary/) ,体验 HEVC 硬解功能(ChromeOS、Android、Mac、Windows 需要添加启动参数 --enable-features=PlatformHEVCDecoderSupport,Linux 版本尚未支持)。

主流设备已得到支持和广泛应用

在 2015 年,苹果的 iPhone6s 就已经在其 A9 芯片首次实现 HEVC 硬解能力,同年,Intel 在第六代 Skylake 的 HD500 在系列核显上,NVIDIA 在 GTX900 该系列的独特性也得到了先后的支持 HEVC 硬解。

在 2017 年发布的 iOS11, macOS 10.13 苹果继续完成它 VideoToolbox 编解码框架对 HEVC 微软还发布了编解码能力的支持 HEVC Video Extension 作为 Windows PC 环境 HEVC 解码能力对标。

从此 HEVC 成为苹果,安卓默认视频格式,成为绝大多数单反 / 无人机 / 相机设备的主推格式。

直到今年,也就是 2022 年,iPhone 已经出到了 13.芯片技术得到了改进 5 然而,我们使用的大多数浏览器仍然无法播放纳米 HEVC 视频。

硬解的必要性

更低的发热

硬解是指使用 GPU 由于解码工作是专门用于解码的芯片, GPU 多核心低频,多核心低频、专一的发热和功耗明显低于 CPU。

更好的性能

通过将 CPU 解放繁重的解码工作可以大大降低系统卡顿。

且 GPU 自然适合图形解码,解码性能秒杀 CPU,视频分辨率越高,显卡解码越可以做到不掉帧输出,因此“永远不要指望单纯靠 CPU 软解能流畅播放 8K 60 帧的 HEVC 视频”。

总结

HEVC 是目前桌面端或手机端播放器最主流的编码格式。考虑到其编码复杂性高,解码消耗资源更多,因此有必要实现硬解。

HEVC 解码的方案

浏览器解码现状

先来看看 Web 侧解码现状:

业内常用 Web HEVC 大致可以分为两种解码方案:换浏览器 或 “WASM 软解,各有各的优势和使用场景。

浏览器-Edge (硬解,仅 Windows)

Chromium 内核的 Edge 在 Windows 在系统下,额外支持硬解 HEVC 但必须满足以下条件:

  1. 操作系统版本必须是 Windows 10 1709(16299.0)以后版本。

  2. 安装付费的 HEVC 视频扩展或免费来自设备制造商 HEVC 视频扩展,版本号必须大于或等于 1.0.50361.0(因为一个存在了一年半以上 Bug,老版本有抖动 Bug,Issue:https://techcommunity.microsoft.com/t5/discussions/hevc-video-decoding-broken-with-b-frames/td-p/2077247/page/4)。

3. 版本号必须大于等于 Edge 99 。

安装插件后,进入 edge://gpu 页面,可以查看 Edge 对于 HEVC 硬解支持的 Profile:

如上图所示,则证明硬解开启成功。

指标:

  1. 最高分辨率支持 8192px * 8192px。

  2. 支持 HEVC Main / Main10 / Main Still Picture Profile。

优势:

  1. 性能是显卡支持中最好的。

  2. HTMLVideoElement、MSE 等原生 API 直接支持。

劣势:

  1. 不支持 Windows 8 和老版本 Windows 10。

  2. 需要手动安装插件。

  3. HDR 支持不够好。

浏览器-Safari (硬解,仅 macOS)

由于 Apple 是 HEVC 因此,标准的主要推动者早在 17 年的 Safari 11 即完成了 HEVC 无需安装任何插件开箱即可支持视频硬解。

指标:

  1. 分辨率最高支持 8192px * 8192px。

  2. 支持 HEVC Main / Main10 Profile,M1 模型支持部分 HEVC Rext Profile。

优势:

  1. 性能是显卡支持中最好的。

  2. HTMLVideoElement、MSE 等原生 API 直接支持。

  3. 开箱即用,无需安装插件。

  4. HDR 最好的支持(比如杜比视觉) Profile五、杜比全景声)。

劣势:

  1. 生态不足,缺乏大量 Chromium 内核下的可用、易用插件。

  2. Safari 俗称下一个 IE其浏览器 API 与实现相比,兼容性 Chromium 仍有差距。

  3. 部分 HEVC 即使视频本身没有问题,视频也无法莫名其妙地播放。

前端解码-WASM(软解,任何平台)

这类方案大多基于此类方案 WASM FFMPEG 实现编译,支持所有支持 WASM 的浏览器。

指标:

  1. 支持 FFMPEG 所有分辨率和支持 Profile。

优势:

  1. 纯前端技术实现不挑浏览器。

劣势:

  1. 需要依赖所在版本浏览器 WASM 的稳定性。

  2. 非 HTML Video Element、MSE、EME 原生 API,需要手动用 js 初始化视频播放,使用有成本。

浏览器-本文方案(硬 / 软解,Windows / macOS / Linux)

本文尝试直接为 Chromium 实现硬解,因为尽管 Safari 和 Edge 均已经实现了 HEVC 硬解,但它们均为闭源软件,无法被各种开源框架集成,而因为 Chromium 是开源的,这可以确保所有人可自行编译支持 Windows / macOS / Linux 硬解的 Chromium / Electron / CEF,考虑到实现原理部分较长,因此

HEVC 硬解的实现原理

正是因为如上瓶颈,“让专业的人做专业的事”这句话同样适用视频解码,GPU 硬解是很有必要的。GPU 解码的存在正是为了让解码工作可以充分利用显卡内部专用芯片,分担 CPU 解码时的压力,因此支持更多格式的硬解能力,已然成为众多显卡厂商的一大卖点。

首先我们需要做一些调研,研究下目前硬解框架是如何存在,并支持哪些“系统” or “GPU”。

下表来自 FFMPEG 项目对不同解码框架硬解支持情况的总结(来源:https://trac.ffmpeg.org/wiki/HWAccelIntro)

​硬解框架的支持情况,表格内容来自 FFmpeg 官网

可以看到硬解框架五花八门,不同的显卡厂商和设备有各自的专用解码框架,操作系统也有定义好的通用解码框架,由于显卡厂商众多,因此大部分播放器一般均基于通用框架实现硬解,少部分播放器在人力充裕的情况可能会为了更好的性能(显卡厂商自己的框架一般比通用框架性能更好,但也不绝对)额外对专用框架二次实现。

其中 Windows 平台通用的解码框架有 Media Foundation, Direct3D 11, DXVA2, 以及 OpenCL。macOS 平台通用的解码框架只有一个,也就是苹果自己的 VideoToolbox。Linux 平台的通用解码框架有 VAAPI 和 OpenCL。

显然,对于 Chrome 而言,为了更好的兼容性和稳定性,基于通用硬解框架实现硬解,更符合最小成本最大收益的目标,并提升了可维护性。

理解 Chromium 解码流程

根据 Chromium Media 模块简介可知,浏览器将音视频播放一共抽象成三种类型,我们比较常见的有:Video Element 标签,MSE API。此外还有支持加密视频播放的 EME API,这三种在底层又存在多种复用关系。

​Chromium 的解码流程,图片来自 Chromium 代码仓库

那么到了最底层的解码模块,整体逻辑大概可以简述为:

  1. 浏览器会从列表中依次按照顺序查找 Decoder,通常来说优先级最高的是硬解 Decoder, 然后会尝试软解 Decoder。

  2. 如有命中其中的某个 Decoder 则执行后续解码逻辑。

  3. 如没有命中的 Decoder,则解码失败,中止。

因此,为了实现 HEVC 硬解,我们首先需要找到各个平台的通用硬解 Decoder:

  • 对于 Windows,根据操作系统以及显卡驱动版本,分为两种:D3D11VideoDecoder 和 VDAVideoDecoder,前者在大于 Windows8 且支持 D3D11 的系统默认被使用,后者则在前者不被使用时(比如 Windows 7)作为 Backup 方案被使用。

  • 对于 macOS,为 VDAVideoDecoder。

  • 对于 Linux,为 VAAPIVideoDecoder。

macOS 的硬解

在了解了大致背景后,便可以开始探索实现 HEVC 硬解实现了,考虑到 Apple 其最新 Apple Silicon 芯片专门实现了支持 H.264、HEVC 和 ProRes 的专用编解码媒体处理引擎,看在 Apple 这么努力的份上,我首先挑选了 macOS 平台来进行尝试😑。

FFMPEG 方案的尝试

虽然 Chrome 没有直接实现 HEVC 解码能力,但由于其实现了 FFMpegVideoDecoder,因此本质上任何 FFMPEG 可以播的视频,只要利用修改 Chromium 的方式为其添加 FFMPEG 解码器的入口,理论上均可以实现播放,此方案其实是本文硬解实现前开源社区最广为流传的一种方案,@斯杰的文章(https://www.infoq.cn/article/s65bFDPWzdfP9CQ6Wbw6)内已有详尽介绍,由于当时的版本是基于 Chromium 79,目前最新的 Chromium 版本号为 104,因此里面的一些实现有所变动,但整体逻辑并没有明显改变,通过修改 Chromium 104 依然可以实现软解。

由于是 CPU 软解且使用行业最标准的 FFMPEG 解码,最终结果是:不挑系统,容错性好,支持任何 CPU 架构、操作系统,性能虽比不过硬解,但依然比前端 WASM 方案性能更好,且原生支持 MSE 和 Video Element。

普通的四核笔记本电脑,即使分辨率只有 1080P,在快进或快退时也会感到明显的卡顿,同时伴随比较高的 CPU 占用,抢占渲染进程 CPU 资源,另外这种方法是否有版权有待评估,但可以确定一点,使用平台提供的解码是合规且没有版权风险的。

当分辨率达到 4K 甚至 8K 级别,8 核甚至更多核的 CPU 也会卡到掉帧。

​FFMPEG 的解码流程,图片来自知乎 @我是小北挖哈哈

根据 FFMPEG 的解码流程如上图(参考:https://zhuanlan.zhihu.com/p/168240163?Futm_source=wechat_session&utm_medium=social&utm_oi=29396161265664),可知道,FFMPEG 除了实现了软解,其实已经完整实现了硬解功能,然而 Chromium 的 FFMpegVideoDecoder 并不支持硬解,因此,我们的同学,首先尝试 FFMpegVideoDecoder 内尝试配置 hw_device_ctx,以开启其硬解能力,具体步骤如下:

开启硬解宏:

// third_party/ffmpeg/chromium/config/Chrome/mac/x64/config.h
 
#define CONFIG_VIDEOTOOLBOX 1
#define CONFIG_HEVC_VIDEOTOOLBOX_HWACCEL 1
#define HAVE_KCMVIDEOCODECTYPE_HEVC 1

设置硬件上下文:

// media/filters/ffmpeg_video_decoder.cc -> FFmpegVideoDecoder::ConfigureDecoder(const VideoDecoderConfig& config, bool low_delay)
 
if (decode_nalus_)
    codec_context_->flags2 |= AV_CODEC_FLAG2_CHUNKS;
 
+ if (codec_context_->codec_id == AVCodecID::AV_CODEC_ID_HEVC) {
+     AVBufferRef *hw_device_ctx = NULL;
+     int err;
+     if ((err = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0)) >= 0) {
+         codec_context_->hw_device_ctx = av_buffer_ref(hw_device_ctx);
+     }
+ }
 
const AVCodec* codec = avcodec_find_decoder(codec_context_->codec_id);

取出解码数据:

// media/ffmpeg/ffmpeg_common.cc -> AVPixelFormatToVideoPixelFormat(AVPixelFormat pixel_format)
 
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_YUVJ420P:
case AV_PIX_FMT_VIDEOTOOLBOX: // hwaccel
  return PIXEL_FORMAT_I420;

将硬件解码得到的数据取出,即 av_hwframe_transfer_data 函数:

// media/ffmpeg/ffmpeg_decoding_loop.cc
 
FFmpegDecodingLoop::DecodeStatus FFmpegDecodingLoop::DecodePacket(const AVPacket* packet, FrameReadyCB frame_ready_cb) {
+    AVFrame* tmp_frame = NULL;
+    AVFrame* sw_frame = av_frame_alloc();
    bool sent_packet = false, frames_remaining = true, decoder_error = false;
    while (!sent_packet || frames_remaining) {
        ......
+        if (frame_.get()->format == AV_PIX_FMT_VIDEOTOOLBOX) {
+            int ret = av_hwframe_transfer_data(sw_frame, frame_.get(), 0);
+            tmp_frame = sw_frame;
+        } else {
+            tmp_frame = frame_.get();
+        }
+        const bool frame_processing_success = frame_ready_cb.Run(tmp_frame);
+        av_frame_unref(tmp_frame);
-        const bool frame_processing_success = frame_ready_cb.Run(frame_.get());
        av_frame_unref(frame_.get());
        if (!frame_processing_success)
            return DecodeStatus::kFrameProcessingFailed;
        }
 
    return decoder_error ? DecodeStatus::kDecodeFrameFailed : DecodeStatus::kOkay;
}

如上,经过多次尝试后,通过活动监视器可以观察到点击< Video >标签播放按钮时 VTDecoderXPCService 进程(Videotoolbox 的解码进程)CPU 占有率有所上升,说明调用 VideoToolbox 硬件解码模块成功,但视频白屏说明解码失败。

探索过程中,阅读 Chromium Media 模块的文档后发现,使用 FFMpegVideoDecoder 不支持在 Sandboxed 的进程调用 VT 硬解框架,为了避免在错误的道路上投入过多精力,遂放弃。

在 GPU 进程实现

上面的方式行不通,说明得换一种思路,需要看看正统的 H264 硬解流程是怎样的,通过使用 Chrome 的搜索引擎(https://source.chromium.org/),发现 macOS 的 H264 硬解实现均位于vt_video_decoder_accelerator.cc这个文件内。

VideoToolbox 简介

由 FFmpeg 介绍可知,如我们想在 macOS 实现 HEVC 硬解,则一定需要使用苹果提供的媒体解码框架 VideoToolbox 来完成。

VideoToolbox is a low-level framework that provides direct access to hardware encoders and decoders. It provides services for video compression and decompression, and for conversion between raster image formats stored in CoreVideo pixel buffers. These services are provided in the form of session objects (compression, decompression, and pixel transfer), which are vended as Core Foundation (CF) types. Apps that don't need direct access to hardware encoders and decoders should not need to use VideoToolbox directly.

根据 Apple Developer 网站介绍(https://developer.apple.com/documentation/videotoolbox)可知,VideoToolbox 是苹果提供的直接用来进行编解码的底层框架,要实现硬解,大体解码流程可以理解为:Chromium -> VDAVideoDecoder -> VideoToolbox -> GPU -> VideoToolbox -> VDAVideoDecoder -> Chromium。

因此我们的目标就是正确按照 VideoToolbox 要求的方式,提交 Image Buffer,并等待 VT 将解码后的数据回传。

添加 Supported Profile

根据 Chromium 解码流程 可知,Chromium 对于特定 Codec 的视频首先会尝试查找硬解 Decoder,如硬解 Decoder 不支持,则继续向后查找 Fallback 的软解 Decoder。

通过观察可发现,在 macOS 下,某种编码格式是否支持硬解,取决于硬解 Decoder 内的 SupportProfiles 是否包含这种编码格式,其代码如下:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
// 这个数组内包含了所有可能支持的Profile,但是否真正支持并不取决于这里
constexpr VideoCodecProfile kSupportedProfiles[] = {
    H264PROFILE_BASELINE, H264PROFILE_EXTENDED, H264PROFILE_MAIN,
    H264PROFILE_HIGH,
 
    // macOS 11以上,会尝试对这两种格式进行硬解
    VP9PROFILE_PROFILE0, VP9PROFILE_PROFILE2,
 
    // macOS 11以上,支持的最主流的HEVC Main / Main10 Profile, 以及
    // Main Still Picture / Main Rext 的硬、软解
    // (Apple Silicon 机型支持硬解HEVC Rext, Intel 机型支持软解HEVC Rext)
    // These are only supported on macOS 11+.
    HEVCPROFILE_MAIN, HEVCPROFILE_MAIN10, HEVCPROFILE_MAIN_STILL_PICTURE,
    HEVCPROFILE_REXT,
 
    // TODO(sandersd): Hi10p fails during
    // CMVideoFormatDescriptionCreateFromH264ParameterSets with
    // kCMFormatDescriptionError_InvalidParameter.
    //
    // H264PROFILE_HIGH10PROFILE,
 
    // TODO(sandersd): Find and test media with these profiles before enabling.
    //
    // H264PROFILE_SCALABLEBASELINE,
    // H264PROFILE_SCALABLEHIGH,
    // H264PROFILE_STEREOHIGH,
    // H264PROFILE_MULTIVIEWHIGH,
};

Session 预热与引导逻辑

实现硬解,需要在 Sandboxed 的进程启用前创建解码 Session 预热,并根据系统版本与支持情况决定最终是否启用硬解:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
bool InitializeVideoToolbox() {
  // 在GPU主进程调用时立刻执行,以确保Sandboxed/非Sandoxed进程均可硬解
  static const bool succeeded = InitializeVideoToolboxInternal();
  return succeeded;
}
 
// 在GPU Sandbox启用前通过创建Videotoolbox的Decompression Session预热,确保Sandboxed/非Sandoxed进程均可硬解
bool InitializeVideoToolboxInternal() {
  VTDecompressionOutputCallbackRecord callback = {0};
  base::ScopedCFTypeRef<VTDecompressionSessionRef> session;
  gfx::Size configured_size;
 
  // 创建H264硬解Session
  const std::vector<uint8_t> sps_h264_normal = {
      0x67, 0x64, 0x00, 0x1e, 0xac, 0xd9, 0x80, 0xd4, 0x3d, 0xa1, 0x00, 0x00,
      0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x16, 0x2d, 0x9a};
  const std::vector<uint8_t> pps_h264_normal = {0x68, 0xe9, 0x7b, 0xcb};
  if (!CreateVideoToolboxSession(
          CreateVideoFormatH264(sps_h264_normal, std::vector<uint8_t>(),
                                pps_h264_normal),
          /*require_hardware=*/true, /*is_hbd=*/false, &callback, &session,
          &configured_size)) {
    // 如果H264硬解Session创建失败,直接禁用整个硬解模块
    DVLOG(1) << "Hardware H264 decoding with VideoToolbox is not supported";
    return false;
  }
 
  session.reset();
 
  // 创建H264软解Session
  // 总结下,如果这台设备连H264硬/软解都不支持,则直接禁用硬解,解码完全走FFMpegVideoDecoder的软解
  const std::vector<uint8_t> sps_h264_small = {
      0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, 0x89, 0x7e, 0x22, 0x10, 0x00,
      0x00, 0x3e, 0x90, 0x00, 0x0e, 0xa6, 0x08, 0xf1, 0x22, 0x59, 0xa0};
  const std::vector<uint8_t> pps_h264_small = {0x68, 0xe9, 0x79, 0x72, 0xc0};
  if (!CreateVideoToolboxSession(
          CreateVideoFormatH264(sps_h264_small, std::vector<uint8_t>(),
                                pps_h264_small),
          /*require_hardware=*/false, /*is_hbd=*/false, &callback, &session,
          &configured_size)) {
    DVLOG(1) << "Software H264 decoding with VideoToolbox is not supported";
    // 如果H264软解 Decompression Session创建失败,直接禁用整个硬解模块
    return false;
  }
 
  session.reset();
 
  if (__builtin_available(macOS 11.0, *)) {
    VTRegisterSupplementalVideoDecoderIfAvailable(kCMVideoCodecType_VP9);
 
    // 当系统大于等于macOS Big Sur时,尝试创建VP9硬解Session
    if (!CreateVideoToolboxSession(
            CreateVideoFormatVP9(VideoColorSpace::REC709(), VP9PROFILE_PROFILE0,
                                 absl::nullopt, gfx::Size(720, 480)),
            /*require_hardware=*/true, /*is_hbd=*/false, &callback, &session,
            &configured_size)) {
      DVLOG(1) << "Hardware VP9 decoding with VideoToolbox is not supported";
      // 如果创建session失败,说明不支持VP9硬解,跳过,但保持H264可继续硬解
    }
  }
 
// 按照Chromium的要求HEVC硬解相关的逻辑,均需要依赖ENABLE_HEVC_PARSER_AND_HW_DECODER宏定义开关,只有开启开关后才会将代码引入
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
  // 即使编译时开启了HEVC硬解宏
  // 当启动时传入`--enable-features=PlatformHEVCDecoderSupport`可启用HEVC硬解
  if (base::FeatureList::IsEnabled(media::kPlatformHEVCDecoderSupport)) {
    // 这里限制了至少是Big Sur系统的原因是,Catalina及以下系统使用
    // CMVideoFormatDescriptionCreateFromHEVCParameterSets API创建解码Session
    // 会失败
    // 注:macOS自身问题,苹果承诺了10.13及以上系统即可使用这个API,然,实测结果并卵
    // 但VLC和FFmpeg等使用的CMVideoFormatDescriptionCreate可以正常创建
    // 但,这与硬解模块实现的风格和结构不符
    if (__builtin_available(macOS 11.0, *)) {
      session.reset();
 
      // 创建HEVC硬解Session
      // vps/sps/pps提取自bear-1280x720-hevc.mp4
      const std::vector<uint8_t> vps_hevc_normal = {
          0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,
          0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,
          0x00, 0x00, 0x03, 0x00, 0x5d, 0x95, 0x98, 0x09};
 
      const std::vector<uint8_t> sps_hevc_normal = {
          0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00,
          0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x5d, 0xa0, 0x02, 0x80, 0x80,
          0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x5a, 0x70, 0x80,
          0x00, 0x01, 0xf4, 0x80, 0x00, 0x3a, 0x98, 0x04};
 
      const std::vector<uint8_t> pps_hevc_normal = {0x44, 0x01, 0xc1, 0x72,
                                                    0xb4, 0x62, 0x40};
 
      if (!CreateVideoToolboxSession(
              CreateVideoFormatHEVC(vps_hevc_normal, sps_hevc_normal,
                                    pps_hevc_normal),
              /*require_hardware=*/true, /*is_hbd=*/false, &callback, &session,
              &configured_size)) {
        DVLOG(1) << "Hardware HEVC decoding with VideoToolbox is not supported";
        // 同VP9逻辑,HEVC硬解预热失败不会禁用H264硬解能力
      }
 
      session.reset();
 
      // 创建HEVC软解Session
      // vps/sps/pps提取自bear-320x240-v_frag-hevc.mp4
      const std::vector<uint8_t> vps_hevc_small = {
          0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,
          0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,
          0x00, 0x00, 0x03, 0x00, 0x3c, 0x95, 0x98, 0x09};
 
      const std::vector<uint8_t> sps_hevc_small = {
          0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90,
          0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3c, 0xa0, 0x0a,
          0x08, 0x0f, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0xc0, 0x40,
          0x40, 0x00, 0x00, 0xfa, 0x40, 0x00, 0x1d, 0x4c, 0x02};
 
      const std::vector<uint8_t> pps_hevc_small = {0x44, 0x01, 0xc1, 0x72,
                                                   0xb4, 0x62, 0x40};
 
      if (!CreateVideoToolboxSession(
              CreateVideoFormatHEVC(vps_hevc_small, sps_hevc_small,
                                    pps_hevc_small),
              /*require_hardware=*/false, /*is_hbd=*/false, &callback, &session,
              &configured_size)) {
        DVLOG(1) << "Software HEVC decoding with VideoToolbox is not supported";
 
        //  同VP9逻辑,HEVC软解预热失败不会禁用H264硬解能力
      }
    }
  }
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
  return true;
}
 
// 实际的最终判断逻辑
VideoDecodeAccelerator::SupportedProfiles
VTVideoDecodeAccelerator::GetSupportedProfiles(
    const gpu::GpuDriverBugWorkarounds& workarounds) {
  SupportedProfiles profiles;
  // H264硬/软解不支持时,禁用硬解模块
  if (!InitializeVideoToolbox())
    return profiles;
 
  for (const auto& supported_profile : kSupportedProfiles) {
    // 目前仅支持VP9 PROFILE0、2两种Profile
    if (supported_profile == VP9PROFILE_PROFILE0 ||
        supported_profile == VP9PROFILE_PROFILE2) {
      // 所有GPU模块的解码都会先读取依赖GPU Workaround
      // 比如需要禁用特定型号或厂商的GPU对特定Codec的硬解支持
      // 则可利用GPU Workaround下发禁用配置
      if (workarounds.disable_accelerated_vp9_decode)
        continue;
      if (!base::mac::IsAtLeastOS11())
        // 系统版本不支持VP9硬解,跳过
        continue;
      if (__builtin_available(macOS 10.13, *)) {
        if ((supported_profile == VP9PROFILE_PROFILE0 ||
             supported_profile == VP9PROFILE_PROFILE2) &&
            !VTIsHardwareDecodeSupported(kCMVideoCodecType_VP9)) {
          // Profile不支持,或操作系统不支持VP9硬解,跳过
          continue;
        }
 
        // 经过GPU workaround、操作系统版本、Profile、以及OS是否支持VP9硬解检查,最终确认支持VP9硬解,并接管解码权限
      } else {
        // 系统版本不支持VP9硬解,跳过
        continue;
      }
    }
 
    // 目前支持HEVC Main、Main10、MSP、Rext四种Profile
    if (supported_profile == HEVCPROFILE_MAIN ||
        supported_profile == HEVCPROFILE_MAIN10 ||
        supported_profile == HEVCPROFILE_MAIN_STILL_PICTURE ||
        supported_profile == HEVCPROFILE_REXT) {
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
      if (!workarounds.disable_accelerated_hevc_decode &&
          base::FeatureList::IsEnabled(kPlatformHEVCDecoderSupport)) {
        if (__builtin_available(macOS 11.0, *)) {
          // 经过GPU workaround、操作系统版本、Profile,编译开关,启动开关检查,最终确认支持HEVC硬解(软解我们也使用Videotoolbox来做,原因后面说),并接管解码权限
          SupportedProfile profile;
          profile.profile = supported_profile;
          profile.min_resolution.SetSize(16, 16);
          // HEVC最大可支持8k 👍
          profile.max_resolution.SetSize(8192, 8192);
          profiles.push_back(profile);
        }
      }
#endif  //  BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
      continue;
    }
 
    // H264和VP9最大支持4k
    SupportedProfile profile;
    profile.profile = supported_profile;
    profile.min_resolution.SetSize(16, 16);
    profile.max_resolution.SetSize(4096, 4096);
    profiles.push_back(profile);
  }
  return profiles;
}

如上,经过 GPU workaround、操作系统版本、Profile、编译开关、启动开关检查,最终如果校验通过,则 HEVC 解码逻辑会由 VideoToolbox 接管,并由 VTDecoderXPCService 进程最终实际负责解码。

理解 HEVC 的 NALU 类型

NALU (network abstraction layer unit),即网络抽象层单元,是 H.264 / AVC 和 HEVC 视频编码标准的核心定义,按白话理解,就是 H264 / HEVC 为不同的视频单元定义了的不同的类型(参考),感兴趣可自行百科,这里不再赘述。对于 H264,存在 32 种,其中保留 Nalu 有 8 种。到了 HEVC,被扩展到了 64 种,保留 Nalu 有 16 种。

​H264 的 Nalu Unit 组成,图片来自 Apple

// media/video/h265_nalu_parser.h
 
enum Type {
    TRAIL_N = 0,  // coded slice segment of a non TSA(Temporal Sub-layer Access)
                  // trailing picture
    TRAIL_R = 1,  // coded slice segment of a non TSA(Temporal Sub-layer Access)
                  // trailing picture
    TSA_N = 2,    // coded slice segment of a TSA(Temporal Sub-layer Access)
                  // trailing picture
    TSA_R = 3,    // coded slice segment of a TSA(Temporal Sub-layer Access)
                  // trailing picture
    STSA_N = 4,   // coded slice segment of a STSA(Step-wise Temporal Sub-layer
                  // Access) trailing picture
    STSA_R = 5,   // coded slice segment of a STSA(Step-wise Temporal Sub-layer
                  // Access) trailing picture
    RADL_N = 6,   // coded slice segment of a RADL(Random Access Decodable
                  // Leading) leading picture
    RADL_R = 7,   // coded slice segment of a RADL(Random Access Decodable
                  // Leading) leading picture
    RASL_N = 8,   // coded slice segment of a RASL(Random Access Skipped
                  // Leading)L leading picture
    RASL_R = 9,  // coded slice segment of a RASL(Random Access Skipped Leading)
                 // leading picture
    RSV_VCL_N10 = 10,     // reserved non-IRAP SLNR VCL
    RSV_VCL_R11 = 11,     // reserved non-IRAP sub-layer reference VCL
    RSV_VCL_N12 = 12,     // reserved non-IRAP SLNR VCL
    RSV_VCL_R13 = 13,     // reserved non-IRAP sub-layer reference VCL
    RSV_VCL_N14 = 14,     // reserved non-IRAP SLNR VCL
    RSV_VCL_R15 = 15,     // reserved non-IRAP sub-layer reference VCL
    BLA_W_LP = 16,        // coded slice segment of a BLA IRAP picture
    BLA_W_RADL = 17,      // coded slice segment of a BLA IRAP picture
    BLA_N_LP = 18,        // coded slice segment of a BLA IRAP picture
    IDR_W_RADL = 19,      // coded slice segment of an IDR IRAP picture
    IDR_N_LP = 20,        // coded slice segment of an IDR IRAP picture
    CRA_NUT = 21,         // coded slice segment of a CRA IRAP picture
    RSV_IRAP_VCL22 = 22,  // reserved IRAP(intra random access point) VCL
    RSV_IRAP_VCL23 = 23,  // reserved IRAP(intra random access point) VCL
    RSV_VCL24 = 24,       // reserved non-IRAP VCL
    RSV_VCL25 = 25,       // reserved non-IRAP VCL
    RSV_VCL26 = 26,       // reserved non-IRAP VCL
    RSV_VCL27 = 27,       // reserved non-IRAP VCL
    RSV_VCL28 = 28,       // reserved non-IRAP VCL
    RSV_VCL29 = 29,       // reserved non-IRAP VCL
    RSV_VCL30 = 30,       // reserved non-IRAP VCL
    RSV_VCL31 = 31,       // reserved non-IRAP VCL
    VPS_NUT = 32,         // vps(video parameter sets)
    SPS_NUT = 33,         // sps(sequence parameter sets)
    PPS_NUT = 34,         // pps(picture parameter sets)
    AUD_NUT = 35,         // access unit delimiter
    EOS_NUT = 36,         // end of sequence
    EOB_NUT = 37,         // end of bitstream
    FD_NUT = 38,          // filter Data
    PREFIX_SEI_NUT = 39,  // sei
    SUFFIX_SEI_NUT = 40,  // sei
    RSV_NVCL41 = 41,      // reserve
    RSV_NVCL42 = 42,      // reserve
    RSV_NVCL43 = 43,      // reserve
    RSV_NVCL44 = 44,      // reserve
    RSV_NVCL45 = 45,      // reserve
    RSV_NVCL46 = 46,      // reserve
    RSV_NVCL47 = 47,      // reserve
    UNSPEC48 = 48,        // unspecified
    UNSPEC49 = 49,        // unspecified
    UNSPEC50 = 50,        // unspecified
    UNSPEC51 = 51,        // unspecified
    UNSPEC52 = 52,        // unspecified
    UNSPEC53 = 53,        // unspecified
    UNSPEC54 = 54,        // unspecified
    UNSPEC55 = 55,        // unspecified
    UNSPEC56 = 56,        // unspecified
    UNSPEC57 = 57,        // unspecified
    UNSPEC58 = 58,        // unspecified
    UNSPEC59 = 59,        // unspecified
    UNSPEC60 = 60,        // unspecified
    UNSPEC61 = 61,        // unspecified
    UNSPEC62 = 62,        // unspecified
    UNSPEC63 = 63,        // unspecified
  };

解析 SPS / PPS / VPS

如想实现 HEVC 解码,首先需要拿到视频的元数据,这就需要通过解析 NALU 类型为 32 (VPS_NUT), 33 (SPS_NUT), 34 (PPS_NUT)的 Nalu Header 来获取。

举个最基本的例子,如果我们希望获取视频的宽高,则需要解析SPS_NUT的 Nalu Header,并取sps->pic_width_in_luma_samples的值,以此类推。

通常媒体开发会使用一个叫做StreamAnalyzer的工具(链接:https://www.elecard.com/zh/products/video-analysis/stream-analyzer)快速解析视频 Nalu Header,我们要做的事其实和这个软件做的差不多:

​Stream Analyzer 解析 Nalu Header 示意

可以看到,SPS_NUT的 Nalu Header 解析后的数据如截图右侧区域显示,感谢 Elecard 开发的这款好用的工具,有了它对我们实现 VPS 解析有很大帮助。

观察 Chromium 的代码结构发现 @Jeffery Kardatzke 大佬已经于 2020 年底完成 Linux 平台 Vappi HEVC 的硬解加速实现,和 H265 Nalu Parse 的大部分逻辑实现,由于 Linux 平台硬解并不需要提取 VPS 参数,因为大佬没有实现 VPS 解析,但根据 Apple Developer 的说明,若我们使用CMVideoFormatDescriptionCreateFromHEVCParameterSets API 创建解码 session,需要提供 VPS, SPS, PPS 三种类型的 Nalu Data,因此实现 macOS 硬解的很大一部分工作即是完成 VPS NALU 的 Header 解析:

首先,参考 T-REC-H.265-202108-I,以及 FFMPEG 定义好的 H265RawVPS Struct Reference,我们需要定义好要解析的 VPS 结构体类型:

// media/video/h265_parser.h
 
// 定义H265VPS的结构体
struct MEDIA_EXPORT H265VPS {
  H265VPS();
 
  int vps_video_parameter_set_id; // 即vps_id,稍后需要用到
  bool vps_base_layer_internal_flag;
  bool vps_base_layer_available_flag;
  int vps_max_layers_minus1;
  int vps_max_sub_layers_minus1;
  bool vps_temporal_id_nesting_flag;
  H265ProfileTierLevel profile_tier_level;
  int vps_max_dec_pic_buffering_minus1[kMaxSubLayers]; // 稍后需要用到
  int vps_max_num_reorder_pics[kMaxSubLayers]; // 稍后需要用到
  int vps_max_latency_increase_plus1[kMaxSubLayers];
  int vps_max_layer_id;
  int vps_num_layer_sets_minus1;
  bool vps_timing_info_present_flag;
 
  // 剩余部分我们不需要,因此暂未实现解析逻辑
};

接着,我们需要完成 H265VPS 的解析逻辑:

// media/video/h265_parser.cc
 
// 解析VPS逻辑
H265Parser::Result H265Parser::ParseVPS(int* vps_id) {
  DVLOG(4) << "Parsing VPS";
  Result res = kOk;
 
  DCHECK(vps_id);
  *vps_id = -1;
 
  std::unique_ptr<H265VPS> vps = std::make_unique<H265VPS>();
 
  // 读4Bit
  READ_BITS_OR_RETURN(4, &vps->vps_video_parameter_set_id);
  // 校验读取结果是否为0-16区间内的值
  IN_RANGE_OR_RETURN(vps->vps_video_parameter_set_id, 0, 16);
  READ_BOOL_OR_RETURN(&vps->vps_base_layer_internal_flag);
  READ_BOOL_OR_RETURN(&vps->vps_base_layer_available_flag);
  READ_BITS_OR_RETURN(6, &vps->vps_max_layers_minus1);
  IN_RANGE_OR_RETURN(vps->vps_max_layers_minus1, 0, 62);
  READ_BITS_OR_RETURN(3, &vps->vps_max_sub_layers_minus1);
  IN_RANGE_OR_RETURN(vps->vps_max_sub_layers_minus1, 0, 7);
  READ_BOOL_OR_RETURN(&vps->vps_temporal_id_nesting_flag);
  SKIP_BITS_OR_RETURN(16);  // 跳过vps_reserved_0xffff_16bits
  res = ParseProfileTierLevel(true, vps->vps_max_sub_layers_minus1,
                              &vps->profile_tier_level);
  if (res != kOk) {
    return res;
  }
 
  bool vps_sub_layer_ordering_info_present_flag;
  READ_BOOL_OR_RETURN(&vps_sub_layer_ordering_info_present_flag);
 
  for (int i = vps_sub_layer_ordering_info_present_flag
                   ? 0
                   : vps->vps_max_sub_layers_minus1;
       i <= vps->vps_max_sub_layers_minus1; ++i) {
    READ_UE_OR_RETURN(&vps->vps_max_dec_pic_buffering_minus1[i]);
    IN_RANGE_OR_RETURN(vps->vps_max_dec_pic_buffering_minus1[i], 0, 15);
    READ_UE_OR_RETURN(&vps->vps_max_num_reorder_pics[i]);
    IN_RANGE_OR_RETURN(vps->vps_max_num_reorder_pics[i], 0,
                       vps->vps_max_dec_pic_buffering_minus1[i]);
    if (i > 0) {
      TRUE_OR_RETURN(vps->vps_max_dec_pic_buffering_minus1[i] >=
                     vps->vps_max_dec_pic_buffering_minus1[i - 1]);
      TRUE_OR_RETURN(vps->vps_max_num_reorder_pics[i] >=
                     vps->vps_max_num_reorder_pics[i - 1]);
    }
    READ_UE_OR_RETURN(&vps->vps_max_latency_increase_plus1[i]);
  }
  if (!vps_sub_layer_ordering_info_present_flag) {
    for (int i = 0; i < vps->vps_max_sub_layers_minus1; ++i) {
      vps->vps_max_dec_pic_buffering_minus1[i] =
          vps->vps_max_dec_pic_buffering_minus1[vps->vps_max_sub_layers_minus1];
      vps->vps_max_num_reorder_pics[i] =
          vps->vps_max_num_reorder_pics[vps->vps_max_sub_layers_minus1];
      vps->vps_max_latency_increase_plus1[i] =
          vps->vps_max_latency_increase_plus1[vps->vps_max_sub_layers_minus1];
    }
  }
 
  READ_BITS_OR_RETURN(6, &vps->vps_max_layer_id);
  IN_RANGE_OR_RETURN(vps->vps_max_layer_id, 0, 62);
  READ_UE_OR_RETURN(&vps->vps_num_layer_sets_minus1);
  IN_RANGE_OR_RETURN(vps->vps_num_layer_sets_minus1, 0, 1023);
 
  *vps_id = vps->vps_video_parameter_set_id;
  // 如果存在相同vps_id的vps,则直接替换
  active_vps_[*vps_id] = std::move(vps);
 
  return res;
}
 
// 获取VPS逻辑
const H265VPS* H265Parser::GetVPS(int vps_id) const {
  auto it = active_vps_.find(vps_id);
  if (it == active_vps_.end()) {
    DVLOG(1) << "Requested a nonexistent VPS id " << vps_id;
    return nullptr;
  }
 
  return it->second.get();
}

完善编写 Unit Test 和 Fuzzer Test:

// media/video/h265_parser_unittest.cc
 
TEST_F(H265ParserTest, VpsParsing) {
  LoadParserFile("bear.hevc");
  H265NALU target_nalu;
  EXPECT_TRUE(ParseNalusUntilNut(&target_nalu, H265NALU::VPS_NUT));
  int vps_id;
  EXPECT_EQ(H265Parser::kOk, parser_.ParseVPS(&vps_id));
  const H265VPS* vps = parser_.GetVPS(vps_id);
  EXPECT_TRUE(!!vps);
  EXPECT_TRUE(vps->vps_base_layer_internal_flag);
  EXPECT_TRUE(vps->vps_base_layer_available_flag);
  EXPECT_EQ(vps->vps_max_layers_minus1, 0);
  EXPECT_EQ(vps->vps_max_sub_layers_minus1, 0);
  EXPECT_TRUE(vps->vps_temporal_id_nesting_flag);
  EXPECT_EQ(vps->profile_tier_level.general_profile_idc, 1);
  EXPECT_EQ(vps->profile_tier_level.general_level_idc, 60);
  EXPECT_EQ(vps->vps_max_dec_pic_buffering_minus1[0], 4);
  EXPECT_EQ(vps->vps_max_num_reorder_pics[0], 2);
  EXPECT_EQ(vps->vps_max_latency_increase_plus1[0], 0);
  for (int i = 1; i < kMaxSubLayers; ++i) {
    EXPECT_EQ(vps->vps_max_dec_pic_buffering_minus1[i], 0);
    EXPECT_EQ(vps->vps_max_num_reorder_pics[i], 0);
    EXPECT_EQ(vps->vps_max_latency_increase_plus1[i], 0);
  }
  EXPECT_EQ(vps->vps_max_layer_id, 0);
  EXPECT_EQ(vps->vps_num_layer_sets_minus1, 0);
  EXPECT_FALSE(vps->vps_timing_info_present_flag);
}
// media/video/h265_parser_fuzzertest.cc
 
case media::H265NALU::VPS_NUT:
  int vps_id;
  res = parser.ParseVPS(&vps_id);
  break;

由于 FFMPEG 已经实现了 VPS 的解析逻辑,因此这里大部分逻辑与 FFMPEG 保持一致即可,经过 UnitTest 测试(编译步骤:autoninja -C out/Release64 media_unittests) 确认无问题,对照 StreamAnalyzer 同样无问题后,完成 VPS 解析逻辑实现。

这里跳过 SPS, PPS, SliceHeader 的解析逻辑,因为代码量过大且琐碎,感兴趣可参考 h265_parser.cc(https://source.chromium.org/chromium/chromium/src/+/main:media/video/h265_parser.cc)

计算 POC (Picture Order Count)

我们知道 H264 / HEVC 视频帧类型大体上有三种:I 帧,P 帧,B 帧,其中 I 帧又称全帧压缩编码帧,为整个 GOP(一个存在了 I,P,B 的帧组)内的第一帧,解码无需参考其他帧,P 帧又称前向预测编码帧,解码需要参考前面的 I,P 帧解码,B 帧又称双向预测内插编码帧,解码需要参考前面的 I、P 帧和后面的 P 帧。

一共存在的这三种帧,他们在编码时不一定会按顺序写入视频流,因此在解码时为了获取不同帧的正确顺序,需要计算图片的顺序即 POC。

StreamEye 解析后的 GOP POC 结果示意

如上图 StreamEye 解析结果所示,POC 呈现:0 -> 4 -> 2 -> 1 -> 3 -> 8 -> 6 ... 规律。

不同帧的出现顺序对于解码来说至关重要,因此我们需要在不同帧解码后对帧按 POC 重新排序,最终确保解码图像按照实际顺序呈现给用户:0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8。

苹果的 VideoToolbox 并不会给我们实现这部分逻辑,因此我们需要自行计算 POC 顺序,并在之后重排序,代码实现如下:

// media/video/h265_poc.h
 
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
 
#ifndef MEDIA_VIDEO_H265_POC_H_
#define MEDIA_VIDEO_H265_POC_H_
 
#include <stdint.h>
 
#include "third_party/abseil-cpp/absl/types/optional.h"
 
namespace media {
 
struct H265SPS;
struct H265PPS;
struct H265SliceHeader;
 
class MEDIA_EXPORT H265POC {
 public:
  H265POC();
 
  H265POC(const H265POC&) = delete;
  H265POC& operator=(const H265POC&) = delete;
 
  ~H265POC();
 
  // 根据SPS和PPS以及解析好的SliceHeader信息计算POC
  int32_t ComputePicOrderCnt(const H265SPS* sps,
                             const H265PPS* pps,
                             const H265SliceHeader& slice_hdr);
  void Reset();
 
 private:
  int32_t ref_pic_order_cnt_msb_;
  int32_t ref_pic_order_cnt_lsb_;
  // 是否为解码过程的首张图
  bool first_picture_;
};
 
}  // namespace media
 
#endif  // MEDIA_VIDEO_H265_POC_H_

POC 的计算逻辑:

// media/video/h265_poc.cc
 
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
 
#include <stddef.h>
 
#include <algorithm>
 
#include "base/cxx17_backports.h"
#include "base/logging.h"
#include "media/video/h265_parser.h"
#include "media/video/h265_poc.h"
 
namespace media {
 
H265POC::H265POC() {
  Reset();
}
 
H265POC::~H265POC() = default;
 
void H265POC::Reset() {
  ref_pic_order_cnt_msb_ = 0;
  ref_pic_order_cnt_lsb_ = 0;
  first_picture_ = true;
}
 
// 如下逻辑所示,我们需要按照HEVC Spec的规范计算POC
//(这里我参考了Jeffery Kardatzke在H265Decoder的实现逻辑)
int32_t H265POC::ComputePicOrderCnt(const H265SPS* sps,
                                    const H265PPS* pps,
                                    const H265SliceHeader& slice_hdr) {
  int32_t pic_order_cnt = 0;
  int32_t max_pic_order_cnt_lsb =
      1 << (sps->log2_max_pic_order_cnt_lsb_minus4 + 4);
  int32_t pic_order_cnt_msb;
  int32_t no_rasl_output_flag;
  // Calculate POC for current picture.
  if (slice_hdr.irap_pic) {
    // 8.1.3
    no_rasl_output_flag = (slice_hdr.nal_unit_type >= H265NALU::BLA_W_LP &&
                           slice_hdr.nal_unit_type <= H265NALU::IDR_N_LP) ||
                          first_picture_;
  } else {
    no_rasl_output_flag = false;
  }
 
  if (!slice_hdr.irap_pic || !no_rasl_output_flag) {
    int32_t prev_pic_order_cnt_lsb = ref_pic_order_cnt_lsb_;
    int32_t prev_pic_order_cnt_msb = ref_pic_order_cnt_msb_;
 
    if ((slice_hdr.slice_pic_order_cnt_lsb < prev_pic_order_cnt_lsb) &&
        ((prev_pic_order_cnt_lsb - slice_hdr.slice_pic_order_cnt_lsb) >=
         (max_pic_order_cnt_lsb / 2))) {
      pic_order_cnt_msb = prev_pic_order_cnt_msb + max_pic_order_cnt_lsb;
    } else if ((slice_hdr.slice_pic_order_cnt_lsb > prev_pic_order_cnt_lsb) &&
               ((slice_hdr.slice_pic_order_cnt_lsb - prev_pic_order_cnt_lsb) >
                (max_pic_order_cnt_lsb / 2))) {
      pic_order_cnt_msb = prev_pic_order_cnt_msb - max_pic_order_cnt_lsb;
    } else {
      pic_order_cnt_msb = prev_pic_order_cnt_msb;
    }
  } else {
    pic_order_cnt_msb = 0;
  }
 
  // 8.3.1 Decoding process for picture order count.
  if (!pps->temporal_id && (slice_hdr.nal_unit_type < H265NALU::RADL_N ||
                            slice_hdr.nal_unit_type > H265NALU::RSV_VCL_N14)) {
    ref_pic_order_cnt_lsb_ = slice_hdr.slice_pic_order_cnt_lsb;
    ref_pic_order_cnt_msb_ = pic_order_cnt_msb;
  }
 
  pic_order_cnt = pic_order_cnt_msb + slice_hdr.slice_pic_order_cnt_lsb;
  first_picture_ = false;
 
  return pic_order_cnt;
}
 
}  // namespace media

计算 MaxReorderCount

计算 POC 并解码后,为了确保视频帧按照正确的顺序展示给用户,需要对视频帧进行 Reorder 重排序,我们可以观察 H264 的最大 Reorder 数计算逻辑,发现很复杂:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
// H264最大Reorder数的计算逻辑
int32_t ComputeH264ReorderWindow(const H264SPS* sps) {
  // When |pic_order_cnt_type| == 2, decode order always matches presentation
  // order.
  // TODO(sandersd): For |pic_order_cnt_type| == 1, analyze the delta cycle to
  // find the minimum required reorder window.
  if (sps->pic_order_cnt_type == 2)
    return 0;
 
  int max_dpb_mbs = H264LevelToMaxDpbMbs(sps->GetIndicatedLevel());
  int max_dpb_frames =
      max_dpb_mbs / ((sps->pic_width_in_mbs_minus1 + 1) *
                     (sps->pic_height_in_map_units_minus1 + 1));
  max_dpb_frames = std::clamp(max_dpb_frames, 0, 16);
 
  // See AVC spec section E.2.1 definition of |max_num_reorder_frames|.
  if (sps->vui_parameters_present_flag && sps->bitstream_restriction_flag) {
    return std::min(sps->max_num_reorder_frames, max_dpb_frames);
  } else if (sps->constraint_set3_flag) {
    if (sps->profile_idc == 44 || sps->profile_idc == 86 ||
        sps->profile_idc == 100 || sps->profile_idc == 110 ||
        sps->profile_idc == 122 || sps->profile_idc == 244) {
      return 0;
    }
  }
  return max_dpb_frames;
}

幸运的是 HEVC 相比 H264 不需要如此繁杂的计算,HEVC 在编码时已经提前将最大 Reorder 数算好了,我们只需按如下方式获取:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
// HEVC最大Reorder数的计算逻辑
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
int32_t ComputeHEVCReorderWindow(const H265VPS* vps) {
  int32_t vps_max_sub_layers_minus1 = vps->vps_max_sub_layers_minus1;
  return vps->vps_max_num_reorder_pics[vps_max_sub_layers_minus1];
}
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

计算好 Reorder 数和 POC 后,继续复用 H264 的 Reorder 逻辑,即可正确完成排序。

提取并缓存 SPS / PPS / VPS

下面我们正式开始解码逻辑实现,首先,需要提取 SPS / PPS / VPS,并对其解析,缓存:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
switch (nalu.nal_unit_type) {
    // 跳过
    ...
    // 解析SPS
    case H265NALU::SPS_NUT: {
        int sps_id = -1;
        result = hevc_parser_.ParseSPS(&sps_id);
        if (result == H265Parser::kUnsupportedStream) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Unsupported SPS");
            NotifyError(PLATFORM_FAILURE, SFT_UNSUPPORTED_STREAM);
            return;
        }
        if (result != H265Parser::kOk) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Could not parse SPS");
            NotifyError(UNREADABLE_INPUT, SFT_INVALID_STREAM);
            return;
        }
        // 按照sps_id缓存SPS的nalu data
        seen_sps_[sps_id].assign(nalu.data, nalu.data + nalu.size);
        break;
    }
    // 解析PPS
    case H265NALU::PPS_NUT: {
        int pps_id = -1;
        result = hevc_parser_.ParsePPS(nalu, &pps_id);
        if (result == H265Parser::kUnsupportedStream) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Unsupported PPS");
            NotifyError(PLATFORM_FAILURE, SFT_UNSUPPORTED_STREAM);
            return;
        }
        if (result == H265Parser::kMissingParameterSet) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR,
                            "Missing SPS from what was parsed");
            NotifyError(PLATFORM_FAILURE, SFT_INVALID_STREAM);
            return;
        }
        if (result != H265Parser::kOk) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Could not parse PPS");
            NotifyError(UNREADABLE_INPUT, SFT_INVALID_STREAM);
            return;
        }
        // 按照pps_id缓存PPS的nalu data
        seen_pps_[pps_id].assign(nalu.data, nalu.data + nalu.size);
        // 将PPS同样作为提交到VT的一部分,这可以解决同一个GOP下不同帧引用不同PPS的问题
        nalus.push_back(nalu);
        data_size += kNALUHeaderLength + nalu.size;
        break;
    }
    // 解析VPS
    case H265NALU::VPS_NUT: {
        int vps_id = -1;
        result = hevc_parser_.ParseVPS(&vps_id);
        if (result == H265Parser::kUnsupportedStream) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Unsupported VPS");
            NotifyError(PLATFORM_FAILURE, SFT_UNSUPPORTED_STREAM);
            return;
        }
        if (result != H265Parser::kOk) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR, "Could not parse VPS");
            NotifyError(UNREADABLE_INPUT, SFT_INVALID_STREAM);
            return;
        }
        // 按照vps_id缓存VPS的nalu data
        seen_vps_[vps_id].assign(nalu.data, nalu.data + nalu.size);
        break;
    }
    // 跳过
    ...
}

创建解码 Format 和 Session

根据解析后的 VPS,SPS,PPS,我们可以创建解码 Format:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
// 使用vps,sps,pps创建解码Format(CMFormatDescriptionRef)
base::ScopedCFTypeRef<CMFormatDescriptionRef> CreateVideoFormatHEVC(
    const std::vector<uint8_t>& vps,
    const std::vector<uint8_t>& sps,
    const std::vector<uint8_t>& pps) {
  DCHECK(!vps.empty());
  DCHECK(!sps.empty());
  DCHECK(!pps.empty());
 
  // Build the configuration records.
  std::vector<const uint8_t*> nalu_data_ptrs;
  std::vector<size_t> nalu_data_sizes;
  nalu_data_ptrs.reserve(3);
  nalu_data_sizes.reserve(3);
  nalu_data_ptrs.push_back(&vps.front());
  nalu_data_sizes.push_back(vps.size());
  nalu_data_ptrs.push_back(&sps.front());
  nalu_data_sizes.push_back(sps.size());
  nalu_data_ptrs.push_back(&pps.front());
  nalu_data_sizes.push_back(pps.size());
 
  // 这里有一个关键点,即,在一个 GOP 内可能存在 >= 2 的引用情况、
  // 比如I帧引用了 pps_id 为 0 的 pps,P帧引用了 pps_id 为 1 的 pps
  // 这种场景经过本人测试,解决方法有两个:
  // 方法1:把两个PPS都传进来,以此创建 CMFormatDescriptionRef(此时nalu_data_ptrs数组长度为4)
  // 方法2(本文选用的方法):仍然只传一个PPS,但把 PPS 的 Nalu Data 提交到 VT,VT 会自动查找到PPS的引用关系,并处理这种情况,见"vt_video_decode_accelerator_mac.cc;l=1380"
  base::ScopedCFTypeRef<CMFormatDescriptionRef> format;
  if (__builtin_available(macOS 11.0, *)) {
    OSStatus status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(
        kCFAllocatorDefault,
        nalu_data_ptrs.size(),     // parameter_set_count
        &nalu_data_ptrs.front(),   // &parameter_set_pointers
        &nalu_data_sizes.front(),  // &parameter_set_sizes
        kNALUHeaderLength,         // nal_unit_header_length
        extensions, format.InitializeInto());
    OSSTATUS_LOG_IF(WARNING, status != noErr, status)
        << "CMVideoFormatDescriptionCreateFromHEVCParameterSets()";
  }
  return format;
}
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)

​VideoToolbox 的解码流程,图片来自 Apple

在创建解码 Format 后,继续创建解码 Session:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
bool VTVideoDecodeAccelerator::ConfigureDecoder() {
  DVLOG(2) << __func__;
  DCHECK(decoder_task_runner_->RunsTasksInCurrentSequence());
 
  base::ScopedCFTypeRef<CMFormatDescriptionRef> format;
  switch (codec_) {
    case VideoCodec::kH264:
      format = CreateVideoFormatH264(active_sps_, active_spsext_, active_pps_);
      break;
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
    case VideoCodec::kHEVC:
      // 创建CMFormatDescriptionRef
      format = CreateVideoFormatHEVC(active_vps_, active_sps_, active_pps_);
      break;
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
    case VideoCodec::kVP9:
      format = CreateVideoFormatVP9(
          cc_detector_->GetColorSpace(config_.container_color_space),
          config_.profile, config_.hdr_metadata,
          cc_detector_->GetCodedSize(config_.initial_expected_coded_size));
      break;
    default:
      NOTREACHED() << "Unsupported codec.";
  }
 
  if (!format) {
    NotifyError(PLATFORM_FAILURE, SFT_PLATFORM_ERROR);
    return false;
  }
 
  if (!FinishDelayedFrames())
    return false;
 
  format_ = format;
  session_.reset();
 
  // 利用创建好的解码format创建解码session
  // 如果是VP9,则强制请求硬解解码
  // 如果是HEVC,由于一些可能的原因,我们选择不强制硬解解码(让VT自己选最适合的解码方式)
  // 可能的原因有:
  // 1. GPU不支持硬解(此时我们希望使用VT软解)
  // 2. 解码的Profile不受支持(比如M1支持HEVC Rext硬解,而Intel/AMD GPU不支持,此时希望软解)
  // 3. GPU繁忙,资源不足,此时希望软解
  const bool require_hardware = config_.profile == VP9PROFILE_PROFILE0 ||
                                config_.profile == VP9PROFILE_PROFILE2;
 
  // 可能是HDR视频,因此希望输出pix_fmt是
  // kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange
  const bool is_hbd = config_.profile == VP9PROFILE_PROFILE2 ||
                      config_.profile == HEVCPROFILE_MAIN10 ||
                      config_.profile == HEVCPROFILE_REXT;
  // 创建解码Session
  if (!CreateVideoToolboxSession(format_, require_hardware, is_hbd, &callback_,
                                 &session_, &configured_size_)) {
    NotifyError(PLATFORM_FAILURE, SFT_PLATFORM_ERROR);
    return false;
  }
 
  // Report whether hardware decode is being used.
  bool using_hardware = false;
  base::ScopedCFTypeRef<CFBooleanRef> cf_using_hardware;
  if (VTSessionCopyProperty(
          session_,
          // kVTDecompressionPropertyKey_UsingHardwareAcceleratedVideoDecoder
          CFSTR("UsingHardwareAcceleratedVideoDecoder"), kCFAllocatorDefault,
          cf_using_hardware.InitializeInto()) == 0) {
    using_hardware = CFBooleanGetValue(cf_using_hardware);
  }
  UMA_HISTOGRAM_BOOLEAN("Media.VTVDA.HardwareAccelerated", using_hardware);
 
  if (codec_ == VideoCodec::kVP9 && !vp9_bsf_)
    vp9_bsf_ = std::make_unique<VP9SuperFrameBitstreamFilter>();
 
  // Record that the configuration change is complete.
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
  configured_vps_ = active_vps_;
#endif  // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
  configured_sps_ = active_sps_;
  configured_spsext_ = active_spsext_;
  configured_pps_ = active_pps_;
  return true;
}

创建解码 Session 的逻辑:

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
// 利用CMFormatDescriptionRef创建VTDecompressionSession
bool CreateVideoToolboxSession(
    const CMFormatDescriptionRef format,
    bool require_hardware,
    bool is_hbd,
    const VTDecompressionOutputCallbackRecord* callback,
    base::ScopedCFTypeRef<VTDecompressionSessionRef>* session,
    gfx::Size* configured_size) {
  // Prepare VideoToolbox configuration dictionaries.
  base::ScopedCFTypeRef<CFMutableDictionaryRef> decoder_config(
      CFDictionaryCreateMutable(kCFAllocatorDefault,
                                1,  // capacity
                                &kCFTypeDictionaryKeyCallBacks,
                                &kCFTypeDictionaryValueCallBacks));
  if (!decoder_config) {
    DLOG(ERROR) << "Failed to create CFMutableDictionary";
    return false;
  }
 
  CFDictionarySetValue(
      decoder_config,
      kVTVideoDecoderSpecification_EnableHardwareAcceleratedVideoDecoder,
      kCFBooleanTrue);
  CFDictionarySetValue(
      decoder_config,
      kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder,
      require_hardware ? kCFBooleanTrue : kCFBooleanFalse);
 
  CGRect visible_rect = CMVideoFormatDescriptionGetCleanAperture(format, true);
  CMVideoDimensions visible_dimensions = {
      base::ClampFloor(visible_rect.size.width),
      base::ClampFloor(visible_rect.size.height)};
  base::ScopedCFTypeRef<CFMutableDictionaryRef> image_config(
      BuildImageConfig(visible_dimensions, is_hbd));
  if (!image_config) {
    DLOG(ERROR) << "Failed to create decoder image configuration";
    return false;
  }
 
  // 创建解码Session的最终逻辑
  OSStatus status = VTDecompressionSessionCreate(
      kCFAllocatorDefault,
      format,          // 我们创建好的CMFormatDescriptionRef
      decoder_config,  // video_decoder_specification
      image_config,    // destination_image_buffer_attributes
      callback,        // output_callback
      session->InitializeInto());
  if (status != noErr) {
    OSSTATUS_DLOG(WARNING, status) << "VTDecompressionSessionCreate()";
    return false;
  }
 
  *configured_size =
      gfx::Size(visible_rect.size.width, visible_rect.size.height);
 
  return true;
}

提取视频帧并解码

这一步开始我们就要开始正式解码了,解码前首先需要提取视频帧的 SliceHeader,并从缓存中拿到到该帧引用的 SPS,PPS,VPS,计算 POC 和最大 Reorder 数。

// media/gpu/mac/vt_video_decode_accelerator_mac.cc
 
switch (nalu.nal_unit_type) {
    case H265NALU::BLA_W_LP:
    case H265NALU::BLA_W_RADL:
    case H265NALU::BLA_N_LP:
    case H265NALU::IDR_W_RADL:
    case H265NALU::IDR_N_LP:
    case H265NALU::TRAIL_N:
    case H265NALU::TRAIL_R:
    case H265NALU::TSA_N:
    case H265NALU::TSA_R:
    case H265NALU::STSA_N:
    case H265NALU::STSA_R:
    case H265NALU::RADL_N:
    case H265NALU::RADL_R:
    case H265NALU::RASL_N:
    case H265NALU::RASL_R:
    case H265NALU::CRA_NUT: {
        // 针对视频帧提取SliceHeader
        curr_slice_hdr.reset(new H265SliceHeader());
        result = hevc_parser_.ParseSliceHeader(nalu, curr_slice_hdr.get(),
                                                last_slice_hdr.get());
 
        if (result == H265Parser::kMissingParameterSet) {
            curr_slice_hdr.reset();
            last_slice_hdr.reset();
            WriteToMediaLog(MediaLogMessageLevel::kERROR,
                            "Missing PPS when parsing slice header");
            continue;
        }
 
        if (result != H265Parser::kOk) {
            curr_slice_hdr.reset();
            last_slice_hdr.reset();
            WriteToMediaLog(MediaLogMessageLevel::kERROR,
                            "Could not parse slice header");
            NotifyError(UNREADABLE_INPUT, SFT_INVALID_STREAM);
            return;
        }
 
        // 这里是一个Workaround,一些iOS设备拍摄的视频如果在Seek过程首个关键帧是CRA帧,
        // 那么下一个帧如果是一个RASL帧,则会立即报kVTVideoDecoderBadDataErr的错误,
        // 因此我们需要判断总输出帧数是否大于5,否则跳过这些RASL帧
        if (output_count_for_cra_rasl_workaround_ < kMinOutputsBeforeRASL &&
            (nalu.nal_unit_type == H265NALU::RASL_N ||
                nalu.nal_unit_type == H265NALU::RASL_R)) {
            continue;
        }
 
        // 根据SliceHeader内的pps_id,拿到缓存的pps nalu data
        const H265PPS* pps =
            hevc_parser_.GetPPS(curr_slice_hdr->slice_pic_parameter_set_id);
        if (!pps) {
            WriteToMediaLog(MediaLogMessageLevel::kERROR,
                            "Missin

标签: 三极管hbd438t2513n10tc接近传感器60p直针直脚连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台