mirror of
https://github.com/jech/galene.git
synced 2024-12-22 23:35:46 +01:00
Improve typing of javascript code.
We now enable typing of sfu.js.
This commit is contained in:
parent
4e14c29fbb
commit
0aa77441bd
3 changed files with 266 additions and 118 deletions
|
@ -7,7 +7,8 @@
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
let a = new Uint8Array(array);
|
||||
|
@ -37,43 +38,52 @@ function randomid() {
|
|||
function ServerConnection() {
|
||||
/**
|
||||
* The id of this connection.
|
||||
*
|
||||
* @type {string}
|
||||
* @const
|
||||
*/
|
||||
this.id = randomid();
|
||||
/**
|
||||
* The group that we have joined, or nil if we haven't joined yet.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.group = null;
|
||||
/**
|
||||
* The underlying websocket.
|
||||
*
|
||||
* @type {WebSocket}
|
||||
*/
|
||||
this.socket = null;
|
||||
/**
|
||||
* The set of all up streams, indexed by their id.
|
||||
* @type {Object.<string,Stream>}
|
||||
*
|
||||
* @type {Object<string,Stream>}
|
||||
*/
|
||||
this.up = {};
|
||||
/**
|
||||
* The set of all down streams, indexed by their id.
|
||||
* @type {Object.<string,Stream>}
|
||||
*
|
||||
* @type {Object<string,Stream>}
|
||||
*/
|
||||
this.down = {};
|
||||
/**
|
||||
* The ICE configuration used by all associated streams.
|
||||
* @type {Array.<RTCIceServer>}
|
||||
*
|
||||
* @type {RTCIceServer[]}
|
||||
*/
|
||||
this.iceServers = null;
|
||||
/**
|
||||
* The permissions granted to this connection.
|
||||
* @type {Object.<string,boolean>}
|
||||
*
|
||||
* @type {Object<string,boolean>}
|
||||
*/
|
||||
this.permissions = {};
|
||||
/**
|
||||
* clientdata is a convenient place to attach data to a ServerConnection.
|
||||
* It is not used by the library.
|
||||
* @type{Object.<string,any>}
|
||||
*
|
||||
* @type{Object<any,any>}
|
||||
*/
|
||||
this.userdata = {};
|
||||
|
||||
|
@ -81,46 +91,54 @@ function ServerConnection() {
|
|||
|
||||
/**
|
||||
* onconnected is called when the connection has been established
|
||||
* @type{function(): any}
|
||||
*
|
||||
* @type{(this: ServerConnection) => any}
|
||||
*/
|
||||
this.onconnected = null;
|
||||
/**
|
||||
* onclose is called when the connection is closed
|
||||
* @type{function(number, string): any}
|
||||
*
|
||||
* @type{(this: ServerConnection, code: number, reason: string) => any}
|
||||
*/
|
||||
this.onclose = null;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* ondownstream is called whenever a new down stream is added. It
|
||||
* should set up the stream's callbacks; actually setting up the UI
|
||||
* should be done in the stream's ondowntrack callback.
|
||||
* @type{function(Stream): any}
|
||||
*
|
||||
* @type{(this: ServerConnection, stream: Stream) => any}
|
||||
*/
|
||||
this.ondownstream = null;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* onclearchat is called whenever the server requests that the chat
|
||||
* be cleared.
|
||||
* @type{function(): any}
|
||||
*
|
||||
* @type{(this: ServerConnection) => any}
|
||||
*/
|
||||
this.onclearchat = null;
|
||||
/**
|
||||
* onusermessage is called when the server sends an error or warning
|
||||
* message that should be displayed to the user.
|
||||
* @type{function(string, string): any}
|
||||
*
|
||||
* @type{(this: ServerConnection, kind: string, message: string) => any}
|
||||
*/
|
||||
this.onusermessage = null;
|
||||
}
|
||||
|
@ -132,14 +150,14 @@ function ServerConnection() {
|
|||
* @property {string} [id]
|
||||
* @property {string} [username]
|
||||
* @property {string} [password]
|
||||
* @property {Object.<string,boolean>} [permissions]
|
||||
* @property {Object<string,boolean>} [permissions]
|
||||
* @property {string} [group]
|
||||
* @property {string} [value]
|
||||
* @property {RTCSessionDescriptionInit} [offer]
|
||||
* @property {RTCSessionDescriptionInit} [answer]
|
||||
* @property {RTCIceCandidate} [candidate]
|
||||
* @property {Object.<string,string>} [labels]
|
||||
* @property {Object.<string,(boolean|number)>} [request]
|
||||
* @property {Object<string,string>} [labels]
|
||||
* @property {Object<string,(boolean|number)>} [request]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -157,17 +175,18 @@ ServerConnection.prototype.close = function() {
|
|||
*/
|
||||
ServerConnection.prototype.send = function(m) {
|
||||
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'));
|
||||
}
|
||||
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
|
||||
* lazily by connect.
|
||||
*
|
||||
* @returns {Promise<Array.<Object>>}
|
||||
* @returns {Promise<RTCIceServer[]>}
|
||||
* @function
|
||||
*/
|
||||
ServerConnection.prototype.getIceServers = async function() {
|
||||
|
@ -294,8 +313,8 @@ ServerConnection.prototype.connect = async function(url) {
|
|||
/**
|
||||
* login authenticates with the server.
|
||||
*
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {string} username - the username to login as.
|
||||
* @param {string} password - the password.
|
||||
*/
|
||||
ServerConnection.prototype.login = function(username, password) {
|
||||
this.send({
|
||||
|
@ -324,7 +343,7 @@ ServerConnection.prototype.join = function(group) {
|
|||
* @param {string} what - One of '', 'audio', 'screenshare' or 'everything'.
|
||||
*/
|
||||
ServerConnection.prototype.request = function(what) {
|
||||
/** @type {Object.<string,boolean>} */
|
||||
/** @type {Object<string,boolean>} */
|
||||
let request = {};
|
||||
switch(what) {
|
||||
case '':
|
||||
|
@ -422,7 +441,7 @@ ServerConnection.prototype.chat = function(username, kind, message) {
|
|||
*
|
||||
* @param {string} kind - One of "clearchat", "lock", "unlock", "record or
|
||||
* "unrecord".
|
||||
* @param {string} [message]
|
||||
* @param {string} [message] - An optional user-readable message.
|
||||
*/
|
||||
ServerConnection.prototype.groupAction = function(kind, message) {
|
||||
this.send({
|
||||
|
@ -436,8 +455,8 @@ ServerConnection.prototype.groupAction = function(kind, message) {
|
|||
* userAction sends a request to act on a user.
|
||||
*
|
||||
* @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
|
||||
* @param {string} id
|
||||
* @param {string} [message]
|
||||
* @param {string} id - The id of the user to act upon.
|
||||
* @param {string} [message] - An optional user-readable message.
|
||||
*/
|
||||
ServerConnection.prototype.userAction = function(kind, id, message) {
|
||||
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.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {Object.<string, string>} labels
|
||||
* @param {Object<string, string>} labels
|
||||
* @param {RTCSessionDescriptionInit} offer
|
||||
* @param {boolean} renegotiate
|
||||
* @function
|
||||
|
@ -656,13 +675,15 @@ function Stream(sc, id, pc) {
|
|||
* The associated ServerConnection.
|
||||
*
|
||||
* @type {ServerConnection}
|
||||
*/
|
||||
* @const
|
||||
*/
|
||||
this.sc = sc;
|
||||
/**
|
||||
* The id of this stream.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
* @const
|
||||
*/
|
||||
this.id = id;
|
||||
/**
|
||||
* For up streams, one of "local" or "screenshare".
|
||||
|
@ -693,20 +714,20 @@ function Stream(sc, id, pc) {
|
|||
/**
|
||||
* Track labels, indexed by track id.
|
||||
*
|
||||
* @type {Object.<string,string>}
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
this.labels = {};
|
||||
/**
|
||||
* Track labels, indexed by mid.
|
||||
*
|
||||
* @type {Object.<string,string>}
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
this.labelsByMid = {};
|
||||
/**
|
||||
* Buffered ICE candidates. This will be flushed by flushIceCandidates
|
||||
* when the PC becomes stable.
|
||||
*
|
||||
* @type {Array.<RTCIceCandidate>}
|
||||
* @type {RTCIceCandidate[]}
|
||||
*/
|
||||
this.iceCandidates = [];
|
||||
/**
|
||||
|
@ -721,7 +742,7 @@ function Stream(sc, id, pc) {
|
|||
* a dictionary indexed by track id, with each value a disctionary of
|
||||
* statistics.
|
||||
*
|
||||
* @type {Object.<string,any>}
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
this.stats = {};
|
||||
/**
|
||||
|
@ -734,7 +755,7 @@ function Stream(sc, id, pc) {
|
|||
/**
|
||||
* clientdata is a convenient place to attach data to a Stream.
|
||||
* It is not used by the library.
|
||||
* @type{Object.<string,any>}
|
||||
* @type{Object<any,any>}
|
||||
*/
|
||||
this.userdata = {};
|
||||
|
||||
|
@ -743,21 +764,21 @@ function Stream(sc, id, pc) {
|
|||
/**
|
||||
* onclose is called when the stream is closed.
|
||||
*
|
||||
* @type{function(): any}
|
||||
* @type{(this: Stream) => any}
|
||||
*/
|
||||
this.onclose = null;
|
||||
/**
|
||||
* onerror is called whenever an error occurs. If the error is
|
||||
* fatal, then onclose will be called afterwards.
|
||||
*
|
||||
* @type{function(any): any}
|
||||
* @type{(this: Stream, error: any) => any}
|
||||
*/
|
||||
this.onerror = null;
|
||||
/**
|
||||
* onnegotiationcompleted is called whenever negotiation or
|
||||
* renegotiation has completed.
|
||||
*
|
||||
* @type{function(): any}
|
||||
* @type{(this: Stream) => any}
|
||||
*/
|
||||
this.onnegotiationcompleted = null;
|
||||
/**
|
||||
|
@ -765,32 +786,32 @@ function Stream(sc, id, pc) {
|
|||
* If the stream parameter differs from its previous value, then it
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* onstatus is called whenever the status of the stream changes.
|
||||
*
|
||||
* @type{function(string): any}
|
||||
* @type{(this: Stream, status: string) => any}
|
||||
*/
|
||||
this.onstatus = null;
|
||||
/**
|
||||
* onabort is called when the server requested that an up stream be
|
||||
* closed. It is the resposibility of the client to close the stream.
|
||||
*
|
||||
* @type{function(): any}
|
||||
* @type{(this: Stream) => any}
|
||||
*/
|
||||
this.onabort = null;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
@ -834,6 +855,7 @@ Stream.prototype.close = function(sendclose) {
|
|||
* @function
|
||||
*/
|
||||
Stream.prototype.flushIceCandidates = async function () {
|
||||
/** @type {Promise<any>[]} */
|
||||
let promises = [];
|
||||
this.iceCandidates.forEach(c => {
|
||||
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
|
||||
* automatically when required. If the client requires renegotiation, it
|
||||
* is probably more effective to call restartIce on the underlying PC
|
||||
* rather than invoking this function directly.
|
||||
* is probably better to call restartIce which will cause negotiate to be
|
||||
* called asynchronously.
|
||||
*
|
||||
* @function
|
||||
* @param {boolean} [restartIce]
|
||||
* @param {boolean} [restartIce] - Whether to restart ICE.
|
||||
*/
|
||||
Stream.prototype.negotiate = async function (restartIce) {
|
||||
let c = this;
|
||||
|
||||
let options = null;
|
||||
let options = {};
|
||||
if(restartIce)
|
||||
options = {iceRestart: true};
|
||||
let offer = await c.pc.createOffer(options);
|
||||
|
@ -892,8 +914,7 @@ Stream.prototype.negotiate = async function (restartIce) {
|
|||
Stream.prototype.restartIce = function () {
|
||||
let c = this;
|
||||
|
||||
/** @ts-ignore */
|
||||
if(typeof c.pc.restartIce === 'function') {
|
||||
if('restartIce' in c.pc) {
|
||||
try {
|
||||
/** @ts-ignore */
|
||||
c.pc.restartIce();
|
||||
|
@ -988,7 +1009,7 @@ Stream.prototype.updateStats = async function() {
|
|||
* setStatsInterval sets the interval in milliseconds at which the onstats
|
||||
* 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) {
|
||||
let c = this;
|
||||
|
|
255
static/sfu.js
255
static/sfu.js
|
@ -103,9 +103,8 @@ function storeSettings(settings) {
|
|||
*
|
||||
* @returns {settings}
|
||||
*/
|
||||
|
||||
function getSettings() {
|
||||
/** @type{settings} */
|
||||
/** @type {settings} */
|
||||
let settings;
|
||||
try {
|
||||
let json = window.sessionStorage.getItem('settings');
|
||||
|
@ -127,22 +126,50 @@ function updateSettings(settings) {
|
|||
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() {
|
||||
let settings = getSettings();
|
||||
let store = false;
|
||||
|
||||
setLocalMute(settings.localMute);
|
||||
|
||||
let videoselect =
|
||||
/** @type {HTMLSelectElement} */(document.getElementById('videoselect'));
|
||||
let videoselect = getSelectElement('videoselect');
|
||||
if(!settings.video || !selectOptionAvailable(videoselect, settings.video)) {
|
||||
settings.video = selectOptionDefault(videoselect);
|
||||
store = true;
|
||||
}
|
||||
videoselect.value = settings.video;
|
||||
|
||||
let audioselect =
|
||||
/** @type {HTMLSelectElement} */(document.getElementById('audioselect'));
|
||||
let audioselect = getSelectElement('audioselect');
|
||||
if(!settings.audio || !selectOptionAvailable(audioselect, settings.audio)) {
|
||||
settings.audio = selectOptionDefault(audioselect);
|
||||
store = true;
|
||||
|
@ -150,24 +177,24 @@ function reflectSettings() {
|
|||
audioselect.value = settings.audio;
|
||||
|
||||
if(settings.request)
|
||||
document.getElementById('requestselect').value = settings.request;
|
||||
getSelectElement('requestselect').value = settings.request;
|
||||
else {
|
||||
settings.request = document.getElementById('requestselect').value;
|
||||
settings.request = getSelectElement('requestselect').value;
|
||||
store = true;
|
||||
}
|
||||
|
||||
if(settings.send)
|
||||
document.getElementById('sendselect').value = settings.send;
|
||||
getSelectElement('sendselect').value = settings.send;
|
||||
else {
|
||||
settings.send = document.getElementById('sendselect').value;
|
||||
settings.send = getSelectElement('sendselect').value;
|
||||
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)
|
||||
storeSettings(settings);
|
||||
|
@ -218,9 +245,9 @@ function setConnected(connected) {
|
|||
} else {
|
||||
resetUsers();
|
||||
let userpass = getUserPass();
|
||||
document.getElementById('username').value =
|
||||
getInputElement('username').value =
|
||||
userpass ? userpass.username : '';
|
||||
document.getElementById('password').value =
|
||||
getInputElement('password').value =
|
||||
userpass ? userpass.password : '';
|
||||
statspan.textContent = 'Disconnected';
|
||||
statspan.classList.remove('connected');
|
||||
|
@ -234,6 +261,7 @@ function setConnected(connected) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @this {ServerConnection} */
|
||||
function gotConnected() {
|
||||
setConnected(true);
|
||||
let up = getUserPass();
|
||||
|
@ -243,6 +271,7 @@ function gotConnected() {
|
|||
}
|
||||
|
||||
/**
|
||||
* @this {ServerConnection}
|
||||
* @param {number} code
|
||||
* @param {string} reason
|
||||
*/
|
||||
|
@ -255,6 +284,7 @@ function gotClose(code, reason) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @this {ServerConnection}
|
||||
* @param {Stream} c
|
||||
*/
|
||||
function gotDownStream(c) {
|
||||
|
@ -291,12 +321,12 @@ setViewportHeight();
|
|||
addEventListener('resize', setViewportHeight);
|
||||
addEventListener('orientationchange', setViewportHeight);
|
||||
|
||||
document.getElementById('presentbutton').onclick = function(e) {
|
||||
getButtonElement('presentbutton').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
addLocalMedia();
|
||||
};
|
||||
|
||||
document.getElementById('unpresentbutton').onclick = function(e) {
|
||||
getButtonElement('unpresentbutton').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
delUpMediaKind('local');
|
||||
resizePeers();
|
||||
|
@ -353,26 +383,34 @@ function setLocalMute(mute) {
|
|||
}
|
||||
}
|
||||
|
||||
document.getElementById('videoselect').onchange = function(e) {
|
||||
getSelectElement('videoselect').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLSelectElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
updateSettings({video: this.value});
|
||||
changePresentation();
|
||||
};
|
||||
|
||||
document.getElementById('audioselect').onchange = function(e) {
|
||||
getSelectElement('audioselect').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLSelectElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
updateSettings({audio: this.value});
|
||||
changePresentation();
|
||||
};
|
||||
|
||||
document.getElementById('blackboardbox').onchange = function(e) {
|
||||
getInputElement('blackboardbox').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLInputElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
updateSettings({blackboardMode: this.checked});
|
||||
changePresentation();
|
||||
}
|
||||
|
||||
document.getElementById('studiobox').onchange = function(e) {
|
||||
getInputElement('studiobox').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLInputElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
updateSettings({studioMode: this.checked});
|
||||
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});
|
||||
let t = getMaxVideoThroughput();
|
||||
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();
|
||||
if(!(this instanceof HTMLSelectElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
updateSettings({request: this.value});
|
||||
serverConnection.request(this.value);
|
||||
};
|
||||
|
@ -433,7 +475,9 @@ const activityDetectionInterval = 200;
|
|||
const activityDetectionPeriod = 700;
|
||||
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});
|
||||
for(let id in serverConnection.down) {
|
||||
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) {
|
||||
let c = this;
|
||||
|
||||
|
@ -477,8 +525,12 @@ function setActive(c, value) {
|
|||
peer.classList.remove('peer-active');
|
||||
}
|
||||
|
||||
/**
|
||||
* @this {Stream}
|
||||
* @param {Object<string,any>} stats
|
||||
*/
|
||||
function gotDownStats(stats) {
|
||||
if(!document.getElementById('activitybox').checked)
|
||||
if(!getInputElement('activitybox').checked)
|
||||
return;
|
||||
|
||||
let c = this;
|
||||
|
@ -514,7 +566,11 @@ function addSelectOption(select, label, value) {
|
|||
if(!value)
|
||||
value = label;
|
||||
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.label !== label) {
|
||||
child.label = label;
|
||||
|
@ -536,10 +592,13 @@ function addSelectOption(select, label, value) {
|
|||
function selectOptionAvailable(select, value) {
|
||||
let children = select.children;
|
||||
for(let i = 0; i < children.length; i++) {
|
||||
let child = /** @type {HTMLOptionElement} */ (select.children[i]);
|
||||
if(child.value === value) {
|
||||
return true;
|
||||
let child = select.children[i];
|
||||
if(!(child instanceof HTMLOptionElement)) {
|
||||
console.warn('Unexpected select child');
|
||||
continue;
|
||||
}
|
||||
if(child.value === value)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -551,7 +610,11 @@ function selectOptionAvailable(select, value) {
|
|||
function selectOptionDefault(select) {
|
||||
/* First non-empty option. */
|
||||
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)
|
||||
return child.value;
|
||||
}
|
||||
|
@ -587,13 +650,13 @@ async function setMediaChoices(done) {
|
|||
if(d.kind === 'videoinput') {
|
||||
if(!label)
|
||||
label = `Camera ${cn}`;
|
||||
addSelectOption(document.getElementById('videoselect'),
|
||||
addSelectOption(getSelectElement('videoselect'),
|
||||
label, d.deviceId);
|
||||
cn++;
|
||||
} else if(d.kind === 'audioinput') {
|
||||
if(!label)
|
||||
label = `Microphone ${mn}`;
|
||||
addSelectOption(document.getElementById('audioselect'),
|
||||
addSelectOption(getSelectElement('audioselect'),
|
||||
label, d.deviceId);
|
||||
mn++;
|
||||
}
|
||||
|
@ -693,6 +756,7 @@ async function addLocalMedia(id) {
|
|||
stopUpMedia(old);
|
||||
|
||||
let constraints = {audio: audio, video: video};
|
||||
/** @type {MediaStream} */
|
||||
let stream = null;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
@ -717,11 +781,11 @@ async function addLocalMedia(id) {
|
|||
t.enabled = false;
|
||||
} else if(t.kind == 'video') {
|
||||
if(settings.blackboardMode) {
|
||||
if('contentHint' in t)
|
||||
t.contentHint = 'detail';
|
||||
/** @ts-ignore */
|
||||
t.contentHint = 'detail';
|
||||
}
|
||||
}
|
||||
let sender = c.pc.addTrack(t, stream);
|
||||
c.pc.addTrack(t, stream);
|
||||
});
|
||||
|
||||
c.onstats = gotUpStats;
|
||||
|
@ -730,12 +794,16 @@ async function addLocalMedia(id) {
|
|||
setButtonsVisibility();
|
||||
}
|
||||
|
||||
async function addShareMedia(setup) {
|
||||
async function addShareMedia() {
|
||||
if(!getUserPass())
|
||||
return;
|
||||
|
||||
/** @type {MediaStream} */
|
||||
let stream = null;
|
||||
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});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
@ -747,7 +815,7 @@ async function addShareMedia(setup) {
|
|||
c.kind = 'screenshare';
|
||||
c.stream = stream;
|
||||
stream.getTracks().forEach(t => {
|
||||
let sender = c.pc.addTrack(t, stream);
|
||||
c.pc.addTrack(t, stream);
|
||||
t.onended = e => {
|
||||
delUpMedia(c);
|
||||
};
|
||||
|
@ -807,6 +875,9 @@ function delUpMediaKind(kind) {
|
|||
hideVideo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} kind
|
||||
*/
|
||||
function findUpMedia(kind) {
|
||||
for(let id in serverConnection.up) {
|
||||
if(serverConnection.up[id].kind === kind)
|
||||
|
@ -815,6 +886,9 @@ function findUpMedia(kind) {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} mute
|
||||
*/
|
||||
function muteLocalTracks(mute) {
|
||||
if(!serverConnection)
|
||||
return;
|
||||
|
@ -846,12 +920,14 @@ function setMedia(c, isUp) {
|
|||
peersdiv.appendChild(div);
|
||||
}
|
||||
|
||||
let media = document.getElementById('media-' + c.id);
|
||||
let media = /** @type {HTMLVideoElement} */
|
||||
(document.getElementById('media-' + c.id));
|
||||
if(!media) {
|
||||
media = document.createElement('video');
|
||||
media.id = 'media-' + c.id;
|
||||
media.classList.add('media');
|
||||
media.autoplay = true;
|
||||
/** @ts-ignore */
|
||||
media.playsinline = true;
|
||||
media.controls = true;
|
||||
if(isUp)
|
||||
|
@ -883,7 +959,9 @@ function delMedia(id) {
|
|||
let peer = document.getElementById('peer-' + id);
|
||||
if(!peer)
|
||||
throw new Error('Removing unknown media');
|
||||
let media = document.getElementById('media-' + id);
|
||||
|
||||
let media = /** @type{HTMLVideoElement} */
|
||||
(document.getElementById('media-' + id));
|
||||
|
||||
media.srcObject = null;
|
||||
mediadiv.removeChild(peer);
|
||||
|
@ -941,14 +1019,19 @@ function resizePeers() {
|
|||
if (!count)
|
||||
// No video, nothing to resize.
|
||||
return;
|
||||
let size = 100 / columns;
|
||||
let container = document.getElementById("video-container")
|
||||
// Peers div has total padding of 30px, we remove 30 on offsetHeight
|
||||
let max_video_height = Math.trunc((peers.offsetHeight - 30) / columns);
|
||||
|
||||
let media_list = document.getElementsByClassName("media");
|
||||
for(let i = 0; i < media_list.length; i++)
|
||||
media_list[i].style['max_height'] = max_video_height + "px";
|
||||
for(let i = 0; i < media_list.length; i++) {
|
||||
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) {
|
||||
peers.style['grid-template-columns'] = "repeat(1, 1fr)";
|
||||
|
@ -957,7 +1040,7 @@ function resizePeers() {
|
|||
}
|
||||
}
|
||||
|
||||
/** @type{Object.<string,string>} */
|
||||
/** @type{Object<string,string>} */
|
||||
let users = {};
|
||||
|
||||
/**
|
||||
|
@ -1051,7 +1134,7 @@ function clearUsername() {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Object.<string,boolean>} perms
|
||||
* @param {Object<string,boolean>} perms
|
||||
*/
|
||||
function gotPermissions(perms) {
|
||||
displayUsername();
|
||||
|
@ -1064,7 +1147,7 @@ const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#=?]+[-a-zA-Z0-9@:%/_\\+~#=]/
|
|||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {Array.<Text|HTMLElement>}
|
||||
* @returns {(Text|HTMLElement)[]}
|
||||
*/
|
||||
function formatLine(line) {
|
||||
let r = new RegExp(urlRegexp);
|
||||
|
@ -1088,7 +1171,7 @@ function formatLine(line) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Array.<string>} lines
|
||||
* @param {string[]} lines
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function formatLines(lines) {
|
||||
|
@ -1113,6 +1196,12 @@ function formatLines(lines) {
|
|||
/** @type {lastMessage} */
|
||||
let lastMessage = {};
|
||||
|
||||
/**
|
||||
* @param {string} peerId
|
||||
* @param {string} nick
|
||||
* @param {string} kind
|
||||
* @param {string} message
|
||||
*/
|
||||
function addToChatbox(peerId, nick, kind, message){
|
||||
let userpass = getUserPass();
|
||||
let row = document.createElement('div');
|
||||
|
@ -1174,7 +1263,7 @@ function clearChat() {
|
|||
* part may be quoted and may include backslash escapes.
|
||||
*
|
||||
* @param {string} line
|
||||
* @returns {Array.<string>}
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function parseCommand(line) {
|
||||
let i = 0;
|
||||
|
@ -1204,7 +1293,8 @@ function parseCommand(line) {
|
|||
}
|
||||
|
||||
function handleInput() {
|
||||
let input = document.getElementById('input');
|
||||
let input = /** @type {HTMLTextAreaElement} */
|
||||
(document.getElementById('input'));
|
||||
let data = input.value;
|
||||
input.value = '';
|
||||
|
||||
|
@ -1356,39 +1446,66 @@ function chatResizer(e) {
|
|||
|
||||
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)";
|
||||
if (level === "info") {
|
||||
background = "linear-gradient(to right, #529518, #96c93d)";
|
||||
}
|
||||
if (level === "warning") {
|
||||
background = "linear-gradient(to right, #edd800, #c9c200)";
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {MessageLevel} [level]
|
||||
*/
|
||||
function displayError(message, level) {
|
||||
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)";
|
||||
break;
|
||||
}
|
||||
|
||||
/** @ts-ignore */
|
||||
Toastify({
|
||||
text: message,
|
||||
duration: 4000,
|
||||
close: true,
|
||||
position: position ? position: 'center',
|
||||
gravity: gravity ? gravity : 'top',
|
||||
backgroundColor: background,
|
||||
className: level,
|
||||
text: message,
|
||||
duration: 4000,
|
||||
close: true,
|
||||
position: position,
|
||||
gravity: gravity,
|
||||
backgroundColor: background,
|
||||
className: level,
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
function displayWarning(message) {
|
||||
let level = "warning";
|
||||
return displayError(message, level);
|
||||
return displayError(message, MessageLevel.warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
function displayMessage(message) {
|
||||
return displayError(message, "info", "right", "bottom");
|
||||
return displayError(message, MessageLevel.info);
|
||||
}
|
||||
|
||||
document.getElementById('userform').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
let username = document.getElementById('username').value.trim();
|
||||
let password = document.getElementById('password').value;
|
||||
let username = getInputElement('username').value.trim();
|
||||
let password = getInputElement('password').value;
|
||||
storeUserPass(username, password);
|
||||
serverConnect();
|
||||
};
|
||||
|
@ -1438,6 +1555,8 @@ document.getElementById('clodeside').onclick = function(e) {
|
|||
|
||||
document.getElementById('collapse-video').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
let width = window.innerWidth;
|
||||
if (width <= 768) {
|
||||
let user_box = document.getElementById('userDropdown');
|
||||
|
@ -1453,6 +1572,8 @@ document.getElementById('collapse-video').onclick = function(e) {
|
|||
|
||||
document.getElementById('switch-video').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
if(!(this instanceof HTMLElement))
|
||||
throw new Error('Unexpected type for this');
|
||||
showVideo();
|
||||
this.style.display = "";
|
||||
document.getElementById('collapse-video').style.display = "block";
|
||||
|
|
|
@ -4,10 +4,16 @@
|
|||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"noImplicitThis": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strictBindCallApply": true
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"files": [
|
||||
"protocol.js"
|
||||
"protocol.js",
|
||||
"sfu.js"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue