RTMP 规范中文版 PDF 下载地址
本文是为截至发稿时止最新 Adobe 官方公布的 RTMP 规范。本文包含 RTMP 规范的全部内容。是第一个比较全面的 RTMP 规范的中译本。由于成文时间仓促,加上作者知识面所限,翻译错误之处在所难免,恳请各位朋友热心指出,可以直接在博客后面留言,先行谢过。rtmp_specification_1.0.pdf 官方下载地址http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf,国内下载地址http://download.csdn.net/detail/defonds/6312051。请随时关注官方文档更新:http://www.adobe.com/cn/devnet/rtmp.html。以下内容来自 rtmp_specification_1.0.pdf。
这里主要讨论的是简单握手协议。simple handshake是rtmp spec 1.0定义的握手方式。而complex handshake是后来变更的方式,Adobe没有公开。在此暂不讨论。
一个 RTMP 连接以握手开始。RTMP 的握手不同于其他协议;RTMP 握手由三个固定长度的块组成,而不是像其他协议一样的带有报头的可变长度的块。
客户端 (发起连接请求的终端) 和服务器端各自发送相同的三块。便于演示,当发送自客户端时这些块被指定为 C0、C1 和 C2;当发送自服务器端时这些块分别被指定为 S0、S1 和 S2。
版本号 (八位):在 C0 中,这一字段指示出客户端要求的 RTMP 版本号。在 S0 中,这一字段指示出服务器端选择的 RTMP 版本号。本文档中规范的版本号为 3。0、1、2 三个值是由早期其他产品使用的,是废弃值;4 - 31 被保留为 RTMP 协议的未来实现版本使用;32 - 255 不允许使用(以区分开 RTMP 和其他常以一个可打印字符开始的文本协议)。无法识别客户端所请求版本号的服务器应该以版本 3 响应,(收到响应的) 客户端可以选择降低到版本 3,或者放弃握手。
如上图,是一个c0+c1一起发送的抓包例子(complex handshake的)。其中选中的字节为c0字段,代表版本号。后面的数据是c1字段。整体包长1537.
由上表可知,当服务器和客户端的握手是按照rtmp协议进行,是不支持h264/aac的,有数据,就是没有视频和声音。原因是adobe变更了握手的数据结构,标准rtmp协议的握手的包是随机的1536字节(S1S2C1C2),变更后的是需要进行摘要和加密。rtmp协议定义的为simple handshake,变更后加密握手可称为complex handshake。
本文详细分析了rtmpd(ccrtmpserver)中的处理逻辑,以及rtmpdump的处理逻辑,从一个全是魔法数字的世界找到他们的数据结构和算法。
complex handshake将C1S1分为4个部分,它们的顺序(schema)一种可能是:
// c1s1 schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes
// c1s1 schema1
time: 4bytes
version: 4bytes
digest: 764bytes
key: 764bytes
无论key和digest位置如何,它们的结构是不变的:
// 764bytes key结构
random-data: (offset)bytes
key-data: 128bytes
random-data: (764-offset-128-4)bytes
offset: 4bytes
// 764bytes digest结构
offset: 4bytes
random-data: (offset)bytes
digest-data: 32bytes
random-data: (764-4-offset-32)bytes
备注:发现FMS只认识digest-key结构。
如下图所示:
// 1536bytes C2S2结构
random-data: 1504bytes
digest-data: 32bytes
C2S2的结构相对比较简单。如下图所示:
下面介绍C1S1C2S2的生成以及验证算法。
// C1S1被digest分成两部分
c1s1-part1: n bytes
digest-data: 32bytes
c1s1-part2: (1536-n-32)bytes
如下图所示:
在生成C1时,需要用到c1s1-part1和c1s1-part2这两个部分的字节拼接起来的字节,定义为:
c1s1-joined = bytes_join(c1s1-part1, c1s1-part2)
也就是说,把1536字节的c1s1中的32字节的digest拿剪刀剪掉,剩下的头和尾加在一起就是c1s1-joined。用到的两个常量FPKey和FMSKey:
u_int8_t FMSKey[] = {
0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
}; // 68
u_int8_t FPKey[] = {
0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
}; // 62
calc_c1_digest(c1, schema) {
get c1s1-joined from c1 by specified schema
digest-data = HMACsha256(c1s1-joined, FPKey, 30)
return digest-data;
}
random fill 1536bytes c1 // also fill the c1-128bytes-key
time = time() // c1[0-3]
version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
schema = choose schema0 or schema1
digest-data = calc_c1_digest(c1, schema)
copy digest-data to c1
生成S1的算法如下:
/*decode c1 try schema0 then schema1*/
c1-digest-data = get-c1-digest-data(schema0)
if c1-digest-data equals to calc_c1_digest(c1, schema0) {
c1-key-data = get-c1-key-data(schema0)
schema = schema0
} else {
c1-digest-data = get-c1-digest-data(schema1)
if c1-digest-data not equals to calc_c1_digest(c1, schema1) {
switch to simple handshake.
return
}
c1-key-data = get-c1-key-data(schema1)
schema = schema1
}
/*generate s1*/
random fill 1536bytes s1
time = time() // c1[0-3]
version = [0x04, 0x05, 0x00, 0x01] // s1[4-7]
DH_compute_key(key = c1-key-data, pub_key=s1-key-data)
get c1s1-joined by specified schema
s1-digest-data = HMACsha256(c1s1-joined, FMSKey, 36)
copy s1-digest-data and s1-key-data to s1.
C1S1的算法完毕。
random fill c2s2 1536 bytes
// client generate C2, or server valid C2
temp-key = HMACsha256(s1-digest, FPKey, 62)
c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
// server generate S2, or client valid S2
temp-key = HMACsha256(c1-digest, FMSKey, 68)
s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
验证的算法是一样的。
sources\thelib\src\protocols\rtmp\inboundrtmpprotocol.cpp: 51
InboundRTMPProtocol::PerformHandshake
// 没有完成握手。
case RTMP_STATE_NOT_INITIALIZED:
// buffer[1537]
// 第一个字节,即c0,表示握手类型(03是明文,06是加密,其他值非法)
handshakeType = GETIBPOINTER(buffer)[0];
// 删除第一个字节,buffer[1536] 即c1
buffer.Ignore(1);
// 第5-8共4个字节表示FPVersion,这个必须非0,0表示不支持complex handshake。
_currentFPVersion = ENTOHLP(GETIBPOINTER(buffer) + 4);
// 进行明文握手(false表示明文)
PerformHandshake(buffer, false);
InboundRTMPProtocol::PerformHandshake
// 先验证client,即验证c1
// 先尝试scheme0
valid = ValidClientScheme(0);
// 若失败,再尝试scheme1
valid = ValidClientScheme(1)
// 若验证成功
if(valid)
// 复杂的handshake:PerformComplexHandshake,主要流程如下:
S0 = 3或6
随机数初始化S1S2
根据C1的public key生成S1的128byets public key
生成S1的32bytes digest
根据C1和S2生成S2的32bytes digest
else
// rtmp spec 1.0定义的握手方式
PerformSimpleHandshake();
complex的包结构如下:C1/S1 1536bytes
time: 4bytes 开头是4字节的当前时间。(u_int32_t)time(NULL)
peer_version: 4bytes 为程序版本。C1一般是0x80000702。S1是0x04050001。
764bytes: 可能是KeyBlock或者DigestBlock
764bytes: 可能是KeyBlock或者DigestBlock
其中scheme1就是KeyBlock在前面DigestBlock在后面,而scheme0是DigestBlock在前面KeyBlock在后面。
子结构KeyBlock定义:
760bytes: 包含128bytes的key的数据。
key_offset: 4bytes 最后4字节定义了key的offset(相对于KeyBlock开头而言)
子结构DigestBlock定义:
digest_offset: 4bytes 开头4字节定义了digest的offset(相对于第DigestBlock的第5字节而言,offset=3表示digestBlock[7~38]为digest
760bytes: 包含32bytes的digest的数据。
其中,key和digest的主要算法是:C1的key为128bytes随机数。C1_32bytes_digest = HMACsha256(P1+P2, 1504, FPKey, 30) ,其中P1为digest之前的部分,P2为digest之后的部分,P1+P2是将这两部分拷贝到新的数组,共1536-32长度。S1的key根据C1的key算出来,算法如下:
DHWrapper dhWrapper(1024);
dhWrapper.Initialize()
dhWrapper.CreateSharedKey(c1_key, 128)
dhWrapper.CopyPublicKey(s1_key, 128)
S1的digest算法同C1。注意,必须先计算S1的key,因为key变化后digest也也重新计算。
先用随机数填充S2
s2data=S2[0-1504]; 前1502字节为随机的数据。
s2digest=S2[1505-1526] 后32bytes为digest。
// 计算s2digest方法如下:
ptemphash[512]: HMACsha256(c1digest, 32, FMSKey, 68, ptemhash)
ps2hash[512]: HMACsha256(s2data, 1504, ptemphash, 32, ps2hash)
将ps2hash[0-31]拷贝到s2的后32bytes。
/*
The MIT License (MIT)
Copyright (c) 2013 winlin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef SRS_CORE_COMPLEX_HANDSHKAE_HPP
#define SRS_CORE_COMPLEX_HANDSHKAE_HPP
/*
#include
*/
#include
class SrsSocket;
/**
* rtmp complex handshake,
* @see also crtmp(crtmpserver) or librtmp,
* @see also: http://blog.csdn.net/win_lin/article/details/13006803
* @doc update the README.cmd
*/
class SrsComplexHandshake
{
public:
SrsComplexHandshake();
virtual ~SrsComplexHandshake();
public:
/**
* complex hanshake.
* @_c1, size of c1 must be 1536.
* @remark, user must free the c1.
* @return user must:
* continue connect app if success,
* try simple handshake if error is ERROR_RTMP_TRY_SIMPLE_HS,
* otherwise, disconnect
*/
virtual int handshake(SrsSocket& skt, char* _c1);
};
#endif
/*
The MIT License (MIT)
Copyright (c) 2013 winlin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include
#include
#include
#include
#include
#include
#include
// 68bytes FMS key which is used to sign the sever packet.
u_int8_t SrsGenuineFMSKey[] = {
0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
}; // 68
// 62bytes FP key which is used to sign the client packet.
u_int8_t SrsGenuineFPKey[] = {
0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
}; // 62
#include
#include
int openssl_HMACsha256(const void* data, int data_size, const void* key, int key_size, void* digest) {
HMAC_CTX ctx;
HMAC_CTX_init(&ctx);
HMAC_Init_ex(&ctx, (unsigned char*) key, key_size, EVP_sha256(), NULL);
HMAC_Update(&ctx, (unsigned char *) data, data_size);
unsigned int digest_size;
HMAC_Final(&ctx, (unsigned char *) digest, &digest_size);
HMAC_CTX_cleanup(&ctx);
if (digest_size != 32) {
return ERROR_OpenSslSha256DigestSize;
}
return ERROR_SUCCESS;
}
#include
#define RFC2409_PRIME_1024 \
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" \
"29024E088A67CC74020BBEA63B139B22514A08798E3404DD" \
"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" \
"E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" \
"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" \
"FFFFFFFFFFFFFFFF"
int __openssl_generate_key(
u_int8_t*& _private_key, u_int8_t*& _public_key, int32_t& size,
DH*& pdh, int32_t& bits_count, u_int8_t*& shared_key, int32_t& shared_key_length, BIGNUM*& peer_public_key
){
int ret = ERROR_SUCCESS;
//1. Create the DH
if ((pdh = DH_new()) == NULL) {
ret = ERROR_OpenSslCreateDH;
return ret;
}
//2. Create his internal p and g
if ((pdh->p = BN_new()) == NULL) {
ret = ERROR_OpenSslCreateP;
return ret;
}
if ((pdh->g = BN_new()) == NULL) {
ret = ERROR_OpenSslCreateG;
return ret;
}
//3. initialize p, g and key length
if (BN_hex2bn(&pdh->p, RFC2409_PRIME_1024) == 0) {
ret = ERROR_OpenSslParseP1024;
return ret;
}
if (BN_set_word(pdh->g, 2) != 1) {
ret = ERROR_OpenSslSetG;
return ret;
}
//4. Set the key length
pdh->length = bits_count;
//5. Generate private and public key
if (DH_generate_key(pdh) != 1) {
ret = ERROR_OpenSslGenerateDHKeys;
return ret;
}
// CreateSharedKey
if (pdh == NULL) {
ret = ERROR_OpenSslGenerateDHKeys;
return ret;
}
if (shared_key_length != 0 || shared_key != NULL) {
ret = ERROR_OpenSslShareKeyComputed;
return ret;
}
shared_key_length = DH_size(pdh);
if (shared_key_length <= 0 || shared_key_length > 1024) {
ret = ERROR_OpenSslGetSharedKeySize;
return ret;
}
shared_key = new u_int8_t[shared_key_length];
memset(shared_key, 0, shared_key_length);
peer_public_key = BN_bin2bn(_private_key, size, 0);
if (peer_public_key == NULL) {
ret = ERROR_OpenSslGetPeerPublicKey;
return ret;
}
if (DH_compute_key(shared_key, peer_public_key, pdh) == -1) {
ret = ERROR_OpenSslComputeSharedKey;
return ret;
}
// CopyPublicKey
if (pdh == NULL) {
ret = ERROR_OpenSslComputeSharedKey;
return ret;
}
int32_t keySize = BN_num_bytes(pdh->pub_key);
if ((keySize <= 0) || (size <= 0) || (keySize > size)) {
//("CopyPublicKey failed due to either invalid DH state or invalid call"); return ret;
ret = ERROR_OpenSslInvalidDHState;
return ret;
}
if (BN_bn2bin(pdh->pub_key, _public_key) != keySize) {
//("Unable to copy key"); return ret;
ret = ERROR_OpenSslCopyKey;
return ret;
}
return ret;
}
int openssl_generate_key(char* _private_key, char* _public_key, int32_t size)
{
int ret = ERROR_SUCCESS;
// Initialize
DH* pdh = NULL;
int32_t bits_count = 1024;
u_int8_t* shared_key = NULL;
int32_t shared_key_length = 0;
BIGNUM* peer_public_key = NULL;
ret = __openssl_generate_key(
(u_int8_t*&)_private_key, (u_int8_t*&)_public_key, size,
pdh, bits_count, shared_key, shared_key_length, peer_public_key
);
if (pdh != NULL) {
if (pdh->p != NULL) {
BN_free(pdh->p);
pdh->p = NULL;
}
if (pdh->g != NULL) {
BN_free(pdh->g);
pdh->g = NULL;
}
DH_free(pdh);
pdh = NULL;
}
if (shared_key != NULL) {
delete[] shared_key;
shared_key = NULL;
}
if (peer_public_key != NULL) {
BN_free(peer_public_key);
peer_public_key = NULL;
}
return ret;
}
// the digest key generate size.
#define OpensslHashSize 512
/**
* 764bytes key结构
* random-data: (offset)bytes
* key-data: 128bytes
* random-data: (764-offset-128-4)bytes
* offset: 4bytes
*/
struct key_block
{
// (offset)bytes
char* random0;
int random0_size;
// 128bytes
char key[128];
// (764-offset-128-4)bytes
char* random1;
int random1_size;
// 4bytes
int32_t offset;
};
// calc the offset of key,
// the key->offset cannot be used as the offset of key.
int srs_key_block_get_offset(key_block* key)
{
int max_offset_size = 764 - 128 - 4;
int offset = 0;
u_int8_t* pp = (u_int8_t*)&key->offset;
offset += *pp++;
offset += *pp++;
offset += *pp++;
offset += *pp++;
return offset % max_offset_size;
}
// create new key block data.
// if created, user must free it by srs_key_block_free
void srs_key_block_init(key_block* key)
{
key->offset = (int32_t)rand();
key->random0 = NULL;
key->random1 = NULL;
int offset = srs_key_block_get_offset(key);
srs_assert(offset >= 0);
key->random0_size = offset;
if (key->random0_size > 0) {
key->random0 = new char[key->random0_size];
for (int i = 0; i < key->random0_size; i++) {
*(key->random0 + i) = rand() % 256;
}
}
for (int i = 0; i < (int)sizeof(key->key); i++) {
*(key->key + i) = rand() % 256;
}
key->random1_size = 764 - offset - 128 - 4;
if (key->random1_size > 0) {
key->random1 = new char[key->random1_size];
for (int i = 0; i < key->random1_size; i++) {
*(key->random1 + i) = rand() % 256;
}
}
}
// parse key block from c1s1.
// if created, user must free it by srs_key_block_free
// @c1s1_key_bytes the key start bytes, maybe c1s1 or c1s1+764
int srs_key_block_parse(key_block* key, char* c1s1_key_bytes)
{
int ret = ERROR_SUCCESS;
char* pp = c1s1_key_bytes + 764;
pp -= sizeof(int32_t);
key->offset = *(int32_t*)pp;
key->random0 = NULL;
key->random1 = NULL;
int offset = srs_key_block_get_offset(key);
srs_assert(offset >= 0);
pp = c1s1_key_bytes;
key->random0_size = offset;
if (key->random0_size > 0) {
key->random0 = new char[key->random0_size];
memcpy(key->random0, pp, key->random0_size);
}
pp += key->random0_size;
memcpy(key->key, pp, sizeof(key->key));
pp += sizeof(key->key);
key->random1_size = 764 - offset - 128 - 4;
if (key->random1_size > 0) {
key->random1 = new char[key->random1_size];
memcpy(key->random1, pp, key->random1_size);
}
return ret;
}
// free the block data create by
// srs_key_block_init or srs_key_block_parse
void srs_key_block_free(key_block* key)
{
if (key->random0) {
srs_freepa(key->random0);
}
if (key->random1) {
srs_freepa(key->random1);
}
}
/**
* 764bytes digest结构
* offset: 4bytes
* random-data: (offset)bytes
* digest-data: 32bytes
* random-data: (764-4-offset-32)bytes
*/
struct digest_block
{
// 4bytes
int32_t offset;
// (offset)bytes
char* random0;
int random0_size;
// 32bytes
char digest[32];
// (764-4-offset-32)bytes
char* random1;
int random1_size;
};
// calc the offset of digest,
// the key->offset cannot be used as the offset of digest.
int srs_digest_block_get_offset(digest_block* digest)
{
int max_offset_size = 764 - 32 - 4;
int offset = 0;
u_int8_t* pp = (u_int8_t*)&digest->offset;
offset += *pp++;
offset += *pp++;
offset += *pp++;
offset += *pp++;
return offset % max_offset_size;
}
// create new digest block data.
// if created, user must free it by srs_digest_block_free
void srs_digest_block_init(digest_block* digest)
{
digest->offset = (int32_t)rand();
digest->random0 = NULL;
digest->random1 = NULL;
int offset = srs_digest_block_get_offset(digest);
srs_assert(offset >= 0);
digest->random0_size = offset;
if (digest->random0_size > 0) {
digest->random0 = new char[digest->random0_size];
for (int i = 0; i < digest->random0_size; i++) {
*(digest->random0 + i) = rand() % 256;
}
}
for (int i = 0; i < (int)sizeof(digest->digest); i++) {
*(digest->digest + i) = rand() % 256;
}
digest->random1_size = 764 - 4 - offset - 32;
if (digest->random1_size > 0) {
digest->random1 = new char[digest->random1_size];
for (int i = 0; i < digest->random1_size; i++) {
*(digest->random1 + i) = rand() % 256;
}
}
}
// parse digest block from c1s1.
// if created, user must free it by srs_digest_block_free
// @c1s1_digest_bytes the digest start bytes, maybe c1s1 or c1s1+764
int srs_digest_block_parse(digest_block* digest, char* c1s1_digest_bytes)
{
int ret = ERROR_SUCCESS;
char* pp = c1s1_digest_bytes;
digest->offset = *(int32_t*)pp;
pp += sizeof(int32_t);
digest->random0 = NULL;
digest->random1 = NULL;
int offset = srs_digest_block_get_offset(digest);
srs_assert(offset >= 0);
digest->random0_size = offset;
if (digest->random0_size > 0) {
digest->random0 = new char[digest->random0_size];
memcpy(digest->random0, pp, digest->random0_size);
}
pp += digest->random0_size;
memcpy(digest->digest, pp, sizeof(digest->digest));
pp += sizeof(digest->digest);
digest->random1_size = 764 - 4 - offset - 32;
if (digest->random1_size > 0) {
digest->random1 = new char[digest->random1_size];
memcpy(digest->random1, pp, digest->random1_size);
}
return ret;
}
// free the block data create by
// srs_digest_block_init or srs_digest_block_parse
void srs_digest_block_free(digest_block* digest)
{
if (digest->random0) {
srs_freepa(digest->random0);
}
if (digest->random1) {
srs_freepa(digest->random1);
}
}
/**
* the schema type.
*/
enum srs_schema_type {
srs_schema0 = 0, // key-digest sequence
srs_schema1 = 1, // digest-key sequence
srs_schema_invalid = 2,
};
void __srs_time_copy_to(char*& pp, int32_t time)
{
// 4bytes time
*(int32_t*)pp = time;
pp += 4;
}
void __srs_version_copy_to(char*& pp, int32_t version)
{
// 4bytes version
*(int32_t*)pp = version;
pp += 4;
}
void __srs_key_copy_to(char*& pp, key_block* key)
{
// 764bytes key block
if (key->random0_size > 0) {
memcpy(pp, key->random0, key->random0_size);
}
pp += key->random0_size;
memcpy(pp, key->key, sizeof(key->key));
pp += sizeof(key->key);
if (key->random1_size > 0) {
memcpy(pp, key->random1, key->random1_size);
}
pp += key->random1_size;
*(int32_t*)pp = key->offset;
pp += 4;
}
void __srs_digest_copy_to(char*& pp, digest_block* digest, bool with_digest)
{
// 732bytes digest block without the 32bytes digest-data
// nbytes digest block part1
*(int32_t*)pp = digest->offset;
pp += 4;
if (digest->random0_size > 0) {
memcpy(pp, digest->random0, digest->random0_size);
}
pp += digest->random0_size;
// digest
if (with_digest) {
memcpy(pp, digest->digest, 32);
pp += 32;
}
// nbytes digest block part2
if (digest->random1_size > 0) {
memcpy(pp, digest->random1, digest->random1_size);
}
pp += digest->random1_size;
}
/**
* copy whole c1s1 to bytes.
*/
void srs_schema0_copy_to(char* bytes, bool with_digest,
int32_t time, int32_t version, key_block* key, digest_block* digest)
{
char* pp = bytes;
__srs_time_copy_to(pp, time);
__srs_version_copy_to(pp, version);
__srs_key_copy_to(pp, key);
__srs_digest_copy_to(pp, digest, with_digest);
if (with_digest) {
srs_assert(pp - bytes == 1536);
} else {
srs_assert(pp - bytes == 1536 - 32);
}
}
void srs_schema1_copy_to(char* bytes, bool with_digest,
int32_t time, int32_t version, digest_block* digest, key_block* key)
{
char* pp = bytes;
__srs_time_copy_to(pp, time);
__srs_version_copy_to(pp, version);
__srs_digest_copy_to(pp, digest, with_digest);
__srs_key_copy_to(pp, key);
if (with_digest) {
srs_assert(pp - bytes == 1536);
} else {
srs_assert(pp - bytes == 1536 - 32);
}
}
/**
* c1s1 is splited by digest:
* c1s1-part1: n bytes (time, version, key and digest-part1).
* digest-data: 32bytes
* c1s1-part2: (1536-n-32)bytes (digest-part2)
*/
char* srs_bytes_join_schema0(int32_t time, int32_t version, key_block* key, digest_block* digest)
{
char* bytes = new char[1536 -32];
srs_schema0_copy_to(bytes, false, time, version, key, digest);
return bytes;
}
/**
* c1s1 is splited by digest:
* c1s1-part1: n bytes (time, version and digest-part1).
* digest-data: 32bytes
* c1s1-part2: (1536-n-32)bytes (digest-part2 and key)
*/
char* srs_bytes_join_schema1(int32_t time, int32_t version, digest_block* digest, key_block* key)
{
char* bytes = new char[1536 -32];
srs_schema1_copy_to(bytes, false, time, version, digest, key);
return bytes;
}
/**
* compare the memory in bytes.
*/
bool srs_bytes_equals(void* pa, void* pb, int size){
u_int8_t* a = (u_int8_t*)pa;
u_int8_t* b = (u_int8_t*)pb;
for(int i = 0; i < size; i++){
if(a[i] != b[i]){
return false;
}
}
return true;
}
/**
* c1s1 schema0
* time: 4bytes
* version: 4bytes
* key: 764bytes
* digest: 764bytes
* c1s1 schema1
* time: 4bytes
* version: 4bytes
* digest: 764bytes
* key: 764bytes
*/
struct c1s1
{
union block {
key_block key;
digest_block digest;
};
// 4bytes
int32_t time;
// 4bytes
int32_t version;
// 764bytes
// if schema0, use key
// if schema1, use digest
block block0;
// 764bytes
// if schema0, use digest
// if schema1, use key
block block1;
// the logic schema
srs_schema_type schema;
c1s1();
virtual ~c1s1();
/**
* get the digest key.
*/
virtual char* get_digest();
/**
* copy to bytes.
*/
virtual void dump(char* _c1s1);
/**
* client: create and sign c1 by schema.
* sign the c1, generate the digest.
* calc_c1_digest(c1, schema) {
* get c1s1-joined from c1 by specified schema
* digest-data = HMACsha256(c1s1-joined, FPKey, 30)
* return digest-data;
* }
* random fill 1536bytes c1 // also fill the c1-128bytes-key
* time = time() // c1[0-3]
* version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
* schema = choose schema0 or schema1
* digest-data = calc_c1_digest(c1, schema)
* copy digest-data to c1
*/
virtual int c1_create(srs_schema_type _schema);
/**
* server: parse the c1s1, discovery the key and digest by schema.
* use the c1_validate_digest() to valid the digest of c1.
*/
virtual int c1_parse(char* _c1s1, srs_schema_type _schema);
/**
* server: validate the parsed schema and c1s1
*/
virtual int c1_validate_digest(bool& is_valid);
/**
* server: create and sign the s1 from c1.
*/
virtual int s1_create(c1s1* c1);
private:
virtual int calc_s1_digest(char*& digest);
virtual int calc_c1_digest(char*& digest);
virtual void destroy_blocks();
};
/**
* the c2s2 complex handshake structure.
* random-data: 1504bytes
* digest-data: 32bytes
*/
struct c2s2
{
char random[1504];
char digest[32];
c2s2();
virtual ~c2s2();
/**
* copy to bytes.
*/
virtual void dump(char* _c2s2);
/**
* create c2.
* random fill c2s2 1536 bytes
*
* // client generate C2, or server valid C2
* temp-key = HMACsha256(s1-digest, FPKey, 62)
* c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
*/
virtual int c2_create(c1s1* s1);
/**
* create s2.
* random fill c2s2 1536 bytes
*
* // server generate S2, or client valid S2
* temp-key = HMACsha256(c1-digest, FMSKey, 68)
* s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
*/
virtual int s2_create(c1s1* c1);
};
c2s2::c2s2()
{
for (int i = 0; i < 1504; i++) {
*(random + i) = rand();
}
for (int i = 0; i < 32; i++) {
*(digest + i) = rand();
}
}
c2s2::~c2s2()
{
}
void c2s2::dump(char* _c2s2)
{
memcpy(_c2s2, random, 1504);
memcpy(_c2s2 + 1504, digest, 32);
}
int c2s2::c2_create(c1s1* s1)
{
int ret = ERROR_SUCCESS;
char temp_key[OpensslHashSize];
if ((ret = openssl_HMACsha256(s1->get_digest(), 32, SrsGenuineFPKey, 62, temp_key)) != ERROR_SUCCESS) {
srs_error("create c2 temp key failed. ret=%d", ret);
return ret;
}
srs_verbose("generate c2 temp key success.");
char _digest[OpensslHashSize];
if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
srs_error("create c2 digest failed. ret=%d", ret);
return ret;
}
srs_verbose("generate c2 digest success.");
memcpy(digest, _digest, 32);
return ret;
}
int c2s2::s2_create(c1s1* c1)
{
int ret = ERROR_SUCCESS;
char temp_key[OpensslHashSize];
if ((ret = openssl_HMACsha256(c1->get_digest(), 32, SrsGenuineFMSKey, 68, temp_key)) != ERROR_SUCCESS) {
srs_error("create s2 temp key failed. ret=%d", ret);
return ret;
}
srs_verbose("generate s2 temp key success.");
char _digest[OpensslHashSize];
if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
srs_error("create s2 digest failed. ret=%d", ret);
return ret;
}
srs_verbose("generate s2 digest success.");
memcpy(digest, _digest, 32);
return ret;
}
c1s1::c1s1()
{
schema = srs_schema_invalid;
}
c1s1::~c1s1()
{
destroy_blocks();
}
char* c1s1::get_digest()
{
srs_assert(schema != srs_schema_invalid);
if (schema == srs_schema0) {
return block1.digest.digest;
} else {
return block0.digest.digest;
}
}
void c1s1::dump(char* _c1s1)
{
srs_assert(schema != srs_schema_invalid);
if (schema == srs_schema0) {
srs_schema0_copy_to(_c1s1, true, time, version, &block0.key, &block1.digest);
} else {
srs_schema1_copy_to(_c1s1, true, time, version, &block0.digest, &block1.key);
}
}
int c1s1::c1_create(srs_schema_type _schema)
{
int ret = ERROR_SUCCESS;
if (_schema == srs_schema_invalid) {
ret = ERROR_RTMP_CH_SCHEMA;
srs_error("create c1 failed. invalid schema=%d, ret=%d", _schema, ret);
return ret;
}
destroy_blocks();
time = ::time(NULL);
version = 0x02070080; // client c1 version
if (_schema == srs_schema0) {
srs_key_block_init(&block0.key);
srs_digest_block_init(&block1.digest);
} else {
srs_digest_block_init(&block0.digest);
srs_key_block_init(&block1.key);
}
schema = _schema;
char* digest = NULL;
if ((ret = calc_c1_digest(digest)) != ERROR_SUCCESS) {
srs_error("sign c1 error, failed to calc digest. ret=%d", ret);
return ret;
}
srs_assert(digest != NULL);
SrsAutoFree(char, digest, true);
if (schema == srs_schema0) {
memcpy(block1.digest.digest, digest, 32);
} else {
memcpy(block0.digest.digest, digest, 32);
}
return ret;
}
int c1s1::c1_parse(char* _c1s1, srs_schema_type _schema)
{
int ret = ERROR_SUCCESS;
if (_schema == srs_schema_invalid) {
ret = ERROR_RTMP_CH_SCHEMA;
srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
return ret;
}
destroy_blocks();
time = *(int32_t*)_c1s1;
version = *(int32_t*)(_c1s1 + 4); // client c1 version
if (_schema == srs_schema0) {
if ((ret = srs_key_block_parse(&block0.key, _c1s1 + 8)) != ERROR_SUCCESS) {
srs_error("parse the c1 key failed. ret=%d", ret);
return ret;
}
if ((ret = srs_digest_block_parse(&block1.digest, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
srs_error("parse the c1 digest failed. ret=%d", ret);
return ret;
}
srs_verbose("parse c1 key-digest success");
} else if (_schema == srs_schema1) {
if ((ret = srs_digest_block_parse(&block0.digest, _c1s1 + 8)) != ERROR_SUCCESS) {
srs_error("parse the c1 key failed. ret=%d", ret);
return ret;
}
if ((ret = srs_key_block_parse(&block1.key, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
srs_error("parse the c1 digest failed. ret=%d", ret);
return ret;
}
srs_verbose("parse c1 digest-key success");
} else {
ret = ERROR_RTMP_CH_SCHEMA;
srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
return ret;
}
schema = _schema;
return ret;
}
int c1s1::c1_validate_digest(bool& is_valid)
{
int ret = ERROR_SUCCESS;
char* c1_digest = NULL;
if ((ret = calc_c1_digest(c1_digest)) != ERROR_SUCCESS) {
srs_error("validate c1 error, failed to calc digest. ret=%d", ret);
return ret;
}
srs_assert(c1_digest != NULL);
SrsAutoFree(char, c1_digest, true);
if (schema == srs_schema0) {
is_valid = srs_bytes_equals(block1.digest.digest, c1_digest, 32);
} else {
is_valid = srs_bytes_equals(block0.digest.digest, c1_digest, 32);
}
return ret;
}
int c1s1::s1_create(c1s1* c1)
{
int ret = ERROR_SUCCESS;
if (c1->schema == srs_schema_invalid) {
ret = ERROR_RTMP_CH_SCHEMA;
srs_error("create s1 failed. invalid schema=%d, ret=%d", c1->schema, ret);
return ret;
}
destroy_blocks();
schema = c1->schema;
time = ::time(NULL);
version = 0x01000504; // server s1 version
if (schema == srs_schema0) {
srs_key_block_init(&block0.key);
srs_digest_block_init(&block1.digest);
} else {
srs_digest_block_init(&block0.digest);
srs_key_block_init(&block1.key);
}
if (schema == srs_schema0) {
if ((ret = openssl_generate_key(c1->block0.key.key, block0.key.key, 128)) != ERROR_SUCCESS) {
srs_error("calc s1 key failed. ret=%d", ret);
return ret;
}
} else {
if ((ret = openssl_generate_key(c1->block1.key.key, block1.key.key, 128)) != ERROR_SUCCESS) {
srs_error("calc s1 key failed. ret=%d", ret);
return ret;
}
}
srs_verbose("calc s1 key success.");
char* s1_digest = NULL;
if ((ret = calc_s1_digest(s1_digest)) != ERROR_SUCCESS) {
srs_error("calc s1 digest failed. ret=%d", ret);
return ret;
}
srs_verbose("calc s1 digest success.");
srs_assert(s1_digest != NULL);
SrsAutoFree(char, s1_digest, true);
if (schema == srs_schema0) {
memcpy(block1.digest.digest, s1_digest, 32);
} else {
memcpy(block0.digest.digest, s1_digest, 32);
}
srs_verbose("copy s1 key success.");
return ret;
}
int c1s1::calc_s1_digest(char*& digest)
{
int ret = ERROR_SUCCESS;
srs_assert(schema == srs_schema0 || schema == srs_schema1);
char* c1s1_joined_bytes = NULL;
if (schema == srs_schema0) {
c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
} else {
c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
}
srs_assert(c1s1_joined_bytes != NULL);
SrsAutoFree(char, c1s1_joined_bytes, true);
digest = new char[OpensslHashSize];
if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFMSKey, 36, digest)) != ERROR_SUCCESS) {
srs_error("calc digest for s1 failed. ret=%d", ret);
return ret;
}
srs_verbose("digest calculated for s1");
return ret;
}
int c1s1::calc_c1_digest(char*& digest)
{
int ret = ERROR_SUCCESS;
srs_assert(schema == srs_schema0 || schema == srs_schema1);
char* c1s1_joined_bytes = NULL;
if (schema == srs_schema0) {
c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
} else {
c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
}
srs_assert(c1s1_joined_bytes != NULL);
SrsAutoFree(char, c1s1_joined_bytes, true);
digest = new char[OpensslHashSize];
if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFPKey, 30, digest)) != ERROR_SUCCESS) {
srs_error("calc digest for c1 failed. ret=%d", ret);
return ret;
}
srs_verbose("digest calculated for c1");
return ret;
}
void c1s1::destroy_blocks()
{
if (schema == srs_schema_invalid) {
return;
}
if (schema == srs_schema0) {
srs_key_block_free(&block0.key);
srs_digest_block_free(&block1.digest);
} else {
srs_digest_block_free(&block0.digest);
srs_key_block_free(&block1.key);
}
}
SrsComplexHandshake::SrsComplexHandshake()
{
}
SrsComplexHandshake::~SrsComplexHandshake()
{
}
int SrsComplexHandshake::handshake(SrsSocket& skt, char* _c1)
{
int ret = ERROR_SUCCESS;
ssize_t nsize;
static bool _random_initialized = false;
if (!_random_initialized) {
srand(0);
_random_initialized = true;
srs_trace("srand initialized the random.");
}
// decode c1
c1s1 c1;
// try schema0.
if ((ret = c1.c1_parse(_c1, srs_schema0)) != ERROR_SUCCESS) {
srs_error("parse c1 schema%d error. ret=%d", srs_schema0, ret);
return ret;
}
// try schema1
bool is_valid = false;
if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
if ((ret = c1.c1_parse(_c1, srs_schema1)) != ERROR_SUCCESS) {
srs_error("parse c1 schema%d error. ret=%d", srs_schema1, ret);
return ret;
}
if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
ret = ERROR_RTMP_TRY_SIMPLE_HS;
srs_info("all schema valid failed, try simple handshake. ret=%d", ret);
return ret;
}
}
srs_verbose("decode c1 success.");
// encode s1
c1s1 s1;
if ((ret = s1.s1_create(&c1)) != ERROR_SUCCESS) {
srs_error("create s1 from c1 failed. ret=%d", ret);
return ret;
}
srs_verbose("create s1 from c1 success.");
c2s2 s2;
if ((ret = s2.s2_create(&c1)) != ERROR_SUCCESS) {
srs_error("create s2 from c1 failed. ret=%d", ret);
return ret;
}
srs_verbose("create s2 from c1 success.");
// sendout s0s1s2
char* s0s1s2 = new char[3073];
SrsAutoFree(char, s0s1s2, true);
// plain text required.
s0s1s2[0] = 0x03;
s1.dump(s0s1s2 + 1);
s2.dump(s0s1s2 + 1537);
if ((ret = skt.write(s0s1s2, 3073, &nsize)) != ERROR_SUCCESS) {
srs_warn("complex handshake send s0s1s2 failed. ret=%d", ret);
return ret;
}
srs_verbose("complex handshake send s0s1s2 success.");
// recv c2
char* c2 = new char[1536];
SrsAutoFree(char, c2, true);
if ((ret = skt.read_fully(c2, 1536, &nsize)) != ERROR_SUCCESS) {
srs_warn("complex handshake read c2 failed. ret=%d", ret);
return ret;
}
srs_verbose("complex handshake read c2 success.");
return ret;
}
Chunk Data (可变的大小):当前块的有效负载,大小由配置决定。
例如:如果csid=288=224+64,那么应该表示成fmt00000011100000(2-byte)或者fmt0000010000000011100000(3-byte)。
csid (六位):这一字段包含有块流 ID,值的范围是 2 - 63。fmt后面紧跟着的6位的值, 0 和 1 用于指示这一字段是 2- 或者 3- 字节版本。
fmt (两个字节):这一字段指示 'chunk message header' 使用的四种格式之一。每种块类型的 'chunk message header' 会在下一小节解释。类型 0 块头的长度是 11 个字节。即AMF0包头为11字节。这一类型必须用在块流的起始位置,和流 timestamp 重来的时候 (比如,重置)。
图例1:符合AMF0类型的协议控制包(play&set buffer length)
例如图例1,是一个fmt=0的rtmp的协议控制包(play&set buffer length),csid=8。图中选中字段为包含消息流ID一字节的AMF0包头(12字节,去掉第一个字节的0x08,即为AMF0的11字节包头)。其中timestamp(3个字节,00 01 8d) 为397,message length(body size字段,3个字节,00 00 18)为24.type id(stream id,1个字节)代表body的帧类型为0x11(AMF3 command类型,1字节 0x11),代表是指令帧。msg stream id为1(四个字节,01 00 00 00).
类型 1 块头长为 7 个字节。即AMF1包头为7字节。不包含消息流 ID;这一块使用前一块一样的流 ID。可变长度消息的流 (例如,一些视频格式) 应该在第一块之后使用这一格式表示之后的每个新消息。
图例2:符合AMF1类型的视频包
例如图例2,是一个fmt=1的rtmp的视频包。csid=4,图中选中字段为包含消息流ID一字节的AMF1包头(8字节,去掉第一个字节的0x44,即为AMF1的包头)。其中timestamp delta为6,message length(body size字段)为628.type id代表body的帧类型为9,是视频帧。
类型 2 块头长度为 3 个字节。即AMF2包头为3字节。既不包含流 ID 也不包含消息长度;这一块具有和前一块相同的流 ID 和消息长度。具有不变长度的消息 (例如,一些音频和数据格式) 应该在第一块之后使用这一格式表示之后的每个新消息。
图例3:符合AMF2类型的包
例如图例3,是一个fmt=2的rtmp的固定长度的数据包。csid=37,图中选中字段为不包含消息流ID 1字节的AMF2包头(3字节,消息流ID为0xa5)。其中timestamp delta为4357388,无message length字段,因为默认与前以对应csid包的数据大小相同。没有帧类型,紧跟数据。
这一节描述了在服务器端和客户端彼此通信交换的消息和命令的不同的类型。
服务器端和客户端交换的不同消息类型包括用于发送音频数据的音频消息、用于发送视频数据的视频消息、用于发送任意用户数据的数据消息、共享对象消息以及命令消息。共享对象消息提供了一个通用的方法来管理多用户和一台服务器之间的分布式数据。命令消息在客户端和服务器端传输 AMF 编码的命令。客户端或者服务器端可以通过使用命令消息和对端通信的流请求远程方法调用 (RPC)。
事件 | 描述 |
---|---|
Use(=1) | 客户端发送这一事件以通知服务器端一个已命名的共享对象已创建。 |
Release(=2) | 当共享对象在客户端被删除时客户端发送这一事件到服务器端。 |
Request Change (=3) | 客户端发送给服务器端这一事件以请求共享对象的已命名的参数所关联到的值的改变。 |
Change (=4) | 服务器端发送这一事件已通知发起这一请求之外的所有客户端,一个已命名参数的值的改变。 |
Success (=5) | 如果请求被接受,服务器端发送这一事件给请求的客户端,以作为 RequestChange 事件的响应。 |
SendMessage (=6) | 客户端发送这一事件到服务器端以广播一条消息。一旦接收到这一事件,服务器端将会给所有的客户端广播这一消息,包括这一消息的发起者。 |
Status (=7) | 服务器端发送这一事件以通知客户端异常情况。 |
Clear (=8) | 服务器端发送这一消息到客户端以清理一个共享对象。服务器端也会对客户端发送的 Use 事件使用这一事件进行响应。 |
Remove (=9) | 服务器端发送这一事件有客户端删除一个 slot。 |
Request Remove (=10) | 客户端发送这一事件有客户端删除一个 slot。 |
Use Success (=11) | 服务器端发送给客户端这一事件表示连接成功。 |
事件 | 描述 |
Stream Begin (=0) | 服务器发送这个事件来通知客户端一个流已就绪并可以用来通信。默认情况下,这一事件在成功接收到客户端的应用连接命令之后以 ID 0 发送。这一事件数据为 4 字节,代表了已就绪流的流 ID。 |
Stream EOF (=1) | 服务器端发送这一事件来通知客户端请求的流的回放数据已经结束。在发送额外的命令之前不再发送任何数据。客户端将丢弃接收到的这个流的消息。这一事件数据为 4 字节,代表了回放已结束的流的流 ID。 |
StreamDry (=2) | 服务器端发送这一事件来通知客户端当前流中已没有数据。当服务器端在一段时间内没有检测到任何消息,它可以通知相关客户端当前流已经没数据了。这一事件数据为 4 字节,代表了已没数据的流的流 ID。 |
SetBuffer Length (=3) | 客户端发送这一事件来通知服务器端用于缓存流中任何数据的缓存大小 (以毫秒为单位)。这一事件在服务器端开始处理流之前就发送。这一事件数据的前 4 个字节代表了流 ID 后 4 个字节代表了以毫秒为单位的缓存的长度。 |
StreamIs Recorded (=4) | 服务器端发送这一事件来通知客户端当前流是一个录制流。这一事件数据为 4 字节,代表了录制流的流 ID。 |
PingRequest (=6) | 服务器端发送这一事件用于测试是否能够送达客户端。时间数据是为一个 4 字节的 timestamp,代表了服务器端发送这一命令时的服务器本地时间。客户端在接收到这一消息后会立即发送 PingResponse 回复。 |
PingResponse (=7) | 客户端作为对 ping 请求的回复发送这一事件到服务器端。这一事件数据是为一个 4 字节的 timestamp,就是接收自 PingRequest 那个。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令的名字。设置给 "connect"。 |
Transaction ID | 数字 | 总是设置为 1。 |
Command Object | 对象 | 具有名值对的命令信息对象。 |
Optional User Arguments | 对象 | 任意可选信息。 |
属性 | 类型 | 描述 | 范例 |
app | 字符串 | 客户端连接到的服务器端应用的名字。 | testapp |
flashver | 字符串 | Flash Player 版本号。和ApplicationScript getversion() 方法返回的是同一个字符串。 | FMSc/1.0 |
swfUrl | 字符串 | 进行当前连接的 SWF 文件源地址。例如:app/viewplayer.swf | file://C:/FlvPlayer.swf |
tcUrl | 字符串 | 服务器 URL。具有以下格式:protocol://servername:port/appName/appInstance,例如rtmp:IP:PORT/live/ | rtmp://localhost:1935/testapp/instance1 |
fpad | 布尔 | 如果使用了代理就是 true。 | true 或者 false。 |
audioCodecs | 数字 | 表明客户端所支持的音频编码。 | SUPPORT_SND_MP3 |
videoCodecs | 数字 | 表明支持的视频编码。 | SUPPORT_VID_SORENSON |
videoFunction | 数字 | 表明所支持的特殊视频方法。 | SUPPORT_VID_CLIENT_SEEK |
pageUrl | 字符串 | SWF 文件所加载的网页 URL。可以是undefined | http://somehost/sample.html |
objectEncoding | 数字 | AMF 编码方法。0、3.一般采用3 | AMF3 |
字段名 | 类型 | 描述 |
Procedure Name | 字符串 | 调用的远程方法的名字。 |
Transaction ID | 数字 | 如果期望回复我们要给一个事务 ID。否则我们传 0 值即可。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Optional Arguments | 对象 | 任意要提供的可选参数。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令的名字。 |
Transaction ID | 数字 | 响应所属的命令的 ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Response | 对象 | 调用方法的回复。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名。设置给 "createStream"。 |
Transaction ID | 数字 | 命令的事务 ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | _result 或者 _error;表明回复是一个结果还是错误。 |
Transaction ID | 数字 | 响应所属的命令的 ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Stream ID | 数字 | 返回值要么是一个流 ID 要么是一个错误信息对象。 |
服务器端使用 "onStatus" 命令向客户端发送 NetStream 状态:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名 "onStatus"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | onStatus 消息没有命令对象。 |
Info Object | 对象 | 一个 AMF 对象至少要有以下三个属性。"level" (字符串):这一消息的等级,"warning"、"status"、"error" 中的某个值;"code" (字符串):消息码,例如 "NetStream.Play.Start";"description" (字符串):关于这个消息人类可读描述。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名。设为 "play"。 |
Transaction ID | 数字 | 事务 ID 设为 0。 |
Command Object | Null | 命令信息不存在。设为 null 类型。 |
Stream Name | 字符串 | 要播放流的名字。要播放视频 (FLV) 文件,使用没有文件扩展名的名字对流名进行定义 (例如,"sample")。要重播 MP3 或者 ID3,你必须在流名前加上 mp3:例如,"mp3:sample"。要播放 H.264/AAC 文件,你必须在流名前加上 mp4:并指定文件扩展名。例如,要播放 sample.m4v 文件,定义 "mp4:sample.m4v"。 |
Start | 数字 | 一个可选的参数,以秒为单位定义开始时间。默认值为 -2,表示用户首先尝试播放流名字段中定义的直播流。如果那个名字的直播流没有找到,它将播放同名的录制流。如果没有那个名字的录制流,客户端将等待一个新的那个名字的直播流,并当其有效时进行播放。如果你在 Start 字段中传递 -1,那么就只播放流名中定义的那个名字的直播流。如果你在 Start 字段中传递 0 或一个整数,那么将从 Start 字段定义的时间开始播放流名中定义的那个录制流。如果没有找到录制流,那么将播放播放列表中的下一项。 |
Duration | 数字 | 一个可选的参数,以秒为单位定义了回放的持续时间。默认值为 -1。-1 值意味着一个直播流会一直播放直到它不再可用或者一个录制流一直播放直到结束。如果你传递 0 值,它将只播放单一一帧,因为播放时间已经在录制流的开始的 Start 字段指定了。假定定义在 Start 字段中的值大于或者等于 0。如果你传递一个正数,将播放 Duration 字段定义的一段直播流。之后,变为可播放状态,或者播放 Duration 字段定义的一段录制流。(如果流在 Duration 字段定义的时间段内结束,那么流结束时回放结束)。如果你在 Duration 字段中传递一个 -1 以外的负数的话,它将把你给的值当做 -1 处理。 |
Reset | 布尔 | 一个可选的布尔值或者数字定义了是否对以前的播放列表进行 flush。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为 "play2"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | 命令信息不存在,设置为 null 类型。 |
Parameters | 对象 | 一个 AMF 编码的对象,该对象的属性是为公开的 flash.net.NetStreamPlayOptions ActionScript 对象所描述的属性。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为 "deleteStream"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | 命令信息对象不存在,设为 null 类型。 |
Stream ID | 数字 | 服务器端消亡的流 ID。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为 "receiveAudio"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | 命令信息对象不存在,设置为 null 类型。 |
Bool Flag | 布尔 | true 或者 false 以表明是否接受音频。 |
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为 "receiveVideo"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | 命令信息对象不存在,设置为 null 类型。 |
Bool Flag | 布尔 | true 或者 false 以表明是否接受视频。 |
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为 "publish"。 |
Transaction ID | 数字 | 事务 ID 设置为 0。 |
Command Object | Null | 命令信息对象不存在,设置为 null 类型。 |
Publishing Name | 字符串 | 发布的流的名字。 |
Publishing Type | 字符串 | 发布类型。可以设置为 "live"、"record" 或者 "append"。record:流被发布,数据被录制到一个新的文件。新文件被存储在服务器上包含服务应用目录的子路径。如果文件已存在,将重写。append:流被发布,数据被添加到一个文件。如果该文件没找着,将新建一个。live:直播数据只被发布,并不对其进行录制。 |
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令的名字,设为 "seek"。 |
Transaction ID | 数字 | 事务 ID 设为 0。 |
Command Object | Null | 没有命令信息对象,设置为 null 类型。 |
milliSeconds | 数字 | 播放列表查找的毫秒数。 |
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设为 "pause"。 |
Transaction ID | 数字 | 没有这一命令的事务 ID,设为 0。 |
Command Object | Null | 命令信息对象不存在,设为 null 类型。 |
Pause/Unpause Flag | 布尔 | true 或者 false,来指示暂停或者重新播放。 |
milliSeconds | 数字 | 流暂停或者重新开始所在的毫秒数。这个是客户端暂停的当前流时间。当回放已恢复时,服务器端值发送带有比这个值大的 timestamp 消息。 |