1 // Copyright (c) 2020 by Juliusz Chroboczek.
  2 
  3 // Permission is hereby granted, free of charge, to any person obtaining a copy
  4 // of this software and associated documentation files (the "Software"), to deal
  5 // in the Software without restriction, including without limitation the rights
  6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 // copies of the Software, and to permit persons to whom the Software is
  8 // furnished to do so, subject to the following conditions:
  9 //
 10 // The above copyright notice and this permission notice shall be included in
 11 // all copies or substantial portions of the Software.
 12 //
 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 19 // THE SOFTWARE.
 20 
 21 'use strict';
 22 
 23 /**
 24  * toHex formats an array as a hexadecimal string.
 25  * @param {number[]|Uint8Array} array - the array to format
 26  * @returns {string} - the hexadecimal representation of array
 27  */
 28 function toHex(array) {
 29     let a = new Uint8Array(array);
 30     function hex(x) {
 31         let h = x.toString(16);
 32         if(h.length < 2)
 33             h = '0' + h;
 34         return h;
 35     }
 36     return a.reduce((x, y) => x + hex(y), '');
 37 }
 38 
 39 /**
 40  * newRandomId returns a random string of 32 hex digits (16 bytes).
 41  *
 42  * @returns {string}
 43  */
 44 function newRandomId() {
 45     let a = new Uint8Array(16);
 46     crypto.getRandomValues(a);
 47     return toHex(a);
 48 }
 49 
 50 let localIdCounter = 0;
 51 
 52 /**
 53  * newLocalId returns a string that is unique in this session.
 54  *
 55  * @returns {string}
 56  */
 57 function newLocalId() {
 58     let id = `${localIdCounter}`
 59     localIdCounter++;
 60     return id;
 61 }
 62 
 63 /**
 64  * @typedef {Object} user
 65  * @property {string} username
 66  * @property {Object<string,boolean>} permissions
 67  * @property {Object<string,any>} status
 68  * @property {Object<string,Object<string,boolean>>} down
 69  */
 70 
 71 /**
 72  * ServerConnection encapsulates a websocket connection to the server and
 73  * all the associated streams.
 74  * @constructor
 75  */
 76 function ServerConnection() {
 77     /**
 78      * The id of this connection.
 79      *
 80      * @type {string}
 81      * @const
 82      */
 83     this.id = newRandomId();
 84     /**
 85      * The group that we have joined, or null if we haven't joined yet.
 86      *
 87      * @type {string}
 88      */
 89     this.group = null;
 90     /**
 91      * The username we joined as.
 92      *
 93      * @type {string}
 94      */
 95     this.username = null;
 96     /**
 97      * The set of users in this group, including ourself.
 98      *
 99      * @type {Object<string,user>}
100      */
101     this.users = {};
102     /**
103      * The underlying websocket.
104      *
105      * @type {WebSocket}
106      */
107     this.socket = null;
108     /**
109      * The set of all up streams, indexed by their id.
110      *
111      * @type {Object<string,Stream>}
112      */
113     this.up = {};
114     /**
115      * The set of all down streams, indexed by their id.
116      *
117      * @type {Object<string,Stream>}
118      */
119     this.down = {};
120     /**
121      * The ICE configuration used by all associated streams.
122      *
123      * @type {RTCConfiguration}
124      */
125     this.rtcConfiguration = null;
126     /**
127      * The permissions granted to this connection.
128      *
129      * @type {Object<string,boolean>}
130      */
131     this.permissions = {};
132     /**
133      * userdata is a convenient place to attach data to a ServerConnection.
134      * It is not used by the library.
135      *
136      * @type{Object<unknown,unknown>}
137      */
138     this.userdata = {};
139 
140     /* Callbacks */
141 
142     /**
143      * onconnected is called when the connection has been established
144      *
145      * @type{(this: ServerConnection) => void}
146      */
147     this.onconnected = null;
148     /**
149      * onclose is called when the connection is closed
150      *
151      * @type{(this: ServerConnection, code: number, reason: string) => void}
152      */
153     this.onclose = null;
154     /**
155      * onuser is called whenever a user in the group changes.  The users
156      * array has already been updated.
157      *
158      * @type{(this: ServerConnection, id: string, kind: string) => void}
159      */
160     this.onuser = null;
161     /**
162      * onjoined is called whenever we join or leave a group or whenever the
163      * permissions we have in a group change.
164      *
165      * kind is one of 'join', 'fail', 'change' or 'leave'.
166      *
167      * @type{(this: ServerConnection, kind: string, group: string, permissions: Object<string,boolean>, status: Object<string,any>, message: string) => void}
168      */
169     this.onjoined = null;
170     /**
171      * ondownstream is called whenever a new down stream is added.  It
172      * should set up the stream's callbacks; actually setting up the UI
173      * should be done in the stream's ondowntrack callback.
174      *
175      * @type{(this: ServerConnection, stream: Stream) => void}
176      */
177     this.ondownstream = null;
178     /**
179      * onchat is called whenever a new chat message is received.
180      *
181      * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, history: boolean, kind: string, message: unknown) => void}
182      */
183     this.onchat = null;
184     /**
185      * onusermessage is called when an application-specific message is
186      * received.  Id is null when the message originated at the server,
187      * a user-id otherwise.
188      *
189      * 'kind' is typically one of 'error', 'warning', 'info' or 'mute'.  If
190      * 'id' is non-null, 'privileged' indicates whether the message was
191      * sent by an operator.
192      *
193      * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: unknown) => void}
194      */
195     this.onusermessage = null;
196 }
197 
198 /**
199   * @typedef {Object} message
200   * @property {string} type
201   * @property {string} [kind]
202   * @property {string} [id]
203   * @property {string} [replace]
204   * @property {string} [source]
205   * @property {string} [dest]
206   * @property {string} [username]
207   * @property {string} [password]
208   * @property {boolean} [privileged]
209   * @property {Object<string,boolean>} [permissions]
210   * @property {Object<string,any>} [status]
211   * @property {string} [group]
212   * @property {unknown} [value]
213   * @property {boolean} [noecho]
214   * @property {string} [sdp]
215   * @property {RTCIceCandidate} [candidate]
216   * @property {string} [label]
217   * @property {Object<string,Array<string>>|Array<string>} [request]
218   * @property {Object<string,any>} [rtcConfiguration]
219   */
220 
221 /**
222  * close forcibly closes a server connection.  The onclose callback will
223  * be called when the connection is effectively closed.
224  */
225 ServerConnection.prototype.close = function() {
226     this.socket && this.socket.close(1000, 'Close requested by client');
227     this.socket = null;
228 };
229 
230 /**
231   * send sends a message to the server.
232   * @param {message} m - the message to send.
233   */
234 ServerConnection.prototype.send = function(m) {
235     if(!this.socket || this.socket.readyState !== this.socket.OPEN) {
236         // send on a closed socket doesn't throw
237         throw(new Error('Connection is not open'));
238     }
239     return this.socket.send(JSON.stringify(m));
240 };
241 
242 /**
243  * connect connects to the server.
244  *
245  * @param {string} url - The URL to connect to.
246  * @returns {Promise<ServerConnection>}
247  * @function
248  */
249 ServerConnection.prototype.connect = async function(url) {
250     let sc = this;
251     if(sc.socket) {
252         sc.socket.close(1000, 'Reconnecting');
253         sc.socket = null;
254     }
255 
256     sc.socket = new WebSocket(url);
257 
258     return await new Promise((resolve, reject) => {
259         this.socket.onerror = function(e) {
260             reject(e);
261         };
262         this.socket.onopen = function(e) {
263             sc.send({
264                 type: 'handshake',
265                 id: sc.id,
266             });
267             if(sc.onconnected)
268                 sc.onconnected.call(sc);
269             resolve(sc);
270         };
271         this.socket.onclose = function(e) {
272             sc.permissions = {};
273             for(let id in sc.up) {
274                 let c = sc.up[id];
275                 c.close();
276             }
277             for(let id in sc.down) {
278                 let c = sc.down[id];
279                 c.close();
280             }
281             for(let id in sc.users) {
282                 delete(sc.users[id]);
283                 if(sc.onuser)
284                     sc.onuser.call(sc, id, 'delete');
285             }
286             if(sc.group && sc.onjoined)
287                 sc.onjoined.call(sc, 'leave', sc.group, {}, {}, '');
288             sc.group = null;
289             sc.username = null;
290             if(sc.onclose)
291                 sc.onclose.call(sc, e.code, e.reason);
292             reject(new Error('websocket close ' + e.code + ' ' + e.reason));
293         };
294         this.socket.onmessage = function(e) {
295             let m = JSON.parse(e.data);
296             switch(m.type) {
297             case 'handshake':
298                 break;
299             case 'offer':
300                 sc.gotOffer(m.id, m.label, m.source, m.username,
301                             m.sdp, m.replace);
302                 break;
303             case 'answer':
304                 sc.gotAnswer(m.id, m.sdp);
305                 break;
306             case 'renegotiate':
307                 sc.gotRenegotiate(m.id);
308                 break;
309             case 'close':
310                 sc.gotClose(m.id);
311                 break;
312             case 'abort':
313                 sc.gotAbort(m.id);
314                 break;
315             case 'ice':
316                 sc.gotRemoteIce(m.id, m.candidate);
317                 break;
318             case 'joined':
319                 if(sc.group) {
320                     if(m.group !== sc.group) {
321                         throw new Error('Joined multiple groups');
322                     }
323                 } else {
324                     sc.group = m.group;
325                 }
326                 sc.username = m.username;
327                 sc.permissions = m.permissions || [];
328                 sc.rtcConfiguration = m.rtcConfiguration || null;
329                 if(m.kind == 'leave') {
330                     for(let id in sc.users) {
331                         delete(sc.users[id]);
332                         if(sc.onuser)
333                             sc.onuser.call(sc, id, 'delete');
334                     }
335                 }
336                 if(sc.onjoined)
337                     sc.onjoined.call(sc, m.kind, m.group,
338                                      m.permissions || {},
339                                      m.status,
340                                      m.value || null);
341                 break;
342             case 'user':
343                 switch(m.kind) {
344                 case 'add':
345                     if(m.id in sc.users)
346                         console.warn(`Duplicate user ${m.id} ${m.username}`);
347                     sc.users[m.id] = {
348                         username: m.username,
349                         permissions: m.permissions || {},
350                         status: m.status || {},
351                         down: {},
352                     };
353                     break;
354                 case 'change':
355                     if(!(m.id in sc.users)) {
356                         console.warn(`Unknown user ${m.id} ${m.username}`);
357                         sc.users[m.id] = {
358                             username: m.username,
359                             permissions: m.permissions || {},
360                             status: m.status || {},
361                             down: {},
362                         };
363                     } else {
364                         sc.users[m.id].username = m.username;
365                         sc.users[m.id].permissions = m.permissions || {};
366                         sc.users[m.id].status = m.status || {};
367                     }
368                     break;
369                 case 'delete':
370                     if(!(m.id in sc.users))
371                         console.warn(`Unknown user ${m.id} ${m.username}`);
372                     delete(sc.users[m.id]);
373                     break;
374                 default:
375                     console.warn(`Unknown user action ${m.kind}`);
376                     return;
377                 }
378                 if(sc.onuser)
379                     sc.onuser.call(sc, m.id, m.kind);
380                 break;
381             case 'chat':
382             case 'chathistory':
383                 if(sc.onchat)
384                     sc.onchat.call(
385                         sc, m.source, m.dest, m.username, m.time, m.privileged,
386                         m.type === 'chathistory', m.kind, m.value,
387                     );
388                 break;
389             case 'usermessage':
390                 if(sc.onusermessage)
391                     sc.onusermessage.call(
392                         sc, m.source, m.dest, m.username, m.time,
393                         m.privileged, m.kind, m.value,
394                     );
395                 break;
396             case 'ping':
397                 sc.send({
398                     type: 'pong',
399                 });
400                 break;
401             case 'pong':
402                 /* nothing */
403                 break;
404             default:
405                 console.warn('Unexpected server message', m.type);
406                 return;
407             }
408         };
409     });
410 };
411 
412 /**
413  * join requests to join a group.  The onjoined callback will be called
414  * when we've effectively joined.
415  *
416  * @param {string} group - The name of the group to join.
417  * @param {string} username - the username to join as.
418  * @param {string} password - the password.
419  */
420 ServerConnection.prototype.join = function(group, username, password) {
421     this.send({
422         type: 'join',
423         kind: 'join',
424         group: group,
425         username: username,
426         password: password,
427     });
428 };
429 
430 /**
431  * leave leaves a group.  The onjoined callback will be called when we've
432  * effectively left.
433  *
434  * @param {string} group - The name of the group to join.
435  */
436 ServerConnection.prototype.leave = function(group) {
437     this.send({
438         type: 'join',
439         kind: 'leave',
440         group: group,
441     });
442 };
443 
444 /**
445  * request sets the list of requested tracks
446  *
447  * @param {Object<string,Array<string>>} what
448  *     - A dictionary that maps labels to a sequence of 'audio', 'video'
449  *       or 'video-low.  An entry with an empty label '' provides the default.
450  */
451 ServerConnection.prototype.request = function(what) {
452     this.send({
453         type: 'request',
454         request: what,
455     });
456 };
457 
458 /**
459  * @param {string} localId
460  * @returns {Stream}
461  */
462 ServerConnection.prototype.findByLocalId = function(localId) {
463     if(!localId)
464         return null;
465 
466     let sc = this;
467 
468     for(let id in sc.up) {
469         let s = sc.up[id];
470         if(s.localId === localId)
471             return s;
472     }
473     return null;
474 }
475 
476 /**
477  * newUpStream requests the creation of a new up stream.
478  *
479  * @param {string} [localId]
480  *   - The local id of the stream to create.  If a stream already exists with
481  *     the same local id, it is replaced with the new stream.
482  * @returns {Stream}
483  */
484 ServerConnection.prototype.newUpStream = function(localId) {
485     let sc = this;
486     let id = newRandomId();
487     if(sc.up[id])
488         throw new Error('Eek!');
489 
490     if(typeof RTCPeerConnection === 'undefined')
491         throw new Error("This browser doesn't support WebRTC");
492 
493     let pc = new RTCPeerConnection(sc.rtcConfiguration);
494     if(!pc)
495         throw new Error("Couldn't create peer connection");
496 
497     let oldId = null;
498     if(localId) {
499         let old = sc.findByLocalId(localId);
500         oldId = old && old.id;
501         if(old)
502             old.close(true);
503     }
504 
505     let c = new Stream(this, id, localId || newLocalId(), pc, true);
506     if(oldId)
507         c.replace = oldId;
508     sc.up[id] = c;
509 
510     pc.onnegotiationneeded = async e => {
511             await c.negotiate();
512     };
513 
514     pc.onicecandidate = e => {
515         if(!e.candidate)
516             return;
517         c.gotLocalIce(e.candidate);
518     };
519 
520     pc.oniceconnectionstatechange = e => {
521         if(c.onstatus)
522             c.onstatus.call(c, pc.iceConnectionState);
523         if(pc.iceConnectionState === 'failed')
524             c.restartIce();
525     };
526 
527     pc.ontrack = console.error;
528 
529     return c;
530 };
531 
532 /**
533  * chat sends a chat message to the server.  The server will normally echo
534  * the message back to the client.
535  *
536  * @param {string} kind
537  *     -  The kind of message, either '', 'me' or an application-specific type.
538  * @param {string} dest - The id to send the message to, empty for broadcast.
539  * @param {string} value - The text of the message.
540  */
541 ServerConnection.prototype.chat = function(kind, dest, value) {
542     this.send({
543         type: 'chat',
544         source: this.id,
545         dest: dest,
546         username: this.username,
547         kind: kind,
548         value: value,
549     });
550 };
551 
552 /**
553  * userAction sends a request to act on a user.
554  *
555  * @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
556  * @param {string} dest - The id of the user to act upon.
557  * @param {any} [value] - An action-dependent parameter.
558  */
559 ServerConnection.prototype.userAction = function(kind, dest, value) {
560     this.send({
561         type: 'useraction',
562         source: this.id,
563         dest: dest,
564         username: this.username,
565         kind: kind,
566         value: value,
567     });
568 };
569 
570 /**
571  * userMessage sends an application-specific message to a user.
572  * This is similar to a chat message, but is not saved in the chat history.
573  *
574  * @param {string} kind - The kind of application-specific message.
575  * @param {string} dest - The id to send the message to, empty for broadcast.
576  * @param {unknown} [value] - An optional parameter.
577  * @param {boolean} [noecho] - If set, don't echo back the message to the sender.
578  */
579 ServerConnection.prototype.userMessage = function(kind, dest, value, noecho) {
580     this.send({
581         type: 'usermessage',
582         source: this.id,
583         dest: dest,
584         username: this.username,
585         kind: kind,
586         value: value,
587         noecho: noecho,
588     });
589 };
590 
591 /**
592  * groupAction sends a request to act on the current group.
593  *
594  * @param {string} kind
595  *     - One of 'clearchat', 'lock', 'unlock', 'record' or 'unrecord'.
596  * @param {string} [message] - An optional user-readable message.
597  */
598 ServerConnection.prototype.groupAction = function(kind, message) {
599     this.send({
600         type: 'groupaction',
601         source: this.id,
602         kind: kind,
603         username: this.username,
604         value: message,
605     });
606 };
607 
608 /**
609  * Called when we receive an offer from the server.  Don't call this.
610  *
611  * @param {string} id
612  * @param {string} label
613  * @param {string} source
614  * @param {string} username
615  * @param {string} sdp
616  * @param {string} replace
617  * @function
618  */
619 ServerConnection.prototype.gotOffer = async function(id, label, source, username, sdp, replace) {
620     let sc = this;
621 
622     if(sc.up[id]) {
623         console.error("Duplicate connection id");
624         sc.send({
625             type: 'abort',
626             id: id,
627         });
628         return;
629     }
630 
631     let oldLocalId = null;
632 
633     if(replace) {
634         let old = sc.down[replace];
635         if(old) {
636             oldLocalId = old.localId;
637             old.close(true);
638         } else
639             console.error("Replacing unknown stream");
640     }
641 
642     let c = sc.down[id];
643     if(c && oldLocalId)
644         console.error("Replacing duplicate stream");
645 
646     if(!c) {
647         let pc;
648         try {
649             pc = new RTCPeerConnection(sc.rtcConfiguration);
650         } catch(e) {
651             console.error(e);
652             sc.send({
653                 type: 'abort',
654                 id: id,
655             });
656             return;
657         }
658         c = new Stream(this, id, oldLocalId || newLocalId(), pc, false);
659         c.label = label;
660         sc.down[id] = c;
661 
662         c.pc.onicecandidate = function(e) {
663             if(!e.candidate)
664                 return;
665             c.gotLocalIce(e.candidate);
666         };
667 
668         pc.oniceconnectionstatechange = e => {
669             if(c.onstatus)
670                 c.onstatus.call(c, pc.iceConnectionState);
671             if(pc.iceConnectionState === 'failed') {
672                 sc.send({
673                     type: 'renegotiate',
674                     id: id,
675                 });
676             }
677         };
678 
679         c.pc.ontrack = function(e) {
680             if(e.streams.length < 1) {
681                 console.error("Got track with no stream");
682                 return;
683             }
684             c.stream = e.streams[0];
685             let changed = recomputeUserStreams(sc, source, c);
686             if(c.ondowntrack) {
687                 c.ondowntrack.call(
688                     c, e.track, e.transceiver, e.streams[0],
689                 );
690             }
691             if(changed && sc.onuser)
692                 sc.onuser.call(sc, source, "change");
693         };
694     }
695 
696     c.source = source;
697     c.username = username;
698 
699     if(sc.ondownstream)
700         sc.ondownstream.call(sc, c);
701 
702     try {
703         await c.pc.setRemoteDescription({
704             type: 'offer',
705             sdp: sdp,
706         });
707 
708         await c.flushRemoteIceCandidates();
709 
710         let answer = await c.pc.createAnswer();
711         if(!answer)
712             throw new Error("Didn't create answer");
713         await c.pc.setLocalDescription(answer);
714         this.send({
715             type: 'answer',
716             id: id,
717             sdp: c.pc.localDescription.sdp,
718         });
719     } catch(e) {
720         try {
721             if(c.onerror)
722                 c.onerror.call(c, e);
723         } finally {
724             c.abort();
725         }
726         return;
727     }
728 
729     c.localDescriptionSent = true;
730     c.flushLocalIceCandidates();
731     if(c.onnegotiationcompleted)
732         c.onnegotiationcompleted.call(c);
733 };
734 
735 /**
736  * Called when we receive an answer from the server.  Don't call this.
737  *
738  * @param {string} id
739  * @param {string} sdp
740  * @function
741  */
742 ServerConnection.prototype.gotAnswer = async function(id, sdp) {
743     let c = this.up[id];
744     if(!c)
745         throw new Error('unknown up stream');
746     try {
747         await c.pc.setRemoteDescription({
748             type: 'answer',
749             sdp: sdp,
750         });
751     } catch(e) {
752         try {
753             if(c.onerror)
754                 c.onerror.call(c, e);
755         } finally {
756             c.close();
757         }
758         return;
759     }
760     await c.flushRemoteIceCandidates();
761     if(c.onnegotiationcompleted)
762         c.onnegotiationcompleted.call(c);
763 };
764 
765 /**
766  * Called when we receive a renegotiation request from the server.  Don't
767  * call this.
768  *
769  * @param {string} id
770  * @function
771  */
772 ServerConnection.prototype.gotRenegotiate = function(id) {
773     let c = this.up[id];
774     if(!c)
775         throw new Error('unknown up stream');
776     c.restartIce();
777 };
778 
779 /**
780  * Called when we receive a close request from the server.  Don't call this.
781  *
782  * @param {string} id
783  */
784 ServerConnection.prototype.gotClose = function(id) {
785     let c = this.down[id];
786     if(!c)
787         console.warn('unknown down stream', id);
788     c.close();
789 };
790 
791 /**
792  * Called when we receive an abort message from the server.  Don't call this.
793  *
794  * @param {string} id
795  */
796 ServerConnection.prototype.gotAbort = function(id) {
797     let c = this.up[id];
798     if(!c)
799         throw new Error('unknown up stream');
800     c.close();
801 };
802 
803 /**
804  * Called when we receive an ICE candidate from the server.  Don't call this.
805  *
806  * @param {string} id
807  * @param {RTCIceCandidate} candidate
808  * @function
809  */
810 ServerConnection.prototype.gotRemoteIce = async function(id, candidate) {
811     let c = this.up[id];
812     if(!c)
813         c = this.down[id];
814     if(!c)
815         throw new Error('unknown stream');
816     if(c.pc.remoteDescription)
817         await c.pc.addIceCandidate(candidate).catch(console.warn);
818     else
819         c.remoteIceCandidates.push(candidate);
820 };
821 
822 /**
823  * Stream encapsulates a MediaStream, a set of tracks.
824  *
825  * A stream is said to go "up" if it is from the client to the server, and
826  * "down" otherwise.
827  *
828  * @param {ServerConnection} sc
829  * @param {string} id
830  * @param {string} localId
831  * @param {RTCPeerConnection} pc
832  *
833  * @constructor
834  */
835 function Stream(sc, id, localId, pc, up) {
836     /**
837      * The associated ServerConnection.
838      *
839      * @type {ServerConnection}
840      * @const
841      */
842     this.sc = sc;
843     /**
844      * The id of this stream.
845      *
846      * @type {string}
847      * @const
848      */
849     this.id = id;
850     /**
851      * The local id of this stream.
852      *
853      * @type {string}
854      * @const
855      */
856     this.localId = localId;
857     /**
858      * Indicates whether the stream is in the client->server direction.
859      *
860      * @type {boolean}
861      * @const
862      */
863     this.up = up;
864     /**
865      * For down streams, the id of the client that created the stream.
866      *
867      * @type {string}
868      */
869     this.source = null;
870     /**
871      * For down streams, the username of the client who created the stream.
872      *
873      * @type {string}
874      */
875     this.username = null;
876     /**
877      * The associated RTCPeerConnection.  This is null before the stream
878      * is connected, and may change over time.
879      *
880      * @type {RTCPeerConnection}
881      */
882     this.pc = pc;
883     /**
884      * The associated MediaStream.  This is null before the stream is
885      * connected, and may change over time.
886      *
887      * @type {MediaStream}
888      */
889     this.stream = null;
890     /**
891      * The label assigned by the originator to this stream.
892      *
893      * @type {string}
894      */
895     this.label = null;
896     /**
897      * The id of the stream that we are currently replacing.
898      *
899      * @type {string}
900      */
901     this.replace = null;
902     /**
903      * Indicates whether we have already sent a local description.
904      *
905      * @type {boolean}
906      */
907     this.localDescriptionSent = false;
908     /**
909      * Buffered local ICE candidates.  This will be flushed by
910      * flushLocalIceCandidates after we send a local description.
911      *
912      * @type {RTCIceCandidate[]}
913      */
914     this.localIceCandidates = [];
915     /**
916      * Buffered remote ICE candidates.  This will be flushed by
917      * flushRemoteIceCandidates when we get a remote SDP description.
918      *
919      * @type {RTCIceCandidate[]}
920      */
921     this.remoteIceCandidates = [];
922     /**
923      * The statistics last computed by the stats handler.  This is
924      * a dictionary indexed by track id, with each value a dictionary of
925      * statistics.
926      *
927      * @type {Object<string,unknown>}
928      */
929     this.stats = {};
930     /**
931      * The id of the periodic handler that computes statistics, as
932      * returned by setInterval.
933      *
934      * @type {number}
935      */
936     this.statsHandler = null;
937     /**
938      * userdata is a convenient place to attach data to a Stream.
939      * It is not used by the library.
940      *
941      * @type{Object<unknown,unknown>}
942      */
943     this.userdata = {};
944 
945     /* Callbacks */
946 
947     /**
948      * onclose is called when the stream is closed.  Replace will be true
949      * if the stream is being replaced by another one with the same id.
950      *
951      * @type{(this: Stream, replace: boolean) => void}
952      */
953     this.onclose = null;
954     /**
955      * onerror is called whenever a fatal error occurs.  The stream will
956      * then be closed, and onclose called normally.
957      *
958      * @type{(this: Stream, error: unknown) => void}
959      */
960     this.onerror = null;
961     /**
962      * onnegotiationcompleted is called whenever negotiation or
963      * renegotiation has completed.
964      *
965      * @type{(this: Stream) => void}
966      */
967     this.onnegotiationcompleted = null;
968     /**
969      * ondowntrack is called whenever a new track is added to a stream.
970      * If the stream parameter differs from its previous value, then it
971      * indicates that the old stream has been discarded.
972      *
973      * @type{(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, stream: MediaStream) => void}
974      */
975     this.ondowntrack = null;
976     /**
977      * onstatus is called whenever the status of the stream changes.
978      *
979      * @type{(this: Stream, status: string) => void}
980      */
981     this.onstatus = null;
982     /**
983      * onstats is called when we have new statistics about the connection
984      *
985      * @type{(this: Stream, stats: Object<unknown,unknown>) => void}
986      */
987     this.onstats = null;
988 }
989 
990 /**
991  * close closes a stream.
992  *
993  * For streams in the up direction, this may be called at any time.  For
994  * streams in the down direction, this will be called automatically when
995  * the server signals that it is closing a stream.
996  *
997  * @param {boolean} [replace]
998  *    - true if the stream is being replaced by another one with the same id
999  */
1000 Stream.prototype.close = function(replace) {
1001     let c = this;
1002 
1003     if(!c.sc) {
1004         console.warn('Closing closed stream');
1005         return;
1006     }
1007 
1008     if(c.statsHandler) {
1009         clearInterval(c.statsHandler);
1010         c.statsHandler = null;
1011     }
1012 
1013     c.pc.close();
1014 
1015     if(c.up && !replace && c.localDescriptionSent) {
1016         try {
1017             c.sc.send({
1018                 type: 'close',
1019                 id: c.id,
1020             });
1021         } catch(e) {
1022         }
1023     }
1024 
1025     if(c.up) {
1026         if(c.sc.up[c.id] === c)
1027             delete(c.sc.up[c.id]);
1028         else
1029             console.warn('Closing unknown stream');
1030     } else {
1031         if(c.sc.down[c.id] === c)
1032             delete(c.sc.down[c.id]);
1033         else
1034             console.warn('Closing unknown stream');
1035         recomputeUserStreams(c.sc, c.source);
1036     }
1037     c.sc = null;
1038 
1039     if(c.onclose)
1040         c.onclose.call(c, replace);
1041 };
1042 
1043 /**
1044  * @param {ServerConnection} sc
1045  * @param {string} id
1046  * @param {Stream} [c]
1047  * @returns {boolean}
1048  */
1049 function recomputeUserStreams(sc, id, c) {
1050     let user = sc.users[id];
1051     if(!user) {
1052         console.warn("recomputing streams for unknown user");
1053         return false;
1054     }
1055 
1056     if(c) {
1057         let changed = false;
1058         if(!user.down[c.label])
1059             user.down[c.label] = {};
1060         c.stream.getTracks().forEach(t => {
1061             if(!user.down[c.label][t.kind]) {
1062                 user.down[c.label][t.kind] = true;
1063                 changed = true;
1064             }
1065         });
1066         return changed;
1067     }
1068 
1069     if(!user.down || Object.keys(user.down).length === 0)
1070         return false;
1071 
1072     let old = user.down;
1073     user.down = {};
1074 
1075     for(id in sc.down) {
1076         let c = sc.down[id];
1077         if(!c.stream)
1078             continue;
1079         if(!user.down[c.label])
1080             user.down[c.label] = {};
1081         c.stream.getTracks().forEach(t => {
1082             user.down[c.label][t.kind] = true;
1083         });
1084     }
1085 
1086     // might lead to false positives.  Oh, well.
1087     return JSON.stringify(old) != JSON.stringify(user.down);
1088 }
1089 
1090 /**
1091  * abort requests that the server close a down stream.
1092  */
1093 Stream.prototype.abort = function() {
1094     let c = this;
1095     if(c.up)
1096         throw new Error("Abort called on an up stream");
1097     c.sc.send({
1098         type: 'abort',
1099         id: c.id,
1100     });
1101 };
1102 
1103 /**
1104  * Called when we get a local ICE candidate.  Don't call this.
1105  *
1106  * @param {RTCIceCandidate} candidate
1107  * @function
1108  */
1109 Stream.prototype.gotLocalIce = function(candidate) {
1110     let c = this;
1111     if(c.localDescriptionSent)
1112         c.sc.send({type: 'ice',
1113                    id: c.id,
1114                    candidate: candidate,
1115                   });
1116     else
1117         c.localIceCandidates.push(candidate);
1118 };
1119 
1120 /**
1121  * flushLocalIceCandidates flushes any buffered local ICE candidates.
1122  * It is called when we send an offer.
1123  * @function
1124  */
1125 Stream.prototype.flushLocalIceCandidates = function () {
1126     let c = this;
1127     let candidates = c.localIceCandidates;
1128     c.localIceCandidates = [];
1129     candidates.forEach(candidate => {
1130         try {
1131             c.sc.send({type: 'ice',
1132                        id: c.id,
1133                        candidate: candidate,
1134                       });
1135         } catch(e) {
1136             console.warn(e);
1137         }
1138     });
1139     c.localIceCandidates = [];
1140 };
1141 
1142 /**
1143  * flushRemoteIceCandidates flushes any buffered remote ICE candidates.  It is
1144  * called automatically when we get a remote description.
1145  * @function
1146  */
1147 Stream.prototype.flushRemoteIceCandidates = async function () {
1148     let c = this;
1149     let candidates = c.remoteIceCandidates;
1150     c.remoteIceCandidates = [];
1151     /** @type {Array.<Promise<void>>} */
1152     let promises = [];
1153     candidates.forEach(candidate => {
1154         promises.push(c.pc.addIceCandidate(candidate).catch(console.warn));
1155     });
1156     return await Promise.all(promises);
1157 };
1158 
1159 /**
1160  * negotiate negotiates or renegotiates an up stream.  It is called
1161  * automatically when required.  If the client requires renegotiation, it
1162  * is probably better to call restartIce which will cause negotiate to be
1163  * called asynchronously.
1164  *
1165  * @function
1166  * @param {boolean} [restartIce] - Whether to restart ICE.
1167  */
1168 Stream.prototype.negotiate = async function (restartIce) {
1169     let c = this;
1170     if(!c.up)
1171         throw new Error('not an up stream');
1172 
1173     let options = {};
1174     if(restartIce)
1175         options = {iceRestart: true};
1176     let offer = await c.pc.createOffer(options);
1177     if(!offer)
1178         throw(new Error("Didn't create offer"));
1179     await c.pc.setLocalDescription(offer);
1180 
1181     c.sc.send({
1182         type: 'offer',
1183         source: c.sc.id,
1184         username: c.sc.username,
1185         kind: this.localDescriptionSent ? 'renegotiate' : '',
1186         id: c.id,
1187         replace: this.replace,
1188         label: c.label,
1189         sdp: c.pc.localDescription.sdp,
1190     });
1191     this.localDescriptionSent = true;
1192     this.replace = null;
1193     c.flushLocalIceCandidates();
1194 };
1195 
1196 /**
1197  * restartIce causes an ICE restart on a stream.  For up streams, it is
1198  * called automatically when ICE signals that the connection has failed,
1199  * but may also be called by the application.  For down streams, it
1200  * requests that the server perform an ICE restart.  In either case,
1201  * it returns immediately, negotiation will happen asynchronously.
1202  */
1203 
1204 Stream.prototype.restartIce = function () {
1205     let c = this;
1206     if(!c.up) {
1207         c.sc.send({
1208             type: 'renegotiate',
1209             id: c.id,
1210         });
1211         return;
1212     }
1213 
1214     if('restartIce' in c.pc) {
1215         try {
1216             /** @ts-ignore */
1217             c.pc.restartIce();
1218             return;
1219         } catch(e) {
1220             console.warn(e);
1221         }
1222     }
1223 
1224     // negotiate is async, but this returns immediately.
1225     c.negotiate(true);
1226 };
1227 
1228 /**
1229  * request sets the list of tracks.  If this is not called, or called with
1230  * a null argument, then the default is provided by ServerConnection.request.
1231  *
1232  * @param {Array<string>} what - a sequence of 'audio', 'video' or 'video-low'.
1233  */
1234 Stream.prototype.request = function(what) {
1235     let c = this;
1236     c.sc.send({
1237         type: 'requestStream',
1238         id: c.id,
1239         request: what,
1240     });
1241 };
1242 
1243 /**
1244  * updateStats is called periodically, if requested by setStatsInterval,
1245  * in order to recompute stream statistics and invoke the onstats handler.
1246  *
1247  * @function
1248  */
1249 Stream.prototype.updateStats = async function() {
1250     let c = this;
1251     let old = c.stats;
1252     /** @type{Object<string,unknown>} */
1253     let stats = {};
1254 
1255     let transceivers = c.pc.getTransceivers();
1256     for(let i = 0; i < transceivers.length; i++) {
1257         let t = transceivers[i];
1258         let stid = t.sender.track && t.sender.track.id;
1259         let rtid = t.receiver.track && t.receiver.track.id;
1260 
1261         let report = null;
1262         if(stid) {
1263             try {
1264                 report = await t.sender.getStats();
1265             } catch(e) {
1266             }
1267         }
1268 
1269         if(report) {
1270             for(let r of report.values()) {
1271                 if(stid && r.type === 'outbound-rtp') {
1272                     let id = stid;
1273                     if(r.rid)
1274                         id = id + '-' + r.rid
1275                     if(!('bytesSent' in r))
1276                         continue;
1277                     if(!stats[id])
1278                         stats[id] = {};
1279                     stats[id][r.type] = {};
1280                     stats[id][r.type].timestamp = r.timestamp;
1281                     stats[id][r.type].bytesSent = r.bytesSent;
1282                     if(old[id] && old[id][r.type])
1283                         stats[id][r.type].rate =
1284                         ((r.bytesSent - old[id][r.type].bytesSent) * 1000 /
1285                          (r.timestamp - old[id][r.type].timestamp)) * 8;
1286                 }
1287             }
1288         }
1289 
1290         report = null;
1291         if(rtid) {
1292             try {
1293                 report = await t.receiver.getStats();
1294             } catch(e) {
1295                 console.error(e);
1296             }
1297         }
1298 
1299         if(report) {
1300             for(let r of report.values()) {
1301                 if(rtid && r.type === 'track') {
1302                     if(!('totalAudioEnergy' in r))
1303                         continue;
1304                     if(!stats[rtid])
1305                         stats[rtid] = {};
1306                     stats[rtid][r.type] = {};
1307                     stats[rtid][r.type].timestamp = r.timestamp;
1308                     stats[rtid][r.type].totalAudioEnergy = r.totalAudioEnergy;
1309                     if(old[rtid] && old[rtid][r.type])
1310                         stats[rtid][r.type].audioEnergy =
1311                         (r.totalAudioEnergy - old[rtid][r.type].totalAudioEnergy) * 1000 /
1312                         (r.timestamp - old[rtid][r.type].timestamp);
1313                 }
1314             }
1315         }
1316     }
1317 
1318     c.stats = stats;
1319 
1320     if(c.onstats)
1321         c.onstats.call(c, c.stats);
1322 };
1323 
1324 /**
1325  * setStatsInterval sets the interval in milliseconds at which the onstats
1326  * handler will be called.  This is only useful for up streams.
1327  *
1328  * @param {number} ms - The interval in milliseconds.
1329  */
1330 Stream.prototype.setStatsInterval = function(ms) {
1331     let c = this;
1332     if(c.statsHandler) {
1333         clearInterval(c.statsHandler);
1334         c.statsHandler = null;
1335     }
1336 
1337     if(ms <= 0)
1338         return;
1339 
1340     c.statsHandler = setInterval(() => {
1341         c.updateStats();
1342     }, ms);
1343 };
1344