RTMP推流流程详解

通过抓取主播端的rtmp数据包,我们能清晰的看到整个从握手到推流的过程,然后我们来分析一下每一个包的内容吧。

三次握手

handshake c0+c1

具体数据格式可以参考我的这一片博客

具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
//c0
char c0Byte = 0x03;//rtmp版本号
NSData *c0 = [NSData dataWithBytes:&c0Byte length:1];
[self writeData:c0];
//c1
uint8_t *c1Bytes = (uint8_t *)malloc(kRtmpSignatureSize);
memset(c1Bytes, 0, 4 + 4);
NSData *c1 = [NSData dataWithBytes:c1Bytes length:kRtmpSignatureSize];
free(c1Bytes);
[self writeData:c1];

handshake c2

代码实现:

1
2
3
4
5
6
NSData *s1 = [self.handShake subdataWithRange:NSMakeRange(0, kRtmpSignatureSize)];
//c2
uint8_t *s1Bytes = (uint8_t *)s1.bytes;
memset(s1Bytes + 4, 0, 4);
NSData *c2 = [NSData dataWithBytes:s1Bytes length:s1.length];
[self writeData:c2];

handshake s0s1s2

这个数据是服务器那边发送过来的 需要我们本地保存一份。

协议消息

流程如下

connect

当客户端检测到收到s2的消息后,就可以发送‘connect’了。

看看connect里面的具体数据

代码实现:

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
- (void)sendConnectPacket{
NSLog(@"sendConnectPacket");
// AMF格式
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSString *url;
NSMutableData *buff = [NSMutableData data];
if (_url.port > 0) {
url = [NSString stringWithFormat:@"%@://%@:%zd/%@",_url.scheme,_url.host,_url.port,_url.app];
}else{
url = [NSString stringWithFormat:@"%@://%@/%@",_url.scheme,_url.host,_url.app];
}
[buff appendString:@"connect"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"connect";
[buff appendByte:kAMFObject];
[buff putKey:@"app" stringValue:_url.app];
[buff putKey:@"type" stringValue:@"nonprivate"];
[buff putKey:@"tcUrl" stringValue:url];
[buff putKey:@"fpad" boolValue:NO];//是否使用代理
[buff putKey:@"capabilities" doubleValue:15.];
[buff putKey:@"audioCodecs" doubleValue:10.];
[buff putKey:@"videoCodecs" doubleValue:7.];
[buff putKey:@"videoFunction" doubleValue:1.];
[buff appendByte16:0];
[buff appendByte:kAMFObjectEnd];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

Window Acknowledgment size

发送端在接收到接受端返回的两个ACK间最多可以发送的字节数

Set Peer Bandwidth && Set Chunk Size && AFM0 Command _result

Set Peer Bandwidth

限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。

Set Chunk Size

这里为默认值128个字节。

AFM0 Command _result(‘NetConnection.Connect.Success’)

这个是上面发送的connect消息的回应消息,消息类型为AFM0 0x14,从返回的code和describtion可以看出,connect已经成功。

接下来客户端需要发送 releaseStream , FCPublish 和 CreateStream 消息

releaseStream 消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sendReleaseStream{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_NOTIFY;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"releaseStream"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"releaseStream";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

FCPublish消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)sendFCPublish{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_NOTIFY;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"FCPublish"];
[buff appendDouble:(++_numOfInvokes)];
self.trackedCommands[@(_numOfInvokes)] = @"FCPublish";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

createStream消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)sendCreateStream{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"createStream"];
self.trackedCommands[@(++_numOfInvokes)] = @"createStream";
[buff appendDouble:_numOfInvokes];
[buff appendByte:kAMFNull];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

之后客户端会收到startStream的回应消息。

startStream result

在收到这个消息后,就可以准备推流了。此时 客户端需要发送一个publish消息。

publish

这个消息告诉服务器,我要准备开始推流了,之后等待服务器给一个可以开始推流的回应。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)sendPublish{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDAudio;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"publish"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"publish";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
[buff appendString:@"live"];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

onStatus(‘NetStream.Publish.Start’)

客户端收到这个消息,表示服务器已经准备好接收流数据,客户端可以正式开始推流。

参考文章和demo