语言: 简体中文 · 对应版本 Recordly v1.4.0(main 分支,HEAD 4992686)
Recordly 开发文档(中文)
本文面向要改代码的人,讲清楚三件事:这套东西怎么跑起来、代码分成哪几块、改了之后怎么验证。
面向用户的功能介绍请看 README.zh-CN.md。
目录
1. 技术栈
Recordly 是 Electron 桌面应用,不是 Web 应用。这一点决定了后面所有分层:主进程管窗口/文件/原生能力,渲染进程管 UI,preload 做唯一的安全桥。
| 层 | 选型 |
|---|---|
| 桌面壳 | Electron 43.1 |
| 构建 | Vite 5.1 + vite-plugin-electron 0.28 |
| 渲染 | React 18.2 + TypeScript 5.2(strict: true) |
| 样式 | Tailwind CSS 3.4 + Tailwind Animate |
| 基础组件 | Radix UI 全家桶(shadcn 风格,见 src/components/ui/) |
| 图标 | @phosphor-icons/react / react-icons |
| 动效 | motion 12 |
| 画布 / 合成 | pixi.js 8.14 + pixi-filters |
| 视频容器 / 解复用 | mediabunny、mp4box、web-demuxer、@fix-webm-duration/fix |
| 转码 / 封装 | ffmpeg-static、ffprobe-static |
| 录制 | capturekit、uiohook-napi(全局键鼠钩子) |
| 时间线交互 | dnd-timeline、react-resizable-panels、react-rnd |
| Lint / 格式 | Biome 2.3(不是 ESLint + Prettier) |
| 测试 | Vitest 3.2(environment: node) |
| 打包 | electron-builder 26(NSIS / dmg / zip / AppImage) |
| 自动更新 | electron-updater(GitHub Releases) |
| 许可证 | AGPL-3.0(见 LICENSE.md) |
规模参考:src/ + electron/ 共 488 个 .ts/.tsx(其中 86 个 .tsx),134 个测试文件。
2. 环境准备
通用
- Node.js 22(CI 固定用 22;本机 22.x 即可)
- npm(仓库带
package-lock.json,请用npm ci或npm install,不要混用 pnpm/yarn,否则postinstall的原生流程可能对不上) - git
各平台额外依赖
| 平台 | 需要装什么 | 用途 |
|---|---|---|
| Windows | Visual Studio 2022(或 Build Tools),勾选 C++ 工作负载 + CMake | 编译 WGC 采集、DXGI 回退、光标监控、GPU 导出等 C++ 辅助程序 |
| macOS | Xcode Command Line Tools(xcode-select --install) | swiftc 编译 ScreenCaptureKit 辅助程序 |
| Linux | build-essential cmake libx11-dev libxt-dev libxtst-dev libxkbfile-dev libxi-dev libxrandr-dev libxinerama-dev | 编译原生辅助程序 + uiohook-napi 的 X11 依赖 |
Windows 不装 CMake 也能跑起来。 详见 5.3 字幕运行时。
系统版本下限
| 平台 | 最低版本 | 原因 |
|---|---|---|
| macOS | 14.0 (Sonoma) | ScreenCaptureKit 系统音频 / 麦克风能力 |
| Windows | 10 20H1(Build 19041) | 原生 WGC 采集与最佳光标隐藏行为 |
| Linux | 任意现代发行版 | 走 Electron 采集 API;系统音频一般要 PipeWire |
3. 快速开始
git clone https://github.com/webadderallorg/Recordly.git recordly
cd recordly
npm install # 注意:会触发原生编译,首次较慢
npm run devnpm install 到底做了什么
不要以为它只是拉包。package.json 里的 postinstall 会依次执行两步,任何一步失败都会让 npm install 以退出码 1 结束:
npm run rebuild:native→electron-rebuild --force --only uiohook-napi
把全局键鼠钩子模块按 Electron 的 ABI 重编译。跳过这步,运行时uiohook-napi会报 ABI 不匹配。npm run build:platform-native-helpers→ 依次构建:build:whisper-runtime(字幕引擎,见下)build:native-helpers(macOS Swift 辅助程序,非 mac 直接跳过)build:windows-capture、build:windows-gpu-export、build:nvidia-cuda-compositor、build:cursor-monitor(Windows / CUDA)
只想改前端、不想编译原生代码
CI 就是这么干的,本地同样适用:
npm ci --ignore-scripts # 跳过 postinstall
node node_modules/ffmpeg-static/install.js
npx electron-builder install-app-deps
npm run dev代价是没有原生采集后端,会退回浏览器采集 / ffmpeg 路径。
开发模式的一个关键点
npm run dev 跑的是 vite --config vite.config.ts。当 VITE_DEV_SERVER_URL 存在时:
electron/appPaths.ts会把userData切到Recordly-dev,和正式版数据完全隔离;- 不申请单实例锁(
shouldEnforceSingleInstanceLock = !IS_DEV),方便同时开多个实例调试。
所以你的开发数据在 %APPDATA%\Recordly-dev,删掉它等于恢复出厂设置。
4. 目录结构
Recordly/
├── electron/ # 主进程(Node 侧)
│ ├── main.ts # 入口:窗口、菜单、托盘、权限、GPU 开关
│ ├── preload.ts # contextBridge,暴露 window.electronAPI
│ ├── electron-env.d.ts # ★ electronAPI 的类型定义(改 API 必须同步这里)
│ ├── windows.ts # 各类窗口的创建与生命周期
│ ├── appPaths.ts # userData / recordings 路径(含 dev 隔离)
│ ├── mediaServer.ts # 本地媒体 HTTP 服务(给 <video> 喂文件)
│ ├── rendererServer.ts # 打包后渲染资源服务
│ ├── updater.ts # electron-updater 封装
│ ├── navigationPolicy.ts # 外链策略
│ ├── permissionPolicy.ts # 媒体 / 屏幕采集权限策略
│ ├── gpuSwitches.ts # 平台相关 GPU 命令行开关
│ ├── cursorHider.ts # 录制时隐藏系统光标
│ ├── ipc/ # ★ IPC 层
│ │ ├── handlers.ts # 注册总入口
│ │ ├── constants.ts # 频道名与常量
│ │ ├── types.ts
│ │ ├── register/ # 按功能拆分的注册模块
│ │ ├── captions/ # whisper 字幕(解析、分段、静音检测)
│ │ ├── cursor/ # 光标遥测
│ │ ├── export/ # 导出流、原生视频导出
│ │ ├── ffmpeg/ # ffmpeg 二进制探测与滤镜
│ │ ├── project/ # 项目文件的原子保存
│ │ ├── recording/ # 录制(windows / mac / ffmpeg 三条实现)
│ │ └── settings/ # 设置持久化
│ └── native/ # ★ 原生源码 + 预编译产物
│ ├── *.swift # macOS 辅助程序
│ ├── wgc-capture/ # Windows Graphics Capture(C++)
│ ├── windows-capture/ # DXGI 回退(C++)
│ ├── cursor-monitor/ # 光标监控(C++)
│ ├── gpu-export-probe/ # GPU 导出能力探测(C++)
│ ├── nvidia-cuda-compositor/ # CUDA 合成(.cu)
│ └── bin/<平台-架构>/ # 预编译产物,随包发布
├── src/ # 渲染进程(React)
│ ├── main.tsx / App.tsx # 按 windowType 分发窗口
│ ├── components/
│ │ ├── launch/ # 录制端:HUD、源选择、倒计时、更新提示
│ │ ├── video-editor/ # 编辑器:时间线、导出、预览、项目、预设
│ │ ├── ui/ # Radix 封装的基础组件
│ │ ├── announcements/ # 公告
│ │ └── countdown/
│ ├── lib/
│ │ ├── exporter/ # ★ 导出引擎(帧渲染、编码、音频、封装)
│ │ ├── announcements.ts / appSettings.ts / wallpapers.ts ...
│ │ └── geometry/
│ ├── contexts/ # I18nContext / ThemeContext / ShortcutsContext
│ ├── hooks/ # useScreenRecorder、设备枚举、音量表
│ ├── i18n/ # 10 语言 × 7 命名空间
│ ├── utils/ # 比例换算、平台判断
│ └── assets/ # 光标素材(macOS / Tahoe / Windows 11 三套)
├── scripts/ # 构建与发布脚本(.mjs)
├── public/ # 静态资源:应用图标、壁纸、wasm
├── build/ # macOS entitlements
├── branding/ # 品牌源素材
├── docs/ # 公告说明 + README 用媒体
├── icons/ # 打包图标(icns / ico / png)
└── .github/workflows/ # CI 与发布流水线三个入口别搞混:
| 入口 | 文件 | 说明 |
|---|---|---|
| 主进程 | electron/main.ts | 构建后是 dist-electron/main.cjs |
| preload | electron/preload.ts | 构建后同目录 |
| 渲染进程 | index.html → src/main.tsx → src/App.tsx | 构建后是 dist/ |
@ 别名指向 src/,在 vite.config.ts、vitest.config.ts、tsconfig.json 三处都配了,新增构建入口记得同步。
5. 构建流水线
5.1 dev 与 build 的区别
| 命令 | 实际动作 |
|---|---|
npm run dev | vite --config vite.config.ts。vite-plugin-electron 顺带编译主进程与 preload 并拉起 Electron |
npm run build | build:platform-native-helpers → tsc → vite build → normalize:electron-main-cjs → smoke:electron-main-cjs → electron-builder |
npm run build:win / :mac / :linux | 同上,最后换成 electron-builder --win / --mac / --linux |
产物:dist/(渲染)、dist-electron/(主进程 + preload)、release/(安装包,electron-builder 的 directories.output)。
5.2 主进程为什么强制 CJS
vite.config.ts 里有三个不显眼但很关键的设计:
electronMainCjsOutputPlugin强制lib.formats = ["cjs"]、入口文件名.cjs。
原因写在注释里:Vite 的mergeConfig会把lib.formats和插件的 ESM 默认值拼接在一起,导致输出格式不确定。inlineDynamicImports: true:主进程打成单文件,避免 Electron 加载动态 chunk 时的路径问题。electronMainCjsGuardPlugin:closeBundle阶段调用scripts/smoke-electron-main-cjs.mjs做冒烟检查,失败就让整个vite build抛错。
也就是说构建产物格式一旦退化,构建会立刻中断而不是留到运行时报错。
排查构建问题先看 npm run smoke:electron-main-cjs,它是这套机制的真相来源。5.3 字幕(whisper)运行时
scripts/build-whisper-runtime.mjs 的逻辑值得单独讲,因为它决定了「Windows 上要不要装 CMake」:
- 优先走预编译:Windows x64 会下载 whisper.cpp 官方
whisper-bin-x64.zip(版本v1.8.4,带 SHA-256 校验),解压后取出whisper-cli.exe与相关 DLL,写入electron/native/bin/win32-x64/。 - 下载失败才回退源码编译:需要 CMake + VS。Windows 上生成器按 VS 2026 → VS 2022 → VS 2019 顺序自动回退(见
scripts/windows-cmake-generators.mjs)。 - 缺 CMake 的失败策略分场合:
postinstall/CI=true/WHISPER_RUNTIME_ALLOW_MISSING=1→ 只警告,退出 0;- 直接调用
npm run build、build:win等 → 硬报错,避免打出一个静默缺失字幕能力的安装包。
- 已 staged 会跳过重复构建,判断依据是同目录下的
whisper-runtime.json清单(版本 + 架构 + 二进制存在)。
5.4 生产构建的两个坑
terser会删掉console.log/console.debug(drop_console: true、drop_debugger: true)。
所以「本地有日志、打包装没有」是预期行为,别为此加console.error。manualChunks只分了三块:react-vendor、video-processing(mediabunny / mp4box / fix-webm-duration)。往src/lib/exporter/里引重库时注意别把主 chunk 撑爆(chunkSizeWarningLimit已放宽到 1000 kB)。
5.5 打包配置要点(electron-builder.json5)
appId: dev.recordly.app,asar: true。asarUnpack:ffmpeg-static、ffprobe-static、uiohook-napi、electron/native/bin/**必须解包,因为要以真实文件路径去exec。extraResources:public/wallpapers→assets/wallpapers(壁纸不进 asar)。- 剥离:
!dist/wallpapers/**、whisper-bench*、whisper-quantize*、whisper-server*、whisper-vad-speech-segments*等调试用二进制不进包。 - macOS:
hardenedRuntime: true+ 两个 entitlements 文件;extendInfo里声明了相机 / 麦克风 / 音频采集用途说明,并注册了.recordly文档类型。 - Windows:NSIS,可选安装目录、建桌面与开始菜单快捷方式。
- publish:GitHub Releases(
webadderall/Recordly,tag 前缀v),publishAutoUpdate: true。
6. 多窗口体系
这是 Recordly 最容易让人迷路的地方:一个渲染入口,多个窗口身份。
src/App.tsx 从 URL 查询参数读 windowType,然后分发组件:
windowType | 组件 | 窗口特点 |
|---|---|---|
hud-overlay | components/launch/HudWindow | 无边框透明浮层,主入口(启动即它)。开启 HTML 级鼠标穿透,hudOverlaySetIgnoreMouse(true) |
source-selector | components/launch/SourceSelector | 透明背景;挑选屏幕 / 窗口 |
countdown | components/countdown/CountdownOverlay | 录制前倒计时浮层 |
update-toast | components/launch/UpdateToastWindow | 透明、overflow: visible,仅 macOS 使用 |
editor | components/video-editor/EditorWindow | 主编辑器窗口 |
主进程侧的窗口创建集中在 electron/windows.ts:createHudOverlayWindow / createSourceSelectorWindow / createEditorWindow / showUpdateToastWindow。
判断一个窗口是不是编辑器,用的是 URL 里有没有 windowType=editor:
function isEditorWindow(window: BrowserWindow) {
return window.webContents.getURL().includes("windowType=editor");
}所以改窗口 URL 参数会直接破坏主进程逻辑,别随手改。
平台差异(都是踩过的坑,注释里写得很细)
- 托盘只在 Linux 启用(
shouldUseTray())。macOS / Windows 走 Dock / 任务栏。 - Win32 HUD 的鼠标穿透脆弱:开启穿透(Win11+)后,对透明 HUD 调
show/moveTop/focus会永久破坏setIgnoreMouseEvents转发,窗口变成彻底点不透。所以托盘唤醒时用showInactive()+ 重新断言鼠标状态。Win10 不支持穿透,HUD 常驻可交互,反而可以正常show/restore。 - Linux Wayland 焦点问题:合成器忽略
focus()。做法是干脆销毁并重建 HUD 窗口(创建路径能拿到焦点),注释里点名提到了 XDG activation token 的缺失。 - Linux Wayland 屏幕共享只弹一次 portal:
setDisplayMediaRequestHandler里,若源 id 是哨兵screen:linux-portal或为空,跳过desktopCapturer.getSources(),直接返回合成 id——因为getSources()本身就会触发一次 portal。
关闭编辑器的行为
Windows 上关闭编辑器不是关窗口,而是隐藏它并重新显示 HUD(closeEditorWindowToHud),目的是让录制收尾(例如摄像头文件 finalize)继续在渲染进程里跑完,同时应用回到「随时可录」状态。有未保存改动时弹三选一:保存关闭 / 放弃关闭 / 取消。
7. IPC 架构与安全策略
7.1 桥的形态
electron/preload.ts通过contextBridge暴露window.electronAPI。- 类型定义集中在
electron/electron-env.d.ts的interface Window里,约 700 行,覆盖录制、导出、项目、字幕、更新、权限等全部能力。 - 新增或修改 API 必须同步这个
.d.ts,否则渲染进程拿不到类型(并且 Biome 对该文件单独放宽了noNamespace和noExplicitAny,说明它是有意保持集中的)。
7.2 主进程侧的注册方式
- 总入口:
electron/ipc/handlers.ts(同时负责cleanupNativeVideoExportSessions、cleanupAllExportStreams、killWindowsCaptureProcess等清理)。 - 按功能拆到
electron/ipc/register/:announcements、assets、captions、export、exportCaptionSidecars、permissions、project、recording、settings、sourceMapping、sources。 - 频道名用 kebab-case,例如
set-has-unsaved-changes、hud-overlay-close、request-save-before-close。 - 有些频道直接写在
main.ts里(更新相关的install-downloaded-update、check-for-app-updates等),因为它们要拿到mainWindow这类窗口引用。
加一个 IPC 的标准动作: 在对应 register/*.ts 里注册 → 在 preload.ts 暴露 → 在 electron-env.d.ts 补类型 → 渲染层调用。
7.3 安全策略(三道闸)
- 导航策略(
navigationPolicy.ts):web-contents-created时统一加固,外链一律shell.openExternal,不在应用内导航。 - 媒体权限策略(
permissionPolicy.ts+main.ts里的setPermissionCheckHandler/setPermissionRequestHandler):
只有可信采集窗口(即 HUD 主框架)且来源在可信 URL 列表内才放行 media 权限。可信列表 =dist/index.html的文件 URL + dev server URL + 打包后的本地渲染服务地址。 - 设备权限直接全拒:
setDevicePermissionHandler(() => false)—— Recordly 不使用 WebHID / Web Serial / WebUSB,默认不授予任何设备。
另外 setDisplayMediaRequestHandler 接管了 getDisplayMedia():渲染层调它不会弹系统选择器,而是落到用户预选的源上。这里有个必须遵守的约束(注释明确警告):
回调只能传 纯{ id, name }的 video 对象。如果图省事把完整的DesktopCapturerSource(带 thumbnail、appIcon 等)强转过去,会破坏 Electron 内部的光标约束传播,导致渲染层设的cursor: 'never'被原生采集管线静默忽略。
8. 原生层
electron/native/ 是「JS 干不了的事」的落点。预编译产物提交在 electron/native/bin/<平台-架构>/ 并随包发布,所以普通开发不需要每次都编原生代码。
macOS(Swift,只在 macOS 上编译)
scripts/build-native-helpers.mjs 在非 macOS 平台直接跳过。用 swiftc 为 arm64-apple-macos14.0 和 x86_64-apple-macos14.0 两个目标各编一份:
| 源文件 | 产物 | 作用 |
|---|---|---|
ScreenCaptureKitRecorder.swift | recordly-screencapturekit-helper | ScreenCaptureKit 录制(含系统音频) |
ScreenCaptureKitWindowList.swift | recordly-window-list | 窗口列表枚举 |
SystemCursorAssets.swift | recordly-system-cursors | 导出系统光标素材(用于编辑器里重绘光标) |
NativeCursorMonitor.swift | recordly-native-cursor-monitor | 原生光标位置 / 类型采样 |
缺 swiftc 会直接抛错并提示安装 Xcode Command Line Tools。
Windows(C++ / CMake)
| 目录 | 产物 | 作用 |
|---|---|---|
wgc-capture/ | wgc-capture.exe | 当前实际编译的 Windows 采集辅助程序:Windows Graphics Capture + Media Foundation 编码 + WASAPI loopback 系统音频 |
cursor-monitor/ | cursor-monitor.exe | 光标遥测(位置、类型、点击事件) |
gpu-export-probe/ | recordly-gpu-export.exe | GPU 导出能力探测 |
nvidia-cuda-compositor/ | recordly-nvidia-cuda-compositor.exe | CUDA 合成(实验性,需 NVIDIA 显卡 + CUDA 工具链) |
windows-capture/ | — | 含 DXGI 回退实现(dxgi_session.cpp),但当前没有任何构建脚本引用它,属遗留代码。别误以为它在打包里 |
Windows 生成器自动回退顺序:Visual Studio 18 2026 → 17 2022 → 16 2019,并会清理 CMakeCache.txt 与 CMakeFiles 后重试。
脚本对已提交的预编译产物也很友好:找得到 CMake 就源码编译,找不到但 bin/ 里已有现成 wgc-capture.exe 就直接复用(并校验清单),两边都不满足才报错退出。
辅助程序清单(helpers-manifest)
scripts/native-helper-manifest.mjs 会为每个原生辅助程序生成 / 校验 helpers-manifest.json:记录源目录与二进制的对应关系。构建时更新,打包冒烟测试时校验。改原生源码后发现产物没更新,先看这个清单的告警。
与主进程的衔接
- 二进制寻址:
electron/ipc/paths/binaries.ts(配套测试binaries.test.ts)。 - 打包后路径校验:
npm run smoke:packaged-binaries会检查打包产物里的原生二进制是否都在位——CI 三个平台都会跑这一步。
9. 渲染层:录制端与编辑器
9.1 录制端(src/components/launch/)
HUD 是常驻入口,内部按功能拆开:
HudWindow.tsx+LaunchWindow.tsx:主体与外壳popovers/:MicPopover、WebcamPopover、SourcePopover、ProjectPopover、MorePopover、CountdownPopover,由LaunchPopoverCoordinator统一协调(同时只允许一个弹出层)hooks/:useLaunchWindowActions、useLaunchWindowSystemState、useRecordingTimer、useHudBarDrag、useWebcamPreviewOverlay、useLaunchHudInteractionStatehudMousePassthrough.ts/hudViewportBounds.ts/floatingWebcamPreview.ts:注意这三个都带独立测试,说明它们是易碎逻辑,改动务必跑测试contexts/HudInteractionContext:HUD 交互态共享
9.2 编辑器(src/components/video-editor/)
体量最大的子系统,按职责再分层:
| 子目录 / 文件 | 职责 |
|---|---|
layout/ | 外壳:EditorShell、EditorHeader、EditorSidebar、EditorPreviewPanel、EditorTimelinePanel、各类 Dialog 汇总 |
timeline/ | 时间线子系统,见下 |
videoPlayback/ | 预览引擎:缩放变换、光标跟随相机、光标渲染、场景动效、播放速率、webcam 同步 |
export/ | 导出控制器、进度、设置、持久化、冒烟导出自动化 |
project/ | 项目生命周期、打开/保存动作、快照模型、项目库 |
state/ | useProjectState、useTimelineState、useEditorUiState、useAppearanceState |
presets/ | 预设与偏好持久化 |
audio/ | 片段音频、预览同步、波形(含 Web Worker waveform.worker.ts) |
captions/ | 自动字幕控制器 |
hooks/ | 区域命令(缩放 / 裁剪 / 注释 / 音频 / 字幕 / 片段)、全局交互、历史记录、时间线投影 |
这一层的架构约定:纯逻辑抽成同名 .ts 并配 .test.ts,React 组件只做装配。
例如 clipSplit.ts / clipSplit.test.ts、zoomTransform.ts / zoomTransform.test.ts、editorHistory.ts / editorHistory.test.ts。要改行为,先改并测那个 .ts,再动组件。
时间线子系统内部:
timeline/
├── core/ rows / spans / time / constants / timelineTypes(纯函数,全部有测试)
├── model/ timelineModel.ts(带测试)
├── dnd/ engine.ts(拖拽引擎,带测试)
├── hooks/ runtime、归一化、范围、选择、键盘快捷键、DnD 绑定、音频峰值
│ ├── actions/ 音频 / 字幕 / 缩放区域动作
│ └── utils/ 放置、通知、选择工具(带测试)
├── components/ axis / markers / overlays / playhead / toolbar / viewport / waveform / wrapper
├── Item.tsx / Row.tsx / TimelineEditor.tsx
└── sourceAudioTracks.ts / timelineLayout.ts / zoomSuggestionUtils.ts9.3 导出引擎(src/lib/exporter/)
这是「录完之后怎么出片」的核心,也是测试最密的目录(27 个测试文件)。三条路线:
- 原生静态布局导出(快,主流):
nativeStaticLayoutRoutePlan.ts决定走cuda-overlay/cuda-scale-cpu-pad/cuda-static-composite/nvidia-cuda-compositor/windows-d3d11-compositor哪条,再由 IPC 调原生合成器,最后用muxer.ts封装。 - 原生逐帧导出:
nativeVideoExport.ts(主进程)+nativeFrameCapture.ts/modernFrameRenderer.ts(渲染侧),支持rawvideo与h264-stream两种输入模式。 - 浏览器导出回退:
modernVideoExporter.ts/videoExporter.ts,带streamingDecodePipeline/streamingDecoder流式解码。
音频侧:audioRoutingEngine 决定路由,sourceTrackRoutingPolicy / editedTrackStrategy 决定「直接复制源音轨」还是「离线渲染编辑后的音轨」,audioEncoder / offlineAudioProcessor 负责编码,exportBitrate / exportTuning / videoColorSpace 管参数。
其他:gifExporter.ts(GIF 输出)、captionRenderer.ts / annotationRenderer.ts(字幕与注释烧录)、exportSavePolicy.ts(保存策略,含覆盖确认)、finalizationTimeout.ts(收尾超时保护)、backendPolicy.ts(后端选择)。
backendPolicy 与 exportSavePolicy 这类"策略"文件都带测试,改判断条件时务必先看测试里约定的优先级。
10. 数据与文件位置
userData 根目录
| 环境 | 路径 |
|---|---|
开发(VITE_DEV_SERVER_URL 存在) | app.getPath("appData")/Recordly-dev(Windows 即 %APPDATA%\Recordly-dev) |
| 正式 | app.getPath("userData"),产品名 Recordly |
目录与文件清单(electron/ipc/constants.ts)
| 名称 | 位置 | 说明 |
|---|---|---|
| 录像目录 | <userData>/recordings | 可被用户改到别处(chooseRecordingsDirectory) |
| 项目目录 | <userData>/Projects | 默认项目存放地 |
| 最近项目 | <userData>/recent-projects.json | 最多 16 条 |
| 快捷键 | <userData>/shortcuts.json | |
| 录制设置 | <userData>/recordings-settings.json | 麦克风 / 系统音频 / 摄像头 |
| 倒计时设置 | <userData>/countdown-settings.json | |
| 应用设置 | <userData>/app-settings.json | |
| whisper 模型 | <userData>/whisper/ggml-small.bin | 从 HuggingFace 下载 |
文件格式约定
| 后缀 | 含义 |
|---|---|
.recordly | 项目文件(JSON)。兼容旧后缀 .openscreen |
.preview.png | 项目缩略图 |
.recordly-session.json | 录制会话清单 |
recording-* | 自动录像文件名前缀 |
自动清理策略
自动录像保留 最近 20 个 且 不超过 14 天(AUTO_RECORDING_RETENTION_COUNT / AUTO_RECORDING_MAX_AGE_MS),逻辑在 electron/ipc/recording/prune.ts(带测试)。
光标遥测
- 版本
CURSOR_TELEMETRY_VERSION = 2,采样间隔 33ms(约 30Hz),上限 1 小时 样本量。 - 数据点包含位置、点击类型、光标类型,供编辑器的缩放建议与光标重绘使用。
11. 国际化
- 支持 10 种语言(
src/i18n/config.ts):en、es、fr、de、it、nl、ko、pt-BR、zh-CN、zh-TW。默认en。 - 7 个命名空间:
common、launch、editor、timeline、settings、dialogs、shortcuts。 - 文件位于
src/i18n/locales/<locale>/<namespace>.json。 - 用法:
src/contexts/I18nContext.tsx提供useI18n(),取词形如t("app.editorTitle", "Recordly Editor")—— 第二个参数是兜底文案。 - 校验:
npm run i18n:check(scripts/i18n-check.mjs)。CI 里目前是 advisory(不阻塞),因为 main 上有已知的语言对齐欠账。 - 翻译规范见仓库根目录的
TRANSLATION_GUIDE.md;有自动翻译工作流.github/workflows/auto-translate-zh.yml。
新增文案请同时补en和zh-CN,否则i18n:check会报差集。
12. 测试
npm test # vitest --run(单次)
npm run test:watch配置要点(vitest.config.ts):
environment: "node"—— 没有 DOM。要测组件得自己 mock 或在该文件加 environment 覆盖。globals: true,可直接用describe/it/expect。include:src/**与electron/**下的*.{test,spec}.*。@别名已配。
测试分布(共 134 个文件):
| 区域 | 文件数 | 覆盖内容 |
|---|---|---|
src/lib/exporter/ | 27 | 编码、音轨路由、帧渲染、封装、比特率、保存策略(最密集) |
electron/ | 38 | 权限、导航、GPU 开关、窗口边界、媒体服务、录制各平台实现、项目原子保存、字幕与 ffmpeg |
src/components/video-editor/timeline/ | 12 | 时间线 core 纯函数、DnD 引擎、键盘快捷键、范围、选择、布局 |
src/components/video-editor/videoPlayback/ | — | 缩放变换、光标跟随 / 缩放 / 摇摆、场景动效、webcam 同步 |
src/components/video-editor/ | — | 片段切分 / 变速 / 跨度、字幕操作、历史记录、项目脏态与持久化 |
src/lib/ | — | 公告、壁纸、资源路径、媒体时间、pixi 生命周期 |
约定:易碎逻辑必须有测试。 现有代码里hudMousePassthrough、cursorScale、audioResourceVersion这类一看就容易错的文件都配了测试,新增同类逻辑请照做。
13. 代码规范
用 Biome(不是 ESLint + Prettier)。
npm run lint # biome lint .
npm run lint:fix
npm run format # biome format --write .
npm run format:check
npx tsc --noEmit # 类型检查(构建脚本里是 npx tsc)格式化约定(biome.json):
- 缩进 Tab(
indentStyle: "tab",indentWidth: 4) - 行宽 100,换行符 LF,字符串 双引号
- 自动整理 import(
assist.actions.source.organizeImports: "on")
Lint 特点:
- 没有启用
recommended预设,每条规则显式列出 —— 想加规则要显式写。 noExplicitAny: error、noNonNullAssertedOptionalChain: error、noTsIgnore: error:不要用any和@ts-ignore走捷径。useExhaustiveDependencies是warn,但useHookAtTopLevel、noUnusedVariables是error。- 忽略
dist、dist-electron、release、.tmp、electron/native/**/build。
TypeScript(tsconfig.json):strict: true、noUnusedLocals、noUnusedParameters、noFallthroughCasesInSwitch,且排除测试文件(测试由 Vitest 负责)。
14. 环境变量与调试开关
| 变量 | 作用 |
|---|---|
VITE_DEV_SERVER_URL | 由 vite-plugin-electron 注入。有值即 dev:userData 切到 Recordly-dev、跳过单实例锁 |
RECORDLY_SMOKE_EXPORT=1 | 启动即进入编辑器冒烟导出,并打印 GPU 特性诊断 |
RECORDLY_SMOKE_EXPORT_INPUT | 冒烟导出的输入文件路径 |
RECORDLY_SMOKE_EXPORT_PROJECT | 冒烟导出的输入项目文件(优先于上一项) |
RECORDLY_DEV_OPEN_RECORDING_INPUT | 启动后直接打开指定录像进编辑器 |
RECORDLY_DEV_PREVIEW_UPDATE=1 | dev 下预览更新提示 UI(macOS 走 toast,其他平台走原生对话框) |
WHISPER_RUNTIME_ARCHS | all / arm64 / x64,控制 whisper 编译目标架构 |
WHISPER_RUNTIME_ALLOW_MISSING=1 | 缺 CMake 时不报错,直接没有字幕能力 |
CI=true | 让 whisper 缺 CMake 变成软失败(同时会影响其他脚本行为) |
PACKAGED_SMOKE_ARCH_TAGS | 打包冒烟测试指定架构标签(CI 传 darwin-x64 / darwin-arm64) |
15. 打包与发布
npm run build:win # NSIS 安装包
npm run build:mac # dmg + zip(x64 / arm64)
npm run build:linux # AppImage相关脚本:
| 脚本 | 用途 |
|---|---|
smoke:packaged-binaries | 校验打包产物中的原生二进制是否齐全 |
smoke:electron-main-cjs | 校验主进程产物是合法 CJS |
normalize:electron-main-cjs | 产物规整 |
verify:macos-distribution | macOS 分发校验 |
checksums:release | 生成 SHA256 校验和文件 |
release:create | 创建 Release |
发布流程的权威说明在 RELEASING.md,请以它为准。
GitHub Actions 流水线:
| 工作流 | 触发 | 作用 |
|---|---|---|
quality.yml | PR / push main | 质量门禁(见下节) |
build.yml | 手动 | 三平台构建 + 冒烟 + 校验和 + attest + 上传 artifact |
release.yml | — | 正式发布 |
winget-releaser.yml | — | 发布到 winget |
homebrew-tap.yml | — | 更新 Homebrew tap(recordly.rb) |
macos-release-candidate.yml | — | macOS RC |
auto-translate-zh.yml | — | 中文翻译自动化 |
16. CI 质量门禁
quality.yml 在 PR 与 push main 时执行(ubuntu,20 分钟超时):
| 步骤 | 命令 | 是否阻塞 |
|---|---|---|
| 安装依赖 | npm ci --ignore-scripts | 阻塞(注意跳过原生编译) |
| 类型检查 | npx tsc --noEmit | 阻塞 |
| Lint | npm run lint | 阻塞 |
| 格式检查 | npm run format:check | advisory(continue-on-error) |
| 测试 | npm test | 阻塞 |
| 翻译校验 | npm run i18n:check | advisory |
提 PR 前本地跑这三条就够:
npx tsc --noEmit && npm run lint && npm test17. 常见问题
npm install 失败 / 卡在 postinstall
先看失败在哪一步。postinstall 分两段:rebuild:native 和 build:platform-native-helpers,日志里都有 [postinstall] Running npm script: xxx 前缀。
如果只是改前端,用第 3 节的 --ignore-scripts 方案绕开。
uiohook-napi 运行时崩溃 / ABI 不匹配npm run rebuild:native(等价于 electron-rebuild --force --only uiohook-napi)。
没有 CMake,字幕功能不可用
Windows x64 通常会走预编译,不会遇到;其他情况装 CMake(含 VS 的 CMake 组件)后执行 npm run build:whisper-runtime。
想先跳过:WHISPER_RUNTIME_ALLOW_MISSING=1。
打包后 ffmpeg / 原生辅助程序找不到
- 确认它们在
asarUnpack列表里; - 跑
npm run smoke:packaged-binaries定位缺哪个。
本地有日志、打包装没有
terser 的 drop_console 把 console.log / console.debug 删了,属预期行为。
Windows 摄像头 / 麦克风不工作
主进程启动时会打印 [permissions] Camera access is "..." 警告。去「设置 → 隐私和安全性 → 相机 / 麦克风」开启。
macOS 录制失败或光标不动
需要 屏幕录制 与 辅助功能 权限。应用内有跳转按钮(openScreenRecordingPreferences / openAccessibilityPreferences)。本地构建若被隔离:xattr -rd com.apple.quarantine /Applications/Recordly.app。
Linux 编译报 X11 头文件缺失
装 .github/workflows/build.yml 里那一串:libx11-dev libxt-dev libxtst-dev libxkbfile-dev libxi-dev libxrandr-dev libxinerama-dev。
Linux 屏幕共享弹两次选择框 / 托盘点了没反应
Wayland 已知问题,处理逻辑在 main.ts 与 setDisplayMediaRequestHandler 里,改这块前先读完相关注释。
改了 windowType 相关代码后行为异常
回顾第 6 节:主进程靠 URL 里的 windowType=editor 判断窗口身份。
Vite 构建时抛 Electron main CJS smoke failed
构建守卫生效了,跑 npm run smoke:electron-main-cjs 看具体原因,通常是主进程被打成了 ESM。
18. 贡献流程
- 从
main切分支(仓库默认分支是main,别用master)。 - 按第 13 节的规范写代码,按第 12 节的习惯补测试。
- 本地过一遍:
npx tsc --noEmit && npm run lint && npm test。 - 提 PR,遵循
.github/pull_request_template.md。 - CI 会跑质量门禁;仓库配置了 CodeRabbit(
.coderabbit.yaml)做自动评审,其评审指令在.github/instructions/general.instructions.md(要求:不打行内评论,只给一条汇总,含等级 A-F、阻塞问题、可选改进、是否可合并)。 - Issue 模板:
.github/ISSUE_TEMPLATE/bug_report.yml、feature_request.yml。
扩展开发:Recordly 有社区扩展系统(光标点击音效、设备边框、浏览器外壳、壁纸、渲染钩子、设置面板等),接口说明见 EXTENSIONS.md,市场在 https://marketplace.recordly.dev/extensions。
附:改动速查表
| 你想改什么 | 先从哪个文件入手 |
|---|---|
| HUD 按钮 / 录制控制 | src/components/launch/LaunchWindow.tsx、RecordingControls.tsx |
| 录制参数(麦克风、系统音频) | electron/ipc/recording/ + electron/ipc/settings/recordingPreferencesStore.ts |
| 时间线拖拽行为 | src/components/video-editor/timeline/engine(timeline/dnd/engine.ts)与 timeline/core/ |
| 导出画质 / 码率 | src/lib/exporter/exportBitrate.ts、exportTuning.ts、backendPolicy.ts |
| 导出后端选择 | src/components/video-editor/export/ + electron/ipc/export/native-video.ts |
| 光标外观 / 跟随 | src/components/video-editor/videoPlayback/cursor*.ts |
| 自动字幕 | electron/ipc/captions/(whisper + 分段 + 静音检测) |
| 新增一个 IPC | electron/ipc/register/* → preload.ts → electron-env.d.ts |
| 新增窗口 | electron/windows.ts + src/App.tsx 的 windowType 分支 |
| 加界面文案 | src/i18n/locales/{en,zh-CN}/*.json |
| 原生辅助程序 | electron/native/ + scripts/build-*.mjs |
| 打包产物内容 | electron-builder.json5 |



