我就想做一个直播推流而已,为什么还要学习RTMP的协议?
不是在 《视频轨码率默认设置为600Kbps,音频轨码率默认设置为64Kbps》已经交叉编译好好RTMP库了吗,作为一个调库仔直接调用API不就完事了吗,为什么还要学生RTMP协议?学不不动了,告辞!!!
我们想回想一下我们推流端的简要流程:
那么在第三步,RTMP包如何封装呢?真的是简单的调用一下API就完事了吗?
我们现在看一段RTMP组包的代码:
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
int bodySize = 13 + sps_len + 3 + pps_len;
RTMPPacket *packet = new RTMPPacket;
//
RTMPPacket_Alloc(packet, bodySize);
int i = 0;
//固定头
packet->m_body[i++] = 0x17;
//类型
packet->m_body[i++] = 0x00;
//composition time 0x000000
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
//版本
packet->m_body[i++] = 0x01;
//编码规格
packet->m_body[i++] = sps[1];
packet->m_body[i++] = sps[2];
packet->m_body[i++] = sps[3];
packet->m_body[i++] = 0xFF;
//整个sps
packet->m_body[i++] = 0xE1;
//sps长度
packet->m_body[i++] = (sps_len >> 8) & 0xff;
packet->m_body[i++] = sps_len & 0xff;
memcpy(&packet->m_body[i], sps, sps_len);
i += sps_len;
//pps
packet->m_body[i++] = 0x01;
packet->m_body[i++] = (pps_len >> 8) & 0xff;
packet->m_body[i++] = (pps_len) & 0xff;
memcpy(&packet->m_body[i], pps, pps_len);
//视频
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
//随意分配一个管道(尽量避开rtmp.c中使用的)
packet->m_nChannel = 10;
//sps pps没有时间戳
packet->m_nTimeStamp = 0;
//不使用绝对时间
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
videoCallback(packet);
}
void VideoChannel::sendFrame(int type, uint8_t *payload, int i_payload) {
if (payload[2] == 0x00) {
i_payload -= 4;
payload += 4;
} else {
i_payload -= 3;
payload += 3;
}
//看表
int bodySize = 9 + i_payload;
RTMPPacket *packet = new RTMPPacket;
//
RTMPPacket_Alloc(packet, bodySize);
packet->m_body[0] = 0x27;
if(type == NAL_SLICE_IDR){
packet->m_body[0] = 0x17;
LOGE("关键帧");
}
//类型
packet->m_body[1] = 0x01;
//时间戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//数据长度 int 4个字节
packet->m_body[5] = (i_payload >> 24) & 0xff;
packet->m_body[6] = (i_payload >> 16) & 0xff;
packet->m_body[7] = (i_payload >> 8) & 0xff;
packet->m_body[8] = (i_payload) & 0xff;
//图片数据
memcpy(&packet->m_body[9], payload, i_payload);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nChannel = 0x10;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
videoCallback(packet);
}
通过代码我们可以看到,在组RTMP包的时候我们会往里面写入各种莫名其妙的十六进制的数据,那么这些数据代表的意义是什么呢?
这就是我们今天要学习的内容。
RTMP是Real Time Messaging Protocol
(实时消息传输协议)的首字母缩写。该协议是基于TCP的高层协议族。
默认使用的端口是1935。
RTMP的包结构
RTMP协议封包是由一个包头和一个包体组成,包头可以是4种长度的任意一种:12, 8, 4, 1 个字节。
完整的RTMP包头应该是12个字节,包含了时间戳,Head_Type,AMFSize,AMFType,StreamID信息,
8字节的包头只纪录了时间戳,Head_Type,AMFSize,AMFType;
4个字节的包头记录了时间戳,Head_Type。
1个字节的包头只记录了Head_Type。
包体最大长度默认为128字节,通过chunkSize可改变包体最大长度,通常当一段AFM数据超过128字节后,超过128的部分就放到了其他的RTMP封包中,包头为一个字节。
一个完整的RTMP包头有12字节,由下面5个部分组成:
名称 | 所占字节大小 | 备注 |
Head_Type | 1 | 包头 |
TIMER | 3 | 时间戳 |
AMFSize | 3 | 数据大小 |
AMFType | 1 | 数据类型 |
StreamID | 4 | 流ID |
1、 协议包头信息
Head_Type占用RTMP包的第一个字节,这个字节里面记录了包的类型和包的ChannelID。
Head_Type字节的前两个Bit决定了包头的长度,它可以用掩码0xC0
进行"与"计算得出。
计算得出结果后对照下表即可知道包头的长度:
Head_Type的前两个Bit用掩码0xC0
进行"与"计算后的结果与包头的长度对应关系:
结果 | 包头的长度(字节) |
00 | 12 bytes |
01 | 8 bytes |
10 | 4 bytes |
11 | 1 byte |
例如rtmp包里面经常看到的0xC2, 就表示一字节的包头。
0xC2的二进制是 11000010
掩码0xC0的二进制是 11000000
它们的前两位与运算的结果是11,所以是查表得出它们的包头是长度是一个字节。
而ChannelID则是2。
而Head_Type的后面6个Bit和StreamID决定了ChannelID。 StreamID和ChannelID对应关系:
StreamID=(ChannelID-4)/5+1
ChannelID与相关值的对应关系表:
ChannelID | 用途 |
02 | Ping 和ByteRead通道 |
03 | Invoke通道 我们的connect() publish()和自字写的NetConnection.Call() 数据都是在这个通道的 |
04 | Audio和Vidio通道 |
05 06 07 | 服务器保留,经观察FMS2用这些Channel也用来发送音频或视频数据 |
2、 TIMMER时间戳
时间戳占用RTMP包头的第2、3、4 三个字节。
音视频流的时间戳是统一排的。可分为绝对时间戳和相对时间戳。
fms对于同一个流,发布(publish)的时间戳和播放(play)的时间戳是有区别的。
publish时间戳,采用相对时间戳,时间戳值等于当前媒体包的绝对时间戳与上个媒体包的绝对时间戳之间的差距,也就是说音视频时间戳在一个时间轴上面,单位毫秒。
play时间戳,相对时间戳,时间戳值等于当前媒体包的绝对时间戳与上个同类型媒体包的绝对时间戳之间的差距,也就是说音视频时间戳分别为单独的时间轴,单位毫秒。
3、 AMFSize
AMFSize占三个字节,这个长度是AMF长度,可超过RTMP包的最大长度128字节。
如果超过了128字节,那么由多个后续RTMP封包组合,每个后续RTMP封包的头只占一个字节。所以后续的包一般就是以0xC?开头。
1个字节的包头表示这个包的时间戳、数据大小、数据类型、流ID都和上一个相同ChannelID的RTMP包完全一样。
4、 AMFType - 数据类型
AMFType是RTMP包里面的数据的类型,占用1个字节。例如音频包的类型为8,视频包的类型为9。下面列出的是常用的数据类型:
AMFType | 数据包类型 |
0×08 | 音频数据包 |
0×09 | 视频数据包 |
… | 其他值这里不列出来了 |
5、 StreamID - 流ID
StreamID占用RTMP包头的最后4个字节,StreamID是音视频流的唯一ID,
一路流如果既有音频包又有视频包,那么这路流音频包的StreamID和他视频包的StreamID相同,但ChannelID不同。
ChannelID 和StreamID之间的计算公式:StreamID=(ChannelID-4)/5+1
如果这个封包既不是音频包,也不是视频包,那么他的StreamID=0。
也就是说AMFType不等于0×08并且不等于0×09时,那么他的StreamID=0。
例如当音视频包ChannelID为2、3、4时StreamID都为1 当音视频包ChannelID为9的时候StreamID为2
封包例子分析
例如有一个RTMP封包的数据 03 00 00 00 00 01 02 14 00 00 00 00 02 00 07 63 6F 6E 6E 65 63 74 00 3F F0 00 00 00 00 00 00 08 ,,,
数据依次解析的含义:
03表示12字节头,channelid=3
00 00 00表示时间戳 Timer=0
00 01 02表示AMFSize=18
14表示AMFType=Invoke 方法调用
00 00 00 00 表示StreamID = 0
到这里RTMP的包头信息就分析完毕了。
RTMP包数据AMF分析
Rtmp包默认的最大长度为128字节,(或通过chunksize改变rtmp包最大长度),
当AMF数据超过128Byte的时候就可能有多个rtmp包组成,如果需要解码的rtmp包太长则被TCP协议分割成多个TCP包。
那么解码的时候需要先将包含rtmp包的tcp封包合并, 再把合并的数据解码,解码后可得到amf格式的数据,将这些AMF数据取出来就可以对AMF数据解码了。
AMF数据里面可以是命令也可以是音视频数据。
组成服务器和Flash客户端之间的所有数据都是用AMF格式的数据在传送,例如connect() publish()等命令。
AMF数据由2部分组成: ObjType 加上 ObjValue。ObjType的大小为一个字节。ObjValue的大小不固定,和ObjType相关。
常用的ObjType类型和对应的ObjValue大小整理如下:
类型说明(ObjType) | 数据 | dataSize |
CORE_String | 0x02 | 2字节 (2字节的数据纪录了String的实际长度) |
CORE_Object | 0x03 | 0字节(开始嵌套0x00000009表示嵌套结束) |
NULL | 0x05 | 0字节 空字节无意义 |
CORE_NUMBER | 0x00 | 8字节 |
CORE_Map | 0x08 | 4字节(开始嵌套) |
CORE_BOOLEAN | 0x01 | 1字节 |
RTMP的相关API
最后我们来看一下使用RTMP推流的主要API: