【音视频】SDL渲染YUV格式像素

发布于:2025-04-14 ⋅ 阅读:(28) ⋅ 点赞:(0)

SDL视频显示的流程

在这里插入图片描述

实现流程

准备视频文件

准备一个格式为yuv420p,分辨率为320x240yuv数据,并且将视频文件放入项目构建的目录下:

在这里插入图片描述

初始化SDL

初始化SDL的视频模块

//初始化 SDL
if(SDL_Init(SDL_INIT_VIDEO))
{
	fprintf( stderr, "Could not initialize SDL - %s\n", SDL_GetError());
	return -1;
}

创建一个窗口

  • 创建一个SDL窗口,用于纹理渲染
  • 初始时窗口大小为YUV视频分辨率大小
#define YUV_WIDTH   320
#define YUV_HEIGHT  240

SDL_Window *window = NULL;
int win_width = YUV_WIDTH;
int win_height = YUV_WIDTH;
window = SDL_CreateWindow("Simplest YUV Player",
					   SDL_WINDOWPOS_UNDEFINED,
					   SDL_WINDOWPOS_UNDEFINED,
					   video_width, video_height,
					   SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
if(!window)
{
	fprintf(stderr, "SDL: could not create window, err:%s\n",SDL_GetError());
	goto _FAIL;
}

设置YUV缓存

设置每次读取视频帧的缓存大小,这里每次就读取1帧即可,计算方式为:Y分量+U分量+V分量大小

  • YUV格式为yuv420p
  • Y分量大小为:Width * Height
  • U分量大小为:Width * Height /4
  • V分量大小为:Width * Height / 4
yuv420p格式示例图

在这里插入图片描述

代码如下:

uint8_t *video_buf = NULL; 

uint32_t y_frame_len = video_width * video_height;
uint32_t u_frame_len = video_width * video_height / 4;
uint32_t v_frame_len = video_width * video_height / 4;
uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;

video_buf = (uint8_t*)malloc(yuv_frame_len);
if(!video_buf)
    {
        fprintf(stderr, "Failed to alloce yuv frame space!\n");
        goto _FAIL;
    }

创建渲染器

创建渲染器,用于视频纹理的渲染

SDL_Renderer *renderer = NULL;  
renderer = SDL_CreateRenderer(window, -1, 0);

创建纹理

根据yuv420p数据格式

其中:

  • YUV_FORMATYUV420P
  • SDL_TEXTUREACCESS_STREAMING 指定纹理的访问方式,STREAMING模式允许高效更新纹理数据,用于视频渲染
SDL_Texture *texture = NULL; 
uint32_t pixformat = YUV_FORMAT; // YUV420P
texture = SDL_CreateTexture(renderer,
							pixformat,
							SDL_TEXTUREACCESS_STREAMING,
							video_width,
							video_height);

打开yuv文件

使用文件操作,二进制读的方式打开视频文件

// 打开YUV文件
video_fd = fopen(yuv_path, "rb");
if( !video_fd )
{
	fprintf(stderr, "Failed to open yuv file\n");
	goto _FAIL;
}

创建定时器

  • 创建一个定时器,用于定时刷新视频帧,控制fps
  • 实际上就是创建一个新的线程,定期唤醒加入自定义的刷新事件

创建定时器

// 创建请求刷新线程
timer_thread = SDL_CreateThread(refresh_video_timer,
								NULL,
								NULL);

定时器线程函数

每次延时40ms左右,大概25fps

#define REFRESH_EVENT   (SDL_USEREVENT + 1)     // 请求画面刷新事件
#define QUIT_EVENT      (SDL_USEREVENT + 2)     // 退出事件

int s_thread_exit = 0;  // 退出标志 = 1则退出
int refresh_video_timer()
{
    while (!s_thread_exit)
    {
        SDL_Event event;
        event.type = REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }

    s_thread_exit = 0;

    //push quit event
    SDL_Event event;
    event.type = QUIT_EVENT;
    SDL_PushEvent(&event);

    return 0;
}

开启事件循环

开启事件循环,进行视频渲染操作

等待事件

  • 等待事件加入队列中,并阻塞在此
SDL_WaitEvent(&event);

画面刷新事件

  • 定时器周期地加入刷新事件
  • 读取yuv文件,并加入到纹理中
  • 设置渲染的矩形窗口为当前窗口的大小以及开始的位置
  • 将纹理的数据拷贝到CPU
  • 渲染CPU端的数据
 if(event.type == REFRESH_EVENT) // 画面刷新事件
        {
            video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
            if(video_buff_len <= 0)
            {
                fprintf(stderr, "Failed to read data from yuv file!\n");
                goto _FAIL;
            }
            // 设置纹理的数据 video_width = 320, plane
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            // 显示区域,可以通过修改w和h进行缩放
            rect.x = 0;
            rect.y = 0;
			rect.w = win_width;
            rect.h = win_height;

            // 清除当前显示
            SDL_RenderClear(renderer);
            // 将纹理的数据拷贝给渲染器
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // 显示
            SDL_RenderPresent(renderer);
        }

窗口事件

  • 接收窗口事件(如窗口移动,窗口大小改变等)
  • 暂时不做操作

退出事件

  • 包括自定义的退出事件QUIT_EVENT
  • 以及QUIT_EVENT,比如(关闭窗口,调用SDL_QUIT等)
  • 调用退出事件后,会通知定时器停止操作,并且退出循环,进行内存清理

    while (1)
    {
        // 收取SDL系统里面的事件
        SDL_WaitEvent(&event);

        if(event.type == REFRESH_EVENT) // 画面刷新事件
        {
            video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
            if(video_buff_len <= 0)
            {
                fprintf(stderr, "Failed to read data from yuv file!\n");
                goto _FAIL;
            }
            // 设置纹理的数据 video_width = 320, plane
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            // 显示区域,可以通过修改w和h进行缩放
            rect.x = 0;
            rect.y = 0;
			rect.w = win_width;
            rect.h = win_height;

            // 清除当前显示
            SDL_RenderClear(renderer);
            // 将纹理的数据拷贝给渲染器
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // 显示
            SDL_RenderPresent(renderer);
        }
        else if(event.type == SDL_WINDOWEVENT)
        {
            //If Resize
            SDL_GetWindowSize(window, &win_width, &win_height);
            printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n",win_width,
                   win_height );
        }
        else if(event.type == SDL_QUIT) //退出事件
        {
            s_thread_exit = 1;
        }
        else if(event.type == QUIT_EVENT)
        {
            break;
        }
    }

关闭操作和内存清理

渲染结束或手动关闭窗口后,需要关闭文件和SDL子系统,并且释放相关内存

_FAIL:
    s_thread_exit = 1;      // 保证线程能够退出
    // 释放资源
    if(timer_thread)
        SDL_WaitThread(timer_thread, NULL); // 等待线程退出
    if(video_buf)
        free(video_buf);
    if(video_fd)
        fclose(video_fd);
    if(texture)
        SDL_DestroyTexture(texture);
    if(renderer)
        SDL_DestroyRenderer(renderer);
    if(window)
        SDL_DestroyWindow(window);

    SDL_Quit();

    return 0;

}

实现的效果

实现的效果如下:

在这里插入图片描述

整体代码

main.c

#include <stdio.h>
#include <string.h>

#include <SDL.h>

//自定义消息类型
#define REFRESH_EVENT   (SDL_USEREVENT + 1)     // 请求画面刷新事件
#define QUIT_EVENT      (SDL_USEREVENT + 2)     // 退出事件

//定义分辨率
// YUV像素分辨率
#define YUV_WIDTH   320
#define YUV_HEIGHT  240
//定义YUV格式
#define YUV_FORMAT  SDL_PIXELFORMAT_IYUV

int s_thread_exit = 0;  // 退出标志 = 1则退出

int refresh_video_timer()
{
    while (!s_thread_exit)
    {
        SDL_Event event;
        event.type = REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }

    s_thread_exit = 0;

    //push quit event
    SDL_Event event;
    event.type = QUIT_EVENT;
    SDL_PushEvent(&event);

    printf("finish Timer\n");
    return 0;
}
#undef main
int main(int argc, char* argv[])
{
    //初始化 SDL
    if(SDL_Init(SDL_INIT_VIDEO))
    {
        fprintf( stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }

    // SDL
    SDL_Event event;                            // 事件
    SDL_Rect rect;                              // 矩形
    SDL_Window *window = NULL;                  // 窗口
    SDL_Renderer *renderer = NULL;              // 渲染
    SDL_Texture *texture = NULL;                // 纹理
    SDL_Thread *timer_thread = NULL;            // 请求刷新线程
    uint32_t pixformat = YUV_FORMAT;            // YUV420P,即是SDL_PIXELFORMAT_IYUV

    // 分辨率
    // 1. YUV的分辨率
    int video_width = YUV_WIDTH;
    int video_height = YUV_HEIGHT;
    // 2.显示窗口的分辨率
    int win_width = YUV_WIDTH;
    int win_height = YUV_WIDTH;

    // YUV文件句柄
    FILE *video_fd = NULL;
    const char *yuv_path = "yuv420p_320x240.yuv";

    size_t video_buff_len = 0;

    uint8_t *video_buf = NULL; //读取数据后先把放到buffer里面

    // 我们测试的文件是YUV420P格式
    uint32_t y_frame_len = video_width * video_height;
    uint32_t u_frame_len = video_width * video_height / 4;
    uint32_t v_frame_len = video_width * video_height / 4;
    uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;

    //创建窗口
    window = SDL_CreateWindow("Simplest YUV Player",
                           SDL_WINDOWPOS_UNDEFINED,
                           SDL_WINDOWPOS_UNDEFINED,
                           video_width, video_height,
                           SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
    if(!window)
    {
        fprintf(stderr, "SDL: could not create window, err:%s\n",SDL_GetError());
        goto _FAIL;
    }
    // 基于窗口创建渲染器
    renderer = SDL_CreateRenderer(window, -1, 0);
    // 基于渲染器创建纹理
    texture = SDL_CreateTexture(renderer,
                                pixformat,
                                SDL_TEXTUREACCESS_STREAMING,
                                video_width,
                                video_height);

    // 分配空间
    video_buf = (uint8_t*)malloc(yuv_frame_len);
    if(!video_buf)
    {
        fprintf(stderr, "Failed to alloce yuv frame space!\n");
        goto _FAIL;
    }

    // 打开YUV文件
    video_fd = fopen(yuv_path, "rb");
    if( !video_fd )
    {
        fprintf(stderr, "Failed to open yuv file\n");
        goto _FAIL;
    }
    // 创建请求刷新线程
    timer_thread = SDL_CreateThread(refresh_video_timer,
                                    NULL,
                                    NULL);

    while (1)
    {
        // 收取SDL系统里面的事件
        SDL_WaitEvent(&event);

        if(event.type == REFRESH_EVENT) // 画面刷新事件
        {
            video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
            if(video_buff_len <= 0)
            {
                fprintf(stderr, "Failed to read data from yuv file!\n");
                goto _FAIL;
            }
            // 设置纹理的数据 video_width = 320, plane
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            rect.w = win_width;
            rect.h = win_height;
            // 显示区域,可以通过修改w和h进行缩放
            rect.x = 0;
            rect.y = 0;
            // float w_ratio = win_width * 1.0 /video_width;
            // float h_ratio = win_height * 1.0 /video_height;
            // // 320x240 怎么保持原视频的宽高比例
            // rect.w = video_width * w_ratio;
            // rect.h = video_height * h_ratio;


//            rect.w = video_width * 0.5;
//            rect.h = video_height * 0.5;

            // 清除当前显示
            SDL_RenderClear(renderer);
            // 将纹理的数据拷贝给渲染器
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // 显示
            SDL_RenderPresent(renderer);
        }
        else if(event.type == SDL_WINDOWEVENT)
        {
            //If Resize
            SDL_GetWindowSize(window, &win_width, &win_height);
            printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n",win_width,
                   win_height );
        }
        else if(event.type == SDL_QUIT) //退出事件
        {
            s_thread_exit = 1;
        }
        else if(event.type == QUIT_EVENT)
        {
            break;
        }
    }

_FAIL:
    s_thread_exit = 1;      // 保证线程能够退出
    // 释放资源
    if(timer_thread)
        SDL_WaitThread(timer_thread, NULL); // 等待线程退出
    if(video_buf)
        free(video_buf);
    if(video_fd)
        fclose(video_fd);
    if(texture)
        SDL_DestroyTexture(texture);
    if(renderer)
        SDL_DestroyRenderer(renderer);
    if(window)
        SDL_DestroyWindow(window);

    SDL_Quit();

    return 0;

}

更多资料

更多资料参考:https://github.com/0voice