移植FFmpeg到Android上
因为Mix Music 解码的需求,所以得选择合适的解码工具.尝试了4种解码方式,最后还是FFmpeg 的效果最好
这是项目的GitHub 地址,如果你的ffmpeg实在是编译不过去,可以去这里下载,然后再进行编译.
编译出ffmpeg的so文件 要编译FFmpeg,第一步就是先获取源码.直接去官网 就可以下载. 解压后得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 └───ffmpeg ├───compat ├───doc ├───ffbuild ├───ffmpeg ├───fftools ├───libavcodec ├───libavdevice ├───libavfilter ├───libavformat ├───libavresample ├───libavutil ├───libpostproc ├───libswresample ├───libswscale ├───presets ├───tests └───tools
打开configure文件找到
1 2 3 4 SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'
将内容修改为
1 2 3 4 SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' SLIB_INSTALL_LINKS='$(SLIBNAME)'
这是因为默认生成的文件名称中版本号位于最后,不符合Android命名规范.
在ffmpeg目录下创建build.sh文件 内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 NDK=F:/android-ndk-r14b SYSROOT=$NDK /platforms/android-21/arch-arm/ TOOLCHAIN=$NDK /toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64 CPU=arm PREFIX=$(pwd )/android/$CPU ADDI_CFLAGS="-marm" function build_one{ ./configure \ --prefix=$PREFIX \ --enable-shared \ --disable-static \ --enable-asm \ --disable-doc \ --disable-gpl \ --enable-small \ --disable-encoders \ --disable-decoders \ --disable-ffmpeg \ --enable-encoder=pcm_s16le \ --enable-decoder=mp3 \ --disable-ffplay \ --disable-ffprobe \ --disable-ffserver \ --disable-doc \ --disable-symver \ --enable-jni \ --cross-prefix=$TOOLCHAIN /bin/arm-linux-androideabi- \ --target-os=android \ --arch =arm \ --enable-cross-compile \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS " \ --extra-ldflags="$ADDI_LDFLAGS " \ $ADDITIONAL_CONFIGURE_FLAG make clean make make install } build_one
更加不幸的是.sh文件现在还不能运行,得先下载MinGW ,记得build.sh文件不编辑的时候,不要挂在后台处在打开状态 下载好后,直接打开 勾选上如图所示的选项
在左上角Installation菜单中点击Apply Changes,然后等待下载并自动安装(可能需要酸酸乳,因为网络质量不是很高)
下载完毕后,直接进度你的MinGW文件夹,找到msys文件夹并进入.双击msys.bat就会启动一个长的很像CMD的程序,至少在打开路径上都是使用cd命令
cd进入到之前build.sh的文件夹,输入./build.sh运行build.sh,它就会自动进行编译操作.如果遇到了各种奇怪问题,不要放弃,因为你的问题前人已经犯过,你只需要善用搜索引擎,踩着他们的脚印即可.
因为我在编译的时候也遇到了各种各样的问题,最后通过更换NDK版本解决了问题. 等待一段时间后,在ffmpeg目录下就会多出android文件夹,我们需要用到的so文件就在这个ffmpeg/android/arm/lib文件夹内
编译完成后我们得到了
1 2 3 4 5 6 7 8 9 10 include lib libavcodec.so libavdevice.so libavfilter.so libavformat.so libavutil.so libpostproc.so libswresample.so libswscale.so
编译安卓可以调用的so文件 终于到了要调用FFmpeg的阶段了. 和常规NDK编译的需求一样,需要创建一个jni文件夹.把上一步得到的include文件夹和那些so文件复制进来.
还需要把位于ffmpeg根目录中的
cmdutils.c和cmdutils.h
ffmpeg.h和ffmpeg.c
ffmpeg_opt.c
ffmpeg_hw.c
ffmpeg_filter.c
va_copy.h
config.h 这个文件只有你之前手动编译了ffmpeg才会得到 也复制到jni文件夹内
创建Application.mk
1 2 3 4 APP_ABI := armeabi-v7a armeabi APP_MODULES := libffmpegjni APP_CFLAGS += -DSTDC_HEADERS
以及Android.mk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= avcodec-prebuilt-armeabi LOCAL_SRC_FILES:= prebuilt/armeabi/libavcodec.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE:= avdevice-prebuilt-armeabi LOCAL_SRC_FILES:= prebuilt/armeabi/libavdevice.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE:= avfilter-prebuilt-armeabi LOCAL_SRC_FILES:= prebuilt/armeabi/libavfilter.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE:= avformat-prebuilt-armeabi LOCAL_SRC_FILES:= prebuilt/armeabi/libavformat.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := avutil-prebuilt-armeabi LOCAL_SRC_FILES := prebuilt/armeabi/libavutil.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := swresample-prebuilt-armeabi LOCAL_SRC_FILES := prebuilt/armeabi/libswresample.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := swscale-prebuilt-armeabi LOCAL_SRC_FILES := prebuilt/armeabi/libswscale.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := libffmpegjni LOCAL_ARM_MODE := arm LOCAL_SRC_FILES := ffmpegJni.c \ ffmpeg.c \ cmdutils.c \ ffmpeg_opt.c \ ffmpeg_filter.c \ ffmpeg_hw.c LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz LOCAL_SHARED_LIBRARIES:= avcodec-prebuilt-armeabi \ avdevice-prebuilt-armeabi \ avfilter-prebuilt-armeabi \ avformat-prebuilt-armeabi \ avutil-prebuilt-armeabi \ swresample-prebuilt-armeabi \ swscale-prebuilt-armeabi LOCAL_C_INCLUDES += -L$(SYSROOT)/usr/include LOCAL_C_INCLUDES += $(LOCAL_PATH)/include LOCAL_CFLAGS := -DUSE_ARM_CONFIG include $(BUILD_SHARED_LIBRARY)
在ffmpeg.c中有些东西需要我们修改下,因为正常情况下ffmpeg执行一条命令,它就会自动退出,我们当然不希望这样.所以需要对他的推出操作进行处理. 打开ffmpeg.c,找到如下内容并修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ret = ffmpeg_parse_options(argc, argv); if (ret < 0 ){ return 1 ; } if (nb_output_files <= 0 && nb_input_files == 0 ) { show_usage(); av_log(NULL , AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n" , program_name); return 1 ; } if (nb_output_files <= 0 ) { av_log(NULL , AV_LOG_FATAL, "At least one output file must be specified\n" ); return 1 ; } if (nb_input_files == 0 ) { av_log(NULL , AV_LOG_FATAL, "At least one input file must be specified\n" ); return 1 ; }
把下面的内容,添加到static void ffmpeg_cleanup(int ret)函数末尾
1 2 3 4 5 6 ffmpeg_exited = 1 ; nb_filtergraphs = 0 ; nb_output_files = 0 ; nb_output_streams = 0 ; nb_input_files = 0 ; nb_input_streams = 0 ;
为了获取到ffmpeg的处理进度 修改log_callback_null函数内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void log_callback_null (void *ptr, int level, const char *fmt, va_list vl) { static int print_prefix = 1 ; static int count; static char prev[1024 ]; char line[1024 ]; static int is_atty; av_log_format_line(ptr, level, fmt, vl, line, sizeof (line), &print_prefix); strcpy (prev, line); if (level <= AV_LOG_WARNING){ XLOGE("%s" , line); }else { XLOGD("%s" , line); callJavaMethod(line); } }
并再main函数的开始处添加
1 av_log_set_callback(log_callback_null);
再打开cmdutils.c,对exit_program进行修改,别忘记修改头文件中的exit_program
1 2 3 4 int exit_program (int ret) { return ret; }
然后创建一个名为ffmpegJni.c的文件和它的头文件ffmpegJni.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include "logjni.h" #include "ffmpegJni.h" #include <stdlib.h> #include <stdbool.h> int main (int argc, char **argv) ;static JavaVM *jvm = NULL ;static jclass m_clazz = NULL ;static jclass m_jobj = NULL ;static JNIEnv *m_env = NULL ;JNIEXPORT jint JNICALL Java_com_chaosgoo_ffmpegproject_ffmpegJni_main (JNIEnv *env, jclass obj, jobjectArray commands) { (*env)->GetJavaVM(env, &jvm); jclass clazz = (*env)->GetObjectClass(env,obj); m_jobj = obj; m_clazz = (*env)->NewGlobalRef(env, clazz); m_env = env; int argc = (*env)->GetArrayLength(env, commands); char *argv[argc]; int i; int result = 0 ; for (i = 0 ; i < argc; i++) { jstring js = (jstring)(*env)->GetObjectArrayElement(env, commands, i); argv[i] = (char *)(*env)->GetStringUTFChars(env, js, 0 ); } LOGD("----------begin---------" ); int ret = main(argc, argv); ffmpegJniDone(1 ); return ret; } void callJavaMethod (char *ret) { int ss = 0 ; char *q = strstr (ret, "time=" ); if (q != NULL ) { char str[14 ] = {0 }; strncpy (str, q, 13 ); int h = (str[5 ] - '0' ) * 10 + (str[6 ] - '0' ); int m = (str[8 ] - '0' ) * 10 + (str[9 ] - '0' ); int s = (str[11 ] - '0' ) * 10 + (str[12 ] - '0' ); ss = s + m * 60 + h * 60 * 60 ; } else { return ; } if (m_clazz == NULL ) { LOGE("---------------m_clazz isNULL---------------" ); return ; } jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onProgress" , "(I)V" ); if (methodID == NULL ) { LOGE("---------------methodID isNULL---------------" ); return ; } LOGE("---------------Call Method---------------" ); (*m_env)->CallVoidMethod(m_env,m_jobj, methodID, ss); } void ffmpegJniDone (int i) { jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onFinish" , "(I)V" ); (*m_env)->CallVoidMethod(m_env,m_jobj, methodID, i); }
logjni.h是为了将ffmpeg自身的输出变为Android Log输出的一个头文件,内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #ifdef ANDROID #include <android/log.h> #ifndef LOG_TAG #define MY_TAG "MYTAG" #define AV_TAG "AVLOG" #endif #define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__) #define LOGD(format, ...) __android_log_print(ANDROID_LOG_DEBUG, MY_TAG, format, ##__VA_ARGS__) #define XLOGD(...) __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__) #define XLOGE(...) __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__) #else #define LOGE(format, ...) printf(MY_TAG format "\n" , ##__VA_ARGS__) #define LOGD(format, ...) printf(MY_TAG format "\n" , ##__VA_ARGS__) #define XLOGE(format, ...) fprintf(stdout, AV_TAG ": " format "\n" , ##__VA_ARGS__) #define XLOGI(format, ...) fprintf(stderr, AV_TAG ": " format "\n" , ##__VA_ARGS__) #endif
到这一步就可以执行ndk-build命令进行编译了. cmd 进入之前创建的jni文件夹,输入ndk-build (别忘记配置好ndk的环境变量)后,就会自动的进行编译操作,
如依然提示各种缺少文件,可以到之前解压出的ffmpeg文件夹里面去寻找到他们,然后复制到对应的文件夹中 最后,编译完成后,就会多出一个libffmpegjni.so文件 接下来就是Android端进行调用了 创建一个FFmpegJni类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class FFmpegJni { private float totalDecodeTime = 0f ; private ProgressBar progressBar; private String fileInPath; private String fileOutPath; MediaPlayer mediaPlayer = new MediaPlayer (); public FFmpegJni (String inPath, String outPath, ProgressBar progressBar) { fileInPath = inPath; fileOutPath = outPath; this .progressBar = progressBar; } public void init () { try { mediaPlayer.setDataSource(fileInPath); mediaPlayer.prepare(); totalDecodeTime = mediaPlayer.getDuration()/1000 ; Log.d("ffmpeg" , "init: " +totalDecodeTime); }catch (Exception e){ e.printStackTrace(); } } public void decode () { new Thread (new Runnable () { @Override public void run () { String[] cmd = {"ffmpeg" , "-i" ,fileInPath, "-f" , "s16be" ,"-ar" ,"44100" ,"-acodec" ,"pcm_s16le" , fileOutPath}; main(cmd); } }).start(); } public void onProgress (int second) { Log.d("ffmpeg" , "onProgress: " + ((second/this .totalDecodeTime) * 100 )); progressBar.setProgress((int )((second/this .totalDecodeTime) * 100 )); } public native int main (String[] commands) ; static { System.loadLibrary("avutil" ); System.loadLibrary("swresample" ); System.loadLibrary("avcodec" ); System.loadLibrary("avformat" ); System.loadLibrary("swscale" ); System.loadLibrary("avfilter" ); System.loadLibrary("avdevice" ); System.loadLibrary("ffmpegjni" ); } }
然后再mainacitivity中调用即可,FFmpegJni的main函数接收命令.
参考资料