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

一.项目背景       

        最近一直在搞一个影视播放器的项目,算是一个嵌入式开发,学习了不少以前没有接触过的知识,很充实。其中播放器有一个需求,完整的需求描述是这样的:播放器的声卡是只能输出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文件,修改一些参数,方便你调用。主要是修改这段命令:

修改完成后:

FFMPEG_MODULE_PATH="/Users/liuqn/project_support/ExoPlayer-release-v2/extensions/ffmpeg/src/main"
NDK_PATH="/Users/liuqn/Library/Android/sdk/ndk/27.0.11718014"
HOST_PLATFORM="darwin-x86_64"
ENABLED_DECODERS=("ac3,mp3,flac")
JOBS=$(nproc 2> /dev/null || sysctl -n hw.ncpu 2> /dev/null || echo 4)
echo "Using $JOBS jobs for make"
COMMON_OPTIONS="
    --target-os=android
    --enable-static
    --disable-shared
    --disable-doc
    --disable-programs
    --disable-everything
    --enable-filter=pan
    --enable-avdevice
    --enable-avformat
    --enable-swscale
    --enable-postproc
    --enable-avfilter
    --enable-symver
    --enable-avresample
    --enable-swresample
    --extra-ldexeflags=-pie
    "

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类:

public class FfmpegRenderFactory extends DefaultRenderersFactory {
    FfmpegAudioRenderer ffmpegAudioRenderer;

    public FfmpegRenderFactory(Context context) {
        super(context);
        setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER);
    }

    @Override
    protected void buildAudioRenderers(Context context,
                                       @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector,
                                       boolean enableDecoderFallback, AudioSink audioSink, Handler eventHandler,
                                       AudioRendererEventListener eventListener, ArrayList<Renderer> out) {
        ffmpegAudioRenderer = new FfmpegAudioRenderer();
        out.add(ffmpegAudioRenderer);
        super.buildAudioRenderers(context, extensionRendererMode, mediaCodecSelector,
                enableDecoderFallback, audioSink, eventHandler, eventListener, out);


        LogUtils.e("test_audio", "创建FfmpegAudioRenderer:" + FfmpegLibrary.getVersion());
        LogUtils.e("test_audio", "ffmpegHasDecoder===" + FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_AC3));

    }
}

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


            DefaultDataSource.Factory zy_dataSourceFactory = new DefaultDataSource.Factory(this);
            MediaSource.Factory dataSourceFactory = new DefaultMediaSourceFactory(/* context= */ this).setDataSourceFactory(zy_dataSourceFactory);
            ExoPlayer.Builder playerBuilder = new ExoPlayer.Builder(/* context= */ this)
                    .setMediaSourceFactory(dataSourceFactory)
                    .setLoadControl(new DefaultLoadControl());

            //重点:使用FFMPEG软解
            playerBuilder.setRenderersFactory(new FfmpegRenderFactory(this)); 


            player = playerBuilder.build();
            player.addListener(new PlayerEventListener());
            player.addAnalyticsListener(new ErrorEventListener());

            player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
            player.setPlayWhenReady(true);
            debugViewHelper = new DebugTextViewHelper(player, debugTextView);
      
        //加入播放路径
        player.setMediaItem(MediaItem.fromUri("/storage/emulated/0/AC3_5_1.ac3"));
        player.seekTo(0, 0);
        player.prepare();

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


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文件,有什么改动都有注释,认真看:


cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR)

# Enable C++11 features.
set(CMAKE_CXX_STANDARD 11)

project(libffmpegJNI C CXX)

# Additional flags needed for "arm64-v8a" from NDK 23.1.7779620 and above.
# See https://github.com/google/ExoPlayer/issues/9933#issuecomment-1029775358.
if (${ANDROID_ABI} MATCHES "arm64-v8a")
    set(CMAKE_CXX_FLAGS "-Wl,-Bsymbolic")
endif ()

set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")

#重点:记得你当时编译出来是8个.a的静态库么?但是原始生成的cmake文件中只有avutil swresample avcodec,是因为最基础的功能只需要这三个库。
#当你需要在你最终生成的so中包含你所需要的库时,要在这里增加对应的库名,比如你需要使用过滤器,就增加avfilter,我记得如果需要使用pan的话,
#还需要增加swresample,其实就算8个全包含,包体也没大多少,建议不解释全包含,省心省力。
foreach (ffmpeg_lib avutil swresample avresample avcodec avfilter avdevice avformat swscale)
    set(ffmpeg_lib_filename lib${ffmpeg_lib}.a)
    set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
    add_library(
            ${ffmpeg_lib}
            STATIC
            IMPORTED)
    set_target_properties(
            ${ffmpeg_lib} PROPERTIES
            IMPORTED_LOCATION
            ${ffmpeg_lib_file_path})
endforeach ()

include_directories(${ffmpeg_location})
find_library(android_log_lib log)

#最终生成的so名、编译成动态库(.so)、实际代码类
add_library(ffmpegJNI
        SHARED
        ffmpeg_jni.cc)

#重点:看上面注释,一样的道理。
target_link_libraries(ffmpegJNI
        PRIVATE android
        PRIVATE avutil
        PRIVATE swresample
        PRIVATE avresample
        PRIVATE avcodec
        PRIVATE avfilter
        PRIVATE avdevice
        PRIVATE avformat
        PRIVATE swscale
        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里”

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

FL:Front Left,即左声道的主要信号。
FC:Front Center,即中央声道的信号,用于环绕声效果。
LFE:Low Frequency Effects,即低频效果信号,用于增强低音效果。
SL:Side Left,即左侧环绕声道的信号。
FR:Front Right,即右声道的主要信号。
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()方法,它负责了整个播放器中:数据包输入-->解码---->重采样---->输出。而我们想使用过滤器来实现一些复杂的业务逻辑,我们就要将现在的流程改成:数据包输入--->解码---->过滤---->重采样---->输出,整个播放过程中,这个方法将不停的被调用。

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

int decodePacket(AVCodecContext *context, AVPacket *packet,
                 uint8_t *outputBuffer, int outputSize) {
  int result = 0;
  // Queue input data.
  真正的解码操作,负责将数据发送给解码器
  result = avcodec_send_packet(context, packet);
  if (result) {
    logError("avcodec_send_packet", result);
    return transformError(result);
  }

  // Dequeue output data until it runs out.
  int outSize = 0;
  while (true) {
    AVFrame *frame = av_frame_alloc();
    if (!frame) {
      LOGE("Failed to allocate output frame.");
      return AUDIO_DECODER_ERROR_INVALID_DATA;
    }
    //提取解码后的数据,用于从解码器中提取出解码后的帧数据
    result = avcodec_receive_frame(context, frame);
    if (result) {
      av_frame_free(&frame);
      if (result == AVERROR(EAGAIN)) {
        break;
      }
      logError("avcodec_receive_frame", result);
      return transformError(result);
    }

    // Resample output.
    AVSampleFormat sampleFormat = context->sample_fmt;
    int channelCount = context->channels;
    int channelLayout = context->channel_layout;
    int sampleRate = context->sample_rate;
    int sampleCount = frame->nb_samples;
    int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
                                              sampleFormat, 1);
    SwrContext *resampleContext;
    if (context->opaque) {
      resampleContext = (SwrContext *)context->opaque;
    } else {
      resampleContext = swr_alloc();
      av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
      av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0);
      av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
      av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
      av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0);
      // The output format is always the requested format.
      av_opt_set_int(resampleContext, "out_sample_fmt",
                     context->request_sample_fmt, 0);
      result = swr_init(resampleContext);
      if (result < 0) {
        logError("swr_init", result);
        av_frame_free(&frame);
        return transformError(result);
      }
      context->opaque = resampleContext;
    }
    int inSampleSize = av_get_bytes_per_sample(sampleFormat);
    int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
    int outSamples = swr_get_out_samples(resampleContext, sampleCount);
    int bufferOutSize = outSampleSize * channelCount * outSamples;
    if (outSize + bufferOutSize > outputSize) {
      LOGE("Output buffer size (%d) too small for output data (%d).",
           outputSize, outSize + bufferOutSize);
      av_frame_free(&frame);
      return AUDIO_DECODER_ERROR_INVALID_DATA;
    }
    //执行真正的重采样操作,此时采样结束后的数据,已经给到了播放器
    result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
                         (const uint8_t **)frame->data, frame->nb_samples);
    av_frame_free(&frame);
    if (result < 0) {
      logError("swr_convert", result);
      return AUDIO_DECODER_ERROR_INVALID_DATA;
    }
    int available = swr_get_out_samples(resampleContext, 0);
    if (available != 0) {
      LOGE("Expected no samples remaining after resampling, but found %d.",
           available);
      return AUDIO_DECODER_ERROR_INVALID_DATA;
    }
    outputBuffer += bufferOutSize;
    outSize += bufferOutSize;
  }
  return outSize;
}

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

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

        

            AVFilterGraph *filter_graph = avfilter_graph_alloc();//创建过滤器图
            AVFilterContext *buffersrc_ctx = nullptr;
            AVFilterContext *buffersink_ctx = nullptr;
            AVFilterInOut *inputs = avfilter_inout_alloc();
            AVFilterInOut *outputs = avfilter_inout_alloc();
            const AVFilter *buffersrc = avfilter_get_by_name("abuffer");
            const AVFilter *buffersink = avfilter_get_by_name("abuffersink");
            int ret;

            char args[512];
            //重点来了,PRIx64太重要了,这里的channel_layout只接受一个这样类型的数据,代表5.1声道或者立体声啥的,必备
            snprintf(args, sizeof(args),
      "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
                     1, frame->sample_rate, frame->sample_rate,
                     av_get_sample_fmt_name(context->sample_fmt),
                     frame->channel_layout);


            avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args,
                                             nullptr, filter_graph)
            avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",                 
                                             nullptr, nullptr,filter_graph)
            outputs->name = av_strdup("in");
            outputs->filter_ctx = buffersrc_ctx;
            outputs->pad_idx = 0;
            outputs->next = nullptr;

            inputs->name = av_strdup("out");
            inputs->filter_ctx = buffersink_ctx;
            inputs->pad_idx = 0;
            inputs->next = nullptr;

            //真正的过滤器逻辑命令
            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,从而避免削波噪声。

            // 将输入输出连接到过滤器图
            ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr);

            // 打开过滤器图
            ret = avfilter_graph_config(filter_graph, nullptr);
            // 将要处理的帧数据交给buffersrc,就是咱们说的第一步
            ret = av_buffersrc_add_frame(buffersrc_ctx, frame);
            // 提取出处理完的帧数据,这里参数给frame,是覆盖原来的frame,因为原来的也没用了,所以省的再开一块
            ret = av_buffersink_get_frame(buffersink_ctx, frame);

            // 释放资源
            avfilter_graph_free(&filter_graph);

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

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

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

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

        sample_rate:采样率,44100,48000等

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

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

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

int decodePacket(AVCodecContext *context, AVPacket *packet,
                 uint8_t *outputBuffer, int outputSize) {
    LOGE("##############################decodePacket START####################################");
    int result = 0;
    // Queue input data.
    result = avcodec_send_packet(context, packet);//真正的解码操作,负责将数据发送给解码器
    if (result) {
        logError("avcodec_send_packet", result);
        return transformError(result);
    }


//    print_all_filters();

    //这块贼牛逼,param_channels=1是双声道,声道数是2.param_channels=2是5.1,声道数是6
    LOGE("检查设置的声道参数,以便确认是否需要过滤器:%d", param_channels);



    // Dequeue output data until it runs out.
    int outSize = 0;
    while (true) {
//        LOGE("&&&&&&&&&&&&&&&&&while循环开始&&&&&&&&&&&&&&&&&");
        AVFrame *frame = av_frame_alloc();//分配一个帧结构体的内存空间
        if (!frame) {
            LOGE("Failed to allocate output frame.");
            return AUDIO_DECODER_ERROR_INVALID_DATA;
        }


        result = avcodec_receive_frame(context, frame);//提取解码后的数据,用于从解码器中提取出解码后的帧数据
//        LOGE("从解码器中提取出解码后的帧数据的情况:result===%d", result);

        if (result) {
            av_frame_free(&frame);
//            LOGE("执行了释放帧的方法");
            if (result == AVERROR(EAGAIN)) {
                break;
            }
            logError("avcodec_receive_frame", result);
            return transformError(result);
        }


        //param_channels=1是双声道,声道数是2.param_channels=2是5.1,声道数是6
        if (param_channels == 1) {//只有java层设置了双声道,才需要走过滤器来做声道融合


            LOGE("帧数据:sample_rate===%d", frame->sample_rate);
//            LOGE("帧数据:nb_samples===%d", frame->nb_samples);
            LOGE("帧数据:声道数量===%d", frame->channels);
//            LOGE("帧数据:音频格式===%s",av_get_sample_fmt_name((AVSampleFormat) frame->format));//8=fltp,1=s16
//            LOGE("帧数据:音频数据===%d", frame->data);



//            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,从而避免削波噪声。
            const char *filter_descr = "pan=stereo|c0=FL+FC+LFE+SL|c1=FR+FC+LFE+SR";//音量与不使用滤波器相同,但是看FFMPEG官方文档中说,这样可能会造成削波噪声
//            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到立体声的示例
//        const char *filter_descr = "pan=stereo|c0=FL|c1=FR";//只保留左右声道声音
            AVFilterGraph *filter_graph = avfilter_graph_alloc();//创建过滤器图
            AVFilterContext *buffersrc_ctx = nullptr;
            AVFilterContext *buffersink_ctx = nullptr;
            AVFilterInOut *inputs = avfilter_inout_alloc();
            AVFilterInOut *outputs = avfilter_inout_alloc();
            const AVFilter *buffersrc = avfilter_get_by_name("abuffer");
            const AVFilter *buffersink = avfilter_get_by_name("abuffersink");
            int ret;

            if (!buffersrc) {
//                LOGE("流程:buffersrc创建失败");
            } else {
//                LOGE("流程:buffersrc创建成功");
            }

            char args[512];
            //重点来了,PRIx64太重要了,这里的channel_layout只接受一个这样类型的数据,代表5.1声道或者立体声啥的,必备
            snprintf(args, sizeof(args),
                     "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
                     1, frame->sample_rate, frame->sample_rate,
                     av_get_sample_fmt_name(context->sample_fmt),
                     frame->channel_layout);


            if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args,
                                             nullptr, filter_graph) < 0) {
//                LOGE("流程:avfilter_graph_create_filter  in 失败");
            } else {
//                LOGE("流程:avfilter_graph_create_filter  in 成功");
            }


            // 创建一个缓冲区接收器过滤器

            if (!buffersink) {
//                LOGE("流程:buffersink创建失败");
            } else {
//                LOGE("流程:buffersink创建成功");
            }
            if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr,
                                             filter_graph) < 0) {
//                LOGE("流程:avfilter_graph_create_filter  out 失败");
            } else {
//                LOGE("流程:avfilter_graph_create_filter  out 成功");
            }
//    // 创建一个音频过滤器并连接到缓冲区源和接收器

            outputs->name = av_strdup("in");
            outputs->filter_ctx = buffersrc_ctx;
            outputs->pad_idx = 0;
            outputs->next = nullptr;

            inputs->name = av_strdup("out");
            inputs->filter_ctx = buffersink_ctx;
            inputs->pad_idx = 0;
            inputs->next = nullptr;

            // 将输入输出连接到过滤器图
            ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr);
            //过滤器输入输出直连,测试用,这样能测试一下代码大体正常否,目前直连的时候,一切正常,能正常输出PCM的音频
//        ret = avfilter_link(buffersrc_ctx, 0, buffersink_ctx, 0);
            if (ret < 0) {
//                LOGE("流程:这里加上pan过滤命令avfilter_graph_parse_ptr 失败:%d", ret);
            } else {
//                LOGE("流程:这里加上pan过滤命令avfilter_graph_parse_ptr 成功:%d", ret);
            }
            // 打开过滤器图
            ret = avfilter_graph_config(filter_graph, nullptr);
            if (ret < 0) {
//                LOGE("流程:avfilter_graph_config 失败:%d", ret);
            } else {
//                LOGE("流程:avfilter_graph_config 成功:%d", ret);
            }

            ret = av_buffersrc_add_frame(buffersrc_ctx, frame);
//        ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF);
//        ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame, AV_BUFFERSRC_FLAG_PUSH);
            if (ret < 0) {
                // 错误处理
//                LOGE("流程:av_buffersrc_add_frame 错误:%d", ret);
            } else {
//                LOGE("流程:av_buffersrc_add_frame 成功");
            }
            ret = av_buffersink_get_frame(buffersink_ctx, frame);
            if (ret < 0) {
                // 错误处理
//                LOGE("流程:av_buffersink_get_frame 错误%d", ret);
            } else {
//                LOGE("流程:av_buffersink_get_frame 成功");
            }

            // 释放资源
            avfilter_graph_free(&filter_graph);

            LOGE("过滤后的帧数据:sample_rate===%d", frame->sample_rate);
//            LOGE("过滤后的帧数据:nb_samples===%d", frame->nb_samples);
            LOGE("过滤后的帧数据:声道数量===%d", frame->channels);
//            LOGE("过滤后的帧数据:音频格式===%s",
//                 av_get_sample_fmt_name((AVSampleFormat) frame->format));//8=fltp,1=s16
//            LOGE("过滤后的帧数据:音频数据===%d", frame->data);
        }




        // Resample output.
        AVSampleFormat in_sampleFormat = context->sample_fmt;//fltp,一般情况都是这个,因为ffmpeg的储存格式就是这个
        AVSampleFormat out_sampleFormat = context->request_sample_fmt;//输出的格式,目前是s16

//        LOGE("音频输入格式in_sampleFormat:%s,%d", av_get_sample_fmt_name(in_sampleFormat),
//             in_sampleFormat);
//        LOGE("音频输出格式out_sampleFormat:%s,%d", av_get_sample_fmt_name(out_sampleFormat),
//             out_sampleFormat);


        int channelCount = frame->channels;
        int channelLayout = frame->channel_layout;
        int sampleRate = frame->sample_rate;
        int sampleCount = frame->nb_samples;
        int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
                                                  in_sampleFormat, 1);

        SwrContext *resampleContext;
        if (context->opaque) {
            resampleContext = (SwrContext *) context->opaque;
        } else {
            resampleContext = swr_alloc();
//            AV_CH_LAYOUT_STEREO,,,AV_CH_LAYOUT_STEREO_DOWNMIX
            av_opt_set_channel_layout(resampleContext, "in_channel_layout", channelLayout, 0);
            av_opt_set_channel_layout(resampleContext, "out_channel_layout",
                                      context->channel_layout, 0);
            av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
            av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
            av_opt_set_sample_fmt(resampleContext, "in_sample_fmt", in_sampleFormat, 0);
            av_opt_set_sample_fmt(resampleContext, "out_sample_fmt", out_sampleFormat, 0);
            result = swr_init(resampleContext);

            if (result < 0) {
                logError("swr_init", result);
                av_frame_free(&frame);
                return transformError(result);
            }
            context->opaque = resampleContext;
        }


        int inSampleSize = av_get_bytes_per_sample(in_sampleFormat);//每帧音频数据量的大小
        int outSampleSize = av_get_bytes_per_sample(out_sampleFormat);
        int outSamples = swr_get_out_samples(resampleContext, sampleCount);
        int bufferOutSize = outSampleSize * context->channels * outSamples;

        LOGE("**********************");
        LOGE("最终使用的数据:channelCount===%d", channelCount);
//        LOGE("最终使用的数据:channelLayout===%d", channelLayout);
        LOGE("最终使用的数据:sampleRate===%d", sampleRate);
//        LOGE("最终使用的数据:sampleCount===%d", sampleCount);
//        LOGE("最终使用的数据:dataSize===%d", dataSize);
//        LOGE("最终使用的数据:inSampleSize====%d", inSampleSize);
//        LOGE("最终使用的数据:outSampleSize====%d", outSampleSize);
//        LOGE("最终使用的数据:outSamples====%d", outSamples);
//        LOGE("最终使用的数据:bufferOutSize====%d", bufferOutSize);
        LOGE("**********************");


        if (outSize + bufferOutSize > outputSize) {
            LOGE("Output buffer size (%d) too small for output data (%d).",
                 outputSize, outSize + bufferOutSize);
            av_frame_free(&frame);
            return AUDIO_DECODER_ERROR_INVALID_DATA;
        }
        if (av_sample_fmt_is_planar(context->sample_fmt)) {
//            LOGE("pcm planar模式");
        } else {
//            LOGE("pcm Pack模式");
        }
        if (av_sample_fmt_is_planar(context->request_sample_fmt)) {
//            LOGE("request_sample_fmt pcm planar模式");
        } else {
//            LOGE("request_sample_fmt pcm Pack模式");
        }

        //执行真正的重采样操作
        result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
                             (const uint8_t **) frame->data, sampleCount);
        av_frame_free(&frame);
        if (result < 0) {
            logError("swr_convert", result);
            return AUDIO_DECODER_ERROR_INVALID_DATA;
        }
        int available = swr_get_out_samples(resampleContext, 0);
        if (available != 0) {
            LOGE("Expected no samples remaining after resampling, but found %d.",
                 available);
            return AUDIO_DECODER_ERROR_INVALID_DATA;
        }
        outputBuffer += bufferOutSize;
        outSize += bufferOutSize;
//        LOGE("&&&&&&&&&&&&&&&&&while循环结束&&&&&&&&&&&&&&&&&");
    }
    LOGE("##############################decodePacket END####################################");
    return outSize;
}

六.结语

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

                

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值