今天就对我实现的
linux
下的
tv
播放器进行一个阶段性的总结吧,目前我的这个
tv
测试程序已经能够实现流畅播放电视,在更改视频帧缓冲设置的情况下录制效果也还不错,并且实现了频道调整和电视频道回放的功能;另外,底层电视库中也增加了亮度、对比度、饱和度、色调调整的代码,以便将来在上层
GUI
界面中添加对应的设置功能;
这次我先比较详细的说明电视播放和录制的原理和关键代码的实现,两个部分就不拆开了,算作是晚交文章的一个惩罚吧,录制部分就算作是补偿的那一篇啦。
要想弄清楚电视播放和录制的原理,首先得搞清楚电视播放和录制时,数据帧流的流向和数据的存储是如何完成的。
下面我就先介绍一下电视播放时视频帧流的流向:
从视频设备/dev/video采集到得视频数据帧
-->
送到buffer存储
-->
1、(经过YUV420P到RGB32得转换)显示;
2、如果是录制状态,则对视频帧进行压缩,准备存储;同时要从音频设备中/dev/dsp中读取音频数据,我这里采用得是压缩存储,则需要经过mp3压缩,准备存储;
-->
1、显示部分,RGB32格式得视频帧可以直接使用QT得函数进行显示,如果你是采用SDL进行显示,那前面得RGB32格式转换都可以省略了,SDL可以直接显示YUV格式得视频帧;
2、录制状态中,在选定存储得文件格式(比如AVI)后,就进行音频和视频得数据存储了;
电视的播放和录制我想到这里你会有一个大概的数据流的概念了,这样,在以后的编码中就是以此数据流为基础,充分利用计算机的各种已有东西,进行编码实现这种理念;
下面我开始结合具体代码和文件结构进行详细说明:
首先分析几个技术难点,对于视频帧的采集、显示、压缩存储,在录制状态下这几样是同时进行的,这种并行计算在计算机中可以采用线程来实现,(由于进程消耗资源比较大,不可取),这样加上主GUI线程,在录制状态下,同时会有四个线程在运行;
其次,对 于数据帧的存储,我这里采用的方法是使用一个内存视频帧缓冲循环队列,视频帧的显示和录制都是从这个队列当中读取视频帧,而采集视频帧的线程则是将视频帧 写入这个队列中;当然了,这期间需要注意的是尽量使用指针而避免采用大量的内存拷贝操作,以便提高性能;至于具体如何操作,后面会进行详述;
最后,在视频进行全屏的实现过程中,至少要经过三个步骤--对视频帧进行全屏大小的转换,进行YUV到RGB32的格式转换,最后进行GUI界面刷屏显示;这三个步骤中前两步是比较耗资源的,我现在实现的tv测试程序,仍然不能够很好的解决电视在进行全屏播放的视频帧刷新缓慢问题,这也是当前的一个突出的难点;
有关这个问题的解决方案,我目前是打算参考mplayer的实现全屏的方法,只是mplayer全屏实现代码太散,目前还没有找到;
好,下面我先说说我的测试程序的文件夹结构,整个程序包括以下几个文件夹--bin、common、doc、gui、hdware、interface、image,最主要的是gui、hdware和interface三个文件夹中的东西,顾名思义,gui主要包含了和界面显示以及tv视频帧显示的代码部分,而hdware则是与硬件相关比较紧密的部分,我在这里放了电视的底层实现库,interface则是gui和hdware之间的接口部分,主要是线程的C++实现和缓冲类以及需要的几个线程类的代码;文件结构如下图所示:
// ------------------------------------------------
# ls gui
Qtgui // 之所以有这个qt的gui文件夹,是因为以后可能还需要写其他界面的界面部分,所以这里留出扩展的余地,以方便将来进行扩展;
# find gui/qtgui
gui/qtgui/
gui/qtgui/mainwindow.cpp // 主窗口类文件;
gui/qtgui/mainwindow.h
gui/qtgui/mainwindow.moc
gui/qtgui/tvwindow.moc // 显示tv的窗口类;
gui/qtgui/tvwindow.cpp
gui/qtgui/tvwindow.h
gui/qtgui/mainwindow.o
gui/qtgui/tvwindow.o
gui/qtgui/main.cpp // 主函数;
gui/qtgui/Makefile
gui/qtgui/qttv.o
gui/qtgui/qtwindow.a // 打包成库;
// ------------------------------------------------
# ls hdware
video
# ls hdware/video/tv
hdware/video/tv/
hdware/video/tv/tv.bak.c
hdware/video/tv/tv.c // tv底层实现库;
hdware/video/tv/tv.h
hdware/video/tv/tv.o
hdware/video/tv/Makefile
hdware/video/tv/tv.a // 打包成库;方便以后将这部分单独拿出来独立成库;
// ------------------------------------------------
# find interface
interface/
interface/thread
interface/thread/Makefile
interface/thread/mthread.cpp // 一个C++封装的线程类,使用方法类似java中的线程类;
interface/thread/mthread.o
interface/thread/thread.a // 打包成库;
interface/thread/mthread.h
interface/Makefile
interface/tvgrabthread.cpp // 从mthread类继承,实现采集视频功能;
interface/tvgrabthread.h
interface/tvgrabthread.o
interface/tvrecordthread.o
interface/tvrecordthread.cpp // 从mthread类继承,实现录制视频功能;
interface/tvrecordthread.h
interface/tvthread.a // 打包成库;
interface/buffer
interface/buffer/videobuffer.o
interface/buffer/videobuffer.h
interface/buffer/Makefile
interface/buffer/buffer.a // 打包成库;
interface/buffer/videobuffer.cpp // 视频帧缓冲类;
其实看了这些,我想你对具体那一部分是做什么的,以及如何编写已经有了大概的认识了,下面我会对一些关键点进行具体的说明;
一共两个关键点--播放和录制;
具体涉及到的技术点一共有四个,分别是:采集视频帧线程、显示视频帧线程、录制视频帧线程、视频帧缓冲队列;
下面我就分别针对这几个技术关键点进行详细说明;
电视播放--
采集视频帧线程
视频帧采集采用线程实现的原因是为了能够提高GUI界面的响应速度,这一点我想大家都是毋庸置疑的。
详细描述:
此线程主要完成从Linux下视频设备/dev/video中读取视频帧,并将其保存在视频帧队列中(详情请看下一条说明);本线程是继承自一个线程类, 这个线程类是自己编写的,特性和使用方法都可以参考java中的线程类;因此,本线程最关键的地方也就是重新实现线程的run方法,这里我将run中关键 代码贴出来,并把加上详细注释;
Void LtvGrabThread::run(){
//
AVFrame *pf_frameYUV420P = NULL;
// Allocate video frame
pm_frame = avcodec_alloc_frame();
pm_frame_YUV420P = pm_frame;
// Read frames
while ( m_tv_playing_flag ){
// while tv is playing, read one frame from packets;
if ( av_read_frame (pm_format_context, &f_packet) < 0 ){
continue;
}
// Is this a packet from the video stream?
if (f_packet.stream_index == m_video_stream)
{
// Decode video frame
avcodec_decode_video (pm_codec_context, pm_frame,
&f_frameFinished, f_packet.data,
f_packet.size);
// Did we get a video frame?
if (f_frameFinished)
{
/*
* 这里不得不用中文说明一些东西:之所以要从原始格式转换成YUV420P,是因为这是一个最通用的格式,
* 无论你是要显示还是要做转换,还是进行压缩,这都是一个中转的格式,所以,这里一定要转换成YUV420P格式;
* 经过测试,这里读取出来的视频帧正是YUV420P格式,因此目前先去除这一步,以后在进行完善;
*/
/* Convert the image from its native format to YUV420P, and
* transmit its address to other threads to display or record;
*/
// img_convert ((AVPicture *) pm_frame_YUV420P, /
PIX_FMT_YUV420P, /
(AVPicture *) pm_frame, /
pm_codec_context->pix_fmt, /
pm_codec_context->width, /
pm_codec_context->height);
/*
* 从buffer中读取指定m_video_buffer_index的buffer节点中的帧,如果帧为空,则为其分配对应的空间,
* 然后将前面转换过的帧直接copy到这个临时帧pf_frameYUV420P上,然后保存此帧的指针地址到buffer中;
* 这里有必要说明的一点是:为什么要采取copy的方式,主要是从整体性能的角度考虑,
* 另外:这里的缓冲节点的读写也都是尽量使用指针读写,以避免大量的内存操作,而影响整体性能;
*/
if ( video_buffer_read(pm_tv->pm_video_buffer, /
m_video_buffer_index, &pf_frameYUV420P) == 0 ){
// read one buffer from video buffers queue;
if ( pf_frameYUV420P == NULL ){
// if the buffer node is NULL, then allocate memory to it;
// Determine required buffer size and allocate buffer
pf_frameYUV420P = avcodec_alloc_frame ();
// allocate picture memory
f_numBytes = avpicture_get_size (PIX_FMT_YUV420P, pm_codec_context->width, /
pm_codec_context->height);
pm_buffer = new uint8_t[f_numBytes];
// Assign appropriate parts of buffer to image planes in pm_frame_YUV420P
int rtbytes = avpicture_fill ((AVPicture *) pf_frameYUV420P, pm_buffer, /
PIX_FMT_YUV420P, /
pm_codec_context->width, pm_codec_context->height);
// then add pf_frameYUV420P into the buffer with index m_video_buffer_index;
video_buffer_write(pm_tv->pm_video_buffer, m_video_buffer_index, /
pf_frameYUV420P);
}// if null then allocate memory;
}
// lock();
thread_wrlock(pm_tv, m_video_buffer_index);
if ( pm_tv->m_deinterlace ){
// deinterlace the frame;
if (avpicture_deinterlace((AVPicture *)pf_frameYUV420P, /
(AVPicture *)pm_frame_YUV420P, PIX_FMT_YUV420P, /
pm_codec_context->width, pm_codec_context->height) < 0){
cerr << "avpicture_deinterlace error" << endl;
}
}
else{
// copy src frame to dst frame buffer;
img_copy((AVPicture *)pf_frameYUV420P, (AVPicture *)pm_frame_YUV420P, /
PIX_FMT_YUV420P, pm_codec_context->width, pm_codec_context->height);
}
thread_unlock(pm_tv, m_video_buffer_index);
// adjust the index of current frame become next in buffer;
m_video_buffer_index ++;
if ( m_video_buffer_index >= m_video_buffer_size ){
m_video_buffer_index = 0;
}
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet (&f_packet);
}
// Free the YUV image
av_free (pm_frame_YUV420P);
// Free the YUV frame
av_free (pm_frame);
}
注意:
这里有两点需要着重提一下:
一、是这里的视频帧队列中存储的视频帧格式均为YUV420P格式的,这样做有两点好处,第一,此格式的视频帧可以进行对其他格式的转换工作;第二,此格式如若能够直接显示,则能够减少从YUV到RGB的转换这一步,这是对提高电视的播放性能起到很关键的一步;
二、采用avpicture_deinterlace进行deinterlace处理,去除运动图象锯齿效果,提高视频显示效果;
视频帧缓冲队列
采用视频帧队列的最初目标或目的是为了提高在电视录制时的电视播放性能,电视播放是对视频帧队列的读取操作,视频帧的抓取则是对视频帧队列的写操作,而电 视录制则也是对视频帧队列的读操作,如果视频帧队列长度为1,也就等价于没有使用视频帧队列,此时,由于读写冲突,可能会出现卡帧的现象,为了避免出现这 种现象,才想到使用视频帧队列的;然而,天不遂人愿啊,目前情况下,视频帧队列长度为1时,录制效果是最好的,当增加长度为4以上时,录制的视频就会出现 重复录制某些视频帧的问题了;这个问题,我目前也还没有想到解决的方法;估计先要对录制原理进行了解过之后在进行改动,才能够解决的了;所以,我想,如果 能够控制好,先不要使用视频帧队列了,就采用一个视频帧作为缓冲即可,但要用加锁机制控制好读写冲突,否则也会出现意想不到的bug的。
显示视频帧线程 视频帧采集采用线程实现的原因也是为了能够提高GUI界面的响应速度,这一点我想大家也都是毋庸置疑的了;)
详细描述:
此线程主要完成从视频帧队列中读取视频帧,并显示出来的功能;
此线程同上也是使用类的实现方式,从QWidget继承而来,采用类的实现方式来实现线程操作;具体仍然是重点介绍run方法的实现方式;
Void LTvWindow::run(){
//
while(!m_tv_stopping){
// while tv don’t stop playing;
pm_frameYUV = get_frame_with_index (pm_tv, f_display_start_index);
while ( pm_frameYUV == NULL ){
//
pm_frameYUV = get_frame_with_index (pm_tv, f_display_start_index);
}
// 循环执行update的目的就是调用paintEvent函数显示视频帧;
update();
// ready to display next frame in the next buffer with index's increment;
f_display_start_index++;
if (f_display_start_index == pm_tv->m_video_buffer_size){
f_display_start_index = 0;
}
}// end while;
}
//
void LTvWindow::paintEvent (QPaintEvent * pv_event)
{
setWFlags(getWFlags() | Qt::WRepaintNoErase);
// 调用xv_display_one_frame显示视频帧;
xv_display_one_frame();
}
//
void LTvWindow::xv_display_one_frame(){
//
int f_src_x = 0;
int f_src_y = 0;
int f_dest_x = 0;
int f_dest_y = 0;
int f_rt_value = 0;
//
if ( m_fs_flag ){
// full screen;
if ( pm_frame_image != NULL ){
// Using Xv to display frames;
f_rt_value = XvPutImage(pm_display, m_port_id, this->winId(), /
*pm_gc, pm_frame_image, /
f_src_x, f_src_y, pm_tv->m_width, pm_tv->m_height, /
f_dest_x, f_dest_y, DSCREEN_FS_WIDTH, DSCREEN_FS_HEIGHT);
// fprintf(stderr, "xvputimage/n");
}
}else{
if ( pm_frame_image != NULL ){
// Using Xv to display frames;
f_rt_value = XvPutImage(pm_display, m_port_id, this->winId(), /
*pm_gc, pm_frame_image, /
f_src_x, f_src_y, pm_tv->m_width, pm_tv->m_height, /
f_dest_x, f_dest_y, pm_tv->m_width, pm_tv->m_height);
}
}
}
这里需要说明的地方比较多,最关键的也就是最后面的这个函数,显示视频帧有多种方法,我实现了两种,其中以上述方法最优,也就是采用Qt+Xv的方法;还 实现了一种是采用将视频帧转换为RGB视频帧之后,利用bitBle方法将QImage刷到屏幕上实现播放,这种方法,对于不经过缩放处理的的视频帧还可 以正常播放,一旦加上缩放操作,明显速度就跟不上了,视频帧刷新明显慢下来了;
上述方法就是采用的Qt+Xv来实习显示视频帧,并且能够实现视频帧全屏播放,而且速度很快;似乎还有更快的方法--XvShmPutImage,我没有试过,估计更快;)