该笔记为学习雷神(雷霄骅)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中进行显示。但以这种方式实现的多屏可能存在问题:多个不同视频帧率不同的时候无法使用同一频率进行刷新。