前面我们已经简单介绍过FFMpeg.AutoGen,今天来看下怎么使用它来实现MP4视频格式的播放。
说明:ffmpeg软解码和硬解码
软解码:指的是使用CPU进行对封装的音视频数据做解码。其优势是有更好的兼容性,不依赖GPU的算力,也不存在内存和显存直接的数据交换,在CPU的性能比较好的机器上能有更好的综合性能表现。如果代码实现过程中需要对解码出来的数据做二次处理,那么更推荐使用软解码。
硬解码:指的是使用GPU进行对封装的音视频数据做解码。使用硬解码过程中,主要消耗GPU资源。其优势是能充分使用显卡资源,提升视频播放的流畅度。如果GPU性能比较差的机器,用软解码或能获得更好的视频播放体验。
环境准备
-
安装FFMpeg.AutoGen nuget包,版本只需要跟FFMpge版本号对应上就好。ffmpeg版本需要注意x86和x64的版本。
-
开启”允许使用unsafe编译代码”
本示例是基于”FFmpeg.AutoGen 6.1.0.1”和”FFmpeg 6.0”版本,以软解码实现方式进行演示
实现步骤
-
初始化解码上下文
由于ffmpeg是非托管代码实现,其中的内存需要我们手动释放。为了方便的管理这些对象,我们自定义一个解码上下文的类,主要用于内存复用和释放。具体定义如下:
/// <summary>
/// 解码上下文
/// </summary>
public unsafe class MediaCodecContext
{
/// <summary>
/// 视频播放总时长,单位为秒
/// </summary>
public double Duration;
/// <summary>
/// 时间基
/// </summary>
public AVRational AVStreamTimeBase;
/// <summary>
/// 帧率
/// </summary>
public int Fps;
/// <summary>
/// 缩放比
/// </summary>
public double Scale;
/// <summary>
///
/// </summary>
public int Width;
/// <summary>
///
/// </summary>
public int Height;
/// <summary>
/// 视频流的索引
/// </summary>
public int VideoStreamIndex;
/// <summary>
/// 音频流的索引
/// </summary>
public int AudioStreamIndex;
/// <summary>
///
/// </summary>
public int PixelFormat;
/// <summary>
/// 音/视频上下文
/// </summary>
public AVFormatContext* FormatContext;
/// <summary>
/// 视频解码器上下文
/// </summary>
public AVCodecContext* VideoCodecContext;
/// <summary>
/// 音频解码上下文
/// </summary>
public AVCodecContext* AudioCodecContext;
/// <summary>
/// 复用数据包
/// </summary>
public AVPacket* Packet;
/// <summary>
/// 加载的文件路径
/// </summary>
public string FileName = string.Empty;
/// <summary>
/// 当前播放的索引
/// </summary>
public int CurrentFrameIndex;
/// <summary>
/// 释放资源占用,重置上下文
/// </summary>
public void Free()
{
var packet = Packet;
if (packet != null)
{
ffmpeg.av_packet_unref(packet);
ffmpeg.av_packet_free(&packet);
Packet = packet;
}
var videoCodecContext = VideoCodecContext;
if (videoCodecContext != null)
{
ffmpeg.avcodec_free_context(&videoCodecContext);
VideoCodecContext = videoCodecContext;
}
var audioCodecContext = AudioCodecContext;
if (audioCodecContext != null)
{
ffmpeg.avcodec_free_context(&audioCodecContext);
AudioCodecContext = audioCodecContext;
}
var formatContext = FormatContext;
if (formatContext != null)
{
ffmpeg.avformat_close_input(&formatContext);
ffmpeg.avformat_free_context(formatContext);
FormatContext = formatContext;
}
CurrentFrameIndex = 0;
AudioStreamIndex = -1;
VideoStreamIndex = -1;
}
- 查找并打开解码器
/// <summary>
/// 打开文件,读取音/视频流,获取解码器,打开解码器
/// </summary>
/// <param name="filePath"></param>
public unsafe bool Open()
{
//分配解码上下文,该上下文贯穿整个解码过程,包含封装格式,音视频流信息,时长等
var formatContext = ffmpeg.avformat_alloc_context();
//打开文件,并读取文件头部
var res = ffmpeg.avformat_open_input(&formatContext, _context.FileName, null, null);
if (res != 0)
{
//打开失败,释放资源
ffmpeg.avformat_free_context(formatContext);
//Log.Error($"avformat_open_input:{res}");
return false;
}
//查找音/视频流
if ((res = ffmpeg.avformat_find_stream_info(formatContext, null)) != 0)
{
ffmpeg.avformat_free_context(formatContext);
//Log.Error($"avformat_find_stream_info:{res}");
return false;
}
//枚举音/视频流
AVStream* videoStream = null, audioStream = null;
for (var i = 0; i < formatContext->nb_streams; i++)
{
var codecpar = formatContext->streams[i]->codecpar;
if (codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
videoStream = formatContext->streams[i];
_context.VideoStreamIndex = i;
}
else if (codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
{
audioStream = formatContext->streams[i];
_context.AudioStreamIndex = i;
}
}
if (videoStream == null && audioStream == null)
{
ffmpeg.avformat_free_context(formatContext);
//Log.Warn($"ffmpeg:检测不到音视频流");
return false;
}
if (audioStream != null)
{
// 初始化音频解码器
if (!InitializeAudioDecoder(audioStream->codecpar))
{
ffmpeg.avformat_free_context(formatContext);
//Log.Error($"ffmpeg:初始化音频解码器失败");
return false;
}
}
if (videoStream != null)
{
// 初始化视频解码器
if (!InitializeVideoDecoder(videoStream->codecpar))
{
ffmpeg.avformat_free_context(formatContext);
//Log.Error($"ffmpeg:初始化视频解码器失败");
return false;
}
}
//初始化解码过程中的复用数据包,为av_packet申请内存,用于存放每一帧的封装的数据包
var packet = ffmpeg.av_packet_alloc();
var fps = videoStream->avg_frame_rate.num / videoStream->avg_frame_rate.den;
#region 初始化上下文
_context.FormatContext = formatContext;
_context.Duration = videoStream->duration * ffmpeg.av_q2d(videoStream->time_base);
_context.Packet = packet;
_context.Fps = fps;
_context.Scale = 1;
#endregion
return true;
}
/// <summary>
/// 初始化视频解码器
/// </summary>
/// <param name="codecParameter"></param>
/// <returns></returns>
private unsafe bool InitializeVideoDecoder(AVCodecParameters* codecParameter)
{
//查找解码器
var codec = ffmpeg.avcodec_find_decoder(codecParameter->codec_id);
//为解码器分配一个上下文
var codecContext = ffmpeg.avcodec_alloc_context3(codec);
//填充解码器上下文参数
var res = ffmpeg.avcodec_parameters_to_context(codecContext, codecParameter);
if (res != 0)
{
//释放上下文资源
ffmpeg.avcodec_free_context(&codecContext);
return false;
}
//打开解码器
res = ffmpeg.avcodec_open2(codecContext, codec, null);
if (res != 0)
{
ffmpeg.avcodec_free_context(&codecContext);
return false;
}
_context.VideoCodecContext = codecContext;
_context.Width = codecParameter->width;
_context.Height = codecParameter->height;
_context.PixelFormat = codecParameter->format;
return true;
}
//初始化音频解码器(暂不做实现)
private unsafe bool InitializeAudioDecoder(AVCodecParameters* codecParameter)
{
//音频暂不做实现
return true;
}
- 读取帧数据,并进行解码和转码
/// <summary>
/// 读取视频帧数据
/// </summary>
/// <returns></returns>
private unsafe VideoFrame ReadVideoFrame()
{
var data = new VideoFrame();
//将av_packet数据发送给解码器
var res = ffmpeg.avcodec_send_packet(_context.VideoCodecContext, _context.Packet);
//为av_frame结构体分配内存
var frame = ffmpeg.av_frame_alloc();
frame->width = _context.Width;
frame->height = _context.Height;
frame->format = _context.PixelFormat;
//根据像素的宽高,给av_frame分配实际占用的内存
ffmpeg.av_frame_get_buffer(frame, 0);
//将解码后的数据填充到av_frame
res = ffmpeg.avcodec_receive_frame(_context.VideoCodecContext, frame);
if (res != 0)
{
data.State = FrameState.Continue;
//减少av_frame的引用计数,防止内存泄露
ffmpeg.av_frame_unref(frame);
//释放av_frame的内存占用
ffmpeg.av_frame_free(&frame);
//减少av_packet的引用计数,防止内存泄露
ffmpeg.av_packet_unref(_context.Packet);
return data;
}
_context.CurrentFrameIndex++;
var scale = _context.Scale;
//将原始帧转码成BGRA,方便使用WPF进行渲染
AVFrame* originalFrame = ConvertToBGRA(frame, (int)(scale * _context.Width), (int)(scale * _context.Height));
//将av_frame转成二进制,传递给wpf
var pixels = CropFrameToPixels(originalFrame, originalFrame->linesize[0], new Int32Rect(0, 0, originalFrame->width, originalFrame->height));
//显示用的二进制像素数据
data.Pixels = pixels;
//每一行像素所占用的字节数
data.Pitch = originalFrame->linesize[0];
//像素宽度
data.Width = originalFrame->width;
//像素高度
data.Height = originalFrame->height;
data.State = FrameState.OK;
//释放av_frame的内存占用
ffmpeg.av_frame_unref(frame);
ffmpeg.av_frame_free(&frame);
ffmpeg.av_frame_unref(originalFrame);
ffmpeg.av_frame_free(&originalFrame);
ffmpeg.av_packet_unref(_context.Packet);
return data;
}
/// <summary>
/// 视频帧数据结构
/// 定义为结构体是为了提升性能
/// </summary>
public struct VideoFrame : IMediaFrame
{
/// <summary>
/// 渲染数据的宽度
/// </summary>
public int Width;
/// <summary>
/// 渲染数据的高度
/// </summary>
public int Height;
/// <summary>
/// 渲染数据的像素信息
/// 视频数据表示 Pixels,音频数据表示 pcm
/// </summary>
public byte[] Pixels;
/// <summary>
/// 每行占用的字节数
/// </summary>
public int Pitch;
/// <summary>
/// 当前帧的状态
/// </summary>
public FrameState State { get; set; }
}
- 转码成可显示的数据格式
大部分音视频数据解码出来后都是YUV格式的,我们需要将他们转码成BRGA格式,方便我们使用WPF进行渲染。
/// <summary>
/// 转码成BGRA
/// </summary>
/// <param name="input"></param>
/// <param name="outputWidth"></param>
/// <param name="outputHeight"></param>
/// <returns></returns>
public unsafe AVFrame* ConvertToBGRA(AVFrame* input, int outputWidth, int outputHeight)
{
//创建转码上下文
SwsContext* swsContext = ffmpeg.sws_getContext(input->width, input->height, (AVPixelFormat)input->format, outputWidth, outputHeight, AVPixelFormat.AV_PIX_FMT_BGRA, ffmpeg.SWS_FAST_BILINEAR, null, null, null);
AVFrame* output = ffmpeg.av_frame_alloc();
output->format = (int)AVPixelFormat.AV_PIX_FMT_BGRA;
output->width = outputWidth;
output->height = outputHeight;
var res = ffmpeg.av_frame_get_buffer(output, 0);
//开始转码
res = ffmpeg.sws_scale(swsContext, input->data, input->linesize, 0, input->height, output->data, output->linesize);
//释放资源
ffmpeg.sws_freeContext(swsContext);
return output;
}
/// <summary>
/// 裁剪帧到像素集合
/// </summary>
/// <param name="srcFrame">原始BGRA帧</param>
/// <param name="targetLineSize">裁剪后,一行包含多少像素</param>
/// <param name="region">裁剪的区域大小</param>
/// <returns></returns>
public unsafe byte[] CropFrameToPixels(AVFrame* srcFrame, int targetLineSize, Int32Rect region)
{
int frameStride = srcFrame->linesize[0];
var size = targetLineSize * region.Height;
byte[] pixels = new byte[size];
for (int height = 0; height < region.Height; ++height)
{
int srcStartIndex = (height + region.Y) * frameStride + region.X * 4;
int targetStartIndex = height * targetLineSize;
var srcStartPointer = &srcFrame->data[0][srcStartIndex];
Marshal.Copy((IntPtr)srcStartPointer, pixels, targetStartIndex, targetLineSize);
}
return pixels;
}
- 渲染播放
在WPF中,我们可以通过使用WriteableBitmap来实现对BGRA数据格式的显示。
//自定义播放器
public MediaPlayer()
{
_child = new DrawingVisual();
AddVisualChild(_child);
}
protected override Visual GetVisualChild(int index)
{
return _child;
}
protected override int VisualChildrenCount => 1;
//需要渲染的帧,这里复用提升性能
private WriteableBitmap _frame;
/// <summary>
/// 绘制视频帧
/// </summary>
/// <param name="data"></param>
private void Draw(VideoFrame data)
{
using (var drawingContext = _child.RenderOpen())
{
if (_frame == null || _frame.Width != data.Width || _frame.Height != data.Height)
{
_frame = new WriteableBitmap(data.Width, data.Height, 96, 96, PixelFormats.Pbgra32, null);
}
if (_frame.TryLock(new Duration(TimeSpan.FromMilliseconds(50))))
{
try
{
_frame.WritePixels(new Int32Rect(0, 0, data.Width, data.Height), data.Pixels, data.Pitch, 0);
drawingContext.DrawImage(_frame, new Rect(0, 0, data.Width, data.Height));
}
catch (Exception ex)
{
}
finally
{
_frame.Unlock();
}
}
}
}
- 播放控制
要想视频动起来,我们就需要写一个循环,让他针对每一帧进行播放,示例代码如下:
Task.Factory.StartNew(() =>
{
_decoder.Load(filename);
if (!_decoder.Open())
{
_decoder.Close();
return;
}
IsPlaying = true;
var startTime = Environment.TickCount;
var delaySpan = 1000 / _decoder.Fps;
while (IsPlaying)
{
_decoder.SetDisplaySize(RenderSize.Width, RenderSize.Height);
var data = _decoder.ReadFrame();
if (data.State == FrameState.OK)
{
if (data is VideoFrame videoFrame)
{
Dispatcher.InvokeAsync(() =>
{
Draw(videoFrame);
});
}
else
{
continue;
}
}
else if (data.State == FrameState.Over)
{
if (data is VideoFrame videoFrame)
{
Dispatcher.InvokeAsync(() =>
{
Draw(videoFrame);
});
}
else
{
break;
}
}
else
{
continue;
}
var decodeTime = Environment.TickCount - startTime;
var delay = Math.Max(0, delaySpan - decodeTime);
Thread.Sleep(delay);
startTime = Environment.TickCount;
}
_decoder.Close();
IsPlaying = false;
});
写在最后
示例代码并非完整代码,大家在实现视频播放功能的时候,还是需要考虑不少细节,上述代码仅供参考!过程中还有什么疑问可以在评论区留言。
博客地址:https://huchengv5.github.io/
微信公众号:
欢迎转载分享,如若转载,请标注署名。
本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/WPF-%E4%BD%BF%E7%94%A8FFMpeg.AutoGen%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。