基于FFmpeg开发视频播放器,音频解码播放(三)

音频的播放,这里用的时OpenSLES,这是一套跨平台,针对嵌入式系统做过优化的api,它为嵌入式移动多媒体设备上
的本地应用程序提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台
部署,降低执行难度.

当然Android平台上音频的播放,也可以借助java层AudioTrack接口,但是因为ffmpeg的整个处理流程都是在native层,所以使用NDK提供的OpenSLES 的api,直接在native层处理音频数据,避免了跟java层之间的数据拷贝,效率更高.

OpenSLES的使用:

OpenSLES通过Object和Interface来使用,什么意思呢?就是一个Object可能提供很多函数,但是你不能直接通过Object来调用它提供的函数,而是要先拿到Object的相应接口Interface,然后通过Interface去调用相应的函数,每一种Object都提供了一系列的Interface,相当于Interface对Object中函数做了一个分类.

使用OpenSLES播放音频的流程:

1. 创建引擎对象
2. 设置混音器
3. 创建播放器
4. 开始,停止播放

结合源码看下实现:

跟视频绘制类似,这里也要有解码线程,播放线程:

void AudioChannel::play() {
    //因为frame_queue中数据格式,可能不是我们想要的,所以这里创建一个转换器
    //第二个参数,输出声道数,
    swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100,
            avCodecContext->channel_layout,avCodecContext->sample_fmt,avCodecContext->sample_rate,
            0, 0);
    swr_init(swrContext);

    isPlaying = 1;
    setEnable(1);

    //解码音频流,单独的线程
    pthread_create(&audioDecodeTask, 0, audioDecode_t, this);
    //播放音频,单独的线程。
    pthread_create(&audioPlayTask, 0, audioPlay_t, this);
}

解码调用的接口,跟视频解码是类似的


void AudioChannel::decode() {
    AVPacket *packet = 0;
    //从待解码队列中取出待解码数据,送去解码,
    while (isPlaying) {
        int ret = pkt_queue.deQueue(packet);
        //如果取出失败,继续循环,如果停止了播放,就退出循环。
        if (!ret) {
            continue;
        }
        if (!isPlaying) {
            break;
        }

        //送去解码,先是send,然后receive
        ret = avcodec_send_packet(avCodecContext, packet);
        releaseAvPacket(packet);//只要把包交给了解码器,就可以释放了,因为解码器会复制一份,
        if (ret <0) {
            break;//如果提交失败了,继续循环,实际项目中,要做更多的处理,
        }

        //获取解码后的数据,
        AVFrame *frame = av_frame_alloc();
        ret = avcodec_receive_frame(avCodecContext, frame);
        if (ret == AVERROR(EAGAIN)) {
            continue;//如果需要更多待解码数据,继续循环。
        } else if (ret < 0) {
            break;
        }

        //放入解码后的数据队列。
        frame_queue.enQueue(frame);
    }
}

播放的过程,我把注释写在了代码里,


void AudioChannel::_play() {
    //创建播放引擎对象,创建成功后,需要初始化。
    SLresult result;
    result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    //引擎对象初始化,第二个参数表示同步还是异步,false表示同步
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }

    //获取引擎对象,可以提供的接口。
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);

    //通过引擎接口,调用引擎对象的方法,创建混音器,
    result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    //混音器创建成功,初始化混音器
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }

    //创建播放器所需的数据源,SLDataSource中的属性,第一个是数据的获取器,或者说是定位器,表示数据从哪里定位,
    // 那么数据从哪里定位呢?就是从队列中定位,我们就是往这个队列中放数据,
    // SLDataLocator_AndroidSimpleBufferQueue是opensl es专门为android平台定义的队列,
    // 第二个是数据的格式,音频数据的格式有多种,
    // 我们这里为播放器指定一种,不管解码出的数据格式是什么样的,都可以通过swresample重采样模块,转成我们指定的格式。
    SLDataLocator_AndroidSimpleBufferQueue android_queue = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};

    //数据类型,声道数,采样率,采样位,容器大小,双声道,小端字节序
    SLDataFormat_PCM 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 slDataSource = {&android_queue, &pcm};

    //创建播放器所需的接收端,SLDataSink, 那么sink,实际是播放过程的一个控制者,它会不断的去拿解码号的数据,
    // 送到播放设备区播放。所以sink是对混音器SLDataLocator_OutputMix的包装。
    //混音器才是真正去播放音频数据的,播放器实际是对混音器的封装,提供了额外的暂停,快进等操作。
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSink = {&outputMix, nullptr};

    //创建播放器希望获取的接口
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};

    //创建播放器,
    //第三个参数,SLDataSource,数据源,以队列的形式提供,播放器会从这个队列中拿数据,
    // 我们只要往这个队列中放数据,就可以连续播放了。
    //第四个参数,audioSink,
    //第五个参数,希望获取这个播放器的几套接口,因为只有获取到接口,才能通过接口调用播放器提供的相应方法,
    // 比如说播放开始,暂停的方法在一套接口中,处理播放队列的方法,在另一套接口中。相当于用接口对 这个对象的方法进行了分类,
    //因为播放器对象,默认提供了一套播放状态控制的接口,不需要主动去获取,这里获取的是额外的一套接口,就是数据队列操作接口,
    //第六个参数,希望获取的接口的ID,
    //第七个参数,希望获取的接口,是不是必须的。
    (*engineInterface)->CreateAudioPlayer(engineInterface, &playerObject, &slDataSource, &audioSink,
            1, ids, req);
    (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

    //播放器对象有了,怎么让他运行起来?
    //1,把播放器设置为播放状态,
    //2,把要播放的数据放入播放队列中,
    //这里的顺序要是先注册队列回调,然后设置为播放状态。

    //获取播放队列操作接口
    (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);
    //设备播放队列的回调方法,在这个回调方法中,给播放器填数据,
    (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerBufferQueueCallback,this);


    //获取播放状态接口,
    (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &statusInterface);
    //设置为播放状态
    (*statusInterface)->SetPlayState(statusInterface, SL_PLAYSTATE_PLAYING);

    //最后一步,要主动调用一次回调方法,才会开始播放
    playerBufferQueueCallback(playerBufferQueue, this);
}

真正开启播放是要调用播放队列接口的回调处理才开始的.

在开始播放前,

//使用转换器,把frame_queue中的数据,转成我们需要的。把转换后的数据放入buffer,返回值表示转换数据的大小。

int AudioChannel::_getData() {
    int dataSize = 0;
    AVFrame *frame = 0;
    while (isPlaying) {
        int ret = frame_queue.deQueue(frame);
        if (!isPlaying) {
            break;
        }
        if (!ret) {
            continue;
        }

        //第二,三个参数,用来接收转换出来的数据,bufferCount表示这个buffer最多可以装多少数据,
        //这里需要注意的最后两个参数,frame->data,
        // 最后一个参数frame->nb_samples,表示一个声道的有效样本数,(而不是frame->data的字节数,),
        // 一个样本大小,是根据采样位,采样率计算的。
        int nb = swr_convert(swrContext, &buffer, bufferCount, (const uint8_t **)frame->data, frame->nb_samples);
        //f返回值,表示转换出来的每个声道的样本数,也即是往buffer中装了多少样本数,再乘以样本的大小,可以得到转换数据的字节数。
        dataSize = nb * out_channels * out_sampleSize;

        //获取这段音频的时刻,pts表示这一帧的时间戳,以time_base为单位的时间戳,time_base是AVRational结构体类型,
        // 也就是pts的单位是 (AVRational.Numerator / AVRational.Denominator),这样下面得出的时间单位是秒。
        clock = frame->pts * av_q2d(time_base);
        break;
    }
    releaseAvFrame(frame);
    return dataSize;
}

//播放器会从这个SLAndroidSimpleBufferQueueItf这个队列中拿数据,那么往这个队列中填的数据的格式,

// 必须是我们在在创建播放器时指定的格式,

// 也就是创建播放器对象的第三个参数SLDataSource slDataSource中指定的SLDataFormat_PCM pcm

void playerBufferQueueCallback(SLAndroidSimpleBufferQueueItf queue, void *context) {
    AudioChannel *audioChannel = static_cast<AudioChannel *>(context);
    int dataSize = audioChannel->_getData();
    //swr_convert,转换后的buffer,提交到播放器的队列中,
    if (dataSize >0) {
        (*queue)->Enqueue(queue, audioChannel->buffer, dataSize);
    }
}

执行到这里,音频就播放出来了,

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值