32_音视频播放器_SDL播放

一、简介

接着上节的音频解码,使用SDL播放音频。

通过上节程序运行打印发现这些音频信息明显不符合SDL的,所以我们需要进行重采样


二、音频重采样

这里我们可以参考之前的《12_采样格式&音频重采样》来实现现在的重采样。

2.1 引入头文件

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

还需要在pro文件中引入swresample

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

2.2 定义重采样相关属性

/******** 音频相关 ********/
typedef struct {
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
    int chs;
    int bytesPerSampleFrame;
} AudioSwrSpec;

/** 音频重采样上下文 */
SwrContext *_aSwrCtx = nullptr;
/** 音频重采样输入\输出参数 */
AudioSwrSpec _aSwrInSpec, _aSwrOutSpec;
/** 音频重采样输入\输出frame */
AVFrame *_aSwrInFrame = nullptr, *_aSwrOutFrame = nullptr;
/** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */
int _aSwrOutIdx = 0;
/** 音频重采样输出PCM的大小 */
int _aSwrOutSize = 0;

/** 初始化音频重采样 */
int initSwr();

2.3初始化重采样

int VideoPlayer::initSwr() {
    // 重采样输入参数
    _aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;
    _aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;
    _aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;
    _aSwrInSpec.chs = _aDecodeCtx->channels;

    // 重采样输出参数
    _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);

    // 创建重采样上下文
    _aSwrCtx = swr_alloc_set_opts(nullptr,
                                  // 输出参数
                                  _aSwrOutSpec.chLayout,
                                  _aSwrOutSpec.sampleFmt,
                                  _aSwrOutSpec.sampleRate,
                                  // 输入参数
                                  _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方法中调用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 重采样

上面进行了重采样的初始化后,现在我们可以在解码出来的PCM进行重采样

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

    // 重采样输出的样本数
    int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,
                                    _aSwrInFrame->nb_samples,
                                    _aSwrInSpec.sampleRate, AV_ROUND_UP);

    // 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致,需要进行重采样
    ret = swr_convert(_aSwrCtx,
                      _aSwrOutFrame->data,
                      outSamples,
                      (const uint8_t **) _aSwrInFrame->data,
                      _aSwrInFrame->nb_samples);
    RET(swr_convert);

    return ret * _aSwrOutSpec.bytesPerSampleFrame;
}

swr_convert函数的参数解释:

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);
  • 参数1:重采样上下文

  • 参数2:输出到什么地方,这里我们希望输出到_aSwrOutFrame->data,到时候可以直接通过_aSwrOutFrame->data[0]拿到它指向的PCM数据,如果是Planar格式data[0]指向第一个声道, data[1]指向下一个声道,但是这里最终重采样出来的数据是非Planar,是s16的所以这里可以直接通data[0]拿到PCM数据

    Planar格式和非Planar格式

  • 参数3:希望输出多少个样本,outSamples = outSampleRate * inSamples / inSampleRate,可以直接使用ffmpeg的av_rescale_rnd函数得到。

  • 参数4:输入数据,可以使用_aSwrInFrame->data

  • 参数5:重采样输入数据里面包含多少个样本,可以使用_aSwrInFrame->nb_samples,这个值不一定是固定的,

  • 返回值:真正转换成功的样本,也就是每一个声道的样本数。

如果此时你运行代码会出现内存错误,这是因为重采样时,_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播放

上面实现了重采样,那么现在我们需要把重采样的数据填充到回调函数sdlAudioCallbackstream里面。

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

    // len:SDL音频缓冲区剩余的大小(还未填充的大小)
    while (len > 0) {
        if (_state == Stopped) break;

        // 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了
        // 需要解码下一个pkt,获取新的PCM数据
        if (_aSwrOutIdx >= _aSwrOutSize) {
            // 全新PCM的大小
            _aSwrOutSize = decodeAudio();
            // 索引清0
            _aSwrOutIdx = 0;
            // 没有解码出PCM数据,那就静音处理
            if (_aSwrOutSize <= 0) {
                // 假定PCM的大小
                _aSwrOutSize = 1024;
                // 给PCM填充0(静音)
                memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);
            }
        }

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

        // 填充SDL缓冲区
        SDL_MixAudio(stream,
                     _aSwrOutFrame->data[0] + _aSwrOutIdx,
                     fillLen, SDL_MIX_MAXVOLUME);

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

SDL_MixAudio函数解释:

extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
                                          Uint32 len, int volume);
  • 参数1:填充的目的地
  • 参数2:数据的源头,就是PCM从那个地方开始
  • 参数3:填充数据的长度,需要填充多少数据
  • 参数4:音量大小

也就是把src这个位置开始的多少个数据len填入到dst里面去

各个字段解释:

_aSwrOutSize:表面这次重采样PCm的大小
fillLen = _aSwrOutSize - _aSwrOutIdx:需要填充到stream中的PCM数据大小,减去_aSwrOutIdx主要是用于一次采样PCM大小大于了stream的缓冲区。

len -= fillLen:SDL音频缓冲区剩余的大小(还未填充的大小)
stream += fillLen:跳过刚刚已经填充的大小
_aSwrOutIdx += fillLen:跳过刚刚已经填充的大小

如果_aSwrOutIdx >= _aSwrOutSize说明PCM所有内容都已经拷贝到stream里面了,此时的PCM数据已经没有利用价值了,这个时候就得解码下一个pkt,获取新的PCM数据,此时_aSwrOutIdx就需要清零。如果没有解码出PCM数据,那就静音处理(_aSwrOutSize = 1024是经验值)。

四、停止功能

首先需要修改videoplayer.cppplay方法,在读取文件时判断当前状态释放时停止状态。

void VideoPlayer::play() {
    if (_state == Playing) return;
    // 状态可能是:暂停、停止、正常完毕

    if(_state == Stopped){
        // 开始线程:读取文件
        std::thread([this](){
            readFile();
        }).detach();// detach 等到readFile方法执行完,这个线程就会销毁
    
        setState(Playing);
    }
}

videoplayer.h新增释放资源的方法

/** 释放资源 */
void free();
void freeAudio();
void freeVideo();

videoplayer.cpp中释放公共的一些资源

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

    freeAudio();
    freeVideo();
}

现在主要是释放音频相关的资源

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]);// 因手动创建了data[0]的空间
        av_frame_free(&_aSwrOutFrame);
    }

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

在解码音频的方法decodeAudio中,还需要判断状态释放为停止状态,因为,一执行此方法就加锁了,就会再次阻塞等待,等到后终于可以拿到锁了,但是在我们等待期间有可能就被我们关掉了,此时就会出现问题,因此这里还需要在判断一下状态_state == Stopped

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

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

videoplayer.cpp的读取文件的while循环中也要判断释放为停止状态

while (true) {
   if(_state == Stopped) break;
   
   AVPacket pkt;
   ret = av_read_frame(_fmtCtx,&pkt);
   if ( ret == 0) {
       if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据
           addAudioPkt(pkt);
       }else if(pkt.stream_index == _vStream->index){// 读取到的是视频数据
           addVideoPkt(pkt);
       }
   }else{
       continue;
   }
}

我们之前分装好的END的宏函数最后是goto去是释放资源,现在我们直接调用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) { // 读取到的是音频数据
           addAudioPkt(pkt);
       } else if (pkt.stream_index == _vStream->index) { // 读取到的是视频数据
           addVideoPkt(pkt);
       }
   } else if (ret == AVERROR_EOF) { // 读到了文件的尾部
       qDebug() << "已经读取到文件尾部";
       break;
   } else {
       ERROR_BUF;
       qDebug() << "av_read_frame error" << errbuf;
       continue;
   }
}

六、实现调节音量

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

七、实现静音功能

代码链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容