录屏原理与鼠标可视化

录屏原理与鼠标可视化 ------ 会话记录

整理日期:2026-08-04

主题:屏幕/声音捕获原理、DRM 限制、鼠标可视化、多屏适配

问答模型:deepseek-v4-flash


一、录屏软件的工作原理

录屏的本质是"在不打扰系统正常工作的情况下,拿到系统的最终画面和声音"。

1. 画面捕获(屏幕输出)

核心矛盾:画面已被系统**合成器(compositor)**画好,录屏软件要的是"拿一份副本"。效率高低取决于"副本是从 GPU 拿还是从 CPU 拿"。

方式 平台 特点
GDI / BitBlt Windows(老式) 走 CPU 截图,慢,有撕裂,基本是上古方案
Desktop Duplication API Windows 8+(主流) 底层走 GPU,对合成器最终纹理做复制,支持变化检测跳过,OBS 等主流方案
ScreenCaptureKit / CGDisplayStream macOS 通过 IOSurface 从 WindowServer 取帧,支持按窗口/应用捕获、HDR
X11 / Wayland Linux X11 用 XShm;Wayland 需通过 xdg-desktop-portal(PipeWire)申请权限

鼠标光标是单独处理的:不随桌面帧一起取,而是单独取形状和位置再叠加合成,这样即使帧率低光标也流畅。

2. 声音捕获(声音输出)

关键点:正常程序的声音发给声卡,录屏软件必须"在半路截胡"------在声音到达扬声器前拷贝一份。

平台 机制
Windows WASAPI loopback:订阅"所有程序混音后的输出流"
macOS AudioUnit Tap / process tap:钩住系统音频进程的输出流
Linux PulseAudio / PipeWire 的 monitor sink:每个输出设备有镜像源

拿到原始音频后还要做重采样(对齐采样率)、和麦克风混音、用时间戳与视频帧对齐(A/V 同步)。

3. 编码与封装

  1. 用高精度时钟(Windows QPC、macOS mach_absolute_time)给每帧打时间戳;
  2. 硬件编码器压缩:NVIDIA NVENC / AMD AMF / Intel QuickSync / macOS VideoToolbox;
  3. 把视频流 + 音频流按时间戳交错写进 MP4 / MKV / FLV。

4. 常见坑与应对

  • DRM 内容:捕获 API 拒绝或加水印;
  • 被遮挡的窗口:Desktop Duplication 只能拿整个屏幕,需要窗口级捕获 API(Windows Graphics Capture);
  • CPU 往返开销:现代方案尽量让数据留在 GPU,最后只在编码器走一遍。

二、遇到数字版权保护(DRM)时还能工作吗

遇到真正的 DRM 内容,录屏软件会直接失效------录出来是黑屏。 不是软件不够强,而是被硬件层 + 系统层一起掐断。

1. 传输保护(HDCP,管"信号线")

保护的是显卡 → 显示器/采集卡这条物理链路,双方握手协商密钥。

  • 采集卡声明不支持 HDCP → 源设备拒绝输出或只输出低分辨率;
  • "HDCP 剥离器"本质是欺骗源设备,但流媒体会检查输出设备等级,检测到不合规直接降级或拒播;
  • 主机/PC 自己录屏走第二层,不走 HDMI 线,HDCP 管不着。

2. 内容保护(PlayReady / Widevine / FairPlay,管"解码路径")

这才是 Netflix、B 站、浏览器看片的机制:

  1. 加密流在安全环境里解密(PlayReady / Widevine CDM / FairPlay),底层依赖 TEE 或 GPU 安全路径;
  2. 解密后的帧标记为"受保护表面",放进 GPU 专用显存区,合成器只拿到占位符
  3. 录屏 API 能拷到的本来就是占位符,拷出来自然是黑块。

为什么"再先进的采集方式都没用":问题不在采集方式,而在于解密和显示被锁在安全的硬件路径里,任何用户态捕获 API 都碰不到真像素。绕过 DRM 本身触犯反规避法律(DMCA 类条款),正规录屏软件绝不会做。

3. 物理漏洞(analog hole)

DRM 加密的是数字链路,但屏幕发出的光是任何加密都管不了的------用相机对着显示器拍理论上可行。但现代流媒体有分辨率限制 + 水印追踪,实际效果差且法律上违规,只作概念存在。


三、录屏软件如何改变/显示鼠标形状

结论:是浮层,但加在"合成到帧之前",不是加在屏幕上。

1. 鼠标光标不是屏幕画面的一部分

系统画光标用的是 hardware cursor(硬件光标层) :独立的小精灵位图,由 GPU 专用图层叠加,不写进桌面帧缓冲

所以捕获 API 默认拿到的帧里根本没有鼠标,而是把光标当"元数据"单独提供:

  • 当前位置(每帧)
  • 当前形状位图
  • 是否可见 / 有没有改变

2. 录屏软件"画自己的光标"

  1. 捕获不带光标的画面帧;
  2. 读取同刻光标位置 + 形状
  3. 在编码流水线里用自己的图叠加到这一帧上(高亮圆圈、放大镜、更大箭头、自定义图案)。

发生在 GPU 上的合成/编码阶段,浮层是加在每一帧的画面数据上,不是屏幕上弹窗口。

3. 位置为什么流畅

光标刷新率比帧率高得多(鼠标回报率 125Hz / 1000Hz)。

  • 帧率不够时每帧是"采样点",位置跳变;
  • 高级软件(OBS 光标滤镜)以远高于帧率的频率轮询光标位置,画最新位置;
  • 可画拖尾:记录最近几次采样位置画在同一帧上形成轨迹。

4. 两种改法对比

改法 机制 效果
录屏层浮层 捕获时隐藏真光标,画自己的图 只改视频,屏幕上看不到变化
改系统光标 SetSystemCursor 替换系统光标位图 屏幕实时生效,录的也是新的

四、Python 模拟鼠标点击的库

1. 常用库

  • PyAutoGUI(最常见)
  • pynput
  • mouse(boppreh 的库)

能模拟鼠标移动/点击/拖拽、键盘输入、截图识别颜色定位目标。

2. 底层原理

最终都调用系统事件注入接口:

平台 接口
Windows SendInput / mouse_event / SetCursorPos
macOS CGEventPost
Linux XTest

关键细节 :Windows 上这类事件会被打 LLMHF_INJECTED 标志标记"程序注入"。真实物理输入没有这个标志。游戏反作弊、银行控件会检查它------所以这类库在游戏/安全场景容易失效或触发风控,这是系统故意留下的指纹。

另外 pynput 和 mouse 还能监听/钩住 全局鼠标事件,用系统钩子实现(Windows SetWindowsHookEx / 全局事件监听),与注入是相反方向。


五、让鼠标点击位置在屏幕上显示出来

思想与录屏浮层一致但方向相反:录屏软件画进视频帧,这里要画在真实屏幕上

1. 两个独立部分

  • 监听点击pynput.Listener(底层是全局鼠标钩子 WH_MOUSE_LL),拿到坐标;
  • 显示点击:全屏、置顶、无边框、透明的"装饰窗口",在点击坐标处画波纹/圆圈并淡出。

2. 装饰窗口三个关键属性

  • -topmost:永远盖在所有窗口上面;
  • -transparentcolor(颜色键):某颜色当透明,只有画上去的圆圈可见;
  • 鼠标穿透:透明区域不接收鼠标消息,真实点击穿透到下面程序,否则自己挡住自己点的按钮。

3. 现成工具

  • Carnac / KeyCastOW:显示按键 + 鼠标点击
  • MouseClickShow:专门做点击波纹
  • OBS:录制时用插件(如 Mouse Highlight)把波纹加进视频

4. 最小实现(Tkinter + pynput)

python 复制代码
import queue
import tkinter as tk
from pynput import mouse

clicks = queue.Queue()

class Overlay:
    def __init__(self):
        self.root = tk.Tk()
        self.root.overrideredirect(True)                 # 无边框
        self.root.attributes('-topmost', True)           # 永远最前
        self.root.attributes('-transparentcolor', '#010101')  # 颜色键当透明(鼠标穿透)
        self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0")
        self.canvas = tk.Canvas(self.root, bg='#010101', highlightthickness=0)
        self.canvas.pack(fill='both', expand=True)
        self.ripples = []

    def tick(self):                                     # 30fps 动画循环
        while not clicks.empty():
            x, y = clicks.get()
            self.ripples.append({'x': x, 'y': y, 'r': 6, 'life': 30})
        self.canvas.delete('all')
        for rp in list(self.ripples):
            rp['r'] += 3
            rp['life'] -= 1
            if rp['life'] <= 0:
                self.ripples.remove(rp)
                continue
            a = int(rp['life'] / 30 * 255)
            self.canvas.create_oval(rp['x']-rp['r'], rp['y']-rp['r'],
                                    rp['x']+rp['r'], rp['y']+rp['r'],
                                    outline=f'#{a:02x}0000', width=3)
        self.root.after(33, self.tick)

    def run(self):
        self.root.after(33, self.tick)
        self.root.mainloop()

o = Overlay()
# 监听线程只往队列塞坐标,UI 在主线程安全地画(避免跨线程碰 tkinter)
mouse.Listener(on_click=lambda x, y, button, pressed: pressed and clicks.put((x, y))).start()
o.run()

5. 实现细节

  • 线程安全 :钩子回调跑后台线程,tkinter 不能在别的线程画,用队列 + after() 轮询,主线程统一处理;
  • 点击 vs 按下 :回调里 pressed 区分按下/抬起,一般只画按下,否则一次点击出两圈;
  • 拖尾/轨迹:多记历史点即可。

六、扩展屏幕(多显示器)时的适配

默认这套代码在扩展屏上会失灵------不是监听问题,是显示层问题。

1. 两个坑

  • 浮层窗口只盖了主屏winfo_screenwidth()/height() 只返回主显示器尺寸,+0+0 是主屏原点;
  • 坐标空间对不上 :pynput 返回的是虚拟桌面坐标 (所有显示器合起来的大画布),副屏在左侧时坐标是负数,可能落在浮层窗口外。

2. 修复思路

把浮层铺满"虚拟桌面",再平移坐标 。Windows 用 GetSystemMetrics 拿整个虚拟桌面矩形:

python 复制代码
user32 = ctypes.windll.user32
vx = user32.GetSystemMetrics(76)   # 虚拟桌面左边界(可能是负)
vy = user32.GetSystemMetrics(77)   # 虚拟桌面上边界
vw = user32.GetSystemMetrics(78)   # 虚拟桌面总宽
vh = user32.GetSystemMetrics(79)   # 虚拟桌面总高
  • 窗口铺满整个虚拟桌面(不能用 -fullscreen True,它只盖主屏);
  • 画圆圈时平移:画布坐标 = 事件坐标 - (vx, vy)

3. 必须一起处理的坑:DPI 缩放

副屏 125%/150% 而主屏 100% 时,pynput 和 Tkinter 的坐标可能是物理像素 vs 逻辑像素,位置系统性偏移。解决:进程启动时声明按显示器感知 DPI:

python 复制代码
ctypes.windll.shcore.SetProcessDpiAwareness(2)  # 必须在创建窗口前调用

4. 修正后的关键部分

python 复制代码
import ctypes, queue, tkinter as tk
from pynput import mouse

clicks = queue.Queue()
ctypes.windll.shcore.SetProcessDpiAwareness(2)
u = ctypes.windll.user32
vx, vy = u.GetSystemMetrics(76), u.GetSystemMetrics(77)
vw, vh = u.GetSystemMetrics(78), u.GetSystemMetrics(79)

class Overlay:
    def __init__(self):
        self.root = tk.Tk()
        self.root.overrideredirect(True)
        self.root.attributes('-topmost', True)
        self.root.attributes('-transparentcolor', '#010101')
        self.root.geometry(f"{vw}x{vh}+{vx}+{vy}")   # 覆盖整个虚拟桌面
        self.canvas = tk.Canvas(self.root, bg='#010101', highlightthickness=0)
        self.canvas.pack(fill='both', expand=True)
        self.ripples = []
        self.ox, self.oy = vx, vy

    def tick(self):
        while not clicks.empty():
            x, y = clicks.get()
            # 虚拟坐标 -> 窗口本地坐标
            self.ripples.append({'x': x - self.ox, 'y': y - self.oy, 'r': 6, 'life': 30})
        self.canvas.delete('all')
        for rp in list(self.ripples):
            rp['r'] += 3; rp['life'] -= 1
            if rp['life'] <= 0:
                self.ripples.remove(rp); continue
            a = int(rp['life'] / 30 * 255)
            self.canvas.create_oval(rp['x']-rp['r'], rp['y']-rp['r'],
                                    rp['x']+rp['r'], rp['y']+rp['r'],
                                    outline=f'#{a:02x}0000', width=3)
        self.root.after(33, self.tick)

o = Overlay()
mouse.Listener(on_click=lambda x, y, b, p: p and clicks.put((x, y))).start()
o.root.after(33, o.tick)
o.root.mainloop()

注意GetSystemMetrics 方案是 Windows 专用的。macOS 要拿全屏空间走别的 API,跨屏浮层更麻烦。极少数情况下单个跨屏透明窗口有显示 bug,兜底方案是用 EnumDisplayMonitors 给每块屏各开一个浮层窗口。

相关推荐
驱动小百科1 天前
笔记本电脑键盘按键失灵怎么修复 教你快速恢复键盘正常输入
计算机外设·电脑·笔记本电脑键盘失灵·笔记本键盘按键没反应·笔记本键盘部分按键失灵
蓝斯4972 天前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
AIex-YH4 天前
菌种工艺四讲(三):育种工具不是“越新越好“,是“越匹配越好“
制造·策略模式
起司喵喵4 天前
Flutter-MacOS桌面OS系统|flutter.+window_manager客户端OS模板
flutter·macos·策略模式
啊真真真4 天前
macOS下libnfc ..写卡失败问题及解决方案
macos·策略模式
带娃的IT创业者5 天前
LG 显示器通过 Windows Update “静默”安装软件:一场关于数字主权的无声博弈
windows·计算机外设·隐私安全·驱动程序·windows update·数字主权·lg显示器
集芯微电科技有限公司5 天前
16V/5A高效率COT模式同步降压转换器集成高低功率MOSFET
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
撩妹帝九歌5 天前
使用VHF框架实现一个虚拟HID键盘
linux·运维·计算机外设
Eric Woo X6 天前
macOS 旧版本无法启动 Steam ?一条命令恢复运行(2026年7月实测)
macos·策略模式