@kvman/kvvideo 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/api.d.ts +79 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/browser/channels.d.ts +6 -0
- package/dist/browser/channels.d.ts.map +1 -0
- package/dist/browser/channels.js +5 -0
- package/dist/browser/channels.js.map +1 -0
- package/dist/browser/launch.d.ts +4 -0
- package/dist/browser/launch.d.ts.map +1 -0
- package/dist/browser/launch.js +8 -0
- package/dist/browser/launch.js.map +1 -0
- package/dist/browser/start-browser.d.ts +5 -0
- package/dist/browser/start-browser.d.ts.map +1 -0
- package/dist/browser/start-browser.js +17 -0
- package/dist/browser/start-browser.js.map +1 -0
- package/dist/compose/ass-file.d.ts +12 -0
- package/dist/compose/ass-file.d.ts.map +1 -0
- package/dist/compose/ass-file.js +36 -0
- package/dist/compose/ass-file.js.map +1 -0
- package/dist/compose/caption-font.d.ts +6 -0
- package/dist/compose/caption-font.d.ts.map +1 -0
- package/dist/compose/caption-font.js +23 -0
- package/dist/compose/caption-font.js.map +1 -0
- package/dist/compose/compose-input.d.ts +48 -0
- package/dist/compose/compose-input.d.ts.map +1 -0
- package/dist/compose/compose-input.js +37 -0
- package/dist/compose/compose-input.js.map +1 -0
- package/dist/compose/player-copies.d.ts +6 -0
- package/dist/compose/player-copies.d.ts.map +1 -0
- package/dist/compose/player-copies.js +31 -0
- package/dist/compose/player-copies.js.map +1 -0
- package/dist/compose/register-compose.d.ts +3 -0
- package/dist/compose/register-compose.d.ts.map +1 -0
- package/dist/compose/register-compose.js +65 -0
- package/dist/compose/register-compose.js.map +1 -0
- package/dist/compose/render.d.ts +16 -0
- package/dist/compose/render.d.ts.map +1 -0
- package/dist/compose/render.js +35 -0
- package/dist/compose/render.js.map +1 -0
- package/dist/compose/scene-args.d.ts +12 -0
- package/dist/compose/scene-args.d.ts.map +1 -0
- package/dist/compose/scene-args.js +42 -0
- package/dist/compose/scene-args.js.map +1 -0
- package/dist/compose/scene-plan.d.ts +28 -0
- package/dist/compose/scene-plan.d.ts.map +1 -0
- package/dist/compose/scene-plan.js +32 -0
- package/dist/compose/scene-plan.js.map +1 -0
- package/dist/connector.d.ts +79 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +44 -0
- package/dist/connector.js.map +1 -0
- package/dist/files/answer-path.d.ts +3 -0
- package/dist/files/answer-path.d.ts.map +1 -0
- package/dist/files/answer-path.js +6 -0
- package/dist/files/answer-path.js.map +1 -0
- package/dist/files/place-output.d.ts +8 -0
- package/dist/files/place-output.d.ts.map +1 -0
- package/dist/files/place-output.js +24 -0
- package/dist/files/place-output.js.map +1 -0
- package/dist/files/workspace-paths.d.ts +7 -0
- package/dist/files/workspace-paths.d.ts.map +1 -0
- package/dist/files/workspace-paths.js +48 -0
- package/dist/files/workspace-paths.js.map +1 -0
- package/dist/fonts/bundled-fonts.d.ts +10 -0
- package/dist/fonts/bundled-fonts.d.ts.map +1 -0
- package/dist/fonts/bundled-fonts.js +10 -0
- package/dist/fonts/bundled-fonts.js.map +1 -0
- package/dist/fonts/font-family.d.ts +2 -0
- package/dist/fonts/font-family.d.ts.map +1 -0
- package/dist/fonts/font-family.js +43 -0
- package/dist/fonts/font-family.js.map +1 -0
- package/dist/fonts/register-fonts.d.ts +3 -0
- package/dist/fonts/register-fonts.d.ts.map +1 -0
- package/dist/fonts/register-fonts.js +12 -0
- package/dist/fonts/register-fonts.js.map +1 -0
- package/dist/guide.d.ts +3 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +6 -0
- package/dist/guide.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/narration/captions.d.ts +10 -0
- package/dist/narration/captions.d.ts.map +1 -0
- package/dist/narration/captions.js +42 -0
- package/dist/narration/captions.js.map +1 -0
- package/dist/narration/piper.d.ts +8 -0
- package/dist/narration/piper.d.ts.map +1 -0
- package/dist/narration/piper.js +19 -0
- package/dist/narration/piper.js.map +1 -0
- package/dist/narration/register-narration.d.ts +17 -0
- package/dist/narration/register-narration.d.ts.map +1 -0
- package/dist/narration/register-narration.js +75 -0
- package/dist/narration/register-narration.js.map +1 -0
- package/dist/narration/speech-services.d.ts +19 -0
- package/dist/narration/speech-services.d.ts.map +1 -0
- package/dist/narration/speech-services.js +50 -0
- package/dist/narration/speech-services.js.map +1 -0
- package/dist/problems.d.ts +12 -0
- package/dist/problems.d.ts.map +1 -0
- package/dist/problems.js +19 -0
- package/dist/problems.js.map +1 -0
- package/dist/programs/kill-tree.d.ts +16 -0
- package/dist/programs/kill-tree.d.ts.map +1 -0
- package/dist/programs/kill-tree.js +28 -0
- package/dist/programs/kill-tree.js.map +1 -0
- package/dist/programs/media-duration.d.ts +10 -0
- package/dist/programs/media-duration.d.ts.map +1 -0
- package/dist/programs/media-duration.js +10 -0
- package/dist/programs/media-duration.js.map +1 -0
- package/dist/programs/piped-program.d.ts +7 -0
- package/dist/programs/piped-program.d.ts.map +1 -0
- package/dist/programs/piped-program.js +45 -0
- package/dist/programs/piped-program.js.map +1 -0
- package/dist/programs/programs.d.ts +6 -0
- package/dist/programs/programs.d.ts.map +1 -0
- package/dist/programs/programs.js +6 -0
- package/dist/programs/programs.js.map +1 -0
- package/dist/programs/run-program.d.ts +22 -0
- package/dist/programs/run-program.d.ts.map +1 -0
- package/dist/programs/run-program.js +46 -0
- package/dist/programs/run-program.js.map +1 -0
- package/dist/record/address.d.ts +5 -0
- package/dist/record/address.d.ts.map +1 -0
- package/dist/record/address.js +28 -0
- package/dist/record/address.js.map +1 -0
- package/dist/record/cursor-script.d.ts +2 -0
- package/dist/record/cursor-script.d.ts.map +1 -0
- package/dist/record/cursor-script.js +21 -0
- package/dist/record/cursor-script.js.map +1 -0
- package/dist/record/encoder-args.d.ts +3 -0
- package/dist/record/encoder-args.d.ts.map +1 -0
- package/dist/record/encoder-args.js +13 -0
- package/dist/record/encoder-args.js.map +1 -0
- package/dist/record/frame-writer.d.ts +8 -0
- package/dist/record/frame-writer.d.ts.map +1 -0
- package/dist/record/frame-writer.js +30 -0
- package/dist/record/frame-writer.js.map +1 -0
- package/dist/record/own-port.d.ts +2 -0
- package/dist/record/own-port.d.ts.map +1 -0
- package/dist/record/own-port.js +23 -0
- package/dist/record/own-port.js.map +1 -0
- package/dist/record/pointer.d.ts +9 -0
- package/dist/record/pointer.d.ts.map +1 -0
- package/dist/record/pointer.js +23 -0
- package/dist/record/pointer.js.map +1 -0
- package/dist/record/recorder.d.ts +24 -0
- package/dist/record/recorder.d.ts.map +1 -0
- package/dist/record/recorder.js +69 -0
- package/dist/record/recorder.js.map +1 -0
- package/dist/record/register-record.d.ts +3 -0
- package/dist/record/register-record.d.ts.map +1 -0
- package/dist/record/register-record.js +49 -0
- package/dist/record/register-record.js.map +1 -0
- package/dist/record/run-step.d.ts +7 -0
- package/dist/record/run-step.d.ts.map +1 -0
- package/dist/record/run-step.js +49 -0
- package/dist/record/run-step.js.map +1 -0
- package/dist/record/screencast.d.ts +9 -0
- package/dist/record/screencast.d.ts.map +1 -0
- package/dist/record/screencast.js +38 -0
- package/dist/record/screencast.js.map +1 -0
- package/dist/record/steps.d.ts +71 -0
- package/dist/record/steps.d.ts.map +1 -0
- package/dist/record/steps.js +29 -0
- package/dist/record/steps.js.map +1 -0
- package/dist/register-with-kvcoder.d.ts +3 -0
- package/dist/register-with-kvcoder.d.ts.map +1 -0
- package/dist/register-with-kvcoder.js +15 -0
- package/dist/register-with-kvcoder.js.map +1 -0
- package/dist/settings.d.ts +29 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +27 -0
- package/dist/settings.js.map +1 -0
- package/dist/setup/browser-readiness.d.ts +14 -0
- package/dist/setup/browser-readiness.d.ts.map +1 -0
- package/dist/setup/browser-readiness.js +16 -0
- package/dist/setup/browser-readiness.js.map +1 -0
- package/dist/setup/ffmpeg-readiness.d.ts +19 -0
- package/dist/setup/ffmpeg-readiness.d.ts.map +1 -0
- package/dist/setup/ffmpeg-readiness.js +33 -0
- package/dist/setup/ffmpeg-readiness.js.map +1 -0
- package/dist/setup/narration-readiness.d.ts +21 -0
- package/dist/setup/narration-readiness.d.ts.map +1 -0
- package/dist/setup/narration-readiness.js +18 -0
- package/dist/setup/narration-readiness.js.map +1 -0
- package/dist/setup/register-setup.d.ts +3 -0
- package/dist/setup/register-setup.d.ts.map +1 -0
- package/dist/setup/register-setup.js +51 -0
- package/dist/setup/register-setup.js.map +1 -0
- package/dist/ui.d.ts +3 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +25 -0
- package/dist/ui.js.map +1 -0
- package/dist/video-start.d.ts +3 -0
- package/dist/video-start.d.ts.map +1 -0
- package/dist/video-start.js +28 -0
- package/dist/video-start.js.map +1 -0
- package/dist/web/components/player.css +2 -0
- package/dist/web/components/player.js +43 -0
- package/docs/guide.md +16 -0
- package/fonts/Amiri-Bold.ttf +0 -0
- package/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
- package/fonts/Lato-Bold.ttf +0 -0
- package/fonts/OFL-Amiri.txt +94 -0
- package/fonts/OFL-IBMPlexSansArabic.txt +93 -0
- package/fonts/OFL-Lato.txt +93 -0
- package/fonts/OFL-Poppins.txt +93 -0
- package/fonts/Poppins-Bold.ttf +0 -0
- package/locales/ar.json +37 -0
- package/locales/en.json +37 -0
- package/package.json +67 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { invalid } from "../problems.js";
|
|
2
|
+
// The addresses a recording may open (plan 16 §16.5, ADR 0107, 18): http or https on this computer, without
|
|
3
|
+
// credentials, and never kvman's own. The recorded page's requests to kvman's port are aborted whatever their host,
|
|
4
|
+
// since `0.0.0.0`, `127.0.0.2`, or `[::ffff:127.0.0.1]` reach kvman too.
|
|
5
|
+
const localHosts = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
6
|
+
function portOf(url) {
|
|
7
|
+
if (url.port !== '')
|
|
8
|
+
return Number(url.port);
|
|
9
|
+
return url.protocol === 'https:' ? 443 : 80;
|
|
10
|
+
}
|
|
11
|
+
/** Whether a request goes to kvman's port, on any host. */
|
|
12
|
+
export function isOwnAddress(url, ownPort) {
|
|
13
|
+
return ownPort !== null && portOf(url) === ownPort;
|
|
14
|
+
}
|
|
15
|
+
/** The address, when it is one a recording may open. */
|
|
16
|
+
export function localAddress(address, ownPort) {
|
|
17
|
+
const url = URL.canParse(address) ? new URL(address) : undefined;
|
|
18
|
+
if (url === undefined || (url.protocol !== 'http:' && url.protocol !== 'https:'))
|
|
19
|
+
throw invalid(`${address} isn't an http or https address.`, { url: address });
|
|
20
|
+
if (!localHosts.has(url.hostname))
|
|
21
|
+
throw invalid(`${address} isn't on this computer: use localhost, 127.0.0.1, or [::1].`, { url: address });
|
|
22
|
+
if (url.username !== '' || url.password !== '')
|
|
23
|
+
throw invalid(`${address} has a user name or password.`, { url: address });
|
|
24
|
+
if (isOwnAddress(url, ownPort))
|
|
25
|
+
throw invalid(`${address} is kvman's own address, which can't be recorded.`, { url: address });
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=address.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.js","sourceRoot":"","sources":["../../src/record/address.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,4GAA4G;AAC5G,oHAAoH;AACpH,yEAAyE;AAEzE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AAEhE,SAAS,MAAM,CAAC,GAAQ;IACtB,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,YAAY,CAAC,GAAQ,EAAE,OAAsB;IAC3D,OAAO,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC;AACrD,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAsB;IAClE,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;QAAE,MAAM,OAAO,CAAC,GAAG,OAAO,kCAAkC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAChK,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,MAAM,OAAO,CAAC,GAAG,OAAO,8DAA8D,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7I,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE;QAAE,MAAM,OAAO,CAAC,GAAG,OAAO,+BAA+B,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3H,IAAI,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC;QAAE,MAAM,OAAO,CAAC,GAAG,OAAO,mDAAmD,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/H,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const cursorScript = "(() => {\n const draw = () => {\n if (document.getElementById('kvvideo-cursor')) return;\n const dot = document.createElement('div');\n dot.id = 'kvvideo-cursor';\n dot.style.cssText = 'position:fixed;inset-inline-start:0;inset-block-start:0;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;background:rgba(20,20,20,.75);border:2px solid #fff;box-shadow:0 0 4px rgba(0,0,0,.5);pointer-events:none;z-index:2147483647;transform:translate(-100px,-100px)';\n document.documentElement.appendChild(dot);\n window.addEventListener('mousemove', (event) => { dot.style.transform = 'translate(' + event.clientX + 'px,' + event.clientY + 'px)'; }, true);\n window.addEventListener('mousedown', (event) => {\n const ring = document.createElement('div');\n ring.className = 'kvvideo-ring';\n ring.style.cssText = 'position:fixed;inset-inline-start:' + (event.clientX - 22) + 'px;inset-block-start:' + (event.clientY - 22) + 'px;width:44px;height:44px;border-radius:50%;border:3px solid rgba(255,190,0,.9);pointer-events:none;z-index:2147483646';\n document.documentElement.appendChild(ring);\n ring.animate([{ opacity: 1, transform: 'scale(.4)' }, { opacity: 0, transform: 'scale(1.3)' }], { duration: 500 }).onfinish = () => ring.remove();\n }, true);\n };\n if (document.documentElement) draw(); else document.addEventListener('DOMContentLoaded', draw);\n})();";
|
|
2
|
+
//# sourceMappingURL=cursor-script.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-script.d.ts","sourceRoot":"","sources":["../../src/record/cursor-script.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,24CAiBnB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The cursor a recording shows (ADR 0107, 19): a browser's recording has no pointer, so the page draws a dot that
|
|
2
|
+
// follows the mouse and a ring at each press. It is added to every page before its own scripts run.
|
|
3
|
+
export const cursorScript = `(() => {
|
|
4
|
+
const draw = () => {
|
|
5
|
+
if (document.getElementById('kvvideo-cursor')) return;
|
|
6
|
+
const dot = document.createElement('div');
|
|
7
|
+
dot.id = 'kvvideo-cursor';
|
|
8
|
+
dot.style.cssText = 'position:fixed;inset-inline-start:0;inset-block-start:0;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;background:rgba(20,20,20,.75);border:2px solid #fff;box-shadow:0 0 4px rgba(0,0,0,.5);pointer-events:none;z-index:2147483647;transform:translate(-100px,-100px)';
|
|
9
|
+
document.documentElement.appendChild(dot);
|
|
10
|
+
window.addEventListener('mousemove', (event) => { dot.style.transform = 'translate(' + event.clientX + 'px,' + event.clientY + 'px)'; }, true);
|
|
11
|
+
window.addEventListener('mousedown', (event) => {
|
|
12
|
+
const ring = document.createElement('div');
|
|
13
|
+
ring.className = 'kvvideo-ring';
|
|
14
|
+
ring.style.cssText = 'position:fixed;inset-inline-start:' + (event.clientX - 22) + 'px;inset-block-start:' + (event.clientY - 22) + 'px;width:44px;height:44px;border-radius:50%;border:3px solid rgba(255,190,0,.9);pointer-events:none;z-index:2147483646';
|
|
15
|
+
document.documentElement.appendChild(ring);
|
|
16
|
+
ring.animate([{ opacity: 1, transform: 'scale(.4)' }, { opacity: 0, transform: 'scale(1.3)' }], { duration: 500 }).onfinish = () => ring.remove();
|
|
17
|
+
}, true);
|
|
18
|
+
};
|
|
19
|
+
if (document.documentElement) draw(); else document.addEventListener('DOMContentLoaded', draw);
|
|
20
|
+
})();`;
|
|
21
|
+
//# sourceMappingURL=cursor-script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-script.js","sourceRoot":"","sources":["../../src/record/cursor-script.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,oGAAoG;AAEpG,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;MAiBtB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder-args.d.ts","sourceRoot":"","sources":["../../src/record/encoder-args.ts"],"names":[],"mappings":"AAEA,gHAAgH;AAChH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CASzF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { framesPerSecond } from "./frame-writer.js";
|
|
2
|
+
/** ffmpeg's arguments for a recording's frames, JPEGs on stdin, to an H.264 file of the size (ADR 0107, 25). */
|
|
3
|
+
export function recordEncoderArgs(width, height, output) {
|
|
4
|
+
const fit = `scale=${String(width)}:${String(height)}:force_original_aspect_ratio=decrease,pad=${String(width)}:${String(height)}:(ow-iw)/2:(oh-ih)/2:color=black,format=yuv420p`;
|
|
5
|
+
return [
|
|
6
|
+
'-hide_banner', '-loglevel', 'error',
|
|
7
|
+
'-f', 'image2pipe', '-framerate', String(framesPerSecond), '-c:v', 'mjpeg', '-i', '-',
|
|
8
|
+
'-vf', fit,
|
|
9
|
+
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '18', '-r', String(framesPerSecond),
|
|
10
|
+
'-an', '-movflags', '+faststart', '-y', output,
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=encoder-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder-args.js","sourceRoot":"","sources":["../../src/record/encoder-args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,gHAAgH;AAChH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAc,EAAE,MAAc;IAC7E,MAAM,GAAG,GAAG,SAAS,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,6CAA6C,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,iDAAiD,CAAC;IAClL,OAAO;QACL,cAAc,EAAE,WAAW,EAAE,OAAO;QACpC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG;QACrF,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC;QACrF,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM;KAC/C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const framesPerSecond = 30;
|
|
2
|
+
export type FrameWriter = {
|
|
3
|
+
frame(data: Buffer, atMs: number): Promise<void>;
|
|
4
|
+
finish(atMs: number): Promise<number>;
|
|
5
|
+
};
|
|
6
|
+
/** Writes frames to `sink`, timed from `startedAt` (ms); `finish` answers how many slots were written. */
|
|
7
|
+
export declare function frameWriter(sink: (frame: Buffer) => Promise<void>, startedAt: number): FrameWriter;
|
|
8
|
+
//# sourceMappingURL=frame-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-writer.d.ts","sourceRoot":"","sources":["../../src/record/frame-writer.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEtH,0GAA0G;AAC1G,wBAAgB,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAsBlG"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A steady 30 frames a second from a screencast that sends a frame only when the picture changes (plan 16 §16.5):
|
|
2
|
+
// each frame is written again, one copy a slot, until the next one comes, and the last until the recording ends.
|
|
3
|
+
export const framesPerSecond = 30;
|
|
4
|
+
/** Writes frames to `sink`, timed from `startedAt` (ms); `finish` answers how many slots were written. */
|
|
5
|
+
export function frameWriter(sink, startedAt) {
|
|
6
|
+
let previous;
|
|
7
|
+
let written = 0;
|
|
8
|
+
const slotAt = (atMs) => Math.max(0, Math.round(((atMs - startedAt) * framesPerSecond) / 1000));
|
|
9
|
+
const fillTo = async (slot) => {
|
|
10
|
+
if (previous === undefined)
|
|
11
|
+
return;
|
|
12
|
+
while (written < slot) {
|
|
13
|
+
await sink(previous);
|
|
14
|
+
written += 1;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
frame: async (data, atMs) => {
|
|
19
|
+
if (previous === undefined)
|
|
20
|
+
previous = data;
|
|
21
|
+
await fillTo(slotAt(atMs));
|
|
22
|
+
previous = data;
|
|
23
|
+
},
|
|
24
|
+
finish: async (atMs) => {
|
|
25
|
+
await fillTo(Math.max(slotAt(atMs), written + 1));
|
|
26
|
+
return written;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=frame-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-writer.js","sourceRoot":"","sources":["../../src/record/frame-writer.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,iHAAiH;AAEjH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAIlC,0GAA0G;AAC1G,MAAM,UAAU,WAAW,CAAC,IAAsC,EAAE,SAAiB;IACnF,IAAI,QAA4B,CAAC;IACjC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,SAAS,CAAC,GAAG,eAAe,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAChH,MAAM,MAAM,GAAG,KAAK,EAAE,IAAY,EAAiB,EAAE;QACnD,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO;QACnC,OAAO,OAAO,GAAG,IAAI,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,OAAO;QACL,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;YAC1B,IAAI,QAAQ,KAAK,SAAS;gBAAE,QAAQ,GAAG,IAAI,CAAC;YAC5C,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3B,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACrB,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YAClD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"own-port.d.ts","sourceRoot":"","sources":["../../src/record/own-port.ts"],"names":[],"mappings":"AAUA,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWjE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from '@kvman/sdk';
|
|
4
|
+
// kvman's own port, so a recording never reaches kvman's API as the person (ADR 0107, 18). kvman sets `KVMAN_HOME`
|
|
5
|
+
// in its environment and keeps its port in `<home>/kvman.lock` (plan 01 §1.2, §1.3). With no home, no lock, or a lock
|
|
6
|
+
// without a port, no port is kvman's.
|
|
7
|
+
const lockSchema = z.object({ pid: z.number(), port: z.number().int().positive().optional() });
|
|
8
|
+
export function ownPortOf(home) {
|
|
9
|
+
if (home === undefined || home === '')
|
|
10
|
+
return null;
|
|
11
|
+
let text;
|
|
12
|
+
try {
|
|
13
|
+
text = readFileSync(path.join(home, 'kvman.lock'), 'utf8');
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
17
|
+
return null;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const lock = lockSchema.safeParse(JSON.parse(text));
|
|
21
|
+
return lock.success && lock.data.port !== undefined ? lock.data.port : null;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=own-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"own-port.js","sourceRoot":"","sources":["../../src/record/own-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAE/B,mHAAmH;AACnH,sHAAsH;AACtH,sCAAsC;AAEtC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAE/F,MAAM,UAAU,SAAS,CAAC,IAAwB;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACnD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACtF,MAAM,KAAK,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
export type Pointer = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function glideTo(page: Page, from: Pointer, to: Pointer): Promise<Pointer>;
|
|
7
|
+
/** The center of the first visible match of a target, scrolled into view. */
|
|
8
|
+
export declare function centerOf(page: Page, target: string): Promise<Pointer>;
|
|
9
|
+
//# sourceMappingURL=pointer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointer.d.ts","sourceRoot":"","sources":["../../src/record/pointer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAQ5C,MAAM,MAAM,OAAO,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAOtF;AAED,6EAA6E;AAC7E,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO3E"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The mouse as the viewer sees it (ADR 0107, 19): it glides to each target over 600 ms, in 30 moves, so the drawn
|
|
2
|
+
// cursor travels instead of jumping.
|
|
3
|
+
const glideMoves = 30;
|
|
4
|
+
const glideMs = 600;
|
|
5
|
+
export async function glideTo(page, from, to) {
|
|
6
|
+
for (let move = 1; move <= glideMoves; move += 1) {
|
|
7
|
+
const share = move / glideMoves;
|
|
8
|
+
await page.mouse.move(from.x + (to.x - from.x) * share, from.y + (to.y - from.y) * share);
|
|
9
|
+
await page.waitForTimeout(glideMs / glideMoves);
|
|
10
|
+
}
|
|
11
|
+
return to;
|
|
12
|
+
}
|
|
13
|
+
/** The center of the first visible match of a target, scrolled into view. */
|
|
14
|
+
export async function centerOf(page, target) {
|
|
15
|
+
const element = page.locator(target).first();
|
|
16
|
+
await element.waitFor({ state: 'visible', timeout: 10_000 });
|
|
17
|
+
await element.scrollIntoViewIfNeeded({ timeout: 10_000 });
|
|
18
|
+
const box = await element.boundingBox({ timeout: 10_000 });
|
|
19
|
+
if (box === null)
|
|
20
|
+
throw new Error(`${target} has no box on the page`);
|
|
21
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=pointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointer.js","sourceRoot":"","sources":["../../src/record/pointer.ts"],"names":[],"mappings":"AAEA,kHAAkH;AAClH,qCAAqC;AAErC,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,OAAO,GAAG,GAAG,CAAC;AAIpB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU,EAAE,IAAa,EAAE,EAAW;IAClE,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,UAAU,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,GAAG,UAAU,CAAC;QAChC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;QAC1F,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,GAAG,UAAU,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAU,EAAE,MAAc;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7C,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7D,MAAM,OAAO,CAAC,sBAAsB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,yBAAyB,CAAC,CAAC;IACtE,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;AACjE,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Browser } from 'playwright-core';
|
|
2
|
+
import { type Program } from '../programs/run-program.ts';
|
|
3
|
+
import type { Step } from './steps.ts';
|
|
4
|
+
export type RecordPlan = {
|
|
5
|
+
url: string;
|
|
6
|
+
steps: readonly Step[];
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
folder: string;
|
|
10
|
+
ownPort: number | null;
|
|
11
|
+
ffmpeg: Program;
|
|
12
|
+
signal: AbortSignal;
|
|
13
|
+
now(): number;
|
|
14
|
+
};
|
|
15
|
+
export type Recording = {
|
|
16
|
+
video: string;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
steps: {
|
|
19
|
+
do: Step['do'];
|
|
20
|
+
atMs: number;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
export declare function record(browser: Browser, plan: RecordPlan): Promise<Recording>;
|
|
24
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/record/recorder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAwB,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAQrE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAMvC,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,GAAG,IAAI,MAAM,CAAA;CAAE,CAAC;AAE7L,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAC;AAqCzG,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAqBnF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { startPiped } from "../programs/piped-program.js";
|
|
3
|
+
import { lastLines } from "../programs/run-program.js";
|
|
4
|
+
import { kvvideoProblem } from "../problems.js";
|
|
5
|
+
import { isOwnAddress } from "./address.js";
|
|
6
|
+
import { cursorScript } from "./cursor-script.js";
|
|
7
|
+
import { recordEncoderArgs } from "./encoder-args.js";
|
|
8
|
+
import { frameWriter } from "./frame-writer.js";
|
|
9
|
+
import { pausesAfter, runStep } from "./run-step.js";
|
|
10
|
+
import { startScreencast } from "./screencast.js";
|
|
11
|
+
const firstLine = (error) => (error instanceof Error ? (error.message.split('\n').find((line) => line.trim() !== '') ?? error.name) : String(error)).trim();
|
|
12
|
+
async function contextFor(browser, plan) {
|
|
13
|
+
const context = await browser.newContext({ viewport: { width: plan.width, height: plan.height }, deviceScaleFactor: 1 });
|
|
14
|
+
await context.route((url) => isOwnAddress(url, plan.ownPort), (route) => route.abort('blockedbyclient'));
|
|
15
|
+
await context.addInitScript(cursorScript);
|
|
16
|
+
return context;
|
|
17
|
+
}
|
|
18
|
+
async function open(page, url) {
|
|
19
|
+
try {
|
|
20
|
+
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw kvvideoProblem('STEP_FAILED', `The page ${url} didn't load: ${firstLine(error)}`, { step: 0, reason: firstLine(error) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function play(page, plan, startedAt) {
|
|
27
|
+
await page.waitForTimeout(500);
|
|
28
|
+
let pointer = { x: plan.width / 2, y: plan.height / 2 };
|
|
29
|
+
await page.mouse.move(pointer.x, pointer.y);
|
|
30
|
+
const done = [];
|
|
31
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
32
|
+
done.push({ do: step.do, atMs: Math.round(plan.now() - startedAt) });
|
|
33
|
+
try {
|
|
34
|
+
pointer = await runStep(page, step, pointer);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw kvvideoProblem('STEP_FAILED', `Step ${String(index + 1)} (${step.do}) failed: ${firstLine(error)}`, { step: index + 1, reason: firstLine(error) });
|
|
38
|
+
}
|
|
39
|
+
if (pausesAfter(step))
|
|
40
|
+
await page.waitForTimeout(400);
|
|
41
|
+
}
|
|
42
|
+
await page.waitForTimeout(1000);
|
|
43
|
+
return done;
|
|
44
|
+
}
|
|
45
|
+
export async function record(browser, plan) {
|
|
46
|
+
const context = await contextFor(browser, plan);
|
|
47
|
+
const page = await context.newPage();
|
|
48
|
+
await open(page, plan.url);
|
|
49
|
+
const video = path.join(plan.folder, 'recording.mp4');
|
|
50
|
+
const stopEncoder = new AbortController();
|
|
51
|
+
const encoder = await startPiped(plan.ffmpeg, recordEncoderArgs(plan.width, plan.height, video), AbortSignal.any([plan.signal, stopEncoder.signal]));
|
|
52
|
+
try {
|
|
53
|
+
const startedAt = plan.now();
|
|
54
|
+
const writer = frameWriter((frame) => encoder.write(frame), startedAt);
|
|
55
|
+
const screencast = await startScreencast(page, plan, (frame, atMs) => writer.frame(frame, atMs), plan.now);
|
|
56
|
+
const steps = await play(page, plan, startedAt).finally(() => screencast.stop());
|
|
57
|
+
const endedAt = plan.now();
|
|
58
|
+
await writer.finish(endedAt);
|
|
59
|
+
const run = await encoder.end();
|
|
60
|
+
if (run.exitCode !== 0)
|
|
61
|
+
throw kvvideoProblem('RENDER_FAILED', `ffmpeg couldn't encode the recording (exit ${String(run.exitCode)}).`, { reason: lastLines(run.output) });
|
|
62
|
+
return { video, durationMs: Math.round(endedAt - startedAt), steps };
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
stopEncoder.abort();
|
|
66
|
+
await context.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/record/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-record.d.ts","sourceRoot":"","sources":["../../src/record/register-record.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAetC,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CA+B7C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { launchChannel } from "../browser/launch.js";
|
|
3
|
+
import { startBrowser } from "../browser/start-browser.js";
|
|
4
|
+
import { answerPath } from "../files/answer-path.js";
|
|
5
|
+
import { placeOutput, workFolder } from "../files/place-output.js";
|
|
6
|
+
import { outputPath } from "../files/workspace-paths.js";
|
|
7
|
+
import { programOf } from "../programs/programs.js";
|
|
8
|
+
import { localAddress } from "./address.js";
|
|
9
|
+
import { ownPortOf } from "./own-port.js";
|
|
10
|
+
import { record } from "./recorder.js";
|
|
11
|
+
import { recordInputSchema, recordOutputSchema } from "./steps.js";
|
|
12
|
+
// `video record` (plan 16 §16.5): every address and the output are checked before a browser starts; the browser is
|
|
13
|
+
// closed when the call ends, fails, or is cancelled, and the work folder removed.
|
|
14
|
+
export function registerRecord(ctx) {
|
|
15
|
+
ctx.registerCommand('kvvideo.clip.record', {
|
|
16
|
+
description: 'Records a page of an app running on this computer, with its steps, as an MP4 file in the workspace.',
|
|
17
|
+
input: recordInputSchema,
|
|
18
|
+
output: recordOutputSchema,
|
|
19
|
+
public: true,
|
|
20
|
+
retries: 0,
|
|
21
|
+
timeoutMs: 600_000,
|
|
22
|
+
handle: async (input) => {
|
|
23
|
+
const root = ctx.job.workspace.path;
|
|
24
|
+
const ownPort = ownPortOf(process.env['KVMAN_HOME']);
|
|
25
|
+
const url = localAddress(input.url, ownPort).href;
|
|
26
|
+
for (const step of input.steps ?? [])
|
|
27
|
+
if (step.do === 'goto')
|
|
28
|
+
localAddress(step.url, ownPort);
|
|
29
|
+
const output = outputPath(root, input.file, '.mp4', input.replace === true);
|
|
30
|
+
const ffmpeg = await programOf(ctx, 'ffmpeg');
|
|
31
|
+
const browser = await startBrowser(await ctx.settings.get('kvvideo.browser'), launchChannel);
|
|
32
|
+
const work = workFolder();
|
|
33
|
+
const stop = () => void browser.close();
|
|
34
|
+
ctx.job.signal.addEventListener('abort', stop, { once: true });
|
|
35
|
+
try {
|
|
36
|
+
const plan = { url, steps: input.steps ?? [], width: input.width ?? 1280, height: input.height ?? 720, folder: work.folder, ownPort, ffmpeg, signal: ctx.job.signal, now: () => performance.now() };
|
|
37
|
+
const recording = await record(browser, plan);
|
|
38
|
+
placeOutput(recording.video, output);
|
|
39
|
+
return { file: answerPath(root, output), durationMs: recording.durationMs, steps: recording.steps };
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
ctx.job.signal.removeEventListener('abort', stop);
|
|
43
|
+
await browser.close();
|
|
44
|
+
work.remove();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=register-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-record.js","sourceRoot":"","sources":["../../src/record/register-record.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import { type Pointer } from './pointer.ts';
|
|
3
|
+
import type { Step } from './steps.ts';
|
|
4
|
+
export declare function runStep(page: Page, step: Step, pointer: Pointer): Promise<Pointer>;
|
|
5
|
+
/** Whether the page is held a moment after the step, so the viewer sees what it did. */
|
|
6
|
+
export declare function pausesAfter(step: Step): boolean;
|
|
7
|
+
//# sourceMappingURL=run-step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-step.d.ts","sourceRoot":"","sources":["../../src/record/run-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAqB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAgBvC,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BxF;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAE/C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { centerOf, glideTo } from "./pointer.js";
|
|
2
|
+
// What each step does on the page (plan 16 §16.5). It answers where the mouse is afterwards.
|
|
3
|
+
const scrollStep = 100;
|
|
4
|
+
async function scroll(page, pixels) {
|
|
5
|
+
let left = pixels;
|
|
6
|
+
while (left !== 0) {
|
|
7
|
+
const part = Math.sign(left) * Math.min(scrollStep, Math.abs(left));
|
|
8
|
+
await page.mouse.wheel(0, part);
|
|
9
|
+
await page.waitForTimeout(30);
|
|
10
|
+
left -= part;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function runStep(page, step, pointer) {
|
|
14
|
+
switch (step.do) {
|
|
15
|
+
case 'goto':
|
|
16
|
+
await page.goto(step.url, { waitUntil: 'load', timeout: 30_000 });
|
|
17
|
+
return pointer;
|
|
18
|
+
case 'click': {
|
|
19
|
+
const at = await glideTo(page, pointer, await centerOf(page, step.target));
|
|
20
|
+
await page.mouse.click(at.x, at.y);
|
|
21
|
+
return at;
|
|
22
|
+
}
|
|
23
|
+
case 'type': {
|
|
24
|
+
const at = await glideTo(page, pointer, await centerOf(page, step.target));
|
|
25
|
+
await page.mouse.click(at.x, at.y);
|
|
26
|
+
await page.keyboard.type(step.text, { delay: 50 });
|
|
27
|
+
return at;
|
|
28
|
+
}
|
|
29
|
+
case 'press':
|
|
30
|
+
await page.keyboard.press(step.key);
|
|
31
|
+
return pointer;
|
|
32
|
+
case 'hover':
|
|
33
|
+
return glideTo(page, pointer, await centerOf(page, step.target));
|
|
34
|
+
case 'scroll':
|
|
35
|
+
await scroll(page, step.pixels);
|
|
36
|
+
return pointer;
|
|
37
|
+
case 'wait':
|
|
38
|
+
await page.waitForTimeout(step.ms);
|
|
39
|
+
return pointer;
|
|
40
|
+
case 'waitFor':
|
|
41
|
+
await page.locator(step.target).first().waitFor({ state: 'visible', timeout: 10_000 });
|
|
42
|
+
return pointer;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Whether the page is held a moment after the step, so the viewer sees what it did. */
|
|
46
|
+
export function pausesAfter(step) {
|
|
47
|
+
return step.do !== 'wait' && step.do !== 'waitFor';
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=run-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-step.js","sourceRoot":"","sources":["../../src/record/run-step.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
export type Screencast = {
|
|
3
|
+
stop(): Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
export declare function startScreencast(page: Page, size: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}, onFrame: (data: Buffer, atMs: number) => Promise<void>, now: () => number): Promise<Screencast>;
|
|
9
|
+
//# sourceMappingURL=screencast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screencast.d.ts","sourceRoot":"","sources":["../../src/record/screencast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAS5C,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEnD,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA4BzL"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from '@kvman/sdk';
|
|
2
|
+
// The page's screencast (plan 16 §16.5): Chrome sends a JPEG frame when the picture changes, and the next one only
|
|
3
|
+
// after the last is acknowledged, so frames are handled one at a time, in order, at the pace the writer takes them. A
|
|
4
|
+
// frame that can't be handled stops the rest, and `stop` throws its error.
|
|
5
|
+
const frameSchema = z.object({ data: z.string(), sessionId: z.number() });
|
|
6
|
+
export async function startScreencast(page, size, onFrame, now) {
|
|
7
|
+
const session = await page.context().newCDPSession(page);
|
|
8
|
+
let handled = Promise.resolve();
|
|
9
|
+
let stopped = false;
|
|
10
|
+
let failure;
|
|
11
|
+
session.on('Page.screencastFrame', (event) => {
|
|
12
|
+
const frame = frameSchema.parse(event);
|
|
13
|
+
const atMs = now();
|
|
14
|
+
handled = handled.then(async () => {
|
|
15
|
+
if (stopped || failure !== undefined)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
await onFrame(Buffer.from(frame.data, 'base64'), atMs);
|
|
19
|
+
await session.send('Page.screencastFrameAck', { sessionId: frame.sessionId });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
failure = error;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
await session.send('Page.startScreencast', { format: 'jpeg', quality: 90, maxWidth: size.width, maxHeight: size.height, everyNthFrame: 1 });
|
|
27
|
+
return {
|
|
28
|
+
stop: async () => {
|
|
29
|
+
stopped = true;
|
|
30
|
+
await session.send('Page.stopScreencast');
|
|
31
|
+
await handled;
|
|
32
|
+
await session.detach();
|
|
33
|
+
if (failure !== undefined)
|
|
34
|
+
throw failure;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=screencast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screencast.js","sourceRoot":"","sources":["../../src/record/screencast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAE/B,mHAAmH;AACnH,sHAAsH;AACtH,2EAA2E;AAE3E,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAI1E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAU,EAAE,IAAuC,EAAE,OAAsD,EAAE,GAAiB;IAClK,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,OAAO,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/C,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,OAAgB,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,sBAAsB,EAAE,CAAC,KAAc,EAAE,EAAE;QACpD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC;QACnB,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAChC,IAAI,OAAO,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO;YAC7C,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;gBACvD,MAAM,OAAO,CAAC,IAAI,CAAC,yBAAyB,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;YAChF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,GAAG,KAAK,CAAC;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5I,OAAO;QACL,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YAC1C,MAAM,OAAO,CAAC;YACd,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;YACvB,IAAI,OAAO,KAAK,SAAS;gBAAE,MAAM,OAAO,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from '@kvman/sdk';
|
|
2
|
+
export declare const stepSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3
|
+
do: z.ZodLiteral<"goto">;
|
|
4
|
+
url: z.ZodString;
|
|
5
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6
|
+
do: z.ZodLiteral<"click">;
|
|
7
|
+
target: z.ZodString;
|
|
8
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
9
|
+
do: z.ZodLiteral<"type">;
|
|
10
|
+
target: z.ZodString;
|
|
11
|
+
text: z.ZodString;
|
|
12
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
13
|
+
do: z.ZodLiteral<"press">;
|
|
14
|
+
key: z.ZodString;
|
|
15
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
16
|
+
do: z.ZodLiteral<"hover">;
|
|
17
|
+
target: z.ZodString;
|
|
18
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
19
|
+
do: z.ZodLiteral<"scroll">;
|
|
20
|
+
pixels: z.ZodNumber;
|
|
21
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
22
|
+
do: z.ZodLiteral<"wait">;
|
|
23
|
+
ms: z.ZodNumber;
|
|
24
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
25
|
+
do: z.ZodLiteral<"waitFor">;
|
|
26
|
+
target: z.ZodString;
|
|
27
|
+
}, z.core.$strict>], "do">;
|
|
28
|
+
/** One step of a recording. */
|
|
29
|
+
export type Step = z.output<typeof stepSchema>;
|
|
30
|
+
export declare const recordInputSchema: z.ZodObject<{
|
|
31
|
+
url: z.ZodString;
|
|
32
|
+
steps: z.ZodExactOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
33
|
+
do: z.ZodLiteral<"goto">;
|
|
34
|
+
url: z.ZodString;
|
|
35
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
36
|
+
do: z.ZodLiteral<"click">;
|
|
37
|
+
target: z.ZodString;
|
|
38
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
39
|
+
do: z.ZodLiteral<"type">;
|
|
40
|
+
target: z.ZodString;
|
|
41
|
+
text: z.ZodString;
|
|
42
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
43
|
+
do: z.ZodLiteral<"press">;
|
|
44
|
+
key: z.ZodString;
|
|
45
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
46
|
+
do: z.ZodLiteral<"hover">;
|
|
47
|
+
target: z.ZodString;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
do: z.ZodLiteral<"scroll">;
|
|
50
|
+
pixels: z.ZodNumber;
|
|
51
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
52
|
+
do: z.ZodLiteral<"wait">;
|
|
53
|
+
ms: z.ZodNumber;
|
|
54
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
55
|
+
do: z.ZodLiteral<"waitFor">;
|
|
56
|
+
target: z.ZodString;
|
|
57
|
+
}, z.core.$strict>], "do">>>;
|
|
58
|
+
file: z.ZodString;
|
|
59
|
+
width: z.ZodExactOptional<z.ZodNumber>;
|
|
60
|
+
height: z.ZodExactOptional<z.ZodNumber>;
|
|
61
|
+
replace: z.ZodExactOptional<z.ZodBoolean>;
|
|
62
|
+
}, z.core.$strict>;
|
|
63
|
+
export declare const recordOutputSchema: z.ZodObject<{
|
|
64
|
+
file: z.ZodString;
|
|
65
|
+
durationMs: z.ZodNumber;
|
|
66
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
67
|
+
do: z.ZodString;
|
|
68
|
+
atMs: z.ZodNumber;
|
|
69
|
+
}, z.core.$strip>>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
//# sourceMappingURL=steps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/record/steps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAO/B,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;0BASrB,CAAC;AAEH,+BAA+B;AAC/B,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAI/C,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAO5B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;iBAI7B,CAAC"}
|