mjpg-streamer簡單解析
前言
相信很多搞過ARM攝像頭的,都會想著怎麼把攝像頭資料繼續傳出去,做個遠端監控什麼的。記得當初學習的時候,不知道什麼壓縮,就按著自己的方法,把採集到的攝像頭資料YUV422轉為RGB565,然後再用Qt顯示,另外還用TCP把資料傳出去。結果可想而知,本來軟體轉碼速度就極慢(VGA,640x480大概需要500ms,2幀。。。),然後當時剛學習,也沒想著什麼多執行緒,直接傳送,然後接收端,大概5,6秒後才會有資料,就是說5~6秒的延遲,幀數還那麼低,然後我就棄了。。。
最近無聊了,又把它們撿起來,發現VGA的資料似乎太大了,軟體編碼500ms,真的不能忍,換了JPEG硬編碼發現也差不多,這就很尷尬了。。然後換成QVGA(320x240)用硬體MFC編碼成H264,大概20~30ms完成一幀資料,還是慢。。。不知道是我的Tiny6410太慢了,還是哪裡處理得不對。
正文
mjpg-streamer,就像它的名字一樣,jpg格式的流傳輸工具,用它可以實現對攝像頭資料採集到傳輸的一整套過程。
mjpg-streamer的實現也相對比較簡單:
主函式
在main函式裡面首先當然是實現命令輸入解析,對一些訊號的處理,主要是SIG_PIPE和SIG_INT:
/* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
signal(SIGPIPE, SIG_IGN);
/* register signal handler for <CTRL>+C in order to clean up */
if (signal(SIGINT, signal_handler) == SIG_ERR) {
LOG("could not register signal handler\n");
closelog();
exit (EXIT_FAILURE);
}
忽略掉SIG_PIPE訊號,做Linux通訊的一般都這麼做,否則當出現Pipe broken時,程式會自動退出。
然後就是為SIG_INT新增處理函式。
然後就是載入動態連結庫,因為mjpg-streamer的命令列輸出就需要輸入要使用的輸入輸出方式,也沒想通為啥要執行時載入,反正載入不是重點,也不管了,重點是載入的內容。
資料採集
通過載入動態連結庫後就可以獲取到要進行操作的方法,這些方法是init、stop、run和cmd這四種。因為是為了攝像頭資料傳輸,別的我也不管了。首先是輸入,檔案是input_uvc.c:
在初始化函式中init(),當然是對攝像頭引數設定和初始化啦:
/* open video device and prepare data structure */
if (init_videoIn(videoIn, dev, width, height, fps, format, 1) < 0) {
IPRINT("init_VideoIn failed\n");
closelog();
exit(EXIT_FAILURE);
}
設定的引數,有攝像頭裝置,採集的解析度,採集速度、格式以及壓縮的JPEG的質量:
/* display the parsed values */
IPRINT("Using V4L2 device.: %s\n", dev);
IPRINT("Desired Resolution: %i x %i\n", width, height);
IPRINT("Frames Per Second.: %i\n", fps);
IPRINT("Format............: %s\n", (format == V4L2_PIX_FMT_YUYV) ? "YUV" : "MJPEG");
if ( format == V4L2_PIX_FMT_YUYV )
IPRINT("JPEG Quality......: %d\n", gquality);
在攝像頭初始化init_videoIn()中,就是Linux V4L2的攝像頭設定:
if (init_v4l2 (vd) < 0) {
fprintf (stderr, " Init v4L2 failed !! exit fatal \n");
goto error;
}
在採集方法中,根據不同的資料格式,分別拷入到不同的buffer中:
switch (vd->formatIn) {
case V4L2_PIX_FMT_MJPEG:
if (vd->buf.bytesused <= HEADERFRAME1) { /* Prevent crash
* on empty image */
fprintf(stderr, "Ignoring empty buffer ...\n");
return 0;
}
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:
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;
}
根據採集的資料格式判斷是否需要壓縮為JPEG:
/*
* If capturing in YUV mode convert to JPEG now.
* This compression requires many CPU cycles, so try to avoid YUV format.
* Getting JPEGs straight from the webcam, is one of the major advantages of
* Linux-UVC compatible devices.
*/
if (videoIn->formatIn == V4L2_PIX_FMT_YUYV) {
DBG("compressing frame\n");
pglobal->size = compress_yuyv_to_jpeg(videoIn, pglobal->buf, videoIn->framesizeIn, gquality);
} else {
DBG("copying frame\n");
pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused);
}
其中pglobal->buf為輸出的buffer,也就是在後面需要傳輸的資料。
資料傳輸
既然是為了遠端視訊傳輸,當然這裡輸出應該選擇output_http.c,即網頁傳輸。輸出方法和輸入方法一樣,主要為init,stop,run和cmd四個函式組成,這裡就主要講輸出傳輸的過程,主要是run函式中,建立了伺服器thread:
/* create thread and pass context to thread function */
pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));
pthread_detach(servers[id].threadID);
其中server_thread()執行緒回撥函式在httpd.c檔案中,其中是一個TCP Server的初始化過程,並且設定的最多listen 10個client:
/* start listening on socket */
if ( listen(pcontext->sd, 10) != 0 ) {
fprintf(stderr, "listen failed\n");
exit(EXIT_FAILURE);
}
為accept的每一個client建立單獨的執行緒,用於處理請求和傳送資料:
/* 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");
pcfd->fd = accept(pcontext->sd, (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");
syslog(LOG_INFO, "serving client: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
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中,根據使用者發來的request來選擇後面的操作:
/* 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;
}
/* determine what to deliver */
if ( strstr(buffer, "GET /?action=snapshot") != NULL ) {
req.type = A_SNAPSHOT;
} else if ( strstr(buffer, "GET /?action=stream") != NULL ) {
req.type = A_STREAM;
}
·····
/* now it's time to answer */
switch ( req.type ) {
case A_SNAPSHOT:
DBG("Request for snapshot\n");
send_snapshot(lcfd.fd);
break;
case A_STREAM:
DBG("Request for stream\n");
send_stream(lcfd.fd);
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_FILE:
if ( lcfd.pc->conf.www_folder == NULL )
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");
}
在send_stream中,傳送我們已經處理好的JPEG資料流:
memcpy(frame, pglobal->buf, frame_size);
DBG("got frame (size: %d kB)\n", frame_size / 1024);
DBG("sending frame\n");
if( write(fd, frame, frame_size) < 0 ) break;
其他的資料傳送這裡就不詳細介紹了,搞過嵌入式網頁的都知道,就是直接傳送html資料,用於在網頁上就可以看到響應的頁面。