9. 【RTMP协议和传输】

RTMP协议介绍

为了替代RMTP协议,苹果出了一个HLS协议,Adobe已经决定不再维护RTMP协议;目前国内大部分还是使用的RTMP协议,它在传输和实时性方面都要强于HLS;
作用:用于娱乐直播的或者点播
通信步骤:

  • 先基于socket进行TCP连接;

  • tcp连接之后再进行握手;
    客户端先发个c0+c1,服务器收到后往客户端发送s0+s1+s2,客户端最后发送个c2,就结束了握手流程;


    握手流程
  • 握手完成后再确定建立rtmp连接;
    客户端向服务端发送个连接到消息;
    服务端回可变窗口控制的大小、带宽和数据块的大小,以及连接成功的消息;
    客户端也会放一个传输块的最大大小;


    连接流程
  • 创建RTMP流,连接之后数据以stream的方式进行数据交互;


    创建rtmp流
  • 推流流程
    其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中都被加了个头部,

  1. 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
  1. 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

  1. 推流步骤
    1. 生成获取FLV文件
      二进制读取方式打开FLV文件,并且跳过flv的头部和第一个pre_tag_size,使用fseek;
    2. 获取FLV中的音视频数据,读取到RTMPPacket中
    • tag的12个字节是tag的头部,从头部中读取关键信息,再根据头部信息的size获取flv文件里面的频数据到packet->mbody中;由于FLV是大端存储,再因为intel处理器是小端读取,所以在头部的信息需要将大端转换成小端进行存储;
    • 设置rtmp头部类型m_headerType为RTMP_PACKET_SIZE_LARGE,用最长的消息长度方式;
    • 设置时间戳m_nTimestamp,音视频同步使用
    • 设置数据类型m_packetType
    • 设置数据大小m_nBodySize
    1. 初始化librtmp对象: RTMP_Alloc、RTMP_Init
      设置超时时间:rtmp->Link->timeout
      设置推流地址 : RTMP_SetupURL
      设置是否是推流:RTMP_EnableWrite 设置了就是推流 未设置就是播流
      连接流媒体服务器:RTMP_Connect
      创建流:RTMP_ConnectStream(); 从0开始,创建流可能会失败
    2. 利用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后,再发送数据;

代码实战

  1. 打开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;
}
  1. 初始化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;
}
  1. 初始化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;
}
  1. 从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;  
}
  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);
}
  1. 推流总体调用
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);
    }

}

你可能感兴趣的:(9. 【RTMP协议和传输】)