Beginning SDL 2.0(5) 基于MFC和SDL的YuvPlayer

本文是在“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函数实现代码

void YuvRender::Deinit()
{
    if (nullptr != m_show_texture)
    {
        SDL_DestroyTexture(m_show_texture);
        m_show_texture = NULL;
    }

    SDLVideoRender::Deinit();
}
View Code

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目录下。

posted @ 2015-08-24 22:49  Tocy  阅读(1199)  评论(0编辑  收藏  举报