手工开发RTMP-HLS简易服务器_2_RTMP块解析

任务

OBS推流工具推送RTMP流码,解析该流码,汇总成Message流码

知识补充

1、源码以及其他知识补充:手工开发RTMP-HLS简易服务器_1_知识梳理

2、OBS推流的RTMP流码是基于TCP传输模式传输的。

3、由于当时C++能力有限,所以选择Java开发,建议使用C++,利用C++的【位域】可以加快开发速度。

4、官方RTMP文档:REAL-TIME MESSAGING PROTOCOL (RTMP) SPECIFICATION

5、Wireshark,如果你是初学者,使用wireshark可以检查你发送或接收到的流码是否正确。

RTMP解析

1、握手流程

服务器端需要发送S0,S1,S2包,用于握手流程。
1.1 C0、S0包格式

版本: 8
C0 中这个字段表示客户端要求的 RTMP 版本 。在 S0 中这个字段表示服务器选择的 RTMP 版本。本规范所定义的版本是 3 0-2 是早期产品所用的,已被丢弃; 4-31 保留在未来使用 ; 32-255 不允许使用 (为了区分其他以某一字符开始的文本协议)。如果服务无法识别客户端请求的版本,应该返回 3 。客户端可以选择减到版本 3 或选择取消握手。
1.2 C1  S1消息格式
  C1 S1 消息有 1536 字节长,由下列字段组成。
 
时间: 4 字节
     本字段包含时间戳。该时间戳应该是发送这个数据块的端点的后续块的时间起始点。可以是 0 ,或其他的任何值。为了同步多个流,端点可能发送其块流的当前值。
零: 4 字节
本字段必须是全零。
随机数据: 1528 字节。
本字段可以包含任何值。因为每个端点必须用自己初始化的握手和对端初始化的握手来区分身份,所以这个数据应有充分的随机性。但是并不需要加密安全的随机值,或者动态值。
1.3 C2  S2 消息格式
C2 S2 消息有 1536 字节长。只是 S1 C1 的回复。本消息由下列字段组成。
      
    时间: 4 字节
   本字段必须包含对等段发送的时间(对 C2 来说是 S1 ,对 S2 来说是 C1 )。
时间 2 4 字节
     本字段必须包含先前发送的并被对端读取的包的时间戳。
随机回复: 1528 字节
     本字段必须包含对端发送的随机数据字段(对 C2 来说是 S1 ,对 S2 来说是 C1 )。
每个对等端可以用时间和时间 2 字段中的时间戳来快速地估计带宽和延迟。但这样做可能并不实用。

2、块格式

这是整个块的样子,接下来将依次介绍。

2.1 块基本头(Basic Header)

fmt格式代表 块消息头的格式(2.2 会具体介绍)

csid是当前块的id,OBS推流信息中 ,csid 为2 的为控制信息,4为视频信息,5为音频信息(4 和 5貌似反了,忘记了。)。

【注】:理解这里有个坑,一般人会认为,既然是块的ID,按理说,不应该每个块的id都不一样吗?一个块可以装下视频信息吗?,答:个人猜测,在设计RTMP之初,考虑到丢包情况,出现了csid,且相互都不同,但是OBS推流的RTMP流码基于TCP,所以丢包情况在链路层以及得以解决,所以CSID可以固定下来。也有人说,是CSID传输完成一个块后,可以复用该块的CSID,所以也固定了下来。

【注】:块基本头还有其他格式,由于OBS推流的块类别少,所以这一种最简单块基本头就可以了。

2.2 块消息头(Message Header)

1)fmt == 0

a、timestamp :  时间戳: 3 字节 ,此处在代码中不需要将该字段转化为int/long类型,单纯保存字节即可,若timestamp= 0x00ffffff,则[extended timestamp]启用,且[extended timestamp] 为当前message的时间戳,timestamp弃用。
b、message length :消息长度。【注】:此处需要理解一个逻辑,多个chuck(块)组成一个message(信息),由于每个chunk最大长度为128(或者通过csid=2的chunk 进行更改长度),
c、message type id : 消息类型。9为视频消息,8为音频消息,20/19为消息控制信息,在之后的文章中会具体介绍。
d、msg stream id : 消息id,多个chunk组成一个message,所以每个chunk需要标注当前chunk属于哪个message。

2)fmt == 1

该类型省略msg stream id,表示接到的chunk 的msg stream id 继承上一个 chunk 的 msg stream id。

3)fmt == 2

该类型省略msg stream id、message type id、message type,同理继承上一个chunk。

4)fmt == 3

全部省略,同理继承上一个chunk。

3、关键代码

public void builder(BufferedInputStream bis,MessageFactory msgFactory) throws IOException, InterruptedException{
		int contentLen = bis.available();
		Map<byte[],MsgBean> saveChunk = msgFactory.getSaveChunk();
		do{
			BasicChunk bc = new BasicChunk();
			byte b = (byte)bis.read();
			byte fmt = ChunkBuilder.makeFmt(b);
			bc.setFmt(fmt);
			bc.setCsid(ChunkBuilder.makeCSID(b,bis));
			byte[] headerbuffer= ChunkBuilder.makeMsgHeader(fmt, bis);
			try{
				makeMsgHeader(bc,headerbuffer);
			}catch (NullPointerException e){
				System.out.println(ByteUtil.bytes2Str(headerbuffer == null ? new byte[]{0} : headerbuffer));
				System.out.println(fmt);
				System.out.println(ByteUtil.bytes2Str(bc.getCsid()));
				msgFactory.close();
				return ;
			}
			
			int extendTimeLen = ByteUtil.bytesEquals(bc.getMsgHeader_time(), new byte[]{-128,-128,-128})?4:0;
			if(extendTimeLen > 0){
				byte[] bArr = new byte[extendTimeLen];
				bis.read(bArr);
				bc.setExtendTime(bArr);
			}
			int msgLen = ByteUtil.bytesToInt(bc.getMsgHeader_length());
			int nowLen = saveChunk.containsKey(bc.getMsgHeader_stream())?saveChunk.get(bc.getMsgHeader_stream()).getNowLen():0;
			int deLen = msgLen - nowLen;
			//int dataLen = msgLen < realDataLen ? msgLen : realDataLen > deLen ? deLen : realDataLen;
			int dataLen = realDataLen < deLen ? realDataLen : deLen;
			dataLen = dataLen < 0 ? 0 : dataLen;
			if(bc.getFmt() == 0){
				nowTimestamp = ByteUtil.bytesToInt(bc.getMsgHeader_time());
			}else{
				nowTimestamp += ByteUtil.bytesToInt(bc.getMsgHeader_time());
			}
			bc.setMsgHeader_time(ByteUtil.intToBytes(nowTimestamp));
			//dataLen = msgLen;
			Log.append("{realDataLen = " +  realDataLen + "\n msgLen = "+msgLen+"\n nowLen =" + nowLen +"\n deLen = " + deLen + "\n dataLen = " + dataLen +"}\n");
			
			//byte[] b2Arr = new byte[dataLen];
			//bis.read(b2Arr);
			bc.setData(bigRead(bis,dataLen));
			//System.out.println(bc.toString());
			
			Log.append(bc.toString() + "\n");
			
			if(ByteUtil.bytesToInt(bc.getCsid()) != 2){
				if(saveChunk.containsKey(bc.getMsgHeader_stream())){
					saveChunk.get(bc.getMsgHeader_stream()).add(bc);
				}else{
					saveChunk.put(bc.getMsgHeader_stream(), new MsgBean(bc));
				}
				msgFactory.checkFull();
			}else{
				ControlChunk cc = new ControlChunk(bc,this,msgFactory);
				cc.control();
			}
			contentLen = bis.available();
		}while(contentLen > 0);
	}

代码思路,大致为:构建一个Map<byte[],list<BasicChunk>>,通过Socket接收TCP推流来的流码,将流码依次读取,按照之前的格式建立BasicChunk对象,根据BasicChunk的msg stream id,存储在map中。(注:Map<msg_stream_id,list<BasicChunk>>),如果BaiscChunk的CSID=2,先执行该Chunk,且不存储该Chunk。以下会介绍Chunk的CSID=2 时,会有哪些控制信息。

至此:你已经能够将OBS推流的RTMP流码拼接为Message了。

4、块消息补充(CSID==2)

       RTMP 块流支持一些协议控制消息。这些消息包含 rtmp 块流协议需要的信息,并且不能传播到更高层的协议。当前有两种消息用于 RTMP 块流。一个协议消息用于设置块大小,另一个用于由于余下的块不可得而取消一个消息。
       协议控制消息应该含有消息流 ID 0 (称为控制流)和块流 ID 2 ,并且应有最高的发送优先级。
       每个协议控制消息有一个固定大小的负载,并且作为一个独立的块发送。
4.1 设置块大小
        协议控制消息 1 ,设置块大小,用于通知对端新的最大块大小。
       块大小可以设置默认值,但是客户端或服务端可以改变和更新这个值。例如,假设一个客户端想发送 131 字节的音频数据,而块大小是 128 。那么,客户端可以向服务端发送一个协议消息通知对方块大小设置为 131 字节。然后,客户端就可以在一个块中发送音频数据。
       最大块大小是 65535 字节。块大小在每个方向上保持独立。
4.2 取消消息
        本协议控制消息用于通知对等端,不用再等待接收块来完成消息,而可以丢弃以前通过块流接收到的消息了。这个协议消息的负载是对等端接收到的块流 ID 。一个应用程序在关闭的时候发送这个消息,以告诉对方不需要继续处理消息了。
5、

5.1.  1
  1 展示一个简单的音频消息流。这个例子显示了信息的冗余。
          
 
Message Stream ID
Message T y pe ID
Time
Length
Msg # 1
  12345
  8  
1000
32
Msg # 2
  12345
  8  
1020
32
Msg # 3
  12345
  8  
1040
32
Msg # 4
  12345
  8  
1060
32
下表显示了这个流产生的块。从消息 3 开始,数据传输开始优化。在消息 3 之后,每个消息只有一个字节的开销。
      
 
Chunk   Stream ID
Chunk   Type
Header Data
No.of Bytes After   Header
Total No.of
Bytes in the   Chunk
Chunk#1
3
0
delta: 1000
length: 32
type: 8
stream ID :1234
(11bytes)
32
44
Chunk#2
3
2
20 (3 bytes)
32
36
Chunk#3
3
3
none(0   bytes)
32
33
Chunk#4
3
3
none(0   bytes)
32
33
5.2 2
       2 演示一个消息由于太长,而被分割成 128 字节的块。
     
 
Message Stream ID
Message TYpe ID
Time
Length
Msg # 1
12346
9 (video)
1000
307
下面是产生的块。
 
Chunk   Stream   ID
Chunk   Type
Header   Data
No. of   Bytes after   Header
Total No. of   bytes in  the chunk
Chunk#1
4
0
delta: 1000
length: 307
type: 9
streamID: 12346
(11   bytes)
128
140  
Chunk#2
4
3
none (0   bytes)
128   
129
Chunk#3
4
3
none (0   bytes)
51
52

感谢

1、官方文档翻译

2、RTMP协议从入门到放弃

3、Windows 7下WireShark抓取127.0.0.1(Loopback)报文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值