Repository navigation
Expand file tree
/
Copy pathremoteDesktopIce.ts
More file actions
126 lines (126 loc) · 3.96 KB
/
Copy pathremoteDesktopIce.ts
File metadata and controls
126 lines (126 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/** Bounded, additive signaling inside the existing peer-authorized desktop channel. */
export interface RemoteDesktopIceCandidate {
candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
usernameFragment?: string;
}
export interface RemoteDesktopIceRequest {
op: "ice";
lease: string;
attemptId: string;
candidates: RemoteDesktopIceCandidate[];
after: number;
}
export interface RemoteDesktopIceReply {
attemptId: string;
candidates: RemoteDesktopIceCandidate[];
next: number;
complete: boolean;
}
// Independent operators; neither endpoint is a guarantee of mainland reachability.
export const REMOTE_DESKTOP_ICE_SERVERS = [
{ urls: "stun:stun.cloudflare.com:3478" },
{ urls: "stun:stun.l.google.com:19302" },
];
// Shared by every TURN configuration fetch (desktop/mobile, video/files).
// Viewer shells allow bridge delivery after this bounded HTTP/resolver budget.
export const REMOTE_DESKTOP_ICE_CONFIG_TIMEOUT_MS = 8_000;
// Sequential host stages, followed by transport and WebView delivery headroom.
// Keep the outer waits derived from these limits so a valid cold start is not discarded.
export const REMOTE_DESKTOP_OFFER_BUDGET = {
captureReadyMs: 10_000,
platformStatusMs: 8_000,
// Windows Node-API probe: pipe open 3s, write 5s, and ready read 5s.
platformHandshakeMs: 13_000,
sourcesMs: 5_000,
hostMs: 18_000,
};
export const REMOTE_DESKTOP_INVOKE_MS =
REMOTE_DESKTOP_OFFER_BUDGET.captureReadyMs +
REMOTE_DESKTOP_OFFER_BUDGET.platformStatusMs +
REMOTE_DESKTOP_OFFER_BUDGET.platformHandshakeMs +
REMOTE_DESKTOP_OFFER_BUDGET.sourcesMs +
REMOTE_DESKTOP_OFFER_BUDGET.hostMs +
5_000;
export const REMOTE_DESKTOP_NETWORK = {
iceConfigMs: REMOTE_DESKTOP_ICE_CONFIG_TIMEOUT_MS,
iceConfigBridgeMs: 500,
maxCandidates: 128,
batchSize: 16,
pollMs: 250,
exchangeMs: 30_000,
disconnectedMs: 5_000,
answerMs: REMOTE_DESKTOP_INVOKE_MS + 2_000,
connectMs: 15_000,
legacyGatherMs: 5_000,
stableMs: 30_000,
retryMs: [1_000, 3_000, 8_000],
};
export function isDesktopAttemptId(value: unknown): value is string {
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
}
export function parseDesktopIceCandidates(
value: unknown,
): RemoteDesktopIceCandidate[] {
if (!Array.isArray(value) || value.length > REMOTE_DESKTOP_NETWORK.batchSize)
throw new Error("INVALID_DESKTOP_ICE");
return value.map((v) => {
if (
!v ||
typeof v !== "object" ||
typeof v.candidate !== "string" ||
!v.candidate.startsWith("candidate:") ||
v.candidate.length > 2048 ||
!(
v.sdpMid === null ||
(typeof v.sdpMid === "string" && v.sdpMid.length <= 128)
) ||
!(
v.sdpMLineIndex === null ||
(Number.isInteger(v.sdpMLineIndex) &&
v.sdpMLineIndex >= 0 &&
v.sdpMLineIndex < 32)
) ||
(v.sdpMid === null && v.sdpMLineIndex === null) ||
!(
v.usernameFragment === undefined ||
(typeof v.usernameFragment === "string" &&
v.usernameFragment.length <= 256)
)
)
throw new Error("INVALID_DESKTOP_ICE");
return {
candidate: v.candidate,
sdpMid: v.sdpMid,
sdpMLineIndex: v.sdpMLineIndex,
...(v.usernameFragment === undefined
? {}
: { usernameFragment: v.usernameFragment }),
};
});
}
export function isDesktopIceCursor(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isSafeInteger(value) &&
value >= 0 &&
value <= REMOTE_DESKTOP_NETWORK.maxCandidates
);
}
export function parseDesktopIceReply(value: unknown): RemoteDesktopIceReply {
const v = value as RemoteDesktopIceReply | null;
if (
!v ||
!isDesktopAttemptId(v.attemptId) ||
!isDesktopIceCursor(v.next) ||
typeof v.complete !== "boolean"
)
throw new Error("INVALID_DESKTOP_ICE");
return {
attemptId: v.attemptId,
next: v.next,
complete: v.complete,
candidates: parseDesktopIceCandidates(v.candidates),
};
}