ffmpeg与sdl的个人笔记

发布于:2024-04-29 ⋅ 阅读:(24) ⋅ 点赞:(0)

说明

这里的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 视频文件 程序

环境配置
  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
#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 数据

环境配置
  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 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;
}