remove macos cursor highlight; wire telemetry session for non-windows

This commit is contained in:
Siddharth
2026-05-10 14:12:54 -07:00
parent 0720a6d802
commit b41c4f49fc
21 changed files with 100 additions and 774 deletions
+22 -23
View File
@@ -4,10 +4,9 @@
"appId": "com.siddharthvaddem.openscreen",
"asar": true,
// .node binaries can't be dlopen'd from inside an asar — must live unpacked.
"asarUnpack": [
"node_modules/uiohook-napi/**/*",
"**/*.node"
],
"asarUnpack": [
"**/*.node"
],
"productName": "Openscreen",
"npmRebuild": true,
"buildDependenciesFromSource": true,
@@ -15,12 +14,12 @@
"directories": {
"output": "release/${version}"
},
"files": [
"dist",
"dist-electron",
"!*.png",
"!preview*.png",
"!*.md",
"files": [
"dist",
"dist-electron",
"!*.png",
"!preview*.png",
"!*.md",
"!README.md",
"!CONTRIBUTING.md",
"!LICENSE"
@@ -65,19 +64,19 @@
"artifactName": "${productName}-Linux-${version}.${ext}",
"category": "AudioVideo"
},
"win": {
"target": [
"nsis"
],
"icon": "icons/icons/win/icon.ico",
"extraResources": [
{
"from": "electron/native/bin",
"to": "electron/native/bin",
"filter": ["**/*"]
}
]
},
"win": {
"target": [
"nsis"
],
"icon": "icons/icons/win/icon.ico",
"extraResources": [
{
"from": "electron/native/bin",
"to": "electron/native/bin",
"filter": ["**/*"]
}
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
-5
View File
@@ -40,11 +40,6 @@ interface Window {
status: string;
error?: string;
}>;
requestAccessibilityAccess: () => Promise<{
success: boolean;
granted: boolean;
error?: string;
}>;
assetBaseUrl: string;
storeRecordedVideo: (
videoData: ArrayBuffer,
-16
View File
@@ -975,22 +975,6 @@ export function registerIpcHandlers(
}
});
// macOS Accessibility prompt for global click capture. First call shows the
// system dialog; the user has to toggle the app in System Settings (no
// programmatic grant exists for Accessibility).
ipcMain.handle("request-accessibility-access", () => {
if (process.platform !== "darwin") {
return { success: true, granted: true };
}
try {
const granted = systemPreferences.isTrustedAccessibilityClient(true);
return { success: true, granted };
} catch (error) {
console.error("Failed to request accessibility access:", error);
return { success: false, granted: false, error: String(error) };
}
});
ipcMain.handle("open-source-selector", () => {
const sourceSelectorWin = getSourceSelectorWindow();
if (sourceSelectorWin) {
@@ -1,6 +1,6 @@
import type { Rectangle } from "electron";
import type { CursorRecordingData } from "../../../../src/native/contracts";
import type { CursorRecordingSession } from "./session";
import { TelemetryRecordingSession } from "./telemetryRecordingSession";
import { WindowsNativeRecordingSession } from "./windowsNativeRecordingSession";
interface CreateCursorRecordingSessionOptions {
@@ -12,21 +12,6 @@ interface CreateCursorRecordingSessionOptions {
startTimeMs?: number;
}
class NoopCursorRecordingSession implements CursorRecordingSession {
async start(): Promise<void> {
// Native cursor capture is currently Windows-only.
}
async stop(): Promise<CursorRecordingData> {
return {
version: 2,
provider: "none",
assets: [],
samples: [],
};
}
}
export function createCursorRecordingSession(
options: CreateCursorRecordingSessionOptions,
): CursorRecordingSession {
@@ -40,5 +25,13 @@ export function createCursorRecordingSession(
});
}
return new NoopCursorRecordingSession();
// macOS / Linux: capture cursor positions via Electron's `screen` API on an
// interval. No cursor sprites/assets and no clicks — just position telemetry,
// which is what auto-zoom and other features consume.
return new TelemetryRecordingSession({
getDisplayBounds: options.getDisplayBounds,
maxSamples: options.maxSamples,
sampleIntervalMs: options.sampleIntervalMs,
startTimeMs: options.startTimeMs,
});
}
-4
View File
@@ -48,10 +48,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
requestCameraAccess: () => {
return ipcRenderer.invoke("request-camera-access");
},
requestAccessibilityAccess: () => {
return ipcRenderer.invoke("request-accessibility-access");
},
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
return ipcRenderer.invoke("store-recorded-video", videoData, fileName);
},
+53 -59
View File
@@ -7,7 +7,6 @@
"": {
"name": "openscreen",
"version": "1.4.0",
"hasInstallScript": true,
"dependencies": {
"@fix-webm-duration/fix": "^1.0.1",
"@pixi/filter-drop-shadow": "^5.2.0",
@@ -48,7 +47,6 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"uiohook-napi": "^1.5.5",
"uuid": "^13.0.0",
"web-demuxer": "^4.0.0"
},
@@ -188,7 +186,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -397,7 +394,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -721,7 +717,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -770,7 +765,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1202,6 +1196,7 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -1223,6 +1218,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -1239,6 +1235,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
@@ -1253,6 +1250,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
@@ -1962,6 +1960,7 @@
"resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz",
"integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/colord": "^2.9.6"
}
@@ -1976,7 +1975,8 @@
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz",
"integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@pixi/core": {
"version": "7.4.3",
@@ -2003,7 +2003,8 @@
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz",
"integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@pixi/filter-drop-shadow": {
"version": "5.2.0",
@@ -2030,19 +2031,22 @@
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz",
"integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@pixi/runner": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz",
"integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@pixi/settings": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz",
"integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/constants": "7.4.3",
"@types/css-font-loading-module": "^0.0.12",
@@ -2054,6 +2058,7 @@
"resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz",
"integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/extensions": "7.4.3",
"@pixi/settings": "7.4.3",
@@ -2065,6 +2070,7 @@
"resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz",
"integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pixi/color": "7.4.3",
"@pixi/constants": "7.4.3",
@@ -3643,7 +3649,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3718,7 +3725,8 @@
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
"integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/debug": {
"version": "4.1.13",
@@ -3756,7 +3764,8 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz",
"integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
@@ -3855,7 +3864,6 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -3867,7 +3875,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -4149,7 +4156,6 @@
"integrity": "sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/browser": "4.1.5",
"@vitest/mocker": "4.1.5",
@@ -4342,7 +4348,6 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -4868,7 +4873,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -5050,6 +5054,7 @@
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
@@ -5400,7 +5405,8 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -5690,7 +5696,6 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.8.1",
"builder-util": "26.8.1",
@@ -5783,7 +5788,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dotenv": {
"version": "16.6.1",
@@ -5832,7 +5838,8 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/ejs": {
"version": "3.1.10",
@@ -6015,6 +6022,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -6035,6 +6043,7 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@@ -6277,7 +6286,8 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/expect-type": {
"version": "1.3.0",
@@ -7689,6 +7699,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -7908,6 +7919,7 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -8093,17 +8105,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-gyp/node_modules/isexe": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
@@ -8221,6 +8222,7 @@
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
@@ -8429,7 +8431,6 @@
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.18.1.tgz",
"integrity": "sha512-6LUPWYgulZhp/w4kam2XHXB0QedISZIqrJbRdHLLQ3csn5a38uzKxAp6B5j6s89QFYaIJbg95kvgTRcbgpO1ow==",
"license": "MIT",
"peer": true,
"workspaces": [
"examples",
"playground"
@@ -8475,7 +8476,6 @@
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright-core": "1.59.1"
},
@@ -8546,7 +8546,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -8691,6 +8690,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -8708,6 +8708,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -8718,6 +8719,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -8733,6 +8735,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -8846,6 +8849,7 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"side-channel": "^1.1.0"
},
@@ -8904,7 +8908,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -8917,7 +8920,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -8954,7 +8956,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-refresh": {
"version": "0.18.0",
@@ -9275,6 +9278,7 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -9481,6 +9485,7 @@
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
@@ -9500,6 +9505,7 @@
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
@@ -9516,6 +9522,7 @@
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -9534,6 +9541,7 @@
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -9874,7 +9882,6 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -9958,6 +9965,7 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
@@ -10272,19 +10280,6 @@
"node": ">=14.17"
}
},
"node_modules/uiohook-napi": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.5.tgz",
"integrity": "sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/undici": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
@@ -10358,6 +10353,7 @@
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
"license": "MIT",
"peer": true,
"dependencies": {
"punycode": "^1.4.1",
"qs": "^6.12.3"
@@ -10370,7 +10366,8 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
@@ -10463,7 +10460,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -10553,8 +10549,7 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/vite/node_modules/fsevents": {
"version": "2.3.3",
@@ -10577,7 +10572,6 @@
"integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.1.5",
"@vitest/mocker": "4.1.5",
+1 -4
View File
@@ -41,9 +41,7 @@
"test:browser:install": "playwright install --with-deps chromium-headless-shell",
"test:e2e": "playwright test",
"test:e2e:windows-native-checklist": "playwright test tests/e2e/windows-native-checklist.spec.ts",
"prepare": "husky",
"rebuild:native": "node ./scripts/rebuild-native.mjs",
"postinstall": "npm run rebuild:native"
"prepare": "husky"
},
"dependencies": {
"@fix-webm-duration/fix": "^1.0.1",
@@ -85,7 +83,6 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"uiohook-napi": "^1.5.5",
"uuid": "^13.0.0",
"web-demuxer": "^4.0.0"
},
-21
View File
@@ -1,21 +0,0 @@
import { spawnSync } from "node:child_process";
import process from "node:process";
// uiohook-napi click capture is macOS-only at runtime (gated in
// electron/ipc/handlers.ts). Skip the rebuild on other platforms so CI runners
// without X11 dev headers don't fail npm install. The library's prebuilt
// .node binaries are still bundled and loadable; we just don't need a fresh
// build against Electron's ABI on platforms where we don't load it.
if (process.platform !== "darwin") {
console.log(
`[rebuild:native] Skipping uiohook-napi rebuild on ${process.platform} (macOS-only).`,
);
process.exit(0);
}
const result = spawnSync(
process.execPath,
["./node_modules/@electron/rebuild/lib/cli.js", "--force", "--only", "uiohook-napi"],
{ stdio: "inherit" },
);
process.exit(result.status ?? 0);
+3 -242
View File
@@ -1,7 +1,6 @@
import * as SliderPrimitive from "@radix-ui/react-slider";
import {
Bug,
ChevronDown,
Crop,
Download,
FileDown,
@@ -28,7 +27,6 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Select,
SelectContent,
@@ -221,12 +219,6 @@ const GRADIENTS = [
];
interface SettingsPanelProps {
cursorHighlight?: import("./videoPlayback/cursorHighlight").CursorHighlightConfig;
onCursorHighlightChange?: (
next: import("./videoPlayback/cursorHighlight").CursorHighlightConfig,
) => void;
// macOS only — gates the "Only on clicks" toggle (needs uiohook).
cursorHighlightSupportsClicks?: boolean;
selected: string;
onWallpaperChange: (path: string) => void;
selectedZoomDepth?: ZoomDepth | null;
@@ -321,7 +313,6 @@ interface SettingsPanelProps {
onCursorClickBounceChange?: (bounce: number) => void;
hasCursorData?: boolean;
showCursorSettings?: boolean;
showCursorHighlightSettings?: boolean;
}
export default SettingsPanel;
@@ -338,9 +329,6 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
type SettingsPanelMode = "background" | "effects" | "layout" | "cursor" | "export";
export function SettingsPanel({
cursorHighlight,
onCursorHighlightChange,
cursorHighlightSupportsClicks = false,
selected,
onWallpaperChange,
selectedZoomDepth,
@@ -430,7 +418,6 @@ export function SettingsPanel({
onCursorClickBounceChange,
hasCursorData = false,
showCursorSettings = true,
showCursorHighlightSettings = true,
}: SettingsPanelProps) {
const t = useScopedT("settings");
const [activePanelMode, setActivePanelMode] = useState<SettingsPanelMode>("background");
@@ -567,9 +554,7 @@ export function SettingsPanel({
const zoomEnabled = Boolean(selectedZoomDepth);
const trimEnabled = Boolean(selectedTrimId);
const hasTimelineSelection = Boolean(selectedZoomId || selectedTrimId || selectedSpeedId);
const hasCursorPanel =
(showCursorSettings && hasCursorData) ||
(showCursorHighlightSettings && Boolean(cursorHighlight && onCursorHighlightChange));
const hasCursorPanel = showCursorSettings && hasCursorData;
const panelModes: Array<{
id: SettingsPanelMode;
label: string;
@@ -583,7 +568,7 @@ export function SettingsPanel({
? [
{
id: "cursor" as const,
label: t("effects.cursorHighlight.title"),
label: t("effects.title"),
icon: MousePointerClick,
},
]
@@ -1288,11 +1273,7 @@ export function SettingsPanel({
) : (
<SlidersHorizontal className="w-4 h-4 text-[#34B27B]" />
)}
<span className="text-xs font-medium">
{activePanelMode === "cursor"
? t("effects.cursorHighlight.title")
: t("effects.title")}
</span>
<span className="text-xs font-medium">{t("effects.title")}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-3">
@@ -1483,226 +1464,6 @@ export function SettingsPanel({
)}
</div>
)}
{activePanelMode === "cursor" &&
showCursorHighlightSettings &&
cursorHighlight &&
onCursorHighlightChange && (
<div className="p-2 rounded-lg editor-control-surface mt-2 space-y-2">
<div className="flex items-center justify-between">
<div className="text-[10px] font-medium text-slate-300">
{t("effects.cursorHighlight.title")}
</div>
<button
type="button"
onClick={() =>
onCursorHighlightChange({
...cursorHighlight,
enabled: !cursorHighlight.enabled,
})
}
className={`text-[10px] px-2 py-0.5 rounded border transition-colors ${
cursorHighlight.enabled
? "bg-[#34B27B]/20 border-[#34B27B]/50 text-[#34B27B]"
: "bg-white/5 border-white/10 text-slate-400"
}`}
>
{cursorHighlight.enabled ? t("effects.on") : t("effects.off")}
</button>
</div>
<div
className={`grid grid-cols-2 gap-1 ${cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"}`}
>
{(["dot", "ring"] as const).map((style) => (
<button
key={style}
type="button"
onClick={() =>
onCursorHighlightChange({ ...cursorHighlight, style })
}
className={`text-[10px] px-2 py-1 rounded border capitalize transition-colors ${
cursorHighlight.style === style
? "bg-[#34B27B]/20 border-[#34B27B]/50 text-[#34B27B]"
: "bg-white/5 border-white/10 text-slate-300 hover:border-white/20"
}`}
>
{t(`effects.cursorHighlight.${style}`)}
</button>
))}
</div>
<div
className={
cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"
}
>
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] text-slate-400">
{t("effects.cursorHighlight.size")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorHighlight.sizePx}px
</span>
</div>
<Slider
value={[cursorHighlight.sizePx]}
onValueChange={(values) =>
onCursorHighlightChange({
...cursorHighlight,
sizePx: values[0],
})
}
min={10}
max={36}
step={1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
{cursorHighlightSupportsClicks && (
<div
className={`flex items-center justify-between ${cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"}`}
>
<div className="text-[10px] text-slate-400">
{t("effects.cursorHighlight.onlyOnClicks")}
</div>
<button
type="button"
onClick={async () => {
const turningOn = !cursorHighlight.onlyOnClicks;
if (turningOn) {
try {
const result =
await window.electronAPI?.requestAccessibilityAccess?.();
if (!result?.granted) {
toast.message(
t("effects.cursorHighlight.accessibilityPermissionTitle"),
{
description: t(
"effects.cursorHighlight.accessibilityPermissionDescription",
),
},
);
return;
}
} catch (err) {
console.warn("Accessibility request failed:", err);
}
}
onCursorHighlightChange({
...cursorHighlight,
onlyOnClicks: turningOn,
});
}}
className={`text-[10px] px-2 py-0.5 rounded border transition-colors ${
cursorHighlight.onlyOnClicks
? "bg-[#34B27B]/20 border-[#34B27B]/50 text-[#34B27B]"
: "bg-white/5 border-white/10 text-slate-400"
}`}
>
{cursorHighlight.onlyOnClicks ? t("effects.on") : t("effects.off")}
</button>
</div>
)}
<div
className={
cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"
}
>
<div className="text-[10px] text-slate-400 mb-1">
{t("effects.cursorHighlight.color")}
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full h-8 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
>
<div
className="w-4 h-4 rounded-full border border-white/20"
style={{ backgroundColor: cursorHighlight.color }}
/>
<span className="text-[10px] text-slate-300 truncate flex-1 text-left font-mono">
{cursorHighlight.color}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
side="top"
className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl"
>
<ColorPicker
selectedColor={cursorHighlight.color}
colorPalette={colorPalette}
translations={{
colorWheel: t("background.colorWheel"),
colorPalette: t("background.colorPalette"),
}}
onUpdateColor={(color) =>
onCursorHighlightChange({
...cursorHighlight,
color,
})
}
/>
</PopoverContent>
</Popover>
</div>
<div
className={
cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"
}
>
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] text-slate-400">
{t("effects.cursorHighlight.offsetX")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{(cursorHighlight.offsetXNorm * 100).toFixed(1)}%
</span>
</div>
<Slider
value={[cursorHighlight.offsetXNorm]}
onValueChange={(values) =>
onCursorHighlightChange({
...cursorHighlight,
offsetXNorm: values[0],
})
}
min={-0.25}
max={0.25}
step={0.005}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div
className={
cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none"
}
>
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] text-slate-400">
{t("effects.cursorHighlight.offsetY")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{(cursorHighlight.offsetYNorm * 100).toFixed(1)}%
</span>
</div>
<Slider
value={[cursorHighlight.offsetYNorm]}
onValueChange={(values) =>
onCursorHighlightChange({
...cursorHighlight,
offsetYNorm: values[0],
})
}
min={-0.25}
max={0.25}
step={0.005}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
)}
+7 -20
View File
@@ -126,7 +126,6 @@ export default function VideoEditor() {
webcamMaskShape,
webcamSizePreset,
webcamPosition,
cursorHighlight,
} = editorState;
// ── Non-undoable state
@@ -205,17 +204,15 @@ export default function VideoEditor() {
const nextSpeedIdRef = useRef(1);
const { shortcuts, isMac } = useShortcuts();
// Windows-only: the synthetic cursor overlay + cursor customization settings
// only apply when there's an actual native cursor recording (cursor frames +
// position samples produced by WindowsNativeRecordingSession). Mac and Linux
// keep their telemetry positions for auto-zoom but never render a synthetic
// cursor or expose cursor customization settings.
const hasEditableCursorRecording =
recordingCursorCaptureMode === "editable-overlay" ||
(recordingCursorCaptureMode === null && hasNativeCursorRecordingData(cursorRecordingData));
nativePlatform === "win32" && hasNativeCursorRecordingData(cursorRecordingData);
const effectiveShowCursor = showCursor && hasEditableCursorRecording;
const showCursorSettings = nativePlatform === "win32" && hasEditableCursorRecording;
// Off-Mac doesn't have click telemetry, so force `onlyOnClicks` off for
// renderers while keeping the persisted value intact for round-tripping.
const effectiveCursorHighlight = useMemo(
() => (isMac ? cursorHighlight : { ...cursorHighlight, onlyOnClicks: false }),
[cursorHighlight, isMac],
);
const showCursorSettings = hasEditableCursorRecording;
const { locale, setLocale, t: rawT } = useI18n();
const t = useScopedT("editor");
const ts = useScopedT("settings");
@@ -534,7 +531,6 @@ export default function VideoEditor() {
gifFrameRate,
gifLoop,
gifSizePreset,
cursorHighlight,
};
const projectData = createProjectData(currentProjectMedia, editorState);
@@ -596,7 +592,6 @@ export default function VideoEditor() {
videoPath,
t,
webcamSizePreset,
cursorHighlight,
],
);
@@ -1569,7 +1564,6 @@ export default function VideoEditor() {
previewHeight,
cursorTelemetry,
cursorClickTimestamps,
cursorHighlight: effectiveCursorHighlight,
onProgress: (progress: ExportProgress) => {
setExportProgress(progress);
},
@@ -1715,7 +1709,6 @@ export default function VideoEditor() {
previewHeight,
cursorTelemetry,
cursorClickTimestamps,
cursorHighlight: effectiveCursorHighlight,
onProgress: (progress: ExportProgress) => {
setExportProgress(progress);
},
@@ -1797,7 +1790,6 @@ export default function VideoEditor() {
handleExportSaved,
cursorTelemetry,
cursorClickTimestamps,
effectiveCursorHighlight,
effectiveShowCursor,
cursorSize,
cursorSmoothing,
@@ -2074,7 +2066,6 @@ export default function VideoEditor() {
onBlurDataChange={handleBlurDataPreviewChange}
onBlurDataCommit={commitState}
cursorTelemetry={cursorTelemetry}
cursorHighlight={effectiveCursorHighlight}
cursorClickTimestamps={cursorClickTimestamps}
showCursor={effectiveShowCursor}
cursorSize={cursorSize}
@@ -2103,9 +2094,6 @@ export default function VideoEditor() {
<div className="editor-settings-rail min-w-0 h-full">
<SettingsPanel
cursorHighlight={cursorHighlight}
onCursorHighlightChange={(next) => pushState({ cursorHighlight: next })}
cursorHighlightSupportsClicks={isMac}
selected={wallpaper}
onWallpaperChange={(w) => pushState({ wallpaper: w })}
selectedZoomDepth={
@@ -2240,7 +2228,6 @@ export default function VideoEditor() {
cursorTelemetry.length > 0 || hasNativeCursorRecordingData(cursorRecordingData)
}
showCursorSettings={showCursorSettings}
showCursorHighlightSettings={isMac}
/>
</div>
</div>
+1 -65
View File
@@ -77,17 +77,7 @@ import {
ZOOM_SCALE_DEADZONE,
ZOOM_TRANSLATION_DEADZONE_PX,
} from "./videoPlayback/constants";
import {
adaptiveSmoothFactor,
interpolateCursorAt,
smoothCursorFocus,
} from "./videoPlayback/cursorFollowUtils";
import {
type CursorHighlightConfig,
clickEmphasisAlpha,
DEFAULT_CURSOR_HIGHLIGHT,
drawCursorHighlightGraphics,
} from "./videoPlayback/cursorHighlight";
import { adaptiveSmoothFactor, smoothCursorFocus } from "./videoPlayback/cursorFollowUtils";
import {
DEFAULT_CURSOR_CONFIG,
PixiCursorOverlay,
@@ -152,7 +142,6 @@ interface VideoPlaybackProps {
onBlurDataChange?: (id: string, blurData: BlurData) => void;
onBlurDataCommit?: () => void;
cursorTelemetry?: CursorTelemetryPoint[];
cursorHighlight?: CursorHighlightConfig;
cursorClickTimestamps?: number[];
showCursor?: boolean;
cursorSize?: number;
@@ -253,7 +242,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
onBlurDataChange,
onBlurDataCommit,
cursorTelemetry = [],
cursorHighlight = DEFAULT_CURSOR_HIGHLIGHT,
cursorClickTimestamps = [],
showCursor = false,
cursorSize = DEFAULT_CURSOR_SIZE,
@@ -285,9 +273,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
const currentTimeRef = useRef(0);
const zoomRegionsRef = useRef<ZoomRegion[]>([]);
const cursorTelemetryRef = useRef<CursorTelemetryPoint[]>([]);
const cursorHighlightRef = useRef<CursorHighlightConfig>(DEFAULT_CURSOR_HIGHLIGHT);
const cursorClickTimestampsRef = useRef<number[]>([]);
const cursorHighlightGraphicsRef = useRef<Graphics | null>(null);
const selectedZoomIdRef = useRef<string | null>(null);
const animationStateRef = useRef({
scale: 1,
@@ -742,13 +728,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
cursorTelemetryRef.current = cursorTelemetry;
}, [cursorTelemetry]);
useEffect(() => {
cursorHighlightRef.current = cursorHighlight;
if (cursorHighlightGraphicsRef.current) {
drawCursorHighlightGraphics(cursorHighlightGraphicsRef.current, cursorHighlight);
}
}, [cursorHighlight]);
useEffect(() => {
cursorClickTimestampsRef.current = cursorClickTimestamps;
}, [cursorClickTimestamps]);
@@ -1084,11 +1063,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
videoContainer.addChild(cursorOverlayRef.current.container);
}
const cursorHighlightGraphics = new Graphics();
cursorHighlightGraphics.visible = false;
videoContainer.addChild(cursorHighlightGraphics);
cursorHighlightGraphicsRef.current = cursorHighlightGraphics;
drawCursorHighlightGraphics(cursorHighlightGraphics, cursorHighlightRef.current);
videoContainer.addChild(nativeCursorSprite);
animationStateRef.current = {
@@ -1153,11 +1127,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
videoContainer.removeChild(maskGraphics);
maskGraphics.destroy();
}
if (cursorHighlightGraphicsRef.current) {
videoContainer.removeChild(cursorHighlightGraphicsRef.current);
cursorHighlightGraphicsRef.current.destroy();
cursorHighlightGraphicsRef.current = null;
}
if (nativeCursorSpriteRef.current) {
videoContainer.removeChild(nativeCursorSpriteRef.current);
nativeCursorSpriteRef.current.destroy();
@@ -1385,39 +1354,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
motionVector,
);
const cursorGraphics = cursorHighlightGraphicsRef.current;
const cursorConfig = cursorHighlightRef.current;
const lockedDims = lockedVideoDimensionsRef.current;
if (cursorGraphics) {
if (cursorConfig.enabled && lockedDims && cursorTelemetryRef.current.length > 0) {
const emphasisAlpha = clickEmphasisAlpha(
currentTimeRef.current,
cursorClickTimestampsRef.current,
cursorConfig,
);
const cursorPoint =
emphasisAlpha > 0
? interpolateCursorAt(cursorTelemetryRef.current, currentTimeRef.current)
: null;
if (cursorPoint) {
const baseScale = baseScaleRef.current;
const baseOffset = baseOffsetRef.current;
const cx = cursorPoint.cx + cursorConfig.offsetXNorm;
const cy = cursorPoint.cy + cursorConfig.offsetYNorm;
cursorGraphics.position.set(
baseOffset.x + cx * lockedDims.width * baseScale,
baseOffset.y + cy * lockedDims.height * baseScale,
);
cursorGraphics.alpha = emphasisAlpha;
cursorGraphics.visible = true;
} else {
cursorGraphics.visible = false;
}
} else {
cursorGraphics.visible = false;
}
}
const isMotionBlurActive =
(motionBlurAmountRef.current || 0) > 0 && isPlayingRef.current && !isScrubbingRef.current;
@@ -80,7 +80,6 @@ export interface ProjectEditorState {
gifFrameRate: GifFrameRate;
gifLoop: boolean;
gifSizePreset: GifSizePreset;
cursorHighlight: import("./videoPlayback/cursorHighlight").CursorHighlightConfig;
}
export interface EditorProjectData {
@@ -489,52 +488,6 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
editor.gifSizePreset === "original"
? editor.gifSizePreset
: "medium",
cursorHighlight: normalizeCursorHighlight(editor.cursorHighlight),
};
}
function normalizeCursorHighlight(
value: unknown,
): import("./videoPlayback/cursorHighlight").CursorHighlightConfig {
const fallback: import("./videoPlayback/cursorHighlight").CursorHighlightConfig = {
enabled: false,
style: "ring",
sizePx: 24,
color: "#FFD700",
opacity: 0.9,
onlyOnClicks: false,
clickEmphasisDurationMs: 350,
offsetXNorm: 0,
offsetYNorm: 0,
};
if (!value || typeof value !== "object") return fallback;
const v = value as Partial<import("./videoPlayback/cursorHighlight").CursorHighlightConfig>;
return {
enabled: typeof v.enabled === "boolean" ? v.enabled : fallback.enabled,
style: v.style === "dot" || v.style === "ring" ? v.style : fallback.style,
sizePx:
typeof v.sizePx === "number" && v.sizePx >= 10 && v.sizePx <= 36 ? v.sizePx : fallback.sizePx,
color:
typeof v.color === "string" && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v.color)
? v.color
: fallback.color,
opacity:
typeof v.opacity === "number" && v.opacity >= 0 && v.opacity <= 1
? v.opacity
: fallback.opacity,
onlyOnClicks: typeof v.onlyOnClicks === "boolean" ? v.onlyOnClicks : fallback.onlyOnClicks,
clickEmphasisDurationMs:
typeof v.clickEmphasisDurationMs === "number" && v.clickEmphasisDurationMs > 0
? v.clickEmphasisDurationMs
: fallback.clickEmphasisDurationMs,
offsetXNorm:
typeof v.offsetXNorm === "number" && Number.isFinite(v.offsetXNorm)
? Math.max(-1, Math.min(1, v.offsetXNorm))
: fallback.offsetXNorm,
offsetYNorm:
typeof v.offsetYNorm === "number" && Number.isFinite(v.offsetYNorm)
? Math.max(-1, Math.min(1, v.offsetYNorm))
: fallback.offsetYNorm,
};
}
@@ -1,125 +0,0 @@
import type { Graphics } from "pixi.js";
export type CursorHighlightStyle = "dot" | "ring";
export interface CursorHighlightConfig {
enabled: boolean;
style: CursorHighlightStyle;
sizePx: number;
color: string;
opacity: number;
// Show only on clicks (macOS — depends on click telemetry from uiohook).
onlyOnClicks: boolean;
clickEmphasisDurationMs: number;
// Per-recording manual nudge. Cursor telemetry is normalized to the display,
// but window recordings frame a subset of the display so the highlight
// lands offset. Users dial these in once to align with the actual cursor.
offsetXNorm: number;
offsetYNorm: number;
}
export const CURSOR_HIGHLIGHT_MIN_SIZE_PX = 10;
export const CURSOR_HIGHLIGHT_MAX_SIZE_PX = 36;
export const DEFAULT_CURSOR_HIGHLIGHT: CursorHighlightConfig = {
enabled: false,
style: "ring",
sizePx: 24,
color: "#FFD700",
opacity: 0.9,
onlyOnClicks: false,
clickEmphasisDurationMs: 350,
offsetXNorm: 0,
offsetYNorm: 0,
};
export const CURSOR_HIGHLIGHT_OFFSET_RANGE = 0.25; // ±25% of recorded surface
// Alpha multiplier for the highlight at `timeMs`. Returns 1 when not in
// click-only mode; in click-only mode fades 1→0 across each click's window.
export function clickEmphasisAlpha(
timeMs: number,
clickTimestampsMs: number[] | undefined,
config: CursorHighlightConfig,
): number {
if (!config.onlyOnClicks) return 1;
if (!clickTimestampsMs || clickTimestampsMs.length === 0) return 0;
const window = Math.max(1, config.clickEmphasisDurationMs);
for (let i = 0; i < clickTimestampsMs.length; i++) {
const dt = timeMs - clickTimestampsMs[i];
if (dt >= 0 && dt <= window) {
return 1 - dt / window;
}
}
return 0;
}
function parseHexColor(hex: string): number {
const cleaned = hex.replace("#", "");
if (cleaned.length === 3) {
const r = cleaned[0];
const g = cleaned[1];
const b = cleaned[2];
return Number.parseInt(`${r}${r}${g}${g}${b}${b}`, 16);
}
return Number.parseInt(cleaned.slice(0, 6), 16);
}
export function drawCursorHighlightGraphics(g: Graphics, config: CursorHighlightConfig): void {
g.clear();
if (!config.enabled) return;
const color = parseHexColor(config.color);
const radius = Math.max(1, config.sizePx / 2);
const alpha = Math.max(0, Math.min(1, config.opacity));
switch (config.style) {
case "dot": {
g.circle(0, 0, radius);
g.fill({ color, alpha });
break;
}
case "ring": {
g.circle(0, 0, radius);
g.stroke({ color, alpha, width: Math.max(2, radius * 0.18) });
break;
}
}
}
export function drawCursorHighlightCanvas(
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
config: CursorHighlightConfig,
pixelScale = 1,
): void {
if (!config.enabled) return;
const radius = Math.max(1, (config.sizePx / 2) * pixelScale);
const alpha = Math.max(0, Math.min(1, config.opacity));
const color = config.color;
ctx.save();
ctx.globalAlpha = alpha;
switch (config.style) {
case "dot": {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
break;
}
case "ring": {
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(2, radius * 0.18);
ctx.stroke();
break;
}
}
ctx.restore();
}
-6
View File
@@ -17,10 +17,6 @@ import {
DEFAULT_WEBCAM_POSITION,
DEFAULT_WEBCAM_SIZE_PRESET,
} from "@/components/video-editor/types";
import {
type CursorHighlightConfig,
DEFAULT_CURSOR_HIGHLIGHT,
} from "@/components/video-editor/videoPlayback/cursorHighlight";
import { DEFAULT_WALLPAPER } from "@/lib/wallpaper";
import type { AspectRatio } from "@/utils/aspectRatioUtils";
@@ -43,7 +39,6 @@ export interface EditorState {
webcamMaskShape: WebcamMaskShape;
webcamSizePreset: WebcamSizePreset;
webcamPosition: WebcamPosition | null;
cursorHighlight: CursorHighlightConfig;
}
export const INITIAL_EDITOR_STATE: EditorState = {
@@ -63,7 +58,6 @@ export const INITIAL_EDITOR_STATE: EditorState = {
webcamMaskShape: DEFAULT_WEBCAM_MASK_SHAPE,
webcamSizePreset: DEFAULT_WEBCAM_SIZE_PRESET,
webcamPosition: DEFAULT_WEBCAM_POSITION,
cursorHighlight: DEFAULT_CURSOR_HIGHLIGHT,
};
type StateUpdate = Partial<EditorState> | ((prev: EditorState) => Partial<EditorState>);
+1 -14
View File
@@ -38,20 +38,7 @@
"on": "تشغيل",
"shadow": "ظل",
"roundness": "الاستدارة",
"padding": "المسافة البادئة",
"cursorHighlight": {
"title": "تمييز المؤشر",
"style": "النمط",
"dot": "نقطة",
"ring": "حلقة",
"size": "الحجم",
"onlyOnClicks": "عند النقر فقط",
"color": "اللون",
"offsetX": "إزاحة X (لتسجيلات النوافذ)",
"offsetY": "إزاحة Y",
"accessibilityPermissionTitle": "مطلوب إذن الوصول",
"accessibilityPermissionDescription": "افتح إعدادات النظام ← الخصوصية والأمان ← إمكانية الوصول، وقم بتفعيل Openscreen، ثم أعد تشغيل التطبيق."
}
"padding": "المسافة البادئة"
},
"background": {
"title": "الخلفية",
+1 -14
View File
@@ -54,20 +54,7 @@
"on": "on",
"shadow": "Shadow",
"roundness": "Roundness",
"padding": "Padding",
"cursorHighlight": {
"title": "Cursor highlight",
"style": "Style",
"dot": "Dot",
"ring": "Ring",
"size": "Size",
"onlyOnClicks": "Only on clicks",
"color": "Color",
"offsetX": "Offset X (window recordings)",
"offsetY": "Offset Y",
"accessibilityPermissionTitle": "Accessibility permission needed",
"accessibilityPermissionDescription": "Open System Settings → Privacy & Security → Accessibility, enable Openscreen, then restart the app."
}
"padding": "Padding"
},
"background": {
"title": "Background",
+1 -14
View File
@@ -46,20 +46,7 @@
"on": "вкл",
"shadow": "Тень",
"roundness": "Скругление",
"padding": "Отступ",
"cursorHighlight": {
"title": "Подсветка курсора",
"style": "Стиль",
"dot": "Точка",
"ring": "Кольцо",
"size": "Размер",
"onlyOnClicks": "Только при кликах",
"color": "Цвет",
"offsetX": "Смещение X (записи окон)",
"offsetY": "Смещение Y",
"accessibilityPermissionTitle": "Требуется разрешение на доступность",
"accessibilityPermissionDescription": "Откройте Системные настройки → Конфиденциальность и безопасность → Универсальный доступ, включите Openscreen, затем перезапустите приложение."
}
"padding": "Отступ"
},
"background": {
"title": "Фон",
-48
View File
@@ -33,14 +33,8 @@ import {
} from "@/components/video-editor/videoPlayback/constants";
import {
adaptiveSmoothFactor,
interpolateCursorAt,
smoothCursorFocus,
} from "@/components/video-editor/videoPlayback/cursorFollowUtils";
import {
type CursorHighlightConfig,
clickEmphasisAlpha,
drawCursorHighlightCanvas,
} from "@/components/video-editor/videoPlayback/cursorHighlight";
import { clampFocusToScale } from "@/components/video-editor/videoPlayback/focusUtils";
import { findDominantRegion } from "@/components/video-editor/videoPlayback/zoomRegionUtils";
import {
@@ -110,7 +104,6 @@ interface FrameRenderConfig {
previewWidth?: number;
previewHeight?: number;
cursorTelemetry?: import("@/components/video-editor/types").CursorTelemetryPoint[];
cursorHighlight?: CursorHighlightConfig;
cursorClickTimestamps?: number[];
platform: string;
}
@@ -450,47 +443,6 @@ export class FrameRenderer {
const willRotate = !isRotation3DIdentity(this.currentRotation3D);
this.compositeWithShadows(webcamFrame, !willRotate);
// Cursor highlight overlay (rendered above video, below annotations)
// Drawn onto foreground so it rotates with the recording.
if (
this.config.cursorHighlight?.enabled &&
this.config.cursorTelemetry &&
this.config.cursorTelemetry.length > 0 &&
this.foregroundCtx
) {
const emphasisAlpha = clickEmphasisAlpha(
timeMs,
this.config.cursorClickTimestamps,
this.config.cursorHighlight,
);
const cursorPoint =
emphasisAlpha > 0 ? interpolateCursorAt(this.config.cursorTelemetry, timeMs) : null;
if (cursorPoint) {
const cx = cursorPoint.cx + this.config.cursorHighlight.offsetXNorm;
const cy = cursorPoint.cy + this.config.cursorHighlight.offsetYNorm;
const stageX =
layoutCache.baseOffset.x + cx * this.config.videoWidth * layoutCache.baseScale;
const stageY =
layoutCache.baseOffset.y + cy * this.config.videoHeight * layoutCache.baseScale;
const appliedScale = this.animationState.appliedScale;
const canvasX = stageX * appliedScale + this.animationState.x;
const canvasY = stageY * appliedScale + this.animationState.y;
const previewW = this.config.previewWidth ?? this.config.width;
const previewH = this.config.previewHeight ?? this.config.height;
const cursorScale = (this.config.width / previewW + this.config.height / previewH) / 2;
drawCursorHighlightCanvas(
this.foregroundCtx,
canvasX,
canvasY,
{
...this.config.cursorHighlight,
opacity: this.config.cursorHighlight.opacity * emphasisAlpha,
},
appliedScale * cursorScale,
);
}
}
await this.drawNativeCursor(timeMs);
// Render annotations on top of foreground (so they rotate with recording).
-2
View File
@@ -57,7 +57,6 @@ interface GifExporterConfig {
previewWidth?: number;
previewHeight?: number;
cursorTelemetry?: import("@/components/video-editor/types").CursorTelemetryPoint[];
cursorHighlight?: import("@/components/video-editor/videoPlayback/cursorHighlight").CursorHighlightConfig;
cursorClickTimestamps?: number[];
onProgress?: (progress: ExportProgress) => void;
}
@@ -175,7 +174,6 @@ export class GifExporter {
previewHeight: this.config.previewHeight,
cursorTelemetry: this.config.cursorTelemetry,
cursorClickTimestamps: this.config.cursorClickTimestamps,
cursorHighlight: this.config.cursorHighlight,
platform,
});
await this.renderer.initialize();
-19
View File
@@ -106,25 +106,6 @@ describe("isSourceCopyFastPathEligible", () => {
videoInfo,
),
).toBe(false);
expect(
isSourceCopyFastPathEligible(
createConfig({
cursorHighlight: {
enabled: true,
style: "ring",
sizePx: 24,
color: "#ffffff",
opacity: 1,
onlyOnClicks: false,
clickEmphasisDurationMs: 350,
offsetXNorm: 0,
offsetYNorm: 0,
},
cursorTelemetry: [{ timeMs: 0, cx: 0.5, cy: 0.5 }],
}),
videoInfo,
),
).toBe(false);
});
});
-9
View File
@@ -48,7 +48,6 @@ export interface VideoExporterConfig extends ExportConfig {
previewWidth?: number;
previewHeight?: number;
cursorTelemetry?: import("@/components/video-editor/types").CursorTelemetryPoint[];
cursorHighlight?: import("@/components/video-editor/videoPlayback/cursorHighlight").CursorHighlightConfig;
cursorClickTimestamps?: number[];
onProgress?: (progress: ExportProgress) => void;
}
@@ -73,12 +72,6 @@ function hasNativeCursorOverlay(config: VideoExporterConfig) {
return (config.cursorScale ?? 0) > 0;
}
function hasCursorHighlightOverlay(config: VideoExporterConfig) {
return Boolean(
config.cursorHighlight?.enabled && config.cursorTelemetry && config.cursorTelemetry.length > 0,
);
}
function isDefaultCrop(cropRegion: CropRegion) {
return (
Math.abs(cropRegion.x) <= SOURCE_COPY_EPSILON &&
@@ -113,7 +106,6 @@ export function getSourceCopyFastPathBlockers(
if (hasActiveTimeRegions(config.annotationRegions))
blockers.push("annotation regions are present");
if (hasNativeCursorOverlay(config)) blockers.push("editable cursor overlay is enabled");
if (hasCursorHighlightOverlay(config)) blockers.push("cursor highlight overlay is enabled");
if (!isDefaultCrop(config.cropRegion)) blockers.push("crop is not default");
if ((config.padding ?? 0) > SOURCE_COPY_EPSILON) blockers.push("padding is not zero");
if ((config.videoPadding ?? 0) > SOURCE_COPY_EPSILON) blockers.push("video padding is not zero");
@@ -262,7 +254,6 @@ export class VideoExporter {
previewHeight: this.config.previewHeight,
cursorTelemetry: this.config.cursorTelemetry,
cursorClickTimestamps: this.config.cursorClickTimestamps,
cursorHighlight: this.config.cursorHighlight,
platform,
});
this.renderer = renderer;