• [音视频] 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
    ", 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 ", 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 ",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 "); 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
    ", 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
    ", 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
    ");
            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.
    ");
            return -1;
        }
        if (avformat_find_stream_info(pFormatCtx, NULL)<0) {
            printf("Couldn't find stream information.
    ");
            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.
    ");
            return -1;
        }
        pCodecCtx = pFormatCtx->streams[videoindex]->codec;
        pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
        if (pCodec == NULL) {
            printf("Codec not found.
    ");
            return -1;
        }
        if (avcodec_open2(pCodecCtx, pCodec, NULL)<0) {
            printf("Could not open codec.
    ");
            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
    ", 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
    ", 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.
    ");
                                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中进行显示。但以这种方式实现的多屏可能存在问题:多个不同视频帧率不同的时候无法使用同一频率进行刷新。

    保持学习,否则迟早要被淘汰*(^ 。 ^ )***
  • 相关阅读:
    开发中的一些总结2
    XML与DataTable/DataSet互转(C#) 把数据库中表的内容转存为XML文件
    闲来无事。。。。
    一:Js的Url中传递中文参数乱码问题,重点:encodeURI编码,decodeURI解码:
    20120301 14:10 js函数内部实现休眠
    设为首页和收藏本站的代码
    Jquery实现对a标签改变选中的背景色 支持多选 再次点击背景色消失
    asp.net上传图片并生成等比例缩略图(.Net自带函数完成)
    类中只有 成员变量 和 一个成员函数表
    CListCtrl 使用技巧
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/14330502.html
Copyright © 2020-2023  润新知