ffmpeg与sdl的个人笔记

ops/2024/11/15 4:52:45/

说明

这里的ffmpeg基础知识和sdl基础知识仅提及与示例代码相关的知识点, 进阶可学习雷神的博客。
https://blog.csdn.net/leixiaohua1020
当然,如代码写的有问题或有更好的见解,欢迎指正!

音视频基础知识

在学习音视频理论知识时,可能会有一些乏味,笔者也是如此,但对于基本原理至少得留个印象

音视频录制原理

在这里插入图片描述

音视频播放原理

在这里插入图片描述

图像表示
  • RGB: red/green/blue,每个像素由8个bit组成
  • YUV: Y:亮度 U/V: 色度
  • YUV格式:有两大类:planar和packed。
    • 对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。
    • 对于packed的YUV格式,每个像素点的Y,U,V是连续交叉存储的。
视频基本概念
  • 视频码率:kb/s,是指视频文件在单位时间内使用的数据流量,也叫码流率。码率越大,说明单位时间内取样率越大,数据流精度就越高。
  • 视频帧率:fps,通常说一个视频的25帧,指的就是这个视频帧率,即1秒中会显示25帧。帧率越高,给人的视觉就越流畅。
  • 视频分辨率:分辨率是x、y方向上的像素点数量。同样大小的图像,分辨率越高越清晰。
视频重要概念(I/P/B帧)

I 帧(Intra coded frames):I帧不需要参考其他画面而生成,解码时仅靠自己就重构完整图像;
I帧图像采用帧内编码方式;
I帧所占数据的信息量比较大;
I帧图像是周期性出现在图像序列中的,出现频率可由编码器选择;
I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;
I帧不需要考虑运动矢量;

P 帧(Predicted frames):根据本帧与相邻的前一帧(I帧或P帧)的不同点来压缩本帧数据,同时利用了空间和时间上的相关性。
P帧属于前向预测的帧间编码。它需要参考前面最靠近它的I帧或P帧来解码。

B 帧(Bi-directional predicted frames):B 帧图像采用双向时间预测,可以大大提高压缩倍数。

音频常见名词
  • 采样频率:每秒钟采样的点的个数。常用的采样频率有:
    22000(22kHz): 无线广播。
    44100(44.1kHz):CD音质。
    48000(48kHz): 数字电视,DVD。
    96000(96kHz): 蓝光,高清DVD。
    192000(192kHz): 蓝光,高清DVD。

  • 采样精度(采样深度):每个“样本点”的大小,
    常用的大小为8bit, 16bit,24bit。

  • 通道数:单声道,双声道,四声道,5.1声道。

  • 比特率:每秒传输的bit数,单位为:bps(Bit Per Second)
    间接衡量声音质量的一个标准。

  • 没有压缩的音频数据的比特率 = 采样频率 * 采样精度 * 通道数。

  • 码率: 压缩后的音频数据的比特率。常见的码率:
    96kbps: FM质量
    128-160kbps:一般质量音频。
    192kbps: CD质量。
    256-320Kbps:高质量音频
    码率越大,压缩效率越低,音质越好,压缩后数据越大。
    码率 = 音频文件大小/时长。

  • 帧:每次编码的采样单元数,比如MP3通常是1152个采样点作为一个编码单元,AAC通常是1024个采样点作为一个编码单元。

  • 帧长:可以指每帧播放持续的时间:每帧持续时间(秒) = 每帧采样点数 / 采样频率(HZ)
    比如:MP3 48k, 1152个采样点,每帧则为 24毫秒
    1152/48000= 0.024 秒 = 24毫秒;
    也可以指压缩后每帧的数据长度。

  • 交错模式:数字音频信号存储的方式。数据以连续帧的方式存放,即首先记录帧1的左声道样本和右声道样本,再开始帧2的记录…

  • 非交错模式:首先记录的是一个周期内所有帧的左声道样本,再记录所有右声道样本

常见的视频封装格式

AVI、MKV、MPE、MPG、MPEG
MP4、WMV、MOV、3GP
M2V、M1V、M4V、OGM
RM、RMS、RMM、RMVB、IFO
SWF、FLV、F4V、
ASF、PMF、XMB、DIVX、PART
DAT、VOB、M2TS、TS、PS

音视频同步

基本概念

  • DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
  • PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

同步方式

  • Audio Master:同步视频到音频
  • Video Master:同步音频到视频
  • External Clock Master:同步音频和视频到外部时钟

ffmpeg__91">ffmpeg 基础知识

ffmpeg_92">ffmpeg封装格式相关函数

◼ avformat_alloc_context();负责申请一个AVFormatContext 结构的内存,并进行简单初始化
◼ avformat_free_context();释放该结构里的所有东西以及该结构本身
◼ avformat_close_input();关闭解复用器。关闭后就不再需要使用avformat_free_context 进行释放。
◼ avformat_open_input();打开输入视频文件
◼ avformat_find_stream_info():获取视频文件信息
◼ av_read_frame(); 读取音视频包
◼ avformat_seek_file(); 定位文件
◼ av_seek_frame():定位文件

解码器相关函数

• avcodec_alloc_context3(): 分配解码器上下文
• avcodec_find_decoder():根据ID查找解码器
• avcodec_find_decoder_by_name():根据解码器名字
• avcodec_open2(): 打开编解码器
• avcodec_decode_video2():解码一帧视频数据
• avcodec_decode_audio4():解码一帧音频数据
• avcodec_send_packet(): 发送编码数据包
• avcodec_receive_frame(): 接收解码后数据
• avcodec_free_context():释放解码器上下文,包含了avcodec_close()
• avcodec_close():关闭解码器

ffmpeg_114">ffmpeg数据结构简介

AVFormatContext: 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。
AVInputFormat demuxer每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。
AVOutputFormat muxer
AVStream 视频文件中每个视频(音频)流对应一个该结构体。
AVCodecContext 编解码器上下文结构体,保存了视频(音频)编解码相关信息。
AVCodec 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
AVPacket 存储一帧压缩编码数据。
AVFrame 存储一帧解码后像素(采样)数据。

AVPacket和AVFrame的关系

在这里插入图片描述

ffmpeg_126">ffmpeg数据结构分析
  • AVFormatContext
    • iformat:输入媒体的AVInputFormat,比如指向AVInputFormat 中 ff_flv_demuxer
    • nb_streams:输入媒体的AVStream 个数
    • streams:输入媒体的AVStream []数组
    • duration:输入媒体的时长(以微秒为单位),计算方式可以参考 av_dump_format()函数。
    • bit_rate:输入媒体的码率
  • AVInputFormat
    • name:封装格式名称
    • extensions:封装格式的扩展名
    • id:封装格式ID
    • 一些封装格式处理的接口函数,比如read_packet()
  • AVStream
    • index:标识该视频/音频流
    • time_base:该流的时基,PTS*time_base=真正的时间(秒)
    • avg_frame_rate: 该流的帧率
    • duration:该视频/音频流长度
    • codecpar:编解码器参数属性
  • AVCodecParameters
    • codec_type:媒体类型AVMEDIA_TYPE_VIDEO/AVMEDIA_TYPE_AUDIO等
    • codec_id:编解码器类型, AV_CODEC_ID_H264/AV_CODEC_ID_AAC等。
  • AVCodecContext
    • codec:编解码器的AVCodec,比如指向AVCodec 中 ff_aac_latm_decoder
    • width, height:图像的宽高(只针对视频)
    • pix_fmt:像素格式(只针对视频)
    • sample_rate:采样率(只针对音频)
    • channels:声道数(只针对音频)
    • sample_fmt:采样格式(只针对音频)
  • AVCodec
    • name:编解码器名称
    • type:编解码器类型
    • id:编解码器ID
    • 一些编解码的接口函数,比如int (*decode)()

ffmpeg_160">下载ffmpeg

ffmpeg__ts__yuv___163">ffmpeg 解码 ts 视频文件得到 yuv 视频文件 程序

环境配置
  1. 创建空项目
    在这里插入图片描述

  2. 填写项目名(大家随意)

  3. 新建一个main.cpp文件

  4. 拷贝ffmpeg到项目路径下
    在这里插入图片描述

  5. ffmpeg-4.2/bin 下的 dll 文件拷贝到项目路径下(即源代码所在目录)
    在这里插入图片描述

  6. 选中项目名,右键选择属性,依次进行如下配置
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

测试代码

#include <stdio.h>extern "C"      //因为ffmpeg是C语言写的,而我们建的是cpp文件.
{
#include "libavformat/avformat.h"
}int main() {const char *p = av_version_info();  //获取ffmpeg版本信息printf("FFmpeg Version : %s ", p);  //打印输出return 0;
}
ffmpeg__ts__yuv_198">ffmpeg 解码 ts 获取 yuv
#pragma warning(disable:4996)#include <stdio.h>extern "C" 
{
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}int main(int argc, char* argv[]) 
{/* 初始化 */AVFormatContext* pFormatContext = NULL; //格式上下文const char* fileName = "believe.ts";    //文件地址int videoIndex = -1;    //视频流索引号int i = 0;  //循环变量AVCodecContext *pCodecContext = NULL;   //编解码上下文AVCodec* pCodec = NULL;     //编解码器AVPacket* pkt = NULL;   //解码前的一帧数据AVFrame* frame = NULL;  //解码后的一帧数据int ret = 0;    //存放avcodec_decode_video2的返回值int gotPicture = 0; //作为avcodec_decode_video2的一个参数av_register_all();      //注册所有组件pFormatContext = avformat_alloc_context();  //分配格式上下文空间/* avformat_open_input返回0表示成功 */if (avformat_open_input(&pFormatContext, fileName, NULL, NULL) != 0){printf("Can't open input %s", fileName);return -1;}/* avformat_find_stream_info返回值 >= 0 表示成功 */if (avformat_find_stream_info(pFormatContext, NULL) < 0){printf("Can't find stream info of %s", fileName);return -1;}/* 寻找视频流 */for (i = 0; i < pFormatContext->nb_streams; i++){/* 判断是否为视频流 */if (pFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){videoIndex = i;break;}}/* 判断是否找到视频流 */if (videoIndex == -1){printf("Can't find video stream !");return -1;}pCodecContext = pFormatContext->streams[i]->codec;      //获取编解码上下文pCodec = avcodec_find_decoder(pCodecContext->codec_id); //寻找解码器,未找到时返回NULL/* 判断pCodec是否为NULL */if (pCodec == NULL){printf("Can't find decoder !");return -1;}/* 打开解码器,avcodec_open2返回 0 表示成功 */if (avcodec_open2(pCodecContext, pCodec, NULL) != 0){printf("Can't open decoder !");return -1;}/* 分配空间并初始化 */pkt = av_packet_alloc();    av_new_packet(pkt, pCodecContext->width * pCodecContext->height);frame = av_frame_alloc();/* 将ts文件改写为h264文件 */FILE* fp_h264 = fopen("test.h264", "wb");/* 将ts文件解码得到yuv文件 */FILE* fp_yuv = fopen("test.yuv", "wb");/* 循环读帧解码,av_read_frame返回0表示读取成功 */while (av_read_frame(pFormatContext, pkt) == 0){/* 判断是否为视频流(除了视频流可能还有音频流,字幕流) */if (pkt->stream_index == videoIndex){/* 写入h264文件 */fwrite(pkt->data, 1, pkt->size, fp_h264);/* avcodec_decode_video2返回值 < 0 表示解码失败 */ret = avcodec_decode_video2(pCodecContext, frame, &gotPicture, pkt);/* 判断是否解码失败 */if (ret < 0){printf("Can't decode video !");return -1;}/* 写入yuv文件,frame->data[0]为Y分量 frame->data[1]为U分量 frame->data[2]为V分量*/fwrite(frame->data[0], 1, pCodecContext->width * pCodecContext->height, fp_yuv);fwrite(frame->data[1], 1, pCodecContext->width * pCodecContext->height / 4, fp_yuv);fwrite(frame->data[2], 1, pCodecContext->width * pCodecContext->height / 4, fp_yuv);}av_free_packet(pkt);}/* 关闭释放相关资源 */fclose(fp_h264);fclose(fp_yuv);avcodec_close(pCodecContext);avformat_close_input(&pFormatContext);return 0;
}
  • 使用ffplay命令播放yuv文件: ffplay -pixel_format yuv420p -video_size 1920x1080 your_yuv_file.yuv
  • 或者使用yuv播放器

sdl__328">sdl 基础知识

sdl__329">sdl 子系统

◼ SDL_INIT_TIMER:定时器
◼ SDL_INIT_AUDIO:音频
◼ SDL_INIT_VIDEO:视频
◼ SDL_INIT_JOYSTICK:摇杆
◼ SDL_INIT_HAPTIC:触摸屏
◼ SDL_INIT_GAMECONTROLLER:游戏控制器
◼ SDL_INIT_EVENTS:事件
◼ SDL_INIT_EVERYTHING:包含上述所有选项

sdl__339">sdl 视频显示相关函数

◼ SDL_Init():初始化SDL系统
◼ SDL_CreateWindow():创建窗口SDL_Window
◼ SDL_CreateRenderer():创建渲染器SDL_Renderer
◼ SDL_CreateTexture():创建纹理SDL_Texture
◼ SDL_UpdateTexture():设置纹理的数据
◼ SDL_RenderCopy():将纹理的数据拷贝给渲染器
◼ SDL_RenderPresent():显示
◼ SDL_Delay():工具函数,用于延时
◼ SDL_Quit():退出SDL系统

SDL数据结构简介

◼ SDL_Window 代表了一个“窗口”
◼ SDL_Renderer 代表了一个“渲染器”
◼ SDL_Texture 代表了一个“纹理”
◼ SDL_Rect 一个简单的矩形结构

SDL事件

◼ 函数

  • SDL_WaitEvent():等待一个事件
  • SDL_PushEvent():发送一个事件
  • SDL_PumpEvents():将硬件设备产生的事件放入事件队列,用于
    读取事件,在调用该函数之前,必须调用SDL_PumpEvents搜集
    键盘等事件
  • SDL_PeepEvents():从事件队列提取一个事件

◼ 数据结构

  • SDL_Event:代表一个事件
SDL线程

◼ SDL线程创建:SDL_CreateThread
◼ SDL线程等待:SDL_WaitThead
◼ SDL互斥锁:SDL_CreateMutex/SDL_DestroyMutex
◼ SDL锁定互斥:SDL_LockMutex/SDL_UnlockMutex
◼ SDL条件变量(信号量):SDL_CreateCond/SDL_DestoryCond
◼ SDL条件变量(信号量)等待/通知:SDL_CondWait/SDL_CondSingal

sdl_yuv__376">sdl yuv 数据显示流程

这里借用雷神的sdl流程图
在这里插入图片描述

我们的代码就是围绕这个流程图编写的。

sdl_382">下载sdl

  • 下载地址: http://www.libsdl.org/

sdl__yuv__384">sdl 显示 yuv 数据

环境配置
  1. 创建空项目

  2. 新建一个main.cpp文件

  3. 拷贝sdl到项目路径下
    在这里插入图片描述

  4. 将./SDL2-2.0.10/lib/x64/SDL2.dll拷贝到项目路径下(即源代码所在目录)

  5. 选中项目名,右键选择属性,依次进行如下配置

在这里插入图片描述
在这里插入图片描述

测试代码

#include <stdio.h>// 引入SDL头文件
extern "C"
{
#include <SDL.h>
}#undef mainint main() {// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO) < 0) {printf("SDL初始化失败: %s\n", SDL_GetError());return 1;}// 创建窗口SDL_Window* sdlWindow = SDL_CreateWindow("SDL_Test", 100, 100, 800, 600, SDL_WINDOW_SHOWN);if (sdlWindow == nullptr) {printf("窗口创建失败: %s\n", SDL_GetError());return 1;}// 主循环bool quit = false;SDL_Event event;while (!quit) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {quit = true;}}}// 销毁窗口SDL_DestroyWindow(sdlWindow);// 退出SDLSDL_Quit();return 0;
}
sdl__yuv___444">sdl 显示 yuv 数据 代码
#pragma warning(disable:4996)#include <stdio.h>extern "C"	//cpp文件引用sdl头文件
{
#include "SDL.h"
};const int bpp = 12;	//Y: 8 + U: 2 + V: 2int screen_w = 800, screen_h = 600;	//屏幕的宽和高(可以自由设置)
const int pixel_w = 1920, pixel_h = 1080;	//画面展示的宽和高(根据视频窗口大小设定)unsigned char buffer[pixel_w * pixel_h * bpp / 8];	//一帧画面的缓冲//Refresh Event
#define REFRESH_EVENT  (SDL_USEREVENT + 1)//Break Event
#define BREAK_EVENT  (SDL_USEREVENT + 2)int thread_exit = 0;	//状态控制变量int refresh_video(void* opaque) 
{thread_exit = 0;/* 循环读帧事件 */while (!thread_exit) {SDL_Event event;event.type = REFRESH_EVENT;SDL_PushEvent(&event);	//SDL_PushEvent函数用于将事件推送到事件队列中SDL_Delay(40);	//延时,不要读的太快了}thread_exit = 0;//BreakSDL_Event event;event.type = BREAK_EVENT;SDL_PushEvent(&event);return 0;
}int main(int argc, char* argv[])
{/* 初始化 */if (SDL_Init(SDL_INIT_VIDEO)) {printf("Could not initialize SDL - %s\n", SDL_GetError());return -1;}SDL_Window* screen;/* * SDL_CreateWindow* SDL_WINDOWPOS_UNDEFINED是SDL库中定义的一个常量,用于指定窗口的位置。* 它表示将窗口的位置设置为未定义,即由操作系统决定窗口的位置。* SDL_WINDOW_RESIZABLE: 表示窗口大小可变* SDL_WINDOW_OPENGL: 表示支持opengl* @Parma title: 窗口的标题* @Parma x: 运行窗口距电脑桌面左侧的距离* @Parma y: 运行创建距电脑桌面上方的距离* @Parma w: 窗口的宽度* @Parma h: 窗口的高度* @Parma flags: 一些支持设置* @Return: 创建成功返回窗口,失败返回NULL*/screen = SDL_CreateWindow("My YUV Player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,screen_w, screen_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);/* 判断是否成功创建窗口 */if (screen == NULL) {printf("SDL: could not create window - exiting:%s\n", SDL_GetError());return -1;}SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0);/* 判断是否成功创建渲染器 */if (sdlRenderer == NULL){printf("SDL: could not create renderer - exiting:%s\n", SDL_GetError());return -1;}Uint32 pixformat = 0;/*IYUV: Y + U + V(3 planes)* YV12: Y + V + U  (3 planes)* SDL_PIXELFORMAT_IYUV: SDL中用于表示IYUV格式的像素格式常量。IYUV是一种YUV格式,其中Y表示亮度分量,U和V表示色度分量。* 在IYUV格式中,亮度分量Y是按照完整的图像大小进行存储的,而色度分量U和V则是按照图像大小的四分之一进行存储的。*/pixformat = SDL_PIXELFORMAT_IYUV;/** SDL_CreateTexture	创建纹理* SDL_TEXTUREACCESS_STREAMING是SDL2中的一个纹理访问标志,用于指定纹理的访问方式。* 具体来说,SDL_TEXTUREACCESS_STREAMING表示纹理可以通过内存访问进行更新,即可以直接访问纹理的像素数据进行修改。*/SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer, pixformat, SDL_TEXTUREACCESS_STREAMING, pixel_w, pixel_h);/* 判断是否创建成功 */if (sdlTexture == NULL){printf("SDL: no rending context is active");return -1;}/* 打开yuv文件,文件路径自行设置 */FILE* fp = fopen("test.yuv", "rb+");/* 判断是否打开成功 */if (fp == NULL) {printf("can't open this file\n");return -1;}/** SDL_Rect: SDL库中定义的一个矩形结构体,用于表示矩形的位置和大小。* 它包含了四个整型成员变量x、y、w和h,分别表示矩形的左上角顶点的x坐标、y坐标,以及矩形的宽度和高度。*/SDL_Rect sdlRect;/** SDL_CreateThread: SDL库中用于创建线程的函数* 该函数接受五个参数:* fn:线程函数指针,指向要在新线程中执行的函数。* name:线程的名称,用于调试目的。* data:传递给线程函数的数据指针。* pfnBeginThread:指向线程启动函数的指针。* pfnEndThread:指向线程结束函数的指针*/SDL_Thread* refresh_thread = SDL_CreateThread(refresh_video, NULL, NULL);/** SDL_Event: SDL中所有事件处理的核心,它是一个联合体,包含了SDL中使用的所有事件结构的并集。* SDL的所有事件都存储在一个队列中,而SDL_Event的常规操作就是从这个队列中读取事件或者写入事件。*/SDL_Event event;while (1) {/* 等待事件 */SDL_WaitEvent(&event);/* 判断事件类型 */if (event.type == REFRESH_EVENT) {/* 读取一帧yuv数据到buffer中 */while (fread(buffer, 1, pixel_w * pixel_h * bpp / 8, fp) != pixel_w * pixel_h * bpp / 8) {// Loopfseek(fp, 0, SEEK_SET);fread(buffer, 1, pixel_w * pixel_h * bpp / 8, fp);}/* SDL_UpdateTexture: SDL库中用于更新纹理数据的函数 */SDL_UpdateTexture(sdlTexture, NULL, buffer, pixel_w);//FIX: If window is resizesdlRect.x = 0;sdlRect.y = 0;sdlRect.w = screen_w;sdlRect.h = screen_h;/* SDL_RenderClear函数用于清空渲染器的颜缓冲区,将其填充为指定的颜色 */SDL_RenderClear(sdlRenderer);/* * SDL_RenderCopy: SDL库中用于将纹理数据复制给渲染目标的函数* 该函数接受四个参数:* renderer:渲染器,用于指定渲染目标。* texture:纹理,包含要复制的图像数据。* srcrect:源矩形,指定要复制的纹理区域。* dstrect:目标矩形,指定要将纹理复制到的位置和大小。*/SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);/* SDL_RenderPresent: SDL库中用于显示画面的函数 */SDL_RenderPresent(sdlRenderer);}/* SDL_WINDOWEVENT: SDL中的一个事件类型,用于处理窗口相关的事件 */else if (event.type == SDL_WINDOWEVENT) {//If ResizeSDL_GetWindowSize(screen, &screen_w, &screen_h);}/* 退出事件 */else if (event.type == SDL_QUIT) {thread_exit = 1;	//退出子线程中的循环}/* 当窗口关闭时,退出循环 */else if (event.type == BREAK_EVENT) {break;}}/* SDL_Quit是SDL库中的一个函数,用于退出SDL子系统并释放相关资源。* 调用SDL_Quit函数后,SDL库将关闭所有已打开的子系统,并释放分配的内存。 */SDL_Quit();return 0;
}
定要将纹理复制到的位置和大小。*/SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);/* SDL_RenderPresent: SDL库中用于显示画面的函数 */SDL_RenderPresent(sdlRenderer);}/* SDL_WINDOWEVENT: SDL中的一个事件类型,用于处理窗口相关的事件 */else if (event.type == SDL_WINDOWEVENT) {//If ResizeSDL_GetWindowSize(screen, &screen_w, &screen_h);}/* 退出事件 */else if (event.type == SDL_QUIT) {thread_exit = 1;	//退出子线程中的循环}/* 当窗口关闭时,退出循环 */else if (event.type == BREAK_EVENT) {break;}}/* SDL_Quit是SDL库中的一个函数,用于退出SDL子系统并释放相关资源。* 调用SDL_Quit函数后,SDL库将关闭所有已打开的子系统,并释放分配的内存。 */SDL_Quit();return 0;
}

http://www.ppmy.cn/ops/23864.html

相关文章

企业架构学习 Togaf 2、概述、简介

文章目录 1 企业架构 TOGAF 概述2 企业架构目的1 解决 IT 复杂性2、对业务,战略落地,制程业务架构的架构 就是架构框架3 Togif 可以帮助项目经理快速的找到落地需要的人才4 简介 Togif -- 自我理解企业架构 ADM像 PMP 项目管理,每个阶段都有输入输出,输出我们的交付物。4 官…

Kubernetes - CentOS7搭建k8s_v1.18集群高可用(kubeadm/二进制包部署方式)实测配置验证手册

Kubernetes - CentOS7搭建k8s集群高可用&#xff08;kubeadm/二进制包部署方式&#xff09;实测配置验证手册 前言概述&#xff1a; 一、Kubernetes—k8s是什么 Kubernetes 这个名字源于希腊语&#xff0c;意为“舵手“或”飞行员"。 Kubernetes&#xff0c;简称K8s&#…

react useEffect中removeEventListener没生效问题解决

在useEffect中写入window.removeEventListener没有生效&#xff0c;代码如下 useEffect(() > {const handleResize () > {console.log(window.innerWidth, window.innerHeight);};window.addEventListener(resize, handleResize);return () > {window.removeEventLi…

XY_RE复现(二)

一&#xff0c;何须相思煮余年 0x55 0x8b 0xec 0x81 0xec 0xa8 0x0 0x0 0x0 0xa1 0x0 0x40 0x41 0x0 0x33 0xc5 0x89 0x45 0xfc 0x68 0x9c 0x0 0x0 0x0 0x6a 0x0 0x8d 0x85 0x60 0xff 0xff 0xff 0x50 0xe8 0x7a 0xc 0x0 0x0 0x83 0xc4…

【智能算法】白鹭群优化算法(ESOA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2022年&#xff0c;Z Chen等人受到白鹭捕食行为和自然行为启发&#xff0c;提出了白鹭群优化算法&#xff08;Egret Swarm Optimization Algorithm, ESOA&#xff09;。 2.算法原理 2.1算法思想 E…

【Spring AI】03. 图像生成 API-OpenAI

文章目录 OpenAI 图像生成先决条件自动装配图像生成属性参数连接属性参数配置属性参数重试属性参数 运行时选项参数 OpenAI 图像生成 Spring AI 支持来自 OpenAI 的图像生成模型 DALL-E。 先决条件 您需要创建一个 OpenAI 的 API 密钥来访问 ChatGPT 模型。在 OpenAI 注册页面…

swift语言学习总结

Var 表示变量&#xff0c; let表示常量。数组和map&#xff0c; 都用中括号[].可以直接赋值。可以用下标或键访问。 var shoppingList ["catfish", "water", "tulips", "blue paint”]//最后一个可以加逗号。 shoppingList[1] "bo…

Grafana 系列|Grafana 监控 TDengine集群

Grafana 监控 TDengine集群有两种方式&#xff1a; 一、 taosKeeper监控 TDengine 通过 taosKeeper 将服务器的 CPU、内存、硬盘空间、带宽、请求数、磁盘读写速度等信息定时写入指定数据库。TDengine 还将重要的系统操作&#xff08;比如登录、创建、删除数据库等&#xff0…