RTMP协议介绍
为了替代RMTP协议,苹果出了一个HLS协议,Adobe已经决定不再维护RTMP协议;目前国内大部分还是使用的RTMP协议,它在传输和实时性方面都要强于HLS;
作用:用于娱乐直播的或者点播
通信步骤:
先基于socket进行TCP连接;
-
tcp连接之后再进行握手;
客户端先发个c0+c1,服务器收到后往客户端发送s0+s1+s2,客户端最后发送个c2,就结束了握手流程;
-
握手完成后再确定建立rtmp连接;
客户端向服务端发送个连接到消息;
服务端回可变窗口控制的大小、带宽和数据块的大小,以及连接成功的消息;
客户端也会放一个传输块的最大大小;
-
创建RTMP流,连接之后数据以stream的方式进行数据交互;
-
推流流程
其metaData就是音视频流的基本信息,如采样率、采样大小、通道数、帧率、分辨率等;
-
播流流程
-
RTMP消息的结构
消息有头部和body组成
basic header:是必须要有的,占用一个字节,前两位,表示格式,后六位表示chunk stream id;
message header:中有时间戳、消息长度、数据类型ID、流ID。当因为数据量太大而被拆分成多个chunk 的时候,根据消息是否属于同一个流、同一个类型数据、消息长度相同、时间戳相同,决定是否需要这几个字段是否需要省略;
Extended timestamp:扩展时间戳,当message header中的时间戳3个字节不足以表示的时候,就需要这个扩展,也就是timestamp=0xFFFFFF的时候;
1.message header和Extended timestamp根据basic header头部中的信息决定的;
2.当chunk stream id为0时,basic header占用2个字节,多出字节用来表示更多的chunk stream id;
3.当chunk stream id为1时,basic header占用4个字节,多出字节用来表示更多的chunk stream id;
4.basic header中的前两位决定message header的长度,当fmt == 00,表示message header全有;当fmt = 01,消息头部占用7个字节,当fmt = 10,消息头部只占用3个字节;当fmt = 11,没有消息头部;
消息的类型:有三种:控制消息、音视频数据、命令消息;
set chunk size : 设置chunk包的大小,默认是128字节;
abort message:当某个流结束的时候,告诉服务端就不需要接受这个流了;
acknowledgement:协商从那个起始点开始确认消息;
window acknowledgement size :设置滑动窗口的大小
set peer bandwidth :告诉对方本机的最大一次可传输的数据量,也就是带宽;
data message : 就是音视频数据的元数据,比如推流前的metaData,AMF0和AMF1是flash数据编码的一种格式;
shared object message : 共享消息
command message : 命令消息
FLV协议
FLV是一种文件,在将视频文件进行推流时,会先生成flv文件。所有的rtmp数据在flv中都被加了个头部,
- FLV文件结构
- 9个字节头部:1=F、2=L 、3=V、4=版本、5=类型、6789=表示头部的大小,固定是9 ;
- 其中头部的第5个字节中的前五位和第七位是保留位,第6位表示是否有音频tag,第8位表示是否有视频tag;
- 后面所有的内容就是由pre_tag_size、tag组成,其中pre_tag_size表示前一个tag的大小,占用4字节;
-
每个tag又由tag_header、tag_data组成,
tag_header是对tag_data的描述包括:TT(标签类型)是音频还是视频, datasize(data的长度)、timesta(时间戳)、E(前面时间戳的扩展)、SID(流id);
tag_data分为音频和视频数据:
其中音频数据audio data又由头部和 aac data组成,头部是音频的采样信息,aac data又由音频配置信息和adts包装的音频数据,这个aac data是rtmp协议真正需要的;
其中的视频数据video data由头部和AVCVideoPacket组成,头部是表示编码器id和编码器类型,AVCVideoPacket前面也有一个类型和时间戳,AVCVideoPacket里面就是sps、pps和NAL组成;
- FLV 文件分析器
Diffindo下载
- 编译:在下载文件的gcc目录下执行 ./flv_compile_clang.sh,成功后生成gcc_flv文件夹;
- 开始分析:FLVParser flv文件路径 输出文件路径;
使用ffmpeg根据视频文件生成flv文件:ffmpeg -i 视频文件 -f flv 文件路径;
推流实践
安装librtmp:
1.使用brew rtmpdump 安装librtmp
2.openssl 我使用的源码的方式直接在文件下面执行./Configure && make && make install
- 推流步骤
- 生成获取FLV文件
二进制读取方式打开FLV文件,并且跳过flv的头部和第一个pre_tag_size,使用fseek; - 获取FLV中的音视频数据,读取到RTMPPacket中
- tag的12个字节是tag的头部,从头部中读取关键信息,再根据头部信息的size获取flv文件里面的频数据到packet->mbody中;由于FLV是大端存储,再因为intel处理器是小端读取,所以在头部的信息需要将大端转换成小端进行存储;
- 设置rtmp头部类型m_headerType为RTMP_PACKET_SIZE_LARGE,用最长的消息长度方式;
- 设置时间戳m_nTimestamp,音视频同步使用
- 设置数据类型m_packetType
- 设置数据大小m_nBodySize
- 初始化librtmp对象: RTMP_Alloc、RTMP_Init
设置超时时间:rtmp->Link->timeout
设置推流地址 : RTMP_SetupURL
设置是否是推流:RTMP_EnableWrite 设置了就是推流 未设置就是播流
连接流媒体服务器:RTMP_Connect
创建流:RTMP_ConnectStream(); 从0开始,创建流可能会失败 - 利用librtmp传输
rtmp传输的数据需要被包装到RTMPPacket中,循环读取flv文件发送;
- 初始化RTMPPacket:malloc 分配空间、RTMPPacket_Alloc分配缓冲区最大传输 = 64 x 1024、RTMPPacket_Reset重置缓冲区、m_hasAbsTimestmp不要绝对时间戳、m_nChannel = 0x4;
- 从flv中读取音视频数据(看第二步);
- 判断rtmp连接是否正常 RMTP_IsConnected
- 发送数据RTMP_Send_Packet,队列大小等于0就好;
- 由于服务端缓冲区有限,所以应该在每发送一段数据后,就延迟数据的播放时长后再发送下一段数据,利用当前tag的时间戳减去上一个tag的时间戳 = 需要休眠的时间。调用usleep后,再发送数据;
- 生成获取FLV文件
代码实战
- 打开flv文件,跳过flv头部和第一个pre_tag_size;
static FILE* open_flv(const char *path) {
FILE *file = fopen(path, "rb");
if (!file) {
printf("flv文件打开失败\n");
return NULL;
}
//跳过flv文件的头部 9个字节
fseek(file, 9, SEEK_SET);
// 跳过第一个presize
fseek(file, 4, SEEK_CUR);
return file;
}
- 初始化librtmp对象,并建立连接
static RTMP* connect_rtmp(char *rtmp_url) {
RTMP *rtmp = NULL;
int result = -1;
rtmp = RTMP_Alloc();
if (rtmp == NULL) {
printf("初始化rtmp 失败\n");
goto __ERROR;
}
RTMP_Init(rtmp);
rtmp->Link.timeout = 10;
RTMP_SetupURL(rtmp, rtmp_url);
// 确认是推流
RTMP_EnableWrite(rtmp);
result = RTMP_Connect(rtmp, NULL);
if (result < 0) {
printf("rtmp 连接失败:%s\n", av_err2str(result));
goto __ERROR;
}
result = RTMP_ConnectStream(rtmp, 0);
if (result < 0) {
printf("rtmp 创建流失败:%s\n", av_err2str(result));
}
return rtmp;
__ERROR:
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
}
return rtmp;
}
- 初始化RTMPPacket对象
static RTMPPacket* init_packet() {
RTMPPacket *packet = NULL;
packet = malloc(sizeof(RTMPPacket));
// 最大传输64kb
if (RTMPPacket_Alloc(packet, 64 * 1024) < 0) {
printf("packet缓冲区分配失败\n");
RTMPPacket_Free(packet);
return NULL;
}
RTMPPacket_Reset(packet);
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x4;
return packet;
}
- 从flv文件中读取tag的数据
//读取文件数据
static int read_data_unsigned8(FILE *file,unsigned char *data) {
if(fread(data, 1, 1, file) != 1) {
return -1;
}
return 0;
}
static int read_data_unsigned24(FILE *file,uint32_t *data) {
if (fread(data, 1, 3, file) != 3) {
return -1;
}
// 因为FLV中是大端存储的 又因为Intel处理器是小端存储的 所以需要将大端转换成小端存储
*data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00);
return 0;
}
static int read_timestamp(FILE *file, uint32_t *data) {
if (fread(data, 1, 4, file) != 4) {
return -1;
}
// 因为FLV中是大端存储的 又因为Intel处理器是小端存储的 所以需要将大端转换成小端存储
// 扩展时间戳解析时放在高8位
*data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00) | (*data & 0xff000000);
return 0;
}
static int read_data_to_packet(FILE *file, RTMPPacket **packet) {
// 数据类型
uint8_t type;
// tag的大小
uint32_t data_size;
// 时间戳
uint32_t timestamp;
// 流id
uint32_t stream_id;
if (read_data_unsigned8(file, &type) < 0 ||
read_data_unsigned24(file, &data_size) < 0 ||
read_timestamp(file, ×tamp) < 0 ||
read_data_unsigned24(file, &stream_id) < 0) {
printf("读取flv tag 头部信息失败!\n");
goto __ERROR;
}
size_t read_size = fread((*packet)->m_body, 1, data_size, file);
if (read_size != data_size) {
printf("读取flv 中的数据出错!\n");
goto __ERROR;
}
// 相当于message的头部全开启 占用11个字节
(*packet)->m_headerType = RTMP_PACKET_SIZE_LARGE;
(*packet)->m_packetType = type;
(*packet)->m_nBodySize = data_size;
(*packet)->m_nTimeStamp = timestamp;
// 跳过4个字节的pre tag size
fseek(file, 4, SEEK_CUR);
return 0;
__ERROR:
return -1;
}
- 推送数据流到rtmp服务
static void push_data(FILE *file, RTMP *rtmp) {
// 延迟时间戳
useconds_t delay_timestamp = 0;
RTMPPacket *packet = init_packet();
packet->m_nInfoField2 = rtmp->m_stream_id;
while (is_living) {
if (read_data_to_packet(file, &packet) < 0) {
printf("从flv文件中读取信息失败或者读取完毕!\n");
RTMPPacket_Free(packet);
break;
}
if (!RTMP_IsConnected(rtmp)) {
printf("连接已断开!");
break;
}
printf("等待时间 == %d\n", (packet->m_nTimeStamp - delay_timestamp) * 1000);
// usleep 使用的是纳秒
usleep((packet->m_nTimeStamp - delay_timestamp) * 1000);
// 开始发送
RTMP_SendPacket(rtmp, packet, 0);
delay_timestamp = packet->m_nTimeStamp;
}
__ERROR:
RTMPPacket_Free(packet);
}
- 推流总体调用
void start_push(void) {
is_living = 1;
// 1. 打开flv文件
char *path = "/Users/cunw/Desktop/learning/音视频学习/音视频文件/iphone.flv";
FILE *file = open_flv(path);
// 2.连接rtmp服务器 本地nginx服务
char *url = "rtmp://localhost/live/1026238004";
RTMP *rtmp = connect_rtmp(url);
// 3.推送数据
push_data(file, rtmp);
is_living = 0;
// 4. 释放资源
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
}
}