ONVIF协议流媒体发布器

实现基于ONVIF规范的IP摄像头发现、管理、播放、视频发布

Posted by SnailStudio on December 20, 2016

“Yeah It’s on. ”

项目介绍

截图预览

本项目实现了基于ONVIF规范的IP摄像头发现、管理、播放、视频发布,基于MVP + Rxjava + Dagger2架构。可用于远程实时监控、网络直播等方面。

项目源码:https://github.com/xuqiqiang/mediadevice


ONVIF简介

2008年5月,由安讯士联合博世及索尼公司三方宣布将携手共同成立一个国际开放型网络视频产品标准网络接口开发论坛,取名为ONVIF(Open Network Video Interface Forum,开放型网络视频接口论坛),并以公开、开放的原则共同制定开放性行业标准。

ONVIF标准将为网络视频设备之间的信息交换定义通用协议,包括装置搜寻、实时视频、音频、元数据和控制信息等。网络视频产品由此所能提供的多种可能性,使终端用户,集成商,顾问和生产厂商能够轻松地从中获益,并获得高性价比、更灵活的解决方案、市场扩张的机会以及更低的风险。

ONVIF规范描述了网络视频的模型、接口、数据类型以及数据交互的模式。并复用了一些现有的标准,如WS系列标准等。ONVIF规范的目标是实现一个网络视频框架协议,使不同厂商所生产的网络视频产品(包括摄录前端、录像设备等)完全互通。

ONVIF规范中设备管理和控制部分所定义的接口均以Web Services的形式提供。ONVIF规范涵盖了完全的XML及WSDL的定义。每一个支持ONVIF规范的终端设备均须提供与功能相应的Web Service。服务端与客户端的数据交互采用SOAP协议。ONVIF中的其他部分比如音视频流则通过RTP/RTSP进行 。


SOAP协议介绍

SOAP(简单对象访问协议)是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息。

SOAP消息基本结构:

<?xml
    version="1.0"?>
<soap:Envelope
    xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
    soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
 
<soap:Header>
</soap:Header>
 
<soap:Body>
<soap:Fault>
</soap:Fault>
</soap:Body>
</soap:Envelope>

主要在web服务中运用。

通过SOAP协议获取rtsp视频流的流程


流媒体协议介绍

流媒体协议介绍

(参考文档 RFC3550/RFC3551)

RTP:实时传输协议(Real-time Transport Protocol),一般用于多媒体数据的传输。 RTP传输音频/视频数据,如果是PLAY,Server发送到Client端,如果是RECORD,可以由Client发送到Server。 整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTP控制协议(即RTCP)。

RTCP:实时传输控制协议(RTP Control Protocol),同RTP一起用于数据传输的监视,控制功能。 RTCP包括Sender Report和Receiver Report,用来进行音频/视频的同步以及其他用途,是一种控制协议。

RTSP:实时流协议(Real Time Streaming Protocol,RTSP),用于多媒体数据流的控制,如播放,暂停等。 RTSP的请求主要有DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN,OPTIONS等,顾名思义可以知道起对话和控制作用
RTSP的对话过程中SETUP可以确定RTP/RTCP使用的端口,PLAY/PAUSE/TEARDOWN可以开始或者停止RTP的发送,等等。

RTP/RTCP相对于底层传输层,和RTSP,SIP等上层协议一起可以实现视频会议,视频直播等应用。
RTP/RTSP/RTCP的区别用一句简单的话总结:RTSP发起/终结流媒体、RTP传输流媒体数据、RTCP对RTP进行控制、同步。

RTMP协议:被Flash用于对象、视频、音频的传输的协议(Real Time Messaging Protocol)。这个协议建立在TCP协议或者轮询HTTP协议之上. RTMP协议就像一个用来装数据包的容器,这些数据既可以是AMF格式的数据,也可以是FLV中的视/音频数据。一个单一的连接可以通过不同的通道传输多路网络流。这些通道中的包都是按照固定大小的包传输的。


基于ffmpeg的rtsp取流分析

ffmpeg代码分析

  • ffmpeg rtsp代码位置
    liveformat/rtsp.c 这个是udp里面的实现
    liveformat/rtspdec.c

avformat_open_input

avformat_open_input >>
 rtsp_read_header >>
ff_rtsp_connect>>
ff_rtsp_setup_input_streams
  • ff_rtsp_connect
    rtsp的控制方式
    RTSP_MODE_PLAIN 普通rtsp
    RTSP_MODE_TUNNEL 基于HTTP
  • 分割url:proto, auth, host ,port ,path

  • rtsp HTTP打开连接
    ffmpeg会在http的请求报文头发送如下格式

"x-sessioncookie: %s\r\n"
                 "Accept: application/x-rtsp-tunnelled\r\n"
                 "Pragma: no-cache\r\n"
                 "Cache-Control: no-cache\r\n",

x-sessioncookie:只是一个随机的值
即使不看方法体也能猜出来

     snprintf(sessioncookie, sizeof(sessioncookie), "%08x%08x",
                 av_get_random_seed(), av_get_random_seed());
  • rtsp tcp打开连接
    简单的建立信道

  • rtsp 发送OPTIONS命令,获取支持的命令
    rtsp 传输方式

    RAW/RAW ::Support receiving plain data over UDP without any RTP encapsulation
    RTP/AVP: RTP transport / audio , video , control protocol
    x-pn-tng: 不懂

  • rtsp 发送 SETUP命令
    status_code = 461 / Unsupported protocol /
    status_
    验证c s两端的协议,必须一致(大概是怕我们乱改代码)

interleaved :
The channel identifier is defined in the Transport header with the
interleaved parameter(Section 12.39).
个人理解,tcp是一个持续的流,所以需要一个interleaved来分割包

  • rsp 通道建立
    ff_rtsp_make_setup_request

ff_read_packet

rtsp_read_packet >> ff_rtsp_fetch_packet

rtp playload格式

参考链接:《rtp学习》

packetization-mode 0 : 单一NALU

单NAL单元包(Single NAL Unit Packet):负载中只包含单一的NAL单元。NAL头的类型等同于原始的NAL单元类型,也就是,1~23的范围。此种包必须只包含单个NAL单元,聚合包和分片单元都不能在这种包内使用。必须按解码顺序发送.

packetization-mode 1 : non-interleaved 非交错封包模式

用于聚合多个NAL单元为单个RTP负载。这种包存在四种版本:单时间聚合包(STAP-A),单时间聚合包(STAP-B),多时间聚合包(MTAP)带16位偏移(MTAP16),多时间聚合包(MTAP)带24位偏移(MTAP24). NAL类型号分配给STAP-A,STAP-B,MTAP16和MTAP24分别为24,25,26,27。

packetization-mode 2 : interleaved 交错封包模式

用于分割单一的NAL单元为多个RTP包,共有两个版本,FU-A和FU-B. 它们的NAL类型号分别为28,29.
分片的原因是为了传输大于64KB的NAL单元。
分片针对单个NAL单元,而不是聚合包。
FU不能嵌套。
FU的时戳设置为被分片NAL单元的NALU时间
FU-A包括一个字节的FU indicator+一个字节的FU header+FU payload
FU-B比FU-A多了一个字节的decoding order number(DON).
FU-B必须只被用在交叉包装模式下NAL分片的第一片。换句话说,在交叉包装模式,每个NALU被分片为FU-B+FU-A+FU-A+…+FU-A

ff_rtsp_fetch_packet

ff_rtsp_fetch_packet 根据lower_transport 调用不同协议read_packet的方法

执行流程

OPTIONS-> DESCRIBE->RTSP/SDP
  • SDP(Session Description Protocol)
    SDP 会话描述协议

  • 简单报文分析

    v=0 协议版本
    o=- 1476286318262307 1 IN IP4 192.168.31.194 (所有者/创建者和会话标识符)
    s=H.264 Video, streamed by the LIVE555 Media Server (会话名称)
    i=bb.264(会话信息)
    t=0 0(会话活动时间)
    a=tool:LIVE555 Streaming Media v2016.09.22
    a=type:broadcast 
    a=control:*
    a=range:npt=0-
    a=x-qt-text-nam:H.264 Video, streamed by the LIVE555 Media Server
    a=x-qt-text-inf:bb.264
    m=video 0 RTP/AVP 96(媒体名称和传输地址)
    c=IN IP4 0.0.0.0(连接信息 ― 如果包含在所有媒体中,则不需要该字段)
    b=AS:500(带宽信息)
    a=rtpmap:96 H264/90000
    a=fmtp:96 packetization-mode=1;profile-level-id=640028;sprop-parametersets=Z2QAKKzZQHgCJ+WEAAADAAQAAAMA8Dxgxlg=,aOvjyyLA
    a=control:track1
  • a=rtpmap
    H264 编码名称
    90000 时钟频率
    96 dynamically assigned
    "a=rtpmap" 行中的编码名称必须是 "H264".
    "a=rtpmap" 行中的时钟频率必须是 90000.
  • packetization-mode:表示支持的封包模式.
    当 packetization-mode 的值为 0 时或不存在时, 必须使用单一 NALU 单元模式.
    当 packetization-mode 的值为 1 时必须使用非交错(non-interleaved)封包模式.
    当 packetization-mode 的值为 2 时必须使用交错(interleaved)封包模式.

  • sprop-parameter-sets: SPS,PPS
    这个参数可以用于传输 H.264 的序列参数集和图像参数 NAL 单元. 这个参数的值
    采用 Base64 进行编码. 不同的参数集间用","号隔开

  • profile-level-id:
    这个参数用于指示 H.264 流的 profile 类型和级别. 由 Base16(十六进制) 表示的 3 个字节. 第一个字节表示 H.264 的 Profile 类型, 第三个字节表示 H.264 的 Profile 级别。


使用librtmp推送h264数据

libRTMP使用说明

名称
librtmp − RTMPDump Real-Time Messaging Protocol API

RTMPDump RTMP(librtmp, -lrtmp)
简介
#include<librtmp/rtmp.h>
描述
实时流协议(Real-TimeMessaging Protocol,RTMP)是用于互联网上传输视音频数据的网络协议。本API提供了支持RTMP, RTMPT,RTMPE, RTMP RTMPS以及以上几种协议的变种(RTMPTE, RTMPTS)协议所需的大部分客户端功能以及少量的服务器功能。尽管Adobe公司已经公布了RTMP协议规范(RTMP specification),但是本工程并不是通过Adobe的协议规范而是通过逆向工程的方式完成的。因此,它的运行方式可能和公布的协议规范有所偏离,但是一般情况下它和Adobe的客户端的运行方式是一模一样的。
RTMPDump 软件包含一个基本的客户端:rtmpdump,一些示例服务器和一个用来提供对RTMP协议进行支持的库(libRTMP)。本页面对libRTMP的函数进行一个概述。
这些函数可以在 -lrtmp 库中找到。其他还有很多函数,但是还没有为这些函数写文档。
基本的操作如下文所述。
RTMP_Alloc() :用于创建一个RTMP会话的句柄。
RTMP_Init():初始化句柄。
RTMP_SetupURL():设置会话的参数。
RTMP_Connect():建立RTMP链接中的网络连接(NetConnection)。
RTMP_ConnectStream():建立RTMP链接中的网络流(NetStream)。
RTMP_Read():读取RTMP流的内容。
客户端可以在调用RTMP_Connect()之前调用RTMP_EnableWrite(),然后在会话开始之后调用RTMP_Write()。
RTMP_Pause():流播放的时候可以用于暂停和继续
RTMP_Seek():改变流播放的位置
RTMP_Read()返回0 字节的时候,代表流已经读取完毕,而后可以调用RTMP_Close()
RTMP_Free():用于清理会话。
所有的数据都使用 FLV 格式进行传输。一个基本的会话需要一个RTMP URL。RTMP URL 格式如下所示:
rtmp[t][e|s]://hostname[:port][/app[/playpath]]
支持普通的,隧道的,以及加密的会话。
其他附加的选项可以在URL的后面添加以空格为间隔符的“key=value”形式的字符串。
选项
网络(Network)参数
这些选项定义了如何连接一个流媒体服务器。
socks=host:port
使用指定 SOCKS4代理。
连接(Connection)参数
这些选项定义了RTMP连接(Connect)请求消息的内容。如果没有提供正确的值,流媒体服务器会拒绝连接请求。
app=name
连接到RTMP的应用名,覆盖RTMP URL中的app。有时rtmpdumpURL 无法正确自动解析app名称。这时必须使用该选项。
tcUrl=url
目标流的URL。默认是 rtmp[t][e|s]://host[:port]/app.
pageUrl=url
流媒体所在的网页的URL。默认情况下没有被发送的价值。
swfUrl=url
流媒体使用的SWF播放器的的URL。默认情况下没有被发送的价值。
flashVer=version
swf播放器使用的Flash版本. 默认是”LNX 10,0,32,18”。
conn=type:data
任意AMF数据追加到connect,类型说明,
B 布尔型
N 数字
S 字符串
O 对象
Z 空
对于布尔型必须是0或1作为FALSE或TRUE,
对于对象数据必须以0或1分别作为结束和开始的标制,子对象中数据项前加前缀类型N,并指定值名称,例如:
-C B:1 -C S:authMe -C O:1 -C NN:code:1.23-C NS:flag:ok -C O:0
会话(Session)参数
这些选项在连接成功后生效。
playpath=path
覆盖RTMP URL解析的playpath,rtmpdump有时不能正确解析,通过该选项明确。
playlist=0|1
在play命令之前发生set_playlist命令。否则播放列表将会值包含playpath。
live=0|1
指定媒体是实时流。在实时流中没有恢复和搜索。
subscribe=path
订阅的实时流名称。默认playpath。
start=num
开始到流的秒数(num),实时流无效。
stop=num
停止到流的秒数(num)。
buffer=num
设置缓冲时间,单位毫秒。 默认值36000000。
timeout=num
num秒后没有收到任何数据会话超时,默认值120。
安全(Security)参数
这些选项处理额外的身份验证,来自服务器的请求。
token=key
输入安全令牌响应,如果服务器需要使用安全令牌验证。
jtv=JSON
JSON令牌用于传统Justin.tv服务器 ,调用NetStream.Authenticate.UsherToken。
swfVfy=0|1
swf播放器的URL,此选项将替换所以三个–swfUrl,–swfhash, and –swfsize选项。使用此选项时,swf播放器将从指定URL检索,并自动计算哈希和大小。此外信息缓存在一个swfinfo文件在用户主目录,所以它在每次rtmpdump运行时,并不需要检索和重新计算。swfinfo记录URL,生成时间,修改SWF文件时间,它的大小,它的哈希,默认情况下,缓冲信息用于30天,然后重新检测。
swfAge=days
指定使用缓存的swf信息天数,然后重新检查,使用0为经常检查,如果检查显示swf具有相同的修改时间戳,它不会被再次检索。
例子
RTMP_SetupURL()使用的一个例子字符串:
“rtmp://flashserver:1935/ondemand/thefile swfUrl=http://flashserver/player.swfswfVfy=1”
作者
Andrej Stepanchuk, Howard Chu,The Flvstreamer Team
http://rtmpdump.mplayerhq.hu

以上说明由leixiaohua1020翻译
原文地址:《RTMPDump Real-Time Messaging Protocol API》

librtmp发布RTMP流的流程图

使用librtmp发布RTMP流的可以使用两种API:RTMP_SendPacket()和RTMP_Write()。使用RTMP_SendPacket()发布流的时候的函数执行流程图如下图所示。使用RTMP_Write()发布流的时候的函数执行流程图相差不大。

流程图

H.264帧的划分

对于 H.264 而言每帧的界定符为 00 00 00 01 或者 00 00 01

比如下面的 h264 文件片断这就包含三帧数据:

00 00 00 01 67 42 C0 28 DA 01 E0 08 9F 96 10 00
00 03 00 10 00 00 03 01 48 F1 83 2A 00 00 00 01
68 CE 3C 80 00 00 01 06 05 FF FF 5D DC 45 E9 BD
E6 D9 48 B7 96 2C D8 20 D9 23 EE EF …

第一帧是 00 00 00 01 67 42 C0 28 DA 01 E0 08 9F 96 10 00 00 03 00 10 00 00 03 01 48 F1 83 2A
第二帧是 00 00 00 01 68 CE 3C 80
第三帧是 00 00 01 06 05 FF FF 5D DC 45 E9 BD E6 D9 48 B7 96 2C D8 20 D9 23 EE EF ..

帧类型有:
NAL_SLICE = 1
NAL_SLICE_DPA = 2
NAL_SLICE_DPB = 3
NAL_SLICE_DPC = 4
NAL_SLICE_IDR = 5
NAL_SEI = 6
NAL_SPS = 7
NAL_PPS = 8
NAL_AUD = 9
NAL_FILLER = 12,

我们发送 RTMP 数据时只需要知道四种帧类型,其它类型我都把它规类成非关键帧。分别是
NAL_SPS(7), sps 帧
NAL_PPS(8), pps 帧
NAL_SLICE_IDR(5), 关键帧
NAL_SLICE(1) 非关键帧

帧类型的方式判断为界面符后首字节的低四位。
第一帧的帧类型为: 0x67 & 0x1F = 7,这是一个 SPS 帧
第二帧的帧类型为: 0x68 & 0x1F = 8,这是一个 PPS 帧
第三帧的帧类型为: 0x06 & 0x1F = 6,这是一个 SEI 帧

以上是我们利用帧界定符划分帧,并可以判断每一个帧的类型。

注意:如果是压缩图像成 H264 帧,我们就可不必进行帧界定,因为每一次压缩的输出都明确了该帧的大小(包括界定符),每一次的压缩的结果可能包函多帧。

H264视频的编码信息

如果我们只是简单的将压缩数据打包发送给 RTMP 服务器,那么 RTMP 服务器是不可以对数据进行解码和播放的,在这之前我们要将视频的编码信息发送给 RTMP 服务器。很多人可能苦于寻找下面的三个编码参数而不得要领。其实要想得到也是很简单的。

SPS

对于 H264 而言,SPS 就是编码后的第一帧。如果是读取 H264 文件,就是第一个帧界定符与第二帧界定符中间的数据长度是 4。

PPS

对于 H264 而言,PPS 就是编码后的第二帧。如果是读取 H264 文件,就是第二个帧界定符与第三帧界定符中间的数据,长度不固定。

/*分配与初始化*/
rtmp = RTMP_Alloc();
RTMP_Init(rtmp);

/*设置URL*/
if (RTMP_SetupURL(rtmp, rtmp_url) == FALSE) {
    log(LOG_ERR, "RTMP_SetupURL() error!");
    RTMP_Free(rtmp);
    return -1;
}

/*设置可写,即发布流,这个函数必须在连接前使用,否则无效*/
RTMP_EnableWrite(rtmp);

/*连接服务器*/
if (RTMP_Connect(rtmp, NULL) == FALSE) {
    log(LOG_ERR, "RTMP_Connect() error!");
    RTMP_Free(rtmp);
    return -1;
} 

/*连接流*/
if (RTMP_ConnectStream(rtmp, 0) == FALSE) {
    log(LOG_ERR, "RTMP_ConnectStream() error!");
    RTMP_Close(rtmp);
    RTMP_Free(rtmp);
    return -1;
}
/*定义包头长度,RTMP_MAX_HEADER_SIZE为rtmp.h中定义值为18*/

#define RTMP_HEAD_SIZE (sizeof(RTMPPacket) + RTMP_MAX_HEADER_SIZE)

RTMPPacket * packet;
unsigned char * body;

/*分配包内存和初始化,len为包体长度*/
packet = (RTMPPacket *) malloc(RTMP_HEAD_SIZE+len);
memset(packet, 0, RTMP_HEAD_SIZE);

/*包体内存*/
packet->m_body = (char *) packet + RTMP_HEAD_SIZE;
body = (unsigned char *) packet->m_body;
packet->m_nBodySize = len;

/*
 * 此处省略包体填充
 */
packet->m_hasAbsTimestamp = 0;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; /*此处为类型有两种一种是音频,一种是视频*/
packet->m_nInfoField2 = rtmp->m_stream_id;
packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_nTimeStamp = timeoffset;

/*发送*/
if (RTMP_IsConnected(rtmp)) {
    ret = RTMP_SendPacket(rtmp, packet, TRUE); /*TRUE为放进发送队列,FALSE是不放进发送队列,直接发送*/
}

/*释放内存*/
free(packet);
/*关闭与释放*/
RTMP_Close(rtmp);
RTMP_Free(rtmp);

H.264编码信息帧

H.264 的编码信息帧是发送给 RTMP 服务器称为 AVC sequence header,RTMP 服务器只有收到 AVC sequence header 中的 sps, pps 才能解析后续发送的 H264 帧。

int send_video_sps_pps()
{
    RTMPPacket * packet;
    unsigned char * body;
    int i;

    packet = (RTMPPacket *) malloc(RTMP_HEAD_SIZE+1024);
    memset(packet, 0, RTMP_HEAD_SIZE);

    packet->m_body = (char *) packet + RTMP_HEAD_SIZE;
    body = (unsigned char *) packet->m_body;

    memcpy(winsys->pps,buf,len);
    winsys->pps_len = len;

    i = 0;
    body[i++] = 0x17;
    body[i++] = 0x00;

    body[i++] = 0x00;
    body[i++] = 0x00;
    body[i++] = 0x00;

    /*AVCDecoderConfigurationRecord*/
    body[i++] = 0x01;
    body[i++] = sps[1];
    body[i++] = sps[2];
    body[i++] = sps[3];
    body[i++] = 0xff;

    /*sps*/
    body[i++] = 0xe1;
    body[i++] = (sps_len >> 8) & 0xff;
    body[i++] = sps_len & 0xff;
    memcpy(&body[i], sps, sps_len);
    i += sps_len;

    /*pps*/
    body[i++] = 0x01;
    body[i++] = (pps_len >> 8) & 0xff;
    body[i++] = (pps_len) & 0xff;
    memcpy(&body[i], pps, pps_len);
    i += pps_len;

    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = i;
    packet->m_nChannel = 0x04;
    packet->m_nTimeStamp = 0;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    packet->m_nInfoField2 = rtmp->m_stream_id;

    /*调用发送接口*/
    RTMP_SendPacket(rtmp, packet, TRUE);
    free(packet);    

    return 0;
}

sps 与 pps 怎么获取到呢?

在前面已经说过,H264 的第 1 帧是 sps 帧, pps 是第 2 帧。

我们在编码时会调用如下接口

size = x264_encoder_encode(cx->hd, &nal, &n, pic, &pout);

int i, last;
for (i = 0, last = 0; i < n; i++)
{
    if (nal[i].i_type == NAL_SPS)
    {
        sps_len = nal[i].i_payload - 4;
        memcpy(sps, nal[i].p_payload + 4, sps_len);
    }
    else if (nal[i].i_type == NAL_PPS)
    {
        pps_len = nal[i].i_payload - 4;
        memcpy(pps, nal[i].p_payload + 4, pps_len);

        /*发送sps pps*/
        send_video_sps_pps();

    }
    else
    {

        /*发送普通帧*/
        send_rtmp_video(nal[i].p_payload, nal[i].i_payload);
    }
    last += nal[i].i_payload;
}

我完全可以不用知道 sps, pps 的具体意义:)

H.264关键帧与非关键帧

视频压缩中,每帧代表一幅静止的图像。而在实际压缩时,会采取各种算法减少数据的容量,其中IPB就是最常见的。

简单地说,I帧是关键帧,属于帧内压缩。就是和AVI的压缩是一样的。 P是向前搜索的意思。B是双向搜索。他们都是基于I帧来压缩数据。

I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)

P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)

B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累~。

我们知道I和P的解码算法比较简单,资源占用也比较少,I只要自己完成就行了,P呢,也只需要解码器把前一个画面缓存一下,遇到P时就使用之前缓存的画面就好了,如果视频流只有I和P,解码器可以不管后面的数据,边读边解码,线性前进,大家很舒服。 但网络上的电影很多都采用了B帧,因为B帧记录的是前后帧的差别,比P帧能节约更多的空间,但这样一来,文件小了,解码器就麻烦了,因为在解码时,不仅要用之前缓存的画面,还要知道下一个I或者P的画面(也就是说要预读预解码),而且,B帧不能简单地丢掉,因为B帧其实也包含了画面信息,如果简单丢掉,并用之前的画面简单重复,就会造成画面卡(其实就是丢帧了),并且由于网络上的电影为了节约空间,往往使用相当多的B帧,B帧用的多,对不支持B帧的播放器就造成更大的困扰,画面也就越卡。

int send_rtmp_video(unsigned char * buf,int len)
{
    int type;
    long timeoffset;
    RTMPPacket *packet;
    unsigned char *body;

    timeoffset = GetTickCount() - start_time; /*start_time为开始直播时的时间戳*/

    /*去掉帧界定符*/
    if (buf[2] == 0x00) { /*00 00 00 01*/
            buf += 4;
            len -= 4;
    } else if (buf[2] == 0x01){ /*00 00 01*/
            buf += 3;
            len -= 3;
    }
    type = buf[0] & 0x1f;

    packet = (RTMPPacket *) base_malloc(RTMP_HEAD_SIZE + len + 9);
    memset(packet, 0, RTMP_HEAD_SIZE);

    packet->m_body = (char *) packet + RTMP_HEAD_SIZE;
    packet->m_nBodySize = len + 9;

    /*send video packet*/
    body = (unsigned char *) packet->m_body;
    memset(body, 0, len + 9);

    /*关键帧*/
    body[0] = 0x27;
    if (type == NAL_SLICE_IDR) {
        /*非关键帧*/
        body[0] = 0x17;
    }

    body[1] = 0x01; /*nal unit*/
    body[2] = 0x00;
    body[3] = 0x00;
    body[4] = 0x00;

    body[5] = (len >> 24) & 0xff;
    body[6] = (len >> 16) & 0xff;
    body[7] = (len >>  8) & 0xff;
    body[8] = (len ) & 0xff;

    /*copy data*/
    memcpy(&body[9], buf, len);

    packet->m_hasAbsTimestamp = 0;
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nInfoField2 = winsys->rtmp->m_stream_id;
    packet->m_nChannel = 0x04;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nTimeStamp = timeoffset;

    /*调用发送接口*/
    RTMP_SendPacket(rtmp, packet, TRUE);
    free(packet);
}

这里要说明一下:
比如说 x264_encoder_encode 输出了 6 帧。
分别是 sps 帧, pps 帧,关键帧,非关键帧,非关键帧,非关键帧。
发送结果应该是, sps,pps 合成为一帧调用发送函数,剩下 4 帧,除去每个 nal 的界定符,分别发送每一个 nal。

在 libx264 中每一次调用 x264_encoder_encode 输出了 n 个帧,我们要从这 n 个帧里找出 sps 和 pps,剩下的分次全部发送 nal,sps 与 pps 的帧界定符都是 00 00 00 01,而普通帧可能是 00 00 00 01 也有可能 00 00 01。

如果 x264_encoder_encode 里没有 sps 帧与 pps 帧,则结果除去第一帧的界定符所以帧做为一个整体调用发送函数,它们的类型是由第一帧类型决定。

另外,H264 的流的第 1 帧一定是 sps 帧(包含帧界定符为 8 个字节),第 2 帧一定是 pps帧。

至此使用librtmp推送h264数据的流程结束了。

要注意的几件事: libRTMP 多线程发送很容易出现问题,可以改成队列发送。将填充好的 packet 通过消息或者其它方式发送给其它线程,发送线程统一发送即可。


视频预览

本项目的视频预览模块加入了ijkplayer

ijkplayer 是一个基于 ffplay 的轻量级 Android/iOS 视频播放器。实现了跨平台功能,API易于集成;编译配置可裁剪,方便控制安装包大小;支持硬件加速解码,更加省电;提供Android平台下应用弹幕集成的解决方案,此方案目前已用于美拍和斗鱼APP。

本项目在android/contrib中集成了ffmpeg、openssl、polarssl、rtmpdump,并且实现了交叉编译脚本。具体实现可以参考我的这篇文章。

另外,项目中解决了ijkplayer播放rtsp流结束的时候未发送teardown标志的问题,具体实现可以参考我的这篇文章


项目架构

本项目按照Android-CleanArchitecture架构标准,基于MVP、Rxjava、Dagger2等框架,使得代码结构清晰。

为什么倾向于cleanArchitecture,那一定是有他的道理的。对比传统开发的MVC开发方式,你会得到以下好处:

  • 代码复用性更高

  • 更易于测试

  • 耦合度更小

由于篇幅过长,本文不再详细说明。关于探讨CleanArchitecture架构方面的文章很多,但是究其源头无非都是出自uncle-bob 叔叔的这篇《The Clean Architecture》


参考文档

《RFC3550》
《RFC3551》
《rtp学习》
《Video File Format Specification Version 10》
《H264视频通过RTMP直播》
《RTMPDump Real-Time Messaging Protocol API》
《The Clean Architecture》


项目源码

https://github.com/xuqiqiang/mediadevice


截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

截图预览

—— SnailStudio 后记于 2017.5