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

Love The Way You Lie 2023-07-14 08:19 102阅读 0赞

音频的播放,这里用的时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. 开始,停止播放

结合源码看下实现:

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

  1. void AudioChannel::play() {
  2. //因为frame_queue中数据格式,可能不是我们想要的,所以这里创建一个转换器
  3. //第二个参数,输出声道数,
  4. swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100,
  5. avCodecContext->channel_layout,avCodecContext->sample_fmt,avCodecContext->sample_rate,
  6. 0, 0);
  7. swr_init(swrContext);
  8. isPlaying = 1;
  9. setEnable(1);
  10. //解码音频流,单独的线程
  11. pthread_create(&audioDecodeTask, 0, audioDecode_t, this);
  12. //播放音频,单独的线程。
  13. pthread_create(&audioPlayTask, 0, audioPlay_t, this);
  14. }

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

  1. void AudioChannel::decode() {
  2. AVPacket *packet = 0;
  3. //从待解码队列中取出待解码数据,送去解码,
  4. while (isPlaying) {
  5. int ret = pkt_queue.deQueue(packet);
  6. //如果取出失败,继续循环,如果停止了播放,就退出循环。
  7. if (!ret) {
  8. continue;
  9. }
  10. if (!isPlaying) {
  11. break;
  12. }
  13. //送去解码,先是send,然后receive
  14. ret = avcodec_send_packet(avCodecContext, packet);
  15. releaseAvPacket(packet);//只要把包交给了解码器,就可以释放了,因为解码器会复制一份,
  16. if (ret <0) {
  17. break;//如果提交失败了,继续循环,实际项目中,要做更多的处理,
  18. }
  19. //获取解码后的数据,
  20. AVFrame *frame = av_frame_alloc();
  21. ret = avcodec_receive_frame(avCodecContext, frame);
  22. if (ret == AVERROR(EAGAIN)) {
  23. continue;//如果需要更多待解码数据,继续循环。
  24. } else if (ret < 0) {
  25. break;
  26. }
  27. //放入解码后的数据队列。
  28. frame_queue.enQueue(frame);
  29. }
  30. }

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

  1. void AudioChannel::_play() {
  2. //创建播放引擎对象,创建成功后,需要初始化。
  3. SLresult result;
  4. result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
  5. if (SL_RESULT_SUCCESS != result) {
  6. return;
  7. }
  8. //引擎对象初始化,第二个参数表示同步还是异步,false表示同步
  9. result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
  10. if (SL_RESULT_SUCCESS != result) {
  11. return;
  12. }
  13. //获取引擎对象,可以提供的接口。
  14. result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
  15. //通过引擎接口,调用引擎对象的方法,创建混音器,
  16. result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
  17. if (SL_RESULT_SUCCESS != result) {
  18. return;
  19. }
  20. //混音器创建成功,初始化混音器
  21. result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
  22. if (SL_RESULT_SUCCESS != result) {
  23. return;
  24. }
  25. //创建播放器所需的数据源,SLDataSource中的属性,第一个是数据的获取器,或者说是定位器,表示数据从哪里定位,
  26. // 那么数据从哪里定位呢?就是从队列中定位,我们就是往这个队列中放数据,
  27. // SLDataLocator_AndroidSimpleBufferQueue是opensl es专门为android平台定义的队列,
  28. // 第二个是数据的格式,音频数据的格式有多种,
  29. // 我们这里为播放器指定一种,不管解码出的数据格式是什么样的,都可以通过swresample重采样模块,转成我们指定的格式。
  30. SLDataLocator_AndroidSimpleBufferQueue android_queue = {
  31. SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
  32. //数据类型,声道数,采样率,采样位,容器大小,双声道,小端字节序
  33. SLDataFormat_PCM pcm = {
  34. SL_DATAFORMAT_PCM,
  35. 2,
  36. SL_SAMPLINGRATE_44_1,
  37. SL_PCMSAMPLEFORMAT_FIXED_16,
  38. SL_PCMSAMPLEFORMAT_FIXED_16,
  39. SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
  40. SL_BYTEORDER_LITTLEENDIAN
  41. };
  42. SLDataSource slDataSource = {&android_queue, &pcm};
  43. //创建播放器所需的接收端,SLDataSink, 那么sink,实际是播放过程的一个控制者,它会不断的去拿解码号的数据,
  44. // 送到播放设备区播放。所以sink是对混音器SLDataLocator_OutputMix的包装。
  45. //混音器才是真正去播放音频数据的,播放器实际是对混音器的封装,提供了额外的暂停,快进等操作。
  46. SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
  47. SLDataSink audioSink = {&outputMix, nullptr};
  48. //创建播放器希望获取的接口
  49. const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
  50. const SLboolean req[1] = {SL_BOOLEAN_TRUE};
  51. //创建播放器,
  52. //第三个参数,SLDataSource,数据源,以队列的形式提供,播放器会从这个队列中拿数据,
  53. // 我们只要往这个队列中放数据,就可以连续播放了。
  54. //第四个参数,audioSink,
  55. //第五个参数,希望获取这个播放器的几套接口,因为只有获取到接口,才能通过接口调用播放器提供的相应方法,
  56. // 比如说播放开始,暂停的方法在一套接口中,处理播放队列的方法,在另一套接口中。相当于用接口对 这个对象的方法进行了分类,
  57. //因为播放器对象,默认提供了一套播放状态控制的接口,不需要主动去获取,这里获取的是额外的一套接口,就是数据队列操作接口,
  58. //第六个参数,希望获取的接口的ID,
  59. //第七个参数,希望获取的接口,是不是必须的。
  60. (*engineInterface)->CreateAudioPlayer(engineInterface, &playerObject, &slDataSource, &audioSink,
  61. 1, ids, req);
  62. (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
  63. //播放器对象有了,怎么让他运行起来?
  64. //1,把播放器设置为播放状态,
  65. //2,把要播放的数据放入播放队列中,
  66. //这里的顺序要是先注册队列回调,然后设置为播放状态。
  67. //获取播放队列操作接口
  68. (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);
  69. //设备播放队列的回调方法,在这个回调方法中,给播放器填数据,
  70. (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerBufferQueueCallback,this);
  71. //获取播放状态接口,
  72. (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &statusInterface);
  73. //设置为播放状态
  74. (*statusInterface)->SetPlayState(statusInterface, SL_PLAYSTATE_PLAYING);
  75. //最后一步,要主动调用一次回调方法,才会开始播放
  76. playerBufferQueueCallback(playerBufferQueue, this);
  77. }

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

在开始播放前,

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

  1. int AudioChannel::_getData() {
  2. int dataSize = 0;
  3. AVFrame *frame = 0;
  4. while (isPlaying) {
  5. int ret = frame_queue.deQueue(frame);
  6. if (!isPlaying) {
  7. break;
  8. }
  9. if (!ret) {
  10. continue;
  11. }
  12. //第二,三个参数,用来接收转换出来的数据,bufferCount表示这个buffer最多可以装多少数据,
  13. //这里需要注意的最后两个参数,frame->data,
  14. // 最后一个参数frame->nb_samples,表示一个声道的有效样本数,(而不是frame->data的字节数,),
  15. // 一个样本大小,是根据采样位,采样率计算的。
  16. int nb = swr_convert(swrContext, &buffer, bufferCount, (const uint8_t **)frame->data, frame->nb_samples);
  17. //f返回值,表示转换出来的每个声道的样本数,也即是往buffer中装了多少样本数,再乘以样本的大小,可以得到转换数据的字节数。
  18. dataSize = nb * out_channels * out_sampleSize;
  19. //获取这段音频的时刻,pts表示这一帧的时间戳,以time_base为单位的时间戳,time_base是AVRational结构体类型,
  20. // 也就是pts的单位是 (AVRational.Numerator / AVRational.Denominator),这样下面得出的时间单位是秒。
  21. clock = frame->pts * av_q2d(time_base);
  22. break;
  23. }
  24. releaseAvFrame(frame);
  25. return dataSize;
  26. }

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

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

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

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

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

发表评论

表情:
评论列表 (有 0 条评论,102人围观)

还没有评论,来说两句吧...

相关阅读