怎么在笔记本上打开access,怎么打开电脑里自带的access

首页 > 实用技巧 > 作者:YD1662023-11-13 07:26:15

因此尝试搜索发现 Windows 平台 VDAVideoDecoder 代码实现均位于dxva_video_decode_accelerator_win.cc文件内,继续寻找蛛丝马迹,发现,在开源的 Chromium 项目内,并不存在 HEVC 硬解相关的任何实现,这说明 Edge 是自己基于某个时期的 Chromium Media 模块“魔改”出来的 HEVC 硬解支持,同时在这个过程发现了个有趣的现象:

  1. Edge 完全不使用 D3D11VideoDecoder 进行解码,而是使用 VDAVideoDecoder 解码,我猜测其目的是为了推广自家的 Media Foundation。
  2. Edge 的色彩处理有问题 (Edge 102),与 Chrome 不一样的点在于,比如对于 Transfer 是 PQ 的 HDR 视频,Edge 并没有对其进行 Tone Mapping,导致 PQ 视频在 Edge 下看起来有“过曝”的问题。

怎么在笔记本上打开access,怎么打开电脑里自带的access(13)

  1. Edge 解码 AV1 需要装 AV1 Video Extension ,才可解码 AV1(软解 硬解),而 Chrome 由于实现了 AV1 的 D3D11VA 硬解,以及 Gav1VideoDecoder 和 DAV1dVideoDecoder 软解 Decoder,不需要安装 AV1 插件也可在受支持的显卡硬解,不受支持的显卡软解。

怎么在笔记本上打开access,怎么打开电脑里自带的access(14)

好吧,尽管没少吐槽 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 内则一切正常。

怎么在笔记本上打开access,怎么打开电脑里自带的access(15)

(左图为 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。

怎么在笔记本上打开access,怎么打开电脑里自带的access(16)

上一页12345下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.