使用GNU/Linux播放电视节目
目前,生活中很多事情都可以在电脑前完成,读书、写程序、听音乐、看视频等。如果也可以在电脑上收看有线电视节目的话,那就更好了。为此,我购买了圆刚视频采集卡AverMedia C725B。如下图所示。
官方给出的此卡介绍为(详见这里):
C725标清采集卡是一张支持AV端子、S端子以及立体声输入的PCI-E撷取卡,可将PAL、NTSC和SECAM等模拟格式影像数字化,撷取并另存为 无压缩的AVI格式档案。C725标清采集卡随附的软件开发工具包(SDK)提供常用功能,能帮助开发者或系统整合商轻松且有效率地完成工作。此外,这套 SDK可兼容于Visual C++和 Visual Basic等主流程序语言,让开发者可更轻易上手。
官方声称支持Linux,而且驱动程序需要向其索取,网站不提供下载。然而,AverMedia C725是否真的可以在自己的Debian Wheezy(3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux)上工作就不好说了,只好冒点风险试一下。
在安装好AverMedia PCI卡后,首先向官方联系取得驱动程序。需要注意的是,一定要获得与自己的Linux内核版本一致的驱动,否则编译极有可能不通过。我自己就是在尝试了官方默认提供的适用于老版本内核的驱动失败后,才要求圆刚的技术人员重新为自己编译了一个适用于Linux 3.2.51的版本。有了官方提供的驱动,再安装linux-source包与linux-headers包,就可以开始编译驱动了。首先,需要导出环境变量C_INCLUDE_PATH,其中包含了linux-source提供的dvb相关文件:
export C_INCLUDE_PATH=/usr/include/:/usr/src/linux-source-3.2/drivers/media/dvb/dvb-core/:/usr/src/linux-source-3.2/drivers/media/dvb/frontends/
之后的编译过程就是通常的make三步曲,一切都很顺利。
有了硬件基础后,电视播放软件我选择使用mplayer,录制视频则是mencoder。由于命令行所需的参数很多,我写了一个脚本程序rtv.sh,可以方便的播放或录制节目。通过指定命令行参数watch、nowatch、onlywatch,该脚本可以在三种模式下运行:
- watch:在播放电视的同时,将视频存为avi文件;
- nowatch:只录制节目存为avi文件,不实时播放;
- onlywatch:只用mplayer看节目而不录制。
该脚本用到了如下几个程序:
- mencoder:从视频采集卡以指定的格式获取视频流并将其输出到一个名称FIFO管道(named FIFO pipe);
- tee:从标准输入获得数据流再将其转向至标准输出。利用管道行,可以将名称FIFO管道的内容作为tee命令的标准输入,而tee命令的标准输出作为mplayer程序的标准输入予以播放。同时,tee命令可以使用命令行参数-a filename。这样,在将数据流送至标准输出乃至mplayer的同时,也会将其保存至指定的文件。利用这个机制,就可以实现同时播放并录制电视节目了;
- mplayer:播放由tee转来的视频流。
脚本程序的执行过程是:首先检测用户的命令行参数输入,然后查看是否已有mencoder、tee、mplayer进程运行。若存在,则表明已经在播放或录制节目了,从而提示用户后退出;否则,执行如下的流程:
- 首先,使用mkfifo创建名称FIFO管道/tmp/tv.fifo;
- 其次,用cat命令输出该管道的内容至标准输出,利用管道行将该输出作为tee命令的标准输入。在录制与播放的模式下(watch),将其分为两路,一路送至avi文件,一路送至mplayer用于播放;在只录制不播放的模式下(nowatch),只输出至avi文件;在纯播放模式下(onlywatch),只送至mplayer。
- 最后,用mencoder从AverMedia C725B采集卡在系统中的设备节点/dev/video0截取视频流,以指定的格式输出到之前创建的名称管道。
脚本程序的源代码为:
#!/bin/bash
script_name="rtv.sh"
script_usage=$(cat <<EOF
rtv MODE [FILE]
EOF
)
script_function=$(cat <<EOF
Record or watch TV. MODE can be 'watch', 'onlywatch', 'nowatch'. When 'watch' or 'nowatch' is specified, the file name must be provided.
EOF
)
script_doc=$(cat <<EOF
-h Display this help.
EOF
)
script_examples=$(cat <<EOF
rtv nowatch test.avi
rtv watch test.avi
rtv onlywatch
EOF
)
state_prefix="==="
warning_prefix="***"
error_prefix="!!!"
function display_help() {
if [ -n "$script_usage" ]; then
echo -e "Usage: $script_usage"
fi
if [ -n "$script_function" ]; then
echo -e "$script_function"
fi
if [ -n "$script_doc" ] ; then
echo -e "\n$script_doc"
fi
if [ -n "$script_examples" ]; then
echo -e "\nExamples"
echo -e "$script_examples"
fi
}
# Process command options
while getopts ":h" opt; do
case $opt in
h ) display_help
exit 0 ;;
\? ) display_help
exit 1 ;;
esac
done
shift $(($OPTIND - 1))
# Define a function for returning a process id
function get_pid_by_name()
{
local process_str
echo "Searching process $1..."
process_str=`ps aux | grep "$1" | tr --squeeze-repeats '[:blank:]+' '\t' | cut -f 2`
if [ -n "$process_str" ]; then
# The process for grep appears in the second field
process_str=`echo $process_str | cut -s -d ' ' -f 1`
if [ -n "$process_str" ]; then
temp_pid=$process_str
echo "The process id is $temp_pid!"
else
echo "The process $1 cannot be found, perfect!"
fi
else
echo "The process $1 cannot be found, perfect!"
fi
}
# Start execute the command
if [ $OSTYPE = 'linux-gnu' ] && [ `hostname` = "QuantumHome" ]; then
if [ -z "$1" ]; then
echo "$error_prefix Please specify the recording and watching mode: watch|nowatch|onlywatch"
exit 0
fi
if [ "$1" != "onlywatch" ] && [ -z "$2" ]; then
echo "$error_prefix Please provide the video file name to be saved!"
exit 0
fi
# Declare the pid as integers
declare -i temp_pid=-1 mplayer_pid=-1 mencoder_pid=-1 tee_pid=-1
get_pid_by_name mencoder
mencoder_pid=$temp_pid
temp_pid=-1
get_pid_by_name tee
tee_pid=$temp_pid
temp_pid=-1
get_pid_by_name mplayer
mplayer_pid=$temp_pid
temp_pid=-1
if [ $(($mencoder_pid!=-1 && $mplayer_pid!=-1 && $tee_pid!=-1)) = 1 ]; then
echo "$error_prefix A tv recording or watching activity is now working, please exit it first!"
exit 0
fi
# Create FIFO named pipe
if [ ! -e "/tmp/tv.fifo" ]; then
echo "$state_prefix FIFO does not exist, now being created..."
mkfifo /tmp/tv.fifo && echo "$state_prefix Creating FIFO successful!"
fi
# Start tee and mplayer
case "$1" in
watch ) echo "$state_prefix Start recording tv and watch it using mplayer..."
# Note: sudo must be used in order to make mplayer appear
cat /tmp/tv.fifo | tee -a "${2%.avi}.avi" | sudo -u orlando DISPLAY=:0.0 mplayer -cache 51200 -framedrop -ao sdl -vo xv - & ;;
nowatch ) echo "$state_prefix Start recording tv without watching it..."
cat /tmp/tv.fifo | tee -a "${2%.avi}.avi" & ;;
onlywatch ) echo "$state_prefix Start watching tv without recording it..."
# Note: "tee -a -" will not work here
cat /tmp/tv.fifo | tee | sudo -u orlando DISPLAY=:0.0 mplayer -cache 51200 -framedrop -ao sdl -vo xv - & ;;
* ) echo "$error_prefix Please specify the recording and watching mode: watch|nowatch|onlywatch"
exit 0;
esac
# Start mencoder to feed the video stream into FIFO
echo "$state_prefix Now start mencoder to capture tv..."
mencoder tv:// -tv driver=v4l2:device=/dev/video0:norm=PAL:alsa:adevice=hw.2,0:amode=1:audiorate=48000:forceaudio:volume=100:immediatemode=0:normid=8:input=1:buffersize=1024:width=768:height=576:outfmt=i420 -oac mp3lame -lameopts fast:preset=standard -ovc lavc -lavcopts vcodec=mpeg4:vhq:vbitrate=1800 -o /tmp/tv.fifo
else
echo "$warning_prefix Operating system or host name is not supported!"
fi
该脚本程序中,mencoder的参数较为复杂,这里对其解释如下:
- driver=v4l2:使用video for linux驱动;
- device=/dev/video0:视频卡设备节点。只有当驱动安装正确后,才会出现该节点;
- norm=PAL:指定模拟电视制式,中国为PAL;
- alsa:从ALSA捕获音频;
- adevice=hw.2,0:指定音频设备。当使用OSS音频系统时,adevice的值为/dev/xxx,当使用ALSA音频系统时,则为形如hw:2,0的硬件ID。需注意的是,由于命令行不能出现冒号,应将hw:2,0中的冒号改为逗号;
- amode=1:指定声音模式。0为单声道,1为立体声;
- audiorate=48000:输入声音采样率;
- forceaudio:强制采集音频,即便v4l库未发现有音频源;
- volume=100:指定视频采集卡上混频器的音量;
- immediatemode=0:0表示同时采集与缓存音频与视频流(mencoder的默认值),1表示只采集视频流,而音频流则会通过回环线路由电视卡送至声卡(mplayer的默认值)。由于我使用的是mencoder,所以设为0;
- normid=8:采集卡的电视标准编号;
- buffersize=1024:采集缓冲区的大小,单位为MB
- width=768:height=576:输出视频像素宽度与高度;
- outfmt=i420:输出视频数据格式;
- -oac mp3lame:指定音频编码器为mp3lame;
- -lameopts fast:preset=standard:指定音频编码器的选项;
- -ovc lavc:指定视频编码器为libavcodec;
- -lavcopts vcodec=mpeg4:vhq:vbitrate=1800:指定视频编码器选项。
需要说明的是,由rtv.sh录制的avi文件没有索引,所以直接用mplayer播放无法快进与快退。为此,可以用下面的脚本程序自动生成索引后再播放(其中,参数$1为avi视频文件名):
#!/bin/bash mplayer -forceidx -saveidx "${1%avi}idx" "$1"
若想回放已经录制过且生成了索引的视频,则可以使用下面的脚本程序(其中,参数$1为avi视频文件名):
#!/bin/bash mplayer -loadidx "${1%avi}idx" "$1"
到这里,用于播放与录制电视节目的脚本程序rtv.sh就介绍完了。下面再来介绍用于停止播放或录制的脚本程序stop_rtv.sh。这个就比较简单了,无非就是杀死相应的进程而已。源代码如下:
#!/bin/bash # Define a function for returning a process id function get_pid_by_name() { local process_str echo "Searching process $1..." process_str=`ps aux | grep "$1" | tr --squeeze-repeats '[:blank:]+' '\t' | cut -f 2` if [ -n "$process_str" ]; then # The process for grep appears in the second field process_str=`echo $process_str | cut -s -d ' ' -f 1` if [ -n "$process_str" ]; then temp_pid=$process_str echo "The process id is $temp_pid!" else echo "The process $1 cannot be found, perfect!" fi else echo "The process $1 cannot be found, perfect!" fi } # Declare pid as integers declare -i temp_pid=-1 mplayer_pid=-1 mencoder_pid=-1 tee_pid=-1 # Kill mencoder process get_pid_by_name mencoder mencoder_pid=$temp_pid temp_pid=-1 if [ $(($mencoder_pid!=-1)) = 1 ]; then # The SIGINT has no effect on mencoder processes while SIGKILL will cause loss of /dev/video0 node kill -2 $mencoder_pid && echo "mencoder has been killed!" else echo "mencoder process does not exist!" fi # Kill tee process get_pid_by_name tee tee_pid=$temp_pid temp_pid=-1 if [ $(($tee_pid!=-1)) = 1 ]; then kill -2 $tee_pid && echo "tee has been killed!" else echo "tee process does not exist!" fi # Kill mplayer process if not in nowatch mode if [ "$1" != "nowatch" ]; then get_pid_by_name mplayer mplayer_pid=$temp_pid temp_pid=-1 if [ $(($mplayer_pid!=-1)) = 1 ]; then # Note: mplayer is started by using sudo, therefore when killing it, sudo should also be used sudo -u orlando kill -2 $mplayer_pid && echo "mplayer has been killed!" else echo "mplayer process does not exist!" fi fi echo "TV recording and playing have been stopped!"
有了rtv.sh与stop_rtv.sh两个脚本,再将其与at命令结合,则可以实现定时录制与播放节目了。例如:
$ at 20:00 today warning: commands will be executed using /bin/sh at> rtv watch 我爱发明 at> <EOT> # Input Ctrl+D job 21 at Wed Feb 5 20:00:00 2014 $ at 21:00 today warning: commands will be executed using /bin/sh at> stop_rtv watch at> <EOT> # Input Ctrl+D job 22 at Wed Feb 5 21:00:00 2014
由于晚上播出的电视节目大部分在第二天白天会重播,因此在自己上班的同时,这些节目便可以按计划一个不落地录下来。同时,原本需要晚上熬夜看的节目也可以保存起来等到第二天再看。
还有一个问题就是,电视卡是插在台式机上的,所以只能在这台电脑上观看电视节目。那么自己在厨房做饭的时候怎么看电视呢?比如晚上7点的新闻联播?由于自己有iPhone,实现这个功能就不难了。首先,在电脑上运行rtv.sh程序(rtv.sh nowatch filename.avi),将电视节目录到文件中。然后,将文件所在的目录共享到 Samba服务器上。最后,用iPhone上的视频播放器OPlayer访问该服务器并在线播放即可。看到的节目会稍微有一点延迟,不过也没有什么太大的关系。下图就是iPhone上看到的电视节目截图: