recordable 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 +303 -0
- package/dist/actions.d.ts +140 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +184 -0
- package/dist/actions.js.map +1 -0
- package/dist/audio/track.d.ts +45 -0
- package/dist/audio/track.d.ts.map +1 -0
- package/dist/audio/track.js +61 -0
- package/dist/audio/track.js.map +1 -0
- package/dist/browser/cursor.d.ts +33 -0
- package/dist/browser/cursor.d.ts.map +1 -0
- package/dist/browser/cursor.js +118 -0
- package/dist/browser/cursor.js.map +1 -0
- package/dist/browser/dom.d.ts +31 -0
- package/dist/browser/dom.d.ts.map +1 -0
- package/dist/browser/dom.js +134 -0
- package/dist/browser/dom.js.map +1 -0
- package/dist/browser/play-button.d.ts +11 -0
- package/dist/browser/play-button.d.ts.map +1 -0
- package/dist/browser/play-button.js +87 -0
- package/dist/browser/play-button.js.map +1 -0
- package/dist/browser/runtime.d.ts +66 -0
- package/dist/browser/runtime.d.ts.map +1 -0
- package/dist/browser/runtime.js +271 -0
- package/dist/browser/runtime.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +131 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/mix.d.ts +13 -0
- package/dist/compose/mix.d.ts.map +1 -0
- package/dist/compose/mix.js +50 -0
- package/dist/compose/mix.js.map +1 -0
- package/dist/compose/recordable.d.ts +149 -0
- package/dist/compose/recordable.d.ts.map +1 -0
- package/dist/compose/recordable.js +337 -0
- package/dist/compose/recordable.js.map +1 -0
- package/dist/compose/session.d.ts +38 -0
- package/dist/compose/session.d.ts.map +1 -0
- package/dist/compose/session.js +122 -0
- package/dist/compose/session.js.map +1 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/ffmpeg.d.ts +8 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +55 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/formats/json.d.ts +12 -0
- package/dist/formats/json.d.ts.map +1 -0
- package/dist/formats/json.js +20 -0
- package/dist/formats/json.js.map +1 -0
- package/dist/formats/markdown/method.d.ts +25 -0
- package/dist/formats/markdown/method.d.ts.map +1 -0
- package/dist/formats/markdown/method.js +48 -0
- package/dist/formats/markdown/method.js.map +1 -0
- package/dist/formats/markdown/parse.d.ts +44 -0
- package/dist/formats/markdown/parse.d.ts.map +1 -0
- package/dist/formats/markdown/parse.js +143 -0
- package/dist/formats/markdown/parse.js.map +1 -0
- package/dist/fs.d.ts +9 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +100 -0
- package/dist/schema.js.map +1 -0
- package/dist/script.d.ts +21 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +26 -0
- package/dist/script.js.map +1 -0
- package/dist/targets.d.ts +6 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +13 -0
- package/dist/targets.js.map +1 -0
- package/dist/timing.d.ts +41 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +149 -0
- package/dist/timing.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +8 -0
- package/dist/utils.js.map +1 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +54 -0
- package/dist/validate.js.map +1 -0
- package/dist/video/recorder.d.ts +57 -0
- package/dist/video/recorder.d.ts.map +1 -0
- package/dist/video/recorder.js +238 -0
- package/dist/video/recorder.js.map +1 -0
- package/dist/video/stitch.d.ts +15 -0
- package/dist/video/stitch.d.ts.map +1 -0
- package/dist/video/stitch.js +111 -0
- package/dist/video/stitch.js.map +1 -0
- package/dist/voiceover/alignment.d.ts +14 -0
- package/dist/voiceover/alignment.d.ts.map +1 -0
- package/dist/voiceover/alignment.js +13 -0
- package/dist/voiceover/alignment.js.map +1 -0
- package/dist/voiceover/cache.d.ts +22 -0
- package/dist/voiceover/cache.d.ts.map +1 -0
- package/dist/voiceover/cache.js +55 -0
- package/dist/voiceover/cache.js.map +1 -0
- package/dist/voiceover/compile.d.ts +35 -0
- package/dist/voiceover/compile.d.ts.map +1 -0
- package/dist/voiceover/compile.js +194 -0
- package/dist/voiceover/compile.js.map +1 -0
- package/dist/voiceover/elevenlabs.d.ts +16 -0
- package/dist/voiceover/elevenlabs.d.ts.map +1 -0
- package/dist/voiceover/elevenlabs.js +66 -0
- package/dist/voiceover/elevenlabs.js.map +1 -0
- package/dist/voiceover/index.d.ts +7 -0
- package/dist/voiceover/index.d.ts.map +1 -0
- package/dist/voiceover/index.js +8 -0
- package/dist/voiceover/index.js.map +1 -0
- package/dist/voiceover/mock.d.ts +15 -0
- package/dist/voiceover/mock.d.ts.map +1 -0
- package/dist/voiceover/mock.js +41 -0
- package/dist/voiceover/mock.js.map +1 -0
- package/dist/voiceover/types.d.ts +31 -0
- package/dist/voiceover/types.d.ts.map +1 -0
- package/dist/voiceover/types.js +10 -0
- package/dist/voiceover/types.js.map +1 -0
- package/package.json +86 -0
- package/recordable.schema.json +738 -0
package/dist/timing.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { getDuration } from "./ffmpeg.js";
|
|
3
|
+
// ─── Gesture timing (single source of truth) ─────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// An interactive action isn't instantaneous: the cursor eases to the target,
|
|
6
|
+
// dips to "press", and a click waits a beat to see if it navigated. The runtime
|
|
7
|
+
// *spends* this time; the voiceover compiler must *predict* it, or every wait it
|
|
8
|
+
// computes is short by a gesture and actions drift late. Both import these
|
|
9
|
+
// constants so the prediction can't silently fall out of step.
|
|
10
|
+
/** Cursor "press" dip on click — scale down… */
|
|
11
|
+
export const PRESS_DOWN_MS = 120;
|
|
12
|
+
/** …then settle back. clickEffect spends their sum. */
|
|
13
|
+
export const PRESS_SETTLE_MS = 60;
|
|
14
|
+
/** Total clickEffect cost. */
|
|
15
|
+
const CLICK_PRESS_MS = PRESS_DOWN_MS + PRESS_SETTLE_MS;
|
|
16
|
+
/** Settle beat between arriving at a target and pressing (jitter base). */
|
|
17
|
+
export const PRE_CLICK_MS = 100;
|
|
18
|
+
/** Post-click probe: how long a click waits for a possible navigation to begin. */
|
|
19
|
+
export const NAV_PROBE_MS = 200;
|
|
20
|
+
/** Cursor-move duration bounds; the move eases from its current position. */
|
|
21
|
+
const CURSOR_MOVE_MIN_MS = 150;
|
|
22
|
+
const CURSOR_MOVE_MAX_MS = 700;
|
|
23
|
+
/** Cursor-move duration for a known pixel distance — eased, clamped. */
|
|
24
|
+
export function cursorMoveMs(dist) {
|
|
25
|
+
return Math.min(CURSOR_MOVE_MAX_MS, Math.max(CURSOR_MOVE_MIN_MS, dist * 0.5));
|
|
26
|
+
}
|
|
27
|
+
/** Compile-time estimate of a cursor move when the distance can't be known (no
|
|
28
|
+
* DOM at compile). A single representative value: the true move is distance-
|
|
29
|
+
* based, so a marker may still land a few hundred ms off — the overrun warning
|
|
30
|
+
* catches the cases that matter. */
|
|
31
|
+
const CURSOR_MOVE_ESTIMATE_MS = 350;
|
|
32
|
+
/** Estimated wall-clock an interactive action spends getting the cursor to its
|
|
33
|
+
* target and pressing — *before* its payload (the keystrokes of a `type`, the
|
|
34
|
+
* value-set of a `select`). The compiler adds this to elapsed so the next
|
|
35
|
+
* narrated word is placed after the gesture, not on top of it. With the cursor
|
|
36
|
+
* overlay off, only the real (non-animated) costs remain. */
|
|
37
|
+
export function gestureLeadMs(step, cfg) {
|
|
38
|
+
const cursor = cfg.cursor ?? true;
|
|
39
|
+
const move = cursor ? CURSOR_MOVE_ESTIMATE_MS : 0;
|
|
40
|
+
const press = cursor ? PRE_CLICK_MS + CLICK_PRESS_MS : 0;
|
|
41
|
+
switch (step.action) {
|
|
42
|
+
case "click":
|
|
43
|
+
case "type":
|
|
44
|
+
case "clear":
|
|
45
|
+
// type/clear focus the field with the same move-press-probe as a click.
|
|
46
|
+
return move + press + NAV_PROBE_MS;
|
|
47
|
+
case "select":
|
|
48
|
+
// Animates to the control and presses, but sets the value directly — no
|
|
49
|
+
// mouse click, so no navigation probe.
|
|
50
|
+
return move + press;
|
|
51
|
+
case "hover":
|
|
52
|
+
return move; // moves only — no press
|
|
53
|
+
default:
|
|
54
|
+
return 0; // key / waitFor / pause / … — no cursor travel
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** How long an action occupies the timeline, so the next wait measures from its end.
|
|
58
|
+
* Omitted durations use the config default, never an elastic fit. The cursor's
|
|
59
|
+
* travel-and-press to a target (`gestureLeadMs`) is added on top, so a click/type
|
|
60
|
+
* doesn't silently push the rest of the paragraph late. */
|
|
61
|
+
export async function actionDurationMs(step, cfg) {
|
|
62
|
+
const lead = gestureLeadMs(step, cfg);
|
|
63
|
+
switch (step.action) {
|
|
64
|
+
case "wait":
|
|
65
|
+
return step.ms ?? 0;
|
|
66
|
+
case "insert": {
|
|
67
|
+
// An inserted clip advances the recorded timeline by its full length; the
|
|
68
|
+
// overlaid narration plays across it, so this much audio-relative time is
|
|
69
|
+
// consumed and the next marker's wait is only the remainder. Resolve the
|
|
70
|
+
// clip against baseDir, the same as the runtime's `_resolveFile`.
|
|
71
|
+
const p = step.path;
|
|
72
|
+
const file = isAbsolute(p) ? p : resolve(cfg.baseDir ?? "", p);
|
|
73
|
+
return (await getDuration(file)) * 1000;
|
|
74
|
+
}
|
|
75
|
+
case "zoom":
|
|
76
|
+
case "resetZoom":
|
|
77
|
+
return step.duration ?? cfg.zoomDuration ?? 600;
|
|
78
|
+
case "scroll":
|
|
79
|
+
return step.duration ?? cfg.scrollDuration ?? 1200;
|
|
80
|
+
case "type": {
|
|
81
|
+
// Travel to the field (lead) then the keystrokes. The runtime's `type` sums
|
|
82
|
+
// its jittered delays to exactly `typingDuration`, so that part agrees.
|
|
83
|
+
const keys = step.duration ??
|
|
84
|
+
typingDuration(step.text ?? "", cfg.typingSpeed ?? 7);
|
|
85
|
+
return lead + keys;
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
return lead; // click / select / hover travel; key / waitFor … are 0
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ─── Randomness ──────────────────────────────────────────────────────────────
|
|
92
|
+
/** Returns `base` ± `variance` (defaults to ±50% of base). */
|
|
93
|
+
export function jitter(base, variance = 0.5) {
|
|
94
|
+
return base + (Math.random() - 0.5) * base * variance * 2;
|
|
95
|
+
}
|
|
96
|
+
// ─── Deterministic typing ──────────────────────────────────────────────────────
|
|
97
|
+
// `type` is jittered for realism yet deterministic in *total* time: the keystroke
|
|
98
|
+
// delays vary but always sum to `typingDuration`. So the voiceover compiler can
|
|
99
|
+
// predict a `type` action's length from the text alone (no stored duration), and
|
|
100
|
+
// the runtime delivers exactly that.
|
|
101
|
+
/** Deterministic 32-bit string hash (FNV-1a). Seeds the typing PRNG so the same
|
|
102
|
+
* text always types with the same rhythm (reproducible recordings). */
|
|
103
|
+
export function hashString(s) {
|
|
104
|
+
let h = 0x811c9dc5;
|
|
105
|
+
for (let i = 0; i < s.length; i++) {
|
|
106
|
+
h ^= s.charCodeAt(i);
|
|
107
|
+
h = Math.imul(h, 0x01000193);
|
|
108
|
+
}
|
|
109
|
+
return h >>> 0;
|
|
110
|
+
}
|
|
111
|
+
/** mulberry32 PRNG → a function yielding floats in [0, 1). Pure integer math,
|
|
112
|
+
* platform-independent, so a given seed reproduces the same sequence anywhere. */
|
|
113
|
+
export function rng(seed) {
|
|
114
|
+
let a = seed >>> 0;
|
|
115
|
+
return () => {
|
|
116
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
117
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
118
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
119
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Total time (ms) a `type` action occupies — a pure function of text length and
|
|
123
|
+
* speed (cps). This is the contract the voiceover compiler estimates against, so
|
|
124
|
+
* it MUST stay identical to the compiler's `type` estimate. Jitter never alters it. */
|
|
125
|
+
export function typingDuration(text, speed) {
|
|
126
|
+
return Math.round((text.length / (speed > 0 ? speed : 1)) * 1000);
|
|
127
|
+
}
|
|
128
|
+
/** Per-keystroke delays (ms) that sum to exactly `total`, with seeded, zero-sum
|
|
129
|
+
* jitter. Returns `[leadPause, delayAfterChar1, …]` (lead beat + one per code
|
|
130
|
+
* point). Punctuation gets a heavier structural weight (a natural micro-pause),
|
|
131
|
+
* but all weights are normalised back onto `total` so the sum is invariant. */
|
|
132
|
+
export function typingGaps(text, speed, total = typingDuration(text, speed), amount = 0.35) {
|
|
133
|
+
const chars = [...text];
|
|
134
|
+
if (chars.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
const a = Math.min(Math.max(amount, 0), 0.95); // keep weights strictly positive
|
|
137
|
+
const next = rng(hashString(text));
|
|
138
|
+
const LEAD_W = 1.2;
|
|
139
|
+
const PUNCT_W = 1.8;
|
|
140
|
+
const perturb = (w) => w * (1 + a * (next() - 0.5) * 2);
|
|
141
|
+
const weights = [perturb(LEAD_W)];
|
|
142
|
+
for (const ch of chars) {
|
|
143
|
+
const structural = ch === " " || ch === "." || ch === "," || ch === "\n" ? PUNCT_W : 1;
|
|
144
|
+
weights.push(perturb(structural));
|
|
145
|
+
}
|
|
146
|
+
const sum = weights.reduce((acc, w) => acc + w, 0);
|
|
147
|
+
return weights.map((w) => (total * w) / sum);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,MAAM,CAEvD"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Tiny, dependency-free helpers shared across layers.
|
|
2
|
+
export function sleep(ms) {
|
|
3
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
}
|
|
5
|
+
export function truncate(text, max = 40) {
|
|
6
|
+
return text.length > max ? text.slice(0, max) + "…" : text;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,EAAE;IAC7C,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RecordableConfig, VoiceoverConfig } from "./config.js";
|
|
2
|
+
/** Validate a recording-config object (JSON `config`, frontmatter, or a caller).
|
|
3
|
+
* Returns it typed; throws {@link RecordableError} `CONFIG_INVALID` on a bad shape. */
|
|
4
|
+
export declare function parseConfig(input: unknown): RecordableConfig;
|
|
5
|
+
/** Validate a `voiceover` frontmatter block. Returns it typed; throws
|
|
6
|
+
* {@link RecordableError} `CONFIG_INVALID` on a bad shape. */
|
|
7
|
+
export declare function parseVoiceover(input: unknown): VoiceoverConfig;
|
|
8
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AA0CrE;wFACwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAS5D;AAED;+DAC+D;AAC/D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAS9D"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { RecordableError } from "./errors.js";
|
|
3
|
+
import { ConfigSchema } from "./config.js";
|
|
4
|
+
// ─── Boundary validation ─────────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Untrusted config enters from JSON `config` blocks, Markdown frontmatter, and
|
|
7
|
+
// programmatic callers. Zod checks the shape *at the boundary* so a bad value
|
|
8
|
+
// (or a typo'd key) fails with a clear message here, not as a confusing crash
|
|
9
|
+
// deep in a run. Action shapes are validated separately by the manifest in
|
|
10
|
+
// `actions.ts` (which also generates the published JSON Schema) — not duplicated here.
|
|
11
|
+
// Validate against a copy of the config schema with every `.default()` stripped
|
|
12
|
+
// and each field made optional. A provided config then passes through with only
|
|
13
|
+
// its own keys (defaults are applied later, when resolving against DEFAULT_CONFIG),
|
|
14
|
+
// so the config-layering in `Recordable` stays intact. `.partial()` alone is not
|
|
15
|
+
// enough — the inner `.default()` still fills missing keys.
|
|
16
|
+
const ConfigInputSchema = z.strictObject(Object.fromEntries(Object.entries(ConfigSchema.shape).map(([key, field]) => [
|
|
17
|
+
key,
|
|
18
|
+
(field instanceof z.ZodDefault ? field.def.innerType : field).optional(),
|
|
19
|
+
])));
|
|
20
|
+
const VoiceoverSchema = z.strictObject({
|
|
21
|
+
provider: z.string().optional(),
|
|
22
|
+
voiceId: z.string().optional(),
|
|
23
|
+
modelId: z.string().optional(),
|
|
24
|
+
apiKey: z.string().optional(),
|
|
25
|
+
voiceSettings: z.record(z.string(), z.number()).optional(),
|
|
26
|
+
format: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
/** One readable line per issue: `<label>.<path>: <message>`. */
|
|
29
|
+
function describe(label, issues) {
|
|
30
|
+
const parts = issues.map((issue) => {
|
|
31
|
+
const path = issue.path.join(".");
|
|
32
|
+
return `${path ? `${label}.${path}` : label}: ${issue.message}`;
|
|
33
|
+
});
|
|
34
|
+
return parts.join("; ");
|
|
35
|
+
}
|
|
36
|
+
/** Validate a recording-config object (JSON `config`, frontmatter, or a caller).
|
|
37
|
+
* Returns it typed; throws {@link RecordableError} `CONFIG_INVALID` on a bad shape. */
|
|
38
|
+
export function parseConfig(input) {
|
|
39
|
+
const result = ConfigInputSchema.safeParse(input ?? {});
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
throw new RecordableError("CONFIG_INVALID", `Invalid config — ${describe("config", result.error.issues)}`);
|
|
42
|
+
}
|
|
43
|
+
return result.data;
|
|
44
|
+
}
|
|
45
|
+
/** Validate a `voiceover` frontmatter block. Returns it typed; throws
|
|
46
|
+
* {@link RecordableError} `CONFIG_INVALID` on a bad shape. */
|
|
47
|
+
export function parseVoiceover(input) {
|
|
48
|
+
const result = VoiceoverSchema.safeParse(input ?? {});
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
throw new RecordableError("CONFIG_INVALID", `Invalid voiceover config — ${describe("voiceover", result.error.issues)}`);
|
|
51
|
+
}
|
|
52
|
+
return result.data;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,gFAAgF;AAChF,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,8EAA8E;AAC9E,2EAA2E;AAC3E,uFAAuF;AAEvF,gFAAgF;AAChF,gFAAgF;AAChF,oFAAoF;AACpF,iFAAiF;AACjF,4DAA4D;AAC5D,MAAM,iBAAiB,GAAG,CAAC,CAAC,YAAY,CACtC,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;IACvD,GAAG;IACH,CAAC,KAAK,YAAY,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE;CACzE,CAAC,CACH,CACF,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,aAAa,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC1D,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEH,gEAAgE;AAChE,SAAS,QAAQ,CAAC,KAAa,EAAE,MAA0B;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;IAClE,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;wFACwF;AACxF,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,eAAe,CACvB,gBAAgB,EAChB,oBAAoB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAC9D,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,IAAwB,CAAC;AACzC,CAAC;AAED;+DAC+D;AAC/D,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,eAAe,CACvB,gBAAgB,EAChB,8BAA8B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAC3E,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Page } from "puppeteer";
|
|
2
|
+
import { type Logger } from "../logger.js";
|
|
3
|
+
import { type InsertOptions, type ResolvedConfig } from "../config.js";
|
|
4
|
+
import { type Segment } from "./stitch.js";
|
|
5
|
+
/**
|
|
6
|
+
* Recorded-time position now: finalised segment time plus the in-flight segment's
|
|
7
|
+
* elapsed time (`frames / fps`). Off-camera (paused) stretches capture no frames,
|
|
8
|
+
* so they never advance this clock — audio lands in recorded time.
|
|
9
|
+
*/
|
|
10
|
+
export declare function timelineMs(completedMs: number, segmentFrames: number, fps: number, capturing: boolean): number;
|
|
11
|
+
export declare class Recorder {
|
|
12
|
+
private readonly getCfg;
|
|
13
|
+
private readonly log;
|
|
14
|
+
private tmpDirPath;
|
|
15
|
+
private segmentList;
|
|
16
|
+
private currentSegment;
|
|
17
|
+
private cdp;
|
|
18
|
+
private ffmpegProc;
|
|
19
|
+
private frameTicker;
|
|
20
|
+
private latestFrame;
|
|
21
|
+
private segmentFrames;
|
|
22
|
+
private segmentFps;
|
|
23
|
+
private captureError;
|
|
24
|
+
private completedMs;
|
|
25
|
+
constructor(getCfg: () => ResolvedConfig, log: Logger);
|
|
26
|
+
/** True while a segment is actively capturing frames. */
|
|
27
|
+
get capturing(): boolean;
|
|
28
|
+
/** The captured + inserted segments, in timeline order (for stitching). */
|
|
29
|
+
get segments(): Segment[];
|
|
30
|
+
/** The temp working directory holding segment files. */
|
|
31
|
+
get tmpDir(): string;
|
|
32
|
+
/** Recorded-time position now: finalised segments + the in-flight segment. */
|
|
33
|
+
currentTimelineMs(): number;
|
|
34
|
+
/** Create the temp working directory for segment files. Call once before use. */
|
|
35
|
+
init(): void;
|
|
36
|
+
/** Lazily create the CDP session used for screencast capture. */
|
|
37
|
+
private _ensureCdp;
|
|
38
|
+
/** Begin capturing into a fresh segment. No-op if already capturing. */
|
|
39
|
+
begin(page: Page): Promise<void>;
|
|
40
|
+
/** End the active segment, flushing ffmpeg and keeping it only if it has frames.
|
|
41
|
+
* Pass `silent` to skip the "pause" log (used by `insert`, which seals the
|
|
42
|
+
* current segment as an internal step rather than a user-visible pause). */
|
|
43
|
+
end(silent?: boolean): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Seal the active segment (silently), normalize the clip to the recording's
|
|
46
|
+
* resolution / fps / codec / pixel format, and append it as the next segment.
|
|
47
|
+
* Fades (ms) are recorded on the segment and applied at stitch time. Doesn't
|
|
48
|
+
* touch recording *intent* — if capture was active the run loop lazily begins a
|
|
49
|
+
* fresh segment before the next action, so recording resumes on its own.
|
|
50
|
+
*/
|
|
51
|
+
insert(path: string, options?: InsertOptions): Promise<void>;
|
|
52
|
+
/** Detach the CDP session. Call after the final segment is sealed. */
|
|
53
|
+
dispose(): Promise<void>;
|
|
54
|
+
/** Remove the temp working directory. Call once the output is written. */
|
|
55
|
+
removeTmp(): void;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/video/recorder.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,IAAI,EAAmB,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAa3C;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,OAAO,GACjB,MAAM,CAGR;AAED,qBAAa,QAAQ;IAiBjB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAjBtB,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,YAAY,CAAsB;IAG1C,OAAO,CAAC,WAAW,CAAK;gBAGL,MAAM,EAAE,MAAM,cAAc,EAC5B,GAAG,EAAE,MAAM;IAG9B,yDAAyD;IACzD,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,2EAA2E;IAC3E,IAAI,QAAQ,IAAI,OAAO,EAAE,CAExB;IAED,wDAAwD;IACxD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,8EAA8E;IAC9E,iBAAiB,IAAI,MAAM;IAK3B,iFAAiF;IACjF,IAAI,IAAI,IAAI;IAIZ,iEAAiE;YACnD,UAAU;IAaxB,wEAAwE;IAClE,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAsEtC;;iFAE6E;IACvE,GAAG,CAAC,MAAM,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CxC;;;;;;OAMG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCtE,sEAAsE;IAChE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B,0EAA0E;IAC1E,SAAS,IAAI,IAAI;CAGlB"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { FFMPEG_PATH, getDuration, runFfmpeg } from "../ffmpeg.js";
|
|
6
|
+
import { RecordableError } from "../errors.js";
|
|
7
|
+
// ─── Video layer: capture ────────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Captures the page via CDP `Page.startScreencast`, pipes JPEG frames into ffmpeg
|
|
10
|
+
// at a steady fps to produce one MP4 per captured stretch, and tracks the
|
|
11
|
+
// recorded-time clock the audio layer positions clips against. Stitching the
|
|
12
|
+
// segments into the final video lives in `./stitch.ts`.
|
|
13
|
+
//
|
|
14
|
+
// Segments are started lazily and ended on pause, so off-camera gaps (page loads,
|
|
15
|
+
// logins, data setup) leave no trace. Config is read through `getCfg` on each call
|
|
16
|
+
// so runtime changes take effect.
|
|
17
|
+
/**
|
|
18
|
+
* Recorded-time position now: finalised segment time plus the in-flight segment's
|
|
19
|
+
* elapsed time (`frames / fps`). Off-camera (paused) stretches capture no frames,
|
|
20
|
+
* so they never advance this clock — audio lands in recorded time.
|
|
21
|
+
*/
|
|
22
|
+
export function timelineMs(completedMs, segmentFrames, fps, capturing) {
|
|
23
|
+
const current = capturing && fps > 0 ? (segmentFrames / fps) * 1000 : 0;
|
|
24
|
+
return completedMs + current;
|
|
25
|
+
}
|
|
26
|
+
export class Recorder {
|
|
27
|
+
getCfg;
|
|
28
|
+
log;
|
|
29
|
+
tmpDirPath = "";
|
|
30
|
+
segmentList = [];
|
|
31
|
+
currentSegment = "";
|
|
32
|
+
cdp = null;
|
|
33
|
+
ffmpegProc = null;
|
|
34
|
+
frameTicker = null;
|
|
35
|
+
latestFrame = null;
|
|
36
|
+
segmentFrames = 0;
|
|
37
|
+
segmentFps = 0;
|
|
38
|
+
// A spawn/encode failure on the capture ffmpeg, surfaced at the next end().
|
|
39
|
+
captureError = null;
|
|
40
|
+
// Timeline clock (recorded time, ms) summed over finalised segments.
|
|
41
|
+
completedMs = 0;
|
|
42
|
+
constructor(getCfg, log) {
|
|
43
|
+
this.getCfg = getCfg;
|
|
44
|
+
this.log = log;
|
|
45
|
+
}
|
|
46
|
+
/** True while a segment is actively capturing frames. */
|
|
47
|
+
get capturing() {
|
|
48
|
+
return this.ffmpegProc !== null;
|
|
49
|
+
}
|
|
50
|
+
/** The captured + inserted segments, in timeline order (for stitching). */
|
|
51
|
+
get segments() {
|
|
52
|
+
return this.segmentList;
|
|
53
|
+
}
|
|
54
|
+
/** The temp working directory holding segment files. */
|
|
55
|
+
get tmpDir() {
|
|
56
|
+
return this.tmpDirPath;
|
|
57
|
+
}
|
|
58
|
+
/** Recorded-time position now: finalised segments + the in-flight segment. */
|
|
59
|
+
currentTimelineMs() {
|
|
60
|
+
const fps = this.segmentFps || this.getCfg().fps;
|
|
61
|
+
return timelineMs(this.completedMs, this.segmentFrames, fps, this.capturing);
|
|
62
|
+
}
|
|
63
|
+
/** Create the temp working directory for segment files. Call once before use. */
|
|
64
|
+
init() {
|
|
65
|
+
this.tmpDirPath = mkdtempSync(join(tmpdir(), "recordable-"));
|
|
66
|
+
}
|
|
67
|
+
/** Lazily create the CDP session used for screencast capture. */
|
|
68
|
+
async _ensureCdp(page) {
|
|
69
|
+
if (this.cdp)
|
|
70
|
+
return this.cdp;
|
|
71
|
+
const cdp = await page.createCDPSession();
|
|
72
|
+
cdp.on("Page.screencastFrame", (frame) => {
|
|
73
|
+
this.latestFrame = Buffer.from(frame.data, "base64");
|
|
74
|
+
cdp
|
|
75
|
+
.send("Page.screencastFrameAck", { sessionId: frame.sessionId })
|
|
76
|
+
.catch(() => { });
|
|
77
|
+
});
|
|
78
|
+
this.cdp = cdp;
|
|
79
|
+
return cdp;
|
|
80
|
+
}
|
|
81
|
+
/** Begin capturing into a fresh segment. No-op if already capturing. */
|
|
82
|
+
async begin(page) {
|
|
83
|
+
if (this.ffmpegProc)
|
|
84
|
+
return;
|
|
85
|
+
const cfg = this.getCfg();
|
|
86
|
+
const idx = this.segmentList.length;
|
|
87
|
+
const file = join(this.tmpDirPath, `seg-${String(idx).padStart(3, "0")}.mp4`);
|
|
88
|
+
const { width, height } = cfg.viewport;
|
|
89
|
+
const fps = cfg.fps;
|
|
90
|
+
// Encode a stream of JPEG frames piped on stdin into an MP4 segment.
|
|
91
|
+
const proc = spawn(FFMPEG_PATH, [
|
|
92
|
+
"-y",
|
|
93
|
+
"-f",
|
|
94
|
+
"image2pipe",
|
|
95
|
+
"-framerate",
|
|
96
|
+
String(fps),
|
|
97
|
+
"-i",
|
|
98
|
+
"pipe:0",
|
|
99
|
+
"-r",
|
|
100
|
+
String(fps),
|
|
101
|
+
"-c:v",
|
|
102
|
+
cfg.videoCodec,
|
|
103
|
+
"-preset",
|
|
104
|
+
cfg.videoPreset,
|
|
105
|
+
"-crf",
|
|
106
|
+
String(cfg.videoCrf),
|
|
107
|
+
"-pix_fmt",
|
|
108
|
+
"yuv420p",
|
|
109
|
+
"-vf",
|
|
110
|
+
"pad=ceil(iw/2)*2:ceil(ih/2)*2", // libx264 needs even dimensions
|
|
111
|
+
file,
|
|
112
|
+
], { stdio: ["pipe", "ignore", "ignore"] });
|
|
113
|
+
proc.on("error", (e) => {
|
|
114
|
+
// Encoding is async; remember the failure and surface it at end() rather
|
|
115
|
+
// than silently dropping a frameless, invalid segment.
|
|
116
|
+
this.captureError = e;
|
|
117
|
+
this.log.error(`ffmpeg: ${String(e)}`);
|
|
118
|
+
});
|
|
119
|
+
this.ffmpegProc = proc;
|
|
120
|
+
this.currentSegment = file;
|
|
121
|
+
this.segmentFrames = 0;
|
|
122
|
+
this.segmentFps = fps;
|
|
123
|
+
this.latestFrame = null;
|
|
124
|
+
// Begin the screencast and push the most recent frame at a steady fps so the
|
|
125
|
+
// output is constant-frame-rate even when the page is idle.
|
|
126
|
+
const cdp = await this._ensureCdp(page);
|
|
127
|
+
await cdp.send("Page.startScreencast", {
|
|
128
|
+
format: "jpeg",
|
|
129
|
+
quality: 90,
|
|
130
|
+
maxWidth: width,
|
|
131
|
+
maxHeight: height,
|
|
132
|
+
everyNthFrame: 1,
|
|
133
|
+
});
|
|
134
|
+
this.frameTicker = setInterval(() => {
|
|
135
|
+
const p = this.ffmpegProc;
|
|
136
|
+
if (!p || !p.stdin?.writable || !this.latestFrame)
|
|
137
|
+
return;
|
|
138
|
+
p.stdin.write(this.latestFrame);
|
|
139
|
+
this.segmentFrames++;
|
|
140
|
+
}, Math.max(1, Math.round(1000 / fps)));
|
|
141
|
+
this.log("Record", idx === 0 ? "start" : `resume (segment ${idx + 1})`);
|
|
142
|
+
}
|
|
143
|
+
/** End the active segment, flushing ffmpeg and keeping it only if it has frames.
|
|
144
|
+
* Pass `silent` to skip the "pause" log (used by `insert`, which seals the
|
|
145
|
+
* current segment as an internal step rather than a user-visible pause). */
|
|
146
|
+
async end(silent = false) {
|
|
147
|
+
if (!this.ffmpegProc)
|
|
148
|
+
return;
|
|
149
|
+
if (this.frameTicker) {
|
|
150
|
+
clearInterval(this.frameTicker);
|
|
151
|
+
this.frameTicker = null;
|
|
152
|
+
}
|
|
153
|
+
await this.cdp?.send("Page.stopScreencast").catch(() => { });
|
|
154
|
+
const proc = this.ffmpegProc;
|
|
155
|
+
this.ffmpegProc = null;
|
|
156
|
+
const frames = this.segmentFrames;
|
|
157
|
+
// Flush stdin and wait for ffmpeg to finish writing the file.
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
proc.once("close", () => resolve());
|
|
160
|
+
try {
|
|
161
|
+
proc.stdin?.end();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (this.captureError) {
|
|
168
|
+
const e = this.captureError;
|
|
169
|
+
this.captureError = null;
|
|
170
|
+
throw new RecordableError("FFMPEG_FAILED", `recording capture failed: ${e.message}`, { cause: e });
|
|
171
|
+
}
|
|
172
|
+
// Only keep segments that actually captured frames (avoids empty/invalid mp4).
|
|
173
|
+
if (this.currentSegment && frames > 0) {
|
|
174
|
+
this.segmentList.push({ path: this.currentSegment, fadeIn: 0, fadeOut: 0 });
|
|
175
|
+
this.completedMs +=
|
|
176
|
+
(frames / (this.segmentFps || this.getCfg().fps)) * 1000;
|
|
177
|
+
}
|
|
178
|
+
this.currentSegment = "";
|
|
179
|
+
this.latestFrame = null;
|
|
180
|
+
if (!silent)
|
|
181
|
+
this.log("Record", "pause");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Seal the active segment (silently), normalize the clip to the recording's
|
|
185
|
+
* resolution / fps / codec / pixel format, and append it as the next segment.
|
|
186
|
+
* Fades (ms) are recorded on the segment and applied at stitch time. Doesn't
|
|
187
|
+
* touch recording *intent* — if capture was active the run loop lazily begins a
|
|
188
|
+
* fresh segment before the next action, so recording resumes on its own.
|
|
189
|
+
*/
|
|
190
|
+
async insert(path, options = {}) {
|
|
191
|
+
if (!existsSync(path))
|
|
192
|
+
throw new RecordableError("FILE_NOT_FOUND", `insert: file not found: ${path}`);
|
|
193
|
+
await this.end(true);
|
|
194
|
+
const cfg = this.getCfg();
|
|
195
|
+
const idx = this.segmentList.length;
|
|
196
|
+
const file = join(this.tmpDirPath, `seg-${String(idx).padStart(3, "0")}.mp4`);
|
|
197
|
+
const { width, height } = cfg.viewport;
|
|
198
|
+
// Letterbox-fit to the viewport and conform fps/codec/pixel format so the clip
|
|
199
|
+
// is concat-compatible with the captured segments.
|
|
200
|
+
await runFfmpeg([
|
|
201
|
+
"-y",
|
|
202
|
+
"-i",
|
|
203
|
+
path,
|
|
204
|
+
"-an",
|
|
205
|
+
"-vf",
|
|
206
|
+
`scale=${width}:${height}:force_original_aspect_ratio=decrease,` +
|
|
207
|
+
`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=${cfg.fps}`,
|
|
208
|
+
"-c:v",
|
|
209
|
+
cfg.videoCodec,
|
|
210
|
+
"-preset",
|
|
211
|
+
cfg.videoPreset,
|
|
212
|
+
"-crf",
|
|
213
|
+
String(cfg.videoCrf),
|
|
214
|
+
"-pix_fmt",
|
|
215
|
+
"yuv420p",
|
|
216
|
+
file,
|
|
217
|
+
]);
|
|
218
|
+
this.segmentList.push({
|
|
219
|
+
path: file,
|
|
220
|
+
fadeIn: Math.max(0, options.fadeIn ?? 0) / 1000,
|
|
221
|
+
fadeOut: Math.max(0, options.fadeOut ?? 0) / 1000,
|
|
222
|
+
});
|
|
223
|
+
this.completedMs += (await getDuration(file)) * 1000;
|
|
224
|
+
}
|
|
225
|
+
/** Detach the CDP session. Call after the final segment is sealed. */
|
|
226
|
+
async dispose() {
|
|
227
|
+
if (this.cdp) {
|
|
228
|
+
await this.cdp.detach().catch(() => { });
|
|
229
|
+
this.cdp = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Remove the temp working directory. Call once the output is written. */
|
|
233
|
+
removeTmp() {
|
|
234
|
+
if (this.tmpDirPath)
|
|
235
|
+
rmSync(this.tmpDirPath, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/video/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Logger } from "../logger.js";
|
|
2
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* One piece of the final timeline: a captured stretch or an inserted clip.
|
|
5
|
+
* `fadeIn`/`fadeOut` (seconds) are non-zero only for inserted clips and request
|
|
6
|
+
* a cross-fade with the neighbouring piece (or with black at the timeline ends).
|
|
7
|
+
*/
|
|
8
|
+
export interface Segment {
|
|
9
|
+
path: string;
|
|
10
|
+
fadeIn: number;
|
|
11
|
+
fadeOut: number;
|
|
12
|
+
}
|
|
13
|
+
/** Stitch `segs` into `out`, choosing move / join / cross-fade automatically. */
|
|
14
|
+
export declare function stitch(segs: Segment[], cfg: ResolvedConfig, log: Logger, out: string, tmpDir: string): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=stitch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stitch.d.ts","sourceRoot":"","sources":["../../src/video/stitch.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AASnD;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iFAAiF;AACjF,wBAAsB,MAAM,CAC1B,IAAI,EAAE,OAAO,EAAE,EACf,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CASf"}
|