[C#] FFmpeg 音视频开发总结

为什么选择FFmpeg?

  1. 延迟低,参数可控,相关函数方便查询,是选择FFmpeg作为编解码器最主要原因,如果是处理实时流,要求低延迟,最好选择是FFmpeg。
  2. 如果需要用Opencv或者C#的Emgucv这种库来处理视频流,也多是用FFmpeg做编解码然后再转换图像数据给Opencv去处理。用Opencv编解码延迟很高。
  3. 其他的库多是基于FFmpeg封装,如果做一个视频播放器,像vlc这种库是非常方便的,缺点是臃肿,需要手动剔除一些文件,当然也有一些是基于FFmpeg封装好的视频播放器库,也能快速实现一个播放器。
  4. 如果是加载单Usb接口中的多Usb摄像头,FFmpeg这时就无能为力了,经过测试使用DirectShow能够实现。AForge一个很好的学习简单样例,它将DirectShow封装。能轻松实现加载单Usb接口中的多Usb摄像头(不过它很久没更新了,目前无法设置摄像头参数,也没有Usb摄像头直接录制,所以我把它重写了),还有WPF-Mediakit这个库,实现使用Directshow封装的更好(功能和结构也更复杂),同时也有DriextX和Media Foundation当然使用其他库也是可以的。
  5. 写此文章时才发现CaptureManager这个2023年4月发布的非常简便好用的基于D3D封装的音视频库,它的官方样例非常丰富,能实现很多功能。我尝试了运行了他的官方样例,打开相同规格的Usb摄像头,发觉cpu占用是FFmpeg的两倍。

如何学习FFmpeg?

记录一下我是如何学习FFmpeg。首先是C#使用FFmpeg基本上用的是FFmpeg.autogen这个库。也可以使用FFmpeg.exe,先不谈论FFmpeg.exe的大小,我尝试过从exe中取数据到C#前端显示,相同参数情况下,延迟比使用FFmpeg.autogen高,主要是不能边播放边录制(可以用其它的库来录制,但是效率比不上只使用一个库)。

当然如果只需要部分功能也可以自己封装FFmpeg(太花时间了,我放弃了。如果是专门从事这一行的可以试试)。

学习FFmpeg.autogen可以先去Github上下载它的样例(其实样例有个小问题,后面说),学习基础的编解码。

后面有人把官网的C++的样例用FFmpeg.autogen写了一遍,我把样例压缩好放夸克网盘了:https://pan.quark.cn/s/c579aad1d8e0

然后是查看一些博客和Github上一些项目,了解编解码整体架构,因为FFmpeg很多参考代码都是c++的所以我基本是参考C++写C#,写出整体的编解码代码。

无论是编解码还是开发Fliter都会涉及到很多参数设置。要查找这些参数,我先是去翻博客,最后还是去FFmpeg官网(官网文档,编解码参数很全),当然制作视频滤镜和一些其他功能,也是参考官网的参数。

对于部分基础函数(有些函数会把帧用掉就释放,要注意)查看FFmpeg的源码,理解原理。

对于一些概念性的东西,我是翻阅硕博论文(一般都有总结这些)。

 C#使用FFmpeg需要注意什么?

  1. FFmpeg.autogen是有一个缺点的,它是全静态的,不支持多线程(这个我问作者了),所以用多进程,而用多进程渲染到同一画面,可以参考我上一篇MAF的文章。
  2. 尤其要注意帧释放,编解码的帧如果没有释放是一定会产生内存泄漏的,而且速度很快。
  3. 其次是c# 要将图像数据渲染到界面显示,最最好使用WriteableBitmap,将WriteableBitmap和绑定到一个Image然后更新WriteableBitmap。我记得在一篇博客中提到高性能渲染,使用MoveMemory来填充WriteableBitmap的BackBuffer,核心代码如下。
    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
        private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);
     writeableBitmap.Lock();
            unsafe
            {
                 fixed (byte* ptr = intPtr)
                  {
                    MoveMemory(writeableBitmap.BackBuffer, new IntPtr(ptr), (uint)intPtr.Length);
                   }
           }
           writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
    
    writeableBitmap.Unlock();

    这样处理有个致命的缺点。WriteableBitamp的宽高必须为2的整数倍,即使是修正过大小,当传入数据为特殊尺寸使用此方法时还是会出现显示异常的情况。所以还是老实使用WriteableBitmap的WritePixels。

  4. 对于FFmpeg很多函数都是会返回错误信息,一定要将错误信息记录到日志,方便查找和查看(基本每个函数要加错误信息判断)。
  5. 软编码会占用大量的CPU资源,所以最好采用硬编码。FFmpeg有一个查找编解码器的函数,它并不能查看硬件编码器。如果要使用硬件加速查找编解码器最好是用其他方式获取系统设备或者直接一个一个打开NVDIA和QSV等加速,都失败了再启用软编解码。
  6. QSV硬编码要求输入的像素格式必须为AVPixelFormat.AV_PIX_FMT_NV12,如果是硬解码出的数据,可以直接编码,否则需要添加格式转换。FFmepg.autogen的官方样例中有格式转换函数,但由于它没有指定转换后的格式会出问题(踩坑)。
  7. 尽量少的格式转换,或者帧复制。这两种方式会提高cpu和内存使用率同时也会有更高的延迟。
  8. 在制作FFmpeg的带有文本的Filter时,将需要使用的字体复制到项目目录然后指定字体位置而不是调用系统的字体(不知道是版本原因还是什么问题,一用系统字体就会产生内存泄漏)。
  9.  注意编解码数据的格式。一些老的格式,虽然解码没有什么问题(ffmpeg 会有提示)但是编码是不支持的,出现这种问题,程序会直接死掉(踩坑)。
  10. 解码时可以通过解码数据自动搜寻硬件解码器,而硬件编码需要手动指定编码器(可以通过,查找并自动选择GPU来实现自动选择)。
  11. 多线程实现播放同时录制时,最好采用帧复制ffmpeg.av_frame_clone(hwframe)不用对同一个帧进行操作。当然也可以不用多线程,同一个帧在播放完成后进行,录制。

  暂时只想到这些,有其他的想法再更新     

如果有任何错误欢迎批评指正。