@bitmagic/asset-core 0.2.24 → 0.2.25-dev.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.
Files changed (89) hide show
  1. package/dist/trailer/analyze.d.ts +24 -0
  2. package/dist/trailer/analyze.d.ts.map +1 -0
  3. package/dist/trailer/analyze.js +186 -0
  4. package/dist/trailer/analyze.js.map +1 -0
  5. package/dist/trailer/auto-edl.d.ts +51 -0
  6. package/dist/trailer/auto-edl.d.ts.map +1 -0
  7. package/dist/trailer/auto-edl.js +266 -0
  8. package/dist/trailer/auto-edl.js.map +1 -0
  9. package/dist/trailer/beat-cut.d.ts +58 -0
  10. package/dist/trailer/beat-cut.d.ts.map +1 -0
  11. package/dist/trailer/beat-cut.js +232 -0
  12. package/dist/trailer/beat-cut.js.map +1 -0
  13. package/dist/trailer/capture.d.ts +102 -0
  14. package/dist/trailer/capture.d.ts.map +1 -0
  15. package/dist/trailer/capture.js +268 -0
  16. package/dist/trailer/capture.js.map +1 -0
  17. package/dist/trailer/edl-time.d.ts +62 -0
  18. package/dist/trailer/edl-time.d.ts.map +1 -0
  19. package/dist/trailer/edl-time.js +105 -0
  20. package/dist/trailer/edl-time.js.map +1 -0
  21. package/dist/trailer/errors.d.ts +15 -0
  22. package/dist/trailer/errors.d.ts.map +1 -0
  23. package/dist/trailer/errors.js +22 -0
  24. package/dist/trailer/errors.js.map +1 -0
  25. package/dist/trailer/ffmpeg.d.ts +19 -0
  26. package/dist/trailer/ffmpeg.d.ts.map +1 -0
  27. package/dist/trailer/ffmpeg.js +100 -0
  28. package/dist/trailer/ffmpeg.js.map +1 -0
  29. package/dist/trailer/hud-state.d.ts +182 -0
  30. package/dist/trailer/hud-state.d.ts.map +1 -0
  31. package/dist/trailer/hud-state.js +523 -0
  32. package/dist/trailer/hud-state.js.map +1 -0
  33. package/dist/trailer/index.d.ts +30 -0
  34. package/dist/trailer/index.d.ts.map +1 -0
  35. package/dist/trailer/index.js +30 -0
  36. package/dist/trailer/index.js.map +1 -0
  37. package/dist/trailer/io.d.ts +23 -0
  38. package/dist/trailer/io.d.ts.map +1 -0
  39. package/dist/trailer/io.js +20 -0
  40. package/dist/trailer/io.js.map +1 -0
  41. package/dist/trailer/link.d.ts +2 -0
  42. package/dist/trailer/link.d.ts.map +1 -0
  43. package/dist/trailer/link.js +15 -0
  44. package/dist/trailer/link.js.map +1 -0
  45. package/dist/trailer/make.d.ts +60 -0
  46. package/dist/trailer/make.d.ts.map +1 -0
  47. package/dist/trailer/make.js +196 -0
  48. package/dist/trailer/make.js.map +1 -0
  49. package/dist/trailer/merge.d.ts +8 -0
  50. package/dist/trailer/merge.d.ts.map +1 -0
  51. package/dist/trailer/merge.js +165 -0
  52. package/dist/trailer/merge.js.map +1 -0
  53. package/dist/trailer/music-beats.d.ts +124 -0
  54. package/dist/trailer/music-beats.d.ts.map +1 -0
  55. package/dist/trailer/music-beats.js +672 -0
  56. package/dist/trailer/music-beats.js.map +1 -0
  57. package/dist/trailer/paths.d.ts +29 -0
  58. package/dist/trailer/paths.d.ts.map +1 -0
  59. package/dist/trailer/paths.js +101 -0
  60. package/dist/trailer/paths.js.map +1 -0
  61. package/dist/trailer/png.d.ts +10 -0
  62. package/dist/trailer/png.d.ts.map +1 -0
  63. package/dist/trailer/png.js +37 -0
  64. package/dist/trailer/png.js.map +1 -0
  65. package/dist/trailer/render.d.ts +93 -0
  66. package/dist/trailer/render.d.ts.map +1 -0
  67. package/dist/trailer/render.js +527 -0
  68. package/dist/trailer/render.js.map +1 -0
  69. package/dist/trailer/revision.d.ts +9 -0
  70. package/dist/trailer/revision.d.ts.map +1 -0
  71. package/dist/trailer/revision.js +31 -0
  72. package/dist/trailer/revision.js.map +1 -0
  73. package/dist/trailer/synth.d.ts +10 -0
  74. package/dist/trailer/synth.d.ts.map +1 -0
  75. package/dist/trailer/synth.js +120 -0
  76. package/dist/trailer/synth.js.map +1 -0
  77. package/dist/trailer/timeline-types.d.ts +196 -0
  78. package/dist/trailer/timeline-types.d.ts.map +1 -0
  79. package/dist/trailer/timeline-types.js +419 -0
  80. package/dist/trailer/timeline-types.js.map +1 -0
  81. package/dist/trailer/titles.d.ts +38 -0
  82. package/dist/trailer/titles.d.ts.map +1 -0
  83. package/dist/trailer/titles.js +110 -0
  84. package/dist/trailer/titles.js.map +1 -0
  85. package/dist/trailer/video-chain.d.ts +130 -0
  86. package/dist/trailer/video-chain.d.ts.map +1 -0
  87. package/dist/trailer/video-chain.js +261 -0
  88. package/dist/trailer/video-chain.js.map +1 -0
  89. package/package.json +5 -1
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Snap a shot list onto a beat grid (or plain seconds) and emit an EDL.
3
+ *
4
+ * A shot says WHICH source frame to start on and HOW LONG to hold, in beats when there is music
5
+ * and in seconds when there is not; this turns that into the frame-exact `edl.json` the renderer
6
+ * consumes. It is the TypeScript port of the make-trailer skill's `beat-edl.py`, so creators need
7
+ * no Python.
8
+ *
9
+ * Each cut's target time comes straight from the beat grid, and the frame count is the difference
10
+ * against the frames ACTUALLY emitted so far — not against the previous target. That matters for
11
+ * speed-ramped clips: at 0.5x the output length is forced to an even number of frames, so a clip
12
+ * whose beat-target is odd lands one frame long. Measuring from actual output makes the next clip
13
+ * absorb that frame instead of every later cut inheriting it.
14
+ *
15
+ * A title cue with `"at": "last-clip"` is resolved to cover the final clip (the end card), so it
16
+ * survives regeneration without hand-editing output times.
17
+ */
18
+ import * as fs from 'fs';
19
+ import { TrailerError } from './errors.js';
20
+ import { warn } from './io.js';
21
+ function isRecord(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+ function finiteNumber(value, ctx) {
25
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
26
+ throw new TrailerError(`${ctx}: expected a number, got ${JSON.stringify(value)}`);
27
+ }
28
+ return value;
29
+ }
30
+ function parseShot(raw, index) {
31
+ const ctx = `shots[${index}]`;
32
+ if (!isRecord(raw))
33
+ throw new TrailerError(`${ctx}: expected an object`);
34
+ const shot = {
35
+ in: finiteNumber(raw.in, `${ctx}.in`),
36
+ label: typeof raw.label === 'string' && raw.label.length > 0 ? raw.label : `shot ${index + 1}`,
37
+ };
38
+ if (shot.in < 0 || !Number.isInteger(shot.in))
39
+ throw new TrailerError(`${ctx}.in: expected a frame index >= 0`);
40
+ if (raw.beats !== undefined) {
41
+ shot.beats = finiteNumber(raw.beats, `${ctx}.beats`);
42
+ if (shot.beats <= 0)
43
+ throw new TrailerError(`${ctx}.beats: must be > 0`);
44
+ }
45
+ if (raw.seconds !== undefined) {
46
+ shot.seconds = finiteNumber(raw.seconds, `${ctx}.seconds`);
47
+ if (shot.seconds <= 0)
48
+ throw new TrailerError(`${ctx}.seconds: must be > 0`);
49
+ }
50
+ if (shot.beats === undefined && shot.seconds === undefined) {
51
+ throw new TrailerError(`${ctx} ("${shot.label}"): needs "beats" (with music) or "seconds"`);
52
+ }
53
+ if (raw.speed !== undefined)
54
+ shot.speed = finiteNumber(raw.speed, `${ctx}.speed`);
55
+ if (raw.ui !== undefined) {
56
+ if (typeof raw.ui !== 'boolean')
57
+ throw new TrailerError(`${ctx}.ui: expected true or false`);
58
+ shot.ui = raw.ui;
59
+ }
60
+ if (raw.transition !== undefined) {
61
+ if (raw.transition !== 'cut' && raw.transition !== 'crossfade') {
62
+ throw new TrailerError(`${ctx}.transition: expected "cut" or "crossfade"`);
63
+ }
64
+ shot.transition = raw.transition;
65
+ }
66
+ if (raw.transitionDuration !== undefined) {
67
+ shot.transitionDuration = finiteNumber(raw.transitionDuration, `${ctx}.transitionDuration`);
68
+ }
69
+ return shot;
70
+ }
71
+ function parseTitle(raw, index) {
72
+ const ctx = `titles[${index}]`;
73
+ if (!isRecord(raw))
74
+ throw new TrailerError(`${ctx}: expected an object`);
75
+ if (typeof raw.text !== 'string')
76
+ throw new TrailerError(`${ctx}.text: expected a string`);
77
+ const title = { text: raw.text, at: 0 };
78
+ if (raw.at === 'last-clip')
79
+ title.at = 'last-clip';
80
+ else
81
+ title.at = finiteNumber(raw.at, `${ctx}.at`);
82
+ if (raw.dur !== undefined)
83
+ title.dur = finiteNumber(raw.dur, `${ctx}.dur`);
84
+ if (typeof raw.sub === 'string')
85
+ title.sub = raw.sub;
86
+ if (typeof raw.note === 'string')
87
+ title.note = raw.note;
88
+ if (raw.style === 'title' || raw.style === 'line' || raw.style === 'card')
89
+ title.style = raw.style;
90
+ return title;
91
+ }
92
+ /** Parse a shots document: either a bare array of shots or `{ version, shots, titles? }`. */
93
+ export function parseShots(raw, source) {
94
+ const shotsRaw = Array.isArray(raw) ? raw : isRecord(raw) ? raw.shots : undefined;
95
+ if (!Array.isArray(shotsRaw))
96
+ throw new TrailerError(`${source}: expected a "shots" array`);
97
+ if (shotsRaw.length === 0)
98
+ throw new TrailerError(`${source}: "shots" is empty — nothing to cut`);
99
+ const file = { version: 1, shots: shotsRaw.map(parseShot) };
100
+ if (isRecord(raw)) {
101
+ if (raw.version !== undefined && raw.version !== 1) {
102
+ throw new TrailerError(`${source}: unsupported version ${JSON.stringify(raw.version)} (this CLI writes version 1)`);
103
+ }
104
+ if (typeof raw.recording === 'string')
105
+ file.recording = raw.recording;
106
+ if (typeof raw.notes === 'string')
107
+ file.notes = raw.notes;
108
+ if (raw.titles !== undefined) {
109
+ if (!Array.isArray(raw.titles))
110
+ throw new TrailerError(`${source}: "titles" must be an array`);
111
+ file.titles = raw.titles.map(parseTitle);
112
+ }
113
+ }
114
+ return file;
115
+ }
116
+ export function loadShots(filePath) {
117
+ if (!fs.existsSync(filePath))
118
+ throw new TrailerError(`Shots file not found: ${filePath}`);
119
+ let raw;
120
+ try {
121
+ raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
122
+ }
123
+ catch (err) {
124
+ throw new TrailerError(`Invalid JSON in ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
125
+ }
126
+ return parseShots(raw, filePath);
127
+ }
128
+ /**
129
+ * Turn shots into frame-exact clips. With `beats` every cut lands on the grid counted from the
130
+ * downbeat; without, each shot's `seconds` decides its length. Returns the EDL plus the numbers
131
+ * the command prints so the author can judge the fit.
132
+ */
133
+ export function cutShots(shots, beats, fps, options = {}) {
134
+ if (!Number.isFinite(fps) || fps <= 0)
135
+ throw new TrailerError(`invalid fps: ${fps}`);
136
+ const grid = beats?.beatTimes ?? null;
137
+ const start = beats?.downbeatIndex ?? 0;
138
+ const t0 = grid !== null ? grid[start] : 0;
139
+ if (grid !== null && (t0 === undefined || grid.length < 2)) {
140
+ throw new TrailerError('The beat grid has too few beats to cut on — check the music file or pass --bpm.');
141
+ }
142
+ const clips = [];
143
+ let cum = start;
144
+ let emitted = 0;
145
+ let worst = 0;
146
+ let truncatedAt;
147
+ for (const shot of shots.shots) {
148
+ const speed = shot.speed ?? 1.0;
149
+ let need;
150
+ let suffix;
151
+ if (grid !== null && shot.beats !== undefined) {
152
+ const next = cum + shot.beats;
153
+ if (next >= grid.length) {
154
+ truncatedAt = shot.label;
155
+ break;
156
+ }
157
+ const target = Math.round((grid[next] - t0) * fps);
158
+ need = target - emitted;
159
+ cum = next;
160
+ suffix = `[${shot.beats}b]`;
161
+ }
162
+ else if (shot.seconds !== undefined) {
163
+ need = Math.round(shot.seconds * fps);
164
+ suffix = `[${shot.seconds}s]`;
165
+ }
166
+ else {
167
+ throw new TrailerError(`Shot "${shot.label}" is timed in beats but there is no beat grid — pass --music (or --beats), or give it "seconds".`);
168
+ }
169
+ const src = Math.max(1, Math.round(need * speed));
170
+ const actual = Math.round(src / speed);
171
+ emitted += actual;
172
+ if (grid !== null && shot.beats !== undefined) {
173
+ const target = Math.round((grid[cum] - t0) * fps);
174
+ worst = Math.max(worst, (Math.abs(emitted - target) / fps) * 1000);
175
+ }
176
+ let inFrame = shot.in;
177
+ if (options.frameCount !== undefined && inFrame + src > options.frameCount) {
178
+ const shifted = Math.max(0, options.frameCount - src);
179
+ warn(`shot "${shot.label}" runs past the recording's last frame — starting it at ${shifted} instead of ${inFrame}`);
180
+ inFrame = shifted;
181
+ }
182
+ const clip = {
183
+ in: inFrame,
184
+ out: inFrame + src,
185
+ speed,
186
+ transition: shot.transition ?? 'cut',
187
+ label: `${shot.label} ${suffix}`,
188
+ };
189
+ if (shot.transitionDuration !== undefined)
190
+ clip.transitionDuration = shot.transitionDuration;
191
+ if (shot.ui !== undefined)
192
+ clip.ui = shot.ui;
193
+ clips.push(clip);
194
+ }
195
+ if (clips.length === 0) {
196
+ throw new TrailerError(`Not a single shot fits the music: "${truncatedAt ?? shots.shots[0].label}" already runs past the end of the track.`);
197
+ }
198
+ const totalSec = emitted / fps;
199
+ const edl = { version: 1, targetDuration: Math.round(totalSec * 100) / 100, clips };
200
+ if (shots.titles !== undefined && shots.titles.length > 0) {
201
+ const last = clips[clips.length - 1];
202
+ const lastDur = Math.round(last.out - last.in) / ((last.speed ?? 1) * fps);
203
+ edl.titles = shots.titles.map((title) => {
204
+ const { at, dur, ...rest } = title;
205
+ if (at === 'last-clip') {
206
+ return { ...rest, at: Math.round((totalSec - lastDur) * 1000) / 1000, dur: Math.round(lastDur * 1000) / 1000 };
207
+ }
208
+ return { ...rest, at, dur: dur ?? 2.5 };
209
+ });
210
+ }
211
+ const result = {
212
+ edl,
213
+ beatsUsed: grid !== null ? cum - start : 0,
214
+ totalSec,
215
+ worstCutErrorMs: worst,
216
+ };
217
+ if (truncatedAt !== undefined)
218
+ result.truncatedAt = truncatedAt;
219
+ return result;
220
+ }
221
+ /** The one-line summary `bitmagic trailer cut` prints, mirroring the Python script's. */
222
+ export function describeCut(result, beats) {
223
+ const parts = [
224
+ beats !== null ? `bpm=${beats.bpm.toFixed(2)}` : 'no beat grid',
225
+ `clips=${result.edl.clips.length}`,
226
+ beats !== null ? `beats=${result.beatsUsed}` : null,
227
+ `total=${result.totalSec.toFixed(3)}s`,
228
+ beats !== null ? `worst cut error=${result.worstCutErrorMs.toFixed(1)} ms` : null,
229
+ ].filter((p) => p !== null);
230
+ return parts.join(' ');
231
+ }
232
+ //# sourceMappingURL=beat-cut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beat-cut.js","sourceRoot":"","sources":["../../src/trailer/beat-cut.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ export type CaptureCodec = 'avc' | 'vp9';
2
+ export declare const CHUNK_HEADER_BYTES = 5;
3
+ export declare const CAPTURE_FPS = 60;
4
+ /** Where ffmpeg drops the extracted PNGs before they are renamed onto the recorder's frame indices. */
5
+ export declare const EXTRACT_DIR = ".extract";
6
+ export declare function isCaptureCodec(value: unknown): value is CaptureCodec;
7
+ /** The elementary stream written during the recording. */
8
+ export declare function elementaryName(codec: CaptureCodec): string;
9
+ /** The playable file kept next to the frames. */
10
+ export declare function containerName(codec: CaptureCodec): string;
11
+ export interface ChunkMessage {
12
+ frameIndex: number;
13
+ key: boolean;
14
+ bytes: Buffer;
15
+ }
16
+ /** Split a binary message into its header and chunk bytes; null when it cannot be one. */
17
+ export declare function parseChunkMessage(data: Buffer): ChunkMessage | null;
18
+ /**
19
+ * Which frame indices arrived. Chrome's encoders emit one chunk per frame in input order, so a
20
+ * gap here is a frame the recorder itself reported as failed; `finalizeFrames` fills it from a
21
+ * neighbour after extraction, the way `FrameSequencer` does for raw frames.
22
+ */
23
+ export declare class ChunkLedger {
24
+ private next;
25
+ readonly present: number[];
26
+ readonly missing: number[];
27
+ /** Whether this chunk is new (a late or duplicate index is ignored). */
28
+ accept(frameIndex: number): boolean;
29
+ /** Mark the tail up to the recorder's declared count as missing. */
30
+ finish(frameCount: number): void;
31
+ get received(): number;
32
+ /** One past the highest index seen (or declared). */
33
+ get expected(): number;
34
+ }
35
+ /** The 32-byte IVF file header (VP9, timebase 1/60). */
36
+ export declare function ivfHeader(width: number, height: number, frameCount: number): Buffer;
37
+ /** Appends chunks to the elementary stream as they arrive; closes with the IVF count patched in. */
38
+ export declare class CaptureWriter {
39
+ readonly codec: CaptureCodec;
40
+ private readonly width;
41
+ private readonly height;
42
+ readonly filePath: string;
43
+ private readonly fd;
44
+ private frames;
45
+ private closed;
46
+ constructor(codec: CaptureCodec, dir: string, width: number, height: number);
47
+ append(frameIndex: number, bytes: Buffer): void;
48
+ close(): void;
49
+ }
50
+ /**
51
+ * `-c copy` into a container. The raw H.264 demuxer would otherwise take its timing from the
52
+ * encoder's VUI (Chrome's software encoder writes a 120 Hz tick), so the frame rate is forced on
53
+ * the input and the timestamps regenerated.
54
+ */
55
+ export declare function remuxArgs(codec: CaptureCodec, dir: string): string[];
56
+ /**
57
+ * One PNG per decoded frame, in decode order, into `.extract/`. `-vsync 0` (the spelling every
58
+ * ffmpeg since 4.x accepts) stops ffmpeg from duplicating or dropping frames to hit a rate.
59
+ */
60
+ export declare function extractArgs(codec: CaptureCodec, dir: string): string[];
61
+ /**
62
+ * Rename the extracted PNGs onto the frame indices the ledger says they belong to, then fill
63
+ * every hole up to the declared count by copying the previous frame (or the first one, for a
64
+ * leading gap) so `<name>_%06d.png` stays aligned with the recorder's frame clock. Returns the
65
+ * same `{written, missing}` the raw path reports.
66
+ */
67
+ export declare function finalizeFrames(dir: string, recordingName: string, ledger: ChunkLedger, frameCount: number): {
68
+ written: number;
69
+ missing: number[];
70
+ };
71
+ /**
72
+ * Mirrors game-play-agent's `isSafePathSegment`, additionally refusing the two dot segments its
73
+ * regex technically admits — `..` would resolve the recording directory to its parent. The
74
+ * recorder only ever sends `gameplay_recording_<timestamp>`, so nothing real is refused.
75
+ */
76
+ export declare function isSafeRecordingName(value: string): boolean;
77
+ /** Run ffmpeg to completion; the error text on failure, null on success. Never throws. */
78
+ export declare function runCaptureTool(binary: string, args: string[]): Promise<string | null>;
79
+ export interface FinishCaptureOptions {
80
+ dir: string;
81
+ recordingName: string;
82
+ writer: CaptureWriter;
83
+ ledger: ChunkLedger;
84
+ /** The recorder's declared frame count; the ledger's high-water mark when it never said. */
85
+ frameCount: number | null;
86
+ /** An error the session already hit (a failed append); skips the ffmpeg steps. */
87
+ error?: string | null;
88
+ ffmpegBinary?: string;
89
+ /** `muxing` once, then `extracting` with the PNG count every two seconds. */
90
+ onProgress?: (phase: 'muxing' | 'extracting', frames: number) => void;
91
+ }
92
+ /**
93
+ * After stop: close the elementary stream, remux it into `capture.mp4`/`.webm`, extract one PNG per
94
+ * frame and put them on the recorder's frame indices (`finalizeFrames`). Resolves with what a
95
+ * `stopped` reply carries; an ffmpeg failure is reported in `error`, never thrown.
96
+ */
97
+ export declare function finishCapture(options: FinishCaptureOptions): Promise<{
98
+ written: number;
99
+ missing: number[];
100
+ error: string | null;
101
+ }>;
102
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/trailer/capture.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AACzC,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,uGAAuG;AACvG,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAE1D;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAGnE;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAK;IACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAM;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAM;IAEhC,wEAAwE;IACxE,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAWnC,oEAAoE;IACpE,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOhC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,qDAAqD;IACrD,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF;AAED,wDAAwD;AACxD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAanF;AASD,oGAAoG;AACpG,qBAAa,aAAa;IAMZ,QAAQ,CAAC,KAAK,EAAE,YAAY;IAAe,OAAO,CAAC,QAAQ,CAAC,KAAK;IAAU,OAAO,CAAC,QAAQ,CAAC,MAAM;IAL9G,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAS;gBAEF,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAmB,KAAK,EAAE,MAAM,EAAmB,MAAM,EAAE,MAAM;IAMtH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAY/C,KAAK,IAAI,IAAI;CAMd;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CASpE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAQtE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAkCxC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBrF;AAUD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,4FAA4F;IAC5F,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,YAAY,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACvE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAqBxI"}
@@ -0,0 +1,268 @@
1
+ /**
2
+ * The video side of the recording stream: the file a sink appends encoded chunks to during a
3
+ * recording, and the ffmpeg steps that turn it into `capture.mp4` and the PNG frames after stop.
4
+ * Two sinks run it: the CLI's `bitmagic dev` WebSocket (cli/src/editor/recording-stream.ts) and
5
+ * game-play-agent's chunked-HTTP sink for the web Creator.
6
+ *
7
+ * The recorder's worker encodes every frame with WebCodecs (see game/src/debug/RecordStream.ts)
8
+ * and sends `[frameIndex u32 LE][keyframe u8][chunk bytes]`. H.264 arrives as Annex B — SPS/PPS
9
+ * inline on every keyframe — so appending the chunks gives a valid `.h264` elementary stream; VP9
10
+ * has no such framing, so its chunks go into an IVF file (a 32-byte header and a 12-byte header
11
+ * per frame). Both remux with `-c copy` and both extract the same `<name>_%06d.png` sequence the
12
+ * trailer pipeline reads, numbered from 0 like every other reader in `@bitmagic/asset-core/trailer`.
13
+ *
14
+ * Pure helpers, testable without a socket or ffmpeg, plus `finishCapture`, which runs them.
15
+ */
16
+ import { spawn } from 'child_process';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { frameFileName } from './timeline-types.js';
20
+ export const CHUNK_HEADER_BYTES = 5;
21
+ export const CAPTURE_FPS = 60;
22
+ /** Where ffmpeg drops the extracted PNGs before they are renamed onto the recorder's frame indices. */
23
+ export const EXTRACT_DIR = '.extract';
24
+ export function isCaptureCodec(value) {
25
+ return value === 'avc' || value === 'vp9';
26
+ }
27
+ /** The elementary stream written during the recording. */
28
+ export function elementaryName(codec) {
29
+ return codec === 'avc' ? 'capture.h264' : 'capture.ivf';
30
+ }
31
+ /** The playable file kept next to the frames. */
32
+ export function containerName(codec) {
33
+ return codec === 'avc' ? 'capture.mp4' : 'capture.webm';
34
+ }
35
+ /** Split a binary message into its header and chunk bytes; null when it cannot be one. */
36
+ export function parseChunkMessage(data) {
37
+ if (data.length <= CHUNK_HEADER_BYTES)
38
+ return null;
39
+ return { frameIndex: data.readUInt32LE(0), key: data[4] === 1, bytes: data.subarray(CHUNK_HEADER_BYTES) };
40
+ }
41
+ /**
42
+ * Which frame indices arrived. Chrome's encoders emit one chunk per frame in input order, so a
43
+ * gap here is a frame the recorder itself reported as failed; `finalizeFrames` fills it from a
44
+ * neighbour after extraction, the way `FrameSequencer` does for raw frames.
45
+ */
46
+ export class ChunkLedger {
47
+ constructor() {
48
+ this.next = 0;
49
+ this.present = [];
50
+ this.missing = [];
51
+ }
52
+ /** Whether this chunk is new (a late or duplicate index is ignored). */
53
+ accept(frameIndex) {
54
+ if (frameIndex < this.next)
55
+ return false;
56
+ while (this.next < frameIndex) {
57
+ this.missing.push(this.next);
58
+ this.next++;
59
+ }
60
+ this.present.push(frameIndex);
61
+ this.next++;
62
+ return true;
63
+ }
64
+ /** Mark the tail up to the recorder's declared count as missing. */
65
+ finish(frameCount) {
66
+ while (this.next < frameCount) {
67
+ this.missing.push(this.next);
68
+ this.next++;
69
+ }
70
+ }
71
+ get received() {
72
+ return this.present.length;
73
+ }
74
+ /** One past the highest index seen (or declared). */
75
+ get expected() {
76
+ return this.next;
77
+ }
78
+ }
79
+ /** The 32-byte IVF file header (VP9, timebase 1/60). */
80
+ export function ivfHeader(width, height, frameCount) {
81
+ const header = Buffer.alloc(32);
82
+ header.write('DKIF', 0, 'ascii');
83
+ header.writeUInt16LE(0, 4);
84
+ header.writeUInt16LE(32, 6);
85
+ header.write('VP90', 8, 'ascii');
86
+ header.writeUInt16LE(width, 12);
87
+ header.writeUInt16LE(height, 14);
88
+ header.writeUInt32LE(CAPTURE_FPS, 16);
89
+ header.writeUInt32LE(1, 20);
90
+ header.writeUInt32LE(frameCount, 24);
91
+ header.writeUInt32LE(0, 28);
92
+ return header;
93
+ }
94
+ function writeAll(fd, bytes, position) {
95
+ let offset = 0;
96
+ while (offset < bytes.length) {
97
+ offset += fs.writeSync(fd, bytes, offset, bytes.length - offset, position === undefined ? null : position + offset);
98
+ }
99
+ }
100
+ /** Appends chunks to the elementary stream as they arrive; closes with the IVF count patched in. */
101
+ export class CaptureWriter {
102
+ constructor(codec, dir, width, height) {
103
+ this.codec = codec;
104
+ this.width = width;
105
+ this.height = height;
106
+ this.frames = 0;
107
+ this.closed = false;
108
+ this.filePath = path.join(dir, elementaryName(codec));
109
+ this.fd = fs.openSync(this.filePath, 'w');
110
+ if (codec === 'vp9')
111
+ writeAll(this.fd, ivfHeader(width, height, 0));
112
+ }
113
+ append(frameIndex, bytes) {
114
+ if (this.closed)
115
+ return;
116
+ if (this.codec === 'vp9') {
117
+ const frameHeader = Buffer.alloc(12);
118
+ frameHeader.writeUInt32LE(bytes.length, 0);
119
+ frameHeader.writeBigUInt64LE(BigInt(frameIndex), 4);
120
+ writeAll(this.fd, frameHeader);
121
+ }
122
+ writeAll(this.fd, bytes);
123
+ this.frames++;
124
+ }
125
+ close() {
126
+ if (this.closed)
127
+ return;
128
+ this.closed = true;
129
+ if (this.codec === 'vp9')
130
+ writeAll(this.fd, ivfHeader(this.width, this.height, this.frames), 0);
131
+ fs.closeSync(this.fd);
132
+ }
133
+ }
134
+ /**
135
+ * `-c copy` into a container. The raw H.264 demuxer would otherwise take its timing from the
136
+ * encoder's VUI (Chrome's software encoder writes a 120 Hz tick), so the frame rate is forced on
137
+ * the input and the timestamps regenerated.
138
+ */
139
+ export function remuxArgs(codec, dir) {
140
+ return [
141
+ '-v', 'error', '-y',
142
+ '-fflags', '+genpts', '-r', String(CAPTURE_FPS),
143
+ '-i', path.join(dir, elementaryName(codec)),
144
+ '-c', 'copy',
145
+ ...(codec === 'avc' ? ['-movflags', '+faststart'] : []),
146
+ path.join(dir, containerName(codec)),
147
+ ];
148
+ }
149
+ /**
150
+ * One PNG per decoded frame, in decode order, into `.extract/`. `-vsync 0` (the spelling every
151
+ * ffmpeg since 4.x accepts) stops ffmpeg from duplicating or dropping frames to hit a rate.
152
+ */
153
+ export function extractArgs(codec, dir) {
154
+ return [
155
+ '-v', 'error', '-y',
156
+ '-i', path.join(dir, containerName(codec)),
157
+ '-vsync', '0', '-start_number', '0',
158
+ '-pix_fmt', 'rgb24', '-c:v', 'png', '-compression_level', '1',
159
+ path.join(dir, EXTRACT_DIR, 'f_%06d.png'),
160
+ ];
161
+ }
162
+ /**
163
+ * Rename the extracted PNGs onto the frame indices the ledger says they belong to, then fill
164
+ * every hole up to the declared count by copying the previous frame (or the first one, for a
165
+ * leading gap) so `<name>_%06d.png` stays aligned with the recorder's frame clock. Returns the
166
+ * same `{written, missing}` the raw path reports.
167
+ */
168
+ export function finalizeFrames(dir, recordingName, ledger, frameCount) {
169
+ const extractDir = path.join(dir, EXTRACT_DIR);
170
+ const files = fs.existsSync(extractDir)
171
+ ? fs.readdirSync(extractDir).filter((name) => /^f_\d{6}\.png$/.test(name)).sort()
172
+ : [];
173
+ const missing = new Set(ledger.missing);
174
+ const have = new Map();
175
+ ledger.present.forEach((index, i) => {
176
+ if (i < files.length) {
177
+ const dest = path.join(dir, frameFileName(recordingName, index));
178
+ fs.renameSync(path.join(extractDir, files[i]), dest);
179
+ have.set(index, dest);
180
+ }
181
+ else {
182
+ missing.add(index);
183
+ }
184
+ });
185
+ const total = Math.max(frameCount, ledger.expected);
186
+ const first = [...have.keys()].sort((a, b) => a - b)[0];
187
+ let previous = first === undefined ? null : have.get(first);
188
+ for (let index = 0; index < total; index++) {
189
+ const own = have.get(index);
190
+ if (own !== undefined) {
191
+ previous = own;
192
+ continue;
193
+ }
194
+ missing.add(index);
195
+ if (previous !== null) {
196
+ const dest = path.join(dir, frameFileName(recordingName, index));
197
+ fs.copyFileSync(previous, dest);
198
+ previous = dest;
199
+ }
200
+ }
201
+ fs.rmSync(extractDir, { recursive: true, force: true });
202
+ return { written: total, missing: [...missing].sort((a, b) => a - b) };
203
+ }
204
+ /**
205
+ * Mirrors game-play-agent's `isSafePathSegment`, additionally refusing the two dot segments its
206
+ * regex technically admits — `..` would resolve the recording directory to its parent. The
207
+ * recorder only ever sends `gameplay_recording_<timestamp>`, so nothing real is refused.
208
+ */
209
+ export function isSafeRecordingName(value) {
210
+ return /^[A-Za-z0-9_\-.]+$/.test(value) && value !== '.' && value !== '..';
211
+ }
212
+ /** Run ffmpeg to completion; the error text on failure, null on success. Never throws. */
213
+ export function runCaptureTool(binary, args) {
214
+ return new Promise((resolve) => {
215
+ let child;
216
+ try {
217
+ child = spawn(binary, args, { stdio: ['ignore', 'ignore', 'pipe'] });
218
+ }
219
+ catch (error) {
220
+ resolve(`ffmpeg could not start: ${error instanceof Error ? error.message : String(error)}`);
221
+ return;
222
+ }
223
+ let stderr = '';
224
+ child.stderr.on('data', (chunk) => { stderr = (stderr + chunk.toString()).slice(-4000); });
225
+ child.on('error', (error) => resolve(`ffmpeg is not available on this machine (${error.message}) — install ffmpeg to record`));
226
+ child.on('close', (code) => {
227
+ resolve(code === 0 ? null : `ffmpeg exited with code ${code}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`);
228
+ });
229
+ });
230
+ }
231
+ function countPngs(dir) {
232
+ try {
233
+ return fs.readdirSync(dir).filter((name) => name.endsWith('.png')).length;
234
+ }
235
+ catch {
236
+ return 0;
237
+ }
238
+ }
239
+ /**
240
+ * After stop: close the elementary stream, remux it into `capture.mp4`/`.webm`, extract one PNG per
241
+ * frame and put them on the recorder's frame indices (`finalizeFrames`). Resolves with what a
242
+ * `stopped` reply carries; an ffmpeg failure is reported in `error`, never thrown.
243
+ */
244
+ export async function finishCapture(options) {
245
+ const { dir, recordingName, writer, ledger } = options;
246
+ const binary = options.ffmpegBinary ?? 'ffmpeg';
247
+ writer.close();
248
+ const declared = options.frameCount ?? ledger.expected;
249
+ ledger.finish(declared);
250
+ options.onProgress?.('muxing', 0);
251
+ let error = options.error ?? null;
252
+ if (error === null)
253
+ error = await runCaptureTool(binary, remuxArgs(writer.codec, dir));
254
+ if (error === null) {
255
+ fs.rmSync(writer.filePath, { force: true });
256
+ const extractDir = path.join(dir, EXTRACT_DIR);
257
+ fs.mkdirSync(extractDir, { recursive: true });
258
+ const ticker = options.onProgress === undefined
259
+ ? null
260
+ : setInterval(() => options.onProgress?.('extracting', countPngs(extractDir)), 2000);
261
+ error = await runCaptureTool(binary, extractArgs(writer.codec, dir));
262
+ if (ticker !== null)
263
+ clearInterval(ticker);
264
+ }
265
+ const result = finalizeFrames(dir, recordingName, ledger, declared);
266
+ return { ...result, error };
267
+ }
268
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/trailer/capture.ts"],"names":[],"mappings":"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"}