您的位置:首页 > 其它

高效率视频播放: 如何使用3D API 进行视频的高质量回放

2007-10-21 10:15 806 查看
http://blog.csdn.net/Nhsoft/archive/2007/10/16/1826933.aspx

这个月准备写一些关于如何使用3D API搭建一个高质量的视频回放后端,其实想想也是时候了,在这个公司快一年了,Backend从开始的支持RGB24,到YUV的支持,到后来质量的不断提高, 尤其是播放质量被SONY这样苛刻的公司接受以后,总觉得该写一点东西了.


其实要在3D 环境播放视频并不是件困难的事情,如果你仅仅是想做一个视频纹理,那么你可以放心的做很多假设,并且让美术提供的视频都必须满足你的假设,你可以使用RGB24的格式,你可以选择让DirectShow来为你完成YUV-RGB的color space的转化,你也可以不考虑播放是不是完全流畅的,因为那仅仅是整个游戏中的一个小小的部分,没人会来关系整个部分的视频是不是流畅.

如果你做的是个播放器,专门用来播放视频的,那么质量问题喝兼容问题显得非常重要:
1: 你要支持YUV格式,至少要支持一种: YV12.
2: 你不能不能考虑播放的流畅性.,不能产生Frame drop现象.
3: 必须适应Decoder出来的frame的步调,除了保证不丢帧以外,还需要保证不会有的帧在屏幕上停留时间太长,有的太短.
4: 你不得不考虑性能问题,因为有些视频文件是巨大的,HD的视频1920 x 1080 这样的分辨率是非常常见的,Decoder会在解码的动作上耗费掉大量的CPU,留给backend的时间并不多..

就以上问题,总结起来是两个: 一,YUV格式的支持.二,提供性能,保证播放流畅性. 下面我来说说一些解决方案.
一:YUV格式的支持.
YUV格式其实有很多种, 大类分成两类,Packed的跟plannar的.前者是YUV是挤到一个int32里的(注意这个挤字),另外一种是YUV三个分量的数据分开保存,等于三个数组.
我们知道用3D API中,用来保存图象的资源就是纹理,对于以上两类YUV格式,显然是第一种比较自然一些,但是必须注意的是packed的YUV格式,通常是交错的 ,比如排列方式是YUYV这样排列, 因为对视频来说,Y亮度信息明显比UV色差信号要来的重要,所以说,在4个字节中挤进去两个像素的信息,这两个像素公用一个UV值. 这种格式就称为YUYV格式. 除此之外还有其他很多对一般应用者来说可能是千奇百怪的格式. 因此,事实上packed的YUV.只是看上去比较自然,实现起来却并不是那么简单轻松.即使是比较容易实现的YUYV格式,也很难实现它的平滑滤波.
相反来说,对于plannar的YUV格式, 我们虽然没办法用一个纹理来模拟它,但是完全可以采用三个纹理的方式来模拟它,这种方式几乎不需要在Pixel Shader做很多特殊处理,无非就是三个纹理采样动作而已.非常幸运的是,现在大部分的解码器都是直接输出YV12格式的,在一个分辨率是MxN大小的YV12格式中,的一块数据是MxN的Y信号.接下来是一块M/2 x N/2大小的U信号,最后是M/2 x N/2的V信号,也就是说,Y信号的分辨率比UV大一倍. 这样我们可以用一个MxN跟两个M/2 x N/2纹理就可以模拟出YV12格式了.并且可以自由的使用各种纹理滤波方式.
解决了如何把YUV信号输入到3D API中,接下来就可以使用Pixel Shader来对采样的YUV信号进行调整,并转化成RGB信号,这个转化是非常简单的.代码如下:

sampler2D texYPlane;
sampler2D texUPlane;
sampler2D texVPlane;
float4 main( float2 texCoord : TEXCOORD0 , float4 fColor : COLOR) : COLOR
{
float4 matYUV2RGB0 = float4 (1.0 , 0.0 , 1.14 , 0.0);
float4 matYUV2RGB1 = float4 (1.0 , -0.390 , -0.58 , 0.0);
float4 matYUV2RGB2 = float4 (1.0 , 2.03 , 0 , 0.0);

//输入的YUV必须加个偏移量.详细细节参见www.fourcc.org
float4 deltaYUV = float4 (-0.00 , -0.500 , -0.500 , 0.0);

float4 yuvColor;

//对三个纹理进行采样
float2 newTexCoord = float2(texCoord.x , texCoord.y);
yuvColor.x = tex2D( texYPlane, newTexCoord ).x;
yuvColor.y = tex2D( texUPlane, newTexCoord ).x;
yuvColor.z = tex2D( texVPlane, newTexCoord ).x;
yuvColor.w = 1.0f;
yuvColor = yuvColor + deltaYUV;
//adjust color
yuvColor = adjustColor(yuvColor);
float4 imageColor;
//YUV 到RGB的转换
imageColor.x =dot(matYUV2RGB0.xyz , yuvColor.xyz);
imageColor.y =dot(matYUV2RGB1.xyz , yuvColor.xyz);
imageColor.z =dot(matYUV2RGB2.xyz , yuvColor.xyz);
imageColor.w = 1.0;
return imageColor;
}
关于adjustColor函数,主要是用来对对比度,饱和度等进行调节的,具体算法,在数字图象处理中都可以找到.关于详细的YUV资料,可以到www.fourcc.org上去找.


二: 如果提高播放的流畅性
播放的流畅性,首先要保证性能. 如果说,解码器解码一帧的时间是40ms.那么无论如何你怎么优化,视频都不会流畅.同样的,如果视频解码器解码一帧费时是25ms.那么如果是24fps为正常播放速率,那么表示留给你去绘制的时间有16ms.在这16ms内,你必须把这一帧图象下载到GPU中,并把它绘制出去,绘制其实可以和解码是异步进行的,那么下载的动作呢?在OpenGL里我们可以采用PBO来进行纹理数据异步下载,这能大大的提高纹理更新的速度.
另外一方面,并不是说你的更新速度足够高就能让视频看起来是流畅的,你还必须保证更新的步调和解码器输出是一致的.也就是说不能更新快,也不能更新慢了.我们可以采用两种方法来进行解码帧和绘制帧两个动作的同步.
第一种方法,线程锁 + 标记变量. 通常绘制和解码不是在同一个线程中的,我们可以为视频输出纹理加一个锁和标记变量,当纹理正在被解码器写入的时候,我们就lock,并在unlock的时候,设置标记变量的状态为dirty.这就等于通知了绘制线程,告诉它视频帧的数据被更新,需要绘制. 而绘制线程会在进行绘制前检测标记变量,只有标记变量是dirty的时候,才进行绘制,或者可以选择不更新画面或者等待标记变量变成dirty状态.
第二种方法,采用信号量. 我们可以声明一个HANDLE drawEvent, 当解码器更新完一帧数据才升起这个信号量.具体代码如下:
int drawFrameThreadFunc(int)
{
initDrawDevice();//分配一个Direct3D/OpenGL的设备

while(1)
{
//其他一些逻辑.........
WaitForSingleObject(drawEvent,200);
drawYV12VideoTexture();
//其他一些逻辑.........
}
closeDrawDevice();//关闭设备.
}

这两种方法,后一种比较精确,前一种相对比较自由.并且可以和有很多动态效果的GUI结合在一起.

到此为止,我简单了介绍了高质量播放视频时候需要注意的一些问题,相对来说都是比较大的概括,细节上的问题还会更加的多.比如缩放滤波的质量,视频的反交错等问题都需要费很大的精力去完成.因此,做一个播放器的后端也不是那么简单的:). 如果读者有更好的方法和建议,可以通过邮件跟我联系.

最近一直在做视频的播放。尤其是HD的视频,即使是1024 x 576的视频播放在CPU消耗上是一个巨大的压力。更大的还有1920 x 1080的。在CPU的消耗上简直就是一个恶梦。
最近用DSHOW做了一个Demo。一般Dshow的例子里都是直接用VMR把视频播放出去。对于游戏开发人员跟视频处理的应用来说,一个额外的要求就是你自己需要写一个Video Renderer截获视频的Frame,并把视频的帧处理后用3DAPI显示出来。
做这个demo的过程中,碰到一个以前不愿意解决的问题就是DSHOW的输出的象素格式问题,以前我一直用RGB24,虽然在Linux的项目中也用过YUV。不过实在不愿意去处理。但是这回扛不住了,很多HD的解码器的filter。输出的只能是YV12跟YUY2格式的。而且即使能输出RGB的解码器,CPU占用率也非常的高,因为YUV->RGB转化占用了巨大的资源。看来把YUV-RGB模块放到GPU端是非做不可了。
补充说明一点,使用CyberLink解码器,采用MS的YUV->RGB代码,在解码1280 x 576的H.264视频时候,CPU占用率为38%(Core2 E6300 2G DDR2). 采用GPU(GF 7600GS)时候CPU占用率约为20%. 播放1920 x 1080的时候CPU约为40%。我这里提供的方法并非为了转换而转换,而是为了播放视频而转换。
这里介绍一下YV12和YUY2的基本知识。当然我假设你知道YUV是什么东西。简单的说,YUV的格式在存储上有两类布局: Packed和Plannar。Packed的方式就是把相邻几个象素打包起来。比如把水平方向2个象素打包到一个DWORD里。Planner方式则相反。Y分量和UV分量完全分开来保存。YUY2和YV12是最常用的两个代表。YUY2是packed方式的。水平方向两个像素打包到一个DWORD,并且UV采样率只有Y的一半,这符合人的视觉特征能有效的压缩数据,具体布局为[Y0, U0,Y1,V0]。 这种格式常见于MPEG1的解码器。YV12则常见于H.264的解码器,它属于plannar方式。对于一个MxN大小的视频来说,数据布局为[Y:M x N] [U:M/2 x N/2] [V:M/2 x N/2]. 也就是说UV的采样率在水平和垂直方向上都只有Y的一半。

知道了数据格式以后,我们就可以出台正确的方针了。
首先我们来看YV12。这个比较简单一些。创建文理的时候,我们创建3个纹理,像素格式均为D3DFMT_L8格式。
代码如下:
m_nVideoTexture = 3;
m_pVideoTexture[0] = m_Device.createTexture(iWidth , iHeight , D3DFMT_L8);
m_pVideoTexture[1] = m_Device.createTexture(iWidth/2 , iHeight/2 , D3DFMT_L8);
m_pVideoTexture[2] = m_Device.createTexture(iWidth/2 , iHeight/2 , D3DFMT_L8);

每一个Frame填充数据的时候代码如下。
const char* pTexData = (const char*)VideFrameData;
m_Device.updateTextureData(m_pVideoTexture[0], pTexData , w , h , D3DFMT_L8);
pTexData += w * h;
m_Device.updateTextureData(m_pVideoTexture[1], pTexData , w/2 , h/2 , D3DFMT_L8);
pTexData += w * h/4;
m_Device.updateTextureData(m_pVideoTexture[2], pTexData , w/2 , h/2 , D3DFMT_L8);


最后就是显示这三张Texture.
因为第一张纹理是一张黑白的图象。也就是说只用第一个纹理。你的电影就是黑白片了。接下来我们需要有一个Pixel Shader在GPU中把YUV转换成RGB.shader如下。
sampler2D YTextue;
sampler2D UTextue;
sampler2D VTextue;
float4 main( Texcoord : TEXCOORD0 ) : COLOR0
{
float3 yuvColor;
float3 delYuv = float3(-16.0/255.0 , -128.0/255.0 , -128.0/255.0);
yuvColor.x = tex2D( YTextue, Texcoord ).x;
yuvColor.y = tex2D( UTextue, Texcoord ).x;
yuvColor.z = tex2D( VTextue, Texcoord ).x;
yuvColor += delYuv;
float3 matYUVRGB1 = float3(1.164, 2.018 , 0.0 );
float3 matYUVRGB2 = float3(1.164, -0.391 , -0.813 );
float3 matYUVRGB3 = float3(1.164, 0.0 , 1.596 );
float4 rgbColor;
rgbColor.x = dot(yuvColor,matYUVRGB1);
rgbColor.y = dot(yuvColor,matYUVRGB2);
rgbColor.z = dot(yuvColor,matYUVRGB3);
rgbColor.w = 1.0f;
return rgbColor;
}
因为RGB这三个纹理是独立缩放的,所以输入输出的大小不一样而产生缩放的时候不会对画面产生影响。只要简单的对齐Pixel到texel中心就可以了。

接下来我们来处理YUY2格式。创建纹理的方式基本一致,因为水平方向两个像素打包成4个字节,所以纹理宽度是视频宽度的一半。代码如下
m_nVideoTexture = 1;
m_pVideoTexture[0] = m_Device.createTexture(iWidth/2, iHeight , D3DFMT_L8);
上传纹理数据和普通纹理没区别。
最后是绘制。这是YUY2区别于YV12最多的一块,关键难点在于,如何将打包的像素解包的问题,我们可以根据输入的纹理坐标计算出对应视频的像素坐标。VideoPixel(x,y)=TexCoord(x,y)*VideoSize(w ,h); 然后根据VideoPixel.x的奇偶性来判断读取哪一个Y。
上述想法有一个严重的问题就是假设输入尺寸和输出是完全一致的,这样才能保证PixelShader里读到的每一个纹理坐标对应的像素坐标都是个整数。这样就没办法处理缩放了。解决的办法就是创建一个和视频尺寸等大的RenderTarget。先绘制到RenderTarget。也就是说RenderTarget里得到的是一个RGB的图象。然后再绘制成你需要的尺寸。这样,创建代码就要相应改成如下:(注意,视频尺寸可能远远大于你的Backbuffer,所以你还需要创建一个DepthBuffer)。
m_nVideoTexture = 1;
m_pVideoTexture[0] = m_Device.createTexture(iWidth/2 , iHeight , D3DFMT_A8R8G8B8);
m_pRenderTarget = m_Device.createRenderTarget(iWidth , iHeight);
m_pDepthBuffer = m_Device.CreateDepthStencilSurface(iWidth,iHeight,D3DFMT_D16);


绘制代码则变成如下
//绘制到RenderTarget里
setupRenderTarget(m_pRenderTarget,m_pDepthBuffer);
//YUV2RGB转化的Shader
setupYUY2ToRGBShader();
setupTexture(m_pVideoTexture[0]);
drawQuad();
setupShader(NULL);
restoreRenderTarget();


//转化完毕,绘制RGB
setupTexture(m_pVideoTexture[0]);
drawQuad();


YUY2转换RGB的Shader如下:

sampler2D YUY2Textue;
float4 TexSize;


float Mod2(int x)
{
int x2 = x/2;
float ix2 = x2;
float fx2 = x/2.0;
if(ix2 == fx2)
return 0;
return 1;
}


float4 main( float2 TexcoordIn:TEXCOORD0) : COLOR0
{
float3 yuvColor;
float3 delYuv = float3(-16.0/255.0 , -128.0/255.0 , -128.0/255.0);
float2 TexCoord = TexcoordIn;
int texCoordX = TexCoord.x*TexSize.x*2;
if( Mod2(texCoordX) > 0.0)
yuvColor.x = tex2D( YUY2Textue, TexCoord ).x;
else
yuvColor.x = tex2D( YUY2Textue, TexCoord ).z;
yuvColor.y = tex2D( YUY2Textue, TexCoord ).w;
yuvColor.z = tex2D( YUY2Textue, TexCoord ).y;


yuvColor += delYuv;
float3 matYUVRGB1 = float3(1.164, 2.018 , 0.0 );
float3 matYUVRGB2 = float3(1.164, -0.391 , -0.813 );
float3 matYUVRGB3 = float3(1.164, 0.0 , 1.596 );


float4 rgbColor;
rgbColor.x = dot(yuvColor,matYUVRGB1);//yuvColor.x;//
rgbColor.y = dot(yuvColor,matYUVRGB2);//yuvColor.x;//
rgbColor.z = dot(yuvColor,matYUVRGB3);//yuvColor.x;//
rgbColor.w = 1.0f;


return rgbColor;
}


未来的计划。
1: 由于采用PixelShader做后期处理速度优势非常明显,可以考虑采用动态组合的方式来实现视频特效。
2: 可以考虑把反交错也放到PS里去做。
3: 没想好,但是要YY一下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: