FFmpeg06:SDL渲染
SDL介绍
- SDL(Simple DirectMedia Layer)
- 由C语言实现的跨平台的媒体开源库
- 多用于游戏、模拟器、媒体播放器等多媒体应用领域
SDL编译与安装
- 拉取源码
- 先切换分支为
SDL2
- 执行autogen.sh生成configure (可不执行)
- 生成Makefile:
configure --prefix=/usr/local
- 安装
sudo make -j 8 && sudo make install
SDL使用
- 头文件
#include<SDL.h>
- 初始化SDL
- 退出SDL
渲染窗口
- SDL_Init/SDL_Quit()
- SDL_CreateWindow()/ SDL_DestoryWindow()
- SDL_CreateRender()/SDL_DestoryRender()
- SDL_RenderClear
- SDL_RenderPresent
#include <SDL.h>int main(int argc, char* argv[]) {SDL_Init(SDL_INIT_VIDEO);SDL_Window* window = SDL_CreateWindow("hello sdl", 200, 200, 640, 470, SDL_WINDOW_SHOWN);SDL_Renderer* render;SDL_Event event;if (!window) {printf("Failed to create window: %s\n", SDL_GetError());goto __EXIT;}render = SDL_CreateRenderer(window, -1, 0);if (!render) {printf("Failed to create renderer: %s\n", SDL_GetError());goto __EXIT;}SDL_PollEvent(&event); // 没有事件处理就不会显示窗口SDL_SetRenderDrawColor(render, 255, 10, 0, 255); // 必须在RenderClear之前设置颜色SDL_RenderClear(render);SDL_RenderPresent(render); // 显示SDL_Delay(300000);__EXIT:SDL_DestroyRenderer(render);SDL_DestroyWindow(window);SDL_Quit();return 0;
}
事件基本原理
- SDL将所有事件都存放在一个队列中
- 所有对事件的操作,其实就是对队列的操作
事件种类
- SDL_WindowEvent:窗口事件
- SDL_KeyboardEvent:键盘事件
- SDL_MouseMotionEvent:鼠标事件
事件处理
- SDL_PollEvent:轮询检测队列
- SDL_WaitEvent:事件触发机制
#include <SDL.h>int main(int argc, char* argv[]) {SDL_Init(SDL_INIT_VIDEO);SDL_Window* window = SDL_CreateWindow("hello sdl", 200, 200, 640, 470, SDL_WINDOW_SHOWN);SDL_Renderer* render;SDL_Event event;int quit = 1;if (!window) {printf("Failed to create window: %s\n", SDL_GetError());goto __EXIT;}render = SDL_CreateRenderer(window, -1, 0);if (!render) {printf("Failed to create renderer: %s\n", SDL_GetError());goto __EXIT;}// SDL_PollEvent(&event); // 没有事件处理就不会显示窗口SDL_SetRenderDrawColor(render, 255, 10, 0, 255); // 必须在RenderClear之前设置颜色SDL_RenderClear(render);SDL_RenderPresent(render); // 显示do {SDL_WaitEvent(&event);switch (event.type){case SDL_QUIT:quit = 0;break;default:SDL_Log("event type: %d", event.type);break;}} while(quit);// SDL_Delay(300000);__EXIT:SDL_DestroyRenderer(render);SDL_DestroyWindow(window);SDL_Quit();return 0;
}
纹理渲染
为什么要使用纹理:假设不使用纹理显示内容,则需要将整个YUV或者RGB数据在CPU计算好,然后传给GPU显示;
而使用纹理则只需要将从哪到哪怎么显示这些信息在CPU准备好,传给GPU进行实际的计算和显示
渲染基本原理
纹理相关API
- SDL_CreateTexture()
- format: YUV, RGB
- access: Texture类型,Target,Stream
- SDL_DestoryTexture()
渲染相关API
- SDL_SetRenderTarget()
- SDL_RenderClear()
- SDL_RenderCopy()
- SDL_RenderPresent()
#include <SDL.h>int main(int argc, char* argv[]) {SDL_Init(SDL_INIT_VIDEO);SDL_Window* window = SDL_CreateWindow("hello sdl", 200, 200, 640, 480, SDL_WINDOW_SHOWN);SDL_Renderer* render;SDL_Event event;SDL_Texture* texture;SDL_Rect rect;rect.w = 30;rect.h = 30;int quit = 1;if (!window) {printf("Failed to create window: %s\n", SDL_GetError());goto __EXIT;}render = SDL_CreateRenderer(window, -1, 0);if (!render) {printf("Failed to create renderer: %s\n", SDL_GetError());goto __EXIT;}// SDL_PollEvent(&event); // 没有事件处理就不会显示窗口SDL_SetRenderDrawColor(render, 255, 10, 0, 255); // 必须在RenderClear之前设置颜色SDL_RenderClear(render);SDL_RenderPresent(render); // 显示texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 640, 480);if (!texture) {printf("Failed to create texture: %s\n", SDL_GetError());goto __EXIT;}do {SDL_PollEvent(&event); // 将waitEvent改成PollEvent就不会阻塞, 因为PollEvent会轮询事件switch (event.type){case SDL_QUIT:quit = 0;break;default:SDL_Log("event type: %d", event.type);break;}rect.x = (rand() % (640 - rect.w));rect.y = (rand() % (480 - rect.h));SDL_SetRenderTarget(render, texture); // 设置渲染目标为纹理SDL_SetRenderDrawColor(render, 0, 0, 0, 0);SDL_RenderClear(render);SDL_RenderDrawRect(render, &rect); // 绘制矩形边框SDL_SetRenderDrawColor(render, 255, 0, 0, 0);SDL_RenderFillRect(render, &rect); // 填充矩形SDL_SetRenderTarget(render, NULL); // 恢复渲染目标为默认SDL_RenderCopy(render, texture, NULL, NULL); // 将纹理复制到渲染器SDL_RenderPresent(render); // 显示} while(quit);// SDL_Delay(300000);__EXIT:SDL_DestroyRenderer(render);SDL_DestroyWindow(window);SDL_DestroyTexture(texture);SDL_Quit();return 0;
}
实现YUV播放器
SDL创建线程
- SDL_CreateThread:fn线程执行函数, name线程名,data执行函数参数
SDL更新纹理
- SDL_UpdateTexture()
- SDL_UpdateYUVTexture
yuvPlayer代码
构造测试要用的yuv,分辨率和pix_fmt要跟代码中一致
ffmpeg -i input.mp4 -s 608x368 -r 25 -pix_fmt yuv420p -f rawvideo output.yuv
#include <SDL.h>
#include <fstream>
using namespace std;#define BLOCK_SIZE 4096000 // 4MB
//event message
#define REFRESH_EVENT (SDL_USEREVENT + 1)
#define QUIT_EVENT (SDL_USEREVENT + 2)int thread_exit = 0;
int refreshVideoTimer(void *updata) {thread_exit = 0;while (!thread_exit) {SDL_Event event;event.type = REFRESH_EVENT;SDL_PushEvent(&event);SDL_Delay(40); // 25fps}thread_exit = 0;// 推送退出消息SDL_Event event;event.type = QUIT_EVENT;SDL_PushEvent(&event);return 0;
}
int main(int argc, char* argv[]) {fstream fs("output.yuv", ios::in | ios::binary);if (!fs.is_open()) {printf("open file failed\n");return -1;} SDL_Event event;SDL_Rect rect;Uint32 pixformat = 0;SDL_Window *window = nullptr;SDL_Renderer *render = nullptr;SDL_Texture *texture = nullptr;SDL_Thread *timerThread = nullptr;int wWidth = 640;int wHeight = 480;const int videoWidth = 608;const int videoHeight = 368;Uint8 *videoPos = nullptr;Uint8 *videoEnd = nullptr;unsigned int remainLen = 0;unsigned int videoBuffLen = 0;unsigned int blankSpaceLen = 0;Uint8 videoBuf[BLOCK_SIZE] = {0};// 只有yuv420p才这样计算// y:w*h, u:w*h/4, v:w*h/4// yuv420p: 1.5 bytes per pixelconst unsigned int yuvFrameLen = videoWidth * videoHeight * 12 / 8;if (SDL_Init(SDL_INIT_VIDEO)) {printf("SDL_Init failed: %s\n", SDL_GetError());return -1;}window = SDL_CreateWindow("yuv player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, wWidth, wHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);if (!window) {printf("SDL_CreateWindow failed: %s\n", SDL_GetError());goto __FAIL;}render = SDL_CreateRenderer(window, -1, 0);//IYUV: Y + U + V (3 planes)//YV12: Y + V + U (3 planes)pixformat = SDL_PIXELFORMAT_IYUV; // YUV420P// 创建纹理 SDL_TEXTUREACCESS_STREAMING去了解一下learn_OPENGL里有提及texture = SDL_CreateTexture(render, pixformat, SDL_TEXTUREACCESS_STREAMING, videoWidth, videoHeight);// 从文件读取数据到缓冲区fs.read((char*)videoBuf, BLOCK_SIZE);videoBuffLen = fs.gcount();// 设置视频指针位置videoPos = videoBuf;videoEnd = videoBuf + videoBuffLen;blankSpaceLen = BLOCK_SIZE - videoBuffLen;timerThread = SDL_CreateThread(refreshVideoTimer, nullptr, nullptr);if (!timerThread) {printf("SDL_CreateThread failed: %s\n", SDL_GetError());goto __FAIL;}do {// wait eventSDL_WaitEvent(&event);if (event.type == REFRESH_EVENT) {// 不够数据展示if (videoPos + yuvFrameLen > videoEnd) {// 将剩余数据移到缓冲区头部remainLen = videoEnd - videoPos;if (remainLen > 0 && !blankSpaceLen) {memcpy(videoBuf, videoPos, remainLen);blankSpaceLen = BLOCK_SIZE - remainLen;videoPos = videoBuf;videoEnd = videoBuf + remainLen;}if (videoEnd == videoBuf + BLOCK_SIZE) {videoPos = videoBuf;videoEnd = videoBuf;blankSpaceLen = BLOCK_SIZE;}// 从文件读取数据到缓冲区fs.read((char*)(videoEnd), blankSpaceLen);videoBuffLen = fs.gcount();if (videoBuffLen <= 0) {printf("file read end, exit thread\n");thread_exit = 1;continue; // 等待waitEvent退出}videoEnd += videoBuffLen;blankSpaceLen -= videoBuffLen;printf("not enought data: pos:%p, video_end:%p, blank_space_len:%d\n", videoPos, videoEnd, blankSpaceLen);}SDL_UpdateTexture(texture, nullptr, videoPos, videoWidth);// 如果窗口resizerect.x = 0;rect.y = 0;rect.w = wWidth;rect.h = wHeight;SDL_RenderClear(render);SDL_RenderCopy(render, texture, nullptr, &rect);SDL_RenderPresent(render);printf("not enought data: pos:%p, video_end:%p, blank_space_len:%d\n", videoPos, videoEnd, blankSpaceLen);videoPos += yuvFrameLen;} else if (event.type == SDL_WINDOWEVENT) {// 窗口大小改变SDL_GetWindowSize(window, &wWidth, &wHeight);} else if (event.type == SDL_QUIT) {thread_exit = 1;} else if (event.type == QUIT_EVENT) {printf("receive quit event, exit\n");break;}} while (1);__FAIL:if (timerThread) {SDL_WaitThread(timerThread, nullptr);timerThread = nullptr;}if (texture) {SDL_DestroyTexture(texture);texture = nullptr;}if (render) {SDL_DestroyRenderer(render);render = nullptr;}if (window) {SDL_DestroyWindow(window);window = nullptr;}SDL_Quit();fs.close();return 0;
}
SDL音频处理
播放音频基本流程
播放音频的基本原则
- 声卡向你要数据而不是你主动推给声卡
- 数据的多少由音频参数决定
SDL音频API
- SDL_OpenAudio/SDL_CloseAudio
- SDL_PauseAudio
- SDL_MixAudio
pcmPlayer代码
构造代码对应的pcm文件
ffmpeg -i input.mp4 -vn -c:a pcm_s16le -ar 44100 -ac 2 -f s16le output.pcm
#include <SDL.h>
#include <fstream>
using namespace std;
#define BLOCK_SIZE 4096000static Uint8 *audioBuf = NULL;
static Uint8 *audioPos = NULL;
static size_t bufferLen = 0;// 音频回调函数
void audio_callback(void *userdata, Uint8 *stream, int len) {if (bufferLen == 0) {return;}SDL_memset(stream, 0, len); // 先将stream清0len = (len > bufferLen ? bufferLen : len);// 混音,将解码后的音频数据混合到streamSDL_MixAudio(stream, audioPos, len, SDL_MIX_MAXVOLUME);audioPos += len;bufferLen -= len;
}int main(int argc, char* argv[]) {int res = -1;fstream fs("output.pcm", ios::in | ios::binary);if (!fs.is_open()) {printf("open file failed\n");return -1;}SDL_AudioSpec spec;if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf("SDL_Init failed: %s\n", SDL_GetError());return -1;}// 申请音频缓冲区audioBuf = new Uint8[BLOCK_SIZE];if (!audioBuf) {printf("new audioBuf failed\n");goto __FAIL;}// 设置音频参数spec.freq = 44100; // 采样率spec.format = AUDIO_S16SYS; // 采样格式spec.channels = 2; // 声道数spec.silence = 0; // 静音数据spec.samples = 1024; // 采样点数spec.callback = audio_callback; // 音频回调函数spec.userdata = nullptr; // 传递给回调函数的参数// 打开音频设备if (SDL_OpenAudio(&spec, nullptr) < 0) {printf("SDL_OpenAudio failed: %s\n", SDL_GetError());goto __FAIL;}// 开始播放SDL_PauseAudio(0);do {// 从文件读取数据到缓冲区fs.read((char*)audioBuf, BLOCK_SIZE);bufferLen = fs.gcount();if (bufferLen <= 0) {printf("file read end, exit\n");break;}audioPos = audioBuf;// 等待数据播放完while (audioPos < (audioBuf + BLOCK_SIZE)) {SDL_Delay(1);}} while (bufferLen != 0);SDL_CloseAudio();res = 0;__FAIL:if (audioBuf) {delete[] audioBuf;audioBuf = nullptr;}SDL_Quit();return res;
}