说明
这里的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 基础知识
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数据结构简介
AVFormatContext: 封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。
 AVInputFormat demuxer每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。
 AVOutputFormat muxer
 AVStream 视频文件中每个视频(音频)流对应一个该结构体。
 AVCodecContext 编解码器上下文结构体,保存了视频(音频)编解码相关信息。
 AVCodec 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
 AVPacket 存储一帧压缩编码数据。
 AVFrame 存储一帧解码后像素(采样)数据。
AVPacket和AVFrame的关系

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
- 官网: https://ffmpeg.org/
ffmpeg 解码 ts 视频文件得到 yuv 视频文件 程序
环境配置
-  创建空项目 
  
-  填写项目名(大家随意) 
-  新建一个main.cpp文件 
-  拷贝ffmpeg到项目路径下 
  
-  将 ffmpeg-4.2/bin 下的 dll 文件拷贝到项目路径下(即源代码所在目录) 
  
-  选中项目名,右键选择属性,依次进行如下配置 
  
  
  
测试代码
#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
#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 基础知识
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 视频显示相关函数
◼ 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 数据显示流程
这里借用雷神的sdl流程图
 
我们的代码就是围绕这个流程图编写的。
下载sdl
- 下载地址: http://www.libsdl.org/
sdl 显示 yuv 数据
环境配置
-  创建空项目 
-  新建一个main.cpp文件 
-  拷贝sdl到项目路径下 
  
-  将./SDL2-2.0.10/lib/x64/SDL2.dll拷贝到项目路径下(即源代码所在目录) 
-  选中项目名,右键选择属性,依次进行如下配置 

 
测试代码
#include <stdio.h>
// 引入SDL头文件
extern "C"
{
#include <SDL.h>
}
#undef main
int main() {
    // 初始化SDL
    if (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);
    // 退出SDL
    SDL_Quit();
    return 0;
}
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: 2
int 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;
	//Break
	SDL_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) {
				// Loop
				fseek(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 resize
			sdlRect.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 Resize
			SDL_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 Resize
			SDL_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;
}
















![[机器学习系列]深入解析K-Means聚类算法:理论、实践与优化](https://img-blog.csdnimg.cn/direct/aea916edec0b4750a552cceef9a766e2.png)


