SDL音视频渲染

642 阅读12分钟

1. SDL-Windows 平台开发简介

1.1 SDL 简介

SDL,全称 Simple DirectMedia Layer,是一套跨平台的开发库,专门用于开发具有图形渲染的应用,如游戏、模拟器、媒体播放器等。由于其简单、直接的接口和高效的性能,SDL 在游戏开发和媒体处理中得到了广泛的应用。

SDL 提供了对图形硬件、音频、网络、事件、定时器等的底层访问。此外,SDL 还提供了对 2D 图形渲染、音频混合、文件系统访问、线程管理和其他相关功能的支持。SDL 对 OpenGL 和 Direct3D 这两种主流的图形接口都提供了支持,使得开发者可以方便地开发 2D 和 3D 应用。

SDL 的设计目标是提供一套简单、高效、稳定且可移植的开发接口。因此,SDL 用 C 语言编写,可以在几乎所有的平台上运行,包括 Windows、Mac OS X、Linux、iOS 和 Android 等。SDL 的开源性质也让开发者可以自由地使用、修改和分发 SDL,使得 SDL 成为了游戏和媒体开发者的重要工具。

1.2 SDL 主要功能模块介绍

  1. 视频: SDL 提供了对各种图形硬件的抽象访问,使得开发者可以更方便地创建窗口、处理窗口事件、以及进行图形渲染。此外,SDL 还支持 OpenGL 和 Direct3D,方便开发者创建2D或3D的图形程序。
  2. 音频: SDL 为音频处理提供了强大的支持,包括音频播放、音频录制、音频混合以及对多种音频格式的支持。开发者可以用 SDL 来播放 PCM 数据,也可以使用 SDL_mixer 扩展库来播放 MP3、WAV 等格式的音频。
  3. 输入: SDL 提供了一套完善的事件处理机制,支持键盘、鼠标、触摸屏以及游戏手柄等多种输入设备。此外,SDL 还支持剪贴板操作,以及对 Unicode 文本的输入。
  4. 线程: SDL 包含了一套线程操作的 API,包括创建线程、线程同步以及线程间的通信。这些功能使得 SDL 可以支持多线程编程。
  5. 定时器: SDL 提供了获取和操作时间的功能,包括获取当前时间、延时等待、设置定时器等。这些功能对于游戏开发和动画效果的实现非常有用。
  6. 文件系统: SDL 提供了一套简单的文件访问接口,支持读写本地文件,也支持通过 HTTP 或者 FTP 访问网络文件。此外,SDL 还支持对 ZIP 和 LZMA 压缩文件的访问。

2. Window 显示

2.1 创建窗口

SDL (Simple DirectMedia Layer) 提供了用于处理窗口的 API,使得开发者可以方便地创建窗口、调整窗口大小、处理窗口事件等。下面是一些关于 SDL 中窗口显示的详细介绍:

  1. 创建窗口: 使用 SDL_CreateWindow 函数可以创建一个窗口。这个函数需要指定窗口的标题、初始位置、初始大小以及一些窗口标志。例如:

    SDL_Window *window = SDL_CreateWindow("Hello, SDL",
                                          SDL_WINDOWPOS_UNDEFINED,
                                          SDL_WINDOWPOS_UNDEFINED,
                                          800, 600,
                                          SDL_WINDOW_SHOWN);
    

    这个函数将创建一个初始大小为 800x600、标题为 "Hello, SDL" 的窗口。

  2. 窗口大小和位置: SDL 提供了 SDL_SetWindowSizeSDL_GetWindowSizeSDL_SetWindowPositionSDL_GetWindowPosition 等函数来设置和获取窗口的大小和位置。

  3. 窗口状态: 使用 SDL_SetWindowFullscreenSDL_ShowWindowSDL_HideWindowSDL_MaximizeWindowSDL_MinimizeWindow 等函数可以改变窗口的状态。例如,可以将窗口设置为全屏模式,或者隐藏窗口。

  4. 窗口事件: SDL 提供了一套事件处理机制,可以用来处理窗口事件。例如,可以通过 SDL_Event 结构体和 SDL_PollEvent 函数来处理窗口关闭事件:

    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            // handle window close event
        }
    }
    
  5. 销毁窗口: 当不再需要一个窗口时,可以使用 SDL_DestroyWindow 函数来销毁它。

以上就是 SDL 中处理窗口的一些基本操作,通过这些操作,开发者可以方便地在应用程序中显示和控制窗口。

2.2 渲染纹理

在 SDL 中,渲染纹理是一个很常见的操作。纹理实际上就是显存中的一块区域,可以用来保存图像数据。在我们需要将图像数据展示到窗口上时,就可以将这个纹理渲染到窗口上。

首先,我们需要一个 SDL_Renderer 对象来进行渲染操作。当我们创建窗口时,可以使用 SDL_CreateRenderer 函数来创建一个 SDL_Renderer 对象:

SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

然后,我们可以使用 SDL_CreateTexture 函数来创建一个纹理:

SDL_Texture *texture = SDL_CreateTexture(renderer,
                                         SDL_PIXELFORMAT_RGBA8888,
                                         SDL_TEXTUREACCESS_TARGET,
                                         width,
                                         height);

这个函数需要指定纹理的像素格式、访问方式以及纹理的宽度和高度。

当我们需要将图像数据展示到窗口上时,就可以使用 SDL_RenderCopy 函数将纹理渲染到窗口上:

SDL_RenderCopy(renderer, texture, NULL, NULL);

最后,我们需要使用 SDL_RenderPresent 函数来更新窗口:

SDL_RenderPresent(renderer);

以上就是在 SDL 中创建和渲染纹理的基本步骤。需要注意的是,当我们不再需要一个纹理时,应该使用 SDL_DestroyTexture 函数来销毁它。同样,当我们不再需要一个 SDL_Renderer 对象时,应该使用 SDL_DestroyRenderer 函数来销毁它。下面给出一个完整示例:

#include <stdio.h>
#include <SDL.h>
#undef main
int main()
{
    int run = 1;
    SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    SDL_Texture *texture = NULL;
    SDL_Rect rect; // 长方形,原点在左上角
    rect.w = 50;    //方块大小
    rect.h = 50;

    SDL_Init(SDL_INIT_VIDEO);//初始化函数,可以确定希望激活的子系统

    window = SDL_CreateWindow("2 Window",
                              SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED,
                              640,
                              480,
                              SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);// 创建窗口

    if (!window)
    {
        return -1;
    }
    renderer = SDL_CreateRenderer(window, -1, 0);//基于窗口创建渲染器
    if (!renderer)
    {
        return -1;
    }

    texture = SDL_CreateTexture(renderer,
                                   SDL_PIXELFORMAT_RGBA8888,
                                   SDL_TEXTUREACCESS_TARGET,
                                   640,
                                   480); //创建纹理

    if (!texture)
    {
        return -1;
    }

    int show_count = 0;
    while (run)
    {
        rect.x = rand() % 600;
        rect.y = rand() % 400;

        SDL_SetRenderTarget(renderer, texture); // 设置渲染目标为纹理
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); // 纹理背景为黑色
        SDL_RenderClear(renderer); //清屏

        SDL_RenderDrawRect(renderer, &rect); //绘制一个长方形
        SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255); //长方形为白色
        SDL_RenderFillRect(renderer, &rect);

        SDL_SetRenderTarget(renderer, NULL); //恢复默认,渲染目标为窗口
        SDL_RenderCopy(renderer, texture, NULL, NULL); //拷贝纹理到CPU

        SDL_RenderPresent(renderer); //输出到目标窗口上
        SDL_Delay(300);
        if(show_count++ > 30)
        {
            run = 0;        // 不跑了
        }
    }

    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window); //销毁窗口
    SDL_Quit();
    return 0;
}

3. Event 事件

在 SDL 中,事件(Event)是一种重要的输入处理机制。通过事件,程序可以接收到来自用户的各种输入,如键盘按键、鼠标移动、窗口关闭等。以下是 SDL 事件处理的基本介绍:

首先需要创建一个 SDL_Event 类型的变量来接收事件数据。这个变量通常在主循环中创建,如:

SDL_Event event;

然后可以使用 SDL_PollEvent 函数来获取一个事件。这个函数会检查事件队列中是否有事件,如果有,就将事件数据填充到提供的 SDL_Event 结构中,并从事件队列中删除这个事件。如果没有事件,这个函数就立即返回。因此,你通常需要在一个循环中调用这个函数,如:

while (SDL_PollEvent(&event)) {
    // handle event
}

接下来可以根据 SDL_Event 结构的 type 成员来判断事件的类型,然后进行相应的处理。例如,你可以这样处理键盘按键事件:

if (event.type == SDL_KEYDOWN) {
    switch (event.key.keysym.sym) {
        case SDLK_LEFT:
            // handle left arrow key
            break;
        case SDLK_RIGHT:
            // handle right arrow key
            break;
        // ...
    }
}

或者这样处理窗口关闭事件:

if (event.type == SDL_QUIT) {
    // handle window close event
}

以上就是 SDL 中处理事件的基本方法。SDL 支持多种类型的事件,如键盘事件、鼠标事件、窗口事件、游戏手柄事件等。通过这些事件可以接收到用户的各种输入,并在程序中进行相应的处理。下面给出一个完整实例。

#include <SDL.h>
#include <stdio.h>
#define FF_QUIT_EVENT    (SDL_USEREVENT + 2) // 用户自定义事件
#undef main
int main(int argc, char* argv[])
{
    SDL_Window *window = NULL;              // Declare a pointer
    SDL_Renderer *renderer = NULL;

    SDL_Init(SDL_INIT_VIDEO);               // Initialize SDL2

    // Create an application window with the following settings:
    window = SDL_CreateWindow(
                "An SDL2 window",                  // window title
                SDL_WINDOWPOS_UNDEFINED,           // initial x position
                SDL_WINDOWPOS_UNDEFINED,           // initial y position
                640,                               // width, in pixels
                480,                               // height, in pixels
                SDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS// flags - see below
                );

    // Check that the window was successfully created
    if (window == NULL)
    {
        // In the case that the window could not be made...
        printf("Could not create window: %s\n", SDL_GetError());
        return 1;
    }

    /* We must call SDL_CreateRenderer in order for draw calls to affect this window. */
    renderer = SDL_CreateRenderer(window, -1, 0);

    /* Select the color for drawing. It is set to red here. */
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);

    /* Clear the entire screen to our selected color. */
    SDL_RenderClear(renderer);

    /* Up until now everything was drawn behind the scenes.
       This will show the new, red contents of the window. */
    SDL_RenderPresent(renderer);

    SDL_Event event;
    int b_exit = 0;
    for (;;)
    {
        SDL_WaitEvent(&event);
        switch (event.type)
        {
        case SDL_KEYDOWN:	/* 键盘事件 */
            switch (event.key.keysym.sym)
            {
            case SDLK_a:
                printf("key down a\n");
                break;
            case SDLK_s:
                printf("key down s\n");
                break;
            case SDLK_d:
                printf("key down d\n");
                break;
            case SDLK_q:
                printf("key down q and push quit event\n");
                SDL_Event event_q;
                event_q.type = FF_QUIT_EVENT;
                SDL_PushEvent(&event_q);
                break;
            default:
                printf("key down 0x%x\n", event.key.keysym.sym);
                break;
            }
            break;
        case SDL_MOUSEBUTTONDOWN:			/* 鼠标按下事件 */
            if (event.button.button == SDL_BUTTON_LEFT)
            {
                printf("mouse down left\n");
            }
            else if(event.button.button == SDL_BUTTON_RIGHT)
            {
                printf("mouse down right\n");
            }
            else
            {
                printf("mouse down %d\n", event.button.button);
            }
            break;
        case SDL_MOUSEMOTION:		/* 鼠标移动事件 */
            printf("mouse movie (%d,%d)\n", event.button.x, event.button.y);
            break;
        case FF_QUIT_EVENT:
            printf("receive quit event\n");
            b_exit = 1;
            break;
        }
        if(b_exit)
            break;
    }

    //destory renderer
    if (renderer)
        SDL_DestroyRenderer(renderer);

    // Close and destroy the window
    if (window)
        SDL_DestroyWindow(window);

    // Clean up
    SDL_Quit();
    return 0;
}

4. Thread

SDL 中的多线程处理是通过 SDL_Thread 来完成的。多线程能让程序同时处理多个任务,这对于需要实时响应用户输入或处理大量数据的程序非常重要。以下是 SDL 中使用线程的基本步骤:

首先需要定义一个函数来作为线程的入口点。这个函数必须接受一个 void * 类型的参数,并返回一个 int

int MyThread(void *data) {
    // do something
    return 0;
}

然后可以使用 SDL_CreateThread 函数来创建一个线程:

SDL_Thread *thread = SDL_CreateThread(MyThread, "MyThread", (void *)NULL);

这个函数需要指定线程入口点函数、线程的名称以及传递给线程的参数。线程创建后将开始运行。

如果你需要等待一个线程结束,可以使用 SDL_WaitThread 函数:

SDL_WaitThread(thread, NULL);

这个函数将阻塞当前线程,直到指定的线程结束。如果你需要获取线程的返回值,可以提供一个 int * 类型的参数。

在多线程环境中,数据访问往往需要同步。SDL 提供了几种同步机制,如互斥量(SDL_mutex)、条件变量(SDL_cond)等。例如,你可以这样创建和使用互斥量:

SDL_mutex *mutex = SDL_CreateMutex();

SDL_LockMutex(mutex);
// critical section
SDL_UnlockMutex(mutex);

SDL_DestroyMutex(mutex);

SDL 还提供了 SDL_Semaphore 类型和一系列函数来支持信号量的使用。以下是在 SDL 中使用同步信号量的基本步骤:

首先使用 SDL_CreateSemaphore 函数来创建一个信号量:

SDL_sem *sem = SDL_CreateSemaphore(initial_value);

这个函数接受一个参数,表示信号量的初始值,即可用的资源数量。

当一个线程需要使用资源时,它应该使用 SDL_SemWaitSDL_SemTryWait 函数来等待信号量:

SDL_SemWait(sem);  // wait until the semaphore is positive
SDL_SemTryWait(sem);  // try to wait and return immediately

SDL_SemWait 会阻塞当前线程,直到信号量变为正。SDL_SemTryWait 会立即返回,如果信号量是正的,它就将信号量减一并返回0,否则就返回非零值。

当一个线程使用完资源后,它应该使用 SDL_SemPost 函数来增加信号量:

SDL_SemPost(sem);  // increase the semaphore

这个函数将信号量加一,并唤醒等待队列中的一个线程。

当你不再需要一个信号量时,应该使用 SDL_DestroySemaphore 函数来销毁它:

SDL_DestroySemaphore(sem);

以上就是在 SDL 中使用信号量的基本方法。使用信号量时需要注意,每次 SDL_SemWaitSDL_SemTryWait 成功后,都应该有对应的 SDL_SemPost。否则,信号量可能会永远变为非正,导致其它线程无法继续执行。多线程编程需要注意数据同步和线程安全问题,否则可能会出现数据错误或程序崩溃等问题。下面给出一个完整实例。

#include <SDL.h>
#include <stdio.h>

SDL_mutex *s_lock = NULL;
SDL_cond *s_cond = NULL;

int thread_work(void *arg)
{
    SDL_LockMutex(s_lock);
    printf("                <============thread_work sleep\n");
    sleep(10);      // 用来测试获取锁
    printf("                <============thread_work wait\n");
    // 释放s_lock资源,并等待signal。之所以释放s_lock是让别的线程能够获取到s_lock
    SDL_CondWait(s_cond, s_lock); //另一个线程(1)发送signal和(2)释放lock后,这个函数退出

    printf("                <===========thread_work receive signal, continue to do ~_~!!!\n");
    printf("                <===========thread_work end\n");
    SDL_UnlockMutex(s_lock);
    return 0;
}

#undef main
int main()
{
    s_lock = SDL_CreateMutex();
    s_cond = SDL_CreateCond();
    SDL_Thread * t = SDL_CreateThread(thread_work,"thread_work",NULL);
    if(!t)
    {
        printf("  %s",SDL_GetError);
        return -1;
    }

    for(int i = 0;i< 2;i++)
    {
        sleep(2);
        printf("main execute =====>\n");
    }
    printf("main SDL_LockMutex(s_lock) before ====================>\n");
    SDL_LockMutex(s_lock);  // 获取锁,但是子线程还拿着锁
    printf("main ready send signal====================>\n");
    printf("main SDL_CondSignal(s_cond) before ====================>\n");
    SDL_CondSignal(s_cond); // 发送信号,唤醒等待的线程
    printf("main SDL_CondSignal(s_cond) after ====================>\n");
    sleep(10);
    SDL_UnlockMutex(s_lock);// 释放锁,让其他线程可以拿到锁
    printf("main SDL_UnlockMutex(s_lock) after ====================>\n");

    SDL_WaitThread(t, NULL);
    SDL_DestroyMutex(s_lock);
    SDL_DestroyCond(s_cond);

    return 0;
}

5. YUV 播放

在 SDL 中,播放 YUV 视频需要经过以下几个步骤:

  1. 初始化 SDL: 首先,我们需要初始化 SDL 的视频子系统。这可以通过调用 SDL_Init() 函数并传入 SDL_INIT_VIDEO 常量来完成:

    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }
    

SDL_Init() 函数的作用是初始化 SDL 库的各种子系统。它的参数可以是一个或多个子系统的常量,通过位或运算(|)组合在一起。SDL_INIT_AUDIO 是其中之一,表示初始化音频子系统。其他常量还包括 SDL_INIT_VIDEO(初始化视频子系统)、SDL_INIT_TIMER(初始化定时器子系统)等。

如果 SDL_Init(SDL_INIT_AUDIO) 执行成功,会返回 0;如果失败,会返回 -1,并通过 SDL_GetError() 函数返回错误信息。

  1. 创建窗口: 接着,我们需要创建一个窗口来显示视频。这可以通过调用 SDL_CreateWindow() 函数来完成:

    SDL_Window *window = SDL_CreateWindow("SDL Video Player", 
                                          SDL_WINDOWPOS_UNDEFINED,
                                          SDL_WINDOWPOS_UNDEFINED, 
                                          width, 
                                          height, 
                                          SDL_WINDOW_OPENGL);
    if (!window) {
        printf("Could not create window - %s\n", SDL_GetError());
        return -1;
    }
    

    这个函数需要指定窗口的标题、初始位置、初始大小以及一些窗口选项。

  2. 创建渲染器: 有了窗口之后,我们还需要创建一个渲染器来进行绘图。这可以通过调用 SDL_CreateRenderer() 函数来完成:

    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (!renderer) {
        printf("Could not create renderer - %s\n", SDL_GetError());
        return -1;
    }
    

    这个函数需要指定关联的窗口、渲染器的索引(-1表示选择第一个支持指定选项的渲染器)以及渲染器选项。

  3. 创建纹理: 接着,我们需要创建一个纹理来存储视频帧。这可以通过调用 SDL_CreateTexture() 函数来完成:

    SDL_Texture *texture = SDL_CreateTexture(renderer, 
                                             SDL_PIXELFORMAT_IYUV, 
                                             SDL_TEXTUREACCESS_STREAMING, 
                                             width, 
                                             height);
    if (!texture) {
        printf("Could not create texture - %s\n", SDL_GetError());
        return -1;
    }
    

    这个函数需要指定关联的渲染器、像素格式(YUV视频通常使用 SDL_PIXELFORMAT_IYUVSDL_PIXELFORMAT_YV12)、纹理访问方式以及纹理的大小。

  4. 显示视频帧: 最后,我们可以通过 SDL_UpdateTexture()SDL_RenderCopy() 函数来更新纹理并将其复制到渲染器,然后使用 SDL_RenderPresent() 函数来显示视频帧:

    SDL_UpdateTexture(texture, NULL, pixels, pitch);
    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);
    

    这里的 pixelspitch 应该从你的视频解码器获取。SDL_UpdateTexture() 函数用于将视频帧数据更新到纹理,SDL_RenderCopy() 函数用于将纹理复制到渲染器,SDL_RenderPresent() 函数用于更新窗口的显示内容。

以上就是在 SDL 中播放 YUV 视频的基本流程。请注意,这个流程只适用于本地视频播放。如果你需要进行流媒体播放或者使用其他视频格式,可能需要进行相应的修改。

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

#include <SDL.h>

//自定义消息类型
#define REFRESH_EVENT   (SDL_USEREVENT + 1)     // 请求画面刷新事件
#define QUIT_EVENT      (SDL_USEREVENT + 2)     // 退出事件

//定义分辨率
// YUV像素分辨率
#define YUV_WIDTH   320
#define YUV_HEIGHT  240
//定义YUV格式
#define YUV_FORMAT  SDL_PIXELFORMAT_IYUV

int s_thread_exit = 0;  // 退出标志 = 1则退出

int refresh_video_timer(void *data)
{
    while (!s_thread_exit)
    {
        SDL_Event event;
        event.type = REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }

    s_thread_exit = 0;

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

    return 0;
}
#undef main
int main(int argc, char* argv[])
{
    //初始化 SDL
    if(SDL_Init(SDL_INIT_VIDEO))
    {
        fprintf( stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }

    // SDL
    SDL_Event event;                            // 事件
    SDL_Rect rect;                              // 矩形
    SDL_Window *window = NULL;                  // 窗口
    SDL_Renderer *renderer = NULL;              // 渲染
    SDL_Texture *texture = NULL;                // 纹理
    SDL_Thread *timer_thread = NULL;            // 请求刷新线程
    uint32_t pixformat = YUV_FORMAT;            // YUV420P,即是SDL_PIXELFORMAT_IYUV

    // 分辨率
    // 1. YUV的分辨率
    int video_width = YUV_WIDTH;
    int video_height = YUV_HEIGHT;
    // 2.显示窗口的分辨率
    int win_width = YUV_WIDTH;
    int win_height = YUV_WIDTH;

    // YUV文件句柄
    FILE *video_fd = NULL;
    const char *yuv_path = "yuv420p_320x240.yuv";

    size_t video_buff_len = 0;

    uint8_t *video_buf = NULL; //读取数据后先把放到buffer里面

    // 我们测试的文件是YUV420P格式
    uint32_t y_frame_len = video_width * video_height;
    uint32_t u_frame_len = video_width * video_height / 4;
    uint32_t v_frame_len = video_width * video_height / 4;
    uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;

    //创建窗口
    window = SDL_CreateWindow("Simplest YUV Player",
                           SDL_WINDOWPOS_UNDEFINED,
                           SDL_WINDOWPOS_UNDEFINED,
                           video_width, video_height,
                           SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
    if(!window)
    {
        fprintf(stderr, "SDL: could not create window, err:%s\n",SDL_GetError());
        goto _FAIL;
    }
    // 基于窗口创建渲染器
    renderer = SDL_CreateRenderer(window, -1, 0);
    // 基于渲染器创建纹理
    texture = SDL_CreateTexture(renderer,
                                pixformat,
                                SDL_TEXTUREACCESS_STREAMING,
                                video_width,
                                video_height);

    // 分配空间
    video_buf = (uint8_t*)malloc(yuv_frame_len);
    if(!video_buf)
    {
        fprintf(stderr, "Failed to alloce yuv frame space!\n");
        goto _FAIL;
    }

    // 打开YUV文件
    video_fd = fopen(yuv_path, "rb");
    if( !video_fd )
    {
        fprintf(stderr, "Failed to open yuv file\n");
        goto _FAIL;
    }
    // 创建请求刷新线程
    timer_thread = SDL_CreateThread(refresh_video_timer,
                                    NULL,
                                    NULL);

    while (1)
    {
        // 收取SDL系统里面的事件
        SDL_WaitEvent(&event);

        if(event.type == REFRESH_EVENT) // 画面刷新事件
        {
            video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
            if(video_buff_len <= 0)
            {
                fprintf(stderr, "Failed to read data from yuv file!\n");
                goto _FAIL;
            }
            // 设置纹理的数据 video_width = 320, plane
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            // 显示区域,可以通过修改w和h进行缩放
            rect.x = 0;
            rect.y = 0;
            float w_ratio = win_width * 1.0 /video_width;
            float h_ratio = win_height * 1.0 /video_height;
            // 320x240 怎么保持原视频的宽高比例
            rect.w = video_width * w_ratio;
            rect.h = video_height * h_ratio;
//            rect.w = video_width * 0.5;
//            rect.h = video_height * 0.5;

            // 清除当前显示
            SDL_RenderClear(renderer);
            // 将纹理的数据拷贝给渲染器
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // 显示
            SDL_RenderPresent(renderer);
        }
        else if(event.type == SDL_WINDOWEVENT)
        {
            //If Resize
            SDL_GetWindowSize(window, &win_width, &win_height);
            printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n",win_width,
                   win_height );
        }
        else if(event.type == SDL_QUIT) //退出事件
        {
            s_thread_exit = 1;
        }
        else if(event.type == QUIT_EVENT)
        {
            break;
        }
    }

_FAIL:
    s_thread_exit = 1;      // 保证线程能够退出
    // 释放资源
    if(timer_thread)
        SDL_WaitThread(timer_thread, NULL); // 等待线程退出
    if(video_buf)
        free(video_buf);
    if(video_fd)
        fclose(video_fd);
    if(texture)
        SDL_DestroyTexture(texture);
    if(renderer)
        SDL_DestroyRenderer(renderer);
    if(window)
        SDL_DestroyWindow(window);

    SDL_Quit();

    return 0;

}

6. PCM 声音播放

  1. 初始化 SDL: 和播放视频一样,我们首先需要初始化 SDL 的音频子系统。这可以通过调用 SDL_Init() 函数并传入 SDL_INIT_AUDIO 常量来完成。

    if (SDL_Init(SDL_INIT_AUDIO) < 0) {
        printf("Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }
    
  2. 设定音频参数: 播放 PCM 数据需要知道一些关键的音频参数,例如采样率(Sample Rate)、采样格式(Sample Format)、声道数(Channels)、采样大小(Sample Size)。这些参数将在 SDL_AudioSpec 结构体中设定。

    SDL_AudioSpec wantSpec;
    wantSpec.freq = SAMPLE_RATE; // e.g., 44100
    wantSpec.format = AUDIO_S16SYS; // e.g., AUDIO_S16SYS
    wantSpec.channels = CHANNELS; // e.g., 2
    wantSpec.silence = 0;
    wantSpec.samples = 1024; // Good low-latency value for callback
    wantSpec.callback = fill_audio; // your audio callback function
    wantSpec.userdata = pcmData; // pointer to your PCM data
    
  3. 打开音频设备: 在设定好音频参数之后,我们可以使用 SDL_OpenAudio()SDL_OpenAudioDevice() 函数打开音频设备。

    if (SDL_OpenAudio(&wantSpec, &haveSpec) < 0) {
        printf("Failed to open audio: %s\n", SDL_GetError());
        return -1;
    }
    

    这里的 haveSpec 结构体将存储系统实际使用的音频参数,可能与 wantSpec 中设定的不同。因此,在播放音频时,应该按照 haveSpec 中的参数进行。

  4. 开始播放音频: 一旦音频设备打开,我们可以调用 SDL_PauseAudio() 函数开始播放音频。

    SDL_PauseAudio(0); // start playing audio
    

    在这个过程中,SDL 会在需要更多音频数据时调用我们在 SDL_AudioSpec 结构体中设定的回调函数。

  5. 在回调函数中填充音频数据: 回调函数应该是这样的:

    void fill_audio(void *udata, Uint8 *stream, int len) {
        // 'stream' is the audio output buffer, 'len' is the length
        // 'udata' is a pointer to the audio data source
        ...
    }
    

    在这个函数中,我们需要将音频数据从 udata 复制到 stream,长度为 len。请注意,这个函数会在音频线程中被调用,因此在此函数中应该避免执行耗时的操作。

以上就是在 SDL 中播放 PCM 数据的基本步骤。使用这个流程,你应该能够播放大多数常见的 PCM 格式的音频。但是,如果你需要播放其他格式的音频(例如 MP3 或 AAC),你可能需要使用额外的库进行解码,然后将解码后的 PCM 数据传递给 SDL 进行播放。

#include <stdio.h>
#include <SDL.h>

// 每次读取2帧数据, 以1024个采样点一帧 2通道 16bit采样点为例
#define PCM_BUFFER_SIZE (1024*2*2*2)

// 音频PCM数据缓存
static Uint8 *s_audio_buf = NULL;
// 目前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;


//音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
    SDL_memset(stream, 0, len);

    if(s_audio_pos >= s_audio_end) // 数据读取完毕
    {
        return;
    }

    // 数据够了就读预设长度,数据不够就只读部分(不够的时候剩多少就读取多少)
    int remain_buffer_len = s_audio_end - s_audio_pos;
    len = (len < remain_buffer_len) ? len : remain_buffer_len;
    // 拷贝数据到stream并调整音量
    SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
    printf("len = %d\n", len);
    s_audio_pos += len;  // 移动缓存指针
}

// 提取PCM文件
// ffmpeg -i input.mp4 -t 20 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
// 测试PCM文件
// ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
#undef main
int main(int argc, char *argv[])
{
    int ret = -1;
    FILE *audio_fd = NULL;
    SDL_AudioSpec spec;
    const char *path = "44100_16bit_2ch.pcm";
    // 每次缓存的长度
    size_t read_buffer_len = 0;

    //SDL initialize
    if(SDL_Init(SDL_INIT_AUDIO))    // 支持AUDIO
    {
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        return ret;
    }

    //打开PCM文件
    audio_fd = fopen(path, "rb");
    if(!audio_fd)
    {
        fprintf(stderr, "Failed to open pcm file!\n");
        goto _FAIL;
    }

    s_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE);

    // 音频参数设置SDL_AudioSpec
    spec.freq = 44100;          // 采样频率
    spec.format = AUDIO_S16SYS; // 采样点格式
    spec.channels = 2;          // 2通道
    spec.silence = 0;
    spec.samples = 1024;       // 23.2ms -> 46.4ms 每次读取的采样数量,多久产生一次回调和 samples
    spec.callback = fill_audio_pcm; // 回调函数
    spec.userdata = NULL;

    //打开音频设备
    if(SDL_OpenAudio(&spec, NULL))
    {
        fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
        goto _FAIL;
    }

    //play audio
    SDL_PauseAudio(0);

    int data_count = 0;
    while(1)
    {
        // 从文件读取PCM数据
        read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
        if(read_buffer_len == 0)
        {
            break;
        }
        data_count += read_buffer_len; // 统计读取的数据总字节数
        printf("now playing %10d bytes data.\n",data_count);
        s_audio_end = s_audio_buf + read_buffer_len;    // 更新buffer的结束位置
        s_audio_pos = s_audio_buf;  // 更新buffer的起始位置
        //the main thread wait for a moment
        while(s_audio_pos < s_audio_end)
        {
            SDL_Delay(10);  // 等待PCM数据消耗
        }
    }
    printf("play PCM finish\n");
    // 关闭音频设备
    SDL_CloseAudio();

_FAIL:
    //release some resources
    if(s_audio_buf)
        free(s_audio_buf);

    if(audio_fd)
        fclose(audio_fd);

    //quit SDL
    SDL_Quit();

    return 0;
}