使用SDL实现一个简单的YUV播放器

本文将通过几个简单示例,最后实现一个YUV播放器


本文内容如下

  • 1、SDL的基本操作
  • 2、SDL的处理事件
  • 3、SDL的纹理渲染
  • 4、使用SDL实现YUV播放器

1.SDL的基本操作

这个例子中,使用SDL来显示一个窗口

使用SDL创建一个窗口的基本流程如下

SDL_Init 初始化SDL

SDL_CreateWindow 创建一个窗口

SDL_CreateRenderer 创建一个操作窗口的渲染器

SDL_SetRenderDrawColor 设置渲染器的渲染的格式

SDL_RenderClear 将渲染器刷到窗口上

SDL_RenderPresent 刷新窗口

Delay 延迟

SDL_DestroyRenderer 摧毁渲染器

SDL_DestroyWindow 摧毁窗口

SDL_Quit 退出SDL

函数详解

函数介绍:初始化SDL,必须在调用其他SDL函数之前调用此函数
参数:flags:SDL_INIT_TIMER 初始化计时器系统SDL_INIT_AUDIO 初始化音频系统SDL_INIT_VIDEO 初始化视频系统SDL_INIT_JOYSTICK 初始化遥杆系统SDL_INIT_EVERYTHING 初始化所有系统
返回值:-1表示错误,0表示成功int SDL_Init(Uint32 flags);
函数介绍:创建一个窗口
参数:title:窗口名称x:窗口起始位置x坐标y:窗口起始位置y坐标w:窗口宽度y:窗口高度flags:SDL_WINDOW_OPENGL 使用OpenGLSDL_WINDOW_SHOWN 窗口可见SDL_WINDOW_RESIZABLE 窗口能改变大小...
返回值:成功返回创建好的窗口,失败返回NULLSDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags);
函数介绍:使用此函数为窗口创建2D呈现上下文
参数:window:SDL_CreateWindow所创建的窗口index:初始化的渲染设备的索引,设置“-1”则初始化默认的渲染设备flags:SDL_RENDERER_SOFTWARE 使用软件渲染SDL_RENDERER_ACCELERATED 使用硬件加速SDL_RENDERER_PRESENTVSYNC 和显示器的刷新率同步
返回值:成功返回创建好的渲染器,失败返回NULLSDL_Renderer *SDL_CreateRenderer(SDL_Window * window, int index, Uint32 flags);
函数介绍:使用此函数可设置用于绘图操作的颜色
参数:renderer:要操作的渲染器r:颜色R分量g:颜色G分量b:颜色B分量a:颜色A分量
返回值:成功返回0,失败返回负的错误码int SDL_SetRenderDrawColor(SDL_Renderer * renderer, Uint8 r, Uint8 g, Uint8 b, Uint8 a);
函数介绍:使用此函数可以用绘图颜色清除当前呈现目标
参数:renderer:要操作的渲染器
返回值:成功返回0,失败返回负的错误码int SDL_RenderClear(SDL_Renderer * renderer);
函数介绍:此函数用于更新屏幕
参数:renderer:要操作的渲染器void SDL_RenderPresent(SDL_Renderer * renderer);
函数介绍:使用此函数可以破坏窗口的呈现上下文和自由关联的纹理
参数:renderer:要操作的渲染器void SDL_DestroyRenderer(SDL_Renderer * renderer);
函数介绍:使用此函数可以破坏窗口
参数:window:要操作的窗口void SDL_DestroyWindow(SDL_Window * window);

源码

#include <SDL.h>
#include <stdio.h>
#include <unistd.h>int main(int argc, char *argv[])
{SDL_Window *pSDLWindow = NULL;SDL_Renderer *pSDLRender = NULL;SDL_Init(SDL_INIT_VIDEO); /* 初始化视频系统 *//* 创建一个起点(0,0),大小640x480的可视窗口 */pSDLWindow = SDL_CreateWindow("sdl window", 0, 0, 640, 480 ,SDL_WINDOW_SHOWN);if(!pSDLWindow){printf("SDL_CreateWindow error!\n");goto end;}/* 创建一个渲染器(操作窗口的上下文) */pSDLRender = SDL_CreateRenderer(pSDLWindow, -1, 0);if(!pSDLRender){printf("SDL_CreateRenderer error!\n");goto end;}/* 设置用于绘制窗口的颜色 */SDL_SetRenderDrawColor(pSDLRender, 0, 255, 0, 255);/* 用当前pSDLRender的颜色去清楚窗口 */SDL_RenderClear(pSDLRender);/* 更新窗口 */SDL_RenderPresent(pSDLRender);sleep(5);end:if(pSDLRender)SDL_DestroyRenderer(pSDLRender); if(pSDLWindow)SDL_DestroyWindow(pSDLWindow);SDL_Quit();return 0;
}

运行效果

2.SDL的处理事件

上述例子中会发现,在程序运行中是无法去操作窗口的,甚至无法强制关闭,要解决这些问题,就需要使用SDL的事件了。

SDL的事件使用也非常简单

只需要调用"SDL_WaitEvent"函数就可以阻塞等待事件,调用"SDL_PollEvent"则为轮询事件

如果想自己产生事件,可以调用 “SDL_PushEvent”,此函数会把产生的事件放入队列中

函数详解

函数介绍:使用此函数无限期地等待下一个可用事件
参数:event:返回队列中的一个事件,或者NULL
返回值:成功返回1,失败返回0int SDL_WaitEvent(SDL_Event * event);
函数介绍:使用此函数等待下一个可用事件,等待时间超过timeout返回
参数:event:返回队列中的一个事件,或者NULLtimeout:等待最长时间
返回值:成功返回1,失败返回0int SDL_WaitEventTimeout(SDL_Event* event, int timeout);
函数介绍:使用此函数去轮询当前挂起的事件
参数:event:返回队列中的一个事件,或者NULL
返回值:成功返回1,失败返回0int SDL_PollEvent(SDL_Event* event);
函数介绍:使用此函数去将一个事件添加到事件队列中
参数:event:要添加的事件
返回值:成功返回1,失败返回0int SDL_PushEvent(SDL_Event* event);

事件的类型有哪些(SDL_Event->type)

SDL_QUIT:退出事件,关闭窗口会触发此类事件
SDL_WINDOWEVENT:窗口事件
SDL_MOUSEMOTION:鼠标移动事件
SDL_KEYDOWN:按键事件
...

此外还可以自定义事件类型

使用 SDL_USEREVENT 该宏就可以定义自己的事件类型,例如:

#define MY_EVENT1  (SDL_USEREVENT + 1)
#define MY_EVENT2  (SDL_USEREVENT + 2)

示例代码

#include <SDL.h>
#include <stdio.h>
#include <unistd.h>int main(int argc, char *argv[])
{SDL_Window *pSDLWindow = NULL;SDL_Renderer *pSDLRender = NULL;SDL_Event sdlEvent;int quit = 0;SDL_Init(SDL_INIT_VIDEO); /* 初始化视频系统 *//* 创建一个起点(0,0),大小640x480的可视窗口 */pSDLWindow = SDL_CreateWindow("sdl window", 0, 0, 640, 480 ,SDL_WINDOW_SHOWN);if(!pSDLWindow){printf("SDL_CreateWindow error!\n");goto end;}/* 创建一个渲染器(操作窗口的上下文) */pSDLRender = SDL_CreateRenderer(pSDLWindow, -1, 0);if(!pSDLRender){printf("SDL_CreateRenderer error!\n");goto end;}/* 设置用于绘制窗口的颜色 */SDL_SetRenderDrawColor(pSDLRender, 0, 255, 0, 255);/* 用当前pSDLRender的颜色去清楚窗口 */SDL_RenderClear(pSDLRender);/* 更新窗口 */SDL_RenderPresent(pSDLRender);while(!quit){/* 等到事件 */SDL_WaitEvent(&sdlEvent);switch(sdlEvent.type){case SDL_QUIT: /* 退出事件 */printf("quit!\n");quit = 1;break;case SDL_MOUSEMOTION: /* 鼠标移动事件 */printf("mouse move!\n");break;default:break;}}end:if(pSDLRender)SDL_DestroyRenderer(pSDLRender);if(pSDLWindow)SDL_DestroyWindow(pSDLWindow);SDL_Quit();return 0;
}

现在点击窗口左上角的"x",就可以正常关闭窗口了

3.SDL的纹理渲染(SDL_Texture)

sdl的纹理渲染,让我们可以在窗口上显示我们想显示的画面,播放器的实现就是不断将图片刷到窗口上,所以这一步对播放器的实现非常重要。

SDL_Texture可以看作是一块视频缓存区,我们可以在它上面绘制图像信息,然后在刷到窗口上

SDL提供了非常好用的操作SDL_Texture的方法,使用SDL_Texute的基本步骤

  • 创建一个 SDL_Texture。
  • 渲染 Texture
  • Destory Texture

下面我们将实现一个功能,在窗口上显示一个不断移动的方块

流程图如下

SDL_CreateTexture 创建一个纹理

SDL_SetRenderTarget 改变渲染器的操作对象

SDL_SetRenderDrawColor 绘制渲染器

SDL_RenderClear 将渲染器的画面刷到纹理上

SDL_Rect 定义一个方块并设置它的大小与位置

SDL_RenderDrawRect 使用渲染器来渲染方块

SDL_SetRenderDrawColor 绘制渲染器

SDL_RenderFillRect 将渲染器的内容刷到方块上

SDL_SetRenderTarget 重新设置渲染器的操作对象为窗口

SDL_RenderCopy 将纹理拷贝到窗口

SDL_RenderPresent 刷新窗口

函数详解

函数介绍:使用此函数创建一个纹理
参数:renderer:渲染器format:纹理的像素格式SDL_PIXELFORMAT_RGBA8888SDL_PIXELFORMAT_IYUV...access:纹理的访问模式SDL_TEXTUREACCESS_STATIC 很少更改,不能锁定SDL_TEXTUREACCESS_STREAMING 频繁改变,锁定SDL_TEXTUREACCESS_TARGET 可以作为渲染的目标,即可以被渲染器渲染
返回值:成功返回创建好的纹理,失败返回NULLSDL_Texture *
SDL_CreateTexture(SDL_Renderer * renderer, Uint32 format, int access, int w, int h);
函数介绍:使用此函数设置一个纹理为渲染器当前的操作对象
参数:renderer:渲染器texture:要操作的纹理
返回值:成功返回0,失败返回负的错误码int SDL_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture);
函数介绍:使用此函数绘制一个矩形在渲染目标上
参数:renderer:渲染器rect:要绘制的矩形
返回值:成功返回0,失败返回负的错误码int SDL_RenderDrawRect(SDL_Renderer * renderer, const SDL_Rect * rect);
函数介绍:使用此函数以绘图颜色填充当前渲染目标上的矩形
参数:renderer:渲染器rect:要绘制的矩形
返回值:成功返回0,失败返回负的错误码int SDL_RenderFillRect(SDL_Renderer * renderer, const SDL_Rect * rect);
函数介绍:使用此函数拷贝纹理到当前的渲染目标上
参数:renderer:渲染器texture:要拷贝的纹理srcrect:纹理的源矩形,可以设置为NULLdstrect:整个渲染对象的目标矩形,可以设置为NULL
返回值:成功返回0,失败返回负的错误码int SDL_RenderCopy(SDL_Renderer * renderer, SDL_Texture * texture,const SDL_Rect * srcrect, const SDL_Rect * dstrect);
方块
typedef struct SDL_Rect
{int x, y; /* 起点 */int w, h; /* 宽高 */
} SDL_Rect;

示例代码

#include <SDL.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>int main(int argc, char *argv[])
{SDL_Window *pSDLWindow = NULL;SDL_Renderer *pSDLRender = NULL;SDL_Event SDLEvent;SDL_Texture *pSDLTexture = NULL;SDL_Rect SDLRect;int quit = 0;SDL_Init(SDL_INIT_VIDEO);pSDLWindow = SDL_CreateWindow("sdl window", 0, 0, 640, 480 ,SDL_WINDOW_SHOWN);if(!pSDLWindow){printf("SDL_CreateWindow error!\n");goto end;}pSDLRender = SDL_CreateRenderer(pSDLWindow, -1, 0);if(!pSDLRender){printf("SDL_CreateRenderer error!\n");goto end;}/* 创建纹理 */pSDLTexture = SDL_CreateTexture(pSDLRender, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 640, 480);if(!pSDLTexture){printf("SDL_CreateTexture error!\n");goto end;}/* 方块 */SDLRect.w = 30;SDLRect.h = 30;SDL_SetRenderDrawColor(pSDLRender, 0, 255, 0, 255);SDL_RenderClear(pSDLRender);SDL_RenderPresent(pSDLRender);while(!quit){SDL_WaitEvent(&SDLEvent);switch(SDLEvent.type){case SDL_QUIT:printf("quit!\n");quit = 1;break;case SDL_MOUSEMOTION:printf("mouse move!\n");break;default:break;}SDLRect.x = rand() % 640;SDLRect.y = rand() % 480;SDL_SetRenderTarget(pSDLRender, pSDLTexture); /* 设置render的操作对象纹理 */SDL_SetRenderDrawColor(pSDLRender, 0, 0, 0, 0); /* 设置纹理的颜色 */SDL_RenderClear(pSDLRender); /* 将画面更新到纹理上 */SDL_RenderDrawRect(pSDLRender, &SDLRect); /* 使用render来绘制方块 */SDL_SetRenderDrawColor(pSDLRender, 255, 0, 0, 0); /* 绘制方块的颜色 */SDL_RenderFillRect(pSDLRender, &SDLRect); /* 将方块刷到纹理上 */SDL_SetRenderTarget(pSDLRender, NULL); /* 改变render的操作对象为window */SDL_RenderCopy(pSDLRender, pSDLTexture, NULL, NULL);SDL_RenderPresent(pSDLRender); /* 显示 */}end:if(pSDLRender)SDL_DestroyRenderer(pSDLRender);if(pSDLWindow)SDL_DestroyWindow(pSDLWindow);SDL_Quit();return 0;
}

运行效果

由于纹理被我们设置为黑色,所以整个窗口的背景为黑色

4.SDL实现YUV播放器

哈哈,终于到了实现YUV播放器了,有了上面的知识,实现一个YUV播放器就是非常简单的事情了

在此之间,先简单说一下YUV格式,此次实验使用的是YUV420P格式

一副YUV420P图像对应的内存为(Y:U:V = 4:1:1):


一个像素点需要1个Y分量,0.25个U分量,0.25个V分量

所以一副宽度为w,高度为h的图像,所需要的内存大小为(w x h x 1.5)

YUV视频就是将这一帧帧的YUV图像放到一起,形成了连贯的视频


下面来看一看程序的流程图

SDL_CreateThread 创建一个线程

init

fread 读取图像

tiemr 定时通知

SDL_UpdateTexture 更新纹理

SDL_RenderPresent 更新窗口

这里我们会创建一个线程,每隔40ms通知一个主线程读取一帧图像并显示

函数详解

函数介绍:使用此函数创建一个线程
参数:fn:线程运行的函数name:线程的名字data:传递参数
返回值:成功返回线程对应的指针,失败返回NULLSDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char* name, void* data);
函数介绍:使用此函数更新纹理
参数:texture:要更新的纹理对象rect:要更新的矩形,可以设置为NULLpixels:原始像素的数据pitch:图像每行的像素数量
返回值:成功返回0,失败返回负的错误码int SDL_UpdateTexture(SDL_Texture * texture, const SDL_Rect * rect, const void *pixels, int pitch);

示例代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <SDL.h>#define REFRESH_EVENT  (SDL_USEREVENT + 1)static int gTimeExit;int refreshTimer(void *pArg)
{gTimeExit=0;while (!gTimeExit){SDL_Event event;event.type = REFRESH_EVENT;SDL_PushEvent(&event);SDL_Delay(40);}gTimeExit=0;return 0;
}void yuvPlayer(const char *fileName, int videoWidth, int videoHeight)
{SDL_Window *pSDLWindow = NULL;SDL_Renderer *pSDLRender = NULL;SDL_Event SDLEvent;SDL_Texture *pSDLTexture = NULL;SDL_Rect SDLRect;SDL_Thread *pSDLThread;FILE *pFile = NULL;int quit = 0;int frameLen;int readLen;unsigned char *pBuf = NULL;frameLen = videoWidth * videoHeight * 3 / 2; /* 一帧数据的大小 */pBuf = malloc(frameLen);if(!pBuf){printf("malloc error!\n");goto end;}SDL_Init(SDL_INIT_VIDEO);pSDLWindow = SDL_CreateWindow("YUV Player", 0, 0, videoWidth, videoHeight ,SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);if(!pSDLWindow){printf("SDL_CreateWindow error!\n");goto end;}pSDLRender = SDL_CreateRenderer(pSDLWindow, -1, 0);if(!pSDLRender){printf("SDL_CreateRenderer error!\n");goto end;}/* 创建纹理     */pSDLTexture = SDL_CreateTexture(pSDLRender, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, videoWidth, videoHeight);if(!pSDLTexture){printf("SDL_CreateTexture error!\n");goto end;}pFile = fopen(fileName, "r");if(!pFile){printf("fopen error!\n");goto end;}SDL_SetRenderDrawColor(pSDLRender, 0, 255, 0, 255);SDL_RenderClear(pSDLRender);SDL_RenderPresent(pSDLRender);/* 创建一个线程 */pSDLThread = SDL_CreateThread(refreshTimer, "timer", NULL);while(!quit){SDL_WaitEvent(&SDLEvent);switch(SDLEvent.type){case SDL_QUIT:printf("quit!\n");gTimeExit = 1;quit = 1;break;case REFRESH_EVENT:/* 以下是显示图像 */readLen = fread(pBuf, 1, frameLen, pFile);if(readLen <= 0){printf("fread error!\n");goto end;}SDL_UpdateTexture(pSDLTexture, NULL, pBuf, videoWidth);SDL_RenderClear(pSDLRender);SDL_RenderCopy(pSDLRender, pSDLTexture, NULL, NULL);SDL_RenderPresent(pSDLRender);//SDL_GetWindowSize(pSDLWindow, &videoWidth, &videoHeight); /* 获取窗口的宽度和高度 */break;default:break;}}end:if(pBuf)free(pBuf);if(pSDLRender)SDL_DestroyRenderer(pSDLRender);if(pSDLWindow)SDL_DestroyWindow(pSDLWindow);SDL_Quit();}int main(int argc, char *argv[])
{char *fileName = NULL;int videoWidth, videoHeight;if(argc != 4){printf("Usage: %s <file name> <video width> <video height>\n", argv[0]);return -1;}fileName = argv[1];videoWidth = atoi(argv[2]);videoHeight = atoi(argv[3]);printf("video :%s, video width:%d ,video height:%d \n", fileName, videoWidth, videoHeight);yuvPlayer(fileName, videoWidth, videoHeight);return 0;
}

下面介绍一些获取yuv数据的方法

ffmpeg -i out.mp4 -vf scale=640:480 out_640x480.mp4 -hide_banner

ffmpeg -i out_640x480.mp4 -an -c:v rawvideo -pix_fmt yuv420p out_640x480.yuv

./a.out out_640x480.yuv 640 480

下面请欣赏葫芦娃

哈哈哈,第一次用markdown写博文,体验极佳。

本文链接:https://my.lmcjl.com/post/6274.html

展开阅读全文

4 评论

留下您的评论.