@bitmagic/cli 0.1.61-dev.0 → 0.1.61-dev.2

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 (119) hide show
  1. package/README.md +47 -8
  2. package/assets/fonts/NotoSans-Bold.ttf +0 -0
  3. package/assets/fonts/OFL.txt +94 -0
  4. package/dist/chrome-launch.d.ts +18 -0
  5. package/dist/chrome-launch.js +24 -0
  6. package/dist/chrome-launch.js.map +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +2 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/generate.d.ts +44 -0
  11. package/dist/commands/generate.js +143 -0
  12. package/dist/commands/generate.js.map +1 -1
  13. package/dist/commands/trailer-publish.d.ts +15 -0
  14. package/dist/commands/trailer-publish.js +45 -0
  15. package/dist/commands/trailer-publish.js.map +1 -0
  16. package/dist/commands/trailer.d.ts +3 -0
  17. package/dist/commands/trailer.js +396 -0
  18. package/dist/commands/trailer.js.map +1 -0
  19. package/dist/editor/hud-replay-page.d.ts +7 -0
  20. package/dist/editor/hud-replay-page.js +219 -0
  21. package/dist/editor/hud-replay-page.js.map +1 -0
  22. package/dist/editor/recording-panel.d.ts +3 -1
  23. package/dist/editor/recording-panel.js +6 -2
  24. package/dist/editor/recording-panel.js.map +1 -1
  25. package/dist/editor/recording-sink.d.ts +3 -20
  26. package/dist/editor/recording-sink.js +14 -35
  27. package/dist/editor/recording-sink.js.map +1 -1
  28. package/dist/editor/server.js +15 -18
  29. package/dist/editor/server.js.map +1 -1
  30. package/dist/editor/shell-page.d.ts +0 -8
  31. package/dist/editor/shell-page.js +23 -19
  32. package/dist/editor/shell-page.js.map +1 -1
  33. package/dist/generate/music.d.ts +51 -0
  34. package/dist/generate/music.js +93 -0
  35. package/dist/generate/music.js.map +1 -0
  36. package/dist/generate/stream.d.ts +13 -0
  37. package/dist/generate/stream.js +17 -1
  38. package/dist/generate/stream.js.map +1 -1
  39. package/dist/project/jobs.d.ts +1 -1
  40. package/dist/scaffold/project-files.d.ts +0 -1
  41. package/dist/scaffold/project-files.js +6 -33
  42. package/dist/scaffold/project-files.js.map +1 -1
  43. package/dist/scaffold/project.js +3 -1
  44. package/dist/scaffold/project.js.map +1 -1
  45. package/dist/scaffold/skill-files.d.ts +22 -0
  46. package/dist/scaffold/skill-files.js +198 -0
  47. package/dist/scaffold/skill-files.js.map +1 -0
  48. package/dist/trailer/analyze.d.ts +23 -0
  49. package/dist/trailer/analyze.js +186 -0
  50. package/dist/trailer/analyze.js.map +1 -0
  51. package/dist/trailer/auto-edl.d.ts +46 -0
  52. package/dist/trailer/auto-edl.js +148 -0
  53. package/dist/trailer/auto-edl.js.map +1 -0
  54. package/dist/trailer/beat-cut.d.ts +57 -0
  55. package/dist/trailer/beat-cut.js +232 -0
  56. package/dist/trailer/beat-cut.js.map +1 -0
  57. package/dist/trailer/client.d.ts +52 -0
  58. package/dist/trailer/client.js +133 -0
  59. package/dist/trailer/client.js.map +1 -0
  60. package/dist/trailer/edl-time.d.ts +61 -0
  61. package/dist/trailer/edl-time.js +105 -0
  62. package/dist/trailer/edl-time.js.map +1 -0
  63. package/dist/trailer/ffmpeg.d.ts +8 -0
  64. package/dist/trailer/ffmpeg.js +55 -0
  65. package/dist/trailer/ffmpeg.js.map +1 -0
  66. package/dist/trailer/hud-raster.d.ts +28 -0
  67. package/dist/trailer/hud-raster.js +114 -0
  68. package/dist/trailer/hud-raster.js.map +1 -0
  69. package/dist/trailer/hud-state.d.ts +110 -0
  70. package/dist/trailer/hud-state.js +410 -0
  71. package/dist/trailer/hud-state.js.map +1 -0
  72. package/dist/trailer/io.d.ts +20 -0
  73. package/dist/trailer/io.js +15 -0
  74. package/dist/trailer/io.js.map +1 -0
  75. package/dist/trailer/make.d.ts +54 -0
  76. package/dist/trailer/make.js +205 -0
  77. package/dist/trailer/make.js.map +1 -0
  78. package/dist/trailer/merge.d.ts +7 -0
  79. package/dist/trailer/merge.js +144 -0
  80. package/dist/trailer/merge.js.map +1 -0
  81. package/dist/trailer/music-beats.d.ts +123 -0
  82. package/dist/trailer/music-beats.js +707 -0
  83. package/dist/trailer/music-beats.js.map +1 -0
  84. package/dist/trailer/paths.d.ts +28 -0
  85. package/dist/trailer/paths.js +101 -0
  86. package/dist/trailer/paths.js.map +1 -0
  87. package/dist/trailer/publish-record.d.ts +17 -0
  88. package/dist/trailer/publish-record.js +59 -0
  89. package/dist/trailer/publish-record.js.map +1 -0
  90. package/dist/trailer/publish.d.ts +45 -0
  91. package/dist/trailer/publish.js +149 -0
  92. package/dist/trailer/publish.js.map +1 -0
  93. package/dist/trailer/record-session.d.ts +72 -0
  94. package/dist/trailer/record-session.js +331 -0
  95. package/dist/trailer/record-session.js.map +1 -0
  96. package/dist/trailer/render.d.ts +57 -0
  97. package/dist/trailer/render.js +475 -0
  98. package/dist/trailer/render.js.map +1 -0
  99. package/dist/trailer/revision.d.ts +8 -0
  100. package/dist/trailer/revision.js +31 -0
  101. package/dist/trailer/revision.js.map +1 -0
  102. package/dist/trailer/synth.d.ts +9 -0
  103. package/dist/trailer/synth.js +120 -0
  104. package/dist/trailer/synth.js.map +1 -0
  105. package/dist/trailer/timeline-types.d.ts +176 -0
  106. package/dist/trailer/timeline-types.js +379 -0
  107. package/dist/trailer/timeline-types.js.map +1 -0
  108. package/dist/trailer/title-raster.d.ts +6 -0
  109. package/dist/trailer/title-raster.js +47 -0
  110. package/dist/trailer/title-raster.js.map +1 -0
  111. package/dist/trailer/titles.d.ts +44 -0
  112. package/dist/trailer/titles.js +119 -0
  113. package/dist/trailer/titles.js.map +1 -0
  114. package/dist/trailer/video-chain.d.ts +41 -0
  115. package/dist/trailer/video-chain.js +78 -0
  116. package/dist/trailer/video-chain.js.map +1 -0
  117. package/dist/verify/browser.js +12 -19
  118. package/dist/verify/browser.js.map +1 -1
  119. package/package.json +5 -4
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The two api-server calls behind `bitmagic trailer publish`: mint a signed PUT for the video,
3
+ * and attach the uploaded URL to the game page's gallery.
4
+ *
5
+ * Status mapping is this module's own, for the reason `http/signed-upload.ts` gives: `client.ts`'s
6
+ * `request()` reads every 403 as a Bitmagic Pro refusal, and on these routes a 403 is more often
7
+ * "not your game" or "this game is banned" — sending that creator to `bitmagic subscribe` would
8
+ * point them somewhere that cannot help. The one status these routes add is 409 `media_full`: the
9
+ * page's gallery is capped at twelve items, and the creator has to make room, or let the CLI swap
10
+ * the previous trailer out (which it does by itself on a re-run, through `replaceUrl`).
11
+ */
12
+ import { CliError } from '../errors.js';
13
+ import { proRefusal } from '../http/client.js';
14
+ import { isJsonObject } from '../project/world-json.js';
15
+ import { apiServerHeaders } from '../telemetry/request-headers.js';
16
+ export const TRAILER_UPLOAD_URL_PATH = '/api/cli/v1/trailer/upload-url';
17
+ export const TRAILER_ATTACH_PATH = '/api/cli/v1/trailer/attach';
18
+ /** The gallery cap api-server enforces (`MAX_MEDIA_ITEMS` in game-page-edit-rules.ts). */
19
+ export const MAX_GALLERY_ITEMS = 12;
20
+ async function readJsonBody(response) {
21
+ try {
22
+ const parsed = await response.json();
23
+ return isJsonObject(parsed) ? parsed : {};
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ async function callApi(environment, path, method, accessToken, body, deps) {
30
+ try {
31
+ return await deps.fetch(`${environment.apiUrl}${path}`, {
32
+ method,
33
+ headers: {
34
+ ...apiServerHeaders(),
35
+ Authorization: `Bearer ${accessToken}`,
36
+ 'Content-Type': 'application/json',
37
+ Accept: 'application/json',
38
+ },
39
+ body: JSON.stringify(body),
40
+ });
41
+ }
42
+ catch {
43
+ throw new CliError(`Cannot reach api-server at ${environment.apiUrl}.`, 1, 'unreachable');
44
+ }
45
+ }
46
+ /**
47
+ * The refusal an error status stands for, shared by both calls so they cannot drift on what a
48
+ * 403 or a 404 means. `what` names the step for the 400 and generic messages.
49
+ */
50
+ async function refusal(response, environment, path, gameId, what) {
51
+ if (response.status === 401) {
52
+ return new CliError('Your session has expired. Run `bitmagic login`.', 1, 'session_expired');
53
+ }
54
+ if (response.status === 403) {
55
+ // Three refusals share the status: the Pro gate on every /api/cli/v1 route (which carries a
56
+ // `code`), the route's own ownership check, and a banned game.
57
+ const clone = response.clone();
58
+ const parsed = await readJsonBody(response);
59
+ if (parsed.code === 'pro_expired' || parsed.code === 'pro_required') {
60
+ const pro = await proRefusal(clone);
61
+ return new CliError(pro.message, 1, pro.code);
62
+ }
63
+ if (parsed.error === 'Game is banned') {
64
+ const reason = typeof parsed.banReason === 'string' && parsed.banReason !== '' ? ` (${parsed.banReason})` : '';
65
+ return new CliError(`Game "${gameId}" is banned${reason}, so its page cannot be changed until an admin lifts the ban.`, 1, 'banned');
66
+ }
67
+ return new CliError(`You do not own game "${gameId}", so ${what} is not allowed. Check the gameId in bitmagic.json.`, 1, 'not_owner');
68
+ }
69
+ if (response.status === 404) {
70
+ return new CliError(`Game "${gameId}" does not exist in the "${environment.name}" environment. Check the gameId in `
71
+ + 'bitmagic.json, or run `bitmagic init` to create a game there.', 1, 'not_found');
72
+ }
73
+ if (response.status === 400) {
74
+ const parsed = await readJsonBody(response);
75
+ const message = typeof parsed.error === 'string' ? parsed.error : `api-server rejected ${what}.`;
76
+ return new CliError(`api-server refused ${what}: ${message}`);
77
+ }
78
+ if (response.status === 503) {
79
+ return new CliError(`api-server reported media uploads are unavailable in the "${environment.name}" environment.`, 1, 'unavailable');
80
+ }
81
+ return new CliError(`api-server returned HTTP ${response.status} for ${path}.`, 1, 'http_error');
82
+ }
83
+ /**
84
+ * Ask api-server for a signed PUT under `game-media/<gameId>/`. Ownership and the video-only rule
85
+ * are checked there before anything is minted, so a refusal here has cost nothing.
86
+ */
87
+ export async function mintTrailerUploadUrl(environment, accessToken, body, deps) {
88
+ const response = await callApi(environment, TRAILER_UPLOAD_URL_PATH, 'POST', accessToken, body, deps);
89
+ if (!response.ok) {
90
+ throw await refusal(response, environment, TRAILER_UPLOAD_URL_PATH, body.gameId, 'the trailer upload');
91
+ }
92
+ const parsed = await readJsonBody(response);
93
+ if (typeof parsed.uploadUrl !== 'string' || typeof parsed.publicUrl !== 'string') {
94
+ throw new CliError('api-server minted an upload URL this CLI cannot read — is it newer than this CLI?');
95
+ }
96
+ return {
97
+ uploadUrl: parsed.uploadUrl,
98
+ publicUrl: parsed.publicUrl,
99
+ path: typeof parsed.path === 'string' ? parsed.path : '',
100
+ maxBytes: typeof parsed.maxBytes === 'number' ? parsed.maxBytes : 0,
101
+ // The server echoes it; fall back to what was asked for against an older api-server.
102
+ contentType: typeof parsed.contentType === 'string' ? parsed.contentType : body.contentType,
103
+ };
104
+ }
105
+ /**
106
+ * Put the uploaded video at the front of the game page's gallery. api-server drops any item already
107
+ * pointing at `url` or at `replaceUrl`, so re-running replaces the previous trailer rather than
108
+ * stacking a new one beside it.
109
+ */
110
+ export async function attachTrailer(environment, accessToken, body, deps, hints = {}) {
111
+ const response = await callApi(environment, TRAILER_ATTACH_PATH, 'PUT', accessToken, body, deps);
112
+ if (response.status === 409) {
113
+ const parsed = await readJsonBody(response);
114
+ const max = typeof parsed.max === 'number' ? parsed.max : MAX_GALLERY_ITEMS;
115
+ const where = hints.manageUrl ? `on ${hints.manageUrl}` : 'on the My games page';
116
+ throw new CliError(`The game page already holds ${max} media items — remove one ${where} or re-run to replace the `
117
+ + 'previous trailer.', 1, 'media_full');
118
+ }
119
+ if (!response.ok) {
120
+ throw await refusal(response, environment, TRAILER_ATTACH_PATH, body.gameId, 'attaching the trailer');
121
+ }
122
+ const parsed = await readJsonBody(response);
123
+ if (typeof parsed.pageUrl !== 'string') {
124
+ throw new CliError('api-server attached the trailer but answered in a shape this CLI cannot read — is it newer than this CLI?');
125
+ }
126
+ return {
127
+ media: Array.isArray(parsed.media) ? parsed.media : [],
128
+ pageUrl: parsed.pageUrl,
129
+ isPublic: parsed.isPublic === true,
130
+ published: parsed.published === true,
131
+ };
132
+ }
133
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/trailer/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pure EDL time mapping: source frames -> output-timeline seconds.
3
+ *
4
+ * Crossfades OVERLAP the previous clip, so every clip after a crossfade
5
+ * starts (and ends) earlier on the output timeline than a cut-only chain
6
+ * would place it. All render math (xfade offsets, sound delays, total
7
+ * duration) derives from this module so it can be unit-tested without ffmpeg.
8
+ */
9
+ import type { EdlClip, TimelineSound } from './timeline-types.js';
10
+ export declare const DEFAULT_CROSSFADE_SEC = 0.25;
11
+ export declare const DEFAULT_TAIL_PAD_FRAMES = 30;
12
+ export declare const MIN_SPEED = 0.25;
13
+ export declare const MAX_SPEED = 2;
14
+ export interface ClipTiming {
15
+ index: number;
16
+ in: number;
17
+ out: number;
18
+ speed: number;
19
+ /** Resolved transition INTO this clip; first clip is always 'cut'. */
20
+ transition: 'cut' | 'crossfade';
21
+ /** Seconds; 0 for cuts. Also the xfade offset shortfall vs a plain concat. */
22
+ transitionDuration: number;
23
+ /** Seconds on the output timeline where this clip's first frame lands. */
24
+ outputStart: number;
25
+ /** Seconds this clip occupies on the output timeline (source frames / fps / speed). */
26
+ outputDuration: number;
27
+ label?: string;
28
+ /** Per-clip UI-overlay override; undefined means "follow the global --ui flag". */
29
+ ui?: boolean;
30
+ }
31
+ export interface ClipTimelineResult {
32
+ timings: ClipTiming[];
33
+ totalDuration: number;
34
+ warnings: string[];
35
+ }
36
+ export declare function computeClipTimeline(clips: EdlClip[], fps: number): ClipTimelineResult;
37
+ /**
38
+ * Output-timeline seconds where a source frame inside `timing` lands. Frames
39
+ * before the clip's first frame (sound tail pad) clamp to the clip start.
40
+ */
41
+ export declare function sourceFrameToOutputSec(timing: ClipTiming, sourceFrame: number, fps: number): number;
42
+ /**
43
+ * Output time at which a clip stops being the only source on screen. A
44
+ * crossfade INTO the next clip begins at that clip's outputStart — before this
45
+ * clip's last frame — so anything that must not double up (UI overlays) hands
46
+ * over there rather than at this clip's own end.
47
+ */
48
+ export declare function exclusiveOutputEnd(timings: readonly ClipTiming[], index: number): number;
49
+ export interface PlacedSound {
50
+ sound: TimelineSound;
51
+ clipIndex: number;
52
+ /** Seconds on the output timeline where this sound instance starts. */
53
+ outputTimeSec: number;
54
+ }
55
+ /**
56
+ * Place timeline sounds onto the output timeline. A clip picks up sounds with
57
+ * frame in [in - tailPadFrames, out); sounds from the tail pad (before the
58
+ * clip's first frame) clamp to the clip's output start. The same sound can be
59
+ * placed once per clip whose source range covers it.
60
+ */
61
+ export declare function placeSounds(timings: ClipTiming[], sounds: TimelineSound[], fps: number, tailPadFrames?: number): PlacedSound[];
@@ -0,0 +1,105 @@
1
+ import { CliError } from '../errors.js';
2
+ export const DEFAULT_CROSSFADE_SEC = 0.25;
3
+ export const DEFAULT_TAIL_PAD_FRAMES = 30;
4
+ export const MIN_SPEED = 0.25;
5
+ export const MAX_SPEED = 2.0;
6
+ export function computeClipTimeline(clips, fps) {
7
+ if (clips.length === 0)
8
+ throw new CliError('EDL has no clips');
9
+ if (!Number.isFinite(fps) || fps <= 0)
10
+ throw new Error(`invalid fps: ${fps}`);
11
+ const warnings = [];
12
+ const timings = [];
13
+ let cursor = 0; // output-timeline end of the previous clip
14
+ clips.forEach((clip, index) => {
15
+ if (!Number.isInteger(clip.in) || !Number.isInteger(clip.out) || clip.in < 0 || clip.out <= clip.in) {
16
+ throw new Error(`clip ${index}: invalid frame range in=${clip.in} out=${clip.out}`);
17
+ }
18
+ let speed = clip.speed ?? 1;
19
+ if (speed < MIN_SPEED || speed > MAX_SPEED) {
20
+ const clamped = Math.min(MAX_SPEED, Math.max(MIN_SPEED, speed));
21
+ warnings.push(`clip ${index}: speed ${speed} outside [${MIN_SPEED}, ${MAX_SPEED}], clamped to ${clamped}`);
22
+ speed = clamped;
23
+ }
24
+ const outputDuration = (clip.out - clip.in) / fps / speed;
25
+ let transition = 'cut';
26
+ let transitionDuration = 0;
27
+ if (index === 0) {
28
+ if (clip.transition === 'crossfade')
29
+ warnings.push('clip 0: transition on the first clip is ignored');
30
+ }
31
+ else if (clip.transition === 'crossfade') {
32
+ transition = 'crossfade';
33
+ transitionDuration = clip.transitionDuration ?? DEFAULT_CROSSFADE_SEC;
34
+ if (transitionDuration <= 0) {
35
+ warnings.push(`clip ${index}: non-positive transitionDuration ${transitionDuration}, treating as cut`);
36
+ transition = 'cut';
37
+ transitionDuration = 0;
38
+ }
39
+ else {
40
+ const maxFade = Math.min(timings[index - 1].outputDuration, outputDuration);
41
+ if (transitionDuration > maxFade) {
42
+ warnings.push(`clip ${index}: transitionDuration ${transitionDuration} longer than an adjacent clip, clamped to ${maxFade}`);
43
+ transitionDuration = maxFade;
44
+ }
45
+ }
46
+ }
47
+ const outputStart = index === 0 ? 0 : cursor - transitionDuration;
48
+ const timing = {
49
+ index,
50
+ in: clip.in,
51
+ out: clip.out,
52
+ speed,
53
+ transition,
54
+ transitionDuration,
55
+ outputStart,
56
+ outputDuration
57
+ };
58
+ if (clip.label !== undefined)
59
+ timing.label = clip.label;
60
+ if (clip.ui !== undefined)
61
+ timing.ui = clip.ui;
62
+ timings.push(timing);
63
+ cursor = outputStart + outputDuration;
64
+ });
65
+ return { timings, totalDuration: cursor, warnings };
66
+ }
67
+ /**
68
+ * Output-timeline seconds where a source frame inside `timing` lands. Frames
69
+ * before the clip's first frame (sound tail pad) clamp to the clip start.
70
+ */
71
+ export function sourceFrameToOutputSec(timing, sourceFrame, fps) {
72
+ return timing.outputStart + Math.max(0, sourceFrame - timing.in) / fps / timing.speed;
73
+ }
74
+ /**
75
+ * Output time at which a clip stops being the only source on screen. A
76
+ * crossfade INTO the next clip begins at that clip's outputStart — before this
77
+ * clip's last frame — so anything that must not double up (UI overlays) hands
78
+ * over there rather than at this clip's own end.
79
+ */
80
+ export function exclusiveOutputEnd(timings, index) {
81
+ const timing = timings[index];
82
+ const ownEnd = timing.outputStart + timing.outputDuration;
83
+ const next = timings[index + 1];
84
+ return next === undefined ? ownEnd : Math.min(ownEnd, next.outputStart);
85
+ }
86
+ /**
87
+ * Place timeline sounds onto the output timeline. A clip picks up sounds with
88
+ * frame in [in - tailPadFrames, out); sounds from the tail pad (before the
89
+ * clip's first frame) clamp to the clip's output start. The same sound can be
90
+ * placed once per clip whose source range covers it.
91
+ */
92
+ export function placeSounds(timings, sounds, fps, tailPadFrames = DEFAULT_TAIL_PAD_FRAMES) {
93
+ const placements = [];
94
+ for (const timing of timings) {
95
+ for (const sound of sounds) {
96
+ if (sound.frame < timing.in - tailPadFrames || sound.frame >= timing.out)
97
+ continue;
98
+ const outputTimeSec = sourceFrameToOutputSec(timing, sound.frame, fps);
99
+ placements.push({ sound, clipIndex: timing.index, outputTimeSec });
100
+ }
101
+ }
102
+ placements.sort((a, b) => a.outputTimeSec - b.outputTimeSec);
103
+ return placements;
104
+ }
105
+ //# sourceMappingURL=edl-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edl-time.js","sourceRoot":"","sources":["../../src/trailer/edl-time.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type FfmpegTool = 'ffmpeg' | 'ffprobe';
2
+ export declare function missingFfmpegMessage(tool?: FfmpegTool): string;
3
+ /**
4
+ * Refuse up front when a tool is missing, before any frames are read or sparks spent. `spawnSync`
5
+ * with `-version` is the cheapest probe that also proves the binary actually runs.
6
+ */
7
+ export declare function ensureFfmpeg(tools?: readonly FfmpegTool[]): void;
8
+ export declare function runFfmpeg(args: string[]): Promise<void>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Thin ffmpeg process wrapper. Always argument arrays, never shell strings.
3
+ *
4
+ * ffmpeg is the one external tool the trailer lane needs and the one a creator's machine is least
5
+ * likely to have, so its absence is reported as a sentence with the install command rather than as
6
+ * an ENOENT stack trace from deep inside a filtergraph.
7
+ */
8
+ import { spawn, spawnSync } from 'child_process';
9
+ import { CliError, errnoCode } from '../errors.js';
10
+ const STDERR_KEEP_BYTES = 65536;
11
+ const STDERR_REPORT_LINES = 30;
12
+ export function missingFfmpegMessage(tool = 'ffmpeg') {
13
+ return (`${tool} was not found on PATH. \`bitmagic trailer\` cuts and encodes with ffmpeg (and ffprobe) — ` +
14
+ 'install it (macOS: brew install ffmpeg · Debian/Ubuntu: sudo apt install ffmpeg · ' +
15
+ 'Windows: winget install Gyan.FFmpeg) and run the command again.');
16
+ }
17
+ /**
18
+ * Refuse up front when a tool is missing, before any frames are read or sparks spent. `spawnSync`
19
+ * with `-version` is the cheapest probe that also proves the binary actually runs.
20
+ */
21
+ export function ensureFfmpeg(tools = ['ffmpeg']) {
22
+ for (const tool of tools) {
23
+ const probe = spawnSync(tool, ['-version'], { stdio: 'ignore' });
24
+ if (probe.error !== undefined && errnoCode(probe.error) === 'ENOENT') {
25
+ throw new CliError(missingFfmpegMessage(tool), 1, 'ffmpeg_missing');
26
+ }
27
+ }
28
+ }
29
+ export async function runFfmpeg(args) {
30
+ return new Promise((resolve, reject) => {
31
+ const child = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
32
+ let stderr = '';
33
+ child.stderr.on('data', (chunk) => {
34
+ stderr += chunk.toString('utf-8');
35
+ if (stderr.length > STDERR_KEEP_BYTES)
36
+ stderr = stderr.slice(-STDERR_KEEP_BYTES / 2);
37
+ });
38
+ child.on('error', (err) => {
39
+ if (errnoCode(err) === 'ENOENT')
40
+ reject(new CliError(missingFfmpegMessage('ffmpeg'), 1, 'ffmpeg_missing'));
41
+ else
42
+ reject(new Error(`failed to start ffmpeg: ${err.message}`));
43
+ });
44
+ child.on('close', (code) => {
45
+ if (code === 0) {
46
+ resolve();
47
+ }
48
+ else {
49
+ const tail = stderr.trim().split('\n').slice(-STDERR_REPORT_LINES).join('\n');
50
+ reject(new CliError(`ffmpeg exited with code ${code}:\n${tail}`));
51
+ }
52
+ });
53
+ });
54
+ }
55
+ //# sourceMappingURL=ffmpeg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/trailer/ffmpeg.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAChC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAI/B,MAAM,UAAU,oBAAoB,CAAC,OAAmB,QAAQ;IAC9D,OAAO,CACL,GAAG,IAAI,4FAA4F;QACnG,oFAAoF;QACpF,iEAAiE,CAClE,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAA+B,CAAC,QAAQ,CAAC;IACpE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QACjE,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC;YACrE,MAAM,IAAI,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAc;IAC5C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAC7E,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClC,IAAI,MAAM,CAAC,MAAM,GAAG,iBAAiB;gBAAE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC,CAAC;QACvF,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,QAAQ;gBAAE,MAAM,CAAC,IAAI,QAAQ,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC;;gBACtG,MAAM,CAAC,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,EAAE,CAAC;YACZ,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9E,MAAM,CAAC,IAAI,QAAQ,CAAC,2BAA2B,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;YACpE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type Browser } from 'playwright-core';
2
+ import type { UiKeyframe } from './hud-state.js';
3
+ export declare const CHROME_MISSING_HINT = "Google Chrome could not be launched \u2014 install it from https://www.google.com/chrome/";
4
+ /** Headless system Chrome for offscreen rasterization; null (with a warning) when it cannot start. */
5
+ export declare function launchRasterBrowser(purpose: string): Promise<Browser | null>;
6
+ export interface RasterizeOptions {
7
+ harnessUrl: string;
8
+ /** Overlay PNGs are written here as ui_NNNN.png, one per keyframe. */
9
+ outDir: string;
10
+ /** Recording resolution — both the browser viewport and the HUD root size. */
11
+ width: number;
12
+ height: number;
13
+ }
14
+ export declare function uiPngPath(outDir: string, index: number): string;
15
+ /**
16
+ * Render one transparent PNG per keyframe, reusing a single browser + page.
17
+ * Returns paths in keyframe order, or null when UI overlays must be skipped
18
+ * (no Chrome, harness unreachable, or the harness rejected a state).
19
+ */
20
+ export declare function rasterizeUiKeyframes(keyframes: readonly UiKeyframe[], options: RasterizeOptions): Promise<string[] | null>;
21
+ /**
22
+ * Width/height straight out of a PNG's IHDR chunk, so the filtergraph only
23
+ * pays for a scale filter when the overlay really is the wrong size.
24
+ */
25
+ export declare function readPngSize(filePath: string): {
26
+ width: number;
27
+ height: number;
28
+ } | null;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Rasterize replay states into transparent PNG overlays with headless Chrome.
3
+ *
4
+ * The harness page (`/hud-replay.html` on the running `bitmagic dev` sidecar) owns a real GameHUD,
5
+ * so the overlay is the game's own UI rather than a reimplementation. Contract:
6
+ * window.__hudReplayReady === true when the page can accept states
7
+ * window.__hudReplay.render(state) synchronous; applies a replay state
8
+ * window.__hudReplay.setViewport(w, h) sizes the HUD root
9
+ * plus a transparent page background, so `omitBackground` yields real alpha.
10
+ *
11
+ * Every failure path here degrades to "no overlays" (returns null) instead of failing the trailer:
12
+ * a UI-less video still beats no video. Chrome is the creator's installed browser
13
+ * (`channel: 'chrome'`), the same one `bitmagic verify` drives — nothing is downloaded.
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { chromium } from 'playwright-core';
18
+ import { out, warn } from './io.js';
19
+ const NAV_TIMEOUT_MS = 20000;
20
+ const READY_TIMEOUT_MS = 20000;
21
+ export const CHROME_MISSING_HINT = 'Google Chrome could not be launched — install it from https://www.google.com/chrome/';
22
+ /**
23
+ * The state is embedded in the expression rather than passed as an argument:
24
+ * Playwright treats a string pageFunction as a plain expression and drops the
25
+ * arg, so an IIFE is the reliable form.
26
+ */
27
+ function applyStateExpression(state, width, height) {
28
+ return ('(() => {' +
29
+ ' const replay = window.__hudReplay;' +
30
+ ' if (!replay || typeof replay.render !== "function") throw new Error("window.__hudReplay.render is missing");' +
31
+ ' if (typeof replay.setViewport === "function") replay.setViewport(' +
32
+ `${JSON.stringify(width)}, ${JSON.stringify(height)});` +
33
+ ` replay.render(${JSON.stringify(state)});` +
34
+ ' return true;' +
35
+ '})()');
36
+ }
37
+ /** Headless system Chrome for offscreen rasterization; null (with a warning) when it cannot start. */
38
+ export async function launchRasterBrowser(purpose) {
39
+ try {
40
+ return await chromium.launch({ headless: true, channel: 'chrome', args: ['--mute-audio'] });
41
+ }
42
+ catch (err) {
43
+ warn(`${purpose}: ${CHROME_MISSING_HINT} (${err instanceof Error ? err.message.split('\n')[0] : String(err)})`);
44
+ return null;
45
+ }
46
+ }
47
+ export function uiPngPath(outDir, index) {
48
+ return path.join(outDir, `ui_${String(index).padStart(4, '0')}.png`);
49
+ }
50
+ /**
51
+ * Render one transparent PNG per keyframe, reusing a single browser + page.
52
+ * Returns paths in keyframe order, or null when UI overlays must be skipped
53
+ * (no Chrome, harness unreachable, or the harness rejected a state).
54
+ */
55
+ export async function rasterizeUiKeyframes(keyframes, options) {
56
+ if (keyframes.length === 0)
57
+ return [];
58
+ const browser = await launchRasterBrowser('ui');
59
+ if (browser === null) {
60
+ warn('ui: rendering without HUD overlays');
61
+ return null;
62
+ }
63
+ fs.mkdirSync(options.outDir, { recursive: true });
64
+ try {
65
+ const context = await browser.newContext({
66
+ viewport: { width: options.width, height: options.height },
67
+ deviceScaleFactor: 1
68
+ });
69
+ const page = await context.newPage();
70
+ await page.goto(options.harnessUrl, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
71
+ await page.waitForFunction('window.__hudReplayReady === true', undefined, { timeout: READY_TIMEOUT_MS });
72
+ const paths = [];
73
+ for (let i = 0; i < keyframes.length; i++) {
74
+ const pngPath = uiPngPath(options.outDir, i);
75
+ await page.evaluate(applyStateExpression(keyframes[i].state, options.width, options.height));
76
+ await page.screenshot({ path: pngPath, omitBackground: true, type: 'png' });
77
+ paths.push(pngPath);
78
+ }
79
+ out(`ui: rasterized ${paths.length} overlay(s) at ${options.width}x${options.height} from ${options.harnessUrl}`);
80
+ return paths;
81
+ }
82
+ catch (err) {
83
+ warn(`ui: overlay rasterization failed against ${options.harnessUrl} ` +
84
+ `(${err instanceof Error ? err.message : String(err)}) — rendering without UI`);
85
+ return null;
86
+ }
87
+ finally {
88
+ try {
89
+ await browser.close();
90
+ }
91
+ catch (err) {
92
+ warn(`ui: browser close failed: ${err instanceof Error ? err.message : String(err)}`);
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Width/height straight out of a PNG's IHDR chunk, so the filtergraph only
98
+ * pays for a scale filter when the overlay really is the wrong size.
99
+ */
100
+ export function readPngSize(filePath) {
101
+ const header = Buffer.alloc(24);
102
+ const fd = fs.openSync(filePath, 'r');
103
+ try {
104
+ if (fs.readSync(fd, header, 0, 24, 0) < 24)
105
+ return null;
106
+ }
107
+ finally {
108
+ fs.closeSync(fd);
109
+ }
110
+ if (header.readUInt32BE(0) !== 0x89504e47 || header.toString('ascii', 12, 16) !== 'IHDR')
111
+ return null;
112
+ return { width: header.readUInt32BE(16), height: header.readUInt32BE(20) };
113
+ }
114
+ //# sourceMappingURL=hud-raster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hud-raster.js","sourceRoot":"","sources":["../../src/trailer/hud-raster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAgB,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAE/B,MAAM,CAAC,MAAM,mBAAmB,GAC9B,sFAAsF,CAAC;AAEzF;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,KAAc,EAAE,KAAa,EAAE,MAAc;IACzE,OAAO,CACL,UAAU;QACV,qCAAqC;QACrC,+GAA+G;QAC/G,oEAAoE;QACpE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI;QACvD,kBAAkB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI;QAC3C,eAAe;QACf,MAAM,CACP,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAe;IACvD,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IAC9F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,OAAO,KAAK,mBAAmB,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChH,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAWD,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,KAAa;IACrD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAgC,EAChC,OAAyB;IAEzB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,oCAAoC,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;YAC1D,iBAAiB,EAAE,CAAC;SACrB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;QAChG,MAAM,IAAI,CAAC,eAAe,CAAC,kCAAkC,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAEzG,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC1C,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAC7C,MAAM,IAAI,CAAC,QAAQ,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAC7F,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5E,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;QACD,GAAG,CAAC,kBAAkB,KAAK,CAAC,MAAM,kBAAkB,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,SAAS,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QAClH,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CACF,4CAA4C,OAAO,CAAC,UAAU,GAAG;YAC/D,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CACjF,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,6BAA6B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE;YAAE,OAAO,IAAI,CAAC;IAC1D,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACtG,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * HUD replay: turn the recorded `hud[]` op track back into UI state.
3
+ *
4
+ * Captured frames are UI-less (the HUD is DOM, not WebGL), so the engine logs
5
+ * semantic HUD *state changes* instead of pixels. This module reduces that
6
+ * sparse op stream back to the complete UI state at any source frame
7
+ * (`reduceHudState`), and picks the frames worth rasterizing per EDL clip
8
+ * (`collectUiKeyframes`). Output-timeline placement comes from edl-time.ts —
9
+ * this file never re-derives crossfade/speed math.
10
+ *
11
+ * Timers are the one derived value: they are logged as start/pause/reset ops,
12
+ * never per tick, and the displayed value is computed from FRAME time (which
13
+ * is more correct than the widget's wall clock under a dilated recording).
14
+ */
15
+ import type { ClipTiming } from './edl-time.js';
16
+ import type { HudElementKind, HudOp } from './timeline-types.js';
17
+ /**
18
+ * Engine-owned chrome that lives in the HUD element registry but must never
19
+ * reach a trailer: interactive controls the viewer can't click in a video.
20
+ * `mute-button` is `MuteControl`'s custom element (engine/ui/MuteControl.ts).
21
+ */
22
+ export declare const CHROME_ELEMENT_IDS: ReadonlySet<string>;
23
+ export interface HudHealthState {
24
+ current: number;
25
+ max: number;
26
+ visible: boolean;
27
+ }
28
+ /**
29
+ * One HUD element. Keys beyond id/type/anchor/visible are per-kind and are
30
+ * omitted when the recording never set them.
31
+ */
32
+ export interface HudElementState {
33
+ id: string;
34
+ type: HudElementKind;
35
+ anchor: string;
36
+ visible: boolean;
37
+ label?: string;
38
+ color?: string;
39
+ width?: number;
40
+ showText?: boolean;
41
+ percent?: number;
42
+ icon?: string;
43
+ text?: string;
44
+ iconSize?: number;
45
+ /** Timer only: the DERIVED displayed seconds; the harness formats it. */
46
+ seconds?: number;
47
+ countDown?: boolean;
48
+ format?: string;
49
+ html?: string;
50
+ css?: string;
51
+ }
52
+ export interface HudToastState {
53
+ message: string;
54
+ variant?: string;
55
+ }
56
+ export interface HudReplayState {
57
+ version: 1;
58
+ hudVisible: boolean;
59
+ /** null until the recording touches health at all. */
60
+ health: HudHealthState | null;
61
+ /** Verbatim from the `theme` op; null when the recording carried none. */
62
+ theme: unknown;
63
+ /** Creation order (= stacking order within an anchor); removed ones dropped. */
64
+ elements: HudElementState[];
65
+ toasts: HudToastState[];
66
+ }
67
+ /** The engine records `frame` on a 60fps source clock. */
68
+ export declare const DEFAULT_HUD_FPS = 60;
69
+ /** IGameHUD.showToast's default duration. */
70
+ export declare const DEFAULT_TOAST_MS = 2500;
71
+ /**
72
+ * The complete HUD state at `frame`. Ops after `frame` are ignored; unknown
73
+ * op names are skipped rather than throwing.
74
+ */
75
+ export declare function reduceHudState(ops: readonly HudOp[], frame: number, fps?: number): HudReplayState;
76
+ /** True when a state would rasterize to a fully transparent frame. */
77
+ export declare function isBlankHudState(state: HudReplayState): boolean;
78
+ export interface UiKeyframe {
79
+ /** Source frame whose state this is. */
80
+ sourceFrame: number;
81
+ clipIndex: number;
82
+ /** Output-timeline seconds this overlay becomes visible. */
83
+ outputStartSec: number;
84
+ /** Output-timeline seconds it stops being visible (next keyframe / clip end). */
85
+ outputEndSec: number;
86
+ state: HudReplayState;
87
+ }
88
+ export interface CollectUiKeyframesOptions {
89
+ /** Source-clock fps. Default 60. */
90
+ fps?: number;
91
+ /** Ceiling on keyframes per second of OUTPUT time. Default 10. */
92
+ maxPerSecond?: number;
93
+ /** Hard ceiling per clip. Default 240. */
94
+ maxPerClip?: number;
95
+ /** Per-clip opt-out; defaults to every clip. */
96
+ clipEnabled?: (clipIndex: number) => boolean;
97
+ /** Where truncation notices go. Defaults to stderr. */
98
+ log?: (line: string) => void;
99
+ }
100
+ export declare const DEFAULT_MAX_KEYFRAMES_PER_SECOND = 10;
101
+ export declare const DEFAULT_MAX_KEYFRAMES_PER_CLIP = 240;
102
+ /**
103
+ * Walk every enabled clip's source range and emit a keyframe wherever the
104
+ * reduced HUD state changes.
105
+ *
106
+ * A clip's overlays stop at `exclusiveOutputEnd`, so during a crossfade the
107
+ * incoming clip's UI takes over at the fade start and exactly one overlay is
108
+ * ever active — two stacked HUDs would double-draw every element.
109
+ */
110
+ export declare function collectUiKeyframes(ops: readonly HudOp[], clips: readonly ClipTiming[], opts?: CollectUiKeyframesOptions): UiKeyframe[];