播放器实战28 总结

本文详细介绍了基于Qt实现的多媒体播放器的内部工作流程,包括播放流程、多线程同步、错误控制、seek策略、音视频同步及设计模式的应用。在多线程处理中,强调了生产者-消费者模型在解封装线程和解码线程间的作用,以及信号量机制确保线程安全。此外,针对暂停后无法再次打开文件的bug,分析了死锁原因并提出了解决方案。
摘要由CSDN通过智能技术生成

至此,播放器的基本功能已经实现,进行一个总结:

一,仅进行播放时的函数调用流程

只做一个大致的梳理且不涉及seek等操作):
01.QT中的整个控件为QWidget类,Xplay2类为其继承,在main()中实例化xplay2类,调用xplay2的构造函数,该构造函数先调用了其基类的构造函数,绘制出一个没有画面的播放界面,
02.再调用解封装线程的start()即初始化:先构造解封装类,再构造视频线程和音频线程,随后开启解封装线程(调用解封装线程的run),视频线程,音频线程。
03.打开多媒体文件时,先调用xplay的open,将文件名与qt窗口类传给解封装线程,调用解封装线程的open,在解封装线程的open中调用视频线程的open与音频线程的open并将解封装获得的视频参数和音频参数传给对应的线程用来创建视频解码器与音频解码器:

re = vt->open(demux->CopyVPara(),call,demux->width,demux->height);

视频线程和音频线程的第一步就是调用各自的clear(),其做了以下事情:
01.释放对应解码器的参数
02.清空packet队列,释放每个packet的空间
是为了多次打开多媒体文件
04.在解封装线程的run中,读出packet,是视频流的packet的话则调用视频线程的push,将视频packet压入视频线程的等待解码队列中;若是音频流的packet的话则调用音频线程的push,将音频packet压入音频线程的等待解码队列中。
05.在视频线程的run中,如果条件满足(不是暂停状态,解码器能打开,pts满足播放条件),则pop等待解码队列,将弹出的packet发给解码器,然后将解码器解码出的frame通过OpenGL进行渲染,音频线程也类似,不过要经过重采样最后送到qt中去播放。
06.在关闭时,调用xplay2的析构,在xplay的析构中调用解封装线程的close(),在解封装线程的close()中分别调用视频线程的close与音频线程的close()
07.音频线程与视频线程都是继承的解码线程,close为虚函数,音频的close使用的是音频线程自己定义的close(),视频线程的close使用的是解码线程的close()。视频线程中调用解码器的close(),在音频的close()中除了调用解码器的close外还调用重采样的close(),再调用音频播放的close(),

二,对于多线程的处理

1.对于多个进程共享变量要加锁,即使是一句C++语句在汇编中也是很多句,若两个线程同时访问一个变量可能会造成问题。

void xdemuxthread::Close()
{
	isexit = true;
	wait();
	//vt与at是解封装线程来调用的,因为解封装线程要负责来清理vt与at
	if (vt) vt->Close();
	if (at) at->Close();
	mux.lock();
	delete vt;
	delete at;
	vt = NULL;
	at = NULL;
	mux.unlock();
}

比如解封装线程的close()函数,如果不加锁的话可能delete到一半又切换到其他线程对这个被delete的空间进行访问,程序就宕掉,弹出红色感叹号那种。
2.要进行差错控制,比如当pkt为空时怎么办,解码器没打开怎么办,不然程序会宕掉

3.一个占有锁的资源在循环中即使会解锁也要等待:
比如解封装线程里有个类似的代码:

while(1)
{
mux.lock();
if (!demux)
{
			mux.unlock();
			//若demux未打开则解锁让其他线程进来,等待5毫秒盼望demux能打开,然后再进行下次判断,判断demux是否打开
			msleep(5);
			continue;
		}
		}

若不sleep()即使会解锁,但for循环切换的很快,会立刻给加上锁,导致打开解封装的线程永远无法获取到锁,导致死锁

三,seek策略

01停下demux线程,video线程,audio线程
02清空packet队列
03seek到关键帧
04恢复所有线程并解码音视频,但不做播放
05解码到的pts<想要seek到的pts时将frame全部丢弃,直到>=时才开始音视频的渲染
06恢复正常播放状态

四,音视频同步策略

使用视频来同步音频,将音频的pts通过解码线程传给视频线程,当音频pts<视频pts时视频线程做等待,每次open一个新的媒体文件时,通过open函数将pts重置为0,
代码流程:
01.在xdecode类中添加pts成员,在receive()函数中获得一个frame时通过该frame的pts来更新xdecode类中的pts
在这里插入图片描述

五,用到的设计模式

比如工厂模式:因为可能由几种播放器:基于QT,基于DIRECXT,用xuadioplay类做为工厂然后返回不同的继承类对象,这里我们基于QT返回audioplay类。将audioplay类的实现放在xuadioplay.cpp中。

生产者消费者模式:对于解封装线程产生的packet,入队解码线程的队列,在解码线程中出队进行解码。使用一个队列解决了解封装线程与解码线程的耦合问题,也解决了解封装与解码速度不一样的矛盾问题,解封装速度大于解码速度,如果没有使用这个队列的话就每解出一个pkt就等着解码线程将其解码再解出下一个packet,这样影响效率,使用这个队列之后可以让解封装线程尽情地解出pkt,不用关心解码线程解码的情况。
在这里插入图片描述
为什么要使用生产者-消费者模型
在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完数据才能够继续生产数据,因为生产过多的数据可能会导致存储不足;同理如果消费者的速度大于生产者那么消费者就会经常处理等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式

简单来说,这里缓冲区的作用就是为了平衡生产者和消费者的数据处理能力,一方面起到缓存作用,另一方面达到解耦合作用。

生产者-消费者模型特点
保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据

当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒
参考:https://www.cnblogs.com/horacle/p/15425808.html
在这里插入图片描述
在我们这里的生产者就是解封装线程,消费者就是解码线程,Buffer就是std::list <AVPacket*> packs;
信号量机制:(packs.size() < maxList)

六,关于动态库静态库

在vs中切换成release模式,并在项目中添加头文件与库文件,在运行后生成.exe文件

首先介绍一下静态库(静态链接库)、动态库(动态链接库)的概念,首先两者都是代码共享的方式。

静态库:在链接步骤中,链接器将从库文件取得所需的代码,复制到生成的可执行文件中,这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。即静态库中的指令都全部被直接包含在最终生成的 EXE 文件中了。在vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件

动态库:动态链接库是一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。在vs中新建生成动态库的工程,编译成功后,产生一个.lib文件和一个.dll文件

那么上述静态库和动态库中的lib有什么区别呢?

静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中

动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供

总之,lib是链接时用到的,dll是运行时用到的。

Debug版本通常称为调试版本,通过编译选项的配合,编译的结果通常包含调试信息,可以设置断点、单步调试、使用TRACE/ASSERT等调试输出语句并且编译器不会对代码进行任何优化,可以使开发人员提供强大的应用程序调试能力。

Release版本通常称为发布版本,是为了用户的使用,一般发布版本上不允许进行调试,所以Release版本通常不包含调试信息,同时,它往往进行了各种优化,以期达到代码量最小和效率最高的目的。

Debug版本程序通常比Release版本程序要慢,尤其是在处理视频方面Release版本要比Debug版本快的多,在Release模式对程序进行调试时候经常会遇到变量虽然已经初始化,但是在查看其值的时候却发现是一个随机数而并不是已经初始化的值,有时候在对变量进行监视的时候,会出现找不到变量的情况,原因如下:

Debug根Release在初始化变量时所做的操作是不同的,Debug是将每个字节位都赋成0xcc,而Release的赋值近似于随机。如果程序中的某个变量没被初始化就被引用,就很有可能出现异常:用作控制变量将导致流程导向不一致;用作数组下标会使程序崩溃;更加可能是造成其他变量的不准确而引起其他的错误。所以在变量声明后马上对其初始化一个默认的值是最简单有效的办法。代码存在错误在debug方式下可能会被忽略而不被察觉到。Debug方式下数组越界再有的情况下也不会出错,但是在Release版本中就会暴露出来。

参考链接:https://blog.csdn.net/double_happiness/article/details/71122821

将release的exe文件加上一些dll文件就可以打包发给其他人进行播放器的使用了
在这里插入图片描述

八,解决暂停后无法再打开多媒体文件的bug

现象:
在暂停一个多媒体文件后如果点暂停然后再打开一个多媒体文件:
将卡在这里:
在这里插入图片描述
然后我们看下正常播放时终端出现的内容:
在这里插入图片描述
会发现是卡在<音频1>与<OPEN AUDIO CODEC/>之间,然后去定位这段代码:

re = at->open(demux->CopyAPara(), demux->sampletrate, demux->channels);

<音频1>出现在demux->CopyAPara()拷贝音频参数时判断流类型时,

bool xaudiothread::open(AVCodecParameters* par,int samplerate,int channels)
{
	Clear();
	//重开了一个音频,不受上一次的影响
	
	pts = 0;
	//传进来的par需要做为解码,重采样的构造函数的参数,但这些功能的open中会清理掉par
	//因此为这些open加一个参数,判断是否要清理,这些就选择不清理,由该代码最后调用清理
	if (!par)return false;
	
	amux.lock();
	
	if (!decode)decode = new xdecode();
	
	if (!resample)resample = new xresample();
	
	if (!audioplay)audioplay = xaudioplay::get();
	
	bool re=true;
	if (!decode->open(par, false))

<OPEN AUDIO CODEC/>出现在decode->open(par, false)中,因此卡住的代码出现在上面,然后再定位具体出现在哪一句,在这一段代码中各行位置加入cout << “2022” << endl;发现加在amux.lock();前可以显示在终端,之后显示不在终端,于是定位问题出在了amux.lock();因此初步推测是出现了死锁。
于是查看amux的所有引用:
在这里插入图片描述
发现除了音频线程的open()就是音频线程的run()中大量使用,为了定位是哪里没有释放锁,在加锁前显示<上锁>,在解锁后显示<解锁>:

void xaudiothread::run()
{
	cout << "开始音频线程" << endl;
	unsigned char* pcm = new unsigned char[1024 * 1024 * 10];
	while (!isexit)
	{
		cout << "上锁!!!!!!!" << endl;
		amux.lock();
		if (isPause)
		{
			amux.unlock();
			cout << "锁已释放!!!!!!!" << endl;
			msleep(5);
			continue;
		}
		if (!decode)
		{
			amux.unlock();
			cout << "锁已释放!!!!!!!" << endl;
			msleep(1);
			continue;
		}
		if ( !resample || !audioplay)
		{	
			amux.unlock();
			cout << "锁已释放!!!!!!!" << endl;
			msleep(1);
			continue;
		}
		AVPacket* pkt = pop();
		bool re = decode->send(pkt);
		if (!re)
		{
			amux.unlock();
			cout << "锁已释放!!!!!!!" << endl;
			msleep(1);
			continue;
		}
		//cout << "adecode->pts" << decode->pts << endl;
		
		//可能一次send多次receive
		while (!isexit)
		{
			AVFrame* frame = decode->receive();
			if (!frame)break;
			//adecode中的pts为当前frame的总pts,所有当前音频的pts的该frame的总pts减去缓冲中剩余pts
			pts = decode->pts-audioplay->GetNoPlayMs();
			//cout << "audio pts:" << pts << endl;
			int size = resample->Resample(frame, pcm);//Resample中会释放frame
			while (!isexit)
			{
				if (size <= 0)break;
				//当缓冲中空余的空间小于需要使用的空间时等待一秒再continue(就不往播放器里写音频数据了)
				if (audioplay->Getfree() < size || isPause)
				{
					msleep(1);
					continue;
				}
				audioplay->write(pcm, size);
				break;
			}
		}
		amux.unlock();
		//msleep(3);
		cout << "锁已释放!!!!!!!" << endl;
	}
	delete []pcm;
}

然后点开一个视频文件之后点暂停果然发现最后锁一直在run中:
在这里插入图片描述
此时音频线程的open()拿不到锁一直阻塞住,造成了死锁。
解决方法一:
在run中的大循坏最后解锁后msleep(3);一会,然后open()有充足的时间拿到锁,但这种方法比较low
解决方法二:
由于是暂停之后会造成的死锁,于是去查看run中暂停的代码能不能让run把锁释放掉:

if (isPause)
		{
			amux.unlock();
			cout << "锁已释放!!!!!!!" << endl;
			msleep(5);
			continue;
		}

将其余释放锁前的cout << “锁已释放!!!!!!!” << endl;删去,再暂停试试看:
在这里插入图片描述
果然发现没有释放掉,于是去查看引起isPause变化的代码:

void xaudiothread::SetPause(bool isPause)
{
	//amux.lock();
	this->isPause = isPause;
	if (audioplay)
		audioplay->SetPause(isPause);
	//amux.unlock();
}

发现没有加锁,将会导致isPause变量还未真正改变CPU就被调度去执行其他代码,于是导致run()中始终保持上锁的状态,于是open()一直拿不到锁,于是给SetPause()中的代码上锁保证其能整个执行完,于是解决了问题
用RAII更方便:

unique_lock<mutex> lock(amux);
	this->isPause = isPause;
	if (audioplay)
		audioplay->SetPause(isPause);

播放器实战到此告一段落,感谢夏曹俊老师的课程:
https://edu.csdn.net/course/detail/3300
虽然播放器比较简陋,实际使用感受与主流播放器相去甚远,但收获颇多,获益匪浅。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值