@livedesk/client 0.1.100 → 0.1.102
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 +1 -1
- package/bin/livedesk-client.js +1 -0
- package/fast/linux-x64/livedesk-client-fast.dll +0 -0
- package/fast/linux-x64/livedesk-client-fast.pdb +0 -0
- package/fast/linux-x64/livedesk-raw-capture-helper.mjs +102 -0
- package/fast/osx-arm64/livedesk-client-fast.dll +0 -0
- package/fast/osx-arm64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-arm64/livedesk-raw-capture-helper.mjs +102 -0
- package/fast/osx-x64/livedesk-client-fast.dll +0 -0
- package/fast/osx-x64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-x64/livedesk-raw-capture-helper.mjs +102 -0
- package/fast/win-x64/livedesk-client-fast.dll +0 -0
- package/fast/win-x64/livedesk-client-fast.pdb +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ npx -y livedesk client --logout
|
|
|
72
72
|
Frame pipeline roadmap:
|
|
73
73
|
|
|
74
74
|
- Mode 1: `mode1-jpeg` - current test path using screen capture, resize, and JPEG binary frames.
|
|
75
|
-
- Mode 2: `mode2-lzo` -
|
|
75
|
+
- Mode 2: `mode2-lzo` - cross-platform wall path using independent RGB565LE frames capped at 320x180 and compressed as LZO1X blocks. The wall defaults to 8 fps. macOS and Linux keep one persistent local capture helper per client process. Legacy `mode2-lz4` settings migrate to this mode.
|
|
76
76
|
- Mode 3: `mode3-h264-hw` - OS-specific hardware H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV. macOS emits one startup key frame and then an approximately one-second GOP. The launcher uses bundled ffmpeg for fallback paths unless `LIVEDESK_FFMPEG` points to a custom binary. On macOS, set `LIVEDESK_FFMPEG_AVFOUNDATION_INPUT` only when the ScreenCaptureKit helper is unavailable and the AVFoundation fallback input is not `1:none`.
|
|
77
77
|
|
|
78
78
|
Legacy mode names such as `remote-fast` and `remote-quality` are treated as
|
package/bin/livedesk-client.js
CHANGED
|
@@ -2521,6 +2521,7 @@ function resolveBundledFfmpegPaths() {
|
|
|
2521
2521
|
|
|
2522
2522
|
function buildFastEnvironment() {
|
|
2523
2523
|
const env = { ...process.env };
|
|
2524
|
+
env.LIVEDESK_NODE_EXECUTABLE = process.execPath;
|
|
2524
2525
|
const bundledFfmpegPaths = resolveBundledFfmpegPaths();
|
|
2525
2526
|
const separator = process.platform === 'win32' ? ';' : ':';
|
|
2526
2527
|
const configuredPaths = env.LIVEDESK_FFMPEG_PATHS
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
const screenshotModule = await import('node-screenshots');
|
|
6
|
+
const Monitor = screenshotModule.Monitor || screenshotModule.default?.Monitor;
|
|
7
|
+
if (!Monitor?.all) {
|
|
8
|
+
throw new Error('node-screenshots Monitor API is unavailable.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clampInteger(value, min, max, fallback) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
if (!Number.isFinite(number)) return fallback;
|
|
14
|
+
return Math.max(min, Math.min(max, Math.round(number)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pickMonitor(index) {
|
|
18
|
+
const monitors = Monitor.all();
|
|
19
|
+
if (!monitors.length) {
|
|
20
|
+
throw new Error('No capturable desktop monitor was found.');
|
|
21
|
+
}
|
|
22
|
+
const selectedIndex = clampInteger(index, 0, monitors.length - 1, 0);
|
|
23
|
+
const monitor = monitors[selectedIndex]
|
|
24
|
+
|| monitors.find(item => item?.isPrimary?.() === true)
|
|
25
|
+
|| monitors[0];
|
|
26
|
+
return { monitor, selectedIndex, monitorCount: monitors.length };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight) {
|
|
30
|
+
if (raw.length < sourceWidth * sourceHeight * 4) {
|
|
31
|
+
throw new Error(`Raw capture is incomplete: ${raw.length} bytes for ${sourceWidth}x${sourceHeight}.`);
|
|
32
|
+
}
|
|
33
|
+
const scale = Math.min(1, maxWidth / sourceWidth, maxHeight / sourceHeight);
|
|
34
|
+
const width = Math.max(1, Math.round(sourceWidth * scale));
|
|
35
|
+
const height = Math.max(1, Math.round(sourceHeight * scale));
|
|
36
|
+
const output = Buffer.allocUnsafe(width * height * 2);
|
|
37
|
+
let target = 0;
|
|
38
|
+
for (let y = 0; y < height; y += 1) {
|
|
39
|
+
const sourceY = Math.min(sourceHeight - 1, Math.floor((y + 0.5) * sourceHeight / height));
|
|
40
|
+
for (let x = 0; x < width; x += 1) {
|
|
41
|
+
const sourceX = Math.min(sourceWidth - 1, Math.floor((x + 0.5) * sourceWidth / width));
|
|
42
|
+
const source = (sourceY * sourceWidth + sourceX) * 4;
|
|
43
|
+
const red = raw[source];
|
|
44
|
+
const green = raw[source + 1];
|
|
45
|
+
const blue = raw[source + 2];
|
|
46
|
+
const pixel = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
|
47
|
+
output[target++] = pixel & 0xff;
|
|
48
|
+
output[target++] = pixel >> 8;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { output, width, height };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeResponse(metadata, payload = Buffer.alloc(0)) {
|
|
55
|
+
const header = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
56
|
+
const prefix = Buffer.allocUnsafe(8);
|
|
57
|
+
prefix.writeUInt32BE(header.length, 0);
|
|
58
|
+
prefix.writeUInt32BE(payload.length, 4);
|
|
59
|
+
process.stdout.write(prefix);
|
|
60
|
+
process.stdout.write(header);
|
|
61
|
+
if (payload.length > 0) process.stdout.write(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
65
|
+
for await (const line of input) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
let request;
|
|
68
|
+
try {
|
|
69
|
+
request = JSON.parse(line);
|
|
70
|
+
const id = Number.isSafeInteger(Number(request.id)) ? Number(request.id) : 0;
|
|
71
|
+
const maxWidth = clampInteger(request.maxWidth, 1, 320, 320);
|
|
72
|
+
const maxHeight = clampInteger(request.maxHeight, 1, 180, 180);
|
|
73
|
+
const { monitor, selectedIndex, monitorCount } = pickMonitor(request.monitorIndex);
|
|
74
|
+
const captureStartedAt = performance.now();
|
|
75
|
+
const image = await monitor.captureImage();
|
|
76
|
+
const raw = await image.toRaw(false);
|
|
77
|
+
const captureMs = Math.max(0, performance.now() - captureStartedAt);
|
|
78
|
+
const sourceWidth = Number(image.width || monitor.width?.() || 0);
|
|
79
|
+
const sourceHeight = Number(image.height || monitor.height?.() || 0);
|
|
80
|
+
const convertStartedAt = performance.now();
|
|
81
|
+
const converted = convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight);
|
|
82
|
+
const convertMs = Math.max(0, performance.now() - convertStartedAt);
|
|
83
|
+
writeResponse({
|
|
84
|
+
ok: true,
|
|
85
|
+
id,
|
|
86
|
+
width: converted.width,
|
|
87
|
+
height: converted.height,
|
|
88
|
+
sourceWidth,
|
|
89
|
+
sourceHeight,
|
|
90
|
+
monitorIndex: selectedIndex,
|
|
91
|
+
monitorCount,
|
|
92
|
+
captureMs: Math.round(captureMs),
|
|
93
|
+
convertMs: Math.round(convertMs)
|
|
94
|
+
}, converted.output);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
writeResponse({
|
|
97
|
+
ok: false,
|
|
98
|
+
id: Number.isSafeInteger(Number(request?.id)) ? Number(request.id) : 0,
|
|
99
|
+
error: String(error?.message || error || 'capture failed').slice(0, 1000)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
const screenshotModule = await import('node-screenshots');
|
|
6
|
+
const Monitor = screenshotModule.Monitor || screenshotModule.default?.Monitor;
|
|
7
|
+
if (!Monitor?.all) {
|
|
8
|
+
throw new Error('node-screenshots Monitor API is unavailable.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clampInteger(value, min, max, fallback) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
if (!Number.isFinite(number)) return fallback;
|
|
14
|
+
return Math.max(min, Math.min(max, Math.round(number)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pickMonitor(index) {
|
|
18
|
+
const monitors = Monitor.all();
|
|
19
|
+
if (!monitors.length) {
|
|
20
|
+
throw new Error('No capturable desktop monitor was found.');
|
|
21
|
+
}
|
|
22
|
+
const selectedIndex = clampInteger(index, 0, monitors.length - 1, 0);
|
|
23
|
+
const monitor = monitors[selectedIndex]
|
|
24
|
+
|| monitors.find(item => item?.isPrimary?.() === true)
|
|
25
|
+
|| monitors[0];
|
|
26
|
+
return { monitor, selectedIndex, monitorCount: monitors.length };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight) {
|
|
30
|
+
if (raw.length < sourceWidth * sourceHeight * 4) {
|
|
31
|
+
throw new Error(`Raw capture is incomplete: ${raw.length} bytes for ${sourceWidth}x${sourceHeight}.`);
|
|
32
|
+
}
|
|
33
|
+
const scale = Math.min(1, maxWidth / sourceWidth, maxHeight / sourceHeight);
|
|
34
|
+
const width = Math.max(1, Math.round(sourceWidth * scale));
|
|
35
|
+
const height = Math.max(1, Math.round(sourceHeight * scale));
|
|
36
|
+
const output = Buffer.allocUnsafe(width * height * 2);
|
|
37
|
+
let target = 0;
|
|
38
|
+
for (let y = 0; y < height; y += 1) {
|
|
39
|
+
const sourceY = Math.min(sourceHeight - 1, Math.floor((y + 0.5) * sourceHeight / height));
|
|
40
|
+
for (let x = 0; x < width; x += 1) {
|
|
41
|
+
const sourceX = Math.min(sourceWidth - 1, Math.floor((x + 0.5) * sourceWidth / width));
|
|
42
|
+
const source = (sourceY * sourceWidth + sourceX) * 4;
|
|
43
|
+
const red = raw[source];
|
|
44
|
+
const green = raw[source + 1];
|
|
45
|
+
const blue = raw[source + 2];
|
|
46
|
+
const pixel = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
|
47
|
+
output[target++] = pixel & 0xff;
|
|
48
|
+
output[target++] = pixel >> 8;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { output, width, height };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeResponse(metadata, payload = Buffer.alloc(0)) {
|
|
55
|
+
const header = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
56
|
+
const prefix = Buffer.allocUnsafe(8);
|
|
57
|
+
prefix.writeUInt32BE(header.length, 0);
|
|
58
|
+
prefix.writeUInt32BE(payload.length, 4);
|
|
59
|
+
process.stdout.write(prefix);
|
|
60
|
+
process.stdout.write(header);
|
|
61
|
+
if (payload.length > 0) process.stdout.write(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
65
|
+
for await (const line of input) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
let request;
|
|
68
|
+
try {
|
|
69
|
+
request = JSON.parse(line);
|
|
70
|
+
const id = Number.isSafeInteger(Number(request.id)) ? Number(request.id) : 0;
|
|
71
|
+
const maxWidth = clampInteger(request.maxWidth, 1, 320, 320);
|
|
72
|
+
const maxHeight = clampInteger(request.maxHeight, 1, 180, 180);
|
|
73
|
+
const { monitor, selectedIndex, monitorCount } = pickMonitor(request.monitorIndex);
|
|
74
|
+
const captureStartedAt = performance.now();
|
|
75
|
+
const image = await monitor.captureImage();
|
|
76
|
+
const raw = await image.toRaw(false);
|
|
77
|
+
const captureMs = Math.max(0, performance.now() - captureStartedAt);
|
|
78
|
+
const sourceWidth = Number(image.width || monitor.width?.() || 0);
|
|
79
|
+
const sourceHeight = Number(image.height || monitor.height?.() || 0);
|
|
80
|
+
const convertStartedAt = performance.now();
|
|
81
|
+
const converted = convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight);
|
|
82
|
+
const convertMs = Math.max(0, performance.now() - convertStartedAt);
|
|
83
|
+
writeResponse({
|
|
84
|
+
ok: true,
|
|
85
|
+
id,
|
|
86
|
+
width: converted.width,
|
|
87
|
+
height: converted.height,
|
|
88
|
+
sourceWidth,
|
|
89
|
+
sourceHeight,
|
|
90
|
+
monitorIndex: selectedIndex,
|
|
91
|
+
monitorCount,
|
|
92
|
+
captureMs: Math.round(captureMs),
|
|
93
|
+
convertMs: Math.round(convertMs)
|
|
94
|
+
}, converted.output);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
writeResponse({
|
|
97
|
+
ok: false,
|
|
98
|
+
id: Number.isSafeInteger(Number(request?.id)) ? Number(request.id) : 0,
|
|
99
|
+
error: String(error?.message || error || 'capture failed').slice(0, 1000)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
const screenshotModule = await import('node-screenshots');
|
|
6
|
+
const Monitor = screenshotModule.Monitor || screenshotModule.default?.Monitor;
|
|
7
|
+
if (!Monitor?.all) {
|
|
8
|
+
throw new Error('node-screenshots Monitor API is unavailable.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clampInteger(value, min, max, fallback) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
if (!Number.isFinite(number)) return fallback;
|
|
14
|
+
return Math.max(min, Math.min(max, Math.round(number)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pickMonitor(index) {
|
|
18
|
+
const monitors = Monitor.all();
|
|
19
|
+
if (!monitors.length) {
|
|
20
|
+
throw new Error('No capturable desktop monitor was found.');
|
|
21
|
+
}
|
|
22
|
+
const selectedIndex = clampInteger(index, 0, monitors.length - 1, 0);
|
|
23
|
+
const monitor = monitors[selectedIndex]
|
|
24
|
+
|| monitors.find(item => item?.isPrimary?.() === true)
|
|
25
|
+
|| monitors[0];
|
|
26
|
+
return { monitor, selectedIndex, monitorCount: monitors.length };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight) {
|
|
30
|
+
if (raw.length < sourceWidth * sourceHeight * 4) {
|
|
31
|
+
throw new Error(`Raw capture is incomplete: ${raw.length} bytes for ${sourceWidth}x${sourceHeight}.`);
|
|
32
|
+
}
|
|
33
|
+
const scale = Math.min(1, maxWidth / sourceWidth, maxHeight / sourceHeight);
|
|
34
|
+
const width = Math.max(1, Math.round(sourceWidth * scale));
|
|
35
|
+
const height = Math.max(1, Math.round(sourceHeight * scale));
|
|
36
|
+
const output = Buffer.allocUnsafe(width * height * 2);
|
|
37
|
+
let target = 0;
|
|
38
|
+
for (let y = 0; y < height; y += 1) {
|
|
39
|
+
const sourceY = Math.min(sourceHeight - 1, Math.floor((y + 0.5) * sourceHeight / height));
|
|
40
|
+
for (let x = 0; x < width; x += 1) {
|
|
41
|
+
const sourceX = Math.min(sourceWidth - 1, Math.floor((x + 0.5) * sourceWidth / width));
|
|
42
|
+
const source = (sourceY * sourceWidth + sourceX) * 4;
|
|
43
|
+
const red = raw[source];
|
|
44
|
+
const green = raw[source + 1];
|
|
45
|
+
const blue = raw[source + 2];
|
|
46
|
+
const pixel = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
|
47
|
+
output[target++] = pixel & 0xff;
|
|
48
|
+
output[target++] = pixel >> 8;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { output, width, height };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeResponse(metadata, payload = Buffer.alloc(0)) {
|
|
55
|
+
const header = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
56
|
+
const prefix = Buffer.allocUnsafe(8);
|
|
57
|
+
prefix.writeUInt32BE(header.length, 0);
|
|
58
|
+
prefix.writeUInt32BE(payload.length, 4);
|
|
59
|
+
process.stdout.write(prefix);
|
|
60
|
+
process.stdout.write(header);
|
|
61
|
+
if (payload.length > 0) process.stdout.write(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
65
|
+
for await (const line of input) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
let request;
|
|
68
|
+
try {
|
|
69
|
+
request = JSON.parse(line);
|
|
70
|
+
const id = Number.isSafeInteger(Number(request.id)) ? Number(request.id) : 0;
|
|
71
|
+
const maxWidth = clampInteger(request.maxWidth, 1, 320, 320);
|
|
72
|
+
const maxHeight = clampInteger(request.maxHeight, 1, 180, 180);
|
|
73
|
+
const { monitor, selectedIndex, monitorCount } = pickMonitor(request.monitorIndex);
|
|
74
|
+
const captureStartedAt = performance.now();
|
|
75
|
+
const image = await monitor.captureImage();
|
|
76
|
+
const raw = await image.toRaw(false);
|
|
77
|
+
const captureMs = Math.max(0, performance.now() - captureStartedAt);
|
|
78
|
+
const sourceWidth = Number(image.width || monitor.width?.() || 0);
|
|
79
|
+
const sourceHeight = Number(image.height || monitor.height?.() || 0);
|
|
80
|
+
const convertStartedAt = performance.now();
|
|
81
|
+
const converted = convertRgbaToRgb565(raw, sourceWidth, sourceHeight, maxWidth, maxHeight);
|
|
82
|
+
const convertMs = Math.max(0, performance.now() - convertStartedAt);
|
|
83
|
+
writeResponse({
|
|
84
|
+
ok: true,
|
|
85
|
+
id,
|
|
86
|
+
width: converted.width,
|
|
87
|
+
height: converted.height,
|
|
88
|
+
sourceWidth,
|
|
89
|
+
sourceHeight,
|
|
90
|
+
monitorIndex: selectedIndex,
|
|
91
|
+
monitorCount,
|
|
92
|
+
captureMs: Math.round(captureMs),
|
|
93
|
+
convertMs: Math.round(convertMs)
|
|
94
|
+
}, converted.output);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
writeResponse({
|
|
97
|
+
ok: false,
|
|
98
|
+
id: Number.isSafeInteger(Number(request?.id)) ? Number(request.id) : 0,
|
|
99
|
+
error: String(error?.message || error || 'capture failed').slice(0, 1000)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
Binary file
|
|
Binary file
|