ownremote 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +211 -2
- package/cli.js +843 -0
- package/package.json +49 -4
- package/public/app.js +957 -0
- package/public/index.html +211 -0
- package/public/style.css +318 -0
- package/src/admin-client.js +64 -0
- package/src/config.js +125 -0
- package/src/daemon.js +43 -0
- package/src/input-helper.ps1 +163 -0
- package/src/input.js +152 -0
- package/src/launch-hidden.vbs +13 -0
- package/src/logo.js +137 -0
- package/src/screen-helper.ps1 +127 -0
- package/src/screen.js +285 -0
- package/src/server.js +567 -0
- package/src/startup.js +74 -0
- package/src/tray-helper.ps1 +183 -0
- package/src/tray.js +85 -0
- package/src/tunnel.js +129 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Helper tangkap layar untuk preview di HP. Dijalankan oleh src/screen.js.
|
|
2
|
+
# stdin (teks, satu perintah per baris):
|
|
3
|
+
# f <maxWidth> <quality> ambil 1 frame dari monitor tempat cursor berada (JPEG)
|
|
4
|
+
# c <1|0> nyalakan/matikan laporan posisi cursor (~60x per detik, hanya saat berubah)
|
|
5
|
+
# q keluar
|
|
6
|
+
# stdout (biner): [1 byte tipe][int32 LE panjang][payload]
|
|
7
|
+
# 'R' siap | 'F' frame: 6x int32 (left, top, width, height, imgW, imgH) + JPEG
|
|
8
|
+
# 'C' cursor: 2x int32 (x, y) | 'E' error: teks UTF-8
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing -TypeDefinition @"
|
|
12
|
+
using System;
|
|
13
|
+
using System.Drawing;
|
|
14
|
+
using System.Drawing.Drawing2D;
|
|
15
|
+
using System.Drawing.Imaging;
|
|
16
|
+
using System.IO;
|
|
17
|
+
using System.Runtime.InteropServices;
|
|
18
|
+
using System.Text;
|
|
19
|
+
using System.Threading;
|
|
20
|
+
using System.Windows.Forms;
|
|
21
|
+
|
|
22
|
+
public static class OwnRemoteScreen {
|
|
23
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
24
|
+
struct POINT { public int X; public int Y; }
|
|
25
|
+
|
|
26
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
27
|
+
[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr value);
|
|
28
|
+
|
|
29
|
+
// Per-Monitor V2: koordinat cursor & monitor dalam piksel fisik, akurat juga di monitor dengan scaling berbeda
|
|
30
|
+
static void MakeDpiAware() {
|
|
31
|
+
try { if (SetProcessDpiAwarenessContext(new IntPtr(-4))) return; } catch { }
|
|
32
|
+
try { SetProcessDPIAware(); } catch { }
|
|
33
|
+
}
|
|
34
|
+
[DllImport("user32.dll")] static extern bool GetCursorPos(out POINT p);
|
|
35
|
+
|
|
36
|
+
static readonly object outLock = new object();
|
|
37
|
+
static Stream stdout;
|
|
38
|
+
static volatile bool cursorOn = false;
|
|
39
|
+
static ImageCodecInfo jpeg;
|
|
40
|
+
|
|
41
|
+
static void Packet(char type, byte[] payload) {
|
|
42
|
+
lock (outLock) {
|
|
43
|
+
stdout.WriteByte((byte)type);
|
|
44
|
+
stdout.Write(BitConverter.GetBytes(payload.Length), 0, 4);
|
|
45
|
+
stdout.Write(payload, 0, payload.Length);
|
|
46
|
+
stdout.Flush();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static void CursorLoop() {
|
|
51
|
+
int lx = int.MinValue, ly = int.MinValue;
|
|
52
|
+
while (true) {
|
|
53
|
+
if (cursorOn) {
|
|
54
|
+
POINT p;
|
|
55
|
+
if (GetCursorPos(out p) && (p.X != lx || p.Y != ly)) {
|
|
56
|
+
lx = p.X;
|
|
57
|
+
ly = p.Y;
|
|
58
|
+
var b = new byte[8];
|
|
59
|
+
BitConverter.GetBytes(p.X).CopyTo(b, 0);
|
|
60
|
+
BitConverter.GetBytes(p.Y).CopyTo(b, 4);
|
|
61
|
+
try { Packet('C', b); } catch { return; }
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
lx = int.MinValue;
|
|
65
|
+
}
|
|
66
|
+
Thread.Sleep(15); // ~60 Hz, hanya kirim saat posisi berubah
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static void Frame(int maxWidth, long quality) {
|
|
71
|
+
POINT p;
|
|
72
|
+
GetCursorPos(out p);
|
|
73
|
+
Rectangle r = Screen.FromPoint(new Point(p.X, p.Y)).Bounds;
|
|
74
|
+
int w = Math.Max(160, Math.Min(maxWidth, r.Width));
|
|
75
|
+
int h = (int)Math.Round((double)r.Height * w / r.Width);
|
|
76
|
+
|
|
77
|
+
using (var full = new Bitmap(r.Width, r.Height, PixelFormat.Format24bppRgb))
|
|
78
|
+
using (var small = new Bitmap(w, h, PixelFormat.Format24bppRgb)) {
|
|
79
|
+
using (var g = Graphics.FromImage(full)) {
|
|
80
|
+
g.CopyFromScreen(r.Left, r.Top, 0, 0, r.Size, CopyPixelOperation.SourceCopy);
|
|
81
|
+
}
|
|
82
|
+
using (var g2 = Graphics.FromImage(small)) {
|
|
83
|
+
g2.InterpolationMode = InterpolationMode.HighQualityBilinear;
|
|
84
|
+
g2.PixelOffsetMode = PixelOffsetMode.Half;
|
|
85
|
+
g2.DrawImage(full, 0, 0, w, h);
|
|
86
|
+
}
|
|
87
|
+
var ms = new MemoryStream();
|
|
88
|
+
ms.Write(new byte[24], 0, 24);
|
|
89
|
+
var ep = new EncoderParameters(1);
|
|
90
|
+
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
|
91
|
+
small.Save(ms, jpeg, ep);
|
|
92
|
+
byte[] buf = ms.ToArray();
|
|
93
|
+
int[] hdr = { r.Left, r.Top, r.Width, r.Height, w, h };
|
|
94
|
+
for (int i = 0; i < hdr.Length; i++) BitConverter.GetBytes(hdr[i]).CopyTo(buf, i * 4);
|
|
95
|
+
Packet('F', buf);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public static void Run() {
|
|
100
|
+
MakeDpiAware();
|
|
101
|
+
stdout = Console.OpenStandardOutput();
|
|
102
|
+
foreach (var c in ImageCodecInfo.GetImageEncoders()) if (c.MimeType == "image/jpeg") jpeg = c;
|
|
103
|
+
|
|
104
|
+
var t = new Thread(CursorLoop);
|
|
105
|
+
t.IsBackground = true;
|
|
106
|
+
t.Start();
|
|
107
|
+
|
|
108
|
+
Packet('R', new byte[0]);
|
|
109
|
+
string line;
|
|
110
|
+
while ((line = Console.In.ReadLine()) != null) {
|
|
111
|
+
try {
|
|
112
|
+
string[] a = line.Trim().Split(' ');
|
|
113
|
+
switch (a[0]) {
|
|
114
|
+
case "f": Frame(int.Parse(a[1]), long.Parse(a[2])); break;
|
|
115
|
+
case "c": cursorOn = a[1] == "1"; break;
|
|
116
|
+
case "q": return;
|
|
117
|
+
}
|
|
118
|
+
} catch (Exception e) {
|
|
119
|
+
// mis. layar terkunci / UAC aktif: desktop tidak bisa ditangkap
|
|
120
|
+
Packet('E', Encoding.UTF8.GetBytes(e.Message));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
"@
|
|
126
|
+
|
|
127
|
+
[OwnRemoteScreen]::Run()
|
package/src/screen.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Preset kualitas preview layar. "normal" = rekomendasi: gambar 5 fps, sementara posisi cursor
|
|
6
|
+
// dikirim terpisah ~30x/detik supaya virtual cursor di HP tetap terasa mulus.
|
|
7
|
+
const PRESETS = {
|
|
8
|
+
saver: { fps: 2, width: 640, quality: 40 },
|
|
9
|
+
normal: { fps: 5, width: 960, quality: 50 },
|
|
10
|
+
sharp: { fps: 8, width: 1280, quality: 60 },
|
|
11
|
+
};
|
|
12
|
+
const LIMITS = { fps: [1, 15], width: [320, 2560], quality: [10, 95] };
|
|
13
|
+
|
|
14
|
+
const clampInt = (v, [min, max], fallback) => {
|
|
15
|
+
const n = Math.round(Number(v));
|
|
16
|
+
return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Ambil parameter stream dari pesan HP: nilai kustom (fps/width/quality) atau nama preset. */
|
|
20
|
+
function resolveParams(opts = {}) {
|
|
21
|
+
const base = PRESETS[opts.preset] || PRESETS.normal;
|
|
22
|
+
return {
|
|
23
|
+
fps: clampInt(opts.fps, LIMITS.fps, base.fps),
|
|
24
|
+
width: clampInt(opts.width, LIMITS.width, base.width),
|
|
25
|
+
quality: clampInt(opts.quality, LIMITS.quality, base.quality),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ACK_TIMEOUT_MS = 4000; // HP tidak membalas -> anggap siap lagi
|
|
30
|
+
const ERROR_RETRY_MS = 2000; // layar terkunci dll -> coba lagi
|
|
31
|
+
const IDLE_STOP_MS = 30_000; // matikan helper setelah tidak ada penonton
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mengalirkan tangkapan layar ke HP yang meminta (opt-in).
|
|
35
|
+
* Hemat data: frame berikutnya baru dikirim setelah HP selesai menampilkan frame sebelumnya (ack),
|
|
36
|
+
* dan frame yang identik dengan sebelumnya tidak dikirim ulang.
|
|
37
|
+
*/
|
|
38
|
+
class ScreenStreamer {
|
|
39
|
+
constructor({ log = () => {} } = {}) {
|
|
40
|
+
this.log = log;
|
|
41
|
+
this.viewers = new Map(); // ws -> { preset, waiting, waitingSince, lastHash, name }
|
|
42
|
+
this.proc = null;
|
|
43
|
+
this.ready = false;
|
|
44
|
+
this.busy = false;
|
|
45
|
+
this.timer = null;
|
|
46
|
+
this.idleTimer = null;
|
|
47
|
+
this.lastFrameAt = 0;
|
|
48
|
+
this.bounds = null; // monitor yang terakhir ditangkap
|
|
49
|
+
this.buf = Buffer.alloc(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get viewerCount() {
|
|
53
|
+
return this.viewers.size;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isViewing(ws) {
|
|
57
|
+
return this.viewers.has(ws);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
subscribe(ws, opts, name) {
|
|
61
|
+
const params = resolveParams(opts);
|
|
62
|
+
const desc = `${params.fps} fps, ${params.width}px, quality ${params.quality}`;
|
|
63
|
+
const existing = this.viewers.get(ws);
|
|
64
|
+
if (existing) {
|
|
65
|
+
if (JSON.stringify(existing.params) !== JSON.stringify(params)) this.log(`Screen preview of ${name} changed to ${desc}.`);
|
|
66
|
+
existing.params = params;
|
|
67
|
+
existing.lastHash = null; // kualitas/resolusi berubah: kirim frame baru walaupun layar diam
|
|
68
|
+
} else {
|
|
69
|
+
this.viewers.set(ws, { params, waiting: false, waitingSince: 0, lastHash: null, name });
|
|
70
|
+
this.log(`Screen preview started by ${name} (${desc}).`);
|
|
71
|
+
}
|
|
72
|
+
clearTimeout(this.idleTimer);
|
|
73
|
+
this.ensureProcess();
|
|
74
|
+
this.send('c 1');
|
|
75
|
+
this.sendMonitorTo(ws);
|
|
76
|
+
this.sendCursorTo(ws);
|
|
77
|
+
this.pump();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unsubscribe(ws) {
|
|
81
|
+
const v = this.viewers.get(ws);
|
|
82
|
+
if (!v) return;
|
|
83
|
+
this.viewers.delete(ws);
|
|
84
|
+
this.log(`Screen preview stopped by ${v.name}.`);
|
|
85
|
+
if (this.viewers.size === 0) {
|
|
86
|
+
this.send('c 0');
|
|
87
|
+
clearTimeout(this.timer);
|
|
88
|
+
this.timer = null;
|
|
89
|
+
this.idleTimer = setTimeout(() => this.stopProcess(), IDLE_STOP_MS);
|
|
90
|
+
this.idleTimer.unref?.();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
unsubscribeAll(reason) {
|
|
95
|
+
for (const ws of [...this.viewers.keys()]) {
|
|
96
|
+
safeSend(ws, JSON.stringify({ t: 'screen-off', reason }));
|
|
97
|
+
this.unsubscribe(ws);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ack(ws) {
|
|
102
|
+
const v = this.viewers.get(ws);
|
|
103
|
+
if (!v) return;
|
|
104
|
+
v.waiting = false;
|
|
105
|
+
this.pump();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
stop() {
|
|
109
|
+
this.viewers.clear();
|
|
110
|
+
clearTimeout(this.timer);
|
|
111
|
+
clearTimeout(this.idleTimer);
|
|
112
|
+
this.stopProcess();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------- proses helper
|
|
116
|
+
ensureProcess() {
|
|
117
|
+
if (this.proc) return;
|
|
118
|
+
const script = path.join(__dirname, 'screen-helper.ps1');
|
|
119
|
+
const proc = spawn(
|
|
120
|
+
'powershell.exe',
|
|
121
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script],
|
|
122
|
+
{ windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
123
|
+
);
|
|
124
|
+
this.proc = proc;
|
|
125
|
+
this.ready = false;
|
|
126
|
+
this.busy = false;
|
|
127
|
+
this.buf = Buffer.alloc(0);
|
|
128
|
+
proc.stdout.on('data', (chunk) => this.onData(chunk));
|
|
129
|
+
proc.stderr.on('data', (d) => {
|
|
130
|
+
const msg = String(d).trim();
|
|
131
|
+
if (msg) this.log(`Screen helper: ${msg}`);
|
|
132
|
+
});
|
|
133
|
+
proc.stdin.on('error', () => { /* proses sudah mati */ });
|
|
134
|
+
proc.on('error', (err) => this.log(`Screen helper failed to start: ${err.message}`));
|
|
135
|
+
proc.on('exit', () => {
|
|
136
|
+
if (this.proc !== proc) return;
|
|
137
|
+
this.proc = null;
|
|
138
|
+
this.ready = false;
|
|
139
|
+
this.busy = false;
|
|
140
|
+
if (this.viewers.size > 0) {
|
|
141
|
+
this.log('Screen helper exited, restarting...');
|
|
142
|
+
setTimeout(() => { if (this.viewers.size > 0) { this.ensureProcess(); this.send('c 1'); } }, 1000);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopProcess() {
|
|
148
|
+
if (!this.proc) return;
|
|
149
|
+
const p = this.proc;
|
|
150
|
+
this.proc = null;
|
|
151
|
+
this.ready = false;
|
|
152
|
+
try { p.stdin.end('q\n'); } catch { /* abaikan */ }
|
|
153
|
+
setTimeout(() => { if (p.exitCode === null) p.kill(); }, 1500).unref?.();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
send(line) {
|
|
157
|
+
if (this.proc && this.proc.stdin.writable) this.proc.stdin.write(`${line}\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onData(chunk) {
|
|
161
|
+
this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;
|
|
162
|
+
while (this.buf.length >= 5) {
|
|
163
|
+
const type = String.fromCharCode(this.buf[0]);
|
|
164
|
+
const len = this.buf.readInt32LE(1);
|
|
165
|
+
if (this.buf.length < 5 + len) break;
|
|
166
|
+
const payload = this.buf.subarray(5, 5 + len);
|
|
167
|
+
this.buf = this.buf.subarray(5 + len);
|
|
168
|
+
if (type === 'R') {
|
|
169
|
+
this.ready = true;
|
|
170
|
+
if (this.viewers.size) this.send('c 1');
|
|
171
|
+
this.pump();
|
|
172
|
+
} else if (type === 'F') {
|
|
173
|
+
this.onFrame(Buffer.from(payload));
|
|
174
|
+
} else if (type === 'C') {
|
|
175
|
+
this.onCursor(payload.readInt32LE(0), payload.readInt32LE(4));
|
|
176
|
+
} else if (type === 'E') {
|
|
177
|
+
this.onError(payload.toString('utf8'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------- frame
|
|
183
|
+
// Beberapa HP menonton sekaligus: pakai nilai tertinggi yang diminta
|
|
184
|
+
currentParams() {
|
|
185
|
+
const p = { fps: 1, width: 320, quality: 10 };
|
|
186
|
+
for (const v of this.viewers.values()) {
|
|
187
|
+
p.fps = Math.max(p.fps, v.params.fps);
|
|
188
|
+
p.width = Math.max(p.width, v.params.width);
|
|
189
|
+
p.quality = Math.max(p.quality, v.params.quality);
|
|
190
|
+
}
|
|
191
|
+
return p;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pump() {
|
|
195
|
+
if (!this.ready || this.busy || this.viewers.size === 0) return;
|
|
196
|
+
const preset = this.currentParams();
|
|
197
|
+
const wait = this.lastFrameAt + 1000 / preset.fps - Date.now();
|
|
198
|
+
if (wait > 0) {
|
|
199
|
+
if (!this.timer) this.timer = setTimeout(() => { this.timer = null; this.pump(); }, wait);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const anyReady = [...this.viewers.values()].some((v) => !v.waiting || now - v.waitingSince > ACK_TIMEOUT_MS);
|
|
204
|
+
if (!anyReady) return; // dipanggil lagi saat ack datang
|
|
205
|
+
this.busy = true;
|
|
206
|
+
this.lastFrameAt = now;
|
|
207
|
+
this.send(`f ${preset.width} ${preset.quality}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onFrame(buf) {
|
|
211
|
+
this.busy = false;
|
|
212
|
+
const [left, top, width, height] = [0, 4, 8, 12].map((o) => buf.readInt32LE(o));
|
|
213
|
+
const monitorChanged = !this.bounds || this.bounds.left !== left || this.bounds.top !== top;
|
|
214
|
+
this.bounds = { left, top, width, height };
|
|
215
|
+
const jpeg = buf.subarray(24);
|
|
216
|
+
const hash = crypto.createHash('sha1').update(jpeg).digest('base64');
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
for (const [ws, v] of this.viewers) {
|
|
219
|
+
if (v.waiting && now - v.waitingSince <= ACK_TIMEOUT_MS) continue;
|
|
220
|
+
if (v.lastHash === hash) continue; // layar tidak berubah: hemat data
|
|
221
|
+
if (ws.bufferedAmount > 1024 * 1024) continue; // jaringan HP sedang lambat
|
|
222
|
+
v.waiting = true;
|
|
223
|
+
v.waitingSince = now;
|
|
224
|
+
v.lastHash = hash;
|
|
225
|
+
ws.send(jpeg, { binary: true });
|
|
226
|
+
}
|
|
227
|
+
if (monitorChanged) {
|
|
228
|
+
for (const ws of this.viewers.keys()) {
|
|
229
|
+
this.sendMonitorTo(ws);
|
|
230
|
+
this.sendCursorTo(ws);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.pump();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
onError(message) {
|
|
237
|
+
this.busy = false;
|
|
238
|
+
this.lastFrameAt = Date.now() + ERROR_RETRY_MS;
|
|
239
|
+
const msg = /handle|invalid|operation/i.test(message)
|
|
240
|
+
? "Can't capture the PC screen (the PC is locked or a UAC prompt is open)."
|
|
241
|
+
: `Screen capture failed: ${message}`;
|
|
242
|
+
for (const ws of this.viewers.keys()) safeSend(ws, JSON.stringify({ t: 'screen-err', msg }));
|
|
243
|
+
this.pump();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------- cursor
|
|
247
|
+
normalized(x, y) {
|
|
248
|
+
const b = this.bounds;
|
|
249
|
+
if (!b) return null;
|
|
250
|
+
return { x: (x - b.left) / b.width, y: (y - b.top) / b.height };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onCursor(x, y) {
|
|
254
|
+
this.lastCursor = { x, y };
|
|
255
|
+
const n = this.normalized(x, y);
|
|
256
|
+
if (!n) return;
|
|
257
|
+
const msg = JSON.stringify({ t: 'cur', x: round4(n.x), y: round4(n.y) });
|
|
258
|
+
for (const ws of this.viewers.keys()) {
|
|
259
|
+
if (ws.bufferedAmount < 256 * 1024) safeSend(ws, msg);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Ukuran monitor dalam piksel fisik: dipakai HP untuk memprediksi posisi cursor tanpa menunggu jaringan
|
|
264
|
+
sendMonitorTo(ws) {
|
|
265
|
+
if (this.bounds) safeSend(ws, JSON.stringify({ t: 'mon', w: this.bounds.width, h: this.bounds.height }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
sendCursorTo(ws) {
|
|
269
|
+
if (!this.lastCursor) return;
|
|
270
|
+
const n = this.normalized(this.lastCursor.x, this.lastCursor.y);
|
|
271
|
+
if (n) safeSend(ws, JSON.stringify({ t: 'cur', x: round4(n.x), y: round4(n.y) }));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function round4(n) {
|
|
276
|
+
return Math.round(n * 10000) / 10000;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function safeSend(ws, data) {
|
|
280
|
+
try {
|
|
281
|
+
if (ws.readyState === 1) ws.send(data);
|
|
282
|
+
} catch { /* koneksi sedang ditutup */ }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { ScreenStreamer, PRESETS, LIMITS, resolveParams };
|