韦东山第三期项目
最近一段时间看了韦东山老师的项目视频,记录一下
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上显示。具体的实现细节就在另一篇博客上写吧。