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 模型理论的目的,
是为了能够了解为什么通讯层要分割成这几层。

Linux的应用--Video Streaming探讨三

本期的重点在介绍影像撷取卡与影像撷取卡常见的 BT848/BT878
芯片。除了介绍影像撷取卡外, 也会来看 xawtv 这个有名的 video4linux/BTTV
应用程式。
什么是影像撷取卡
影像撷取卡的主要功能当然就是做影像的撷取了,
一张影像撷取卡应该拥有底下的基本功能。
影像撷取
影像撷取卡可撷取的影像页框 (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 则是 Linuxkernel 的
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 的输出结果
这是 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: 调整视讯的对比、亮度、颜色。
设定 TV Norm
点取 TV Norm 项目后, 将 TV Norm 设定在正确的参数, 笔者使用的是 NTSC 的
CCD, 因此必须将这个项目设成 NTSC。
设定 Video Source
点取 Video Source 后, 再选取正确的视讯来源, 大部份的 CCD 都是 Compoiste
端子, 因此选择 Composite1。
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 档案。

Linux的应用--Video Streaming探讨二

继上一篇介绍过 Video Streaming的影像标准与网络通讯协定后,本期将要实际介绍目前常见的 Video Streaming 产品,并且由基本构成开始讲解。本期首先介绍 video4linux的设计方式。
Video Streaming 产品介绍
目前在网络上流行的 Video Streaming 产品相当多,这些利用 Video Streaming技术设计的软件在网络多媒体的应用已经有相当长的一段时间了。底下先来介绍几套常用的 Video Streaming 软件。
Read Video
Real Video 是 Real Networks 公司的产品,Real Video 主要支援了video-on-demand*1 的功能。Real Video 可以让我们经由网站来播放串流影像 (streamingvideo)。
由于我们的最终目的是实作出一个可以做 video streaming的软件,所以在这里我们将以 Real Video 做为标竿,并以 Linux为基础来设计 video streaming 的软件。
mod_mp3
mod_mp3 是 Open Source 的 streaming 软件。mod_mp3 并不是 Video Streaming 的软件,但同样是利用 streaming 的技术所设计的 apache module。
mod_mp3 可以利用 apache 来架设 streaming server,主要的功能是将 MP3放进 cache 里,再利用拨放程序就可以经由网络享受 MP3 streaming的服务。
mod_mp3 的架设相当简单,将 mod_mp3 以 DSO 方式安装后,只要在httpd.conf 里加上 VirtualHost 的设定即可:
Listen 7000
<VirtualHost www.jollen.org:7000>;
ServerName www.jollen.org
MP3Engine On
MP3CastName "jollen box"
MP3Genre "Much, nutty"
MP3 /home/nfs/private/mp3
MP3Random On
Timeout 600
ErrorLog /var/log/mp3_stream.log
</VirtualHost>
其中的设定项目说明如下:
MP3 - MP3 路径或档名
MP3Engine - 启动或关闭 MP3 streaming server
MP3CastName - server name
MP3Genre - Genre that will be sent to the client
MP3Playlist - 如果 MP3 Player 支援 Playlist,可以设定这个项目
MP3Cache - cache 目录
VIC
VIC 也是属于 Open Source 的软件。VIC 全名为video conferencing,故名其义,VIC是一种视讯会议的软件。VIC 是由加州柏克来大学的 Network Research Group所发展。
VIC 是相当棒非常适合用来研究 Video Streaming 的 Open Source软件,主要是因为 VIC 几乎包含了 Video Streaming 相关的技术。VIC 值得我们研究的原因是因为 VIC 支援了底下所列的功能:
IPv6
使用 video4linux 的捕像捕捉功能
H261、H263 与 H263+ codec
Software JPEG 与 BVC 编码
Raw YUV packetiser/codec
RTIP/RTP 通讯协定
the IP Multicast Backbone (MBone)
支援 video4linux 的 mmap
这些特色几乎已经包括 Video Streaming所应具备的技术了,基于这些特点,VIC的原始程序码相当吸引人,因此有意研究 Video Streaming 的 programmer应该好好阅读一下 VIC的原始程序码。
VideoLAN
VideoLAN 是一个可以做 MPEG 与 DVD 扩播 (broadcast) 播放的软件,VideoLAN分成二个部份,一个是 VLAN server,另一个则是 vlc 用户端播放程序。
VLAN server 将 DVD 与 MPEG 影像利用 broadcast 方式扩播到区域网络上,使用者端再利用 vlc 接收封包并播放。这样做的好处是可以减少重覆的 I/O 动作,VLAN server 将影像扩播出去后,区域网络上的用户端再利用vlc 接收封包并播放。
VideoLAN 支援 X11、SDL、Linux framebuffer、GGI、BeOS API、MacOS X API 播放方式,并且支援 DVD 与 AC3 (杜比音效)。
video4linux 实作
看过几套现成的 Video Streaming 后,还是要回到本文的主题 -- Linux 如何设计 VideoStreaming 的应用程式。上一期所介绍的 Video Streaming基本观念是进入 Video Streaming领域相当重要而且基本的知识,像是PASL/NTSC、RTP...等等。
RealNetworks 公司的产品里,要建置网站的即时 (live) 影像是相当容易的。只要利用 RealNetworks 公司的产品配合影像捕捉卡(Video Capture Card) 与 CCD 就可以达到。
从这里可以看出,如果我们想要实作一套这样的小系统,第一个所要面临的问题就是如何在Linux 下躯动影像捕捉卡,再来就是如何设计影像捕捉的程序。
在影像捕捉卡方面,Linux kernel 2.2 版本的支援已经相当完备了,很多影像捕捉卡在 Linux kernel 2.2 上都可以顺利躯动并且正常工作。
而在程序设计方面,我们则是先利用 Linux kernel 所提供的 video4linuxAPIs 来设计程序。这一期的目的在于利用 video4linux来实作一个供应用程式使用的程序库 (library)。
影像捕捉卡
先来检视一下 Osprey 100 这张影像捕捉卡。Osprey 100 是 Real Networks公司所推荐配合他们产品的一张影像捕捉卡,配合 Osprey 100 与RealNetworks 的产品我们可以利用broadcast 或 on-demand 做到实况转播 (live)的功能。
Osprey 100 在硬件功能上可以支援到每秒 30 个画面 (fps -- frame per second),并且支援 NTSC 与 PAL 输入。
不过在实作上,笔者并不使用 Osprey 100。笔者使用的影像捕捉卡是,这张卡算是比较「俗」一点的卡,但是也有好处,因为在 Linux 上很容易安装。
在继续往下发展我们的系统前,必须先安装好影像捕捉卡与躯动程序,这部份不在这篇文章的范围,所以请您参考相关的文章来安装躯动程序。
以笔者这张卡为例,使用的是 Brooktree Corporation 的卡,所以只要安装 bttv 模组即可,同时,bttv模组在 Linux kernel 2.2.17 下也会用到 i2c-old 与 videodev 两个模组,所以也要一并安装。在命令列下,安装这三个模组的命令为:
linux# insmod i2c-old
linux# insmod videodev
linux# insmod bttv
当然要确定 Linux kernel有编译这三个模组的支援,然后再把这三个模组加到 /etc/modules.conf(Red Hat 7.0) 里。
不同版本的 kernel 所要安装的模组不一定相同!还请注意,例如 i2c相关模组就是如此。
video4linux 使用的设备档
Linux 下与 video4linux 相关的设备档与其用途:
/dev/video Video Capture Interface
/dev/radio AM/FM Radio Devices
/dev/vtx Teletext Interface Chips
/dev/vbi Raw VBI Data (Intercast/teletext)
video4linux 除了提供 programmer 与影像捕捉有关的 API 外,也支援其它像是收音机装置。
接下来介绍 video4linux 设计方式,所使用的 Linux kernel 版本为 2.2.16。这篇文章将简单介绍实作video4linux 的方法,所以请准备好 Linux kernel 原始码下的Documentation/v4l/API.html 文件并了解 What's video4linux。
_v4l_struct -- 定义资料结构
首先,先定义会用到的资料结构如下:
#ifndef _V4L_H_
#define _V4L_H_
实作 video4linux 时,必须 include 底下二个档案:
#include <sys/types.h>
#include <linux/videodev.h>
接下来是 PAL、CIF、NTSC 规格的画面大小定义:
#define PAL_WIDTH 768
#define PAL_HEIGHT 576
#define CIF_WIDTH 352
#define CIF_HEIGHT 288
#define NTSC_WIDTH 640
#define NTSC_HEIGHT 480
接下来我们的重点是 _v4l_struct structure,这个 structure 包含了在API.html 提到,将会使用到的 data structure,底下将完整地定义 _v4l_struct,但在实作时并不会全部用到。 _v4l_struct 定义如下:
struct _v4l_struct
{
int fd;
struct video_capability capability;
struct video_buffer buffer;
struct video_window window;
struct video_channel channel[8];
struct video_picture picture;
struct video_tuner tuner;
struct video_audio audio[8];
struct video_mmap mmap;
struct video_mbuf mbuf;
unsigned char *map;
};
为了设计方便,我们再做底下的定义:
typedef struct _v4l_struct v4l_device;
以后宣告 struct _v4l_struct 时,将一律使用 v4l_device。
实作函数宣告
底下宣告将要实作的 functions,我们采取 top-down的实作方式,也就是先将所有会用到的函数事先规划,并宣告在原始码里。当然,本文并不会介绍底下所有的函数,但重要的函数则会做说明。
实际做设计时,有些函数可能会在后期才会被设计出来。我们所要实作的函数与函数宣告如下:
extern int v4l_open(char *, v4l_device *);
extern int v4l_close(v4l_device *);
extern int v4l_get_capability(v4l_device *);
extern int v4l_set_norm(v4l_device *, int);
extern int v4l_get_channels(v4l_device *);
extern int v4l_get_audios(v4l_device *);
extern int v4l_get_picture(v4l_device *);
extern int v4l_grab_init(v4l_device *, int, int);
extern int v4l_grab_frame(v4l_device *, int);
extern int v4l_grab_sync(v4l_device *);
extern int v4l_mmap_init(v4l_device *);
extern int v4l_get_mbuf(v4l_device *);
extern int v4l_get_picture(v4l_device *);
extern int v4l_grab_picture(v4l_device *, unsigned int);
extern int v4l_set_buffer(v4l_device *);
extern int v4l_get_buffer(v4l_device *);
extern int v4l_switch_channel(v4l_device *, int);
v4l_open() -- 开启 device file
首先,v4l_open() 是我们第一个应该要撰写的函数。v4l_open()用来开启影像来源的设备档。依据 v4l_open() 的宣告,在应用程式里,我们会这样呼叫 v4l_open():
v4l_device vd;
if (v4l_open("/dev/video0", &vd)) {
return -1;
}
在应用程式里,我们宣告了一个 vd 变数 (v4l_device 型态),再呼叫v4l_open() 将设备档开启。如果可以开启 "/dev/video0" 则将取回的信息放到 vd 里,vd 是v4l_device 也就是之前宣告的 _v4l_struct。接下来,让我们来看看 v4l_open() 要如何实作:
#define DEFAULT_DEVICE "/dev/video0"
int v4l_open(char *dev, v4l_device *vd)
{
if (!dev)
dev = DEFAULT_DEVICE;
if ((vd->fd = open(dev, O_RDWR)) < 0) {
perror("v4l_open:");
return -1;
}
if (v4l_get_capability(vd))
return -1;
if (v4l_get_picture(vd))
return -1;
return 0;
}
为了设计出完整的 video4linux 程序库,一开始我们就定义了DEFAULT_DEVICE,当应用程式输入的 dev设备档参数不存在时,就使用预设的设备档名称。程序片段如下:
if (!dev)
dev = DEFAULT_DEVICE;
与一般 Linux Programming 一样,我们使用 open() 将 device file 打开:
if ((vd->fd = open(dev, O_RDWR)) < 0) {
perror("v4l_open:");
return -1;
}
如果您不熟悉 Linux 下 open() 的使用方法,请参考 Linux programming相关资料。熟悉 UNIXprogramming 的读者一定知道,open() 也与 STREAMS 的观念相关,这部份在后面会再另外做介绍。
将设备档开启后,把传回来的 file description 放到 vd->fd 里。
成功开启设备档后,根据 API.html的说法,我们要先取得设备的信息与影像视窗的信息,所以这里再实作 v4l_get_capability() 与 v4l_get_picture() 来完成这二件工作。
v4l_get_capability() 会利用 ioctl()取得设备档的相关信息,并且将取得的信息放到 structvideo_capability结构里。同理,v4l_get_picture() 也会呼叫 ioctl(),并将影像视窗信息放到 struct video_picture 结构。
v4l_get_capability() 函数程序码如下:
int v4l_get_capability(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {
perror("v4l_get_capability:");
return -1;
}
return 0;
}
在这里,其实只有底下这一行才是 v4l_get_capability 的主力:
ioctl(vd->fd, VIDIOCGCAP, &(vd->capability));
其它部份都是属于错误处理的程序码,在本文,笔者都将函数写的完整一点,即包含了错误检查,因为我们想要实作一个v4l 的 library。
vd->fd 是由 v4l_open 传回来的 file descriptor,而传递 VIDIOCGCAP 给ioctl() 则会传回设备相关信息,在这里则是存放于 vd->capability。
v4l_get_ picture() -- picture 的初始化
取得设备信息后,我们还要再取得影像信息,所谓的影像信息指的是输入到影像捕捉卡的影像格式。在 _v4l_struct 结构里,我们宣告 channel 如下:
struct video_picture picture;
初始化 picture的意思就是要取得输入到影像捕捉卡的影像信息,我们设计 v4l_get_picture() 函数来完成这件工作。v4l_get_ picture () 完整程序码如下:
int v4l_get_picture(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {
perror("v4l_get_picture:");
return -1;
}
return 0;
}
传递VIDIOCGPICT 给 ioctl() 则会传回影像的属性 (image properties),这里则是将影像属性存放于vd-> picture。
v4l_get_channels() -- channel 的初始化
接下来,我们还要再做 channel 的初始化工作。还记得在 _v4l_struct结构里,我们宣告 channel 如下:
struct video_channel channel[8];
channel 是一个 8 个元素的数组,一般绝大部份都会宣告 4 个元素,因为大部份的影像捕捉卡都只有 4 个 channel。几乎没有影像捕捉卡有8 个 channel的。
初始化 channel 的意思就是要取得「每个」 channel 的信息,我们设计v4l_get_channels() 函数来完成这件工作。v4l_get_channels() 完整程序码如下:
int v4l_get_channels(v4l_device *vd)
{
int i;
for (i = 0; i < vd->capability.channels; i++) {
vd->channel.channel = i;
if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel)) < 0) {
perror("v4l_get_channel:");
return -1;
}
}
return 0;
}
要记得,我们是对每个 channel做初始化,所以必须用一个回圈来处理每个 channel。那我们怎么知道影像捕捉卡上有几个channel 呢?记得我们设计 v4l_open() 时也「顺路」呼叫了v4l_get_capability()吗!v4l_get_capability()所取得的设备信息,就包含了影像捕捉卡的 channel 数。这个信息储存于vd->capability.channels 里。
由于 v4l_get_capability() 是必备的程序,所以我们就顺便写在 v4l_open()里。当然,如果您没有在v4l_open() 里呼叫 v4l_get_capability(),这样的设计方式当然没有错,只是在设计应用程式时,要记得在v4l_open() 后还要再呼叫v4l_capability() 才行。
在回圈里,首先先替每个 channel 做编号:
vd->channel.channel = i
然后再取得 channel 的信息:
ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel);
传递 VIDIOCGCHAN 给 ioctl() 则会传回 channel 的资 讯,这里则是将channel 的信息存放于 vd-> channel。 要注意一下,在 kernel 2.4 的 API.html 文件里,粗心的 programmer 将VIDIOCGCHAN 打成 VDIOCGCHAN,少了一个 "I"。
v4l_get_audios() -- audio 的初始化
接下来,我们再做 audio 的初始化工作,audio 的初始化方式与初始化channel 的方法很像。在 _v4l_struct 结构里,我们宣告 auduio的结构如下:
struct video_audio audio[8];
audio 是一个 8 个元素的数组,与 channel 一样。一般绝大部份都会宣告 4个元素,因为大部份的影像捕捉卡都只有 4 个audio。几乎没有影像捕捉卡有8 个audio的。
初始化 audio 的意思就是要取得「每个」 audio 的信息,我们设计v4l_get_audios() 函数来完成这件工作。v4l_get_audios() 完整程序码如下:
int v4l_get_audios(v4l_device *vd)
{
int i;
for (i = 0; i < vd->capability.audios; i++) {
vd->audio.audio = i;
if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio)) < 0) {
perror("v4l_get_audio:");
return -1;
}
}
return 0;
}
别忘了,我们仍然要对每个 audio 做初始化,所以必须用一个回圈来处理每个 audio。那我们怎么知道影像捕捉卡上有几个audio 呢?与取得 channel 的方式一样,audio 数量的信息储存于vd->capability.audios 里。
在v4l_get_audios() 的回圈里,首先先替每个 audio 做编号:
vd->audio.audio = i;
然后再取得 audio 的信息:
ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio);
传递 VIDIOCGAUDIO 给 ioctl() 则会传回 audio 的信息,这里则是将 audio的信息存放于 vd-> audio。
v4l_close() -- 关闭装置档
v4l_close()程序相当简单,所以不用再多做介绍啦!直接列出程序码如下:
int v4l_close(v4l_device *vd)
{
close(vd->fd);
return 0;
}
配合应用程式来设计
设计了几个函式后,接下来我们要实地设计一个应用程式来说明如何使用v4l_xxx() 系列的函式。底下是一个在应用程式里初始化影像捕捉卡,并且列出取得的信息的程序范例 (完整程序码):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "v4l/v4l.h"
v4l_device vd;
int device_init(char *dev)
{
if (dev == NULL) {
dev = "/dev/video0"; //set to default device
}
if (v4l_open(dev, &vd)) return -1;
if (v4l_get_channels(&vd)) return -1;
printf("%s: initialization OK... %s\n"
"%d channels\n"
"%d audios\n\n",
dev, vd.capability.name, vd.capability.channels,
vd.capability.audios);
v4l_close(&vd);
return 0;
}
int main()
{
if (device_init("/dev/video0") == -1) {
perror("device_init: failed...");
exit(1);
} else {
printf("OK!\n");
}
exit(0);
}
我们将这个程序存成 main.c,整个程序不用再多做介绍了吧!程序里用到的地方都有介绍过,其中vd.capability.name 代表界面的 canonical name。
device_init() 最后呼叫 v4l_close()将装置档关闭,别忘了这个重要的工作!v4l/v4l.h 的内容如下 (完整程序码):
#ifndef _V4L_H_
#define _V4L_H_
#include <sys/types.h>
#include <linux/videodev.h>
#define PAL_WIDTH 768
#define PAL_HEIGHT 576
#define CIF_WIDTH 352
#define CIF_HEIGHT 288
#define NTSC_WIDTH 640
#define NTSC_HEIGHT 480
struct _v4l_struct
{
int fd;
struct video_capability capability;
struct video_buffer buffer;
struct video_window window;
struct video_channel channel[8];
struct video_picture picture;
struct video_tuner tuner;
struct video_audio audio[8];
struct video_mmap mmap;
struct video_mbuf mbuf;
unsigned char *map;
};
typedef struct _v4l_struct v4l_device;
extern int v4l_open(char *, v4l_device *);
extern int v4l_close(v4l_device *);
extern int v4l_get_capability(v4l_device *);
extern int v4l_set_norm(v4l_device *, int);
extern int v4l_get_channels(v4l_device *);
extern int v4l_get_audios(v4l_device *);
extern int v4l_get_picture(v4l_device *);
extern int v4l_grab_init(v4l_device *, int, int);
extern int v4l_grab_frame(v4l_device *, int);
extern int v4l_grab_sync(v4l_device *);
extern int v4l_mmap_init(v4l_device *);
extern int v4l_get_mbuf(v4l_device *);
extern int v4l_get_picture(v4l_device *);
extern int v4l_grab_picture(v4l_device *, unsigned int);
extern int v4l_set_buffer(v4l_device *);
extern int v4l_get_buffer(v4l_device *);
extern int v4l_switch_channel(v4l_device *, int);
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)) < dev =" DEFAULT_DEVICE;">fd = open(dev, O_RDWR)) <>fd 里。
成功开启设备档后,根据 API.html的说法,我们要先取得设备的信息与影像视窗的信息,所以这里再实作 v4l_get_capability() 与 v4l_get_picture() 来完成这二件工作。
v4l_get_capability() 会利用 ioctl()取得设备档的相关信息,并且将取得的信息放到 structvideo_capability结构里。同理,v4l_get_picture() 也会呼叫 ioctl(),并将影像视窗信息放到 struct video_picture 结构。
v4l_get_capability() 函数程序码如下:
int v4l_get_capability(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) <>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)) <> 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 <>capability.channels; i++) {
vd->channel.channel = i;
if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel)) <>capability.channels 里。
由于 v4l_get_capability() 是必备的程序,所以我们就顺便写在 v4l_open()里。当然,如果您没有在v4l_open() 里呼叫 v4l_get_capability(),这样的设计方式当然没有错,只是在设计应用程式时,要记得在v4l_open() 后还要再呼叫v4l_capability() 才行。
在回圈里,首先先替每个 channel 做编号:
vd->channel.channel = i
然后再取得 channel 的信息:
ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel);
传递 VIDIOCGCHAN 给 ioctl() 则会传回 channel 的资 讯,这里则是将channel 的信息存放于 vd-> channel。 要注意一下,在 kernel 2.4 的 API.html 文件里,粗心的 programmer 将VIDIOCGCHAN 打成 VDIOCGCHAN,少了一个 "I"。
v4l_get_audios() -- audio 的初始化
接下来,我们再做 audio 的初始化工作,audio 的初始化方式与初始化channel 的方法很像。在 _v4l_struct 结构里,我们宣告 auduio的结构如下:
struct video_audio audio[8];
audio 是一个 8 个元素的数组,与 channel 一样。一般绝大部份都会宣告 4个元素,因为大部份的影像捕捉卡都只有 4 个audio。几乎没有影像捕捉卡有8 个audio的。
初始化 audio 的意思就是要取得「每个」 audio 的信息,我们设计v4l_get_audios() 函数来完成这件工作。v4l_get_audios() 完整程序码如下:
int v4l_get_audios(v4l_device *vd)
{
int i;
for (i = 0; i <>capability.audios; i++) {
vd->audio.audio = i;
if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio)) <>capability.audios 里。
在v4l_get_audios() 的回圈里,首先先替每个 audio 做编号:
vd->audio.audio = i;
然后再取得 audio 的信息:
ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio);
传递 VIDIOCGAUDIO 给 ioctl() 则会传回 audio 的信息,这里则是将 audio的信息存放于 vd-> audio。
v4l_close() -- 关闭装置档
v4l_close()程序相当简单,所以不用再多做介绍啦!直接列出程序码如下:
int v4l_close(v4l_device *vd)
{
close(vd->fd);
return 0;
}
配合应用程式来设计
设计了几个函式后,接下来我们要实地设计一个应用程式来说明如何使用v4l_xxx() 系列的函式。底下是一个在应用程式里初始化影像捕捉卡,并且列出取得的信息的程序范例 (完整程序码):
#include
#include
#include
#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
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#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)) <>fd, VIDIOCGCAP, &(vd->capability)) < i =" 0;">capability.channels; i++) {
vd->channel.channel = i;
if (ioctl(vd->fd, VIDIOCGCHAN, &(vd->channel)) < i =" 0;">capability.audios; i++) {
vd->audio.audio = i;
if (ioctl(vd->fd, VIDIOCGAUDIO, &(vd->audio)) <>fd, VIDIOCGPICT, &(vd->picture)) <>fd);
return 0;
}
当程程序无法初始化装置时,会出现的错误讯息:
v4l_open:: No such device
device_init: failed...: No such device
如果出现这样的错误:
v4l_open:: Device or resource busy
device_init: failed...: Device or resource busy
最大可能的原因可能是:(1 )躯动程序没有安装好或躯动程序不适用,(2)「前人」的程序忘了将装置档关闭。如果程序可以顺利初始化装置,就会看到这样的讯息:
/dev/video0: initialization OK... BT878(Hauppauge new)
3 channels
1 audios
OK!
我们将取得的装置信息 print到屏幕上,以了解取得的相关信息。在下一期的文章里,我们将会介绍更多video4linux 的设计方法,来做到更高级的工作。在这里我们看到程序已经成功初始代我们的装置,并且知道装置有 3 个 channel、1 个 audio。
STREAMS Programming
接下来要介绍的是属于观念性的话题,比较不重要。我们将以理论为主,来讲解"STREAMS" 程序设计的基础观念。
什么是 STREAMS?
在 Solaris 2 的 kernel 里,STREAMS定义了一个标准界面,这个界面主要的功能是提供装置与kernel之间的 I/O 沟通管道。这个界面其实是由系统呼叫 (system calls)与核心常式 (kernelroutines) 所组成,我们可以简单表示成下图:
图 1
图中的 Module 标示为 Optional,也就是在 Stream Head 与 Driver 之间,并不一定存在这个Module,这个 Module 属于中间者的角色,也就是,当 stream (解释成资料串流或许比较好理解) 在Stream Head 与 Driver 之间「流」动时,Module 会从中做额外的处理。
有时这个 Module是相当重要的,因为资料串流必须经过特殊的处理,才能流向彼方。这种 kernel 设计的方式相当好,因为 Module 一定是动态 (dynamic) 被装到串流里的。而且,这个Module 是由 user process 所载入,因此,user 可以根据不同的心情「抽换」不同的 Module。
在最底下 Driver 的地方一般指的是 UNIX底下的设备档,到这里,读者有没有感觉到,是不是有些观念跟我们实作出来video4linux 程序库可以相连呢!
由图可以看出,根据 stream 的流向,可以将 stream 分成 downstream 与upstream。由于stream 是双向的,所以我们可以把 STREAMS 称为全双工模式 (full-duplex) 的资料处理与传送。 我们可以把图 1 再简单表示成下图:
图 2
由这里可以发现一个事实,整个 STREAMS 的起点是 Driver,而终点是User Process。在 user space 与 kernel space 之间则是由 Stream head 来连接。
当然,user process 可能是 local user process 或者 remote user process。目前为止,我们尚未进入user process 的部份,所以暂时不会提到 RTP 等通讯协定的设计。
接下来,再介绍一下 downstream 与 upstream。通常,downstream 也称为writeside,也就是写入资料那一方;而 upstream 则称为 read side,也就是读取资料那一方。那么,在UNIXprogramming 里,什么时候会牵涉到 STREAMS 呢?
最简单的例子莫过于由终端机读取字元的范例了。一个简单的程序片段如下:
main()
{
char buf[1024];
int fd;
int count;
if ((fd = open("/dev/tty1", )_RDWR)) < count =" read(fd,"> 0) {
if (write(fd, buf, count) != count) {
perror("write: /dev/tty1");
break;
}
}
exit(0);
}
对 Network programming 而言,如果我们要经由 Socket读取字元,可以写一个简单的程序如下:
int main(int argc, char *argv[])
{
char *buff = "Hello, socket!";
int sockfd;
struct sockaddr_in serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.1.10"); // ip
serv_addr.sin_port = htons(3999); // port
connect(sockfd, &serv_addr, sizeof(serv_addr));
write(fd, buff, strlen(buff));
close(sockfd);
exit(0);
}
这是一个 client 端向 server送出字元的程序范例,这段程序主要是要让读者看出,经由终端机设备写入字元时,是利用write() 函数,而在 socket 上写入字元,却也是利用 write() 函数。
这种 UNIX kernel 整合外围设备与网络 I/O 的机制事实上就是 STREAMSprogramming所要解决的问题。整合 UNIX kernel 与网络 I/O 的工作首先由Dennis Ritchie这位大师所进行,所以现在我们才会拥有现今这么强大的 UNIX系统。
到目前为止,我们仍然只对 video capture card 做初始化的动作,并讨论一些观念,接下来的文章将以循序渐进的方式实作整个 Video Streaming 系统。

Linux的应用--Video Streaming探讨一

作者: 陈俊宏 www.jollen.org
Video Streaming 的技术已经出现多年,在 Internet 上的应用也已经相当广泛,而利用 Linux 建构 VideoStreaming Server 也是相当热门的话题。如果您想了解什么是 Video Streaming,本文对 VideoStreaming 的观念与技术将做了清楚而基本的介绍。
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,我们称这样的技术为 VideoStreaming 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),谈到这些技术,应用最广泛的编码标准底下四种:
1. ) H.261
2. ) H.263
3. ) JPEG, MJPEG
4. ) MPEG
底下将分别简单介绍这四种编码技术。
H.261 标准
H.261 的影像编码标准出现在 1990 年的 ITU。一开始的用途主要是支援影像电话 (video phone) 与视讯会议 (video conferencing)。H.261 的格式有二种,分别有不同的解析度:
1. ) QCIF:176x144
2. ) 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 to30)。
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 的标准共支援五种不同的解析度,分别为:
1. ) Sub-QCIF:128x96
2. ) QCIF:176x144
3. ) CIF:352x288
4. ) 4CIF:704x576
5. ) 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 图档。许多 VideoStreaming 的场合,像是简单的视讯会议软件都会使用 MJPEG 来取代 MPEG,原因无它,因为 MJPEG格式简单,但缺点是不支援声音。
MPEG-1 标准
MPEG 的标准由 ISO (International Standards Organization) 所制定,全名为 MovingPictures 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,因此 VideoStreaming 的应用将更为广泛。
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 ofPictures),每个 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 应用程式。



video for linux 编程概述

1.什么是video4linux
Video4linux(简称V4L),是linux中关于视频设备的内核驱动,现在已有Video4linux2,还未加入linux内核,使用需自己下载补丁。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video0下。

2.Video4linux下视频编程的流程
(1)打开视频设备:
(2) 读取设备信息
(3)更改设备当前设置(没必要的话可以不做)
(4)进行视频采集,两种方法:
a.内存映射
b.直接从设备读取
(5)对采集的视频进行处理
(6)关闭视频设备。

为程序定义的数据结构
typedef struct v4l_struct
{
int fd;
struct video_capability capability;
struct video_channel channel[4];
struct video_picture picture;
struct video_window window;
struct video_capture capture;
struct video_buffer buffer;
struct video_mmap mmap;
struct video_mbuf mbuf;
unsigned char *map;
int frame;
int framestat[2];
}vd;

3.Video4linux支持的数据结构及其用途
(1) video_capability 包含设备的基本信息(设备名称、支持的最大最小分辨率、信号源信息等),包含的分量:
•name[32] //设备名称
•maxwidth ,maxheight,minwidth,minheight
•Channels //信号源个数
•type //是否能capture,彩色还是黑白,是否能裁剪等等。值如VID_TYPE_CAPTURE等
•
(2)video_picture 设备采集的图象的各种属性
•brightness 0~65535
•hue
•colour
•contrast
•whiteness
•depth // 24
•palette //VIDEO_PALETTE_RGB24

(3)video_channel 关于各个信号源的属性
Channel //信号源的编号
name
tuners
Type VIDEO_TYPE_TV | IDEO_TYPE_CAMERA
Norm制式

(4)video_window //包含关于capture area的信息
xx windows 中的坐标.
y x windows 中的坐标.
width The width of the image capture.
height The height of the image capture.
chromakey A host order RGB32 value for the chroma key.
flags Additional capture flags.
clips A list of clipping rectangles. (Set only)
clipcount The number of clipping rectangles. (Set only)
(5)video_mbuf //利用mmap进行映射的帧的信息
size //每帧大小
Frames //最多支持的帧数
Offsets //每帧相对基址的偏移
(6)video_buffer 最底层对buffer的描述
void *baseBase physical address of the buffer
int heightHeight of the frame buffer
int widthWidth of the frame buffer
int depthDepth of the frame buffer
int bytesperlineNumber of bytes of memory between the start of two adjacent lines
实际显示的部分一般比它描述的部分小
(7)video_mmap //用于mmap

4.关键步骤介绍
(1)打开视频:
Open("/dev/video0",vdàfd);
关闭视频设备用close("/dev/video0",vdàfd);
(2)读video_capability 中信息
ioctl(vd->fd, VIDIOCGCAP, &(vd->capability))
成功后可读取vd->capability各分量 eg.
(3)读video_picture中信息
ioctl(vd->fd, VIDIOCGPICT, &(vd->picture));
(4)改变video_picture中分量的值 (可以不做的)
先为分量赋新值,再调用VIDIOCSPICT
Eg.
•vd->picture.colour = 65535;
•if(ioctl(vd->fd, VIDIOCSPICT, &(vd->picture)) < 0)
•{
•perror("VIDIOCSPICT");
•return -1;
•}
(5)初始化channel (可以不做的)
•必须先做得到vd->capability中的信息
•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;
• }
• }

重点:截取图象的两种方法
1,用mmap(内存映射)方式截取视频
•mmap( )系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
•两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然
•采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝
(1)设置picture的属性
(2) 初始化video_mbuf,以得到所映射的buffer的信息
ioctl(vd->fd, VIDIOCGMBUF, &(vd->mbuf))
(3)可以修改video_mmap和帧状态的当前设置
• Eg. vd->mmap.format = VIDEO_PALETTE_RGB24
• vd->framestat[0] = vd->framestat[1] = 0; vd->frame = 0;
(4)将mmap与video_mbuf绑定
•void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
•len //映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起
•Prot //指定共享内存的访问权限 PROT_READ(可读), PROT_WRITE (可写), PROT_EXEC (可执行)
•flags // MAP_SHARED MAP_PRIVATE中必选一个 // MAP_ FIXED不推荐使用addr //共内存享的起始地址,一般设0,表示由系统分配
•Mmap( ) 返回值是系统实际分配的起始地址
•if((vd->map = (unsigned char*)mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, vd->fd, 0)) < 0)
•{
•perror("v4l_mmap mmap:");
•return -1;
•}
(5)Mmap方式下真正做视频截取的 VIDIOCMCAPTURE
ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) ;
•若调用成功,开始一帧的截取,是非阻塞的,
•是否截取完毕留给VIDIOCSYNC来判断
(6)调用VIDIOCSYNC等待一帧截取结束
•if(ioctl(vd->fd, VIDIOCSYNC, &frame) < 0)
•{
•perror("v4l_sync:VIDIOCSYNC");
•return -1;
•}
若成功,表明一帧截取已完成。可以开始做下一次 VIDIOCMCAPTURE
•frame是当前截取的帧的序号。

****关于双缓冲:
•video_bmuf bmuf.frames = 2;
•一帧被处理时可以采集另一帧
•int frame; //当前采集的是哪一帧
•int framestat[2]; //帧的状态 没开始采集|等待采集结束
•帧的地址由vd->map + vd->mbuf.offsets[vd->frame]得到
•采集工作结束后调用munmap取消绑定
•munmap(vd->map, vd->mbuf.size)

2,视频截取的第二种方法:直接读设备
关于缓冲大小,图象等的属性须由使用者事先设置
•调用read();
•int read (要访问的文件描述符;指向要读写的信息的指针;应该读写的字符数);
•返回值为实际读写的字符数
•int len ;
•unsigned char *vd->map= (unsigned char *) malloc(vdàcapability.maxwidth*vdàcapability.maxheight );
•len = read(vdàfd,vdà vd->map,
• vdàcapability.maxwidth*vdàcapability.maxheight*3 );

推荐几个能在linux 下看的影视网站

http://www.cnmovie.net.cn
电影网站,片子不少,而且分辨率也比较高。

http://www.fzlm.com/
在线电视,电视节目不少,速度也还可以。虽然写着IE only 但我的FF 3.0.12
能正常访问。

酷6高清(http://hd.ku6.com/)

新浪大片(http://video.sina.com.cn/movie/)

HD高清(http://hd.openv.com/)

蔡明介:不介意被称为“山寨之父”

周六去清华参加联发科董事长蔡明介报告会本以为会人山人海,没想到主办者预定的会议室规模不到百人,虽然后来有许多学生站在后排或在前排席地就坐,还是感到在集成电路领域呼风唤雨的蔡明介在大陆学生中知名度远远不够,据主办老师介绍之所以预约的会议室比较小,是因为前段时间在清华电子系做过一个调查,学生中知道蔡明介的很少,知道联发科的也不多,知道山寨机的倒占了大多数,因为担心冷场,所以没敢定一个较大的报告厅。
同样是微电子行业,记得前几年展讯CEO武平先生到清华演讲,虽然租用了学校的礼堂,同样有很多学生因为座位不够不得不站着听完了整个演讲,可能因为武平是清华校友的缘故,两者之间的对比,可见大陆大学生对微电子行业的了解还是很少,与台湾知名大学生毕业之后很多选择微电子行业就业不同,大陆知名大学学生可能因为就业机会太多,对微电子行业关注远远不够。
蔡明介先生演讲题目为半导体行业的未来及创新,可能因为"山寨机之父"的缘故,从演讲后提问来看,显然听众对山寨机兴趣更大些,大多数提问也是围绕山寨机进行,老杳很关心蔡先生对山寨的评价,只是没有机会提问,好在一位同学提到这个问题,询问他对"山寨之父"的看法,蔡明介的回答是不介意大家称之为"山寨机之父",至少从这个称谓说明联发科在商业上很成功。
蔡明介将山寨现象定义为"破坏性创新",准确的将应当是Turnkey模式对集成电路特别是手机芯片领域的破坏性创业更合适,毕竟山寨手机流行一方面与联发科Turnkey模式有关,也与许多媒体口诛笔伐的偷税漏税、质量缺陷有关,这也可能是许多消费者对山寨并不认可而联发科却不以为耻的根本原因。
作为嘉宾曾经两次参加电视台主办有关山寨手机的研讨,一次是凤凰卫视、一次是天津卫视,当主持人询问观众如何看待山寨机,是否使用过山寨机,大多数观众都认为山寨手机缺乏创新、质量有问题,也极力否认自己是山寨手机用户,由此可见消费者对并不认可蔡明介山寨是破坏性创新的说法,甚至由山寨漂白的天宇朗通也极力避谈山寨话题,前不久央视曾经做一期有关山寨手机的节目,希望天宇朗通能够参加,但天宇朗通因为并不认为自己出身山寨而拒绝参与,由此可见即使在手机厂商中,山寨也并非是褒义字眼,甚至连中性的含义都很少,之所以如此与大多数消费者对山寨的理解有很大关系。
蔡明介认可山寨之父并将山寨定义为破坏性创新,天宇朗通不认可山寨拒绝参加央视有关山寨节目,大多数消费者对山寨手机质量问题充满顾虑,可见不同角度对山寨的看法存在巨大差异,这也是为什么山寨一词一直是媒体争论热点的主要原因,蔡明介不介意是因为山寨为联发科带来巨大的商业成功,天宇躲避是担心因为山寨降低销量,而消费者担心是因为山寨手机的负面影响被媒体无限夸大。
联发科的破坏性创新确实让之前手机领域的霸主如TI业绩下滑,不过turnkey模式也让业界其他公司看到超越的希望,与蔡明介认为当代集成电路创业太难不同,大陆包括海思、瑞芯微等集成电路的佼佼者已经在沿着联发科的思路闯入这一领域,并大有在智能手机及3G手机后来居上的趋势,同样出自台湾的Mstar也被业界誉为比MTK更MTK,已经在数字电视领域将联发科远远抛在后面,可见任何创新也总有落伍的时候。(作者,老杳)