这个文档详细介绍了,如何在本地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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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文件在不改代码时也是没问题的呢?暂时没结果。