FFMpeg是一套C编译的开源工具集。主要用于视频处理,可以编解码视频,建立流媒体服务器等等。官方网站:http://ffmpeg.org/
FFMpeg.AutoGen封装方法以方便C#调用FFmpeg。项目地址:https://github.com/Ruslan-B/FFmpeg.AutoGen。可以使用NuGet安装。
AutoGen只是封装调用FFmpeg,程序还是需要下在FFmpeg工具放在程序目录里,且版本要对应。 笔者用FFMpeg.AutoGetn的官方example代码介绍一下FFMpege如何使用(源代码在其github库里)。
example是一个命令行程序,mian函数里面的代码如下。我将通过此函数调用顺序介绍ffmpeg.AutoGet的用法。
目录:
1.注册FFmpeg库。实际就将ffmpeg库的地址告诉autogen
2.ffmpeg 一些调用其的配置(可选)
2.1 配置日志输出
2.2配置硬件解码器ffmpeg是支持硬解的.具体支持类型可以参考ffmpeg官方文档。转载网友摘录的ffmpeg支持硬解编码的枚举。
3.解码函数DecodeAllFramesToImages
3.1 VideoStreamDecoder类
3.2 VideoFrameConverter类
3.3 相关数据结构AVPacket,AVFrame
本文使用ffmpeg.autogen版本4.2.2,对应ffmpeg版本也是4.2.2。
1 private static void Main(string[] args) 2 { 3 Console.WriteLine("Current directory: " + Environment.CurrentDirectory); 4 Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32"); 5 6 FFmpegBinariesHelper.RegisterFFmpegBinaries(); 7 8 Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}"); 9 10 //配置ffmpeg输出日志 11 SetupLogging(); 12 //配置硬件解码器 13 ConfigureHWDecoder(out var deviceType); 14 15 //解码 16 Console.WriteLine("Decoding..."); 17 DecodeAllFramesToImages(deviceType); 18 19 //编码 20 Console.WriteLine("Encoding..."); 21 EncodeImagesToH264(); 22 }
1.注册FFmpeg库。实际就将ffmpeg库的地址告诉autogen
1 FFmpegBinariesHelper.RegisterFFmpegBinaries();注册FFmpeg,这里的FFmpegBinariesHelper类需要在程序里重写。我这里摘抄官方demo的代码
1 namespace FFmpeg.AutoGen.Example 2 { 3 public class FFmpegBinariesHelper 4 { 5 internal static void RegisterFFmpegBinaries() 6 { 7 var current = Environment.CurrentDirectory; 8 var probe = Path.Combine("FFmpeg", "bin", Environment.Is64BitProcess ? "x64" : "x86"); 9 while (current != null) 10 { 11 var ffmpegBinaryPath = Path.Combine(current, probe); 12 if (Directory.Exists(ffmpegBinaryPath)) 13 { 14 Console.WriteLine($"FFmpeg binaries found in: {ffmpegBinaryPath}"); 15 ffmpeg.RootPath = ffmpegBinaryPath; 16 return; 17 } 18 19 current = Directory.GetParent(current)?.FullName; 20 } 21 } 22 } 23 }
代码的功能就是寻找ffmpeg的路径。
核心代码:
1 ffmpeg.RootPath = ffmpegBinaryPath;2.ffmpeg 一些调用其的配置(可选)
2.1 配置日志输出
1 /// <summary> 2 /// 配置日志 3 /// </summary> 4 private static unsafe void SetupLogging() 5 { 6 ffmpeg.av_log_set_level(ffmpeg.AV_LOG_VERBOSE); 7 8 // do not convert to local function 9 av_log_set_callback_callback logCallback = (p0, level, format, vl) => 10 { 11 if (level > ffmpeg.av_log_get_level()) return; 12 13 var lineSize = 1024; 14 var lineBuffer = stackalloc byte[lineSize]; 15 var printPrefix = 1; 16 ffmpeg.av_log_format_line(p0, level, format, vl, lineBuffer, lineSize, &printPrefix); 17 var line = Marshal.PtrToStringAnsi((IntPtr) lineBuffer); 18 Console.ForegroundColor = ConsoleColor.Yellow; 19 Console.Write(line); 20 Console.ResetColor(); 21 }; 22 23 ffmpeg.av_log_set_callback(logCallback); 24 }
主要就是配置日志回调。
核心代码:
1 ffmpeg.av_log_set_callback(logCallback)2.2配置硬件解码器ffmpeg是支持硬解的.具体支持类型可以参考ffmpeg官方文档。转载网友摘录的ffmpeg支持硬解编码的枚举。
1 enum AVHWDeviceType { 2 AV_HWDEVICE_TYPE_NONE, 3 AV_HWDEVICE_TYPE_VDPAU, 4 AV_HWDEVICE_TYPE_CUDA, 5 AV_HWDEVICE_TYPE_VAAPI, 6 AV_HWDEVICE_TYPE_DXVA2, 7 AV_HWDEVICE_TYPE_QSV, 8 AV_HWDEVICE_TYPE_VIDEOTOOLBOX, 9 AV_HWDEVICE_TYPE_D3D11VA, 10 AV_HWDEVICE_TYPE_DRM, 11 AV_HWDEVICE_TYPE_OPENCL, 12 AV_HWDEVICE_TYPE_MEDIACODEC, 13 };
1 /// <summary> 2 /// 配置硬件解码器 3 /// </summary> 4 /// <param name="HWtype"></param> 5 private static void ConfigureHWDecoder(out AVHWDeviceType HWtype) 6 { 7 HWtype = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE; 8 Console.WriteLine("Use hardware acceleration for decoding?[n]"); 9 var key = Console.ReadLine(); 10 var availableHWDecoders = new Dictionary<int, AVHWDeviceType>(); 11 if (key == "y") 12 { 13 Console.WriteLine("Select hardware decoder:"); 14 var type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE; 15 var number = 0; 16 while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE) 17 { 18 Console.WriteLine($"{++number}. {type}"); 19 availableHWDecoders.Add(number, type); 20 } 21 if (availableHWDecoders.Count == 0) 22 { 23 Console.WriteLine("Your system have no hardware decoders."); 24 HWtype = 。; 25 return; 26 } 27 int decoderNumber = availableHWDecoders.SingleOrDefault(t => t.Value == AVHWDeviceType.AV_HWDEVICE_TYPE_DXVA2).Key; 28 if (decoderNumber == 0) 29 decoderNumber = availableHWDecoders.First().Key; 30 Console.WriteLine($"Selected [{decoderNumber}]"); 31 int.TryParse(Console.ReadLine(),out var inputDecoderNumber); 32 availableHWDecoders.TryGetValue(inputDecoderNumber == 0 ? decoderNumber: inputDecoderNumber, out HWtype); 33 } 34 } 35
核心代码:ffmpeg.av_hwdevice_iterate_types(type)获得系统支持的硬件解码。
ffmpeg.av_hwdevice_iterate_types(type)根据传入的硬件解码其类型,返回AVHWDeviceType枚举里下一个系统支持的硬件解码器类型。
3.Example里的解码函数DecodeAllFramesToImages
1 /// <summary> 2 /// 解码 3 /// </summary> 4 /// <param name="HWDevice"></param> 5 private static unsafe void DecodeAllFramesToImages(AVHWDeviceType HWDevice) 6 { 7 // decode all frames from url, please not it might local resorce, e.g. string url = "../../sample_mpeg4.mp4"; 8 var url = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"; // be advised this file holds 1440 frames 9 using (var vsd = new VideoStreamDecoder(url,HWDevice)) 10 { 11 Console.WriteLine($"codec name: {vsd.CodecName}"); 12 13 var info = vsd.GetContextInfo(); 14 info.ToList().ForEach(x => Console.WriteLine($"{x.Key} = {x.Value}")); 15 16 var sourceSize = vsd.FrameSize; 17 var sourcePixelFormat = HWDevice == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE ? vsd.PixelFormat : GetHWPixelFormat(HWDevice); 18 var destinationSize = sourceSize; 19 var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24; 20 using (var vfc = new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat)) 21 { 22 var frameNumber = 0; 23 while (vsd.TryDecodeNextFrame(out var frame)) 24 { 25 var convertedFrame = vfc.Convert(frame); 26 27 using (var bitmap = new Bitmap(convertedFrame.width, convertedFrame.height, convertedFrame.linesize[0], PixelFormat.Format24bppRgb, (IntPtr) convertedFrame.data[0])) 28 bitmap.Save($"frame.{frameNumber:D8}.jpg", ImageFormat.Jpeg); 29 30 Console.WriteLine($"frame: {frameNumber}"); 31 frameNumber++; 32 } 33 } 34 } 35 }
example源代码里解码主要使用VideoStreamDecoder和VideoFrameConverter两个类。这两个类不是FFMpeg.AutoGen里的类型,而是example代码里。也就是说解码工作是需要用户自己封装解码类。图省事可以直接照搬example里的代码。笔者很推荐读一下这两个类的源代码(可以在文档末尾查附件看注释过的这两个类),可以搞清楚ffmpeg的解码流程。
3.1 example里的VideoStreamDecoder类
VideoStreamDecoder作用:通过配置解码器获取实际有用的帧数据,大概的流程是:
- 打开流并参数返回格式上下文AVFormatContext (avformat_open_input(&pFormatContext, url, null, null))
- 获取媒体信息数据存到AVFormatContext 格式上下文(avformat_find_stream_info(_pFormatContext, null))
- 据根AVFormatContext和媒体信息(AVMediaType.AVMEDIA_TYPE_VIDEO )找到最佳匹配的流索引并参数返回解码器AVCodec(av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO , -1, -1 , &codec, 0))
- 根据AVCodec分配解码器上下文AVCodecContext(_pCodecContext=avcodec_alloc_context3(codec),如果指定硬解还要配置AVCodecContext里硬件解码器hw_device_ctx)
- 配置解码器上下文格式参数,avcodec_parameters_to_context(根据_pFormatContext->streams[_streamIndex]->codecpar)
- 根据解码器codec初始化解码器上下文 avcodec_open2(_pCodecContext, codec, null)
- 轮询帧:把未解码帧包(AVPacket)放入解码器(avcodec_send_packet)从解码器里获取解码的帧(AVFrame)(avcodec_receive_frame)
3.2example里的VideoFrameConverter类
VideoFrameConverter作用:对帧数据进行规格形状转换,格式转换的大概流程
- 创建帧格式转换器SwsContext(ffmpeg.sws_getContext,可以指定转换器的算法,具体可以看参考文档【6】)
- 计算转换过程中需要的缓存
- 创建缓存:创建缓存指针(ref _dstData, ref _dstLinesize)——创建缓存实际内存——两者关联(av_image_fill_arrays),具体可以看参考文档【7】【8】
- 轮询转换:实际上就是调用sws_scale,最终返回一个转换好的AVFrame
3.3.相关数据结构AVPacket,AVFrame
其中有两个概念包和帧需要注意一下,这里转载灰色飘零博客里描述(参考文档【5】):
AVPacket
用于存储压缩的数据,分别包括有音频压缩数据,视频压缩数据和字幕压缩数据。它通常在解复用操作后存储压缩数据,然后作为输入传给解码器。或者由编码器输出然后传递给复用器。对于视频压缩数据,一个AVPacket通常包括一个视频帧。对于音频压缩数据,可能包括几个压缩的音频帧。
AVFrame
用于存储解码后的音频或者视频数据。AVFrame必须通过av_frame_alloc进行分配,通过av_frame_free释放。
两者之间的关系
av_read_frame得到压缩的数据包AVPacket,一般有三种压缩的数据包(视频、音频和字幕),都用AVPacket表示。
然后调用avcodec_send_packet 和 avcodec_receive_frame对AVPacket进行解码得到AVFrame。
注:从 FFmpeg 3.x 开始,avcodec_decode_video2 就被废弃了,取而代之的是 avcodec_send_packet 和 avcodec_receive_frame。
参考文档:
【2】FFmpeg开发之PacketQueue中AVPacket和AVFrame关系
【5】FFMPEG-数据结构解释(AVCodecContext,AVStream,AVFormatContext)
【6】ffmpeg中的sws_scale算法性能测试 一片云雾 2011-10-29
【7】av_image_fill_arrays详解 韭菜大葱馅鸡蛋 2019-12-14
【8】FFmpeg av_image_fill_arrays填充AVFrame数据缓冲 fengyuzaitu 2019-11-12
附件1 Example中unsafe void DecodeAllFramesToImages(AVHWDeviceType HWDevice)解码函数源码及注释
1 /// <summary> 2 /// 解码 3 /// </summary> 4 /// <param name="HWDevice">硬件解码类型</param> 5 private static unsafe void DecodeAllFramesToImages(AVHWDeviceType HWDevice) 6 { 7 // decode all frames from url, please not it might local resorce, e.g. string url = "../../sample_mpeg4.mp4"; 8 var url = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"; // be advised this file holds 1440 frames 9 10 //使用自行编写的视频解码器类进行解码 11 using (var vsd = new VideoStreamDecoder(url,HWDevice)) 12 { 13 Console.WriteLine($"codec name: {vsd.CodecName}"); 14 15 //获取媒体信息 16 var info = vsd.GetContextInfo(); 17 info.ToList().ForEach(x => Console.WriteLine($"{x.Key} = {x.Value}")); 18 19 var sourceSize = vsd.FrameSize; 20 //资源编码格式 21 var sourcePixelFormat = HWDevice == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE ? vsd.PixelFormat : GetHWPixelFormat(HWDevice); 22 //目标尺寸与原尺寸一致 23 var destinationSize = sourceSize; 24 //目标媒体格式是bit类型 25 var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24; 26 //帧格式转换 27 using (var vfc = new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat)) 28 { 29 var frameNumber = 0; 30 while (vsd.TryDecodeNextFrame(out var frame)) 31 { 32 var convertedFrame = vfc.Convert(frame); 33 34 using (var bitmap = new Bitmap(convertedFrame.width, convertedFrame.height, convertedFrame.linesize[0], PixelFormat.Format24bppRgb, (IntPtr) convertedFrame.data[0])) 35 bitmap.Save($"frame.{frameNumber:D8}.jpg", ImageFormat.Jpeg); 36 37 Console.WriteLine($"frame: {frameNumber}"); 38 frameNumber++; 39 } 40 } 41 } 42 } 43