直播用到的技术
服务器端搭建
Nginx
Nginx是一个高性能的HTTP和反向代理服务器,用来处理前端(Andorid ios Web)过来的请求,以前在一台服务器上需要部署多个服务,需要通过端口号执行访问的具体服务,部署完Nginx之后就不需要这样了,可以用Nginx来导流和分发。
下载地址:http://nginx.org/en/download.html
在Linux上下载当前最新版本并解压1
2wget http://nginx.org/download/nginx-1.17.0.tar.gz
tar -zxvf nginx-1.17.0.tar.gz
使用nginx-rtmp-module作为直播模块 https://github.com/arut/nginx-rtmp-module
下载最新版本并解压1
2
3
4
5wget https://codeload.github.com/arut/nginx-rtmp-module/tar.gz/v1.2.1
tar -zxvf v1.2.1
//或者
wget https://github.com/arut/nginx-rtmp-module/archive/v1.2.1.tar.gz
tar -zxvf v1.2.1.tar.gz
进入到nginx的解压目录,给它添加直播模块
1 | #--prefix代表编译到哪个目录 --add-module 指向rtmp模块目录 |
开始安装1
make && make install
有时候可能会出现zlib,pcre,openssl这几个库找不到可以根据下面的链接中的安装
https://blog.csdn.net/z920954494/article/details/52132125
安装成功后会生成一个bin目录,进入bin/conf修改nginx.conf文件配置rtmp和http协议。在nginx-rtmp-module-1.2.1/test/nginx.conf
中有示例1
2cd bin/conf
vim nginx.conf
1 | worker_processes 1; |
查看端口是否被占用:
netstat -tunlp|grep 8081
回到Nginx解压根目录,打开nginx1
bin/sbin//nginx
浏览器打开后台网址 58.320.63.116:8081/stat如果能够打开说明搭建成功了。
视频推流
1 | graph LR |
摄像头搜集到视频源数据Android中是NV21格式,它是YUV中的一种这个源数据非常大,需要对其编码压缩,可以使用h264协议进行压缩。
H264是连续帧,包括I帧 B帧 P帧
- I帧:也是关键帧,它保留了一幅图的完整信息
- P帧:根据I帧形成的,表示与I帧之间的差别
- B帧:I帧和P帧之间的,由前面的I帧或者后面的P帧预测而来
封装成pcket,使用rtmp协议传输,rtmpDump工具
rtmpDump 下载地址:http://rtmpdump.mplayerhq.hu/download/
下载解压,把libtrmp复制到我们工程的cpp目录下面
编译失败1
2
3F:\sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang.exe --target=x86_64-none-linux-android21 --gcc-toolchain=F:/sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=F:/sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -O0 -fno-limit-debug-info -fPIC -MD -MT librtmp/CMakeFiles/rtmp.dir/hashswf.c.o -MF librtmp\CMakeFiles\rtmp.dir\hashswf.c.o.d -o librtmp/CMakeFiles/rtmp.dir/hashswf.c.o -c D:/android/A1/MyPusher/app/src/main/cpp/librtmp/hashswf.c
D:/android/A1/MyPusher/app/src/main/cpp/librtmp/hashswf.c:56:10: fatal error: 'openssl/ssl.h' file not found
#include <openssl/ssl.h>
说是在hashswf.c的56行找不到<openssl/ssl.h>,这个主要是用来加密的,加密意味着效率慢,所以通常的直播是不需要加密的,可以忽略掉这一部分。
进入hashswf.c中可以看到:<openssl/ssl.h>的引用是在#ifdef CRYPTO
之后,只有定义了CRYPTO这个参数才能走下面引用openssl的方法,所以可以想办法绕过这里,可以直接注释掉CRYPTO也可以通过下面的方法
在CMakeLists.txt文件中传递一个NO_CRYPTO参数1
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
-D后面就是传递的参数,传递之后再编译就不会出错了。
使用x264工具来负责h264的编解码
x264地址 :https://www.videolan.org/developers/x264.html
x264的源码很多,就不能跟前面rtmpdump一样直接导入AndoroidStudio中使用了,因为源码太多会编译非常慢慢到你怀疑人生,所以需要下载到Linux服务器在服务器中编译完在导入到AndoridStudio中。
在服务器中直接通过git下载,clone之前要安装git,或者直接官网下载zip文件在上传到服务器中1
git clone https://code.videolan.org/videolan/x264.git
为什么视频编码采用YUV而不是RGB
- RGB原理:定义RGB从颜色发光的原理来设定,任何一种颜色都可以通过红、绿、蓝三种颜色混合而成,亮度等于参与混合的颜色之和,越混合亮度越高,即混合加法,RGB24指RGB三个各占8位
- YUV原理:YUV只要用于优化色彩视频信号的传输,与RGB视频信号传输相比,它的最大优点是占用极少的频宽。RGB要求三个独立的视频信号同时传输。Y表示亮度,U和V表示色度
NV21 YUVI420
为什么要对视频进行编码?
视频是由一帧帧的图像组成,一般的视频为了不让用户感到卡顿,一秒钟至少需要16帧的画面(一般是30帧),假如该视频是一个1280720分辨率的视频,那么不经过编码的一秒钟大小结果是:1280720*60≈843.75M,非常大不适合保存和传输。
H264的编码规则
在相邻的几幅图像中,一般差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内,所以对于一段变化不大的图像,我们可以先编码出一个完整的图像的帧A
随后,B帧就不用编码全部的图像,只写入和A帧的差别,这样B帧的大小就只有完成帧的1/10或者更小。B帧之后的C帧如果变化不大,我们可以继续以参考B帧的方法编码C帧如此循环
这样的一段图像成为一个序列。序列就是有相同特点的一段数据。当某个图像的差别跟前一个很大的时候无法参考前面的来生成,就结束这个序列开始下一段序列。
编码后的I帧B帧P帧也不能直接发送,还需要通过帧内压缩来进一步减小其大小。
NALU单元设计,H264原始码流(裸流)是由一个接一个的NALU组成的。
NALU包括NALU头和RBSP(切片),。NALU头就相当于一辆汽车的头,切片就相当于它拉的货物,他们两个组成一个完整的一个火车运送到目的地,H264的原始流就是一辆一辆的这样的货车运送。
什么是切片:H264默认使用16*16大小的区域作为一个宏块,若干个宏块就可以组成一个切片。
SPS和PPS
SPS和PPS包含了初始化H264解码器所需要的信息参数,包括编码所用的profile,level图像宽和高,deblock滤波器等。
SPS:序列参数集 PPS:图像参数集
在H264中,都是以 “0x00 0x00 0x01” 或者 “0x00 0x00 0x00 0x01” 为开始编码的,找到开始码之后使用开始码之后的第一个字节的底5位判断是否为7sps或者8pps。
RTMPDump的地址:http://rtmpdump.mplayerhq.hu/
使用RTMPDump的步骤:
- RTMP_Alloc 申请内存
- RTMP_Init 初始化
- RTMP_SetupURL 设置地址
- RTMP_EnableWrite 开启输出模式
- RTMP_Connect 连接服务器
- RTMP_ConnectStream 连接流
- RTMP_SendPacket 发从音频和视频数据包
- RTMP_Close 关闭连接
- RTMP_Free 释放
音频推流
一般使用aac编码音频
AAC:
高级音频编码(Advanced Audio Coding),出现于1997年,基于MPEG-2的音频编码技术,目的是取代MP3格式。2000年,MPEG-4标准出现后,AAC重新集成了其特性,为了区别于传统的MPEG-2 AAC又称为MPEG-4 AAC。相对于mp3,AAC格式的音质更佳,文件更小。
AAC的音频文件格式有 ADIF & ADTS
aac一种是在连续的音频数据的开始处存有解码信息,一种是在每一小段音频数据头部存放7个或者9个字节的头信息用于播放器解码。
RTMP推流需要的是aac的裸数据。所以如果编码出adts格式的数据,需要去掉7个或者9个字节的adts头信息。
类似于推送视频,第一个包总是包含sps和pps的音频序列包,推送音频同样第一个包是包含了接下来数据的格式的音频序列包
下载地址:
https://sourceforge.net/projects/faac/files/faac-src/faac-1.29/faac-1.29.9.2.tar.gz/
编译faac
1 | #!/bin/bash |
音视频播放和同步
使用FFmpeg来播放视频 使用OpenSL ES 播放音频
FFmpeg:
AvformatContext:获取视频流和音频流 字幕流,这是经过编码后的压缩数据
AVcodecContext:解压的上下文,可以获得宽度高度编码信息等
AVcondec:解码器 解码成yuv数据
SwsContext :转换上下文 视频缩放等操作1
2
3
4
5
6
7
8
9
10
11
12FAILED: D:/android/A1/MyPlayer/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libmyplayer.so
cmd.exe /C "cd . && F:\sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe --target=armv7-none-linux-androideabi19 --gcc-toolchain=F:/sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=F:/sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -fPIC -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -mfpu=vfpv3-d16 -fno-addrsig -march=armv7-a -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++ -LD:/android/A1/MyPlayer/app/src/main/cpp/../../../libs/armeabi-v7a -O0 -fno-limit-debug-info -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--warn-shared-textrel -Wl,--fatal-warnings -Wl,--exclude-libs,libunwind.a -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -shared -Wl,-soname,libmyplayer.so -o D:\android\A1\MyPlayer\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libmyplayer.so CMakeFiles/myplayer.dir/native-lib.cpp.o -lavcodec -lavfilter -lavformat -lavutil -lswresample -lswscale -landroid -lz -lOpenSLES -llog -latomic -lm && cd ."
libavcodec/v4l2_buffers.c:439: error: undefined reference to 'mmap64'
libavformat/utils.c:5610: error: undefined reference to 'av_bitstream_filter_filter'
libavformat/codec2.c:74: error: undefined reference to 'avpriv_codec2_mode_bit_rate'
libavformat/codec2.c:75: error: undefined reference to 'avpriv_codec2_mode_frame_size'
libavformat/codec2.c:76: error: undefined reference to 'avpriv_codec2_mode_block_align'
libavformat/hls.c:840: error: undefined reference to 'atof'
libavformat/spdifdec.c:63: error: undefined reference to 'av_adts_header_parse'
libavformat/hlsproto.c:141: error: undefined reference to 'atof'
clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
ninja: build stopped: subcommand failed.
原因依赖顺序有问题:后面的库会用到前面库的方法
原来的1
avcodec avfilter avformat avutil swresample swscale
改后1
avfilter avformat avcodec avutil swresample swscale
https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md
https://github.com/android-ndk/ndk/issues/536
https://www.jianshu.com/p/e0e042a10000
- avcodec:编解码
- avformat:封装格式处理
- avfilter:滤镜特效处理
- avutil:工具库
- swresample:音频采样数据格式转换
- swscale:视频像素数据格式转换
armv8a编译脚本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31#!/bin/bash
NDK_ROOT=/root/ff/NDK/android-ndk-r17c
PREFIX=./android/arm64-v8a
#TOOLCHAIN 变量指向ndk中的交叉编译gcc所在的目录
TOOLCHAIN=$NDK_ROOT/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64
FLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/aarch64-linux-android -D__ANDROID_API__=21 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -Wa,--noexecstack -Wformat -Werror=format-security -O0 -fPIC"
# --disable-cli : 关闭命令行
# 其他和ffmpeg一样
./configure \
--prefix=$PREFIX \
--enable-small \
--disable-programs \
--disable-avdevice \
--disable-encoders \
--disable-muxers \
--disable-filters \
--enable-cross-compile \
--cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
--enable-shared \
--enable-static \
--sysroot=$NDK_ROOT/platforms/android-21/arch-arm64 \
--extra-cflags="$FLAGS $INCLUDES" \
--extra-cflags="-isysroot $NDK_ROOT/sysroot" \
--arch=aarch64 \
--target-os=android
make clean
make install
多个版本一起编译1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129#!/bin/sh
MY_LIBS_NAME=ffmpeg-4.0
MY_DIR=ffmpeg-4.0
# cd ./${MY_DIR}
#编译的过程中产生的中间件的存放目录,为了区分编译目录,源码目录,install目录
MY_BUILD_DIR=binary
NDK_PATH=/home/as/Android/android-ndk-r15c
BUILD_PLATFORM=linux-x86_64
TOOLCHAIN_VERSION=4.9
ANDROID_VERSION=24
ANDROID_ARMV5_CFLAGS="-march=armv5te"
ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon" #-mfloat-abi=hard -mfpu=vfpv3-d16 #-mfloat-abi=hard -mfpu=vfp
ANDROID_ARMV8_CFLAGS="-march=armv8-a"
ANDROID_X86_CFLAGS="-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32"
ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"
# params($1:arch,$2:arch_abi,$3:host,$4:cross_prefix,$5:cflags)
build_bin() {
echo "-------------------start build $2-------------------------"
ARCH=$1 # arm arm64 x86 x86_64
ANDROID_ARCH_ABI=$2 # armeabi armeabi-v7a x86 mips
PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/
HOST=$3
SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-${ARCH}
CFALGS=$5
TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}
CROSS_PREFIX=${TOOLCHAIN}/bin/$4-
# build 中间件
BUILD_DIR=./${MY_BUILD_DIR}/${ANDROID_ARCH_ABI}
echo "pwd==$(pwd)"
echo "ARCH==${ARCH}"
echo "PREFIX==${PREFIX}"
echo "HOST==${HOST}"
echo "SYSROOT=${SYSROOT}"
echo "CFALGS=$5"
echo "CFALGS=${CFALGS}"
echo "TOOLCHAIN==${TOOLCHAIN}"
echo "CROSS_PREFIX=${CROSS_PREFIX}"
#echo "-------------------------按任意键继续---------------------"
#read -n 1
#echo "-------------------------继续执行-------------------------"
mkdir -p ${BUILD_DIR} #创建当前arch_abi的编译目录,比如:binary/armeabi-v7a
cd ${BUILD_DIR} #此处 进了当前arch_abi的2级编译目录
sh ../../${MY_DIR}/configure \
--prefix=${PREFIX} \
--target-os=linux \
--arch=${ARCH} \
--sysroot=$SYSROOT \
--enable-cross-compile \
--cross-prefix=${CROSS_PREFIX} \
--extra-cflags="$CFALGS -Os -fPIC -DANDROID -Wfatal-errors -Wno-deprecated" \
--extra-cxxflags="-D__thumb__ -fexceptions -frtti" \
--extra-ldflags="-L${SYSROOT}/usr/lib" \
--enable-shared \
--enable-asm \
--enable-neon \
--disable-encoders \
--enable-encoder=aac \
--enable-encoder=mjpeg \
--enable-encoder=png \
--disable-decoders \
--enable-decoder=aac \
--enable-decoder=aac_latm \
--enable-decoder=h264 \
--enable-decoder=mpeg4 \
--enable-decoder=mjpeg \
--enable-decoder=png \
--disable-demuxers \
--enable-demuxer=image2 \
--enable-demuxer=h264 \
--enable-demuxer=aac \
--disable-parsers \
--enable-parser=aac \
--enable-parser=ac3 \
--enable-parser=h264 \
--enable-gpl \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-symver \
--disable-debug \
--enable-small
make clean
make
make install
#从当前arch_abi编译目录跳出,对应上面的cd ${BUILD_DIR},以便function多次执行
cd ../../
echo "-------------------$2 build end-------------------------"
}
# build armeabi
build_bin arm armeabi arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV5_CFLAGS"
#build armeabi-v7a
build_bin arm armeabi-v7a arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV7_CFLAGS"
#build arm64-v8a
build_bin arm64 arm64-v8a aarch64-linux-android aarch64-linux-android "$ANDROID_ARMV8_CFLAGS"
#build x86
build_bin x86 x86 x86 i686-linux-android "$ANDROID_X86_CFLAGS"
#build x86_64
build_bin x86_64 x86_64 x86_64 x86_64-linux-android "$ANDROID_X86_64_CFLAGS"
OpenSL ES
OpenSL ES是一套无授权费跨平台,针对嵌入式系统精心优化的硬件音频加速API,为移动多媒体设备提供标准化、高性能、低响应时间的音频功能实现方法,并实现软硬件音频性能的直接跨平台部署,降低执行难度。促进高级音频时长的发展。
Android播放音频的几种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 //1.使用MediaPlayer播放音频
//直接创建,不需要设置setDataSource
MediaPlayer mMediaPlayer;
mMediaPlayer=MediaPlayer.create(this, R.raw.audio);
mMediaPlayer.start();
//2 使用AudioTrack播放音频
AudioTrack audio = new AudioTrack(
AudioManager.STREAM_MUSIC, // 指定流的类型
32000, // 设置音频数据的採样率 32k,假设是44.1k就是44100
AudioFormat.CHANNEL_OUT_STEREO, // 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道
AudioFormat.ENCODING_PCM_16BIT, // 设置音频数据块是8位还是16位。这里设置为16位。
//3 使用OpenSL ES播放
/混音器
SLObjectItf outputMixObject = NULL;//用SLObjectItf创建混音器接口对象
SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;////创建具体的混音器对象实例
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);//利用引擎接口对象创建混音器接口对象
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);//实现(Realize)混音器接口对象
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);//利用混音器接口对象初始化具体混音器实例
使用OpenSl ES 播放的优势
- C 语言接口,需要在 NDK 下开发,能更好地集成在 native 应用中
- 运行于 native 层,播放 速度极快,延时低 对于音视频同步中以音频为准的最为适合不过
- 减少java层频繁的反射调用,如果通过AudioTrack播放需要将解码后得pcm数据反射java 增加开
销
Android native操作Java的官方例子
https://github.com/googlesamples/android-ndk
OpenSl ES的执行流程
- 创建音频引擎
- 设置混音器
- 创建播放器
- 设置缓冲队列和回调函数
- 设置播放状态
- 启动回调函数
音视频同步以音频为准,以视频为准,自定义时间为准。一般音频为准,因为人的耳朵对音频比较敏感。
音频丢一帧耳朵能听出卡顿,视频丢一帧人眼一般发现不了。
帧率 解码速度 渲染速度都会影响同步
ffmpeg中提供了时间戳 DTS和PTS
DTS Decoding Time Stamp 解码时间戳,告诉解码器packet的解码顺序
PTS presentation Time Stamp 显示时间戳,从packet中解码出来的数据的显示顺序
在音频中两者是相同的,但是在视频中由于B帧(双向预测)的存在,会造成解码顺序与显示顺序不相同。视频中DTS和PTS不一定相同。
视频中B帧是根据I帧和P帧计算出来的,所以解码顺序有可能不一样。比如I帧和P帧先解码然后在解码B帧。