autokap 1.3.5 → 1.3.6
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/browser.d.ts +5 -1
- package/dist/browser.js +21 -3
- package/dist/cli-runner.js +7 -1
- package/dist/cli.js +48 -15
- package/dist/web-playwright-local.js +53 -13
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export declare class Browser {
|
|
|
96
96
|
private browser;
|
|
97
97
|
private context;
|
|
98
98
|
private page;
|
|
99
|
+
private nativeVideoStartedAt;
|
|
99
100
|
private elementMap;
|
|
100
101
|
private akNodeIndex;
|
|
101
102
|
private poolContext;
|
|
@@ -116,7 +117,9 @@ export declare class Browser {
|
|
|
116
117
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
117
118
|
* the cursor overlay script so clicks/hover moments are visible.
|
|
118
119
|
*/
|
|
119
|
-
static forClipCapture(options: BrowserOptions, cursorScript: string
|
|
120
|
+
static forClipCapture(options: BrowserOptions, cursorScript: string, recording?: {
|
|
121
|
+
nativeVideoDir?: string;
|
|
122
|
+
}): Promise<Browser>;
|
|
120
123
|
/**
|
|
121
124
|
* Close only the browser context (not the browser process).
|
|
122
125
|
* Used by clip capture to release the context promptly after the CDP loop
|
|
@@ -332,6 +335,7 @@ export declare class Browser {
|
|
|
332
335
|
resizeViewport(width: number, height: number): Promise<void>;
|
|
333
336
|
get currentPage(): Page;
|
|
334
337
|
get browserContext(): BrowserContext;
|
|
338
|
+
get nativeVideoStartTime(): number | null;
|
|
335
339
|
/**
|
|
336
340
|
* Observation pass for mock data generation.
|
|
337
341
|
* Navigates to the given URL, waits for network idle, and records all JSON API responses.
|
package/dist/browser.js
CHANGED
|
@@ -772,6 +772,7 @@ export class Browser {
|
|
|
772
772
|
browser = null;
|
|
773
773
|
context = null;
|
|
774
774
|
page = null;
|
|
775
|
+
nativeVideoStartedAt = null;
|
|
775
776
|
elementMap = new Map();
|
|
776
777
|
akNodeIndex = new Map();
|
|
777
778
|
poolContext = false;
|
|
@@ -804,7 +805,7 @@ export class Browser {
|
|
|
804
805
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
805
806
|
* the cursor overlay script so clicks/hover moments are visible.
|
|
806
807
|
*/
|
|
807
|
-
static async forClipCapture(options, cursorScript) {
|
|
808
|
+
static async forClipCapture(options, cursorScript, recording) {
|
|
808
809
|
const instance = new Browser(options);
|
|
809
810
|
const deviceScaleFactor = normalizeDeviceScaleFactor(options.deviceScaleFactor);
|
|
810
811
|
// Enable GPU compositor on non-Linux platforms so Chrome can render
|
|
@@ -832,13 +833,23 @@ export class Browser {
|
|
|
832
833
|
headless: !options.headed,
|
|
833
834
|
args: clipArgs,
|
|
834
835
|
});
|
|
835
|
-
|
|
836
|
+
const contextOptions = {
|
|
836
837
|
viewport: options.viewport,
|
|
837
838
|
deviceScaleFactor,
|
|
838
839
|
locale: langToLocale(options.lang ?? 'en'),
|
|
839
840
|
colorScheme: options.colorScheme ?? 'light',
|
|
840
841
|
storageState: options.storageState,
|
|
841
|
-
}
|
|
842
|
+
};
|
|
843
|
+
if (recording?.nativeVideoDir) {
|
|
844
|
+
contextOptions.recordVideo = {
|
|
845
|
+
dir: recording.nativeVideoDir,
|
|
846
|
+
size: {
|
|
847
|
+
width: Math.round(options.viewport.width),
|
|
848
|
+
height: Math.round(options.viewport.height),
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
instance.context = await instance.browser.newContext(contextOptions);
|
|
842
853
|
// Inject cursor overlay at context level — survives all navigations in this session
|
|
843
854
|
await instance.context.addInitScript(cursorScript);
|
|
844
855
|
// Also hide dev/prototype chrome on every document, including navigations
|
|
@@ -863,7 +874,11 @@ export class Browser {
|
|
|
863
874
|
document.addEventListener('DOMContentLoaded', install, { once: true });
|
|
864
875
|
}
|
|
865
876
|
}, { styleId: CAPTURE_HIDE_STYLE_ID, css: getCaptureHideCSS() });
|
|
877
|
+
const nativeVideoStartedAt = Date.now();
|
|
866
878
|
instance.page = await instance.context.newPage();
|
|
879
|
+
if (recording?.nativeVideoDir) {
|
|
880
|
+
instance.nativeVideoStartedAt = nativeVideoStartedAt;
|
|
881
|
+
}
|
|
867
882
|
return instance;
|
|
868
883
|
}
|
|
869
884
|
/**
|
|
@@ -5043,6 +5058,9 @@ export class Browser {
|
|
|
5043
5058
|
get browserContext() {
|
|
5044
5059
|
return this.ensureContext();
|
|
5045
5060
|
}
|
|
5061
|
+
get nativeVideoStartTime() {
|
|
5062
|
+
return this.nativeVideoStartedAt;
|
|
5063
|
+
}
|
|
5046
5064
|
/**
|
|
5047
5065
|
* Observation pass for mock data generation.
|
|
5048
5066
|
* Navigates to the given URL, waits for network idle, and records all JSON API responses.
|
package/dist/cli-runner.js
CHANGED
|
@@ -241,7 +241,13 @@ export async function runCapture(options) {
|
|
|
241
241
|
}
|
|
242
242
|
if (recordable) {
|
|
243
243
|
recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
|
|
244
|
-
|
|
244
|
+
const nativeCloudClipRecording = program.mediaMode === 'clip'
|
|
245
|
+
&& process.env.AUTOKAP_CLOUD_RUNNER === '1'
|
|
246
|
+
&& process.env.AUTOKAP_CLIP_RECORDER !== 'cdp';
|
|
247
|
+
if (nativeCloudClipRecording) {
|
|
248
|
+
logger.info('[capture] Cloud clip recorder: native Playwright video (CDP screenshot loop disabled)');
|
|
249
|
+
}
|
|
250
|
+
browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'), nativeCloudClipRecording ? { nativeVideoDir: recordingDir } : undefined);
|
|
245
251
|
}
|
|
246
252
|
else if (browserOptions.headed) {
|
|
247
253
|
// Headed mode: standalone browser (pool is always headless)
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,40 @@ function ensureExportFormat(format) {
|
|
|
79
79
|
function printJson(value) {
|
|
80
80
|
console.log(JSON.stringify(value, null, 2));
|
|
81
81
|
}
|
|
82
|
+
function displayPresetName(preset) {
|
|
83
|
+
const name = preset.name?.trim();
|
|
84
|
+
return name && name.length > 0 ? name : preset.id;
|
|
85
|
+
}
|
|
86
|
+
function cloudCaptureProgressCheckpoint(event) {
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case 'variant_start':
|
|
89
|
+
return {
|
|
90
|
+
type: 'preset_progress',
|
|
91
|
+
message: `Capture started: ${event.variantId}`,
|
|
92
|
+
};
|
|
93
|
+
case 'variant_end':
|
|
94
|
+
return {
|
|
95
|
+
type: 'preset_progress',
|
|
96
|
+
message: event.status === 'ok'
|
|
97
|
+
? `Capture completed: ${event.variantId}`
|
|
98
|
+
: `Capture failed: ${event.variantId}`,
|
|
99
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
100
|
+
};
|
|
101
|
+
case 'upload_start':
|
|
102
|
+
return {
|
|
103
|
+
type: 'upload_start',
|
|
104
|
+
message: 'Export started',
|
|
105
|
+
};
|
|
106
|
+
case 'upload_end':
|
|
107
|
+
return {
|
|
108
|
+
type: 'upload_end',
|
|
109
|
+
message: event.status === 'failed' ? 'Export failed' : 'Export completed',
|
|
110
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
111
|
+
};
|
|
112
|
+
default:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
function buildEndpointAssetUrl(config, endpointId) {
|
|
83
117
|
return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
|
|
84
118
|
}
|
|
@@ -329,7 +363,7 @@ program
|
|
|
329
363
|
totalPresets: data.presets.length,
|
|
330
364
|
completedPresets: 0,
|
|
331
365
|
failedPresets: 0,
|
|
332
|
-
message: `planned ${data.presets.length} preset(s)`,
|
|
366
|
+
message: `Run planned: ${data.presets.length} preset(s)`,
|
|
333
367
|
});
|
|
334
368
|
if (data.presets.length === 0) {
|
|
335
369
|
logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
|
|
@@ -343,9 +377,10 @@ program
|
|
|
343
377
|
totalPresets: data.presets.length,
|
|
344
378
|
completedPresets: 0,
|
|
345
379
|
failedPresets: 0,
|
|
346
|
-
message: `
|
|
380
|
+
message: `Runner started: ${data.presets.length} preset(s) to capture`,
|
|
347
381
|
});
|
|
348
382
|
for (const [index, preset] of data.presets.entries()) {
|
|
383
|
+
const presetDisplayName = displayPresetName(preset);
|
|
349
384
|
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
350
385
|
logger.info(`[auto-recapture] Running ${label}`);
|
|
351
386
|
await postCloudCheckpoint({
|
|
@@ -356,7 +391,7 @@ program
|
|
|
356
391
|
totalPresets: data.presets.length,
|
|
357
392
|
completedPresets: index,
|
|
358
393
|
failedPresets: failures.length,
|
|
359
|
-
message: `
|
|
394
|
+
message: `Preset started: ${presetDisplayName}`,
|
|
360
395
|
});
|
|
361
396
|
const result = await runCapture({
|
|
362
397
|
presetId: preset.id,
|
|
@@ -364,23 +399,21 @@ program
|
|
|
364
399
|
headed: opts.headed,
|
|
365
400
|
allowUploadFailure: opts.allowUploadFailure,
|
|
366
401
|
onProgress: (event) => {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? 'upload_end'
|
|
371
|
-
: 'preset_progress';
|
|
402
|
+
const checkpoint = cloudCaptureProgressCheckpoint(event);
|
|
403
|
+
if (!checkpoint)
|
|
404
|
+
return;
|
|
372
405
|
void postCloudCheckpoint({
|
|
373
|
-
type:
|
|
406
|
+
type: checkpoint.type,
|
|
374
407
|
presetId: preset.id,
|
|
375
408
|
presetName: preset.name ?? null,
|
|
376
409
|
presetIndex: index,
|
|
377
410
|
totalPresets: data.presets.length,
|
|
378
411
|
completedPresets: index,
|
|
379
412
|
failedPresets: failures.length,
|
|
380
|
-
message:
|
|
413
|
+
message: checkpoint.message,
|
|
381
414
|
progressEvent: event,
|
|
382
|
-
status:
|
|
383
|
-
}
|
|
415
|
+
status: checkpoint.status,
|
|
416
|
+
});
|
|
384
417
|
},
|
|
385
418
|
});
|
|
386
419
|
const childRunId = result.runId;
|
|
@@ -399,7 +432,7 @@ program
|
|
|
399
432
|
childRunId,
|
|
400
433
|
status: 'failed',
|
|
401
434
|
errorMessage: error,
|
|
402
|
-
message: `failed ${
|
|
435
|
+
message: `Preset failed: ${presetDisplayName}`,
|
|
403
436
|
});
|
|
404
437
|
}
|
|
405
438
|
else {
|
|
@@ -413,7 +446,7 @@ program
|
|
|
413
446
|
failedPresets: failures.length,
|
|
414
447
|
childRunId,
|
|
415
448
|
status: 'completed',
|
|
416
|
-
message: `completed ${
|
|
449
|
+
message: `Preset completed: ${presetDisplayName}`,
|
|
417
450
|
});
|
|
418
451
|
}
|
|
419
452
|
}
|
|
@@ -445,7 +478,7 @@ program
|
|
|
445
478
|
completedPresets: data.presets.length,
|
|
446
479
|
failedPresets: 0,
|
|
447
480
|
status: 'completed',
|
|
448
|
-
message: '
|
|
481
|
+
message: 'Cloud recapture completed',
|
|
449
482
|
});
|
|
450
483
|
await notifyCloudCallback('completed', {
|
|
451
484
|
totalPresets: data.presets.length,
|
|
@@ -352,21 +352,32 @@ export class WebPlaywrightLocal {
|
|
|
352
352
|
const cloudClipFps = isCloudRunner ? 30 : defaultFps;
|
|
353
353
|
const targetFps = options.captureFps
|
|
354
354
|
?? (options.mediaMode === 'video' ? 30 : cloudClipFps);
|
|
355
|
-
const
|
|
356
|
-
page
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
355
|
+
const nativeClipVideo = options.mediaMode === 'clip' && isCloudRunner
|
|
356
|
+
? page.video()
|
|
357
|
+
: null;
|
|
358
|
+
let loop = null;
|
|
359
|
+
if (nativeClipVideo) {
|
|
360
|
+
logger.info('[capture] Native cloud clip recorder enabled — using Playwright video stream instead of CDP screenshots');
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
loop = new ClipCaptureLoop({
|
|
364
|
+
page,
|
|
365
|
+
framesDir,
|
|
366
|
+
targetFps,
|
|
367
|
+
// Cloud runners have CPU headroom — drop the Linux 50 ms idle cushion
|
|
368
|
+
// (sized for tight CI runners) to let the loop stay close to its target.
|
|
369
|
+
minRestMs: process.platform === 'linux' && !isCloudRunner ? 50 : 16,
|
|
370
|
+
});
|
|
371
|
+
await loop.start();
|
|
372
|
+
}
|
|
364
373
|
this.recording = {
|
|
365
374
|
mediaMode: options.mediaMode,
|
|
366
375
|
startedAt: Date.now(),
|
|
367
376
|
framesDir,
|
|
368
377
|
mp4Path: path.join(baseDir, `${options.mediaMode}.mp4`),
|
|
369
378
|
loop,
|
|
379
|
+
nativeVideo: nativeClipVideo,
|
|
380
|
+
nativeVideoStartedAt: this.browser.nativeVideoStartTime,
|
|
370
381
|
finalized: false,
|
|
371
382
|
};
|
|
372
383
|
this.clipCursor = {
|
|
@@ -446,20 +457,47 @@ export class WebPlaywrightLocal {
|
|
|
446
457
|
throw new Error('recording was not started');
|
|
447
458
|
}
|
|
448
459
|
if (this.recording.finalized) {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
460
|
+
const sourcePath = this.recording.sourcePath ?? this.recording.mp4Path;
|
|
461
|
+
const buffer = await fs.readFile(sourcePath);
|
|
462
|
+
const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(sourcePath);
|
|
451
463
|
this.recording.encodedDurationMs = durationMs;
|
|
452
464
|
return {
|
|
453
465
|
buffer,
|
|
454
466
|
durationMs,
|
|
455
|
-
mimeType: 'video/mp4',
|
|
456
|
-
trimStartMs: this.recording.result?.trimStartMs ?? 0,
|
|
467
|
+
mimeType: this.recording.sourceMimeType ?? 'video/mp4',
|
|
468
|
+
trimStartMs: this.recording.trimStartMs ?? this.recording.result?.trimStartMs ?? 0,
|
|
457
469
|
};
|
|
458
470
|
}
|
|
459
471
|
if (this.recordingNavWatcher) {
|
|
460
472
|
this.recordingNavWatcher.detach();
|
|
461
473
|
this.recordingNavWatcher = null;
|
|
462
474
|
}
|
|
475
|
+
if (this.recording.nativeVideo) {
|
|
476
|
+
const video = this.recording.nativeVideo;
|
|
477
|
+
const trimStartMs = Math.max(0, this.recording.startedAt
|
|
478
|
+
- (this.recording.nativeVideoStartedAt ?? this.sessionStartedAt));
|
|
479
|
+
await this.browser.closeContext();
|
|
480
|
+
const videoPath = await video.path();
|
|
481
|
+
const durationMs = await getMediaDurationMs(videoPath);
|
|
482
|
+
logger.info(`[capture] Native clip recording finalized: source ${durationMs}ms, ` +
|
|
483
|
+
`trim start ${Math.round(trimStartMs)}ms`);
|
|
484
|
+
this.recording.finalized = true;
|
|
485
|
+
this.recording.sourcePath = videoPath;
|
|
486
|
+
this.recording.sourceMimeType = 'video/webm';
|
|
487
|
+
this.recording.trimStartMs = trimStartMs;
|
|
488
|
+
this.recording.encodedDurationMs = durationMs;
|
|
489
|
+
this.clipCursor = null;
|
|
490
|
+
const buffer = await fs.readFile(videoPath);
|
|
491
|
+
return {
|
|
492
|
+
buffer,
|
|
493
|
+
durationMs,
|
|
494
|
+
mimeType: 'video/webm',
|
|
495
|
+
trimStartMs,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (!this.recording.loop) {
|
|
499
|
+
throw new Error('recording loop was not initialized');
|
|
500
|
+
}
|
|
463
501
|
const result = await this.recording.loop.stop();
|
|
464
502
|
logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
|
|
465
503
|
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s ` +
|
|
@@ -475,6 +513,8 @@ export class WebPlaywrightLocal {
|
|
|
475
513
|
});
|
|
476
514
|
this.recording.finalized = true;
|
|
477
515
|
this.recording.result = result;
|
|
516
|
+
this.recording.sourcePath = this.recording.mp4Path;
|
|
517
|
+
this.recording.sourceMimeType = 'video/mp4';
|
|
478
518
|
this.recording.encodedDurationMs = await getMediaDurationMs(this.recording.mp4Path);
|
|
479
519
|
this.clipCursor = null;
|
|
480
520
|
const buffer = await fs.readFile(this.recording.mp4Path);
|