本文是在“Beginning SDL 2.0(4) YUV加载及渲染”(以下简称BS4)基础上做的功能完善,如果你对之间介绍的内容了解不多,麻烦先阅读之前的内容。
本文主要介绍如何完成一个基于MFC和SDL 2.0的YUV播放器,基本思路是使用Windows的WM_TIMER消息,定期刷新画面。(正规的播放器通常使用一个独立的线程用于做固定帧率的刷新,这里为了简单期间使用系统提供的定时器实现。)
工程创建
使用vs10创建mfc基于对话框的工程,2_sdl_yuv_player,配置好SDL包含路径,同时包含BS4中提供的YuvRender类。如果不想处理unicode字符,建议将工程属性的字符集设置为多字节编码。
并在主对话框中编辑出如下几个控件:一个Static用于YUV视频显示,一个播放按钮用于选择yuv路径,并开启播放,三个输入框分别用于输入视频宽、高及帧率。效果如下:
YuvRender类更新
由于BS4中的YuvRender是读取本地文件目录下的yuv图像,然后显示视频的,这里需要修改下,以支持动态的YUV画面渲染。
具体接口如下:
#pragma once #include "sdlvideorender.h" class YuvRender :public SDLVideoRender { public: YuvRender(void); ~YuvRender(void); // Init use parent impl //bool Init(HWND show_wnd, RECT show_rect); void Deinit(); // width x height resolution // data[] for Y\U\V, stride is linesize of each raw void Update(int width, int height, unsigned char *data[3], int stride[3]); bool Render(); private: bool CreateTexture(int width, int height); void FillTexture(unsigned char *data[3], int stride[3]); private: // texture size int m_in_width, m_in_height; SDL_Texture * m_show_texture; };
相比之前的版本这里最大的区别是Update函数不再是空实现,添加了CreateTexture函数,主要考虑我们事先是不知道需要创建Texture的分辨率。
这里Init函数功能,完全可以直接使用父类提供的实现。
下面是Deinit函数实现代码
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
void YuvRender::Deinit() { if (nullptr != m_show_texture) { SDL_DestroyTexture(m_show_texture); m_show_texture = NULL; } SDLVideoRender::Deinit(); }
Update函数会调用CreateTexture和FillTexture两个函数,用于创建和填充纹理,其实现代码如下:
bool YuvRender::CreateTexture(int width, int height) { if (m_in_height == height && m_in_width == width && nullptr != m_show_texture) { return true; } ASSERT(width > 0 && width < 10000); ASSERT(height > 0 && height < 10000); m_show_texture = SDL_CreateTexture(m_sdl_renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, width, height); if (nullptr != m_show_texture) { m_in_width = width; m_in_height = height; } return NULL != m_show_texture; } void YuvRender::FillTexture(unsigned char *data[3], int stride[3]) { void * pixel = NULL; int pitch = 0; if(0 == SDL_LockTexture(m_show_texture, NULL, &pixel, &pitch)) { // for Y int h = m_in_height; int w = m_in_width; unsigned char * dst = reinterpret_cast<unsigned char *>(pixel); unsigned char * src = data[0]; for (int i = 0; i < h; ++i) { memcpy(dst, src, w); dst += pitch; src += stride[0]; } h >>= 1; w >>= 1; pitch >>= 1; // for U for (int i = 0; i < h; ++i) { memcpy(dst, src, w); dst += pitch; src += stride[1]; } // for V for (int i = 0; i < h; ++i) { memcpy(dst, src, w); dst += pitch; src += stride[2]; } SDL_UnlockTexture(m_show_texture); } } // width x height resolution // data[] for Y\U\V, stride is linesize of each raw void YuvRender::Update(int width, int height, unsigned char *data[3], int stride[3]) { if (nullptr == m_show_texture) { CreateTexture(width, height); } if (nullptr != m_show_texture) { FillTexture(data, stride); } }
最后一个函数是Render,实现相对简单,直接将texture复制并提交到显存中。
bool YuvRender::Render() { if (NULL != m_show_texture) { SDL_RenderCopy(m_sdl_renderer, m_show_texture, NULL, &m_show_rect); SDL_RenderPresent(m_sdl_renderer); } return true; }
主程序中的修改
主要修改位于CMy2_sdl_yuv_playerDlg中,依次添加OnBnClickedButtonPlay、WM_TIMER、WM_DESTORY的消息处理函数,并添加三个输入框的关联变量,m_width、m_height、m_fps。同时定义m_yuv_render用于显示yuv数据。我们将需要的数据通过文件指针的形式保存,每次读取一帧YUV数据。
首先看一下OnBnClickedButtonPlay的功能,需要调用打开对话框,选择指定的yuv,分配资源,启动定时器,相关实现如下:
enum{ DFT_WIDTH = 720, DFT_HEIGHT = 576, DFT_FPS = 25, SHOW_TIMER_ID = WM_USER + 1, }; bool CMy2_sdl_yuv_playerDlg::InitRender(CString file_path) { UpdateData(TRUE); m_plane_size = (m_width * m_height) >> 2; m_frame_length = m_plane_size * 6; m_frame_data = new unsigned char[m_frame_length]; if (nullptr == m_frame_data) { return false; } m_plane_size <<= 2; m_in_file = nullptr; if (0 != fopen_s(&m_in_file, (LPCTSTR)file_path, "rb")) { CString strMsg; strMsg.Format("open failed! %s", file_path); AfxMessageBox(strMsg); return false; } CRect rect; CStatic * pStatic = (CStatic *)GetDlgItem(IDC_STATIC_VIDEO); pStatic->GetClientRect(&rect); // 因为SDL_DestoryWindow会调用ShowWindow使窗口隐藏 // 为了实现重复使用播放窗口的目的,这里直接将其显示出来 pStatic->ShowWindow(SW_SHOW); m_yuv_render.Init(pStatic->GetSafeHwnd(), rect); ASSERT(0 != m_fps); int interval = 1000 / m_fps; SetTimer(SHOW_TIMER_ID, interval, NULL); return true; } void CMy2_sdl_yuv_playerDlg::DeinitRender() { if (nullptr != m_in_file) { KillTimer(SHOW_TIMER_ID); fclose(m_in_file); m_in_file = nullptr; } m_yuv_render.Deinit(); if (nullptr != m_frame_data) { delete [] m_frame_data; m_frame_data = nullptr; } m_plane_size = 0; } void CMy2_sdl_yuv_playerDlg::OnBnClickedButtonPlay() { CString file_name = _T(""); CFileDialog fd(TRUE, NULL, file_name, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR, NULL, NULL); if (fd.DoModal() == IDOK) { DeinitRender(); InitRender(fd.GetPathName()); } }
注意这里额外调用了ShowWindow函数,你可以尝试下看看这个到底有什么功能。相关修改是参考SDL2.0的源码中SDL_DestroyWindow实现。
定时消息处理函数的基本功能是读取一帧yuv,渲染,如果文件到头,重置文件指针。
void CMy2_sdl_yuv_playerDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == SHOW_TIMER_ID && nullptr != m_in_file) { size_t read_size = fread(m_frame_data, 1, m_frame_length, m_in_file); if(read_size == m_frame_length) { unsigned char *src[3] = {NULL}; //Y、U、V数据首地址 src[0] = m_frame_data; src[1] = src[0] + m_plane_size; src[2] = src[1] + (m_plane_size>>2); int stride[3] = {m_width, m_width/2, m_width/2}; m_yuv_render.Update(m_width, m_height, src, stride); m_yuv_render.Render(); } else { // 循环播放 fseek(m_in_file, 0, SEEK_SET); } } CDialogEx::OnTimer(nIDEvent); }
WM_DESTROY函数主要做必要的退出处理,并清理SDL的资源。
void CMy2_sdl_yuv_playerDlg::OnDestroy() { CDialogEx::OnDestroy(); DeinitRender(); if (SDL_WasInit(0))SDL_Quit(); }
最终程序运行效果如下图:
总结
在BS4的基础上实现YUV播放器相对比较简单,整理这篇文章主要目的在于梳理SDL中视频渲染机制,同时提供尽可能直接的YUV渲染方法。
相关代码可以从我的git下载,url如下:https://git.oschina.net/Tocy/SampleCode.git,位于TocySDL2VisualTutorial目录下。