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硬件渲染。

 

Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.
Возможность комментирования русскоязычного контента была отключена. Узнать подробнее.