NDK FFmpeg音视频播放器三

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;
}
NDK FFmpeg音视频播放器三_第1张图片

音视频--音频和视频解码与播放渲染功能完成,接下来。。。

你可能感兴趣的:(NDK,ffmpeg,音视频,NDK)