当前位置: 首页 > 工具软件 > AutoGen > 使用案例 >

FFMpeg.AutoGen+D2D解码并播放视频(含音频流)

堵才哲
2023-12-01

最近在捣鼓FFMpeg这个东西,可惜网上的资料实在难找,对于c#里面的FFmpeg.AutoGen更是如此。所以走了不少弯路。(语言组织能力不太好,这篇文章的东西会很杂。涉及到d2d绘图的部分,我封装了一个Direct2DImage类,可以把图像动态绘制到Image控件上,具体实现原理不在这里赘述,请看Github源码)

FFmpeg.AutoGen这个东西就是把ffmpeg的一些接口封装在一个类里面,本身并不包含解码器,在调用方法上和c区别不大。所以在调用之前必须定位ffmpeg的库,代码如下。

ffmpeg.av_register_all();
ffmpeg.avcodec_register_all();
ffmpeg.avformat_network_init();

这三个方法调用一次即可,作用是让ffmpeg加载自己的链接库,顺便一提,ffmpeg的链接库在官网就有,download页面选择Shared下载就是链接库。
然后我们需要一个全局性的对象(pFormatContext ),标志着媒体文件,如果说行话叫“解封装”。

var pFormatContext = ffmpeg.avformat_alloc_context();

ffmpeg.avformat_open_input(&pFormatContext, url, null, null);
ffmpeg.avformat_find_stream_info(pFormatContext, null);

我们需要从媒体中找到视频和音频流,如下。

for (var i = 0; i < pFormatContext->nb_streams; i++)
{
	if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
	{
		pStream = pFormatContext->streams[i];//视频流
	}
	else if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
	{
		aStream = pFormatContext->streams[i];//音频流
	}
}

我们首先讨论视频的解码部分。
“pStream->codecpar->codec_id”代表视频流所对应的解码器的ID,将他传入以下的函数,可以得到一个解码器对象,随后我们需要使用这个对象获取解码器上下文(pCodecContext )。并使用avcodec_open2打开解码器。

var pCodec = ffmpeg.avcodec_find_decoder(pStream->codecpar->codec_id);
pCodecContext = ffmpeg.avcodec_alloc_context3(pCodec);
ffmpeg.avcodec_parameters_to_context(pCodecContext, pStream->codecpar);

ffmpeg.avcodec_open2(pCodecContext, pCodec, null).ThrowExceptionIfError();

在很多时候,视频流直接解码出的数据是YUV格式的,这并不便于我们后续的绘图操作。但是好在ffmpeg提供了一组方便的接口让我们可以转换图像的格式:

pConvertContext = ffmpeg.sws_getContext(width, height, sourcePixFmt,
width, height, destinationPixFmt,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);

如上的代码可以获得一个swr对象(pConvertContext ),sourcePixFmt表示源数据的像素格式,可以通过pCodecContext->pix_fmt来获取,目标像素格式一般为BGRA,随后我们需要对swr分配一个帧对象,并为转换后的数据初始化缓冲区.

pConvertedFrame = ffmpeg.av_frame_alloc();

convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
dstData = new byte_ptrArray4();
dstLinesize = new int_array4();
ffmpeg.av_image_fill_arrays(ref dstData, ref dstLinesize, (byte*)convertedFrameBufferPtr, destinationPixFmt, width, height, 1);

至此为止,视频流解码的准备工作已经全部完成,我们再来说一下音频流的处理(播放使用DirectSound,我把相关的播放代码写进了一个dll,源代码位于Shinehelper,需要自取,这里不对其详细解释)。
回到刚获取到视频和音频流的位置,我们同样需要用流中标识的解码器id来找到并打来解码器:

var cedecparA = *aStream->codecpar;
var pCodec_A = ffmpeg.avcodec_find_decoder(cedecparA.codec_id);
//获取解码器上下文,打开解码器
pcodecContext_A = ffmpeg.avcodec_alloc_context3(pCodec_A);
ffmpeg.avcodec_parameters_to_context(pcodecContext_A, &cedecparA);
ffmpeg.avcodec_open2(pcodecContext_A, pCodec_A, null);

与视频解码不同的是,随后我们需要定义一些音频的信息,鉴于可能有一些朋友不太了解其意义(我也怕自己忘了),我在这里详细描述一下。
描述一段原始音频数据(PCM)的信息中,最常用的有采样率(bits_per_coded_sample)、声道数(channels)、样本格式(sample_fmt)、位深度。而其他的基本都可以通过这几位来计算。
那么该如何理解呢?众所周知,视频拍摄的原理就是快速连续的拍照,而拍照的速度就是帧率。帧率为30就是一秒拍照30次,同样播放时每秒也需要展示30帧。那么数字音频有没有类似的概念呢?有的。
音频数据在计算机中是以数字的形式存在的,与模拟信号不同,数字信号不可能完全精确的描述原波形,我们需要的精确度越高,相同时间内的数据就越大。实际上,数字音频采集的原理与视频类似,我们可以理解为在一段时间内连续对原始音频“拍照”,在播放时再按照原来的速度还原,这个速度就是采样率。比如,采样率为44100,就是每秒给原始音频拍44100张照片,而位深度就是每张照片所占用的大小。如果位深度为16bit,每个音频帧就占用两字节。而声道数和样本格式就比较好理解了,通常情况下声道有2个,因为我们需要左右两个喇叭,而样本格式中常用的是S16(有符号16位)。
说完这些我们言归正传:

ulong out_channel_layout = ffmpeg.AV_CH_LAYOUT_STEREO; //声道描述
out_nb_samples = pcodecContext_A->frame_size; //从视频中读取到的每个音频帧所包含的样本数
bit_per_sample = pcodecContext_A->bits_per_coded_sample; //位深度
AVSampleFormat out_sample_fmt = AVSampleFormat.AV_SAMPLE_FMT_S16; //带符号16位格式
out_sample_rate = pcodecContext_A->sample_rate; //样本率
out_channels = ffmpeg.av_get_channel_layout_nb_channels(out_channel_layout); //声道数
out_buffer_size = ffmpeg.av_samples_get_buffer_size((int*)0, out_channels, out_nb_samples, out_sample_fmt, 1);//缓冲区大小
long in_channel_layout = ffmpeg.av_get_default_channel_layout(pcodecContext_A->channels); //输入的声道描述

因为同样的原因,从视频中解码出的音频数据不便于播放,我们要把他转换为便于播放的数据格式:

au_convert_ctx = ffmpeg.swr_alloc();
au_convert_ctx = ffmpeg.swr_alloc_set_opts(au_convert_ctx, (long)out_channel_layout, out_sample_fmt, out_sample_rate,
in_channel_layout, pcodecContext_A->sample_fmt, pcodecContext_A->sample_rate, 0, (void*)0);
ffmpeg.swr_init(au_convert_ctx);

至此为止,音频解码的准备工作也已经完成,下面要正式开始解码。
一般情况下,我们使用两个线程分别解码并播放音频和视频,但是那样不便于音视频同步,所以我把音频和视频的解码放在同一个线程里面,配合播放的速度解码,而释放解码后的资源的工作则放在播放线程里面。

public struct VideoFrame
{
	public WICBitmap frame;
	public double time_base;
}

public struct AudioFrame
{
	public IntPtr data;
	public double time_base;
}
public List<VideoFrame?> bits = new List<VideoFrame?>();
public List<AudioFrame?> abits = new List<AudioFrame?>();

解码线程会实时填充bits和abits这两个容器,并会配合播放的速度,播放线程处理过以后即释放。

首先我们从媒体流中读取一个数据包(pPacket),然后通过pPacket->stream_index来判断这个数据包是来自视频流还是音频流,并对其采取不同的处理方式。

int error = ffmpeg.av_read_frame(pFormatContext, pPacket);
if (error == ffmpeg.AVERROR_EOF) break;

ffmpeg.avcodec_send_packet(pCodecContext, pPacket).ThrowExceptionIfError();
error = ffmpeg.avcodec_receive_frame(pCodecContext, pDecodedFrame);

如果是视频,我们这样处理:

double timeset = ffmpeg.av_frame_get_best_effort_timestamp(pDecodedFrame) * ffmpeg.av_q2d(pStream->time_base);//该帧的时间,用于音视频同步
ffmpeg.sws_scale(pConvertContext, pDecodedFrame->data, pDecodedFrame->linesize, 0, pCodecContext->height, dstData, dstLinesize); //转换为位数据
ffmpeg.av_packet_unref(pPacket);//释放数据包对象引用
ffmpeg.av_frame_unref(pDecodedFrame);//释放解码帧对象引用
//转换出位数据后,我们用其加载一个WIC图像,准备后续的Direct2D绘图。
var m_bitLoads = new WICBitmap(mFty, pCodecContext->width, pCodecContext->height, SharpDX.WIC.PixelFormat.Format32bppBGR, new DataRectangle(convertedFrameBufferPtr, dstLinesize[0]));
//将其推入容器:
bits.Add(new VideoFrame() {
	frame = m_bitLoads,
    time_base = timeset
});
//在此处,nFram储存了播放线程所处理过的帧数,预处理120帧。
if (bits.Count - nFram >= 120)
{
	while (bits.Count - nFram > 60 && CanRun)
		Thread.Sleep(1);
};

然后是音频的解码:

ffmpeg.avcodec_decode_audio4(pcodecContext_A, pAudioFrame, &got_picture, pPacket); //解码数据包
double timeset = ffmpeg.av_frame_get_best_effort_timestamp(pAudioFrame) * ffmpeg.av_q2d(aStream->time_base); //时间
if (got_picture > 0)
{
	ffmpeg.swr_convert(au_convert_ctx, &out_buffer, 19200, (byte**)&pAudioFrame->data, pAudioFrame->nb_samples);
	//为原始数据分配内存
	var mbuf = Marshal.AllocHGlobal(out_buffer_size);
	RtlMoveMemory((void*)mbuf, out_buffer, out_buffer_size);
	abits.Add(new AudioFrame() {
		data = mbuf, 
		time_base = timeset 
	});
	index++;
}
ffmpeg.av_packet_unref(pPacket);//释放数据包对象引用
ffmpeg.av_frame_unref(pAudioFrame);//释放解码帧对象引用

视频播放的部分如下:

 private DrawProcResult DrawCallback(Direct2DInformation view, object Loadedsouce, int Width, int Height)
        {
            if (Loadedsouce == null)
                return DrawProcResult.Ignore;
            Video video = Loadedsouce as Video;

            if (video.nFarm == video.bits.Count)
                return DrawProcResult.Ignore;

            if (video.bits[video.nFarm]?.frame.IsDisposed == true)
                return DrawProcResult.Ignore;
            video_time = (double)(video.bits[video.nFarm]?.time_base);

         //   Debug.WriteLine("vd"+video_time.ToString());
         //   Debug.WriteLine("au"+audio_time.ToString());

            if (audio_time - video_time > 0.1)
            {
                video.bits[video.nFarm]?.frame.Dispose();

                video.nFarm++;
                return DrawProcResult.Ignore;
            }
            if (audio_time - video_time < -0.1)
            {
                return DrawProcResult.Normal;
            }
            // Console.WriteLine(video.nFarm.ToString() + ":Using");
            Bitmap farme = Bitmap.FromWicBitmap(view.View, video.bits[video.nFarm]?.frame);

            view.View.BeginDraw();
            view.View.DrawBitmap(farme, 
                1, SharpDX.Direct2D1.BitmapInterpolationMode.Linear );
            view.View.EndDraw();

            farme.Dispose();
            //   Console.WriteLine(video.nFarm.ToString() + ":Disposing");
            video.bits[video.nFarm]?.frame.Dispose();

            video.nFarm++;
            return DrawProcResult.Normal;
        }

此为Direct2DImage类的绘图回调,包含了音视频同步代码,返回值表示要true/false更新前台数据,如果返回false则不计算此帧。Direct2DImage里面有一个额外的时钟稳定帧率。

音频播放:

 new Task(() =>
            {

                while (!video.CanRun)
                    Thread.Sleep(1);
                unsafe
                {
                    //       var intp = getPCM("assets.shine:09.pcm");
                    waveInit(hWnd, video.Out_channels, video.Out_sample_rate, video.Bit_per_sample, video.Out_buffer_size);


                    while (true)
                    {

                        if (!video.CanRun)
                            break;
                        if (video.EntiryPlayed && i == video.abits.Count && video.nFarm == video.bits.Count)
                            break;
                        if (i == video.abits.Count)
                        {
                            Thread.Sleep(1);
                            continue;
                        }
                        audio_time = (double)(video.abits[i]?.time_base);

                        waveWrite((byte*)video.abits[i]?.data, video.Out_buffer_size);
                        Marshal.FreeHGlobal((IntPtr)video.abits[i]?.data);
                        video.abits[i] = null;
                        i++;
                    }

                    waveClose();
                    this.Dispatcher.Invoke(endplay);
                }

            }).Start();

Git:https://github.com/rootacite/Shinengine

 类似资料: