Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解

更新:11-07 民间故事 我要投稿 纠错 投诉

大家好,如果您还对Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解不太了解,没有关系,今天就由本站为大家分享Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解的知识,包括的问题都会给大家分析到,还望可以解决大家的问题,下面我们就开始吧!

教程代码:【Github传送门】

目录

一、Android音视频硬解码篇:

1.音视频基础知识2.音视频硬解码流程:封装基本解码框架3.音视频播放:音视频同步4.音视频解封装与封装:生成MP4

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

1、初步了解OpenGL ES2、使用OpenGL渲染视频图像3、OpenGL渲染多个视频、实现画中画4、深入了解OpenGL的EGL5、OpenGL FBO数据缓冲区6、Android音视频硬编码:生成MP4

三、Android FFmpeg音视频解码篇

1、FFmpeg so库编译2、Android介绍FFmpeg3、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

类AudioDecoder: 公共BaseDecoder {

私人:

const char *TAG="音频解码器";

//音频转换器

SwrContext *m_swr=NULL;

//音频渲染器

音频渲染*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

类AudioDecoder: 公共BaseDecoder {

私人:

//省略成员变量.

/**

* 初始化转换工具

*/

无效InitSwr();

/**

* 初始化输出缓冲区

*/

无效InitOutBuffer();

/**

* 初始化渲染器

*/

无效初始化渲染();

/**

* 释放缓冲区

*/

无效ReleaseOutBuffer();

/**

* 采样格式:16位

*/

AVSampleFormat GetSampleFmt() {

返回AV_SAMPLE_FMT_S16;

}

/**

*目标采样率

*/

int GetSampleRate(int spr) {

返回AUDIO_DEST_SAMPLE_RATE; //44100Hz

}

公共:

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

音频解码器();

无效SetRender(AudioRender *渲染);

受保护:

无效准备(JNIEnv * env)覆盖;

void Render(AVFrame *frame) 覆盖;

void Release() 覆盖;

bool NeedLoopDecode() 覆盖{

返回真;

}

AVMediaType GetMediaType() 覆盖{

返回AVMEDIA_TYPE_AUDIO;

}

const char *const LogSpec() 覆盖{

返回“音频”;

};

};上面的代码并不复杂。它们都是初始化相关的方法以及BaseDecoder中定义的抽象方法的实现。

我们重点关注这两个方法:

/**

* 采样格式:16位

*/

AVSampleFormat GetSampleFmt() {

返回AV_SAMPLE_FMT_S16;

}

/**

*目标采样率

*/

int GetSampleRate(int spr) {

返回AUDIO_DEST_SAMPLE_RATE; //44100Hz

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

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

播放和编码配置也略有不同。这里,采样位数为16位,采样率为44100。

接下来我们看看具体的实现。

实现解码流程//a_decoder.cpp

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

env, 路径, forSynthesizer) {

}

无效AudioDecoder:~AudioDecoder() {

如果(m_render!=NULL){

删除m_render;

}

}

无效AudioDecoder:SetRender(AudioRender *渲染){

m_render=渲染;

}

void AudioDecoder:Prepare(JNIEnv *env) {

初始化Swr();

初始化输出缓冲区();

初始化渲染();

}

//省略其他.i. 初始化重点关注Prepare方法。该方法将在基类BaseDecoder初始化解码器后被调用。

在Prepare方法中,调用了以下内容:

InitSwr(),初始化转换器

InitOutBuffer(),初始化输出缓冲区

InitRender(),初始化渲染器。下面详细分析如何配置初始化参数。

SwrContext配置:

//a_解码器.cpp

无效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_解码器.cpp

无效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(

空,AUDIO_DEST_CHANNEL_COUNTS,

m_dest_nb_sample, GetSampleFmt(), 1);

m_out_buffer[0]=(uint8_t *) malloc(m_dest_data_size);

}

无效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 **)帧数据,帧nb_样本);

如果(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

无效AudioDecoder:Release(){

如果(m_swr!=NULL){

swr_free(m_swr);

}

如果(m_render!=NULL){

m_render-ReleaseRender();

}

释放输出缓冲区();

}

无效AudioDecoder:ReleaseOutBuffer(){

if (m_out_buffer[0] !=NULL) {

自由(m_out_buffer [0]);

m_out_buffer[0]=NULL;

}

}解码完成后,退出播放时,需要释放转换器和输出缓冲区。

二、接入 OpenSL ES

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

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

什么是 OpenSL ES

OpenSL ES(嵌入式系统开放声音库)是一个免许可、跨平台的硬件音频加速API,针对嵌入式系统进行了精心优化。为嵌入式移动多媒体设备上的本地应用开发者提供了标准化、高性能、低响应时间的音频功能实现方法,并可实现软/硬件音频性能的直接跨平台部署,降低执行难度。

OpenSL ES 提供哪些功能

OpenSL主要提供录音和回放功能。本文重点介绍播放功能。

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

我们使用FFmpeg进行解码,因此播放源是PCM。

OpenSL ES 状态机

OpenSL ES是一个基于C语言开发的库,但其接口是采用面向对象的编程思想编写的。它的接口不能直接调用,必须在对象创建和初始化后通过对象调用。

Object 和 InterfaceOpenSL ES提供了一系列Object,它们有一些基本的操作方法,如Realize、Resume、GetState、Destroy、GetInterface等。

一个对象有一个或多个接口方法,但一个接口只属于一个对象。

如果要调用Object中的Interface方法,首先要通过Object的GetInterface获取接口Interface,然后通过获取到的Interface来调用。

例如:

//创建引擎

SLObjectItf m_engine_obj=NULL;

SLresult 结果=slCreateEngine(m_engine_obj, 0, NULL, 0, NULL, NULL);

//初始化引擎

结果=(*m_engine_obj)-实现(m_engine_obj, SL_BOOLEAN_FALSE);

//获取引擎接口

SLEngineItf m_engine=NULL;

结果=(*m_engine_obj)-GetInterface(m_engine_obj, SL_IID_ENGINE, m_engine);可见Object需要创建并初始化后才能使用。这就是OpenSL ES中的状态机机制。

OpenSL ES状态机Object创建完成后,就进入Unrealized状态。调用Realize()方法后,会分配相关的内存资源,并进入Realized状态。只有这样才能获取并使用Object的Interface方法。

在后续执行过程中,如果发生错误,Object将进入Suspending状态。调用Resume()恢复到Realized状态。

OpenSL ES 播放初始化配置

我们看一下官方的播放流程图。

OpenSL ES 播放流程这张图非常清楚地展示了OpenSL ES 的运作方式。

OpenSL ES 播放所需的两个核心是Audio Player 和Output Mix,即播放和混音器,这两个核心都是由OpenSL ES 引擎Engine 创建的。

因此,整个初始化过程可以概括为:

通过Engine创建一个Output Mix/混音器,并使用混音器作为参数。创建音频播放器/播放器时,将其绑定到音频播放器作为输出。

DataSource 和 DataSink创建音频播放器时,需要为其设置数据源和输出目标,以便播放器知道如何获取播放数据以及将数据输出到哪里进行播放。

这需要使用两个OpenSL ES 结构:DataSource 和DataSink。

typedef 结构SLDataSource_ {

无效*pLocator;

无效*pFormat;

}SL 数据源;

typedef 结构SLDataSink_ {

无效*pLocator;

无效*pFormat;

} SLDataSink;其中,

SLDataSource pLocator 有以下类型:

SLDataLocator_地址

SLDataLocator_BufferQueue

SLDataLocator_IODevice

SLDataLocator_MIDIBufferQueue

SLDataLocator_URI 使用SLDataLocator_BufferQueue 缓冲队列播放PCM。

SLDataSink pLocator 一般为SL_DATALOCATOR_OUTPUTMIX。

另一个参数pFormat是数据的格式。

实现渲染流程

在连接OpenSL ES之前,首先定义上面提到的音频渲染接口,以方便标准化和扩展。

//audio_render.h

音频渲染类{

公共:

虚拟无效InitRender()=0;

虚拟无效渲染(uint8_t *pcm,int大小)=0;

虚拟无效ReleaseRender()=0;

虚拟~AudioRender() {}

};在CMakeList.txt中,开启OpenSL ES支持

#CMakeList.txt

# 省略其他.

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

目标链接库(

本机库

阿乌蒂尔

采样

AV编解码器

AV过滤器

swscale

AV格式

AV设备

-landroid

# 开启opensl es支持

开放式SLES

# 将目标库链接到日志库

# 包含在NDK 中。

${log-lib} )初始化i. 定义成员变量首先定义需要用到的引擎、混音器、播放器、缓冲队列接口、音量调节接口等。

//opensl_render.h

类OpenSLRender: 公共AudioRender {

私人:

//引擎接口

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;

SLPl

ayItf 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以后,不能马上开始进入播放状态,而是要等待解码数据第一帧,才能开始播放。 这里,通过线程的等待方式,等待数据。 在前面的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::queuem_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); } }其实很简单,就是把解码得到的数据压入队列,并且发送数据缓冲准备完毕信号,通知播放线程可以进入播放了。 这样,就完成了整个流程,总结一下: 初始化OpenSL,开启「开始播放等待线程」,并进入播放等待;将数据压入缓冲队列,通知播放线程恢复执行,进入播放;开启播放时,将OpenSL设置为播放状态,并压入一帧数据;OpenSL播放完一帧数据后,自动回调通知继续压入数据;解码线程不断压入数据到缓冲队列;在接下来的过程中,「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(); }

用户评论

陌上花

终于看到了关于 OpenSL ES 解码播放的文章!期待详细教程,自己也想要尝试用 FFmpeg 和 OpenSL ES 在 Android 上实现音频解码。

    有7位网友表示赞同!

打个酱油卖个萌

FFmpeg 的音视频编解码功能简直太强了,OpenSL ES 也很实用,这俩组合起来玩音频开发真是太棒了。

    有12位网友表示赞同!

不相忘

Android 音频开发我有点苦手啊, 这篇文章正好能帮到我!

    有7位网友表示赞同!

疲倦了

之前也研究过 FFmpeg 的音频处理, OpenSL ES 确实更好用一些,交互比较简便。

    有11位网友表示赞同!

执妄

要实现高品质的音频播放体验,FFmpeg 和 OpenSL ES 是必不可少的工具,期待这篇文章能分享一些经验。

    有13位网友表示赞同!

々爱被冰凝固ゝ

学习 Android 开发,音视频编解码篇是必经之路啊! 希望能看到更浅显易懂的讲解。

    有16位网友表示赞同!

?亡梦爱人

OpenSL ES 的 API 设计的还挺好的,上手速度比较快,感觉比其他音频库要容易一些。

    有5位网友表示赞同!

身影

期待这篇文章能详细介绍 FFmpeg 和 OpenSL ES 的结合方式,以及一些常见音频码流的解码技巧。

    有17位网友表示赞同!

?娘子汉

我正在做一款支持多种音频格式播放的 Android 应用,FFmpeg 和 OpenSL ES 可以帮我解决很多难题。

    有9位网友表示赞同!

£烟消云散

Android 设备上的系统自带媒体框架有时候表现不太稳定,用 FFmpeg 和 OpenSL ES 自行编解码可以提升可靠性 。

    有8位网友表示赞同!

一生荒唐

这个平台上分享的 FFmpeg 相关内容不多啊,期待这篇文章能提供一些新的思路和解决方案。

    有5位网友表示赞同!

赋流云

FFmpeg 是开源的,这意味着它是一个非常灵活强大的工具,配合 OpenSL ES 可以实现各种音频处理功能。

    有12位网友表示赞同!

野兽之美

学习音频开发真的很需要时间和耐心,希望这篇文章能给我一些启发,让我更快地入门。

    有18位网友表示赞同!

矜暮

做 Android 音频开发之前,我从来没有接触过 FFmpeg 和 OpenSL ES,真是太惊喜了!

    有6位网友表示赞同!

oО清风挽发oО

文章标题说的“打怪升级”真有意思,感觉自己学习音频开发也是一个不断挑战、突破自己的过程。

    有5位网友表示赞同!

有阳光还感觉冷

最近想尝试用 Android 开发一些新颖的音视频应用,这篇文章能给我带来很多灵感!

    有7位网友表示赞同!

゛指尖的阳光丶

希望这篇文章能够深入浅出地讲解 FFmpeg 和 OpenSL ES 的使用方法,让新手朋友也能轻松理解。

    有19位网友表示赞同!

可儿

做Android开发的朋友都知道音频解码的重要性, 这篇文章来得真是太及时了 !

    有19位网友表示赞同!

七级床震

我一直想学习一下FFmpeg 和 OpenSL ES,这个主题的文章正好适合我!

    有11位网友表示赞同!

【Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活

上一篇:探索快速赚取零花钱的十大手机应用 下一篇:《奇幻冒险:穿靴子的猫的故事》