livedesk 0.1.430 → 0.1.432
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 +23 -16
- package/bootstrap/legacy-client-update.js +25 -17
- package/client/bin/livedesk-client-node.js +8 -13
- package/client/package.json +5 -5
- package/client/src/runtime/client-runtime-server.js +32 -2
- package/hub/src/live-desk-update.js +1 -1
- package/hub/src/remote-hub.js +100 -24
- package/hub/src/server.js +15 -5
- package/package.json +6 -6
- package/web/dist/assets/index-DNSD69O9.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-D6ZESHC0.js +0 -25
package/bin/livedesk.js
CHANGED
|
@@ -5,7 +5,7 @@ import net from 'node:net';
|
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { execFile, spawn } from 'node:child_process';
|
|
8
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { resolveDeviceRole, writeRoleCache } from '../bootstrap/device-role.js';
|
|
@@ -62,17 +62,12 @@ function prepareUpdateNeutralCwd(kind, operationId) {
|
|
|
62
62
|
`${safeUpdatePathSegment(kind, 'update')}-${safeUpdatePathSegment(operationId)}`
|
|
63
63
|
);
|
|
64
64
|
mkdirSync(neutralCwd, { recursive: true });
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
const parent = dirname(ancestor);
|
|
75
|
-
if (parent === ancestor) break;
|
|
65
|
+
const unexpectedEntry = readdirSync(neutralCwd)[0];
|
|
66
|
+
if (unexpectedEntry) {
|
|
67
|
+
const shadowPath = join(neutralCwd, unexpectedEntry);
|
|
68
|
+
throw new Error(
|
|
69
|
+
`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`
|
|
70
|
+
);
|
|
76
71
|
}
|
|
77
72
|
return neutralCwd;
|
|
78
73
|
}
|
|
@@ -94,6 +89,7 @@ function buildNeutralNpxEnvironment(baseEnv, neutralCwd) {
|
|
|
94
89
|
}
|
|
95
90
|
env.INIT_CWD = neutralCwd;
|
|
96
91
|
env.npm_config_local_prefix = neutralCwd;
|
|
92
|
+
env.npm_config_workspaces = 'false';
|
|
97
93
|
env.npm_config_include_workspace_root = 'false';
|
|
98
94
|
return env;
|
|
99
95
|
}
|
|
@@ -1037,7 +1033,7 @@ function resolveNpxInvocation(npxArgs) {
|
|
|
1037
1033
|
function buildHubRestartBootstrapScript() {
|
|
1038
1034
|
return [
|
|
1039
1035
|
'const { execFile, spawn } = require("node:child_process");',
|
|
1040
|
-
'const { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } = require("node:fs");',
|
|
1036
|
+
'const { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } = require("node:fs");',
|
|
1041
1037
|
'const { dirname, join } = require("node:path");',
|
|
1042
1038
|
'const net = require("node:net");',
|
|
1043
1039
|
'const waitPid = Number(process.env.LIVEDESK_RESTART_WAIT_PID || 0);',
|
|
@@ -1077,8 +1073,8 @@ function buildHubRestartBootstrapScript() {
|
|
|
1077
1073
|
'const healthTimeoutMs = terminationFailureOperationId === operationId ? numberFromEnv("LIVEDESK_TEST_HUB_RESTART_TERMINATION_HEALTH_TIMEOUT_MS", 1000, 250) : configuredHealthTimeoutMs;',
|
|
1078
1074
|
'const pollMs = numberFromEnv("LIVEDESK_RESTART_HEALTH_POLL_MS", 250, 50);',
|
|
1079
1075
|
'const stabilityMs = numberFromEnv("LIVEDESK_RESTART_STABILITY_MS", 5000, 5000);',
|
|
1080
|
-
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk Hub update neutral working directory is unavailable"); mkdirSync(neutralCwd, { recursive: true });
|
|
1081
|
-
'const buildLaunchEnv = () => { const env = { ...process.env }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_include_workspace_root = "false"; return env; };',
|
|
1076
|
+
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk Hub update neutral working directory is unavailable"); mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = join(neutralCwd, unexpectedEntry); throw new Error(`LiveDesk Hub update neutral working directory is shadowed by ${shadow}: ${neutralCwd}`); } };',
|
|
1077
|
+
'const buildLaunchEnv = () => { const env = { ...process.env }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_workspaces = "false"; env.npm_config_include_workspace_root = "false"; return env; };',
|
|
1082
1078
|
'const requestJson = async path => { const response = await fetch(new URL(path, healthBaseUrl), { cache: "no-store", signal: AbortSignal.timeout(Math.min(2000, healthTimeoutMs)) }); if (!response.ok) throw new Error(`HTTP ${response.status} from ${path}`); return response.json(); };',
|
|
1083
1079
|
'const probeRemotePort = () => new Promise((resolve, reject) => { const socket = net.createConnection({ host: remoteHost, port: remotePort }); const timer = setTimeout(() => socket.destroy(new Error("TCP probe timed out")), Math.min(1500, healthTimeoutMs)); timer.unref?.(); socket.once("connect", () => { clearTimeout(timer); socket.destroy(); resolve(); }); socket.once("error", error => { clearTimeout(timer); reject(error); }); });',
|
|
1084
1080
|
'const probeReplacement = async requiredVersion => { const [status, health] = await Promise.all([requestJson("/api/runtime/status"), requestJson("/api/health")]); const runtimePid = Number(status?.runtimePid || 0); if (status?.role !== "hub" || status?.runtimeStarted !== true || runtimePid <= 1) throw new Error("replacement runtime status is not ready"); if (previousRuntimePid > 1 && runtimePid === previousRuntimePid) throw new Error(`old runtime pid=${runtimePid} is still answering`); if (requiredVersion && String(status?.appVersion || "") !== requiredVersion) throw new Error(`version mismatch expected=${requiredVersion} actual=${status?.appVersion || "unknown"}`); if (health?.ok !== true || health?.product !== "LiveDesk") throw new Error("replacement health response is invalid"); await probeRemotePort(); return { runtimePid, appVersion: String(status?.appVersion || "") }; };',
|
|
@@ -1192,7 +1188,18 @@ async function runManager(args, resolvedRole = null) {
|
|
|
1192
1188
|
|| prepareUpdateNeutralCwd('hub', operationId)
|
|
1193
1189
|
);
|
|
1194
1190
|
const packageTarget = `livedesk@${requestedVersion}`;
|
|
1195
|
-
const nextArgs = [
|
|
1191
|
+
const nextArgs = [
|
|
1192
|
+
'-y',
|
|
1193
|
+
'--prefer-online',
|
|
1194
|
+
'--prefix',
|
|
1195
|
+
neutralCwd,
|
|
1196
|
+
'--workspaces=false',
|
|
1197
|
+
packageTarget,
|
|
1198
|
+
'--force-role',
|
|
1199
|
+
'hub',
|
|
1200
|
+
'--no-open',
|
|
1201
|
+
...args
|
|
1202
|
+
];
|
|
1196
1203
|
const restartInvocation = resolveNpxInvocation(nextArgs);
|
|
1197
1204
|
const fallbackInvocation = {
|
|
1198
1205
|
command: process.execPath,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
mkdirSync,
|
|
8
8
|
openSync,
|
|
9
9
|
readFileSync,
|
|
10
|
+
readdirSync,
|
|
10
11
|
renameSync,
|
|
11
12
|
rmSync,
|
|
12
13
|
statSync,
|
|
@@ -138,18 +139,12 @@ function prepareNeutralCwd(cwd) {
|
|
|
138
139
|
if (!configuredCwd) throw new Error('LiveDesk update neutral working directory is unavailable');
|
|
139
140
|
const neutralCwd = resolve(configuredCwd);
|
|
140
141
|
mkdirSync(neutralCwd, { recursive: true });
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
throw new Error(
|
|
148
|
-
`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
const parent = dirname(ancestor);
|
|
152
|
-
if (parent === ancestor) break;
|
|
142
|
+
const unexpectedEntry = readdirSync(neutralCwd)[0];
|
|
143
|
+
if (unexpectedEntry) {
|
|
144
|
+
const shadowPath = join(neutralCwd, unexpectedEntry);
|
|
145
|
+
throw new Error(
|
|
146
|
+
`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`
|
|
147
|
+
);
|
|
153
148
|
}
|
|
154
149
|
return neutralCwd;
|
|
155
150
|
}
|
|
@@ -171,10 +166,23 @@ export function buildLegacyClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
|
|
|
171
166
|
}
|
|
172
167
|
env.INIT_CWD = neutralCwd;
|
|
173
168
|
env.npm_config_local_prefix = neutralCwd;
|
|
169
|
+
env.npm_config_workspaces = 'false';
|
|
174
170
|
env.npm_config_include_workspace_root = 'false';
|
|
175
171
|
return env;
|
|
176
172
|
}
|
|
177
173
|
|
|
174
|
+
function buildExactNpxArgs(neutralCwd, preference, version, args = []) {
|
|
175
|
+
return [
|
|
176
|
+
'-y',
|
|
177
|
+
preference,
|
|
178
|
+
'--prefix',
|
|
179
|
+
neutralCwd,
|
|
180
|
+
'--workspaces=false',
|
|
181
|
+
`livedesk@${version}`,
|
|
182
|
+
...args
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
178
186
|
function positivePid(value) {
|
|
179
187
|
const parsed = Number(value);
|
|
180
188
|
return Number.isInteger(parsed) && parsed > 1 ? parsed : 0;
|
|
@@ -1011,11 +1019,11 @@ export function prefetchExactNpxRestore(config, env = process.env, options = {})
|
|
|
1011
1019
|
if (!version) {
|
|
1012
1020
|
throw new Error('LiveDesk cannot prefetch an exact restore without the current product version.');
|
|
1013
1021
|
}
|
|
1022
|
+
const neutralCwd = prepareNeutralCwd(config?.neutralCwd);
|
|
1014
1023
|
const invocation = resolveNpxInvocation(
|
|
1015
|
-
|
|
1024
|
+
buildExactNpxArgs(neutralCwd, '--prefer-online', version, ['--version']),
|
|
1016
1025
|
env
|
|
1017
1026
|
);
|
|
1018
|
-
const neutralCwd = prepareNeutralCwd(config?.neutralCwd);
|
|
1019
1027
|
const result = (options.spawnSyncImpl || spawnSync)(
|
|
1020
1028
|
invocation.command,
|
|
1021
1029
|
invocation.args,
|
|
@@ -1067,12 +1075,12 @@ export function spawnReplacement(version, preference, env = process.env, restart
|
|
|
1067
1075
|
const isLocalRestore = env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_KIND === 'restore'
|
|
1068
1076
|
&& localRestoreLauncher
|
|
1069
1077
|
&& existsSync(localRestoreLauncher);
|
|
1070
|
-
const invocation = isLocalRestore
|
|
1071
|
-
? { command: process.execPath, args: [localRestoreLauncher, ...roleArgs] }
|
|
1072
|
-
: resolveNpxInvocation(['-y', preference, `livedesk@${version}`, ...roleArgs], env);
|
|
1073
1078
|
const neutralCwd = isLocalRestore
|
|
1074
1079
|
? ''
|
|
1075
1080
|
: prepareNeutralCwd(env.LIVEDESK_UPDATE_NEUTRAL_CWD);
|
|
1081
|
+
const invocation = isLocalRestore
|
|
1082
|
+
? { command: process.execPath, args: [localRestoreLauncher, ...roleArgs] }
|
|
1083
|
+
: resolveNpxInvocation(buildExactNpxArgs(neutralCwd, preference, version, roleArgs), env);
|
|
1076
1084
|
const replacementEnv = isLocalRestore
|
|
1077
1085
|
? { ...env }
|
|
1078
1086
|
: buildLegacyClientUpdateNpxEnvironment(env, neutralCwd);
|
|
@@ -58,16 +58,10 @@ function getClientUpdateNeutralCwd(operationId) {
|
|
|
58
58
|
async function prepareClientUpdateNeutralCwd(operationId) {
|
|
59
59
|
const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
|
|
60
60
|
await fs.mkdir(neutralCwd, { recursive: true });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
].find(candidate => existsSync(candidate));
|
|
66
|
-
if (shadowPath) {
|
|
67
|
-
throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
68
|
-
}
|
|
69
|
-
const parent = path.dirname(ancestor);
|
|
70
|
-
if (parent === ancestor) break;
|
|
61
|
+
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
62
|
+
if (unexpectedEntry) {
|
|
63
|
+
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
64
|
+
throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
71
65
|
}
|
|
72
66
|
return neutralCwd;
|
|
73
67
|
}
|
|
@@ -89,6 +83,7 @@ function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
|
|
|
89
83
|
}
|
|
90
84
|
env.INIT_CWD = neutralCwd;
|
|
91
85
|
env.npm_config_local_prefix = neutralCwd;
|
|
86
|
+
env.npm_config_workspaces = 'false';
|
|
92
87
|
env.npm_config_include_workspace_root = 'false';
|
|
93
88
|
return env;
|
|
94
89
|
}
|
|
@@ -1583,13 +1578,13 @@ function buildClientUpdateBootstrapScript() {
|
|
|
1583
1578
|
'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { await new Promise(resolve => { const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); let done = false; const finish = () => { if (done) return; done = true; clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { try { killer.kill("SIGKILL"); } catch {} finish(); }, 10000); killer.once("error", finish); killer.once("exit", finish); }); if (isAlive(pid)) { try { child.kill("SIGKILL"); } catch {} } return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
|
|
1584
1579
|
'const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });',
|
|
1585
1580
|
'const monitorPreflight = child => { let settled = false; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const inspect = () => { const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") finish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); while (!settled) { inspect(); if (settled) return; let outcome; try { outcome = writeCancellation(error); } catch (lockError) { finish(lockError); return; } if (outcome === "ready" || outcome === "failed") { inspect(); if (settled) return; } else if (outcome === "cancelled" || outcome === "superseded") { await terminateTree(child); finish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff(); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (!settled) finish(new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ").")); }); inspect(); };',
|
|
1586
|
-
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true });
|
|
1581
|
+
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = fs.readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = path.join(neutralCwd, unexpectedEntry); throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); } };',
|
|
1587
1582
|
'if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();',
|
|
1588
|
-
'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_include_workspace_root = "false";',
|
|
1583
|
+
'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_workspaces = "false"; env.npm_config_include_workspace_root = "false";',
|
|
1589
1584
|
'const nodeDir = path.dirname(process.execPath);',
|
|
1590
1585
|
'const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));',
|
|
1591
1586
|
'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
|
|
1592
|
-
'const args = ["-y", "--prefer-online", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
|
|
1587
|
+
'const args = ["-y", "--prefer-online", "--prefix", neutralCwd, "--workspaces=false", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
|
|
1593
1588
|
'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
|
|
1594
1589
|
'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
|
|
1595
1590
|
'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.190",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"ws": "^8.18.3"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
47
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
44
|
+
"@livedesk/fast-linux-x64": "0.1.397",
|
|
45
|
+
"@livedesk/fast-osx-arm64": "0.1.397",
|
|
46
|
+
"@livedesk/fast-osx-x64": "0.1.397",
|
|
47
|
+
"@livedesk/fast-win-x64": "0.1.397"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|
|
@@ -49,7 +49,20 @@ function normalizeAccountAvatarUrl(value) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function readAccessTokenProfile(accessToken) {
|
|
53
|
+
const token = normalizeString(accessToken, 8192);
|
|
54
|
+
const payloadSegment = token.split('.')[1] || '';
|
|
55
|
+
if (!payloadSegment) return {};
|
|
56
|
+
try {
|
|
57
|
+
const payload = JSON.parse(Buffer.from(payloadSegment, 'base64url').toString('utf8'));
|
|
58
|
+
return payload && typeof payload === 'object' ? payload : {};
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
function readClientAccountProfile(sessionOrUser) {
|
|
65
|
+
const tokenProfile = readAccessTokenProfile(sessionOrUser?.access_token);
|
|
53
66
|
const user = sessionOrUser?.user && typeof sessionOrUser.user === 'object'
|
|
54
67
|
? sessionOrUser.user
|
|
55
68
|
: sessionOrUser && typeof sessionOrUser === 'object'
|
|
@@ -58,12 +71,19 @@ function readClientAccountProfile(sessionOrUser) {
|
|
|
58
71
|
const metadata = user.user_metadata && typeof user.user_metadata === 'object'
|
|
59
72
|
? user.user_metadata
|
|
60
73
|
: {};
|
|
61
|
-
const
|
|
74
|
+
const tokenMetadata = tokenProfile.user_metadata && typeof tokenProfile.user_metadata === 'object'
|
|
75
|
+
? tokenProfile.user_metadata
|
|
76
|
+
: {};
|
|
77
|
+
const email = normalizeString(user.email || tokenProfile.email, 320);
|
|
62
78
|
const name = normalizeString(
|
|
63
79
|
metadata.full_name
|
|
64
80
|
|| metadata.name
|
|
65
81
|
|| metadata.preferred_username
|
|
82
|
+
|| tokenMetadata.full_name
|
|
83
|
+
|| tokenMetadata.name
|
|
84
|
+
|| tokenMetadata.preferred_username
|
|
66
85
|
|| user.name
|
|
86
|
+
|| tokenProfile.name
|
|
67
87
|
|| email
|
|
68
88
|
|| user.id,
|
|
69
89
|
160
|
|
@@ -71,6 +91,8 @@ function readClientAccountProfile(sessionOrUser) {
|
|
|
71
91
|
const avatarUrl = normalizeAccountAvatarUrl(
|
|
72
92
|
metadata.avatar_url
|
|
73
93
|
|| metadata.picture
|
|
94
|
+
|| tokenMetadata.avatar_url
|
|
95
|
+
|| tokenMetadata.picture
|
|
74
96
|
|| user.avatar_url
|
|
75
97
|
|| user.picture
|
|
76
98
|
);
|
|
@@ -79,7 +101,11 @@ function readClientAccountProfile(sessionOrUser) {
|
|
|
79
101
|
|
|
80
102
|
function mergeClientAccountProfile(session, sourceUser) {
|
|
81
103
|
const profile = readClientAccountProfile(sourceUser);
|
|
104
|
+
const existingUserMetadata = session?.user?.user_metadata && typeof session.user.user_metadata === 'object'
|
|
105
|
+
? session.user.user_metadata
|
|
106
|
+
: {};
|
|
82
107
|
const userMetadata = {
|
|
108
|
+
...existingUserMetadata,
|
|
83
109
|
...(profile.name ? { full_name: profile.name } : {}),
|
|
84
110
|
...(profile.avatarUrl ? { avatar_url: profile.avatarUrl } : {})
|
|
85
111
|
};
|
|
@@ -1275,7 +1301,11 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1275
1301
|
setImmediate(() => {
|
|
1276
1302
|
void (async () => {
|
|
1277
1303
|
try {
|
|
1278
|
-
|
|
1304
|
+
// Older persisted sessions kept only id/email on `user`, while
|
|
1305
|
+
// Supabase still carries Google name/avatar claims in the
|
|
1306
|
+
// access-token payload. Recover those display-only claims
|
|
1307
|
+
// locally before any best-effort network hydration.
|
|
1308
|
+
const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
|
|
1279
1309
|
const hydratedSession = await hydrateClientAccountProfile(normalizedSession);
|
|
1280
1310
|
if (!writeSavedSession(hydratedSession)) throw new Error('refresh-token-required');
|
|
1281
1311
|
state.auth.persisted = true;
|
|
@@ -71,7 +71,7 @@ function encodePowerShell(value) {
|
|
|
71
71
|
|
|
72
72
|
function buildLegacyPackageSupervisorStarterSource() {
|
|
73
73
|
const source = [
|
|
74
|
-
"const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(require('node:os').tmpdir(),'livedesk-update-cwd','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);
|
|
74
|
+
"const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(require('node:os').tmpdir(),'livedesk-update-cwd','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','--prefix',w,'--workspaces=false','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);Object.keys(e).forEach(k=>/^(INIT_CWD|npm_config_(local_prefix|workspaces?|include_workspace_root))$/i.test(k)&&Reflect.deleteProperty(e,k));Object.assign(e,{INIT_CWD:w,npm_config_local_prefix:w,npm_config_workspaces:'false',npm_config_include_workspace_root:'false',LIVEDESK_UPDATE_NEUTRAL_CWD:w});e.i=x?{c:j,a:[x,...a]}:s==='win32'?{c:e.ComSpec||'cmd.exe',a:['/d','/s','/c','call'+z+q(n)+z+a.map(q).join(z)]}:{c:n,a};e.r=spawn(e.i.c,e.i.a,{cwd:w,env:e,detached:true,stdio:'ignore',windowsHide:true});",
|
|
75
75
|
"e.r.on('error',()=>process.exit(1));e.r.unref();"
|
|
76
76
|
].join('');
|
|
77
77
|
if (source.includes(' ')) throw new Error('LiveDesk legacy starter source must not contain spaces.');
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -685,7 +685,9 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
685
685
|
const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
|
|
686
686
|
return {
|
|
687
687
|
type,
|
|
688
|
-
|
|
688
|
+
// A missing monitor is not equivalent to the primary display. Control
|
|
689
|
+
// input must prove the exact monitor owned by its capture generation.
|
|
690
|
+
monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : null,
|
|
689
691
|
normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
|
|
690
692
|
normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
|
|
691
693
|
button: safeString(input.button || input.Button, 24),
|
|
@@ -2144,9 +2146,12 @@ export function createRemoteHub(options = {}) {
|
|
|
2144
2146
|
byteLength: frame.byteLength
|
|
2145
2147
|
});
|
|
2146
2148
|
streamState.latestFrame = frame;
|
|
2147
|
-
streamState.lastFrameAt = frame.receivedAt;
|
|
2148
|
-
streamState.lastFrameSeq = frame.frameSeq;
|
|
2149
|
-
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
2149
|
+
streamState.lastFrameAt = frame.receivedAt;
|
|
2150
|
+
streamState.lastFrameSeq = frame.frameSeq;
|
|
2151
|
+
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
2152
|
+
// A synthetic PNG is a complete independently decodable surface and
|
|
2153
|
+
// therefore key-frame equivalent for Control readiness.
|
|
2154
|
+
streamState.readyFrameReceived = true;
|
|
2150
2155
|
device.lastSeenAt = frame.receivedAt;
|
|
2151
2156
|
device.counters.liveFramesReceived += 1;
|
|
2152
2157
|
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
@@ -3337,6 +3342,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3337
3342
|
|| result.InputType
|
|
3338
3343
|
|| pending?.inputType,
|
|
3339
3344
|
48),
|
|
3345
|
+
monitorIndex: Number(
|
|
3346
|
+
message.monitorIndex
|
|
3347
|
+
?? message.MonitorIndex
|
|
3348
|
+
?? result.monitorIndex
|
|
3349
|
+
?? result.MonitorIndex
|
|
3350
|
+
?? pending?.monitorIndex
|
|
3351
|
+
?? 0) || 0,
|
|
3340
3352
|
issuedAtEpochMs: Number(
|
|
3341
3353
|
message.issuedAtEpochMs
|
|
3342
3354
|
?? message.IssuedAtEpochMs
|
|
@@ -3602,13 +3614,35 @@ export function createRemoteHub(options = {}) {
|
|
|
3602
3614
|
: 0;
|
|
3603
3615
|
}
|
|
3604
3616
|
|
|
3605
|
-
function readFrameStageMs(message, property) {
|
|
3606
|
-
const value = Number(message?.[property]);
|
|
3607
|
-
return Number.isFinite(value) && value >= 0
|
|
3608
|
-
? Math.round(value)
|
|
3609
|
-
: 0;
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3617
|
+
function readFrameStageMs(message, property) {
|
|
3618
|
+
const value = Number(message?.[property]);
|
|
3619
|
+
return Number.isFinite(value) && value >= 0
|
|
3620
|
+
? Math.round(value)
|
|
3621
|
+
: 0;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
function readOptionalFrameStageMs(message, property) {
|
|
3625
|
+
const rawValue = message?.[property];
|
|
3626
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
3627
|
+
return undefined;
|
|
3628
|
+
}
|
|
3629
|
+
const value = Number(rawValue);
|
|
3630
|
+
return Number.isFinite(value) && value >= 0
|
|
3631
|
+
? Math.round(value)
|
|
3632
|
+
: undefined;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
function hasFrameCaptureTiming(message) {
|
|
3636
|
+
if (message?.captureTimingAvailable === false) {
|
|
3637
|
+
return false;
|
|
3638
|
+
}
|
|
3639
|
+
if (message?.captureTimingAvailable === true) {
|
|
3640
|
+
return true;
|
|
3641
|
+
}
|
|
3642
|
+
return ['captureP95Ms', 'captureAverageMs', 'captureStageMs']
|
|
3643
|
+
.some(property => readOptionalFrameStageMs(message, property) !== undefined);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3612
3646
|
function computeSameContentStreak(previousFrame, contentHash) {
|
|
3613
3647
|
if (!contentHash || !previousFrame?.contentHash || previousFrame.contentHash !== contentHash) {
|
|
3614
3648
|
return 0;
|
|
@@ -3682,7 +3716,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3682
3716
|
byteLength,
|
|
3683
3717
|
transport,
|
|
3684
3718
|
contentHash,
|
|
3685
|
-
captureMs: readFrameCaptureMs(message),
|
|
3719
|
+
captureMs: readFrameCaptureMs(message),
|
|
3686
3720
|
captureStageMs: readFrameStageMs(message, 'captureStageMs'),
|
|
3687
3721
|
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
3688
3722
|
compressMs: readFrameStageMs(message, 'compressMs'),
|
|
@@ -3774,6 +3808,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3774
3808
|
lastFrameAt: '',
|
|
3775
3809
|
lastFrameSeq: 0,
|
|
3776
3810
|
framesReceived: 0,
|
|
3811
|
+
readyFrameReceived: false,
|
|
3777
3812
|
latestFrame: null,
|
|
3778
3813
|
stoppedAt: '',
|
|
3779
3814
|
stopReason: ''
|
|
@@ -3991,6 +4026,8 @@ export function createRemoteHub(options = {}) {
|
|
|
3991
4026
|
&& (activeCaptureGeneration <= 0
|
|
3992
4027
|
|| (frameCaptureGeneration > 0
|
|
3993
4028
|
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
4029
|
+
const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
|
|
4030
|
+
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
3994
4031
|
device.latestLiveFrame = {
|
|
3995
4032
|
streamId,
|
|
3996
4033
|
frameSeq,
|
|
@@ -4037,10 +4074,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4037
4074
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
4038
4075
|
handshake: transfer.handshake,
|
|
4039
4076
|
fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
|
|
4040
|
-
requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
|
|
4041
|
-
effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
|
|
4042
|
-
|
|
4043
|
-
|
|
4077
|
+
requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
|
|
4078
|
+
effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
|
|
4079
|
+
captureTimingAvailable,
|
|
4080
|
+
captureAverageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureAverageMs') : undefined,
|
|
4081
|
+
captureP95Ms: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureP95Ms') : undefined,
|
|
4044
4082
|
slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
|
|
4045
4083
|
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4046
4084
|
capturedAt,
|
|
@@ -4048,8 +4086,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4048
4086
|
byteLength,
|
|
4049
4087
|
transport,
|
|
4050
4088
|
contentHash,
|
|
4051
|
-
captureMs:
|
|
4052
|
-
captureStageMs:
|
|
4089
|
+
captureMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureMs') : undefined,
|
|
4090
|
+
captureStageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureStageMs') : undefined,
|
|
4053
4091
|
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
4054
4092
|
compressMs: readFrameStageMs(message, 'compressMs'),
|
|
4055
4093
|
captureSourceSerial: Number.isFinite(Number(message.captureSourceSerial)) ? Number(message.captureSourceSerial) : 0,
|
|
@@ -4073,9 +4111,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4073
4111
|
mimeType: device.latestLiveFrame.mimeType,
|
|
4074
4112
|
byteLength: device.latestLiveFrame.byteLength
|
|
4075
4113
|
});
|
|
4076
|
-
streamState.lastFrameAt = device.lastSeenAt;
|
|
4077
|
-
streamState.lastFrameSeq = frameSeq;
|
|
4078
|
-
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
4114
|
+
streamState.lastFrameAt = device.lastSeenAt;
|
|
4115
|
+
streamState.lastFrameSeq = frameSeq;
|
|
4116
|
+
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
4117
|
+
streamState.readyFrameReceived = readyFrameReceivedBeforeFrame
|
|
4118
|
+
|| (currentGenerationVerified
|
|
4119
|
+
&& liveFrameSatisfiesReadiness(streamState, device.latestLiveFrame));
|
|
4079
4120
|
streamState.mode = transfer.mode;
|
|
4080
4121
|
streamState.frameMode = transfer.frameMode;
|
|
4081
4122
|
streamState.frameProfile = transfer.frameProfile;
|
|
@@ -4093,7 +4134,9 @@ export function createRemoteHub(options = {}) {
|
|
|
4093
4134
|
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
4094
4135
|
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
4095
4136
|
device.counters.liveFramesReceived += 1;
|
|
4096
|
-
if (
|
|
4137
|
+
if (!readyFrameReceivedBeforeFrame
|
|
4138
|
+
&& streamState.readyFrameReceived === true
|
|
4139
|
+
&& liveStreamHasCurrentFrame(streamState)) {
|
|
4097
4140
|
emitRemoteEvent('RemoteLiveStreamReady', device, {
|
|
4098
4141
|
streamId,
|
|
4099
4142
|
commandId: streamState.commandId,
|
|
@@ -5292,6 +5335,15 @@ export function createRemoteHub(options = {}) {
|
|
|
5292
5335
|
activeCaptureGeneration: Number(controlStream.captureGeneration || 0)
|
|
5293
5336
|
};
|
|
5294
5337
|
}
|
|
5338
|
+
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
5339
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
5340
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
5341
|
+
return {
|
|
5342
|
+
ok: false,
|
|
5343
|
+
error: 'STALE_CONTROL_MONITOR',
|
|
5344
|
+
activeMonitorIndex
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5295
5347
|
|
|
5296
5348
|
const inputSocket = device.inputSocket;
|
|
5297
5349
|
if (inputSocket && !inputSocket.destroyed) {
|
|
@@ -5311,7 +5363,8 @@ export function createRemoteHub(options = {}) {
|
|
|
5311
5363
|
inputSocket: true,
|
|
5312
5364
|
sessionId: device.sessionId,
|
|
5313
5365
|
commandId: controlStream.commandId,
|
|
5314
|
-
captureGeneration: controlStream.captureGeneration
|
|
5366
|
+
captureGeneration: controlStream.captureGeneration,
|
|
5367
|
+
monitorIndex: activeMonitorIndex
|
|
5315
5368
|
};
|
|
5316
5369
|
}
|
|
5317
5370
|
|
|
@@ -5332,6 +5385,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5332
5385
|
commandId: fallback.commandId,
|
|
5333
5386
|
inputSeq: normalized.inputSeq,
|
|
5334
5387
|
inputType: normalized.type,
|
|
5388
|
+
monitorIndex: normalized.monitorIndex,
|
|
5335
5389
|
issuedAtEpochMs: normalized.issuedAtEpochMs,
|
|
5336
5390
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
5337
5391
|
hubForwardedAtEpochMs,
|
|
@@ -5348,6 +5402,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5348
5402
|
deliveryCommandId: fallback.commandId,
|
|
5349
5403
|
commandId: controlStream.commandId,
|
|
5350
5404
|
captureGeneration: controlStream.captureGeneration,
|
|
5405
|
+
monitorIndex: activeMonitorIndex,
|
|
5351
5406
|
inputSeq: normalized.inputSeq
|
|
5352
5407
|
}
|
|
5353
5408
|
: {
|
|
@@ -5855,12 +5910,31 @@ export function createRemoteHub(options = {}) {
|
|
|
5855
5910
|
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_REPLACEMENT_PENDING_MS;
|
|
5856
5911
|
}
|
|
5857
5912
|
|
|
5913
|
+
function liveStreamRequiresReadyKeyFrame(activeLiveStream) {
|
|
5914
|
+
if (safeString(activeLiveStream?.streamPurpose, 24).toLowerCase() !== 'control') {
|
|
5915
|
+
return false;
|
|
5916
|
+
}
|
|
5917
|
+
const codec = safeString(activeLiveStream?.codec, 80).toLowerCase();
|
|
5918
|
+
const mode = safeString(activeLiveStream?.frameMode || activeLiveStream?.mode, 80).toLowerCase();
|
|
5919
|
+
return codec.includes('h264') || mode === 'mode3-h264-hw';
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5922
|
+
function liveFrameSatisfiesReadiness(activeLiveStream, frame) {
|
|
5923
|
+
return !liveStreamRequiresReadyKeyFrame(activeLiveStream)
|
|
5924
|
+
|| frame?.isKeyFrame === true
|
|
5925
|
+
|| safeString(frame?.chunkType, 20).toLowerCase() === 'key';
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5858
5928
|
function liveStreamHasCurrentFrame(activeLiveStream) {
|
|
5859
5929
|
if (!activeLiveStream?.active
|
|
5860
5930
|
|| activeLiveStream.open !== true
|
|
5861
5931
|
|| Number(activeLiveStream.framesReceived || 0) < 1) {
|
|
5862
5932
|
return false;
|
|
5863
5933
|
}
|
|
5934
|
+
if (liveStreamRequiresReadyKeyFrame(activeLiveStream)
|
|
5935
|
+
&& activeLiveStream.readyFrameReceived !== true) {
|
|
5936
|
+
return false;
|
|
5937
|
+
}
|
|
5864
5938
|
const latestFrame = activeLiveStream.latestFrame;
|
|
5865
5939
|
if (!latestFrame || latestFrame.currentGenerationVerified !== true) {
|
|
5866
5940
|
return false;
|
|
@@ -5984,6 +6058,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5984
6058
|
lastFrameAt: '',
|
|
5985
6059
|
lastFrameSeq: 0,
|
|
5986
6060
|
framesReceived: 0,
|
|
6061
|
+
readyFrameReceived: false,
|
|
5987
6062
|
streamPurpose,
|
|
5988
6063
|
captureGeneration
|
|
5989
6064
|
});
|
|
@@ -6201,8 +6276,9 @@ export function createRemoteHub(options = {}) {
|
|
|
6201
6276
|
stoppedAt: '',
|
|
6202
6277
|
stopReason: '',
|
|
6203
6278
|
lastFrameAt: '',
|
|
6204
|
-
lastFrameSeq: 0,
|
|
6279
|
+
lastFrameSeq: 0,
|
|
6205
6280
|
framesReceived: 0,
|
|
6281
|
+
readyFrameReceived: false,
|
|
6206
6282
|
streamPurpose,
|
|
6207
6283
|
captureGeneration,
|
|
6208
6284
|
latestFrame: null,
|
package/hub/src/server.js
CHANGED
|
@@ -2045,8 +2045,13 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2045
2045
|
fps: Number(frame.fps || 0) || 0,
|
|
2046
2046
|
requestedFps: Number(frame.requestedFps || 0) || 0,
|
|
2047
2047
|
effectiveFps: Number(frame.effectiveFps || 0) || 0,
|
|
2048
|
-
|
|
2049
|
-
|
|
2048
|
+
captureTimingAvailable: frame.captureTimingAvailable === true,
|
|
2049
|
+
captureAverageMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureAverageMs))
|
|
2050
|
+
? Number(frame.captureAverageMs)
|
|
2051
|
+
: null,
|
|
2052
|
+
captureP95Ms: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureP95Ms))
|
|
2053
|
+
? Number(frame.captureP95Ms)
|
|
2054
|
+
: null,
|
|
2050
2055
|
slowFrameCount: Number(frame.slowFrameCount || 0) || 0,
|
|
2051
2056
|
captureHelperRestarts: Number(frame.captureHelperRestarts || 0) || 0,
|
|
2052
2057
|
capturedAt: frame.capturedAt || '',
|
|
@@ -2055,8 +2060,12 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2055
2060
|
hubPacketEpochMs,
|
|
2056
2061
|
byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
|
|
2057
2062
|
contentHash: frame.contentHash || '',
|
|
2058
|
-
captureMs: Number(frame.captureMs
|
|
2059
|
-
|
|
2063
|
+
captureMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureMs))
|
|
2064
|
+
? Number(frame.captureMs)
|
|
2065
|
+
: null,
|
|
2066
|
+
captureStageMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureStageMs))
|
|
2067
|
+
? Number(frame.captureStageMs)
|
|
2068
|
+
: null,
|
|
2060
2069
|
convertMs: Number(frame.convertMs || 0) || 0,
|
|
2061
2070
|
compressMs: Number(frame.compressMs || 0) || 0,
|
|
2062
2071
|
captureSourceSerial: Number(frame.captureSourceSerial || 0) || 0,
|
|
@@ -2157,7 +2166,8 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
2157
2166
|
|| Number(frame.monitorIndex || 0) !== expectedBinding.monitorIndex) {
|
|
2158
2167
|
continue;
|
|
2159
2168
|
}
|
|
2160
|
-
|
|
2169
|
+
const requiresReadyKeyFrame = String(expectedBinding.streamPurpose || '').toLowerCase() === 'control' && isH264;
|
|
2170
|
+
if (!expectedBinding.readySent && (!requiresReadyKeyFrame || isKeyFrame)) {
|
|
2161
2171
|
expectedBinding.readySent = sendJson(client, {
|
|
2162
2172
|
type: 'RemoteFrameStreamReady',
|
|
2163
2173
|
deviceId,
|