FFMpeg是一套C编译的开源工具集。主要用于视频处理,可以编解码视频,建立流媒体服务器等等。官方网站:http://ffmpeg.org/
FFMpeg.AutoGen封装方法以方便C#调用FFmpeg。项目地址:https://github.com/Ruslan-B/FFmpeg.AutoGen。可以使用NuGet安装。
本文使用ffmpeg.autogen版本4.2.2,对应ffmpeg版本也是4.2.2。
AutoGen只是封装调用FFmpeg,程序还是需要下在FFmpeg工具放在程序目录里,且版本要对应。 笔者用FFMpeg.AutoGetn的官方example代码介绍一下FFMpege如何使用(源代码在其github库里)。example是一个命令行程序,mian函数里面的代码如下。我将通过此函数调用顺序介绍ffmpeg.AutoGet的用法。
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.解码
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的解码流程。大概的流程是:
1 初始化AVFormatContext(音视频信息数据)——据根据源找到流——根据流配置视频格式——根据流生成解码器codecContext(解码器信息)——
其中有两个概念包和帧需要注意一下,这里转载灰色飘零博客里描述:
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。
参考文档: