diff --git a/electron-builder.json5 b/electron-builder.json5 index 40fce0a4..1702dfcd 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -38,13 +38,13 @@ ], "icon": "icons/icons/mac/icon.icns", "artifactName": "${productName}-Mac-${arch}-${version}-Installer.${ext}", - "extendInfo": { - "NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.", - "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.", - "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.", - "NSCameraUseContinuityCameraDeviceType": true, - "com.apple.security.device.audio-input": true - } + "extendInfo": { + "NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.", + "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.", + "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.", + "NSCameraUseContinuityCameraDeviceType": true, + "com.apple.security.device.audio-input": true + } }, "linux": { "target": [ @@ -54,14 +54,22 @@ "artifactName": "${productName}-Linux-${version}.${ext}", "category": "AudioVideo" }, - "win": { - "target": [ - "nsis" - ], - "icon": "icons/icons/win/icon.ico" - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true - } -} + "win": { + "target": [ + "nsis" + ], + "icon": "icons/icons/win/icon.ico", + "requestedExecutionLevel": "asInvoker", + "artifactName": "${productName}-Windows-${version}-Setup.${ext}" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": "always", + "createStartMenuShortcut": true, + "shortcutName": "Openscreen", + "installerIcon": "icons/icons/win/icon.ico", + "uninstallerIcon": "icons/icons/win/icon.ico", + "uninstallDisplayName": "Openscreen" + } +} diff --git a/electron/main.ts b/electron/main.ts index 7e19d468..7552ce14 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -18,6 +18,21 @@ import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Single instance lock +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + app.quit(); +} else { + app.on("second-instance", (_event, _commandLine, _workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + }); +} + // Use Screen & System Audio Recording permissions instead of CoreAudio Tap API on macOS. // CoreAudio Tap requires NSAudioCaptureUsageDescription in the parent app's Info.plist, // which doesn't work when running from a terminal/IDE during development, makes my life easier @@ -25,6 +40,11 @@ if (process.platform === "darwin") { app.commandLine.appendSwitch("disable-features", "MacCatapLoopbackAudioForScreenShare"); } +if (process.platform === "win32") { + app.setAppUserModelId("com.siddharthvaddem.openscreen"); + app.commandLine.appendSwitch("high-dpi-support", "1"); +} + export const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { diff --git a/package-lock.json b/package-lock.json index fdbd6b92..ca139aa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -194,6 +194,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -422,6 +423,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -738,6 +740,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -786,6 +789,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1549,7 +1553,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1571,7 +1574,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1588,7 +1590,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1603,7 +1604,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2305,6 +2305,7 @@ "integrity": "sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/core": "^0.16.13" @@ -2347,6 +2348,7 @@ "integrity": "sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2361,6 +2363,7 @@ "integrity": "sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2389,6 +2392,7 @@ "integrity": "sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13", @@ -2438,6 +2442,7 @@ "integrity": "sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2581,6 +2586,7 @@ "integrity": "sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2595,6 +2601,7 @@ "integrity": "sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -2612,6 +2619,7 @@ "integrity": "sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.2", "@jimp/utils": "^0.16.13" @@ -3038,7 +3046,6 @@ "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" } @@ -3053,8 +3060,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz", "integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@pixi/core": { "version": "7.4.3", @@ -3081,8 +3087,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz", "integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@pixi/filter-drop-shadow": { "version": "5.2.0", @@ -3109,22 +3114,19 @@ "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", - "peer": true + "license": "MIT" }, "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", - "peer": true + "license": "MIT" }, "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", @@ -3136,7 +3138,6 @@ "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", @@ -3148,7 +3149,6 @@ "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", @@ -3163,22 +3163,19 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@pixi/utils/node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@pixi/utils/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -4599,8 +4596,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4812,6 +4808,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4823,6 +4820,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5131,6 +5129,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5971,6 +5970,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6283,7 +6283,6 @@ "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" @@ -6733,8 +6732,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -7082,6 +7080,7 @@ "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.7.0", "builder-util": "26.4.1", @@ -7174,8 +7173,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-walk": { "version": "0.1.2", @@ -7516,7 +7514,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -7537,7 +7534,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9253,6 +9249,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10073,7 +10070,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10907,7 +10903,6 @@ "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" }, @@ -11453,6 +11448,7 @@ "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz", "integrity": "sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==", "license": "MIT", + "peer": true, "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", @@ -11566,6 +11562,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11710,7 +11707,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11728,7 +11724,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -11739,7 +11734,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11755,7 +11749,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11768,8 +11761,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/proc-log": { "version": "5.0.0", @@ -11915,7 +11907,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -11974,6 +11965,7 @@ "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" }, @@ -11986,6 +11978,7 @@ "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" @@ -12813,7 +12806,6 @@ "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", @@ -12833,7 +12825,6 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -12850,7 +12841,6 @@ "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", @@ -12869,7 +12859,6 @@ "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", @@ -13537,6 +13526,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13609,7 +13599,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -13673,7 +13662,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -13688,7 +13676,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -13702,6 +13689,7 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13854,6 +13842,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14130,7 +14119,6 @@ "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" @@ -14143,8 +14131,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/use-callback-ref": { "version": "1.3.3", @@ -14258,6 +14245,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14332,7 +14320,8 @@ "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" + "license": "MIT", + "peer": true }, "node_modules/vitest": { "version": "4.0.16", @@ -14896,6 +14885,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14909,6 +14899,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index a27fbb9f..ad3cb05a 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -640,14 +640,14 @@ export default function VideoEditor() { ); const handleZoomSuggested = useCallback( - (span: Span, focus: ZoomFocus) => { + (span: Span, focus: ZoomFocus, depth: number) => { const id = `zoom-${nextZoomIdRef.current++}`; const newRegion: ZoomRegion = { id, startMs: Math.round(span.start), endMs: Math.round(span.end), - depth: DEFAULT_ZOOM_DEPTH, - focus: clampFocusToDepth(focus, DEFAULT_ZOOM_DEPTH), + depth: depth, + focus: clampFocusToDepth(focus, depth), }; pushState((prev) => ({ zoomRegions: [...prev.zoomRegions, newRegion] })); setSelectedZoomId(id); @@ -1654,6 +1654,8 @@ export default function VideoEditor() { onAspectRatioChange={(ar) => pushState({ aspectRatio: ar, + padding: ar === "native" ? 0 : padding, + borderRadius: ar === "native" ? 0 : borderRadius, webcamLayoutPreset: !isPortraitAspectRatio(ar) && webcamLayoutPreset === "vertical-stack" ? "picture-in-picture" diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index b64aad0b..5de50c67 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -56,7 +56,7 @@ interface TimelineEditorProps { cursorTelemetry?: CursorTelemetryPoint[]; zoomRegions: ZoomRegion[]; onZoomAdded: (span: Span) => void; - onZoomSuggested?: (span: Span, focus: ZoomFocus) => void; + onZoomSuggested?: (span: Span, focus: ZoomFocus, depth: number) => void; onZoomSpanChange: (id: string, span: Span) => void; onZoomDelete: (id: string) => void; selectedZoomId: string | null; @@ -1037,9 +1037,13 @@ export default function TimelineEditor({ return; } - const centeredStart = Math.round(candidate.centerTimeMs - defaultDuration / 2); - const candidateStart = Math.max(0, Math.min(centeredStart, totalMs - defaultDuration)); - const candidateEnd = candidateStart + defaultDuration; + const candidateStart = Math.max(0, Math.min(candidate.startTimeMs, totalMs - 100)); + const candidateEnd = Math.min(candidate.endTimeMs, totalMs); + + if (candidateEnd - candidateStart < 200) { + return; + } + const hasOverlap = reservedSpans.some( (span) => candidateEnd > span.start && candidateStart < span.end, ); @@ -1050,7 +1054,11 @@ export default function TimelineEditor({ reservedSpans.push({ start: candidateStart, end: candidateEnd }); acceptedCenters.push(candidate.centerTimeMs); - onZoomSuggested({ start: candidateStart, end: candidateEnd }, candidate.focus); + onZoomSuggested( + { start: candidateStart, end: candidateEnd }, + candidate.focus, + candidate.depth, + ); addedCount += 1; }); diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.ts index 9f807d32..7fe9442a 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.ts @@ -1,13 +1,18 @@ import type { CursorTelemetryPoint, ZoomFocus } from "../types"; -export const MIN_DWELL_DURATION_MS = 450; -export const MAX_DWELL_DURATION_MS = 2600; -export const DWELL_MOVE_THRESHOLD = 0.02; +export const MIN_DWELL_DURATION_MS = 600; // Increased to require more intent +export const MAX_DWELL_DURATION_MS = 5000; +export const DWELL_MOVE_THRESHOLD = 0.025; // Higher tolerance for tremors in WQHD +export const GROUPING_THRESHOLD_MS = 1000; +export const ZOOM_REACTION_DELAY_MS = 300; // Zoom starts AFTER you stopped export interface ZoomDwellCandidate { - centerTimeMs: number; + centerTimeMs: number; // Kept for interface compatibility, but ignored in new logic + startTimeMs: number; + endTimeMs: number; focus: ZoomFocus; strength: number; + depth: number; } function normalizeTelemetrySample( @@ -34,48 +39,106 @@ export function normalizeCursorTelemetry( .map((sample) => normalizeTelemetrySample(sample, totalMs)); } +function calculateAdaptiveDepth(cx: number, cy: number, durationMs: number): number { + const distFromCenter = Math.hypot(cx - 0.5, cy - 0.5); + let depth = 2; + if (distFromCenter < 0.2) depth += 1; + if (durationMs > 2000) depth += 1; + return Math.min(4, depth); +} + export function detectZoomDwellCandidates(samples: CursorTelemetryPoint[]): ZoomDwellCandidate[] { - if (samples.length < 2) { - return []; - } + if (samples.length < 2) return []; - const dwellCandidates: ZoomDwellCandidate[] = []; + const rawDwells: ZoomDwellCandidate[] = []; let runStart = 0; - const pushRunIfDwell = (startIndex: number, endIndexExclusive: number) => { - if (endIndexExclusive - startIndex < 2) { - return; - } + for (let index = 1; index < samples.length; index += 1) { + const prev = samples[index - 1]; + const curr = samples[index]; + const distance = Math.hypot(curr.cx - prev.cx, curr.cy - prev.cy); - const start = samples[startIndex]; - const end = samples[endIndexExclusive - 1]; - const runDuration = end.timeMs - start.timeMs; - if (runDuration < MIN_DWELL_DURATION_MS || runDuration > MAX_DWELL_DURATION_MS) { - return; + if (distance > DWELL_MOVE_THRESHOLD) { + const start = samples[runStart]; + const end = samples[index - 1]; + const duration = end.timeMs - start.timeMs; + + if (duration >= MIN_DWELL_DURATION_MS) { + const runSamples = samples.slice(runStart, index); + const avgCx = runSamples.reduce((sum, s) => sum + s.cx, 0) / runSamples.length; + const avgCy = runSamples.reduce((sum, s) => sum + s.cy, 0) / runSamples.length; + + rawDwells.push({ + startTimeMs: start.timeMs + ZOOM_REACTION_DELAY_MS, // Applies reaction delay + endTimeMs: end.timeMs, + centerTimeMs: Math.round((start.timeMs + end.timeMs) / 2), + focus: { cx: avgCx, cy: avgCy }, + strength: duration, + depth: calculateAdaptiveDepth(avgCx, avgCy, duration), + }); + } + runStart = index; } + } + + // Flush the last run if it's a valid dwell + const lastStart = samples[runStart]; + const lastEnd = samples[samples.length - 1]; + const lastDuration = lastEnd.timeMs - lastStart.timeMs; - const runSamples = samples.slice(startIndex, endIndexExclusive); - const avgCx = runSamples.reduce((sum, sample) => sum + sample.cx, 0) / runSamples.length; - const avgCy = runSamples.reduce((sum, sample) => sum + sample.cy, 0) / runSamples.length; + if (lastDuration >= MIN_DWELL_DURATION_MS) { + const runSamples = samples.slice(runStart); + const avgCx = runSamples.reduce((sum, s) => sum + s.cx, 0) / runSamples.length; + const avgCy = runSamples.reduce((sum, s) => sum + s.cy, 0) / runSamples.length; - dwellCandidates.push({ - centerTimeMs: Math.round((start.timeMs + end.timeMs) / 2), + rawDwells.push({ + startTimeMs: lastStart.timeMs + ZOOM_REACTION_DELAY_MS, + endTimeMs: lastEnd.timeMs, + centerTimeMs: Math.round((lastStart.timeMs + lastEnd.timeMs) / 2), focus: { cx: avgCx, cy: avgCy }, - strength: runDuration, + strength: lastDuration, + depth: calculateAdaptiveDepth(avgCx, avgCy, lastDuration), }); - }; + } - for (let index = 1; index < samples.length; index += 1) { - const prev = samples[index - 1]; - const curr = samples[index]; - const distance = Math.hypot(curr.cx - prev.cx, curr.cy - prev.cy); + if (rawDwells.length === 0) return []; - if (distance > DWELL_MOVE_THRESHOLD) { - pushRunIfDwell(runStart, index); - runStart = index; + const groupedDwells: ZoomDwellCandidate[] = []; + let currentGroup: ZoomDwellCandidate[] = [rawDwells[0]]; + + for (let i = 1; i < rawDwells.length; i++) { + const prev = rawDwells[i - 1]; + const curr = rawDwells[i]; + + if (curr.startTimeMs - prev.endTimeMs < GROUPING_THRESHOLD_MS) { + currentGroup.push(curr); + } else { + groupedDwells.push(mergeDwellGroup(currentGroup)); + currentGroup = [curr]; } } - pushRunIfDwell(runStart, samples.length); + groupedDwells.push(mergeDwellGroup(currentGroup)); - return dwellCandidates; + return groupedDwells; +} + +function mergeDwellGroup(group: ZoomDwellCandidate[]): ZoomDwellCandidate { + if (group.length === 1) return group[0]; + + const totalStrength = group.reduce((sum, d) => sum + d.strength, 0); + const startTimeMs = group[0].startTimeMs; + const endTimeMs = group[group.length - 1].endTimeMs; + + const avgCx = group.reduce((sum, d) => sum + d.focus.cx * d.strength, 0) / totalStrength; + const avgCy = group.reduce((sum, d) => sum + d.focus.cy * d.strength, 0) / totalStrength; + const maxDepth = Math.max(...group.map((d) => d.depth)); + + return { + startTimeMs, + endTimeMs, + centerTimeMs: Math.round((startTimeMs + endTimeMs) / 2), + focus: { cx: avgCx, cy: avgCy }, + strength: totalStrength, + depth: maxDepth, + }; } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 2b07e247..da7c96f9 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -6,13 +6,15 @@ import { requestCameraAccess } from "@/lib/requestCameraAccess"; const TARGET_FRAME_RATE = 60; const MIN_FRAME_RATE = 30; -const TARGET_WIDTH = 3840; -const TARGET_HEIGHT = 2160; -const FOUR_K_PIXELS = TARGET_WIDTH * TARGET_HEIGHT; +const TARGET_WIDTH = 7680; +const TARGET_HEIGHT = 4320; +const EIGHT_K_PIXELS = TARGET_WIDTH * TARGET_HEIGHT; +const FOUR_K_PIXELS = 3840 * 2160; const QHD_WIDTH = 2560; const QHD_HEIGHT = 1440; const QHD_PIXELS = QHD_WIDTH * QHD_HEIGHT; +const BITRATE_8K = 80_000_000; const BITRATE_4K = 45_000_000; const BITRATE_QHD = 28_000_000; const BITRATE_BASE = 18_000_000; @@ -134,6 +136,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const highFrameRateBoost = TARGET_FRAME_RATE >= HIGH_FRAME_RATE_THRESHOLD ? HIGH_FRAME_RATE_BOOST : 1; + if (pixels >= EIGHT_K_PIXELS) { + return Math.round(BITRATE_8K * highFrameRateBoost); + } + if (pixels >= FOUR_K_PIXELS) { return Math.round(BITRATE_4K * highFrameRateBoost); }