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)