@bitmagic/cli 0.1.60 → 0.1.61-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.
- package/README.md +47 -8
- package/assets/fonts/NotoSans-Bold.ttf +0 -0
- package/assets/fonts/OFL.txt +94 -0
- package/dist/chrome-launch.d.ts +18 -0
- package/dist/chrome-launch.js +24 -0
- package/dist/chrome-launch.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate.d.ts +44 -0
- package/dist/commands/generate.js +143 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/trailer-publish.d.ts +15 -0
- package/dist/commands/trailer-publish.js +45 -0
- package/dist/commands/trailer-publish.js.map +1 -0
- package/dist/commands/trailer.d.ts +3 -0
- package/dist/commands/trailer.js +396 -0
- package/dist/commands/trailer.js.map +1 -0
- package/dist/editor/hud-replay-page.d.ts +7 -0
- package/dist/editor/hud-replay-page.js +219 -0
- package/dist/editor/hud-replay-page.js.map +1 -0
- package/dist/editor/recording-panel.d.ts +3 -1
- package/dist/editor/recording-panel.js +6 -2
- package/dist/editor/recording-panel.js.map +1 -1
- package/dist/editor/recording-sink.d.ts +3 -20
- package/dist/editor/recording-sink.js +14 -35
- package/dist/editor/recording-sink.js.map +1 -1
- package/dist/editor/server.js +15 -18
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.d.ts +0 -8
- package/dist/editor/shell-page.js +23 -19
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/generate/music.d.ts +51 -0
- package/dist/generate/music.js +93 -0
- package/dist/generate/music.js.map +1 -0
- package/dist/generate/stream.d.ts +13 -0
- package/dist/generate/stream.js +17 -1
- package/dist/generate/stream.js.map +1 -1
- package/dist/project/jobs.d.ts +1 -1
- package/dist/scaffold/project-files.d.ts +0 -1
- package/dist/scaffold/project-files.js +6 -33
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +3 -1
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.d.ts +22 -0
- package/dist/scaffold/skill-files.js +198 -0
- package/dist/scaffold/skill-files.js.map +1 -0
- package/dist/trailer/analyze.d.ts +23 -0
- package/dist/trailer/analyze.js +186 -0
- package/dist/trailer/analyze.js.map +1 -0
- package/dist/trailer/auto-edl.d.ts +46 -0
- package/dist/trailer/auto-edl.js +143 -0
- package/dist/trailer/auto-edl.js.map +1 -0
- package/dist/trailer/beat-cut.d.ts +57 -0
- package/dist/trailer/beat-cut.js +232 -0
- package/dist/trailer/beat-cut.js.map +1 -0
- package/dist/trailer/client.d.ts +52 -0
- package/dist/trailer/client.js +133 -0
- package/dist/trailer/client.js.map +1 -0
- package/dist/trailer/edl-time.d.ts +61 -0
- package/dist/trailer/edl-time.js +105 -0
- package/dist/trailer/edl-time.js.map +1 -0
- package/dist/trailer/ffmpeg.d.ts +8 -0
- package/dist/trailer/ffmpeg.js +55 -0
- package/dist/trailer/ffmpeg.js.map +1 -0
- package/dist/trailer/hud-raster.d.ts +28 -0
- package/dist/trailer/hud-raster.js +114 -0
- package/dist/trailer/hud-raster.js.map +1 -0
- package/dist/trailer/hud-state.d.ts +110 -0
- package/dist/trailer/hud-state.js +410 -0
- package/dist/trailer/hud-state.js.map +1 -0
- package/dist/trailer/io.d.ts +20 -0
- package/dist/trailer/io.js +15 -0
- package/dist/trailer/io.js.map +1 -0
- package/dist/trailer/make.d.ts +54 -0
- package/dist/trailer/make.js +205 -0
- package/dist/trailer/make.js.map +1 -0
- package/dist/trailer/merge.d.ts +7 -0
- package/dist/trailer/merge.js +144 -0
- package/dist/trailer/merge.js.map +1 -0
- package/dist/trailer/music-beats.d.ts +123 -0
- package/dist/trailer/music-beats.js +707 -0
- package/dist/trailer/music-beats.js.map +1 -0
- package/dist/trailer/paths.d.ts +28 -0
- package/dist/trailer/paths.js +101 -0
- package/dist/trailer/paths.js.map +1 -0
- package/dist/trailer/publish-record.d.ts +17 -0
- package/dist/trailer/publish-record.js +59 -0
- package/dist/trailer/publish-record.js.map +1 -0
- package/dist/trailer/publish.d.ts +45 -0
- package/dist/trailer/publish.js +149 -0
- package/dist/trailer/publish.js.map +1 -0
- package/dist/trailer/record-session.d.ts +72 -0
- package/dist/trailer/record-session.js +328 -0
- package/dist/trailer/record-session.js.map +1 -0
- package/dist/trailer/render.d.ts +57 -0
- package/dist/trailer/render.js +475 -0
- package/dist/trailer/render.js.map +1 -0
- package/dist/trailer/revision.d.ts +8 -0
- package/dist/trailer/revision.js +31 -0
- package/dist/trailer/revision.js.map +1 -0
- package/dist/trailer/synth.d.ts +9 -0
- package/dist/trailer/synth.js +120 -0
- package/dist/trailer/synth.js.map +1 -0
- package/dist/trailer/timeline-types.d.ts +176 -0
- package/dist/trailer/timeline-types.js +379 -0
- package/dist/trailer/timeline-types.js.map +1 -0
- package/dist/trailer/title-raster.d.ts +6 -0
- package/dist/trailer/title-raster.js +47 -0
- package/dist/trailer/title-raster.js.map +1 -0
- package/dist/trailer/titles.d.ts +44 -0
- package/dist/trailer/titles.js +119 -0
- package/dist/trailer/titles.js.map +1 -0
- package/dist/trailer/video-chain.d.ts +41 -0
- package/dist/trailer/video-chain.js +78 -0
- package/dist/trailer/video-chain.js.map +1 -0
- package/dist/verify/browser.js +12 -19
- package/dist/verify/browser.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trailer tool's HUD replay harness, served by the dev sidecar at `/hud-replay.html`.
|
|
3
|
+
*
|
|
4
|
+
* Recorded trailer frames have no UI — the HUD is DOM, not WebGL — so the recorder logs HUD STATE
|
|
5
|
+
* into timeline.json and `bitmagic trailer render --ui` replays it here: this page builds a REAL
|
|
6
|
+
* `GameHUD` from the project's own vendored engine (same classes, same theme CSS) and headless
|
|
7
|
+
* Chrome screenshots it with a transparent background to get alpha overlays. Using the engine's own
|
|
8
|
+
* HUD is the point: a reimplementation would drift from what the game actually renders.
|
|
9
|
+
*
|
|
10
|
+
* The page is served from the sidecar's origin but imports the engine from vite's (the game port):
|
|
11
|
+
* the import map below is the project's own `index.html` map with every local alias made absolute,
|
|
12
|
+
* so `engine/GameHUD.js` resolves to `http://localhost:<gamePort>/dist/engine/GameHUD.js`. Vite's
|
|
13
|
+
* default `server.cors` admits localhost origins, which is what makes the cross-origin module load
|
|
14
|
+
* work.
|
|
15
|
+
*
|
|
16
|
+
* Contract with `trailer/hud-raster.ts` (unchanged from the web lane's `game/hud-replay.html`):
|
|
17
|
+
* window.__hudReplayReady === true when the HUD is constructed
|
|
18
|
+
* window.__hudReplay.setViewport(w, h) size the surface to the recording
|
|
19
|
+
* window.__hudReplay.render(state) apply a state snapshot (synchronous)
|
|
20
|
+
*
|
|
21
|
+
* Same template-literal rule as the shell page: **no backticks** in the emitted output.
|
|
22
|
+
*/
|
|
23
|
+
import { CDN_IMPORTS } from '../scaffold/project-files.js';
|
|
24
|
+
import { importMapEntries } from '../scaffold/aliases.js';
|
|
25
|
+
/** The import map for the harness: CDN entries as-is, local aliases absolute on the game origin. */
|
|
26
|
+
export function hudReplayImportMap(gamePort) {
|
|
27
|
+
const origin = `http://localhost:${gamePort}`;
|
|
28
|
+
const local = Object.fromEntries(Object.entries(importMapEntries()).map(([key, target]) => [key, `${origin}${target}`]));
|
|
29
|
+
return { ...CDN_IMPORTS, ...local };
|
|
30
|
+
}
|
|
31
|
+
export function renderHudReplayHtml(options) {
|
|
32
|
+
const imports = hudReplayImportMap(options.gamePort);
|
|
33
|
+
return `<!DOCTYPE html>
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8">
|
|
37
|
+
<title>HUD Replay (trailer overlay)</title>
|
|
38
|
+
<style>
|
|
39
|
+
/* Transparent everywhere — the renderer captures with omitBackground so only HUD pixels carry alpha. */
|
|
40
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; }
|
|
41
|
+
/* The HUD fades itself in only on GameState.PLAYING, which never happens here; force it visible and
|
|
42
|
+
kill transitions so a screenshot taken immediately after render() is the settled frame. */
|
|
43
|
+
.hud-root { opacity: 1 !important; transition: none !important; }
|
|
44
|
+
.hud-root, .hud-root * { animation: none !important; transition: none !important; }
|
|
45
|
+
/* Gameplay layer holds the reticle/ESC hint — not part of a trailer overlay. */
|
|
46
|
+
.hud-gameplay-layer { display: none !important; }
|
|
47
|
+
</style>
|
|
48
|
+
<script type="importmap">
|
|
49
|
+
${JSON.stringify({ imports }, null, 2)}
|
|
50
|
+
</script>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<script type="module">
|
|
54
|
+
import { GameHUD } from 'engine/GameHUD.js';
|
|
55
|
+
import { applyThemeGlobals } from 'engine/hud/index.js';
|
|
56
|
+
|
|
57
|
+
var hud = null;
|
|
58
|
+
var builtKey = '';
|
|
59
|
+
|
|
60
|
+
function disposeHud() {
|
|
61
|
+
if (hud) { hud.dispose(); hud = null; }
|
|
62
|
+
builtKey = '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Identity of a state's STRUCTURE (not its values): unchanged means mutate, changed means rebuild. */
|
|
66
|
+
function structureKey(state) {
|
|
67
|
+
return JSON.stringify([
|
|
68
|
+
(state.elements || []).map(function (e) {
|
|
69
|
+
return [e.id, e.type, e.anchor, e.label == null ? null : e.label, e.icon == null ? null : e.icon,
|
|
70
|
+
e.color == null ? null : e.color, e.width == null ? null : e.width, e.showText == null ? null : e.showText,
|
|
71
|
+
e.iconSize == null ? null : e.iconSize, e.type === 'custom' ? (e.css == null ? null : e.css) : null];
|
|
72
|
+
}),
|
|
73
|
+
state.health ? 'health' : null,
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildFrom(state) {
|
|
78
|
+
disposeHud();
|
|
79
|
+
hud = new GameHUD();
|
|
80
|
+
if (state.theme) {
|
|
81
|
+
// applyThemeGlobals is what the HUD's private ThemeManager calls; re-applying is idempotent and
|
|
82
|
+
// synchronous, so a screenshot right after render() never catches the HUD mid-theme.
|
|
83
|
+
applyThemeGlobals(state.theme, { manageFontLink: true });
|
|
84
|
+
}
|
|
85
|
+
(state.elements || []).forEach(createElement);
|
|
86
|
+
if (state.health) hud.showHealth();
|
|
87
|
+
builtKey = structureKey(state);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createElement(el) {
|
|
91
|
+
var anchor = el.anchor;
|
|
92
|
+
switch (el.type) {
|
|
93
|
+
case 'counter':
|
|
94
|
+
hud.createCounter(el.id, { anchor: anchor, label: el.label, icon: el.icon, initialValue: 0 });
|
|
95
|
+
break;
|
|
96
|
+
case 'progress':
|
|
97
|
+
hud.createProgressBar(el.id, {
|
|
98
|
+
anchor: anchor, label: el.label, color: el.color, width: el.width,
|
|
99
|
+
showText: el.showText !== false, initialValue: 0, maxValue: 100,
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
case 'icon-text':
|
|
103
|
+
hud.createIconText(el.id, { anchor: anchor, icon: el.icon == null ? '' : el.icon, text: el.text == null ? '' : el.text, iconSize: el.iconSize });
|
|
104
|
+
break;
|
|
105
|
+
case 'timer':
|
|
106
|
+
hud.createTimer(el.id, {
|
|
107
|
+
anchor: anchor, label: el.label, countDown: !!el.countDown,
|
|
108
|
+
startSeconds: el.seconds == null ? 0 : el.seconds, format: el.format || 'mm:ss',
|
|
109
|
+
});
|
|
110
|
+
// The engine timer ticks on wall clock; the replay owns the value, so it must never start here.
|
|
111
|
+
break;
|
|
112
|
+
case 'custom':
|
|
113
|
+
hud.createCustomElement(el.id, { anchor: anchor, html: el.html == null ? '' : el.html, css: el.css == null ? '' : el.css });
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setText(root, selector, value) {
|
|
121
|
+
if (value === undefined || value === null) return;
|
|
122
|
+
var node = root.querySelector(selector);
|
|
123
|
+
if (node && node.textContent !== value) node.textContent = value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Write the exact rendered strings the recording captured. */
|
|
127
|
+
function applyValues(state) {
|
|
128
|
+
(state.elements || []).forEach(function (el) {
|
|
129
|
+
var element = hud.getElement(el.id);
|
|
130
|
+
if (!element) return;
|
|
131
|
+
var root = element.container;
|
|
132
|
+
root.style.display = el.visible === false ? 'none' : '';
|
|
133
|
+
switch (el.type) {
|
|
134
|
+
case 'counter':
|
|
135
|
+
setText(root, '.hud-counter__value', el.text);
|
|
136
|
+
break;
|
|
137
|
+
case 'progress': {
|
|
138
|
+
var fill = root.querySelector('.hud-progress-bar__fill');
|
|
139
|
+
if (fill && typeof el.percent === 'number') fill.style.setProperty('--hud-progress-value', el.percent + '%');
|
|
140
|
+
setText(root, '.hud-progress-bar__text', el.text);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'icon-text':
|
|
144
|
+
setText(root, '.hud-icon-text__icon', el.icon);
|
|
145
|
+
setText(root, '.hud-icon-text__label', el.text);
|
|
146
|
+
break;
|
|
147
|
+
case 'timer': {
|
|
148
|
+
// Format through the engine's own timer so mm:ss / hh:mm:ss stay identical to the game.
|
|
149
|
+
hud.resetTimer(el.id, el.seconds == null ? 0 : el.seconds);
|
|
150
|
+
var urgent = !!el.countDown && (el.seconds == null ? 0 : el.seconds) <= 10;
|
|
151
|
+
root.toggleAttribute('data-urgent', urgent);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'custom':
|
|
155
|
+
if (typeof el.html === 'string' && root.innerHTML !== el.html) root.innerHTML = el.html;
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (state.health) {
|
|
163
|
+
hud.showHealth();
|
|
164
|
+
hud.updateHealth(state.health.current, state.health.max);
|
|
165
|
+
var container = document.getElementById('hud-health-container');
|
|
166
|
+
if (container) {
|
|
167
|
+
container.style.display = state.health.visible === false ? 'none' : '';
|
|
168
|
+
// updateHealth arms a 200 ms damage flash on a wall-clock timeout; strip it so overlays don't flicker.
|
|
169
|
+
delete container.dataset.damaged;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
var hudRoot = document.querySelector('.hud-root');
|
|
174
|
+
if (hudRoot) hudRoot.style.display = state.hudVisible === false ? 'none' : '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Toasts are transient and timer-managed in the engine; the replay owns visibility, so draw them plainly. */
|
|
178
|
+
function applyToasts(state) {
|
|
179
|
+
document.querySelectorAll('.hud-replay-toasts').forEach(function (stale) { stale.remove(); });
|
|
180
|
+
var toasts = state.toasts || [];
|
|
181
|
+
if (!toasts.length) return;
|
|
182
|
+
var anchorEl = document.querySelector('.hud-anchor-top-center') || document.querySelector('.hud-root');
|
|
183
|
+
if (!anchorEl) return;
|
|
184
|
+
var stack = document.createElement('div');
|
|
185
|
+
stack.className = 'hud-toast-stack hud-toast-stack--newest-top hud-replay-toasts';
|
|
186
|
+
toasts.forEach(function (toast) {
|
|
187
|
+
var node = document.createElement('div');
|
|
188
|
+
node.className = 'hud-element hud-toast';
|
|
189
|
+
node.dataset.kind = toast.variant || 'info';
|
|
190
|
+
node.textContent = toast.message;
|
|
191
|
+
stack.appendChild(node);
|
|
192
|
+
});
|
|
193
|
+
anchorEl.appendChild(stack);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
window.__hudReplay = {
|
|
197
|
+
setViewport: function (width, height) {
|
|
198
|
+
document.body.style.width = width + 'px';
|
|
199
|
+
document.body.style.height = height + 'px';
|
|
200
|
+
},
|
|
201
|
+
render: function (state) {
|
|
202
|
+
var key = structureKey(state);
|
|
203
|
+
if (!hud || key !== builtKey) buildFrom(state);
|
|
204
|
+
applyValues(state);
|
|
205
|
+
applyToasts(state);
|
|
206
|
+
return true;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Build an empty HUD up front so the first render() has styles injected and the anchor grid in place.
|
|
211
|
+
hud = new GameHUD();
|
|
212
|
+
builtKey = structureKey({ elements: [] });
|
|
213
|
+
window.__hudReplayReady = true;
|
|
214
|
+
</script>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=hud-replay-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hud-replay-page.js","sourceRoot":"","sources":["../../src/editor/hud-replay-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAO1D,oGAAoG;AACpG,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,MAAM,MAAM,GAAG,oBAAoB,QAAQ,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAC9B,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,CACvF,CAAC;IACF,OAAO,EAAE,GAAG,WAAW,EAAE,GAAG,KAAK,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrD,OAAO;;;;;;;;;;;;;;;;EAgBP,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwKrC,CAAC;AACF,CAAC"}
|
|
@@ -30,4 +30,6 @@ export declare const RECORDING_PANEL_CSS = "\n /* Level with #hq-overlay and #p
|
|
|
30
30
|
* (`creator/src/vue/components/tools/DevToolsPanel.vue`), verbatim — one recording feature, two
|
|
31
31
|
* hosts, no drift in what a creator is offered.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
/** The six resolutions the panel offers — the Creator's list, and what `?rec=` on the shell accepts. */
|
|
34
|
+
export declare const RECORDING_RESOLUTIONS: readonly string[];
|
|
35
|
+
export declare const RECORDING_PANEL_HTML = " <div id=\"record-overlay\" class=\"bm-overlay\">\n <div id=\"record-dialog\" class=\"bm-dialog\">\n <h2>Record gameplay</h2>\n <span class=\"bm-label\">Recording mode</span>\n <label class=\"record-choice\">\n <input type=\"radio\" name=\"record-mode\" value=\"frames\"> 60 fps frame by frame\n </label>\n <label class=\"record-choice\">\n <input type=\"radio\" name=\"record-mode\" value=\"video\"> Video capture (real-time)\n </label>\n <label class=\"bm-label\" for=\"record-resolution\">Resolution</label>\n <select id=\"record-resolution\" class=\"bm-input\">\n <option value=\"1920x1080\">1920x1080 (Full HD)</option>\n <option value=\"1280x720\">1280x720 (HD)</option>\n <option value=\"3840x2160\">3840x2160 (4K)</option>\n <option value=\"1080x1920\">1080x1920 (Vertical Full HD)</option>\n <option value=\"720x1280\">720x1280 (Vertical HD)</option>\n <option value=\"2160x3840\">2160x3840 (Vertical 4K)</option>\n </select>\n <p id=\"record-hint\">Press F9 in the game to start and stop recording.</p>\n <p id=\"record-frames-note\" class=\"record-note\">Frames and a timeline land in\n <span id=\"record-note-path\">.bitmagic/trailer/recordings/gameplay_recording_…/</span> in this\n project, ready for <code>bitmagic trailer make</code>.</p>\n <p id=\"record-video-note\" class=\"record-note\">The capture downloads in your browser as a\n .webm file.</p>\n <div id=\"record-upgrade\">This project's engine predates saving frames to disk from here.\n Run <code>bitmagic upgrade</code>, then reload.</div>\n <div id=\"record-actions\">\n <button id=\"record-close\" class=\"bm-pill bm-pill--primary\" type=\"button\">Done</button>\n </div>\n </div>\n </div>\n";
|
|
@@ -52,6 +52,10 @@ export const RECORDING_PANEL_CSS = `
|
|
|
52
52
|
* (`creator/src/vue/components/tools/DevToolsPanel.vue`), verbatim — one recording feature, two
|
|
53
53
|
* hosts, no drift in what a creator is offered.
|
|
54
54
|
*/
|
|
55
|
+
/** The six resolutions the panel offers — the Creator's list, and what `?rec=` on the shell accepts. */
|
|
56
|
+
export const RECORDING_RESOLUTIONS = [
|
|
57
|
+
'1920x1080', '1280x720', '3840x2160', '1080x1920', '720x1280', '2160x3840',
|
|
58
|
+
];
|
|
55
59
|
export const RECORDING_PANEL_HTML = ` <div id="record-overlay" class="bm-overlay">
|
|
56
60
|
<div id="record-dialog" class="bm-dialog">
|
|
57
61
|
<h2>Record gameplay</h2>
|
|
@@ -73,8 +77,8 @@ export const RECORDING_PANEL_HTML = ` <div id="record-overlay" class="bm-overla
|
|
|
73
77
|
</select>
|
|
74
78
|
<p id="record-hint">Press F9 in the game to start and stop recording.</p>
|
|
75
79
|
<p id="record-frames-note" class="record-note">Frames and a timeline land in
|
|
76
|
-
<span id="record-note-path"
|
|
77
|
-
|
|
80
|
+
<span id="record-note-path">.bitmagic/trailer/recordings/gameplay_recording_…/</span> in this
|
|
81
|
+
project, ready for <code>bitmagic trailer make</code>.</p>
|
|
78
82
|
<p id="record-video-note" class="record-note">The capture downloads in your browser as a
|
|
79
83
|
.webm file.</p>
|
|
80
84
|
<div id="record-upgrade">This project's engine predates saving frames to disk from here.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-panel.js","sourceRoot":"","sources":["../../src/editor/recording-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBlC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCnC,CAAC"}
|
|
1
|
+
{"version":3,"file":"recording-panel.js","sourceRoot":"","sources":["../../src/editor/recording-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBlC,CAAC;AAEF;;;;;;GAMG;AACH,wGAAwG;AACxG,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW;CAC3E,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCnC,CAAC"}
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recording is a DEV-LINE capability: the Record panel, the sink routes and the sink announcement
|
|
3
|
-
* all exist only in a build installed from `@bitmagic/cli@dev`.
|
|
4
|
-
*
|
|
5
|
-
* The line is derived from the version field rather than asked for — a `-dev.N` suffix is what puts
|
|
6
|
-
* a build on the `dev` dist-tag in the first place (`docs/npm-publishing.md`), and `updateTagFor` is
|
|
7
|
-
* the same read the CLI already uses to decide which line to offer as an update. So there is no new
|
|
8
|
-
* switch to keep in step with the publish workflow, and nothing to remember to flip at release: a
|
|
9
|
-
* promotion drops the suffix and the capability goes with it.
|
|
10
|
-
*
|
|
11
|
-
* An unreadable version reads as a release and the feature is OFF. Failing closed is the direction
|
|
12
|
-
* that matters here — a release build quietly gaining an unfinished capability is worse than a dev
|
|
13
|
-
* build missing one, and the second is visible the moment someone looks for the button.
|
|
14
|
-
*/
|
|
15
|
-
export declare function recordingAvailable(version?: string): boolean;
|
|
16
1
|
/**
|
|
17
2
|
* PNG frames of a 4K recording run to tens of megabytes, and a long session's timeline manifest is
|
|
18
3
|
* event-per-frame JSON — both far past `server.ts`'s 8 MB scene-snapshot cap, which is why the sink
|
|
@@ -56,9 +41,7 @@ export declare function parseSaveFrameRequest(contentType: string | undefined, b
|
|
|
56
41
|
export declare function parseTimelineRequest(body: Buffer): TimelineSave | SinkParseError;
|
|
57
42
|
/** `<name>_%06d.png` — the frame naming the ffmpeg command and the trailer renderer both expect. */
|
|
58
43
|
export declare function frameFileName(recordingName: string, frameIndex: number): string;
|
|
59
|
-
/** `<
|
|
60
|
-
export declare function
|
|
61
|
-
/** Write one frame, creating the recording directory on first use. Returns the file path. */
|
|
62
|
-
export declare function writeRecordingFrame(home: string, save: FrameSave): Promise<string>;
|
|
44
|
+
/** Write one frame under `<root>/.bitmagic/trailer/recordings/<name>/`, creating it on first use. */
|
|
45
|
+
export declare function writeRecordingFrame(root: string, save: FrameSave): Promise<string>;
|
|
63
46
|
/** Write the timeline manifest next to the frames. Returns the file path. */
|
|
64
|
-
export declare function writeRecordingTimeline(
|
|
47
|
+
export declare function writeRecordingTimeline(root: string, save: TimelineSave): Promise<string>;
|
|
@@ -8,37 +8,20 @@
|
|
|
8
8
|
* POSTs went to `getAgentUrl()` — the upload proxy (which 404s them) or a dead `localhost:4111` —
|
|
9
9
|
* and the recording was silently lost. Now they land here.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* `save-recording-timeline`) on purpose
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Request shapes mirror game-play-agent's own routes (`mastra/routes.ts`, `save-frame` /
|
|
12
|
+
* `save-recording-timeline`) on purpose, and the directory LAYOUT is identical too —
|
|
13
|
+
* `<dir>/<name>_%06d.png` + `timeline.json` — so `bitmagic trailer` reads a pro-lane recording
|
|
14
|
+
* exactly like a web-lane one. Only the parent differs: the web lane writes `~/Downloads/<name>/`,
|
|
15
|
+
* this lane writes into the project (`.bitmagic/trailer/recordings/<name>/`, see `trailer/paths.ts`),
|
|
16
|
+
* so a recording belongs to the game it was taken from and never lands in a stranger's Downloads.
|
|
16
17
|
*
|
|
17
18
|
* Parsing and path-building live here rather than in `server.ts` so they are testable the way
|
|
18
|
-
* `save.ts` and `voxel-save.ts` are; the writes take
|
|
19
|
-
* real
|
|
19
|
+
* `save.ts` and `voxel-save.ts` are; the writes take the project root as a parameter so tests never
|
|
20
|
+
* touch a real project.
|
|
20
21
|
*/
|
|
21
22
|
import * as fs from 'fs';
|
|
22
23
|
import * as path from 'path';
|
|
23
|
-
import {
|
|
24
|
-
import { updateTagFor } from '../update/notice.js';
|
|
25
|
-
/**
|
|
26
|
-
* Recording is a DEV-LINE capability: the Record panel, the sink routes and the sink announcement
|
|
27
|
-
* all exist only in a build installed from `@bitmagic/cli@dev`.
|
|
28
|
-
*
|
|
29
|
-
* The line is derived from the version field rather than asked for — a `-dev.N` suffix is what puts
|
|
30
|
-
* a build on the `dev` dist-tag in the first place (`docs/npm-publishing.md`), and `updateTagFor` is
|
|
31
|
-
* the same read the CLI already uses to decide which line to offer as an update. So there is no new
|
|
32
|
-
* switch to keep in step with the publish workflow, and nothing to remember to flip at release: a
|
|
33
|
-
* promotion drops the suffix and the capability goes with it.
|
|
34
|
-
*
|
|
35
|
-
* An unreadable version reads as a release and the feature is OFF. Failing closed is the direction
|
|
36
|
-
* that matters here — a release build quietly gaining an unfinished capability is worse than a dev
|
|
37
|
-
* build missing one, and the second is visible the moment someone looks for the button.
|
|
38
|
-
*/
|
|
39
|
-
export function recordingAvailable(version = currentCliVersion()) {
|
|
40
|
-
return updateTagFor(version) === 'dev';
|
|
41
|
-
}
|
|
24
|
+
import { recordingDir } from '../trailer/paths.js';
|
|
42
25
|
/**
|
|
43
26
|
* PNG frames of a 4K recording run to tens of megabytes, and a long session's timeline manifest is
|
|
44
27
|
* event-per-frame JSON — both far past `server.ts`'s 8 MB scene-snapshot cap, which is why the sink
|
|
@@ -127,21 +110,17 @@ export function parseTimelineRequest(body) {
|
|
|
127
110
|
export function frameFileName(recordingName, frameIndex) {
|
|
128
111
|
return recordingName + '_' + String(frameIndex).padStart(6, '0') + '.png';
|
|
129
112
|
}
|
|
130
|
-
/** `<
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
/** Write one frame, creating the recording directory on first use. Returns the file path. */
|
|
135
|
-
export async function writeRecordingFrame(home, save) {
|
|
136
|
-
const dir = recordingDir(home, save.recordingName);
|
|
113
|
+
/** Write one frame under `<root>/.bitmagic/trailer/recordings/<name>/`, creating it on first use. */
|
|
114
|
+
export async function writeRecordingFrame(root, save) {
|
|
115
|
+
const dir = recordingDir(root, save.recordingName);
|
|
137
116
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
138
117
|
const filePath = path.join(dir, frameFileName(save.recordingName, save.frameIndex));
|
|
139
118
|
await fs.promises.writeFile(filePath, save.frame);
|
|
140
119
|
return filePath;
|
|
141
120
|
}
|
|
142
121
|
/** Write the timeline manifest next to the frames. Returns the file path. */
|
|
143
|
-
export async function writeRecordingTimeline(
|
|
144
|
-
const dir = recordingDir(
|
|
122
|
+
export async function writeRecordingTimeline(root, save) {
|
|
123
|
+
const dir = recordingDir(root, save.recordingName);
|
|
145
124
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
146
125
|
const filePath = path.join(dir, 'timeline.json');
|
|
147
126
|
await fs.promises.writeFile(filePath, save.json);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-sink.js","sourceRoot":"","sources":["../../src/editor/recording-sink.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"recording-sink.js","sourceRoot":"","sources":["../../src/editor/recording-sink.ts"],"names":[],"mappings":"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"}
|
package/dist/editor/server.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import * as http from 'http';
|
|
20
|
-
import * as os from 'os';
|
|
21
20
|
import * as path from 'path';
|
|
22
21
|
import { createRequire } from 'module';
|
|
23
22
|
import { fileURLToPath } from 'url';
|
|
@@ -48,7 +47,8 @@ import { startUploadProxy } from '../forge/upload-proxy.js';
|
|
|
48
47
|
import { currentCliVersion } from '../update/cli-version.js';
|
|
49
48
|
import { updateTagFor } from '../update/notice.js';
|
|
50
49
|
import { writeDevViewRecord } from './tab-reuse.js';
|
|
51
|
-
import { RECORDING_BODY_LIMIT_BYTES,
|
|
50
|
+
import { RECORDING_BODY_LIMIT_BYTES, parseSaveFrameRequest, parseTimelineRequest, writeRecordingFrame, writeRecordingTimeline, } from './recording-sink.js';
|
|
51
|
+
import { renderHudReplayHtml } from './hud-replay-page.js';
|
|
52
52
|
/** A shell poll this recent counts as a tab being there, when its stream has not reconnected yet. */
|
|
53
53
|
const SHELL_POLL_RECENT_MS = 2_000;
|
|
54
54
|
/** Bodies are a scene snapshot, not an upload — a megabyte is already generous. */
|
|
@@ -255,12 +255,7 @@ function readWorld(worldPath) {
|
|
|
255
255
|
export async function startEditorServer(options) {
|
|
256
256
|
const log = options.log ?? (() => { });
|
|
257
257
|
const worldPath = projectWorldJsonPath(options.root);
|
|
258
|
-
//
|
|
259
|
-
// rendered once here too, so the routes below and the markup can never disagree about it.
|
|
260
|
-
const recording = recordingAvailable();
|
|
261
|
-
// The install line straight from the version, not from `recordingAvailable()` — that is a feature
|
|
262
|
-
// flag that happens to follow the line today, and the badge must keep saying which build this is
|
|
263
|
-
// even after the recorder ships on both.
|
|
258
|
+
// The install line straight from the version: the badge says which build this is.
|
|
264
259
|
const cliVersion = currentCliVersion();
|
|
265
260
|
const shell = renderEditorShell({
|
|
266
261
|
gamePort: options.gamePort,
|
|
@@ -269,8 +264,10 @@ export async function startEditorServer(options) {
|
|
|
269
264
|
environment: options.environment,
|
|
270
265
|
cliLine: updateTagFor(cliVersion),
|
|
271
266
|
cliVersion,
|
|
272
|
-
recording,
|
|
273
267
|
});
|
|
268
|
+
// The trailer tool's HUD replay harness: a real GameHUD built from this project's vendored engine
|
|
269
|
+
// (served by vite on the game port), screenshotted by `bitmagic trailer render --ui`.
|
|
270
|
+
const hudReplay = renderHudReplayHtml({ gamePort: options.gamePort });
|
|
274
271
|
/** When the page last polled us — the poll carries `?from=shell`; the CLI's own probes do not. */
|
|
275
272
|
let lastShellPollAt = 0;
|
|
276
273
|
/** Flipped by `dev` once vite answers; the page defers a restart-reload until then. */
|
|
@@ -912,7 +909,7 @@ export async function startEditorServer(options) {
|
|
|
912
909
|
return;
|
|
913
910
|
}
|
|
914
911
|
try {
|
|
915
|
-
const filePath = await writeRecordingFrame(
|
|
912
|
+
const filePath = await writeRecordingFrame(options.root, parsed);
|
|
916
913
|
sendJson(res, 200, { ok: true, path: filePath });
|
|
917
914
|
}
|
|
918
915
|
catch (error) {
|
|
@@ -937,7 +934,7 @@ export async function startEditorServer(options) {
|
|
|
937
934
|
return;
|
|
938
935
|
}
|
|
939
936
|
try {
|
|
940
|
-
const filePath = await writeRecordingTimeline(
|
|
937
|
+
const filePath = await writeRecordingTimeline(options.root, parsed);
|
|
941
938
|
log(`[editor] recording timeline saved: ${filePath}`);
|
|
942
939
|
sendJson(res, 200, { ok: true, path: filePath });
|
|
943
940
|
}
|
|
@@ -1287,19 +1284,19 @@ export async function startEditorServer(options) {
|
|
|
1287
1284
|
}
|
|
1288
1285
|
// The F9 recorder in the game iframe streams these. Paths identical to game-play-agent's so
|
|
1289
1286
|
// the vendored engine posts the same request whichever host is the sink.
|
|
1290
|
-
|
|
1291
|
-
// Gated on the dev line with the same predicate that draws the panel, so a release build has
|
|
1292
|
-
// no sink even for a recorder told about one by a stale page — and answering 404 rather than
|
|
1293
|
-
// 403 keeps a release sidecar indistinguishable from one that never had the route, which is
|
|
1294
|
-
// what game-play-agent's own devOnly wrapper does outside development.
|
|
1295
|
-
if (recording && req.method === 'POST' && url === '/api/dev/save-frame') {
|
|
1287
|
+
if (req.method === 'POST' && url === '/api/dev/save-frame') {
|
|
1296
1288
|
await handleRecordingFrame(req, res);
|
|
1297
1289
|
return;
|
|
1298
1290
|
}
|
|
1299
|
-
if (
|
|
1291
|
+
if (req.method === 'POST' && url === '/api/dev/save-recording-timeline') {
|
|
1300
1292
|
await handleRecordingTimeline(req, res);
|
|
1301
1293
|
return;
|
|
1302
1294
|
}
|
|
1295
|
+
if (req.method === 'GET' && url === '/hud-replay.html') {
|
|
1296
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1297
|
+
res.end(hudReplay);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1303
1300
|
sendJson(res, 404, { ok: false, error: `No route for ${req.method ?? '?'} ${url}` });
|
|
1304
1301
|
})();
|
|
1305
1302
|
});
|