mjpg-streamer实现细节分析
文章目录
- mjpg-streamer实现细节分析
- 输入初始化input_init
- init_videoIn
- init_v4l2
- 启动摄像头输入线程
- cam_thread
- uvcGrab
- compress_yuyv_to_jpeg
- memcpy_picture
- 输出初始化output_init
- 启动摄像头输出线程
- server_thread
- 设置 SO_REUSEADDR 选项。
- 设置 IPV6_V6ONLY 选项。
- 绑定地址/开始监听
- 等待客户端连接
- 处理与客户端建立的连接。
- client_thread
- _readline
- send_snapshot
- send_stream
输入初始化input_init
init_videoIn
input_init->init_videoIn
这段代码是init_videoIn函数的实现。该函数用于初始化视频输入设备。
函数接收多个参数,包括指向vdIn结构体的指针vd、设备名称device、宽度width、高度height、帧率fps、格式format、抓取方法grabmethod、全局上下文指针pglobal和IDid。
函数的主要步骤如下:
检查vd和device是否为NULL,如果是则返回-1。
检查width和height是否为0,如果是则返回-1。
检查grabmethod的值是否在0和1之间,如果不是,则将其设置为1(默认值)。
分配内存并初始化vd结构体中的指针成员。
使用calloc函数分配内存并将设备名称拷贝到vd->videodevice中。
设置vd结构体的其他成员变量,如toggleAvi、getPict、signalquit、width、height、fps、formatIn和grabmethod。
调用init_v4l2函数初始化V4L2设备。
枚举支持的格式并存储到全局上下文的in_formats数组中。
如果当前格式与指定的format匹配,则将其索引存储到currentFormat中。
枚举当前格式下支持的分辨率,并存储到相应的数据结构中。
分配临时缓冲区和帧缓冲区,根据指定的格式和分辨率确定缓冲区的大小。
返回0表示初始化成功。
如果在初始化过程中发生错误,将执行错误处理步骤,释放已分配的内存并关闭视频设备,然后返回-1表示初始化失败。
int init_videoIn(struct vdIn *vd, char *device, int width,
int height, int fps, int format, int grabmethod, globals *pglobal, int id)
{
if(vd == NULL || device == NULL) // 如果vd或device为空,则返回-1
return -1;
if(width == 0 || height == 0) // 如果width或height为0,则返回-1
return -1;
if(grabmethod < 0 || grabmethod > 1) // 如果grabmethod小于0或大于1,则将其设置为1
grabmethod = 1; //默认使用mmap;
vd->videodevice = NULL; // 初始化vd的videodevice、status、pictName为NULL
vd->status = NULL;
vd->pictName = NULL;
vd->videodevice = (char *) calloc(1, 16 * sizeof(char)); // 为vd的videodevice、status、pictName分配内存
vd->status = (char *) calloc(1, 100 * sizeof(char));
vd->pictName = (char *) calloc(1, 80 * sizeof(char));
snprintf(vd->videodevice, 12, "%s", device); // 将device的值复制到vd的videodevice中
vd->toggleAvi = 0; // 初始化vd的toggleAvi、getPict、signalquit为0、0、1
vd->getPict = 0;
vd->signalquit = 1;
vd->width = width; // 初始化vd的width、height、fps、formatIn、grabmethod为传入的参数
vd->height = height;
vd->fps = fps;
vd->formatIn = format;
vd->grabmethod = grabmethod;
if(init_v4l2(vd) < 0) { // 如果init_v4l2返回小于0的值,则输出错误信息并跳转到error标签
fprintf(stderr, " Init v4L2 failed !! exit fatal \n");
goto error;;
}
// 枚举格式
int currentWidth, currentHeight = 0;
struct v4l2_format currentFormat;
currentFormat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(xioctl(vd->fd, VIDIOC_G_FMT, ¤tFormat) == 0) {
currentWidth = currentFormat.fmt.pix.width;
currentHeight = currentFormat.fmt.pix.height;
DBG("Current size: %dx%d\n", currentWidth, currentHeight);
}
pglobal->in[id].in_formats = NULL;
for(pglobal->in[id].formatCount = 0; 1; pglobal->in[id].formatCount++) {
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = pglobal->in[id].formatCount;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(xioctl(vd->fd, VIDIOC_ENUM_FMT, &fmtdesc) < 0) {
break;
}
if (pglobal->in[id].in_formats == NULL) {
pglobal->in[id].in_formats = (input_format*)calloc(1, sizeof(input_format));
} else {
pglobal->in[id].in_formats = (input_format*)realloc(pglobal->in[id].in_formats, (pglobal->in[id].formatCount + 1) * sizeof(input_format));
}
if (pglobal->in[id].in_formats == NULL) {
DBG("Calloc/realloc failed: %s\n", strerror(errno));
return -1;
}
// 将fmtdesc复制到pglobal->in[id].in_formats[pglobal->in[id].formatCount]中
memcpy(&pglobal->in[id].in_formats[pglobal->in[id].formatCount], &fmtdesc, sizeof(input_format));
// 如果fmtdesc.pixelformat等于format,则将pglobal->in[id].currentFormat设置为pglobal->in[id].formatCount
if(fmtdesc.pixelformat == format)
pglobal->in[id].currentFormat = pglobal->in[id].formatCount;
DBG("Supported format: %s\n", fmtdesc.description);
// 枚举分辨率
struct v4l2_frmsizeenum fsenum;
fsenum.index = pglobal->in[id].formatCount;
fsenum.pixel_format = fmtdesc.pixelformat;
int j = 0;
pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions = NULL;
pglobal->in[id].in_formats[pglobal->in[id].formatCount].resolutionCount = 0;
pglobal->in[id].in_formats[pglobal->in[id].formatCount].currentResolution = -1;
while(1) {
fsenum.index = j;
j++;
if(xioctl(vd->fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0) {
pglobal->in[id].in_formats[pglobal->in[id].formatCount].resolutionCount++;
// 为pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions分配内存
if (pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions == NULL) {
pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions =
(input_resolution*)calloc(1, sizeof(input_resolution));
} else {
pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions =
(input_resolution*)realloc(pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions, j * sizeof(input_resolution));
}
// 如果分配内存失败,则输出错误信息并返回-1
if (pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions == NULL) {
DBG("Calloc/realloc failed\n");
return -1;
}
// 将分辨率信息添加到pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions中
pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions[j-1].width = fsenum.discrete.width;
pglobal->in[id].in_formats[pglobal->in[id].formatCount].supportedResolutions[j-1].height = fsenum.discrete.height;
// 如果format等于fmtdesc.pixelformat,则将pglobal->in[id].in_formats[pglobal->in[id].formatCount].currentResolution设置为(j - 1)
if(format == fmtdesc.pixelformat) {
pglobal->in[id].in_formats[pglobal->in[id].formatCount].currentResolution = (j - 1);
DBG("\tSupported size with the current format: %dx%d\n", fsenum.discrete.width, fsenum.discrete.height);
} else {
DBG("\tSupported size: %dx%d\n", fsenum.discrete.width, fsenum.discrete.height);
}
} else {
break;
}
}
}
/* 为重构图像分配临时缓冲区 */
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(pglobal->in[id].in_parameters);
free(vd->videodevice);
free(vd->status);
free(vd->pictName);
CLOSE_VIDEO(vd->fd);
return -1;
}
init_v4l2
input_init->init_videoIn->init_v4l2
代码与初始化V4L2(Video4Linux2)应用程序中的视频输入有关。以下是代码的功能解析:
init_v4l2函数用于初始化视频捕获的V4L2接口。
使用带有O_RDWR标志的OPEN_VIDEO宏打开指定的视频设备(由vd->videodevice指定)。
使用VIDIOC_QUERYCAP ioctl查询视频设备的功能。如果查询失败,将打印错误消息,并跳转到fatal标签,表示发生致命错误。
使用设备功能的capabilities字段中的V4L2_CAP_VIDEO_CAPTURE标志检查是否支持视频捕获。如果不支持,则打印错误消息,并跳转到fatal标签。
根据vd->grabmethod指定的捕获方法,检查设备是否支持流式I/O(V4L2_CAP_STREAMING)或读取I/O(V4L2_CAP_READWRITE)。如果不支持,将打印错误消息,并跳转到fatal标签。
使用VIDIOC_S_FMT ioctl设置视频捕获的所需格式。格式参数(如宽度、高度、像素格式和字段)在vd->fmt结构中设置。如果ioctl调用失败,将打印错误消息,并跳转到fatal标签。
如果请求的格式不可用,则根据设备报告的支持格式调整宽度、高度和像素格式。如果调整后的格式不受支持,或者请求的格式为MJPEG并且设备不支持MJPEG模式,或者请求的格式为YUV并且设备不支持YUV模式,则打印错误消息,并跳转到fatal标签。
使用VIDIOC_S_PARM ioctl设置所需的帧率。帧率在struct v4l2_streamparm结构的timeperframe字段中设置。如果ioctl调用失败,则忽略错误,函数继续执行。
使用VIDIOC_REQBUFS ioctl请求视频缓冲区。请求的缓冲区数量和内存类型(V4L2_MEMORY_MMAP)在vd->rb结构中设置。如果ioctl调用失败,将打印错误消息,并跳转到fatal标签。
使用mmap函数将视频缓冲区映射到应用程序的内存中。每个缓冲区使用从VIDIOC_QUERYBUF ioctl获得的缓冲区长度和偏移量进行映射。如果映射失败,将打印错误消息,并跳转到fatal标签。
使用VIDIOC_QBUF ioctl将映射的缓冲区排队进行视频捕获。索引、类型和内存类型在vd->buf结构中设置。如果排队失败,将打印错误消息,并跳转到fatal标签。
// 初始化视频设备
static int init_v4l2(struct vdIn *vd)
{
int i;
int ret = 0;
// 打开视频设备
if((vd->fd = OPEN_VIDEO(vd->videodevice, O_RDWR)) == -1) {
perror("ERROR opening V4L interface");
DBG("errno: %d", errno);
return -1;
}
// 查询设备信息
memset(&vd->cap, 0, sizeof(struct v4l2_capability));
ret = xioctl(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;;
}
// 判断设备是否支持流式I/O或读写I/O
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;
}
}
/*
* 设置视频格式
*/
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 = xioctl(vd->fd, VIDIOC_S_FMT, &vd->fmt); // 设置视频格式
if(ret < 0) {
fprintf(stderr, "Unable to set format: %d res: %dx%d\n", vd->formatIn, vd->width, vd->height);
goto fatal;
}
if((vd->fmt.fmt.pix.width != vd->width) ||
(vd->fmt.fmt.pix.height != vd->height)) {
fprintf(stderr, "i: The format asked unavailable, so the 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;
/*
* 检查所需格式是否可用
*/
if(vd->formatIn != vd->fmt.fmt.pix.pixelformat) {
if(vd->formatIn == V4L2_PIX_FMT_MJPEG) {
fprintf(stderr, "The inpout device does not supports MJPEG mode\nYou may also try the YUV mode (-yuv option), but it requires a much more CPU power\n");
goto fatal;
} else if(vd->formatIn == V4L2_PIX_FMT_YUYV) {
fprintf(stderr, "The input device does not supports YUV mode\n");
goto fatal;
}
} else {
vd->formatIn = vd->fmt.fmt.pix.pixelformat;
}
}
/*
* 设置帧率
*/
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 = xioctl(vd->fd, VIDIOC_S_PARM, setfps); // 设置帧率
free(setfps);
/*
* 请求缓冲区
*/
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 = xioctl(vd->fd, VIDIOC_REQBUFS, &vd->rb); // 请求缓冲区
if(ret < 0) {
perror("Unable to allocate buffers");
goto fatal;
}
/*
* map 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 = xioctl(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 | PROT_WRITE, 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 = xioctl(vd->fd, VIDIOC_QBUF, &vd->buf); // 将缓冲区加入队列
if(ret < 0) { // 加入队列失败
perror("Unable to queue buffer"); // 输出错误信息
goto fatal;; // 跳转到错误处理
}
}
return 0;
fatal:
return -1;
}
启动摄像头输入线程
cam_thread
这个函数是一个摄像头线程函数,主要功能是从摄像头抓取视频帧并进行处理。下面是对函数的概括总结:
函数接受一个参数 arg,该参数是一个结构体指针 context。
函数中使用了一个全局变量 pglobal,该变量指向 pcontext 结构体中的全局变量。
函数使用 pthread_cleanup_push 设置了清理处理程序 cam_cleanup,以便在函数结束时清理分配的资源。
函数通过一个循环来不断抓取视频帧,直到全局变量 pglobal 的 stop 标志被设置为真。
在抓取视频帧之前,检查视频流的状态是否为暂停状态,如果是暂停状态,则使用 usleep 函数进行延迟等待。
使用 uvcGrab 函数抓取一帧视频。
对于捕获的视频帧,进行一些检查和处理:
如果视频帧的大小小于 minimum_size,则认为是损坏的帧,跳过处理。
如果视频帧的输入格式为 V4L2_PIX_FMT_YUYV,则进行 YUV 到 JPEG 的压缩处理;否则,直接将视频帧复制到全局缓冲区。
复制处理后的帧到全局缓冲区之前,获取全局缓冲区的互斥锁,确保线程安全。
将处理后的帧的大小和时间戳复制到全局缓冲区,并通过信号 db_update 通知其他线程。
释放全局缓冲区的互斥锁。
根据帧率决定是否使用 usleep 进行等待,如果帧率低于 5,则等待一定时间,否则直接等待下一帧。
当全局变量 pglobal 的 stop 标志被设置为真时,退出循环。
在函数结束之前,使用 pthread_cleanup_pop 调用清理处理程序。
函数返回 NULL。
总体而言,该函数负责从摄像头抓取视频帧,并将处理后的帧复制到全局缓冲区,同时控制帧率和处理错误帧。
// 摄像头线程函数
void *cam_thread(void *arg)
{
context *pcontext = arg;
pglobal = pcontext->pglobal;
/* 设置清理处理程序以清理分配的资源 */
pthread_cleanup_push(cam_cleanup, pcontext);
while(!pglobal->stop) {
while(pcontext->videoIn->streamingState == STREAMING_PAUSED) {
usleep(1); // 可能不是最好的方法,所以FIXME
}
/* 抓取一帧 */
if(uvcGrab(pcontext->videoIn) < 0) {
IPRINT("Error grabbing frames\n");
exit(EXIT_FAILURE);
}
DBG("received frame of size: %d from plugin: %d\n", pcontext->videoIn->buf.bytesused, pcontext->id);
/*
* 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(pcontext->videoIn->buf.bytesused < minimum_size) {
DBG("dropping too small frame, assuming it as broken\n");
continue;
}
/* 将 JPG 图片复制到全局缓冲区 */
pthread_mutex_lock(&pglobal->in[pcontext->id].db);
/*
* 如果以 YUV 模式捕获,则现在将其转换为 JPEG。
* 此压缩需要许多 CPU 周期,因此尽量避免使用 YUV 格式。
* 直接从网络摄像头获取 JPEG 是 Linux-UVC 兼容设备的主要优点之一。
*/
if(pcontext->videoIn->formatIn == V4L2_PIX_FMT_YUYV) {
DBG("compressing frame from input: %d\n", (int)pcontext->id);
pglobal->in[pcontext->id].size = compress_yuyv_to_jpeg(pcontext->videoIn, pglobal->in[pcontext->id].buf, pcontext->videoIn->framesizeIn, gquality);
} else {
DBG("compressing frame from input: %d\n", (int)pcontext->id);
pglobal->in[pcontext->id].size = memcpy_picture(pglobal->in[pcontext->id].buf, pcontext->videoIn->tmpbuffer, pcontext->videoIn->buf.bytesused);
}
#if 0
/* 运动检测可以通过比较图片大小来完成,但不是非常准确!! */
if((prev_size - global->size)*(prev_size - global->size) > 4 * 1024 * 1024) {
DBG("检测到运动(差值:%d kB)\n", (prev_size - global->size) / 1024);
}
prev_size = global->size;
#endif
/* 将此帧的时间戳复制到用户空间 */
pglobal->in[pcontext->id].timestamp = pcontext->videoIn->buf.timestamp;
/* 信号 fresh_frame */
pthread_cond_broadcast(&pglobal->in[pcontext->id].db_update);
pthread_mutex_unlock(&pglobal->in[pcontext->id].db);
/* 只有在 fps 低于 5 时才使用 usleep,否则开销太大 */
if(pcontext->videoIn->fps < 5) {
DBG("waiting for next frame for %d us\n", 1000 * 1000 / pcontext->videoIn->fps);
usleep(1000 * 1000 / pcontext->videoIn->fps);
} else {
DBG("waiting for next frame\n");
}
}
DBG("leaving input thread, calling cleanup function now\n");
pthread_cleanup_pop(1);
return NULL;
}
uvcGrab
这段代码是用于从视频设备中抓取一帧视频的函数 uvcGrab。以下是对函数的概括总结:
函数接受一个参数 vd,该参数是一个指向 struct vdIn 结构体的指针。
首先检查视频流的状态,如果当前状态为关闭状态,则通过调用 video_enable 函数启动视频流。
将缓冲区 vd->buf 清零,并设置缓冲区的类型为视频捕获 (V4L2_BUF_TYPE_VIDEO_CAPTURE),内存类型为内存映射 (V4L2_MEMORY_MMAP)。
使用 xioctl 函数从队列中取出一个缓冲区,存储在 vd->buf 中。
根据输入的格式进行处理:
如果输入格式为 V4L2_PIX_FMT_MJPEG,检查当前缓冲区的大小是否小于等于 HEADERFRAME1(宏定义的值),如果是,则输出警告信息并返回。
将缓冲区的内容复制到临时缓冲区 vd->tmpbuffer。
如果输入格式为 V4L2_PIX_FMT_YUYV,将缓冲区的内容复制到帧缓冲区 vd->framebuffer,根据实际大小进行复制。
对于其他输入格式,跳转到错误处理。
使用 xioctl 函数将缓冲区重新放入队列。
如果发生错误,设置退出信号为 0,并返回 -1。
函数正常执行完成后,返回 0。
该函数的主要目的是从视频设备中获取一帧视频数据,并根据输入格式进行相应处理和复制。
int uvcGrab(struct vdIn *vd)
{
#define HEADERFRAME1 0xaf
int ret;
if(vd->streamingState == STREAMING_OFF) { // 如果当前状态为关闭状态
if(video_enable(vd)) // 启动视频流
goto err;
}
memset(&vd->buf, 0, sizeof(struct v4l2_buffer)); // 将缓冲区清零
vd->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置缓冲区类型为视频捕获
vd->buf.memory = V4L2_MEMORY_MMAP; // 设置缓冲区内存类型为内存映射
ret = xioctl(vd->fd, VIDIOC_DQBUF, &vd->buf); // 从队列中取出缓冲区
if(ret < 0) { // 取出失败
perror("Unable to dequeue buffer"); // 输出错误信息
goto err; // 跳转到错误处理
}
switch(vd->formatIn) { // 根据输入格式进行处理
case V4L2_PIX_FMT_MJPEG: // 如果是MJPEG格式
if(vd->buf.bytesused <= HEADERFRAME1) { // 如果当前缓冲区大小小于等于0xaf
/* Prevent crash
* on empty image */
fprintf(stderr, "Ignoring empty buffer ...\n"); // 输出警告信息
return 0; // 返回0
}
/* memcpy(vd->tmpbuffer, vd->mem[vd->buf.index], vd->buf.bytesused);
memcpy (vd->tmpbuffer, vd->mem[vd->buf.index], HEADERFRAME1);
memcpy (vd->tmpbuffer + HEADERFRAME1, dht_data, sizeof(dht_data));
memcpy (vd->tmpbuffer + HEADERFRAME1 + sizeof(dht_data), vd->mem[vd->buf.index] + HEADERFRAME1, (vd->buf.bytesused - HEADERFRAME1));
*/
memcpy(vd->tmpbuffer, vd->mem[vd->buf.index], vd->buf.bytesused); // 将缓冲区内容复制到临时缓冲区
if(debug)
fprintf(stderr, "bytes in used %d \n", vd->buf.bytesused); // 输出调试信息
break;
case V4L2_PIX_FMT_YUYV: // 如果是YUYV格式
if(vd->buf.bytesused > vd->framesizeIn)
memcpy(vd->framebuffer, vd->mem[vd->buf.index], (size_t) vd->framesizeIn); // 将缓冲区内容复制到帧缓冲区
else
memcpy(vd->framebuffer, vd->mem[vd->buf.index], (size_t) vd->buf.bytesused); // 将缓冲区内容复制到帧缓冲区
break;
default:
goto err; // 跳转到错误处理
break;
}
ret = xioctl(vd->fd, VIDIOC_QBUF, &vd->buf); // 将缓冲区加入队列
if(ret < 0) { // 加入队列失败
perror("Unable to requeue buffer"); // 输出错误信息
goto err; // 跳转到错误处理
}
return 0; // 返回0
err:
vd->signalquit = 0; // 设置退出信号为0
return -1; // 返回-1
}
// 启用视频流
static int video_enable(struct vdIn *vd)
{
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置视频流类型
int ret;
ret = xioctl(vd->fd, VIDIOC_STREAMON, &type); // 启用视频流
if(ret < 0) { // 启用失败
perror("Unable to start capture"); // 输出错误信息
return ret; // 返回错误码
}
vd->streamingState = STREAMING_ON; // 设置视频流状态为开启
return 0; // 返回成功
}
compress_yuyv_to_jpeg
这段代码是用于将 YUYV 格式的帧数据压缩为 JPEG 格式的函数 compress_yuyv_to_jpeg。以下是对函数的概括总结:
函数接受四个参数:指向 struct vdIn 结构体的指针 vd,存储压缩后数据的缓冲区 buffer,缓冲区大小 size,以及压缩的质量 quality。
首先分配一行像素数据的缓冲区 line_buffer,并获取 YUYV 格式的帧数据存储在 yuyv 中。
初始化 JPEG 压缩结构体 cinfo 和 JPEG 错误管理器 jerr。
将压缩后的数据存储到内存中的 buffer 中,通过调用 dest_buffer 函数来实现,同时记录已经写入的字节数。
设置图像的宽度、高度、颜色空间等信息。
设置 JPEG 压缩参数,包括压缩质量。
开始压缩过程,调用 jpeg_start_compress 函数。
遍历每一行像素数据:
遍历每个像素点,根据 YUV 值计算对应的 RGB 值。
将 RGB 值存储到 line_buffer 中。
更新 YUV 值。
存储一行像素数据,通过调用 jpeg_write_scanlines 函数实现。
压缩结束后,调用 jpeg_finish_compress 完成压缩过程。
销毁 JPEG 压缩结构体,通过调用 jpeg_destroy_compress 函数。
释放缓冲区 line_buffer。
返回已经写入的字节数。
该函数使用 libjpeg 库将 YUYV 格式的帧数据压缩为 JPEG 格式,并将压缩后的数据存储在指定的缓冲区中,并返回已经写入的字节数。
int compress_yuyv_to_jpeg(struct vdIn *vd, unsigned char *buffer, int size, int quality)
{
// 初始化jpeg压缩结构体
struct jpeg_compress_struct cinfo;
// 初始化jpeg错误管理器
struct jpeg_error_mgr jerr;
// 存储一行像素数据
JSAMPROW row_pointer[1];
// 存储一行像素数据的缓冲区
unsigned char *line_buffer, *yuyv;
// 计数器
int z;
// 已经写入的字节数
static int written;
// 分配一行像素数据的缓冲区
line_buffer = calloc(vd->width * 3, 1);
// 获取YUYV格式的帧缓冲区
yuyv = vd->framebuffer;
// 初始化jpeg错误管理器
cinfo.err = jpeg_std_error(&jerr);
// 创建jpeg压缩结构体
jpeg_create_compress(&cinfo);
// 将压缩后的数据存储到内存中
dest_buffer(&cinfo, buffer, size, &written);
// 设置图像的宽、高、颜色空间等信息
cinfo.image_width = vd->width;
cinfo.image_height = vd->height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
// 设置jpeg压缩参数
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
// 开始压缩
jpeg_start_compress(&cinfo, TRUE);
// 遍历每一行像素数据
z = 0;
while(cinfo.next_scanline < vd->height) {
int x;
unsigned char *ptr = line_buffer;
// 遍历每一个像素点
for(x = 0; x < vd->width; x++) {
int r, g, b;
int y, u, v;
// 获取YUV值
if(!z)
y = yuyv[0] << 8;
else
y = yuyv[2] << 8;
u = yuyv[1] - 128;
v = yuyv[3] - 128;
// 转换为RGB值
r = (y + (359 * v)) >> 8;
g = (y - (88 * u) - (183 * v)) >> 8;
b = (y + (454 * u)) >> 8;
// 存储RGB值
*(ptr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r);
*(ptr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g);
*(ptr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b);
// 更新YUV值
if(z++) {
z = 0;
yuyv += 4;
}
}
// 存储一行像素数据
row_pointer[0] = line_buffer;
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
// 压缩结束
jpeg_finish_compress(&cinfo);
// 销毁jpeg压缩结构体
jpeg_destroy_compress(&cinfo);
// 释放缓冲区
free(line_buffer);
// 返回已经写入的字节数
return (written);
}
memcpy_picture
这段代码是用于复制图像数据的函数 memcpy_picture。以下是对函数的概括总结:
函数接受三个参数:目标缓冲区 out,源数据缓冲区 buf,以及源数据大小 size。
首先检查源数据是否使用哈夫曼编码,通过调用 is_huffman 函数来判断。
如果源数据不是哈夫曼编码:
初始化指针 ptdeb、ptlimit 和 ptcur,分别指向源数据的起始位置、结束位置和当前位置。
在源数据中查找标识为 0xffc0 的位置,表示图像数据的起始。
如果没有找到标识,说明源数据不完整,函数返回当前位置 pos。
计算需要复制的前半部分的大小 sizein。
将前半部分复制到目标缓冲区 out 中,并更新 pos。
将 DHT 数据复制到目标缓冲区 out 中,并更新 pos。
将后半部分复制到目标缓冲区 out 中,并更新 pos。
如果源数据使用哈夫曼编码:
将源数据直接复制到目标缓冲区 out 中,并更新 pos。
返回当前位置 pos。
该函数用于将图像数据复制到目标缓冲区,并根据是否使用哈夫曼编码进行相应的处理。如果源数据不完整或者不是哈夫曼编码,函数将返回当前复制的位置,否则返回复制完成后的位置。
// 复制图片
int memcpy_picture(unsigned char *out, unsigned char *buf, int size)
{
unsigned char *ptdeb, *ptlimit, *ptcur = buf;
int sizein, pos = 0;
if(!is_huffman(buf)) { // 如果不是哈夫曼编码
ptdeb = ptcur = buf; // 设置起始位置
ptlimit = buf + size; // 设置结束位置
while((((ptcur[0] << 8) | ptcur[1]) != 0xffc0) && (ptcur < ptlimit)) // 查找0xffc0
ptcur++; // 移动指针
if(ptcur >= ptlimit) // 如果指针超出范围
return pos; // 返回当前位置
sizein = ptcur - ptdeb; // 计算需要复制的大小
memcpy(out + pos, buf, sizein); pos += sizein; // 复制前半部分
memcpy(out + pos, dht_data, sizeof(dht_data)); pos += sizeof(dht_data); // 复制DHT数据
memcpy(out + pos, ptcur, size - sizein); pos += size - sizein; // 复制后半部分
} else { // 如果是哈夫曼编码
memcpy(out + pos, ptcur, size); pos += size; // 直接复制
}
return pos; // 返回当前位置
}
输出初始化output_init
该函数接受两个参数:一个名为 param 的指向 output_parameter 结构体的指针和一个整数 id。
以下是代码的执行过程:
初始化变量:
port 被设为 htons(8080) 的结果,将端口号转换为网络字节顺序。
credentials 和 www_folder 被设为 NULL。
nocommands 被设为 0。
使用 DBG 宏打印调试信息,指示输出编号。
将 param->argv(一个字符串数组)的第一个元素设为 OUTPUT_PLUGIN_NAME。
使用 getopt_long_only 循环遍历命令行选项:
如果遇到无法识别的选项,调用 help 函数并返回 1。
否则,根据 option_index 的值进行切换:
Case 0 和 1:使用 help 函数打印帮助信息并返回 1。
Case 2 和 3:解析端口选项(-p 或 --port),将提供的值转换为网络字节顺序后设置给 port。
Case 4 和 5:解析凭证选项(-c 或 --credentials),为 credentials 分配内存并复制提供的值。
Case 6 和 7:解析 WWW 选项(-w 或 --www),为 www_folder 分配内存并复制提供的值。如果值不以斜杠结尾,则添加斜杠。
Case 8 和 9:将 nocommands 设为 1,表示禁用命令。
根据解析的选项设置服务器的配置值:
将 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 宏打印配置值。
返回 0 表示初始化成功。
int output_init(output_parameter *param, int id)
{
int i;
int port;
char *credentials, *www_folder;
char nocommands;
DBG("output #%02d\n", param->id);
port = htons(8080);
credentials = NULL;
www_folder = NULL;
nocommands = 0;
param->argv[0] = OUTPUT_PLUGIN_NAME;
/* show all parameters for DBG purposes */
for(i = 0; i < param->argc; i++) {
DBG("argv[%d]=%s\n", i, param->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(param->argc, param->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[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;
}
启动摄像头输出线程
server_thread
服务器线程函数,用于接受客户端的连接请求并创建子线程处理每个客户端连接。下面是代码的主要步骤:
初始化变量和数据结构。
获取服务器地址信息,包括IP地址和端口号。
创建套接字,并设置套接字选项。
绑定套接字到服务器地址。
监听套接字,等待客户端连接。
循环等待客户端连接请求,使用
select函数等待可读套接字。
当有客户端连接时,创建一个子线程来处理连接。
子线程通过调用accept函数接受客户端连接,并传递连接套接字和上下文信息给子线程。
在子线程中处理客户端请求和响应。
主线程继续等待下一个客户端连接。
当收到停止信号时,退出循环,关闭套接字,并执行清理函数。
// 服务器线程函数
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);
// 初始化hints结构体
bzero(&hints, sizeof(hints));
hints.ai_family = PF_UNSPEC; // 支持IPv4和IPv6
hints.ai_flags = AI_PASSIVE; // 用于bind函数,表示返回的套接字地址结构体中的IP地址是通配地址
hints.ai_socktype = SOCK_STREAM; // TCP协议
// 获取地址信息
snprintf(name, sizeof(name), "%d", ntohs(pcontext->conf.port)); // 将端口号转换为字符串
if((err = getaddrinfo(NULL, name, &hints, &aip)) != 0) { // 获取地址信息
perror(gai_strerror(err)); // 输出错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 初始化所有套接字为-1
for(i = 0; i < MAX_SD_LEN; i++)
pcontext->sd[i] = -1;
/* open sockets for server (1 socket / address family) */
i = 0;
for(aip2 = aip; aip2 != NULL; aip2 = aip2->ai_next) {
if((pcontext->sd[i] = socket(aip2->ai_family, aip2->ai_socktype, 0)) < 0) { // 创建套接字
continue;
}
/* ignore "socket already in use" errors */
on = 1;
if(setsockopt(pcontext->sd[i], 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[i], 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[i], aip2->ai_addr, aip2->ai_addrlen) < 0) { // 绑定套接字
perror("bind");
pcontext->sd[i] = -1;
continue;
}
if(listen(pcontext->sd[i], 10) < 0) { // 监听套接字
perror("listen");
pcontext->sd[i] = -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[i] != -1) {
FD_SET(pcontext->sd[i], &selectfds);
if(pcontext->sd[i] > max_fds)
max_fds = pcontext->sd[i];
}
}
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[i] != -1 && FD_ISSET(pcontext->sd[i], &selectfds)) {
pcfd->fd = accept(pcontext->sd[i], (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");
#if 0
/* commented out as it fills up syslog with many redundant entries */
if(getnameinfo((struct sockaddr *)&client_addr, addr_len, name, sizeof(name), NULL, 0, NI_NUMERICHOST) == 0) {
syslog(LOG_INFO, "serving client: %s\n", name);
}
#endif
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;
}
设置 SO_REUSEADDR 选项。
该选项允许在套接字关闭后立即重新使用相同的地址。
通过设置 SO_REUSEADDR 选项,可以在套接字关闭后,立即重新使用相同的地址,而不需要等待操作系统释放该地址的等待时间。这在处理服务器应用程序时很常见,因为服务器通常会频繁地启动和关闭,并在相同的地址上监听连接。
/* ignore "socket already in use" errors */
on = 1;
if(setsockopt(pcontext->sd[i], SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { // 设置套接字选项
perror("setsockopt(SO_REUSEADDR) failed");
}
设置 IPV6_V6ONLY 选项。
该选项用于在 IPv6 套接字上限制仅接受 IPv6 连接,以避免 IPv4 连接进入 IPv6 套接字。
通过设置 IPV6_V6ONLY 选项,可以确保 IPv6 套接字仅接受 IPv6 连接,防止 IPv4 连接进入该套接字。这对于处理同时支持 IPv4 和 IPv6 的服务器应用程序非常重要,以确保连接按照正确的协议进行处理。
/* 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[i], IPPROTO_IPV6, IPV6_V6ONLY,
(const void *)&on , sizeof(on)) < 0) { // 设置套接字选项
perror("setsockopt(IPV6_V6ONLY) failed");
}
绑定地址/开始监听
通过 bind 函数将套接字与指定地址绑定,并使用 listen 函数开始监听连接请求。
/* perhaps we will use this keep-alive feature oneday */
/* setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); */
if(bind(pcontext->sd[i], aip2->ai_addr, aip2->ai_addrlen) < 0) { // 绑定套接字
perror("bind");
pcontext->sd[i] = -1;
continue;
}
if(listen(pcontext->sd[i], 10) < 0) { // 监听套接字
perror("listen");
pcontext->sd[i] = -1;
} else {
i++;
if(i >= MAX_SD_LEN) {
OPRINT("%s(): maximum number of server sockets exceeded", __FUNCTION__);
i--;
break;
}
}
/* 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[i] != -1) {
FD_SET(pcontext->sd[i], &selectfds);
if(pcontext->sd[i] > max_fds)
max_fds = pcontext->sd[i];
}
}
err = select(max_fds + 1, &selectfds, NULL, NULL, NULL); // 等待客户端连接
if(err < 0 && errno != EINTR) { // 如果出错,程序退出
perror("select");
exit(EXIT_FAILURE);
}
} while(err <= 0); // 如果没有客户端连接,继续等待
等待客户端连接
当有客户端连接请求到达时,将会创建一个子进程来处理该连接。这段代码使用 select 函数来实现非阻塞的等待,并通过文件描述符集合 selectfds 来管理待监听的套接字。
do { // 循环等待客户端连接
FD_ZERO(&selectfds); // 清空文件描述符集合
for(i = 0; i < MAX_SD_LEN; i++) { // 将所有套接字加入文件描述符集合
if(pcontext->sd[i] != -1) {
FD_SET(pcontext->sd[i], &selectfds);
if(pcontext->sd[i] > max_fds)
max_fds = pcontext->sd[i];
}
}
err = select(max_fds + 1, &selectfds, NULL, NULL, NULL); // 等待客户端连接
if(err < 0 && errno != EINTR) { // 如果出错,程序退出
perror("select");
exit(EXIT_FAILURE);
}
} while(err <= 0); // 如果没有客户端连接,继续等待
处理与客户端建立的连接。
对于每个就绪的套接字,它会创建一个新线程来处理客户端连接,并将相关的套接字和上下文信息传递给线程函数。线程函数 client_thread 负责实际处理客户端连接的逻辑。主线程继续循环等待并处理更多的连接请求。
for(i = 0; i < max_fds + 1; i++) {
if(pcontext->sd[i] != -1 && FD_ISSET(pcontext->sd[i], &selectfds)) {
pcfd->fd = accept(pcontext->sd[i], (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");
#if 0
/* commented out as it fills up syslog with many redundant entries */
if(getnameinfo((struct sockaddr *)&client_addr, addr_len, name, sizeof(name), NULL, 0, NI_NUMERICHOST) == 0) {
syslog(LOG_INFO, "serving client: %s\n", name);
}
#endif
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); // 分离线程
}
}
}
client_thread
这段代码是一个HTTP客户端线程的函数实现。它接收一个指向cfd结构的指针作为参数,然后进行一系列的操作来处理客户端的请求。
以下是代码的主要流程:
初始化变量和数据结构。
读取客户端的请求行。
根据请求行确定请求的类型,并设置相应的标记。
解析HTTP请求的其余部分,包括请求头和可选的用户名密码验证信息。
检查用户名和密码是否匹配配置文件中的设置。
根据请求类型处理请求,可能涉及发送快照、发送流、执行命令、发送插件描述符JSON文件或发送文件等操作。
关闭文件描述符,释放请求相关的内存。
需要注意的是,代码中的部分逻辑可能与具体的应用程序有关,例如根据配置文件限制命令的执行或检查输入编号的范围等。
/* thread for clients that connected to this server */
void *client_thread(void *arg)
{
int cnt;
char input_suffixed = 0;
int input_number = 0;
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 */ // 我们确实需要文件描述符,并且它必须由我们释放
if(arg != NULL) {
memcpy(&lcfd, arg, sizeof(cfd));
free(arg);
} else
return NULL;
/* initializes the structures */ // 初始化结构体
init_iobuffer(&iobuf);
init_request(&req);
/* What does the client want to receive? Read the request. */ // 客户端想要接收什么?读取请求
memset(buffer, 0, sizeof(buffer));
if((cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1) { // 读取请求行
close(lcfd.fd);
return NULL;
}
/* 确定要提供什么 */
if(strstr(buffer, "GET /?action=snapshot") != NULL) { // 如果请求是获取快照
req.type = A_SNAPSHOT; // 设置请求类型为获取快照
#ifdef WXP_COMPAT
} else if((strstr(buffer, "GET /cam") != NULL) && (strstr(buffer, ".jpg") != NULL)) { // 如果请求是获取jpg格式的快照
req.type = A_SNAPSHOT; // 设置请求类型为获取快照
#endif
input_suffixed = 255; // 标记请求中是否包含插件编号
} else if(strstr(buffer, "GET /?action=stream") != NULL) { // 如果请求是获取流
input_suffixed = 255; // 标记请求中是否包含插件编号
req.type = A_STREAM; // 设置请求类型为获取流
#ifdef WXP_COMPAT
} else if((strstr(buffer, "GET /cam") != NULL) && (strstr(buffer, ".mjpg") != NULL)) { // 如果请求是获取mjpg格式的流
req.type = A_STREAM; // 设置请求类型为获取流
#endif
input_suffixed = 255; // 标记请求中是否包含插件编号
} else if((strstr(buffer, "GET /input") != NULL) && (strstr(buffer, ".json") != NULL)) { // 如果请求是获取输入插件的json格式数据
req.type = A_INPUT_JSON; // 设置请求类型为获取输入插件的json格式数据
input_suffixed = 255; // 标记请求中是否包含插件编号
} else if((strstr(buffer, "GET /output") != NULL) && (strstr(buffer, ".json") != NULL)) { // 如果请求是获取输出插件的json格式数据
req.type = A_OUTPUT_JSON; // 设置请求类型为获取输出插件的json格式数据
input_suffixed = 255; // 标记请求中是否包含插件编号
} else if(strstr(buffer, "GET /program.json") != NULL) { // 如果请求是获取程序的json格式数据
req.type = A_PROGRAM_JSON; // 设置请求类型为获取程序的json格式数据
input_suffixed = 255; // 标记请求中是否包含插件编号
} 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"); // a pb points to thestring after the first & after 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); // 复制参数
if(unescape(req.parameter) == -1) { // 如果解码失败
free(req.parameter); // 释放参数内存
send_error(lcfd.fd, 500, "could not properly unescape command parameter string"); // 发送错误信息
LOG("could not properly unescape command parameter string\n"); // 输出调试信息
close(lcfd.fd); // 关闭文件描述符
return NULL; // 返回空指针
}
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 /"); // 跳过"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); // 输出调试信息
}
/*
* 当我们使用多个输入插件时,有些url可能会有一个_[插件编号后缀]
* 为了兼容性,可以在这种情况下保留,输出将从第0个输入插件生成
*/
if(input_suffixed) {
char *sch = strchr(buffer, '_');
if(sch != NULL) { // 如果url中有_,则输入编号应该存在
DBG("sch %s\n", sch + 1); // FIXME 如果添加了超过10个输入插件
char numStr[3];
memset(numStr, 0, 3);
strncpy(numStr, sch + 1, 1);
input_number = atoi(numStr);
}
DBG("input plugin_no: %d\n", input_number);
}
/*
* 解析HTTP请求的其余部分
* 请求头的结尾由一个单独的空行"\r\n"标记
*/
do {
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;
}
if(strstr(buffer, "User-Agent: ") != NULL) {
req.client = strdup(buffer + strlen("User-Agent: "));
} else if(strstr(buffer, "Authorization: Basic ") != NULL) {
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'));
/* 如果给出了参数-c,则检查用户名和密码 */
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");
}
/* 现在是回应请求的时候 */
if(!(input_number < pglobal->incnt)) { // 如果输入编号超出范围
DBG("Input number: %d out of range (valid: 0..%d)\n", input_number, pglobal->incnt-1); // 输出调试信息
send_error(lcfd.fd, 404, "Invalid input plugin number"); // 发送错误信息
req.type = A_UNKNOWN; // 设置请求类型为未知
}
switch(req.type) { // 根据请求类型进行处理
case A_SNAPSHOT: // 请求快照
DBG("Request for snapshot from input: %d\n", input_number); // 输出调试信息
send_snapshot(lcfd.fd, input_number); // 发送快照
break;
case A_STREAM: // 请求流
DBG("Request for stream from input: %d\n", input_number); // 输出调试信息
send_stream(lcfd.fd, input_number); // 发送流
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_INPUT_JSON: // 请求输入插件描述符JSON文件
DBG("Request for the Input plugin descriptor JSON file\n"); // 输出调试信息
send_Input_JSON(lcfd.fd, input_number); // 发送输入插件描述符JSON文件
break;
case A_OUTPUT_JSON: // 请求输出插件描述符JSON文件
DBG("Request for the Output plugin descriptor JSON file\n"); // 输出调试信息
send_Output_JSON(lcfd.fd, input_number); // 发送输出插件描述符JSON文件
break;
case A_PROGRAM_JSON: // 请求程序描述符JSON文件
DBG("Request for the program descriptor JSON file\n"); // 输出调试信息
send_Program_JSON(lcfd.fd); // 发送程序描述符JSON文件
break;
case A_FILE: // 请求文件
if(lcfd.pc->conf.www_folder == NULL) // 如果没有配置www文件夹
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;
}
_readline
函数_read用于从文件描述符中读取数据,并将读取的数据存储到缓冲区中。下面是函数的主要步骤:
初始化变量和数据结构。
循环读取数据,直到满足读取长度的要求。
使用select函数等待数据到达或超时。
调用read函数从文件描述符中读取数据。
将读取的数据存储到缓冲区中,并更新相关计数器。
如果读取的字节数小于缓冲区大小,将数据移动到缓冲区末尾。
返回已读取的字节数。
函数_readline是基于_read函数的封装,用于从文件描述符中读取一行数据,直到遇到换行符或达到最大长度。它调用_read函数逐个读取字符,并将字符存储到指定的缓冲区中,直到满足结束条件。返回读取的字符数或-1(表示超时或出错)。
这两个函数可能是某个网络或文件处理程序中的一部分,用于读取和处理输入数据。它们在循环读取和处理数据时提供了超时机制,以防止程序永久阻塞。
int _read(int fd, iobuffer *iobuf, void *buffer, size_t len, int timeout)
{
int copied = 0, rc, i;
fd_set fds;
struct timeval tv;
memset(buffer, 0, len); // 将buffer清零
while((copied < len)) { // 循环读取数据
i = MIN(iobuf->level, len - copied); // 计算需要读取的字节数
memcpy(buffer + copied, iobuf->buffer + IO_BUFFER - iobuf->level, i); // 将读取到的数据存入buffer中
iobuf->level -= i; // 更新iobuf中的level
copied += i; // 更新已读取的字节数
if(copied >= len) // 如果已读取的字节数等于需要读取的字节数,返回已读取的字节数
return copied;
/* select将在超时或有新数据到达时返回 */
tv.tv_sec = timeout;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(fd, &fds);
if((rc = select(fd + 1, &fds, NULL, NULL, &tv)) <= 0) { // 调用select函数等待数据到达或超时
if(rc < 0) // 如果返回值小于0,说明出错
exit(EXIT_FAILURE);
/* 这里一定是超时 */
return copied; // 返回已读取的字节数
}
init_iobuffer(iobuf); // 初始化iobuf
/*
* 由于select函数已经返回,所以这里应该至少有一个字节可读
* 但是,由于在select和read之间,远程socket可能会关闭,所以不能保证一定有数据可读
*/
if((iobuf->level = read(fd, &iobuf->buffer, IO_BUFFER)) <= 0) { // 调用read函数读取数据
/* 出错了 */
return -1; // 返回-1
}
/* 如果读取的字节数小于IO_BUFFER,将数据移动到缓冲区末尾 */
memmove(iobuf->buffer + (IO_BUFFER - iobuf->level), iobuf->buffer, iobuf->level);
}
return 0;
}
/******************************************************************************
Description.: Read a single line from the provided fildescriptor.
This funtion will return under two conditions:
* line end was reached
* timeout occured
Input Value.: * fd.....: fildescriptor to read from
* iobuf..: iobuffer that allows to use this functions from multiple
threads because the complete context is the iobuffer.
* buffer.: The buffer to store values at, will be set to zero
before storing values.
* len....: the length of buffer
* timeout: seconds to wait for an answer
Return Value: * buffer.: will become filled with bytes read
* iobuf..: May get altered to save the context for future calls.
* func().: bytes copied to buffer or -1 in case of error
******************************************************************************/
/* read just a single line or timeout */
int _readline(int fd, iobuffer *iobuf, void *buffer, size_t len, int timeout)
{
char c = '\0', *out = buffer; // 定义字符变量c和指向buffer的指针out
int i;
memset(buffer, 0, len); // 将buffer清零
for(i = 0; i < len && c != '\n'; i++) { // 循环读取每个字符,直到读取到换行符或达到最大长度
if(_read(fd, iobuf, &c, 1, timeout) <= 0) { // 调用_read函数读取一个字符,如果返回值小于等于0,说明超时或出错
/* timeout or error occured */ // 超时或出错
return -1; // 返回-1
}
*out++ = c; // 将读取到的字符存入buffer中
}
return i; // 返回读取到的字符数
}
send_snapshot
函数send_snapshot用于将快照数据发送给客户端。下面是对函数的概括:
等待获取新的一帧数据。
锁定输入缓冲区的互斥锁,并等待输入缓冲区的更新条件变量。
读取输入缓冲区的帧大小。
为当前帧分配内存空间。
将输入缓冲区的时间戳复制到用户空间。
将输入缓冲区的帧数据复制到分配的内存空间中。
解锁输入缓冲区的互斥锁。
构建响应头部信息,包括HTTP状态行、标准头部和图片类型等。
将响应头部发送到客户端。
将帧数据发送到客户端。
释放帧数据的内存空间。
/* 发送快照给客户端 */
void send_snapshot(int fd, int input_number)
{
unsigned char *frame = NULL;
int frame_size = 0;
char buffer[BUFFER_SIZE] = {0};
struct timeval timestamp;
/* 等待获取新的一帧 */
pthread_mutex_lock(&pglobal->in[input_number].db);
pthread_cond_wait(&pglobal->in[input_number].db_update, &pglobal->in[input_number].db);
/* 读取缓冲区 */
frame_size = pglobal->in[input_number].size;
/* 为这一帧分配一个缓冲区 */
if((frame = malloc(frame_size + 1)) == NULL) {
free(frame);
pthread_mutex_unlock(&pglobal->in[input_number].db);
send_error(fd, 500, "not enough memory");
return;
}
/* 将 v4l2_buffer 的时间戳复制到用户空间 */
timestamp = pglobal->in[input_number].timestamp;
memcpy(frame, pglobal->in[input_number].buf, frame_size);
DBG("got frame (size: %d kB)\n", frame_size / 1024);
pthread_mutex_unlock(&pglobal->in[input_number].db);
/* 写入响应 */
sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
STD_HEADER \
"Content-type: image/jpeg\r\n" \
"X-Timestamp: %d.%06d\r\n" \
"\r\n", (int) timestamp.tv_sec, (int) timestamp.tv_usec);
/* 现在发送头和图像 */
if(write(fd, buffer, strlen(buffer)) < 0 || \
write(fd, frame, frame_size) < 0) {
free(frame);
return;
}
free(frame);
}
send_stream
函数send_stream用于发送视频流给客户端。下面是对函数的概括:
准备HTTP响应头部信息,包括状态行、标准头部和多部分数据流的Content-Type等。
将响应头部发送给客户端。
在循环中,等待获取新的一帧数据。
锁定输入缓冲区的互斥锁,并等待输入缓冲区的更新条件变量。
读取输入缓冲区的帧大小。
检查帧缓冲区的大小是否足够,如果不够则增加缓冲区的大小。
将输入缓冲区的时间戳复制到用户空间。
将输入缓冲区的帧数据复制到帧缓冲区中。
解锁输入缓冲区的互斥锁。
构建帧的响应头部信息,包括Content-Type、Content-Length和时间戳等。
将帧的响应头部发送给客户端。
将帧数据发送给客户端。
发送分隔符boundary。
重复步骤3-13直到停止条件满足。
释放帧缓冲区的内存空间。
void send_stream(int fd, int input_number)
{
unsigned char *frame = NULL, *tmp = NULL;
int frame_size = 0, max_frame_size = 0;
char buffer[BUFFER_SIZE] = {0};
struct timeval timestamp;
DBG("preparing header\n");
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) {
/* 等待获取新的一帧 */
pthread_mutex_lock(&pglobal->in[input_number].db);
pthread_cond_wait(&pglobal->in[input_number].db_update, &pglobal->in[input_number].db);
/* 读取缓冲区 */
frame_size = pglobal->in[input_number].size;
/* 检查帧缓冲区是否足够大,如果不够大则增加缓冲区大小 */
if(frame_size > max_frame_size) {
DBG("增加缓冲区大小到 %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->in[input_number].db);
send_error(fd, 500, "内存不足");
return;
}
frame = tmp;
}
/* 将 v4l2_buffer 的时间戳复制到用户空间 */
timestamp = pglobal->in[input_number].timestamp;
memcpy(frame, pglobal->in[input_number].buf, frame_size);
DBG("got frame (size: %d kB)\n", frame_size / 1024);
pthread_mutex_unlock(&pglobal->in[input_number].db);
/*
* 打印单个 mimetype 和长度
* 发送内容长度可以修复在 firefox 中观察到的随机流中断
*/
sprintf(buffer, "Content-Type: image/jpeg\r\n" \
"Content-Length: %d\r\n" \
"X-Timestamp: %d.%06d\r\n" \
"\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
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);
}