Skip to content

硬件加速

ck edited this page Jul 9, 2018 · 1 revision

各个平台上的硬件加速标准介绍

支持硬件加速,是现代播放器需要实现的一个重要功能。原因在于,视频分辨率越来越高,带来的是更高的清晰度和视觉享受,同时给工程师带来的是更多的挑战,对计算机的硬软件也是更多的负担。

从最初的 VCD、DVD、到蓝光高清,分辨率从 352x240、640x480 到 720p、1080p、2K、4K,数据量是成倍,甚至数十倍的增加。视频压缩算法从 MPEG-1、MPEG-2、MPEG-4、wmv 到 rmvb、h264、h265 等等各种视频压缩标准。视频分辨率越来越高,压缩算法也越来越复杂,播放器在进行解码和回放时,要做的事情越来越多,完全依靠 cpu 执行软件解码算法,去解码和播放高清视频已经越来越难,甚至成为了不可能的事情。

因此无论在 PC 上,还是在手机、平板上,都出现了利用 GPU 或者 VPU 这类硬件单元,来实现视频的硬件解码功能。在 windows 上,有许多的硬件解码接口标准,微软的 dxva、dxva2、d3dva,NVIDIA 定义了 CUDA / CUVID / NVENC 接口标准,intel 有 libmfx,android 上有 mediacodec 接口,...

windows 平台上,如果要使用 NVIDIA 或者 intel 的硬件解码功能,都需要用户安装第三方的驱动程序或者软件包,这就导致了使用上的不方便和一些兼容问题。而微软的 dxva2 和 d3d11va 则是 windows 系统自带的功能,不需要额外安装任何的第三方驱动,具备良好的兼容性。只要是 win7 以上的 PC 都可以很好的支持 dxva2,而 d3d11va 则是 win10 自带支持,win7 上需要安装 DirectX11。因此在 fanplayer 的 win32 平台实现中,目前只提供了对 dxva2 的硬件解码支持。

android 平台上,目前可用的标准只有 mediacodec,fanplayer 也提供了对 mediacodec 的支持。

fanplayer 中对 dxva2 和 mediacodec 的支持,都是通过 ffmpeg 实现的,也就是说 ffmpeg 当前实际上已经实现了对 dxva2 和 mediacodec 硬件加速的支持,我们要做的就是搞懂如何在 ffmpeg 的基础上,调用相关的 api 接口,实现硬解码功能。

在这个网站,我们可以看到 ffmpeg 对不同平台,不同硬件解码标准的支持情况: https://trac.ffmpeg.org/wiki/HWAccelIntro

同时,在 ffmpeg 中也有相关的源代码可参考,如何去实现硬解码。

Android 平台的 mediacodec 硬件加速

android 平台上的硬件码实现相对容易很多,只需要把 decoder 替换为硬件解码的 decoder 即可(弊端就是没法实现 0-copy 后面会介绍)。播放器在打开一个视频文件时,会侦测(probe)视频类型,以及用的是什么压缩标准编码的音视频,比如 h264, aac。如果是 h264 播放器会创建一个 h264_decoder 这样的东西去解码。在 android mediacode 的硬件加速方案里,ffmpeg 针对不同的压缩标准,提供了对应的 mediacodec 的 decoder,比如 h264 的 h264_mediacodec,它就是 android 平台上支持硬件解码的 h264 解码器。fanplayer 中有这样的代码段:

    //+ try android mediacodec hardware decoder
    if (player->init_params.video_hwaccel) {
#ifdef ANDROID
        switch (player->vcodec_context->codec_id) {
        case AV_CODEC_ID_H264      : decoder = avcodec_find_decoder_by_name("h264_mediacodec" ); break;
        case AV_CODEC_ID_HEVC      : decoder = avcodec_find_decoder_by_name("hevc_mediacodec" ); break;
        case AV_CODEC_ID_VP8       : decoder = avcodec_find_decoder_by_name("vp8_mediacodec"  ); break;
        case AV_CODEC_ID_VP9       : decoder = avcodec_find_decoder_by_name("vp9_mediacodec"  ); break;
        case AV_CODEC_ID_MPEG2VIDEO: decoder = avcodec_find_decoder_by_name("mpeg2_mediacodec"); break;
        case AV_CODEC_ID_MPEG4     : decoder = avcodec_find_decoder_by_name("mpeg4_mediacodec"); break;
        default: break;
        }
        if (decoder && avcodec_open2(player->vcodec_context, decoder, NULL) == 0) {
            player->vstream_index = idx;
            av_log(NULL, AV_LOG_WARNING, "using android mediacodec hardware decoder %s !\n", decoder->name);
        } else {
            avcodec_close(player->vcodec_context);
            decoder = NULL;
        }
        player->init_params.video_hwaccel = decoder ? 1 : 0;
#endif
    }
    //- try android mediacodec hardware decoder

这段代码会根据当前视频文件的视频压缩标准,去查找是否有可用的 mediacode 的硬件解码器,如果有那么就使用这个硬件解码器,之后在 video_decode_thread 中,调用 avcodec_decode_video2 这个接口,实际上就已经是硬件解码了。除了增加这个代码段,播放器的其他部分代码是不需要做任何改动就可以实现硬件解码了,因此说在 android 平台实现 mediacodec 硬件解码是很容易的事情。

windows 上的 dxva2 硬件加速

win32 上要实现 dxva2 硬件加速,我们在 fanplayer 中增加了 dxva2hwa.cpp 和 dxva2hwa.h。dxva2hwa 的接口定义如下: int dxva2hwa_init(AVCodecContext *ctxt, void *d3ddev); void dxva2hwa_free(AVCodecContext *ctxt); 可以看到,接口很简单,就是一个初始化和反初始化。初始化的时候需要一个 d3ddev,这个 d3ddev 是对应 d3d 的设备,这个设备是用 d3d 的 CreateDevice 接口方法创建出来的。我们在 vdev-d3d.cpp 中,vdev_d3d_create 函数里面就创建了这个设备了。现在就需要把这个设备传给这个 dxva2hwa_init 初始化函数即可完成初始化。

那么怎么传?我们修改了 vdev_getparam 代码,通过 vdev_getparam 来获取这个 dev。但是 dxva2hwa_init 是在 ffplayer.cpp 中调用的,这个地方我们只能访问到 ffrender 对象,而 vdev 设备是在 ffrender 对象内部的。我们有两个方法,可从 player_getparam 开始到 vdev_getparam 把这个获取 d3ddev 的接口通路打通即可,等于是通过 player_getparam 可以直接得到 d3ddev。另外就是通过 player_getparam 得到 vdev 对象,然后通过 vdev_getparam 得到 d3ddev。我选择了后者,这里不关乎性能问题,因为获取 d3ddev 只需要在初始化的时候执行一次。更多的考虑是不希望暴露更多的接口给外部,就是说不希望用户可以直接通过 player_getparam 来获取 d3ddev。

知道怎么获取 d3ddev 那么我们就要想想,在什么地方去调用这个接口了。肯定是必须在 render 创建之后,否则我们是没法获取到正确的 d3ddev 的,所以我们就需要在 player_prepare 中去执行它。另外 dxva2hwa_init 返回值的含义,如果为 0 代表当前平台是支持 dxva2 硬件加速的。那么我们的代码就这样写了:

if (player->vstream_index == -1) {
    int effect = VISUAL_EFFECT_WAVEFORM;
    render_setparam(player->render, PARAM_VISUAL_EFFECT, &effect);
} else if (player->init_params.video_hwaccel) {
#ifdef WIN32
    void *d3ddev = NULL;
    render_getparam(player->render, PARAM_VDEV_GET_D3DDEV, &d3ddev);
    if (dxva2hwa_init(player->vcodec_context, d3ddev) != 0) {
        player->init_params.video_hwaccel = 0;
    }
#endif
}

可以看出逻辑流程是,播放器上层传入了 init_params.video_hwaccel 希望使用硬件加速,因此 if 语句中尝试初始化 dxva2hwa 硬件加速,调用初始化如果成功,说明当前是可以支持硬件加速的,如果失败则不支持,同时把 init_params.video_hwaccel 设置为 0,这样可以让上层调用者知道实际的硬件加速的开启情况。

一旦 dxva2hwa_init 调用成功,ffmpeg 内部就会采用 dxva2 硬件加速的方式去解码。抛开视频渲染不考虑,播放器的其他部分代码就不需要做任何改动了,就跟软件解码一样。但是 dxva2 能做到更好的地方是,解码后的视频数据的 0-copy(零拷贝)播放。简单说,就是解码出来,不用再做像素格式转换,也不用再做任何的内存拷贝动作,可以直接渲染显示到屏幕,这样可以极大的减少内存拷贝消耗的 cpu 资源。别小看这拷贝,现在的 2k、4k 的高清视频,解码后的数据量是非常大的,简单算算就知道 4k * 4k * 30 = 480MB(30 表示每秒 30 帧),每秒的数据量是在 500MB 这样的级别。DDR3 的内存带宽有多大,这样的数据量得吃掉多大的带宽啊?还是很恐怖的。所以播放高清,除了硬解,一定要有 0-copy,否则硬解的性能根本就没能真正发挥出来。

那么如何实现 0-copy,实际上 ffmpeg 的 dxva2 就是 0-copy 的,如果只调用了 dxva2hwa_init,其他地方不改,我们发现视频解码了,但是没法显示,播放器是黑屏的。原因就在于 avcodec_decode_video2 执行解码后,得到的不在是一个存放了实际图像数据的 vframe 了,而是将一个 d3d surface 的指针,放到了 vframe 的 data[3] 中。我们只需要想办法吧 data[3] 作为 surface 扔给 vdev-d3d 去显示即可。

因此,我们修改了 vdev-d3d 的 vdev_d3d_setparam 接口,使用 PARAM_VDEV_POST_SURFACE 控制字来把这个 surface 送给 vdev-d3d。同时还得修改 ffrender.cpp 代码,render_video 中:

    if (video->format == AV_PIX_FMT_DXVA2_VLD) {
        vdev_setparam(render->vdev, PARAM_VDEV_POST_SURFACE, video);
    } else {
        vdev_lock(render->vdev, picture.data, picture.linesize);
        if (picture.data[0] && video->pts != -1) {
            sws_scale(render->sws_context, video->data, video->linesize, 0, render->video_height, picture.data, picture.linesize);
        }
        vdev_unlock(render->vdev, video->pts);
    }

我们看到,采用 dxva2 硬解出来的 frame 它的 format,即像素格式是 AV_PIX_FMT_DXVA2_VLD,这样我们就可以在 render_video 中做判断来分别处理,对于 AV_PIX_FMT_DXVA2_VLD 类型的 frame,直接送给 vdev-d3d 去,其他的则用 vdev_lock -> sws_scale -> vdev_unlock 去实现渲染。这里我们就看到了什么是 0-copy。sws_scale 是要做像素格式转换和拷贝动作的,这个就是 cpu 的开销。而 PARAM_VDEV_POST_SURFACE 的方法,则不用做像素格式转换和拷贝动作,是 0-copy 的渲染。

总结

总结来说,win32 的 dxva2 硬件加速,需要涉及到 ffplayer.cpp, ffrender.cpp, vdev-d3d.cpp 等相关模块的修改,打通硬件解码和渲染的通路。同时这个通路是 0-copy 的。因此 dxva2 的速度,是比 android mediacode 的硬解播放更快的,更省 cpu 资源,更少的功耗和发热量。我也思考过 android mediacode 能否实现 0-copy,但是目前还没有找到好的方法。

关于 dxva2hwa 的代码,这部分代码实际上就是参考 ffmpeg 里面的源代码来修改实现的,这个可以理解为套路,我也没有过多深入研究,大家有兴趣可以看下,大致也能看明白原理和流程,不复杂。这部分代码也可能会跟随 ffmpeg 的版本升级而变更,因此升级 ffmpeg 库的时候,要格外小心的去看看这部分代码有没有更新。

另外要说明的是,我们的实现中,是修改了 ffmpeg 的源代码的,因为 ffmpeg 内部需要一个 d3ddev,这个东西在前面的分析中我们知道了,是由 vdev-d3d 创建的,然后通过 dxva2hwa_init 传给 ffmpeg 的。但是 ffmpeg 的原始做法是由 ffmpeg 内部去创建和维护的。这样就导致程序中有两个 d3d dev 设备,这样就麻烦了。所以为了实现方便,我干脆就修改了 ffmpeg 的相关源代码,直接把 ffmpeg 内部创建 d3d dev 的代码删掉了。

完了。