MFC的YUV播放器实现
本文记录使用MFC编写一个YUV播放器的过程,尽量实现播放器都有的常用功能。功能参考与网上的各个文章。
目前已支持功能:
1、播放/暂停
2、停止
3、倒放
4、循环播放
5、快进/快退
6、单帧步进/步退
7、滑动条显示进度,点击滑动条任意位置精确跳转到相应帧。
8、鼠标悬停在滑动条上时显示预览小窗口
9、鼠标悬停在按钮上有提示语
主界面如下图
动态演示如下图:
一、主要参考链接
https://latelee.blog.youkuaiyun.com/article/details/47832985
二、开发踩坑记录
目前发现基于此类方法制作的播放器在全屏或者放大窗口的情况下,程序会跑得很慢,估计是使用BMP绘图的像素点多了的原因。
1、Gdiplus 绘图前置条件
1 需要导入静态库和命名控件
//gdi+库
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
2 在窗口App头文件定义如下
ULONG_PTR m_gdiplusToken;
virtual int ExitInstance();
3 在初始化函数添加上代码
// CYUVPlayerApp 初始化
BOOL CVideoStartApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
GdiplusStartupInput gdiplusStartupInput;
//ULONG_PTR gdiplusToken;
GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
CWinApp::InitInstance();
AfxEnableControlContainer();
// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;
// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
CYUVPlayerDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
// 删除上面创建的 shell 管理器。
if (pShellManager != NULL)
{
delete pShellManager;
}
// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}
int CVideoStartApp::ExitInstance()
{
// TODO: Add your specialized code here and/or call the base class
GdiplusShutdown(m_gdiplusToken); // ??
return CWinApp::ExitInstance();
}
2、播放时点击滑竿能精准跳转
首先在OnHScroll加上代碼,然後重寫Slider类的OnLButtonDown函数
// 响应滚动条处理
void CVideoStartDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_nCurrentFrame = m_Slider1.GetPos();
if (nPos)
m_nCurrentFrame = nPos;
this->Read(m_nCurrentFrame);
this->Show();
CDialogEx::OnHScroll(nSBCode, nPos, pScrollBar);
}
MySlider.h
#pragma once
#include <afxcmn.h>
class MySlider :
public CSliderCtrl
{
DECLARE_DYNAMIC(MySlider)
public:
MySlider();
virtual ~MySlider();
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
};
MySlider.c
#include "pch.h"
#include "MySlider.h"
IMPLEMENT_DYNAMIC(MySlider, CSliderCtrl)
MySlider::MySlider(){}
MySlider::~MySlider(){}
BEGIN_MESSAGE_MAP(MySlider, CSliderCtrl)
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
void MySlider::OnLButtonDown(UINT nFlags, CPoint point)
{
#if 1
CRect rc, trc;
GetChannelRect(rc);
GetThumbRect(trc);
rc.InflateRect(0, (trc.Height() - rc.Height()) / 2);
if (!PtInRect(rc, point))
return;
LONG range = GetRangeMax();
LONG pos = point.x - rc.left - trc.Width() / 2;
LONG width = rc.Width() - trc.Width();
CSliderCtrl::SetPos(int(DOUBLE(pos) * range / width + 0.5));
CSliderCtrl::OnLButtonDown(nFlags, point);
#endif
}
3、鼠标悬停在滑竿上时显示预览小窗口
创建预览子窗口时,需要把外观中的Border属性设置为None,如下图
主窗口中:
// 首先添加 WM_SETCURSOR 消息
// 1 原理就是当鼠标进入Slider控件区域后,计算出鼠标当前所处的滑竿位置
// 2 然后计算出相应的帧数,在创建一个的小窗口,将图片以BMP的形式显示在上面
// 3 当进入滑竿区域就显示预览窗口,离开则隐藏预览窗口(无需关闭预览窗口)
// 4 根据鼠标位置实时移动预览小窗口
// 5 关键的几个函数如下 OnSetCursor,PreviewShow,m_pPreview->ShowPicture
BOOL CVideoStartDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (pWnd->GetDlgCtrlID() == IDC_SLIDER1)
{
CString str;
RECT rect, rt, crt;
CPoint point, point1;
float fNum = 0;
int posFrame = 0;
pWnd->GetWindowRect(&rect);// 获取的坐标为屏幕坐标系中的坐标
this->ScreenToClient(&rect);// 转换为客户区坐标
int nWhide = rect.right - rect.left;
GetCursorPos(&point);
point1 = point;
ScreenToClient(&point);
if (point.x > 0 && point.x < rect.right)
{
fNum = (float)point.x / nWhide;
posFrame = (int)(fNum * m_nTotalFrame);
}
str.Format(L"x=%d, y=%d, w=%d. f=%.4f, fps=%d", point.x, point.y, nWhide, fNum, posFrame);
m_pPreview->GetWindowRect(&crt);
if (m_pPreview)
{
// 显示预览小窗口
PreviewShow(posFrame);
m_pPreview->SetWindowPos(NULL, point1.x + 5, point1.y - 5 - crt.bottom + crt.top, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
m_pPreview->ShowWindow(SW_SHOW);
}
IDC_PREVIEW;
AfxGetMainWnd()->SetWindowText(str);
}
else
{
if (m_pPreview)
{
m_pPreview->ShowWindow(SW_HIDE);
}
AfxGetMainWnd()->SetWindowText(L"xxxx");
}
return CDialogEx::OnSetCursor(pWnd, nHitTest, message);
}
// 显示小预览图
void CVideoStartDlg::PreviewShow(int nCurrentFrame)
{
// 防止越界
if (nCurrentFrame < 1 || nCurrentFrame > m_nTotalFrame)
return;
m_cFile.Seek(m_nYuvSize * (nCurrentFrame - 1), SEEK_SET);
m_cFile.Read(m_pbYuvData, m_nYuvSize);
// 先添加BMP头
m_bmHeader2.bfType = 'MB';
m_bmHeader2.bfSize = m_nBmpSize;// + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
m_bmHeader2.bfReserved1 = 0;
m_bmHeader2.bfReserved2 = 0;
m_bmHeader2.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
m_bmInfo2.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
m_bmInfo2.bmiHeader.biWidth = m_nWidth;
// note BMP图像是倒过来的
m_bmInfo2.bmiHeader.biHeight = -m_nHeight;
m_bmInfo2.bmiHeader.biPlanes = 1;
m_bmInfo2.bmiHeader.biBitCount = 24;
m_bmInfo2.bmiHeader.biCompression = BI_RGB;
m_bmInfo2.bmiHeader.biSizeImage = m_nBmpSize - 54;
m_bmInfo2.bmiHeader.biXPelsPerMeter = 0;
m_bmInfo2.bmiHeader.biYPelsPerMeter = 0;
m_bmInfo2.bmiHeader.biClrUsed = 0;
m_bmInfo2.bmiHeader.biClrImportant = 0;
memcpy(m_pbBmpData2, &m_bmHeader2, sizeof(BITMAPFILEHEADER));
memcpy(m_pbBmpData2 + sizeof(BITMAPFILEHEADER), &m_bmInfo2, sizeof(BITMAPINFOHEADER));
// 再转换格式
if (m_nYuvFormat == FMT_RGB24 || m_nYuvFormat == FMT_BGR24)
{
memcpy(m_pbBmpData2 + 54, (unsigned char *)m_pbYuvData, m_nBmpSize - 54);
}
else
{
yuv_to_rgb24((YUV_TYPE)m_nYuvFormat, (unsigned char *)m_pbYuvData, (unsigned char *)m_pbBmpData2 + 54, m_nWidth, m_nHeight);
}
// BMP是BGR格式的,要转换 rgb->bgr
if (m_nYuvFormat != FMT_BGR24)
{
swaprgb((BYTE*)(m_pbBmpData2 + 54), m_nBmpSize - 54);
}
m_pPreview->ShowPicture((BYTE*)m_pbBmpData2, m_nBmpSize);
}
预览子窗口中:
inline void RenderBitmap(CWnd *pWnd, Bitmap* pbmp)
{
RECT rect;
pWnd->GetClientRect(&rect);
Graphics grf(pWnd->m_hWnd);
if (grf.GetLastStatus() == Ok)
{
Rect rc(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
grf.DrawImage(pbmp, rc);
}
}
// CPreview 消息处理程序
void CPreview::ShowPicture(BYTE* pbData, int iSize)
{
// 显示
CWnd* pWnd = GetDlgItem(IDC_PREVIEW); // IDC_VIDEO:picture contral 控件ID
IStream* pPicture = NULL;
CreateStreamOnHGlobal(NULL, TRUE, &pPicture);
if (pPicture != NULL)
{
pPicture->Write((BYTE *)pbData, iSize, NULL);
LARGE_INTEGER liTemp = { 0 };
pPicture->Seek(liTemp, STREAM_SEEK_SET, NULL);
Bitmap TempBmp(pPicture);
RenderBitmap(pWnd, &TempBmp);
}
if (pPicture != NULL)
{
pPicture->Release();
pPicture = NULL;
}
}