视频的显示同步

八月 31st, 2008

视频的同步通过视频流的包里的dts(decoding time stamp)和pts ( presentation time stamp )
信息来实现。
视频有三种帧。I(intra)帧为静态图像可以直接解码。P(predicted)帧基于之前的帧通过运动估计形成的图像和实际图像的残差,解码需依赖之前的帧。B(bidirectional)帧基于之前和之后的帧运动估计得到的残差,解码依赖之前和之后的帧。
如果有一段电影由I B P P帧的顺序组成。第一帧I帧可以直接解码,但第二和第三帧是B帧依赖于第四帧P帧的信息才能解码,为了方便解码在编码的时候就直接以I P B B的顺序存储了。所以我们获取的AVStream的帧顺序是I P B B,这是DTS即解码的顺序,但这样用这个顺序来显示显然是不行的,好在每个帧都有PTS来表明显示顺序,它会告诉我们先缓存几帧用I B B P的顺序来显示。

Mplayer笔记

八月 24th, 2008

看一下电脑上存的电影文件,后缀名为AVI,rmvb,mkv的比较多,Windows根据这些扩展名来显示电影文件的图标以及当用户双击文件时用什么播放器来播放,跟视频采用的编码方式无关,只是影音文件在磁盘中的存放方式,专业一点叫做Container,用来把一个或多个视频音频字幕等放到同一个文件中。

AVI是微软提出的Container,因为在文件大小和对先进编码器的支持上存在缺陷,慢慢开始被遗弃,但目前仍是一种非常普遍的格式。微软准备用ASF/WMV来替代他。但更被看好的是另一种开放的Container标准MKV,http://en.wikipedia.org/wiki/Matroska。当然rmvb是Real Media公司的Container。有一个叫MediaInfo的软件可以查看Container中的具体信息。

因此要播放视频文件的第一步就是把文件中包含的视频音频分离出来(demux),然后才能解码。Matroska Muxer等工具可以用来将多个音频轨道、视频轨道、字幕等合并成一个单独的Container文件。与之对应的分离出音频视频的工作是由分离器 Splitter来做,如Haali Media Splitter ,而Mplayer则由一堆Demux_*文件来处理各种不同的Container。Mencoder.exe用Muxer*文件来生成不同的 Container。
http://www.ogg.cn/
http://www.matroska.org/downloads/windows.html

Mplayer的内置解码器一般在vd_*文件中定义,只能被Mplayer使用,外置解码器是通过在Windows注册表登记,而可被所有需要视频解码功能的程序使用的解码器,如用regsvr32 /s ffdshow.ax注册后,系统内置的Media Player也可以通过调用ffdshow支持大部分视频格式了。DirectX SDK中的GraphEdit可用来查看系统中注册的解码器。

解码后的视频通过VO输出,Windows推荐directx,Linux推荐xv。可用mplayer -vo help查看支持的vo。

在vo输出之前,可以设置各种滤镜(filter)来操作视频,如scale滤镜可用来放大缩小视频,screenshot用来截屏。在mplayer的源码中实现这种功能的文件用vf_开头。mplayer甚至可以直接把解码后的yuv文件写到磁盘。

MP另外一个比较有用的功能是Software Volume adjustment,即用软件的方式增大音量,这用来处理那些音量调到最大还不能听清的影片比较有用。

mplayer quiet.avi -softvol -softvol-max 300  3倍音量。

Windows下编译MPlayer可用mingw。

http://oss.netfarm.it/mplayer-win32.php

http://forum.doom9.org/

Mplayer代码阅读

八月 21st, 2008

从Mplayer.c的main开始

处理参数
mconfig = m_config_new();
m_config_register_options(mconfig,mplayer_opts);
// TODO : add something to let modules register their options
mp_input_register_options(mconfig);
parse_cfgfiles(mconfig);
初始化mpctx结构体,mpctx应该是mplayer context的意思,顾名思义是一个统筹全局的变量。
static MPContext *mpctx = &mpctx_s;
// Not all functions in mplayer.c take the context as an argument yet
static MPContext mpctx_s = {
.osd_function = OSD_PLAY,
.begin_skip = MP_NOPTS_VALUE,
.play_tree_step = 1,
.global_sub_pos = -1,
.set_of_sub_pos = -1,
.file_format = DEMUXER_TYPE_UNKNOWN,
.loop_times = -1,
#ifdef HAS_DVBIN_SUPPORT
.last_dvb_step = 1,
#endif
};

原型
typedef struct MPContext {
int osd_show_percentage;
int osd_function;
ao_functions_t *audio_out;
play_tree_t *playtree;
play_tree_iter_t *playtree_iter;
int eof;
int play_tree_step;
int loop_times;

stream_t *stream;
demuxer_t *demuxer;
sh_audio_t *sh_audio;
sh_video_t *sh_video;
demux_stream_t *d_audio;
demux_stream_t *d_video;
demux_stream_t *d_sub;
mixer_t mixer;
vo_functions_t *video_out;
// Frames buffered in the vo ready to flip. Currently always 0 or 1.
// This is really a vo variable but currently there’s no suitable vo
// struct.
int num_buffered_frames;

// AV sync: the next frame should be shown when the audio out has this
// much (in seconds) buffered data left. Increased when more data is
// written to the ao, decreased when moving to the next frame.
// In the audio-only case used as a timer since the last seek
// by the audio CPU usage meter.
double delay;

float begin_skip; ///< start time of the current skip while on edlout mode
// audio is muted if either EDL or user activates mute
short edl_muted; ///< Stores whether EDL is currently in muted mode.
short user_muted; ///< Stores whether user wanted muted mode.

int global_sub_size; // this encompasses all subtitle sources
int global_sub_pos; // this encompasses all subtitle sources
int set_of_sub_pos;
int set_of_sub_size;
int global_sub_indices[SUB_SOURCES];

一些GUI相关的操作
打开字幕流
打开音视频流
mpctx->stream=open_stream(filename,0,&mpctx->file_format);
fileformat文件还是TV流DEMUXER_TYPE_PLAYLIST或DEMUXER_TYPE_UNKNOWN DEMUXER_TYPE_TV
current_module记录状态vobsub  open_stream handle_playlist dumpstream

stream_reset(mpctx->stream);
stream_seek(mpctx->stream,mpctx->stream->start_pos);
f=fopen(stream_dump_name,”wb”); dump文件流

stream->type==STREAMTYPE_DVD

//============ Open DEMUXERS — DETECT file type ======================
Demux。分离视频流和音频流
mpctx->demuxer=demux_open(mpctx->stream,mpctx->file_format,audio_id,video_id,dvdsub_id,filename);

Demux过程
demux_open
get_demuxer_type_from_name
……

mpctx->d_audio=mpctx->demuxer->audio;
mpctx->d_video=mpctx->demuxer->video;
mpctx->d_sub=mpctx->demuxer->sub;

mpctx->sh_audio=mpctx->d_audio->sh;
mpctx->sh_video=mpctx->d_video->sh;

分离了之后就开始分别Play audio和video
这里只关心play video

/*========================== PLAY VIDEO ============================*/

vo_pts=mpctx->sh_video->timer*90000.0;
vo_fps=mpctx->sh_video->fps;

if (!mpctx->num_buffered_frames) {
double frame_time = update_video(&blit_frame);
mp_dbg(MSGT_AVSYNC,MSGL_DBG2,”*** ftime=%5.3f ***\n”,frame_time);
if (mpctx->sh_video->vf_inited < 0) {
mp_msg(MSGT_CPLAYER,MSGL_FATAL, MSGTR_NotInitializeVOPorVO);
mpctx->eof = 1; goto goto_next_file;
}
if (frame_time < 0)
mpctx->eof = 1;
else {
// might return with !eof && !blit_frame if !correct_pts
mpctx->num_buffered_frames += blit_frame;
time_frame += frame_time / playback_speed;  // for nosound
}
}
关键的函数是update_video
根据pts是否正确调整一下同步并在必要的时候丢帧处理。
最终调用decode_video开始解码(包括generate_video_frame里)。
mpi = mpvdec->decode(sh_video, start, in_size, drop_frame);
mpvdec是在main里通过reinit_video_chain的一系列调用动态选定的解码程序。
其实就一结构体。它的原型是
typedef struct vd_functions_s
{
vd_info_t *info;
int (*init)(sh_video_t *sh);
void (*uninit)(sh_video_t *sh);
int (*control)(sh_video_t *sh,int cmd,void* arg, …);
mp_image_t* (*decode)(sh_video_t *sh,void* data,int len,int flags);
} vd_functions_t;
这是所有解码器必须实现的接口。
int (*init)(sh_video_t *sh);是一个名为init的指针,指向一个接受sh_video_t *类型参数,并返回int类型值的函数地址。
那些vd_开头的文件都是解码相关的。随便打开一个vd文件以上几个函数和info变量肯定都包含了。

mpi被mplayer用来存储解码后的图像。在mp_image.h里定义。
typedef struct mp_image_s {
unsigned short flags;
unsigned char type;
unsigned char bpp;  // bits/pixel. NOT depth! for RGB it will be n*8
unsigned int imgfmt;
int width,height;  // stored dimensions
int x,y,w,h;  // visible dimensions
unsigned char* planes[MP_MAX_PLANES];
int stride[MP_MAX_PLANES];
char * qscale;
int qstride;
int pict_type; // 0->unknown, 1->I, 2->P, 3->B
int fields;
int qscale_type; // 0->mpeg1/4/h263, 1->mpeg2
int num_planes;
/* these are only used by planar formats Y,U(Cb),V(Cr) */
int chroma_width;
int chroma_height;
int chroma_x_shift; // horizontal
int chroma_y_shift; // vertical
/* for private use by filter or vo driver (to store buffer id or dmpi) */
void* priv;
} mp_image_t;

图像在解码以后会输出到显示器,mplayer本来就是一个视频播放器么。但也有可能作为输入提供给编码器进行二次编码,MP附带的mencoder.exe就是专门用来编码的。在这之前可以定义filter对图像进行处理,以实现各种效果。所有以vf_开头的文件,都是这样的filter。

图像的显示是通过vo,即video out来实现的。解码器只负责把解码完成的帧传给vo,怎样显示就不用管了。这也是平台相关性最大的部分,单独分出来的好处是不言而喻的,像在Windows下有通过direcx实现的vo,Linux下有输出到X的vo。vo_*文件是各种不同的vo实现,只是他们不都是以显示为目的,像vo_md5sum.c只是计算一下图像的md5值。

在解码完成以后,即得到mpi以后,filter_video被调用,其结果是整个filter链上的所有filter都被调用了一遍,包括最后的VO,在vo的put_image里把图像输出到显示器。这个时候需要考虑的是图像存储的方法即用哪种色彩空间。

[MPlayer core]
| (1)
_____V______   (2)  /~~~~~~~~~~\    (3,4)  |~~~~~~|
|          | —–> | vd_XXX.c |  ——-> | vd.c |
| decvideo |        \__________/  <-(3a)– |______|
|          | —–,  ,………….(3a,4a)…..:
~~~~~~~~~~~~  (6) V  V
/~~~~~~~~\     /~~~~~~~~\  (8)
| vf_X.c | –> | vf_Y.c | —->  vf_vo.c / ve_XXX.c
\________/     \________/
|              ^
(7) |   |~~~~~~|   : (7a)
`-> | vf.c |…:
|______|

感觉Mplayer的开发人员们都是无比的牛,硬是用原始的C实现了很多OO语言才支持的特性,带来不好的结果是代码看起来比较费劲,记下来慢慢看先。

一幅图片是由一个个的像素点组成的,作为有色光的三基色,红绿蓝(RGB)可以以一定的比例混合出任何颜色,所以一幅彩色图片的每个像素都可以用RGB三个分量来表现自己,常见的每个分量用8位,这样一个像素需要3×8=24位,而一幅图片就是一个每元素24位的二维数组。但是研究发现人眼对颜色的敏感比不上对亮度的敏感,因此可以将亮度分量从色度分量中提取出来而以较少的位来表示色度信息,从而更加有效的表示彩色空间。

因此与RGB相对,另一种颜色空间为Y:Cr:Cb,这里Y为亮度分类,即一幅彩色图片的黑白版本,由RGB加权平均得出。Cr,Cb是R,B与Y之差,为色差信息,因Cr+Cb+Cg为常数,所以Cg无需传输。

RGB与Y:Cr:Cb转换公式
Y=0.299R+0.587G+0.114B     Cb=0.564(B-Y)      Cr=0.713(R-Y)
R=Y+1.402Cr     G=Y-0.344Cb-0.714Cr         B=Y+1.772Cb

利用人眼对亮度敏感的优势,在一幅彩色图片中,我们可以交叉地使一些点只有亮度分量没有色度分量,而不被人眼察觉。

4:2:2采样(YUY2):对于图像数组每隔一列只采样Y分量。
4:2:0采样(YV12):每隔一列和一行只采样Y分量。

因此4:2:0采样的图像只有1/4的像素是24位的,其余3/4的像素只有Y分量的8位,即平均每像素1/4×24+3/4×8=12位,这也是为什么它在FOURCC代码中的表示为YV12。

一个视频编码器的主要功能有:
1,滤波。消除视频源中的干扰信号。
2,源模型编码。把视频变换成一种易于压缩的格式。
3,熵编码。无损压缩上一步得到的数据。

最关键的是第二步了。在这里视频编码被分为帧内编码(Intra-frame Coding)和帧间编码(Inter-frame coding),视频无非是一张张连续的图片,帧内编码就相当于压缩一张图片,因为组成一段视频的图片有一定的相似性,据此我们可以只完全编码压缩某一帧图片,该帧称为I帧,其后的帧只保存那一帧与I帧或基于I帧的某种运动预测后的图片的差别,如果是连续的场景,物体的运动预测又足够准确的话,这种差别是很小的,因此我们只需保存很小的数据量就能恢复出原图像。运动预测的一般方法:把当前帧的某个区块向各个方向移动几格,并与下一帧的对应区块进行比较(通过计算两个图像数组的均方差),保留均方差最小的那个方向做为运动矢量,用于解编码。这个过程会用到各种不同的搜索方式和大量的比较运算,是衡量一个编码器好坏的关键。

编码步骤
变换编码。因为一幅图像的各个部分的空间相关性很高,不利于直接压缩。变换编码就是把图像用一种各部分相关性弱的形式表示,这样就可以去掉一些不重要的点,来进行有损压缩。其实质就是一个24位二进制数的二维数组,从一个各值很平均的形式变到另一种各值落差很大的形式,当然这个过程应该是可逆的。通常落差很大意味着有很多值在零值附近,通过量化就可以变成一个有很多零值的稀疏矩阵。
量化过程可以理解为把数组中所有的值除以一个数N,这样小于N的数就变成了0,而那些大数需要被传输的位数也降低了,其恢复过程为整个数组乘以N,那些在除法过程中被舍弃的位数就成了不可恢复的误差了。
常见的变换编码有DCT和小波变换。
熵编码,根据统计信息将频繁出现的符号用较少的比特表示,不经常出现的符号用较多的比特表示,进而达到对数据压缩的目的,而在此之前往往会通过游程编码重新排列数组,以使更多的零排在一起。这些排在一起的零将通过(run,level)编码,用一个符号表示。
显然最直接的熵编码算法是哈夫曼编码,但实际应用中的熵编码会更加复杂。

参考书籍:Iain E.G.Richardson 视频编解码器设计