C# + FFmpeg 音视频开发总结

303 阅读9分钟

为什么选择 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封装的更好(功能和结构也更复杂)。经过学习和代码编写,DirectShow录制视频能力有限,还是推荐使用DirectShow显示Media Foundation录制。

4、写此文章时才发现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写了一遍,我把样例压缩好放夸克网盘了:pan.quark.cn/s/c579aad1d…

然后是查看一些博客和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(00, width, height));
writeableBitmap.Unlock();

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

所以还是老实使用WriteableBitmap的WritePixels。

4、对于FFmpeg很多函数都是会返回错误信息,一定要将错误信息记录到日志,方便查找和查看(基本每个函数要加错误信息判断)。

一般的错误返回的是一个int类型的负值,可以使用下面函数转为string:

public static string ErrorToString(int tag)
{
 tag = Math.Abs(tag);
 if (tag < 100) return tag.ToString();
 return Convert.ToChar((byte)(tag)).ToString() + Convert.ToChar((byte)(tag >> 8)) + Convert.ToChar((byte)(tag >> 16)) + Convert.ToChar((byte)(tag >> 24));
 }

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)不用对同一个帧进行操作。当然也可以不用多线程,同一个帧在播放完成后进行,录制。

12、ffmpeg查找编解码器的函数,找到了编码器并不是物理存在的,而已支持的编码器(比如我一个inter核显的本子也能找到cuda的编码器),所以要确认是否能够打开,打不开就继续找。

可以通过循环查找硬件配置

  AVCodecHWConfig* config = ffmpeg.avcodec_get_hw_config(codec, i);

然后是检查是否能够打开硬件编码器

int error = ffmpeg.av_hwdevice_ctx_create(&_avcodecCtx->hw_device_ctx, deviceType, null, null, 0);
if(error!=0){
//硬件设备打开失败
}

稳妥起见还是通过查找硬件后来指定,硬件编码器

  private void ConfigureHWDecoder(out AVHWDeviceType type,out AVPixelFormat aVPixel)
        {
            type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
            aVPixel = AVPixelFormat.AV_PIX_FMT_NONE;
            if (!Hw) return;
            switch (Gputype)
            {
                case 0:
                    type = AVHWDeviceType.AV_HWDEVICE_TYPE_CUDA;
                    aVPixel = AVPixelFormat.AV_PIX_FMT_CUDA;
                    return;
                case 1:
                    type = AVHWDeviceType.AV_HWDEVICE_TYPE_QSV;
                    aVPixel = AVPixelFormat.AV_PIX_FMT_QSV;
                    return;
                case 2:
                    type = AVHWDeviceType.AV_HWDEVICE_TYPE_OPENCL;
                    aVPixel = AVPixelFormat.AV_PIX_FMT_OPENCL;
                    return;
            }
        }

13、对于H264或者H265这种网络流,接收到错误的配置信息错误的设置会导致获取的 FPS是实际两倍,直接录制就会出现录制时长为真实时长一半的问题(踩坑)

14、对于解码或者编码时会创建和使用AVPacket和AVFrame。因为音视频一般是连续的,一般只初始化然后重复使用例如:

AVFrame* pDecodedFrame = ffmpeg.av_frame_alloc();
AVPacket* pPacket = ffmpeg.av_packet_alloc();
while(!CloseCommand){
  error = ffmpeg.av_read_frame(formatContext, pPacket);//读取frame
   ....
}    

这是解码,当然编码也是同理。

原因是在C#上连续多次重复创建unsafe对象,即使主动释放有GC回收,还是会慢慢泄漏(跑俩小时解码一个视频,内存泄漏了50m,短期看不出来)

15、录制音视频开始的前几帧可能会出现dts错误(坏帧) Application provided invalid, non monotonically increasing dts to muxer in stream x: -xxx>= xxx,所以要在 ffmpeg.av_interleaved_write_frame(写入帧)之前对前几帧进行检查。

#region 帧检查
        int checkcount = 10;//检查前10帧
        long m_last_video_dts =ffmpeg. AV_NOPTS_VALUE;  
        long m_last_audio_dts = ffmpeg.AV_NOPTS_VALUE;  
        long m_video_interframe_dts = ffmpeg.AV_NOPTS_VALUE;
        bool Check_packet(AVPacket* pkt)
        {
            AVStream* in_stream = _formatContext->streams[pkt->stream_index];
            if (in_stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
            {
                if (m_last_video_dts == ffmpeg.AV_NOPTS_VALUE)
                {
                    if (pkt->dts != 0)
                    {
                        pkt->pts = pkt->dts = m_last_video_dts = 0;
                    }
                }

                if ( m_last_video_dts >= pkt->dts)
                {
                    if (m_video_interframe_dts == ffmpeg.AV_NOPTS_VALUE)
                    {
                        AVRational r =ffmpeg.av_d2q(in_stream->avg_frame_rate.den, in_stream->avg_frame_rate.num);
                        m_video_interframe_dts = ffmpeg.av_rescale_q_rnd(1, r, in_stream->time_base,
                                      (AVRounding.AV_ROUND_NEAR_INF | AVRounding.AV_ROUND_PASS_MINMAX));

                    }
                    pkt->pts = pkt->dts = m_last_video_dts += m_video_interframe_dts;
                }
                else
                {
                    m_last_video_dts = pkt->dts;
                }
            }
            else if (in_stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
            {
                if (m_last_audio_dts >= pkt->dts)
                {
                    ffmpeg.av_packet_unref(pkt);
                    return false;
                }
                else
                {
                    m_last_audio_dts = pkt->dts;
                }
            }
            else
            {
               ffmpeg.av_packet_unref(pkt);
                return false;
            }
            return true;
        }
        #endregion

16、显示Usb摄像头时需要设置rtbufsize缓存大小,看其他人设置的1080p视频大小是 622080 ,有时会出现丢帧(因为是软解码)

dshow @ 000001d76c1ce000] real-time buffer [OBS Virtual Camera] [video input] too full or near too full (500% of size: 622080 [rtbufsize parameter])! frame dropped!

所以我把rtbufsize大小扩大了1000倍,没有出现丢包情况了

    ffmpeg.av_dict_set_int(&options, "rtbufsize", 622080000, 0);//1080P

17、连接rtsp这类流要实现秒连需要设置以下两个参数的值,经过测试连接延迟的最主要的影响是max_analyze_duration(值太小的话会有错误)

            ffmpeg.av_dict_set(&options, "buffer_size", "2048000", 0);
            ffmpeg.av_dict_set(&options, "max_analyze_duration", $"{ffmpeg.AV_TIME_BASE}/2", 0);//ffmpeg.AV_TIME_BASE的倍数

相关链接

FFmpeg 官网

ffmpeg.org/documentati…

github.com/Ruslan-B/FF…

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:莫如风

出处:cnblogs.com/mrf2233/p/17442871.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!