录屏只是壳,Recordly 把 Screen Studio 的自动缩放做成了光标遥测

发布于 · 3,790 字 · 约 9 分钟#Github 解读#Video原文链接
录屏只是壳,Recordly 把 Screen Studio 的自动缩放做成了光标遥测 封面图
  • Recordly fork 自已归档的 OpenScreen,七个月获 32K 星,录制、编辑、导出、字幕核心链路全部本地完成
  • 录制时以 33 毫秒间隔采样归一化光标遥测并捕获点击事件,存为 sidecar 结构化数据,为后续效果推导提供基础
  • 自动缩放是 530 行纯函数管线,经遥测清洗、三路候选生成、点击聚类产出建议区间,且永不覆盖手动缩放
  • 光标动画基于阻尼谐振弹簧解析解加速度驱动倾斜与运动模糊,导出走三条可回退路由并结构化记录决策原因
  • LICENSE 为三层混编文件,叠加商标与 UI 署名条款涉嫌违反 AGPLv3 第 7 条,且 85% 提交来自单一作者,单点风险明显

去年 10 月,GitHub 上冒出个叫 OpenScreen 的项目,描述栏写得很直白,Screen Studio 的开源替代,不要订阅,没有水印,商业用途免费。它攒到了 39,956 颗星。然后今年 6 月 17 日,这个仓库推送了最后一次 commit,归档了。

接棒的人在 3 月 12 日就已经入场。作者 webadderall 建了 Recordly,fork 自 OpenScreen,README 里自己承认超过 80% 的代码已经分叉。七个月,31,964 颗星,2,542 个 fork,上周刚发布 v1.4.0。

这俩项目的关系比 fork 一词要绕。作者在 README 的 Credits 里说,OpenScreen 的很多功能,比如 zoom 动画,其实是直接从 Recordly 的早期版本移植过去的,而 Recordly 又以 OpenScreen 的 fork 起家。代码在两个仓库之间来回流动过,谁欠谁的一笔烂账,最后以一个归档、一个接棒收场。

录屏工具我拆过不少,大多数的卖点停留在「能录」。Recordly 让我坐下来拆的原因不一样,它把 Screen Studio 那种「录完像剪过」的效果,从动效设计师的手艺活,变成了一套可以读源码的算法。

录制层,三套原生方案各管一个平台

Electron 在这个项目里只干协调的活,真正的捕获全靠原生代码。

macOS 这边最下本钱。electron/native/ScreenCaptureKitRecorder.swift 是一个完整的 Swift 录制器,实现了 SCStreamOutput 协议直连 ScreenCaptureKit,视频走 AVAssetWriter 的 pixel buffer adaptor 管道,目标帧率写死 60。它有几个细节值得单说。

光标隐藏不是靠后期擦除,是在捕获配置里传了 excludedProcessIds,从源头上不让真实光标进画面。系统音频和麦克风是两条独立的 AVAssetWriter 轨道,各自有单独的输出路径,混音留给编辑器决定。录制收尾时还有一套轮询机制,writerReadinessPollAttempts 设成 100 次乘 10 毫秒,专门等编码队列排空再补尾帧,防止最后半秒丢帧。

窗口列表、光标素材、光标位置监控,分别由 ScreenCaptureKitWindowList.swift、SystemCursorAssets.swift 和 NativeCursorMonitor.swift 三个独立的 Swift 工具承担。npm 依赖里还有一个 capturekit 包,v1.0.13,干的是同一件事,把 ScreenCaptureKit 包成 Node 能调的接口。

Windows 走 Windows Graphics Capture,音频交给原生 WASAPI。比较意外的是 electron/native/ 下面还躺着两个重型组件,nvidia-cuda-compositor 和 gpu-export-probe,看名字就知道是拿 CUDA 做合成与导出加速的,附带一个 run-mp4-pipeline.mjs 脚本。README 对这些只字未提,后面细说。

Linux 是待遇最差的一个,直接用 Electron 自带的 capture API,代价是隐藏不了真实光标。README 在 Limitations 一节老实承认了,如果你同时开着渲染光标,导出画面里会出现两个光标。系统音频还得指望 PipeWire。

坦白讲,三套方案投入这么不均衡,倒不是偏见,是 OS 能力就长这样。

把四层关系画成一张图,捕获、遥测、编辑、导出各自的位置一目了然。

Recordly 系统架构
Recordly 系统架构

图里最值得盯的是中间那两层,视频从上面进来,遥测从侧面进来,在编辑层汇合。

33 毫秒一次的光标采样

整个项目最值钱的设计,我认为是录制时顺手做的那份数据。

electron/ipc/cursor/telemetry.ts 定义了采样节奏,CURSOR_SAMPLE_INTERVAL_MS 是 33,也就是约 30 Hz 记一次光标位置。坐标不是像素值,是归一化到 0 到 1 的相对位置,这样换分辨率、换窗口大小都不影响回放。样本上限 MAX_CURSOR_SAMPLES 是 108,000,注释写着 1 hour @ 30 Hz,录一小时封顶。数据带版本号,CURSOR_TELEMETRY_VERSION 已经迭代到 2,存成视频旁边的 sidecar 文件,不塞进视频流里。

点击、双击、右键、中键这些交互事件,靠 uiohook-napi 这个全局键鼠钩子库捕获,electron/ipc/cursor/interaction.ts 里做了一堆防御性解析,连 uiohook 模块导出形状不对都有兜底。

你想想看这个决策的分量。普通录屏工具录完,手里只有一条视频流,后面想加任何效果都得对着像素猜。Recordly 录完,手里多了一份结构化数据,光标什么时候在哪、点了什么、拖了多远。自动缩放、光标替换、循环导出,全都可以从这份数据推导,不用碰视频本身。

自动缩放是怎么猜的

Screen Studio 招牌的 auto-zoom,在 Recordly 里是一个 530 行的纯函数模块,src/components/video-editor/timeline/zoomSuggestionUtils.ts,输入光标遥测,输出一组建议的缩放区间。我把算法链路完整读了一遍。

第一步是清洗。normalizeCursorTelemetry 会做一次很聪明的回填,看到 click 之后 160 毫秒以上、位移超过 1.5% 屏幕的拖拽,就把这段时间的光标类型改写成 text 或 closed-hand,水平位移超过垂直 1.8 倍判定为文本选择。这么一来渲染层能画出「拖拽时箭头变 I 型」的细节。

第二步找候选,分三路。

显式点击这一路最可信,uiohook 直接报告的 click、double-click、right-click、middle-click 事件。每个点击再往后看 2 秒的轨迹,classifyPostClickBehavior 函数给它分类。点击后净向下位移超过 3% 且垂直位移是水平的 1.5 倍以上,判定为 dropdown-open,你在展开菜单里挑选项。点击后最大位移不到 2%,判定为 text-field-click,你点进了输入框。水平拖拽超 3% 屏幕宽度,是 text-selection。不同类型有不同权重,double-click 1500,text-selection 1300,dropdown-open 1200,text-field-click 1100,普通点击 900,权重影响后面聚类时取哪个点做焦点。

驻留这一路是给没有点击事件的场景兜底的。detectZoomDwellCandidates 把相邻采样点位移小于 2% 视为停住,停住时长在 450 到 2600 毫秒之间算一个候选。停太短可能是手抖,停太长可能是走神了,窗口卡得刚刚好。

第三路最妙,拿两个短驻留拼双击。两次都不超过 900 毫秒的驻留,间隔 450 毫秒以内、空间距离 3.5% 以内,合成一个 double-click-like 候测,权重是两次驻留时长之和再加 500。

候选齐了之后 buildClickClusters 做聚类,间隔 2500 毫秒以内的点击并成一簇,簇的焦点取权重最高的那个点击,找不着就取质心。最后每个簇前后各垫 500 毫秒 padding 生成缩放区间,并且跳过所有与手动缩放区间重叠的建议,机器的建议永远不抢人的决定。

整条管线的分叉与过滤长这样。

自动缩放建议流程
自动缩放建议流程

注意中间那个菱形,dwell 和合成双击两路候选在 buildInteractionZoomSuggestions 入口处就被 filter 掉了,最终只有 uiohook 报告的显式点击能变成缩放建议,代码里那句注释写得很清楚,只信显式交互。

还有一个平台特判挺见功力。shouldAutoApplyFreshRecordingZoomsForSource 里,Windows 直接返回 true,其他平台要求源画面宽高比不低于 1.2 才自动应用缩放。注释解释了原因,Windows 的窗口捕获经常产生竖屏或近方形的源,而点击遥测本来就归一化到捕获窗口内,竖屏不该成为禁用自动缩放的理由。一个 boolean 后面是一整套跨平台差异的思考。

一根弹簧解决的手感

光标动画是这类工具最容易被低估的部分。Recordly 的实现在 videoPlayback/cursorRenderer.ts,1,714 行,底层是 PixiJS 加 pixi-filters 的 MotionBlurFilter。

平滑不是 lerp 插值,是正经物理。motionSmoothing.ts 实现了阻尼谐振弹簧的闭式解析解,注释里公式写得明明白白,F = −kx − cv,刚度 k,阻尼 c,质量 m,三种阻尼状态各有精确解。欠阻尼会过冲再回弹,临界阻尼最快收敛不振荡,过阻尼慢慢蹭过去。你在 Screen Studio 里看到的那个「跟手但不生硬」的光标,背后是选定阻尼比之后的解析积分,不是调参调出来的黑盒缓动。

sway 效果在 cursorSway.ts,光标移动时会朝运动方向倾斜,最大转角 π/18 也就是 10 度。倾斜量由速度驱动,参考速度 1,400 像素每秒,垂直方向位移权重 0.65,快了才歪,慢了不歪。加上 click bounce 的回弹、方向性 motion blur,还有从 macOS 系统里提取的原生光标素材 atlas,几层叠出来就是那个「贵」的感觉。

顺带一提,motionSmoothing.ts 的第一行不是 import,是一句注释,提醒你这段代码遵循 AGPL-3.0,用了要署名。版权意识从文件头抓起。

导出走三条路,坏一条换一条

src/lib/exporter/ 目录躺了 60 多个文件,我数了一下光 audio 处理就分了 encoder、processor、routing engine、timeline processor 好几层。核心的路由决策在 backendPolicy.ts,内部代号叫 Lightning。

三条路由。native-static-layout 是原生静态布局渲染,布局不变时直接走原生管线。breeze-stream 是流式导出。webcodecs 是浏览器 WebCodecs API 的软件兜底。macOS 和 Windows 的 auto 模式优先 breeze-stream,Linux 默认 webcodecs,用户手动指定则完全尊重。每条路由的决策带 status 和 reasons 字段,选了什么、拒了什么、为什么,全都结构化记录,这个可观测性做法值得抄。

有一个注释暴露了伤疤。WINDOWS_AUTO_STATIC_LAYOUT_FIRST_ENABLED 被写死为 false,注释说 Windows 的静态布局探测会让导出卡在 Preparing export 界面,v1.3.0 稳定期先禁用,回落到流式管线。功能没删,闸门拉了,这种「带着伤疤注释活着」的代码比假装无事发生的那种可信得多。

导出的场景合成与预览共用同一套逻辑,README 的 How It Works 一节强调了这一点,你预览看到的就是你导出的。NVIDIA CUDA 合成器是显式 opt-in 的,对应 useNvidiaCudaExportOptIn.ts。

README 没写的那些

读代码读出来的东西,比 README 给的多不少。

字幕系统在代码里已经相当完整。scripts/build-whisper-runtime.mjs 从 whisper.cpp v1.8.4 构建本地运行时,small 模型按需下载,electron/ipc/captions/whisper.ts 里是一套带进度事件、断点清理、原子重命名的下载器。渲染层的 captionLayout、captionOps、captionTimeline 模块配着一排测试文件,连 issue 区都有人在给字幕接自定义 LLM 供应商了。而 README 的 All Features 清单里,字幕一个字没有。

云服务这块也有存货。services/supabase/ 是一套完整的后端,migrations、Edge Functions、单测俱全,目前承担应用内登录和反馈收集,反馈配额做得很细,每账户每天 10 次提交、25 个文件、50 MB,失败也计数防刷。services/recordly-share/ 是一个 Cloudflare Worker 分享服务,THIRD_PARTY_NOTICES.md 里声明了它 fork 自 MIT 协议的 Voom 项目。

扩展系统是另一个 README 只给了一节的能力。recordly-extension.json 清单加权限门控的 host API,扩展可以注册 render hook 直接往渲染管线里画,也能贡献光标样式、壁纸、设备边框这类打包素材,配了一个线上的扩展市场。桌面应用做插件系统容易做成样子货,这套 manifest 校验、权限声明、截图轮播的完整度看着是当真在运营的。

要说外部依赖,核心链路录制、编辑、导出、字幕全部本地完成,云是可选项。唯一需要联网的是 whisper 模型首次下载和应用内更新检查。

协议里埋的私货

这里要泼冷水了,而且是亲自踩过之后泼的。

README 说 Recordly 遵循 AGPL 3.0,badge 也挂着 AGPL3.0。但 GitHub API 对这个仓库的协议识别返回的是 NOASSERTION。为什么,我打开 LICENSE.md 看了。

这份 252 行的文件是个三层结构。最上面是一段自撰的 QUICK SUMMARY,里面塞了两条 AGPLv3 原文里不存在的东西。其一,你不能用 Recordly 这个名字和品牌做自己的项目。其二,如果你用了 Recordly 的代码或衍生代码,必须在用户可见的 UI 和仓库里给 Recordly 署名。中间一层自称是 the full text of the AGPLv3,我拿官方文本逐条比对过,条款 0 到 17 一条不少,但版式被压得面目全非,标准文本的条款正文 619 行,这里挤成 187 行,段间空行全删,段落并成超长行。最底下还有一层 PART 2,MIT 协议的原始署名,来自上游 OpenScreen 的作者 Siddharth Vaddem。

问题不只是观感。AGPLv3 第 7 条明确禁止在许可证上叠加与原条款相抵触的额外限制,把商标条款和强制 UI 署名写进 LICENSE 文件并冠以 MUST,严格说这让整份协议的合法性处在灰色地带。商标保护本来该单独注册商标去解决,署名诉求 AGPL 自身的 copyleft 机制也会间接达成。想要开源社区的信任又想保留商业抓手,这种纠结可以理解,但混编文件的代价是下游法务没法干净地做合规判断。

维护集中度也值得摆在台面上。最近 200 个提交里 171 个来自 webadderall 本人,占 85.5%,第二名贡献者 19 个,剩下的人都是个位数。Git 历史里大量 codex/ 前缀的合并分支名,说明开发重度借助 AI 编码工具,一个人加 AI 干出了一个 60 多文件导出子系统的体量,这既是效率的证明,也是 bus factor 的警报。加上 OpenScreen 归档的前车之鉴,这个项目的单点风险是真实存在的。

一次采集,无限派生

拆完之后我一直想给它提炼个东西。

Recordly 真正的架构决策不是「录得清楚」,也不是「特效好看」,是它在录制那一刻就把一次性的屏幕活动变成了结构化数据。33 毫秒一次的光标轨迹、每个点击的类型判定、每次拖拽的方向和时长,这些数据一旦存在,自动缩放就是聚类问题,光标动画就是弹簧求解问题,循环导出就是把遥测首尾对齐的问题。编辑器消费的是数据,视频只是数据的渲染结果之一。

我把它叫遥测驱动编辑。同样思路可以搬去很多地方,录用户的键盘轨迹可以做输入回放分析,录 API 调用序列可以做故障复现,录编辑器撤销栈可以做协作冲突的测试语料。凡是「过程本身有价值,但大家只存了结果」的场景,都欠这么一份 sidecar 文件。

选型建议给个干脆的。你要在 macOS 上做产品演示视频,不想为 Screen Studio 付费,Recordly 现在的完成度完全够用,自动缩放的光标权重那套算法比多数同类走得远。Linux 用户要接受双光标和 PipeWire 依赖。想拿它做商业产品的二次开发,先让法务把那份三层混编的 LICENSE.md 读干净,附加条款不是 README 一句 AGPL 3.0 能概括的。

一个人,七个月,一份光标遥测,接下了一个归档项目的 32K 颗星。开源世界的接力棒,有时候就是这样交接的。

评论互动

© 2026 王若风的技术博客 · Powered by Astro