天嵌 ARM开发社区

 找回密码
 注册
查看: 10543|回复: 14

近日无聊分析一下mjpg-streamer源码,记录过程

[复制链接]
yuki 发表于 2013-8-24 13:44:16 | 显示全部楼层 |阅读模式
本帖最后由 yuki 于 2013-8-28 12:25 编辑

声明:本帖为无聊所做,自娱自乐,非严谨技术文,勿喷!高手请绕道,如不慎进入,请点击右上角叉叉,默默退出。另,内容非一次性完成,无耐心者请勿围观。

        近日,在网上买了几个设备,等了几天都没来,闲来无聊,蛋疼!为了打发时间,排解心中空虚寂寞,分析一下mjpg-streamer源码,以舒缓心中压抑之情。
        闲话少说,先上道具:
        1.mjpg-streamer源码一份,由大公无私的天嵌公司提供。
        2.《Makefile编程.pdf》一份,用来瞻仰Makefile优美的语法。为徐海兵老前辈翻译整理,造福我等英文不过关的小辈。
        3.《gcc_4.6_官方手册.pdf》一份,用来查询GCC庞杂的选项。为英文,但较为齐全,将就着用,必要时带上有道字典,以备不时之需。
        4.man page一份,由linux系统提供,例如,在中端输入man dlopen,即可将dlopen函数用法都查询出来。
        5.source Insight 3.5源码查看工具一份,感谢该工具作者及推广者,解放了我们的生产力。

        本文从WebCam\mjpg-streamer\mjpg-streamer目录下的Makefile开始。
 楼主| yuki 发表于 2013-8-27 11:12:22 | 显示全部楼层
本帖最后由 yuki 于 2013-8-28 12:20 编辑

        网上看到一位前辈的博客,关于mjpg-streamer分析,有一个流程图画得很清晰,所以引用一下(辨别不出原作者是谁)。直接贴图,发现很模糊,之后作为附件上传:mjpg-streamer流程.jpg。这个流程图从开始到打开动态链接库到图像获取再到网络传输等相关步骤一些关键的函数都标得很清楚。
        但也许是关注角度不同,也有一些函数是被忽略掉的,个人认为,这些函数对整个代码的理解也是有很大帮助的,也有必要把这些函数理一下。下面以流程图为主,穿插一些个人认为比较重要的函数说明一下。首先要先把以下几个函数理解透彻,才能往下分析:
1.getopt_long_only:这个函数是用来分析命令行参数的。
2.dlopen:用来打开动态链接库的。
3.dlsym:用来获取动态链接库指定函数的地址,注意不是调用函数,调用和其它指针函数调用一样。这个函数和dlopen配合使用。
        对比了多个资料说明,发现man page说明是最详尽的,这里不一一列举。这几个函数后面有好几个地方用到。

1.getopt_long_only函数
main函数一开始就是命令行分析,主要是在while(1)循环完成(177到241行),这里不是死循环,它是遇到-1就跳出循环。194到197行有一下代码:
    c = getopt_long_only(argc, argv, "", long_options, &option_index);    //  1.解析命令行 argv,返回参数项?,参数标号写入option_index,解析完成时返回-1
    /* no more options to parse */
    if (c == -1) break;
        从注解可以看出,-1有getopt_long_only函数提供。
        整个while(1)的允许机制就是,在循环里面使用getopt_long_only获取命令行数组argv的参数项,每次获取一个,存放到合适的地方。一直到命令行分析完毕,返回-1,跳出while(1)。保存参数项在213行和219行完成:
       213行: input = strdup(optarg);            //命令行解析到的input的参数保存到input字符串中
       219行:output[global.outcnt++] = strdup(optarg);       //命令行解析到的output参数保存到output数组

2.dlopen函数
位置:这个是系统函数
功能:打开动态链接库

3.dlsym函数
位置:这个是系统函数
功能:获取动态链接库中函数地址。注意这里并没有调用函数。

        往下分析,代码就比较清晰了,以注解说明一下:
293行:/* open input plugin */
  tmp = (size_t)(strchr(input, ' ')-input);      // 获取第一个空格在字符串input的位置
  global.in.plugin = (tmp > 0)?strndup(input, tmp):strdup(input);
  global.in.handle = dlopen(global.in.plugin, RTLD_LAZY);  // 2.打开input_uvc.so动态链接库
  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 = dlsym(global.in.handle, "input_init");   // 3.获取input_uvc.so中?input_init函数地址
  if ( global.in.init == NULL ) {
    LOG("%s\n", dlerror());
    exit(EXIT_FAILURE);
  }
  global.in.stop = dlsym(global.in.handle, "input_stop");   // 4.获取input_uvc.so中?input_stop函数地址
  if ( global.in.stop == NULL ) {
    LOG("%s\n", dlerror());
    exit(EXIT_FAILURE);
  }
  global.in.run = dlsym(global.in.handle, "input_run");   // 5.获取input_uvc.so中?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");   // 6.获取input_uvc.so中?input_cmd函数地址
  global.in.param.parameter_string = strchr(input, ' ');   //7.获取input_init函数输入参数param的parameter
  global.in.param.global = &global;       //7.获取input_init函数输入参数param的global
  if ( global.in.init(&global.in.param) ) {      //7.调用input_init函数
    LOG("input_init() return value signals to exit");
    closelog();
    exit(0);
  }
  /* open output plugin */
  for (i=0; i<global.outcnt; i++) {
    tmp = (size_t)(strchr(output, ' ')-output);
    global.out.plugin = (tmp > 0)?strndup(output, tmp):strdup(output);
    global.out.handle = dlopen(global.out.plugin, RTLD_LAZY);    //8.打开动态库output_uvc.so
    if ( !global.out.handle ) {
      LOG("ERROR: could not find output plugin %s\n", global.out.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.init = dlsym(global.out.handle, "output_init");    //9.获取output_init函数地址
    if ( global.out.init == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    global.out.stop = dlsym(global.out.handle, "output_stop");    //10.获取output_stop函数地址
    if ( global.out.stop == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    global.out.run = dlsym(global.out.handle, "output_run");    //11.获取output_run函数地址
    if ( global.out.run == NULL ) {
      LOG("%s\n", dlerror());
      exit(EXIT_FAILURE);
    }
    /* try to find optional command */
    global.out.cmd = dlsym(global.out.handle, "output_cmd");    //12.获取output_cmd函数地址
    global.out.param.parameter_string = strchr(output, ' ');
    global.out.param.global = &global;
    global.out.param.id = i;
    if ( global.out.init(&global.out.param) ) {       //13.调用output_init函数
      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");
  if ( global.in.run() ) {             //14.调用input_run函数
    LOG("can not run input plugin\n");
    closelog();
    return 1;
  }
  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.plugin, global.out.param.id);
    global.out.run(global.out.param.id);        //15.调用output_run函数
  }
  /* wait for signals */
389行:  pause();               //16.调用pause暂停
        至此mjpg-streamer.c的代码就分析完了,要特别注意一下global结构体变量的数据,global一直贯穿着整个代码。属于数据流驱动,它的值不理解,后面很多地方都会莫名其妙。再次感叹一下:数据结构是多么的重要啊!
        其它地方配合一下流程图还是很好理解的。



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
回复 支持 1 反对 0

使用道具 举报

 楼主| yuki 发表于 2013-8-24 15:15:37 | 显示全部楼层
从Makefile文件众多字符中,找到了第一个冒号,即第一个目标,在文件49行,如下:
all: application plugins
可以看出all依赖于application plugins,分别找到application和plugins,在51和53行:
application: $(APP_BINARY)
plugins: $(PLUGINS)
这2分别依赖$(APP_BINARY)和$(PLUGINS),都是变量,往回看,看到含义如下:
APP_BINARY = mjpg_streamer
# define the names and targets of the plugins
PLUGINS = input_uvc.so
PLUGINS += output_file.so
PLUGINS += output_http.so
PLUGINS += input_testpicture.so
PLUGINS += output_autofocus.so
PLUGINS += input_gspcav1.so
PLUGINS += input_file.so
PLUGINS += input_control.so
PLUGINS += input_cmoscamera.so
# PLUGINS += input_http.so
PLUGINS += output_viewer.so
# define the names of object files
OBJECTS=mjpg_streamer.o utils.o

继续往下看发现: $(APP_BINARY)即mjpg_streamer,在55行生成,如下
$(APP_BINARY): mjpg_streamer.c mjpg_streamer.h mjpg_streamer.o utils.c utils.h utils.o
$(CC) $(CFLAGS) $(LFLAGS) $(OBJECTS) -o $(APP_BINARY)

将下面一句代入变量展开就是
arm-linux-gcc -O2 -DLINUX -D_GNU_SOURCE -Wall -lpthread -ldl mjpg_streamer.o utils.o -o mjpg_streamer
《gcc_4.6_官方手册.pdf》终于可以派上用场了,赶紧查一下。
另外,mjpg_streamer.o utils.o这2怎么来的呢?
原来Makefile有隐含规则这种概念,mjpg_streamer.o utils.o是根据隐含规则自动产生的,来源就是mjpg_streamer.c mjpg_streamer.h utils.c utils.h这4个。
        至此,第1、2个要分析的源文件就出来了:
1.mjpg_streamer.c
2.utils.c

回头分析plugins这一支,
很容易跟踪到:
output_autofocus.so: mjpg_streamer.h utils.h
make -C plugins/output_autofocus all
cp plugins/output_autofocus/output_autofocus.so .
input_testpicture.so: mjpg_streamer.h utils.h
make -C plugins/input_testpicture all
cp plugins/input_testpicture/input_testpicture.so .
input_uvc.so: mjpg_streamer.h utils.h
make -C plugins/input_uvc all
cp plugins/input_uvc/input_uvc.so .
input_control.so: mjpg_streamer.h utils.h
make -C plugins/input_control all
cp plugins/input_control/input_control.so .
input_cmoscamera.so: mjpg_streamer.h utils.h
make -C plugins/input_cmoscamera all
cp plugins/input_cmoscamera/input_cmoscamera.so .
output_file.so: mjpg_streamer.h utils.h
make -C plugins/output_file all
cp plugins/output_file/output_file.so .
output_http.so: mjpg_streamer.h utils.h
make -C plugins/output_http all
cp plugins/output_http/output_http.so .
input_gspcav1.so: mjpg_streamer.h utils.h
make -C plugins/input_gspcav1 all
cp plugins/input_gspcav1/input_gspcav1.so .
input_file.so: mjpg_streamer.h utils.h
make -C plugins/input_file all
cp plugins/input_file/input_file.so .

只对其中一个进行分析,69行:
input_uvc.so: mjpg_streamer.h utils.h
make -C plugins/input_uvc all
cp plugins/input_uvc/input_uvc.so .


make -C plugins/input_uvc all意思是到plugins/input_uvc 下找Makefile文件,然后根据Makefile编译。
这里不再把Makefile列出来,结果是,又找到了4个要分析的源文件:
3.input_uvc.c
4.v4l2uvc.c
5.jpeg_utils.c
6.dynctrl.c
好困~~~~,鼠标又没电了,睡觉去。














 楼主| yuki 发表于 2013-8-25 12:57:51 | 显示全部楼层
        接着开始分析mjpg_streamer.cmain函数就在里面。
        在main函数里面,已开始就能看到有个global变量,在这里要再次感谢source Insight让我们可以很快定位到定义:      
struct _globals {
  int stop;
  /* signal fresh frames */
  pthread_mutex_t db;
  pthread_cond_t  db_update;
  /* global JPG frame, this is more or less the "database" */
  unsigned char *buf;
  int size;
  /* input plugin */
  input in;
  /* output plugin */
  output out[MAX_OUTPUT_PLUGINS];
  int outcnt;
  /* pointer to control functions */
  int (*control)(int command, char *details);
}
_globals结构里面还有pthread_mutex_t, pthread_cond_t, input, output, input_parameter, output_parameter
这几个结构依次列一下:
//pthread_mutex_t
typedef union
{
  struct __pthread_mutex_s
  {
    int __lock;
    unsigned int __count;
    int __owner;
    /* KIND must stay at this position in the structure to maintain
       binary compatibility.  */
    int __kind;
    unsigned int __nusers;
    __extension__ union
    {
      int __spins;
      __pthread_slist_t __list;
    };
  } __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;

//pthread_cond_t
typedef union
{
  struct
  {
    int __lock;
    unsigned int __futex;
    __extension__ unsigned long long int __total_seq;
    __extension__ unsigned long long int __wakeup_seq;
    __extension__ unsigned long long int __woken_seq;
    void *__mutex;
    unsigned int __nwaiters;
    unsigned int __broadcast_seq;
  } __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;

//input
struct _input {
  char *plugin;
  void *handle;
  input_parameter param;
  int (*init)(input_parameter *);
  int (*stop)(void);
  int (*run)(void);
  int (*cmd)(in_cmd_type, int);
};

//output
struct _output {
  char *plugin;
  void *handle;
  output_parameter param;
  int (*init)(output_parameter *);
  int (*stop)(int);
  int (*run)(int);
  int (*cmd)(int, out_cmd_type, int);
};

//input_parameter
struct _input_parameter {
  char *parameter_string;
  struct _globals *global;
};

//output_parameter
struct _output_parameter {
  int id;
  char *parameter_string;
  struct _globals *global;
};

再深层里面的结构不一一列举。

另外还有一个结构option,后面解析命令用到:
struct option {const char *name;int has_arg;int *flag;int val;};
好不容易,数据结构列举完了,分析到这里,发觉自己的数据结构知识又要恶补了。C语言编程就是和数据结构打交道。
吃饭去!

 楼主| yuki 发表于 2013-8-27 17:50:57 | 显示全部楼层
本帖最后由 yuki 于 2013-8-28 12:11 编辑

        一个回复都没有,看来没人喜欢这个话题啊。赶紧草草了事,结贴走人!说笑,闲话少少讲,继续。
        终于轮到input_init函数了。为啥是input_init,认真看过上面的代码注释就肯定知道。
4.input_init函数
        input_init函数是在plugins\input_uvc.c里面。
        函数一开始就是初始化互斥锁:
  /* initialize the mutes variable */
  if( pthread_mutex_init(&controls_mutex, NULL) != 0 ) {
    IPRINT("could not initialize mutex variable\n");
    exit(EXIT_FAILURE);
  }

        下面又是一轮input命令项参数分析,和main函数差不多,看到眼都花。
        终于到了打开视频驱动函数init_videoIn
291行:
  /* open video device and prepare data structure */
  if (init_videoIn(videoIn, dev, width, height, fps, format, 1) < 0) {      //调用init_videoIn函数
    IPRINT("init_VideoIn failed\n");
    closelog();
    exit(EXIT_FAILURE);
  }

init_videoIn有好几个参数,在我眼里,主重要的是videoIn

基于分析之前先列数据结构的原则,先列一下videoIn的结构:
struct vdIn {
    int fd;      //设备 描述符, 文件描述符
    char *videodevice;   //设备路径指针, 视频捕捉接口文件
    char *status;    //状态
    char *pictName;
    struct v4l2_capability cap; //设备支持的操作能力
    struct v4l2_format fmt;  //捕获视频的格式
    struct v4l2_buffer buf;  //驱动申请的内存区信息
    struct v4l2_requestbuffers rb;//请求驱动申请一片连续的内存用于缓存视频信息
    void *mem[NB_BUFFER];
    unsigned char *tmpbuffer;
    unsigned char *framebuffer;//帧缓冲区
    int isstreaming;
    int grabmethod;
    int width;     //采集图片的宽度
    int height;     //采集图片的高度
    int fps;
    int formatIn;    //帧输入
    int formatOut;    //帧输出
    int framesizeIn;   //帧输入长度
    int signalquit;    //退出信号
    int toggleAvi;    //avi开关
    int getPict;
    int rawFrameCapture;  //原始帧获取
    /* raw frame capture */
    unsigned int fileCounter;  //文件计数器
    /* raw frame stream capture */
    unsigned int rfsFramesWritten;
    unsigned int rfsBytesWritten;
    /* raw stream capture */
    FILE *captureFile;   //抓取文件
    unsigned int framesWritten; //帧可写
    unsigned int bytesWritten; //字节可写
    int framecount;   //帧计数
    int recordstart;   //记录开始
    int recordtime;    //记录时间
};

       以上结构体有些注释为猜测,后面逐步纠正。为了理解这个结构体,还要理解下面几个结构体:
下面几个结构体在内核代码中有定义,如:linux-2.6.30.4\include\linux\videodev2.h中有定义。
/*
* D R I V E R   C A P A B I L I T I E S
*/
struct v4l2_capability {
__u8 driver[16]; /* i.e. "bttv" */
__u8 card[32]; /* i.e. "Hauppauge WinTV" */
__u8 bus_info[32]; /* "PCI:" + pci_name(pci_dev) */
__u32   version;        /* should use KERNEL_VERSION() */
__u32 capabilities; /* Device capabilities */
__u32 reserved[4];
};

/* Stream data format
*/
struct v4l2_format {
enum v4l2_buf_type type;
union {
  struct v4l2_pix_format  pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
  struct v4l2_window  win;     /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
  struct v4l2_vbi_format  vbi;     /* V4L2_BUF_TYPE_VBI_CAPTURE */
  struct v4l2_sliced_vbi_format sliced;  /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
  __u8 raw_data[200];                   /* user-defined */
} fmt;
};

struct v4l2_buffer {
__u32   index;
enum v4l2_buf_type      type;
__u32   bytesused;
__u32   flags;
enum v4l2_field  field;
struct timeval  timestamp;
struct v4l2_timecode timecode;
__u32   sequence;
/* memory location */
enum v4l2_memory        memory;
union {
  __u32           offset;
  unsigned long   userptr;
} m;
__u32   length;
__u32   input;
__u32   reserved;
};

/*
* M E M O R Y - M A P P I N G   B U F F E R S
*/
struct v4l2_requestbuffers {
__u32   count;
enum v4l2_buf_type      type;
enum v4l2_memory        memory;
__u32   reserved[2];
};
        这几个是v4l2接口的几个重要结构。
        V4L2(Video For Linux Two) 是内核提供给应用程序访问音、视频驱动的统一接口。
        关于V4L2了解不多,无法深入。后续研究。
5.init_videoIn函数
        init_videoIn函数本身不复杂,大部分都是对vdIn 结构变量vd初始化。
        接着掉用init_v4l2函数:
第57行:
  if (init_v4l2 (vd) < 0) {
    fprintf (stderr, " Init v4L2 failed !! exit fatal \n");
    goto error;;
  }

6.init_v412函数
        init_v412里面主要是使用ioctl设置驱动相关属性。

有篇文章写得很清晰:
http://www.cnblogs.com/emouse/archive/2013/03/04/2943243.html

init_v412有几个地方调用了ioctl,分别是:
102行:
  ret = ioctl(vd->fd, VIDIOC_QUERYCAP, &vd->cap);
135行:
  ret = ioctl(vd->fd, VIDIOC_S_FMT, &vd->fmt);
161行:
  ret = ioctl(vd->fd, VIDIOC_S_PARM, setfps);
171行:
  ret = ioctl(vd->fd, VIDIOC_REQBUFS, &vd->rb);
185行:
    ret = ioctl(vd->fd, VIDIOC_QUERYBUF, &vd->buf);
213行:
    ret = ioctl(vd->fd, VIDIOC_QBUF, &vd->buf);

        到此,input_init函数总算,粗略走完了。下面是output_init函数。





 楼主| yuki 发表于 2013-8-28 12:05:26 | 显示全部楼层
本帖最后由 yuki 于 2013-8-28 16:45 编辑

7.output_init函数
定义在mjpg-streamer/plugins/output_http/output_http.c里面,这个函数比较简单,主要完成:
        1.解析--output参数项,这个和input_init函数几乎是一样的。
        2.初始化一些结构,包括侦听端口等信息
192行:
  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;

8.input_run函数
定义在mjpg-streamer/plugins/input_uvc/input_uvc.c里面,这个函数比较简单,主要完成:
1.分配帧内存
2.创建线程cam,线程函数cam_thread主要完成视频抓取功能

/******************************************************************************
Description.: spins of a worker thread
Input Value.: -
Return Value: always 0
******************************************************************************/
int input_run(void) {
  pglobal->buf = malloc(videoIn->framesizeIn);        //为pglobal->buf 分配内存,大小和videoIn的帧大小一样
  if (pglobal->buf == NULL) {
    fprintf(stderr, "could not allocate memory\n");
    exit(EXIT_FAILURE);
  }
  pthread_create(&cam, 0, cam_thread, NULL);     //创建线程,线程函数为cam_thread
  pthread_detach(cam);                                    //等待线程cam退出时回收资源,不阻塞线程。
  return 0;
}

9.cam_thread函数
定义在mjpg-streamer/plugins/input_uvc/input_uvc.c里面,主要完成:
1.调用uvcGrab函数抓取视频帧数据。
2.调用compress_yuyv_to_jpeg或memcpy_picture拷贝帧数据到pglobal->buf,这2个都是系统提供的函数。

这个函数比较有意思,里面有多线程共享资源,主要用到互斥锁技术,值得慢慢品味。代码加上注释列一下:
/******************************************************************************
Description.: this thread worker grabs a frame and copies it to the global buffer
Input Value.: unused
Return Value: unused, always NULL
******************************************************************************/
void *cam_thread( void *arg ) {
  /* set cleanup handler to cleanup allocated ressources */
  pthread_cleanup_push(cam_cleanup, NULL);   //将清除函数cam_cleanup压人清除栈

  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 */
    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);//把抓取到的视频数据从缓存帧videoIn->framesizeIn 中拷贝到pglobal->buf
    }
    else {
      DBG("copying frame\n");
      pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused);//和上面 compress_yuyv_to_jpeg一样不同的格式类型使用不同的拷贝函数
    }

#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 */
    if ( videoIn->fps < 5 ) {
      usleep(1000*1000/videoIn->fps);
    }
  }

  DBG("leaving input thread, calling cleanup function now\n");
  pthread_cleanup_pop(1);       //将清除函数弹出清除栈

  return NULL;
}


10.uvcGrab函数
位置:在mjpg-streamer/plugins/input_uvc/input_uvc.c
功能:抓取视频帧

314行:
  ret = ioctl(vd->fd, VIDIOC_DQBUF, &vd->buf);  //使用驱动提供的ioctl函数读取视频帧到vd->buf

11.memcpy_picture函数
位置:V4L2 接口提供的函数

功能:拷贝数据到指定内存中
 楼主| yuki 发表于 2013-8-28 17:33:02 | 显示全部楼层
12.output_run函数
位置:mjpg-streamer/plugins/output_http/output_http.c
功能:为客户端提供服务,传输视频数据

函数结构比较简单,创建线程,线程函数为server_thread
/******************************************************************************
Description.: This creates and starts the server thread
Input Value.: id determines which server instance to send commands to
Return Value: always 0
******************************************************************************/
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])); //创建线程,线程函数为server_thread
  pthread_detach(servers[id].threadID);          //等待线程退出时回收资源,不阻塞线程
  return 0;
}

13.server_thread函数
位置:mjpg-streamer/plugins/output_http/httpd.c
功能:创建TCP socket,为客户端提供网络服务

主要调用函数socket、bin、listen、accept,最后调用pthread_create创建线程,线程函数为client_thread。
/******************************************************************************
Description.: Open a TCP socket and wait for clients to connect. If clients
              connect, start a new thread for each accepted connection.
Input Value.: arg is a pointer to the globals struct
Return Value: always NULL, will only return on exit
******************************************************************************/
void *server_thread( void *arg ) {
  int on;
  pthread_t client;
  struct addrinfo *aip, *aip2;
  struct addrinfo hints;
  struct sockaddr_storage client_addr;
  socklen_t addr_len = sizeof(struct sockaddr_storage);
  fd_set selectfds;
  int max_fds = 0;
  char name[NI_MAXHOST];
  int err;
  int i;

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

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

  bzero(&hints, sizeof(hints));
  hints.ai_family = PF_UNSPEC;
  hints.ai_flags = AI_PASSIVE;
  hints.ai_socktype = SOCK_STREAM;

  snprintf(name, sizeof(name), "%d", ntohs(pcontext->conf.port));
  if((err = getaddrinfo(NULL, name, &hints, &aip)) != 0) {
    perror(gai_strerror(err));
    exit(EXIT_FAILURE);
  }

  for(i = 0; i < MAX_SD_LEN; i++)
    pcontext->sd = -1;

  /* open sockets for server (1 socket / address family) */
  i = 0;
  for(aip2 = aip; aip2 != NULL; aip2 = aip2->ai_next)
  {
    if((pcontext->sd = socket(aip2->ai_family, aip2->ai_socktype, 0)) < 0) {  // 1.建立一个socket通信
      continue;
    }

    /* 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");
    }

    /* IPv6 socket should listen to IPv6 only, otherwise we will get "socket already in use" */
    on = 1;
    if(aip2->ai_family == AF_INET6 && setsockopt(pcontext->sd, IPPROTO_IPV6, IPV6_V6ONLY,
                  (const void *)&on , sizeof(on)) < 0) {
      perror("setsockopt(IPV6_V6ONLY) failed");
    }

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

    if(bind(pcontext->sd, aip2->ai_addr, aip2->ai_addrlen) < 0) {   // 2. 绑定
      perror("bind");
      pcontext->sd = -1;
      continue;
    }

    if(listen(pcontext->sd, 10) < 0) {          // 3.侦听
      perror("listen");
      pcontext->sd = -1;
    } else {
      i++;
      if(i >= MAX_SD_LEN) {
        OPRINT("%s(): maximum number of server sockets exceeded", __FUNCTION__);
        i--;
        break;
      }
    }
  }

  pcontext->sd_len = i;
  if(pcontext->sd_len < 1) {
    OPRINT("%s(): bind(%d) failed", __FUNCTION__, htons(pcontext->conf.port));
    closelog();
    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");
    do {
      FD_ZERO(&selectfds);

      for(i = 0; i < MAX_SD_LEN; i++) {
        if(pcontext->sd != -1) {
          FD_SET(pcontext->sd, &selectfds);

          if(pcontext->sd > max_fds)
            max_fds = pcontext->sd;
        }
      }

      err = select(max_fds + 1, &selectfds, NULL, NULL, NULL);
      if (err < 0 && errno != EINTR) {
        perror("select");
        exit(EXIT_FAILURE);
      }
    } while(err <= 0);

    for(i = 0; i < max_fds + 1; i++) {
      if(pcontext->sd != -1 && FD_ISSET(pcontext->sd, &selectfds)) {
        pcfd->fd = accept(pcontext->sd, (struct sockaddr *)&client_addr, &addr_len);   // 4.接收请求
        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");

        if(getnameinfo((struct sockaddr *)&client_addr, addr_len, name, sizeof(name), NULL, 0, NI_NUMERICHOST) == 0) {
          syslog(LOG_INFO, "serving client: %s\n", name);
        }

        if( pthread_create(&client, NULL, &client_thread, pcfd) != 0 ) {   // 5. 创建线程client
          DBG("could not launch another client thread\n");
          close(pcfd->fd);
          free(pcfd);
          continue;
        }
        pthread_detach(client);            // 6.等待线程client 退出时回收资源,不阻塞线程
      }
    }
  }

  DBG("leaving server thread, calling cleanup function now\n");
  pthread_cleanup_pop(1);       //将清除函数弹出清除栈

  return NULL;
}

14.client_thread函数
位置:mjpg-streamer/plugins/output_http/httpd.c
功能:为客户端发送数据

主要调用函数send_snapshotsend_stream发socket发送数据
772行:   
   case A_SNAPSHOT:
      DBG("Request for snapshot\n");
      send_snapshot(lcfd.fd);   // 6.发送快照
      break;
    case A_STREAM:
      DBG("Request for stream\n");
      send_stream(lcfd.fd);    //7.发送数据

15.send_snapshot和send_stream函数
位置:mjpg-streamer/plugins/output_http/httpd.c
功能:发送数据

主要调用write发送数据,虽然兜来兜去,其实write的参数fd就是上面创建的socket:
320行:
  /* send header and image now */    //8. 往socket写入数据
  if( write(fd, buffer, strlen(buffer)) < 0 || \            
      write(fd, frame, frame_size) < 0) {
    free(frame);
    return;
  }


348行:
  if ( write(fd, buffer, strlen(buffer)) < 0 ) {  //往socket写入数据
    free(frame);
    return;
  }


16.write函数
位置:系统函数
功能:往文件写入数据,这里的文件指的是socket。

      至此,mjpg-streamer代码总算分析完毕。本次只分析mjpg_streamer.c和input_uvc和output_http这3部分,其它部分不再分析。
     另外过程中出现的一些技术也没做过多描述,如:
     1.多线性
     2.V4L2接口
     这些内容不是三言两语就能说明白,也没理解透彻,留作后面捣鼓。

后天 发表于 2013-8-28 21:24:26 | 显示全部楼层
楼主牛b,学习了
万里 发表于 2013-8-28 23:06:10 | 显示全部楼层
辛苦了!
 楼主| yuki 发表于 2013-8-29 09:30:43 | 显示全部楼层
万里 发表于 2013-8-28 23:06
辛苦了!

没什么,反正也是闲着无聊,闹着玩,当复习,加深记忆。
Xflyan 发表于 2013-8-29 13:43:29 | 显示全部楼层
无聊至此的境界 必须顶啊!
sunhaojie 发表于 2014-9-7 16:11:07 | 显示全部楼层
咨询楼主一个问题,视频流的速度如何控制?
 楼主| yuki 发表于 2014-11-6 23:51:07 | 显示全部楼层
sunhaojie 发表于 2014-9-7 16:11
咨询楼主一个问题,视频流的速度如何控制?

你说的是帧数?一般设置可以使用 ioctl函数。不太了解你的具体问题,建议在网上下载“v4l2中文手册(规范)全五章(包含驱动编写)”来看看。里面应该有你需要的答案。
zhpic 发表于 2014-12-6 10:29:58 | 显示全部楼层
崇拜楼主对技术的这份热爱精神!
我不是酒神 发表于 2015-4-28 16:43:32 | 显示全部楼层
一点没看懂 怎么办?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

i.MX8系列ARM cortex A53 M4 工控板上一条 /1 下一条

Archiver|手机版|小黑屋|天嵌 嵌入式开发社区 ( 粤ICP备11094220号 )

GMT+8, 2024-4-28 13:21 , Processed in 1.031263 second(s), 22 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表