当前位置:   article > 正文

Exoplayer使用FFMPEG托管音频并进行音频处理(例如软解+5.1声道DownMix至立体声等)_exo ffmpeg音频播放

exo ffmpeg音频播放

一.项目背景       

        最近一直在搞一个影视播放器的项目,算是一个嵌入式开发,学习了不少以前没有接触过的知识,很充实。其中播放器有一个需求,完整的需求描述是这样的:播放器的声卡是只能输出AC3的音频,但是电影院线那边可能没有AC3音频的解码器,那么我们的播放器需要增加软解功能。又因为我们使用的片源中音频信息都是AC3  5.1声道的声音,但是有可能人家那边没有5.1声道的设备,所以我们要把5.1声道的音频DownMix至双声道立体声中。

        我来讲一下写这篇文章的目的,首先肯定不是给大佬看的,这玩意会的都会,也不用看,这篇文章主要给普通的android开发者来快速学习下,毕竟不是所有的普通andorid开发者都有精力再去学一遍c++,所以我每个步骤都尽量写的详细一些,遇到的坑也都描述一下,希望有需要的人拿起来就能用。

        先说一下从0开始的流程,首先我们要实现一个以Exoplayer为基础的播放器,这个属于业务范畴,不细表,只说核心功能。

********************************************无情分割线************************************************

二.准备工作

        1.下载Exoplayer、FFMPEG源码,注意,这块就有一个坑,你要分清你使用的源码版本,我们项目创建较早,使用的是Exoplayer2,现在官网基本上都是3了,2和3差别很大,注意分清。重点来了,如果你用的是Exoplayer2,那么请你去FFMPEG的git主页上找到分支,选择4.3版本下载,否则你会焦头烂额的,信我,开发不骗开发!

        2.编译FFMPEG,这里其实还涉及一小步,Exoplayer提供了一个方便编译ffmpeg的可执行文件,V2版本路径:/ExoPlayer-release-v2/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh,V3版本路径:/media/libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh ,我们需要将ffmpeg源码关联到Exoplayer项目代码中,这块一会详解。

        3.按照你的业务需求,来修改ffmpeg_jni.cc文件(路径跟build_ffmpeg.sh同级),大概率你的业务需求需要修改的也就是ffmpeg_jni文件中的decodePacket方法,信不信由你>.<

        4.编译出aar,给你的播放器项目使用,当然,如果你只有一个项目需要用的话,也可以直接引入项目,更方便。

********************************************无情分割线************************************************

三.正式开工

        咱们一步一步的来。

        1.下载源码

        这块我主要用的是Exoplayer2+FFMPEG4.3

        exoplayer2的下载地址:https://github.com/google/ExoPlayer

        exoplayer3的下载地址:https://github.com/androidx/media

        ffmpeg下载地址:https://github.com/FFmpeg/FFmpeg/branches  (这是分支地址,找到4.3下载,如果使用exoplayer3的话,建议用6.0版本或以上)

        从现在起我将只写我项目实际中2+4.3版本的使用啦。两个版本一定要对的上,能省你很多事,信我。这里我将exo和ffmpeg下载到了我本机的/Users/liuqn/project_support路径下,那么两个项目的路径分别是:

E(方便后续表述,E代表exoplayer2的源码项目)项目路径:/Users/liuqn/project_support/ExoPlayer-release-v2

F(方便后续表述,F代表ffmpeg4.3的源码项目)项目路径:/Users/liuqn/project_support/FFmpeg-release-4.3

记得这俩路径,一会用得着。

        2.编译ffmpeg

        这块真是重点,要说的有很多点,咱们慢慢说。先说一下我的编译环境,我是macOS系统,windows其实差不多,只有很少的地方有区别,同时我默认你是一个安卓开发,你应该已经有了NDK环境吧?如果没有的话,你需要先去配置一下你的NDK开发环境,这就不细说了,网上资料一抓一大把。

        要想编译ffmpeg,首先要在你的E项目中关联上F源码。

        a. cd到你E项目中的jni目录,终端命令:

            cd /Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main/jni

        b. 关联F源码,终端命令:

            ln -s /Users/liuqn/project_support/FFmpeg-release-4.3 ffmpeg

            这时候你会发现你E项目extensions/ffmpeg/src/main/jni下面多了一个ffmpeg文件夹,这个就是FFMPEG源码,我们编译的时候也需要它。

        c.  cd ffmpeg    //进入ffmpeg文件夹下

        d.  ./configure   //执行configure文件,这一步时间较长,会生成一些文件,有可能会报一些错误,你要根据错误信息来解决这些问题,大概就是需要的一些命令你的开发环境没有或者过时了,升级或者安装一下就行,我好像就是make命令没有安装,反正就是看提示安装或者升级下就行。当执行完后,终端会打印一些信息,这块看看就得了:

        e.  cd ..     //返回上一级,其实就是又回到了jni文件夹下。

        f.  先用编译器或者文本编辑器打开build_ffmpeg.sh文件,修改一些参数,方便你调用。主要是修改这段命令:

修改完成后:

  1. FFMPEG_MODULE_PATH="/Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main"
  2. NDK_PATH="/Users/liuqn/Library/Android/sdk/ndk/27.0.11718014"
  3. HOST_PLATFORM="darwin-x86_64"
  4. ENABLED_DECODERS=("ac3,mp3,flac")
  5. JOBS=$(nproc 2> /dev/null || sysctl -n hw.ncpu 2> /dev/null || echo 4)
  6. echo "Using $JOBS jobs for make"
  7. COMMON_OPTIONS="
  8. --target-os=android
  9. --enable-static
  10. --disable-shared
  11. --disable-doc
  12. --disable-programs
  13. --disable-everything
  14. --enable-filter=pan
  15. --enable-avdevice
  16. --enable-avformat
  17. --enable-swscale
  18. --enable-postproc
  19. --enable-avfilter
  20. --enable-symver
  21. --enable-avresample
  22. --enable-swresample
  23. --extra-ldexeflags=-pie
  24. "

FFMPEG_MODULE_PATH指向你E项目中ffmpeg支持库的main文件夹

NDK_PATH你的NDK路径,这里我用的版本是27 

HOST_PLATFORM代表编译平台,Lunux为"linux-x86_64",MacOS为“darwin-x86_64”,这块到底是啥你可以看下你的NDK路径下文件夹名字叫啥,比如我的:/Users/liuqn/Library/Android/sdk/ndk/27.0.11718014/toolchains/llvm/prebuilt/darwin-x86_64 ,这块是啥就选啥。

ENABLED_DECODERS代表你预期想要支持的解码器。

COMMON_OPTIONS中最好直接抄我的,当然你也可以研究下configure文件中的说明,会告诉你这些配置都代表的是什么,例如--enable-filter=pan,代表我要支持pan过滤器,这块得根据你具体业务需求来设置,需要什么样的过滤器,就在此增加什么类型的过滤器,这块就是一个坑,当时我开发功能的时候,打印了一下所有支持的过滤器,发现就没有pan,找了好多资料也没说出个所以然,后来灵光一现,发现这里了,加上以后你的C++中就可以愉快的使用了。至于什么功能对应什么过滤器,你需要查找ffmpeg官方文档了,地址:FFmpeg Filters Documentation

对了,还有一个坑,有可能你待会编译的时候会报错,类似于:

别慌,看报错信息我们知道,我们的NDK版本为27,我发现这个路径下,根本没有armv7a-linux-androideabi16-clang这个版本的clang(C++编译器)了,我们这个版本的NDK只有以下这些:

那么我们改一下就好了,挑一个自己喜欢的版本,我这里选的是23,还是build_ffmpeg.sh文件中,修改红框框里面的东西:

全改成23(看你喜欢,哈哈,改成啥版本都行,只要你有)。

        g.  设置好build_ffmpeg.sh文件后,就可以执行它开始编译你心心念念的ffmpeg库了,终端命令:

            ./build_ffmpeg.sh

            编译完成后你会发现在你F项目的根目录下(你的E项目因为链接着F项目,所以你的E项目下ffmpeg文件夹下也能看到)生成了一个android-libs文件夹,这里面就是你编译好的ffmpeg库文件啦,至此,你的ffmpeg编译工作结束,撒花!

3.实现ffmpeg软解

        经历了上面编译工作后,我们终于可以正式进入开发业务逻辑的过程中了,开心~。首先我们需要让你的exoplayer能够实现让ffmpeg代理音频处理。播放器本身的业务逻辑我不管,跟本文没什么关系,我想既然您能搜索到这片文章,说明您至少已经能够使用exoplayer正常的播放影片或者音频了吧,咱们只说代理音频(视频也一样)处理的问题。这块其实也很简单,代码量很少,我们不说exoplayer本身的处理逻辑,源码分析网上文章一搜一大把,有需要也有时间的人沉下心去阅读,这里只说业务层上面如何使用:

新建一个FfmpegRenderFactory类:

  1. public class FfmpegRenderFactory extends DefaultRenderersFactory {
  2. FfmpegAudioRenderer ffmpegAudioRenderer;
  3. public FfmpegRenderFactory(Context context) {
  4. super(context);
  5. setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER);
  6. }
  7. @Override
  8. protected void buildAudioRenderers(Context context,
  9. @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector,
  10. boolean enableDecoderFallback, AudioSink audioSink, Handler eventHandler,
  11. AudioRendererEventListener eventListener, ArrayList<Renderer> out) {
  12. ffmpegAudioRenderer = new FfmpegAudioRenderer();
  13. out.add(ffmpegAudioRenderer);
  14. super.buildAudioRenderers(context, extensionRendererMode, mediaCodecSelector,
  15. enableDecoderFallback, audioSink, eventHandler, eventListener, out);
  16. LogUtils.e("test_audio", "创建FfmpegAudioRenderer:" + FfmpegLibrary.getVersion());
  17. LogUtils.e("test_audio", "ffmpegHasDecoder===" + FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_AC3));
  18. }
  19. }

   你实际使用播放器的Activity中修改代码:

  1. DefaultDataSource.Factory zy_dataSourceFactory = new DefaultDataSource.Factory(this);
  2. MediaSource.Factory dataSourceFactory = new DefaultMediaSourceFactory(/* context= */ this).setDataSourceFactory(zy_dataSourceFactory);
  3. ExoPlayer.Builder playerBuilder = new ExoPlayer.Builder(/* context= */ this)
  4. .setMediaSourceFactory(dataSourceFactory)
  5. .setLoadControl(new DefaultLoadControl());
  6. //重点:使用FFMPEG软解
  7. playerBuilder.setRenderersFactory(new FfmpegRenderFactory(this));
  8. player = playerBuilder.build();
  9. player.addListener(new PlayerEventListener());
  10. player.addAnalyticsListener(new ErrorEventListener());
  11. player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
  12. player.setPlayWhenReady(true);
  13. debugViewHelper = new DebugTextViewHelper(player, debugTextView);
  14. //加入播放路径
  15. player.setMediaItem(MediaItem.fromUri("/storage/emulated/0/AC3_5_1.ac3"));
  16. player.seekTo(0, 0);
  17. player.prepare();

这块代码是一个简单的初始化播放器的例子,您就挑您眼熟的看,重点只有一行代码:

  1. playerBuilder.setRenderersFactory(new FfmpegRenderFactory(this));

这块顺嘴说一下,/Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg路径下我们可以看到以下一些java类:

这些都是E项目源码自带的,暂时不用修改什么,当然也得看您的业务是否需要向C++层传值,那么就需要修改这些类了。还有路径:/Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main/jni下的ffmpeg_jni.cc文件,重中之重,这个文件是我们实现复杂业务需求最需要关注的C++类,我们实际的业务需求都是在这一层中实现的。不过暂时我们不需要修改,至此,我们已经实现了利用ffmpeg来代理我们的音视频解码等操作,就是俗称的软解,可是我们还没有运行是不是?别着急,马上说。

4.编译AAR(直接引入也行)

        exoplayer项目的ffmpeg支持库本身就提供了一个CMake文件供你轻松编译你的库文件,路径:/Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main/jni/CMakeLists.txt

        这里您可能又会遇到一些坑,我提前给您说说,首先是NDK的问题,刚才咱们编译ffmpeg库的时候,我选择的是NDK27,那么您看一下您的项目SDK路径配置,最好也设置成相同版本的NDK,如果选择版本较低的NDK时,编译v8a框架的时候可能会报错(我记得当时我项目里设置的NDK版本是21)。

        下面直接贴出我的CMake文件,有什么改动都有注释,认真看:

  1. cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR)
  2. # Enable C++11 features.
  3. set(CMAKE_CXX_STANDARD 11)
  4. project(libffmpegJNI C CXX)
  5. # Additional flags needed for "arm64-v8a" from NDK 23.1.7779620 and above.
  6. # See https://github.com/google/ExoPlayer/issues/9933#issuecomment-1029775358.
  7. if (${ANDROID_ABI} MATCHES "arm64-v8a")
  8. set(CMAKE_CXX_FLAGS "-Wl,-Bsymbolic")
  9. endif ()
  10. set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
  11. set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
  12. #重点:记得你当时编译出来是8个.a的静态库么?但是原始生成的cmake文件中只有avutil swresample avcodec,是因为最基础的功能只需要这三个库。
  13. #当你需要在你最终生成的so中包含你所需要的库时,要在这里增加对应的库名,比如你需要使用过滤器,就增加avfilter,我记得如果需要使用pan的话,
  14. #还需要增加swresample,其实就算8个全包含,包体也没大多少,建议不解释全包含,省心省力。
  15. foreach (ffmpeg_lib avutil swresample avresample avcodec avfilter avdevice avformat swscale)
  16. set(ffmpeg_lib_filename lib${ffmpeg_lib}.a)
  17. set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
  18. add_library(
  19. ${ffmpeg_lib}
  20. STATIC
  21. IMPORTED)
  22. set_target_properties(
  23. ${ffmpeg_lib} PROPERTIES
  24. IMPORTED_LOCATION
  25. ${ffmpeg_lib_file_path})
  26. endforeach ()
  27. include_directories(${ffmpeg_location})
  28. find_library(android_log_lib log)
  29. #最终生成的so名、编译成动态库(.so)、实际代码类
  30. add_library(ffmpegJNI
  31. SHARED
  32. ffmpeg_jni.cc)
  33. #重点:看上面注释,一样的道理。
  34. target_link_libraries(ffmpegJNI
  35. PRIVATE android
  36. PRIVATE avutil
  37. PRIVATE swresample
  38. PRIVATE avresample
  39. PRIVATE avcodec
  40. PRIVATE avfilter
  41. PRIVATE avdevice
  42. PRIVATE avformat
  43. PRIVATE swscale
  44. PRIVATE ${android_log_lib})

当你以上操作都没有问题成功后,我们就可以执行E项目中extensions下的ffmpeg支持库的build任务:

两者都行,看你心情,顺利build完成后,会在图中目录下生成aar文件:

这个时候你就可以拿着它愉快的玩耍了~当然,你不想用aar,直接在你的播放器项目中引入你的ffmpeg支持库也是可以的。

implementation project(:'extension-ffmpeg')

至此,我们使用ffmpeg代理exoplayer的音视频解码基础功能(软解)就算彻底完成了,撒花~下面我们要说一说较为复杂的音频处理该如何实现。

********************************************无情分割线************************************************

四.使用ffmpeg过滤器

        我们在实现ffmpeg相关功能的时候,实际需求应该不会这么简单吧(但是该说不说,其实ffmpeg默认功能已经很强大了,它会自己寻找可用的解码器来进行音视频软解,甚至不需要你有什么额外操作,如果你的需求真的就是类似于:XXX格式音乐咱们系统无法播放,那么恭喜你,当你成功走到这一步的时候,这类需求你应该已经实现了)?可能会有一些比较复杂的功能,大多数我们都可以通过ffmpeg过滤器来实现。

        言归正传,我们通过一个实际需求来配合代码让你快速可以做到让你的exoplayer再加上一层过滤器来达到你需求的目标,需求如下:

“把5.1声道的音频DownMix至双声道立体声中,同时严格按照声道进行配置,把FL、SL、CEN、LFE融合到FL里,把FR、SR、CEN、LFE融合到FR里”

        需求分析:我们首先要搞清楚声道分别是什么

  1. FL:Front Left,即左声道的主要信号。
  2. FC:Front Center,即中央声道的信号,用于环绕声效果。
  3. LFE:Low Frequency Effects,即低频效果信号,用于增强低音效果。
  4. SL:Side Left,即左侧环绕声道的信号。
  5. FR:Front Right,即右声道的主要信号。
  6. SR:Side Right,即右侧环绕声道的信号。

        5.1声道会有6个喇叭,分别为上面这6个,7.1声道则额外加了BL,和BR,而立体声只有两个声道,分别为FL和FR。

        那么通过ffmpeg官方文档我们可以知道用ffmpeg命令行的方式如何实现这个功能:

ffmpeg -i 音频路径.ac3 -af pan="stereo| FL < FL + 0.5*FC + 0.6*LFE + 0.6*SL | FR < FR + 0.5*FC + 0.6*LFE + 0.6*SR" -acodec ac3 输出音频路径.ac3

其中pan就是过滤器的一种,stereo是立体声的标识,FL < FL + 0.5*FC + 0.6*LFE + 0.6*SL意思是将FL、0.5倍音量的FC、0.6倍音量的LFE、0.6倍音量的SL融合到立体声的FL声道中,同时保持整体音量不变(<),"|"为多逻辑分割。这里得说一下(<和=)的用法,文档中明确表示:

If the ‘=’ in a channel specification is replaced by ‘<’, then the gains for that specification will be renormalized so that the total is 1, thus avoiding clipping noise.

大概意思就是说,如果使用<的话,多声道融合后的音量增益比率还是保持1,以便于消除噪音,实际应用的时候我会发现使用<以后耳朵听到的声音感觉比之前明显小了跟多,所以我选择用=,这块还是要看你实际的需求。

        了解了过滤器命令行操作的方式,这个时候就该想如何在我们的项目代码中使用该功能咧?因为项目代码才是我们最终的归宿。

********************************************无情分割线************************************************

五.实现C++层过滤器

        这里最重要的一个文件就是E项目源码中给你生成好的ffmpeg_jni.cc类,这个类贯穿了整个JAVA层与C++层数据的交换,我们最应该关注这个类中的重点方法:decodePacket()方法,它负责了整个播放器中:数据包输入-->解码---->重采样---->输出。而我们想使用过滤器来实现一些复杂的业务逻辑,我们就要将现在的流程改成:数据包输入--->解码---->过滤---->重采样---->输出,整个播放过程中,这个方法将不停的被调用。

        我们来看一下这个方法的原始模样,我在重点代码上加了注释,便于了解整个方法的运作流程:

  1. int decodePacket(AVCodecContext *context, AVPacket *packet,
  2. uint8_t *outputBuffer, int outputSize) {
  3. int result = 0;
  4. // Queue input data.
  5. 真正的解码操作,负责将数据发送给解码器
  6. result = avcodec_send_packet(context, packet);
  7. if (result) {
  8. logError("avcodec_send_packet", result);
  9. return transformError(result);
  10. }
  11. // Dequeue output data until it runs out.
  12. int outSize = 0;
  13. while (true) {
  14. AVFrame *frame = av_frame_alloc();
  15. if (!frame) {
  16. LOGE("Failed to allocate output frame.");
  17. return AUDIO_DECODER_ERROR_INVALID_DATA;
  18. }
  19. //提取解码后的数据,用于从解码器中提取出解码后的帧数据
  20. result = avcodec_receive_frame(context, frame);
  21. if (result) {
  22. av_frame_free(&frame);
  23. if (result == AVERROR(EAGAIN)) {
  24. break;
  25. }
  26. logError("avcodec_receive_frame", result);
  27. return transformError(result);
  28. }
  29. // Resample output.
  30. AVSampleFormat sampleFormat = context->sample_fmt;
  31. int channelCount = context->channels;
  32. int channelLayout = context->channel_layout;
  33. int sampleRate = context->sample_rate;
  34. int sampleCount = frame->nb_samples;
  35. int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
  36. sampleFormat, 1);
  37. SwrContext *resampleContext;
  38. if (context->opaque) {
  39. resampleContext = (SwrContext *)context->opaque;
  40. } else {
  41. resampleContext = swr_alloc();
  42. av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
  43. av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0);
  44. av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
  45. av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
  46. av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0);
  47. // The output format is always the requested format.
  48. av_opt_set_int(resampleContext, "out_sample_fmt",
  49. context->request_sample_fmt, 0);
  50. result = swr_init(resampleContext);
  51. if (result < 0) {
  52. logError("swr_init", result);
  53. av_frame_free(&frame);
  54. return transformError(result);
  55. }
  56. context->opaque = resampleContext;
  57. }
  58. int inSampleSize = av_get_bytes_per_sample(sampleFormat);
  59. int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
  60. int outSamples = swr_get_out_samples(resampleContext, sampleCount);
  61. int bufferOutSize = outSampleSize * channelCount * outSamples;
  62. if (outSize + bufferOutSize > outputSize) {
  63. LOGE("Output buffer size (%d) too small for output data (%d).",
  64. outputSize, outSize + bufferOutSize);
  65. av_frame_free(&frame);
  66. return AUDIO_DECODER_ERROR_INVALID_DATA;
  67. }
  68. //执行真正的重采样操作,此时采样结束后的数据,已经给到了播放器
  69. result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
  70. (const uint8_t **)frame->data, frame->nb_samples);
  71. av_frame_free(&frame);
  72. if (result < 0) {
  73. logError("swr_convert", result);
  74. return AUDIO_DECODER_ERROR_INVALID_DATA;
  75. }
  76. int available = swr_get_out_samples(resampleContext, 0);
  77. if (available != 0) {
  78. LOGE("Expected no samples remaining after resampling, but found %d.",
  79. available);
  80. return AUDIO_DECODER_ERROR_INVALID_DATA;
  81. }
  82. outputBuffer += bufferOutSize;
  83. outSize += bufferOutSize;
  84. }
  85. return outSize;
  86. }

我们要实现过滤器,其实就是在这个方法重采样之前来实现我们的业务需求。

        过滤器的运作流程我们可以看做一条流水线,输入缓冲区---->过滤操作---->接收器,透过现象看本质,其实过滤器原理就是这么简单,展开讲讲就是原始音频帧甩给输入缓冲区,中间开始过滤操作,操作完成后就会把处理后的数据甩给接收器,接收器的参数为一个AVFrame格式数据,就是我们处理后的音频帧数据,下面开始实操:

        

  1. AVFilterGraph *filter_graph = avfilter_graph_alloc();//创建过滤器图
  2. AVFilterContext *buffersrc_ctx = nullptr;
  3. AVFilterContext *buffersink_ctx = nullptr;
  4. AVFilterInOut *inputs = avfilter_inout_alloc();
  5. AVFilterInOut *outputs = avfilter_inout_alloc();
  6. const AVFilter *buffersrc = avfilter_get_by_name("abuffer");
  7. const AVFilter *buffersink = avfilter_get_by_name("abuffersink");
  8. int ret;
  9. char args[512];
  10. //重点来了,PRIx64太重要了,这里的channel_layout只接受一个这样类型的数据,代表5.1声道或者立体声啥的,必备
  11. snprintf(args, sizeof(args),
  12. "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
  13. 1, frame->sample_rate, frame->sample_rate,
  14. av_get_sample_fmt_name(context->sample_fmt),
  15. frame->channel_layout);
  16. avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args,
  17. nullptr, filter_graph)
  18. avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
  19. nullptr, nullptr,filter_graph)
  20. outputs->name = av_strdup("in");
  21. outputs->filter_ctx = buffersrc_ctx;
  22. outputs->pad_idx = 0;
  23. outputs->next = nullptr;
  24. inputs->name = av_strdup("out");
  25. inputs->filter_ctx = buffersink_ctx;
  26. inputs->pad_idx = 0;
  27. inputs->next = nullptr;
  28. //真正的过滤器逻辑命令
  29. const char *filter_descr = "pan=stereo|FL<FL+0.5*FC+0.6*LFE+0.6*SL|FR<FR+0.5*FC+0.6*LFE+0.6*SR";//如果通道规范中的“=”被“<”替换,则该规范的增益将被重新规范化,使总数为1,从而避免削波噪声。
  30. // 将输入输出连接到过滤器图
  31. ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr);
  32. // 打开过滤器图
  33. ret = avfilter_graph_config(filter_graph, nullptr);
  34. // 将要处理的帧数据交给buffersrc,就是咱们说的第一步
  35. ret = av_buffersrc_add_frame(buffersrc_ctx, frame);
  36. // 提取出处理完的帧数据,这里参数给frame,是覆盖原来的frame,因为原来的也没用了,所以省的再开一块
  37. ret = av_buffersink_get_frame(buffersink_ctx, frame);
  38. // 释放资源
  39. avfilter_graph_free(&filter_graph);

        至此,过滤器就完成了,一定要仔细看注释,这段过滤器的相关代码,放在decodePacket方法中avcodec_receive_frame解码生成原始帧数据后、重采样之前执行,这里面有个坑:

  1. snprintf(args, sizeof(args),
  2. "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
  3. 1, frame->sample_rate, frame->sample_rate,
  4. av_get_sample_fmt_name(context->sample_fmt),
  5. frame->channel_layout);

其中channel_layout只能接受一个PRIx64格式的数据,所以这里要这么写,当时怎么创建输入缓冲区都是失败,要么就是最后声音全是噪音,就是因为格式不对。这段代码的意思就是给输入缓冲区定一个存储数据的格式,这是音频数据的格式,如果是视频数据,是另外一个写法,网上随便找找就有,如果有需要的话大家可以搜索一下。

        这里稍微解释下这些参数都是啥意思:

        sample_rate:采样率,44100,48000等

        sample_fmt:存储格式,fltp(ffmpeg自己的存储格式),s16(PCM音频流大多数是这种)等

        channel_layout:声道布局,比如立体声啊,5.1环绕音啊,7.1更牛逼的环绕音啊等

        行了,写到这里差不多了,下面放出无修正完整版decodePacket方法,很乱,但是可以看到我研究过程中的心路历程:

  1. int decodePacket(AVCodecContext *context, AVPacket *packet,
  2. uint8_t *outputBuffer, int outputSize) {
  3. LOGE("##############################decodePacket START####################################");
  4. int result = 0;
  5. // Queue input data.
  6. result = avcodec_send_packet(context, packet);//真正的解码操作,负责将数据发送给解码器
  7. if (result) {
  8. logError("avcodec_send_packet", result);
  9. return transformError(result);
  10. }
  11. // print_all_filters();
  12. //这块贼牛逼,param_channels=1是双声道,声道数是2.param_channels=2是5.1,声道数是6
  13. LOGE("检查设置的声道参数,以便确认是否需要过滤器:%d", param_channels);
  14. // Dequeue output data until it runs out.
  15. int outSize = 0;
  16. while (true) {
  17. // LOGE("&&&&&&&&&&&&&&&&&while循环开始&&&&&&&&&&&&&&&&&");
  18. AVFrame *frame = av_frame_alloc();//分配一个帧结构体的内存空间
  19. if (!frame) {
  20. LOGE("Failed to allocate output frame.");
  21. return AUDIO_DECODER_ERROR_INVALID_DATA;
  22. }
  23. result = avcodec_receive_frame(context, frame);//提取解码后的数据,用于从解码器中提取出解码后的帧数据
  24. // LOGE("从解码器中提取出解码后的帧数据的情况:result===%d", result);
  25. if (result) {
  26. av_frame_free(&frame);
  27. // LOGE("执行了释放帧的方法");
  28. if (result == AVERROR(EAGAIN)) {
  29. break;
  30. }
  31. logError("avcodec_receive_frame", result);
  32. return transformError(result);
  33. }
  34. //param_channels=1是双声道,声道数是2.param_channels=2是5.1,声道数是6
  35. if (param_channels == 1) {//只有java层设置了双声道,才需要走过滤器来做声道融合
  36. LOGE("帧数据:sample_rate===%d", frame->sample_rate);
  37. // LOGE("帧数据:nb_samples===%d", frame->nb_samples);
  38. LOGE("帧数据:声道数量===%d", frame->channels);
  39. // LOGE("帧数据:音频格式===%s",av_get_sample_fmt_name((AVSampleFormat) frame->format));//8=fltp,1=s16
  40. // LOGE("帧数据:音频数据===%d", frame->data);
  41. // const char *filter_descr = "pan=stereo|FL<FL+0.5*FC+0.6*LFE+0.6*SL|FR<FR+0.5*FC+0.6*LFE+0.6*SR";//如果通道规范中的“=”被“<”替换,则该规范的增益将被重新规范化,使总数为1,从而避免削波噪声。
  42. const char *filter_descr = "pan=stereo|c0=FL+FC+LFE+SL|c1=FR+FC+LFE+SR";//音量与不使用滤波器相同,但是看FFMPEG官方文档中说,这样可能会造成削波噪声
  43. // const char *filter_descr = "pan=stereo|FL<FL+0.5*FC+0.6*BL+0.6*SL|FR<FR+0.5*FC+0.6*BR+0.6*SR";//FFMPEG官方文档中给的downmix到立体声的示例
  44. // const char *filter_descr = "pan=stereo|c0=FL|c1=FR";//只保留左右声道声音
  45. AVFilterGraph *filter_graph = avfilter_graph_alloc();//创建过滤器图
  46. AVFilterContext *buffersrc_ctx = nullptr;
  47. AVFilterContext *buffersink_ctx = nullptr;
  48. AVFilterInOut *inputs = avfilter_inout_alloc();
  49. AVFilterInOut *outputs = avfilter_inout_alloc();
  50. const AVFilter *buffersrc = avfilter_get_by_name("abuffer");
  51. const AVFilter *buffersink = avfilter_get_by_name("abuffersink");
  52. int ret;
  53. if (!buffersrc) {
  54. // LOGE("流程:buffersrc创建失败");
  55. } else {
  56. // LOGE("流程:buffersrc创建成功");
  57. }
  58. char args[512];
  59. //重点来了,PRIx64太重要了,这里的channel_layout只接受一个这样类型的数据,代表5.1声道或者立体声啥的,必备
  60. snprintf(args, sizeof(args),
  61. "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
  62. 1, frame->sample_rate, frame->sample_rate,
  63. av_get_sample_fmt_name(context->sample_fmt),
  64. frame->channel_layout);
  65. if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args,
  66. nullptr, filter_graph) < 0) {
  67. // LOGE("流程:avfilter_graph_create_filter in 失败");
  68. } else {
  69. // LOGE("流程:avfilter_graph_create_filter in 成功");
  70. }
  71. // 创建一个缓冲区接收器过滤器
  72. if (!buffersink) {
  73. // LOGE("流程:buffersink创建失败");
  74. } else {
  75. // LOGE("流程:buffersink创建成功");
  76. }
  77. if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr,
  78. filter_graph) < 0) {
  79. // LOGE("流程:avfilter_graph_create_filter out 失败");
  80. } else {
  81. // LOGE("流程:avfilter_graph_create_filter out 成功");
  82. }
  83. // // 创建一个音频过滤器并连接到缓冲区源和接收器
  84. outputs->name = av_strdup("in");
  85. outputs->filter_ctx = buffersrc_ctx;
  86. outputs->pad_idx = 0;
  87. outputs->next = nullptr;
  88. inputs->name = av_strdup("out");
  89. inputs->filter_ctx = buffersink_ctx;
  90. inputs->pad_idx = 0;
  91. inputs->next = nullptr;
  92. // 将输入输出连接到过滤器图
  93. ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr);
  94. //过滤器输入输出直连,测试用,这样能测试一下代码大体正常否,目前直连的时候,一切正常,能正常输出PCM的音频
  95. // ret = avfilter_link(buffersrc_ctx, 0, buffersink_ctx, 0);
  96. if (ret < 0) {
  97. // LOGE("流程:这里加上pan过滤命令avfilter_graph_parse_ptr 失败:%d", ret);
  98. } else {
  99. // LOGE("流程:这里加上pan过滤命令avfilter_graph_parse_ptr 成功:%d", ret);
  100. }
  101. // 打开过滤器图
  102. ret = avfilter_graph_config(filter_graph, nullptr);
  103. if (ret < 0) {
  104. // LOGE("流程:avfilter_graph_config 失败:%d", ret);
  105. } else {
  106. // LOGE("流程:avfilter_graph_config 成功:%d", ret);
  107. }
  108. ret = av_buffersrc_add_frame(buffersrc_ctx, frame);
  109. // ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF);
  110. // ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame, AV_BUFFERSRC_FLAG_PUSH);
  111. if (ret < 0) {
  112. // 错误处理
  113. // LOGE("流程:av_buffersrc_add_frame 错误:%d", ret);
  114. } else {
  115. // LOGE("流程:av_buffersrc_add_frame 成功");
  116. }
  117. ret = av_buffersink_get_frame(buffersink_ctx, frame);
  118. if (ret < 0) {
  119. // 错误处理
  120. // LOGE("流程:av_buffersink_get_frame 错误%d", ret);
  121. } else {
  122. // LOGE("流程:av_buffersink_get_frame 成功");
  123. }
  124. // 释放资源
  125. avfilter_graph_free(&filter_graph);
  126. LOGE("过滤后的帧数据:sample_rate===%d", frame->sample_rate);
  127. // LOGE("过滤后的帧数据:nb_samples===%d", frame->nb_samples);
  128. LOGE("过滤后的帧数据:声道数量===%d", frame->channels);
  129. // LOGE("过滤后的帧数据:音频格式===%s",
  130. // av_get_sample_fmt_name((AVSampleFormat) frame->format));//8=fltp,1=s16
  131. // LOGE("过滤后的帧数据:音频数据===%d", frame->data);
  132. }
  133. // Resample output.
  134. AVSampleFormat in_sampleFormat = context->sample_fmt;//fltp,一般情况都是这个,因为ffmpeg的储存格式就是这个
  135. AVSampleFormat out_sampleFormat = context->request_sample_fmt;//输出的格式,目前是s16
  136. // LOGE("音频输入格式in_sampleFormat:%s,%d", av_get_sample_fmt_name(in_sampleFormat),
  137. // in_sampleFormat);
  138. // LOGE("音频输出格式out_sampleFormat:%s,%d", av_get_sample_fmt_name(out_sampleFormat),
  139. // out_sampleFormat);
  140. int channelCount = frame->channels;
  141. int channelLayout = frame->channel_layout;
  142. int sampleRate = frame->sample_rate;
  143. int sampleCount = frame->nb_samples;
  144. int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
  145. in_sampleFormat, 1);
  146. SwrContext *resampleContext;
  147. if (context->opaque) {
  148. resampleContext = (SwrContext *) context->opaque;
  149. } else {
  150. resampleContext = swr_alloc();
  151. // AV_CH_LAYOUT_STEREO,,,AV_CH_LAYOUT_STEREO_DOWNMIX
  152. av_opt_set_channel_layout(resampleContext, "in_channel_layout", channelLayout, 0);
  153. av_opt_set_channel_layout(resampleContext, "out_channel_layout",
  154. context->channel_layout, 0);
  155. av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
  156. av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
  157. av_opt_set_sample_fmt(resampleContext, "in_sample_fmt", in_sampleFormat, 0);
  158. av_opt_set_sample_fmt(resampleContext, "out_sample_fmt", out_sampleFormat, 0);
  159. result = swr_init(resampleContext);
  160. if (result < 0) {
  161. logError("swr_init", result);
  162. av_frame_free(&frame);
  163. return transformError(result);
  164. }
  165. context->opaque = resampleContext;
  166. }
  167. int inSampleSize = av_get_bytes_per_sample(in_sampleFormat);//每帧音频数据量的大小
  168. int outSampleSize = av_get_bytes_per_sample(out_sampleFormat);
  169. int outSamples = swr_get_out_samples(resampleContext, sampleCount);
  170. int bufferOutSize = outSampleSize * context->channels * outSamples;
  171. LOGE("**********************");
  172. LOGE("最终使用的数据:channelCount===%d", channelCount);
  173. // LOGE("最终使用的数据:channelLayout===%d", channelLayout);
  174. LOGE("最终使用的数据:sampleRate===%d", sampleRate);
  175. // LOGE("最终使用的数据:sampleCount===%d", sampleCount);
  176. // LOGE("最终使用的数据:dataSize===%d", dataSize);
  177. // LOGE("最终使用的数据:inSampleSize====%d", inSampleSize);
  178. // LOGE("最终使用的数据:outSampleSize====%d", outSampleSize);
  179. // LOGE("最终使用的数据:outSamples====%d", outSamples);
  180. // LOGE("最终使用的数据:bufferOutSize====%d", bufferOutSize);
  181. LOGE("**********************");
  182. if (outSize + bufferOutSize > outputSize) {
  183. LOGE("Output buffer size (%d) too small for output data (%d).",
  184. outputSize, outSize + bufferOutSize);
  185. av_frame_free(&frame);
  186. return AUDIO_DECODER_ERROR_INVALID_DATA;
  187. }
  188. if (av_sample_fmt_is_planar(context->sample_fmt)) {
  189. // LOGE("pcm planar模式");
  190. } else {
  191. // LOGE("pcm Pack模式");
  192. }
  193. if (av_sample_fmt_is_planar(context->request_sample_fmt)) {
  194. // LOGE("request_sample_fmt pcm planar模式");
  195. } else {
  196. // LOGE("request_sample_fmt pcm Pack模式");
  197. }
  198. //执行真正的重采样操作
  199. result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
  200. (const uint8_t **) frame->data, sampleCount);
  201. av_frame_free(&frame);
  202. if (result < 0) {
  203. logError("swr_convert", result);
  204. return AUDIO_DECODER_ERROR_INVALID_DATA;
  205. }
  206. int available = swr_get_out_samples(resampleContext, 0);
  207. if (available != 0) {
  208. LOGE("Expected no samples remaining after resampling, but found %d.",
  209. available);
  210. return AUDIO_DECODER_ERROR_INVALID_DATA;
  211. }
  212. outputBuffer += bufferOutSize;
  213. outSize += bufferOutSize;
  214. // LOGE("&&&&&&&&&&&&&&&&&while循环结束&&&&&&&&&&&&&&&&&");
  215. }
  216. LOGE("##############################decodePacket END####################################");
  217. return outSize;
  218. }

六.结语

        其实关于音视频,还有好多基础知识需要汲取,这些知识合起来能写好几本书。我因为换了工作,加入到了这个行业中,所以开始恶补音视频相关的知识,越看越觉得有意思,希望有一天在音视频的领域里,我也能成为像我两个同事一样的可靠全栈大前辈。

                

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/1001680
推荐阅读
相关标签
  

闽ICP备14008679号