官方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的widthheight ime_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 }