前言 .NET在跨平臺後對於應用的部署而言,不在像.NET Framework的時候那麼單一化了,一個.NET Core應用的部署工作就可以涉及到很多知識點。 就對於windows而言,我們可以選擇使用IIS和Kestrel作為我們的Web伺服器。既可以把網站的部署用“進程內托管”方式插入IIS管道 ...
WinUI3的Window App Sdk,雖然已經更新到1.12了但是依然沒有MediaPlayerElement控制項,最近在學習FFmpeg,所以寫一下文章記錄一下。由於是我剛剛開始學習FFmpeg 的使用,所以現在只能做到播放視頻,播放音頻並沒有做好,所以這遍文章先展示一下播放視頻的流程。效果圖如下。
一、準備工作
1.在NeGet上引入 FFmpeg.autogen庫;
2.下載已經編譯好ffmpeg dll文件 下載地址:(需要下載對應FFmpeg.autogen的版本)https://github.com/BtbN/FFmpeg-Builds/releases?page=2,下載好後解壓文件提取裡面的dll文件,併在項目中新建目錄並改名為FFmpe下麵為目錄結構。並將所有ffmpeg的dll文件屬性 複製到輸出目錄改為 “始終複製”或者“如果較新則複製” 選項
3.新建一個類,並改名為 FFmpegHelper.寫一個註冊庫文件的方法,這個方法的主要功能就是告訴ffmpeg,我們所用的dll文件放置在哪裡,ffmpeg會自動去註冊這些dll的;
public static class FFmpegHelper { public static void RegisterFFmpegBinaries() { //獲取當前軟體啟動的位置 var currentFolder = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; //ffmpeg在項目中放置的位置 var probe = Path.Combine("FFmpeg", "bin", Environment.Is64BitOperatingSystem ? "x64" : "x86"); while (currentFolder != null) { var ffmpegBinaryPath = Path.Combine(currentFolder, probe); if (Directory.Exists(ffmpegBinaryPath)) { //找到dll放置的目錄,並賦值給rootPath; ffmpeg.RootPath = ffmpegBinaryPath; return; } currentFolder = Directory.GetParent(currentFolder)?.FullName; } //舊版本需要要調用這個方法來註冊dll文件,新版本已經會自動註冊了 //ffmpeg.avdevice_register_all(); } }
2).在軟體啟動時調用 RegisterFFmpegBinaries函數註冊dll文件;(在 App.Xaml.cs的OnLaunched上添加 FFmpegHelper.RegisterFFmpegBinaries()函數)
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { m_window = new MainWindow(); m_window.Activate(); FFmpegHelper.RegisterFFmpegBinaries(); }
二.解碼流程
1.在開始解碼前我們先將需要用到的解碼結構都聲明;這些結構都是在整個解碼過程我們需要操作的指針。
//媒體格式上下文(媒體容器) AVFormatContext* format; //編解碼上下文 AVCodecContext* codecContext; //媒體數據包 AVPacket* packet; //媒體幀數據 AVFrame* frame; //圖像轉換器 SwsContext* convert; //視頻流 AVStream* videoStream; // 視頻流在媒體容器上流的索引 int videoStreamIndex;
2.InitDecodecVideo() 初始化解碼器函數 .
void InitDecodecVideo(string path) { int error = 0; //創建一個 媒體格式上下文 format = ffmpeg.avformat_alloc_context(); if (format == null) { Debug.WriteLine("創建媒體格式(容器)失敗"); return; } var tempFormat = format; //打開視頻 error = ffmpeg.avformat_open_input(&tempFormat, path, null, null); if (error < 0) { Debug.WriteLine("打開視頻失敗"); return; } //獲取流信息 ffmpeg.avformat_find_stream_info(format, null); //編解碼器類型 AVCodec* codec = null; //獲取視頻流索引 videoStreamIndex = ffmpeg.av_find_best_stream(format, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (videoStreamIndex < 0) { Debug.WriteLine("沒有找到視頻流"); return; } //根據流索引找到視頻流 videoStream = format->streams[videoStreamIndex]; //創建解碼器上下文 codecContext = ffmpeg.avcodec_alloc_context3(codec); //將視頻流裡面的解碼器參數設置到 解碼器上下文中 error = ffmpeg.avcodec_parameters_to_context(codecContext, videoStream->codecpar); if (error < 0) { Debug.WriteLine("設置解碼器參數失敗"); return; } //打開解碼器 error = ffmpeg.avcodec_open2(codecContext, codec, null); if (error < 0) { Debug.WriteLine("打開解碼器失敗"); return; } //視頻時長等視頻信息 //Duration = TimeSpan.FromMilliseconds(videoStream->duration / ffmpeg.av_q2d(videoStream->time_base)); Duration = TimeSpan.FromMilliseconds(format->duration / 1000); CodecId = videoStream->codecpar->codec_id.ToString(); CodecName = ffmpeg.avcodec_get_name(videoStream->codecpar->codec_id); Bitrate = (int)videoStream->codecpar->bit_rate; FrameRate = ffmpeg.av_q2d(videoStream->r_frame_rate); FrameWidth = videoStream->codecpar->width; FrameHeight = videoStream->codecpar->height; frameDuration = TimeSpan.FromMilliseconds(1000 / FrameRate); //初始化轉換器,將圖片從源格式 轉換成 BGR0 (8:8:8)格式 var result = InitConvert(FrameWidth, FrameHeight, codecContext->pix_fmt, FrameWidth, FrameHeight, AVPixelFormat.AV_PIX_FMT_BGR0); //所有內容都初始化成功了開啟時鐘,用來記錄時間 if (result) { //從記憶體中分配控制項給 packet 和frame packet = ffmpeg.av_packet_alloc(); frame = ffmpeg.av_frame_alloc(); clock.Start(); DisaplayVidwoInfo(); } }
在初始解碼過程中,我們也是可以拿到視頻裡面所包含的信息,比如 解碼器類型,比特率,幀率,視頻的款高度,還有視頻時長等信息。在配置完解碼信息後也能從代碼中看到了調用 InitConvert() 初始化轉碼器的函數,這裡我將最後一個參數設置了為 AVPixelFormat.AV_PIX_FMT_BGR0,這裡會到後面的創建 CanvasBitmap 點陣圖的格式對應。
3.InitConvert() 函數中創建了一個將讀取的幀數據轉換成指定圖像格式的 SwsContext 對象;
bool InitConvert(int sourceWidth, int sourceHeight, AVPixelFormat sourceFormat, int targetWidth, int targetHeight, AVPixelFormat targetFormat) { //根據輸入參數和輸出參數初始化轉換器 convert = ffmpeg.sws_getContext(sourceWidth, sourceHeight, sourceFormat, targetWidth, targetHeight, targetFormat, ffmpeg.SWS_FAST_BILINEAR, null, null, null); if (convert == null) { Debug.WriteLine("創建轉換器失敗"); return false; } //獲取轉換後圖像的 緩衝區大小 var bufferSize = ffmpeg.av_image_get_buffer_size(targetFormat, targetWidth, targetHeight, 1); //創建一個指針 FrameBufferPtr = Marshal.AllocHGlobal(bufferSize); TargetData = new byte_ptrArray4(); TargetLinesize = new int_array4(); ffmpeg.av_image_fill_arrays(ref TargetData, ref TargetLinesize, (byte*)FrameBufferPtr, targetFormat, targetWidth, targetHeight, 1); return true; }
4.TreadNextFrame()讀取下一幀數據,在讀取到 數據包的時候需要判斷一下是不是視頻幀,因為在一個“媒體容器”裡面會包含 視頻,音頻,字母,額外數據等信息的;
bool TryReadNextFrame(out AVFrame outFrame) { lock (SyncLock) { int result = -1; //清理上一幀的數據 ffmpeg.av_frame_unref(frame); while (true) { //清理上一幀的數據包 ffmpeg.av_packet_unref(packet); //讀取下一幀,返回一個int 查看讀取數據包的狀態 result = ffmpeg.av_read_frame(format, packet); //讀取了最後一幀了,沒有數據了,退出讀取幀 if (result == ffmpeg.AVERROR_EOF || result < 0) { outFrame = *frame; return false; } //判斷讀取的幀數據是否是視頻數據,不是則繼續讀取 if (packet->stream_index != videoStreamIndex) continue; //將包數據發送給解碼器解碼 ffmpeg.avcodec_send_packet(codecContext, packet); //從解碼器中接收解碼後的幀 result = ffmpeg.avcodec_receive_frame(codecContext, frame); if (result < 0) continue; outFrame = *frame; return true; } } }
5.FrameConvertBytes() 將讀取到的幀通過轉換器將數據轉換成 byte[] ;
byte[] FrameConvertBytes(AVFrame* sourceFrame) { // 利用轉換器將yuv 圖像數據轉換成指定的格式數據 ffmpeg.sws_scale(convert, sourceFrame->data, sourceFrame->linesize, 0, sourceFrame->height, TargetData, TargetLinesize); var data = new byte_ptrArray8(); data.UpdateFrom(TargetData); var linesize = new int_array8(); linesize.UpdateFrom(TargetLinesize); //創建一個位元組數據,將轉換後的數據從記憶體中讀取成位元組數組 byte[] bytes = new byte[FrameWidth * FrameHeight * 4]; Marshal.Copy((IntPtr)data[0], bytes, 0, bytes.Length); return bytes; }
6.創建一個新的任務線程,通過一個while迴圈來讀取幀數據,並轉換成 byte[] 以便於創建 CannvasBitmap 點陣圖對象繪製到屏幕上;
PlayTask = new Task(() => { while (true) { lock (SyncLock) { //播放中 if (Playing) { if (clock.Elapsed > Duration) StopPlay(); if (lastTime == TimeSpan.Zero) { lastTime = clock.Elapsed; isNextFrame = true; } else { if (clock.Elapsed - lastTime >= frameDuration) { lastTime = clock.Elapsed; isNextFrame = true; } else isNextFrame = false; } if (isNextFrame) { if (TryReadNextFrame(out var frame)) { var bytes = FrameConvertBytes(&frame); bitmap = CanvasBitmap.CreateFromBytes(CanvasDevice.GetSharedDevice(), bytes, FrameWidth, FrameHeight, DirectXPixelFormat.B8G8R8A8UIntNormalized); canvas.Invalidate(); } } } } } }); PlayTask.Start();
三、通過上面的幾個步驟我們就可以從 打開一個媒體文件-》初始化解碼流程-》讀取幀數據-》繪製到屏幕,來完整的播放一個視頻了。下一篇文章我將展示如何通過進度條來進行視頻從哪裡開始播放;
項目Demo地址:FFmpegDecodecVideo · 吃飯訓覺/LearnFFmppeg - 碼雲 - 開源中國 (gitee.com)