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

  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)

posted @ 2022-07-21 23:53  吃饭/睡觉  阅读(2777)  评论(1编辑  收藏  举报