FFMPEG录屏(16)--- MAG(Magnification)捕获桌面

news2024/11/25 18:47:24

最近增加了对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的方式用以判断是否支持。

  1. 定义Magnification相关函数类型
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);
  1. 加载动态库并获取函数地址
_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"));

  1. 初始化Magnification
BOOL result = _mag_initialize_func();
if (!result) {
  al_info("Failed to initialize ScreenCapturerWinMagnifier: error from "
          "MagInitialize %ld",
          GetLastError());
  return false;
}
  1. 选择Magnifier

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方式来实现抓取屏幕。

  1. 获取当前进程句柄以注册用以显示放大内容的窗口类
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;
}
  1. 注册窗口类并创建

此处建议阅读 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);
  1. 是否需要设置缩放比?

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(&amp;matrix, 0, sizeof(matrix));
    matrix.v[0][0] = magFactor;
    matrix.v[1][1] = magFactor;
    matrix.v[2][2] = 1.0f;

    return MagSetWindowTransform(hwndMag, &amp;matrix);  
}
  1. 设置放大器回调函数

和其他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),是用于同进程间的线程间同步数据的。

  1. 更新采集区域

在我们的采集线程中,以一个固定的帧率通过 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/534498.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaWeb】-- HTTP、Tomcat、Servlet

文章目录 HTTP1.简介2.请求数据格式2.2.1 格式介绍 3.响应数据格式3.1 格式介绍3.2 响应状态码 Tomcat1.简介1.1 什么是Web服务器 2.基本使用2.1 安装2.2 启动2.3 关闭 3.Maven创建Web项目4.IDEA使用Tomcat4.1集成本地Tomcat4.2 Tomcat Maven插件 Servlet1.简介2.快速入门3.执行…

okhttp篇3:RealCall

Call Call一般代表一个已经准备好的Request&#xff0c;Request的包装类&#xff0c;可执行&#xff0c;它一般有两个主要的方法&#xff1a; execute(立即执行&#xff0c;并阻塞线程&#xff0c;直到Response返回)enqueue(将Request放入队列&#xff0c;等待线程池调度执行…

spring源码学习_01 本地环境搭建

参考网上各种资源&#xff0c;终于把spring源码运行起来了&#xff1b;步骤总结如下&#xff1a; spring版本&#xff1a; 5.2.x 本地系统mac idea 2019.3.2 下载地址&#xff1a;https://www.jetbrains.com/idea/download/other.html jdk 11 下载地址&#xff1a;https://repo…

【设计模式二十三剑】✨编写更高质量代码的秘诀✨

文章目录 ✨✨简述&#x1f386;【万能之门】简单工厂模式&#xff08;Simple Factory&#xff09;&#x1f387;【制造之剑】工厂方法模式&#xff08;Factory Method&#xff09;&#x1f387;【生产之剑】抽象工厂模式&#xff08;Abstract Factory&#xff09;&#x1f387…

C语言基础知识:函数的调用

函数的定义 一般来说&#xff0c;执行源程序就是执行主函数main&#xff0c;其他函数只能被主函数所调用&#xff0c;而其他函数之间也可以相互调用。 1.标准库函数&#xff1a; 分为&#xff1a;I/O函数&#xff0c;字符串&#xff0c;字符处理函数&#xff0c;数学函数&am…

Go语言的函数和defer用法

目录 函数的基本用法 函数中的变长参数 递归函数(recursion) 函数是“一等公民” 函数中defer的用法 defer的底层原理 使用 defer 跟踪函数的执行过程 defer的注意事项 &#xff08;1&#xff09;Go语言内置的函数中哪些可以作为deferred 函数 &#xff08;2&#xf…

面试code(1)—— 排序算法

算法动画 从小到大排序 1 冒泡排序 被动的将最大值送到最右边 1、比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。 2、对每一对相邻元素作同样的工作&#xff0c;从开始第一对到结尾的最后一对。这步做完后&#xff0c;最后的元素会是最大的数。 3、针对…

Redis修炼 (17. redis主从集群的数据同步原理 全量同步/增量同步)

数据同步 在一个集群之中 数据同步是很重要的。 redis的集群有多种。 我们这次主要说 主从集群。 全量同步 既然是主从集群 那么肯定是 1个master节点 多个从节点。redis的集群中的第一次同步 我们叫 全量同步&#xff1a; 为什么要强调第一次&#xff1f; 因为如果你是第一…

独家策略大放送:最高年化150%的策略,谁不感兴趣?(含免费版)

上一节我们在沪深300中回测了550中均线交叉策略,有朋友想看看这些策略在沪深300以外的中小市场表现如何,同时大家都非常好奇表现抢眼的老Q自研指标WMA_Q系列到底是怎么计算的。 于是老Q又选择了中证500和创业板指来验证下这些策略是否能有同样的表现(PART 1),顺便在今天的…

流水线中的握手信号笔记

1.《握手信号的打拍(一)》 解释了&#xff0c;为什么在流水线中&#xff0c;握手信号不能简单得加一级寄存器 业界关于流水线级握手信号的标准答案是 skid buffer&#xff0c;此外还有人提到了 Register slice 2.《握手信号的打拍(二)》 为什么简单加一级寄存器会握手失败 &…

抖音SEO系统源码开发搭建/MVC二次开发定制

首先&#xff0c;抖音SEO矩阵系统源码开发&#xff0c;如何做独立部署&#xff0c;首先我们需要深刻理解这个系统的开发逻辑是什么&#xff1f;开发的前言是在抖音平台做流量新增&#xff0c;现在抖音及各大主流短视频平台&#xff0c;流量新增&#xff0c;各大企业需要在短视频…

Redis BigKey

Redis BigKey 一 面试题引入二 MoreKey案例2.1 大批量往redis里面插入2000W测试数据key2.1.1 Linux Bash下执行&#xff0c;插入100W2.1.2 通过redis提供的管道 --pipe命令插入100W大批量数据 2.2 key *2.3 生产上如何限制keys*/flushdb/flushall等危险命令以防止误删误用&…

我们在操作自动化测如何实现用例设计实例

在编写用例之间&#xff0c;笔者再次强调几点编写自动化测试用例的原则&#xff1a; 1、一个脚本是一个完整的场景&#xff0c;从用户登陆操作到用户退出系统关闭浏览器。 2、一个脚本脚本只验证一个功能点&#xff0c;不要试图用户登陆系统后把所有的功能都进行验证再退出系统…

【结构体-位段】

位段 在结构体中&#xff0c;以位为单位的成员&#xff0c;咱们称之为位段(位域)。 struct packed_data{unsigned int a:2;unsigned int b:6;unsigned int c:4;unsigned int d:4;unsigned int i; } data;注意&#xff1a;不能对位段成员取地址。 #include<stdio.h>str…

5.名词复数、动词规则、代词、形容词、副词(不包含不规则)

目录 一、 名词、动词、代词、形容词、副词五种的规则变化。 &#xff08;1&#xff09;名词。 &#xff08;1.1&#xff09;名词复数变化。 &#xff08;1.2&#xff09;名词所有格。 &#xff08;2&#xff09; 动词变化规则。 &#xff08;3&#xff09;代词。 &…

C语言-double和float在内存中的存储方式

本文主要介绍double和float数据类型在C语言中的存储方式 文章目录 double和float存储方式介绍如何存储&#xff1f; double和float存储方式介绍 从存储结构和算法上来讲&#xff0c;double和float是一样的&#xff0c;不一样的地方仅仅是float是32位的&#xff0c;double是64位…

【CAN卡通信的下位机-STM32cubeIDE-hal库+STMF1xx和STMF4xx+数据发送和接收+轮询接收方式+基础样例(1)】

【CAN卡通信的下位机-STM32cubeIDE-hal库数据发送和接收轮询接收方式基础样例1】 1、概述2、实验环境3、自我总结与提升(1)道理学习了一堆&#xff0c;如何使用STM32进行can的收发的话&#xff0c;配置还是挺简单。(2)自己实现了can的收发后&#xff0c;要反过来&#xff0c;补…

shell编程——Here Document免交互与Expect(免交互,高效率)

shell编程——Here Document免交互与Expect&#xff08;免交互&#xff0c;高效率&#xff09; 一、Here Document免交互概述二、Here Document常规用法1、免交互方式实现对行数地统计2、通过read/tee命令接受输入并打印3、通过passwd给用户设置密码4、支持变量替换5、整体赋值…

K8S基础操作之命令篇

目录 第一章.陈述式资源管理 1.1陈述式资源管理方法 1.2.基本命令查看信息 1.3.K8S管理操作分为2大类 1.4.数据网络端口访问流程 第二章.基本信息查看 2.1.命令格式 2.2.命令 2.3.项目的生命周期 第三章.service 3.1.概述 3.2.service 的 type 类型 3.3 headless …

神级指标DMI魔改免费公开!在宽基指数上也可以收获40倍收益,每年都在创新高!

一、写在前头 今天,我们要讲的DMI实际上是一组指标,它由表示多空方向的PDI、MDI以及表示趋势强度的ADX、ADXR共四条线组成。在正式开讲之前,我们先聊几句近期的行情。 上周我们根据量化策略提示了一些板块的机会,其中有一些已经开始有所表现。比如今天涨幅前十的板块中,…