Adobe 官方公布的 RTMP 规范+未公布的部分

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。

       

1. 简介

        Adobe 公司的实时消息传输协议 (RTMP) 通过一个可靠地流传输提供了一个双向多通道消息服务,比如 TCP [RFC0793],意图在通信端之间传递带有时间信息的视频、音频和数据消息流。实现通常对不同类型的消息分配不同的优先级,当运载能力有限时,这会影响等待流传输的消息的次序。
        本文档将对实时流传输协议 (Real Time Messaging Protocol) 的语法和操作进行描述。

       

1.1. 术语

        本文档中出现的关键字,"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY" 、"OPTIONAL",都将在 [RFC2119] 中进行解释。

       

2. 贡献者

        Rajesh Mallipeddi,Adobe Systems 原成员,起草了本文档原始规范,并提供大部分的原始内容。
        Mohit Srivastava,Adobe Systems 成员,促成了本规范的开发。

       

3. 名词解释

        Payload (有效载荷):包含于一个数据包中的数据,例如音频采样或者压缩的视频数据。payload 的格式和解释,超出了本文档的范围。
        Packet (数据包):一个数据包由一个固定头和有效载荷数据构成。一些个底层协议可能会要求对数据包定义封装。
        Port (端口):"传输协议用以区分开指定一台主机的不同目的地的一个抽象。TCP/IP 使用小的正整数对端口进行标识。" OSI 传输层使用的运输选择器 (TSEL) 相当于端口。
        Transport address (传输地址):用以识别传输层端点的网络地址和端口的组合,例如一个 IP 地址和一个 TCP 端口。数据包由一个源传输地址传送到一个目的传输地址。
        Message stream (消息流):通信中消息流通的一个逻辑通道。
        Message stream ID (消息流 ID):每个消息有一个关联的 ID,使用 ID 可以识别出流通中的消息流。
        Chunk (块):消息的一段。消息在网络发送之前被拆分成很多小的部分。块可以确保端到端交付所有消息有序 timestamp,即使有很多不同的流。
        Chunk stream (块流):通信中允许块流向一个特定方向的逻辑通道。块流可以从客户端流向服务器,也可以从服务器流向客户端。
        Chunk stream ID (块流 ID):每个块有一个关联的 ID,使用 ID 可以识别出流通中的块流。
        Multiplexing (合成):将独立的音频/视频数据合成为一个连续的音频/视频流的加工,这样可以同时发送几个视频和音频。
        DeMultiplexing (分解):Multiplexing 的逆向处理,将交叉的音频和视频数据还原成原始音频和视频数据的格式。
        Remote Procedure Call (RPC 远程方法调用):允许客户端或服务器调用对端的一个子程序或者程序的请求。
        Metadata (元数据):关于数据的一个描述。一个电影的 metadata 包括电影标题、持续时间、创建时间等等。
        Application Instance (应用实例):服务器上应用的实例,客户端可以连接这个实例并发送连接请求。
        Action Message Format (AMF 动作消息格式协议):一个用于序列化 ActionScript 对象图的紧凑的二进制格式。AMF 有两个版本:AMF 0 [AMF0] 和 AMF 3 [AMF3]。

       

4. 字节序、对齐和时间格式

        所有整数型属性以网络字节顺序传输,字节 0 代表第一个字节,零位是一个单词或字段最常用的有效位。字节序通常是大端排序。关于传输顺序的更多细节描述参考 IP 协议 [RFC0791]。除非另外注明,本文档中的数值常量都是十进制的 (以 10 为基础)。
        除非另有规定,RTMP 中的所有数据都是字节对准的;例如,一个十六位的属性可能会在一个奇字节偏移上。填充后,填充字节应该有零值。
        RTMP 中的 Timestamps 以一个整数形式给出,表示一个未指明的时间点。典型地,每个流会以一个为 0 的 timestamp 起始,但这不是必须的,只要双端能够就时间点达成一致。注意这意味着任意不同流 (尤其是来自不同主机的) 的同步需要 RTMP 之外的机制。
        因为 timestamp 的长度为 32 位,每隔 49 天 17 小时 2 分钟和 47.296 秒就要重来一次。因为允许流连续传输,有可能要多年,RTMP 应用在处理 timestamp 时应该使用序列码算法 [RFC1982],并且能够处理无限循环。例如,一个应用假定所有相邻的 timestamp 都在 2^31 - 1 毫秒之内,因此 10000 在 4000000000 之后,而 3000000000 在 4000000000 之前。
        timestamp 也可以使用无符整数定义,相对于前面的 timestamp。timestamp 的长度可能会是 24 位或者 32 位。

       

5. RTMP 块流

        本节介绍实时消息传输协议的块流 (RTMP 块流)。 它为上层多媒体流协议提供合并和打包的服务。
        当设计 RTMP 块流使用实时消息传输协议时,它可以处理任何发送消息流的协议。每个消息包含 timestamp 和 payload 类型标识。RTMP块流和RTMP一起适合各种音频-视频应用,从一对一和一对多直播到点播服务,到互动会议应用
        当使用可靠传输协议时,比如 TCP [RFC0793],RTMP 块流能够对于多流提供所有消息可靠的 timestamp 有序端对端传输。RTMP 块流并不提供任何优先权或类似形式的控制,但是可以被上层协议用来提供这种优先级。例如,一个直播视频服务器可能会基于发送时间或者每个消息的确认时间丢弃一个传输缓慢的客户端的视频消息以确保及时获取其音频消息。
        RTMP 块流包括其自身的带内协议控制信息,并且提供机制为上层协议植入用户控制消息。

      

5.1 消息格式

        可以被分割为块以支持组合的消息的格式取决于上层协议。消息格式必须包含以下创建块所需的字段。
        Timestamp:消息的 timestamp。这个字段可以传输四个字节。
        Length:消息的有效负载长度。如果不能省略掉消息头,那它也被包括进这个长度。这个字段占用了块头的三个字节。
        Type Id:一些类型 ID 保留给协议控制消息使用。这些传播信息的消息由 RTMP 块流协议和上层协议共同处理。其他的所有类型 ID 可用于上层协议,它们被 RTMP 块流处理为不透明值。事实上,RTMP 块流中没有任何地方要把这些值当做类型使用;所有消息必须是同一类型,或者应用使用这一字段来区分同步跟踪,而不是类型。这一字段占用了块头的一个字节。
        Message Stream ID:message stream (消息流) ID 可以使任意值。合并到同一个块流的不同的消息流是根据各自的消息流 ID 进行分解。除此之外,对 RTMP 块流而言,这是一个不透明的值。这个字段以小端格式占用了块头的四个字节。

       

5.2 简单握手(simple handshake)

        这里主要讨论的是简单握手协议。simple handshake是rtmp spec 1.0定义的握手方式。而complex handshake是后来变更的方式,Adobe没有公开。在此暂不讨论。

       一个 RTMP 连接以握手开始。RTMP 的握手不同于其他协议;RTMP 握手由三个固定长度的块组成,而不是像其他协议一样的带有报头的可变长度的块。
        客户端 (发起连接请求的终端) 和服务器端各自发送相同的三块。便于演示,当发送自客户端时这些块被指定为 C0、C1 和 C2;当发送自服务器端时这些块分别被指定为 S0、S1 和 S2。

        5.2.1. 握手顺序
        握手以客户端发送 C0 和 C1 块开始
        客户端必须等待接收到 S1 才能发送 C2
        客户端必须等待接收到 S2 才能发送任何其他数据

        服务器端必须等待接收到 C0 才能发送 S0 和 S1,也可以等待接收到 C1 再发送 S0 和 S1。服务器端必须等待接收到 C1 才能发送 S2。服务器端必须等待接收到 C2 才能发送任何其他数据。
        5.2.2. C0 和 S0 的格式
        C0 和 S0 包都是一个单一的八位字节,以一个单独的八位整型域进行处理:
C0 and S0 bits
        以下是 C0/S0 包中的字段:

        版本号 (八位):在 C0 中,这一字段指示出客户端要求的 RTMP 版本号。在 S0 中,这一字段指示出服务器端选择的 RTMP 版本号。本文档中规范的版本号为 3。0、1、2 三个值是由早期其他产品使用的,是废弃值;4 - 31 被保留为 RTMP 协议的未来实现版本使用;32 - 255 不允许使用(以区分开 RTMP 和其他常以一个可打印字符开始的文本协议)。无法识别客户端所请求版本号的服务器应该以版本 3 响应,(收到响应的) 客户端可以选择降低到版本 3,或者放弃握手。

        如上图,是一个c0+c1一起发送的抓包例子(complex handshake的)。其中选中的字节为c0字段,代表版本号。后面的数据是c1字段。整体包长1537.

        5.2.3. C1 和 S1 的格式
        C1 和 S1 数据包的长度都是 1536 字节,包含以下字段:
C1 and S1 bits
        Time (四个字节):这个字段包含一个 timestamp,用于本终端发送的所有后续块的时间起点。这个值可以是 0,或者一些任意值。要同步多个块流,终端可以发送其他块流当前的 timestamp 的值
        Zero (四个字节):这个字段必须都是 0。
        Random data (1528 个字节):这个字段可以包含任意值。终端需要区分出响应来自它发起的握手还是对端发起的握手,这个数据应该发送一些足够随机的数。这个不需要对随机数进行加密保护,也不需要动态值。
        5.2.4. C2 和 S2 的格式
        C2 和 S2 数据包长度都是 1536 字节基本就是 S1 和 C1 的副本 (分别),包含有以下字段:
C2 and S2 bits
        Time (四个字节):这个字段必须包含终端在 S1 (给 C2) 或者 C1 (给 S2) 发的 timestamp。
        Time2 (四个字节):这个字段必须包含终端先前发出数据包 (s1 或者 c1) timestamp。
        Random echo (1528 个字节):这个字段必须包含终端发的 S1 (给 C2) 或者 S2 (给 C1) 的随机数。两端都可以一起使用 time 和 time2 字段再加当前 timestamp 以快速估算带宽和/或者连接延迟,但这不太可能是有多大用处。
        5.2.5. 握手示意图
Pictorial Representation of Handshake
        下面描述了握手示意图中提到的状态:
        Uninitialized (未初始化):协议的版本号在这个阶段被发送。客户端和服务器都是 uninitialized (未初始化) 状态。之后客户端在数据包 C0 中将协议版本号发出。如果服务器支持这个版本,它将在回应中发送 S0 和 S1。如果不支持呢,服务器会才去适当的行为进行响应。在 RTMP 协议中,这个行为就是终止连接。
        Version Sent (版本已发送):在未初始化状态之后,客户端和服务器都进入 Version Sent (版本已发送) 状态。客户端会等待接收数据包 S1 而服务器在等待 C1。一旦拿到期待的包,客户端会发送数据包 C2 而服务器发送数据包 S2。(客户端和服务器各自的)状态随即变为 Ack Sent (确认已发送)。
        Ack Sent (确认已发送):客户端和服务器分别等待 S2 和 C2。
        Handshake Done (握手结束):客户端和服务器可以开始交换消息了。

       

5.3 复杂握手(complex handshake)

        rtmp 1.0规范中,指定了RTMP的握手协议:
  • c0/s0:一个字节,说明是明文还是加密。
  • c1/s1: 1536字节,4字节时间,4字节0x00,1528字节随机数
  • c2/s2: 1536字节,4字节时间1,4字节时间2,1528随机数和s1相同。这个就是srs以及其他开源软件所谓的simple handshake,简单握手,标准握手,FMLE也是使用这个握手协议。
        Flash播放器连接服务器时,若服务器只支持简单握手,则无法播放h264和aac的流,可能是adobe的限制。adobe将简单握手改为了有一系列加密算法的复杂握手(complex handshake)。 下表是总结:

        由上表可知,当服务器和客户端的握手是按照rtmp协议进行,是不支持h264/aac的,有数据,就是没有视频和声音。原因是adobe变更了握手的数据结构,标准rtmp协议的握手的包是随机的1536字节(S1S2C1C2),变更后的是需要进行摘要和加密。rtmp协议定义的为simple handshake,变更后加密握手可称为complex handshake。

        本文详细分析了rtmpd(ccrtmpserver)中的处理逻辑,以及rtmpdump的处理逻辑,从一个全是魔法数字的世界找到他们的数据结构和算法。

        5.3.1. complex handshake C1 S1结构(此处参照了winlin博客相关文章)

        complex handshake将C1S1分为4个部分,它们的顺序(schema)一种可能是:

// c1s1 schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes

        其中,key和digest可能会交换位置,即schema可能是:

// c1s1 schema1
time: 4bytes
version: 4bytes
digest: 764bytes
key: 764bytes

        客户端来决定使用哪种schema,服务器端则需要先尝试按照schema0解析,失败则用schema1解析。如下图所示:


        无论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结构。

如下图所示:


crtmp中这些全是魔法数字。

        5.3.2.complex handshake  C2 S2结构:
c2 s2主要是用来提供对C1 S1的验证,结构如下:

// 1536bytes C2S2结构
random-data: 1504bytes
digest-data: 32bytes
C2S2的结构相对比较简单。如下图所示:


下面介绍C1S1C2S2的生成以及验证算法。

        5.3.3.complex handshake  C1 S1算法
C1S1中都是包含32字节的digest,而且digest将C1S1分成两部分:

// 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

生成C1的算法如下:

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的算法完毕。
        5.3.4.complex handshake  C2 S2
C2S2的生成算法如下:

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)
验证的算法是一样的。

        5.3.5.解析补充
读rtmpd(ccrtmpserver)代码发现,handshake确实变更了。BTW:rtmpd的代码可读性要强很多,很快就能知道它在做什么;不过,它是部分C++部分C的混合体;譬如,使用了BaseRtmpProtocol和InboundRtmpProtocol这种C++的解决方式;以及在解析complex handshake时对1536字节的包直接操作,offset=buf[772]+buf[773]+buf[774]+buf[775],这个就很难看明白在做什么了,其实1536是由4字节的time+4字节的version+764字节的key+764字节的digest,key的offset在后面,digest的offset在前面,若定义两个结构再让它们自己去解析,就很明白在做什么了。

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();

其实到后面看明白了,scheme1和scheme2这两种方式,是包结构的调换。

<strong>complex的包结构如下:</strong>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在后面。
<strong>子结构KeyBlock定义:</strong>
    760bytes: 包含128bytes的key的数据。
    key_offset: 4bytes 最后4字节定义了key的offset(相对于KeyBlock开头而言)
<pre name="code" class="cpp"><strong>子结构DigestBlock定义:</strong>
    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/C2没有key,只有一个digest,是根据C1/S1算出来的:

先用随机数填充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。

        5.3.6.代码实现
/*
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 <srs_core_complex_handshake.hpp>
*/

#include <srs_core.hpp>

class SrsSocket;

/**
* rtmp complex handshake,
* @see also crtmp(crtmpserver) or librtmp,
* @see also: http://blog.csdn.net/win_lin/article
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值