NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。
音视频播放器系列:
NDK FFmpeg音视频播放器一
NDK FFmpeg音视频播放器二
NDK FFmpeg音视频播放器三
NDK FFmpeg音视频播放器四
NDK FFmpeg音视频播放器五
NDK FFmpeg音视频播放器六
音视频一和二节已经实现了视频播放,本文主要是通过OpenSLES来完成音频的播放。
主要内容如下:
1.音频解码工作。
2.OpenSLES使用。
3.音频重采样工作。
用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija
一、音频解码
音频的播放流程跟第二节的视频解码播放流程基本一致,创建两个线程,
第一个线程:取出队列的音频压缩包,进行解码,解码后得到原始包,再push队列中去;
第二线线程:从队列取出音频原始包,播放(通过OpenSLES完成播放工作)。
void AudioChannel::start() {
LOGI("AudioChannel::start()");
isPlaying = 1;
// 队列开始工作了
packets.setWork(1);
frames.setWork(1);
// 第一个线程: 音频:取出队列的压缩包 进行解码 解码后的原始包 再push队列中去
pthread_create(&pid_audio_decode, 0, task_audio_decode, this);
// 第二线线程:音频:从队列取出原始包,播放
pthread_create(&pid_audio_play, 0, task_audio_play, this);
}
/**
* 函数指针 解码
* @param video_channel
* @return
*/
void *task_audio_decode(void *audio_channel) {
auto *audio_channel_ = static_cast(audio_channel);
audio_channel_->audio_decode();
return 0;
}
/**
* 第一个线程: 音频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去(音频:PCM数据)
*/
void AudioChannel::audio_decode() {
LOGI("AudioChannel::audio_decode()");
AVPacket *pkt = 0;
while (isPlaying) {
// 获取AVPacket * 压缩包
int result = packets.getQueueAndDel(pkt);
if (!isPlaying) {
// 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
break;
}
if (!result) {
// 获取失败,可能是压缩包数据还没有加入队列,继续获取
continue;
}
// 1.发送pkt(压缩包)给缓冲区,@return 0 on success
result = avcodec_send_packet(codecContext, pkt);
// FFmpeg源码缓存一份pkt,释放即可
releaseAVPacket(&pkt);
if (result) {
// avcodec_send_packet 出现了错误
break;
}
AVFrame *frame = av_frame_alloc();
// 2.从缓冲区拿出来(原始包),@return 0: success
result = avcodec_receive_frame(codecContext, frame);
if (result == AVERROR(EAGAIN)) {
// 有可能音频帧,也会获取失败,重新拿一次
continue;
} else if (result != 0) {
// avcodec_receive_frame 出现了错误
break;
}
// 拿到了原始包,并将原始包push到队列 PCM数据
frames.insertToQueue(frame);
}
// 解码获取原始包后,释放压缩包
releaseAVPacket(&pkt);
}
二、OpenSLES
1)OpenSLES概述
OpenSL ES 是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。该库都允许使用C或C++来实现高性能,低延迟的音频操作。 Android的OpenSL ES库同样位于NDK的platforms文件夹内。
2)环境配置
CMakeLists.txt 中添加OpenSLES库的链接:
target_link_libraries(
native-lib
......
OpenSLES
)
引入OpenSLES的头文件:
#include
#include
声明要用到的一些结构体:
//引擎
SLObjectItf engineObject = 0;
//引擎接口
SLEngineItf engineInterface = 0;
//混音器
SLObjectItf outputMixObject = 0;
//播放器
SLObjectItf bqPlayerObject = 0;
//播放器接口
SLPlayItf bqPlayerPlay = 0;
//播放器队列接口
SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue = 0;
3)开发流程七步曲
一部曲:创建引擎并获取引擎接口
SLresult result;
// 1.1 创建引擎对象:SLObjectItf engineObject
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.2 初始化引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.3 获取引擎接口 SLEngineItf engineInterface
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE,
&engineInterface);
if (SL_RESULT_SUCCESS != result) {
return;
}
二部曲:设置混音器
// 2.1 创建混音器:SLObjectItf outputMixObject
result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject,
0,0, 0);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 2.2 初始化混音器
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
//不启用混响可以不用获取混音器接口
// 获得混音器接口
//result = (*outputMixObject)->GetInterface(outputMixObject,
SL_IID_ENVIRONMENTALREVERB,
// &outputMixEnvironmentalReverb);
//if (SL_RESULT_SUCCESS == result) {
//设置混响 : 默认。
//SL_I3DL2_ENVIRONMENT_PRESET_ROOM: 室内
//SL_I3DL2_ENVIRONMENT_PRESET_AUDITORIUM : 礼堂 等
//const SLEnvironmentalReverbSettings settings =
SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;
//(*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
// outputMixEnvironmentalReverb, &settings);
//}
三部曲:创建播放器
PCM是不能直接播放,mp3可以直接播放(参数集),PCM无参数集,需要手动设置参数。
//3.1 配置输入声音信息
//创建buffer缓冲类型的队列 2个队列
SLDataLocator_AndroidSimpleBufferQueue loc_bufq =
{SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
//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:小端模式
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN};
//数据源 将上述配置信息放到这个数据源中
SLDataSource audioSrc = {&loc_bufq, &format_pcm};
//3.2 配置音轨(输出)
//设置混音器
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};
//3.3 创建播放器
result = (*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject,
&audioSrc, &audioSnk, 1, ids, req);
if (SL_RESULT_SUCCESS != result) {
return;
}
//3.4 初始化播放器:SLObjectItf bqPlayerObject
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
//3.5 获取播放器接口:SLPlayItf bqPlayerPlay
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY,
&bqPlayerPlay);
if (SL_RESULT_SUCCESS != result) {
return;
}
四部曲:设置播放回调函数
//4.1 获取播放器队列接口:SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
&bqPlayerBufferQueue);
//4.2 设置回调 void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void
*context)
(*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback,
this);
//4.3 创建回调函数
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
...
}
五部曲:设置播放器状态为播放状态
(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
六部曲:手动激活回调函数
bqPlayerCallback(bqPlayerBufferQueue, this);
七部曲:释放
//7.1 设置停止状态
if (bqPlayerPlay) {
(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
bqPlayerPlay = 0;
}
//7.2 销毁播放器
if (bqPlayerObject) {
(*bqPlayerObject)->Destroy(bqPlayerObject);
bqPlayerObject = 0;
bqPlayerBufferQueue = 0;
}
//7.3 销毁混音器
if (outputMixObject) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = 0;
}
//7.4 销毁引擎
if (engineObject) {
(*engineObject)->Destroy(engineObject);
engineObject = 0;
engineInterface = 0;
}
三、音频重采样
1)音频三要素
1.采样率 44100 48000
2.位声/采用格式大小 16bit == 2字节
3.声道数 2 --- 人类就是两个耳朵
out_channels = av_get_channel_layout_nb_channels(
AV_CH_LAYOUT_STEREO); // STEREO:双声道类型 == 获取 声道数 2
out_sample_size = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16); // 每个sample是16 bit == 2字节
out_sample_rate = 44100; // 采样率
// out_buffers_size = 176,400
out_buffers_size = out_sample_rate * out_sample_size * out_channels; // 44100 * 2 * 2 = 176,400
out_buffers = static_cast(malloc(out_buffers_size)); // 堆区开辟
2)音频压缩数据包 AAC
1.采样率 44100
2.位声/采用格式大小 32bit == 4字节 算法效率高 浮点运算高
3.声道数 2
3)重采样 音频格式转换(将AAC 位声/采用格式大小 32bit --> 16bit)
swr_ctx = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, out_sample_rate,
codecContext->channel_layout, codecContext->sample_fmt,
codecContext->sample_rate, 0, 0);
// 初始化 重采样上下文
swr_init(swr_ctx);
/**
* 1.out_buffers 给予数据
* 2.out_buffers 给予数据的大小计算工作
* @return 大小还要计算,因为我们还要做重采样工作,重采样之后,大小不同了
*/
int AudioChannel::getPCM() {
LOGI("AudioChannel::getPCM");
int pcm_data_size = 0;
// 从frames队列中,获取PCM数据,frame->data == PCM数据(待 重采样 32bit)
AVFrame *frame = 0;
while (isPlaying) {
int result = frames.getQueueAndDel(frame);
if (!isPlaying) {
break; // 如果关闭了播放,跳出循环,releaseAVPacket(&pkt);
}
if (!result) {
continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
}
/**
* 开始重采样
* 如:来源:10个48000 ----> 目标:44100 11个44100
* 获取单通道的样本数 (计算目标样本数: ? 10个48000 ---> 48000/44100因为除不尽 11个44100)
* 参数1:swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples 获取下一个输入样本相对于下一个输出样本将经历的延迟
* 参数2:out_sample_rate 输出采样率
* 参数3:frame->sample_rate 输入采样率
* 参数4:AV_ROUND_UP 先上取 取去11个才能容纳的上
*/
int dst_nb_samples = av_rescale_rnd(
swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples,
out_sample_rate, frame->sample_rate, AV_ROUND_UP);
/**
* pcm的处理逻辑
* 音频播放器的数据格式是我们自己在下面定义的
* 而原始数据(待播放的音频pcm数据)
* TODO 重采样工作
* 返回的结果:每个通道输出的样本数(注意:是转换后的) 重采样实验(通道基本上都是:1024)
* 参数1:swr_ctx SwrContext
* TODO 下面是输出区域
* 参数2:out_buffers 重采样后的成果的buff
* 参数3:dst_nb_samples 成果的 单通道的样本数 无法与out_buffers对应,所以有下面的pcm_data_size计算
* TODO 下面是输入区域
* 参数4:(const uint8_t **) frame->data 队列的AVFrame * 的PCM数据 未重采样的
* 参数5:frame->nb_samples 输入的样本数
* 参数6:
*/
int samples_per_channel = swr_convert(swr_ctx, &out_buffers, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
/**
* 由于out_buffers 和 dst_nb_samples 无法对应,所以pcm_data_size需要重新计算
* 941通道样本数 * 2样本格式字节数 * 2声道数 =3764
*/
pcm_data_size = samples_per_channel * out_sample_size * out_channels;
break;
} // while end
/**
* FFmpeg录制 Mac 麦克风 输出 每一个音频包的size == 4096
* 4096是单声道的样本数,44100是每秒钟采样的数
* 单通道样本数:1024 * 2声道 * 2(16bit) = 4,096 == 4096是单声道的样本数
* 采样率 44100是每秒钟采样的次数
* 样本数 = 采样率 * 声道数 * 位声
* 双声道的样本数 = (采样率 * 声道数 * 位声) * 2
*/
return pcm_data_size;
}
音视频--音频和视频解码与播放渲染功能完成,接下来。。。