jjzjj

WinUI3 FFmpeg.autogen解析视频帧,使用win2d显示内容.

吃饭/睡觉 2023-03-28 原文

  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();

三、通过上面的几个步骤我们就可以从 打开一个媒体文件-》初始化解码流程-》读取帧数据-》绘制到屏幕,来完整的播放一个视频了。下一篇文章我将展示如何通过进度条来进行视频从哪里开始播放;

WinUI3 FFmpeg.autogen 播放视频,实现播放,暂停,停止,进度条设置播放时间。 - 吃饭/睡觉 - 博客园 (cnblogs.com)

Winui3 FFmpeg.autogen 解析音频,使用NAudio播放; - 吃饭/睡觉 - 博客园 (cnblogs.com)

项目Demo地址:FFmpegDecodecVideo · 吃饭训觉/LearnFFmppeg - 码云 - 开源中国 (gitee.com)

有关WinUI3 FFmpeg.autogen解析视频帧,使用win2d显示内容.的更多相关文章

  1. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  2. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

    我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

  3. u盘安装系统(win10为例) - 2

    下载微PE工具箱进入官网下载微PE工具箱-下载 安装好后,打开微PE工具箱客户端,选择安装PE到U盘 PE壁纸可选择自己喜欢的壁纸,勾选上包含DOS工具箱,个性化盘符图标 下载原版系统进入网站下载镜像NEXT,ITELLYOU如果没有账号,注册一下就好进入选择开始使用选择win10 这里我们选择消费者版,用迅雷把BT种子下载下来 下面的两个盘符,是PE工具箱安装进U盘后,分成的盘符,注意EFI的盘符,这里面不能删东西,也不能添东西,另一个盘符可以当做正常的U盘空间使用,我们现在需要把下载下来的景象文件复制到正常的U盘空间中去 这个时候我们的系统U盘就只做好了 安装系统我们将U盘插入电脑,开机,

  4. Win10 / 11新电脑最简单跳过联网激活和使用本地账户登录方法 - 2

    跳过联网激活:OOBE界面直接按Ctrl+Shift+F3进入审核模式。这样就可以直接进入系统进行一些硬件测试等,而不用联网激活导致新机无法退货。需要注意的是,在审核模式下进行的一些操作都会保留,并不会在退出后自动还原!安装的软件在正常开机进系统后还会看见!如果电脑确实没连互联网又不想强行跳过OOBE(网上很多教程会叫你直接结束OOBE进程,但这是不推荐的,因为一些厂商自带优化程序和系统初始化设置在后面都会应用,对于笔记本跳过的话你会发现驱动和内置应用都没有装上。其实这部分脚本就在系统盘的Recovery隐藏文件夹下),可以参考以下方式:https://www.landiannews.com/

  5. ruby - 安装gem : Couldn't reserve space for cygwin's heap, Win32错误487错误 - 2

    我正在尝试在我的机器上安装win32-apigem,但在构建native扩展时我遇到了一些问题:$geminstallwin32-api--no-ri--rdocTemporarilyenhancingPATHtoincludeDevKit...Buildingnativeextensions.Thiscouldtakeawhile...C:\Programs\dev_kit\bin\make.exe:***Couldn'treservespaceforcygwin'sheap,Win32error0ERROR:Errorinstallingwin32-api:ERROR:Failed

  6. ruby - 如何使用 bash 命令或 Ruby 使用 ffmpeg 将 mp4 文件批量转换为 ogg - 2

    我运行的是OSX,对视频转换一无所知。但我有大约200个视频都是mp4格式,无法在Firefox中播放。我需要将它们转换为ogg才能使用html5视频标签。这些文件位于一个文件夹结构中,这使得一次一个地处理一个文件变得困难。我希望bash命令或Ruby命令遍历所有子文件夹并找到所有.mp4并转换它们。我找到了一份关于如何使用Google执行此操作的引用资料:http://athmasagar.wordpress.com/2011/05/12/a-bash-script-to-convert-mp4-files-to-oggogv/#!/bin/bashforfin$(ls*mp4|se

  7. Ruby 1.9 - 没有这样的文件可以加载 'win32/open3' - 2

    我在Windows上运行ruby​​1.9.2并试图移植在Ruby1.8中工作的代码。该代码使用以前运行良好的Open4.popen4。对于1.9.2,我做了以下事情:通过geminstallPOpen4安装了POpen4需要POpen4通过require'popen4'尝试像这样使用POpen4:Open4.popen4("cmd"){|io_in,io_out,io_er|...}当我这样做时,我得到了错误:nosuchfiletoload--win32/open3如果我尝试安装win32-open3(geminstallwin32-open3),我会收到错误消息:win32-op

  8. Java调用ffmpeg处理视频,并记录下遇到的坑 - 2

    目录需求基于JavaCV跨平台执行ffmpeg命令[^1]坑一内存不足坑二多个ffmpeg进程并行导致IO负载大,进而导致ioerror?坑三使用Java操作ffmpeg时,有时会卡死坑四Process的waitFor死锁问题及解决办法需求给透明背景的视频自动叠加一张背景图片基于JavaCV跨平台执行ffmpeg命令1我测试发现的本需求的最小依赖:dependency>groupId>org.bytedecogroupId>artifactId>ffmpeg-platform-gplartifactId>version>5.0-1.5.7version>dependency>核心代码:Stri

  9. 基于ffmpeg的视频处理与MPEG的压缩试验(下载安装使用全流程) - 2

    基于ffmpeg的视频处理与MPEG的压缩试验ffmpeg介绍与基础知识对提取到的图像进行处理RGB并转化为YUV对YUV进行DCT变换对每个8*8的图像块进行进行量化操作ffmpeg介绍与基础知识ffmpeg是视频和图像处理的工具包,它的下载网址是https://ffmpeg.org/download.html。页面都是英文且下载正确的包的路径笔者找的时候还费点劲,这里记录一下也方便读者。选中这个Windows下的下午files,选择第一个这里有essential和full版本的,大家根据需要自行选择版本包下载下载好之后,在官网上下载ffmpeg的full包,一共300+MB解压,然后安装b

  10. ruby-on-rails - Rails 在上传时使用 stremio-ffmpeg gem 给电影加水印 - 2

    我正在使用carrierwave将电影上传到amazons3,效果非常好。现在我想在上传时或上传后给电影加水印,我不知道,最好的方法是什么?我试过这个:movie_controller.rbAction上传视频movie=FFMPEG::Movie.new(@vid.video.url)puts"........................................"putsmovie.inspectif@vid.save只是为了查明视频是否是从stremio中捕获的。但后来我得到了找不到电影的错误,问题是它没有在amazons3存储桶中查找,它在我的本地服务器上查找Nosu

随机推荐