@livedesk/client 0.1.179 → 0.1.181
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 +3 -2
- package/bin/livedesk-client.js +214 -29
- package/package.json +5 -5
- package/src/runtime/client-runtime-server.js +134 -10
- package/src/runtime/linux-video-acceleration.js +390 -0
package/README.md
CHANGED
|
@@ -51,8 +51,9 @@ the matching callback URL to Supabase Auth redirect URLs.
|
|
|
51
51
|
The client registers the device, sends status heartbeats, can return
|
|
52
52
|
Hub-requested thumbnails, can stream a focused view-only live screen, and
|
|
53
53
|
can receive safe task-only instructions. The C# RemoteFast engine also supports
|
|
54
|
-
keyboard/mouse control on Windows, macOS, and Linux/X11, file transfer, and
|
|
55
|
-
|
|
54
|
+
keyboard/mouse control on Windows, macOS, and Linux/X11, file transfer, and
|
|
55
|
+
system-audio playback from Windows, macOS, and Linux. Linux captures the default
|
|
56
|
+
output through PipeWire `pw-record` or PulseAudio `parec`. macOS control requires Accessibility
|
|
56
57
|
permission for the terminal app that started LiveDesk; screen streaming and
|
|
57
58
|
system-audio capture require Screen Recording permission. LiveDesk checks these
|
|
58
59
|
permissions without opening repeated system dialogs and prints the matching
|
package/bin/livedesk-client.js
CHANGED
|
@@ -13,6 +13,10 @@ import { formatClientVersionBanner, readClientPackageVersion } from './client-ve
|
|
|
13
13
|
import { createClientRuntimeServer } from '../src/runtime/client-runtime-server.js';
|
|
14
14
|
import { installAgentTerminationHandlers } from '../src/runtime/agent-process-lifecycle.js';
|
|
15
15
|
import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
|
|
16
|
+
import {
|
|
17
|
+
inspectLinuxVideoAcceleration,
|
|
18
|
+
installLinuxVideoAcceleration
|
|
19
|
+
} from '../src/runtime/linux-video-acceleration.js';
|
|
16
20
|
import { normalizeRuntimeAuthSession } from '../../runtime-core/src/auth-session.js';
|
|
17
21
|
import { startRoleTransitionSupervisor } from '../../runtime-core/src/role-transition-supervisor.js';
|
|
18
22
|
|
|
@@ -48,10 +52,13 @@ const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
|
48
52
|
const CLIENT_HUB_TARGET_CACHE_STORAGE_KEY = 'livedesk.client.last-hub-target';
|
|
49
53
|
const DEVICE_ROLE_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-role.json');
|
|
50
54
|
const FAST_PREFLIGHT_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'fast-preflight.json');
|
|
55
|
+
const CLIENT_SLOT_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-slot.json');
|
|
51
56
|
const CLIENT_UPDATE_STATE_PATH = process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH
|
|
52
57
|
|| join(UNIFIED_CLIENT_STATE_DIR, 'client-update.json');
|
|
53
58
|
let activeAgentProcess = null;
|
|
54
59
|
let roleRestartRequest = null;
|
|
60
|
+
let agentRestartRequest = null;
|
|
61
|
+
let linuxVideoAccelerationStatus = null;
|
|
55
62
|
let discoveryWakeController = new AbortController();
|
|
56
63
|
let networkChangeMonitor = null;
|
|
57
64
|
const sessionRefreshesInFlight = new WeakMap();
|
|
@@ -436,10 +443,48 @@ function normalizeSlotNumber(value) {
|
|
|
436
443
|
if (!Number.isInteger(number) || number < 1 || number > 999) {
|
|
437
444
|
return '';
|
|
438
445
|
}
|
|
439
|
-
return String(number);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function
|
|
446
|
+
return String(number);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function readSavedDeviceSlot(deviceId = '') {
|
|
450
|
+
try {
|
|
451
|
+
const state = JSON.parse(readFileSync(CLIENT_SLOT_PATH, 'utf8'));
|
|
452
|
+
const savedDeviceId = String(state?.deviceId || '').trim();
|
|
453
|
+
const expectedDeviceId = String(deviceId || '').trim();
|
|
454
|
+
if (savedDeviceId && expectedDeviceId && savedDeviceId !== expectedDeviceId) {
|
|
455
|
+
return '';
|
|
456
|
+
}
|
|
457
|
+
return normalizeSlotNumber(state?.slotNumber);
|
|
458
|
+
} catch {
|
|
459
|
+
return '';
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function writeSavedDeviceSlot(deviceId, slotNumber) {
|
|
464
|
+
const normalizedDeviceId = String(deviceId || '').trim();
|
|
465
|
+
const normalizedSlot = normalizeSlotNumber(slotNumber);
|
|
466
|
+
if (!normalizedDeviceId || !normalizedSlot) {
|
|
467
|
+
throw new Error('device-id-and-slot-required');
|
|
468
|
+
}
|
|
469
|
+
mkdirSync(dirname(CLIENT_SLOT_PATH), { recursive: true });
|
|
470
|
+
const temporaryPath = `${CLIENT_SLOT_PATH}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
471
|
+
try {
|
|
472
|
+
writeFileSync(temporaryPath, JSON.stringify({
|
|
473
|
+
deviceId: normalizedDeviceId,
|
|
474
|
+
slotNumber: Number(normalizedSlot),
|
|
475
|
+
updatedAt: new Date().toISOString()
|
|
476
|
+
}, null, 2), 'utf8');
|
|
477
|
+
try { chmodSync(temporaryPath, 0o600); } catch { /* Windows profile ACLs remain authoritative. */ }
|
|
478
|
+
renameSync(temporaryPath, CLIENT_SLOT_PATH);
|
|
479
|
+
try { chmodSync(CLIENT_SLOT_PATH, 0o600); } catch { /* Windows profile ACLs remain authoritative. */ }
|
|
480
|
+
} catch (error) {
|
|
481
|
+
rmSync(temporaryPath, { force: true });
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
return Number(normalizedSlot);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function upsertForwardedOption(args, flag, value) {
|
|
443
488
|
if (!value) {
|
|
444
489
|
return args;
|
|
445
490
|
}
|
|
@@ -2975,6 +3020,7 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
2975
3020
|
appVersion: process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion(),
|
|
2976
3021
|
deviceId: options.deviceId,
|
|
2977
3022
|
engine: options.engine,
|
|
3023
|
+
slot: options.slot,
|
|
2978
3024
|
savedSession: options.savedSession,
|
|
2979
3025
|
initialChoice: options.initialChoice,
|
|
2980
3026
|
initialChoiceMessage: options.initialChoiceMessage,
|
|
@@ -3033,7 +3079,41 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3033
3079
|
requestLocalDiscoveryWake('role-change');
|
|
3034
3080
|
try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop observes the role request */ }
|
|
3035
3081
|
return { ok: true, restarting: true, role, roleVersion };
|
|
3036
|
-
},
|
|
3082
|
+
},
|
|
3083
|
+
videoAcceleration: linuxVideoAccelerationStatus,
|
|
3084
|
+
installVideoAcceleration: async () => {
|
|
3085
|
+
const result = await installLinuxVideoAcceleration(
|
|
3086
|
+
linuxVideoAccelerationStatus || inspectLinuxVideoAcceleration()
|
|
3087
|
+
);
|
|
3088
|
+
linuxVideoAccelerationStatus = result;
|
|
3089
|
+
return result;
|
|
3090
|
+
},
|
|
3091
|
+
onVideoAccelerationReady: result => {
|
|
3092
|
+
if (!result?.ready || process.platform !== 'linux') return;
|
|
3093
|
+
agentRestartRequest = {
|
|
3094
|
+
reason: 'linux-video-acceleration-ready',
|
|
3095
|
+
requestedAt: new Date().toISOString()
|
|
3096
|
+
};
|
|
3097
|
+
requestLocalDiscoveryWake('linux-video-acceleration-ready');
|
|
3098
|
+
try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop restarts with the new ffmpeg preference */ }
|
|
3099
|
+
},
|
|
3100
|
+
assignSlot: async request => {
|
|
3101
|
+
const result = await requestHubSlotAssignment(request);
|
|
3102
|
+
if (result?.ok !== true) {
|
|
3103
|
+
return result;
|
|
3104
|
+
}
|
|
3105
|
+
const slotNumber = writeSavedDeviceSlot(request.deviceId, result.slotNumber);
|
|
3106
|
+
agentRestartRequest = {
|
|
3107
|
+
reason: 'slot-updated',
|
|
3108
|
+
slotNumber,
|
|
3109
|
+
requestedAt: new Date().toISOString()
|
|
3110
|
+
};
|
|
3111
|
+
requestLocalDiscoveryWake('slot-updated');
|
|
3112
|
+
setTimeout(() => {
|
|
3113
|
+
try { activeAgentProcess?.kill(); } catch { /* the lifecycle loop restarts with the saved slot */ }
|
|
3114
|
+
}, 150);
|
|
3115
|
+
return { ...result, slotNumber, saved: true, restarting: true };
|
|
3116
|
+
},
|
|
3037
3117
|
onRestart: () => process.kill(process.pid, 'SIGTERM'),
|
|
3038
3118
|
onShutdown: () => process.kill(process.pid, 'SIGTERM'),
|
|
3039
3119
|
onDisconnect: () => activeAgentProcess?.kill(),
|
|
@@ -3075,7 +3155,7 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3075
3155
|
return { ...choice, connectionPage };
|
|
3076
3156
|
}
|
|
3077
3157
|
|
|
3078
|
-
function parseManagerEndpoint(value) {
|
|
3158
|
+
function parseManagerEndpoint(value) {
|
|
3079
3159
|
let text = String(value || '').trim().replace(/^tcp:\/\//i, '');
|
|
3080
3160
|
const separator = text.lastIndexOf(':');
|
|
3081
3161
|
if (separator <= 0) {
|
|
@@ -3086,9 +3166,71 @@ function parseManagerEndpoint(value) {
|
|
|
3086
3166
|
if (!host || !Number.isFinite(port)) {
|
|
3087
3167
|
return null;
|
|
3088
3168
|
}
|
|
3089
|
-
return { host, port };
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3169
|
+
return { host, port };
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
function requestHubSlotAssignment({ manager, pairToken, deviceId, slotNumber, timeoutMs = 5000 }) {
|
|
3173
|
+
const endpoint = parseManagerEndpoint(manager);
|
|
3174
|
+
const normalizedPairToken = String(pairToken || '').trim();
|
|
3175
|
+
const normalizedDeviceId = String(deviceId || '').trim();
|
|
3176
|
+
const normalizedSlot = normalizeSlotNumber(slotNumber);
|
|
3177
|
+
if (!endpoint) {
|
|
3178
|
+
return Promise.resolve({ ok: false, error: 'hub-endpoint-unavailable' });
|
|
3179
|
+
}
|
|
3180
|
+
if (!normalizedPairToken) {
|
|
3181
|
+
return Promise.resolve({ ok: false, error: 'hub-pair-token-unavailable' });
|
|
3182
|
+
}
|
|
3183
|
+
if (!normalizedDeviceId) {
|
|
3184
|
+
return Promise.resolve({ ok: false, error: 'device-id-required' });
|
|
3185
|
+
}
|
|
3186
|
+
if (!normalizedSlot) {
|
|
3187
|
+
return Promise.resolve({ ok: false, error: 'invalid-slot-number' });
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
return new Promise(resolveAssignment => {
|
|
3191
|
+
const socket = net.createConnection(endpoint);
|
|
3192
|
+
let settled = false;
|
|
3193
|
+
let buffer = '';
|
|
3194
|
+
const settle = result => {
|
|
3195
|
+
if (settled) return;
|
|
3196
|
+
settled = true;
|
|
3197
|
+
socket.removeAllListeners();
|
|
3198
|
+
socket.destroy();
|
|
3199
|
+
resolveAssignment(result);
|
|
3200
|
+
};
|
|
3201
|
+
socket.setEncoding('utf8');
|
|
3202
|
+
socket.setTimeout(timeoutMs);
|
|
3203
|
+
socket.once('connect', () => {
|
|
3204
|
+
socket.write(`${JSON.stringify({
|
|
3205
|
+
type: 'slot.assign',
|
|
3206
|
+
pairToken: normalizedPairToken,
|
|
3207
|
+
deviceId: normalizedDeviceId,
|
|
3208
|
+
slotNumber: Number(normalizedSlot)
|
|
3209
|
+
})}\n`);
|
|
3210
|
+
});
|
|
3211
|
+
socket.on('data', chunk => {
|
|
3212
|
+
buffer += chunk;
|
|
3213
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
3214
|
+
if (newlineIndex < 0) return;
|
|
3215
|
+
try {
|
|
3216
|
+
const response = JSON.parse(buffer.slice(0, newlineIndex).trim());
|
|
3217
|
+
if (response?.type !== 'slot.assignment') {
|
|
3218
|
+
settle({ ok: false, error: response?.error || 'unexpected-hub-response' });
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
settle(response);
|
|
3222
|
+
} catch {
|
|
3223
|
+
settle({ ok: false, error: 'invalid-hub-response' });
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
socket.once('timeout', () => settle({ ok: false, error: 'hub-slot-request-timeout' }));
|
|
3227
|
+
socket.once('error', error => settle({ ok: false, error: error?.message || 'hub-slot-request-failed' }));
|
|
3228
|
+
socket.once('close', () => {
|
|
3229
|
+
if (!settled) settle({ ok: false, error: 'hub-slot-response-missing' });
|
|
3230
|
+
});
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3092
3234
|
export function canConnectToEndpoint(endpoint, timeoutMs = ENDPOINT_PROBE_TIMEOUT_MS) {
|
|
3093
3235
|
const parsedEndpoint = parseManagerEndpoint(endpoint);
|
|
3094
3236
|
if (!parsedEndpoint) {
|
|
@@ -3734,7 +3876,7 @@ function buildClientAgentEnvironment(prepared) {
|
|
|
3734
3876
|
return env;
|
|
3735
3877
|
}
|
|
3736
3878
|
|
|
3737
|
-
function buildFastEnvironment(prepared) {
|
|
3879
|
+
function buildFastEnvironment(prepared) {
|
|
3738
3880
|
const env = buildClientAgentEnvironment(prepared);
|
|
3739
3881
|
env.LIVEDESK_NODE_EXECUTABLE = process.execPath;
|
|
3740
3882
|
env.LIVEDESK_CLIENT_PACKAGE_VERSION = readPackageVersion();
|
|
@@ -3743,7 +3885,11 @@ function buildFastEnvironment(prepared) {
|
|
|
3743
3885
|
const configuredPaths = env.LIVEDESK_FFMPEG_PATHS
|
|
3744
3886
|
? env.LIVEDESK_FFMPEG_PATHS.split(separator).filter(Boolean)
|
|
3745
3887
|
: [];
|
|
3746
|
-
const configuredSingle = env.LIVEDESK_FFMPEG ? [env.LIVEDESK_FFMPEG] : [];
|
|
3888
|
+
const configuredSingle = env.LIVEDESK_FFMPEG ? [env.LIVEDESK_FFMPEG] : [];
|
|
3889
|
+
const systemHardwareFfmpeg = process.platform === 'linux'
|
|
3890
|
+
&& linuxVideoAccelerationStatus?.ready
|
|
3891
|
+
? [linuxVideoAccelerationStatus.ffmpegPath]
|
|
3892
|
+
: [];
|
|
3747
3893
|
const merged = [];
|
|
3748
3894
|
const addMerged = path => {
|
|
3749
3895
|
const value = String(path || '').trim();
|
|
@@ -3759,18 +3905,26 @@ function buildFastEnvironment(prepared) {
|
|
|
3759
3905
|
if (bundledFfmpegPaths[0]) {
|
|
3760
3906
|
env.LIVEDESK_FFMPEG = bundledFfmpegPaths[0];
|
|
3761
3907
|
}
|
|
3762
|
-
} else {
|
|
3763
|
-
configuredPaths.forEach(addMerged);
|
|
3764
|
-
configuredSingle.forEach(addMerged);
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3908
|
+
} else {
|
|
3909
|
+
configuredPaths.forEach(addMerged);
|
|
3910
|
+
configuredSingle.forEach(addMerged);
|
|
3911
|
+
systemHardwareFfmpeg.forEach(addMerged);
|
|
3912
|
+
bundledFfmpegPaths.forEach(addMerged);
|
|
3913
|
+
if (!env.LIVEDESK_FFMPEG && merged[0]) {
|
|
3914
|
+
env.LIVEDESK_FFMPEG = merged[0];
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3770
3917
|
|
|
3771
|
-
if (merged.length > 0) {
|
|
3772
|
-
env.LIVEDESK_FFMPEG_PATHS = merged.join(separator);
|
|
3773
|
-
}
|
|
3918
|
+
if (merged.length > 0) {
|
|
3919
|
+
env.LIVEDESK_FFMPEG_PATHS = merged.join(separator);
|
|
3920
|
+
}
|
|
3921
|
+
if (process.platform === 'linux'
|
|
3922
|
+
&& linuxVideoAccelerationStatus?.ready
|
|
3923
|
+
&& linuxVideoAccelerationStatus.driver
|
|
3924
|
+
&& linuxVideoAccelerationStatus.driver !== 'auto'
|
|
3925
|
+
&& !env.LIBVA_DRIVER_NAME) {
|
|
3926
|
+
env.LIBVA_DRIVER_NAME = linuxVideoAccelerationStatus.driver;
|
|
3927
|
+
}
|
|
3774
3928
|
|
|
3775
3929
|
if (process.platform === 'darwin' || process.platform === 'linux' || env.LIVEDESK_DEBUG_FFMPEG === '1') {
|
|
3776
3930
|
const describe = path => path.includes('ffmpeg-static')
|
|
@@ -4008,12 +4162,28 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
|
|
|
4008
4162
|
return;
|
|
4009
4163
|
}
|
|
4010
4164
|
|
|
4011
|
-
console.log(formatClientVersionBanner());
|
|
4165
|
+
console.log(formatClientVersionBanner());
|
|
4166
|
+
linuxVideoAccelerationStatus = inspectLinuxVideoAcceleration();
|
|
4167
|
+
if (process.platform === 'linux') {
|
|
4168
|
+
const accelerationLabel = linuxVideoAccelerationStatus.ready
|
|
4169
|
+
? `${linuxVideoAccelerationStatus.encoder} via ${linuxVideoAccelerationStatus.ffmpegPath}`
|
|
4170
|
+
: linuxVideoAccelerationStatus.message || linuxVideoAccelerationStatus.state;
|
|
4171
|
+
console.log(`[LiveDesk Client] Linux video acceleration: ${accelerationLabel}`);
|
|
4172
|
+
}
|
|
4012
4173
|
|
|
4013
|
-
let connectionPage = null;
|
|
4014
|
-
while (true) {
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4174
|
+
let connectionPage = null;
|
|
4175
|
+
while (true) {
|
|
4176
|
+
const savedSlot = readSavedDeviceSlot(parsed.deviceId);
|
|
4177
|
+
if (savedSlot) {
|
|
4178
|
+
parsed.slot = savedSlot;
|
|
4179
|
+
}
|
|
4180
|
+
const prepared = await prepareLoginConnection(parsed, connectionPage);
|
|
4181
|
+
connectionPage = prepared.connectionPage || connectionPage;
|
|
4182
|
+
prepared.connectionPage?.update({
|
|
4183
|
+
manager: prepared.manager,
|
|
4184
|
+
pairToken: prepared.pair,
|
|
4185
|
+
slotNumber: Number(prepared.slot || 0)
|
|
4186
|
+
});
|
|
4017
4187
|
const roleRestartBeforeAgent = connectionPage?.getRoleRestartRequest?.() || roleRestartRequest;
|
|
4018
4188
|
if (roleRestartBeforeAgent?.role) {
|
|
4019
4189
|
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
|
|
@@ -4114,8 +4284,23 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
|
|
|
4114
4284
|
await spawnUnifiedRoleRestart(requestedRoleRestart.role);
|
|
4115
4285
|
connectionPage?.close?.();
|
|
4116
4286
|
process.exit(0);
|
|
4117
|
-
}
|
|
4118
|
-
if (
|
|
4287
|
+
}
|
|
4288
|
+
if (agentRestartRequest) {
|
|
4289
|
+
const restart = agentRestartRequest;
|
|
4290
|
+
agentRestartRequest = null;
|
|
4291
|
+
connectionPage?.update({
|
|
4292
|
+
agent: {
|
|
4293
|
+
requestedEngine: prepared.engine,
|
|
4294
|
+
state: 'reconnecting'
|
|
4295
|
+
},
|
|
4296
|
+
message: restart.reason === 'slot-updated'
|
|
4297
|
+
? `Slot ${String(restart.slotNumber || prepared.slot || '').padStart(3, '0')} saved. Restarting the LiveDesk Agent.`
|
|
4298
|
+
: 'Linux hardware video is ready. Restarting the LiveDesk Agent.'
|
|
4299
|
+
});
|
|
4300
|
+
console.log(`[LiveDesk Client] Restarting Agent after ${restart.reason}.`);
|
|
4301
|
+
continue;
|
|
4302
|
+
}
|
|
4303
|
+
if (result?.signal) {
|
|
4119
4304
|
connectionPage?.close?.();
|
|
4120
4305
|
process.kill(process.pid, result.signal);
|
|
4121
4306
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.181",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ws": "^8.18.3"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
43
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
45
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
42
|
+
"@livedesk/fast-linux-x64": "0.1.387",
|
|
43
|
+
"@livedesk/fast-osx-arm64": "0.1.387",
|
|
44
|
+
"@livedesk/fast-osx-x64": "0.1.387",
|
|
45
|
+
"@livedesk/fast-win-x64": "0.1.387"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
@@ -43,6 +43,13 @@ function normalizePort(value, fallback = DEFAULT_PORT) {
|
|
|
43
43
|
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : fallback;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function normalizeSlotNumber(value) {
|
|
47
|
+
const slotNumber = Number(String(value ?? '').trim());
|
|
48
|
+
return Number.isInteger(slotNumber) && slotNumber >= 1 && slotNumber <= 999
|
|
49
|
+
? slotNumber
|
|
50
|
+
: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
function readCpuTotals() {
|
|
47
54
|
return os.cpus().reduce((totals, cpu) => {
|
|
48
55
|
const times = Object.values(cpu.times || {}).map(Number);
|
|
@@ -543,8 +550,10 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
543
550
|
let networkRefreshTimer;
|
|
544
551
|
const csrfToken = randomBytes(32).toString('hex');
|
|
545
552
|
const state = {
|
|
546
|
-
route: '/computer',
|
|
553
|
+
route: '/computer',
|
|
547
554
|
manager: '',
|
|
555
|
+
pairToken: '',
|
|
556
|
+
slotNumber: normalizeSlotNumber(options.slot),
|
|
548
557
|
assignedHubId: '',
|
|
549
558
|
endpointCandidates: [],
|
|
550
559
|
roleVersion: 0,
|
|
@@ -552,6 +561,9 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
552
561
|
message: 'Sign in with Google or enter a Hub PIN to start this Client.',
|
|
553
562
|
startup: false,
|
|
554
563
|
agent: { requestedEngine: normalizeString(options.engine, 40), state: 'waiting', pid: null, engine: '' },
|
|
564
|
+
videoAcceleration: options.videoAcceleration && typeof options.videoAcceleration === 'object'
|
|
565
|
+
? { ...options.videoAcceleration, busy: false }
|
|
566
|
+
: null,
|
|
555
567
|
lastError: '',
|
|
556
568
|
auth: {
|
|
557
569
|
lastAttemptAt: '',
|
|
@@ -719,8 +731,9 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
719
731
|
runtimeId: `client-${snapshot.runtimePid}-${snapshot.startedAt}`,
|
|
720
732
|
runtimePid: snapshot.runtimePid,
|
|
721
733
|
startedAt: snapshot.startedAt,
|
|
722
|
-
route: state.route,
|
|
734
|
+
route: state.route,
|
|
723
735
|
manager: state.manager,
|
|
736
|
+
slotNumber: state.slotNumber,
|
|
724
737
|
assignedHubId: state.assignedHubId,
|
|
725
738
|
endpointCandidates: state.endpointCandidates,
|
|
726
739
|
roleVersion: state.roleVersion,
|
|
@@ -731,8 +744,9 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
731
744
|
message: state.message,
|
|
732
745
|
lastError: state.lastError,
|
|
733
746
|
auth: { ...state.auth },
|
|
734
|
-
startup: state.startup,
|
|
735
|
-
agent: { ...state.agent, pid: runtime.getSnapshot().agentPid },
|
|
747
|
+
startup: state.startup,
|
|
748
|
+
agent: { ...state.agent, pid: runtime.getSnapshot().agentPid },
|
|
749
|
+
videoAcceleration: state.videoAcceleration ? { ...state.videoAcceleration } : null,
|
|
736
750
|
system: {
|
|
737
751
|
platform: process.platform,
|
|
738
752
|
arch: process.arch,
|
|
@@ -951,13 +965,121 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
951
965
|
setTimeout(() => options.onRestart?.(), 150);
|
|
952
966
|
return;
|
|
953
967
|
}
|
|
954
|
-
if (pathname === '/api/runtime/shutdown' && req.method === 'POST') {
|
|
955
|
-
runtime.emit('runtime.shutdown.requested', { role: 'client' });
|
|
968
|
+
if (pathname === '/api/runtime/shutdown' && req.method === 'POST') {
|
|
969
|
+
runtime.emit('runtime.shutdown.requested', { role: 'client' });
|
|
956
970
|
respondJson(200, { ok: true, shuttingDown: true, role: 'client' });
|
|
957
|
-
setTimeout(() => options.onShutdown?.(), 150);
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
if (pathname === '/api/
|
|
971
|
+
setTimeout(() => options.onShutdown?.(), 150);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (pathname === '/api/client/slot' && req.method === 'POST') {
|
|
975
|
+
const body = parseJsonBody(await readBody(req));
|
|
976
|
+
const slotNumber = normalizeSlotNumber(body.slotNumber ?? body.slot);
|
|
977
|
+
if (!slotNumber) {
|
|
978
|
+
respondJson(400, { ok: false, error: 'invalid-slot-number', slotNumber: state.slotNumber });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (!state.manager || !state.pairToken || state.agent.state !== 'running') {
|
|
982
|
+
respondJson(409, { ok: false, error: 'hub-not-connected', slotNumber: state.slotNumber });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (typeof options.assignSlot !== 'function') {
|
|
986
|
+
respondJson(409, { ok: false, error: 'slot-assignment-unavailable', slotNumber: state.slotNumber });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
runtime.emit('client.slot.assignment-requested', {
|
|
990
|
+
deviceId,
|
|
991
|
+
previousSlotNumber: state.slotNumber,
|
|
992
|
+
slotNumber
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
const result = await options.assignSlot({
|
|
996
|
+
manager: state.manager,
|
|
997
|
+
pairToken: state.pairToken,
|
|
998
|
+
deviceId,
|
|
999
|
+
slotNumber
|
|
1000
|
+
});
|
|
1001
|
+
if (result?.ok !== true) {
|
|
1002
|
+
const error = normalizeString(result?.error, 120) || 'slot-assignment-failed';
|
|
1003
|
+
runtime.emit('client.slot.assignment-rejected', {
|
|
1004
|
+
deviceId,
|
|
1005
|
+
slotNumber,
|
|
1006
|
+
error,
|
|
1007
|
+
conflictDeviceId: normalizeString(result?.conflict?.deviceId, 160)
|
|
1008
|
+
});
|
|
1009
|
+
respondJson(error === 'slot-taken' ? 409 : 502, {
|
|
1010
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
1011
|
+
ok: false,
|
|
1012
|
+
error,
|
|
1013
|
+
slotNumber: state.slotNumber
|
|
1014
|
+
});
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
state.slotNumber = slotNumber;
|
|
1018
|
+
state.message = `Slot ${String(slotNumber).padStart(3, '0')} saved. Reconnecting this Client.`;
|
|
1019
|
+
runtime.emit('client.slot.assigned', { deviceId, slotNumber });
|
|
1020
|
+
respondJson(200, { ...getState(), ...result, ok: true, slotNumber });
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
const message = normalizeString(error?.message || error, 240) || 'slot-assignment-failed';
|
|
1023
|
+
runtime.emit('client.slot.assignment-failed', { deviceId, slotNumber, error: message });
|
|
1024
|
+
respondJson(502, { ok: false, error: message, slotNumber: state.slotNumber });
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (pathname === '/api/client/video-acceleration/install' && req.method === 'POST') {
|
|
1029
|
+
if (process.platform !== 'linux') {
|
|
1030
|
+
respondJson(409, { ok: false, error: 'linux-video-acceleration-only' });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (state.videoAcceleration?.busy) {
|
|
1034
|
+
respondJson(409, { ok: false, error: 'video-acceleration-install-in-progress', ...getState() });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof options.installVideoAcceleration !== 'function') {
|
|
1038
|
+
respondJson(409, { ok: false, error: 'video-acceleration-install-unavailable' });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
state.videoAcceleration = {
|
|
1042
|
+
...(state.videoAcceleration || {}),
|
|
1043
|
+
supported: true,
|
|
1044
|
+
state: 'installing',
|
|
1045
|
+
busy: true,
|
|
1046
|
+
ready: false,
|
|
1047
|
+
error: '',
|
|
1048
|
+
message: 'Installing Linux hardware video support…'
|
|
1049
|
+
};
|
|
1050
|
+
runtime.emit('client.video-acceleration.installing', {
|
|
1051
|
+
packageManager: normalizeString(state.videoAcceleration.packageManager?.id, 40)
|
|
1052
|
+
});
|
|
1053
|
+
try {
|
|
1054
|
+
const result = await options.installVideoAcceleration();
|
|
1055
|
+
state.videoAcceleration = {
|
|
1056
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
1057
|
+
busy: false
|
|
1058
|
+
};
|
|
1059
|
+
const ok = state.videoAcceleration.ready === true;
|
|
1060
|
+
runtime.emit(ok ? 'client.video-acceleration.ready' : 'client.video-acceleration.failed', {
|
|
1061
|
+
encoder: normalizeString(state.videoAcceleration.encoder, 80),
|
|
1062
|
+
driver: normalizeString(state.videoAcceleration.driver, 80),
|
|
1063
|
+
error: normalizeString(state.videoAcceleration.error, 240)
|
|
1064
|
+
});
|
|
1065
|
+
respondJson(ok ? 200 : 409, { ...getState(), ok });
|
|
1066
|
+
if (ok && state.videoAcceleration.restartAgent) {
|
|
1067
|
+
setTimeout(() => options.onVideoAccelerationReady?.(state.videoAcceleration), 150);
|
|
1068
|
+
}
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
state.videoAcceleration = {
|
|
1071
|
+
...(state.videoAcceleration || {}),
|
|
1072
|
+
state: 'error',
|
|
1073
|
+
busy: false,
|
|
1074
|
+
ready: false,
|
|
1075
|
+
error: normalizeString(error?.message || error, 320)
|
|
1076
|
+
};
|
|
1077
|
+
runtime.emit('client.video-acceleration.failed', { error: state.videoAcceleration.error });
|
|
1078
|
+
respondJson(409, { ok: false, ...getState(), error: state.videoAcceleration.error });
|
|
1079
|
+
}
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (pathname === '/api/runtime/switch-role' && req.method === 'POST') {
|
|
961
1083
|
const body = parseJsonBody(await readBody(req));
|
|
962
1084
|
if (String(body.role || '').trim().toLowerCase() !== 'hub') {
|
|
963
1085
|
respondJson(400, { ok: false, error: 'client-can-only-transition-to-hub' });
|
|
@@ -1077,6 +1199,8 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1077
1199
|
if (patch.agent.state === 'running') runtime.update({ state: RuntimeState.RUNNING }, 'agent-running');
|
|
1078
1200
|
}
|
|
1079
1201
|
if (patch.manager) state.manager = normalizeString(patch.manager, 256);
|
|
1202
|
+
if (patch.pairToken) state.pairToken = normalizeString(patch.pairToken, 256);
|
|
1203
|
+
if (normalizeSlotNumber(patch.slotNumber)) state.slotNumber = normalizeSlotNumber(patch.slotNumber);
|
|
1080
1204
|
if (patch.assignedHubId) state.assignedHubId = normalizeString(patch.assignedHubId, 160);
|
|
1081
1205
|
if (patch.message) state.message = normalizeString(patch.message, 1000);
|
|
1082
1206
|
if (Number.isInteger(patch.roleVersion)) state.roleVersion = patch.roleVersion;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PROBE_TIMEOUT_MS = 6_000;
|
|
6
|
+
const INTEL_VENDOR_ID = '0x8086';
|
|
7
|
+
const AMD_VENDOR_ID = '0x1002';
|
|
8
|
+
|
|
9
|
+
function cleanText(value, maxLength = 320) {
|
|
10
|
+
return String(value ?? '').replace(/[\r\n\t]+/g, ' ').trim().slice(0, maxLength);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function executableExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
accessSync(path, constants.X_OK);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function uniquePaths(values) {
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
const candidate = String(value || '').trim();
|
|
26
|
+
if (!candidate) continue;
|
|
27
|
+
let normalized = candidate;
|
|
28
|
+
try {
|
|
29
|
+
normalized = realpathSync(candidate);
|
|
30
|
+
} catch {
|
|
31
|
+
normalized = resolve(candidate);
|
|
32
|
+
}
|
|
33
|
+
if (!result.includes(normalized)) result.push(normalized);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function findExecutable(name, options = {}) {
|
|
39
|
+
const platform = options.platform || process.platform;
|
|
40
|
+
const env = options.env || process.env;
|
|
41
|
+
const exists = options.executableExists || executableExists;
|
|
42
|
+
const executableName = platform === 'win32' && !String(name).toLowerCase().endsWith('.exe')
|
|
43
|
+
? `${name}.exe`
|
|
44
|
+
: String(name);
|
|
45
|
+
const candidates = String(env.PATH || '')
|
|
46
|
+
.split(delimiter)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map(entry => join(entry, executableName));
|
|
49
|
+
return candidates.find(candidate => exists(candidate)) || '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findLinuxRenderDevices(options = {}) {
|
|
53
|
+
if (Array.isArray(options.renderDevices)) {
|
|
54
|
+
return uniquePaths(options.renderDevices);
|
|
55
|
+
}
|
|
56
|
+
const driRoot = options.driRoot || '/dev/dri';
|
|
57
|
+
try {
|
|
58
|
+
return uniquePaths(readdirSync(driRoot)
|
|
59
|
+
.filter(name => /^renderD\d+$/.test(name))
|
|
60
|
+
.map(name => join(driRoot, name))
|
|
61
|
+
.filter(executable => existsSync(executable)));
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveSystemFfmpeg(options = {}) {
|
|
68
|
+
const env = options.env || process.env;
|
|
69
|
+
const exists = options.executableExists || executableExists;
|
|
70
|
+
const explicit = String(env.LIVEDESK_SYSTEM_FFMPEG || '').trim();
|
|
71
|
+
const candidates = uniquePaths([
|
|
72
|
+
explicit,
|
|
73
|
+
'/usr/bin/ffmpeg',
|
|
74
|
+
'/usr/local/bin/ffmpeg',
|
|
75
|
+
'/snap/bin/ffmpeg',
|
|
76
|
+
findExecutable('ffmpeg', { ...options, env, platform: 'linux', executableExists: exists })
|
|
77
|
+
]);
|
|
78
|
+
return candidates.find(candidate => exists(candidate) && !candidate.includes('/node_modules/')) || '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readGpuVendor(renderDevice, options = {}) {
|
|
82
|
+
if (options.gpuVendor) return String(options.gpuVendor).trim().toLowerCase();
|
|
83
|
+
try {
|
|
84
|
+
return readFileSync(`/sys/class/drm/${renderDevice.split('/').pop()}/device/vendor`, 'utf8').trim().toLowerCase();
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function detectLinuxPackageManager(options = {}) {
|
|
91
|
+
const managers = [
|
|
92
|
+
{ id: 'apt', command: 'apt-get' },
|
|
93
|
+
{ id: 'dnf', command: 'dnf' },
|
|
94
|
+
{ id: 'pacman', command: 'pacman' },
|
|
95
|
+
{ id: 'zypper', command: 'zypper' }
|
|
96
|
+
];
|
|
97
|
+
for (const manager of managers) {
|
|
98
|
+
const path = findExecutable(manager.command, { ...options, platform: 'linux' });
|
|
99
|
+
if (path) return { ...manager, path };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function packagesFor(managerId, gpuVendor) {
|
|
105
|
+
if (managerId === 'apt') {
|
|
106
|
+
if (gpuVendor === INTEL_VENDOR_ID) return ['ffmpeg', 'vainfo', 'intel-media-va-driver', 'i965-va-driver'];
|
|
107
|
+
if (gpuVendor === AMD_VENDOR_ID) return ['ffmpeg', 'vainfo', 'mesa-va-drivers'];
|
|
108
|
+
return ['ffmpeg', 'vainfo'];
|
|
109
|
+
}
|
|
110
|
+
if (managerId === 'dnf') {
|
|
111
|
+
return ['ffmpeg', 'libva-utils'];
|
|
112
|
+
}
|
|
113
|
+
if (managerId === 'pacman') {
|
|
114
|
+
if (gpuVendor === INTEL_VENDOR_ID) return ['ffmpeg', 'libva-utils', 'intel-media-driver', 'libva-intel-driver'];
|
|
115
|
+
if (gpuVendor === AMD_VENDOR_ID) return ['ffmpeg', 'libva-utils', 'libva-mesa-driver'];
|
|
116
|
+
return ['ffmpeg', 'libva-utils'];
|
|
117
|
+
}
|
|
118
|
+
if (managerId === 'zypper') {
|
|
119
|
+
return ['ffmpeg', 'libva-utils'];
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildLinuxVideoInstallPlan(status, options = {}) {
|
|
125
|
+
const manager = options.packageManager || status?.packageManager;
|
|
126
|
+
if (!manager?.id || !manager?.path) return null;
|
|
127
|
+
const packages = packagesFor(manager.id, status?.gpuVendor || '');
|
|
128
|
+
if (packages.length === 0) return null;
|
|
129
|
+
let args;
|
|
130
|
+
if (manager.id === 'apt' || manager.id === 'dnf') {
|
|
131
|
+
args = ['install', '-y', ...packages];
|
|
132
|
+
} else if (manager.id === 'pacman') {
|
|
133
|
+
args = ['-S', '--needed', '--noconfirm', ...packages];
|
|
134
|
+
} else if (manager.id === 'zypper') {
|
|
135
|
+
args = ['--non-interactive', 'install', ...packages];
|
|
136
|
+
} else {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
manager,
|
|
141
|
+
packages,
|
|
142
|
+
args,
|
|
143
|
+
displayCommand: `sudo ${manager.command} ${args.join(' ')}`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function probeLinuxVideoAcceleration(ffmpegPath, renderDevice, options = {}) {
|
|
148
|
+
const run = options.spawnSyncImpl || spawnSync;
|
|
149
|
+
const env = options.env || process.env;
|
|
150
|
+
const drivers = Array.isArray(options.drivers) && options.drivers.length > 0
|
|
151
|
+
? options.drivers
|
|
152
|
+
: ['', 'iHD', 'i965'];
|
|
153
|
+
let lastError = '';
|
|
154
|
+
for (const driver of drivers) {
|
|
155
|
+
const probeEnv = { ...env };
|
|
156
|
+
if (driver) probeEnv.LIBVA_DRIVER_NAME = driver;
|
|
157
|
+
else delete probeEnv.LIBVA_DRIVER_NAME;
|
|
158
|
+
const result = run(ffmpegPath, [
|
|
159
|
+
'-hide_banner', '-loglevel', 'error',
|
|
160
|
+
'-vaapi_device', renderDevice,
|
|
161
|
+
'-f', 'lavfi',
|
|
162
|
+
'-i', 'color=c=black:s=64x64:r=1',
|
|
163
|
+
'-vf', 'format=nv12,hwupload',
|
|
164
|
+
'-frames:v', '1',
|
|
165
|
+
'-an',
|
|
166
|
+
'-c:v', 'h264_vaapi',
|
|
167
|
+
'-f', 'h264',
|
|
168
|
+
'pipe:1'
|
|
169
|
+
], {
|
|
170
|
+
env: probeEnv,
|
|
171
|
+
encoding: null,
|
|
172
|
+
timeout: options.timeoutMs || PROBE_TIMEOUT_MS,
|
|
173
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
174
|
+
windowsHide: true
|
|
175
|
+
});
|
|
176
|
+
const outputBytes = Buffer.isBuffer(result.stdout) ? result.stdout.length : 0;
|
|
177
|
+
if (result.status === 0 && outputBytes > 0) {
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
encoder: 'h264_vaapi',
|
|
181
|
+
driver: driver || 'auto',
|
|
182
|
+
outputBytes
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
lastError = cleanText(
|
|
186
|
+
result.error?.message
|
|
187
|
+
|| (Buffer.isBuffer(result.stderr) ? result.stderr.toString('utf8') : result.stderr)
|
|
188
|
+
|| `ffmpeg exited with ${result.status ?? 'no status'}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return { ok: false, error: lastError || 'VAAPI hardware encoding probe failed.' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function baseStatus(platform) {
|
|
195
|
+
return {
|
|
196
|
+
platform,
|
|
197
|
+
supported: platform === 'linux',
|
|
198
|
+
state: platform === 'linux' ? 'checking' : 'not-applicable',
|
|
199
|
+
ready: false,
|
|
200
|
+
busy: false,
|
|
201
|
+
canInstall: false,
|
|
202
|
+
encoder: '',
|
|
203
|
+
driver: '',
|
|
204
|
+
ffmpegPath: '',
|
|
205
|
+
renderDevice: '',
|
|
206
|
+
gpuVendor: '',
|
|
207
|
+
packageManager: null,
|
|
208
|
+
manualCommand: '',
|
|
209
|
+
message: '',
|
|
210
|
+
error: '',
|
|
211
|
+
checkedAt: new Date().toISOString()
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function inspectLinuxVideoAcceleration(options = {}) {
|
|
216
|
+
const platform = options.platform || process.platform;
|
|
217
|
+
const status = baseStatus(platform);
|
|
218
|
+
if (platform !== 'linux') return status;
|
|
219
|
+
|
|
220
|
+
const renderDevices = findLinuxRenderDevices(options);
|
|
221
|
+
const renderDevice = renderDevices[0] || '';
|
|
222
|
+
const packageManager = options.packageManager === undefined
|
|
223
|
+
? detectLinuxPackageManager(options)
|
|
224
|
+
: options.packageManager;
|
|
225
|
+
const gpuVendor = renderDevice ? readGpuVendor(renderDevice, options) : '';
|
|
226
|
+
Object.assign(status, { renderDevice, gpuVendor, packageManager });
|
|
227
|
+
const installPlan = buildLinuxVideoInstallPlan(status, { packageManager });
|
|
228
|
+
status.canInstall = Boolean(installPlan);
|
|
229
|
+
status.manualCommand = installPlan?.displayCommand || '';
|
|
230
|
+
|
|
231
|
+
if (!renderDevice) {
|
|
232
|
+
status.state = 'unavailable';
|
|
233
|
+
status.message = 'No Linux hardware video device was found.';
|
|
234
|
+
return status;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ffmpegPath = options.ffmpegPath === undefined
|
|
238
|
+
? resolveSystemFfmpeg(options)
|
|
239
|
+
: String(options.ffmpegPath || '');
|
|
240
|
+
status.ffmpegPath = ffmpegPath;
|
|
241
|
+
if (!ffmpegPath) {
|
|
242
|
+
status.state = 'setup-required';
|
|
243
|
+
status.message = 'Hardware video setup is available.';
|
|
244
|
+
return status;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const probe = (options.probeImpl || probeLinuxVideoAcceleration)(ffmpegPath, renderDevice, options);
|
|
248
|
+
if (!probe?.ok) {
|
|
249
|
+
status.state = 'setup-required';
|
|
250
|
+
status.message = 'Hardware video needs setup.';
|
|
251
|
+
status.error = cleanText(probe?.error || 'VAAPI hardware encoding probe failed.');
|
|
252
|
+
return status;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
status.state = 'ready';
|
|
256
|
+
status.ready = true;
|
|
257
|
+
status.canInstall = false;
|
|
258
|
+
status.encoder = cleanText(probe.encoder || 'h264_vaapi', 80);
|
|
259
|
+
status.driver = cleanText(probe.driver || 'auto', 80);
|
|
260
|
+
status.message = 'VAAPI hardware video is ready.';
|
|
261
|
+
status.error = '';
|
|
262
|
+
return status;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function runChild(command, args, options = {}) {
|
|
266
|
+
const launch = options.spawnImpl || spawn;
|
|
267
|
+
return new Promise((resolvePromise, reject) => {
|
|
268
|
+
let child;
|
|
269
|
+
try {
|
|
270
|
+
child = launch(command, args, {
|
|
271
|
+
env: options.env || process.env,
|
|
272
|
+
stdio: 'inherit',
|
|
273
|
+
windowsHide: true
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
reject(error);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
child.once('error', reject);
|
|
280
|
+
child.once('exit', (code, signal) => resolvePromise({ code: code ?? 1, signal: signal || '' }));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function installLinuxVideoAcceleration(currentStatus, options = {}) {
|
|
285
|
+
if ((options.platform || process.platform) !== 'linux') {
|
|
286
|
+
return { ...baseStatus(options.platform || process.platform), state: 'not-applicable', error: 'linux-required' };
|
|
287
|
+
}
|
|
288
|
+
const plan = buildLinuxVideoInstallPlan(currentStatus, options);
|
|
289
|
+
if (!plan) {
|
|
290
|
+
return {
|
|
291
|
+
...currentStatus,
|
|
292
|
+
state: 'error',
|
|
293
|
+
busy: false,
|
|
294
|
+
ready: false,
|
|
295
|
+
error: 'No supported Linux package manager was found.'
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const env = options.env || process.env;
|
|
300
|
+
const find = name => findExecutable(name, { ...options, env, platform: 'linux' });
|
|
301
|
+
const isRoot = options.isRoot ?? (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
302
|
+
const pkexec = find('pkexec');
|
|
303
|
+
const sudo = find('sudo');
|
|
304
|
+
const graphicalSession = Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
305
|
+
let command = '';
|
|
306
|
+
let args = [];
|
|
307
|
+
let elevation = '';
|
|
308
|
+
if (isRoot) {
|
|
309
|
+
command = plan.manager.path;
|
|
310
|
+
args = plan.args;
|
|
311
|
+
elevation = 'root';
|
|
312
|
+
} else if (pkexec && graphicalSession) {
|
|
313
|
+
command = pkexec;
|
|
314
|
+
args = [plan.manager.path, ...plan.args];
|
|
315
|
+
elevation = 'pkexec';
|
|
316
|
+
} else if (sudo && (options.stdinIsTTY ?? process.stdin.isTTY)) {
|
|
317
|
+
command = sudo;
|
|
318
|
+
args = [plan.manager.path, ...plan.args];
|
|
319
|
+
elevation = 'sudo';
|
|
320
|
+
} else {
|
|
321
|
+
return {
|
|
322
|
+
...currentStatus,
|
|
323
|
+
state: 'error',
|
|
324
|
+
busy: false,
|
|
325
|
+
ready: false,
|
|
326
|
+
manualCommand: plan.displayCommand,
|
|
327
|
+
error: 'Administrator approval is unavailable in this session.'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
options.onProgress?.({
|
|
332
|
+
...currentStatus,
|
|
333
|
+
state: 'installing',
|
|
334
|
+
busy: true,
|
|
335
|
+
ready: false,
|
|
336
|
+
error: '',
|
|
337
|
+
message: 'Installing Linux hardware video support…',
|
|
338
|
+
elevation
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
result = options.runInstall
|
|
344
|
+
? await options.runInstall({ command, args, plan, elevation })
|
|
345
|
+
: await runChild(command, args, { ...options, env });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
...currentStatus,
|
|
349
|
+
state: 'error',
|
|
350
|
+
busy: false,
|
|
351
|
+
ready: false,
|
|
352
|
+
manualCommand: plan.displayCommand,
|
|
353
|
+
error: cleanText(error?.message || error)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (result?.code !== 0) {
|
|
357
|
+
return {
|
|
358
|
+
...currentStatus,
|
|
359
|
+
state: 'error',
|
|
360
|
+
busy: false,
|
|
361
|
+
ready: false,
|
|
362
|
+
manualCommand: plan.displayCommand,
|
|
363
|
+
error: `Package installation stopped with exit code ${result?.code ?? 'unknown'}.`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const refreshed = (options.inspectImpl || inspectLinuxVideoAcceleration)({
|
|
368
|
+
...options,
|
|
369
|
+
platform: 'linux',
|
|
370
|
+
packageManager: plan.manager,
|
|
371
|
+
ffmpegPath: undefined
|
|
372
|
+
});
|
|
373
|
+
if (!refreshed.ready) {
|
|
374
|
+
return {
|
|
375
|
+
...refreshed,
|
|
376
|
+
state: 'error',
|
|
377
|
+
busy: false,
|
|
378
|
+
ready: false,
|
|
379
|
+
canInstall: true,
|
|
380
|
+
manualCommand: plan.displayCommand,
|
|
381
|
+
error: refreshed.error || 'Hardware video was installed but the VAAPI test did not pass.'
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
...refreshed,
|
|
386
|
+
busy: false,
|
|
387
|
+
restartAgent: true,
|
|
388
|
+
message: 'VAAPI hardware video is ready. Restarting the LiveDesk Agent…'
|
|
389
|
+
};
|
|
390
|
+
}
|