mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
fix: address native capture review feedback
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types";
|
||||
import {
|
||||
isCursorCaptureActive,
|
||||
interactionCaptureCleanup,
|
||||
setInteractionCaptureCleanup,
|
||||
hasLoggedInteractionHookFailure,
|
||||
setHasLoggedInteractionHookFailure,
|
||||
interactionCaptureCleanup,
|
||||
isCursorCaptureActive,
|
||||
lastLeftClick,
|
||||
setHasLoggedInteractionHookFailure,
|
||||
setInteractionCaptureCleanup,
|
||||
setLastLeftClick,
|
||||
setLinuxCursorScreenPoint,
|
||||
} from "../state";
|
||||
import type {
|
||||
CursorInteractionType,
|
||||
HookMouseEvent,
|
||||
UiohookLike,
|
||||
UiohookModuleNamespace,
|
||||
} from "../types";
|
||||
import {
|
||||
getNormalizedCursorPoint,
|
||||
getCursorCaptureElapsedMs,
|
||||
getHookCursorScreenPoint,
|
||||
getNormalizedCursorPoint,
|
||||
isCursorCapturePaused,
|
||||
pushCursorSample,
|
||||
} from "./telemetry";
|
||||
@@ -182,6 +187,52 @@ export function shouldStartGlobalInteractionHook(platform: NodeJS.Platform = pro
|
||||
return platform !== "darwin";
|
||||
}
|
||||
|
||||
export function recordCursorMouseDown(button: 1 | 2 | 3) {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = getNormalizedCursorPoint();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeMs = getCursorCaptureElapsedMs();
|
||||
let interactionType: CursorInteractionType = "click";
|
||||
|
||||
if (button === 2) {
|
||||
interactionType = "right-click";
|
||||
} else if (button === 3) {
|
||||
interactionType = "middle-click";
|
||||
} else {
|
||||
const thresholdMs = 350;
|
||||
const distance = lastLeftClick
|
||||
? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
if (lastLeftClick && timeMs - lastLeftClick.timeMs <= thresholdMs && distance <= 0.04) {
|
||||
interactionType = "double-click";
|
||||
}
|
||||
|
||||
setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy });
|
||||
}
|
||||
|
||||
pushCursorSample(point.cx, point.cy, timeMs, interactionType);
|
||||
}
|
||||
|
||||
export function recordCursorMouseUp() {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = getNormalizedCursorPoint();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "mouseup");
|
||||
}
|
||||
|
||||
export async function startInteractionCapture() {
|
||||
if (!isCursorCaptureActive) {
|
||||
return;
|
||||
@@ -192,9 +243,7 @@ export async function startInteractionCapture() {
|
||||
}
|
||||
|
||||
if (!shouldStartGlobalInteractionHook()) {
|
||||
console.warn(
|
||||
"[CursorTelemetry] Skipping the blocking global interaction hook on macOS.",
|
||||
);
|
||||
console.warn("[CursorTelemetry] Skipping the blocking global interaction hook on macOS.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,63 +269,15 @@ export async function startInteractionCapture() {
|
||||
}
|
||||
|
||||
const onMouseDown = (event: HookMouseEvent) => {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = getNormalizedCursorPoint();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeMs = getCursorCaptureElapsedMs();
|
||||
const button = getHookMouseButton(event);
|
||||
let interactionType: CursorInteractionType = "click";
|
||||
|
||||
if (button === 2) {
|
||||
interactionType = "right-click";
|
||||
} else if (button === 3) {
|
||||
interactionType = "middle-click";
|
||||
} else {
|
||||
const thresholdMs = 350;
|
||||
const distance = lastLeftClick
|
||||
? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
if (
|
||||
lastLeftClick &&
|
||||
timeMs - lastLeftClick.timeMs <= thresholdMs &&
|
||||
distance <= 0.04
|
||||
) {
|
||||
interactionType = "double-click";
|
||||
}
|
||||
|
||||
setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy });
|
||||
}
|
||||
|
||||
pushCursorSample(point.cx, point.cy, timeMs, interactionType);
|
||||
recordCursorMouseDown(getHookMouseButton(event));
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
if (!isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = getNormalizedCursorPoint();
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeMs = getCursorCaptureElapsedMs();
|
||||
pushCursorSample(point.cx, point.cy, timeMs, "mouseup");
|
||||
recordCursorMouseUp();
|
||||
};
|
||||
|
||||
const onMouseMove = (event: HookMouseEvent) => {
|
||||
if (
|
||||
process.platform !== "linux" ||
|
||||
!isCursorCaptureActive ||
|
||||
isCursorCapturePaused()
|
||||
) {
|
||||
if (process.platform !== "linux" || !isCursorCaptureActive || isCursorCapturePaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { BrowserWindow } from "electron";
|
||||
import type { CursorVisualType } from "../types";
|
||||
import { ensureNativeCursorMonitorBinary, getCursorMonitorExePath } from "../paths/binaries";
|
||||
import {
|
||||
currentCursorVisualType,
|
||||
nativeCursorMonitorOutputBuffer,
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
setNativeCursorMonitorOutputBuffer,
|
||||
setNativeCursorMonitorProcess,
|
||||
} from "../state";
|
||||
import { getCursorMonitorExePath, ensureNativeCursorMonitorBinary } from "../paths/binaries";
|
||||
import type { CursorVisualType } from "../types";
|
||||
import { recordCursorMouseDown, recordCursorMouseUp } from "./interaction";
|
||||
|
||||
export function emitCursorStateChanged(cursorType: CursorVisualType) {
|
||||
BrowserWindow.getAllWindows().forEach((window) => {
|
||||
@@ -27,6 +28,17 @@ export function handleCursorMonitorStdout(chunk: Buffer) {
|
||||
setNativeCursorMonitorOutputBuffer(lines.pop() ?? "");
|
||||
|
||||
for (const line of lines) {
|
||||
const interactionMatch = line.match(/^INTERACTION:(mousedown|mouseup)(?::([123]))?$/);
|
||||
if (interactionMatch) {
|
||||
if (interactionMatch[1] === "mouseup") {
|
||||
recordCursorMouseUp();
|
||||
} else {
|
||||
const button = Number(interactionMatch[2]);
|
||||
recordCursorMouseDown(button === 2 || button === 3 ? button : 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = line.match(/^STATE:(.+)$/);
|
||||
if (!match) continue;
|
||||
const next = match[1].trim() as CursorVisualType;
|
||||
|
||||
@@ -406,6 +406,70 @@ if CommandLine.arguments.contains("--export-images") {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
func mouseInteractionCallback(
|
||||
proxy: CGEventTapProxy,
|
||||
type: CGEventType,
|
||||
event: CGEvent,
|
||||
refcon: UnsafeMutableRawPointer?
|
||||
) -> Unmanaged<CGEvent>? {
|
||||
let action: String
|
||||
let button: Int
|
||||
switch type {
|
||||
case .leftMouseDown:
|
||||
action = "mousedown"
|
||||
button = 1
|
||||
case .leftMouseUp:
|
||||
action = "mouseup"
|
||||
button = 1
|
||||
case .rightMouseDown:
|
||||
action = "mousedown"
|
||||
button = 2
|
||||
case .rightMouseUp:
|
||||
action = "mouseup"
|
||||
button = 2
|
||||
case .otherMouseDown:
|
||||
action = "mousedown"
|
||||
button = 3
|
||||
case .otherMouseUp:
|
||||
action = "mouseup"
|
||||
button = 3
|
||||
default:
|
||||
return Unmanaged.passUnretained(event)
|
||||
}
|
||||
|
||||
print("INTERACTION:\(action):\(button)")
|
||||
fflush(stdout)
|
||||
return Unmanaged.passUnretained(event)
|
||||
}
|
||||
|
||||
let mouseEventTypes: [CGEventType] = [
|
||||
.leftMouseDown,
|
||||
.leftMouseUp,
|
||||
.rightMouseDown,
|
||||
.rightMouseUp,
|
||||
.otherMouseDown,
|
||||
.otherMouseUp,
|
||||
]
|
||||
let mouseEventMask = mouseEventTypes.reduce(CGEventMask(0)) { mask, type in
|
||||
mask | (CGEventMask(1) << type.rawValue)
|
||||
}
|
||||
let mouseEventTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .listenOnly,
|
||||
eventsOfInterest: mouseEventMask,
|
||||
callback: mouseInteractionCallback,
|
||||
userInfo: nil
|
||||
)
|
||||
if let mouseEventTap,
|
||||
let eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0) {
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), eventTapSource, .commonModes)
|
||||
CGEvent.tapEnable(tap: mouseEventTap, enable: true)
|
||||
} else {
|
||||
fputs("Mouse interaction event tap unavailable; click telemetry disabled\n", stderr)
|
||||
fflush(stderr)
|
||||
}
|
||||
|
||||
var lastState = ""
|
||||
func emitStateIfNeeded() {
|
||||
let state = currentSystemCursorType()
|
||||
@@ -433,4 +497,3 @@ DispatchQueue.global(qos: .utility).async {
|
||||
}
|
||||
|
||||
RunLoop.main.run()
|
||||
|
||||
|
||||
@@ -436,7 +436,14 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
await microphoneOnlyWriter.finishWriting()
|
||||
}
|
||||
|
||||
let finalizeFailure: Error? = assetWriter.flatMap { $0.status == .completed ? nil : ($0.error ?? unfinalizedWriterError(status: $0.status)) }
|
||||
let finalizeFailure: Error? = [assetWriter, systemAudioWriter, microphoneOnlyWriter]
|
||||
.compactMap { $0 }
|
||||
.compactMap { writer in
|
||||
writer.status == .completed
|
||||
? nil
|
||||
: (writer.error ?? unfinalizedWriterError(status: writer.status))
|
||||
}
|
||||
.first
|
||||
let path = outputURL?.path ?? ""
|
||||
assetWriter = nil
|
||||
videoInput = nil
|
||||
@@ -676,6 +683,7 @@ final class RecorderService {
|
||||
private let recorder = ScreenCaptureRecorder()
|
||||
private let queue = DispatchQueue(label: "recordly.screencapturekit.commands")
|
||||
private let completionGroup = DispatchGroup()
|
||||
private var succeeded = true
|
||||
|
||||
private func enqueue(_ operation: @escaping () async -> Void) {
|
||||
queue.async {
|
||||
@@ -694,6 +702,7 @@ final class RecorderService {
|
||||
do {
|
||||
try await self.recorder.startCapture(configJSON: configJSON)
|
||||
} catch {
|
||||
self.succeeded = false
|
||||
fputs("Error starting capture: \(error.localizedDescription)\n", stderr)
|
||||
fflush(stderr)
|
||||
self.completionGroup.leave()
|
||||
@@ -709,6 +718,7 @@ final class RecorderService {
|
||||
fflush(stdout)
|
||||
self.completionGroup.leave()
|
||||
} catch {
|
||||
self.succeeded = false
|
||||
fputs("Error stopping capture: \(error.localizedDescription)\n", stderr)
|
||||
fflush(stderr)
|
||||
self.completionGroup.leave()
|
||||
@@ -734,8 +744,9 @@ final class RecorderService {
|
||||
}
|
||||
}
|
||||
|
||||
func waitUntilFinished() {
|
||||
func waitUntilFinished() -> Bool {
|
||||
completionGroup.wait()
|
||||
return succeeded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,4 +819,6 @@ DispatchQueue.global(qos: .utility).async {
|
||||
}
|
||||
}
|
||||
|
||||
service.waitUntilFinished()
|
||||
if !service.waitUntilFinished() {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1470,14 +1470,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
const { selectedSource, useNativeMacScreenCapture, useNativeWindowsCapture, micLabel } =
|
||||
preparedStart;
|
||||
const useNativeCapture =
|
||||
useNativeMacScreenCapture || useNativeWindowsCapture;
|
||||
const useNativeCapture = useNativeMacScreenCapture || useNativeWindowsCapture;
|
||||
const shouldWarmStartNativeCapture = useNativeCapture && countdownDelay > 0;
|
||||
if (countdownDelay > 0 && !shouldWarmStartNativeCapture) {
|
||||
setCountdownActive(true);
|
||||
try {
|
||||
const result = await window.electronAPI.startCountdown(countdownDelay);
|
||||
if (!result.success || result.cancelled) {
|
||||
cleanupCapturedMedia();
|
||||
await stopWebcamRecorder();
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user