直播用到的技术

服务器端搭建

Nginx

Nginx是一个高性能的HTTP和反向代理服务器,用来处理前端(Andorid ios Web)过来的请求,以前在一台服务器上需要部署多个服务,需要通过端口号执行访问的具体服务,部署完Nginx之后就不需要这样了,可以用Nginx来导流和分发。

下载地址:http://nginx.org/en/download.html

在Linux上下载当前最新版本并解压

wget 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

下载最新版本并解压

wget 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的解压目录,给它添加直播模块

#--prefix代表编译到哪个目录  --add-module 指向rtmp模块目录
./configure --prefix=./bin --add-module=../nginx-rtmp-module-1.2.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中有示例

cd bin/conf
vim nginx.conf 
worker_processes  1;

error_log  logs/error.log debug;

events {
#最大连接数
    worker_connections  1024;
}

rtmp {
    server {
        listen 1935;
        application myapp {
            live on;
            #大于5秒不响应丢弃
            drop_idle_publisher 5s;
        }
    }
}
http {
    server {
        listen      8081;
        location /stat {
            rtmp_stat all;
            rtmp_stat_stylesheet stat.xsl;
        }
        location /stat.xsl {
        #这里改成自己的路径
            root /root/nginx-rtmp-module-1.2.1/;
        }
        location /control {
            rtmp_control all;
        }
        location /rtmp-publisher {
        #这里改成自己的路径
            root /root/nginx-rtmp-module-1.2.1/test;
        }

        location / {
        #这里改成自己的路径
            root /root/nginx-rtmp-module-1.2.1/test/www;
        }
    }
}

查看端口是否被占用:
netstat -tunlp|grep 8081

回到Nginx解压根目录,打开nginx

bin/sbin//nginx

浏览器打开后台网址 58.320.63.116:8081/stat如果能够打开说明搭建成功了。

视频推流

graph LR
摄像头-->视频通道
麦克风-->音频通道
视频通道-->打包
音频通道-->打包
打包-->Native推流

摄像头搜集到视频源数据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目录下面

编译失败

  F:\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 

说是在hashswf.c的56行找不到,这个主要是用来加密的,加密意味着效率慢,所以通常的直播是不需要加密的,可以忽略掉这一部分。

进入hashswf.c中可以看到:的引用是在#ifdef CRYPTO之后,只有定义了CRYPTO这个参数才能走下面引用openssl的方法,所以可以想办法绕过这里,可以直接注释掉CRYPTO也可以通过下面的方法

在CMakeLists.txt文件中传递一个NO_CRYPTO参数

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文件在上传到服务器中

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的步骤:

  1. RTMP_Alloc 申请内存
  2. RTMP_Init 初始化
  3. RTMP_SetupURL 设置地址
  4. RTMP_EnableWrite 开启输出模式
  5. RTMP_Connect 连接服务器
  6. RTMP_ConnectStream 连接流
  7. RTMP_SendPacket 发从音频和视频数据包
  8. RTMP_Close 关闭连接
  9. 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://www.audiocoding.com/

下载地址:

https://sourceforge.net/projects/faac/files/faac-src/faac-1.29/faac-1.29.9.2.tar.gz/

编译faac

#!/bin/bash
PREFIX=`pwd`/android/armeabi-v7a
NDK_ROOT=/root/android-ndk-r17c
TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
CROSS_COMPILE=$TOOLCHAIN/bin/arm-linux-androideabi

FLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -D__ANDROID_API__=17 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -std=c++11  -O0  -fPIC"

export CC="$CROSS_COMPILE-gcc --sysroot=$NDK_ROOT/platforms/android-17/arch-arm"
export CFLAGS="$FLAGS"


./configure \
--prefix=$PREFIX \
--host=arm-linux \
--with-pic \
--enable-shared=no

make clean
make install

音视频播放和同步

使用FFmpeg来播放视频 使用OpenSL ES 播放音频

FFmpeg:

AvformatContext:获取视频流和音频流 字幕流,这是经过编码后的压缩数据

AVcodecContext:解压的上下文,可以获得宽度高度编码信息等

AVcondec:解码器 解码成yuv数据

SwsContext :转换上下文 视频缩放等操作

  FAILED: 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.

原因依赖顺序有问题:后面的库会用到前面库的方法

原来的

avcodec avfilter avformat avutil swresample swscale

改后

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编译脚本

#!/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

多个版本一起编译

#!/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.使用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的执行流程

  1. 创建音频引擎
  2. 设置混音器
  3. 创建播放器
  4. 设置缓冲队列和回调函数
  5. 设置播放状态
  6. 启动回调函数

音视频同步以音频为准,以视频为准,自定义时间为准。一般音频为准,因为人的耳朵对音频比较敏感。

音频丢一帧耳朵能听出卡顿,视频丢一帧人眼一般发现不了。

帧率 解码速度 渲染速度都会影响同步

ffmpeg中提供了时间戳 DTS和PTS

DTS Decoding Time Stamp 解码时间戳,告诉解码器packet的解码顺序

PTS presentation Time Stamp 显示时间戳,从packet中解码出来的数据的显示顺序

在音频中两者是相同的,但是在视频中由于B帧(双向预测)的存在,会造成解码顺序与显示顺序不相同。视频中DTS和PTS不一定相同。

视频中B帧是根据I帧和P帧计算出来的,所以解码顺序有可能不一样。比如I帧和P帧先解码然后在解码B帧。

你可能感兴趣的:(c/c++)