@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 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` - Windows wall path using independent RGB565LE frames capped at 320x180 and compressed as LZO1X blocks. The wall defaults to 8 fps. Legacy `mode2-lz4` settings migrate to this mode.
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
@@ -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
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {