Github地址 本文主要介绍一个用ffmpeg来封装的音视频的框架。这个框架的功能是完成从解码到最后播放的过程。能清晰地展现各个层级之间的关系。
文章目录
- 1 框架整体结构
- 2 解封装过程
- 3 音视频解码过程
- 3.1 解码器的创建
- 3.2 音视频解码数据的存储过程
- 3.3 解码过程
- 4 音视频同步
- 5 视频使用OpenGL ES渲染
- 6 音频使用OpenSL ES渲染
1 框架整体结构
这个类图主要是标明解封装,解码,和音视频渲染之间的关系。
IDemux是解封装类,作为被观察者。IDecode是其观察者,当解封装出数据后,直接交给IDecode来解码。
IDecode是解码类,除了作为观察者外,也作为被观察者。会有两个IDecode对象,音频解码器和视频解码器。当解码出数据后,如果当前是视频解码器,则IDecode对象的观察者是IVideoView,如果是音频解码器,则其观察者是IResample。
IVideoView和IResample同为IDecode的观察者,一个用于渲染视频数据,一个用于渲染音频数据。
这个结构图主要是介绍如何使用此框架,对于调用者来说,不需要关心如何创建解码器和解封装器,只需要调用IPlayerProxy接口中的方法,来实现播放的各种功能。
2 解封装过程
IDemux作为解封装对象,会在子线程中用死循环中执行解封装操作。并把解封的数据传给下游的解码器。
void IDemux::Main()
{
while (!isExit)
{
XData d = Read();
if(d.size > 0){
Notify(d);
}else{
XSleep(2);
}
}
}
IDemux这一层可以直接获取到一些基础信息,如视频总时长,视频参数,音频参数等。还可以通过控制解封装的位置来控制播放的seek操作。
class IDemux : public IObserver{
public:
virtual bool Seek(double pos) = 0;
//读取视频参数
virtual XParameter GetVPara() = 0;
//获取音频参数
virtual XParameter GetAPara() = 0;
//总时长(毫秒)
int totalMs = 0;
};
3 音视频解码过程
3.1 解码器的创建
创建时,音视频解码器并没有不同
IDecode *vdecode = CreateDecode();
IDecode *adecode = CreateDecode();
把两个解码器分别赋值给IPlayer对象
play->adecode = adecode;
play->vdecode = vdecode;
解码器打开时,会根据XParameter参数不同来标识是视频解码还是音频解码。
IPlayer#open()
if(!vdecode || !vdecode->Open(demux->GetVPara(),isHardDecode)){
XLOGE("vdecode->Open %s failed!",path);
//return false;
}
if(!vdecode || !adecode->Open(demux->GetAPara()))
{
XLOGE("adecode->Open %s failed!", path);
}
3.2 音视频解码数据的存储过程
IDecode作为IDemux的观察者,IDemux解封装出数据后,会调用IDecode的Update方法。
void IDecode::Update(XData pkt)
{
if(pkt.isAudio != isAudio)
{
return;
}
while (!isExit){
packsMutex.lock();
//阻塞
if(packs.size() < maxList){
//生产者
packs.push_back(pkt);
packsMutex.unlock();
break;
}
packsMutex.unlock();
XSleep(1);
}
}
在IDecode的UpDate方法中会根据isAudio字段判断是否是当前的解码器。视频解码器只会将视频数据添加到自己的队列中,音频解码器只会解码音频数据添加到自己的队列中。
isAudio字段是在其子类FFDecode中赋值的,
if(codec->codec_type == AVMEDIA_TYPE_VIDEO){
this->isAudio = false;
}else{
this->isAudio = true;
}
这里会有一点绕,总之IDemux作为被观察者,不关心具体谁是观察者,只是将解封装出数据发送给观察者。但是对于IDecode来说,会有两个IDecode对象,一个音频解码器audioDecode和视频解码器videoDecode。两个解码器没有其他不同,只是会根据isAudio字段不同来区分。
3.3 解码过程
IDecode会在子线程中通过死循环来不断轮训IDemux传过来的解封装后的数据。解码完成后,会通知所有观察者来进行下一步的处理。
void IDecode::Main()
{
while(!isExit)
{
//取出packet 消费者
XData pack = packs.front();
packs.pop_front();
//发送数据到解码线程, 一个数据包,可能解码多个结果
if(this->SendPacket(pack))
{
while(!isExit)
{
//获取解码数据
XData frame = RecvFrame();
if(!frame.data){
break;
}
this->Notify(frame);
}
}
pack.Drop();
packsMutex.unlock();
}
}
4 音视频同步
音视频同步的做发就是用音频就同步视频。也就是音频正常播放,用视频去和音频同步。
框架中会有两个并行的IDecode对象,audioDecode和videoDecode,同事接收来自解封装器发送过来的数据,并分别执行音频解码和视频解码。
IDecode *adecode = CreateDecode();
demux->AddObs(vdecode);
demux->AddObs(adecode);
IPlayer作为全局的管理者与发起者。会持有audioDecode和videoDecode对象。
class IPlayer : public XThread{
IDecode *vdecode = 0;
IDecode *adecode = 0;
protected:
//用作音视频同步
void Main();
};
IPlayer对象的在子线程中开启一个无限循环,并把音频解码器中的播放时间戳传递给视频解码器。
void IPlayer::Main()
{
while (!isExit)
{
//同步
//获取音频的pts 告诉视频
int apts = audioPlay->pts;
vdecode->synPts = apts;
mux.unlock();
XSleep(2);
}
}
视频解码器中会判断音频的播放时间戳是否比自己当前解码出的播放时间戳小,则线程停止1毫秒,来降低解码速度。
void IDecode::Main()
{
while(!isExit)
{
//判断音视频同步
if(!isAudio && synPts >0){
if(synPts < pts){
packsMutex.unlock();
XSleep(1);
continue;
}
}
}
5 视频使用OpenGL ES渲染
IVideoView作为视频解码的下游,负责接受解码后的视频数据。
IVideoView *view = CreateVideoView();
vdecode->AddObs(view);
GlVideoView作为IVideoView的子类,接收到视频数据后,会先创建XTexture并初始化。然后直接通过XTexture绘制
void GLVideoView::Render(XData data)
{
if(!view){
return;
}
if(!txt){
txt = XTexture::Create();
txt->Init(view, (XTextureType)data.format);
}
txt->Draw(data.datas, data.width, data.height);
}
XTexture初始化过程中会首先通过其持有的XEGL对象来完成EGL的初始化。
再通过XShader来完成Shader和OpenGl的初始化。此初始化过程主要包括顶点和片元着色器的创建,渲染程序的创建,以及向程序中传递顶点坐标和纹理坐标等。
virtual bool Init(void *win, XTextureType type)
{
if(!XEGL::Get()->Init(win)){
mux.unlock();
return false;
}
sh.Init((XShaderType)type);
return true;
}
最后的绘制过程首先是获取yuv数据对应的纹理。YUV数据格式不同则会对应不同的纹理id。然后将yuv数据传递到对应的纹理id中。
再调用openGl和EGL的绘制方法。
XTexture#draw()
virtual void Draw(unsigned char *data[], int width, int height)
{
mux.lock();
sh.GetTexture(0, width, height,data[0]); //Y
if(type == XTEXTURE_YUV420P)
{
sh.GetTexture(1, width/2, height/2, data[1]); //U
sh.GetTexture(2, width/2, height/2, data[2]); //V
}
else{
sh.GetTexture(1, width/2, height/2, data[1]); //UV
}
sh.Draw();
XEGL::Get()->Draw();
mux.unlock();
}
XEGL#Draw()
virtual void Draw()
{
mux.lock();
if(display == EGL_NO_DISPLAY || surface == EGL_NO_SURFACE){
mux.unlock();
return;
}
eglSwapBuffers(display, surface);
mux.unlock();
}
XTexture#Draw()
void XShader::Draw()
{
mux.lock();
if(!program){
mux.unlock();
return;
}
//三维绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0 ,4);
mux.unlock();
}
6 音频使用OpenSL ES渲染
IAudioPlay作为音频解码器的下游,负责接收解码后的音频的数据。和视频不同的是,这里的音频数据会存储到队列中等待被消费。而视频数据传递一帧渲染一帧。
void IAudioPlay::Update(XData data)
{
//压入缓冲队列
if(data.size<=0 || !data.data)return;
while (!isExit)
{
framesMutex.lock();
if(frames.size() > maxFrame)
{
framesMutex.unlock();
XSleep(1);
continue;
}
frames.push_back(data);
}
}
SLAudioPlay作为IAudioPlay的子类,首先会创建播放引擎对象SLEngineItf。并配置SLEngineItf一系列的参数。
其中一项是配置队列的回调方法
SLAudioPlay#StartPlay()
(*pcmQue)->RegisterCallback(pcmQue, PcmCall, this);
在音频的播放过程中,会回调PcmCall方法来获取音频数据。
SLAudioPlay#PcmCall()
static void PcmCall(SLAndroidSimpleBufferQueueItf bf, void *contex)
{
SLAudioPlay *ap = (SLAudioPlay *)contex;
if(!ap)
{
XLOGE("PcmCall failed contex is NULL");
return;
}
ap->PlayCall((void *)bf);
}
而PcmCall方法会调用PlayCall()方法。在PlayCall方法中首先会获取存储在队列中的音频数据,再添加到音频播放器的播放队列中
SLAudioPlay#PlayCall()
void SLAudioPlay::PlayCall(void *bufq)
{
if(!bufq) return;
SLAndroidSimpleBufferQueueItf bf = (SLAndroidSimpleBufferQueueItf)(bufq);
//阻塞
XData d = GetData();
memcpy(buf, d.data, d.size);
mux.lock();
(*bf)->Enqueue(bf, buf, d.size);
mux.unlock();
d.Drop();
}
整个框架介绍结束。