@bitmagic/cli 0.1.61-dev.4 → 0.1.61-dev.6

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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * The F9 recorder's WebSocket frame sink: encoded video (or raw RGBA) in, PNG frames on disk out.
3
+ *
4
+ * Why not the HTTP sink (`recording-sink.ts`) for frames: every PNG the browser encodes with
5
+ * `canvas.toBlob` is a Blob, a Blob lives in Chrome's blob storage until its JS wrapper is
6
+ * garbage-collected, and a game loop rendering at 20 fps starves V8's GC enough that the store
7
+ * fills after roughly a thousand 1080p frames — from then on every new Blob is invalid and every
8
+ * upload fails until the tab reloads. Dropping the Blobs sooner did not help; the only cure is not
9
+ * making them. So the recorder reads raw pixels into the heap and ships them over one WebSocket.
10
+ *
11
+ * Why video rather than raw pixels: raw 1080p RGBA is 8.3 MB a frame and this socket carries
12
+ * ~95 MB/s, which pinned capture at 11 fps and made a guided session play in slow motion. The
13
+ * recorder's worker now encodes with WebCodecs (H.264 or VP9, ~100 KB a frame at 60 Mbps) and the
14
+ * heavy work moves to after the recording: this side appends the chunks to a file, and at stop it
15
+ * remuxes them into `capture.mp4` and extracts the PNGs with ffmpeg (`capture-file.ts`). Nothing
16
+ * downstream changes — `bitmagic trailer` reads the same `<name>_%06d.png` + `timeline.json`.
17
+ * Raw RGBA stays for a browser without an encoder, piped straight into ffmpeg during the recording.
18
+ *
19
+ * Wire protocol, all on `/api/dev/record-stream`:
20
+ * client → text {type:'start', recordingName, width, height, codec?: 'avc'|'vp9'} (no codec: raw RGBA)
21
+ * server → text {type:'started', codec?, container?} | {type:'error', error}
22
+ * client → binary video: [frameIndex: uint32 LE][keyframe: uint8][encoded chunk]
23
+ * raw: [frameIndex: uint32 LE][RGBA bytes, width*height*4]
24
+ * server → text {type:'ack', frameIndex} once the frame is on disk / handed to the encoder
25
+ * client → text {type:'stop', frameCount}
26
+ * server → text {type:'progress', phase: 'muxing'|'extracting', frames} while ffmpeg runs after stop
27
+ * server → text {type:'stopped', written, missing: number[], error?} once every PNG is on disk
28
+ *
29
+ * Frames must arrive in index order (a WebSocket delivers in order, and the recorder sends in
30
+ * order); a gap is filled by repeating the previous frame so the `%06d` sequence stays aligned
31
+ * with the recorder's frame clock, and the gap is reported back as `missing`. The HTTP routes
32
+ * stay for recorders that predate this sink.
33
+ */
34
+ import { spawn } from 'child_process';
35
+ import * as fs from 'fs';
36
+ import * as path from 'path';
37
+ import { WebSocketServer } from 'ws';
38
+ import { isSafeRecordingName } from './recording-sink.js';
39
+ import { recordingDir } from '../trailer/paths.js';
40
+ import { CaptureWriter, ChunkLedger, EXTRACT_DIR, containerName, extractArgs, finalizeFrames, isCaptureCodec, parseChunkMessage, remuxArgs, } from './capture-file.js';
41
+ export const RECORD_STREAM_PATH = '/api/dev/record-stream';
42
+ export const FRAME_HEADER_BYTES = 4;
43
+ /** Sanity bounds on a start request — nothing a real recorder sends is refused. */
44
+ export const MAX_FRAME_DIMENSION = 8192;
45
+ /** Validate the `start` message. */
46
+ export function parseStreamStart(raw) {
47
+ if (typeof raw !== 'object' || raw === null)
48
+ return { error: 'start must be an object' };
49
+ const msg = raw;
50
+ const recordingName = typeof msg.recordingName === 'string' ? msg.recordingName : '';
51
+ if (!isSafeRecordingName(recordingName))
52
+ return { error: 'invalid recordingName' };
53
+ const width = Number(msg.width);
54
+ const height = Number(msg.height);
55
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0
56
+ || width > MAX_FRAME_DIMENSION || height > MAX_FRAME_DIMENSION) {
57
+ return { error: 'invalid frame size' };
58
+ }
59
+ if (msg.codec !== undefined && !isCaptureCodec(msg.codec))
60
+ return { error: `unknown codec ${JSON.stringify(msg.codec)}` };
61
+ return { recordingName, width, height, ...(msg.codec !== undefined ? { codec: msg.codec } : {}) };
62
+ }
63
+ /** Split a binary message into its frame index and pixel bytes; null when it is not a frame. */
64
+ export function parseFrameMessage(data, frameBytes) {
65
+ if (data.length !== FRAME_HEADER_BYTES + frameBytes)
66
+ return null;
67
+ const frameIndex = data.readUInt32LE(0);
68
+ return { frameIndex, pixels: data.subarray(FRAME_HEADER_BYTES) };
69
+ }
70
+ /**
71
+ * Keeps the raw encoder's input contiguous. The encoder numbers frames sequentially from 0, so the
72
+ * frame written Nth becomes `_00000N.png`; feeding it exactly one frame per index — the real one
73
+ * when it arrived, the previous one when it did not — is what keeps the files aligned with the
74
+ * recorder's own frame count (the trailer's clock).
75
+ */
76
+ export class FrameSequencer {
77
+ expected = 0;
78
+ previous = null;
79
+ missing = [];
80
+ /** The buffers to hand to the encoder for this arrival, in order. Empty for a duplicate/late frame. */
81
+ accept(frameIndex, pixels) {
82
+ if (frameIndex < this.expected)
83
+ return [];
84
+ const out = [];
85
+ while (this.expected < frameIndex) {
86
+ if (this.previous !== null)
87
+ out.push(this.previous);
88
+ else
89
+ out.push(Buffer.alloc(pixels.length));
90
+ this.missing.push(this.expected);
91
+ this.expected++;
92
+ }
93
+ out.push(pixels);
94
+ this.previous = pixels;
95
+ this.expected++;
96
+ return out;
97
+ }
98
+ /** Pad up to the recorder's declared count at stop, so the sequence ends where the timeline says. */
99
+ finish(frameCount) {
100
+ const out = [];
101
+ while (this.expected < frameCount && this.previous !== null) {
102
+ out.push(this.previous);
103
+ this.missing.push(this.expected);
104
+ this.expected++;
105
+ }
106
+ return out;
107
+ }
108
+ get written() {
109
+ return this.expected;
110
+ }
111
+ }
112
+ /**
113
+ * Raw path ffmpeg arguments: RGBA on stdin, one PNG per frame at `<dir>/<name>_%06d.png`, numbered
114
+ * from 0 — the image2 muxer's own default is 1, which would put frame 0 in `_000001.png` while
115
+ * every reader in `cli/src/trailer/` looks for `_000000.png`.
116
+ */
117
+ export function encoderArgs(start, dir) {
118
+ return [
119
+ '-v', 'error', '-y',
120
+ '-f', 'rawvideo', '-pixel_format', 'rgba', '-video_size', `${start.width}x${start.height}`, '-framerate', '60',
121
+ '-i', 'pipe:0',
122
+ // Fast compression: these files are read once by the renderer and are disposable.
123
+ '-c:v', 'png', '-compression_level', '1', '-start_number', '0',
124
+ path.join(dir, `${start.recordingName}_%06d.png`),
125
+ ];
126
+ }
127
+ function send(ws, message) {
128
+ if (ws.readyState === ws.OPEN)
129
+ ws.send(JSON.stringify(message));
130
+ }
131
+ /** Write to the raw encoder honouring its backpressure: resolves once the chunk is accepted. */
132
+ function writeFrame(session, chunk) {
133
+ return new Promise((resolve) => {
134
+ if (session.stdinClosed || session.encoderError !== null) {
135
+ resolve();
136
+ return;
137
+ }
138
+ const ok = session.encoder.stdin.write(chunk);
139
+ if (ok)
140
+ resolve();
141
+ else
142
+ session.encoder.stdin.once('drain', () => resolve());
143
+ });
144
+ }
145
+ /** Run one ffmpeg step to completion; null on success, else the reason. */
146
+ function runCapture(binary, args) {
147
+ return new Promise((resolve) => {
148
+ let child;
149
+ try {
150
+ child = spawn(binary, args, { stdio: ['ignore', 'ignore', 'pipe'] });
151
+ }
152
+ catch (error) {
153
+ resolve(`ffmpeg could not start: ${error instanceof Error ? error.message : String(error)}`);
154
+ return;
155
+ }
156
+ let stderr = '';
157
+ child.stderr.on('data', (chunk) => { stderr = (stderr + chunk.toString()).slice(-4000); });
158
+ child.on('error', (error) => resolve(`ffmpeg is not available on this machine (${error.message}) — install ffmpeg to record`));
159
+ child.on('close', (code) => {
160
+ resolve(code === 0 ? null : `ffmpeg exited with code ${code}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`);
161
+ });
162
+ });
163
+ }
164
+ function countPngs(dir) {
165
+ try {
166
+ return fs.readdirSync(dir).filter((name) => name.endsWith('.png')).length;
167
+ }
168
+ catch {
169
+ return 0;
170
+ }
171
+ }
172
+ /**
173
+ * Mount the sink on the sidecar's HTTP server. One session per socket; a second `start` on the
174
+ * same socket is refused. Every failure is reported to the client as `{type:'error'}` so the
175
+ * recorder can fall back to the HTTP sink rather than lose the recording.
176
+ */
177
+ export function attachRecordingStream(server, root, log, ffmpegBinary = 'ffmpeg') {
178
+ const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_FRAME_DIMENSION * MAX_FRAME_DIMENSION * 4 + FRAME_HEADER_BYTES });
179
+ server.on('upgrade', (req, socket, head) => {
180
+ const url = (req.url ?? '/').split('?')[0];
181
+ if (url !== RECORD_STREAM_PATH) {
182
+ socket.destroy();
183
+ return;
184
+ }
185
+ wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
186
+ });
187
+ wss.on('connection', (ws) => {
188
+ let session = null;
189
+ // Frames are processed strictly one after another so the file sees them in order and the
190
+ // ack for a frame means "it is on disk / the encoder has it".
191
+ let chain = Promise.resolve();
192
+ const finishRaw = async (s, frameCount) => {
193
+ await chain;
194
+ if (frameCount !== null) {
195
+ for (const chunk of s.sequencer.finish(frameCount))
196
+ await writeFrame(s, chunk);
197
+ }
198
+ s.stdinClosed = true;
199
+ s.encoder.stdin.end();
200
+ const code = await new Promise((resolve) => {
201
+ if (s.encoder.exitCode !== null)
202
+ resolve(s.encoder.exitCode);
203
+ else
204
+ s.encoder.once('close', (c) => resolve(c));
205
+ });
206
+ if (code !== 0 && s.encoderError === null)
207
+ s.encoderError = `ffmpeg exited with code ${code}`;
208
+ const written = s.sequencer.written;
209
+ log(`[editor] recording stream ${s.start.recordingName}: ${written} frames encoded, ${s.sequencer.missing.length} missing${s.encoderError ? `, encoder: ${s.encoderError}` : ''}`);
210
+ send(ws, {
211
+ type: 'stopped',
212
+ written,
213
+ missing: s.sequencer.missing,
214
+ ...(s.encoderError !== null ? { error: s.encoderError } : {}),
215
+ });
216
+ };
217
+ // After stop the real work happens: remux, then extract. Progress goes back every two seconds
218
+ // so the recorder (and `bitmagic trailer record` behind it) can tell a long extraction from a
219
+ // dead sidecar.
220
+ const finishVideo = async (s, frameCount) => {
221
+ await chain;
222
+ s.writer.close();
223
+ const declared = frameCount ?? s.ledger.expected;
224
+ s.ledger.finish(declared);
225
+ const name = s.start.recordingName;
226
+ const codecName = s.codec === 'avc' ? 'H.264' : 'VP9';
227
+ log(`[editor] recording stream ${name}: ${s.ledger.received} frames captured as ${codecName}, ${s.ledger.missing.length} missing — writing ${containerName(s.codec)}`);
228
+ send(ws, { type: 'progress', phase: 'muxing', frames: 0 });
229
+ let error = s.error;
230
+ if (error === null)
231
+ error = await runCapture(ffmpegBinary, remuxArgs(s.codec, s.dir));
232
+ if (error === null) {
233
+ fs.rmSync(s.writer.filePath, { force: true });
234
+ const extractDir = path.join(s.dir, EXTRACT_DIR);
235
+ fs.mkdirSync(extractDir, { recursive: true });
236
+ const ticker = setInterval(() => send(ws, { type: 'progress', phase: 'extracting', frames: countPngs(extractDir) }), 2000);
237
+ error = await runCapture(ffmpegBinary, extractArgs(s.codec, s.dir));
238
+ clearInterval(ticker);
239
+ }
240
+ const result = finalizeFrames(s.dir, name, s.ledger, declared);
241
+ log(`[editor] recording stream ${name}: ${result.written} frames on disk, ${result.missing.length} missing${error !== null ? `, error: ${error}` : ''}`);
242
+ send(ws, { type: 'stopped', written: result.written, missing: result.missing, ...(error !== null ? { error } : {}) });
243
+ };
244
+ const finish = async (frameCount) => {
245
+ const s = session;
246
+ if (s === null)
247
+ return;
248
+ session = null;
249
+ if (s.kind === 'raw')
250
+ await finishRaw(s, frameCount);
251
+ else
252
+ await finishVideo(s, frameCount);
253
+ };
254
+ ws.on('message', (data, isBinary) => {
255
+ if (isBinary) {
256
+ const s = session;
257
+ if (s === null)
258
+ return;
259
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
260
+ if (s.kind === 'video') {
261
+ const chunk = parseChunkMessage(buffer);
262
+ if (chunk === null) {
263
+ send(ws, { type: 'error', error: `chunk message of ${buffer.length} bytes carries no frame` });
264
+ return;
265
+ }
266
+ const bytes = Buffer.from(chunk.bytes);
267
+ chain = chain.then(() => {
268
+ if (s.ledger.accept(chunk.frameIndex)) {
269
+ try {
270
+ s.writer.append(chunk.frameIndex, bytes);
271
+ }
272
+ catch (error) {
273
+ s.error = `could not write ${s.writer.filePath}: ${error instanceof Error ? error.message : String(error)}`;
274
+ }
275
+ }
276
+ send(ws, { type: 'ack', frameIndex: chunk.frameIndex });
277
+ });
278
+ return;
279
+ }
280
+ const frame = parseFrameMessage(buffer, s.frameBytes);
281
+ if (frame === null) {
282
+ send(ws, { type: 'error', error: `frame message of ${buffer.length} bytes does not match ${s.start.width}x${s.start.height} RGBA` });
283
+ return;
284
+ }
285
+ chain = chain.then(async () => {
286
+ for (const chunk of s.sequencer.accept(frame.frameIndex, frame.pixels))
287
+ await writeFrame(s, chunk);
288
+ send(ws, { type: 'ack', frameIndex: frame.frameIndex });
289
+ });
290
+ return;
291
+ }
292
+ let msg;
293
+ try {
294
+ msg = JSON.parse(data.toString());
295
+ }
296
+ catch {
297
+ send(ws, { type: 'error', error: 'message must be JSON' });
298
+ return;
299
+ }
300
+ const type = msg.type;
301
+ if (type === 'start') {
302
+ if (session !== null) {
303
+ send(ws, { type: 'error', error: 'a recording is already streaming on this socket' });
304
+ return;
305
+ }
306
+ const start = parseStreamStart(msg);
307
+ if ('error' in start) {
308
+ send(ws, { type: 'error', error: start.error });
309
+ return;
310
+ }
311
+ const dir = recordingDir(root, start.recordingName);
312
+ fs.mkdirSync(dir, { recursive: true });
313
+ if (start.codec !== undefined) {
314
+ let writer;
315
+ try {
316
+ writer = new CaptureWriter(start.codec, dir, start.width, start.height);
317
+ }
318
+ catch (error) {
319
+ send(ws, { type: 'error', error: `could not open the capture file: ${error instanceof Error ? error.message : String(error)}` });
320
+ return;
321
+ }
322
+ session = { kind: 'video', start, codec: start.codec, dir, ledger: new ChunkLedger(), writer, error: null };
323
+ log(`[editor] recording stream ${start.recordingName}: ${start.width}x${start.height} ${start.codec === 'avc' ? 'H.264' : 'VP9'} → ${dir}`);
324
+ send(ws, { type: 'started', codec: start.codec, container: containerName(start.codec) });
325
+ return;
326
+ }
327
+ let encoder;
328
+ try {
329
+ encoder = spawn(ffmpegBinary, encoderArgs(start, dir), { stdio: ['pipe', 'ignore', 'pipe'] });
330
+ }
331
+ catch (error) {
332
+ send(ws, { type: 'error', error: `ffmpeg could not start: ${error instanceof Error ? error.message : String(error)}` });
333
+ return;
334
+ }
335
+ const s = { kind: 'raw', start, frameBytes: start.width * start.height * 4, sequencer: new FrameSequencer(), encoder, encoderError: null, stdinClosed: false };
336
+ let stderr = '';
337
+ encoder.stderr.on('data', (chunk) => { stderr = (stderr + chunk.toString()).slice(-4000); });
338
+ encoder.on('error', (error) => {
339
+ s.encoderError = error.message;
340
+ send(ws, { type: 'error', error: `ffmpeg is not available on this machine (${error.message}) — install ffmpeg to record` });
341
+ });
342
+ encoder.stdin.on('error', () => { s.stdinClosed = true; });
343
+ encoder.on('close', (code) => {
344
+ if (code !== 0 && s.encoderError === null)
345
+ s.encoderError = `ffmpeg exited with code ${code}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`;
346
+ });
347
+ session = s;
348
+ log(`[editor] recording stream ${start.recordingName}: ${start.width}x${start.height} raw → ${dir}`);
349
+ send(ws, { type: 'started' });
350
+ return;
351
+ }
352
+ if (type === 'stop') {
353
+ const frameCount = Number(msg.frameCount);
354
+ void finish(Number.isInteger(frameCount) && frameCount >= 0 ? frameCount : null);
355
+ return;
356
+ }
357
+ send(ws, { type: 'error', error: `unknown message type ${JSON.stringify(type)}` });
358
+ });
359
+ ws.on('close', () => {
360
+ // Socket gone mid-recording: keep what arrived, numbered as it came.
361
+ void finish(null);
362
+ });
363
+ });
364
+ return wss;
365
+ }
366
+ //# sourceMappingURL=recording-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording-stream.js","sourceRoot":"","sources":["../../src/editor/recording-stream.ts"],"names":[],"mappings":"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"}
@@ -49,6 +49,7 @@ import { updateTagFor } from '../update/notice.js';
49
49
  import { writeDevViewRecord } from './tab-reuse.js';
50
50
  import { RECORDING_BODY_LIMIT_BYTES, parseSaveFrameRequest, parseTimelineRequest, writeRecordingFrame, writeRecordingTimeline, } from './recording-sink.js';
51
51
  import { renderHudReplayHtml } from './hud-replay-page.js';
52
+ import { attachRecordingStream } from './recording-stream.js';
52
53
  /** A shell poll this recent counts as a tab being there, when its stream has not reconnected yet. */
53
54
  const SHELL_POLL_RECENT_MS = 2_000;
54
55
  /** Bodies are a scene snapshot, not an upload — a megabyte is already generous. */
@@ -894,7 +895,7 @@ export async function startEditorServer(options) {
894
895
  * game-play-agent's `save-frame` route exactly, so the recorder needs no per-host branching and
895
896
  * the trailer tooling reads a pro-lane recording unchanged.
896
897
  */
897
- const handleRecordingFrame = async (req, res) => {
898
+ const handleRecordingFrame = async (req, res, query) => {
898
899
  let body;
899
900
  try {
900
901
  body = await readBodyBuffer(req, RECORDING_BODY_LIMIT_BYTES);
@@ -903,7 +904,7 @@ export async function startEditorServer(options) {
903
904
  sendJson(res, 413, { ok: false, error: messageOf(error) });
904
905
  return;
905
906
  }
906
- const parsed = await parseSaveFrameRequest(req.headers['content-type'], body);
907
+ const parsed = await parseSaveFrameRequest(req.headers['content-type'], body, query);
907
908
  if ('error' in parsed) {
908
909
  sendJson(res, 400, { ok: false, error: parsed.error });
909
910
  return;
@@ -951,6 +952,7 @@ export async function startEditorServer(options) {
951
952
  return;
952
953
  }
953
954
  const url = (req.url ?? '/').split('?')[0];
955
+ const query = new URL(req.url ?? '/', 'http://localhost').searchParams;
954
956
  if (req.method === 'GET' && (url === '/' || url === '/index.html')) {
955
957
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(shell);
956
958
  return;
@@ -1285,7 +1287,7 @@ export async function startEditorServer(options) {
1285
1287
  // The F9 recorder in the game iframe streams these. Paths identical to game-play-agent's so
1286
1288
  // the vendored engine posts the same request whichever host is the sink.
1287
1289
  if (req.method === 'POST' && url === '/api/dev/save-frame') {
1288
- await handleRecordingFrame(req, res);
1290
+ await handleRecordingFrame(req, res, query);
1289
1291
  return;
1290
1292
  }
1291
1293
  if (req.method === 'POST' && url === '/api/dev/save-recording-timeline') {
@@ -1300,6 +1302,9 @@ export async function startEditorServer(options) {
1300
1302
  sendJson(res, 404, { ok: false, error: `No route for ${req.method ?? '?'} ${url}` });
1301
1303
  })();
1302
1304
  });
1305
+ // The F9 recorder's frame stream (raw RGBA over a WebSocket, PNGs via ffmpeg) — see
1306
+ // recording-stream.ts for why the HTTP frame route is no longer what a current recorder uses.
1307
+ attachRecordingStream(server, options.root, log);
1303
1308
  await new Promise((resolve, reject) => {
1304
1309
  server.once('error', (error) => {
1305
1310
  reject(error.code === 'EADDRINUSE'