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) 所组成,我们可以简单表示成下图:![](http://1.bp.blogspot.com/_EGsewCBJBq4/Snaqxj-mzfI/AAAAAAAAAXY/L6nfmQXGeCo/s400/1.png)
图 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 再简单表示成下图: ![](http://3.bp.blogspot.com/_EGsewCBJBq4/Snaqxw63ReI/AAAAAAAAAXg/-05DnuumEN8/s400/2.png)
图 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 系统。
目前在网络上流行的 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) 所组成,我们可以简单表示成下图:
![](http://1.bp.blogspot.com/_EGsewCBJBq4/Snaqxj-mzfI/AAAAAAAAAXY/L6nfmQXGeCo/s400/1.png)
图中的 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 再简单表示成下图:
![](http://3.bp.blogspot.com/_EGsewCBJBq4/Snaqxw63ReI/AAAAAAAAAXg/-05DnuumEN8/s400/2.png)
由这里可以发现一个事实,整个 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 系统。