当前位置: 首页 > 工具软件 > AutoGen > 使用案例 >

FFMpeg.AutoGen(2)讲解官方example代码:编码

殳俊晤
2023-12-01

官方Example代码里编码的作业:把上一讲解码出来的jpg图片,编码成264格式的文件。

编码的流程,也是本文目录

1.把图片转换byte[]

2.把图片转换成yuv格式,封装成avframe。

3.编码。

先贴出编码的主函数。函数里分析下来,就是通过调用相关类和函数实现的目录三步。

  1         /// <summary>
  2         /// 编码 把解码出来的jpg文件,再编码成UV420P
  3         /// </summary>
  4         private static unsafe void EncodeImagesToH264()
  5         {
  6 
  7             //获取解码出来的文件队列
  8             var frameFiles = Directory.GetFiles(".", "frame.*.jpg").OrderBy(x => x).ToArray();
  9             //获取第一张帧图片
 10             var fistFrameImage = Image.FromFile(frameFiles.First());
 11 
 12             //设置导出媒体信息
 13             var outputFileName = "out.h264";
 14             var fps = 25;
 15             var sourceSize = fistFrameImage.Size;
 16             var sourcePixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24;
 17             var destinationSize = sourceSize;
 18             var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_YUV420P;
 19             //创建格式转换其 把rgb 转变成yuv ,同时对分辨率进行缩放
 20             using (var vfc = new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat))
 21             {
 22                 // be advise only ffmpeg based player (like ffplay or vlc) can play this file, for the others you need to go through muxing
 23                 //建议基于ffmpeg的播放器播放这个文件out.h264,否则需要多路复用技术
 24                 //这个文件就是用ffmpeg把rgb图片转变成264的一个个帧。
 25                 using (var fs = File.Open(outputFileName, FileMode.Create))
 26 
 27                 {
 28                     //创建264转换 把要保存的文件句柄fs 帧率fps 源大小destinationSize 传入
 29                     using (var vse = new H264VideoStreamEncoder(fs, fps, destinationSize))
 30                     {
 31                         var frameNumber = 0;
 32                         //读取每一张图片,作为一帧
 33                         foreach (var frameFile in frameFiles)
 34                         {
 35                             byte[] bitmapData;
 36 
 37                             using (var frameImage = Image.FromFile(frameFile))
 38                             using (var frameBitmap = frameImage is Bitmap bitmap ? bitmap : new Bitmap(frameImage))// is 后面接变量申明 这个写法比较有意思
 39                             {
 40                                 bitmapData = GetBitmapData(frameBitmap);
 41                             }
 42                             //固化pBitmapData内存地址
 43                             fixed (byte* pBitmapData = bitmapData)
 44                             {
 45                                 //指针数组用于保存指向帧实际内存空间的地址
 46                                 var data = new byte_ptrArray8 { [0] = pBitmapData };
 47                                 //每行大小
 48                                 var linesize = new int_array8 { [0] = bitmapData.Length / sourceSize.Height };
 49                                 var frame = new AVFrame
 50                                 {
 51                                     data = data,
 52                                     linesize = linesize,
 53                                     height = sourceSize.Height
 54                                 };
 55                                 //把rgb转换为yuv,同时对分辨率进行缩放
 56                                 var convertedFrame = vfc.Convert(frame);
 57                                 //设置时间戳 帧的序号 x 帧率
 58                                 convertedFrame.pts = frameNumber * fps;
 59                                 //把yuv420p编码成264,并写到 "out.h264"文件中
 60                                 vse.Encode(convertedFrame);
 61                             }
 62 
 63                             Console.WriteLine($"frame: {frameNumber}");
 64                             frameNumber++;
 65                         }
 66                     }
 67                 }
 68             }
 69         }
 70 

1.把图片转换byte[]

主要使用这个函数:bitmapData = GetBitmapData(frameBitmap); 函数简单代码很少,直接看代码和我的注释即可。

  1         /// <summary>
  2         /// 把bitmap转换为byte[]
  3         /// 从Scan0开始把每个像素字节返回成数组byte[]
  4         /// </summary>
  5         /// <param name="frameBitmap"></param>
  6         /// <returns></returns>
  7         private static byte[] GetBitmapData(Bitmap frameBitmap)
  8         {
  9             var bitmapData = frameBitmap.LockBits(new Rectangle(Point.Empty, frameBitmap.Size), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
 10             try
 11             {
 12                 //Stride像素实际占据字节长度
 13                 var length = bitmapData.Stride * bitmapData.Height;
 14                 var data = new byte[length];
 15                 //Scan0 放位图像素内存中的第一个地址
 16                 Marshal.Copy(bitmapData.Scan0, data, 0, length);
 17                 return data;
 18             }
 19             finally
 20             {
 21                 frameBitmap.UnlockBits(bitmapData);
 22             }
 23         }
 24     }

2.把图片转换成yuv格式,封装成avframe。

使用VideoFrameConverter类,进行图像格式转换从rgb转为yuv,此类在上一篇有具体描述这里不再阐述:

var sourcePixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24;

var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_YUV420P;

var vfc = new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat)

从上面代码看到,在申明和实例化时,告诉VideoFrameConverter对象源格式24bit rgb 需要转换为 yuv420p。同时指定了源和目的的尺寸,可以缩放。

并通过vfc.Convert(frame)转换成yuv格式。返回的数据是封装好的AVFrame格式。

3.编码

通过实现H264VideoStreamEncoder类实现编码264。编码的核心使用的是依旧是解码器AVCodecContext此时它应该被成为编码器。因为是编码,所以没有解码获取媒体信息获取有效流索引这些操作。建议阅读源码。文件末尾附笔者注释过的源码。

流程为

3.1.创建编码器

通过ffmpeg.avcodec_alloc_context3(_pCodec)创建AVCodecContext。这里的_pCodec是通过ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264)获取的。

3.2.配置编码器

配置AVCodecContext的width\height\time_base(时间戳基准,这里是1/fps ,时间基准的详细内容可看参考文档【2】)\pix_fmt(帧像素格式,也就是源格式)\设置参数preset 值为veryslow(264的参数,使用函数ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryslow", 0)设置)。

3.3.打开编码器

ffmpeg.avcodec_open2(_pCodecContext, _pCodec, null)。

4.轮询编码

实例外,循环调用Encode(AVFrame)进行编码。合计3步,编码2步,写入文件流1步:

1)放入编码器ffmpeg.avcodec_send_frame(_pCodecContext, &frame)

2)从编码器读取编码后的帧 通过参数返回 error = ffmpeg.avcodec_receive_packet(_pCodecContext, pPacket)。

3)把编码后的AVPacket包格式用UnmanagedMemoryStream写入文件流。

参考文档:

【1】FFmpeg X264的preset和tune 2017-05-25 Lerry.Zhao

【2】ffmpeg里time_base总结 2016-11-02 耕地

附件:

  1 using System;
  2 using System.Drawing;
  3 using System.IO;
  4 
  5 namespace FFmpeg.AutoGen.Example
  6 {
  7      /// <summary>
  8      /// H264转换类
  9      /// </summary>
 10     public sealed unsafe class H264VideoStreamEncoder : IDisposable
 11     {
 12         private readonly Size _frameSize;
 13         private readonly int _linesizeU;
 14         private readonly int _linesizeV;
 15         private readonly int _linesizeY;
 16         private readonly AVCodec* _pCodec;
 17         private readonly AVCodecContext* _pCodecContext;
 18         private readonly Stream _stream;
 19         private readonly int _uSize;
 20         private readonly int _ySize;
 21 
 22         /// <summary>
 23         /// 构造H264VideoStreamEncoder
 24         /// </summary>
 25         /// <param name="stream">转换源流</param>
 26         /// <param name="fps">帧率信息</param>
 27         /// <param name="frameSize">帧大小</param>
 28         public H264VideoStreamEncoder(Stream stream, int fps, Size frameSize)
 29         {
 30             _stream = stream;
 31             _frameSize = frameSize;
 32 
 33             var codecId = AVCodecID.AV_CODEC_ID_H264;
 34             _pCodec = ffmpeg.avcodec_find_encoder(codecId);
 35             if (_pCodec == null) throw new InvalidOperationException("Codec not found.");
 36             //根据解码器分配一个AVCodecContext ,仅仅分配工具,还没有初始化。
 37             _pCodecContext = ffmpeg.avcodec_alloc_context3(_pCodec);
 38             //配置解码器格式信息
 39             _pCodecContext->width = frameSize.Width;
 40             _pCodecContext->height = frameSize.Height;
 41             _pCodecContext->time_base = new AVRational {num = 1, den = fps};
 42             _pCodecContext->pix_fmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
 43             //设置参数preset 值为veryslow 配置264的参数 
 44             ffmpeg.av_opt_set(_pCodecContext->priv_data, "preset", "veryslow", 0);
 45             //打开编码器
 46             ffmpeg.avcodec_open2(_pCodecContext, _pCodec, null).ThrowExceptionIfError();
 47             //每一行yuv的大小 每个像素的y值是记录的。相邻两行的各两个像素共享一个UV
 48             _linesizeY = frameSize.Width;
 49             _linesizeU = frameSize.Width / 2;
 50             _linesizeV = frameSize.Width / 2;
 51             //y的大小就是像素的数量 uv只有像素的1/4 四个像素共享一个uv
 52             _ySize = _linesizeY * frameSize.Height;
 53             _uSize = _linesizeU * frameSize.Height / 2;
 54         }
 55 
 56         public void Dispose()
 57         {
 58             ffmpeg.avcodec_close(_pCodecContext);
 59             ffmpeg.av_free(_pCodecContext);
 60             ffmpeg.av_free(_pCodec);
 61         }
 62 
 63         /// <summary>
 64         /// 编码成264格式
 65         /// </summary>
 66         /// <param name="frame">源帧</param>
 67         public void Encode(AVFrame frame)
 68         {
 69             if (frame.format != (int) _pCodecContext->pix_fmt) throw new ArgumentException("Invalid pixel format.", nameof(frame));
 70             if (frame.width != _frameSize.Width) throw new ArgumentException("Invalid width.", nameof(frame));
 71             if (frame.height != _frameSize.Height) throw new ArgumentException("Invalid height.", nameof(frame));
 72             if (frame.linesize[0] != _linesizeY) throw new ArgumentException("Invalid Y linesize.", nameof(frame));
 73             if (frame.linesize[1] != _linesizeU) throw new ArgumentException("Invalid U linesize.", nameof(frame));
 74             if (frame.linesize[2] != _linesizeV) throw new ArgumentException("Invalid V linesize.", nameof(frame));
 75             if (frame.data[1] - frame.data[0] != _ySize) throw new ArgumentException("Invalid Y data size.", nameof(frame));
 76             if (frame.data[2] - frame.data[1] != _uSize) throw new ArgumentException("Invalid U data size.", nameof(frame));
 77 
 78             //创建AVPacket包
 79             var pPacket = ffmpeg.av_packet_alloc();
 80             try
 81             {
 82                 int error;
 83                 do
 84                 {
 85                     //把帧放入解码器
 86                     ffmpeg.avcodec_send_frame(_pCodecContext, &frame).ThrowExceptionIfError();
 87                     //从解码器里读取帧,放到pPacket包里
 88                     error = ffmpeg.avcodec_receive_packet(_pCodecContext, pPacket);
 89                 } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
 90 
 91                 error.ThrowExceptionIfError();
 92                 //UnmanagedMemoryStream 类提供从托管代码访问非托管内存块的能
 93                 //把包里的数据写入_stream(构造函数传入)
 94                 using (var packetStream = new UnmanagedMemoryStream(pPacket->data, pPacket->size)) packetStream.CopyTo(_stream);
 95             }
 96             finally
 97             {
 98                 ffmpeg.av_packet_unref(pPacket);
 99             }
100         }
101     }
102 }
View Code
 类似资料: