@deeeed/metamask-harness 0.92.5 → 0.92.7
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.92.7 - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Adopt the shared ProjectRuntime, ActionCatalog and ActionHandler contracts and pin their coordinated public package versions.
|
|
10
|
+
|
|
11
|
+
## 0.92.6 - 2026-10-15
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Pin adapter-web 0.10.2 and use the shared Android recorder and wallet HUD, preserving device selection, mirror fallback, recording failures and footer spacing. Remove the duplicate implementations and the unnecessary Android ffmpeg readiness check.
|
|
16
|
+
|
|
5
17
|
## 0.92.5 - 2026-10-15
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -111,11 +111,10 @@ async function selectMobileRecorder() {
|
|
|
111
111
|
const androidSerial = resolveAndroidDeviceSerial();
|
|
112
112
|
const iosSimulator = androidSerial ? void 0 : resolveMobileIosSimulatorName();
|
|
113
113
|
if (!androidSerial && !iosSimulator) return void 0;
|
|
114
|
+
const { createAndroidVideoRecorder, createIosSimulatorVideoRecorder } = await import("@farmslot/adapter-rn");
|
|
114
115
|
if (androidSerial && resolveAndroidRecorderKind(androidSerial) === "screenrecord") {
|
|
115
|
-
|
|
116
|
-
return { recorder: createAndroidScreenrecordRecorder(androidSerial), device: androidSerial };
|
|
116
|
+
return { recorder: createAndroidVideoRecorder(androidSerial), device: androidSerial };
|
|
117
117
|
}
|
|
118
|
-
const { createAndroidVideoRecorder, createIosSimulatorVideoRecorder } = await import("@farmslot/adapter-rn");
|
|
119
118
|
if (androidSerial) {
|
|
120
119
|
const { createAndroidMirrorVideoRecorder } = await import("@farmslot/recipe-runner");
|
|
121
120
|
const mirror = createAndroidMirrorVideoRecorder({ serial: androidSerial, fallback: createAndroidVideoRecorder(androidSerial) });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { isAppUrl } from '@farmslot/adapter-web/origin';
|
|
10
10
|
import { cdpListenerPids } from '@farmslot/adapter-web/web-dapp';
|
|
11
11
|
|
|
12
|
-
import { readHudState, resolveSlowMo } from './adapter-web-lib.mjs';
|
|
12
|
+
import { hudRenderExpression, readHudState, resolveSlowMo } from './adapter-web-lib.mjs';
|
|
13
13
|
import { appOrigin, cdpPort, readBrowserState, webDappRuntimeDir } from './runtime.mjs';
|
|
14
14
|
import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
|
|
15
15
|
|
|
@@ -77,6 +77,14 @@ export async function selectAppTarget(input) {
|
|
|
77
77
|
// step is drawn on the MetaMask page for the capture only, then removed. A
|
|
78
78
|
// finished or absent state (--hud hide) draws nothing.
|
|
79
79
|
class WalletSurfacePage extends CdpWebPage {
|
|
80
|
+
#origin;
|
|
81
|
+
|
|
82
|
+
constructor(session, targetUrl) {
|
|
83
|
+
super(session);
|
|
84
|
+
const target = new URL(targetUrl);
|
|
85
|
+
this.#origin = `${target.protocol}//${target.host}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
evaluate(expression) {
|
|
81
89
|
return this.evaluateInIsolatedWorld(expression, 'web-dapp-wallet-surface');
|
|
82
90
|
}
|
|
@@ -84,61 +92,16 @@ class WalletSurfacePage extends CdpWebPage {
|
|
|
84
92
|
async screenshot(context, relPath, metadata = {}) {
|
|
85
93
|
const state = context?.projectRoot ? readHudState(webDappRuntimeDir(context.projectRoot)) : null;
|
|
86
94
|
const drawn = state?.current && !state.finished
|
|
87
|
-
? await this.evaluate(
|
|
95
|
+
? await this.evaluate(hudRenderExpression(state, this.#origin, { layout: 'bottom-dock' })).then((result) => result?.hud === true, () => false)
|
|
88
96
|
: false;
|
|
89
97
|
try {
|
|
90
98
|
return await super.screenshot(context, relPath, metadata);
|
|
91
99
|
} finally {
|
|
92
|
-
if (drawn) await this.evaluate(
|
|
100
|
+
if (drawn) await this.evaluate(hudRenderExpression(null, this.#origin, { layout: 'bottom-dock' })).catch(() => {});
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
// The wallet-page HUD: a bar docked to the bottom edge that reserves its own
|
|
98
|
-
// height (a spacer after the content and the body's bottom padding), as the
|
|
99
|
-
// Extension's HUD does on MetaMask pages, so the request and its footer
|
|
100
|
-
// buttons move up instead of being covered. Inert, aria-hidden, pointer-events
|
|
101
|
-
// none. null removes it and restores the body padding.
|
|
102
|
-
export const WALLET_HUD_ID = 'mm-harness-wallet-hud';
|
|
103
|
-
|
|
104
|
-
export function walletHudExpression(state) {
|
|
105
|
-
const progress = state?.current?.progress;
|
|
106
|
-
const view = state ? {
|
|
107
|
-
badge: ['RUN', progress ? `${progress.current}/${progress.total}` : ''].filter(Boolean).join(' '),
|
|
108
|
-
intent: String(state.current?.intent ?? ''),
|
|
109
|
-
} : null;
|
|
110
|
-
return `(() => {
|
|
111
|
-
const view = ${JSON.stringify(view)};
|
|
112
|
-
const id = ${JSON.stringify(WALLET_HUD_ID)};
|
|
113
|
-
const old = document.getElementById(id);
|
|
114
|
-
if (old) {
|
|
115
|
-
document.body.style.paddingBottom = old.dataset.previousPadding || '';
|
|
116
|
-
old.remove();
|
|
117
|
-
document.getElementById(id + '-space')?.remove();
|
|
118
|
-
}
|
|
119
|
-
if (!view) return { hud: false, cleared: Boolean(old) };
|
|
120
|
-
const bar = document.createElement('div');
|
|
121
|
-
bar.id = id;
|
|
122
|
-
bar.setAttribute('aria-hidden', 'true');
|
|
123
|
-
bar.inert = true;
|
|
124
|
-
bar.dataset.previousPadding = document.body.style.paddingBottom;
|
|
125
|
-
Object.assign(bar.style, { position: 'fixed', left: '0', right: '0', bottom: '0', zIndex: '2147483647', pointerEvents: 'none', boxSizing: 'border-box', padding: '5px 8px', background: 'rgba(8, 10, 14, 0.9)', color: '#fff', font: '600 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', borderTop: '1px solid rgba(255,255,255,0.14)' });
|
|
126
|
-
const badge = document.createElement('span');
|
|
127
|
-
Object.assign(badge.style, { color: '#8ab4ff', fontSize: '9px', fontWeight: '800', marginRight: '6px' });
|
|
128
|
-
badge.textContent = view.badge;
|
|
129
|
-
bar.append(badge, view.intent);
|
|
130
|
-
document.body.append(bar);
|
|
131
|
-
const height = Math.ceil(bar.getBoundingClientRect().height);
|
|
132
|
-
const space = document.createElement('div');
|
|
133
|
-
space.id = id + '-space';
|
|
134
|
-
space.setAttribute('aria-hidden', 'true');
|
|
135
|
-
Object.assign(space.style, { display: 'block', height: height + 'px', pointerEvents: 'none' });
|
|
136
|
-
document.body.insertBefore(space, bar);
|
|
137
|
-
document.body.style.paddingBottom = height + 'px';
|
|
138
|
-
return { hud: true, docked: true, height };
|
|
139
|
-
})()`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
105
|
// Number inputs ignore select(), so the generic setInput would append to a
|
|
143
106
|
// prefilled value (the order form prefills the mid price). Clear the field with
|
|
144
107
|
// the editor's selectAll command, as a user's Cmd+A would, type over the
|
|
@@ -282,7 +245,7 @@ export async function withAppPage(input, callback, deps = PAGE_DEPS) {
|
|
|
282
245
|
await session.call('Page.enable');
|
|
283
246
|
await session.call('Runtime.enable');
|
|
284
247
|
if (focusBrowserRequested(deps.env)) await session.call('Page.bringToFront');
|
|
285
|
-
const page = surface === 'wallet' ? new WalletSurfacePage(session) : new AppPage(session);
|
|
248
|
+
const page = surface === 'wallet' ? new WalletSurfacePage(session, target.url) : new AppPage(session);
|
|
286
249
|
const result = await callback(page);
|
|
287
250
|
return result && typeof result === 'object' && !Array.isArray(result)
|
|
288
251
|
? { ...result, surface, cdpPort: port, targetUrl: target.url.replace(/0x[0-9a-fA-F]{40}/gu, '<address>') }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.92.
|
|
3
|
+
"version": "0.92.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-node": "0.1.
|
|
25
|
-
"@farmslot/adapter-rn": "0.22.
|
|
26
|
-
"@farmslot/adapter-sdk": "0.
|
|
27
|
-
"@farmslot/adapter-web": "0.10.
|
|
28
|
-
"@farmslot/agent-runtime": "0.
|
|
24
|
+
"@farmslot/adapter-node": "0.1.26",
|
|
25
|
+
"@farmslot/adapter-rn": "0.22.13",
|
|
26
|
+
"@farmslot/adapter-sdk": "0.15.0",
|
|
27
|
+
"@farmslot/adapter-web": "0.10.5",
|
|
28
|
+
"@farmslot/agent-runtime": "0.23.3",
|
|
29
29
|
"@farmslot/handoff": "^0.3.1",
|
|
30
|
-
"@farmslot/protocol": "0.
|
|
31
|
-
"@farmslot/recipe-cli": "0.18.
|
|
32
|
-
"@farmslot/recipe-runner": "0.
|
|
30
|
+
"@farmslot/protocol": "0.47.0",
|
|
31
|
+
"@farmslot/recipe-cli": "0.18.4",
|
|
32
|
+
"@farmslot/recipe-runner": "0.37.0",
|
|
33
33
|
"agent-device": "0.19.3",
|
|
34
34
|
"commander": "^12.0.0",
|
|
35
35
|
"esbuild": "0.28.1",
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { createAndroidVideoRecorder } from "@farmslot/adapter-rn";
|
|
5
|
-
import { optionalVideoTiming } from "@farmslot/recipe-runner";
|
|
6
|
-
import { recordingFailure } from "./recording-outcome.js";
|
|
7
|
-
const SEGMENT_SECONDS = 180;
|
|
8
|
-
const START_TIMEOUT_MS = 5e3;
|
|
9
|
-
const STOP_TIMEOUT_MS = 1e4;
|
|
10
|
-
const START_GRACE_MS = 1e3;
|
|
11
|
-
const HOST_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
12
|
-
function createAndroidScreenrecordRecorder(serial, options = {}) {
|
|
13
|
-
const adb = (args) => spawnSync("adb", ["-s", serial, ...args], { encoding: "utf8", timeout: 6e4 });
|
|
14
|
-
return {
|
|
15
|
-
name: "adb-screenrecord",
|
|
16
|
-
platform: "android",
|
|
17
|
-
async doctor() {
|
|
18
|
-
const device = await createAndroidVideoRecorder(serial).doctor();
|
|
19
|
-
if (!device.ok) return device;
|
|
20
|
-
const ffmpeg = spawnSync(ffmpegPath(), ["-version"], { encoding: "utf8", timeout: 1e4 });
|
|
21
|
-
if (ffmpeg.status !== 0) {
|
|
22
|
-
return {
|
|
23
|
-
ok: false,
|
|
24
|
-
code: "ffmpeg_unavailable",
|
|
25
|
-
message: `ffmpeg, which joins Android screenrecord files, is unavailable: ${failure(ffmpeg)}`,
|
|
26
|
-
suggestedFix: "Install ffmpeg (brew install ffmpeg) or set FFMPEG_PATH."
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return { ok: true, code: "ok", message: `Android on-device screen recording is ready on ${serial}.` };
|
|
30
|
-
},
|
|
31
|
-
start: (request) => startRecording(serial, request, adb, options)
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
async function startRecording(serial, request, adb, options) {
|
|
35
|
-
if (request.target.kind !== "android-device" || request.target.serial !== serial) {
|
|
36
|
-
throw new Error("Android recording target does not match the selected device.");
|
|
37
|
-
}
|
|
38
|
-
const pollMs = options.pollMs ?? 100;
|
|
39
|
-
const host = options.host ?? process;
|
|
40
|
-
const running = screenrecordPids(adb);
|
|
41
|
-
if (running.length > 0) {
|
|
42
|
-
throw new Error(`Android screenrecord is already running on ${serial} (pid ${running.join(", ")}). Stop that recording before retrying.`);
|
|
43
|
-
}
|
|
44
|
-
const prefix = `${options.remoteDir ?? "/sdcard/Download"}/mm-harness-recording-${Date.now()}-${process.pid}`;
|
|
45
|
-
const segments = [];
|
|
46
|
-
let stopping = false;
|
|
47
|
-
let interruption;
|
|
48
|
-
let chain = Promise.resolve();
|
|
49
|
-
const localFiles = /* @__PURE__ */ new Set();
|
|
50
|
-
const segmentFailure = (segment) => `screenrecord on ${serial} stopped with exit ${segment.exitCode ?? "error"}: ${segment.output.trim() || "no recorder output"}`;
|
|
51
|
-
const startSegment = async () => {
|
|
52
|
-
const remotePath = `${prefix}-${segments.length}.mp4`;
|
|
53
|
-
const startedAtUnixMs = Date.now();
|
|
54
|
-
const child = spawn(
|
|
55
|
-
"adb",
|
|
56
|
-
["-s", serial, "shell", "screenrecord", "--time-limit", String(options.segmentSeconds ?? SEGMENT_SECONDS), remotePath],
|
|
57
|
-
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
58
|
-
);
|
|
59
|
-
const exited = new Promise((resolve) => {
|
|
60
|
-
child.once("error", () => resolve(null));
|
|
61
|
-
child.once("exit", (code) => resolve(code));
|
|
62
|
-
});
|
|
63
|
-
const segment = { remotePath, child, output: "", startedAtUnixMs, exited };
|
|
64
|
-
child.stderr?.setEncoding("utf8");
|
|
65
|
-
child.stderr?.on("data", (chunk) => {
|
|
66
|
-
segment.output = (segment.output + chunk).slice(-2e3);
|
|
67
|
-
});
|
|
68
|
-
segments.push(segment);
|
|
69
|
-
void exited.then((code) => {
|
|
70
|
-
segment.stoppedAtUnixMs = Date.now();
|
|
71
|
-
segment.exitCode = code;
|
|
72
|
-
if (!segment.pid || segment.killed || interruption || stopping) return;
|
|
73
|
-
if (code !== 0) {
|
|
74
|
-
interruption = segmentFailure(segment);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
chain = startSegment().catch((error) => {
|
|
78
|
-
interruption ??= error instanceof Error ? error.message : String(error);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
const deadline = Date.now() + (options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
82
|
-
while (Date.now() < deadline) {
|
|
83
|
-
if (segment.stoppedAtUnixMs !== void 0) {
|
|
84
|
-
throw new Error(`Android screenrecord exited before capture started on ${serial}: ${segment.output.trim() || "no recorder output"}`);
|
|
85
|
-
}
|
|
86
|
-
const pid = screenrecordPids(adb)[0];
|
|
87
|
-
if (pid) {
|
|
88
|
-
segment.pid = pid;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
await delay(pollMs);
|
|
92
|
-
}
|
|
93
|
-
child.kill("SIGTERM");
|
|
94
|
-
throw new Error(`Android screenrecord did not start on ${serial} within ${options.startTimeoutMs ?? START_TIMEOUT_MS}ms.`);
|
|
95
|
-
};
|
|
96
|
-
const abandon = () => {
|
|
97
|
-
const live = segments.filter((segment) => segment.stoppedAtUnixMs === void 0);
|
|
98
|
-
for (const segment of live) segment.killed = true;
|
|
99
|
-
if (live.length > 0) {
|
|
100
|
-
const starting = live.some((segment) => !segment.pid);
|
|
101
|
-
const graceUntil = Date.now() + (starting ? START_GRACE_MS : 0);
|
|
102
|
-
const giveUpAt = Date.now() + (options.stopTimeoutMs ?? STOP_TIMEOUT_MS);
|
|
103
|
-
const signaled = /* @__PURE__ */ new Set();
|
|
104
|
-
while (Date.now() < giveUpAt) {
|
|
105
|
-
const pids = screenrecordPids(adb).filter((pid) => starting || live.some((segment) => segment.pid === pid));
|
|
106
|
-
if (pids.length === 0 && Date.now() >= graceUntil) break;
|
|
107
|
-
for (const pid of pids) {
|
|
108
|
-
if (!signaled.has(pid)) adb(["shell", "kill", "-2", pid]);
|
|
109
|
-
signaled.add(pid);
|
|
110
|
-
}
|
|
111
|
-
sleepSync(pollMs);
|
|
112
|
-
}
|
|
113
|
-
for (const segment of live) segment.child.kill("SIGTERM");
|
|
114
|
-
}
|
|
115
|
-
if (segments.length > 0) adb(["shell", "rm", "-f", ...segments.map((segment) => segment.remotePath)]);
|
|
116
|
-
for (const localPath of localFiles) fs.rmSync(localPath, { force: true });
|
|
117
|
-
};
|
|
118
|
-
const onExit = () => abandon();
|
|
119
|
-
const signalHandlers = Object.fromEntries(
|
|
120
|
-
HOST_SIGNALS.map((signal) => [
|
|
121
|
-
signal,
|
|
122
|
-
() => {
|
|
123
|
-
stopping = true;
|
|
124
|
-
abandon();
|
|
125
|
-
removeGuards();
|
|
126
|
-
if (host.listenerCount(signal) === 0) host.kill(host.pid, signal);
|
|
127
|
-
}
|
|
128
|
-
])
|
|
129
|
-
);
|
|
130
|
-
const removeGuards = () => {
|
|
131
|
-
host.removeListener("exit", onExit);
|
|
132
|
-
for (const signal of HOST_SIGNALS) host.removeListener(signal, signalHandlers[signal]);
|
|
133
|
-
};
|
|
134
|
-
host.once("exit", onExit);
|
|
135
|
-
for (const signal of HOST_SIGNALS) host.once(signal, signalHandlers[signal]);
|
|
136
|
-
try {
|
|
137
|
-
await startSegment();
|
|
138
|
-
} catch (error) {
|
|
139
|
-
stopping = true;
|
|
140
|
-
abandon();
|
|
141
|
-
removeGuards();
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
let stopped = false;
|
|
145
|
-
return {
|
|
146
|
-
async stop() {
|
|
147
|
-
if (stopped) throw new Error(`Android screen recording on ${serial} was already stopped.`);
|
|
148
|
-
stopped = true;
|
|
149
|
-
stopping = true;
|
|
150
|
-
const stagingDir = path.dirname(request.outputPath);
|
|
151
|
-
try {
|
|
152
|
-
await chain;
|
|
153
|
-
const tail = segments.at(-1);
|
|
154
|
-
if (tail.stoppedAtUnixMs === void 0 && tail.pid) adb(["shell", "kill", "-2", tail.pid]);
|
|
155
|
-
if (!await settles(tail.exited, options.stopTimeoutMs ?? STOP_TIMEOUT_MS)) {
|
|
156
|
-
tail.killed = true;
|
|
157
|
-
tail.child.kill("SIGKILL");
|
|
158
|
-
throw new Error(`Android screenrecord on ${serial} did not stop within ${options.stopTimeoutMs ?? STOP_TIMEOUT_MS}ms.`);
|
|
159
|
-
}
|
|
160
|
-
const kept = interruption ? segments.filter((segment) => segment.exitCode === 0) : tail.exitCode === 0 ? segments : segments.slice(0, -1);
|
|
161
|
-
if (kept.length === 0) {
|
|
162
|
-
throw recordingFailure(interruption ? "RECORDER_INTERRUPTED" : "RECORDING_FAILED", interruption ?? segmentFailure(tail));
|
|
163
|
-
}
|
|
164
|
-
fs.mkdirSync(stagingDir, { recursive: true });
|
|
165
|
-
const recorded = [];
|
|
166
|
-
for (const [index, segment] of kept.entries()) {
|
|
167
|
-
const localPath = path.join(stagingDir, `${path.basename(request.outputPath)}.segment-${index}.mp4`);
|
|
168
|
-
localFiles.add(localPath);
|
|
169
|
-
const pull = adb(["pull", segment.remotePath, localPath]);
|
|
170
|
-
if (pull.status !== 0) throw new Error(`Could not pull Android recording from ${serial}: ${failure(pull)}`);
|
|
171
|
-
const bytes = fs.statSync(localPath).size;
|
|
172
|
-
if (bytes > 0) recorded.push({ ...segment, localPath, bytes });
|
|
173
|
-
}
|
|
174
|
-
if (recorded.length === 0) throw recordingFailure("RECORDING_FAILED", `Android recording output is empty: ${request.outputPath}`);
|
|
175
|
-
const startedAtUnixMs = recorded[0].startedAtUnixMs;
|
|
176
|
-
const placed = recorded.map((segment) => ({ ...segment, offsetMs: segment.startedAtUnixMs - startedAtUnixMs }));
|
|
177
|
-
const timingEvidencePath = `${path.basename(request.outputPath)}.segments.json`;
|
|
178
|
-
fs.writeFileSync(path.join(stagingDir, timingEvidencePath), `${JSON.stringify({
|
|
179
|
-
recorder: "adb-screenrecord",
|
|
180
|
-
serial,
|
|
181
|
-
segments: placed.map(({ offsetMs, startedAtUnixMs: startedAtUnixMs2, stoppedAtUnixMs, bytes }) => ({
|
|
182
|
-
offsetMs,
|
|
183
|
-
startedAtUnixMs: startedAtUnixMs2,
|
|
184
|
-
stoppedAtUnixMs,
|
|
185
|
-
bytes
|
|
186
|
-
}))
|
|
187
|
-
})}
|
|
188
|
-
`);
|
|
189
|
-
const recorder = { name: "adb-screenrecord", platform: "android", target: { selector: "adb-serial", value: serial } };
|
|
190
|
-
const originals = await Promise.all(recorded.map(async (segment, index) => ({
|
|
191
|
-
path: path.basename(segment.localPath),
|
|
192
|
-
...index > 0 ? { gapBefore: true } : {},
|
|
193
|
-
...await optionalVideoTiming(segment.localPath, {
|
|
194
|
-
startedAtUnixMs: segment.startedAtUnixMs,
|
|
195
|
-
stoppedAtUnixMs: segment.stoppedAtUnixMs
|
|
196
|
-
}),
|
|
197
|
-
timingEvidencePath,
|
|
198
|
-
recorder
|
|
199
|
-
})));
|
|
200
|
-
for (const segment of recorded) localFiles.delete(segment.localPath);
|
|
201
|
-
return {
|
|
202
|
-
segments: originals,
|
|
203
|
-
...interruption ? { interruption: {
|
|
204
|
-
frames: originals.reduce((sum, segment) => sum + (segment.timing?.framesMs.length ?? 0), 0),
|
|
205
|
-
mediaTimeMs: placed.at(-1).offsetMs + (originals.at(-1).timing?.framesMs.at(-1) ?? 0),
|
|
206
|
-
cause: interruption
|
|
207
|
-
} } : {}
|
|
208
|
-
};
|
|
209
|
-
} finally {
|
|
210
|
-
abandon();
|
|
211
|
-
removeGuards();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function ffmpegPath() {
|
|
217
|
-
return process.env.FFMPEG_PATH || "ffmpeg";
|
|
218
|
-
}
|
|
219
|
-
function screenrecordPids(adb) {
|
|
220
|
-
const result = adb(["shell", "pidof", "screenrecord"]);
|
|
221
|
-
if (result.status !== 0) return [];
|
|
222
|
-
return result.stdout.trim().split(/\s+/u).filter(Boolean);
|
|
223
|
-
}
|
|
224
|
-
function settles(promise, ms) {
|
|
225
|
-
return new Promise((resolve) => {
|
|
226
|
-
const timer = setTimeout(() => resolve(false), ms);
|
|
227
|
-
void promise.then(() => {
|
|
228
|
-
clearTimeout(timer);
|
|
229
|
-
resolve(true);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
function failure(result) {
|
|
234
|
-
return result.error?.message || result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? "unknown"}`;
|
|
235
|
-
}
|
|
236
|
-
function sleepSync(ms) {
|
|
237
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
238
|
-
}
|
|
239
|
-
function delay(ms) {
|
|
240
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
241
|
-
}
|
|
242
|
-
export {
|
|
243
|
-
createAndroidScreenrecordRecorder
|
|
244
|
-
};
|