【AndroidStudio 中使用FFMPEG】冲天香阵透长安,满城尽带黄金甲。这篇文章主要讲述AndroidStudio 中使用FFMPEG相关的知识,希望能为你提供帮助。
1.下载 FFmpeg 源码
git clone https://git.ffmpeg.org/ffmpeg.git
这一步可能会花比较长的时间
2.编译 FFmpeg for android 2.1.修改 FFmpeg 的 configure由于FFMPEG默认编译出来的动态库文件名的版本号在.so之后(例如“libavcodec.so.5.100.1”),但是android平台不能识别这样文件名,所以我们需要修改FFMPEG生成的动态库的文件名。
打开 configure 文件,找到:
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)\'
修改为
SLIBNAME_WITH_MAJOR=\'$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)\' LIB_INSTALL_EXTRA_CMD=\'$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"\' SLIB_INSTALL_NAME=\'$(SLIBNAME_WITH_MAJOR)\' SLIB_INSTALL_LINKS=\'$(SLIBNAME)\'
2.2.编写 Android 编译脚本
#!/bin/sh NDK=/home/cent/Android/Sdk/ndk-bundle SYSROOT=$NDK/platforms/android-19/arch-arm TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64 build_android() { ./configure \\ --prefix=$PREFIX \\ --enable-shared \\ --disable-static \\ --disable-doc \\ --disable-ffmpeg \\ --disable-ffplay \\ --disable-ffprobe \\ --disable-ffserver \\ --disable-avdevice \\ --disable-doc \\ --disable-symver \\ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \\ --target-os=linux \\ --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 } CPU=arm PREFIX=$(pwd)/android/$CPU ADDI_CFLAGS="-marm" build_android
2.3.编译 执行上面的脚本编译出我们需要的动态库
./build_android.sh
进入android/$CPU目录可以看到生成的动态库和我们需要的头文件
. └── arm ├── include │ ├── libavcodec │ ├── libavfilter │ ├── libavformat │ ├── libavutil │ ├── libswresample │ └── libswscale └── lib ├── libavcodec-57.so ├── libavcodec.so -> libavcodec-57.so ├── libavfilter-6.so ├── libavfilter.so -> libavfilter-6.so ├── libavformat-57.so ├── libavformat.so -> libavformat-57.so ├── libavutil-55.so ├── libavutil.so -> libavutil-55.so ├── libswresample-2.so ├── libswresample.so -> libswresample-2.so ├── libswscale-4.so ├── libswscale.so -> libswscale-4.so └── pkgconfig
3.将上一步生成的头文件和库文件导入到Android Studio工程中首先新建一个工程,并且勾选 Include C++ Support 即可得到一个基于CMake的模板工程。目录结构如下所示
. ├── app │ ├── app.iml │ ├── build │ │ ├── generated │ │ │ ├── res │ │ │ └── source │ │ ├── intermediates │ │ │ ├── blame │ │ │ ├── incremental │ │ │ ├── manifest │ │ │ ├── manifests │ │ │ ├── res │ │ │ ├── rs │ │ │ └── symbols │ │ └── outputs │ │ └── logs │ ├── build.gradle │ ├── CMakeLists.txt │ ├── CMakeLists.txt~ │ ├── libs │ │ ├── armeabi │ │ │ ├── libavcodec-57.so │ │ │ ├── libavfilter-6.so │ │ │ ├── libavformat-57.so │ │ │ ├── libavutil-55.so │ │ │ ├── libswresample-2.so │ │ │ └── libswscale-4.so │ │ └── include │ │ ├── libavcodec │ │ ├── libavfilter │ │ ├── libavformat │ │ ├── libavutil │ │ ├── libswresample │ │ └── libswscale │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ │ └── java │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── cpp │ │ ├── java │ │ └── res │ └── test │ └── java ├── build │ ├── android-profile │ │ └── profile-2017-03-31-23-04-31-347.rawproto │ └── generated │ └── mockable-android-25.jar ├── build.gradle ├── FFMPEGTest.iml ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle
然后将上面编译FFMPEG生成的头文件和动态库拷贝到app/libs目录下,拷贝完后的目录结构如下所示
├── app │├── libs ││├── armeabi │││├── libavcodec-57.so │││├── libavfilter-6.so │││├── libavformat-57.so │││├── libavutil-55.so │││├── libswresample-2.so │││└── libswscale-4.so ││└── include ││├── libavcodec ││├── libavfilter ││├── libavformat ││├── libavutil ││├── libswresample ││└── libswscale │├── proguard-rules.pro │└── src
这样还没完,我当时就是这样直接去编译,然后就踩了一个大坑,APP启动之后一直crash,原因就是没有找到我们在java文件里load的动态库。为什么呢?原因是在编译的时候,我们根本没有将我们的动态库打包到APP中,我们还需要修改app/build.gradle将我们放在libs目录下的动态库打包到APP中去
apply plugin: \'com.android.application\'android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "com.example.cent.ffmpegtest" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" sourceSets { main { jniLibs.srcDirs = [\'libs\'] } } externalNativeBuild { cmake { cppFlags "-frtti -fexceptions" } ndk{ abiFilters "armeabi" } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(\'proguard-android.txt\'), \'proguard-rules.pro\' } }externalNativeBuild { cmake { path "CMakeLists.txt" } } }dependencies { compile fileTree(dir: \'libs\', include: [\'*.jar\']) androidTestCompile(\'com.android.support.test.espresso:espresso-core:2.2.2\', { exclude group: \'com.android.support\', module: \'support-annotations\' }) compile \'com.android.support.constraint:constraint-layout:1.0.1\' testCompile \'junit:junit:4.12\' }
紧接着我们还要指定abiFilters,因为AndroidStudio默认会编译所有架构的动态库,但是在本次例子中,我们实际上只拷贝了
├── libs ││├── armeabi
架构(目录名)的动态库,所以我们需要指定一个abiFilters来过滤一下,否则会出现编译错误。
externalNativeBuild { cmake { cppFlags "-frtti -fexceptions" } ndk{ abiFilters "armeabi" } } }
紧接着就是来编写我们的CMakeLists.txt文件来编译我们的动态库和native源文件了
cmake_minimum_required(VERSION 3.4.1)find_library( log-lib log )set(distribution_DIR ../../../../libs)add_library( native-lib SHARED src/main/cpp/native-lib.cpp )add_library( avcodec-57 SHARED IMPORTED) set_target_properties( avcodec-57 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libavcodec-57.so)add_library( avfilter-6 SHARED IMPORTED) set_target_properties( avfilter-6 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libavfilter-6.so)add_library( avformat-57 SHARED IMPORTED) set_target_properties( avformat-57 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libavformat-57.so)add_library( avutil-55 SHARED IMPORTED) set_target_properties( avutil-55 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libavutil-55.so)add_library( swresample-2 SHARED IMPORTED) set_target_properties( swresample-2 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libswresample-2.so)add_library( swscale-4 SHARED IMPORTED) set_target_properties( swscale-4 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/armeabi/libswscale-4.so)include_directories(libs/include)target_link_libraries( native-lib avcodec-57 avfilter-6 avformat-57 avutil-55 swresample-2 swscale-4 ${log-lib} )
这样基本上就大功告成了。
4.使用FFMPEG下面我们将通过一个小例子来看一下怎样使用FFMPEG。使用FFMPEG进行视频解码(音频和视频很相似)的一般流程如下图所示
文章图片
首先需要在JAVA文件中加载我们需要的动态库
//MainActivity.java public class MainActivity extends Activity {// Used to load the \'native-lib\' library on application startup. static { System.loadLibrary("native-lib"); System.loadLibrary("avcodec-57"); System.loadLibrary("avfilter-6"); System.loadLibrary("avformat-57"); System.loadLibrary("avutil-55"); System.loadLibrary("swresample-2"); System.loadLibrary("swscale-4"); }@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = (TextView) findViewById(R.id.sample_text); tv.setText(stringFromJNI()); String input = new File(Environment.getExternalStorageDirectory(),"input.mp4").getAbsolutePath(); String output = new File(Environment.getExternalStorageDirectory(),"output_yuv420p.yuv").getAbsolutePath(); decode(input, output); }/** * A native method that is implemented by the \'native-lib\' native library, * which is packaged with this application. */ public native String stringFromJNI(); public native static void decode(String input,String output); }
然后在native代码中实现主要逻辑
//native-lib.cpp #include < jni.h> #include < string> #include < android/log.h> extern "C" { //编码 #include "libavcodec/avcodec.h" //封装格式处理 #include "libavformat/avformat.h" //像素处理 #include "libswscale/swscale.h" }#define FFLOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"ffmpeg",FORMAT,##__VA_ARGS__); #define FFLOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"ffmpeg",FORMAT,##__VA_ARGS__); extern "C" JNIEXPORT jstring JNICALL Java_com_example_cent_ffmpegtest_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env-> NewStringUTF(hello.c_str()); }extern "C" JNIEXPORT void JNICALL Java_com_example_cent_ffmpegtest_MainActivity_decode(JNIEnv *env, jclass type, jstring input_, jstring output_) { //获取输入输出文件名 const char *input = env-> GetStringUTFChars(input_, 0); const char *output = env-> GetStringUTFChars(output_, 0); //1.注册所有组件 av_register_all(); //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息 AVFormatContext *pFormatCtx = avformat_alloc_context(); //2.打开输入视频文件 if (avformat_open_input(& pFormatCtx, input, NULL, NULL) != 0) { FFLOGE("%s","无法打开输入视频文件"); return; }//3.获取视频文件信息 if (avformat_find_stream_info(pFormatCtx,NULL) < 0) { FFLOGE("%s","无法获取视频文件信息"); return; }//获取视频流的索引位置 //遍历所有类型的流(音频流、视频流、字幕流),找到视频流 int v_stream_idx = -1; int i = 0; //number of streams for (; i < pFormatCtx-> nb_streams; i++) { //流的类型 if (pFormatCtx-> streams[i]-> codec-> codec_type == AVMEDIA_TYPE_VIDEO) { v_stream_idx = i; break; } }if (v_stream_idx == -1) { FFLOGE("%s","找不到视频流\\n"); return; }//只有知道视频的编码方式,才能够根据编码方式去找到解码器 //获取视频流中的编解码上下文 AVCodecContext *pCodecCtx = pFormatCtx-> streams[v_stream_idx]-> codec; //4.根据编解码上下文中的编码id查找对应的解码 AVCodec *pCodec = avcodec_find_decoder(pCodecCtx-> codec_id); if (pCodec == NULL) { FFLOGE("%s","找不到解码器\\n"); return; }//5.打开解码器 if (avcodec_open2(pCodecCtx,pCodec,NULL)< 0) { FFLOGE("%s","解码器无法打开\\n"); return; }//输出视频信息 FFLOGI("视频的文件格式:%s",pFormatCtx-> iformat-> name); FFLOGI("视频时长:%d", (pFormatCtx-> duration)/1000000); FFLOGI("视频的宽高:%d,%d",pCodecCtx-> width,pCodecCtx-> height); FFLOGI("解码器的名称:%s",pCodec-> name); //准备读取 //AVPacket用于存储一帧一帧的压缩数据(H264) //缓冲区,开辟空间 AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket)); //AVFrame用于存储解码后的像素数据(YUV) //内存分配 AVFrame *pFrame = av_frame_alloc(); //YUV420 AVFrame *pFrameYUV = av_frame_alloc(); //只有指定了AVFrame的像素格式、画面大小才能真正分配内存 //缓冲区分配内存 uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx-> width, pCodecCtx-> height)); //初始化缓冲区 avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx-> width, pCodecCtx-> height); //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等 struct SwsContext *sws_ctx = sws_getContext(pCodecCtx-> width,pCodecCtx-> height,pCodecCtx-> pix_fmt, pCodecCtx-> width, pCodecCtx-> height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); int got_picture, ret; FILE *fp_yuv = fopen(output, "wb+"); int frame_count = 0; //6.一帧一帧的读取压缩数据 while (av_read_frame(pFormatCtx, packet) > = 0) { //只要视频压缩数据(根据流的索引位置判断) if (packet-> stream_index == v_stream_idx) { //7.解码一帧视频压缩数据,得到视频像素数据 ret = avcodec_decode_video2(pCodecCtx, pFrame, & got_picture, packet); if (ret < 0) { FFLOGE("%s","解码错误"); return; }//为0说明解码完成,非0正在解码 if (got_picture) { //AVFrame转为像素格式YUV420,宽高 //2 6输入、输出数据 //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的 //4 输入数据第一列要转码的位置 从0开始 //5 输入画面的高度 sws_scale(sws_ctx, pFrame-> data, pFrame-> linesize, 0, pCodecCtx-> height, pFrameYUV-> data, pFrameYUV-> linesize); //输出到YUV文件 //AVFrame像素帧写入文件 //data解码后的图像像素数据(音频采样数据) //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感 //U V 个数是Y的1/4 int y_size = pCodecCtx-> width * pCodecCtx-> height; fwrite(pFrameYUV-> data[0], 1, y_size, fp_yuv); fwrite(pFrameYUV-> data[1], 1, y_size / 4, fp_yuv); fwrite(pFrameYUV-> data[2], 1, y_size / 4, fp_yuv); frame_count++; FFLOGI("解码第%d帧",frame_count); } }//释放资源 av_free_packet(packet); }fclose(fp_yuv); av_frame_free(& pFrame); avcodec_close(pCodecCtx); avformat_free_context(pFormatCtx); env-> ReleaseStringUTFChars(input_, input); env-> ReleaseStringUTFChars(output_, output); }
记得在Manifest文件中添加需要的权限
< uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> < uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
最后简单了解一下FFMPEG中使用的几个主要数据结构的作用
文章图片
推荐阅读
- 踩坑实录Android studio中关于 No cached version of **** available for of处理办法
- 安装mysql时,提示This application requires .NET framework 4.0问题
- Codeforces 348B(Apple Tree(DFS+LCM+思维))
- 简记Ubuntu下载 Android源码
- 华为 Mate8 Emui 5.0 安卓 7.0 root 记录
- 常用 Android 图片处理框架的比较
- 灰色的博弈(那些靠“方便”赚钱的APP们)
- Appium 002--环境安装(安装Android模拟器)
- Appium 001--环境安装(安装Appium)