Linux的應用--Video Streaming探討 四

作者: 陳俊宏
www.jollen.org

本期的重點在介紹擷取 frame 的方法, 並且將重心由 video4linux 轉移到網絡方面。在網絡影像即時傳送方面, 我們採用的 RTP 也是各大廠商使用的標準, 在這一期裏, 我們將可以學習到利用 JRTPLIB 來加入網絡功能的方法。


video4linux 擷取 frame 的方法

在上一期的 xawtv 裏, 我們看到了 xawtv 的影像擷取功能, 其中對我們最重要的部份是利用 video4linux 做影像擷取的部份。只要可以寫出 video4linux 的軀動部份, 要做影像擷取其實是很容易的, 我們利用的是 mmap 的方式來擷取影像。

mmap 擷取方式

爲了說明如何以 mmap 方式來擷取影像, 我們不建議讀者直接去研究 xawtv 關於這部份的程序碼。研究過幾個有關支援 mmap 影像擷取的軟件原始碼後, 我們建議讀者去下載一支名爲 EffecTV 的程序, 其官方網頁爲:  

    http://effectv.sourceforge.net/index.html

EffecTV 是日本人設計的程序, 也是經由 video4linux 做影像擷取, 在 mmap 的程序碼方面, EffecTV 會比較容易懂, 同時也可以藉由 EffecTV 來學習一些影像處理的技巧。EffecTV 是一個可以支援特效功能的視訊軟件, 是頗有趣的程序。

主要函數介紹

EffecTV 裏與影像擷取 (frame grab) 有關的函數爲:  

    int video_grab_check(int palette);
    int video_set_grabformat();
    int video_grabstart();
    int video_grabstop();
    int video_syncframe();
    int video_grabframe();

這些函數定義在 video.h 裏。我們不再重覆介紹 video4linux 初始化的地方, 在 frame grab 方面, 呼叫 video_grabstart() 開始進行影像擷取的工作, 程序碼如下:  

    /* Start the continuous grabbing */
    int video_grabstart()
    {
       vd.frame = 0;
       if(v4lgrabstart(&vd, 0) < 0)
          return -1;
       if(v4lgrabstart(&vd, 1) < 0)
          return -1;
       return 0;
    }  

其中主角是 v4lgrabstart() 函數, 這個函數被實作在 v4lutils/v4lutils.c 裏, 程序碼如下:  

    /*
     * v4lgrabstart - activate mmap capturing
     *
     * vd: v4l device object
     * frame: frame number for storing captured image
     */
    int v4lgrabstart(v4ldevice *vd, int frame)
    {
       if(v4l_debug) fprintf(stderr, "v4lgrabstart: grab frame %d./n",frame);
       if(vd->framestat[frame]) {
          fprintf(stderr, "v4lgrabstart: frame %d is already used to grab./n", frame);
       }
      vd->mmap.frame = frame;
       if(ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {
          v4lperror("v4lgrabstart:VIDIOCMCAPTURE");
         return -1;
       }
      vd->framestat[frame] = 1;
       return 0;
    }  

v4lgrabstart() 是利用 mmap 的方式來取得影像。v4lgrabstart() 也是利用 ioctl() 來完成這個低階的動作, 與第本文第二篇實作 video4linux 時一樣, 寫法爲:  

    ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap));  

vd 裏的 framestat 欄位主要是紀錄目前的 frame 狀態:  

    vd->framestat[frame]  

這個欄位定義在 v4lutils.h 裏, 而利用 mmap 的方式我們需要兩個 frame 來存放影像資料, 所以 framestat 宣告成二個元素的數組, 我們將 EffecTV 的 v4l 結構定義完整列出如下:  

    struct _v4ldevice
    {
       int fd;
       struct video_capability capability;
       struct video_channel channel[10];
       struct video_picture picture;
       struct video_clip clip;
       struct video_window window;
       struct video_capture capture;
       struct video_buffer buffer;
       struct video_mmap mmap;
       struct video_mbuf mbuf;
       struct video_unit unit;
       unsigned char *map;
       pthread_mutex_t mutex;
       int frame;
       int framestat[2];
       int overlay;
    };  

請讀者回頭對應一下本文第二篇文章所實作的內容, EffecTV 的實作更爲完整。當我們開始 grab 影像到其中一個 frame 時, 我們就把 frame 的狀態設成 1:  

    vd->framestat[frame] = 1;  

然後利用 v4lsync() 等待 frame 擷取完成, 利用 ioctl() 傳入 VIDIOCSYNC 可以檢查 frame 是否已經擷取完成:  

    if(ioctl(vd->fd, VIDIOCSYNC, &frame) < 0) {
       v4lperror("v4lsync:VIDIOCSYNC");
      return -1;
          }
          vd->framestat[frame] = 0;
          return 0;  

如果 frame 已擷取完成, 那麼我們就將 frame 的狀態設成 0, 表示目前這個 frame 並沒有在做擷取的動作, 也因此在 v4lsync() 一開始的地方我們會先做這部份的檢查:  

    if(vd->framestat[frame] == 0) {
       fprintf(stderr, "v4lsync: grabbing to frame %d is not started./n", frame);
          }  

v4lsync() 函數也是一個重要的函數, 程序碼如下:  

    /*
     * v4lsync - wait until mmap capturing of the frame is finished
     *
     * vd: v4l device object
     * frame: frame number
     */
    int v4lsync(v4ldevice *vd, int frame)
    {
       if(v4l_debug) fprintf(stderr, "v4lsync: sync frame %d./n",frame);
       if(vd->framestat[frame] == 0) {
          fprintf(stderr, "v4lsync: grabbing to frame %d is not started./n", frame);
       }
      if(ioctl(vd->fd, VIDIOCSYNC, &frame) < 0) {
          v4lperror("v4lsync:VIDIOCSYNC");
         return -1;
       }
      vd->framestat[frame] = 0;
       return 0;
    }  

在 EffecTV 裏則是要呼叫 video_syncframe() 函數來做 frame 等待的動作, 而 video_syncframe() 則會去呼叫 v4lsync() 函數。video_syncframe() 函數的原始碼如下:  

    int video_syncframe()
    {
       return v4lsyncf(&vd);
    }  

frame 擷取實作

看過這幾個核心函數後, 那麼在 EffecTV 裏會在那裏用到這些函數呢? EffecTV 是一個輸出特效畫面的視訊軟件, 在 effects/ 目錄下每個檔案都是獨立支援一種特效的, 因此我們介紹的這幾個函數都是由每個特效獨立來呼叫使用。讓我們來看 Life 這個特效的主程序 ━ life.c, 首先應該先由 lifeStart() 函數看起, 其程序碼如下:  

    int lifeStart()
    {
       screen_clear(0);
      image_stretching_buffer_clear(0);
      image_set_threshold_y(40);
      field1 = field;
       field2 = field + video_area;
       clear_field();
      if(video_grabstart())
         return -1;
    
       stat = 1;
       return 0;
    }  

lifeStart() 在完成一些初始化的設定工作後, 會呼叫 video_grabstart() 函數開始進行影像擷取。在 lifeDraw() 函數裏, 則是呼叫 video_syncframe() 等待 frame 擷取完成後再做輸出的動作。

YUV 與 YIQ

在 PAL 視訊標準方面, 主要的模式爲 YUV, 這與我們在計算機上常用的 RGB 不同。相對的, 在 NTSC 視訊標準, 則是使用 YIQ 模式。針對這二種視訊影像模式, 我們還必須設計 YUV 與 RGB、YIQ 與 RGB 的轉換程序。在 EffecTV 裏也有 yuv.c 的程序碼負責做轉換的工作。

RGB 介紹

RGB 以三原色紅、綠、藍 (Red-Green-Blue) 來表現影像, 將紅色與藍色重疊後會成爲品紅色 (magenta)、紅色與綠色重成爲黃色 (yellow), 三色重疊則是白色 (white)。RGB 的三原色指的是光線的顏色, 並非顏料的顏色, RGB 模式常使用於監視器上, 與 PAL 或 NTSC 視訊的標準不同。

YUV 與 YIQ 的轉換

YUV 、 YIQ 與 RGB 之間的換系與轉換公式如下:  

    Y = 0.299R + 0.587G + 0.114B
    U = B ━ Y
    V = R ━ Y
    I = 0.877(R-Y)cos33 ━ 0.492(B-Y)sin33
    Q = 0.877(R-Y)sin33 + 0.492(B-Y)cos33  

RGB 是由 R, G, B 三原色組成, 同理 YUV 是由 Y, U, V 三個元素組成。在 PAL 實作 U, V 我們使用的轉換公式爲:  

    U = 0.492(B-Y)
    V = 0.877(R-Y)  

YIQ 則可以簡化成轉換矩陣:  

    (手稿) 

JRTPLIB 的使用方法

在 Video Streaming 方面, 有了影像擷取的程序實作能力後, 要完成完整的影像串流軟件, 例如視訊會議軟件, 當然就必須要加入網絡傳送的功能。

爲了能利用網絡傳送影像, 並且做到 real-time (即時) 的功能, 我們必須使用RTP通訊協定來完成。在這裏我們已經完成第一大部份的工作了, 接下來就是加入網絡即時傳送撥放的功能, 到這裏 video4linux已告一段落,我們將 Video Streaming 的主角換到 RTP 繼續討論。

加入 RTP Protocol 

利用 Video Streaming 來設計視訊會議軟件, 其中在技術層面不可或缺的一部份就是 RTP Protocol 的部份。RTP 也是 VoIP (Voice over IP) 相關技術所使用的通訊協定。

爲了配合 Video Streaming 來設計完整的視訊會議軟件, 我們勢必要加入 RTP 的技術。在這方面, 我們選擇使用現成的 RTP 程序庫 ━ JRTPLIB。

與 video4linux 程序庫不同的是, video4linux 在決策上我們選擇自行發展, 但JRTPLIB則是一個很成熟的專案了, 而且仍在持續維護, 未來 JRTPLIB 還會加入 IPV6 與 multicasting方面的完整支援,因此使用 JRTPLIB 來發展我們的 Video Streaming 軟件纔是解決之道。

JRTPLIB 簡介

RTP 全名爲 Real-time Transport Protocol, 定義於 RFC 1889 與 RFC 1890, 我們在第一篇文章裏已經簡單介紹過 RTP。在 RFC 1889 裏, 對 RTP 的定義爲:  

    RTP: A Transport Protocol for Real-Time Applications  

而在 RFC 1890 裏, 對 RTP 的描述則是:  

    RTP Profile for Audio and Video Conferences with Minimal Control  

即然我們要利用 Video Streaming 來設計視訊會議方面的軟件, 對於 RTP的討論與研究則是必修功課之一。對視訊會議軟件而言,RTP 也提供 Audio 部份的支援, 事實上, 任何與 real-time (即時)相關的話題都與 RTP 脫不了關係。

與 JRTPLIB 相關的計畫包括 JVOIPLIB 與 JTHREAD, 這兩個程序庫對我們的工作是相關有幫助的, 本文則先將重心放在 JRTPLIB上面。JRTPLIB 實作了 RTP 協定, 並且提供了簡單易用的 API 供軟件開發使用。JRTPLIB 也支援了 session, 並且可在底下平臺執行:
  

  • MS-Windows 95,98 and NT
  • Linux
  • FreeBSD
  • HP-UX
  • Solaris
  • VxWorks


JRTPLIB 的官方首頁爲:  

    http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html

使用前請務必先閱讀一下 JRTPLIB 的版權宣告。

JRTPLIB 的第一個程序

底下我們介紹過 JRTPLIB 的設計方法後, 大家就會發覺到 JRTPLIB 實在很好上手。要利用 RTP 通訊協定傳送資料, 第一步要先建立一個 session, 方法如下:  

    #include "rtpsession.h"    
                  int main(void)
                  {
       RTPSession sess;
    
       sess.Create(5000);
      return 0;
                  }  

第一步我們要先把 rtpsession.h 給 include 進來:  

    #include "rtpsession.h"  

接下來再產生 RTPSession 類別的物件:  

    RTPSession sess;  

最後再建立 session 就完成最簡單的初始動作了:  

    sess.Create(5000);  

Create() 成員函數接收一個 portbase 的參數, 指定 session 的 port, 接着開始初始化 timestamp 與 packet sequence number。RTPSession::Create() 程序碼如下:  

    int RTPSession::Create(int localportbase,unsigned long localip)
    { 
      int status;
    
       if (initialized)
          return ERR_RTP_SESSIONALREADYINITIALIZED;
       if ((status = rtpconn.Create(localportbase,localip)) < 0)
          return status;
       if ((status = contribsources.CreateLocalCNAME()) < 0)
       {
         rtpconn.Destroy();
         return status;
       }
      CreateNewSSRC();
      localinf.CreateSessionParameters();
      rtcpmodule.Initialize();
      initialized = true;
       return 0;
    }  

Create() 接着會再建立一個 SSRC:  

    CreateNewSSRC();  

SSRC 爲 local synchronization source identifier。

指定目的端

接下來再指定目的端的 IP 位址:  

    unsigned long addr = ntohl(inet_addr("127.0.0.1"));
                          sess.AddDestination(addr,5000);  

這裏表示我們要將封包傳送到 127.0.0.1 (本地端) 的 port 5000, 只要照着套用即可。

傳送 RTP 封包

          sess.SendPacket("1234567890",10,0,false,10);

1234567890 是要傳送的字串, 第二個參數 (10) 表示傳送字串的長度, 第三個參數爲 payload type, 第四個參數爲 mark flag, 最後第五個參數則是 timestamp 的遞增單位。在下一期裏, 我們將會配合 SDL 來做影像的輸出, 因此我們會在下一期再介紹如何接收 RTP 封包。我們使用 SDL 主要目的是爲了將影像輸出到屏幕上。

RTP 的封包格式

RTP 的標準受許多大廠採用, 例如: Microsoft、Intel, 也因此我們需要了解一下 RTP 的低層技術部份。RTP 與其它 Internet 通訊協定一樣, 在封包裏也會有封包檔頭, 接着纔是封包的資料。  


圖 1 是 RTP 的封包檔頭格式, 整個檔頭分爲 10 個欄位 (field)。

在 RTPsession::SendPacket() 的第三個參數與第四個參數分別是 payload type 與 mark flag, 在 RTP 封包檔頭裏, 分別是 Payload 欄位與 M 欄位。Payload 欄位的長度爲 7 bits, M 欄位的長度爲 1 bits。

RTP 的 Payload type

RTP 檔頭的 Payload type 指定封包資料的編碼方式, 我們列出五個常用的 Audio 標準, 與三個常用的 Video標準,其中 JPEG/H.261/H.263 我們本文第一篇裏都有做過簡單的介紹。常用的 Payload type 如下表:  

                                             

Payload type 編碼標準 支援Audio或Video Clock Rate (Hz)
2 G.721 A 8000
4 G.723 A 8000
7 LPC A 8000
9 G.722 A 8000
15 G.728 A 8000
26 JPEG V 90000
31 H.261 V 90000
34 H.263 V 90000
   

Linux 的 IP Stacks

提要網絡的應用, 當然也要對 Linux IP Stacks 有簡單的認識, 我們建議大家直接去研究 Linux kernel 的程序碼, 當然現在已經有專門的書在做討論:  

    Linux IP Stacks Commentary, Stephen T. Satchell & H.B.J. Clifford, CoriolisOpen Press, ISBN 1-57610-470-2  

Linux 是網絡操作系統, 而且 Linux 對於網絡的支援也相當的完整, 包括2.4 系列 kernel 已經加入對 IPv6。 Linux kernel 與 module 提供的通訊層功能包括:
  

  • 各種 Ethernet、token ring 與 FDDI (Fiber Distributed Data Interface) 界面卡的軀動程序
  • PPP、SLIP 與 SLIP 通訊協定的軀動程序
  • 提供 IPX (Internet Package Exchange) 通訊協定
  • 提供工餘無線電用的軀動程序 (AX25)
  • 提供 AppleTalk 軀動程序
  • 其它鏈接層使用的軀動程序
  • 支援 router 的功能,包括 RIP (Router Information Protocol) 通訊協定
  • 支援 ICMP (Internet Control Message Protocol) 通訊協定
  • 支援 IGMP (Internet Group Message Protocol) 通訊協定
  • 支援 IP (Internet Protocol) 通訊協定
  • 支援 TCP (Transmission Control Protocol) 通訊協定
  • 支援 UDP (User Datagram Protocol) 通訊協定


Linux IP Stacks 一書的書點放在基本與重要的 TCP/IP 服務上, 包含:繞送、封包管理、datagram 與 datastream。

Linux IP Stacks 導讀

裏的導讀參考自 Linux IP Stacks 一書的第一章, 在研究 Linux IP Stacks 這本書前, 請讀者先好好研究一下這本書的結構, 到時才比較容易上手。

第二章的部份介紹 TCP/IP 的背景知識與歷史,包括 TCP/IP 的發展過程, 這一章原則上只要瞭解一下即可。

第三章則以學術的觀點來比較 TCP/IP 與 ISO 模型。這一章比較偏向 ISO 的理論,而書上解釋到, 要學習 ISO 模型理論的目的, 是爲了能夠了解爲什麼通訊層要分割成這幾層。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章