使用librtmp实现本地推流

这个文档详细介绍了,如何在本地ubuntu上搭建自己的流服务器。并通过librtmp进行测试。

1.0 背景

客户需要我们提供rtmp推流的源代码,然后他们DVR的供应商会负责移植到盒子中。这个demo演示了如何用c实现rtmp推流。

2.0 安装配置流服务器

下面详细介绍如何在ubuntu14.04上安装配置流服务器

2.1 安装 nginx

$ sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev
$ wget http://nginx.org/download/nginx-1.15.1.tar.gz
$ wget https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/dev.zip
$ tar -zxvf nginx-1.15.1.tar.gz
$ unzip dev.zip
$ cd nginx-1.15.1
$ ./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-dev
$ make
$ sudo make install

启动测试下

$ sudo /usr/local/nginx/sbin/nginx

浏览器访问 http://127.0.0.1
测试ngix正常启动了

2.2 安装nginx rtmp服务插件:

vim /usr/local/nginx/conf/nginx.conf

把下面这段添加到末尾

rtmp {
        server {
              listen 1935;
              chunk_size 4096;
              application live {
                     live on;
                     record off;
              }
       }
}

上面配置了rtmp的默认端口是1935,以及rtmp app的名字,这里叫“live”

2.3 重启nginx

$ sudo /usr/local/nginx/sbin/nginx -s stop
$ sudo /usr/local/nginx/sbin/nginx

2.4 测试效果

为了测试我们的流服务器,我们需要安装ffmpeg来往上面推视频流,然后浏览器拉流查看播放结果。

2.4.1 安装ffmpeg

$ sudo add-apt-repository ppa:mc3man/trusty-media
$ sudo apt-get update
$ sudo apt-get install ffmpeg

2.4.2 测试

从 https://sample-videos.com 下载一个mp4文件。
然后用 ffmpeg 推流,如下命令:

$ ffmpeg -re -i ./sample.mp4 -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 rtmp://localhost:1935/live/testav

最后在浏览器中输入:rtmp://127.0.0.1/live/testav 查看推流结果。第一次运行浏览器可能会要求你安装flash插件,点击“安装”即可。

3.0 使用librtmp推流

以上,是利用ffmpeg工具实现的推流,下面介绍如何用c代码实现推流。我们尝试把一个flv文件推到服务器上,并且用浏览器播放。

3.1 下载编译librtmp

首先,下载librtmp的源码。

git clone git://git.ffmpeg.org/rtmpdump

新建一个文件夹,用来存放我们的测试代码main函数,以及Makefile,首先是测试代码,保存为rtmp_push.c

#include 
#include 
#include 
#include 


#include "librtmp/rtmp_sys.h"
#include "librtmp/log.h"

typedef struct FINT16
{
    unsigned char Byte1;
    unsigned char Byte2;
}fint16;

typedef struct FINT24
{
    unsigned char Byte1;
    unsigned char Byte2;
    unsigned char Byte3;
}fint24;

typedef struct FINT32
{
    unsigned char Byte1;
    unsigned char Byte2;
    unsigned char Byte3;
    unsigned char Byte4;
}fint32;

typedef struct FLVHEADER
{
    unsigned char F;
    unsigned char L;
    unsigned char V;
    unsigned char type;
    unsigned char info;
    fint32 len;
}FlvHeader;

typedef struct TAGHEADER
{
    unsigned char type;
    fint24 datalen;
    fint32 timestamp;
    fint24 streamsid;
}TagHeader;

typedef struct VIDEODATAPRE
{
    unsigned char FrameTypeAndCodecid;
    unsigned char AVCPacketType;
    fint24 CompositionTime;
}VideoData;

#pragma pack()

#define FINT16TOINT(x) ((x.Byte1<<8 & 0xff00) | (x.Byte2 & 0xff))
#define FINT24TOINT(x) ((x.Byte1<<16 & 0xff0000) | (x.Byte2<<8 & 0xff00) | (x.Byte3 & 0xff))
#define FINT32TOINT(x) ((x.Byte1<<24 & 0xff000000) | (x.Byte2<<16 & 0xff0000) | (x.Byte3<<8 & 0xff00) | (x.Byte4 & 0xff))

int main(int argc, char **argv)
{
int res = 0;
RTMP* rtmp = RTMP_Alloc();
RTMP_Init(rtmp);

res = RTMP_SetupURL(rtmp, "rtmp://127.0.0.1/live/testav");//推流地址
if (res == FALSE) {
printf("RTMP_SetupURL error.\n");
}
RTMP_EnableWrite(rtmp);//推流要设置写
res = RTMP_Connect(rtmp, NULL);
if (res == FALSE) {
printf("RTMP_Connect error.\n");
}
res = RTMP_ConnectStream(rtmp,0);
if (res == FALSE) {
printf("RTMP_ConnectStream error.\n");
}


//推流
FILE *fp_push=fopen("save.flv","rb");//本地用作推流的flv视频文件
FlvHeader flvheader;
fread(&flvheader, sizeof(flvheader), 1, fp_push);
int32_t preTagLen = 0;//前一个Tag长度
fread(&preTagLen, 4, 1, fp_push);
TagHeader tagHeader;
uint32_t begintime=RTMP_GetTime(),nowtime,pretimetamp = 0;

while (1)
{
fread(&tagHeader, sizeof(tagHeader), 1, fp_push);
if(tagHeader.type != 0x09)
{
int num = FINT24TOINT(tagHeader.datalen);
fseek(fp_push, FINT24TOINT(tagHeader.datalen)+4, SEEK_CUR);
continue;
}
fseek(fp_push, -sizeof(tagHeader), SEEK_CUR);
if((nowtime=RTMP_GetTime()-begintime)<pretimetamp)
{
printf("%d - %d\n", pretimetamp, nowtime);
usleep(1000 * (pretimetamp-nowtime));
continue;
}

char* pFileBuf=(char*)malloc(11+FINT24TOINT(tagHeader.datalen)+4);
memset(pFileBuf,0,11+FINT24TOINT(tagHeader.datalen)+4);
if(fread(pFileBuf,1,11+FINT24TOINT(tagHeader.datalen)+4,fp_push)!=11+FINT24TOINT(tagHeader.datalen)+4)
break;

if ((res = RTMP_Write(rtmp,pFileBuf,11+FINT24TOINT(tagHeader.datalen)+4)) <= 0)
{
printf("RTMP_Write end.\n");
break;
}
pretimetamp = FINT24TOINT(tagHeader.timestamp);

free(pFileBuf);
pFileBuf=NULL;
}
    return 0;
}

然后,我们需要编写Makefile编译工程,我们只需要使用librtmp中amf.c log.c parseurl.c rtmp.c hashswf.c这几个文件就好了:
下面是Makefile,对于需要修改的地方,都注释好了。根据自己的系统路径,做适当的修改。

CFLAGS=
#添加下面的编译参数,不使用ssl库 zlib等等
DFLAGS=-DNO_SSL -DNO_CRYPTO
LDFLAGS=


CC=gcc

BUILD_DIR=./build
OBJ_DIR=$(BUILD_DIR)/objs
# 修改为你下载下来的librtmp库的目录
SRC_DIR=../../rtmpdump/librtmp

# 修改为librtmp库的头文件目录
INC= \
-I../../rtmpdump/librtmp \
-I../../rtmpdump

SRC = \
amf.c \
log.c \
parseurl.c \
rtmp.c \
hashswf.c \
rtmp_push.c

vpath %.c $(SRC_DIR) ./


OBJS = $(notdir $(patsubst %c,%o,$(SRC)))

%.o:%.c | out
    @true "CC $<"
    $(CC) $(CFLAGS) $(DFLAGS) $(INC) -o $(addprefix $(OBJ_DIR)/,$@) -c $<

rtmp_push: $(OBJS)
    @true "TARGET rtmp_push"
    $(CC) -o rtmp_push $(addprefix $(OBJ_DIR)/,$(OBJS)) $(LDFLAGS)

.PHONY : clean
clean:
    rm -rf $(BUILD_DIR)
    rm -rf rtmp_push
    rm -rf *~
out:
    mkdir -p $(OBJ_DIR)

Makefile修改完成后,直接make就可以了。这样,我们的测试代码连同librtmp库就编译完成了。

3.2 测试librtmp库

首先,用ffmpeg工具把之前的mp4文件,转化为flv文件:

ffmpeg -i source.mp4 -c:v libx264 -crf 19 save.flv
./rtmp_push

然后,打开浏览器,输入 rtmp://127.0.0.1/live/testav 就可以看到我们推的rtmp流了。测试结束。

最后,附上相关文件:
Makefile
rtmp_push.c

4. 推h264裸流

一般地,客户会发一段h264裸流视频文件让云端验证前端播放器的兼容性问题。这就涉及到如何推h264裸流文件。
我们可以参考雷神的代码:

git clone https://github.com/leixiaohua1020/simplest_librtmp_example.git
cd simplest_librtmp_example/simplest_librtmp_send264/   # 这个是推送264的example代码

这个代码是在VS里面编译的工程,我们移植起来会不方便,所以,选择在linux下编译安装测试。这个代码主要的功能是解析h264文件,并且按照flv格式用RTMP推送视频流到服务器。
下面开始编译。

4.1 复制lei神代码

mkdir push_test && cd push_test/  # 新建一个工程文件夹 我们把需要的源码从git里面拷贝出来编译
cp ../simplest_librtmp_example/simplest_librtmp_send264/cuc_ieschool.h264 \
    ../simplest_librtmp_example/simplest_librtmp_send264/librtmp_send264.cpp \
    ../simplest_librtmp_example/simplest_librtmp_send264/librtmp_send264.h \
    ../simplest_librtmp_example/simplest_librtmp_send264/sps_decode.h \
    ../simplest_librtmp_example/simplest_librtmp_send264/simplest_librtmp_send264.cpp  .

4.2 编写Makefile

这里的Makefile和上面的类似,只是增加了两个cpp文件需要一起集成编译一下

CFLAGS=
#添加下面的编译参数,不使用ssl库 zlib等等
DFLAGS=-DNO_SSL -DNO_CRYPTO
LDFLAGS=



CC=gcc

BUILD_DIR=./build
OBJ_DIR=$(BUILD_DIR)/objs
# 修改为你下载下来的librtmp库的目录
SRC_DIR=../../../rtmpdump/librtmp

# 修改为librtmp库的头文件目录
INC= \
-I../../../rtmpdump/librtmp \
-I../../../rtmpdump

SRC = \
amf.c \
log.c \
parseurl.c \
rtmp.c \
hashswf.c

vpath %.c $(SRC_DIR) ./
vpath %.cpp $(SRC_DIR) ./


OBJS = $(notdir $(patsubst %c,%o,$(SRC))) simplest_librtmp_send264.o librtmp_send264.o

%.o:%.c | out
    @echo "CC $<"
    $(CC) $(CFLAGS) $(DFLAGS) $(INC) -o $(addprefix $(OBJ_DIR)/,$@) -c $<


rtmp_push: $(OBJS)
    @echo "TARGET rtmp_push"
    $(CC) -o rtmp_push $(addprefix $(OBJ_DIR)/,$(OBJS)) $(LDFLAGS)

simplest_librtmp_send264.o:simplest_librtmp_send264.cpp | out
    @echo "CC $<"
    $(CC) $(CFLAGS) $(DFLAGS) $(INC) -o $(addprefix $(OBJ_DIR)/,$@) -c $<

librtmp_send264.o:librtmp_send264.cpp | out
    @echo "CC $<"
    $(CC) $(CFLAGS) $(DFLAGS) $(INC) -o $(addprefix $(OBJ_DIR)/,$@) -c $<

.PHONY : clean
clean:
    rm -rf $(BUILD_DIR)
    rm -rf rtmp_push
    rm -rf *~
out:
    mkdir -p $(OBJ_DIR)

直接make一下,报错:

librtmp_send264.cpp:18:10: fatal error: 'librtmp\rtmp.h' file not found
#include "librtmp\rtmp.h"

因为是windows下的程序,路径中的反斜杠需要改成linux中的斜杠,修改完成,继续make,还是报错

gcc -o rtmp_push ./build/objs/amf.o ./build/objs/log.o ./build/objs/parseurl.o ./build/objs/rtmp.o ./build/objs/hashswf.o ./build/objs/simplest_librtmp_send264.o ./build/objs/librtmp_send264.o 
Undefined symbols for architecture x86_64:
  "operator delete[](void*)", referenced from:
      h264_decode_sps(unsigned char*, unsigned int, int&, int&, int&) in librtmp_send264.o
  "operator new[](unsigned long)", referenced from:
      h264_decode_sps(unsigned char*, unsigned int, int&, int&, int&) in librtmp_send264.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [rtmp_push] Error 1

因为是用c编译器,无法识别c++中的new delete等关键字,所以,我们还得修改代码。。定位到 sps_decode.h中176行位置,

            // 这几句话看起来没具体作用,直接注释掉
            // int *offset_for_ref_frame=new int[num_ref_frames_in_pic_order_cnt_cycle];
            // for( int i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++ )
            //     offset_for_ref_frame[i]=Se(buf,nLen,StartBit);
            // delete [] offset_for_ref_frame;

继续make,编译通过。

4.3 测试demo中的h264文件推流

修改 simplest_librtmp_send264.cpp 38行

// 这里,我们使用百度 lss 提供的RTMP推流地址
RTMP264_Connect("rtmp://push.ivc.gz.baidubce.com/xxx/test");

推流,发现程序在发完第一个relu之后就卡住了,发现是msleep的问题,修改 librtmp_send264.cpp 680行位置

tick +=tick_gap;
now=RTMP_GetTime();
msleep((int)(tick_gap-now+last_update));  // 这里需要用 int  强制类型转化,不然就会卡住。莫名其妙,不懂,求大佬指点。
//msleep(40);

这样修改之后,运行 ./rtmp_push 就可以推流了,在客户端使用ffplay播放:

用rtmp格式播放
ffplay "rtmp://rtmp.play.ivc.gz.baidubce.com/xxx/test?only-video=1"
或者用flv格式播放
ffplay "http://flv.play.ivc.gz.baidubce.com/xxx/test.flv?only-video=1"  # 必须加上 only-video=1 参数因为我们的264文件中只有视频 没有音频,默认情况下server回去做音/视频同步,导致30s左右延迟!
# 加上这个参数直接跳过“同步”的过程,差不多5s内开首屏。

4.4 测试客户h264文件

如果你测试客户发过来的h264文件,你会发现用上面的代码多半是跑不起来的。
雷神代码中默认是按照第一个帧是sps pps来解析的,这本身应该没有问题,因为客户手机一般也是在检测到第一个sps pps之后,才开始推流的。开头并不会出现“无用的”P帧数据。
但是,客户发过来的h264文件一般都是在开头夹杂着“无用的”P帧数据,所以用上面的代码肯定是不行的,我们要做的是把客户h264文件开头的P帧数据去掉,才开始用上面的代码推。
这就涉及到如何编辑二进制h264文件了。首先提供一个工具:
truncate_head_n.c

gcc truncate_head_n.c -o truncate_head_n
./truncate_head_n 1000  # 该命令会去掉当前目录下 命名为 temp的二进制文件的开始1000个字节

有了该工具,我们只需要找到264文件中第一个sps的偏移地址就可以了,可以直接用vim 查看

vim -b h264data.h264
:%!xxd

找到偏移,并且用工具去掉无用P帧之后,就可以用上面的demo推客户的流了,步骤就不赘述。
但是,我这里遇到一个很奇怪的问题,发现打开还是很慢,需要30s多。经过百度lss同学指点,说需要修改 librtmp_send264.cpp 中,只需要在开始时推一次sps pps,推流过程中,不再推sps pps,经过验证,在去掉推流中间过程的sps pps时候,首屏开启5s左右!那为何demo中的264文件在不改代码时也是没问题的呢?暂时没结果。

你可能感兴趣的:(流媒体)