rtmp2 chunk : chunk basic header / chunk msg header / extended timestamp

rtmp 2 chunk

zhangbin 20191204
参考 https://www.jianshu.com/p/bae4ee898019 简书 作者:杨玉奇 2019 年 1 月 22 日

rtmp 层次结构

chunk

  • 握手之后,连接复用一个或多个块流。创建的每个块都有一个唯一 ID 对其进行关联,这个 ID 叫做 chunk stream ID (块流 ID),每一类 csid 都对应一种功能

  • 在接收端,这些块被根据块流 ID 被组装成消息。

  • 组块允许更高层协议中的大消息分解成较小的消息,例如防止大的、低优先级的消息(如视频)阻塞较小但高优先级的消息,如:音频(高优先级)或控制(中优先级)。块大小是可配置的。

  • 实际底层传输,用的是chunk 块

- 消息是rtmp协议的基本数据单元,
- 在网络传输时,消息会被重新封装成块进行传输,每个块都有固定的大小,
- 如果消息大小大于块的大小,消息就会被拆分成几个块发送。
  • 为了节省流量,chunk有多种类型和大小

块格式

  • header + payload
  • header 分为 basic header 、 chunk msg header 、 extended timestamp
  • 块的basic header 、 chunk msg header 大小都是不固定的
左对齐 右对齐 居中对齐
chunk basic header 1 、2、3 byte 块基本头
chunk msg header 11、2、3 byte 块消息头
extended timestamp 4 byte 扩展时间戳

chunk header

  • chunk header
  • code
// Chunk Header

type ChunkHeader struct {
	BasicHeader       *BasicHeader
	MessageHeader     *MessageHeader
	ExtendedTimestamp uint32
}

chunk basic header

Basic Header (基本头,1 到 3 个字节)
RTMP 协议最多支持 65597 个流(chunk),CS ID 范围为: 3 ~ 65599。ID 0、1、2 被保留。

  • 0 值表示二字节形式,并且 ID 范围 64 ~ 319 (第二个字节 + 64)。

  • 1 值表示三字节形式,并且 ID 范围为 64 ~ 65599 ((第三个字节) * 256 + 第二个字节 + 64)。3 ~ 63 范围内的值表示整个流 ID。

  • 带有 2 值的块流 ID 被保留,用于下层协议控制消息和命令。

  • 块基本头中的 0 ~ 5 位 (最低有效) 代表块流 ID,块流 ID 2 ~ 63 可以编进这一字段的一字节版本中。

chunk basic header

type BasicHeader struct {
	FMT           uint8  
	ChunkStreamID uint32 //0 到4 个字节
}
  • 这个字段对块流 ID 和块类型进行编码。
  • 块流 ID,因为块流 ID 是一个可变长度的字段。
  • 第一个字节分为 2 bit 和 6bit的cs id
左对齐 右对齐 居中对齐
fmt 2 bit 块格式
cs d 6 bit 块流id
  • fmt 有四种,代表有四种类型的chunk
fmt 含义
0 块消息头类型 0
1 块消息头类型 1
2 块消息头类型 2
3 块消息头类型 3

chunk Basic Header fmt 对 chunk Message Header 长度的影响

  • fmt == 0:Message Header 长度为 11 ,这是一个新流,11个字节的完整包头

  • chunk message header 11 bytes

    • timestamp:对于 fmt == 0 的 chunk,绝对时间戳在这里表示,如果时间戳值大于等于 0xffffff(16777215),该值必须是 0xffffff, 且时间戳扩展字段 (Extended Timestamp) 必须发送,其他情况没有要求。
      – message length:Message 的长度,注意这里的长度并不是跟随 chunk head 其后的 chunk data(Payload)的长度,而是前文提到的一条信令或者一帧视频数据或音频数据的长度。前文提到过信令或者媒体数据都称之为 Message,一条 Message 可以分为一条或者多条 chunk。
  • fmt == 1:Message Header 长度为 7 ,少了mesesage stream id
    在这里插入图片描述

  • fmt == 2:Message Header 长度为 3 , 其中时间戳部分是相对时间。

    • timestamp delta : 相对时间,是与上一个 相同 CSID 的 chunk 之间的差值。如果 timestamp delta 大于或等于 16777215(十六进制 0xFFFFFF),这个字段必须是 16777215,指示扩展时间戳字段 (Extended Timestamp) 的存在。
  • 在这里插入图片描述

  • fmt == 3:Message Header 长度为 0

  • Chunk Msg Header 可变的原因是为了压缩传输的字节数,把一些相同类型的 chunk 的 head 去掉一些字节,换句话说就是四种类型的包头都可以通过一定的规则还原成 11 个字节,这个压缩和还原在 RTMP 协议中称之为复用/解复用。

cs id 的取值从0 到 64, 意义不同:

csid 含义
0 块基本头2个字节,块流ID计算方法(第2个字节+64),范围(64-319)
1 块基本头3个字节,块流ID计算方法(第3个字节*255+第2个字节+64),范围(3-65599)
2 块基本头1个字节,2表示低层协议消息
3 - 64 块基本头1个字节,该数表示块流ID,范围(3-64)
  • 前面三种都保留了。只用刀了3到64 作为csid

  • 本协议支持65597种流,ID从3-65599。ID 0、1、2作为保留。

  • code :

  • 应该都是大端的吧,

  • FMT + ChunkStreamID 生成一个大端的字节 :高两位是fmt,低六位是 csid

  • 根据fmt 的类型,使用csid计算 chunkstreamid

    • 返回一个大端的basic header
  • 根据 ChunkStreamID 可以反推 基本头的大小和 csid的值 ,填入 chunk basic header 作为后六位
    
func genBasicHeader(bh *BasicHeader) ([]byte, error) {
	if bh.ChunkStreamID < 64 { //一个字节
		x := uint8(bh.ChunkStreamID&(0x3f)) + (bh.FMT << 6)
		return []byte{x}, nil
	} else if bh.ChunkStreamID < 320 {
		x := make([]byte, 2) //俩字节
		x[0] = bh.FMT << 6
		x[1] = uint8(bh.ChunkStreamID - 64)
		return x, nil
	} else if bh.ChunkStreamID < 65599 { //3 个字节
		x := make([]byte, 3)
		x[0] = bh.FMT<<6 + uint8(0xff&0x3f)
		binary.BigEndian.PutUint16(x[1:], uint16(bh.ChunkStreamID-64))
		return x, nil
	} else {
		return []byte{}, errInvalidChunkStreamID
	}
}

对网络流中的basic header 解码

  • 这一字段对正在发送的消息 (不管是整个消息,还是只是一小部分) 的信息进行编码。
  • 这一字段的长度可以使用块头中定义的块类型进行决定。
  • 收到一个网络流,从中得到basic header
  • 读取一个字节x
    • 字节的前面两位读取,变为十进制的fmt : uint8(x) >> 6
    • 后面六位应该是csid了吧 : csid := x & (32 + 16 + 8 + 4 + 2 + 1)
  • TODO csid := x & (32 + 16 + 8 + 4 + 2 + 1) 计算csid
  • 根据csid ,可以知道这几个basic header 大小
  • csid :
    • 0 //第二个字节+ 64 = chunk stream id
func readBasicHeader(br *bufio.Reader) (*BasicHeader, error) {
	x, err := br.ReadByte()
	if err != nil {
		return nil, err
	}
	h := new(BasicHeader)
	h.FMT = uint8(x) >> 6
	csid := x & (32 + 16 + 8 + 4 + 2 + 1)

	switch csid {
	case 0: //2个字节
		// Chunk stream IDs: 64-319
		//  0                   1
		//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |fmt|     0     |   cs id - 64  |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		y, err := br.ReadByte() //第二个字节
		if err != nil {
			return nil, err
		}
		h.ChunkStreamID = uint32(y) + 64 //第二个字节+ 64 = chunk stream id 
	case 1: //basic header 3 bytes 
		// Chunk stream IDs: 64-65599 这个范围TODO 
		//  0                   1                   2
		//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |fmt|     1     |         cs id - 64            |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		z := make([]byte, 2) //直接读取两个字节 块流ID计算方法(第3个字节*255+第2个字节+64)
		_, err = io.ReadAtLeast(br, z, 2) 
		if err != nil {
			return nil, err
		}
		//读取的z是16位的,变为10进制,相当于 第3个字节*255+第2个字节 ???TODO 
		h.ChunkStreamID = uint32(binary.BigEndian.Uint16(z)) + 64
	default: // 2 和 3 都只有 1个字节
		// Chunk Stream IDs: 2-63
		//  0 1 2 3 4 5 6 7
		// +-+-+-+-+-+-+-+-+
		// |fmt|   cs id   |
		// +-+-+-+-+-+-+-+-+
		h.ChunkStreamID = uint32(csid)
	}
	return h, nil
}

chunk basic header 有 1 、2 、3 个字节三种情况

chunk msg header

息头,0,3,7,或者 11 个字节)

  • code
type MessageHeader struct {
	Timestamp       uint32
	TimestampDelta  uint32
	MessageLength   uint32
	MessageTypeID   uint8
	MessageStreamID uint32
}
  • rtmp server
  • fmt 决定的0 1 2 3 四种类型的msg header 大小不同;

类型0

  • 块头共11字节长,该类型必须使用在块流的开头和任何流时间戳回滚(如向后seek)的时候。
  • CODE , fmt 为0 时,msg header 11个字节;
  • 下面的图里,一行是32位,4个字节。
  • 3 个字节时间戳;
  • 3 字节消息长度
  • 消息id 1个字节
  • 消息流id 4 个字节
  • 都是大端表示的,所以给你一个timestamp ,按照如下转为3个字节的大端传输
x[0] = byte(timestamp >> 16)
		x[1] = byte(timestamp >> 8)
		x[2] = byte(timestamp)
  • • timestamp(3 bytes): 对于类型0的块,这里填写发送消息时的绝对时间戳。如时间戳大于或等于 16777215(十六进制 0xFFFFFF)时,该字段值必须为16777215,且扩展时间戳必须出现以编码完整的32位时间戳。除此之外,该字段必须为完整时间戳值。
  • code generate fmt 0 to transport
 // 生成msg header  genMessageHeader  :
	switch fmt {
	case 0:
		//  0                   1                   2                   3
		//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |             timestamp                         |message length |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |     message length (cont)     |message type id| msg stream id |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |            message stream id (cont)           |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		x := make([]byte, 11)
		x[0] = byte(timestamp >> 16)
		x[1] = byte(timestamp >> 8)
		x[2] = byte(timestamp)
		x[3] = byte(mh.MessageLength >> 16)
		x[4] = byte(mh.MessageLength >> 8)
		x[5] = byte(mh.MessageLength)
		x[6] = mh.MessageTypeID
		binary.LittleEndian.PutUint32(x[7:], mh.MessageStreamID)
		return x, nil

类型1

  • fmt 1 ,七个字节 ;消息流id (4个字节) 使用前面的
  • 类型1 块头长7字节,消息流ID未包含在内,该块使用和前面块一致的块流ID。可变长消息的流(如许多视频格式)应当使用本格式作为每条新消息的第二块:
  • code
case 1:
		//  0                   1                   2                   3
		//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |                 timestamp delta               |message length |
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		// |     message length (cont)     |message type id|
		// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		x := make([]byte, 7)
		x[0] = byte(timestampDelta >> 16)
		x[1] = byte(timestampDelta >> 8)
		x[2] = byte(timestampDelta)
		x[3] = byte(mh.MessageLength >> 16)
		x[4] = byte(mh.MessageLength >> 8)
		x[5] = byte(mh.MessageLength)
		x[6] = mh.MessageTypeID
		return x, nil

类型2

  • fmt 2 三个字节,消息流id 和 消息长度 都没有,与前面的一致。
case 2:
   	//  0                   1                   2
   	//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
   	// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   	// | timestamp delta |
   	// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   	x := make([]byte, 3)
   	x[0] = byte(timestampDelta >> 16)
   	x[1] = byte(timestampDelta >> 8)
   	x[2] = byte(timestampDelta)
   	return x, nil
  • 类型2 块头长3字节,消息流ID和长度均未包含在内;该块使用和前面块一致的块流ID和长度值。
  • 具有固定长度消息的流(如许多音频和数据格式)可使用本格式作为每条消息的第二块:

类型3

  • 这种chunk 直接没有 chunk msg header
  • 没有消息头 ,没有流id,没有消息长度;
  • 类型3 块没有消息头,流ID、消息长度以及时间戳增量字段都不会呈现;本类型的块是钱前面块的流ID。当一条消息分解成块后,除第一块外,所有其他块都该使用此类型(参考示例2,Section 4.3.2.2)。由完全相同大小、流ID和时间间隔的消息流可在开头的类型2块后的所有块中使用类型3块(参考示例1,Section 4.3.2.1)。如果第二条消息和第一条消息的时间增量与第一条消息的时间戳相同,那么可以在类型0块后直接跟随类型3块,因为没有必要使用类型2块说明时间增量。也就是说,如果类型3块紧跟类型0块,那么该类型3块的时间增量是和类型0块一致的。
  • code
case 3:
		// chunk message header is empty
		return []byte{}, nil

下面是读取chunk msg header

  • 从 大端模式的字节流中,读取所需要的chunk msg header
  • 大端变10进制
func readMessageHeader(br *bufio.Reader, fmt uint8) (*MessageHeader, error) {
	mh := new(MessageHeader)
	switch fmt {
	case 0:
		x := make([]byte, 11)
		_, err := io.ReadAtLeast(br, x, 11)
		if err != nil {
			return nil, err
		}
		mh.Timestamp = binary.BigEndian.Uint32(append([]byte{0x0}, x[:3]...))
		mh.MessageLength = binary.BigEndian.Uint32(append([]byte{0x0}, x[3:6]...))
		mh.MessageTypeID = x[6]
		mh.MessageStreamID = binary.LittleEndian.Uint32(x[7:11])
		return mh, nil
	case 1:
		x := make([]byte, 7)
		_, err := io.ReadAtLeast(br, x, 7)
		if err != nil {
			return nil, err
		}
		mh.TimestampDelta = binary.BigEndian.Uint32(append([]byte{0x0}, x[:3]...))
		mh.MessageLength = binary.BigEndian.Uint32(append([]byte{0x0}, x[3:6]...))
		mh.MessageTypeID = x[6]
		return mh, nil
	case 2:
		x := make([]byte, 3)
		_, err := io.ReadAtLeast(br, x, 3)
		if err != nil {
			return nil, err
		}
		mh.TimestampDelta = binary.BigEndian.Uint32(x)
		return mh, nil
	case 3:
		return mh, nil
	default:
		return nil, errUnknownFMT
	}

4.3.1.2.5 常用消息头变量

  • code
type MessageHeader struct {
	Timestamp       uint32
	TimestampDelta  uint32
	MessageLength   uint32
	MessageTypeID   uint8
	MessageStreamID uint32
}
  • 下面列出块消息头中的每个变量说明:
  • • 时间戳增量(3字节):
    – 用于类型1和类型2块,标识与前一块时间戳的差值。如果增量大于或等于16777215(十六进制 0xFFFFFF),该变量必须为16777215,以说明将同时使用到扩展时间戳以完成32位完整增量的编码。除此之外,该变量应该等于实际差值。
  • • 消息长度(3字节):用于类型0和类型1块,标识消息长度。注意该变量通常和块负载长度不同。The chunk payload length is the maximum chunk size for all but the last chunk, and the remainder (which may be the entire length, for small messages) for the last chunk.
  • • 消息类型ID(1字节):用于类型0和类型1块,标识当前发送消息的类型。
  • • 消息流ID(4字节):用于类型0块,标识存储的消息流ID。消息流ID使用little-endian格式存储。通常,同一块流中的所有消息消息流ID相同,但将多个消息流复用到同一块流中也是可以的,不过这样就会失去可使用块头压缩而带来的好处。不过当一个消息流关闭且另外一个紧接着打开时,也没有理由不通过发送新的类型0块对已经存在的块流进行复用。

4.3.1.3 扩展时间戳

  • Extended Timestamp (扩展 timestamp,0 或 4 字节):这一字段是否出现取决于块消息头中的 timestamp 或者 timestamp delta 字段。它表示的是时间戳的增量,需要与 timestamp 或者 timestamp delta 相加获得总时间戳。
  • 扩展时间戳用于编码大于16777215(十六进制0xFFFFFF)的时间戳或时间戳增量,即用于类型0,1,2块中不适合24位变量的时间戳或时间戳增量。该变量编码了完整的32位时间戳或时间戳增量。该变量的出现意味着块0中时间戳或块1/2中时间戳增量值为16777215(0xFFFFFF)。该变量会在当最近的具有相同块流ID类型0/1/2块中出现扩展时间戳变量时,出现在紧跟其后的类型3块中。
发布了664 篇原创文章 · 获赞 55 · 访问量 217万+

猜你喜欢

转载自blog.csdn.net/commshare/article/details/103393461