这个系列的文章将会研究最纯粹的Android直播的实现,而且不是用现在的集成SDK来达到直播的技术实现,而是从一个比较底层的直播实现来探讨这个技术,这样子对于直播技术的实现,现成的一些直播框架等都有一个比较好的理解。
现在的一些视频文件都是经过编码–>封装得到的,比如说一个mp4的视频文件mp4就是它的封装格式,h.264就是它的编码格式(h.264是现在最广泛的编码格式了绝大部分的视频文件都是用它来进行编码的),那么我们的一个视频的播放就会是下面的流程了:
mp4文件–>h.264文件(解封装后生成)–>yuv文件(解码后生成)
yuv就是比较原始的视频文件了,它是非常的大的,和h.264文件相比,可以达到1:100就是说10M大小的h.264文件解码后生成的yuv文件会达到100多M。所以我们的视频文件都会经过一个编码的过程。
那么反过来,我们生成一个视频文件的时候也就会经过:
yuv文件(h.264编码)–>h.264文件(mp4封装)–>mp4文件
比如说我们Android默认的Camera的预览数据就是一种yuv的数据来的,所以我们从Camera里面获取的yuv数据,然后经过一个h.264的编码,然后就可以进行封装或者传输了。
那么直播的这个过程就有可能会是下面那样了:
这系列的文章主要就会研究Android端的直播和看直播的技术,直播并不单单只有RTMP一种协议,还有RTP这些协议等,RTMP比较常用,它使用的封装格式是FLV的,用的网络协议是TCP,RTP用的封装格式是mpegts,用的网络协议是UDP。这里就会使用RTMP这种协议。
在那个图片里面可以看到,在推流前,主要做的就是完成一个视频文件的生成了yuv–>h.264,在Camera获取到的yuv数据里面,其实我们还可以加上养颜啊,特效叠加这些功能,然后再进行编码。也可以对声音进行处理,比如说娃娃音这些。
注意:声音是在视频封装的时候才加上去的,解封装的时候是会把图像数据和音频数据分开的
要注意的是编码格式和封装格式是有很多种的,这个系列文章并不会详细的研究这个,只会在需要的时候讲解这些知识,所以如果想详细知道这些编码和封装的知识的,可以先去Google一下。下面我们就要进入我们这篇文章的实战了,首先就是平台、工具的介绍:
Windows7
MinGW
AndroidStudio 2.2
NDK(r13b)
FFmpeg 3.4
libx264
因为我是在Windows平台下进行编译的,所以就需要MinGW了,MinGW的安装一定要安装msys,不然就没办法进行编译了。
在开发过程中,推荐使用AndroidStudio2.2以上的版本,因为2.2之后,对NDK的支持有了很大的提升,比如说:不需要自己javah来生成native方法对应的C方法了,AS会帮我们自动生成,这个功能就很值得使用了,当然还有其他强大的NDK支持。
我使用的NDK的版本是r13b,最好使用比较新版本的NDK,因为AndroidStudio都使用到2.2了
在直播的流程中,可以看到我们需要对视频进行编码,封装等操作,那么进行这些操作的话,我们需要FFmpeg这个库来完成,FFmpeg对音视频的处理是非常的强大的。这样说吧,常见的播放器,QQ影音,暴风影音啊,都是使用FFmpeg来进行核心的解码,编码等操作的,格式工厂
这些转换软件也是,而且现在的一些集成的直播方案,绝大部分也是对FFmpeg进行了包装。所以在这个系列中,就会使用FFmpeg来还原最纯粹的直播技术,这样就可以知道现在打包好的直播技术是怎样实现的。PS:FFmpeg的读法是: F F mpeg 而不是一个一个字母的读
libx264是一个h.264的编码器,在使用FFmpeg的AV_CODEC_ID_H264编码器进行编码的时候,我们就需要使用到这个库了,所以编码FFmpeg的时候,我们会把libx264加进去。
默认Android开发环境已经配置好,NDK环境弄好,安装完成MinGW之后,我们再把FFmpeg和libx264下载下来就可以了,然后解压好。
然后我们修改一下里面的configure文件,让我们编译出来的文件不会带有奇怪的名字,不被Android识别。只要把
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)'
修改完成之后,我们就要在当前目录新建一个.sh文件,就是shell脚本
#!/bin/bash
NDK=E:/Programs/android-ndk-r13b
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
function build_one {
./configure \
--prefix=$PREFIX \
--enable-asm \
--enable-neon \
--enable-static \
--enable-small \
--disable-shared \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-avdevice \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--cpu=armv7-a \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-fPIC -DANDROID -mfpu=neon -mfloat-abi=softfp -I$NDK/platforms/android-19/arch-arm/usr/include" \
--extra-ldflags="$ADDI_LDFLAGS"
make clean
make
make install
}
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
build_one
在这个脚本前面,配置了三个变量,都是指定好NDK的一些工具,目录的,要注意的是在:SYSROOT里面,我编译的platform是android-19的,因为NDK对platform有时会有一些影响的,做过深入的NDK开发的同学应该清楚,所以这个要看具体的需要进行更改什么的。
–enable-static –disable-shared这两个是看编译出来的库是静态库(.a)还是动态库(.so),如果要编译成动态库就–enable-shared –disable-static。或者两个都编译出来。
动态库就是只要Android设备里面有公开这个so,那APP就可以使用,就是只要一次安装,其他APP就可以使用了
但APP打包进去的so都是在当前APP的私有目录下的,不能被其他APP使用的,所以动态库的优势就没有了,
而且就暴露了它的缺点,就是so非常大的时候,就会形成Apk包非常大
静态库就是会把需要的代码打包到自己的so里面,所以这样子就可以解决动态库上面的那个缺点
所以这里我们编译出来的是静态库,因为编译出来的库有50多M,所以使用静态库的方式,可以使打包出来的apk最小化
其他的配置都是比较通俗易懂的了,就不多做解释了。上面那个脚本编译出来的就是一个完整功能的FFmpeg库了,我们就可以打开MinGW的msys了
打开msys.bat,然后cd到FFmpeg的根目录,执行./build_script.sh ,就是执行我们上面写的脚本
要打开一个盘:如打开E盘,cd /e 就可以了。
运行那个脚本就会进行一个编译的了,编译过程中,有时可以什么反应都没有,千万不要以为没有运行,停止它,因为编译是比较耗时的,编译个20分钟是很正常的,电脑性能好的,会快点,差的可能半小时都正常,要确定有没有在编译,可以查看一下cpu的使用率就可以了。
经过一段时间的编译,我们就可以在脚本里面定义的那个目录里面看到编译成功的库了。在当前目录下就会有一个android的目录,点进里面后,就会看到编译成功的库了
可以看到有7个库左右。可能版本不一样或配置不一样会有不一样的库,一般用到的会是libavcodec libavfilter libavformat libavutil libswscale这些。
这些库的功能都是非常齐全的,包含了大量的编解码器等,但有时我们并不需要那么多功能,这样就可以通过禁用一些功能来达到精简一些库的大小了
如:不需要解码器 –disable-decoders就可以在上面的那个编译脚本里面加上这个配置。如果只需要某个解码器就可以指定–disable-decoders –enable-decoder=h264 这样就是先禁用解码器,然后指定一个解码器,就是只需要h264的解码器了
所以就可能通过上面那个文件达到精简库的大小了,如下面那个精简的脚本:
#!/bin/bash
NDK=E:/Programs/android-ndk-r13b
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
function build_one {
./configure \
--prefix=$PREFIX \
--enable-asm \
--enable-neon \
--enable-static \
--enable-small \
--disable-shared \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-avdevice \
--disable-symver \
--disable-muxers \
--enable-muxer=mov \
--enable-muxer=mp4 \
--enable-muxer=avi \
--disable-decoders \
--enable-decoder=aac \
--enable-decoder=h264 \
--enable-decoder=mpeg4 \
--disable-demuxers \
--enable-demuxer=h264 \
--enable-demuxer=avi \
--disable-parsers \
--enable-parser=aac \
--enable-parser=h264 \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--cpu=armv7-a \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-fPIC -DANDROID -mfpu=neon -mfloat-abi=softfp -I$NDK/platforms/android-19/arch-arm/usr/include" \
--extra-ldflags="$ADDI_LDFLAGS"
make clean
make
make install
}
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
build_one
这样就可以大大的缩小库的大小了
那么,接下来就要编译包含了libx264的FFmpeg库了,首先,我们要先编译libx264,我们先在msys里面cd到libx264的目录下面,然后再新建一个shell脚本:
#!/bin/bash
NDK=E:/Programs/android-ndk-r13b
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
function build_one {
./configure \
--prefix=$PREFIX \
--enable-static \
--enable-pic \
--disable-asm \
--disable-cli \
--host=arm-linux \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$SYSROOT
make clean
make
make install
}
PREFIX=$(pwd)/android-lib/
build_one
配置基本上是差不多的,其实这些配置都是可以在configure文件里面找到的,大家可以看看。这里,我们也是编译成静态库。
编译成功后,我们就会在当前目录下的android-lib目录里面看到编译的结果。
那么我们就要把android-lib整个目录拷贝到FFmpeg的根目录下面了,然后就准备编译FFmpeg
其实也很简单,只需要把上面的编译文件添加一些东西就行了
#!/bin/bash
NDK=E:/Programs/android-ndk-r13b
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
EXTRA_LIB=E:/Programs/FFmpeg-3.2.4/android-lib
function build_one {
./configure \
--prefix=$PREFIX \
--enable-asm \
--enable-neon \
--enable-static \
--enable-small \
--enable-libx264 \
--enable-gpl \
--enable-encoder=libx264 \
--disable-shared \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-avdevice \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--cpu=armv7-a \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-I$EXTRA_LIB/include -fPIC -DANDROID -mfpu=neon -mfloat-abi=softfp -I$NDK/platforms/android-19/arch-arm/usr/include" \
--extra-ldflags="-L$EXTRA_LIB/lib -lx264 $ADDI_LDFLAGS"
make clean
make
make install
}
CPU=arm
PREFIX=$(pwd)/android/x264_lib/$CPU
ADDI_CFLAGS="-marm"
build_one
这样子,我们就可以编译一个带有libx264编码器的FFmpeg库了
首先,新建一个新的AndroidStudio项目
在新建项目的时候,最好不要勾选*Include C++ Support,因为它默认是采用cmake编译的,所以对于我们想到ndk_build的话就要修改配置了,所以最好不要勾选,除非你用cmake。
新建项目完成之后呢,我们就把视图选成project,然后再在main目录下新建一个cpp的目录
新建cpp目录完成后,就可以把Android.mk和Application.mk文件给拷贝或新建到这个目录里面了,然后再新建一个c的源文件
注意:这里新建的文件是.c的源文件,为什么不要新建.cpp的源文件,
是因为使用.cpp的源文件的时候,会编译不通过,因为FFmpeg是一个纯c语言的项目
也有可能是我的项目配置的问题,所以这里我是新建.c的文件
有找到原因或解决方法的也可以告诉我
我在.cpp 里面使用 extern “C” 也是会编译不通过的
做完这些之后,就可以配置Gradle了,在main目录上右键
要注意,这里我们要选择的是ndk_build,除非用想使用的是cmake,选完这个之后,就再把我们上面添加的Android.mk文件指定到下面的那个Project Path里面去。确定完成后,Gradle就会自动添加一些编译一次的。这样子,这个项目就可以成为一个NDK的项目了
接下来,就要把我们刚刚编译成功的FFmpeg,libx264的库,头文件,都拷贝到cpp这个目录下
然后就要编写Android.mk文件了
LOCAL_PATH := $(call my-dir)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := libavcodec.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := libavfilter.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := libavformat.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := libavutil.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := libpostproc.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := libswresample.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := libswscale.a
include $(PREBUILT_STATIC_LIBRARY)
# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := x264
LOCAL_SRC_FILES := libx264.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a
LOCAL_MODULE := live_jni
LOCAL_SRC_FILES := live_jni.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_CFLAGS := -D__STDC_CONSTANT_MACROS -Wno-sign-compare -Wno-switch -Wno-pointer-sign -DHAVE_NEON=1 -mfpu=neon -mfloat-abi=softfp -fPIC -DANDROID
LOCAL_STATIC_LIBRARIES := avfilter avformat avcodec postproc swresample swscale avutil x264
LOCAL_LDLIBS := -L$(NDK_ROOT)/platforms/$(APP_PLATFORM)/arch-arm/usr/lib -L$(LOCAL_PATH) -llog -ljnigraphics -lz -ldl
include $(BUILD_SHARED_LIBRARY)
这个Android.mk有些不要必须的,之些踩坑可能就会写得有些重复。所以看需要各位进行调整
TARGET_ARCH_ABI := armeabi-v7a
最好指定一下平台,有时候ndk prebuild的时候,有可能会编译全平台,就会出现找不到对应的库的问题的了
LOCAL_STATIC_LIBRARIES := avfilter avformat avcodec postproc swresample swscale avutil x264
这个是非常重要的,后面的那些库的顺序是有讲究的,如果顺序有问题,有可能会报找不到链接的问题
因为FFmpeg每个版本都可能不一样的,所以想要知道这个的顺序,可以去FFmpeg目录下的Makefile文件看看
# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE) += avdevice
FFLIBS-$(CONFIG_AVFILTER) += avfilter
FFLIBS-$(CONFIG_AVFORMAT) += avformat
FFLIBS-$(CONFIG_AVCODEC) += avcodec
FFLIBS-$(CONFIG_AVRESAMPLE) += avresample
FFLIBS-$(CONFIG_POSTPROC) += postproc
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE) += swscale
Android.mk文件写好了之后,我们就写一下Application.mk文件了
APP_STL := gnustl_static
APP_LDFLAGS := -latomic
APP_ABI := armeabi-v7a
APP_PLATFORM := android-19
APP_LDFLAGS
这个最好指定一下 不然可能报 undefined reference to '__atomic_fetch_add_4 error
编写完成Application.mk后,我们最好还是去配置一下app的build.gradle
externalNativeBuild {
ndkBuild {
arguments "NDK_APPLICATION_MK=src/main/cpp/Application.mk"
cppFlags "-frtti", "-fexceptions"
abiFilters "armeabi-v7a"
}
}
到现在,我们的整个配置就可以完成了。现在就可以回到MainActivity里面,然后添加一个native方法,然后在方法上Alt+Enter,这样子就会有提示,然后Enter让它生成native方法对应的c方法就可以了
但是,第一次生成对应的native方法的时候,可能会有点问题的
它会新建一个jni的目录,里面新建一个c文件,生成在里面了,我们只要把这个生成的方法,拷贝到我们自己新建的c文件里面,就可以自动建立关联了,这样,以后再自动生成的方法都会在我们新建的那个文件里面了。然后,我们把jni这个目录删掉就行了。
Android NDK官方指南
按照Android的官方文档显示,Application.mk是不需要自己指定的,它会自动的添加进去的(要和Android.mk同一路径)
这样子,我们就可以写我们的FFmpeg的代码了
#include
#include "libavcodec/avcodec.h"
JNIEXPORT jstring JNICALL
Java_com_xiaoxiao_live_MainActivity_helloFromFFmpeg(JNIEnv *env, jobject instance) {
// TODO
char info[10000] = {0};
sprintf(info, "%s\n", avcodec_configuration());
return (*env)->NewStringUTF(env, info);
}
然后在MainActivity里面把so加载进去就可以了
写完这个之后,我们就可以把我们的项目运行起来了
得到这个结果就说明我们的FFmpeg编译是成功的,而且我们也可以在Android上面使用了
而且从上面看到,我们打包出来的so是非常的小的,这样就可以大大减少我们apk包的大小了
提示:如果修改了Android.mk 或build.gradle文件,建议执行一下下面的操作
到这里,我们的编译和运行就已经完成了,那么接下来,我们就要处理Android的Camera了,以及完成编码,推流等操作了。