基于ITOP4412开发板的实时视频监控系统实现

6 篇文章 1 订阅
2 篇文章 0 订阅

实时视频监控系统实现

设计一种RTMP嵌入式监控系统,该系统基于服务器/客户端模式,用户使用PC机或者使用移动设备通过网络实时监控观测对象。系统基于ITOP4412开发板,通过V4L2接口从摄像头采集YUV420格式的视频,通过X264对视频数据进行编码,然后通过RTMP协议发送至支持了rtmp的nginx流媒体服务器,然后客户端使用potplayer从服务器拉流显示。测试结果表明客户端播放器能够流畅地播放视频数据,系统稳定可靠。
V4L2----->X264----->RTMP------>nginx------>potplayer

一、开发环境

1、开发板硬件参数

itop4412 开发板
(1)计算能力
CPU采用EXYNOS4412,四核Cortex-A9架构,主频为1.4Ghz-1.6GHz;
(2)内存能力
采用1GB 双通道DDR3内存
(3)网络能力
使用DM9621网卡,百兆网卡
(4)IO能力
采用4GB EMMC

2、摄像头参数

OV5640摄像头 500W像素
输出格式,支持YUV420、YUV422、YUV444

3、操作系统

使用的是linux操作系统

二、相关技术简介

1、V4L2

V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写。
应用程序通过V4L2框架,对摄像头进行操作,如 设置摄像头的频率、图像参数、查看摄像头支持的配置等等;在使用V4L2框架采集摄像头时,主要步骤 1、打开设备;2、对设备进行配置;3、设置数据采集方式;4、处理数据;5、关闭设备。

2、RTMP

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四层模型的应用层。

3、H.264

H.264技术是目前在视频编码压缩中采用的最为普遍的一种技术。由于H.264拥有更低的码率,与MPEG2和MPEG4 ASP等压缩技术相比,在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济;并且它的容错能力也很强,网络适应能力非常好。
在本设计中,对于动态变化不明显的视频,H.264显示出了非常强大的压缩能力,使得网络带宽大大减少。本设计只是用了H.264的编码部分,使用目前最为流行,性能最好的X264作为编码器

4 、YUV图像

常用的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格式,具体区别见下图
在这里插入图片描述

5、nginx流媒体服务器

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有较大的延时。

子模块划分

1、数据采集模块

使用V4L2框架对摄像头进行操作,主要步骤其实就是1、打开摄像头、2、对摄像头进行配置,3、设置数据采集方式;例如使用mmap内存映射,4、对数据进行操作;5、关闭设备。
在这里,对数据的操作是,从视频输出队列获取到的数据直接交给H264编码器进行编码
在这里插入图片描述

2、数据压缩模块

这部分主要是对编码器的初始化,以及配置,因为实时监控需要的编码延迟要求较高,所以在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,以便区分
在这里插入图片描述

3、本地保存模块

本模块主要是将编码后的H264数据流写入到文件中去,根据H264码流依次写入分隔符(0x00 00 00 01) sps分隔符pps分隔符 I帧 分隔符P帧 分隔符 P帧也就是一个RBSP单元,然后在每一个I帧之前都需要有sps和pps;与网络传输的区别就是,在网络传输时添加了NAL单元
RBSP单元
在这里插入图片描述
网络传输添加NAL头
网络传输需要对H.264裸流进行封包

在下面RTMP封包时会介绍
在这里插入图片描述

4、RTMP协议推流模块

首先是对RTMP的初始化,如分配内存初始化设置,开启输出模式, 然后就是连接服务器,连接流。
接下来便是封包阶段这里封包采用如下结构,封包完毕之后便是推流阶段;
在这里插入图片描述
因为在解码时,客户端首先会根据sps、pps 信息获取视频参数,所以在每一个I帧之前都必须要有sps、pps数据,所以在发送I帧之前,将sps、pps包数据发送至服务器即可

在这里插入图片描述

四、源代码

在这里先贴出核心代码,其余等后续整理在发出来
1、数据采集

/*************************************************************************
    > File Name: camer.c
    > 作者:YJK 
    > Mail: 745506980@qq.com 
    > Created Time: 2020年11月18日 星期三 14时17分50秒
 ************************************************************************/

#include"./include/camer.h"
#include <linux/i2c.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include "include/rtmp_send.h"
#include "include/x264_encoder.h"
#include <sys/select.h>
#include<time.h>


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: 745506980@qq.com 
    > Created Time: 2020年11月27日 星期五 16时18分39秒
 ************************************************************************/
#include "include/x264_encoder.h"
#include <sys/types.h>
#include "include/rtmp_send.h"
#include "include/x264.h"
#include<stdio.h>
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: 745506980@qq.com 
    > Created Time: 2020年12月03日 星期四 15时02分31秒
 ************************************************************************/

#include<stdio.h>


#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

有什么问题欢迎大家讨论,共同进步

  • 16
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要将Linux系统烧写到itop4412开发板上,可以按照以下步骤进行操作: 1. 准备所需材料:一台运行Linux的主机、itop4412开发板、USB数据线、SD卡读卡器。 2. 下载Linux系统镜像:从官方网站或其他可靠来源下载适用于itop4412开发板的Linux系统镜像文件。 3. 将SD卡格式化:使用SD卡读卡器将SD卡连接到主机上,使用磁盘工具将其格式化为FAT32文件系统。 4. 将Linux系统镜像写入SD卡:使用磁盘工具将下载的Linux系统镜像写入SD卡。可以使用命令行工具如dd或者图形界面工具如Etcher来完成烧写操作。 5. 插入SD卡到itop4412开发板:将烧写好Linux系统的SD卡插入itop4412开发板的SD卡槽中。 6. 连接itop4412开发板到主机上:使用USB数据线将itop4412开发板连接到主机上。确保连接稳定并能被主机识别到。 7. 进入开发板的烧写模式:按下itop4412开发板上的烧写模式按钮,或者根据开发板的说明文档进行设置,使其进入烧写模式。 8. 在主机上进行烧写操作:使用烧写工具(如rkdeveloptool或者fastboot等)在主机上执行相应的烧写命令,将Linux系统镜像写入itop4412开发板的启动介质(一般是eMMC或者SD卡)。 9. 完成烧写:等待烧写完成,并确保没有报错信息出现。 10. 断开连接并启动itop4412开发板:断开连到主机的USB数据线。按下开发板上的重启按钮或者重新上电,itop4412开发板将会启动并运行烧写好的Linux系统。 尽管这些步骤可以作为一个参考,但为了确保顺利烧写Linux系统,建议查阅相关的官方文档和开发板的说明书,并根据具体情况进行操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值