该图为整个项目的流程图
其中左边的流程为总流程,包括通过中断读取摄像头的帧数据,通过内存映射将内核态的数据映射到用户态,读取用户态的数据,采用循环发送图片数据。
右边是发送图片的流程图,将用户态的缓冲区的数据通过内存映射到用户态,通过内存拷贝将用户态的指针地址对应的数据保存在一个字符数组中,然后通过循环读取字符数组中的数据并通过socket 将数据发送给服务器。
项目场景:
该项目是为了在linux arm开发板上通过linux内置的v4l2接口读取摄像头数据,并通过网络协议将图片数据发送给服务器。因为之前用过opencv ffmpeg等开源工具读取过linux三的usb摄像头,可以看到这两种接口都是封装了v4l2的接口,所以我想在底层深入了解怎么采用v4l2查看usb摄像头的数据。
问题描述
如何正确的读取用户态下的数据:
原因分析:
如何正确的读取用户态下的数据:
我在通过ioctl 接口调用读取到单帧的数据,得到的是一个指针地址,以及该指针地址后面length个字节。
eg: ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer);
得到的数据首地址在 mptr[readbuffer.index] 保存的地址中,并且数据长度为 readbuffer.length
这里我需要怎样读取 mptr[readbuffer.index]地址里面的数据
解决方案:
初始化一个指针,指向数据的首地址,通过移动指针的位置,来访问数据的不同位置:
void *ptr = mptr[readbuffer.index];
int send_num = sendto(sockfd, ptr,1024 ,0,(SA)&saddr,sizeof(saddr));
ptr += 1024;
问题描述
UDP发送数据时,会出现数据丢失的情况:
比如 char sent[614400] 总共是614400大小的一个字符数组,但是udp接收端接收到小于614400大小的数据
原因分析:
通过查阅资料,发现,是由于udp发送端发送数据速度较快,而接收端不仅要接收数据还要处理数据,导致接收端处理数据较慢,由于接收端有一个接收缓冲区,当发送端发送的数据超过了缓冲区的大小,那么缓冲区里面的数据就会被覆盖掉:
udp发送端发送数据速度较快,而接收端不仅要接收数据还要处理数据,导致接收端处理数据较慢,由于接收端有一个接收缓冲区,当发送端发送的数据超过了缓冲区的大小,那么缓冲区里面的数据就会被覆盖掉:
解决方案:
两个解决方案,接收端采用多线程读写两个线程进行处理,或者发送端发送数据后,休眠1微秒:
这里我采用的发送端在发送完数据后,休眠一微秒,然后再发送剩下的数据,如此循环往复。
以上就是我在做项目中遇到的两个问题,及解决办法。总共耗时两天。
接下来我给出整体代码。
server端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <fcntl.h>
typedef struct sockaddr * (SA);
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建套接字描述符
if(-1 == sockfd)
{
perror("sockfd");
exit(1);
}
struct sockaddr_in ser,cli;
bzero(&ser,sizeof(ser));
bzero(&cli,sizeof(cli));
ser.sin_family =AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("192.168.0.89");
int ret = bind(sockfd,(SA)&ser,sizeof(ser));
if(-1 == ret)
{
perror("bind");
exit(1);
}
int len = sizeof(cli);
char save_name[10];
int save_idx = 0 ;
while(1)
{
char buf1[1024]={0};
int rd_num1 = recvfrom(sockfd,buf1,1024,0,(SA)&cli,&len);
if(0 == strcmp(buf1,"^_^"))
{
sprintf(save_name,"my_%d.jpg",save_idx++);
}
int fd = open(save_name, O_CREAT |O_WRONLY|O_TRUNC,0666);
if(-1 == fd)
{
perror("open error!\n");
exit(1);
}
int cnt = 0 ;
while(1)
{
char buf[1024]={0};int left = 614400 - cnt;int rd_num ;
//if(left < 1024 )
//rd_num = recvfrom(sockfd,buf,left,0,(SA)&cli,&len);
//else
rd_num = recvfrom(sockfd,buf,1024,0,(SA)&cli,&len);
//printf("%d \n ",rd_num);
cnt += rd_num;
//if(cnt >= 614400){printf("%d \n",cnt);break;}
//收到发送完毕的信号,退出
if(0 == strcmp(buf,"^_^"))
{
printf("file end %d \n",cnt);
break;
}
write(fd,buf,rd_num);
bzero(buf,sizeof(buf));
//usleep(0.01);
}
close(fd);
}
close(sockfd);
return 0;
}
client端
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
#include <jpeglib.h>
#include <linux/fb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr * (SA);
//如下函数read_JPEG_file为封装好的函数,需要转换格式的时候直接调用即可,不用深入理解
int read_JPEG_file (const char *jpegData, char *rgbdata, int size)
{
struct jpeg_error_mgr jerr;
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr);
//1创建解码对象并且初始化
jpeg_create_decompress(&cinfo);
//2.装备解码的数据
//jpeg_stdio_src(&cinfo, infile);
jpeg_mem_src(&cinfo,jpegData, size);
//3.获取jpeg图片文件的参数
(void) jpeg_read_header(&cinfo, TRUE);
/* Step 4: set parameters for decompression */
//5.开始解码
(void) jpeg_start_decompress(&cinfo);
//6.申请存储一行数据的内存空间
int row_stride = cinfo.output_width * cinfo.output_components;
unsigned char *buffer = malloc(row_stride);
int i=0;
while (cinfo.output_scanline < cinfo.output_height) {
//printf("****%d\n",i);
(void) jpeg_read_scanlines(&cinfo, &buffer, 1);
memcpy(rgbdata+i*640*3, buffer, row_stride );
i++;
}
//7.解码完成
(void) jpeg_finish_decompress(&cinfo);
//8.释放解码对象
jpeg_destroy_decompress(&cinfo);
return 1;
}
int fd_fb;
static struct fb_var_screeninfo var; /* LCD可变参数 */
static unsigned int *fb_base = NULL; /* Framebuffer映射基地址 */
int lcd_w = 800 ,lcd_h= 480; //定义显示器分辨率
//将数据流以3字节为单位拷贝到rgb显存中
void lcd_show_rgb(unsigned char *rgbdata, int w ,int h)
{
unsigned int *ptr = fb_base;
for(int i = 0; i <h; i++) {
for(int j = 0; j < w; j++) {
memcpy(ptr+j,rgbdata+j*3,3);//
}
ptr += lcd_w;
rgbdata += w*3;
}
}
int main(void)
{
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
assert( sockfd != -1 );
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(50000);
saddr.sin_addr.s_addr = inet_addr("192.168.0.89");
char buff[614400] = {0};
//虚拟机-ubuntu
lcd_w = var.xres_virtual; //xres_virtual参数可以自动获取当前虚拟机显示器分辨率
lcd_h = var.yres_virtual;
//建立显示器fb内存映射 方便控制
fb_base = (unsigned int*)mmap(NULL,lcd_w*lcd_h*4,PROT_READ|PROT_WRITE,MAP_SHARED, fd_fb,0);
if(fb_base == NULL)
{
printf("can't mmap Framebuffer\n");
goto err1;
}
int fd = open("/dev/video0",O_RDWR); //打开摄像头设备
if (fd < 0)
{
perror("打开设备失败");
return -1;
}
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头采集
vfmt.fmt.pix.width = 640; //设置摄像头采集参数,不可以任意设置
vfmt.fmt.pix.height = 480;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //设置视频采集格式为mjpg格式
int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt);
if (ret < 0)
{
perror("设置格式失败1");
}
//申请内核空间
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式
ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
if (ret < 0)
{
perror("申请空间失败");
}
//映射
unsigned char *mptr[4];//保存映射后用户空间的首地址
unsigned int size[4];
struct v4l2_buffer mapbuffer;
//初始化type和index
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for(int i = 0; i <4;i++) {
mapbuffer.index = i;
ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer); //从内核空间中查询一个空间作映射
if (ret < 0)
{
perror("查询内核空间失败");
}
//映射到用户空间
mptr[i] = (unsigned char *)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
size[i] = mapbuffer.length; //保存映射长度用于后期释放
//查询后通知内核已经放回
ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer);
if (ret < 0)
{
perror("放回失败");
}
}
//开始采集
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd,VIDIOC_STREAMON,&type);
if (ret < 0){ perror("开启失败");}
//定义一个空间存储解码后的rgb
unsigned char rgbdata[640*480*3];
int jpg_idx = 0 ;
char jpg_name[10];
char * buf = malloc(1024);
int sum = 100;//这里是用来控制读取的帧的数量 实际场景下 可以不要这个限制
while(1)
{
if(sum -- < 0 ){return -1 ; }
//strcpy(buf,"^_^");
//sendto(sockfd,buf,strlen(buf),0,(SA)&saddr,sizeof(saddr));
//从队列中提取一帧数据
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //每个结构体都需要设置type为这个参赛要记住
ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer);
int read_len = mapbuffer.length;
int send_len = 1024;
printf("%d \n",read_len);
if (ret < 0)
{
perror("读取数据失败");
}
if(1)
{
void *ptr = mptr[readbuffer.index];
strcpy(buf,"^_^");//发送UDP头 方便接收方确认数据开始
sendto(sockfd,buf,strlen(buf),0,(SA)&saddr,sizeof(saddr));
int cnt = 0 ; //用来保存已经发送的字节数量
while(1)
{
if(cnt >= readbuffer.length)
{
break;
}
cnt += 1024;
int send_num = sendto(sockfd, ptr,1024 ,0,(SA)&saddr,sizeof(saddr));
ptr += 1024;
usleep(1);
bzero(buf,sizeof(buf));
}
strcpy(buf,"^_^");//发送UDP尾巴 方便接收方确认数据结束
sendto(sockfd,buf,strlen(buf),0,(SA)&saddr,sizeof(saddr));
usleep(5);
}
//通知内核使用完毕
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if(ret < 0)
{
perror("放回队列失败");
}
}
//停止采集
ret = ioctl(fd,VIDIOC_STREAMOFF,&type);
//释放映射
for(int i=0; i<4; i)munmap(mptr[i], size[i]);
close(fd); //关闭文件
return 0;
err1:
close(fd_fb);
return -1;
}