FLV(2) ------FLV实战

我们上一节讲了FLV格式具体结构,今天我们通过librtmp进行推流来实战。

准备工作:

我们使用Xcode进行代码编写。

本文要使用到librtml库,我们要安装下。

brew install rtmpdump

安装成功以后,我们通过pkg-config --libs --cflags librtmp发现依赖于openssl

brew install openssl

安装成功后执行下面命令完成环境变量设置

 export PKG_CONFIG_PATH=$(find /usr/local/Cellar -name 'pkgconfig' -type d | grep lib/pkgconfig | tr '\n' ':' | sed s/.$//) 

再执行

pkg-config --libs --cflags librtmp

         

其中提示了需要引入的头文件和库文件的位置

我们在xcode当中进行引入

1、引入头文件

2、引入库文件

Xcode添加库无法好到/usr/local目录。

可以如下操作

按下CMD + Shift + G,然后输入“/usr/local/xxxxx”,之后选择libxxx.dylib文件。

其中libz是系统默认就有的

准备flv文件

使用ffmpeg 生成 将mp4 生成 flv文件

ffmpeg -i out.mp4 -vcodec copy -acodec copy out.flv

推流步骤简单描述就是下面这样的

  • 解析FLV文件
  • 获取音视频数据
  • 利用librtmp进行推流

解析flv文件

我们首先要打开flv文件

第一步:打开flv文件

static FILE* open_flv(char* flvaddr){
    FILE* fp = NULL;
    fp = fopen(flvaddr, "rb");
    if(!fp){
        printf("failed to open flv:%s", flvaddr);
        return NULL;
    }
    fseek(fp, 9, SEEK_SET); //跳过flv头 9个字节
    fseek(fp, 4, SEEK_CUR); //跳过pretagsize 4个字节
    return fp;

}

我们知道flv文件有一个flvheader,flvheader的具体内容如下

第1-3字节表示当前是‘F’ 、'L'、‘V’

第4个字节表示当前的flv版本号

第5个字节:具体表示为1-5位为0,6位代表存在音频,7位保留位是0, 8位代表存在视频

第6-9个字节:代表header的大小,目前数值为9。

所以也就有了 fseek(fp, 9, SEEK_SET); //跳过flv头 9个字节 SEEK_SET是文件头位置

flvheader完了以后,是pretagsize 前一个tag的大小,占4个字节。

所以就有了 fseek(fp, 4, SEEK_CUR); //跳过pretagsize 4个字节,SEEK_CUR游标当前位置。

这时候,游标执行了真正的数据区tagData。

第二步 连接RTMP服务器

                                                                                                                     

我们要想来接rtmp服务器,首先我们alloc一个rtmp的空间,然后初始化,设置超时时间、推流地址,设置开启推流,然后进行连接,然后连接流。

RTMP_EnableWrite这个方法调用就是推流,不调用就是拉流。

static RTMP* connect_rtmp_server(char* rtmp_addr){
    RTMP* rtmp = NULL;
    rtmp = RTMP_Alloc();
    if(!rtmp){
        printf("NO Memory");
        goto __ERROR;
    }
    RTMP_Init(rtmp);
    //设置rtmp的超时时间和rtmp的连接地址
    rtmp->Link.timeout = 10;
    RTMP_SetupURL(rtmp, rtmp_addr);
    //设置推流还是拉流,设置开启是推流,不设置是拉流
    RTMP_EnableWrite(rtmp);

    //建立链接
    if(!RTMP_Connect(rtmp, NULL)){
        printf("failed to connect");
        goto __ERROR;
    }
    //创建流
    RTMP_ConnectStream(rtmp, 0);
    return rtmp;
__ERROR:
    if(rtmp){
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    
    return rtmp;
}

第三步读取数据并发送

                                                                           

读取的数据是放在RTMPPacket当中的,读取后我们也是要把RTMPPacket当中的数据发到RTMP服务器上。

所以首先我们要通过下面的代码分配一个alloc_packet

static RTMPPacket *alloc_packet(){
    RTMPPacket *pack = NULL;
//    pack = (RTMPPacket*)malloc(sizeof(RTMPPacket));
    pack =(RTMPPacket*)malloc(sizeof(RTMPPacket));
    if(!pack){
        printf("NO Memory alloc_packet");
        return NULL;
    }
    RTMPPacket_Alloc(pack, 64 * 1024);
    RTMPPacket_Reset(pack);
    pack->m_hasAbsTimestamp = 0;
    pack->m_nChannel = 0x04;
    
    return pack;
}

代码中我们谁知不使用就对时间戳,传输通道设置为4.

最关键的是读数据,我们知道tag有tag头和tag Data。我们先分析tag头。

第一个字节 TT(Tag Type), 0x08 音频,0x09 视频, 0x12 script

2-4, Tag body 的长度,

5-7, 时间戳,单位是毫秒; script 它的时间戳是0

第8个字节,扩展时间戳。真正时间戳结格 [扩展,时间戳] 一共是4字节。

9-11, streamID, 0 目前我知道的都是0

我们自定义几个读一个字节 二个字节 三个字节的函数

static void read_u8(FILE* fp, unsigned int *u8){
    fread(u8, 1, 1, fp);
    return;
}



static void read_u24(FILE* fp, unsigned int *u24){
    unsigned int temp;
    fread(&temp, 1, 3, fp);
    *u24 = ((temp >> 16) & 0xFF)| ((temp << 16) & 0xFF0000) | (temp &0xFF00);
    return;
}


static void read_u32(FILE* fp, unsigned int *u32){
    unsigned int temp;
    fread(&temp, 1, 4, fp);
    *u32 = ((temp >> 24) & 0xFF) ||  ((temp >> 8) & 0xFF00)| ((temp << 8) & 0xFF0000) | ((temp << 24)&0xFF000000);
    return;
}

电脑的存储是小端格式存储的,但是RTMP的数据是大端的所以也就有了上面代码中的移位和与操作。

在这里需要注意时间戳的读取

时间戳加上扩展字段总共4个字节

static int read_ts(FILE *fp, unsigned int *ts){
    unsigned int tmp;
    if(fread(&tmp, 1, 4, fp) !=4) {
        printf("Failed to read_ts!\n");
        return -1;
    }
    
    *ts = ((tmp >> 16) & 0xFF) | ((tmp << 16) & 0xFF0000) | (tmp & 0xFF00) | (tmp & 0xFF000000);
    
    return 0;
}

通过下面的函数我们读取到tag的类型,tag数据的size,时间戳是四字节,以及streamId。

read_u8(fp, &tt);

read_u24(fp, &tag_data_size);

read_ts(fp, &ts);

read_u24(fp, &streamId);

我们通过

fread((*pack)->m_body, 1, tag_data_size, fp) 将数据读取到RTMPPacket的body中

同时我们将获取到的时间戳,数据类型,数据大小放入RTMPPacket中

(*pack)->m_nTimeStamp = ts;

(*pack)->m_packetType = tt;

(*pack)->m_nBodySize = tag_data_size;

我们同时设置m_headerType,用来标识Message Header有几个字段。我们这里设置为RTMP_PACKET_SIZE_LARGE

(*pack)->m_headerType = RTMP_PACKET_SIZE_LARGE;

RTMP结构当中我们知道

Message Header

如果fmt = 10b,Message Header 有3个字节 TimeStamp

fmt == 01b, MessageHeader 有7个字节 . TimeStamp MsgLength TypeID

fmt == 00b, MessageHeader 有11个字节 TimeStamp MsgLength TypeID StreamID

同时我们查看源码得知

#define RTMP_PACKET_SIZE_LARGE 0

#define RTMP_PACKET_SIZE_MEDIUM 1

#define RTMP_PACKET_SIZE_SMALL 2

#define RTMP_PACKET_SIZE_MINIMUM 3

我们设置RTMP_PACKET_SIZE_LARGE,这时我们的 Message Header就有四部分 TimeStamp MsgLength TypeID StreamID

int  read_data(FILE* fp, RTMPPacket **pack){
    /*
        * tag header
        * 第一个字节 TT(Tag Type), 0x08 音频,0x09 视频, 0x12 script
        * 2-4, Tag body 的长度, PreTagSize - Tag Header size
        * 5-7, 时间戳,单位是毫秒; script 它的时间戳是0
        * 第8个字节,扩展时间戳。真正时间戳结格 [扩展,时间戳] 一共是4字节。
        * 9-11, streamID, 0
        */
    unsigned int tt;
    unsigned int tag_data_size;
    unsigned int ts;
    unsigned int streamId;
    unsigned int tag_pre_size;

    int ret = -1;
    size_t data_size = 0;
    if(read_u8(fp, &tt)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &tag_data_size)){
        goto __ERROR;
    }
    
    if(read_ts(fp, &ts)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &streamId)){
        goto __ERROR;
    }
    data_size = fread((*pack)->m_body, 1, tag_data_size, fp);
    if(tag_data_size != data_size){
        printf("read data size error tag_data_size = %d, real data size = %d\n", tag_data_size, data_size);
        goto __ERROR;
    }
    (*pack)->m_headerType = RTMP_PACKET_SIZE_LARGE;
    (*pack)->m_nTimeStamp = ts;
    (*pack)->m_packetType = tt;
    (*pack)->m_nBodySize = tag_data_size;
//每个tag前面都有一个pre_tag_size,读取下一个数据的时候,需要跳过。
    read_u32(fp, &tag_pre_size);
    ret = 0;
__ERROR:
    return ret;
    
}

数据读出来以后我们就可以通过RTMP_SendPacket发送出去了。

       RTMP_SendPacket(rtmp, packet, 0);

每一个tag都有自己的播放时间,如果我们把所有数据都一下推送出去肯定是有问题。那么我们可以推送了一个tag,休眠这个tag的播放时间,然后再读取数据进行推送。

我在本机搭建了一个rtmp服务器 rtmp的地址是"rtmp://localhost/live/room"

接下来的文章我会讲解如何搭建rtmp服务器。

我们推送开始后可以通过ffplay rtmp://localhost/live/room进行播放

下面是完整的代码

//
//  pushstream.c
//  PushStream
//
//  Created by yuanxuzhen on 2021/4/15.
//

#include "pushstream.h"
#include "librtmp/rtmp.h"

/*
 flv文件 头部有9个字节,
 第一个字节是字母F,
 第二个字节是L,
 第三个字节是V,
 第四个字节是版本号,
 第五个字节 1-5位保留,必须是0,第6位是否有音频tag 第7位保留必须是0,第8位是否有视频tag
 第六到九字节代表header的大小,必须是9
 */

static int status = 1;
void set_status(int state){
    status = state;
}

static FILE* open_flv(char* flvaddr){
    FILE* fp = NULL;
    fp = fopen(flvaddr, "rb");
    if(!fp){
        printf("failed to open flv:%s", flvaddr);
        return NULL;
    }
    fseek(fp, 9, SEEK_SET); //跳过flv头 9个字节
    fseek(fp, 4, SEEK_CUR); //跳过pretagsize 4个字节
    return fp;

}

static RTMP* connect_rtmp_server(char* rtmp_addr){
    RTMP* rtmp = NULL;
    rtmp = RTMP_Alloc();
    if(!rtmp){
        printf("NO Memory");
        goto __ERROR;
    }
    RTMP_Init(rtmp);
    //设置rtmp的超时时间和rtmp的连接地址
    rtmp->Link.timeout = 10;
    RTMP_SetupURL(rtmp, rtmp_addr);
    //设置推流还是拉流,设置开启是推流,不设置是拉流
    RTMP_EnableWrite(rtmp);

    //建立链接
    if(!RTMP_Connect(rtmp, NULL)){
        printf("failed to connect");
        goto __ERROR;
    }
    //创建流
    RTMP_ConnectStream(rtmp, 0);
    return rtmp;
__ERROR:
    if(rtmp){
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    
    return NULL;
}
static RTMPPacket *alloc_packet(){
    RTMPPacket *pack = NULL;
//    pack = (RTMPPacket*)malloc(sizeof(RTMPPacket));
    pack =(RTMPPacket*)malloc(sizeof(RTMPPacket));
    if(!pack){
        printf("NO Memory alloc_packet");
        return NULL;
    }
    RTMPPacket_Alloc(pack, 64 * 1024);
    RTMPPacket_Reset(pack);
    pack->m_hasAbsTimestamp = 0;
    pack->m_nChannel = 0x4;
    
    return pack;
}

static int read_u8(FILE* fp, unsigned int *u8){
    unsigned int tmp;
    if(fread(&tmp, 1, 1, fp) != 1){
        printf("Failed to read_u8!\n");
        return -1;
    }
    
    *u8 = tmp & 0xFF;
    return 0;
}



static int read_u24(FILE* fp, unsigned int *u24){
    unsigned int tmp;
    if(fread(&tmp, 1, 3, fp) != 3){
        printf("Failed to read_u24!\n");
        return -1;
    }
    *u24 = ((tmp >> 16) & 0xFF)| ((tmp << 16) & 0xFF0000) | (tmp &0xFF00);
    return 0;
}


static int read_u32(FILE* fp, unsigned int *u32){
    unsigned int tmp;
    if(fread(&tmp, 1, 4, fp) != 4){
        printf("Failed to read_u32!\n");
        return -1;
    }
    *u32 = ((tmp >> 24) & 0xFF) ||  ((tmp >> 8) & 0xFF00)| ((tmp << 8) & 0xFF0000) | ((tmp << 24)&0xFF000000);
    return 0;
}

static int read_ts(FILE *fp, unsigned int *ts){
    unsigned int tmp;
    if(fread(&tmp, 1, 4, fp) !=4) {
        printf("Failed to read_ts!\n");
        return -1;
    }
    
    *ts = ((tmp >> 16) & 0xFF) | ((tmp << 16) & 0xFF0000) | (tmp & 0xFF00) | (tmp & 0xFF000000);
    
    return 0;
}

int  read_data(FILE* fp, RTMPPacket **pack){
    /*
        * tag header
        * 第一个字节 TT(Tag Type), 0x08 音频,0x09 视频, 0x12 script
        * 2-4, Tag body 的长度, PreTagSize - Tag Header size
        * 5-7, 时间戳,单位是毫秒; script 它的时间戳是0
        * 第8个字节,扩展时间戳。真正时间戳结格 [扩展,时间戳] 一共是4字节。
        * 9-11, streamID, 0
        */
    unsigned int tt;
    unsigned int tag_data_size;
    unsigned int ts;
    unsigned int streamId;
    unsigned int tag_pre_size;

    int ret = -1;
    size_t data_size = 0;
    if(read_u8(fp, &tt)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &tag_data_size)){
        goto __ERROR;
    }
    
    if(read_ts(fp, &ts)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &streamId)){
        goto __ERROR;
    }
    data_size = fread((*pack)->m_body, 1, tag_data_size, fp);
    if(tag_data_size != data_size){
        printf("read data size error tag_data_size = %d, real data size = %d\n", tag_data_size, data_size);
        goto __ERROR;
    }
    (*pack)->m_headerType = RTMP_PACKET_SIZE_LARGE;
    (*pack)->m_nTimeStamp = ts;
    (*pack)->m_packetType = tt;
    (*pack)->m_nBodySize = tag_data_size;
    read_u32(fp, &tag_pre_size);
    ret = 0;
__ERROR:
    return ret;
    
}

static void send_data(FILE* fp, RTMP *rtmp){
    //1、创建RTMPPacket对象
    RTMPPacket *packet = NULL;
    unsigned int pre_ts = 0;
    packet = alloc_packet();
    packet->m_nInfoField2 = rtmp->m_stream_id;

    while (1) {
        //从flv读取文件
        //2.从flv文件中读取数据
        if(read_data(fp, &packet)){
            printf("over!\n");
            break;
        }
        //判断rtmp是否还处在链接状态
        if(!RTMP_IsConnected(rtmp)){
            printf("Disconnect....\n");
            break;
        }

        unsigned int diff = packet->m_nTimeStamp - pre_ts;
        usleep(diff * 1000);
        //发送数据
        RTMP_SendPacket(rtmp, packet, 0);
        pre_ts = packet->m_nTimeStamp;

        
    }
    
}




void push_stream(){
    char* rtmp_addr = "rtmp://localhost/live/room";

    char* flv = "/Users/yuanxuzhen/study/mac/PushStream/PushStream/output/test_yuv.flv";
    //读flv文件
    FILE* fp = open_flv(flv);
    
    //链接RTMP服务器
    RTMP* rtmp = connect_rtmp_server(rtmp_addr);
    
    //push video/audio data
    send_data(fp, rtmp);
    
}

gitlee地址:

https://gitee.com/creat151/ffmpeg.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值