Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import type { DesktopCapturerSource } from "electron";
import type { DesktopCapturerSource, Rectangle } from "electron";
import {
app,
BrowserWindow,
Expand Down Expand Up @@ -424,6 +424,8 @@ let nativeMacCursorRecordingStartMs = 0;
let nativeMacPauseStartedAtMs: number | null = null;
let nativeMacPauseRanges: Array<{ startMs: number; endMs: number }> = [];
let nativeMacIsPaused = false;
// Global frame of the region captured by the SCK helper (see getSelectedSourceBounds).
let activeMacCaptureBounds: Rectangle | null = null;
Comment thread
EtienneLescot marked this conversation as resolved.

function normalizeCursorSample(sample: unknown): CursorRecordingSample | null {
if (!sample || typeof sample !== "object") {
Expand Down Expand Up @@ -573,6 +575,14 @@ function resolveAssetBasePath() {
}

function getSelectedSourceBounds() {
// Single-window capture records only the window's region, not the whole display.
// Normalizing the cursor against display bounds leaves a fixed offset in the export,
// so prefer the helper-reported window frame when capturing a window.
const isWindowSource = selectedSource?.id?.startsWith("window:") === true;
if (isWindowSource && activeMacCaptureBounds) {
return activeMacCaptureBounds;
}

const cursor = screen.getCursorScreenPoint();
const sourceDisplayId = Number(selectedSource?.display_id);
const sourceDisplay = Number.isFinite(sourceDisplayId)
Expand Down Expand Up @@ -1039,11 +1049,19 @@ function tryParseNativeHelperEvent(line: string) {
}
}

function dispatchNativeMacHelperEvent(event: Record<string, unknown>) {
Comment thread
EtienneLescot marked this conversation as resolved.
const bounds = event.captureBounds as Rectangle | undefined;
if (bounds && bounds.width > 0 && bounds.height > 0) {
activeMacCaptureBounds = bounds;
}
nativeMacCaptureEvents.emit("helper-event", event);
}

function inspectNativeMacCaptureOutput() {
for (const line of nativeMacCaptureOutput.split(/\r?\n/)) {
const event = tryParseNativeHelperEvent(line.trim());
if (event) {
nativeMacCaptureEvents.emit("helper-event", event);
dispatchNativeMacHelperEvent(event);
}
}
}
Expand All @@ -1059,7 +1077,7 @@ function attachNativeMacCaptureOutputDrain(proc: ChildProcessWithoutNullStreams)
for (const line of lines) {
const event = tryParseNativeHelperEvent(line.trim());
if (event) {
nativeMacCaptureEvents.emit("helper-event", event);
dispatchNativeMacHelperEvent(event);
}
}
};
Expand Down Expand Up @@ -1817,6 +1835,7 @@ export function registerIpcHandlers(
nativeMacPauseStartedAtMs = null;
nativeMacPauseRanges = [];
nativeMacIsPaused = false;
Comment thread
EtienneLescot marked this conversation as resolved.
activeMacCaptureBounds = null;

const cursorStartTimeMs = Date.now();
if (cursorCaptureMode === "editable-overlay") {
Expand Down Expand Up @@ -2132,6 +2151,7 @@ export function registerIpcHandlers(
nativeMacPauseStartedAtMs = null;
nativeMacPauseRanges = [];
nativeMacIsPaused = false;
activeMacCaptureBounds = null;
const source = selectedSource || { name: "Screen" };
if (onRecordingStateChange) {
onRecordingStateChange(false, source.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
let filter: SCContentFilter
let width: Int
let height: Int
// Global frame (points, top-left origin) of the captured region. Used by the
// renderer to normalize cursor positions into the captured window's space.
let captureFrame: CGRect
}

private let request: RecordingRequest
Expand All @@ -143,6 +146,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
private var nativeMicrophoneEnabled = false
private var outputWidth = 1920
private var outputHeight = 1080
private var captureFrame = CGRect.zero
private let microphoneOutputTypeRawValue = 2
private let hostClock = CMClockGetHostTimeClock()

Expand All @@ -160,6 +164,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
let target = try makeCaptureTarget(from: content)
outputWidth = target.width
outputHeight = target.height
captureFrame = target.captureFrame
let configuration = makeStreamConfiguration()
let stream = SCStream(filter: target.filter, configuration: configuration, delegate: self)

Expand All @@ -178,7 +183,10 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
try setupWriter()

Comment thread
EtienneLescot marked this conversation as resolved.
self.stream = stream
emit(["event": "ready", "schemaVersion": 1])
emit([
"event": "ready",
"schemaVersion": 1,
])
try await stream.startCapture()
}

Expand Down Expand Up @@ -305,6 +313,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
"timestampMs": Int(Date().timeIntervalSince1970 * 1000),
"width": outputWidth,
"height": outputHeight,
"captureBounds": captureBoundsPayload(),
])
}
}
Expand Down Expand Up @@ -337,6 +346,15 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
}
}

private func captureBoundsPayload() -> [String: Double] {
return [
"x": captureFrame.origin.x,
"y": captureFrame.origin.y,
"width": captureFrame.size.width,
"height": captureFrame.size.height,
]
}

private func makeCaptureTarget(from content: SCShareableContent) throws -> CaptureTarget {
switch request.source.type {
case "display":
Expand All @@ -351,7 +369,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
return CaptureTarget(
filter: SCContentFilter(display: display, excludingWindows: []),
width: clampCaptureDimension(width, fallback: request.video.width),
height: clampCaptureDimension(height, fallback: request.video.height)
height: clampCaptureDimension(height, fallback: request.video.height),
captureFrame: display.frame
)
case "window":
guard let windowId = request.source.windowId else {
Expand All @@ -369,7 +388,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
return CaptureTarget(
filter: SCContentFilter(desktopIndependentWindow: window),
width: clampCaptureDimension(width, fallback: request.video.width),
height: clampCaptureDimension(height, fallback: request.video.height)
height: clampCaptureDimension(height, fallback: request.video.height),
captureFrame: window.frame
)
default:
throw HelperError.invalidSourceType(request.source.type)
Expand Down
1 change: 1 addition & 0 deletions src/lib/nativeMacRecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type NativeMacHelperReadyEvent = {
export type NativeMacHelperRecordingStartedEvent = {
event: "recording-started";
timestampMs: number;
captureBounds?: Rectangle;
};

export type NativeMacHelperRecordingStoppedEvent = {
Expand Down
Loading