目录组成:
文件夹名 | 功能 |
---|---|
cmake | makefile文件 |
plugins | 输入输出组件,用于采集及传输 |
scripts | 执行脚本 |
www | 用于浏览器功能代码 |
mjpg-streamer框架:
dlopen
打开 输入组件(动态链接库 .so),有以下几种:
dlopen
打开 输出组件(动态链接库 .so),有以下几种:
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);
参数:
-h
参数,则返回索引值 0。返回值:
-1
?
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"字符串
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 +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(); // 等待信号
输出组件同理
/* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
signal(SIGPIPE, SIG_IGN); // 用于忽略 SIGPIPE 信号
/* register signal handler for +C in order to clean up */
if(signal(SIGINT, signal_handler) == SIG_ERR) { // SIGINT信号代表由InterruptKey产生 ,当按下 ctrl+c时,调用 signal_handler,做清理工作
当按下 ctrl+c时,调用 signal_handler
函数用于做程序中断后的清理工作。
char *strchr(const char *str, int c)
该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。
示例测试:
#include
#include
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
char* strndup(const char* src, size_t len);
拷贝前 len 个字符,还自动为新的字符串添加一个’\0’表示结尾,返回 该新字符串的地址
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;
}
以 input_uvc.c
(plugins\input_uvc) 进行分析
int input_init(input_parameter *param, int id)
根据 main 函数传递的参数进行设置:
主要为 指定 USB 摄像头设备、分辨率、帧率、格式、质量、请求buf,队列buf以及一些其他的图像参数。
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;
}
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) {
... ...
}
... ...
}
}
}
以 input_uvc.c
(plugins\output_http) 进行分析
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地址、文件路径等
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
void *server_thread(void *arg)
{
... ...
// 当线程结束时,会调用 server_cleanup 进行相关的清理工作
pthread_cleanup_push(server_cleanup, pcontext);
... ...
/* 以下为 socket网络编程 创建并连接客户端 */
... ...
}
该线程主要为 创建并连接客户端,并创建 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
发送了什么请求,因此 客户端必须发送一个字符串,以换行符为结束。
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.先发送一次请求字符串
2.再发送一次字符串,其中包含 用户名以及密码
如果 client
发送的请求为 “GET /?action=stream\n”