使用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 使用OpenGL
		SDL_WINDOW_SHOWN 窗口可见
		SDL_WINDOW_RESIZABLE 窗口能改变大小
		...
返回值:
    成功返回创建好的窗口,失败返回NULL

SDL_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 和显示器的刷新率同步
返回值:
    成功返回创建好的渲染器,失败返回NULL

SDL_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,失败返回0

int SDL_WaitEvent(SDL_Event * event);
函数介绍:
    使用此函数等待下一个可用事件,等待时间超过timeout返回
参数:
    event:返回队列中的一个事件,或者NULL
    timeout:等待最长时间
返回值:
    成功返回1,失败返回0

int SDL_WaitEventTimeout(SDL_Event* event, int timeout);
函数介绍:
    使用此函数去轮询当前挂起的事件
参数:
    event:返回队列中的一个事件,或者NULL
返回值:
    成功返回1,失败返回0

int SDL_PollEvent(SDL_Event* event);
函数介绍:
    使用此函数去将一个事件添加到事件队列中
参数:
    event:要添加的事件
返回值:
    成功返回1,失败返回0

int 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_RGBA8888
    	SDL_PIXELFORMAT_IYUV
    	...
    access:纹理的访问模式
    	SDL_TEXTUREACCESS_STATIC 很少更改,不能锁定
    	SDL_TEXTUREACCESS_STREAMING 频繁改变,锁定
    	SDL_TEXTUREACCESS_TARGET 可以作为渲染的目标,即可以被渲染器渲染
返回值:
    成功返回创建好的纹理,失败返回NULL

SDL_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:纹理的源矩形,可以设置为NULL
    dstrect:整个渲染对象的目标矩形,可以设置为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:传递参数
返回值:
    成功返回线程对应的指针,失败返回NULL

SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char* name, void* data);
函数介绍:
    使用此函数更新纹理
参数:
    texture:要更新的纹理对象
    rect:要更新的矩形,可以设置为NULL
    pixels:原始像素的数据
    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获取yuv原始数据

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

运行

./a.out out_640x480.yuv 640 480

下面请欣赏葫芦娃

在这里插入图片描述

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

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值