Linux的應用--Video Streaming探討 二

作者: 陳俊宏
www.jollen.org
    
繼上一篇介紹過 Video Streaming的影像標準與網絡通訊協定後,本期將要實際介紹目前常見的 Video Streaming 產品,並且由基本構成開始講解。本期首先介紹 video4linux的設計方式。

Video Streaming 產品介紹

目前在網絡上流行的 Video Streaming 產品相當多,這些利用 Video Streaming技術設計的軟件在網絡多媒體的應用已經有相當長的一段時間了。底下先來介紹幾套常用的 Video Streaming 軟件。

Read Video

Real Video 是 Real Networks 公司的產品,Real Video 主要支援了video-on-demand*1 的功能。Real Video 可以讓我們經由網站來播放串流影像 (streamingvideo)。

由於我們的最終目的是實作出一個可以做 video streaming的軟件,所以在這裏我們將以 Real Video 做爲標竿,並以 Linux爲基礎來設計 video streaming 的軟件。

mod_mp3

mod_mp3 是 Open Source 的 streaming 軟件。mod_mp3 並不是 Video Streaming 的軟件,但同樣是利用 streaming 的技術所設計的 apache module。

mod_mp3 可以利用 apache 來架設 streaming server,主要的功能是將 MP3放進 cache 裏,再利用撥放程序就可以經由網絡享受 MP3 streaming的服務。

mod_mp3 的架設相當簡單,將 mod_mp3 以 DSO 方式安裝後,只要在httpd.conf 里加上 VirtualHost 的設定即可:
   

      Listen 7000
      <VirtualHost www.jollen.org:7000>;
      ServerName www.jollen.org
      MP3Engine On
      MP3CastName "jollen box"
      MP3Genre "Much, nutty"
      MP3 /home/nfs/private/mp3
      MP3Random On
      Timeout 600
      ErrorLog /var/log/mp3_stream.log
      </VirtualHost>
   

其中的設定項目說明如下:

  • MP3 - MP3 路徑或檔名
  • MP3Engine - 啓動或關閉 MP3 streaming server
  • MP3CastName - server name
  • MP3Genre - Genre that will be sent to the client
  • MP3Playlist - 如果 MP3 Player 支援 Playlist,可以設定這個項目
  • MP3Cache - cache 目錄


VIC

VIC 也是屬於 Open Source 的軟件。VIC 全名爲video conferencing,故名其義,VIC是一種視訊會議的軟件。VIC 是由加州柏克來大學的 Network Research Group所發展。

VIC 是相當棒非常適合用來研究 Video Streaming 的 Open Source軟件,主要是因爲 VIC 幾乎包含了 Video Streaming 相關的技術。VIC 值得我們研究的原因是因爲 VIC 支援了底下所列的功能:

  • IPv6
  • 使用 video4linux 的捕像捕捉功能
  • H261、H263 與 H263+ codec
  • Software JPEG 與 BVC 編碼
  • Raw YUV packetiser/codec
  • RTIP/RTP 通訊協定
  • the IP Multicast Backbone (MBone)
  • 支援 video4linux 的 mmap


這些特色幾乎已經包括 Video Streaming所應具備的技術了,基於這些特點,VIC的原始程序碼相當吸引人,因此有意研究 Video Streaming 的 programmer應該好好閱讀一下 VIC的原始程序碼。

VideoLAN

VideoLAN 是一個可以做 MPEG 與 DVD 擴播 (broadcast) 播放的軟件,VideoLAN分成二個部份,一個是 VLAN server,另一個則是 vlc 用戶端播放程序。

VLAN server 將 DVD 與 MPEG 影像利用 broadcast     方式擴播到區域網絡上,使用者端再利用 vlc    接收封包並播放。這樣做的好處是可以減少重覆的 I/O 動作,VLAN     server 將影像擴播出去後,區域網絡上的用戶端再利用vlc     接收封包並播放。

VideoLAN 支援 X11、SDL、Linux framebuffer、GGI、BeOS API、MacOS X API     播放方式,並且支援 DVD 與 AC3 (杜比音效)。

video4linux 實作

看過幾套現成的 Video Streaming 後,還是要回到本文的主題 -- Linux 如何設計 VideoStreaming 的應用程式。上一期所介紹的 Video Streaming基本觀念是進入 Video Streaming領域相當重要而且基本的知識,像是PASL/NTSC、RTP...等等。

RealNetworks 公司的產品裏,要建置網站的即時 (live) 影像是相當容易的。只要利用 RealNetworks 公司的產品配合影像捕捉卡(Video Capture Card) 與 CCD 就可以達到。

從這裏可以看出,如果我們想要實作一套這樣的小系統,第一個所要面臨的問題就是如何在Linux 下軀動影像捕捉卡,再來就是如何設計影像捕捉的程序。

在影像捕捉卡方面,Linux kernel 2.2     版本的支援已經相當完備了,很多影像捕捉卡在 Linux kernel 2.2     上都可以順利軀動並且正常工作。

而在程序設計方面,我們則是先利用 Linux kernel 所提供的 video4linuxAPIs 來設計程序。這一期的目的在於利用 video4linux來實作一個供應用程式使用的程序庫 (library)。

影像捕捉卡

先來檢視一下 Osprey 100 這張影像捕捉卡。Osprey 100 是 Real Networks公司所推薦配合他們產品的一張影像捕捉卡,配合 Osprey 100 與RealNetworks 的產品我們可以利用broadcast 或 on-demand 做到實況轉播 (live)的功能。

Osprey 100 在硬件功能上可以支援到每秒 30 個畫面 (fps -- frame per second),並且支援 NTSC 與 PAL 輸入。

不過在實作上,筆者並不使用 Osprey 100。筆者使用的影像捕捉卡是,這張卡算是比較「俗」一點的卡,但是也有好處,因爲在 Linux 上很容易安裝。

在繼續往下發展我們的系統前,必須先安裝好影像捕捉卡與軀動程序,這部份不在這篇文章的範圍,所以請您參考相關的文章來安裝軀動程序。

以筆者這張卡爲例,使用的是 Brooktree Corporation     的卡,所以只要安裝 bttv 模組即可,同時,bttv模組在 Linux kernel 2.2.17 下也會用到 i2c-old 與 videodev    兩個模組,所以也要一併安裝。在命令列下,安裝這三個模組的命令爲:   

      linux# insmod i2c-old
      linux# insmod videodev
      linux# insmod bttv   

當然要確定 Linux kernel有編譯這三個模組的支援,然後再把這三個模組加到 /etc/modules.conf(Red Hat 7.0) 裏。

不同版本的 kernel 所要安裝的模組不一定相同!還請注意,例如 i2c相關模組就是如此。

video4linux 使用的設備檔
    
Linux 下與 video4linux 相關的設備檔與其用途:
                     

/dev/video    Video Capture Interface
/dev/radio    AM/FM Radio Devices
/dev/vtx    Teletext Interface Chips
/dev/vbi    Raw  VBI Data (Intercast/teletext)

    
video4linux 除了提供 programmer 與影像捕捉有關的 API 外,也支援其它像是收音機裝置。

接下來介紹 video4linux 設計方式,所使用的 Linux kernel 版本爲 2.2.16。這篇文章將簡單介紹實作video4linux 的方法,所以請準備好 Linux kernel 原始碼下的Documentation/v4l/API.html 文件並瞭解 What's video4linux。

_v4l_struct -- 定義資料結構

首先,先定義會用到的資料結構如下:   

      #ifndef _V4L_H_
      #define _V4L_H_   

實作 video4linux 時,必須 include 底下二個檔案:   

      #include <sys/types.h>
      #include <linux/videodev.h>   

接下來是 PAL、CIF、NTSC 規格的畫面大小定義:   

      #define PAL_WIDTH    768
      #define PAL_HEIGHT    576
      #define CIF_WIDTH    352
      #define CIF_HEIGHT    288 
      #define NTSC_WIDTH    640
      #define NTSC_HEIGHT    480    

接下來我們的重點是 _v4l_struct structure,這個 structure 包含了在API.html 提到,將會使用到的 data structure,底下將完整地定義 _v4l_struct,但在實作時並不會全部用到。 _v4l_struct 定義如下:   

      struct _v4l_struct 
      {
         int fd;
         struct video_capability capability;
         struct video_buffer buffer;
         struct video_window window;
         struct video_channel channel[8];
         struct video_picture picture;
         struct video_tuner tuner;
         struct video_audio audio[8];
         struct video_mmap mmap;
         struct video_mbuf mbuf;
         unsigned char *map;
      };   

爲了設計方便,我們再做底下的定義:   

      typedef struct _v4l_struct v4l_device;   

以後宣告 struct _v4l_struct 時,將一律使用 v4l_device。

實作函數宣告

底下宣告將要實作的 functions,我們採取 top-down的實作方式,也就是先將所有會用到的函數事先規劃,並宣告在原始碼裏。當然,本文並不會介紹底下所有的函數,但重要的函數則會做說明。

實際做設計時,有些函數可能會在後期纔會被設計出來。我們所要實作的函數與函數宣告如下:   

      extern int v4l_open(char *, v4l_device *);
      extern int v4l_close(v4l_device *);
      extern int v4l_get_capability(v4l_device *);
      extern int v4l_set_norm(v4l_device *, int);
      extern int v4l_get_channels(v4l_device *);
      extern int v4l_get_audios(v4l_device *);
      extern int v4l_get_picture(v4l_device *);
      extern int v4l_grab_init(v4l_device *, int, int);
      extern int v4l_grab_frame(v4l_device *, int);
      extern int v4l_grab_sync(v4l_device *);
      extern int v4l_mmap_init(v4l_device *);
      extern int v4l_get_mbuf(v4l_device *);
      extern int v4l_get_picture(v4l_device *);
      extern int v4l_grab_picture(v4l_device *, unsigned int);
      extern int v4l_set_buffer(v4l_device *);
      extern int v4l_get_buffer(v4l_device *);
      extern int v4l_switch_channel(v4l_device *, int);   

v4l_open() -- 開啓 device file

首先,v4l_open() 是我們第一個應該要撰寫的函數。v4l_open()用來開啓影像來源的設備檔。依據 v4l_open() 的宣告,在應用程式裏,我們會這樣呼叫 v4l_open():    

      v4l_device vd;
      
      if (v4l_open("/dev/video0", &vd)) {
         return -1;
      }   

在應用程式裏,我們宣告了一個 vd 變數 (v4l_device 型態),再呼叫v4l_open() 將設備檔開啓。如果可以開啓 "/dev/video0" 則將取回的信息放到 vd 裏,vd 是v4l_device 也就是之前宣告的 _v4l_struct。接下來,讓我們來看看 v4l_open() 要如何實作:

      #define DEFAULT_DEVICE "/dev/video0"
      
      int v4l_open(char *dev, v4l_device *vd)
      {
         if (!dev)
            dev = DEFAULT_DEVICE;
      
         if ((vd->fd = open(dev, O_RDWR)) < 0) {
            perror("v4l_open:");
            return -1;
         }
      
         if (v4l_get_capability(vd))
            return -1;
      
         if (v4l_get_picture(vd))
            return -1;
      
         return 0;
      }   

爲了設計出完整的 video4linux 程序庫,一開始我們就定義了DEFAULT_DEVICE,當應用程式輸入的 dev設備檔參數不存在時,就使用預設的設備檔名稱。程序片段如下:   

      if (!dev)
         dev = DEFAULT_DEVICE;   

與一般 Linux Programming 一樣,我們使用 open() 將 device file 打開:   

      if ((vd->fd = open(dev, O_RDWR)) < 0) {
         perror("v4l_open:");
         return -1;
      }   

如果您不熟悉 Linux 下 open() 的使用方法,請參考 Linux programming相關資料。熟悉 UNIXprogramming 的讀者一定知道,open() 也與 STREAMS     的觀念相關,這部份在後面會再另外做介紹。

將設備檔開啓後,把傳回來的 file description 放到 vd->fd 裏。

成功開啓設備檔後,根據 API.html的說法,我們要先取得設備的信息與影像視窗的信息,所以這裏再實作 v4l_get_capability() 與 v4l_get_picture() 來完成這二件工作。

v4l_get_capability() 會利用 ioctl()取得設備檔的相關信息,並且將取得的信息放到 structvideo_capability結構裏。同理,v4l_get_picture() 也會呼叫 ioctl(),並將影像視窗信息放到 struct video_picture 結構。

v4l_get_capability() 函數程序碼如下:   

      int v4l_get_capability(v4l_device *vd)
      {
         if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {
            perror("v4l_get_capability:");
            return -1;
         }
         return 0;
      }   

在這裏,其實只有底下這一行纔是 v4l_get_capability 的主力:   

      ioctl(vd->fd, VIDIOCGCAP, &(vd->capability));     

其它部份都是屬於錯誤處理的程序碼,在本文,筆者都將函數寫的完整一點,即包含了錯誤檢查,因爲我們想要實作一個v4l 的 library。

vd->fd 是由 v4l_open 傳回來的 file descriptor,而傳遞 VIDIOCGCAP 給ioctl() 則會傳回設備相關信息,在這裏則是存放於 vd->capability。

v4l_get_ picture() -- picture 的初始化
    
取得設備信息後,我們還要再取得影像信息,所謂的影像信息指的是輸入到影像捕捉卡的影像格式。在 _v4l_struct 結構裏,我們宣告 channel 如下:   

      struct video_picture picture;   

初始化 picture的意思就是要取得輸入到影像捕捉卡的影像信息,我們設計 v4l_get_picture() 函數來完成這件工作。v4l_get_ picture () 完整程序碼如下:   

      int v4l_get_picture(v4l_device *vd)
      {
         if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {
            perror("v4l_get_picture:");
            return -1;
         }
         return 0;
      }   

傳遞VIDIOCGPICT 給 ioctl() 則會傳回影像的屬性 (image properties),這裏則是將影像屬性存放於vd-> picture。

v4l_get_channels() -- channel 的初始化
    
接下來,我們還要再做 channel 的初始化工作。還記得在 _v4l_struct結構裏,我們宣告 channel 如下:   

      struct video_channel channel[8];   

channel 是一個 8 個元素的數組,一般絕大部份都會宣告 4 個元素,因爲大部份的影像捕捉卡都只有 4 個 channel。幾乎沒有影像捕捉卡有8 個 channel的。

初始化 channel 的意思就是要取得「每個」 channel 的信息,我們設計v4l_get_channels() 函數來完成這件工作。v4l_get_channels() 完整程序碼如下:   

      int v4l_get_channels(v4l_device *vd)
      {
         int i;
      
         for (i = 0; i < vd->capability.channels; i++) {
            vd->channel.channel = i;
      
            if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel))       < 0) {
               perror("v4l_get_channel:");
               return -1;
            }
         }
         return 0;
      }   

要記得,我們是對每個 channel做初始化,所以必須用一個迴圈來處理每個 channel。那我們怎麼知道影像捕捉卡上有幾個channel 呢?記得我們設計 v4l_open() 時也「順路」呼叫了v4l_get_capability()嗎!v4l_get_capability()所取得的設備信息,就包含了影像捕捉卡的 channel     數。這個信息儲存於vd->capability.channels 裏。

由於 v4l_get_capability() 是必備的程序,所以我們就順便寫在 v4l_open()裏。當然,如果您沒有在v4l_open() 裏呼叫 v4l_get_capability(),這樣的設計方式當然沒有錯,只是在設計應用程式時,要記得在v4l_open() 後還要再呼叫v4l_capability() 才行。

在迴圈裏,首先先替每個 channel 做編號:   

      vd->channel.channel = i   

然後再取得 channel 的信息:   

      ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel);   

傳遞 VIDIOCGCHAN 給 ioctl() 則會傳回 channel 的資 訊,這裏則是將channel 的信息存放於 vd-> channel。 要注意一下,在 kernel 2.4 的 API.html 文件裏,粗心的 programmer 將VIDIOCGCHAN 打成 VDIOCGCHAN,少了一個 "I"。

v4l_get_audios() -- audio 的初始化
    
接下來,我們再做 audio 的初始化工作,audio 的初始化方式與初始化channel 的方法很像。在 _v4l_struct 結構裏,我們宣告 auduio的結構如下:   

      struct video_audio audio[8];   

audio 是一個 8 個元素的數組,與 channel 一樣。一般絕大部份都會宣告 4個元素,因爲大部份的影像捕捉卡都只有 4 個audio。幾乎沒有影像捕捉卡有8 個audio的。

初始化 audio 的意思就是要取得「每個」 audio 的信息,我們設計v4l_get_audios() 函數來完成這件工作。v4l_get_audios() 完整程序碼如下:   

      int v4l_get_audios(v4l_device *vd)
      {
         int i;
      
         for (i = 0; i < vd->capability.audios; i++) {
            vd->audio.audio = i;
      
            if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio))       < 0) {
               perror("v4l_get_audio:");
               return -1;
            }
         }
         return 0;
      }   

別忘了,我們仍然要對每個 audio 做初始化,所以必須用一個迴圈來處理每個 audio。那我們怎麼知道影像捕捉卡上有幾個audio 呢?與取得 channel 的方式一樣,audio 數量的信息儲存於vd->capability.audios 裏。

在v4l_get_audios() 的迴圈裏,首先先替每個 audio 做編號:   

      vd->audio.audio = i;   

然後再取得 audio 的信息:   

      ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio);   

傳遞 VIDIOCGAUDIO 給 ioctl() 則會傳回 audio 的信息,這裏則是將 audio的信息存放於 vd-> audio
    
v4l_close() -- 關閉裝置檔

v4l_close()程序相當簡單,所以不用再多做介紹啦!直接列出程序碼如下:   

      int v4l_close(v4l_device *vd)
      {
         close(vd->fd);
         return 0;
      }   

配合應用程式來設計

設計了幾個函式後,接下來我們要實地設計一個應用程式來說明如何使用v4l_xxx() 系列的函式。底下是一個在應用程式裏初始化影像捕捉卡,並且列出取得的信息的程序範例 (完整程序碼):   

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include "v4l/v4l.h"
      
      v4l_device vd;
      
      int device_init(char *dev)
      {
         if (dev == NULL) {
            dev = "/dev/video0"; //set to default device
         }
      
         if (v4l_open(dev, &vd)) return -1;
         if (v4l_get_channels(&vd)) return -1;
      
         printf("%s: initialization OK... %s/n"
                  "%d       channels/n"
                  "%d       audios/n/n",
                  dev,       vd.capability.name, vd.capability.channels,
                  vd.capability.audios);
      
         v4l_close(&vd);
         return 0;
      }
      
      int main()
      {
         if (device_init("/dev/video0") == -1) {
            perror("device_init: failed...");
            exit(1);
         } else {
            printf("OK!/n");
         }
         exit(0);
      }   

我們將這個程序存成 main.c,整個程序不用再多做介紹了吧!程序裏用到的地方都有介紹過,其中vd.capability.name 代表界面的 canonical name。

device_init() 最後呼叫 v4l_close()將裝置檔關閉,別忘了這個重要的工作!v4l/v4l.h 的內容如下 (完整程序碼):   

      #ifndef _V4L_H_
      #define _V4L_H_
      
      #include <sys/types.h>
      #include <linux/videodev.h>
      
      #define PAL_WIDTH 768
      #define PAL_HEIGHT 576
      #define CIF_WIDTH 352
      #define CIF_HEIGHT 288
      #define NTSC_WIDTH 640
      #define NTSC_HEIGHT 480
      
      struct _v4l_struct
      {
         int fd;
         struct video_capability capability;
         struct video_buffer buffer;
         struct video_window window;
         struct video_channel channel[8];
         struct video_picture picture;
         struct video_tuner tuner;
         struct video_audio audio[8];
         struct video_mmap mmap;
         struct video_mbuf mbuf;
         unsigned char *map;
      };
      
      typedef struct _v4l_struct v4l_device;
      
      extern int v4l_open(char *, v4l_device *);
      extern int v4l_close(v4l_device *);
      extern int v4l_get_capability(v4l_device *);
      extern int v4l_set_norm(v4l_device *, int);
      extern int v4l_get_channels(v4l_device *);
      extern int v4l_get_audios(v4l_device *);
      extern int v4l_get_picture(v4l_device *);
      extern int v4l_grab_init(v4l_device *, int, int);
      extern int v4l_grab_frame(v4l_device *, int);
      extern int v4l_grab_sync(v4l_device *);
      extern int v4l_mmap_init(v4l_device *);
      extern int v4l_get_mbuf(v4l_device *);
      extern int v4l_get_picture(v4l_device *);
      extern int v4l_grab_picture(v4l_device *, unsigned int);
      extern int v4l_set_buffer(v4l_device *);
      extern int v4l_get_buffer(v4l_device *);
      extern int v4l_switch_channel(v4l_device *, int);
      
      #endif   

爲了維護方便,這裏我們建立一個 v4l/ 的目錄來放 v4l.h 與底下的v4l.c 檔案。編譯 main.c 時,也別了也要編譯 v4l.c,並且要指定 v4l.o 的位置給 main.o 才能順利link;或者我們可以把 v4l.o 再做成 libv4l.a形式,這是屬於 Linux programming相關的主題,請自行參考這方面的資料。

我們的 v4l_xxx() 函數則是放在 v4l/v4l.c 檔案裏。v4l/v4l.c 的內容如下 (完整程序碼,只列出目前會用到的函數):   

      #include <stdio.h>
      #include <unistd.h>
      #include <error.h>
      #include <assert.h>
      #include <fcntl.h>
      #include <sys/ioctl.h>
      #include <sys/types.h>
      #include <sys/mman.h>
      #include <linux/videodev.h>
      #include "v4l.h"
      
      #define DEFAULT_DEVICE "/dev/video0"
      
      int v4l_open(char *dev, v4l_device *vd)
      {
         if (!dev)
            dev = DEFAULT_DEVICE;
      
         if ((vd->fd = open(dev, O_RDWR)) < 0) {
            perror("v4l_open:");
            return -1;
         }
      
         if (v4l_get_capability(vd))
            return -1;
      
         if (v4l_get_picture(vd))
            return -1;
      
         return 0;
      }
      
      int v4l_get_capability(v4l_device *vd)
      {
         if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {
            perror("v4l_get_capability:");
            return -1;
         }
         return 0;
      }
      
      int v4l_get_channels(v4l_device *vd)
      {
         int i;
      
         for (i = 0; i < vd->capability.channels; i++) {
            vd->channel.channel = i;
      
            if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel))       < 0) {
               perror("v4l_get_channel:");
               return -1;
            }
         }
         return 0;
      }
      
      int v4l_get_audios(v4l_device *vd)
      {
         int i;
      
         for (i = 0; i < vd->capability.audios; i++) {
            vd->audio.audio = i;
      
            if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio))       < 0) {
               perror("v4l_get_audio:");
               return -1;
            }
         }
         return 0;
      }
      
      int v4l_get_picture(v4l_device *vd)
      {
         if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {
            perror("v4l_get_picture:");
            return -1;
         }
         return 0;
      }
      
      int v4l_close(v4l_device *vd)
      {
         close(vd->fd);
         return 0;
      }   

當程程序無法初始化裝置時,會出現的錯誤訊息:   

      v4l_open:: No such device
      device_init: failed...: No such device   

如果出現這樣的錯誤:   

      v4l_open:: Device or resource busy
      device_init: failed...: Device or resource busy   

最大可能的原因可能是:(1 )軀動程序沒有安裝好或軀動程序不適用,(2)「前人」的程序忘了將裝置檔關閉。如果程序可以順利初始化裝置,就會看到這樣的訊息:   

      /dev/video0: initialization OK... BT878(Hauppauge new)
      3 channels
      1 audios
      
      OK!   

我們將取得的裝置信息 print到屏幕上,以瞭解取得的相關信息。在下一期的文章裏,我們將會介紹更多video4linux 的設計方法,來做到更高級的工作。在這裏我們看到程序已經成功初始代我們的裝置,並且知道裝置有 3 個 channel、1 個 audio。

STREAMS Programming

接下來要介紹的是屬於觀念性的話題,比較不重要。我們將以理論爲主,來講解"STREAMS" 程序設計的基礎觀念。

什麼是 STREAMS?

在 Solaris 2 的 kernel 裏,STREAMS定義了一個標準界面,這個界面主要的功能是提供裝置與kernel之間的 I/O 溝通管道。這個界面其實是由系統呼叫 (system calls)與核心常式 (kernelroutines) 所組成,我們可以簡單表示成下圖:

   

 

    圖 1


圖中的 Module 標示爲 Optional,也就是在 Stream Head 與 Driver 之間,並不一定存在這個Module,這個 Module 屬於中間者的角色,也就是,當 stream (解釋成資料串流或許比較好理解) 在Stream Head 與 Driver 之間「流」動時,Module 會從中做額外的處理。

有時這個 Module是相當重要的,因爲資料串流必須經過特殊的處理,才能流向彼方。這種 kernel 設計的方式相當好,因爲 Module 一定是動態 (dynamic) 被裝到串流裏的。而且,這個Module 是由 user process 所載入,因此,user     可以根據不同的心情「抽換」不同的 Module。

在最底下 Driver 的地方一般指的是 UNIX底下的設備檔,到這裏,讀者有沒有感覺到,是不是有些觀念跟我們實作出來video4linux 程序庫可以相連呢!

由圖可以看出,根據 stream 的流向,可以將 stream 分成 downstream 與upstream。由於stream 是雙向的,所以我們可以把 STREAMS 稱爲全雙工模式 (full-duplex) 的資料處理與傳送。 我們可以把圖 1 再簡單表示成下圖:

    
    圖 2


由這裏可以發現一個事實,整個 STREAMS 的起點是 Driver,而終點是User Process。在 user space 與 kernel space 之間則是由 Stream head 來連接。

當然,user process 可能是 local user process 或者 remote user process。目前爲止,我們尚未進入user process 的部份,所以暫時不會提到 RTP 等通訊協定的設計。

接下來,再介紹一下 downstream 與 upstream。通常,downstream 也稱爲writeside,也就是寫入資料那一方;而 upstream 則稱爲 read side,也就是讀取資料那一方。那麼,在UNIXprogramming 裏,什麼時候會牽涉到 STREAMS 呢?

最簡單的例子莫過於由終端機讀取字元的範例了。一個簡單的程序片段如下:   

      main()
      {
         char buf[1024];
         int fd;
         int count;
      
         if ((fd = open("/dev/tty1", )_RDWR)) < 0) {
            perror("open: /dev/tty1");
            exit(1);
         }
      
         while ((count = read(fd, buf, sizeof(buf))) > 0) {
            if (write(fd, buf, count) != count) {
               perror("write: /dev/tty1");
               break;
            }
         }
      
         exit(0);
      }   

對 Network programming 而言,如果我們要經由 Socket讀取字元,可以寫一個簡單的程序如下:   

      int main(int argc, char *argv[])
      {
         char *buff = "Hello, socket!";
         int sockfd;
         struct sockaddr_in serv_addr;
      
         sockfd = socket(AF_INET, SOCK_STREAM, 0); 
      
         serv_addr.sin_family = AF_INET;
         serv_addr.sin_addr.s_addr = inet_addr("192.168.1.10");           // ip
         serv_addr.sin_port = htons(3999);                       //       port
      
         connect(sockfd, &serv_addr, sizeof(serv_addr));
         write(fd, buff, strlen(buff));
      
         close(sockfd);
         exit(0);
      }   

這是一個 client 端向 server送出字元的程序範例,這段程序主要是要讓讀者看出,經由終端機設備寫入字元時,是利用write() 函數,而在 socket 上寫入字元,卻也是利用 write() 函數。

這種 UNIX kernel 整合外圍設備與網絡 I/O 的機制事實上就是 STREAMSprogramming所要解決的問題。整合 UNIX kernel 與網絡 I/O 的工作首先由Dennis Ritchie這位大師所進行,所以現在我們纔會擁有現今這麼強大的 UNIX系統。

到目前爲止,我們仍然只對 video capture card 做初始化的動作,並討論一些觀念,接下來的文章將以循序漸進的方式實作整個  Video Streaming 系統。

 

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