?在Android開發(fā)中,OpenSLES(Open Sound Library for Embedded Systems)是一個(gè) C/C++ 音頻庫(kù),提供了底層的音頻功能和處理接口。它是 Android 平臺(tái)上用于實(shí)現(xiàn)低延遲和高性能音頻功能的一種選擇。
本文的主線任務(wù)是描述 一個(gè)媒體文件通過(guò) FFmpeg 解碼后用 OpenSL ES 播放音頻的過(guò)程
因?yàn)榇a量很多,所以我直接從 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 函數(shù),上層傳遞媒體文件的全路徑到 Native 層(因?yàn)?FFmpeg 讀取文件需要),之后開啟準(zhǔn)備線程(就是要開始進(jìn)行異步對(duì)文件做處理了)
/**
* FFmpeg 對(duì)媒體文件 做處理
*/
void *task_prepare(void *args) {
const char *data_source = (const char *) args;
LOGI("data_source: %s", data_source)
formatContext = avformat_alloc_context(); // 給 媒體上下文 開辟內(nèi)存
av_dict_set(&dictionary, "timeout", "5000000", 0); // 設(shè)置字典參數(shù)
// TODO 打開媒體地址(如:文件路徑,直播地址rtmp等)
avformat_open_input(&formatContext, data_source, nullptr, &dictionary);
// 釋放字典(用完就釋放)
av_dict_free(&dictionary);
// TODO 查找媒體中的音視頻流的信息
avformat_find_stream_info(formatContext, nullptr);
// TODO 根據(jù)流信息,把 音頻流、視頻流 分開處理
for (int stream_index = 0; stream_index < formatContext->nb_streams; ++stream_index) {
AVStream *stream = formatContext->streams[stream_index]; // 獲取媒體流(視頻,音頻)
AVCodecParameters *parameters = stream->codecpar; // 從流中獲取 編解碼 參數(shù)
AVCodec *codec = avcodec_find_decoder(parameters->codec_id); // 獲取編解碼器
AVCodecContext *codecContext = avcodec_alloc_context3(codec); // 給 codecContext 開辟內(nèi)存
avcodec_parameters_to_context(codecContext, parameters); // codecContext 初始化
avcodec_open2(codecContext, codec, nullptr); // 打開 編解碼器
// 從編解碼參數(shù)中,區(qū)分流的類型,分別處理 (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); // 數(shù)據(jù)傳輸線程
} else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
LOGI("視頻流")
} else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_SUBTITLE) {
LOGI("字幕流")
}
}
return nullptr; // 函數(shù)的返回值是 void* 必須返回 nullptr
}
總結(jié):把文件路徑給到 FFmpeg 去讀取媒體文件,讀取后對(duì)媒體文件的流分開操作(視頻搞視頻的,音頻搞音頻的),這里音頻處理封裝了 AudioChannel 這個(gè)類。
需要注意的是,我為了盡可能減少代碼,省略了很多 FFmpeg 函數(shù)的返回值,比如 avcodec_parameters_to_context() 和?avcodec_open2() 都是有返回值的,非0為失敗,可以自行對(duì)錯(cuò)誤做處理
可以看到又開啟了一個(gè)線程:task_start
/**
* 將 AVPacket 傳給 AudioChannel
*/
void *task_start(void *args) {
while (1) {
if (audio_channel && audio_channel->packets.size() > 100) {
av_usleep(10 * 1000); // FFmpeg 的時(shí)間是微秒,所以這個(gè)是10毫秒
continue;
}
AVPacket *packet = av_packet_alloc(); // 給 AVPacket 開辟內(nèi)存
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;
// 隊(duì)列開始工作
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);
}
開啟兩個(gè)線程:一個(gè)解碼線程,一個(gè)播放線程(FFmpeg 解碼,OpenSL ES 播放)
void *task_audio_decode(void *args) { // 很頭痛,C的子線程函數(shù)必須是這個(gè)格式,所以要包裝一層....
auto *audio_channel = static_cast<AudioChannel *>(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; // 生產(chǎn)-消費(fèi)模型,所以可能會(huì)失敗,重來(lái)就行
}
// TODO 把 AVPacket 給 codecContext 解碼
ret = avcodec_send_packet(codecContext, pkt);
AVFrame *frame = av_frame_alloc(); // 給 AVFrame 開辟內(nèi)存
// TODO 從 codecContext 中拿解碼后的產(chǎn)物 AVFrame
ret = avcodec_receive_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN))
continue; // 音頻幀可能獲取失敗,重新拿一次
// 原始包 AVFrame 加入播放隊(duì)列
frames.insertToQueue(frame);
}
}
總結(jié):FFmpeg 將 AVPacket 解碼為 AVFrame 并塞進(jìn)播放隊(duì)列
接下來(lái)是播放線程:
void *task_audio_play(void *args) { // 頭痛頭痛
auto *audio_channel = static_cast<AudioChannel *>(args);
audio_channel->audio_play();
return nullptr;
}
/**
* 音頻播放
*/
void AudioChannel::audio_play() {
SLresult result; // 用于接收 執(zhí)行成功或者失敗的返回值
// TODO 創(chuàng)建引擎對(duì)象并獲取【引擎接口】
slCreateEngine(&engineObject, 0, 0,
0, 0, 0);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
// TODO 創(chuàng)建、初始化混音器
(*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// TODO 創(chuàng)建并初始化播放器
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10};
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, // PCM數(shù)據(jù)格式
2, // 聲道數(shù)
SL_SAMPLINGRATE_44_1, // 采樣率(每秒44100個(gè)點(diǎn))
SL_PCMSAMPLEFORMAT_FIXED_16, // 每秒采樣樣本 存放大小 16bit
SL_PCMSAMPLEFORMAT_FIXED_16, // 每個(gè)樣本位數(shù) 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 設(shè)置回調(diào)函數(shù)
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
(*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);
// TODO 設(shè)置播放狀態(tài)
(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
// 6.手動(dòng)激活回調(diào)函數(shù)
bqPlayerCallback(bqPlayerBufferQueue, this);
}
怎么樣?頭痛嗎,我也很頭痛,代碼量真的挺多的(我還把返回值去掉了的),這個(gè) OpenSL ES 的使用真的沒(méi)十年腦血栓設(shè)計(jì)不出來(lái),其實(shí)基本上都是樣板代碼,大概知道什么意思就行了,關(guān)鍵是回調(diào)函數(shù):
/**
* 真正播放的函數(shù),這個(gè)函數(shù)會(huì)一直調(diào)用
* 關(guān)鍵是 SLAndroidSimpleBufferQueueItf 這個(gè)結(jié)構(gòu)體,往這個(gè)結(jié)構(gòu)體的隊(duì)列加 PCM 數(shù)據(jù)就能播放了
*/
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *args) {
auto *audio_channel = static_cast<AudioChannel *>(args);
int pcm_size = audio_channel->getPCM();
(*bq)->Enqueue(bq, audio_channel->out_buffers, pcm_size);
}
?播放音頻的關(guān)鍵就是這個(gè),往 Enqueue 上加 PCM 數(shù)據(jù)就能播放了
本篇文章僅僅是實(shí)現(xiàn)了 FFmpeg 和 OpenGL ES 配和播放媒體文件音頻的功能,其中有非常多的細(xì)節(jié)沒(méi)有去完善(比如函數(shù)錯(cuò)誤返回值的處理、內(nèi)存泄漏等等),因?yàn)?strong>我為了更好的閱讀和理解 FFmpeg 和 OpenSL ES,對(duì)非主線代碼做了刪減,所以讀者可以自行添加文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-481991.html
源代碼鏈接:https://github.com/yinwokang/Android-OpenSLES/文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-481991.html
到了這里,關(guān)于Android FFmpeg 解碼 OpenSL ES 播放音頻的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!