@edge-base/web 0.1.5 → 0.2.1

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.
@@ -0,0 +1,690 @@
1
+ import { createSubscription } from './room.js';
2
+ const DEFAULT_SIGNAL_PREFIX = 'edgebase.media.p2p';
3
+ const DEFAULT_ICE_SERVERS = [
4
+ { urls: 'stun:stun.l.google.com:19302' },
5
+ ];
6
+ const DEFAULT_MEMBER_READY_TIMEOUT_MS = 10_000;
7
+ function buildTrackKey(memberId, trackId) {
8
+ return `${memberId}:${trackId}`;
9
+ }
10
+ function buildExactDeviceConstraint(deviceId) {
11
+ return { deviceId: { exact: deviceId } };
12
+ }
13
+ function normalizeTrackKind(track) {
14
+ if (track.kind === 'audio')
15
+ return 'audio';
16
+ if (track.kind === 'video')
17
+ return 'video';
18
+ return null;
19
+ }
20
+ function serializeDescription(description) {
21
+ return {
22
+ type: description.type,
23
+ sdp: description.sdp ?? undefined,
24
+ };
25
+ }
26
+ function serializeCandidate(candidate) {
27
+ if ('toJSON' in candidate && typeof candidate.toJSON === 'function') {
28
+ return candidate.toJSON();
29
+ }
30
+ return candidate;
31
+ }
32
+ export class RoomP2PMediaTransport {
33
+ room;
34
+ options;
35
+ localTracks = new Map();
36
+ peers = new Map();
37
+ remoteTrackHandlers = [];
38
+ remoteTrackKinds = new Map();
39
+ emittedRemoteTracks = new Set();
40
+ pendingRemoteTracks = new Map();
41
+ subscriptions = [];
42
+ localMemberId = null;
43
+ connected = false;
44
+ constructor(room, options) {
45
+ this.room = room;
46
+ this.options = {
47
+ rtcConfiguration: {
48
+ ...options?.rtcConfiguration,
49
+ iceServers: options?.rtcConfiguration?.iceServers && options.rtcConfiguration.iceServers.length > 0
50
+ ? options.rtcConfiguration.iceServers
51
+ : DEFAULT_ICE_SERVERS,
52
+ },
53
+ peerConnectionFactory: options?.peerConnectionFactory
54
+ ?? ((configuration) => new RTCPeerConnection(configuration)),
55
+ mediaDevices: options?.mediaDevices
56
+ ?? (typeof navigator !== 'undefined' ? navigator.mediaDevices : undefined),
57
+ signalPrefix: options?.signalPrefix ?? DEFAULT_SIGNAL_PREFIX,
58
+ };
59
+ }
60
+ getSessionId() {
61
+ return this.localMemberId;
62
+ }
63
+ getPeerConnection() {
64
+ if (this.peers.size !== 1) {
65
+ return null;
66
+ }
67
+ return this.peers.values().next().value?.pc ?? null;
68
+ }
69
+ async connect(payload) {
70
+ if (this.connected && this.localMemberId) {
71
+ return this.localMemberId;
72
+ }
73
+ if (payload && typeof payload === 'object' && 'sessionDescription' in payload) {
74
+ throw new Error('RoomP2PMediaTransport.connect() does not accept sessionDescription; use room.signals through the built-in transport instead.');
75
+ }
76
+ const currentMember = await this.waitForCurrentMember();
77
+ if (!currentMember) {
78
+ throw new Error('Join the room before connecting a P2P media transport.');
79
+ }
80
+ this.localMemberId = currentMember.memberId;
81
+ this.connected = true;
82
+ this.hydrateRemoteTrackKinds();
83
+ this.attachRoomSubscriptions();
84
+ try {
85
+ for (const member of this.room.members.list()) {
86
+ if (member.memberId !== this.localMemberId) {
87
+ this.ensurePeer(member.memberId);
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ this.rollbackConnectedState();
93
+ throw error;
94
+ }
95
+ return this.localMemberId;
96
+ }
97
+ async waitForCurrentMember(timeoutMs = DEFAULT_MEMBER_READY_TIMEOUT_MS) {
98
+ const startedAt = Date.now();
99
+ while (Date.now() - startedAt < timeoutMs) {
100
+ const member = this.room.members.current();
101
+ if (member) {
102
+ return member;
103
+ }
104
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
105
+ }
106
+ return this.room.members.current();
107
+ }
108
+ async enableAudio(constraints = true) {
109
+ const track = await this.createUserMediaTrack('audio', constraints);
110
+ if (!track) {
111
+ throw new Error('P2P transport could not create a local audio track.');
112
+ }
113
+ const providerSessionId = await this.ensureConnectedMemberId();
114
+ this.rememberLocalTrack('audio', track, track.getSettings().deviceId, true);
115
+ await this.room.media.audio.enable?.({
116
+ trackId: track.id,
117
+ deviceId: track.getSettings().deviceId,
118
+ providerSessionId,
119
+ });
120
+ this.syncAllPeerSenders();
121
+ return track;
122
+ }
123
+ async enableVideo(constraints = true) {
124
+ const track = await this.createUserMediaTrack('video', constraints);
125
+ if (!track) {
126
+ throw new Error('P2P transport could not create a local video track.');
127
+ }
128
+ const providerSessionId = await this.ensureConnectedMemberId();
129
+ this.rememberLocalTrack('video', track, track.getSettings().deviceId, true);
130
+ await this.room.media.video.enable?.({
131
+ trackId: track.id,
132
+ deviceId: track.getSettings().deviceId,
133
+ providerSessionId,
134
+ });
135
+ this.syncAllPeerSenders();
136
+ return track;
137
+ }
138
+ async startScreenShare(constraints = { video: true, audio: false }) {
139
+ const devices = this.options.mediaDevices;
140
+ if (!devices?.getDisplayMedia) {
141
+ throw new Error('Screen sharing is not available in this environment.');
142
+ }
143
+ const stream = await devices.getDisplayMedia(constraints);
144
+ const track = stream.getVideoTracks()[0] ?? null;
145
+ if (!track) {
146
+ throw new Error('P2P transport could not create a screen-share track.');
147
+ }
148
+ track.addEventListener('ended', () => {
149
+ void this.stopScreenShare();
150
+ }, { once: true });
151
+ const providerSessionId = await this.ensureConnectedMemberId();
152
+ this.rememberLocalTrack('screen', track, track.getSettings().deviceId, true);
153
+ await this.room.media.screen.start?.({
154
+ trackId: track.id,
155
+ deviceId: track.getSettings().deviceId,
156
+ providerSessionId,
157
+ });
158
+ this.syncAllPeerSenders();
159
+ return track;
160
+ }
161
+ async disableAudio() {
162
+ this.releaseLocalTrack('audio');
163
+ this.syncAllPeerSenders();
164
+ await this.room.media.audio.disable();
165
+ }
166
+ async disableVideo() {
167
+ this.releaseLocalTrack('video');
168
+ this.syncAllPeerSenders();
169
+ await this.room.media.video.disable();
170
+ }
171
+ async stopScreenShare() {
172
+ this.releaseLocalTrack('screen');
173
+ this.syncAllPeerSenders();
174
+ await this.room.media.screen.stop();
175
+ }
176
+ async setMuted(kind, muted) {
177
+ const localTrack = this.localTracks.get(kind)?.track;
178
+ if (localTrack) {
179
+ localTrack.enabled = !muted;
180
+ }
181
+ if (kind === 'audio') {
182
+ await this.room.media.audio.setMuted?.(muted);
183
+ }
184
+ else {
185
+ await this.room.media.video.setMuted?.(muted);
186
+ }
187
+ }
188
+ async switchDevices(payload) {
189
+ if (payload.audioInputId && this.localTracks.has('audio')) {
190
+ const nextAudioTrack = await this.createUserMediaTrack('audio', buildExactDeviceConstraint(payload.audioInputId));
191
+ if (nextAudioTrack) {
192
+ this.rememberLocalTrack('audio', nextAudioTrack, payload.audioInputId, true);
193
+ }
194
+ }
195
+ if (payload.videoInputId && this.localTracks.has('video')) {
196
+ const nextVideoTrack = await this.createUserMediaTrack('video', buildExactDeviceConstraint(payload.videoInputId));
197
+ if (nextVideoTrack) {
198
+ this.rememberLocalTrack('video', nextVideoTrack, payload.videoInputId, true);
199
+ }
200
+ }
201
+ this.syncAllPeerSenders();
202
+ await this.room.media.devices.switch(payload);
203
+ }
204
+ onRemoteTrack(handler) {
205
+ this.remoteTrackHandlers.push(handler);
206
+ return createSubscription(() => {
207
+ const index = this.remoteTrackHandlers.indexOf(handler);
208
+ if (index >= 0) {
209
+ this.remoteTrackHandlers.splice(index, 1);
210
+ }
211
+ });
212
+ }
213
+ destroy() {
214
+ this.connected = false;
215
+ this.localMemberId = null;
216
+ for (const subscription of this.subscriptions.splice(0)) {
217
+ subscription.unsubscribe();
218
+ }
219
+ for (const peer of this.peers.values()) {
220
+ this.destroyPeer(peer);
221
+ }
222
+ this.peers.clear();
223
+ for (const kind of Array.from(this.localTracks.keys())) {
224
+ this.releaseLocalTrack(kind);
225
+ }
226
+ this.remoteTrackKinds.clear();
227
+ this.emittedRemoteTracks.clear();
228
+ this.pendingRemoteTracks.clear();
229
+ }
230
+ attachRoomSubscriptions() {
231
+ if (this.subscriptions.length > 0) {
232
+ return;
233
+ }
234
+ this.subscriptions.push(this.room.members.onJoin((member) => {
235
+ if (member.memberId !== this.localMemberId) {
236
+ this.ensurePeer(member.memberId);
237
+ }
238
+ }), this.room.members.onSync((members) => {
239
+ const activeMemberIds = new Set();
240
+ for (const member of members) {
241
+ if (member.memberId !== this.localMemberId) {
242
+ activeMemberIds.add(member.memberId);
243
+ this.ensurePeer(member.memberId);
244
+ }
245
+ }
246
+ for (const memberId of Array.from(this.peers.keys())) {
247
+ if (!activeMemberIds.has(memberId)) {
248
+ this.removeRemoteMember(memberId);
249
+ }
250
+ }
251
+ }), this.room.members.onLeave((member) => {
252
+ this.removeRemoteMember(member.memberId);
253
+ }), this.room.signals.on(this.offerEvent, (payload, meta) => {
254
+ void this.handleDescriptionSignal('offer', payload, meta);
255
+ }), this.room.signals.on(this.answerEvent, (payload, meta) => {
256
+ void this.handleDescriptionSignal('answer', payload, meta);
257
+ }), this.room.signals.on(this.iceEvent, (payload, meta) => {
258
+ void this.handleIceSignal(payload, meta);
259
+ }), this.room.media.onTrack((track, member) => {
260
+ if (member.memberId !== this.localMemberId) {
261
+ this.ensurePeer(member.memberId);
262
+ }
263
+ this.rememberRemoteTrackKind(track, member);
264
+ }), this.room.media.onTrackRemoved((track, member) => {
265
+ if (!track.trackId)
266
+ return;
267
+ const key = buildTrackKey(member.memberId, track.trackId);
268
+ this.remoteTrackKinds.delete(key);
269
+ this.emittedRemoteTracks.delete(key);
270
+ this.pendingRemoteTracks.delete(key);
271
+ }));
272
+ }
273
+ hydrateRemoteTrackKinds() {
274
+ this.remoteTrackKinds.clear();
275
+ this.emittedRemoteTracks.clear();
276
+ this.pendingRemoteTracks.clear();
277
+ for (const mediaMember of this.room.media.list()) {
278
+ for (const track of mediaMember.tracks) {
279
+ this.rememberRemoteTrackKind(track, mediaMember.member);
280
+ }
281
+ }
282
+ }
283
+ rememberRemoteTrackKind(track, member) {
284
+ if (!track.trackId || member.memberId === this.localMemberId) {
285
+ return;
286
+ }
287
+ const key = buildTrackKey(member.memberId, track.trackId);
288
+ this.remoteTrackKinds.set(key, track.kind);
289
+ const pending = this.pendingRemoteTracks.get(key);
290
+ if (pending) {
291
+ this.pendingRemoteTracks.delete(key);
292
+ this.emitRemoteTrack(member.memberId, pending.track, pending.stream, track.kind);
293
+ return;
294
+ }
295
+ this.flushPendingRemoteTracks(member.memberId, track.kind);
296
+ }
297
+ ensurePeer(memberId) {
298
+ const existing = this.peers.get(memberId);
299
+ if (existing) {
300
+ this.syncPeerSenders(existing);
301
+ return existing;
302
+ }
303
+ const pc = this.options.peerConnectionFactory(this.options.rtcConfiguration);
304
+ const peer = {
305
+ memberId,
306
+ pc,
307
+ polite: !!this.localMemberId && this.localMemberId.localeCompare(memberId) > 0,
308
+ makingOffer: false,
309
+ ignoreOffer: false,
310
+ isSettingRemoteAnswerPending: false,
311
+ pendingCandidates: [],
312
+ senders: new Map(),
313
+ };
314
+ pc.onicecandidate = (event) => {
315
+ if (!event.candidate)
316
+ return;
317
+ void this.room.signals.sendTo(memberId, this.iceEvent, {
318
+ candidate: serializeCandidate(event.candidate),
319
+ });
320
+ };
321
+ pc.onnegotiationneeded = () => {
322
+ void this.negotiatePeer(peer);
323
+ };
324
+ pc.ontrack = (event) => {
325
+ const stream = event.streams[0] ?? new MediaStream([event.track]);
326
+ const key = buildTrackKey(memberId, event.track.id);
327
+ const exactKind = this.remoteTrackKinds.get(key);
328
+ const fallbackKind = exactKind ? null : this.resolveFallbackRemoteTrackKind(memberId, event.track);
329
+ const kind = exactKind ?? fallbackKind ?? normalizeTrackKind(event.track);
330
+ if (!kind || (!exactKind && !fallbackKind && kind === 'video' && event.track.kind === 'video')) {
331
+ this.pendingRemoteTracks.set(key, { memberId, track: event.track, stream });
332
+ return;
333
+ }
334
+ this.emitRemoteTrack(memberId, event.track, stream, kind);
335
+ };
336
+ this.peers.set(memberId, peer);
337
+ this.syncPeerSenders(peer);
338
+ return peer;
339
+ }
340
+ async negotiatePeer(peer) {
341
+ if (!this.connected
342
+ || peer.pc.connectionState === 'closed'
343
+ || peer.makingOffer
344
+ || peer.isSettingRemoteAnswerPending
345
+ || peer.pc.signalingState !== 'stable') {
346
+ return;
347
+ }
348
+ try {
349
+ peer.makingOffer = true;
350
+ await peer.pc.setLocalDescription();
351
+ if (!peer.pc.localDescription) {
352
+ return;
353
+ }
354
+ await this.room.signals.sendTo(peer.memberId, this.offerEvent, {
355
+ description: serializeDescription(peer.pc.localDescription),
356
+ });
357
+ }
358
+ catch (error) {
359
+ console.warn('[RoomP2PMediaTransport] Failed to negotiate peer offer.', {
360
+ memberId: peer.memberId,
361
+ signalingState: peer.pc.signalingState,
362
+ error,
363
+ });
364
+ }
365
+ finally {
366
+ peer.makingOffer = false;
367
+ }
368
+ }
369
+ async handleDescriptionSignal(expectedType, payload, meta) {
370
+ const senderId = typeof meta.memberId === 'string' && meta.memberId.trim() ? meta.memberId.trim() : '';
371
+ if (!senderId || senderId === this.localMemberId) {
372
+ return;
373
+ }
374
+ const description = this.normalizeDescription(payload);
375
+ if (!description || description.type !== expectedType) {
376
+ return;
377
+ }
378
+ const peer = this.ensurePeer(senderId);
379
+ const readyForOffer = !peer.makingOffer && (peer.pc.signalingState === 'stable' || peer.isSettingRemoteAnswerPending);
380
+ const offerCollision = description.type === 'offer' && !readyForOffer;
381
+ peer.ignoreOffer = !peer.polite && offerCollision;
382
+ if (peer.ignoreOffer) {
383
+ return;
384
+ }
385
+ try {
386
+ peer.isSettingRemoteAnswerPending = description.type === 'answer';
387
+ await peer.pc.setRemoteDescription(description);
388
+ peer.isSettingRemoteAnswerPending = false;
389
+ await this.flushPendingCandidates(peer);
390
+ if (description.type === 'offer') {
391
+ this.syncPeerSenders(peer);
392
+ await peer.pc.setLocalDescription();
393
+ if (!peer.pc.localDescription) {
394
+ return;
395
+ }
396
+ await this.room.signals.sendTo(senderId, this.answerEvent, {
397
+ description: serializeDescription(peer.pc.localDescription),
398
+ });
399
+ }
400
+ }
401
+ catch (error) {
402
+ console.warn('[RoomP2PMediaTransport] Failed to apply remote session description.', {
403
+ memberId: senderId,
404
+ expectedType,
405
+ signalingState: peer.pc.signalingState,
406
+ error,
407
+ });
408
+ peer.isSettingRemoteAnswerPending = false;
409
+ }
410
+ }
411
+ async handleIceSignal(payload, meta) {
412
+ const senderId = typeof meta.memberId === 'string' && meta.memberId.trim() ? meta.memberId.trim() : '';
413
+ if (!senderId || senderId === this.localMemberId) {
414
+ return;
415
+ }
416
+ const candidate = this.normalizeCandidate(payload);
417
+ if (!candidate) {
418
+ return;
419
+ }
420
+ const peer = this.ensurePeer(senderId);
421
+ if (!peer.pc.remoteDescription) {
422
+ peer.pendingCandidates.push(candidate);
423
+ return;
424
+ }
425
+ try {
426
+ await peer.pc.addIceCandidate(candidate);
427
+ }
428
+ catch (error) {
429
+ console.warn('[RoomP2PMediaTransport] Failed to add ICE candidate.', {
430
+ memberId: senderId,
431
+ error,
432
+ });
433
+ if (!peer.ignoreOffer) {
434
+ peer.pendingCandidates.push(candidate);
435
+ }
436
+ }
437
+ }
438
+ async flushPendingCandidates(peer) {
439
+ if (!peer.pc.remoteDescription || peer.pendingCandidates.length === 0) {
440
+ return;
441
+ }
442
+ const pending = [...peer.pendingCandidates];
443
+ peer.pendingCandidates.length = 0;
444
+ for (const candidate of pending) {
445
+ try {
446
+ await peer.pc.addIceCandidate(candidate);
447
+ }
448
+ catch (error) {
449
+ console.warn('[RoomP2PMediaTransport] Failed to flush pending ICE candidate.', {
450
+ memberId: peer.memberId,
451
+ error,
452
+ });
453
+ if (!peer.ignoreOffer) {
454
+ peer.pendingCandidates.push(candidate);
455
+ }
456
+ }
457
+ }
458
+ }
459
+ syncAllPeerSenders() {
460
+ for (const peer of this.peers.values()) {
461
+ this.syncPeerSenders(peer);
462
+ }
463
+ }
464
+ syncPeerSenders(peer) {
465
+ const activeKinds = new Set();
466
+ let changed = false;
467
+ for (const [kind, localTrack] of this.localTracks.entries()) {
468
+ activeKinds.add(kind);
469
+ const sender = peer.senders.get(kind);
470
+ if (sender) {
471
+ if (sender.track !== localTrack.track) {
472
+ void sender.replaceTrack(localTrack.track);
473
+ changed = true;
474
+ }
475
+ continue;
476
+ }
477
+ const addedSender = peer.pc.addTrack(localTrack.track, new MediaStream([localTrack.track]));
478
+ peer.senders.set(kind, addedSender);
479
+ changed = true;
480
+ }
481
+ for (const [kind, sender] of Array.from(peer.senders.entries())) {
482
+ if (activeKinds.has(kind)) {
483
+ continue;
484
+ }
485
+ try {
486
+ peer.pc.removeTrack(sender);
487
+ }
488
+ catch {
489
+ // Ignore duplicate removals during shutdown.
490
+ }
491
+ peer.senders.delete(kind);
492
+ changed = true;
493
+ }
494
+ if (changed) {
495
+ void this.negotiatePeer(peer);
496
+ }
497
+ }
498
+ emitRemoteTrack(memberId, track, stream, kind) {
499
+ const key = buildTrackKey(memberId, track.id);
500
+ if (this.emittedRemoteTracks.has(key)) {
501
+ return;
502
+ }
503
+ this.emittedRemoteTracks.add(key);
504
+ this.remoteTrackKinds.set(key, kind);
505
+ const participant = this.findMember(memberId);
506
+ const payload = {
507
+ kind,
508
+ track,
509
+ stream,
510
+ trackName: track.id,
511
+ providerSessionId: memberId,
512
+ participantId: memberId,
513
+ userId: participant?.userId,
514
+ };
515
+ for (const handler of this.remoteTrackHandlers) {
516
+ handler(payload);
517
+ }
518
+ }
519
+ resolveFallbackRemoteTrackKind(memberId, track) {
520
+ const normalizedKind = normalizeTrackKind(track);
521
+ if (!normalizedKind) {
522
+ return null;
523
+ }
524
+ if (normalizedKind === 'audio') {
525
+ return 'audio';
526
+ }
527
+ return this.getNextUnassignedPublishedVideoLikeKind(memberId);
528
+ }
529
+ flushPendingRemoteTracks(memberId, roomKind) {
530
+ const expectedTrackKind = roomKind === 'audio' ? 'audio' : 'video';
531
+ for (const [key, pending] of this.pendingRemoteTracks.entries()) {
532
+ if (pending.memberId !== memberId || pending.track.kind !== expectedTrackKind) {
533
+ continue;
534
+ }
535
+ this.pendingRemoteTracks.delete(key);
536
+ this.emitRemoteTrack(memberId, pending.track, pending.stream, roomKind);
537
+ return;
538
+ }
539
+ }
540
+ getPublishedVideoLikeKinds(memberId) {
541
+ const mediaMember = this.room.media.list().find((entry) => entry.member.memberId === memberId);
542
+ if (!mediaMember) {
543
+ return [];
544
+ }
545
+ const publishedKinds = new Set();
546
+ for (const track of mediaMember.tracks) {
547
+ if ((track.kind === 'video' || track.kind === 'screen') && track.trackId) {
548
+ publishedKinds.add(track.kind);
549
+ }
550
+ }
551
+ return Array.from(publishedKinds);
552
+ }
553
+ getNextUnassignedPublishedVideoLikeKind(memberId) {
554
+ const publishedKinds = this.getPublishedVideoLikeKinds(memberId);
555
+ if (publishedKinds.length === 0) {
556
+ return null;
557
+ }
558
+ const assignedKinds = new Set();
559
+ for (const key of this.emittedRemoteTracks) {
560
+ if (!key.startsWith(`${memberId}:`)) {
561
+ continue;
562
+ }
563
+ const kind = this.remoteTrackKinds.get(key);
564
+ if (kind === 'video' || kind === 'screen') {
565
+ assignedKinds.add(kind);
566
+ }
567
+ }
568
+ return publishedKinds.find((kind) => !assignedKinds.has(kind)) ?? null;
569
+ }
570
+ closePeer(memberId) {
571
+ const peer = this.peers.get(memberId);
572
+ if (!peer)
573
+ return;
574
+ this.destroyPeer(peer);
575
+ this.peers.delete(memberId);
576
+ }
577
+ removeRemoteMember(memberId) {
578
+ this.remoteTrackKinds.forEach((_kind, key) => {
579
+ if (key.startsWith(`${memberId}:`)) {
580
+ this.remoteTrackKinds.delete(key);
581
+ }
582
+ });
583
+ this.emittedRemoteTracks.forEach((key) => {
584
+ if (key.startsWith(`${memberId}:`)) {
585
+ this.emittedRemoteTracks.delete(key);
586
+ }
587
+ });
588
+ this.pendingRemoteTracks.forEach((_pending, key) => {
589
+ if (key.startsWith(`${memberId}:`)) {
590
+ this.pendingRemoteTracks.delete(key);
591
+ }
592
+ });
593
+ this.closePeer(memberId);
594
+ }
595
+ findMember(memberId) {
596
+ return this.room.members.list().find((member) => member.memberId === memberId);
597
+ }
598
+ rollbackConnectedState() {
599
+ this.connected = false;
600
+ this.localMemberId = null;
601
+ for (const subscription of this.subscriptions.splice(0)) {
602
+ subscription.unsubscribe();
603
+ }
604
+ for (const peer of this.peers.values()) {
605
+ this.destroyPeer(peer);
606
+ }
607
+ this.peers.clear();
608
+ this.remoteTrackKinds.clear();
609
+ this.emittedRemoteTracks.clear();
610
+ this.pendingRemoteTracks.clear();
611
+ }
612
+ destroyPeer(peer) {
613
+ peer.pc.onicecandidate = null;
614
+ peer.pc.onnegotiationneeded = null;
615
+ peer.pc.ontrack = null;
616
+ try {
617
+ peer.pc.close();
618
+ }
619
+ catch {
620
+ // Ignore duplicate closes.
621
+ }
622
+ }
623
+ async createUserMediaTrack(kind, constraints) {
624
+ const devices = this.options.mediaDevices;
625
+ if (!devices?.getUserMedia || constraints === false) {
626
+ return null;
627
+ }
628
+ const stream = await devices.getUserMedia(kind === 'audio'
629
+ ? { audio: constraints, video: false }
630
+ : { audio: false, video: constraints });
631
+ return kind === 'audio' ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
632
+ }
633
+ rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
634
+ this.releaseLocalTrack(kind);
635
+ this.localTracks.set(kind, {
636
+ kind,
637
+ track,
638
+ deviceId,
639
+ stopOnCleanup,
640
+ });
641
+ }
642
+ releaseLocalTrack(kind) {
643
+ const local = this.localTracks.get(kind);
644
+ if (!local)
645
+ return;
646
+ if (local.stopOnCleanup) {
647
+ local.track.stop();
648
+ }
649
+ this.localTracks.delete(kind);
650
+ }
651
+ async ensureConnectedMemberId() {
652
+ if (this.localMemberId) {
653
+ return this.localMemberId;
654
+ }
655
+ return this.connect();
656
+ }
657
+ normalizeDescription(payload) {
658
+ if (!payload || typeof payload !== 'object') {
659
+ return null;
660
+ }
661
+ const raw = payload.description;
662
+ if (!raw || typeof raw.type !== 'string') {
663
+ return null;
664
+ }
665
+ return {
666
+ type: raw.type,
667
+ sdp: typeof raw.sdp === 'string' ? raw.sdp : undefined,
668
+ };
669
+ }
670
+ normalizeCandidate(payload) {
671
+ if (!payload || typeof payload !== 'object') {
672
+ return null;
673
+ }
674
+ const raw = payload.candidate;
675
+ if (!raw || typeof raw.candidate !== 'string') {
676
+ return null;
677
+ }
678
+ return raw;
679
+ }
680
+ get offerEvent() {
681
+ return `${this.options.signalPrefix}.offer`;
682
+ }
683
+ get answerEvent() {
684
+ return `${this.options.signalPrefix}.answer`;
685
+ }
686
+ get iceEvent() {
687
+ return `${this.options.signalPrefix}.ice`;
688
+ }
689
+ }
690
+ //# sourceMappingURL=room-p2p-media.js.map