转载地址:http://www.jianshu.com/p/da64059799d5
我们的目标是:
1.mac平台编译FFmpeg生成so动态库。 2.Android通过cmake集成,使用so动态库。
环境
mac os: 10.12.6 ndk: 15.1.4119039 android studio: 2.3.3 cmake: cmake_minimum_required(VERSION 3.4.1)
前置条件:
1:NDK环境配置
#编辑本地配置文件,没有自行创建 vim .bash_profile
顺便把android环境也配置后,后面会用到
#重新加载配置文件 source .bash_profile #验证是否配置正确 ndk-build
出现上述结果说明已经能找到ndk
这个地方点击Sourcecode,要吐槽一下他们的设计,开始以为要点上面的那个按钮,点了半天才反应过来要点下面的字。
解压之后放到一个目录:
这里目录名称可以随便,我统一都放在ffmpeg下
/Users/leon/Desktop/ffmpeg
2:Mac编译脚本编写(坑点颇多)
在目录下新建ffmpeg_build.sh(名称可修改)
此处我用Sublime Text编辑,最好不要用其他平台的文件,比如从window平台复制一份已经写好的脚本文件,可能出现字符编码问题.所以我为了避免麻烦直接在Mac平台重写.
#!/bin/bash # 因为我是在根目录,所以要先进入ffmpeg-3.3.1目录 cd ffmpeg-3.3.1 make clean # NDK的路径,根据自己的安装位置进行设置 export NDK=/Users/leon/develop/android/android-sdk-macosx/ndk-bundle export SYSROOT=$NDK/platforms/android-16/arch-arm/ export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64 export CPU=arm export PREFIX=$(pwd)/android/$CPU export ADDI_CFLAGS="-marm" function build_one { ./configure \ --prefix=$PREFIX \ --target-os=linux \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --arch=arm \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ --cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \ --nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \ --enable-shared \ --enable-runtime-cpudetect \ --enable-gpl \ --enable-small \ --enable-cross-compile \ --disable-debug \ --disable-static \ --disable-doc \ --disable-asm \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-ffserver \ --enable-postproc \ --enable-avdevice \ --disable-symver \ --disable-stripping \ $ADDITIONAL_CONFIGURE_FLAG sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h make clean # 这里是定义用几个CPU编译,我用4个,一般在5分钟之内编译完成 make -j4 make install } build_one
这个文件在编译时会出现各种奇怪问题,其中一个坑是在每行语句后面不能有空格.比如
3.修改configure文件(修改前最好backup一份)
修改如下所示:
大概在3300行,注释前四行掉,然后换成没有注释的#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)'
这里要修改的目的是因为如果不修改生成so文件的名称大概是 xxxx.so.5.60 ,版本号跟在了.so后面,拿到android下无法正确读取,即使手动改变文件名称也不行.
修改后版本号是在.so前面,文件是以.so结尾.*这里进入此目录,执行configure文件生成一些xx.mk配置文件,主要目的是读取本地编译环境信息并记录。
./configure --disable-yasm
4:编译
激动人心的时刻要到了,大部分时候都是要折腾很久才能爽这一下,不好意思,又要开车了。
# 命令行到脚本所在目录 ./ffmpeg_build.sh
此时正确的姿势是这样的
虽然其中会出现这种情况,但是不必管他
第一次折腾好时,一看表已经凌晨2点,说多了都是泪.
编译结束,在FFmpeg-3.3.1目录中会多一个android目录,这里面就是我们要用到的so
在开头我们定的目标一已经完成,来看一下我们目标二.
下面开始使用Android Studio创建App工程,并使用以上我们编译生成的动态库,编写一个简单的jni调用ffmpeg播放本地视频文件
5:创建Android项目
可以先运行一下项目,保证新创建的项目没有问题,这里模拟器不能使用x86架构的,但是arm架构的模拟器又巨慢,我用真机测试
- main文件目录中新建 jniLibs文件件
- 把本地编译的include文件夹和so赋值过来
6:编写CMakeLists
cmake_minimum_required(VERSION 3.4.1) add_library( native-ffmpeg SHARED src/main/cpp/native-ffmpeg.cpp ) find_library( log-lib log ) find_library( android-lib android ) set(distribution_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}) add_library( avutil-55 SHARED IMPORTED ) set_target_properties( avutil-55 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavutil-55.so) add_library( swresample-2 SHARED IMPORTED ) set_target_properties( swresample-2 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libswresample-2.so) add_library( avcodec-57 SHARED IMPORTED ) set_target_properties( avcodec-57 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavcodec-57.so) add_library( avfilter-6 SHARED IMPORTED ) set_target_properties( avfilter-6 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavfilter-6.so) add_library( swscale-4 SHARED IMPORTED ) set_target_properties( swscale-4 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libswscale-4.so) add_library( avdevice-57 SHARED IMPORTED ) set_target_properties( avdevice-57 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavdevice-57.so) add_library( avformat-57 SHARED IMPORTED ) set_target_properties( avformat-57 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libavformat-57.so) add_library( postproc-54 SHARED IMPORTED ) set_target_properties( postproc-54 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libpostproc-54.so) set(CMAKE_VERBOSE_MAKEFILE on) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") include_directories(src/main/cpp) include_directories(src/main/jniLibs/include) target_link_libraries(native-ffmpeg avutil-55 #工具库(大部分需要) swresample-2 #音频采样数据格式转换 avcodec-57 #编解码(重要) avfilter-6 #滤镜特效处理 swscale-4 #视频像素数据格式转换 avdevice-57 #各种设备的输入输出 avformat-57 #封装格式处理 postproc-54 #后加工 ${log-lib} ${android-lib})
这里cmake需要看下官方文档了解下
7:编写JNI文件
1)java控制类:PlayerNative.java
package com.example.leon.ffmpegandroiddemo; /** * Created by leon */ public class PlayerNative { /** * 这里加载有依赖关系 */ static { System.loadLibrary("avutil-55"); System.loadLibrary("swresample-2"); System.loadLibrary("avcodec-57"); System.loadLibrary("avfilter-6"); System.loadLibrary("swscale-4"); System.loadLibrary("avdevice-57"); System.loadLibrary("avformat-57"); System.loadLibrary("postproc-54"); System.loadLibrary("native-ffmpeg"); } /** * 音视频解码播放 * * @param path * @param view */ public native static void paly(String path, Object view); /** * 音视频解码停止 * */ public native static void stop(); }
2)Jni:native-ffmpeg.cpp(可以不用写.h文件)
#include <jni.h> #include <string> #include "native-ffmpeg.h" #include <android/native_window.h> #include <android/native_window_jni.h> #include <unistd.h> #include <android/log.h> #define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"TAG",FORMAT,##__VA_ARGS__); #define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"TAG",FORMAT,##__VA_ARGS__); extern "C" { #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include <libavfilter/avfiltergraph.h> #include "libavfilter/avfilter.h" #include "libavutil/imgutils.h" #include "libavutil/avutil.h" #include "libavfilter/buffersink.h" #include "libavfilter/buffersrc.h" #include "libavcodec/avcodec.h" } extern "C" /** * 播放 */ JNIEXPORT void JNICALL Java_com_example_leon_ffmpegandroiddemo_PlayerNative_paly (JNIEnv *env, jclass cls, jstring path_, jobject view) { LOGE("%s", "play()"); const char *path = env->GetStringUTFChars(path_, 0); //注册所有的编解码器 av_register_all(); int ret; //封装格式上线文 AVFormatContext *fmt_ctx = avformat_alloc_context(); //打开输入流并读取头文件。此时编解码器还没有打开 if (avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0) { return; } //获取信息 if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { return; } //获取视频流的索引位置 int video_stream_index = -1; for (int i = 0; i < fmt_ctx->nb_streams; i++) { if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; LOGE("找到视频流索引位置video_stream_index=%d", video_stream_index); break; } } if (video_stream_index == -1) { LOGE("未找到视频流索引"); } ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, view); if (nativeWindow == NULL) { LOGE("ANativeWindow_fromSurface error"); return; } //绘制时候的缓冲区 ANativeWindow_Buffer outBuffer; //获取视频流解码器 AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL); avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar); AVCodec *avCodec = avcodec_find_decoder(codec_ctx->codec_id); //打开解码器 if ((ret = avcodec_open2(codec_ctx, avCodec, NULL)) < 0) { ret = -3; return; } //循环从文件读取一帧压缩数据 //开始读取视频 int y_size = codec_ctx->width * codec_ctx->height; AVPacket *pkt = (AVPacket *) malloc(sizeof(AVPacket));//分配一个packet av_new_packet(pkt, y_size);//分配packet的数据 AVFrame *yuvFrame = av_frame_alloc(); AVFrame *rgbFrame = av_frame_alloc(); // 颜色转换器 SwsContext *m_swsCtx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGBA, SWS_BICUBIC, NULL, NULL, NULL); LOGE("开始解码"); int index = 0; while (1) { if (av_read_frame(fmt_ctx, pkt) < 0) { //这里就认为视频读完了 break; } if (pkt->stream_index == video_stream_index) { //视频解码 ret = avcodec_send_packet(codec_ctx, pkt); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { LOGE("avcodec_send_packet ret=%d", ret); av_packet_unref(pkt); continue; } //从解码器返回解码输出数据 ret = avcodec_receive_frame(codec_ctx, yuvFrame); if (ret < 0 && ret != AVERROR_EOF) { LOGE("avcodec_receive_frame ret=%d", ret); av_packet_unref(pkt); continue; } //avcodec_decode_video2(codec_ctx,yuvFrame,&got_pictue,&pkt); sws_scale(m_swsCtx, (const uint8_t *const *) yuvFrame->data, yuvFrame->linesize, 0, codec_ctx->height, rgbFrame->data, rgbFrame->linesize); //设置缓冲区的属性 ANativeWindow_setBuffersGeometry(nativeWindow, codec_ctx->width, codec_ctx->height, WINDOW_FORMAT_RGBA_8888); ret = ANativeWindow_lock(nativeWindow, &outBuffer, NULL); if (ret != 0) { LOGE("ANativeWindow_lock error"); return; } av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, (const uint8_t *) outBuffer.bits, AV_PIX_FMT_RGBA, codec_ctx->width, codec_ctx->height, 1); //fill_ANativeWindow(&outBuffer,outBuffer.bits,rgbFrame); //将缓冲区数据显示到surfaceView ret = ANativeWindow_unlockAndPost(nativeWindow); if (ret != 0) { LOGE("ANativeWindow_unlockAndPost error"); return; } LOGE("成功显示到缓冲区%d次", ++index); } av_packet_unref(pkt); usleep(1000 * 8); } av_frame_free(&rgbFrame); avcodec_close(codec_ctx); sws_freeContext(m_swsCtx); avformat_close_input(&fmt_ctx); ANativeWindow_release(nativeWindow); env->ReleaseStringUTFChars(path_, path); LOGI("解析完成"); } /** * 暂停 */ JNIEXPORT void JNICALL Java_com_example_leon_ffmpegandroiddemo_PlayerNative_stop (JNIEnv *env, jclass cls) { LOGE("%s", "stop()"); }
3)其他文件
MainActivity.javapackage com.example.leon.ffmpegandroiddemo; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import java.io.File; public class MainActivity extends AppCompatActivity { private VideoView mVideo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mVideo = (VideoView) findViewById(R.id.video); } public void paly(View view) { final String dir = Environment.getExternalStorageDirectory() + File.separator + "Download"; final String path = dir + File.separator + "1.mp4"; final VideoView mVideo = (VideoView) findViewById(R.id.video); if (new File(path).exists()) { runOnUiThread(new Runnable() { @Override public void run() { PlayerNative.paly(path, mVideo.getHolder().getSurface()); } }); } else { System.out.println("文件不存在"); } } public void stop(View view) { PlayerNative.stop(); } }
VideoView.java
package com.example.leon.ffmpegandroiddemo; import android.content.Context; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * * Created by leon */ public class VideoView extends SurfaceView { public VideoView(Context context) { super(context); init(); } public VideoView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { SurfaceHolder holder = getHolder(); holder.setFormat(PixelFormat.RGBA_8888); } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="paly" android:text="开始" /> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="stop" android:text="结束" /> </LinearLayout> <com.example.leon.ffmpegandroiddemo.VideoView android:id="@+id/video" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="10dp" /> </LinearLayout>
到此我们定的两个目标已经完成,以后会再继续写FFmpeg使用教程。
最后总结一下各种问题:
1:cannot locate symbol "atof" referenced by "libavformat-57.so"...的问题原因:Android的stdlib.h中atof是内联的
解决方法:将所有的atof改成strtod
示例代码:
char *strpi = "3.1415";
double dpi;
dpi = atof(strpi); 修改为: dpi = strtod(strpi, NULL);或者脚本文件中定义platforms版本使用低版本,比如
export SYSROOT=$NDK/platforms/android-16/arch-arm/2.cannot locate symbol "log2f" referenced by "libavcodec-57.so"..
原因: 这个跟ndk与android版本有关。如果使用高版本ndk编译,so会判断系统版本并使用系统log,但实际手机系统版本较低,并没有此方法,所以会报找不到此方法
解决办法:
1),使用platforms 低版本
2),修改 ./libavutil/libm.h里面的定义,不再判断是否已经存在函数。使用重新定义//#if !HAVE_LOG2 //#undef log2 #define log2(x) (log(x) * 1.44269504088896340736) //#endif /* HAVE_LOG2 */ //#if !HAVE_LOG2F //#undef log2f #define log2f(x) ((float)log2(x)) //#endif /* HAVE_LOG2F */