SDL播放器实战

Posted 响箭零零一

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SDL播放器实战相关的知识,希望对你有一定的参考价值。

1 SDL简介

SDL(Simple DirectMedia Layer)是一个跨平台开发库(Windows、macOS、Linux、iosandroid等),旨在通过 OpenGL 和 Direct3D 提供对音频、键盘、鼠标、游戏杆和图形硬件的低级访问,开发者只需要编写一套代码既可以支持跨平台的运行。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。本文主要用到的是SDL中音视频控制和基础事件部分。下图可以看出SDL主要是对于不同平台的硬件控制库进行支持,使得上层应用对于底层控制无感,降低了开发的门槛。

2 SDL音视频播放基础API

2.1 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:包含上述所有选项

2.2 SDL视频模块

2.2.1 视频API

视频API三大控件窗口(window)、渲染器(renderer)、纹理(texture)。窗口作为所有界面的载体负责基础的窗口事件的捕获和响应;渲染器则是将图像数据渲染成图像;纹理主要是用来管理图像将图像数据加入到纹理,方便后续加载到渲染器。

- 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系统

2.2.2 视频结构体

视频基础:窗口、渲染器、纹理和矩形等对象。

 -  SDL_Window                  		// 窗口
 -  SDL_Renderer             			// 渲染
 -  SDL_Texture             		 	// 纹理
 -  SDL_Rect                         	// 矩形

2.2.2 音频API

音频API基础流程比较简单,首先是打开音频设备SDL_OpenAudio,并开启音频播放SDL_PauseAudio,利用音频回调函数SDL_AudioCallback将数据写入音频播放缓冲区播放SDL_MixAudio,最后关闭音频播放SDL_CloseAudio。

- SDL_Init():初始化SDL系统
- SDL_OpenAudio():打开音频设备
- SDL_AudioCallback():音频播放回调函数
- SDL_PauseAudio():开启或者暂停播放
- SDL_MixAudio():将音频数据写入播放缓冲区
- SDL_CloseAudio():关闭音频设备
- SDL_Quit():退出SDL系统

3 SDL开发环境构建

3.1 SDL源码下载与编译

本文介绍的是Linux下的环境搭建,主要通过源码进行编译获取SDL动态库。下载路径:https://www.libsdl.org/download-2.0.php

mkdir SDL
cd SDL
wget https://www.libsdl.org/release/SDL2-2.0.22.tar.gz
tar -xvf SDL2-2.0.22.tar.gz
cd SDL2-2.0.22
mkdir build
cmake ..
make -j4

整个编译过程很顺利,最终会得到编译文件如下图,这里会用到其中libSDL2-2.0.so动态库文件和根目录下的include文件夹。

3.2 工程构建

得到SDL2动态库后,接下来就是进行工程构建,创建一个SDL_project工程目录,包含该标准bin、build、include、lib、src目录文件。并且将依赖库拷贝到工程目录下。基本框架就构建好了。

mkdir SDL_project
cd SDL_project
mkdir bin  build include  lib  src
cp ../SDL2-2.0.22/build/libSDL2-2.0.so* ./lib/
cp ../SDL2-2.0.22/include/ ./src/ -rf
touch  CMakeLists.txt
touch  ./src/CMakeLists.txt

4 SDL简易视频播放器

这里截取部分源码进行描述;具体项目见yuv_player源码
第一初始化基础结构;

创建窗口:SDL_CreateWindow
创建渲染器:SDL_CreateRenderer
创建纹理:SDL_CreateTexture
创建刷新线程:SDL_CreateThread

第二是利用SDL_WaitEvent,响应不同SDL事件,包括视频显示流程:

读取yuv文件
将视频缓冲数据更新到纹理:SDL_UpdateTexture;
清理渲染器:SDL_RenderClear
纹理拷贝到渲染器:SDL_RenderCopy
显示视频画面:SDL_RenderPresent

//SDL_CreateWindow
    window = SDL_CreateWindow("Simple 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;
    
    // SDL_CreateRenderer
    renderer = SDL_CreateRenderer(window, -1, 0);
    // SDL_CreateTexture
    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;
    

    // open yuv_path
    video_fd = fopen(yuv_path, "rb");
    if( !video_fd )
    
        fprintf(stderr, "Failed to open yuv file:%s\\n", yuv_path);
        goto _FAIL;
    
    
    // SDL_CreateThread
    timer_thread = SDL_CreateThread(refresh_video_timer,
                                    NULL,
                                    NULL);

    while (1)
    
        // SDL_WaitEvent 
        SDL_WaitEvent(&event);

        if(event.type == SDL_REFRESH_EVENT) // update video frame
        
            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;
            
            // SDL_UpdateTexture
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            // keep window scale
            rect.x = 0;
            rect.y = 0;
            float w_ratio = win_width * 1.0 /video_width;
            float h_ratio = win_height * 1.0 /video_height;
            rect.w = video_width * w_ratio;
            rect.h = video_height * h_ratio;

            // SDL_RenderClear
            SDL_RenderClear(renderer);
            // SDL_RenderCopy
            SDL_RenderCopy(renderer, texture, NULL, &rect);
            // SDL_RenderPresent
            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) //退出事件
        
            g_thread_exit = 1;
        
        else if(event.type == SDL_QUIT_EVENT)
        
            break;
        
    

yuv播放器测试,播放一个yuv文件,可以正常播放画面,调节画面大小。

5 SDL简易音频播放器

SDL 音频播放相对视频播放器更为简单:

音频设备初始化并且读取音频数据:

打开音频设备:SDL_OpenAudio
开启音频设备播放
读取pcm文件,写入音频缓冲区

        if(SDL_Init(SDL_INIT_AUDIO))    
    
        fprintf(stderr, "Could not initialize SDL - %s\\n", SDL_GetError());
        return ret;
    

    //open pcmfile
    audio_fd = fopen(path, "rb");
    if(!audio_fd)
    
        fprintf(stderr, "Failed to open pcm file!\\n");
        goto _FAIL;
    

    g_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE);

    // SDL_AudioSpec
    spec.freq = 44100;          
    spec.format = AUDIO_S16SYS; 
    spec.channels = 2;          
    spec.silence = 0;
    spec.samples = 1024;       
    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)
    
        // read pcm
        read_buffer_len = fread(g_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);
        g_audio_end = g_audio_buf + read_buffer_len;    
        g_audio_pos = g_audio_buf;  
        //the main thread wait for a moment
        while(g_audio_pos < g_audio_end)
        
            SDL_Delay(10);  
        
    
    printf("play PCM finish\\n");
    // SDL_CloseAudio
    SDL_CloseAudio();

回调函数将音频数据写入播放缓冲区:

void mix_audio_pcm(void *udata, Uint8 *stream, int len)

    SDL_memset(stream, 0, len);

    if(g_audio_pos >= g_audio_end) 
    
        return;
    

    int remain_buffer_len = g_audio_end - g_audio_pos;
    len = (len < remain_buffer_len) ? len : remain_buffer_len;
    SDL_MixAudio(stream, g_audio_pos, len, SDL_MIX_MAXVOLUME/8);
    printf("len = %d\\n", len);
    g_audio_pos += len;  

ffmpeg实战教程用SDL播放YUV,并结合ffmpeg实现简易播放器

我们先实现用SDL播放YUV数据

先来了解一下基本概念

SDL基本函数:
[初始化]
* SDL_Init(): 初始化SDL。
* SDL_CreateWindow(): 创建窗口(Window)。
* SDL_CreateRenderer(): 基于窗口创建渲染器(Render)。
* SDL_CreateTexture(): 创建纹理(Texture)。

[循环渲染数据]
* SDL_UpdateTexture(): 设置纹理的数据。
* SDL_RenderCopy(): 纹理复制给渲染器。
* SDL_RenderPresent(): 显示。

C中的fseek函数
int fseek( FILE *stream, long offset, int origin );
第一个参数stream为文件指针
第二个参数offset为偏移量,整数表示正向偏移,负数表示负向偏移
第三个参数origin设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、 SEEK_END 或 SEEK_SET
SEEK_SET: 文件开头
SEEK_CUR: 当前位置
SEEK_END: 文件结尾
其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2.
简言之:
fseek(fp,100L,0);把fp指针移动到离文件开头100字节处;
fseek(fp,100L,1);把fp指针移动到离文件当前位置100字节处;
fseek(fp,100L,2);把fp指针退回到离文件结尾100字节处。

下面看一下运行效果:
这里写图片描述

下面看源代码 注释很详细



#include <stdio.h>

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

const int bpp=12;

int screen_w=500,screen_h=500;
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)

#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); //发送事件 即handler
        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)) {  //初始化SDL系统
        printf( "Could not initialize SDL - %s\\n", SDL_GetError()); 
        return -1;
    } 

    SDL_Window *screen; 
    //SDL 2.0  创建窗口(Window)。
    screen = SDL_CreateWindow("WS 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;
    }
    //创建基于窗口创建渲染器(Render)
    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;  
    //创建纹理(Texture)
    SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer,pixformat, SDL_TEXTUREACCESS_STREAMING,pixel_w,pixel_h);

    FILE *fp=NULL;
    //用我们上篇博客解码出来的yuv文件
    fp=fopen("output.yuv","rb+");

    if(fp==NULL){
        printf("cannot open this file\\n");
        return -1;
    }
    //window 视频显示框
    SDL_Rect sdlRect;  

    SDL_Thread *refresh_thread = SDL_CreateThread(refresh_video,NULL,NULL);//创建线程
    SDL_Event event;
    while(1){
        //Wait等待事件 即监听
        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){
            //If Resize SDL_WINDOWEVENT事件 可以拉伸播放器界面
            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();//退出系统
    return 0;
}

我们把上篇博客解码出来的YUV数据拷贝到项目跟目录,然后编译运行即可
配置环境以及编译运行部分看 此系列的实战(一)
http://blog.csdn.net/king1425/article/details/71160339

下面我们结合上篇博客 把MP4解码为YUV,然后在SDL上播放。这样一个简易版的视频播放器就出现了

我们把一个MP4文件拷贝到项目跟目录 命名为ws.mp4
然后运行源码文件
看效果图:
这里写图片描述


#include <stdio.h>

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL2/SDL.h>
#ifdef __cplusplus
};
#endif
#endif

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

#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2)

int thread_exit=0;
int thread_pause=0;

int sfp_refresh_thread(void *opaque){
    thread_exit=0;
    thread_pause=0;

    while (!thread_exit) {
        if(!thread_pause){
            SDL_Event event;
            event.type = SFM_REFRESH_EVENT;
            SDL_PushEvent(&event);
        }
        SDL_Delay(40);
    }
    thread_exit=0;
    thread_pause=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;
    SDL_Thread *video_tid;
    SDL_Event event;

    struct SwsContext *img_convert_ctx;

    char filepath[]="ws.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(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

    //Output Info-----------------------------
    printf("---------------- File Information ---------------\\n");
    av_dump_format(pFormatCtx,0,filepath,0);
    printf("-------------------------------------------------\\n");

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


    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | 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("WS ffmpeg player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        500, 500,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;
    sdlRect.h=screen_h;

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

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

    for (;;) {
        //Wait
        SDL_WaitEvent(&event);
        if(event.type==SFM_REFRESH_EVENT){
            //------------------------------
            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, &sdlRect, &sdlRect );  
                        SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL);  
                        SDL_RenderPresent( sdlRenderer );  
                        //SDL End-----------------------
                    }
                }
                av_free_packet(packet);
            }else{
                //Exit Thread
                thread_exit=1;
            }
        }else if(event.type==SDL_KEYDOWN){
            //Pause
            if(event.key.keysym.sym==SDLK_SPACE)
                thread_pause=!thread_pause;
        }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;
}

以上是关于SDL播放器实战的主要内容,如果未能解决你的问题,请参考以下文章

最简单的基于FFMPEG+SDL的视频播放器 ver2 (採用SDL2.0)

基于ffmpeg+SDL视频播放器制作任务概述

音视频连载-007基础学习篇-SDL 播放 PCM 音频文件(上)

(SDL)一个简单的SDL视频文件播放程序

golang调用sdl2,播放yuv视频

SDL2播放FFmpeg解压的视频