1.前言
由于QAudioOutput支持的输入数据必须是原始数据,所以播放mp3,WAV,AAC等格式文件,需要解封装后才能支持播放.
而在QT中,提供了QMediaPlayer类可以支持解封装,但是该类的解码协议都是基于平台的,如果平台自身无法播放,那么QMediaPlayer也无法播放.有兴趣的朋友可以去试试.
所以接下来,我们使用ffmpeg+QAudioOutput来实现一个简单的音频播放器.
在此之前,需要学习:
- 2.AVFormatContext和AVInputFormat
- 3.AVPacket使用
- 4.FFMPEG-AVFrame
- 5.AVStream和AVCodecParameters
- 6.AVCodecContext和AVCodec
- 7.SwrContext音频重采样使用
- 8.ffmpeg-基础常用知识
- 9.下载ffmpeg、使QT支持同时编译32位和64位
- 10.QT-QAudioOutput类使用
2.界面展示
因为业余爱好,只是简单实现了大部分功能,支持播放、暂停、恢复、换歌、播放进度调节,如下图所示:
3.效果展示
下载链接(已经把ffmpeg移植好了,可以直接编译):https://download.csdn.net/download/qq_37997682/12834268
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())); 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()); }