mitmi 1.1.7 → 1.1.9

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/src/Stream.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { Conference } from "./Conference";
2
2
  import { setLocalStream } from "./utils";
3
3
 
4
+ /**
5
+ * Stream constraints
6
+ */
4
7
  export interface StreamParams {
5
8
  audio: boolean;
6
9
  video: boolean;
7
10
  }
8
11
 
12
+ /**
13
+ * This class represent a video, camera or screenshare.
14
+ */
9
15
  class Stream {
10
16
  mediastream: MediaStream;
11
17
  domElement: undefined | HTMLVideoElement;
@@ -17,10 +23,14 @@ class Stream {
17
23
  params: StreamParams;
18
24
 
19
25
  /**
26
+ * Create a Stream
27
+ *
28
+ * @param mediastream - The video input to attach to the stream
29
+ * @param ownerId - The ownerId of the mediastream
30
+ * @param ownerName - The owner of the mediastream
20
31
  *
21
- * @param mediastream
22
- * @param owner "" => ourself, id instead
23
32
  */
33
+ //TODO Enlever le fait que ownerid est une string vide si l'owner est soit meme
24
34
  constructor(mediastream: MediaStream, ownerId: string, ownerName: string) {
25
35
  this.mediastream = mediastream;
26
36
  this.ownerId = ownerId;
@@ -29,15 +39,24 @@ class Stream {
29
39
  this.params = { audio: true, video: false };
30
40
  }
31
41
 
42
+ /**
43
+ * Check if the stream is local
44
+ *
45
+ * @returns - True if it's a localstream, false instead
46
+ */
32
47
  isLocal(): boolean {
33
48
  return this.ownerId === "";
34
49
  }
35
50
 
36
51
  /**
37
- * Will update lastest camera stream known (for DeviceMananger)
38
- * @param video
39
- * @param audio
40
- * @returns
52
+ * Get camera input
53
+ *
54
+ * @param video - Is video enable ?
55
+ * @param audio - Is audio enable ?
56
+ * @param audioDeviceId optional - Use a specific audio device
57
+ * @param videoDeviceId optional - Use a specific video device
58
+ *
59
+ * @returns A stream with your camera.
41
60
  */
42
61
  static async getCamera(
43
62
  video: boolean,
@@ -65,82 +84,97 @@ class Stream {
65
84
  }
66
85
  static getScreen() {}
67
86
 
87
+ /**
88
+ * Attach the stream to your DOM
89
+ *
90
+ * @param domElement - The HTML element to attach the stream
91
+ */
68
92
  attachToElement(domElement: HTMLVideoElement): void {
69
93
  this.domElement = domElement;
70
94
  domElement.srcObject = this.mediastream;
71
95
 
72
96
  if (this.isLocal()) {
73
- this.disableAudio();
97
+ this.localMuteAudio();
74
98
  }
75
99
  }
76
100
 
101
+ /**
102
+ * Detach the stream to your DOM
103
+ */
77
104
  detachToElement(): void {
78
105
  if (!this.domElement) return;
79
106
  this.domElement.srcObject = null;
80
107
  }
81
108
 
82
- /**
83
- * If the stream is published (so its yours) :
84
- * - video will be disabled for everyone
85
- * If the stream is not published (its yours but not publish, or other ppl stream):
86
- * - video will be disabled for you only
87
- */
88
- muteVideo() {
89
- this.params.video = false;
109
+ globalMuteVideo() {
90
110
  if (this.conferencePublish) {
91
- //your publish stream
92
- this.conferencePublish.session.socketInteraction.setConstraint(this);
111
+ this.params.video = false;
112
+ //this.conferencePublish.session.socketInteraction.setConstraint(this);
113
+ this.mediastream.getVideoTracks()[0].enabled = false;
93
114
  }
94
- this.mediastream.getVideoTracks()[0].enabled = false;
95
115
  }
96
116
 
97
- unmuteVideo() {
98
- this.params.video = true;
117
+ globalUnmuteVideo() {
99
118
  if (this.conferencePublish) {
100
- //your publish stream
101
- this.conferencePublish.session.socketInteraction.setConstraint(this);
119
+ this.params.video = true;
120
+ //this.conferencePublish.session.socketInteraction.setConstraint(this);
121
+ this.mediastream.getVideoTracks()[0].enabled = true;
102
122
  }
103
- this.mediastream.getVideoTracks()[0].enabled = true;
104
123
  }
105
124
 
106
- muteAudio(): void {
107
- this.params.audio = false;
125
+ globalMuteAudio(): void {
108
126
  if (this.conferencePublish) {
109
- this.conferencePublish.session.socketInteraction.setConstraint(this);
127
+ this.params.audio = false;
128
+ //this.conferencePublish.session.socketInteraction.setConstraint(this);
129
+ this.mediastream.getAudioTracks()[0].enabled = false;
110
130
  }
111
- this.mediastream.getAudioTracks()[0].enabled = false;
112
131
  }
113
132
 
114
- unmuteAudio(): void {
115
- this.params.audio = true;
133
+ globalUnmuteAudio(): void {
116
134
  if (this.conferencePublish) {
117
- this.conferencePublish.session.socketInteraction.setConstraint(this);
135
+ this.params.audio = true;
136
+ //this.conferencePublish.session.socketInteraction.setConstraint(this);
137
+ this.mediastream.getAudioTracks()[0].enabled = true;
118
138
  }
119
- this.mediastream.getAudioTracks()[0].enabled = true;
120
139
  }
121
140
 
122
141
  /**
123
- * This function exist to avoir Larsen.
124
- * You need to call here when a localstream is started.
142
+ * Disable local audio
125
143
  */
126
- disableAudio(): void {
144
+ localMuteAudio(): void {
127
145
  if (!this.domElement) return;
128
146
 
129
- // Important: cette ligne n'affecte pas l'envoi audio
130
147
  this.domElement.muted = true;
131
148
  this.domElement.volume = 0;
132
149
  }
133
150
 
134
151
  /**
135
- * TODO do better
152
+ * Enable local audio
136
153
  */
137
- enableAudio(): void {
154
+ localUnmuteAudio(): void {
138
155
  if (!this.domElement) return;
139
156
 
140
- // Important: cette ligne n'affecte pas l'envoi audio
141
157
  this.domElement.muted = false;
142
158
  this.domElement.volume = 1;
143
159
  }
160
+
161
+ /**
162
+ * Disable local video
163
+ */
164
+ localMuteVideo(): void {
165
+ if (!this.domElement) return;
166
+
167
+ this.domElement.pause();
168
+ }
169
+
170
+ /**
171
+ * Enable local video
172
+ */
173
+ async localUnmuteVideo(): Promise<void> {
174
+ if (!this.domElement) return;
175
+
176
+ await this.domElement.play();
177
+ }
144
178
  }
145
179
 
146
180
  export { Stream };
@@ -2,8 +2,11 @@ import { io, Socket } from "socket.io-client";
2
2
  import { serverUrl } from "../constants";
3
3
  import { getCurrentSession } from "../utils";
4
4
  import { Stream, StreamParams } from "../Stream";
5
- import { ContactInfo } from "../utils";
5
+ import { ContactInfo } from "../Contact";
6
6
 
7
+ /**
8
+ * Type of message between server and client
9
+ */
7
10
  interface SocketMessage {
8
11
  from: ContactInfo;
9
12
  target?: string;
@@ -16,6 +19,12 @@ interface SocketMessage {
16
19
  };
17
20
  }
18
21
 
22
+ /**
23
+ * Main interaction between socket and server
24
+ *
25
+ * @private
26
+ *
27
+ */
19
28
  export class SocketInteraction extends EventTarget {
20
29
  private socket!: Socket;
21
30
  private _userId?: string;
@@ -24,6 +33,10 @@ export class SocketInteraction extends EventTarget {
24
33
  private senders: Array<RTCRtpSender> = [];
25
34
 
26
35
  private peerConnections: Record<string, RTCPeerConnection> = {};
36
+ private pendingCandidates: Record<
37
+ string,
38
+ Array<RTCIceCandidate | RTCIceCandidateInit>
39
+ > = {};
27
40
 
28
41
  async init(): Promise<string> {
29
42
  this.socket = io(serverUrl, {});
@@ -45,6 +58,12 @@ export class SocketInteraction extends EventTarget {
45
58
  return this._userId;
46
59
  }
47
60
 
61
+ /**
62
+ * Publish a Stream into all peers
63
+ *
64
+ * @param stream - Stream to publish
65
+ */
66
+ //TODO Check to publish multiple stream, ATM => one at the moment
48
67
  publish(stream: Stream) {
49
68
  this.localStream = stream;
50
69
 
@@ -55,6 +74,11 @@ export class SocketInteraction extends EventTarget {
55
74
  console.log("[RTC] Stream published to all peers");
56
75
  }
57
76
 
77
+ /**
78
+ * Unbuplish stream into all peers
79
+ *
80
+ * @param stream - Stream to unpublish
81
+ */
58
82
  unpublish(stream: Stream) {
59
83
  if (this.localStream != stream) throw new Error("this is not your stream");
60
84
  this.localStream = undefined;
@@ -67,8 +91,10 @@ export class SocketInteraction extends EventTarget {
67
91
  }
68
92
 
69
93
  /**
94
+ * Set constraint to you stream ex : muteAudio
95
+ * Stream have stream.params, use to set constraint on the peer
70
96
  *
71
- * @param stream Stream have stream.params, use to set constraint on the peer
97
+ * @param stream - Affected Stream with constraints
72
98
  */
73
99
  setConstraint(stream: Stream) {
74
100
  if (this.localStream != stream) throw new Error("this is not your stream");
@@ -91,6 +117,13 @@ export class SocketInteraction extends EventTarget {
91
117
  console.log("[RTC] Set constraint");
92
118
  }
93
119
 
120
+ /**
121
+ * Attach a stream to a peer
122
+ *
123
+ * @param pc - PeerConnection to attach this.localstream
124
+ * @returns
125
+ */
126
+ //TODO don't use this.localstream, but a parameter instead ?
94
127
  private attachStreamToPeer(pc: RTCPeerConnection) {
95
128
  if (!this.localStream) return;
96
129
 
@@ -100,6 +133,13 @@ export class SocketInteraction extends EventTarget {
100
133
  });
101
134
  }
102
135
 
136
+ /**
137
+ * Detach a stream to a peer
138
+ *
139
+ * @param pc - PeerConnection to detach this.localstream
140
+ * @returns
141
+ */
142
+ //TODO don't use this.localstream, but a parameter instead ?
103
143
  private removeStreamToPeer(pc: RTCPeerConnection) {
104
144
  if (!this.localStream) return;
105
145
 
@@ -108,6 +148,13 @@ export class SocketInteraction extends EventTarget {
108
148
  });
109
149
  }
110
150
 
151
+ /**
152
+ * Stop sending a specific track
153
+ *
154
+ * @param pc - Affected Peerconnection
155
+ * @param track - Track to disable
156
+ * @returns
157
+ */
111
158
  private disableTrackToPeer(pc: RTCPeerConnection, track: MediaStreamTrack) {
112
159
  if (!this.localStream) return;
113
160
 
@@ -120,6 +167,13 @@ export class SocketInteraction extends EventTarget {
120
167
  });
121
168
  }
122
169
 
170
+ /**
171
+ * Activate a track into a peer
172
+ *
173
+ * @param pc - Affected Peerconnection
174
+ * @param track - Track to enable
175
+ * @returns
176
+ */
123
177
  private enableTrackToPeer(pc: RTCPeerConnection, track: MediaStreamTrack) {
124
178
  if (!this.localStream) return;
125
179
 
@@ -132,6 +186,11 @@ export class SocketInteraction extends EventTarget {
132
186
  });
133
187
  }
134
188
 
189
+ /**
190
+ * Send a join message to the server
191
+ *
192
+ * @param confId - ID conference
193
+ */
135
194
  register(confId: number) {
136
195
  /*if (!this.publishStream) {
137
196
  throw new Error("Call publish() before register()");
@@ -147,7 +206,24 @@ export class SocketInteraction extends EventTarget {
147
206
  console.log(`[CONF] Join request sent for room ${confId}`);
148
207
  }
149
208
 
209
+ /**
210
+ * Disconnect
211
+ */
150
212
  unregister() {
213
+ //Stop all the track before (release camera and microphone)
214
+ if (this.localStream) {
215
+ this.localStream.mediastream.getTracks().forEach((track) => track.stop());
216
+ }
217
+
218
+ const sender = getCurrentSession()?.contact!;
219
+ this.sendMessage({
220
+ from: sender.toString(),
221
+ payload: {
222
+ action: "close",
223
+ disconnect: sender.toString().id,
224
+ },
225
+ });
226
+
151
227
  Object.values(this.peerConnections).forEach((pc) => pc.close());
152
228
  this.peerConnections = {};
153
229
 
@@ -157,6 +233,11 @@ export class SocketInteraction extends EventTarget {
157
233
  console.log("[CONF] Unregistered and socket closed");
158
234
  }
159
235
 
236
+ /**
237
+ * Set Listeners
238
+ *
239
+ * @private
240
+ */
160
241
  private setupSocketListeners() {
161
242
  this.socket.on("message", async (message: SocketMessage) => {
162
243
  if (!this._confId) return;
@@ -165,44 +246,70 @@ export class SocketInteraction extends EventTarget {
165
246
 
166
247
  switch (payload.action) {
167
248
  case "join":
168
- console.log(`[RTC] Join received from ${from}`);
249
+ console.log(`[RTC] Join received from ${from.name}`);
169
250
  await this.createPeerConnection(from, true);
170
251
  break;
171
252
 
172
253
  case "offer":
173
- console.log(`[RTC] Offer received from ${from}`);
254
+ console.log(`[RTC] Offer received from ${from.name}`);
174
255
  await this.handleOffer(from, payload.sdp!);
175
256
  break;
176
257
 
177
258
  case "answer":
178
- console.log(`[RTC] Answer received from ${from}`);
259
+ console.log(`[RTC] Answer received from ${from.name}`);
179
260
  await this.handleAnswer(from, payload.sdp!);
180
261
  break;
181
262
 
182
263
  case "ice":
183
- console.log(`[RTC] ICE received from ${from}`);
264
+ console.log(`[RTC] ICE received from ${from.name}`);
184
265
  await this.handleIce(from, payload.candidate!);
185
266
  break;
186
267
 
187
268
  case "close":
188
- console.log(`[RTC] Peer ${payload.disconnect} disconnected`);
189
- this.removePeer(payload.disconnect!);
190
- this.dispatchEvent(
191
- new CustomEvent("peopleLeave", {
192
- detail: { leaveId: payload.disconnect },
193
- })
194
- );
195
- console.log("[Socket] leave" + payload.disconnect);
196
-
269
+ if (from) {
270
+ //Normal way
271
+ console.log(`[RTC] Peer ${from.name} disconnected`);
272
+ this.removePeer(payload.disconnect!);
273
+ this.dispatchEvent(
274
+ new CustomEvent("peopleLeave", {
275
+ detail: { leaveId: payload.disconnect, name: from.name },
276
+ })
277
+ );
278
+ } else {
279
+ //Message send by server
280
+ //Normal way
281
+ console.log(`[RTC] Someone disconnected`);
282
+ this.removePeer(payload.disconnect!);
283
+ this.dispatchEvent(
284
+ new CustomEvent("peopleLeave", {
285
+ detail: {
286
+ leaveId: payload.disconnect,
287
+ name: "Call conference.leave()",
288
+ },
289
+ })
290
+ );
291
+ }
197
292
  break;
198
293
  }
199
294
  });
200
295
  }
201
296
 
202
- private async createPeerConnection(from: ContactInfo, initiator: boolean) {
297
+ /**
298
+ * Create a Peerconnection with the remote contact
299
+ *
300
+ * @param from - Peerconnection with this contact
301
+ * @param initiator - Are you the applicant ? IF yes send an Offer
302
+ * @returns
303
+ */
304
+ private async createPeerConnection(
305
+ from: ContactInfo,
306
+ initiator: boolean
307
+ ): Promise<RTCPeerConnection> {
203
308
  const remoteUserId = from.id;
204
309
  const remoteUserName = from.name;
205
- if (this.peerConnections[remoteUserId]) return; //existe deja
310
+ if (this.peerConnections[remoteUserId]) {
311
+ return this.peerConnections[remoteUserId]; // Already exist
312
+ }
206
313
 
207
314
  const pc = new RTCPeerConnection();
208
315
  this.peerConnections[remoteUserId] = pc;
@@ -247,14 +354,27 @@ export class SocketInteraction extends EventTarget {
247
354
  },
248
355
  });
249
356
  }
357
+
358
+ return pc;
250
359
  }
251
360
 
361
+ /**
362
+ * Event handle the offer
363
+ *
364
+ * @param from - Contact who send the offer
365
+ * @param sdp - Offer
366
+ */
252
367
  private async handleOffer(from: ContactInfo, sdp: RTCSessionDescriptionInit) {
253
368
  const remoteUserId = from.id;
254
- await this.createPeerConnection(from, false);
369
+ const pc = await this.createPeerConnection(from, false);
255
370
 
256
- const pc = this.peerConnections[remoteUserId];
257
- await pc.setRemoteDescription(new RTCSessionDescription(sdp));
371
+ //const pc = this.peerConnections[remoteUserId];
372
+
373
+ try {
374
+ await pc.setRemoteDescription(new RTCSessionDescription(sdp));
375
+ } catch (error) {
376
+ console.error("Remote decription error : ", error);
377
+ }
258
378
 
259
379
  const answer = await pc.createAnswer();
260
380
  await pc.setLocalDescription(answer);
@@ -269,6 +389,9 @@ export class SocketInteraction extends EventTarget {
269
389
  },
270
390
  });
271
391
 
392
+ // flush pending ICE candidates received before remoteDescription was set
393
+ await this.flushPendingCandidates(remoteUserId);
394
+
272
395
  const event: CustomEvent = new CustomEvent("newPeople", {
273
396
  detail: {
274
397
  contact: from,
@@ -277,6 +400,13 @@ export class SocketInteraction extends EventTarget {
277
400
  this.dispatchEvent(event);
278
401
  }
279
402
 
403
+ /**
404
+ * Event handle the enswer
405
+ *
406
+ * @param from - Contact who send tjhe answer
407
+ * @param sdp - The answer
408
+ * @returns
409
+ */
280
410
  private async handleAnswer(
281
411
  from: ContactInfo,
282
412
  sdp: RTCSessionDescriptionInit
@@ -286,6 +416,10 @@ export class SocketInteraction extends EventTarget {
286
416
  if (!pc) return;
287
417
 
288
418
  await pc.setRemoteDescription(new RTCSessionDescription(sdp));
419
+
420
+ // flush pending ICE candidates received before remoteDescription was set
421
+ await this.flushPendingCandidates(remoteUserId);
422
+
289
423
  const event: CustomEvent = new CustomEvent("newPeople", {
290
424
  detail: {
291
425
  contact: from,
@@ -294,19 +428,70 @@ export class SocketInteraction extends EventTarget {
294
428
  this.dispatchEvent(event);
295
429
  }
296
430
 
431
+ /**
432
+ * Event when receive ice candidates
433
+ *
434
+ * @param from - Contact
435
+ * @param candidate - Icecandidate
436
+ * @returns
437
+ */
297
438
  private async handleIce(from: ContactInfo, candidate: RTCIceCandidate) {
298
439
  const remoteUserId = from.id;
440
+ /*if (!pc) return;
441
+
442
+ try {
443
+ await pc.addIceCandidate(candidate);
444
+ } catch (error) {
445
+ console.error("Ice error : ", error);
446
+ console.error(pc);
447
+ }*/
299
448
  const pc = this.peerConnections[remoteUserId];
300
- if (!pc) return;
449
+ if (!pc || !pc.remoteDescription || !pc.remoteDescription.type) {
450
+ this.pendingCandidates[remoteUserId] =
451
+ this.pendingCandidates[remoteUserId] || [];
452
+ this.pendingCandidates[remoteUserId].push(candidate);
453
+ return;
454
+ }
301
455
 
302
- await pc.addIceCandidate(candidate);
456
+ try {
457
+ await pc.addIceCandidate(candidate);
458
+ } catch (error) {
459
+ console.error("Ice error : ", error);
460
+ }
303
461
  }
304
462
 
463
+ private async flushPendingCandidates(remoteUserId: string): Promise<void> {
464
+ const pc = this.peerConnections[remoteUserId];
465
+ if (!pc) return; // No remote description attach to this user
466
+
467
+ const pendingCandidates = this.pendingCandidates[remoteUserId];
468
+ if (!pendingCandidates || !pendingCandidates.length) return; // No pending candidates
469
+
470
+ for (const pendingCandidate of pendingCandidates) {
471
+ try {
472
+ await pc.addIceCandidate(pendingCandidate);
473
+ } catch (e) {
474
+ console.error("Error adding pending ICE candidate", e);
475
+ }
476
+ }
477
+ delete this.pendingCandidates[remoteUserId];
478
+ }
479
+
480
+ /**
481
+ * Event when receive a disconnection message
482
+ *
483
+ * @param remoteId - ID that leave
484
+ */
305
485
  private removePeer(remoteId: string) {
306
486
  this.peerConnections[remoteId]?.close();
307
487
  delete this.peerConnections[remoteId];
308
488
  }
309
489
 
490
+ /**
491
+ * Send a message on the socket
492
+ *
493
+ * @param msg - Message to send on the socket
494
+ */
310
495
  private sendMessage(msg: SocketMessage) {
311
496
  this.socket.emit("message", msg);
312
497
  }
package/src/index.ts CHANGED
@@ -4,3 +4,7 @@ export { HelloWorld } from "./HelloWorld";
4
4
  export { Stream } from "./Stream";
5
5
  export { Conference } from "./Conference";
6
6
  export { DeviceManager } from "./DeviceManager";
7
+
8
+ //Interfaces
9
+ export type { StreamParams } from "./Stream";
10
+ export type { ContactInfo } from "./Contact";
package/src/utils.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { Session } from "./Session";
2
2
  import { Stream } from "./Stream";
3
3
 
4
+ /**
5
+ * Generate a random string
6
+ *
7
+ * @returns - A random string format, ex: de0b1c44-b42e-c617-bcc2-14586c5fffe2
8
+ */
4
9
  function uidGenerator(): String {
5
10
  var S4 = function () {
6
11
  return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
@@ -21,6 +26,9 @@ function uidGenerator(): String {
21
26
  );
22
27
  }
23
28
 
29
+ /**
30
+ * Function for global session
31
+ */
24
32
  let currentSession: Session | undefined = undefined;
25
33
  function setCurrentSession(newSession: undefined | Session) {
26
34
  currentSession = newSession;
@@ -29,6 +37,10 @@ function getCurrentSession() {
29
37
  return currentSession;
30
38
  }
31
39
 
40
+ //TODO Useless ?
41
+ /**
42
+ * Function for localstream
43
+ */
32
44
  let localStream: Stream | undefined = undefined;
33
45
  function setLocalStream(newStream: undefined | Stream) {
34
46
  localStream = newStream;
@@ -37,12 +49,6 @@ function getLocalStream() {
37
49
  return localStream;
38
50
  }
39
51
 
40
- interface ContactInfo {
41
- id: string;
42
- name: string;
43
- }
44
-
45
- export { ContactInfo };
46
52
  export { setCurrentSession };
47
53
  export { getCurrentSession };
48
54
  export { setLocalStream };