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  *
 26  * @param {number[]|Uint8Array} array - the array to format
 27  * @returns {string} - the hexadecimal representation of array
 28  */
 29 function toHex(array) {
 30     let a = new Uint8Array(array);
 31     function hex(x) {
 32         let h = x.toString(16);
 33         if(h.length < 2)
 34             h = '0' + h;
 35         return h;
 36     }
 37     return a.reduce((x, y) => x + hex(y), '');
 38 }
 39 
 40 /**
 41  * newRandomId returns a random string of 32 hex digits (16 bytes).
 42  *
 43  * @returns {string}
 44  */
 45 function newRandomId() {
 46     let a = new Uint8Array(16);
 47     crypto.getRandomValues(a);
 48     return toHex(a);
 49 }
 50 
 51 let localIdCounter = 0;
 52 
 53 /**
 54  * newLocalId returns a string that is unique in this session.
 55  *
 56  * @returns {string}
 57  */
 58 function newLocalId() {
 59     let id = `${localIdCounter}`
 60     localIdCounter++;
 61     return id;
 62 }
 63 
 64 /**
 65  * @typedef {Object} user
 66  * @property {string} username
 67  * @property {Array<string>} permissions
 68  * @property {Object<string,any>} data
 69  * @property {Object<string,Object<string,boolean>>} streams
 70  */
 71 
 72 /**
 73  * ServerConnection encapsulates a websocket connection to the server and
 74  * all the associated streams.
 75  * @constructor
 76  */
 77 function ServerConnection() {
 78     /**
 79      * The id of this connection.
 80      *
 81      * @type {string}
 82      * @const
 83      */
 84     this.id = newRandomId();
 85     /**
 86      * The group that we have joined, or null if we haven't joined yet.
 87      *
 88      * @type {string}
 89      */
 90     this.group = null;
 91     /**
 92      * The username we joined as.
 93      *
 94      * @type {string}
 95      */
 96     this.username = null;
 97     /**
 98      * The set of users in this group, including ourself.
 99      *
100      * @type {Object<string,user>}
101      */
102     this.users = {};
103     /**
104      * The underlying websocket.
105      *
106      * @type {WebSocket}
107      */
108     this.socket = null;
109     /**
110      * The negotiated protocol version.
111      *
112      * @type {string}
113      */
114     this.version = null;
115     /**
116      * The set of all up streams, indexed by their id.
117      *
118      * @type {Object<string,Stream>}
119      */
120     this.up = {};
121     /**
122      * The set of all down streams, indexed by their id.
123      *
124      * @type {Object<string,Stream>}
125      */
126     this.down = {};
127     /**
128      * The ICE configuration used by all associated streams.
129      *
130      * @type {RTCConfiguration}
131      */
132     this.rtcConfiguration = null;
133     /**
134      * The permissions granted to this connection.
135      *
136      * @type {Array<string>}
137      */
138     this.permissions = [];
139     /**
140      * userdata is a convenient place to attach data to a ServerConnection.
141      * It is not used by the library.
142      *
143      * @type{Object<unknown,unknown>}
144      */
145     this.userdata = {};
146 
147     /* Callbacks */
148 
149     /**
150      * onconnected is called when the connection has been established
151      *
152      * @type{(this: ServerConnection) => void}
153      */
154     this.onconnected = null;
155     /**
156      * onclose is called when the connection is closed
157      *
158      * @type{(this: ServerConnection, code: number, reason: string) => void}
159      */
160     this.onclose = null;
161     /**
162      * onpeerconnection is called before we establish a new peer connection.
163      * It may either return null, or a new RTCConfiguration that overrides
164      * the value obtained from the server.
165      *
166      * @type{(this: ServerConnection) => RTCConfiguration}
167      */
168     this.onpeerconnection = null;
169     /**
170      * onuser is called whenever a user in the group changes.  The users
171      * array has already been updated.
172      *
173      * @type{(this: ServerConnection, id: string, kind: string) => void}
174      */
175     this.onuser = null;
176     /**
177      * onjoined is called whenever we join or leave a group or whenever the
178      * permissions we have in a group change.
179      *
180      * kind is one of 'join', 'fail', 'change' or 'leave'.
181      *
182      * @type{(this: ServerConnection, kind: string, group: string, permissions: Array<string>, status: Object<string,any>, data: Object<string,any>, error: string, message: string) => void}
183      */
184     this.onjoined = null;
185     /**
186      * ondownstream is called whenever a new down stream is added.  It
187      * should set up the stream's callbacks; actually setting up the UI
188      * should be done in the stream's ondowntrack callback.
189      *
190      * @type{(this: ServerConnection, stream: Stream) => void}
191      */
192     this.ondownstream = null;
193     /**
194      * onchat is called whenever a new chat message is received.
195      *
196      * @type {(this: ServerConnection, id: string, dest: string, username: string, time: Date, privileged: boolean, history: boolean, kind: string, message: unknown) => void}
197      */
198     this.onchat = null;
199     /**
200      * onusermessage is called when an application-specific message is
201      * received.  Id is null when the message originated at the server,
202      * a user-id otherwise.
203      *
204      * 'kind' is typically one of 'error', 'warning', 'info' or 'mute'.  If
205      * 'id' is non-null, 'privileged' indicates whether the message was
206      * sent by an operator.
207      *
208      * @type {(this: ServerConnection, id: string, dest: string, username: string, time: Date, privileged: boolean, kind: string, error: string, message: unknown) => void}
209      */
210     this.onusermessage = null;
211     /**
212      * The set of files currently being transferred.
213      *
214      * @type {Object<string,TransferredFile>}
215     */
216     this.transferredFiles = {};
217     /**
218      * onfiletransfer is called whenever a peer offers a file transfer.
219      *
220      * If the transfer is accepted, it should set up the file transfer
221      * callbacks and return immediately.  It may also throw an exception
222      * in order to reject the file transfer.
223      *
224      * @type {(this: ServerConnection, f: TransferredFile) => void}
225      */
226     this.onfiletransfer = null;
227 }
228 
229 /**
230   * @typedef {Object} message
231   * @property {string} type
232   * @property {Array<string>} [version]
233   * @property {string} [kind]
234   * @property {string} [error]
235   * @property {string} [id]
236   * @property {string} [replace]
237   * @property {string} [source]
238   * @property {string} [dest]
239   * @property {string} [username]
240   * @property {string} [password]
241   * @property {string} [token]
242   * @property {boolean} [privileged]
243   * @property {Array<string>} [permissions]
244   * @property {Object<string,any>} [status]
245   * @property {Object<string,any>} [data]
246   * @property {string} [group]
247   * @property {unknown} [value]
248   * @property {boolean} [noecho]
249   * @property {string|number} [time]
250   * @property {string} [sdp]
251   * @property {RTCIceCandidate} [candidate]
252   * @property {string} [label]
253   * @property {Object<string,Array<string>>|Array<string>} [request]
254   * @property {Object<string,any>} [rtcConfiguration]
255   */
256 
257 /**
258  * close forcibly closes a server connection.  The onclose callback will
259  * be called when the connection is effectively closed.
260  */
261 ServerConnection.prototype.close = function() {
262     this.socket && this.socket.close(1000, 'Close requested by client');
263     this.socket = null;
264 };
265 
266 /**
267   * send sends a message to the server.
268   * @param {message} m - the message to send.
269   */
270 ServerConnection.prototype.send = function(m) {
271     if(!this.socket || this.socket.readyState !== this.socket.OPEN) {
272         // send on a closed socket doesn't throw
273         throw(new Error('Connection is not open'));
274     }
275     return this.socket.send(JSON.stringify(m));
276 };
277 
278 /**
279  * connect connects to the server.
280  *
281  * @param {string} url - The URL to connect to.
282  * @returns {Promise<ServerConnection>}
283  * @function
284  */
285 ServerConnection.prototype.connect = async function(url) {
286     let sc = this;
287     if(sc.socket) {
288         sc.socket.close(1000, 'Reconnecting');
289         sc.socket = null;
290     }
291 
292     sc.socket = new WebSocket(url);
293 
294     return await new Promise((resolve, reject) => {
295         this.socket.onerror = function(e) {
296             reject(e);
297         };
298         this.socket.onopen = function(e) {
299             sc.send({
300                 type: 'handshake',
301                 version: ['2'],
302                 id: sc.id,
303             });
304             if(sc.onconnected)
305                 sc.onconnected.call(sc);
306             resolve(sc);
307         };
308         this.socket.onclose = function(e) {
309             sc.permissions = [];
310             for(let id in sc.up) {
311                 let c = sc.up[id];
312                 c.close();
313             }
314             for(let id in sc.down) {
315                 let c = sc.down[id];
316                 c.close();
317             }
318             for(let id in sc.users) {
319                 delete(sc.users[id]);
320                 if(sc.onuser)
321                     sc.onuser.call(sc, id, 'delete');
322             }
323             if(sc.group && sc.onjoined)
324                 sc.onjoined.call(sc, 'leave', sc.group, [], {}, {}, '', '');
325             sc.group = null;
326             sc.username = null;
327             if(sc.onclose)
328                 sc.onclose.call(sc, e.code, e.reason);
329             reject(new Error('websocket close ' + e.code + ' ' + e.reason));
330         };
331         this.socket.onmessage = function(e) {
332             let m = JSON.parse(e.data);
333             switch(m.type) {
334             case 'handshake': {
335                 if((m.version instanceof Array) && m.version.includes('2')) {
336                     sc.version = '2';
337                 } else {
338                     sc.version = null;
339                     console.error(`Unknown protocol version ${m.version}`);
340                     throw new Error(`Unknown protocol version ${m.version}`);
341                 }
342                 break;
343             }
344             case 'offer':
345                 sc.gotOffer(m.id, m.label, m.source, m.username,
346                             m.sdp, m.replace);
347                 break;
348             case 'answer':
349                 sc.gotAnswer(m.id, m.sdp);
350                 break;
351             case 'renegotiate':
352                 sc.gotRenegotiate(m.id);
353                 break;
354             case 'close':
355                 sc.gotClose(m.id);
356                 break;
357             case 'abort':
358                 sc.gotAbort(m.id);
359                 break;
360             case 'ice':
361                 sc.gotRemoteIce(m.id, m.candidate);
362                 break;
363             case 'joined':
364                 if(m.kind === 'leave' || m.kind === 'fail') {
365                     for(let id in sc.users) {
366                         delete(sc.users[id]);
367                         if(sc.onuser)
368                             sc.onuser.call(sc, id, 'delete');
369                     }
370                     sc.username = null;
371                     sc.permissions = [];
372                     sc.rtcConfiguration = null;
373                 } else if(m.kind === 'join' || m.kind == 'change') {
374                     if(m.kind === 'join' && sc.group) {
375                         throw new Error('Joined multiple groups');
376                     } else if(m.kind === 'change' && m.group != sc.group) {
377                         console.warn('join(change) for inconsistent group');
378                         break;
379                     }
380                     sc.group = m.group;
381                     sc.username = m.username;
382                     sc.permissions = m.permissions || [];
383                     sc.rtcConfiguration = m.rtcConfiguration || null;
384                 }
385                 if(sc.onjoined)
386                     sc.onjoined.call(sc, m.kind, m.group,
387                                      m.permissions || [],
388                                      m.status, m.data,
389                                      m.error || null, m.value || null);
390                 break;
391             case 'user':
392                 switch(m.kind) {
393                 case 'add':
394                     if(m.id in sc.users)
395                         console.warn(`Duplicate user ${m.id} ${m.username}`);
396                     sc.users[m.id] = {
397                         username: m.username,
398                         permissions: m.permissions || [],
399                         data: m.data || {},
400                         streams: {},
401                     };
402                     break;
403                 case 'change':
404                     if(!(m.id in sc.users)) {
405                         console.warn(`Unknown user ${m.id} ${m.username}`);
406                         sc.users[m.id] = {
407                             username: m.username,
408                             permissions: m.permissions || [],
409                             data: m.data || {},
410                             streams: {},
411                         };
412                     } else {
413                         sc.users[m.id].username = m.username;
414                         sc.users[m.id].permissions = m.permissions || [];
415                         sc.users[m.id].data = m.data || {};
416                     }
417                     break;
418                 case 'delete':
419                     if(!(m.id in sc.users))
420                         console.warn(`Unknown user ${m.id} ${m.username}`);
421                     for(let t in sc.transferredFiles) {
422                         let f = sc.transferredFiles[t];
423                         if(f.userid === m.id)
424                             f.fail('user has gone away');
425                     }
426                     delete(sc.users[m.id]);
427                     break;
428                 default:
429                     console.warn(`Unknown user action ${m.kind}`);
430                     return;
431                 }
432                 if(sc.onuser)
433                     sc.onuser.call(sc, m.id, m.kind);
434                 break;
435             case 'chat':
436             case 'chathistory':
437                 if(sc.onchat)
438                     sc.onchat.call(
439                         sc, m.source, m.dest, m.username, parseTime(m.time),
440                         m.privileged, m.type === 'chathistory', m.kind, m.value,
441                     );
442                 break;
443             case 'usermessage':
444                 if(m.kind === 'filetransfer')
445                     sc.fileTransfer(m.source, m.username, m.value);
446                 else if(sc.onusermessage)
447                     sc.onusermessage.call(
448                         sc, m.source, m.dest, m.username, parseTime(m.time),
449                         m.privileged, m.kind, m.error, m.value,
450                     );
451                 break;
452             case 'ping':
453                 sc.send({
454                     type: 'pong',
455                 });
456                 break;
457             case 'pong':
458                 /* nothing */
459                 break;
460             default:
461                 console.warn('Unexpected server message', m.type);
462                 return;
463             }
464         };
465     });
466 };
467 
468 /**
469  * Protocol version 1 uses integers for dates, later versions use dates in
470  * ISO 8601 format.  This function takes a date in either format and
471  * returns a Date object.
472  *
473  * @param {string|number} value
474  * @returns {Date}
475  */
476 function parseTime(value) {
477     if(!value)
478         return null;
479     try {
480         return new Date(value);
481     } catch(e) {
482         console.warn(`Couldn't parse ${value}:`, e);
483         return null;
484     }
485 }
486 
487 /**
488  * join requests to join a group.  The onjoined callback will be called
489  * when we've effectively joined.
490  *
491  * @param {string} group - The name of the group to join.
492  * @param {string} username - the username to join as.
493  * @param {string|Object} credentials - password or authServer.
494  * @param {Object<string,any>} [data] - the initial associated data.
495  */
496 ServerConnection.prototype.join = async function(group, username, credentials, data) {
497     let m = {
498         type: 'join',
499         kind: 'join',
500         group: group,
501     };
502     if(typeof username !== 'undefined' && username !== null)
503         m.username = username;
504 
505     if((typeof credentials) === 'string') {
506         m.password = credentials;
507     } else {
508         switch(credentials.type) {
509         case 'password':
510             m.password = credentials.password;
511             break;
512         case 'token':
513             m.token = credentials.token;
514             break;
515         case 'authServer':
516             let r = await fetch(credentials.authServer, {
517                 method: "POST",
518                 headers: {
519                     "Content-Type": "application/json",
520                 },
521                 body: JSON.stringify({
522                     location: credentials.location,
523                     username: username,
524                     password: credentials.password,
525                 }),
526             });
527             if(!r.ok)
528                 throw new Error(
529                     `The authorisation server said ${r.status} ${r.statusText}`,
530                 );
531             if(r.status === 204) {
532                 // no data, fallback to password auth
533                 m.password = credentials.password;
534                 break;
535             }
536             let ctype = r.headers.get("Content-Type");
537             if(!ctype)
538                 throw new Error(
539                     "The authorisation server didn't return a content type",
540                 );
541             let semi = ctype.indexOf(";");
542             if(semi >= 0)
543                 ctype = ctype.slice(0, semi);
544             ctype = ctype.trim();
545             switch(ctype.toLowerCase()) {
546             case 'application/jwt':
547                 let data = await r.text();
548                 if(!data)
549                     throw new Error(
550                         "The authorisation server returned empty token",
551                     );
552                 m.token = data;
553                 break;
554             default:
555                 throw new Error(`The authorisation server returned ${ctype}`);
556                 break;
557             }
558             break;
559         default:
560             throw new Error(`Unknown credentials type ${credentials.type}`);
561         }
562     }
563 
564     if(data)
565         m.data = data;
566 
567     this.send(m);
568 };
569 
570 /**
571  * leave leaves a group.  The onjoined callback will be called when we've
572  * effectively left.
573  *
574  * @param {string} group - The name of the group to join.
575  */
576 ServerConnection.prototype.leave = function(group) {
577     this.send({
578         type: 'join',
579         kind: 'leave',
580         group: group,
581     });
582 };
583 
584 /**
585  * request sets the list of requested tracks
586  *
587  * @param {Object<string,Array<string>>} what
588  *     - A dictionary that maps labels to a sequence of 'audio', 'video'
589  *       or 'video-low.  An entry with an empty label '' provides the default.
590  */
591 ServerConnection.prototype.request = function(what) {
592     this.send({
593         type: 'request',
594         request: what,
595     });
596 };
597 
598 /**
599  * findByLocalId finds an active connection with the given localId.
600  * It returns null if none was find.
601  *
602  * @param {string} localId
603  * @returns {Stream}
604  */
605 ServerConnection.prototype.findByLocalId = function(localId) {
606     if(!localId)
607         return null;
608 
609     let sc = this;
610 
611     for(let id in sc.up) {
612         let s = sc.up[id];
613         if(s.localId === localId)
614             return s;
615     }
616     return null;
617 }
618 
619 /**
620  * getRTCConfiguration returns the RTCConfiguration that should be used
621  * with this peer connection.  This usually comes from the server, but may
622  * be overridden by the onpeerconnection callback.
623  *
624  * @returns {RTCConfiguration}
625  */
626 ServerConnection.prototype.getRTCConfiguration = function() {
627     if(this.onpeerconnection) {
628         let conf = this.onpeerconnection.call(this);
629         if(conf !== null)
630             return conf;
631     }
632     return this.rtcConfiguration;
633 }
634 
635 /**
636  * newUpStream requests the creation of a new up stream.
637  *
638  * @param {string} [localId]
639  *   - The local id of the stream to create.  If a stream already exists with
640  *     the same local id, it is replaced with the new stream.
641  * @returns {Stream}
642  */
643 ServerConnection.prototype.newUpStream = function(localId) {
644     let sc = this;
645     let id = newRandomId();
646     if(sc.up[id])
647         throw new Error('Eek!');
648 
649     if(typeof RTCPeerConnection === 'undefined')
650         throw new Error("This browser doesn't support WebRTC");
651 
652 
653     let pc = new RTCPeerConnection(sc.getRTCConfiguration());
654     if(!pc)
655         throw new Error("Couldn't create peer connection");
656 
657     let oldId = null;
658     if(localId) {
659         let old = sc.findByLocalId(localId);
660         oldId = old && old.id;
661         if(old)
662             old.close(true);
663     }
664 
665     let c = new Stream(this, id, localId || newLocalId(), pc, true);
666     if(oldId)
667         c.replace = oldId;
668     sc.up[id] = c;
669 
670     pc.onnegotiationneeded = async e => {
671             await c.negotiate();
672     };
673 
674     pc.onicecandidate = e => {
675         if(!e.candidate)
676             return;
677         c.gotLocalIce(e.candidate);
678     };
679 
680     pc.oniceconnectionstatechange = e => {
681         if(c.onstatus)
682             c.onstatus.call(c, pc.iceConnectionState);
683         if(pc.iceConnectionState === 'failed')
684             c.restartIce();
685     };
686 
687     pc.ontrack = console.error;
688     return c;
689 };
690 
691 /**
692  * chat sends a chat message to the server.  The server will normally echo
693  * the message back to the client.
694  *
695  * @param {string} kind
696  *     -  The kind of message, either '', 'me' or an application-specific type.
697  * @param {string} dest - The id to send the message to, empty for broadcast.
698  * @param {string} value - The text of the message.
699  */
700 ServerConnection.prototype.chat = function(kind, dest, value) {
701     this.send({
702         type: 'chat',
703         source: this.id,
704         dest: dest,
705         username: this.username,
706         kind: kind,
707         value: value,
708     });
709 };
710 
711 /**
712  * userAction sends a request to act on a user.
713  *
714  * @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
715  * @param {string} dest - The id of the user to act upon.
716  * @param {any} [value] - An action-dependent parameter.
717  */
718 ServerConnection.prototype.userAction = function(kind, dest, value) {
719     this.send({
720         type: 'useraction',
721         source: this.id,
722         dest: dest,
723         username: this.username,
724         kind: kind,
725         value: value,
726     });
727 };
728 
729 /**
730  * userMessage sends an application-specific message to a user.
731  * This is similar to a chat message, but is not saved in the chat history.
732  *
733  * @param {string} kind - The kind of application-specific message.
734  * @param {string} dest - The id to send the message to, empty for broadcast.
735  * @param {unknown} [value] - An optional parameter.
736  * @param {boolean} [noecho] - If set, don't echo back the message to the sender.
737  */
738 ServerConnection.prototype.userMessage = function(kind, dest, value, noecho) {
739     this.send({
740         type: 'usermessage',
741         source: this.id,
742         dest: dest,
743         username: this.username,
744         kind: kind,
745         value: value,
746         noecho: noecho,
747     });
748 };
749 
750 /**
751  * groupAction sends a request to act on the current group.
752  *
753  * @param {string} kind
754  * @param {any} [data]
755  */
756 ServerConnection.prototype.groupAction = function(kind, data) {
757     this.send({
758         type: 'groupaction',
759         source: this.id,
760         kind: kind,
761         username: this.username,
762         value: data,
763     });
764 };
765 
766 /**
767  * gotOffer is called when we receive an offer from the server.  Don't call this.
768  *
769  * @param {string} id
770  * @param {string} label
771  * @param {string} source
772  * @param {string} username
773  * @param {string} sdp
774  * @param {string} replace
775  * @function
776  */
777 ServerConnection.prototype.gotOffer = async function(id, label, source, username, sdp, replace) {
778     let sc = this;
779 
780     if(sc.up[id]) {
781         console.error("Duplicate connection id");
782         sc.send({
783             type: 'abort',
784             id: id,
785         });
786         return;
787     }
788 
789     let oldLocalId = null;
790 
791     if(replace) {
792         let old = sc.down[replace];
793         if(old) {
794             oldLocalId = old.localId;
795             old.close(true);
796         } else
797             console.error("Replacing unknown stream");
798     }
799 
800     let c = sc.down[id];
801     if(c && oldLocalId)
802         console.error("Replacing duplicate stream");
803 
804     if(!c) {
805         let pc;
806         try {
807             pc = new RTCPeerConnection(sc.getRTCConfiguration());
808         } catch(e) {
809             console.error(e);
810             sc.send({
811                 type: 'abort',
812                 id: id,
813             });
814             return;
815         }
816         c = new Stream(this, id, oldLocalId || newLocalId(), pc, false);
817         c.label = label;
818         sc.down[id] = c;
819 
820         c.pc.onicecandidate = function(e) {
821             if(!e.candidate)
822                 return;
823             c.gotLocalIce(e.candidate);
824         };
825 
826         pc.oniceconnectionstatechange = e => {
827             if(c.onstatus)
828                 c.onstatus.call(c, pc.iceConnectionState);
829             if(pc.iceConnectionState === 'failed') {
830                 sc.send({
831                     type: 'renegotiate',
832                     id: id,
833                 });
834             }
835         };
836 
837         c.pc.ontrack = function(e) {
838             if(e.streams.length < 1) {
839                 console.error("Got track with no stream");
840                 return;
841             }
842             c.stream = e.streams[0];
843             let changed = recomputeUserStreams(sc, source);
844             if(c.ondowntrack) {
845                 c.ondowntrack.call(
846                     c, e.track, e.transceiver, e.streams[0],
847                 );
848             }
849             if(changed && sc.onuser)
850                 sc.onuser.call(sc, source, "change");
851         };
852     }
853 
854     c.source = source;
855     c.username = username;
856 
857     if(sc.ondownstream)
858         sc.ondownstream.call(sc, c);
859 
860     try {
861         await c.pc.setRemoteDescription({
862             type: 'offer',
863             sdp: sdp,
864         });
865 
866         await c.flushRemoteIceCandidates();
867 
868         let answer = await c.pc.createAnswer();
869         if(!answer)
870             throw new Error("Didn't create answer");
871         await c.pc.setLocalDescription(answer);
872         this.send({
873             type: 'answer',
874             id: id,
875             sdp: c.pc.localDescription.sdp,
876         });
877     } catch(e) {
878         try {
879             if(c.onerror)
880                 c.onerror.call(c, e);
881         } finally {
882             c.abort();
883         }
884         return;
885     }
886 
887     c.localDescriptionSent = true;
888     c.flushLocalIceCandidates();
889     if(c.onnegotiationcompleted)
890         c.onnegotiationcompleted.call(c);
891 };
892 
893 /**
894  * gotAnswer is called when we receive an answer from the server.  Don't
895  * call this.
896  *
897  * @param {string} id
898  * @param {string} sdp
899  * @function
900  */
901 ServerConnection.prototype.gotAnswer = async function(id, sdp) {
902     let c = this.up[id];
903     if(!c)
904         throw new Error('unknown up stream');
905     try {
906         await c.pc.setRemoteDescription({
907             type: 'answer',
908             sdp: sdp,
909         });
910     } catch(e) {
911         try {
912             if(c.onerror)
913                 c.onerror.call(c, e);
914         } finally {
915             c.close();
916         }
917         return;
918     }
919     await c.flushRemoteIceCandidates();
920     if(c.onnegotiationcompleted)
921         c.onnegotiationcompleted.call(c);
922 };
923 
924 /**
925  * gotRenegotiate is called when we receive a renegotiation request from
926  * the server.  Don't call this.
927  *
928  * @param {string} id
929  * @function
930  */
931 ServerConnection.prototype.gotRenegotiate = function(id) {
932     let c = this.up[id];
933     if(!c)
934         throw new Error('unknown up stream');
935     c.restartIce();
936 };
937 
938 /**
939  * gotClose is called when we receive a close request from the server.
940  * Don't call this.
941  *
942  * @param {string} id
943  */
944 ServerConnection.prototype.gotClose = function(id) {
945     let c = this.down[id];
946     if(!c) {
947         console.warn('unknown down stream', id);
948         return;
949     }
950     c.close();
951 };
952 
953 /**
954  * gotAbort is called when we receive an abort message from the server.
955  * Don't call this.
956  *
957  * @param {string} id
958  */
959 ServerConnection.prototype.gotAbort = function(id) {
960     let c = this.up[id];
961     if(!c)
962         throw new Error('unknown up stream');
963     c.close();
964 };
965 
966 /**
967  * gotRemoteIce is called when we receive an ICE candidate from the server.
968  * Don't call this.
969  *
970  * @param {string} id
971  * @param {RTCIceCandidate} candidate
972  * @function
973  */
974 ServerConnection.prototype.gotRemoteIce = async function(id, candidate) {
975     let c = this.up[id];
976     if(!c)
977         c = this.down[id];
978     if(!c)
979         throw new Error('unknown stream');
980     if(c.pc.remoteDescription)
981         await c.pc.addIceCandidate(candidate).catch(console.warn);
982     else
983         c.remoteIceCandidates.push(candidate);
984 };
985 
986 /**
987  * Stream encapsulates a MediaStream, a set of tracks.
988  *
989  * A stream is said to go "up" if it is from the client to the server, and
990  * "down" otherwise.
991  *
992  * @param {ServerConnection} sc
993  * @param {string} id
994  * @param {string} localId
995  * @param {RTCPeerConnection} pc
996  *
997  * @constructor
998  */
999 function Stream(sc, id, localId, pc, up) {
1000     /**
1001      * The associated ServerConnection.
1002      *
1003      * @type {ServerConnection}
1004      * @const
1005      */
1006     this.sc = sc;
1007     /**
1008      * The id of this stream.
1009      *
1010      * @type {string}
1011      * @const
1012      */
1013     this.id = id;
1014     /**
1015      * The local id of this stream.
1016      *
1017      * @type {string}
1018      * @const
1019      */
1020     this.localId = localId;
1021     /**
1022      * Indicates whether the stream is in the client->server direction.
1023      *
1024      * @type {boolean}
1025      * @const
1026      */
1027     this.up = up;
1028     /**
1029      * For down streams, the id of the client that created the stream.
1030      *
1031      * @type {string}
1032      */
1033     this.source = null;
1034     /**
1035      * For down streams, the username of the client who created the stream.
1036      *
1037      * @type {string}
1038      */
1039     this.username = null;
1040     /**
1041      * The associated RTCPeerConnection.  This is null before the stream
1042      * is connected, and may change over time.
1043      *
1044      * @type {RTCPeerConnection}
1045      */
1046     this.pc = pc;
1047     /**
1048      * The associated MediaStream.  This is null before the stream is
1049      * connected, and may change over time.
1050      *
1051      * @type {MediaStream}
1052      */
1053     this.stream = null;
1054     /**
1055      * The label assigned by the originator to this stream.
1056      *
1057      * @type {string}
1058      */
1059     this.label = null;
1060     /**
1061      * The id of the stream that we are currently replacing.
1062      *
1063      * @type {string}
1064      */
1065     this.replace = null;
1066     /**
1067      * Indicates whether we have already sent a local description.
1068      *
1069      * @type {boolean}
1070      */
1071     this.localDescriptionSent = false;
1072     /**
1073      * Buffered local ICE candidates.  This will be flushed by
1074      * flushLocalIceCandidates after we send a local description.
1075      *
1076      * @type {RTCIceCandidate[]}
1077      */
1078     this.localIceCandidates = [];
1079     /**
1080      * Buffered remote ICE candidates.  This will be flushed by
1081      * flushRemoteIceCandidates when we get a remote SDP description.
1082      *
1083      * @type {RTCIceCandidate[]}
1084      */
1085     this.remoteIceCandidates = [];
1086     /**
1087      * The statistics last computed by the stats handler.  This is
1088      * a dictionary indexed by track id, with each value a dictionary of
1089      * statistics.
1090      *
1091      * @type {Object<string,unknown>}
1092      */
1093     this.stats = {};
1094     /**
1095      * The id of the periodic handler that computes statistics, as
1096      * returned by setInterval.
1097      *
1098      * @type {number}
1099      */
1100     this.statsHandler = null;
1101     /**
1102      * userdata is a convenient place to attach data to a Stream.
1103      * It is not used by the library.
1104      *
1105      * @type{Object<unknown,unknown>}
1106      */
1107     this.userdata = {};
1108 
1109     /* Callbacks */
1110 
1111     /**
1112      * onclose is called when the stream is closed.  Replace will be true
1113      * if the stream is being replaced by another one with the same id.
1114      *
1115      * @type{(this: Stream, replace: boolean) => void}
1116      */
1117     this.onclose = null;
1118     /**
1119      * onerror is called whenever a fatal error occurs.  The stream will
1120      * then be closed, and onclose called normally.
1121      *
1122      * @type{(this: Stream, error: unknown) => void}
1123      */
1124     this.onerror = null;
1125     /**
1126      * onnegotiationcompleted is called whenever negotiation or
1127      * renegotiation has completed.
1128      *
1129      * @type{(this: Stream) => void}
1130      */
1131     this.onnegotiationcompleted = null;
1132     /**
1133      * ondowntrack is called whenever a new track is added to a stream.
1134      * If the stream parameter differs from its previous value, then it
1135      * indicates that the old stream has been discarded.
1136      *
1137      * @type{(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, stream: MediaStream) => void}
1138      */
1139     this.ondowntrack = null;
1140     /**
1141      * onstatus is called whenever the status of the stream changes.
1142      *
1143      * @type{(this: Stream, status: string) => void}
1144      */
1145     this.onstatus = null;
1146     /**
1147      * onstats is called when we have new statistics about the connection
1148      *
1149      * @type{(this: Stream, stats: Object<unknown,unknown>) => void}
1150      */
1151     this.onstats = null;
1152 }
1153 
1154 /**
1155  * setStream sets the stream of an upwards connection.
1156  *
1157  * @param {MediaStream} stream
1158  */
1159 Stream.prototype.setStream = function(stream) {
1160     let c = this;
1161     c.stream = stream;
1162     let changed = recomputeUserStreams(c.sc, c.sc.id);
1163     if(changed && c.sc.onuser)
1164         c.sc.onuser.call(c.sc, c.sc.id, "change");
1165 }
1166 
1167 /**
1168  * close closes a stream.
1169  *
1170  * For streams in the up direction, this may be called at any time.  For
1171  * streams in the down direction, this will be called automatically when
1172  * the server signals that it is closing a stream.
1173  *
1174  * @param {boolean} [replace]
1175  *    - true if the stream is being replaced by another one with the same id
1176  */
1177 Stream.prototype.close = function(replace) {
1178     let c = this;
1179 
1180     if(!c.sc) {
1181         console.warn('Closing closed stream');
1182         return;
1183     }
1184 
1185     if(c.statsHandler) {
1186         clearInterval(c.statsHandler);
1187         c.statsHandler = null;
1188     }
1189 
1190     c.pc.close();
1191 
1192     if(c.up && !replace && c.localDescriptionSent) {
1193         try {
1194             c.sc.send({
1195                 type: 'close',
1196                 id: c.id,
1197             });
1198         } catch(e) {
1199         }
1200     }
1201 
1202     let userid;
1203     if(c.up) {
1204         userid = c.sc.id;
1205         if(c.sc.up[c.id] === c)
1206             delete(c.sc.up[c.id]);
1207         else
1208             console.warn('Closing unknown stream');
1209     } else {
1210         userid = c.source;
1211         if(c.sc.down[c.id] === c)
1212             delete(c.sc.down[c.id]);
1213         else
1214             console.warn('Closing unknown stream');
1215     }
1216     let changed = recomputeUserStreams(c.sc, userid);
1217     if(changed && c.sc.onuser)
1218         c.sc.onuser.call(c.sc, userid, "change");
1219 
1220     if(c.onclose)
1221         c.onclose.call(c, replace);
1222 
1223     c.sc = null;
1224 };
1225 
1226 /**
1227  * recomputeUserStreams recomputes the user.streams array for a given user.
1228  * It returns true if anything changed.
1229  *
1230  * @param {ServerConnection} sc
1231  * @param {string} id
1232  * @returns {boolean}
1233  */
1234 function recomputeUserStreams(sc, id) {
1235     let user = sc.users[id];
1236     if(!user) {
1237         console.warn("recomputing streams for unknown user");
1238         return false;
1239     }
1240 
1241     let streams = id === sc.id ? sc.up : sc.down;
1242     let old = user.streams;
1243     user.streams = {};
1244     for(id in streams) {
1245         let c = streams[id];
1246         if(!c.stream)
1247             continue;
1248         if(!user.streams[c.label])
1249             user.streams[c.label] = {};
1250         c.stream.getTracks().forEach(t => {
1251             user.streams[c.label][t.kind] = true;
1252         });
1253     }
1254 
1255     return JSON.stringify(old) != JSON.stringify(user.streams);
1256 }
1257 
1258 /**
1259  * abort requests that the server close a down stream.
1260  */
1261 Stream.prototype.abort = function() {
1262     let c = this;
1263     if(c.up)
1264         throw new Error("Abort called on an up stream");
1265     c.sc.send({
1266         type: 'abort',
1267         id: c.id,
1268     });
1269 };
1270 
1271 /**
1272  * gotLocalIce is Called when we get a local ICE candidate.  Don't call this.
1273  *
1274  * @param {RTCIceCandidate} candidate
1275  * @function
1276  */
1277 Stream.prototype.gotLocalIce = function(candidate) {
1278     let c = this;
1279     if(c.localDescriptionSent)
1280         c.sc.send({type: 'ice',
1281                    id: c.id,
1282                    candidate: candidate,
1283                   });
1284     else
1285         c.localIceCandidates.push(candidate);
1286 };
1287 
1288 /**
1289  * flushLocalIceCandidates flushes any buffered local ICE candidates.
1290  * It is called when we send an offer.
1291  *
1292  * @function
1293  */
1294 Stream.prototype.flushLocalIceCandidates = function () {
1295     let c = this;
1296     let candidates = c.localIceCandidates;
1297     c.localIceCandidates = [];
1298     candidates.forEach(candidate => {
1299         try {
1300             c.sc.send({type: 'ice',
1301                        id: c.id,
1302                        candidate: candidate,
1303                       });
1304         } catch(e) {
1305             console.warn(e);
1306         }
1307     });
1308     c.localIceCandidates = [];
1309 };
1310 
1311 /**
1312  * flushRemoteIceCandidates flushes any buffered remote ICE candidates.  It is
1313  * called automatically when we get a remote description.
1314  *
1315  * @function
1316  */
1317 Stream.prototype.flushRemoteIceCandidates = async function () {
1318     let c = this;
1319     let candidates = c.remoteIceCandidates;
1320     c.remoteIceCandidates = [];
1321     /** @type {Array.<Promise<void>>} */
1322     let promises = [];
1323     candidates.forEach(candidate => {
1324         promises.push(c.pc.addIceCandidate(candidate).catch(console.warn));
1325     });
1326     return await Promise.all(promises);
1327 };
1328 
1329 /**
1330  * negotiate negotiates or renegotiates an up stream.  It is called
1331  * automatically when required.  If the client requires renegotiation, it
1332  * is probably better to call restartIce which will cause negotiate to be
1333  * called asynchronously.
1334  *
1335  * @function
1336  * @param {boolean} [restartIce] - Whether to restart ICE.
1337  */
1338 Stream.prototype.negotiate = async function (restartIce) {
1339     let c = this;
1340     if(!c.up)
1341         throw new Error('not an up stream');
1342 
1343     let options = {};
1344     if(restartIce)
1345         options = {iceRestart: true};
1346     let offer = await c.pc.createOffer(options);
1347     if(!offer)
1348         throw(new Error("Didn't create offer"));
1349     await c.pc.setLocalDescription(offer);
1350 
1351     c.sc.send({
1352         type: 'offer',
1353         source: c.sc.id,
1354         username: c.sc.username,
1355         kind: this.localDescriptionSent ? 'renegotiate' : '',
1356         id: c.id,
1357         replace: this.replace,
1358         label: c.label,
1359         sdp: c.pc.localDescription.sdp,
1360     });
1361     this.localDescriptionSent = true;
1362     this.replace = null;
1363     c.flushLocalIceCandidates();
1364 };
1365 
1366 /**
1367  * restartIce causes an ICE restart on a stream.  For up streams, it is
1368  * called automatically when ICE signals that the connection has failed,
1369  * but may also be called by the application.  For down streams, it
1370  * requests that the server perform an ICE restart.  In either case,
1371  * it returns immediately, negotiation will happen asynchronously.
1372  */
1373 
1374 Stream.prototype.restartIce = function () {
1375     let c = this;
1376     if(!c.up) {
1377         c.sc.send({
1378             type: 'renegotiate',
1379             id: c.id,
1380         });
1381         return;
1382     }
1383 
1384     if('restartIce' in c.pc) {
1385         try {
1386             c.pc.restartIce();
1387             return;
1388         } catch(e) {
1389             console.warn(e);
1390         }
1391     }
1392 
1393     // negotiate is async, but this returns immediately.
1394     c.negotiate(true);
1395 };
1396 
1397 /**
1398  * request sets the list of tracks.  If this is not called, or called with
1399  * a null argument, then the default is provided by ServerConnection.request.
1400  *
1401  * @param {Array<string>} what - a sequence of 'audio', 'video' or 'video-low'.
1402  */
1403 Stream.prototype.request = function(what) {
1404     let c = this;
1405     c.sc.send({
1406         type: 'requestStream',
1407         id: c.id,
1408         request: what,
1409     });
1410 };
1411 
1412 /**
1413  * updateStats is called periodically, if requested by setStatsInterval,
1414  * in order to recompute stream statistics and invoke the onstats handler.
1415  *
1416  * @function
1417  */
1418 Stream.prototype.updateStats = async function() {
1419     let c = this;
1420     let old = c.stats;
1421     /** @type{Object<string,unknown>} */
1422     let stats = {};
1423 
1424     let transceivers = c.pc.getTransceivers();
1425     for(let i = 0; i < transceivers.length; i++) {
1426         let t = transceivers[i];
1427         let stid = t.sender.track && t.sender.track.id;
1428         let rtid = t.receiver.track && t.receiver.track.id;
1429 
1430         let report = null;
1431         if(stid) {
1432             try {
1433                 report = await t.sender.getStats();
1434             } catch(e) {
1435             }
1436         }
1437 
1438         if(report) {
1439             for(let r of report.values()) {
1440                 if(stid && r.type === 'outbound-rtp') {
1441                     let id = stid;
1442                     // Firefox doesn't implement rid, use ssrc
1443                     // to discriminate simulcast tracks.
1444                     id = id + '-' + r.ssrc;
1445                     if(!('bytesSent' in r))
1446                         continue;
1447                     if(!stats[id])
1448                         stats[id] = {};
1449                     stats[id][r.type] = {};
1450                     stats[id][r.type].timestamp = r.timestamp;
1451                     stats[id][r.type].bytesSent = r.bytesSent;
1452                     if(old[id] && old[id][r.type])
1453                         stats[id][r.type].rate =
1454                         ((r.bytesSent - old[id][r.type].bytesSent) * 1000 /
1455                          (r.timestamp - old[id][r.type].timestamp)) * 8;
1456                 }
1457             }
1458         }
1459 
1460         report = null;
1461         if(rtid) {
1462             try {
1463                 report = await t.receiver.getStats();
1464             } catch(e) {
1465                 console.error(e);
1466             }
1467         }
1468 
1469         if(report) {
1470             for(let r of report.values()) {
1471                 if(rtid && r.type === 'inbound-rtp') {
1472                     if(!('totalAudioEnergy' in r))
1473                         continue;
1474                     if(!stats[rtid])
1475                         stats[rtid] = {};
1476                     stats[rtid][r.type] = {};
1477                     stats[rtid][r.type].timestamp = r.timestamp;
1478                     stats[rtid][r.type].totalAudioEnergy = r.totalAudioEnergy;
1479                     if(old[rtid] && old[rtid][r.type])
1480                         stats[rtid][r.type].audioEnergy =
1481                         (r.totalAudioEnergy - old[rtid][r.type].totalAudioEnergy) * 1000 /
1482                         (r.timestamp - old[rtid][r.type].timestamp);
1483                 }
1484             }
1485         }
1486     }
1487 
1488     c.stats = stats;
1489 
1490     if(c.onstats)
1491         c.onstats.call(c, c.stats);
1492 };
1493 
1494 /**
1495  * setStatsInterval sets the interval in milliseconds at which the onstats
1496  * handler will be called.  This is only useful for up streams.
1497  *
1498  * @param {number} ms - The interval in milliseconds.
1499  */
1500 Stream.prototype.setStatsInterval = function(ms) {
1501     let c = this;
1502     if(c.statsHandler) {
1503         clearInterval(c.statsHandler);
1504         c.statsHandler = null;
1505     }
1506 
1507     if(ms <= 0)
1508         return;
1509 
1510     c.statsHandler = setInterval(() => {
1511         c.updateStats();
1512     }, ms);
1513 };
1514 
1515 
1516 /**
1517  * A file in the process of being transferred.
1518  * These are stored in the ServerConnection.transferredFiles dictionary.
1519  *
1520  * State transitions:
1521  * @example
1522  * '' -> inviting -> connecting -> connected -> done -> closed
1523  * any -> cancelled -> closed
1524  *
1525  *
1526  * @parm {ServerConnection} sc
1527  * @parm {string} userid
1528  * @parm {string} rid
1529  * @parm {boolean} up
1530  * @parm {string} username
1531  * @parm {string} mimetype
1532  * @parm {number} size
1533  * @constructor
1534  */
1535 function TransferredFile(sc, userid, id, up, username, name, mimetype, size) {
1536     /**
1537      * The server connection this file is associated with.
1538      *
1539      * @type {ServerConnection}
1540      */
1541     this.sc = sc;
1542     /** The id of the remote peer.
1543      *
1544      * @type {string}
1545      */
1546     this.userid = userid;
1547     /**
1548      * The id of this file transfer.
1549      *
1550      * @type {string}
1551      */
1552     this.id = id;
1553     /**
1554      * True if this is an upload.
1555      *
1556      * @type {boolean}
1557      */
1558     this.up = up;
1559     /**
1560      * The state of this file transfer.  See the description of the
1561      * constructor for possible state transitions.
1562      *
1563      * @type {string}
1564      */
1565     this.state = '';
1566     /**
1567      * The username of the remote peer.
1568      *
1569      * @type {string}
1570      */
1571     this.username = username;
1572     /**
1573      * The name of the file being transferred.
1574      *
1575      * @type {string}
1576      */
1577     this.name = name;
1578     /**
1579      * The MIME type of the file being transferred.
1580      *
1581      * @type {string}
1582      */
1583     this.mimetype = mimetype;
1584     /**
1585      * The size in bytes of the file being transferred.
1586      *
1587      * @type {number}
1588      */
1589     this.size = size;
1590     /**
1591      * The file being uploaded.  Unused for downloads.
1592      *
1593      * @type {File}
1594      */
1595     this.file = null;
1596     /**
1597      * The peer connection used for the transfer.
1598      *
1599      * @type {RTCPeerConnection}
1600      */
1601     this.pc = null;
1602     /**
1603      * The datachannel used for the transfer.
1604      *
1605      * @type {RTCDataChannel}
1606      */
1607     this.dc = null;
1608     /**
1609      * Buffered remote ICE candidates.
1610      *
1611      * @type {Array<RTCIceCandidateInit>}
1612      */
1613     this.candidates = [];
1614     /**
1615      * The data received to date, stored as a list of blobs or array buffers,
1616      * depending on what the browser supports.
1617      *
1618      * @type {Array<Blob|ArrayBuffer>}
1619      */
1620     this.data = [];
1621     /**
1622      * The total size of the data received to date.
1623      *
1624      * @type {number}
1625      */
1626     this.datalen = 0;
1627     /**
1628      * The main filetransfer callback.
1629      *
1630      * This is called whenever the state of the transfer changes,
1631      * but may also be called multiple times in a single state, for example
1632      * in order to display a progress bar.  Call this.cancel in order
1633      * to cancel the transfer.
1634      *
1635      * @type {(this: TransferredFile, type: string, [data]: string) => void}
1636      */
1637     this.onevent = null;
1638 }
1639 
1640 /**
1641  * The full id of this file transfer, used as a key in the transferredFiles
1642  * dictionary.
1643  */
1644 TransferredFile.prototype.fullid = function() {
1645     return this.userid + (this.up ? '+' : '-') + this.id;
1646 };
1647 
1648 /**
1649  * Retrieve a transferred file from the transferredFiles dictionary.
1650  *
1651  * @param {string} userid
1652  * @param {string} fileid
1653  * @param {boolean} up
1654  * @returns {TransferredFile}
1655  */
1656 ServerConnection.prototype.getTransferredFile = function(userid, fileid, up) {
1657     return this.transferredFiles[userid + (up ? '+' : '-') + fileid];
1658 };
1659 
1660 /**
1661  * Close a file transfer and remove it from the transferredFiles dictionary.
1662  * Do not call this, call 'cancel' instead.
1663  */
1664 TransferredFile.prototype.close = function() {
1665     let f = this;
1666     if(f.state === 'closed')
1667         return;
1668     if(f.state !== 'done' && f.state !== 'cancelled')
1669         console.warn(
1670             `TransferredFile.close called in unexpected state ${f.state}`,
1671         );
1672     if(f.dc) {
1673         f.dc.onclose = null;
1674         f.dc.onerror = null;
1675         f.dc.onmessage = null;
1676     }
1677     if(f.pc)
1678         f.pc.close();
1679     f.dc = null;
1680     f.pc = null;
1681     f.data = [];
1682     f.datalen = 0;
1683     delete(f.sc.transferredFiles[f.fullid()]);
1684     f.event('closed');
1685 }
1686 
1687 /**
1688  * Buffer a chunk of data received during a file transfer.
1689  * Do not call this, it is called automatically when data is received.
1690  *
1691  * @param {Blob|ArrayBuffer} data
1692  */
1693 TransferredFile.prototype.bufferData = function(data) {
1694     let f = this;
1695     if(f.up)
1696         throw new Error('buffering data in the wrong direction');
1697     if(data instanceof Blob) {
1698         f.datalen += data.size;
1699     } else if(data instanceof ArrayBuffer) {
1700         f.datalen += data.byteLength;
1701     } else {
1702         throw new Error('unexpected type for received data');
1703     }
1704     f.data.push(data);
1705 }
1706 
1707 /**
1708  * Retreive the data buffered during a file transfer.  Don't call this.
1709  *
1710  * @returns {Blob}
1711  */
1712 TransferredFile.prototype.getBufferedData = function() {
1713     let f = this;
1714     if(f.up)
1715         throw new Error('buffering data in wrong direction');
1716     let blob = new Blob(f.data, {type: f.mimetype});
1717     if(blob.size != f.datalen)
1718         throw new Error('Inconsistent data size');
1719     f.data = [];
1720     f.datalen = 0;
1721     return blob;
1722 }
1723 
1724 /**
1725  * Set the file's state, and call the onevent callback.
1726  *
1727  * This calls the callback even if the state didn't change, which is
1728  * useful if the client needs to display a progress bar.
1729  *
1730  * @param {string} state
1731  * @param {any} [data]
1732  */
1733 TransferredFile.prototype.event = function(state, data) {
1734     let f = this;
1735     f.state = state;
1736     if(f.onevent)
1737         f.onevent.call(f, state, data);
1738 }
1739 
1740 
1741 /**
1742  * Cancel a file transfer.
1743  *
1744  * Depending on the state, this will either forcibly close the connection,
1745  * send a handshake, or do nothing.  It will set the state to cancelled.
1746  *
1747  * @param {string|Error} [data]
1748  */
1749 TransferredFile.prototype.cancel = function(data) {
1750     let f = this;
1751     if(f.state === 'closed')
1752         return;
1753     if(f.state !== '' && f.state !== 'done' && f.state !== 'cancelled') {
1754         let m = {
1755             type: f.up ? 'cancel' : 'reject',
1756             id: f.id,
1757         };
1758         if(data)
1759             m.message = data.toString();
1760         f.sc.userMessage('filetransfer', f.userid, m);
1761     }
1762     if(f.state !== 'done' && f.state !== 'cancelled')
1763         f.event('cancelled', data);
1764     f.close();
1765 }
1766 
1767 /**
1768  * Forcibly terminate a file transfer.
1769  *
1770  * This is like cancel, but will not attempt to handshake.
1771  * Use cancel instead of this, unless you know what you are doing.
1772  *
1773  * @param {string|Error} [data]
1774  */
1775 TransferredFile.prototype.fail = function(data) {
1776     let f = this;
1777     if(f.state === 'done' || f.state === 'cancelled' || f.state === 'closed')
1778         return;
1779     f.event('cancelled', data);
1780     f.close();
1781 }
1782 
1783 /**
1784  * Initiate a file upload.
1785  *
1786  * This will cause the onfiletransfer callback to be called, at which
1787  * point you should set up the onevent callback.
1788  *
1789  * @param {string} id
1790  * @param {File} file
1791  */
1792 ServerConnection.prototype.sendFile = function(id, file) {
1793     let sc = this;
1794     let fileid = newRandomId();
1795     let user = sc.users[id];
1796     if(!user)
1797         throw new Error('offering upload to unknown user');
1798     let f = new TransferredFile(
1799         sc, id, fileid, true, user.username, file.name, file.type, file.size,
1800     );
1801     f.file = file;
1802 
1803     try {
1804         if(sc.onfiletransfer)
1805             sc.onfiletransfer.call(sc, f);
1806         else
1807             throw new Error('this client does not implement file transfer');
1808     } catch(e) {
1809         f.cancel(e);
1810         return;
1811     }
1812 
1813     sc.transferredFiles[f.fullid()] = f;
1814     sc.userMessage('filetransfer', id, {
1815         type: 'invite',
1816         id: fileid,
1817         name: f.name,
1818         size: f.size,
1819         mimetype: f.mimetype,
1820     });
1821     f.event('inviting');
1822 }
1823 
1824 /**
1825  * Receive a file.
1826  *
1827  * Call this after the onfiletransfer callback has yielded an incoming
1828  * file (up field set to false).  If you wish to reject the file transfer,
1829  * call cancel instead.
1830  */
1831 TransferredFile.prototype.receive = async function() {
1832     let f = this;
1833     if(f.up)
1834         throw new Error('Receiving in wrong direction');
1835     if(f.pc)
1836         throw new Error('Download already in progress');
1837     let pc = new RTCPeerConnection(f.sc.getRTCConfiguration());
1838     if(!pc) {
1839         let err = new Error("Couldn't create peer connection");
1840         f.fail(err);
1841         return;
1842     }
1843     f.pc = pc;
1844     f.event('connecting');
1845 
1846     f.candidates = [];
1847     pc.onsignalingstatechange = function(e) {
1848         if(pc.signalingState === 'stable') {
1849             f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
1850             f.candidates = [];
1851         }
1852     };
1853     pc.onicecandidate = function(e) {
1854         f.sc.userMessage('filetransfer', f.userid, {
1855             type: 'downice',
1856             id: f.id,
1857             candidate: e.candidate,
1858         });
1859     };
1860     f.dc = pc.createDataChannel('file');
1861     f.data = [];
1862     f.datalen = 0;
1863     f.dc.onclose = function(e) {
1864         f.cancel('remote peer closed connection');
1865     };
1866     f.dc.onmessage = function(e) {
1867         f.receiveData(e.data).catch(e => f.cancel(e));
1868     };
1869     f.dc.onerror = function(e) {
1870         /** @ts-ignore */
1871         let err = e.error;
1872         f.cancel(err)
1873     };
1874     let offer = await pc.createOffer();
1875     if(!offer) {
1876         f.cancel(new Error("Couldn't create offer"));
1877         return;
1878     }
1879     await pc.setLocalDescription(offer);
1880     f.sc.userMessage('filetransfer', f.userid, {
1881         type: 'offer',
1882         id: f.id,
1883         sdp: pc.localDescription.sdp,
1884     });
1885 }
1886 
1887 /**
1888  * Negotiate a file transfer on the sender side.
1889  * Don't call this, it is called automatically we receive an offer.
1890  *
1891  * @param {string} sdp
1892  */
1893 TransferredFile.prototype.answer = async function(sdp) {
1894     let f = this;
1895     if(!f.up)
1896         throw new Error('Sending file in wrong direction');
1897     if(f.pc)
1898         throw new Error('Transfer already in progress');
1899     let pc = new RTCPeerConnection(f.sc.getRTCConfiguration());
1900     if(!pc) {
1901         let err = new Error("Couldn't create peer connection");
1902         f.fail(err);
1903         return;
1904     }
1905     f.pc = pc;
1906     f.event('connecting');
1907 
1908     f.candidates = [];
1909     pc.onicecandidate = function(e) {
1910         f.sc.userMessage('filetransfer', f.userid, {
1911             type: 'upice',
1912             id: f.id,
1913             candidate: e.candidate,
1914         });
1915     };
1916     pc.onsignalingstatechange = function(e) {
1917         if(pc.signalingState === 'stable') {
1918             f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
1919             f.candidates = [];
1920         }
1921     };
1922     pc.ondatachannel = function(e) {
1923         if(f.dc) {
1924             f.cancel(new Error('Duplicate datachannel'));
1925             return;
1926         }
1927         f.dc = /** @type{RTCDataChannel} */(e.channel);
1928         f.dc.onclose = function(e) {
1929             f.cancel('remote peer closed connection');
1930         };
1931         f.dc.onerror = function(e) {
1932             /** @ts-ignore */
1933             let err = e.error;
1934             f.cancel(err);
1935         }
1936         f.dc.onmessage = function(e) {
1937             if(e.data === 'done' && f.datalen === f.size) {
1938                 f.event('done');
1939                 f.dc.onclose = null;
1940                 f.dc.onerror = null;
1941                 f.close();
1942             } else {
1943                 f.cancel(new Error('unexpected data from receiver'));
1944             }
1945         }
1946         f.send().catch(e => f.cancel(e));
1947     };
1948 
1949     await pc.setRemoteDescription({
1950         type: 'offer',
1951         sdp: sdp,
1952     });
1953 
1954     let answer = await pc.createAnswer();
1955     if(!answer)
1956         throw new Error("Couldn't create answer");
1957     await pc.setLocalDescription(answer);
1958     f.sc.userMessage('filetransfer', f.userid, {
1959         type: 'answer',
1960         id: f.id,
1961         sdp: pc.localDescription.sdp,
1962     });
1963 
1964     f.event('connected');
1965 }
1966 
1967 /**
1968  * Transfer file data.  Don't call this, it is called automatically
1969  * after negotiation completes.
1970  */
1971 TransferredFile.prototype.send = async function() {
1972     let f = this;
1973     if(!f.up)
1974         throw new Error('sending in wrong direction');
1975     let r = f.file.stream().getReader();
1976 
1977     f.dc.bufferedAmountLowThreshold = 65536;
1978 
1979     /** @param {Uint8Array} a */
1980     async function write(a) {
1981         while(f.dc.bufferedAmount > f.dc.bufferedAmountLowThreshold) {
1982             await new Promise((resolve, reject) => {
1983                 if(!f.dc) {
1984                     reject(new Error('File is closed.'));
1985                     return;
1986                 }
1987                 f.dc.onbufferedamountlow = function(e) {
1988                     if(!f.dc) {
1989                         reject(new Error('File is closed.'));
1990                         return;
1991                     }
1992                     f.dc.onbufferedamountlow = null;
1993                     resolve();
1994                 }
1995             });
1996         }
1997         f.dc.send(a);
1998         f.datalen += a.length;
1999         // we're already in the connected state, but invoke callbacks to
2000         // that the application can display progress
2001         f.event('connected');
2002     }
2003 
2004     while(true) {
2005         let v = await r.read();
2006         if(v.done)
2007             break;
2008         let data = v.value;
2009         if(!(data instanceof Uint8Array))
2010             throw new Error('Unexpected type for chunk');
2011         /* Base SCTP only supports up to 16kB data chunks.  There are
2012            extensions to handle larger chunks, but they don't interoperate
2013            between browsers, so we chop the file into small pieces. */
2014         if(data.length <= 16384) {
2015             await write(data);
2016         } else {
2017             for(let i = 0; i < v.value.length; i += 16384) {
2018                 let d = data.subarray(i, Math.min(i + 16384, data.length));
2019                 await write(d);
2020             }
2021         }
2022     }
2023 }
2024 
2025 /**
2026  * Called after we receive an answer.  Don't call this.
2027  *
2028  * @param {string} sdp
2029  */
2030 TransferredFile.prototype.receiveFile = async function(sdp) {
2031     let f = this;
2032     if(f.up)
2033         throw new Error('Receiving in wrong direction');
2034     await f.pc.setRemoteDescription({
2035         type: 'answer',
2036         sdp: sdp,
2037     });
2038     f.event('connected');
2039 }
2040 
2041 /**
2042  * Called whenever we receive a chunk of data.  Don't call this.
2043  *
2044  * @param {Blob|ArrayBuffer} data
2045  */
2046 TransferredFile.prototype.receiveData = async function(data) {
2047     let f = this;
2048     if(f.up)
2049         throw new Error('Receiving in wrong direction');
2050     f.bufferData(data);
2051 
2052     if(f.datalen < f.size) {
2053         f.event('connected');
2054         return;
2055     }
2056 
2057     f.dc.onmessage = null;
2058 
2059     if(f.datalen != f.size) {
2060         f.cancel('extra data at end of file');
2061         return;
2062     }
2063 
2064     let blob = f.getBufferedData();
2065     if(blob.size != f.size) {
2066         f.cancel("inconsistent data size (this shouldn't happen)");
2067         return;
2068     }
2069     f.event('done', blob);
2070 
2071     // we've received the whole file.  Send the final handshake, but don't
2072     // complain if the peer has closed the channel in the meantime.
2073     await new Promise((resolve, reject) => {
2074         let timer = setTimeout(function(e) { resolve(); }, 2000);
2075         f.dc.onclose = function(e) {
2076             clearTimeout(timer);
2077             resolve();
2078         };
2079         f.dc.onerror = function(e) {
2080             clearTimeout(timer);
2081             resolve();
2082         };
2083         f.dc.send('done');
2084     });
2085 
2086     f.close();
2087 }
2088 
2089 /**
2090  * fileTransfer handles a usermessage of kind 'filetransfer'.  Don't call
2091  * this, it is called automatically as needed.
2092  *
2093  * @param {string} id
2094  * @param {string} username
2095  * @param {object} message
2096  */
2097 ServerConnection.prototype.fileTransfer = function(id, username, message) {
2098     let sc = this;
2099     switch(message.type) {
2100     case 'invite': {
2101         let f = new TransferredFile(
2102             sc, id, message.id, false, username,
2103             message.name, message.mimetype, message.size,
2104         );
2105         f.state = 'inviting';
2106 
2107         try {
2108             if(sc.onfiletransfer)
2109                 sc.onfiletransfer.call(sc, f);
2110             else {
2111                 f.cancel('this client does not implement file transfer');
2112                 return;
2113             }
2114         } catch(e) {
2115             f.cancel(e);
2116             return;
2117         }
2118 
2119         if(f.fullid() in sc.transferredFiles) {
2120             console.error('Duplicate id for file transfer');
2121             f.cancel("duplicate id (this shouldn't happen)");
2122             return;
2123         }
2124         sc.transferredFiles[f.fullid()] = f;
2125         break;
2126     }
2127     case 'offer': {
2128         let f = sc.getTransferredFile(id, message.id, true);
2129         if(!f) {
2130             console.error('Unexpected offer for file transfer');
2131             return;
2132         }
2133         f.answer(message.sdp).catch(e => f.cancel(e));
2134         break;
2135     }
2136     case 'answer': {
2137         let f = sc.getTransferredFile(id, message.id, false);
2138         if(!f) {
2139             console.error('Unexpected answer for file transfer');
2140             return;
2141         }
2142         f.receiveFile(message.sdp).catch(e => f.cancel(e));
2143         break;
2144     }
2145     case 'downice':
2146     case 'upice': {
2147         let f = sc.getTransferredFile(
2148             id, message.id, message.type === 'downice',
2149         );
2150         if(!f || !f.pc) {
2151             console.warn(`Unexpected ${message.type} for file transfer`);
2152             return;
2153         }
2154         if(f.pc.signalingState === 'stable')
2155             f.pc.addIceCandidate(message.candidate).catch(console.warn);
2156         else
2157             f.candidates.push(message.candidate);
2158         break;
2159     }
2160     case 'cancel':
2161     case 'reject': {
2162         let f = sc.getTransferredFile(id, message.id, message.type === 'reject');
2163         if(!f) {
2164             console.error(`Unexpected ${message.type} for file transfer`);
2165             return;
2166         }
2167         f.event('cancelled', message.value || null);
2168         f.close();
2169         break;
2170     }
2171     default:
2172         console.error(`Unknown filetransfer message ${message.type}`);
2173         break;
2174     }
2175 }
2176