Android FFmpeg视频播放器三 音频封装格式解码播放

Posted 若之灵动

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android FFmpeg视频播放器三 音频封装格式解码播放相关的知识,希望对你有一定的参考价值。

Android FFmpeg视频播放器一解封装

Android Android FFmpeg视频播放器二 视频封装格式解码播放

视频解封装之后就会得到音频流和视频流,解封状得到的数据是AVPackage类型数据,需要进一步解码成AVFrame一帧一帧数据才能进行播放。

1.从AVPackage队列获取数据进行解码操作

pthread_create(&pid_audio_decode, nullptr, task_audio_decode, this);

/**
 * 音频解码线程回调
 * @param args
 * @return
 */
void *task_audio_decode(void * args) 
    auto *audio_channel = static_cast<AudioChannel *>(args);
    audio_channel->audio_decode();
    return nullptr;


/**
 *  音频:取出队列的压缩包 进行解码 解码后的原始包 再push队列中去 (音频:PCM数据)
 */
void AudioChannel::audio_decode() 
    AVPacket *pkt = nullptr;
    while (isPlaying) 
        if (isPlaying && frames.size() > AV_MAX_SIZE) 
            av_usleep(10000);
            continue;
        
        /*阻塞式函数*/
        int ret = packets.getQueueAndDel(pkt);
        if (!isPlaying) 
            /*如果关闭了播放,跳出循环,releaseAVPacket(&pkt);*/
            break;
        

        if (!ret) 
            /*压缩包加入队列慢,继续*/
            continue;
        

        /*
         * 1.发送pkt(压缩包)给缓冲区,
         * 2.从缓冲区拿出来(原始包)
         * */
        ret = avcodec_send_packet(codecContext, pkt);

        if (ret) 
            /* avcodec_send_packet 出现了错误,结束循环*/
            break;
        

        /*AVFrame: 解码后的视频原始数据包 pcm */
        AVFrame *frame = av_frame_alloc();
        /*从 FFmpeg缓冲区 获取 原始包*/
        ret = avcodec_receive_frame(codecContext, frame);
        if (ret == AVERROR(EAGAIN)) 
            /*有可能音频帧也会获取失败,重新获取一次*/
            continue; //
         else if (ret != 0) 
            if (frame) 
                releaseAVFrame(&frame);
            
            break;
        
        /*原始包-- PCM数据 将PCM数据添加到帧队列*/
        frames.insertToQueue(frame);
        av_packet_unref(pkt);
        releaseAVPacket(&pkt);
    
    
    /*释放结构体内部成员在堆区分配的内存*/
    av_packet_unref(pkt);
    /*释放AVPacket **/
    releaseAVPacket(&pkt);


  • pthread_create:AVPackage解码得到AVFrame耗时操作,创建线程
  • getQueueAndDel:从AVPackage队列获取数据,阻塞队列,如果队列为空会进行wait阻塞
  • avcodec_send_packet:将获取到的AVPackage数据发送给ffmpeg缓冲区,ffmpeg会进行解码操作。
  • avcodec_receive_frame:从ffmpeg缓冲区得到解码之后的pcm数据。
  • frames.insertToQueue(frame):将pcm数据添加到帧队列中。

2.初始化OpenSL ES

 /*第二线线程:视频:从队列取出原始包,播放  音频播放OpenSLES*/
pthread_create(&pid_audio_play, nullptr, task_audio_play, this);


/**
 * 音频播放子线程回调
 * @param args
 * @return
 */
void *task_audio_play(void *args) 
    auto *audio_channel = static_cast<AudioChannel *>(args);
    audio_channel->audio_play();
    return nullptr;



/**
 * 音频:从队列取出原始包PCM,OpenSLES音频播放
 */
void AudioChannel::audio_play() 
    /*用于接收 执行成功或者失败的返回值*/
    SLresult result;

    /*创建引擎对象并获取  创建引擎对象:SLObjectItf engineObject*/
    result = slCreateEngine(&engineObject, 0, 0, 0, 0, 0);
    if (SL_RESULT_SUCCESS != result) 
        LOGE("创建引擎 slCreateEngine error");
        return;
    

    /*
     * 初始化引擎
     * SL_BOOLEAN_FALSE:同步等待创建成功
     * */
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) 
        LOGE("创建引擎 Realize error");
        return;
    

    /*
     * 获取引擎接口
     * */
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
    if (SL_RESULT_SUCCESS != result) 
        LOGE("创建引擎接口 Realize error");
        return;
    

    /*健壮判断*/
    if (engineInterface) 
        LOGD("创建引擎接口 create success");
     else 
        LOGD("创建引擎接口 create error");
        return;
    

    /*创建混音器  环境特效,混响特效 */
    result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject,
                                                 0, nullptr, nullptr);
    if (SL_RESULT_SUCCESS != result) 
        LOGD("初始化混音器 CreateOutputMix failed");
        return;
    

    /*
     * 初始化混音器
     * SL_BOOLEAN_FALSE:同步等待创建成功
     * */
    result = (*outputMixObject)->Realize(outputMixObject,
                                         SL_BOOLEAN_FALSE);  
    if (SL_RESULT_SUCCESS != result) 
        LOGD("初始化混音器 (*outputMixObject)->Realize failed");
        return;
    


    /*创建buffer缓存类型的队列 */
    SLDataLocator_androidSimpleBufferQueue loc_bufq = 
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
                                                       10;

    /*
     * 声明PCM 数据参数集 format_pcm
     * pcm数据格式不能直接播放,需要设置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:小端模式 字节序(小端) 例如:int类型四个字节(高位在前 还是 低位在前 的排序方式,一般都是小端)
     * */
    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;

    /**
     * 使用数据源 和PCM数据格式,初始化SLDataSource
     * 独立声卡:24bit  集成声卡16bit
     */
    SLDataSource audiosrc = &loc_bufq, &format_pcm;

    /*
     * 配置音轨(输出)
     * 设置混音器
     * SL_DATALOCATOR_OUTPUTMIX:输出混音器类型
     * */
    SLDataLocator_OutputMix loc_outmix = SL_DATALOCATOR_OUTPUTMIX,
                                          outputMixObject;
    /*得到outmix最终混音器*/
    SLDataSink audioSnk = &loc_outmix, NULL;

    /* 需要的接口 操作队列的接口*/
    const SLInterfaceID ids[1] = SL_IID_BUFFERQUEUE;
    const SLboolean req[1] = SL_BOOLEAN_TRUE;

    /*
     * 创建播放器 SLObjectItf bqPlayerObject
     * 参数1:引擎接口
     * 参数2:播放器
     * 参数3:音频配置信息
     * 参数4:混音器
     * 参数5:开放的参数的个数
     * 参数6:代表我们需要 Buff
     * 参数7:代表我们上面的Buff 需要开放出去
     * */
    result = (*engineInterface)->CreateAudioPlayer(engineInterface,
                                                   &bqPlayerObject,
                                                   &audioSrc,
                                                   &audioSnk,

                                                   1,
                                                   ids,
                                                   req
    );

    if (SL_RESULT_SUCCESS != result) 
        LOGD("创建播放器 CreateAudioPlayer failed!");
        return;
    

    /*
     * 初始化播放器:SLObjectItf bqPlayerObject
     * SL_BOOLEAN_FALSE:同步等待创建成功
     * */
    result = (*bqPlayerObject)->Realize(bqPlayerObject,
                                        SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) 
        LOGD("实例化播放器 CreateAudioPlayer failed!");
        return;
    
    LOGD("创建播放器 CreateAudioPlayer success!");

    /*
     * 获取播放器接口 播放全部使用播放器接口
     * SL_IID_PLAY:播放接口 == iplayer
     * */
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY,
                                             &bqPlayerPlay);
    if (SL_RESULT_SUCCESS != result) 
        LOGD("获取播放接口 GetInterface SL_IID_PLAY failed!");
        return;
    
    LOGI("创建播放器 Success");


    /*
     * 设置回调函数
     * 获取播放器队列接口:SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue
     * 播放需要的队列
     * */
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                             &bqPlayerBufferQueue);
    if (result != SL_RESULT_SUCCESS) 
        LOGD("获取播放队列 GetInterface SL_IID_BUFFERQUEUE failed!");
        return;
    

    /*
     * 设置回调 void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
     * 传入刚刚设置好的队列
     * 给回调函数的参数
     * */
    (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue,
                                             bqPlayerCallback,
                                             this);
    LOGI("设置播放回调函数 Success");

    /*设置播放器状态为播放状态*/
    (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    LOGI("设置播放器状态为播放状态 Success");

    /*手动激活回调函数*/
    bqPlayerCallback(bqPlayerBufferQueue, this);
    LOGI("手动激活回调函数 Success");

  • pthread_create:创建音频播放线程。
  • slCreateEngine:创建OpenSL ES 引擎对象:SLObjectItf engineObject。
  • Realize(engineObject, SL_BOOLEAN_FALSE):同步的方式初始化引擎对象engineObject
  • (*engineObject)->GetInterface:通过引擎对象获取引擎接口。
  • (*engineInterface)->CreateOutputMix:通过引擎接口创建混音器对象outputMixObject
  • Realize(outputMixObject,SL_BOOLEAN_FALSE):同步初始化混合音器对象
  • SLDataLocator_AndroidSimpleBufferQueue loc_bufq = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,10:创建buffer缓存类型的队列
  • SLDataFormat_PCM format_pcm:设置PCM数据格式,通道数量、采样率、位深、大小端。
  • SLDataSource audioSrc = &loc_bufq, &format_pcm:前面的缓存队列和数据PCM格式是为了初始化SLDataSource
  • CreateAudioPlayer:创建播放器 SLObjectItf bqPlayerObject
  • Realize(bqPlayerObject, SL_BOOLEAN_FALSE):实例化音频播放器
  • (*bqPlayerObject)->GetInterface:创建播放器接口,播放全部使用播放器接口bqPlayerPlay
  • (*bqPlayerObject)->GetInterface:得到播放器接口队列bqPlayerBufferQueue
  • RegisterCallback:注册接口播放器队列回调,bqPlayerCallback这个是一个函数指针是一个回调函数。
  • bqPlayerCallback(bqPlayerBufferQueue, this):执行函数bqPlayerCallback,将接口队列当作参数进行传递。

3.OpenSL ES播放PCM格式的音频数据

/**
 * 回调函数
 * @param bq  SLAndroidSimpleBufferQueueItf队列
 * @param args  this  给回调函数的参数
 */
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *args) 

    auto *audio_channel = static_cast<AudioChannel *>(args);
    int pcm_size = audio_channel->getPCM();

    /*
     * PCM数据添加数据到缓冲区里面去,声音就播放出来了
     * bq:队列接口本身,因为没有this,所以把自己传进去了
     * audio_channel->out_buffers:音频PCM数据
     * pcm_size:PCM数据大小
     * */
    (*bq)->Enqueue(
            bq,
            audio_channel->out_buffers,
            pcm_size);



/**
 * 输入的音频数据的采样率可能是各种各样的,为了兼容所以需要重采样 
 * @return  重采样之后音频数据的大小
 */
int AudioChannel::getPCM() 
    int pcm_data_size = 0;
    /*
     * PCM数据在队列frames队列中:frame->data == PCM数据
     * */
    AVFrame *frame = 0;
    while (isPlaying) 
        int ret = frames.getQueueAndDel(frame);
        if (!isPlaying) 
            break;
        
        if (!ret) 
            /*原始包加入队列慢,再次获取一下*/
            continue;
        

        /*
         * 开始重采样  
         * 数据源10个48000  目标:44100 11个44100
         * 第一个参数:获取下一个输入样本相对于下一个输出样本将经历的延迟
         * 第二个参数: 输出采样率
         * 第三个参数:输入采样率
         * 第四个参数:先上取
         * */
        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数据)
         * 重采样工作
         * 返回的结果:每个通道输出的样本数(注意:是转换后的)    做一个简单的重采样实验(通道基本上都是:1024)
         * */
        int samples_per_channel = swr_convert(swr_ctx,
                                              /*输出区域*/
                                              /*重采样后的buffer数据*/
                                              &out_buffers,
                                              /*单通道的样本数 无法与out_buffers对应,需要pcm_data_size从新计算*/
                                              dst_nb_samples, 
                                                /*输入区域*/
                                                /*队列的AVFrame *PCM数据 未重采样的*/
                                              (const uint8_t **) frame->data, 
                                              /*输入的样本数*/
                                              frame->nb_samples);

        /*由于out_buffers 和 dst_nb_samples 无法对应,所以需要重新计算
         * 计算公式:样本数量*位深(16位2个字节)*通道数量
         * 比如:941通道样本数  *  2样本格式字节数  *  2声道数  =3764
         * */
        pcm_data_size = samples_per_channel * out_sample_size *
                        out_channels; 

        break; 

      


    /*
     * FFmpeg录制麦克风  输出 每一个音频包的size == 4096
     * 4096是单声道的样本数,  44100是每秒钟采样的数
     * 样本数 = 采样率 * 声道数 * 位声
     * 采样率 44100是每秒钟采样的次数
     * */

    av_frame_unref(frame);
    av_frame_free(&frame);

    return pcm_data_size;


  • 音频播放,初始化OpenSL ES之后,只需要通过音频缓冲队列的Enqueue方法将数据添加到队列就可以播放音频了
  • getPCM():从音频AVFrame帧队列,获取已经将解码的PCM数据,获取之后不能直接使用,因为音频的采样率不确定,为了兼容性需要进行重采样,然后重新计算重采样之后的数据大小。
  • swr_convert:对数据进行重采样,dst_nb_samples这个是单通道的样本数,out_buffers这个是重采样之后PCM数据大小。
  • PCM数据包大小计算:单通道样本数*通道数*位深

4.音频播放结束之后OpenSL ES资源释放

void AudioChannel::stop() 

    /*设置停止状态*/
    if (bqPlayerPlay) 
        (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
        bqPlayerPlay = nullptr;
    

    /*销毁播放器*/
    if (bqPlayerObject) 
        (*bqPlayerObject)->Destroy(bqPlayerObject);
        bqPlayerObject = nullptr;
        bqPlayerBufferQueue = nullptr;
    

    /*销毁混音器*/
    if (outputMixObject) 
        (*outputMixObject)->Destroy(outputMixObject);
        outputMixObject = nullptr;
    

    /*销毁引擎*/
    if (engineObject) 
        (*engineObject)->Destroy(engineObject);
        engineObject = nullptr;
        engineInterface = nullptr;
    



总结:音频播放流程:将AVPackage解码成AVFrame数据->将AVFrame帧数据放入队列->对OpenSL ES进行初始化,得到缓存队列接口->为了兼容性对数据进行重采样->将重采样的数据加入到缓冲接口队列中去因为就播放出来了。

Android使用FFmpeg播放视频(二):音频播放

参考技术A Android使用FFmpeg播放视频(一):视频播放
Android NDK开发:利用OpenSL ES实现声音播放

这里我创建了两个JNI函数,一个是播放的,一个是释放的如下:

这里我在用于播放的JNI函数中依次初始化了FFmpeg和OpenSLES

其中初始化FFmpeg的函数中的逻辑其实和使用FFmpeg播放视频画面中的逻辑差不多,主要区别就是要找到音频的索引以及后面对于解析音频的一些配置;而初始化OpenSLES基本就和之前使用OpenSLES播放PCM数据是一样的,具体如下:

最后再加入释放资源的逻辑即可

这里的案例源码是和之前播放视频画面的分开了
https://gitee.com/itfitness/ffmpeg-audio-demo

以上是关于Android FFmpeg视频播放器三 音频封装格式解码播放的主要内容,如果未能解决你的问题,请参考以下文章

FFmpeg解封装解码音频和视频(分别使用OpenGL和OpenAL播放)

FFMPEG+SDL实现视频播放器

FFmpeg播放视频文件流程

(四)Android通过ffmpeg解码音频

第二节:使用FFmpeg3.0+进行视频播放

Android音频开发(三)——音频编解码