Adobe官方文档描述不清,而且跟实际实现又有些不一样,实在很混乱,还是自己做做笔记靠谱。
这篇只讲Chunk,握手另外写篇文章。
Chunk Format
RTMP收发的数据称为Message,所谓Message不外乎音视频数据和信令,而真正收发数据时把Message拆分为Chunk发送,每个Chunk的默认大小是128字节,当然这个大小可以通过控制信息来修改。
Message要分块很容易理解,因为音视频数据特别是视频数据太大了,不分块直接发的话会造成不同Message之间阻塞,自然出现音视频卡顿、信令不能及时得到反馈等问题,这跟以往我们用udp实现私有协议音视频实时传输一个道理,都需要分块。
Chunk如上图由Chunk Header和Chuk Data组成,Chunk Header又由3个部分组成,下面一个个来细说。
1. Chunk Basic Header
Chunk Basic Header包含了chunk stream ID(流通道Id)和chunk type(chunk的类型),chunk stream id在下图中简写为cs id,chunk type下图为fmt。
csid用来唯一标识一个特定的流通道,fmt决定了后面Message Header的格式。
Basic Header的长度可能是1,2,或3个字节,其中fmt的长度是固定的(占2位,注意单位是位,bit),Basic Header的长度取决于csid的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。
注意在这儿csid和fmt是没有关系的,只有csid的长度才决定整个Basic Header的长度,而fmt决定的是后面Message Header的格式。
RTMP协议支持用户自定义[3,65599]之间的CSID,0,1,2由协议保留表示特殊信息。0代表Basic Header总共要占用2个字节,CSID在[64,319]之间,1代表占用3个字节,CSID在[64,65599]之间,2代表该chunk是控制信息和一些命令信息。
chunk type的长度固定为2位,因此CSID的长度是(6=8-2)、(14=16-2)、(22=24-2)中的一个。
下图分别表示Basic Header分别为1、2、3个字节的情况:
-
当Basic Header为1个字节时,CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在[0,63]之间,其中用户可自定义的范围为[3,63]。
-
当Basic Header为2个字节时,CSID占14位,此时协议将与chunk type所在字节的其他位都置为0,剩下的一个字节来表示CSID-64,这样共有8个二进制位来存储CSID,8位可以表示[0,255]共256个数,因此这种情况下CSID在[64,319],其中319=255+64。
-
当Basic Header为3个字节时,CSID占22位,此时协议将[2,8]字节置为1,余下的16个字节表示CSID-64,这样共有16个位来存储CSID,16位可以表示[0,65535]共65536个数,因此这种情况下CSID在[64,65599],其中65599=65535+64,需要注意的是,Basic Header是采用小端存储的方式,越往后的字节数量级越高,因此通过这3个字节每一位的值来计算CSID时,应该是:<第三个字节的值>x256+<第二个字节的值>+64。
可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的[64,319],但实际实现时还是应该秉着最少字节的原则使用2个字节的表示方式来表示[64,319]的CSID。
代码实现:
来对比一下srs中的实现(srs_rtmp_stack.cpp):
int SrsProtocol::read_basic_header(char& fmt, int& cid)
{
int ret = ERROR_SUCCESS;
if ((ret = in_buffer->grow(skt, 1)) != ERROR_SUCCESS) {
if (ret != ERROR_SOCKET_TIMEOUT && !srs_is_client_gracefully_close(ret)) {
srs_error("read 1bytes basic header failed. required_size=%d, ret=%d", 1, ret);
}
return ret;
}
fmt = in_buffer->read_1byte();
cid = fmt & 0x3f;
fmt = (fmt >> 6) & 0x03;
// 2-63, 1B chunk header
if (cid > 1) {//大于1肯定是1个字节,所以直接返回即可
srs_verbose("basic header parsed. fmt=%d, cid=%d", fmt, cid);
return ret;
}
// 64-319, 2B chunk header
if (cid == 0) {//为0表示2个字节
if ((ret = in_buffer->grow(skt, 1)) != ERROR_SUCCESS) {
if (ret != ERROR_SOCKET_TIMEOUT && !srs_is_client_gracefully_close(ret)) {
srs_error("read 2bytes basic header failed. required_size=%d, ret=%d", 1, ret);
}
return ret;
}
cid = 64;
cid += (u_int8_t)in_buffer->read_1byte();
srs_verbose("2bytes basic header parsed. fmt=%d, cid=%d", fmt, cid);
// 64-65599, 3B chunk header
} else if (cid == 1) {//为1表示3个字节
if ((ret = in_buffer->grow(skt, 2)) != ERROR_SUCCESS) {
if (ret != ERROR_SOCKET_TIMEOUT && !srs_is_client_gracefully_close(ret)) {
srs_error("read 3bytes basic header failed. required_size=%d, ret=%d", 2, ret);
}
return ret;
}
cid = 64;
cid += (u_int8_t)in_buffer->read_1byte();
cid += ((u_int8_t)in_buffer->read_1byte()) * 256;
srs_verbose("3bytes basic header parsed. fmt=%d, cid=%d", fmt, cid);
} else {
srs_error("invalid path, impossible basic header.");
srs_assert(false);
}
return ret;
}
而同为srs作者的ossrs/go-oryx-lib的rtmp.go的代码是有bug的,这个库的代码不能用(不仅仅是这个bug),不仅n久没更新也没正式Release:
func (v *Protocol) readBasicHeader() (format formatType, cid chunkID, err error) {
// 2-63, 1B chunk header
var t uint8
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
return format, cid, oe.Wrap(err, "read basic header")
}
cid = chunkID(t & 0x3f)
format = formatType((t >> 6) & 0x03)
if cid > 1 {//大于1肯定是1个字节,所以直接返回即可
return
}
// 64-319, 2B chunk header
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
return format, cid, oe.Wrapf(err, "read basic header for cid=%v", cid)
}
cid = chunkID(64 + uint32(t))
// 64-65599, 3B chunk header
if cid == 1 {//这儿有bug!!!
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
return format, cid, oe.Wrapf(err, "read basic header for cid=%v", cid)
}
cid += chunkID(uint32(t) * 256)
}
return
}
2. Message Header
未完待续......
参考文章
这篇比较好:带你吃透RTMP
Adobe的RTMP 协议官方1.0文档
Adobe 官方公布的 RTMP 规范+未公布的部分
RTMP协议分析及H.264打包原理