简单视频监控项目的设计与实现(一)

韦东山第三期项目
最近一段时间看了韦东山老师的项目视频,记录一下

1. 硬件准备

IMX6ULL开发板一套
USB摄像头

2. 软件准备

移植Linux内核,我的版本是4.1.15
根文件系统
libjpeg库
mjpeg-streamer
svgalib库安装

3. 项目需求

通过USB摄像头实现远程视频监控
(1)USB摄像头设备采集数据,
(2)采集应用程序通过USB驱动获得所采集的数据
(3)应用程序通过网络将数据传输到PC上的显示程序
(5)PC显示程序显示数据

4. 项目设计流程

根据项目的需求可知主要完成三个部分工作:
(1)USB摄像头驱动配置
(2)mjpg-streamer移植
(3)显示应用程序编写

5.开发环境

编译环境:Ubuntu16.04.6(64位),Ubuntu10.04(32位的)
Ubuntu16.04.6用于开发板通过tftp启动Linux系统和挂载根文件系统,32位Ubuntu主要是用来编写客户端程序,这是因为svgalib库只能安装在32位系统上,之前在64位系统上试了很多版本都安装不上,查了很久,应该是这个原因
交叉编译器:arm-linux-gnueabihf-gcc 4.9.4
开发板:IMX6ULL(Cortex A7)

6.USB摄像头驱动配置

Linux内核支持UVC标准的USB摄像头驱动
打开图形化配置界面make menuconfig
进行配置

USB support  --->
	{*} Support for Host-side USB
		[*] USB announce new devices
Device Drivers  --->
<*> Multimedia support  --->
	[*] Cameras/video grabbers support 
	[*] Media Controller API
	[*] V4L2 sub-device userspace API
	[*]   Media USB Adapters  --->
		[*]   V4L USB devices  ---> 
			 <*>   USB Video Class (UVC)
				[*]     UVC input events device support  
	[*]  V4L platform devices
		<*> platform camera support
		<*>  SoC camera support

编译内核,将内核文件拷贝进tftboot目录,开发板通过tftp启动内核

7.相关库的安装与移植

这个具体的安装移植过程在我另一篇博客上https://blog.csdn.net/ChenNightZ/article/details/108170758

8.项目分析

我们先简单分析下mjpg-streamer的源码文件
mjpg_streamer.c

...
int main(int argc, char *argv[])
{
  //输入参数字符串,默认为以下值
  char *input  = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0";
  char *output[MAX_OUTPUT_PLUGINS];				//输出参数字符串
  int daemon=0, i;
  size_t tmp=0;

  //此时的输出通道有几种方式,默认为"output_http.so --port 8080"
  output[0] = "output_http.so --port 8080";
  global.outcnt = 0;

  /* parameter parsing */
  /*对命令行参数进行解析
	主要利用
	int getopt_long_only (int ___argc, char *const *___argv,  
                 const char *__shortopts,  
                     const struct option *__longopts, int *__longind); 
   	参考博客https://blog.csdn.net/pengrui18/article/details/8078813
	参数解释:
	argc argv :直接从main函数传递而来
	shortopts:短选项字符串。短选项字符串不需要‘-’,而且但选项需要传递参数时,在短选项后面加上“:”。
	longopts:struct option 数组,用于存放长选项参数。
	longind:用于返回长选项在longopts结构体数组中的索引值,用于调试。一般置为NULL
	返回值:解析完毕,返回-1
			出现未定义的长选项或短选项,返回?
			程序中使用短选项,则返回短选项字符(如‘n'),当需要参数是,则在返回之前将参数存入到optarg中。
			程序中使用长选项,返回值根据flag和val确定。当flag为NULL,则返回val值。所以根据val值做不同的处理,这也说明了val必须唯一。当val值等于短选项值,则可以使用短选项解析函数解析长选项;当flag不为NULL,则将val值存入flag所指向的存储空间,getopt_long返回0
  */
  while(1) {
    int option_index = 0, c=0;
    static struct option long_options[] = \
    {
      {"h", no_argument, 0, 0},
      {"help", no_argument, 0, 0},
      {"i", required_argument, 0, 0},
      {"input", required_argument, 0, 0},
      {"o", required_argument, 0, 0},
      {"output", required_argument, 0, 0},
      {"v", no_argument, 0, 0},
      {"version", no_argument, 0, 0},
      {"b", no_argument, 0, 0},
      {"background", no_argument, 0, 0},
      {0, 0, 0, 0}
    };

    c = getopt_long_only(argc, argv, "", long_options, &option_index);

    /* no more options to parse */
    //参数解析完成
    if (c == -1) break;

    /* unrecognized option */
    //如果参数不正确,则打印帮助信息
    if(c=='?'){ help(argv[0]); return 0; }

    switch (option_index) {
      /* h, help */
      case 0:
      case 1:
        help(argv[0]);
        return 0;
        break;

      /* i, input */
      case 2:
      case 3:
      //如果指定输入参数,则将字符串拷贝到新建的位置处
        input = strdup(optarg);
        break;

      /* o, output */
      case 4:
      case 5:
       //如果指定输出参数,则将字符串拷贝到新建的位置处
        output[global.outcnt++] = strdup(optarg);
        break;

      /* v, version */
      case 6:
      case 7:
      //如果指定了-v参数,则打印版本信息
        printf("MJPG Streamer Version: %s\n" \
               "Compilation Date.....: %s\n" \
               "Compilation Time.....: %s\n", SOURCE_VERSION, __DATE__, __TIME__);
        return 0;
        break;

      /* b, background */
      //后台运行
      case 8:
      case 9:
        daemon=1;
        break;

      default:
        help(argv[0]);
        return 0;
    }
  }
  //打开一个系统记录器的链接
  openlog("MJPG-streamer ", LOG_PID|LOG_CONS, LOG_USER);
  //openlog("MJPG-streamer ", LOG_PID|LOG_CONS|LOG_PERROR, LOG_USER);
  syslog(LOG_INFO, "starting application");

  /* fork to the background */
  //如果daemon = 1,则将程序后台运行
  if ( daemon ) {
    LOG("enabling daemon mode");
    daemon_mode();
  }

  /* initialise the global variables */
  //初始化global结构成员变量
  global.stop      = 0;
  global.buf       = NULL;
  global.size      = 0;
  global.in.plugin = NULL;

  /* this mutex and the conditional variable are used to synchronize access to the global picture buffer */
  //初始化锁和条件变量,用于同步接受的数据
  if( pthread_mutex_init(&global.db, NULL) != 0 ) {
    LOG("could not initialize mutex variable\n");
    closelog();
    exit(EXIT_FAILURE);
  }
  if( pthread_cond_init(&global.db_update, NULL) != 0 ) {
    LOG("could not initialize condition variable\n");
    closelog();
    exit(EXIT_FAILURE);
  }

  /* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
  //忽略SIGPIPE信号
  signal(SIGPIPE, SIG_IGN);

  /* register signal handler for <CTRL>+C in order to clean up */
  //注册SIGINT的信号处理函数,当用户按下<CTRL>+C时执行一些清理工作:释放资源,销毁锁和条件变量等
  if (signal(SIGINT, signal_handler) == SIG_ERR) {
    LOG("could not register signal handler\n");
    closelog();
    exit(EXIT_FAILURE);
  }

  /*
   * messages like the following will only be visible on your terminal
   * if not running in daemon mode
   */
  LOG("MJPG Streamer Version.: %s\n", SOURCE_VERSION);

  /* check if at least one output plugin was selected */
  if ( global.outcnt == 0 ) {
    /* no? Then use the default plugin instead */
    global.outcnt = 1;
  }

  /* open input plugin */
  //打开input链接库,如"input_uvc.so"
  tmp = (size_t)(strchr(input, ' ')-input);//tmp = "input_uvc.so"的长度
  //如果tmp=0,则说明没有指定-i,则使用默认参数
  global.in.plugin = (tmp > 0)?strndup(input, tmp):strdup(input);
  //打开输入的动态链接库
  global.in.handle = dlopen(global.in.plugin, RTLD_LAZY);
  if ( !global.in.handle ) {
    LOG("ERROR: could not find input plugin\n");
    LOG("       Perhaps you want to adjust the search path with:\n");
    LOG("       # export LD_LIBRARY_PATH=/path/to/plugin/folder\n");
    LOG("       dlopen: %s\n", dlerror() );
    closelog();
    exit(EXIT_FAILURE);
  }
  //global.in.init = input_init
  global.in.init = dlsym(global.in.handle, "input_init");
  if ( global.in.init == NULL ) {
    LOG("%s\n", dlerror());
    exit(EXIT_FAILURE);
  }
  //global.in.stop= input_stop
  global.in.stop = dlsym(global.in.handle, "input_stop");
  if ( global.in.stop == NULL ) {
    LOG("%s\n", dlerror());
    exit(EXIT_FAILURE);
  }
  //global.in.run = input_run
  global.in.run = dlsym(global.in.handle, "input_run");
  if ( global.in.run == NULL ) {
    LOG("%s\n", dlerror());
    exit(EXIT_FAILURE);
  }
  /* try to find optional command */
  global.in.cmd = dlsym(global.in.handle, "input_cmd");
	
  //global.in.param.parameter_string = "input_uvc.so"后面的字符串
  global.in.param.parameter_string = strchr(input, ' ');
  global.in.param.global = &global;
  //调用链接库中input-init(&global.in.param)函数
  if ( global.in.init(&global.in.param) ) {
    LOG("input_init() return value signals to exit");
    closelog();
    exit(0);
  }

  /* open output plugin */
  //打开ouput链接库,与input同理
  for (i=0; i<global.outcnt; i++) {
    tmp = (size_t)(strchr(output[i], ' ')-output[i]);
    global.out[i].plugin = (tmp > 0)?strndup(output[i], tmp):strdup(output[i]);
    global.out[i].handle = dlopen(global.out[i].plugin, RTLD_LAZY);
    if ( !global.out[i].handle ) {
      LOG("ERROR: could not find output plugin %s\n", global.out[i].plugin);
      LOG("       Perhaps you want to adjust the search path with:\n");
      LOG("       # export LD_LIBRARY_PATH=/path/to/plugin/folder\n");
      LOG("       dlopen: %s\n", dlerror() );
      closelog();
      exit(EXIT_FAILURE);
    }
    global.out[i].init = dlsym(global.out[i].handle, "output_init");
    if ( global.out[i].init == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    global.out[i].stop = dlsym(global.out[i].handle, "output_stop");
    if ( global.out[i].stop == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    global.out[i].run = dlsym(global.out[i].handle, "output_run");
    if ( global.out[i].run == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    /* try to find optional command */
    global.out[i].cmd = dlsym(global.out[i].handle, "output_cmd");

    global.out[i].param.parameter_string = strchr(output[i], ' ');
    global.out[i].param.global = &global;
    global.out[i].param.id = i;
    //调用链接库中output_init(&global.out[i].param)函数
    if ( global.out[i].init(&global.out[i].param) ) {
      LOG("output_init() return value signals to exit");
      closelog();
      exit(0);
    }
  }

  /* start to read the input, push pictures into global buffer */
  DBG("starting input plugin\n");
  syslog(LOG_INFO, "starting input plugin");
  //调用输入链接库的input_run函数
  global.in.run();

  DBG("starting %d output plugin(s)\n", global.outcnt);
  for(i=0; i<global.outcnt; i++) {
    syslog(LOG_INFO, "starting output plugin: %s (ID: %02d)", global.out[i].plugin, global.out[i].param.id);
    //调用输出链接库中的output_run函数
    global.out[i].run(global.out[i].param.id);
  }

  /* wait for signals */
  pause();

  return 0;
}

可以看出这个函数主要是解析命令行输入参数,并根据参数选择不同的动态链接库,最后都调用init函数进行输入输出的初始化,然后调用run函数开始数据的读取与输出。
(1)mjpg-streamer采集摄像头数据
在开发板上运行mjpg-streamer,当输入命令行参数 ./mjpg_streamer -i “input_uvc.so -f 10 -r 320*240 -d /dev/video0 -y” -o "output_http.so -w www"后,摄像头开始从UVC摄像头采集数据。这里mjpg-streamer主要调用了V4L2相关函数。
在上面main函数中调用了input_init函数,在指定uvc输入后将执行在plugins\input_uvc目录下的input_uvc.c中的函数。
在input_uvc.c中主要是进一步解析输入参数,如-d,-f,-r等,根据参数初始化相应的变量,如图片大小,格式,打开设备名等等,然后给videoIn结构体(定义在v4l2uvc.h中)分配空间,最后调用init_videoIn函数。
init_videoIn在v4l2uvc.c中实现,主要是初始化videoIn结构体,然后调用init_v4l2 函数。

int init_videoIn(struct vdIn *vd, char *device, int width, int height, int fps, int format, int grabmethod)
{
  if (vd == NULL || device == NULL)
    return -1;
  if (width == 0 || height == 0)
    return -1;
  if (grabmethod < 0 || grabmethod > 1)
    grabmethod = 1;		//mmap by default;
  vd->videodevice = NULL;
  vd->status = NULL;
  vd->pictName = NULL;
  vd->videodevice = (char *) calloc (1, 16 * sizeof (char));
  vd->status = (char *) calloc (1, 100 * sizeof (char));
  vd->pictName = (char *) calloc (1, 80 * sizeof (char));
  snprintf (vd->videodevice, 12, "%s", device);
  vd->toggleAvi = 0;
  vd->getPict = 0;
  vd->signalquit = 1;
  vd->width = width;
  vd->height = height;
  vd->fps = fps;
  vd->formatIn = format;
  vd->grabmethod = grabmethod;
  if (init_v4l2 (vd) < 0) {
    fprintf (stderr, " Init v4L2 failed !! exit fatal \n");
    goto error;;
  }
  /* alloc a temp buffer to reconstruct the pict */
  vd->framesizeIn = (vd->width * vd->height << 1);
  switch (vd->formatIn) {
  case V4L2_PIX_FMT_MJPEG:
    vd->tmpbuffer = (unsigned char *) calloc(1, (size_t) vd->framesizeIn);
    if (!vd->tmpbuffer)
      goto error;
    vd->framebuffer =
        (unsigned char *) calloc(1, (size_t) vd->width * (vd->height + 8) * 2);
    break;
  case V4L2_PIX_FMT_YUYV:
    vd->framebuffer =
        (unsigned char *) calloc(1, (size_t) vd->framesizeIn);
    break;
  default:
    fprintf(stderr, " should never arrive exit fatal !!\n");
    goto error;
    break;
  }
  if (!vd->framebuffer)
    goto error;
  return 0;
error:
  free(vd->videodevice);
  free(vd->status);
  free(vd->pictName);
  close(vd->fd);
  return -1;
}

在init_v4l2 函数中,对摄像头进行初始化

static int init_v4l2(struct vdIn *vd)
{
  int i;
  int ret = 0;
  //打开摄像头
  if ((vd->fd = open(vd->videodevice, O_RDWR)) == -1) {
    perror("ERROR opening V4L interface");
    return -1;
  }
  //查询摄像头功能,是否是视频捕获装置
  memset(&vd->cap, 0, sizeof(struct v4l2_capability));
  ret = ioctl(vd->fd, VIDIOC_QUERYCAP, &vd->cap);
  if (ret < 0) {
    fprintf(stderr, "Error opening device %s: unable to query device.\n", vd->videodevice);
    goto fatal;
  }
  
  if ((vd->cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0) {
    fprintf(stderr, "Error opening device %s: video capture not supported.\n",
           vd->videodevice);
    goto fatal;;
  }
 //判断是否是否支持该种数据传输方式
  if (vd->grabmethod) {
    if (!(vd->cap.capabilities & V4L2_CAP_STREAMING)) {		//流传输
      fprintf(stderr, "%s does not support streaming i/o\n", vd->videodevice);
      goto fatal;
    }
  } else {
    if (!(vd->cap.capabilities & V4L2_CAP_READWRITE)) {		//读写传输
      fprintf(stderr, "%s does not support read i/o\n", vd->videodevice);
      goto fatal;
    }
  }

  /*
   * set format in
   * 设置摄像头输出图片的格式,宽,高
   */
  memset(&vd->fmt, 0, sizeof(struct v4l2_format));
  vd->fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  vd->fmt.fmt.pix.width = vd->width;
  vd->fmt.fmt.pix.height = vd->height;
  vd->fmt.fmt.pix.pixelformat = vd->formatIn;
  vd->fmt.fmt.pix.field = V4L2_FIELD_ANY;
  ret = ioctl(vd->fd, VIDIOC_S_FMT, &vd->fmt);
  if (ret < 0) {
    perror("Unable to set format");
    goto fatal;
  }
  //查询是否设置成功
  if ((vd->fmt.fmt.pix.width != vd->width) ||
      (vd->fmt.fmt.pix.height != vd->height)) {
    fprintf(stderr, " format asked unavailable get width %d height %d \n", vd->fmt.fmt.pix.width, vd->fmt.fmt.pix.height);
    vd->width = vd->fmt.fmt.pix.width;
    vd->height = vd->fmt.fmt.pix.height;
    /*
     * look the format is not part of the deal ???
     */
    // vd->formatIn = vd->fmt.fmt.pix.pixelformat;
  }

  /*
   * set framerate
   * 设置摄像头的帧率
   */
  struct v4l2_streamparm *setfps;
  setfps = (struct v4l2_streamparm *) calloc(1, sizeof(struct v4l2_streamparm));
  memset(setfps, 0, sizeof(struct v4l2_streamparm));
  setfps->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  setfps->parm.capture.timeperframe.numerator = 1;
  setfps->parm.capture.timeperframe.denominator = vd->fps;
  ret = ioctl(vd->fd, VIDIOC_S_PARM, setfps);

  /*
   * request buffers
   * 申请缓冲区
   */
  memset(&vd->rb, 0, sizeof(struct v4l2_requestbuffers));
  vd->rb.count = NB_BUFFER;
  vd->rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  vd->rb.memory = V4L2_MEMORY_MMAP;

  ret = ioctl(vd->fd, VIDIOC_REQBUFS, &vd->rb);
  if (ret < 0) {
    perror("Unable to allocate buffers");
    goto fatal;
  }

  /*
   * map the buffers
   * Mmap映射缓冲区
   */
  for (i = 0; i < NB_BUFFER; i++) {
    memset(&vd->buf, 0, sizeof(struct v4l2_buffer));
    vd->buf.index = i;
    vd->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vd->buf.memory = V4L2_MEMORY_MMAP;
    ret = ioctl(vd->fd, VIDIOC_QUERYBUF, &vd->buf);
    if (ret < 0) {
      perror("Unable to query buffer");
      goto fatal;
    }

    if (debug)
      fprintf(stderr, "length: %u offset: %u\n", vd->buf.length, vd->buf.m.offset);

    vd->mem[i] = mmap(0 /* start anywhere */ ,
                      vd->buf.length, PROT_READ, MAP_SHARED, vd->fd,
                      vd->buf.m.offset);
    if (vd->mem[i] == MAP_FAILED) {
      perror("Unable to map buffer");
      goto fatal;
    }
    if (debug)
      fprintf(stderr, "Buffer mapped at address %p.\n", vd->mem[i]);
  }

  /*
   * Queue the buffers.
   * 将缓冲区入队
   */
  for (i = 0; i < NB_BUFFER; ++i) {
    memset(&vd->buf, 0, sizeof(struct v4l2_buffer));
    vd->buf.index = i;
    vd->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vd->buf.memory = V4L2_MEMORY_MMAP;
    ret = ioctl(vd->fd, VIDIOC_QBUF, &vd->buf);
    if (ret < 0) {
      perror("Unable to queue buffer");
      goto fatal;;
    }
  }
  return 0;
fatal:
  return -1;

}
//开始采集
static int video_enable(struct vdIn *vd)
{
  int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  int ret;

  ret = ioctl(vd->fd, VIDIOC_STREAMON, &type);
  if (ret < 0) {
    perror("Unable to start capture");
    return ret;
  }
  vd->isstreaming = 1;
  return 0;
}
//停止采集
static int video_disable(struct vdIn *vd)
{
  int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  int ret;

  ret = ioctl(vd->fd, VIDIOC_STREAMOFF, &type);
  if (ret < 0) {
    perror("Unable to stop capture");
    return ret;
  }
  vd->isstreaming = 0;
  return 0;
}
...
...

后面当main函数执行完output_init函数进行图像采集时将调用init_run函数

int input_run(void) {
  pglobal->buf = malloc(videoIn->framesizeIn);
  if (pglobal->buf == NULL) {
    fprintf(stderr, "could not allocate memory\n");
    exit(EXIT_FAILURE);
  }
  //创建一个线程cam_thread来处理数据采集
  pthread_create(&cam, 0, cam_thread, NULL);
  pthread_detach(cam);

  return 0;
}

cam_thread:

void *cam_thread( void *arg ) {
  /* set cleanup handler to cleanup allocated ressources
   * 将清除处理程序设置为清除已分配的资源
   */
  pthread_cleanup_push(cam_cleanup, NULL);

  while( !pglobal->stop ) {

    /* grab a frame */
    //捕获一帧图像数据
    if( uvcGrab(videoIn) < 0 ) {
      IPRINT("Error grabbing frames\n");
      exit(EXIT_FAILURE);
    }
  
    DBG("received frame of size: %d\n", videoIn->buf.bytesused);

    /*
     * Workaround for broken, corrupted frames:
     * Under low light conditions corrupted frames may get captured.
     * The good thing is such frames are quite small compared to the regular pictures.
     * For example a VGA (640x480) webcam picture is normally >= 8kByte large,
     * corrupted frames are smaller.
     */
    if ( videoIn->buf.bytesused < minimum_size ) {
      DBG("dropping too small frame, assuming it as broken\n");
      continue;
    }

    /* copy JPG picture to global buffer */
    //将捕获到JPG图片复制到pglobal的buf中
    pthread_mutex_lock( &pglobal->db );

    /*
     * If capturing in YUV mode convert to JPEG now.
     * This compression requires many CPU cycles, so try to avoid YUV format.
     * Getting JPEGs straight from the webcam, is one of the major advantages of
     * Linux-UVC compatible devices.
     */
    if (videoIn->formatIn == V4L2_PIX_FMT_YUYV) {
      DBG("compressing frame\n");
      pglobal->size = compress_yuyv_to_jpeg(videoIn, pglobal->buf, videoIn->framesizeIn, gquality);
    }
    else {
      DBG("copying frame\n");
      pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused);
    }

#if 0
    /* motion detection can be done just by comparing the picture size, but it is not very accurate!! */
    //运动检测
    if ( (prev_size - global->size)*(prev_size - global->size) > 4*1024*1024 ) {
        DBG("motion detected (delta: %d kB)\n", (prev_size - global->size) / 1024);
    }
    prev_size = global->size;
#endif

    /* signal fresh_frame */
    //通知输出通道数据获取完毕
    pthread_cond_broadcast(&pglobal->db_update);
    pthread_mutex_unlock( &pglobal->db );

    DBG("waiting for next frame\n");

    /* only use usleep if the fps is below 5, otherwise the overhead is too long */
    //只有当fps小于5时使用usleep
    if ( videoIn->fps < 5 ) {
      usleep(1000*1000/videoIn->fps);
    }
  }

  DBG("leaving input thread, calling cleanup function now\n");
  pthread_cleanup_pop(1);

  return NULL;
}

(2)客户端接收程序
当我们指定mjpg_streamer的输出方式为output_http时,开发板采集到的摄像头数据后通过http协议发送数据。
我们先分析下输出通道,主要是output_init函数和output_run函数
当我们指定output_http时,将调用plugins\output_http\output_http.c中的output_init函数,如下所示

context servers[MAX_OUTPUT_PLUGINS];
...
int output_init(output_parameter *param) {
  char *argv[MAX_ARGUMENTS]={NULL};
  int  argc=1, i;
  int  port;
  char *credentials, *www_folder;
  char nocommands;

  DBG("output #%02d\n", param->id);
  //指定固定端口号8080
  port = htons(8080);
  credentials = NULL;
  www_folder = NULL;
  nocommands = 0;

  /* convert the single parameter-string to an array of strings */
  argv[0] = OUTPUT_PLUGIN_NAME;
  if ( param->parameter_string != NULL && strlen(param->parameter_string) != 0 ) {
    char *arg=NULL, *saveptr=NULL, *token=NULL;

    arg=(char *)strdup(param->parameter_string);

    if ( strchr(arg, ' ') != NULL ) {
      token=strtok_r(arg, " ", &saveptr);
      if ( token != NULL ) {
        argv[argc] = strdup(token);
        argc++;
        while ( (token=strtok_r(NULL, " ", &saveptr)) != NULL ) {
          argv[argc] = strdup(token);
          argc++;
          if (argc >= MAX_ARGUMENTS) {
            OPRINT("ERROR: too many arguments to output plugin\n");
            return 1;
          }
        }
      }
    }
  }

  /* show all parameters for DBG purposes */
  //打印所有参数
  for (i=0; i<argc; i++) {
    DBG("argv[%d]=%s\n", i, argv[i]);
  }
  //解析参数
  reset_getopt();
  while(1) {
    int option_index = 0, c=0;
    static struct option long_options[] = \
    {
      {"h", no_argument, 0, 0},
      {"help", no_argument, 0, 0},
      {"p", required_argument, 0, 0},
      {"port", required_argument, 0, 0},
      {"c", required_argument, 0, 0},
      {"credentials", required_argument, 0, 0},
      {"w", required_argument, 0, 0},
      {"www", required_argument, 0, 0},
      {"n", no_argument, 0, 0},
      {"nocommands", no_argument, 0, 0},
      {0, 0, 0, 0}
    };

    c = getopt_long_only(argc, argv, "", long_options, &option_index);

    /* no more options to parse */
    //参数解析完毕
    if (c == -1) break;

    /* unrecognized option */
    //输入参数无法识别,返回?,打印帮助信息
    if (c == '?'){
      help();
      return 1;
    }

    switch (option_index) {
      /* h, help */
      case 0:
      case 1:
        DBG("case 0,1\n");
        help();
        return 1;
        break;

      /* p, port */
      case 2:
      case 3:
        DBG("case 2,3\n");
        port = htons(atoi(optarg));
        break;

      /* c, credentials */
      case 4:
      case 5:
        DBG("case 4,5\n");
        credentials = strdup(optarg);
        break;

      /* w, www */
      case 6:
      case 7:
        DBG("case 6,7\n");
        www_folder = malloc(strlen(optarg)+2);
        strcpy(www_folder, optarg);
        if ( optarg[strlen(optarg)-1] != '/' )
          strcat(www_folder, "/");
        break;

      /* n, nocommands */
      case 8:
      case 9:
        DBG("case 8,9\n");
        nocommands = 1;
        break;
    }
  }
  //初始化servers成员变量
  servers[param->id].id = param->id;
  servers[param->id].pglobal = param->global;
  servers[param->id].conf.port = port;
  servers[param->id].conf.credentials = credentials;
  servers[param->id].conf.www_folder = www_folder;
  servers[param->id].conf.nocommands = nocommands;

  OPRINT("www-folder-path...: %s\n", (www_folder==NULL)?"disabled":www_folder);
  OPRINT("HTTP TCP port.....: %d\n", ntohs(port));
  OPRINT("username:password.: %s\n", (credentials==NULL)?"disabled":credentials);
  OPRINT("commands..........: %s\n", (nocommands)?"disabled":"enabled");

  return 0;
}

output_run函数如下所示

int output_run(int id) {
  DBG("launching server thread #%02d\n", id);

  /* create thread and pass context to thread function */
  //创建线程来处理数据输出
  pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));
  pthread_detach(servers[id].threadID);

  return 0;
}

server_thread在httpd.c中:

void *server_thread( void *arg ) {
  struct sockaddr_in addr, client_addr;
  int on;
  pthread_t client;
  socklen_t addr_len = sizeof(struct sockaddr_in);

  context *pcontext = arg;
  pglobal = pcontext->pglobal;

  /* set cleanup handler to cleanup ressources */
  pthread_cleanup_push(server_cleanup, pcontext);

  /* open socket for server */
  //创建socket
  pcontext->sd = socket(PF_INET, SOCK_STREAM, 0);
  if ( pcontext->sd < 0 ) {
    fprintf(stderr, "socket failed\n");
    exit(EXIT_FAILURE);
  }

  /* ignore "socket already in use" errors */
  //设置端口复用
  on = 1;
  if (setsockopt(pcontext->sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
    perror("setsockopt(SO_REUSEADDR) failed");
    exit(EXIT_FAILURE);
  }

  /* perhaps we will use this keep-alive feature oneday */
  /* setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); */

  /* configure server address to listen to all local IPs */
  //绑定套接字
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = pcontext->conf.port; /* is already in right byteorder */
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  if ( bind(pcontext->sd, (struct sockaddr*)&addr, sizeof(addr)) != 0 ) {
    perror("bind");
    OPRINT("%s(): bind(%d) failed", __FUNCTION__, htons(pcontext->conf.port));
    closelog();
    exit(EXIT_FAILURE);
  }

  /* start listening on socket */
  //设置监听最大数
  if ( listen(pcontext->sd, 10) != 0 ) {
    fprintf(stderr, "listen failed\n");
    exit(EXIT_FAILURE);
  }

  /* create a child for every client that connects */
  //为每一个连接的客户端创建一个子线程来处理
  while ( !pglobal->stop ) {
    //int *pfd = (int *)malloc(sizeof(int));
    cfd *pcfd = malloc(sizeof(cfd));

    if (pcfd == NULL) {
      fprintf(stderr, "failed to allocate (a very small amount of) memory\n");
      exit(EXIT_FAILURE);
    }
	//监听套接字
    DBG("waiting for clients to connect\n");
    pcfd->fd = accept(pcontext->sd, (struct sockaddr *)&client_addr, &addr_len);
    pcfd->pc = pcontext;

    /* start new thread that will handle this TCP connected client */
    DBG("create thread to handle client that just established a connection\n");
    syslog(LOG_INFO, "serving client: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
   //创建子线程
    if( pthread_create(&client, NULL, &client_thread, pcfd) != 0 ) {
      DBG("could not launch another client thread\n");
      close(pcfd->fd);
      free(pcfd);
      continue;
    }
    pthread_detach(client);
  }

  DBG("leaving server thread, calling cleanup function now\n");
  pthread_cleanup_pop(1);

  return NULL;
}

client_thread:

void *client_thread( void *arg ) {
  int cnt;
  char buffer[BUFFER_SIZE]={0}, *pb=buffer;
  iobuffer iobuf;
  request req;
  cfd lcfd; /* local-connected-file-descriptor */

  /* we really need the fildescriptor and it must be freeable by us */
  //如果我们传入的参数不为空,则将参数的内容拷贝到lcfd中(参数为pcfd,不为空)
  if (arg != NULL) {
    memcpy(&lcfd, arg, sizeof(cfd));
    free(arg);
  }
  else
    return NULL;

  /* initializes the structures */
  init_iobuffer(&iobuf);	
  init_request(&req);	//http协议需要客户端发送一个请求给服务器,而req即是这个请求

  /* What does the client want to receive? Read the request. */
  //从客户端读一行数据,遇到'\n'停止,表示客户端发来的请求,才知道给客户端发什么数据
  memset(buffer, 0, sizeof(buffer));
  if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) {
    close(lcfd.fd);
    return NULL;
  }

  /* determine what to deliver */
  //确定请求类型
  if ( strstr(buffer, "GET /?action=snapshot") != NULL ) {
    req.type = A_SNAPSHOT;
  }
  else if ( strstr(buffer, "GET /?action=stream") != NULL ) {
    req.type = A_STREAM;
  }
  else if ( strstr(buffer, "GET /?action=command") != NULL ) {
    int len;
    req.type = A_COMMAND;

    /* advance by the length of known string */
    if ( (pb = strstr(buffer, "GET /?action=command")) == NULL ) {
      DBG("HTTP request seems to be malformed\n");
      send_error(lcfd.fd, 400, "Malformed HTTP request");
      close(lcfd.fd);
      return NULL;
    }
    pb += strlen("GET /?action=command");

    /* only accept certain characters */
    len = MIN(MAX(strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-=&1234567890"), 0), 100);
    req.parameter = malloc(len+1);
    if ( req.parameter == NULL ) {
      exit(EXIT_FAILURE);
    }
    memset(req.parameter, 0, len+1);
    strncpy(req.parameter, pb, len);

    DBG("command parameter (len: %d): \"%s\"\n", len, req.parameter);
  }
  else {
    int len;

    DBG("try to serve a file\n");
    req.type = A_FILE;

    if ( (pb = strstr(buffer, "GET /")) == NULL ) {
      DBG("HTTP request seems to be malformed\n");
      send_error(lcfd.fd, 400, "Malformed HTTP request");
      close(lcfd.fd);
      return NULL;
    }

    pb += strlen("GET /");
    len = MIN(MAX(strspn(pb, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-1234567890"), 0), 100);
    req.parameter = malloc(len+1);
    if ( req.parameter == NULL ) {
      exit(EXIT_FAILURE);
    }
    memset(req.parameter, 0, len+1);
    strncpy(req.parameter, pb, len);

    DBG("parameter (len: %d): \"%s\"\n", len, req.parameter);
  }

  /*
   * parse the rest of the HTTP-request
   * the end of the request-header is marked by a single, empty line with "\r\n"
   */
   //解析后面的http请求
  do {
    //客户端必须再发送一次字符串,如果有用户名和密码则发送用户名和密码,否则发送cnt <= 2 或 (buffer[0] == '\r' || buffer[1] == '\n')的字符串,如"f\n"
    memset(buffer, 0, sizeof(buffer));

    if ( (cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer)-1, 5)) == -1 ) {
      free_request(&req);
      close(lcfd.fd);
      return NULL;
    }
    //客户端发送了用户名,则将用户名保存到req.client中
    if ( strstr(buffer, "User-Agent: ") != NULL ) {
      req.client = strdup(buffer+strlen("User-Agent: "));
    }
    else if ( strstr(buffer, "Authorization: Basic ") != NULL ) {
    //如果有密码,则将密码保存到req.credentials中
      req.credentials = strdup(buffer+strlen("Authorization: Basic "));
      decodeBase64(req.credentials);	//对密码进行解码
      DBG("username:password: %s\n", req.credentials);
    }

  } while( cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n') );

  /* check for username and password if parameter -c was given */
  //如果支持密码功能,则检查用户名和密码是否匹配
  if ( lcfd.pc->conf.credentials != NULL ) {
    if ( req.credentials == NULL || strcmp(lcfd.pc->conf.credentials, req.credentials) != 0 ) {
      DBG("access denied\n");
      send_error(lcfd.fd, 401, "username and password do not match to configuration");
      close(lcfd.fd);
      if ( req.parameter != NULL ) free(req.parameter);
      if ( req.client != NULL ) free(req.client);
      if ( req.credentials != NULL ) free(req.credentials);
      return NULL;
    }
    DBG("access granted\n");
  }

  /* now it's time to answer */
  //根据请求类型,采取相应行动
  switch ( req.type ) {
    case A_SNAPSHOT:
      DBG("Request for snapshot\n");
      send_snapshot(lcfd.fd);
      break;
    case A_STREAM:
      DBG("Request for stream\n");
      send_stream(lcfd.fd);
      break;
    case A_COMMAND:
      if ( lcfd.pc->conf.nocommands ) {
        send_error(lcfd.fd, 501, "this server is configured to not accept commands");
        break;
      }
      command(lcfd.pc->id, lcfd.fd, req.parameter);
      break;
    case A_FILE:
      if ( lcfd.pc->conf.www_folder == NULL )
        send_error(lcfd.fd, 501, "no www-folder configured");
      else
        send_file(lcfd.pc->id, lcfd.fd, req.parameter);
      break;
    default:
      DBG("unknown request\n");
  }

  close(lcfd.fd);
  free_request(&req);

  DBG("leaving HTTP client thread\n");
  return NULL;
}

后面对send_stream函数进行简单的分析:

void send_stream(int fd) {
  unsigned char *frame=NULL, *tmp=NULL;
  int frame_size=0, max_frame_size=0;
  char buffer[BUFFER_SIZE] = {0};

  DBG("preparing header\n");
  //将http回应报文写入buffer中,并发送出去
  sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
                  STD_HEADER \
                  "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
                  "\r\n" \
                  "--" BOUNDARY "\r\n");
                  
  if ( write(fd, buffer, strlen(buffer)) < 0 ) {
    free(frame);
    return;
  }

  DBG("Headers send, sending stream now\n");

  while ( !pglobal->stop ) {

    /* wait for fresh frames */
    //等待输入通道更新数据
    pthread_cond_wait(&pglobal->db_update, &pglobal->db);

    /* read buffer */
    frame_size = pglobal->size;

    /* check if framebuffer is large enough, increase it if necessary */
    //如果一帧数据大于分配的大小,则重新分配足够的空间
    if ( frame_size > max_frame_size ) {
      DBG("increasing buffer size to %d\n", frame_size);

      max_frame_size = frame_size+TEN_K;
      if ( (tmp = realloc(frame, max_frame_size)) == NULL ) {
        free(frame);
        pthread_mutex_unlock( &pglobal->db );
        send_error(fd, 500, "not enough memory");
        return;
      }

      frame = tmp;
    }

    memcpy(frame, pglobal->buf, frame_size);
    DBG("got frame (size: %d kB)\n", frame_size/1024);

    pthread_mutex_unlock( &pglobal->db );

    /*
     * print the individual mimetype and the length
     * sending the content-length fixes random stream disruption observed
     * with firefox
     */
     //在发送一帧数据前,先告诉客户端数据的大小
    sprintf(buffer, "Content-Type: image/jpeg\r\n" \
                    "Content-Length: %d\r\n" \
                    "\r\n", frame_size);
    DBG("sending intemdiate header\n");
    //发送一帧数据
    if ( write(fd, buffer, strlen(buffer)) < 0 ) break;

    DBG("sending frame\n");
    if( write(fd, frame, frame_size) < 0 ) break;

    DBG("sending boundary\n");
    sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    if ( write(fd, buffer, strlen(buffer)) < 0 ) break;
  }

  free(frame);
}

这样,mjpg-streamer采集发送数据的流程我们算是基本弄明白了,所以后面编写客户端程序,只需参照相应的源码文件按照http协议接受数据即可。接收完数据后我们再将它转换为RGB格式在PC上显示。具体的实现细节就在另一篇博客上写吧。

猜你喜欢

转载自blog.csdn.net/ChenNightZ/article/details/108168109