@livedesk/client 0.1.239 → 0.1.241

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.
@@ -1,390 +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
- }
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
+ }