livedesk 0.1.436 → 0.1.438
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/bin/livedesk.js +5 -1
- package/client/bin/livedesk-client.js +2 -0
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +718 -121
- package/hub/src/server.js +84 -27
- package/hub/src/transport/udp-hub-transport.js +36 -21
- package/hub/src/transport/udp-protocol.js +105 -35
- package/package.json +6 -6
- package/web/dist/assets/{icons-VU9ZIqNm.js → icons-BIyRF-ou.js} +1 -1
- package/web/dist/assets/{index-Bd1_4ydL.css → index-BPpXtm8K.css} +1 -1
- package/web/dist/assets/index-BS2HuoZk.js +25 -0
- package/web/dist/assets/{react-JoD2dl9B.js → react-C7a9r614.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-2-aflaqj.js +0 -25
package/hub/src/server.js
CHANGED
|
@@ -61,7 +61,7 @@ const runtimeManager = createHubRuntime({
|
|
|
61
61
|
});
|
|
62
62
|
runtimeManager.emit('runtime.started', { role: runtimeRole, pid: process.pid });
|
|
63
63
|
const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 8 * 1024 * 1024);
|
|
64
|
-
const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS',
|
|
64
|
+
const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 8);
|
|
65
65
|
const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 2);
|
|
66
66
|
const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
|
|
67
67
|
const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
|
|
@@ -1821,11 +1821,21 @@ function enqueueFramePacketForClient(ws, packet, meta) {
|
|
|
1821
1821
|
ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
1822
1822
|
}
|
|
1823
1823
|
}
|
|
1824
|
-
while (lane.queue.length >= frameClientQueueLimit(ws)) {
|
|
1825
|
-
dropQueuedFrameForLane(lane);
|
|
1826
|
-
ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1824
|
+
while (lane.queue.length >= frameClientQueueLimit(ws)) {
|
|
1825
|
+
dropQueuedFrameForLane(lane);
|
|
1826
|
+
ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
1827
|
+
}
|
|
1828
|
+
// Queue eviction can put this H.264 device into key-frame recovery after
|
|
1829
|
+
// the pre-overflow check above. Never enqueue the current dependent delta
|
|
1830
|
+
// while the decoder is waiting for a fresh recovery key frame.
|
|
1831
|
+
if (meta.isH264 && meta.isKeyFrame) {
|
|
1832
|
+
lane.awaitingKeyFrames.delete(meta.deviceId);
|
|
1833
|
+
} else if (meta.isH264 && lane.awaitingKeyFrames.has(meta.deviceId)) {
|
|
1834
|
+
lane.dropped += 1;
|
|
1835
|
+
ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
lane.queue.push({
|
|
1829
1839
|
packet,
|
|
1830
1840
|
deviceId: meta.deviceId,
|
|
1831
1841
|
frameSeq: meta.frameSeq,
|
|
@@ -2043,10 +2053,28 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2043
2053
|
agentPaceMs: Number(frame.agentPaceMs || 0) || 0,
|
|
2044
2054
|
agentQueueBeforePaceMs: Number(frame.agentQueueBeforePaceMs || 0) || 0,
|
|
2045
2055
|
agentFrameAgeMs: Number(frame.agentFrameAgeMs || 0) || 0,
|
|
2046
|
-
droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
|
|
2047
|
-
hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
|
|
2048
|
-
hubClientDropped: Number(diagnostics.hubClientDropped || 0) || 0,
|
|
2049
|
-
|
|
2056
|
+
droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
|
|
2057
|
+
hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
|
|
2058
|
+
hubClientDropped: Number(diagnostics.hubClientDropped || 0) || 0,
|
|
2059
|
+
hubClientQueueDepth: Number(diagnostics.hubClientQueueDepth || 0) || 0,
|
|
2060
|
+
hubClientQueueLimit: Number(diagnostics.hubClientQueueLimit || 0) || 0,
|
|
2061
|
+
hubClientBufferedBytes: Number(diagnostics.hubClientBufferedBytes || 0) || 0,
|
|
2062
|
+
udpIncompleteFrames: Number(frame.udpIncompleteFrames || 0) || 0,
|
|
2063
|
+
transport: frame.transport || '',
|
|
2064
|
+
videoTransport: frame.videoTransport || (frame.transport === 'udp-p2p'
|
|
2065
|
+
? 'p2p-udp'
|
|
2066
|
+
: frame.transport === 'ws-binary'
|
|
2067
|
+
? 'direct-ws'
|
|
2068
|
+
: 'direct-tcp'),
|
|
2069
|
+
frameTransportActive: frame.frameTransportActive || '',
|
|
2070
|
+
frameTransportProtocol: frame.frameTransportProtocol || '',
|
|
2071
|
+
frameTransportState: frame.frameTransportState || '',
|
|
2072
|
+
frameTransportP2pReady: frame.frameTransportP2pReady === true,
|
|
2073
|
+
frameTransportReason: frame.frameTransportReason || '',
|
|
2074
|
+
frameTransportChangedAt: frame.frameTransportChangedAt || '',
|
|
2075
|
+
frameTransportSwitchCount: Number(frame.frameTransportSwitchCount || 0) || 0,
|
|
2076
|
+
frameTransportTcpFailureCount: Number(frame.frameTransportTcpFailureCount || 0) || 0,
|
|
2077
|
+
hardwareEncoder: frame.hardwareEncoder || '',
|
|
2050
2078
|
platformProfile: frame.platformProfile || '',
|
|
2051
2079
|
monitorIndex: Number.isFinite(Number(frame.monitorIndex)) ? Number(frame.monitorIndex) : 0,
|
|
2052
2080
|
monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
|
|
@@ -2157,7 +2185,6 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
2157
2185
|
const isMode5 = String(frame.frameMode || frame.mode || '').toLowerCase() === 'mode5-lzo-delta';
|
|
2158
2186
|
const isKeyFrame = frame.isKeyFrame === true || String(frame.chunkType || '').toLowerCase() === 'key';
|
|
2159
2187
|
const frameSeq = Number(frame.frameSeq || 0) || 0;
|
|
2160
|
-
let sharedPacket = null;
|
|
2161
2188
|
for (const client of targetClients) {
|
|
2162
2189
|
if (client.readyState !== client.OPEN) {
|
|
2163
2190
|
continue;
|
|
@@ -2197,13 +2224,18 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
2197
2224
|
client.liveDeskFrameBackpressureDrops = Number(client.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
2198
2225
|
continue;
|
|
2199
2226
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2227
|
+
const packet = buildRemoteFrameBinaryPacket(frameEvent, {
|
|
2228
|
+
hubClientDropped: lane.dropped,
|
|
2229
|
+
hubClientQueueDepth: lane.queue.length,
|
|
2230
|
+
hubClientQueueLimit: frameClientQueueLimit(client),
|
|
2231
|
+
hubClientBufferedBytes: Number(client.bufferedAmount || 0) || 0
|
|
2232
|
+
});
|
|
2233
|
+
if (!packet) {
|
|
2202
2234
|
continue;
|
|
2203
2235
|
}
|
|
2204
|
-
enqueueFramePacketForClient(client,
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2236
|
+
enqueueFramePacketForClient(client, packet, { deviceId, frameSeq, isH264, isMode5, isKeyFrame });
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2207
2239
|
|
|
2208
2240
|
function broadcastRemoteBinaryAudio(audioEvent) {
|
|
2209
2241
|
const deviceId = String(audioEvent?.deviceId || audioEvent?.frame?.deviceId || '').trim();
|
|
@@ -4034,12 +4066,26 @@ app.post('/api/remote/devices/:deviceId/live/start', requireHubFeatureAccess, (r
|
|
|
4034
4066
|
res.json(remoteHub.startLiveStream(req.params.deviceId, req.body || {}));
|
|
4035
4067
|
});
|
|
4036
4068
|
|
|
4037
|
-
app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
|
|
4038
|
-
noStore(res);
|
|
4039
|
-
res.json(remoteHub.stopLiveStream(req.params.deviceId, req.body || {}));
|
|
4040
|
-
});
|
|
4041
|
-
|
|
4042
|
-
app.post('/api/remote/devices/:deviceId/
|
|
4069
|
+
app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
|
|
4070
|
+
noStore(res);
|
|
4071
|
+
res.json(remoteHub.stopLiveStream(req.params.deviceId, req.body || {}));
|
|
4072
|
+
});
|
|
4073
|
+
|
|
4074
|
+
app.post('/api/remote/devices/:deviceId/live/pause', requireHubFeatureAccess, async (req, res, next) => {
|
|
4075
|
+
noStore(res);
|
|
4076
|
+
try {
|
|
4077
|
+
res.json(await remoteHub.pauseLiveCapture(req.params.deviceId, req.body || {}));
|
|
4078
|
+
} catch (error) {
|
|
4079
|
+
next(error);
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
app.post('/api/remote/devices/:deviceId/live/resume', requireHubFeatureAccess, (req, res) => {
|
|
4084
|
+
noStore(res);
|
|
4085
|
+
res.json(remoteHub.resumeLiveCapture(req.params.deviceId, req.body || {}));
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
app.post('/api/remote/devices/:deviceId/audio/start', requireHubFeatureAccess, (req, res) => {
|
|
4043
4089
|
noStore(res);
|
|
4044
4090
|
res.json(remoteHub.startAudioStream(req.params.deviceId, req.body || {}));
|
|
4045
4091
|
});
|
|
@@ -4203,9 +4249,10 @@ audioWss.on('connection', (ws, req) => {
|
|
|
4203
4249
|
});
|
|
4204
4250
|
});
|
|
4205
4251
|
|
|
4206
|
-
inputWss.on('connection', ws => {
|
|
4207
|
-
inputClients.add(ws);
|
|
4208
|
-
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
4252
|
+
inputWss.on('connection', ws => {
|
|
4253
|
+
inputClients.add(ws);
|
|
4254
|
+
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
4255
|
+
ws.liveDeskInputDeviceIds = new Set();
|
|
4209
4256
|
try {
|
|
4210
4257
|
ws._socket?.setNoDelay?.(true);
|
|
4211
4258
|
} catch {
|
|
@@ -4229,6 +4276,9 @@ inputWss.on('connection', ws => {
|
|
|
4229
4276
|
const input = payload?.input && typeof payload.input === 'object'
|
|
4230
4277
|
? { ...payload.input, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() }
|
|
4231
4278
|
: { ...payload, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() };
|
|
4279
|
+
if (deviceId) {
|
|
4280
|
+
ws.liveDeskInputDeviceIds.add(deviceId);
|
|
4281
|
+
}
|
|
4232
4282
|
const result = remoteHub.sendInputControl(deviceId, input);
|
|
4233
4283
|
const inputType = String(input?.type || '').toLowerCase();
|
|
4234
4284
|
const fireAndForget = payload?.fireAndForget === true
|
|
@@ -4248,8 +4298,15 @@ inputWss.on('connection', ws => {
|
|
|
4248
4298
|
timestamp: new Date().toISOString()
|
|
4249
4299
|
});
|
|
4250
4300
|
});
|
|
4251
|
-
|
|
4252
|
-
|
|
4301
|
+
const releaseBrowserInputOwner = reason => {
|
|
4302
|
+
inputClients.delete(ws);
|
|
4303
|
+
for (const deviceId of ws.liveDeskInputDeviceIds || []) {
|
|
4304
|
+
remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
|
|
4305
|
+
}
|
|
4306
|
+
ws.liveDeskInputDeviceIds?.clear?.();
|
|
4307
|
+
};
|
|
4308
|
+
ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
|
|
4309
|
+
ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
|
|
4253
4310
|
sendJson(ws, {
|
|
4254
4311
|
type: 'RemoteInputSocketReady',
|
|
4255
4312
|
protocol: 'livedesk.remote.input.json.v1',
|
|
@@ -30,16 +30,16 @@ function sameEndpoint(left, right) {
|
|
|
30
30
|
return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
|
|
34
|
-
const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED,
|
|
35
|
-
const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P,
|
|
33
|
+
export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
|
|
34
|
+
const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
|
|
35
|
+
const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
|
|
36
36
|
const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
|
|
37
37
|
const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
|
|
38
38
|
const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
|
|
39
|
-
const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || '', 128);
|
|
39
|
+
const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
|
|
40
40
|
const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
|
|
41
41
|
const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
|
|
42
|
-
const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000,
|
|
42
|
+
const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
|
|
43
43
|
const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
|
|
44
44
|
const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
|
|
45
45
|
const socket = dgram.createSocket('udp4');
|
|
@@ -146,15 +146,23 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
if (packet.type !== 'frame') return;
|
|
149
|
-
const complete = session.reassembler.ingest(packet);
|
|
150
|
-
if (!complete) return;
|
|
151
|
-
session.framesReceived += 1;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
const complete = session.reassembler.ingest(packet);
|
|
150
|
+
if (!complete) return;
|
|
151
|
+
session.framesReceived += 1;
|
|
152
|
+
const reassembly = session.reassembler.getStats();
|
|
153
|
+
frameHandler({
|
|
154
|
+
deviceId: session.deviceId,
|
|
155
|
+
sessionId: session.sessionId,
|
|
156
|
+
header: {
|
|
157
|
+
...complete.header,
|
|
158
|
+
hubIngressDropped: Math.max(
|
|
159
|
+
Number(complete.header?.hubIngressDropped || 0) || 0,
|
|
160
|
+
reassembly.droppedFrames
|
|
161
|
+
),
|
|
162
|
+
udpIncompleteFrames: reassembly.droppedFrames
|
|
163
|
+
},
|
|
164
|
+
payload: complete.payload,
|
|
165
|
+
transport: 'udp-p2p'
|
|
158
166
|
});
|
|
159
167
|
sendPacket(session, 'pong', {
|
|
160
168
|
state: 'frame-ack',
|
|
@@ -171,10 +179,16 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
171
179
|
socket.once('error', onError);
|
|
172
180
|
socket.once('listening', onListening);
|
|
173
181
|
socket.bind(requestedPort, bindHost);
|
|
174
|
-
});
|
|
175
|
-
started = true;
|
|
176
|
-
boundPort = socket.address().port;
|
|
177
|
-
|
|
182
|
+
});
|
|
183
|
+
started = true;
|
|
184
|
+
boundPort = socket.address().port;
|
|
185
|
+
try {
|
|
186
|
+
socket.setRecvBufferSize(8 * 1024 * 1024);
|
|
187
|
+
socket.setSendBufferSize(1024 * 1024);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
|
|
190
|
+
}
|
|
191
|
+
logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
|
|
178
192
|
return getStatus();
|
|
179
193
|
}
|
|
180
194
|
|
|
@@ -240,9 +254,10 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
240
254
|
state: session.ready ? 'udp-ready' : 'udp-session-created',
|
|
241
255
|
ready: session.ready,
|
|
242
256
|
sessionId: session.sessionId,
|
|
243
|
-
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
244
|
-
framesReceived: session.framesReceived
|
|
245
|
-
|
|
257
|
+
lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
|
|
258
|
+
framesReceived: session.framesReceived,
|
|
259
|
+
...session.reassembler.getStats()
|
|
260
|
+
};
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
async function close() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { inflateRawSync } from 'node:zlib';
|
|
2
3
|
|
|
3
4
|
export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
|
|
4
5
|
export const UDP_P2P_VERSION = 1;
|
|
@@ -17,8 +18,10 @@ const UDP_FIXED_HEADER_BYTES = 15;
|
|
|
17
18
|
const UDP_NONCE_BYTES = 12;
|
|
18
19
|
const UDP_TAG_BYTES = 16;
|
|
19
20
|
const UDP_MAX_SESSION_ID_BYTES = 96;
|
|
20
|
-
const UDP_MAX_HEADER_BYTES = 4096;
|
|
21
|
-
const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
|
|
21
|
+
const UDP_MAX_HEADER_BYTES = 4096;
|
|
22
|
+
const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
|
|
23
|
+
const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
|
|
24
|
+
const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
|
|
22
25
|
|
|
23
26
|
function asBuffer(value) {
|
|
24
27
|
if (Buffer.isBuffer(value)) return value;
|
|
@@ -32,13 +35,53 @@ function normalizeKey(key) {
|
|
|
32
35
|
return value.length === 32 ? value : crypto.createHash('sha256').update(value).digest();
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function normalizeSessionId(value) {
|
|
38
|
+
function normalizeSessionId(value) {
|
|
36
39
|
const sessionId = String(value || '').trim();
|
|
37
40
|
if (!sessionId || Buffer.byteLength(sessionId, 'utf8') > UDP_MAX_SESSION_ID_BYTES) {
|
|
38
41
|
throw new Error('invalid-udp-session-id');
|
|
39
42
|
}
|
|
40
43
|
return sessionId;
|
|
41
|
-
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function decodeFrameHeader(header) {
|
|
47
|
+
if (header?.frameHeader && typeof header.frameHeader === 'object') {
|
|
48
|
+
return header.frameHeader;
|
|
49
|
+
}
|
|
50
|
+
const encoded = String(header?.frameHeaderDeflate || '');
|
|
51
|
+
if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
|
|
56
|
+
const value = JSON.parse(json);
|
|
57
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function decodeFramedPayload(payload, legacyHeader = null) {
|
|
64
|
+
const bytes = asBuffer(payload);
|
|
65
|
+
if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
|
|
66
|
+
return { header: legacyHeader || {}, payload: bytes };
|
|
67
|
+
}
|
|
68
|
+
const compressedHeaderLength = bytes.readUInt32BE(4);
|
|
69
|
+
if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
|
|
70
|
+
|| 8 + compressedHeaderLength > bytes.length) return null;
|
|
71
|
+
try {
|
|
72
|
+
const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
|
|
73
|
+
maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
|
|
74
|
+
}).toString('utf8');
|
|
75
|
+
const header = JSON.parse(json);
|
|
76
|
+
if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
|
|
77
|
+
return {
|
|
78
|
+
header,
|
|
79
|
+
payload: bytes.subarray(8 + compressedHeaderLength)
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
42
85
|
|
|
43
86
|
function packetTypeCode(type) {
|
|
44
87
|
const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
|
|
@@ -147,12 +190,15 @@ export class UdpReplayWindow {
|
|
|
147
190
|
}
|
|
148
191
|
}
|
|
149
192
|
|
|
150
|
-
export class UdpFrameReassembler {
|
|
151
|
-
constructor({ maxPending = 2, timeoutMs =
|
|
152
|
-
this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
|
|
153
|
-
this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) ||
|
|
154
|
-
this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
|
|
155
|
-
this.pending = new Map();
|
|
193
|
+
export class UdpFrameReassembler {
|
|
194
|
+
constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
|
|
195
|
+
this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
|
|
196
|
+
this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
|
|
197
|
+
this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
|
|
198
|
+
this.pending = new Map();
|
|
199
|
+
this.droppedFrames = 0;
|
|
200
|
+
this.expiredFrames = 0;
|
|
201
|
+
this.evictedFrames = 0;
|
|
156
202
|
}
|
|
157
203
|
|
|
158
204
|
ingest(packet, now = Date.now()) {
|
|
@@ -166,11 +212,14 @@ export class UdpFrameReassembler {
|
|
|
166
212
|
|| chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
|
|
167
213
|
let state = this.pending.get(frameId);
|
|
168
214
|
if (!state) {
|
|
169
|
-
while (this.pending.size >= this.maxPending) {
|
|
170
|
-
const oldest = this.pending.keys().next().value;
|
|
171
|
-
if (oldest === undefined) break;
|
|
172
|
-
this.pending.delete(oldest)
|
|
173
|
-
|
|
215
|
+
while (this.pending.size >= this.maxPending) {
|
|
216
|
+
const oldest = this.pending.keys().next().value;
|
|
217
|
+
if (oldest === undefined) break;
|
|
218
|
+
if (this.pending.delete(oldest)) {
|
|
219
|
+
this.droppedFrames += 1;
|
|
220
|
+
this.evictedFrames += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
174
223
|
state = { createdAt: now, chunkCount, chunks: new Array(chunkCount), received: 0, totalBytes: 0, frameHeader: null };
|
|
175
224
|
this.pending.set(frameId, state);
|
|
176
225
|
}
|
|
@@ -179,26 +228,47 @@ export class UdpFrameReassembler {
|
|
|
179
228
|
state.chunks[chunkIndex] = payload;
|
|
180
229
|
state.received += 1;
|
|
181
230
|
state.totalBytes += payload.length;
|
|
182
|
-
|
|
183
|
-
if (state.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
231
|
+
const decodedFrameHeader = decodeFrameHeader(header);
|
|
232
|
+
if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
|
|
233
|
+
if (state.totalBytes > this.maxFrameBytes) {
|
|
234
|
+
if (this.pending.delete(frameId)) this.droppedFrames += 1;
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (state.received !== state.chunkCount) return null;
|
|
238
|
+
this.pending.delete(frameId);
|
|
239
|
+
const complete = decodeFramedPayload(
|
|
240
|
+
Buffer.concat(state.chunks, state.totalBytes),
|
|
241
|
+
state.frameHeader || header.frameHeader || {}
|
|
242
|
+
);
|
|
243
|
+
if (!complete) {
|
|
244
|
+
this.droppedFrames += 1;
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
frameId,
|
|
249
|
+
header: complete.header,
|
|
250
|
+
payload: complete.payload
|
|
251
|
+
};
|
|
252
|
+
}
|
|
195
253
|
|
|
196
|
-
expire(now = Date.now()) {
|
|
197
|
-
for (const [frameId, state] of this.pending) {
|
|
198
|
-
if (now - state.createdAt > this.timeoutMs
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
254
|
+
expire(now = Date.now()) {
|
|
255
|
+
for (const [frameId, state] of this.pending) {
|
|
256
|
+
if (now - state.createdAt > this.timeoutMs && this.pending.delete(frameId)) {
|
|
257
|
+
this.droppedFrames += 1;
|
|
258
|
+
this.expiredFrames += 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getStats() {
|
|
264
|
+
return {
|
|
265
|
+
pendingFrames: this.pending.size,
|
|
266
|
+
droppedFrames: this.droppedFrames,
|
|
267
|
+
expiredFrames: this.expiredFrames,
|
|
268
|
+
evictedFrames: this.evictedFrames
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
202
272
|
|
|
203
273
|
export function encodeRendezvousMessage(message) {
|
|
204
274
|
const payload = Buffer.from(JSON.stringify(message && typeof message === 'object' ? message : {}), 'utf8');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livedesk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"livedeskClientVersion": "0.1.
|
|
3
|
+
"version": "0.1.438",
|
|
4
|
+
"livedeskClientVersion": "0.1.194",
|
|
5
5
|
"buildFlavor": "production",
|
|
6
6
|
"description": "LiveDesk Hub and client launcher",
|
|
7
7
|
"type": "module",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"ws": "^8.18.3"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
54
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
55
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
56
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
53
|
+
"@livedesk/fast-linux-x64": "0.1.401",
|
|
54
|
+
"@livedesk/fast-osx-arm64": "0.1.401",
|
|
55
|
+
"@livedesk/fast-osx-x64": "0.1.401",
|
|
56
|
+
"@livedesk/fast-win-x64": "0.1.401"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|