mjpg-streamer实现细节分析

  • Post author:
  • Post category:其他




mjpg-streamer实现细节分析



在这里插入图片描述



输入初始化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, &currentFormat) == 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);
}

如果文章对您有帮助,点赞👍支持,感谢🤝



版权声明:本文为weixin_52849254原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。