2009年8月3日星期一

Linux的应用--Video Streaming探讨四

本期的重点在介绍撷取 frame 的方法, 并且将重心由 video4linux 转移到网络方面。在网络影像即时传送方面, 我们采用的 RTP 也是各大厂商使用的标准, 在这一期里, 我们将可以学习到利用 JRTPLIB
来加入网络功能的方法。
video4linux 撷取 frame 的方法
在上一期的 xawtv 里, 我们看到了 xawtv 的影像撷取功能,
其中对我们最重要的部份是利用 video4linux 做影像撷取的部份。只要可以写出
video4linux 的躯动部份, 要做影像撷取其实是很容易的, 我们利用的是 mmap
的方式来撷取影像。
mmap 撷取方式
为了说明如何以 mmap 方式来撷取影像, 我们不建议读者直接去研究 xawtv
关于这部份的程序码。研究过几个有关支援 mmap 影像撷取的软件原始码后,
我们建议读者去下载一支名为 EffecTV 的程序, 其官方网页为:
http://effectv.sourceforge.net/index.html
EffecTV 是日本人设计的程序, 也是经由 video4linux 做影像撷取, 在 mmap 的程序码方面, EffecTV 会比较容易懂, 同时也可以借由 EffecTV 来学习一些影像处理的技巧。EffecTV 是一个可以支援特效功能的视讯软件,
是颇有趣的程序。
主要函数介绍
EffecTV 里与影像撷取 (frame grab) 有关的函数为:
int video_grab_check(int palette);
int video_set_grabformat();
int video_grabstart();
int video_grabstop();
int video_syncframe();
int video_grabframe();
这些函数定义在 video.h 里。我们不再重覆介绍 video4linux 初始化的地方, 在
frame grab 方面, 呼叫 video_grabstart() 开始进行影像撷取的工作, 程序码如下:
/* Start the continuous grabbing */
int video_grabstart()
{
vd.frame = 0;
if(v4lgrabstart(&vd, 0) < 0)
return -1;
if(v4lgrabstart(&vd, 1) < 0)
return -1;
return 0;
}
其中主角是 v4lgrabstart() 函数, 这个函数被实作在 v4lutils/v4lutils.c 里,
程序码如下:
/*
* v4lgrabstart - activate mmap capturing
*
* vd: v4l device object
* frame: frame number for storing captured image
*/
int v4lgrabstart(v4ldevice *vd, int frame)
{
if(v4l_debug) fprintf(stderr, "v4lgrabstart: grab frame
%d.\n",frame);
if(vd->framestat[frame]) {
fprintf(stderr, "v4lgrabstart: frame %d is already used to
grab.\n", frame);
}
vd->mmap.frame = frame;
if(ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {
v4lperror("v4lgrabstart:VIDIOCMCAPTURE");
return -1;
}
vd->framestat[frame] = 1;
return 0;
}
v4lgrabstart() 是利用 mmap 的方式来取得影像。v4lgrabstart() 也是利用
ioctl() 来完成这个低阶的动作, 与第本文第二篇实作 video4linux 时一样,
写法为:
ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap));
vd 里的 framestat 栏位主要是纪录目前的 frame 状态:
vd->framestat[frame]
这个栏位定义在 v4lutils.h 里, 而利用 mmap 的方式我们需要两个 frame
来存放影像资料, 所以 framestat 宣告成二个元素的数组, 我们将 EffecTV 的 v4l
结构定义完整列出如下:
struct _v4ldevice
{
int fd;
struct video_capability capability;
struct video_channel channel[10];
struct video_picture picture;
struct video_clip clip;
struct video_window window;
struct video_capture capture;
struct video_buffer buffer;
struct video_mmap mmap;
struct video_mbuf mbuf;
struct video_unit unit;
unsigned char *map;
pthread_mutex_t mutex;
int frame;
int framestat[2];
int overlay;
};
请读者回头对应一下本文第二篇文章所实作的内容, EffecTV
的实作更为完整。当我们开始 grab 影像到其中一个 frame 时, 我们就把 frame
的状态设成 1:
vd->framestat[frame] = 1;
然后利用 v4lsync() 等待 frame 撷取完成, 利用 ioctl() 传入 VIDIOCSYNC
可以检查 frame 是否已经撷取完成:
if(ioctl(vd->fd, VIDIOCSYNC, &frame) < 0) {
v4lperror("v4lsync:VIDIOCSYNC");
return -1;
}
vd->framestat[frame] = 0;
return 0;
如果 frame 已撷取完成, 那么我们就将 frame 的状态设成 0, 表示目前这个 frame
并没有在做撷取的动作, 也因此在 v4lsync()
一开始的地方我们会先做这部份的检查:
if(vd->framestat[frame] == 0) {
fprintf(stderr, "v4lsync: grabbing to frame %d is not started.\n",
frame);
}
v4lsync() 函数也是一个重要的函数, 程序码如下:
/*
* v4lsync - wait until mmap capturing of the frame is finished
*
* vd: v4l device object
* frame: frame number
*/
int v4lsync(v4ldevice *vd, int frame)
{
if(v4l_debug) fprintf(stderr, "v4lsync: sync frame %d.\n",frame);
if(vd->framestat[frame] == 0) {
fprintf(stderr, "v4lsync: grabbing to frame %d is not
started.\n", frame);
}
if(ioctl(vd->fd, VIDIOCSYNC, &frame) < 0) {
v4lperror("v4lsync:VIDIOCSYNC");
return -1;
}
vd->framestat[frame] = 0;
return 0;
}
在 EffecTV 里则是要呼叫 video_syncframe() 函数来做 frame 等待的动作, 而
video_syncframe() 则会去呼叫 v4lsync() 函数。video_syncframe()
函数的原始码如下:
int video_syncframe()
{
return v4lsyncf(&vd);
}
frame 撷取实作
看过这几个核心函数后, 那么在 EffecTV 里会在那里用到这些函数呢? EffecTV
是一个输出特效画面的视讯软件, 在 effects/
目录下每个档案都是独立支援一种特效的,
因此我们介绍的这几个函数都是由每个特效独立来呼叫使用。让我们来看 Life
这个特效的主程序 ━ life.c, 首先应该先由 lifeStart() 函数看起, 其程序码如下:
int lifeStart()
{
screen_clear(0);
image_stretching_buffer_clear(0);
image_set_threshold_y(40);
field1 = field;
field2 = field + video_area;
clear_field();
if(video_grabstart())
return -1;
stat = 1;
return 0;
}
lifeStart() 在完成一些初始化的设定工作后, 会呼叫 video_grabstart()
函数开始进行影像撷取。在 lifeDraw() 函数里, 则是呼叫 video_syncframe()
等待 frame 撷取完成后再做输出的动作。
YUV 与 YIQ
在 PAL 视讯标准方面, 主要的模式为 YUV, 这与我们在计算机上常用的 RGB
不同。相对的, 在 NTSC 视讯标准, 则是使用 YIQ 模式。针对这二种视讯影像模式,
我们还必须设计 YUV 与 RGB、YIQ 与 RGB 的转换程序。在 EffecTV 里也有 yuv.c
的程序码负责做转换的工作。
RGB 介绍
RGB 以三原色红、绿、蓝 (Red-Green-Blue) 来表现影像,
将红色与蓝色重叠后会成为品红色 (magenta)、红色与绿色重成为黄色 (yellow),
三色重叠则是白色 (white)。RGB 的三原色指的是光线的颜色, 并非颜料的颜色,
RGB 模式常使用于监视器上, 与 PAL 或 NTSC 视讯的标准不同。
YUV 与 YIQ 的转换
YUV 、 YIQ 与 RGB 之间的换系与转换公式如下:
Y = 0.299R + 0.587G + 0.114B
U = B ━ Y
V = R ━ Y
I = 0.877(R-Y)cos33 ━ 0.492(B-Y)sin33
Q = 0.877(R-Y)sin33 + 0.492(B-Y)cos33
RGB 是由 R, G, B 三原色组成, 同理 YUV 是由 Y, U, V 三个元素组成。在 PAL
实作 U, V 我们使用的转换公式为:
U = 0.492(B-Y)
V = 0.877(R-Y)
YIQ 则可以简化成转换矩阵:
(手稿)
JRTPLIB 的使用方法
在 Video Streaming 方面, 有了影像撷取的程序实作能力后,
要完成完整的影像串流软件, 例如视讯会议软件,
当然就必须要加入网络传送的功能。
为了能利用网络传送影像, 并且做到 real-time (即时) 的功能,
我们必须使用RTP通讯协定来完成。在这里我们已经完成第一大部份的工作了,
接下来就是加入网络即时传送拨放的功能, 到这里 video4linux已告一段落,我们将
Video Streaming 的主角换到 RTP 继续讨论。
加入 RTP Protocol
利用 Video Streaming 来设计视讯会议软件,
其中在技术层面不可或缺的一部份就是 RTP Protocol 的部份。RTP 也是 VoIP
(Voice over IP) 相关技术所使用的通讯协定。
为了配合 Video Streaming 来设计完整的视讯会议软件, 我们势必要加入 RTP
的技术。在这方面, 我们选择使用现成的 RTP 程序库 ━ JRTPLIB。
与 video4linux 程序库不同的是, video4linux 在决策上我们选择自行发展,
但JRTPLIB则是一个很成熟的专案了, 而且仍在持续维护, 未来 JRTPLIB 还会加入
IPV6 与 multicasting方面的完整支援,因此使用 JRTPLIB 来发展我们的 Video
Streaming 软件才是解决之道。
JRTPLIB 简介
RTP 全名为 Real-time Transport Protocol, 定义于 RFC 1889 与 RFC 1890,
我们在第一篇文章里已经简单介绍过 RTP。在 RFC 1889 里, 对 RTP 的定义为:
RTP: A Transport Protocol for Real-Time Applications
而在 RFC 1890 里, 对 RTP 的描述则是:
RTP Profile for Audio and Video Conferences with Minimal Control
即然我们要利用 Video Streaming 来设计视讯会议方面的软件, 对于
RTP的讨论与研究则是必修功课之一。对视讯会议软件而言,RTP 也提供 Audio
部份的支援, 事实上, 任何与 real-time (即时)相关的话题都与 RTP 脱不了关系。
与 JRTPLIB 相关的计画包括 JVOIPLIB 与 JTHREAD,
这两个程序库对我们的工作是相关有帮助的, 本文则先将重心放在
JRTPLIB上面。JRTPLIB 实作了 RTP 协定, 并且提供了简单易用的 API
供软件开发使用。JRTPLIB 也支援了 session, 并且可在底下平台执行:
MS-Windows 95,98 and NT
Linux
FreeBSD
HP-UX
Solaris
VxWorks
JRTPLIB 的官方首页为:
http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html
使用前请务必先阅读一下 JRTPLIB 的版权宣告。
JRTPLIB 的第一个程序
底下我们介绍过 JRTPLIB 的设计方法后, 大家就会发觉到 JRTPLIB
实在很好上手。要利用 RTP 通讯协定传送资料, 第一步要先建立一个 session,
方法如下:
#include "rtpsession.h"
int main(void)
{
RTPSession sess;
sess.Create(5000);
return 0;
}
第一步我们要先把 rtpsession.h 给 include 进来:
#include "rtpsession.h"
接下来再产生 RTPSession 类别的物件:
RTPSession sess;
最后再建立 session 就完成最简单的初始动作了:
sess.Create(5000);
Create() 成员函数接收一个 portbase 的参数, 指定 session 的 port,
接着开始初始化 timestamp 与 packet sequence number。RTPSession::Create()
程序码如下:
int RTPSession::Create(int localportbase,unsigned long localip)
{
int status;
if (initialized)
return ERR_RTP_SESSIONALREADYINITIALIZED;
if ((status = rtpconn.Create(localportbase,localip)) < 0)
return status;
if ((status = contribsources.CreateLocalCNAME()) < 0)
{
rtpconn.Destroy();
return status;
}
CreateNewSSRC();
localinf.CreateSessionParameters();
rtcpmodule.Initialize();
initialized = true;
return 0;
}
Create() 接着会再建立一个 SSRC:
CreateNewSSRC();
SSRC 为 local synchronization source identifier。
指定目的端
接下来再指定目的端的 IP 位址:
unsigned long addr = ntohl(inet_addr("127.0.0.1"));
sess.AddDestination(addr,5000);
这里表示我们要将封包传送到 127.0.0.1 (本地端) 的 port 5000,
只要照着套用即可。
传送 RTP 封包
sess.SendPacket("1234567890",10,0,false,10);
1234567890 是要传送的字串, 第二个参数 (10) 表示传送字串的长度,
第三个参数为 payload type, 第四个参数为 mark flag, 最后第五个参数则是
timestamp 的递增单位。在下一期里, 我们将会配合 SDL 来做影像的输出,
因此我们会在下一期再介绍如何接收 RTP 封包。我们使用 SDL
主要目的是为了将影像输出到屏幕上。
RTP 的封包格式
RTP 的标准受许多大厂采用, 例如: Microsoft、Intel, 也因此我们需要了解一下
RTP 的低层技术部份。RTP 与其它 Internet 通讯协定一样,
在封包里也会有封包档头, 接着才是封包的资料。图 1 是 RTP 的封包档头格式, 整个档头分为 10 个栏位 (field)。
在 RTPsession::SendPacket() 的第三个参数与第四个参数分别是 payload type 与
mark flag, 在 RTP 封包档头里, 分别是 Payload 栏位与 M 栏位。Payload
栏位的长度为 7 bits, M 栏位的长度为 1 bits。
RTP 的 Payload type
RTP 档头的 Payload type 指定封包资料的编码方式, 我们列出五个常用的 Audio
标准, 与三个常用的 Video标准,其中 JPEG/H.261/H.263
我们本文第一篇里都有做过简单的介绍。常用的 Payload type 如下表:
Payload
type 编码标准 支援Audio或Video Clock Rate (Hz)
2 G.721 A 8000
4 G.723 A 8000
7 LPC A 8000
9 G.722 A 8000
15 G.728 A 8000
26 JPEG V 90000
31 H.261 V 90000
34 H.263 V 90000
Linux 的 IP Stacks
提要网络的应用, 当然也要对 Linux IP Stacks 有简单的认识,
我们建议大家直接去研究 Linux kernel 的程序码,
当然现在已经有专门的书在做讨论:
Linux IP Stacks Commentary, Stephen T. Satchell & H.B.J. Clifford,
CoriolisOpen Press, ISBN 1-57610-470-2
Linux 是网络操作系统, 而且 Linux 对于网络的支援也相当的完整, 包括2.4 系列
kernel 已经加入对 IPv6。 Linux kernel 与 module 提供的通讯层功能包括:
各种 Ethernet、token ring 与 FDDI (Fiber Distributed Data Interface)
界面卡的躯动程序
PPP、SLIP 与 SLIP 通讯协定的躯动程序
提供 IPX (Internet Package Exchange) 通讯协定
提供工余无线电用的躯动程序 (AX25)
提供 AppleTalk 躯动程序
其它链接层使用的躯动程序
支援 router 的功能,包括 RIP (Router Information Protocol) 通讯协定
支援 ICMP (Internet Control Message Protocol) 通讯协定
支援 IGMP (Internet Group Message Protocol) 通讯协定
支援 IP (Internet Protocol) 通讯协定
支援 TCP (Transmission Control Protocol) 通讯协定
支援 UDP (User Datagram Protocol) 通讯协定
Linux IP Stacks 一书的书点放在基本与重要的 TCP/IP 服务上,
包含:绕送、封包管理、datagram 与 datastream。
Linux IP Stacks 导读
里的导读参考自 Linux IP Stacks 一书的第一章, 在研究 Linux IP Stacks
这本书前, 请读者先好好研究一下这本书的结构, 到时才比较容易上手。
第二章的部份介绍 TCP/IP 的背景知识与历史,包括 TCP/IP 的发展过程,
这一章原则上只要了解一下即可。
第三章则以学术的观点来比较 TCP/IP 与 ISO 模型。这一章比较偏向 ISO
的理论,而书上解释到, 要学习 ISO 模型理论的目的,
是为了能够了解为什么通讯层要分割成这几层。

没有评论: