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

您所在的位置:网站首页 ffmpeg硬件解码和编码 基于FFmpeg开发视频播放器,音频解码播放(三)

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

2023-05-25 16:29| 来源: 网络整理| 查看: 265

音频的播放,这里用的时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 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(context); int dataSize = audioChannel->_getData(); //swr_convert,转换后的buffer,提交到播放器的队列中, if (dataSize >0) { (*queue)->Enqueue(queue, audioChannel->buffer, dataSize); } }

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

原文链接:基于FFmpeg开发视频播放器,音频解码播放(三)_lin-0410的博客-CSDN博客

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3