我们上一节讲了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地址: