11.QT-ffmpeg+QAudioOutput實現音訊播放器

NQian發表於2020-09-10
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());

}

 

 
 
 
 
 
 

相關文章