嵌入式 servfox分析

时间:2021-08-28 08:43:52
构建嵌入式Linux网络视频监控系统中,我们采用servfox来做服务器采集程序.servfox涉及到的内容主要有:V4L1接口、套接字和多线程编程. 这里简单分析一下servfox-R1_1_3.

1. servfox做了什么?

servfox在采集图像的过程中主要做什么事情?
它初始化摄像头设备后创建了线程1采集视频图像. 然后主程序创建一个套接字监听,阻塞等待客户端的请求连接.连接成功后再创建线程2发送采集到的图像数据给客户端.

  • 线程1:采集视频图像.
  • 线程2:发送图像数据给客户端.

在采集线程和发送线程同时运行的情况下,会存在对存储压缩过的图像数据的缓冲区这个临界区竞争的情况.为了能把采集到的一帧图像数据完整地发送出去,需要采用一些同步机制.servfox只是个应用程序,它初始化设备,获取设备属性和图像属性,设置图像参数,捕捉图像数据,都是通过Video4Linux接口标准调用驱动的相关函数完成的.本文末尾将会列举部分摄像头设备驱动要实现的file_oerations结构体里面的函数.

2. servfox运行步骤

servfox运行流程图如下:
嵌入式 servfox分析

2.1 从命令行传递参数给变量

main()函数内,首先执行的是一个for循环体. 看一下里面的几个语句:

...
if (strcmp (argv[i], "-d") == 0) {
if (i + 1 >= argc) {
if(debug)
printf ("No parameter specified with -d, aborting.\n");
exit (1);
}
videodevice = strdup (argv[i + 1]);
}
...

videodevice保存了摄像头设备节点名称. 用户不指定的话,后面会将它设置为"/dev/video0".

...
if (strcmp (argv[i], "-g") == 0) {

grabmethod = 0;
}
...

通过grabmethod的设置就指定了采集图像时使用mmap()内存映射的方法还是read()读取的方法.采用read系统调用来读取图像数据的话在连续抓取的情况下会发生频繁的用户态和内核态的切换,效率低.通过mmap内存映射的话,把摄像头对应的设备文件映射到进程内存中,减少I/O操作,提高了效率.因此启动servfox时不加"-g"选项的话默认采用grabmethod=1为mmap方式.

在for循环体里面还根据用户输入的选项分配了存储分辨率大小width/height,创建套接字时用的端口号serverport(默认为7070).

2.2 初始化视频采集设备

接下来主要要执行的语句有:

memset (&videoIn, 0, sizeof (struct vdIn));   // 将结构体videoIn初始化为0

先来看看videoIn这个结构体:

  • vdIn 结构体(在spcav4l.h中定义,它里面的成员都是依据Video4Linux接口标准而定义的):
    struct vdIn {
    int fd; // 设备文件描述符
    char *videodevice ; // 设备,视频捕捉接口文件
    struct video_mmap vmmap;


    struct video_capability videocap;


    int mmapsize;
    struct video_mbuf videombuf;


    struct video_picture videopict; // 采集到的图像的各种属性
    struct video_window videowin; // 包含capture area的信息
    struct video_channel videochan; // 各个信号源的属性
    struct video_param videoparam;
    int cameratype ; // 是否能capture,彩色还是黑白,是否能裁剪等等
    char *cameraname; // 设备名称
    char bridge[9];
    int sizenative; // available size in jpeg.
    int sizeothers; // others palette.
    int palette; // available palette.
    int norme ; // set spca506 usb video grabber.
    int channel ; // set spca506 usb video grabber 信号源个数
    int grabMethod ;
    unsigned char *pFramebuffer; // 指向内存映射的指针
    unsigned char *ptframe[4]; // 指向压缩后的帧的指针数组
    int framelock[4];
    pthread_mutex_t grabmutex; // 视频采集线程和传输线程的互斥信号
    int framesizeIn ; // 视频帧的大小
    volatile int frame_cour; // 指向压缩后的帧的指针数组下标
    int bppIn; // 采集的视频帧的BPP
    int hdrwidth; // 采集的视频帧的宽度
    int hdrheight; // 采集的视频帧的高度
    int formatIn; // 采集的视频帧的格式
    int signalquit; // 停止视频采集的信号
    };

接下来执行:

if (init_videoIn
(&videoIn, videodevice, width, height, format,grabmethod) != 0)

这个函数主要是设置了grabmethod:用mmap方式还是read方式;
设置videodevice成员设备文件名称,默认是 "/dev/video0";
设置信号vd->signalquit=1,图像宽高:vd->hdrwidth=width;vd->hdrheight=height;
设置图像格式为VIDEO_PALETTE_JPEG:vd->formatIn = format;
获得色深:vd->bppIn = GetDepth (vd->formatIn);


调用init_v4l():
=================进入init_v4l()================================================
init_v4l()是初始化v4l视频设备的函数,它首先通过系统调用open()打开视频设备,成功打开后主要执行下面几个步骤:

  • 1. 通过系统调用ioctl (vd->fd, VIDIOCGCAP,&(vd->videocap))取得设备信息。读取structvideo_capability中有关摄像头的信息,保存到vd->videocap中.
  • 2. 初始化图像.

ioctl (vd->fd, VIDIOCGPICT, &vd->videopict);
带VIDIOCGPICT参数的ioctl调用会获取图像的属性,并保存在vd->videopict指向的结构体中.

  • 3. 读取ruct videochanel中有关设备通道的信息,保存到vd->videochan指向的结构体中。

ioctl (vd->fd, VIDIOCGCHAN, &vd->videochan);

  • 4. 设置摄像头参数.

读取摄像头数据前,需要对摄像头进行设置,主要包括图像参数和分辨率.

ioctl (vd->fd, VIDIOCSPICT, &vd->videopict)

设置分辨率主要是对vd->videowin各分量进行修改,若为read方式,具体实现为:

if (ioctl (vd->fd, VIDIOCGWIN, &(vd->videowin)) < 0)   // 获得捕获源的大小
perror ("VIDIOCGWIN failed \n");
vd->videowin.height = vd->hdrheight;
vd->videowin.width = vd->hdrwidth;
if (ioctl (vd->fd, VIDIOCSWIN, &(vd->videowin)) < 0)
perror ("VIDIOCSWIN failed \n");
  • 5. 摄像头设备文件映射初始化或read方式初始化

完成上述初始化设备工作后,就可以对访问到摄像头设备文件的内容了.如果选用mmap()内存映射方式的话,下面的步骤将摄像头设备文件映射到进程内存,这样就可以直接读取映射了的这片内存,而不必read设备文件了:

a. 获取摄像头缓冲区帧信息:

ioctl (vd->fd, VIDIOCGMBUF, &(vd->videombuf));

该操作获取摄像头存储缓冲区的帧信息:包括帧的大小(size),最多支持的帧数(frames),每帧相对基址的偏移(offset).这些参数都是由摄像头设备硬件决定的.这些信息将被保存在videombuf结构体里面,下面的映射摄像头设备文件到内存操作马上就要用到了:

b. 映射摄像头设备文件到内存:

vd->pFramebuffer =
(unsigned char *) mmap (0, vd->videombuf.size, PROT_READ | PROT_WRITE,
MAP_SHARED, vd->fd, 0);

该操作把摄像头对应的设备文件映射到内存区. 该映射内容区可读可写并且不同进程间可共享. 帧的大小(vd->videombuf.size)是a步骤获取的.该函数成功返回映像内存区的指针,该指针赋值给vd->pFramebuffer,失败时返回-1.

c. 视频图像捕捉测试:

if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->vmmap))) {
perror ("cmcapture");
}

该操作捕捉一帧图像,获取图像信息到vmmap里.它会根据vmmap中设置的属性参数(frame,height,width和format)通知驱动程序启动摄像头抓拍图像.该操作是非阻塞的,是否截取完毕留给VDIOCSYNC来判断.在init_v4l()这里只是为了测试是否可以成功捕获一帧图像,真正采集图像是在采集线程时执行v4lGrab()这个函数的时候.

以上是用mmap内存映射方式,如果采用直接读取摄像头设备文件的方式获取图像的话,将执行:

els {


vd->pFramebuffer = (unsigned char *) realloc(vd->pFramebuffer, \
(size_t) vd->framesizeIn);


if (ioctl (vd->fd, VIDIOCGWIN, &(vd->videowin)) < 0) // 获得捕获源的大小
perror("VIDIOCGWIN failed \n");
vd->videowin.height = vd->hdrheight;
vd->videowin.width = vd->hdrwidth;
if (ioctl(vd->fd, VIDIOCSWIN, &(vd->videowin)) < 0)
perror("VIDIOCSWIN failed \n");
}

摄像头设备文件映射初始化或read方式初始化完成后,返回init_videoIn().

=============从init_v4l()返回==========================================================


从init_v4l()返回到init_videoIn()后,分配vd->ptframe[i]空间.

 

for (i = 0; i < OUTFRMNUMB; i++) {
vd->ptframe[i] = NULL;
vd->ptframe[i] = (unsigned char *) realloc (vd->ptframe[i],\
sizeof(struct frame_t) + (size_t) vd->framesizeIn );
vd->framelock[i] = 0;
}

unsigned char* ptframe[4]:指向四个buffer缓冲数组,用来存放已压缩完成的图像数据.

2.3 采集图像数据线程

init_videoIn()执行完后返回main(),接下来创建采集视频图像的线程:

pthread_create (&w1, NULL, (void *) grab, NULL);

进入grab()函数:可以看到在死循环体里面调用v4lGrab()函数.
进入v4lGrab()函数,先判断一下是用mmap方法还是用read方法. 下面仅就mmap方法分析:

ioctl (vd->fd, VIDIOCSYNC, &vd->vmmap.frame);

这条语句是等待捕捉完这一帧图像,调用成功后表明一帧图像捕捉完毕,可以开始进行下一次图像捕捉.vd->vmmap.frame是当前捕捉到帧的序号.

接下来的是个循环睡眠等待:

while((vd->framelock[vd->frame_cour] != 0) && vd->signalquit)
usleep(1000);

它是等待之后执行的另一个用来的发送采集到的图像数据给客户端的线程,直到它把这一帧图像完整地发送出去. 每隔1毫秒就检查一次是否发完.如果不等待就执行下面的操作的话,那么还没发送完就把本来要发送的图像数据重写掉,采集到的数据没用上.可以采用更好的同步机制--信号量来实现.

等到上一帧图像数据发送出去之后,这个线程等待直到获得一把线程互斥锁:

pthread_mutex_lock (&vd->grabmutex);

它把临界区资源vd->ptframe锁住,防止下面获取时间和拷贝数据到ptframe及设置一帧图像的头部时被别的线程抢占.虽然在发送线程里并没有找到相关互斥锁的操作(这个应该是要加的),但为了扩展,有可能以后我们添加一些访问临界区vd->ptframe的线程时可以用它这把锁.

然后执行:

  tems = ms_time();

tems获得的是距离UNIX的Epoch时间即:1970年1月1日0时0分0秒算起的毫秒数. 它可以用在视频图像的时间戳.
然后执行:

jpegsize= convertframe(vd->ptframe[vd->frame_cour]+ sizeof(struct frame_t),
vd->pFramebuffer + vd->videombuf.offsets[vd->vmmap.frame],
vd->hdrwidth,vd->hdrheight,vd->formatIn,vd->framesizeIn);

跟踪进去可以看出要是视频图像格式是VIDEO_PALETTE_JPEG的话,直接将pFramebuffer中的数据拷贝到ptframe缓存中去,而不压缩处理,因为获得的就是已经压缩过的jpeg格式了(是硬件或底层驱动做了,一般USB摄像头对采集到的图像都作了jpeg格式压缩(内置JPEG硬件压缩)).获得jpeg格式文件的大小是通过调用get_jpegsize()实现的.进入get_jpegsize()可以发现,它利用了jpeg文件格式中是以0xFF 0xD9结尾的这个特性.ptframe里面的经压缩过的图像数据就是发送线程要发送出去的内容了.

pFramebuffer中的数据拷贝进ptframe完成后,就截取下一帧图像数据了:

if ((ioctl (vd->fd, VIDIOCMCAPTURE, &(vd->vmmap))) < 0) {
perror ("cmcapture");
if(debug) printf (">>cmcapture err \n");
erreur = -1;
}
vd->vmmap.frame = (vd->vmmap.frame + 1) % vd->videombuf.frames;
vd->frame_cour = (vd->frame_cour +1) % OUTFRMNUMB;

执行完后,跳出v4lGrab()函数体,返回到grab()去. 正常运行状态下,将不断循环调用v4lGrab()采集图像数据.采集线程分析完毕.

2.4 建立TCP套接字服务端,为图像数据发送线程做好准备

回到main(),继续往下执行:

serv_sock = open_sock(serverport);

跟踪进入open_sock()里面可以看到通过执行socket(),bind(),listen()建立了一个TCP套接字服务端并在指定端口上监听,等待客户端连接.紧跟在socket()后面有一句:

setsockopt(server_handle, SOL_SOCKET, SO_REUSEADDR, &O_on, sizeof (int));

这个语句应该是为了允许启动多个服务端或多个servfox. 参见:http://blog.csdn.net/liusujian02/article/details/1944520(关于SO_REUSEADDR的使用说明)

执行完serv_sock = open_sock(serverport)这个语句之后,下一条语句是:

signal(SIGPIPE, SIG_IGN);        

这是为了忽略SIGPIPE信号:若客户端关闭了和服务端的连接,但服务端依然试图发送图像数据给客户端(write to pipewith noreaders),系统就会发出一个SIGPIPE信号,默认对SIGPIPE的处理是terminate(终止),那么负责发送图像数据的服务端就挂掉了,即使还有别的客户端连接.这当然不是我们想要的,因此把我们要执行这句语句把SIGPIPE信号忽略掉.

2.5 发送图像数据到客户端的线程

接下来,是一个while(videoIn.signalquit)循环体,如果没有接收到退出信号,它就一直循环运行里面的语句:

while (videoIn.signalquit) {
sin_size = sizeof(struct sockaddr_in);


if ((new_sock = accept(serv_sock, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
continue;
}
syslog(LOG_ERR,"Got connection from %s\n",inet_ntoa(their_addr.sin_addr));
printf("Got connection from %s\n",inet_ntoa(their_addr.sin_addr));
pthread_create(&server_th, NULL, (void *)service, &new_sock);
}

之前建立的服务端一直监听等待客户端来连接,一旦有客户端connect()过来,服务端执行accept()建立连接后,就创建了发送图像数据到客户端的线程了:

pthread_create(&server_th, NULL, (void *)service, &new_sock);

我们再进入这个线程执行的service()函数去分析:


=============进入service()==============================

 

    bright = upbright(&videoIn);
contrast = upcontrast(&videoIn);
bright = downbright(&videoIn);
contrast = downcontrast(&videoIn);

上面所谓的初始话视频设置,是先增大一下亮度和对比度,在减小亮度和对比度恢复到原来的状态,顺便将亮度值保存在bright变量,将对比度值保存在contrast变量.
然后是一个死循环体:

for (; ;) {
memset(&message,0,sizeof(struct client_t));
ret = read(sock,(unsigned char*)&message,sizeof(struct client_t));
......
if (message.updobright){
switch (message.updobright){
case 1: bright = upbright(&videoIn);
break;
case 2: bright = downbright(&videoIn);
break;
}
ack = 1;
} else if (message.updocontrast){
switch (message.updocontrast){
case 1: contrast = upcontrast(&videoIn);
break;
case 2: contrast = downcontrast(&videoIn);
break;
}
ack = 1;
} else if (message.updoexposure){
switch (message.updoexposure){
case 1: spcaSetAutoExpo(&videoIn);
break;
case 2:;
break;
}
ack = 1;
} else if (message.updosize){ //compatibility FIX chg quality factor ATM
switch (message.updosize){
case 1: qualityUp(&videoIn);
break;
case 2: qualityDown(&videoIn);
break;
}
ack = 1;
} else if (message.fps){
switch (message.fps){
case 1: timeDown(&videoIn);
break;
case 2: timeUp(&videoIn);
break;
}
ack = 1;
} else if (message.sleepon){
ack = 1;
} else ack =0;
while ((frameout == videoIn.frame_cour) && videoIn.signalquit)
usleep(1000);
if (videoIn.signalquit){
videoIn.framelock[frameout]++;
headerframe = (struct frame_t *) videoIn.ptframe[frameout];
headerframe->acknowledge = ack;
headerframe->bright = bright;
headerframe->contrast = contrast;
headerframe->wakeup = wakeup;
ret = write_sock(sock, (unsigned char *)headerframe, sizeof(struct frame_t)) ;


if(!wakeup)
ret = write_sock(sock,(unsigned char*)(videoIn.ptframe[frameout] + \
sizeof(struct frame_t)),headerframe->size);

videoIn.framelock[frameout]--;
frameout = (frameout+1)%4;
} else {
if(debug)
printf("reader %d going out \n",*id);
break;
}
}

和客户端建立连接后,客户端会先将设置图像的信息发给服务端,因此上面代码,首先读取客户端对图像的设置,把设置信息存放在message结构体里,然后是根据message里的信息对采集图像的显示属性(如亮度bright,对比度contrast等)进行设置,具体操作是通过ioctl()调用底层驱动来完成对摄像头抓拍图像的显示设置.

设置完采集图像显示属性后,执行:

while ((frameout == videoIn.frame_cour) && videoIn.signalquit) 
usleep(1000);

frame_cour是指向压缩后的图像帧的指针数组下标,我们一共存储4帧(unsigned char*ptframe[4]),为了按顺序读取每一帧,就等待知道frameout和videoIn.frame_cour相等时才执行后面的发送操作,发送这帧图像完成后会执行frameout= (frameout+1)%4使得下一次发送下一帧图像. 个人觉得这里采取信号量的同步机制更好.
等采集线程完成一帧采集使得videoIn.frame_cour等于frameout之后(因为这里没有采用同步机制,有可能这一轮会落空),就开始执行发送这一帧图像给客户端的操作了:先将让headerframe指向帧信息头,然后发送headerframe指向的信息头给客户端,再发送剩下的图像数据.这样就把完整的一帧图像发送给客户端了.
只要没有收到客户端退出的信号,以上的发送过程会循环执行.

当收到客户端退出的信息后,它就退出循环,执行close_sock(sock)关闭套接字,终止线程.

=============从service()返回=======================================================================


服务器发送图像线程终止后,只要进程没有退出信号还会在while(videoIn.signalquit)这个循环体继续,阻塞等待客户端的连接,重复上面的过程.

 

若videoIn.signalquit等于0了,就不再执行这个循环体,等待采集线程退出:pthread_join (w1,NULL);关闭套接字:close(serv_sock);回收以前分配的资源:close_v4l(&videoIn);整个程序就正常退出了.

3. servfox与底层驱动的接口

前面说过,servfox只是个应用程序,它初始化设备,获取设备属性和图像属性,设置图像参数,捕捉图像数据,都是通过V4L1接口标准调用驱动的相关函数完成的.V4L1就是Video4Linux的版本1,Video4Linux已整合进Linux内核里面了.新版本是v4l2,它和v4l1不是完全兼容的.而V4L1已经是过时了.从Linux2.6.38内核就已经完全放弃了对v4l1的支持,因此不修改过的servfox不能在2.6.38以上的内核上运行了.不过有功能更强大的mjpeg-streamer来取代servfox.而mjpeg-streamer是基于v4l2接口的.

由于servfox体积小,在它上面进行扩展是很容易的,比如加入基于libjpeg库的本地解码jpeg显示到lcd屏的线程,加入截屏的线程等.

下面列出了servfox用到的一些v4l1的接口,如果非要把servfox移植到2.6.38的Linux内核上运行的话,必须修改这些v4l1的接口使之兼容于v4l2.

  • 摄像头驱动里要实现的ioctl()
    1. ioctl(vd->fd, VIDIOCSYNC, &vd->vmmap.frame)




2. if ((ioctl (vd->fd, VIDIOCMCAPTURE, &(vd->vmmap))) < 0)


3. 读video_picture中信息
ioctl(vd->fd, VIDIOCGPICT, &(vd->picture));

if (ioctl (vd->fd, VIDIOCGPICT, &vd->videopict) < 0)
访问摄像头设备采集的图像的各种属性。然后通过访问结构体vd->videopict 就可以读出图像的各种信息。

vd->videopict中分量的值是可以改变的,实现方法为:先为分量赋新值,再调用VIDIOCSPICT. 如:

4.
if (ioctl (vd->fd, VIDIOCGCAP, &(vd->videocap)) == -1)
exit_fatal ("Couldn't get videodevice capability");

读video_capability 中信息
ioctl(vd->fd, VIDIOCGCAP, &(vd->capability))
成功后可读取vd->capability各分量 eg.
Printf(”maxwidth = %d”vd->capability.maxwidth);
5. 初始化channel:
if (ioctl (vd->fd, VIDIOCGCHAN, &vd->videochan) == -1)
// 用来取得和设置channel信息,例如使用那个输入源,制式等
{
if(debug) printf ("Hmm did not support Video_channel\n");
vd->cameratype = UNOW;
}
6. 初始化video_mbuf,以得到所映射的buffer的信息
ioctl(vd->fd, VIDIOCGMBUF, &(vd->mbuf))
if (ioctl (vd->fd, VIDIOCGMBUF, &(vd->videombuf)) < 0)
{
perror (" init VIDIOCGMBUF FAILED\n");
}
     // 要确定是否捕捉到图象,要用到下一个命令。
if (ioctl (vd->fd, VIDIOCMCAPTURE, &(vd->vmmap)))
{
perror ("cmcapture");
}
7. if (ioctl (vd->fd, VIDIOCGWIN, &(vd->videowin)) < 0)      // 获得捕获源的大小
perror ("VIDIOCGWIN failed \n");
  • v4l2中的ioctl()的cmd:

在进行V4L2开发中,一般会用到以下的命令标志符:

1. VIDIOC_REQBUFS:分配内存

2. VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址

3. VIDIOC_QUERYCAP:查询驱动功能

4. VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式

5. VIDIOC_S_FMT:设置当前驱动的频捕获格式

6. VIDIOC_G_FMT:读取当前驱动的频捕获格式

7. VIDIOC_TRY_FMT:验证当前驱动的显示格式

8. VIDIOC_CROPCAP:查询驱动的修剪能力

9. VIDIOC_S_CROP:设置视频信号的边框

10. VIDIOC_G_CROP:读取视频信号的边框

11. VIDIOC_QBUF:把数据从缓存中读取出来

12. VIDIOC_DQBUF:把数据放回缓存队列

13. VIDIOC_STREAMON:开始视频显示函数

14. VIDIOC_STREAMOFF:结束视频显示函数

15. VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如PAL或NTSC。

这些IO调用,有些是必须的,有些是可选择的。