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, &timestamp) < 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);
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容