@aperture-browser/live-session 0.0.0-init.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index.d.ts +807 -0
- package/dist/index.js +1150 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { Recording, resolveTenantHeader } from "@aperture-browser/api-client";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Deferred from "effect/Deferred";
|
|
7
|
+
import "effect/Fiber";
|
|
8
|
+
import * as FiberSet from "effect/FiberSet";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
//#region src/input-keycodes.ts
|
|
11
|
+
const evdevKeycodeByCode = {
|
|
12
|
+
Escape: 1,
|
|
13
|
+
Digit1: 2,
|
|
14
|
+
Digit2: 3,
|
|
15
|
+
Digit3: 4,
|
|
16
|
+
Digit4: 5,
|
|
17
|
+
Digit5: 6,
|
|
18
|
+
Digit6: 7,
|
|
19
|
+
Digit7: 8,
|
|
20
|
+
Digit8: 9,
|
|
21
|
+
Digit9: 10,
|
|
22
|
+
Digit0: 11,
|
|
23
|
+
Minus: 12,
|
|
24
|
+
Equal: 13,
|
|
25
|
+
Backspace: 14,
|
|
26
|
+
Tab: 15,
|
|
27
|
+
KeyQ: 16,
|
|
28
|
+
KeyW: 17,
|
|
29
|
+
KeyE: 18,
|
|
30
|
+
KeyR: 19,
|
|
31
|
+
KeyT: 20,
|
|
32
|
+
KeyY: 21,
|
|
33
|
+
KeyU: 22,
|
|
34
|
+
KeyI: 23,
|
|
35
|
+
KeyO: 24,
|
|
36
|
+
KeyP: 25,
|
|
37
|
+
BracketLeft: 26,
|
|
38
|
+
BracketRight: 27,
|
|
39
|
+
Enter: 28,
|
|
40
|
+
ControlLeft: 29,
|
|
41
|
+
KeyA: 30,
|
|
42
|
+
KeyS: 31,
|
|
43
|
+
KeyD: 32,
|
|
44
|
+
KeyF: 33,
|
|
45
|
+
KeyG: 34,
|
|
46
|
+
KeyH: 35,
|
|
47
|
+
KeyJ: 36,
|
|
48
|
+
KeyK: 37,
|
|
49
|
+
KeyL: 38,
|
|
50
|
+
Semicolon: 39,
|
|
51
|
+
Quote: 40,
|
|
52
|
+
Backquote: 41,
|
|
53
|
+
ShiftLeft: 42,
|
|
54
|
+
Backslash: 43,
|
|
55
|
+
KeyZ: 44,
|
|
56
|
+
KeyX: 45,
|
|
57
|
+
KeyC: 46,
|
|
58
|
+
KeyV: 47,
|
|
59
|
+
KeyB: 48,
|
|
60
|
+
KeyN: 49,
|
|
61
|
+
KeyM: 50,
|
|
62
|
+
Comma: 51,
|
|
63
|
+
Period: 52,
|
|
64
|
+
Slash: 53,
|
|
65
|
+
ShiftRight: 54,
|
|
66
|
+
NumpadMultiply: 55,
|
|
67
|
+
AltLeft: 56,
|
|
68
|
+
Space: 57,
|
|
69
|
+
CapsLock: 58,
|
|
70
|
+
F1: 59,
|
|
71
|
+
F2: 60,
|
|
72
|
+
F3: 61,
|
|
73
|
+
F4: 62,
|
|
74
|
+
F5: 63,
|
|
75
|
+
F6: 64,
|
|
76
|
+
F7: 65,
|
|
77
|
+
F8: 66,
|
|
78
|
+
F9: 67,
|
|
79
|
+
F10: 68,
|
|
80
|
+
NumLock: 69,
|
|
81
|
+
ScrollLock: 70,
|
|
82
|
+
Numpad7: 71,
|
|
83
|
+
Numpad8: 72,
|
|
84
|
+
Numpad9: 73,
|
|
85
|
+
NumpadSubtract: 74,
|
|
86
|
+
Numpad4: 75,
|
|
87
|
+
Numpad5: 76,
|
|
88
|
+
Numpad6: 77,
|
|
89
|
+
NumpadAdd: 78,
|
|
90
|
+
Numpad1: 79,
|
|
91
|
+
Numpad2: 80,
|
|
92
|
+
Numpad3: 81,
|
|
93
|
+
Numpad0: 82,
|
|
94
|
+
NumpadDecimal: 83,
|
|
95
|
+
IntlBackslash: 86,
|
|
96
|
+
F11: 87,
|
|
97
|
+
F12: 88,
|
|
98
|
+
NumpadEnter: 96,
|
|
99
|
+
ControlRight: 97,
|
|
100
|
+
NumpadDivide: 98,
|
|
101
|
+
PrintScreen: 99,
|
|
102
|
+
AltRight: 100,
|
|
103
|
+
Home: 102,
|
|
104
|
+
ArrowUp: 103,
|
|
105
|
+
PageUp: 104,
|
|
106
|
+
ArrowLeft: 105,
|
|
107
|
+
ArrowRight: 106,
|
|
108
|
+
End: 107,
|
|
109
|
+
ArrowDown: 108,
|
|
110
|
+
PageDown: 109,
|
|
111
|
+
Insert: 110,
|
|
112
|
+
Delete: 111,
|
|
113
|
+
Pause: 119,
|
|
114
|
+
MetaLeft: 125,
|
|
115
|
+
MetaRight: 126,
|
|
116
|
+
ContextMenu: 127
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/keyboard.ts
|
|
120
|
+
const CODE_TO_WINDOWS_VIRTUAL_KEY = {
|
|
121
|
+
Backspace: 8,
|
|
122
|
+
Tab: 9,
|
|
123
|
+
Enter: 13,
|
|
124
|
+
NumpadEnter: 13,
|
|
125
|
+
ShiftLeft: 16,
|
|
126
|
+
ShiftRight: 16,
|
|
127
|
+
ControlLeft: 17,
|
|
128
|
+
ControlRight: 17,
|
|
129
|
+
AltLeft: 18,
|
|
130
|
+
AltRight: 18,
|
|
131
|
+
Pause: 19,
|
|
132
|
+
CapsLock: 20,
|
|
133
|
+
Escape: 27,
|
|
134
|
+
Space: 32,
|
|
135
|
+
PageUp: 33,
|
|
136
|
+
PageDown: 34,
|
|
137
|
+
End: 35,
|
|
138
|
+
Home: 36,
|
|
139
|
+
ArrowLeft: 37,
|
|
140
|
+
ArrowUp: 38,
|
|
141
|
+
ArrowRight: 39,
|
|
142
|
+
ArrowDown: 40,
|
|
143
|
+
Insert: 45,
|
|
144
|
+
Delete: 46,
|
|
145
|
+
MetaLeft: 91,
|
|
146
|
+
MetaRight: 92,
|
|
147
|
+
ContextMenu: 93,
|
|
148
|
+
Semicolon: 186,
|
|
149
|
+
Equal: 187,
|
|
150
|
+
Comma: 188,
|
|
151
|
+
Minus: 189,
|
|
152
|
+
Period: 190,
|
|
153
|
+
Slash: 191,
|
|
154
|
+
Backquote: 192,
|
|
155
|
+
BracketLeft: 219,
|
|
156
|
+
Backslash: 220,
|
|
157
|
+
BracketRight: 221,
|
|
158
|
+
Quote: 222
|
|
159
|
+
};
|
|
160
|
+
const KEY_TO_WINDOWS_VIRTUAL_KEY = {
|
|
161
|
+
Backspace: 8,
|
|
162
|
+
Tab: 9,
|
|
163
|
+
Enter: 13,
|
|
164
|
+
Shift: 16,
|
|
165
|
+
Control: 17,
|
|
166
|
+
Alt: 18,
|
|
167
|
+
Pause: 19,
|
|
168
|
+
CapsLock: 20,
|
|
169
|
+
Escape: 27,
|
|
170
|
+
" ": 32,
|
|
171
|
+
PageUp: 33,
|
|
172
|
+
PageDown: 34,
|
|
173
|
+
End: 35,
|
|
174
|
+
Home: 36,
|
|
175
|
+
ArrowLeft: 37,
|
|
176
|
+
ArrowUp: 38,
|
|
177
|
+
ArrowRight: 39,
|
|
178
|
+
ArrowDown: 40,
|
|
179
|
+
Insert: 45,
|
|
180
|
+
Delete: 46,
|
|
181
|
+
Meta: 91,
|
|
182
|
+
ContextMenu: 93
|
|
183
|
+
};
|
|
184
|
+
const FORWARDED_BROWSER_SHORTCUT_CODES = /* @__PURE__ */ new Set([
|
|
185
|
+
"KeyA",
|
|
186
|
+
"KeyC",
|
|
187
|
+
"KeyF",
|
|
188
|
+
"KeyG",
|
|
189
|
+
"KeyH",
|
|
190
|
+
"KeyJ",
|
|
191
|
+
"KeyK",
|
|
192
|
+
"KeyL",
|
|
193
|
+
"KeyN",
|
|
194
|
+
"KeyO",
|
|
195
|
+
"KeyP",
|
|
196
|
+
"KeyR",
|
|
197
|
+
"KeyS",
|
|
198
|
+
"KeyT",
|
|
199
|
+
"KeyW",
|
|
200
|
+
"KeyX",
|
|
201
|
+
"KeyV",
|
|
202
|
+
"KeyZ",
|
|
203
|
+
"KeyY",
|
|
204
|
+
"BracketLeft",
|
|
205
|
+
"BracketRight",
|
|
206
|
+
"ArrowLeft",
|
|
207
|
+
"ArrowRight",
|
|
208
|
+
"ArrowUp",
|
|
209
|
+
"ArrowDown",
|
|
210
|
+
"Backspace",
|
|
211
|
+
"Delete",
|
|
212
|
+
"Enter",
|
|
213
|
+
"Tab"
|
|
214
|
+
]);
|
|
215
|
+
function keyboardModifiers(event) {
|
|
216
|
+
let modifiers = 0;
|
|
217
|
+
if (event.altKey) modifiers |= 1;
|
|
218
|
+
if (event.ctrlKey) modifiers |= 2;
|
|
219
|
+
if (event.metaKey) modifiers |= 4;
|
|
220
|
+
if (event.shiftKey) modifiers |= 8;
|
|
221
|
+
return modifiers;
|
|
222
|
+
}
|
|
223
|
+
function isPrintableKey(event) {
|
|
224
|
+
return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
225
|
+
}
|
|
226
|
+
function keyboardInputMessage(event, action) {
|
|
227
|
+
const virtualKeyCode = windowsVirtualKeyCodeForCodeOrKey(event.code, event.key);
|
|
228
|
+
const modifiers = keyboardModifiers(event);
|
|
229
|
+
const text = action === "down" && isPrintableKey(event) ? event.key : void 0;
|
|
230
|
+
const unmodifiedText = event.key.length === 1 ? event.key : void 0;
|
|
231
|
+
return {
|
|
232
|
+
key: event.key,
|
|
233
|
+
code: event.code,
|
|
234
|
+
text,
|
|
235
|
+
unmodifiedText,
|
|
236
|
+
modifiers,
|
|
237
|
+
windowsVirtualKeyCode: virtualKeyCode,
|
|
238
|
+
nativeVirtualKeyCode: virtualKeyCode,
|
|
239
|
+
location: event.location,
|
|
240
|
+
autoRepeat: event.repeat,
|
|
241
|
+
isKeypad: event.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function shouldForwardBrowserShortcut(event) {
|
|
245
|
+
if (event.key === "Escape") return false;
|
|
246
|
+
if (isModifierCode(event.code)) return true;
|
|
247
|
+
if (!(event.metaKey || event.ctrlKey)) return true;
|
|
248
|
+
return FORWARDED_BROWSER_SHORTCUT_CODES.has(event.code);
|
|
249
|
+
}
|
|
250
|
+
function windowsVirtualKeyCodeForCodeOrKey(code, key) {
|
|
251
|
+
const byCode = code ? CODE_TO_WINDOWS_VIRTUAL_KEY[code] : void 0;
|
|
252
|
+
if (byCode !== void 0) return byCode;
|
|
253
|
+
if (code?.startsWith("Key") && code.length === 4) return code.charCodeAt(3);
|
|
254
|
+
if (code?.startsWith("Digit") && code.length === 6) return code.charCodeAt(5);
|
|
255
|
+
if (code?.startsWith("Numpad") && code.length === 7) {
|
|
256
|
+
const digit = Number(code.slice(6));
|
|
257
|
+
if (Number.isInteger(digit) && digit >= 0 && digit <= 9) return 96 + digit;
|
|
258
|
+
}
|
|
259
|
+
if (code?.startsWith("F")) {
|
|
260
|
+
const functionKey = Number(code.slice(1));
|
|
261
|
+
if (Number.isInteger(functionKey) && functionKey >= 1 && functionKey <= 24) return 111 + functionKey;
|
|
262
|
+
}
|
|
263
|
+
const byKey = key ? KEY_TO_WINDOWS_VIRTUAL_KEY[key] : void 0;
|
|
264
|
+
if (byKey !== void 0) return byKey;
|
|
265
|
+
if (key?.length === 1) return key.toUpperCase().charCodeAt(0);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
function isModifierCode(code) {
|
|
269
|
+
return code === "AltLeft" || code === "AltRight" || code === "ControlLeft" || code === "ControlRight" || code === "MetaLeft" || code === "MetaRight" || code === "ShiftLeft" || code === "ShiftRight";
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/protocol.ts
|
|
273
|
+
const LIVE_SESSION_PROTOCOL = "aperture-session.v1";
|
|
274
|
+
/**
|
|
275
|
+
* Server messages are decoded strictly: an unknown property means the server speaks a
|
|
276
|
+
* protocol revision this client does not understand.
|
|
277
|
+
*/
|
|
278
|
+
const strictParseOptions = { onExcessProperty: "error" };
|
|
279
|
+
const int = Schema.Number.check(Schema.isInt());
|
|
280
|
+
const positive = Schema.Number.check(Schema.isGreaterThan(0));
|
|
281
|
+
const positiveInt = int.check(Schema.isGreaterThan(0));
|
|
282
|
+
const unitInterval = Schema.Number.check(Schema.isBetween({
|
|
283
|
+
minimum: 0,
|
|
284
|
+
maximum: 1
|
|
285
|
+
}));
|
|
286
|
+
const emptyArray = Effect.succeed([]);
|
|
287
|
+
function arrayOrEmpty(item) {
|
|
288
|
+
return Schema.Array(item).pipe(Schema.withDecodingDefaultKey(emptyArray));
|
|
289
|
+
}
|
|
290
|
+
const LeaseMode = Schema.Literals(["implicit", "explicit"]);
|
|
291
|
+
const PresentationQuality = Schema.Struct({
|
|
292
|
+
profile: Schema.String,
|
|
293
|
+
fps: int.check(Schema.isBetween({
|
|
294
|
+
minimum: 1,
|
|
295
|
+
maximum: 120
|
|
296
|
+
})),
|
|
297
|
+
bitrateKbps: int.check(Schema.isGreaterThanOrEqualTo(100)),
|
|
298
|
+
keyframeInterval: positiveInt
|
|
299
|
+
});
|
|
300
|
+
const PresentationProfile = Schema.Struct({
|
|
301
|
+
id: Schema.String,
|
|
302
|
+
label: Schema.String,
|
|
303
|
+
codec: Schema.String,
|
|
304
|
+
mimeType: Schema.String,
|
|
305
|
+
sdpFmtpLine: Schema.String
|
|
306
|
+
});
|
|
307
|
+
const Presentation = Schema.Struct({
|
|
308
|
+
quality: Schema.optionalKey(PresentationQuality),
|
|
309
|
+
profiles: Schema.Array(PresentationProfile),
|
|
310
|
+
cursorVisible: Schema.Boolean
|
|
311
|
+
});
|
|
312
|
+
const Target = Schema.Struct({
|
|
313
|
+
id: Schema.String,
|
|
314
|
+
type: Schema.String,
|
|
315
|
+
title: Schema.String,
|
|
316
|
+
url: Schema.String,
|
|
317
|
+
loading: Schema.Boolean,
|
|
318
|
+
viewport: Schema.optionalKey(Schema.Struct({
|
|
319
|
+
width: positive,
|
|
320
|
+
height: positive,
|
|
321
|
+
contentWidth: positive,
|
|
322
|
+
contentHeight: positive,
|
|
323
|
+
canvasWidth: positive,
|
|
324
|
+
canvasHeight: positive,
|
|
325
|
+
deviceScaleFactor: positive
|
|
326
|
+
}))
|
|
327
|
+
});
|
|
328
|
+
const Participant = Schema.Struct({
|
|
329
|
+
clientId: Schema.String,
|
|
330
|
+
name: Schema.String,
|
|
331
|
+
avatarHash: Schema.String,
|
|
332
|
+
role: Schema.Literals([
|
|
333
|
+
"owner",
|
|
334
|
+
"editor",
|
|
335
|
+
"viewer",
|
|
336
|
+
"automation"
|
|
337
|
+
]),
|
|
338
|
+
activeTargetId: Schema.optionalKey(Schema.String),
|
|
339
|
+
followingClientId: Schema.optionalKey(Schema.String),
|
|
340
|
+
holdingInput: Schema.Boolean,
|
|
341
|
+
leaseMode: Schema.optionalKey(LeaseMode),
|
|
342
|
+
recovering: Schema.optionalKey(Schema.Boolean)
|
|
343
|
+
});
|
|
344
|
+
const Snapshot = Schema.Struct({
|
|
345
|
+
type: Schema.Literal("session.snapshot"),
|
|
346
|
+
clientId: Schema.String,
|
|
347
|
+
resumeSecret: Schema.String,
|
|
348
|
+
role: Schema.Literals([
|
|
349
|
+
"owner",
|
|
350
|
+
"editor",
|
|
351
|
+
"viewer"
|
|
352
|
+
]),
|
|
353
|
+
transport: Schema.Literals(["webrtc", "websocket"]),
|
|
354
|
+
holderClientId: Schema.optionalKey(Schema.String),
|
|
355
|
+
mode: Schema.optionalKey(LeaseMode),
|
|
356
|
+
activeTargetId: Schema.optionalKey(Schema.String),
|
|
357
|
+
targets: arrayOrEmpty(Target),
|
|
358
|
+
participants: arrayOrEmpty(Participant),
|
|
359
|
+
recordings: arrayOrEmpty(Recording),
|
|
360
|
+
presentation: Presentation
|
|
361
|
+
});
|
|
362
|
+
const PresenceState = Schema.Struct({
|
|
363
|
+
type: Schema.Literal("presence.state"),
|
|
364
|
+
participants: arrayOrEmpty(Participant)
|
|
365
|
+
});
|
|
366
|
+
const InputState = Schema.Struct({
|
|
367
|
+
type: Schema.Literal("input.state"),
|
|
368
|
+
holderClientId: Schema.optionalKey(Schema.String),
|
|
369
|
+
mode: Schema.optionalKey(LeaseMode)
|
|
370
|
+
});
|
|
371
|
+
const Cursor = Schema.Struct({
|
|
372
|
+
type: Schema.Literal("presence.cursor"),
|
|
373
|
+
clientId: Schema.String,
|
|
374
|
+
targetId: Schema.String,
|
|
375
|
+
x: unitInterval,
|
|
376
|
+
y: unitInterval,
|
|
377
|
+
realtimeCounter: positiveInt
|
|
378
|
+
});
|
|
379
|
+
const CursorClear = Schema.Struct({
|
|
380
|
+
type: Schema.Literal("presence.cursor.clear"),
|
|
381
|
+
clientId: Schema.String,
|
|
382
|
+
realtimeCounter: positiveInt
|
|
383
|
+
});
|
|
384
|
+
const Paint = Schema.Struct({
|
|
385
|
+
type: Schema.Literal("paint.point"),
|
|
386
|
+
clientId: Schema.String,
|
|
387
|
+
targetId: Schema.String,
|
|
388
|
+
strokeId: Schema.String,
|
|
389
|
+
color: Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
|
390
|
+
width: Schema.Number.check(Schema.isBetween({
|
|
391
|
+
minimum: 1,
|
|
392
|
+
maximum: 16
|
|
393
|
+
})),
|
|
394
|
+
phase: Schema.Literals([
|
|
395
|
+
"start",
|
|
396
|
+
"move",
|
|
397
|
+
"end"
|
|
398
|
+
]),
|
|
399
|
+
x: unitInterval,
|
|
400
|
+
y: unitInterval,
|
|
401
|
+
realtimeCounter: Schema.optionalKey(positiveInt)
|
|
402
|
+
});
|
|
403
|
+
const TargetsState = Schema.Struct({
|
|
404
|
+
type: Schema.Literal("targets.state"),
|
|
405
|
+
activeTargetId: Schema.optionalKey(Schema.String),
|
|
406
|
+
targets: arrayOrEmpty(Target)
|
|
407
|
+
});
|
|
408
|
+
const RecordingsState = Schema.Struct({
|
|
409
|
+
type: Schema.Literal("recordings.state"),
|
|
410
|
+
recordings: arrayOrEmpty(Recording)
|
|
411
|
+
});
|
|
412
|
+
const PresentationState = Schema.Struct({
|
|
413
|
+
type: Schema.Literal("presentation.state"),
|
|
414
|
+
presentation: Presentation
|
|
415
|
+
});
|
|
416
|
+
const ErrorMessage = Schema.Struct({
|
|
417
|
+
type: Schema.Literal("error"),
|
|
418
|
+
code: Schema.String,
|
|
419
|
+
message: Schema.String
|
|
420
|
+
});
|
|
421
|
+
const CommandResult = Schema.Struct({
|
|
422
|
+
type: Schema.Literals([
|
|
423
|
+
"target.select.result",
|
|
424
|
+
"target.create.result",
|
|
425
|
+
"target.close.result",
|
|
426
|
+
"page.navigate.result",
|
|
427
|
+
"page.history-back.result",
|
|
428
|
+
"page.history-forward.result",
|
|
429
|
+
"page.reload.result",
|
|
430
|
+
"page.stop-loading.result",
|
|
431
|
+
"viewport.set.result",
|
|
432
|
+
"presentation.quality.set.result",
|
|
433
|
+
"presentation.cursor.set.result",
|
|
434
|
+
"recording.start.result",
|
|
435
|
+
"recording.stop.result",
|
|
436
|
+
"recording.cancel.result"
|
|
437
|
+
]),
|
|
438
|
+
requestId: Schema.String,
|
|
439
|
+
ok: Schema.Boolean,
|
|
440
|
+
code: Schema.optionalKey(Schema.String),
|
|
441
|
+
message: Schema.optionalKey(Schema.String),
|
|
442
|
+
targetId: Schema.optionalKey(Schema.String),
|
|
443
|
+
recording: Schema.optionalKey(Recording),
|
|
444
|
+
presentation: Schema.optionalKey(Presentation)
|
|
445
|
+
});
|
|
446
|
+
const LiveSessionServerMessage = Schema.Union([
|
|
447
|
+
Snapshot,
|
|
448
|
+
PresenceState,
|
|
449
|
+
InputState,
|
|
450
|
+
Cursor,
|
|
451
|
+
CursorClear,
|
|
452
|
+
Paint,
|
|
453
|
+
TargetsState,
|
|
454
|
+
RecordingsState,
|
|
455
|
+
PresentationState,
|
|
456
|
+
ErrorMessage,
|
|
457
|
+
CommandResult
|
|
458
|
+
]);
|
|
459
|
+
/** Decodes one JSON text frame from the server, or returns undefined when it is invalid. */
|
|
460
|
+
const decodeServerMessage = Schema.decodeUnknownOption(Schema.fromJsonString(LiveSessionServerMessage), strictParseOptions);
|
|
461
|
+
const RasterFrameHeader = Schema.Struct({
|
|
462
|
+
type: Schema.Literal("presentation.frame"),
|
|
463
|
+
targetId: Schema.String,
|
|
464
|
+
width: positive,
|
|
465
|
+
height: positive
|
|
466
|
+
});
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/viewport.ts
|
|
469
|
+
const VIEWPORT_PRESETS = [
|
|
470
|
+
createViewportPreset(1280, 720, 1),
|
|
471
|
+
createViewportPreset(1440, 900, 1),
|
|
472
|
+
createViewportPreset(1920, 1080, 1),
|
|
473
|
+
createViewportPreset(2560, 1440, 1),
|
|
474
|
+
createViewportPreset(768, 1024, 1),
|
|
475
|
+
createViewportPreset(390, 844, 1)
|
|
476
|
+
];
|
|
477
|
+
const DEFAULT_VIEWPORT = VIEWPORT_PRESETS[0];
|
|
478
|
+
const VIEWPORT_DEVICE_SCALE_FACTORS = [
|
|
479
|
+
1,
|
|
480
|
+
1.5,
|
|
481
|
+
2,
|
|
482
|
+
3
|
|
483
|
+
];
|
|
484
|
+
const STALE_FRAME_MS = 3e3;
|
|
485
|
+
function createViewportPreset(width, height, deviceScaleFactor) {
|
|
486
|
+
return {
|
|
487
|
+
id: `${width}x${height}@${formatViewportScale(deviceScaleFactor)}`,
|
|
488
|
+
label: `${width}×${height}`,
|
|
489
|
+
width,
|
|
490
|
+
height,
|
|
491
|
+
deviceScaleFactor
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function formatViewportScale(deviceScaleFactor) {
|
|
495
|
+
return Number.isInteger(deviceScaleFactor) ? String(deviceScaleFactor) : String(deviceScaleFactor).replace(/0+$/, "").replace(/\.$/, "");
|
|
496
|
+
}
|
|
497
|
+
function computeRenderMetrics(containerWidth, containerHeight, viewportWidth, viewportHeight) {
|
|
498
|
+
if (viewportWidth <= 0 || viewportHeight <= 0 || containerWidth <= 0 || containerHeight <= 0) return {
|
|
499
|
+
scale: 1,
|
|
500
|
+
offsetX: 0,
|
|
501
|
+
offsetY: 0,
|
|
502
|
+
renderedWidth: containerWidth,
|
|
503
|
+
renderedHeight: containerHeight
|
|
504
|
+
};
|
|
505
|
+
const scale = Math.min(containerWidth / viewportWidth, containerHeight / viewportHeight);
|
|
506
|
+
const renderedWidth = viewportWidth * scale;
|
|
507
|
+
const renderedHeight = viewportHeight * scale;
|
|
508
|
+
return {
|
|
509
|
+
scale,
|
|
510
|
+
offsetX: (containerWidth - renderedWidth) / 2,
|
|
511
|
+
offsetY: (containerHeight - renderedHeight) / 2,
|
|
512
|
+
renderedWidth,
|
|
513
|
+
renderedHeight
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function mapClientToViewport(clientX, clientY, rect, viewportWidth, viewportHeight) {
|
|
517
|
+
const metrics = computeRenderMetrics(rect.width, rect.height, viewportWidth, viewportHeight);
|
|
518
|
+
const localX = clientX - rect.left - metrics.offsetX;
|
|
519
|
+
const localY = clientY - rect.top - metrics.offsetY;
|
|
520
|
+
if (localX < 0 || localY < 0 || localX > metrics.renderedWidth || localY > metrics.renderedHeight) return null;
|
|
521
|
+
return {
|
|
522
|
+
x: Math.round(localX / metrics.scale),
|
|
523
|
+
y: Math.round(localY / metrics.scale)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/connection.ts
|
|
528
|
+
var connection_exports = /* @__PURE__ */ __exportAll({
|
|
529
|
+
LiveSessionError: () => LiveSessionError,
|
|
530
|
+
make: () => make
|
|
531
|
+
});
|
|
532
|
+
/** A live session operation that could not complete. */
|
|
533
|
+
var LiveSessionError = class extends Data.TaggedError("LiveSessionError") {};
|
|
534
|
+
const SignalResponse = Schema.Union([
|
|
535
|
+
Schema.Struct({
|
|
536
|
+
version: Schema.Literal(1),
|
|
537
|
+
type: Schema.Literal("answer"),
|
|
538
|
+
sdp: Schema.String
|
|
539
|
+
}),
|
|
540
|
+
Schema.Struct({
|
|
541
|
+
version: Schema.Literal(1),
|
|
542
|
+
type: Schema.Literal("ice-candidate"),
|
|
543
|
+
candidate: Schema.Struct({
|
|
544
|
+
candidate: Schema.String,
|
|
545
|
+
sdpMid: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
546
|
+
sdpMLineIndex: Schema.optionalKey(Schema.NullOr(Schema.Number.check(Schema.isInt()))),
|
|
547
|
+
usernameFragment: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
548
|
+
})
|
|
549
|
+
}),
|
|
550
|
+
Schema.Struct({
|
|
551
|
+
version: Schema.Literal(1),
|
|
552
|
+
type: Schema.Literal("error"),
|
|
553
|
+
error: Schema.Struct({
|
|
554
|
+
code: Schema.String,
|
|
555
|
+
message: Schema.String
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
]);
|
|
559
|
+
const decodeSignalResponse = Schema.decodeUnknownOption(Schema.fromJsonString(SignalResponse), strictParseOptions);
|
|
560
|
+
const decodeRasterFrameHeader = Schema.decodeUnknownOption(Schema.fromJsonString(RasterFrameHeader), strictParseOptions);
|
|
561
|
+
const WEBRTC_DEADLINE_MS = 5e3;
|
|
562
|
+
const WEBSOCKET_RETRY_MS = 500;
|
|
563
|
+
const WEBRTC_RETRY_MAX_MS = 15e3;
|
|
564
|
+
/**
|
|
565
|
+
* Opens a live session connection. It prefers WebRTC presentation, falls back to
|
|
566
|
+
* WebSocket raster frames, and keeps retrying WebRTC in the background. Closing the scope
|
|
567
|
+
* closes every transport and fails pending commands.
|
|
568
|
+
*/
|
|
569
|
+
const make = Effect.fnUntraced(function* (options) {
|
|
570
|
+
const { callbacks } = options;
|
|
571
|
+
const runFork = yield* FiberSet.makeRuntime();
|
|
572
|
+
const fork = (effect) => runFork(effect);
|
|
573
|
+
const after = (millis, f) => fork(Effect.sleep(millis).pipe(Effect.andThen(Effect.sync(f))));
|
|
574
|
+
let active = null;
|
|
575
|
+
let candidate = null;
|
|
576
|
+
let identity = null;
|
|
577
|
+
let preferredTransport = options.webrtcSupported ? "webrtc" : "websocket";
|
|
578
|
+
let transportRequest = null;
|
|
579
|
+
let disposed = false;
|
|
580
|
+
let retryTimer = null;
|
|
581
|
+
let webrtcRetryMs = 1e3;
|
|
582
|
+
let nextRequestId = 0;
|
|
583
|
+
let realtimeCounter = 0;
|
|
584
|
+
let inboundRealtimeCounter = 0;
|
|
585
|
+
let candidateMessages = [];
|
|
586
|
+
let candidateFrame = null;
|
|
587
|
+
const pendingCommands = /* @__PURE__ */ new Map();
|
|
588
|
+
const hello = () => ({
|
|
589
|
+
type: "session.hello",
|
|
590
|
+
...identity ?? options.identity
|
|
591
|
+
});
|
|
592
|
+
const transportCallbacks = {
|
|
593
|
+
hello,
|
|
594
|
+
message: (transport, message) => handleMessage(transport, message),
|
|
595
|
+
ready: (transport) => activate(transport),
|
|
596
|
+
failed: (transport, error) => transportFailed(transport, error),
|
|
597
|
+
frame: (transport, frame) => {
|
|
598
|
+
if (active === transport) callbacks.onFrame(frame);
|
|
599
|
+
else if (candidate === transport) candidateFrame = frame;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
const dropCandidate = () => {
|
|
603
|
+
candidate?.close();
|
|
604
|
+
candidate = null;
|
|
605
|
+
candidateMessages = [];
|
|
606
|
+
candidateFrame = null;
|
|
607
|
+
};
|
|
608
|
+
const clearRetry = () => {
|
|
609
|
+
retryTimer?.interruptUnsafe();
|
|
610
|
+
retryTimer = null;
|
|
611
|
+
};
|
|
612
|
+
const rejectPending = (message) => {
|
|
613
|
+
const error = new LiveSessionError({ message });
|
|
614
|
+
for (const deferred of pendingCommands.values()) Deferred.doneUnsafe(deferred, Effect.fail(error));
|
|
615
|
+
pendingCommands.clear();
|
|
616
|
+
};
|
|
617
|
+
const rejectTransportRequest = (message) => {
|
|
618
|
+
if (!transportRequest) return;
|
|
619
|
+
const request = transportRequest;
|
|
620
|
+
transportRequest = null;
|
|
621
|
+
Deferred.doneUnsafe(request.deferred, Effect.fail(new LiveSessionError({ message })));
|
|
622
|
+
};
|
|
623
|
+
const startPreferredTransport = () => {
|
|
624
|
+
if (preferredTransport === "webrtc" && options.webrtcSupported) startWebRTC();
|
|
625
|
+
else startWebSocket();
|
|
626
|
+
};
|
|
627
|
+
const startWebRTC = () => {
|
|
628
|
+
if (disposed || candidate) return;
|
|
629
|
+
let transport;
|
|
630
|
+
try {
|
|
631
|
+
transport = new WebRTCSessionTransport({
|
|
632
|
+
baseUrl: options.baseUrl,
|
|
633
|
+
sessionId: options.sessionId,
|
|
634
|
+
credentials: options.credentials,
|
|
635
|
+
sessionToken: options.sessionToken,
|
|
636
|
+
iceServers: options.iceServers,
|
|
637
|
+
callbacks: transportCallbacks,
|
|
638
|
+
fork
|
|
639
|
+
});
|
|
640
|
+
} catch (cause) {
|
|
641
|
+
candidateFailed("webrtc", setupError(cause, "WebRTC setup failed"));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
candidate = transport;
|
|
645
|
+
candidateMessages = [];
|
|
646
|
+
candidateFrame = null;
|
|
647
|
+
transport.connect();
|
|
648
|
+
after(WEBRTC_DEADLINE_MS, () => {
|
|
649
|
+
if (candidate !== transport) return;
|
|
650
|
+
dropCandidate();
|
|
651
|
+
candidateFailed("webrtc", new LiveSessionError({ message: "WebRTC presentation timed out" }));
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
const startWebSocket = () => {
|
|
655
|
+
if (disposed || candidate) return;
|
|
656
|
+
let transport;
|
|
657
|
+
try {
|
|
658
|
+
transport = new WebSocketSessionTransport({
|
|
659
|
+
baseUrl: options.baseUrl,
|
|
660
|
+
sessionId: options.sessionId,
|
|
661
|
+
credentials: options.credentials,
|
|
662
|
+
sessionToken: options.sessionToken,
|
|
663
|
+
callbacks: transportCallbacks,
|
|
664
|
+
fork
|
|
665
|
+
});
|
|
666
|
+
} catch (cause) {
|
|
667
|
+
candidateFailed("websocket", setupError(cause, "WebSocket setup failed"));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
candidate = transport;
|
|
671
|
+
candidateMessages = [];
|
|
672
|
+
candidateFrame = null;
|
|
673
|
+
transport.connect();
|
|
674
|
+
};
|
|
675
|
+
const startWebSocketLater = (millis) => after(millis, () => {
|
|
676
|
+
if (!disposed && active === null && candidate === null) startWebSocket();
|
|
677
|
+
});
|
|
678
|
+
const activate = (transport) => {
|
|
679
|
+
if (disposed || candidate !== transport) {
|
|
680
|
+
transport.close();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const previous = active;
|
|
684
|
+
const messages = candidateMessages;
|
|
685
|
+
const frame = candidateFrame;
|
|
686
|
+
if (previous && previous !== transport) rejectPending("live session transport replaced");
|
|
687
|
+
candidate = null;
|
|
688
|
+
candidateMessages = [];
|
|
689
|
+
candidateFrame = null;
|
|
690
|
+
active = transport;
|
|
691
|
+
realtimeCounter = 0;
|
|
692
|
+
inboundRealtimeCounter = 0;
|
|
693
|
+
webrtcRetryMs = 1e3;
|
|
694
|
+
callbacks.onPhase("connected");
|
|
695
|
+
callbacks.onTransport(transport.kind);
|
|
696
|
+
if (transportRequest?.kind === transport.kind) {
|
|
697
|
+
const request = transportRequest;
|
|
698
|
+
transportRequest = null;
|
|
699
|
+
Deferred.doneUnsafe(request.deferred, Effect.void);
|
|
700
|
+
}
|
|
701
|
+
if (transport instanceof WebRTCSessionTransport) {
|
|
702
|
+
callbacks.onFrame(null);
|
|
703
|
+
callbacks.onStream(transport.mediaStream());
|
|
704
|
+
} else {
|
|
705
|
+
callbacks.onStream(null);
|
|
706
|
+
if (frame) callbacks.onFrame(frame);
|
|
707
|
+
}
|
|
708
|
+
for (const message of messages) deliverMessage(message);
|
|
709
|
+
if (transport.kind === "websocket" && preferredTransport === "webrtc" && options.webrtcSupported) scheduleWebRTCRetry();
|
|
710
|
+
if (previous && previous !== transport) previous.close();
|
|
711
|
+
};
|
|
712
|
+
const transportFailed = (transport, error) => {
|
|
713
|
+
if (disposed) return;
|
|
714
|
+
if (candidate === transport) {
|
|
715
|
+
dropCandidate();
|
|
716
|
+
candidateFailed(transport.kind, error);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (active !== transport) return;
|
|
720
|
+
active = null;
|
|
721
|
+
transport.close();
|
|
722
|
+
rejectPending("live session transport was lost");
|
|
723
|
+
callbacks.onPhase("disconnected");
|
|
724
|
+
callbacks.onError(error.message);
|
|
725
|
+
callbacks.onFrame(null);
|
|
726
|
+
callbacks.onStream(null);
|
|
727
|
+
callbacks.onTransport(null);
|
|
728
|
+
startWebSocketLater(transport.kind === "webrtc" ? 0 : WEBSOCKET_RETRY_MS);
|
|
729
|
+
};
|
|
730
|
+
const handleMessage = (transport, message) => {
|
|
731
|
+
if (message.type === "error" && message.code === "resume_rejected" && candidate === transport && active === null) identity = null;
|
|
732
|
+
if (message.type === "session.snapshot") identity = {
|
|
733
|
+
clientId: message.clientId,
|
|
734
|
+
resumeSecret: message.resumeSecret
|
|
735
|
+
};
|
|
736
|
+
if (active !== transport) {
|
|
737
|
+
if (candidate !== transport) return;
|
|
738
|
+
candidateMessages.push(message);
|
|
739
|
+
if (message.type === "session.snapshot" && transport.kind === "websocket") activate(transport);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
deliverMessage(message);
|
|
743
|
+
};
|
|
744
|
+
const deliverMessage = (message) => {
|
|
745
|
+
if ("realtimeCounter" in message && message.realtimeCounter !== void 0) {
|
|
746
|
+
if (message.realtimeCounter <= inboundRealtimeCounter) return;
|
|
747
|
+
inboundRealtimeCounter = message.realtimeCounter;
|
|
748
|
+
}
|
|
749
|
+
if ("requestId" in message) {
|
|
750
|
+
const deferred = pendingCommands.get(message.requestId);
|
|
751
|
+
if (deferred) {
|
|
752
|
+
pendingCommands.delete(message.requestId);
|
|
753
|
+
Deferred.doneUnsafe(deferred, message.ok ? Effect.succeed(message) : Effect.fail(new LiveSessionError({ message: message.message ?? "live session command failed" })));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
callbacks.onMessage(message);
|
|
757
|
+
};
|
|
758
|
+
const scheduleWebRTCRetry = () => {
|
|
759
|
+
if (disposed || preferredTransport !== "webrtc" || !options.webrtcSupported || active?.kind === "webrtc" || retryTimer !== null) return;
|
|
760
|
+
const delay = webrtcRetryMs;
|
|
761
|
+
webrtcRetryMs = Math.min(WEBRTC_RETRY_MAX_MS, webrtcRetryMs * 2);
|
|
762
|
+
retryTimer = after(delay, () => {
|
|
763
|
+
retryTimer = null;
|
|
764
|
+
startWebRTC();
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
const candidateFailed = (kind, error) => {
|
|
768
|
+
if (transportRequest?.kind === kind) {
|
|
769
|
+
rejectTransportRequest(error.message);
|
|
770
|
+
if (active) preferredTransport = active.kind;
|
|
771
|
+
}
|
|
772
|
+
if (active) {
|
|
773
|
+
if (active.kind === "websocket") scheduleWebRTCRetry();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (kind === "webrtc") {
|
|
777
|
+
startWebSocket();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
callbacks.onPhase("disconnected");
|
|
781
|
+
callbacks.onError(error.message);
|
|
782
|
+
startWebSocketLater(WEBSOCKET_RETRY_MS);
|
|
783
|
+
};
|
|
784
|
+
const sendReliable = (message) => active?.sendReliable(message) ?? false;
|
|
785
|
+
const sendRealtime = (message) => {
|
|
786
|
+
realtimeCounter += 1;
|
|
787
|
+
return active?.sendRealtime({
|
|
788
|
+
realtimeCounter,
|
|
789
|
+
...message
|
|
790
|
+
}) ?? false;
|
|
791
|
+
};
|
|
792
|
+
const close = () => {
|
|
793
|
+
disposed = true;
|
|
794
|
+
clearRetry();
|
|
795
|
+
dropCandidate();
|
|
796
|
+
active?.close();
|
|
797
|
+
active = null;
|
|
798
|
+
rejectTransportRequest("live session connection closed");
|
|
799
|
+
rejectPending("live session connection closed");
|
|
800
|
+
callbacks.onFrame(null);
|
|
801
|
+
callbacks.onStream(null);
|
|
802
|
+
callbacks.onTransport(null);
|
|
803
|
+
};
|
|
804
|
+
yield* Effect.addFinalizer(() => Effect.sync(close));
|
|
805
|
+
callbacks.onPhase("connecting");
|
|
806
|
+
startPreferredTransport();
|
|
807
|
+
return {
|
|
808
|
+
reconnect: Effect.sync(() => {
|
|
809
|
+
clearRetry();
|
|
810
|
+
dropCandidate();
|
|
811
|
+
active?.close();
|
|
812
|
+
active = null;
|
|
813
|
+
rejectTransportRequest("live session transport replaced");
|
|
814
|
+
rejectPending("live session transport replaced");
|
|
815
|
+
callbacks.onPhase("connecting");
|
|
816
|
+
startPreferredTransport();
|
|
817
|
+
}),
|
|
818
|
+
selectTransport: (kind) => Effect.suspend(() => {
|
|
819
|
+
if (kind === "webrtc" && !options.webrtcSupported) return Effect.fail(new LiveSessionError({ message: "WebRTC presentation is unavailable" }));
|
|
820
|
+
preferredTransport = kind;
|
|
821
|
+
clearRetry();
|
|
822
|
+
if (candidate && candidate.kind !== kind) dropCandidate();
|
|
823
|
+
rejectTransportRequest("presentation selection was replaced");
|
|
824
|
+
if (active?.kind === kind) return Effect.void;
|
|
825
|
+
const deferred = Deferred.makeUnsafe();
|
|
826
|
+
transportRequest = {
|
|
827
|
+
kind,
|
|
828
|
+
deferred
|
|
829
|
+
};
|
|
830
|
+
if (candidate === null) if (kind === "webrtc") startWebRTC();
|
|
831
|
+
else startWebSocket();
|
|
832
|
+
return Deferred.await(deferred);
|
|
833
|
+
}),
|
|
834
|
+
sendReliable,
|
|
835
|
+
sendRealtime,
|
|
836
|
+
command: (type, payload = {}) => Effect.suspend(() => {
|
|
837
|
+
nextRequestId += 1;
|
|
838
|
+
const requestId = `request-${nextRequestId}`;
|
|
839
|
+
const deferred = Deferred.makeUnsafe();
|
|
840
|
+
pendingCommands.set(requestId, deferred);
|
|
841
|
+
if (!sendReliable({
|
|
842
|
+
type,
|
|
843
|
+
requestId,
|
|
844
|
+
...payload
|
|
845
|
+
})) {
|
|
846
|
+
pendingCommands.delete(requestId);
|
|
847
|
+
return Effect.fail(new LiveSessionError({ message: "live session transport is unavailable" }));
|
|
848
|
+
}
|
|
849
|
+
return Deferred.await(deferred).pipe(Effect.onInterrupt(() => Effect.sync(() => pendingCommands.delete(requestId))));
|
|
850
|
+
})
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
function setupError(cause, fallback) {
|
|
854
|
+
return new LiveSessionError({ message: cause instanceof Error && cause.message ? cause.message : fallback });
|
|
855
|
+
}
|
|
856
|
+
var WebSocketSessionTransport = class {
|
|
857
|
+
kind = "websocket";
|
|
858
|
+
socket;
|
|
859
|
+
callbacks;
|
|
860
|
+
fork;
|
|
861
|
+
pendingRealtime = /* @__PURE__ */ new Map();
|
|
862
|
+
pendingRasterPacket = null;
|
|
863
|
+
rasterDecoder = null;
|
|
864
|
+
decodingRaster = false;
|
|
865
|
+
realtimeFrame = null;
|
|
866
|
+
closed = false;
|
|
867
|
+
constructor(options) {
|
|
868
|
+
this.callbacks = options.callbacks;
|
|
869
|
+
this.fork = options.fork;
|
|
870
|
+
this.socket = new WebSocket(sessionWebSocketURL(options.baseUrl, options.sessionId), sessionProtocols(options.credentials, options.sessionToken));
|
|
871
|
+
this.socket.binaryType = "blob";
|
|
872
|
+
}
|
|
873
|
+
connect() {
|
|
874
|
+
this.socket.addEventListener("open", () => {
|
|
875
|
+
this.sendReliable(this.callbacks.hello());
|
|
876
|
+
});
|
|
877
|
+
this.socket.addEventListener("message", (event) => {
|
|
878
|
+
if (typeof event.data === "string") {
|
|
879
|
+
const message = decodeServerMessage(event.data);
|
|
880
|
+
if (Option.isSome(message)) this.callbacks.message(this, message.value);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (event.data instanceof Blob) {
|
|
884
|
+
this.pendingRasterPacket = event.data;
|
|
885
|
+
if (!this.decodingRaster) {
|
|
886
|
+
this.decodingRaster = true;
|
|
887
|
+
this.rasterDecoder = this.fork(this.decodeRasterPackets());
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
this.socket.addEventListener("close", () => {
|
|
892
|
+
if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message: "WebSocket session transport closed" }));
|
|
893
|
+
});
|
|
894
|
+
this.socket.addEventListener("error", () => {
|
|
895
|
+
if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message: "WebSocket session transport failed" }));
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
sendReliable(message) {
|
|
899
|
+
this.flushRealtime();
|
|
900
|
+
return this.sendNow(message);
|
|
901
|
+
}
|
|
902
|
+
sendRealtime(message) {
|
|
903
|
+
if (this.socket.readyState !== WebSocket.OPEN) return false;
|
|
904
|
+
this.pendingRealtime.delete(message.type);
|
|
905
|
+
this.pendingRealtime.set(message.type, message);
|
|
906
|
+
if (this.realtimeFrame === null) this.realtimeFrame = window.requestAnimationFrame(() => {
|
|
907
|
+
this.realtimeFrame = null;
|
|
908
|
+
this.flushRealtime();
|
|
909
|
+
});
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
close() {
|
|
913
|
+
this.closed = true;
|
|
914
|
+
this.pendingRasterPacket = null;
|
|
915
|
+
this.rasterDecoder?.interruptUnsafe();
|
|
916
|
+
this.rasterDecoder = null;
|
|
917
|
+
if (this.realtimeFrame !== null) {
|
|
918
|
+
window.cancelAnimationFrame(this.realtimeFrame);
|
|
919
|
+
this.realtimeFrame = null;
|
|
920
|
+
}
|
|
921
|
+
this.pendingRealtime.clear();
|
|
922
|
+
this.socket.close();
|
|
923
|
+
}
|
|
924
|
+
decodeRasterPackets() {
|
|
925
|
+
return Effect.gen({ self: this }, function* () {
|
|
926
|
+
while (true) {
|
|
927
|
+
const packet = this.pendingRasterPacket;
|
|
928
|
+
if (packet === null || this.closed) {
|
|
929
|
+
this.decodingRaster = false;
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.pendingRasterPacket = null;
|
|
933
|
+
const frame = yield* decodeRasterFrame(packet);
|
|
934
|
+
if (Option.isSome(frame) && !this.closed) this.callbacks.frame(this, frame.value);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
flushRealtime() {
|
|
939
|
+
if (this.realtimeFrame !== null) {
|
|
940
|
+
window.cancelAnimationFrame(this.realtimeFrame);
|
|
941
|
+
this.realtimeFrame = null;
|
|
942
|
+
}
|
|
943
|
+
for (const message of this.pendingRealtime.values()) this.sendNow(message);
|
|
944
|
+
this.pendingRealtime.clear();
|
|
945
|
+
}
|
|
946
|
+
sendNow(message) {
|
|
947
|
+
if (this.socket.readyState !== WebSocket.OPEN) return false;
|
|
948
|
+
this.socket.send(JSON.stringify(message));
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
var WebRTCSessionTransport = class {
|
|
953
|
+
kind = "webrtc";
|
|
954
|
+
connection;
|
|
955
|
+
reliable;
|
|
956
|
+
realtime;
|
|
957
|
+
signal;
|
|
958
|
+
callbacks;
|
|
959
|
+
fork;
|
|
960
|
+
pendingCandidates = [];
|
|
961
|
+
reliableOpen = false;
|
|
962
|
+
realtimeOpen = false;
|
|
963
|
+
helloSent = false;
|
|
964
|
+
snapshotReceived = false;
|
|
965
|
+
stream = null;
|
|
966
|
+
streamReady = false;
|
|
967
|
+
closed = false;
|
|
968
|
+
constructor(options) {
|
|
969
|
+
this.callbacks = options.callbacks;
|
|
970
|
+
this.fork = options.fork;
|
|
971
|
+
this.connection = new RTCPeerConnection({ iceServers: options.iceServers.map((server) => ({
|
|
972
|
+
...server,
|
|
973
|
+
urls: [...server.urls]
|
|
974
|
+
})) });
|
|
975
|
+
this.reliable = this.connection.createDataChannel("application", { ordered: true });
|
|
976
|
+
this.realtime = this.connection.createDataChannel("application-realtime", {
|
|
977
|
+
ordered: false,
|
|
978
|
+
maxRetransmits: 0
|
|
979
|
+
});
|
|
980
|
+
this.connection.addTransceiver("video", { direction: "recvonly" });
|
|
981
|
+
this.signal = new WebSocket(signalWebSocketURL(options.baseUrl, options.sessionId), sessionProtocols(options.credentials, options.sessionToken));
|
|
982
|
+
}
|
|
983
|
+
connect() {
|
|
984
|
+
this.reliable.addEventListener("open", () => {
|
|
985
|
+
this.reliableOpen = true;
|
|
986
|
+
this.sendHello();
|
|
987
|
+
});
|
|
988
|
+
this.realtime.addEventListener("open", () => {
|
|
989
|
+
this.realtimeOpen = true;
|
|
990
|
+
this.sendHello();
|
|
991
|
+
});
|
|
992
|
+
this.reliable.addEventListener("message", (event) => this.handleApplicationMessage(event));
|
|
993
|
+
this.realtime.addEventListener("message", (event) => this.handleApplicationMessage(event));
|
|
994
|
+
for (const channel of [this.reliable, this.realtime]) {
|
|
995
|
+
channel.addEventListener("close", () => this.fail("WebRTC session channel closed"));
|
|
996
|
+
channel.addEventListener("error", () => this.fail("WebRTC session channel failed"));
|
|
997
|
+
}
|
|
998
|
+
this.connection.addEventListener("track", (event) => {
|
|
999
|
+
this.stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
1000
|
+
const ready = () => {
|
|
1001
|
+
if (!this.stream) return;
|
|
1002
|
+
this.streamReady = true;
|
|
1003
|
+
this.maybeReady();
|
|
1004
|
+
};
|
|
1005
|
+
if (event.track.muted) event.track.addEventListener("unmute", ready, { once: true });
|
|
1006
|
+
else ready();
|
|
1007
|
+
});
|
|
1008
|
+
this.connection.addEventListener("icecandidate", (event) => {
|
|
1009
|
+
if (event.candidate && this.signal.readyState === WebSocket.OPEN) this.signal.send(JSON.stringify({
|
|
1010
|
+
version: 1,
|
|
1011
|
+
type: "ice-candidate",
|
|
1012
|
+
candidate: event.candidate.toJSON()
|
|
1013
|
+
}));
|
|
1014
|
+
});
|
|
1015
|
+
this.connection.addEventListener("connectionstatechange", () => {
|
|
1016
|
+
if (this.connection.connectionState === "failed" || this.connection.connectionState === "closed") this.fail(`WebRTC ${this.connection.connectionState}`);
|
|
1017
|
+
});
|
|
1018
|
+
this.signal.addEventListener("open", () => {
|
|
1019
|
+
this.fork(this.sendOffer().pipe(Effect.catchTag("LiveSessionError", (error) => Effect.sync(() => this.fail(error.message)))));
|
|
1020
|
+
});
|
|
1021
|
+
this.signal.addEventListener("message", (event) => this.handleSignal(event));
|
|
1022
|
+
this.signal.addEventListener("close", () => this.fail("WebRTC signaling closed"));
|
|
1023
|
+
this.signal.addEventListener("error", () => this.fail("WebRTC signaling failed"));
|
|
1024
|
+
}
|
|
1025
|
+
sendReliable(message) {
|
|
1026
|
+
if (this.reliable.readyState !== "open") return false;
|
|
1027
|
+
this.reliable.send(JSON.stringify(message));
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
sendRealtime(message) {
|
|
1031
|
+
if (this.realtime.readyState !== "open") return false;
|
|
1032
|
+
this.realtime.send(JSON.stringify(message));
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
close() {
|
|
1036
|
+
this.closed = true;
|
|
1037
|
+
this.reliable.close();
|
|
1038
|
+
this.realtime.close();
|
|
1039
|
+
this.connection.close();
|
|
1040
|
+
this.signal.close();
|
|
1041
|
+
}
|
|
1042
|
+
mediaStream() {
|
|
1043
|
+
return this.stream;
|
|
1044
|
+
}
|
|
1045
|
+
sendHello() {
|
|
1046
|
+
if (this.helloSent || !this.reliableOpen || !this.realtimeOpen) return;
|
|
1047
|
+
this.helloSent = this.sendReliable(this.callbacks.hello());
|
|
1048
|
+
}
|
|
1049
|
+
handleApplicationMessage(event) {
|
|
1050
|
+
if (typeof event.data !== "string") return;
|
|
1051
|
+
const message = decodeServerMessage(event.data);
|
|
1052
|
+
if (Option.isNone(message)) {
|
|
1053
|
+
this.fail("WebRTC session message is invalid");
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
this.callbacks.message(this, message.value);
|
|
1057
|
+
if (message.value.type === "session.snapshot") {
|
|
1058
|
+
this.snapshotReceived = true;
|
|
1059
|
+
this.maybeReady();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
handleSignal(event) {
|
|
1063
|
+
const decoded = typeof event.data === "string" ? decodeSignalResponse(event.data) : Option.none();
|
|
1064
|
+
if (Option.isNone(decoded)) {
|
|
1065
|
+
this.fail("WebRTC signaling message is invalid");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const signal = decoded.value;
|
|
1069
|
+
if (signal.type === "error") {
|
|
1070
|
+
this.fail(signal.error.message);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (signal.type === "answer") {
|
|
1074
|
+
this.fork(Effect.tryPromise(() => this.connection.setRemoteDescription({
|
|
1075
|
+
type: "answer",
|
|
1076
|
+
sdp: signal.sdp
|
|
1077
|
+
})).pipe(Effect.andThen(Effect.suspend(() => Effect.forEach(this.pendingCandidates.splice(0), (candidate) => Effect.tryPromise(() => this.connection.addIceCandidate(candidate)), { discard: true }))), Effect.catch(() => Effect.sync(() => this.fail("WebRTC answer is invalid")))));
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (!this.connection.remoteDescription) {
|
|
1081
|
+
this.pendingCandidates.push(signal.candidate);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
this.fork(Effect.tryPromise(() => this.connection.addIceCandidate(signal.candidate)).pipe(Effect.catch(() => Effect.sync(() => this.fail("WebRTC ICE candidate is invalid")))));
|
|
1085
|
+
}
|
|
1086
|
+
sendOffer() {
|
|
1087
|
+
return Effect.gen({ self: this }, function* () {
|
|
1088
|
+
const negotiationFailed = (cause) => setupError(cause, "WebRTC negotiation failed");
|
|
1089
|
+
const offer = yield* Effect.tryPromise({
|
|
1090
|
+
try: () => this.connection.createOffer(),
|
|
1091
|
+
catch: negotiationFailed
|
|
1092
|
+
});
|
|
1093
|
+
yield* Effect.tryPromise({
|
|
1094
|
+
try: () => this.connection.setLocalDescription(offer),
|
|
1095
|
+
catch: negotiationFailed
|
|
1096
|
+
});
|
|
1097
|
+
const sdp = this.connection.localDescription?.sdp;
|
|
1098
|
+
if (!sdp) return yield* new LiveSessionError({ message: "WebRTC offer is unavailable" });
|
|
1099
|
+
this.signal.send(JSON.stringify({
|
|
1100
|
+
version: 1,
|
|
1101
|
+
type: "offer",
|
|
1102
|
+
sdp
|
|
1103
|
+
}));
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
maybeReady() {
|
|
1107
|
+
if (this.snapshotReceived && this.stream && this.streamReady) this.callbacks.ready(this);
|
|
1108
|
+
}
|
|
1109
|
+
fail(message) {
|
|
1110
|
+
if (!this.closed) this.callbacks.failed(this, new LiveSessionError({ message }));
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
function decodeRasterFrame(packet) {
|
|
1114
|
+
return Effect.gen(function* () {
|
|
1115
|
+
if (packet.size < 4) return Option.none();
|
|
1116
|
+
const prefix = yield* Effect.tryPromise(() => packet.slice(0, 4).arrayBuffer());
|
|
1117
|
+
const headerLength = new DataView(prefix).getUint32(0);
|
|
1118
|
+
if (headerLength === 0 || headerLength > packet.size - 4) return Option.none();
|
|
1119
|
+
const header = decodeRasterFrameHeader(yield* Effect.tryPromise(() => packet.slice(4, 4 + headerLength).text()));
|
|
1120
|
+
return Option.map(header, (value) => ({
|
|
1121
|
+
targetId: value.targetId,
|
|
1122
|
+
data: packet.slice(4 + headerLength, packet.size, "image/jpeg"),
|
|
1123
|
+
width: value.width,
|
|
1124
|
+
height: value.height
|
|
1125
|
+
}));
|
|
1126
|
+
}).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
1127
|
+
}
|
|
1128
|
+
function sessionWebSocketURL(baseUrl, sessionId) {
|
|
1129
|
+
return webSocketURL(baseUrl, `/sessions/${encodeURIComponent(sessionId)}/session`);
|
|
1130
|
+
}
|
|
1131
|
+
function signalWebSocketURL(baseUrl, sessionId) {
|
|
1132
|
+
return webSocketURL(baseUrl, `/sessions/${encodeURIComponent(sessionId)}/webrtc/signal`);
|
|
1133
|
+
}
|
|
1134
|
+
function webSocketURL(baseUrl, path) {
|
|
1135
|
+
const url = new URL(path, baseUrl ?? window.location.href);
|
|
1136
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1137
|
+
return url.toString();
|
|
1138
|
+
}
|
|
1139
|
+
function sessionProtocols(credentials, sessionToken) {
|
|
1140
|
+
const protocols = [LIVE_SESSION_PROTOCOL];
|
|
1141
|
+
if (sessionToken) protocols.push(`authorization.bearer.${sessionToken}`);
|
|
1142
|
+
else if (credentials.kind === "bearer") protocols.push(`authorization.bearer.${credentials.token}`);
|
|
1143
|
+
const tenantId = resolveTenantHeader(credentials, "tenant-scoped");
|
|
1144
|
+
if (tenantId) protocols.push(`x-aperture-tenant-id.${tenantId}`);
|
|
1145
|
+
return protocols;
|
|
1146
|
+
}
|
|
1147
|
+
//#endregion
|
|
1148
|
+
export { DEFAULT_VIEWPORT, LIVE_SESSION_PROTOCOL, connection_exports as LiveSessionConnection, LiveSessionError, LiveSessionServerMessage, RasterFrameHeader, STALE_FRAME_MS, VIEWPORT_DEVICE_SCALE_FACTORS, VIEWPORT_PRESETS, computeRenderMetrics, createViewportPreset, decodeServerMessage, evdevKeycodeByCode, formatViewportScale, isPrintableKey, keyboardInputMessage, keyboardModifiers, mapClientToViewport, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey };
|
|
1149
|
+
|
|
1150
|
+
//# sourceMappingURL=index.js.map
|