mjpg-streamer实现细节分析

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, &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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/22851.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

uvc驱动ioctl分析下

uvc驱动ioctl分析下 文章目录 uvc驱动ioctl分析下uvc_ioctl_enum_input枚举输入uvc_query_ctrl__uvc_query_ctrluvc_ioctl_g_input 获取输入uvc_ioctl_s_input 设置输入uvc_query_v4l2_ctrluvc_ioctl_queryctrl查询控制器uvc_ioctl_query_ext_ctrl查询扩展控制器 uvc_ioctl_g_c…

爬虫基本原理

爬虫基本原理 1.1获取网页1.1.1提取信息1.1.2保存数据 1.2请求1.2.1 请求方法1.2.2 请求网址1.2.3 请求头1.2.4请求体1.3响应 1.1获取网页 爬虫首先要做的工作就是获取网页&#xff0c;这里就是获取网页的源代码。源代码里包含了网页的部分有用信息&#xff0c;所以只要把源代…

【航空和卫星图像中检测建筑物】使用gabor特征和概率的城市区域和建筑物检测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

如何把数据从 TDengine 2.x 迁移到 3.x ?

一.迁移背景&#xff1a; 随着时序数据库&#xff08;Time Series Database&#xff09; TDengine 3.0 的发布至今&#xff0c;我们除了在持续地优化产品质量的本身&#xff0c;也一直在努力地提升用户体验。但由于 3.0 底层有大量的重构优化&#xff0c;导致开源版的 2.0 用户…

免费部署你的私人 ChatGPT 网页应用

免费部署你的私人 ChatGPT 网页应用 1、注册Github账号&#xff0c;拷贝仓库 第一步、打开GitHub官网&#xff0c;点击右上角Sign up注册即可 第二步、打开开源项目【Chatgpt-next-web】,点击fork&#xff0c;点击Create fork完成操作 2、选择免费的容器【vercel】或者【r…

2023新版Spring6全新讲解-核心内容之IoC

Spring核心之IoC 一、IoC概念介绍 1.IoC 介绍 IoC 是 Inversion of Control 的简写&#xff0c;译为“控制反转”&#xff0c;它不是一门技术&#xff0c;而是一种设计思想&#xff0c;是一个重要的面向对象编程法则&#xff0c;能够指导我们如何设计出松耦合、更优良的程序。…

USB摄像头描述符参数获取和来源分析

USB摄像头描述符参数获取和来源分析 文章目录 USB摄像头描述符参数获取和来源分析描述符USB设备描述符描述符 USB摄像头参数获取myuvc.c结果device descriptor设备描述符configuration descriptor配置描述符interface association接口关联inteface desciptor atsettingvideocon…

Linux :: 【基础指令篇 :: 用户管理:(2)】::设置用户密码(及本地Xshell 登录云服务器操作演示) :: passwd

前言&#xff1a;本篇是 Linux 基本操作篇章的内容&#xff01; 笔者使用的环境是基于腾讯云服务器&#xff1a;CentOS 7.6 64bit。 目录索引&#xff1a; 1. 基本语法 2. 基本用法 3. 注意点 4. 补充&#xff1a;指定用户设置密码操作实例测试及登录本地 Xshell 登录演…

前端微服务无界实践 | 京东云技术团队

一、前言 随着项目的发展&#xff0c;前端SPA应用的规模不断加大、业务代码耦合、编译慢&#xff0c;导致日常的维护难度日益增加。同时前端技术的发展迅猛&#xff0c;导致功能扩展吃力&#xff0c;重构成本高&#xff0c;稳定性低。因此前端微服务应运而生。 前端微服务优势…

什么是智慧校园?

什么是智慧校园&#xff1f; 智慧校园平台是目前教育信息化领域的热点之一。 随着数字化转型的加速&#xff0c;越来越多的学校开始寻求解决方案&#xff0c;以提高教育管理的效率和质量。 在使用智慧校园平台的过程中&#xff0c;一些痛点问题也浮现出来。为解决这些问题&a…

10 工具Bootchart的使用(windows)

Bootchart的使用方法&#xff08;windows&#xff09; 下载bootchart.jar并拷贝到windows, 然后保证windows也安装了open jdk 1.8; 下载地址&#xff1a;https://download.csdn.net/download/Johnny2004/87807973 打开设备开机启动bootchart的开关: adb shell touch /data/boo…

DID-M3D 论文学习

1. 解决了什么问题&#xff1f; 单目 3D 检测成本低、配置简单&#xff0c;对一张 RGB 图像预测 3D 空间的 3D 边框。最难的任务就是预测实例深度&#xff0c;因为相机投影后会丢失深度信息。以前的方法大多直接预测深度&#xff0c;本文则指出 RGB 图像上的实例深度不是一目了…

【学习日记2023.5.22】 之 套餐模块完善

4. 功能模块完善之套餐模块 4.1 新增套餐 4.1.1 需求分析与设计 产品原型 后台系统中可以管理套餐信息&#xff0c;通过 新增功能来添加一个新的套餐&#xff0c;在添加套餐时需要添加套餐对应菜品的信息&#xff0c;并且需要上传套餐图片。 新增套餐原型&#xff1a; 当填…

自动化如何做?爆肝整理企业自动化测试工具/框架选择实施,你要的都有...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

web基础与HTTP协议

web基础与HTTP协议 一、域名概述二、网页的概念三、HTML四、web概述静态网页&#xff1a;动态页面动态页面与静态页面的区别 五、HTTP 一、域名概述 域名的概念&#xff1a;IP地址不易记忆 早期使用Hosts文件解析域名 – 主机名称重复 – 主机维护困难 DNS&#xff08;域名系…

大学4年做出来这个算不算丢人

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

开发者关系工程师如何帮助开发者在Sui上构建

近期&#xff0c;我们与Sui开发者关系负责人Brian Hennessey-Hsien进行了对话&#xff0c;就Sui上的开源、去中心化和开发者成就等话题展开讨论。 日前&#xff0c;我们采访了Sui基金会的开发者关系负责人Brian Hennessey-Hsieh&#xff0c;共同探讨了其对于Web3中开发者发展历…

2009.03-2022.06华证ESG季度评级(季度)

2009.03-2022.06华证ESG评级&#xff08;季度&#xff09; 1、时间&#xff1a;2009.03-2022.06.15 2、来源&#xff1a;整理自Wind 3、指标&#xff1a;华证ESG&#xff08;只有综合评级&#xff0c;无细分评级数据&#xff09; 4、样本数量&#xff1a;A股4800多家公司 …

【数据安全-02】AI打假利器数字水印,及java+opencv实现

AIGC 的火爆引燃了数字水印&#xff0c;说实话数字水印并不是一项新的技术&#xff0c;但是这时候某些公司拿出来宣传一下特别应景&#xff0c;相应股票蹭蹭地涨。数字水印是什么呢&#xff0c;顾名思义&#xff0c;和我们在pdf中打的水印作用差不多&#xff0c;起到明确版权、…

拉货搬家货运APP开发分析和功能列表

作为国家经济发展的重要基础设施&#xff0c;物流行业正在面对转型升级的风口。巨大的市场体量&#xff0c;也迎来了激烈的市场竞争。为了从同质化的服务中脱颖而出&#xff0c;开拓更大的市场&#xff0c;并且解决线下司机的载货痛点&#xff0c;货运APP的开发必不可少。 开发…