callway 1.0.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/README.md +172 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +726 -0
- package/dist/index.js.map +1 -0
- package/dist/mediaUtils-5gVEq26H.d.ts +49 -0
- package/dist/react/index.d.ts +87 -0
- package/dist/react/index.js +186 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
// src/core/PeerManager.ts
|
|
2
|
+
var PeerManager = class {
|
|
3
|
+
// Multi-peer support: Map of remote peer IDs to their connections
|
|
4
|
+
peerConnections = /* @__PURE__ */ new Map();
|
|
5
|
+
// Legacy 1:1 support (for backward compatibility)
|
|
6
|
+
peerConnection = null;
|
|
7
|
+
remoteId = null;
|
|
8
|
+
localId;
|
|
9
|
+
signalingHandler = null;
|
|
10
|
+
signalingAdapter = null;
|
|
11
|
+
registeredInAdapter = false;
|
|
12
|
+
config;
|
|
13
|
+
remoteStreamCallback = null;
|
|
14
|
+
connectionStateCallback = null;
|
|
15
|
+
iceConnectionStateCallback = null;
|
|
16
|
+
constructor(localId, config = {}) {
|
|
17
|
+
this.localId = localId;
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Set the signaling handler for sending messages
|
|
22
|
+
*/
|
|
23
|
+
setSignalingHandler(handler) {
|
|
24
|
+
this.signalingHandler = handler;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set a signaling adapter. This will register this peer and wire sending.
|
|
28
|
+
*/
|
|
29
|
+
setSignalingAdapter(adapter, roomId) {
|
|
30
|
+
this.signalingAdapter = adapter;
|
|
31
|
+
this.signalingHandler = async (message) => {
|
|
32
|
+
await adapter.sendMessage(message);
|
|
33
|
+
};
|
|
34
|
+
adapter.registerPeer(this.localId, async (message) => {
|
|
35
|
+
if (message.type === "offer") {
|
|
36
|
+
await this.handleOffer(message.data, message.from);
|
|
37
|
+
} else if (message.type === "answer") {
|
|
38
|
+
await this.handleAnswer(message.data, message.from);
|
|
39
|
+
} else if (message.type === "ice-candidate") {
|
|
40
|
+
await this.addIceCandidate(message.data, message.from);
|
|
41
|
+
}
|
|
42
|
+
}, roomId);
|
|
43
|
+
this.registeredInAdapter = true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a new peer connection for a specific remote peer
|
|
47
|
+
*/
|
|
48
|
+
createPeerConnection(remoteId, polite) {
|
|
49
|
+
const pc = new RTCPeerConnection({
|
|
50
|
+
iceServers: this.config.iceServers || [
|
|
51
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
pc.onicecandidate = (event) => {
|
|
55
|
+
if (event.candidate && this.signalingHandler) {
|
|
56
|
+
console.log(`[PeerManager ${this.localId}] ICE candidate for ${remoteId}:`, event.candidate);
|
|
57
|
+
this.signalingHandler({
|
|
58
|
+
type: "ice-candidate",
|
|
59
|
+
from: this.localId,
|
|
60
|
+
to: remoteId,
|
|
61
|
+
data: event.candidate.toJSON()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
pc.onconnectionstatechange = () => {
|
|
66
|
+
console.log(`[PeerManager ${this.localId}] Connection state with ${remoteId}:`, pc.connectionState);
|
|
67
|
+
if (this.connectionStateCallback) {
|
|
68
|
+
this.connectionStateCallback(pc.connectionState, remoteId);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
pc.oniceconnectionstatechange = () => {
|
|
72
|
+
console.log(`[PeerManager ${this.localId}] ICE connection state with ${remoteId}:`, pc.iceConnectionState);
|
|
73
|
+
if (this.iceConnectionStateCallback) {
|
|
74
|
+
this.iceConnectionStateCallback(pc.iceConnectionState, remoteId);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
pc.ontrack = (event) => {
|
|
78
|
+
console.log(`[PeerManager ${this.localId}] Remote track received from ${remoteId}:`, event.track.kind);
|
|
79
|
+
if (this.remoteStreamCallback) {
|
|
80
|
+
if (event.streams && event.streams[0]) {
|
|
81
|
+
this.remoteStreamCallback(event.streams[0], remoteId);
|
|
82
|
+
} else {
|
|
83
|
+
const stream = new MediaStream([event.track]);
|
|
84
|
+
this.remoteStreamCallback(stream, remoteId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
pc.onnegotiationneeded = async () => {
|
|
89
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
90
|
+
if (!peerInfo) return;
|
|
91
|
+
if (peerInfo.makingOffer) return;
|
|
92
|
+
peerInfo.makingOffer = true;
|
|
93
|
+
try {
|
|
94
|
+
if (pc.signalingState !== "stable") {
|
|
95
|
+
console.log(`[PeerManager ${this.localId}] negotiationneeded aborted (state ${pc.signalingState}) for ${remoteId}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log(`[PeerManager ${this.localId}] negotiationneeded -> creating offer for ${remoteId}`);
|
|
99
|
+
const offer = await pc.createOffer();
|
|
100
|
+
await pc.setLocalDescription(offer);
|
|
101
|
+
peerInfo.isInitiator = true;
|
|
102
|
+
if (this.signalingHandler) {
|
|
103
|
+
await this.signalingHandler({
|
|
104
|
+
type: "offer",
|
|
105
|
+
from: this.localId,
|
|
106
|
+
to: remoteId,
|
|
107
|
+
data: offer
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn(`[PeerManager ${this.localId}] negotiationneeded failed for ${remoteId}:`, err);
|
|
112
|
+
} finally {
|
|
113
|
+
peerInfo.makingOffer = false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return pc;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Initialize peer connection for a call (1:1 compatibility)
|
|
120
|
+
* For multi-peer calls, use addPeer() instead
|
|
121
|
+
*/
|
|
122
|
+
async initialize(remoteId) {
|
|
123
|
+
if (this.peerConnection || this.peerConnections.has(remoteId)) {
|
|
124
|
+
throw new Error("Peer connection already initialized");
|
|
125
|
+
}
|
|
126
|
+
this.remoteId = remoteId;
|
|
127
|
+
const polite = this.localId > remoteId;
|
|
128
|
+
this.peerConnection = this.createPeerConnection(remoteId, polite);
|
|
129
|
+
this.peerConnections.set(remoteId, {
|
|
130
|
+
connection: this.peerConnection,
|
|
131
|
+
remoteId,
|
|
132
|
+
isInitiator: false,
|
|
133
|
+
iceCandidateQueue: [],
|
|
134
|
+
remoteDescriptionSet: false,
|
|
135
|
+
makingOffer: false,
|
|
136
|
+
polite
|
|
137
|
+
});
|
|
138
|
+
console.log(`[PeerManager ${this.localId}] Initialized peer connection for remote: ${remoteId}`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Add a new peer to the room (multi-peer support)
|
|
142
|
+
* Creates a new RTCPeerConnection for this peer
|
|
143
|
+
*/
|
|
144
|
+
async addPeer(remoteId, isInitiator = false) {
|
|
145
|
+
if (this.peerConnections.has(remoteId)) {
|
|
146
|
+
console.warn(`[PeerManager ${this.localId}] Peer ${remoteId} already exists`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const polite = this.localId > remoteId;
|
|
150
|
+
const pc = this.createPeerConnection(remoteId, polite);
|
|
151
|
+
this.peerConnections.set(remoteId, {
|
|
152
|
+
connection: pc,
|
|
153
|
+
remoteId,
|
|
154
|
+
isInitiator,
|
|
155
|
+
iceCandidateQueue: [],
|
|
156
|
+
remoteDescriptionSet: false,
|
|
157
|
+
makingOffer: false,
|
|
158
|
+
polite
|
|
159
|
+
});
|
|
160
|
+
console.log(`[PeerManager ${this.localId}] Added peer: ${remoteId} (initiator: ${isInitiator})`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Remove a peer from the room
|
|
164
|
+
*/
|
|
165
|
+
async removePeer(remoteId) {
|
|
166
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
167
|
+
if (!peerInfo) {
|
|
168
|
+
console.warn(`[PeerManager ${this.localId}] Peer ${remoteId} not found`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(`[PeerManager ${this.localId}] Removing peer: ${remoteId}`);
|
|
172
|
+
peerInfo.connection.close();
|
|
173
|
+
this.peerConnections.delete(remoteId);
|
|
174
|
+
if (this.remoteId === remoteId) {
|
|
175
|
+
this.peerConnection = null;
|
|
176
|
+
this.remoteId = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get all remote peer IDs
|
|
181
|
+
*/
|
|
182
|
+
getRemotePeerIds() {
|
|
183
|
+
return Array.from(this.peerConnections.keys());
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check if a peer exists
|
|
187
|
+
*/
|
|
188
|
+
hasPeer(remoteId) {
|
|
189
|
+
return this.peerConnections.has(remoteId);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get peer connection for a specific remote peer (multi-peer support)
|
|
193
|
+
*/
|
|
194
|
+
getPeerConnectionFor(remoteId) {
|
|
195
|
+
return this.peerConnections.get(remoteId)?.connection ?? null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create an offer for a specific peer (caller side)
|
|
199
|
+
*/
|
|
200
|
+
async createOffer(remoteId) {
|
|
201
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
202
|
+
if (!peerInfo) {
|
|
203
|
+
throw new Error(`Peer ${remoteId} not found. Call addPeer() first.`);
|
|
204
|
+
}
|
|
205
|
+
const pc = peerInfo.connection;
|
|
206
|
+
console.log(`[PeerManager ${this.localId}] Creating offer for ${remoteId}...`);
|
|
207
|
+
peerInfo.makingOffer = true;
|
|
208
|
+
try {
|
|
209
|
+
const offer = await pc.createOffer();
|
|
210
|
+
await pc.setLocalDescription(offer);
|
|
211
|
+
peerInfo.isInitiator = true;
|
|
212
|
+
console.log(`[PeerManager ${this.localId}] Offer created for ${remoteId}:`, offer);
|
|
213
|
+
if (this.signalingHandler) {
|
|
214
|
+
await this.signalingHandler({
|
|
215
|
+
type: "offer",
|
|
216
|
+
from: this.localId,
|
|
217
|
+
to: remoteId,
|
|
218
|
+
data: offer
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return offer;
|
|
222
|
+
} finally {
|
|
223
|
+
peerInfo.makingOffer = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Create offers for all peers (multi-peer support)
|
|
228
|
+
*/
|
|
229
|
+
async createOffersForAll() {
|
|
230
|
+
const promises = Array.from(this.peerConnections.keys()).map(
|
|
231
|
+
(remoteId) => this.createOffer(remoteId)
|
|
232
|
+
);
|
|
233
|
+
await Promise.all(promises);
|
|
234
|
+
console.log(`[PeerManager ${this.localId}] Created offers for all ${this.peerConnections.size} peers`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Handle incoming offer from a peer (callee side)
|
|
238
|
+
*
|
|
239
|
+
* Collision strategy: the peer who already has a local offer ignores the
|
|
240
|
+
* incoming offer (impolite); with deterministic initiator selection in the
|
|
241
|
+
* caller, collisions should be rare. This keeps the signaling state valid.
|
|
242
|
+
*/
|
|
243
|
+
async handleOffer(offer, remoteId) {
|
|
244
|
+
let peerInfo = this.peerConnections.get(remoteId);
|
|
245
|
+
if (!peerInfo) {
|
|
246
|
+
await this.addPeer(remoteId, false);
|
|
247
|
+
peerInfo = this.peerConnections.get(remoteId);
|
|
248
|
+
}
|
|
249
|
+
const pc = peerInfo.connection;
|
|
250
|
+
const collision = peerInfo.makingOffer || pc.signalingState === "have-local-offer";
|
|
251
|
+
const ignoreOffer = !peerInfo.polite && collision;
|
|
252
|
+
if (ignoreOffer) {
|
|
253
|
+
console.log(`[PeerManager ${this.localId}] Ignoring offer from ${remoteId} (collision, impolite)`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (collision && peerInfo.polite) {
|
|
257
|
+
console.log(`[PeerManager ${this.localId}] Offer collision with ${remoteId} (polite) - rolling back and accepting`);
|
|
258
|
+
try {
|
|
259
|
+
await pc.setLocalDescription({ type: "rollback" });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.warn(`[PeerManager ${this.localId}] rollback failed:`, err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (pc.signalingState !== "stable" && pc.signalingState !== "have-local-offer") {
|
|
265
|
+
console.warn(`[PeerManager ${this.localId}] Cannot handle offer from ${remoteId}, signaling state is ${pc.signalingState}`);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
console.log(`[PeerManager ${this.localId}] Handling offer from ${remoteId}...`);
|
|
269
|
+
try {
|
|
270
|
+
const currentPc = peerInfo.connection;
|
|
271
|
+
await currentPc.setRemoteDescription(new RTCSessionDescription(offer));
|
|
272
|
+
peerInfo.remoteDescriptionSet = true;
|
|
273
|
+
await this.processIceCandidateQueue(remoteId);
|
|
274
|
+
const answer = await currentPc.createAnswer();
|
|
275
|
+
await currentPc.setLocalDescription(answer);
|
|
276
|
+
peerInfo.isInitiator = false;
|
|
277
|
+
console.log(`[PeerManager ${this.localId}] Answer created for ${remoteId}:`, answer);
|
|
278
|
+
if (this.signalingHandler) {
|
|
279
|
+
await this.signalingHandler({
|
|
280
|
+
type: "answer",
|
|
281
|
+
from: this.localId,
|
|
282
|
+
to: remoteId,
|
|
283
|
+
data: answer
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return answer;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error(`[PeerManager ${this.localId}] Error handling offer from ${remoteId}:`, error);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Handle incoming answer from a peer (caller side)
|
|
294
|
+
*/
|
|
295
|
+
async handleAnswer(answer, remoteId) {
|
|
296
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
297
|
+
if (!peerInfo) {
|
|
298
|
+
throw new Error(`Peer ${remoteId} not found. Call addPeer() first.`);
|
|
299
|
+
}
|
|
300
|
+
const pc = peerInfo.connection;
|
|
301
|
+
if (pc.signalingState !== "have-local-offer") {
|
|
302
|
+
if (pc.signalingState === "stable") {
|
|
303
|
+
console.log(`[PeerManager ${this.localId}] Ignoring answer from ${remoteId} - already in stable state`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.warn(`[PeerManager ${this.localId}] Cannot handle answer from ${remoteId}, signaling state is ${pc.signalingState}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log(`[PeerManager ${this.localId}] Handling answer from ${remoteId}...`);
|
|
310
|
+
try {
|
|
311
|
+
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
|
312
|
+
peerInfo.remoteDescriptionSet = true;
|
|
313
|
+
await this.processIceCandidateQueue(remoteId);
|
|
314
|
+
console.log(`[PeerManager ${this.localId}] Answer processed for ${remoteId}`);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error(`[PeerManager ${this.localId}] Error handling answer from ${remoteId}:`, error);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Process queued ICE candidates for a peer
|
|
322
|
+
*/
|
|
323
|
+
async processIceCandidateQueue(remoteId) {
|
|
324
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
325
|
+
if (!peerInfo || peerInfo.iceCandidateQueue.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const pc = peerInfo.connection;
|
|
329
|
+
console.log(`[PeerManager ${this.localId}] Processing ${peerInfo.iceCandidateQueue.length} queued ICE candidates for ${remoteId}`);
|
|
330
|
+
const candidates = [...peerInfo.iceCandidateQueue];
|
|
331
|
+
peerInfo.iceCandidateQueue = [];
|
|
332
|
+
for (const candidate of candidates) {
|
|
333
|
+
try {
|
|
334
|
+
await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.warn(`[PeerManager ${this.localId}] Failed to add queued ICE candidate for ${remoteId}:`, error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Add ICE candidate for a specific peer
|
|
342
|
+
* Queues candidates if remote description is not set yet
|
|
343
|
+
*/
|
|
344
|
+
async addIceCandidate(candidate, remoteId) {
|
|
345
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
346
|
+
if (!peerInfo) {
|
|
347
|
+
console.warn(`[PeerManager ${this.localId}] Cannot add ICE candidate: Peer ${remoteId} not found`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const pc = peerInfo.connection;
|
|
351
|
+
if (!peerInfo.remoteDescriptionSet) {
|
|
352
|
+
console.log(`[PeerManager ${this.localId}] Queuing ICE candidate for ${remoteId} (remote description not set yet)`);
|
|
353
|
+
peerInfo.iceCandidateQueue.push(candidate);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
console.log(`[PeerManager ${this.localId}] Adding ICE candidate for ${remoteId}`);
|
|
358
|
+
await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error instanceof Error) {
|
|
361
|
+
if (error.message.includes("duplicate") || error.message.includes("already been added")) {
|
|
362
|
+
console.log(`[PeerManager ${this.localId}] Duplicate ICE candidate for ${remoteId} (ignored)`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (pc.connectionState === "closed" || pc.connectionState === "failed") {
|
|
366
|
+
console.warn(`[PeerManager ${this.localId}] Cannot add ICE candidate: connection to ${remoteId} is ${pc.connectionState}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
console.warn(`[PeerManager ${this.localId}] Failed to add ICE candidate for ${remoteId}:`, error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Add a media track to a specific peer connection
|
|
375
|
+
*/
|
|
376
|
+
addTrack(track, stream, remoteId) {
|
|
377
|
+
if (remoteId) {
|
|
378
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
379
|
+
if (!peerInfo) {
|
|
380
|
+
throw new Error(`Peer ${remoteId} not found. Call addPeer() first.`);
|
|
381
|
+
}
|
|
382
|
+
console.log(`[PeerManager ${this.localId}] Adding track to ${remoteId}:`, track.kind);
|
|
383
|
+
peerInfo.connection.addTrack(track, stream);
|
|
384
|
+
} else {
|
|
385
|
+
if (!this.peerConnection) {
|
|
386
|
+
throw new Error("Peer connection not initialized. Call initialize() first.");
|
|
387
|
+
}
|
|
388
|
+
console.log(`[PeerManager ${this.localId}] Adding track:`, track.kind);
|
|
389
|
+
this.peerConnection.addTrack(track, stream);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Add media tracks to all peer connections (multi-peer support)
|
|
394
|
+
*/
|
|
395
|
+
addTrackToAll(track, stream) {
|
|
396
|
+
this.peerConnections.forEach((peerInfo, remoteId) => {
|
|
397
|
+
console.log(`[PeerManager ${this.localId}] Adding track to ${remoteId}:`, track.kind);
|
|
398
|
+
peerInfo.connection.addTrack(track, stream);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get the remote media stream for a specific peer
|
|
403
|
+
*/
|
|
404
|
+
getRemoteStream(remoteId) {
|
|
405
|
+
const peerInfo = this.peerConnections.get(remoteId);
|
|
406
|
+
if (!peerInfo) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const pc = peerInfo.connection;
|
|
410
|
+
const tracks = [];
|
|
411
|
+
pc.getReceivers().forEach((receiver) => {
|
|
412
|
+
if (receiver.track) {
|
|
413
|
+
tracks.push(receiver.track);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
if (tracks.length === 0) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return new MediaStream(tracks);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Set handler for when remote stream is available
|
|
423
|
+
* Callback receives (stream, remoteId) for multi-peer support
|
|
424
|
+
* Can be called before or after initialization
|
|
425
|
+
*/
|
|
426
|
+
onRemoteStream(callback) {
|
|
427
|
+
this.remoteStreamCallback = callback;
|
|
428
|
+
this.peerConnections.forEach((peerInfo, remoteId) => {
|
|
429
|
+
peerInfo.connection.ontrack = (event) => {
|
|
430
|
+
console.log(`[PeerManager ${this.localId}] Remote track received from ${remoteId}:`, event.track.kind);
|
|
431
|
+
if (event.streams && event.streams[0]) {
|
|
432
|
+
callback(event.streams[0], remoteId);
|
|
433
|
+
} else {
|
|
434
|
+
const stream = new MediaStream([event.track]);
|
|
435
|
+
callback(stream, remoteId);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Set handler for connection state changes
|
|
442
|
+
* Callback receives (state, remoteId) for multi-peer support
|
|
443
|
+
*/
|
|
444
|
+
onConnectionStateChange(callback) {
|
|
445
|
+
this.connectionStateCallback = callback;
|
|
446
|
+
this.peerConnections.forEach((peerInfo, remoteId) => {
|
|
447
|
+
callback(peerInfo.connection.connectionState, remoteId);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Set handler for ICE connection state changes
|
|
452
|
+
* Callback receives (state, remoteId) for multi-peer support
|
|
453
|
+
*/
|
|
454
|
+
onIceConnectionStateChange(callback) {
|
|
455
|
+
this.iceConnectionStateCallback = callback;
|
|
456
|
+
this.peerConnections.forEach((peerInfo, remoteId) => {
|
|
457
|
+
callback(peerInfo.connection.iceConnectionState, remoteId);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get the peer connection instance (legacy 1:1 support)
|
|
462
|
+
*/
|
|
463
|
+
getPeerConnection() {
|
|
464
|
+
return this.peerConnection;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Cleanup and close all peer connections
|
|
468
|
+
*/
|
|
469
|
+
async cleanup() {
|
|
470
|
+
const cleanupPromises = Array.from(this.peerConnections.values()).map((peerInfo) => {
|
|
471
|
+
console.log(`[PeerManager ${this.localId}] Cleaning up connection to ${peerInfo.remoteId}...`);
|
|
472
|
+
peerInfo.connection.close();
|
|
473
|
+
});
|
|
474
|
+
await Promise.all(cleanupPromises);
|
|
475
|
+
this.peerConnections.clear();
|
|
476
|
+
if (this.peerConnection) {
|
|
477
|
+
this.peerConnection.close();
|
|
478
|
+
this.peerConnection = null;
|
|
479
|
+
this.remoteId = null;
|
|
480
|
+
}
|
|
481
|
+
if (this.signalingAdapter && this.registeredInAdapter) {
|
|
482
|
+
this.signalingAdapter.unregisterPeer(this.localId);
|
|
483
|
+
this.signalingAdapter = null;
|
|
484
|
+
this.registeredInAdapter = false;
|
|
485
|
+
}
|
|
486
|
+
console.log(`[PeerManager ${this.localId}] All connections cleaned up`);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/core/MediaManager.ts
|
|
491
|
+
var MediaManager = class {
|
|
492
|
+
localStream = null;
|
|
493
|
+
/**
|
|
494
|
+
* Request user media (audio and/or video)
|
|
495
|
+
*/
|
|
496
|
+
async getUserMedia(constraints = { audio: true, video: true }) {
|
|
497
|
+
console.log("[MediaManager] Requesting user media with constraints:", constraints);
|
|
498
|
+
try {
|
|
499
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
500
|
+
this.localStream = stream;
|
|
501
|
+
console.log("[MediaManager] User media obtained:", {
|
|
502
|
+
audioTracks: stream.getAudioTracks().length,
|
|
503
|
+
videoTracks: stream.getVideoTracks().length
|
|
504
|
+
});
|
|
505
|
+
return stream;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.error("[MediaManager] Error getting user media:", error);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get the current local stream
|
|
513
|
+
*/
|
|
514
|
+
getLocalStream() {
|
|
515
|
+
return this.localStream;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Stop all tracks in the local stream
|
|
519
|
+
*/
|
|
520
|
+
stopLocalStream() {
|
|
521
|
+
if (this.localStream) {
|
|
522
|
+
console.log("[MediaManager] Stopping local stream...");
|
|
523
|
+
this.localStream.getTracks().forEach((track) => {
|
|
524
|
+
track.stop();
|
|
525
|
+
console.log(`[MediaManager] Stopped track: ${track.kind}`);
|
|
526
|
+
});
|
|
527
|
+
this.localStream = null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Attach local stream to a peer connection (1:1 or specific peer)
|
|
532
|
+
* This adds all tracks from the local stream to the peer connection
|
|
533
|
+
*/
|
|
534
|
+
attachToPeer(peerManager, remoteId) {
|
|
535
|
+
if (!this.localStream) {
|
|
536
|
+
throw new Error("No local stream available. Call getUserMedia() first.");
|
|
537
|
+
}
|
|
538
|
+
console.log(`[MediaManager] Attaching local stream to peer${remoteId ? ` ${remoteId}` : ""}...`);
|
|
539
|
+
this.localStream.getTracks().forEach((track) => {
|
|
540
|
+
peerManager.addTrack(track, this.localStream, remoteId);
|
|
541
|
+
});
|
|
542
|
+
console.log(`[MediaManager] Local stream attached${remoteId ? ` to ${remoteId}` : ""}`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Attach local stream to all peers in a room (multi-peer support)
|
|
546
|
+
* This adds all tracks from the local stream to all peer connections
|
|
547
|
+
*/
|
|
548
|
+
attachToAllPeers(peerManager) {
|
|
549
|
+
if (!this.localStream) {
|
|
550
|
+
throw new Error("No local stream available. Call getUserMedia() first.");
|
|
551
|
+
}
|
|
552
|
+
const peerIds = peerManager.getRemotePeerIds();
|
|
553
|
+
console.log(`[MediaManager] Attaching local stream to ${peerIds.length} peers...`);
|
|
554
|
+
this.localStream.getTracks().forEach((track) => {
|
|
555
|
+
peerManager.addTrackToAll(track, this.localStream);
|
|
556
|
+
});
|
|
557
|
+
console.log(`[MediaManager] Local stream attached to all peers`);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Check if camera (video) is off/muted
|
|
561
|
+
* Returns true if camera is off, false if on
|
|
562
|
+
*/
|
|
563
|
+
isCameraOff() {
|
|
564
|
+
if (!this.localStream) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
const videoTracks = this.localStream.getVideoTracks();
|
|
568
|
+
if (videoTracks.length === 0) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
return videoTracks.every((track) => track.muted || !track.enabled || track.readyState === "ended");
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if microphone (audio) is off/muted
|
|
575
|
+
* Returns true if microphone is off, false if on
|
|
576
|
+
*/
|
|
577
|
+
isMicrophoneOff() {
|
|
578
|
+
if (!this.localStream) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
const audioTracks = this.localStream.getAudioTracks();
|
|
582
|
+
if (audioTracks.length === 0) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
return audioTracks.every((track) => track.muted || !track.enabled || track.readyState === "ended");
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get camera track state
|
|
589
|
+
* Returns the first video track or null
|
|
590
|
+
*/
|
|
591
|
+
getCameraTrack() {
|
|
592
|
+
if (!this.localStream) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const videoTracks = this.localStream.getVideoTracks();
|
|
596
|
+
return videoTracks.length > 0 ? videoTracks[0] : null;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get microphone track state
|
|
600
|
+
* Returns the first audio track or null
|
|
601
|
+
*/
|
|
602
|
+
getMicrophoneTrack() {
|
|
603
|
+
if (!this.localStream) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const audioTracks = this.localStream.getAudioTracks();
|
|
607
|
+
return audioTracks.length > 0 ? audioTracks[0] : null;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get detailed media state information
|
|
611
|
+
*/
|
|
612
|
+
getMediaState() {
|
|
613
|
+
const videoTrack = this.getCameraTrack();
|
|
614
|
+
const audioTrack = this.getMicrophoneTrack();
|
|
615
|
+
return {
|
|
616
|
+
hasStream: this.localStream !== null,
|
|
617
|
+
camera: {
|
|
618
|
+
available: videoTrack !== null,
|
|
619
|
+
enabled: videoTrack?.enabled ?? false,
|
|
620
|
+
muted: videoTrack?.muted ?? true,
|
|
621
|
+
readyState: videoTrack?.readyState ?? null
|
|
622
|
+
},
|
|
623
|
+
microphone: {
|
|
624
|
+
available: audioTrack !== null,
|
|
625
|
+
enabled: audioTrack?.enabled ?? false,
|
|
626
|
+
muted: audioTrack?.muted ?? true,
|
|
627
|
+
readyState: audioTrack?.readyState ?? null
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Cleanup - stop all tracks
|
|
633
|
+
*/
|
|
634
|
+
cleanup() {
|
|
635
|
+
this.stopLocalStream();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// src/core/mediaUtils.ts
|
|
640
|
+
function isCameraOff(stream) {
|
|
641
|
+
if (!stream) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
const videoTracks = stream.getVideoTracks();
|
|
645
|
+
if (videoTracks.length === 0) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
return videoTracks.every((track) => track.muted || !track.enabled || track.readyState === "ended");
|
|
649
|
+
}
|
|
650
|
+
function isMicrophoneOff(stream) {
|
|
651
|
+
if (!stream) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
const audioTracks = stream.getAudioTracks();
|
|
655
|
+
if (audioTracks.length === 0) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
return audioTracks.every((track) => track.muted || !track.enabled || track.readyState === "ended");
|
|
659
|
+
}
|
|
660
|
+
function getCameraTrack(stream) {
|
|
661
|
+
if (!stream) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
const videoTracks = stream.getVideoTracks();
|
|
665
|
+
return videoTracks.length > 0 ? videoTracks[0] : null;
|
|
666
|
+
}
|
|
667
|
+
function getMicrophoneTrack(stream) {
|
|
668
|
+
if (!stream) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
const audioTracks = stream.getAudioTracks();
|
|
672
|
+
return audioTracks.length > 0 ? audioTracks[0] : null;
|
|
673
|
+
}
|
|
674
|
+
function getMediaState(stream) {
|
|
675
|
+
const videoTrack = getCameraTrack(stream);
|
|
676
|
+
const audioTrack = getMicrophoneTrack(stream);
|
|
677
|
+
return {
|
|
678
|
+
hasStream: stream !== null,
|
|
679
|
+
camera: {
|
|
680
|
+
available: videoTrack !== null,
|
|
681
|
+
enabled: videoTrack?.enabled ?? false,
|
|
682
|
+
muted: videoTrack?.muted ?? true,
|
|
683
|
+
readyState: videoTrack?.readyState ?? null
|
|
684
|
+
},
|
|
685
|
+
microphone: {
|
|
686
|
+
available: audioTrack !== null,
|
|
687
|
+
enabled: audioTrack?.enabled ?? false,
|
|
688
|
+
muted: audioTrack?.muted ?? true,
|
|
689
|
+
readyState: audioTrack?.readyState ?? null
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function observeMediaState(stream, callback) {
|
|
694
|
+
if (!stream) {
|
|
695
|
+
callback(getMediaState(null));
|
|
696
|
+
return () => {
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
callback(getMediaState(stream));
|
|
700
|
+
const trackHandlers = /* @__PURE__ */ new Map();
|
|
701
|
+
const updateState = () => {
|
|
702
|
+
callback(getMediaState(stream));
|
|
703
|
+
};
|
|
704
|
+
const allTracks = [...stream.getAudioTracks(), ...stream.getVideoTracks()];
|
|
705
|
+
allTracks.forEach((track) => {
|
|
706
|
+
const handleMute = () => updateState();
|
|
707
|
+
const handleUnmute = () => updateState();
|
|
708
|
+
const handleEnded = () => updateState();
|
|
709
|
+
track.addEventListener("mute", handleMute);
|
|
710
|
+
track.addEventListener("unmute", handleUnmute);
|
|
711
|
+
track.addEventListener("ended", handleEnded);
|
|
712
|
+
trackHandlers.set(track, () => {
|
|
713
|
+
track.removeEventListener("mute", handleMute);
|
|
714
|
+
track.removeEventListener("unmute", handleUnmute);
|
|
715
|
+
track.removeEventListener("ended", handleEnded);
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
return () => {
|
|
719
|
+
trackHandlers.forEach((cleanup) => cleanup());
|
|
720
|
+
trackHandlers.clear();
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export { MediaManager, PeerManager, getCameraTrack, getMediaState, getMicrophoneTrack, isCameraOff, isMicrophoneOff, observeMediaState };
|
|
725
|
+
//# sourceMappingURL=index.js.map
|
|
726
|
+
//# sourceMappingURL=index.js.map
|