一、音视频推流框架概述
1.什么是推流
转载:原文链接:【知识拓展】音视频中的推流与拉流_推流拉流_No8g攻城狮的博客-CSDN博客
推流:把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号传到网络的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观众观看直播时就会发生卡顿等现象,观看体验很是糟糕。
要想用于推流还必须把音视频数据使用传输协议进行封装,变成流数据。常用的流传输协议有RTSP、RTMP、HLS等,使用RTMP传输的延时通常在1–3秒,对于手机直播这种实时性要求非常高的场景,RTMP也成为手机直播中最常用的流传输协议。最后通过一定的Qos算法将音视频流数据推送到网络断,通过CDN进行分发。
在直播场景中,网络不稳定是非常常见的,这时就需要Qos来保证网络不稳情况下的用户观看直播的体验,通常是通过主播端和播放端设置缓存,让码率均匀。另外,针对实时变化的网络状况,动态码率和帧率也是最常用的策略。
直播中使用广泛的“推流协议”一般是RTMP(Real Time Messaging Protocol——实时消息传输协议)。该协议是一个基于TCP的协议族,是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。
2.什么是拉流
拉流是指服务器已有直播内容,根据协议类型(如RTMP、RTP、RTSP、HTTP等),与服务器建立连接并接收数据,进行拉取的过程。拉流端的核心处理在播放器端的解码和渲染,在互动直播中还需集成聊天室、点赞和礼物系统等功能。
拉流端现在支持RTMP、HLS、HDL(HTTP-FLV)三种协议,其中,在网络稳定的情况下,对于HDL协议的延时控制可达1s,完全满足互动直播的业务需求。RTMP是Adobe的专利协议,开源软件和开源库都支持的比较好,延时一般在1-3秒。HLS是苹果提出的基于HTTP的流媒体传输协议,优先是跨平台性比较好,HTML5可以直接打开播放,移动端兼容性良好,但是缺点是延迟比较高。注意,切片越多,关键帧量就越多,需要内存就越大,传输就慢。
3.编码解码原理
3.1为啥压缩
在推流和拉流过程中,都要根据协议对源数据进行编码或者解码,以压缩数据便于传输。
3.2方法
- 空间冗余:图像相邻像素之间有较强的相关性
- 时间冗余:视频序列的相邻图像之间内容相似
- 编码冗余:不同像素值出现的概率不同
- 视觉冗余:人的视觉系统对某些细节不敏感
- 知识冗余:规律性的结构可由先验知识和背景知识得到
3.3压缩分类
无损压缩(Lossless):压缩前、解压缩后图像完全一致X=X',压缩比低(2:1~3:1)。典型格式例如:Winzip,JPEG-LS。
有损压缩(Lossy):压缩前解压缩后图像不一致X≠X',压缩比高(10:1~20:1),利用人的视觉系统的特性。典型格式例如:MPEG-2,H.264/AVC,AVS
3.4编码步骤
- 预测:对一幅图像中的每个块,根据某几个相邻的像素值,在指定的方向上对下一个像素点值用公式做预测。作用:获取差值,差值矩阵存储内存较小(包含信息能量低),为了到达编码阶段,编码压缩的数据量更小,压缩效率高。
方法:a.帧内预测:一幅图像;
b.帧间预测:运动轨迹预测;
一些概念:
- 关键帧I:用来做预测的帧,信息量大,后续获取的图像都是基于关键帧预测的(原始图像的残差)
- 预测帧(P):根据前一幅图像预测得到的本帧图像。
- B帧:根据前面的图像和后的图像进行双向预测得到的本帧图像。
- GOP:I+nB+nP
- CDN:内容分发网络(分布式网络架构),为了提高内容传输速率和效率,采用类似就近原则。
- 变换
DCT变换(离散余弦变换):将低频的、幅值高的信重要息放在矩阵左上角,反之放在右下角,即对频率做划分。改变队形为后续编码做准备。
- 量化
根据步长对数据进行分级,这样右下侧较小的数据都被归为0。(缺点:使数据失真。)
再对数据矩阵从左上角进行“Z”字扫描,方便把非0数据放一起。
- 熵编码
第一步:行程编码:把重复的数据用数据加重复次数表示。例如aabbcc->a2b2c2
第二步:变长编码:如霍夫曼编码,将出现频率高的字符串用最短的码来替换,从整体上减少了原始数据长度。
解码顺序就是编码的逆序。
二、视频监控项目
将使用 RTMP 流媒体服务来实现视频监控
推流端负责将视频数据通过 RTMP 流媒体协议传输给 RTMP 流媒体服务器,拉流端可以从流媒体服务
器中通过 RTMP 协议获取到视频数据;而流媒体服务器负责接收推流端的视频数据、当有客户端(拉流端)想要获取视频数据时再将其发给相应的客户端。
推流我们可以使用 FFmpeg 来做,拉流则可以实现 VLC 播放器来做,而流媒体客户端则使用 Nginx 来搭建。
方案:使用usb摄像头采集数据,然后搭建nginx服务器,用FFmpeg对数据进行推流,最后在Windows端使用VLC播放器拉流。
1.usb摄像头获取数据
1.V4L2 摄像头应用编程概述
V4L2 是 Video for linux two 的简称,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范。
使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X 标准一个数字编号,0、1、2、3……),每一个 videoX 设备文件就代表一个视频类设备。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备,
2. V4L2 摄像头应用程序步骤
1. 首先是打开摄像头设备;
2. 查询设备的属性或功能;
3. 设置设备的参数,譬如像素格式、帧大小、帧率;
4. 申请帧缓冲、内存映射;
5. 帧缓冲入队;
6. 开启视频采集;
7. 帧缓冲出队、对采集的数据进行处理;
8. 处理完后,再次将帧缓冲入队,往复;
9. 结束采集
摄像头视频采集流程图
从图上可以看出几乎对摄像头的所有操作都是通过 ioctl()来完成,搭配不同的 V4L2 指令(request参数)请求不同的操作,这些指令定义在头文件linux/videodev2.h 中,在摄像头应用程序代码中,需要包含头文件 linux/videodev2.h,该头文件中申明了很多与摄像头应用编程相关的数据结构以及宏定义。
每一个 不 同 的 指 令 宏 就 表 示 向 设 备 请 求 不 同 的 操 作 , 从 上 面 可 以 看 到 , 每 一 个 宏 后 面(_IOWR/_IOR/_IOW)还携带了一个 struct 数据结构体,譬如 struct v4l2_capability、struct v4l2_fmtdesc,这就是调用 ioctl()时需要传入的第三个参数的类型;调用 ioctl()前,定义一个该类型变量,调用 ioctl()时、将变量的指针作为 ioctl()的第三个参数传入。
常见的iotcl指令如下:
eg:
struct v4l2_capability cap;
……
ioctl(fd, VIDIOC_QUERYCAP, &cap);
2.1.打开摄像头设备
视频类设备对应的设备节点为/dev/videoX,X 为数字编号,通常从 0 开始;摄像头应用编程的第一步便
是打开设备,调用 open 打开,得到文件描述符 fd;
int fd = -1;
/* 打开摄像头 */
fd = open("/dev/video0", O_RDWR);
if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
return -1;
}
2.2查询设备的属性或功能
打开设备之后,接着需要查询设备的属性,确定该设备是否是一个视频采集类设备、以及其它一些属性
ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *cap);
此ioctl将获取到一个struct v4l2_capability类型数据,该数据结构描述了设备的一些属性,其中capabilities字段描述了设备拥有的能力
struct v4l2_capability {
__u8 driver[16]; /* 驱动的名字 */
__u8 card[32]; /* 设备的名字 */
__u8 bus_info[32]; /* 总线的名字 */
__u32 version; /* 版本信息 */
__u32 capabilities; /* 设备拥有的能力 */
__u32 device_caps;
__u32 reserved[3]; /* 保留字段 */
};
V4L2_CAP_VIDEO_CAPTURE字段内容
#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */
对于摄像头设备来说,它的capabilities字段必须包含 V4L2_CAP_VIDEO_CAPTURE,表示它支持视频采集功能。所以我们可以通过判断 capabilities字段是否包含 V4L2_CAP_VIDEO_CAPTURE、来确定它是否是一个摄像头设备;
/* 查询设备功能 */
ioctl(fd, VIDIOC_QUERYCAP, &cap);
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE &cap.capabilities)) {
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
2.3 设置设备的参数,譬如像素格式、帧大小、帧率;
一个摄像头通常会支持多种不同的像素格式,譬如 RGB、YUYV 以及压缩格式 MJPEG 等,并且还支
持多种不同的视频采集分辨率,譬如 640*480、320*240、1280*720 等,除此之外,同一分辨率可能还支持多种不同的视频采集帧率(15fps、30fps)。
摄像头支持像素格式:VIDIOC_ENUM_FMT
ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);
摄像头支持分辨率:VIDIOC_ENUM_FRAMESIZES
ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);
摄像头采集帧率:VIDIOC_ENUM_FRAMEINTERVALS
ioctl(int fd, VIDIOC_ENUM_FRAMEINTERVALS, struct v4l2_frmivalenum *frmival);
查看或设置当前的格式:VIDIOC_G_FMT、VIDIOC_S_FMT
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);
设置或获取当前的流类型相关参数:VIDIOC_G_PARM、VIDIOC_S_PARM
ioctl(int fd, VIDIOC_S_PARM, struct v4l2_streamparm *streamparm);
2.4申请帧缓冲、内存映射
使用streaming I/O 方式(高级I/O)来读取摄像头数据。
申请帧缓冲 struct v4l2_requestbuffers *reqbuf
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);
streaming I/O 方式会在内核空间中维护一个帧缓冲队列,驱动程序会将从摄像头读取的一帧数据写入到
队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队
将帧缓冲映射到进程地址空间 struct v4l2_buffer *buf
使用VIDIOC_QUERYBUF查询的长度、偏移量等信息
ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);
调用 mmap()将帧缓冲映射到用户地址空间。
2.5入队
使用 VIDIOC_QBUF 指令将帧缓冲放入到内核的帧缓冲队列中
ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *buf);
2.6开启视频采集
使用 VIDIOC_DQBUF 指令开启视频采集
ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集
2.7读取数据、对数据进行处理
接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据,需要将帧缓冲从内核的帧缓冲队列中取出,这个操作叫做帧缓冲出队。使用 VIDIOC_DQBUF 指令执行出队操作:
ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);
3.实验代码
下面是我所使用的usb摄像头实例,该摄像头支持YUYV格式,但不支持RGB格式,而使用的LCD屏幕为7 寸 800*480 RGB 屏,像素深度为 16(一个像素点的颜色值将使用 16bit(也就是 2 个字节)来表示),所以需要对采集的数据做格式转换才能显示到LCD上。
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : v4l2_camera.c
作者 : 邓涛
版本 : V1.0
描述 : V4L2摄像头应用编程实战
其他 : 东改
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/7/09 邓涛创建
***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#define FB_DEV "/dev/fb0" // LCD设备节点
#define FRAMEBUFFER_COUNT 3 //帧缓冲数量
/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format
{
unsigned char description[32]; //字符串描述信息
unsigned int pixelformat; //像素格式
} cam_fmt;
/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info
{
unsigned short *start; //帧缓冲起始地址
unsigned long length; //帧缓冲长度
} cam_buf_info;
static int width; // LCD宽度
static int height; // LCD高度
static unsigned short *screen_base = NULL; // LCD显存基地址
static int fb_fd = -1; // LCD设备文件描述符
static int v4l2_fd = -1; //摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height; //视频帧宽度和高度
static int fb_dev_init(void)
{
struct fb_var_screeninfo fb_var = {0};
struct fb_fix_screeninfo fb_fix = {0};
unsigned long screen_size;
/* 打开framebuffer设备 */
fb_fd = open(FB_DEV, O_RDWR);
if (0 > fb_fd)
{
fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
return -1;
}
/* 获取framebuffer设备信息 */
ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 内存映射 */
screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (MAP_FAILED == (void *)screen_base)
{
perror("mmap error");
close(fb_fd);
return -1;
}
/* LCD背景刷白 */
memset(screen_base, 0xFF, screen_size);
return 0;
}
static int v4l2_dev_init(const char *device)
{
struct v4l2_capability cap = {0};
/* 打开摄像头 */
v4l2_fd = open(device, O_RDWR);
if (0 > v4l2_fd)
{
fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
return -1;
}
/* 查询设备功能 */
ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities))
{
fprintf(stderr, "Error: %s: No capture video device!\n", device);
close(v4l2_fd);
return -1;
}
return 0;
}
static void v4l2_enum_formats(void)
{
struct v4l2_fmtdesc fmtdesc = {0};
/* 枚举摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc))
{
// 将枚举出来的格式以及描述信息存放在数组中
cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
fmtdesc.index++;
}
}
static void v4l2_print_formats(void)
{
struct v4l2_frmsizeenum frmsize = {0};
struct v4l2_frmivalenum frmival = {0};
int i;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for (i = 0; cam_fmts[i].pixelformat; i++)
{
printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
cam_fmts[i].description);
/* 枚举出摄像头所支持的所有视频采集分辨率 */
frmsize.index = 0;
frmsize.pixel_format = cam_fmts[i].pixelformat;
frmival.pixel_format = cam_fmts[i].pixelformat;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize))
{
printf("size<%d*%d> ",
frmsize.discrete.width,
frmsize.discrete.height);
frmsize.index++;
/* 获取摄像头视频采集帧率 */
frmival.index = 0;
frmival.width = frmsize.discrete.width;
frmival.height = frmsize.discrete.height;
while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival))
{
printf("<%dfps>", frmival.discrete.denominator /
frmival.discrete.numerator);
frmival.index++;
}
printf("\n");
}
printf("\n");
}
}
static int v4l2_set_format(void)
{
struct v4l2_format fmt = {0};
struct v4l2_streamparm streamparm = {0};
/* 设置帧格式 */
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // type类型
fmt.fmt.pix.width = width; //视频帧宽度
fmt.fmt.pix.height = height; //视频帧高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
// V4L2_PIX_FMT_RGB565; //像素格式
if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt))
{
fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
return -1;
}
/*** 判断是否已经设置为我们要求的YUYV像素格式
如果没有设置成功表示该设备不支持YUYV像素格式 */
if (V4L2_PIX_FMT_YUYV != fmt.fmt.pix.pixelformat)
{
fprintf(stderr, "Error: the device does not support RGB565 format!\n");
return -1;
}
else
{
printf("像素格式V4L2_PIX_FMT_YUYV");
}
frm_width = fmt.fmt.pix.width; //获取实际的帧宽度
frm_height = fmt.fmt.pix.height; //获取实际的帧高度
printf("视频帧大小<%d * %d>\n", frm_width, frm_height);
/* 获取streamparm */
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability)
{
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 30; // 30fps
if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
{
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
static int v4l2_init_buffer(void)
{
struct v4l2_requestbuffers reqbuf = {0};
struct v4l2_buffer buf = {0};
/* 申请帧缓冲 */
reqbuf.count = FRAMEBUFFER_COUNT; //帧缓冲的数量
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf))
{
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}
/* 建立内存映射 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
{
ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
buf_infos[buf.index].length = buf.length;
buf_infos[buf.index].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
v4l2_fd, buf.m.offset);
if (MAP_FAILED == buf_infos[buf.index].start)
{
perror("mmap error");
return -1;
}
}
/* 入队 */
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
{
if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf))
{
fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
static int v4l2_stream_on(void)
{
/* 打开摄像头、摄像头开始采集数据 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type))
{
fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
return -1;
}
return 0;
}
// framebuffer:一帧数据 长度min_w*2
#define WIDTH 640 //摄像头拍摄以及显示的大小
#define HEIGHT 480
void yuyv_to_rgb(unsigned char* yuv,unsigned char* rgb565)
{
unsigned int i;
unsigned char* y0 = yuv + 0;
unsigned char* u0 = yuv + 1;
unsigned char* y1 = yuv + 2;
unsigned char* v0 = yuv + 3;
unsigned short* rgb0 = (unsigned short*)rgb565;
unsigned short* rgb1 = (unsigned short*)(rgb565 + 2);
float rt0 = 0, gt0 = 0, bt0 = 0, rt1 = 0, gt1 = 0, bt1 = 0;
for (i = 0; i <= (WIDTH * HEIGHT) / 2; i++) {
bt0 = 1.164 * (*y0 - 16) + 2.018 * (*u0 - 128);
gt0 = 1.164 * (*y0 - 16) - 0.813 * (*v0 - 128) - 0.394 * (*u0 - 128);
rt0 = 1.164 * (*y0 - 16) + 1.596 * (*v0 - 128);
bt1 = 1.164 * (*y1 - 16) + 2.018 * (*u0 - 128);
gt1 = 1.164 * (*y1 - 16) - 0.813 * (*v0 - 128) - 0.394 * (*u0 - 128);
rt1 = 1.164 * (*y1 - 16) + 1.596 * (*v0 - 128);
if (rt0 > 31) rt0 = 31;
if (rt0 < 0) rt0 = 0;
if (gt0 > 63) gt0 = 63;
if (gt0 < 0) gt0 = 0;
if (bt0 > 31) bt0 = 31;
if (bt0 < 0) bt0 = 0;
if (rt1 > 31) rt1 = 31;
if (rt1 < 0) rt1 = 0;
if (gt1 > 63) gt1 = 63;
if (gt1 < 0) gt1 = 0;
if (bt1 > 31) bt1 = 31;
if (bt1 < 0) bt1 = 0;
*rgb0 = ((unsigned short)rt0 << 11) | ((unsigned short)gt0 << 5) | (unsigned short)bt0;
*rgb1 = ((unsigned short)rt1 << 11) | ((unsigned short)gt1 << 5) | (unsigned short)bt1;
yuv = yuv + 4;
rgb565 = rgb565 + 4;
if (yuv == NULL)
break;
y0 = yuv;
u0 = yuv + 1;
y1 = yuv + 2;
v0 = yuv + 3;
rgb0 = (unsigned short*)rgb565;
rgb1 = (unsigned short*)(rgb565 + 2);
}
}
void lcd_show_rgb(unsigned char *rgbdata, int w ,int h)
{
unsigned short *ptr;
ptr = screen_base; //lcd基地址操作
for(int i = 0; i <h; i++) {
for(int j = 0; j < w; j++) {
memcpy(ptr+j,rgbdata+j*2,2);//rgb565用3个字节表示一个像素点
}
ptr += width;
rgbdata += w*2;
}
}
static void v4l2_read_data(void)
{
struct v4l2_buffer buf = {0};
unsigned short *base;
unsigned short *start;
int min_w, min_h;
int j;
//定义一个空间存储解码后的rgb
unsigned char rgbdata[WIDTH * HEIGHT * 3];
if (width > frm_width)
min_w = frm_width;
else
min_w = width;
if (height > frm_height)
min_h = frm_height;
else
min_h = height;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (;;)
{
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++)
{
ioctl(v4l2_fd, VIDIOC_DQBUF, &buf); //出队
yuyv_to_rgb((unsigned char*)buf_infos[buf.index].start,rgbdata);
lcd_show_rgb(rgbdata,640,480);
// 数据处理完之后、再入队、往复
ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
}
}
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* 初始化LCD */
if (fb_dev_init())
exit(EXIT_FAILURE);
/* 初始化摄像头 */
if (v4l2_dev_init(argv[1]))
exit(EXIT_FAILURE);
/* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
v4l2_enum_formats();
v4l2_print_formats();
/* 设置格式 */
if (v4l2_set_format())
exit(EXIT_FAILURE);
/* 初始化帧缓冲:申请、内存映射、入队 */
if (v4l2_init_buffer())
exit(EXIT_FAILURE);
/* 开启视频采集 */
if (v4l2_stream_on())
exit(EXIT_FAILURE);
/* 读取数据:出队 */
v4l2_read_data(); //在函数内循环采集数据、将其显示到LCD屏
exit(EXIT_SUCCESS);
}
4.问题
摄像头格式转换
补充:
YUYV格式
YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度(Luminance、Luma),而 U 和 V 表示色度(Chrominance、Chroma)。
RGB 到 YUV 的转换
就是将图像所有像素点的 R、G、B 分量转换到 Y、U、V 分量
公式:
YUV 采样格式
YUV 图像的主流采样方式有如下三种:
YUV 4:4:4 采样
YUV 4:2:2 采样
YUV 4:2:0 采样
其中,我们采集的格式为4:2:2
YUV 4:2:2 采样,意味着 UV 分量是 Y 分量采样的一半,Y 分量和 UV 分量按照 2 : 1 的比例采样。如果水平方向有 10 个像素点,那么采样了 10 个 Y 分量,而只采样了 5 个 UV 分量。
举个例子 :
假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
那么采样的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。
最后映射出的像素点为 [Y0 U0 V1]、[Y1 U0 V1]、[Y2 U2 V3]、[Y3 U2 V3]
一张 1280 * 720 大小的图片(像素深度8位),在 YUV 4:2:2 采样时的大小为:
(1280 * 720 * 8 + 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB
在YUV 4:2:2采样中,每个像素的亮度(Y)分量保持完整,而色度(U和V)分量则以2:1的水平采样率进行采样。
对于一张1280x720大小的图片,在YUV 4:2:2采样下,计算大小的方法如下:
计算Y分量的大小:
Y分量的大小等于图像的宽度乘以高度乘以每个像素的位数(通常为8位)。
Y大小 = 1280 * 720 * 8 = 7372800 bits
计算U和V分量的大小:
U和V分量的水平采样率为2:1,即每2个像素共用一组U和V分量数据。
首先计算每行中共享的U和V分量的数量,即图像的宽度除以2。
共享的U和V分量数量 = 1280 / 2 = 640
然后计算每个共享的U和V分量的大小,即每个像素的位数乘以2。
每个共享的U和V分量大小 = 8 * 2 = 16 bits
最后计算U和V分量的总大小,即共享的U和V分量的数量乘以每个共享的U和V分量的大小,再乘以图像的高度。
U和V大小 = 640 * 16 * 720 = 7372800 bits
计算总大小:
总大小等于Y分量的大小加上U和V分量的大小。
总大小 = Y大小 + U和V大小 = 7372800 + 7372800 = 14745600 bits
将总大小转换为字节:
总大小(字节)= 总大小(bits)/ 8 = 14745600 / 8 = 1843200 bytes
因此,一张1280x720大小的图片在YUV 4:2:2采样时的大小为1843200字节(约为1.76 MB)。
参考:一文读懂 YUV 的采样与格式 - 知乎 (zhihu.com)
YUYV 4:2:2 to RGB888 to RGB565
读取时对出队的每一帧图像数据作格式转换
void yuyv_to_rgb(unsigned char* yuv,unsigned char* rgb565)
2.LCD显示问题
补充:LCD显示原理