【Linux】mjpg-streamer 源码分析

1.总体流程

mjpg-streamer Github
mjpg-streamer tree

目录组成:

文件夹名功能
cmakemakefile文件
plugins输入输出组件,用于采集及传输
scripts执行脚本
www用于浏览器功能代码

mjpg-streamer框架:

唤醒
socket
bind
listen
accept
创建client_thread线程
send_stream
pthread_cond_wait 等待仓库数据更新
write 通过socket发送帧图形
dlopen 打开组件
dlsym 获取相关函数
input_init 输入组件初始化
init_videoIn
init_v4l2
output_init 输出组件初始化
input_run
创建cam_thread线程
uvcGrab 获取一帧
memcpy_picture 拷贝图像
图像数据
pthread_cond_broadcast 唤醒发送帧数据
output_run
创建server_thread线程
  • dlopen 打开 输入组件(动态链接库 .so),有以下几种:

    • input_file.so
    • input_http.so
    • input_uvc.so
  • dlopen 打开 输出组件(动态链接库 .so),有以下几种:

    • output_file.so
    • output_http.so
    • output_udp.so
    • output_rtsp.so

mjpg-streamer plugins

2.主进程的源码分析

2.1 参数接收与解析

mjpg_streamer.c 中接收参数分析:

int main(int argc, char *argv[])
{
    //char *input  = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0"; //改为 mjpg-steamer 默认的参数
    char *input[MAX_INPUT_PLUGINS];
    char *output[MAX_OUTPUT_PLUGINS];
    int daemon = 0, i, j;
    size_t tmp = 0;

    output[0] = "output_http.so --port 8080";
    global.outcnt = 0;
    global.incnt = 0;

    while(1) {
        int c = 0;
        static struct option long_options[] = {
            {"help", no_argument, NULL, 'h'},
            {"input", required_argument, NULL, 'i'},
            {"output", required_argument, NULL, 'o'},
            {"version", no_argument, NULL, 'v'},
            {"background", no_argument, NULL, 'b'},
            {NULL, 0, NULL, 0}
        };

        c = getopt_long(argc, argv, "hi:o:vb", long_options, NULL);
        if(c == -1) break; // 用于判断是否解析完毕,解析完毕返回 -1
    ... ...

其中用于解析命令的为 getopt_long_only() 函数:

int getopt_long_only(int argc, char * const argv[],
          const char *optstring,
          const struct option *longopts, int *longindex);
  • 参数:

    • argc argv :直接从main函数传递而来,表示传递的参数
    • shortopts:短选项字符串。如”f:v",这里需要指出的是,短选项字符串不需要“-”,而且但选项需要传递参数时,在短选项后面加上“:”。eg.第一个短选项 f 后面带有冒号,: 表示短选项f后面是要指定参数的,如 -f 30。
    • longopts:struct option 数组,用于存放长选项参数。
    • longind:用于返回长选项在 longopts 结构体数组中的索引值,用于调试,一般置为NULL。eg.根据上述源代码传入了 -h 参数,则返回索引值 0。
  • 返回值:

    • 解析完毕,getopt_long_only 返回 -1
    • 出现未定义的长选项或者短选项,getopt_long 返回 ?

2.2 获取参数

        switch(c) {
        case 'i':
            input[global.incnt++] = strdup(optarg);
            break;

        case 'o':
            output[global.outcnt++] = strdup(optarg);
            break;

        case 'v':
            printf("MJPG Streamer Version: %s\n",SOURCE_VERSION);
            return 0;
            break;

        case 'b': // 后台模式
            daemon = 1;
            break;

        case 'h': /* fall through */
        default:
            help(argv[0]);
            exit(EXIT_FAILURE);
        }

当输入如下命令时:

mjpg_streamer -i "input_uvc.so -f 30 -r 1080*720" -o "output_http.so -w www"

接收到 -i 参数后,strdup(optarg) 用来获取相应的标记后的参数,即:

input[global.incnt++]  指向 "input_uvc.so -f 30 -r 1080*720" 字符串

strdup() 函数是 c 语言中常用的一种字符串拷贝库函数:
char *strdup(const char *s);

接收到 -o 参数,同理:

output[global.outcnt++] 指向  "output_http.so -w www"字符串

2.3 调用输入函数

dlopen() 函数负责打开 intput_uvc.so 插件, dlsym() 负责调用插件中的相关函数。

    /* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
    signal(SIGPIPE, SIG_IGN); // 用于忽略 SIGPIPE 信号

    /* register signal handler for <CTRL>+C in order to clean up */
    if(signal(SIGINT, signal_handler) == SIG_ERR) { // SIGINT信号代表由InterruptKey产生 ,当按下 ctrl+c时,调用 signal_handler,做清理工作
        ... ...
    }
    ... ...
    /* open input plugin */
    for(i = 0; i < global.incnt; i++) {
        /* this mutex and the conditional variable are used to synchronize access to the global picture buffer 即用于传输新的帧信号 */
        if(pthread_mutex_init(&global.in[i].db, NULL) != 0) { 
            ... ...
        }
        if(pthread_cond_init(&global.in[i].db_update, NULL) != 0) {
            ... ...
        }

        tmp = (size_t)(strchr(input[i], ' ') - input[i]); // tmp = "input_uvc.so" 字符串的长度
        global.in[i].stop      = 0;
        global.in[i].context   = NULL;
        global.in[i].buf       = NULL;
        global.in[i].size      = 0;
        global.in[i].plugin = (tmp > 0) ? strndup(input[i], tmp) : strdup(input[i]); // 复制前 tmp 的字符,即 global.in[i].plugin = "input_uvc.so" 
        global.in[i].handle = dlopen(global.in[i].plugin, RTLD_LAZY); // 打开  "input_ucv.so" 动态链接库       
        if(!global.in[i].handle) {
            ... ...
        }
        global.in[i].init = dlsym(global.in[i].handle, "input_init"); // global.in[i].init = "input_ucv.c" 里面的 input_init 函数
        ... ...
        global.in[i].stop = dlsym(global.in[i].handle, "input_stop"); // global.in[i].stop = "input_ucv.c" 里面的 input_stop 函数
        ... ...
        global.in[i].run = dlsym(global.in[i].handle, "input_run"); // global.in[i].run = "input_ucv.c" 里面的 input_run 函数
        ... ...
        /* try to find optional command */
        global.in[i].cmd = dlsym(global.in[i].handle, "input_cmd"); // global.in[i].cmd = "input_ucv.c" 里面的 input_cmd 函数

        global.in[i].param.parameters = strchr(input[i], ' '); // 参数字符串为 ' ' 后面的内容,即global.in[i].param.parameters = " -f 30 -r 1080*720"

        for (j = 0; j<MAX_PLUGIN_ARGUMENTS; j++) {
            global.in[i].param.argv[j] = NULL;
        }

        split_parameters(global.in[i].param.parameters, &global.in[i].param.argc, global.in[i].param.argv); // 分割参数,方便后续使用
        global.in[i].param.global = &global;
        global.in[i].param.id = i;

        if(global.in[i].init(&global.in[i].param, i)) { // 调用input_uvc.c 中的 input_init 函数
            LOG("input_init() return value signals to exit\n");
            closelog();
            exit(0);
        }
    }
    
    /* start to read the input, push pictures into global buffer */
    DBG("starting %d input plugin\n", global.incnt);
    for(i = 0; i < global.incnt; i++) {
        ... ...
        if(global.in[i].run(i)) { // 启动读取数据
            ... ...
        }
    }
    for(i = 0; i < global.outcnt; i++) {
        ... ...
        global.out[i].run(global.out[i].param.id);
    }
    
    /* wait for signals */
    pause(); // 等待信号

输出组件同理

2.3.1 程序手动中断信号

    /* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
    signal(SIGPIPE, SIG_IGN); // 用于忽略 SIGPIPE 信号

    /* register signal handler for <CTRL>+C in order to clean up */
    if(signal(SIGINT, signal_handler) == SIG_ERR) { // SIGINT信号代表由InterruptKey产生 ,当按下 ctrl+c时,调用 signal_handler,做清理工作

当按下 ctrl+c时,调用 signal_handler 函数用于做程序中断后的清理工作。

2.3.2 strchr()函数

char *strchr(const char *str, int c)
  • str :要被检索的字符串
  • c : 在 str 中要搜索的字符

该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。

示例测试:

#include <stdio.h>
#include <string.h>
int main (int argc, char **argv)
{
    const char *input = "input_uvc.so -f 30 -r 1080*720";
    int tmp = (strchr(input, ' ') - input); // tmp = "input_uvc.so" 字符串的长度
    printf("input: %d, ' ': %d\n", input, strchr(input, ' '));
    printf("tmp = %d \n", tmp);
    printf("剩余的字符串 =%s \n", strchr(input, ' '));
    return 0;
}
$ .\strchr
input: 6422189, ' ': 6422201
tmp = 12
剩余的字符串 = -f 30 -r 1080*720

2.3.3 strndup()函数

char* strndup(const char* src, size_t len);

拷贝前 len 个字符,还自动为新的字符串添加一个’\0’表示结尾,返回 该新字符串的地址

2.3.4 分离参数

static int split_parameters(char *parameter_string, int *argc, char **argv)
{
    int count = 1;
    argv[0] = NULL; // the plugin may set it to 'INPUT_PLUGIN_NAME'
    if(parameter_string != NULL && strlen(parameter_string) != 0) {
        char *arg = NULL, *saveptr = NULL, *token = NULL;

        arg = strdup(parameter_string); // 拷贝,arg = " -f 30 -r 1080*720"

        if(strchr(arg, ' ') != NULL) {
            token = strtok_r(arg, " ", &saveptr); // 按照 " " 分割字符串
            if(token != NULL) {
                argv[count] = strdup(token); // argv[1] = "-f 
                count++;
                while((token = strtok_r(NULL, " ", &saveptr)) != NULL) { // 一直按照 " " 分割字符串,直到分割完毕
                    argv[count] = strdup(token); // argv[count++] = 后续参数
                    count++;
                    if(count >= MAX_PLUGIN_ARGUMENTS) {
                        IPRINT("ERROR: too many arguments to input plugin\n");
                        return 0;
                    }
                }
            }
        }
        free(arg);
    }
    *argc = count; // 保存参数个数
    return 1;
}

3.输入通道源码分析

input_uvc.c(plugins\input_uvc) 进行分析

3.1 input_init

int input_init(input_parameter *param, int id)

根据 main 函数传递的参数进行设置:

主要为 指定 USB 摄像头设备、分辨率、帧率、格式、质量、请求buf,队列buf以及一些其他的图像参数。

input_init 输入组件初始化
init_videoIn
init_v4l2

3.2 input_run

input_run
pthread_create 创建 cam_thread 线程
pthread_detach
uvcGrab 获取一帧
memcpy_picture 拷贝图像
int input_run(int id)
{
    input * in = &pglobal->in[id];
    context *pctx = (context*)in->context;
    // 给仓库分配一帧的空间
    in->buf = malloc(pctx->videoIn->framesizeIn);
    ... ...
    // 创建 cam_thread 线程
    pthread_create(&(pctx->threadID), NULL, cam_thread, in);
    // 等待线程执行完,然后回收其资源
    pthread_detach(pctx->threadID);
    return 0;
}

3.3 cam_thread

void *cam_thread(void *arg)
{
    ... ...
    // 当线程执行完后,会调用 cam_cleanup,做一些清理回收工作
    pthread_cleanup_push(cam_cleanup, in);
    ... ...
    // 使能视频捕获设备
    if (video_enable(pcontext->videoIn)) {
        ... ...
    }
    // 当 pglobal->stop = 0时,一直执行while,当按下 Crtl+C时(signal_handler函数),pglobal->stop = 1,停止执行
    while(!pglobal->stop) {
        while(pcontext->videoIn->streamingState == STREAMING_PAUSED) {
            usleep(1); // maybe not the best way so FIXME
        }
        ... ...
        if (FD_ISSET(pcontext->videoIn->fd, &rd_fds)) {
            // 获取一帧数据
            if(uvcGrab(pcontext->videoIn) < 0) {
                ... ...
            }
        ... ...
        }
    }
}

4.输出通道源码分析

input_uvc.c(plugins\output_http) 进行分析

4.1 output_init

int output_init(output_parameter *param, int id)
{
    ... ...
    servers[param->id].id = param->id;
    servers[param->id].pglobal = param->global;
    servers[param->id].conf.port = port;
    servers[param->id].conf.hostname = hostname;
    servers[param->id].conf.credentials = credentials;
    servers[param->id].conf.www_folder = www_folder;
    servers[param->id].conf.nocommands = nocommands;
    ... ...
}

根据 main 函数传递的参数进行设置,省略的部分为解析命令参数,可以看到该函数主要为 给相关变量进行赋值

主要为 指定 端口号、IP地址、文件路径等

4.2 output_run

socket
bind
listen
accept
pthread_create 创建client_thread线程
send_stream
pthread_cond_wait 等待仓库数据更新
write 发送帧图形
output_run
pthread_create 创建server_thread线程
int output_run(int id)
{
    ... ...
    /* create thread and pass context to thread function */
    pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));
    // 等待线程结束,以便回收资源
    pthread_detach(servers[id].threadID);

    return 0;
}

同理,也是创建一个线程 server_thread

4.3 server_thread

void *server_thread(void *arg)
{
    ... ...
    // 当线程结束时,会调用 server_cleanup 进行相关的清理工作
    pthread_cleanup_push(server_cleanup, pcontext);
    ... ...
    /* 以下为 socket网络编程 创建并连接客户端 */
    ... ...
}

该线程主要为 创建并连接客户端,并创建 client_thread

4.4 client_thread

void *client_thread(void *arg)
{
    ... ...
    // iobuf清零
    init_iobuffer(&iobuf);
    // http协议,需要客户端给服务器发送一个请求,因此初始化一个req
    init_request(&req);
    ... ...
    // 从客户端读取一行数据,以换行符为结束
    if((cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1) {
        ... ...
    }
    ... ...
    // 如果请求字符串为"GET /?action=snapshot",修改请求类型为 拍照
    if(strstr(buffer, "GET /?action=snapshot") != NULL) {
        req.type = A_SNAPSHOT; 
        ... ...
    }
    ... ...
    // 如果请求字符串为"GET /?action=stream",修改请求类型为 stream 视频流
    else if(strstr(buffer, "GET /?action=stream") != NULL) {
        req.type = A_STREAM;
        ... ...
    }
    ... ...
    do {
        ... ...
        // 再一次从客户端读取一行数据
        if((cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1) {
            ... ...
        }
        // 解析buffer,若其中包含 用户名,则将用户名保存至req.client
        if(strcasestr(buffer, "User-Agent: ") != NULL) {
            req.client = strdup(buffer + strlen("User-Agent: "));
        } 
        // 如何包含了密码,则将密码保存至req.credentials
        else if(strcasestr(buffer, "Authorization: Basic ") != NULL) {
            req.credentials = strdup(buffer + strlen("Authorization: Basic "));
            // 对密码进行解码
            decodeBase64(req.credentials);
            ... ...
        }
    // 字符串<=2字节(除回车换行),不使用该功能
    } while(cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n'));

    // 根据不同的请求,进行对应的操作
    switch(req.type) {
        case A_SNAPSHOT_WXP:
        case A_SNAPSHOT:
            send_snapshot(&lcfd, input_number);
            break;
        case A_STREAM:
            send_stream(&lcfd, input_number);
            ... ...
    }
}

_readline 函数用于读取 client发送了什么请求,因此 客户端必须发送一个字符串,以换行符为结束。

4.5 send_stream

void send_stream(cfd *context_fd, int input_number)
{
    ... ...
    sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
            "Access-Control-Allow-Origin: *\r\n" \
            STD_HEADER \
            "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
            "\r\n" \
            "--" BOUNDARY "\r\n");
    // 发送报文头
    if(write(context_fd->fd, buffer, strlen(buffer)) < 0) {
        ... ...
    }
    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);
        ... ...
        // 从仓库中取出 一帧图像
        memcpy(frame, pglobal->in[input_number].buf, frame_size);
    
        pthread_mutex_unlock(&pglobal->in[input_number].db);
        ... ...
        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);
        // 发送报文,表明即将发送图像的大小 以及 时间戳
        if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break;
        
        if(write(context_fd->fd, frame, frame_size) < 0) break;
        // 发送报文,表明该帧结束
        sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
        if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break; 
    }   
    // 释放缓存
    free(frame);

若自己写对应的客户端:

  • 1.先发送一次请求字符串

    • “GET /?action=snapshot\n”
    • “GET /?action=stream\n”
    • “GET /?action=command\n”
    • … …
  • 2.再发送一次字符串,其中包含 用户名以及密码

    • "User-Agent: "
    • "Authorization: Basic "
      如果不需要密码功能,只需要发送任意长度 小于等于2字节的字符串,eg. “no”

如果 client 发送的请求为 “GET /?action=stream\n”

  • 3.接收一次字符串,为服务器发送的报文
  • 4.再接收一次报文,解析获取一帧图像的大小 以及 时间错
  • 5.接收 size 个字节的数据,转换为图像
  • 6.接收 报文尾消息
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页