livedesk 0.1.176 → 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 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 && !paths.includes(path)) paths.push(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
- send({ type: 'log', level: 'warn', message: `Mode 4 ${candidate.name} input: ${error.message}` });
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 during startup; trying the next encoder.` });
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
- }, 3500);
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) send({ type: 'log', level: 'warn', message: `Mode 4 encoder write: ${error.message}` });
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
- remoteHub.startLiveStream(deviceId, {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.176",
3
+ "version": "0.1.178",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {