1
Fork 0

Improve typing of javascript code.

We now enable typing of sfu.js.
This commit is contained in:
Juliusz Chroboczek 2020-09-20 14:33:13 +02:00
parent 4e14c29fbb
commit 0aa77441bd
3 changed files with 266 additions and 118 deletions

View File

@ -7,7 +7,8 @@
/** /**
* toHex formats an array as a hexadecimal string. * toHex formats an array as a hexadecimal string.
* @returns {string} * @param {number[]|Uint8Array} array - the array to format
* @returns {string} - the hexadecimal representation of array
*/ */
function toHex(array) { function toHex(array) {
let a = new Uint8Array(array); let a = new Uint8Array(array);
@ -37,43 +38,52 @@ function randomid() {
function ServerConnection() { function ServerConnection() {
/** /**
* The id of this connection. * The id of this connection.
*
* @type {string} * @type {string}
* @const
*/ */
this.id = randomid(); this.id = randomid();
/** /**
* The group that we have joined, or nil if we haven't joined yet. * The group that we have joined, or nil if we haven't joined yet.
*
* @type {string} * @type {string}
*/ */
this.group = null; this.group = null;
/** /**
* The underlying websocket. * The underlying websocket.
*
* @type {WebSocket} * @type {WebSocket}
*/ */
this.socket = null; this.socket = null;
/** /**
* The set of all up streams, indexed by their id. * The set of all up streams, indexed by their id.
* @type {Object.<string,Stream>} *
* @type {Object<string,Stream>}
*/ */
this.up = {}; this.up = {};
/** /**
* The set of all down streams, indexed by their id. * The set of all down streams, indexed by their id.
* @type {Object.<string,Stream>} *
* @type {Object<string,Stream>}
*/ */
this.down = {}; this.down = {};
/** /**
* The ICE configuration used by all associated streams. * The ICE configuration used by all associated streams.
* @type {Array.<RTCIceServer>} *
* @type {RTCIceServer[]}
*/ */
this.iceServers = null; this.iceServers = null;
/** /**
* The permissions granted to this connection. * The permissions granted to this connection.
* @type {Object.<string,boolean>} *
* @type {Object<string,boolean>}
*/ */
this.permissions = {}; this.permissions = {};
/** /**
* clientdata is a convenient place to attach data to a ServerConnection. * clientdata is a convenient place to attach data to a ServerConnection.
* It is not used by the library. * It is not used by the library.
* @type{Object.<string,any>} *
* @type{Object<any,any>}
*/ */
this.userdata = {}; this.userdata = {};
@ -81,46 +91,54 @@ function ServerConnection() {
/** /**
* onconnected is called when the connection has been established * onconnected is called when the connection has been established
* @type{function(): any} *
* @type{(this: ServerConnection) => any}
*/ */
this.onconnected = null; this.onconnected = null;
/** /**
* onclose is called when the connection is closed * onclose is called when the connection is closed
* @type{function(number, string): any} *
* @type{(this: ServerConnection, code: number, reason: string) => any}
*/ */
this.onclose = null; this.onclose = null;
/** /**
* onuser is called whenever a user is added or removed from the group * onuser is called whenever a user is added or removed from the group
* @type{function(string, string, string): any} *
* @type{(this: ServerConnection, id: string, kind: string, username: string) => any}
*/ */
this.onuser = null; this.onuser = null;
/** /**
* onpermissions is called whenever the current user's permissions change * onpermissions is called whenever the current user's permissions change
* @type{function(Object.<string,boolean>): any} *
* @type{(this: ServerConnection, permissions: Object<string,boolean>) => any}
*/ */
this.onpermissions = null; this.onpermissions = null;
/** /**
* ondownstream is called whenever a new down stream is added. It * ondownstream is called whenever a new down stream is added. It
* should set up the stream's callbacks; actually setting up the UI * should set up the stream's callbacks; actually setting up the UI
* should be done in the stream's ondowntrack callback. * should be done in the stream's ondowntrack callback.
* @type{function(Stream): any} *
* @type{(this: ServerConnection, stream: Stream) => any}
*/ */
this.ondownstream = null; this.ondownstream = null;
/** /**
* onchat is called whenever a new chat message is received. * onchat is called whenever a new chat message is received.
* @type {function(string, string, string, string): any} *
* @type {(this: ServerConnection, id: string, username: string, kind: string, message: string) => any}
*/ */
this.onchat = null; this.onchat = null;
/** /**
* onclearchat is called whenever the server requests that the chat * onclearchat is called whenever the server requests that the chat
* be cleared. * be cleared.
* @type{function(): any} *
* @type{(this: ServerConnection) => any}
*/ */
this.onclearchat = null; this.onclearchat = null;
/** /**
* onusermessage is called when the server sends an error or warning * onusermessage is called when the server sends an error or warning
* message that should be displayed to the user. * message that should be displayed to the user.
* @type{function(string, string): any} *
* @type{(this: ServerConnection, kind: string, message: string) => any}
*/ */
this.onusermessage = null; this.onusermessage = null;
} }
@ -132,14 +150,14 @@ function ServerConnection() {
* @property {string} [id] * @property {string} [id]
* @property {string} [username] * @property {string} [username]
* @property {string} [password] * @property {string} [password]
* @property {Object.<string,boolean>} [permissions] * @property {Object<string,boolean>} [permissions]
* @property {string} [group] * @property {string} [group]
* @property {string} [value] * @property {string} [value]
* @property {RTCSessionDescriptionInit} [offer] * @property {RTCSessionDescriptionInit} [offer]
* @property {RTCSessionDescriptionInit} [answer] * @property {RTCSessionDescriptionInit} [answer]
* @property {RTCIceCandidate} [candidate] * @property {RTCIceCandidate} [candidate]
* @property {Object.<string,string>} [labels] * @property {Object<string,string>} [labels]
* @property {Object.<string,(boolean|number)>} [request] * @property {Object<string,(boolean|number)>} [request]
*/ */
/** /**
@ -157,17 +175,18 @@ ServerConnection.prototype.close = function() {
*/ */
ServerConnection.prototype.send = function(m) { ServerConnection.prototype.send = function(m) {
if(!this.socket || this.socket.readyState !== this.socket.OPEN) { if(!this.socket || this.socket.readyState !== this.socket.OPEN) {
// send on a closed connection doesn't throw // send on a closed socket doesn't throw
throw(new Error('Connection is not open')); throw(new Error('Connection is not open'));
} }
return this.socket.send(JSON.stringify(m)); return this.socket.send(JSON.stringify(m));
} }
/** getIceServers fetches an ICE configuration from the server and /**
* getIceServers fetches an ICE configuration from the server and
* populates the iceServers field of a ServerConnection. It is called * populates the iceServers field of a ServerConnection. It is called
* lazily by connect. * lazily by connect.
* *
* @returns {Promise<Array.<Object>>} * @returns {Promise<RTCIceServer[]>}
* @function * @function
*/ */
ServerConnection.prototype.getIceServers = async function() { ServerConnection.prototype.getIceServers = async function() {
@ -294,8 +313,8 @@ ServerConnection.prototype.connect = async function(url) {
/** /**
* login authenticates with the server. * login authenticates with the server.
* *
* @param {string} username * @param {string} username - the username to login as.
* @param {string} password * @param {string} password - the password.
*/ */
ServerConnection.prototype.login = function(username, password) { ServerConnection.prototype.login = function(username, password) {
this.send({ this.send({
@ -324,7 +343,7 @@ ServerConnection.prototype.join = function(group) {
* @param {string} what - One of '', 'audio', 'screenshare' or 'everything'. * @param {string} what - One of '', 'audio', 'screenshare' or 'everything'.
*/ */
ServerConnection.prototype.request = function(what) { ServerConnection.prototype.request = function(what) {
/** @type {Object.<string,boolean>} */ /** @type {Object<string,boolean>} */
let request = {}; let request = {};
switch(what) { switch(what) {
case '': case '':
@ -422,7 +441,7 @@ ServerConnection.prototype.chat = function(username, kind, message) {
* *
* @param {string} kind - One of "clearchat", "lock", "unlock", "record or * @param {string} kind - One of "clearchat", "lock", "unlock", "record or
* "unrecord". * "unrecord".
* @param {string} [message] * @param {string} [message] - An optional user-readable message.
*/ */
ServerConnection.prototype.groupAction = function(kind, message) { ServerConnection.prototype.groupAction = function(kind, message) {
this.send({ this.send({
@ -436,8 +455,8 @@ ServerConnection.prototype.groupAction = function(kind, message) {
* userAction sends a request to act on a user. * userAction sends a request to act on a user.
* *
* @param {string} kind - One of "op", "unop", "kick", "present", "unpresent". * @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
* @param {string} id * @param {string} id - The id of the user to act upon.
* @param {string} [message] * @param {string} [message] - An optional user-readable message.
*/ */
ServerConnection.prototype.userAction = function(kind, id, message) { ServerConnection.prototype.userAction = function(kind, id, message) {
this.send({ this.send({
@ -452,7 +471,7 @@ ServerConnection.prototype.userAction = function(kind, id, message) {
* Called when we receive an offer from the server. Don't call this. * Called when we receive an offer from the server. Don't call this.
* *
* @param {string} id * @param {string} id
* @param {Object.<string, string>} labels * @param {Object<string, string>} labels
* @param {RTCSessionDescriptionInit} offer * @param {RTCSessionDescriptionInit} offer
* @param {boolean} renegotiate * @param {boolean} renegotiate
* @function * @function
@ -656,12 +675,14 @@ function Stream(sc, id, pc) {
* The associated ServerConnection. * The associated ServerConnection.
* *
* @type {ServerConnection} * @type {ServerConnection}
* @const
*/ */
this.sc = sc; this.sc = sc;
/** /**
* The id of this stream. * The id of this stream.
* *
* @type {string} * @type {string}
* @const
*/ */
this.id = id; this.id = id;
/** /**
@ -693,20 +714,20 @@ function Stream(sc, id, pc) {
/** /**
* Track labels, indexed by track id. * Track labels, indexed by track id.
* *
* @type {Object.<string,string>} * @type {Object<string,string>}
*/ */
this.labels = {}; this.labels = {};
/** /**
* Track labels, indexed by mid. * Track labels, indexed by mid.
* *
* @type {Object.<string,string>} * @type {Object<string,string>}
*/ */
this.labelsByMid = {}; this.labelsByMid = {};
/** /**
* Buffered ICE candidates. This will be flushed by flushIceCandidates * Buffered ICE candidates. This will be flushed by flushIceCandidates
* when the PC becomes stable. * when the PC becomes stable.
* *
* @type {Array.<RTCIceCandidate>} * @type {RTCIceCandidate[]}
*/ */
this.iceCandidates = []; this.iceCandidates = [];
/** /**
@ -721,7 +742,7 @@ function Stream(sc, id, pc) {
* a dictionary indexed by track id, with each value a disctionary of * a dictionary indexed by track id, with each value a disctionary of
* statistics. * statistics.
* *
* @type {Object.<string,any>} * @type {Object<string,any>}
*/ */
this.stats = {}; this.stats = {};
/** /**
@ -734,7 +755,7 @@ function Stream(sc, id, pc) {
/** /**
* clientdata is a convenient place to attach data to a Stream. * clientdata is a convenient place to attach data to a Stream.
* It is not used by the library. * It is not used by the library.
* @type{Object.<string,any>} * @type{Object<any,any>}
*/ */
this.userdata = {}; this.userdata = {};
@ -743,21 +764,21 @@ function Stream(sc, id, pc) {
/** /**
* onclose is called when the stream is closed. * onclose is called when the stream is closed.
* *
* @type{function(): any} * @type{(this: Stream) => any}
*/ */
this.onclose = null; this.onclose = null;
/** /**
* onerror is called whenever an error occurs. If the error is * onerror is called whenever an error occurs. If the error is
* fatal, then onclose will be called afterwards. * fatal, then onclose will be called afterwards.
* *
* @type{function(any): any} * @type{(this: Stream, error: any) => any}
*/ */
this.onerror = null; this.onerror = null;
/** /**
* onnegotiationcompleted is called whenever negotiation or * onnegotiationcompleted is called whenever negotiation or
* renegotiation has completed. * renegotiation has completed.
* *
* @type{function(): any} * @type{(this: Stream) => any}
*/ */
this.onnegotiationcompleted = null; this.onnegotiationcompleted = null;
/** /**
@ -765,32 +786,32 @@ function Stream(sc, id, pc) {
* If the stream parameter differs from its previous value, then it * If the stream parameter differs from its previous value, then it
* indicates that the old stream has been discarded. * indicates that the old stream has been discarded.
* *
* @type{function(MediaStreamTrack, RTCRtpTransceiver, string, MediaStream): any} * @type{(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, label: string, stream: MediaStream) => any}
*/ */
this.ondowntrack = null; this.ondowntrack = null;
/** /**
* onlabel is called whenever the server sets a new label for the stream. * onlabel is called whenever the server sets a new label for the stream.
* *
* @type{function(string): any} * @type{(this: Stream, label: string) => any}
*/ */
this.onlabel = null; this.onlabel = null;
/** /**
* onstatus is called whenever the status of the stream changes. * onstatus is called whenever the status of the stream changes.
* *
* @type{function(string): any} * @type{(this: Stream, status: string) => any}
*/ */
this.onstatus = null; this.onstatus = null;
/** /**
* onabort is called when the server requested that an up stream be * onabort is called when the server requested that an up stream be
* closed. It is the resposibility of the client to close the stream. * closed. It is the resposibility of the client to close the stream.
* *
* @type{function(): any} * @type{(this: Stream) => any}
*/ */
this.onabort = null; this.onabort = null;
/** /**
* onstats is called when we have new statistics about the connection * onstats is called when we have new statistics about the connection
* *
* @type{function(Object.<string,any>): any} * @type{(this: Stream, stats: Object<any,any>) => any}
*/ */
this.onstats = null; this.onstats = null;
} }
@ -834,6 +855,7 @@ Stream.prototype.close = function(sendclose) {
* @function * @function
*/ */
Stream.prototype.flushIceCandidates = async function () { Stream.prototype.flushIceCandidates = async function () {
/** @type {Promise<any>[]} */
let promises = []; let promises = [];
this.iceCandidates.forEach(c => { this.iceCandidates.forEach(c => {
promises.push(this.pc.addIceCandidate(c).catch(console.warn)); promises.push(this.pc.addIceCandidate(c).catch(console.warn));
@ -845,16 +867,16 @@ Stream.prototype.flushIceCandidates = async function () {
/** /**
* negotiate negotiates or renegotiates an up stream. It is called * negotiate negotiates or renegotiates an up stream. It is called
* automatically when required. If the client requires renegotiation, it * automatically when required. If the client requires renegotiation, it
* is probably more effective to call restartIce on the underlying PC * is probably better to call restartIce which will cause negotiate to be
* rather than invoking this function directly. * called asynchronously.
* *
* @function * @function
* @param {boolean} [restartIce] * @param {boolean} [restartIce] - Whether to restart ICE.
*/ */
Stream.prototype.negotiate = async function (restartIce) { Stream.prototype.negotiate = async function (restartIce) {
let c = this; let c = this;
let options = null; let options = {};
if(restartIce) if(restartIce)
options = {iceRestart: true}; options = {iceRestart: true};
let offer = await c.pc.createOffer(options); let offer = await c.pc.createOffer(options);
@ -892,8 +914,7 @@ Stream.prototype.negotiate = async function (restartIce) {
Stream.prototype.restartIce = function () { Stream.prototype.restartIce = function () {
let c = this; let c = this;
/** @ts-ignore */ if('restartIce' in c.pc) {
if(typeof c.pc.restartIce === 'function') {
try { try {
/** @ts-ignore */ /** @ts-ignore */
c.pc.restartIce(); c.pc.restartIce();
@ -988,7 +1009,7 @@ Stream.prototype.updateStats = async function() {
* setStatsInterval sets the interval in milliseconds at which the onstats * setStatsInterval sets the interval in milliseconds at which the onstats
* handler will be called. This is only useful for up streams. * handler will be called. This is only useful for up streams.
* *
* @param {number} ms * @param {number} ms - The interval in milliseconds.
*/ */
Stream.prototype.setStatsInterval = function(ms) { Stream.prototype.setStatsInterval = function(ms) {
let c = this; let c = this;

View File

@ -103,7 +103,6 @@ function storeSettings(settings) {
* *
* @returns {settings} * @returns {settings}
*/ */
function getSettings() { function getSettings() {
/** @type {settings} */ /** @type {settings} */
let settings; let settings;
@ -127,22 +126,50 @@ function updateSettings(settings) {
storeSettings(s); storeSettings(s);
} }
/**
* @param {string} id
*/
function getSelectElement(id) {
let elt = document.getElementById(id);
if(!elt || !(elt instanceof HTMLSelectElement))
throw new Error(`Couldn't find ${id}`);
return elt;
}
/**
* @param {string} id
*/
function getInputElement(id) {
let elt = document.getElementById(id);
if(!elt || !(elt instanceof HTMLInputElement))
throw new Error(`Couldn't find ${id}`);
return elt;
}
/**
* @param {string} id
*/
function getButtonElement(id) {
let elt = document.getElementById(id);
if(!elt || !(elt instanceof HTMLButtonElement))
throw new Error(`Couldn't find ${id}`);
return elt;
}
function reflectSettings() { function reflectSettings() {
let settings = getSettings(); let settings = getSettings();
let store = false; let store = false;
setLocalMute(settings.localMute); setLocalMute(settings.localMute);
let videoselect = let videoselect = getSelectElement('videoselect');
/** @type {HTMLSelectElement} */(document.getElementById('videoselect'));
if(!settings.video || !selectOptionAvailable(videoselect, settings.video)) { if(!settings.video || !selectOptionAvailable(videoselect, settings.video)) {
settings.video = selectOptionDefault(videoselect); settings.video = selectOptionDefault(videoselect);
store = true; store = true;
} }
videoselect.value = settings.video; videoselect.value = settings.video;
let audioselect = let audioselect = getSelectElement('audioselect');
/** @type {HTMLSelectElement} */(document.getElementById('audioselect'));
if(!settings.audio || !selectOptionAvailable(audioselect, settings.audio)) { if(!settings.audio || !selectOptionAvailable(audioselect, settings.audio)) {
settings.audio = selectOptionDefault(audioselect); settings.audio = selectOptionDefault(audioselect);
store = true; store = true;
@ -150,24 +177,24 @@ function reflectSettings() {
audioselect.value = settings.audio; audioselect.value = settings.audio;
if(settings.request) if(settings.request)
document.getElementById('requestselect').value = settings.request; getSelectElement('requestselect').value = settings.request;
else { else {
settings.request = document.getElementById('requestselect').value; settings.request = getSelectElement('requestselect').value;
store = true; store = true;
} }
if(settings.send) if(settings.send)
document.getElementById('sendselect').value = settings.send; getSelectElement('sendselect').value = settings.send;
else { else {
settings.send = document.getElementById('sendselect').value; settings.send = getSelectElement('sendselect').value;
store = true; store = true;
} }
document.getElementById('activitybox').checked = settings.activityDetection; getInputElement('activitybox').checked = settings.activityDetection;
document.getElementById('blackboardbox').checked = settings.blackboardMode; getInputElement('blackboardbox').checked = settings.blackboardMode;
document.getElementById('studiobox').checked = settings.studioMode; getInputElement('studiobox').checked = settings.studioMode;
if(store) if(store)
storeSettings(settings); storeSettings(settings);
@ -218,9 +245,9 @@ function setConnected(connected) {
} else { } else {
resetUsers(); resetUsers();
let userpass = getUserPass(); let userpass = getUserPass();
document.getElementById('username').value = getInputElement('username').value =
userpass ? userpass.username : ''; userpass ? userpass.username : '';
document.getElementById('password').value = getInputElement('password').value =
userpass ? userpass.password : ''; userpass ? userpass.password : '';
statspan.textContent = 'Disconnected'; statspan.textContent = 'Disconnected';
statspan.classList.remove('connected'); statspan.classList.remove('connected');
@ -234,6 +261,7 @@ function setConnected(connected) {
} }
} }
/** @this {ServerConnection} */
function gotConnected() { function gotConnected() {
setConnected(true); setConnected(true);
let up = getUserPass(); let up = getUserPass();
@ -243,6 +271,7 @@ function gotConnected() {
} }
/** /**
* @this {ServerConnection}
* @param {number} code * @param {number} code
* @param {string} reason * @param {string} reason
*/ */
@ -255,6 +284,7 @@ function gotClose(code, reason) {
} }
/** /**
* @this {ServerConnection}
* @param {Stream} c * @param {Stream} c
*/ */
function gotDownStream(c) { function gotDownStream(c) {
@ -291,12 +321,12 @@ setViewportHeight();
addEventListener('resize', setViewportHeight); addEventListener('resize', setViewportHeight);
addEventListener('orientationchange', setViewportHeight); addEventListener('orientationchange', setViewportHeight);
document.getElementById('presentbutton').onclick = function(e) { getButtonElement('presentbutton').onclick = function(e) {
e.preventDefault(); e.preventDefault();
addLocalMedia(); addLocalMedia();
}; };
document.getElementById('unpresentbutton').onclick = function(e) { getButtonElement('unpresentbutton').onclick = function(e) {
e.preventDefault(); e.preventDefault();
delUpMediaKind('local'); delUpMediaKind('local');
resizePeers(); resizePeers();
@ -353,26 +383,34 @@ function setLocalMute(mute) {
} }
} }
document.getElementById('videoselect').onchange = function(e) { getSelectElement('videoselect').onchange = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this');
updateSettings({video: this.value}); updateSettings({video: this.value});
changePresentation(); changePresentation();
}; };
document.getElementById('audioselect').onchange = function(e) { getSelectElement('audioselect').onchange = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this');
updateSettings({audio: this.value}); updateSettings({audio: this.value});
changePresentation(); changePresentation();
}; };
document.getElementById('blackboardbox').onchange = function(e) { getInputElement('blackboardbox').onchange = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this');
updateSettings({blackboardMode: this.checked}); updateSettings({blackboardMode: this.checked});
changePresentation(); changePresentation();
} }
document.getElementById('studiobox').onchange = function(e) { getInputElement('studiobox').onchange = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this');
updateSettings({studioMode: this.checked}); updateSettings({studioMode: this.checked});
changePresentation(); changePresentation();
} }
@ -413,7 +451,9 @@ function getMaxVideoThroughput() {
} }
} }
document.getElementById('sendselect').onchange = async function(e) { getSelectElement('sendselect').onchange = async function(e) {
if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this');
updateSettings({send: this.value}); updateSettings({send: this.value});
let t = getMaxVideoThroughput(); let t = getMaxVideoThroughput();
for(let id in serverConnection.up) { for(let id in serverConnection.up) {
@ -423,8 +463,10 @@ document.getElementById('sendselect').onchange = async function(e) {
} }
} }
document.getElementById('requestselect').onchange = function(e) { getSelectElement('requestselect').onchange = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this');
updateSettings({request: this.value}); updateSettings({request: this.value});
serverConnection.request(this.value); serverConnection.request(this.value);
}; };
@ -433,7 +475,9 @@ const activityDetectionInterval = 200;
const activityDetectionPeriod = 700; const activityDetectionPeriod = 700;
const activityDetectionThreshold = 0.2; const activityDetectionThreshold = 0.2;
document.getElementById('activitybox').onchange = function(e) { getInputElement('activitybox').onchange = function(e) {
if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this');
updateSettings({activityDetection: this.checked}); updateSettings({activityDetection: this.checked});
for(let id in serverConnection.down) { for(let id in serverConnection.down) {
let c = serverConnection.down[id]; let c = serverConnection.down[id];
@ -446,6 +490,10 @@ document.getElementById('activitybox').onchange = function(e) {
} }
} }
/**
* @this {Stream}
* @param {Object<string,any>} stats
*/
function gotUpStats(stats) { function gotUpStats(stats) {
let c = this; let c = this;
@ -477,8 +525,12 @@ function setActive(c, value) {
peer.classList.remove('peer-active'); peer.classList.remove('peer-active');
} }
/**
* @this {Stream}
* @param {Object<string,any>} stats
*/
function gotDownStats(stats) { function gotDownStats(stats) {
if(!document.getElementById('activitybox').checked) if(!getInputElement('activitybox').checked)
return; return;
let c = this; let c = this;
@ -514,7 +566,11 @@ function addSelectOption(select, label, value) {
if(!value) if(!value)
value = label; value = label;
for(let i = 0; i < select.children.length; i++) { for(let i = 0; i < select.children.length; i++) {
let child = /** @type {HTMLOptionElement} */ (select.children[i]); let child = select.children[i];
if(!(child instanceof HTMLOptionElement)) {
console.warn('Unexpected select child');
continue;
}
if(child.value === value) { if(child.value === value) {
if(child.label !== label) { if(child.label !== label) {
child.label = label; child.label = label;
@ -536,10 +592,13 @@ function addSelectOption(select, label, value) {
function selectOptionAvailable(select, value) { function selectOptionAvailable(select, value) {
let children = select.children; let children = select.children;
for(let i = 0; i < children.length; i++) { for(let i = 0; i < children.length; i++) {
let child = /** @type {HTMLOptionElement} */ (select.children[i]); let child = select.children[i];
if(child.value === value) { if(!(child instanceof HTMLOptionElement)) {
return true; console.warn('Unexpected select child');
continue;
} }
if(child.value === value)
return true;
} }
return false; return false;
} }
@ -551,7 +610,11 @@ function selectOptionAvailable(select, value) {
function selectOptionDefault(select) { function selectOptionDefault(select) {
/* First non-empty option. */ /* First non-empty option. */
for(let i = 0; i < select.children.length; i++) { for(let i = 0; i < select.children.length; i++) {
let child = /** @type {HTMLOptionElement} */ (select.children[i]); let child = select.children[i];
if(!(child instanceof HTMLOptionElement)) {
console.warn('Unexpected select child');
continue;
}
if(child.value) if(child.value)
return child.value; return child.value;
} }
@ -587,13 +650,13 @@ async function setMediaChoices(done) {
if(d.kind === 'videoinput') { if(d.kind === 'videoinput') {
if(!label) if(!label)
label = `Camera ${cn}`; label = `Camera ${cn}`;
addSelectOption(document.getElementById('videoselect'), addSelectOption(getSelectElement('videoselect'),
label, d.deviceId); label, d.deviceId);
cn++; cn++;
} else if(d.kind === 'audioinput') { } else if(d.kind === 'audioinput') {
if(!label) if(!label)
label = `Microphone ${mn}`; label = `Microphone ${mn}`;
addSelectOption(document.getElementById('audioselect'), addSelectOption(getSelectElement('audioselect'),
label, d.deviceId); label, d.deviceId);
mn++; mn++;
} }
@ -693,6 +756,7 @@ async function addLocalMedia(id) {
stopUpMedia(old); stopUpMedia(old);
let constraints = {audio: audio, video: video}; let constraints = {audio: audio, video: video};
/** @type {MediaStream} */
let stream = null; let stream = null;
try { try {
stream = await navigator.mediaDevices.getUserMedia(constraints); stream = await navigator.mediaDevices.getUserMedia(constraints);
@ -717,11 +781,11 @@ async function addLocalMedia(id) {
t.enabled = false; t.enabled = false;
} else if(t.kind == 'video') { } else if(t.kind == 'video') {
if(settings.blackboardMode) { if(settings.blackboardMode) {
if('contentHint' in t) /** @ts-ignore */
t.contentHint = 'detail'; t.contentHint = 'detail';
} }
} }
let sender = c.pc.addTrack(t, stream); c.pc.addTrack(t, stream);
}); });
c.onstats = gotUpStats; c.onstats = gotUpStats;
@ -730,12 +794,16 @@ async function addLocalMedia(id) {
setButtonsVisibility(); setButtonsVisibility();
} }
async function addShareMedia(setup) { async function addShareMedia() {
if(!getUserPass()) if(!getUserPass())
return; return;
/** @type {MediaStream} */
let stream = null; let stream = null;
try { try {
if(!('getDisplayMedia' in navigator.mediaDevices))
throw new Error('Your browser does not support screen sharing');
/** @ts-ignore */
stream = await navigator.mediaDevices.getDisplayMedia({video: true}); stream = await navigator.mediaDevices.getDisplayMedia({video: true});
} catch(e) { } catch(e) {
console.error(e); console.error(e);
@ -747,7 +815,7 @@ async function addShareMedia(setup) {
c.kind = 'screenshare'; c.kind = 'screenshare';
c.stream = stream; c.stream = stream;
stream.getTracks().forEach(t => { stream.getTracks().forEach(t => {
let sender = c.pc.addTrack(t, stream); c.pc.addTrack(t, stream);
t.onended = e => { t.onended = e => {
delUpMedia(c); delUpMedia(c);
}; };
@ -807,6 +875,9 @@ function delUpMediaKind(kind) {
hideVideo(); hideVideo();
} }
/**
* @param {string} kind
*/
function findUpMedia(kind) { function findUpMedia(kind) {
for(let id in serverConnection.up) { for(let id in serverConnection.up) {
if(serverConnection.up[id].kind === kind) if(serverConnection.up[id].kind === kind)
@ -815,6 +886,9 @@ function findUpMedia(kind) {
return null; return null;
} }
/**
* @param {boolean} mute
*/
function muteLocalTracks(mute) { function muteLocalTracks(mute) {
if(!serverConnection) if(!serverConnection)
return; return;
@ -846,12 +920,14 @@ function setMedia(c, isUp) {
peersdiv.appendChild(div); peersdiv.appendChild(div);
} }
let media = document.getElementById('media-' + c.id); let media = /** @type {HTMLVideoElement} */
(document.getElementById('media-' + c.id));
if(!media) { if(!media) {
media = document.createElement('video'); media = document.createElement('video');
media.id = 'media-' + c.id; media.id = 'media-' + c.id;
media.classList.add('media'); media.classList.add('media');
media.autoplay = true; media.autoplay = true;
/** @ts-ignore */
media.playsinline = true; media.playsinline = true;
media.controls = true; media.controls = true;
if(isUp) if(isUp)
@ -883,7 +959,9 @@ function delMedia(id) {
let peer = document.getElementById('peer-' + id); let peer = document.getElementById('peer-' + id);
if(!peer) if(!peer)
throw new Error('Removing unknown media'); throw new Error('Removing unknown media');
let media = document.getElementById('media-' + id);
let media = /** @type{HTMLVideoElement} */
(document.getElementById('media-' + id));
media.srcObject = null; media.srcObject = null;
mediadiv.removeChild(peer); mediadiv.removeChild(peer);
@ -941,14 +1019,19 @@ function resizePeers() {
if (!count) if (!count)
// No video, nothing to resize. // No video, nothing to resize.
return; return;
let size = 100 / columns;
let container = document.getElementById("video-container") let container = document.getElementById("video-container")
// Peers div has total padding of 30px, we remove 30 on offsetHeight // Peers div has total padding of 30px, we remove 30 on offsetHeight
let max_video_height = Math.trunc((peers.offsetHeight - 30) / columns); let max_video_height = Math.trunc((peers.offsetHeight - 30) / columns);
let media_list = document.getElementsByClassName("media"); let media_list = document.getElementsByClassName("media");
for(let i = 0; i < media_list.length; i++) for(let i = 0; i < media_list.length; i++) {
media_list[i].style['max_height'] = max_video_height + "px"; let media = media_list[i];
if(!(media instanceof HTMLMediaElement)) {
console.warn('Unexpected media');
continue;
}
media.style['max_height'] = max_video_height + "px";
}
if (count <= 2 && container.offsetHeight > container.offsetWidth) { if (count <= 2 && container.offsetHeight > container.offsetWidth) {
peers.style['grid-template-columns'] = "repeat(1, 1fr)"; peers.style['grid-template-columns'] = "repeat(1, 1fr)";
@ -957,7 +1040,7 @@ function resizePeers() {
} }
} }
/** @type{Object.<string,string>} */ /** @type{Object<string,string>} */
let users = {}; let users = {};
/** /**
@ -1051,7 +1134,7 @@ function clearUsername() {
} }
/** /**
* @param {Object.<string,boolean>} perms * @param {Object<string,boolean>} perms
*/ */
function gotPermissions(perms) { function gotPermissions(perms) {
displayUsername(); displayUsername();
@ -1064,7 +1147,7 @@ const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#=?]+[-a-zA-Z0-9@:%/_\\+~#=]/
/** /**
* @param {string} line * @param {string} line
* @returns {Array.<Text|HTMLElement>} * @returns {(Text|HTMLElement)[]}
*/ */
function formatLine(line) { function formatLine(line) {
let r = new RegExp(urlRegexp); let r = new RegExp(urlRegexp);
@ -1088,7 +1171,7 @@ function formatLine(line) {
} }
/** /**
* @param {Array.<string>} lines * @param {string[]} lines
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
function formatLines(lines) { function formatLines(lines) {
@ -1113,6 +1196,12 @@ function formatLines(lines) {
/** @type {lastMessage} */ /** @type {lastMessage} */
let lastMessage = {}; let lastMessage = {};
/**
* @param {string} peerId
* @param {string} nick
* @param {string} kind
* @param {string} message
*/
function addToChatbox(peerId, nick, kind, message){ function addToChatbox(peerId, nick, kind, message){
let userpass = getUserPass(); let userpass = getUserPass();
let row = document.createElement('div'); let row = document.createElement('div');
@ -1174,7 +1263,7 @@ function clearChat() {
* part may be quoted and may include backslash escapes. * part may be quoted and may include backslash escapes.
* *
* @param {string} line * @param {string} line
* @returns {Array.<string>} * @returns {string[]}
*/ */
function parseCommand(line) { function parseCommand(line) {
let i = 0; let i = 0;
@ -1204,7 +1293,8 @@ function parseCommand(line) {
} }
function handleInput() { function handleInput() {
let input = document.getElementById('input'); let input = /** @type {HTMLTextAreaElement} */
(document.getElementById('input'));
let data = input.value; let data = input.value;
input.value = ''; input.value = '';
@ -1356,39 +1446,66 @@ function chatResizer(e) {
document.getElementById('resizer').addEventListener('mousedown', chatResizer, false); document.getElementById('resizer').addEventListener('mousedown', chatResizer, false);
/** @enum {string} */
const MessageLevel = {
info: 'info',
warning: 'warning',
error: 'error',
}
function displayError(message, level, position, gravity) { /**
var background = "linear-gradient(to right, #e20a0a, #df2d2d)"; * @param {string} message
if (level === "info") { * @param {MessageLevel} [level]
background = "linear-gradient(to right, #529518, #96c93d)"; */
} function displayError(message, level) {
if (level === "warning") { if(!level)
level = MessageLevel.error;
var background = 'linear-gradient(to right, #e20a0a, #df2d2d)';
var position = 'center';
var gravity = 'top';
switch(level) {
case MessageLevel.info:
background = 'linear-gradient(to right, #529518, #96c93d)';
position = 'right';
gravity = 'bottom';
break;
case MessageLevel.warning:
background = "linear-gradient(to right, #edd800, #c9c200)"; background = "linear-gradient(to right, #edd800, #c9c200)";
break;
} }
/** @ts-ignore */
Toastify({ Toastify({
text: message, text: message,
duration: 4000, duration: 4000,
close: true, close: true,
position: position ? position: 'center', position: position,
gravity: gravity ? gravity : 'top', gravity: gravity,
backgroundColor: background, backgroundColor: background,
className: level, className: level,
}).showToast(); }).showToast();
} }
/**
* @param {string} message
*/
function displayWarning(message) { function displayWarning(message) {
let level = "warning"; return displayError(message, MessageLevel.warning);
return displayError(message, level);
} }
/**
* @param {string} message
*/
function displayMessage(message) { function displayMessage(message) {
return displayError(message, "info", "right", "bottom"); return displayError(message, MessageLevel.info);
} }
document.getElementById('userform').onsubmit = function(e) { document.getElementById('userform').onsubmit = function(e) {
e.preventDefault(); e.preventDefault();
let username = document.getElementById('username').value.trim(); let username = getInputElement('username').value.trim();
let password = document.getElementById('password').value; let password = getInputElement('password').value;
storeUserPass(username, password); storeUserPass(username, password);
serverConnect(); serverConnect();
}; };
@ -1438,6 +1555,8 @@ document.getElementById('clodeside').onclick = function(e) {
document.getElementById('collapse-video').onclick = function(e) { document.getElementById('collapse-video').onclick = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLElement))
throw new Error('Unexpected type for this');
let width = window.innerWidth; let width = window.innerWidth;
if (width <= 768) { if (width <= 768) {
let user_box = document.getElementById('userDropdown'); let user_box = document.getElementById('userDropdown');
@ -1453,6 +1572,8 @@ document.getElementById('collapse-video').onclick = function(e) {
document.getElementById('switch-video').onclick = function(e) { document.getElementById('switch-video').onclick = function(e) {
e.preventDefault(); e.preventDefault();
if(!(this instanceof HTMLElement))
throw new Error('Unexpected type for this');
showVideo(); showVideo();
this.style.display = ""; this.style.display = "";
document.getElementById('collapse-video').style.display = "block"; document.getElementById('collapse-video').style.display = "block";

View File

@ -4,10 +4,16 @@
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"declaration": true, "declaration": true,
"noImplicitThis": true,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"strictBindCallApply": true "strictFunctionTypes": true,
"strictBindCallApply": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true
}, },
"files": [ "files": [
"protocol.js" "protocol.js",
"sfu.js"
] ]
} }