@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.
- package/README.md +9 -4
- package/dist/commands/trailer.js +9 -3
- package/dist/commands/trailer.js.map +1 -1
- package/dist/editor/capture-file.d.ts +70 -0
- package/dist/editor/capture-file.js +204 -0
- package/dist/editor/capture-file.js.map +1 -0
- package/dist/editor/recording-sink.d.ts +1 -1
- package/dist/editor/recording-sink.js +16 -2
- package/dist/editor/recording-sink.js.map +1 -1
- package/dist/editor/recording-stream.d.ts +52 -0
- package/dist/editor/recording-stream.js +366 -0
- package/dist/editor/recording-stream.js.map +1 -0
- package/dist/editor/server.js +8 -3
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +3 -1
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/scaffold/skill-files.js +4 -2
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/trailer/record-session.d.ts +20 -1
- package/dist/trailer/record-session.js +70 -8
- package/dist/trailer/record-session.js.map +1 -1
- package/package.json +6 -4
|
@@ -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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"}
|
package/dist/editor/server.js
CHANGED
|
@@ -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'
|