在上一個程序SDL2:第三個程序(Mac):顯示任意圖片我們利用sdl2_image庫已經可以做到顯示任意格式的圖片了。
可我這種最終要利用SDL2做視頻工具的男人,怎麼會滿足於小小的圖片呢。
因爲FFmpeg解碼出來的視頻幀數據是以YUV數據的格式展現,所以我必須先弄清楚YUV格式,在SDL2中是怎麼顯示的。
爲此,我專門用FFmpeg命令,將小王子的圖片從jpg格式,轉成了I420的YUV4:2:0的格式,以便測試。下載鏈接如下:little_prince_yv12_960x540.yuv
不知道爲啥,下載居然要積分,大家可以自己利用ffmpeg轉一個就成。
ffmpeg -pix_fmt yuv420p -s 960x540 -i little_prince.jpg little_prince_yuv420p_960x540.yuv
- -pix_fmt:表示要用什麼格式轉換,yuv420p是參數,你也可以通過
ffmpeg -pix_fmts
查看其它支持的類型。 - -s:表示一幀的尺寸,這個尺寸將是你生成yuv數據的寬和高,需要牢牢記住,因爲轉換成yuv數據後,數據將不會存儲任何無關信息,包括尺寸。
- -i:很簡單,就是需要轉換的文件,i表示input。參數不僅可以是jpg格式的,也可以是bmp等任何其它常見類型。
- little_prince_yuv420p_960x540.yuv:我的輸出文件名,在文件名中,記錄了yuv的格式,和尺寸,這些信息在顯示過程中比較重要。
下面來看看代碼:
代碼
代碼是在SDL2:第三個程序(Mac):顯示任意圖片基礎上擴展而來,cmake部分,毫無變化:
cmake_minimum_required(VERSION 3.10)
project(PlaySDL)
set(CMAKE_CXX_STANDARD 11)
set(MY_LIBRARY_DIR /usr/local/Cellar)
set(SDL_DIR ${MY_LIBRARY_DIR}/sdl2/2.0.9_1/)
set(SDL_IMAGE_DIR ${MY_LIBRARY_DIR}/sdl2_image/2.0.4/)
include_directories(${SDL_DIR}/include/SDL2/
${SDL_IMAGE_DIR}/include/)
link_libraries(${SDL_DIR}/lib/
${SDL_IMAGE_DIR}/lib/)
add_executable(PlaySDL main.cpp)
target_link_libraries(PlaySDL SDL2 SDL2_image)
其中有部分關於sdl2_image的庫,在本次的程序中使用不到的可以直接刪掉。
下面是代碼部分:
#include <iostream>
#include <SDL.h>
#include <SDL2/SDL_image.h>
using namespace std;
const int WIDTH = 960, HEIGHT = 540;
int main() {
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
cout << "SDL could not initialized with error: " << SDL_GetError() << endl;
return -1;
}
SDL_Window *window = SDL_CreateWindow("Hello SDL world!", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
WIDTH, HEIGHT, SDL_WINDOW_ALLOW_HIGHDPI);
if (NULL == window) {
cout << "SDL could not create window with error: " << SDL_GetError() << endl;
return -1;
}
if (!(IMG_Init(IMG_INIT_JPG) & IMG_INIT_JPG)) {
cout << "SDL_image could not init with error: " << IMG_GetError() << endl;
return -1;
}
FILE* pFile = fopen("little_prince_i420_960x540.yuv", "rb");
if (pFile == NULL) {
cerr << "little_prince_i420_960x540.yuv open failed" << endl;
}
unsigned char *m_yuv_data;
int frameSize = HEIGHT * WIDTH * 3 / 2; // 單幀數據的bit數
m_yuv_data = (unsigned char*)malloc(frameSize * sizeof(unsigned char));
fread(m_yuv_data, 1, frameSize, pFile);
fclose(pFile);
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0); // 創建渲染器
// 創建紋理
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
if (texture != NULL) {
SDL_Event windowEvent;
while (true) {
if (SDL_PollEvent(&windowEvent)) {
if (SDL_QUIT == windowEvent.type) {
cout << "SDL quit!!" << endl;
break;
}
}
SDL_UpdateTexture(texture, NULL, m_yuv_data, WIDTH); // 更新紋理
SDL_RenderClear(renderer); // 清楚渲染器
SDL_RenderCopy(renderer, texture, NULL, NULL); // 拷貝渲染器到紋理。
SDL_RenderPresent(renderer); // 渲染
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
}
先來看一下顯示效果:
YUV數據提取
這裏需要講一下關於YUV數據填充的部分,即:
FILE* pFile = fopen("little_prince_i420_960x540.yuv", "rb");
unsigned char *m_yuv_data;
int frameSize = HEIGHT * WIDTH * 3 / 2; // 單幀數據的bit數
m_yuv_data = (unsigned char*)malloc(frameSize * sizeof(unsigned char));
fread(m_yuv_data, 1, frameSize, pFile);
fclose(pFile);
fopen就不用說了,通過fopen獲得我們前面通過ffmpeg製作好的yuv文件little_prince_yv12_960x540.yuv的句柄。核心問題是,如何從這個句柄中,提取出我們需要的Y、U、V分量。
關鍵在於YUV數據的格式和size。格式決定了YUV分量在文件中的排列方式,size決定了總的像素值。我們已經知道圖片是I420格式。也就是yuv420p,它是三平面存儲,每個像素內存計算方式是:
亮度[(4) + U(1) + V(1)]/4(像素) = 3/2bit
所有I420單幀圖片要佔用的內存爲:int frameSize = HEIGHT * WIDTH * 3 / 2
接着就是分配一個無符號的內存空間,來存儲從文件中讀取的yuv數據。
本次程序的流程圖如下:
代碼流程圖中,黃色部分函數調用,是從第一個程序copy過來的,它幾乎是所有SDL程序的必要流程了,要豐富SDL程序,都是在黃色框架之間擴充功能和代碼,相關函數解釋可以查看:
SDL2常用函數&結構分析:SDL_Window&SDL_CreateWindow
SDL_Window
SDL2常用函數&結構分析:SDL_Event&SDL_PollEvent
綠色部分,是本次代碼新增,用於顯示YUV的極簡邏輯,從文件提取yuv數據在本文中已經說到,其它沒有提到的相關函數解釋可以查看:
SDL2常用函數&結構分析:SDL_Renderer&SDL_CreateRenderer&SDL_RenderCopy&SDL_RenderPresent
SDL2常用函數&結構分析:SDL_Texture&SDL_CreateTexture&SDL_UpdateTexture
github地址: