Android ffmpeg的x86编译和优化

一、        认识FFMPEGAndroid NDK中的使用

Android内置的编解码器比较少,流媒体功能也比较薄弱,现有的android关于远程视频的程序大部分使用了FFMPEGFFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPLGPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多codec都是从头开发的。FFmpeg支持MPEGDivXMPEG4AC3DVFLV40多种编码,AVIMPEGOGGMatroskaASF90多种解码.TCPMP, VLC, MPlayer等开源播放器都用到了FFmpeg。这些播放器也大部分移植到了Android 上,只是都是基于ARM的,本文基于目前流行的2个开源项目,介绍了如何移植FFMPEGx86android平台上。

基于FFMPEGAndroid-ARM项目很多,最具有代表性的有2个:

havlenapetr :最早期的完全基于FFMEPGandroid开源项目,整个项目比较简单,音视频同步做的很初步,音视频的显示是直接在android工程内添加hack,直接从NDK层调用系统的音视频输出。该项目比较简单,但是整个工程的编译写的非常好,适合初学者学习和移植ffmpeg,项目地址:https://github.com/havlenapetr

VideoLAN:(简称 VLC):整合了FFMEPG和众多开源项目,全平台全格式,且有网络支持,VLC大而全,功能完善,所有功能都实现为独立的module,且各个module可以动态加载。VLC也支持android全平台,视频输出使用系统库,只是VLC相当复杂,不太好移植项目地址:http://git.videolan.org/

二、        如何配置和编译X86 FFMPEGhavlenapetr

我们首先选择havlenapetr进行x86的编译和优化。准备工具:linuxubuntu)且配置了NDK编译环境(需要选择r6b以上)。使用git clone https://github.com/havlenapetr/FFMpeg 下载havlenapetr的整个代码。Havlenapetr是基于arm编译的,核心编译文件  av.mk (arm下编译正常,在x86下编译有bugHavlenapetr合理的利用了ffmpeg原有的makefile文件,在av.mkinclude $(LOCAL_PATH)/Makefile,在每个目录的Android.mk include av.mk,各个目录的Android.mk是完全一致的。Havlenapetr已经完全配置好了armffmpeg编译环境,这里我们从一个完全没有配置的ffmpeg说说如何配置x86的编译环境。

首先在根目录下make version.sh生成version.h,有些ffmpeg版本会在95行出现问题 ( library.mak:95: *** missing separator. Stop ),解决方法是在95$(eval $(RULES))前加上tab(或者空格)。生成了version.h后添加配置文件config.shHavlenapetr已经添加了arm版本的config.sh。我们需要改为x86版本的。几个关键点:--enable-static --disable-shared(说明编译的是静态库),--disable-amd3dnow --disable-amd3dnowext --disable-ssse3(关闭medfiled不支持的优化选项)--disable-yasm(关闭yasm编译器选项)。需要注意的是不要--disable-asm--disable-asm将会关闭所有汇编的支持,这样x86上的mmxsse优化也同时被关闭了。添加config.sh后运行它就会生成config.makconfig.h。这里可能会出现error bash: ./configure: /bin/sh^M: bad interpreter: No such file or directory,这是因为configure是在window下写的,所以在每行后面会加个ctrl+m就是^M,所以后面的,sh就变成sh^M当然是没有这个命令的,所以脚本就不能运行了,把^M去掉就没问题了。解决方法:在linux上打开configure 文件,把这个文件存成linux/unix格式。这一步成功后,就可以开始编译了,在编译前,还需要确保每个需要编译的目录内都有android.mk文件。这个android.mk文件都是一样的。内容如下:

 

LOCAL_PATH := $(call my-dir)

 

include $(CLEAR_VARS)

 

include $(LOCAL_PATH)/../av.mk

 

LOCAL_SRC_FILES := $(FFFILES)

 

LOCAL_C_INCLUDES :=            \

 

         $(LOCAL_PATH)        \

 

         $(LOCAL_PATH)/..

 

LOCAL_CFLAGS += $(FFCFLAGS)

 

LOCAL_LDLIBS := -lz

 

LOCAL_STATIC_LIBRARIES := $(FFLIBS)

 

LOCAL_MODULE := $(FFNAME)

 

include $(BUILD_STATIC_LIBRARY)

 

 

 

 

 

    同时还需要添加Application.mk 指定APP_ABI := x86Av.mk还需要做一些修改,修改FFCFLAGS = -march=i686 -DANDROID -DPIC -fpic  -std=c99 -fomit-frame-pointer。添加 SUBDIR = $(LOCAL_PATH)/ 。修改ALL_S_FILES := $(wildcard $(LOCAL_PATH)/$(TARGET_ARCH)/*.S) 修改后缀名为.asm。将include $(LOCAL_PATH)/../config-$(ARCH).mak 改为include $(LOCAL_PATH)/../config.mak。修改后的av.mk如下:

 

# LOCAL_PATH is one of libavutil, libavcodec, libavformat, or libswscale

 

include $(LOCAL_PATH)/../config.mak

 

OBJS :=

 

OBJS-yes :=

 

MMX-OBJS-yes :=

 

SUBDIR = $(LOCAL_PATH)/

 

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 = $(CFLAGS)

 

FFCFLAGS = -march=atom  -DANDROID -DPIC -fpic  -std=c99 -fomit-frame-pointer

 

FFCFLAGS  += -DHAVE_AV_CONFIG_H -Wno-sign-compare -Wno-switch -Wno-pointer-sign

 

ifeq ($(HAVE_MMX),yes)

 

FFCFLAGS +=   -mmmx

 

endif

 

ifeq ($(HAVE_SSE),yes)

 

FFCFLAGS +=   -msse

 

Endif                  

 

     这里先提一下调试mk文件的技巧:可以在mk文件内添加 $(warning $(FFFILES)),就可以查看FFFILES变量的值,通过查看FFFILES的值就可以发现大量的x86目录下的文件没有编译进去,然后接着$(warning $(SUBDIR)),就会发现SUBDIR没有设置。

Android动态链接库的编译需要添加-fpic,表示编译的代码是位置无关。普通的重定位方法需要修改代码段,比如偏移地址0x100处需要重定位,loader就修改代码段的0x100处的内容,通过查找重定位信息得到具体的值.这种方法需要修改代码段的内容,对于动态连接库来说,其初衷是让多个进程共享代码段,若对其进行写操作,就会引起COW CopyOnWrite,写时复制),从而失去共享.non-PIC PIC 代码的区别主要在于 access global data, jump label 的不同。 -fpic选项告诉编绎器使用GOTPLT的方法重定位。GOT data section, 是一个 table, 除专用的几个 entry,每个 entry 的内容可以再执行的时候修改; PLT text section, 是一段一段的 code,执行中不需要修改。-DPIC:因为添加了-fpicfpic需要使用ebx寄存器,所以如果汇编代码内使用了ebx寄存器,需对ebx做保护 如:

#if defined(PIC)

      "push             %%"REG_b"             \n\t"

#endif

… do somethings

#if defined(PIC)

       "pop              %%"REG_b"             \n\t"

#endif

 

    接下来就是使用ndkr6b以上版本编译ffmpeg。我们会发现mpegvideo_mmx_template.c 编译不通过。因为使用了-fpic,该文件内的asm内嵌代码使用了过多的寄存器,在mpegvideo.c函数ff_dct_common_init中注释掉MPV_common_init_mmx(可以参考http://ffmpeg.org/faq.html#error_003a-can_0027t-find-a-register-in-class-_0027GENERAL_005fREGS_0027-while-reloading-_0027asm_0027)编译完后会生成libavformat libavcodec  libavutil libswscale libmediaplayer libavfilter libpostproc等库文件。注意库文件的连接顺序,在libavcodec 内会引用libavformat 的函数,如果把libavformat 放在libavcodec的后面, 会导致链接失败。

注:可以修改mpegvideo_mmx_template.c内的内嵌汇编代码,只要少使用一个外部变量读入“r”就可以编译通过:分析代码,发现qmat, bias2个变量其实只在地址上有固定偏移,可以在代码内用qmat的地址偏移去代替bias就可以编译通过了。

三、        编译开源全格式播放器X86版本

     推荐使用videolan编译android全格式播放器。Videolan本身有android版本的。见http://git.videolan.org/?p=vlc/vlc-android.git;a=summary 。Videolan编译比较复杂一些,需要先bootstrap,实时配置环境并下载各个模块的源码。这里推荐一个基于videolan的android开源项目tewilove_faplayer。tewilove_faplayer已经下载了各个模块的代码,是一个完整的基于android的arm base全格式播放器。项目地址: https://github.com/tewilove/faplayer。推荐在linux下直接git下载,因为内有很多symbol link,zip包需要修改。

     Videolan是一个播放器框架。Videolan本身并不负责视频的编解码,它仅仅提供了一个播放器框架,在vlc\modules内实现了各个功能模组,这些modules是可以动态加载的,各个modules的具体实现在modules内或者在ext内(比如ffmpeg)。

    核心代码 modules.c 内的 vlc_module_load函数,它使用psz_capability字段来辨别各个模块的功能,如果有多个模块属于 video-output,会根据priority依次加载模块,取第一个成功加载的模块。

模块定义如下:

vlc_module_begin()

    set_category(CAT_VIDEO)

    set_subcategory(SUBCAT_VIDEO_VOUT)

    set_shortname("AndroidSurface")

    set_description(N_("Android Surface video output"))

    set_capability("vout display", 155)

    add_shortcut("androidsurface", "android")

    set_callbacks(Open, Close)

vlc_module_end()

 这个模块的加载函数就是open,psz_capability是vout_display ,优先级155

tewilove_faplayer已经配置好了videolanarm编译环境,我们需要做一些修改来编译x86的版本。

1.         修改vlc_fixups.h,去除掉__cplusplus的定义,不然会导致编译错误

2.         修改\jni\vlc\config.h

添加宏定义

#define CAN_COMPILE_MMX  1

#define CAN_COMPILE_MMXEXT 1

#define CAN_COMPILE_SSE 1

#define CAN_COMPILE_SSE2 1

#define asm __asm__

#define MODULE_NAME_IS_i422_yuy2_sse2

#define MODULE_NAME_IS_i420_yuy2_sse2

#define MODULE_NAME_IS_i420_rgb_sse2

(见 i422_yuy2.c 需要这2个宏指定 yuv2rgb使用什么优化算法)

3.         使用X86 FFMpeg替代ext中的ffmpeg目录

tewilove_faplayerffmpeg编译写的不是很好,我们可以直接使用上文修改好的x86 ffmpeg

4.         修改Application.mk

APP_ABI := x86

BUILD_WITH_NEON := 0

OPT_CPPFLAGS += -frtti fexceptions typeid函数需要 frtti

rtti(runtime type identification)

5.         去除neno的库

修改libvlcjni.h  去掉yuv2rgb

Yuv2rgb 是基于neno的,对应的有x86的,在sse2目录下

6.         可以不添加fastmencpy模块。经过测试glibcmemcpy效率比较高

a)         使用Vlc下面的fastmemcpy.hmmx拷贝和sse拷贝 RDTSC来精确计时,拷贝50Mbyte数据

b)         标准glibc里面标准memcpy 耗时 91690808 cycles

c)         Mmx拷贝 耗时183769311cycles

d)         Sse拷贝 耗时91996729cycles

e)         结论是sse拷贝比mmx1倍, 但是glibc里面的memcpy反而效率最高

如果需要添加fastmencpy模块,在mmx目录下添加Android.mk,修改libvlcjni.h  添加mmx module,修改\jni\vlc\Modules.mk添加mmx_plugin

 

         做了以上修改后,直接使用ndk编译,会生成11M多的libvlccore.so,将这个so放在libs/x86目录下,在eclipse中就可以生成fplayer.apk。编译后生成的apkandroid4.0上不能播放声音和显示图像,还需要在代码中做一些修改。

 

1.    修改vlc/src/audio-output/output.c,去掉声音格式转换的部分。因为没有S16NFI32声音采样格式的转换module
2.       android4.0上无法显示图像,因为

Android2.2使用libsurfaceflinger_client.so显示图像

Android2.3使用libui.so显示图像

Android4.0使用libgui.so显示图像

# define ANDROID_SYM_S_LOCK "_ZN7android7Surface4lockEPNS0_11SurfaceInfoEb"

# define ANDROID_SYM_S_UNLOCK "_ZN7android7Surface13unlockAndPostEv"

# define ANDROID_SYM_S_LOCK2 "_ZN7android7Surface4lockEPNS0_11SurfaceInfoEPNS_6RegionE"

注意在android4.0中lock函数的参数有变化,最后一个参数从1改为NULL。

四、        性能测试

我们使用1080P的mp4文件,强制软件测试ffmpeg的解码性能。使用联想K800手机,cpu频率始终保持在1.6G上,cpu占用率在50~60%之间。去掉ffmpeg的优化 (-disable-asm 去掉mmx sse)后重新编译的fplayer,cpu占用率70%以上。可见优化是有效果的。实际上要想做一个速度更快的播放器,除了优化编解码器,对渲染(图像显示)的优化也是很重要的,测试表明对于640P的解码,渲染耗费的cpu资源远远高于解码器的cpu耗费资源,可以利用sse对vlc的swscale做进一步优化,或者使用Open-GL,可以获得更大的性能提升。

五、        使用SSE2swscale做进一步优化

SSE2(Streaming SIMD Extensions 2),是Intel继MMX和SSE后在SIMD技术上的再次扩展。SSE2的寄存器容量是MMX的两倍,寄存器存属数据也增加了两倍。在指令处理器速度保持不变的的情况下,通过SSE2优化过的程序和软件运行速度也能提升两倍。由于SSE指令集和MMX指令集相兼容。因此,被MMX优化过的程序很容易用SSE2进行更深层次的优化,达到更好的效果。

接下来我们将使用SSE2对ffmpeg的yuv2mmx进行优化。修改的代码主要集中在yuv2rgb_template.c中,修改的核心步骤主要有以下几条:

1.       sse2中使用xmm寄存器,而在mmx2中使用mm寄存器

2.       mov指令的不同:在sse2中使用movntpsmovups,而在mmx2中使用movntqmovq

3.       sse2128位的操作数,所以原有的64bit操作数要改成128bit
比如  mmx中定义的DECLARE_ALIGNED(8, uint64_t, redDither);
SSE2中改成:DECLARE_ALIGNED(16, uint64_t, redDither);
               DECLARE_ALIGNED(8, uint64_t, redDither1);

使用连续的264bit来形成一个128bit的操作数

4.       修改循环体:mmx2一次减少8颜色值sse2一次要减少16个颜色值

 

把sse2和mmx2的代码提取出来在x86平台上做验证,做1920*1080p的yuv2rgb图像变换,循环50次,使用寄存器rdtsc来统计cpu的运行cycle,结果如下:

           SSE(42000), MMX(70748)

 

可见sse2能提高大约40%的性能。使用sse2优化后,我们测试强制软件解码1080Pmp4,播放性能提升了20%

当然如果GPU支持YUV的渲染,使用硬件刷新可以获得更多的性能提升,在IOS上,已经有API可以实现这一个功能,但是在android上还需要利用到源码自己编写opengl-esyuv刷新代码,在下一篇文章中,我们将继续讲解如何在android上(powervrgpu),使用opengl-es进行yuv硬件渲染。

 

Per informazioni più dettagliate sulle ottimizzazioni basate su compilatore, vedere il nostro Avviso sull'ottimizzazione.
Contrassegni: