jev-cdp 0.1.6 → 0.1.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/README.md +6 -6
- package/dist/cli.js +226 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ This is an early experimental port. See [NOTICE.md](NOTICE.md) for source attrib
|
|
|
27
27
|
Run the published CLI without adding it to a project. Bun must be installed for either command:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
bunx jev-cdp@0.1.
|
|
31
|
-
npx -y jev-cdp@0.1.
|
|
30
|
+
bunx jev-cdp@0.1.7 help run
|
|
31
|
+
npx -y jev-cdp@0.1.7 help run
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg with H.264 encoding (`libx264`) is required for `--recording`. Jev selects `-fps_mode vfr` when FFmpeg supports it and falls back to `-vsync vfr` for older builds. Run `jev-cdp doctor` to check the installed encoder with a short MP4 encode and decode.
|
|
@@ -96,13 +96,13 @@ bun run run -- run \
|
|
|
96
96
|
--final-state
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`--recording` captures Chrome's compositor screencast stream for the full goal and renders
|
|
99
|
+
`--recording` captures Chrome's compositor screencast stream for the full goal and renders a 1280×720 H.264 MP4. A 40-pixel browser bar shows the active tab's URL above the page. Chrome uses a 1506×800 viewport, scaled to fit the video beneath the bar, so controls remain at their observed coordinates while more of the page fits on screen. Because Chrome's native pointer is not part of the stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay after opening or loading a page, changing the URL within a page, or moving to a click target before pressing the mouse. Jev chooses during page pauses, and the adapter waits only for any time left before acting. Jev does not choose or observe those delays. When an action opens a new tab, the recording shows a "New tab opened" notice over the previous tab for the full configured pause, then switches to the new tab. With zero pause, the notice appears for one frame without delaying Jev. `--screenshot` saves a 1280×720 image with the same browser bar, even when recording is off. If a new tab opened, the final screenshot also includes the notice.
|
|
100
100
|
|
|
101
101
|
`jev-cdp run` writes JSON Lines to standard output: one `type:"action"` object per executed action, followed by one `type:"result"` object with the final status and action budget. Action objects include elapsed execution time, the operation, page and tab URLs, the viewport, and a CSS selector, role, name, frame, and coordinates for the element. Fill actions include the entered text; password fields and `--field-value-env` values are redacted and must be supplied separately for replay. Select and scroll actions include their option value or wheel delta. The CSS selector and page URL can be used as Playwright replay targets, with the coordinates as a fallback at the recorded viewport size. `--final-state` adds the final semantic page snapshot: a frame tree with URLs and loading state, actionable elements with frame identity, screen bounds, nearby text, region, clickability, and any element covering the click point, plus new-tab and redirect transitions. Automatic navigation and embedded-content waits use `--wait-budget-ms` (15 seconds by default) and do not consume `--max-steps`; a timeout returns `wait_timeout`, its pending condition and elapsed wait time, and the latest semantic state. Runtime failures emit a `type:"result"` object with `status:"error"`; diagnostic text uses standard error.
|
|
102
102
|
|
|
103
103
|
Each action object includes `consoleErrors` observed during that step. The result object includes `initialConsoleErrors` already present when attaching to the tab and `consoleErrors` for the full run. These fields include browser console errors, uncaught exceptions, and error-level DevTools log entries from the page and its frames. Review them alongside the semantic state; a console error does not by itself establish that the goal failed.
|
|
104
104
|
|
|
105
|
-
The snapshot includes visible controls in nested iframes, including cross-origin frames. Clicking a link there uses its frame coordinates and checks that the observed control is still current. If that click opens a new tab, Jev switches to it, returns its target ID for the next goal, and keeps the
|
|
105
|
+
The snapshot includes visible controls in nested iframes, including cross-origin frames. Clicking a link there uses its frame coordinates and checks that the observed control is still current. If that click opens a new tab, Jev switches to it, returns its target ID for the next goal, and keeps the 1506×800 viewport consistent across the switch.
|
|
106
106
|
|
|
107
107
|
## Supply known field values without another LLM
|
|
108
108
|
|
|
@@ -184,9 +184,9 @@ The context is disposed after evidence capture by default. Combine it with `--ke
|
|
|
184
184
|
| `--cdp URL` | `CHROME_CDP_URL` | `http://127.0.0.1:9222` | Chrome DevTools HTTP endpoint |
|
|
185
185
|
| `--tab TARGET_ID` | — | create a new tab | Attach to one exact existing Chrome page target |
|
|
186
186
|
| `tabs` | — | — | Print target IDs, titles, and URLs for open page tabs |
|
|
187
|
-
| `--recording PATH.mp4` | — | off | Record
|
|
187
|
+
| `--recording PATH.mp4` | — | off | Record a 1280×720 video with a URL bar, animated cursor, and tab notices |
|
|
188
188
|
| `--interaction-pauses MS` | — | `0` | Pause after page loads and before clicks, overlapping page pauses with Jev decisions |
|
|
189
|
-
| `--screenshot PATH.jpg` | — | off | Save the
|
|
189
|
+
| `--screenshot PATH.jpg` | — | off | Save a 1280×720 image with the URL bar and any new-tab notice |
|
|
190
190
|
| `--final-state` | — | off | Include the final semantic page state in stdout JSON |
|
|
191
191
|
| `--field-value LABEL=VALUE` | — | Luna fallback | Type caller-provided test data into the exactly labeled field |
|
|
192
192
|
| `--field-value-env LABEL=NAME` | — | off | Read a sensitive field value from an environment variable |
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "jev-cdp",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.7",
|
|
7
7
|
description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
|
|
8
8
|
type: "module",
|
|
9
9
|
license: "MIT",
|
|
@@ -225,6 +225,82 @@ file '${quoted}'
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// src/recording-visual.ts
|
|
229
|
+
var VIDEO_WIDTH = 1280;
|
|
230
|
+
var VIDEO_HEIGHT = 720;
|
|
231
|
+
var BAR_HEIGHT = 40;
|
|
232
|
+
var PAGE_WIDTH = 1506;
|
|
233
|
+
var PAGE_HEIGHT = 800;
|
|
234
|
+
function composeFrameExpression(jpeg, url, newTabUrl) {
|
|
235
|
+
return `(async () => {
|
|
236
|
+
const bytes = Uint8Array.from(atob(${JSON.stringify(jpeg)}), character => character.charCodeAt(0));
|
|
237
|
+
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/jpeg' }));
|
|
238
|
+
const canvas = document.createElement('canvas');
|
|
239
|
+
canvas.width = ${VIDEO_WIDTH}; canvas.height = ${VIDEO_HEIGHT};
|
|
240
|
+
const context = canvas.getContext('2d');
|
|
241
|
+
if (!context) throw new Error('Could not create recording canvas');
|
|
242
|
+
context.fillStyle = '#f5f7fa';
|
|
243
|
+
context.fillRect(0, 0, canvas.width, ${BAR_HEIGHT});
|
|
244
|
+
context.fillStyle = '#ffffff';
|
|
245
|
+
context.beginPath();
|
|
246
|
+
context.roundRect(12, 6, canvas.width - 24, 28, 7);
|
|
247
|
+
context.fill();
|
|
248
|
+
context.strokeStyle = '#d7dce4';
|
|
249
|
+
context.lineWidth = 1;
|
|
250
|
+
context.stroke();
|
|
251
|
+
context.fillStyle = '#617185';
|
|
252
|
+
context.beginPath(); context.arc(28, 20, 4, 0, Math.PI * 2); context.fill();
|
|
253
|
+
const fit = (value, maxWidth) => {
|
|
254
|
+
if (context.measureText(value).width <= maxWidth) return value;
|
|
255
|
+
while (value.length && context.measureText(value + '\u2026').width > maxWidth) value = value.slice(0, -1);
|
|
256
|
+
return value + '\u2026';
|
|
257
|
+
};
|
|
258
|
+
const url = ${JSON.stringify(url)};
|
|
259
|
+
context.font = '14px system-ui, sans-serif';
|
|
260
|
+
context.fillStyle = '#263343';
|
|
261
|
+
context.textBaseline = 'middle';
|
|
262
|
+
context.fillText(fit(url, canvas.width - 72), 42, 20);
|
|
263
|
+
const chromeBorder = '#61738a';
|
|
264
|
+
context.fillStyle = chromeBorder;
|
|
265
|
+
context.fillRect(0, ${BAR_HEIGHT - 2}, canvas.width, 2);
|
|
266
|
+
context.drawImage(bitmap, 0, ${BAR_HEIGHT}, canvas.width, canvas.height - ${BAR_HEIGHT});
|
|
267
|
+
bitmap.close();
|
|
268
|
+
const newTabUrl = ${JSON.stringify(newTabUrl)};
|
|
269
|
+
if (newTabUrl) {
|
|
270
|
+
const cardWidth = 620, cardHeight = 104, radius = 12, x = (canvas.width - cardWidth) / 2;
|
|
271
|
+
const cardPath = () => {
|
|
272
|
+
context.beginPath();
|
|
273
|
+
context.moveTo(x, 0);
|
|
274
|
+
context.lineTo(x + cardWidth, 0);
|
|
275
|
+
context.lineTo(x + cardWidth, cardHeight - radius);
|
|
276
|
+
context.quadraticCurveTo(x + cardWidth, cardHeight, x + cardWidth - radius, cardHeight);
|
|
277
|
+
context.lineTo(x + radius, cardHeight);
|
|
278
|
+
context.quadraticCurveTo(x, cardHeight, x, cardHeight - radius);
|
|
279
|
+
context.closePath();
|
|
280
|
+
};
|
|
281
|
+
context.save();
|
|
282
|
+
context.shadowColor = 'rgba(15, 23, 42, .24)';
|
|
283
|
+
context.shadowBlur = 14;
|
|
284
|
+
context.shadowOffsetY = 5;
|
|
285
|
+
context.fillStyle = '#ffffff';
|
|
286
|
+
cardPath(); context.fill();
|
|
287
|
+
context.restore();
|
|
288
|
+
context.strokeStyle = chromeBorder;
|
|
289
|
+
context.lineWidth = 1.5;
|
|
290
|
+
cardPath(); context.stroke();
|
|
291
|
+
context.fillStyle = '#2563eb';
|
|
292
|
+
context.beginPath(); context.arc(x + 28, 35, 7, 0, Math.PI * 2); context.fill();
|
|
293
|
+
context.fillStyle = '#172033';
|
|
294
|
+
context.font = '600 19px system-ui, sans-serif';
|
|
295
|
+
context.fillText('New tab opened', x + 48, 35);
|
|
296
|
+
context.fillStyle = '#536277';
|
|
297
|
+
context.font = '14px system-ui, sans-serif';
|
|
298
|
+
context.fillText(fit(newTabUrl, cardWidth - 52), x + 24, 72);
|
|
299
|
+
}
|
|
300
|
+
return canvas.toDataURL('image/jpeg', .88).split(',')[1];
|
|
301
|
+
})()`;
|
|
302
|
+
}
|
|
303
|
+
|
|
228
304
|
// src/snapshot.js
|
|
229
305
|
var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1ad2 under the MIT License.
|
|
230
306
|
(() => {
|
|
@@ -417,6 +493,12 @@ class Browser {
|
|
|
417
493
|
#recordingStartedAt = 0;
|
|
418
494
|
#recordingSequence = 0;
|
|
419
495
|
#recordingWrites = Promise.resolve();
|
|
496
|
+
#activeUrl = "about:blank";
|
|
497
|
+
#recordingUrlEvents = [];
|
|
498
|
+
#newTabNoticeUntil = 0;
|
|
499
|
+
#newTabNoticeUrl = null;
|
|
500
|
+
#newTabNoticeWindows = [];
|
|
501
|
+
#hadNewTab = false;
|
|
420
502
|
#stopRecordingEvents;
|
|
421
503
|
#stopPageEvents = [];
|
|
422
504
|
#mainFrameId;
|
|
@@ -512,6 +594,8 @@ class Browser {
|
|
|
512
594
|
}
|
|
513
595
|
if (!info?.targetId || !info.url || info.url === "about:blank")
|
|
514
596
|
return;
|
|
597
|
+
if (info.targetId === browser.#targetId)
|
|
598
|
+
browser.setActiveUrl(info.url);
|
|
515
599
|
const urls = browser.#targetUrls.get(info.targetId) ?? [];
|
|
516
600
|
if (urls.at(-1) !== info.url)
|
|
517
601
|
urls.push(info.url);
|
|
@@ -537,8 +621,8 @@ class Browser {
|
|
|
537
621
|
await browser.watchConsole();
|
|
538
622
|
await browser.call("Page.enable");
|
|
539
623
|
await browser.call("Emulation.setDeviceMetricsOverride", {
|
|
540
|
-
width:
|
|
541
|
-
height:
|
|
624
|
+
width: PAGE_WIDTH,
|
|
625
|
+
height: PAGE_HEIGHT,
|
|
542
626
|
deviceScaleFactor: 1,
|
|
543
627
|
mobile: false
|
|
544
628
|
});
|
|
@@ -547,6 +631,7 @@ class Browser {
|
|
|
547
631
|
if (options.url)
|
|
548
632
|
await browser.call("Page.navigate", { url: options.url });
|
|
549
633
|
await browser.waitForReady();
|
|
634
|
+
browser.#activeUrl = await browser.evaluate("location.href") ?? browser.#activeUrl;
|
|
550
635
|
browser.#pauseUntil = performance.now() + browser.#interactionPauses;
|
|
551
636
|
await browser.startRecording();
|
|
552
637
|
return browser;
|
|
@@ -563,6 +648,18 @@ class Browser {
|
|
|
563
648
|
get targetId() {
|
|
564
649
|
return this.#targetId;
|
|
565
650
|
}
|
|
651
|
+
setActiveUrl(url) {
|
|
652
|
+
if (!url || url === this.#activeUrl)
|
|
653
|
+
return;
|
|
654
|
+
this.#activeUrl = url;
|
|
655
|
+
if (this.#recordingDirectory && this.#recordingFrames.length) {
|
|
656
|
+
this.#recordingUrlEvents.push({
|
|
657
|
+
elapsedMs: performance.now() - this.#recordingStartedAt,
|
|
658
|
+
targetId: this.#targetId,
|
|
659
|
+
url
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
566
663
|
takeConsoleErrors() {
|
|
567
664
|
return this.#consoleErrors.splice(0);
|
|
568
665
|
}
|
|
@@ -606,6 +703,8 @@ class Browser {
|
|
|
606
703
|
await this.call("Page.enable");
|
|
607
704
|
const tree = await this.call("Page.getFrameTree");
|
|
608
705
|
this.#mainFrameId = tree.frameTree.frame.id;
|
|
706
|
+
if (tree.frameTree.frame.url)
|
|
707
|
+
this.setActiveUrl(tree.frameTree.frame.url);
|
|
609
708
|
await this.call("Runtime.addBinding", { name: "__jevDomChanged" });
|
|
610
709
|
const observeDom = `(() => {
|
|
611
710
|
if (window.__jevDomWatching) return;
|
|
@@ -642,8 +741,11 @@ class Browser {
|
|
|
642
741
|
this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
|
|
643
742
|
const frame = params.frame;
|
|
644
743
|
this.signalChange();
|
|
645
|
-
if (frame && frame.id === this.#mainFrameId && !frame.parentId)
|
|
744
|
+
if (frame && frame.id === this.#mainFrameId && !frame.parentId) {
|
|
745
|
+
if (frame.url)
|
|
746
|
+
this.setActiveUrl(frame.url);
|
|
646
747
|
loading({ frameId: frame.id });
|
|
748
|
+
}
|
|
647
749
|
}), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
|
|
648
750
|
if (typeof params.frameId === "string")
|
|
649
751
|
this.#loadingFrames.delete(params.frameId);
|
|
@@ -653,6 +755,8 @@ class Browser {
|
|
|
653
755
|
}), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
|
|
654
756
|
this.signalChange();
|
|
655
757
|
if (params.frameId === this.#mainFrameId) {
|
|
758
|
+
if (typeof params.url === "string")
|
|
759
|
+
this.setActiveUrl(params.url);
|
|
656
760
|
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
657
761
|
this.resetRecordingCursor().catch(() => {
|
|
658
762
|
return;
|
|
@@ -728,7 +832,14 @@ class Browser {
|
|
|
728
832
|
await Bun.write(initialPath, Buffer.from(initial.data, "base64"));
|
|
729
833
|
if (firstSegment)
|
|
730
834
|
this.#recordingStartedAt = performance.now();
|
|
731
|
-
|
|
835
|
+
const initialElapsedMs = firstSegment ? 0 : performance.now() - this.#recordingStartedAt;
|
|
836
|
+
this.#recordingFrames.push({
|
|
837
|
+
path: initialPath,
|
|
838
|
+
elapsedMs: initialElapsedMs,
|
|
839
|
+
targetId: this.#targetId,
|
|
840
|
+
url: this.#activeUrl,
|
|
841
|
+
newTab: false
|
|
842
|
+
});
|
|
732
843
|
this.#stopRecordingEvents = this.#cdp.on("Page.screencastFrame", this.#sessionId, (params) => {
|
|
733
844
|
const frame = params;
|
|
734
845
|
this.call("Page.screencastFrameAck", { sessionId: frame.sessionId }).catch(() => {
|
|
@@ -736,20 +847,49 @@ class Browser {
|
|
|
736
847
|
});
|
|
737
848
|
const sequence = this.#recordingSequence++;
|
|
738
849
|
const path = join2(this.#recordingDirectory, `${String(sequence).padStart(6, "0")}.jpg`);
|
|
739
|
-
const
|
|
850
|
+
const capturedAt = performance.now();
|
|
851
|
+
const elapsedMs = sequence ? capturedAt - this.#recordingStartedAt : 0;
|
|
852
|
+
const url = this.#activeUrl;
|
|
853
|
+
const targetId = this.#targetId;
|
|
854
|
+
const notice = capturedAt < this.#newTabNoticeUntil;
|
|
855
|
+
const noticeUrl = notice ? this.#newTabNoticeUrl ?? undefined : undefined;
|
|
740
856
|
this.#recordingWrites = this.#recordingWrites.then(async () => {
|
|
741
857
|
await Bun.write(path, Buffer.from(frame.data, "base64"));
|
|
742
|
-
this.#recordingFrames.push({
|
|
858
|
+
this.#recordingFrames.push({
|
|
859
|
+
path,
|
|
860
|
+
elapsedMs,
|
|
861
|
+
targetId,
|
|
862
|
+
url,
|
|
863
|
+
newTab: notice,
|
|
864
|
+
...noticeUrl ? { noticeUrl } : {}
|
|
865
|
+
});
|
|
743
866
|
});
|
|
744
867
|
});
|
|
745
868
|
await this.call("Page.startScreencast", {
|
|
746
869
|
format: "jpeg",
|
|
747
870
|
quality: 70,
|
|
748
|
-
maxWidth:
|
|
749
|
-
maxHeight:
|
|
871
|
+
maxWidth: PAGE_WIDTH,
|
|
872
|
+
maxHeight: PAGE_HEIGHT,
|
|
750
873
|
everyNthFrame: 3
|
|
751
874
|
});
|
|
752
875
|
}
|
|
876
|
+
async recordNewTabNotice(url) {
|
|
877
|
+
if (!this.#recordingPath || !this.#recordingDirectory)
|
|
878
|
+
return;
|
|
879
|
+
const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 70 });
|
|
880
|
+
const elapsedMs = performance.now() - this.#recordingStartedAt;
|
|
881
|
+
const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
|
|
882
|
+
await Bun.write(path, Buffer.from(capture.data, "base64"));
|
|
883
|
+
this.#recordingFrames.push({
|
|
884
|
+
path,
|
|
885
|
+
elapsedMs,
|
|
886
|
+
targetId: this.#targetId,
|
|
887
|
+
url: this.#activeUrl,
|
|
888
|
+
newTab: true,
|
|
889
|
+
noticeUrl: url
|
|
890
|
+
});
|
|
891
|
+
return elapsedMs;
|
|
892
|
+
}
|
|
753
893
|
async finishRecording() {
|
|
754
894
|
if (!this.#recordingPath || !this.#recordingDirectory)
|
|
755
895
|
return;
|
|
@@ -762,8 +902,53 @@ class Browser {
|
|
|
762
902
|
await this.#recordingWrites;
|
|
763
903
|
if (!this.#recordingFrames.length)
|
|
764
904
|
throw new Error("Recording produced no browser frames");
|
|
905
|
+
const stoppedAt = performance.now() - this.#recordingStartedAt;
|
|
906
|
+
this.#recordingFrames.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
|
907
|
+
for (const event of this.#recordingUrlEvents) {
|
|
908
|
+
let index = this.#recordingFrames.length - 1;
|
|
909
|
+
while (index >= 0 && (this.#recordingFrames[index].elapsedMs >= event.elapsedMs || this.#recordingFrames[index].targetId !== event.targetId))
|
|
910
|
+
index--;
|
|
911
|
+
const before = this.#recordingFrames[index];
|
|
912
|
+
if (!before || before.url === event.url)
|
|
913
|
+
continue;
|
|
914
|
+
const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
|
|
915
|
+
await Bun.write(path, Bun.file(before.path));
|
|
916
|
+
const frame = {
|
|
917
|
+
path,
|
|
918
|
+
elapsedMs: event.elapsedMs,
|
|
919
|
+
targetId: event.targetId,
|
|
920
|
+
url: event.url,
|
|
921
|
+
newTab: this.#newTabNoticeWindows.some((window) => event.elapsedMs >= window.startMs && event.elapsedMs < window.endMs),
|
|
922
|
+
noticeUrl: before.noticeUrl
|
|
923
|
+
};
|
|
924
|
+
this.#recordingFrames.push(frame);
|
|
925
|
+
this.#recordingFrames.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
|
926
|
+
}
|
|
927
|
+
for (const { endMs } of this.#newTabNoticeWindows) {
|
|
928
|
+
if (this.#interactionPauses > 0)
|
|
929
|
+
continue;
|
|
930
|
+
let index = this.#recordingFrames.length - 1;
|
|
931
|
+
while (index >= 0 && this.#recordingFrames[index].elapsedMs >= endMs)
|
|
932
|
+
index--;
|
|
933
|
+
const before = this.#recordingFrames[index];
|
|
934
|
+
if (!before?.newTab || this.#recordingFrames[index + 1]?.elapsedMs === endMs)
|
|
935
|
+
continue;
|
|
936
|
+
const path = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
|
|
937
|
+
await Bun.write(path, Bun.file(before.path));
|
|
938
|
+
this.#recordingFrames.splice(index + 1, 0, {
|
|
939
|
+
path,
|
|
940
|
+
elapsedMs: endMs,
|
|
941
|
+
targetId: before.targetId,
|
|
942
|
+
url: before.url,
|
|
943
|
+
newTab: false
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
for (const frame of this.#recordingFrames) {
|
|
947
|
+
const jpeg = Buffer.from(await Bun.file(frame.path).arrayBuffer()).toString("base64");
|
|
948
|
+
await Bun.write(frame.path, Buffer.from(await this.composeFrame(jpeg, frame.url, frame.newTab ? frame.noticeUrl ?? frame.url : null), "base64"));
|
|
949
|
+
}
|
|
765
950
|
await mkdir(dirname(this.#recordingPath), { recursive: true });
|
|
766
|
-
const finishedAt =
|
|
951
|
+
const finishedAt = Math.max(stoppedAt, this.#recordingFrames.at(-1).elapsedMs + 40);
|
|
767
952
|
const quoted = (path) => path.replaceAll("'", "'\\''");
|
|
768
953
|
const lines = ["ffconcat version 1.0"];
|
|
769
954
|
for (let index = 0;index < this.#recordingFrames.length; index++) {
|
|
@@ -785,16 +970,15 @@ class Browser {
|
|
|
785
970
|
if (!this.#screenshotPath)
|
|
786
971
|
return;
|
|
787
972
|
await mkdir(dirname(this.#screenshotPath), { recursive: true });
|
|
788
|
-
if (this.#recordingDirectory) {
|
|
789
|
-
await this.#recordingWrites;
|
|
790
|
-
const finalFrame = this.#recordingFrames.at(-1);
|
|
791
|
-
if (finalFrame) {
|
|
792
|
-
await Bun.write(this.#screenshotPath, Bun.file(finalFrame.path));
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
973
|
const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 90 });
|
|
797
|
-
await
|
|
974
|
+
const url = await this.evaluate("location.href") ?? this.#activeUrl;
|
|
975
|
+
await Bun.write(this.#screenshotPath, Buffer.from(await this.composeFrame(capture.data, url, this.#hadNewTab ? url : null), "base64"));
|
|
976
|
+
}
|
|
977
|
+
async composeFrame(jpeg, url, newTabUrl) {
|
|
978
|
+
const composed = await this.evaluate(composeFrameExpression(jpeg, url, newTabUrl), true);
|
|
979
|
+
if (!composed)
|
|
980
|
+
throw new Error("Could not compose browser recording frame");
|
|
981
|
+
return composed;
|
|
798
982
|
}
|
|
799
983
|
async animateCursor(x, y, click = false) {
|
|
800
984
|
if (!this.#recordingPath)
|
|
@@ -941,13 +1125,25 @@ class Browser {
|
|
|
941
1125
|
const attached = await this.#cdp.command("Target.attachToTarget", { targetId: target.id, flatten: true });
|
|
942
1126
|
try {
|
|
943
1127
|
await this.#cdp.command("Emulation.setDeviceMetricsOverride", {
|
|
944
|
-
width:
|
|
945
|
-
height:
|
|
1128
|
+
width: PAGE_WIDTH,
|
|
1129
|
+
height: PAGE_HEIGHT,
|
|
946
1130
|
deviceScaleFactor: 1,
|
|
947
1131
|
mobile: false
|
|
948
1132
|
}, attached.sessionId);
|
|
949
1133
|
} finally {
|
|
950
1134
|
if (this.#switchOnPopup) {
|
|
1135
|
+
const noticeStart = await this.recordNewTabNotice(target.url);
|
|
1136
|
+
const noticeStartAt = noticeStart === undefined ? performance.now() : this.#recordingStartedAt + noticeStart;
|
|
1137
|
+
this.#newTabNoticeUrl = target.url;
|
|
1138
|
+
this.#newTabNoticeUntil = this.#interactionPauses > 0 ? Infinity : 0;
|
|
1139
|
+
const noticeWindow = noticeStart === undefined ? undefined : {
|
|
1140
|
+
startMs: noticeStart,
|
|
1141
|
+
endMs: noticeStart + Math.max(40, this.#interactionPauses)
|
|
1142
|
+
};
|
|
1143
|
+
if (noticeWindow)
|
|
1144
|
+
this.#newTabNoticeWindows.push(noticeWindow);
|
|
1145
|
+
if (this.#interactionPauses > 0)
|
|
1146
|
+
await Bun.sleep(Math.max(0, noticeStartAt + this.#interactionPauses - performance.now()));
|
|
951
1147
|
if (this.#recordingPath) {
|
|
952
1148
|
await this.call("Page.stopScreencast").catch(() => {
|
|
953
1149
|
return;
|
|
@@ -956,13 +1152,20 @@ class Browser {
|
|
|
956
1152
|
this.#stopRecordingEvents = undefined;
|
|
957
1153
|
await this.#recordingWrites;
|
|
958
1154
|
}
|
|
1155
|
+
if (noticeWindow && this.#interactionPauses > 0)
|
|
1156
|
+
noticeWindow.endMs = performance.now() - this.#recordingStartedAt;
|
|
1157
|
+
this.#newTabNoticeUrl = null;
|
|
1158
|
+
this.#newTabNoticeUntil = 0;
|
|
959
1159
|
this.#sessionId = attached.sessionId;
|
|
960
1160
|
this.#targetId = target.id;
|
|
1161
|
+
this.#activeUrl = target.url;
|
|
961
1162
|
this.#loadingFrames.clear();
|
|
962
1163
|
await this.watchConsole();
|
|
963
1164
|
await this.watchPageLoads();
|
|
964
1165
|
this.#switchOnPopup = false;
|
|
1166
|
+
this.#hadNewTab = true;
|
|
965
1167
|
await this.startRecording();
|
|
1168
|
+
this.#pauseUntil = performance.now();
|
|
966
1169
|
} else {
|
|
967
1170
|
await this.#cdp.command("Target.detachFromTarget", { sessionId: attached.sessionId });
|
|
968
1171
|
}
|
|
@@ -1140,7 +1343,7 @@ ${child.text}`.slice(0, 6000);
|
|
|
1140
1343
|
const page = { ...info, frames, transitions: [...this.#transitions], fingerprint: fingerprint(info) };
|
|
1141
1344
|
if (screenshot) {
|
|
1142
1345
|
const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 72 });
|
|
1143
|
-
page.screenshot = capture.data;
|
|
1346
|
+
page.screenshot = await this.composeFrame(capture.data, info.url, performance.now() < this.#newTabNoticeUntil ? this.#newTabNoticeUrl : null);
|
|
1144
1347
|
}
|
|
1145
1348
|
return page;
|
|
1146
1349
|
}
|
|
@@ -2045,7 +2248,7 @@ Known field values:
|
|
|
2045
2248
|
Evidence and output:
|
|
2046
2249
|
--recording <file.mp4> Record the complete goal with an animated cursor.
|
|
2047
2250
|
Requires FFmpeg.
|
|
2048
|
-
--screenshot <file.jpg> Save the
|
|
2251
|
+
--screenshot <file.jpg> Save a 1280x720 image with the URL bar.
|
|
2049
2252
|
--final-state Add the final semantic page state to stdout JSON.
|
|
2050
2253
|
-h, --help Show this help and exit.
|
|
2051
2254
|
|