当前位置: 首页 > news >正文

【Android 音视频开发打怪升级:FFmpeg音视频编解码】四、Android FFmpeg+OpenSL ES音频解码播放...


系列文章:

  1. 【Android音视频开发打怪升级:音视频硬解码篇】一、音视频基础知识

  2. 【Android 音视频开发打怪升级:音视频硬解码篇】二、音视频硬解码流程:封装基础解码框架

  3. 【Android 音视频开发打怪升级:FFmpeg 音视频编解码篇】三、Android FFmpeg视频解码播放

教程代码:【https://github.com/ChenLittlePing/LearningVideo】

一、Android音视频硬解码篇:

  • 1,音视频基础知识

  • 2,音视频硬解码流程:封装基础解码框架

  • 3,音视频播放:音视频同步

  • 4,音视频解封和封装:生成一个MP4

二、使用OpenGL渲染视频画面篇

  • 1,初步了解OpenGL ES

  • 2,使用OpenGL渲染视频画面

  • 3,OpenGL渲染多视频,实现画中画

  • 4,深入了解OpenGL之EGL

  • 5,OpenGL FBO数据缓冲区

  • 6,Android音视频硬编码:生成一个MP4

三、Android FFmpeg音视频解码篇

  • 1,FFmpeg so库编译

  • 2,Android 引入FFmpeg

  • 3. Android FFmpeg 视频解码播放

  • 4,Android FFmpeg+OpenSL ES音频解码播放

  • 5,Android FFmpeg+OpenGL ES播放视频

  • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装

  • 7,Android FFmpeg视频编码

本文你可以了解到

本文介绍如何使用 FFmpeg 进行音频解码,重点讲解如何使用 OpenSL ES 在 DNK 层实现音频渲染播放。

音频解码

在上篇文章中,详细介绍了 FFmepg 的播放流程,以及抽象了解码流程框架,整合视频和音频解码流程的共同点,形成了 BaseDecoder类。通过继承 BaseDecoder 实现了视频解码子类 VideoDeocder,并整合到了 Player 中,实现了视频的播放渲染。

本文就利用已经定义好的解码基类 BaseDecoder 实现音频解码子类 AudioDecoder 。

实现音频解码子类

  • 定义解码流程

我们通过头文件 a_decoder.h ,将需要的成员变量和流程方法定义好。

i. 成员变量定义

//a_decoder.h


class AudioDecoder: public BaseDecoder {
private:


    const char *TAG = "AudioDecoder";


    // 音频转换器
    SwrContext *m_swr = NULL;


    // 音频渲染器
    AudioRender *m_render = NULL;


    // 输出缓冲
    uint8_t *m_out_buffer[1] = {NULL};


    // 重采样后,每个通道包含的采样数
    // acc默认为1024,重采样后可能会变化
    int m_dest_nb_sample = 1024;


    // 重采样以后,一帧数据的大小
    size_t m_dest_data_size = 0;
    
    //......
}

其中,SwrContext 是 FFmpeg 提供的音频转化工具,位于 swresample 中,可用来转换采样率、解码通道数、采样位数等。这里用来将音频数据转换为 双通道立体 声音,统一 采样位数

⚠️ AudioRender 是自定义的音频渲染器,将在后面介绍。

其他的变量,则是音频转换中需要配合使用的,转换输出缓冲、缓冲区大小、采样数。

ii. 定义成员方法

//a_decoder.h


class AudioDecoder: public BaseDecoder {
private:


    // 省略成员变量......


    /**
     * 初始化转换工具
     */
    void InitSwr();


    /**
     * 初始化输出缓冲
     */
    void InitOutBuffer();


    /**
     * 初始化渲染器
     */
    void InitRender();


    /**
     * 释放缓冲区
     */
    void ReleaseOutBuffer();


    /**
     * 采样格式:16位
     */
    AVSampleFormat GetSampleFmt() {
        return AV_SAMPLE_FMT_S16;
    }


    /**
     * 目标采样率
     */
    int GetSampleRate(int spr) {
        return AUDIO_DEST_SAMPLE_RATE; //44100Hz
    }


public:
    AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer);
    ~AudioDecoder();


    void SetRender(AudioRender *render);


protected:
    void Prepare(JNIEnv *env) override;
    void Render(AVFrame *frame) override;
    void Release() override;


    bool NeedLoopDecode() override {
        return true;
    }


    AVMediaType GetMediaType() override {
        return AVMEDIA_TYPE_AUDIO;
    }


    const char *const LogSpec() override {
        return "AUDIO";
    };
};

以上代码也不复杂,都是一些初始化相关的方法,以及对 BaseDecoder 中定义的抽象方法的实现。

重点讲解一下这两个方法:

/**
 * 采样格式:16位
 */
AVSampleFormat GetSampleFmt() {
    return AV_SAMPLE_FMT_S16;
}


/**
 * 目标采样率
 */
int GetSampleRate(int spr) {
    return AUDIO_DEST_SAMPLE_RATE; //44100Hz
}

首先要知道的是,这两个方法的目的是为了兼容以后编码的。

我们知道音频的采样率和采样位数是音频数据特有的,并且每个音频都有可能不一样,所以在播放或者重新编码的时候,通常会将数据转换为固定的规格,这样才能正常播放或重新编码。

播放和编码的配置也稍有不同,这里,采样位数是 16 位,采样率使用 44100 。

接下来,看看具体的实现。

  • 实现解码流程

// a_decoder.cpp


AudioDecoder::AudioDecoder(JNIEnv *env, const jstring path, bool forSynthesizer) : BaseDecoder(
        env, path, forSynthesizer) {
    
}


void AudioDecoder::~AudioDecoder() {
    if (m_render != NULL) {
        delete m_render;
    }
}


void AudioDecoder::SetRender(AudioRender *render) {
    m_render = render;
}


void AudioDecoder::Prepare(JNIEnv *env) {
    InitSwr();
    InitOutBuffer();
    InitRender();
}


//省略其他....

i. 初始化

重点看 Prepare 方法,这个方法在基类 BaseDecoder 初始化完解码器以后,就会调用。

在 Prepare 方法中,依次调用了:

InitSwr(),初始化转换器


InitOutBuffer(),初始化输出缓冲


InitRender(),初始化渲染器

下面具体解析如何配置初始化参数。

SwrContext 配置:

// a_decoder.cpp


void AudioDecoder::InitSwr() {
    
    // codec_cxt() 为解码上下文,从子类 BaseDecoder 中获取
    AVCodecContext *codeCtx = codec_cxt();


    //初始化格式转换工具
    m_swr = swr_alloc();


    // 配置输入/输出通道类型
    av_opt_set_int(m_swr, "in_channel_layout",  codeCtx->channel_layout, 0);
    
    // 这里 AUDIO_DEST_CHANNEL_LAYOUT = AV_CH_LAYOUT_STEREO,即 立体声
    av_opt_set_int(m_swr, "out_channel_layout", AUDIO_DEST_CHANNEL_LAYOUT, 0);
    
    // 配置输入/输出采样率
    av_opt_set_int(m_swr, "in_sample_rate", codeCtx->sample_rate, 0);
    av_opt_set_int(m_swr, "out_sample_rate", GetSampleRate(codeCtx->sample_rate), 0);


    // 配置输入/输出数据格式
    av_opt_set_sample_fmt(m_swr, "in_sample_fmt",  codeCtx->sample_fmt, 0);
    av_opt_set_sample_fmt(m_swr, "out_sample_fmt", GetSampleFmt(),  0);


    swr_init(m_swr);
}

初始化很简单,首先调用 FFmpeg 的 swr_alloc 方法,分配内存,得到一个转化工具 m_swr ,接着调用对应的方法,设置输入和输出的音频数据参数。

输入输出参数的设置,也可通过一个统一的方法 swr_alloc_set_opts 设置,具体可以参看该接口注释。

输出缓冲配置:

// a_decoder.cpp


void AudioDecoder::InitOutBuffer() {
    // 重采样后一个通道采样数
    m_dest_nb_sample = (int)av_rescale_rnd(ACC_NB_SAMPLES, GetSampleRate(codec_cxt()->sample_rate),
                                           codec_cxt()->sample_rate, AV_ROUND_UP);
    // 重采样后一帧数据的大小
    m_dest_data_size = (size_t)av_samples_get_buffer_size(
            NULL, AUDIO_DEST_CHANNEL_COUNTS,
            m_dest_nb_sample, GetSampleFmt(), 1);


    // 分配输出缓冲内存
    m_out_buffer[0] = (uint8_t *) malloc(m_dest_data_size);
}


void AudioDecoder::InitRender() {
    m_render->InitRender();
}

在转换音频数据之前,我们需要一个数据缓冲区来存储转换后的数据,因此需要知道转换后的音频数据有多大,并以此来分配缓冲区。

影响数据缓冲大小的因素有三个:采样个数通道数采样位数

采样个数计算

我们知道 AAC 一帧数据包含采样个数是 1024 个。如果对一帧音频数据进行重采样的话,那么采样个数就会发生变化。

如果采样率变大,那么采样个数会变多;采样率变小,则采样个数变少。并且成比例关系。

计算方式如下:【目标采样个数 = 原采样个 *(目标采样率 / 原采样率)】

FFmpeg 提供了 av_rescale_rnd 用于计算这种缩放关系,优化了计算益处问题。

FFmpeg 提供了 av_samples_get_buffer_size 方法来帮我们计算这个缓存区的大小。只需提供计算出来的 目标采样个数通道数采样位数 。

得到缓存大小后,通过 malloc 分配内存。

ii. 渲染

// a_decoder.cpp


void AudioDecoder::Render(AVFrame *frame) {
    // 转换,返回每个通道的样本数
    int ret = swr_convert(m_swr, m_out_buffer, m_dest_data_size/2,
                          (const uint8_t **) frame->data, frame->nb_samples);
    if (ret > 0) {
        m_render->Render(m_out_buffer[0], (size_t) m_dest_data_size);
    }
}

子类 BaseDecoder 解码数据后,回调子类渲染方法 Render 。在渲染之前,调用 swr_convert 方法,转换音频数据。

以下为接口原型:

/**
 * out:输出缓冲区
 * out_count:输出数据单通道采样个数
 * in:待转换原音频数据
 * in_count:原音频单通道采样个数
 */
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);

最后调用渲染器 m_render 渲染播放。

iii.释放资源

// a_decoder.cpp


void AudioDecoder::Release() {
    if (m_swr != NULL) {
        swr_free(&m_swr);
    }
    if (m_render != NULL) {
        m_render->ReleaseRender();
    }
    ReleaseOutBuffer();
}


void AudioDecoder::ReleaseOutBuffer() {
    if (m_out_buffer[0] != NULL) {
        free(m_out_buffer[0]);
        m_out_buffer[0] = NULL;
    }
}

解码完毕,退出播放的时候,需要将转换器、输出缓冲区释放。

接入 OpenSL ES

在 Android 上播放音频,通常使用的 AudioTrack ,但是在 NDK 层,没有提供直接的类,需要通过 NDK 调用 Java 层的方式,回调实现播放。相对来说比较麻烦,效率也比较低。

在 NDK 层,提供另一种播放音频的方法:OpenSL ES 。

什么是 OpenSL ES

OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化,高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度。

OpenSL ES 提供哪些功能

OpenSL 主要提供了录制和播放的功能,本文主讲播放功能。

播放源支持 PCM 、sdcard资源 、 res/assets资源 、 网络资源 。

我们使用的 FFmpeg 解码,所以播放源是 PCM 。

OpenSL ES 状态机

OpenSL ES 是基于 C 语言开发的库,但是其接口是使用了面向对象的编程思想编写的,它的接口不能直接调用,而是要经过对象创建、初始化后,通过对象来调用。

  • Object 和 Interface

OpenSL ES 提供了一系列 Object ,它们拥有一些基础操作方法,比如 Realize,Resume,GetState,Destroy,GetInterface等。

一个 Object 拥有一个或多个 Interface 方法,但是一个 Intefcace 只属于一个 Obejct 。

想要调用 Object 中的 Interface 方法,必须要通过 Object 的 GetInterface 先获取到接口 Interface ,再通过获取到的 Interface 来调用。

比如:

// 创建引擎
SLObjectItf m_engine_obj = NULL;
SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);


// 初始化引擎
result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);


// 获取引擎接口
SLEngineItf m_engine = NULL;
result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine);

可以看到,Object 需要经过创建、初始化之后才能使用,这就是 OpenSL ES 中的状态机机制。

Object 被创建后,进入 Unrealized 状态,调用 Realize() 方法以后会分配相关的内存资源,进入 Realized 状态,这时 Object 的 Interface 方法才能被获取和使用。

在后续执行过程中,如果出现错误,Object 会进入 Suspended 状态。调用 Resume() 可以恢复到 Realized 状态。

OpenSL ES 播放初始化

来看一张官方的播放流程图

这张图非常清晰的展示了 OpenSL ES 是如何运作的。

OpenSL ES 播放需要的两个核心是 Audio Player 和 Output Mix ,即 播放起 和 混音器 ,而这两个都是由 OpenSL ES 的引擎 Engine 创建(creates)出来的。

所以,整个初始化流程可以总结为:

通过 Engine 创建 Output Mix/混音器,并将 混音器 作为参数,在创建 Audio Player/播放器 时,绑定给 Audio Player 作为输出。

  • DataSource 和 DataSink

在创建 Audio Player 的时候,需要给其设置 数据源 和 输出目标 ,这样播放器才知道,如何获取播放数据、将数据输出到哪里进行播放。

这就需要用到 OpenSL ES 的两个结构体 DataSource 和 DataSink 。

typedef struct SLDataSource_ {
  void *pLocator;
  void *pFormat;
} SLDataSource;


typedef struct SLDataSink_ {
  void *pLocator;
  void *pFormat;
} SLDataSink;

其中, SLDataSource pLocator 有以下几种类型:

SLDataLocator_Address
SLDataLocator_BufferQueue
SLDataLocator_IODevice
SLDataLocator_MIDIBufferQueue
SLDataLocator_URI

播放 PCM 使用的是 SLDataLocator_BufferQueue 缓冲队列。

SLDataSink pLocator 一般为 SL_DATALOCATOR_OUTPUTMIX 。

另外一个参数 pFormat 为数据的格式。

实现渲染流程

在接入 OpenSL ES 之前,先定义好上文提到的音频渲染接口,方便规范和拓展。

// audio_render.h


class AudioRender {
public:
    virtual void InitRender() = 0;
    virtual void Render(uint8_t *pcm, int size) = 0;
    virtual void ReleaseRender() = 0;
    virtual ~AudioRender() {}
};

在 CMakeList.txt 中,打开 OpenSL ES 支持

# CMakeList.txt


# 省略其他...


# 指定编译目标库时,cmake要链接的库
target_link_libraries(


        native-lib


        avutil
        swresample
        avcodec
        avfilter
        swscale
        avformat
        avdevice


        -landroid
        
        # 打开opensl es支持
        OpenSLES
        
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )
  • 初始化

i. 定义成员变量

先定义需要用到的引擎、混音器、播放器、以及缓冲队列接口、音量调节接口等。

// opensl_render.h


class OpenSLRender: public AudioRender {


private:


    // 引擎接口
    SLObjectItf m_engine_obj = NULL;
    SLEngineItf m_engine = NULL;


    //混音器
    SLObjectItf m_output_mix_obj = NULL;
    SLEnvironmentalReverbItf m_output_mix_evn_reverb = NULL;
    SLEnvironmentalReverbSettings m_reverb_settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;


    //pcm播放器
    SLObjectItf m_pcm_player_obj = NULL;
    SLPlayItf m_pcm_player = NULL;
    SLVolumeItf m_pcm_player_volume = NULL;


    //缓冲器队列接口
    SLAndroidSimpleBufferQueueItf m_pcm_buffer;


    //省略其他......
}

ii. 定义相关成员方法

// opensl_render.h


class OpenSLRender: public AudioRender {


private:


    // 省略成员变量...
    
    // 创建引擎
    bool CreateEngine();
    
    // 创建混音器
    bool CreateOutputMixer();
    
    // 创建播放器
    bool CreatePlayer();
    
    // 开始播放渲染
    void StartRender();
    
    // 音频数据压入缓冲队列
    void BlockEnqueue();


    // 检查是否发生错误
    bool CheckError(SLresult result, std::string hint);
    
    // 数据填充通知接口,后续会介绍这个方法的作用
    void static sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context);


public:
    OpenSLRender();
    ~OpenSLRender();


    void InitRender() override;
    void Render(uint8_t *pcm, int size) override;
    void ReleaseRender() override;

iii. 实现初始化流程

// opensl_render.cpp


OpenSLRender::OpenSLRender() {
}


OpenSLRender::~OpenSLRender() {
}


void OpenSLRender::InitRender() {
    if (!CreateEngine()) return;
    if (!CreateOutputMixer()) return;
    if (!CreatePlayer()) return;
}


// 省略其他......

创建引擎

// opensl_render.cpp


bool OpenSLRender::CreateEngine() {
    SLresult result = slCreateEngine(&m_engine_obj, 0, NULL, 0, NULL, NULL);
    if (CheckError(result, "Engine")) return false;


    result = (*m_engine_obj)->Realize(m_engine_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Engine Realize")) return false;


    result = (*m_engine_obj)->GetInterface(m_engine_obj, SL_IID_ENGINE, &m_engine);
    return !CheckError(result, "Engine Interface");
}

创建混音器

// opensl_render.cpp


bool OpenSLRender::CreateOutputMixer() {
    SLresult result = (*m_engine)->CreateOutputMix(m_engine, &m_output_mix_obj, 1, NULL, NULL);
    if (CheckError(result, "Output Mix")) return false;


    result = (*m_output_mix_obj)->Realize(m_output_mix_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Output Mix Realize")) return false;
    
    return true;
}

按照前面状态机的机制,先创建引擎对象 m_engine_obj、然后 Realize 初始化,然后再通过 GetInterface 方法,获取到引擎接口 m_engine 。

创建播放器

// opensl_render.cpp


bool OpenSLRender::CreatePlayer() {


    //【1.配置数据源 DataSource】----------------------
    
    //配置PCM格式信息
    SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, SL_QUEUE_BUFFER_COUNT};
    SLDataFormat_PCM pcm = {
            SL_DATAFORMAT_PCM,//播放pcm格式的数据
            (SLuint32)2,//2个声道(立体声)
            SL_SAMPLINGRATE_44_1,//44100hz的频率
            SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位
            SL_PCMSAMPLEFORMAT_FIXED_16,//和位数一致就行
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)
            SL_BYTEORDER_LITTLEENDIAN//结束标志
    };
    SLDataSource slDataSource = {&android_queue, &pcm};


    //【2.配置输出 DataSink】----------------------
    
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, m_output_mix_obj};
    SLDataSink slDataSink = {&outputMix, NULL};


    const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
    const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};


    //【3.创建播放器】----------------------
    
    SLresult result = (*m_engine)->CreateAudioPlayer(m_engine, &m_pcm_player_obj, &slDataSource, &slDataSink, 3, ids, req);
    if (CheckError(result, "Player")) return false;


    //初始化播放器
    result = (*m_pcm_player_obj)->Realize(m_pcm_player_obj, SL_BOOLEAN_FALSE);
    if (CheckError(result, "Player Realize")) return false;


    //【4.获取播放器接口】----------------------
    
    //得到接口后调用,获取Player接口
    result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_PLAY, &m_pcm_player);
    if (CheckError(result, "Player Interface")) return false;


    //获取音量接口
    result =  (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_VOLUME, &m_pcm_player_volume);
    if (CheckError(result, "Player Volume Interface")) return false;


    //【5. 获取缓冲队列接口】----------------------
    
    //注册回调缓冲区,获取缓冲队列接口
    result = (*m_pcm_player_obj)->GetInterface(m_pcm_player_obj, SL_IID_BUFFERQUEUE, &m_pcm_buffer);
    if (CheckError(result, "Player Queue Buffer")) return false;


    //注册缓冲接口回调
    result = (*m_pcm_buffer)->RegisterCallback(m_pcm_buffer, sReadPcmBufferCbFun, this);
    if (CheckError(result, "Register Callback Interface")) return false;


    LOGI(TAG, "OpenSL ES init success")
    
    return true;
}

播放器的初始化比较麻烦一些,不过都是根据前面介绍的初始化流程,按部就班。

配置数据源、输出器、以及初始化后,获取播放接口、音量调节接口等。

⚠️ 要注意的是最后一步,即代码中的第【5】。

数据源为 缓冲队列 的时候,需要获取一个缓冲接口,用于将数据填入缓冲区。

那么什么时候填充数据呢?这就是最后注册回调接口的作用。

我们需要注册一个回调函数到播放器中,当播放器中的数据播放完,就会回调这个方法,告诉我们:数据播完啦,要填充新的数据了。

sReadPcmBufferCbFun 是一个静态方法,可以推测出,OpenSL ES 播放音频内部是一个独立的线程,这个线程不断的读取缓冲区的数据,进行渲染,并在数据渲染完了以后,通过这个回调接口通知我们填充新数据。

  • 实现播放

启动 OpenSL ES 渲染很简单,只需调用播放器的播放接口,并且往缓冲区压入一帧数据,就可以启动渲染流程。

如果是播放一个 sdcard 的 pcm 文件,那只要在回调方法 sReadPcmBufferCbFun 中读取一帧数据填入即可。

但是,在我们这里没有那么简单,还记得我们的 BaseDeocder 中启动了一个解码线程吗?而 OpenSL ES 渲染也是一个独立的线程,因此,在这里变成两个线程的数据同步问题。

当然了,也可以将 FFmpeg 做成一个简单的解码模块,在 OpenSL ES 的渲染线程实现解码播放,处理起来就会简单得多。

为了解码流程的统一,这里将会采用两个独立线程。

i. 开启播放等待

上面已经提到,播放和解码是两个所以数据需要同步,因此,在初始化为 OpenSL ES 以后,不能马上开始进入播放状态,而是要等待解码数据第一帧,才能开始播放。

这里,通过线程的等待方式,等待数据。

在前面的 InitRender 方法中,首先初始化了 OpenSL ,在这方法的最后,我们让播放进入等待状态。

// opensl_render.cpp


OpenSLRender::OpenSLRender() {
}


OpenSLRender::~OpenSLRender() {
}


void OpenSLRender::InitRender() {
    if (!CreateEngine()) return;
    if (!CreateOutputMixer()) return;
    if (!ConfigPlayer()) return;
    
    // 开启线程,进入播放等待
    std::thread t(sRenderPcm, this);
    t.detach();
}


void OpenSLRender::sRenderPcm(OpenSLRender *that) {
    that->StartRender();
}


void OpenSLRender::StartRender() {
    while (m_data_queue.empty()) {
        WaitForCache();
    }
    (*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);
    sReadPcmBufferCbFun(m_pcm_buffer, this);
}


/**
 * 线程进入等待
 */
void OpenSLRender::WaitForCache() {
    pthread_mutex_lock(&m_cache_mutex);
    pthread_cond_wait(&m_cache_cond, &m_cache_mutex);
    pthread_mutex_unlock(&m_cache_mutex);
}


/**
 * 通知线程恢复执行
 */
void OpenSLRender::SendCacheReadySignal() {
    pthread_mutex_lock(&m_cache_mutex);
    pthread_cond_signal(&m_cache_cond);
    pthread_mutex_unlock(&m_cache_mutex);
}

最后的 StartRender() 方法是真正被线程执行的方法,进入该方法,首先判断数据缓冲队列是否有数据,没有则进入等待,直到数据到来。

其中,m_data_queue 是自定义的数据缓冲队列,如下:

// opensl_render.h


class OpenSLRender: public AudioRender {


private:
    /**
     * 封装 PCM 数据,主要用于实现数据内存的释放
     */
    class PcmData {
    public:
        PcmData(uint8_t *pcm, int size) {
            this->pcm = pcm;
            this->size = size;
        }
        ~PcmData() {
            if (pcm != NULL) {
                //释放已使用的内存
                free(pcm);
                pcm = NULL;
                used = false;
            }
        }
        uint8_t *pcm = NULL;
        int size = 0;
        bool used = false;
    };
    
    // 数据缓冲列表
    std::queue<PcmData *> m_data_queue;
    
    // 省略其他...
}

ii. 数据同步与播放

接下来,就来看看如何尽心数据同步与播放。

初始化 OpenSL 的时候,在最后注册了播放回调接口 sReadPcmBufferCbFun,首先来看看它的实现。

// opensl_render.cpp


void OpenSLRender::sReadPcmBufferCbFun(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {
    OpenSLRender *player = (OpenSLRender *)context;
    player->BlockEnqueue();
}


void OpenSLRender::BlockEnqueue() {
    if (m_pcm_player == NULL) return;


    // 先将已经使用过的数据移除
    while (!m_data_queue.empty()) {
        PcmData *pcm = m_data_queue.front();
        if (pcm->used) {
            m_data_queue.pop();
            delete pcm;
        } else {
            break;
        }
    }


    // 等待数据缓冲
    while (m_data_queue.empty() && m_pcm_player != NULL) {// if m_pcm_player is NULL, stop render
        WaitForCache();
    }


    PcmData *pcmData = m_data_queue.front();
    if (NULL != pcmData && m_pcm_player) {
        SLresult result = (*m_pcm_buffer)->Enqueue(m_pcm_buffer, pcmData->pcm, (SLuint32) pcmData->size);
        if (result == SL_RESULT_SUCCESS) {
            // 只做已经使用标记,在下一帧数据压入前移除
            // 保证数据能正常使用,否则可能会出现破音
            pcmData->used = true;
        }
    }
}

当 StartRender() 等待到缓冲数据的到来时,就会通过以下方法启动播放

(*m_pcm_player)->SetPlayState(m_pcm_player, SL_PLAYSTATE_PLAYING);
sReadPcmBufferCbFun(m_pcm_buffer, this);

这时候,经过一层层调用,最后调用的是 BlockEnqueue() 方法。

在这个方法中,

首先,将 m_data_queue 中已经使用的数据先删除,回收资源;

接着,判断是否还有未播放的缓冲数据,没有则进入等待;

最后,通过 (*m_pcm_buffer)->Enqueue() 方法,将数据压入 OpenSL 队列。

⚠️ 注:在接下来的播放过程中,OpenSL 只要播放完数据,就会自动回调 sReadPcmBufferCbFun 重新进入以上的播放流程。

  • 压入数据,开启播放

以上是整个播放的流程,最后还有关键的一点,来开启这个播放流程,那就是 AudioRender 定义的渲染播放接口 void Render(uint8_t *pcm, int size)

// opensl_render.cpp


void OpenSLRender::Render(uint8_t *pcm, int size) {
    if (m_pcm_player) {
        if (pcm != NULL && size > 0) {
            // 只缓存两帧数据,避免占用太多内存,导致内存申请失败,播放出现杂音
            while (m_data_queue.size() >= 2) {
                SendCacheReadySignal();
                usleep(20000);
            }
            
            // 将数据复制一份,并压入队列
            uint8_t *data = (uint8_t *) malloc(size);
            memcpy(data, pcm, size);
            
            PcmData *pcmData = new PcmData(pcm, size);
            m_data_queue.push(pcmData);
            
            // 通知播放线程推出等待,恢复播放
            SendCacheReadySignal();
        }
    } else {
        free(pcm);
    }
}

其实很简单,就是把解码得到的数据压入队列,并且发送数据缓冲准备完毕信号,通知播放线程可以进入播放了。

这样,就完成了整个流程,总结一下:

  1. 初始化 OpenSL ES ,开启「开始播放等待线程」,并进入播放等待;

  2. 将数据压入缓冲队列,通知播放线程恢复执行,进入播放;

  3. 开启播放时,将 OpenSL ES 设置为播放状态,并压入一帧数据;

  4. OpenSL ES 播放完一帧数据后,自动回调通知继续压入数据;

  5. 解码线程不断压入数据到缓冲队列;

  6. 在接下来的过程中,「OpenSL ES 播放线程」和「FFMpeg 解码线程」会同时执行,重复「2 ~ 5 」,并且在数据缓冲不足的情况下,「播放线程 」会等待「解码线程」压入数据后,再继续执行,直到完成播放,双方退出线程。

整合播放

上文中,已经完成 OpenSL ES 播放器的相关功能,并且实现了 AudioRander 中定义的接口,只要在 AudioDecoder 中正确调用就可以了。

如何调用也已经在第一节中介绍,现在只需把它们整合到 Player 中,就可以实现音频的播放了。

在播放器中,新增音频解码器和渲染器:

//player.h


class Player {
private:
    VideoDecoder *m_v_decoder;
    VideoRender *m_v_render;


    // 新增音频解码和渲染器
    AudioDecoder *m_a_decoder;
    AudioRender *m_a_render;


public:
    Player(JNIEnv *jniEnv, jstring path, jobject surface);
    ~Player();


    void play();
    void pause();
};

实例化音频解码器和渲染器:

// player.cpp


Player::Player(JNIEnv *jniEnv, jstring path, jobject surface) {
    m_v_decoder = new VideoDecoder(jniEnv, path);
    m_v_render = new NativeRender(jniEnv, surface);
    m_v_decoder->SetRender(m_v_render);


    // 实例化音频解码器和渲染器
    m_a_decoder = new AudioDecoder(jniEnv, path, false);
    m_a_render = new OpenSLRender();
    m_a_decoder->SetRender(m_a_render);
}


Player::~Player() {
    // 此处不需要 delete 成员指针
    // 在BaseDecoder中的线程已经使用智能指针,会自动释放
}


void Player::play() {
    if (m_v_decoder != NULL) {
        m_v_decoder->GoOn();
        m_a_decoder->GoOn();
    }
}


void Player::pause() {
    if (m_v_decoder != NULL) {
        m_v_decoder->Pause();
        m_a_decoder->Pause();
    }
}


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

一文读懂 YUV 的采样与格式

OpenGL 之 GPUImage 源码分析

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

相关文章:

  • 一个简简单单的Plugin入门
  • 给大家分享一下阿里三面的面试真题
  • WebRTC Android 开发学习环境搭建~
  • Android 11 最终 Beta 版发布,正式版即将到来!
  • NDK中使用 MediaCodec 编解码视频
  • 【资源分享】免费学 清华大学 · 游戏程序设计公开课啦!!!
  • 谈一谈Android上的SurfaceTexture
  • 你还不知道 OpenGL ES 和 EGL 的关系?
  • 腾讯云视频云巅峰论剑——王者对决,等你来评!
  • 高大上的非线性编辑是怎么一回事?
  • C++ 万字长文第二篇---拿下字节面试
  • Android自定义View-SVG动画
  • 谈一谈Flutter外接纹理
  • Android 11 强制用户使用系统相机?
  • 3A之自动白平衡(AWB)篇
  • 【159天】尚学堂高琪Java300集视频精华笔记(128)
  • 10个确保微服务与容器安全的最佳实践
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • happypack两次报错的问题
  • JavaScript 奇技淫巧
  • javascript面向对象之创建对象
  • JavaScript设计模式之工厂模式
  • JavaWeb(学习笔记二)
  • Java精华积累:初学者都应该搞懂的问题
  • linux学习笔记
  • Linux中的硬链接与软链接
  • Node + FFmpeg 实现Canvas动画导出视频
  • Spring Cloud Feign的两种使用姿势
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • uva 10370 Above Average
  • 蓝海存储开关机注意事项总结
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 手写一个CommonJS打包工具(一)
  • 微信小程序设置上一页数据
  • 一、python与pycharm的安装
  • 智能网联汽车信息安全
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • 完善智慧办公建设,小熊U租获京东数千万元A+轮融资 ...
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (Ruby)Ubuntu12.04安装Rails环境
  • (补)B+树一些思想
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (剑指Offer)面试题41:和为s的连续正数序列
  • (顺序)容器的好伴侣 --- 容器适配器
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • .NET CF命令行调试器MDbg入门(一)
  • .NET CORE 第一节 创建基本的 asp.net core
  • .net core使用RPC方式进行高效的HTTP服务访问