MJPG-streamer是一個輕量級的視訊伺服器軟體,一個可以從單一輸入元件擷取圖像并傳輸到多個輸出元件的指令行應用程式,可應用在基于IP協定的網絡中,從網絡攝像頭中擷取并傳輸JPEG格式的圖像到浏覽器
MJPG-streamer的源碼基本上可以分為main函數、輸入插件和輸出插件三部分。在源碼包中,mjpg-streamer.c檔案主要是完成全局變量的定義,解析輸入參數,列印help資訊,信号處理,輸入輸出通道的初始化和啟動以及在擷取外部終止信号時對記憶體清理等工作。mjpg-streamer.h則定義了一些宏和全局變量結構體,包括終止信号位、互斥鎖、條件變量、全局緩沖區和輸入輸出插件。plugin檔案夾中包括了所需的所有插件的源碼,這些插件在運作時可以被主函數調用。整個mjpg-streamer的運作過程如下圖所示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN0LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90TUNNzYU1kenRVT4FEVkZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TM0kjMzEDN5EDNxgDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
主程式部分如下:
int main(int argc, char *argv[])
{
//input指針未能指派時使用預設值
char *input = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0";
// 指向輸出選項的參數字元串
char *output[MAX_OUTPUT_PLUGINS];
//daemon用于标記是否讓程式在背景運作
int daemon=0, i;
size_t tmp=0;
//output[0]初始值,設定預設端口号為8080
output[0] = "output_http.so --port 8080";
//此時輸出通道有幾種方式
global.outcnt = 0;
//運作mjpg_streamer指令時所帶入的參數,while循環用于解析這些參數(關鍵是看懂 getopt_long_only 函數)
while(1) {
int option_index = 0, c=0;
static struct option long_options[] = \
{
{"h", no_argument, 0, 0},
{"help", no_argument, 0, 0},
{"i", required_argument, 0, 0},
{"input", required_argument, 0, 0},
{"o", required_argument, 0, 0},
{"output", required_argument, 0, 0},
{"v", no_argument, 0, 0},
{"version", no_argument, 0, 0},
{"b", no_argument, 0, 0},
{"background", no_argument, 0, 0},
{0, 0, 0, 0}
};
//用于解析指令行選項
//根據傳入的參數,逐個在struct option 數組裡面進行比對,當比對到相同的參數,傳回在數組中的下标。
//如傳入-i選項 ,argv中自然有i選項,和struct option 數組項進行比對,找到時傳回下标,option_index索引值為2.
//參數解析:
//第二個參數:直接從main函數傳遞而來,表示我們傳入的參數,例如傳入:
// -i "input_uvc.so -f 10 -r 320*240" -o "output_http.so -w www"
//第三個參數:短選項字元串
//第四個參數:是 struct option 數組,用于存放長選項參數進行比對
//第五個參數:用于傳回長選項在longopts結構體數組中的索引值
//傳回值:
//解析完畢,傳回-1,出現未定義的長選項或者短選項,getopt_long傳回“?”,否則傳回對應索引值
//傳回值:
//解析完畢,傳回-1
//出現未定義的長選項或者短選項,getopt_long傳回?
c = getopt_long_only(argc, argv, "", long_options, &option_index);
//當為-1時表示參數解析完成,跳出循環
if (c == -1) break;
//如果傳入的參數不正确,則列印幫助資訊
if(c=='?'){ help(argv[0]); return 0; }
//根據傳回的結構體數組的下标,來執行相應的選項
switch (option_index) {
/* h, help */
case 0:
case 1:
help(argv[0]);
return 0;
break;
/* i, input */
//在getopt_long_only函數中,
//把傳入的-i選項對應的字元串"input_uvc.so -f 10 -r 320*240"賦給變量optarg,
//而strdup()函數用于将變量optarg的值賦予指針input
case 2:
case 3:
input = strdup(optarg);
break;
/* o, output */
// output[0] = "output_http.so -w www"
case 4:
case 5:
output[global.outcnt++] = strdup(optarg);
break;
/* v, version */
case 6:
case 7:
printf("MJPG Streamer Version: %s\n" \
"Compilation Date.....: %s\n" \
"Compilation Time.....: %s\n", SOURCE_VERSION, __DATE__, __TIME__);
return 0;
break;
/* b, background */
//參數傳入-b選項時,讓daemon=1;讓程式在背景運作
case 8:
case 9:
daemon=1;
break;
default:
help(argv[0]);
return 0;
}
}
//打開一個程式的系統記錄器的連結 */也就是打開一個調試用的記錄本相當于log.txt
openlog("MJPG-streamer ", LOG_PID|LOG_CONS, LOG_USER);
syslog(LOG_INFO, "starting application");
//如果daemon = 1,則讓程式在背景運作
if ( daemon ) {
LOG("enabling daemon mode");
daemon_mode();
}
/* 初始化全局變量,global結構在程式後面列出 */
global.stop = 0;
global.buf = NULL;
global.size = 0;
global.in.plugin = NULL;
//初始化 global.db 成員 ,是互斥鎖相關的,用于同步通路全局圖像緩沖區
if( pthread_mutex_init(&global.db, NULL) != 0 ) {
LOG("could not initialize mutex variable\n");
closelog();
exit(EXIT_FAILURE);
}
//初始化 global.db_update(條件變量) 成員,程序裡線程間進行通信的變量
if( pthread_cond_init(&global.db_update, NULL) != 0 ) {
LOG("could not initialize condition variable\n");
closelog();
exit(EXIT_FAILURE);
}
/* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
signal(SIGPIPE, SIG_IGN);
//signal()函數是信号綁定,當我們按下 <CTRL>+C 時,則調用signal_handler()函數,做一些清理工作
//signal_handler()把stop标志位置1,攝像頭采集線程退出,然後清理工作開始
if (signal(SIGINT, signal_handler) == SIG_ERR) {
LOG("could not register signal handler\n");
closelog();
exit(EXIT_FAILURE);
}
LOG("MJPG Streamer Version.: %s\n", SOURCE_VERSION);
// 如果輸出方式的個數為0,則讓他為1,保證至少有一個輸出通道
if ( global.outcnt == 0 ) {
/* no? Then use the default plugin instead */
global.outcnt = 1;
}
//打開加載輸入插件
//strchr函數原型:extern char *strchr(const char *s,char c),查找字元串s中首次出現字元c的位置,
//傳回值:成功則傳回要查找字元第一次出現的位置,失敗傳回NULL
//讓tmp 等于 "input_uvc.so"字元串的長度,如果tmp大于0,就取字元串input的前tmp位,也就是input_uvc.so
tmp = (size_t)(strchr(input, ' ')-input);
//經過指派後,global.in.plugin = "input_uvc.so"
global.in.plugin = (tmp > 0)?strndup(input, tmp):strdup(input);
//通過dlopen()函數打開 "input_uvc.so" 這個動态連結庫
global.in.handle = dlopen(global.in.plugin, RTLD_LAZY);
if ( !global.in.handle ) {
LOG("ERROR: could not find input plugin\n");
LOG(" Perhaps you want to adjust the search path with:\n");
LOG(" # export LD_LIBRARY_PATH=/path/to/plugin/folder\n");
LOG(" dlopen: %s\n", dlerror() );
closelog();
exit(EXIT_FAILURE);
}
//global.in.init等于剛打開的動态連結庫(input_uvc.c)的input_init函數,global.in.init = input_init()
global.in.init = dlsym(global.in.handle, "input_init");
if ( global.in.init == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//global.in.stop等于 剛打開的動态連結庫(input_uvc.c)的input_stop函數,global.in.stop = input_stop
global.in.stop = dlsym(global.in.handle, "input_stop");
if ( global.in.stop == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//global.in.run等于 剛打開的動态連結庫(input_uvc.c)的input_run函數,global.in.run = input_run
global.in.run = dlsym(global.in.handle, "input_run");
if ( global.in.run == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//global.in.cmd等于 剛打開的動态連結庫(input_uvc.c)的input_cmd函數,global.in.cmd = input_cmd
global.in.cmd = dlsym(global.in.handle, "input_cmd");
//對全局輸入插件的設定參數進行初始化,例如global.in.param.parameter_string = "-f 10 -r 320*240"
global.in.param.parameter_string = strchr(input, ' ');
global.in.param.global = &global;
//執行輸入插件部分的初始化,
//在這部分中,包括初始化互斥鎖、條件變量、圖像采集參數(dev,height,weight,fps,format)
//并在打開相機裝置後,對相機的輸入格式,幀率,緩沖區等進行初始化,并将相機采集緩沖區映射到記憶體,啟動相機等工作。
if ( global.in.init(&global.in.param) ) {
LOG("input_init() return value signals to exit");
closelog();
exit(0);
}
/* 打開加載輸出插件,這部分跟前面的輸入插件部分類似 */
for (i=0; i<global.outcnt; i++) {
//讓tmp 等于 "output_http.so"字元串的長度
tmp = (size_t)(strchr(output[i], ' ')-output[i]);
//讓 global.out[i].plugin = "output_http.so"
global.out[i].plugin = (tmp > 0)?strndup(output[i], tmp):strdup(output[i]);
//打開 "output_http.so" 動态連結庫
global.out[i].handle = dlopen(global.out[i].plugin, RTLD_LAZY);
if ( !global.out[i].handle ) {
LOG("ERROR: could not find output plugin %s\n", global.out[i].plugin);
LOG(" Perhaps you want to adjust the search path with:\n");
LOG(" # export LD_LIBRARY_PATH=/path/to/plugin/folder\n");
LOG(" dlopen: %s\n", dlerror() );
closelog();
exit(EXIT_FAILURE);
}
//global.out[i].init等于 剛打開的動态連結庫(Output_http.c)的output_init函數,global.out[i].init = output_init,
global.out[i].init = dlsym(global.out[i].handle, "output_init");
if ( global.out[i].init == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//global.out[i].stop等于 剛打開的動态連結庫(Output_http.c)的output_stop函數,global.out[i].stop = output_stop
global.out[i].stop = dlsym(global.out[i].handle, "output_stop");
if ( global.out[i].stop == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//global.out[i].run等于 剛打開的動态連結庫(Output_http.c)的output_run函數,global.out[i].run = output_run
global.out[i].run = dlsym(global.out[i].handle, "output_run");
if ( global.out[i].run == NULL ) {
LOG("%s\n", dlerror());
exit(EXIT_FAILURE);
}
//lobal.out[i].cmd等于 剛打開的動态連結庫(Output_http.c)的output_cmd函數,global.out[i].cmd = output_cmd
global.out[i].cmd = dlsym(global.out[i].handle, "output_cmd");
//讓 global.out[i].param.parameter_string = "-w www"
global.out[i].param.parameter_string = strchr(output[i], ' ');
global.out[i].param.global = &global;
global.out[i].param.id = i;
//調用 output_http.c中的output_init(&global.out[i].param)函數
if ( global.out[i].init(&global.out[i].param) ) {
LOG("output_init() return value signals to exit");
closelog();
exit(0);
}
}
/* start to read the input, push pictures into global buffer */
DBG("starting input plugin\n");
syslog(LOG_INFO, "starting input plugin");
///調用 input_uvc.c中的input_run函數,相機開始采集,并源源不斷的将圖像資料放在全局圖像緩沖區中
global.in.run();
//列印調試資訊
DBG("starting %d output plugin(s)\n", global.outcnt);
for(i=0; i<global.outcnt; i++) {
syslog(LOG_INFO, "starting output plugin: %s (ID: %02d)", global.out[i].plugin, global.out[i].param.id);
//調用 output_http.c 中的 output_run 函數,通過多線程的方式發送圖像資料,這部分比較複雜
global.out[i].run(global.out[i].param.id);
}
//程式等待信号的發生
pause();
return 0;
}
當按下ctrl+C時,程式調用signal_handle函數進行清理工作
void signal_handler(int sig)
{
int i;
/* 将全局終止信号位置1,在輸入和輸出的run函數中,讀取相機資料和發送過程的循環體都跟這個參數有關 */
/* 此處置1後,各線程跳出循環體,并執行各線程對應的清理工作,此處的清理工作是全局變量部分和關閉線程 */
LOG("setting signal to stop\n");
global.stop = 1;
usleep(1000*1000);
/* 執行stop函數,終止線程 */
LOG("force cancelation of threads and cleanup ressources\n");
global.in.stop();
for(i=0; i<global.outcnt; i++) {
global.out[i].stop(global.out[i].param.id);
}
usleep(1000*1000);
/* 關閉輸入插件的handle */
dlclose(&global.in.handle);
for(i=0; i<global.outcnt; i++)
dlclose(global.out[i].handle);
DBG("all plugin handles closed\n");
pthread_cond_destroy(&global.db_update);
pthread_mutex_destroy(&global.db);
LOG("done\n");
closelog();
exit(0);
return;
}
主函數的全局變量global定義為:
typedef struct _globals globals;
struct _globals {
int stop;
/* 互斥鎖和條件變量,用于通知重新整理資料資訊 */
pthread_mutex_t db;
pthread_cond_t db_update;
/* 全局圖像緩沖區 */
unsigned char *buf;
int size;
/* 輸入插件*/
input in;
/* 輸出插件 */
output out[MAX_OUTPUT_PLUGINS];
int outcnt;
};
總結:
主函數部分的工作,主要是解析輸入參數,加載輸入輸出插件,執行輸入輸出插件初始化并啟動,以及程式終止時的清理工作,即:依次調用以下函數
input_init();
output_init();【循環調用,多個輸出】
input_run();
output_run();【循環調用,多個輸出】
signal_handler();