librtmp实时消息传输协议(RTMP)库代码浅析
来源:互联网 发布:口红哪个牌子好 知乎 编辑:程序博客网 时间:2024/05/16 18:52
没事碰到了librtmp库,这个库是ffmpeg的依赖库,用来接收,发布RTMP协议格式的数据。
代码在这里:git clone git://git.ffmpeg.org/rtmpdump
先看一段通过librtmp.so库下载RTMP源发布的数据的例子,从rtmpdump中抽取出来。使用的大体流程如下:
- RTMP_Init主要就初始化了一下RTMP*rtmp变量的成员。
- RTMP_SetupURL 函数将rtmp源地址的端口,app,等url参数进行解析,设置到rtmp变量中。比如这样的地址: rtmp://host[:port]/path swfUrl=url tcUrl=url 。
- RTMP_SetBufferMS 函数设置一下缓冲大小;
- RTMP_Connect函数完成了连接的建立,一级RTMP协议层的应用握手,待会介绍。
- RTMP_ConnectStream总的来说,完成了一个流的创建,以及打开,触发服务端发送数据过来,返回后,服务端应该就开始发送数据了。
- Download 其实是RTMP_Read函数的封装,后者读取服务端的数据返回。
1
RTMP_Init(&rtmp);
//初始化RTMP参数
2
//指定了-i 参数,直接设置URL
3
if
(RTMP_SetupURL(&rtmp, fullUrl.av_val) == FALSE) {
4
RTMP_Log(RTMP_LOGERROR,
"Couldn't parse URL: %s"
, fullUrl.av_val);
5
return
RD_FAILED;
6
}
7
8
rtmp.Link.timeout = timeout ;
9
/* Try to keep the stream moving if it pauses on us */
10
if
(!bLiveStream )
11
rtmp.Link.lFlags |= RTMP_LF_BUFX;
12
13
while
(!RTMP_ctrlC)
14
{
15
RTMP_Log(RTMP_LOGDEBUG,
"Setting buffer time to: %dms"
, DEF_BUFTIME);
16
RTMP_SetBufferMS(&rtmp, DEF_BUFTIME);
//告诉服务器帮我缓存多久
17
18
RTMP_LogPrintf(
"Connecting ...\n"
);
19
if
(!RTMP_Connect(&rtmp, NULL)) {
//建立连接,发送"connect"
20
nStatus = RD_NO_CONNECT;
21
break
;
22
}
23
RTMP_Log(RTMP_LOGINFO,
"Connected..."
);
24
25
//处理服务端返回的各种控制消息包,比如收到connect的result后就进行createStream,以及发送play(test)消息
26
if
(!RTMP_ConnectStream(&rtmp, 0)) {
//一旦返回,表示服务端开始发送数据了.可以play了
27
nStatus = RD_FAILED;
28
break
;
29
}
30
31
nStatus = Download(&rtmp, file, bStdoutMode, bLiveStream );
32
33
if
(nStatus != RD_INCOMPLETE || !RTMP_IsTimedout(&rtmp) || bLiveStream)
34
break
;
35
}
为了方便理解,贴一个本文的数据包图:
一、建立协议连接
下面来详细介绍下RTMP_Connect函数的工作。
先看代码,下面RTMP_Connect的工作是连接对端,进行握手,并且发送”connect” 控制消息,附带一些app,tcurl等参数。其实时调用了2个函数完成工作的:RTMP_Connect0, RTMP_Connect1 。
1
int
RTMP_Connect(RTMP *r, RTMPPacket *cp)
2
{
//连接对端,进行握手,并且发送"connect" 控制消息,附带一些app,tcurl等参数
3
struct
sockaddr_in service;
4
if
(!r->Link.hostname.av_len)
5
return
FALSE;
6
7
memset
(&service, 0,
sizeof
(
struct
sockaddr_in));
8
service.sin_family = AF_INET;
9
10
if
(r->Link.socksport)
11
{
12
/* Connect via SOCKS */
13
if
(!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport))
14
return
FALSE;
15
}
16
else
17
{
18
/* Connect directly */
19
if
(!add_addr_info(&service, &r->Link.hostname, r->Link.port))
20
return
FALSE;
21
}
22
23
if
(!RTMP_Connect0(r, (
struct
sockaddr *)&service))
//建立一个socket连接
24
return
FALSE;
25
26
r->m_bSendCounter = TRUE;
27
28
return
RTMP_Connect1(r, cp);
//进行C0-2/S0-2协议握手,发送connect命令
29
}
其中RTMP_Connect0 函数比较简单,标准的socket, conect 流程,另外设置了一下TCP_NODELAY选项,方便小包发送等。以及SO_RCVTIMEO读超时,这部分属于基本的TCP层面的连接;
RTMP_Connect1 函数则完成类似HTTP层面的RTMP协议的连接建立过程。首先是HandShake 握手。RTMP的握手是通过客户端跟服务端互相发送数据包来完成的,每人3个数据包,名之为C0,C1,C2 以及S0,S1, S2。其发送数据有严格的限制的。因为互相依赖。这个在官方文档中有详细的介绍,不多说。
对于librtmp来说,可能的一种流程是:
CLIENT SERVER
C0,C1 —>
<— S0, S1,S2
C2 –>
具体看一下代码,比较长。
1
static
int
HandShake(RTMP *r,
int
FP9HandShake)
2
{
//C0,C1 -- S0, S1, S2 -- C2 消息握手协议
3
int
i;
4
uint32_t uptime, suptime;
5
int
bMatch;
6
char
type;
7
char
clientbuf[RTMP_SIG_SIZE + 1], *clientsig = clientbuf + 1;
8
char
serversig[RTMP_SIG_SIZE];
9
10
clientbuf[0] = 0x03;
//C0, 一个字节。03代表协议版本号为3 /* not encrypted */
11
12
uptime = htonl(RTMP_GetTime());
//这是一个时间戳,放在C1消息头部
13
memcpy
(clientsig, &uptime, 4);
14
memset
(&clientsig[4], 0, 4);
//后面放4个字节的空数据然后就是随机数据
15
16
//后面是随机数据,总共1536字节的C0消息
17
#ifdef _DEBUG
18
for
(i = 8; i < RTMP_SIG_SIZE; i++)
19
clientsig[i] = 0xff;
20
#else
21
for
(i = 8; i < RTMP_SIG_SIZE; i++)
22
clientsig[i] = (
char
)(
rand
() % 256);
//发送C0, C1消息
23
#endif
24
25
if
(!WriteN(r, clientbuf, RTMP_SIG_SIZE + 1))
26
return
FALSE;
27
28
//下面读一个字节也就是S0消息,看协议是否一样
29
if
(ReadN(r, &type, 1) != 1)
/* 0x03 or 0x06 */
30
return
FALSE;
31
RTMP_Log(RTMP_LOGDEBUG,
"%s: Type Answer : %02X"
, __FUNCTION__, type);
32
if
(type != clientbuf[0])
//C/S版本不一致
33
RTMP_Log(RTMP_LOGWARNING,
"%s: Type mismatch: client sent %d, server answered %d"
, __FUNCTION__, clientbuf[0], type);
34
35
//读取S1消息,里面有服务器运行时间
36
if
(ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
37
return
FALSE;
38
39
/* decode server response */
40
41
memcpy
(&suptime, serversig, 4);
42
suptime = ntohl(suptime);
43
44
RTMP_Log(RTMP_LOGDEBUG,
"%s: Server Uptime : %d"
, __FUNCTION__, suptime);
45
RTMP_Log(RTMP_LOGDEBUG,
"%s: FMS Version : %d.%d.%d.%d"
, __FUNCTION__, serversig[4], serversig[5], serversig[6], serversig[7]);
46
47
/* 2nd part of handshake */
48
if
(!WriteN(r, serversig, RTMP_SIG_SIZE))
//发送C2消息,内容就等于S1消息的内容。
49
return
FALSE;
50
51
//读取S2消息
52
if
(ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
53
return
FALSE;
54
55
bMatch = (
memcmp
(serversig, clientsig, RTMP_SIG_SIZE) == 0);
56
if
(!bMatch)
//服务端返回的S2消息必须跟C1消息一致才行
57
{
58
RTMP_Log(RTMP_LOGWARNING,
"%s, client signature does not match!"
, __FUNCTION__);
59
}
60
return
TRUE;
61
}
握手的目的其实是互相沟通一下支持的协议版本号,服务器时间戳等。确保连接的对端真的是RTMP支持的。
发送请求给服务端。
然后就是SendConnectPacket的工作了。总结一句其功能是成一个“connect消息以及其app,tcurl等参数,然后调用RTMP_SendPacket函数将其数据发送出去。
到这里连接建立完成了。
二、准备数据通道
RTMP_ConnectStream完成了通道的建立。其处理服务端返回的各种控制消息包,比如收到connect的result后就进行createStream,以及发送play(test)消息。一旦返回,表示服务端开始发送数据了.可以play了。
函数本身比较简单,就是一个while循环,不断的调用RTMP_ReadPacket读取服务端发送过来的数据包进行相应的处理。直到m_bPlaying变老变为TRUE为止,也就是可以播放的时候为止。数据包的处理函数为RTMP_ClientPacket。
1
int
RTMP_ConnectStream(RTMP *r,
int
seekTime)
2
{
//循环读取服务端发送过来的各种消息,比如window ack**, set peer bandwidth, set chunk size, _result等
3
//直到接收到了play
4
RTMPPacket packet = { 0 };
5
6
/* seekTime was already set by SetupStream / SetupURL.
7
* This is only needed by ReconnectStream.
8
*/
9
if
(seekTime > 0)
10
r->Link.seekTime = seekTime;
11
12
r->m_mediaChannel = 0;
13
14
//一个个包的读取,直到服务端告诉我说可以play了为止
15
while
(!r->m_bPlaying && RTMP_IsConnected(r) && RTMP_ReadPacket(r, &packet))
16
{
17
if
(RTMPPacket_IsReady(&packet))
//是否读取完毕。((a)->m_nBytesRead == (a)->m_nBodySize)
18
{
19
if
(!packet.m_nBodySize)
20
continue
;
21
if
((packet.m_packetType == RTMP_PACKET_TYPE_AUDIO) ||
22
(packet.m_packetType == RTMP_PACKET_TYPE_VIDEO) ||
23
(packet.m_packetType == RTMP_PACKET_TYPE_INFO))
24
{
25
RTMP_Log(RTMP_LOGWARNING,
"Received FLV packet before play()! Ignoring."
);
26
RTMPPacket_Free(&packet);
27
continue
;
28
}
29
30
RTMP_ClientPacket(r, &packet);
//处理一下这个数据包,其实里面就是处理服务端发送过来的各种消息等。直到接受到了play/publish
31
RTMPPacket_Free(&packet);
32
}
33
}
34
//返回当前是否接收到了play/publish 或者stopd等
35
return
r->m_bPlaying;
36
}
RTMP_ReadPacket 跟Send类似,函数比较长,基本是处理RTMP数据包RTMPPacket的包头,包体的读写等碎碎代码。真正处理事件的函数为RTMP_ClientPacket。
RTMP_ClientPacket函数是一个很大的数据包分发器。负责将不同类型m_packetType的数据包传递给对应的函数进行处理。比如:
- RTMP_PACKET_TYPE_CHUNK_SIZE 块大小设置消息 HandleChangeChunkSize;
- RTMP_PACKET_TYPE_CONTROL 控制消息 HandleCtrl ;
- RTMP_PACKET_TYPE_AUDIO 音频消息 HandleAudio;
- RTMP_PACKET_TYPE_INFO 元数据设置消息 HandleMetadata;
- RTMP_PACKET_TYPE_INVOKE 远程过程调用 HandleInvoke;
其中比较重要的是HandleInvoke 远程过程调用。其里面实际是个状态机。
前面说过,建立连接握手的时候,客户端回发送connect字符串以及必要的参数给服务端。然后服务端会返回_result消息。当客户端收到_result消息后,会从消息里面取出其消息号,从而在r->m_methodCalls[i].name 中找到对应发送的消息是什么消息。从而客户端能够确认发送的那条消息被服务端处理了。进而可以进行后续的处理了。来看HandleInvoke开头的代码。
1
static
int
HandleInvoke(RTMP *r,
const
char
*body, unsigned
int
nBodySize){
2
AMFObject obj;
3
AVal method;
4
double
txn;
5
int
ret = 0, nRes;
6
7
nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
8
if
(nRes < 0){
9
RTMP_Log(RTMP_LOGERROR,
"%s, error decoding invoke packet"
, __FUNCTION__);
10
return
0;
11
}
12
13
AMF_Dump(&obj);
14
AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
15
txn = AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1));
16
RTMP_Log(RTMP_LOGDEBUG,
"%s, server invoking <%s>"
, __FUNCTION__, method.av_val);
17
18
if
(AVMATCH(&method, &av__result))
19
{
//接收到服务端返回的一个_result包,所以我们需要找到这个包对应的那条命令,从而处理这条命令的对应事件。
20
//比如我们之前发送了个connect给服务端,服务端必然会返回_result,然后我们异步收到result后,会调用
21
//RTMP_SendServerBW,RTMP_SendCtrl,以及RTMP_SendCreateStream来创建一个stream
22
AVal methodInvoked = {0};
23
int
i;
24
25
for
(i=0; i<r->m_numCalls; i++) {
//找到这条指令对应的触发的方法
26
if
(r->m_methodCalls[i].num == (
int
)txn) {
27
methodInvoked = r->m_methodCalls[i].name;
28
AV_erase(r->m_methodCalls, &r->m_numCalls, i, FALSE);
29
break
;
30
}
31
}
上面可以看出,librtmp发送出一条需要得到服务端返回结果的消息的时候,会将消息名称记录在m_methodCalls数组上面,其下标就是告诉服务端的消息id。从而每次收到_result的时候就能知道对那个的是哪条消息methodInvoked。
然后就可以进行对应的处理了,举个例子:在之前发送connect的时候,body部分的第二个元素为一个整数,代表一个唯一ID,这里是1,如下图:
服务端对此数据包的回包会是如下的样子:
注意蓝底的Number 1, 他会跟上面的connect(live)消息对应的。因此methodInvoked变量就能等于connect,所以HandleInvoke函数会进入到如下的分支:
1
//下面根据不同的方法进行不同的处理
2
if
(AVMATCH(&methodInvoked, &av_connect))
3
{
4
if
(r->Link.protocol & RTMP_FEATURE_WRITE)
5
{
6
SendReleaseStream(r);
7
SendFCPublish(r);
8
}
9
else
10
{
//告诉服务端,我们的期望是什么,窗口大小,等
11
RTMP_SendServerBW(r);
12
RTMP_SendCtrl(r, 3, 0, 300);
13
}
14
RTMP_SendCreateStream(r);
//因为服务端同意了我们的connect,所以这里发送createStream创建一个流
15
//创建完成后,会再次进如这个函数从而走到下面的av_createStream分支,从而发送play过去
16
17
if
(!(r->Link.protocol & RTMP_FEATURE_WRITE))
18
{
19
/* Authenticate on Justin.tv legacy servers before sending FCSubscribe */
20
if
(r->Link.usherToken.av_len)
21
SendUsherToken(r, &r->Link.usherToken);
22
/* Send the FCSubscribe if live stream or if subscribepath is set */
23
if
(r->Link.subscribepath.av_len)
24
SendFCSubscribe(r, &r->Link.subscribepath);
25
else
if
(r->Link.lFlags & RTMP_LF_LIVE)
26
SendFCSubscribe(r, &r->Link.playpath);
27
}
28
}
29
else
if
(AVMATCH(&methodInvoked, &av_createStream))
上面的分支在服务端同意客户端的connect请求后,客户端调用。
根据流的配置类型不同,进行不同的处理,比如如果是播放的话,那么就会调用SendReleaseStream,以及SendFCPublish发送publish消息;
否则会调用RTMP_SendServerBW设置缓冲大小,也就是图中的“Window Acknowledgement Size 5000000” 。 然后就是RTMP_SendCtrl设置缓冲时间;
之后就会调用RTMP_SendCreateStream函数,发送注明的流创建过程。发送createStream消息给服务端,创建数据传输通道。当然这里只是发送了数据,什么时候能够确定创建成功呢?答案很简单:当接收到服务端的数据包后,如果其为过程调用,且为_result,并且AVMATCH(&methodInvoked, &av_createStream)的时候,就代表创建成功。看如下代码:
1
else
if
(AVMATCH(&methodInvoked, &av_createStream))
2
{
3
r->m_stream_id = (
int
)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));
4
5
if
(r->Link.protocol & RTMP_FEATURE_WRITE)
6
{
//如果是要发送,那么高尚服务端,我们要发数据
7
SendPublish(r);
8
}
9
else
10
{
//否则告诉他我们要接受数据
11
if
(r->Link.lFlags & RTMP_LF_PLST)
12
SendPlaylist(r);
13
SendPlay(r);
//发送play过去,
14
RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
//以及我们的buf大小
15
}
16
}
17
else
if
(AVMATCH(&methodInvoked, &av_play) ||
18
AVMATCH(&methodInvoked, &av_publish))
19
{
//接收到了play的回复,那么标记为play
20
r->m_bPlaying = TRUE;
21
}
22
free
(methodInvoked.av_val);
23
}
createStream消息确认收到后,客户端就是发送SendPlay 请求开始接收数据,或者SendPublish请求开始发布数据;
此后再经过几次简短的消息传输,比如:onStatus(‘NetStream.Play.Start’) | |RtmpSampleAccess() | onMetaData() 等,真正的数据就能够开始接收了。也就是服务端开始发送数据了。通信的信道已经建立好。
三、读取数据
连接经过漫长的过程建立起来后,数据读取比较简短,只需要调用nRead = RTMP_Read(rtmp, buffer, bufferSize)函数不断的读取数据就行。这些数据就是发送方放入RTMP通道里面的数据了。
所以这部分其实就等于:通道已经建立,读使用RTMP_Read,发送使用RTMP_SendPacket等。
介绍的差不多了,再细致的后续有时间再补上。基本框架就在这里。过段时间看看nginx_rtmp_module模块学习一下
- librtmp实时消息传输协议(RTMP)库代码浅析
- librtmp实时消息传输协议(RTMP)库代码浅析
- librtmp实时消息传输协议(RTMP)库代码浅析
- librtmp实时消息传输协议(RTMP)库代码浅析
- 实时消息传输协议 RTMP(Real Time Messaging Protocol)
- 实时消息传输协议 RTMP(Real Time Messaging Protocol)
- RTMP——实时消息传送协议协议
- [AS3]RTMP协议(实时消息传送协议)详解(2)
- 实时直播协议:编译RTMPdump(libRTMP)
- 实时传输协议详解
- RTP:实时传输协议
- 实时传输协议 RTCP
- 实时传输协议
- 实时传输协议RTP
- 实时传输协议 RTP
- RTP 实时传输协议
- 实时传输协议详解
- 实时传输协议RTP
- 信用有多重要?欠款200元,多付4万利息!(附逾期补救办法)
- Struts2 转发和重定向的实现方法
- FFmpeg基础知识总结
- C语言中的指针(指针数组 数组指针 函数指针 函数指针数组 指向函数指针数组的指针)
- 第二篇:开发使用 linux 常用命令整理
- librtmp实时消息传输协议(RTMP)库代码浅析
- 第三讲 跳转结果的方式
- SPOJ
- 【PMP认证考试之个人总结】第 4 章 项目范围管理
- 基本网络知识
- Android Studio中使用apt
- day6 linux操作基础命令 第三弹
- BZOJ 4027 [HEOI2015] 兔子与樱花
- 面试例题5—感知器实现and函数