本文出處: www.jollen.org
已取得原作者授權使用
轉貼方便自己閱讀
Linux 的應用(刊載於 PC2000 雜誌四月號) -- Video Streaming 探討 (1)
jollen 發表於 February 13, 2001 3:41 AM
Video Streaming 的技術已經出現多年,在 Internet 上的應用也已經相當廣泛,而利用 Linux 建構 Video Streaming Server 也是相當熱門的話題。如果您想瞭解什麼是 Video Streaming,本文對 Video Streaming 的觀念與技術將做了清楚而基本的介紹。
Video Streaming 來的正是時候
看 到 "Video" Streaming 就知道這個技術與影音有關。Video Streaming 是一種經由網路來撥放影音檔案的技術,Video "Streaming" 的基本概念為「一邊下載一邊撥放」,我們稱之為「Play as received」。
經由 Internet 如果要收看遠端伺服器的電視檔案,最原始的做為是「下載後再撥放」,也就是經由 FTP 或 HTTP 將整個檔案下載至本地端後再利用撥放程式來撥放,我們稱之為「Play after download」。
以 最常見的例子來講,我們可以經由網路將 MP3 完整下載後再撥放,也就是 Play after download,或者我們也可以一邊下載 MP3 一邊聽音樂,這種方式則稱為 Play as received。其他的應用範例還很多,例如利用 RealPlayer「即時觀看影片或是聽音樂」,這種即時撥放的方式即是 Play as received,我們稱這樣的技術為 Video Streaming Technology。
為什麼說 Video Streaming 的技術來的正是時候呢?舉個最簡單的情況,你可能在下載 MP3 之前想要試聽一下音樂的內容,而下載音樂片段的方式又顯得不夠友善,這時如果利用 Video Streaming 的技術來讓網友視聽,不但方便,而且不必浪費時間來下載不喜歡的音樂的。
隨著網路頻寬的改進與 IPv6 的出現,Video Streaming 的應用將越來越廣泛,利用 Video Streaming 來設計的資訊家電也會越來越多,例如較常被提及的網路電話 (InterPhone)。
除了寬頻網路的普及外,儲存設備的大進步也是加速 Video Streaming 進用普及的最大動力,例如利用 Linux 設計的 NAS 可以被用來當做大型的影音伺服器或是動態電影資料庫 (DMD)。
VCD & MPEG-1
要提到撥放動態影音的先軀,絕對要先從 VCD 說起,因為這是在電腦出現後,最能深入消費者市場的產品。
想 當年,要撥放 VCD 除了要有一台當時算是高擋貨的 1x CD-ROM 外,還要再另外加裝也是高擋貨的 MPEG 卡。撥放 VCD 其實就是 Video Streaming 的技術,因為 VCD 的資料是一邊讀取一邊撥放,那為什麼要有 MPEG Card 呢?理所當然的,這是因為MPEG-1 的標準。
MPEG-1 被設計在 1x CD-ROM 上撥放 VCD,而 VCD 上的資料必須經由 MPEG Card 解碼 (decode) 後才能撥放。但是隨著硬體的進步,現在可以經由軟體來做 MPEG-1 解碼的工作。我們將在後文介紹 MPEG-1 的設計概念。
DVD & MPEG-2
MPEG-2 常被提到則是由於 DVD 的流行。不管是 MPEG-1 或是 MPEG-2,其實是一種編碼的技術,而 DVD-ROM 則是選擇了 MPEG-2 來當做它的壓縮標準。
而 DVD 之所以無法像 VCD 一樣能被「備份到硬碟」,則是由於其中又加進了三道的保護,第二道保護就是玩家們耳熟能詳的 CSS。在 Linux 下之所以無法有「合法」的 DVD Player,就是受制於 CSS 的關係。
利 用 NAS 來設計 Video Server,要儲存並廣撥 VCD 已經沒有技術上的障礙,但是要儲存並擴撥 DVD,則有法律上的問題,這點是值得我們深思的一個問題。因為目前 CSS 的技術掌握在日本人手裡,要取得這項技術必須簽署 NDA,不過 CSS 並非不可破解想必大家都知道,但是就是有版權上的問題。
影像編碼技術介紹
目前學術界已經發展出許多處理影像訊號壓縮及編碼的技術 (codecs),談到這些技術,應用最廣泛的編碼標準底下四種:
- ) H.261
- ) H.263
- ) JPEG, MJPEG
- ) MPEG
底下將分別簡單介紹這四種編碼技術。
H.261 標準
H.261 的影像編碼標準出現在 1990 年的 ITU。一開始的用途主要是支援影像電話 (video phone) 與視訊會議 (video conferencing)。H.261 的格式有二種,分別有不同的解析度:
- ) QCIF:176x144
- ) CIF:352x288
H.261 的 fps (frames per second) 可以達到 7.5, 10, 15 與 30 fps。由於 H.261 一開始是架構在 ISDN B 上面,而 ISDN B 的傳輸速度為 64 Kbps,所以 H.261 也被稱為 Px64 (x = 1 to 30)。
CIF 全名為 (Common Intermediate Format),主要是為了要支援各種不同解析度的電影而被定義出來,例如 NTSC, PAL, SECAM 電視系統。而 QCIF 則是 Quarter-CIF 也就是 CIF 解析度的一半。
除此之外,H.261 也可以說是 MPEG-1 標準的前輩。
H.263 標準
H.263 是 H.261 的加強版,誕生於 1994 年 (ITU)。H.263 開始支援 PSTN,不過要特別說明一點,H.263 比 MPEG-1 還要晚出現,而且 H.263 還是基於 MPEG-1 而發展。
H.263 的標準共支援五種不同的解析度,分別為:
- ) Sub-QCIF:128x96
- ) QCIF:176x144
- ) CIF:352x288
- ) 4CIF:704x576
- ) 16CIF:1408x1152
傳輸速度為 8 Kbps ~ 1.5 Mbps。除此之外,H.263 也是 MPEG-4 標準的基礎。
JPEG
提 到 JPEG 的標準,大家可就不莫生了,由其是在 GIF 發出禁令後,JPEG 更是被廣泛應用。JPEG 的全名大家一定不莫生,他可是赫赫有名的一群技術團隊的縮寫 - Joint Photographic Experts Group。
JPEG 是 24-bit 的 "true-color" 影像標準,JPEG 的工作是將 RGB 格式的影像轉換成 YCrCB 格式,目的是為了減少檔案大小,一般約可減少 1/3 ~ 1/2 左右。
不 過 MJPEG 才是我們的主角,MJPEG 全名為 "Motion" JPEG,也就是會動的 JPEG 圖檔。許多 Video Streaming 的場合,像是簡單的視訊會議軟體都會使用 MJPEG 來取代 MPEG,原因無它,因為 MJPEG 格式簡單,但缺點是不支援聲音。
MPEG-1 標準
MPEG 的標準由 ISO (International Standards Organization) 所制定,全名為 Moving Pictures Experts Group (MPEG 為 ISO 工作),這些團隊制定了包括 MPEG-1、MPEG-2、MPEG-4 等標準。
MPEG-1 的標準比 H.263 早出現,MPEG-1 制定於 1992 年,主要用途為:視訊會議、影像電話、電腦遊戲與支援第一代的 CD-ROM。MPEG-1 被設計來支援大部份的影像與 CD-ROM 的音效,傳輸速度為 1.5 Mbps (30 fps)。
除此之外,MPEG-1 也支援 playback,例如快轉、倒帶或是跳躍,這也是 MPEG-1 好用的地方。稍後我們會簡單介紹一下 MPEG-1 的原因,以明白 MPEG-1 如何做到這些功能。
MPEG-2 標準
MPEG-2 的標準出現於 1994 年,MPEG-2 相容於 MPEG-1。MPEG-2 的出現並不是為了要取代 MPEG-1 的舊標準,而是要加強 MPEG-1 不足的地方。因此 MPEG-2 更能昇任其它工作環境,例如 HDTV、視訊廣播。
同時,MPEG-2 的解析度也支援到 HDTV 1280x720。在音效方面,MPEG-2 也支援到 6 個頻道 (MPEG-1 只支援 2 個頻道)。MPEG-2 的傳輸速度也提昇至 2Mbps ~ 10 Mbps,因此需要 4x 的 CD-ROM,但也因此 4x CD-ROM 只能儲存 18 分種的影像,所以我們利用 DVD-ROM 來儲存 MPEG-2 格式的影像,而且 DVD-ROM 也支援 Dolby 音效。
MPEG-4 標準
MPEG-4 的標準在 1993 年被提出,主要的應用用途比較廣,包括:視訊會議、影音郵件、無線裝置等等,支援的傳輸速度為 8Kbps ~ 35Mbps。
MPEG-4 可以傳送影像的物件,而不是只有影像的 "frame",例如一連串的動作指令。因此,MPEG-1 與 MPEG-2 皆是 "frame-based" 的標準,而 MPEG-4 則是 "object-based" 的標準,未來在網路與多媒體的應會也會更普遍。
MPEG-4 目前在 Linux 上已經有 OpenMPEG 的專案計畫,發起本計畫的目地在希望可以在 Linux 上發展完整的 MPEG-4 支援環境。
Video Streaming 的網路技術
要 建置一個完整的 Video Streaming 環境,在整體來看,要準備的硬體包括:伺服器、CCD、影像補捉卡等等,而在底層的技術面方面,除了前面提到的影像編碼技術外,也要配合通訊協定才能實作 出 Video Streaming 的應用程式。
Video Streaming 的方式
Video 在做 Streaming 時,有三種方式可以應用:broadcasting、unicasting、multicasting。
broadcasting 的方式比較單純,他是在 LAN 上直接將一個個的影像封包丟到網路上 (server 端),再由 client 的應用程式自網路上取回封包播放。但網路硬體層上,仍有許多需要考慮的問題,例如在 Shared Non-Switched Enthernet 上時,就會發生一些小問題。
unicasting 與 multicasting 都是屬於 IP 的傳輸方式。unicasting 採取 1 對 1 的方向傳影像給遠端,稱為 Video-on-Demand (VoD),multicasting 則是 1 對多的傳輸方式,稱為 Near-Video-on-Demand (NVoD)。未來 IPv6 將支援 IP Multicasting,因此 Video Streaming 的應用將更為廣泛。
IPv6
IPv6 全名為 IP version 6,未來將取代目前所使用的 IPv4。IPv6 新增許多對多媒體傳輸的支援,例如 multicasting、authentication/encryption等。不過目前尚仍未全面升級,所以應用仍受限。
UDP 通訊協定
UDP 全名是 Universal Datagram Protocol,UDP 通訊協定是 Video Streaming 的基石。UDP 與 TCP 不同的是,UDP 並沒有錯誤檢查,不過這在 Video Streaming 的應用是影向不太的。
RTP 通訊協定
RTP 全名為 Real-Time Protocol,RTP 是在 UDP 封包之前多加 10 bytes 的檔頭,裡面記載有時間、序號、壓縮型態等資訊。RTP 是目前大多數 Video Streaming 軟體所使用的通訊協定。
RTP 可用來針對各種不同的多媒體格式做 Streaming 的工作,因為我們將影像分解成數個 RTP 封包再傳送出去,因此會遇到許多網路技術常會遇到的問題。例如,因為封包送達的時間不一,造成播放時會畫面不流暢的現像,因此,在播發時就必須使用一個緩 衝區 (playout buffer) 來暫時存放並處理網路上接受到的封包。
由網路上接收的影像封包因為彼此之間到達的時間間隔不同 (Synchronous Data Packets),所以必須利用緩衝區將這些封包做緩衝,讓彼此之間的時間間隔一樣 (Isochronous Data Packets)。
MPEG-1 的原理
MPEG-1 的編碼原理大家都應該要認識一下。MPEG-1 將影像分成一個 Group,即 GOP (Group of Pictures),每個 GOP 有一個 Entry Point,稱為 I-frame,每個 I-frame 的大小為 15KB,另外還有 P-frame 與 B-frame。
I-frame 稱為 Independent-frame,為每個 GOP 的起點,緊接著的是二個 B-frame,B-frame 稱為 Bi-directional frame,每個 B-frame 大小為 3KB,每二個 B-frame 之間再插入一個 P-frame,即 Predictive-frame,每個 B-frame 的大小為 8KB。
GOP 共有三種不同的 frame,每種 frame 存放的影像與性質皆不同,這在以後我們實做程式時會再做更清楚的介紹。例如,我們要將影片快轉時,可以忽略掉所有的 B-frame 與 P-frame,只撥放 I-frame,如此一來便可以節省許多貴寶的網路頻寬與時間,但前提是,I-frame 必須存放主要的影像資料,事實上也是如此。
本期結語
本篇文章主要的目的是要讓大家對影像的一些標準與 Video Streaming 所使用的通訊協定有一定的認識,更多與影像有關的內容也會陸續為大家做介紹。
下一期我們要介紹更多有趣的主題,包括 Linux 核心裡的 video4linux,與 Linux 環境下的影像專案與好用的 Video Streaming 應用程式。
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
圖 中的 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 也稱為 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(一)
網路資源&參考資料:
- VideoLAN -- http://www.videolan.org/
- VIC -- http://www-nrg.ee.lbl.gov/vic/
- -- http://www-mice.cs.ucl.ac.uk/multimedia/software/vic/
- mod_mp3 -- http://media.tangent.org/
- Linux kernel 文件 -- video4linux
- Real Networks -- http://www.realnetworks.com/
- Solaris 2.5 -- STREAMS programming
- BTTV -- http://www.metzlerbros.de/bttv.html
Linux 的應用(刊載於 PC2000 雜誌八月號)-- Video Streaming 探討 (3)
jollen 發表於 July 17, 2001 6:49 AM
本期的重點在介紹影像擷取卡與影像擷取卡常見的 BT848/BT878 晶片。除了介紹影像擷取卡外, 也會來看 xawtv 這個有名的 video4linux/BTTV 應用程式。
作者: 陳俊宏 (www.jollen.org)
什麼是影像擷取卡
影像擷取卡的主要功能當然就是做影像的擷取了, 一張影像擷取卡應該擁有底下的基本功能。
影像擷取
影像擷取卡可擷取的影像頁框 (frame) 的大小是很重要的, 本文第一篇裡提到的幾個基本影像頁框大小都應該要支援, 包含 NTSC (640x480) 與 PAL (768x576)。
影像擷取卡所捕捉的畫面都是屬於動態的畫面, 影像的來源則要看所支援的視訊系統與所有的視訊裝置。
支援的視訊系統
大部份影像擷取卡都會具備一組視訊輸入端子, 即 S-Video (Y/C) 端子或 Composite 端子。
在台灣的標準當然是 NTSC 系統, 一般而言, 我們是希望一張影像擷取卡可以支援越多視訊系統越好, 包括: NTSC/PAL/PALN/PLAM/SECAM。
可使用的視訊裝置有較常見的 CCD, 或是家用 V8、Hi8 皆可, 一般而言我們也是希望一張影像擷取卡可以接越多視訊裝置越好。
安裝影像擷取卡
以 筆者的影像擷取卡為例,使用的是 Chronos Video Shuttle I 的卡,所以只要安裝 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) 裡。
如果是使用 Linux kernel 2.4.x (如 Red Hat Linux 7.1), 也可以直接在 /etc/modules.conf 裡加上一行:
alias char-major-81 bttv
就可以了。然後再利用上一篇文章 (2) 的程式來做初始化, 可以看到底下的訊息:
/dev/video0: initialization OK... BT878(Chronos Video Shuttle I)
3 channels
3 audios
OK!
BT878 晶片初始化成功, 影像擷取卡名稱為 Chronos Video Shuttle I, 成功找到並初始化 3 個 channel。
bttv 模組就是底下會提到的 BTTV 軀動程式, 主要用途是軀動影像擷取卡上的 BT848/BT878 晶片。
影像擷取卡的應用
在 PC 上加裝影像擷取卡, 就可以利用 PC 做影片剪輯的工作, 例如可以將類比的 V8 影片轉換成數位影像, 並且儲存到電腦裡, 或製作成光碟保存。
配合這類的軟體, 我們還可以 DIY 做影像的特效、轉場特效、字幕、旁白等等。利用 PC 做影像剪輯的工是很耗電腦資源的, 所以不能用太陽春的 PC, 不然可以會很累人的!
壓縮比
壓縮比是判斷一張影像擷取卡優劣的主要關鍵所在, 由於動態的影像所佔用的空間相當大, 所以如果一張影像擷取卡無法有效將擷取出來的影像做壓縮, 所需的硬碟空間都會相當驚人。
壓縮比越好的影像擷取卡, 其工作效能越佳, 一般壓縮比大約是 4:1, 使用壓縮比越好的晶片, 當然也就越貴, 所以影像擷取卡是一分錢一分貨的。
BT 878 晶片
目 前大部份的數位影像擷取卡大部份都是以 BT878 單顆晶片為影像擷取卡之中心。BT878 運作方式是以軟體來進行影像解壓縮工作, BT878 晶片負責將擷取之影像丟給 Linux 做影像處理, 而 BTTV 則是 Linux kernel 的 BT878 晶片軀動程式。
由於影像是利用 BT878 擷取後交由軟體來做影像處理, 因此在處理效能上自然就會比較差。如果是經由網路來傳送影像的話, 我們就會再利用影像壓縮技術 (H.261/H.263...等等) 來做影像處理。
什麼是 BTTV
BTTV 是 Linux 上的 Bt848/849/878/879 晶片的軀動程式, 主要功能是做頁框
的截取 (frame grabber)。
BTTV 是 video4linux 裡重要的軀動程式, 目前分為二個版本:
- 0.8.x 的發展中版本
- 0.7.x 的穩定版本
Linux 上可用的影像擷取卡
在 http://www.linhardware.com/db/searchproduct.cgi?_catid=17 網頁上可以找到在 Linux 上支援程度比較好的幾張影像擷取卡。而一般 Linux 上較受歡迎的影像擷取卡則是 Hauppauge 的幾張卡, 筆者使用的也是 Hauppauge 的卡。
在 linhardware 網站上可以找到底下六張卡:
- Hauppauge 401 WinTV-radio dbx-TV stereo
- Hauppauge WinTV PCI TV Card
- Hauppauge WinTV-GO PCI TV Card
- Hauppauge WinTV-PCI Hauppauge
- Hauppauge WinTV-Radio+NICAM
- Hauppauge WinTV/PCI TV Card
關於 Linux 對於影像捕捉卡支援的中文文件 (HOWTO) 可以在 CLDP 網站上取得:
http://www.linux.org.tw/CLDP/Hardware-HOWTO-22.html
BTTV 相關軟體 - xawtv
官方網站: http://bytesex.org/xawtv/index.html
安裝方式:
1. linux# ./configure
2. linux# make depend
3. linux# make
4. linux# make install
如果您有 Red Hat Linux 7.1 PowerTools 光碟片的話, 也可以直接由 PowerTools 光碟片安裝 xawtv 套件:
linux# rpm -ivh xawtv-3.34-1.i386.rpm
安裝 xawtv 需要 libjpeg 與 libjpeg-devel 套件, 如果您是使用 Red Hat Linux 7.1 的話, 應該安裝底下二個套件:
libjpeg-6b-15.i386.rpm (Disc 1)
libjpeg-devel-6b-15.i386.rpm (Disc 2)
xawtv 整個架構可以分成 7 個部份如下:
- xawtv: 主程式部份。
- fbtv: linux console 模式的 TV 應用程式, 使用 linux kernel 2.2.x 的 framebuffer。
- set-tv: 命令列模式的工具, 用來設定 video4linux 的參數。
- streamer: 命令列模式的工具, 用來捉取動態影像與 avi 影像。
- radio: radio 應用程式。
- webcam: 將捉取的影像以 FTP 方式上傳到 Web Server 端, 用來設計 Web 即時影像的工具。
- alevtd: videotext pages 的 Web Server。
xawtv 的 video4linux
xawtv 是相當好的 video4linux 方面的教材, 在上一期我們看過一遍 video4linux 的設計方法後, 接下來我們要先捨棄我們上一期所實作的 video4linux 程式庫!
因 為我們將要帶導大家研究 xawtv 裡以 video4linux API 設計的影像捕捉 (grab) 部份的程式碼, 這個程式碼的實作更完整, 因此在開始進入這個主題前, 必須先理解上一期所介紹的內容, 並且懂得我們實作的程式庫內容, 才能順利進入狀況。
取 回 xawtv 的原始程式碼解開後, 在 libng/ 目錄下可以看到 grab-v4l.c 的檔案, 另外還有一個 grab-v4l2.c 的檔案, 這是 video4linux2 (version 2) 的版本。這裡我們先介紹 video4linux 的版本 -grab-v4l.c。
grab-v4l.c 的程式碼架構
大略掃瞄過 grab-v4l.c 的程式碼後, 發現 grab-v4l.c 與上一期我們實作的 video4linux 程式庫的架構很像。
其實大多數利用 video4linux 來設計應用程式的軟體, 其 video4linux 的 API 部份大多是自行重寫的, 反而很少利用現成的程式庫, 因此仔細研究別人的程式碼, 再來改進我們自己的 video4linux 程式庫是我們的最終目的。
程式一開始也是宣告了一個 ng_driver 結構的變數 (v4l_driver):
#ifndef __linux__
const struct ng_driver v4l_driver;
#else /* __linux__ */
然後可以看到 open 與 close 二個函數:
static void* v4l_open(char *device);
static int v4l_close(void *handle);
接下來還有許多函數的宣告, 同時還可以看到一些結構的宣告, 例如:
static struct STRTAB norms_v4l[] = {
{ VIDEO_MODE_PAL, "PAL" },
{ VIDEO_MODE_NTSC, "NTSC" },
{ VIDEO_MODE_SECAM, "SECAM" },
{ VIDEO_MODE_AUTO, "AUTO" },
{ -1, NULL }
};
我們可以用來顯示 channel 是 PAL/NTSC/SECAM 或是 AUTO。
xawtv 實作出來的 v4l_open() 函數可精彩了, 除了有設計函數應該要有的嚴謹的錯誤檢查外, v4l_open 也對硬體做了一些檢查。
xawtv 的 v4l 程式庫也有 v4l_close() 函數, 用來關閉已開啟的 video4linux 裝置。
在影像頁框擷取方面, 共有底下 5 個函數:
static int v4l_setformat(void *handle, struct ng_video_fmt *fmt);
static int v4l_startvideo(void *handle, int fps, int buffers);
static void v4l_stopvideo(void *handle);
static struct ng_video_buf* v4l_nextframe(void *handle);
static struct ng_video_buf* v4l_getimage(void *handle);
函數的名稱已經將其功能表達的很清楚了, 因此不再重覆解釋。另外比較特別的是, xawtv 的 video4linux 也支援了 framebuffer, 主要函數為:
int v4l_setupfb(void *handle, struct ng_video_fmt *fmt, void *base)
xawtv 的 video4linux 程式庫整體架構相當明白簡單, 程式碼也很好懂, 剩下的重點在於如何利用 video4linux 的 mmap 模式來擷取二頁的影像, xawtv 的 video4linux 程式庫的使用方式我們只要瞭解這一點即可。
xawtv 的 video4linux 其中對影像擷取卡做初始化的方法我們已經會了, 再來就是做影像擷取的部份。
mmap 方式我們將在下一期再做介紹。接下來的部份將瀏覽一下 xawtv 的功能架構與使用界面, 以利我們去研究 xawtv 的程式碼, 並且還會介紹一個 Linux 下的 MP3 撥放程式 --, 會介紹 xxx 的原因是因為 xxx 也用到了 streaming 的技術來撥放 MP3 檔案。
xawtv 的功能
底下我們將展示以 CCD (Composite 端子) 視訊裝置輸入影像, 並且利用 xawtv 顯示動態影像的設定方式。
xawtv 的輸出結果
圖 1
這是 xawtv 顯示動態影像的結果, 我們利用的是 CCD 視訊裝置, 視訊來源為 Composite, TV Norm 為 NTSC。
要有正確的顯示結果, 必須正確地設定 TV Norm 與 Video Source 兩個項目。
xawtv 的設定選單
在 xawtv 的主畫面按二下滑鼠右鍵後, 就會出現圖 2 的視窗。幾個常用的功能說明如下:
- Full Screen on/off: 切換是否要全螢幕顯示, 不過影像的大小則是跟視訊裝置有關。
- Grab Image: 將目前的畫面存成圖檔 (ppm 或 jpeg)。
- Record Movie (avi): 錄製 avi 的電影檔。
- Bright/Hue/Contrast/Color: 調整視訊的對比、亮度、顏色。
圖2
設定 TV Norm
點取 TV Norm 項目後, 將 TV Norm 設定在正確的參數, 筆者使用的是 NTSC 的 CCD, 因此必須將這個項目設成 NTSC。
#圖: xawtv-03.tif
圖 3
設定 Video Source
點取 Video Source 後, 再選取正確的視訊來源, 大部份的 CCD 都是 Compoiste 端子, 因此選擇 Composite1。
#圖: xawtv-04.tif
圖 4
xawtv 會列出目前影像擷取卡可用的視訊來源, 我們只要將視訊裝置接到正確的視訊來源, 並完成 TV Norm 與 Video Source 的設定後就可以正確顯示動態影像了。
利用 video4linux 來初始化影像擷取卡的方法在上一篇文章 (2) 中已列出完整範例程式碼, 並且做了詳細的說明。下一期我們將會更進一步來撰寫設定 TV Norm 與 Video Source 的程式碼。
配合介紹如何設計 TV Norm 與 Video Source 的設定程式碼, 我們還會根據 xawtv 的幾個功能並配合 video4linux 來實作 xawtv 上的功能 (frame grab), 並且會列出完整的程式碼範例做說明。
xawtv 也提供針對 FreeBSD 與 OpenBSD 的 BT848/BT878 軀動程式 (bktr) 所設計的影像擷取功能, 原始程式碼放在 xawtv 裡的 libng/ 目錄, 檔名為 grab-bsd.c 檔案。
(MP3 - streaming 部份...)
Linux 的應用(刊載於 PC2000 雜誌九月號)-- Video Streaming 探討 (4)
jollen 發表於 August 13, 2001 3:32 AM
本期的重點在介紹擷取 frame 的方法, 並且將重心由 video4linux 轉移到網路方面。在網路影像即時傳送方面, 我們採用的 RTP 也是各大廠商使用的標準, 在這一期裡, 我們將可以學習到利用 JRTPLIB 來加入網路功能的方法。
作者: 陳俊宏
www.jollen.org
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) 通訊協定
inux IP Stacks 一書的書點放在基本與重要的 TCP/IP 服務上, 包含:遶送、封包管理、datagram 與 datastream。
Linux IP Stacks 導讀
裡的導讀參考自 Linux IP Stacks 一書的第一章, 在研究 Linux IP Stacks 這本書前, 請讀者先好好研究一下這本書的結構, 到時才比較容易上手。
第二章的部份介紹 TCP/IP 的背景知識與歷史,包括 TCP/IP 的發展過程, 這一章原則上只要瞭解一下即可。
第三章則以學術的觀點來比較 TCP/IP 與 ISO 模型。這一章比較偏向 ISO 的理論,而書上解釋到, 要學習 ISO 模型理論的目的, 是為了能夠瞭解為什麼通訊層要分割成這幾層。
Linux 的應用(刊載於 PC2000 雜誌十一月號)-- Video Streaming 探討 (5)
jollen 發表於 September 14, 2001 8:31 AM
本期將以完整的程式範例為主, 說明之前未深入說明的地方。並且更詳細地介紹video4linux 如何以 mmap (filp-flop) 方式擷取影像資料, 同時也會展示如何將擷取出來的影像存成圖檔, 並且利用繪圖軟體開啟。
作者: 陳俊宏
www.jollen.org
mmap 的初始化從那裡開始
繼 前四期介紹有關 Video Streaming 的內容後, 最近收到幾位讀者的來信, 詢問有關 video4linux 利用 mmap 擷取影像的方法。video4linux 以 mmap 擷取影像的方法在本文第 4 篇曾經簡單介紹過, 但是有讀者希望可以做更詳細的介紹, 因此筆者特別將相關的程式碼完整列出供參考。
要提到 mmap 的初始化, 我們要配闔第 2 篇文章的程式範例。底下是對影像擷取裝置做初始化的程式碼, 與第 2 篇文章的範例比較, 底下的函數設計的更完整:
int device_init(char *dev, int channel, int norm)
{
int i;
if (dev == NULL) {
dev = "/dev/video0"; //set to default device
}
if (v4l_open(dev, &vd)) {
return -1;
} else {
v4l_grab_init(&vd, screen_width, screen_height); //wake up drivers!
v4l_close(&vd);
}
if (v4l_open(dev, &vd)) return -1;
if (v4l_get_channels(&vd)) return -1;
if (v4l_set_norm(&vd, norm)) return -1;
if (v4l_mmap_init(&vd)) return -1;
if (v4l_switch_channel(&vd, channel)) 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);
for (i = 0; i < vd.capability.channels; i++) {
printf("Channel %d: %s (%s)\n", i, vd.channel[i].name,
v4l_norms[vd.channel[i].norm].name);
}
printf("v4l: mmap's address = %p\n", vd.map);
printf("v4l: mmap's buffer size = 0x%x\n", vd.mbuf.size);
printf("v4l: mmap's frames = %d (%d max)\n", vd.mbuf.frames, VIDEO_MAX_FRAME);
for (i = 0; i < vd.mbuf.frames; i++) {
printf("v4l: frames %d's offset = 0x%x\n", i, vd.mbuf.offsets[i]);
}
printf("v4l: channel switch to %d (%s)\n", channel, vd.channel[channel].name);
// start initialize grab
if (v4l_get_picture(&vd)) return -1;
if (v4l_set_palette(&vd, DEFAULT_PALETTE)) return -1;
if (v4l_grab_init(&vd, screen_width, screen_height)) return -1;
if (v4l_grab_sync(&vd)) return -1;
return 0;
}
我們又把 device_init() 寫的更完整了。粗體字的地方是我們初始化 mmap 的程式碼, 一開始的程式可能又讓人覺得一臉汒然:
if (v4l_open(dev, &vd)) {
return -1;
} else {
v4l_grab_init(&vd, screen_width, screen_height); //wake up drivers!
v4l_close(&vd);
}
將 device 開啟成功後, 做了一次 v4l_grab_init 後再把 device 關掉, 用意何在呢? 其實, 是因為 bttv 的 driver 是以 module 的方式安裝到 Linux kernel, 所以 bttv driver 會因為沒有被使用, 而「睡覺了」。
我們加上一次 v4l_grab_init() 的目的就是為了要「叫醒」bttv 的 driver, 其實這個動作可有可無, 但一般認為加上會比較好。
v4l_mmap_init() 是對 mmap 做初始化的工作, 不過要特別注意, 這個動作要在 channel 與 norm 都設定好後才進行, 底下會再說明一次。
v4l_mmap_init() 相當重要, 因為我們要利用 mmap() 函數將 v4l_deivce 結構裡的 map「連接」起來。mmap() 是 POSIX.4 的標準函數, 用途是將 device 給 map 到記憶體, 也就是底下粗體字的地方:
int v4l_mmap_init(v4l_device *vd)
{
if (v4l_get_mbuf(vd) < 0)
return -1;
if ((vd->map = mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, vd->fd, 0)) < 0) {
perror("v4l_mmap_init:mmap");
return -1;
}
return 0;
}
PROT_READ 表示可讀取該 memory page , PROT_WRITE 則是可寫入, MAP_SHARED 則是讓這塊 mapping 的區域和其它 process 分享。第一個參數旦 0 是啟始位置, vd->mbuf.size 則是長度 (length)。vd->fd 則是 device 的 file description, 最後一個參數是 offset。
v4l_get_mbuf() 和之前介紹過的沒有什麼出入。在新的 device_init() 函數裡, 我們也把初始化好的 mmap 相關資訊印出。
channel 與 norm
我們提過, 在做 v4l_mmap_init() 前要先做 channel 與 norm 的設定, 分別是 v4l_get_channels() 與 v4l_set_norm() 函數。
在這裡要捕充說明一點, 以筆者的 CCD 頭來講, 和擷取卡是以 Composite1 連接, 所以在 channel 方面, 就要利用 v4l_switch_channel() 將 channel 切到 Composite1 端。
v4l_switch_channel() 程式碼如下:
int v4l_switch_channel(v4l_device *vd, int c)
{
if (ioctl(vd->fd, VIDIOCSCHAN, &(vd->channel[c])) < 0) {
perror("v4l_switch_channel:");
return -1;
}
return 0;
}
傳入的 c 是 channel, 而 channel number 我們已經在 device_init() 裡列印出來:
Channel 0: Television
Channel 1: Composite1
Channel 2: S-Video
我們可以看到 Composite1 位於 Channel 1 (由 0 算起), 所以 v4l_switch_channel() 的參數 c 要傳入 1。
如何設定 norm
norm 的話就比較單純一點, 參數如下:
VIDEO_MODE_PAL
VIDEO_MODE_NTSC
VIDEO_MODE_SECAM
VIDEO_MODE_AUTO
這些參數都定義於 videodev.h 檔案裡。v4l_set_norm() 是我們用來設定 norm 的函數, 程式碼如下:
int v4l_set_norm(v4l_device *vd, int norm)
{
int i;
for (i = 0; i < vd->capability.channels; i++) {
vd->channel[i].norm = norm;
}
if (v4l_get_capability(vd)) {
perror("v4l_set_norm");
return -1;
}
if (v4l_get_picture(vd)) {
perror("v4l_set_norm");
}
return 0;
}
要仔細注意, 我們是對所有的 channel 設定 norm, 設定完成後, 底下又做了一次 v4l_get_capability(), 主要目的是確保每個 channel 的設定都有被設定成功。然後呼叫 v4l_get_picture。
v4l_get_capability() 會利用 ioctl() 取得設備檔的相關資訊,並且將取得的資訊放到 struct video_capability 結構裡。同理,v4l_get_picture() 也會呼叫 ioctl() ,並將影像視窗資訊放到 struct video_picture 結構。
如何 get 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。
這部份我們也曾經介紹過, 在這裡要再捕充一點。如果是以 GREY 方式擷取影像, 那麼我們可以利用 VIDIOCSPIC 來設定像素的亮度與灰階度, 請參考 API.html 裡的 struct video_picture 說明。
初始化 grab
初始化 grab 的程式碼如下:
if (v4l_get_picture(&vd)) return -1;
if (v4l_set_palette(&vd, DEFAULT_PALETTE)) return -1;
if (v4l_grab_init(&vd, screen_width, screen_height)) return -1;
if (v4l_grab_sync(&vd)) return -1;
v4l_get_picture() 與之前介紹的一樣, 而 v4l_set_palette() 則是用來設定調色盤, 由於我們希望得到的是 RGB32, 所以 DEFAULT_PALETTE 定義成:
#define DEFAULT_PALETTE VIDEO_PALETTE_RGB32
如果沒有硬體轉換, 前一篇文章 (4) 我們也提到將 YUV (PAL) 轉成 RGB 的方法了。再來將就是對 grab 做初始化, v4l_grab_init()
int v4l_grab_init(v4l_device *vd, int width, int height)
{
vd->mmap.width = width;
vd->mmap.height = height;
vd->mmap.format = vd->picture.palette;
vd->frame_current = 0;
vd->frame_using[0] = FALSE;
vd->frame_using[1] = FALSE;
return v4l_grab_frame(vd, 0);
}
初始化的目的是將 mmap 結構填入適當的值。針對 RGB32、NTSC 的 CCD 影像擷取, mmap 的大小不妨設定成 640*480 或 320*240 都可以, 給定 mmap 的大小後, 再來還要將 format 填入調色盤類型。
最後設定 frame_current 變數與 frame_using[] 陣列, 這裡等於上一篇 (4) 介紹的 frame 變數與 framestat[] 陣列。
如何所有的程式碼都沒有錯誤, 當裝置正常軀動時, 就可以看到底下的初始化訊息, 這裡的訊息比起之前的範例更清楚、完整:
/dev/video0: initialization OK... BT878(Chronos Video Shuttle I)
3 channels
3 audios
Channel 0: Television (NTSC)
Channel 1: Composite1 (NTSC)
Channel 2: S-Video (NTSC)
v4l: mmap's address = 0x40173000
v4l: mmap's buffer size = 0x410000
v4l: mmap's frames = 2 (32 max)
v4l: frames 0's offset = 0x0
v4l: frames 1's offset = 0x208000
v4l: channel switch to 1 (Composite1)
Image pointer: 0x4037b000
v4l_grab_frame() 的用處
讀者可能還不明白 v4l_grab_frame() 的用途, v4l_grab_frame() 是真正將影像放到 mmap 裡的函數。
我們重寫一次 v4l_grab_frame() 函數, 並且再說明一次:
int v4l_grab_frame(v4l_device *vd, int frame)
{
if (vd->frame_using[frame]) {
fprintf(stderr, "v4l_grab_frame: frame %d is already used.\n", frame);
return -1;
}
vd->mmap.frame = frame;
if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {
perror("v4l_grab_frame");
return -1;
}
vd->frame_using[frame] = TRUE;
vd->frame_current = frame;
return 0;
}
因為我們用 frame_using[] 陣列來紀錄那個 frame 已經被使用, 所以一開始當然要先判斷目前的 frame 是否已經被使用:
if (vd->frame_using[frame]) {
fprintf(stderr, "v4l_grab_frame: frame %d is already used.\n", frame);
return -1;
}
如 果沒有被使用, 就把 mmap 的 frame 填入 frame 編號, 然後利用 VIDIOCMCAPTURE 擷取出影像。結束前要把目前 frame 的狀態標示成使用中 (frame_using[]), 然後把 frame_current 指定成現在的 frame, 完成工作後離開。
mmap 如何做 filp-flop
這是一位讀者問的問題。這個問題問的相當聰明, 每個人可能都有不同的方法來做 flip-flop 的動作, 這裡筆者以 2 個 frame 為例, 我們可以再寫一個函數來做 flip-flop:
int device_grab_frame()
{
vd.frame_current = 0;
if (v4l_grab_frame(&vd, 0) < 0)
return -1;
return 0;
}
int device_next_frame()
{
vd.frame_current ^= 1;
if (v4l_grab_frame(&vd, vd.frame_current) < 0)
return -1;
return 0;
}
device_next_frame() 是主要核心所在, 因為我們只有二個 frame, 所以 frame_current 不是 0 就是 1。
擷取出來的影像放在那裡
因為我們特別寫了上面的函數來做 mmap 的 flip-flop, 所以在主程式裡就改用 device_next_frame 來持續擷取影像。
所以配合主程式, 我們的程式寫法如下:
device_next_frame(); //Ok, grab a frame.
device_grab_sync(); //Wait until captured.
img = device_get_address(); //Get image pointer.
printf("\nImage pointer: %p\n", img);
這段程式就是我們的重點好戲, 當我們呼叫 device_next_frame() 擷取 frame 之後, 必須做一個等待的動作, 讓 frame 擷取完成再取出影像。
v4l_grab_sync() 程式碼如下:
int v4l_grab_sync(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCSYNC, &(vd->frame_current)) < 0) {
perror("v4l_grab_sync");
}
vd->frame_using[vd->frame_current] = FALSE;
return 0;
}
利用 VIDIOCSSYNC 等待完成後, 別忘了將目前 frame 的狀態改回未被使用。接下來我們要問, 擷出出來的 frame 到底放到那裡去了呢?
答案就是之們利用 mmap() 將 device 所 map 的記憶體裡, 因為我們是利用 mmap (flip-flop) 方式, 所以會有 2 個 (或以上) 的 frame, 這時就要計算一下 offset, 才知道到底目前的影像資料被放到那裡了。
算式如下:
vd.map + vd.mbuf.offsets[vd.frame_current]
device_get_address() 函數就是這麼回事。
如何輸出影像資料呢
輸出影像資料的方法很多, 可以直接輸出到 framebuffer 上, 或是利用 SDL 顯示。在這裡筆者要示範最原始的方法 – 輸出到檔案裡。
當我們利用 device_get_address() 取得 frame 的影像資料後, 再將 frame 的影像資料輸出成 PPM 格式的檔案。
程式碼如下:
FILE *fp;
fp = fopen("test.ppm", "w");
fprintf(fp, "P6\n%d %d\n255\n", NTSC_WIDTH, NTSC_HEIGHT);
fwrite(img, NTSC_WIDTH, 3*NTSC_HEIGHT, fp);
fclose(fp);
先利用 fprintf() 寫入 PPM 檔案的檔頭資訊, 然後以 fwrite() 將傳回的影像資料寫到檔案裡。
img 指向記憶體裡的 frame 影像資料, 寫入時, 請特別注意粗體字的地方, 因為我們是用 RGB32 的調色盤, 而 RGB 是以 3 個 sample 來表示一個 pixel, 所以要乘上 3。
如果是 GREY 調色盤, 就不用再乘 3 了。最後將輸出的 PPM 檔案轉換格式成 TIFF 就可以用一盤的繪圖軟體打開了:
linux$ ppm2tiff test.ppm test.tiff
將影像存成 JPEG 的方法
最後我們再完成一個功能, 就可以實作出一個完整的 Webcam 軟體。之前我們將影像存成 PPM 格式的圖檔, 不過因為檔案過太, 會造成傳輸的不便。因此, 我們勢必要將影像資料存成更小的檔案才具實用性。
JPEG 或MJPEG 都是在本文第 1 篇介紹過的格式。以 JPEG 來存放圖檔, 相當容易可以實作出 Webcam 的功能, 但缺點就是無法傳送聲音資料。
我們使用 mpeglib 來完成這項任務, mpeglib 可至 www.ijg.org 下載。
將影像資料存成 JPEG 的方法在「各大」與 video streaming 有關的軟體 (例如: xawtv) 都可以看得到範例。不過因此這部份已脫離 v4l 的主, 所以筆者只列出底下的 write_jpeg() 完整函數, 供讀者使用:
int write_jpeg(char *filename, unsigned char * img, int width, int height, int quality, int gray)
{
struct jpeg_compress_struct jcfg;
struct jpeg_error_mgr jerr;
FILE *fp;
unsigned char *line;
int line_length;
int i;
if ((fp = fopen(filename,"w")) == NULL) {
fprintf(stderr,"write_jpeg: can't open %s: %s\n", filename, strerror(errno));
return -1;
}
jcfg.image_width = width;
jcfg.image_height = height;
jcfg.input_components = gray ? 1: 3; // 3 sample per pixel (RGB)
jcfg.in_color_space = gray ? JCS_GRAYSCALE: JCS_RGB;
jcfg.err = jpeg_std_error(&jerr);
jpeg_create_compress(&jcfg);
jpeg_stdio_dest(&jcfg, fp);
jpeg_set_defaults(&jcfg);
jpeg_set_quality(&jcfg, quality, TRUE);
jpeg_start_compress(&jcfg, TRUE);
line_length = gray ? width : width * 3;
for (i = 0, line = img; i < height; i++, line += line_length)
jpeg_write_scanlines(&jcfg, &line, 1);
jpeg_finish_compress(&jcfg);
jpeg_destroy_compress(&jcfg);
fclose(fp);
return 0;
}
利用 mpeglib 寫入 JPEG 影像資料時, 必須分別對每行 scanline 寫入。呼叫範例:
write_jpeg("test01.jpg", img, NTSC_WIDTH, NTSC_HEIGHT, 50, FALSE );
第一個參數是圖檔名稱, 第二個參數是影像資料, 然後第三、第四個參數接著影像的大小, 第五個參數 50 表示 JPEG 圖檔的壓縮品質 (quality), 最後一個參數 FALSE 表示影像資料不是 grey (灰階) 影像。
灰階影像與彩色影像的差別在於 input_components、in_color_space 與 scanline 的長度。
結語
在 一連串的 Video Streaming 主題裡, 我們學到 video4linux 擷取影像的方式, 以 mmap (flip-flop) 來連續擷取影像, 並做到 VOD 的功能是我們的最終目的。到這裡為止, 我們已經有能力實作出簡單的 Webcam 軟體, 類似這種取固定間隔傳送影像的方式應用也很廣, 例如路口交通狀況回報。
利用到這裡所學的方法, 將擷取的影像存成 JPEG, 然後放到 Web 上, 固定一段時間更新, 我們也可以設計一套簡單的路口交通狀況回報系統, 或是家裡的監視系統。
後面接著的主題, 將會以現有的程式為基礎, 實作真正具有 VOD 能力的軟體。
Linux 的應用(刊載於 PC2000 雜誌十二月號)-- Video Streaming 探討 (6)
jollen 發表於 October 18, 2001 12:06 PM
本期將是我們一系列 Video Streaming 專欄的結束,本期最重要的工作當然免不了是對之前的內容做系統性的整理,然後再對 Video Streaming 的應用就目前最被討論的理論做簡單介紹。
作者: 陳俊宏
www.jollen.org
Video Streaming 系統概觀
一個完整的 Video Streaming 系統應該包含四個部份:
- Content (例如影像、聲音、coded 資料)
- Server
- Client
- Data network (例如 Internet,或任何連結 server-client 的媒介)
那 麼我們在這一系列 Video Streaming 主題介紹的有那些呢?從一開始的名詞介紹、影像擷取、RTP 通訊協定,我們將重心放在 contente 與 data network 上。當然我們系列主題名為「Linux 的應用」,最關鍵的地方則是在如何在 Linux 環境下設計影像擷取程式。
對於 data network 的部份因為與 Linux 較無關,但也是 Video Streaming 的重點主題之一,所以也介紹了 jrtplib 這套程式庫給大家,在底下的部份也會補充 Nsync 這套 toolkit。
而 server 端似乎是我們著墨不深的地方,但也沒關係,在這最後一篇的文章裡,我們針對理論方面做補充的介紹,我們將以 Video Conferencing 實作時,實務上會面臨的主題來做解,包括:多媒體資料的儲存、OS 的磁碟排程 (SCAN-EDF),以做為您搜尋參考資料的起頭。
在 client 方面,我們在最後的完整版範例程式時,示範了如何利用 SDL 將擷取下來的影像資料秀在螢幕上。這裡也會配合了上一篇所介紹的 mmap 擷取方式,以將影像資料填入 SDL 的 display 結構方式秀圖。
Video Streaming 的開始
我們一開始就介紹了 Video Streaming 是一種經由網路來撥放影音檔案的技術,Video "Streaming" 的基本概念為「一邊下載一邊撥放」,我們稱之為「Play as received」。
經由 Internet 如果要收看遠端伺服器的電影檔案,最原始的做為是「下載後再撥放」,也就是經由 FTP 或 HTTP 將整個檔案下載至本地端後再利用撥放程式來撥放,我們稱之為「Play after download」。
舉個最簡單的情況,你可能在下載 MP3 之前想要試聽一下音樂的內容,而下載音樂片段的方式又顯得不夠友善,這時如果利用 Video Streaming 的技術來讓網友視聽,不但方便,而且不必浪費時間來下載不喜歡的音樂的。
除此之外,大家最熟悉的莫過於在線上即時播放影片的 real player了。
Real Video 產品介紹
Real Video 是 Real Networks 公司的產品,Real Video 主要支援了 video-on-demand*1 的功能。Real Video 可以讓我們經由網站來播放串流影像 (streaming video)。
由於我們的最終目的是實作出一個可以做 video streaming 的軟體,所以在這裡我們將以 Real Video 做為標竿,並以 Linux 為基礎來設計 video streaming 的軟體。
其它經典的 streaming 範例程式
網路上有幾套很值得玩味研究的相關開放原始碼軟體,我們也曾經提過。VIC 和 VideoLAN 則是其中絕對優秀的教學範例。
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 (杜比音效)。
影像編碼技術介紹 (coded)
目前學術界已經發展出許多處理影像訊號壓縮及編碼的技術 (codecs),談到這些技術,應用最廣泛的編碼標準底下四種:
- H.261
- H.263
- JPEG, MJPEG
- MPEG
本文第一篇文章就是在對這四種編碼技術做簡單的介紹。
data network 的技術主題
Video 在做 Streaming 時,有三種方式可以應用:broadcasting、unicasting、multicasting。
broadcasting 的方式比較單純,他是在 LAN 上直接將一個個的影像封包丟到網路上 (server 端),再由 client 的應用程式自網路上取回封包播放。但網路硬體層上,仍有許多需要考慮的問題,例如在 Shared Non-Switched Enthernet 上時,就會發生一些小問題。
unicasting 與 multicasting 都是屬於 IP 的傳輸方式。unicasting 採取 1 對 1 的方向傳影像給遠端,稱為 Video-on-Demand (VoD),multicasting 則是 1 對多的傳輸方式,稱為 Near-Video-on-Demand (NVoD)。未來 IPv6 將支援 IP Multicasting,因此 Video Streaming 的應用將更為廣泛。
在通訊協定方面,我們也介紹了 RTP。
RTP 全名為 Real-Time Protocol,RTP 是在 UDP 封包之前多加 10 bytes 的檔頭,裡面記載有時間、序號、壓縮型態等資訊。RTP 是目前大多數 Video Streaming 軟體所使用的通訊協定。
RTP 可用來針對各種不同的多媒體格式做 Streaming 的工作,因為我們將影像分解成數個 RTP 封包再傳送出去,因此會遇到許多網路技術常會遇到的問題。例如,因為封包送達的時間不一,造成播放時會畫面不流暢的現像,因此,在播發時就必須使用一個緩 衝區 (playout buffer) 來暫時存放並處理網路上接受到的封包。
由網路上接收的影像封包因為彼此之間到達的時間間隔不同 (Synchronous Data Packets),所以必須利用緩衝區將這些封包做緩衝,讓彼此之間的時間間隔一樣 (Isochronous Data Packets)。
其它重要的通訊協定像是 SIP、或是 FEC (forward error correction) 除錯技術,都是一定要去研究的主題。
影像擷取卡
在 Linux 下設計影像擷取程式,當然一定要配備有適合的影像擷取卡。
我 們曾經介紹給大家的是 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 相關模組就是如此。
Linux 上可用的影像擷取卡
在 http://www.linhardware.com/db/searchproduct.cgi?_catid=17 網頁上可以找到在 Linux 上支援程度比較好的幾張影像擷取卡。而一般 Linux 上較受歡迎的影像擷取卡則是 Hauppauge 的幾張卡, 筆者使用的也是 Hauppauge 的卡。
在 linhardware 網站上可以找到底下六張卡:
- Hauppauge 401 WinTV-radio dbx-TV stereo
- Hauppauge WinTV PCI TV Card
- Hauppauge WinTV-GO PCI TV Card
- Hauppauge WinTV-PCI Hauppauge
- Hauppauge WinTV-Radio+NICAM
- Hauppauge WinTV/PCI TV Card
關於 Linux 對於影像擷取卡支援的中文文件 (HOWTO) 可以在 CLDP 網站上取得:
http://www.linux.org.tw/CLDP/Hardware-HOWTO-22.html
影像擷取卡支援的視訊系統
大部份影像擷取卡都會具備一組視訊輸入端子, 即 S-Video (Y/C) 端子或 Composite 端子。
在台灣的標準當然是 NTSC 系統, 一般而言, 我們是希望一張影像擷取卡可以支援越多視訊系統越好, 包括: NTSC/PAL/PALN/PLAM/SECAM。
可使用的視訊裝置有較常見的 CCD, 或是家用 V8、Hi8 皆可, 一般而言我們也是希望一張影像擷取卡可以接越多視訊裝置越好。
BT 878 晶片
跟隨在影像擷取卡之後的主題當然就是 BT878 晶片的介紹,因為支援 BT 8x8 晶片的 BTTV 軀動程式是我們設計影像擷取軟體的核力主力!
目 前大部份的數位影像擷取卡大部份都是以 BT878 單顆晶片為影像擷取卡之中心。BT878 運作方式是以軟體來進行影像解壓縮工作, BT878 晶片負責將擷取之影像丟給 Linux 做影像處理, 而 BTTV 則是 Linux kernel 的 BT878 晶片軀動程式。
由於影像是利用 BT878 擷取後交由軟體來做影像處理, 因此在處理效能上自然就會比較差。如果是經由網路來傳送影像的話, 我們就會再利用影像壓縮技術 (H.261/H.263...等等) 來做影像處理。
什麼是 BTTV
BTTV 是 Linux 上的 Bt848/849/878/879 晶片的軀動程式, 主要功能是做頁框的截取 (frame grabber)。
BTTV 是 video4linux 裡重要的軀動程式, 目前分為二個版本:
- 0.8.x 的發展中版本
- 0.7.x 的穩定版本
BTTV 相關應用軟體 - xawtv
官方網站: http://bytesex.org/xawtv/index.html
安裝方式:
linux# ./configure
linux# make depend
linux# make
linux# make install
如果您有 Red Hat Linux 7.1 PowerTools 光碟片的話, 也可以直接由 PowerTools 光碟片安裝 xawtv 套件:
linux# rpm -ivh xawtv-3.34-1.i386.rpm
安裝 xawtv 需要 libjpeg 與 libjpeg-devel 套件, 如果您是使用 Red Hat Linux 7.1 的話, 應該安裝底下二個套件:
- libjpeg-6b-15.i386.rpm (Disc 1)
- libjpeg-devel-6b-15.i386.rpm (Disc 2)
xawtv 整個架構可以分成 7 個部份如下:
- xawtv: 主程式部份。
- fbtv: linux console 模式的 TV 應用程式, 使用 linux kernel 2.2.x 的 framebuffer。
- set-tv: 命令列模式的工具, 用來設定 video4linux 的參數。
- streamer: 命令列模式的工具, 用來捉取動態影像與 avi 影像。
- radio: radio 應用程式。
- webcam: 將捉取的影像以 FTP 方式上傳到 Web Server 端, 用來設計 Web 即時影像的工具。
- alevtd: videotext pages 的 Web Server。
xawtv 的 video4linux
xawtv 是相當好的 video4linux 方面的教材,我們極力推薦讀者研究 xawtv 的 video4linux 部份的原始碼。
將取回 xawtv 的原始程式碼解開後, 在 libng/ 目錄下可以看到 grab-v4l.c 的檔案, 另外還有一個 grab-v4l2.c 的檔案, 這是 video4linux2 (version 2) 的版本。
在 xawtv 的 video4linux 主題現身之前,我們很詳盡介紹了 video4linux 的基本設計方法,接下來在 xawtv 之後更是再進一步說明了 video4linux 的經典 – mmap 擷取技巧。
那麼跨越三期內容的程式碼那一個才是完整的呢?事實上都沒有,不過請讀者們放心,本期我們將列出所有我們曾經介紹過的主題所實作的程式,當然是完整的實作程式碼!
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。
Video Streaming 的其它關鍵議題
接下來的主題將介紹 Video Streaming 其它值得研究的主題,我們會在最後才提出來的原因是因為這些主題將不會影響我們之前的程式實作,但在設計完整的 Video Streaming 系統時,則是有必要加以考慮的。
Video Conferencing 應用的重要性
Video Conferencing 在多媒體設計上之所以重要,最重要的原因是因為 Video Conferencing 渉及的技術議題包括:
- 即時性問題 (real-time systems problem)
- 互動式應用程式的 latency 與 throughput 問題
這樣的問題當然首先是發生在網路頻寬的問題上,由於網路視訊會議系統耗費大量的頻寬,而且網路視訊會議的品質也容易受網路品質與頻寬影響,因此這是值得我們研究的問題之一。
Video Conferencing 另外一個迷人的地方是在於 Video Conferencing 提供良好的 person-to-person 環境。Video Conferencing 應用軟體在多媒體程式設計上,常常也被視為「殺手級」的應用之一,可見 Video Conferencing 應用的重要性。
Video Conferencing 的應用領域
Video Conferencing 目前的應用領域則是有:遠距教學 (distance learning systems)、遠端診視系統 (remote consultation systems)、遊戲…等等。
就如同我們先前所講的,解決網路、傳輸問題變成是下一代通訊應用的的關鍵。在軟體的支援上也是如此,當然這其中有許多的解決方案是設計新的多媒體作業系統 (Multimedia Operating Systems) 來解決。
因為我們要實現 VOD 的技術,因此將會涉及網路的主題,所以要考慮的層面也會比較多。除了網路相關問題外,也會在底下一併討論其它幾個主要的大問題。
Video Streaming 的傳送問題
Video Conferencing 所遭遇到的第一個問題是如何遞送 (deliver) 影像串流 (video streams),這其中又要考慮到串流的管理、與網際網路即時性 (real-time over the Internet) 問題。
Video Conferencing 重要的關鍵之一是在於如何有效縮短 latency。所以我們也必須尋找一個有效的方便,來適應各種不同網路頻寬的環境。
Video Streaming 的資料儲存問題
Video Streaming 的應用還要考慮的問題則是儲存設備 (storage) 的選擇。Video Streaming 的應用必須要有良好的儲存環境,來儲存各種型態的多媒體資料,包含:文字、影像、聲音、圖片等等,每種資料的特性都不相同。
檔案系統 (filesystem) 對於多媒體物件的管理也是很重要的因素之一,必須要有一個可以快速存取並且有效管理多媒體物件的檔案系統,才能滿足效能的需求。
作業系統的磁碟儲存
在 現階段 Video Streaming 以至於多媒體應用程式的設計上,對於 OS 支援的磁碟排程 (Disk Scheduler) 也被列入我們考慮研究的項目之一。傳統上,一般我們設計 OS 時都會選擇 SCAN 或是 SSTF 演算法,不過這些傳統的磁碟排程演算法並無法滿足我們的需求。
較先進的磁碟排程演算法應考慮到 Video 與 Audio 的應用,而目前較普遍被選擇用來設計 multimedia I/O 系統的磁碟排程演算法則是 SCAN-EDF 演算法。
SCAN-EDF 演算是結合 SCAN 與 EDF 優點的解決方案,SCAN 是眾所皆知的 seek optimizing 磁碟排程演算法;而 EDF (Earliest Deadline First) 則是屬於 real-time scheduling 的演算法。
磁碟排程對於 Video Streaming 應用的影響
引進 SCAN-EDF 磁碟排程演算法的重要之處在於我們必須要能支援 real-time request,其影響的範圍包括:
- Maximum allowable streams
- Reponse time
SCAN-EDF 已被分析並證實可以改善以上的效能,那麼,對於目前廣受歡迎的 Linux 而言,由於 SCAN-EDF 已經早就在實作應用的範圍內了,我們可以將 SCAN-EDF 磁碟排程演算法加到 Linux kernel 裡。如此一來,Linux 在 Video Streaming 的應用上也算是重要的效能改良。
Video Conferencing 的 Synchronization 問題
當我們進行多方 (n-way) 視訊會議時,程式總不能讓每個人所看到的影像畫面都不相同吧!就算無法真正做到每個人的畫面同一時間都相同,但至少也要控制在合理可接受的範圍之內。
Synchronization (同步) 問題的研究主要是在建立互動式 (interactive) 的多媒體應用程式上,同步問題的解決是需要相當多的時間與精神的,好在目前有重量級的 toolkit 供我們使用,那就是 Nsync (in-sync)。
Nsync 共包含二大部份:
- Synchronization definition language
- Run-time presentation management system
當然我們的 Video Conferencing 應用程式當然也需要 Nsync 的幫忙!
程式補充包
底下我們將補充二個簡單的函數,供讀者使用,這二個函數與我們的範例程式並沒有直接關係,但在測試時可能會有機會使用到:
- jpeg.c:將擷取的影像資料利用 libjpeg 存成 JPEG 圖檔。
- ppm.c:將擷取的影像直接寫成 PPM 圖檔。
存成 JPEG 圖檔部份
檔案:jpeg.h
/*
* JoTV - Video Streaming Systems
* (c) 2001 Jollen <jollen@o3.net>
*/
#ifndef _JPEG_H_
#define _JPEG_H_
int write_jpeg(char *filename, IMG *img, int width, int height,
int quality, int gray);
#endif
檔案:jpeg.c
/*
* JoTV - Video Streaming Systems
*
* jpeg.c - output image to the jpeg files
* (c) 2001 Jollen <jollen@o3.net>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
#include <jpeglib.h>
#include <jerror.h>
#include <jconfig.h>
#include <pthread.h>
#include <sys/param.h>
#include <sys/types.h>
#include "JoTV.h"
int write_jpeg(char *filename, IMG *img, int width, int height,
int quality, int gray)
{
struct jpeg_compress_struct jcfg;
struct jpeg_error_mgr jerr;
FILE *fp;
unsigned char *line;
int line_length;
int i;
if ((fp = fopen(filename,"wb")) == NULL) {
fprintf(stderr,"write_jpeg: can't open %s: %s\n", filename, strerror(errno));
return -1;
}
jcfg.err = jpeg_std_error(&jerr);
jpeg_create_compress(&jcfg);
jcfg.image_width = width;
jcfg.image_height = height;
jcfg.input_components = gray ? 1: 3; // 3 sample per pixel (RGB)
jcfg.in_color_space = gray ? JCS_GRAYSCALE: JCS_RGB;
jpeg_set_defaults(&jcfg); // problem ...
jpeg_stdio_dest(&jcfg, fp);
jpeg_set_quality(&jcfg, quality, TRUE);
jpeg_start_compress(&jcfg, TRUE);
line_length = gray ? width : width * 3;
line = (unsigned char *)img;
for (i = 0; i < height; i++, line += line_length)
jpeg_write_scanlines(&jcfg, &line, 1);
jpeg_finish_compress(&jcfg);
jpeg_destroy_compress(&jcfg);
fclose(fp);
return 0;
}
函數的呼叫方法如下,直接加到範例的主程式裡即可:
write_jpeg("JoTV.jpg", img, DEFAULT_WIDTH, DEFAULT_HEIGHT, 75, FALSE /*not grey*/);
中標=存成 PPM 圖檔部份
檔案:ppm.c
/*
* JoTV - Video Streaming Systems
*
* ppm.c - output image to the ppm file
* (c) 2001 Jollen <jollen@o3.net>
*/
int write_ppm(IMG *);
int write_ppm(IMG *img)
{
FILE *fp;
fp = fopen("test.ppm", "w");
fprintf(fp, "P6\n%d %d\n255\n", NTSC_WIDTH, NTSC_HEIGHT);
fwrite(img, NTSC_HEIGHT, 3*NTSC_WIDTH, fp);
fclose(fp);
}
數的呼叫方法如下,直接加到範例的主程式裡即可:
write_ppm(img);
圖檔會存成檔名:test.ppm,由於大部份的繪圖軟體對於 TIFF 的支援較好,所以可以再利用 ppm2tiff 工具將 PPM 圖檔轉成 TIFF 格式:
$ ppm2tiff test.ppm test.tif
Video Streaming 的應用實例
經 過這一系列的 Linux Video Streaming 應用探討,我們已經可以有能力設計出許多簡單的 Video Streaming 應用程式。例如以 video4linux、bttv、rtp 和 jpeg 壓縮技術便能實作簡單的 Webcam 應用程式。
底下是我們利用 Java 實作出來的 Linux Webcam 應用程式,在遠端直接以瀏覽器來執行即可。
圖 1 利用 Java 與 Video Streaming 技術設計的 Linux Webcam
圖 2 利用 Java 與 Video Streaming 技術設計的 Linux Webcam
Webcam 與 CCD 結合的應用包括:遠距教學、視訊會議、安全監控…等等。
0 comments:
Post a Comment