mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 15:55:35 +00:00
170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import { getTelemetryPathForVideo, getScreen } from "../utils";
|
|
import {
|
|
CURSOR_TELEMETRY_VERSION,
|
|
MAX_CURSOR_SAMPLES,
|
|
CURSOR_SAMPLE_INTERVAL_MS,
|
|
} from "../constants";
|
|
import type { CursorVisualType, CursorInteractionType, CursorTelemetryPoint } from "../types";
|
|
import {
|
|
cursorCaptureInterval,
|
|
setCursorCaptureInterval,
|
|
cursorCaptureStartTimeMs,
|
|
activeCursorSamples,
|
|
pendingCursorSamples,
|
|
setPendingCursorSamples,
|
|
isCursorCaptureActive,
|
|
currentCursorVisualType,
|
|
linuxCursorScreenPoint,
|
|
selectedSource,
|
|
selectedWindowBounds,
|
|
} from "../state";
|
|
|
|
export function clamp(value: number, min: number, max: number) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
export function stopCursorCapture() {
|
|
if (cursorCaptureInterval) {
|
|
clearInterval(cursorCaptureInterval);
|
|
setCursorCaptureInterval(null);
|
|
}
|
|
}
|
|
|
|
export function getNormalizedCursorPoint() {
|
|
const fallbackCursor = getScreen().getCursorScreenPoint();
|
|
const linuxCursorCache = process.platform === "linux" ? linuxCursorScreenPoint : null;
|
|
const isLinuxCacheFresh = !!linuxCursorCache && Date.now() - linuxCursorCache.updatedAt <= 1000;
|
|
|
|
const primarySf =
|
|
process.platform !== "darwin" ? getScreen().getPrimaryDisplay().scaleFactor || 1 : 1;
|
|
|
|
const cursor = isLinuxCacheFresh
|
|
? { x: linuxCursorCache.x / primarySf, y: linuxCursorCache.y / primarySf }
|
|
: fallbackCursor;
|
|
|
|
const windowBounds = selectedSource?.id?.startsWith("window:") ? selectedWindowBounds : null;
|
|
if (windowBounds) {
|
|
const sf =
|
|
process.platform !== "darwin"
|
|
? getScreen().getDisplayNearestPoint({
|
|
x: windowBounds.x / primarySf,
|
|
y: windowBounds.y / primarySf,
|
|
}).scaleFactor || 1
|
|
: 1;
|
|
const width = Math.max(1, windowBounds.width / sf);
|
|
const height = Math.max(1, windowBounds.height / sf);
|
|
|
|
return {
|
|
cx: clamp((cursor.x - windowBounds.x / sf) / width, 0, 1),
|
|
cy: clamp((cursor.y - windowBounds.y / sf) / height, 0, 1),
|
|
};
|
|
}
|
|
|
|
const sourceDisplayId = Number(selectedSource?.display_id);
|
|
const sourceDisplay = Number.isFinite(sourceDisplayId)
|
|
? (getScreen()
|
|
.getAllDisplays()
|
|
.find((display) => display.id === sourceDisplayId) ?? null)
|
|
: null;
|
|
const display = sourceDisplay ?? getScreen().getDisplayNearestPoint(cursor);
|
|
const bounds = display.bounds;
|
|
const width = Math.max(1, bounds.width);
|
|
const height = Math.max(1, bounds.height);
|
|
|
|
const cx = clamp((cursor.x - bounds.x) / width, 0, 1);
|
|
const cy = clamp((cursor.y - bounds.y) / height, 0, 1);
|
|
return { cx, cy };
|
|
}
|
|
|
|
export function getHookCursorScreenPoint(
|
|
event: { x?: number; y?: number; data?: { x?: number; y?: number; screenX?: number; screenY?: number }; screenX?: number; screenY?: number } | null | undefined,
|
|
): { x: number; y: number } | null {
|
|
const rawX = event?.x ?? event?.data?.x ?? event?.screenX ?? event?.data?.screenX;
|
|
const rawY = event?.y ?? event?.data?.y ?? event?.screenY ?? event?.data?.screenY;
|
|
|
|
if (
|
|
typeof rawX !== "number" ||
|
|
!Number.isFinite(rawX) ||
|
|
typeof rawY !== "number" ||
|
|
!Number.isFinite(rawY)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return { x: rawX, y: rawY };
|
|
}
|
|
|
|
export function pushCursorSample(
|
|
cx: number,
|
|
cy: number,
|
|
timeMs: number,
|
|
interactionType: CursorInteractionType = "move",
|
|
cursorType?: CursorVisualType,
|
|
) {
|
|
activeCursorSamples.push({
|
|
timeMs: Math.max(0, timeMs),
|
|
cx,
|
|
cy,
|
|
interactionType,
|
|
cursorType: cursorType ?? currentCursorVisualType,
|
|
} as CursorTelemetryPoint);
|
|
|
|
if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) {
|
|
activeCursorSamples.shift();
|
|
}
|
|
}
|
|
|
|
export function sampleCursorPoint() {
|
|
const point = getNormalizedCursorPoint();
|
|
pushCursorSample(point.cx, point.cy, Date.now() - cursorCaptureStartTimeMs, "move");
|
|
}
|
|
|
|
export async function persistPendingCursorTelemetry(videoPath: string) {
|
|
const telemetryPath = getTelemetryPathForVideo(videoPath);
|
|
if (pendingCursorSamples.length > 0) {
|
|
await fs.writeFile(
|
|
telemetryPath,
|
|
JSON.stringify(
|
|
{ version: CURSOR_TELEMETRY_VERSION, samples: pendingCursorSamples },
|
|
null,
|
|
2,
|
|
),
|
|
"utf-8",
|
|
);
|
|
}
|
|
setPendingCursorSamples([]);
|
|
}
|
|
|
|
export function snapshotCursorTelemetryForPersistence() {
|
|
if (activeCursorSamples.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (pendingCursorSamples.length === 0) {
|
|
setPendingCursorSamples([...activeCursorSamples]);
|
|
return;
|
|
}
|
|
|
|
const lastPendingTimeMs = pendingCursorSamples[pendingCursorSamples.length - 1]?.timeMs ?? -1;
|
|
setPendingCursorSamples([
|
|
...pendingCursorSamples,
|
|
...activeCursorSamples.filter((sample) => sample.timeMs > lastPendingTimeMs),
|
|
]);
|
|
}
|
|
|
|
export function startCursorSampling() {
|
|
stopCursorCapture();
|
|
setCursorCaptureInterval(
|
|
setInterval(() => {
|
|
if (isCursorCaptureActive) {
|
|
sampleCursorPoint();
|
|
}
|
|
}, CURSOR_SAMPLE_INTERVAL_MS),
|
|
);
|
|
}
|
|
|
|
// Re-export for consumers that use it from this module
|
|
export { getTelemetryPathForVideo } from "../utils";
|
|
export { CURSOR_SAMPLE_INTERVAL_MS } from "../constants";
|