Linux 的應用(刊載於 PC2000 雜誌九月號)-- Video Streaming 探討 (4)
本期的重點在介紹擷取 frame 的方法, 並且將重心由 video4linux 轉移到網路方面。在網路影像即時傳送方面, 我們採用的 RTP 也是各大廠商使用的標準, 在這一期裡, 我們將可以學習到利用 JRTPLIB 來加入網路功能的方法。
作者: 陳俊宏
www.jollen.org
video4linux 擷取 frame 的方法
在上一期的 xawtv 裡, 我們看到了 xawtv 的影像擷取功能, 其中對我們最重要的部份是利用 video4linux 做影像擷取的部份。
只要可以寫出 video4linux 的軀動部份, 要做影像擷取其實是很容易的, 我們利用的是 mmap 的方式來擷取影像。
mmap 擷取方式
為了說明如何以 mmap 方式來擷取影像, 我們不建議讀者直接去研究 xawtv 關於這部份的程式碼。研究過幾個有關支援 mmap 影像擷取的軟體原始碼後, 我們建議讀者去下載一支名為 EffecTV 的程式, 其官方網頁為:
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 的官方首頁為:
使用前請務必先閱讀一下 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 模型理論的目的, 是為了能夠了解為什麼通訊層要分割成這幾層。