简介
RTMP(Real Time Messaging Protocol)是Adobe公司开发的实时消息协议,下面是Adobe的RTMP协议文档中的简介:
an application-level protocol designed for multiplexing and packetizing multimedia transport streams (such as audio, video, and interactive content) over a suitable transport protocol (such as TCP).
大致意思是:是一种应用层协议,使用合适的传输层协议对多媒体流进行多路复用(一个连接内传输多种媒体流)。
RTMP的通用实现都是使用TCP作为传输层协议,但是这也带来了一些问题,比如由于TCP的特性,传输的实时性相比UDP要差,再加上中间服务器转发使得从用户推流到服务器到最终拉流播放这一整条路径不确定中间要经过多少网络节点,所以延时普遍在秒级。但是它最大的优点是可以在一路连接上复用多种消息,极大地节省了网络资源。
协议组成
协议主要有几个部分:握手(也就是建立RTMP连接),表示消息的Chunk(分块)。
握手
握手部分分为三个阶段,分别为:C0 S0, C1 S1, C2 S2
在Adobe公布的RTMP文档里描述的握手,在FMS和Flash中并不可用,在传输h264+aac的时候会出现有数据,但是无法播放音视频,原因是Adobe私自变更了握手,但是没有公布出现。所以RTMP的握手被称为两种握手,按照Adobe的RTMP文档的握手成为简单握手,Adobe私自变更的握手成为复杂握手。参考srs大神的文章:rtmp复杂握手
复杂握手实现C1实现如下:代码摘抄自ZLMediaKit
class RtmpHandshake {
public:
RtmpHandshake(uint32_t _time, uint8_t *_random = nullptr) {
_time = htonl(_time);
memcpy(time_stamp, &_time, 4);
if (!_random) {
random_generate((char *) random, sizeof(random));
} else {
memcpy(random, _random, sizeof(random));
}
}
uint8_t time_stamp[4];
uint8_t zero[4] = {0};
uint8_t random[RANDOM_LEN];
void random_generate(char *bytes, int size) {
static char cdata[] = {0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x2d, 0x72,
0x74, 0x6d, 0x70, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x2d, 0x77, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x2d, 0x77, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x40, 0x31, 0x32, 0x36, 0x2e, 0x63, 0x6f, 0x6d};
for (int i = 0; i < size; i++) {
bytes[i] = cdata[rand() % (sizeof(cdata) - 1)];
}
}
void create_complex_c0c1();
}PACKED;
//发送复杂握手c0c1
void RtmpHandshake::create_complex_c0c1() {
#ifdef ENABLE_OPENSSL
//复杂握手时,zero字段不再是全0,而是指定的版本0x80,0x00,0x07,0x02
memcpy(zero, "\x80\x00\x07\x02", 4);
//复杂握手时,C1S1结构的random[1528]数据变成 key[764]+digest[764],也可能是digest[764]+key[764]
//key[764]的结构:random[offset]+key[128]+random[764-offset-128-4]+offset[4]
//digest[764]的结构:offset[4]+random[offset]+digest[32]+random[764-4-offset-32]
//Adobe的FMS只认识digest-key结构,digest结构可以认为是key结构的循环右移
//C2S2结构:random[1504]+digest[32],主要是提供对C1S1的验证
//digest随机偏移长度
auto offset_value = rand() % (C1_SCHEMA_SIZE - C1_OFFSET_SIZE - C1_DIGEST_SIZE);
//设置digest偏移长度
auto offset_ptr = random + C1_SCHEMA_SIZE;
offset_ptr[0] = offset_ptr[1] = offset_ptr[2] = offset_value / 4;
offset_ptr[3] = offset_value - 3 * (offset_value / 4);
//去除digest后的剩余随机数据
string str((char *) this, sizeof(*this));
//这里生成的C1是key-digest结构
//加上8是因为RtmpHandshake前面存储的是ts[4]+zero[4]这两个字段
//C1_SCHEMA_SIZE是key[764]的长度,C1_OFFSET_SIZE是digest结构中offset[4]的长度
//offset_value是digest结构中random[offset]的长度,接下来就是digest[32]
//需要擦除digest[32],拿剩下的数据生成32字节的摘要
str.erase(8 + C1_SCHEMA_SIZE + C1_OFFSET_SIZE + offset_value, C1_DIGEST_SIZE);
//获取摘要
auto digest_value = openssl_HMACsha256(FPKey, C1_FPKEY_SIZE, str.data(), str.size());
//插入摘要
memcpy(random + C1_SCHEMA_SIZE + C1_OFFSET_SIZE + offset_value, digest_value.data(), digest_value.size());
#endif
}
void RtmpProtocol::send_complex_S0S1S2(int schemeType,const string &digest){
//S1S2计算参考自:https://github.com/hitYangfei/golang/blob/master/rtmpserver.go
//发送S0
char handshake_head = HANDSHAKE_PLAINTEXT;
onSendRawData(obtainBuffer(&handshake_head, 1));
//S1
RtmpHandshake s1(0);
//复杂握手模式S1的zero字段是指定的版本号:0x04,0x05,0x00,0x01
memcpy(s1.zero, "\x04\x05\x00\x01", 4);
char *digestPos;
//解出C1的digest的位置
if (schemeType == 0) {
/* c1s1 schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes
*/
get_C1_digest(s1.random + C1_SCHEMA_SIZE, &digestPos);
} else {
/* c1s1 schema1
time: 4bytes
version: 4bytes
digest: 764bytes
key: 764bytes
*/
get_C1_digest(s1.random, &digestPos);
}
//在c1中digest的相同位置出的S1中加入S1的digest,使用FMSKey计算s1的digest
char *s1_start = (char *) &s1;
string s1_joined(s1_start, sizeof(s1));
s1_joined.erase(digestPos - s1_start, C1_DIGEST_SIZE);
string s1_digest = openssl_HMACsha256(FMSKey, S1_FMS_KEY_SIZE, s1_joined.data(), s1_joined.size());
memcpy(digestPos, s1_digest.data(), s1_digest.size());
onSendRawData(obtainBuffer((char *) &s1, sizeof(s1)));
//S2
//使用C1的digest和FMSKey计算s2的key
string s2_key = openssl_HMACsha256(FMSKey, S2_FMS_KEY_SIZE, digest.data(), digest.size());
RtmpHandshake s2(0);
s2.random_generate((char *) &s2, 8);
//使用s2的key和s2计算s2的digest
string s2_digest = openssl_HMACsha256(s2_key.data(), s2_key.size(), &s2, sizeof(s2) - C1_DIGEST_SIZE);
memcpy((char *) &s2 + C1_HANDSHARK_SIZE - C1_DIGEST_SIZE, s2_digest.data(), C1_DIGEST_SIZE);
onSendRawData(obtainBuffer((char *) &s2, sizeof(s2)));
//等待C2
_next_step_func = [this](const char *data, size_t len) {
return handle_C2(data, len);
};
}
rtmp协议结构理解:
RTMP:(chunk)
1. handshake chunk
1.1 handshake sequence (C0,C1,C2, S0,S1,S2)
1.1.1 C0/S0 -> 8-bits(version = 3)
1.1.2 C1/S1 -> 1536-bytes(time(4bytes)-zero(4bytes)-random(1528bytes))
1.1.3 C2/S2 -> 1536bytes(echo C2=S1, S2=C1, time-time-random)
1.2 握手流程:
1.2.1 C0C1 -> S0S1S2 -> C2
2. Message chunk
2.1 Basic Header(1,2,3 bytes, 共3种版本)
2.1.1 fmt(2-bits) - 别名 cunk_type, 指明是哪一种 Message Header,总共4种(需要注意,这个字段是用来判断消息头类型的)
2.1.2 chunk stream id(6-bits), 正常取值范围 2-63,
2.1.3 chunk stream id(6-bits)取值0表示2字节头,chunk-stream-id=后面1字节的数值减去 64
2.1.4 chunk stream id(6-bits)取值1表示3字节头,chunk-stream-id=后面2字节的数值减去 64
2.1.5 chunk stream id 分类:
network - 2
system - 3
createStream 前 - 4
createStream 前 - 5
audio - 6
video - 7
2.2 Message Header(0, 3, 7, 11 bytes)
2.2.1 fmt==0, type0(11bytes),(timestamp->message_length->message_type_id->message_stream_id)
timestamp(3bytes)- 绝对时间戳
message length(3bytes)
message type id(1byte)
set_chunk - 1 - 协议控制消息 0-1bit,chunk_size - 31bits
abort - 2 - 协议控制消息 chunk_stream_id - 4bytes
ack - 3 - 协议控制消息 sequence_number - 4bytes
user_control - 4 - Event_type(16bits) + Event_data
windows_size - 5 - 协议控制消息 window_size - 4bytes
set_peer_bw - 6 - 协议控制消息 window_size - 4bytes + limit_type 1byte(0-hard, 1-soft, 2-dynamic)
audio - 8
video - 9
AMF - 18 - Data Message - metadata, @setDataFrame
AMF3 - 15 - Data Message
AMF - 20 - Command Message (NetConnection Command, Netstream Command)
AMF3 - 17 - Command Message
NetConnection Command:
connect:
name=connect
transaction_id=1
command_Object:
app="live"
flashver="FMSc/1.0"
swfUrl="rtmp://host:port/live/streamid"
tcUrl="rtmp://host:port/live/streamid"
fpad=proxy ? true : false
audioCodecs=
VideoCodecs=
object_encoding(amf0-0, amf3-3)
Option_user_arguments_object:
call:
close:
createStream:
回复命令:name, properties,information(code,level,description)
_error:
_result:
_onStaus:
NetStream Command:
play:
play2:
deleteStream:
closeStream:
receiveAudio:
receiveVideo:
publish:
name="publish"
transaction_id=0
command_object=null
Publishing_name="streamid"
Publish_type="live/record/append"
seek:
pause:
回复命令:(name="onStatus", transaction_id=0, info_object(level(warning,status,error),code,description))
onStatus:
AMF - 19 - Shared Object Message - 一般不用
AMF3 - 16 - Shared Object Message
Aggregate Message - 22
Header + Aggregate Message body
Heder0 + Message data0 + Back Pointer0 + ...
message stream id(4bytes)
control - 0
media - 1
2.2.2 fmt==1, type1, 7bytes,相比type0少了message stream id,时间戳是相对时间戳
2.2.3 fmt==2, type2, 3bytes,只有相对时间戳,其它信息复用上一个chunk包的头信息
2.2.4 fmt==3, type3, 不需要Message Header,复用上一个chunk包的头信息
2.3 Extended Timestamp(0 or 4 bytes),当Message Header的3字节时间戳满了,才使用这个字段,并且timestamp字段设置为0xFFFFFF, 否则不需要这个字段。
2.4 Chunk Data - FLV--VIdeoTag/AudioTag(tag-header(5bytes) + tag-body(audio/video/decoderconfig))
-------------------------------------------------------------------------------------------------------------------------------
额外补充:rtmp的chunk data默认使用FLV的VideoTag或者AUdioTag格式,是因为FLV就是Adobe公司为rtmp开发的封装格式。
原来FLV的文档并不支持HEVC,但是随着音视频技术的发展,国内金山云率先推出了支持HEVC的FLV格式,但是由于过于简单(仅仅是扩展了CodecID为12),并不具备扩展其他更多的音视频格式,所以国际上很多知名的开源软件和公司并不打算按照金山云的方案支持。
后来官方推出了Enhance-RTMP方案,主要是使用FourCC实现任意音视频扩展,自此,国际上才开始慢慢支持起来。其实说是Enhance-FLV更为合适。
-------------------------------------------------------------------------------------------------------------------------------
rtmp的几个问题:
1. 复杂握手过程,并没有文档公开。
2. 控制命令中的chunk_stream_id并不是严格按照头定义的大小(最大2字节),而是4字节。
3. rtmp协议文档中并没有指明chunk data的格式是什么,而是默认使用flv的VideoTag和AudioTag格式。