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

  • WinUI3 FFmpeg.autogen解析视频帧,使用win2d显示内容.已关闭评论
  • 172 次浏览
  • A+
所属分类:.NET技术
摘要

  WinUI3的Window App Sdk,虽然已经更新到1.12了但是依然没有MediaPlayerElement控件,最近在学习FFmpeg,所以写一下文章记录一下。由于是我刚刚开始学习FFmpeg 的使用,所以现在只能做到播放视频,播放音频并没有做好,所以这遍文章先展示一下播放视频的流程。效果图如下。

  WinUI3的Window App Sdk,虽然已经更新到1.12了但是依然没有MediaPlayerElement控件,最近在学习FFmpeg,所以写一下文章记录一下。由于是我刚刚开始学习FFmpeg 的使用,所以现在只能做到播放视频,播放音频并没有做好,所以这遍文章先展示一下播放视频的流程。效果图如下。WinUI3 FFmpeg.autogen解析视频帧,使用win2d显示内容.

一、准备工作

  1.在NeGet上引入 FFmpeg.autogen库;

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

  2.下载已经编译好ffmpeg dll文件 下载地址:(需要下载对应FFmpeg.autogen的版本)https://github.com/BtbN/FFmpeg-Builds/releases?page=2,下载好后解压文件提取里面的dll文件,并在项目中新建目录并改名为FFmpe下面为目录结构。并将所有ffmpeg的dll文件属性 复制到输出目录改为 “始终复制”或者“如果较新则复制” 选项

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

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

二.解码流程

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

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)