设计一种RTMP嵌入式监控系统,该系统基于服务器/客户端模式,用户使用PC机或者使用移动设备通过网络实时监控观测对象。系统基于ITOP4412开发板,通过V4L2接口从摄像头采集YUV420格式的视频,通过X264对视频数据进行编码,然后通过RTMP协议发送至支持了rtmp的nginx流媒体服务器,然后客户端使用potplayer从服务器拉流显示。测试结果表明客户端播放器能够流畅地播放视频数据,系统稳定可靠。
V4L2----->X264----->RTMP------>nginx------>potplayer
itop4412 开发板
(1)计算能力
CPU采用EXYNOS4412,四核Cortex-A9架构,主频为1.4Ghz-1.6GHz;
(2)内存能力
采用1GB 双通道DDR3内存
(3)网络能力
使用DM9621网卡,百兆网卡
(4)IO能力
采用4GB EMMC
OV5640摄像头 500W像素
输出格式,支持YUV420、YUV422、YUV444
使用的是linux操作系统
V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写。
应用程序通过V4L2框架,对摄像头进行操作,如 设置摄像头的频率、图像参数、查看摄像头支持的配置等等;在使用V4L2框架采集摄像头时,主要步骤 1、打开设备;2、对设备进行配置;3、设置数据采集方式;4、处理数据;5、关闭设备。
RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议族,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。
H.264技术是目前在视频编码压缩中采用的最为普遍的一种技术。由于H.264拥有更低的码率,与MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济;并且它的容错能力也很强,网络适应能力非常好。
在本设计中,对于动态变化不明显的视频,H.264显示出了非常强大的压缩能力,使得网络带宽大大减少。本设计只是用了H.264的编码部分,使用目前最为流行,性能最好的X264作为编码器
常用的YUV元素图像格式有YUV422格式和YUV420格式。与RGB相比YUV422的大小是其2/3,而YUV420是其1/2。在YUV422格式中,按照U、V分量在时空上的排列顺序不同,可以将他们分为YUYV、YVYU、UYVY、VYUY四种不同的排列方式;在YUV420格式中,又分为I420(YU12)、YV12、NV12、NV21。
按照YUV的排列方式的不同又可以分为打包格式(packet)和平面格式(planner),对于packet格式 上面的YUYV就是典型的packet格式,具体区别见下图
nginx本身是一个非常出色的HTTP服务器,但是并不支持流媒体协议,但是可以通过使用开源的nginx-http-flv-module模块来支持,nginx-http-flv-module是在nginx-rtmp-module基础上开发的一个直播模块,解决了nginx-rtmp-module的一些bug,并且完美兼容,支持HTTP-FLV方式的直播,支持GOP缓存,以减少首屏等待时间,支持虚拟主机功能等等
本设计参考目前流行的流媒体解决方案,基于linux平台,利用V4L2(Video for linux2)框架,对OV5640摄像头进行操作,采集YUV420格式的数据,然后通过X264对每一帧YUV420数据进行编码,然后通过RTMP协议进行封包推流至nginx服务器。然后客户端使用vlc
、potplayer等播放器进行拉流,其中使用效果较好的是potplayer,由于vlc设有缓冲区相对于potplayer有较大的延时。
使用V4L2框架对摄像头进行操作,主要步骤其实就是1、打开摄像头、2、对摄像头进行配置,3、设置数据采集方式;例如使用mmap内存映射,4、对数据进行操作;5、关闭设备。
在这里,对数据的操作是,从视频输出队列获取到的数据直接交给H264编码器进行编码
这部分主要是对编码器的初始化,以及配置,因为实时监控需要的编码延迟要求较高,所以在x264_param_default_preset(&en->param, “ultrafast”, “zerolatency”); 中选择了
zerolatency配置选项,能够有效的降低编码延迟,然后就是在编码速度上选择了“ultrafast”,就是最快编码速度,这样的结果是降低了编码质量;因为itop4412开发板性能有限,在使用更高编码质量时,大大降低了帧率,使用veryfast时只有15fps;其次就是因为是实时监控要求实时性,不管用户什么时候拉流都能快速的解码出画面,这样就要求GOP序列的长度必须要短,在这里是30fps一个GOP序列也就是编码一个I帧,作为实时传输时,B帧的个数为0;然后就是设置比特率(码率),这里码率控制采用了平均码率,根据H264码流在每一个I帧前面都必须要有SPS、PPS序列,所以在配置完编码器后,便可直接解析出SPS
、PPS序列的信息将其保存到缓冲区,在发送时于I帧之前发送即可;
编码阶段本设计采用了数据量较小的YUV420格式,在编码之前需要将Y、U、V分量分离出来依次存入picture.img.plane[0]、picture.img.plane[1]、picture.img.plane[2]中然后进行编码即可,解析H264裸流,如果该帧被编码为I帧便将标志位置为1,以便区分
本模块主要是将编码后的H264数据流写入到文件中去,根据H264码流依次写入分隔符(0x00 00 00 01) sps分隔符pps分隔符 I帧 分隔符P帧 分隔符 P帧也就是一个RBSP单元,然后在每一个I帧之前都需要有sps和pps;与网络传输的区别就是,在网络传输时添加了NAL单元
RBSP单元
网络传输添加NAL头
网络传输需要对H.264裸流进行封包
首先是对RTMP的初始化,如分配内存初始化设置,开启输出模式, 然后就是连接服务器,连接流。
接下来便是封包阶段这里封包采用如下结构,封包完毕之后便是推流阶段;
因为在解码时,客户端首先会根据sps、pps 信息获取视频参数,所以在每一个I帧之前都必须要有sps、pps数据,所以在发送I帧之前,将sps、pps包数据发送至服务器即可
在这里先贴出核心代码,其余等后续整理在发出来
1、数据采集
/*************************************************************************
> File Name: camer.c
> 作者:YJK
> Mail: [email protected]
> Created Time: 2020年11月18日 星期三 14时17分50秒
************************************************************************/
#include"./include/camer.h"
#include
#include
#include
#include
#include "include/rtmp_send.h"
#include "include/x264_encoder.h"
#include
#include
int fd;
int file_fd;
int frame_size;
static Video_Buffer * buffer = NULL;
int ioctl_(int fd, int request, void *arg)
{
int ret = 0;
do{
ret = ioctl(fd, request, arg);
}while(ret == -1 && ret == EINTR);
return ret;
}
int open_device(const char * device_name)
{
struct stat st;
if( -1 == stat( device_name, &st ) )
{
printf( "Cannot identify '%s'\n" , device_name );
return -1;
}
if ( !S_ISCHR( st.st_mode ) )
{
printf( "%s is no device\n" , device_name );
return -1;
}
fd = open(device_name, O_RDWR | O_NONBLOCK , 0);
if ( -1 == fd )
{
printf( "Cannot open '%s'\n" , device_name );
return -1;
}
return 0;
}
int init_device(uint32_t pixformat)
{
//查询设备信息
struct v4l2_capability cap;
if (ioctl_(fd, VIDIOC_QUERYCAP, &cap) == -1)
{
perror("VIDIOC_QUERYCAP");
return -1;
}
printf("---------------------LINE:%d\n", __LINE__);
printf("DriverName:%s\nCard Name:%s\nBus info:%s\nDriverVersion:%u.%u.%u\n",
cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0xFF,(cap.version>>8)&0xFF,(cap.version)&0xFF);
//选择视频输入
struct v4l2_input input;
CLEAN(input);
input.index = 0;
if ( ioctl_(fd, VIDIOC_S_INPUT,&input) == -1){
printf("VIDIOC_S_INPUT IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
/*查看摄像头支持的视频格式*/
struct v4l2_fmtdesc fmtdesc;
// struct v4l2_frmsizeenum frmsize;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("fm:\n");
while(ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != -1){ //列举出所有支持的格式
printf("%d.%s %c%c%c%c\n", fmtdesc.index + 1, fmtdesc.description,
fmtdesc.pixelformat & 0xFF,
(fmtdesc.pixelformat >> 8) & 0xFF,
(fmtdesc.pixelformat >> 16) & 0xFF,
(fmtdesc.pixelformat >> 24) & 0xFF);
#if 0
frmsize.pixel_format = fmtdesc.pixelformat;
frmsize.index = 0;
while(ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) != -1){
printf("%dx%d\n",frmsize.discrete.width, frmsize.discrete.height);
frmsize.index++;
}
#endif
fmtdesc.index++;
}
/*查看摄像头支持的分辨率*/
//设置帧格式
struct v4l2_format fmt;
CLEAN(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
//视频格式
fmt.fmt.pix.pixelformat = pixformat;
// fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YVU420;
// fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl_(fd, VIDIOC_S_FMT, &fmt) == -1)
{
printf("VIDIOC_S_FMT IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
fmt.type = V4L2_BUF_TYPE_PRIVATE;
if (ioctl_(fd, VIDIOC_S_FMT, &fmt) == -1){
printf("VIDIOC_S_FMT IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
//查看帧格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if ( ioctl_(fd, VIDIOC_G_FMT, &fmt) == -1){
printf("VIDIOC_G_FMT IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
printf("width:%d\nheight:%d\npixelformat:%c%c%c%c field:%d\n",
fmt.fmt.pix.width, fmt.fmt.pix.height,
fmt.fmt.pix.pixelformat & 0xFF,
(fmt.fmt.pix.pixelformat >> 8) & 0xFF,
(fmt.fmt.pix.pixelformat >> 16) & 0xFF,
(fmt.fmt.pix.pixelformat >> 24) & 0xFF,
fmt.fmt.pix.field
);
#if 0
/*设置流相关 帧率*/
struct v4l2_streamparm parm;
CLEAN(parm);
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
parm.parm.capture.capability = V4L2_CAP_TIMEPERFRAME; //是否可以被timeperframe参数控制帧率
parm.parm.capture.timeperframe.denominator = 30; //时间间隔分母
parm.parm.capture.timeperframe.numerator = 1; //分子
if (ioctl_(fd, VIDIOC_S_PARM, &parm) == -1){
// printf("VIDIOC_S_PARM IS ERROR! \n");
perror("VIDIOC_S_PARM");
return -1;
}
if (ioctl_(fd, VIDIOC_G_PARM, (struct v4l2_streamparm*)&parm) == -1){
printf("VIDIOC_G_PARM IS ERROR! \n");
return -1;
}
#endif
#if 1
/*YUYV*/
__u32 min = fmt.fmt.pix.width * 2;
if ( fmt.fmt.pix.bytesperline < min )
fmt.fmt.pix.bytesperline = min;
/*YUV420*/
min = ( unsigned int )WIDTH * HEIGHT * 3 / 2;
if ( fmt.fmt.pix.sizeimage < min )
fmt.fmt.pix.sizeimage = min;
frame_size = fmt.fmt.pix.sizeimage;
printf("After Buggy driver paranoia\n");
printf(" >>fmt.fmt.pix.sizeimage = %d\n", fmt.fmt.pix.sizeimage);
printf(" >>fmt.fmt.pix.bytesperline = %d\n", fmt.fmt.pix.bytesperline);
printf("-#-#-#-#-#-#-#-#-#-#-#-#-#-\n");
printf("\n");
#endif
return 0;
}
int init_mmap()
{
//申请帧缓冲区
struct v4l2_requestbuffers req;
CLEAN(req);
req.count = 4;
req.memory = V4L2_MEMORY_MMAP; //使用内存映射缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//申请4个帧缓冲区,在内核空间中
if ( ioctl_(fd, VIDIOC_REQBUFS, &req) == -1 )
{
printf("VIDIOC_REQBUFS IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
//获取每个帧信息,并映射到用户空间
buffer = (Video_Buffer *)calloc(req.count, sizeof(Video_Buffer));
if (buffer == NULL){
printf("calloc is error! LINE:%d\n",__LINE__);
return -1;
}
struct v4l2_buffer buf;
int buf_index = 0;
for (buf_index = 0; buf_index < req.count; buf_index ++)
{
CLEAN(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.index = buf_index;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl_(fd, VIDIOC_QUERYBUF, &buf) == -1) //获取每个帧缓冲区的信息 如length和offset
{
printf("VIDIOC_QUERYBUF IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
//将内核空间中的帧缓冲区映射到用户空间
buffer[buf_index].length = buf.length;
buffer[buf_index].start = mmap(NULL, //由内核分配映射的起始地址
buf.length,//长度
PROT_READ | PROT_WRITE, //可读写
MAP_SHARED,//可共享
fd,
buf.m.offset);
if (buffer[buf_index].start == MAP_FAILED){
printf("MAP_FAILED LINE:%d\n",__LINE__);
return -1;
}
//将帧缓冲区放入视频输入队列
if (ioctl_(fd, VIDIOC_QBUF, &buf) == -1)
{
printf("VIDIOC_QBUF IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
printf("Frame buffer :%d address :0x%x length:%d\n",buf_index, (__u32)buffer[buf_index].start, buffer[buf_index].length);
}
return 0;
}
void start_stream()
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl_(fd, VIDIOC_STREAMON, &type) == -1){
fprintf(stderr, "VIDIOC_STREAMON IS ERROR! LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
}
void end_stream()
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl_(fd, VIDIOC_STREAMOFF, &type) == -1){
fprintf(stderr, "VIDIOC_STREAMOFF IS ERROR! LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
}
int read_frame(Encode *en, sps_pps_buf *buf_sp, uint32_t pixformat, uint32_t timer)
{
struct v4l2_buffer buf;
int ret = 0;
CLEAN(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl_(fd, VIDIOC_DQBUF, &buf) == -1){
printf("VIDIOC_DQBUF! LINEL:%d\n", __LINE__);
return -1;
}
// time_t start,end;
// start = time(NULL);
#if 0
ret = write(file_fd, buffer[0].start ,frame_size);
// printf("write:%d\n",frame_size);
#endif
#if 1
ret = Encode_frame(en, pixformat, file_fd, buf_sp, buffer[0].start, WIDTH, HEIGHT, timer);
if (ret == -1){
fprintf(stderr,"Encode_frame\n");
return -1;
}
#endif
// end = time(NULL);
// printf("time:%0.f\n",difftime(end, start));
if (ioctl_(fd, VIDIOC_QBUF, &buf) == -1){
printf("VIDIOC_QBUF! LINE:%d\n", __LINE__);
return -1;
}
return 0;
}
int open_file(const char * file_name)
{
file_fd = open(file_name, O_RDWR | O_CREAT, 0777);
if (file_fd == -1)
{
printf("open file is error! LINE:%d\n", __LINE__);
return -1;
}
return 0;
// file = fopen(file_name, "wr+");
}
void close_mmap()
{
int i = 0;
for (i = 0; i < 4 ; i++)
{
munmap(buffer[i].start, buffer[i].length);
}
free(buffer);
}
void close_device()
{
close(fd);
close(file_fd);
}
int process_frame(Encode * en, sps_pps_buf * buf, uint32_t pixformat, uint32_t timer)
{
struct timeval tvptr;
int ret;
tvptr.tv_usec = 0; //等待50 us
tvptr.tv_sec = 2;
fd_set fdread;
FD_ZERO(&fdread);
FD_SET(fd, &fdread);
ret = select(fd + 1, &fdread, NULL, NULL, &tvptr);
if (ret == -1){
perror("EXIT_FAILURE");
exit(EXIT_FAILURE);
}
if (ret == 0){
printf("timeout! \n");
}
// struct timeval start,end;
// gettimeofday(&start, NULL);
if(read_frame(en, buf, pixformat, timer) == -1)
{
fprintf(stderr, "readframe is error\n");
return -1;
}
// gettimeofday(&end, NULL);
// printf("time:%ldms\n",(end.tv_sec - start.tv_sec)*1000 + (end.tv_usec - start.tv_usec)/1000);
return 0;
}
2、x264编码
/*************************************************************************
> File Name: x264_encoder.c
> 作者:YJK
> Mail: [email protected]
> Created Time: 2020年11月27日 星期五 16时18分39秒
************************************************************************/
#include "include/x264_encoder.h"
#include
#include "include/rtmp_send.h"
#include "include/x264.h"
#include
int
Encode_init(struct encode *en, sps_pps *sp, uint32_t pixformat, uint32_t width, uint32_t height, uint32_t fps, uint32_t bitrate, int ConstantBitRate)
{
//初始化配置
x264_param_default(&en->param);
// zerolatency 选项是为了降低在线转码编码的延迟
// 缓存帧的个数为0 rc_lookahead = 0
// sync_lookhead 关闭线程预测 可减小延迟,但也会降低性能
// B 帧为0 实时视频需要
// sclied_threads = 1 基于分片的线程,默认off 开启该方法在压缩率和编码效率上都略低于默认方法,但是没有编码延迟。除非在编码实时流或者对地延迟要求较高的场合开启该方法
x264_param_default_preset(&en->param, "ultrafast", "zerolatency");
/*CPU FLAGS*/
// 线程设置 自动获取线程大小
en->param.i_sync_lookahead = X264_SYNC_LOOKAHEAD_AUTO;//自动获取线程超前缓冲区的大小
/*自动选择并行编码多帧*/
en->param.i_threads = X264_SYNC_LOOKAHEAD_AUTO; //取空缓冲区继续使用,不死锁的保证
// en->param.i_threads = 1;
/*VIDEO FLAGS*/
en->param.i_width = width;
en->param.i_height = height;
en->param.i_frame_total = 0; //如果已知要编码的帧数,否则为0
en->param.i_keyint_min = 0; //I帧的最小间隔
en->param.i_keyint_max = (int)fps * 2; //I帧的最大间隔 也就是最大2s一个I帧
en->param.b_annexb = 1; //为1将开始码0x0000 0001放在NAL单元之前
en->param.b_repeat_headers = 0; // 关键帧前面是否放SPS和PPS 0 否 1 放
//在RTMP协议中,SPS和PPS有单独的格式发送,所以这里不让关键帧前面放SPS和PPS
/* if (pixformat == V4L2_PIX_FMT_YUYV)
en->param.i_csp = X264_CSP_I422; //CSP 图像输入格式 YUYV YUV420
if (pixformat == V4L2_PIX_FMT_YUV420)*/
en->param.i_csp = X264_CSP_I420;
/*I帧间隔*/
en->param.i_fps_den = 1; //帧率分母
en->param.i_fps_num = fps; //帧率分子
en->param.i_timebase_num = (int)(fps * 1000 + .5);
en->param.i_timebase_den = 1000;
/*B帧设置*/
en->param.i_bframe = 0; //2幅参考图之间有多少个B帧 作为实时传输 B帧为0
en->param.i_bframe_pyramid = 0; // 保留一些B帧作为引用(参考) 0 off 不使用B帧作为参考帧 1 严格分层 2 正常的
en->param.b_open_gop = 0; //不使用open_gop 码流里面包含B帧的时候才会出现open_gop,一个GOP里面的某一帧在解码时要依赖于前一个GOP的某些帧,这个GOP就成为open-gop。有些解码器不能完全支持open-gop,所以默认是关闭的
en->param.i_bframe_adaptive = X264_B_ADAPT_FAST; //B帧的适应算法
/*log参数*/
// en->param.i_log_level = X264_LOG_DEBUG; //打印编码信息
/*速率控制参数*/
en->param.rc.i_bitrate = bitrate; //设置比特率 单位时间发送的比特数量 kbps
en->param.rc.i_lookahead = 0;
/*是否为恒定码率*/
if (!ConstantBitRate)
{
en->param.rc.i_rc_method = X264_RC_ABR; //码率控制,CQP(恒定质量), CRF(恒定码率),ABR(平均码率)
en->param.rc.i_vbv_max_bitrate = bitrate; //平均码率下,最大瞬时码率,默认0
en->param.rc.i_vbv_buffer_size = bitrate;
}
else{
en->param.rc.b_filler = 1; //设置为CBR模式
en->param.rc.i_rc_method = X264_RC_CRF; //恒定码率
en->param.rc.i_vbv_buffer_size = bitrate; //VBV Video Buffering Verifier 视频缓存检验器
en->param.rc.i_vbv_max_bitrate = bitrate; //平均码率模式下最大瞬时码率
}
/*根据参数初始化X264级别*/
en->handle = x264_encoder_open(&en->param);
/*初始化图片信息*/
x264_picture_init(&en->picture);
/*按YUYV格式分配空间 最后要x264_picture_clean*/
if (pixformat == V4L2_PIX_FMT_YUYV){
x264_picture_alloc(&en->picture, X264_CSP_I422, width, height);
}
if (pixformat == V4L2_PIX_FMT_YUV420){
x264_picture_alloc(&en->picture, X264_CSP_I420, width, height);
}
en->picture.i_pts = 0;
int pi_nal = 0;
/*en->nal 返回用于整个流的SPS、PPS 和SEI
*pi_nal 返回的是en->nal的单元数 3 SPS PPS SEI
*en->nal->i_payload 是p_payload有效负载大小也就是p_payload的长度 包含起始码0x00000001
*en->nal->p_payload 是里面存放的是SPS PPS SEI的数据 包含起始码0x00000001
* */
x264_encoder_headers(en->handle, &en->nal, &pi_nal);
if ( pi_nal > 0 )
{
int i = 0;
for(i = 0; i < pi_nal; i++)
{
if (en->nal[i].i_type == NAL_SPS) //SPS数据 0x67&1F
{
sp->sps = malloc(en->nal[i].i_payload - 4); //去掉起始码的四个字节
sp->sps_len = en->nal[i].i_payload - 4;
memcpy(sp->sps, en->nal[i].p_payload + 4, sp->sps_len);//跳过起始码
}
/*PPS*/
if (en->nal[i].i_type == NAL_PPS)
{
sp->pps = malloc(en->nal[i].i_payload - 4);
sp->pps_len = en->nal[i].i_payload - 4;
memcpy(sp->pps, en->nal[i].p_payload + 4, sp->pps_len);
}
}
return 1; //成功获取SPS 和 PPS
}
/*对获取SPS PPS*/
return 0;
}
int
Encode_frame(Encode *en, uint32_t pixformat, int fd, sps_pps_buf *buf, uint8_t *frame, uint32_t width, uint32_t height, uint32_t timer)
{
/*H264编码并发送*/
int num, i;
int index_y, index_u, index_v;
/*plane[0] 、plane[1] 、 plane[2] 分别存储Y、U、V分量*/
uint8_t * y = en->picture.img.plane[0];
uint8_t * u = en->picture.img.plane[1];
uint8_t * v = en->picture.img.plane[2];
uint8_t * frame_ = frame;
/*对YUYV图像YUV分量分离*/
if (pixformat == V4L2_PIX_FMT_YUYV)
{
index_y = 0;
index_u = 0;
index_v = 0;
num = (width * height)*2 - 4;
/*YUYV*/
for (i = 0; i < num; i = i + 4)
{
*(y + (index_y++)) = *(frame_ + i);
*(u + (index_u++)) = *(frame_ + i + 1);
*(y + (index_y++)) = *(frame_ + i + 2);
*(v + (index_v++)) = *(frame_ + i + 3);
}
}
/*YUV420*/
if (pixformat == V4L2_PIX_FMT_YUV420){
index_y = width * height;
index_u =index_y >> 2; //u v 分量长度一致
index_v = index_y + index_u;
/*摄像头采集的是YU12 Y = w*h u = w*h >> 2
*307200
*384000
*
*
* 460800
* */
memcpy(y, frame_, index_y); //分离Y分量
memcpy(u, frame_ + index_y, index_u);
memcpy(v, frame_ + index_v, index_u);
#if 0
frame_ = frame_ + index_y;
num = (index_y >> 1) - 2;
index_v = 0;
index_u = 0;
for (i = 0; i < num; i = i + 2)
{
*(u + index_v++) = *(frame_ + i);
*(v + index_u++) = *(frame_ + i + 1);
}
#endif
}
/*对图像进行编码*/
int pi_nal = 0;
x264_picture_t out_picture;
x264_picture_init(&out_picture);
int type = 0;
int ret = x264_encoder_encode(en->handle, &en->nal, &pi_nal, &en->picture, &out_picture);
if (ret < 0){
fprintf(stderr,"x264_encoder_encode is error\n");
return -1;
}
// en->picture.i_pts++;
/* 获取编码后的数据*/
if (pi_nal > 0)
{
for (i = 0; i < pi_nal; i++)
{
type = 0;
if (en->nal[i].i_type == NAL_SLICE || en->nal[i].i_type == NAL_SLICE_IDR)
{
if (en->nal[i].i_type == NAL_SLICE_IDR) //如果是关键帧
{
type = 1; //标记为关键帧
//写入文件
//I帧前面需要有sps + pps信息
ret = write(fd, buf->buf, buf->length); //写入sps pps
if (ret == -1){
fprintf(stderr, "write sps_pps_buf is error!\n");
return -1;
}
}
/*发送编码过后的数据*/
#if 1
int ret = Send_h264_packet(en->nal[i].p_payload + 4, en->nal[i].i_payload - 4, type, timer);
if (ret < 0){
fprintf(stderr,"Send_h264_packet is error\n");
return -1;
}
//写入文件
ret = write(fd, en->nal[i].p_payload, en->nal[i].i_payload);
if (ret == -1)
{
fprintf(stderr, "write frame is error !\n");
return -1;
}
#endif
}
}
}
return 0;
}
void Encode_end(x264_t *handle, x264_picture_t * picture, uint8_t *sps, uint8_t * pps)
{
/*释放内存*/
free(sps);
sps = NULL;
free(pps);
pps = NULL;
/*清空picture*/
x264_picture_clean(picture);
/*关闭编码*/
x264_encoder_close(handle);
}
3、RTMP推流和写入文件
/*************************************************************************
> File Name: rtmp_send.c
> 作者:YJK
> Mail: [email protected]
> Created Time: 2020年12月03日 星期四 15时02分31秒
************************************************************************/
#include
#include "include/librtmp/amf.h"
#include"include/rtmp_send.h"
#include "include/librtmp/rtmp.h"
#define RTMP_HEADER_SIZE (sizeof(RTMPPacket) + RTMP_MAX_HEADER_SIZE)
int
Rtmp_Begin(char * URL)
{
int ret = 0;
//申请内存-RTMP
rtmp = RTMP_Alloc();
if (rtmp == NULL){
perror("RTMP_Alloc");
return -1;
}
/*初始化*/
RTMP_Init(rtmp);
/*设置地址*/
ret = RTMP_SetupURL(rtmp, URL);
if (ret == FALSE){
perror("RTMP_SetupURL");
return -1;
}
/*开启输出模式*/
RTMP_EnableWrite(rtmp);
/*连接服务器*/
ret = RTMP_Connect(rtmp, NULL);
if (ret == FALSE){
perror("RTMP_Connect");
return -1;
}
/*连接流*/
ret = RTMP_ConnectStream(rtmp, 0);
if (ret == FALSE){
perror("RTMP_ConnectStream");
return -1;
}
return 0;
}
/*SPS PPS*/
RTMPPacket * Create_sps_packet(sps_pps *sp)
{
/*分配packet空间*/
RTMPPacket * packet = (RTMPPacket *)malloc(RTMP_HEADER_SIZE + 512);
if (packet == NULL) return NULL;
packet->m_body = (char *)packet + RTMP_HEADER_SIZE;
/*填充视频包数据*/
int i = 0;
packet->m_body[i++] = 0x17;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x01; //版本号
packet->m_body[i++] = sp->sps[1]; //配置信息 baseline 宽高
packet->m_body[i++] = sp->sps[2]; //兼容性
packet->m_body[i++] = sp->sps[3]; //profile_level
packet->m_body[i++] = 0xFF; //几个字节表示NALU的长度 0xFF & 3 + 1 4个字节
packet->m_body[i++] = 0xE1; //SPS个数 0xE1 & 0x1F = 1
/*sps 长度 2个字节*/ //网络中采用大端模式
packet->m_body[i++] = (sp->sps_len >> 8) & 0xFF; //低8位
packet->m_body[i++] = sp->sps_len & 0xFF; //高8位
//sps数据
memcpy(&packet->m_body[i], sp->sps, sp->sps_len);
i += sp->sps_len;
//pps个数
packet->m_body[i++] = 0x01;
//整个pps长度
packet->m_body[i++] = (sp->pps_len >> 8) & 0xFF;
packet->m_body[i++] = sp->pps_len & 0xFF;
//pps数据
memcpy(&packet->m_body[i], sp->pps, sp->pps_len);
i += sp->pps_len;
//packet配置
/*massage type id (1~7)控制协议8,9音视频,10以后AMF编码消息*/
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; //视频格式
//数据长度
packet->m_nBodySize = i;
//块流ID
packet->m_nChannel = 0x04; //Audio 和 video的通道
packet->m_nTimeStamp = 0; //绝对时间戳
packet->m_hasAbsTimestamp = 0; //相对时间戳
packet->m_headerType = RTMP_PACKET_SIZE_LARGE; //ChunkMsgHeader的类型(4种)
packet->m_nInfoField2 = rtmp->m_stream_id; //消息流ID
return packet;
}
int Send_h264_packet(uint8_t * H264_Stream, int length, int type, uint32_t timer)
{
RTMPPacket *packet = (RTMPPacket *)malloc(RTMP_HEADER_SIZE + length + 9);
if (packet == NULL){
fprintf(stderr, "packet malloc is error!\n");
return -1;
}
int i = 0;
packet->m_body = (char *)packet + RTMP_HEADER_SIZE;
int ret = 0;
if (type == 1) //关键帧
{
packet->m_body[i++] = 0x17;
packet->m_body[i++] = 0x01;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
//NALUSIZE 数据长度
packet->m_body[i++] = (length >> 24) & 0xFF;
packet->m_body[i++] = (length >> 16) & 0xFF;
packet->m_body[i++] = (length >> 8) & 0xFF;
packet->m_body[i++] = length & 0xFF;
memcpy(&packet->m_body[i], H264_Stream, length);
i += length;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
//数据长度
packet->m_nBodySize = i;
packet->m_hasAbsTimestamp = 0;
packet->m_nTimeStamp = timer;
packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_nInfoField2 = rtmp->m_stream_id;
//SPS PPS 包
ret = RTMP_SendPacket(rtmp, packet_sp, FALSE); //TRUE 放进发送队列
if (RTMP_IsConnected(rtmp))
{
ret = RTMP_SendPacket(rtmp, packet, FALSE);
}
}
if (type == 0)
{
packet->m_body[i++] = 0x27;
packet->m_body[i++] = 0x01;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = (length >> 24) & 0xFF;
packet->m_body[i++] = (length >> 16) & 0xFF;
packet->m_body[i++] = (length >> 8) & 0xFF;
packet->m_body[i++] = length & 0xFF;
memcpy(&packet->m_body[i], H264_Stream, length);
i += length;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = i;
packet->m_nTimeStamp = timer;
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
packet->m_nInfoField2 = rtmp->m_stream_id;
ret = RTMP_SendPacket(rtmp, packet, FALSE);
}
free(packet);
return ret;
}
void RTMP_END()
{
RTMP_Close(rtmp);
RTMP_Free(rtmp);
free(packet_sp);
}
服务器使用的是添加了nginx-http-flv-module模块的nginx,播放器使用PotPlayer或者VLC都可以,不过VLC默认是有缓冲区的所以看着延时会比较高大概2~3s的样子,但是使用PotPlayer的话延时在1s以内(局域网),
首屏进入的话
PotPlayer 10s左右
VLC 5s左右
测试的时候发现推流4.66小时的时候会断开连接
原因是 RTMP协议在处理扩展时间戳的时候有个bug,也就是只用了3个字节大小来存储时间戳,当时间戳超过0FFFFFF时,应该使用扩展时间戳的也就是最大为0xFFFFFFFF
在这里我更新了RTMP的版本之前使用的是2.3 现在使用2.4已经不存在上述问题
目前摄像头已经跑了24小时,这是今天早上八点连接的一直没有中断过;
理论上时间戳的最大值是0xFFFFFFFF,也就是可以用49天
目前项目已开源至 github
https://github.com/745506980/V4L2-to-Rtmp.git
有什么问题欢迎大家讨论,共同进步