.移植ffmpeg到android
-----------------------另一个方法
搞左2日,找了好多资料,终于把ffmpeg移植到android上了。其中参考了一篇介绍移植方法的doc文档,里面是用windows+Cygwin+ndk(windows版本)作为编译环境的,我这里直接用ubuntu了,省了很多配置了^_^。感谢前辈的无私奉献。。。
-->环境:
操作系统:Ubuntu 9.10
ffmpeg源码版本:ffmpeg-0.6.1(可以在http://ffmpeg.org/download.html下载源码)
android ndk版本:android-ndk-r4b-linux-x86(可以在http://androidappdocs.appspot.com/sdk/ndk/index.html下载)
-->设置NDK环境变量:
1、用root用户登陆,打开命令窗口,输入cd /root
2、输入ls -a,会显示一个隐藏文件.bashrc
3、输入vi .bashrc,按i进入编辑模式
4、文件底部添加以下两行:
NDK_ROOT=/home/ndk。这里指你ndk的目录路径
export NDK_ROOT
5、按esc退出,再按:wq保存
你可以测试一下有没有安装成功:
$ cd $NDK_ROOT
$ ./ndk-build NDK_PROJECT_PATH=$NDK_ROOT/samples/two-libs
找到生成的文件就代表安装正常了。
1、如果提示某个文件"Permission denied"之类的信息时,执行chmod 777your_filename就行了
2、如果提示找不到'cc1'等信息,执行chmod -R 777 *,就能解决了
-->配置源代码
在/home/ndk/samples/创建一个FFMPEG文件夹,在里面再新建一个jni文件夹,然后把ffmpeg源码放在jni里面,所以最后ffmpeg源码的路径是:
/home/ndk/samples/FFMPEG/jni/ffmpeg
1.
#!/bin/bash
PREBUILT=/home/ndk/build/prebuilt/linux/arm-eabi-4.4.0
PLATFORM=/home/ndk/build/platforms/android-3/arch-arm
./configure --target-os=linux /
记得PREBUILT和PLATFORM要设置正确,它在你的NDK里面,你可以按自己的填写
2.
$ chmod +xconfig.sh
$ ./config.sh
如果配置正确的话显示出来的最后两行是这样的:
License: nonfree and unredistributable
Creating config.mak and config.h...
3、Configure完成后接下做NDK编译的条件了。
Android的GCC是不支持restrict关键字的,所以把ffmpeg源码下configure生成的config.h文件中的这一行:
#define restrict restrict
如果重新Configure的话记得要把这个关键字去掉。
编辑libavutil/libm.h把其中的static的方法都删除(如果找不到libavutil/libm.h,就表明你的ffmpeg版本不是0.6而是0.5的。)
分别把libavutil、libavcodec、libavformat、libavfilter、libpostproct和libswscale下的Makefile文件中下面两行删除掉:
include $(SUBDIR)../subdir.mak
include $(SUBDIR)../config.mak
然后在ffmpeg源文件夹下新建一个av.mk文件,内容如下:(这些不用替换回车)
# LOCAL_PATH is one of libavutil, libavcodec, libavformat, orlibswscale
#include $(LOCAL_PATH)/../config-$(TARGET_ARCH).mak
include $(LOCAL_PATH)/../config.mak
OBJS :=
OBJS-yes :=
MMX-OBJS-yes :=
include $(LOCAL_PATH)/Makefile
# collect objects
OBJS-$(HAVE_MMX) += $(MMX-OBJS-yes)
OBJS += $(OBJS-yes)
FFNAME := lib$(NAME)
FFLIBS := $(foreach,NAME,$(FFLIBS),lib$(NAME))
FFCFLAGS
FFCFLAGS += -DTARGET_CONFIG=/"config-$(TARGET_ARCH).h/"
ALL_S_FILES := $(wildcard $(LOCAL_PATH)/$(TARGET_ARCH)
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
include $(LOCAL_PATH)/../av.mk
LOCAL_SRC_FILES := $(FFFILES)
LOCAL_C_INCLUDES :=
LOCAL_CFLAGS += $(FFCFLAGS)
#LOCAL_SHARED_LIBRARIES :=(这行根据下面蓝色的来填写相应的lib)
LOCAL_MODULE := $(FFNAME)
include $(BUILD_SHARED_LIBRARY)
----------------------------------------
ffmpeg/libavutil:不需要这一行,直接加#屏蔽掉。
ffmpeg/libswscale:LOCAL_SHARED_LIBRARIES :=libavutil
ffmpeg/libavfilter:LOCAL_SHARED_LIBRARIES :=libavutil libswscalelibavcodec
ffmpeg/libpostproc:LOCAL_SHARED_LIBRARIES :=libavutil
可能编译libavdevice也是用上面这个Android.mk,然后根据提示的错误没改变红色这行就OK了。
最后就是运行ndk-build了:
$ cd $NDK_ROOT
$ ./ndk-build NDK_PROJECT_PATH=$NDK_ROOT/samples/FFMPEG
最后生成libavcodec.so、libavutil.so、libavformat.so、libavfilter.so、libswscale.so和libpostproc.so以及libffmpeg.so,你会发现libffmpeg.so只有2K(1599个字节),其实这个文件是没有加载任何东西而编译成的空SO文件,没有任何函数的SO文件。所以不能用,但其它的都可以。估计是没有LOCAL_SRC_FILES的原因。
另外一篇不错的参考文章:http://www.eoeandroid.com/thread-42184-1-1.html,它生成的是静态库
-
7楼
abc526541845 2011-05-06 14:29发表 [回复] [引用] [举报]
-
关注
最近也要做这方面的工作
-
6楼
HongMenzhouxiangang 2011-04-1414:28发表 [回复] [引用] [举报]
- 我也编译成功了。不过不是用root用户。
-
-
Re:
shenchao8528 2011-04-21 18:37发表 [回复] [引用] [举报]
- 回复 HongMenzhouxiangang:你编译成功以后,是不是有两个libffmpeg.so?
-
5楼
hj475955463 2011-03-23 17:34发表 [回复] [引用] [举报]
- 楼主您好,现在我已经把ffmpeg编译成功了,但是不知道怎么在androidNDK下使用,网上的例子也都没说明白,您能提供一个一个可以使用的例子么?万分感谢,可以发我邮箱hj2060@126.com
用NDK编译FFmpeg [复制链接]
android内置的编解码器实在太少,于是我们需要FFmpeg。Android提供了NDK,为我们使用FFmpeg这种C语言代码提供了方便。
首先创建一个标准的Android项目vPlayer
- android create project -n vPlayer -t 8 -p vPlayer -kme.abitno.vplayer -a PlayerView
- mkdir jni && cd jni
- wget http://ffmpeg.org/releases/ffmpeg-0.6.tar.bz2
- tar xf ffmpeg-0.6.tar.bz2 && mvffmpeg-0.6 ffmpeg && cd ffmpeg
- #!/bin/bash
- PREBUILT=/home/abitno/Android/android-ndk-r4/build/prebuilt/linux-x86/arm-eabi-4.4.0
- PLATFORM=/home/abitno/Android/android-ndk-r4/build/platforms/android-8/arch-arm
- ./configure --target-os=linux \
-
--arch=arm \
-
--enable-version3 \
-
--enable-gpl \
-
--enable-nonfree \
-
--disable-stripping \
-
--disable-ffmpeg \
-
--disable-ffplay \
-
--disable-ffserver \
-
--disable-ffprobe \
-
--disable-encoders \
-
--disable-muxers \
-
--disable-devices \
-
--disable-protocols \
-
--enable-protocol=file \
-
--enable-avfilter \
-
--disable-network \
-
--disable-mpegaudio-hp \
-
--disable-avdevice \
-
--enable-cross-compile \
-
--cc=$PREBUILT/bin/arm-eabi-gcc \
-
--cross-prefix=$PREBUILT/bin/arm-eabi-\
-
--nm=$PREBUILT/bin/arm-eabi-nm \
-
--extra-cflags="-fPIC -DANDROID" \
-
--disable-asm \
-
--enable-neon \
-
--enable-armv5te \
-
--extra-ldflags="-Wl,-T,$PREBUILT/arm-eabi/lib/ldscripts/armelf.x-Wl,-rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -nostdlib$PREBUILT/lib/gcc/arm-eabi/4.4.0/crtbegin.o$PREBUILT/lib/gcc/arm-eabi/4.4.0/crtend.o -lc -lm -ldl"
- chmod +x config.sh
- ./config.sh
- #define restrict restrict
- #define restrict
分别修改libavcodec、libavfilter、libavformat、libavutil、libpostproc和libswscale下的Makefile,把下面两句删除
- include $(SUBDIR)../subdir.mak
- include $(SUBDIR)../config.mak
- # LOCAL_PATH is one of libavutil, libavcodec, libavformat, orlibswscale
- #include $(LOCAL_PATH)/../config-$(TARGET_ARCH).mak
- include $(LOCAL_PATH)/../config.mak
- OBJS :=
- OBJS-yes :=
- MMX-OBJS-yes :=
- include $(LOCAL_PATH)/Makefile
- # collect objects
- OBJS-$(HAVE_MMX) += $(MMX-OBJS-yes)
- OBJS += $(OBJS-yes)
- FFNAME := lib$(NAME)
- FFLIBS := $(foreach,NAME,$(FFLIBS),lib$(NAME))
- FFCFLAGS
=-DHAVE_AV_CONFIG_H -Wno-sign-compare -Wno-switch-Wno-pointer-sign
- FFCFLAGS +=-DTARGET_CONFIG=\"config-$(TARGET_ARCH).h\"
- ALL_S_FILES := $(wildcard$(LOCAL_PATH)/$(TARGET_ARCH)
- JNIEXPORT
void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello -
(JNIEnv *, jobject, jstring);
仔细观察一下这个方法,在注释上标注类名、方法名、签名(Signature),至于这个签名是做什么用的,我们以后再说。在这里最重要的是Java_com_chnic_jni_SayHellotoCPP_sayHello这个方法签名。在Java端我们执行sayHello(Stringname)这个方法之后,JVM就会帮我们唤醒在DLL里的Java_com_chnic_jni_SayHellotoCPP_sayHello这个方法。因此我们新建一个C++ source file来实现这个方法。
- #include
<iostream.h> - #include
"com_chnic_jni_SayHellotoCPP.h" -
-
- JNIEXPORT
void JNICALL Java_com_chnic_jni_SayHellotoCPP_sayHello -
(JNIEnv* env, jobject obj, jstring name) - {
-
const char* pname = env->GetStringUTFChars(name, NULL); -
cout << "Hello, " << pname << endl; - }
因为我们生成的那个头文件是在C++工程的根目录不是在环境目录,所以我们要把尖括号改成单引号,至于VC++的环境目录可以在Tools->Options->Directories里设置。F7编译工程发现缺少jni.h这个头文件。这个头文件可以在%JAVA_HOME%\include目录下找到。把这个文件拷贝到C++工程目录,继续编译发现还是找不到。原来是因为在我们刚刚生成的那个头文件里,jni.h这个文件是被 #include<jni.h>引用进来的,因此我们把尖括号改成双引号#include"jni.h",继续编译发现少了jni_md.h文件,接着在%JAVA_HOME%\include\win32下面找到那个头文件,放入到工程根目录,F7编译成功。在Debug目录里会发现生成了HelloEnd.dll这个文件。
这个时候后端的C++代码也已经完成,接下来的任务就是怎么把他们连接在一起了,要让前端的java程序“认识并找到”这个动态链接库,就必须把这个DLL放在windowspath环境变量下面。有两种方法可以做到:
- 把这个DLL放到windows下面的sysytem32文件夹下面,这个是windows默认的path
- 复制你工程的Debug目录,我这里是C:\Program Files\Microsoft VisualStudio\MyProjects\HelloEnd\Debug这个目录,把这个目录配置到Uservariable的Path下面。重启eclipse,让eclipse在启动的时候重新读取这个path变量。
比较起来,第二种方法比较灵活,在开发的时候不用来回copydll文件了,节省了很多工作量,所以在开发的时候推荐用第二种方法。在这里我们使用的也是第二种,eclipse重启之后打开SayHellotoCPP这个类。其实我们上面做的那些是不是是让JVM能找到那些DLL文件,接下来我们要让我们自己的java代码“认识”这个动态链接库。加入System.loadLibrary("HelloEnd");这句到静态初始化块里。
- package
com.chnic.jni; -
- public
class SayHellotoCPP { -
-
static{ -
System.loadLibrary("HelloEnd"); -
} -
public SayHellotoCPP(){ -
} -
public native void sayHello(String name); -
- }
这样我们的代码就能认识并加载这个动态链接库文件了。万事俱备,只欠测试代码了,接下来编写测试代码。
- SayHellotoCPP
shp = new SayHellotoCPP(); - shp.sayHello("World");
我们不让他直接Hello,World。我们把World传进去,执行代码。发现控制台打印出来Hello,World这句话。就此一个最简单的JNI程序已经开发完成。也许有朋友会对CPP代码里的
- const
char* pname = env->GetStringUTFChars(name, NULL);
- 因为JNI有一个Native这个特点,一点有项目用了JNI,也就说明这个项目基本不能跨平台了。
- JNI调用是相当慢的,在实际使用的之前一定要先想明白是否有这个必要。
- 因为C++和C这样的语言非常灵活,一不小心就容易出错,比如我刚刚的代码就没有写析构字符串释放内存,对于javadeveloper来说因为有了GC垃圾回收机制,所以大多数人没有写析构函数这样的概念。所以JNI也会增加程序中的风险,增大程序的不稳定性。
其实在Java代码中,除了对本地方法标注native关键字和加上要加载动态链接库之外, JNI基本上是对上层coder透明的,上层coder调用那些本地方法的时候并不知道这个方法的方法体究竟是在哪里,这个道理就像我们用JDK所提供的API一样。所以在Java中使用 JNI还是很简单的,相比之下在C++中调用java,就比前者要复杂的多了。
现在来介绍下JNI里的数据类型。在C++里,编译器会很据所处的平台来为一些基本的数据类型来分配长度,因此也就造成了平台不一致性,而这个问题在Java中则不存在,因为有JVM的缘故,所以Java中的基本数据类型在所有平台下得到的都是相同的长度,比如int的宽度永远都是32位。基于这方面的原因,java和c++的基本数据类型就需要实现一些mapping,保持一致性。下面的表可以概括:
| | |
int | long | jint |
long | _int64 | jlong |
byte | signed char | jbyte |
boolean | unsigned char | jboolean |
char | unsigned short | jchar |
short | short | jshort |
float | float | jfloat |
double | double | jdouble |
Object | _jobject* | jobject |
上面的表格是我在网上搜的,放上来给大家对比一下。对于每一种映射的数据类型,JNI的设计者其实已经帮我们取好了相应的别名以方便记忆。如果想了解一些更加细致的信息,可以去看一些jni.h这个头文件,各种数据类型的定义以及别名就被定义在这个文件中。
了解了JNI中的数据类型,下面就来看这次的例子。这次我们用Java来实现一个前端的market(以下就用Foreground代替)用CPP来实现一个后端factory(以下用backend代替)。我们首先还是来编写包含本地方法的java类。
- package
com.chnic.service; -
- import
com.chnic.bean.Order; -
- public
class Business { -
static{ -
System.loadLibrary("FruitFactory"); -
} -
-
public Business(){ -
-
} -
-
public native double getPrice(String name); -
public native Order getOrder(String name, int amount); -
public native Order getRamdomOrder(); -
public native void analyzeOrder(Order order); -
-
public void notification(){ -
System.out.println("Got a notification."); -
} -
-
public static void notificationByStatic(){ -
System.out.println("Got a notification in a static method."); -
} - }
这个类里面包含4个本地方法,一个静态初始化块加载将要生成的dll文件。剩下的方法都是很普通的java方法,等会在backend中回调这些方法。这个类需要一个名为Order的JavaBean。
- package
com.chnic.bean; -
- public
class Order { -
-
private String name = "Fruit"; -
private double price; -
private int amount = 30; -
-
public Order(){ -
-
} -
-
public int getAmount() { -
return amount; -
} -
-
public void setAmount(int amount) { -
this.amount = amount; -
} -
-
public String getName() { -
return name; -
} -
-
public void setName(String name) { -
this.name = name; -
} -
-
public double getPrice() { -
return price; -
} -
-
public void setPrice(double price) { -
this.price = price; -
} - }
JavaBean中,我们为两个私有属性赋值,方便后面的例子演示。到此为止除了测试代码之外的Java端的代码就全部高调了,接下来进行生成.h头文件、建立C++工程的工作,在这里就一笔带过,不熟悉的朋友请回头看第一篇。在工程里我们新建一个名为Foctory的C++source file 文件,去实现那些native方法。具体的代码如下。
- #include
<iostream.h> - #include
<string.h> - #include
"com_chnic_service_Business.h" -
- jobject
getInstance(JNIEnv* env, jclass obj_class); -
- JNIEXPORT
jdouble JNICALL Java_com_chnic_service_Business_getPrice(JNIEnv* env, -
jobject obj, -
jstring name) - {
-
const char* pname = env->GetStringUTFChars(name, NULL); -
cout << "Before release: " << pname << endl; -
-
if (strcmp(pname, "Apple") == 0) -
{ -
env->ReleaseStringUTFChars(name, pname); -
cout << "After release: " << pname << endl; -
return 1.2; -
} -
else -
{ -
env->ReleaseStringUTFChars(name, pname); -
cout << "After release: " << pname << endl; -
return 2.1; -
} - }
-
-
- JNIEXPORT
jobject JNICALL Java_com_chnic_service_Business_getOrder(JNIEnv* env, -
jobject obj, -
jstring name, -
jint amount) - {
-
jclass order_class = env->FindClass("com/chnic/bean/Order"); -
jobject order = getInstance(env, order_class); -
-
jmethodID setName_method = env->GetMethodID(order_class, "setName", "(Ljava/lang/String;)V"); -
env->CallVoidMethod(order, setName_method, name); -
-
jmethodID setAmount_method = env->GetMethodID(order_class, "setAmount", "(I)V"); -
env->CallVoidMethod(order, setAmount_method, amount); -
-
return order; - }
-
- JNIEXPORT
jobject JNICALL Java_com_chnic_service_Business_getRamdomOrder(JNIEnv* env, -
jobject obj) - {
-
jclass business_class = env->GetObjectClass(obj); -
jobject business_obj = getInstance(env, business_class); -
-
jmethodID notification_method = env->GetMethodID(business_class, "notification", "()V"); -
env->CallVoidMethod(obj, notification_method); -
-
jclass order_class = env->FindClass("com/chnic/bean/Order"); -
jobject order = getInstance(env, order_class); -
jfieldID amount_field = env->GetFieldID(order_class, "amount", "I"); -
jint amount = env->GetIntField(order, amount_field); -
cout << "amount: " << amount << endl; -
return order; - }
-
-
- JNIEXPORT
void JNICALL Java_com_chnic_service_Business_analyzeOrder (JNIEnv* env, -
jclass cls, -
jobject obj) - {
-
jclass order_class = env->GetObjectClass(obj); -
jmethodID getName_method = env->GetMethodID(order_class, "getName", "()Ljava/lang/String;"); -
jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method)); -
const char* pname = env->GetStringUTFChars(name_str, NULL); -
-
cout << "Name in Java_com_chnic_service_Business_analyzeOrder: " << pname << endl; -
jmethodID notification_method_static = env->GetStaticMethodID(cls, "notificationByStatic", "()V"); -
env->CallStaticVoidMethod(cls, notification_method_static); -
- }
-
- jobject
getInstance(JNIEnv* env, jclass obj_class) - {
-
jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V"); -
jobject obj = env->NewObject(obj_class, construction_id); -
return obj; - }
可以看到,在我Java中的四个本地方法在这里全部被实现,接下来针对这四个方法来解释下,一些JNI相关的API的使用方法。先从第一个方法讲起吧:
1.getPrice(String name)
这个方法是从foreground传递一个类型为string的参数到backend,然后backend判断返回相应的价格。在cpp的代码中,我们用GetStringUTFChars这个方法来把传来的jstring变成一个UTF-8编码的char型字符串。因为jstring的实际类型是jobject,所以无法直接比较。
GetStringUTFChars方法包含两个参数,第一参数是你要处理的jstring对象,第二个参数是否需要在内存中生成一个副本对象。将jstring转换成为了一个constchar*了之后,我们用string.h中带strcmp函数来比较这两个字符串,如果传来的字符串是“Apple”的话我们返回1.2。反之返回2.1。在这里还要多说一下ReleaseStringUTFChars这个函数,这个函数从字面上不难理解,就是释放内存用的。有点像cpp里的析构函数,只不过Sun帮我们已经封装好了。由于在JVM中有GC这个东东,所以多数javacoder并没有写析构的习惯,不过在JNI里是必须的了,否则容易造成内存泄露。我们在这里在release之前和之后分别打出这个字符串来看一下效果。
粗略的解释完一些API之后,我们编写测试代码。
- Business
b = new Business(); - System.out.println(b.getPrice("Apple"));
运行这段测试代码,控制台上打出
Before release: Apple
After release:
- GCC 警告提示的用法 (2011-10-11 16:36:12)
- BCB (2011-10-09 10:28:36)
- Uboot对U盘的支持 (2011-10-07 21:57:05)
- rvds 4.x 总结与延展 (2011-09-23 08:33:59)
- NFS 挂载文件系统出现 not responding 错误的解决方法 (2011-09-20 15:41:20)
- 使用 Strace 和 GDB 调试工具 (2011-08-26 19:23:49)