因此尝试搜索发现 Windows 平台 VDAVideoDecoder 代码实现均位于dxva_video_decode_accelerator_win.cc文件内,继续寻找蛛丝马迹,发现,在开源的 Chromium 项目内,并不存在 HEVC 硬解相关的任何实现,这说明 Edge 是自己基于某个时期的 Chromium Media 模块“魔改”出来的 HEVC 硬解支持,同时在这个过程发现了个有趣的现象:
- Edge 完全不使用 D3D11VideoDecoder 进行解码,而是使用 VDAVideoDecoder 解码,我猜测其目的是为了推广自家的 Media Foundation。
- Edge 的色彩处理有问题 (Edge 102),与 Chrome 不一样的点在于,比如对于 Transfer 是 PQ 的 HDR 视频,Edge 并没有对其进行 Tone Mapping,导致 PQ 视频在 Edge 下看起来有“过曝”的问题。
- Edge 解码 AV1 需要装 AV1 Video Extension ,才可解码 AV1(软解 硬解),而 Chrome 由于实现了 AV1 的 D3D11VA 硬解,以及 Gav1VideoDecoder 和 DAV1dVideoDecoder 软解 Decoder,不需要安装 AV1 插件也可在受支持的显卡硬解,不受支持的显卡软解。
好吧,尽管没少吐槽 Edge,但是他确实是 Windows 平台唯一支持 HEVC 硬解的浏览器(当然,马上就不是了)。
接着看 dxva_video_decode_accelerator_win.cc 的实现,从上述 Edge 解码需要安装 AV1 插件的逻辑反推,如果我们照着 AV1 的方式实现 HEVC,是否可行?答案是肯定的。
观察 Supported Profile,然后将我们需要支持的 HEVCPROFILE_MAIN、HEVCPROFILE_MAIN10 加入:
// media/gpu/windows/dxva_video_decode_accelerator_win.cc
// 我们可以看到与macOS类似,VDAVideoDecoder支持的格式都被放到了Supported Profiles内
// 如下,一目了然,VDAVideoDecoder原始支持H264,VP8,VP9,AV1
constexpr VideoCodecProfile kSupportedProfiles[] = {
H264PROFILE_BASELINE, H264PROFILE_MAIN, H264PROFILE_HIGH,
VP8PROFILE_ANY, VP9PROFILE_PROFILE0, VP9PROFILE_PROFILE2,
AV1PROFILE_PROFILE_MAIN, AV1PROFILE_PROFILE_HIGH, AV1PROFILE_PROFILE_PRO,
// 添加我们需要支持的两种Profile
HEVCPROFILE_MAIN, HEVCPROFILE_MAIN10,
};
之后按照 AV1 的逻辑,加入 HEVC Codec,同时值得一提的是必须在调用 SetOutput 方法前设置分辨率(这块坑了我大概一天的时间 Debug),代码如下:
// media/gpu/windows/dxva_video_decode_accelerator_win.cc
...
if (config.profile == VP9PROFILE_PROFILE2 ||
config.profile == VP9PROFILE_PROFILE3 ||
config.profile == H264PROFILE_HIGH10PROFILE) {
// Input file has more than 8 bits per channel.
use_fp16_ = true;
decoder_output_p010_or_p016_ = true;
// the OS for VP9 which is why it works and AV1 doesn't.
HRESULT hr = CreateAV1Decoder(IID_PPV_ARGS(&decoder_));
RETURN_ON_HR_FAILURE(hr, "Failed to create decoder instance", false);
} else if (profile >= HEVCPROFILE_MAIN && profile <= HEVCPROFILE_MAIN10) {
codec_ = kCodecHEVC;
clsid = CLSID_MSH265DecoderMFT;
// 这里必须提前设置分辨率,否则SetOutput会失败
using_ms_vpx_mft_ = true;
// 经过各种探索发现只有1.0.31823版本的HEVC视频扩展没有抖动问题,
// 其他情况包括最新版本(1.0.51361.0)的HEVC视频扩展,依然存在解码跳帧问题
// 显然如果希望1.0.51361版本插件正常解码,需要额外的配置,但无法从官网文档找到配置方法
HRESULT hr = CreateHEVCDecoder(IID_PPV_ARGS(&decoder_));
RETURN_ON_HR_FAILURE(hr, "Failed to create hevc decoder instance", false);
} else {
if (!decoder_dll)
RETURN_ON_FAILURE(false, "Unsupported codec.", false);
HRESULT hr = MFCreateMediaType(&media_type);
RETURN_ON_HR_FAILURE(hr, "MFCreateMediaType failed", false);
// 设置主类型,参考:https://docs.microsoft.com/en-us/windows/win32/medfound/mf-mt-major-type-attribute
hr = media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
RETURN_ON_HR_FAILURE(hr, "Failed to set major input type", false);
if (codec_ == kCodecH264) {
// 设置辅类型,参考:https://docs.microsoft.com/en-us/windows/win32/medfound/video-subtype-guids
if (codec_ == kCodecHEVC) {
hr = media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_HEVC);
} else if (codec_ == kCodecH264) {
hr = media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
} else if (codec_ == kCodecVP9) {
hr = media_type->SetGUID(MF_MT_SUBTYPE, MEDIASUBTYPE_VP90);
接着实现一下 HEVCDecoder 的获取逻辑:
// media/gpu/windows/dxva_video_decode_accelerator_win.cc
// 不同于H264等格式,由于HEVC是以可选插件形式支持的,因此直接读取DLL的方式并不可行
// 参考AV1的实现方法,以及微软的官方文档,使用::MFTEnumEx方法,最终可以拿到HEVCDecoder
HRESULT CreateHEVCDecoder(const IID& iid, void** object) {
MFT_REGISTER_TYPE_INFO type_info = {MFMediaType_Video, MFVideoFormat_HEVC};
base::win::ScopedCoMem<IMFActivate*> acts;
UINT32 acts_num;
HRESULT hr =
::MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER, MFT_ENUM_FLAG_SORTANDFILTER,
&type_info, nullptr, &acts, &acts_num);
if (FAILED(hr))
return hr;
if (acts_num < 1)
return E_FAIL;
hr = acts[0]->ActivateObject(iid, object);
for (UINT32 i = 0; i < acts_num; i)
acts[i]->Release();
return hr;
}
同时将 HEVC Main, Main10 Profile 加入到 supported_profile_helpers.cc:
// media/gpu/windows/supported_profile_helpers.cc
// 对Windows10 1709以上版本添加HEVC硬解支持:
if (base::win::GetVersion() >= base::win::Version::WIN10_RS2) {
if (profile_id == D3D11_DECODER_PROFILE_HEVC_VLD_MAIN) {
supported_resolutions[HEVCPROFILE_MAIN] = GetResolutionsForGUID(
video_device.Get(), profile_id, kModernResolutions, DXGI_FORMAT_NV12);
continue;
}
if (profile_id == D3D11_DECODER_PROFILE_HEVC_VLD_MAIN10) {
supported_resolutions[HEVCPROFILE_MAIN10] = GetResolutionsForGUID(
video_device.Get(), profile_id, kModernResolutions, DXGI_FORMAT_P010);
continue;
}
}
最后,还需要修改一下引导逻辑,强制让 HEVC 编码格式使用 VDAVideoDecoder 而不是 D3D11VideoDecoder:
// media/mojo/services/gpu_mojo_media_client_win.cc
...
// 强制令HEVC编码格式的视频使用VDAVideoDecoder解码
std::unique_ptr<VideoDecoder> CreatePlatformVideoDecoder(
const VideoDecoderTraits& traits) {
if (!ShouldUseD3D11VideoDecoder(*traits.gpu_workarounds) || (
config.profile() >= HEVCPROFILE_MAIN &&
config.profile() <= HEVCPROFILE_MAIN10)) {
if (traits.gpu_workarounds->disable_dxva_video_decoder)
return nullptr;
return VdaVideoDecoder::Create(
...
后面省略了一些透传 Profile 的代码。
在上述步骤执行后,一切大功告成,代码实现基本完成,HEVC视频扩展帮我们处理了大部分的解码逻辑,所以实现过程相当简单。
但,问题来了!由于HEVC视频扩展插件在 1.0.31823 之后的版本存在抖动问题,而 1.0.50361 虽然解决了抖动的问题,但其官网文档并没有明确详述如何配置 Decoder 解决该问题(注:欢迎贡献配置方法),因此,如果我们需要用HEVC视频扩展的方案,则必须限制用户本地强行使用 1.0.31823 版本。
为此我尝试写过 nsh 脚本,试图在用户电脑存在非 31823 版本HEVC视频扩展的情况,强制卸载并重装 1.0.31823 版本的 HEVC视频扩展,但,因为 Windows 商店会默认对 Appx 扩展自动更新,这导致如果希望用户电脑不更新HEVC视频扩展, 则必须强迫用户关闭 Windows 商店的自动更新,这无疑意味着这个方案是个半成品,很可能需要换技术方案了,但我们的选择真的不多。
然而就在即将放弃的时候,我突然想到为啥不照着 Chromium 把 D3D11VA 的方案实现一遍呢?Media Foundation 绝不是唯一解!时间点是 2022 年的 2 月中旬,我抱着尝试的态度,打开了 source.chromium.org 这个网站,试图学习下其他格式 D3D11VA 的解码方法,并在 media 文件夹偶然间瞥到了一个叫 d3d11_h265_accelerator.cc 的文件,这是啥?怎么可能?Windows 不是没有人实现过 HEVC 硬解么?然后我果断看了下提交时间,发现在 2 月 8 号,这个文件才被合入到 Chromium!感谢作者 @Jianlin Qiu(来自 Intel 的大佬),把 Windows 的 D3D11 硬解加速实现的差不多。
使用 D3D11VA 硬解Trace 了下 @Jianlin Qiu 实现相关的 crbug(https://bugs.chromium.org/p/chromium/issues/detail?id=1286132#c18),合入其代码,简单做了下测试发现一半的视频可以播(早期版本有些小问题,目前均已解决)。
遂观察其实现逻辑,发现 Windows 的硬解实现逻辑与 macOS 完全不同,在 macOS,尽管我会对 SPS / PPS / VPS / Slice Header 进行 Parse,但是实际上,最终调用CMVideoFormatDescriptionCreateFromHEVCParameterSets 方法创建解码 Format 时,传给 VT 的参数是包含了 VPS, SPS, PPS 的 Nalu Data 的数组,也就是说理论上如果我们不计算 POC,不 Reorder,直接将 Nalu Data 塞给 VideoToolbox,也可以解码,只是帧组会抖动罢了。
但到了 Windows 和 Linux 这里,实现起来要麻烦的多。
根据 Mircosoft 官网,可知 D3D11VA 硬解的实际流程可参考这篇文章(https://docs.microsoft.com/en-us/windows/win32/medfound/supporting-direct3d-11-video-decoding-in-media-foundation#open-a-device-handle),总结起来其实主要工作在于对每一个视频帧的图片参数拼装。
GPU 是否支持硬解检测GPU 是否支持硬解,这里的逻辑,首先假定默认是不支持 HEVC Main / Main10 的,然后调用 D3D11 Device 提供的 GetVideoDecoderConfig 方法,拿到支持的 Codec 列表,若列表中存在 HEVC 则认为支持:
// media/gpu/windows/d3d11_video_decoder.cc
D3D11_VIDEO_DECODER_CONFIG dec_config = {};
bool found = false;
for (UINT i = 0; i < config_count; i ) {
// 调用该方法,d3d11会返回其所支持的全部codec类型
hr = video_device_->GetVideoDecoderConfig(
decoder_configurator_->DecoderDescriptor(), i, &dec_config);
if (FAILED(hr))
return {D3D11Status::Codes::kGetDecoderConfigFailed, hr};
if (dec_config.ConfigBitstreamRaw == 1 &&
(config_.codec() == VideoCodec::kVP9 ||
config_.codec() == VideoCodec::kAV1 ||
config_.codec() == VideoCodec::kHEVC)) {
// DXVA HEVC, VP9, and AV1 specifications say ConfigBitstreamRaw
// "shall be 1".
// 如果类型中有HEVC类型,且ConfigBitstreamRaw == 1,则显卡支持硬解
found = true;
break;
}
if (config_.codec() == VideoCodec::kH264 &&
dec_config.ConfigBitstreamRaw == 2) {
// ConfigBitstreamRaw == 2 means the decoder uses DXVA_Slice_H264_Short.
found = true;
break;
}
}
if (!found)
return D3D11Status::Codes::kDecoderUnsupportedConfig;
理解 DXVA HEVC Spec
如上流程可知,根据 DXVA HEVC Spec,要正确实现解码,需要自己提前解析好其要的 Picture Params,以 HEVC 的 Picture Params 为例,结构体如下,每一个参数都不能缺少,这本身工作量就不小,但好在 @Jeffery 大佬在实现 Linux 的 H265 Decoder 和 H265 Parser 时已经完成了大部分工作,因此 @Jianlin 大佬的工作主要是如何正确的将这些已经 Parse 好的 Params 拼装和计算,并塞给 D3D11。
// 诚然,如果每个软件都要实现一遍拼装逻辑,成本高的离谱
// 相比macOS的API设计,DXVA规范设计的非常复杂
// 但我相信其一定有自己的理由
typedef struct _DXVA_PicEntry_HEVC {
union {
struct {
UCHAR Index7Bits :7;
UCHAR AssociatedFlag :1;
};
UCHAR bPicEntry;
};
} DXVA_PicEntry_HEVC, *PDXVA_PicEntry_HEVC;
typedef struct _DXVA_PicParams_HEVC {
USHORT PicWidthInMinCbsY;
USHORT PicHeightInMinCbsY;
union {
struct {
USHORT chroma_format_idc :2;
USHORT separate_colour_plane_flag :1;
USHORT bit_depth_luma_minus8 :3;
USHORT bit_depth_chroma_minus8 :3;
USHORT log2_max_pic_order_cnt_lsb_minus4 :4;
USHORT NoPicReorderingFlag :1;
USHORT NoBiPredFlag :1;
USHORT ReservedBits1 :1;
};
USHORT wFormatAndSequenceInfoFlags;
};
DXVA_PicEntry_HEVC CurrPic;
UCHAR sps_max_dec_pic_buffering_minus1;
UCHAR log2_min_luma_coding_block_size_minus3;
UCHAR log2_diff_max_min_luma_coding_block_size;
UCHAR log2_min_transform_block_size_minus2;
UCHAR log2_diff_max_min_transform_block_size;
UCHAR max_transform_hierarchy_depth_inter;
UCHAR max_transform_hierarchy_depth_intra;
UCHAR num_short_term_ref_pic_sets;
UCHAR num_long_term_ref_pics_sps;
UCHAR num_ref_idx_l0_default_active_minus1;
UCHAR num_ref_idx_l1_default_active_minus1;
CHAR init_qp_minus26;
UCHAR ucNumDeltaPocsOfRefRpsIdx;
USHORT wNumBitsForShortTermRPSInSlice;
USHORT ReservedBits2;
union {
struct {
UINT32 scaling_list_enabled_flag :1;
UINT32 amp_enabled_flag :1;
UINT32 sample_adaptive_offset_enabled_flag :1;
UINT32 pcm_enabled_flag :1;
UINT32 pcm_sample_bit_depth_luma_minus1 :4;
UINT32 pcm_sample_bit_depth_chroma_minus1 :4;
UINT32 log2_min_pcm_luma_coding_block_size_minus3 :2;
UINT32 log2_diff_max_min_pcm_luma_coding_block_size :2;
UINT32 pcm_loop_filter_disabled_flag :1;
UINT32 long_term_ref_pics_present_flag :1;
UINT32 sps_temporal_mvp_enabled_flag :1;
UINT32 strong_intra_smoothing_enabled_flag :1;
UINT32 dependent_slice_segments_enabled_flag :1;
UINT32 output_flag_present_flag :1;
UINT32 num_extra_slice_header_bits :3;
UINT32 sign_data_hiding_enabled_flag :1;
UINT32 cabac_init_present_flag :1;
UINT32 ReservedBits3 :5;
};
UINT32 dwCodingParamToolFlags;
union {
struct {
UINT32 constrained_intra_pred_flag :1;
UINT32 transform_skip_enabled_flag :1;
UINT32 cu_qp_delta_enabled_flag :1;
UINT32 pps_slice_chroma_qp_offsets_present_flag :1;
UINT32 weighted_pred_flag :1;
UINT32 weighted_bipred_flag :1;
UINT32 transquant_bypass_enabled_flag :1;
UINT32 tiles_enabled_flag :1;
UINT32 entropy_coding_sync_enabled_flag :1;
UINT32 uniform_spacing_flag :1;
UINT32 loop_filter_across_tiles_enabled_flag :1;
UINT32 pps_loop_filter_across_slices_enabled_flag :1;
UINT32 deblocking_filter_override_enabled_flag :1;
UINT32 pps_deblocking_filter_disabled_flag :1;
UINT32 lists_modification_present_flag :1;
UINT32 slice_segment_header_extension_present_flag :1;
UINT32 IrapPicFlag :1;
UINT32 IdrPicFlag :1;
UINT32 IntraPicFlag :1;
UINT32 ReservedBits4 :13;
};
UINT32 dwCodingSettingPicturePropertyFlags;
};
CHAR pps_cb_qp_offset;
CHAR pps_cr_qp_offset;
UCHAR num_tile_columns_minus1;
UCHAR num_tile_rows_minus1;
USHORT column_width_minus1[19];
USHORT row_height_minus1[21];
UCHAR diff_cu_qp_delta_depth;
CHAR pps_beta_offset_div2;
CHAR pps_tc_offset_div2;
UCHAR log2_parallel_merge_level_minus2;
INT CurrPicOrderCntVal;
DXVA_PicEntry_HEVC RefPicList[15];
UCHAR ReservedBits5;
INT PicOrderCntValList[15];
UCHAR RefPicSetStCurrBefore[8];
UCHAR RefPicSetStCurrAfter[8];
UCHAR RefPicSetLtCurr[8];
USHORT ReservedBits6;
USHORT ReservedBits7;
UINT StatusReportFeedbackNumber;
};
} DXVA_PicParams_HEVC, *PDXVA_PicParams_HEVC;
填充默认 Picture Params
实现硬解加速本身不需要实现解码逻辑,因此其实 H265Accelerator 本身的功能主要在于拼装 DXVA 所要的 Picture Params,并正确提交。这个过程,首先需要填充默认的 Picture Params:
// media/gpu/windows/d3d11_h265_accelerator.cc
void D3D11H265Accelerator::FillPicParamsWithConstants(
DXVA_PicParams_HEVC* pic) {
// According to DXVA spec section 2.2, this optional 1-bit flag
// has no meaning when used for CurrPic so always configure to 0.
pic->CurrPic.AssociatedFlag = 0;
// num_tile_columns_minus1 and num_tile_rows_minus1 will only
// be set if tiles are enabled. Set to 0 by default.
pic->num_tile_columns_minus1 = 0;
pic->num_tile_rows_minus1 = 0;
// Host decoder may set this to 1 if sps_max_num_reorder_pics is 0,
// but there is no requirement that NoPicReorderingFlag must be
// derived from it. So we always set it to 0 here.
pic->NoPicReorderingFlag = 0;
// Must be set to 0 in absence of indication whether B slices are used
// or not, and it does not affect the decoding process.
pic->NoBiPredFlag = 0;
// Shall be set to 0 and accelerators shall ignore its value.
pic->ReservedBits1 = 0;
// Bit field added to enable DWORD alignment and should be set to 0.
pic->ReservedBits2 = 0;
// Should always be set to 0.
pic->ReservedBits3 = 0;
// Should be set to 0 and ignored by accelerators
pic->ReservedBits4 = 0;
// Should always be set to 0.
pic->ReservedBits5 = 0;
// Should always be set to 0.
pic->ReservedBits6 = 0;
// Should always be set to 0.
pic->ReservedBits7 = 0;
}
从 SPS 等位置提取 Picture Params
下面基本都是一些枯燥的流程, 利用 H265 Parser 解析后的结果,去填充 Picture Params:
// media/gpu/windows/d3d11_h265_accelerator.cc
#define ARG_SEL(_1, _2, NAME, ...) NAME
#define SPS_TO_PP1(a) pic_param->a = sps->a;
#define SPS_TO_PP2(a, b) pic_param->a = sps->b;
#define SPS_TO_PP(...) ARG_SEL(__VA_ARGS__, SPS_TO_PP2, SPS_TO_PP1)(__VA_ARGS__)
void D3D11H265Accelerator::PicParamsFromSPS(DXVA_PicParams_HEVC* pic_param,
const H265SPS* sps) {
// Refer to formula 7-14 and 7-16 of HEVC spec.
int min_cb_log2_size_y = sps->log2_min_luma_coding_block_size_minus3 3;
pic_param->PicWidthInMinCbsY =
sps->pic_width_in_luma_samples >> min_cb_log2_size_y;
pic_param->PicHeightInMinCbsY =
sps->pic_height_in_luma_samples >> min_cb_log2_size_y;
// wFormatAndSequenceInfoFlags from SPS
SPS_TO_PP(chroma_format_idc);
SPS_TO_PP(separate_colour_plane_flag);
SPS_TO_PP(bit_depth_luma_minus8);
SPS_TO_PP(bit_depth_chroma_minus8);
SPS_TO_PP(log2_max_pic_order_cnt_lsb_minus4);
// HEVC DXVA spec does not clearly state which slot
// in sps->sps_max_dec_pic_buffering_minus1 should
// be used here. However section A4.1 of HEVC spec
// requires the slot of highest tid to be used for
// indicating the maximum DPB size if level is not
// 8.5.
int highest_tid = sps->sps_max_sub_layers_minus1;
pic_param->sps_max_dec_pic_buffering_minus1 =
sps->sps_max_dec_pic_buffering_minus1[highest_tid];
SPS_TO_PP(log2_min_luma_coding_block_size_minus3);
SPS_TO_PP(log2_diff_max_min_luma_coding_block_size);
// DXVA spec names them differently with HEVC spec.
SPS_TO_PP(log2_min_transform_block_size_minus2,
log2_min_luma_transform_block_size_minus2);
SPS_TO_PP(log2_diff_max_min_transform_block_size,
log2_diff_max_min_luma_transform_block_size);
SPS_TO_PP(max_transform_hierarchy_depth_inter);
SPS_TO_PP(max_transform_hierarchy_depth_intra);
SPS_TO_PP(num_short_term_ref_pic_sets);
SPS_TO_PP(num_long_term_ref_pics_sps);
// dwCodingParamToolFlags extracted from SPS
SPS_TO_PP(scaling_list_enabled_flag);
SPS_TO_PP(amp_enabled_flag);
SPS_TO_PP(sample_adaptive_offset_enabled_flag);
SPS_TO_PP(pcm_enabled_flag);
// 这里发现过一个bug
//(fix:https://chromium-review.googlesource.com/c/chromium/src/ /3538144)
// 部分单反拍出的视频如果这里填充错误会导致花屏
if (sps->pcm_enabled_flag) {
SPS_TO_PP(pcm_sample_bit_depth_luma_minus1);
SPS_TO_PP(pcm_sample_bit_depth_chroma_minus1);
SPS_TO_PP(log2_min_pcm_luma_coding_block_size_minus3);
SPS_TO_PP(log2_diff_max_min_pcm_luma_coding_block_size);
SPS_TO_PP(pcm_loop_filter_disabled_flag);
}
SPS_TO_PP(long_term_ref_pics_present_flag);
SPS_TO_PP(sps_temporal_mvp_enabled_flag);
SPS_TO_PP(strong_intra_smoothing_enabled_flag);
}
#undef SPS_TO_PP
#undef SPS_TO_PP2
#undef SPS_TO_PP1
Picture Params 还需要从 PPS,SliceHeader,以及计算好的 Ref Pic List,Picture 填充,考虑到内容过于繁琐,这里暂时省略,整体思路可以概括为参数拼装。
处理分辨率,色彩深度的突变现实中的实际视频,尤其是在 WebRTC 场景产生的视频,可能存在分辨率或者色彩深度突变的情况,因此,在实际实现 Decoder 的过程中,处理这种情况至关重要,如果处理不好,轻则会导致视频花屏、绿屏,重则会导致 D3D11 device context lost,并最终导致 GPU 进程崩溃。
// media/gpu/h265_decoder.cc
switch (curr_nalu_->nal_unit_type) {
// 对每个视频帧解码
case H265NALU::BLA_W_LP: // fallthrough
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:
if (!curr_slice_hdr_) {
curr_slice_hdr_.reset(new H265SliceHeader());
// 对所有视频帧,解析SliceHeader
par_res = parser_.ParseSliceHeader(*curr_nalu_, curr_slice_hdr_.get(),
last_slice_hdr_.get());
....
state_ = kTryPreprocessCurrentSlice;
// 这里负责处理检测是否为irap帧 (之前因为使用sps的id去判断是否发生变化,
// 导致了部分视频崩溃),因此使用irap作为判断条件,如果是irap
// 则去检查是否该帧引用的分辨率,色彩空间等参数是否发生变化
if (curr_slice_hdr_->irap_pic) {
bool need_new_buffers = false;
if (!ProcessPPS(curr_slice_hdr_->slice_pic_parameter_set_id,
&need_new_buffers)) {
SET_ERROR_AND_RETURN();
}
// 如果发生变化,则need_new_buffers赋值true,返回kConfigChange,
// 并重新创建D3D11Decoder
if (need_new_buffers) {
curr_pic_ = nullptr;
return kConfigChange;
}
}
}
....
// 这里是实际的检测逻辑,profile,色深,分辨率若发生变化,则need_new_buffers改为true
bool H265Decoder::ProcessPPS(int pps_id, bool* need_new_buffers) {
DVLOG(4) << "Processing PPS id:" << pps_id;
const H265PPS* pps = parser_.GetPPS(pps_id);
// Slice header parsing already verified this should exist.
DCHECK(pps);
const H265SPS* sps = parser_.GetSPS(pps->pps_seq_parameter_set_id);
// PPS parsing already verified this should exist.
DCHECK(sps);
if (need_new_buffers)
*need_new_buffers = false;
gfx::Size new_pic_size = sps->GetCodedSize();
gfx::Rect new_visible_rect = sps->GetVisibleRect();
if (visible_rect_ != new_visible_rect) {
DVLOG(2) << "New visible rect: " << new_visible_rect.ToString();
visible_rect_ = new_visible_rect;
}
if (!IsYUV420Sequence(*sps)) {
DVLOG(1) << "Only YUV 4:2:0 is supported";
return false;
}
// Equation 7-8
max_pic_order_cnt_lsb_ =
std::pow(2, sps->log2_max_pic_order_cnt_lsb_minus4 4);
VideoCodecProfile new_profile = H265Parser::ProfileIDCToVideoCodecProfile(
sps->profile_tier_level.general_profile_idc);
uint8_t new_bit_depth = 0;
if (!ParseBitDepth(*sps, new_bit_depth))
return false;
if (!IsValidBitDepth(new_bit_depth, new_profile)) {
DVLOG(1) << "Invalid bit depth=" << base::strict_cast<int>(new_bit_depth)
<< ", profile=" << GetProfileName(new_profile);
return false;
}
if (pic_size_ != new_pic_size || dpb_.max_num_pics() != sps->max_dpb_size ||
profile_ != new_profile || bit_depth_ != new_bit_depth) {
if (!Flush())
return false;
DVLOG(1) << "Codec profile: " << GetProfileName(new_profile)
<< ", level(x30): " << sps->profile_tier_level.general_level_idc
<< ", DPB size: " << sps->max_dpb_size
<< ", Picture size: " << new_pic_size.ToString()
<< ", bit_depth: " << base::strict_cast<int>(new_bit_depth);
profile_ = new_profile;
bit_depth_ = new_bit_depth;
pic_size_ = new_pic_size;
dpb_.set_max_num_pics(sps->max_dpb_size);
if (need_new_buffers)
*need_new_buffers = true;
}
return true;
}
可以看到在返回 kConfigChange 后,实际上是重新创建了一个新的 D3D11Decoder,这个过程用户在前端完全无感知,创建速度非常快,整体视频播放不会感受到一丝卡顿,是连贯的,相比 VLC 处理的体验更好。
// media/gpu/windows/d3d11_video_decoder.cc
...
} else if (result == media::AcceleratedVideoDecoder::kConfigChange) {
// 忽略首次变化的情况
const auto new_bit_depth = accelerated_video_decoder_->GetBitDepth();
const auto new_profile = accelerated_video_decoder_->GetProfile();
const auto new_coded_size = accelerated_video_decoder_->GetPicSize();
if (new_profile == config_.profile() &&
new_coded_size == config_.coded_size() &&
new_bit_depth == bit_depth_ && !picture_buffers_.size()) {
continue;
}
// Update the config.
MEDIA_LOG(INFO, media_log_)
<< "D3D11VideoDecoder config change: profile: "
<< static_cast<int>(new_profile) << " coded_size: ("
<< new_coded_size.width() << ", " << new_coded_size.height() << ")";
profile_ = new_profile;
config_.set_profile(profile_);
config_.set_coded_size(new_coded_size);
// 如果发生变化,则重新创建D3D11Decoder
auto video_decoder_or_error = CreateD3D11Decoder();
if (video_decoder_or_error.has_error()) {
return NotifyError(std::move(video_decoder_or_error).error());
}
DCHECK(set_accelerator_decoder_cb_);
set_accelerator_decoder_cb_.Run(
std::move(video_decoder_or_error).value());
picture_buffers_.clear();
} else if (result == media::AcceleratedVideoDecoder::kTryAgain) {
...
处理非 HEVC Main / Main10 的其他 Profile
根据 HEVC Spec 2021,HEVC 一共存在 11 种 Profile,具体视频使用哪种 Profile 可由 SPS 中的general_profile_idc的值来判断,由于之前 Chromium 没有定义其他 8 种 Profile,导致其他 Profile 会被当作 Main Profile,并使 FFMpegVideoDecoder 的兜底逻辑失败,因此在这个 CL (https://chromium-review.googlesource.com/c/chromium/src/ /3552293)中将其他几种 Profile 添加解决了这个问题。
HEVC 的 11 种 Profile:
// media/mojo/mojom/stable/stable_video_decoder_types.mojom
// Maps to |media.mojom.VideoCodecProfile|.
[Stable, Extensible]
enum VideoCodecProfile {
// Keep the values in this enum unique, as they imply format (h.264 vs. VP8,
// for example), and keep the values for a particular format grouped
// together for clarity.
// Next version: 2
// Next value: 37
// 跳过
...,
kHEVCProfileMin = 16,
// 下面的三种Profile是HEVC Version1定义的三种基础Profile
// HEVC Main Profile,最高支持8Bit,YUV420
// 苹果老款不支持杜比世界的iPhone拍的都是这种
kHEVCProfileMain = kHEVCProfileMin,
// HEVC Main10 Profile, 支持最高10bit,YUV420
// 苹果新款支持杜比视界(HLG8.4)的iPhone拍的HDR视频都是这种
kHEVCProfileMain10 = 17,
// 一个传说中的Profile,并没有见过一个视频是这个Profile
// 使用`ffmpeg -i bear-1280x720.mp4 -vcodec hevc -profile:v mainstillpicture bear-1280x720-hevc-msp.mp4`
// 转码后也无法获得该类型profile
kHEVCProfileMainStillPicture = 18,
kHEVCProfileMax = kHEVCProfileMainStillPicture,
// 跳过
...,
// 这里是新增的8种Profile
[MinVersion=1] kHEVCProfileExtMin = 29,
// Format range extension(HEVC扩展格式,HEVC Version2新增)
// 佳能,索尼,尼康等新机型拍出来的422 10bit HEVC都是这种 最高支持16bit,YUV444
// 在macOS M1 Mac机型10bit及以下可硬解
// 在Windows Intel机型可硬解,因为Intel自己扩展了DXVA规范实现了这部分能力
//(VLC支持,但目前Chromium还没支持)
[MinVersion=1] kHEVCProfileRext = kHEVCProfileExtMin,
// 后面的这7种都是存在于Spec上的Profile,俺也没见找到过样片,只知道他们都不能硬解
[MinVersion=1] kHEVCProfileHighThroughput = 30,
[MinVersion=1] kHEVCProfileMultiviewMain = 31,
[MinVersion=1] kHEVCProfileScalableMain = 32,
[MinVersion=1] kHEVCProfile3dMain = 33,
[MinVersion=1] kHEVCProfileScreenExtended = 34,
[MinVersion=1] kHEVCProfileScalableRext = 35,
[MinVersion=1] kHEVCProfileHighThroughputScreenExtended = 36,
[MinVersion=1] kHEVCProfileExtMax = kHEVCProfileHighThroughputScreenExtended,
};
Profile 的赋值逻辑:
// media/ffmpeg/ffmpeg_common.cc
int hevc_profile = -1;
// 这里由于chrome并没有引入ffmpeg hevcps相关代码,因此需要自己解析一遍
// 拿到HEVCDecoderConfigurationRecord,并获取general_profile_idc
if (codec_context->extradata && codec_context->extradata_size) {
mp4::HEVCDecoderConfigurationRecord hevc_config;
if (hevc_config.Parse(codec_context->extradata,
codec_context->extradata_size)) {
hevc_profile = hevc_config.general_profile_idc;
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
if (!color_space.IsSpecified()) {
// 由于没有引入hevc_ps相关代码,在无法从容器获取色彩空间的情况
// 手动从SPS提取色彩空间
color_space = hevc_config.GetColorSpace();
}
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
}
}
// The values of general_profile_idc are taken from the HEVC standard, see
// the latest https://www.itu.int/rec/T-REC-H.265/en
switch (hevc_profile) {
case 1:
profile = HEVCPROFILE_MAIN;
break;
case 2:
profile = HEVCPROFILE_MAIN10;
break;
case 3:
profile = HEVCPROFILE_MAIN_STILL_PICTURE;
break;
case 4:
profile = HEVCPROFILE_REXT;
break;
case 5:
profile = HEVCPROFILE_HIGH_THROUGHPUT;
break;
case 6:
profile = HEVCPROFILE_MULTIVIEW_MAIN;
break;
case 7:
profile = HEVCPROFILE_SCALABLE_MAIN;
break;
case 8:
profile = HEVCPROFILE_3D_MAIN;
break;
case 9:
profile = HEVCPROFILE_SCREEN_EXTENDED;
break;
case 10:
profile = HEVCPROFILE_SCALABLE_REXT;
break;
case 11:
profile = HEVCPROFILE_HIGH_THROUGHPUT_SCREEN_EXTENDED;
break;
default:
// Always assign a default if all heuristics fail.
profile = HEVCPROFILE_MAIN;
break;
}
当 Profile 能力补齐后,就可以支持将硬解不支持的 Profile 自动 fallback 到 FFMpegVideoDecoder 软解的能力了,这样可以确保我们目前可见的所有 HEVC Profile 都可以正常播放(能走硬解走硬解,否则走软解)。
处理色彩空间提取逻辑之前版本的 Chromium 提取色彩空间的逻辑要么是利用 ffmpeg 的 avcodec_parameters_to_context 获取,最终利用 ffmpeg 解析 mov 或者 mp4 container 的逻辑获取,要么在 demux 阶段提取 FOURCC_COLR Box 获取,这样做对于标准的 mov, mp4 视频并没有什么问题,然而很多编码器在实现时并没有将色彩空间信息写入容器,导致 Chromium 的之前的逻辑无法正确提取到 HEVC 视频的色彩空间。
因此我们需要利用解析好的 HEVCDecoderConfigurationRecord,在 demux 阶段对 SPS 进行解析,并提取其 sps->vui_parameters->colour_primaries , sps->vui_parameters->transfer_characteristics , sps->vui_parameters->matrix_coeffs , 以及 sps->vui_parameters->video_full_range_flag以生成 VideoColorSpace。
// media/formats/mp4/hevc.cc
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
VideoColorSpace HEVCDecoderConfigurationRecord::GetColorSpace() {
// 利用HEVCDecoderConfigurationRecord的HVCCNALArray,解析SPS
if (!arrays.size()) {
DVLOG(1) << "HVCCNALArray not found, fallback to default colorspace";
return VideoColorSpace();
}
std::vector<uint8_t> buffer;
for (size_t j = 0; j < arrays.size(); j ) {
for (size_t i = 0; i < arrays[j].units.size(); i) {
buffer.insert(buffer.end(), kAnnexBStartCode,
kAnnexBStartCode kAnnexBStartCodeSize);
buffer.insert(buffer.end(), arrays[j].units[i].begin(),
arrays[j].units[i].end());
}
}
H265Parser parser;
H265NALU nalu;
parser.SetStream(buffer.data(), buffer.size());
while (true) {
H265Parser::Result result = parser.AdvanceToNextNALU(&nalu);
if (result != H265Parser::kOk)
return VideoColorSpace();
switch (nalu.nal_unit_type) {
case H265NALU::SPS_NUT: {
int sps_id = -1;
result = parser.ParseSPS(&sps_id);
if (result != H265Parser::kOk) {
DVLOG(1) << "Could not parse SPS, fallback to default colorspace";
return VideoColorSpace();
}
const H265SPS* sps = parser.GetSPS(sps_id);
DCHECK(sps);
return sps->GetColorSpace();
}
default:
break;
}
}
}
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
// media/formats/mp4/box_definitions.cc
case FOURCC_HEV1:
case FOURCC_HVC1: {
DVLOG(2) << __func__ << " parsing HEVCDecoderConfigurationRecord (hvcC)";
std::unique_ptr<HEVCDecoderConfigurationRecord> hevcConfig(
new HEVCDecoderConfigurationRecord());
RCHECK(reader->ReadChild(hevcConfig.get()));
video_codec = VideoCodec::kHEVC;
// 这里调用,获取一下色彩空间
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
video_color_space = hevcConfig->GetColorSpace();
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
video_codec_profile = hevcConfig->GetVideoProfile();
...
case FOURCC_DVH1:
case FOURCC_DVHE: {
DVLOG(2) << __func__ << " reading HEVCDecoderConfigurationRecord (hvcC)";
std::unique_ptr<HEVCDecoderConfigurationRecord> hevcConfig(
new HEVCDecoderConfigurationRecord());
RCHECK(reader->ReadChild(hevcConfig.get()));
// 这里调用,获取一下色彩空间
#if BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
video_color_space = hevcConfig->GetColorSpace();
#endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
...
在与 Edge 进行对比后可以发现,Edge 只通过容器读取色彩空间,而没有 SPS 读取的逻辑,这会导致 HDR 视频无法正确 Tone Mapping,最终渲染视频异常,而在 Chromium 内则一切正常。
(左图为 Edge 在处理 HLG 视频时 Tone Mapping 异常的问题)
总结在上述步骤后,硬解步骤已完成的差不多,目前所有的 CL 和 Fix 已合入 Chromium 104(main 分支),Windows 平台具体实现过程和代码 Diff 也可以追溯这个 Crbug(https://bugs.chromium.org/p/chromium/issues/detail?id=1286132)。
与 Edge / Safari 的对比与测试说了一堆技术实现可能会很枯燥,下面来到最有趣的环节:“与竞品对比”。为了公平起见,使用原生 HTML 原生 Video 标签方式,排除一切外界干扰完成一个基础的测试页面,并收集了 28 个不同 Profile、HDR / 非 HDR、不同位深的测试 Case(测试素材来自网络:https://lf3-cdn-tos.bytegoofy.com/obj/tcs-client/resources/video_demo_hevc.html),下面开始测试:
HDR 测试我们首先进行 HDR 能力测试,测试选择了多个 PQ、HLG Transfer 的 HEVC 视频。
PQ SDR 显示器测试毕竟不是所有人都使用 HDR 显示器,甚至可以说 99.99% 的用户仍在使用 SDR 显示器,因此 HDR 视频是否能在普通 SDR 显示器正确显示,是非常重要的,将 HDR 视频转换为 SDR 视频的过程一般被称作做 Tone Mapping,因此下述测试主要测试浏览器是否支持 Tone Mapping。