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

声 明

首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。android

码字不易,转载请注明出处!git

教程代码:【Github传送门

目录

1、Android音视频硬解码篇:

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

3、Android FFmpeg音视频解码篇

本文你能够了解到

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

1、音频解码

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

本文就利用已经定义好的解码基类 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;   //...... } 复制代码

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

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

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

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;  } } 复制代码

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

2、接入 OpenSL ES

Android 上播放音频,一般使用的 AudioTrack ,可是在 NDK 层,没有提供直接的类,须要经过 NDK 调用 Java 层的方式,回调实现播放。相对来讲比较麻烦,效率也比较低。

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

什么是 OpenSL ES

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

OpenSL ES 提供哪些功能

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

播放源支持 PCMsdcard资源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 方法,必需要经过 ObjectGetInterface 先获取到接口 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 中的状态机机制。

OpenSL ES 状态机
OpenSL ES 状态机

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

在后续执行过程当中,若是出现错误,Object 会进入 Suspended 状态。调用 Resume() 能够恢复到 Realized 状态。

OpenSL ES 播放初始化配置

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

OpenSL ES 播放流程
OpenSL ES 播放流程

这张图很是清晰的展现了 OpenSL ES 是如何运做的。

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

因此,整个初始化流程能够总结为:

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

  • DataSource 和 DataSink

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

这就须要用到 OpenSL ES 的两个结构体 DataSourceDataSink

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 渲染很简单,只需调用播放器的播放接口,而且往缓冲区压入一帧数据,就能够启动渲染流程。

若是是播放一个 sdcardpcm 文件,那只要在回调方法 sReadPcmBufferCbFun 中读取一帧数据填入便可。

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

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

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

i. 开启播放等待

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

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

在前面的 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 ,开启「开始播放等待线程」,并进入播放等待;
  2. 将数据压入缓冲队列,通知播放线程恢复执行,进入播放;
  3. 开启播放时,将 OpenSL 设置为播放状态,并压入一帧数据;
  4. OpenSL 播放完一帧数据后,自动回调通知继续压入数据;
  5. 解码线程不断压入数据到缓冲队列;
  6. 在接下来的过程当中,「OpenSL ES 播放线程」和「FFMpeg 解码线程」会同时执行,重复「2 ~ 5 」,而且在数据缓冲不足的状况下,「播放线程 」会等待「解码线程」压入数据后,再继续执行,直到完成播放,双方退出线程。

3、整合播放

上文中,已经完成 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();  } } 复制代码
相关文章
相关标签/搜索