1. 协议简介

1.1 RTMP

RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议族,包括RTMP基本协议RTMPT/RTMPS/RTMPE等多种变种。
RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。
支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。

  • 应用层协议,依靠TCP保证可靠传输。

  • 默认端口:1953,可能被防火墙屏蔽。

  • 在流媒体/交互服务器之间进行音视频和数据通信

1.2 HLS

HLS(HTTP Live Streaming)是一个由苹果公司提出的基于HTTP的流媒体网络传输协议。
所以在 Apple 的全系列产品包括 iPhone、 iPad、safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在 Android 也加入了对 HLS 的支持。

工作原理 : 把整个流分成一个个小的基于HTTP的TS文件来下载,每次只下载一些。
当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。

HLS请求基本的HTTP报文,与实时传输协议(RTP)不同
HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。

HLS缺陷:

  • 实时性差,延迟高。延迟基本在 10s+ 以上
  • ts 切片较小,会造成海量小文件,对存储和缓存有影响

HLS规范:

  • 视频的封装格式是TS

  • 音视频采用H264编码和AAC编码。

  • 除了TS视频文件本身,还定义了用来控制播放的m3u8索引文件

1.3 对比RTMP,HLS, HTTP-FLV

在开始之前,我们先要明确一个问题,根据应用场景不同,流媒体协议分为:

  • 推流协议

  • 拉流播放协议

RTMP >> 双端
HLS >> 拉流端
HTTP-FLV >> 拉流端。

RTMP HTTP-FLV HLS
传输协议 TCP HTTP HTTP
视频封装格式 flv flv ts
延时 1-3秒 1-3秒 5-20秒
Web支持 H5需要使用插件 H5需要使用插件 支持H5
数据 连续流 连续流 切片文件

ps: H5 是 HTML5

1. RTMP & HTTP-FLV

  • 这两个协议实际上传输的数据是一样的,数据都是flv文件的tag

  • RTMP:实时播放服务器的 FLV 文件或服务器转发的FLV数据,本地无 FLV 缓存文件FLV保密性好

  • HTTP-FLV:将 FLV 下载到本地再播放,FLV保密性不好

2. HLS & RTMP

  • RTMP:采用1935端口,而非HTTP80端口,在某些网络环境下可能被屏蔽

  • RTMP:是一种有状态协议,需要为每一个播放视频流的客户端维护状态,服务器平滑扩展难度大

  • HLS:基于无状态HTTP协议,客户端只需要按顺序使用下载的TS文件就可,负载均衡如同普通的HTTP文件服务器负载均衡一样。

3. HTTP-FLV

HTTP-FLV 结合了 RTMP 和 HLS 的优点,易用(HTTP协议)低延时(flv)

4. 为什么 RTPM 比 HLS 快

  • HLS拉流:服务器音视频数据切片生成 TS 文件

  • HLS拉流:客户端必须等待服务端至少生成一个 TS 文件
    通常下载完两个媒体文件后才能保证不同分段音视频间的无缝连接。

  • HLS一直在等切片数据,RTMP不需要切片


2. 封装简介

2.1 FLV

1. FLV的封装格式

FLV(Flash Video),Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。除了播放视频,在直播时也可以使用。采用FLV格式封装的文件后缀为.flv,格式如下(FLV = FLV Header + Body

2. FLV Header

Header 部分记录了FLV的类型、版本、流信息、Header 长度等。
一般整个Header占用9个字节,大于9个字节则表示头部信息在这基础之上还存在扩展数据。
FLV Header 的信息排布如下所示:

3. FLV Body

Body 是由一个个Tag组成的,每个Tag下面有一块4个字节的空间,用来记录这个Tag 的长度。
这个后置的PreviousTagSize用于逆向读取处理,表示的是前面的Tag的大小。
FLV Body 的信息排布如下:

4. FLV Tag

每个Tag 也是由两部分组成的:Tag HeaderTag Data
Tag Header 存放了当前Tag的类型,数据长度、时间戳、时间戳扩展、StreamsID等信息,然后再接着数据区Tag Data。
Tag的排布如下:

5. Tag Data

Tag Data分成 AudioVideoScript 三种。

5.1 Audio Tag Data

音频的Tag Data又分为 AudioTagHeaderData 数据区,其排布结构如下图所示:

5.2 Video Tag Data

  • Video Tag 由一个字节的VideoTagHeaderVideo数据区部分组成

  • Video数据区部分格式不确定。对于AVC (H.264)编码部分,Video数据区排布如下:

5.3 Script Tag Data

2.2 TS

TS(Transport Stream,传输流),一种常见的视频封装格式,是基于MPEG-2的封装格式(所以也叫MPEG-TS),后缀为.ts

1. TS的分层结构

TS文件分为三层,如下所示(可以倒序看更好理解):

  • TS层(Transport Stream)
    在PES层基础上加入了数据流识别信息和传输信息。

  • PES层(Packet Elemental Stream)
    在ES层基础上加入时间戳(PTS/DTS)等信息。

  • ES层(Elementary Stream)
    压缩编码后的音视频数据。

2. TS层

ts包大小固定为188字节 ,ts层分为三个部分:ts headeradaptation fieldpayload

  • ts header固定4个字节;

  • adaptation field可能存在也可能不存在,主要作用是给不足188字节的数据做填充

  • payloadPES 数据,或者PATPMT等。

◆ ts Header + adaptation field 格式如下:

2.1 TS Header

  • TS Header格式如下:

  • pid 决定了负载内容的类型,主要包括:PAT表,PMT表,视频流,音频流。常用的PID值:
PAT CAT TSDT EIT,ST RST,ST TDT,TOT,ST
PID 0x0000 0x0001 0x0002 0x0012 0x0013 0x0014

2.2 调整字段

2.3 PAT表结构(指明PMT表的PID值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct TS_PAT{
unsigned table_id :8 //固定位0x00,表示该表是PAT
unsigned section_syntax_indicator :1 //段语法标志,固定为1
unsigned zero :1 //固定为0
unsigned reserved_1 :2 //第一个保留位
unsigned section_length :12 //表示这个字节之后有用的字节数,包括CRC_32
unsigned transport_stream_id :16 //传输流的ID,区别于一个网络中其他多路复用的流
unsigned reserved_2 :2 //第二个保留位
unsigned version_number :5 //表示PAT的版本号
unsigned current_next_indicator :1 //表示发送的PAT是当前有效还是下一个有效,为1时代表当前有效
unsigned section_number :8 //如果PAT分段传输,那么此值每次递增1
unsigned last_section_number :8 //最后一个分段的号码
for(int i=0;i<N;i++){
unsigned Program_number :16 //节目号
unsigned Reversed_3 :3 //保留位
if(Program_number == 0)
Network_id :13 //网络信息表(NIT)的PID
else
Program_MAP_PID :13 //节目映射表的PID,每个节目都有一个
}
unsigned CRC_32 :32 //CRC32校检码
}TS_PAT

2.4 PMT表(指明音视频流的PID值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct TS_program_map_section{
unsigned Table_id :8 //标志PSI分段的内容,对于PMT,此值为0x02
unsigned Section_syntax_indicator :1 //置为1
unsigned '0' :1
unsigned Reserved :2 //保留位
unsigned Section_length :12 //指明了自此到最后CRC_32的字节数
unsigned Program_number :16 //指出该节目的节目号,与PAT表对应
unsigned Reserved :2 //保留位
unsigned Version_number :5 //取值0-31,代表当前PMT的版本号
unsigned Current_next_indicator :1 //代表当前PMT是否有效
unsigned Section_number :8 //给出了当前所处段的数目
unsigned Last_section_number :8 //给出了最后一个分段,即分段的最大数目
unsigned Reserved :3 //保留位
unsigned PCR_PID :13 //指示TS包的PCR值,该TS包含有PCR字段
unsigned Reserved :4 //保留位
unsigned Program_info_length :12 //该字段描述跟随其后对节目信息描述的字节数
for(int i = 0; i < N; i++)
Descriptr()
for(int i = 0; i < N; i++){
unsigned Stream_type :8 //0x00:保留, 0x01:MPEG1视频,0x02:MPEG2视频,0x03:MPEG1音频,0x04:MPEG2音频,0x05:私有字段,0x06:含有私有数据的PES包 ......
unsigned Reserved :3 //保留
unsigned Elementary_PID :13 //指示TS包的PID,这些TS包含有相同的PID
unsigned Reserved :4 //保留
unsigned ES_info_length :12 //指示跟随其后描述相关节目元素的字节数
for(int j = 0; j < N2; j++)
Descriptr()
}
unsigned CRC_32 :32 //循环校检位
}

2.5 补充

  • 打包ts流时PATPMT表是没有调整字段的,不够的长度直接补0xff即可。

  • 视频流音频流都需要加adaptation field通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。

3. PES 层

  • PES(Packetized Elementary Stream,打包的ES),在 ES 层的基础上加入了时间戳(PTS/TDS)等信息。

  • ES数据包比较大,加入PES头时需将ES进行分割,只在第一个分割的ES上加PES头,如下图所示

  • PES packet length — 指示PES 包中跟随该字段最后字节的字节数。0 值指示PES 包长度既未指示也未限定并且仅在这样的PES 包中才被允许,该PES 包的有效载荷由来自传输流包中所包含的视频基本流的字节组成。

  • PES结构如下:

  • PES 关键字段说明

4. ES 层

  • ES(Elementary Stream,基本码流),就是音视频编码数据流,比如视频H.264,音频AAC

  • 一个 ES 流中只包含一种类型的数据(视频,或音频,或字幕)。


3. HLS_Server(demo)

3.1 ffmpeg 命令生成m3u8切片

第一种方式

1
ffmpeg -i input.mp4 -c:v libx264 -c:a copy -f hls -hls_time 10 -hls_list_size 0 -hls_start_number 0 input/index.m3u8

注解 :

  • -c:v codec(编解码器): video(视频)。 == -vcodec
  • -c:a audio(音频) == -acodec
  • -f (format) 以hls格式
  • -hls_time n: 设置每片的长度,默认值为2,单位为秒
  • -hls_list_size n:设置播放列表保存的最多条目,设置为0会保存有所片信息,默认值为5
  • -hls_start_number n:设置播放列表中sequence number的值为number,默认值为0
  • -hls_wrap n:设置多少片之后开始覆盖,如果设置为0则不会覆盖,默认值为0
    这个选项能够避免在磁盘上存储过多的片,而且能够限制写入磁盘的最多的片的数量
  • input 文件需要自己新建

第二种方式

1
2
ffmpeg -i input.mp4 -fflags flush_packets -max_delay 2 -flags -global_header 
-hls_time 5 -hls_list_size 0 -vcodec libx264 -acodec aac -r 30 -g 60 index.m3u8

注解:

  • -fflags 设置输入/输出文件或流的标志(flags)
    用来激活或修改 FFmpeg 内部的标志,以控制特定的行为
  • -flush_packets 导致 FFmpeg 立即输出已经在内存缓冲中的数据包(packets)
    而不是等待缓冲区满或其他条件触发输出。
  • -max_delay 2:设置最大延迟时间为 2 秒
  • -flags -global_header:这是一个设置视频编码器标志的选项。在这个情况下,-flags 用于设置特定的编码器标志。-global_header 标志指示在视频流的第一个关键帧(I帧)中包含全局头信息(global headers),这对于某些视频流的处理和解码非常重要。

  • -vcodec libx264:指定视频编码器为 libx264,用于对视频进行 H.264 编码。

  • -acodec aac:指定音频编码器为 AAC,用于对音频进行 AAC 编码。

3.2 源代码

Log.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#pragma warning(disable:4996)
#include <chrono>
#include <string>

static std::string GetCur_FormatTime(const char* format = "%Y-%m-%d %H:%M:%S")
{
time_t t = time(nullptr);
char str[64];
strftime(str, sizeof(str), format, localtime(&t));
std::string time;
return time;
}


#define LOGI(format, ...) \
fprintf(stderr, "[INFO]%s, [%s:%d %s()]" format "\n", GetCur_FormatTime().data(), __FILE__, __LINE__,__func__, ##__VA_ARGS__)
#define LOGE(format, ...) \
fprintf(stderr, "[INFO]%s, [%s:%d %s()]" format "\n", GetCur_FormatTime().data(), __FILE__, __LINE__, __func__, ##__VA_ARGS__)

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <WinSock2.h>
#include "Log.h"
#include "connection.h"
#pragma comment(lib, "ws2_32.lib")

int main()
{
//ffplay -i http://127.0.0.1:8080/index.m3u8
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
{
LOGE("WSAStartup error");
return -1;
}

int port = 8080;
SOCKET serverFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKADDR_IN server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);

if (bind(serverFd, (SOCKADDR*)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
LOGE("socket bind error");
return -1;
}
if (listen(serverFd, SOMAXCONN) < 0)
{
LOGE("socket bind error");
return -1;
}

while (true)
{
LOGI("wait new connect");
int len = sizeof(SOCKADDR);
SOCKADDR_IN accept_addr;
int clientFd = accept(serverFd, (SOCKADDR*)&accept_addr, &len);
if (clientFd == SOCKET_ERROR)
{
LOGE("accept connection error");
break;
}
LOGI("new connect clientFd = %d", clientFd);
Connection conn(clientFd);
conn.start();
}
closesocket(serverFd);
return 0;
}

connection.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once
#include "Log.h"

class Connection
{
public:
Connection(int clientFd);
~Connection();
int start();

private:
int mclientFd;
};

connection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "connection.h"
#include <WinSock2.h>
#include <iostream>


Connection::Connection(int clientFd)
:mclientFd(clientFd)
{}

Connection::~Connection()
{
closesocket(mclientFd);
}


char buf[1500000];
int Connection::start()
{
char bufRecv[2000] = { 0 };
int bufRecvSize = recv(mclientFd, bufRecv, sizeof(bufRecv), 0);

char uri[100] = { 0 }; //index0.ts, index1.ts
const char* sep = "\n";
char* line = strtok(bufRecv, sep); //before sep
while (line)
{
if (strstr(line, "GET"))
{
if (sscanf(line, "GET %s HTTP/1.1\r\n", &uri) != 1)
LOGE("parse uri error");
}
line = strtok(nullptr, sep); //after sep
}
printf("uri = %s\n", uri);

std::string filename = "D:/ffmpeg/learn/m3u8/test" + std::string(uri);
FILE* fp = fopen(filename.data(), "rb");
if (!fp)
{
LOGE("fopen error");
return -1;
}

int buflen = fread(buf, 1, sizeof(buf), fp);
LOGI("buflen = %d", buflen);
if (fp) fclose(fp);

char http_headers[2000];
if (strcmp("/index.m3u8", uri) == 0)
{
sprintf(http_headers, "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: * \r\n"
"Connection: keep-alive\r\n"
"Content-Length: %d\r\n"
"Content-Type: application/vnd.apple.mpegurl; charset=utf-8\r\n"
"Keep-Alive: timeout=30, max=100\r\n"
"Server: hlsServer\r\n"
"\r\n",
buflen);
}
else
{
sprintf(http_headers, "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: * \r\n"
"Connection: close\r\n"
"Content-Length: %d\r\n"
"Content-Type: video/mp2t; charset=utf-8\r\n"
"Keep-Alive: timeout=30, max=100\r\n"
"Server: hlsServer\r\n"
"\r\n",
buflen);
}

send(mclientFd, http_headers, strlen(http_headers), 0);
send(mclientFd, buf, buflen, 0);
Sleep(10);
return 0;
}

抓包分析


4. Http-Flv Server(demo)

4.1 ffmpeg命令 mp4转flv

1
ffmpeg.exe -i source.mp4 -c:v libx264 -crf 24 destination.flv

4.2 源码

Log.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
#pragma warning(disable : 4996)
#include <time.h>
#include <string>

static std::string Get_CurTime(const char* fmt = "%Y-%m-%d %H:%M:%S")
{
time_t t = time(nullptr);
char str[64];
strftime(str, sizeof(str), fmt, localtime(&t));
std::string time(str);
return time;
}

#define LOGI(format, ...) \
fprintf(stderr, "[INFO]%s [%s:%d %s()]" format"\n", Get_CurTime().data(), __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#define LOGE(format, ...) \
fprintf(stderr, "[ERROR]%s [%s:%d %s()]" format"\n", Get_CurTime().data(), __FILE__, __LINE__, __func__, ##__VA_ARGS__)

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include "Log.h"
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
int port = 8080;
const char* filename = "D:/ffmpeg/learn/test.flv";

WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
LOGE("WSAStartup error");
return -1;
}
SOCKADDR_IN sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
sockAddr.sin_port = htons(port);

SOCKET serverFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (bind(serverFd, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
LOGE("socket bind error");
return -1;
}
if (listen(serverFd, SOMAXCONN) < 0)
{
LOGE("socket listen error");
return -1;
}
//Expires 设置资源的有效期来控制http的缓存
//pragma 用于客户端发送的请求中, 客户端会要求所有的中间服务器不返回缓存的资源
const char http_headers[] = \
"HTTP/1.1 200 OK\r\n" \
"Access-Control-Allow-Origin: * \r\n" \
"Content-Type: video/x-flv\r\n" \
"Content-Length: -1\r\n" \
"Connection: Keep-Alive\r\n" \
"Expires: -1\r\n" \
"Pragma: no-cache\r\n" \
"\r\n";
int http_headersLen = strlen(http_headers);

while (true)
{
LOGI("wait connection");
int len = sizeof(SOCKADDR);
SOCKADDR_IN accept_addr;
int clientFd = accept(serverFd, (SOCKADDR*)&accept_addr, &len);
if (clientFd == SOCKET_ERROR)
{
LOGE("accept connection error");
break;
}
LOGI("new connetion clientFd = %d", clientFd);

FILE* fp = fopen(filename, "rb");
if (!fp) return -1;

unsigned char buf[5000];
char bufRecv[2000] = { 0 };
int times = 0;

while (true)
{
++times;
if (times == 1)
{
int bufRecvSize = recv(clientFd, bufRecv, 2000, 0);
LOGI("bufRecvSize = %d, bufRecv =\n%s", bufRecvSize, bufRecv);
send(clientFd, http_headers, http_headersLen, 0);
}
else
{
Sleep(10);
int bufLen = fread(buf, 1, sizeof(buf), fp);
int ret = send(clientFd, (char*)buf, bufLen, 0);
if (ret <= 0) break;
}
}
if (fp) fclose(fp);
closesocket(clientFd);
LOGI("close connect clientFd = %d", clientFd);

}
closesocket(serverFd);
return 0;
}

抓包分析

1
ffplay -i http://127.0.0.1:8080

与运行结果一致


5. RTMPServer

5.1 RTMP协议推流流程简介

RTMP协议规范详细可看rtmp_specification_1.0.pdf
中文版

TCP三次握手后, RTMP协议的流媒体推流需经过以下步骤:

握手
RTMP连接都是以握手作为开始的
服务端与客户端需要通过3次交换报文完成握手,RTMP是由三个静态大小的块,而非可变的块
客户端与服务器发送相同的三个chunk,客户端发送c0,c1,c2,服务端发送s0,s1,s2

客户端发送 C0,C1 块,握手开始
服务端在发送 S0 和 S1 之前必须等待接收 C0, 也可等待 C1, 发送 S2 之前必须等待接收 C1
客户端在发送 C2 之前客户端必须等待接收 S1
客户端在发送任何数据之前客户端必须等待接收 S2
服务端在发送任何数据之前必须等待接收 C2.

RTMP建立连接

  1. 客户端发送命令消息中的连接(connect)到服务器,请求与一个服务应用实例建立连接。
  2. 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序, 并且发送设置带宽协议(Set Peer Bandwidth)消息到客户端。
  3. 客户端处理设置带宽协议(Set Peer Bandwidth)消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。
  4. 服务器发送用户控制消息中的流开始(Stream Begin)消息到客户端, 并发送命令消息中的结果(_result),通知客户端连接的状态。

RTMP建流

客户端在收到来自服务器的建流命令(createstream)的成功结果(_result)后发送play命令。
服务器在接收到play命令后,发送一个来设置块大小(SetChunkSize)消息。
再发送另一个用户控制消息,指定事件“流记录”(StreamIsRecorded)和流ID, 这个消息的头2字节携带事件类型,最后4字节携带流ID。
再发送另一个用户控制消息,指定事件“流开始”(StreamBegin)。向客户端指示流的开始。
如果客户端发送的播放(play)命令成功,服务器发送命令消息(onStatus),NeStream.Play.Start & NeStream.Play.Reset
只有当客户端发送的play命令设置了reset标志时,服务器才会发送NeStream.Play.Reset
如果没有找到要播放的流,服务器将发送onStatus消息NeStream.Play.StreamNotFound
之后,客户端播放服务器发送的音频和视频数据

通过抓包理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Client :
connect(‘livetv’)
#
Server :
Window Acknowledgement Size 5000000
Set Peer Bandwidth 5000000,Dynamic
Set Chunk Size 4096
_result(‘NetConnection.Connect.Success’)
#
Client :
Window Acknowledgement Size 5000000
createStream()
#
Server :
_result()
#
Client :
getStreamLength()
play(‘hunantv’)
Set Buffer Length 1,3000ms
#
Server :
Stream Begin 1
onStatus(‘NetStream.Play.Start’)
RtmpSampleAccess()
onMetaData()
#
Server :
Video Data
Audio Data

5.2 源码

RTMPServer


6. 浏览器播放HLS和HTTP-FLV流

6.1 zlm 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

// rtsp推流(文件推流)
ffmpeg -re -i test.mp4 -rtsp_transport tcp -c copy -f rtsp rtsp://127.0.0.1:554/live/test

// rtsp推流(文件循环推流)
ffmpeg -re -stream_loop -1 -i test.mp4 -rtsp_transport tcp -c copy -f rtsp rtsp://127.0.0.1:554/live/test

// rtmp推流(文件推流)
ffmpeg -re -i test.mp4 -vcodec h264_nvenc -acodec aac -f flv rtmp://192.168.1.3:1935/live/test

// rtmp推流(文件循环推流)
ffmpeg -re -stream_loop -1 -i test.mp4 -vcodec h264 -acodec aac -f flv rtmp://127.0.0.1:1935/live/test

// ZLMediaKit支持多种流媒体协议的转换,协议转换后的播放地址

//rtsp播放
rtsp://127.0.0.1:554/live/test

//rtmp播放
rtmp://127.0.0.1:1935/live/test

//hls播放
http://127.0.0.1:80/live/test/hls.m3u8

//http-flv播放
http://127.0.0.1:80/live/test.live.flv

//http-ts播放
http://127.0.0.1:80/live/test.live.ts

6.2 网页播放hls

视频需要先推流给zlm(服务端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HlsPlayer</title>
<link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet"/>
</head>
<body style="text-align:center;">

<h1>网页端播放 HLS(<code>.m3u8</code>) 视频</h1>
<p>借助 video.js 和 videojs-contrib-hls</p>
<p>由于 videojs-contrib-hls 需要通过 XHR 来获取解析 m3u8 文件, 因此会遭遇跨域问题, 请设置浏览器运行跨域</p>

<video id="video" class="video-js vjs-default-skin vjs-big-play-centered" controls style="margin: auto" width="640" height="268" >
<<source src="http://127.0.0.1:80/live/test/hls.m3u8" type="application/x-mpegURL">
<!--source src="http://127.0.0.1:9003/live/S4de6606d99305/hls.m3u8" type="application/x-mpegURL"> -->
</video>
<script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>
<!-- 网页端浏览器不支持播放 hls 文件(m3u8), 需要 videojs-contrib-hls 来解码 -->
<script src="https://unpkg.com/videojs-contrib-hls/dist/videojs-contrib-hls.js"></script>
<script>
const player = videojs('video');
player.play();
</script>
</body>
</html>

6.3 网页播放http-flv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>httpflvplayer</title>

<style>
#flv-player{
/*background-color: rgba(0,0,00,0.6);*/
width: 100%;
height: 100%;
max-height: 520px;
/*object-fit: fill;*/
border-radius: 5px;
border: 1px solid #f0f0f0;

}
#flv-player:focus {
outline: -webkit-focus-ring-color auto 0px;
}

</style>

</head>
<body>
<div>
<video id="flv-player" controls autoplay>
Your browser is too old which doesn't support HTML5 video.
</video>
<div>
<button id="play" onclick="play()">播放</button>
<button id="stop" onclick="stop()">停止</button>

</div>
</div>
</body>
<script src="flv/flv.min.js"></script>
<script>

let eleFlvPlayer= document.getElementById("flv-player");

let eleBtnPlay = document.getElementById("play");
let eleBtnStop = document.getElementById("stop");


function play(){
//let flvUrl = "http://127.0.0.1:8080/test.flv";
let flvUrl = "http://127.0.0.1:80/live/test.live.flv";
console.log(flvUrl);

if(!flvjs.isSupported()){
alert("浏览器不支持flv.js!");
return;
}
if(typeof flvUrl === "undefined" || !flvUrl.endsWith(".flv")){
alert("播放地址格式不正确!");
return;
}


let mediaDataSource = {
type: 'flv',
enableStashBuffer:true,
isLive:true,
withCredentials:false,
hasAudio:true,
hasVideo:true,
url:flvUrl
};
//console.log('MediaDataSource', mediaDataSource);
//eleFlvPlayer.setAttribute('poster',"/static/images/media.jpg")

if (typeof player !== "undefined") {
if (player != null) {
player.unload();
player.detachMediaElement();
player.destroy();
player = null;
}
}
player = flvjs.createPlayer(mediaDataSource, {
enableWorker: false,
lazyLoadMaxDuration: 3 * 60,
seekType: 'range',
});
player.attachMediaElement(eleFlvPlayer);
player.load();
player.play();
}
function stop(){
if (typeof player === "undefined" || player === null) {
alert("播放器暂未启动!");
return;
}
player.pause();
player.unload();
player.detachMediaElement();
player.destroy();
player = null;

}

eleBtnPlay.click();

</script>

</html>

6.4 高性能HTTP-FLV服务器

6.4.1 配置boost库和jsoncpp库

6.4.2 源码

HTTP-FLVServer