官方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 }