专栏首页DotNet程序园使用C#+FFmpeg+DirectX+dxva2硬件解码播放h264流

使用C#+FFmpeg+DirectX+dxva2硬件解码播放h264流

本文门槛较高,因此行文看起来会乱一些,如果你看到某处能会心一笑请马上联系我开始摆龙门阵 如果你跟随这篇文章实现了播放器,那你会得到一个高效率,低cpu占用(单路720p视频解码播放占用1%左右cpu),且代码和引用精简(无其他托管和非托管的dll依赖,更无需安装任何插件,你的程序完全绿色运行);并且如果硬解不可用,切换到软件是自动过程

  首先需要准备好visual studio/msys2/ffmpeg源码/dx9sdk。因为我们要自己编译ffmpeg,并且是改动代码后编译,ffmpeg我们编译时会裁剪。

  • ffmpeg源码大家使用4.2.1,和我保持同步,这样比较好对应,下载地址为ffmpeg-4.2.1.tar.gz
  • msys2安装好后不需要装mingw和其他东西,只需要安装make(见下方图片;我们编译工具链会用msvc而非mingw-gcc)

msys2安装make

  • visual studio版本按道理是不需要新版本的,应该是2008-2019都可以(不过还是得看看ffmpeg代码里是否用了c99 c11等低版本不支持的东西),vs需要安装c++和c#的模块(见下方图片;应该也不需要特意去打开什么功能)

vs所需功能模块

  • dx9的sdk理论上是不用安装的(如果你是高手,可以用c#的ilgenerator直接写calli;亦或者写unsafe代码直接进行内存call,文章最后我会为大家揭秘如何用c#调用c++甚至com组件)。我用了directx的managecode,由官方为我们做了dx的调用(见下方图片)

安装好dx的sdk后我们得到c#的托管引用dll

  第二步是修改ffmpeg源码并编译,我们要修改的源码只有一个文件的十余行,而且是增量修改。

修改的文件位于libavutil/hwcontext_dxva2.c文件,我先将修改部分贴出来然后再给大家解释

hwcontext_dxva2.c修改部分

  代码中dxva2_device_create9_extend函数是我新加入的,并且在dxva2_device_create函数(这个函数是ffmpeg原始流程中的,我的改动不影响原本任何功能)中适时调用;简单来说,原来的ffmpeg也能基于dxva2硬件解码,但是它没法将解码得到的surface用于前台播放,因为它创建device时并未指定窗口和其他相关参数,大家可以参考我代码实现,我将窗口句柄传入后创建过程完全改变(其他人如果使用我们编译的代码,他没有传入窗口句柄,就执行原来的创建,因此百分百兼容)。

原始文件(版本不一致,仅供参考)

  (ps:在这里我讲一下网络上另外一种写法(两年前我也用的他们的,因为没时间详细看ffmpeg源码),他们是在外面创建的device和surface然后想办法传到ffmpeg内部进行替换,这样做有好处,就是不用自己修改和编译ffmpeg,坏处是得自己维护device和surface。至于二进制兼容方面考虑,两种做法都不是太好)

代码修改完成后我们使用msys2编译

  • 首先是需要把编译器设置为msvc,这个步骤通过使用vs的命令行工具即可,如下图

打开vs的编译工具

  • 然后是设置msys2继承环境变量(这样make时才能找到cl/link)

设置msys继承环境变量

将msys自带link重命名避免冲突

  • 打开msys,查看变量是否正确

检查变量正确性

  • 编译ffmpeg
./configure --enable-shared --enable-small --disable-all --disable-autodetect --enable-avcodec --enable-decoder=h264 --enable-dxva2 --enable-hwaccel=h264_dxva2 --toolchain=msvc --prefix=host
make && make install

cmake和make语句

编译完成后头文件和dll在host文件夹内(编译产出的dll也是clear的,不依赖msvc**.dll)

编译产出

  在C#中使用我们产出的方式需要使用p/invoke和unsafe代码。

我先贴出我针对ffmpeg写的一个工具类,然后给大家稍微讲解一下

FFHelper.cs

上文中主要有几个地方是知识点,大家做c#的如果需要和底层交互可以了解一下

  • 结构体的使用   结构体在c#与c/c++基本一致,都是内存连续变量的一种组合方式。与c/c++相同,在c#中,如果我们不知道(或者可以规避,因为结构体可能很复杂,很多无关字段)结构体细节只知道结构体整体大小时,我们可以用Pack=1,SizeConst=来表示一个大小已知的结构体。
  • 指针的使用   c#中,有两种存储内存地址(指针)的方式,一是使用interop体系中的IntPtr类型(大家可以将其想象成void*),一是在不安全的上下文(unsafe)中使用结构体类型指针(此处不讨论c++类指针)
  • unsafe和fixed使用   简单来说,有了unsafe你才能用指针;而有了fixed你才能确保指针指向位置不被GC压缩。我们使用fixed达到的效果就是显式跳过了结构体中前部无关数据(参考上文中AVCodecContext等结构体定义),后文中我们还会使用fixed。

  现在我们开始编写解码和播放部分(即我们的具体应用)代码

FFPlayer.cs

下面讲解代码最主要的三个部分

  • 初始化ffmpeg   主要在静态块和构造函数中,过程中我没有将AVPacket和AVFrame局部化,很多网上的代码包括官方代码都是局部化这两个对象。我对此持保留意见(等我程序报错了再说)
  • 将收到的数据送入ffmpeg解码并将拿到的数据进行展示   这里值得一提的是get_format,官方有一个示例,下图

官方的硬解码示例

它有一个get_format过程(详见215行和63行),我没有采用。这里给大家解释一下原因:

这个get_format的作用是ffmpeg给你提供了多个解码器让你来选一个,而且它内部有一个机制,如果你第一次选的解码器不生效(初始化错误等),它会调用get_format第二次(第三次。。。)让你再选一个,而我们首先认定了要用dxva2的硬件解码器,其次,如果dxva2初始化错误,ffmpeg内部会自动降级为内置264软解,因此我们无需多此一举。

  • 发现解码和播放过程中出现异常的解决办法
    • 不支持硬解 代码中已经做出了一部分兼容,因为baseline的判定必须解出sps/pps才能知道,因此这个错误可能会延迟爆出(不过不用担心,如果此时报错,ffmpeg会自动降级为软解)
    • 窗体大小改变 基于DirectX中设备后台缓冲的宽高无法动态重设,我们只能在控件大小改变时推倒重来。如若不然,你绘制的画面会进行意向不到的缩放
    • 网络掉包导致硬件解码器错误 见代码
    • 其他directx底层异常 代码中我加了一个try-catch,捕获的异常类型是DirectXException,在c/c++中,我们一般是调用完函数后会得到一个HRESULT,并通过FAILED宏判定他,而这个步骤在c#自动帮我们做了,取而代之的是一个throw DirectXException过程,我们通过try-catch进行可能的异常处理(实际上还是推倒重来)

  番外篇:C#对DiretX调用的封装 上文中我们使用DirectX的方式看起来即非COM组件,又非C-DLL的P/Invoke,难道DirectX真有托管代码? 答案是否定的,C#的dll当然也是调用系统的d3d9.dll。不过我们有必要一探究竟,因为这里面有一个隐藏副本

首先请大家准备好ildasm和visual studio,我们打开visual studio,创建一个c++工程(类型随意),然后新建一个cpp文件,然后填入下面的代码

测试代码

如果你能执行,你会发现输出是88;然后我们使用ildasm找到StrechRectangle的代码

ildasm中的呈现

你会发现也有一个+88的过程,那么其实道理就很容易懂了,c#通过calli(CLR指令)可以执行内存call,而得益于微软com组件的函数表偏移量约定,我们可以通过头文件知道函数对于对象指针的偏移(其实就是一个简单的ThisCall)。具体细节大家查阅d3d9.h和calli的网络文章即可。

本文分享自微信公众号 - DotNet程序园(dotnetblog),作者:云中双月

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ASP.NET Core 2.2 : 二十七. JWT与用户授权(细化到Action)

    上一章分享了如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新,本章继续进行下一步,用户授权。涉及到的例子也以上一章的为基础。(ASP....

    梁规晓
  • ORM 开发环境之利器:MVC 中间件 FreeSql.AdminLTE

    这是一篇纯技术干货的分享文章,FreeSql 已经基本完成 .NETCore 最方便的 ORM 使命,我们正在筹备生态的建立,比如 ABP 中如何使用 Free...

    梁规晓
  • 已实现乐观锁功能,FreeSql.DbContext 准备起航

    上回说到 FreeSql.DbContext 的规则,以及演示它的执行过程,可惜当时还不支持“乐观锁”,对于更新数据来讲并不安全。

    梁规晓
  • 最令程序员沮丧的 10 件事

    软件开发是一个伟大的工作——和任何其他工作一样,它也有它的缺点。下面的10件事就是大多数程序员关于编程所无法苟同的。

    哲洛不闹
  • 基于Jquery WeUI的微信开发H5页面控件的经验总结(2)

      在微信开发H5页面的时候,往往借助于WeUI或者Jquery WeUI等基础上进行界面效果的开发,由于本人喜欢在Asp.net的Web界面上使用JQuery...

    不会飞的小鸟
  • 最令程序员沮丧的十件事

    er双旦快乐~! 软件开发是一个伟大的工作——和任何其他工作一样,它也有它的缺点。下面的十件事就是大多数程序员关于编程所无法苟同的。 对于非软件开发人员来说,...

    智能算法
  • 最令程序员恐惧的 10 件事,据说还没有全部“躺枪”的

    软件开发是一个伟大的工作——和任何其他工作一样,它也有它的缺点。下面的10件事就是大多数程序猿关于编程所无法苟同的。 对于非软件开发人员来说,开发人员的工作看起...

    BestSDK
  • 如何阅读一份代码?

    上文谈到了像读书一样阅读源码的重要性,今天谈谈如何阅读一份代码。我所谓的一份代码,其范围可能从几千行到数万行,有时甚至可多达数十万行。这些代码作为一个有机体,共...

    tyrchen
  • 25个常规方法优化你的jquery代码

    1. 从Google Code加载jQueryGoogle Code上已经托管了多种JavaScript类库,从Google Code上加载jQuery比直接从...

    javascript.shop
  • 算法与数据结构(十七) 基数排序(Swift 3.0版)

    前面几篇博客我们已经陆陆续续的为大家介绍了7种排序方式,今天博客的主题依然与排序算法相关。今天这篇博客就来聊聊基数排序,基数排序算法是不稳定的排序算法,在排序数...

    lizelu

扫码关注云+社区

领取腾讯云代金券