最近增加了对Magnification API捕获桌面的支持,记录一下过程和其中遇到的问题。
参考资料
Magnification API overview
Magnification API sample
webrtc screen_capturer_win_magnifier.cc
Structured Exception Handling (C/C++)
前言
我又不得不吐槽一下了,微软你做新API的时候,有考虑过开发人员的感受吗,不修BUG就不修呗,整个DXGI WGC,性能是有了,功能啥也不管,但凡你每一套API都兼顾基本要求,谁还研究各种老技术。
正文
The original release of the Magnification API is supported on Windows Vista and later operating systems. On Windows 8 and later, the API supports additional features, including full-screen magnification and setting the visibility of the magnified system cursor. For more information, see What’s New.
Support for the Magnification API is provided by Magnification.dll. To compile your application, include Magnification.h and link to Magnification.lib.
Note
The Magnification API is not supported under WOW64; that is, a 32-bit magnifier application will not run correctly on 64-bit Windows.
根据微软描述,Magnification API仅仅在Vista之后才支持,所以我们依然像之前的DXGI一样,采用动态加载Magnification.dll的方式用以判断是否支持。
typedef BOOL(WINAPI *MagImageScalingCallback)(
HWND hwnd, void *srcdata, MAGIMAGEHEADER srcheader, void *destdata,
MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty);
typedef BOOL(WINAPI *MagInitializeFunc)(void);
typedef BOOL(WINAPI *MagUninitializeFunc)(void);
typedef BOOL(WINAPI *MagSetWindowSourceFunc)(HWND hwnd, RECT rect);
typedef BOOL(WINAPI *MagSetWindowFilterListFunc)(HWND hwnd,
DWORD dwFilterMode,
int count, HWND *pHWND);
typedef BOOL(WINAPI *MagSetImageScalingCallbackFunc)(
HWND hwnd, MagImageScalingCallback callback);
_mag_lib_handle = load_system_library("Magnification.dll");
if (!_mag_lib_handle)
return false;
// Initialize Magnification API function pointers.
_mag_initialize_func = reinterpret_cast<MagInitializeFunc>(
GetProcAddress(_mag_lib_handle, "MagInitialize"));
_mag_uninitialize_func = reinterpret_cast<MagUninitializeFunc>(
GetProcAddress(_mag_lib_handle, "MagUninitialize"));
_mag_set_window_source_func = reinterpret_cast<MagSetWindowSourceFunc>(
GetProcAddress(_mag_lib_handle, "MagSetWindowSource"));
_mag_set_window_filter_list_func =
reinterpret_cast<MagSetWindowFilterListFunc>(
GetProcAddress(_mag_lib_handle, "MagSetWindowFilterList"));
_mag_set_image_scaling_callback_func =
reinterpret_cast<MagSetImageScalingCallbackFunc>(
GetProcAddress(_mag_lib_handle, "MagSetImageScalingCallback"));
BOOL result = _mag_initialize_func();
if (!result) {
al_info("Failed to initialize ScreenCapturerWinMagnifier: error from "
"MagInitialize %ld",
GetLastError());
return false;
}
The API supports two types of magnifiers, the full-screen magnifier and the magnifier control. The full-screen magnifier magnifies the content of the entire screen, while the magnifier control magnifies the content of a particular area of the screen and displays the content in a window. For both magnifiers, images and text are magnified, and both enable you to control the amount of magnification. You can also apply color effects to the magnified screen content, making it easier to see for people who have color deficiencies or need colors that have more or less contrast.
什么意思呢,一共有两种放大镜,按字面理解一种是全屏放大镜,一种是区域放大镜,前者放大全屏,后者呢不仅可以放大指定区域还可以将放大后的内容显示在指定的窗口中。而且无论哪一种放大镜,都可以按照你设置的倍数放大所有的内容,并且可以设置特定的颜色滤镜以帮助有颜色识别障碍人士,这也是放大器功能设计的初衷。
综上所述,我们将使用magnifier control方式来实现抓取屏幕。
HMODULE hInstance = nullptr;
result =
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<char *>(&DefWindowProc), &hInstance);
if (!result) {
_mag_uninitialize_func();
al_info("Failed to initialize ScreenCapturerWinMagnifier: "
"error from GetModulehandleExA %ld",
GetLastError());
return false;
}
此处建议阅读 Creating the Magnifier Control,大致流程就是描述了一些条件比如窗口样式要WS_EX_LAYERED,还有设置窗口透明度属性用以透传鼠标,设置MS_SHOWMAGNIFIEDCURSOR 用以绘制鼠标之类的。但是,我们使用Magnification是为了捕获图像,而不是为了将桌面放大后展示出来,所以我们仅仅保留了WS_EX_LAYERED属性,并且在创建完所需的窗口之后将窗口立即隐藏。
// kMagnifierWindowClass has to be "Magnifier" according to the Magnification
// API. The other strings can be anything.
static wchar_t kMagnifierHostClass[] = L"ScreenCapturerWinMagnifierHost";
static wchar_t kHostWindowName[] = L"MagnifierHost";
static wchar_t kMagnifierWindowClass[] = L"Magnifier";
static wchar_t kMagnifierWindowName[] = L"MagnifierWindow";
// Register the host window class. See the MSDN documentation of the
// Magnification API for more infomation.
WNDCLASSEXW wcex = {};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = &DefWindowProc;
wcex.hInstance = hInstance;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.lpszClassName = kMagnifierHostClass;
// Ignore the error which may happen when the class is already registered.
RegisterClassExW(&wcex);
// Create the host window.
_host_window =
CreateWindowExW(WS_EX_LAYERED, kMagnifierHostClass, kHostWindowName, 0, 0,
0, 0, 0, nullptr, nullptr, hInstance, nullptr);
if (!_host_window) {
_mag_uninitialize_func();
al_info("Failed to initialize ScreenCapturerWinMagnifier: "
"error from creating host window %ld",
GetLastError());
return false;
}
// Create the magnifier control.
_magnifier_window = CreateWindowW(kMagnifierWindowClass, kMagnifierWindowName,
WS_CHILD | WS_VISIBLE, 0, 0, 0, 0,
_host_window, nullptr, hInstance, nullptr);
if (!_magnifier_window) {
_mag_uninitialize_func();
al_info("Failed to initialize ScreenCapturerWinMagnifier: "
"error from creating magnifier window %ld",
GetLastError());
return false;
}
隐藏窗口
// Hide the host window.
ShowWindow(_host_window, SW_HIDE);
Initializing the Control 中有讲到要为放大器设置缩放比,但不设置也无妨
// Description:
// Sets the magnification factor for a magnifier control.
// Parameters:
// hwndMag - Handle of the magnifier control.
// magFactor - New magnification factor.
//
BOOL SetMagnificationFactor(HWND hwndMag, float magFactor)
{
MAGTRANSFORM matrix;
memset(&matrix, 0, sizeof(matrix));
matrix.v[0][0] = magFactor;
matrix.v[1][1] = magFactor;
matrix.v[2][2] = 1.0f;
return MagSetWindowTransform(hwndMag, &matrix);
}
和其他capture不同的是我们不通过定时器或线程循环去主动拷贝图像数据,而是利用 MagSetImageScalingCallback
Sets the callback function for external image filtering and scaling.
BOOL MagSetImageScalingCallback(
[in] HWND hwnd,
[in] MagImageScalingCallback callback
);
顾名思义这个回调是用来为图像做额外的滤镜或缩放的,巧的是 MagImageScalingCallback 提供了图像数据和相关参数。
BOOL Magimagescalingcallback(
[in] HWND hwnd,
[in] void *srcdata,
[in] MAGIMAGEHEADER srcheader,
[out] void *destdata,
[in] MAGIMAGEHEADER destheader,
[in] RECT unclipped,
[in] RECT clipped,
[in] HRGN dirty
)
定义并注册回调函数
void record_desktop_mag::on_mag_data(void *data, const MAGIMAGEHEADER &header) {
// do something
}
BOOL __stdcall record_desktop_mag::on_mag_scaling_callback(
HWND hwnd, void *srcdata, MAGIMAGEHEADER srcheader, void *destdata,
MAGIMAGEHEADER destheader, RECT unclipped, RECT clipped, HRGN dirty) {
record_desktop_mag *owner =
reinterpret_cast<record_desktop_mag *>(TlsGetValue(GetTlsIndex()));
TlsSetValue(GetTlsIndex(), nullptr);
owner->on_mag_data(srcdata, srcheader);
return TRUE;
}
// Set the scaling callback to receive captured image.
result = _mag_set_image_scaling_callback_func(
_magnifier_window, &record_desktop_mag::on_mag_scaling_callback);
if (!result) {
_mag_uninitialize_func();
al_info("Failed to initialize ScreenCapturerWinMagnifier: "
"error from MagSetImageScalingCallback %ld",
GetLastError());
return false;
}
关于 TlsGetValue/GetTlsIndex(Using Thread Local Storage),是用于同进程间的线程间同步数据的。
在我们的采集线程中,以一个固定的帧率通过 MagSetWindowSource 来更新采集区域,这也是整个Magnification中最核心的函数了吧,通过调用MagSetWindowSource可以触发MagImageScalingCallback 回调函数,而且经过测试会在MagSetWindowSource调用返回前触发。
int record_desktop_mag::do_mag_record() {
if (!_magnifier_initialized) {
al_error("Magnifier initialization failed.");
return AE_NEED_INIT;
}
auto capture_image = [&](const RECORD_DESKTOP_RECT &rect) {
// Set the magnifier control to cover the captured rect. The content of the
// magnifier control will be the captured image.
BOOL result =
SetWindowPos(_magnifier_window, NULL, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top, 0);
if (!result) {
al_error("Failed to call SetWindowPos: %ld. Rect = {%d, %d, %d, %d}",
GetLastError(), rect.left, rect.top, rect.right, rect.bottom);
return false;
}
_magnifier_capture_succeeded = false;
RECT native_rect = {rect.left, rect.top, rect.right, rect.bottom};
TlsSetValue(GetTlsIndex(), this);
// on_mag_data will be called via on_mag_scaling_callback and fill in the
// frame before _mag_set_window_source_func returns.
DWORD exception = 0;
result =
seh_mag_set_window_source(_magnifier_window, native_rect, exception);
if (!result) {
al_error("Failed to call MagSetWindowSource: %ld Exception: %ld. Rect = {%d, %d, %d, %d}",
GetLastError(), exception, rect.left, rect.top, rect.right,
rect.bottom);
return false;
}
return _magnifier_capture_succeeded;
};
// capture_image may fail in some situations, e.g. windows8 metro mode. So
// defer to the fallback capturer if magnifier capturer did not work.
if (!capture_image(_rect)) {
al_error("Magnifier capturer failed to capture a frame.");
return AE_ERROR;
}
return AE_NO;
}
其中需要注意的一点是 seh_mag_set_window_source
seh_mag_set_window_source(_magnifier_window, native_rect, exception);
bool record_desktop_mag::seh_mag_set_window_source(HWND hwnd, RECT rect,
DWORD &exception) {
if (!_mag_set_window_source_func)
return false;
__try {
return _mag_set_window_source_func(hwnd, rect);
} __except (EXCEPTION_EXECUTE_HANDLER) {
exception = ::GetExceptionCode();
return false;
}
return false;
}
为什么要用try catch包一层呢,经过大家的诸多测试,MagSetWindowSource会在很多种情况下抛出异常,比如捕获副屏、副屏大小大于主屏幕等等。。。总之微软没说什么时候会崩,但是大概率会崩溃。于是我们用 Structured Exception Handling 方法对异常进行捕获并返回一般错误,从而避免程序崩溃。
完整代码还在老地方
screen-recorder