livedesk 0.1.177 → 0.1.178
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/bin/livedesk.js +1 -0
- package/hub/src/mode4-atlas-worker.js +37 -10
- package/hub/src/server.js +27 -12
- package/package.json +1 -1
package/bin/livedesk.js
CHANGED
|
@@ -306,6 +306,7 @@ async function runManager(args) {
|
|
|
306
306
|
...process.env,
|
|
307
307
|
LIVEDESK_HUB_HTTP_HOST: options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1',
|
|
308
308
|
LIVEDESK_HUB_HTTP_PORT: String(httpPort),
|
|
309
|
+
LIVEDESK_MANAGER_VERSION: readVersion(),
|
|
309
310
|
REMOTE_HUB_PORT: String(remotePort),
|
|
310
311
|
REMOTE_HUB_PAIR_TOKEN: pairToken,
|
|
311
312
|
LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html')) ? packagedWebDist : (process.env.LIVEDESK_WEB_DIST || '')
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
5
6
|
import { decodeLzo1xBlock } from './lzo1x.js';
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
@@ -42,6 +43,7 @@ let nextTickAt = 0;
|
|
|
42
43
|
let closed = false;
|
|
43
44
|
let lastComposeMs = 0;
|
|
44
45
|
let poolStarved = 0;
|
|
46
|
+
const ENCODER_STARTUP_OUTPUT_TIMEOUT_MS = 8_000;
|
|
45
47
|
|
|
46
48
|
function clamp(value, min, max, fallback) {
|
|
47
49
|
const number = Number(value);
|
|
@@ -85,7 +87,8 @@ function resolveFfmpegPaths() {
|
|
|
85
87
|
const paths = [];
|
|
86
88
|
const add = value => {
|
|
87
89
|
const path = String(value || '').trim();
|
|
88
|
-
if (path
|
|
90
|
+
if (!path || paths.includes(path)) return;
|
|
91
|
+
if (path === 'ffmpeg' || existsSync(path)) paths.push(path);
|
|
89
92
|
};
|
|
90
93
|
add(process.env.LIVEDESK_FFMPEG);
|
|
91
94
|
try { add(require('@ffmpeg-installer/ffmpeg')?.path); } catch {}
|
|
@@ -130,12 +133,14 @@ function startEncoder() {
|
|
|
130
133
|
'-bsf:v', 'h264_metadata=aud=insert', '-f', 'h264', 'pipe:1'
|
|
131
134
|
];
|
|
132
135
|
const child = spawn(candidate.path, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
133
|
-
encoder = { child, candidate };
|
|
136
|
+
encoder = { child, candidate, inputSeen: false };
|
|
134
137
|
encoderOutputSeen = false;
|
|
135
138
|
outputBuffer = Buffer.alloc(0);
|
|
136
139
|
child.stdout.on('data', appendEncodedBytes);
|
|
137
140
|
child.stdin.on('error', error => {
|
|
138
|
-
|
|
141
|
+
if (encoder?.child === child && !isClosedPipeError(error)) {
|
|
142
|
+
send({ type: 'log', level: 'warn', message: `Mode 4 ${candidate.name} input: ${error.message}` });
|
|
143
|
+
}
|
|
139
144
|
});
|
|
140
145
|
child.stderr.on('data', chunk => {
|
|
141
146
|
const message = String(chunk || '').trim();
|
|
@@ -153,14 +158,24 @@ function startEncoder() {
|
|
|
153
158
|
clearTimeout(encoderRestartTimer);
|
|
154
159
|
encoderRestartTimer = setTimeout(startEncoder, encoderOutputSeen ? 800 : 120);
|
|
155
160
|
});
|
|
161
|
+
send({ type: 'status', state: 'encoder-starting', encoder: candidate.name });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isClosedPipeError(error) {
|
|
165
|
+
const code = String(error?.code || '').toUpperCase();
|
|
166
|
+
const message = String(error?.message || '').toLowerCase();
|
|
167
|
+
return code === 'EPIPE' || code === 'EOF' || code === 'ERR_STREAM_DESTROYED'
|
|
168
|
+
|| message.includes('write eof') || message.includes('broken pipe');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function armEncoderStartupTimer(child, candidate) {
|
|
156
172
|
clearTimeout(encoderStartupTimer);
|
|
157
173
|
encoderStartupTimer = setTimeout(() => {
|
|
158
|
-
if (encoder?.child === child && !encoderOutputSeen) {
|
|
159
|
-
send({ type: 'log', level: 'warn', message: `Mode 4 ${candidate.name}: no output
|
|
174
|
+
if (encoder?.child === child && encoder.inputSeen && !encoderOutputSeen) {
|
|
175
|
+
send({ type: 'log', level: 'warn', message: `Mode 4 ${candidate.name}: no output after receiving input; trying the next encoder.` });
|
|
160
176
|
try { child.kill(); } catch {}
|
|
161
177
|
}
|
|
162
|
-
},
|
|
163
|
-
send({ type: 'status', state: 'encoder-starting', encoder: candidate.name });
|
|
178
|
+
}, ENCODER_STARTUP_OUTPUT_TIMEOUT_MS);
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
function stopEncoder() {
|
|
@@ -306,6 +321,7 @@ function ingestFrame(message) {
|
|
|
306
321
|
const rgb565 = decodeLzo1xBlock(new Uint8Array(compressed), expectedLength);
|
|
307
322
|
const tileState = { width, height, rgb565, rendered: null, frameSeq: Number(message.frameSeq || 0) };
|
|
308
323
|
latestTiles.set(deviceId, tileState);
|
|
324
|
+
if (!encoder && encoderCandidates.length > 0) startEncoder();
|
|
309
325
|
if (config.deviceIds.length > 0 && config.deviceIds.every(id => latestTiles.has(id))) {
|
|
310
326
|
clearTimeout(inputWaitTimer);
|
|
311
327
|
inputWaitTimer = null;
|
|
@@ -319,6 +335,10 @@ function ingestFrame(message) {
|
|
|
319
335
|
|
|
320
336
|
function writeAtlasFrame() {
|
|
321
337
|
if (latestTiles.size === 0) return;
|
|
338
|
+
if (!encoder) {
|
|
339
|
+
startEncoder();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
322
342
|
const composeStartedAt = performance.now();
|
|
323
343
|
const target = framePool.pop();
|
|
324
344
|
const stdin = encoder?.child?.stdin;
|
|
@@ -340,13 +360,20 @@ function writeAtlasFrame() {
|
|
|
340
360
|
}
|
|
341
361
|
lastComposeMs = Math.max(0, performance.now() - composeStartedAt);
|
|
342
362
|
try {
|
|
363
|
+
const encoderEntry = encoder;
|
|
364
|
+
if (!encoderEntry.inputSeen) {
|
|
365
|
+
encoderEntry.inputSeen = true;
|
|
366
|
+
armEncoderStartupTimer(encoderEntry.child, encoderEntry.candidate);
|
|
367
|
+
}
|
|
343
368
|
stdin.write(target, error => {
|
|
344
369
|
framePool.push(target);
|
|
345
|
-
if (error
|
|
370
|
+
if (error && encoder?.child === encoderEntry.child && !isClosedPipeError(error)) {
|
|
371
|
+
send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
|
|
372
|
+
}
|
|
346
373
|
});
|
|
347
374
|
} catch (error) {
|
|
348
375
|
framePool.push(target);
|
|
349
|
-
send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
|
|
376
|
+
if (!isClosedPipeError(error)) send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
|
|
350
377
|
}
|
|
351
378
|
}
|
|
352
379
|
|
|
@@ -386,7 +413,7 @@ function configure(value) {
|
|
|
386
413
|
stopEncoder();
|
|
387
414
|
encoderCandidates = buildEncoderCandidates();
|
|
388
415
|
encoderCandidateIndex = 0;
|
|
389
|
-
startEncoder();
|
|
416
|
+
if (latestTiles.size > 0) startEncoder();
|
|
390
417
|
}
|
|
391
418
|
startComposeClock();
|
|
392
419
|
}
|
package/hub/src/server.js
CHANGED
|
@@ -78,6 +78,11 @@ function handleRemoteHubEvent(type, event) {
|
|
|
78
78
|
if (!deviceId) {
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
for (const ws of atlasClients.keys()) {
|
|
82
|
+
if (ws.readyState === 1 && ws.liveDeskAtlasDeviceIds?.has(deviceId)) {
|
|
83
|
+
startMode4AtlasInput(ws, deviceId, 'device-connected');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
81
86
|
const clients = new Set([
|
|
82
87
|
...(frameClientsByDeviceId.get(deviceId) || []),
|
|
83
88
|
...frameWildcardClients
|
|
@@ -1012,6 +1017,7 @@ function configureMode4Atlas(ws, payload = {}) {
|
|
|
1012
1017
|
const tileWidth = sharpTiles ? 480 : 320;
|
|
1013
1018
|
const tileHeight = sharpTiles ? 270 : 180;
|
|
1014
1019
|
ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
|
|
1020
|
+
ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, monitorSelections };
|
|
1015
1021
|
ws.liveDeskAtlasSession.configure({
|
|
1016
1022
|
deviceIds,
|
|
1017
1023
|
width: clampNumber(payload.width, 640, 3840, 1920),
|
|
@@ -1021,18 +1027,7 @@ function configureMode4Atlas(ws, payload = {}) {
|
|
|
1021
1027
|
fps: clampNumber(payload.fps, 1, 30, 20)
|
|
1022
1028
|
});
|
|
1023
1029
|
for (const deviceId of deviceIds) {
|
|
1024
|
-
|
|
1025
|
-
fps: 8,
|
|
1026
|
-
maxWidth: tileWidth,
|
|
1027
|
-
maxHeight: tileHeight,
|
|
1028
|
-
quality: 60,
|
|
1029
|
-
mode: 'mode2-lzo',
|
|
1030
|
-
frameMode: 'mode2-lzo',
|
|
1031
|
-
streamPurpose: 'atlas',
|
|
1032
|
-
monitorIndex: Number(monitorSelections[deviceId] || 0),
|
|
1033
|
-
reuseExisting: false,
|
|
1034
|
-
silentReuse: true
|
|
1035
|
-
});
|
|
1030
|
+
startMode4AtlasInput(ws, deviceId, 'atlas-configure');
|
|
1036
1031
|
}
|
|
1037
1032
|
sendJson(ws, {
|
|
1038
1033
|
type: 'Mode4AtlasSubscription',
|
|
@@ -1045,6 +1040,24 @@ function configureMode4Atlas(ws, payload = {}) {
|
|
|
1045
1040
|
});
|
|
1046
1041
|
}
|
|
1047
1042
|
|
|
1043
|
+
function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
|
|
1044
|
+
const options = ws?.liveDeskAtlasInputOptions;
|
|
1045
|
+
if (!options || !ws.liveDeskAtlasDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
|
|
1046
|
+
return remoteHub.startLiveStream(deviceId, {
|
|
1047
|
+
fps: 8,
|
|
1048
|
+
maxWidth: options.tileWidth,
|
|
1049
|
+
maxHeight: options.tileHeight,
|
|
1050
|
+
quality: 60,
|
|
1051
|
+
mode: 'mode2-lzo',
|
|
1052
|
+
frameMode: 'mode2-lzo',
|
|
1053
|
+
streamPurpose: 'atlas',
|
|
1054
|
+
monitorIndex: Number(options.monitorSelections?.[deviceId] || 0),
|
|
1055
|
+
reuseExisting: false,
|
|
1056
|
+
silentReuse: true,
|
|
1057
|
+
restartReason: reason
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1048
1061
|
function wantsBinaryFrame(req) {
|
|
1049
1062
|
return /^(1|true|yes|on|binary|raw)$/i.test(String(req.query?.binary ?? req.query?.raw ?? ''))
|
|
1050
1063
|
|| /image\//i.test(String(req.headers.accept || ''));
|
|
@@ -1619,6 +1632,8 @@ await remoteHub.start();
|
|
|
1619
1632
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
1620
1633
|
httpServer.listen(httpPort, httpHost, () => {
|
|
1621
1634
|
const status = remoteHub.getStatus({ includeSecrets: true });
|
|
1635
|
+
const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
|
|
1636
|
+
console.log(`[LiveDesk Hub] Version ${managerVersion}`);
|
|
1622
1637
|
console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
1623
1638
|
console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
1624
1639
|
});
|