=====================================================
最简单的视音频播放示例系列文章列表:
最简单的视音频播放示例1:总述
最简单的视音频播放示例2:GDI播放YUV, RGB
最简单的视音频播放示例3:Direct3D播放YUV,RGB(通过Surface)
最简单的视音频播放示例4:Direct3D播放RGB(通过Texture)
最简单的视音频播放示例5:OpenGL播放RGB/YUV
最简单的视音频播放示例6:OpenGL播放YUV420P(通过Texture,使用Shader)
最简单的视音频播放示例7:SDL2播放RGB/YUV
最简单的视音频播放示例8:DirectSound播放PCM
最简单的视音频播放示例9:SDL2播放PCM
=====================================================
前一篇文章对“Simplest Media Play”工程作了概括性介绍。后续几篇文章打算详细介绍每个子工程中的几种技术。在记录Direct3D,OpenGL这两种相对复杂的技术之前,打算先记录一种和它们属于同一层面的的简单的技术——GDI作为热身。
GDI简介
下面这段文字摘自维基百科:
图形设备接口(Graphics Device Interface或Graphical Device Interface,缩写GDI),是微软公司视窗操作系统(Microsoft Windows)的三大核心部件(另外两个是kernel、user)之一。GDI是微软视窗系统表征图形对象及将其传送给诸如显示器、打印机之类输出设备的标准。其他系统也有类似GDI的东西,比如Macintosh的Quartz(传统的QuickDraw),和GTK的GDK/Xlib。
GDI用来完成一些和绘制有关的工作,像直线或曲线的绘制,文字渲染,调色板控制。它最大的好处是它有可以直接访问硬件设备的能力。通过GDI可以非常容易的在不同类型的设备上绘制图形,像显示屏和打印机或类似的显示设备。这些能力是微软Windows系统“所见即所得”程序的核心。
简单的不需要快速图形渲染的游戏可能会用到GDI。但是GDI对一些高级的动画制作无能为力,它缺少显卡中帧的概念,还缺少3D图形硬件光栅化的支持等等。现代的游戏通常使用DirectX和OpenGL而不是GDI,因为这些技术能更好的让程序员利用硬件的特性来加速图形图像的显示。
下面这张图可以很好的表现GDI(以及Direct3D)在系统中的位置。可以看出它们位于应用程序和硬件之间,和Direct3D属于同一类型的东西。
我自己在之前做的码流分析程序《VideoEye》中的“单帧详细分析”模块中曾经大量使用了GDI,因为那个功能需要在窗口中绘制帧图像,量化参数,宏块类型,运动矢量等参数。因此对GDI这部分的函数还算熟悉。例如下图是当时画出的“量化参数”,“宏块划分”和“运动矢量”分析结果。图中的背景图像,数字,直线都是通过GDI画上去的。
视频像素数据
视频显示的输入数据一般情况下是非压缩的RGB/YUV数据。像H.264这些压缩码流是不能用于显示的。非压缩的RGB/YUV数据在我们电脑里并不常见,因为它的体积实在是太大了。几秒钟的RGB/YUV数据就有几十MB甚至上百MB大。举个例子,5秒分辨率为1280x720,格式为RGB24的视频数据体积(按照每秒25帧计算)为:
1280*720*3*25*5=345600000B=345.6MB
我们日常生活中比较常见的存储非压缩的RGB像素数据的格式就是BMP。它的文件体(除去文件头)中存储的是RGB数据。很容易发现,BMP文件明显大于JPEG等压缩格式的文件。
本文记录的例子的使用的是纯像素数据,和BMP这种封装过的RGB数据最大的不同在于没有文件头,需要使用特定的播放器,设定参数之后才能正确播放。
PS:纯像素数据播放器推荐以下两个:
YUV Player Deluxe: 只能播放YUV格式,但是确实很好使。
Vooya: 除了支持YUV之外,还支持各种各样的RGB数据,更加强大。
视频显示的要点
用GDI显示像素数据是极其简单的,难度远远低于Direct3D和OpenGL。可以分成两步:
1.构造一张BMP。
(1) 构造文件头
(2) 读取像素数据
2.调用函数画上去。
下面分步说明:
1.构造一张BMP
(1)构造文件头
构造BMP需要用到结构体BITMAPINFO,该结构体主要用于存储BMP文件头信息:
//BMP HeaderBITMAPINFO m_bmphdr={0};DWORD dwBmpHdr = sizeof(BITMAPINFO);m_bmphdr.bmiHeader.biBitCount = 24;m_bmphdr.bmiHeader.biClrImportant = 0;m_bmphdr.bmiHeader.biSize = dwBmpHdr;m_bmphdr.bmiHeader.biSizeImage = 0;m_bmphdr.bmiHeader.biWidth = pixel_w;//注意BMP在y方向是反着存储的,一次必须设置一个负值,才能使图像正着显示出来m_bmphdr.bmiHeader.biHeight = -pixel_h;m_bmphdr.bmiHeader.biXPelsPerMeter = 0;m_bmphdr.bmiHeader.biYPelsPerMeter = 0;m_bmphdr.bmiHeader.biClrUsed = 0;m_bmphdr.bmiHeader.biPlanes = 1;m_bmphdr.bmiHeader.biCompression = BI_RGB;
从构造BMP这一步我们可以得知:像素格式必须转换为RGB(即不能是YUV)才能使用GDI画上去。因为BMP存储像素是RGB格式的。
(2)读取像素数据
大端和小端
读取像素数据的时候,又涉及到一个知识点:大端模式和小端模式。
假使我们直接读取rgb24格式的一帧数据(在硬盘中的存储方式为R1|G1|B1,R2|G2|B2,R3|G3|B3),然后显示出来,会发现所有的人物都变成了“阿凡达”(忽然感觉这个比喻还挺形象的):就是人的皮肤都变成了蓝色了。导致这个的原因实际上是系统把“R”当成“B”,而把“B”当成“R”的结果。因此,如果我们如果把“R”和“B”的顺序调换一下的话(“G”保持不变),显示就正常了。
针对上述现象,我们可以得知BMP的RGB24格式的数据实际上是B1|G1|R1,B2|G2|R2这样的顺序存储的。那么问题来了,为什么会这么存储呢?这么存储的话,岂不是把像素格式起名字叫“BGR24”更合适?下面就详细分析一下这个问题。
首先来看一下“RGB24”这种名称的起名规则。它们是按照“高字节->低字节”的方式起名的。即高字节存“R”,低字节存“B”。在系统中,一个像素点的像素值(RGB24则是包含R,G,B三个8Byte的数据;RGBA则包含R,G,B,A四个8Byte的数据)被认为是一个“颜色”变量(官方有正规的结构体定义:tagRGBTRIPLE,tagRGBQUAD。也是类似于int,char这样的变量)。这种长度超过1Byte的变量(int的长度也超过了1Byte)存储的时候就涉及到一个的问题:变量的低字节应该保存在内存的高地址还是低地址?
对于上述问题,有两种存储方法:
大端模式(Big Endian),是指数据的低字节(注意是整个字节)保存在内存的高地址中,而数据的高字节,保存在内存的低地址中。其实这是一个更符合人类习惯的存储方式。
小端模式(Little Endian),是指数据的低字节保存在内存的低地址中,而数据的高字节保存在内存的高地址中。
大部分用户的操作系统(如Windows,FreeBsd,Linux)是Little Endian的。少部分,如MAC OS是Big Endian 的。此外,网络字节序是Big Endian的。BMP文件的存储规定是Little Endian。因此,例如对于3Byte的RGB24变量,其低字节为“B”,而低字节应该保存在内存的低地址中,因此应该保存在最“前面”。所以RGB24格式的像素数据,在BMP文件中的存储方式是B1|G1|R1,B2|G2|R2…
也可以看一下官方的定义:
typedef struct tagRGBTRIPLE {BYTE rgbtBlue;// 蓝色分量BYTE rgbtGreen;// 绿色分量BYTE rgbtRed;// 红色分量} RGBTRIPLE;typedef struct tagRGBQUAD {BYTE rgbBlue;// 蓝色分量BYTE rgbGreen;
如果觉得《最简单的视音频播放示例2 GDI播放YUV RGB》对你有帮助,请点赞、收藏,并留下你的观点哦!