@kvman/kvvideo 0.0.0-stage → 0.1.1
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 +105 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/brand/brand.d.ts +17 -0
- package/dist/brand/brand.d.ts.map +1 -0
- package/dist/brand/brand.js +44 -0
- package/dist/brand/brand.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 +6 -0
- package/dist/browser/launch.d.ts.map +1 -0
- package/dist/browser/launch.js +14 -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 +17 -0
- package/dist/compose/ass-file.d.ts.map +1 -0
- package/dist/compose/ass-file.js +65 -0
- package/dist/compose/ass-file.js.map +1 -0
- package/dist/compose/background.d.ts +23 -0
- package/dist/compose/background.d.ts.map +1 -0
- package/dist/compose/background.js +31 -0
- package/dist/compose/background.js.map +1 -0
- package/dist/compose/camera.d.ts +32 -0
- package/dist/compose/camera.d.ts.map +1 -0
- package/dist/compose/camera.js +98 -0
- package/dist/compose/camera.js.map +1 -0
- package/dist/compose/caption-font.d.ts +8 -0
- package/dist/compose/caption-font.d.ts.map +1 -0
- package/dist/compose/caption-font.js +24 -0
- package/dist/compose/caption-font.js.map +1 -0
- package/dist/compose/compose-drawing.d.ts +19 -0
- package/dist/compose/compose-drawing.d.ts.map +1 -0
- package/dist/compose/compose-drawing.js +38 -0
- package/dist/compose/compose-drawing.js.map +1 -0
- package/dist/compose/compose-input.d.ts +187 -0
- package/dist/compose/compose-input.d.ts.map +1 -0
- package/dist/compose/compose-input.js +76 -0
- package/dist/compose/compose-input.js.map +1 -0
- package/dist/compose/compose-plan.d.ts +36 -0
- package/dist/compose/compose-plan.d.ts.map +1 -0
- package/dist/compose/compose-plan.js +55 -0
- package/dist/compose/compose-plan.js.map +1 -0
- package/dist/compose/compose-size.d.ts +50 -0
- package/dist/compose/compose-size.d.ts.map +1 -0
- package/dist/compose/compose-size.js +72 -0
- package/dist/compose/compose-size.js.map +1 -0
- package/dist/compose/drawn-images.d.ts +19 -0
- package/dist/compose/drawn-images.d.ts.map +1 -0
- package/dist/compose/drawn-images.js +75 -0
- package/dist/compose/drawn-images.js.map +1 -0
- package/dist/compose/final-pass.d.ts +34 -0
- package/dist/compose/final-pass.d.ts.map +1 -0
- package/dist/compose/final-pass.js +106 -0
- package/dist/compose/final-pass.js.map +1 -0
- package/dist/compose/frame-layout.d.ts +30 -0
- package/dist/compose/frame-layout.d.ts.map +1 -0
- package/dist/compose/frame-layout.js +33 -0
- package/dist/compose/frame-layout.js.map +1 -0
- package/dist/compose/overlays.d.ts +20 -0
- package/dist/compose/overlays.d.ts.map +1 -0
- package/dist/compose/overlays.js +30 -0
- package/dist/compose/overlays.js.map +1 -0
- package/dist/compose/page-again.d.ts +26 -0
- package/dist/compose/page-again.d.ts.map +1 -0
- package/dist/compose/page-again.js +50 -0
- package/dist/compose/page-again.js.map +1 -0
- package/dist/compose/player-copies.d.ts +10 -0
- package/dist/compose/player-copies.d.ts.map +1 -0
- package/dist/compose/player-copies.js +34 -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 +151 -0
- package/dist/compose/register-compose.js.map +1 -0
- package/dist/compose/render.d.ts +25 -0
- package/dist/compose/render.d.ts.map +1 -0
- package/dist/compose/render.js +61 -0
- package/dist/compose/render.js.map +1 -0
- package/dist/compose/scene-args.d.ts +37 -0
- package/dist/compose/scene-args.d.ts.map +1 -0
- package/dist/compose/scene-args.js +103 -0
- package/dist/compose/scene-args.js.map +1 -0
- package/dist/compose/scene-look.d.ts +31 -0
- package/dist/compose/scene-look.d.ts.map +1 -0
- package/dist/compose/scene-look.js +46 -0
- package/dist/compose/scene-look.js.map +1 -0
- package/dist/compose/scene-plan.d.ts +26 -0
- package/dist/compose/scene-plan.d.ts.map +1 -0
- package/dist/compose/scene-plan.js +34 -0
- package/dist/compose/scene-plan.js.map +1 -0
- package/dist/compose/sound-effects.d.ts +23 -0
- package/dist/compose/sound-effects.d.ts.map +1 -0
- package/dist/compose/sound-effects.js +40 -0
- package/dist/compose/sound-effects.js.map +1 -0
- package/dist/compose/srt.d.ts +3 -0
- package/dist/compose/srt.d.ts.map +1 -0
- package/dist/compose/srt.js +10 -0
- package/dist/compose/srt.js.map +1 -0
- package/dist/compose/transitions.d.ts +16 -0
- package/dist/compose/transitions.d.ts.map +1 -0
- package/dist/compose/transitions.js +38 -0
- package/dist/compose/transitions.js.map +1 -0
- package/dist/connector.d.ts +132 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +63 -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/sidecar.d.ts +209 -0
- package/dist/files/sidecar.d.ts.map +1 -0
- package/dist/files/sidecar.js +66 -0
- package/dist/files/sidecar.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 +30 -0
- package/dist/index.js.map +1 -0
- package/dist/narration/alignment.d.ts +15 -0
- package/dist/narration/alignment.d.ts.map +1 -0
- package/dist/narration/alignment.js +21 -0
- package/dist/narration/alignment.js.map +1 -0
- package/dist/narration/captions.d.ts +28 -0
- package/dist/narration/captions.d.ts.map +1 -0
- package/dist/narration/captions.js +70 -0
- package/dist/narration/captions.js.map +1 -0
- package/dist/narration/join-args.d.ts +3 -0
- package/dist/narration/join-args.d.ts.map +1 -0
- package/dist/narration/join-args.js +19 -0
- package/dist/narration/join-args.js.map +1 -0
- package/dist/narration/narration-cache.d.ts +25 -0
- package/dist/narration/narration-cache.d.ts.map +1 -0
- package/dist/narration/narration-cache.js +36 -0
- package/dist/narration/narration-cache.js.map +1 -0
- package/dist/narration/piper.d.ts +4 -0
- package/dist/narration/piper.d.ts.map +1 -0
- package/dist/narration/piper.js +13 -0
- package/dist/narration/piper.js.map +1 -0
- package/dist/narration/pronunciations.d.ts +11 -0
- package/dist/narration/pronunciations.d.ts.map +1 -0
- package/dist/narration/pronunciations.js +40 -0
- package/dist/narration/pronunciations.js.map +1 -0
- package/dist/narration/register-narration.d.ts +25 -0
- package/dist/narration/register-narration.d.ts.map +1 -0
- package/dist/narration/register-narration.js +103 -0
- package/dist/narration/register-narration.js.map +1 -0
- package/dist/narration/register-voice.d.ts +27 -0
- package/dist/narration/register-voice.d.ts.map +1 -0
- package/dist/narration/register-voice.js +82 -0
- package/dist/narration/register-voice.js.map +1 -0
- package/dist/narration/sentence-speaker.d.ts +35 -0
- package/dist/narration/sentence-speaker.d.ts.map +1 -0
- package/dist/narration/sentence-speaker.js +44 -0
- package/dist/narration/sentence-speaker.js.map +1 -0
- package/dist/narration/speech-services.d.ts +39 -0
- package/dist/narration/speech-services.d.ts.map +1 -0
- package/dist/narration/speech-services.js +67 -0
- package/dist/narration/speech-services.js.map +1 -0
- package/dist/narration/voice-args.d.ts +3 -0
- package/dist/narration/voice-args.d.ts.map +1 -0
- package/dist/narration/voice-args.js +10 -0
- package/dist/narration/voice-args.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 +22 -0
- package/dist/programs/media-duration.d.ts.map +1 -0
- package/dist/programs/media-duration.js +23 -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 +23 -0
- package/dist/programs/run-program.d.ts.map +1 -0
- package/dist/programs/run-program.js +47 -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 +26 -0
- package/dist/record/address.js.map +1 -0
- package/dist/record/cursor-script.d.ts +3 -0
- package/dist/record/cursor-script.d.ts.map +1 -0
- package/dist/record/cursor-script.js +59 -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/motion.d.ts +19 -0
- package/dist/record/motion.d.ts.map +1 -0
- package/dist/record/motion.js +59 -0
- package/dist/record/motion.js.map +1 -0
- package/dist/record/narration-cues.d.ts +14 -0
- package/dist/record/narration-cues.d.ts.map +1 -0
- package/dist/record/narration-cues.js +12 -0
- package/dist/record/narration-cues.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/page-ready.d.ts +4 -0
- package/dist/record/page-ready.d.ts.map +1 -0
- package/dist/record/page-ready.js +15 -0
- package/dist/record/page-ready.js.map +1 -0
- package/dist/record/page-route.d.ts +13 -0
- package/dist/record/page-route.d.ts.map +1 -0
- package/dist/record/page-route.js +64 -0
- package/dist/record/page-route.js.map +1 -0
- package/dist/record/pointer.d.ts +8 -0
- package/dist/record/pointer.d.ts.map +1 -0
- package/dist/record/pointer.js +24 -0
- package/dist/record/pointer.js.map +1 -0
- package/dist/record/recorder.d.ts +34 -0
- package/dist/record/recorder.d.ts.map +1 -0
- package/dist/record/recorder.js +99 -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 +83 -0
- package/dist/record/register-record.js.map +1 -0
- package/dist/record/run-step.d.ts +20 -0
- package/dist/record/run-step.d.ts.map +1 -0
- package/dist/record/run-step.js +69 -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 +124 -0
- package/dist/record/steps.d.ts.map +1 -0
- package/dist/record/steps.js +47 -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/review/analysis.d.ts +19 -0
- package/dist/review/analysis.d.ts.map +1 -0
- package/dist/review/analysis.js +36 -0
- package/dist/review/analysis.js.map +1 -0
- package/dist/review/frames.d.ts +9 -0
- package/dist/review/frames.d.ts.map +1 -0
- package/dist/review/frames.js +28 -0
- package/dist/review/frames.js.map +1 -0
- package/dist/review/register-review.d.ts +24 -0
- package/dist/review/register-review.d.ts.map +1 -0
- package/dist/review/register-review.js +81 -0
- package/dist/review/register-review.js.map +1 -0
- package/dist/review/sheet-args.d.ts +11 -0
- package/dist/review/sheet-args.d.ts.map +1 -0
- package/dist/review/sheet-args.js +24 -0
- package/dist/review/sheet-args.js.map +1 -0
- package/dist/settings.d.ts +33 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +29 -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 +26 -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 +55 -0
- package/docs/guide.md +28 -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 +48 -0
- package/locales/en.json +48 -0
- package/package.json +73 -3
- package/sounds/SOURCES.md +13 -0
- package/sounds/click.wav +0 -0
- package/sounds/keys.wav +0 -0
- package/sounds/pop.wav +0 -0
- package/sounds/whoosh.wav +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What a recorded page draws (plan 16 §16.5; ADR 0115, 9 and 53): a browser's recording has no pointer, so on a
|
|
2
|
+
// desktop the page draws an arrow that follows the mouse, hidden until the mouse first moves; a press shrinks it for
|
|
3
|
+
// 120 ms and shows a ring. On a phone it draws a round mark at each touch instead. Scroll bars are hidden. The script
|
|
4
|
+
// is added to every page before its own scripts run.
|
|
5
|
+
const arrowPath = 'M3 2 L3 20 L7.6 15.6 L10.6 22.6 L13.8 21.2 L10.9 14.4 L17.2 14.4 Z';
|
|
6
|
+
const shared = `
|
|
7
|
+
const hideScrollBars = () => {
|
|
8
|
+
const style = document.createElement('style');
|
|
9
|
+
style.textContent = 'html { scrollbar-width: none; } ::-webkit-scrollbar { display: none; }';
|
|
10
|
+
document.documentElement.appendChild(style);
|
|
11
|
+
};
|
|
12
|
+
const ring = (x, y, size, color) => {
|
|
13
|
+
const mark = document.createElement('div');
|
|
14
|
+
mark.className = 'kvvideo-ring';
|
|
15
|
+
mark.style.cssText = 'position:fixed;direction:ltr;inset-inline-start:' + (x - size / 2) + 'px;inset-block-start:' + (y - size / 2) + 'px;inline-size:' + size + 'px;block-size:' + size + 'px;border-radius:50%;box-sizing:border-box;border:3px solid ' + color + ';pointer-events:none;z-index:2147483646';
|
|
16
|
+
document.documentElement.appendChild(mark);
|
|
17
|
+
mark.animate([{ opacity: 1, transform: 'scale(.4)' }, { opacity: 0, transform: 'scale(1.3)' }], { duration: 500 }).onfinish = () => mark.remove();
|
|
18
|
+
};`;
|
|
19
|
+
const desktop = `
|
|
20
|
+
const draw = () => {
|
|
21
|
+
if (document.getElementById('kvvideo-cursor')) return;
|
|
22
|
+
hideScrollBars();
|
|
23
|
+
const pointer = document.createElement('div');
|
|
24
|
+
pointer.id = 'kvvideo-cursor';
|
|
25
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
26
|
+
for (const [name, value] of [['width', '26'], ['height', '26'], ['viewBox', '0 0 26 26']]) svg.setAttribute(name, value);
|
|
27
|
+
const shape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
28
|
+
for (const [name, value] of [['d', ${JSON.stringify(arrowPath)}], ['fill', '#fff'], ['stroke', '#111'], ['stroke-width', '1.6'], ['stroke-linejoin', 'round']]) shape.setAttribute(name, value);
|
|
29
|
+
svg.appendChild(shape);
|
|
30
|
+
pointer.appendChild(svg);
|
|
31
|
+
pointer.style.cssText = 'position:fixed;direction:ltr;inset-inline-start:0;inset-block-start:0;inline-size:26px;block-size:26px;margin-block-start:-2px;margin-inline-start:-3px;display:none;transform-origin:3px 2px;pointer-events:none;z-index:2147483647;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))';
|
|
32
|
+
document.documentElement.appendChild(pointer);
|
|
33
|
+
let x = 0; let y = 0; let scale = 1;
|
|
34
|
+
const place = () => { pointer.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(' + scale + ')'; };
|
|
35
|
+
window.addEventListener('mousemove', (event) => { x = event.clientX; y = event.clientY; pointer.style.display = 'block'; place(); }, true);
|
|
36
|
+
window.addEventListener('mousedown', (event) => {
|
|
37
|
+
scale = 0.85; place();
|
|
38
|
+
setTimeout(() => { scale = 1; place(); }, 120);
|
|
39
|
+
ring(event.clientX, event.clientY, 44, 'rgba(255,190,0,.9)');
|
|
40
|
+
}, true);
|
|
41
|
+
};`;
|
|
42
|
+
const phone = `
|
|
43
|
+
const draw = () => {
|
|
44
|
+
if (document.getElementById('kvvideo-touch')) return;
|
|
45
|
+
hideScrollBars();
|
|
46
|
+
const marker = document.createElement('meta');
|
|
47
|
+
marker.id = 'kvvideo-touch';
|
|
48
|
+
document.documentElement.appendChild(marker);
|
|
49
|
+
window.addEventListener('touchstart', (event) => {
|
|
50
|
+
for (const touch of event.changedTouches) ring(touch.clientX, touch.clientY, 40, 'rgba(255,255,255,.9)');
|
|
51
|
+
}, true);
|
|
52
|
+
};`;
|
|
53
|
+
/** The script a page of the device gets. */
|
|
54
|
+
export function cursorScript(device) {
|
|
55
|
+
return `(() => {${shared}${device === 'phone' ? phone : desktop}
|
|
56
|
+
if (document.documentElement) draw(); else document.addEventListener('DOMContentLoaded', draw);
|
|
57
|
+
})();`;
|
|
58
|
+
}
|
|
59
|
+
//# 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,gHAAgH;AAChH,qHAAqH;AACrH,sHAAsH;AACtH,qDAAqD;AAErD,MAAM,SAAS,GAAG,oEAAoE,CAAC;AAEvF,MAAM,MAAM,GAAG;;;;;;;;;;;;KAYV,CAAC;AAEN,MAAM,OAAO,GAAG;;;;;;;;;yCASyB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;;;;;;;;;;;;;KAa7D,CAAC;AAEN,MAAM,KAAK,GAAG;;;;;;;;;;KAUT,CAAC;AAEN,4CAA4C;AAC5C,MAAM,UAAU,YAAY,CAAC,MAA2B;IACtD,OAAO,WAAW,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;;MAE3D,CAAC;AACP,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', '14', '-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,19 @@
|
|
|
1
|
+
export declare const frameMs = 16;
|
|
2
|
+
export type Point = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
/** Cubic easing in and out, from 0 to 1. */
|
|
7
|
+
export declare function easeInOut(share: number): number;
|
|
8
|
+
/** How long the pointer takes to travel `distance` pixels. */
|
|
9
|
+
export declare function glideMs(distance: number): number;
|
|
10
|
+
/** The pointer's positions, one a frame, from `from` (left out) to `to`, along a gentle curve. */
|
|
11
|
+
export declare function glidePath(from: Point, to: Point): Point[];
|
|
12
|
+
/** The wait before each character of `text`: 35 to 110 ms, and 150 ms more after a space or a punctuation mark. */
|
|
13
|
+
export declare function typingDelays(text: string): number[];
|
|
14
|
+
/** An eased scroll of `pixels`: its length and its move in each frame, which add up to `pixels`. */
|
|
15
|
+
export declare function scrollPlan(pixels: number): {
|
|
16
|
+
durationMs: number;
|
|
17
|
+
moves: number[];
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=motion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/record/motion.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,OAAO,KAAK,CAAC;AAE1B,MAAM,MAAM,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,4CAA4C;AAC5C,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAID,8DAA8D;AAC9D,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,KAAK,EAAE,CAYzD;AAgBD,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAInD;AAED,oGAAoG;AACpG,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAWlF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// How things move in a recording (plan 16 §16.5; ADR 0115, 9): the pointer's eased, gently curved path, a typing
|
|
2
|
+
// pace that varies but is the same for the same text, and an eased scroll. Each is planned in frames of `frameMs`.
|
|
3
|
+
export const frameMs = 16;
|
|
4
|
+
/** Cubic easing in and out, from 0 to 1. */
|
|
5
|
+
export function easeInOut(share) {
|
|
6
|
+
return share < 0.5 ? 4 * share ** 3 : 1 - (-2 * share + 2) ** 3 / 2;
|
|
7
|
+
}
|
|
8
|
+
const frameCount = (durationMs) => Math.max(1, Math.round(durationMs / frameMs));
|
|
9
|
+
/** How long the pointer takes to travel `distance` pixels. */
|
|
10
|
+
export function glideMs(distance) {
|
|
11
|
+
return Math.min(900, Math.max(400, Math.round(300 + 0.6 * distance)));
|
|
12
|
+
}
|
|
13
|
+
/** The pointer's positions, one a frame, from `from` (left out) to `to`, along a gentle curve. */
|
|
14
|
+
export function glidePath(from, to) {
|
|
15
|
+
const dx = to.x - from.x;
|
|
16
|
+
const dy = to.y - from.y;
|
|
17
|
+
const bend = 0.08;
|
|
18
|
+
const control = { x: (from.x + to.x) / 2 - dy * bend, y: (from.y + to.y) / 2 + dx * bend };
|
|
19
|
+
const frames = frameCount(glideMs(Math.hypot(dx, dy)));
|
|
20
|
+
return Array.from({ length: frames }, (_, index) => {
|
|
21
|
+
const share = easeInOut((index + 1) / frames);
|
|
22
|
+
const toControl = 2 * (1 - share) * share;
|
|
23
|
+
const toEnd = share * share;
|
|
24
|
+
return { x: from.x + toControl * (control.x - from.x) + toEnd * dx, y: from.y + toControl * (control.y - from.y) + toEnd * dy };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// A small seeded generator (mulberry32 over an FNV-1a hash of the text), so the same text types the same way.
|
|
28
|
+
function seededRandom(text) {
|
|
29
|
+
let seed = 0x811c9dc5;
|
|
30
|
+
for (const unit of text)
|
|
31
|
+
seed = Math.imul(seed ^ (unit.codePointAt(0) ?? 0), 0x01000193);
|
|
32
|
+
return () => {
|
|
33
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
34
|
+
let mixed = Math.imul(seed ^ (seed >>> 15), seed | 1);
|
|
35
|
+
mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61);
|
|
36
|
+
return ((mixed ^ (mixed >>> 14)) >>> 0) / 4_294_967_296;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const pausesAfter = (character) => /[\s\p{P}]/u.test(character);
|
|
40
|
+
/** The wait before each character of `text`: 35 to 110 ms, and 150 ms more after a space or a punctuation mark. */
|
|
41
|
+
export function typingDelays(text) {
|
|
42
|
+
const random = seededRandom(text);
|
|
43
|
+
const characters = [...text];
|
|
44
|
+
return characters.map((_, index) => 35 + Math.floor(random() * 76) + (index > 0 && pausesAfter(characters[index - 1] ?? '') ? 150 : 0));
|
|
45
|
+
}
|
|
46
|
+
/** An eased scroll of `pixels`: its length and its move in each frame, which add up to `pixels`. */
|
|
47
|
+
export function scrollPlan(pixels) {
|
|
48
|
+
const durationMs = Math.min(3000, Math.round(400 + 0.5 * Math.abs(pixels)));
|
|
49
|
+
const frames = frameCount(durationMs);
|
|
50
|
+
let before = 0;
|
|
51
|
+
const moves = Array.from({ length: frames }, (_, index) => {
|
|
52
|
+
const at = Math.round(pixels * easeInOut((index + 1) / frames));
|
|
53
|
+
const move = at - before;
|
|
54
|
+
before = at;
|
|
55
|
+
return move;
|
|
56
|
+
});
|
|
57
|
+
return { durationMs, moves };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=motion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"motion.js","sourceRoot":"","sources":["../../src/record/motion.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const narrationLeadMs = 300;
|
|
2
|
+
export type Cues = {
|
|
3
|
+
lineStartsMs: number[];
|
|
4
|
+
leastLengthMs: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function cuesOf(narration: {
|
|
7
|
+
durationMs: number;
|
|
8
|
+
captions: readonly {
|
|
9
|
+
startMs: number;
|
|
10
|
+
}[];
|
|
11
|
+
}): Cues;
|
|
12
|
+
/** How long to wait, `nowMs` into the recording, until `atMs`; nothing once it has passed. */
|
|
13
|
+
export declare function waitUntilMs(atMs: number, nowMs: number): number;
|
|
14
|
+
//# sourceMappingURL=narration-cues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narration-cues.d.ts","sourceRoot":"","sources":["../../src/record/narration-cues.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC,MAAM,MAAM,IAAI,GAAG;IAAE,YAAY,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,wBAAgB,MAAM,CAAC,SAAS,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,GAAG,IAAI,CAExG;AAED,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// When a recording's steps start, from its narration (plan 16 §16.5; ADR 0115, 16 and 41): the narration starts
|
|
2
|
+
// 300 ms into the scene, so line `n` is heard at 300 ms plus its start, and the recording lasts at least the
|
|
3
|
+
// narration with 300 ms before and after it.
|
|
4
|
+
export const narrationLeadMs = 300;
|
|
5
|
+
export function cuesOf(narration) {
|
|
6
|
+
return { lineStartsMs: narration.captions.map((caption) => narrationLeadMs + caption.startMs), leastLengthMs: narrationLeadMs + narration.durationMs + narrationLeadMs };
|
|
7
|
+
}
|
|
8
|
+
/** How long to wait, `nowMs` into the recording, until `atMs`; nothing once it has passed. */
|
|
9
|
+
export function waitUntilMs(atMs, nowMs) {
|
|
10
|
+
return Math.max(0, Math.round(atMs - nowMs));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=narration-cues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narration-cues.js","sourceRoot":"","sources":["../../src/record/narration-cues.ts"],"names":[],"mappings":"AAAA,gHAAgH;AAChH,6GAA6G;AAC7G,6CAA6C;AAE7C,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAInC,MAAM,UAAU,MAAM,CAAC,SAA2E;IAChG,OAAO,EAAE,YAAY,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,GAAG,SAAS,CAAC,UAAU,GAAG,eAAe,EAAE,CAAC;AAC3K,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC;AAC/C,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 @@
|
|
|
1
|
+
{"version":3,"file":"page-ready.d.ts","sourceRoot":"","sources":["../../src/record/page-ready.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAO5C,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,kBAAkB,GAAG,UAAU,CAAC,CAAC;AAIpE,wBAAsB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A page is ready to record once its network has been idle for 500 ms, waited for at most 10 s, and all its fonts
|
|
2
|
+
// have loaded (plan 16 §16.5). A network that never goes idle doesn't stop the recording; any other failure does.
|
|
3
|
+
const idleTimeoutMs = 10_000;
|
|
4
|
+
const isTimeout = (error) => error instanceof Error && error.name === 'TimeoutError';
|
|
5
|
+
export async function waitUntilReady(page) {
|
|
6
|
+
try {
|
|
7
|
+
await page.waitForLoadState('networkidle', { timeout: idleTimeoutMs });
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!isTimeout(error))
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
await page.evaluate('document.fonts.ready.then(() => true)');
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=page-ready.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-ready.js","sourceRoot":"","sources":["../../src/record/page-ready.ts"],"names":[],"mappings":"AAEA,kHAAkH;AAClH,kHAAkH;AAElH,MAAM,aAAa,GAAG,MAAM,CAAC;AAI7B,MAAM,SAAS,GAAG,CAAC,KAAc,EAAW,EAAE,CAAC,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC;AAEvG,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAe;IAClD,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACzE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC;IACrC,CAAC;IACD,MAAM,IAAI,CAAC,QAAQ,CAAC,uCAAuC,CAAC,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const pageOrigin = "http://page.kvvideo.invalid";
|
|
2
|
+
export type PageAnswer = {
|
|
3
|
+
status: 200;
|
|
4
|
+
file: string;
|
|
5
|
+
contentType: string;
|
|
6
|
+
} | {
|
|
7
|
+
status: 404;
|
|
8
|
+
};
|
|
9
|
+
/** The address a page file is opened at. */
|
|
10
|
+
export declare function pageAddress(page: string): string;
|
|
11
|
+
/** What a request of the page's origin is answered with, from the page's folder. */
|
|
12
|
+
export declare function answerPageRequest(folder: string, address: string): PageAnswer;
|
|
13
|
+
//# sourceMappingURL=page-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-route.d.ts","sourceRoot":"","sources":["../../src/record/page-route.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,UAAU,gCAAgC,CAAC;AA6BxD,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAE9F,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAiBD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAO7E"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// An HTML page of the workspace, recorded without a server (plan 16 §16.5; ADR 0115, 32): it is opened on an origin
|
|
4
|
+
// that never resolves, and each request to that origin is answered with the file at its path under the page's folder.
|
|
5
|
+
// A path that leaves the folder, through `..` or a symlink, or a file that doesn't exist, is answered 404.
|
|
6
|
+
export const pageOrigin = 'http://page.kvvideo.invalid';
|
|
7
|
+
const contentTypes = {
|
|
8
|
+
'.html': 'text/html; charset=utf-8',
|
|
9
|
+
'.htm': 'text/html; charset=utf-8',
|
|
10
|
+
'.css': 'text/css; charset=utf-8',
|
|
11
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
12
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
13
|
+
'.json': 'application/json',
|
|
14
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
15
|
+
'.svg': 'image/svg+xml',
|
|
16
|
+
'.png': 'image/png',
|
|
17
|
+
'.jpg': 'image/jpeg',
|
|
18
|
+
'.jpeg': 'image/jpeg',
|
|
19
|
+
'.gif': 'image/gif',
|
|
20
|
+
'.webp': 'image/webp',
|
|
21
|
+
'.avif': 'image/avif',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.woff': 'font/woff',
|
|
24
|
+
'.woff2': 'font/woff2',
|
|
25
|
+
'.ttf': 'font/ttf',
|
|
26
|
+
'.otf': 'font/otf',
|
|
27
|
+
'.mp4': 'video/mp4',
|
|
28
|
+
'.webm': 'video/webm',
|
|
29
|
+
'.mp3': 'audio/mpeg',
|
|
30
|
+
'.wav': 'audio/wav',
|
|
31
|
+
'.ogg': 'audio/ogg',
|
|
32
|
+
};
|
|
33
|
+
/** The address a page file is opened at. */
|
|
34
|
+
export function pageAddress(page) {
|
|
35
|
+
return `${pageOrigin}/${encodeURIComponent(path.basename(page))}`;
|
|
36
|
+
}
|
|
37
|
+
function inside(folder, candidate) {
|
|
38
|
+
const relative = path.relative(folder, candidate);
|
|
39
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
40
|
+
}
|
|
41
|
+
function decodedPath(address) {
|
|
42
|
+
const { pathname } = new URL(address);
|
|
43
|
+
try {
|
|
44
|
+
return decodeURIComponent(pathname).replace(/^\/+/, '');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof URIError)
|
|
48
|
+
return undefined;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** What a request of the page's origin is answered with, from the page's folder. */
|
|
53
|
+
export function answerPageRequest(folder, address) {
|
|
54
|
+
const relative = decodedPath(address);
|
|
55
|
+
if (relative === undefined)
|
|
56
|
+
return { status: 404 };
|
|
57
|
+
const file = path.resolve(folder, relative);
|
|
58
|
+
if (!inside(folder, file) || !existsSync(file))
|
|
59
|
+
return { status: 404 };
|
|
60
|
+
if (!inside(realpathSync(folder), realpathSync(file)) || !statSync(file).isFile())
|
|
61
|
+
return { status: 404 };
|
|
62
|
+
return { status: 200, file, contentType: contentTypes[path.extname(file).toLowerCase()] ?? 'application/octet-stream' };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=page-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-route.js","sourceRoot":"","sources":["../../src/record/page-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import { type Point } from './motion.ts';
|
|
3
|
+
import type { Box } from './steps.ts';
|
|
4
|
+
export declare function glideTo(page: Page, from: Point, to: Point): Promise<Point>;
|
|
5
|
+
/** The box of the first visible match of a target, scrolled into view, in whole CSS pixels of the viewport. */
|
|
6
|
+
export declare function targetBox(page: Page, target: string): Promise<Box>;
|
|
7
|
+
export declare function centerOf(box: Box): Point;
|
|
8
|
+
//# 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;AAC5C,OAAO,EAAsB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAKtC,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAMhF;AAED,+GAA+G;AAC/G,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAOxE;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,CAExC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { frameMs, glidePath } from "./motion.js";
|
|
2
|
+
// The mouse as the viewer sees it (plan 16 §16.5; ADR 0115, 9): it glides to each target along an eased, gently
|
|
3
|
+
// curved path, one move a frame, so the drawn pointer travels instead of jumping.
|
|
4
|
+
export async function glideTo(page, from, to) {
|
|
5
|
+
for (const point of glidePath(from, to)) {
|
|
6
|
+
await page.mouse.move(point.x, point.y);
|
|
7
|
+
await page.waitForTimeout(frameMs);
|
|
8
|
+
}
|
|
9
|
+
return to;
|
|
10
|
+
}
|
|
11
|
+
/** The box of the first visible match of a target, scrolled into view, in whole CSS pixels of the viewport. */
|
|
12
|
+
export async function targetBox(page, target) {
|
|
13
|
+
const element = page.locator(target).first();
|
|
14
|
+
await element.waitFor({ state: 'visible', timeout: 10_000 });
|
|
15
|
+
await element.scrollIntoViewIfNeeded({ timeout: 10_000 });
|
|
16
|
+
const box = await element.boundingBox({ timeout: 10_000 });
|
|
17
|
+
if (box === null)
|
|
18
|
+
throw new Error(`${target} has no box on the page`);
|
|
19
|
+
return { x: Math.round(box.x), y: Math.round(box.y), width: Math.round(box.width), height: Math.round(box.height) };
|
|
20
|
+
}
|
|
21
|
+
export function centerOf(box) {
|
|
22
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=pointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointer.js","sourceRoot":"","sources":["../../src/record/pointer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAc,MAAM,aAAa,CAAC;AAG7D,gHAAgH;AAChH,kFAAkF;AAElF,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU,EAAE,IAAW,EAAE,EAAS;IAC9D,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QACxC,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,+GAA+G;AAC/G,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAU,EAAE,MAAc;IACxD,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,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;AACtH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAQ;IAC/B,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,34 @@
|
|
|
1
|
+
import type { Browser, BrowserContext } from 'playwright-core';
|
|
2
|
+
import { type Program } from '../programs/run-program.ts';
|
|
3
|
+
import { type Cues } from './narration-cues.ts';
|
|
4
|
+
import type { Action, Step } from './steps.ts';
|
|
5
|
+
export type RecordSource = {
|
|
6
|
+
kind: 'site';
|
|
7
|
+
url: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'page';
|
|
10
|
+
file: string;
|
|
11
|
+
};
|
|
12
|
+
export type RecordPlan = {
|
|
13
|
+
source: RecordSource;
|
|
14
|
+
steps: readonly Step[];
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
device: 'desktop' | 'phone';
|
|
18
|
+
cues: Cues | undefined;
|
|
19
|
+
folder: string;
|
|
20
|
+
ownPort: number | null;
|
|
21
|
+
ffmpeg: Program;
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
now(): number;
|
|
24
|
+
stepStarted(number: number, of: number): void;
|
|
25
|
+
};
|
|
26
|
+
export type Recording = {
|
|
27
|
+
video: string;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
actions: Action[];
|
|
30
|
+
};
|
|
31
|
+
/** A fresh browser context for a recording: its size and device, kvman's port aborted, a page's files, the pointer. */
|
|
32
|
+
export declare function recordingContext(browser: Browser, plan: Pick<RecordPlan, 'source' | 'width' | 'height' | 'device' | 'ownPort'>): Promise<BrowserContext>;
|
|
33
|
+
export declare function record(browser: Browser, plan: RecordPlan): Promise<Recording>;
|
|
34
|
+
//# 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,EAAE,cAAc,EAAQ,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAMrE,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAK7D,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAO/C,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1F,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,GAAG,IAAI,MAAM,CAAC;IACd,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAMjF,uHAAuH;AACvH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAa9J;AA0CD,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAsBnF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { waitUntilMs } from "./narration-cues.js";
|
|
10
|
+
import { answerPageRequest, pageAddress, pageOrigin } from "./page-route.js";
|
|
11
|
+
import { waitUntilReady } from "./page-ready.js";
|
|
12
|
+
import { pausesAfter, runStep } from "./run-step.js";
|
|
13
|
+
import { startScreencast } from "./screencast.js";
|
|
14
|
+
const scale = 2;
|
|
15
|
+
const firstLine = (error) => (error instanceof Error ? (error.message.split('\n').find((line) => line.trim() !== '') ?? error.name) : String(error)).trim();
|
|
16
|
+
/** A fresh browser context for a recording: its size and device, kvman's port aborted, a page's files, the pointer. */
|
|
17
|
+
export async function recordingContext(browser, plan) {
|
|
18
|
+
const phone = plan.device === 'phone';
|
|
19
|
+
const context = await browser.newContext({ viewport: { width: plan.width, height: plan.height }, deviceScaleFactor: scale, isMobile: phone, hasTouch: phone });
|
|
20
|
+
await context.route((url) => isOwnAddress(url, plan.ownPort), (route) => route.abort('blockedbyclient'));
|
|
21
|
+
const { source } = plan;
|
|
22
|
+
if (source.kind === 'page') {
|
|
23
|
+
await context.route(`${pageOrigin}/**`, async (route) => {
|
|
24
|
+
const answer = answerPageRequest(path.dirname(source.file), route.request().url());
|
|
25
|
+
await (answer.status === 404 ? route.fulfill({ status: 404, body: '' }) : route.fulfill({ status: 200, path: answer.file, contentType: answer.contentType }));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
await context.addInitScript(cursorScript(plan.device));
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
async function open(page, source) {
|
|
32
|
+
const address = source.kind === 'site' ? source.url : pageAddress(source.file);
|
|
33
|
+
try {
|
|
34
|
+
await page.goto(address, { waitUntil: 'load', timeout: 30_000 });
|
|
35
|
+
await waitUntilReady(page);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw kvvideoProblem('STEP_FAILED', `The page ${address} didn't load: ${firstLine(error)}`, { step: 0, reason: firstLine(error) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function waitForCue(page, plan, step, elapsed) {
|
|
42
|
+
if (step.do === 'wait' || step.atLine === undefined || plan.cues === undefined)
|
|
43
|
+
return;
|
|
44
|
+
const wait = waitUntilMs(plan.cues.lineStartsMs[step.atLine - 1] ?? 0, elapsed());
|
|
45
|
+
if (wait > 0)
|
|
46
|
+
await page.waitForTimeout(wait);
|
|
47
|
+
}
|
|
48
|
+
async function play(page, plan, startedAt) {
|
|
49
|
+
const elapsed = () => Math.round(plan.now() - startedAt);
|
|
50
|
+
await page.waitForTimeout(500);
|
|
51
|
+
const state = { page, device: plan.device, pointer: { x: plan.width / 2, y: plan.height / 2 }, elapsed };
|
|
52
|
+
const actions = [];
|
|
53
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
54
|
+
plan.stepStarted(index + 1, plan.steps.length);
|
|
55
|
+
await waitForCue(page, plan, step, elapsed);
|
|
56
|
+
const atMs = elapsed();
|
|
57
|
+
try {
|
|
58
|
+
const result = await runStep(state, step);
|
|
59
|
+
state.pointer = result.pointer;
|
|
60
|
+
actions.push({ do: step.do, atMs, endMs: elapsed(), ...(result.act === undefined ? {} : { actMs: result.act.atMs, box: result.act.box }) });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw kvvideoProblem('STEP_FAILED', `Step ${String(index + 1)} (${step.do}) failed: ${firstLine(error)}`, { step: index + 1, reason: firstLine(error) });
|
|
64
|
+
}
|
|
65
|
+
if (pausesAfter(step))
|
|
66
|
+
await page.waitForTimeout(400);
|
|
67
|
+
}
|
|
68
|
+
await page.waitForTimeout(1000);
|
|
69
|
+
const rest = plan.cues === undefined ? 0 : waitUntilMs(plan.cues.leastLengthMs, elapsed());
|
|
70
|
+
if (rest > 0)
|
|
71
|
+
await page.waitForTimeout(rest);
|
|
72
|
+
return actions;
|
|
73
|
+
}
|
|
74
|
+
export async function record(browser, plan) {
|
|
75
|
+
const context = await recordingContext(browser, plan);
|
|
76
|
+
const page = await context.newPage();
|
|
77
|
+
await open(page, plan.source);
|
|
78
|
+
const size = { width: plan.width * scale, height: plan.height * scale };
|
|
79
|
+
const video = path.join(plan.folder, 'recording.mp4');
|
|
80
|
+
const stopEncoder = new AbortController();
|
|
81
|
+
const encoder = await startPiped(plan.ffmpeg, recordEncoderArgs(size.width, size.height, video), AbortSignal.any([plan.signal, stopEncoder.signal]));
|
|
82
|
+
try {
|
|
83
|
+
const startedAt = plan.now();
|
|
84
|
+
const writer = frameWriter((frame) => encoder.write(frame), startedAt);
|
|
85
|
+
const screencast = await startScreencast(page, size, (frame, atMs) => writer.frame(frame, atMs), plan.now);
|
|
86
|
+
const actions = await play(page, plan, startedAt).finally(() => screencast.stop());
|
|
87
|
+
const endedAt = plan.now();
|
|
88
|
+
await writer.finish(endedAt);
|
|
89
|
+
const run = await encoder.end();
|
|
90
|
+
if (run.exitCode !== 0)
|
|
91
|
+
throw kvvideoProblem('RENDER_FAILED', `ffmpeg couldn't encode the recording (exit ${String(run.exitCode)}).`, { reason: lastLines(run.output) });
|
|
92
|
+
return { video, durationMs: Math.round(endedAt - startedAt), actions };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
stopEncoder.abort();
|
|
96
|
+
await context.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=recorder.js.map
|