1. 程式人生 > >mjpg-streamer簡單解析

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資料,用於在網頁上就可以看到響應的頁面。