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

打印 上一主题 下一主题

主题 843|帖子 843|积分 2531

  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的;
  1. public static class FFmpegHelper
  2.     {
  3.         public static  void RegisterFFmpegBinaries()
  4.         {
  5.             //获取当前软件启动的位置
  6.             var currentFolder = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
  7.             //ffmpeg在项目中放置的位置
  8.             var probe = Path.Combine("FFmpeg", "bin", Environment.Is64BitOperatingSystem ? "x64" : "x86");
  9.             while (currentFolder != null)
  10.             {
  11.                 var ffmpegBinaryPath = Path.Combine(currentFolder, probe);
  12.                 if (Directory.Exists(ffmpegBinaryPath))
  13.                 {
  14.                     //找到dll放置的目录,并赋值给rootPath;
  15.                     ffmpeg.RootPath = ffmpegBinaryPath;
  16.                     return;
  17.                 }
  18.                 currentFolder = Directory.GetParent(currentFolder)?.FullName;
  19.             }
  20.             //旧版本需要要调用这个方法来注册dll文件,新版本已经会自动注册了
  21.             //ffmpeg.avdevice_register_all();
  22.         }
  23. }
复制代码
  2).在软件启动时调用 RegisterFFmpegBinaries函数注册dll文件;(在 App.Xaml.cs的OnLaunched上添加 FFmpegHelper.RegisterFFmpegBinaries()函数)
  1. protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
  2.         {
  3.             m_window = new MainWindow();
  4.             m_window.Activate();
  5.             FFmpegHelper.RegisterFFmpegBinaries();
  6.         }
复制代码
二.解码流程


1.在开始解码前我们先将需要用到的解码结构都声明;这些结构都是在整个解码过程我们需要操作的指针。
  1. //媒体格式上下文(媒体容器)
  2. AVFormatContext* format;
  3. //编解码上下文
  4. AVCodecContext* codecContext;
  5. //媒体数据包
  6. AVPacket* packet;
  7. //媒体帧数据
  8. AVFrame* frame;
  9. //图像转换器
  10. SwsContext* convert;
  11. //视频流
  12. AVStream* videoStream;
  13. // 视频流在媒体容器上流的索引
  14. int videoStreamIndex;
复制代码
  2.InitDecodecVideo() 初始化解码器函数 .
  1. void InitDecodecVideo(string path)
  2.         {
  3.             int error = 0;
  4.             //创建一个 媒体格式上下文
  5.             format = ffmpeg.avformat_alloc_context();
  6.             if (format == null)
  7.             {
  8.                 Debug.WriteLine("创建媒体格式(容器)失败");
  9.                 return;
  10.             }
  11.             var tempFormat = format;
  12.             //打开视频
  13.             error = ffmpeg.avformat_open_input(&tempFormat, path, null, null);
  14.             if (error < 0)
  15.             {
  16.                 Debug.WriteLine("打开视频失败");
  17.                 return;
  18.             }
  19.             //获取流信息
  20.             ffmpeg.avformat_find_stream_info(format, null);
  21.             //编解码器类型
  22.             AVCodec* codec = null;
  23.             //获取视频流索引
  24.             videoStreamIndex = ffmpeg.av_find_best_stream(format, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
  25.             if (videoStreamIndex < 0)
  26.             {
  27.                 Debug.WriteLine("没有找到视频流");
  28.                 return;
  29.             }
  30.             //根据流索引找到视频流
  31.             videoStream = format->streams[videoStreamIndex];
  32.             //创建解码器上下文
  33.             codecContext = ffmpeg.avcodec_alloc_context3(codec);
  34.             //将视频流里面的解码器参数设置到 解码器上下文中
  35.             error = ffmpeg.avcodec_parameters_to_context(codecContext, videoStream->codecpar);
  36.             if (error < 0)
  37.             {
  38.                 Debug.WriteLine("设置解码器参数失败");
  39.                 return;
  40.             }
  41.             //打开解码器
  42.             error = ffmpeg.avcodec_open2(codecContext, codec, null);
  43.             if (error < 0)
  44.             {
  45.                 Debug.WriteLine("打开解码器失败");
  46.                 return;
  47.             }
  48.             //视频时长等视频信息
  49.             //Duration = TimeSpan.FromMilliseconds(videoStream->duration / ffmpeg.av_q2d(videoStream->time_base));
  50.             Duration = TimeSpan.FromMilliseconds(format->duration / 1000);
  51.             CodecId = videoStream->codecpar->codec_id.ToString();
  52.             CodecName = ffmpeg.avcodec_get_name(videoStream->codecpar->codec_id);
  53.             Bitrate = (int)videoStream->codecpar->bit_rate;
  54.             FrameRate = ffmpeg.av_q2d(videoStream->r_frame_rate);
  55.             FrameWidth = videoStream->codecpar->width;
  56.             FrameHeight = videoStream->codecpar->height;
  57.             frameDuration = TimeSpan.FromMilliseconds(1000 / FrameRate);
  58.             //初始化转换器,将图片从源格式 转换成 BGR0 (8:8:8)格式
  59.             var result = InitConvert(FrameWidth, FrameHeight, codecContext->pix_fmt, FrameWidth, FrameHeight, AVPixelFormat.AV_PIX_FMT_BGR0);
  60.             //所有内容都初始化成功了开启时钟,用来记录时间
  61.             if (result)
  62.             {
  63.                 //从内存中分配控件给 packet 和frame
  64.                 packet = ffmpeg.av_packet_alloc();
  65.                 frame = ffmpeg.av_frame_alloc();
  66.                 clock.Start();
  67.                 DisaplayVidwoInfo();
  68.             }
  69.         }
复制代码
  在初始解码过程中,我们也是可以拿到视频里面所包含的信息,比如 解码器类型,比特率,帧率,视频的款高度,还有视频时长等信息。在配置完解码信息后也能从代码中看到了调用              InitConvert() 初始化转码器的函数,这里我将最后一个参数设置了为 AVPixelFormat.AV_PIX_FMT_BGR0,这里会到后面的创建 CanvasBitmap 位图的格式对应。

  3.InitConvert() 函数中创建了一个将读取的帧数据转换成指定图像格式的 SwsContext 对象;
  1. bool InitConvert(int sourceWidth, int sourceHeight, AVPixelFormat sourceFormat, int targetWidth, int targetHeight, AVPixelFormat targetFormat)
  2.         {
  3.             //根据输入参数和输出参数初始化转换器
  4.             convert = ffmpeg.sws_getContext(sourceWidth, sourceHeight, sourceFormat, targetWidth, targetHeight, targetFormat, ffmpeg.SWS_FAST_BILINEAR, null, null, null);
  5.             if (convert == null)
  6.             {
  7.                 Debug.WriteLine("创建转换器失败");
  8.                 return false;
  9.             }
  10.             //获取转换后图像的 缓冲区大小
  11.             var bufferSize = ffmpeg.av_image_get_buffer_size(targetFormat, targetWidth, targetHeight, 1);
  12.             //创建一个指针
  13.             FrameBufferPtr = Marshal.AllocHGlobal(bufferSize);
  14.             TargetData = new byte_ptrArray4();
  15.             TargetLinesize = new int_array4();
  16.             ffmpeg.av_image_fill_arrays(ref TargetData, ref TargetLinesize, (byte*)FrameBufferPtr, targetFormat, targetWidth, targetHeight, 1);
  17.             return true;
  18.         }
复制代码
  4.TreadNextFrame()读取下一帧数据,在读取到 数据包的时候需要判断一下是不是视频帧,因为在一个“媒体容器”里面会包含 视频,音频,字母,额外数据等信息的; 
  1. bool TryReadNextFrame(out AVFrame outFrame)
  2.         {
  3.             lock (SyncLock)
  4.             {
  5.                 int result = -1;
  6.                 //清理上一帧的数据
  7.                 ffmpeg.av_frame_unref(frame);
  8.                 while (true)
  9.                 {
  10.                     //清理上一帧的数据包
  11.                     ffmpeg.av_packet_unref(packet);
  12.                     //读取下一帧,返回一个int 查看读取数据包的状态
  13.                     result = ffmpeg.av_read_frame(format, packet);
  14.                     //读取了最后一帧了,没有数据了,退出读取帧
  15.                     if (result == ffmpeg.AVERROR_EOF || result < 0)
  16.                     {
  17.                         outFrame = *frame;
  18.                         return false;
  19.                     }
  20.                     //判断读取的帧数据是否是视频数据,不是则继续读取
  21.                     if (packet->stream_index != videoStreamIndex)
  22.                         continue;
  23.                     //将包数据发送给解码器解码
  24.                     ffmpeg.avcodec_send_packet(codecContext, packet);
  25.                     //从解码器中接收解码后的帧
  26.                     result = ffmpeg.avcodec_receive_frame(codecContext, frame);
  27.                     if (result < 0)
  28.                         continue;
  29.                     outFrame = *frame;
  30.                     return true;
  31.                 }
  32.             }
  33.       }
复制代码
  5.FrameConvertBytes() 将读取到的帧通过转换器将数据转换成 byte[] ; 
  1. byte[] FrameConvertBytes(AVFrame* sourceFrame)
  2.         {
  3.             // 利用转换器将yuv 图像数据转换成指定的格式数据
  4.             ffmpeg.sws_scale(convert, sourceFrame->data, sourceFrame->linesize, 0, sourceFrame->height, TargetData, TargetLinesize);
  5.             var data = new byte_ptrArray8();
  6.             data.UpdateFrom(TargetData);
  7.             var linesize = new int_array8();
  8.             linesize.UpdateFrom(TargetLinesize);
  9.             //创建一个字节数据,将转换后的数据从内存中读取成字节数组
  10.             byte[] bytes = new byte[FrameWidth * FrameHeight * 4];
  11.             Marshal.Copy((IntPtr)data[0], bytes, 0, bytes.Length);
  12.             return bytes;
  13.         }
复制代码
    6.创建一个新的任务线程,通过一个while循环来读取帧数据,并转换成 byte[] 以便于创建 CannvasBitmap 位图对象绘制到屏幕上;
  1. PlayTask = new Task(() =>
  2.              {
  3.                  while (true)
  4.                  {
  5.                      lock (SyncLock)
  6.                      {
  7.                          //播放中
  8.                          if (Playing)
  9.                          {
  10.                              if (clock.Elapsed > Duration)
  11.                                  StopPlay();
  12.                              if (lastTime == TimeSpan.Zero)
  13.                              {
  14.                                  lastTime = clock.Elapsed;
  15.                                  isNextFrame = true;
  16.                              }
  17.                              else
  18.                              {
  19.                                  if (clock.Elapsed - lastTime >= frameDuration)
  20.                                  {
  21.                                      lastTime = clock.Elapsed;
  22.                                      isNextFrame = true;
  23.                                  }
  24.                                  else
  25.                                      isNextFrame = false;
  26.                              }
  27.                              if (isNextFrame)
  28.                              {
  29.                                  if (TryReadNextFrame(out var frame))
  30.                                  {
  31.                                      var bytes = FrameConvertBytes(&frame);
  32.                                      bitmap = CanvasBitmap.CreateFromBytes(CanvasDevice.GetSharedDevice(), bytes, FrameWidth, FrameHeight, DirectXPixelFormat.B8G8R8A8UIntNormalized);
  33.                                      canvas.Invalidate();
  34.                                  }
  35.                              }
  36.                          }
  37.                      }
  38.                  }
  39.              });
  40.             PlayTask.Start();
复制代码
三、通过上面的几个步骤我们就可以从 打开一个媒体文件-》初始化解码流程-》读取帧数据-》绘制到屏幕,来完整的播放一个视频了。下一篇文章我将展示如何通过进度条来进行视频从哪里开始播放;



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

拉不拉稀肚拉稀

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表