播放器实战20 音频线程与视频线程

本文介绍了音频线程与视频线程的设计,包括xaudiothread和xvideothread类,展示了如何通过解封装模块调用音频解码、重采样和播放功能,以及使用生产者消费者模式管理音频包和视频帧。重点讲解了线程初始化、数据推送与处理流程,以及为何使用抽象类ivideocall以解耦视频线程和播放控制组件。
摘要由CSDN通过智能技术生成

在这里插入图片描述
以音频线程为例,解封装模块调用音频线程模块,音频线程模块模块将声音播放模块,解码模块,重采样模块结合起来,用音频线程模块去调用其他模块

1.xaudiothread.h

#include<qthread.h>
#include"xdecode.h"
#include"xaudioplay.h"
#include"xresample.h"
#include<mutex>
class QThread;
struct AVCodecParameter;
class xaudiothread:public QThread
{
public:
	xaudiothread();
	virtual ~xaudiothread();
	void run();
	virtual bool open(AVCodecParameters* par, int samplerate, int channels);
	virtual void push(AVPacket* pkt);
	int maxlist = 100;//防止队列中packet没有被读取造成内存泄漏
	bool isexit = false;//控制线程退出

protected:
	std::mutex mux;
	std::list<AVPacket*>packs;//生产者消费者模式:调用者生产packet,在队列中消费packet
	xdecode* adecode=0;
	xaudioplay* audioplay=0;
	xresample* resample=0;
};

2.virtual bool open(AVCodecParameters* par, int samplerate, int channels);

使用该函数时使用 at->open(demux->CopyAPara(), demux->sampletrate, demux->channels),第一个参数即为AVCodecParameters* 类型,该函数做为解封装线程与音频线程的桥梁

bool xaudiothread::open(AVCodecParameters* par,int samplerate,int channels)
{

	if (!par)return false;
	mux.lock();
	if (!adecode)adecode = new xdecode();
	if (!resample)resample = new xresample();
	if (!audioplay)audioplay = xaudioplay::get();
	bool re=true;
	if (!adecode->open(par, false))
	{
		cout << "音频解码打开失败" << endl;
		re = false;
	}
	if (!resample->Open(par, false))
	{
		cout << "重采样打开失败" << endl;
		re = false;
	}
	audioplay->samplerate = samplerate;
	cout << "audioplay->samplerate" << audioplay->samplerate << endl;
	audioplay->channels = channels;
	cout << "audioplay->samplesize" << audioplay->samplesize << endl;
	if (!audioplay->open())
	{
		cout << "音频播放打开失败" << endl;
		re = false;
	}
	avcodec_parameters_free(&par);
	mux.unlock();
	cout << "xaudiothread打开(1成功0失败):" <<re<< endl;
	return re;

传进来的par需要作为解码,重采样的构造函数的参数,但这些功能的open中会清理掉par
因此为这些open加一个参数,判断是否要清理,这里就选择不清理,由该代码最后调用清理:avcodec_parameters_free(&par);
因此当某个功能没有打开时,不需要解锁,也不需要有个返回值

3.~xaudiothread()

xaudiothread::~xaudiothread()
{
	//等待线程退出
	isexit = true;
	wait();
	cout << "音频线程退出" << endl;
}

当释放该线程类的时候,将bool型成员isexit设为true,通过这样来中断run()和push()

4.virtual void push(AVPacket* pkt);

int maxlist = 100;//防止队列中packet没有被读取造成内存泄漏

使用生产者消费者模式的思想

void xaudiothread::push(AVPacket* pkt)
{
	if(!pkt)return;
	while (!isexit)
	{
		mux.lock();
		if (packs.size() < maxlist)
		{
			packs.push_back(pkt);
			cout << "push" << endl;
			mux.unlock();
			break;
		}
		mux.unlock();
		msleep(1);
	}
}

读取速度高于解码速度,队列经常放满,因此还未放进队列的pakcet不能丢弃,这里我们采用msleep(1)等待一秒再进入下次循环并判断,只有当队列packs的长度小于最大长度时,才往队列里面塞pkt,塞一次之后就break出循环。

生产者消费者模式:
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
在这里插入图片描述
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
详细:https://blog.csdn.net/u011109589/article/details/80519863

5.run()

void xaudiothread::run()
{
	cout << "开始音频线程" << endl;
	unsigned char* pcm = new unsigned char[1024 * 1024 * 10];
	while (!isexit)
	{
		mux.lock();
		if (packs.empty() || !adecode || !resample || !audioplay)
		{
			mux.unlock();
			msleep(1);
			continue;
		}
		AVPacket* pkt = packs.front();
		packs.pop_front();
		bool re = adecode->send(pkt);
		if (!re)
		{
			mux.unlock();
			msleep(1);
			continue;
		}
		while (!isexit)
		{
			AVFrame* frame = adecode->receive();
			if (!frame)break;
			int size = resample->Resample(frame, pcm);//Resample中会释放frame
			while (!isexit)
			{
				if (size <= 0)break;
				if (audioplay->Getfree() < size)
				{
					msleep(1);
					continue;
				}
				audioplay->write(pcm, size);
				break;
			}
		}
		mux.unlock();
	}
	delete pcm;
}

1.当isexit为false时,进入循环,队列为空或者没有初始化成功则解锁该线程,等待数据或者初始化,等1秒再判断一次
2.当队列非空并且初始化成功之后,从队列头部弹出一个packet发给解码线程
3. 可能一次send多次receive,因此一次send之后当调用析构函数使isexit=false之后或者 adecode->receive()将解码队列读干净后才进入到下次send()
4. 对接收到的frame进行重采样得到pcm,并获得重采样后的大小size,当播放器中空余空间大于size时将pcm进行播放

6.xvideothread

在这里插入图片描述

与音频线程类似:

xvideothread.h

#pragma once
#include<qthread.h>
#include"xvideowidget.h"
#include"xvideothread.h"
#include"xdecode.h"
#include<mutex>
#include"ivideocall.h"
class QThread;
class xvideothread:public QThread
{
public:
	xvideothread();
	virtual ~xvideothread();
	virtual bool open(AVCodecParameters*par,ivideocall*call,int width,int height);
	void run();
	virtual bool push(AVPacket* pkt);
	int maxlist = 100;
	bool isexit = false;
protected:
	xdecode* vdecode=0;
	xvideowidget* video=0;
	ivideocall* call = 0;
	std::list<AVPacket*>packs;
	std::mutex  mux;
};

增加了videocall* call = 0;

class ivideocall
{
public:
	virtual void Init(int width,int height) = 0;
	virtual void Repaint(AVFrame*)=0;
};

该类只有纯虚函数,为抽象类,让播放控件xvideowidget 继承,因此这两个纯虚函数的实现靠的是xvideowidget中的函数定义,此时可以用父类指针(call)来调用子类(xvideowidget)中 的方法:
call->Init(width, height);,call->Repaint(frame);
为什么这么做,而不是直接将xvideowidget做为参数?
原因:

int main(int argc, char *argv[])
{
    testtread tt;
    

    QApplication a(argc, argv);
    Xplay2 w;
    w.show();

    //w.ui.openGLWidget->Init(tt.demux.width, tt.demux.height);

    tt.video = w.ui.openGLWidget;
    tt.init();
    tt.start();
    return a.exec();
}

原本是先通过视频线程来初始化控件openGLWidget,然而视频线程的初始化也需要控件OpenGLwidget(不然视频没有地方显示),为了解决这个矛盾,将控件openglwidget的初始化放在视频线程中,将待初始化的控件做为参数传递进去,因为传进去的时候还没有初始化(实例化)该控件,所以将其抽象类ivideocall传进去

xvideothread.cpp

#include "xvideothread.h"
#include<iostream>
using namespace std;
xvideothread::xvideothread()
{

}
xvideothread::~xvideothread()
{	//等待线程退出
	isexit = true;
	wait();
	cout << "视频线程退出" << endl;
}
bool xvideothread::open(AVCodecParameters* par, ivideocall* call, int width, int height)
{
	if (!par)return false;
	//初始化显示窗口
	this->call = call;
	if(call)
	{
		call->Init(width, height);
	}
	bool re = true;

	//打开解码器
	if (!vdecode)vdecode = new xdecode();
	if (!vdecode->open(par))
	{
		cout << "video decode open failed!" << endl;
		re = false;
	}
	cout << "xvideothread open:(0失败1成功)" << re << endl;
	return re;
}
bool xvideothread::push(AVPacket* pkt)
{
	if (!pkt)return false;
	while (!isexit)
	{
		mux.lock();
		if (packs.size() < maxlist)
		{
			packs.push_back(pkt);
			cout << "push" << endl;
			mux.unlock();
			break;
		}
		mux.unlock();
		msleep(1);
	}

}
void xvideothread::run()
{
	cout << "开始视频线程" << endl;
	while (!isexit)
	{
		mux.lock();
		//没数据或者没有初始化成功则解锁该线程,等待数据或者初始化,等1秒再判断一次
		if (packs.empty() || !vdecode)
		{
			mux.unlock();
			msleep(1);
			continue;
		}
		AVPacket* pkt = packs.front();
		packs.pop_front();
		bool re = vdecode->send(pkt);
		if (!re)
		{
			mux.unlock();
			msleep(1);
			continue;
		}
		//可能一次send多次receive
		while (!isexit)
		{
			AVFrame* frame = vdecode->receive();
			if (!frame)break;
			if(call)
			{
				call->Repaint(frame);
			}
		}
		mux.unlock();
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值