赞
踩
最近一直在搞一个影视播放器的项目,算是一个嵌入式开发,学习了不少以前没有接触过的知识,很充实。其中播放器有一个需求,完整的需求描述是这样的:播放器的声卡是只能输出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,给你的播放器项目使用,当然,如果你只有一个项目需要用的话,也可以直接引入项目,更方便。
********************************************无情分割线************************************************
咱们一步一步的来。
这块我主要用的是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
记得这俩路径,一会用得着。
这块真是重点,要说的有很多点,咱们慢慢说。先说一下我的编译环境,我是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编译工作结束,撒花!
经历了上面编译工作后,我们终于可以正式进入开发业务逻辑的过程中了,开心~。首先我们需要让你的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来代理我们的音视频解码等操作,就是俗称的软解,可是我们还没有运行是不是?别着急,马上说。
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默认功能已经很强大了,它会自己寻找可用的解码器来进行音视频软解,甚至不需要你有什么额外操作,如果你的需求真的就是类似于: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,以便于消除噪音,实际应用的时候我会发现使用<以后耳朵听到的声音感觉比之前明显小了跟多,所以我选择用=,这块还是要看你实际的需求。
了解了过滤器命令行操作的方式,这个时候就该想如何在我们的项目代码中使用该功能咧?因为项目代码才是我们最终的归宿。
********************************************无情分割线************************************************
这里最重要的一个文件就是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;
- }

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