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