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