说明
这里的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;
}