基于Qt、FFMpeg的音视频播放器设计五(FFMpeg音频实现)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hfuu1504011020/article/details/82722731

对于音频部分,主要从以下几个部分实现。

1、音频播放的启动、停止、暂停、缓冲写入接口实现

2、ffmpeg音频解码器打开和音频解码

3、ffmpeg音频重采样标准化音频的输出格式

4、多线程和缓冲队列实现音视频同步播放

一、音频播放的启动和停止接口实现

首先我们创建一个音频播放类XAudioPlay,使用单例模式,提供一个接口,在XAudioPlay.cpp中对此接口重新实现,申明如下。

#pragma once
class XAudioPlay
{
public:
	XAudioPlay *Get();//单例模式
	virtual bool Start()=0;//启动
	virtual void  Play(bool isplay)=0;//暂停
	virtual bool Write(const char *data,int datasize) = 0;//将音频写入
	virtual void Stop()=0;//停止
	virtual int GetFree() = 0;//获取剩余空间
	virtual ~XAudioPlay();
	int sampleRate = 48000;//样本率
	int sampleSize = 16;//样本大小
	int channel = 2;///通道数
protected:
	XAudioPlay();
};

对启动、停止、暂停、缓冲写入函数的定义如下。

#include "XAudioPlay.h"
#include <QAudioOutput>
#include<QMutex>
class CXAudioPlay :public XAudioPlay
{
public:
	
	QAudioOutput *output = NULL;
	QIODevice *io = NULL;
	QMutex mutex;
	void Stop()
	{
		mutex.lock();
		if (output)//为打开AudioOutput
		{
			output->stop();
			delete output;
			output = NULL;
			io = NULL;
		}
		mutex.unlock();
	}

	//首先设置播放的格式以及参数
	bool Start()
	{
		Stop();
		mutex.lock();
		QAudioOutput *out;//播放音频
		QAudioFormat fmt;//设置音频输出格式
		fmt.setSampleRate(48000);//1秒的音频采样率
		fmt.setSampleSize(16);//声音样本的大小
		fmt.setChannelCount(2);//声道
		fmt.setCodec("audio/pcm");//解码格式
		fmt.setByteOrder(QAudioFormat::LittleEndian);
		fmt.setSampleType(QAudioFormat::UnSignedInt);//设置音频类型
		output = new QAudioOutput(fmt);
		io = output->start();//播放开始
		mutex.unlock();
		return true;
	}
	
	void  Play(bool isplay)
	{
		mutex.lock();
		if (!output)
		{
			mutex.unlock();
			return;
		}

		if (isplay)
		{
			output->resume();//恢复播放
		}
		else
		{
			output->suspend();//暂停播放
		}
		mutex.unlock();
	}


	int GetFree()
	{
		mutex.lock();
		if (!output)
		{
			mutex.unlock();
			return 0;
		}
		int free = output->bytesFree();//剩余的空间

		mutex.unlock();

		return free;
	}

	bool Write(const char *data, int datasize)
	{
		mutex.lock();
		if (io)
		   io->write(data, datasize);//将获取的音频写入到缓冲区中
		mutex.unlock();
		return true;
	}
};

XAudioPlay::XAudioPlay()
{
}


XAudioPlay::~XAudioPlay()
{
}

XAudioPlay * XAudioPlay::Get()
{
	static CXAudioPlay ap;
	return &ap;

}

这里在我们缓冲区写入时,我们先得保证缓冲区有空间让我们写入,所以这里的GetFree()函数用来检测当前缓冲区大小。

二、ffmpeg音频解码器打开和音频解码

之前我们在XFFmpeg.cpp中Open视频时,我们只对视频处理了,音频并未处理,现在我们在Open中进行音频处理,先打开音频解码器,在.h中定义

audioStream//音频流

int sampleRate = 48000;//样本率
int sampleSize = 16;//样本大小
int channel = 2;///通道数

在XFFmpeg.cpp中在判断视频流后面加入判断音频流如下:

else if (enc->codec_type == AVMEDIA_TYPE_AUDIO)//若未音频流
		{
			audioStream = i;//音频流
			AVCodec *codec = avcodec_find_decoder(enc->codec_id);//查找解码器
			if (avcodec_open2(enc, codec, NULL) < 0)
			{
				mutex.unlock();
				return 0;
			}
			this->sampleRate = enc->sample_rate;//样本率
			this->channel = enc->channels;//通道数
			switch (enc->sample_fmt)//样本大小
			{
			case AV_SAMPLE_FMT_S16://signed 16 bits
				this->sampleSize = 16;
				break;
			case  AV_SAMPLE_FMT_S32://signed 32 bits
				this->sampleSize = 32;
			default:
				break;
			}
			printf("audio sample rate:%d sample size:%d chanle:%d\n",this->sampleRate,this->sampleSize,this->channel);

		}

现在我们考虑一下音频解码,在XFFmpeg.cpp的Decode()的函数中,有avcodec_send_packet()函数用来发送数据包,是用同一个接口发送音频和视频数据的,那就会有一个问题,在我们后面使用解码后音频和视频是否可以存放在一起,因为后面我们需要做音频、视频得同步,也就是说音频和视频需要多线程播放的,这样在同步播放时会有一些问题。所以这里给音频再增加一块内存。首先在.h中增加解码后存放音频的申明,现在又有一个问题,之前我们的播放是以视频为准的,可以通过视频控制播放速度,在增加音频后,我们的视频依旧可以适应音频播放,但音频尽量不要适应视频播放,因为一旦控制了音频的播放速度,就可能导致音频的失真。在XFFmpeg..h中我们存了一个pts,所以这里在解码后是音频时,我们处理这个pts,为视频时不改变它,那么视频又如何跟上音频呢,这里我们需要改写AVFrame * XFFmpeg::Decode(const AVPacket *pkt),之前我们返回的是解码后的视频帧,现在我们返回显示时间。

int XFFmpeg::Decode(const AVPacket *pkt)
{
	mutex.lock();
	if (!ic)//若未打开视频
	{
		mutex.unlock();
		return NULL;

	}
	if (yuv == NULL)//申请解码的对象空间
	{
		yuv = av_frame_alloc();
	}
	if (pcm == NULL)
	{
		pcm = av_frame_alloc();
	}
	AVFrame *frame = yuv;//此时的frame是解码后的视频流
	if (pkt->stream_index == audioStream)//若未音频
	{
		frame = pcm;//此时frame是解码后的音频流
	}
	int re = avcodec_send_packet(ic->streams[pkt->stream_index]->codec, pkt);//发送之前读取的pkt
	if (re != 0)
	{
		mutex.unlock();
		return NULL;
	}
	re = avcodec_receive_frame(ic->streams[pkt->stream_index]->codec, frame);//解码pkt后存入yuv中
	if (re != 0)
	{
		mutex.unlock();
		return NULL;
	}
	qDebug() << "pts=" << frame->pts;
	
	mutex.unlock();
    int p = frame->pts*r2d(ic->streams[pkt->stream_index]->time_base);//当前解码的显示时间
	if (pkt->stream_index == audioStream)//为音频流时设置pts
		this->pts = p;
	return p;
}

三、ffmpeg音频重采样标准化音频的输出格式、以及视音播放

音频的重采样和视频得转化有很多相同之处,我们在.h中申明int ToPCM(char *out);//音频的重采样,以及SwrContext *aCtx = NULL;//音频重采样上下文,在.cpp中定义如下

int XFFmpeg::ToPCM(char *out)
{
	mutex.lock();
	if (!ic || !pcm || !out)//文件未打开,解码器未打开,无数据
	{
		mutex.unlock();
		return 0;
	}
	AVCodecContext *ctx = ic->streams[audioStream]->codec;//音频解码器上下文
	if (aCtx == NULL)
	{
		aCtx = swr_alloc();//初始化
		swr_alloc_set_opts(aCtx,ctx->channel_layout,
			AV_SAMPLE_FMT_S16,
			  ctx->sample_rate,
			  ctx->channels,
			  ctx->sample_fmt,
			  ctx->sample_rate,
			  0,0
			  );
		swr_init(aCtx);
	}
	uint8_t  *data[1];
	data[0] = (uint8_t *)out;

    //音频的重采样过程
	int len = swr_convert(aCtx, data, 10000,
		(const uint8_t **)pcm->data,
		pcm->nb_samples
		);
	if (len <= 0)
	{
		mutex.unlock();
		return 0;
	}
	int outsize = av_samples_get_buffer_size(NULL, ctx->channels,
		pcm->nb_samples,
		AV_SAMPLE_FMT_S16,
		0);

	mutex.unlock();
	return outsize;
}

在ffmpeg里面使用av_sample_get_buffer_size来计算音频占用的字节数的。现在我们来实现音视频的播放,首先在aginexplay.cpp中的open函数中对音频重采样的采样率、通道数、采样大小进行设置,我们这里使用原视频中的数据,然后启动音视频的重采样。如下。

void agineXplay::open()
{
	QString name = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("选择视频文件"));//打开视频文件
	if (name.isEmpty())
		return;
	this->setWindowTitle(name);//设置窗口的标题
	int totalMs = XFFmpeg::Get()->Open(name.toLocal8Bit());//获取视频总时间
	if (totalMs <= 0)//未打开成功
	{
		QMessageBox::information(this, "err", "file open failed!");//弹出错误窗口
		return;
	}
	XAudioPlay::Get()->sampleRate = XFFmpeg::Get()->sampleRate;
	XAudioPlay::Get()->channel = XFFmpeg::Get()->channel;
	XAudioPlay::Get()->sampleSize = 16;
	XAudioPlay::Get()->Start();//启动音频
	char buf[1024] = { 0 };//用来存放总时间
	int min = (totalMs) / 60;
	int sec = (totalMs) % 60;
	sprintf(buf, "%03d:%02d", min, sec);//存入buf中
	ui.totaltime->setText(buf);//显示在界面中
	play();
}

,设置好音频的重采样的格式后,现在我们需要达到播放音频的效果,考虑音频的播放要求高于视频每帧的播放,我们在XVideoThread.cpp中进行如下修改。

void XVideoThread::run()
{
	char out[10000] = {0};
	while (!isexit)//线程未退出
	{
		if (!XFFmpeg::Get()->isPlay)//如果为暂停状态,不处理
		{
			msleep(10);
			continue;
		}
		int free = XAudioPlay::Get()->GetFree();//此时缓冲区的空间大小
		if (free < 10000)
		{
			msleep(1);
			continue;
		}
		AVPacket pkt = XFFmpeg::Get()->Read();
		if (pkt.size <= 0)//未打开视频
		{
			msleep(10);
			continue;
		}
		if (pkt.stream_index == XFFmpeg::Get()->audioStream)
		{
			XFFmpeg::Get()->Decode(&pkt);//解码音频
			av_packet_unref(&pkt);//释放pkt包
			int len = XFFmpeg::Get()->ToPCM(out);//重采样音频
			XAudioPlay::Get()->Write(out, len);//写入音频
			continue;
		}
		XFFmpeg::Get()->Decode(&pkt);//解码视频帧
		av_packet_unref(&pkt); 
	}

}

其中这里的GetFree()函数是确保在我们在写入音频时,此时它的缓冲区还能存放下该帧音频,避免后面音频读取出错。

四、多线程和缓冲队列实现音视频同步播放

上面所用到的在我播放本地视频时不会有什么问题,但是当我们在播放网络视频时可能就会出现音视频不同步,声音错落的现象,基本上经常出现的是视频跟不上音频的状况,有时也会出现音频跟不上视频得现象,这样我们没办法通常只能减少视频得帧画面来降低视频的播放。这里我们如何来同步音视频呢,一种方法就是我们采用一个List链表,将每次AVPacket到音频前的视频帧存放进去,在我们读取到音频解码时,同时将List链表里所有的视频帧解码出来进行播放,保证了音视频的播放,如何处理呢?首先我们需要解码后音视频的Pts,之前在XFFMpeg.cpp中有个Decode()函数,我们已经进行了修改返回的是解码后的pts,所以这里我们在XVideoThread.cpp中当读取到为音频是,记录下它的pts,再处理list链表中的AVPacket的视频帧进行解码,具体过程下方有详细说明。

#include "XVideoThread.h"
#include "XFFmpeg.h"
#include "XAudioPlay.h"
#include <list>
using namespace std;

static list<AVPacket> videos;//用来存放解码前的视频帧
bool isexit = false;//线程未退出
static int apts = -1;//音频的pts
XVideoThread::XVideoThread()
{
}


XVideoThread::~XVideoThread()
{
}

void XVideoThread::run()
{
	char out[10000] = {0};
	while (!isexit)//线程未退出
	{
		if (!XFFmpeg::Get()->isPlay)//如果为暂停状态,不处理
		{
			msleep(10);
			continue;
		}
		while (videos.size()>0)//确定list中是否有AVpacket包
		{
			AVPacket pack = videos.front();//每次取出list中的第一个AVPack包
			int pts = XFFmpeg::Get()->GetPts(&pack);//获得该包的pts
			if (pts > apts)//若视屏包大于音频包的pts,结束
			{
				break;
			}
	 		XFFmpeg::Get()->Decode(&pack);//解码视频帧
			av_packet_unref(&pack);//清理该AVPacket包
			videos.pop_front();//从list链表中删除
		}

		int free = XAudioPlay::Get()->GetFree();//此时缓冲区的空间大小
		if (free < 10000)
		{
			msleep(1);
			continue;
		}
		AVPacket pkt = XFFmpeg::Get()->Read();
		if (pkt.size <= 0)//未打开视频
		{
			msleep(10);
			continue;
		}
		if (pkt.stream_index == XFFmpeg::Get()->audioStream)
		{
			apts = XFFmpeg::Get()->Decode(&pkt);//解码音频
			av_packet_unref(&pkt);//释放pkt包S
			int len = XFFmpeg::Get()->ToPCM(out);//重采样音频
			XAudioPlay::Get()->Write(out, len);//写入音频
			continue;
		}
		videos.push_back(pkt);
	}


}

 

此时无论是本地视频还是网络视频我们播放时都可以完成音视频的同步问题,至此音频的处理也已完毕,整个音视频播放的也已结束,其中涉及到的内容有如下几点:

FFMpeg,Qt带有的QOpenGL绘制视频(后面有用OpenGL通过GPU间接绘制视频的代码链接)、多线程实现音频视频得读取、解码以及音频的重采样,而视频的转码绘制单独在VideoWidget.cpp中实现.

效果图:

效果图代码链接:https://download.csdn.net/download/hfuu1504011020/10672140

 

 

 

猜你喜欢

转载自blog.csdn.net/hfuu1504011020/article/details/82722731