大家好,如果您还对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(); }【Android音视频开发进阶:FFmpeg编解码与OpenSL ES音频播放技术详解】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于看到了关于 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位网友表示赞同!
文章标题说的“打怪升级”真有意思,感觉自己学习音频开发也是一个不断挑战、突破自己的过程。
有5位网友表示赞同!
最近想尝试用 Android 开发一些新颖的音视频应用,这篇文章能给我带来很多灵感!
有7位网友表示赞同!
希望这篇文章能够深入浅出地讲解 FFmpeg 和 OpenSL ES 的使用方法,让新手朋友也能轻松理解。
有19位网友表示赞同!
做Android开发的朋友都知道音频解码的重要性, 这篇文章来得真是太及时了 !
有19位网友表示赞同!
我一直想学习一下FFmpeg 和 OpenSL ES,这个主题的文章正好适合我!
有11位网友表示赞同!