31_音视频播放器_音频解码

一、简介

如上图,我们在主线程中开启一个子线程进行解封装,然后在开两个线程分别进行视频解码和音频解码,其中音频解码我们使用的是SDL去渲染,SDL自己会管理子线程,不用我们来创建子线程,而视频解码是需要我们自己创建子线程进行管理。

解封装会解出视频包和音频包,分别塞入各自的队列中,然后各自解码器取出各自的包进行解码,这种模式就是典型的生产者和消费者模式,
所以这里就需要锁机制,我们可以使用《29_SDL多线程与锁机制》里分装好的锁机制类。

#include "condmutex.h"

CondMutex::CondMutex() {
    // 创建互斥锁
    _mutex = SDL_CreateMutex();
    // 创建条件变量
    _cond = SDL_CreateCond();
}

CondMutex::~CondMutex() {
    SDL_DestroyMutex(_mutex);
    SDL_DestroyCond(_cond);
}

void CondMutex::lock() {
    SDL_LockMutex(_mutex);
}

void CondMutex::unlock() {
    SDL_UnlockMutex(_mutex);
}

void CondMutex::signal() {
    SDL_CondSignal(_cond);
}

void CondMutex::broadcast() {
    SDL_CondBroadcast(_cond);
}

void CondMutex::wait() {
    SDL_CondWait(_cond, _mutex);
}

二、音频解码

这节我们先介绍音频解码过程。如果都在VideoPlayer类里做视频解码和音频解码,那么这个类里的代码会很多,我们可以拆开处理。

videoplayert.cppvideoplayert_audio.cppvideoplayert_video.cpp三个文件都公用同一个videoplayert.h,而videoplayert_audio.cpp专门负责音频相关的处理,videoplayert_video.cpp专门负责视频相关的处理,videoplayert.cpp是音视频共同的处理。

2.1 videoplayert.h添加音频相关方法

#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QObject>
#include <QDebug>
#include <list>
#include "condmutex.h"

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}

#define ERROR_BUF \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

#define END(func) \
    if (ret < 0) { \
        ERROR_BUF; \
        qDebug() << #func << "error" << errbuf; \
        setState(Stopped); \
        emit playFailed(this); \
        goto end; \
    }

#define RET(func) \
    if (ret < 0) { \
        ERROR_BUF; \
        qDebug() << #func << "error" << errbuf; \
        return ret; \
    }

/**
 * 预处理视频数据(不负责显示、渲染视频)
 */
class VideoPlayer : public QObject {
    Q_OBJECT
public:
    // 状态
    typedef enum {
        Stopped = 0,
        Playing,
        Paused
    } State;

    explicit VideoPlayer(QObject *parent = nullptr);
    ~VideoPlayer();

    /** 播放 */
    void play();
    /** 暂停 */
    void pause();
    /** 停止 */
    void stop();
    /** 是否正在播放中 */
    bool isPlaying();
    /** 获取当前的状态 */
    State getState();
    /** 设置文件名 */
    void setFilename(const char *filename);
    /** 获取总时长(单位是微妙,1秒=1000毫秒=1000000微妙)*/
    int64_t getDuration();

signals:
    void stateChanged(VideoPlayer *player);
    void initFinished(VideoPlayer *player);
    void playFailed(VideoPlayer *player);

private:
    /******** 音频相关 ********/
    /** 解码上下文 */
    AVCodecContext *_aDecodeCtx = nullptr;
    /** 流 */
    AVStream *_aStream = nullptr;
    /** 存放解码后的数据 */
    AVFrame *_aFrame = nullptr;
    /** 存放音频包的列表 */
    std::list<AVPacket> *_aPktList = nullptr;
    /** 音频包列表的锁 */
    CondMutex *_aMutex = nullptr;

    /** 初始化音频信息 */
    int initAudioInfo();
    /** 初始化SDL */
    int initSDL();
    /** 添加数据包到音频包列表中 */
    void addAudioPkt(AVPacket &pkt);
    /** 清空音频包列表 */
    void clearAudioPktList();
    /** SDL填充缓冲区的回调函数 */
    static void sdlAudioCallbackFunc(void *userdata, Uint8 *stream, int len);
    /** SDL填充缓冲区的回调函数 */
    void sdlAudioCallback(Uint8 *stream, int len);
    /** 音频解码 */
    int decodeAudio();

    /******** 视频相关 ********/
    /** 解码上下文 */
    AVCodecContext *_vDecodeCtx = nullptr;
    /** 流 */
    AVStream *_vStream = nullptr;
    /** 存放解码后的数据 */
    AVFrame *_vFrame = nullptr;
    /** 存放视频包的列表 */
    std::list<AVPacket> *_vPktList = nullptr;
    /** 视频包列表的锁 */
    CondMutex *_vMutex = nullptr;

    /** 初始化视频信息 */
    int initVideoInfo();
    /** 添加数据包到视频包列表中 */
    void addVideoPkt(AVPacket &pkt);
    /** 清空视频包列表 */
    void clearVideoPktList();


    /******** 其他 ********/
    /** 当前的状态 */
    State _state = Stopped;
    /** 文件名 */
    const char *_filename;
    // 解封装上下文
    AVFormatContext *_fmtCtx = nullptr;
    /** 初始化解码器和解码上下文 */
    int initDecoder(AVCodecContext **decodeCtx,
                    AVStream **stream,
                    AVMediaType type);

    /** 改变状态 */
    void setState(State state);
    /** 读取文件数据 */
    void readFile();
};

#endif // VIDEOPLAYER_H

3.2 videoplayer.cpp中分发音频和视频包

void VideoPlayer::readFile(){
   ......
   // 从输入文件中读取数据
   while (true) {
       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;
       }
   }

......

3.3 实现各个方法

3.3.1 实现队列添加和清理音频包

// videoplayert_audio.cpp

void VideoPlayer::addAudioPkt(AVPacket &pkt){
    _aMutex->lock();
    _aPktList->push_back(pkt);
    _aMutex->signal();
    _aMutex->unlock();
}

void VideoPlayer::clearAudioPktList(){
    _aMutex->lock();
    for(AVPacket &pkt : *_aPktList){
        av_packet_unref(&pkt);
    }
    _aPktList->clear();
    _aMutex->unlock();
}

3.3.2 初始化音频信息

// videoplayert_audio.cpp

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

    // 初始化frame
    _aFrame = av_frame_alloc();
    if (!_aFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

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

    return 0;
}

int VideoPlayer::initSDL(){
    // 音频参数
    SDL_AudioSpec spec;
    // 采样率
    spec.freq = 44100;
    // 采样格式(s16le)
    spec.format = AUDIO_S16LSB;
    // 声道数
    spec.channels = 2;
    // 音频缓冲区的样本数量(这个值必须是2的幂)
    spec.samples = 512;
    // 回调
    spec.callback = sdlAudioCallbackFunc;
    // 传递给回调的参数
    spec.userdata = this;

    // 打开音频设备
    if (SDL_OpenAudio(&spec, nullptr)) {
        qDebug() << "SDL_OpenAudio error" << SDL_GetError();
        return -1;
    }

    // 开始播放
    SDL_PauseAudio(0);

    return 0;
}

3.3.3 SDL回调函数

// videoplayert_audio.cpp

void VideoPlayer::sdlAudioCallbackFunc(void *userdata, uint8_t *stream, int len){
    VideoPlayer *player = (VideoPlayer *)userdata;
    player->sdlAudioCallback(stream,len);
}


void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){
    // len:SDL音频缓冲区剩余的大小(还未填充的大小)
    while (len > 0) {
        int dataSize = decodeAudio();
        qDebug() <<"解码出来的pcm大小:"<<dataSize;
        if (dataSize <= 0) {

        } else {

        }

//        // 将一个pkt包解码后的pcm数据填充到SDL的音频缓冲区
//        SDL_MixAudio(stream, src, srcLen, SDL_MIX_MAXVOLUME);

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

3.3.4 解码音频

// videoplayert_audio.cpp

/**
 * @brief VideoPlayer::decodeAudio
 * @return 解码出来的pcm大小
 */
int VideoPlayer::decodeAudio(){
    // 加锁
    _aMutex->lock();

//    while (_aPktList->empty()) {
//        _aMutex->wait();
//    }
    if (_aPktList->empty()) {
        _aMutex->unlock();
        return 0;
    }

    // 取出头部的数据包
    AVPacket pkt = _aPktList->front();
    // 从头部中删除
    _aPktList->pop_front();

    // 解锁
    _aMutex->unlock();

    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(_aDecodeCtx, &pkt);
    // 释放pkt
    av_packet_unref(&pkt);
    RET(avcodec_send_packet);

    // 获取解码后的数据
    ret = avcodec_receive_frame(_aDecodeCtx, _aFrame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        return 0;
    } else RET(avcodec_receive_frame);

    // 将解码后的数据写入文件
    qDebug() << _aFrame->sample_rate
             << _aFrame->channels
             << av_get_sample_fmt_name((AVSampleFormat) _aFrame->format);

    // 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致
    // 需要进行重采样


    return _aFrame->nb_samples
           * _aFrame->channels
           * av_get_bytes_per_sample((AVSampleFormat) _aFrame->format);
}

代码链接

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

推荐阅读更多精彩内容