一.项目背景
最近一直在搞一个影视播放器的项目,算是一个嵌入式开发,学习了不少以前没有接触过的知识,很充实。其中播放器有一个需求,完整的需求描述是这样的:播放器的声卡是只能输出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;
}
六.结语
其实关于音视频,还有好多基础知识需要汲取,这些知识合起来能写好几本书。我因为换了工作,加入到了这个行业中,所以开始恶补音视频相关的知识,越看越觉得有意思,希望有一天在音视频的领域里,我也能成为像我两个同事一样的可靠全栈大前辈。