SDL視頻顯示的流程
實現流程
準備視頻文件
準備一個格式為yuv420p
,分辨率為320x240
的yuv
數據,并且將視頻文件放入項目構建的目錄下:
初始化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 240SDL_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_FORMAT
為YUV420P
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 eventSDL_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, planeSDL_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, planeSDL_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 ResizeSDL_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_IYUVint 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 eventSDL_Event event;event.type = QUIT_EVENT;SDL_PushEvent(&event);printf("finish Timer\n");return 0;
}
#undef main
int main(int argc, char* argv[])
{//初始化 SDLif(SDL_Init(SDL_INIT_VIDEO)){fprintf( stderr, "Could not initialize SDL - %s\n", SDL_GetError());return -1;}// SDLSDL_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, planeSDL_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 ResizeSDL_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