32_音視頻播放器_SDL播放

一、簡介

接著上節(jié)的音頻解碼,使用SDL播放音頻。

通過上節(jié)程序運(yùn)行打印發(fā)現(xiàn)這些音頻信息明顯不符合SDL的,所以我們需要進(jìn)行重采樣


二、音頻重采樣

這里我們可以參考之前的《12_采樣格式&音頻重采樣》來實(shí)現(xiàn)現(xiàn)在的重采樣。

2.1 引入頭文件

extern "C" {
#include <libswresample/swresample.h>
}

還需要在pro文件中引入swresample

LIBS += -L $${FFMPEG_HOME}/lib \
        -lavcodec \
        -lavformat \
        -lavutil \
        -lswresample

2.2 定義重采樣相關(guān)屬性

/******** 音頻相關(guān) ********/
typedef struct {
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
    int chs;
    int bytesPerSampleFrame;
} AudioSwrSpec;

/** 音頻重采樣上下文 */
SwrContext *_aSwrCtx = nullptr;
/** 音頻重采樣輸入\輸出參數(shù) */
AudioSwrSpec _aSwrInSpec, _aSwrOutSpec;
/** 音頻重采樣輸入\輸出frame */
AVFrame *_aSwrInFrame = nullptr, *_aSwrOutFrame = nullptr;
/** 音頻重采樣輸出PCM的索引(從哪個位置開始取出PCM數(shù)據(jù)填充到SDL的音頻緩沖區(qū)) */
int _aSwrOutIdx = 0;
/** 音頻重采樣輸出PCM的大小 */
int _aSwrOutSize = 0;

/** 初始化音頻重采樣 */
int initSwr();

2.3初始化重采樣

int VideoPlayer::initSwr() {
    // 重采樣輸入?yún)?shù)
    _aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;
    _aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;
    _aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;
    _aSwrInSpec.chs = _aDecodeCtx->channels;

    // 重采樣輸出參數(shù)
    _aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;
    _aSwrOutSpec.sampleRate = 44100;
    _aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;
    _aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);
    _aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs
                                       * av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt);

    // 創(chuàng)建重采樣上下文
    _aSwrCtx = swr_alloc_set_opts(nullptr,
                                  // 輸出參數(shù)
                                  _aSwrOutSpec.chLayout,
                                  _aSwrOutSpec.sampleFmt,
                                  _aSwrOutSpec.sampleRate,
                                  // 輸入?yún)?shù)
                                  _aSwrInSpec.chLayout,
                                  _aSwrInSpec.sampleFmt,
                                  _aSwrInSpec.sampleRate,
                                  0, nullptr);
    if (!_aSwrCtx) {
        qDebug() << "swr_alloc_set_opts error";
        return -1;
    }

    // 初始化重采樣上下文
    int ret = swr_init(_aSwrCtx);
    RET(swr_init);

    // 初始化重采樣的輸入frame
    _aSwrInFrame = av_frame_alloc();
    if (!_aSwrInFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    // 初始化重采樣的輸出frame
    _aSwrOutFrame = av_frame_alloc();
    if (!_aSwrOutFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    return 0;
}

initAudioInfo方法中調(diào)用initSwr方法

int VideoPlayer::initAudioInfo() {
    int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);
    RET(initDecoder);

    // 初始化音頻重采樣
    ret = initSwr();
    RET(initSwr);

    // 初始化SDL
    ret = initSDL();
    RET(initSDL);

    return 0;
}

2.4 重采樣

上面進(jìn)行了重采樣的初始化后,現(xiàn)在我們可以在解碼出來的PCM進(jìn)行重采樣

int VideoPlayer::decodeAudio(){
    ......

    // 重采樣輸出的樣本數(shù)
    int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,
                                    _aSwrInFrame->nb_samples,
                                    _aSwrInSpec.sampleRate, AV_ROUND_UP);

    // 由于解碼出來的PCM。跟SDL要求的PCM格式可能不一致,需要進(jìn)行重采樣
    ret = swr_convert(_aSwrCtx,
                      _aSwrOutFrame->data,
                      outSamples,
                      (const uint8_t **) _aSwrInFrame->data,
                      _aSwrInFrame->nb_samples);
    RET(swr_convert);

    return ret * _aSwrOutSpec.bytesPerSampleFrame;
}

swr_convert函數(shù)的參數(shù)解釋:

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);
  • 參數(shù)1:重采樣上下文

  • 參數(shù)2:輸出到什么地方,這里我們希望輸出到_aSwrOutFrame->data,到時候可以直接通過_aSwrOutFrame->data[0]拿到它指向的PCM數(shù)據(jù),如果是Planar格式data[0]指向第一個聲道, data[1]指向下一個聲道,但是這里最終重采樣出來的數(shù)據(jù)是非Planar,是s16的所以這里可以直接通data[0]拿到PCM數(shù)據(jù)

    Planar格式和非Planar格式

  • 參數(shù)3:希望輸出多少個樣本,outSamples = outSampleRate * inSamples / inSampleRate,可以直接使用ffmpeg的av_rescale_rnd函數(shù)得到。

  • 參數(shù)4:輸入數(shù)據(jù),可以使用_aSwrInFrame->data

  • 參數(shù)5:重采樣輸入數(shù)據(jù)里面包含多少個樣本,可以使用_aSwrInFrame->nb_samples,這個值不一定是固定的,

  • 返回值:真正轉(zhuǎn)換成功的樣本,也就是每一個聲道的樣本數(shù)。

如果此時你運(yùn)行代碼會出現(xiàn)內(nèi)存錯誤,這是因?yàn)橹夭蓸訒r,_aSwrOutFrame->datadata[0]未分配空間,所以需要在初始化重采樣的地方給data[0]分配空間

// 初始化重采樣的輸出frame的data[0]空間
ret = av_samples_alloc(_aSwrOutFrame->data,
                       _aSwrOutFrame->linesize,
                       _aSwrOutSpec.chs,
                       4096, _aSwrOutSpec.sampleFmt, 1);
RET(av_samples_alloc);

三、SDL播放

上面實(shí)現(xiàn)了重采樣,那么現(xiàn)在我們需要把重采樣的數(shù)據(jù)填充到回調(diào)函數(shù)sdlAudioCallbackstream里面。

void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){
    // 清零(靜音)
    SDL_memset(stream, 0, len);

    // len:SDL音頻緩沖區(qū)剩余的大?。ㄟ€未填充的大小)
    while (len > 0) {
        if (_state == Stopped) break;

        // 說明當(dāng)前PCM的數(shù)據(jù)已經(jīng)全部拷貝到SDL的音頻緩沖區(qū)了
        // 需要解碼下一個pkt,獲取新的PCM數(shù)據(jù)
        if (_aSwrOutIdx >= _aSwrOutSize) {
            // 全新PCM的大小
            _aSwrOutSize = decodeAudio();
            // 索引清0
            _aSwrOutIdx = 0;
            // 沒有解碼出PCM數(shù)據(jù),那就靜音處理
            if (_aSwrOutSize <= 0) {
                // 假定PCM的大小
                _aSwrOutSize = 1024;
                // 給PCM填充0(靜音)
                memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);
            }
        }

        // 本次需要填充到stream中的PCM數(shù)據(jù)大小
        int fillLen = _aSwrOutSize - _aSwrOutIdx;
        fillLen = std::min(fillLen, len);

        // 填充SDL緩沖區(qū)
        SDL_MixAudio(stream,
                     _aSwrOutFrame->data[0] + _aSwrOutIdx,
                     fillLen, SDL_MIX_MAXVOLUME);

        // 移動偏移量
        len -= fillLen;
        stream += fillLen;
        _aSwrOutIdx += fillLen;
    }
}

SDL_MixAudio函數(shù)解釋:

extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
                                          Uint32 len, int volume);
  • 參數(shù)1:填充的目的地
  • 參數(shù)2:數(shù)據(jù)的源頭,就是PCM從那個地方開始
  • 參數(shù)3:填充數(shù)據(jù)的長度,需要填充多少數(shù)據(jù)
  • 參數(shù)4:音量大小

也就是把src這個位置開始的多少個數(shù)據(jù)len填入到dst里面去

各個字段解釋:

_aSwrOutSize:表面這次重采樣PCm的大小
fillLen = _aSwrOutSize - _aSwrOutIdx:需要填充到stream中的PCM數(shù)據(jù)大小,減去_aSwrOutIdx主要是用于一次采樣PCM大小大于了stream的緩沖區(qū)。

len -= fillLen:SDL音頻緩沖區(qū)剩余的大?。ㄟ€未填充的大?。?br> stream += fillLen:跳過剛剛已經(jīng)填充的大小
_aSwrOutIdx += fillLen:跳過剛剛已經(jīng)填充的大小

如果_aSwrOutIdx >= _aSwrOutSize說明PCM所有內(nèi)容都已經(jīng)拷貝到stream里面了,此時的PCM數(shù)據(jù)已經(jīng)沒有利用價值了,這個時候就得解碼下一個pkt,獲取新的PCM數(shù)據(jù),此時_aSwrOutIdx就需要清零。如果沒有解碼出PCM數(shù)據(jù),那就靜音處理(_aSwrOutSize = 1024是經(jīng)驗(yàn)值)。

四、停止功能

首先需要修改videoplayer.cppplay方法,在讀取文件時判斷當(dāng)前狀態(tài)釋放時停止?fàn)顟B(tài)。

void VideoPlayer::play() {
    if (_state == Playing) return;
    // 狀態(tài)可能是:暫停、停止、正常完畢

    if(_state == Stopped){
        // 開始線程:讀取文件
        std::thread([this](){
            readFile();
        }).detach();// detach 等到readFile方法執(zhí)行完,這個線程就會銷毀
    
        setState(Playing);
    }
}

videoplayer.h新增釋放資源的方法

/** 釋放資源 */
void free();
void freeAudio();
void freeVideo();

videoplayer.cpp中釋放公共的一些資源

void VideoPlayer::free(){
    avformat_close_input(&_fmtCtx);

    freeAudio();
    freeVideo();
}

現(xiàn)在主要是釋放音頻相關(guān)的資源

void VideoPlayer::freeAudio(){
    _aSwrOutIdx = 0;
    _aSwrOutSize =0;

    clearAudioPktList();
    avcodec_free_context(&_aDecodeCtx);
    swr_free(&_aSwrCtx);
    av_frame_free(&_aSwrInFrame);
    if(_aSwrOutFrame){
        av_freep(&_aSwrOutFrame->data[0]);// 因手動創(chuàng)建了data[0]的空間
        av_frame_free(&_aSwrOutFrame);
    }

    // 停止播放
    SDL_PauseAudio(1);
    SDL_CloseAudio();
}

在解碼音頻的方法decodeAudio中,還需要判斷狀態(tài)釋放為停止?fàn)顟B(tài),因?yàn)?,一?zhí)行此方法就加鎖了,就會再次阻塞等待,等到后終于可以拿到鎖了,但是在我們等待期間有可能就被我們關(guān)掉了,此時就會出現(xiàn)問題,因此這里還需要在判斷一下狀態(tài)_state == Stopped

int VideoPlayer::decodeAudio(){
    // 加鎖
    _aMutex->lock();

    if (_aPktList->empty() || _state == Stopped) {
        _aMutex->unlock();
        return 0;
    }
    ......
}

videoplayer.cpp的讀取文件的while循環(huán)中也要判斷釋放為停止?fàn)顟B(tài)

while (true) {
   if(_state == Stopped) break;
   
   AVPacket pkt;
   ret = av_read_frame(_fmtCtx,&pkt);
   if ( ret == 0) {
       if (pkt.stream_index == _aStream->index) { // 讀取到的是音頻數(shù)據(jù)
           addAudioPkt(pkt);
       }else if(pkt.stream_index == _vStream->index){// 讀取到的是視頻數(shù)據(jù)
           addVideoPkt(pkt);
       }
   }else{
       continue;
   }
}

我們之前分裝好的END的宏函數(shù)最后是goto去是釋放資源,現(xiàn)在我們直接調(diào)用free方法就可以了

#define END(func) \
    if (ret < 0) { \
        ERROR_BUF; \
        qDebug() << #func << "error" << errbuf; \
        setState(Stopped); \
        emit playFailed(this); \
        free(); \
        return; \
    }
// 初始化音頻信息
bool hasAudio = initAudioInfo() >= 0;
// 初始化視頻信息
bool hasVideo = initVideoInfo() >= 0;
if (!hasAudio && !hasVideo) {
   emit playFailed(this);
   free();
   return;
}

五、處理讀完音頻包的情況

while (_state != Stopped) {
   AVPacket pkt;
   ret = av_read_frame(_fmtCtx, &pkt);
   if (ret == 0) {
       if (pkt.stream_index == _aStream->index) { // 讀取到的是音頻數(shù)據(jù)
           addAudioPkt(pkt);
       } else if (pkt.stream_index == _vStream->index) { // 讀取到的是視頻數(shù)據(jù)
           addVideoPkt(pkt);
       }
   } else if (ret == AVERROR_EOF) { // 讀到了文件的尾部
       qDebug() << "已經(jīng)讀取到文件尾部";
       break;
   } else {
       ERROR_BUF;
       qDebug() << "av_read_frame error" << errbuf;
       continue;
   }
}

六、實(shí)現(xiàn)調(diào)節(jié)音量

修改videplayer.h文件
修改mainwindow.cpp文件
修改videplayer.cpp文件
修改videplayer_audio.cpp文件

七、實(shí)現(xiàn)靜音功能

代碼鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內(nèi)容