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();
复制代码 三、通过上面的几个步骤我们就可以从 打开一个媒体文件-》初始化解码流程-》读取帧数据-》绘制到屏幕,来完整的播放一个视频了。下一篇文章我将展示如何通过进度条来进行视频从哪里开始播放;
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |