本文介绍在MacOS系统下搭建移动端(Android,iOS)音视频开发环境, 并添加C++
支持和交叉编译
三方库
iOS搭建项目
-
使用 XCode 新建Single View Appliation,一直Next最后创建成功
-
使用CocoaPods 安装第三方开源库,如JSONKit, AFNetworking,更多安装和使用详情参考
iOS增加C++支持
如何在iOS 项目中引用C++三方库
-
编译阶段配置参数
extra-cflags, -I
来指定引用头文件的位置, -
extra-cflags, -DDEBUG_TEST
, 相当于#define DEBUG_TEST
-
链接阶段需要配置参数
ld-flags,-L
来指定静态库的位置, -L来指定应用的哪一个库 -
Header Search Path指定头文件搜索路径
Android搭建项目
使用 Android Studio 新建Android 项目,输入项目名字,包名字,一直Next
Android 增加C++支持
使用JNI
编程框架,允许运行于JVM的Java程序去调用本地代码(C/C++
和汇编代码
),
JNI有两种调用形式
-
Java代码调用Native代码
-
Native代码调用Java代码
Java代码调用Native代码过程
-
编写Java类, 在方法签名中添加
native
修饰符 -
使用javac命令编译第一步产生的Java类,生成class文件
-
使用javah命令将class文件作为输入,生成JNI头文件
-
将JNI头文件复制到项目的jni目录,并且建立一个cpp的实现文件实现该JNI头文件的函数
-
编写
Android.mk
文件,加入第4步的本地代码,利用ndk-build
生成动态链接库 -
在Java类中加载第5步产生的动态链接库
arduino
static {
System.loadLibrary("xxxx")
}
- 在Java类中调用该Native方法
本地编译
原理是使用本机器的编译器,将源代码编译链接成为 一个可以在本机器上运行的程序。这就是正常的编译过程,也称为 Native Compilation
交叉编译
-
在一个平台(如
PC
)上生成另外一个平台(Android、iOS或者其他嵌入式设备)的可执行代码。 -
因为嵌入式设备
存储空间
和运算能力有限
,而且编译工具以及整个编译过程异常繁琐,所以在ARM平台进行本机编译几乎不太可能, PC才是理想的选择
交叉工具编译链
通过交叉工具编译链,开发者就能在 PC上编译出可以运行在ARM平台下的程序了。
主要包含如下几个工具:
-
CC: 编译器, 对C源文件进行编译处理,生成汇编文件。
-
AS: 将汇编文件生成目标文件(汇编文件使用的是指令助记符,AS将它翻译成机器码)。
-
AR: 打包器,用于库操作,可以通过该工具从一个库中删除或者增加目标代码模块。
-
LD: 链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者是可执行文件。
-
GDB: 调试工具,可以对运行过程中的程序进行代码调试工作。
-
STRIP: 以最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码。
-
NM: 查看静态库文件中的符号表。
-
Objdump: 查看静态库或者动态库的方法签名。
bash
正常编译一个程序过程如下
1. 编译: gcc -c main.cpp ./test.cpp -I /libssl/include
2. 打包: ar cr libmedia.a test.o
3. 链接: g++ -o main main.o -L ../prebuilt -l mdedia
iOS平台交叉编译的实践
iOS arm 包含如下CPU架构和对应机器
-
armv6: iPhone, iPhone 3G, iPod 1G/2G
-
armv7: iPhone 3GS, iPhone 4, iPhone 4S, iPod 3G/4G/5G, iPad, iPad 2, iPad 3, iPad Mini
-
armv7s:iPhone 5, iPhone 5c, iPad 4
-
arm64: iPhone X,iPhone 8(Plus),iPhone 7(Plus),iPhone 6(Plus),iPhone 6s(Plus), iPhone 5s, iPad Air(2), Retina iPad Mini(2,3)
-
arm64e: iPhone XS\XR\XS Max以上的设备
机器对指令集的支持是向下兼容的。
Build Active Architecture Only
选项表示是否只编译当前适用的指令集。一般情况下 在Debug的时候设置为YES,以便可以更加快速、高效地调试程序,而 在Release的情况下设置为NO,以便App在各个机器上都能够以最高效 率运行。
Excluded Architectures
排除指定的架构,减少编译后体积
LAME的交叉编译
LAME是目前非常优秀的一种MP3 编码引擎,在业界,转码成MP3格式的音频文件时,最常用的编码器就是LAME库。
-
从官网 LAME下载源码
-
编写build_arm64.sh 脚本
ini
./configure \ // 符合GNU标准的软件包发布必备的命令,使用configure的方式生成Makefile文件
--disable-shared \ // 关闭动态链接库
--disable-frontend \ // 不编译LAME可执行文件
--host=arm-apple-darwin `
--prefix="./thin/arm64" \ // 指定编译好的库放哪个目录下
// 交叉编译工具链路径就是GCC路径
CC="xcrun -sdk iphoneos clang -arch arm64" \
// CFLAGS 指定编译时所带的参数
CFLOGS="-arch arm64 -fembed-bitcode -miphoneos-version-min=11.0" \
// LDFLAGS: 指定链接过程中的参数
LDFLAGS="-arch arm64 -fembed-bitcode -miphoneos-version-min=11.0"
make clean
make -j8
make install // 使用make和make install编译和安装整个库
FDK_AAC
FDK_AAC是用来编码和解码 AAC格式音频文件的开源库,Android系统编码和解码AAC所用的就是这个库。
交叉编译过程和LAME类似
X264
X264是一个开源的H.264/MPEG-4 AVC视频编码函数库,是最好的有损
视频编码器之一。
-
输入是视频帧的YUV表示,输出是编码之后的H264的数据包,
-
支持VBR(Variable Bit Rate)可变码率,就是每一秒画面的大小是不固定。可以在编码的过程中直接改变码率的设置, 在
直播场景
中非常实用的(码率自适应
)
交叉编译过程类似LAME
Android平台交叉编译实践
Android NDK
Android原生开发包(NDK)用于Android平台上C++开发,包含API,交叉编译器,链接程序,调试器,构建工具
等综合工具集
经常会用到的组件如下
-
ARM,x86交叉编译器
-
构建系统
-
Java原生接口头文件
-
C库
-
Math库
-
最小的C++库
-
ZLib压缩裤
-
POSIX线程
-
Android日志库
-
Android原生应用API
-
OpenGL ES(EGL)库
-
OpenSL ES库
NDK 根目录结构
-
ndk-build: 该Shell脚本是Android NDK构建系统的起始点,仅仅执行该命令就可以编译出动态链接库了
-
ndk-gdb: 该Shell允许用GUN调试器调试Native代码,可以像调试Java代码一样调试Native代码
-
ndk-stack: 该Shell脚本帮助分析Native代码崩溃时的堆栈信息
-
build: 该目录包含NDK构建系统的所有模块
-
platforms: 该目录包含支持不同Android目标版本的头文件和库文件
-
toolchanins: 该目录包含目前NDK所支持的不同平台下的交叉编译器-ARM,x86
Android.mk
是Android平台上构建一个C或C++语言编写的Makefile文件
分为如下几个部分
-
LOCAL_PATH: 返回当前文件在系统中的路径
-
include $(CLEAR_VARS): 清除上一次构建的所有全局变量
-
LOCAL_SRC_FILES: 要编译的C或者Cpp文件
-
LOCAL_STATIC_LIBRARIES: 所依赖的静态库文件
-
LOCAL_LDLIBS: 指定编译过程所依赖的NDK提供的动态和静态库
-
LOCAL_CFLAGS: 编译C或者Cpp编译标志,在实际编译的时候会发送给编译器, 比如加上-DDEBUG_TEST, 然后就可以使用 #ifdef DEBUG_TEST 做一些特殊处理了
-
LOCAL_LDFLAGS: 链接标志的可选列表,将这些标志传给链接器
-
LOCAL_MODULE: 该模块编译的目标名,用于区分各个模块
-
include $(BUILD_SHARED_LIBRARY: 构建动态库
类似的还有
BUILD_STATIC_LIBRARY
(构建静态库),BUILD_EXECUTABLE
(构建可执行程序)
Application.mk
是应用程序本身的描述文件,描述要针对哪些CPU架构打包so包,以及一些编译和链接参数,分为如下几个部分
-
APP_ABI=XXX: 构建不同的平台的库, 包括 x86, armeabi-v7a, arm64-v8a, all 等
-
APP_STL: 荀泽C++版本
-
APP_CPPFLAGS: 指定编译过程中flag,可以开关exception rtti 等C++特性
-
NDK_TOOLCHAIN_VERSION: 交叉工具编译链的版本
-
APP_PLATFORM: 指定创建的动态库平台
Android 平台交叉编译
LAME
ini
#!/bin/bash
// 设置NDK_ROOT,并且声明platform和prebuilt,最终配置可在环境变量中查看。
NDK_ROOT=/Users/apple/soft/android/android-ndk-r9b
PREBUILT=$NDK_ROOT/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86_64
PLATFORM=$NDK_ROOT/platforms/android-9/arch-arm
export PATH=$PATH:$PREBUILT/bin:$PLATFORM/usr/include:
// CFLAGS与LDFLAGS,其目的是在编译和链接阶段找到正确的头文件与链接到正确的库文件。这里需要特别注意的是,在这两个设置的后边都加上了-march=armv7-a,这相当于是让编译器知道要编译的目标平台是armv7-a。
export LDFLAGS="-L$PLATFORM/usr/lib -L$PREBUILT/arm-linux-androideabi/lib
-march=armv7-a"
export CFLAGS="-I$PLATFORM/usr/include -march=armv7-a -mfloat-abi=softfp -mfpu=vfp
-ffast-math -O2"
// 声明CC、AS、AR、LD、NM、STRIP等工具,具体每一个工具是做什么用的,前面都已经介绍过了,如果要编译armv5、x86或者arm64-v8a,那么在代码仓库中会提供全量编译的Shell脚本文件。
export CPPFLAGS="$CFLAGS"
export CFLAGS="$CFLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$LDFLAGS"
export AS=$PREBUILT/bin/arm-linux-androideabi-as
export LD=$PREBUILT/bin/arm-linux-androideabi-ld
export CXX="$PREBUILT/bin/arm-linux-androideabi-g++ ------sysroot=${PLATFORM}"
export CC="$PREBUILT/bin/arm-linux-androideabi-gcc ------sysroot=${PLATFORM}
-march=armv7-a "
export NM=$PREBUILT/bin/arm-linux-androideabi-nm
export STRIP=$PREBUILT/bin/arm-linux-androideabi-strip
export RANLIB=$PREBUILT/bin/arm-linux-androideabi-ranlib
export AR=$PREBUILT/bin/arm-linux-androideabi-ar
// 使用LAME本身的Configure进行编译裁剪
./configure ------host=arm-linux \
------disable-shared \
------disable-frontend \
------enable-static \
------prefix=./armv7a
make clean
make -j8 // 编译链接
make install // 安装
FDK_ACC和X264 Android 平台交叉编译和LAME类似
使用LAME编码MP3文件
首先新建两个文件:mp3_encoder.h和mp3_encoder.cpp。
然后编写encode方法,负责读取PCM数据,并且调用LAME进行编码,然后将编码之后的数据写入文件。
csharp
class Mp3Encoder {
private:
FILE* pcmFile; // 输入pcm数据文件地址
FILE* mp3File; // 输出mp3文件地址
lame_t lameClient; // LAME
public:
Mp3Encoder();
~Mp3Encoder();
// 初始化
int Init(const char* pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate);
void Encode(); // 负责读取PCM数据,并且调用 LAME进行编码,然后将编码之后的数据写入文件
void Destory(); // 关闭所有的 资源
};
ini
int Mp3Encoder:Init(const char* pcmFilePath,const char *
mp3FilePath,int sampleRate,int channels,int bitRate) {
int ret = -1;
pcmFile = fopen(pcmFilePath,"rb");
if(!pcmFile) {
return ret;
}
mp3File = fopen(mp3FilePath,"wb");
if(mp3File) {
lameClient = lame_init();
lame_set_in_samplerate(lameClient,sampleRate);
lame_set_out_samplerate(lameClient,sampleRate);
lame_set_num_channels(lameClient,channels);
lame_set_brate(lameClient,bitRate / 1000);
lame_init_params(lameClient);
ret = 0;
}
return ret;
}
void Mp3Encoder:Encode() {
int bufferSize = 1024 * 256;
short* buffer = new short[bufferSize / 2];
short* leftBuffer = new short[bufferSize / 4];
short* rightBuffer = new short[bufferSize / 4];
unsigned char* mp3_buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
// 循环读取一段bufferSize大小的PCM数据buffer
while ((readBufferSize = fread(buffer,2,bufferSize / 2,pcmFile)) > 0) {
// 分开左右声道
for (int i = 0; i < readBufferSize; i++) {
if (i % 2 == 0) {
leftBuffer[i / 2]= buffer[i];
} else {
rightBuffer[i / 2]= buffer[i];
}
}
// 送入LAME编码器来编码该buffer
size_t wroteSize = lame_encode_buffer(lameClient,(short
int *) leftBuffer,(short int *) rightBuffer,
(int)(readBufferSize / 2),mp3_buffer,bufferSize);
// 将编码后的数据写入MP3文件中
fwrite(mp3_buffer,1,wroteSize,mp3File);
}
// 删除临时内存对象
delete[]buffer;
delete[]leftBuffer;
delete[]rightBuffer;
delete[]mp3_buffer;
}
// 关闭PCM文件,关闭MP3文件,销毁LAME
void Mp3Encoder:Destory() {
if(pcmFile) {
fclose(pcmFile);
}
if(mp3File) {
fclose(mp3File);
lame_close(lameClient);
}
}