在Android开发中,OpenSLES(Open Sound Library for Embedded Systems)是一个 C/C++ 音频库,提供了底层的音频功能和处理接口。它是 Android 平台上用于实现低延迟和高性能音频功能的一种选择。
本文的主线任务是描述 一个媒体文件通过 FFmpeg 解码后用 OpenSL ES 播放音频的过程
因为代码量很多,所以我直接从 Native 层开始了,看不懂的可以下载源代码配合着看(末尾)
extern "C"
JNIEXPORT void JNICALL
Java_cn_wk_opensl_1demo_MainActivity_audioPlayer(JNIEnv *env, jobject thiz, jstring dataStr) {
const char *dataSource = env->GetStringUTFChars(dataStr, nullptr);
pthread_create(&pid_prepare, nullptr, task_prepare, (void *) dataSource);
}
这是 JNI 函数,上层传递媒体文件的全路径到 Native 层(因为 FFmpeg 读取文件需要),之后开启准备线程(就是要开始进行异步对文件做处理了)
/**
* FFmpeg 对媒体文件 做处理
*/
void *task_prepare(void *args) {
const char *data_source = (const char *) args;
LOGI("data_source: %s", data_source)
formatContext = avformat_alloc_context(); // 给 媒体上下文 开辟内存
av_dict_set(&dictionary, "timeout", "5000000", 0); // 设置字典参数
// TODO 打开媒体地址(如:文件路径,直播地址rtmp等)
avformat_open_input(&formatContext, data_source, nullptr, &dictionary);
// 释放字典(用完就释放)
av_dict_free(&dictionary);
// TODO 查找媒体中的音视频流的信息
avformat_find_stream_info(formatContext, nullptr);
// TODO 根据流信息,把 音频流、视频流 分开处理
for (int stream_index = 0; stream_index < formatContext->nb_streams; ++stream_index) {
AVStream *stream = formatContext->streams[stream_index]; // 获取媒体流(视频,音频)
AVCodecParameters *parameters = stream->codecpar; // 从流中获取 编解码 参数
AVCodec *codec = avcodec_find_decoder(parameters->codec_id); // 获取编解码器
AVCodecContext *codecContext = avcodec_alloc_context3(codec); // 给 codecContext 开辟内存
avcodec_parameters_to_context(codecContext, parameters); // codecContext 初始化
avcodec_open2(codecContext, codec, nullptr); // 打开 编解码器
// 从编解码参数中,区分流的类型,分别处理 (codec_type == 音频流/视频流/字幕流)
if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO) {
LOGI("音频流")
audio_channel = new AudioChannel(codecContext); // codecContext 才是真正干活的
audio_channel->start(); // 开启 解码线程 和 播放线程
pthread_create(&pid_start, nullptr, task_start, nullptr); // 数据传输线程
} else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
LOGI("视频流")
} else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_SUBTITLE) {
LOGI("字幕流")
}
}
return nullptr; // 函数的返回值是 void* 必须返回 nullptr
}
总结:把文件路径给到 FFmpeg 去读取媒体文件,读取后对媒体文件的流分开操作(视频搞视频的,音频搞音频的),这里音频处理封装了 AudioChannel 这个类。
需要注意的是,我为了尽可能减少代码,省略了很多 FFmpeg 函数的返回值,比如 avcodec_parameters_to_context() 和 avcodec_open2() 都是有返回值的,非0为失败,可以自行对错误做处理
可以看到又开启了一个线程:task_start
/**
* 将 AVPacket 传给 AudioChannel
*/
void *task_start(void *args) {
while (1) {
if (audio_channel && audio_channel->packets.size() > 100) {
av_usleep(10 * 1000); // FFmpeg 的时间是微秒,所以这个是10毫秒
continue;
}
AVPacket *packet = av_packet_alloc(); // 给 AVPacket 开辟内存
int ret = av_read_frame(formatContext, packet); // 从 formatContext 读帧赋值到 AVPacket
if (!ret) {
audio_channel->packets.insertToQueue(packet);
} else {
break;
}
}
return nullptr;
}
工具线程:将 FFmpeg 读取到的 AVPacket 传给 AudioChannel 而已
重头戏:AudioChannel
void AudioChannel::start() {
isPlaying = 1;
// 队列开始工作
packets.setWork(1);
frames.setWork(1);
// 音频解码线程
pthread_create(&pid_audio_decode, nullptr, task_audio_decode, this);
// 音频播放线程
pthread_create(&pid_audio_play, nullptr, task_audio_play, this);
}
开启两个线程:一个解码线程,一个播放线程(FFmpeg 解码,OpenSL ES 播放)
void *task_audio_decode(void *args) { // 很头痛,C的子线程函数必须是这个格式,所以要包装一层....
auto *audio_channel = static_cast(args);
audio_channel->audio_decode();
return nullptr;
}
/**
* 音频解码:codecContext 把 AVPacket 解码为 AVFrame
*/
void AudioChannel::audio_decode() {
AVPacket *pkt = nullptr;
while (isPlaying) {
if (isPlaying && frames.size() > 100) {
av_usleep(10 * 1000);
continue;
}
int ret = packets.getQueueAndDel(pkt);
if (!ret) {
continue; // 生产-消费模型,所以可能会失败,重来就行
}
// TODO 把 AVPacket 给 codecContext 解码
ret = avcodec_send_packet(codecContext, pkt);
AVFrame *frame = av_frame_alloc(); // 给 AVFrame 开辟内存
// TODO 从 codecContext 中拿解码后的产物 AVFrame
ret = avcodec_receive_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN))
continue; // 音频帧可能获取失败,重新拿一次
// 原始包 AVFrame 加入播放队列
frames.insertToQueue(frame);
}
}
总结:FFmpeg 将 AVPacket 解码为 AVFrame 并塞进播放队列
接下来是播放线程:
void *task_audio_play(void *args) { // 头痛头痛
auto *audio_channel = static_cast(args);
audio_channel->audio_play();
return nullptr;
}
/**
* 音频播放
*/
void AudioChannel::audio_play() {
SLresult result; // 用于接收 执行成功或者失败的返回值
// TODO 创建引擎对象并获取【引擎接口】
slCreateEngine(&engineObject, 0, 0,
0, 0, 0);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
// TODO 创建、初始化混音器
(*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// TODO 创建并初始化播放器
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10};
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, // PCM数据格式
2, // 声道数
SL_SAMPLINGRATE_44_1, // 采样率(每秒44100个点)
SL_PCMSAMPLEFORMAT_FIXED_16, // 每秒采样样本 存放大小 16bit
SL_PCMSAMPLEFORMAT_FIXED_16, // 每个样本位数 16bit
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // 前左声道 前右声道
SL_BYTEORDER_LITTLEENDIAN};
SLDataSource audioSrc = {&loc_bufq, &format_pcm};
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
(*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &audioSrc, &audioSnk, 1, ids, req);
(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
// TODO 设置回调函数
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
(*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);
// TODO 设置播放状态
(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
// 6.手动激活回调函数
bqPlayerCallback(bqPlayerBufferQueue, this);
}
怎么样?头痛吗,我也很头痛,代码量真的挺多的(我还把返回值去掉了的),这个 OpenSL ES 的使用真的没十年脑血栓设计不出来,其实基本上都是样板代码,大概知道什么意思就行了,关键是回调函数:
/**
* 真正播放的函数,这个函数会一直调用
* 关键是 SLAndroidSimpleBufferQueueItf 这个结构体,往这个结构体的队列加 PCM 数据就能播放了
*/
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *args) {
auto *audio_channel = static_cast(args);
int pcm_size = audio_channel->getPCM();
(*bq)->Enqueue(bq, audio_channel->out_buffers, pcm_size);
}
播放音频的关键就是这个,往 Enqueue 上加 PCM 数据就能播放了
本篇文章仅仅是实现了 FFmpeg 和 OpenGL ES 配和播放媒体文件音频的功能,其中有非常多的细节没有去完善(比如函数错误返回值的处理、内存泄漏等等),因为我为了更好的阅读和理解 FFmpeg 和 OpenSL ES,对非主线代码做了删减,所以读者可以自行添加
源代码链接:https://github.com/yinwokang/Android-OpenSLES/