[音视频] SDL库

该笔记为学习雷神(雷霄骅)FFmpeg+SDL视频播放器教程的笔记。内容和代码均来自于视频教程,缅怀雷神。

一、什么是SDL库

SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。

SDL(Simple DirectMedia Layer)库封装了复杂的视音频底层交互工作(将视频或音频数据传递给显示器、音响等设备进行处理的过程),简化了视音频处理的难度。

SDL层次架构:

 

 可以从上图看到,SDL屏蔽了操作系统之间的接口差异,实现了跨平台开发视音频应用的功能。SDL可以自动根据当前平台的实际情况调用对应的底层接口。

二、SDL开发环境搭建

1.拷贝文件

请参考ffmpeg开发环境的搭建流程,过程大致相同。

这里需要注意SDL库是X86还是X64版本。下载地址:http://www.libsdl.org/download-2.0.php

将上图中的文件和目录拷贝到工程目录下。

2.参考FFmpeg的配置流程

1) 配置属性-->C/C++-->常规-->附加包含目录,输入"include"(项目目录下的include)。

2)配置属性-->连接器-->常规-->附加库目录,输入"lib"(项目目录下的lib)。

3)配置属性-->连接器-->输入-->附加依赖项,输入lib目录下所有"*.lib"文件,用分号隔开。

4)动态库不用配置。

3.测试

#include <stdio.h>

extern "C"
{
#include "SDL2/SDL.h"
}


int main(int argc, char* argv[])
{
    if(SDL_Init(SDL_INIT_VIDEO)) {  
        printf( "Could not initialize SDL - %s\n", SDL_GetError()); 
    } else{
        printf("Success init SDL");
    }
    return 0;
}

如果打印“Success init SDL”表示SDL开发环境搭建成功。

三、SDL播放YUV文件示例

1.SDL视频显示流程

 

1.SDL_Init():初始化SDL组件。

2.SDL_CreateWindow():创建窗口实例SDL_Window。

3.SDL_Window:窗口实例。

4.SDL_CreateRenderer():创建一个渲染器实例SDL_Renderer。

5.SDL_Renderer:渲染器实例。

6.SDL_CreateTexture():创建一个材质(或质地)实例SDL_Texture,显示不同的图像就是替换不同的质地。

7.SDL_Texture:质地实例。

8.SDL_UpdateTexture():将一个YUV或RGB数据设置为材质的数据。

9.SDL_RenderCopy():自动将材质的数据拷贝到渲染器。

10.SDL_RenderPresent():显示到显示器。

11.Decode():代表图像帧的解码操作。

12.YUV/RGB:代表图像帧解码后的原始数据,可能是YUV或RGB格式的。

注意:其中7、8、9、10、11、12形成一个循环,代表一帧一帧显示到屏幕上。

另外还要两个比较重要的工具函数:

SDL_Delay():用于每帧显示之间的延时。

SDL_Quit():退出SDL系统。

2.SDL视频显示的数据结构

 

1.YUV Data:表示一帧解码后的原始图像,这里可能存在多个YUV数据,因为一个Window中可以同时显示多屏视频(例如4x4的监控画面)。

2.SDL_Texture:每个YUV数据对应一个质地实例,质地实例用来存放图像数据。

3.SDL_Rect:表示一个矩形的区域(其中包含坐标),表示在某个位置显示图像,当然,一个YUV数据可以显示在多个地方,所以可以对应多个Rect。

4.SDL_Renderer:接收到所有要显示的数据和位置,然后将数据画到Window中。

5.SDL_Window:展示图像的地方。

3.示例代码

#include <stdio.h>
#include "stdafx.h" extern "C" { #include "SDL2/SDL.h" }; const int bpp=12; int screen_w=1280,screen_h=720; const int pixel_w=1280,pixel_h=720; unsigned char buffer[pixel_w*pixel_h*bpp/8]; int main(int argc, char* argv[]) { if(SDL_Init(SDL_INIT_VIDEO)) { // 初始化SDL组件 printf( "Could not initialize SDL - %s\n", SDL_GetError()); return -1; } SDL_Window *screen; // 定义一个SDL_Window指针 //SDL 2.0 Support for multiple windows screen = SDL_CreateWindow("Simplest Video Play SDL2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h,SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE); // 创建一个SDL_Window实例 if(!screen) { printf("SDL: could not create window - exiting:%s\n",SDL_GetError()); return -1; } SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0); // 创建一个SDL_Renderer实例 Uint32 pixformat=0; //IYUV: Y + U + V (3 planes) //YV12: Y + V + U (3 planes) pixformat= SDL_PIXELFORMAT_IYUV; // IYUV模式,YUV数据的存放顺序不同 SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer,pixformat, SDL_TEXTUREACCESS_STREAMING,pixel_w,pixel_h); // 创建一个SDL_Texture材质实例 FILE *fp=NULL; fp=fopen("sintel_640_360.yuv","rb+"); // 打开一个YUV文件 if(fp==NULL){ printf("cannot open this file\n"); return -1; } SDL_Rect sdlRect; while(1){ // 循环读取YUV的帧数据,存放到buffer中 if (fread(buffer, 1, pixel_w*pixel_h*bpp/8, fp) != pixel_w*pixel_h*bpp/8){ // 注意,这里的bpp/8为1.5,因为一帧Y数据为w*h字节,而U和V数据分别为w*h*1/4字节,加起来是w*h*1.5字节 // 这里是循环播放一个文件,重新重头开始读取 fseek(fp, 0, SEEK_SET); fread(buffer, 1, pixel_w*pixel_h*bpp/8, fp); } // 将YUV帧数据更新到Texture中 SDL_UpdateTexture( sdlTexture, NULL, buffer, pixel_w); // 设置显示位置,左上角的坐标的0,0。宽为640,高位360 sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = screen_w; // 这里矩形的宽为window的宽度,如果要实现2x2屏,则为screen_w/2 sdlRect.h = screen_h; // 这里矩形的宽为window的高度,如果要实现2x2屏,则为screen_h/2 // 清除Render中的数据 SDL_RenderClear( sdlRenderer ); // 重新拷贝Texture中的数据到Renderer中 SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect); // 显示到屏幕 SDL_RenderPresent( sdlRenderer ); // 设置帧与帧之间的显示间隔,40ms相当于帧率为25fps,如果要快放或慢放,则修改该时间 SDL_Delay(40); } SDL_Quit(); // 退出SDL系统 return 0; }

在上述程序代码中,存在以下两个问题:

1.窗口处于无法移动的状态(繁忙状态)。

2.鼠标无法操作。

存在以上两个问题的原因是窗口阻塞(SDL_Delay(40)导致阻塞),我们需要使用多线程和消息机制来处理这个问题。

四、多线程解决窗口阻塞

#include <stdio.h>
#include "stdafx.h"

extern "C"
{
#include "SDL2/SDL.h"
};

const int bpp = 12;

int screen_w = 1280, screen_h = 720;  // 这里的宽和高指Window的宽和高
const int pixel_w = 1280, pixel_h = 720;  // 特别注意,这里的宽和高代表图像的宽和高,一定要个YUV视频的宽高一致,否则数据会错位

unsigned char buffer[pixel_w*pixel_h*bpp / 8];

//Refresh Event
#define REFRESH_EVENT  (SDL_USEREVENT + 1)  // 自定义一个刷新信号
//Break
#define BREAK_EVENT  (SDL_USEREVENT + 2)  // 自定义一个返回信号

// 一个全局变量,控制子线程的退出
int thread_exit = 0;

// 子线程要执行的函数,该函数中每40毫秒给主线程发送一个刷新画面的信号,这样子线程代替了主线程进行阻塞等待,主线程中的窗口就不会阻塞了。
int refresh_video(void *opaque) {
    thread_exit = 0;
    while (thread_exit == 0) {
        SDL_Event event;
        event.type = REFRESH_EVENT;  // 定义了一个REFRESH_EVENT信号
        SDL_PushEvent(&event);  // 发送信号
        SDL_Delay(40);  // 等待40ms
    }
    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 2.0 Support for multiple windows
    screen = SDL_CreateWindow("Simplest Video Play SDL2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        screen_w, screen_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!screen) {
        printf("SDL: could not create window - exiting:%s\n", SDL_GetError());
        return -1;
    }
    SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0);

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

    SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer, pixformat, SDL_TEXTUREACCESS_STREAMING, pixel_w, pixel_h);

    FILE *fp = NULL;
    fp = fopen("testyuv.yuv", "rb+");

    if (fp == NULL) {
        printf("cannot open this file\n");
        return -1;
    }

    SDL_Rect sdlRect;

    // 创建一个子线程负责每帧显示间隔的等待
    SDL_Thread *refresh_thread = SDL_CreateThread(refresh_video, NULL, NULL);
    // 定义一个SDL_Event实例,用于接收子线程发送的信号
    SDL_Event event;
    while (1) {
        // 等待刷新信号的到来,这个函数是阻塞的,直到接收到一个信号
        SDL_WaitEvent(&event);
        // 判断信号是否是刷新信号(来自子线程),如果是刷新信号,则开始显示下一帧图像
        if (event.type == REFRESH_EVENT) {
            if (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(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(sdlRenderer);
            SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
            SDL_RenderPresent(sdlRenderer);

        }
        else if (event.type == SDL_WINDOWEVENT) {  // 如果信号是窗口信号,则获取当前窗口的宽和高,存放到screen_w和screen_h变量中,下一帧图像则会根据该变量的值进行窗口大小的变化。
                                                   //If Resize
            SDL_GetWindowSize(screen, &screen_w, &screen_h);
        }
        else if (event.type == SDL_QUIT) {  // 如果接受到的信号是退出信号(点击窗口X触发的信号)
            thread_exit = 1;  // 将子线程退出的标志变量的值修改为1,表示退出。
        }
        else if (event.type == BREAK_EVENT) {  // 退出循环信号,由子线程发出。我们点击窗口X后,thread_exit=1,子线程会发送一个BREAK_EVENT信号,这里接受到该信号后会退出循环。
            break;
        }
    }
    SDL_Quit();  // 退出SDL系统
    return 0;
}

以上代码是使用了多线程和信号机制优化后的程序(注释部分为该代码的核心部分),该程序可以移动窗口(不阻塞),并且我们可以在其中扩展更多的信号操作。

 五、SDL结合FFmpeg播放MP4文件

#include "stdafx.h"
#include <stdio.h>


#define __STDC_CONSTANT_MACROS
#define SWS_BICUBIC 4

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
};




//Refresh Event
#define SFM_REFRESH_EVENT  (SDL_USEREVENT + 1)

#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2)

int thread_exit = 0;

int sfp_refresh_thread(void *opaque) {
    thread_exit = 0;
    while (!thread_exit) {
        SDL_Event event;
        event.type = SFM_REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }
    thread_exit = 0;
    //Break
    SDL_Event event;
    event.type = SFM_BREAK_EVENT;
    SDL_PushEvent(&event);

    return 0;
}


int main(int argc, char* argv[])
{

    AVFormatContext    *pFormatCtx;
    int                i, videoindex;
    AVCodecContext    *pCodecCtx;
    AVCodec            *pCodec;
    AVFrame    *pFrame, *pFrameYUV;
    uint8_t *out_buffer;
    AVPacket *packet;
    int ret, got_picture;

    //------------SDL----------------
    int screen_w, screen_h;
    SDL_Window *screen;
    SDL_Renderer* sdlRenderer;
    SDL_Texture* sdlTexture;
    SDL_Rect sdlRect, sdlRect2, sdlRect3, sdlRect4;
    SDL_Thread *video_tid;
    SDL_Event event;

    struct SwsContext *img_convert_ctx;

    char filepath[] = "output.mp4";

    av_register_all();
    avformat_network_init();
    pFormatCtx = avformat_alloc_context();

    if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0) {
        printf("Couldn't open input stream.\n");
        return -1;
    }
    if (avformat_find_stream_info(pFormatCtx, NULL)<0) {
        printf("Couldn't find stream information.\n");
        return -1;
    }
    videoindex = -1;
    for (i = 0; i<pFormatCtx->nb_streams; i++)
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoindex = i;
            break;
        }
    if (videoindex == -1) {
        printf("Didn't find a video stream.\n");
        return -1;
    }
    pCodecCtx = pFormatCtx->streams[videoindex]->codec;
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL) {
        printf("Codec not found.\n");
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec, NULL)<0) {
        printf("Could not open codec.\n");
        return -1;
    }
    pFrame = av_frame_alloc();
    pFrameYUV = av_frame_alloc();
    out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
        pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);


    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) {
        printf("Could not initialize SDL - %s\n", SDL_GetError());
        return -1;
    }
    //SDL 2.0 Support for multiple windows
    screen_w = pCodecCtx->width;
    screen_h = pCodecCtx->height;
    screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        screen_w, screen_h, SDL_WINDOW_OPENGL);

    if (!screen) {
        printf("SDL: could not create window - exiting:%s\n", SDL_GetError());
        return -1;
    }
    sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
    //IYUV: Y + U + V  (3 planes)
    //YV12: Y + V + U  (3 planes)
    sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);

    sdlRect.x = 0;
    sdlRect.y = 0;
    sdlRect.w = screen_w/2;
    sdlRect.h = screen_h/2;
    sdlRect2.x = screen_w / 2;
    sdlRect2.y = screen_h / 2;
    sdlRect2.w = screen_w/2;
    sdlRect2.h = screen_h/2;
    sdlRect3.x = screen_w / 2;
    sdlRect3.y = 0;
    sdlRect3.w = screen_w / 2;
    sdlRect3.h = screen_h / 2;
    sdlRect4.x = 0;
    sdlRect4.y = screen_h / 2;
    sdlRect4.w = screen_w / 2;
    sdlRect4.h = screen_h / 2;

    packet = (AVPacket *)av_malloc(sizeof(AVPacket));

    video_tid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL);
    //------------SDL End------------
    //Event Loop

    for (;;) {
        //等待刷新信号
        SDL_WaitEvent(&event);
        if (event.type == SFM_REFRESH_EVENT) {

            while (1) {
                if (av_read_frame(pFormatCtx, packet) >= 0) {
                    if (packet->stream_index == videoindex) {
                        ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
                        if (ret < 0) {
                            printf("Decode Error.\n");
                            return -1;
                        }
                        if (got_picture) {
                            sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
                            //SDL---------------------------
                            SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0]);
                            SDL_RenderClear(sdlRenderer);
                            SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);  // 这里可以使用多个Rect实现多屏显示
                            SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect2);
                            SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect3);
                            SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect4);
                            //SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL);
                            SDL_RenderPresent(sdlRenderer);
                            //SDL End-----------------------
                        }
                        av_free_packet(packet);
                        break;
                    }
                    else {  // 如果不是视频帧,则跳过,直到读取到视频帧进行处理
                        av_free_packet(packet);
                        continue;
                    }
                }
                else {  // 文件读取结束
                    thread_exit = 1;
                    break;

                }
            }
        }
        else if (event.type == SDL_QUIT) {
            thread_exit = 1;
        }
        else if (event.type == SFM_BREAK_EVENT) {
            break;
        }

    }

    sws_freeContext(img_convert_ctx);

    SDL_Quit();
    //--------------
    av_frame_free(&pFrameYUV);
    av_frame_free(&pFrame);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    return 0;
}

特别注意:

1.该代码未处理音频数据。

2.在每帧处理的循环中,使用第二层循环跳过音频帧的处理,否则音频帧也会使子线程延迟40ms,视频会变得很慢。

3.在 SDL_RenderCopy 部分可以使用多次来达到多屏显示的效果。

4.当鼠标拖动窗口的时候,画面会定住,这是因为窗口主线程响应了鼠标事件,渲染图像的循环被暂停了。所以,读取和渲染图像的过程应该放到子线程中完成。

效果如下:

以上效果是将一个文件复制到4个Rect中进行显示。但以这种方式实现的多屏可能存在问题:多个不同视频帧率不同的时候无法使用同一频率进行刷新。

 

posted @ 2021-01-27 18:29  风间悠香  阅读(590)  评论(0编辑  收藏  举报