MJPG-streamer是一个轻量级的视频服务器软件,一个可以从单一输入组件获取图像并传输到多个输出组件的命令行应用程序,可应用在基于IP协议的网络中,从网络摄像头中获取并传输JPEG格式的图像到浏览器
MJPG-streamer的源码基本上可以分为main函数、输入插件和输出插件三部分。在源码包中,mjpg-streamer.c文件主要是完成全局变量的定义,解析输入参数,打印help信息,信号处理,输入输出通道的初始化和启动以及在获取外部终止信号时对内存清理等工作。mjpg-streamer.h则定义了一些宏和全局变量结构体,包括终止信号位、互斥锁、条件变量、全局缓冲区和输入输出插件。plugin文件夹中包括了所需的所有插件的源码,这些插件在运行时可以被主函数调用。整个mjpg-streamer的运行过程如下图所示:
主程序部分如下:
int main(int argc, char *argv[]) { //input指针未能赋值时使用默认值 char *input = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0"; // 指向输出选项的参数字符串 char *output[MAX_OUTPUT_PLUGINS]; //daemon用于标记是否让程序在后台运行 int daemon=0, i; size_t tmp=0; //output[0]初始值,设置默认端口号为8080 output[0] = "output_http.so --port 8080"; //此时输出通道有几种方式 global.outcnt = 0; //运行mjpg_streamer命令时所带入的参数,while循环用于解析这些参数(关键是看懂 getopt_long_only 函数) 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} }; //用于解析命令行选项 //根据传入的参数,逐个在struct option 数组里面进行匹配,当匹配到相同的参数,返回在数组中的下标。 //如传入-i选项 ,argv中自然有i选项,和struct option 数组项进行匹配,找到时返回下标,option_index索引值为2. //参数解析: //第二个参数:直接从main函数传递而来,表示我们传入的参数,例如传入: // -i "input_uvc.so -f 10 -r 320*240" -o "output_http.so -w www" //第三个参数:短选项字符串 //第四个参数:是 struct option 数组,用于存放长选项参数进行匹配 //第五个参数:用于返回长选项在longopts结构体数组中的索引值 //返回值: //解析完毕,返回-1,出现未定义的长选项或者短选项,getopt_long返回“?”,否则返回对应索引值 //返回值: //解析完毕,返回-1 //出现未定义的长选项或者短选项,getopt_long返回? c = getopt_long_only(argc, argv, "", long_options, &option_index); //当为-1时表示参数解析完成,跳出循环 if (c == -1) break; //如果传入的参数不正确,则打印帮助信息 if(c=='?'){ help(argv[0]); return 0; } //根据返回的结构体数组的下标,来执行相应的选项 switch (option_index) { /* h, help */ case 0: case 1: help(argv[0]); return 0; break; /* i, input */ //在getopt_long_only函数中, //把传入的-i选项对应的字符串"input_uvc.so -f 10 -r 320*240"赋给变量optarg, //而strdup()函数用于将变量optarg的值赋予指针input case 2: case 3: input = strdup(optarg); break; /* o, output */ // output[0] = "output_http.so -w www" case 4: case 5: output[global.outcnt++] = strdup(optarg); break; /* v, version */ case 6: case 7: printf("MJPG Streamer Version: %s\n" \ "Compilation Date.....: %s\n" \ "Compilation Time.....: %s\n", SOURCE_VERSION, __DATE__, __TIME__); return 0; break; /* b, background */ //参数传入-b选项时,让daemon=1;让程序在后台运行 case 8: case 9: daemon=1; break; default: help(argv[0]); return 0; } } //打开一个程序的系统记录器的链接 */也就是打开一个调试用的记录本相当于log.txt openlog("MJPG-streamer ", LOG_PID|LOG_CONS, LOG_USER); syslog(LOG_INFO, "starting application"); //如果daemon = 1,则让程序在后台运行 if ( daemon ) { LOG("enabling daemon mode"); daemon_mode(); } /* 初始化全局变量,global结构在程序后面列出 */ global.stop = 0; global.buf = NULL; global.size = 0; global.in.plugin = NULL; //初始化 global.db 成员 ,是互斥锁相关的,用于同步访问全局图像缓冲区 if( pthread_mutex_init(&global.db, NULL) != 0 ) { LOG("could not initialize mutex variable\n"); closelog(); exit(EXIT_FAILURE); } //初始化 global.db_update(条件变量) 成员,进程里线程间进行通信的变量 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) */ signal(SIGPIPE, SIG_IGN); //signal()函数是信号绑定,当我们按下 <CTRL>+C 时,则调用signal_handler()函数,做一些清理工作 //signal_handler()把stop标志位置1,摄像头采集线程退出,然后清理工作开始 if (signal(SIGINT, signal_handler) == SIG_ERR) { LOG("could not register signal handler\n"); closelog(); exit(EXIT_FAILURE); } LOG("MJPG Streamer Version.: %s\n", SOURCE_VERSION); // 如果输出方式的个数为0,则让他为1,保证至少有一个输出通道 if ( global.outcnt == 0 ) { /* no? Then use the default plugin instead */ global.outcnt = 1; } //打开加载输入插件 //strchr函数原型:extern char *strchr(const char *s,char c),查找字符串s中首次出现字符c的位置, //返回值:成功则返回要查找字符第一次出现的位置,失败返回NULL //让tmp 等于 "input_uvc.so"字符串的长度,如果tmp大于0,就取字符串input的前tmp位,也就是input_uvc.so tmp = (size_t)(strchr(input, ' ')-input); //经过赋值后,global.in.plugin = "input_uvc.so" global.in.plugin = (tmp > 0)?strndup(input, tmp):strdup(input); //通过dlopen()函数打开 "input_uvc.so" 这个动态链接库 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_uvc.c)的input_init函数,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_uvc.c)的input_stop函数,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_uvc.c)的input_run函数,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); } //global.in.cmd等于 刚打开的动态链接库(input_uvc.c)的input_cmd函数,global.in.cmd = input_cmd global.in.cmd = dlsym(global.in.handle, "input_cmd"); //对全局输入插件的设定参数进行初始化,例如global.in.param.parameter_string = "-f 10 -r 320*240" global.in.param.parameter_string = strchr(input, ' '); global.in.param.global = &global; //执行输入插件部分的初始化, //在这部分中,包括初始化互斥锁、条件变量、图像采集参数(dev,height,weight,fps,format) //并在打开相机设备后,对相机的输入格式,帧率,缓冲区等进行初始化,并将相机采集缓冲区映射到内存,启动相机等工作。 if ( global.in.init(&global.in.param) ) { LOG("input_init() return value signals to exit"); closelog(); exit(0); } /* 打开加载输出插件,这部分跟前面的输入插件部分类似 */ for (i=0; i<global.outcnt; i++) { //让tmp 等于 "output_http.so"字符串的长度 tmp = (size_t)(strchr(output[i], ' ')-output[i]); //让 global.out[i].plugin = "output_http.so" global.out[i].plugin = (tmp > 0)?strndup(output[i], tmp):strdup(output[i]); //打开 "output_http.so" 动态链接库 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等于 刚打开的动态链接库(Output_http.c)的output_init函数,global.out[i].init = output_init, 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等于 刚打开的动态链接库(Output_http.c)的output_stop函数,global.out[i].stop = output_stop 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等于 刚打开的动态链接库(Output_http.c)的output_run函数,global.out[i].run = output_run global.out[i].run = dlsym(global.out[i].handle, "output_run"); if ( global.out[i].run == NULL ) { LOG("%s\n", dlerror()); exit(EXIT_FAILURE); } //lobal.out[i].cmd等于 刚打开的动态链接库(Output_http.c)的output_cmd函数,global.out[i].cmd = output_cmd global.out[i].cmd = dlsym(global.out[i].handle, "output_cmd"); //让 global.out[i].param.parameter_string = "-w www" global.out[i].param.parameter_string = strchr(output[i], ' '); global.out[i].param.global = &global; global.out[i].param.id = i; //调用 output_http.c中的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_uvc.c中的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_http.c 中的 output_run 函数,通过多线程的方式发送图像数据,这部分比较复杂 global.out[i].run(global.out[i].param.id); } //程序等待信号的发生 pause(); return 0; }
当按下ctrl+C时,程序调用signal_handle函数进行清理工作
void signal_handler(int sig) { int i; /* 将全局终止信号位置1,在输入和输出的run函数中,读取相机数据和发送过程的循环体都跟这个参数有关 */ /* 此处置1后,各线程跳出循环体,并执行各线程对应的清理工作,此处的清理工作是全局变量部分和关闭线程 */ LOG("setting signal to stop\n"); global.stop = 1; usleep(1000*1000); /* 执行stop函数,终止线程 */ LOG("force cancelation of threads and cleanup ressources\n"); global.in.stop(); for(i=0; i<global.outcnt; i++) { global.out[i].stop(global.out[i].param.id); } usleep(1000*1000); /* 关闭输入插件的handle */ dlclose(&global.in.handle); for(i=0; i<global.outcnt; i++) dlclose(global.out[i].handle); DBG("all plugin handles closed\n"); pthread_cond_destroy(&global.db_update); pthread_mutex_destroy(&global.db); LOG("done\n"); closelog(); exit(0); return; }
主函数的全局变量global定义为:
typedef struct _globals globals; struct _globals { int stop; /* 互斥锁和条件变量,用于通知刷新数据信息 */ pthread_mutex_t db; pthread_cond_t db_update; /* 全局图像缓冲区 */ unsigned char *buf; int size; /* 输入插件*/ input in; /* 输出插件 */ output out[MAX_OUTPUT_PLUGINS]; int outcnt; };
总结:
主函数部分的工作,主要是解析输入参数,加载输入输出插件,执行输入输出插件初始化并启动,以及程序终止时的清理工作,即:依次调用以下函数
input_init();
output_init();【循环调用,多个输出】
input_run();
output_run();【循环调用,多个输出】
signal_handler();