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/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