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