cdp-toolkit 1.4.0 → 1.6.0
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 +96 -0
- package/CONTRACT.md +3 -1
- package/README.md +21 -17
- package/dist/cli.js +399 -30
- package/dist/index.js +384 -15
- package/dist/mcp.js +470 -31
- package/package.json +3 -2
- package/src/bidi/driver.ts +4 -0
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +3 -0
- package/src/driver.ts +4 -1
- package/src/index.ts +15 -4
- package/src/leases-tools.ts +1 -1
- package/src/leases.ts +1 -1
- package/src/manifest.ts +70 -0
- package/src/mcp.ts +1 -1
- package/src/shared-tools.ts +2 -1
- package/src/tools/screencast.ts +741 -0
package/dist/cli.js
CHANGED
|
@@ -682,6 +682,8 @@ var REQUIRED_CAPABILITIES = {
|
|
|
682
682
|
performance_trace: ["trace.performance"],
|
|
683
683
|
take_heapsnapshot: ["heap.snapshot"],
|
|
684
684
|
lighthouse_audit: ["audit.lighthouse"],
|
|
685
|
+
start_screen_recording: ["capture.screencast"],
|
|
686
|
+
stop_screen_recording: ["capture.screencast"],
|
|
685
687
|
mock_request: ["network.intercept"],
|
|
686
688
|
list_mocks: ["network.intercept"],
|
|
687
689
|
clear_mocks: ["network.intercept"]
|
|
@@ -937,7 +939,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
937
939
|
"snapshot.accessibilityTree",
|
|
938
940
|
"input.insertTextAtomic",
|
|
939
941
|
"locate.text",
|
|
940
|
-
"locate.xpath"
|
|
942
|
+
"locate.xpath",
|
|
943
|
+
"capture.screencast"
|
|
941
944
|
]);
|
|
942
945
|
|
|
943
946
|
class CdpPageDriver {
|
|
@@ -2579,11 +2582,375 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2579
2582
|
return { ...analyzeEvents(events), tracePath: args.tracePath };
|
|
2580
2583
|
}
|
|
2581
2584
|
|
|
2582
|
-
// src/tools/
|
|
2583
|
-
import {
|
|
2585
|
+
// src/tools/screencast.ts
|
|
2586
|
+
import { spawn } from "node:child_process";
|
|
2587
|
+
import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2584
2588
|
import { join as join5 } from "node:path";
|
|
2585
2589
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2586
|
-
|
|
2590
|
+
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
2591
|
+
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
2592
|
+
return ledger.map((entry, i) => {
|
|
2593
|
+
const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
|
|
2594
|
+
return Math.max(1, Math.round(next - entry.timestamp));
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
var CONCAT_GRID_MS = 40;
|
|
2598
|
+
function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
|
|
2599
|
+
if (!ledger.length)
|
|
2600
|
+
return [];
|
|
2601
|
+
const raw = frameDurationsMs(ledger, stopWallclockMs);
|
|
2602
|
+
let elapsed = 0;
|
|
2603
|
+
const kept = [];
|
|
2604
|
+
let lastSlot = -1;
|
|
2605
|
+
for (let i = 0;i < ledger.length; i++) {
|
|
2606
|
+
const slot = Math.round(elapsed / gridMs);
|
|
2607
|
+
if (slot > lastSlot) {
|
|
2608
|
+
kept.push({ file: ledger[i].file, slot });
|
|
2609
|
+
lastSlot = slot;
|
|
2610
|
+
}
|
|
2611
|
+
elapsed += raw[i];
|
|
2612
|
+
}
|
|
2613
|
+
const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
|
|
2614
|
+
return kept.map((frame, i) => ({
|
|
2615
|
+
file: frame.file,
|
|
2616
|
+
durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
|
|
2617
|
+
}));
|
|
2618
|
+
}
|
|
2619
|
+
function buildConcatManifest(frames) {
|
|
2620
|
+
if (!frames.length)
|
|
2621
|
+
throw new CdpError("cannot build a concat manifest from an empty frame ledger");
|
|
2622
|
+
const lines = ["ffconcat version 1.0"];
|
|
2623
|
+
for (const frame of frames) {
|
|
2624
|
+
lines.push(`file ${frame.file}`);
|
|
2625
|
+
lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
|
|
2626
|
+
}
|
|
2627
|
+
lines.push(`file ${frames[frames.length - 1].file}`);
|
|
2628
|
+
return `${lines.join(`
|
|
2629
|
+
`)}
|
|
2630
|
+
`;
|
|
2631
|
+
}
|
|
2632
|
+
function evenDimensions(width, height) {
|
|
2633
|
+
return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
|
|
2634
|
+
}
|
|
2635
|
+
function pngSize(buf) {
|
|
2636
|
+
if (buf.length < 24)
|
|
2637
|
+
return;
|
|
2638
|
+
if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
|
|
2639
|
+
return;
|
|
2640
|
+
if (buf.toString("latin1", 12, 16) !== "IHDR")
|
|
2641
|
+
return;
|
|
2642
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
2643
|
+
}
|
|
2644
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
2645
|
+
function jpegSize(buf) {
|
|
2646
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
2647
|
+
return;
|
|
2648
|
+
let i = 2;
|
|
2649
|
+
while (i + 3 < buf.length) {
|
|
2650
|
+
if (buf[i] !== 255) {
|
|
2651
|
+
i += 1;
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
const marker = buf[i + 1];
|
|
2655
|
+
if (marker === 255) {
|
|
2656
|
+
i += 1;
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
2660
|
+
i += 2;
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
if (i + 3 >= buf.length)
|
|
2664
|
+
return;
|
|
2665
|
+
const segLen = buf.readUInt16BE(i + 2);
|
|
2666
|
+
if (JPEG_SOF.has(marker)) {
|
|
2667
|
+
if (i + 9 > buf.length)
|
|
2668
|
+
return;
|
|
2669
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
2670
|
+
}
|
|
2671
|
+
if (segLen < 2)
|
|
2672
|
+
return;
|
|
2673
|
+
i += 2 + segLen;
|
|
2674
|
+
}
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
function imageSize(buf) {
|
|
2678
|
+
return pngSize(buf) ?? jpegSize(buf);
|
|
2679
|
+
}
|
|
2680
|
+
var ENCODER_LADDER = [
|
|
2681
|
+
{ encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
|
|
2682
|
+
{ encoder: "h264_videotoolbox", codec: "h264" },
|
|
2683
|
+
{ encoder: "libx265", codec: "hevc", tag: "hvc1" },
|
|
2684
|
+
{ encoder: "libx264", codec: "h264" }
|
|
2685
|
+
];
|
|
2686
|
+
function ffmpegEncoderNames(encodersOutput) {
|
|
2687
|
+
const names = new Set;
|
|
2688
|
+
for (const line of encodersOutput.split(`
|
|
2689
|
+
`)) {
|
|
2690
|
+
const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
|
|
2691
|
+
if (m)
|
|
2692
|
+
names.add(m[1]);
|
|
2693
|
+
}
|
|
2694
|
+
return names;
|
|
2695
|
+
}
|
|
2696
|
+
function selectEncoder(encodersOutput) {
|
|
2697
|
+
const available = ffmpegEncoderNames(encodersOutput);
|
|
2698
|
+
for (const candidate of ENCODER_LADDER) {
|
|
2699
|
+
if (available.has(candidate.encoder))
|
|
2700
|
+
return candidate;
|
|
2701
|
+
}
|
|
2702
|
+
throw new CdpError(`ffmpeg reports none of the supported video encoders (${ENCODER_LADDER.map((c) => c.encoder).join(", ")}). ` + "Install a full ffmpeg build: 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu).");
|
|
2703
|
+
}
|
|
2704
|
+
function buildFfmpegArgs(manifestPath, outPath, choice) {
|
|
2705
|
+
return [
|
|
2706
|
+
"-hide_banner",
|
|
2707
|
+
"-loglevel",
|
|
2708
|
+
"error",
|
|
2709
|
+
"-y",
|
|
2710
|
+
"-f",
|
|
2711
|
+
"concat",
|
|
2712
|
+
"-safe",
|
|
2713
|
+
"0",
|
|
2714
|
+
"-i",
|
|
2715
|
+
manifestPath,
|
|
2716
|
+
"-vsync",
|
|
2717
|
+
"vfr",
|
|
2718
|
+
"-vf",
|
|
2719
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
2720
|
+
"-c:v",
|
|
2721
|
+
choice.encoder,
|
|
2722
|
+
...choice.tag ? ["-tag:v", choice.tag] : [],
|
|
2723
|
+
"-pix_fmt",
|
|
2724
|
+
"yuv420p",
|
|
2725
|
+
"-movflags",
|
|
2726
|
+
"+faststart",
|
|
2727
|
+
"-an",
|
|
2728
|
+
outPath
|
|
2729
|
+
];
|
|
2730
|
+
}
|
|
2731
|
+
function run(bin, args) {
|
|
2732
|
+
return new Promise((resolve, reject) => {
|
|
2733
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2734
|
+
let stdout = "";
|
|
2735
|
+
let stderr = "";
|
|
2736
|
+
child.stdout?.on("data", (chunk) => {
|
|
2737
|
+
stdout += chunk.toString("utf8");
|
|
2738
|
+
});
|
|
2739
|
+
child.stderr?.on("data", (chunk) => {
|
|
2740
|
+
stderr += chunk.toString("utf8");
|
|
2741
|
+
});
|
|
2742
|
+
child.on("error", reject);
|
|
2743
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
var encoderProbe;
|
|
2747
|
+
var FFMPEG_MISSING = `ffmpeg is required to encode a screen recording but '${FFMPEG_BIN}' could not be executed. ` + "Install it with 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu), " + "or point CDP_FFMPEG at the binary.";
|
|
2748
|
+
async function probeEncoder() {
|
|
2749
|
+
if (!encoderProbe) {
|
|
2750
|
+
encoderProbe = (async () => {
|
|
2751
|
+
let res;
|
|
2752
|
+
try {
|
|
2753
|
+
res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
|
|
2756
|
+
}
|
|
2757
|
+
if (res.code !== 0)
|
|
2758
|
+
throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
|
|
2759
|
+
return selectEncoder(`${res.stdout}
|
|
2760
|
+
${res.stderr}`);
|
|
2761
|
+
})();
|
|
2762
|
+
encoderProbe.catch(() => {
|
|
2763
|
+
encoderProbe = undefined;
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
return encoderProbe;
|
|
2767
|
+
}
|
|
2768
|
+
var liveRecordings = new Map;
|
|
2769
|
+
function shortId2(id) {
|
|
2770
|
+
return id.slice(0, 8);
|
|
2771
|
+
}
|
|
2772
|
+
function assertNotRecording(targetId, activeIds) {
|
|
2773
|
+
if (activeIds.includes(targetId)) {
|
|
2774
|
+
throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function chooseRecordingId(activeIds, selector) {
|
|
2778
|
+
if (!activeIds.length) {
|
|
2779
|
+
throw new CdpError("no screen recording is in progress in this process. start_screen_recording parks a live CDP connection in memory, so a recording cannot be stopped from a different process than the one that started it (the CLI is one process per call: use the MCP server for the pair).");
|
|
2780
|
+
}
|
|
2781
|
+
if (selector === undefined || selector === "active") {
|
|
2782
|
+
if (activeIds.length === 1)
|
|
2783
|
+
return activeIds[0];
|
|
2784
|
+
throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
|
|
2785
|
+
}
|
|
2786
|
+
return activeIds.find((id) => id === selector);
|
|
2787
|
+
}
|
|
2788
|
+
function stamp3() {
|
|
2789
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2790
|
+
}
|
|
2791
|
+
function target32(t) {
|
|
2792
|
+
return { id: t.id, url: t.url, title: t.title };
|
|
2793
|
+
}
|
|
2794
|
+
async function startScreenRecording(args = {}) {
|
|
2795
|
+
const format = args.format ?? "jpeg";
|
|
2796
|
+
if (format !== "jpeg" && format !== "png")
|
|
2797
|
+
throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
|
|
2798
|
+
if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
|
|
2799
|
+
throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
|
|
2800
|
+
}
|
|
2801
|
+
const encoder = await probeEncoder();
|
|
2802
|
+
const { conn, target } = await openPage(args.target);
|
|
2803
|
+
try {
|
|
2804
|
+
assertNotRecording(target.id, [...liveRecordings.keys()]);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
conn.close();
|
|
2807
|
+
throw err;
|
|
2808
|
+
}
|
|
2809
|
+
const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
2810
|
+
try {
|
|
2811
|
+
await mkdir6(spoolDir, { recursive: true });
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
conn.close();
|
|
2814
|
+
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
2815
|
+
}
|
|
2816
|
+
const ext = format === "png" ? "png" : "jpg";
|
|
2817
|
+
const rec = {
|
|
2818
|
+
conn,
|
|
2819
|
+
target,
|
|
2820
|
+
spoolDir,
|
|
2821
|
+
ledger: [],
|
|
2822
|
+
startedAt: Date.now(),
|
|
2823
|
+
format,
|
|
2824
|
+
encoder,
|
|
2825
|
+
writes: Promise.resolve(),
|
|
2826
|
+
dropped: 0,
|
|
2827
|
+
unwritten: new Set,
|
|
2828
|
+
off: () => {}
|
|
2829
|
+
};
|
|
2830
|
+
let seq = 0;
|
|
2831
|
+
rec.off = conn.on("Page.screencastFrame", (params) => {
|
|
2832
|
+
const sessionId = params.sessionId;
|
|
2833
|
+
if (typeof sessionId === "number") {
|
|
2834
|
+
conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
|
|
2835
|
+
}
|
|
2836
|
+
const data = params.data;
|
|
2837
|
+
if (typeof data !== "string" || !data.length) {
|
|
2838
|
+
rec.dropped += 1;
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
const meta = params.metadata;
|
|
2842
|
+
const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
|
|
2843
|
+
const buf = Buffer.from(data, "base64");
|
|
2844
|
+
const size = imageSize(buf);
|
|
2845
|
+
if (!size || size.width <= 0 || size.height <= 0) {
|
|
2846
|
+
rec.dropped += 1;
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
seq += 1;
|
|
2850
|
+
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
2851
|
+
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
2852
|
+
rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
|
|
2853
|
+
rec.dropped += 1;
|
|
2854
|
+
rec.unwritten.add(file);
|
|
2855
|
+
});
|
|
2856
|
+
});
|
|
2857
|
+
try {
|
|
2858
|
+
await conn.send("Page.enable");
|
|
2859
|
+
if (args.bringToFront)
|
|
2860
|
+
await conn.send("Page.bringToFront").catch(() => {});
|
|
2861
|
+
const params = { format };
|
|
2862
|
+
if (format === "jpeg" && args.quality !== undefined)
|
|
2863
|
+
params.quality = args.quality;
|
|
2864
|
+
if (args.maxWidth !== undefined)
|
|
2865
|
+
params.maxWidth = args.maxWidth;
|
|
2866
|
+
if (args.maxHeight !== undefined)
|
|
2867
|
+
params.maxHeight = args.maxHeight;
|
|
2868
|
+
if (args.everyNthFrame !== undefined)
|
|
2869
|
+
params.everyNthFrame = args.everyNthFrame;
|
|
2870
|
+
await conn.send("Page.startScreencast", params);
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
rec.off();
|
|
2873
|
+
conn.close();
|
|
2874
|
+
await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2875
|
+
throw err;
|
|
2876
|
+
}
|
|
2877
|
+
liveRecordings.set(target.id, rec);
|
|
2878
|
+
return {
|
|
2879
|
+
target: target32(target),
|
|
2880
|
+
format,
|
|
2881
|
+
spoolDir,
|
|
2882
|
+
encoder: encoder.encoder,
|
|
2883
|
+
codec: encoder.codec,
|
|
2884
|
+
startedAt: rec.startedAt,
|
|
2885
|
+
note: "Recording. Call stop_screen_recording in THIS process to encode the MP4; frames only arrive on repaint, so a still page yields one long-held frame."
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
async function pickRecording(selector) {
|
|
2889
|
+
const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
|
|
2890
|
+
if (chosen)
|
|
2891
|
+
return liveRecordings.get(chosen);
|
|
2892
|
+
const { conn, target } = await openPage(selector);
|
|
2893
|
+
conn.close();
|
|
2894
|
+
const hit = liveRecordings.get(target.id);
|
|
2895
|
+
if (!hit) {
|
|
2896
|
+
throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
|
|
2897
|
+
}
|
|
2898
|
+
return hit;
|
|
2899
|
+
}
|
|
2900
|
+
async function stopScreenRecording(args = {}) {
|
|
2901
|
+
const rec = await pickRecording(args.target);
|
|
2902
|
+
rec.off();
|
|
2903
|
+
try {
|
|
2904
|
+
await rec.conn.send("Page.stopScreencast");
|
|
2905
|
+
} catch {}
|
|
2906
|
+
const stoppedAt = Date.now();
|
|
2907
|
+
await rec.writes.catch(() => {});
|
|
2908
|
+
rec.conn.close();
|
|
2909
|
+
liveRecordings.delete(rec.target.id);
|
|
2910
|
+
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
2911
|
+
if (!ledger.length) {
|
|
2912
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2913
|
+
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
2914
|
+
}
|
|
2915
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2916
|
+
const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
2917
|
+
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
2918
|
+
const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
|
|
2919
|
+
await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
|
|
2920
|
+
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
2921
|
+
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
2922
|
+
let res;
|
|
2923
|
+
try {
|
|
2924
|
+
res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
|
|
2927
|
+
}
|
|
2928
|
+
if (res.code !== 0) {
|
|
2929
|
+
throw new CdpError(`ffmpeg (${rec.encoder.encoder}) exited ${res.code} encoding ${frames.length} frames. Spooled frames kept at ${rec.spoolDir}, re-run by hand with: ` + `${FFMPEG_BIN} ${buildFfmpegArgs(manifestPath, outPath, rec.encoder).join(" ")}
|
|
2930
|
+
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
2931
|
+
}
|
|
2932
|
+
const { size } = await stat2(outPath);
|
|
2933
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2934
|
+
return {
|
|
2935
|
+
path: outPath,
|
|
2936
|
+
bytes: size,
|
|
2937
|
+
durationMs,
|
|
2938
|
+
frameCount: ledger.length,
|
|
2939
|
+
encodedFrames: frames.length,
|
|
2940
|
+
codec: rec.encoder.codec,
|
|
2941
|
+
encoder: rec.encoder.encoder,
|
|
2942
|
+
width,
|
|
2943
|
+
height,
|
|
2944
|
+
droppedFrames: rec.dropped,
|
|
2945
|
+
target: target32(rec.target)
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/tools/heap.ts
|
|
2950
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
2951
|
+
import { join as join6 } from "node:path";
|
|
2952
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2953
|
+
function stamp4(targetId) {
|
|
2587
2954
|
const short = targetId.slice(0, 8);
|
|
2588
2955
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2589
2956
|
return `${short}-${iso}`;
|
|
@@ -2606,9 +2973,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2606
2973
|
if (snapshot.length === 0) {
|
|
2607
2974
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2608
2975
|
}
|
|
2609
|
-
await
|
|
2610
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2611
|
-
await
|
|
2976
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2977
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
2978
|
+
await writeFile7(path, snapshot, "utf8");
|
|
2612
2979
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2613
2980
|
return {
|
|
2614
2981
|
path,
|
|
@@ -2620,15 +2987,15 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2620
2987
|
}
|
|
2621
2988
|
|
|
2622
2989
|
// src/tools/lighthouse.ts
|
|
2623
|
-
import { spawn } from "node:child_process";
|
|
2624
|
-
import { mkdir as
|
|
2625
|
-
var
|
|
2990
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2991
|
+
import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
2992
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2626
2993
|
function cdpPort() {
|
|
2627
2994
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
2628
2995
|
const m = base.match(/:(\d+)/);
|
|
2629
2996
|
return m?.[1] ? Number(m[1]) : 9222;
|
|
2630
2997
|
}
|
|
2631
|
-
function
|
|
2998
|
+
function stamp5() {
|
|
2632
2999
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2633
3000
|
}
|
|
2634
3001
|
function shortHost(url) {
|
|
@@ -2645,7 +3012,7 @@ function runProcess(cmd, args, opts) {
|
|
|
2645
3012
|
let stdout = "";
|
|
2646
3013
|
let stderr = "";
|
|
2647
3014
|
let settled = false;
|
|
2648
|
-
const child =
|
|
3015
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2649
3016
|
const timer = setTimeout(() => {
|
|
2650
3017
|
if (settled)
|
|
2651
3018
|
return;
|
|
@@ -2682,8 +3049,8 @@ async function lighthouseAudit(args) {
|
|
|
2682
3049
|
const url = args.url.trim();
|
|
2683
3050
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2684
3051
|
const port = cdpPort();
|
|
2685
|
-
await
|
|
2686
|
-
const outPath = args.savePath ?? `${
|
|
3052
|
+
await mkdir8(ARTIFACT_DIR6, { recursive: true });
|
|
3053
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
2687
3054
|
const formFactor = args.formFactor ?? "desktop";
|
|
2688
3055
|
const lhArgs = [
|
|
2689
3056
|
"--yes",
|
|
@@ -2738,7 +3105,7 @@ async function lighthouseAudit(args) {
|
|
|
2738
3105
|
if (Object.keys(categories).length === 0) {
|
|
2739
3106
|
throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
|
|
2740
3107
|
}
|
|
2741
|
-
const { size } = await
|
|
3108
|
+
const { size } = await stat3(outPath);
|
|
2742
3109
|
return {
|
|
2743
3110
|
path: outPath,
|
|
2744
3111
|
bytes: size,
|
|
@@ -2792,6 +3159,8 @@ var TOOLS = {
|
|
|
2792
3159
|
performance_stop_trace: performanceStopTrace,
|
|
2793
3160
|
performance_analyze_insight: performanceAnalyzeInsight,
|
|
2794
3161
|
performance_trace: performanceTrace,
|
|
3162
|
+
start_screen_recording: startScreenRecording,
|
|
3163
|
+
stop_screen_recording: stopScreenRecording,
|
|
2795
3164
|
take_heapsnapshot: takeHeapsnapshot,
|
|
2796
3165
|
lighthouse_audit: lighthouseAudit,
|
|
2797
3166
|
mock_request: mockRequest,
|
|
@@ -3341,8 +3710,8 @@ function hostFromUrl(url, toolName) {
|
|
|
3341
3710
|
function driverError2(code, message, data) {
|
|
3342
3711
|
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
3343
3712
|
}
|
|
3344
|
-
function encodeUid2(
|
|
3345
|
-
return `bidi:${
|
|
3713
|
+
function encodeUid2(stamp6) {
|
|
3714
|
+
return `bidi:${stamp6}`;
|
|
3346
3715
|
}
|
|
3347
3716
|
function decodeStamp(uid) {
|
|
3348
3717
|
if (!uid.startsWith("bidi:"))
|
|
@@ -3449,7 +3818,7 @@ function parseSnapshotLine(line) {
|
|
|
3449
3818
|
const m = LINE_RE.exec(line);
|
|
3450
3819
|
if (!m)
|
|
3451
3820
|
return;
|
|
3452
|
-
const indent = m[1] ?? "",
|
|
3821
|
+
const indent = m[1] ?? "", stamp6 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
|
|
3453
3822
|
const depth = indent.length / 2;
|
|
3454
3823
|
let name;
|
|
3455
3824
|
if (nameLit) {
|
|
@@ -3477,7 +3846,7 @@ function parseSnapshotLine(line) {
|
|
|
3477
3846
|
extras[key] = val;
|
|
3478
3847
|
}
|
|
3479
3848
|
}
|
|
3480
|
-
return { uid: encodeUid2(
|
|
3849
|
+
return { uid: encodeUid2(stamp6), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
|
|
3481
3850
|
}
|
|
3482
3851
|
var INTERACTIVE_ROLES3 = new Set([
|
|
3483
3852
|
"button",
|
|
@@ -3510,8 +3879,8 @@ function sharedRefOf(node, describe) {
|
|
|
3510
3879
|
}
|
|
3511
3880
|
async function resolveElementLocator2(conn, contextId, loc) {
|
|
3512
3881
|
if ("uid" in loc) {
|
|
3513
|
-
const
|
|
3514
|
-
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
3882
|
+
const stamp6 = decodeStamp(loc.uid);
|
|
3883
|
+
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
3515
3884
|
if (!nodes2.length)
|
|
3516
3885
|
throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
|
|
3517
3886
|
return sharedRefOf(nodes2[0], loc.uid);
|
|
@@ -3723,14 +4092,14 @@ class BidiPageDriver {
|
|
|
3723
4092
|
return loc.uid;
|
|
3724
4093
|
}
|
|
3725
4094
|
const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
|
|
3726
|
-
const
|
|
3727
|
-
if (typeof
|
|
4095
|
+
const stamp6 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
|
|
4096
|
+
if (typeof stamp6 !== "string")
|
|
3728
4097
|
throw driverError2("page-error", "stamping the located element did not return a string");
|
|
3729
|
-
return encodeUid2(
|
|
4098
|
+
return encodeUid2(stamp6);
|
|
3730
4099
|
}
|
|
3731
4100
|
async resolve(uid) {
|
|
3732
|
-
const
|
|
3733
|
-
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
4101
|
+
const stamp6 = decodeStamp(uid);
|
|
4102
|
+
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
3734
4103
|
if (!nodes.length)
|
|
3735
4104
|
throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
|
|
3736
4105
|
}
|
|
@@ -4159,11 +4528,11 @@ function createFirefoxDriver(port, opts) {
|
|
|
4159
4528
|
}
|
|
4160
4529
|
|
|
4161
4530
|
// src/bidi/launch.ts
|
|
4162
|
-
import { spawn as
|
|
4531
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4163
4532
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
4164
4533
|
import { createConnection, createServer } from "node:net";
|
|
4165
4534
|
import { tmpdir } from "node:os";
|
|
4166
|
-
import { join as
|
|
4535
|
+
import { join as join7 } from "node:path";
|
|
4167
4536
|
function resolveBinary() {
|
|
4168
4537
|
const candidates = [
|
|
4169
4538
|
process.env.FIREFOX_BIN,
|
|
@@ -4237,11 +4606,11 @@ async function launchFirefox(opts = {}) {
|
|
|
4237
4606
|
const binary = resolveBinary();
|
|
4238
4607
|
const port = await pickPort(opts.port);
|
|
4239
4608
|
const isThrowaway = opts.profilePath === undefined;
|
|
4240
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
4609
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join7(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
4241
4610
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
4242
4611
|
if (opts.headless)
|
|
4243
4612
|
args.push("--headless");
|
|
4244
|
-
const proc =
|
|
4613
|
+
const proc = spawn3(binary, args, { stdio: "ignore", detached: false });
|
|
4245
4614
|
let closed = false;
|
|
4246
4615
|
const close = async () => {
|
|
4247
4616
|
if (closed)
|