replicas-cli 0.2.328 → 0.2.329
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +733 -140
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -8086,7 +8086,7 @@ Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` wo
|
|
|
8086
8086
|
When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
|
|
8087
8087
|
|
|
8088
8088
|
### \`replicas computer record start <path> [--fps N]\`
|
|
8089
|
-
Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
8089
|
+
Starts an ffmpeg screen recorder. Output is post-processed when stopped: action windows stay at normal speed, idle gaps accelerate, click moments get eased camera zoom, and a synthetic cursor animates between logged mouse positions. The raw capture is fragmented MP4 while active (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
8090
8090
|
|
|
8091
8091
|
Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
|
|
8092
8092
|
|
|
@@ -9537,7 +9537,7 @@ var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
|
9537
9537
|
var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
|
|
9538
9538
|
|
|
9539
9539
|
// ../shared/src/cli-version.ts
|
|
9540
|
-
var CLI_VERSION = "0.2.
|
|
9540
|
+
var CLI_VERSION = "0.2.329";
|
|
9541
9541
|
|
|
9542
9542
|
// ../shared/src/engine/environment.ts
|
|
9543
9543
|
var DESKTOP_NOVNC_PORT = 6080;
|
|
@@ -14481,12 +14481,17 @@ async function mediaListCommand(options) {
|
|
|
14481
14481
|
}
|
|
14482
14482
|
}
|
|
14483
14483
|
|
|
14484
|
-
// src/commands/computer.ts
|
|
14485
|
-
import { spawn as
|
|
14484
|
+
// src/commands/computer/index.ts
|
|
14485
|
+
import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
|
|
14486
14486
|
import { createHash as createHash2 } from "crypto";
|
|
14487
|
-
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "fs";
|
|
14488
|
-
import { dirname
|
|
14487
|
+
import { closeSync, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
14488
|
+
import { dirname as dirname2 } from "path";
|
|
14489
14489
|
import chalk20 from "chalk";
|
|
14490
|
+
|
|
14491
|
+
// src/commands/computer/desktop.ts
|
|
14492
|
+
import { spawnSync } from "child_process";
|
|
14493
|
+
import { existsSync } from "fs";
|
|
14494
|
+
import { isAbsolute, resolve } from "path";
|
|
14490
14495
|
var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
|
|
14491
14496
|
var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
|
|
14492
14497
|
var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
|
|
@@ -14547,6 +14552,23 @@ function getDisplayDimensions() {
|
|
|
14547
14552
|
if (!match) fail("could not read display dimensions from xdpyinfo");
|
|
14548
14553
|
return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
|
|
14549
14554
|
}
|
|
14555
|
+
function getMouseLocation() {
|
|
14556
|
+
const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
|
|
14557
|
+
if (!out) return null;
|
|
14558
|
+
const values = Object.fromEntries(
|
|
14559
|
+
out.split("\n").map((line) => {
|
|
14560
|
+
const [key, value] = line.split("=");
|
|
14561
|
+
return [key.toLowerCase(), Number.parseInt(value, 10)];
|
|
14562
|
+
})
|
|
14563
|
+
);
|
|
14564
|
+
if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
|
|
14565
|
+
return {
|
|
14566
|
+
x: values.x,
|
|
14567
|
+
y: values.y,
|
|
14568
|
+
screen: Number.isFinite(values.screen) ? values.screen : 0,
|
|
14569
|
+
window: Number.isFinite(values.window) ? values.window : 0
|
|
14570
|
+
};
|
|
14571
|
+
}
|
|
14550
14572
|
function parseScreenCoord(value, label, size) {
|
|
14551
14573
|
if (value.endsWith("%")) {
|
|
14552
14574
|
const rawPct = value.slice(0, -1);
|
|
@@ -14562,8 +14584,655 @@ function parseScreenCoord(value, label, size) {
|
|
|
14562
14584
|
function resolvePath(p) {
|
|
14563
14585
|
return isAbsolute(p) ? p : resolve(process.cwd(), p);
|
|
14564
14586
|
}
|
|
14587
|
+
function configuredDesktopDimensions() {
|
|
14588
|
+
const width = parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
|
|
14589
|
+
const height = parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
14590
|
+
if (!Number.isFinite(width) || width <= 0) fail(`REPLICAS_DESKTOP_WIDTH must be a positive integer`);
|
|
14591
|
+
if (!Number.isFinite(height) || height <= 0) fail(`REPLICAS_DESKTOP_HEIGHT must be a positive integer`);
|
|
14592
|
+
return { width, height };
|
|
14593
|
+
}
|
|
14594
|
+
function clamp(n, min, max) {
|
|
14595
|
+
return Math.min(max, Math.max(min, n));
|
|
14596
|
+
}
|
|
14597
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14598
|
+
|
|
14599
|
+
// src/commands/computer/recording.ts
|
|
14600
|
+
import { spawn as spawn3 } from "child_process";
|
|
14601
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14602
|
+
import { dirname } from "path";
|
|
14603
|
+
|
|
14604
|
+
// src/commands/computer/recording/render.ts
|
|
14605
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
14606
|
+
import { copyFileSync, rmSync, writeFileSync } from "fs";
|
|
14607
|
+
|
|
14608
|
+
// src/commands/computer/recording/config.ts
|
|
14609
|
+
var cameraMotion = {
|
|
14610
|
+
clickLeadSeconds: 1.2,
|
|
14611
|
+
clickTailSeconds: 0.5,
|
|
14612
|
+
actionLeadSeconds: 0.9,
|
|
14613
|
+
clickZoom: 1.24,
|
|
14614
|
+
closeClickOutputGapSeconds: 6,
|
|
14615
|
+
clickMoveSeconds: 1.2,
|
|
14616
|
+
returnSeconds: 1,
|
|
14617
|
+
minSegmentSeconds: 0.05
|
|
14618
|
+
};
|
|
14619
|
+
var cursorMotion = {
|
|
14620
|
+
minMoveSeconds: 0.38,
|
|
14621
|
+
maxMoveSeconds: 1.7,
|
|
14622
|
+
speedPxPerSecond: 700,
|
|
14623
|
+
defaultLeadSeconds: 0.16,
|
|
14624
|
+
clickShrinkSeconds: 0.08,
|
|
14625
|
+
clickGrowSeconds: 0.14,
|
|
14626
|
+
clickDwellGapStartSeconds: 0.75,
|
|
14627
|
+
clickDwellFullGapSeconds: 3.2,
|
|
14628
|
+
clickMinDwellSeconds: 0.12,
|
|
14629
|
+
clickMaxDwellSeconds: 1.15,
|
|
14630
|
+
maxDwellGapShare: 0.45,
|
|
14631
|
+
dragLeadSeconds: 0.55,
|
|
14632
|
+
maxKeyframes: 120
|
|
14633
|
+
};
|
|
14634
|
+
var cursorStyle = {
|
|
14635
|
+
size: 30,
|
|
14636
|
+
clickSize: 24
|
|
14637
|
+
};
|
|
14638
|
+
|
|
14639
|
+
// src/commands/computer/recording/ffmpeg-expressions.ts
|
|
14640
|
+
function expressionNumber(n) {
|
|
14641
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(3);
|
|
14642
|
+
}
|
|
14643
|
+
function mixNumber(from, to, progress) {
|
|
14644
|
+
return from + (to - from) * progress;
|
|
14645
|
+
}
|
|
14646
|
+
function easeInOutSmootherNumber(progress) {
|
|
14647
|
+
const p = clamp(progress, 0, 1);
|
|
14648
|
+
return p * p * p * (p * (p * 6 - 15) + 10);
|
|
14649
|
+
}
|
|
14650
|
+
function easeInOutSmootherExpression(progress) {
|
|
14651
|
+
const p = `(${progress})`;
|
|
14652
|
+
return `${p}*${p}*${p}*(${p}*(${p}*6-15)+10)`;
|
|
14653
|
+
}
|
|
14654
|
+
function progressOverTime(timeExpression, seconds) {
|
|
14655
|
+
return easeInOutSmootherExpression(`min(max((${timeExpression})/${seconds.toFixed(3)}\\,0)\\,1)`);
|
|
14656
|
+
}
|
|
14657
|
+
function progressBetween(start, duration) {
|
|
14658
|
+
return easeInOutSmootherExpression(`min(max((t-${start.toFixed(3)})/${duration.toFixed(3)}\\,0)\\,1)`);
|
|
14659
|
+
}
|
|
14660
|
+
function mixExpression(from, to, progress) {
|
|
14661
|
+
return `${expressionNumber(from)}+(${expressionNumber(to - from)})*${progress}`;
|
|
14662
|
+
}
|
|
14663
|
+
function cropOrigin(point, zoom, size) {
|
|
14664
|
+
return clamp(point * zoom - size / 2, 0, size * zoom - size);
|
|
14665
|
+
}
|
|
14666
|
+
function easedCropOrigin(fromPoint, toPoint, fromZoom, toZoom, size, progress) {
|
|
14667
|
+
return mixExpression(
|
|
14668
|
+
cropOrigin(fromPoint, fromZoom, size),
|
|
14669
|
+
cropOrigin(toPoint, toZoom, size),
|
|
14670
|
+
progress
|
|
14671
|
+
);
|
|
14672
|
+
}
|
|
14673
|
+
function betweenExpression(start, end) {
|
|
14674
|
+
return `between(t\\,${start.toFixed(3)}\\,${end.toFixed(3)})`;
|
|
14675
|
+
}
|
|
14676
|
+
|
|
14677
|
+
// src/commands/computer/recording/timeline.ts
|
|
14678
|
+
function centerOf(size) {
|
|
14679
|
+
return { x: size.width / 2, y: size.height / 2 };
|
|
14680
|
+
}
|
|
14681
|
+
function actionPoint(action, size) {
|
|
14682
|
+
const x = action.toX ?? action.x ?? size.width / 2;
|
|
14683
|
+
const y = action.toY ?? action.y ?? size.height / 2;
|
|
14684
|
+
return { x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) };
|
|
14685
|
+
}
|
|
14686
|
+
function idleSpeed(duration) {
|
|
14687
|
+
if (duration < 1.2) return 1;
|
|
14688
|
+
return clamp(1 + Math.log1p(duration - 0.8) * 1.8, 1.5, 5);
|
|
14689
|
+
}
|
|
14690
|
+
function renderedGapSeconds(duration) {
|
|
14691
|
+
return duration / idleSpeed(duration);
|
|
14692
|
+
}
|
|
14693
|
+
function timelineActions(actions) {
|
|
14694
|
+
return actions.filter((action) => action.type !== "key" && action.type !== "move");
|
|
14695
|
+
}
|
|
14696
|
+
function actionWindow(action, duration) {
|
|
14697
|
+
const shouldZoom = action.type === "click";
|
|
14698
|
+
const at = clamp(action.atMs / 1e3, 0, duration);
|
|
14699
|
+
const start = clamp(at - (shouldZoom ? cameraMotion.clickLeadSeconds : cameraMotion.actionLeadSeconds), 0, duration);
|
|
14700
|
+
const tail = shouldZoom ? cameraMotion.clickTailSeconds : action.type === "type" ? 1.5 : 0.9;
|
|
14701
|
+
return { start, end: clamp(at + tail, start, duration), shouldZoom };
|
|
14702
|
+
}
|
|
14703
|
+
function nextClickStart(actions, index, duration) {
|
|
14704
|
+
const nextClick = actions.slice(index + 1).find((candidate) => candidate.type === "click");
|
|
14705
|
+
return nextClick ? clamp(nextClick.atMs / 1e3 - cameraMotion.clickLeadSeconds, 0, duration) : null;
|
|
14706
|
+
}
|
|
14707
|
+
function shouldPreserveZoom(nextStart, cursor, lastZoom) {
|
|
14708
|
+
return lastZoom > 1 && nextStart !== null && nextStart > cursor && renderedGapSeconds(nextStart - cursor) <= cameraMotion.closeClickOutputGapSeconds;
|
|
14709
|
+
}
|
|
14710
|
+
function fullFrameSegment(duration, size) {
|
|
14711
|
+
const center = centerOf(size);
|
|
14712
|
+
return {
|
|
14713
|
+
start: 0,
|
|
14714
|
+
end: duration,
|
|
14715
|
+
speed: 1,
|
|
14716
|
+
motionSeconds: cameraMotion.returnSeconds,
|
|
14717
|
+
fromZoom: 1,
|
|
14718
|
+
zoom: 1,
|
|
14719
|
+
fromX: center.x,
|
|
14720
|
+
fromY: center.y,
|
|
14721
|
+
...center
|
|
14722
|
+
};
|
|
14723
|
+
}
|
|
14724
|
+
function buildRecordingSegments(actions, duration, size) {
|
|
14725
|
+
const actionsForTimeline = timelineActions(actions);
|
|
14726
|
+
if (actionsForTimeline.length === 0) return [fullFrameSegment(duration, size)];
|
|
14727
|
+
const center = centerOf(size);
|
|
14728
|
+
const segments = [];
|
|
14729
|
+
let cursor = 0;
|
|
14730
|
+
let lastPoint = center;
|
|
14731
|
+
let lastZoom = 1;
|
|
14732
|
+
for (let i = 0; i < actionsForTimeline.length; i++) {
|
|
14733
|
+
const action = actionsForTimeline[i];
|
|
14734
|
+
const { start, end, shouldZoom } = actionWindow(action, duration);
|
|
14735
|
+
const preserveZoom = !shouldZoom && shouldPreserveZoom(nextClickStart(actionsForTimeline, i, duration), cursor, lastZoom);
|
|
14736
|
+
const point = shouldZoom ? actionPoint(action, size) : preserveZoom ? lastPoint : center;
|
|
14737
|
+
const zoom = shouldZoom ? cameraMotion.clickZoom : preserveZoom ? lastZoom : 1;
|
|
14738
|
+
if (start > cursor + cameraMotion.minSegmentSeconds) {
|
|
14739
|
+
const gap = start - cursor;
|
|
14740
|
+
const speed = idleSpeed(gap);
|
|
14741
|
+
const bridgeZoom = shouldZoom && lastZoom > 1 && renderedGapSeconds(gap) <= cameraMotion.closeClickOutputGapSeconds;
|
|
14742
|
+
segments.push({
|
|
14743
|
+
start: cursor,
|
|
14744
|
+
end: start,
|
|
14745
|
+
speed,
|
|
14746
|
+
motionSeconds: bridgeZoom ? Math.min(cameraMotion.clickMoveSeconds, Math.max(0.18, gap / speed)) : cameraMotion.returnSeconds,
|
|
14747
|
+
fromZoom: lastZoom,
|
|
14748
|
+
zoom: bridgeZoom ? cameraMotion.clickZoom : zoom,
|
|
14749
|
+
fromX: lastPoint.x,
|
|
14750
|
+
fromY: lastPoint.y,
|
|
14751
|
+
...point
|
|
14752
|
+
});
|
|
14753
|
+
lastZoom = bridgeZoom ? cameraMotion.clickZoom : zoom;
|
|
14754
|
+
lastPoint = point;
|
|
14755
|
+
}
|
|
14756
|
+
const actionStart = Math.max(cursor, start);
|
|
14757
|
+
if (end > actionStart + cameraMotion.minSegmentSeconds) {
|
|
14758
|
+
segments.push({
|
|
14759
|
+
start: actionStart,
|
|
14760
|
+
end,
|
|
14761
|
+
speed: 1,
|
|
14762
|
+
motionSeconds: shouldZoom ? cameraMotion.clickMoveSeconds : cameraMotion.returnSeconds,
|
|
14763
|
+
fromZoom: lastZoom,
|
|
14764
|
+
zoom,
|
|
14765
|
+
fromX: lastPoint.x,
|
|
14766
|
+
fromY: lastPoint.y,
|
|
14767
|
+
...point
|
|
14768
|
+
});
|
|
14769
|
+
cursor = end;
|
|
14770
|
+
lastZoom = zoom;
|
|
14771
|
+
}
|
|
14772
|
+
lastPoint = point;
|
|
14773
|
+
}
|
|
14774
|
+
if (duration > cursor + cameraMotion.minSegmentSeconds) {
|
|
14775
|
+
segments.push({
|
|
14776
|
+
start: cursor,
|
|
14777
|
+
end: duration,
|
|
14778
|
+
speed: idleSpeed(duration - cursor),
|
|
14779
|
+
motionSeconds: cameraMotion.returnSeconds,
|
|
14780
|
+
fromZoom: lastZoom,
|
|
14781
|
+
zoom: 1,
|
|
14782
|
+
fromX: lastPoint.x,
|
|
14783
|
+
fromY: lastPoint.y,
|
|
14784
|
+
...center
|
|
14785
|
+
});
|
|
14786
|
+
}
|
|
14787
|
+
return segments.filter((segment) => segment.end > segment.start + cameraMotion.minSegmentSeconds);
|
|
14788
|
+
}
|
|
14789
|
+
|
|
14790
|
+
// src/commands/computer/recording/cursor.ts
|
|
14791
|
+
var cursorHotspotRatio = { x: 8 / 32, y: 4 / 32 };
|
|
14792
|
+
function cursorAssets(stamp) {
|
|
14793
|
+
return {
|
|
14794
|
+
path: `/tmp/replicas-cursor-${stamp}.svg`,
|
|
14795
|
+
size: cursorStyle.size
|
|
14796
|
+
};
|
|
14797
|
+
}
|
|
14798
|
+
function clickDwellSeconds(gapSeconds) {
|
|
14799
|
+
if (gapSeconds <= cursorMotion.clickDwellGapStartSeconds) return cursorMotion.defaultLeadSeconds;
|
|
14800
|
+
const progress = (gapSeconds - cursorMotion.clickDwellGapStartSeconds) / (cursorMotion.clickDwellFullGapSeconds - cursorMotion.clickDwellGapStartSeconds);
|
|
14801
|
+
const dwell = mixNumber(
|
|
14802
|
+
cursorMotion.clickMinDwellSeconds,
|
|
14803
|
+
cursorMotion.clickMaxDwellSeconds,
|
|
14804
|
+
easeInOutSmootherNumber(progress)
|
|
14805
|
+
);
|
|
14806
|
+
return Math.min(dwell, gapSeconds * cursorMotion.maxDwellGapShare);
|
|
14807
|
+
}
|
|
14808
|
+
function arrivalLeadSeconds(action, gapSeconds) {
|
|
14809
|
+
return action.type === "click" ? clickDwellSeconds(gapSeconds) : cursorMotion.defaultLeadSeconds;
|
|
14810
|
+
}
|
|
14811
|
+
function cursorKeyframe(action, at, previousAt, size) {
|
|
14812
|
+
if (typeof action.x !== "number" || typeof action.y !== "number") return null;
|
|
14813
|
+
const point = actionPoint(action, size);
|
|
14814
|
+
const lead = arrivalLeadSeconds(action, at - previousAt);
|
|
14815
|
+
return { ...point, at, arriveAt: clamp(at - lead, previousAt, at) };
|
|
14816
|
+
}
|
|
14817
|
+
function dedupeKeyframes(points) {
|
|
14818
|
+
return points.sort((a, b) => a.at - b.at).filter(
|
|
14819
|
+
(point, index, all) => index === 0 || point.at - all[index - 1].at > 0.08 || Math.hypot(point.x - all[index - 1].x, point.y - all[index - 1].y) > 24
|
|
14820
|
+
);
|
|
14821
|
+
}
|
|
14822
|
+
function downsampleKeyframes(points, actions, size) {
|
|
14823
|
+
if (points.length <= cursorMotion.maxKeyframes) return points;
|
|
14824
|
+
const anchors = actions.filter((action) => action.type !== "move").map((action) => actionPoint(action, size));
|
|
14825
|
+
const result = points.filter(
|
|
14826
|
+
(point, index) => index === 0 || index === points.length - 1 || anchors.some((anchor) => Math.hypot(anchor.x - point.x, anchor.y - point.y) < 1)
|
|
14827
|
+
);
|
|
14828
|
+
const step = points.length / Math.max(1, cursorMotion.maxKeyframes - result.length);
|
|
14829
|
+
for (let i = 0; result.length < cursorMotion.maxKeyframes && i < points.length; i += step) {
|
|
14830
|
+
const point = points[Math.floor(i)];
|
|
14831
|
+
if (!result.includes(point)) result.push(point);
|
|
14832
|
+
}
|
|
14833
|
+
return result.sort((a, b) => a.at - b.at);
|
|
14834
|
+
}
|
|
14835
|
+
function cursorKeyframes(actions, duration, size) {
|
|
14836
|
+
const center = { x: size.width / 2, y: size.height / 2 };
|
|
14837
|
+
const points = [{ at: 0, arriveAt: 0, ...center }];
|
|
14838
|
+
for (const action of actions) {
|
|
14839
|
+
const at = clamp(action.atMs / 1e3, 0, duration);
|
|
14840
|
+
const previousAt = points[points.length - 1].at;
|
|
14841
|
+
if (action.type === "drag" && typeof action.x === "number" && typeof action.y === "number") {
|
|
14842
|
+
const startAt = clamp(at - cursorMotion.dragLeadSeconds, 0, duration);
|
|
14843
|
+
points.push({
|
|
14844
|
+
at: startAt,
|
|
14845
|
+
arriveAt: startAt,
|
|
14846
|
+
x: clamp(action.x, 0, size.width),
|
|
14847
|
+
y: clamp(action.y, 0, size.height)
|
|
14848
|
+
});
|
|
14849
|
+
if (typeof action.toX === "number" && typeof action.toY === "number") {
|
|
14850
|
+
points.push({
|
|
14851
|
+
at,
|
|
14852
|
+
arriveAt: at,
|
|
14853
|
+
x: clamp(action.toX, 0, size.width),
|
|
14854
|
+
y: clamp(action.toY, 0, size.height)
|
|
14855
|
+
});
|
|
14856
|
+
}
|
|
14857
|
+
continue;
|
|
14858
|
+
}
|
|
14859
|
+
const point = cursorKeyframe(action, at, previousAt, size);
|
|
14860
|
+
if (point) points.push(point);
|
|
14861
|
+
}
|
|
14862
|
+
return downsampleKeyframes(dedupeKeyframes(points), actions, size);
|
|
14863
|
+
}
|
|
14864
|
+
function cursorAxisExpression(points, axis) {
|
|
14865
|
+
if (points.length === 0) return "0";
|
|
14866
|
+
let expression = expressionNumber(points[points.length - 1][axis]);
|
|
14867
|
+
for (let i = points.length - 2; i >= 0; i--) {
|
|
14868
|
+
const from = points[i];
|
|
14869
|
+
const to = points[i + 1];
|
|
14870
|
+
const arriveAt = clamp(to.arriveAt, from.at, to.at);
|
|
14871
|
+
const moveWindow = Math.max(1e-3, arriveAt - from.at);
|
|
14872
|
+
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
14873
|
+
const moveSeconds = Math.min(
|
|
14874
|
+
moveWindow,
|
|
14875
|
+
Math.max(cursorMotion.minMoveSeconds, Math.min(cursorMotion.maxMoveSeconds, distance / cursorMotion.speedPxPerSecond))
|
|
14876
|
+
);
|
|
14877
|
+
const moveStart = Math.max(from.at, arriveAt - moveSeconds);
|
|
14878
|
+
const duration = Math.max(1e-3, arriveAt - moveStart);
|
|
14879
|
+
const interpolated = mixExpression(from[axis], to[axis], progressBetween(moveStart, duration));
|
|
14880
|
+
expression = `if(lte(t\\,${moveStart.toFixed(3)})\\,${expressionNumber(from[axis])}\\,if(lte(t\\,${to.at.toFixed(3)})\\,${interpolated}\\,${expression}))`;
|
|
14881
|
+
}
|
|
14882
|
+
return expression;
|
|
14883
|
+
}
|
|
14884
|
+
function cursorSizeExpression(actions, normalSize) {
|
|
14885
|
+
let expression = expressionNumber(normalSize);
|
|
14886
|
+
const clicks = actions.filter((action) => action.type === "click").sort((a, b) => b.atMs - a.atMs);
|
|
14887
|
+
for (const action of clicks) {
|
|
14888
|
+
const at = action.atMs / 1e3;
|
|
14889
|
+
const shrinkStart = Math.max(0, at - cursorMotion.clickShrinkSeconds);
|
|
14890
|
+
const shrink = mixExpression(
|
|
14891
|
+
normalSize,
|
|
14892
|
+
cursorStyle.clickSize,
|
|
14893
|
+
progressBetween(shrinkStart, Math.max(1e-3, at - shrinkStart))
|
|
14894
|
+
);
|
|
14895
|
+
const grow = mixExpression(cursorStyle.clickSize, normalSize, progressBetween(at, cursorMotion.clickGrowSeconds));
|
|
14896
|
+
expression = `if(${betweenExpression(shrinkStart, at)}\\,${shrink}\\,if(${betweenExpression(at, at + cursorMotion.clickGrowSeconds)}\\,${grow}\\,${expression}))`;
|
|
14897
|
+
}
|
|
14898
|
+
return expression;
|
|
14899
|
+
}
|
|
14900
|
+
function cursorOverlayPositionExpression(cursor, size, hotspotRatio, overlaySize) {
|
|
14901
|
+
return `max(0\\,min(${expressionNumber(size)}-${overlaySize}\\,(${cursor})-${hotspotRatio.toFixed(6)}*${overlaySize}))`;
|
|
14902
|
+
}
|
|
14903
|
+
function cursorSvg(size) {
|
|
14904
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7.7 3.4c-1.45 0-2.62 1.17-2.62 2.62v19.7c0 2.46 3.02 3.65 4.7 1.85l5.06-5.44a5.42 5.42 0 0 1 3.96-1.73h5.82c2.48 0 3.63-3.08 1.76-4.7L9.42 4.04A2.62 2.62 0 0 0 7.7 3.4Z" fill="white" fill-opacity="0.98" stroke="black" stroke-opacity="0.88" stroke-width="2.9" stroke-linejoin="round"/></svg>`;
|
|
14905
|
+
}
|
|
14906
|
+
function buildCursorOverlayFilter(actions, duration, size, assets, projectCursor = null, inputLabel = "[screen]", outputLabel = "[v]") {
|
|
14907
|
+
const points = cursorKeyframes(actions, duration, size);
|
|
14908
|
+
const rawCursorX = cursorAxisExpression(points, "x");
|
|
14909
|
+
const rawCursorY = cursorAxisExpression(points, "y");
|
|
14910
|
+
const cursorX = projectCursor ? projectCursor.x(rawCursorX) : rawCursorX;
|
|
14911
|
+
const cursorY = projectCursor ? projectCursor.y(rawCursorY) : rawCursorY;
|
|
14912
|
+
const cursorSize = cursorSizeExpression(actions, assets.size);
|
|
14913
|
+
const cursorOverlayX = cursorOverlayPositionExpression(cursorX, size.width, cursorHotspotRatio.x, "overlay_w");
|
|
14914
|
+
const cursorOverlayY = cursorOverlayPositionExpression(cursorY, size.height, cursorHotspotRatio.y, "overlay_h");
|
|
14915
|
+
return `[1:v]scale=w='${cursorSize}':h='${cursorSize}':eval=frame[cursor];${inputLabel}[cursor]overlay=x='${cursorOverlayX}':y='${cursorOverlayY}':eof_action=repeat:shortest=1:eval=frame${outputLabel}`;
|
|
14916
|
+
}
|
|
14917
|
+
|
|
14918
|
+
// src/commands/computer/recording/render.ts
|
|
14919
|
+
function videoDuration(path6) {
|
|
14920
|
+
const r = spawnSync2("ffprobe", [
|
|
14921
|
+
"-v",
|
|
14922
|
+
"error",
|
|
14923
|
+
"-show_entries",
|
|
14924
|
+
"format=duration",
|
|
14925
|
+
"-of",
|
|
14926
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
14927
|
+
path6
|
|
14928
|
+
], { stdio: "pipe" });
|
|
14929
|
+
if (r.status !== 0) fail(`ffprobe failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
14930
|
+
const duration = Number.parseFloat(r.stdout?.toString().trim() ?? "");
|
|
14931
|
+
if (!Number.isFinite(duration) || duration <= 0) fail(`could not read recording duration for ${path6}`);
|
|
14932
|
+
return duration;
|
|
14933
|
+
}
|
|
14934
|
+
function renderedSegmentSpans(segments) {
|
|
14935
|
+
let outputStart = 0;
|
|
14936
|
+
return segments.map((segment) => {
|
|
14937
|
+
const outputEnd = outputStart + (segment.end - segment.start) / segment.speed;
|
|
14938
|
+
const span = {
|
|
14939
|
+
rawStart: segment.start,
|
|
14940
|
+
rawEnd: segment.end,
|
|
14941
|
+
outputStart,
|
|
14942
|
+
outputEnd,
|
|
14943
|
+
speed: segment.speed
|
|
14944
|
+
};
|
|
14945
|
+
outputStart = outputEnd;
|
|
14946
|
+
return span;
|
|
14947
|
+
});
|
|
14948
|
+
}
|
|
14949
|
+
function outputTimeForRawTime(spans, rawTime) {
|
|
14950
|
+
const span = spans.find((candidate) => rawTime >= candidate.rawStart && rawTime <= candidate.rawEnd);
|
|
14951
|
+
if (span) return span.outputStart + (rawTime - span.rawStart) / span.speed;
|
|
14952
|
+
if (spans.length === 0) return rawTime;
|
|
14953
|
+
if (rawTime <= spans[0].rawStart) return spans[0].outputStart;
|
|
14954
|
+
const lastSpan = spans[spans.length - 1];
|
|
14955
|
+
if (rawTime >= lastSpan.rawEnd) return lastSpan.outputEnd;
|
|
14956
|
+
return spans.find((candidate) => rawTime < candidate.rawStart)?.outputStart ?? lastSpan.outputEnd;
|
|
14957
|
+
}
|
|
14958
|
+
function actionsOnRenderedTimeline(actions, spans) {
|
|
14959
|
+
return actions.map((action) => ({
|
|
14960
|
+
...action,
|
|
14961
|
+
atMs: outputTimeForRawTime(spans, action.atMs / 1e3) * 1e3
|
|
14962
|
+
}));
|
|
14963
|
+
}
|
|
14964
|
+
function segmentMotionSeconds(segment) {
|
|
14965
|
+
return Math.min(segment.motionSeconds, Math.max(0.12, (segment.end - segment.start) / segment.speed));
|
|
14966
|
+
}
|
|
14967
|
+
function scaledFrameClampExpression(value, zoom, outputSize) {
|
|
14968
|
+
return `max(0\\,min(${outputSize}*(${zoom})-${outputSize}\\,${value}))`;
|
|
14969
|
+
}
|
|
14970
|
+
function cameraTransformExpressions(segment, size, timeExpression) {
|
|
14971
|
+
const progress = progressOverTime(timeExpression, segmentMotionSeconds(segment));
|
|
14972
|
+
const zoom = mixExpression(segment.fromZoom, segment.zoom, progress);
|
|
14973
|
+
return {
|
|
14974
|
+
zoom,
|
|
14975
|
+
cropX: scaledFrameClampExpression(
|
|
14976
|
+
easedCropOrigin(segment.fromX, segment.x, segment.fromZoom, segment.zoom, size.width, progress),
|
|
14977
|
+
zoom,
|
|
14978
|
+
size.width
|
|
14979
|
+
),
|
|
14980
|
+
cropY: scaledFrameClampExpression(
|
|
14981
|
+
easedCropOrigin(segment.fromY, segment.y, segment.fromZoom, segment.zoom, size.height, progress),
|
|
14982
|
+
zoom,
|
|
14983
|
+
size.height
|
|
14984
|
+
)
|
|
14985
|
+
};
|
|
14986
|
+
}
|
|
14987
|
+
function projectedCursorExpression(rawCursor, axis, segments, spans, size) {
|
|
14988
|
+
let expression = rawCursor;
|
|
14989
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
14990
|
+
const span = spans[i];
|
|
14991
|
+
const camera = cameraTransformExpressions(segments[i], size, `t-${span.outputStart.toFixed(3)}`);
|
|
14992
|
+
const crop = axis === "x" ? camera.cropX : camera.cropY;
|
|
14993
|
+
const projected = `(${rawCursor})*(${camera.zoom})-(${crop})`;
|
|
14994
|
+
expression = `if(${betweenExpression(span.outputStart, span.outputEnd)}\\,${projected}\\,${expression})`;
|
|
14995
|
+
}
|
|
14996
|
+
return expression;
|
|
14997
|
+
}
|
|
14998
|
+
function renderRecording(rawPath, target, actions, fps, size) {
|
|
14999
|
+
if (actions.length === 0) {
|
|
15000
|
+
copyFileSync(rawPath, target);
|
|
15001
|
+
return;
|
|
15002
|
+
}
|
|
15003
|
+
const duration = videoDuration(rawPath);
|
|
15004
|
+
const segments = buildRecordingSegments(actions, duration, size);
|
|
15005
|
+
if (segments.length === 0) {
|
|
15006
|
+
copyFileSync(rawPath, target);
|
|
15007
|
+
return;
|
|
15008
|
+
}
|
|
15009
|
+
const stamp = `${process.pid}-${Date.now()}`;
|
|
15010
|
+
const cursor = cursorAssets(stamp);
|
|
15011
|
+
writeFileSync(cursor.path, cursorSvg(cursor.size));
|
|
15012
|
+
const spans = renderedSegmentSpans(segments);
|
|
15013
|
+
const renderedDuration = spans.length ? spans[spans.length - 1].outputEnd : duration;
|
|
15014
|
+
const renderedActions = actionsOnRenderedTimeline(actions, spans);
|
|
15015
|
+
const cursorFilter = buildCursorOverlayFilter(
|
|
15016
|
+
renderedActions,
|
|
15017
|
+
renderedDuration,
|
|
15018
|
+
size,
|
|
15019
|
+
cursor,
|
|
15020
|
+
{
|
|
15021
|
+
x: (cursorX) => projectedCursorExpression(cursorX, "x", segments, spans, size),
|
|
15022
|
+
y: (cursorY) => projectedCursorExpression(cursorY, "y", segments, spans, size)
|
|
15023
|
+
}
|
|
15024
|
+
);
|
|
15025
|
+
const screenLabels = segments.map((_, index) => `[screen${index}]`).join("");
|
|
15026
|
+
const screenSplitFilter = `[0:v]split=${segments.length}${screenLabels}`;
|
|
15027
|
+
const filters = segments.map((segment, index) => {
|
|
15028
|
+
const { zoom, cropX, cropY } = cameraTransformExpressions(segment, size, "t");
|
|
15029
|
+
return `[screen${index}]trim=start=${segment.start.toFixed(3)}:end=${segment.end.toFixed(3)},setpts=(PTS-STARTPTS)/${segment.speed.toFixed(3)},scale=w=${size.width}*(${zoom}):h=${size.height}*(${zoom}):eval=frame,crop=${size.width}:${size.height}:x=${cropX}:y=${cropY},setsar=1[v${index}]`;
|
|
15030
|
+
});
|
|
15031
|
+
const concatInputs = segments.map((_, index) => `[v${index}]`).join("");
|
|
15032
|
+
const filter = `${screenSplitFilter};${filters.join(";")};${concatInputs}concat=n=${segments.length}:v=1:a=0[screen];${cursorFilter}`;
|
|
15033
|
+
const filterPath = `/tmp/replicas-recording-filter-${stamp}.ffgraph`;
|
|
15034
|
+
writeFileSync(filterPath, filter);
|
|
15035
|
+
try {
|
|
15036
|
+
const r = spawnSync2("ffmpeg", [
|
|
15037
|
+
"-y",
|
|
15038
|
+
"-hide_banner",
|
|
15039
|
+
"-loglevel",
|
|
15040
|
+
"warning",
|
|
15041
|
+
"-i",
|
|
15042
|
+
rawPath,
|
|
15043
|
+
"-loop",
|
|
15044
|
+
"1",
|
|
15045
|
+
"-i",
|
|
15046
|
+
cursor.path,
|
|
15047
|
+
"-filter_complex_script",
|
|
15048
|
+
filterPath,
|
|
15049
|
+
"-map",
|
|
15050
|
+
"[v]",
|
|
15051
|
+
"-r",
|
|
15052
|
+
String(fps),
|
|
15053
|
+
"-c:v",
|
|
15054
|
+
"libx264",
|
|
15055
|
+
"-preset",
|
|
15056
|
+
"veryfast",
|
|
15057
|
+
"-crf",
|
|
15058
|
+
"23",
|
|
15059
|
+
"-pix_fmt",
|
|
15060
|
+
"yuv420p",
|
|
15061
|
+
"-movflags",
|
|
15062
|
+
"+faststart",
|
|
15063
|
+
target
|
|
15064
|
+
], { stdio: "pipe", maxBuffer: 20 * 1024 * 1024 });
|
|
15065
|
+
if (r.status !== 0) {
|
|
15066
|
+
fail(`recording post-processing failed: ${r.error?.message || r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
15067
|
+
}
|
|
15068
|
+
} finally {
|
|
15069
|
+
rmSync(filterPath, { force: true });
|
|
15070
|
+
rmSync(cursor.path, { force: true });
|
|
15071
|
+
}
|
|
15072
|
+
}
|
|
15073
|
+
|
|
15074
|
+
// src/commands/computer/recording.ts
|
|
15075
|
+
var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
|
|
15076
|
+
var RECORD_PATH_FILE = `${STATE_DIR}/recording-path.txt`;
|
|
15077
|
+
var RECORD_RAW_PATH_FILE = `${STATE_DIR}/recording-raw-path.txt`;
|
|
15078
|
+
var RECORD_STARTED_AT_FILE = `${STATE_DIR}/recording-started-at.txt`;
|
|
15079
|
+
var RECORD_FPS_FILE = `${STATE_DIR}/recording-fps.txt`;
|
|
15080
|
+
var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
|
|
15081
|
+
var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
|
|
15082
|
+
function recordingStartedAt() {
|
|
15083
|
+
if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
|
|
15084
|
+
const startedAt = Number.parseInt(readFileSync(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
|
|
15085
|
+
return Number.isFinite(startedAt) ? startedAt : null;
|
|
15086
|
+
}
|
|
15087
|
+
function logRecordingAction(action) {
|
|
15088
|
+
const startedAt = recordingStartedAt();
|
|
15089
|
+
if (!startedAt) return;
|
|
15090
|
+
const atMs = Date.now() - startedAt;
|
|
15091
|
+
appendFileSync(RECORD_ACTIONS_FILE, `${JSON.stringify({ ...action, atMs })}
|
|
15092
|
+
`);
|
|
15093
|
+
}
|
|
15094
|
+
function readRecordingDimensions() {
|
|
15095
|
+
if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
|
|
15096
|
+
try {
|
|
15097
|
+
const dimensions = JSON.parse(readFileSync(RECORD_DIMENSIONS_FILE, "utf8"));
|
|
15098
|
+
const width = dimensions?.width;
|
|
15099
|
+
const height = dimensions?.height;
|
|
15100
|
+
if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
|
|
15101
|
+
return { width, height };
|
|
15102
|
+
}
|
|
15103
|
+
} catch {
|
|
15104
|
+
}
|
|
15105
|
+
fail("invalid recording dimensions state");
|
|
15106
|
+
}
|
|
15107
|
+
function isRecordingActionType(value) {
|
|
15108
|
+
return value === "click" || value === "drag" || value === "key" || value === "move" || value === "scroll" || value === "type";
|
|
15109
|
+
}
|
|
15110
|
+
function isOptionalNumber(value) {
|
|
15111
|
+
return value === void 0 || typeof value === "number";
|
|
15112
|
+
}
|
|
15113
|
+
function readRecordingActions() {
|
|
15114
|
+
if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
|
|
15115
|
+
return readFileSync(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
15116
|
+
try {
|
|
15117
|
+
const value = JSON.parse(line);
|
|
15118
|
+
if (typeof value !== "object" || value === null) return [];
|
|
15119
|
+
const type = "type" in value ? value.type : void 0;
|
|
15120
|
+
const atMs = "atMs" in value ? value.atMs : void 0;
|
|
15121
|
+
const x = "x" in value ? value.x : void 0;
|
|
15122
|
+
const y = "y" in value ? value.y : void 0;
|
|
15123
|
+
const toX = "toX" in value ? value.toX : void 0;
|
|
15124
|
+
const toY = "toY" in value ? value.toY : void 0;
|
|
15125
|
+
if (!isRecordingActionType(type) || typeof atMs !== "number" || !Number.isFinite(atMs) || !isOptionalNumber(x) || !isOptionalNumber(y) || !isOptionalNumber(toX) || !isOptionalNumber(toY)) {
|
|
15126
|
+
return [];
|
|
15127
|
+
}
|
|
15128
|
+
return [{ type, atMs, x, y, toX, toY }];
|
|
15129
|
+
} catch {
|
|
15130
|
+
return [];
|
|
15131
|
+
}
|
|
15132
|
+
}).sort((a, b) => a.atMs - b.atMs);
|
|
15133
|
+
}
|
|
15134
|
+
async function computerRecordStartCommand(path6, options) {
|
|
15135
|
+
ensureServicesRunning();
|
|
15136
|
+
if (existsSync2(RECORD_PID_FILE)) {
|
|
15137
|
+
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15138
|
+
if (Number.isFinite(pid)) {
|
|
15139
|
+
let alive = false;
|
|
15140
|
+
try {
|
|
15141
|
+
process.kill(pid, 0);
|
|
15142
|
+
alive = true;
|
|
15143
|
+
} catch {
|
|
15144
|
+
}
|
|
15145
|
+
if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
const target = resolvePath(path6);
|
|
15149
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
15150
|
+
const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
|
|
15151
|
+
const { width, height } = configuredDesktopDimensions();
|
|
15152
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
15153
|
+
const rawTarget = `${target}.raw-${Date.now()}.mp4`;
|
|
15154
|
+
rmSync2(RECORD_ACTIONS_FILE, { force: true });
|
|
15155
|
+
const child = spawn3("ffmpeg", [
|
|
15156
|
+
"-y",
|
|
15157
|
+
"-hide_banner",
|
|
15158
|
+
"-loglevel",
|
|
15159
|
+
"warning",
|
|
15160
|
+
"-f",
|
|
15161
|
+
"x11grab",
|
|
15162
|
+
"-framerate",
|
|
15163
|
+
String(fps),
|
|
15164
|
+
"-video_size",
|
|
15165
|
+
`${width}x${height}`,
|
|
15166
|
+
"-draw_mouse",
|
|
15167
|
+
"0",
|
|
15168
|
+
"-i",
|
|
15169
|
+
DEFAULT_DISPLAY,
|
|
15170
|
+
"-c:v",
|
|
15171
|
+
"libx264",
|
|
15172
|
+
"-preset",
|
|
15173
|
+
"ultrafast",
|
|
15174
|
+
"-tune",
|
|
15175
|
+
"zerolatency",
|
|
15176
|
+
"-crf",
|
|
15177
|
+
"23",
|
|
15178
|
+
"-pix_fmt",
|
|
15179
|
+
"yuv420p",
|
|
15180
|
+
"-movflags",
|
|
15181
|
+
"+faststart+frag_keyframe+empty_moov",
|
|
15182
|
+
rawTarget
|
|
15183
|
+
], { detached: true, stdio: "ignore" });
|
|
15184
|
+
child.unref();
|
|
15185
|
+
if (!child.pid) fail("failed to launch ffmpeg");
|
|
15186
|
+
writeFileSync2(RECORD_PID_FILE, String(child.pid));
|
|
15187
|
+
writeFileSync2(RECORD_PATH_FILE, target);
|
|
15188
|
+
writeFileSync2(RECORD_RAW_PATH_FILE, rawTarget);
|
|
15189
|
+
writeFileSync2(RECORD_STARTED_AT_FILE, String(Date.now()));
|
|
15190
|
+
writeFileSync2(RECORD_FPS_FILE, String(fps));
|
|
15191
|
+
writeFileSync2(RECORD_DIMENSIONS_FILE, JSON.stringify({ width, height }));
|
|
15192
|
+
console.log(target);
|
|
15193
|
+
}
|
|
15194
|
+
async function computerRecordStopCommand() {
|
|
15195
|
+
if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
|
|
15196
|
+
if (existsSync2(RECORD_PID_FILE)) {
|
|
15197
|
+
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15198
|
+
if (!Number.isFinite(pid)) fail("invalid recording pidfile");
|
|
15199
|
+
try {
|
|
15200
|
+
process.kill(pid, "SIGINT");
|
|
15201
|
+
} catch {
|
|
15202
|
+
}
|
|
15203
|
+
for (let i = 0; i < 30; i++) {
|
|
15204
|
+
try {
|
|
15205
|
+
process.kill(pid, 0);
|
|
15206
|
+
} catch {
|
|
15207
|
+
break;
|
|
15208
|
+
}
|
|
15209
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
15210
|
+
}
|
|
15211
|
+
rmSync2(RECORD_PID_FILE, { force: true });
|
|
15212
|
+
}
|
|
15213
|
+
if (existsSync2(RECORD_PATH_FILE)) {
|
|
15214
|
+
const target = readFileSync(RECORD_PATH_FILE, "utf8").trim();
|
|
15215
|
+
const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
|
|
15216
|
+
const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
|
|
15217
|
+
const size = readRecordingDimensions();
|
|
15218
|
+
const actions = readRecordingActions();
|
|
15219
|
+
if (rawPath !== target) {
|
|
15220
|
+
renderRecording(rawPath, target, actions, Number.isFinite(fps) ? fps : 60, size);
|
|
15221
|
+
rmSync2(rawPath, { force: true });
|
|
15222
|
+
}
|
|
15223
|
+
console.log(target);
|
|
15224
|
+
rmSync2(RECORD_PATH_FILE, { force: true });
|
|
15225
|
+
rmSync2(RECORD_RAW_PATH_FILE, { force: true });
|
|
15226
|
+
rmSync2(RECORD_STARTED_AT_FILE, { force: true });
|
|
15227
|
+
rmSync2(RECORD_FPS_FILE, { force: true });
|
|
15228
|
+
rmSync2(RECORD_DIMENSIONS_FILE, { force: true });
|
|
15229
|
+
rmSync2(RECORD_ACTIONS_FILE, { force: true });
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
|
|
15233
|
+
// src/commands/computer/index.ts
|
|
14565
15234
|
function desktopBridgeStatus() {
|
|
14566
|
-
const r =
|
|
15235
|
+
const r = spawnSync3("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
|
|
14567
15236
|
if (r.status !== 0) return null;
|
|
14568
15237
|
try {
|
|
14569
15238
|
return JSON.parse(r.stdout.toString());
|
|
@@ -14580,7 +15249,6 @@ function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
|
|
|
14580
15249
|
if (pids.length > 0) parts.push(`pid${pids.length === 1 ? "" : "s"} ${pids.join(",")}`);
|
|
14581
15250
|
return ` (${parts.join(", ")})`;
|
|
14582
15251
|
}
|
|
14583
|
-
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14584
15252
|
async function lookupDesktopViewerUrl() {
|
|
14585
15253
|
try {
|
|
14586
15254
|
const list = await listAgentPreviews();
|
|
@@ -14614,7 +15282,7 @@ async function computerStatusCommand() {
|
|
|
14614
15282
|
const bridge = desktopBridgeStatus();
|
|
14615
15283
|
const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
|
|
14616
15284
|
for (const p of procs) {
|
|
14617
|
-
const r =
|
|
15285
|
+
const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
|
|
14618
15286
|
const running = r.status === 0 && !!r.stdout?.toString().trim();
|
|
14619
15287
|
const suffix = p === "x11vnc" && bridge ? bridgeStatus(bridge.x11vnc, true) : p === "websockify" && bridge ? bridgeStatus(bridge.websockify, false, true) : "";
|
|
14620
15288
|
console.log(` ${running ? chalk20.green("\u25CF") : chalk20.red("\u25CB")} ${p}${suffix}`);
|
|
@@ -14646,12 +15314,12 @@ function brandSvgPath() {
|
|
|
14646
15314
|
}
|
|
14647
15315
|
function loadBrandSvg(canvasW, canvasH) {
|
|
14648
15316
|
const path6 = brandSvgPath();
|
|
14649
|
-
if (!
|
|
15317
|
+
if (!existsSync3(path6)) {
|
|
14650
15318
|
fail(
|
|
14651
15319
|
`Brand wallpaper SVG missing at ${path6}. The workspace image is out of date \u2014 \`desktop/brand-wallpaper.svg\` must be installed at $REPLICAS_DESKTOP_TEMPLATES.`
|
|
14652
15320
|
);
|
|
14653
15321
|
}
|
|
14654
|
-
return
|
|
15322
|
+
return readFileSync2(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
|
|
14655
15323
|
}
|
|
14656
15324
|
var BRAND_PAD_FRACTION = 0.06;
|
|
14657
15325
|
var SCREENSHOT_CORNER_FRACTION = 0.022;
|
|
@@ -14686,8 +15354,8 @@ ${labels.join("\n")}
|
|
|
14686
15354
|
</svg>`;
|
|
14687
15355
|
}
|
|
14688
15356
|
function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
|
|
14689
|
-
|
|
14690
|
-
const r =
|
|
15357
|
+
writeFileSync3(gridPath, buildGridSvg(width, height, gridSize));
|
|
15358
|
+
const r = spawnSync3(
|
|
14691
15359
|
"ffmpeg",
|
|
14692
15360
|
[
|
|
14693
15361
|
"-y",
|
|
@@ -14714,7 +15382,7 @@ function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
|
|
|
14714
15382
|
}
|
|
14715
15383
|
async function computerScreenshotCommand(path6, options = {}) {
|
|
14716
15384
|
const target = resolvePath(path6);
|
|
14717
|
-
|
|
15385
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
14718
15386
|
const stamp = `${process.pid}-${Date.now()}`;
|
|
14719
15387
|
const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
|
|
14720
15388
|
const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
|
|
@@ -14726,7 +15394,7 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14726
15394
|
const { width, height } = readPngDimensions(rawPath);
|
|
14727
15395
|
const gridSize = parseGridSize(options.grid);
|
|
14728
15396
|
if (options.raw && gridSize === null) {
|
|
14729
|
-
|
|
15397
|
+
copyFileSync2(rawPath, target);
|
|
14730
15398
|
console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels)`);
|
|
14731
15399
|
return;
|
|
14732
15400
|
}
|
|
@@ -14746,16 +15414,16 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14746
15414
|
const shadowMargin = shadowSigma * 3;
|
|
14747
15415
|
const shadowW = width + shadowMargin * 2;
|
|
14748
15416
|
const shadowH = height + shadowMargin * 2;
|
|
14749
|
-
|
|
14750
|
-
|
|
15417
|
+
writeFileSync3(svgPath, loadBrandSvg(canvasW, canvasH));
|
|
15418
|
+
writeFileSync3(
|
|
14751
15419
|
maskPath,
|
|
14752
15420
|
SCREENSHOT_MASK_TEMPLATE.replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
14753
15421
|
);
|
|
14754
|
-
|
|
15422
|
+
writeFileSync3(
|
|
14755
15423
|
shadowPath,
|
|
14756
15424
|
SHADOW_MASK_TEMPLATE.replace(/__SW__/g, String(shadowW)).replace(/__SH__/g, String(shadowH)).replace(/__M__/g, String(shadowMargin)).replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
14757
15425
|
);
|
|
14758
|
-
const r =
|
|
15426
|
+
const r = spawnSync3(
|
|
14759
15427
|
"ffmpeg",
|
|
14760
15428
|
[
|
|
14761
15429
|
"-y",
|
|
@@ -14784,16 +15452,16 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14784
15452
|
fail(`ffmpeg branding failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
14785
15453
|
}
|
|
14786
15454
|
} finally {
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
15455
|
+
rmSync3(rawPath, { force: true });
|
|
15456
|
+
rmSync3(svgPath, { force: true });
|
|
15457
|
+
rmSync3(maskPath, { force: true });
|
|
15458
|
+
rmSync3(shadowPath, { force: true });
|
|
15459
|
+
rmSync3(gridPath, { force: true });
|
|
14792
15460
|
}
|
|
14793
15461
|
console.log(target);
|
|
14794
15462
|
}
|
|
14795
15463
|
function hashFile(path6) {
|
|
14796
|
-
return createHash2("sha256").update(
|
|
15464
|
+
return createHash2("sha256").update(readFileSync2(path6)).digest("hex");
|
|
14797
15465
|
}
|
|
14798
15466
|
async function captureStableRawScreenshot(target, options) {
|
|
14799
15467
|
const start = Date.now();
|
|
@@ -14820,7 +15488,7 @@ async function captureStableRawScreenshot(target, options) {
|
|
|
14820
15488
|
lastChangeAt = now;
|
|
14821
15489
|
}
|
|
14822
15490
|
lastHash = hash;
|
|
14823
|
-
|
|
15491
|
+
copyFileSync2(framePath, target);
|
|
14824
15492
|
if (frames > 1 && now - lastChangeAt >= options.stableMs) {
|
|
14825
15493
|
return { width, height, stable: true, elapsedMs: now - start, frames, changes };
|
|
14826
15494
|
}
|
|
@@ -14828,26 +15496,9 @@ async function captureStableRawScreenshot(target, options) {
|
|
|
14828
15496
|
}
|
|
14829
15497
|
return { width, height, stable: false, elapsedMs: Date.now() - start, frames, changes };
|
|
14830
15498
|
} finally {
|
|
14831
|
-
|
|
15499
|
+
rmSync3(framePath, { force: true });
|
|
14832
15500
|
}
|
|
14833
15501
|
}
|
|
14834
|
-
function getMouseLocation() {
|
|
14835
|
-
const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
|
|
14836
|
-
if (!out) return null;
|
|
14837
|
-
const values = Object.fromEntries(
|
|
14838
|
-
out.split("\n").map((line) => {
|
|
14839
|
-
const [key, value] = line.split("=");
|
|
14840
|
-
return [key.toLowerCase(), Number.parseInt(value, 10)];
|
|
14841
|
-
})
|
|
14842
|
-
);
|
|
14843
|
-
if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
|
|
14844
|
-
return {
|
|
14845
|
-
x: values.x,
|
|
14846
|
-
y: values.y,
|
|
14847
|
-
screen: Number.isFinite(values.screen) ? values.screen : 0,
|
|
14848
|
-
window: Number.isFinite(values.window) ? values.window : 0
|
|
14849
|
-
};
|
|
14850
|
-
}
|
|
14851
15502
|
function getActiveWindowTitle() {
|
|
14852
15503
|
return tryDisplayCmd("xdotool", ["getactivewindow", "getwindowname"]);
|
|
14853
15504
|
}
|
|
@@ -14855,9 +15506,13 @@ function getVisibleWindowTitles() {
|
|
|
14855
15506
|
const out = tryDisplayCmd("xdotool", ["search", "--onlyvisible", "--name", ".", "getwindowname", "%@"]);
|
|
14856
15507
|
return out ? out.split("\n").filter(Boolean).slice(0, 20) : [];
|
|
14857
15508
|
}
|
|
15509
|
+
function recordingMousePosition() {
|
|
15510
|
+
const mouse = getMouseLocation();
|
|
15511
|
+
return mouse ? { x: mouse.x, y: mouse.y } : null;
|
|
15512
|
+
}
|
|
14858
15513
|
async function computerObserveCommand(path6, options = {}) {
|
|
14859
15514
|
const target = resolvePath(path6);
|
|
14860
|
-
|
|
15515
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
14861
15516
|
const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
|
|
14862
15517
|
const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
|
|
14863
15518
|
const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
|
|
@@ -14871,7 +15526,7 @@ async function computerObserveCommand(path6, options = {}) {
|
|
|
14871
15526
|
const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
|
|
14872
15527
|
const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
|
|
14873
15528
|
if (gridSize === null) {
|
|
14874
|
-
|
|
15529
|
+
copyFileSync2(rawPath, target);
|
|
14875
15530
|
} else {
|
|
14876
15531
|
overlayGrid(rawPath, target, capture.width, capture.height, gridSize, gridPath);
|
|
14877
15532
|
}
|
|
@@ -14890,8 +15545,8 @@ async function computerObserveCommand(path6, options = {}) {
|
|
|
14890
15545
|
gridSize
|
|
14891
15546
|
}, null, 2));
|
|
14892
15547
|
} finally {
|
|
14893
|
-
|
|
14894
|
-
|
|
15548
|
+
rmSync3(rawPath, { force: true });
|
|
15549
|
+
rmSync3(gridPath, { force: true });
|
|
14895
15550
|
}
|
|
14896
15551
|
}
|
|
14897
15552
|
async function fetchChromeJson(path6) {
|
|
@@ -15017,7 +15672,7 @@ async function evaluateChromeTarget(webSocketDebuggerUrl, expression) {
|
|
|
15017
15672
|
}));
|
|
15018
15673
|
});
|
|
15019
15674
|
ws.addEventListener("message", (event) => {
|
|
15020
|
-
const data = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf8");
|
|
15675
|
+
const data = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : Buffer.from(event.data).toString("utf8");
|
|
15021
15676
|
let parsed;
|
|
15022
15677
|
try {
|
|
15023
15678
|
parsed = JSON.parse(data);
|
|
@@ -15342,6 +15997,7 @@ async function computerClickCommand(xStr, yStr, options) {
|
|
|
15342
15997
|
}
|
|
15343
15998
|
}
|
|
15344
15999
|
runDisplayCmd("xdotool", args);
|
|
16000
|
+
logRecordingAction({ type: "click", x, y });
|
|
15345
16001
|
console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
|
|
15346
16002
|
}
|
|
15347
16003
|
async function computerMoveCommand(xStr, yStr) {
|
|
@@ -15349,15 +16005,20 @@ async function computerMoveCommand(xStr, yStr) {
|
|
|
15349
16005
|
const x = parseScreenCoord(xStr, "x", dimensions.width);
|
|
15350
16006
|
const y = parseScreenCoord(yStr, "y", dimensions.height);
|
|
15351
16007
|
runDisplayCmd("xdotool", ["mousemove", "--sync", String(x), String(y)]);
|
|
16008
|
+
logRecordingAction({ type: "move", x, y });
|
|
15352
16009
|
console.log(`moved to (${x},${y})`);
|
|
15353
16010
|
}
|
|
15354
16011
|
async function computerTypeCommand(text, options) {
|
|
15355
16012
|
const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
|
|
15356
16013
|
runDisplayCmd("xdotool", ["type", "--delay", String(delay), "--", text]);
|
|
16014
|
+
const position = recordingMousePosition();
|
|
16015
|
+
logRecordingAction({ type: "type", ...position ?? {} });
|
|
15357
16016
|
console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
|
|
15358
16017
|
}
|
|
15359
16018
|
async function computerKeyCommand(combo) {
|
|
15360
16019
|
runDisplayCmd("xdotool", ["key", "--", combo]);
|
|
16020
|
+
const position = recordingMousePosition();
|
|
16021
|
+
logRecordingAction({ type: "key", ...position ?? {} });
|
|
15361
16022
|
console.log(`pressed ${combo}`);
|
|
15362
16023
|
}
|
|
15363
16024
|
async function computerScrollCommand(direction, options) {
|
|
@@ -15367,17 +16028,24 @@ async function computerScrollCommand(direction, options) {
|
|
|
15367
16028
|
if (!button) fail(`direction must be one of up|down|left|right (got "${direction}")`);
|
|
15368
16029
|
const amount = options.amount ? parseCoord(options.amount, "--amount") : 3;
|
|
15369
16030
|
const args = [];
|
|
16031
|
+
let hoverPosition = null;
|
|
15370
16032
|
if (options.x && options.y) {
|
|
15371
16033
|
const dimensions = getDisplayDimensions();
|
|
16034
|
+
hoverPosition = {
|
|
16035
|
+
x: parseScreenCoord(options.x, "--x", dimensions.width),
|
|
16036
|
+
y: parseScreenCoord(options.y, "--y", dimensions.height)
|
|
16037
|
+
};
|
|
15372
16038
|
args.push(
|
|
15373
16039
|
"mousemove",
|
|
15374
16040
|
"--sync",
|
|
15375
|
-
String(
|
|
15376
|
-
String(
|
|
16041
|
+
String(hoverPosition.x),
|
|
16042
|
+
String(hoverPosition.y)
|
|
15377
16043
|
);
|
|
15378
16044
|
}
|
|
15379
16045
|
args.push("click", "--repeat", String(amount), "--delay", "30", button);
|
|
15380
16046
|
runDisplayCmd("xdotool", args);
|
|
16047
|
+
const position = hoverPosition ?? recordingMousePosition();
|
|
16048
|
+
logRecordingAction({ type: "scroll", ...position ?? {} });
|
|
15381
16049
|
console.log(`scrolled ${dir} x${amount}`);
|
|
15382
16050
|
}
|
|
15383
16051
|
async function computerDragCommand(fx, fy, tx, ty) {
|
|
@@ -15386,20 +16054,24 @@ async function computerDragCommand(fx, fy, tx, ty) {
|
|
|
15386
16054
|
const fromY = parseScreenCoord(fy, "fromY", dimensions.height);
|
|
15387
16055
|
const toX = parseScreenCoord(tx, "toX", dimensions.width);
|
|
15388
16056
|
const toY = parseScreenCoord(ty, "toY", dimensions.height);
|
|
15389
|
-
|
|
16057
|
+
const distance = Math.hypot(toX - fromX, toY - fromY);
|
|
16058
|
+
const steps = clamp(Math.ceil(distance / 70), 8, 24);
|
|
16059
|
+
const args = [
|
|
15390
16060
|
"mousemove",
|
|
15391
16061
|
"--sync",
|
|
15392
16062
|
String(fromX),
|
|
15393
16063
|
String(fromY),
|
|
15394
16064
|
"mousedown",
|
|
15395
|
-
"1",
|
|
15396
|
-
"mousemove",
|
|
15397
|
-
"--sync",
|
|
15398
|
-
String(toX),
|
|
15399
|
-
String(toY),
|
|
15400
|
-
"mouseup",
|
|
15401
16065
|
"1"
|
|
15402
|
-
]
|
|
16066
|
+
];
|
|
16067
|
+
for (let i = 1; i <= steps; i++) {
|
|
16068
|
+
const x = Math.round(fromX + (toX - fromX) * i / steps);
|
|
16069
|
+
const y = Math.round(fromY + (toY - fromY) * i / steps);
|
|
16070
|
+
args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
|
|
16071
|
+
}
|
|
16072
|
+
args.push("mouseup", "1");
|
|
16073
|
+
runDisplayCmd("xdotool", args);
|
|
16074
|
+
logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
|
|
15403
16075
|
console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
|
|
15404
16076
|
}
|
|
15405
16077
|
var APP_ALIASES = {
|
|
@@ -15422,10 +16094,10 @@ var LEGACY_CHROME_ALIAS = [
|
|
|
15422
16094
|
];
|
|
15423
16095
|
async function computerLaunchCommand(app, args) {
|
|
15424
16096
|
ensureServicesRunning();
|
|
15425
|
-
const baseArgs = app === "chrome" && !
|
|
16097
|
+
const baseArgs = app === "chrome" && !existsSync3(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
|
|
15426
16098
|
const bin = baseArgs[0];
|
|
15427
16099
|
const fullArgs = [...baseArgs.slice(1), ...args];
|
|
15428
|
-
const child =
|
|
16100
|
+
const child = spawn4(bin, fullArgs, {
|
|
15429
16101
|
env: withDisplay(),
|
|
15430
16102
|
detached: true,
|
|
15431
16103
|
stdio: "ignore"
|
|
@@ -15433,85 +16105,6 @@ async function computerLaunchCommand(app, args) {
|
|
|
15433
16105
|
child.unref();
|
|
15434
16106
|
console.log(`launched ${bin} (pid ${child.pid})`);
|
|
15435
16107
|
}
|
|
15436
|
-
var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
|
|
15437
|
-
async function computerRecordStartCommand(path6, options) {
|
|
15438
|
-
ensureServicesRunning();
|
|
15439
|
-
if (existsSync(RECORD_PID_FILE)) {
|
|
15440
|
-
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15441
|
-
if (Number.isFinite(pid)) {
|
|
15442
|
-
let alive = false;
|
|
15443
|
-
try {
|
|
15444
|
-
process.kill(pid, 0);
|
|
15445
|
-
alive = true;
|
|
15446
|
-
} catch {
|
|
15447
|
-
}
|
|
15448
|
-
if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
|
|
15449
|
-
}
|
|
15450
|
-
}
|
|
15451
|
-
const target = resolvePath(path6);
|
|
15452
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
15453
|
-
const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
|
|
15454
|
-
const width = process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH);
|
|
15455
|
-
const height = process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT);
|
|
15456
|
-
const child = spawn3("ffmpeg", [
|
|
15457
|
-
"-y",
|
|
15458
|
-
"-hide_banner",
|
|
15459
|
-
"-loglevel",
|
|
15460
|
-
"warning",
|
|
15461
|
-
"-f",
|
|
15462
|
-
"x11grab",
|
|
15463
|
-
"-framerate",
|
|
15464
|
-
String(fps),
|
|
15465
|
-
"-video_size",
|
|
15466
|
-
`${width}x${height}`,
|
|
15467
|
-
"-draw_mouse",
|
|
15468
|
-
"1",
|
|
15469
|
-
"-i",
|
|
15470
|
-
DEFAULT_DISPLAY,
|
|
15471
|
-
"-c:v",
|
|
15472
|
-
"libx264",
|
|
15473
|
-
"-preset",
|
|
15474
|
-
"ultrafast",
|
|
15475
|
-
"-tune",
|
|
15476
|
-
"zerolatency",
|
|
15477
|
-
"-crf",
|
|
15478
|
-
"23",
|
|
15479
|
-
"-pix_fmt",
|
|
15480
|
-
"yuv420p",
|
|
15481
|
-
"-movflags",
|
|
15482
|
-
"+faststart+frag_keyframe+empty_moov",
|
|
15483
|
-
target
|
|
15484
|
-
], { detached: true, stdio: "ignore" });
|
|
15485
|
-
child.unref();
|
|
15486
|
-
if (!child.pid) fail("failed to launch ffmpeg");
|
|
15487
|
-
mkdirSync(STATE_DIR, { recursive: true });
|
|
15488
|
-
writeFileSync(RECORD_PID_FILE, String(child.pid));
|
|
15489
|
-
writeFileSync(`${STATE_DIR}/recording-path.txt`, target);
|
|
15490
|
-
console.log(target);
|
|
15491
|
-
}
|
|
15492
|
-
async function computerRecordStopCommand() {
|
|
15493
|
-
if (!existsSync(RECORD_PID_FILE)) fail("no recording in progress");
|
|
15494
|
-
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15495
|
-
if (!Number.isFinite(pid)) fail("invalid recording pidfile");
|
|
15496
|
-
try {
|
|
15497
|
-
process.kill(pid, "SIGINT");
|
|
15498
|
-
} catch {
|
|
15499
|
-
}
|
|
15500
|
-
for (let i = 0; i < 30; i++) {
|
|
15501
|
-
try {
|
|
15502
|
-
process.kill(pid, 0);
|
|
15503
|
-
} catch {
|
|
15504
|
-
break;
|
|
15505
|
-
}
|
|
15506
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
15507
|
-
}
|
|
15508
|
-
rmSync(RECORD_PID_FILE, { force: true });
|
|
15509
|
-
const pathFile = `${STATE_DIR}/recording-path.txt`;
|
|
15510
|
-
if (existsSync(pathFile)) {
|
|
15511
|
-
console.log(readFileSync(pathFile, "utf8").trim());
|
|
15512
|
-
rmSync(pathFile, { force: true });
|
|
15513
|
-
}
|
|
15514
|
-
}
|
|
15515
16108
|
|
|
15516
16109
|
// src/commands/interactive.ts
|
|
15517
16110
|
import chalk21 from "chalk";
|