chrome-relay 0.4.0 → 0.5.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/dist/cli.js +166 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { writeFileSync } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
var CHROME_RELAY_VERSION = true ? "0.
|
|
8
|
+
var CHROME_RELAY_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
9
9
|
|
|
10
10
|
// src/install/install.ts
|
|
11
11
|
import os from "os";
|
|
@@ -644,6 +644,171 @@ Notes:
|
|
|
644
644
|
if (typeof opts.limit === "number") args.limit = opts.limit;
|
|
645
645
|
await run("chrome_console", args);
|
|
646
646
|
});
|
|
647
|
+
tabOpt(
|
|
648
|
+
program.command("hover [selector]").description("Move the pointer over an element or coordinates. Fires :hover styles.").option("--x <px>", "explicit x coordinate (CSS pixels)", (v) => Number(v)).option("--y <px>", "explicit y coordinate (CSS pixels)", (v) => Number(v)).addHelpText(
|
|
649
|
+
"after",
|
|
650
|
+
`
|
|
651
|
+
|
|
652
|
+
Examples:
|
|
653
|
+
chrome-relay hover --tab 123 'button[title="Install runner"]'
|
|
654
|
+
chrome-relay hover --tab 123 --x 1327 --y 771
|
|
655
|
+
|
|
656
|
+
Use before screencast to capture hover-driven micro-states (button glow,
|
|
657
|
+
tooltip appearance, etc.) that a bare click would skip past too quickly.
|
|
658
|
+
`
|
|
659
|
+
)
|
|
660
|
+
).action(async (selector, opts) => {
|
|
661
|
+
const args = {};
|
|
662
|
+
Object.assign(args, baseArgs(opts));
|
|
663
|
+
if (selector) args.selector = selector;
|
|
664
|
+
if (typeof opts.x === "number" && typeof opts.y === "number") {
|
|
665
|
+
args.x = opts.x;
|
|
666
|
+
args.y = opts.y;
|
|
667
|
+
}
|
|
668
|
+
await run("chrome_hover", args);
|
|
669
|
+
});
|
|
670
|
+
const screencast = program.command("screencast").description("Record a tab via CDP (paint-driven). Requires an active tab.").addHelpText(
|
|
671
|
+
"after",
|
|
672
|
+
`
|
|
673
|
+
|
|
674
|
+
Examples:
|
|
675
|
+
chrome-relay screencast start --tab 123 --quality 80 --max-width 900
|
|
676
|
+
# ... drive the interaction (hover, click, etc.) ...
|
|
677
|
+
chrome-relay screencast stop --tab 123 --out /tmp/recording
|
|
678
|
+
|
|
679
|
+
# The --out path becomes a directory of frame_NNNN.jpg files. If ffmpeg
|
|
680
|
+
# is on PATH and --gif is also passed, an animated GIF is written next to
|
|
681
|
+
# the frames at /tmp/recording.gif.
|
|
682
|
+
|
|
683
|
+
Notes:
|
|
684
|
+
Frames buffer in the extension service worker. A 10-second capture at
|
|
685
|
+
default settings (jpeg q=60, ~15fps, full viewport) lands ~2-3 MB.
|
|
686
|
+
Pass --max-width to downscale and lighten the buffer.
|
|
687
|
+
Each frame is base64 JPEG; the CLI decodes them when --out is given.
|
|
688
|
+
`
|
|
689
|
+
);
|
|
690
|
+
tabOpt(
|
|
691
|
+
screencast.command("start").description("Begin screencast capture on a tab.").option("--format <fmt>", "jpeg | png (default jpeg)").option("--quality <n>", "jpeg quality 0-100 (default 80)", (v) => Number(v)).option("--max-width <px>", "downscale; aspect preserved", (v) => Number(v)).option("--max-height <px>", "downscale; aspect preserved", (v) => Number(v)).option("--every-nth <n>", "throttle: keep 1 in N frames (default 1)", (v) => Number(v))
|
|
692
|
+
).action(async (opts) => {
|
|
693
|
+
const args = { action: "start" };
|
|
694
|
+
Object.assign(args, baseArgs(opts));
|
|
695
|
+
if (opts.format) args.format = opts.format;
|
|
696
|
+
if (typeof opts.quality === "number") args.quality = opts.quality;
|
|
697
|
+
if (typeof opts.maxWidth === "number") args.maxWidth = opts.maxWidth;
|
|
698
|
+
if (typeof opts.maxHeight === "number") args.maxHeight = opts.maxHeight;
|
|
699
|
+
if (typeof opts.everyNth === "number") args.everyNthFrame = opts.everyNth;
|
|
700
|
+
await run("chrome_screencast", args);
|
|
701
|
+
});
|
|
702
|
+
tabOpt(
|
|
703
|
+
screencast.command("stop").description("Stop the screencast and emit frames (or write to disk).").option("-o, --out <dir>", "write frames as JPEGs into this directory (created if missing)").option("--gif", "after writing frames, ffmpeg them into <dir>.gif").option("--mp4", "after writing frames, ffmpeg them into <dir>.mp4").option("--fps <n>", "assumed framerate when invoking ffmpeg (default 15)", (v) => Number(v)).option("--no-dedupe", "keep raw frames; default collapses consecutive identical frames via SHA-256")
|
|
704
|
+
).action(async (opts) => {
|
|
705
|
+
const args = { action: "stop" };
|
|
706
|
+
Object.assign(args, baseArgs(opts));
|
|
707
|
+
try {
|
|
708
|
+
const result = await callTool("chrome_screencast", args);
|
|
709
|
+
if (!opts.out) {
|
|
710
|
+
const { frames, ...summary } = result;
|
|
711
|
+
process.stdout.write(JSON.stringify({ ...summary, framesOmitted: frames.length, hint: "pass --out <dir> to save" }, null, 2) + "\n");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const { mkdirSync, writeFileSync: writeFileSync2, renameSync, unlinkSync } = await import("fs");
|
|
715
|
+
const path2 = await import("path");
|
|
716
|
+
const { createHash } = await import("crypto");
|
|
717
|
+
mkdirSync(opts.out, { recursive: true });
|
|
718
|
+
result.frames.forEach((f, i) => {
|
|
719
|
+
const name = `frame_${String(i + 1).padStart(4, "0")}.jpg`;
|
|
720
|
+
writeFileSync2(path2.join(opts.out, name), Buffer.from(f.data, "base64"));
|
|
721
|
+
});
|
|
722
|
+
process.stdout.write(`Wrote ${result.frames.length} frames to ${opts.out}
|
|
723
|
+
`);
|
|
724
|
+
const dedupeOn = opts.dedupe !== false;
|
|
725
|
+
if (dedupeOn && result.frames.length > 1) {
|
|
726
|
+
const hashes = result.frames.map(
|
|
727
|
+
(f) => createHash("sha256").update(Buffer.from(f.data, "base64")).digest("hex")
|
|
728
|
+
);
|
|
729
|
+
const kept = [];
|
|
730
|
+
let prev = "";
|
|
731
|
+
hashes.forEach((h, i) => {
|
|
732
|
+
if (h !== prev) kept.push(i);
|
|
733
|
+
prev = h;
|
|
734
|
+
});
|
|
735
|
+
const dropped = result.frames.length - kept.length;
|
|
736
|
+
if (dropped > 0) {
|
|
737
|
+
for (let i = 0; i < result.frames.length; i++) {
|
|
738
|
+
const src = path2.join(opts.out, `frame_${String(i + 1).padStart(4, "0")}.jpg`);
|
|
739
|
+
try {
|
|
740
|
+
unlinkSync(src);
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
kept.forEach((srcIdx, newIdx) => {
|
|
745
|
+
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
746
|
+
writeFileSync2(tmp, Buffer.from(result.frames[srcIdx].data, "base64"));
|
|
747
|
+
});
|
|
748
|
+
kept.forEach((_, newIdx) => {
|
|
749
|
+
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
750
|
+
const final = path2.join(opts.out, `frame_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
751
|
+
renameSync(tmp, final);
|
|
752
|
+
});
|
|
753
|
+
process.stdout.write(`Deduped: dropped ${dropped} identical frames, ${kept.length} remain.
|
|
754
|
+
`);
|
|
755
|
+
} else {
|
|
756
|
+
process.stdout.write(`Deduped: no consecutive duplicates found.
|
|
757
|
+
`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (opts.gif || opts.mp4) {
|
|
761
|
+
const fps = typeof opts.fps === "number" ? opts.fps : 15;
|
|
762
|
+
const { spawnSync } = await import("child_process");
|
|
763
|
+
const which = spawnSync("which", ["ffmpeg"]);
|
|
764
|
+
if (which.status !== 0) {
|
|
765
|
+
process.stderr.write("[chrome-relay] ffmpeg not on PATH \u2014 skipping --gif/--mp4.\n");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (opts.gif) {
|
|
769
|
+
const gifOut = `${opts.out.replace(/\/$/, "")}.gif`;
|
|
770
|
+
const r = spawnSync("ffmpeg", [
|
|
771
|
+
"-y",
|
|
772
|
+
"-framerate",
|
|
773
|
+
String(fps),
|
|
774
|
+
"-i",
|
|
775
|
+
path2.join(opts.out, "frame_%04d.jpg"),
|
|
776
|
+
"-vf",
|
|
777
|
+
`fps=${fps},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
|
|
778
|
+
"-loop",
|
|
779
|
+
"0",
|
|
780
|
+
gifOut
|
|
781
|
+
], { stdio: "inherit" });
|
|
782
|
+
if (r.status === 0) process.stdout.write(`Wrote ${gifOut}
|
|
783
|
+
`);
|
|
784
|
+
}
|
|
785
|
+
if (opts.mp4) {
|
|
786
|
+
const mp4Out = `${opts.out.replace(/\/$/, "")}.mp4`;
|
|
787
|
+
const r = spawnSync("ffmpeg", [
|
|
788
|
+
"-y",
|
|
789
|
+
"-framerate",
|
|
790
|
+
String(fps),
|
|
791
|
+
"-i",
|
|
792
|
+
path2.join(opts.out, "frame_%04d.jpg"),
|
|
793
|
+
"-c:v",
|
|
794
|
+
"libx264",
|
|
795
|
+
"-pix_fmt",
|
|
796
|
+
"yuv420p",
|
|
797
|
+
"-crf",
|
|
798
|
+
"20",
|
|
799
|
+
mp4Out
|
|
800
|
+
], { stdio: "inherit" });
|
|
801
|
+
if (r.status === 0) process.stdout.write(`Wrote ${mp4Out}
|
|
802
|
+
`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
process.stderr.write(
|
|
807
|
+
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
808
|
+
);
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
647
812
|
return program;
|
|
648
813
|
}
|
|
649
814
|
|
package/dist/index.js
CHANGED