Linux 的應用(刊載於 PC2000 雜誌六月號)-- Video Streaming 探討 (2)

jollen 發表於 May 14, 2001 2:36 PM

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

繼上一篇介紹過 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 可以讓我們經由網站來播放串流影像 (streaming video)。

由於我們的最終目的是實作出一個可以做 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 如何設計 Video Streaming 的應用程式。上一期所介紹的 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 所提供的 video4linux APIs 來設計程式。這一期的目的在於利用 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 相關資料。熟悉 UNIX programming 的讀者一定知道,open() 也與 STREAMS 的觀念相關,這部份在後面會再另外做介紹。

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

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

v4l_get_capability() 會利用 ioctl() 取得設備檔的相關資訊,並且將取得的資訊放到 struct video_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[i].channel = i;

if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel[i])) < 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[i].channel = i

然後再取得 channel 的資訊:

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

傳遞 VIDIOCGCHAN 給 ioctl() 則會傳回 channel 的資 訊,這裡則是將 channel 的資訊存放於 vd-> channel[i]。

要注意一下,在 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[i].audio = i;

if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio[i])) < 0) {
perror("v4l_get_audio:");
return -1;
}
}
return 0;
}

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

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

vd->audio[i].audio = i;

然後再取得 audio 的資訊:

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

傳遞 VIDIOCGAUDIO 給 ioctl() 則會傳回 audio 的資訊,這裡則是將 audio 的資訊存放於 vd-> audio[i]。

中標=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[i].channel = i;

if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel[i])) < 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[i].audio = i;

if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio[i])) < 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) 與核心常式 (kernel routines) 所組成,我們可以簡單表示成下圖:

1.png (46483 bytes)

圖 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.png (28851 bytes)

圖 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 也稱為 write side,也就是寫入資料那一方;而 upstream 則稱為 read side,也就是讀取資料那一方。那麼,在 UNIX programming 裡,什麼時候會牽涉到 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 的機制事實上就是 STREAMS programming 所要解決的問題。整合 UNIX kernel 與網路 I/O 的工作首先由Dennis Ritchie 這位大師所進行,所以現在我們才會擁有現今這麼強大的 UNIX 系統。

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

下期預告

下一期我們將實作更進階的 video4linux,並且討論一下 i2c 與 bttv 這兩個 driver,同時也介紹一些較實用且有趣的相關主題。

*1: 見 PC2000/4月份:Linux 的應用--Video Streaming(一)

網路資源&參考資料:

Jollen's Blog 使用 Github issues 與讀者交流討論。請點擊上方的文章專屬 issue,或 open a new issue

您可透過電子郵件 jollen@jollen.org,或是 Linkedin 與我連絡。更歡迎使用微信,請搜尋 WeChat ID:jollentw