音视频开发十九:SDL线程和事件

SDL事件相关API

DL_Event 是SDL库中使用的一个数据结构,用于表示系统中发生的事件。一个事件可以是键盘按键、鼠标移动、游戏手柄移动、窗口事件等。SDL_Event 结构包含有关事件类型、事件发生时间和事件参数的详细信息。

SDL_Event 结构具有以下成员:

  • type:表示事件类型的整数。
    • SDL_QUIT:表示退出事件,当用户关闭窗口或按下键盘上的 Alt + F4 或 Cmd + Q 组合键时会触发该事件。
    • SDL_KEYDOWN / SDL_KEYUP:表示键盘按键事件,当用户按下或释放键盘上的按键时会触发该事件。
    • SDL_MOUSEMOTION / SDL_MOUSEBUTTONDOWN / SDL_MOUSEBUTTONUP:表示鼠标事件,当用户移动鼠标或按下、释放鼠标按钮时会触发该事件。
    • SDL_WINDOWEVENT:表示窗口事件,当窗口状态发生变化时会触发该事件,例如窗口大小发生变化,窗口获得或失去焦点等。
    • SDL_JOYAXISMOTION / SDL_JOYBALLMOTION / SDL_JOYHATMOTION / SDL_JOYBUTTONDOWN / SDL_JOYBUTTONUP / SDL_JOYDEVICEADDED / SDL_JOYDEVICEREMOVED:表示游戏手柄事件,当游戏手柄状态发生变化时会触发该事件,例如手柄移动、按下、释放按钮等。
    • SDL_CONTROLLERAXISMOTION / SDL_CONTROLLERBUTTONDOWN / SDL_CONTROLLERBUTTONUP / SDL_CONTROLLERDEVICEADDED / SDL_CONTROLLERDEVICEREMOVED:表示控制器事件,当控制器状态发生变化时会触发该事件,例如控制器移动、按下、释放按钮等。
    • SDL_USEREVENT:表示用户自定义事件,当应用程序调用 SDL_PushEvent 函数将自定义事件添加到事件队列时,会触发该事件。
  • timestamp:表示事件发生时间的 Uint32 类型数值。
  • window:表示事件所属的 SDL_Window 数据指针。
  • key:表示键盘事件的 SDL_KeyboardEvent 数据结构。
  • motion:表示鼠标移动事件的 SDL_MouseMotionEvent 数据结构。
  • button:表示鼠标按钮事件的 SDL_MouseButtonEvent 数据结构。
  • wheel:表示鼠标滚轮事件的 SDL_MouseWheelEvent 数据结构。
  • jaxis:表示游戏手柄轴移动事件的 SDL_JoyAxisEvent 数据结构。
  • jball:表示游戏手柄球移动事件的 SDL_JoyBallEvent 数据结构。
  • jbutton:表示游戏手柄按钮事件的 SDL_JoyButtonEvent 数据结构。
  • jdevice:表示游戏手柄设备事件的 SDL_JoyDeviceEvent 数据结构。
  • caxis:表示游戏手柄轴移动事件的 SDL_ControllerAxisEvent 数据结构。
  • cbutton:表示游戏手柄按钮事件的 SDL_ControllerButtonEvent 数据结构。
  • cdevice:表示游戏手柄设备事件的 SDL_ControllerDeviceEvent 数据结构。
  • quit:表示退出事件的 SDL_QuitEvent 数据结构。
  • user:表示用户自定义事件的 SDL_UserEvent 数据结构。

通过处理 SDL_Event 结构数据,应用程序可以处理用户输入和系统事件,以产生交互行为。

SDL_PollEvent()

SDL_PollEvent()是SDL事件处理库中的一个函数,用于从事件队列中轮询(一直循环)事件并返回。它可以用于捕获用户输入,控制游戏状态等。

在调用SDL_PollEvent()函数时,它会检查SDL事件队列中是否有事件,并将此事件从队列顶部弹出并返回。如果事件队列为空,则该函数将立即返回0。如果有事件,则该函数将返回1,并将事件结构体填充到指定的SDL_Event结构体中。根据事件类型的不同,SDL_Event结构体的不同字段将被填充。

以下是一个简单的SDL_PollEvent()函数的示例,它只处理鼠标事件:

SDL_Event event;

while (SDL_PollEvent(&event)) {
    
    
    switch (event.type) {
    
    
        case SDL_MOUSEMOTION:
            // 处理鼠标移动事件
            break;
        case SDL_MOUSEBUTTONDOWN:
            // 处理鼠标按下事件
            break;
        case SDL_MOUSEBUTTONUP:
            // 处理鼠标释放事件
            break;
        default:
            break;
    }
}

由于SDL_PollEvent()函数是轮询事件队列的,因此它不会占用大量资源,可在循环中反复使用。但要注意,如果在事件处理期间执行某些任务会导致轮询事件的延迟。

SDL_WaitEvent

SDL_WaitEvent()是SDL事件处理库中的一个函数,用于等待并获取下一个事件。它可用于阻塞程序执行,直到有事件发生。

在调用SDL_WaitEvent()函数时,它会检查事件队列中是否有事件。如果事件队列为空,则该函数会阻塞当前线程并等待事件的到来。如果队列中有事件,则该函数将该事件弹出队列并返回一个非零值。与SDL_PollEvent()不同,不需要指定要轮询的事件类型。

以下是一个简单的SDL_WaitEvent()函数的示例,它等待并处理一个QUIT事件。QUIT事件表示用户关闭窗口或程序正在退出:

SDL_Event event;

while (SDL_WaitEvent(&event)) {
    if (event.type == SDL_QUIT) {
        // 处理退出事件
        break;
    }
}

需要注意的是,SDL_WaitEvent()函数会阻塞当前线程,因此如果等待事件的时间过长,将会导致程序停滞,并不利于程序的执行。建议在需要响应用户输入的情况下使用SDL_WaitEvent(),在不需要响应事件的情况下建议使用SDL_PollEvent()。

SDL 线程相关API

示例代码:

	#include <SDL.h>
	#include <stdio.h>
	
	SDL_mutex *mutex = NULL;
	SDL_cond *cond = NULL;
	
	int thread_work(void *arg) {
    
    
	
	  printf("Thread-2 Lock Mutex Start\n");
	  SDL_LockMutex(mutex);
	  printf("Thread-2 Lock Mutex Success\n");
	
	  printf("Thread-2 Working with Mutex\n");
	  sleep(4);
	
	  printf("Thread-2 Wait Condition Start\n");
	  SDL_CondWait(cond, mutex);
	  printf("Thread-2 Wait Condition Success\n");
	
	  printf("Thread-2 Unlock Mutex\n");
	  SDL_UnlockMutex(mutex);
	
	  printf("Thread-2 Finish\n");
	
	  return 0;
	}
	
	#undef main
	int main() {
    
    
	
	  mutex = SDL_CreateMutex();
	  cond = SDL_CreateCond();
	
	  SDL_Thread *t = SDL_CreateThread(thread_work, "Thread-2", NULL);
	
	  printf("Thread-1 Working\n");
	  sleep(2);
	
	  printf("Thread-1 Lock Mutex Start\n");
	  SDL_LockMutex(mutex);
	  printf("Thread-1 Lock Mutex Success\n");
	
	  printf("Thread-1 Working with Mutex\n");
	  sleep(2);
	
	  printf("Thread-1 Send Condition Signal\n");
	  SDL_CondSignal(cond);
	
	  printf("Thread-1 Working with Mutex\n");
	  sleep(2);
	
	  printf("Thread-1 Unlock Mutex\n");
	  SDL_UnlockMutex(mutex);
	
	  printf("Thread-1 Wait Thread-2 Finish Start\n");
	  SDL_WaitThread(t, NULL);
	  printf("Thread-1 Wait Thread-2 Finish Success\n");
	
	  printf("Thread-1 Destroy Mutex Start\n");
	  printf("Thread-1 Destroy Condition Start\n");
	  SDL_DestroyMutex(mutex);
	  SDL_DestroyCond(cond);
	  printf("Thread-1 Destroy Mutex Success\n");
	  printf("Thread-1 Destroy Condition Success\n");
	
	  return 0;
	}


SDL_CreateThread

SDL_CreateThread函数用于创建一个新线程。其原型如下:

SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char* name, void* data);

其中,fn是一个线程执行的函数,函数指针的定义是:

typedef int (*SDL_ThreadFunction)(void* data);

它接收一个void指针,指向当前线程的参数数据;name是线程名称;data是传递给线程函数的数据参数。

调用SDL_CreateThread函数后,将会创建一个新的线程,并在其中执行参数fn所指定的线程执行函数。创建完成后,函数会返回一个指向SDL_Thread结构体的指针,这个结构体包含了线程的ID和名称等信息。

示例代码:

// 线程执行函数,接收一个void指针作为参数
int threadFunc(void *data)
{
    
    
    // 要执行的线程任务
    return 0;
}

// 在主线程中创建一个新线程
SDL_Thread* thread = SDL_CreateThread(threadFunc, "MyThread", NULL);

在这个示例中,我们通过SDL_CreateThread函数在主线程中创建了一个新线程,用于执行线程执行函数threadFunc。在本例中,函数没有传递任何参数,所以最后的NULL表示不传递数据参数。

SDL_WaitThread

SDL_WaitThread是SDL库提供的一个线程等待函数,它的作用是等待指定的线程执行完毕。函数原型如下:

int SDL_WaitThread(SDL_Thread* thread, int* status);

该函数的第一个参数是SDL_Thread类型的指针,表示要等待的线程。第二个参数是一个整型指针,用于存储线程的退出状态。函数的返回值为0表示线程已正常退出,其他值表示错误。

SDL_WaitThread函数会挂起当前线程,等待指定的线程执行完毕,并接收指定线程的返回状态。如果线程已经退出,函数会立刻返回,并将线程的退出状态存入status指针所指的内存中。

SDL_CreateMutex

SDL_CreateMutex() 是SDL库中用于创建互斥锁(mutex)的函数。互斥锁是一种用于保护共享资源的同步原语,它可以确保同一时间内只有一个线程访问共享资源,以避免数据竞争和并发问题。

SDL_CreateMutex() 的函数原型如下:

SDL_mutex* SDL_CreateMutex(void)

该函数不需要任何参数,它返回一个指向新创建互斥锁的指针(SDL_mutex*类型),如果创建失败则返回 NULL。

SDL_LockMutex

SDL_LockMutex() 是SDL库中用于锁定互斥锁(mutex)的函数

SDL_LockMutex() 的函数原型如下:

int SDL_LockMutex(SDL_mutex* mutex)

该函数的参数为互斥锁指针(SDL_mutex*类型),函数返回一个整型值,表示函数调用的结果。若函数成功锁定互斥锁,则返回0;否则返回错误代码。

当一个线程执行 SDL_LockMutex() 函数时,如果互斥锁当前没有被其他线程锁定,则该线程会立即获得互斥锁并可以访问受保护的共享资源。如果互斥锁当前已经被其他线程锁定,那么该线程会一直阻塞等待直到互斥锁被释放并可以被该线程获得为止。

需要注意的是,在使用互斥锁时,要遵循“谁申请谁释放”的原则,即使用 SDL_LockMutex() 函数锁定互斥锁的线程必须在访问完共享资源后使用 SDL_UnlockMutex() 函数释放互斥锁,否则其他线程将无法获得该锁访问共享资源。

下面是一个简单的示例代码,展示了如何使用 SDL_LockMutex() 函数锁定互斥锁,以避免多线程下的共享资源的并发问题:

#include <SDL.h>

// 定义一个全局变量作为共享的资源
int shared_data = 0;

SDL_mutex* mutex;

void some_function(void) {
    
    
    // 等待并锁定互斥锁
    SDL_LockMutex(mutex);

    // 访问受保护的共享资源
    shared_data += 1;

    // 解锁互斥锁
    SDL_UnlockMutex(mutex);
}

int main() {
    
    
    // 初始化SDL库
    SDL_Init(SDL_INIT_EVERYTHING);

    // 创建互斥锁
    mutex = SDL_CreateMutex();

    // 启动多个线程访问共享资源
    // ...

    // 最后释放互斥锁
    SDL_DestroyMutex(mutex);

    // 退出SDL库
    SDL_Quit();
    return 0;
}

SDL_UnlockMutex

SDL_LockMutex() 是SDL库中用于释放互斥锁(mutex)的函数

SDL_UnlockMutex 函数的函数原型为:

int SDL_UnlockMutex(SDL_mutex* mutex);

SDL_DestroyMutex

SDL_LockMutex() 是SDL库中用于销毁互斥锁(mutex)的函数

SDL_DestroyMutex 函数的函数原型为:

void SDL_DestroyMutex(SDL_mutex* mutex);

SDL_CreateCond

SDL_CreateCond() 是SDL库中用于创建条件变量(condition variable)的函数。条件变量是一种线程间同步原语,它允许一个线程等待另一个线程满足某个条件后再继续执行,从而更好地控制线程的执行顺序和并发性。

SDL_CreateCond() 的函数原型如下:

SDL_cond* SDL_CreateCond(void)

该函数没有参数,返回一个指向新创建条件变量的指针(SDL_cond*类型),如果创建失败则返回 NULL。

SDL_CondSignal

SDL_CondSignal() 是SDL库中用于发送条件变量(condition variable)信号的函数。

SDL_CondSignal() 的函数原型如下:

int SDL_CondSignal(SDL_cond* cond)

该函数的参数为条件变量指针(SDL_cond*类型),函数返回一个整型值,表示函数调用的结果。若函数成功发送条件变量信号,则返回0;否则返回错误代码。

SDL_CondWait

SDL_CondWait() 是SDL库中用于等待条件变量(condition variable)信号的函数

DL_CondWait() 函数允许线程在等待条件变量时解锁互斥锁,从而让其他线程可以继续访问共享资源

SDL_CondWait() 的函数原型如下:

int SDL_CondWait(SDL_cond* cond, SDL_mutex* mutex)

该函数的参数为条件变量指针(SDL_cond类型)和互斥锁指针(SDL_mutex类型),函数返回一个整型值,表示函数调用的结果。如果条件变量成功地接收到信号,则返回0;否则返回错误代码。

当一个线程调用 SDL_CondWait() 函数时,它会先解锁互斥锁(把自己的锁解开,所以使用SDL_CondWait函数之前,必须先获取到锁),然后一直等待条件变量的信号(SDL_CondSignal或SDL_CondBroadcast),直到信号被发送给该条件变量。当线程接收到信号后,它会重新获取互斥锁(重新获取就是尝试获取,如果其他线程不释放该锁,即使收到信号也不会向下执行。),以访问共享资源。

SDL_DestroyCond

SDL_DestroyCond() 是SDL库中用于销毁条件变量(condition variable)的函数。

SDL_DestroyCond() 的函数原型如下:

void SDL_DestroyCond(SDL_cond* cond)

该函数的参数为条件变量指针(SDL_cond*类型),没有返回值。在销毁条件变量时,需要先保证该条件变量没有被任何线程在等待,然后将其释放掉。

线程应用

通过线程完成一个YUV播放器,每隔40ms更新窗口显示画面

逻辑流程图

代码

#include <stdio.h>
#include <string.h>

#include <SDL.h>

#define BLOCK_SIZE 4096000

//event message
#define REFRESH_EVENT  (SDL_USEREVENT + 1)
#define QUIT_EVENT  (SDL_USEREVENT + 2)

int thread_exit = 0;

// 用线程控制每一帧的播放时间,每隔多少时间触发一个事件通知
int refresh_video_timer(void* udata) {
    
    

    thread_exit = 0;

    while (!thread_exit) {
    
    
        SDL_Event event;
        event.type = REFRESH_EVENT; // 更新事件
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }

    thread_exit = 0;

    //push quit event
    SDL_Event event;
    event.type = QUIT_EVENT;
    SDL_PushEvent(&event);

    return 0;
}

int yuv_play()
{
    
    

    FILE* video_fd = NULL;

    SDL_Event event;
    SDL_Rect rect;

    Uint32 pixformat = 0;

    SDL_Window* win = NULL;
    SDL_Renderer* renderer = NULL;
    SDL_Texture* texture = NULL;

    SDL_Thread* timer_thread = NULL;

    int w_width = 640, w_height = 480;
    const int video_width = 608, video_height = 368;

    Uint8* video_pos = NULL;
    Uint8* video_end = NULL;

    unsigned int remain_len = 0;
    unsigned int video_buff_len = 0;
    unsigned int blank_space_len = 0;
    Uint8 video_buf[BLOCK_SIZE];

    const char* path = "f:\\test_data\\out.yuv";

    // 一个yuv图片的长度 
    /*
        这个公式用于计算YUV格式视频每帧数据的长度(字节数)。
        YUV是一种基于颜色空间的视频格式,其中每个像素点不是一个RGB变量,而是由一个Luma(亮度)和两个色度(Chroma)组成的,通常用YUV 4:2:0的格式存储。在YUV 4:2:0格式中,Y分量的采样率是1,而U和V分量的采样率是1/2,即每四个Y像素只有一个U和V像素。因此,每个像素点需要1.5个字节的存储空间。

        根据这个公式,假设视频的宽度是video_width,高度是video_height,每个像素点需要1.5个字节的存储空间,那么一个YUV帧的大小可以表示为:

        yuv_frame_len = video_width * video_height * 1.5

        这个公式中12/8用于将1.5个字节转化为12位,然后再将结果转化为字节。
    *
    */
    const unsigned int yuv_frame_len = video_width * video_height * 12 / 8;

    //1.初始化SDL
    if (SDL_Init(SDL_INIT_VIDEO)) {
    
    
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }

    //creat window from SDL
    win = SDL_CreateWindow("YUV Player",
        SDL_WINDOWPOS_UNDEFINED,
        SDL_WINDOWPOS_UNDEFINED,
        w_width, w_height,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!win) {
    
    
        fprintf(stderr, "Failed to create window, %s\n", SDL_GetError());
        goto __FAIL;
    }

    // 创建渲染器
    renderer = SDL_CreateRenderer(win, -1, 0);

    //IYUV: Y + U + V  (3 planes)
    //YV12: Y + V + U  (3 planes)
    pixformat = SDL_PIXELFORMAT_IYUV;

    
    //根据渲染器创建纹理
    texture = SDL_CreateTexture(renderer,
        pixformat,
        SDL_TEXTUREACCESS_STREAMING,
        video_width,
        video_height);

    //打开yuv文件
    video_fd = fopen(path, "r");
    if (!video_fd) {
    
    
        fprintf(stderr, "Failed to open yuv file\n");
        goto __FAIL;
    }

    //读取yuv数据保存到video_buf中
    if ((video_buff_len = fread(video_buf, 1, BLOCK_SIZE, video_fd)) <= 0) {
    
    
        fprintf(stderr, "Failed to read data from yuv file!\n");
        goto __FAIL;
    }

    //set video positon
    video_pos = video_buf;
    video_end = video_buf + video_buff_len;
    blank_space_len = BLOCK_SIZE - video_buff_len;

    timer_thread = SDL_CreateThread(refresh_video_timer,
        NULL,
        NULL);

    do {
    
    
        //等待事件
        SDL_WaitEvent(&event);
        if (event.type == REFRESH_EVENT) {
    
    
            //not enought data to render
            if ((video_pos + yuv_frame_len) > video_end) {
    
    

                //have remain data, but there isn't space
                remain_len = video_end - video_pos;
                if (remain_len && !blank_space_len) {
    
    
                    //copy data to header of buffer
                    memcpy(video_buf, video_pos, remain_len);

                    blank_space_len = BLOCK_SIZE - remain_len;
                    video_pos = video_buf;
                    video_end = video_buf + remain_len;
                }

                //at the end of buffer, so rotate to header of buffer
                if (video_end == (video_buf + BLOCK_SIZE)) {
    
    
                    video_pos = video_buf;
                    video_end = video_buf;
                    blank_space_len = BLOCK_SIZE;
                }

                //从文件中读取yuv数据缓存到video_end
                if ((video_buff_len = fread(video_end, 1, blank_space_len, video_fd)) <= 0) {
    
    
                    fprintf(stderr, "eof, exit thread!");
                    thread_exit = 1;
                    continue;// to wait event for exiting
                }

                //reset video_end
                video_end += video_buff_len;
                blank_space_len -= video_buff_len;
                printf("not enought data: pos:%p, video_end:%p, blank_space_len:%d\n", video_pos, video_end, blank_space_len);
            }

            // 更新纹理
            SDL_UpdateTexture(texture, NULL, video_pos, video_width);

            //FIX: If window is resize
            rect.x = 0;
            rect.y = 0;
            rect.w = w_width;
            rect.h = w_height;

            // 显示到窗口
            SDL_RenderClear(renderer);
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            SDL_RenderPresent(renderer);

            printf("not enought data: pos:%p, video_end:%p, blank_space_len:%d\n", video_pos, video_end, blank_space_len);
            video_pos += yuv_frame_len;

        }
        else if (event.type == SDL_WINDOWEVENT) {
    
    
            //If Resize
            SDL_GetWindowSize(win, &w_width, &w_height);
        }
        else if (event.type == SDL_QUIT) {
    
    
            thread_exit = 1;
        }
        else if (event.type == QUIT_EVENT) {
    
    
            break;
        }
    } while (1);

__FAIL:

    //close file
    if (video_fd) {
    
    
        fclose(video_fd);
    }

    SDL_Quit();

    return 0;
}


猜你喜欢

转载自blog.csdn.net/qq_38056514/article/details/130190876