=====================================================
最简单的视音频播放演示样例系列文章列表:
最简单的视音频播放演示样例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
=====================================================
本文记录SDL播放视频的技术。在这里使用的版本号是SDL2。实际上SDL本身并不提供视音频播放的功能,它仅仅是封装了视音频播放的底层API。在Windows平台下。SDL封装了Direct3D这类的API用于播放视频;封装了DirectSound这类的API用于播放音频。由于SDL的编写目的就是简化视音频播放的开发难度,所以使用SDL播放视频(YUV/RGB)和音频(PCM)数据非常的easy。
下文记录一下使用SDL播放视频数据的技术。
SDL简单介绍
SDL(Simple DirectMedia Layer)是一套开放源码的跨平台多媒体开发库,使用C语言写成。
SDL提供了数种控制图像、声音、输出入的函数。让开发人员仅仅要用同样或是类似的代码就能够开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。眼下SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。用以下这张图能够非常明白地说明SDL的位置。
SDL实际上并不限于视音频的播放,它将功能分成下列数个子系统(subsystem):
Video(图像):图像控制以及线程(thread)和事件管理(event)。
Audio(声音):声音控制
Joystick(摇杆):游戏摇杆控制
CD-ROM(光盘驱动器):光盘媒体控制
Window Management(视窗管理):与视窗程序设计集成
Event(事件驱动):处理事件驱动
在Windows下。SDL与DirectX的相应关系例如以下。
SDL |
DirectX |
SDL_Video、SDL_Image |
DirectDraw、Direct3D |
SDL_Audio、SDL_Mixer |
DirectSound |
SDL_Joystick、SDL_Base |
DirectInput |
SDL_Net |
DirectPlay |
SDL播放视频的流程
SDL播放视频的技术在此前做的FFmpeg的演示样例程序中已经多次用到。在这里又一次总结一下流程。1. 初始化
1) 初始化SDL2. 循环显示画面
2) 创建窗体(Window)
3) 基于窗体创建渲染器(Render)
4) 创建纹理(Texture)
1) 设置纹理的数据
2) 纹理复制给渲染目标
3) 显示
以下具体分析一下上文的流程。
1. 初始化
1) 初始化SDL
使用SDL_Init()初始化SDL。该函数能够确定希望激活的子系统。SDL_Init()函数原型例如以下:
int SDLCALL SDL_Init(Uint32 flags)
当中。flags能够取下列值:
SDL_INIT_TIMER:定时器
SDL_INIT_AUDIO:音频
SDL_INIT_VIDEO:视频
SDL_INIT_JOYSTICK:摇杆
SDL_INIT_HAPTIC:触摸屏
SDL_INIT_GAMECONTROLLER:游戏控制器
SDL_INIT_EVENTS:事件
SDL_INIT_NOPARACHUTE:不捕获关键信号(这个没研究过)
SDL_INIT_EVERYTHING:包括上述全部选项
有关SDL_Init()有一点须要注意:初始化的时候尽量做到“够用就好”,而不要用SDL_INIT_EVERYTHING。由于有些情况下使用SDL_INIT_EVERYTHING会出现一些不可预知的问题。
比如,在MFC应用程序中播放纯音频。假设初始化SDL的时候使用SDL_INIT_EVERYTHING。那么就会出现听不到声音的情况。
后来发现,去掉了SDL_INIT_VIDEO之后,问题才得以解决。
2) 创建窗体(Window)
使用SDL_CreateWindow()创建一个用于视频播放的窗体。SDL_CreateWindow()的原型例如以下。
SDL_Window * SDLCALL SDL_CreateWindow(const char *title,
int x, int y, int w,
int h, Uint32 flags);
參数含义例如以下。
title :窗体标题
x :窗体位置x坐标。也能够设置为SDL_WINDOWPOS_CENTERED或SDL_WINDOWPOS_UNDEFINED。
y :窗体位置y坐标。同上。
w :窗体的宽
h :窗体的高
flags :支持下列标识。
包括了窗体的是否最大化、最小化,是否能调整边界等等属性。
::SDL_WINDOW_FULLSCREEN, ::SDL_WINDOW_OPENGL,
::SDL_WINDOW_HIDDEN, ::SDL_WINDOW_BORDERLESS,
::SDL_WINDOW_RESIZABLE, ::SDL_WINDOW_MAXIMIZED,
::SDL_WINDOW_MINIMIZED, ::SDL_WINDOW_INPUT_GRABBED,
::SDL_WINDOW_ALLOW_HIGHDPI.
返回创建完毕的窗体的ID。假设创建失败则返回0。
3) 基于窗体创建渲染器(Render)
使用SDL_CreateRenderer()基于窗体创建渲染器。
SDL_CreateRenderer()原型例如以下。
SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window * window,
int index, Uint32 flags);
參数含义例如以下。
window : 渲染的目标窗体。
index :打算初始化的渲染设备的索引。
设置“-1”则初始化默认的渲染设备。
flags :支持以下值(位于SDL_RendererFlags定义中)
SDL_RENDERER_SOFTWARE :使用软件渲染返回创建完毕的渲染器的ID。假设创建失败则返回NULL。
SDL_RENDERER_ACCELERATED :使用硬件加速
SDL_RENDERER_PRESENTVSYNC:和显示器的刷新率同步
SDL_RENDERER_TARGETTEXTURE :不太懂
4) 创建纹理(Texture)
使用SDL_CreateTexture()基于渲染器创建一个纹理。SDL_CreateTexture()的原型例如以下。
SDL_Texture * SDLCALL SDL_CreateTexture(SDL_Renderer * renderer,
Uint32 format,
int access, int w,
int h);
參数的含义例如以下。
renderer:目标渲染器。
format :纹理的格式。后面会详述。
access :能够取以下值(定义位于SDL_TextureAccess中)
SDL_TEXTUREACCESS_STATIC :变化极少w :纹理的宽
SDL_TEXTUREACCESS_STREAMING :变化频繁
SDL_TEXTUREACCESS_TARGET :临时没有理解
h :纹理的高
创建成功则返回纹理的ID,失败返回0。
在纹理的创建过程中。须要指定纹理的格式(即第二个參数)。
SDL的中的格式非常多,例如以下所列。
SDL_PIXELFORMAT_UNKNOWN,
SDL_PIXELFORMAT_INDEX1LSB =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_4321, 0,
1, 0),
SDL_PIXELFORMAT_INDEX1MSB =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_1234, 0,
1, 0),
SDL_PIXELFORMAT_INDEX4LSB =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_4321, 0,
4, 0),
SDL_PIXELFORMAT_INDEX4MSB =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_1234, 0,
4, 0),
SDL_PIXELFORMAT_INDEX8 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX8, 0, 0, 8, 1),
SDL_PIXELFORMAT_RGB332 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED8, SDL_PACKEDORDER_XRGB,
SDL_PACKEDLAYOUT_332, 8, 1),
SDL_PIXELFORMAT_RGB444 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
SDL_PACKEDLAYOUT_4444, 12, 2),
SDL_PIXELFORMAT_RGB555 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
SDL_PACKEDLAYOUT_1555, 15, 2),
SDL_PIXELFORMAT_BGR555 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
SDL_PACKEDLAYOUT_1555, 15, 2),
SDL_PIXELFORMAT_ARGB4444 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB,
SDL_PACKEDLAYOUT_4444, 16, 2),
SDL_PIXELFORMAT_RGBA4444 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA,
SDL_PACKEDLAYOUT_4444, 16, 2),
SDL_PIXELFORMAT_ABGR4444 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR,
SDL_PACKEDLAYOUT_4444, 16, 2),
SDL_PIXELFORMAT_BGRA4444 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA,
SDL_PACKEDLAYOUT_4444, 16, 2),
SDL_PIXELFORMAT_ARGB1555 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB,
SDL_PACKEDLAYOUT_1555, 16, 2),
SDL_PIXELFORMAT_RGBA5551 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA,
SDL_PACKEDLAYOUT_5551, 16, 2),
SDL_PIXELFORMAT_ABGR1555 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR,
SDL_PACKEDLAYOUT_1555, 16, 2),
SDL_PIXELFORMAT_BGRA5551 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA,
SDL_PACKEDLAYOUT_5551, 16, 2),
SDL_PIXELFORMAT_RGB565 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
SDL_PACKEDLAYOUT_565, 16, 2),
SDL_PIXELFORMAT_BGR565 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
SDL_PACKEDLAYOUT_565, 16, 2),
SDL_PIXELFORMAT_RGB24 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_RGB, 0,
24, 3),
SDL_PIXELFORMAT_BGR24 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_BGR, 0,
24, 3),
SDL_PIXELFORMAT_RGB888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XRGB,
SDL_PACKEDLAYOUT_8888, 24, 4),
SDL_PIXELFORMAT_RGBX8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBX,
SDL_PACKEDLAYOUT_8888, 24, 4),
SDL_PIXELFORMAT_BGR888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XBGR,
SDL_PACKEDLAYOUT_8888, 24, 4),
SDL_PIXELFORMAT_BGRX8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRX,
SDL_PACKEDLAYOUT_8888, 24, 4),
SDL_PIXELFORMAT_ARGB8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB,
SDL_PACKEDLAYOUT_8888, 32, 4),
SDL_PIXELFORMAT_RGBA8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBA,
SDL_PACKEDLAYOUT_8888, 32, 4),
SDL_PIXELFORMAT_ABGR8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ABGR,
SDL_PACKEDLAYOUT_8888, 32, 4),
SDL_PIXELFORMAT_BGRA8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRA,
SDL_PACKEDLAYOUT_8888, 32, 4),
SDL_PIXELFORMAT_ARGB2101010 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB,
SDL_PACKEDLAYOUT_2101010, 32, 4),
SDL_PIXELFORMAT_YV12 = /**< Planar mode: Y + V + U (3 planes) */
SDL_DEFINE_PIXELFOURCC('Y', 'V', '1', '2'),
SDL_PIXELFORMAT_IYUV = /**< Planar mode: Y + U + V (3 planes) */
SDL_DEFINE_PIXELFOURCC('I', 'Y', 'U', 'V'),
SDL_PIXELFORMAT_YUY2 = /**< Packed mode: Y0+U0+Y1+V0 (1 plane) */
SDL_DEFINE_PIXELFOURCC('Y', 'U', 'Y', '2'),
SDL_PIXELFORMAT_UYVY = /**< Packed mode: U0+Y0+V0+Y1 (1 plane) */
SDL_DEFINE_PIXELFOURCC('U', 'Y', 'V', 'Y'),
SDL_PIXELFORMAT_YVYU = /**< Packed mode: Y0+V0+Y1+U0 (1 plane) */
SDL_DEFINE_PIXELFOURCC('Y', 'V', 'Y', 'U')
这一看确实给人一种“眼花缭乱”的感觉。
简单分析一下当中的定义吧。比如ARGB8888的定义例如以下。
SDL_PIXELFORMAT_ARGB8888 =
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB,
SDL_PACKEDLAYOUT_8888, 32, 4),
当中用了一个宏SDL_DEFINE_PIXELFORMAT用于将几种属性合并到一个格式中。
以下我们看看一个格式都包括哪些属性:
SDL_PIXELTYPE_PACKED32:代表了像素分量的存储方式。
PACKED代表了像素的几个分量是一起存储的。内存中存储方式例如以下:R1|G1|B1,R2|G2|B2…;ARRAY则代表了像素的几个分量是分开存储的。内存中存储方式例如以下:R1|R2|R3…。G1|G2|G3…,B1|B2|B3…
SDL_PACKEDORDER_ARGB:代表了PACKED存储方式下像素分量的顺序。注意。这里所说的顺序涉及到了一个“大端”和“小端”的问题。
这个问题在《最简单的视音频播放演示样例2:GDI播放YUV, RGB》中已经叙述,不再反复记录。
对于Windows这种“小端”系统,“ARGB”格式在内存中的存储顺序是B|G|R|A。
SDL_PACKEDLAYOUT_8888:说明了每一个分量占领的比特数。比如ARGB格式每一个分量分别占领了8bit。
32:每一个像素占用的比特数。
比如ARGB格式占用了32bit(每一个分量占领8bit)。
4:每一个像素占用的字节数。
比如ARGB格式占用了4Byte(每一个分量占领1Byte)。
2. 循环显示画面
1) 设置纹理的数据
使用SDL_UpdateTexture()设置纹理的像素数据。SDL_UpdateTexture()的原型例如以下。int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const void *pixels, int pitch);
參数的含义例如以下。
texture:目标纹理。
rect:更新像素的矩形区域。设置为NULL的时候更新整个区域。
pixels:像素数据。
pitch:一行像素数据的字节数。
成功的话返回0。失败的话返回-1。
2) 纹理复制给渲染目标
使用SDL_RenderCopy()将纹理数据复制给渲染目标。在使用SDL_RenderCopy()之前,能够使用SDL_RenderClear()先使用清空渲染目标。实际上视频播放的时候不使用SDL_RenderClear()也是能够的,由于视频的后一帧会全然覆盖前一帧。
SDL_RenderClear()原型例如以下。
int SDLCALL SDL_RenderClear(SDL_Renderer * renderer);
參数renderer用于指定渲染目标。
SDL_RenderCopy()原型例如以下。
int SDLCALL SDL_RenderCopy(SDL_Renderer * renderer,
SDL_Texture * texture,
const SDL_Rect * srcrect,
const SDL_Rect * dstrect);
參数的含义例如以下。
renderer:渲染目标。
texture:输入纹理。
srcrect:选择输入纹理的一块矩形区域作为输入。设置为NULL的时候整个纹理作为输入。
dstrect:选择渲染目标的一块矩形区域作为输出。
设置为NULL的时候整个渲染目标作为输出。
成功的话返回0,失败的话返回-1。
3) 显示
使用SDL_RenderPresent()显示画面。
SDL_RenderPresent()原型例如以下。
void SDLCALL SDL_RenderPresent(SDL_Renderer * renderer);
參数renderer用于指定渲染目标。
流程总结
在《最简单的基于FFMPEG+SDL的视频播放器 ver2(採用SDL2.0)》中总结过SDL2播放视频的流程。在这里简单复制过来。
使用SDL播放视频的流程能够概括为下图。
SDL中几个关键的结构体之间的关系能够用下图概述。
简单解释一下各变量的作用:
SDL_Window就是使用SDL的时候弹出的那个窗体。
在SDL1.x版本号中,仅仅能够创建一个一个窗体。在SDL2.0版本号中,能够创建多个窗体。
SDL_Texture用于显示YUV数据。一个SDL_Texture相应一帧YUV数据。
SDL_Renderer用于渲染SDL_Texture至SDL_Window。
SDL_Rect用于确定SDL_Texture显示的位置。
代码
贴出源码。
/**
* 最简单的SDL2播放视频的样例(SDL2播放RGB/YUV)
* Simplest Video Play SDL2 (SDL2 play RGB/YUV)
*
* 雷霄骅 Lei Xiaohua
* [email protected]
* 中国传媒大学/数字电视技术
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
* 本程序使用SDL2播放RGB/YUV视频像素数据。
SDL实际上是对底层画图 * API(Direct3D,OpenGL)的封装,使用起来明显简单于直接调用底层 * API。 * * 函数调用过程例如以下: * * [初始化] * SDL_Init(): 初始化SDL。 * SDL_CreateWindow(): 创建窗体(Window)。 * SDL_CreateRenderer(): 基于窗体创建渲染器(Render)。 * SDL_CreateTexture(): 创建纹理(Texture)。 * * [循环渲染数据] * SDL_UpdateTexture(): 设置纹理的数据。 * SDL_RenderCopy(): 纹理复制给渲染器。 * SDL_RenderPresent(): 显示。 * * This software plays RGB/YUV raw video data using SDL2. * SDL is a wrapper of low-level API (Direct3D, OpenGL). * Use SDL is much easier than directly call these low-level API. * * The process is shown as follows: * * [Init] * SDL_Init(): Init SDL. * SDL_CreateWindow(): Create a Window. * SDL_CreateRenderer(): Create a Render. * SDL_CreateTexture(): Create a Texture. * * [Loop to Render data] * SDL_UpdateTexture(): Set Texture's data. * SDL_RenderCopy(): Copy Texture to Render. * SDL_RenderPresent(): Show. */ #include
执行结果
程序的执行结果例如以下图所看到的。下载
代码位于“Simplest Media Play”中
SourceForge项目地址:https://sourceforge.net/projects/simplestmediaplay/
CSDN下载地址:http://download.csdn.net/detail/leixiaohua1020/8054395
注:
该项目会不定时的更新并修复一些小问题。最新的版本号请參考该系列文章的总述页面:
《最简单的视音频播放演示样例1:总述》
上述project包括了使用各种API(Direct3D。OpenGL,GDI。DirectSound,SDL2)播放多媒体样例。当中音频输入为PCM採样数据。输出至系统的声卡播放出来。
视频输入为YUV/RGB像素数据。输出至显示器上的一个窗体播放出来。
通过本project的代码刚開始学习的人能够高速学习使用这几个API播放视频和音频的技术。
一共包括了例如以下几个子project:
simplest_audio_play_directsound: 使用DirectSound播放PCM音频採样数据。
simplest_audio_play_sdl2: 使用SDL2播放PCM音频採样数据。
simplest_video_play_direct3d: 使用Direct3D的Surface播放RGB/YUV视频像素数据。
simplest_video_play_direct3d_texture:使用Direct3D的Texture播放RGB视频像素数据。
simplest_video_play_gdi: 使用GDI播放RGB/YUV视频像素数据。
simplest_video_play_opengl: 使用OpenGL播放RGB/YUV视频像素数据。
simplest_video_play_opengl_texture: 使用OpenGL的Texture播放YUV视频像素数据。
simplest_video_play_sdl2: 使用SDL2播放RGB/YUV视频像素数据。