WinUI3 FFmpeg.autogen解析視頻幀,使用win2d顯示內容.

来源:https://www.cnblogs.com/chifan/archive/2022/07/21/16504109.html
-Advertisement-
Play Games

前言 .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)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 5 如何合理使用索引加速 tips: 500萬條建表sql參照網盤sql腳本 [root@linux-141 bin]# ./mysql -u root -p itcast < product_list-5072825.sql 索引是資料庫優化最常用也是最重要的手段之一, 通過索引通常可以幫助用戶解 ...
  • phpstorm2022是一款非常好用的php開發軟體,軟體支持所有PHP語言功能,提供最優秀的代碼補全、重構、實時錯誤預防等等功能,能夠為程式員提供更為效率的php開發,新版本改進了phpstorm軟體的自動完成功能。還增加了代碼清理工具,可以刪除不必要的部分來優化全類名稱,從而更好的提高用戶的工 ...
  • Mybatis Generator 使用xml配置文件形式自動生成 只生成實體類、mapper介面及mapper.xml。並且包含豐富的內容 首先添加mybatis依賴和相關插件 <!-- 依賴MyBatis核心包 --> <dependencies> <dependency> <groupId>o ...
  • 技術群里有人發了一段代碼: 附言:兄弟們,這個單例怎麼樣? 我回覆:什麼鬼,看不懂啊?! 也有其他小伙伴表示看不懂,看來大家的C#基礎和我一樣並不全面。 我看不懂,主要是因為我沒用過TaskCompletionSource和Interlocked的CompareExchange方法,然後經過我1、2 ...
  • 1.Xshell遠程登錄Linux系統 在實際的項目部署工作中,遠程登錄到伺服器上是繞不開的彎。本文遠程登錄Linux系統選用工具的是目前最常用、最好用的Xshell。Xsheel是一個強大的安全終端模擬軟體,它支持SSH1、SSH2以及Windows系統的Telnet協議。它的運行速度流程並且完美 ...
  • 1.保障應用程式埠的連通性 通常情況下伺服器的防火牆通常都是開啟的狀態,所以我們需要保證我們部署應用程式的埠是開啟了相應的訪問許可權,否則我們的應用程式將無法被外界進行訪問。這裡為了快速測試應用程式的埠連通性,我們使用比較方便的Telnet工具進行測試,該工具的安裝包內置在Windows操作系統 ...
  • 一:背景 1. 講故事 前段時間有位朋友說他的程式 CPU 出現了暴漲現象,由於程式是買來的,所以問題就比較棘手了,那既然找到我,就想辦法幫朋友找出來吧,分析下來,問題比較經典,有必要和大家做一下分享。 二:WinDbg 分析 1. CPU 真的爆高嗎 一直關註這個系列的朋友應該知道,用 !tp 驗 ...
  • “don't worry”,部署ASP.NET Core應用可以和原來部署.NET Framework的ASP.NET應用一樣的簡單,還是“熟悉的配方,熟悉的味道”,甚至提供了更加便捷的Kestrel部署方式,下麵主要介紹在windows平臺下兩種常用部署方式: 方式一:Kestrel部署Web應用 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...