2009年8月3日星期一

opera for linux 版本的区别

opera-10.00-b1.gcc4-qt4.i386.rpm
opera-10.00-b1.gcc4-static-qt3.i386.rpm
一种是用Qt4开发,一种是用Qt3开发。如果你系统有Qt4/Qt3的话,安装第一种或者shared目录里的; 否则就安装static版的。

opera-10.00-b2.gcc4-bundled-qt4.i386. tar.gz
opera-10.00-b2.gcc4-qt4.i386.tar.gz
bundbled 里面有 usr/lib/opera/libQtGui.so.4 和 usr/lib/opera/libQtCore.so.4 这两个文件,并且 opera 里多了这么几行:
代码:
~% diff -u ./opera-10.00-4502.gcc4-qt4.x86_64/opera ./opera-10.00-4502.gcc4-bundled-qt4.x86_64/opera
--- ./opera-10.00-4502.gcc4-qt4.x86_64/opera 2009-07-21 18:50:22.000000000 +0800
+++ ./opera-10.00-4502.gcc4-bundled-qt4.x86_64/opera 2009-07-21 18:53:27.000000000 +0800
@@ -352,6 +352,19 @@
fi
done

+# Use Qt provided by the host, if available. Set OPERA_QT_LIBPATH to override the path to Qt shared libraries.
+if [ -z "$OPERA_QT_LIBPATH" ]; then
+ OPERA_QT_LIBPATH=`LD_PRELOAD=libQtGui.so.4 LD_TRACE_LOADED_OBJECTS=1 usr/lib/opera/works 2>/dev/null | sed -ne '/^.*libQtCore\.so\.4 => \(.*\)\/libQtCore\.so.*$/s//\1/p'`
+fi
+if [ -n "$OPERA_QT_LIBPATH" ]; then
+ if [ -r "$OPERA_QT_LIBPATH/libQtCore.so.4" -a -r "$OPERA_QT_LIBPATH/libQtCore.so.4" ]; then
+ LD_PRELOAD="$OPERA_QT_LIBPATH/libQtCore.so.4:$OPERA_QT_LIBPATH/libQtGui.so.4${LD_PRELOAD:+:}$LD_PRELOAD"
+ export LD_PRELOAD
+ else
+ echo "Cannot find Qt shared libraries in $OPERA_QT_LIBPATH" >&2
+ fi
+fi
+
# Setting environment relative to current working directory
# Bundled Qt will be found here unless preloaded by the stance above
LD_LIBRARY_PATH="$PWD/usr/lib/opera${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH"

看样子貌似是在系统里没有 Qt4 的时候用自带的,有的话还是用系统的~ 看来 qt4 版本的都不是静态链接的了~

投资基金当注重复利效应淡待大盘点位

  投资基金特别是以投资股票为对象的股票型和偏股型基金,一般而言,收益在一定时期内与大盘点位的涨跌升降有着同方向、紧密性的变化的;但从基金投资的长期复利效应观察看,则与大盘点位的关系就不哪么十分密切了,动辄有时还会出现与大盘方向相反的情况,也即大盘跌时它反涨。所以,对长期投资并欲深谙基金赚钱机理者,"注重复利效应而淡待大盘点位"才是理性明智的可取之态。这缘自于:

  一、投资基金的复利效应乃基金的本身内在特性所决定
  以投资股票为对象的股票型和偏股型基金,因内在配置资产的类型不同、股票资产的占比不同和个股的构成不同,便有了各自不同特性的蕴涵:诸如成长股票型的、价值股票型的、均衡股票型的、收益偏股型的、混合偏股型的、平衡偏股型的等不同品种的基金。在不同收益与风险的对称性中,虽因有几十到逾百只的不同数量、不同行业、不同盘别、不同板块、不同题材的不同个股构成及其仓位高低的不同,都在市场中呈相同、相近或不同的净值增长变化表现,但寻根究源的是:在其基金制度设计上的股票投资最高达95%、最低只有20%的可调控变换的十分宽泛性,既利于紧跟或应对市场的涨跌调整变化,又便于周而复始地把行情中赚得的前一波又一波的红利投入到后一波又一波的行情中去博弈,用赚得的钱再去赚钱,以此不断"滚动"的"利滚利",把基金投资的"复利效应"奥秘显示出来;进而随着时间上的日积月累,其"复利效应"的叠加递进,便又顺理成章地"升级"成了人们常说的"时间红利"了。这当然全有如此内在特性的基金,主动结合了市场的"涌动",才有的结果。
  故而,正是有了基金这种独有的自身特性,一方面由于其所有资产最高只有95%参与市场的运作和所投资单只个股的最高比例限制与精选全部个股有机组合的风险分散,常常是大盘涨时基金的净值涨,大盘跌时基金的净值跌得少;另一方面由于基金红利所产生的复利效果,既能抵消或摊薄大盘下跌时的基金市值的下跌,又能加速超赶大盘回调时的基金市值上升速度。所以,投资基金当注重其复利效应而淡待大盘的点位高低,并要与市场长厢斯守,才能尽享其复利效应带来的"超强"之回报。

  二、投资基金的复利效应乃基金管理人的主动运作所铸就
  股票型和偏股型基金在制度设计上,从资产的构成、股票的占比和个股的组合及只数的多少等的全方位的三个不同层次上,都赋予了基金管理人的"进可攻、退可守"的主动运作空间。基金管理人依基金设立时或更新后的投资理念、目标、策略、风格和业绩比较基准等要求,经对上市流通的股票及其上市公司的筛选和深入地调查研究,用不断更新形成的股票池,再结合适时地紧跟对市场地预测和把握:在市场正常运行和热点板块轮动的市场境况下,对于已投资的并赚到了"资本利得"的价格相对已经高估的股票卖掉,而为投资人把赚到的钱实实在在地拿到手,同时再将包括本利在内的钱买进被市场相对低估的或者增发与新上市的"股票池"中的股票;在市场的调整波动变化来临前,主动通过调整降低持股仓位,同样把投资股票赚到的钱巧妙地留下,再将本利投资于能暂时规避市场调整波动风险的工具与品种,一样能让红利的复利效应得到发挥,待市场调整波动结束前再一次地做基金全部资产的调动布局,以赢得更加可观的复利收获。对投资者而言,要想使投资基金的复利效应发挥的更充分,必须要经历基金管理人有利于既能保住红利、又能充分发挥红利的复利效应的多个主动运作的轮回,才能实现;倘若是时间短了,则是不行的;再倘若是逢市场已步入调整期,基金投资人若再去势单力薄"单挑独斗"地做赎回避险,显然是在"步"基金管理人的"后尘",而做有损红利、复利的无谓"自我牺牲"。
  因此由上述可知,有基金管理人用极专业的专家团队理财智慧,在前方一线"知己知彼"的与市场在博弈,一般情况下,早就为投资人做了应对与打理,什么大盘"点高点低"的也就不在话下了。

  三、投资基金的复利效应乃股票市场波浪式螺旋上升中所孕育
  股市的运行总是在涨涨跌跌、跌跌涨涨中进行,无论"熊市"还是"牛市",均概莫能外;因人气和资金的进退,便助推了一波又一波的市场涨跌行情,而呈波浪式螺旋上升的一种总态势。正是这波浪式螺旋上升之态势,融一波又一波的市场行情中,在持续不断地把基金投资的复利效应来孕育:热点板块的轮动而致市场中股票投资价值的高估或低估、老股增发而扩大了市场容量增加了供应、新股上市而在稀释市场风险中凭添了新生力量,获利盘的回吐在释放市场风险中酝酿健康、永无休止的技术修复在调整与掌控着市场的步骤、探底回调演义着多空双方"势不罢休"的能量与智慧的博弈……这"摸"不到但却能看到和感受得到股市中的一幕幕"波澜壮阔"的"场景",对市场研究透彻的基金管理人,在积极应对中,或"该出手时就出手"增加仓位,或保持仓位"按兵不动"蓄势待发,或"收兵回营"降低仓位寓"小憩"中"司机返扑",或"修筑工事"调仓换仓等,借以把未分红的红利与投资人的分红再投,融入前浪推后浪的波浪式的螺旋上升的市场大潮中,获得一波又一波的复利之复利。
  这同样不难看出,投资基金的复利获得,其"每一波"自然得多少借助市场的上行来实现,但就在整个持续不停的市场博弈中,则少不了基金管理人及基金经理们,能恰到好处地做到了将基金资产与市场行情的最佳结合,致此相比之下,大盘点位的是高是低就显得不那么重要了。
  以上的"基金、基金管理人、股市"的"三位一体"的有机融合,只是能从资本市场的不断运行变化中获得复利的一种机制,但欲求能从"这种机制"中真正得到回报,有两门"功课"的"作业",是投资者必须要做要交的:一是要投资于选中的基金特别是配置股票资产的基金;二是要长期坚持,否则就"前功后弃"得不偿失了。

这个时代的个十百千万

坐拥一百多万房产。

身背几十万元房贷。

手握几万元钱基金。

每月几千块钱工资。

身穿几百块钱名牌。

上班十几块的车钱。

天天几块钱的早饭。

Linux摄像头编程

这次是在linux下开发摄像头的程序,主要用的是video4linux来做的,界面用qt来实现,开始准备用framebuffer来直接写屏但是效果不怎么好,后来就用qt来做了,这样用起来效果还蛮好的,帧率也可以,可以上到30fps;运用v4l来编程主要掌握其api,要提高帧率最重要的是用到内存映射,其实用qt和framebuffer的时候都要用到内存映射来做,只有这样才可以达到较高的帧率,不过要注意资源的利用问题.mmap后一定要munmap.对于framebuffer是很有意思的一个东西,特别是驱动的设计.
代码如下:
1.open device:
video_dev = open("\dev\video0",O_RDWR));
2.get the information of the video device
struct video_capability video_cap;
memset(&video_cap,0,sizeof(video_cap));
if(ioctl(video_dev,VIDIOCGCAP,&video_cap) == -1)
{
perror("Cann't get the information of the video device");
close(video_dev);
exit(1);
}
3.get the information of the channels
struct video_channel video_chan;
memset(&video_chan,0,sizeof(video_chan));
for(channel = 0;channel < video_cap.channels;channel++)
{
video_chan.channel = channel;
if(ioctl(video_dev,VIDIOCGCHAN,&video_chan) == -1)
{
perror("Cann't get the information of the channels");
close(video_dev);
exit(3);
}
if(video_chan.type == VIDEO_TYPE_TV)
{
#ifdef DEBUG
printf("NO.%d channel is %s,type is
tv!\n",channel,video_chan.name);

在linux下使用视频采集卡

这一部分将会介绍如何在linux中对电视卡编程。
开始已经提到过,电视卡使用的是video for
linux驱动,简称v4l,实际上,现在已经有了video for linux two驱动
,即v4l2.它解决了v4l中存在的一些问题,并且提高了硬件性能。但是,目前来说,v4l2仍然没有集成到linux的内核中,要使用v4l2的话,只有去下载v4l2补丁了,以下如无特别说明,所涉及的内容只针对v4l设备而言。
我们都知道,在linux中,为了屏蔽用户对设备访问的复杂性,采用了设备文件,即可以通过像访问普通文件一样的方式来对设备进行访问读写。电视卡在linux中和打印机,鼠标一样,属于字符设备。其主设备号是81,在实际操作上,访问控制电视卡也和一般的设备文件没有什么不同。用open打开设备,
int fd;
fd = open("/dev/video0",O_RDWR);
用一系列的ioctl发命令控制设备。v4l支持的ioctl命令大概有二十几个,为了尽快的编出一个
简单的图象捕捉程序,让我们先来看看几个主要的命令:
1. ioctl(fd,VIDIOCGCAP,&cap);
该命令主要是为了获取电视卡的功能信息。例如电视卡的名称,类型,channel等。参数cap是一个结构,当ioctl命令返回时,结构的各成员就被赋值了,结构体的定义为:
struct video_capability
{
char name[32];
int type;
int channels; /* Num channels */
int audios; /* Num audio devices */
int maxwidth; /* Supported width */
int maxheight; /* And height */
int minwidth; /* Supported width */
int minheight; /* And height */
};
channel 指的是有几个信号输入源,例如television,composite,s-video等。
2.ioctl(fd,VIDIOCGCHAN,&vc)
3.ioctl(fd,VIDIOCSCHAN.&vc)
这两个命令用来取得和设置电视卡的channel信息,例如使用那个输入源,制式等。
vc 是一个video_channel结构,其定义为:
struct video_capability
{
char name[32];
int type;
int channels; /* Num channels */
int audios; /* Num audio devices */
int maxwidth; /* Supported width */
int maxheight; /* And height */
int minwidth; /* Supported width */
int minheight; /* And height */
};
struct video_channel
{
int channel;
char name[32];
int tuners;//number of tuners for this input
__u32 flags;
__u16 type;
__u16 norm;
};
成员channel代表输入源,通常,0: television 1:composite1 2:s-video
name 表示该输入源的名称。
norm 表示制式,通常,0:pal 1:ntsc 2:secam 3:auto
4. ioctl(fd,VIDIOCGMBUF,*mbuf)
获得电视卡缓存的信息,参数mbuf是video_mbuf结构。其定义如下:
struct video_mbuf
{
int size; /* Total memory to map */
int frames; /* Frames */
int offsets[VIDEO_MAX_FRAME];
};
size是缓存的大小,frames表明该电视卡的缓存可以容纳的帧数,数组offsets则表明
对应一帧的起始位置,0帧对应offsets[0],1帧对应offsets[1]....
执行完该命令后,就可以用mmap函数将缓存映射到内存中了。大致用法可以参考以下的代

struct video_mbuf mbuf;
unsigned char *buf1,*buf2;
if(ioctl(fd,VIDIOCGMBUF,&mbuf)<0)
{
perror("VIDIOCGMBUF");
return -1;
}
printf("the frame number is %d\n",mbuf.frames);
buf1 = (unsigned
char*)mmap(0,mbuf.size,PROT_READ|PROT_WRITE,MAP_SHARED,fd.0);
buf1 = buf1 + mbuf.offset[0];
buf2 = buf1 + mbuf.offset[1];//当然,如果mbuf.frames=1,就不需要下面的了。
......
5. ioctl(fd.VIDIOCMCAPTURE,&mm)
启动硬件去捕捉图象,mm 是video_mmap
结构,设置捕捉图象需要设置的信息。结构体
如下定义:
struct video_mmap
{
unsigned int frame; /* Frame (0 - n) for
double buffer */
int height,width;
unsigned int format; /* should be
VIDEO_PALETTE_* */
};
frame :设置当前是第几帧
height,width:设置图象的高和宽。
format :颜色模式
要注意的是,该命令是非阻塞的,也就是说,它仅仅设置了硬件,而不负责是否捕捉到图象。
要确定是否捕捉到图象,要用到下一个命令。
6. ioctl(fd,VIDIOCSYNC,&frame)
等待捕捉到这一帧图象。frame
是要等待的图象,它的值应和上一个命令中设置的frame相对应。
好了,说了这么多,读者大概也对视频捕捉有了一个了解,是不是想亲自动手试一下,那就让我们
开始实际程序的编写吧。
下面我们会编一个程序,将捕捉到的图象存为jpeg文件。为此,还要向大家介绍一个函数,
int write_jpeg(char *filename,unsigned char *buf,int quality,int width,
int height, int gray)
{
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *fp;
int i;
unsigned char *line;
int line_length;
if (NULL == (fp = fopen(filename,"w")))
{
fprintf(stderr,"grab: can't open %s:
%s\n",filename,strerror(errno));
return -1;
}
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
jpeg_stdio_dest(&cinfo, fp);
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = gray ? 1: 3;
cinfo.in_color_space = gray ? JCS_GRAYSCALE: JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
line_length = gray ? width : width * 3;
for (i = 0, line = buf; i < height; i++, line += line_length)
jpeg_write_scanlines(&cinfo, &line, 1);
jpeg_finish_compress(&(cinfo));
jpeg_destroy_compress(&(cinfo));
fclose(fp);
return 0;
}
这个函数很通用,它的作用是把buf中的数据压缩成jpeg格式。
/* 下面是一个完整的程序 test.c
* gcc test.c -o test -ljpeg
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <linux/videodev.h>
#include <jpeglib.h>
#define WIDTH 320
#define HEIGHT 240
#define V4L_DEVICE "/dev/video0"
main()
{
unsigned char* buf;
int i,j;
int fd;
int re;
struct video_capability vcap;
struct video_channel vc;
struct video_mbuf mbuf;
struct video_mmap mm;
fd = open(V4L_DEVICE, O_RDWR);
if(fd<=0)
{
perror("open");
exit(1);
}
if(ioctl(fd, VIDIOCGCAP, &vcap)<0)
{
perror("VIDIOCGCAP");
exit(1);
}
fprintf(stderr,"Video Capture Device Name : %s\n",vcap.name);
for(i=0;i<vcap.channels;i++)
{
vc.channel = i;
if(ioctl(fd, VIDIOCGCHAN, &vc)<0)
{
perror("VIDIOCGCHAN");
exit(1);
}
fprintf(stderr,"Video Source (%d) Name : %s\n",i, vc.name);
}
vc.channel =1;
vc.norm=1;
if(ioctl(fd, VIDIOCSCHAN, &vc) < 0)
{
perror("VIDIOCSCHAN");
exit(1);
}
if(ioctl(fd, VIDIOCGMBUF, &mbuf) < 0)
{
perror("VIDIOCGMBUF");
exit(1);
}
fprintf(stderr,"the frames number is %d\n",mbuf.frames);
buf = (unsigned char*)mmap(0, mbuf.size, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if((int)buf < 0)
{
perror("mmap");
exit(1);
}
mm.frame = 0;
mm.height = HEIGHT;
mm.width = WIDTH;
mm.format = VIDEO_PALETTE_RGB24;
if(ioctl(fd, VIDIOCMCAPTURE, &mm)<0)
{
perror("VIDIOCMCAPTURE");
exit(1);
}
if(ioctl(fd, VIDIOCSYNC, &mm.frame)<0)
{
perror("VIDIOCSYNC");
exit(1);
}
if(-1 == (write_jpeg("./pic001.jpeg",buf,75,WIDTH,HEIGHT,0)))
{
printf("write_jpeg error\n");
exit(1);
}
munmap(buf,mbuf.size);
close(fd);
}

Linux的应用--Video Streaming探讨六

本期将是我们一系列 Video Streaming
专栏的结束,本期最重要的工作当然免不了是对之前的内容做系统性的整理,然后再对
Video Streaming 的应用就目前最被讨论的理论做简单介绍。
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
端似乎是我们着墨不深的地方,但也没关系,在这最后一篇的文章里,我们针对理论方面做补充的介绍,我们将以VideoConferencing
实作时,实务上会面临的主题来做解,包括:多媒体资料的储存、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-SwitchedEnthernet上时,就会发生一些小问题。
unicasting 与 multicasting 都是属于 IP 的传输方式。unicasting 采取 1
对1的方向传影像给远端,称为 Video-on-Demand (VoD),multicasting 则是
1对多的传输方式,称为Near-Video-on-Demand (NVoD)。未来 IPv6 将支援 IP
Multicasting,因此VideoStreaming
的应用将更为广泛。在通讯协定方面,我们也介绍了 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 是
RealNetworks公司所推荐配合他们产品的一张影像撷取卡,配合 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 则是 Linuxkernel 的
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
环境。VideoConferencing应用软体在多媒体程序设计上,常常也被视为「杀手级」的应用之一,可见
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
支援的磁盘排程
(DiskScheduler)也被列入我们考虑研究的项目之一。传统上,一般我们设计 OS
时都会选择 SCAN
或是SSTF算法,不过这些传统的磁盘排程算法并无法满足我们的需求。
较先进的磁盘排程算法应考虑到 Video 与 Audio
的应用,而目前较普遍被选择用来设计 multimedia I/O 系统的磁盘排程算法则是
SCAN-EDF 算法。
SCAN-EDF 演算是结合 SCAN 与 EDF 优点的解决方案,SCAN 是众所皆知的
seekoptimizing磁盘排程算法;而 EDF (Earliest Deadline First) 则是属于
real-timescheduling 的算法。
磁盘排程对于 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 图档部份
/*
* 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);

Linux的应用--Video Streaming探讨五

本期将以完整的程序范例为主,
说明之前未深入说明的地方。并且更详细地介绍video4linux 如何以 mmap
(filp-flop) 方式撷取影像资料, 同时也会展示如何将撷取出来的影像存成图档,
并且利用绘图软件开启。
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.name,
v4l_norms[vd.channel.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);
}
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 的方式安装到
Linuxkernel, 所以 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.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()
取得设备档的相关信息,并且将取得的信息放到structvideo_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的应用--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,已经在数字电视领域将联发科远远抛在后面,可见任何创新也总有落伍的时候。(作者,老杳)