@livedesk/hub 0.1.0 → 0.1.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/README.md +7 -1
- package/package.json +5 -2
- package/src/filesystem/directory-reader.js +187 -0
- package/src/filesystem/hub-filesystem.js +78 -0
- package/src/filesystem/path-registry.js +78 -0
- package/src/filesystem/roots.js +115 -0
- package/src/filesystem/shared-folders.js +180 -0
- package/src/filesystem/transfer-jobs.js +229 -0
- package/src/lzo1x.js +109 -0
- package/src/mode4-atlas-worker.js +439 -0
- package/src/mode4-atlas.js +129 -0
- package/src/remote-hub.js +1295 -163
- package/src/server.js +1150 -69
package/src/remote-hub.js
CHANGED
|
@@ -8,7 +8,7 @@ const DEFAULT_HEARTBEAT_MS = 5000;
|
|
|
8
8
|
const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
|
|
9
9
|
const MAX_LINE_CHARS = 4 * 1024 * 1024;
|
|
10
10
|
const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
|
|
11
|
-
const MAX_STREAM_BASE64_CHARS =
|
|
11
|
+
const MAX_STREAM_BASE64_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
|
|
12
12
|
const MAX_THUMBNAIL_BINARY_BYTES = Math.floor(MAX_THUMBNAIL_BASE64_CHARS * 3 / 4);
|
|
13
13
|
const MAX_STREAM_BINARY_BYTES = Math.floor(MAX_STREAM_BASE64_CHARS * 3 / 4);
|
|
14
14
|
const MAX_AUDIO_BASE64_CHARS = 1024 * 1024;
|
|
@@ -17,17 +17,22 @@ const MAX_AGENT_TASK_CHARS = 4000;
|
|
|
17
17
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
18
18
|
const RECENT_TASK_LIMIT = 12;
|
|
19
19
|
const RECENT_TASK_BATCH_LIMIT = 16;
|
|
20
|
-
const RECENT_FRAME_CACHE_TTL_MS =
|
|
21
|
-
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT =
|
|
22
|
-
const RECENT_LIVE_FRAME_CACHE_LIMIT =
|
|
23
|
-
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES =
|
|
24
|
-
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES =
|
|
20
|
+
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
21
|
+
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
22
|
+
const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
23
|
+
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
25
|
+
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
26
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
27
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
25
28
|
const REMOTE_PROTOCOL_VERSION = 2;
|
|
26
29
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
27
30
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
28
31
|
const REMOTE_FRAME_PROTOCOL_VERSION = 2;
|
|
29
32
|
const REMOTE_FRAME_HANDSHAKE = 'smartview-request-open-frame';
|
|
30
|
-
const DEFAULT_REMOTE_FRAME_MODE = '
|
|
33
|
+
const DEFAULT_REMOTE_FRAME_MODE = 'mode1-jpeg';
|
|
34
|
+
const TCP_BINARY_FRAME_MAGIC = Buffer.from('LDFB', 'ascii');
|
|
35
|
+
const TCP_BINARY_FRAME_HEADER_BYTES = 12;
|
|
31
36
|
const REMOTE_FRAME_MODE_PROFILES = Object.freeze({
|
|
32
37
|
thumbnail: Object.freeze({
|
|
33
38
|
mode: 'thumbnail',
|
|
@@ -40,54 +45,102 @@ const REMOTE_FRAME_MODE_PROFILES = Object.freeze({
|
|
|
40
45
|
modeVersion: 1,
|
|
41
46
|
implemented: true
|
|
42
47
|
}),
|
|
43
|
-
'
|
|
44
|
-
mode: '
|
|
45
|
-
label: '
|
|
48
|
+
'mode1-jpeg': Object.freeze({
|
|
49
|
+
mode: 'mode1-jpeg',
|
|
50
|
+
label: 'Mode 1 - Test JPEG Binary',
|
|
46
51
|
transport: 'ws-binary',
|
|
47
52
|
encoding: 'image',
|
|
48
53
|
codec: 'jpeg',
|
|
49
54
|
compression: 'image/jpeg',
|
|
50
|
-
profile: 'jpeg-binary-v1',
|
|
55
|
+
profile: 'mode1-jpeg-binary-v1',
|
|
51
56
|
modeVersion: 1,
|
|
52
57
|
implemented: true
|
|
53
58
|
}),
|
|
54
|
-
'
|
|
55
|
-
mode: '
|
|
56
|
-
label: '
|
|
59
|
+
'mode2-lzo': Object.freeze({
|
|
60
|
+
mode: 'mode2-lzo',
|
|
61
|
+
label: 'Mode 2 - RGB565 LZO Wall',
|
|
57
62
|
transport: 'ws-binary',
|
|
58
|
-
encoding: 'image',
|
|
59
|
-
codec: '
|
|
60
|
-
compression: '
|
|
61
|
-
profile: '
|
|
62
|
-
modeVersion:
|
|
63
|
+
encoding: 'image-raw',
|
|
64
|
+
codec: 'rgb565',
|
|
65
|
+
compression: 'lzo1x',
|
|
66
|
+
profile: 'mode2-lzo-rgb565-v1',
|
|
67
|
+
modeVersion: 2,
|
|
63
68
|
implemented: true
|
|
64
69
|
}),
|
|
65
|
-
'
|
|
66
|
-
mode: '
|
|
67
|
-
label: '
|
|
70
|
+
'mode3-h264-hw': Object.freeze({
|
|
71
|
+
mode: 'mode3-h264-hw',
|
|
72
|
+
label: 'Mode 3 - Hardware H.264',
|
|
68
73
|
transport: 'ws-binary',
|
|
69
74
|
encoding: 'video',
|
|
70
|
-
codec: '
|
|
71
|
-
compression: 'video
|
|
72
|
-
profile: '
|
|
73
|
-
modeVersion:
|
|
74
|
-
implemented:
|
|
75
|
+
codec: 'h264',
|
|
76
|
+
compression: 'video/h264',
|
|
77
|
+
profile: 'mode3-h264-hardware-v1',
|
|
78
|
+
modeVersion: 3,
|
|
79
|
+
implemented: true
|
|
80
|
+
}),
|
|
81
|
+
'mode5-lzo-delta': Object.freeze({
|
|
82
|
+
mode: 'mode5-lzo-delta',
|
|
83
|
+
label: 'Mode 5 - RGB565 Tile Delta LZO',
|
|
84
|
+
transport: 'ws-binary',
|
|
85
|
+
encoding: 'image-delta',
|
|
86
|
+
codec: 'rgb565-delta',
|
|
87
|
+
compression: 'lzo1x',
|
|
88
|
+
profile: 'mode5-lzo-rgb565-delta-v1',
|
|
89
|
+
modeVersion: 5,
|
|
90
|
+
implemented: true
|
|
75
91
|
})
|
|
76
92
|
});
|
|
77
93
|
const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
78
|
-
['
|
|
79
|
-
['
|
|
80
|
-
['
|
|
81
|
-
['
|
|
82
|
-
['
|
|
83
|
-
['
|
|
84
|
-
['
|
|
85
|
-
['
|
|
94
|
+
['1', 'mode1-jpeg'],
|
|
95
|
+
['mode1', 'mode1-jpeg'],
|
|
96
|
+
['mode-1', 'mode1-jpeg'],
|
|
97
|
+
['mode1-jpeg', 'mode1-jpeg'],
|
|
98
|
+
['jpeg', 'mode1-jpeg'],
|
|
99
|
+
['jpg', 'mode1-jpeg'],
|
|
100
|
+
['test', 'mode1-jpeg'],
|
|
101
|
+
['fast', 'mode1-jpeg'],
|
|
102
|
+
['remotefast', 'mode1-jpeg'],
|
|
103
|
+
['remote_fast', 'mode1-jpeg'],
|
|
104
|
+
['remote-fast', 'mode1-jpeg'],
|
|
105
|
+
['quality', 'mode1-jpeg'],
|
|
106
|
+
['remotequality', 'mode1-jpeg'],
|
|
107
|
+
['remote_quality', 'mode1-jpeg'],
|
|
108
|
+
['remote-quality', 'mode1-jpeg'],
|
|
109
|
+
['2', 'mode2-lzo'],
|
|
110
|
+
['mode2', 'mode2-lzo'],
|
|
111
|
+
['mode-2', 'mode2-lzo'],
|
|
112
|
+
['mode2-lzo', 'mode2-lzo'],
|
|
113
|
+
['lzo', 'mode2-lzo'],
|
|
114
|
+
['lzo1x', 'mode2-lzo'],
|
|
115
|
+
['rgb565', 'mode2-lzo'],
|
|
116
|
+
['tile-lzo', 'mode2-lzo'],
|
|
117
|
+
['tiles-lzo', 'mode2-lzo'],
|
|
118
|
+
['mode2-lz4', 'mode2-lzo'],
|
|
119
|
+
['lz4', 'mode2-lzo'],
|
|
120
|
+
['tile-lz4', 'mode2-lzo'],
|
|
121
|
+
['tiles-lz4', 'mode2-lzo'],
|
|
122
|
+
['3', 'mode3-h264-hw'],
|
|
123
|
+
['mode3', 'mode3-h264-hw'],
|
|
124
|
+
['mode-3', 'mode3-h264-hw'],
|
|
125
|
+
['mode3-h264', 'mode3-h264-hw'],
|
|
126
|
+
['mode3-h264-hw', 'mode3-h264-hw'],
|
|
127
|
+
['h264', 'mode3-h264-hw'],
|
|
128
|
+
['h264-hw', 'mode3-h264-hw'],
|
|
129
|
+
['hardware', 'mode3-h264-hw'],
|
|
130
|
+
['hardware-h264', 'mode3-h264-hw'],
|
|
131
|
+
['5', 'mode5-lzo-delta'],
|
|
132
|
+
['mode5', 'mode5-lzo-delta'],
|
|
133
|
+
['mode-5', 'mode5-lzo-delta'],
|
|
134
|
+
['mode5-lzo', 'mode5-lzo-delta'],
|
|
135
|
+
['mode5-lzo-delta', 'mode5-lzo-delta'],
|
|
136
|
+
['lzo-delta', 'mode5-lzo-delta'],
|
|
137
|
+
['tile-delta', 'mode5-lzo-delta'],
|
|
138
|
+
['control-lzo', 'mode5-lzo-delta'],
|
|
86
139
|
['thumb', 'thumbnail'],
|
|
87
140
|
['thumbnail', 'thumbnail'],
|
|
88
|
-
['video', '
|
|
89
|
-
['codec', '
|
|
90
|
-
['video-codec', '
|
|
141
|
+
['video', 'mode3-h264-hw'],
|
|
142
|
+
['codec', 'mode3-h264-hw'],
|
|
143
|
+
['video-codec', 'mode3-h264-hw']
|
|
91
144
|
]);
|
|
92
145
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
93
146
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
@@ -96,6 +149,9 @@ const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
|
96
149
|
const DUPLICATE_DEVICE_ACTIVE_REJECT_MS = 15000;
|
|
97
150
|
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
98
151
|
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
152
|
+
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 256;
|
|
153
|
+
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
154
|
+
const AGENT_BINARY_INGRESS_DRAIN_BUDGET_PACKETS = 64;
|
|
99
155
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
100
156
|
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
101
157
|
const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
|
|
@@ -134,7 +190,11 @@ function safeString(value, maxLength = 200) {
|
|
|
134
190
|
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
135
191
|
}
|
|
136
192
|
|
|
137
|
-
function
|
|
193
|
+
function generatePairingPin() {
|
|
194
|
+
return crypto.randomInt(0, 1000000).toString().padStart(6, '0');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeRemoteFrameMode(value, fallback = DEFAULT_REMOTE_FRAME_MODE, { allowUnimplemented = false } = {}) {
|
|
138
198
|
const requested = safeString(value, 80).toLowerCase();
|
|
139
199
|
const fallbackMode = REMOTE_FRAME_MODE_PROFILES[fallback] ? fallback : DEFAULT_REMOTE_FRAME_MODE;
|
|
140
200
|
if (!requested) {
|
|
@@ -143,7 +203,7 @@ function normalizeRemoteFrameMode(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
|
143
203
|
|
|
144
204
|
const normalized = REMOTE_FRAME_MODE_ALIASES.get(requested) || requested;
|
|
145
205
|
const profile = REMOTE_FRAME_MODE_PROFILES[normalized];
|
|
146
|
-
if (!profile || profile.implemented !== true) {
|
|
206
|
+
if (!profile || (!allowUnimplemented && profile.implemented !== true)) {
|
|
147
207
|
return fallbackMode;
|
|
148
208
|
}
|
|
149
209
|
|
|
@@ -164,14 +224,13 @@ function serializeRemoteFrameModeProfile(profile) {
|
|
|
164
224
|
};
|
|
165
225
|
}
|
|
166
226
|
|
|
167
|
-
function getRemoteFrameModeProfile(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
168
|
-
const mode = normalizeRemoteFrameMode(value, fallback);
|
|
227
|
+
function getRemoteFrameModeProfile(value, fallback = DEFAULT_REMOTE_FRAME_MODE, options = {}) {
|
|
228
|
+
const mode = normalizeRemoteFrameMode(value, fallback, options);
|
|
169
229
|
return serializeRemoteFrameModeProfile(REMOTE_FRAME_MODE_PROFILES[mode]);
|
|
170
230
|
}
|
|
171
231
|
|
|
172
232
|
function getSupportedRemoteFrameModeProfiles() {
|
|
173
233
|
return Object.values(REMOTE_FRAME_MODE_PROFILES)
|
|
174
|
-
.filter(profile => profile.implemented === true)
|
|
175
234
|
.map(serializeRemoteFrameModeProfile);
|
|
176
235
|
}
|
|
177
236
|
|
|
@@ -222,14 +281,14 @@ function normalizeIncomingFrameModeProfile(value) {
|
|
|
222
281
|
}
|
|
223
282
|
|
|
224
283
|
if (typeof value === 'string') {
|
|
225
|
-
return getRemoteFrameModeProfile(value);
|
|
284
|
+
return getRemoteFrameModeProfile(value, DEFAULT_REMOTE_FRAME_MODE, { allowUnimplemented: true });
|
|
226
285
|
}
|
|
227
286
|
|
|
228
287
|
if (typeof value !== 'object') {
|
|
229
288
|
return null;
|
|
230
289
|
}
|
|
231
290
|
|
|
232
|
-
const profile =
|
|
291
|
+
const profile = getRemoteFrameModeProfile(value.mode || value.frameMode || value.profile, DEFAULT_REMOTE_FRAME_MODE, { allowUnimplemented: true });
|
|
233
292
|
return {
|
|
234
293
|
mode: profile.mode,
|
|
235
294
|
label: safeString(value.label, 80) || profile.mode,
|
|
@@ -452,6 +511,10 @@ function normalizeSlotNumber(value) {
|
|
|
452
511
|
return Number.isInteger(number) && number >= 1 && number <= 999 ? number : 0;
|
|
453
512
|
}
|
|
454
513
|
|
|
514
|
+
function normalizeMonitorIndex(value) {
|
|
515
|
+
return clampNumber(value, 0, 63, 0);
|
|
516
|
+
}
|
|
517
|
+
|
|
455
518
|
function parseManagerEndpoint(value) {
|
|
456
519
|
const endpoint = safeString(value, 256);
|
|
457
520
|
const match = endpoint.match(/^(\[[^\]]+\]|[^:\s]+):(\d{1,5})$/);
|
|
@@ -505,8 +568,15 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
505
568
|
const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
|
|
506
569
|
const deltaX = Number(input.deltaX ?? input.DeltaX);
|
|
507
570
|
const deltaY = Number(input.deltaY ?? input.DeltaY);
|
|
571
|
+
const keyCode = Number(input.keyCode ?? input.KeyCode);
|
|
572
|
+
const location = Number(input.location ?? input.Location);
|
|
573
|
+
const monitorIndex = Number(input.monitorIndex ?? input.MonitorIndex ?? input.screenIndex ?? input.ScreenIndex ?? input.displayIndex ?? input.DisplayIndex);
|
|
574
|
+
const inputSeq = Number(input.inputSeq ?? input.InputSeq);
|
|
575
|
+
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
576
|
+
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
508
577
|
return {
|
|
509
578
|
type,
|
|
579
|
+
monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : 0,
|
|
510
580
|
normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
|
|
511
581
|
normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
|
|
512
582
|
button: safeString(input.button || input.Button, 24),
|
|
@@ -514,9 +584,19 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
514
584
|
deltaY: Number.isFinite(deltaY) ? Math.max(-4096, Math.min(4096, deltaY)) : 0,
|
|
515
585
|
key: safeString(input.key || input.Key, 80),
|
|
516
586
|
code: safeString(input.code || input.Code, 80),
|
|
587
|
+
keyCode: Number.isFinite(keyCode) ? Math.max(0, Math.min(65535, Math.trunc(keyCode))) : 0,
|
|
588
|
+
location: Number.isFinite(location) ? Math.max(0, Math.min(255, Math.trunc(location))) : 0,
|
|
517
589
|
text: safeText(input.text || input.Text, 256),
|
|
518
590
|
repeat: input.repeat === true || input.Repeat === true,
|
|
591
|
+
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
592
|
+
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
593
|
+
altKey: input.altKey === true || input.AltKey === true,
|
|
594
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
519
595
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
596
|
+
inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
|
|
597
|
+
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
598
|
+
issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
|
|
599
|
+
hubReceivedAtEpochMs: Number.isFinite(hubReceivedAtEpochMs) ? hubReceivedAtEpochMs : 0,
|
|
520
600
|
issuedAt: safeString(input.issuedAt || input.IssuedAt, 80)
|
|
521
601
|
};
|
|
522
602
|
}
|
|
@@ -811,6 +891,13 @@ function serializeDevice(device, options = {}) {
|
|
|
811
891
|
lastDisconnectReason: device.lastDisconnectReason,
|
|
812
892
|
remoteAddress: device.remoteAddress,
|
|
813
893
|
remotePort: device.remotePort,
|
|
894
|
+
channels: {
|
|
895
|
+
control: !!device.socket && !device.socket.destroyed,
|
|
896
|
+
input: !!device.inputSocket && !device.inputSocket.destroyed,
|
|
897
|
+
frame: !!device.frameSocket && !device.frameSocket.destroyed,
|
|
898
|
+
audio: !!device.audioSocket && !device.audioSocket.destroyed,
|
|
899
|
+
file: !!device.fileSocket && !device.fileSocket.destroyed
|
|
900
|
+
},
|
|
814
901
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
815
902
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
816
903
|
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
@@ -958,6 +1045,112 @@ function parseRemoteHubWebSocketBinaryFrame(buffer) {
|
|
|
958
1045
|
return { header, payload };
|
|
959
1046
|
}
|
|
960
1047
|
|
|
1048
|
+
function hasTcpBinaryFrameMagicPrefix(buffer) {
|
|
1049
|
+
if (!Buffer.isBuffer(buffer) || buffer.length <= 0 || buffer.length > TCP_BINARY_FRAME_MAGIC.length) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
1054
|
+
if (buffer[index] !== TCP_BINARY_FRAME_MAGIC[index]) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function startsWithTcpBinaryFrameMagic(buffer) {
|
|
1062
|
+
return Buffer.isBuffer(buffer)
|
|
1063
|
+
&& buffer.length >= TCP_BINARY_FRAME_MAGIC.length
|
|
1064
|
+
&& buffer.subarray(0, TCP_BINARY_FRAME_MAGIC.length).equals(TCP_BINARY_FRAME_MAGIC);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function isJsonObjectStartByte(value) {
|
|
1068
|
+
return value === 0x7b;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function isWhitespaceByte(value) {
|
|
1072
|
+
return value === 0x20 || value === 0x09 || value === 0x0d || value === 0x0a;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function findFirstNonWhitespaceByte(buffer) {
|
|
1076
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
1077
|
+
return -1;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
1081
|
+
if (!isWhitespaceByte(buffer[index])) {
|
|
1082
|
+
return index;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return -1;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function findTcpFrameOnlySyncIndex(buffer, startIndex = 0) {
|
|
1089
|
+
if (!Buffer.isBuffer(buffer) || buffer.length <= 0) {
|
|
1090
|
+
return -1;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const safeStart = Math.max(0, Math.min(buffer.length, Math.floor(Number(startIndex) || 0)));
|
|
1094
|
+
const magicIndex = buffer.indexOf(TCP_BINARY_FRAME_MAGIC, safeStart);
|
|
1095
|
+
const jsonIndex = buffer.indexOf(0x7b, safeStart);
|
|
1096
|
+
if (magicIndex < 0) {
|
|
1097
|
+
return jsonIndex;
|
|
1098
|
+
}
|
|
1099
|
+
if (jsonIndex < 0) {
|
|
1100
|
+
return magicIndex;
|
|
1101
|
+
}
|
|
1102
|
+
return Math.min(magicIndex, jsonIndex);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function keepTcpFrameOnlySyncTail(buffer) {
|
|
1106
|
+
if (!Buffer.isBuffer(buffer) || buffer.length <= 0) {
|
|
1107
|
+
return Buffer.alloc(0);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const maxTailLength = Math.min(buffer.length, TCP_BINARY_FRAME_MAGIC.length - 1);
|
|
1111
|
+
for (let tailLength = maxTailLength; tailLength > 0; tailLength -= 1) {
|
|
1112
|
+
const tail = buffer.subarray(buffer.length - tailLength);
|
|
1113
|
+
if (hasTcpBinaryFrameMagicPrefix(tail)) {
|
|
1114
|
+
return tail;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return Buffer.alloc(0);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function parseTcpBinaryFramePacket(buffer) {
|
|
1121
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < TCP_BINARY_FRAME_HEADER_BYTES) {
|
|
1122
|
+
return { incomplete: true };
|
|
1123
|
+
}
|
|
1124
|
+
if (!startsWithTcpBinaryFrameMagic(buffer)) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const metaLength = buffer.readUInt32BE(4);
|
|
1129
|
+
const payloadLength = buffer.readUInt32BE(8);
|
|
1130
|
+
const maxPayloadBytes = Math.max(MAX_THUMBNAIL_BINARY_BYTES, MAX_STREAM_BINARY_BYTES, MAX_AUDIO_BINARY_BYTES);
|
|
1131
|
+
if (!Number.isFinite(metaLength)
|
|
1132
|
+
|| !Number.isFinite(payloadLength)
|
|
1133
|
+
|| metaLength <= 0
|
|
1134
|
+
|| metaLength > 64 * 1024
|
|
1135
|
+
|| payloadLength <= 0
|
|
1136
|
+
|| payloadLength > maxPayloadBytes) {
|
|
1137
|
+
return { invalid: true, reason: 'invalid-tcp-binary-frame-size' };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const packetLength = TCP_BINARY_FRAME_HEADER_BYTES + metaLength + payloadLength;
|
|
1141
|
+
if (buffer.length < packetLength) {
|
|
1142
|
+
return { incomplete: true };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
const header = JSON.parse(buffer.subarray(TCP_BINARY_FRAME_HEADER_BYTES, TCP_BINARY_FRAME_HEADER_BYTES + metaLength).toString('utf8'));
|
|
1147
|
+
const payload = buffer.subarray(TCP_BINARY_FRAME_HEADER_BYTES + metaLength, packetLength);
|
|
1148
|
+
return { header, payload, packetLength };
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
return { invalid: true, reason: err?.message || 'invalid-tcp-binary-frame-header' };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
961
1154
|
class RemoteHubWebSocketAgentSocket {
|
|
962
1155
|
constructor(socket) {
|
|
963
1156
|
this.__remoteHubWebSocket = true;
|
|
@@ -973,9 +1166,21 @@ class RemoteHubWebSocketAgentSocket {
|
|
|
973
1166
|
this.onCloseMessage = () => {};
|
|
974
1167
|
}
|
|
975
1168
|
|
|
976
|
-
setNoDelay() {
|
|
1169
|
+
setNoDelay(value = true) {
|
|
1170
|
+
try {
|
|
1171
|
+
this.socket?.setNoDelay?.(value);
|
|
1172
|
+
} catch {
|
|
1173
|
+
// Best-effort latency hint.
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
977
1176
|
|
|
978
|
-
setKeepAlive() {
|
|
1177
|
+
setKeepAlive(value = true, initialDelay = 0) {
|
|
1178
|
+
try {
|
|
1179
|
+
this.socket?.setKeepAlive?.(value, initialDelay);
|
|
1180
|
+
} catch {
|
|
1181
|
+
// Best-effort connection liveness hint.
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
979
1184
|
|
|
980
1185
|
sendJson(payload) {
|
|
981
1186
|
return this.sendText(JSON.stringify(payload));
|
|
@@ -1153,6 +1358,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1153
1358
|
const pairToken = safeString(
|
|
1154
1359
|
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
|
|
1155
1360
|
256);
|
|
1361
|
+
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
1362
|
+
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
1363
|
+
: generatePairingPin();
|
|
1364
|
+
let pairingPinUpdatedAt = new Date().toISOString();
|
|
1156
1365
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
1157
1366
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
1158
1367
|
5000,
|
|
@@ -1167,6 +1376,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1167
1376
|
const devices = new Map();
|
|
1168
1377
|
const taskBatches = new Map();
|
|
1169
1378
|
const sockets = new Map();
|
|
1379
|
+
const inputSockets = new Map();
|
|
1380
|
+
const frameSockets = new Map();
|
|
1381
|
+
const audioSockets = new Map();
|
|
1382
|
+
const fileSockets = new Map();
|
|
1170
1383
|
const allSockets = new Set();
|
|
1171
1384
|
const duplicateDeviceLogAt = new Map();
|
|
1172
1385
|
let server = null;
|
|
@@ -1485,6 +1698,8 @@ export function createRemoteHub(options = {}) {
|
|
|
1485
1698
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
1486
1699
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
1487
1700
|
pairTokenPreview: maskToken(pairToken),
|
|
1701
|
+
pairingPin: includeSecrets ? pairingPin : '',
|
|
1702
|
+
pairingPinUpdatedAt,
|
|
1488
1703
|
deviceCount: devices.size,
|
|
1489
1704
|
connectedDeviceCount: connectedDevices,
|
|
1490
1705
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -1506,6 +1721,19 @@ export function createRemoteHub(options = {}) {
|
|
|
1506
1721
|
};
|
|
1507
1722
|
}
|
|
1508
1723
|
|
|
1724
|
+
function rotatePairingPin() {
|
|
1725
|
+
pairingPin = generatePairingPin();
|
|
1726
|
+
pairingPinUpdatedAt = new Date().toISOString();
|
|
1727
|
+
emitRemoteEvent('RemotePairingPinRotated', null, {
|
|
1728
|
+
pairingPinUpdatedAt
|
|
1729
|
+
});
|
|
1730
|
+
return {
|
|
1731
|
+
ok: true,
|
|
1732
|
+
pairingPin,
|
|
1733
|
+
pairingPinUpdatedAt
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1509
1737
|
function listDevices(options = {}) {
|
|
1510
1738
|
const serializeOptions = {
|
|
1511
1739
|
includeDataUrl: options.includeDataUrl === true
|
|
@@ -1593,8 +1821,8 @@ export function createRemoteHub(options = {}) {
|
|
|
1593
1821
|
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
1594
1822
|
frameSeq,
|
|
1595
1823
|
commandId: safeString(options.commandId, 128),
|
|
1596
|
-
width: clampNumber(options.width, 2,
|
|
1597
|
-
height: clampNumber(options.height, 1,
|
|
1824
|
+
width: clampNumber(options.width, 2, 3840, mode === 'thumbnail' ? 360 : 640),
|
|
1825
|
+
height: clampNumber(options.height, 1, 2160, mode === 'thumbnail' ? 220 : 360),
|
|
1598
1826
|
mimeType: 'image/png',
|
|
1599
1827
|
format: 'image/png',
|
|
1600
1828
|
mode,
|
|
@@ -1606,7 +1834,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1606
1834
|
transferProtocol: descriptor.transferProtocol,
|
|
1607
1835
|
transferProtocolVersion: descriptor.transferProtocolVersion,
|
|
1608
1836
|
handshake: descriptor.handshake,
|
|
1609
|
-
fps: clampNumber(options.fps, 1,
|
|
1837
|
+
fps: clampNumber(options.fps, 1, 20, mode === 'thumbnail' ? 1 : 2),
|
|
1610
1838
|
capturedAt: now,
|
|
1611
1839
|
receivedAt: now,
|
|
1612
1840
|
byteLength: 68,
|
|
@@ -1614,6 +1842,8 @@ export function createRemoteHub(options = {}) {
|
|
|
1614
1842
|
contentHash: SYNTHETIC_FRAME_HASH,
|
|
1615
1843
|
captureMs: 0,
|
|
1616
1844
|
sameContentStreak: 0,
|
|
1845
|
+
monitorIndex: normalizeMonitorIndex(options.monitorIndex ?? 0),
|
|
1846
|
+
monitorCount: clampNumber(options.monitorCount, 1, 64, 1),
|
|
1617
1847
|
dataUrl: SYNTHETIC_FRAME_DATA_URL,
|
|
1618
1848
|
payload: SYNTHETIC_FRAME_PAYLOAD,
|
|
1619
1849
|
accessToken: createFrameAccessToken()
|
|
@@ -1646,15 +1876,18 @@ export function createRemoteHub(options = {}) {
|
|
|
1646
1876
|
}
|
|
1647
1877
|
|
|
1648
1878
|
function applySyntheticLiveFrame(device, streamId, options = {}) {
|
|
1649
|
-
|
|
1879
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
1880
|
+
if (!streamState?.active) {
|
|
1650
1881
|
return null;
|
|
1651
1882
|
}
|
|
1652
1883
|
|
|
1653
|
-
const frame = makeSyntheticFrame(device, streamId ||
|
|
1654
|
-
commandId: options.commandId ||
|
|
1884
|
+
const frame = makeSyntheticFrame(device, streamId || streamState.streamId, streamState.mode || DEFAULT_REMOTE_FRAME_MODE, {
|
|
1885
|
+
commandId: options.commandId || streamState.commandId,
|
|
1655
1886
|
width: options.maxWidth || 960,
|
|
1656
1887
|
height: options.maxHeight || 540,
|
|
1657
|
-
|
|
1888
|
+
monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
|
|
1889
|
+
monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
|
|
1890
|
+
fps: options.fps || streamState.fps
|
|
1658
1891
|
});
|
|
1659
1892
|
device.latestLiveFrame = frame;
|
|
1660
1893
|
rememberRecentFramePayload(device, 'live', frame);
|
|
@@ -1666,9 +1899,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1666
1899
|
mimeType: frame.mimeType,
|
|
1667
1900
|
byteLength: frame.byteLength
|
|
1668
1901
|
});
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1902
|
+
streamState.latestFrame = frame;
|
|
1903
|
+
streamState.lastFrameAt = frame.receivedAt;
|
|
1904
|
+
streamState.lastFrameSeq = frame.frameSeq;
|
|
1905
|
+
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
1672
1906
|
device.lastSeenAt = frame.receivedAt;
|
|
1673
1907
|
device.counters.liveFramesReceived += 1;
|
|
1674
1908
|
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
@@ -1681,6 +1915,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1681
1915
|
|
|
1682
1916
|
function closeExistingDeviceSocket(deviceId, nextSessionId) {
|
|
1683
1917
|
const existing = devices.get(deviceId);
|
|
1918
|
+
closeInputSocket(existing, 'replaced-by-new-session');
|
|
1919
|
+
closeFrameSocket(existing, 'replaced-by-new-session');
|
|
1920
|
+
closeAudioSocket(existing, 'replaced-by-new-session');
|
|
1921
|
+
closeFileSocket(existing, 'replaced-by-new-session');
|
|
1684
1922
|
if (!existing?.socket || existing.socket.destroyed) {
|
|
1685
1923
|
return;
|
|
1686
1924
|
}
|
|
@@ -1695,6 +1933,154 @@ export function createRemoteHub(options = {}) {
|
|
|
1695
1933
|
existing.socket.destroy();
|
|
1696
1934
|
}
|
|
1697
1935
|
|
|
1936
|
+
function closeFrameSocket(device, reason = 'frame-socket-closed') {
|
|
1937
|
+
const socket = device?.frameSocket;
|
|
1938
|
+
if (!socket) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
device.frameSocket = null;
|
|
1943
|
+
frameSockets.delete(socket);
|
|
1944
|
+
try {
|
|
1945
|
+
writeJsonLine(socket, { type: 'disconnect', channel: 'frame', reason });
|
|
1946
|
+
} catch {
|
|
1947
|
+
// Best-effort notice before closing the side channel.
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
socket.destroy?.();
|
|
1951
|
+
} catch {
|
|
1952
|
+
// Ignore close failures.
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function closeAudioSocket(device, reason = 'audio-socket-closed') {
|
|
1957
|
+
const socket = device?.audioSocket;
|
|
1958
|
+
if (!socket) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
device.audioSocket = null;
|
|
1963
|
+
audioSockets.delete(socket);
|
|
1964
|
+
try {
|
|
1965
|
+
writeJsonLine(socket, { type: 'disconnect', channel: 'audio', reason });
|
|
1966
|
+
} catch {
|
|
1967
|
+
// Best-effort notice before closing the side channel.
|
|
1968
|
+
}
|
|
1969
|
+
try {
|
|
1970
|
+
socket.destroy?.();
|
|
1971
|
+
} catch {
|
|
1972
|
+
// Ignore close failures.
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
1977
|
+
const socket = device?.fileSocket;
|
|
1978
|
+
if (!socket) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
device.fileSocket = null;
|
|
1983
|
+
fileSockets.delete(socket);
|
|
1984
|
+
try {
|
|
1985
|
+
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
1986
|
+
} catch {
|
|
1987
|
+
// Best-effort notice before closing the side channel.
|
|
1988
|
+
}
|
|
1989
|
+
try {
|
|
1990
|
+
socket.destroy?.();
|
|
1991
|
+
} catch {
|
|
1992
|
+
// Ignore close failures.
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
1997
|
+
const socket = device?.inputSocket;
|
|
1998
|
+
if (!socket) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
device.inputSocket = null;
|
|
2003
|
+
inputSockets.delete(socket);
|
|
2004
|
+
try {
|
|
2005
|
+
writeJsonLine(socket, { type: 'disconnect', reason });
|
|
2006
|
+
} catch {
|
|
2007
|
+
// Best-effort notice before closing the side channel.
|
|
2008
|
+
}
|
|
2009
|
+
try {
|
|
2010
|
+
socket.destroy?.();
|
|
2011
|
+
} catch {
|
|
2012
|
+
// Ignore close failures.
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
2017
|
+
const deviceId = inputSockets.get(socket);
|
|
2018
|
+
inputSockets.delete(socket);
|
|
2019
|
+
if (!deviceId) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const device = devices.get(deviceId);
|
|
2024
|
+
if (!device || device.inputSocket !== socket) {
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
device.inputSocket = null;
|
|
2029
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
2030
|
+
emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function detachFrameSocket(socket, reason = 'frame-socket-closed') {
|
|
2034
|
+
const deviceId = frameSockets.get(socket);
|
|
2035
|
+
frameSockets.delete(socket);
|
|
2036
|
+
if (!deviceId) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const device = devices.get(deviceId);
|
|
2041
|
+
if (!device || device.frameSocket !== socket) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
device.frameSocket = null;
|
|
2046
|
+
device.frameLastSeenAt = new Date().toISOString();
|
|
2047
|
+
emitRemoteEvent('RemoteFrameSocketDisconnected', device, { reason });
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function detachAudioSocket(socket, reason = 'audio-socket-closed') {
|
|
2051
|
+
const deviceId = audioSockets.get(socket);
|
|
2052
|
+
audioSockets.delete(socket);
|
|
2053
|
+
if (!deviceId) {
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
const device = devices.get(deviceId);
|
|
2058
|
+
if (!device || device.audioSocket !== socket) {
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
device.audioSocket = null;
|
|
2063
|
+
device.audioLastSeenAt = new Date().toISOString();
|
|
2064
|
+
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
2068
|
+
const deviceId = fileSockets.get(socket);
|
|
2069
|
+
fileSockets.delete(socket);
|
|
2070
|
+
if (!deviceId) {
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const device = devices.get(deviceId);
|
|
2075
|
+
if (!device || device.fileSocket !== socket) {
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
device.fileSocket = null;
|
|
2080
|
+
device.fileLastSeenAt = new Date().toISOString();
|
|
2081
|
+
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1698
2084
|
function getDeviceActivityMs(device) {
|
|
1699
2085
|
return Math.max(
|
|
1700
2086
|
Date.parse(device?.lastSeenAt || '') || 0,
|
|
@@ -2003,7 +2389,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2003
2389
|
transferProtocol: transfer.transferProtocol,
|
|
2004
2390
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2005
2391
|
handshake: transfer.handshake,
|
|
2006
|
-
fps:
|
|
2392
|
+
fps: 2,
|
|
2007
2393
|
startedAt: seenAt,
|
|
2008
2394
|
stoppedAt: '',
|
|
2009
2395
|
stopReason: '',
|
|
@@ -2013,7 +2399,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2013
2399
|
};
|
|
2014
2400
|
applySyntheticLiveFrame(device, device.activeLiveStream.streamId, {
|
|
2015
2401
|
commandId: device.activeLiveStream.commandId,
|
|
2016
|
-
fps:
|
|
2402
|
+
fps: 2
|
|
2017
2403
|
});
|
|
2018
2404
|
live += 1;
|
|
2019
2405
|
}
|
|
@@ -2079,6 +2465,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2079
2465
|
|
|
2080
2466
|
const device = {
|
|
2081
2467
|
socket,
|
|
2468
|
+
inputSocket: null,
|
|
2469
|
+
frameSocket: null,
|
|
2470
|
+
audioSocket: null,
|
|
2471
|
+
fileSocket: null,
|
|
2082
2472
|
deviceId,
|
|
2083
2473
|
sessionId,
|
|
2084
2474
|
deviceName: safeString(hello.deviceName || hello.hostname || deviceId, 120),
|
|
@@ -2095,6 +2485,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2095
2485
|
capabilities,
|
|
2096
2486
|
connected: true,
|
|
2097
2487
|
connectedAt: now,
|
|
2488
|
+
inputConnectedAt: '',
|
|
2489
|
+
inputLastSeenAt: '',
|
|
2490
|
+
frameConnectedAt: '',
|
|
2491
|
+
frameLastSeenAt: '',
|
|
2492
|
+
audioConnectedAt: '',
|
|
2493
|
+
audioLastSeenAt: '',
|
|
2494
|
+
fileConnectedAt: '',
|
|
2495
|
+
fileLastSeenAt: '',
|
|
2098
2496
|
disconnectedAt: '',
|
|
2099
2497
|
lastSeenAt: now,
|
|
2100
2498
|
lastStatusAt: '',
|
|
@@ -2138,6 +2536,126 @@ export function createRemoteHub(options = {}) {
|
|
|
2138
2536
|
return device;
|
|
2139
2537
|
}
|
|
2140
2538
|
|
|
2539
|
+
function attachInputSocket(socket, hello) {
|
|
2540
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
2541
|
+
const device = devices.get(deviceId);
|
|
2542
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
2543
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
2544
|
+
socket.destroy();
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
2549
|
+
|
|
2550
|
+
const now = new Date().toISOString();
|
|
2551
|
+
device.inputSocket = socket;
|
|
2552
|
+
device.inputConnectedAt = now;
|
|
2553
|
+
device.inputLastSeenAt = now;
|
|
2554
|
+
inputSockets.set(socket, deviceId);
|
|
2555
|
+
|
|
2556
|
+
writeJsonLine(socket, {
|
|
2557
|
+
type: 'welcome',
|
|
2558
|
+
channel: 'input',
|
|
2559
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
2560
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2561
|
+
sessionId: device.sessionId,
|
|
2562
|
+
deviceId,
|
|
2563
|
+
serverTime: now
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
emitRemoteEvent('RemoteInputSocketConnected', device);
|
|
2567
|
+
return device;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function attachFrameSocket(socket, hello) {
|
|
2571
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
2572
|
+
const device = devices.get(deviceId);
|
|
2573
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
2574
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
2575
|
+
socket.destroy();
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
closeFrameSocket(device, 'replaced-by-new-frame-socket');
|
|
2580
|
+
|
|
2581
|
+
const now = new Date().toISOString();
|
|
2582
|
+
device.frameSocket = socket;
|
|
2583
|
+
device.frameConnectedAt = now;
|
|
2584
|
+
device.frameLastSeenAt = now;
|
|
2585
|
+
frameSockets.set(socket, deviceId);
|
|
2586
|
+
|
|
2587
|
+
writeJsonLine(socket, {
|
|
2588
|
+
type: 'welcome',
|
|
2589
|
+
channel: 'frame',
|
|
2590
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
2591
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2592
|
+
sessionId: device.sessionId,
|
|
2593
|
+
deviceId,
|
|
2594
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2595
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2596
|
+
serverTime: now
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
emitRemoteEvent('RemoteFrameSocketConnected', device);
|
|
2600
|
+
return device;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function attachAudioSocket(socket, hello) {
|
|
2604
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
2605
|
+
const device = devices.get(deviceId);
|
|
2606
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
2607
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
2608
|
+
socket.destroy();
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
closeAudioSocket(device, 'replaced-by-new-audio-socket');
|
|
2613
|
+
const now = new Date().toISOString();
|
|
2614
|
+
device.audioSocket = socket;
|
|
2615
|
+
device.audioConnectedAt = now;
|
|
2616
|
+
device.audioLastSeenAt = now;
|
|
2617
|
+
audioSockets.set(socket, deviceId);
|
|
2618
|
+
writeJsonLine(socket, {
|
|
2619
|
+
type: 'welcome',
|
|
2620
|
+
channel: 'audio',
|
|
2621
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
2622
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2623
|
+
sessionId: device.sessionId,
|
|
2624
|
+
deviceId,
|
|
2625
|
+
serverTime: now
|
|
2626
|
+
});
|
|
2627
|
+
emitRemoteEvent('RemoteAudioSocketConnected', device);
|
|
2628
|
+
return device;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
function attachFileSocket(socket, hello) {
|
|
2632
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
2633
|
+
const device = devices.get(deviceId);
|
|
2634
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
2635
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
2636
|
+
socket.destroy();
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
2641
|
+
const now = new Date().toISOString();
|
|
2642
|
+
device.fileSocket = socket;
|
|
2643
|
+
device.fileConnectedAt = now;
|
|
2644
|
+
device.fileLastSeenAt = now;
|
|
2645
|
+
fileSockets.set(socket, deviceId);
|
|
2646
|
+
writeJsonLine(socket, {
|
|
2647
|
+
type: 'welcome',
|
|
2648
|
+
channel: 'file',
|
|
2649
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
2650
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2651
|
+
sessionId: device.sessionId,
|
|
2652
|
+
deviceId,
|
|
2653
|
+
serverTime: now
|
|
2654
|
+
});
|
|
2655
|
+
emitRemoteEvent('RemoteFileSocketConnected', device);
|
|
2656
|
+
return device;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2141
2659
|
function detachSocket(socket, reason = 'socket-closed') {
|
|
2142
2660
|
const deviceId = sockets.get(socket);
|
|
2143
2661
|
sockets.delete(socket);
|
|
@@ -2152,13 +2670,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2152
2670
|
|
|
2153
2671
|
device.connected = false;
|
|
2154
2672
|
device.socket = null;
|
|
2673
|
+
closeInputSocket(device, reason);
|
|
2674
|
+
closeFrameSocket(device, reason);
|
|
2155
2675
|
device.disconnectedAt = new Date().toISOString();
|
|
2156
2676
|
device.lastDisconnectReason = reason;
|
|
2157
|
-
|
|
2158
|
-
device.activeLiveStream.active = false;
|
|
2159
|
-
device.activeLiveStream.stoppedAt = device.disconnectedAt;
|
|
2160
|
-
device.activeLiveStream.stopReason = reason;
|
|
2161
|
-
}
|
|
2677
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2162
2678
|
failAllPendingTasks(device, 'device-disconnected');
|
|
2163
2679
|
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2164
2680
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
@@ -2500,7 +3016,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2500
3016
|
|
|
2501
3017
|
function buildFramePayloadBuffer(framePayload, frameData) {
|
|
2502
3018
|
if (Buffer.isBuffer(framePayload)) {
|
|
2503
|
-
return
|
|
3019
|
+
return framePayload;
|
|
2504
3020
|
}
|
|
2505
3021
|
|
|
2506
3022
|
const raw = String(frameData || '');
|
|
@@ -2534,6 +3050,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2534
3050
|
: 0;
|
|
2535
3051
|
}
|
|
2536
3052
|
|
|
3053
|
+
function readFrameStageMs(message, property) {
|
|
3054
|
+
const value = Number(message?.[property]);
|
|
3055
|
+
return Number.isFinite(value) && value >= 0
|
|
3056
|
+
? Math.round(value)
|
|
3057
|
+
: 0;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2537
3060
|
function computeSameContentStreak(previousFrame, contentHash) {
|
|
2538
3061
|
if (!contentHash || !previousFrame?.contentHash || previousFrame.contentHash !== contentHash) {
|
|
2539
3062
|
return 0;
|
|
@@ -2572,6 +3095,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2572
3095
|
commandId: safeString(message.commandId, 128),
|
|
2573
3096
|
width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
|
|
2574
3097
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
3098
|
+
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(message.width || 0),
|
|
3099
|
+
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(message.height || 0),
|
|
2575
3100
|
mimeType,
|
|
2576
3101
|
format: mimeType,
|
|
2577
3102
|
frameMode: transfer.frameMode,
|
|
@@ -2579,6 +3104,24 @@ export function createRemoteHub(options = {}) {
|
|
|
2579
3104
|
encoding: transfer.encoding,
|
|
2580
3105
|
codec: transfer.codec,
|
|
2581
3106
|
compression: transfer.compression || mimeType,
|
|
3107
|
+
pixelFormat: safeString(message.pixelFormat, 40),
|
|
3108
|
+
bytesPerPixel: Number.isFinite(Number(message.bytesPerPixel)) ? Number(message.bytesPerPixel) : 0,
|
|
3109
|
+
uncompressedByteLength: Number.isFinite(Number(message.uncompressedByteLength)) ? Number(message.uncompressedByteLength) : 0,
|
|
3110
|
+
h264Format: safeString(message.h264Format, 40),
|
|
3111
|
+
isKeyFrame: message.isKeyFrame === true || safeString(message.chunkType, 20).toLowerCase() === 'key',
|
|
3112
|
+
chunkType: safeString(message.chunkType, 20),
|
|
3113
|
+
timestampUs: Number.isFinite(Number(message.timestampUs)) ? Number(message.timestampUs) : 0,
|
|
3114
|
+
durationUs: Number.isFinite(Number(message.durationUs)) ? Number(message.durationUs) : 0,
|
|
3115
|
+
sourceGapMs: Number.isFinite(Number(message.sourceGapMs)) ? Number(message.sourceGapMs) : 0,
|
|
3116
|
+
agentSendGapMs: Number.isFinite(Number(message.agentSendGapMs)) ? Number(message.agentSendGapMs) : 0,
|
|
3117
|
+
agentSendDurationMs: Number.isFinite(Number(message.agentSendDurationMs)) ? Number(message.agentSendDurationMs) : 0,
|
|
3118
|
+
agentSendDurationFrameSeq: Number.isFinite(Number(message.agentSendDurationFrameSeq)) ? Number(message.agentSendDurationFrameSeq) : 0,
|
|
3119
|
+
agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
|
|
3120
|
+
agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
|
|
3121
|
+
agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
|
|
3122
|
+
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
3123
|
+
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
3124
|
+
platformProfile: safeString(message.platformProfile, 80),
|
|
2582
3125
|
transferProtocol: transfer.transferProtocol,
|
|
2583
3126
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2584
3127
|
handshake: transfer.handshake,
|
|
@@ -2588,6 +3131,16 @@ export function createRemoteHub(options = {}) {
|
|
|
2588
3131
|
transport,
|
|
2589
3132
|
contentHash,
|
|
2590
3133
|
captureMs: readFrameCaptureMs(message),
|
|
3134
|
+
captureStageMs: readFrameStageMs(message, 'captureStageMs'),
|
|
3135
|
+
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
3136
|
+
compressMs: readFrameStageMs(message, 'compressMs'),
|
|
3137
|
+
captureSourceSerial: Number.isFinite(Number(message.captureSourceSerial)) ? Number(message.captureSourceSerial) : 0,
|
|
3138
|
+
captureSourceSkipped: Number.isFinite(Number(message.captureSourceSkipped)) ? Number(message.captureSourceSkipped) : 0,
|
|
3139
|
+
deltaTileSize: Number.isFinite(Number(message.deltaTileSize)) ? Number(message.deltaTileSize) : 0,
|
|
3140
|
+
deltaTileCount: Number.isFinite(Number(message.deltaTileCount)) ? Number(message.deltaTileCount) : 0,
|
|
3141
|
+
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
3142
|
+
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
3143
|
+
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
2591
3144
|
sameContentStreak,
|
|
2592
3145
|
payload,
|
|
2593
3146
|
accessToken: createFrameAccessToken()
|
|
@@ -2608,7 +3161,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2608
3161
|
|
|
2609
3162
|
function applyLiveStreamOpen(device, message, transport = 'json') {
|
|
2610
3163
|
const streamId = safeString(message.streamId, 128) || 'live';
|
|
2611
|
-
|
|
3164
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
3165
|
+
if (!streamState?.active) {
|
|
2612
3166
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
2613
3167
|
reason: 'stale-live-stream-open',
|
|
2614
3168
|
streamId,
|
|
@@ -2617,10 +3171,23 @@ export function createRemoteHub(options = {}) {
|
|
|
2617
3171
|
return false;
|
|
2618
3172
|
}
|
|
2619
3173
|
|
|
2620
|
-
const
|
|
3174
|
+
const commandId = safeString(message.commandId, 128);
|
|
3175
|
+
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3176
|
+
if (commandId && activeCommandId && commandId !== activeCommandId) {
|
|
3177
|
+
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3178
|
+
reason: 'stale-live-stream-generation',
|
|
3179
|
+
streamId,
|
|
3180
|
+
commandId,
|
|
3181
|
+
activeCommandId,
|
|
3182
|
+
transport
|
|
3183
|
+
});
|
|
3184
|
+
return false;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
2621
3188
|
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
2622
|
-
|
|
2623
|
-
...
|
|
3189
|
+
const nextStreamState = {
|
|
3190
|
+
...streamState,
|
|
2624
3191
|
open: true,
|
|
2625
3192
|
openedAt,
|
|
2626
3193
|
openTransport: transport,
|
|
@@ -2633,10 +3200,15 @@ export function createRemoteHub(options = {}) {
|
|
|
2633
3200
|
transferProtocol: transfer.transferProtocol,
|
|
2634
3201
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2635
3202
|
handshake: transfer.handshake,
|
|
2636
|
-
width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(
|
|
2637
|
-
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(
|
|
2638
|
-
|
|
3203
|
+
width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(streamState.width || 0),
|
|
3204
|
+
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(streamState.height || 0),
|
|
3205
|
+
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(streamState.sourceWidth || 0),
|
|
3206
|
+
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(streamState.sourceHeight || 0),
|
|
3207
|
+
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
3208
|
+
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
3209
|
+
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(streamState.openedFrameSeq || 0)
|
|
2639
3210
|
};
|
|
3211
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
2640
3212
|
emitRemoteEvent('RemoteLiveStreamOpened', device, {
|
|
2641
3213
|
streamId,
|
|
2642
3214
|
mode: transfer.mode,
|
|
@@ -2655,7 +3227,9 @@ export function createRemoteHub(options = {}) {
|
|
|
2655
3227
|
: safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
|
|
2656
3228
|
const frameSeq = Number(message.frameSeq);
|
|
2657
3229
|
const streamId = safeString(message.streamId, 128) || 'live';
|
|
2658
|
-
|
|
3230
|
+
const commandId = safeString(message.commandId, 128);
|
|
3231
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
3232
|
+
if (!streamState?.active) {
|
|
2659
3233
|
device.counters.liveFramesDropped += 1;
|
|
2660
3234
|
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
2661
3235
|
reason: 'stale-live-stream-frame',
|
|
@@ -2665,6 +3239,19 @@ export function createRemoteHub(options = {}) {
|
|
|
2665
3239
|
});
|
|
2666
3240
|
return false;
|
|
2667
3241
|
}
|
|
3242
|
+
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3243
|
+
if (commandId && activeCommandId && commandId !== activeCommandId) {
|
|
3244
|
+
device.counters.liveFramesDropped += 1;
|
|
3245
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
3246
|
+
reason: 'stale-live-stream-generation-frame',
|
|
3247
|
+
streamId,
|
|
3248
|
+
commandId,
|
|
3249
|
+
activeCommandId,
|
|
3250
|
+
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
3251
|
+
transport
|
|
3252
|
+
});
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
2668
3255
|
|
|
2669
3256
|
const byteLength = normalizeFrameByteLength(framePayload, frameData);
|
|
2670
3257
|
if ((!Buffer.isBuffer(framePayload) && !frameData)
|
|
@@ -2684,28 +3271,31 @@ export function createRemoteHub(options = {}) {
|
|
|
2684
3271
|
const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
|
|
2685
3272
|
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
2686
3273
|
const contentHash = buildFrameContentHash(message, payload);
|
|
2687
|
-
const sameContentStreak = computeSameContentStreak(
|
|
2688
|
-
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message,
|
|
2689
|
-
if (
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3274
|
+
const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
|
|
3275
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3276
|
+
if (streamState.open !== true) {
|
|
3277
|
+
streamState.open = true;
|
|
3278
|
+
streamState.openedAt = device.lastSeenAt;
|
|
3279
|
+
streamState.openTransport = `${transport}-implicit`;
|
|
3280
|
+
streamState.mode = transfer.mode;
|
|
3281
|
+
streamState.frameMode = transfer.frameMode;
|
|
3282
|
+
streamState.frameProfile = transfer.frameProfile;
|
|
3283
|
+
streamState.encoding = transfer.encoding;
|
|
3284
|
+
streamState.codec = transfer.codec;
|
|
3285
|
+
streamState.compression = transfer.compression;
|
|
3286
|
+
streamState.transferProtocol = transfer.transferProtocol;
|
|
3287
|
+
streamState.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
3288
|
+
streamState.handshake = transfer.handshake;
|
|
2702
3289
|
}
|
|
2703
3290
|
device.latestLiveFrame = {
|
|
2704
3291
|
streamId,
|
|
2705
3292
|
frameSeq,
|
|
2706
|
-
|
|
3293
|
+
streamFrameSeq: Number.isFinite(Number(message.streamFrameSeq)) ? Number(message.streamFrameSeq) : frameSeq,
|
|
3294
|
+
commandId,
|
|
2707
3295
|
width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
|
|
2708
3296
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
3297
|
+
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(message.width || 0),
|
|
3298
|
+
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(message.height || 0),
|
|
2709
3299
|
mimeType,
|
|
2710
3300
|
format: mimeType,
|
|
2711
3301
|
mode: transfer.mode,
|
|
@@ -2714,20 +3304,52 @@ export function createRemoteHub(options = {}) {
|
|
|
2714
3304
|
encoding: transfer.encoding,
|
|
2715
3305
|
codec: transfer.codec,
|
|
2716
3306
|
compression: transfer.compression || mimeType,
|
|
3307
|
+
pixelFormat: safeString(message.pixelFormat, 40),
|
|
3308
|
+
bytesPerPixel: Number.isFinite(Number(message.bytesPerPixel)) ? Number(message.bytesPerPixel) : 0,
|
|
3309
|
+
uncompressedByteLength: Number.isFinite(Number(message.uncompressedByteLength)) ? Number(message.uncompressedByteLength) : 0,
|
|
3310
|
+
h264Format: safeString(message.h264Format, 40),
|
|
3311
|
+
isKeyFrame: message.isKeyFrame === true || safeString(message.chunkType, 20).toLowerCase() === 'key',
|
|
3312
|
+
chunkType: safeString(message.chunkType, 20),
|
|
3313
|
+
timestampUs: Number.isFinite(Number(message.timestampUs)) ? Number(message.timestampUs) : 0,
|
|
3314
|
+
durationUs: Number.isFinite(Number(message.durationUs)) ? Number(message.durationUs) : 0,
|
|
3315
|
+
sourceGapMs: Number.isFinite(Number(message.sourceGapMs)) ? Number(message.sourceGapMs) : 0,
|
|
3316
|
+
agentSendGapMs: Number.isFinite(Number(message.agentSendGapMs)) ? Number(message.agentSendGapMs) : 0,
|
|
3317
|
+
agentSendDurationMs: Number.isFinite(Number(message.agentSendDurationMs)) ? Number(message.agentSendDurationMs) : 0,
|
|
3318
|
+
agentSendDurationFrameSeq: Number.isFinite(Number(message.agentSendDurationFrameSeq)) ? Number(message.agentSendDurationFrameSeq) : 0,
|
|
3319
|
+
agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
|
|
3320
|
+
agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
|
|
3321
|
+
agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
|
|
3322
|
+
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
3323
|
+
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
3324
|
+
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
3325
|
+
platformProfile: safeString(message.platformProfile, 80),
|
|
3326
|
+
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
3327
|
+
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
2717
3328
|
transferProtocol: transfer.transferProtocol,
|
|
2718
3329
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2719
3330
|
handshake: transfer.handshake,
|
|
2720
|
-
fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) :
|
|
3331
|
+
fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
|
|
2721
3332
|
capturedAt,
|
|
2722
3333
|
receivedAt: device.lastSeenAt,
|
|
2723
3334
|
byteLength,
|
|
2724
3335
|
transport,
|
|
2725
3336
|
contentHash,
|
|
2726
3337
|
captureMs: readFrameCaptureMs(message),
|
|
3338
|
+
captureStageMs: readFrameStageMs(message, 'captureStageMs'),
|
|
3339
|
+
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
3340
|
+
compressMs: readFrameStageMs(message, 'compressMs'),
|
|
3341
|
+
captureSourceSerial: Number.isFinite(Number(message.captureSourceSerial)) ? Number(message.captureSourceSerial) : 0,
|
|
3342
|
+
captureSourceSkipped: Number.isFinite(Number(message.captureSourceSkipped)) ? Number(message.captureSourceSkipped) : 0,
|
|
3343
|
+
deltaTileSize: Number.isFinite(Number(message.deltaTileSize)) ? Number(message.deltaTileSize) : 0,
|
|
3344
|
+
deltaTileCount: Number.isFinite(Number(message.deltaTileCount)) ? Number(message.deltaTileCount) : 0,
|
|
3345
|
+
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
3346
|
+
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
3347
|
+
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
2727
3348
|
sameContentStreak,
|
|
2728
3349
|
payload,
|
|
2729
3350
|
accessToken: createFrameAccessToken()
|
|
2730
3351
|
};
|
|
3352
|
+
streamState.latestFrame = device.latestLiveFrame;
|
|
2731
3353
|
rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
|
|
2732
3354
|
emitFrame({
|
|
2733
3355
|
kind: 'live',
|
|
@@ -2737,29 +3359,26 @@ export function createRemoteHub(options = {}) {
|
|
|
2737
3359
|
mimeType: device.latestLiveFrame.mimeType,
|
|
2738
3360
|
byteLength: device.latestLiveFrame.byteLength
|
|
2739
3361
|
});
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
3362
|
+
streamState.lastFrameAt = device.lastSeenAt;
|
|
3363
|
+
streamState.lastFrameSeq = frameSeq;
|
|
3364
|
+
streamState.framesReceived = (streamState.framesReceived || 0) + 1;
|
|
3365
|
+
streamState.mode = transfer.mode;
|
|
3366
|
+
streamState.frameMode = transfer.frameMode;
|
|
3367
|
+
streamState.frameProfile = transfer.frameProfile;
|
|
3368
|
+
streamState.encoding = transfer.encoding;
|
|
3369
|
+
streamState.codec = transfer.codec;
|
|
3370
|
+
streamState.compression = transfer.compression;
|
|
3371
|
+
streamState.transferProtocol = transfer.transferProtocol;
|
|
3372
|
+
streamState.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
3373
|
+
streamState.handshake = transfer.handshake;
|
|
3374
|
+
streamState.width = device.latestLiveFrame.width;
|
|
3375
|
+
streamState.height = device.latestLiveFrame.height;
|
|
3376
|
+
streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
|
|
3377
|
+
streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
|
|
3378
|
+
streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
|
|
3379
|
+
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
3380
|
+
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
2752
3381
|
device.counters.liveFramesReceived += 1;
|
|
2753
|
-
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
2754
|
-
streamId,
|
|
2755
|
-
frameSeq,
|
|
2756
|
-
width: device.latestLiveFrame.width,
|
|
2757
|
-
height: device.latestLiveFrame.height,
|
|
2758
|
-
mode: device.latestLiveFrame.mode,
|
|
2759
|
-
transport,
|
|
2760
|
-
contentHash,
|
|
2761
|
-
sameContentStreak
|
|
2762
|
-
});
|
|
2763
3382
|
return true;
|
|
2764
3383
|
}
|
|
2765
3384
|
|
|
@@ -2827,13 +3446,6 @@ export function createRemoteHub(options = {}) {
|
|
|
2827
3446
|
mimeType,
|
|
2828
3447
|
byteLength
|
|
2829
3448
|
});
|
|
2830
|
-
emitRemoteEvent('RemoteAudioFrameReceived', device, {
|
|
2831
|
-
streamId,
|
|
2832
|
-
frameSeq,
|
|
2833
|
-
mimeType,
|
|
2834
|
-
byteLength,
|
|
2835
|
-
transport
|
|
2836
|
-
});
|
|
2837
3449
|
return true;
|
|
2838
3450
|
}
|
|
2839
3451
|
|
|
@@ -2845,8 +3457,20 @@ export function createRemoteHub(options = {}) {
|
|
|
2845
3457
|
}
|
|
2846
3458
|
|
|
2847
3459
|
const device = state.device;
|
|
3460
|
+
if (state.inputOnly || state.fileOnly) {
|
|
3461
|
+
writeJsonLine(socket, { type: 'error', error: 'input-channel-binary-not-supported' });
|
|
3462
|
+
socket.destroy();
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
2848
3466
|
device.counters.messagesReceived += 1;
|
|
2849
3467
|
device.lastSeenAt = new Date().toISOString();
|
|
3468
|
+
if (state.frameOnly) {
|
|
3469
|
+
device.frameLastSeenAt = device.lastSeenAt;
|
|
3470
|
+
}
|
|
3471
|
+
if (state.audioOnly) {
|
|
3472
|
+
device.audioLastSeenAt = device.lastSeenAt;
|
|
3473
|
+
}
|
|
2850
3474
|
|
|
2851
3475
|
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
2852
3476
|
if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
|
|
@@ -2855,7 +3479,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2855
3479
|
}
|
|
2856
3480
|
|
|
2857
3481
|
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
2858
|
-
applyLiveFrame(device,
|
|
3482
|
+
applyLiveFrame(device, {
|
|
3483
|
+
...header,
|
|
3484
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
3485
|
+
}, framePayload, 'binary');
|
|
2859
3486
|
return;
|
|
2860
3487
|
}
|
|
2861
3488
|
|
|
@@ -2870,6 +3497,103 @@ export function createRemoteHub(options = {}) {
|
|
|
2870
3497
|
});
|
|
2871
3498
|
}
|
|
2872
3499
|
|
|
3500
|
+
function dropQueuedAgentBinaryFrame(state) {
|
|
3501
|
+
if (!Array.isArray(state.binaryQueue) || state.binaryQueue.length === 0) {
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
const deltaIndex = state.binaryQueue.findIndex(item => {
|
|
3505
|
+
const header = item?.header || {};
|
|
3506
|
+
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
3507
|
+
const chunkType = safeString(header.chunkType, 20).toLowerCase();
|
|
3508
|
+
return (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary')
|
|
3509
|
+
&& header.isKeyFrame !== true
|
|
3510
|
+
&& chunkType !== 'key';
|
|
3511
|
+
});
|
|
3512
|
+
const index = deltaIndex >= 0 ? deltaIndex : 0;
|
|
3513
|
+
state.binaryQueue.splice(index, 1);
|
|
3514
|
+
state.binaryQueueDrops = Number(state.binaryQueueDrops || 0) + 1;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function isMode5BinaryHeader(header) {
|
|
3518
|
+
const mode = safeString(header?.frameMode || header?.mode, 40).toLowerCase();
|
|
3519
|
+
return mode === 'mode5-lzo-delta';
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
function dropQueuedMode5AgentBinaryFrame(state, header) {
|
|
3523
|
+
const streamId = safeString(header?.streamId, 160);
|
|
3524
|
+
const dropIndex = state.binaryQueue.findIndex(item => isMode5BinaryHeader(item?.header)
|
|
3525
|
+
&& (!streamId || safeString(item?.header?.streamId, 160) === streamId));
|
|
3526
|
+
if (dropIndex < 0) {
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
state.binaryQueue.splice(dropIndex, 1);
|
|
3530
|
+
state.binaryQueueDrops = Number(state.binaryQueueDrops || 0) + 1;
|
|
3531
|
+
return true;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
function scheduleAgentBinaryIngress(socket, state) {
|
|
3535
|
+
if (!state || state.closed || !socket || socket.destroyed) {
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
state.binaryQueueSocket = socket;
|
|
3540
|
+
if (state.binaryQueueScheduled === true) {
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
state.binaryQueueScheduled = true;
|
|
3544
|
+
setImmediate(() => drainAgentBinaryIngressLane(state));
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
function drainAgentBinaryIngressLane(state) {
|
|
3548
|
+
state.binaryQueueScheduled = false;
|
|
3549
|
+
const socket = state.binaryQueueSocket;
|
|
3550
|
+
if (state.closed || !socket || socket.destroyed) {
|
|
3551
|
+
if (Array.isArray(state.binaryQueue)) {
|
|
3552
|
+
state.binaryQueue.length = 0;
|
|
3553
|
+
}
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
let processed = 0;
|
|
3558
|
+
while (state.binaryQueue.length > 0 && processed < AGENT_BINARY_INGRESS_DRAIN_BUDGET_PACKETS) {
|
|
3559
|
+
const item = state.binaryQueue.shift();
|
|
3560
|
+
if (!item) {
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
handleAgentBinaryFrame(socket, state, item.header, item.payload);
|
|
3564
|
+
processed += 1;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
if (state.binaryQueue.length > 0 && !state.closed && !socket.destroyed) {
|
|
3568
|
+
scheduleAgentBinaryIngress(socket, state);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function enqueueAgentBinaryFrame(socket, state, header, payload) {
|
|
3573
|
+
if (state.closed || socket.destroyed) {
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
if (!Array.isArray(state.binaryQueue)) {
|
|
3577
|
+
state.binaryQueue = [];
|
|
3578
|
+
}
|
|
3579
|
+
if (isMode5BinaryHeader(header)) {
|
|
3580
|
+
const streamId = safeString(header?.streamId, 160);
|
|
3581
|
+
let queuedMode5Packets = state.binaryQueue.filter(item => isMode5BinaryHeader(item?.header)
|
|
3582
|
+
&& (!streamId || safeString(item?.header?.streamId, 160) === streamId)).length;
|
|
3583
|
+
while (queuedMode5Packets >= MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS) {
|
|
3584
|
+
if (!dropQueuedMode5AgentBinaryFrame(state, header)) {
|
|
3585
|
+
break;
|
|
3586
|
+
}
|
|
3587
|
+
queuedMode5Packets -= 1;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
while (state.binaryQueue.length >= AGENT_BINARY_INGRESS_QUEUE_PACKETS) {
|
|
3591
|
+
dropQueuedAgentBinaryFrame(state);
|
|
3592
|
+
}
|
|
3593
|
+
state.binaryQueue.push({ header, payload });
|
|
3594
|
+
scheduleAgentBinaryIngress(socket, state);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
2873
3597
|
function handleAgentMessage(socket, state, message) {
|
|
2874
3598
|
if (!message || typeof message !== 'object') {
|
|
2875
3599
|
return;
|
|
@@ -2888,6 +3612,52 @@ export function createRemoteHub(options = {}) {
|
|
|
2888
3612
|
return;
|
|
2889
3613
|
}
|
|
2890
3614
|
|
|
3615
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
3616
|
+
if (channel === 'input') {
|
|
3617
|
+
const device = attachInputSocket(socket, message);
|
|
3618
|
+
if (!device) {
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
state.authenticated = true;
|
|
3623
|
+
state.device = device;
|
|
3624
|
+
state.inputOnly = true;
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
if (channel === 'frame') {
|
|
3628
|
+
const device = attachFrameSocket(socket, message);
|
|
3629
|
+
if (!device) {
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
state.authenticated = true;
|
|
3634
|
+
state.device = device;
|
|
3635
|
+
state.frameOnly = true;
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
if (channel === 'audio') {
|
|
3639
|
+
const device = attachAudioSocket(socket, message);
|
|
3640
|
+
if (!device) {
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
state.authenticated = true;
|
|
3645
|
+
state.device = device;
|
|
3646
|
+
state.audioOnly = true;
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (channel === 'file') {
|
|
3650
|
+
const device = attachFileSocket(socket, message);
|
|
3651
|
+
if (!device) {
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
state.authenticated = true;
|
|
3656
|
+
state.device = device;
|
|
3657
|
+
state.fileOnly = true;
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
2891
3661
|
const device = attachDevice(socket, message);
|
|
2892
3662
|
if (!device) {
|
|
2893
3663
|
return;
|
|
@@ -2904,6 +3674,34 @@ export function createRemoteHub(options = {}) {
|
|
|
2904
3674
|
return;
|
|
2905
3675
|
}
|
|
2906
3676
|
|
|
3677
|
+
if (state.inputOnly) {
|
|
3678
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
3679
|
+
if (message.type === 'input.applied') {
|
|
3680
|
+
emitEvent('RemoteInputApplied', {
|
|
3681
|
+
deviceId: device.deviceId,
|
|
3682
|
+
inputSeq: Number(message.inputSeq || 0) || 0,
|
|
3683
|
+
inputType: safeString(message.inputType, 48),
|
|
3684
|
+
issuedAtEpochMs: Number(message.issuedAtEpochMs || 0) || 0,
|
|
3685
|
+
hubReceivedAtEpochMs: Number(message.hubReceivedAtEpochMs || 0) || 0,
|
|
3686
|
+
hubForwardedAtEpochMs: Number(message.hubForwardedAtEpochMs || 0) || 0,
|
|
3687
|
+
agentApplyMs: Number(message.agentApplyMs || 0) || 0,
|
|
3688
|
+
hubAcknowledgedAtEpochMs: Date.now()
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (state.frameOnly) {
|
|
3694
|
+
device.frameLastSeenAt = new Date().toISOString();
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (state.audioOnly) {
|
|
3698
|
+
device.audioLastSeenAt = new Date().toISOString();
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (state.fileOnly) {
|
|
3702
|
+
device.fileLastSeenAt = new Date().toISOString();
|
|
3703
|
+
}
|
|
3704
|
+
|
|
2907
3705
|
device.counters.messagesReceived += 1;
|
|
2908
3706
|
device.lastSeenAt = new Date().toISOString();
|
|
2909
3707
|
|
|
@@ -2975,7 +3773,17 @@ export function createRemoteHub(options = {}) {
|
|
|
2975
3773
|
|
|
2976
3774
|
const state = {
|
|
2977
3775
|
authenticated: false,
|
|
2978
|
-
device: null
|
|
3776
|
+
device: null,
|
|
3777
|
+
inputOnly: false,
|
|
3778
|
+
frameOnly: false,
|
|
3779
|
+
audioOnly: false,
|
|
3780
|
+
fileOnly: false,
|
|
3781
|
+
binaryQueue: [],
|
|
3782
|
+
binaryQueueDraining: false,
|
|
3783
|
+
binaryQueueScheduled: false,
|
|
3784
|
+
binaryQueueSocket: null,
|
|
3785
|
+
binaryQueueDrops: 0,
|
|
3786
|
+
closed: false
|
|
2979
3787
|
};
|
|
2980
3788
|
|
|
2981
3789
|
const helloTimer = setTimeout(() => {
|
|
@@ -3016,7 +3824,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3016
3824
|
return;
|
|
3017
3825
|
}
|
|
3018
3826
|
|
|
3019
|
-
|
|
3827
|
+
enqueueAgentBinaryFrame(socket, state, packet.header, packet.payload);
|
|
3020
3828
|
} catch (err) {
|
|
3021
3829
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
3022
3830
|
logWarn('remote', `invalid websocket binary frame: ${err?.message || err}`);
|
|
@@ -3024,8 +3832,16 @@ export function createRemoteHub(options = {}) {
|
|
|
3024
3832
|
};
|
|
3025
3833
|
|
|
3026
3834
|
const detach = reason => {
|
|
3835
|
+
state.closed = true;
|
|
3836
|
+
state.binaryQueue.length = 0;
|
|
3837
|
+
state.binaryQueueScheduled = false;
|
|
3838
|
+
state.binaryQueueSocket = null;
|
|
3027
3839
|
allSockets.delete(socket);
|
|
3028
3840
|
clearTimeout(helloTimer);
|
|
3841
|
+
detachInputSocket(socket, reason);
|
|
3842
|
+
detachFrameSocket(socket, reason);
|
|
3843
|
+
detachAudioSocket(socket, reason);
|
|
3844
|
+
detachFileSocket(socket, reason);
|
|
3029
3845
|
detachSocket(socket, reason);
|
|
3030
3846
|
};
|
|
3031
3847
|
|
|
@@ -3115,8 +3931,18 @@ export function createRemoteHub(options = {}) {
|
|
|
3115
3931
|
const state = {
|
|
3116
3932
|
authenticated: false,
|
|
3117
3933
|
device: null,
|
|
3934
|
+
inputOnly: false,
|
|
3935
|
+
frameOnly: false,
|
|
3936
|
+
audioOnly: false,
|
|
3937
|
+
fileOnly: false,
|
|
3118
3938
|
buffer: Buffer.alloc(0),
|
|
3119
|
-
pendingBinaryFrame: null
|
|
3939
|
+
pendingBinaryFrame: null,
|
|
3940
|
+
binaryQueue: [],
|
|
3941
|
+
binaryQueueDraining: false,
|
|
3942
|
+
binaryQueueScheduled: false,
|
|
3943
|
+
binaryQueueSocket: null,
|
|
3944
|
+
binaryQueueDrops: 0,
|
|
3945
|
+
closed: false
|
|
3120
3946
|
};
|
|
3121
3947
|
|
|
3122
3948
|
const helloTimer = setTimeout(() => {
|
|
@@ -3142,10 +3968,57 @@ export function createRemoteHub(options = {}) {
|
|
|
3142
3968
|
const framePayload = state.buffer.subarray(0, byteLength);
|
|
3143
3969
|
state.buffer = state.buffer.subarray(byteLength);
|
|
3144
3970
|
state.pendingBinaryFrame = null;
|
|
3145
|
-
|
|
3971
|
+
enqueueAgentBinaryFrame(socket, state, header, framePayload);
|
|
3146
3972
|
continue;
|
|
3147
3973
|
}
|
|
3148
3974
|
|
|
3975
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly) && state.buffer.length > 0) {
|
|
3976
|
+
const firstNonWhitespaceIndex = findFirstNonWhitespaceByte(state.buffer);
|
|
3977
|
+
if (firstNonWhitespaceIndex < 0) {
|
|
3978
|
+
state.buffer = Buffer.alloc(0);
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
if (firstNonWhitespaceIndex > 0) {
|
|
3982
|
+
state.buffer = state.buffer.subarray(firstNonWhitespaceIndex);
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
if (state.buffer.length < TCP_BINARY_FRAME_MAGIC.length && hasTcpBinaryFrameMagicPrefix(state.buffer)) {
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
if (startsWithTcpBinaryFrameMagic(state.buffer)) {
|
|
3990
|
+
const packet = parseTcpBinaryFramePacket(state.buffer);
|
|
3991
|
+
if (packet?.incomplete) {
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
if (!packet || packet.invalid) {
|
|
3995
|
+
const syncIndex = findTcpFrameOnlySyncIndex(state.buffer, 1);
|
|
3996
|
+
if (syncIndex > 0) {
|
|
3997
|
+
state.buffer = state.buffer.subarray(syncIndex);
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
state.buffer = keepTcpFrameOnlySyncTail(state.buffer);
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
state.buffer = state.buffer.subarray(packet.packetLength);
|
|
4006
|
+
enqueueAgentBinaryFrame(socket, state, packet.header, packet.payload);
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
if (!isJsonObjectStartByte(state.buffer[0])) {
|
|
4011
|
+
const syncIndex = findTcpFrameOnlySyncIndex(state.buffer, 1);
|
|
4012
|
+
if (syncIndex > 0) {
|
|
4013
|
+
state.buffer = state.buffer.subarray(syncIndex);
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
state.buffer = keepTcpFrameOnlySyncTail(state.buffer);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3149
4022
|
const newlineIndex = state.buffer.indexOf(0x0a);
|
|
3150
4023
|
if (newlineIndex < 0) {
|
|
3151
4024
|
if (state.buffer.length > MAX_LINE_CHARS) {
|
|
@@ -3181,6 +4054,9 @@ export function createRemoteHub(options = {}) {
|
|
|
3181
4054
|
|
|
3182
4055
|
handleAgentMessage(socket, state, message);
|
|
3183
4056
|
} catch (err) {
|
|
4057
|
+
if (state.authenticated && (state.frameOnly || state.audioOnly)) {
|
|
4058
|
+
continue;
|
|
4059
|
+
}
|
|
3184
4060
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
3185
4061
|
logWarn('remote', `invalid agent message: ${err?.message || err}`);
|
|
3186
4062
|
}
|
|
@@ -3193,13 +4069,29 @@ export function createRemoteHub(options = {}) {
|
|
|
3193
4069
|
}
|
|
3194
4070
|
|
|
3195
4071
|
socket.on('close', () => {
|
|
4072
|
+
state.closed = true;
|
|
4073
|
+
state.binaryQueue.length = 0;
|
|
4074
|
+
state.binaryQueueScheduled = false;
|
|
4075
|
+
state.binaryQueueSocket = null;
|
|
3196
4076
|
allSockets.delete(socket);
|
|
3197
4077
|
clearTimeout(helloTimer);
|
|
4078
|
+
detachInputSocket(socket);
|
|
4079
|
+
detachFrameSocket(socket);
|
|
4080
|
+
detachAudioSocket(socket);
|
|
4081
|
+
detachFileSocket(socket);
|
|
3198
4082
|
detachSocket(socket);
|
|
3199
4083
|
});
|
|
3200
4084
|
socket.on('error', err => {
|
|
4085
|
+
state.closed = true;
|
|
4086
|
+
state.binaryQueue.length = 0;
|
|
4087
|
+
state.binaryQueueScheduled = false;
|
|
4088
|
+
state.binaryQueueSocket = null;
|
|
3201
4089
|
allSockets.delete(socket);
|
|
3202
4090
|
clearTimeout(helloTimer);
|
|
4091
|
+
detachInputSocket(socket, err?.message || 'socket-error');
|
|
4092
|
+
detachFrameSocket(socket, err?.message || 'socket-error');
|
|
4093
|
+
detachAudioSocket(socket, err?.message || 'socket-error');
|
|
4094
|
+
detachFileSocket(socket, err?.message || 'socket-error');
|
|
3203
4095
|
detachSocket(socket, err?.message || 'socket-error');
|
|
3204
4096
|
});
|
|
3205
4097
|
}
|
|
@@ -3236,9 +4128,9 @@ export function createRemoteHub(options = {}) {
|
|
|
3236
4128
|
started = true;
|
|
3237
4129
|
boundPort = server.address()?.port || requestedPort;
|
|
3238
4130
|
lastError = '';
|
|
3239
|
-
logEvent('remote', `
|
|
4131
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
3240
4132
|
if (host === '0.0.0.0' || host === '::') {
|
|
3241
|
-
logWarn('remote', '
|
|
4133
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
|
|
3242
4134
|
}
|
|
3243
4135
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
3244
4136
|
resolve();
|
|
@@ -3301,11 +4193,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3301
4193
|
device.connected = false;
|
|
3302
4194
|
device.disconnectedAt = new Date().toISOString();
|
|
3303
4195
|
device.lastDisconnectReason = reason;
|
|
3304
|
-
|
|
3305
|
-
device.activeLiveStream.active = false;
|
|
3306
|
-
device.activeLiveStream.stoppedAt = device.disconnectedAt;
|
|
3307
|
-
device.activeLiveStream.stopReason = reason;
|
|
3308
|
-
}
|
|
4196
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
3309
4197
|
failAllPendingTasks(device, 'device-disconnected');
|
|
3310
4198
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
|
|
3311
4199
|
return true;
|
|
@@ -3350,19 +4238,26 @@ export function createRemoteHub(options = {}) {
|
|
|
3350
4238
|
}
|
|
3351
4239
|
|
|
3352
4240
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
4241
|
+
const commandName = safeString(command?.command || 'ping', 80);
|
|
3353
4242
|
const payload = {
|
|
3354
4243
|
type: 'command',
|
|
3355
4244
|
commandId,
|
|
3356
|
-
command:
|
|
4245
|
+
command: commandName,
|
|
3357
4246
|
payload: command?.payload ?? null,
|
|
3358
4247
|
issuedAt: new Date().toISOString()
|
|
3359
4248
|
};
|
|
3360
4249
|
|
|
3361
|
-
|
|
4250
|
+
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
4251
|
+
&& device.fileSocket
|
|
4252
|
+
&& !device.fileSocket.destroyed
|
|
4253
|
+
? device.fileSocket
|
|
4254
|
+
: null;
|
|
4255
|
+
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
3362
4256
|
device.counters.commandsSent += 1;
|
|
3363
4257
|
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3364
4258
|
commandId,
|
|
3365
|
-
command: payload.command
|
|
4259
|
+
command: payload.command,
|
|
4260
|
+
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
3366
4261
|
});
|
|
3367
4262
|
return { ok: true, commandId };
|
|
3368
4263
|
}
|
|
@@ -3386,10 +4281,30 @@ export function createRemoteHub(options = {}) {
|
|
|
3386
4281
|
return { ok: false, error: 'missing-input-type' };
|
|
3387
4282
|
}
|
|
3388
4283
|
|
|
4284
|
+
const inputSocket = device.inputSocket;
|
|
4285
|
+
if (inputSocket && !inputSocket.destroyed) {
|
|
4286
|
+
const sent = writeJsonLine(inputSocket, {
|
|
4287
|
+
type: 'input.control',
|
|
4288
|
+
payload: {
|
|
4289
|
+
...normalized,
|
|
4290
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
4291
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
if (sent) {
|
|
4295
|
+
device.counters.commandsSent += 1;
|
|
4296
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
4297
|
+
return { ok: true, inputSocket: true };
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
4301
|
+
}
|
|
4302
|
+
|
|
3389
4303
|
return sendCommand(deviceId, {
|
|
3390
4304
|
command: 'input.control',
|
|
3391
4305
|
payload: {
|
|
3392
4306
|
...normalized,
|
|
4307
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
3393
4308
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
3394
4309
|
}
|
|
3395
4310
|
});
|
|
@@ -3603,6 +4518,121 @@ export function createRemoteHub(options = {}) {
|
|
|
3603
4518
|
});
|
|
3604
4519
|
}
|
|
3605
4520
|
|
|
4521
|
+
function ensureDeviceLiveStreams(device) {
|
|
4522
|
+
if (!(device?.activeLiveStreams instanceof Map)) {
|
|
4523
|
+
Object.defineProperty(device, 'activeLiveStreams', {
|
|
4524
|
+
value: new Map(),
|
|
4525
|
+
enumerable: false,
|
|
4526
|
+
configurable: true,
|
|
4527
|
+
writable: true
|
|
4528
|
+
});
|
|
4529
|
+
if (device?.activeLiveStream?.streamId) {
|
|
4530
|
+
device.activeLiveStreams.set(device.activeLiveStream.streamId, device.activeLiveStream);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
return device.activeLiveStreams;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
function getDeviceLiveStream(device, streamId) {
|
|
4537
|
+
const id = safeString(streamId, 128);
|
|
4538
|
+
if (!id) {
|
|
4539
|
+
return device?.activeLiveStream || null;
|
|
4540
|
+
}
|
|
4541
|
+
return ensureDeviceLiveStreams(device).get(id)
|
|
4542
|
+
|| (device?.activeLiveStream?.streamId === id ? device.activeLiveStream : null);
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
function setDeviceLiveStream(device, stream) {
|
|
4546
|
+
if (!device || !stream?.streamId) {
|
|
4547
|
+
return stream;
|
|
4548
|
+
}
|
|
4549
|
+
ensureDeviceLiveStreams(device).set(stream.streamId, stream);
|
|
4550
|
+
device.activeLiveStream = stream;
|
|
4551
|
+
return stream;
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
4555
|
+
const streams = ensureDeviceLiveStreams(device);
|
|
4556
|
+
for (const stream of streams.values()) {
|
|
4557
|
+
stream.active = false;
|
|
4558
|
+
stream.stoppedAt = stoppedAt;
|
|
4559
|
+
stream.stopReason = reason;
|
|
4560
|
+
}
|
|
4561
|
+
if (device?.activeLiveStream) {
|
|
4562
|
+
device.activeLiveStream.active = false;
|
|
4563
|
+
device.activeLiveStream.stoppedAt = stoppedAt;
|
|
4564
|
+
device.activeLiveStream.stopReason = reason;
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
function makeStableLiveStreamId(deviceId, purpose = 'wall') {
|
|
4569
|
+
const hash = crypto.createHash('sha1').update(String(deviceId || 'device')).digest('hex').slice(0, 16);
|
|
4570
|
+
const role = safeString(purpose, 24).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'wall';
|
|
4571
|
+
return `${role}-${hash}`;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
function normalizeLiveStreamStartOptions(options = {}) {
|
|
4575
|
+
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
4576
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
4577
|
+
const maxFps = streamPurpose === 'control' ? 60 : 30;
|
|
4578
|
+
const fps = clampNumber(options.fps, 1, maxFps, 8);
|
|
4579
|
+
const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
|
|
4580
|
+
const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
|
|
4581
|
+
const quality = clampNumber(options.quality, 20, 95, 45);
|
|
4582
|
+
const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
|
|
4583
|
+
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose };
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
function liveStreamStartStillPending(activeLiveStream) {
|
|
4587
|
+
if (!activeLiveStream?.active || activeLiveStream.open === true) {
|
|
4588
|
+
return false;
|
|
4589
|
+
}
|
|
4590
|
+
const startedAt = Date.parse(activeLiveStream.startedAt || '');
|
|
4591
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
function getLiveStreamFreshWindowMs(activeLiveStream) {
|
|
4595
|
+
const fps = Number(activeLiveStream?.fps || 0);
|
|
4596
|
+
const frameMs = Number.isFinite(fps) && fps > 0 ? 1000 / fps : 500;
|
|
4597
|
+
return Math.max(
|
|
4598
|
+
LIVE_STREAM_MIN_FRESH_MS,
|
|
4599
|
+
Math.min(LIVE_STREAM_MAX_FRESH_MS, Math.round(frameMs * 20))
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
function getLiveStreamIdleMs(activeLiveStream) {
|
|
4604
|
+
const lastFrameAt = Date.parse(activeLiveStream?.lastFrameAt || activeLiveStream?.openedAt || activeLiveStream?.startedAt || '');
|
|
4605
|
+
if (!Number.isFinite(lastFrameAt)) {
|
|
4606
|
+
return Infinity;
|
|
4607
|
+
}
|
|
4608
|
+
return Math.max(0, Date.now() - lastFrameAt);
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
function liveStreamIsReusable(activeLiveStream) {
|
|
4612
|
+
if (!activeLiveStream?.active) {
|
|
4613
|
+
return false;
|
|
4614
|
+
}
|
|
4615
|
+
if (liveStreamStartStillPending(activeLiveStream)) {
|
|
4616
|
+
return true;
|
|
4617
|
+
}
|
|
4618
|
+
if (activeLiveStream.open !== true) {
|
|
4619
|
+
return false;
|
|
4620
|
+
}
|
|
4621
|
+
return getLiveStreamIdleMs(activeLiveStream) <= getLiveStreamFreshWindowMs(activeLiveStream);
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
function liveStreamMatchesOptions(activeLiveStream, normalized) {
|
|
4625
|
+
if (!activeLiveStream?.active) {
|
|
4626
|
+
return false;
|
|
4627
|
+
}
|
|
4628
|
+
return activeLiveStream.frameMode === normalized.transfer.frameMode
|
|
4629
|
+
&& Number(activeLiveStream.fps || 0) === normalized.fps
|
|
4630
|
+
&& Number(activeLiveStream.maxWidth || 0) === normalized.maxWidth
|
|
4631
|
+
&& Number(activeLiveStream.maxHeight || 0) === normalized.maxHeight
|
|
4632
|
+
&& Number(activeLiveStream.quality || 0) === normalized.quality
|
|
4633
|
+
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex;
|
|
4634
|
+
}
|
|
4635
|
+
|
|
3606
4636
|
function startLiveStream(deviceId, options = {}) {
|
|
3607
4637
|
const device = devices.get(String(deviceId || ''));
|
|
3608
4638
|
if (device?.synthetic === true && device.connected) {
|
|
@@ -3611,12 +4641,28 @@ export function createRemoteHub(options = {}) {
|
|
|
3611
4641
|
}
|
|
3612
4642
|
|
|
3613
4643
|
const now = new Date().toISOString();
|
|
3614
|
-
const
|
|
3615
|
-
const fps
|
|
4644
|
+
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options });
|
|
4645
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
4646
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
4647
|
+
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
3616
4648
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3617
|
-
|
|
4649
|
+
if (activeLiveStream
|
|
4650
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
4651
|
+
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
4652
|
+
return {
|
|
4653
|
+
ok: true,
|
|
4654
|
+
commandId: activeLiveStream.commandId,
|
|
4655
|
+
streamId,
|
|
4656
|
+
fps,
|
|
4657
|
+
mode: transfer.mode,
|
|
4658
|
+
frameMode: transfer.frameMode,
|
|
4659
|
+
monitorIndex,
|
|
4660
|
+
synthetic: true,
|
|
4661
|
+
reused: true
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
3618
4664
|
device.counters.commandsSent += 1;
|
|
3619
|
-
device
|
|
4665
|
+
setDeviceLiveStream(device, {
|
|
3620
4666
|
streamId,
|
|
3621
4667
|
commandId,
|
|
3622
4668
|
active: true,
|
|
@@ -3633,18 +4679,25 @@ export function createRemoteHub(options = {}) {
|
|
|
3633
4679
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3634
4680
|
handshake: transfer.handshake,
|
|
3635
4681
|
fps,
|
|
4682
|
+
maxWidth,
|
|
4683
|
+
maxHeight,
|
|
4684
|
+
quality,
|
|
4685
|
+
monitorIndex,
|
|
4686
|
+
monitorCount: 1,
|
|
3636
4687
|
startedAt: now,
|
|
3637
4688
|
stoppedAt: '',
|
|
3638
4689
|
stopReason: '',
|
|
3639
4690
|
lastFrameAt: '',
|
|
3640
4691
|
lastFrameSeq: 0,
|
|
3641
|
-
framesReceived: 0
|
|
3642
|
-
|
|
4692
|
+
framesReceived: 0,
|
|
4693
|
+
streamPurpose
|
|
4694
|
+
});
|
|
3643
4695
|
device.counters.liveStreamsStarted += 1;
|
|
3644
4696
|
applySyntheticLiveFrame(device, streamId, {
|
|
3645
4697
|
commandId,
|
|
3646
|
-
maxWidth
|
|
3647
|
-
maxHeight
|
|
4698
|
+
maxWidth,
|
|
4699
|
+
maxHeight,
|
|
4700
|
+
monitorIndex,
|
|
3648
4701
|
fps
|
|
3649
4702
|
});
|
|
3650
4703
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
@@ -3654,7 +4707,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3654
4707
|
mode: transfer.mode,
|
|
3655
4708
|
synthetic: true
|
|
3656
4709
|
});
|
|
3657
|
-
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode, synthetic: true };
|
|
4710
|
+
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode, monitorIndex, synthetic: true };
|
|
3658
4711
|
}
|
|
3659
4712
|
|
|
3660
4713
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
@@ -3665,10 +4718,76 @@ export function createRemoteHub(options = {}) {
|
|
|
3665
4718
|
}
|
|
3666
4719
|
|
|
3667
4720
|
const now = new Date().toISOString();
|
|
3668
|
-
const
|
|
3669
|
-
const fps
|
|
4721
|
+
const normalized = normalizeLiveStreamStartOptions(options);
|
|
4722
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
4723
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
4724
|
+
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
3670
4725
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3671
|
-
|
|
4726
|
+
if (activeLiveStream
|
|
4727
|
+
&& options.forceRestart !== true
|
|
4728
|
+
&& options.reuseExisting === true
|
|
4729
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
4730
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
4731
|
+
return {
|
|
4732
|
+
ok: true,
|
|
4733
|
+
commandId: activeLiveStream.commandId,
|
|
4734
|
+
streamId,
|
|
4735
|
+
fps: Number(activeLiveStream.fps || fps),
|
|
4736
|
+
mode: activeLiveStream.mode || transfer.mode,
|
|
4737
|
+
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
4738
|
+
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
4739
|
+
reused: true
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
if (activeLiveStream
|
|
4743
|
+
&& options.forceRestart !== true
|
|
4744
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
4745
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
4746
|
+
if (options.silentReuse !== true) {
|
|
4747
|
+
emitRemoteEvent('RemoteLiveStreamStartReused', device, {
|
|
4748
|
+
streamId,
|
|
4749
|
+
fps,
|
|
4750
|
+
mode: transfer.mode,
|
|
4751
|
+
frameMode: transfer.frameMode,
|
|
4752
|
+
monitorIndex
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
return {
|
|
4756
|
+
ok: true,
|
|
4757
|
+
commandId: activeLiveStream.commandId,
|
|
4758
|
+
streamId,
|
|
4759
|
+
fps,
|
|
4760
|
+
mode: transfer.mode,
|
|
4761
|
+
frameMode: transfer.frameMode,
|
|
4762
|
+
monitorIndex,
|
|
4763
|
+
reused: true
|
|
4764
|
+
};
|
|
4765
|
+
}
|
|
4766
|
+
if (options.forceRestart === true && activeLiveStream) {
|
|
4767
|
+
emitRemoteEvent('RemoteLiveStreamForcedRestart', device, {
|
|
4768
|
+
streamId,
|
|
4769
|
+
fps,
|
|
4770
|
+
mode: transfer.mode,
|
|
4771
|
+
frameMode: transfer.frameMode,
|
|
4772
|
+
monitorIndex,
|
|
4773
|
+
reason: safeString(options.restartReason || 'browser-stale', 128)
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
if (activeLiveStream
|
|
4777
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
4778
|
+
&& activeLiveStream.open === true
|
|
4779
|
+
&& !liveStreamIsReusable(activeLiveStream)) {
|
|
4780
|
+
emitRemoteEvent('RemoteLiveStreamStaleRestart', device, {
|
|
4781
|
+
streamId,
|
|
4782
|
+
fps,
|
|
4783
|
+
mode: transfer.mode,
|
|
4784
|
+
frameMode: transfer.frameMode,
|
|
4785
|
+
monitorIndex,
|
|
4786
|
+
idleMs: getLiveStreamIdleMs(activeLiveStream),
|
|
4787
|
+
staleAfterMs: getLiveStreamFreshWindowMs(activeLiveStream),
|
|
4788
|
+
lastFrameAt: activeLiveStream.lastFrameAt || ''
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
3672
4791
|
const result = sendCommand(deviceId, {
|
|
3673
4792
|
command: 'stream.start',
|
|
3674
4793
|
commandId,
|
|
@@ -3684,9 +4803,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3684
4803
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3685
4804
|
handshake: transfer.handshake,
|
|
3686
4805
|
fps,
|
|
3687
|
-
maxWidth
|
|
3688
|
-
maxHeight
|
|
3689
|
-
quality
|
|
4806
|
+
maxWidth,
|
|
4807
|
+
maxHeight,
|
|
4808
|
+
quality,
|
|
4809
|
+
monitorIndex,
|
|
4810
|
+
streamPurpose,
|
|
3690
4811
|
requestedAt: now
|
|
3691
4812
|
}
|
|
3692
4813
|
});
|
|
@@ -3695,7 +4816,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3695
4816
|
return result;
|
|
3696
4817
|
}
|
|
3697
4818
|
|
|
3698
|
-
device
|
|
4819
|
+
setDeviceLiveStream(device, {
|
|
3699
4820
|
streamId,
|
|
3700
4821
|
commandId,
|
|
3701
4822
|
active: true,
|
|
@@ -3712,23 +4833,30 @@ export function createRemoteHub(options = {}) {
|
|
|
3712
4833
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3713
4834
|
handshake: transfer.handshake,
|
|
3714
4835
|
fps,
|
|
4836
|
+
maxWidth,
|
|
4837
|
+
maxHeight,
|
|
4838
|
+
quality,
|
|
4839
|
+
monitorIndex,
|
|
4840
|
+
monitorCount: 1,
|
|
3715
4841
|
startedAt: now,
|
|
3716
4842
|
stoppedAt: '',
|
|
3717
4843
|
stopReason: '',
|
|
3718
4844
|
lastFrameAt: '',
|
|
3719
4845
|
lastFrameSeq: 0,
|
|
3720
|
-
framesReceived: 0
|
|
3721
|
-
|
|
4846
|
+
framesReceived: 0,
|
|
4847
|
+
streamPurpose
|
|
4848
|
+
});
|
|
3722
4849
|
device.counters.liveStreamsStarted += 1;
|
|
3723
4850
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
3724
4851
|
streamId,
|
|
3725
4852
|
commandId,
|
|
3726
4853
|
fps,
|
|
3727
4854
|
mode: transfer.mode,
|
|
3728
|
-
frameMode: transfer.frameMode
|
|
4855
|
+
frameMode: transfer.frameMode,
|
|
4856
|
+
monitorIndex
|
|
3729
4857
|
});
|
|
3730
4858
|
|
|
3731
|
-
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode };
|
|
4859
|
+
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode, monitorIndex };
|
|
3732
4860
|
}
|
|
3733
4861
|
|
|
3734
4862
|
function stopLiveStream(deviceId, options = {}) {
|
|
@@ -3737,10 +4865,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3737
4865
|
const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
|
|
3738
4866
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3739
4867
|
device.counters.commandsSent += 1;
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
4868
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
4869
|
+
if (streamState) {
|
|
4870
|
+
streamState.active = false;
|
|
4871
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
4872
|
+
streamState.stopReason = 'manager-request';
|
|
3744
4873
|
}
|
|
3745
4874
|
device.counters.liveStreamsStopped += 1;
|
|
3746
4875
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -3770,10 +4899,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3770
4899
|
return result;
|
|
3771
4900
|
}
|
|
3772
4901
|
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
4902
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
4903
|
+
if (streamState) {
|
|
4904
|
+
streamState.active = false;
|
|
4905
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
4906
|
+
streamState.stopReason = 'manager-request';
|
|
3777
4907
|
}
|
|
3778
4908
|
device.counters.liveStreamsStopped += 1;
|
|
3779
4909
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -3925,6 +5055,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3925
5055
|
requestAgentTask,
|
|
3926
5056
|
requestAgentTaskBatch,
|
|
3927
5057
|
setHostTarget,
|
|
5058
|
+
rotatePairingPin,
|
|
3928
5059
|
requestThumbnail,
|
|
3929
5060
|
startLiveStream,
|
|
3930
5061
|
stopLiveStream,
|
|
@@ -3935,7 +5066,8 @@ export function createRemoteHub(options = {}) {
|
|
|
3935
5066
|
getFramePayload,
|
|
3936
5067
|
seedSyntheticFleet,
|
|
3937
5068
|
clearSyntheticFleet,
|
|
3938
|
-
getPairToken: () => pairToken
|
|
5069
|
+
getPairToken: () => pairToken,
|
|
5070
|
+
getPairingPin: () => pairingPin
|
|
3939
5071
|
};
|
|
3940
5072
|
}
|
|
3941
5073
|
|