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 = https://www.cnblogs.com/chifan/archive/2022/07/21/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)
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/500041.html
標籤:.NET技术
