@sidebandchat/client 0.2.1 → 0.3.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/dist/activity/activity-sharing-controller.d.ts +2 -0
- package/dist/activity/activity-sharing-controller.d.ts.map +1 -1
- package/dist/activity/activity-sharing-controller.js +40 -9
- package/dist/activity/activity-sharing-controller.js.map +1 -1
- package/dist/calls/call-runtime.d.ts +37 -0
- package/dist/calls/call-runtime.d.ts.map +1 -0
- package/dist/calls/call-runtime.js +2 -0
- package/dist/calls/call-runtime.js.map +1 -0
- package/dist/calls/index.d.ts +1 -0
- package/dist/calls/index.d.ts.map +1 -1
- package/dist/calls/media-controls.d.ts +5 -0
- package/dist/calls/media-controls.d.ts.map +1 -1
- package/dist/calls/media-controls.js +79 -4
- package/dist/calls/media-controls.js.map +1 -1
- package/dist/calls/media.d.ts +1 -1
- package/dist/calls/media.d.ts.map +1 -1
- package/dist/calls/media.js +5 -12
- package/dist/calls/media.js.map +1 -1
- package/dist/client/ephemeral-state.d.ts.map +1 -1
- package/dist/client/ephemeral-state.js +31 -23
- package/dist/client/ephemeral-state.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/platform-adapters.d.ts +21 -1
- package/dist/client/platform-adapters.d.ts.map +1 -1
- package/dist/client/session-runtime.d.ts +35 -1
- package/dist/client/session-runtime.d.ts.map +1 -1
- package/dist/client/session-runtime.js +428 -36
- package/dist/client/session-runtime.js.map +1 -1
- package/dist/client/sideband-client.d.ts.map +1 -1
- package/dist/client/sideband-client.js +18 -1
- package/dist/client/sideband-client.js.map +1 -1
- package/dist/communities/community-client.d.ts.map +1 -1
- package/dist/communities/community-client.js +11 -2
- package/dist/communities/community-client.js.map +1 -1
- package/dist/domain.d.ts +3 -2
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js.map +1 -1
- package/dist/realtime/realtime-client.d.ts +8 -0
- package/dist/realtime/realtime-client.d.ts.map +1 -1
- package/dist/realtime/realtime-client.js +9 -0
- package/dist/realtime/realtime-client.js.map +1 -1
- package/dist/sbp/records.d.ts.map +1 -1
- package/dist/sbp/records.js +20 -5
- package/dist/sbp/records.js.map +1 -1
- package/dist/sbp/value.d.ts.map +1 -1
- package/dist/sbp/value.js +1 -1
- package/dist/sbp/value.js.map +1 -1
- package/dist/social/social-client.d.ts +1 -0
- package/dist/social/social-client.d.ts.map +1 -1
- package/dist/social/social-client.js +3 -0
- package/dist/social/social-client.js.map +1 -1
- package/dist/workspace/workspace-controller.d.ts +1 -1
- package/dist/workspace/workspace-controller.d.ts.map +1 -1
- package/dist/workspace/workspace-controller.js +3 -6
- package/dist/workspace/workspace-controller.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decodeEntityRef } from "../sbp/codec.js";
|
|
2
2
|
import { decodeCall, decodePrivateConversation, decodePrivateTransfer, } from "../sbp/records.js";
|
|
3
|
+
import { SbpProtocolError } from "../sbp/session.js";
|
|
3
4
|
import { MediaControls } from "../calls/media-controls.js";
|
|
4
5
|
import { arrayValue, bytesValue, mapValue, required, unsignedValue, } from "../sbp/value.js";
|
|
5
6
|
export class SessionRuntime {
|
|
@@ -11,8 +12,10 @@ export class SessionRuntime {
|
|
|
11
12
|
media;
|
|
12
13
|
crypto;
|
|
13
14
|
transferAdapter;
|
|
15
|
+
options;
|
|
14
16
|
mediaControls;
|
|
15
17
|
current = {
|
|
18
|
+
callConnection: "idle",
|
|
16
19
|
calls: new Map(),
|
|
17
20
|
privateConversations: new Map(),
|
|
18
21
|
transfers: new Map(),
|
|
@@ -20,6 +23,7 @@ export class SessionRuntime {
|
|
|
20
23
|
};
|
|
21
24
|
listeners = new Set();
|
|
22
25
|
privateEventListeners = new Set();
|
|
26
|
+
callEventListeners = new Set();
|
|
23
27
|
unsubscribers = [];
|
|
24
28
|
pending = Promise.resolve();
|
|
25
29
|
cryptoHandles = new Map();
|
|
@@ -28,7 +32,10 @@ export class SessionRuntime {
|
|
|
28
32
|
connectingTransfers = new Map();
|
|
29
33
|
transferKeys = new Map();
|
|
30
34
|
transferProgress = new Map();
|
|
31
|
-
|
|
35
|
+
connectingAnsweredCalls = new Set();
|
|
36
|
+
watchedCommunityCallIds = new Set();
|
|
37
|
+
mediaGeneration = 0;
|
|
38
|
+
constructor(session, calls, privateConversations, endpoint, ownUserId, media, crypto, transferAdapter, mediaPreferences, options = {}) {
|
|
32
39
|
this.session = session;
|
|
33
40
|
this.calls = calls;
|
|
34
41
|
this.privateConversations = privateConversations;
|
|
@@ -37,6 +44,7 @@ export class SessionRuntime {
|
|
|
37
44
|
this.media = media;
|
|
38
45
|
this.crypto = crypto;
|
|
39
46
|
this.transferAdapter = transferAdapter;
|
|
47
|
+
this.options = options;
|
|
40
48
|
if (media)
|
|
41
49
|
this.mediaControls = new MediaControls(media, mediaPreferences);
|
|
42
50
|
}
|
|
@@ -51,6 +59,32 @@ export class SessionRuntime {
|
|
|
51
59
|
this.privateEventListeners.add(listener);
|
|
52
60
|
return () => this.privateEventListeners.delete(listener);
|
|
53
61
|
};
|
|
62
|
+
subscribeCallEvents = (listener) => {
|
|
63
|
+
this.callEventListeners.add(listener);
|
|
64
|
+
return () => this.callEventListeners.delete(listener);
|
|
65
|
+
};
|
|
66
|
+
callView() {
|
|
67
|
+
const current = this.current.currentCallId
|
|
68
|
+
? this.current.calls.get(this.current.currentCallId)
|
|
69
|
+
: undefined;
|
|
70
|
+
const incoming = [...this.current.calls.values()].find((call) => openCall(call) &&
|
|
71
|
+
call.id !== current?.id &&
|
|
72
|
+
call.originatorId !== this.ownUserId &&
|
|
73
|
+
ownParticipant(call, this.ownUserId)?.state === "ringing");
|
|
74
|
+
return {
|
|
75
|
+
connection: this.current.callConnection,
|
|
76
|
+
phase: current ? callPhase(current) : undefined,
|
|
77
|
+
current,
|
|
78
|
+
incoming,
|
|
79
|
+
joinable: new Map([...this.current.calls].filter(([, call]) => openCall(call) && call.target.kind !== "user")),
|
|
80
|
+
pendingParticipantIds: current && current.target.kind === "conversation" && openCall(current)
|
|
81
|
+
? current.participants
|
|
82
|
+
.filter((participant) => participant.userId !== current.originatorId &&
|
|
83
|
+
["invited", "ringing", "joining"].includes(participant.state))
|
|
84
|
+
.map((participant) => participant.userId)
|
|
85
|
+
: [],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
54
88
|
privateConversationView(id) {
|
|
55
89
|
const session = id
|
|
56
90
|
? this.current.privateConversations.get(id)
|
|
@@ -101,16 +135,23 @@ export class SessionRuntime {
|
|
|
101
135
|
if (this.unsubscribers.length)
|
|
102
136
|
return;
|
|
103
137
|
const unsubscribeStatus = this.session.subscribeStatus?.((status) => this.receiveStatus(status));
|
|
138
|
+
const unsubscribeMedia = this.media?.subscribeEvents?.((event) => this.receiveMediaEvent(event));
|
|
104
139
|
this.unsubscribers = [
|
|
105
140
|
this.session.subscribeEvents((frame) => this.receiveEvent(frame)),
|
|
106
141
|
this.session.subscribeControls((frame) => this.receiveControl(frame)),
|
|
107
142
|
...(unsubscribeStatus ? [unsubscribeStatus] : []),
|
|
143
|
+
...(unsubscribeMedia ? [unsubscribeMedia] : []),
|
|
108
144
|
];
|
|
109
145
|
void this.mediaControls?.start();
|
|
110
146
|
}
|
|
111
147
|
stop() {
|
|
148
|
+
const currentCallId = this.current.currentCallId;
|
|
149
|
+
this.mediaGeneration += 1;
|
|
112
150
|
this.unsubscribers.splice(0).forEach((unsubscribe) => unsubscribe());
|
|
151
|
+
if (currentCallId)
|
|
152
|
+
void this.media?.disconnect(currentCallId);
|
|
113
153
|
this.current = {
|
|
154
|
+
callConnection: "idle",
|
|
114
155
|
calls: new Map(),
|
|
115
156
|
privateConversations: new Map(),
|
|
116
157
|
transfers: new Map(),
|
|
@@ -121,6 +162,7 @@ export class SessionRuntime {
|
|
|
121
162
|
this.cryptoHandles.clear();
|
|
122
163
|
this.privateInvites.clear();
|
|
123
164
|
this.interruptedPrivateConversations.clear();
|
|
165
|
+
this.watchedCommunityCallIds.clear();
|
|
124
166
|
for (const transferId of this.connectingTransfers.keys())
|
|
125
167
|
void this.transferAdapter?.disconnect(transferId);
|
|
126
168
|
this.connectingTransfers.clear();
|
|
@@ -131,9 +173,12 @@ export class SessionRuntime {
|
|
|
131
173
|
receiveStatus(status) {
|
|
132
174
|
if (status.state !== "offline")
|
|
133
175
|
return;
|
|
176
|
+
const currentCallId = this.current.currentCallId;
|
|
177
|
+
this.mediaGeneration += 1;
|
|
178
|
+
if (currentCallId)
|
|
179
|
+
void this.media?.disconnect(currentCallId);
|
|
180
|
+
this.mediaControls?.resetSession();
|
|
134
181
|
const interrupted = [...this.current.privateConversations.values()].filter((session) => session.state !== "ended");
|
|
135
|
-
if (!interrupted.length)
|
|
136
|
-
return;
|
|
137
182
|
for (const session of interrupted)
|
|
138
183
|
this.interruptedPrivateConversations.add(session.id);
|
|
139
184
|
for (const handle of this.cryptoHandles.values())
|
|
@@ -145,53 +190,61 @@ export class SessionRuntime {
|
|
|
145
190
|
this.connectingTransfers.clear();
|
|
146
191
|
this.transferKeys.clear();
|
|
147
192
|
this.transferProgress.clear();
|
|
193
|
+
this.watchedCommunityCallIds.clear();
|
|
148
194
|
this.set({
|
|
149
195
|
...this.current,
|
|
196
|
+
currentCallId: undefined,
|
|
197
|
+
callConnection: "idle",
|
|
198
|
+
calls: new Map(),
|
|
150
199
|
privateConversations: new Map([...this.current.privateConversations].filter(([, session]) => session.state === "ended")),
|
|
151
200
|
transfers: new Map(),
|
|
152
201
|
privateMessages: new Map(),
|
|
153
202
|
});
|
|
154
203
|
}
|
|
155
204
|
acceptCall(callId) {
|
|
205
|
+
return this.enqueue(() => this.acceptCallNow(callId));
|
|
206
|
+
}
|
|
207
|
+
beginCall(...args) {
|
|
156
208
|
return this.enqueue(async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
await this.
|
|
162
|
-
|
|
163
|
-
if (!this.media)
|
|
164
|
-
return;
|
|
165
|
-
const call = this.current.calls.get(callId);
|
|
166
|
-
if (!call)
|
|
167
|
-
throw new Error("The call is not available");
|
|
168
|
-
try {
|
|
169
|
-
const admission = await this.calls.admit(callId);
|
|
170
|
-
await this.media.connect({
|
|
171
|
-
endpoint: this.endpoint,
|
|
172
|
-
localUserId: this.ownUserId,
|
|
173
|
-
call,
|
|
174
|
-
admission,
|
|
175
|
-
});
|
|
176
|
-
await this.calls.mediaConnected(callId);
|
|
177
|
-
}
|
|
178
|
-
catch (reason) {
|
|
179
|
-
await this.media.disconnect(callId).catch(() => undefined);
|
|
180
|
-
await this.calls.leave(callId).catch(() => undefined);
|
|
181
|
-
throw reason;
|
|
209
|
+
if (this.callView().current)
|
|
210
|
+
throw new Error("End the current call before starting another");
|
|
211
|
+
const existing = this.findJoinableTarget(args[0]);
|
|
212
|
+
if (existing) {
|
|
213
|
+
await this.acceptCallNow(existing.id);
|
|
214
|
+
return existing;
|
|
182
215
|
}
|
|
216
|
+
const call = await this.calls.start(...args);
|
|
217
|
+
this.upsertCall(call);
|
|
218
|
+
this.selectCurrentCall(call.id);
|
|
219
|
+
this.setCallConnection("ringing", call);
|
|
220
|
+
await this.acceptCallNow(call.id);
|
|
221
|
+
return call;
|
|
183
222
|
});
|
|
184
223
|
}
|
|
224
|
+
declineCall(callId) {
|
|
225
|
+
return this.enqueue(() => this.calls.decline(callId));
|
|
226
|
+
}
|
|
185
227
|
leaveCall(callId) {
|
|
186
228
|
return this.enqueue(async () => {
|
|
229
|
+
this.mediaGeneration += 1;
|
|
230
|
+
this.setCallConnection("disconnecting");
|
|
187
231
|
await this.media?.disconnect(callId);
|
|
188
232
|
await this.calls.leave(callId);
|
|
233
|
+
if (this.current.currentCallId === callId)
|
|
234
|
+
this.clearCurrentCall();
|
|
235
|
+
this.mediaControls?.resetSession();
|
|
189
236
|
});
|
|
190
237
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
238
|
+
startCall(...args) {
|
|
239
|
+
return this.enqueue(async () => {
|
|
240
|
+
if (this.callView().current)
|
|
241
|
+
throw new Error("End the current call before starting another");
|
|
242
|
+
const call = await this.calls.start(...args);
|
|
243
|
+
this.upsertCall(call);
|
|
244
|
+
this.selectCurrentCall(call.id);
|
|
245
|
+
this.setCallConnection("ringing", call);
|
|
246
|
+
return call;
|
|
247
|
+
});
|
|
195
248
|
}
|
|
196
249
|
async startPrivateConversation(peerId) {
|
|
197
250
|
if (!this.crypto)
|
|
@@ -264,9 +317,11 @@ export class SessionRuntime {
|
|
|
264
317
|
}
|
|
265
318
|
acceptPrivateConversation(id, encryptedAnswer) {
|
|
266
319
|
return this.enqueue(async () => {
|
|
267
|
-
const activeCall =
|
|
320
|
+
const activeCall = this.callView().current;
|
|
268
321
|
if (activeCall)
|
|
269
322
|
await this.calls.leave(activeCall.id);
|
|
323
|
+
if (activeCall)
|
|
324
|
+
this.clearCurrentCall();
|
|
270
325
|
let answer = encryptedAnswer;
|
|
271
326
|
if (this.crypto) {
|
|
272
327
|
const session = this.current.privateConversations.get(id);
|
|
@@ -319,7 +374,7 @@ export class SessionRuntime {
|
|
|
319
374
|
}
|
|
320
375
|
receiveRealtime(opcode, payload, timestamp) {
|
|
321
376
|
if (opcode === 400)
|
|
322
|
-
this.
|
|
377
|
+
this.upsertCall(decodeCall(payload));
|
|
323
378
|
else if (opcode === 401)
|
|
324
379
|
this.updateCallParticipant(payload);
|
|
325
380
|
else if (opcode === 420)
|
|
@@ -444,6 +499,16 @@ export class SessionRuntime {
|
|
|
444
499
|
this.set({ ...this.current, privateMessages });
|
|
445
500
|
}
|
|
446
501
|
receiveControl(frame) {
|
|
502
|
+
if (frame.opcode === 45) {
|
|
503
|
+
this.replaceCommunityCalls(frame.payload);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (frame.opcode === 46) {
|
|
507
|
+
const call = decodeCall(required(mapValue(frame.payload), 1));
|
|
508
|
+
this.watchedCommunityCallIds.add(call.id);
|
|
509
|
+
this.upsertCall(call);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
447
512
|
if ([400, 401, 420, 422].includes(frame.opcode)) {
|
|
448
513
|
this.receiveRealtime(frame.opcode, frame.payload, new Date());
|
|
449
514
|
return;
|
|
@@ -469,12 +534,25 @@ export class SessionRuntime {
|
|
|
469
534
|
if (conversation.encryptedInvitation)
|
|
470
535
|
this.privateInvites.set(conversation.id, conversation.encryptedInvitation);
|
|
471
536
|
}
|
|
537
|
+
const calls = keyed(arrayValue(required(map, 2)).map(decodeCall), (item) => item.id);
|
|
538
|
+
const previousCalls = this.current.calls;
|
|
539
|
+
const previousCurrent = this.callView().current;
|
|
540
|
+
const currentCallId = selectCurrentCallId(calls, this.ownUserId, this.current.currentCallId);
|
|
472
541
|
this.set({
|
|
473
|
-
|
|
542
|
+
callConnection: currentCallId ? this.current.callConnection : "idle",
|
|
543
|
+
currentCallId,
|
|
544
|
+
calls,
|
|
474
545
|
privateConversations: keyed(availablePrivateConversations, (item) => item.id),
|
|
475
546
|
transfers: this.current.transfers,
|
|
476
547
|
privateMessages: this.current.privateMessages,
|
|
477
548
|
});
|
|
549
|
+
for (const call of calls.values())
|
|
550
|
+
this.emitCallUpdate(call, previousCalls.get(call.id));
|
|
551
|
+
this.options.onCallUsers?.([...calls.values()].flatMap((call) => call.participants.map((item) => item.userId)));
|
|
552
|
+
const nextCurrent = this.callView().current;
|
|
553
|
+
this.emitCurrentChanged(previousCurrent, nextCurrent);
|
|
554
|
+
if (previousCurrent && !nextCurrent)
|
|
555
|
+
this.teardownLocalMedia(previousCurrent.id);
|
|
478
556
|
}
|
|
479
557
|
updateCallParticipant(payload) {
|
|
480
558
|
const map = mapValue(payload);
|
|
@@ -498,7 +576,16 @@ export class SessionRuntime {
|
|
|
498
576
|
...call.participants.filter((item) => item.userId !== userId),
|
|
499
577
|
{ userId, state },
|
|
500
578
|
];
|
|
501
|
-
|
|
579
|
+
const updated = { ...call, participants };
|
|
580
|
+
this.upsertCall(updated);
|
|
581
|
+
this.emitCallEvent({
|
|
582
|
+
type: "participant-state-changed",
|
|
583
|
+
call: updated,
|
|
584
|
+
userId,
|
|
585
|
+
state,
|
|
586
|
+
previousState: call.participants.find((item) => item.userId === userId)
|
|
587
|
+
?.state,
|
|
588
|
+
});
|
|
502
589
|
}
|
|
503
590
|
connectTransfer(transfer) {
|
|
504
591
|
if (!this.transferAdapter)
|
|
@@ -565,6 +652,146 @@ export class SessionRuntime {
|
|
|
565
652
|
this.upsert("privateConversations", { ...session, state: "ended" }, (item) => item.id);
|
|
566
653
|
});
|
|
567
654
|
}
|
|
655
|
+
async acceptCallNow(callId) {
|
|
656
|
+
const activePrivate = [...this.current.privateConversations.values()].find((item) => item.state !== "ended");
|
|
657
|
+
if (activePrivate)
|
|
658
|
+
await this.privateConversations.end(activePrivate.id);
|
|
659
|
+
const previous = this.callView().current;
|
|
660
|
+
if (previous && previous.id !== callId) {
|
|
661
|
+
this.mediaGeneration += 1;
|
|
662
|
+
this.setCallConnection("replacing", previous);
|
|
663
|
+
await this.media?.disconnect(previous.id);
|
|
664
|
+
await this.calls.leave(previous.id);
|
|
665
|
+
}
|
|
666
|
+
const call = this.current.calls.get(callId);
|
|
667
|
+
if (!call)
|
|
668
|
+
throw new Error("The call is not available");
|
|
669
|
+
this.selectCurrentCall(callId);
|
|
670
|
+
this.setCallConnection("connecting", call);
|
|
671
|
+
try {
|
|
672
|
+
await this.calls.join(callId);
|
|
673
|
+
if (!this.media) {
|
|
674
|
+
this.setCallConnection("active", call);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const admission = await this.calls.admit(callId);
|
|
678
|
+
await this.media.connect({
|
|
679
|
+
endpoint: this.endpoint,
|
|
680
|
+
localUserId: this.ownUserId,
|
|
681
|
+
call,
|
|
682
|
+
admission,
|
|
683
|
+
});
|
|
684
|
+
await this.calls.mediaConnected(callId);
|
|
685
|
+
this.setCallConnection("active", call);
|
|
686
|
+
}
|
|
687
|
+
catch (reason) {
|
|
688
|
+
this.mediaGeneration += 1;
|
|
689
|
+
await this.media?.disconnect(callId).catch(() => undefined);
|
|
690
|
+
if (call.target.kind !== "user" && staleCallError(reason)) {
|
|
691
|
+
this.discardCall(callId);
|
|
692
|
+
this.mediaControls?.resetSession();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
await this.calls.leave(callId).catch(() => undefined);
|
|
696
|
+
if (this.current.currentCallId === callId)
|
|
697
|
+
this.clearCurrentCall();
|
|
698
|
+
this.mediaControls?.resetSession();
|
|
699
|
+
throw reason;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
findJoinableTarget(target) {
|
|
703
|
+
return [...this.callView().joinable.values()].find((call) => sameCallTarget(call.target, target));
|
|
704
|
+
}
|
|
705
|
+
discardCall(callId) {
|
|
706
|
+
const calls = new Map(this.current.calls);
|
|
707
|
+
const previous = calls.get(callId);
|
|
708
|
+
const wasCurrent = this.current.currentCallId === callId;
|
|
709
|
+
calls.delete(callId);
|
|
710
|
+
this.set({
|
|
711
|
+
...this.current,
|
|
712
|
+
calls,
|
|
713
|
+
currentCallId: this.current.currentCallId === callId
|
|
714
|
+
? undefined
|
|
715
|
+
: this.current.currentCallId,
|
|
716
|
+
callConnection: this.current.currentCallId === callId
|
|
717
|
+
? "idle"
|
|
718
|
+
: this.current.callConnection,
|
|
719
|
+
});
|
|
720
|
+
if (previous && wasCurrent)
|
|
721
|
+
this.emitCurrentChanged(previous, undefined);
|
|
722
|
+
}
|
|
723
|
+
receiveMediaEvent(event) {
|
|
724
|
+
if (event.callId !== this.current.currentCallId)
|
|
725
|
+
return;
|
|
726
|
+
if (event.type === "room-ended") {
|
|
727
|
+
void this.terminateMediaCall(event.callId, "room-ended");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (event.type !== "disconnected")
|
|
731
|
+
return;
|
|
732
|
+
if (event.reason) {
|
|
733
|
+
void this.terminateMediaCall(event.callId, event.reason);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const generation = ++this.mediaGeneration;
|
|
737
|
+
this.setCallConnection("reconnecting", this.current.calls.get(event.callId));
|
|
738
|
+
void this.reconnectMedia(event.callId, generation);
|
|
739
|
+
}
|
|
740
|
+
async reconnectMedia(callId, generation) {
|
|
741
|
+
const attempts = this.options.reconnectAttempts ?? 6;
|
|
742
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
743
|
+
if (!this.reconnectCurrent(callId, generation))
|
|
744
|
+
return;
|
|
745
|
+
if (attempt > 0)
|
|
746
|
+
await (this.options.reconnectDelay ?? reconnectDelay)(attempt);
|
|
747
|
+
if (!this.reconnectCurrent(callId, generation))
|
|
748
|
+
return;
|
|
749
|
+
const call = this.current.calls.get(callId);
|
|
750
|
+
if (!call || !this.media)
|
|
751
|
+
return;
|
|
752
|
+
try {
|
|
753
|
+
const admission = await this.calls.admit(callId);
|
|
754
|
+
if (!this.reconnectCurrent(callId, generation))
|
|
755
|
+
return;
|
|
756
|
+
await this.media.connect({
|
|
757
|
+
endpoint: this.endpoint,
|
|
758
|
+
localUserId: this.ownUserId,
|
|
759
|
+
call,
|
|
760
|
+
admission,
|
|
761
|
+
});
|
|
762
|
+
if (!this.reconnectCurrent(callId, generation)) {
|
|
763
|
+
await this.media.disconnect(callId).catch(() => undefined);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await this.calls.mediaConnected(callId);
|
|
767
|
+
this.setCallConnection("active", call);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
await this.media.disconnect(callId).catch(() => undefined);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (this.reconnectCurrent(callId, generation))
|
|
775
|
+
await this.terminateMediaCall(callId, "reconnect-failed");
|
|
776
|
+
}
|
|
777
|
+
reconnectCurrent(callId, generation) {
|
|
778
|
+
return (generation === this.mediaGeneration &&
|
|
779
|
+
this.current.currentCallId === callId);
|
|
780
|
+
}
|
|
781
|
+
async terminateMediaCall(callId, reason) {
|
|
782
|
+
if (this.current.currentCallId !== callId)
|
|
783
|
+
return;
|
|
784
|
+
this.mediaGeneration += 1;
|
|
785
|
+
this.setCallConnection("disconnecting", this.current.calls.get(callId), reason);
|
|
786
|
+
await this.media?.disconnect(callId).catch(() => undefined);
|
|
787
|
+
await this.calls.leave(callId).catch(() => undefined);
|
|
788
|
+
if (this.current.currentCallId === callId)
|
|
789
|
+
this.clearCurrentCall(reason);
|
|
790
|
+
this.mediaControls?.resetSession();
|
|
791
|
+
const message = terminalCallMessage(reason);
|
|
792
|
+
if (message)
|
|
793
|
+
this.options.onError?.(new Error(message));
|
|
794
|
+
}
|
|
568
795
|
requireTransfer(id) {
|
|
569
796
|
const transfer = this.current.transfers.get(id);
|
|
570
797
|
if (!transfer)
|
|
@@ -588,6 +815,115 @@ export class SessionRuntime {
|
|
|
588
815
|
values.set(key(value), value);
|
|
589
816
|
this.set({ ...this.current, [name]: values });
|
|
590
817
|
}
|
|
818
|
+
upsertCall(call) {
|
|
819
|
+
const previous = this.current.calls.get(call.id);
|
|
820
|
+
const previousCurrent = this.callView().current;
|
|
821
|
+
const calls = new Map(this.current.calls);
|
|
822
|
+
calls.set(call.id, call);
|
|
823
|
+
const currentCallId = selectCurrentCallId(calls, this.ownUserId, this.current.currentCallId);
|
|
824
|
+
this.set({ ...this.current, calls, currentCallId });
|
|
825
|
+
this.emitCallUpdate(call, previous);
|
|
826
|
+
const nextCurrent = this.callView().current;
|
|
827
|
+
this.emitCurrentChanged(previousCurrent, nextCurrent);
|
|
828
|
+
if (previousCurrent && !nextCurrent)
|
|
829
|
+
this.teardownLocalMedia(previousCurrent.id);
|
|
830
|
+
this.connectAnsweredDirectCall(call);
|
|
831
|
+
this.options.onCallUsers?.(call.participants.map((item) => item.userId));
|
|
832
|
+
}
|
|
833
|
+
replaceCommunityCalls(payload) {
|
|
834
|
+
const map = mapValue(payload);
|
|
835
|
+
const incoming = arrayValue(required(map, 1)).map(decodeCall);
|
|
836
|
+
const incomingIds = new Set(incoming.map((call) => call.id));
|
|
837
|
+
const calls = new Map(this.current.calls);
|
|
838
|
+
for (const callId of this.watchedCommunityCallIds) {
|
|
839
|
+
if (incomingIds.has(callId))
|
|
840
|
+
continue;
|
|
841
|
+
const call = calls.get(callId);
|
|
842
|
+
if (call && !locallySelectable(call, this.ownUserId))
|
|
843
|
+
calls.delete(callId);
|
|
844
|
+
}
|
|
845
|
+
for (const call of incoming)
|
|
846
|
+
calls.set(call.id, call);
|
|
847
|
+
this.watchedCommunityCallIds = incomingIds;
|
|
848
|
+
this.set({ ...this.current, calls });
|
|
849
|
+
this.options.onCallUsers?.(incoming.flatMap((call) => call.participants.map((item) => item.userId)));
|
|
850
|
+
}
|
|
851
|
+
connectAnsweredDirectCall(call) {
|
|
852
|
+
const answered = call.participants.some((participant) => participant.userId !== this.ownUserId && participant.state === "active");
|
|
853
|
+
if (call.target.kind !== "user" ||
|
|
854
|
+
call.originatorId !== this.ownUserId ||
|
|
855
|
+
this.current.currentCallId !== call.id ||
|
|
856
|
+
this.current.callConnection !== "ringing" ||
|
|
857
|
+
!answered ||
|
|
858
|
+
this.connectingAnsweredCalls.has(call.id))
|
|
859
|
+
return;
|
|
860
|
+
this.connectingAnsweredCalls.add(call.id);
|
|
861
|
+
void this.acceptCall(call.id).finally(() => {
|
|
862
|
+
this.connectingAnsweredCalls.delete(call.id);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
selectCurrentCall(callId) {
|
|
866
|
+
const previous = this.callView().current;
|
|
867
|
+
const currentCallId = selectCurrentCallId(this.current.calls, this.ownUserId, callId);
|
|
868
|
+
if (currentCallId === this.current.currentCallId)
|
|
869
|
+
return;
|
|
870
|
+
this.set({ ...this.current, currentCallId });
|
|
871
|
+
this.emitCurrentChanged(previous, this.callView().current);
|
|
872
|
+
}
|
|
873
|
+
clearCurrentCall(reason) {
|
|
874
|
+
const previous = this.callView().current;
|
|
875
|
+
const previousState = this.current.callConnection;
|
|
876
|
+
if (!this.current.currentCallId)
|
|
877
|
+
return;
|
|
878
|
+
this.set({
|
|
879
|
+
...this.current,
|
|
880
|
+
currentCallId: undefined,
|
|
881
|
+
callConnection: "idle",
|
|
882
|
+
});
|
|
883
|
+
this.mediaControls?.resetSession();
|
|
884
|
+
this.emitCurrentChanged(previous, undefined);
|
|
885
|
+
this.emitCallEvent({
|
|
886
|
+
type: "connection-state-changed",
|
|
887
|
+
state: "idle",
|
|
888
|
+
previousState,
|
|
889
|
+
call: previous,
|
|
890
|
+
reason,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
setCallConnection(state, call = this.callView().current, reason) {
|
|
894
|
+
const previousState = this.current.callConnection;
|
|
895
|
+
if (state === previousState)
|
|
896
|
+
return;
|
|
897
|
+
this.set({ ...this.current, callConnection: state });
|
|
898
|
+
this.emitCallEvent({
|
|
899
|
+
type: "connection-state-changed",
|
|
900
|
+
state,
|
|
901
|
+
previousState,
|
|
902
|
+
call,
|
|
903
|
+
reason,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
teardownLocalMedia(callId) {
|
|
907
|
+
this.mediaGeneration += 1;
|
|
908
|
+
void this.media?.disconnect(callId);
|
|
909
|
+
this.mediaControls?.resetSession();
|
|
910
|
+
this.setCallConnection("idle");
|
|
911
|
+
}
|
|
912
|
+
emitCallUpdate(call, previous) {
|
|
913
|
+
if (previous === call)
|
|
914
|
+
return;
|
|
915
|
+
this.emitCallEvent({ type: "call-updated", call, previous });
|
|
916
|
+
if (previous && openCall(previous) && !openCall(call))
|
|
917
|
+
this.emitCallEvent({ type: "call-ended", call });
|
|
918
|
+
}
|
|
919
|
+
emitCurrentChanged(previous, call) {
|
|
920
|
+
if (previous?.id === call?.id)
|
|
921
|
+
return;
|
|
922
|
+
this.emitCallEvent({ type: "current-changed", call, previous });
|
|
923
|
+
}
|
|
924
|
+
emitCallEvent(event) {
|
|
925
|
+
this.callEventListeners.forEach((listener) => listener(event));
|
|
926
|
+
}
|
|
591
927
|
set(state) {
|
|
592
928
|
this.current = state;
|
|
593
929
|
this.listeners.forEach((listener) => listener());
|
|
@@ -601,7 +937,63 @@ export class SessionRuntime {
|
|
|
601
937
|
function keyed(values, key) {
|
|
602
938
|
return new Map(values.map((value) => [key(value), value]));
|
|
603
939
|
}
|
|
940
|
+
function openCall(call) {
|
|
941
|
+
return !["ended", "declined", "expired"].includes(call.state);
|
|
942
|
+
}
|
|
943
|
+
function callPhase(call) {
|
|
944
|
+
if (call.target.kind === "user")
|
|
945
|
+
return call.state === "ringing" ? "proposal" : "session";
|
|
946
|
+
if (call.target.kind === "channel")
|
|
947
|
+
return "session";
|
|
948
|
+
return call.establishedAt ? "session" : "proposal";
|
|
949
|
+
}
|
|
950
|
+
function sameCallTarget(left, right) {
|
|
951
|
+
if (left.kind !== right.kind)
|
|
952
|
+
return false;
|
|
953
|
+
if (left.kind === "user" && right.kind === "user")
|
|
954
|
+
return left.userId === right.userId;
|
|
955
|
+
if (left.kind === "conversation" && right.kind === "conversation")
|
|
956
|
+
return left.conversationId === right.conversationId;
|
|
957
|
+
return (left.kind === "channel" &&
|
|
958
|
+
right.kind === "channel" &&
|
|
959
|
+
left.channelId === right.channelId);
|
|
960
|
+
}
|
|
961
|
+
function staleCallError(reason) {
|
|
962
|
+
return reason instanceof SbpProtocolError && [4, 5].includes(reason.code);
|
|
963
|
+
}
|
|
964
|
+
function ownParticipant(call, ownUserId) {
|
|
965
|
+
return call.participants.find((participant) => participant.userId === ownUserId);
|
|
966
|
+
}
|
|
967
|
+
function locallyParticipating(call, ownUserId) {
|
|
968
|
+
const state = ownParticipant(call, ownUserId)?.state;
|
|
969
|
+
return openCall(call) && (state === "joining" || state === "active");
|
|
970
|
+
}
|
|
971
|
+
function locallySelectable(call, ownUserId) {
|
|
972
|
+
const state = ownParticipant(call, ownUserId)?.state;
|
|
973
|
+
return (openCall(call) && Boolean(state && state !== "declined" && state !== "left"));
|
|
974
|
+
}
|
|
975
|
+
function selectCurrentCallId(calls, ownUserId, preferred) {
|
|
976
|
+
const preferredCall = preferred ? calls.get(preferred) : undefined;
|
|
977
|
+
if (preferredCall && locallySelectable(preferredCall, ownUserId))
|
|
978
|
+
return preferredCall.id;
|
|
979
|
+
return [...calls.values()].find((call) => locallyParticipating(call, ownUserId))?.id;
|
|
980
|
+
}
|
|
604
981
|
function completeTransferMetadata(transfer) {
|
|
605
982
|
return Boolean(transfer.filename && transfer.mediaType && transfer.sha256);
|
|
606
983
|
}
|
|
984
|
+
function reconnectDelay(attempt) {
|
|
985
|
+
const delay = Math.min(8_000, 1_000 * 2 ** (attempt - 1));
|
|
986
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
987
|
+
}
|
|
988
|
+
function terminalCallMessage(reason) {
|
|
989
|
+
if (reason === "authorization_revoked")
|
|
990
|
+
return "You no longer have access to this call.";
|
|
991
|
+
if (reason === "policy_violation")
|
|
992
|
+
return "The call was ended because of invalid media activity.";
|
|
993
|
+
if (reason === "replaced")
|
|
994
|
+
return "This call was opened in another Sideband window.";
|
|
995
|
+
if (reason === "reconnect-failed")
|
|
996
|
+
return "The call could not be reconnected.";
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
607
999
|
//# sourceMappingURL=session-runtime.js.map
|