1.前言
由于QAudioOutput支持的输入数据必须是原始数据,所以播放mp3,WAV,AAC等格式文件,需要解封装后才能支持播放.
而在QT中,提供了QMediaPlayer类可以支持解封装,但是该类的解码协议都是基于平台的,如果平台自身无法播放,那么QMediaPlayer也无法播放.有兴趣的朋友可以去试试.
所以接下来,我们使用ffmpeg+QAudioOutput来实现一个简单的音频播放器.
2.界面展示
因为业余爱好,只是简单实现了大部分功能,支持播放、暂停、恢复、换歌、播放进度调节,如下图所示:
3.效果展示
4.代码流程
首先创建一个playthread线程类,然后在线程中,不断解数据,重采样,并输入到QAudioOutput的缓冲区进行播放.以及处理界面发来的命令
然后创建一个Widget界面类,通过用户操作,向playthread线程类发送控制命令.然后在playthread线程类中处理命令,命令有以下这些:
4.1 playthread线程类
在playthread线程类中,最核心的函数是runPlay(),该函数就是在不断的不断解数据,重采样,并输入到QAudioOutput的缓冲区进行播放.
playtherad.cpp如下所示:
#include "playthread.h"
playthread::playthread()
{
audio=NULL;
type = control_none;
}
bool playthread::initAudio(int SampleRate)
{
QAudioFormat format;
if(audio!=NULL)
return true;
format.setSampleRate(SampleRate); //设置采样率
format.setChannelCount(2); //设置通道数
format.setSampleSize(16); //样本数据16位
format.setCodec("audio/pcm"); //播出格式为pcm格式
format.setByteOrder(QAudioFormat::LittleEndian); //默认小端模式
format.setSampleType(QAudioFormat::UnSignedInt); //无符号整形数
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); //选择默认输出设备
// foreach(int count,info.supportedChannelCounts())
// {
// qDebug()<<"输出设备支持的通道数:"<<count;
// }
// foreach(int count,info.supportedSampleRates())
// {
// qDebug()<<"输出设备支持的采样率:"<<count;
// }
// foreach(int count,info.supportedSampleSizes())
// {
// qDebug()<<"输出设备支持的样本数据位数:"<<count;
// }
if (!info.isFormatSupported(format))
{
qDebug()<<"输出设备不支持该格式,不能播放音频";
return false;
}
audio = new QAudioOutput(format, this);
audio->setBufferSize(100000);
return true;
}
void playthread::play(QString filePath)
{
this->filePath = filePath;
type = control_play;
if(!this->isRunning())
{
this->start();
}
}
void playthread::stop()
{
if(this->isRunning())
{
type = control_stop;
}
}
void playthread::pause()
{
if(this->isRunning())
{
type = control_pause;
}
}
void playthread::resume()
{
if(this->isRunning())
{
type = control_resume;
}
}
void playthread::seek(int value)
{
if(this->isRunning())
{
seekMs = value;
type = control_seek;
}
}
void playthread::debugErr(QString prefix, int err) //根据错误编号获取错误信息并打印
{
char errbuf[512]={0};
av_strerror(err,errbuf,sizeof(errbuf));
qDebug()<<prefix<<":"<<errbuf;
emit ERROR(prefix+":"+errbuf);
}
bool playthread::runIsBreak() //处理控制,判断是否需要停止
{
bool ret = false;
//处理播放暂停
if(type == control_pause)
{
while(type == control_pause)
{
audio->suspend();
msleep(500);
}
if(type == control_resume)
{
audio->resume();
}
}
if(type == control_play) //重新播放
{
ret = true;
if(audio->state()== QAudio::ActiveState)
audio->stop();
}
if(type == control_stop) //停止
{
ret = true;
if(audio->state()== QAudio::ActiveState)
audio->stop();
}
return ret;
}
void playthread::runPlay()
{
int ret;
int destMs,currentMs;
if(audio==NULL)
{
emit ERROR("输出设备不支持该格式,不能播放音频");
return ;
}
//初始化网络库 (可以打开rtsp rtmp http 协议的流媒体视频)
avformat_network_init();
AVFormatContext *pFmtCtx=NULL;
ret = avformat_open_input(&pFmtCtx, this->filePath.toLocal8Bit().data(),NULL, NULL) ; //打开音视频文件并创建AVFormatContext结构体以及初始化.
if (ret!= 0)
{
debugErr("avformat_open_input",ret);
return ;
}
ret = avformat_find_stream_info(pFmtCtx, NULL); //初始化流信息
if (ret!= 0)
{
debugErr("avformat_find_stream_info",ret);
return ;
}
int audioindex=-1;
audioindex = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
qDebug()<<"audioindex:"<<audioindex;
AVCodec *acodec = avcodec_find_decoder(pFmtCtx->streams[audioindex]->codecpar->codec_id);//获取codec
AVCodecContext *acodecCtx = avcodec_alloc_context3(acodec); //构造AVCodecContext ,并将vcodec填入AVCodecContext中
avcodec_parameters_to_context(acodecCtx, pFmtCtx->streams[audioindex]->codecpar); //初始化AVCodecContext
ret = avcodec_open2(acodecCtx, NULL,NULL); //打开解码器,由于之前调用avcodec_alloc_context3(vcodec)初始化了vc,那么codec(第2个参数)可以填NULL
if (ret!= 0)
{
debugErr("avcodec_open2",ret);
return ;
}
SwrContext *swrctx =NULL;
swrctx=swr_alloc_set_opts(swrctx, av_get_default_channel_layout(2),AV_SAMPLE_FMT_S16,44100,
acodecCtx->channel_layout, acodecCtx->sample_fmt,acodecCtx->sample_rate, NULL,NULL);
swr_init(swrctx);
destMs = av_q2d(pFmtCtx->streams[audioindex]->time_base)*1000*pFmtCtx->streams[audioindex]->duration;
qDebug()<<"码率:"<<acodecCtx->bit_rate;
qDebug()<<"格式:"<<acodecCtx->sample_fmt;
qDebug()<<"通道:"<<acodecCtx->channels;
qDebug()<<"采样率:"<<acodecCtx->sample_rate;
qDebug()<<"时长:"<<destMs;
qDebug()<<"解码器:"<<acodec->name;
AVPacket * packet =av_packet_alloc();
AVFrame *frame =av_frame_alloc();
audio->stop();
QIODevice*io = audio->start();
while(1)
{
if(runIsBreak())
break;
if(type == control_seek)
{
av_seek_frame(pFmtCtx, audioindex, seekMs/(double)1000/av_q2d(pFmtCtx->streams[audioindex]->time_base),AVSEEK_FLAG_BACKWARD);
type = control_none;
emit seekOk();
}
ret = av_read_frame(pFmtCtx, packet);
if (ret!= 0)
{
debugErr("av_read_frame",ret);
emit duration(destMs,destMs);
break ;
}
//解码一帧数据
ret = avcodec_send_packet(acodecCtx, packet);
av_packet_unref(packet);
if (ret != 0)
{
debugErr("avcodec_send_packet",ret);
continue ;
}
if(packet->stream_index==audioindex)
{
while( avcodec_receive_frame(acodecCtx, frame) == 0)
{
if(runIsBreak())
break;
uint8_t *data[2] = { 0 };
int byteCnt=frame->nb_samples * 2 * 2;
unsigned char *pcm = new uint8_t[byteCnt]; //frame->nb_samples*2*2表示分配样本数据量*两通道*每通道2字节大小
data[0] = pcm; //输出格式为AV_SAMPLE_FMT_S16(packet类型),所以转换后的LR两通道都存在data[0]中
ret = swr_convert(swrctx,
data, frame->nb_samples, //输出
(const uint8_t**)frame->data,frame->nb_samples ); //输入
//将重采样后的data数据发送到输出设备,进行播放
while (audio->bytesFree() < byteCnt)
{
if(runIsBreak())
break;
msleep(10);
}
if(!runIsBreak())
io->write((const char *)pcm,byteCnt);
currentMs = av_q2d(pFmtCtx->streams[audioindex]->time_base)*1000*frame->pts;
//qDebug()<<"时长:"<<destMs<<currentMs;
emit duration(currentMs,destMs);
delete[] pcm;
}
}
}
//释放内存
av_frame_free(&frame);
av_packet_free(&packet);
swr_free(&swrctx);
avcodec_free_context(&acodecCtx);
avformat_close_input(&pFmtCtx);
}
void playthread::run()
{
if(!initAudio(44100))
{
emit ERROR("输出设备不支持该格式,不能播放音频");
}
while(1)
{
switch(type)
{
case control_none: msleep(100); break;
case control_play : type=control_none;runPlay(); break; //播放
default: type=control_none; break;
}
}
}
4.2 widget界面类
而在界面中要处理的就很简单,widget.cpp如下所示:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setAcceptDrops(true);
thread = new playthread();
connect(thread,SIGNAL(duration(int,int)),this,SLOT(onDuration(int,int)));
connect(thread,SIGNAL(seekOk()),this,SLOT(onSeekOk()));
void duration(long currentMs,long destMs); //播放时长
thread->start();
sliderSeeking =false;
}
Widget::~Widget()
{
delete ui;
thread->stop();
}
void Widget::onSeekOk()
{
sliderSeeking=false;
}
void Widget::onDuration(int currentMs,int destMs) //时长
{
static int currentMs1=-1,destMs1=-1;
if(currentMs1==currentMs&&destMs1==destMs)
{
return;
}
currentMs1 = currentMs;
destMs1 = destMs;
qDebug()<<"onDuration:"<<currentMs<<destMs<<sliderSeeking;
QString currentTime = QString("%1:%2:%3").arg(currentMs1/360000%60,2,10,QChar('0')).arg(currentMs1/6000%60,2,10,QChar('0')).arg(currentMs1/1000%60,2,10,QChar('0'));
QString destTime = QString("%1:%2:%3").arg(destMs1/360000%60,2,10,QChar('0')).arg(destMs1/6000%60,2,10,QChar('0')).arg(destMs1/1000%60,2,10,QChar('0'));
ui->label_duration->setText(currentTime+"/"+destTime);
if(!sliderSeeking) //未滑动
{
ui->slider->setMaximum(destMs);
ui->slider->setValue(currentMs);
}
}
void Widget::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasUrls()) //判断拖的类型
{
event->acceptProposedAction();
}
else
{
event->ignore();
}
}
void Widget::dropEvent(QDropEvent *event)
{
if(event->mimeData()->hasUrls()) //判断放的类型
{
QList<QUrl> List = event->mimeData()->urls();
if(List.length()!=0)
{
ui->line_audioPath->setText(List[0].toLocalFile());
}
}
else
{
event->ignore();
}
}
void Widget::on_btn_start_clicked()
{
sliderSeeking=false;
thread->play(ui->line_audioPath->text());
}
void Widget::on_btn_stop_clicked()
{
thread->stop();
}
void Widget::on_btn_pause_clicked()
{
thread->pause();
}
void Widget::on_btn_resume_clicked()
{
thread->resume();
}
void Widget::on_slider_sliderPressed()
{
sliderSeeking=true;
}
void Widget::on_slider_sliderReleased()
{
thread->seek(ui->slider->value());
}