目录
1、桌面共享的概念
2、实现高清共享视频画面的需求调研
3、功能实现
作者:dvlinker
欢迎关注,专注于C++开发、软件异常分析技术等。
本文将讲述如何通过HOOK窗口去拦截窗口消息,去实现视频会议中桌面共享图像的实时拖动。
开发工具:Visual Studio 2010
开发语言:C++
桌面图像共享是视频会议软件的一个基本需求,所有的视会议软件都实现了这一基本功能。不同厂商实现桌面图像共享的原理基本是一样的,都是桌面共享发送端实时不间断地截取桌面图像,以每秒若干帧的速率编出视频码流,然后发送给平台,平台再将经过编码的桌面共享图像发送给除发送端之外的每个与会方,这样其他人都能看到发送方的桌面内容了。不同的是,不同厂商再不同细节上的处理是有所差异的。在接收到发送端转过来的视频图像效果上,不同厂商的软件会有较大的差别,主要体现在视频图像的清晰度上。
经过调研发现,有若干厂商,为了满足不同用户的需求,在桌面共享的接受端支持桌面共享视频图像的缩放,既能将桌面共享图像铺满整个视频窗口,也能按照发送方的采集的图像大小的100%显示,以保证图像的清晰度,桌面共享接受方支持多种对桌面共享图像的缩放选项。
以发送方采集的1080P的共享图像为例,如果接受方在没有将视频显示窗口最大化或全屏时,可能显示桌面共享图像的窗口的尺寸,小于1080P,如果要看共享图像的全貌,则选择铺满整个窗口的显示模式,这样就会对共享图像进行缩小,图像就会变得模糊。
如果想要共享图像变清晰,则可以选择100%的显示比例,保证图像就是发送方的清晰度。如果接受方想对区域进行放大查看,则可以选择200%和300%的显示比例。
当显示视频的窗口小于图像的尺寸时,就有一部分的图像不在可见区域,就需要用户通过鼠标拖动屏幕,将不可见的区域拖到可见的窗口范围内,这样就能看到之前看不到的那部分区域图像了。
我们的软件一直存在接收桌面共享图像不清晰的问题,始终被客户诟病,领导下决心要解决这一难题。经过对友商产品的调研,我们确定了相关的需求:
1)要支持共享图像的缩放;
2)当播放视频图像的窗口尺寸小于共享图像的分辨率尺寸时,要支持通过鼠标拖动,将不可见的区域显示出来;
3)要做到实时的拖动,客户一拖动鼠标就要立即响应,不能有明显的延迟。
一般情况下,实时感知鼠标的拖动,应该是由UI层去做的,但有个问题。UI层到底层的音视频编解码层之间经过了好几层,函数调用时异步的,如果由UI层感知到用户鼠标拖动的坐标信息发给音视频编解码层,会有延时,并且拖动鼠标的过程中要频繁地调用接口给编解码层传递位置坐标信息,这样频繁的调用函数会有一定的开销,会拖慢程序的运行效率。
所以后来决定,UI层将显示共享图像的窗口句柄给编解码层(本来即使没有这个功能,也要设置给编解码层,他们负责将解码后的视频图像绘制到窗口上的),然后由编解码层去HOOK这个窗口,直接去拦截这个窗口的鼠标拖动消息,直接计算鼠标移动的位置,这样既解决了UI层传递过来的数据不实时到达编解码层的问题,也解决了频繁调用函数的开销问题,真可谓是一举多得啊!
接下来由我们UI层研究HOOK窗口、拦截窗口消息的方法,然后编写出demo,给底层的编解码层参考。毕竟我们应用层做UI的相关事务比较多,比他们要有经验一点,所以由我们给出具体的代码方案。
经过一番研究后,写出相关的demo程序,先是调用SetWindowHookEx,将目标窗口的消息都回传到一个指定的接口中,在该接口中拦截WM_LBUTTONDOWN鼠标左键按下、WM_LBUTTONUP鼠标左键弹起、WM_MOUSEMOVE鼠标移动等消息。
一次完整的拖动操作,从鼠标按下开始拖动,一直按着鼠标左键拖动鼠标,鼠标左键松开就完成了此次拖动操作。
所以要关注WM_LBUTTONDOWN、WM_LBUTTONUP和WM_MOUSEMOVE等消息。
对于系统API接口SetWindowHookEx,可以参看微软MSDN上的官方说明:
HHOOK SetWindowsHookExA(
[in] int idHook,
[in] HOOKPROC lpfn,
[in] HINSTANCE hmod,
[in] DWORD dwThreadId
);
给第一个参数传入WH_GETMESSAGE,即安装一个钩子来将目标线程中消息队列中的消息先发到指定的HOOKPROC钩子处理函数(传入的第二个参数)中,在该接口中进行消息拦截,出号处理后,再调用CallNextHookEx将消息转交给原来的消息队列去处理。相关的代码如下所示:
BOOL32 CDShowDraw::HookWinodws()
{
if (g_hHook != NULL)
{
return TRUE;
}
MPrintf("[enter] hook status --- m_bStaticSwitchOfHook=%d, m_bInstSwitchOfHook=%d, g_hHook=0x%p\n",
m_bStaticSwitchOfHook, m_bInstSwitchOfHook, g_hHook);
RECT rc = {0};
if (false == GetWindowRect(g_hDrawWnd, &rc))
{
MError("[quit] GetWindowRect() failed. Window handle is invalid. g_hDrawWnd=0x%p\n", g_hDrawWnd);
return FALSE;
}
else
{
MPrintf("g_hDrawWnd=0x%p : (%d,%d)-(%d,%d) %dx%d\n", g_hDrawWnd, rc.left, rc.top, rc.right, rc.bottom, rc.right - rc.left, rc.bottom - rc.top);
}
// 获得窗口所属进程ID和线程ID
DWORD dwProcessId = 0;
DWORD dwThreadId = GetWindowThreadProcessId(g_hDrawWnd, &dwProcessId);
DWORD dwThreadId2 = GetCurrentThreadId();
MPrintf("window ProcessId: %d, window ThreadId: %d, current ThreadId: %d\n", dwProcessId, dwThreadId, dwThreadId2);
// 这段代码只能写在dll工程中
// 安装一个钩子子程,在系统将消息发送到目标窗口过程之前监视消息;
// WH_GETMESSAGE:可过滤和修改post消息;CallWndProc:处理函数
// 输入的线程ID 必须是 创建窗口的线程ID,在TL中就是UI主线程的ID
g_hHook = SetWindowsHookEx( WH_GETMESSAGE, CallWndProc, 0, dwThreadId );
if (g_hHook == NULL)
{
DWORD dwErrId = GetLastError();
MError("[quit] SetWindowsHookEx() failed. err=%d:%s\n", dwErrId, strerror(dwErrId));
return FALSE;
}
MPrintf("[quit] ==========> Hook window success. g_hDrawWnd=0x%p\n", g_hDrawWnd);
return TRUE;
}
LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
if (g_nDecLogon == 9)
{
MPrintf("------------CallWndProc\n");
}
//DWORD dwThreadId2 = GetCurrentThreadId(); //查看实际处理线程 是不是 UI主线程
MSG* pWpsStruct =(MSG*)lParam;
switch ( pWpsStruct->message )
{
case WM_LBUTTONDOWN:
if ( pWpsStruct->hwnd == g_hDrawWnd ) // 判断响应事件的窗口句柄
{
if ( pWpsStruct->wParam == MK_LBUTTON) // 鼠标左键按下
{
g_bStartMove = true;
g_tCursorPoint.x = LOWORD(pWpsStruct->lParam);
g_tCursorPoint.y = HIWORD(pWpsStruct->lParam);
if (g_nDecLogon == 9)
{
MPrintf("------------鼠标左键按下: client coordinate (%d,%d)\n", g_tCursorPoint.x, g_tCursorPoint.y);
}
}
}
break;
case WM_LBUTTONUP: //鼠标左键松开
{
g_bStartMove = false;
g_bTracking = false;
g_tCursorPoint.x = LOWORD(pWpsStruct->lParam);
g_tCursorPoint.y = HIWORD(pWpsStruct->lParam);
if (g_nDecLogon == 9)
{
MPrintf("------------鼠标左键松开: client coordinate (%d,%d)\n", g_tCursorPoint.x, g_tCursorPoint.y);
}
}
break;
case WM_MOUSEMOVE: // 鼠标抓取移动
if ( g_bStartMove )
{
if (!g_bTracking)
{
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = g_hDrawWnd;
tme.dwFlags = TME_LEAVE;
tme.dwHoverTime = 1;
g_bTracking = TrackMouseEvent(&tme);
}
g_tCursorPoint.x = LOWORD(pWpsStruct->lParam);
g_tCursorPoint.y = HIWORD(pWpsStruct->lParam);
if (g_nDecLogon == 9)
{
MPrintf("------------鼠标抓取移动: g_bTracking=%d, cursor client coordinate (%d,%d)\n", g_bTracking, g_tCursorPoint.x, g_tCursorPoint.y);
}
}
break;
case WM_MOUSELEAVE:
{
POINT point = {0};
RECT rcDlg = {0};
GetCursorPos(&point);
GetClientRect(g_hDrawWnd, &rcDlg);
ClientToScreen(g_hDrawWnd, (LPPOINT)&rcDlg);
ClientToScreen(g_hDrawWnd, ((LPPOINT)&rcDlg) + 1);
if ( !::PtInRect( &rcDlg, point ) )
{
// 拖动时,鼠标已离开双流画面,
int a = 0;
}
g_bStartMove = false;
g_bTracking = false;
g_tCursorPoint.x = LOWORD(pWpsStruct->lParam);
g_tCursorPoint.y = HIWORD(pWpsStruct->lParam);
if (g_nDecLogon == 9)
{
MPrintf("------------鼠标离开窗口: g_bTracking=%d, cursor client coordinate (%d,%d)\n", g_bTracking, g_tCursorPoint.x, g_tCursorPoint.y);
}
}
break;
default:
break;
}
return CallNextHookEx( g_hHook, code, wParam, lParam );
}