ffmpeg与sdl的个人笔记

news2024/11/23 7:09:08

说明

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1633489.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学数据结构之前是否该把c语言吃透?

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「数据结构的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 我见过的第一个版本的数据…

vue+elementui(笔记)

vueelementui 表格 <div class"tableStyle"><el-table :data"pointsSettingsTableData" style"width: 70%" :stripe"true" size"mini"header-cell-class-name"headerClassName" :cell-style"{ tex…

合规基线:让安全大检查更顺利

前言 说起安全检查&#xff0c;安全从业人员可能都非常熟悉“安全标准”概念。所有企事业单位网络安全建设都需要满足来自于国家或监管单位的安全标准&#xff0c;如等保2.0、CIS安全标准等。安全标准&#xff0c;还有一个叫法就是“安全基线”。字典上对“基线”的解释是&…

AI视频教程下载:用ChatGPT做SEO的终极教程

ChatGPT是由OpenAI开发的一款尖端人工智能&#xff0c;它已经彻底改变了我们进行搜索引擎优化&#xff08;SEO&#xff09;的方式。其先进的语言处理能力使其成为增强网站内容、提高搜索引擎排名和显著提升在线可见性的宝贵工具。 这个全面的课程旨在为你提供使用ChatGPT进行SE…

算法必备数学基础:图论方法由浅入深实践与应用

作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任大厂数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python 欢迎加入社区&#xff1a;码上找工作 作者专栏每日更新&#xff1a; LeetCode解锁1000题: 打怪升级之旅 python数据分析…

Phi-3-mini-4k-instruct 的功能测试

Model card 介绍 Phi-3-Mini-4K-Instruct 是一个 3.8B 参数、轻量级、最先进的开放模型&#xff0c;使用 Phi-3 数据集进行训练&#xff0c;其中包括合成数据和经过过滤的公开可用网站数据&#xff0c;重点是 高品质和推理密集的属性。 该型号属于 Phi-3 系列&#xff0c;Mini…

运营版游戏陪玩平台源码/tt语音聊天/声优服务/陪玩系统源码开黑/约玩源码

简述 一个人的游戏叫孤独&#xff0c;一群人的游戏才是乐趣&#xff0c;随着电竞产业在国内的快速发展&#xff0c;游戏陪练行业也迅速成长&#xff0c;现在很多游戏玩家为了追求更高质量的游戏体验感&#xff0c;往往会在玩游戏的过程中找陪练&#xff0c;通过陪玩系统进行预…

MySQL随便聊----之MySQL的调控按钮-启动选项和系统变量

-------MySQL是怎么运行的 基本介绍 如果你用过手机&#xff0c;你的手机上一定有一个设置的功能&#xff0c;你可以选择设置手机的来电铃声、设置音量大小、设置解锁密码等等。假如没有这些设置功能&#xff0c;我们的生活将置于尴尬的境地&#xff0c;比如在图书馆里无法把手…

react项目发布后,浏览器源码泄露的解决方案

在使用create-react-app时&#xff0c;打包生产环境npm run build&#xff0c;浏览器打开后仍然是可以看到源码的。源码都没上传&#xff0c;为啥线上能看到源码 。 例&#xff1a;线上与服务器 线上与源码 react-scripts build和npm run build 有什么不同 react-scripts bui…

spring高级篇(五)

1、参数解析器 前篇提到过&#xff0c;参数解析器是HandlerAdapters中的组件&#xff0c;用于解析controller层方法中加了注解的参数信息。 有一个controller&#xff0c;方法的参数加上了各种注解&#xff1a; public class Controller {public void test(RequestParam("…

Redux Toolkit 中持久化路由配置数组的实践指南

要将路由配置数组保存到 Redux Toolkit 中并持久化,你可以按照以下步骤进行操作: 创建一个 Slice 来管理路由配置 在 Redux Toolkit 中,我们使用 createSlice 来创建一个 slice,用于管理路由配置的状态。 import { createSlice } from reduxjs/toolkit;const routesSlice c…

【重磅】史上最全企业数字化转型项目流程管理资料大合集下载(2.5G,1429份)

重磅分享&#xff0c;史上最全企业数字化转型项目流程管理资料大合集&#xff0c;共1429份。 一、项目管理实战工具大全&#xff08;甘特图、表格、项目阶段文档等527份&#xff09; 二、项目管理流程规范制度&#xff08;各类流程制度共86份&#xff09; 三、项目管理模板&…

编译Qt6.5.3LTS版本(Mac/Windows)的mysql驱动(附带编译后的全部文件)

文章目录 0 背景1 编译过程2 福利参考 0 背景 因为项目要用到对MYSQL数据库操作&#xff0c;所以需要连接到MYSQL数据库。但是连接需要MYSQL驱动&#xff0c;但是Qt本身不自带MYSQL驱动&#xff0c;需要自行编译。网上有很多qt之前版本的mysql驱动&#xff0c;但是没有找到qt6…

Python数据结构与算法(1):将序列分解为单独的变量

问题 现在有一个包含 N 个元素的元组或者是序列&#xff0c;怎样将它里面的值解压后同时赋值给 N 个变量&#xff1f; 解决方案 任何的序列&#xff08;或者是可迭代对象&#xff09;可以通过一个简单的赋值操作来分解为单独的变量。 唯一的要求就是变量的总数和结构必须与序…

【数据结构7-1-查找-线性-二分法-二叉树-哈希表】

目录 1 查找基本概念2 线性表的查找2.1 顺序查找2.2 二分法查找2.3 分块查找 3 树表的查询3.1 二叉排序树3.1.1 定义3.1.2 二叉树的建立、遍历、查找、增加、删除&#xff1a;3.1.3 代码实现&#xff1a; 3.2 平衡二叉树3.2.1 平横因子3.2.2 不平横树的调整-左旋3.2.3 不平横树…

Unity 数字字符串逗号千分位

使用InputField时处理输入的数字型字符串千分位自动添加逗号&#xff0c;且自动保留两位有效数字 输入&#xff1a;123 输出&#xff1a;123.00 输入&#xff1a;12345 输出&#xff1a;12,345.00 代码非常简单 using UnityEngine; using TMPro;public class …

[机器学习系列]深入解析K-Means聚类算法:理论、实践与优化

目录 一、KMeans (一)Kmeans简介 (二)Kmeans作用和优点 (三)Kmeans局限和缺点 (四)Kmeans步骤 (五)如何选取最佳的K值的三种方法 (六)手肘法和目标函数的变化两种确定K值方法的区别 (七)如何选取第一次迭代的K个类中心------KMeans方法 (八)KMeans的常用参数介绍 二、…

【C语言刷题系列】对数字添加逗号

目录 一、问题描述 二、解题思路 三、源代码 拓展&#xff1a; 个人主页&#xff1a; 倔强的石头的博客 系列专栏 &#xff1a;C语言指南 C语言刷题系列 一、问题描述 二、解题思路 题目的要求&#xff0c;即对于一个较大的整数&#xff0c;每三位数字之间添加…

【go项目01_学习记录day01】

博客系统 1 vscode开发go项目插件推荐1.1 CtrlShiftP&#xff08;俗称万能键&#xff09; &#xff1a;打开命令面板。在打开的输入框内&#xff0c;可以输入任何命令。1.2 开发时&#xff0c;我们需要经常查阅 Go 语言官方文档&#xff0c;可惜因国内访问外网不稳定&#xff0…

自己手写了一个大模型RAG项目-05.基于知识库的大模型问答

大家好&#xff0c;我是程序锅。 github上的代码封装程度高&#xff0c;不利于小白学习入门。 常规的大模型RAG框架有langchain等&#xff0c;但是langchain等框架源码理解困难&#xff0c;debug源码上手难度大。 因此&#xff0c;我写了一个人人都能看懂、人人都能修改的大…