@abraca/dabra 1.0.2 → 1.0.3
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/abracadabra-provider.cjs +1197 -10
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +1188 -11
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +361 -1
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +18 -0
- package/src/AbracadabraWS.ts +3 -1
- package/src/index.ts +1 -0
- package/src/webrtc/AbracadabraWebRTC.ts +540 -0
- package/src/webrtc/DataChannelRouter.ts +110 -0
- package/src/webrtc/FileTransferChannel.ts +359 -0
- package/src/webrtc/PeerConnection.ts +133 -0
- package/src/webrtc/SignalingSocket.ts +366 -0
- package/src/webrtc/YjsDataChannel.ts +195 -0
- package/src/webrtc/index.ts +20 -0
- package/src/webrtc/types.ts +159 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import EventEmitter from "../EventEmitter.ts";
|
|
2
|
+
import { DataChannelRouter } from "./DataChannelRouter.ts";
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_NAMES,
|
|
5
|
+
FILE_MSG,
|
|
6
|
+
DEFAULT_FILE_CHUNK_SIZE,
|
|
7
|
+
TRANSFER_ID_BYTES,
|
|
8
|
+
SHA256_BYTES,
|
|
9
|
+
type FileTransferMeta,
|
|
10
|
+
type FileTransferStatus,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle for tracking a file transfer in progress.
|
|
15
|
+
*/
|
|
16
|
+
export class FileTransferHandle extends EventEmitter {
|
|
17
|
+
public readonly transferId: string;
|
|
18
|
+
public progress = 0;
|
|
19
|
+
public status: FileTransferStatus = "pending";
|
|
20
|
+
|
|
21
|
+
private abortController = new AbortController();
|
|
22
|
+
|
|
23
|
+
constructor(transferId: string) {
|
|
24
|
+
super();
|
|
25
|
+
this.transferId = transferId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cancel(): void {
|
|
29
|
+
this.status = "cancelled";
|
|
30
|
+
this.abortController.abort();
|
|
31
|
+
this.emit("cancelled");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get signal(): AbortSignal {
|
|
35
|
+
return this.abortController.signal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
_setProgress(p: number): void {
|
|
40
|
+
this.progress = p;
|
|
41
|
+
this.emit("progress", p);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @internal */
|
|
45
|
+
_setStatus(s: FileTransferStatus): void {
|
|
46
|
+
this.status = s;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ReceiveState {
|
|
51
|
+
meta: FileTransferMeta;
|
|
52
|
+
chunks: (Uint8Array | null)[];
|
|
53
|
+
receivedCount: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Chunked binary file transfer over a dedicated WebRTC data channel.
|
|
58
|
+
*/
|
|
59
|
+
export class FileTransferChannel extends EventEmitter {
|
|
60
|
+
private receives = new Map<string, ReceiveState>();
|
|
61
|
+
private chunkSize: number;
|
|
62
|
+
private channelMessageHandler: ((data: { name: string; data: any }) => void) | null = null;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private readonly router: DataChannelRouter,
|
|
66
|
+
chunkSize?: number,
|
|
67
|
+
) {
|
|
68
|
+
super();
|
|
69
|
+
this.chunkSize = chunkSize ?? DEFAULT_FILE_CHUNK_SIZE;
|
|
70
|
+
|
|
71
|
+
this.channelMessageHandler = ({ name, data }: { name: string; data: any }) => {
|
|
72
|
+
if (name === CHANNEL_NAMES.FILE_TRANSFER) {
|
|
73
|
+
this.handleMessage(data);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
this.router.on("channelMessage", this.channelMessageHandler);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Send a file to a peer. Returns a handle for tracking progress. */
|
|
80
|
+
async send(
|
|
81
|
+
file: File | Blob,
|
|
82
|
+
filename: string,
|
|
83
|
+
): Promise<FileTransferHandle> {
|
|
84
|
+
const transferId = generateTransferId();
|
|
85
|
+
const handle = new FileTransferHandle(transferId);
|
|
86
|
+
const transferIdBytes = hexToBytes(transferId);
|
|
87
|
+
|
|
88
|
+
const totalSize = file.size;
|
|
89
|
+
const totalChunks = Math.ceil(totalSize / this.chunkSize);
|
|
90
|
+
const mimeType =
|
|
91
|
+
file instanceof File ? file.type : "application/octet-stream";
|
|
92
|
+
|
|
93
|
+
const meta: FileTransferMeta = {
|
|
94
|
+
transferId,
|
|
95
|
+
filename,
|
|
96
|
+
mimeType,
|
|
97
|
+
totalSize,
|
|
98
|
+
chunkSize: this.chunkSize,
|
|
99
|
+
totalChunks,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const channel = this.router.getChannel(CHANNEL_NAMES.FILE_TRANSFER);
|
|
103
|
+
if (!channel || channel.readyState !== "open") {
|
|
104
|
+
handle._setStatus("error");
|
|
105
|
+
handle.emit("error", new Error("File transfer channel not open"));
|
|
106
|
+
return handle;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Send start message.
|
|
110
|
+
const startMsg = new Uint8Array(
|
|
111
|
+
1 + new TextEncoder().encode(JSON.stringify(meta)).length,
|
|
112
|
+
);
|
|
113
|
+
startMsg[0] = FILE_MSG.START;
|
|
114
|
+
startMsg.set(new TextEncoder().encode(JSON.stringify(meta)), 1);
|
|
115
|
+
channel.send(startMsg);
|
|
116
|
+
|
|
117
|
+
handle._setStatus("sending");
|
|
118
|
+
|
|
119
|
+
// Read and send chunks.
|
|
120
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
121
|
+
const fileBytes = new Uint8Array(arrayBuffer);
|
|
122
|
+
|
|
123
|
+
// SHA-256 for integrity check.
|
|
124
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", fileBytes);
|
|
125
|
+
const hashBytes = new Uint8Array(hashBuffer);
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
128
|
+
if (handle.signal.aborted) {
|
|
129
|
+
// Send cancel.
|
|
130
|
+
const cancelMsg = new Uint8Array(1 + TRANSFER_ID_BYTES);
|
|
131
|
+
cancelMsg[0] = FILE_MSG.CANCEL;
|
|
132
|
+
cancelMsg.set(transferIdBytes, 1);
|
|
133
|
+
channel.send(cancelMsg);
|
|
134
|
+
return handle;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const offset = i * this.chunkSize;
|
|
138
|
+
const chunk = fileBytes.slice(
|
|
139
|
+
offset,
|
|
140
|
+
Math.min(offset + this.chunkSize, totalSize),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// [type(1)] [transferId(16)] [chunkIndex(4)] [chunk data]
|
|
144
|
+
const msg = new Uint8Array(
|
|
145
|
+
1 + TRANSFER_ID_BYTES + 4 + chunk.length,
|
|
146
|
+
);
|
|
147
|
+
msg[0] = FILE_MSG.CHUNK;
|
|
148
|
+
msg.set(transferIdBytes, 1);
|
|
149
|
+
new DataView(msg.buffer).setUint32(
|
|
150
|
+
1 + TRANSFER_ID_BYTES,
|
|
151
|
+
i,
|
|
152
|
+
false,
|
|
153
|
+
);
|
|
154
|
+
msg.set(chunk, 1 + TRANSFER_ID_BYTES + 4);
|
|
155
|
+
|
|
156
|
+
// Wait for bufferedAmount to drop before sending more chunks
|
|
157
|
+
// to avoid overwhelming the data channel.
|
|
158
|
+
while (channel.bufferedAmount > this.chunkSize * 4) {
|
|
159
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
160
|
+
if (handle.signal.aborted) return handle;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
channel.send(msg);
|
|
164
|
+
handle._setProgress((i + 1) / totalChunks);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Send complete message.
|
|
168
|
+
const completeMsg = new Uint8Array(
|
|
169
|
+
1 + TRANSFER_ID_BYTES + SHA256_BYTES,
|
|
170
|
+
);
|
|
171
|
+
completeMsg[0] = FILE_MSG.COMPLETE;
|
|
172
|
+
completeMsg.set(transferIdBytes, 1);
|
|
173
|
+
completeMsg.set(hashBytes, 1 + TRANSFER_ID_BYTES);
|
|
174
|
+
channel.send(completeMsg);
|
|
175
|
+
|
|
176
|
+
handle._setStatus("complete");
|
|
177
|
+
handle.emit("complete");
|
|
178
|
+
|
|
179
|
+
return handle;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private handleMessage(data: ArrayBuffer | Uint8Array): void {
|
|
183
|
+
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
184
|
+
if (buf.length < 1) return;
|
|
185
|
+
|
|
186
|
+
const type = buf[0];
|
|
187
|
+
|
|
188
|
+
switch (type) {
|
|
189
|
+
case FILE_MSG.START:
|
|
190
|
+
this.handleStart(buf);
|
|
191
|
+
break;
|
|
192
|
+
case FILE_MSG.CHUNK:
|
|
193
|
+
this.handleChunk(buf);
|
|
194
|
+
break;
|
|
195
|
+
case FILE_MSG.COMPLETE:
|
|
196
|
+
this.handleComplete(buf);
|
|
197
|
+
break;
|
|
198
|
+
case FILE_MSG.CANCEL:
|
|
199
|
+
this.handleCancel(buf);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private handleStart(buf: Uint8Array): void {
|
|
205
|
+
const json = new TextDecoder().decode(buf.slice(1));
|
|
206
|
+
let meta: FileTransferMeta;
|
|
207
|
+
try {
|
|
208
|
+
meta = JSON.parse(json);
|
|
209
|
+
} catch {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.receives.set(meta.transferId, {
|
|
214
|
+
meta,
|
|
215
|
+
chunks: new Array(meta.totalChunks).fill(null),
|
|
216
|
+
receivedCount: 0,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.emit("receiveStart", meta);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private handleChunk(buf: Uint8Array): void {
|
|
223
|
+
if (buf.length < 1 + TRANSFER_ID_BYTES + 4) return;
|
|
224
|
+
|
|
225
|
+
const transferId = bytesToHex(
|
|
226
|
+
buf.slice(1, 1 + TRANSFER_ID_BYTES),
|
|
227
|
+
);
|
|
228
|
+
const chunkIndex = new DataView(buf.buffer, buf.byteOffset).getUint32(
|
|
229
|
+
1 + TRANSFER_ID_BYTES,
|
|
230
|
+
false,
|
|
231
|
+
);
|
|
232
|
+
const chunkData = buf.slice(1 + TRANSFER_ID_BYTES + 4);
|
|
233
|
+
|
|
234
|
+
const state = this.receives.get(transferId);
|
|
235
|
+
if (!state) return;
|
|
236
|
+
|
|
237
|
+
if (chunkIndex < state.chunks.length && !state.chunks[chunkIndex]) {
|
|
238
|
+
state.chunks[chunkIndex] = chunkData;
|
|
239
|
+
state.receivedCount++;
|
|
240
|
+
|
|
241
|
+
const progress = state.receivedCount / state.meta.totalChunks;
|
|
242
|
+
this.emit("receiveProgress", {
|
|
243
|
+
transferId,
|
|
244
|
+
received: state.receivedCount,
|
|
245
|
+
total: state.meta.totalChunks,
|
|
246
|
+
progress,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async handleComplete(buf: Uint8Array): Promise<void> {
|
|
252
|
+
if (buf.length < 1 + TRANSFER_ID_BYTES + SHA256_BYTES) return;
|
|
253
|
+
|
|
254
|
+
const transferId = bytesToHex(
|
|
255
|
+
buf.slice(1, 1 + TRANSFER_ID_BYTES),
|
|
256
|
+
);
|
|
257
|
+
const expectedHash = buf.slice(1 + TRANSFER_ID_BYTES, 1 + TRANSFER_ID_BYTES + SHA256_BYTES);
|
|
258
|
+
|
|
259
|
+
const state = this.receives.get(transferId);
|
|
260
|
+
if (!state) return;
|
|
261
|
+
|
|
262
|
+
// Assemble the file.
|
|
263
|
+
const totalSize = state.meta.totalSize;
|
|
264
|
+
const assembled = new Uint8Array(totalSize);
|
|
265
|
+
let offset = 0;
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < state.chunks.length; i++) {
|
|
268
|
+
const chunk = state.chunks[i];
|
|
269
|
+
if (!chunk) {
|
|
270
|
+
this.emit("receiveError", {
|
|
271
|
+
transferId,
|
|
272
|
+
error: `Missing chunk ${i}`,
|
|
273
|
+
});
|
|
274
|
+
this.receives.delete(transferId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
assembled.set(chunk, offset);
|
|
278
|
+
offset += chunk.length;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Verify SHA-256 integrity.
|
|
282
|
+
const actualHashBuffer = await crypto.subtle.digest(
|
|
283
|
+
"SHA-256",
|
|
284
|
+
assembled,
|
|
285
|
+
);
|
|
286
|
+
const actualHash = new Uint8Array(actualHashBuffer);
|
|
287
|
+
|
|
288
|
+
if (!constantTimeEqual(expectedHash, actualHash)) {
|
|
289
|
+
this.emit("receiveError", {
|
|
290
|
+
transferId,
|
|
291
|
+
error: "SHA-256 integrity check failed",
|
|
292
|
+
});
|
|
293
|
+
this.receives.delete(transferId);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const blob = new Blob([assembled], { type: state.meta.mimeType });
|
|
298
|
+
|
|
299
|
+
this.emit("receiveComplete", {
|
|
300
|
+
transferId,
|
|
301
|
+
blob,
|
|
302
|
+
filename: state.meta.filename,
|
|
303
|
+
mimeType: state.meta.mimeType,
|
|
304
|
+
size: state.meta.totalSize,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
this.receives.delete(transferId);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private handleCancel(buf: Uint8Array): void {
|
|
311
|
+
if (buf.length < 1 + TRANSFER_ID_BYTES) return;
|
|
312
|
+
|
|
313
|
+
const transferId = bytesToHex(
|
|
314
|
+
buf.slice(1, 1 + TRANSFER_ID_BYTES),
|
|
315
|
+
);
|
|
316
|
+
this.receives.delete(transferId);
|
|
317
|
+
this.emit("receiveCancelled", { transferId });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
destroy(): void {
|
|
321
|
+
if (this.channelMessageHandler) {
|
|
322
|
+
this.router.off("channelMessage", this.channelMessageHandler);
|
|
323
|
+
this.channelMessageHandler = null;
|
|
324
|
+
}
|
|
325
|
+
this.receives.clear();
|
|
326
|
+
this.removeAllListeners();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function generateTransferId(): string {
|
|
333
|
+
const bytes = new Uint8Array(TRANSFER_ID_BYTES);
|
|
334
|
+
crypto.getRandomValues(bytes);
|
|
335
|
+
return bytesToHex(bytes);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
339
|
+
return Array.from(bytes)
|
|
340
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
341
|
+
.join("");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
345
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
346
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
347
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
348
|
+
}
|
|
349
|
+
return bytes;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
353
|
+
if (a.length !== b.length) return false;
|
|
354
|
+
let result = 0;
|
|
355
|
+
for (let i = 0; i < a.length; i++) {
|
|
356
|
+
result |= a[i] ^ b[i];
|
|
357
|
+
}
|
|
358
|
+
return result === 0;
|
|
359
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import EventEmitter from "../EventEmitter.ts";
|
|
2
|
+
import { DataChannelRouter } from "./DataChannelRouter.ts";
|
|
3
|
+
|
|
4
|
+
export class PeerConnection extends EventEmitter {
|
|
5
|
+
public readonly connection: RTCPeerConnection;
|
|
6
|
+
public readonly router: DataChannelRouter;
|
|
7
|
+
public readonly peerId: string;
|
|
8
|
+
private pendingCandidates: RTCIceCandidateInit[] = [];
|
|
9
|
+
private hasRemoteDescription = false;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
peerId: string,
|
|
13
|
+
iceServers: RTCIceServer[],
|
|
14
|
+
) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
this.peerId = peerId;
|
|
18
|
+
|
|
19
|
+
this.connection = new RTCPeerConnection({ iceServers });
|
|
20
|
+
this.router = new DataChannelRouter(this.connection);
|
|
21
|
+
|
|
22
|
+
this.connection.onicecandidate = (event) => {
|
|
23
|
+
if (event.candidate) {
|
|
24
|
+
this.emit("iceCandidate", {
|
|
25
|
+
peerId: this.peerId,
|
|
26
|
+
candidate: JSON.stringify(event.candidate.toJSON()),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.connection.oniceconnectionstatechange = () => {
|
|
32
|
+
const state = this.connection.iceConnectionState;
|
|
33
|
+
this.emit("iceStateChange", { peerId: this.peerId, state });
|
|
34
|
+
|
|
35
|
+
if (state === "failed") {
|
|
36
|
+
this.emit("iceFailed", { peerId: this.peerId });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.connection.onconnectionstatechange = () => {
|
|
41
|
+
this.emit("connectionStateChange", {
|
|
42
|
+
peerId: this.peerId,
|
|
43
|
+
state: this.connection.connectionState,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get connectionState(): RTCPeerConnectionState {
|
|
49
|
+
return this.connection.connectionState;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get iceConnectionState(): RTCIceConnectionState {
|
|
53
|
+
return this.connection.iceConnectionState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Create an SDP offer (initiator side). */
|
|
57
|
+
async createOffer(iceRestart = false): Promise<string> {
|
|
58
|
+
const offer = await this.connection.createOffer(
|
|
59
|
+
iceRestart ? { iceRestart: true } : undefined,
|
|
60
|
+
);
|
|
61
|
+
await this.connection.setLocalDescription(offer);
|
|
62
|
+
return JSON.stringify(this.connection.localDescription?.toJSON());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Set a remote offer and create an answer (receiver side). Returns the SDP answer. */
|
|
66
|
+
async setRemoteOffer(sdp: string): Promise<string> {
|
|
67
|
+
const offer = JSON.parse(sdp) as RTCSessionDescriptionInit;
|
|
68
|
+
await this.connection.setRemoteDescription(
|
|
69
|
+
new RTCSessionDescription(offer),
|
|
70
|
+
);
|
|
71
|
+
this.hasRemoteDescription = true;
|
|
72
|
+
await this.flushPendingCandidates();
|
|
73
|
+
|
|
74
|
+
const answer = await this.connection.createAnswer();
|
|
75
|
+
await this.connection.setLocalDescription(answer);
|
|
76
|
+
return JSON.stringify(this.connection.localDescription?.toJSON());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Set the remote answer (initiator side). */
|
|
80
|
+
async setRemoteAnswer(sdp: string): Promise<void> {
|
|
81
|
+
const answer = JSON.parse(sdp) as RTCSessionDescriptionInit;
|
|
82
|
+
await this.connection.setRemoteDescription(
|
|
83
|
+
new RTCSessionDescription(answer),
|
|
84
|
+
);
|
|
85
|
+
this.hasRemoteDescription = true;
|
|
86
|
+
await this.flushPendingCandidates();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Add a remote ICE candidate. Queues if remote description not yet set. */
|
|
90
|
+
async addIceCandidate(candidateJson: string): Promise<void> {
|
|
91
|
+
const candidate = JSON.parse(candidateJson) as RTCIceCandidateInit;
|
|
92
|
+
|
|
93
|
+
if (!this.hasRemoteDescription) {
|
|
94
|
+
this.pendingCandidates.push(candidate);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this.connection.addIceCandidate(new RTCIceCandidate(candidate));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async flushPendingCandidates(): Promise<void> {
|
|
102
|
+
for (const candidate of this.pendingCandidates) {
|
|
103
|
+
await this.connection.addIceCandidate(
|
|
104
|
+
new RTCIceCandidate(candidate),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
this.pendingCandidates = [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
close(): void {
|
|
111
|
+
this.router.close();
|
|
112
|
+
try {
|
|
113
|
+
this.connection.close();
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
destroy(): void {
|
|
120
|
+
this.router.destroy();
|
|
121
|
+
this.connection.onicecandidate = null;
|
|
122
|
+
this.connection.oniceconnectionstatechange = null;
|
|
123
|
+
this.connection.onconnectionstatechange = null;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
this.connection.close();
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.removeAllListeners();
|
|
132
|
+
}
|
|
133
|
+
}
|