一、简介
1、RTMP—Real Time Messaging Protocol( 实时消息传输协议)
RTMP基于TCP/IP协议,属于应用层协议,最初由Macromedia为通过互联网在Flash播放器与一个服务器之间传输流媒体音频、视频和数据而开发的一个专有协议。Macromedia后被Adobe Systems收购,该协议也已发布了不完整的规范供公众使用。
RTMP协议有许多变种:
RTMP本身,基于TCP,默认使用1935端口的“明文”协议。
RTMPS,通过TLS/SSL连接传输的RTMP。
RTMPE,使用Adobe自有安全机制加密的RTMP。虽然实现上的细节是专有的,但该机制使用行业标准的密码学加密算法。
RTMPT,将RTMP封装在HTTP中,用于穿透防火墙。RTMPT通常使用TCP的80和443端口,从而能够绕过大多数的公司流量过滤。封装的会话中可以携带明文RTMP、RTMPS或RTMPE数据包。
RTMFP, 基于UDP而非TCP的RTMP,用于取代RTMP Chunk Stream。Adobe开发了安全的实时媒体流协议包,可以让最终用户互相之间直接连接和通信(P2P)。
2、RTMP基本操作
RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接,在Connection链接上会传输一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。
RTMP将流拆分为片段,其大小由客户端和服务器动态协商。有时,它保持不变,音频数据的默认片段大小为 64 字节,视频数据和大多数其他数据类型的默认片段大小为 128 字节。不同流的片段可以交错,并通过单个连接进行多路复用。于较长的数据块,协议因此每个片段仅携带一个字节的标头,因此产生的开销非常小。然而,在实践中,单个片段通常不会交错。相反,交错和多路复用是在数据包级别完成的,跨多个不同活动通道的 RTMP 数据包以某种方式交错,以确保每个通道都满足其带宽、延迟和其他服务质量要求。以这种方式交错的数据包被视为不可分割的,并且不会在片段级别交错。RTMP 定义了几个虚拟通道,数据包可以在这些虚拟通道上发送和接收,并且这些通道彼此独立运行。例如,有一个通道用于处理 RPC 请求和响应,一个通道用于视频流数据,一个通道用于音频流数据,一个通道用于带外控制消息(片段大小协商等)等等。
在典型的 RTMP 会话期间,多个通道可能在任何给定时间同时处于活动状态。当对 RTMP 数据进行编码时,会生成数据包头。数据包头指定了要发送数据包的通道 ID、生成数据包的时间戳(如果需要)以及数据包有效负载的大小等。此头后面是数据包的实际有效负载内容,该内容在通过连接发送之前根据当前商定的片段大小进行分段。数据包头本身永远不会分段,其大小不计入数据包第一个片段中的数据。换句话说,只有实际的数据包有效负载(媒体数据)才会进行分段。
3、加密
RTMP 会话可以使用以下两种方法之一进行加密:
使用行业标准TLS/SSL机制。底层 RTMP 会话只是包装在普通 TLS/SSL 会话中。
使用 RTMPE,它将 RTMP 会话包装在更轻量级的加密层中。
4、RTMPT
RMPT是RTMP用http包装后的协议,可穿越防火墙。来自客户端的消息被发送到服务器上的端口 80(HTTP 的默认端口)。尽管由于 HTTP 标头的原因,RTMPT 中的消息比等效的非隧道 RTMP 消息更大,但 RTMPT 可以在无法使用非隧道 RTMP 的场景中促进 RTMP 的使用,例如当客户端位于阻止非 HTTP 和非 HTTPS 出站流量的 防火墙后面时。
该协议通过 POST URL 发送命令,通过 POST 主体发送 AMF 消息: 打开链接
POST /open/1 HTTP/1.1
二、数据包结构

数据包通过 TCP 连接发送,该连接首先在客户端和服务器之间建立。它们包含一个报头和一个正文,在连接和控制命令的情况下,使用动作消息格式(AMF) 进行编码。报头分为基本报头和块消息报头。基本报头是数据包中唯一不变的部分,通常由单个复合字节组成,其中两个最高有效位是块类型(规范中为fmt),其余位组成流 ID。根据前者的值,可以省略消息报头的某些字段,它们的值来自前面的数据包,而根据后者的值,基本报头可以用一个或两个额外的字节进行扩展(如图所示,它总共有三个字节 (c))。如果基本头(BH)的其余六位(最低有效位) 的值为 0,则 BH 为两个字节,表示从流 ID 64 到 319 (64+255);如果值为 1,则 BH 为三个字节(最后两个字节编码为 16 位 Little Endian),表示从流 ID 64 到 65599 (64+65535);如果值为 2,则 BH 为一个字节,为低级协议控制消息和命令保留。块消息头包含元数据信息,例如消息大小(以字节为单位)、时间戳增量和消息类型。最后一个值是一个字节,定义数据包是音频、视频、命令还是“低级”RTMP 数据包(例如 RTMP Ping)。

当 Flash 客户端执行以下代码时捕获的示例:
var流:NetStream = new NetStream ( connectionObject );
这将生成以下块:
| Hex Code | ASCII |
|---|---|
| 03 00 0B 68 00 00 19 14 00 00 00 00 02 00 0C 63 72 65 61 74 65 53 74 72 65 61 6D 00 40 00 00 00 00 00 00 00 05 | ␃ ␀ @ I ␀ ␀ ␙ ␔ ␀ ␀ ␀ ␀ ␂ ␀ ␌ c r e a t e S t r e a m ␀ @ ␀ ␀ ␀ ␀ ␀ ␀ ␀ ␅ |
数据包以单字节 (0x03) 的基本标头开始,其中两个最高有效位 (b 00 000011) 定义块标头类型为 0,而其余位 (b00 000011 ) 定义块流 ID 为 3。标头类型的四个可能值及其重要性如下:
b00 = 12 字节标头(完整标头)。
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) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Chunk Message Header - Type 0
b01 = 8 个字节 – 类似于 b00 类型,但不包括消息 ID(最后 4 个字节)。
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| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Chunk Message Header - Type 1
b10 = 4 个字节 – 包括基本头和时间戳(3 个字节)。
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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Chunk Message Header - Type 2
b11 = 1 字节 – 仅包含基本头。表示这个chunk的Message Header和上一个是完全相同的,无需再次传送
最后一种类型 (b11) 总是用于聚合消息的情况,在上面的例子中,第二条消息将以 0xC3 (b11000011) 的 ID 开头,这意味着所有消息头字段都应从流 ID 为 3 的消息(即它上面的消息)派生而来。构成流 ID 的六个最低有效位可以取 3 到 63 之间的值。有些值有特殊含义,例如 1 代表扩展 ID 格式,在这种情况下,后面将有两个字节。值 2 适用于低级消息,例如 Ping 和设置客户端带宽。
RTMP 标头的下一个字节(包括上面示例数据包中的值)解码如下:
字节#1(0x03)=块头类型。
字节#2-4(0x000b68)= 时间戳增量。
字节#5-7(0x000019)= 数据包长度 – 在本例中为 0x000019 = 25 字节。
字节#8(0x14)= 消息类型 ID – 0x14(20)定义 AMF0 编码的命令消息。
字节 #9-12 (0x00000000) = 消息流 ID。这是按小端顺序排列的。
消息类型 ID 字节定义数据包是否包含音频数据、视频数据、远程对象或命令。一些可能的值包括:
0x01 = 设置数据包大小消息。
0x02 = 中止。
0x03 = 确认。
0x04 = 控制消息。
0x05 = 服务器带宽
0x06 = 客户端带宽。
0x07 = 虚拟控制。
0x08 = 音频包。
0x09 = 视频包。
0x0F = 数据扩展。
0x10 = 容器扩展。
0x11 = 命令扩展(AMF3 类型命令)。
0x12 = 数据(调用(onMetaData 信息以此方式发送))。
0x13 = 容器。
0x14 = 命令(AMF0 类型命令)。
0x15 = UDP
0x16 = 聚合
0x17 = 存在
在标头之后,0x02 表示大小为 0x000C 且值为 0x63 0x72 … 0x6D 的字符串(“createStream”命令)。之后是 0x00(数字),它是值为 2.0 的事务 ID。最后一个字节是 0x05(空),表示没有参数。
调用消息结构(0x14、0x11)
上面显示的某些消息类型(例如 Ping 和设置客户端/服务器带宽)被视为不使用 AMF 编码格式的低级 RTMP 协议消息。另一方面,命令消息(无论是 AMF0(消息类型为 0x14)还是 AMF3(0x11))都使用该格式,并具有如下所示的一般形式:
(字符串)<命令名称>
(数字)<交易 ID>
(混合)<参数> 例如 Null、String、Object:{key1:value1, key2:value2 ... }
事务 ID 用于可以有回复的命令。值可以是字符串(如上例所示),也可以是一个或多个对象,每个对象由一组键/值对组成,其中键始终编码为字符串,而值可以是任何 AMF 数据类型,包括数组等复杂类型。
控制消息结构 (0x04)
控制消息未经过 AMF 编码。它们以流 ID 0x02 开头,这意味着完整的(类型 0)标头,并且消息类型为 0x04。标头后跟六个字节,解释如下:
#0-1——控制类型。
#2-3 – 第二个参数(这在特定的控制类型中有意义)
#4-5 – 第三个参数(相同)
消息主体的前两个字节定义了 Ping 类型,该类型显然[ 11 ]可以采用六种可能的值。
类型 0 – 清除流:在连接建立且不携带其他数据时发送
类型 1-清除缓冲区。
类型 2——流干。
类型 3 – 客户端的缓冲时间。第三个参数以毫秒为单位保存该值。
类型 4-重置流。
类型 6 – 从服务器 ping 客户端。第二个参数是当前时间。
类型 7 – 客户端的 Pong 回复。第二个参数是客户端收到 Ping 的时间。
类型 8-UDP 请求。
类型 9-UDP 响应。
类型 10——带宽限制。
类型 11-带宽。
类型 12——节流带宽。
类型 13-流已创建。
类型 14-流已删除。
类型 15-设置读取访问权限。
类型 16-设置写访问权限。
类型 17——流元请求。
类型 18-流元响应。
类型 19——获取段边界。
类型 20-设置段边界。
类型 21 – 断开连接。
类型 22——设置关键链接。
类型 23-断开连接。
类型 24-哈希更新。
类型 25-哈希超时。
类型 26-哈希请求。
类型 27-哈希响应。
类型 28——检查带宽。
类型 29-设置音频样本访问。
类型 30-设置视频样本访问。
类型 31-油门开始。
类型 32——油门端。
类型 33 – DRM 通知。
类型 34-RTMFP 同步。
类型 35-查询 IHello。
类型 36-转发我你好。
类型 37-重定向 IHello。
类型 38-通知 EOF。
类型 39 – 代理继续。
类型 40-代理删除上游。
类型 41——RTMFP 设置 Keepalive。
类型 46-未找到段。
Pong是 Ping 回复的名称,所用的值如上所示。
ServerBw/ClientBw 消息结构(0x05、0x06)
这涉及与客户端上行和服务器下行比特率有关的消息。主体由四个字节组成,显示带宽值,可能还有一个字节的扩展,用于设置限制类型。这可以有以下三个可能值之一:硬、软或动态(软或硬)。
设置区块大小(0x01)
正文的四个字节中收到的值。默认值为 128 字节,仅当需要更改时才发送该消息。
三、协议
1、握手流程
建立 TCP 连接后,首先建立 RTMP 连接,通过交换来自每一方的三个数据包(在官方文档中也称为 Chunk)来执行握手。这些数据包分别称为客户端发送的数据包的 C0-2 和服务器端的 S0-2,不要将其与只能在握手完成后交换的 RTMP 数据包混淆。这些数据包具有自己的结构,C1 包含一个设置“epoch”时间戳的字段,但由于可以将其设置为零(如在第三方实现中所做的那样),因此可以简化数据包。客户端通过发送 C0 数据包来初始化连接,该数据包具有表示当前协议版本的常量值 0x03。它直接跟随着 C1,而无需等待首先收到包含 1536 个字节的 S0,其中前四个表示纪元时间戳,后四个全部为 0,其余为随机(在第三方实现中可以将其设置为 0)。 C2 和 S2 分别是 S1 和 C1 的回显,但第二个四个字节是接收相应消息的时间(而不是 0)。收到 C2 和 S2 后,握手即视为完成。

简单握手:
C1和S1,4个字节表示时间,4个字节0,从第9个字节开始为随机数。
S2是C1的复制,C2是S1的复制。
C0和S0都有8个字节,代表协议版本号,C0(客户端版本)、S0(服务器版本),目前版本为3。
复杂握手:
复杂握手将简单握手中1528比特的随机数部分平分为两部分。
764比特的public key(公共密匙)
764比特的digest(32字节的密文)
复杂握手的Version部分不为0,服务器可以由此判断握手类型。
+-------------+ +-------------+ | Client | TCP/IP Network | Server | +-------------+ | +-------------+ | | | Uninitialized | Uninitialized | C0 | | |------------------->| C0 | | |-------------------->| | C1 | | |------------------->| S0 | | |<--------------------| | | S1 | Version sent |<--------------------| | S0 | | |<-------------------| | | S1 | | |<-------------------| Version sent | | C1 | | |-------------------->| | C2 | | |------------------->| S2 | | |<--------------------| Ack sent | Ack Sent | S2 | | |<-------------------| | | | C2 | | |-------------------->| Handshake Done | Handshake Done | | | Pictorial Representation of Handshake
握手开始于客户端发送、
C0块。服务器收到
C1或
C0后发送
C1和
S0。
S1
当客户端收齐和
S0后,开始发送
S1。当服务器收齐
C2和
C0后,开始发送
C1。
S2
当客户端和服务器分别收到和
S2后,握手完成。
C2
RTMP握手的这个过程就是完成了两件事:
校验客户端和服务器端RTMP协议版本号
是发了一堆随机数据,校验网络状况。
注意:
在实际工程应用中,一般是客户端先将,
C0块同时发出,服务器在收到
C1 之后同时将
C1,
S0,
S1发给客户端。
S2的内容就是收到的
S2块的内容。之后客户端收到
C1块,并原样返回给服务器,简单握手完成。按照RTMP协议个要求,客户端需要校验
S1块的内容和
C1块的内容是否相同,相同的话才彻底完成握手过程,实际编写程序用一般都不去做校验
S2
C0和S0握手包
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ | version | +-+-+-+-+-+-+-+-+ C0 and S0 bits
C1和S1握手包:字节时间戳,
4字节的
4,
0字节的随机数
1528
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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time (4 bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | zero (4 bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | random bytes | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | random bytes | | (cont) | | .... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ C1 and S1 bits
C2和S2握手包:字节时间戳,
4字节从对端读到的时间戳,
4字节随机数
1528
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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time (4 bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time2 (4 bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | random echo | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | random echo | | (cont) | | .... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ C2 and S2 bits
2、信息交换
解析RTMP协议的Chunk信息:
四、librtmp
官方 下载地址:https://rtmpdump.mplayerhq.hu/download/
ffmpeg下载地址:https://git.ffmpeg.org/rtmpdump.git
五、FFMPEG推拉流调研
在编码过程当中,主要使用了如下的函数
| 函数名 | 作用 |
|---|---|
| av_log_set_level | 配置输出日志级别 (AV_LOG_TRACE最详细) |
| avformat_network_init | 初始化网络模块 |
| avformat_open_input | 打开输入文件,并且将文件信息赋值给AVFormatContext保存 |
| avformat_find_stream_info | 根据AVFormatContext查找流信息 |
| av_dump_format | 将AVFormatContext中的媒体文件的信息进行格式化输出 |
| avformat_alloc_output_context2 | 根据format_name(或filename或oformat)创建输出文件的AVFormatContext信息 |
| avformat_new_stream | 根据AVFormatContext和AVCodecContext创建新的流 |
| avcodec_parameters_copy | 拷贝AVCodecParameters |
| avio_open | 根据url进行AVIOContext的创建与初始化(这个url在推流时就是服务器地址) |
| avformat_write_header | 为流分配priv_data并且将流的头信息写入到输出媒体文件 |
| av_read_frame | 根据AVFormatContext所提供的的信息读取一帧,存入AVPacket |
| av_interleaved_write_frame | 以交错的方式将帧送入到媒体文件中 |
| av_packet_unref | 释放AVPacket |
| av_write_trailer | 将流的尾部写入到输出的媒体文件中,并且释放文件中的priv_data |
| avformat_close_input | 释放AVFormatContext |
从使用的函数来看,主要的操作流程和数据流走向大约为:
初始化网络模块,为RTMP传输进行准备(avformat_network_init)
打开输入文件,创建输入文件结构体并且读取输入文件信息(avformat_open_input),此时也会创建输入的流信息结构体
根据输入文件查找流信息,赋值给流信息结构体(avformat_find_stream_info)
打印输入文件信息(av_dump_format)
根据输出文件信息来创建输出文件结构体(avformat_alloc_output_context2)
创建输出流(avformat_new_stream)
将输入流的参数拷贝给输出给输出流(avcodec_parameters_copy)
打印输出文件信息(av_dump_format)
打开输出口,准备推流(avio_open)
写入流的头部信息(avformat_write_header)
读取一帧信息,存储到AVPacket中(av_read_frame)
处理时间戳;PTS是播放时间戳,告诉播放器播放这一帧的时间;DTS是解码时间戳,告诉播放器解码这一帧的时间;PTS通常是按照递增顺序排列的。延时很重要,如果不对前后帧推流的时间进行控制,帧会瞬时推送到服务器端,会出现服务器无法正常接收帧的情况
将帧推流(av_interleaved_write_frame)
写入流的尾部信息(av_write_trailer)
释放结构体信息(av_packet_unref、av_write_trailer和avformat_close_input)
六、推拉流demo调研
1、下载本地RTMP服务器
https://github.com/bluenviron/mediamtx/releases
下载完毕后解压,并运行RTSP服务器

2、使用FFMPEG推流
端口:
[RTSP] listener opened on :8554 (TCP), :8000 (UDP/RTP), :8001 (UDP/RTCP)
[RTMP] listener opened on :1935
ffmpeg -re -stream_loop -1 -i 01.flv -c -copy -f rtsp rtsp://127.0.0.1:8554/video

3、使用FFMPEG拉流
ffplay rtsp rtsp://127.0.0.1:8554/video

4、推流本地摄像头
(1)、获取本地摄像头名称
//获取本地摄像头名称 ffmpeg -list_devices true -f dshow -i dummy

(2)、指定摄像头 已经编码方式进行推流
ffmpeg -f dshow -i video="Integrated Camera" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/camera_test ffmpeg -f dshow -i video="Integrated Camera" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -rtsp_transport tcp -f flv rtmp://127.0.0.1:1935/live

(3)、拉取摄像头流
ffplay rtsp://127.0.0.1:8554/camera_test

5、不带编码的FFMPEG代码推流
提前转码好的flv文件(H264编码视频)
int pushStream()
{
AVOutputFormat* ofmt = NULL;
AVFormatContext* ifmt_ctx = NULL, * ofmt_ctx = NULL;
AVPacket pkt;
const char* in_filename, * out_filename;
int ret, i;
int videoindex = -1;
int frame_index = 0;
int64_t start_time = 0;
in_filename = "e:/01.flv";//输入URL(Input file URL)
out_filename = "rtmp://127.0.0.1:1935/video";//输出 URL(Output URL)[RTMP]
//Network
avformat_network_init();
//输入(Input)
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf("Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf("Failed to retrieve input stream information");
goto end;
}
for (i = 0; i < ifmt_ctx->nb_streams; i++)
if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0);
//输出(Output)
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);//UDP
if (!ofmt_ctx) {
printf("Could not create output context
");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = (AVOutputFormat*)ofmt_ctx->oformat;
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//根据输入流创建输出流(Create output AVStream according to input AVStream)
AVStream* in_stream = ifmt_ctx->streams[i];
AVStream* out_stream = avformat_new_stream(ofmt_ctx, nullptr);
if (!out_stream) {
printf("Failed allocating output stream
");
ret = AVERROR_UNKNOWN;
goto end;
}
//复制AVCodecContext的设置(Copy the settings of AVCodecContext)
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) {
printf("Failed to copy context from input to output stream codec context
");
goto end;
}
out_stream->codecpar->codec_tag = 0;
}
//Dump Format------------------
av_dump_format(ofmt_ctx, 0, out_filename, 1);
//打开输出URL(Open output URL)
if (!(ofmt->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
printf("Could not open output URL '%s'", out_filename);
goto end;
}
}
//写文件头(Write file header)
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
printf("Error occurred when opening output URL
");
goto end;
}
start_time = av_gettime();
while (1) {
AVStream* in_stream, * out_stream;
//获取一个AVPacket(Get an AVPacket)
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if (pkt.pts == AV_NOPTS_VALUE) {
//Write PTS
AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts = (double)(frame_index * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
pkt.dts = pkt.pts;
pkt.duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
}
//Important:Delay
if (pkt.stream_index == videoindex) {
AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
AVRational time_base_q = { 1,AV_TIME_BASE };
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/* copy packet */
//转换PTS/DTS(Convert PTS/DTS)
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
//Print to Screen
if (pkt.stream_index == videoindex) {
printf("Send %8d video frames to output URL
", frame_index);
frame_index++;
}
//ret = av_write_frame(ofmt_ctx, &pkt);
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
printf("Error muxing packet
");
break;
}
av_packet_unref(&pkt);
}
//写文件尾(Write file trailer)
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf("Error occurred.
");
return -1;
}
return 0;
}
运行结果:

运行ffplay拉流:

七、RTMP推拉流服务器加密实现
这种加密抓包还是可以抓到
1、Token校验
用户向服务器请求token
服务器生成一个唯一的token,保存在服务器端,并将token返回给用户
用户在推送或拉取流时,在请求中包含这个token
服务器检查token是否有效。如果有效,允许请求;如果无效,拒绝请求
rtmp://example.com/live/stream?token=YOUR_TOKEN
2、签名及时间戳校验
客户端准备请求参数,包括URL和当前时间戳。
客户端使用服务器的公钥对参数进行加密,生成签名。
客户端在推送或拉取流时,将签名和时间戳包含在请求中。
服务器收到请求,使用私钥对签名进行解密,并检查时间戳是否过期。如果签名匹配且时间戳未过期,允许请求;否则,拒绝请求。
rtmp://example.com/live/stream?timestamp=YOUR_TIMESTAMP&sign=YOUR_SIGN
3、RTMPS协议
RTMPS是RTMP的安全变体,它采用安全套接字层 (SSL) 和传输层安全性 (TLS) 两种加密协议,使数据传输更加安全。
RTMPS协议使用安全套接字层 (SSL) 或传输层安全性 (TLS) 证书,提供了一种保护通过互联网交换的数据的方法。