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进行初始化,得到缓存队列接口->为了兼容性对数据进行重采样->将重采样的数据加入到缓冲接口队列中去因为就播放出来了。