51好读  ›  专栏  ›  saka

移植jrtplib到安卓平台

saka  · 掘金  ·  · 2018-02-24 12:40

正文

关于rtp协议

RTP协议介绍

实时传输协议RTP(Real-time Transport Protocol)是网络传输协议的一种,构建与TCP/IP之上,广泛用于局域网推送视频与音频的推送。RTP协议本身比较复杂,而且各厂商基本不提供基于RTP协议的sdk,大多数是基于RTMP和RTSP,但是后边两者实时性远不如RTP高。为了实现将安卓手机屏幕录屏取得H264流,并将之分片或者组合发送至电脑端播放器,延时低于1s,最后选择采用RTP协议发送。

RTP报文由两部分组成:报头和有效载荷。RTP报头格式如图所示,其中:

  1. V:RTP协议的版本号,占2位,当前协议版本号为2。
  2. P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
  3. X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
  4. CC:CSRC计数器,占4位,指示CSRC 标识符的个数。
  5. M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
  6. 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
  7. 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
  8. PT: 有效载荷类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等。
  9. 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。接收者通过序列号来检测报文丢失情况,重新排序报文,恢复数据。
  10. 时戳(Timestamp):占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。

这里的同步信源是指产生媒体流的信源,它通过RTP报头中的一个32位数字SSRC标识符来标识,而不依赖于网络地址,接收者将根据SSRC标识符来区分不同的信源,进行RTP报文的分组。特约信源是指当混合器接收到一个或多个同步信源的RTP报文后,经过混合处理产生一个新的组合RTP报文,并把混合器作为组合RTP报文的SSRC,而将原来所有的SSRC都作为CSRC传送给接收者,使接收者知道组成组合报文的各个SSRC。

RTP协议的复杂封包

网络传输的MTU最大值一般是1400-1500字节。rtp推荐使UDP作为传输协议,为了保证数据不丢失,我们需要将H264流的NALU单元限制在MTU以内。H264流的SPS和PPS只占很少的字节,并且在画面变化很少时产生的NAL单元也很小,这时候可能需要组包发送,将两个或者多个NAL单元封装在一个包内发送;当产生的NAL单元超过MTU的限制后,假如每个载体还要传送一个NAL,则可能会丢失数据,导致接收端接收的不是完整的一帧数据,这个时候需要分包发送,将一个NAL单元拆分成两个或者更多个包发送。
想想头都大了,因为分包和组合包

单一NAL单元模式

对于 NALU 的长度小于 MTU 大小的包, 一般采用单一 NAL 单元模式.
对于一个原始的 H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成, 其中 Start Code 用于标示这是一个
NALU 单元的开始, 必须是 “00 00 00 01” 或 “00 00 01”, NALU 头仅一个字节, 其后都是 NALU 单元内容.
打包时去除 “00 00 01” 或 “00 00 00 01” 的开始码, 把其他数据封包的 RTP 包即可.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI|  type   |                                               |
+-+-+-+-+-+-+-+-+                                               |
|                                                               |
|               Bytes 2..n of a Single NAL unit                 |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               :...OPTIONAL RTP padding        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

组合封包模式

当 NALU 的长度特别小时, 可以把几个 NALU 单元封在一个 RTP 包中.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          RTP Header                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR |         NALU 1 Size           | NALU 1 HDR    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         NALU 1 Data                           |
:                                                               :
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               | NALU 2 Size                   | NALU 2 HDR    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         NALU 2 Data                           |
:                                                               :
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               :...OPTIONAL RTP padding        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

分包模式

当NALU的长度超过MTU时,就必须对NALU单元进行分片封包.也称为Fragmentation Units(FUs).


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator  |   FU header   |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
|                                                               |
|                         FU payload                            |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               :...OPTIONAL RTP padding        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这几种模式看着就很复杂,假如是非专业人士很难搞定。网上比较有名的就是ffmpeg和jrtplib,他们都对RTP协议做了较好的封装。这里我使用用C++编写的jrtplib工程移植到安卓平台。

JRTPLIB介绍

jrtplib一个用C ++编写的面向对象库,旨在帮助开发人员使用RFC 3550中描述的实时传输协议(RTP)。

该库使得用户可以使用RTP发送和接收数据,而不用担心SSRC冲突,调度和传输RTCP数据等。用户只需要向库提供要发送的有效载荷数据,并且该库能给用户访问传入的RTP和RTCP数据的权限。

jrtplib支持定义于RFC3550中的RTP协议,它使得发送和接收RTP报文变得异常简单,用户不用担心SSRC冲突,也不用考虑如何传输RTCP数据,因为RTCP功能完全在内部实现,不需用户手动操作。
当发送RTP报文时,用户只需简单的给发送函数提供负载数据;当接收数据时,jrtplib提供了访问传入的RTP和RTCP数据的接口。

目前为止,jrtplib支持以下平台:

*GNU/Linux
*MS-Windows(Win32和WinCE)
*Solaris
当然也可以运行于其他类unix环境。

jthread封装了pthread,提供了一些特定的接口使用起来更方便一点。
jrtplib可以使用jthread库在后台自动轮询传入的数据,所以推荐安装jthread。当然如果没有安装jthread,jrtplib也能正常工作,但是需要用户自己轮询传入的数据了。3.x.x版本的jrtplib至少需要1.3.0版本的jthread。

jrtplib文档地址

jrtplib-github地址

jthread-github地址

两个库全都使用cmake构建,并且系统内提供了对主要平台的支持测试,保证在各个平台正常使用。现在我们需要借助ndk交叉编译为安卓平台的架构。

编译jthread

官方的jthread是一个基于pthead的封装库,用来解决unix平台多线程编程。封装置后调用相对简单,使用jthread可以轮询查询是否接收到rtp包并且取出。

文件结构如下:

└── JThread
    ├── CMakeLists.txt
    ├── ChangeLog
    ├── LICENSE.MIT
    ├── README.md
    ├── TODO
    ├── builddist.sh
    ├── cmake
    │   └── JThreadConfig.cmake.in
    ├── doc
    │   └── manual.tex
    ├── pkgconfig
    │   ├── CMakeLists.txt
    │   └── jthread.pc.in
    ├── sphinxdoc
    │   ├── Makefile
    │   ├── README.md
    │   └── source
    │       ├── _static
    │       ├── _templates
    │       └── conf.py
    └── src
        ├── CMakeLists.txt
        ├── jmutex.h
        ├── jmutexautolock.h
        ├── jthread.h
        ├── jthreadconfig.h.in
        ├── pthread
        │   ├── jmutex.cpp
        │   └── jthread.cpp
        └── win32
            ├── jmutex.cpp
            └── jthread.cpp

文件很少,主要的源文件在src文件夹下,这里的文件是实现jthread的主要代码,我们不用管,重点关注该文件夹下的 CMakeLists.txt 文件。doc、pkgconfi和spinxdoc三个文件夹是unix平台安装的文件,也可以不用管。主要的是CMakeLists.txt文件和cmake文件夹下的 JThreadConfig.cmake.in 。下面一起分析一下上面提到的三个需要关注的文件。
关于cmake的详细文档请参 考官方文档 或者 这个系列文章

根目录下的CMakeList.txt文件

该文件是整个工程构建系统的入口。

cmake_minimum_required(VERSION 3.0)

project(jthread)
set(VERSION 1.3.3)

来看看这三个蛋疼的玩意,指定了使用cmake的最小版本,构建的工程的名称以及该库的版本。

include(CheckCXXSourceCompiles)

这个就牛逼了,是用来测试c源码是否包含某个功能,稍后在src文件夹下CMakeLists.txt文件介绍会用到。

set (_DEFAULT_LIBRARY_INSTALL_DIR lib)
if (EXISTS "${CMAKE_INSTALL_PREFIX}/lib32/" AND CMAKE_SIZEOF_VOID_P EQUAL 4)
	set (_DEFAULT_LIBRARY_INSTALL_DIR lib32)
elseif (EXISTS "${CMAKE_INSTALL_PREFIX}/lib64/" AND CMAKE_SIZEOF_VOID_P EQUAL 8)
	set (_DEFAULT_LIBRARY_INSTALL_DIR lib64)
endif ()

set(LIBRARY_INSTALL_DIR "${_DEFAULT_LIBRARY_INSTALL_DIR}" CACHE PATH "Library installation directory")
if(NOT IS_ABSOLUTE "${LIBRARY_INSTALL_DIR}")
	set(LIBRARY_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${LIBRARY_INSTALL_DIR}")
endif()

这几个是关于库的安装路径设置,暂时忽略,因为我们在交叉编译的时候会手动指定安装目录。

find_package(Threads)
if (NOT CMAKE_USE_WIN32_THREADS_INIT)
	if (NOT CMAKE_USE_PTHREADS_INIT)
		message(FATAL_ERROR "Can find neither pthread support nor Win32 thread support")
	endif (NOT CMAKE_USE_PTHREADS_INIT)
endif (NOT CMAKE_USE_WIN32_THREADS_INIT)

find_package可以用来查询系统是否包含某个库,包含会返回变量成功,不包含变量返回NOTFOUND。
这个是用来寻找threads库,假如找到会生成以下变量:

CMAKE_THREAD_LIBS_INIT     - 库名称
CMAKE_USE_SPROC_INIT       - 使用sproc?
CMAKE_USE_WIN32_THREADS_INIT - 使用WIN32 threads?
CMAKE_USE_PTHREADS_INIT    - 使用pthreads
CMAKE_HP_PTHREADS_INIT     - 使用pthreads

稍后会用到其中的一些变量。
最后一行:

add_subdirectory(src)

这个是提供执行构建src文件夹下的CMakeLists.txt文件的一个入口,添加这句后src中的cmake文件就可以引用这个cmake文件中的一些变量和环境设置。

src文件夹下的CMakeLists.txt文件

这个文件的内比较多,挑一些重要的讲一讲:

if (NOT MSVC OR JTHREAD_COMPILE_STATIC)
	set(JTHREAD_INSTALLTARGETS jthread-static)
	add_library(jthread-static STATIC ${SOURCES} ${HEADERS})
	set_target_properties(jthread-static PROPERTIES OUTPUT_NAME jthread)
	set_target_properties(jthread-static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
	target_link_libraries(jthread-static ${CMAKE_THREAD_LIBS_INIT})
endif()

if ((NOT MSVC AND NOT JTHREAD_COMPILE_STATIC_ONLY) OR (MSVC AND NOT JTHREAD_COMPILE_STATIC))
	add_library(jthread-shared SHARED ${SOURCES} ${HEADERS})
	set_target_properties(jthread-shared PROPERTIES VERSION ${VERSION})
	set_target_properties(jthread-shared PROPERTIES OUTPUT_NAME jthread)
	set_target_properties(jthread-shared PROPERTIES CLEAN_DIRECT_OUTPUT 1)
	set(JTHREAD_INSTALLTARGETS ${JTHREAD_INSTALLTARGETS} jthread-shared)
	target_link_libraries(jthread-shared ${CMAKE_THREAD_LIBS_INIT})
endif ()

这段代码的作用是在非windows平台下构建动态库和静态库,假如不需要全部构建只需要构建动态库或者静态库,注释掉其中的一部分即可(上边构建静态库.a文件,下边构建动态库.so文件)。

install(FILES ${HEADERS} DESTINATION include/jthread)
install(TARGETS ${JTHREAD_INSTALLTARGETS} DESTINATION ${LIBRARY_INSTALL_DIR})

这两句是用来安装文件,cmake系统默认的安装路径是/usr/local/*,假如设置了 CMAKE_INSTALL_PREFIX ,则会改变默认的安装路径到该变量指向的路径。这两句话的作用是将变量 HEADERS 包含的文件-主要是头文件-安装到 CMAKE_INSTALL_PREFIX 指向路径的 include/jthread 文件夹下,同理,动态库和静态库会安装到指向路径的 lib 文件夹下。

关于上边提到的测试, include(CheckCXXSourceCompiles)

# Test pthread_cancel (doesn't exits on Android)
set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_THREAD_LIBS_INIT})
check_cxx_source_compiles("#include <pthread.h>\nint main(void) { pthread_cancel((pthread_t)0); return 0; }" JTHREAD_HAVE_PTHREADCANCEL)
if (NOT JTHREAD_HAVE_PTHREADCANCEL)
    #message("Enabling JTHREAD_SKIP_PTHREAD_CANCEL")
	add_definitions(-DJTHREAD_SKIP_PTHREAD_CANCEL)
else ()
	#message("pthread_cancel appears to exist")
endif (NOT JTHREAD_HAVE_PTHREADCANCEL)

这个功能是测试pthread是否有cancel函数,并将结构存储在 JTHREAD_HAVE_PTHREADCANCEL 变量中,程序执行成功则会返回true,执行失败则会返回false,然后执行if中的语句,来添加跳过执行cancel函数的变量,这样在编译后程序就不会执行cancel函数了。

来看一下另一端代码:

configure_file("${PROJECT_SOURCE_DIR}/cmake/JThreadConfig.cmake.in" 
	       "${PROJECT_BINARY_DIR}/cmake/JThreadConfig.cmake")

configure_file的作用是将第一个参数指向的文件复制到第二个参数指向的路径,也会重命名该文件,并且在生成的文件中替换源文件中的变量。看一下源文件,指定了三个变量,后两个变量会随着你的参数指定而变化。

set(JTHREAD_FOUND 1)

set(JTHREAD_INCLUDE_DIRS "${CMAKE_INSTALL_PREFIX}/include")

set(JTHREAD_LIBRARIES ${JTHREAD_LIBS_CMAKECONFIG})

这个文件本身并没有太大的作用,主要目的是让jrtplib寻找到jthread库。实际在jrtplib中定义的find宏中提供了两种方式,不是必须采用这种方式。

编译全abi的jthread库

用ndk-build来构建时比较简单的,但是要自己编写.mk文件。我闲的蛋疼写了一个bash脚本来生成全abi的动态库和静态库。安卓支持的abi版本共有7种,armeabi arm64-v8a armeabi-v7a mips mips64 x86 x86_64,我们需要分别生成这些版本的动态库.so文件与静态库.o文件。







请到「今天看啥」查看全文