@livedesk/hub 0.1.4 → 0.1.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/package.json +2 -2
- package/src/agents/agent-tool-registry.js +19 -19
- package/src/agents/codex-agent-runtime.js +7 -3
- package/src/filesystem/shared-folders.js +1 -1
- package/src/filesystem/transfer-jobs.js +90 -5
- package/src/live-desk-update.js +585 -302
- package/src/mode4-atlas-pool.js +123 -0
- package/src/mode4-atlas-sizing.js +32 -0
- package/src/mode4-atlas-worker.js +58 -11
- package/src/mode4-atlas.js +9 -1
- package/src/remote-hub.js +1259 -423
- package/src/server.js +776 -275
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Mode4AtlasSession } from './mode4-atlas.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export function buildMode4AtlasSessionKey(config = {}) {
|
|
5
|
+
return JSON.stringify({
|
|
6
|
+
deviceIds: Array.isArray(config.deviceIds) ? config.deviceIds : [],
|
|
7
|
+
width: Number(config.width || 1920),
|
|
8
|
+
height: Number(config.height || 1080),
|
|
9
|
+
tileWidth: Number(config.tileWidth || 320),
|
|
10
|
+
tileHeight: Number(config.tileHeight || 180),
|
|
11
|
+
fps: Number(config.fps || 20)
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function planMode4AtlasInputTransitions(previous = {}, next = {}) {
|
|
16
|
+
const previousIds = new Set(Array.isArray(previous.inputDeviceIds) ? previous.inputDeviceIds : []);
|
|
17
|
+
const nextIds = new Set(Array.isArray(next.inputDeviceIds) ? next.inputDeviceIds : []);
|
|
18
|
+
const previousMonitorSelections = previous.monitorSelections || {};
|
|
19
|
+
const nextMonitorSelections = next.monitorSelections || {};
|
|
20
|
+
const captureSizeChanged = previousIds.size > 0
|
|
21
|
+
&& (Number(previous.tileWidth || 0) !== Number(next.tileWidth || 0)
|
|
22
|
+
|| Number(previous.tileHeight || 0) !== Number(next.tileHeight || 0));
|
|
23
|
+
const captureFpsChanged = previousIds.size > 0
|
|
24
|
+
&& Number(previous.inputFps || 0) !== Number(next.inputFps || 0);
|
|
25
|
+
const stop = [...previousIds].filter(deviceId => !nextIds.has(deviceId));
|
|
26
|
+
const start = [];
|
|
27
|
+
const restart = [];
|
|
28
|
+
const unchanged = [];
|
|
29
|
+
|
|
30
|
+
for (const deviceId of nextIds) {
|
|
31
|
+
if (!previousIds.has(deviceId)) {
|
|
32
|
+
start.push(deviceId);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const previousMonitorIndex = Number(previousMonitorSelections[deviceId] || 0);
|
|
36
|
+
const nextMonitorIndex = Number(nextMonitorSelections[deviceId] || 0);
|
|
37
|
+
if (captureSizeChanged || captureFpsChanged || previousMonitorIndex !== nextMonitorIndex) {
|
|
38
|
+
restart.push(deviceId);
|
|
39
|
+
} else {
|
|
40
|
+
unchanged.push(deviceId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { stop, start, restart, unchanged };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Mode4AtlasPool {
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.createSession = typeof options.createSession === 'function'
|
|
50
|
+
? options.createSession
|
|
51
|
+
: callbacks => new Mode4AtlasSession(callbacks);
|
|
52
|
+
this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : () => {};
|
|
53
|
+
this.entries = new Map();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
acquire(config, callbacks = {}) {
|
|
57
|
+
const key = buildMode4AtlasSessionKey(config);
|
|
58
|
+
let entry = this.entries.get(key);
|
|
59
|
+
if (!entry) {
|
|
60
|
+
entry = {
|
|
61
|
+
key,
|
|
62
|
+
clients: new Map(),
|
|
63
|
+
latestLayout: null,
|
|
64
|
+
session: null,
|
|
65
|
+
streamId: `mode4-atlas-shared-${randomUUID()}`
|
|
66
|
+
};
|
|
67
|
+
entry.session = this.createSession({
|
|
68
|
+
onFrame: output => {
|
|
69
|
+
for (const listener of entry.clients.values()) listener.onFrame?.(output);
|
|
70
|
+
},
|
|
71
|
+
onLayout: layout => {
|
|
72
|
+
entry.latestLayout = layout;
|
|
73
|
+
for (const listener of entry.clients.values()) listener.onLayout?.(layout);
|
|
74
|
+
},
|
|
75
|
+
onStatus: status => {
|
|
76
|
+
this.onStatus(status);
|
|
77
|
+
for (const listener of entry.clients.values()) listener.onStatus?.(status);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
entry.session.configure(config);
|
|
81
|
+
this.entries.set(key, entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const clientId = Symbol(key);
|
|
85
|
+
entry.clients.set(clientId, callbacks);
|
|
86
|
+
if (entry.latestLayout) {
|
|
87
|
+
queueMicrotask(() => {
|
|
88
|
+
if (entry.clients.has(clientId)) callbacks.onLayout?.(entry.latestLayout);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let released = false;
|
|
93
|
+
return {
|
|
94
|
+
key,
|
|
95
|
+
session: entry.session,
|
|
96
|
+
streamId: entry.streamId,
|
|
97
|
+
release: () => {
|
|
98
|
+
if (released) return;
|
|
99
|
+
released = true;
|
|
100
|
+
entry.clients.delete(clientId);
|
|
101
|
+
if (entry.clients.size === 0) {
|
|
102
|
+
this.entries.delete(key);
|
|
103
|
+
entry.session.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ingest(frameEvent) {
|
|
110
|
+
for (const entry of this.entries.values()) {
|
|
111
|
+
entry.session.ingest(frameEvent);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get size() {
|
|
116
|
+
return this.entries.size;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
close() {
|
|
120
|
+
for (const entry of this.entries.values()) entry.session.close();
|
|
121
|
+
this.entries.clear();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const TILE_PRESETS = Object.freeze([
|
|
2
|
+
Object.freeze({ width: 320, height: 180 }),
|
|
3
|
+
Object.freeze({ width: 480, height: 270 }),
|
|
4
|
+
Object.freeze({ width: 640, height: 360 })
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export function resolveMode4AtlasTileSize(options = {}) {
|
|
8
|
+
const deviceCount = Math.max(1, Math.floor(Number(options.deviceCount) || 1));
|
|
9
|
+
const atlasWidth = Math.max(640, Math.floor(Number(options.atlasWidth) || 1920));
|
|
10
|
+
const atlasHeight = Math.max(360, Math.floor(Number(options.atlasHeight) || 1080));
|
|
11
|
+
const requestedWidth = Number(options.requestedWidth) || 320;
|
|
12
|
+
const requestedHeight = Number(options.requestedHeight) || 180;
|
|
13
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(deviceCount)));
|
|
14
|
+
const rows = Math.max(1, Math.ceil(deviceCount / columns));
|
|
15
|
+
const requestedPresetIndex = requestedWidth >= 640 || requestedHeight >= 360
|
|
16
|
+
? 2
|
|
17
|
+
: requestedWidth >= 480 || requestedHeight >= 270
|
|
18
|
+
? 1
|
|
19
|
+
: 0;
|
|
20
|
+
const supportedCandidates = TILE_PRESETS.slice(0, requestedPresetIndex + 1).reverse();
|
|
21
|
+
const selected = supportedCandidates.find(candidate => (
|
|
22
|
+
candidate.width * columns <= atlasWidth
|
|
23
|
+
&& candidate.height * rows <= atlasHeight
|
|
24
|
+
)) || TILE_PRESETS[0];
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
width: selected.width,
|
|
28
|
+
height: selected.height,
|
|
29
|
+
columns,
|
|
30
|
+
rows
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -4,15 +4,16 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { decodeLzo1xBlock } from './lzo1x.js';
|
|
7
|
+
import { resolveMode4AtlasTileSize } from './mode4-atlas-sizing.js';
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const DEFAULT_WIDTH = 1920;
|
|
10
11
|
const DEFAULT_HEIGHT = 1080;
|
|
11
12
|
const DEFAULT_FPS = 20;
|
|
12
13
|
const MAX_DEVICES = 100;
|
|
13
|
-
const FRAME_POOL_SIZE =
|
|
14
|
-
const MAX_TILE_WIDTH =
|
|
15
|
-
const MAX_TILE_HEIGHT =
|
|
14
|
+
const FRAME_POOL_SIZE = 3;
|
|
15
|
+
const MAX_TILE_WIDTH = 640;
|
|
16
|
+
const MAX_TILE_HEIGHT = 360;
|
|
16
17
|
|
|
17
18
|
const rgb565Red = new Uint8Array(65_536);
|
|
18
19
|
const rgb565Green = new Uint8Array(65_536);
|
|
@@ -43,7 +44,11 @@ let nextTickAt = 0;
|
|
|
43
44
|
let closed = false;
|
|
44
45
|
let lastComposeMs = 0;
|
|
45
46
|
let poolStarved = 0;
|
|
47
|
+
let inputRevision = 0;
|
|
48
|
+
let lastEncodedInputRevision = -1;
|
|
49
|
+
let lastAtlasWriteAtEpochMs = 0;
|
|
46
50
|
const ENCODER_STARTUP_OUTPUT_TIMEOUT_MS = 8_000;
|
|
51
|
+
const ATLAS_KEEPALIVE_MS = 1_000;
|
|
47
52
|
|
|
48
53
|
function clamp(value, min, max, fallback) {
|
|
49
54
|
const number = Number(value);
|
|
@@ -55,12 +60,18 @@ function normalizeConfig(value) {
|
|
|
55
60
|
.map(item => String(item || '').trim()).filter(Boolean))].slice(0, MAX_DEVICES);
|
|
56
61
|
const maxWidth = clamp(value?.width, 640, 3840, DEFAULT_WIDTH);
|
|
57
62
|
const maxHeight = clamp(value?.height, 360, 2160, DEFAULT_HEIGHT);
|
|
58
|
-
const sharpTiles = Number(value?.tileWidth) >= 480 || Number(value?.tileHeight) >= 270;
|
|
59
|
-
const sourceTileWidth = sharpTiles ? 480 : 320;
|
|
60
|
-
const sourceTileHeight = sharpTiles ? 270 : 180;
|
|
61
63
|
const count = Math.max(1, deviceIds.length);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
+
const tileSize = resolveMode4AtlasTileSize({
|
|
65
|
+
requestedWidth: value?.tileWidth,
|
|
66
|
+
requestedHeight: value?.tileHeight,
|
|
67
|
+
deviceCount: count,
|
|
68
|
+
atlasWidth: maxWidth,
|
|
69
|
+
atlasHeight: maxHeight
|
|
70
|
+
});
|
|
71
|
+
const columns = tileSize.columns;
|
|
72
|
+
const rows = tileSize.rows;
|
|
73
|
+
const sourceTileWidth = tileSize.width;
|
|
74
|
+
const sourceTileHeight = tileSize.height;
|
|
64
75
|
const tileWidth = Math.max(2, Math.min(sourceTileWidth, Math.floor(maxWidth / columns))) & ~1;
|
|
65
76
|
const tileHeight = Math.max(2, Math.min(sourceTileHeight, Math.floor(maxHeight / rows))) & ~1;
|
|
66
77
|
return {
|
|
@@ -229,6 +240,12 @@ function emitEncodedUnit(payload) {
|
|
|
229
240
|
clearTimeout(encoderStartupTimer);
|
|
230
241
|
encoderStartupTimer = null;
|
|
231
242
|
frameSeq += 1;
|
|
243
|
+
const readyTiles = [...latestTiles.values()];
|
|
244
|
+
const inputTimes = readyTiles
|
|
245
|
+
.map(tile => Number(tile.hubReceivedAtEpochMs || 0))
|
|
246
|
+
.filter(value => value > 0);
|
|
247
|
+
const sourceNewestAtEpochMs = inputTimes.length > 0 ? Math.max(...inputTimes) : 0;
|
|
248
|
+
const sourceOldestAtEpochMs = inputTimes.length > 0 ? Math.min(...inputTimes) : 0;
|
|
232
249
|
send({
|
|
233
250
|
type: 'frame',
|
|
234
251
|
payload: Buffer.from(payload),
|
|
@@ -248,7 +265,11 @@ function emitEncodedUnit(payload) {
|
|
|
248
265
|
composeMs: lastComposeMs,
|
|
249
266
|
poolStarved,
|
|
250
267
|
readyTileCount: latestTiles.size,
|
|
251
|
-
tileCount: config.deviceIds.length
|
|
268
|
+
tileCount: config.deviceIds.length,
|
|
269
|
+
inputRevision,
|
|
270
|
+
sourceNewestAtEpochMs,
|
|
271
|
+
sourceOldestAtEpochMs,
|
|
272
|
+
sourceMaxAgeMs: sourceOldestAtEpochMs > 0 ? Math.max(0, Date.now() - sourceOldestAtEpochMs) : 0
|
|
252
273
|
}
|
|
253
274
|
});
|
|
254
275
|
}
|
|
@@ -317,10 +338,28 @@ function ingestFrame(message) {
|
|
|
317
338
|
const expectedLength = width * height * 2;
|
|
318
339
|
if (!width || !height || Number(message.uncompressedByteLength) !== expectedLength) return;
|
|
319
340
|
try {
|
|
341
|
+
const contentHash = String(message.contentHash || '');
|
|
342
|
+
const previous = latestTiles.get(deviceId);
|
|
343
|
+
const inputMetadata = {
|
|
344
|
+
frameSeq: Number(message.frameSeq || 0),
|
|
345
|
+
monitorIndex: Number(message.monitorIndex || 0),
|
|
346
|
+
capturedAtEpochMs: Number(message.capturedAtEpochMs || 0),
|
|
347
|
+
hubReceivedAtEpochMs: Number(message.hubReceivedAtEpochMs || 0) || Date.now(),
|
|
348
|
+
contentHash
|
|
349
|
+
};
|
|
350
|
+
if (previous
|
|
351
|
+
&& contentHash
|
|
352
|
+
&& previous.contentHash === contentHash
|
|
353
|
+
&& previous.width === width
|
|
354
|
+
&& previous.height === height) {
|
|
355
|
+
Object.assign(previous, inputMetadata);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
320
358
|
const compressed = Buffer.isBuffer(message.payload) ? message.payload : Buffer.from(message.payload || []);
|
|
321
359
|
const rgb565 = decodeLzo1xBlock(new Uint8Array(compressed), expectedLength);
|
|
322
|
-
const tileState = { width, height, rgb565, rendered: null,
|
|
360
|
+
const tileState = { width, height, rgb565, rendered: null, ...inputMetadata };
|
|
323
361
|
latestTiles.set(deviceId, tileState);
|
|
362
|
+
inputRevision += 1;
|
|
324
363
|
if (!encoder && encoderCandidates.length > 0) startEncoder();
|
|
325
364
|
if (config.deviceIds.length > 0 && config.deviceIds.every(id => latestTiles.has(id))) {
|
|
326
365
|
clearTimeout(inputWaitTimer);
|
|
@@ -339,6 +378,11 @@ function writeAtlasFrame() {
|
|
|
339
378
|
startEncoder();
|
|
340
379
|
return;
|
|
341
380
|
}
|
|
381
|
+
const nowEpochMs = Date.now();
|
|
382
|
+
if (inputRevision === lastEncodedInputRevision
|
|
383
|
+
&& nowEpochMs - lastAtlasWriteAtEpochMs < ATLAS_KEEPALIVE_MS) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
342
386
|
const composeStartedAt = performance.now();
|
|
343
387
|
const target = framePool.pop();
|
|
344
388
|
const stdin = encoder?.child?.stdin;
|
|
@@ -371,6 +415,8 @@ function writeAtlasFrame() {
|
|
|
371
415
|
send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
|
|
372
416
|
}
|
|
373
417
|
});
|
|
418
|
+
lastEncodedInputRevision = inputRevision;
|
|
419
|
+
lastAtlasWriteAtEpochMs = nowEpochMs;
|
|
374
420
|
} catch (error) {
|
|
375
421
|
framePool.push(target);
|
|
376
422
|
if (!isClosedPipeError(error)) send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
|
|
@@ -396,6 +442,7 @@ function configure(value) {
|
|
|
396
442
|
const next = normalizeConfig(value);
|
|
397
443
|
const encoderChanged = next.width !== config.width || next.height !== config.height || next.fps !== config.fps;
|
|
398
444
|
config = next;
|
|
445
|
+
lastEncodedInputRevision = -1;
|
|
399
446
|
latestTiles = new Map([...latestTiles].filter(([deviceId]) => config.deviceIds.includes(deviceId)));
|
|
400
447
|
clearTimeout(inputWaitTimer);
|
|
401
448
|
inputWaitTimer = config.deviceIds.length > 0 ? setTimeout(() => {
|
|
@@ -404,7 +451,7 @@ function configure(value) {
|
|
|
404
451
|
send({
|
|
405
452
|
type: 'log',
|
|
406
453
|
level: 'warn',
|
|
407
|
-
message: `Mode 4 waiting for
|
|
454
|
+
message: `Mode 4 waiting for internal Atlas input from ${missing.length}/${config.deviceIds.length} devices: ${missing.slice(0, 8).join(', ')}`
|
|
408
455
|
});
|
|
409
456
|
}
|
|
410
457
|
}, 3000) : null;
|
package/src/mode4-atlas.js
CHANGED
|
@@ -107,7 +107,11 @@ export class Mode4AtlasSession {
|
|
|
107
107
|
const frame = frameEvent?.frame || {};
|
|
108
108
|
const deviceId = String(frameEvent?.deviceId || frame.deviceId || '').trim();
|
|
109
109
|
const mode = String(frame.frameMode || frame.mode || '').toLowerCase();
|
|
110
|
-
|
|
110
|
+
const streamPurpose = String(frame.streamPurpose || '').trim().toLowerCase();
|
|
111
|
+
if (!this.inputDeviceSet.has(deviceId)
|
|
112
|
+
|| mode !== 'mode2-lzo'
|
|
113
|
+
|| streamPurpose !== 'atlas'
|
|
114
|
+
|| !Buffer.isBuffer(frameEvent?.payload)) return;
|
|
111
115
|
this.pendingFrames.set(deviceId, {
|
|
112
116
|
type: 'frame',
|
|
113
117
|
deviceId,
|
|
@@ -115,6 +119,10 @@ export class Mode4AtlasSession {
|
|
|
115
119
|
width: Number(frame.width || 0),
|
|
116
120
|
height: Number(frame.height || 0),
|
|
117
121
|
uncompressedByteLength: Number(frame.uncompressedByteLength || 0),
|
|
122
|
+
monitorIndex: Number(frame.monitorIndex || 0),
|
|
123
|
+
capturedAtEpochMs: Date.parse(frame.capturedAt || '') || 0,
|
|
124
|
+
hubReceivedAtEpochMs: Date.parse(frame.receivedAt || '') || Date.now(),
|
|
125
|
+
contentHash: String(frame.contentHash || ''),
|
|
118
126
|
payload: frameEvent.payload
|
|
119
127
|
});
|
|
120
128
|
this.pumpDeviceFrame(deviceId);
|