2020-04-24 19:38:21 +02:00
|
|
|
// Copyright (c) 2020 by Juliusz Chroboczek.
|
|
|
|
|
2020-12-19 17:26:16 +01:00
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
|
|
// in the Software without restriction, including without limitation the rights
|
|
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
|
|
// furnished to do so, subject to the following conditions:
|
|
|
|
//
|
|
|
|
// The above copyright notice and this permission notice shall be included in
|
|
|
|
// all copies or substantial portions of the Software.
|
|
|
|
//
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
// THE SOFTWARE.
|
2020-04-24 19:38:21 +02:00
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
2020-08-24 22:31:22 +02:00
|
|
|
/** @type {string} */
|
2020-04-24 19:38:21 +02:00
|
|
|
let group;
|
2020-08-24 22:31:22 +02:00
|
|
|
|
|
|
|
/** @type {ServerConnection} */
|
2020-08-11 17:09:31 +02:00
|
|
|
let serverConnection;
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2021-10-26 22:22:48 +02:00
|
|
|
/** @type {Object} */
|
|
|
|
let groupStatus = {};
|
|
|
|
|
2022-02-18 20:27:53 +01:00
|
|
|
/** @type {string} */
|
|
|
|
let token = null;
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
/**
|
|
|
|
* @typedef {Object} settings
|
|
|
|
* @property {boolean} [localMute]
|
|
|
|
* @property {string} [video]
|
|
|
|
* @property {string} [audio]
|
2021-05-08 21:25:22 +02:00
|
|
|
* @property {string} [simulcast]
|
2020-09-18 17:57:37 +02:00
|
|
|
* @property {string} [send]
|
|
|
|
* @property {string} [request]
|
|
|
|
* @property {boolean} [activityDetection]
|
2020-12-14 19:31:42 +01:00
|
|
|
* @property {Array.<number>} [resolution]
|
2021-01-23 01:15:53 +01:00
|
|
|
* @property {boolean} [mirrorView]
|
2020-09-18 19:14:10 +02:00
|
|
|
* @property {boolean} [blackboardMode]
|
2021-01-12 01:25:35 +01:00
|
|
|
* @property {string} [filter]
|
2022-02-21 18:07:49 +01:00
|
|
|
* @property {boolean} [preprocessing]
|
|
|
|
* @property {boolean} [hqaudio]
|
2022-03-22 18:22:18 +01:00
|
|
|
* @property {boolean} [forceRelay]
|
2020-09-18 17:57:37 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
/** @type{settings} */
|
|
|
|
let fallbackSettings = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {settings} settings
|
|
|
|
*/
|
|
|
|
function storeSettings(settings) {
|
|
|
|
try {
|
|
|
|
window.sessionStorage.setItem('settings', JSON.stringify(settings));
|
|
|
|
fallbackSettings = null;
|
|
|
|
} catch(e) {
|
2021-03-30 07:28:20 +02:00
|
|
|
console.warn("Couldn't store settings:", e);
|
2020-09-18 17:57:37 +02:00
|
|
|
fallbackSettings = settings;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This always returns a dictionary.
|
|
|
|
*
|
|
|
|
* @returns {settings}
|
|
|
|
*/
|
|
|
|
function getSettings() {
|
2020-09-20 14:33:13 +02:00
|
|
|
/** @type {settings} */
|
2020-09-18 17:57:37 +02:00
|
|
|
let settings;
|
|
|
|
try {
|
|
|
|
let json = window.sessionStorage.getItem('settings');
|
|
|
|
settings = JSON.parse(json);
|
|
|
|
} catch(e) {
|
2021-03-30 07:28:20 +02:00
|
|
|
console.warn("Couldn't retrieve settings:", e);
|
2020-09-18 17:57:37 +02:00
|
|
|
settings = fallbackSettings;
|
|
|
|
}
|
|
|
|
return settings || {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {settings} settings
|
|
|
|
*/
|
|
|
|
function updateSettings(settings) {
|
|
|
|
let s = getSettings();
|
|
|
|
for(let key in settings)
|
|
|
|
s[key] = settings[key];
|
|
|
|
storeSettings(s);
|
|
|
|
}
|
|
|
|
|
2020-09-23 21:01:29 +02:00
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {any} value
|
|
|
|
*/
|
|
|
|
function updateSetting(key, value) {
|
|
|
|
let s = {};
|
|
|
|
s[key] = value;
|
|
|
|
updateSettings(s);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
*/
|
|
|
|
function delSetting(key) {
|
|
|
|
let s = getSettings();
|
|
|
|
if(!(key in s))
|
|
|
|
return;
|
|
|
|
delete(s[key]);
|
2020-12-26 17:28:44 +01:00
|
|
|
storeSettings(s);
|
2020-09-23 21:01:29 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
|
|
|
* @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;
|
|
|
|
}
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
function reflectSettings() {
|
|
|
|
let settings = getSettings();
|
|
|
|
let store = false;
|
|
|
|
|
|
|
|
setLocalMute(settings.localMute);
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
let videoselect = getSelectElement('videoselect');
|
2020-11-30 21:53:25 +01:00
|
|
|
if(!settings.hasOwnProperty('video') ||
|
|
|
|
!selectOptionAvailable(videoselect, settings.video)) {
|
2020-09-18 17:57:37 +02:00
|
|
|
settings.video = selectOptionDefault(videoselect);
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
videoselect.value = settings.video;
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
let audioselect = getSelectElement('audioselect');
|
2020-11-30 21:53:25 +01:00
|
|
|
if(!settings.hasOwnProperty('audio') ||
|
|
|
|
!selectOptionAvailable(audioselect, settings.audio)) {
|
2020-09-18 17:57:37 +02:00
|
|
|
settings.audio = selectOptionDefault(audioselect);
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
audioselect.value = settings.audio;
|
|
|
|
|
2021-01-13 13:57:05 +01:00
|
|
|
if(settings.hasOwnProperty('filter')) {
|
|
|
|
getSelectElement('filterselect').value = settings.filter;
|
|
|
|
} else {
|
|
|
|
let s = getSelectElement('filterselect').value;
|
|
|
|
if(s) {
|
|
|
|
settings.filter = s;
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-30 21:53:25 +01:00
|
|
|
if(settings.hasOwnProperty('request')) {
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('requestselect').value = settings.request;
|
2020-11-30 21:53:25 +01:00
|
|
|
} else {
|
2020-09-20 14:33:13 +02:00
|
|
|
settings.request = getSelectElement('requestselect').value;
|
2020-09-18 17:57:37 +02:00
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
2020-11-30 21:53:25 +01:00
|
|
|
if(settings.hasOwnProperty('send')) {
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('sendselect').value = settings.send;
|
2020-11-30 21:53:25 +01:00
|
|
|
} else {
|
2020-09-20 14:33:13 +02:00
|
|
|
settings.send = getSelectElement('sendselect').value;
|
2020-09-18 17:57:37 +02:00
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
2021-05-08 21:25:22 +02:00
|
|
|
if(settings.hasOwnProperty('simulcast')) {
|
|
|
|
getSelectElement('simulcastselect').value = settings.simulcast
|
|
|
|
} else {
|
|
|
|
settings.simulcast = getSelectElement('simulcastselect').value;
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
2021-01-23 01:01:53 +01:00
|
|
|
if(settings.hasOwnProperty('blackboardMode')) {
|
|
|
|
getInputElement('blackboardbox').checked = settings.blackboardMode;
|
|
|
|
} else {
|
|
|
|
settings.blackboardMode = getInputElement('blackboardbox').checked;
|
|
|
|
store = true;
|
|
|
|
}
|
2020-09-18 17:57:37 +02:00
|
|
|
|
2021-01-23 01:15:53 +01:00
|
|
|
if(settings.hasOwnProperty('mirrorView')) {
|
|
|
|
getInputElement('mirrorbox').checked = settings.mirrorView;
|
|
|
|
} else {
|
|
|
|
settings.mirrorView = getInputElement('mirrorbox').checked;
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
2021-01-23 01:01:53 +01:00
|
|
|
if(settings.hasOwnProperty('activityDetection')) {
|
|
|
|
getInputElement('activitybox').checked = settings.activityDetection;
|
|
|
|
} else {
|
|
|
|
settings.activityDetection = getInputElement('activitybox').checked;
|
|
|
|
store = true;
|
|
|
|
}
|
2020-09-18 19:14:10 +02:00
|
|
|
|
2022-02-21 18:07:49 +01:00
|
|
|
if(settings.hasOwnProperty('preprocessing')) {
|
|
|
|
getInputElement('preprocessingbox').checked = settings.preprocessing;
|
|
|
|
} else {
|
|
|
|
settings.preprocessing = getInputElement('preprocessingbox').checked;
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(settings.hasOwnProperty('hqaudio')) {
|
|
|
|
getInputElement('hqaudiobox').checked = settings.hqaudio;
|
|
|
|
} else {
|
|
|
|
settings.hqaudio = getInputElement('hqaudiobox').checked;
|
|
|
|
store = true;
|
|
|
|
}
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
if(store)
|
|
|
|
storeSettings(settings);
|
|
|
|
}
|
|
|
|
|
2021-03-26 20:18:20 +01:00
|
|
|
function isMobileLayout() {
|
2021-03-28 22:29:04 +02:00
|
|
|
if (window.matchMedia('only screen and (max-width: 1024px)').matches)
|
|
|
|
return true;
|
|
|
|
return false;
|
2020-09-01 10:23:35 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
|
|
|
* @param {boolean} [force]
|
|
|
|
*/
|
2020-09-01 10:23:35 +02:00
|
|
|
function hideVideo(force) {
|
|
|
|
let mediadiv = document.getElementById('peers');
|
2020-09-12 16:34:52 +02:00
|
|
|
if(mediadiv.childElementCount > 0 && !force)
|
|
|
|
return;
|
2021-03-26 20:18:20 +01:00
|
|
|
setVisibility('video-container', false);
|
2021-05-14 18:44:49 +02:00
|
|
|
scheduleReconsiderDownRate();
|
2020-09-09 20:26:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-26 20:18:20 +01:00
|
|
|
function showVideo() {
|
|
|
|
let hasmedia = document.getElementById('peers').childElementCount > 0;
|
|
|
|
if(isMobileLayout()) {
|
|
|
|
setVisibility('show-video', false);
|
|
|
|
setVisibility('collapse-video', hasmedia);
|
|
|
|
}
|
|
|
|
setVisibility('video-container', hasmedia);
|
2021-05-14 18:44:49 +02:00
|
|
|
scheduleReconsiderDownRate();
|
2020-09-01 10:23:35 +02:00
|
|
|
}
|
|
|
|
|
2020-09-05 12:59:28 +02:00
|
|
|
/**
|
|
|
|
* @param{boolean} connected
|
|
|
|
*/
|
2020-04-24 19:38:21 +02:00
|
|
|
function setConnected(connected) {
|
2020-11-24 17:36:52 +01:00
|
|
|
let userbox = document.getElementById('profile');
|
2020-09-02 15:35:55 +02:00
|
|
|
let connectionbox = document.getElementById('login-container');
|
2020-04-24 19:38:21 +02:00
|
|
|
if(connected) {
|
2020-08-11 17:09:31 +02:00
|
|
|
clearChat();
|
2020-09-02 15:35:55 +02:00
|
|
|
userbox.classList.remove('invisible');
|
|
|
|
connectionbox.classList.add('invisible');
|
2020-04-25 18:09:31 +02:00
|
|
|
displayUsername();
|
2021-05-14 18:44:49 +02:00
|
|
|
window.onresize = function(e) {
|
|
|
|
scheduleReconsiderDownRate();
|
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
} else {
|
2020-09-02 15:35:55 +02:00
|
|
|
userbox.classList.add('invisible');
|
|
|
|
connectionbox.classList.remove('invisible');
|
2020-12-04 22:42:20 +01:00
|
|
|
displayError('Disconnected', 'error');
|
2020-09-09 20:26:19 +02:00
|
|
|
hideVideo();
|
2021-05-14 18:44:49 +02:00
|
|
|
window.onresize = null;
|
2020-08-11 17:09:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-20 15:32:18 +01:00
|
|
|
/**
|
|
|
|
* @this {ServerConnection}
|
|
|
|
* @param {string} [username]
|
|
|
|
*/
|
|
|
|
async function gotConnected(username) {
|
2021-10-29 23:37:05 +02:00
|
|
|
let credentials;
|
2022-02-18 20:27:53 +01:00
|
|
|
if(token) {
|
2021-10-29 23:37:05 +02:00
|
|
|
credentials = {
|
2022-02-18 20:27:53 +01:00
|
|
|
type: 'token',
|
|
|
|
token: token,
|
2021-10-29 23:37:05 +02:00
|
|
|
};
|
2022-02-18 20:27:53 +01:00
|
|
|
token = null;
|
|
|
|
} else {
|
|
|
|
setConnected(true);
|
|
|
|
|
2022-02-20 15:32:18 +01:00
|
|
|
username = getInputElement('username').value.trim();
|
2022-02-18 20:27:53 +01:00
|
|
|
let pw = getInputElement('password').value;
|
|
|
|
getInputElement('password').value = '';
|
|
|
|
if(!groupStatus.authServer)
|
|
|
|
credentials = pw;
|
|
|
|
else
|
|
|
|
credentials = {
|
|
|
|
type: 'authServer',
|
|
|
|
authServer: groupStatus.authServer,
|
|
|
|
location: location.href,
|
|
|
|
password: pw,
|
|
|
|
};
|
|
|
|
}
|
2021-10-29 23:37:05 +02:00
|
|
|
|
2021-10-29 23:37:59 +02:00
|
|
|
try {
|
2021-10-29 23:37:05 +02:00
|
|
|
await this.join(group, username, credentials);
|
2021-10-29 23:37:59 +02:00
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
serverConnection.close();
|
|
|
|
}
|
2020-08-11 17:09:31 +02:00
|
|
|
}
|
|
|
|
|
2022-03-22 18:22:18 +01:00
|
|
|
/**
|
|
|
|
* @this {ServerConnection}
|
|
|
|
*/
|
2022-03-23 00:11:12 +01:00
|
|
|
function onPeerConnection() {
|
2022-03-22 18:22:18 +01:00
|
|
|
if(!getSettings().forceRelay)
|
|
|
|
return null;
|
|
|
|
let old = this.rtcConfiguration;
|
|
|
|
/** @type {RTCConfiguration} */
|
|
|
|
let conf = {};
|
|
|
|
for(let key in old)
|
|
|
|
conf[key] = old[key];
|
|
|
|
conf.iceTransportPolicy = 'relay';
|
|
|
|
return conf;
|
|
|
|
}
|
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
2020-09-20 14:33:13 +02:00
|
|
|
* @this {ServerConnection}
|
2020-09-12 16:34:52 +02:00
|
|
|
* @param {number} code
|
|
|
|
* @param {string} reason
|
|
|
|
*/
|
2020-08-11 17:09:31 +02:00
|
|
|
function gotClose(code, reason) {
|
2021-07-16 00:11:38 +02:00
|
|
|
closeUpMedia();
|
2020-08-11 17:09:31 +02:00
|
|
|
setConnected(false);
|
2020-09-01 10:23:35 +02:00
|
|
|
if(code != 1000) {
|
2020-08-11 17:09:31 +02:00
|
|
|
console.warn('Socket close', code, reason);
|
2020-09-01 10:23:35 +02:00
|
|
|
}
|
2020-08-11 17:09:31 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
2020-09-20 14:33:13 +02:00
|
|
|
* @this {ServerConnection}
|
2020-08-13 20:55:13 +02:00
|
|
|
* @param {Stream} c
|
|
|
|
*/
|
2020-08-11 17:09:31 +02:00
|
|
|
function gotDownStream(c) {
|
2021-01-17 20:20:35 +01:00
|
|
|
c.onclose = function(replace) {
|
|
|
|
if(!replace)
|
2021-01-31 19:00:09 +01:00
|
|
|
delMedia(c.localId);
|
2020-08-11 17:09:31 +02:00
|
|
|
};
|
|
|
|
c.onerror = function(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-08-11 17:09:31 +02:00
|
|
|
c.ondowntrack = function(track, transceiver, label, stream) {
|
|
|
|
setMedia(c, false);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2021-02-03 19:46:44 +01:00
|
|
|
c.onnegotiationcompleted = function() {
|
2021-04-28 17:00:50 +02:00
|
|
|
resetMedia(c);
|
2021-02-03 19:46:44 +01:00
|
|
|
}
|
2020-08-11 17:09:31 +02:00
|
|
|
c.onstatus = function(status) {
|
|
|
|
setMediaStatus(c);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-09-11 23:20:46 +02:00
|
|
|
c.onstats = gotDownStats;
|
2020-09-18 17:57:37 +02:00
|
|
|
if(getSettings().activityDetection)
|
2020-09-11 23:20:46 +02:00
|
|
|
c.setStatsInterval(activityDetectionInterval);
|
2020-12-25 20:29:46 +01:00
|
|
|
|
|
|
|
setMedia(c, false);
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-09 20:26:19 +02:00
|
|
|
// Store current browser viewport height in css variable
|
|
|
|
function setViewportHeight() {
|
2020-09-12 16:34:52 +02:00
|
|
|
document.documentElement.style.setProperty(
|
|
|
|
'--vh', `${window.innerHeight/100}px`,
|
|
|
|
);
|
2021-03-26 20:18:20 +01:00
|
|
|
showVideo();
|
2020-10-07 09:33:38 +02:00
|
|
|
// Ajust video component size
|
|
|
|
resizePeers();
|
|
|
|
}
|
2020-09-09 20:26:19 +02:00
|
|
|
|
|
|
|
// On resize and orientation change, we update viewport height
|
|
|
|
addEventListener('resize', setViewportHeight);
|
|
|
|
addEventListener('orientationchange', setViewportHeight);
|
|
|
|
|
2020-11-09 01:11:13 +01:00
|
|
|
getButtonElement('presentbutton').onclick = async function(e) {
|
2020-04-24 19:38:21 +02:00
|
|
|
e.preventDefault();
|
2020-11-09 01:11:13 +01:00
|
|
|
let button = this;
|
|
|
|
if(!(button instanceof HTMLButtonElement))
|
|
|
|
throw new Error('Unexpected type for this.');
|
|
|
|
// there's a potential race condition here: the user might click the
|
|
|
|
// button a second time before the stream is set up and the button hidden.
|
|
|
|
button.disabled = true;
|
|
|
|
try {
|
2021-04-28 17:00:50 +02:00
|
|
|
let id = findUpMedia('camera');
|
2020-11-09 01:11:13 +01:00
|
|
|
if(!id)
|
|
|
|
await addLocalMedia();
|
|
|
|
} finally {
|
|
|
|
button.disabled = false;
|
|
|
|
}
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getButtonElement('unpresentbutton').onclick = function(e) {
|
2020-05-21 18:22:30 +02:00
|
|
|
e.preventDefault();
|
2021-04-28 17:00:50 +02:00
|
|
|
closeUpMedia('camera');
|
2020-09-03 20:54:27 +02:00
|
|
|
resizePeers();
|
2020-05-21 18:22:30 +02:00
|
|
|
};
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {boolean} visible
|
|
|
|
*/
|
2020-05-21 18:22:30 +02:00
|
|
|
function setVisibility(id, visible) {
|
|
|
|
let elt = document.getElementById(id);
|
|
|
|
if(visible)
|
|
|
|
elt.classList.remove('invisible');
|
|
|
|
else
|
|
|
|
elt.classList.add('invisible');
|
|
|
|
}
|
|
|
|
|
|
|
|
function setButtonsVisibility() {
|
2021-02-04 23:11:40 +01:00
|
|
|
let connected = serverConnection && serverConnection.socket;
|
2020-08-11 17:09:31 +02:00
|
|
|
let permissions = serverConnection.permissions;
|
2022-02-19 23:43:44 +01:00
|
|
|
let present = permissions.indexOf('present') >= 0;
|
2021-04-28 17:00:50 +02:00
|
|
|
let local = !!findUpMedia('camera');
|
2021-02-14 18:06:50 +01:00
|
|
|
let canWebrtc = !(typeof RTCPeerConnection === 'undefined');
|
2021-03-26 20:18:20 +01:00
|
|
|
let mediacount = document.getElementById('peers').childElementCount;
|
|
|
|
let mobilelayout = isMobileLayout();
|
2020-09-23 21:46:30 +02:00
|
|
|
|
2020-05-21 18:22:30 +02:00
|
|
|
// don't allow multiple presentations
|
2022-02-19 23:43:44 +01:00
|
|
|
setVisibility('presentbutton', canWebrtc && present && !local);
|
2020-05-21 18:22:30 +02:00
|
|
|
setVisibility('unpresentbutton', local);
|
2020-09-23 21:46:30 +02:00
|
|
|
|
2022-02-19 23:43:44 +01:00
|
|
|
setVisibility('mutebutton', !connected || present);
|
2020-11-30 17:39:23 +01:00
|
|
|
|
2020-05-21 18:22:30 +02:00
|
|
|
// allow multiple shared documents
|
2022-02-19 23:43:44 +01:00
|
|
|
setVisibility('sharebutton', canWebrtc && present &&
|
2020-10-07 09:33:38 +02:00
|
|
|
('getDisplayMedia' in navigator.mediaDevices));
|
2020-05-21 18:22:30 +02:00
|
|
|
|
2022-02-19 23:43:44 +01:00
|
|
|
setVisibility('mediaoptions', present);
|
|
|
|
setVisibility('sendform', present);
|
|
|
|
setVisibility('simulcastform', present);
|
2021-03-26 20:18:20 +01:00
|
|
|
|
|
|
|
setVisibility('collapse-video', mediacount && mobilelayout);
|
2020-05-21 18:22:30 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
|
|
|
* @param {boolean} mute
|
2020-11-30 14:22:36 +01:00
|
|
|
* @param {boolean} [reflect]
|
2020-09-12 16:34:52 +02:00
|
|
|
*/
|
2020-11-30 14:22:36 +01:00
|
|
|
function setLocalMute(mute, reflect) {
|
2020-09-18 17:57:37 +02:00
|
|
|
muteLocalTracks(mute);
|
2020-06-09 18:05:16 +02:00
|
|
|
let button = document.getElementById('mutebutton');
|
2020-11-09 15:57:30 +01:00
|
|
|
let icon = button.querySelector("span .fas");
|
2020-09-18 17:57:37 +02:00
|
|
|
if(mute){
|
2020-09-11 10:39:32 +02:00
|
|
|
icon.classList.add('fa-microphone-slash');
|
|
|
|
icon.classList.remove('fa-microphone');
|
2020-06-09 18:05:16 +02:00
|
|
|
button.classList.add('muted');
|
2020-09-11 10:39:32 +02:00
|
|
|
} else {
|
|
|
|
icon.classList.remove('fa-microphone-slash');
|
|
|
|
icon.classList.add('fa-microphone');
|
2020-06-09 18:05:16 +02:00
|
|
|
button.classList.remove('muted');
|
2020-09-11 10:39:32 +02:00
|
|
|
}
|
2020-11-30 14:22:36 +01:00
|
|
|
if(reflect)
|
|
|
|
updateSettings({localMute: mute});
|
2020-06-09 18:05:16 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('videoselect').onchange = function(e) {
|
2020-05-05 17:50:16 +02:00
|
|
|
e.preventDefault();
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 17:57:37 +02:00
|
|
|
updateSettings({video: this.value});
|
2021-05-06 23:39:27 +02:00
|
|
|
replaceCameraStream();
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-05-05 17:50:16 +02:00
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('audioselect').onchange = function(e) {
|
2020-05-05 17:50:16 +02:00
|
|
|
e.preventDefault();
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 17:57:37 +02:00
|
|
|
updateSettings({audio: this.value});
|
2021-05-06 23:39:27 +02:00
|
|
|
replaceCameraStream();
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-05-05 17:50:16 +02:00
|
|
|
|
2021-01-23 01:15:53 +01:00
|
|
|
getInputElement('mirrorbox').onchange = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
updateSettings({mirrorView: this.checked});
|
2021-05-06 23:39:27 +02:00
|
|
|
// no need to reopen the camera
|
|
|
|
replaceUpStreams('camera');
|
2021-01-23 01:15:53 +01:00
|
|
|
};
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getInputElement('blackboardbox').onchange = function(e) {
|
2020-09-18 19:14:10 +02:00
|
|
|
e.preventDefault();
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 19:14:10 +02:00
|
|
|
updateSettings({blackboardMode: this.checked});
|
2021-05-06 23:39:27 +02:00
|
|
|
replaceCameraStream();
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-09-18 19:14:10 +02:00
|
|
|
|
2022-02-21 18:07:49 +01:00
|
|
|
getInputElement('preprocessingbox').onchange = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
updateSettings({preprocessing: this.checked});
|
|
|
|
replaceCameraStream();
|
|
|
|
};
|
|
|
|
|
|
|
|
getInputElement('hqaudiobox').onchange = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
updateSettings({hqaudio: this.checked});
|
|
|
|
replaceCameraStream();
|
|
|
|
};
|
|
|
|
|
2020-06-09 18:05:16 +02:00
|
|
|
document.getElementById('mutebutton').onclick = function(e) {
|
|
|
|
e.preventDefault();
|
2020-09-18 17:57:37 +02:00
|
|
|
let localMute = getSettings().localMute;
|
|
|
|
localMute = !localMute;
|
2020-11-30 14:22:36 +01:00
|
|
|
setLocalMute(localMute, true);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-06-09 18:05:16 +02:00
|
|
|
|
2020-05-21 18:22:30 +02:00
|
|
|
document.getElementById('sharebutton').onclick = function(e) {
|
2020-04-24 19:38:21 +02:00
|
|
|
e.preventDefault();
|
2020-05-21 18:22:30 +02:00
|
|
|
addShareMedia();
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2021-01-13 13:57:05 +01:00
|
|
|
getSelectElement('filterselect').onchange = async function(e) {
|
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
updateSettings({filter: this.value});
|
2021-05-09 18:59:46 +02:00
|
|
|
let c = findUpMedia('camera');
|
|
|
|
if(c) {
|
|
|
|
let filter = (this.value && filters[this.value]) || null;
|
|
|
|
if(filter)
|
|
|
|
c.userdata.filterDefinition = filter;
|
|
|
|
else
|
|
|
|
delete c.userdata.filterDefinition;
|
|
|
|
replaceUpStream(c);
|
|
|
|
}
|
2021-01-13 13:57:05 +01:00
|
|
|
};
|
|
|
|
|
2020-08-26 18:32:01 +02:00
|
|
|
/** @returns {number} */
|
|
|
|
function getMaxVideoThroughput() {
|
2020-09-18 17:57:37 +02:00
|
|
|
let v = getSettings().send;
|
2020-08-26 18:32:01 +02:00
|
|
|
switch(v) {
|
|
|
|
case 'lowest':
|
|
|
|
return 150000;
|
|
|
|
case 'low':
|
|
|
|
return 300000;
|
|
|
|
case 'normal':
|
|
|
|
return 700000;
|
|
|
|
case 'unlimited':
|
|
|
|
return null;
|
|
|
|
default:
|
|
|
|
console.error('Unknown video quality', v);
|
|
|
|
return 700000;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('sendselect').onchange = async function(e) {
|
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 17:57:37 +02:00
|
|
|
updateSettings({send: this.value});
|
2021-05-09 15:06:10 +02:00
|
|
|
await reconsiderSendParameters();
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-08-26 18:32:01 +02:00
|
|
|
|
2021-05-08 21:25:22 +02:00
|
|
|
getSelectElement('simulcastselect').onchange = async function(e) {
|
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
updateSettings({simulcast: this.value});
|
2021-05-09 15:06:10 +02:00
|
|
|
await reconsiderSendParameters();
|
2021-05-08 21:25:22 +02:00
|
|
|
};
|
|
|
|
|
2021-04-28 17:00:50 +02:00
|
|
|
/**
|
|
|
|
* @param {string} what
|
|
|
|
* @returns {Object<string,Array<string>>}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function mapRequest(what) {
|
|
|
|
switch(what) {
|
|
|
|
case '':
|
|
|
|
return {};
|
|
|
|
break;
|
|
|
|
case 'audio':
|
|
|
|
return {'': ['audio']};
|
|
|
|
break;
|
2021-04-29 17:03:25 +02:00
|
|
|
case 'screenshare-low':
|
|
|
|
return {screenshare: ['audio','video-low'], '': ['audio']};
|
|
|
|
break;
|
2021-04-28 17:00:50 +02:00
|
|
|
case 'screenshare':
|
|
|
|
return {screenshare: ['audio','video'], '': ['audio']};
|
|
|
|
break;
|
2021-04-29 17:03:25 +02:00
|
|
|
case 'everything-low':
|
|
|
|
return {'': ['audio','video-low']};
|
|
|
|
break;
|
2021-04-28 17:00:50 +02:00
|
|
|
case 'everything':
|
|
|
|
return {'': ['audio','video']}
|
|
|
|
break;
|
|
|
|
default:
|
2021-05-14 18:44:49 +02:00
|
|
|
throw new Error(`Unknown value ${what} in request`);
|
2021-04-28 17:00:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-14 18:44:49 +02:00
|
|
|
/**
|
|
|
|
* @param {string} what
|
|
|
|
* @param {string} label
|
|
|
|
* @returns {Array<string>}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function mapRequestLabel(what, label) {
|
|
|
|
let r = mapRequest(what);
|
|
|
|
if(label in r)
|
|
|
|
return r[label];
|
|
|
|
else
|
|
|
|
return r[''];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getSelectElement('requestselect').onchange = function(e) {
|
2020-05-09 19:39:34 +02:00
|
|
|
e.preventDefault();
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!(this instanceof HTMLSelectElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 17:57:37 +02:00
|
|
|
updateSettings({request: this.value});
|
2021-04-28 17:00:50 +02:00
|
|
|
serverConnection.request(mapRequest(this.value));
|
2021-05-14 18:44:49 +02:00
|
|
|
reconsiderDownRate();
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-05-09 19:39:34 +02:00
|
|
|
|
2020-09-14 17:15:21 +02:00
|
|
|
const activityDetectionInterval = 200;
|
2020-09-11 23:20:46 +02:00
|
|
|
const activityDetectionPeriod = 700;
|
|
|
|
const activityDetectionThreshold = 0.2;
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
getInputElement('activitybox').onchange = function(e) {
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2020-09-18 17:57:37 +02:00
|
|
|
updateSettings({activityDetection: this.checked});
|
2020-09-11 23:20:46 +02:00
|
|
|
for(let id in serverConnection.down) {
|
|
|
|
let c = serverConnection.down[id];
|
2020-09-14 17:15:21 +02:00
|
|
|
if(this.checked)
|
2020-09-11 23:20:46 +02:00
|
|
|
c.setStatsInterval(activityDetectionInterval);
|
|
|
|
else {
|
|
|
|
c.setStatsInterval(0);
|
2020-09-18 17:57:37 +02:00
|
|
|
setActive(c, false);
|
2020-09-11 23:20:46 +02:00
|
|
|
}
|
|
|
|
}
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-09-11 23:20:46 +02:00
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
|
|
|
* @this {Stream}
|
|
|
|
* @param {Object<string,any>} stats
|
|
|
|
*/
|
2020-09-11 23:20:46 +02:00
|
|
|
function gotUpStats(stats) {
|
2020-08-11 17:09:31 +02:00
|
|
|
let c = this;
|
2020-04-26 19:21:01 +02:00
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
let values = [];
|
2020-06-12 15:42:44 +02:00
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
for(let id in stats) {
|
|
|
|
if(stats[id] && stats[id]['outbound-rtp']) {
|
|
|
|
let rate = stats[id]['outbound-rtp'].rate;
|
|
|
|
if(typeof rate === 'number') {
|
|
|
|
values.push(rate);
|
|
|
|
}
|
2020-06-12 15:42:44 +02:00
|
|
|
}
|
2021-04-29 17:03:25 +02:00
|
|
|
}
|
2020-06-12 15:42:44 +02:00
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
if(values.length === 0) {
|
|
|
|
setLabel(c, '');
|
|
|
|
} else {
|
|
|
|
values.sort((x,y) => x - y);
|
|
|
|
setLabel(c, values
|
|
|
|
.map(x => Math.round(x / 1000).toString())
|
|
|
|
.reduce((x, y) => x + '+' + y));
|
|
|
|
}
|
2020-04-26 19:21:01 +02:00
|
|
|
}
|
|
|
|
|
2020-09-11 23:20:46 +02:00
|
|
|
/**
|
|
|
|
* @param {Stream} c
|
|
|
|
* @param {boolean} value
|
|
|
|
*/
|
|
|
|
function setActive(c, value) {
|
2021-01-31 19:00:09 +01:00
|
|
|
let peer = document.getElementById('peer-' + c.localId);
|
2020-09-11 23:20:46 +02:00
|
|
|
if(value)
|
|
|
|
peer.classList.add('peer-active');
|
|
|
|
else
|
|
|
|
peer.classList.remove('peer-active');
|
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
|
|
|
* @this {Stream}
|
|
|
|
* @param {Object<string,any>} stats
|
|
|
|
*/
|
2020-09-11 23:20:46 +02:00
|
|
|
function gotDownStats(stats) {
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!getInputElement('activitybox').checked)
|
2020-09-11 23:20:46 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
let c = this;
|
|
|
|
|
|
|
|
let maxEnergy = 0;
|
|
|
|
|
|
|
|
c.pc.getReceivers().forEach(r => {
|
|
|
|
let tid = r.track && r.track.id;
|
|
|
|
let s = tid && stats[tid];
|
|
|
|
let energy = s && s['track'] && s['track'].audioEnergy;
|
|
|
|
if(typeof energy === 'number')
|
|
|
|
maxEnergy = Math.max(maxEnergy, energy);
|
|
|
|
});
|
|
|
|
|
|
|
|
// totalAudioEnergy is defined as the integral of the square of the
|
|
|
|
// volume, so square the threshold.
|
|
|
|
if(maxEnergy > activityDetectionThreshold * activityDetectionThreshold) {
|
|
|
|
c.userdata.lastVoiceActivity = Date.now();
|
|
|
|
setActive(c, true);
|
|
|
|
} else {
|
|
|
|
let last = c.userdata.lastVoiceActivity;
|
|
|
|
if(!last || Date.now() - last > activityDetectionPeriod)
|
|
|
|
setActive(c, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
2020-09-18 17:57:37 +02:00
|
|
|
* @param {HTMLSelectElement} select
|
|
|
|
* @param {string} label
|
|
|
|
* @param {string} [value]
|
2020-09-12 16:34:52 +02:00
|
|
|
*/
|
2020-05-05 17:50:16 +02:00
|
|
|
function addSelectOption(select, label, value) {
|
|
|
|
if(!value)
|
|
|
|
value = label;
|
|
|
|
for(let i = 0; i < select.children.length; i++) {
|
2020-09-20 14:33:13 +02:00
|
|
|
let child = select.children[i];
|
|
|
|
if(!(child instanceof HTMLOptionElement)) {
|
|
|
|
console.warn('Unexpected select child');
|
|
|
|
continue;
|
|
|
|
}
|
2020-09-18 17:57:37 +02:00
|
|
|
if(child.value === value) {
|
|
|
|
if(child.label !== label) {
|
|
|
|
child.label = label;
|
|
|
|
}
|
2020-05-05 17:50:16 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let option = document.createElement('option');
|
|
|
|
option.value = value;
|
|
|
|
option.textContent = label;
|
|
|
|
select.appendChild(option);
|
|
|
|
}
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
/**
|
|
|
|
* @param {HTMLSelectElement} select
|
|
|
|
* @param {string} value
|
|
|
|
*/
|
|
|
|
function selectOptionAvailable(select, value) {
|
|
|
|
let children = select.children;
|
|
|
|
for(let i = 0; i < children.length; i++) {
|
2020-09-20 14:33:13 +02:00
|
|
|
let child = select.children[i];
|
|
|
|
if(!(child instanceof HTMLOptionElement)) {
|
|
|
|
console.warn('Unexpected select child');
|
|
|
|
continue;
|
2020-09-18 17:57:37 +02:00
|
|
|
}
|
2020-09-20 14:33:13 +02:00
|
|
|
if(child.value === value)
|
|
|
|
return true;
|
2020-09-18 17:57:37 +02:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {HTMLSelectElement} select
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
function selectOptionDefault(select) {
|
|
|
|
/* First non-empty option. */
|
|
|
|
for(let i = 0; i < select.children.length; i++) {
|
2020-09-20 14:33:13 +02:00
|
|
|
let child = select.children[i];
|
|
|
|
if(!(child instanceof HTMLOptionElement)) {
|
|
|
|
console.warn('Unexpected select child');
|
|
|
|
continue;
|
|
|
|
}
|
2020-09-18 17:57:37 +02:00
|
|
|
if(child.value)
|
|
|
|
return child.value;
|
|
|
|
}
|
|
|
|
/* The empty option is always available. */
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/* media names might not be available before we call getDisplayMedia. So
|
|
|
|
we call this twice, the second time to update the menu with user-readable
|
|
|
|
labels. */
|
2020-08-24 22:37:48 +02:00
|
|
|
/** @type {boolean} */
|
2020-05-05 17:50:16 +02:00
|
|
|
let mediaChoicesDone = false;
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
/**
|
|
|
|
* @param{boolean} done
|
|
|
|
*/
|
|
|
|
async function setMediaChoices(done) {
|
2020-05-05 17:50:16 +02:00
|
|
|
if(mediaChoicesDone)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let devices = [];
|
|
|
|
try {
|
|
|
|
devices = await navigator.mediaDevices.enumerateDevices();
|
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let cn = 1, mn = 1;
|
|
|
|
|
|
|
|
devices.forEach(d => {
|
|
|
|
let label = d.label;
|
|
|
|
if(d.kind === 'videoinput') {
|
|
|
|
if(!label)
|
|
|
|
label = `Camera ${cn}`;
|
2020-09-20 14:33:13 +02:00
|
|
|
addSelectOption(getSelectElement('videoselect'),
|
2020-05-05 17:50:16 +02:00
|
|
|
label, d.deviceId);
|
|
|
|
cn++;
|
|
|
|
} else if(d.kind === 'audioinput') {
|
|
|
|
if(!label)
|
|
|
|
label = `Microphone ${mn}`;
|
2020-09-20 14:33:13 +02:00
|
|
|
addSelectOption(getSelectElement('audioselect'),
|
2020-05-05 17:50:16 +02:00
|
|
|
label, d.deviceId);
|
|
|
|
mn++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
mediaChoicesDone = done;
|
2020-05-05 17:50:16 +02:00
|
|
|
}
|
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
2021-01-31 19:00:09 +01:00
|
|
|
* @param {string} [localId]
|
2020-08-13 20:55:13 +02:00
|
|
|
*/
|
2021-01-31 19:00:09 +01:00
|
|
|
function newUpStream(localId) {
|
|
|
|
let c = serverConnection.newUpStream(localId);
|
2020-08-11 17:09:31 +02:00
|
|
|
c.onstatus = function(status) {
|
|
|
|
setMediaStatus(c);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-08-11 17:09:31 +02:00
|
|
|
c.onerror = function(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
2020-12-26 17:28:44 +01:00
|
|
|
};
|
2020-08-11 17:09:31 +02:00
|
|
|
return c;
|
|
|
|
}
|
|
|
|
|
2020-08-26 18:32:01 +02:00
|
|
|
/**
|
2021-05-09 15:06:10 +02:00
|
|
|
* Sets an up stream's video throughput and simulcast parameters.
|
|
|
|
*
|
2020-08-26 18:32:01 +02:00
|
|
|
* @param {Stream} c
|
2021-05-09 15:06:10 +02:00
|
|
|
* @param {number} bps
|
|
|
|
* @param {boolean} simulcast
|
2020-08-26 18:32:01 +02:00
|
|
|
*/
|
2021-05-09 15:06:10 +02:00
|
|
|
async function setSendParameters(c, bps, simulcast) {
|
|
|
|
if(!c.up)
|
|
|
|
throw new Error('Setting throughput of down stream');
|
2020-08-26 18:32:01 +02:00
|
|
|
let senders = c.pc.getSenders();
|
|
|
|
for(let i = 0; i < senders.length; i++) {
|
|
|
|
let s = senders[i];
|
|
|
|
if(!s.track || s.track.kind !== 'video')
|
|
|
|
continue;
|
|
|
|
let p = s.getParameters();
|
2022-03-20 21:52:24 +01:00
|
|
|
if((!p.encodings ||
|
|
|
|
!simulcast && p.encodings.length != 1) ||
|
2021-04-29 17:03:25 +02:00
|
|
|
(simulcast && p.encodings.length != 2)) {
|
|
|
|
await replaceUpStream(c);
|
|
|
|
return;
|
|
|
|
}
|
2020-08-26 18:32:01 +02:00
|
|
|
p.encodings.forEach(e => {
|
2021-04-29 17:03:25 +02:00
|
|
|
if(!e.rid || e.rid === 'h')
|
|
|
|
e.maxBitrate = bps || unlimitedRate;
|
2020-08-26 18:32:01 +02:00
|
|
|
});
|
2021-04-29 17:03:25 +02:00
|
|
|
await s.setParameters(p);
|
2020-08-26 18:32:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-09 15:06:10 +02:00
|
|
|
let reconsiderParametersTimer = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the send parameters for all up streams.
|
|
|
|
*/
|
|
|
|
async function reconsiderSendParameters() {
|
|
|
|
cancelReconsiderParameters();
|
|
|
|
let t = getMaxVideoThroughput();
|
|
|
|
let s = doSimulcast();
|
|
|
|
let promises = [];
|
|
|
|
for(let id in serverConnection.up) {
|
|
|
|
let c = serverConnection.up[id];
|
|
|
|
promises.push(setSendParameters(c, t, s));
|
|
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedules a call to reconsiderSendParameters after a delay.
|
|
|
|
* The delay avoids excessive flapping.
|
|
|
|
*/
|
|
|
|
function scheduleReconsiderParameters() {
|
|
|
|
cancelReconsiderParameters();
|
|
|
|
reconsiderParametersTimer =
|
|
|
|
setTimeout(reconsiderSendParameters, 10000 + Math.random() * 10000);
|
|
|
|
}
|
|
|
|
|
|
|
|
function cancelReconsiderParameters() {
|
|
|
|
if(reconsiderParametersTimer) {
|
|
|
|
clearTimeout(reconsiderParametersTimer);
|
|
|
|
reconsiderParametersTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-13 13:29:45 +01:00
|
|
|
/**
|
|
|
|
* @typedef {Object} filterDefinition
|
|
|
|
* @property {string} [description]
|
|
|
|
* @property {string} [contextType]
|
2021-01-13 14:20:23 +01:00
|
|
|
* @property {Object} [contextAttributes]
|
2021-01-13 18:10:16 +01:00
|
|
|
* @property {(this: Filter, ctx: RenderingContext) => void} [init]
|
|
|
|
* @property {(this: Filter) => void} [cleanup]
|
2021-01-13 13:29:45 +01:00
|
|
|
* @property {(this: Filter, src: CanvasImageSource, width: number, height: number, ctx: RenderingContext) => boolean} f
|
|
|
|
*/
|
|
|
|
|
2021-01-12 01:25:35 +01:00
|
|
|
/**
|
2021-01-12 03:47:25 +01:00
|
|
|
* @param {MediaStream} stream
|
2021-01-13 13:29:45 +01:00
|
|
|
* @param {filterDefinition} definition
|
2021-01-12 01:25:35 +01:00
|
|
|
* @constructor
|
|
|
|
*/
|
2021-01-13 13:29:45 +01:00
|
|
|
function Filter(stream, definition) {
|
|
|
|
/** @ts-ignore */
|
|
|
|
if(!HTMLCanvasElement.prototype.captureStream) {
|
|
|
|
throw new Error('Filters are not supported on this platform');
|
|
|
|
}
|
|
|
|
|
2021-01-12 01:25:35 +01:00
|
|
|
/** @type {MediaStream} */
|
2021-01-12 03:36:51 +01:00
|
|
|
this.inputStream = stream;
|
2021-01-13 13:29:45 +01:00
|
|
|
/** @type {filterDefinition} */
|
|
|
|
this.definition = definition;
|
2021-01-12 03:36:51 +01:00
|
|
|
/** @type {number} */
|
|
|
|
this.frameRate = 30;
|
2021-01-12 01:25:35 +01:00
|
|
|
/** @type {HTMLVideoElement} */
|
|
|
|
this.video = document.createElement('video');
|
|
|
|
/** @type {HTMLCanvasElement} */
|
|
|
|
this.canvas = document.createElement('canvas');
|
2021-01-13 13:29:45 +01:00
|
|
|
/** @type {any} */
|
2021-01-13 14:20:23 +01:00
|
|
|
this.context = this.canvas.getContext(
|
|
|
|
definition.contextType || '2d',
|
|
|
|
definition.contextAttributes || null);
|
2021-01-12 01:25:35 +01:00
|
|
|
/** @type {MediaStream} */
|
|
|
|
this.captureStream = null;
|
|
|
|
/** @type {MediaStream} */
|
|
|
|
this.outputStream = null;
|
|
|
|
/** @type {number} */
|
|
|
|
this.timer = null;
|
2021-01-12 03:36:51 +01:00
|
|
|
/** @type {number} */
|
|
|
|
this.count = 0;
|
2021-01-13 13:29:45 +01:00
|
|
|
/** @type {boolean} */
|
|
|
|
this.fixedFramerate = false;
|
2021-01-13 18:10:16 +01:00
|
|
|
/** @type {Object} */
|
|
|
|
this.userdata = {}
|
2022-01-25 01:22:59 +01:00
|
|
|
/** @type {MediaStream} */
|
2021-01-12 01:25:35 +01:00
|
|
|
this.captureStream = this.canvas.captureStream(0);
|
2022-01-25 01:22:59 +01:00
|
|
|
|
2021-01-13 13:29:45 +01:00
|
|
|
/** @ts-ignore */
|
|
|
|
if(!this.captureStream.getTracks()[0].requestFrame) {
|
|
|
|
console.warn('captureFrame not supported, using fixed framerate');
|
|
|
|
/** @ts-ignore */
|
|
|
|
this.captureStream = this.canvas.captureStream(this.frameRate);
|
|
|
|
this.fixedFramerate = true;
|
|
|
|
}
|
2021-01-12 01:25:35 +01:00
|
|
|
|
|
|
|
this.outputStream = new MediaStream();
|
|
|
|
this.outputStream.addTrack(this.captureStream.getTracks()[0]);
|
2021-01-12 03:36:51 +01:00
|
|
|
this.inputStream.getTracks().forEach(t => {
|
2021-01-12 01:25:35 +01:00
|
|
|
t.onended = e => this.stop();
|
|
|
|
if(t.kind != 'video')
|
|
|
|
this.outputStream.addTrack(t);
|
|
|
|
});
|
|
|
|
this.video.srcObject = stream;
|
|
|
|
this.video.muted = true;
|
|
|
|
this.video.play();
|
2021-01-13 18:10:16 +01:00
|
|
|
if(this.definition.init)
|
|
|
|
this.definition.init.call(this, this.context);
|
2021-01-12 03:36:51 +01:00
|
|
|
this.timer = setInterval(() => this.draw(), 1000 / this.frameRate);
|
2021-01-12 01:25:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Filter.prototype.draw = function() {
|
2021-03-30 07:28:20 +02:00
|
|
|
// check framerate every 30 frames
|
2021-01-12 03:36:51 +01:00
|
|
|
if((this.count % 30) === 0) {
|
|
|
|
let frameRate = 0;
|
|
|
|
this.inputStream.getTracks().forEach(t => {
|
|
|
|
if(t.kind === 'video') {
|
|
|
|
let r = t.getSettings().frameRate;
|
|
|
|
if(r)
|
|
|
|
frameRate = r;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if(frameRate && frameRate != this.frameRate) {
|
2021-01-13 20:15:39 +01:00
|
|
|
clearInterval(this.timer);
|
2021-01-12 03:36:51 +01:00
|
|
|
this.timer = setInterval(() => this.draw(), 1000 / this.frameRate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-13 17:07:40 +01:00
|
|
|
let ok = false;
|
|
|
|
try {
|
|
|
|
ok = this.definition.f.call(this, this.video,
|
2021-01-13 13:29:45 +01:00
|
|
|
this.video.videoWidth,
|
|
|
|
this.video.videoHeight,
|
|
|
|
this.context);
|
2021-01-13 17:07:40 +01:00
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2021-01-13 13:29:45 +01:00
|
|
|
if(ok && !this.fixedFramerate) {
|
2021-01-12 03:47:25 +01:00
|
|
|
/** @ts-ignore */
|
|
|
|
this.captureStream.getTracks()[0].requestFrame();
|
|
|
|
}
|
2021-01-13 17:07:40 +01:00
|
|
|
|
|
|
|
this.count++;
|
2021-01-12 01:25:35 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
Filter.prototype.stop = function() {
|
|
|
|
if(!this.timer)
|
|
|
|
return;
|
|
|
|
this.captureStream.getTracks()[0].stop();
|
2021-01-13 20:15:39 +01:00
|
|
|
clearInterval(this.timer);
|
2021-01-12 01:25:35 +01:00
|
|
|
this.timer = null;
|
2021-01-13 18:10:16 +01:00
|
|
|
if(this.definition.cleanup)
|
|
|
|
this.definition.cleanup.call(this);
|
2021-01-12 01:25:35 +01:00
|
|
|
};
|
|
|
|
|
2021-01-12 03:47:25 +01:00
|
|
|
/**
|
2021-05-06 23:39:27 +02:00
|
|
|
* Removes any filter set on c.
|
|
|
|
*
|
2021-01-13 13:29:45 +01:00
|
|
|
* @param {Stream} c
|
2021-05-06 23:39:27 +02:00
|
|
|
*/
|
|
|
|
function removeFilter(c) {
|
|
|
|
let old = c.userdata.filter;
|
|
|
|
if(!old)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(!(old instanceof Filter))
|
|
|
|
throw new Error('userdata.filter is not a filter');
|
|
|
|
|
2022-03-25 17:06:13 +01:00
|
|
|
c.setStream(old.inputStream);
|
2021-05-06 23:39:27 +02:00
|
|
|
old.stop();
|
|
|
|
c.userdata.filter = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the filter described by c.userdata.filterDefinition on c.
|
|
|
|
*
|
|
|
|
* @param {Stream} c
|
|
|
|
*/
|
|
|
|
function setFilter(c) {
|
|
|
|
removeFilter(c);
|
2021-01-13 13:29:45 +01:00
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
if(!c.userdata.filterDefinition)
|
|
|
|
return;
|
2021-01-13 13:29:45 +01:00
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
let filter = new Filter(c.stream, c.userdata.filterDefinition);
|
2022-03-25 17:06:13 +01:00
|
|
|
c.setStream(filter.outputStream);
|
2021-05-06 23:39:27 +02:00
|
|
|
c.userdata.filter = filter;
|
2021-01-13 13:29:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Object.<string,filterDefinition>}
|
2021-01-12 03:47:25 +01:00
|
|
|
*/
|
2021-01-12 01:25:35 +01:00
|
|
|
let filters = {
|
2021-01-13 13:29:45 +01:00
|
|
|
'mirror-h': {
|
|
|
|
description: "Horizontal mirror",
|
|
|
|
f: function(src, width, height, ctx) {
|
|
|
|
if(!(ctx instanceof CanvasRenderingContext2D))
|
|
|
|
throw new Error('bad context type');
|
2021-01-13 17:07:40 +01:00
|
|
|
if(ctx.canvas.width !== width || ctx.canvas.height !== height) {
|
|
|
|
ctx.canvas.width = width;
|
|
|
|
ctx.canvas.height = height;
|
|
|
|
}
|
2021-01-13 13:29:45 +01:00
|
|
|
ctx.scale(-1, 1);
|
|
|
|
ctx.drawImage(src, -width, 0);
|
2021-01-13 14:36:29 +01:00
|
|
|
ctx.resetTransform();
|
2021-01-13 13:29:45 +01:00
|
|
|
return true;
|
|
|
|
},
|
2021-01-12 01:25:35 +01:00
|
|
|
},
|
2021-01-13 13:29:45 +01:00
|
|
|
'mirror-v': {
|
|
|
|
description: "Vertical mirror",
|
|
|
|
f: function(src, width, height, ctx) {
|
|
|
|
if(!(ctx instanceof CanvasRenderingContext2D))
|
|
|
|
throw new Error('bad context type');
|
2021-01-13 17:07:40 +01:00
|
|
|
if(ctx.canvas.width !== width || ctx.canvas.height !== height) {
|
|
|
|
ctx.canvas.width = width;
|
|
|
|
ctx.canvas.height = height;
|
|
|
|
}
|
2021-01-13 13:29:45 +01:00
|
|
|
ctx.scale(1, -1);
|
|
|
|
ctx.drawImage(src, 0, -height);
|
2021-01-13 14:36:29 +01:00
|
|
|
ctx.resetTransform();
|
2021-01-13 13:29:45 +01:00
|
|
|
return true;
|
|
|
|
},
|
2021-01-12 01:25:35 +01:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-01-13 13:57:05 +01:00
|
|
|
function addFilters() {
|
|
|
|
for(let name in filters) {
|
|
|
|
let f = filters[name];
|
|
|
|
let d = f.description || name;
|
|
|
|
addSelectOption(getSelectElement('filterselect'), d, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-05 01:46:45 +01:00
|
|
|
function isSafari() {
|
|
|
|
let ua = navigator.userAgent.toLowerCase();
|
|
|
|
return ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0;
|
|
|
|
}
|
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
const unlimitedRate = 1000000000;
|
|
|
|
const simulcastRate = 100000;
|
2022-02-21 18:07:49 +01:00
|
|
|
const hqAudioRate = 128000;
|
2021-04-29 17:03:25 +02:00
|
|
|
|
|
|
|
/**
|
2021-05-09 15:06:10 +02:00
|
|
|
* Decide whether we want to send simulcast.
|
|
|
|
*
|
2021-04-29 17:03:25 +02:00
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function doSimulcast() {
|
2021-05-08 21:25:22 +02:00
|
|
|
switch(getSettings().simulcast) {
|
|
|
|
case 'on':
|
|
|
|
return true;
|
|
|
|
case 'off':
|
2021-04-29 17:03:25 +02:00
|
|
|
return false;
|
2021-05-08 21:25:22 +02:00
|
|
|
default:
|
2021-12-02 00:08:48 +01:00
|
|
|
let count = 0;
|
|
|
|
for(let n in serverConnection.users) {
|
|
|
|
if(!serverConnection.users[n].permissions["system"]) {
|
|
|
|
count++;
|
|
|
|
if(count > 2)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(count <= 2)
|
2021-05-09 15:06:10 +02:00
|
|
|
return false;
|
2021-05-08 21:25:22 +02:00
|
|
|
let bps = getMaxVideoThroughput();
|
|
|
|
return bps <= 0 || bps >= 2 * simulcastRate;
|
|
|
|
}
|
2021-04-29 17:03:25 +02:00
|
|
|
}
|
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
/**
|
2021-05-06 23:39:27 +02:00
|
|
|
* Sets up c to send the given stream. Some extra parameters are stored
|
|
|
|
* in c.userdata.
|
|
|
|
*
|
2021-04-29 17:03:25 +02:00
|
|
|
* @param {Stream} c
|
|
|
|
* @param {MediaStream} stream
|
|
|
|
*/
|
2021-04-29 17:03:25 +02:00
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
function setUpStream(c, stream) {
|
|
|
|
if(c.stream != null)
|
|
|
|
throw new Error("Setting nonempty stream");
|
|
|
|
|
2022-03-25 17:06:13 +01:00
|
|
|
c.setStream(stream);
|
2021-05-06 23:39:27 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
setFilter(c);
|
|
|
|
} catch(e) {
|
|
|
|
displayWarning("Couldn't set filter: " + e);
|
|
|
|
}
|
|
|
|
|
|
|
|
c.onclose = replace => {
|
|
|
|
removeFilter(c);
|
|
|
|
if(!replace) {
|
|
|
|
stopStream(c.stream);
|
|
|
|
if(c.userdata.onclose)
|
|
|
|
c.userdata.onclose.call(c);
|
|
|
|
delMedia(c.localId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {MediaStreamTrack} t
|
|
|
|
*/
|
|
|
|
function addUpTrack(t) {
|
2022-02-21 18:07:49 +01:00
|
|
|
let settings = getSettings();
|
2021-05-06 23:39:27 +02:00
|
|
|
if(c.label === 'camera') {
|
|
|
|
if(t.kind == 'audio') {
|
|
|
|
if(settings.localMute)
|
|
|
|
t.enabled = false;
|
|
|
|
} else if(t.kind == 'video') {
|
|
|
|
if(settings.blackboardMode) {
|
|
|
|
t.contentHint = 'detail';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t.onended = e => {
|
|
|
|
stream.onaddtrack = null;
|
|
|
|
stream.onremovetrack = null;
|
|
|
|
c.close();
|
|
|
|
};
|
|
|
|
|
2021-04-29 17:03:25 +02:00
|
|
|
let encodings = [];
|
2021-05-10 02:53:22 +02:00
|
|
|
let simulcast = doSimulcast();
|
2021-05-06 23:39:27 +02:00
|
|
|
if(t.kind === 'video') {
|
|
|
|
let bps = getMaxVideoThroughput();
|
2022-03-21 19:19:19 +01:00
|
|
|
// Firefox doesn't like us setting the RID if we're not
|
|
|
|
// simulcasting.
|
|
|
|
if(simulcast) {
|
|
|
|
encodings.push({
|
|
|
|
rid: 'h',
|
|
|
|
maxBitrate: bps || unlimitedRate,
|
|
|
|
});
|
2021-04-29 17:03:25 +02:00
|
|
|
encodings.push({
|
|
|
|
rid: 'l',
|
|
|
|
scaleResolutionDownBy: 2,
|
|
|
|
maxBitrate: simulcastRate,
|
|
|
|
});
|
2022-03-21 19:19:19 +01:00
|
|
|
} else {
|
|
|
|
encodings.push({
|
|
|
|
maxBitrate: bps || unlimitedRate,
|
|
|
|
});
|
|
|
|
}
|
2022-02-21 17:41:49 +01:00
|
|
|
} else {
|
2022-04-15 00:36:47 +02:00
|
|
|
if(settings.hqaudio) {
|
2022-02-21 17:41:49 +01:00
|
|
|
encodings.push({
|
2022-02-21 18:07:49 +01:00
|
|
|
maxBitrate: hqAudioRate,
|
2022-02-21 17:41:49 +01:00
|
|
|
});
|
|
|
|
}
|
2021-05-06 23:39:27 +02:00
|
|
|
}
|
2021-05-10 02:53:22 +02:00
|
|
|
let tr = c.pc.addTransceiver(t, {
|
2021-05-06 23:39:27 +02:00
|
|
|
direction: 'sendonly',
|
|
|
|
streams: [stream],
|
|
|
|
sendEncodings: encodings,
|
|
|
|
});
|
2022-03-20 21:52:24 +01:00
|
|
|
|
|
|
|
// Firefox workaround
|
2022-03-21 19:40:41 +01:00
|
|
|
function match(a, b) {
|
|
|
|
if(!a || !b)
|
|
|
|
return false;
|
|
|
|
if(a.length !== b.length)
|
|
|
|
return false;
|
|
|
|
for(let i = 0; i < a.length; i++) {
|
|
|
|
if(a.maxBitrate !== b.maxBitrate)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-03-20 21:52:24 +01:00
|
|
|
let p = tr.sender.getParameters();
|
2022-03-21 19:40:41 +01:00
|
|
|
if(!p || !match(p.encodings, encodings)) {
|
2022-03-20 21:52:24 +01:00
|
|
|
p.encodings = encodings;
|
|
|
|
tr.sender.setParameters(p);
|
2021-05-10 02:53:22 +02:00
|
|
|
}
|
2021-05-06 23:39:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// c.stream might be different from stream if there's a filter
|
|
|
|
c.stream.getTracks().forEach(addUpTrack);
|
|
|
|
|
|
|
|
stream.onaddtrack = function(e) {
|
|
|
|
addUpTrack(e.track);
|
|
|
|
};
|
|
|
|
|
|
|
|
stream.onremovetrack = function(e) {
|
|
|
|
let t = e.track;
|
|
|
|
|
|
|
|
/** @type {RTCRtpSender} */
|
|
|
|
let sender;
|
|
|
|
c.pc.getSenders().forEach(s => {
|
|
|
|
if(s.track === t)
|
|
|
|
sender = s;
|
|
|
|
});
|
|
|
|
if(sender) {
|
|
|
|
c.pc.removeTrack(sender);
|
|
|
|
} else {
|
|
|
|
console.warn('Removing unknown track');
|
|
|
|
}
|
|
|
|
|
|
|
|
let found = false;
|
|
|
|
c.pc.getSenders().forEach(s => {
|
|
|
|
if(s.track)
|
|
|
|
found = true;
|
|
|
|
});
|
|
|
|
if(!found) {
|
|
|
|
stream.onaddtrack = null;
|
|
|
|
stream.onremovetrack = null;
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
c.onstats = gotUpStats;
|
|
|
|
c.setStatsInterval(2000);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces c with a freshly created stream, duplicating any relevant
|
|
|
|
* parameters in c.userdata.
|
|
|
|
*
|
|
|
|
* @param {Stream} c
|
|
|
|
* @returns {Promise<Stream>}
|
|
|
|
*/
|
|
|
|
async function replaceUpStream(c) {
|
|
|
|
removeFilter(c);
|
|
|
|
let cn = newUpStream(c.localId);
|
|
|
|
cn.label = c.label;
|
|
|
|
if(c.userdata.filterDefinition)
|
|
|
|
cn.userdata.filterDefinition = c.userdata.filterDefinition;
|
|
|
|
if(c.userdata.onclose)
|
|
|
|
cn.userdata.onclose = c.userdata.onclose;
|
|
|
|
let media = /** @type{HTMLVideoElement} */
|
|
|
|
(document.getElementById('media-' + c.localId));
|
|
|
|
setUpStream(cn, c.stream);
|
2021-05-09 18:59:46 +02:00
|
|
|
await setMedia(cn, true,
|
|
|
|
cn.label == 'camera' && getSettings().mirrorView,
|
|
|
|
cn.label == 'video' && media);
|
2021-05-06 23:39:27 +02:00
|
|
|
return cn;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces all up streams with the given label. If label is null,
|
|
|
|
* replaces all up stream.
|
|
|
|
*
|
|
|
|
* @param {string} label
|
|
|
|
*/
|
|
|
|
async function replaceUpStreams(label) {
|
|
|
|
let promises = [];
|
|
|
|
for(let id in serverConnection.up) {
|
|
|
|
let c = serverConnection.up[id];
|
|
|
|
if(label && c.label !== label)
|
|
|
|
continue
|
|
|
|
promises.push(replaceUpStream(c));
|
|
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes and reopens the camera then replaces the camera stream.
|
|
|
|
*/
|
|
|
|
function replaceCameraStream() {
|
|
|
|
let c = findUpMedia('camera');
|
|
|
|
if(c)
|
|
|
|
addLocalMedia(c.localId);
|
2021-04-29 17:03:25 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
2021-01-31 19:00:09 +01:00
|
|
|
* @param {string} [localId]
|
2020-08-13 20:55:13 +02:00
|
|
|
*/
|
2021-01-31 19:00:09 +01:00
|
|
|
async function addLocalMedia(localId) {
|
2020-09-18 17:57:37 +02:00
|
|
|
let settings = getSettings();
|
|
|
|
|
|
|
|
let audio = settings.audio ? {deviceId: settings.audio} : false;
|
2020-11-27 11:13:48 +01:00
|
|
|
let video = settings.video ? {deviceId: settings.video} : false;
|
2020-05-05 17:50:16 +02:00
|
|
|
|
2020-09-18 19:14:10 +02:00
|
|
|
if(video) {
|
2020-12-14 19:31:42 +01:00
|
|
|
let resolution = settings.resolution;
|
|
|
|
if(resolution) {
|
|
|
|
video.width = { ideal: resolution[0] };
|
|
|
|
video.height = { ideal: resolution[1] };
|
|
|
|
} else if(settings.blackboardMode) {
|
2020-09-18 19:14:10 +02:00
|
|
|
video.width = { min: 640, ideal: 1920 };
|
|
|
|
video.height = { min: 400, ideal: 1080 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-21 18:07:49 +01:00
|
|
|
if(audio) {
|
|
|
|
if(!settings.preprocessing) {
|
|
|
|
audio.echoCancellation = false;
|
|
|
|
audio.noiseSuppression = false;
|
|
|
|
audio.autoGainControl = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-31 19:00:09 +01:00
|
|
|
let old = serverConnection.findByLocalId(localId);
|
2021-07-17 00:38:26 +02:00
|
|
|
if(old) {
|
2021-01-17 20:20:35 +01:00
|
|
|
// make sure that the camera is released before we try to reopen it
|
2021-07-17 00:38:26 +02:00
|
|
|
removeFilter(old);
|
|
|
|
stopStream(old.stream);
|
2021-01-17 20:20:35 +01:00
|
|
|
}
|
2020-06-10 20:25:25 +02:00
|
|
|
|
2020-05-05 17:50:16 +02:00
|
|
|
let constraints = {audio: audio, video: video};
|
2020-09-20 14:33:13 +02:00
|
|
|
/** @type {MediaStream} */
|
2020-05-05 17:50:16 +02:00
|
|
|
let stream = null;
|
|
|
|
try {
|
|
|
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
} catch(e) {
|
2020-09-04 01:17:06 +02:00
|
|
|
displayError(e);
|
2020-05-05 17:50:16 +02:00
|
|
|
return;
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
2020-05-05 17:50:16 +02:00
|
|
|
|
2020-09-18 17:57:37 +02:00
|
|
|
setMediaChoices(true);
|
2020-05-05 17:50:16 +02:00
|
|
|
|
2021-02-14 18:06:50 +01:00
|
|
|
let c;
|
|
|
|
|
|
|
|
try {
|
|
|
|
c = newUpStream(localId);
|
|
|
|
} catch(e) {
|
|
|
|
console.log(e);
|
|
|
|
displayError(e);
|
|
|
|
return;
|
|
|
|
}
|
2020-06-10 20:25:25 +02:00
|
|
|
|
2021-04-28 17:00:50 +02:00
|
|
|
c.label = 'camera';
|
2021-01-13 13:29:45 +01:00
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
if(settings.filter) {
|
|
|
|
let filter = filters[settings.filter];
|
|
|
|
if(filter)
|
|
|
|
c.userdata.filterDefinition = filter;
|
|
|
|
else
|
|
|
|
displayWarning(`Unknown filter ${settings.filter}`);
|
2021-01-12 01:25:35 +01:00
|
|
|
}
|
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
setUpStream(c, stream);
|
2021-01-23 01:15:53 +01:00
|
|
|
await setMedia(c, true, settings.mirrorView);
|
2020-09-01 10:23:35 +02:00
|
|
|
setButtonsVisibility();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-12-05 01:46:45 +01:00
|
|
|
let safariScreenshareDone = false;
|
2020-10-31 22:31:14 +01:00
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
async function addShareMedia() {
|
2022-05-19 13:24:44 +02:00
|
|
|
if(!safariScreenshareDone) {
|
|
|
|
if(isSafari()) {
|
|
|
|
let ok = confirm(
|
2022-05-19 15:24:15 +02:00
|
|
|
'Screen sharing in Safari is badly broken. ' +
|
2022-05-19 13:24:44 +02:00
|
|
|
'It will work at first, ' +
|
|
|
|
'but then your video will randomly freeze. ' +
|
|
|
|
'Are you sure that you wish to enable screensharing?'
|
|
|
|
);
|
|
|
|
if(!ok)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
safariScreenshareDone = true;
|
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/** @type {MediaStream} */
|
2020-05-21 18:22:30 +02:00
|
|
|
let stream = null;
|
|
|
|
try {
|
2020-09-20 14:33:13 +02:00
|
|
|
if(!('getDisplayMedia' in navigator.mediaDevices))
|
|
|
|
throw new Error('Your browser does not support screen sharing');
|
2022-02-21 18:19:25 +01:00
|
|
|
stream = await navigator.mediaDevices.getDisplayMedia({
|
|
|
|
video: true,
|
|
|
|
audio: true,
|
|
|
|
});
|
2020-05-21 18:22:30 +02:00
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
2020-09-04 01:17:06 +02:00
|
|
|
displayError(e);
|
2020-04-24 19:38:21 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-05-21 18:22:30 +02:00
|
|
|
|
2020-09-08 03:05:25 +02:00
|
|
|
let c = newUpStream();
|
2021-04-28 17:00:50 +02:00
|
|
|
c.label = 'screenshare';
|
2021-05-06 23:39:27 +02:00
|
|
|
setUpStream(c, stream);
|
2020-08-11 17:09:31 +02:00
|
|
|
await setMedia(c, true);
|
2020-12-26 17:28:44 +01:00
|
|
|
setButtonsVisibility();
|
2020-05-21 18:22:30 +02:00
|
|
|
}
|
|
|
|
|
2020-11-24 19:22:38 +01:00
|
|
|
/**
|
|
|
|
* @param {File} file
|
|
|
|
*/
|
|
|
|
async function addFileMedia(file) {
|
|
|
|
let url = URL.createObjectURL(file);
|
|
|
|
let video = document.createElement('video');
|
|
|
|
video.src = url;
|
2020-11-24 23:05:43 +01:00
|
|
|
video.controls = true;
|
2021-02-01 01:08:39 +01:00
|
|
|
let stream;
|
|
|
|
/** @ts-ignore */
|
|
|
|
if(video.captureStream)
|
|
|
|
/** @ts-ignore */
|
|
|
|
stream = video.captureStream();
|
2020-11-24 19:22:38 +01:00
|
|
|
/** @ts-ignore */
|
2021-02-01 01:08:39 +01:00
|
|
|
else if(video.mozCaptureStream)
|
|
|
|
/** @ts-ignore */
|
|
|
|
stream = video.mozCaptureStream();
|
|
|
|
else {
|
|
|
|
displayError("This browser doesn't support file playback");
|
|
|
|
return;
|
|
|
|
}
|
2020-11-24 19:22:38 +01:00
|
|
|
|
|
|
|
let c = newUpStream();
|
2021-04-28 17:00:50 +02:00
|
|
|
c.label = 'video';
|
2021-05-06 23:39:27 +02:00
|
|
|
c.userdata.onclose = function() {
|
2021-01-12 02:23:52 +01:00
|
|
|
let media = /** @type{HTMLVideoElement} */
|
2021-01-31 19:00:09 +01:00
|
|
|
(document.getElementById('media-' + this.localId));
|
2021-01-12 02:23:52 +01:00
|
|
|
if(media && media.src) {
|
|
|
|
URL.revokeObjectURL(media.src);
|
|
|
|
media.src = null;
|
|
|
|
}
|
2020-11-24 19:22:38 +01:00
|
|
|
};
|
2021-05-06 23:39:27 +02:00
|
|
|
await setUpStream(c, stream);
|
2020-11-24 23:22:24 +01:00
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
let presenting = !!findUpMedia('camera');
|
|
|
|
let muted = getSettings().localMute;
|
|
|
|
if(presenting && !muted) {
|
|
|
|
setLocalMute(true, true);
|
|
|
|
displayWarning('You have been muted');
|
|
|
|
}
|
2020-11-29 22:16:28 +01:00
|
|
|
|
2020-12-05 01:46:45 +01:00
|
|
|
await setMedia(c, true, false, video);
|
2020-12-03 01:07:48 +01:00
|
|
|
c.userdata.play = true;
|
2020-12-26 17:28:44 +01:00
|
|
|
setButtonsVisibility();
|
2020-11-24 19:22:38 +01:00
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
2021-01-14 14:56:15 +01:00
|
|
|
* @param {MediaStream} s
|
2020-08-13 20:55:13 +02:00
|
|
|
*/
|
2021-01-14 14:56:15 +01:00
|
|
|
function stopStream(s) {
|
|
|
|
s.getTracks().forEach(t => {
|
2020-06-10 20:25:25 +02:00
|
|
|
try {
|
|
|
|
t.stop();
|
|
|
|
} catch(e) {
|
2021-01-12 01:25:35 +01:00
|
|
|
console.warn(e);
|
2020-06-10 20:25:25 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
2021-04-28 17:00:50 +02:00
|
|
|
* closeUpMedia closes all up connections with the given label. If label
|
|
|
|
* is null, it closes all up connections.
|
2021-01-14 14:56:15 +01:00
|
|
|
*
|
2021-07-16 00:11:38 +02:00
|
|
|
* @param {string} [label]
|
2020-08-23 19:07:52 +02:00
|
|
|
*/
|
2021-04-28 17:00:50 +02:00
|
|
|
function closeUpMedia(label) {
|
2020-08-11 17:09:31 +02:00
|
|
|
for(let id in serverConnection.up) {
|
|
|
|
let c = serverConnection.up[id];
|
2021-04-28 17:00:50 +02:00
|
|
|
if(label && c.label !== label)
|
2020-05-21 18:22:30 +02:00
|
|
|
continue
|
2021-01-14 14:56:15 +01:00
|
|
|
c.close();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
2020-05-21 18:22:30 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
2021-04-28 17:00:50 +02:00
|
|
|
* @param {string} label
|
2021-01-31 19:00:09 +01:00
|
|
|
* @returns {Stream}
|
2020-09-20 14:33:13 +02:00
|
|
|
*/
|
2021-04-28 17:00:50 +02:00
|
|
|
function findUpMedia(label) {
|
2020-08-11 17:09:31 +02:00
|
|
|
for(let id in serverConnection.up) {
|
2021-01-31 19:00:09 +01:00
|
|
|
let c = serverConnection.up[id]
|
2021-04-28 17:00:50 +02:00
|
|
|
if(c.label === label)
|
2021-01-31 19:00:09 +01:00
|
|
|
return c;
|
2020-05-21 18:22:30 +02:00
|
|
|
}
|
2020-06-10 20:25:25 +02:00
|
|
|
return null;
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
|
|
|
* @param {boolean} mute
|
|
|
|
*/
|
2020-06-09 18:05:16 +02:00
|
|
|
function muteLocalTracks(mute) {
|
2020-08-11 17:09:31 +02:00
|
|
|
if(!serverConnection)
|
|
|
|
return;
|
|
|
|
for(let id in serverConnection.up) {
|
|
|
|
let c = serverConnection.up[id];
|
2021-04-28 17:00:50 +02:00
|
|
|
if(c.label === 'camera') {
|
2020-06-09 18:05:16 +02:00
|
|
|
let stream = c.stream;
|
|
|
|
stream.getTracks().forEach(t => {
|
|
|
|
if(t.kind === 'audio') {
|
|
|
|
t.enabled = !mute;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-14 18:44:49 +02:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {boolean} force
|
|
|
|
* @param {boolean} [value]
|
|
|
|
*/
|
|
|
|
function forceDownRate(id, force, value) {
|
|
|
|
let c = serverConnection.down[id];
|
|
|
|
if(!c)
|
|
|
|
throw new Error("Unknown down stream");
|
|
|
|
if('requested' in c.userdata) {
|
|
|
|
if(force)
|
|
|
|
c.userdata.requested.force = !!value;
|
|
|
|
else
|
|
|
|
delete(c.userdata.requested.force);
|
|
|
|
} else {
|
|
|
|
if(force)
|
|
|
|
c.userdata.requested = {force: value};
|
|
|
|
}
|
|
|
|
reconsiderDownRate(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps 'video' to 'video-low'. Returns null if nothing changed.
|
|
|
|
*
|
|
|
|
* @param {string[]} requested
|
|
|
|
* @returns {string[]}
|
|
|
|
*/
|
|
|
|
function mapVideoToLow(requested) {
|
|
|
|
let result = [];
|
|
|
|
let found = false;
|
|
|
|
for(let i = 0; i < requested.length; i++) {
|
|
|
|
let r = requested[i];
|
|
|
|
if(r === 'video') {
|
|
|
|
r = 'video-low';
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
result.push(r);
|
|
|
|
}
|
|
|
|
if(!found)
|
|
|
|
return null;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reconsider the video track requested for a given down stream.
|
|
|
|
*
|
|
|
|
* @param {string} [id] - the id of the track to reconsider, all if null.
|
|
|
|
*/
|
|
|
|
function reconsiderDownRate(id) {
|
|
|
|
if(!serverConnection)
|
|
|
|
return;
|
|
|
|
if(!id) {
|
|
|
|
for(let id in serverConnection.down) {
|
|
|
|
reconsiderDownRate(id);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let c = serverConnection.down[id];
|
|
|
|
if(!c)
|
|
|
|
throw new Error("Unknown down stream");
|
|
|
|
let normalrequest = mapRequestLabel(getSettings().request, c.label);
|
|
|
|
|
|
|
|
let requestlow = mapVideoToLow(normalrequest);
|
|
|
|
if(requestlow === null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let old = c.userdata.requested;
|
|
|
|
let low = false;
|
|
|
|
if(old && ('force' in old)) {
|
|
|
|
low = old.force;
|
|
|
|
} else {
|
|
|
|
let media = /** @type {HTMLVideoElement} */
|
|
|
|
(document.getElementById('media-' + c.localId));
|
|
|
|
if(!media)
|
|
|
|
throw new Error("No media for stream");
|
|
|
|
let w = media.scrollWidth;
|
|
|
|
let h = media.scrollHeight;
|
|
|
|
if(w && h && w * h <= 320 * 240) {
|
|
|
|
low = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(low !== !!(old && old.low)) {
|
|
|
|
if('requested' in c.userdata)
|
|
|
|
c.userdata.requested.low = low;
|
|
|
|
else
|
|
|
|
c.userdata.requested = {low: low};
|
|
|
|
c.request(low ? requestlow : null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let reconsiderDownRateTimer = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedules reconsiderDownRate() to be run later. The delay avoids too
|
|
|
|
* much recomputations when resizing the window.
|
|
|
|
*/
|
|
|
|
function scheduleReconsiderDownRate() {
|
|
|
|
if(reconsiderDownRateTimer)
|
|
|
|
return;
|
|
|
|
reconsiderDownRateTimer =
|
|
|
|
setTimeout(() => {
|
|
|
|
reconsiderDownRateTimer = null;
|
|
|
|
reconsiderDownRate();
|
|
|
|
}, 200);
|
|
|
|
}
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
/**
|
2020-11-28 01:53:29 +01:00
|
|
|
* setMedia adds a new media element corresponding to stream c.
|
|
|
|
*
|
2020-08-11 17:09:31 +02:00
|
|
|
* @param {Stream} c
|
|
|
|
* @param {boolean} isUp
|
2020-11-28 01:53:29 +01:00
|
|
|
* - indicates whether the stream goes in the up direction
|
2020-12-03 18:38:36 +01:00
|
|
|
* @param {boolean} [mirror]
|
|
|
|
* - whether to mirror the video
|
2020-11-24 19:22:38 +01:00
|
|
|
* @param {HTMLVideoElement} [video]
|
2020-11-28 01:53:29 +01:00
|
|
|
* - the video element to add. If null, a new element with custom
|
|
|
|
* controls will be created.
|
2020-08-11 17:09:31 +02:00
|
|
|
*/
|
2020-12-05 01:46:45 +01:00
|
|
|
async function setMedia(c, isUp, mirror, video) {
|
2020-04-24 19:38:21 +02:00
|
|
|
let peersdiv = document.getElementById('peers');
|
|
|
|
|
2021-01-31 19:00:09 +01:00
|
|
|
let div = document.getElementById('peer-' + c.localId);
|
2020-04-24 19:38:21 +02:00
|
|
|
if(!div) {
|
|
|
|
div = document.createElement('div');
|
2021-01-31 19:00:09 +01:00
|
|
|
div.id = 'peer-' + c.localId;
|
2020-04-24 19:38:21 +02:00
|
|
|
div.classList.add('peer');
|
|
|
|
peersdiv.appendChild(div);
|
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
let media = /** @type {HTMLVideoElement} */
|
2021-01-31 19:00:09 +01:00
|
|
|
(document.getElementById('media-' + c.localId));
|
2021-05-06 23:39:27 +02:00
|
|
|
if(!media) {
|
2020-11-24 19:22:38 +01:00
|
|
|
if(video) {
|
|
|
|
media = video;
|
|
|
|
} else {
|
|
|
|
media = document.createElement('video');
|
|
|
|
if(isUp)
|
|
|
|
media.muted = true;
|
|
|
|
}
|
2020-11-28 01:53:29 +01:00
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
media.classList.add('media');
|
|
|
|
media.autoplay = true;
|
2022-01-25 01:28:18 +01:00
|
|
|
media.playsInline = true;
|
2021-01-31 19:00:09 +01:00
|
|
|
media.id = 'media-' + c.localId;
|
2020-04-24 19:38:21 +02:00
|
|
|
div.appendChild(media);
|
2021-05-08 02:28:04 +02:00
|
|
|
addCustomControls(media, div, c, !!video);
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2021-01-31 19:00:09 +01:00
|
|
|
if(mirror)
|
|
|
|
media.classList.add('mirror');
|
|
|
|
else
|
|
|
|
media.classList.remove('mirror');
|
|
|
|
|
2021-01-13 14:59:43 +01:00
|
|
|
if(!video && media.srcObject !== c.stream)
|
2020-12-01 16:20:25 +01:00
|
|
|
media.srcObject = c.stream;
|
|
|
|
|
2021-05-14 18:44:49 +02:00
|
|
|
if(!isUp) {
|
|
|
|
media.onfullscreenchange = function(e) {
|
|
|
|
forceDownRate(c.id, document.fullscreenElement === media, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-31 19:00:09 +01:00
|
|
|
let label = document.getElementById('label-' + c.localId);
|
2020-04-24 19:38:21 +02:00
|
|
|
if(!label) {
|
|
|
|
label = document.createElement('div');
|
2021-01-31 19:00:09 +01:00
|
|
|
label.id = 'label-' + c.localId;
|
2020-04-24 19:38:21 +02:00
|
|
|
label.classList.add('label');
|
2020-05-10 21:18:07 +02:00
|
|
|
div.appendChild(label);
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
setLabel(c);
|
|
|
|
setMediaStatus(c);
|
2020-05-01 01:22:17 +02:00
|
|
|
|
2020-09-01 10:23:35 +02:00
|
|
|
showVideo();
|
2020-05-01 01:22:17 +02:00
|
|
|
resizePeers();
|
2020-12-05 01:46:45 +01:00
|
|
|
|
2021-04-28 17:00:50 +02:00
|
|
|
if(!isUp && isSafari() && !findUpMedia('camera')) {
|
2020-12-05 01:46:45 +01:00
|
|
|
// Safari doesn't allow autoplay unless the user has granted media access
|
|
|
|
try {
|
|
|
|
let stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
|
|
stream.getTracks().forEach(t => t.stop());
|
|
|
|
} catch(e) {
|
|
|
|
}
|
|
|
|
}
|
2020-09-28 17:00:25 +02:00
|
|
|
}
|
|
|
|
|
2021-02-03 19:46:44 +01:00
|
|
|
/**
|
|
|
|
* resetMedia resets the source stream of the media element associated
|
|
|
|
* with c. This has the side-effect of resetting any frozen frames.
|
|
|
|
*
|
|
|
|
* @param {Stream} c
|
|
|
|
*/
|
|
|
|
function resetMedia(c) {
|
|
|
|
let media = /** @type {HTMLVideoElement} */
|
|
|
|
(document.getElementById('media-' + c.localId));
|
|
|
|
if(!media) {
|
|
|
|
console.error("Resetting unknown media element")
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
media.srcObject = media.srcObject;
|
|
|
|
}
|
|
|
|
|
2020-09-28 17:00:25 +02:00
|
|
|
/**
|
2020-11-28 01:53:29 +01:00
|
|
|
* @param {Element} elt
|
2020-09-28 17:00:25 +02:00
|
|
|
*/
|
2020-11-28 01:53:29 +01:00
|
|
|
function cloneHTMLElement(elt) {
|
|
|
|
if(!(elt instanceof HTMLElement))
|
|
|
|
throw new Error('Unexpected element type');
|
|
|
|
return /** @type{HTMLElement} */(elt.cloneNode(true));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {HTMLVideoElement} media
|
|
|
|
* @param {HTMLElement} container
|
|
|
|
* @param {Stream} c
|
|
|
|
*/
|
2021-05-08 02:28:04 +02:00
|
|
|
function addCustomControls(media, container, c, toponly) {
|
|
|
|
if(!toponly && !document.getElementById('controls-' + c.localId)) {
|
|
|
|
media.controls = false;
|
2020-11-28 01:53:29 +01:00
|
|
|
|
2021-05-08 02:28:04 +02:00
|
|
|
let template =
|
|
|
|
document.getElementById('videocontrols-template').firstElementChild;
|
|
|
|
let controls = cloneHTMLElement(template);
|
|
|
|
controls.id = 'controls-' + c.localId;
|
|
|
|
|
|
|
|
let volume = getVideoButton(controls, 'volume');
|
|
|
|
|
2021-05-08 14:19:46 +02:00
|
|
|
if(c.up && c.label === 'camera') {
|
2021-05-08 02:28:04 +02:00
|
|
|
volume.remove();
|
|
|
|
} else {
|
|
|
|
setVolumeButton(media.muted,
|
|
|
|
getVideoButton(controls, "volume-mute"),
|
|
|
|
getVideoButton(controls, "volume-slider"));
|
|
|
|
}
|
|
|
|
container.appendChild(controls);
|
2020-09-28 17:00:25 +02:00
|
|
|
}
|
2020-11-28 01:53:29 +01:00
|
|
|
|
2021-05-08 14:19:46 +02:00
|
|
|
if(c.up && !document.getElementById('topcontrols-' + c.localId)) {
|
2021-05-08 02:28:04 +02:00
|
|
|
let toptemplate =
|
|
|
|
document.getElementById('topvideocontrols-template').firstElementChild;
|
|
|
|
let topcontrols = cloneHTMLElement(toptemplate);
|
|
|
|
topcontrols.id = 'topcontrols-' + c.localId;
|
|
|
|
container.appendChild(topcontrols);
|
|
|
|
}
|
2021-05-06 23:39:27 +02:00
|
|
|
registerControlHandlers(c.localId, media, container);
|
2020-09-28 17:00:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-11-28 01:53:29 +01:00
|
|
|
* @param {HTMLElement} container
|
|
|
|
* @param {string} name
|
2020-09-28 17:00:25 +02:00
|
|
|
*/
|
2020-11-28 01:53:29 +01:00
|
|
|
function getVideoButton(container, name) {
|
|
|
|
return /** @type {HTMLElement} */(container.getElementsByClassName(name)[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {boolean} muted
|
2020-12-11 19:37:09 +01:00
|
|
|
* @param {HTMLElement} button
|
|
|
|
* @param {HTMLElement} slider
|
2020-11-28 01:53:29 +01:00
|
|
|
*/
|
2020-12-11 19:37:09 +01:00
|
|
|
function setVolumeButton(muted, button, slider) {
|
2020-11-28 01:53:29 +01:00
|
|
|
if(!muted) {
|
2020-12-08 13:27:56 +01:00
|
|
|
button.classList.remove("fa-volume-mute");
|
2020-11-28 01:53:29 +01:00
|
|
|
button.classList.add("fa-volume-up");
|
|
|
|
} else {
|
|
|
|
button.classList.remove("fa-volume-up");
|
2020-12-08 13:27:56 +01:00
|
|
|
button.classList.add("fa-volume-mute");
|
2020-09-28 17:00:25 +02:00
|
|
|
}
|
2020-12-11 19:37:09 +01:00
|
|
|
|
|
|
|
if(!(slider instanceof HTMLInputElement))
|
|
|
|
throw new Error("Couldn't find volume slider");
|
|
|
|
slider.disabled = muted;
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-28 17:00:25 +02:00
|
|
|
/**
|
2021-05-06 23:39:27 +02:00
|
|
|
* @param {string} localId
|
2020-11-28 01:53:29 +01:00
|
|
|
* @param {HTMLVideoElement} media
|
|
|
|
* @param {HTMLElement} container
|
2020-09-28 17:00:25 +02:00
|
|
|
*/
|
2021-05-06 23:39:27 +02:00
|
|
|
function registerControlHandlers(localId, media, container) {
|
2020-12-15 15:20:53 +01:00
|
|
|
let play = getVideoButton(container, 'video-play');
|
|
|
|
if(play) {
|
|
|
|
play.onclick = function(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
media.play();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-03-29 20:44:24 +02:00
|
|
|
let stop = getVideoButton(container, 'video-stop');
|
|
|
|
if(stop) {
|
|
|
|
stop.onclick = function(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
try {
|
2021-05-06 23:39:27 +02:00
|
|
|
let c = serverConnection.findByLocalId(localId);
|
|
|
|
if(!c)
|
|
|
|
throw new Error('Closing unknown stream');
|
2021-05-07 02:44:04 +02:00
|
|
|
c.close();
|
2021-03-29 20:44:24 +02:00
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-11-28 01:53:29 +01:00
|
|
|
let volume = getVideoButton(container, 'volume');
|
2020-11-09 12:08:04 +01:00
|
|
|
if (volume) {
|
|
|
|
volume.onclick = function(event) {
|
2020-12-08 13:27:56 +01:00
|
|
|
let target = /** @type{HTMLElement} */(event.target);
|
|
|
|
if(!target.classList.contains('volume-mute'))
|
|
|
|
// if click on volume slider, do nothing
|
|
|
|
return;
|
2020-11-09 12:08:04 +01:00
|
|
|
event.preventDefault();
|
2020-11-28 01:53:29 +01:00
|
|
|
media.muted = !media.muted;
|
2020-12-11 19:37:09 +01:00
|
|
|
setVolumeButton(media.muted, target,
|
|
|
|
getVideoButton(volume, "volume-slider"));
|
2020-12-08 13:27:56 +01:00
|
|
|
};
|
|
|
|
volume.oninput = function() {
|
|
|
|
let slider = /** @type{HTMLInputElement} */
|
|
|
|
(getVideoButton(volume, "volume-slider"));
|
|
|
|
media.volume = parseInt(slider.value, 10)/100;
|
2020-11-09 12:08:04 +01:00
|
|
|
};
|
|
|
|
}
|
2020-11-08 17:07:17 +01:00
|
|
|
|
2020-11-28 01:53:29 +01:00
|
|
|
let pip = getVideoButton(container, 'pip');
|
2020-11-24 23:05:43 +01:00
|
|
|
if(pip) {
|
|
|
|
if(HTMLVideoElement.prototype.requestPictureInPicture) {
|
2020-11-28 01:53:29 +01:00
|
|
|
pip.onclick = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if(media.requestPictureInPicture) {
|
|
|
|
media.requestPictureInPicture();
|
|
|
|
} else {
|
|
|
|
displayWarning('Picture in Picture not supported.');
|
|
|
|
}
|
2020-11-24 23:05:43 +01:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
pip.style.display = 'none';
|
|
|
|
}
|
2020-11-09 00:35:52 +01:00
|
|
|
}
|
2020-11-08 17:07:17 +01:00
|
|
|
|
2020-11-28 01:53:29 +01:00
|
|
|
let fs = getVideoButton(container, 'fullscreen');
|
2020-11-24 23:05:43 +01:00
|
|
|
if(fs) {
|
2020-12-05 14:13:30 +01:00
|
|
|
if(HTMLVideoElement.prototype.requestFullscreen ||
|
|
|
|
/** @ts-ignore */
|
|
|
|
HTMLVideoElement.prototype.webkitRequestFullscreen) {
|
2020-11-28 01:53:29 +01:00
|
|
|
fs.onclick = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if(media.requestFullscreen) {
|
|
|
|
media.requestFullscreen();
|
2020-12-05 14:13:30 +01:00
|
|
|
/** @ts-ignore */
|
|
|
|
} else if(media.webkitRequestFullscreen) {
|
|
|
|
/** @ts-ignore */
|
|
|
|
media.webkitRequestFullscreen();
|
2020-11-28 01:53:29 +01:00
|
|
|
} else {
|
|
|
|
displayWarning('Full screen not supported!');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
fs.style.display = 'none';
|
|
|
|
}
|
2020-11-24 23:05:43 +01:00
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
2021-01-31 19:00:09 +01:00
|
|
|
* @param {string} localId
|
2020-09-12 16:34:52 +02:00
|
|
|
*/
|
2021-01-31 19:00:09 +01:00
|
|
|
function delMedia(localId) {
|
2020-04-24 19:38:21 +02:00
|
|
|
let mediadiv = document.getElementById('peers');
|
2021-01-31 19:00:09 +01:00
|
|
|
let peer = document.getElementById('peer-' + localId);
|
2020-08-19 14:39:40 +02:00
|
|
|
if(!peer)
|
|
|
|
throw new Error('Removing unknown media');
|
2020-09-20 14:33:13 +02:00
|
|
|
|
|
|
|
let media = /** @type{HTMLVideoElement} */
|
2021-01-31 19:00:09 +01:00
|
|
|
(document.getElementById('media-' + localId));
|
2020-04-24 19:38:21 +02:00
|
|
|
|
|
|
|
media.srcObject = null;
|
|
|
|
mediadiv.removeChild(peer);
|
2020-05-01 01:22:17 +02:00
|
|
|
|
2021-01-14 14:56:15 +01:00
|
|
|
setButtonsVisibility();
|
2020-05-01 01:22:17 +02:00
|
|
|
resizePeers();
|
2020-09-01 10:23:35 +02:00
|
|
|
hideVideo();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
/**
|
|
|
|
* @param {Stream} c
|
|
|
|
*/
|
|
|
|
function setMediaStatus(c) {
|
2020-06-12 21:28:35 +02:00
|
|
|
let state = c && c.pc && c.pc.iceConnectionState;
|
|
|
|
let good = state === 'connected' || state === 'completed';
|
|
|
|
|
2021-01-31 19:00:09 +01:00
|
|
|
let media = document.getElementById('media-' + c.localId);
|
2020-06-11 19:33:39 +02:00
|
|
|
if(!media) {
|
|
|
|
console.warn('Setting status of unknown media.');
|
|
|
|
return;
|
|
|
|
}
|
2020-12-03 01:07:48 +01:00
|
|
|
if(good) {
|
2020-06-11 19:33:39 +02:00
|
|
|
media.classList.remove('media-failed');
|
2020-12-03 01:07:48 +01:00
|
|
|
if(c.userdata.play) {
|
|
|
|
if(media instanceof HTMLMediaElement)
|
|
|
|
media.play().catch(e => {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
});
|
|
|
|
delete(c.userdata.play);
|
|
|
|
}
|
|
|
|
} else {
|
2020-06-11 19:33:39 +02:00
|
|
|
media.classList.add('media-failed');
|
2020-12-03 01:07:48 +01:00
|
|
|
}
|
2020-06-11 19:33:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
/**
|
|
|
|
* @param {Stream} c
|
|
|
|
* @param {string} [fallback]
|
|
|
|
*/
|
|
|
|
function setLabel(c, fallback) {
|
2021-01-31 19:00:09 +01:00
|
|
|
let label = document.getElementById('label-' + c.localId);
|
2020-04-24 19:38:21 +02:00
|
|
|
if(!label)
|
|
|
|
return;
|
2021-01-03 12:04:39 +01:00
|
|
|
let l = c.username;
|
2020-04-25 14:45:48 +02:00
|
|
|
if(l) {
|
|
|
|
label.textContent = l;
|
|
|
|
label.classList.remove('label-fallback');
|
|
|
|
} else if(fallback) {
|
|
|
|
label.textContent = fallback;
|
|
|
|
label.classList.add('label-fallback');
|
|
|
|
} else {
|
|
|
|
label.textContent = '';
|
|
|
|
label.classList.remove('label-fallback');
|
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-05-01 01:22:17 +02:00
|
|
|
function resizePeers() {
|
2020-10-07 09:33:38 +02:00
|
|
|
// Window resize can call this method too early
|
|
|
|
if (!serverConnection)
|
|
|
|
return;
|
2020-08-11 17:09:31 +02:00
|
|
|
let count =
|
|
|
|
Object.keys(serverConnection.up).length +
|
|
|
|
Object.keys(serverConnection.down).length;
|
2020-09-01 10:23:35 +02:00
|
|
|
let peers = document.getElementById('peers');
|
2020-05-01 01:22:17 +02:00
|
|
|
let columns = Math.ceil(Math.sqrt(count));
|
2020-09-03 20:54:27 +02:00
|
|
|
if (!count)
|
|
|
|
// No video, nothing to resize.
|
|
|
|
return;
|
2020-10-07 09:33:38 +02:00
|
|
|
let container = document.getElementById("video-container");
|
|
|
|
// Peers div has total padding of 40px, we remove 40 on offsetHeight
|
|
|
|
// Grid has row-gap of 5px
|
|
|
|
let rows = Math.ceil(count / columns);
|
2020-10-07 09:46:00 +02:00
|
|
|
let margins = (rows - 1) * 5 + 40;
|
2020-09-03 17:01:21 +02:00
|
|
|
|
2020-10-07 09:33:38 +02:00
|
|
|
if (count <= 2 && container.offsetHeight > container.offsetWidth) {
|
|
|
|
peers.style['grid-template-columns'] = "repeat(1, 1fr)";
|
|
|
|
rows = count;
|
|
|
|
} else {
|
|
|
|
peers.style['grid-template-columns'] = `repeat(${columns}, 1fr)`;
|
|
|
|
}
|
2020-10-07 09:46:00 +02:00
|
|
|
if (count === 1)
|
2020-10-07 09:33:38 +02:00
|
|
|
return;
|
|
|
|
let max_video_height = (peers.offsetHeight - margins) / rows;
|
|
|
|
let media_list = peers.querySelectorAll(".media");
|
2020-09-20 14:33:13 +02:00
|
|
|
for(let i = 0; i < media_list.length; i++) {
|
|
|
|
let media = media_list[i];
|
|
|
|
if(!(media instanceof HTMLMediaElement)) {
|
|
|
|
console.warn('Unexpected media');
|
|
|
|
continue;
|
|
|
|
}
|
2020-10-07 09:33:38 +02:00
|
|
|
media.style['max-height'] = max_video_height + "px";
|
2020-09-09 20:26:19 +02:00
|
|
|
}
|
2020-05-01 01:22:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-08 15:11:52 +02:00
|
|
|
/**
|
|
|
|
* Lexicographic order, with case differences secondary.
|
|
|
|
* @param{string} a
|
|
|
|
* @param{string} b
|
|
|
|
*/
|
|
|
|
function stringCompare(a, b) {
|
2020-12-26 17:28:44 +01:00
|
|
|
let la = a.toLowerCase();
|
|
|
|
let lb = b.toLowerCase();
|
2020-10-08 15:11:52 +02:00
|
|
|
if(la < lb)
|
|
|
|
return -1;
|
|
|
|
else if(la > lb)
|
|
|
|
return +1;
|
|
|
|
else if(a < b)
|
|
|
|
return -1;
|
|
|
|
else if(a > b)
|
|
|
|
return +1;
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2022-02-16 20:41:58 +01:00
|
|
|
/**
|
|
|
|
* @param {HTMLElement} elt
|
|
|
|
*/
|
|
|
|
function userMenu(elt) {
|
|
|
|
if(!elt.id.startsWith('user-'))
|
|
|
|
throw new Error('Unexpected id for user menu');
|
|
|
|
let id = elt.id.slice('user-'.length);
|
|
|
|
let user = serverConnection.users[id];
|
|
|
|
if(!user)
|
|
|
|
throw new Error("Couldn't find user")
|
|
|
|
let items = [];
|
|
|
|
if(id === serverConnection.id) {
|
|
|
|
let mydata = serverConnection.users[serverConnection.id].data;
|
|
|
|
if(mydata['raisehand'])
|
2022-03-26 09:13:49 +01:00
|
|
|
items.push({label: 'Unraise hand', onClick: () => {
|
2022-02-16 20:41:58 +01:00
|
|
|
serverConnection.userAction(
|
|
|
|
'setdata', serverConnection.id, {'raisehand': null},
|
|
|
|
);
|
|
|
|
}});
|
|
|
|
else
|
|
|
|
items.push({label: 'Raise hand', onClick: () => {
|
|
|
|
serverConnection.userAction(
|
|
|
|
'setdata', serverConnection.id, {'raisehand': true},
|
|
|
|
);
|
|
|
|
}});
|
2022-02-21 18:36:36 +01:00
|
|
|
if(serverConnection.permissions.indexOf('present')>= 0 && canFile())
|
|
|
|
items.push({label: 'Broadcast file', onClick: presentFile});
|
2022-02-16 20:41:58 +01:00
|
|
|
items.push({label: 'Restart media', onClick: renegotiateStreams});
|
|
|
|
} else {
|
|
|
|
items.push({label: 'Send file', onClick: () => {
|
|
|
|
sendFile(id);
|
|
|
|
}});
|
2022-02-19 23:43:44 +01:00
|
|
|
if(serverConnection.permissions.indexOf('op') >= 0) {
|
2022-02-16 20:41:58 +01:00
|
|
|
items.push({type: 'seperator'}); // sic
|
2022-02-19 23:43:44 +01:00
|
|
|
if(user.permissions.indexOf('present') >= 0)
|
2022-02-16 20:41:58 +01:00
|
|
|
items.push({label: 'Forbid presenting', onClick: () => {
|
|
|
|
serverConnection.userAction('unpresent', id);
|
|
|
|
}});
|
|
|
|
else
|
|
|
|
items.push({label: 'Allow presenting', onClick: () => {
|
|
|
|
serverConnection.userAction('present', id);
|
|
|
|
}});
|
|
|
|
items.push({label: 'Mute', onClick: () => {
|
2022-02-16 23:10:43 +01:00
|
|
|
serverConnection.userMessage('mute', id);
|
2022-02-16 20:41:58 +01:00
|
|
|
}});
|
|
|
|
items.push({label: 'Kick out', onClick: () => {
|
|
|
|
serverConnection.userAction('kick', id);
|
|
|
|
}});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/** @ts-ignore */
|
|
|
|
new Contextual({
|
|
|
|
items: items,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
2022-01-28 22:13:14 +01:00
|
|
|
* @param {user} userinfo
|
2020-08-13 20:55:13 +02:00
|
|
|
*/
|
2022-01-28 22:13:14 +01:00
|
|
|
function addUser(id, userinfo) {
|
2020-04-24 19:38:21 +02:00
|
|
|
let div = document.getElementById('users');
|
|
|
|
let user = document.createElement('div');
|
|
|
|
user.id = 'user-' + id;
|
2020-09-11 10:39:32 +02:00
|
|
|
user.classList.add("user-p");
|
2022-03-25 16:44:37 +01:00
|
|
|
setUserStatus(id, user, userinfo);
|
2022-02-16 20:41:58 +01:00
|
|
|
user.addEventListener('click', function(e) {
|
|
|
|
let elt = e.target;
|
|
|
|
if(!elt || !(elt instanceof HTMLElement))
|
|
|
|
throw new Error("Couldn't find user div");
|
|
|
|
userMenu(elt);
|
|
|
|
});
|
|
|
|
|
2022-02-16 20:33:59 +01:00
|
|
|
let us = div.children;
|
|
|
|
|
|
|
|
if(id === serverConnection.id) {
|
|
|
|
if(us.length === 0)
|
|
|
|
div.appendChild(user);
|
|
|
|
else
|
|
|
|
div.insertBefore(user, us[0]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-28 22:13:14 +01:00
|
|
|
if(userinfo.username) {
|
2020-09-12 15:23:38 +02:00
|
|
|
for(let i = 0; i < us.length; i++) {
|
|
|
|
let child = us[i];
|
2022-02-16 20:33:59 +01:00
|
|
|
let childid = child.id.slice('user-'.length);
|
|
|
|
if(childid === serverConnection.id)
|
|
|
|
continue;
|
|
|
|
let childuser = serverConnection.users[childid] || null;
|
2021-04-27 18:58:21 +02:00
|
|
|
let childname = (childuser && childuser.username) || null;
|
2022-01-28 22:13:14 +01:00
|
|
|
if(!childname || stringCompare(childname, userinfo.username) > 0) {
|
2020-09-12 15:23:38 +02:00
|
|
|
div.insertBefore(user, child);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-16 20:33:59 +01:00
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
div.appendChild(user);
|
|
|
|
}
|
|
|
|
|
2022-03-25 16:44:37 +01:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {user} userinfo
|
|
|
|
*/
|
2021-12-27 21:58:15 +01:00
|
|
|
function changeUser(id, userinfo) {
|
2022-03-25 16:44:37 +01:00
|
|
|
let elt = document.getElementById('user-' + id);
|
|
|
|
if(!elt) {
|
2021-04-27 18:58:21 +02:00
|
|
|
console.warn('Unknown user ' + id);
|
|
|
|
return;
|
|
|
|
}
|
2022-03-25 16:44:37 +01:00
|
|
|
setUserStatus(id, elt, userinfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {HTMLElement} elt
|
|
|
|
* @param {user} userinfo
|
|
|
|
*/
|
|
|
|
function setUserStatus(id, elt, userinfo) {
|
|
|
|
elt.textContent = userinfo.username ? userinfo.username : '(anon)';
|
|
|
|
if(userinfo.data.raisehand)
|
|
|
|
elt.classList.add('user-status-raisehand');
|
2021-12-27 21:58:15 +01:00
|
|
|
else
|
2022-03-25 16:44:37 +01:00
|
|
|
elt.classList.remove('user-status-raisehand');
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2021-04-27 18:58:21 +02:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
*/
|
|
|
|
function delUser(id) {
|
|
|
|
let div = document.getElementById('users');
|
|
|
|
let user = document.getElementById('user-' + id);
|
|
|
|
div.removeChild(user);
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 20:55:13 +02:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {string} kind
|
|
|
|
*/
|
2021-04-27 18:58:21 +02:00
|
|
|
function gotUser(id, kind) {
|
2020-08-12 13:51:31 +02:00
|
|
|
switch(kind) {
|
|
|
|
case 'add':
|
2022-01-28 22:13:14 +01:00
|
|
|
addUser(id, serverConnection.users[id]);
|
2021-05-09 15:06:10 +02:00
|
|
|
if(Object.keys(serverConnection.users).length == 3)
|
|
|
|
reconsiderSendParameters();
|
2020-08-12 13:51:31 +02:00
|
|
|
break;
|
|
|
|
case 'delete':
|
2021-04-27 18:58:21 +02:00
|
|
|
delUser(id);
|
2021-05-09 15:06:10 +02:00
|
|
|
if(Object.keys(serverConnection.users).length < 3)
|
|
|
|
scheduleReconsiderParameters();
|
2021-04-27 18:58:21 +02:00
|
|
|
break;
|
|
|
|
case 'change':
|
2021-12-27 21:58:15 +01:00
|
|
|
changeUser(id, serverConnection.users[id]);
|
2020-08-12 13:51:31 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.warn('Unknown user kind', kind);
|
|
|
|
break;
|
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-04-25 18:09:31 +02:00
|
|
|
function displayUsername() {
|
2022-02-20 15:32:18 +01:00
|
|
|
document.getElementById('userspan').textContent = serverConnection.username;
|
2022-02-19 23:43:44 +01:00
|
|
|
let op = serverConnection.permissions.indexOf('op') >= 0;
|
|
|
|
let present = serverConnection.permissions.indexOf('present') >= 0;
|
2020-04-25 18:09:31 +02:00
|
|
|
let text = '';
|
2022-02-19 23:43:44 +01:00
|
|
|
if(op && present)
|
2020-11-24 17:36:52 +01:00
|
|
|
text = '(op, presenter)';
|
2022-02-19 23:43:44 +01:00
|
|
|
else if(op)
|
2020-11-24 17:36:52 +01:00
|
|
|
text = 'operator';
|
2022-02-19 23:43:44 +01:00
|
|
|
else if(present)
|
2020-11-24 17:36:52 +01:00
|
|
|
text = 'presenter';
|
|
|
|
document.getElementById('permspan').textContent = text;
|
2020-04-25 18:09:31 +02:00
|
|
|
}
|
|
|
|
|
2020-12-01 00:26:14 +01:00
|
|
|
let presentRequested = null;
|
|
|
|
|
2021-07-16 19:41:00 +02:00
|
|
|
/**
|
2021-07-16 23:03:36 +02:00
|
|
|
* @param {string} s
|
|
|
|
*/
|
|
|
|
function capitalise(s) {
|
|
|
|
if(s.length <= 0)
|
|
|
|
return s;
|
|
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} title
|
2021-07-16 19:41:00 +02:00
|
|
|
*/
|
|
|
|
function setTitle(title) {
|
|
|
|
function set(title) {
|
|
|
|
document.title = title;
|
|
|
|
document.getElementById('title').textContent = title;
|
|
|
|
}
|
2021-07-16 23:03:36 +02:00
|
|
|
if(title)
|
2021-07-16 19:41:00 +02:00
|
|
|
set(title);
|
2021-10-26 22:38:05 +02:00
|
|
|
else
|
|
|
|
set('Galène');
|
2021-07-16 19:41:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
/**
|
2020-12-01 22:42:06 +01:00
|
|
|
* @this {ServerConnection}
|
|
|
|
* @param {string} group
|
2022-02-19 23:43:44 +01:00
|
|
|
* @param {Array<string>} perms
|
2021-07-16 19:41:00 +02:00
|
|
|
* @param {Object<string,any>} status
|
2022-01-29 22:54:44 +01:00
|
|
|
* @param {Object<string,any>} data
|
2021-07-16 19:41:00 +02:00
|
|
|
* @param {string} message
|
2020-08-11 17:09:31 +02:00
|
|
|
*/
|
2022-01-29 22:54:44 +01:00
|
|
|
async function gotJoined(kind, group, perms, status, data, message) {
|
2020-12-04 22:42:20 +01:00
|
|
|
let present = presentRequested;
|
|
|
|
presentRequested = null;
|
|
|
|
|
|
|
|
switch(kind) {
|
|
|
|
case 'fail':
|
2020-12-01 22:42:06 +01:00
|
|
|
displayError('The server said: ' + message);
|
|
|
|
this.close();
|
2021-02-04 23:11:40 +01:00
|
|
|
setButtonsVisibility();
|
2020-12-01 22:42:06 +01:00
|
|
|
return;
|
2020-12-04 22:42:20 +01:00
|
|
|
case 'redirect':
|
|
|
|
this.close();
|
2021-07-16 19:41:00 +02:00
|
|
|
document.location.href = message;
|
2020-12-04 22:42:20 +01:00
|
|
|
return;
|
|
|
|
case 'leave':
|
|
|
|
this.close();
|
2021-02-04 23:11:40 +01:00
|
|
|
setButtonsVisibility();
|
2020-12-04 22:42:20 +01:00
|
|
|
return;
|
|
|
|
case 'join':
|
2020-12-12 13:45:49 +01:00
|
|
|
case 'change':
|
2021-10-26 22:22:48 +02:00
|
|
|
groupStatus = status;
|
2021-07-16 23:03:36 +02:00
|
|
|
setTitle((status && status.displayName) || capitalise(group));
|
2020-12-12 13:45:49 +01:00
|
|
|
displayUsername();
|
|
|
|
setButtonsVisibility();
|
|
|
|
if(kind === 'change')
|
|
|
|
return;
|
2020-12-04 22:42:20 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
displayError('Unknown join message');
|
|
|
|
this.close();
|
|
|
|
return;
|
2020-12-01 22:42:06 +01:00
|
|
|
}
|
|
|
|
|
2020-12-11 18:28:17 +01:00
|
|
|
let input = /** @type{HTMLTextAreaElement} */
|
|
|
|
(document.getElementById('input'));
|
|
|
|
input.placeholder = 'Type /help for help';
|
|
|
|
setTimeout(() => {input.placeholder = '';}, 8000);
|
|
|
|
|
2022-03-22 18:18:04 +01:00
|
|
|
if(status.locked)
|
|
|
|
displayWarning('This group is locked');
|
|
|
|
|
2021-02-14 18:06:50 +01:00
|
|
|
if(typeof RTCPeerConnection === 'undefined')
|
|
|
|
displayWarning("This browser doesn't support WebRTC");
|
|
|
|
else
|
2021-04-28 17:00:50 +02:00
|
|
|
this.request(mapRequest(getSettings().request));
|
2020-12-04 22:42:20 +01:00
|
|
|
|
2022-02-19 23:43:44 +01:00
|
|
|
if(serverConnection.permissions.indexOf('present') >= 0 &&
|
|
|
|
!findUpMedia('camera')) {
|
2020-12-04 22:42:20 +01:00
|
|
|
if(present) {
|
|
|
|
if(present === 'mike')
|
|
|
|
updateSettings({video: ''});
|
|
|
|
else if(present === 'both')
|
|
|
|
delSetting('video');
|
|
|
|
reflectSettings();
|
|
|
|
|
|
|
|
let button = getButtonElement('presentbutton');
|
|
|
|
button.disabled = true;
|
|
|
|
try {
|
|
|
|
await addLocalMedia();
|
|
|
|
} finally {
|
|
|
|
button.disabled = false;
|
2020-12-01 00:26:14 +01:00
|
|
|
}
|
2020-12-04 22:42:20 +01:00
|
|
|
} else {
|
|
|
|
displayMessage(
|
2021-07-29 14:46:02 +02:00
|
|
|
"Press Enable to enable your camera or microphone"
|
2020-12-04 22:42:20 +01:00
|
|
|
);
|
2020-12-01 00:26:14 +01:00
|
|
|
}
|
|
|
|
}
|
2020-04-25 02:25:51 +02:00
|
|
|
}
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
/** @type {Object<string,TransferredFile>} */
|
2022-01-30 06:32:55 +01:00
|
|
|
let transferredFiles = {};
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* A file in the process of being transferred.
|
|
|
|
*
|
|
|
|
* @constructor
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function TransferredFile(id, userid, up, username, name, type, size) {
|
|
|
|
/** @type {string} */
|
|
|
|
this.id = id;
|
|
|
|
/** @type {string} */
|
|
|
|
this.userid = userid;
|
|
|
|
/** @type {boolean} */
|
|
|
|
this.up = up;
|
|
|
|
/** @type {string} */
|
|
|
|
this.username = username;
|
|
|
|
/** @type {string} */
|
|
|
|
this.name = name;
|
|
|
|
/** @type {string} */
|
|
|
|
this.type = type;
|
|
|
|
/** @type {number} */
|
|
|
|
this.size = size;
|
|
|
|
/** @type {File} */
|
|
|
|
this.file = null;
|
|
|
|
/** @type {RTCPeerConnection} */
|
|
|
|
this.pc = null;
|
|
|
|
/** @type {RTCDataChannel} */
|
|
|
|
this.dc = null;
|
|
|
|
/** @type {Array<RTCIceCandidateInit>} */
|
|
|
|
this.candidates = [];
|
|
|
|
/** @type {Array<Blob|ArrayBuffer>} */
|
|
|
|
this.data = [];
|
|
|
|
/** @type {number} */
|
|
|
|
this.datalen = 0;
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
TransferredFile.prototype.fullid = function() {
|
|
|
|
return this.userid + (this.up ? '+' : '-') + this.id;
|
|
|
|
};
|
|
|
|
|
2022-01-30 06:32:55 +01:00
|
|
|
/**
|
|
|
|
* @param {boolean} up
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {string} userid
|
2022-01-30 06:32:55 +01:00
|
|
|
* @param {string} fileid
|
2022-02-01 18:46:51 +01:00
|
|
|
* @returns {TransferredFile}
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
TransferredFile.get = function(up, userid, fileid) {
|
|
|
|
return transferredFiles[userid + (up ? '+' : '-') + fileid];
|
|
|
|
};
|
|
|
|
|
|
|
|
TransferredFile.prototype.close = function() {
|
2022-03-23 00:40:23 +01:00
|
|
|
if(this.dc) {
|
|
|
|
this.dc.onclose = null;
|
|
|
|
this.dc.onerror = null;
|
|
|
|
this.dc.onmessage = null;
|
|
|
|
}
|
2022-02-01 18:46:51 +01:00
|
|
|
if(this.pc)
|
|
|
|
this.pc.close();
|
|
|
|
this.dc = null;
|
|
|
|
this.pc = null;
|
|
|
|
this.data = [];
|
|
|
|
this.datalen = 0;
|
|
|
|
delete(transferredFiles[this.fullid()]);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
TransferredFile.prototype.pushData = function(data) {
|
|
|
|
if(data instanceof Blob) {
|
|
|
|
this.datalen += data.size;
|
|
|
|
} else if(data instanceof ArrayBuffer) {
|
|
|
|
this.datalen += data.byteLength;
|
|
|
|
} else {
|
|
|
|
throw new Error('unexpected type for received data');
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
2022-02-01 18:46:51 +01:00
|
|
|
this.data.push(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
TransferredFile.prototype.getData = function() {
|
|
|
|
let blob = new Blob(this.data, {type: this.type});
|
|
|
|
if(blob.size != this.datalen)
|
|
|
|
throw new Error('Inconsistent data size');
|
|
|
|
this.data = [];
|
|
|
|
this.datalen = 0;
|
|
|
|
return blob;
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function fileTransferBox(f) {
|
2022-01-30 06:32:55 +01:00
|
|
|
let p = document.createElement('p');
|
2022-02-01 18:46:51 +01:00
|
|
|
if(f.up)
|
2022-01-30 06:32:55 +01:00
|
|
|
p.textContent =
|
|
|
|
`We have offered to send a file called "${f.name}" ` +
|
|
|
|
`to user ${f.username}.`;
|
|
|
|
else
|
|
|
|
p.textContent =
|
|
|
|
`User ${f.username} offered to send us a file ` +
|
|
|
|
`called "${f.name}" of size ${f.size}.`
|
|
|
|
let bno = null, byes = null;
|
2022-02-01 18:46:51 +01:00
|
|
|
if(f.up) {
|
2022-01-30 06:32:55 +01:00
|
|
|
bno = document.createElement('button');
|
|
|
|
bno.textContent = 'Cancel';
|
|
|
|
bno.onclick = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
cancelFile(f);
|
2022-01-30 06:32:55 +01:00
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
bno.id = "bno-" + f.fullid();
|
2022-01-30 06:32:55 +01:00
|
|
|
} else {
|
|
|
|
byes = document.createElement('button');
|
|
|
|
byes.textContent = 'Accept';
|
|
|
|
byes.onclick = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
getFile(f);
|
2022-01-30 06:32:55 +01:00
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
byes.id = "byes-" + f.fullid();
|
2022-01-30 06:32:55 +01:00
|
|
|
bno = document.createElement('button');
|
|
|
|
bno.textContent = 'Decline';
|
|
|
|
bno.onclick = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
rejectFile(f);
|
2022-01-30 06:32:55 +01:00
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
bno.id = "bno-" + f.fullid();
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
let status = document.createElement('div');
|
2022-02-01 18:46:51 +01:00
|
|
|
status.id = 'status-' + f.fullid();
|
|
|
|
if(!f.up) {
|
2022-01-30 19:22:43 +01:00
|
|
|
status.textContent =
|
|
|
|
'(Choosing "Accept" will disclose your IP address.)';
|
|
|
|
}
|
2022-01-30 06:32:55 +01:00
|
|
|
let div = document.createElement('div');
|
2022-02-01 18:46:51 +01:00
|
|
|
div.id = 'file-' + f.fullid();
|
2022-01-30 06:32:55 +01:00
|
|
|
div.appendChild(p);
|
|
|
|
if(byes)
|
|
|
|
div.appendChild(byes);
|
|
|
|
if(bno)
|
|
|
|
div.appendChild(bno);
|
|
|
|
div.appendChild(status);
|
|
|
|
div.classList.add('message');
|
|
|
|
div.classList.add('message-private');
|
2022-02-01 18:46:51 +01:00
|
|
|
div.classList.add('message-row');
|
2022-01-30 06:32:55 +01:00
|
|
|
let box = document.getElementById('box');
|
|
|
|
box.appendChild(div);
|
|
|
|
return div;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
* @param {string} status
|
|
|
|
* @param {boolean} [delyes]
|
|
|
|
* @param {boolean} [delno]
|
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function setFileStatus(f, status, delyes, delno) {
|
|
|
|
let statusdiv = document.getElementById('status-' + f.fullid());
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!statusdiv)
|
|
|
|
throw new Error("Couldn't find statusdiv");
|
|
|
|
statusdiv.textContent = status;
|
|
|
|
if(delyes || delno) {
|
2022-02-01 18:46:51 +01:00
|
|
|
let div = document.getElementById('file-' + f.fullid());
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!div)
|
|
|
|
throw new Error("Couldn't find file div");
|
|
|
|
if(delyes) {
|
2022-02-01 18:46:51 +01:00
|
|
|
let byes = document.getElementById('byes-' + f.fullid())
|
2022-01-30 06:32:55 +01:00
|
|
|
if(byes)
|
|
|
|
div.removeChild(byes);
|
|
|
|
}
|
|
|
|
if(delno) {
|
2022-02-01 18:46:51 +01:00
|
|
|
let bno = document.getElementById('bno-' + f.fullid())
|
2022-01-30 06:32:55 +01:00
|
|
|
if(bno)
|
|
|
|
div.removeChild(bno);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-01 13:33:02 +01:00
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-02-01 13:33:02 +01:00
|
|
|
* @param {any} message
|
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function failFile(f, message) {
|
2022-03-23 00:40:23 +01:00
|
|
|
if(!f.dc)
|
|
|
|
return;
|
2022-02-01 13:33:02 +01:00
|
|
|
console.error('File transfer failed:', message);
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, message ? `Failed: ${message}` : 'Failed.');
|
|
|
|
f.close();
|
2022-02-01 13:33:02 +01:00
|
|
|
}
|
|
|
|
|
2022-01-30 06:32:55 +01:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {File} file
|
|
|
|
*/
|
2022-02-16 20:41:58 +01:00
|
|
|
function offerFile(id, file) {
|
2022-01-30 06:32:55 +01:00
|
|
|
let fileid = newRandomId();
|
2022-02-16 20:41:58 +01:00
|
|
|
let username = serverConnection.users[id].username;
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = new TransferredFile(
|
|
|
|
fileid, id, true, username, file.name, file.type, file.size,
|
|
|
|
);
|
|
|
|
f.file = file;
|
|
|
|
transferredFiles[f.fullid()] = f;
|
|
|
|
try {
|
|
|
|
fileTransferBox(f);
|
|
|
|
serverConnection.userMessage('offerfile', id, {
|
|
|
|
id: fileid,
|
|
|
|
name: f.name,
|
|
|
|
size: f.size,
|
|
|
|
type: f.type,
|
|
|
|
});
|
|
|
|
} catch(e) {
|
|
|
|
displayError(e);
|
|
|
|
f.close();
|
|
|
|
}
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function cancelFile(f) {
|
|
|
|
serverConnection.userMessage('cancelfile', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
});
|
2022-02-01 18:46:51 +01:00
|
|
|
f.close();
|
|
|
|
setFileStatus(f, 'Cancelled.', true, true);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
async function getFile(f) {
|
2022-01-30 06:32:55 +01:00
|
|
|
if(f.pc)
|
2022-02-01 18:46:51 +01:00
|
|
|
throw new Error("Download already in progress");
|
|
|
|
setFileStatus(f, 'Connecting...', true);
|
2022-03-23 00:12:51 +01:00
|
|
|
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!pc)
|
|
|
|
throw new Error("Couldn't create peer connection");
|
|
|
|
f.pc = pc;
|
|
|
|
f.candidates = [];
|
|
|
|
pc.onsignalingstatechange = function(e) {
|
|
|
|
if(pc.signalingState === 'stable') {
|
|
|
|
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
|
|
|
f.candidates = [];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
pc.onicecandidate = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
serverConnection.userMessage('filedownice', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
candidate: e.candidate,
|
|
|
|
});
|
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc = pc.createDataChannel('file');
|
2022-01-30 06:32:55 +01:00
|
|
|
f.data = [];
|
|
|
|
f.datalen = 0;
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onclose = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
try {
|
2022-02-01 18:46:51 +01:00
|
|
|
closeReceiveFileData(f);
|
2022-02-01 13:33:02 +01:00
|
|
|
} catch(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, e);
|
2022-02-01 13:33:02 +01:00
|
|
|
}
|
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onmessage = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
try {
|
2022-02-01 18:46:51 +01:00
|
|
|
receiveFileData(f, e.data);
|
2022-02-01 13:33:02 +01:00
|
|
|
} catch(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, e);
|
2022-02-01 13:33:02 +01:00
|
|
|
}
|
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onerror = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
/** @ts-ignore */
|
|
|
|
let err = e.error;
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, err);
|
2022-02-01 13:33:02 +01:00
|
|
|
};
|
2022-01-30 06:32:55 +01:00
|
|
|
let offer = await pc.createOffer();
|
|
|
|
if(!offer)
|
|
|
|
throw new Error("Couldn't create offer");
|
|
|
|
await pc.setLocalDescription(offer);
|
2022-02-01 18:46:51 +01:00
|
|
|
serverConnection.userMessage('getfile', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
offer: pc.localDescription.sdp,
|
|
|
|
});
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Negotiating...', true);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
async function rejectFile(f) {
|
|
|
|
serverConnection.userMessage('rejectfile', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
});
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Rejected.', true, true);
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
* @param {string} sdp
|
|
|
|
*/
|
2022-02-21 16:53:22 +01:00
|
|
|
async function sendOfferedFile(f, sdp) {
|
2022-01-30 06:32:55 +01:00
|
|
|
if(f.pc)
|
|
|
|
throw new Error('Transfer already in progress');
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Negotiating...', true);
|
2022-03-23 00:12:51 +01:00
|
|
|
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!pc)
|
|
|
|
throw new Error("Couldn't create peer connection");
|
|
|
|
f.pc = pc;
|
|
|
|
f.candidates = [];
|
|
|
|
pc.onicecandidate = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
serverConnection.userMessage('fileupice', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
candidate: e.candidate,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
pc.onsignalingstatechange = function(e) {
|
|
|
|
if(pc.signalingState === 'stable') {
|
|
|
|
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
|
|
|
f.candidates = [];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
pc.ondatachannel = function(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
if(f.dc)
|
|
|
|
throw new Error('Duplicate datachannel');
|
|
|
|
f.dc = /** @type{RTCDataChannel} */(e.channel);
|
|
|
|
f.dc.onclose = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
try {
|
2022-02-01 18:46:51 +01:00
|
|
|
closeSendFileData(f);
|
2022-02-01 13:33:02 +01:00
|
|
|
} catch(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, e);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
2022-02-01 13:33:02 +01:00
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onerror = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
/** @ts-ignore */
|
|
|
|
let err = e.error;
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, err);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onmessage = function(e) {
|
2022-02-01 13:33:02 +01:00
|
|
|
try {
|
2022-02-01 18:46:51 +01:00
|
|
|
ackSendFileData(f, e.data);
|
2022-02-01 13:33:02 +01:00
|
|
|
} catch(e) {
|
2022-02-01 18:46:51 +01:00
|
|
|
failFile(f, e);
|
2022-02-01 13:33:02 +01:00
|
|
|
}
|
|
|
|
};
|
2022-02-01 18:46:51 +01:00
|
|
|
sendFileData(f).catch(e => failFile(f, e));
|
2022-01-30 06:32:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
await pc.setRemoteDescription({
|
|
|
|
type: 'offer',
|
|
|
|
sdp: sdp,
|
|
|
|
});
|
|
|
|
|
|
|
|
let answer = await pc.createAnswer();
|
|
|
|
if(!answer)
|
|
|
|
throw new Error("Couldn't create answer");
|
|
|
|
await pc.setLocalDescription(answer);
|
2022-02-01 18:46:51 +01:00
|
|
|
serverConnection.userMessage('sendfile', f.userid, {
|
2022-01-30 06:32:55 +01:00
|
|
|
id: f.id,
|
|
|
|
answer: pc.localDescription.sdp,
|
|
|
|
});
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Uploading...', true);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
* @param {string} sdp
|
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
async function receiveFile(f, sdp) {
|
2022-01-30 06:32:55 +01:00
|
|
|
await f.pc.setRemoteDescription({
|
|
|
|
type: 'answer',
|
|
|
|
sdp: sdp,
|
|
|
|
});
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Downloading...', true);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
async function sendFileData(f) {
|
|
|
|
let r = f.file.stream().getReader();
|
2022-01-30 06:32:55 +01:00
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.bufferedAmountLowThreshold = 65536;
|
2022-01-30 06:32:55 +01:00
|
|
|
|
|
|
|
async function write(a) {
|
2022-02-01 18:46:51 +01:00
|
|
|
while(f.dc.bufferedAmount > f.dc.bufferedAmountLowThreshold) {
|
2022-01-30 06:32:55 +01:00
|
|
|
await new Promise((resolve, reject) => {
|
2022-03-23 00:40:23 +01:00
|
|
|
if(!f.dc) {
|
2022-02-01 18:46:51 +01:00
|
|
|
reject(new Error('File is closed.'));
|
2022-03-23 00:40:23 +01:00
|
|
|
return;
|
2022-02-01 18:46:51 +01:00
|
|
|
}
|
|
|
|
f.dc.onbufferedamountlow = function(e) {
|
2022-03-23 00:40:23 +01:00
|
|
|
if(!f.dc) {
|
2022-02-01 18:46:51 +01:00
|
|
|
reject(new Error('File is closed.'));
|
2022-03-23 00:40:23 +01:00
|
|
|
return;
|
2022-02-01 18:46:51 +01:00
|
|
|
}
|
|
|
|
f.dc.onbufferedamountlow = null;
|
2022-01-30 06:32:55 +01:00
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.send(a);
|
2022-01-30 06:32:55 +01:00
|
|
|
f.datalen += a.length;
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, `Uploading... ${f.datalen}/${f.size}`, true);
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
while(true) {
|
|
|
|
let v = await r.read();
|
|
|
|
if(v.done)
|
|
|
|
break;
|
2022-02-01 18:46:51 +01:00
|
|
|
if(!(v.value instanceof Uint8Array))
|
|
|
|
throw new Error('Unexpected type for chunk');
|
|
|
|
if(v.value.length <= 16384) {
|
2022-01-30 06:32:55 +01:00
|
|
|
await write(v.value);
|
|
|
|
} else {
|
|
|
|
for(let i = 0; i < v.value.length; i += 16384) {
|
|
|
|
let a = new Uint8Array(
|
|
|
|
v.value.buffer, i, Math.min(16384, v.value.length - i),
|
|
|
|
);
|
|
|
|
await write(a);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function ackSendFileData(f, data) {
|
2022-01-30 06:32:55 +01:00
|
|
|
if(data === 'done' && f.datalen == f.size)
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Done.', true, true);
|
2022-01-30 06:32:55 +01:00
|
|
|
else
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Failed.', true, true);
|
|
|
|
f.dc.onclose = null;
|
|
|
|
f.dc.onerror = null;
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function closeSendFileData(f) {
|
|
|
|
setFileStatus(f, 'Failed.', true, true);
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-02-01 13:33:02 +01:00
|
|
|
* @param {Blob|ArrayBuffer} data
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function receiveFileData(f, data) {
|
|
|
|
f.pushData(data);
|
|
|
|
setFileStatus(f, `Downloading... ${f.datalen}/${f.size}`, true);
|
2022-01-30 06:32:55 +01:00
|
|
|
|
|
|
|
if(f.datalen < f.size)
|
|
|
|
return;
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
if(f.datalen != f.size) {
|
|
|
|
setFileStatus(f, 'Failed.', true, true);
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onmessage = null;
|
|
|
|
doneReceiveFileData(f);
|
|
|
|
}
|
2022-02-01 13:33:02 +01:00
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
/**
|
|
|
|
* @param {TransferredFile} f
|
|
|
|
*/
|
|
|
|
async function doneReceiveFileData(f) {
|
|
|
|
setFileStatus(f, 'Done.', true, true);
|
|
|
|
let blob = f.getData();
|
|
|
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
let timer = setTimeout(function(e) { resolve(); }, 2000);
|
|
|
|
f.dc.onclose = function(e) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
f.dc.onerror = function(e) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
f.dc.send('done');
|
|
|
|
});
|
2022-01-30 06:32:55 +01:00
|
|
|
|
2022-02-01 18:46:51 +01:00
|
|
|
f.dc.onclose = null;
|
|
|
|
f.dc.onerror = null;
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
|
|
|
|
let url = URL.createObjectURL(blob);
|
|
|
|
let a = document.createElement('a');
|
|
|
|
a.href = url;
|
|
|
|
a.textContent = f.name;
|
|
|
|
a.download = f.name;
|
|
|
|
a.type = f.type;
|
|
|
|
a.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-01 18:46:51 +01:00
|
|
|
* @param {TransferredFile} f
|
2022-01-30 06:32:55 +01:00
|
|
|
*/
|
2022-02-01 18:46:51 +01:00
|
|
|
function closeReceiveFileData(f) {
|
2022-03-23 00:40:23 +01:00
|
|
|
if(f.datalen !== f.size) {
|
2022-02-01 18:46:51 +01:00
|
|
|
setFileStatus(f, 'Failed.', true, true)
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-28 01:42:26 +01:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
* @param {string} dest
|
|
|
|
* @param {string} username
|
|
|
|
* @param {number} time
|
|
|
|
* @param {boolean} privileged
|
|
|
|
* @param {string} kind
|
2022-01-30 06:32:55 +01:00
|
|
|
* @param {any} message
|
2020-12-28 01:42:26 +01:00
|
|
|
*/
|
|
|
|
function gotUserMessage(id, dest, username, time, privileged, kind, message) {
|
|
|
|
switch(kind) {
|
2021-07-30 19:38:02 +02:00
|
|
|
case 'kicked':
|
2020-12-28 01:42:26 +01:00
|
|
|
case 'error':
|
|
|
|
case 'warning':
|
|
|
|
case 'info':
|
|
|
|
let from = id ? (username || 'Anonymous') : 'The Server';
|
|
|
|
if(privileged)
|
|
|
|
displayError(`${from} said: ${message}`, kind);
|
|
|
|
else
|
|
|
|
console.error(`Got unprivileged message of kind ${kind}`);
|
|
|
|
break;
|
|
|
|
case 'mute':
|
|
|
|
if(privileged) {
|
|
|
|
setLocalMute(true, true);
|
|
|
|
let by = username ? ' by ' + username : '';
|
|
|
|
displayWarning(`You have been muted${by}`);
|
|
|
|
} else {
|
|
|
|
console.error(`Got unprivileged message of kind ${kind}`);
|
|
|
|
}
|
|
|
|
break;
|
2021-01-03 17:47:56 +01:00
|
|
|
case 'clearchat':
|
|
|
|
if(privileged) {
|
|
|
|
clearChat();
|
|
|
|
} else {
|
|
|
|
console.error(`Got unprivileged message of kind ${kind}`);
|
|
|
|
}
|
2021-01-04 13:15:50 +01:00
|
|
|
break;
|
2022-01-30 06:32:55 +01:00
|
|
|
case 'offerfile': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = new TransferredFile(
|
|
|
|
message.id, id, false, username,
|
|
|
|
message.name, message.type, message.size,
|
|
|
|
);
|
|
|
|
transferredFiles[f.fullid()] = f;
|
|
|
|
fileTransferBox(f);
|
2022-01-30 06:32:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'cancelfile': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(false, id, message.id);
|
|
|
|
if(!f)
|
|
|
|
throw new Error('unexpected cancelfile');
|
|
|
|
setFileStatus(f, 'Cancelled.', true, true);
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'getfile': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(true, id, message.id);
|
|
|
|
if(!f)
|
|
|
|
throw new Error('unexpected getfile');
|
2022-02-21 16:53:22 +01:00
|
|
|
sendOfferedFile(f, message.offer);
|
2022-01-30 06:32:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'rejectfile': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(true, id, message.id);
|
|
|
|
if(!f)
|
|
|
|
throw new Error('unexpected rejectfile');
|
|
|
|
setFileStatus(f, 'Rejected.', true, true);
|
|
|
|
f.close();
|
2022-01-30 06:32:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'sendfile': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(false, id, message.id);
|
|
|
|
if(!f)
|
|
|
|
throw new Error('unexpected sendfile');
|
|
|
|
receiveFile(f, message.answer);
|
2022-01-30 06:32:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'filedownice': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(true, id, message.id);
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!f.pc) {
|
|
|
|
console.warn('Unexpected filedownice');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(f.pc.signalingState === 'stable')
|
|
|
|
f.pc.addIceCandidate(message.candidate).catch(console.warn);
|
|
|
|
else
|
|
|
|
f.candidates.push(message.candidate);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'fileupice': {
|
2022-02-01 18:46:51 +01:00
|
|
|
let f = TransferredFile.get(false, id, message.id);
|
2022-01-30 06:32:55 +01:00
|
|
|
if(!f.pc) {
|
|
|
|
console.warn('Unexpected fileupice');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(f.pc.signalingState === 'stable')
|
|
|
|
f.pc.addIceCandidate(message.candidate).catch(console.warn);
|
|
|
|
else
|
|
|
|
f.candidates.push(message.candidate);
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
2020-12-28 01:42:26 +01:00
|
|
|
default:
|
|
|
|
console.warn(`Got unknown user message ${kind}`);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2020-11-09 00:11:55 +01:00
|
|
|
const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#&()=?]+[-a-zA-Z0-9@:%/_\\+~#&()=]/g;
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2020-09-12 16:34:52 +02:00
|
|
|
/**
|
|
|
|
* @param {string} line
|
2020-12-01 18:18:58 +01:00
|
|
|
* @returns {Array.<Text|HTMLElement>}
|
2020-09-12 16:34:52 +02:00
|
|
|
*/
|
2020-04-24 19:38:21 +02:00
|
|
|
function formatLine(line) {
|
|
|
|
let r = new RegExp(urlRegexp);
|
|
|
|
let result = [];
|
|
|
|
let pos = 0;
|
|
|
|
while(true) {
|
|
|
|
let m = r.exec(line);
|
|
|
|
if(!m)
|
|
|
|
break;
|
|
|
|
result.push(document.createTextNode(line.slice(pos, m.index)));
|
|
|
|
let a = document.createElement('a');
|
|
|
|
a.href = m[0];
|
|
|
|
a.textContent = m[0];
|
|
|
|
a.target = '_blank';
|
|
|
|
a.rel = 'noreferrer noopener';
|
|
|
|
result.push(a);
|
|
|
|
pos = m.index + m[0].length;
|
|
|
|
}
|
|
|
|
result.push(document.createTextNode(line.slice(pos)));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-04-20 17:38:55 +02:00
|
|
|
/**
|
|
|
|
* @param {string[]} lines
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
*/
|
|
|
|
function formatLines(lines) {
|
|
|
|
let elts = [];
|
|
|
|
if(lines.length > 0)
|
|
|
|
elts = formatLine(lines[0]);
|
|
|
|
for(let i = 1; i < lines.length; i++) {
|
|
|
|
elts.push(document.createElement('br'));
|
|
|
|
elts = elts.concat(formatLine(lines[i]));
|
|
|
|
}
|
|
|
|
let elt = document.createElement('p');
|
|
|
|
elts.forEach(e => elt.appendChild(e));
|
|
|
|
return elt;
|
|
|
|
}
|
|
|
|
|
2020-09-30 00:33:23 +02:00
|
|
|
/**
|
|
|
|
* @param {number} time
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
function formatTime(time) {
|
|
|
|
let delta = Date.now() - time;
|
|
|
|
let date = new Date(time);
|
2020-10-12 12:48:12 +02:00
|
|
|
let m = date.getMinutes();
|
2020-11-05 21:09:16 +01:00
|
|
|
if(delta > -30000)
|
2020-10-12 12:48:12 +02:00
|
|
|
return date.getHours() + ':' + ((m < 10) ? '0' : '') + m;
|
2020-09-30 00:33:23 +02:00
|
|
|
return date.toLocaleString();
|
|
|
|
}
|
|
|
|
|
2020-08-24 22:37:48 +02:00
|
|
|
/**
|
|
|
|
* @typedef {Object} lastMessage
|
|
|
|
* @property {string} [nick]
|
|
|
|
* @property {string} [peerId]
|
2020-10-01 16:52:01 +02:00
|
|
|
* @property {string} [dest]
|
2020-12-01 02:51:15 +01:00
|
|
|
* @property {number} [time]
|
2020-08-24 22:37:48 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
/** @type {lastMessage} */
|
2020-04-24 19:38:21 +02:00
|
|
|
let lastMessage = {};
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
|
|
|
* @param {string} peerId
|
2021-07-31 14:42:26 +02:00
|
|
|
* @param {string} dest
|
2020-09-20 14:33:13 +02:00
|
|
|
* @param {string} nick
|
2020-09-30 00:33:23 +02:00
|
|
|
* @param {number} time
|
2021-07-31 14:42:26 +02:00
|
|
|
* @param {boolean} privileged
|
|
|
|
* @param {boolean} history
|
2020-09-20 14:33:13 +02:00
|
|
|
* @param {string} kind
|
2020-12-28 01:42:26 +01:00
|
|
|
* @param {unknown} message
|
2020-09-20 14:33:13 +02:00
|
|
|
*/
|
2021-07-31 14:42:26 +02:00
|
|
|
function addToChatbox(peerId, dest, nick, time, privileged, history, kind, message) {
|
2020-08-27 21:17:46 +02:00
|
|
|
let row = document.createElement('div');
|
|
|
|
row.classList.add('message-row');
|
2020-04-24 19:38:21 +02:00
|
|
|
let container = document.createElement('div');
|
|
|
|
container.classList.add('message');
|
2020-08-27 21:17:46 +02:00
|
|
|
row.appendChild(container);
|
2020-10-12 12:48:12 +02:00
|
|
|
let footer = document.createElement('p');
|
|
|
|
footer.classList.add('message-footer');
|
2020-09-23 21:01:29 +02:00
|
|
|
if(!peerId)
|
|
|
|
container.classList.add('message-system');
|
2022-03-25 16:30:31 +01:00
|
|
|
if(serverConnection && peerId === serverConnection.id)
|
2020-09-23 21:01:29 +02:00
|
|
|
container.classList.add('message-sender');
|
2020-10-01 16:52:01 +02:00
|
|
|
if(dest)
|
|
|
|
container.classList.add('message-private');
|
|
|
|
|
2020-08-12 12:17:56 +02:00
|
|
|
if(kind !== 'me') {
|
2022-04-20 17:38:55 +02:00
|
|
|
let p = formatLines(message.toString().split('\n'));
|
2020-12-01 02:51:15 +01:00
|
|
|
let doHeader = true;
|
|
|
|
if(!peerId && !dest && !nick) {
|
|
|
|
doHeader = false;
|
|
|
|
} else if(lastMessage.nick !== (nick || null) ||
|
|
|
|
lastMessage.peerId !== peerId ||
|
|
|
|
lastMessage.dest !== (dest || null) ||
|
|
|
|
!time || !lastMessage.time) {
|
|
|
|
doHeader = true;
|
|
|
|
} else {
|
|
|
|
let delta = time - lastMessage.time;
|
|
|
|
doHeader = delta < 0 || delta > 60000;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(doHeader) {
|
2020-09-30 00:33:23 +02:00
|
|
|
let header = document.createElement('p');
|
2020-12-01 02:51:15 +01:00
|
|
|
if(peerId || nick || dest) {
|
|
|
|
let user = document.createElement('span');
|
2021-04-27 18:58:21 +02:00
|
|
|
let u = serverConnection.users[dest];
|
|
|
|
let name = (u && u.username);
|
2020-12-01 02:51:15 +01:00
|
|
|
user.textContent = dest ?
|
2021-04-27 18:58:21 +02:00
|
|
|
`${nick||'(anon)'} \u2192 ${name || '(anon)'}` :
|
2020-12-01 02:51:15 +01:00
|
|
|
(nick || '(anon)');
|
|
|
|
user.classList.add('message-user');
|
|
|
|
header.appendChild(user);
|
|
|
|
}
|
2020-09-30 00:33:23 +02:00
|
|
|
header.classList.add('message-header');
|
|
|
|
container.appendChild(header);
|
|
|
|
if(time) {
|
|
|
|
let tm = document.createElement('span');
|
|
|
|
tm.textContent = formatTime(time);
|
|
|
|
tm.classList.add('message-time');
|
|
|
|
header.appendChild(tm);
|
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
2020-12-01 02:51:15 +01:00
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
p.classList.add('message-content');
|
|
|
|
container.appendChild(p);
|
2020-10-08 15:18:22 +02:00
|
|
|
lastMessage.nick = (nick || null);
|
2020-04-24 19:38:21 +02:00
|
|
|
lastMessage.peerId = peerId;
|
2020-10-01 16:52:01 +02:00
|
|
|
lastMessage.dest = (dest || null);
|
2020-12-01 02:51:15 +01:00
|
|
|
lastMessage.time = (time || null);
|
2020-04-24 19:38:21 +02:00
|
|
|
} else {
|
|
|
|
let asterisk = document.createElement('span');
|
|
|
|
asterisk.textContent = '*';
|
|
|
|
asterisk.classList.add('message-me-asterisk');
|
|
|
|
let user = document.createElement('span');
|
2020-10-08 15:18:22 +02:00
|
|
|
user.textContent = nick || '(anon)';
|
2020-04-24 19:38:21 +02:00
|
|
|
user.classList.add('message-me-user');
|
|
|
|
let content = document.createElement('span');
|
2020-12-28 01:42:26 +01:00
|
|
|
formatLine(message.toString()).forEach(elt => {
|
2020-04-24 19:38:21 +02:00
|
|
|
content.appendChild(elt);
|
|
|
|
});
|
|
|
|
content.classList.add('message-me-content');
|
|
|
|
container.appendChild(asterisk);
|
|
|
|
container.appendChild(user);
|
|
|
|
container.appendChild(content);
|
|
|
|
container.classList.add('message-me');
|
2020-10-01 16:52:01 +02:00
|
|
|
lastMessage = {};
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
2020-12-28 01:47:23 +01:00
|
|
|
container.appendChild(footer);
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2020-05-11 02:58:48 +02:00
|
|
|
let box = document.getElementById('box');
|
2020-08-27 21:17:46 +02:00
|
|
|
box.appendChild(row);
|
2020-04-24 19:38:21 +02:00
|
|
|
if(box.scrollHeight > box.clientHeight) {
|
|
|
|
box.scrollTop = box.scrollHeight - box.clientHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
2021-01-11 19:28:57 +01:00
|
|
|
/**
|
|
|
|
* @param {string} message
|
|
|
|
*/
|
|
|
|
function localMessage(message) {
|
2021-08-07 15:39:00 +02:00
|
|
|
return addToChatbox(null, null, null, Date.now(), false, false, '', message);
|
2021-01-11 19:28:57 +01:00
|
|
|
}
|
|
|
|
|
2020-08-11 17:09:31 +02:00
|
|
|
function clearChat() {
|
2020-04-25 21:16:49 +02:00
|
|
|
lastMessage = {};
|
|
|
|
document.getElementById('box').textContent = '';
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:55:52 +01:00
|
|
|
/**
|
|
|
|
* A command known to the command-line parser.
|
|
|
|
*
|
|
|
|
* @typedef {Object} command
|
|
|
|
* @property {string} [parameters]
|
|
|
|
* - A user-readable list of parameters.
|
|
|
|
* @property {string} [description]
|
|
|
|
* - A user-readable description, null if undocumented.
|
|
|
|
* @property {() => string} [predicate]
|
|
|
|
* - Returns null if the command is available.
|
|
|
|
* @property {(c: string, r: string) => void} f
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The set of commands known to the command-line parser.
|
|
|
|
*
|
|
|
|
* @type {Object.<string,command>}
|
|
|
|
*/
|
2020-12-26 17:28:44 +01:00
|
|
|
let commands = {};
|
2020-11-24 14:55:52 +01:00
|
|
|
|
|
|
|
function operatorPredicate() {
|
2020-12-01 03:01:19 +01:00
|
|
|
if(serverConnection && serverConnection.permissions &&
|
2022-02-19 23:43:44 +01:00
|
|
|
serverConnection.permissions.indexOf('op') >= 0)
|
2020-11-24 14:55:52 +01:00
|
|
|
return null;
|
|
|
|
return 'You are not an operator';
|
|
|
|
}
|
|
|
|
|
|
|
|
function recordingPredicate() {
|
2020-12-01 03:01:19 +01:00
|
|
|
if(serverConnection && serverConnection.permissions &&
|
2022-02-19 23:43:44 +01:00
|
|
|
serverConnection.permissions.indexOf('record') >= 0)
|
2020-11-24 14:55:52 +01:00
|
|
|
return null;
|
|
|
|
return 'You are not allowed to record';
|
|
|
|
}
|
|
|
|
|
|
|
|
commands.help = {
|
|
|
|
description: 'display this help',
|
|
|
|
f: (c, r) => {
|
|
|
|
/** @type {string[]} */
|
|
|
|
let cs = [];
|
|
|
|
for(let cmd in commands) {
|
|
|
|
let c = commands[cmd];
|
|
|
|
if(!c.description)
|
|
|
|
continue;
|
|
|
|
if(c.predicate && c.predicate())
|
|
|
|
continue;
|
|
|
|
cs.push(`/${cmd}${c.parameters?' ' + c.parameters:''}: ${c.description}`);
|
|
|
|
}
|
2021-08-07 15:42:16 +02:00
|
|
|
localMessage(cs.sort().join('\n'));
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.me = {
|
|
|
|
f: (c, r) => {
|
|
|
|
// handled as a special case
|
|
|
|
throw new Error("this shouldn't happen");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.set = {
|
|
|
|
f: (c, r) => {
|
|
|
|
if(!r) {
|
|
|
|
let settings = getSettings();
|
|
|
|
let s = "";
|
|
|
|
for(let key in settings)
|
|
|
|
s = s + `${key}: ${JSON.stringify(settings[key])}\n`;
|
2021-01-11 19:28:57 +01:00
|
|
|
localMessage(s);
|
2020-11-24 14:55:52 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
let p = parseCommand(r);
|
|
|
|
let value;
|
|
|
|
if(p[1]) {
|
2020-12-26 17:28:44 +01:00
|
|
|
value = JSON.parse(p[1]);
|
2020-11-24 14:55:52 +01:00
|
|
|
} else {
|
|
|
|
value = true;
|
|
|
|
}
|
|
|
|
updateSetting(p[0], value);
|
|
|
|
reflectSettings();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.unset = {
|
|
|
|
f: (c, r) => {
|
|
|
|
delSetting(r.trim());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.leave = {
|
|
|
|
description: "leave group",
|
|
|
|
f: (c, r) => {
|
2020-12-01 03:01:19 +01:00
|
|
|
if(!serverConnection)
|
|
|
|
throw new Error('Not connected');
|
2020-11-24 14:55:52 +01:00
|
|
|
serverConnection.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.clear = {
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
description: 'clear the chat history',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('clearchat');
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.lock = {
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
description: 'lock this group',
|
|
|
|
parameters: '[message]',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('lock', r);
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.unlock = {
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
description: 'unlock this group, revert the effect of /lock',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('unlock');
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.record = {
|
|
|
|
predicate: recordingPredicate,
|
|
|
|
description: 'start recording',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('record');
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.unrecord = {
|
|
|
|
predicate: recordingPredicate,
|
|
|
|
description: 'stop recording',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('unrecord');
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-12-02 19:47:32 +01:00
|
|
|
commands.subgroups = {
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
description: 'list subgroups',
|
|
|
|
f: (c, r) => {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.groupAction('subgroups');
|
2020-12-02 19:47:32 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-02-16 20:41:58 +01:00
|
|
|
function renegotiateStreams() {
|
|
|
|
for(let id in serverConnection.up)
|
|
|
|
serverConnection.up[id].restartIce();
|
|
|
|
for(let id in serverConnection.down)
|
|
|
|
serverConnection.down[id].restartIce();
|
|
|
|
}
|
|
|
|
|
2020-12-05 21:02:28 +01:00
|
|
|
commands.renegotiate = {
|
|
|
|
description: 'renegotiate media streams',
|
|
|
|
f: (c, r) => {
|
2022-02-16 20:41:58 +01:00
|
|
|
renegotiateStreams();
|
2020-12-05 21:02:28 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-05-06 23:39:27 +02:00
|
|
|
commands.replace = {
|
|
|
|
f: (c, r) => {
|
|
|
|
replaceUpStreams(null);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2020-09-16 01:42:39 +02:00
|
|
|
/**
|
|
|
|
* parseCommand splits a string into two space-separated parts. The first
|
|
|
|
* part may be quoted and may include backslash escapes.
|
|
|
|
*
|
|
|
|
* @param {string} line
|
2020-09-20 14:33:13 +02:00
|
|
|
* @returns {string[]}
|
2020-09-16 01:42:39 +02:00
|
|
|
*/
|
|
|
|
function parseCommand(line) {
|
|
|
|
let i = 0;
|
|
|
|
while(i < line.length && line[i] === ' ')
|
|
|
|
i++;
|
|
|
|
let start = ' ';
|
|
|
|
if(i < line.length && line[i] === '"' || line[i] === "'") {
|
|
|
|
start = line[i];
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
let first = "";
|
|
|
|
while(i < line.length) {
|
|
|
|
if(line[i] === start) {
|
|
|
|
if(start !== ' ')
|
|
|
|
i++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if(line[i] === '\\' && i < line.length - 1)
|
|
|
|
i++;
|
|
|
|
first = first + line[i];
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
while(i < line.length && line[i] === ' ')
|
|
|
|
i++;
|
|
|
|
return [first, line.slice(i)];
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:55:52 +01:00
|
|
|
/**
|
|
|
|
* @param {string} user
|
|
|
|
*/
|
|
|
|
function findUserId(user) {
|
2021-04-27 18:58:21 +02:00
|
|
|
if(user in serverConnection.users)
|
2020-11-24 14:55:52 +01:00
|
|
|
return user;
|
|
|
|
|
2021-04-27 18:58:21 +02:00
|
|
|
for(let id in serverConnection.users) {
|
|
|
|
let u = serverConnection.users[id];
|
|
|
|
if(u && u.username === user)
|
2020-11-24 14:55:52 +01:00
|
|
|
return id;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
commands.msg = {
|
|
|
|
parameters: 'user message',
|
|
|
|
description: 'send a private message',
|
|
|
|
f: (c, r) => {
|
|
|
|
let p = parseCommand(r);
|
|
|
|
if(!p[0])
|
|
|
|
throw new Error('/msg requires parameters');
|
|
|
|
let id = findUserId(p[0]);
|
|
|
|
if(!id)
|
|
|
|
throw new Error(`Unknown user ${p[0]}`);
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.chat('', id, p[1]);
|
|
|
|
addToChatbox(serverConnection.id, id, serverConnection.username,
|
2021-07-31 14:42:26 +02:00
|
|
|
Date.now(), false, false, '', p[1]);
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
@param {string} c
|
|
|
|
@param {string} r
|
|
|
|
*/
|
|
|
|
function userCommand(c, r) {
|
|
|
|
let p = parseCommand(r);
|
|
|
|
if(!p[0])
|
|
|
|
throw new Error(`/${c} requires parameters`);
|
|
|
|
let id = findUserId(p[0]);
|
|
|
|
if(!id)
|
|
|
|
throw new Error(`Unknown user ${p[0]}`);
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.userAction(c, id, p[1]);
|
2020-11-24 14:55:52 +01:00
|
|
|
}
|
|
|
|
|
2020-11-30 14:22:36 +01:00
|
|
|
function userMessage(c, r) {
|
|
|
|
let p = parseCommand(r);
|
|
|
|
if(!p[0])
|
|
|
|
throw new Error(`/${c} requires parameters`);
|
|
|
|
let id = findUserId(p[0]);
|
|
|
|
if(!id)
|
|
|
|
throw new Error(`Unknown user ${p[0]}`);
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.userMessage(c, id, p[1]);
|
2020-11-30 14:22:36 +01:00
|
|
|
}
|
2020-11-24 14:55:52 +01:00
|
|
|
|
|
|
|
commands.kick = {
|
|
|
|
parameters: 'user [message]',
|
|
|
|
description: 'kick out a user',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userCommand,
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.op = {
|
|
|
|
parameters: 'user',
|
|
|
|
description: 'give operator status',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userCommand,
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.unop = {
|
|
|
|
parameters: 'user',
|
|
|
|
description: 'revoke operator status',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userCommand,
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.present = {
|
|
|
|
parameters: 'user',
|
|
|
|
description: 'give user the right to present',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userCommand,
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.unpresent = {
|
|
|
|
parameters: 'user',
|
|
|
|
description: 'revoke the right to present',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userCommand,
|
|
|
|
};
|
|
|
|
|
2020-11-30 14:22:36 +01:00
|
|
|
commands.mute = {
|
|
|
|
parameters: 'user',
|
|
|
|
description: 'mute a remote user',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: userMessage,
|
|
|
|
};
|
|
|
|
|
2021-01-11 16:30:19 +01:00
|
|
|
commands.muteall = {
|
|
|
|
description: 'mute all remote users',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: (c, r) => {
|
|
|
|
serverConnection.userMessage('mute', null, null, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-30 15:48:23 +01:00
|
|
|
commands.warn = {
|
|
|
|
parameters: 'user message',
|
|
|
|
description: 'send a warning to a user',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: (c, r) => {
|
|
|
|
userMessage('warning', r);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
commands.wall = {
|
|
|
|
parameters: 'message',
|
|
|
|
description: 'send a warning to all users',
|
|
|
|
predicate: operatorPredicate,
|
|
|
|
f: (c, r) => {
|
|
|
|
if(!r)
|
|
|
|
throw new Error('empty message');
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.userMessage('warning', '', r);
|
2020-11-30 15:48:23 +01:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-04-28 14:45:45 +02:00
|
|
|
commands.raise = {
|
|
|
|
description: 'raise hand',
|
|
|
|
f: (c, r) => {
|
|
|
|
serverConnection.userAction(
|
2022-01-29 22:54:44 +01:00
|
|
|
"setdata", serverConnection.id, {"raisehand": true},
|
2021-04-28 14:45:45 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
commands.unraise = {
|
|
|
|
description: 'unraise hand',
|
|
|
|
f: (c, r) => {
|
|
|
|
serverConnection.userAction(
|
2022-01-29 22:54:44 +01:00
|
|
|
"setdata", serverConnection.id, {"raisehand": null},
|
2021-04-28 14:45:45 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-21 18:36:36 +01:00
|
|
|
/** @returns {boolean} */
|
|
|
|
function canFile() {
|
|
|
|
let v =
|
|
|
|
/** @ts-ignore */
|
|
|
|
!!HTMLVideoElement.prototype.captureStream ||
|
|
|
|
/** @ts-ignore */
|
|
|
|
!!HTMLVideoElement.prototype.mozCaptureStream;
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
2022-02-21 17:32:32 +01:00
|
|
|
function presentFile() {
|
|
|
|
let input = document.createElement('input');
|
|
|
|
input.type = 'file';
|
|
|
|
input.accept="audio/*,video/*";
|
|
|
|
input.onchange = function(e) {
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
|
|
|
let files = this.files;
|
|
|
|
for(let i = 0; i < files.length; i++) {
|
|
|
|
addFileMedia(files[i]).catch(e => {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
input.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
commands.presentfile = {
|
|
|
|
description: 'broadcast a video or audio file',
|
|
|
|
f: (c, r) => {
|
|
|
|
presentFile();
|
|
|
|
},
|
|
|
|
predicate: () => {
|
2022-02-21 18:36:36 +01:00
|
|
|
if(!canFile())
|
|
|
|
return 'Your browser does not support presenting arbitrary files';
|
2022-02-21 17:32:32 +01:00
|
|
|
if(!serverConnection || !serverConnection.permissions ||
|
|
|
|
serverConnection.permissions.indexOf('present') < 0)
|
2022-02-21 18:36:36 +01:00
|
|
|
return 'You are not authorised to present.';
|
2022-02-21 17:32:32 +01:00
|
|
|
return null;
|
2022-02-21 18:36:36 +01:00
|
|
|
}
|
2022-02-21 17:32:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2022-02-16 20:41:58 +01:00
|
|
|
/**
|
|
|
|
* @param {string} id
|
|
|
|
*/
|
|
|
|
function sendFile(id) {
|
|
|
|
let input = document.createElement('input');
|
|
|
|
input.type = 'file';
|
|
|
|
input.onchange = function(e) {
|
|
|
|
if(!(this instanceof HTMLInputElement))
|
|
|
|
throw new Error('Unexpected type for this');
|
2022-02-21 17:32:32 +01:00
|
|
|
let files = this.files;
|
2022-02-16 20:41:58 +01:00
|
|
|
for(let i = 0; i < files.length; i++) {
|
|
|
|
try {
|
|
|
|
offerFile(id, files[i]);
|
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
input.click();
|
|
|
|
}
|
|
|
|
|
2022-01-30 06:32:55 +01:00
|
|
|
commands.sendfile = {
|
|
|
|
parameters: 'user',
|
2022-01-30 19:22:43 +01:00
|
|
|
description: 'send a file (this will disclose your IP address)',
|
2022-01-30 06:32:55 +01:00
|
|
|
f: (c, r) => {
|
|
|
|
let p = parseCommand(r);
|
|
|
|
if(!p[0])
|
|
|
|
throw new Error(`/${c} requires parameters`);
|
|
|
|
let id = findUserId(p[0]);
|
|
|
|
if(!id)
|
|
|
|
throw new Error(`Unknown user ${p[0]}`);
|
2022-02-16 20:41:58 +01:00
|
|
|
sendFile(id);
|
|
|
|
},
|
2022-01-30 06:32:55 +01:00
|
|
|
};
|
|
|
|
|
2021-01-11 18:45:20 +01:00
|
|
|
/**
|
|
|
|
* Test loopback through a TURN relay.
|
|
|
|
*
|
|
|
|
* @returns {Promise<number>}
|
|
|
|
*/
|
2021-01-11 18:18:55 +01:00
|
|
|
async function relayTest() {
|
2021-01-11 18:45:20 +01:00
|
|
|
if(!serverConnection)
|
|
|
|
throw new Error('not connected');
|
2022-03-23 00:12:51 +01:00
|
|
|
let conf = Object.assign({}, serverConnection.getRTCConfiguration());
|
2021-01-11 18:18:55 +01:00
|
|
|
conf.iceTransportPolicy = 'relay';
|
|
|
|
let pc1 = new RTCPeerConnection(conf);
|
|
|
|
let pc2 = new RTCPeerConnection(conf);
|
|
|
|
pc1.onicecandidate = e => {e.candidate && pc2.addIceCandidate(e.candidate);};
|
|
|
|
pc2.onicecandidate = e => {e.candidate && pc1.addIceCandidate(e.candidate);};
|
|
|
|
try {
|
2021-01-11 18:45:20 +01:00
|
|
|
return await new Promise(async (resolve, reject) => {
|
2021-01-11 18:18:55 +01:00
|
|
|
let d1 = pc1.createDataChannel('loopbackTest');
|
|
|
|
d1.onopen = e => {
|
2021-01-11 18:45:20 +01:00
|
|
|
d1.send(Date.now().toString());
|
2021-01-11 18:18:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
await pc1.setLocalDescription(offer);
|
2021-01-11 20:27:39 +01:00
|
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
2021-01-11 18:18:55 +01:00
|
|
|
let answer = await pc2.createAnswer();
|
2021-01-11 20:27:39 +01:00
|
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
2021-01-11 18:18:55 +01:00
|
|
|
|
|
|
|
pc2.ondatachannel = e => {
|
|
|
|
let d2 = e.channel;
|
|
|
|
d2.onmessage = e => {
|
2021-01-11 18:45:20 +01:00
|
|
|
let t = parseInt(e.data);
|
|
|
|
if(isNaN(t))
|
|
|
|
reject(new Error('corrupt data'));
|
2021-01-11 18:18:55 +01:00
|
|
|
else
|
2021-01-11 18:45:20 +01:00
|
|
|
resolve(Date.now() - t);
|
2021-01-11 18:18:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-11 18:45:20 +01:00
|
|
|
setTimeout(() => reject(new Error('timeout')), 5000);
|
2021-01-11 18:18:55 +01:00
|
|
|
})
|
|
|
|
} finally {
|
|
|
|
pc1.close();
|
|
|
|
pc2.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
commands['relay-test'] = {
|
|
|
|
f: async (c, r) => {
|
2021-01-11 19:28:57 +01:00
|
|
|
localMessage('Relay test in progress...');
|
2021-01-11 18:18:55 +01:00
|
|
|
try {
|
2021-01-11 18:45:20 +01:00
|
|
|
let s = Date.now();
|
|
|
|
let rtt = await relayTest();
|
|
|
|
let e = Date.now();
|
2021-01-11 19:28:57 +01:00
|
|
|
localMessage(`Relay test successful in ${e-s}ms, RTT ${rtt}ms`);
|
2021-01-11 18:18:55 +01:00
|
|
|
} catch(e) {
|
2021-01-11 19:28:57 +01:00
|
|
|
localMessage(`Relay test failed: ${e}`);
|
2021-01-11 18:18:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
function handleInput() {
|
2020-09-20 14:33:13 +02:00
|
|
|
let input = /** @type {HTMLTextAreaElement} */
|
|
|
|
(document.getElementById('input'));
|
2020-04-24 19:38:21 +02:00
|
|
|
let data = input.value;
|
|
|
|
input.value = '';
|
|
|
|
|
|
|
|
let message, me;
|
|
|
|
|
|
|
|
if(data === '')
|
|
|
|
return;
|
|
|
|
|
2020-09-16 01:42:39 +02:00
|
|
|
if(data[0] === '/') {
|
|
|
|
if(data.length > 1 && data[1] === '/') {
|
2020-11-24 14:55:52 +01:00
|
|
|
message = data.slice(1);
|
2020-04-24 19:38:21 +02:00
|
|
|
me = false;
|
|
|
|
} else {
|
2020-09-12 12:26:07 +02:00
|
|
|
let cmd, rest;
|
|
|
|
let space = data.indexOf(' ');
|
2020-04-24 19:38:21 +02:00
|
|
|
if(space < 0) {
|
2020-11-24 14:55:52 +01:00
|
|
|
cmd = data.slice(1);
|
2020-04-24 19:38:21 +02:00
|
|
|
rest = '';
|
|
|
|
} else {
|
2020-11-24 14:55:52 +01:00
|
|
|
cmd = data.slice(1, space);
|
2020-09-12 12:26:07 +02:00
|
|
|
rest = data.slice(space + 1);
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-11-24 14:55:52 +01:00
|
|
|
if(cmd === 'me') {
|
2020-04-24 19:38:21 +02:00
|
|
|
message = rest;
|
|
|
|
me = true;
|
2020-11-24 14:55:52 +01:00
|
|
|
} else {
|
|
|
|
let c = commands[cmd];
|
|
|
|
if(!c) {
|
|
|
|
displayError(`Uknown command /${cmd}, type /help for help`);
|
2020-09-23 21:01:29 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-11-24 14:55:52 +01:00
|
|
|
if(c.predicate) {
|
|
|
|
let s = c.predicate();
|
|
|
|
if(s) {
|
|
|
|
displayError(s);
|
2020-09-23 21:01:29 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-04-25 17:36:35 +02:00
|
|
|
}
|
2020-11-24 14:55:52 +01:00
|
|
|
try {
|
|
|
|
c.f(cmd, rest);
|
|
|
|
} catch(e) {
|
2021-08-07 15:39:00 +02:00
|
|
|
console.error(e);
|
2020-11-24 14:55:52 +01:00
|
|
|
displayError(e);
|
2020-10-01 16:52:01 +02:00
|
|
|
}
|
2020-04-25 17:36:35 +02:00
|
|
|
return;
|
2020-05-10 21:18:12 +02:00
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
message = data;
|
|
|
|
me = false;
|
|
|
|
}
|
|
|
|
|
2020-09-23 21:22:55 +02:00
|
|
|
if(!serverConnection || !serverConnection.socket) {
|
|
|
|
displayError("Not connected.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-09 01:46:17 +02:00
|
|
|
try {
|
2021-01-03 12:04:39 +01:00
|
|
|
serverConnection.chat(me ? 'me' : '', '', message);
|
2020-06-09 01:46:17 +02:00
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e);
|
|
|
|
}
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
document.getElementById('inputform').onsubmit = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
handleInput();
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('input').onkeypress = function(e) {
|
|
|
|
if(e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
|
|
|
|
e.preventDefault();
|
|
|
|
handleInput();
|
|
|
|
}
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-04-24 19:38:21 +02:00
|
|
|
|
|
|
|
function chatResizer(e) {
|
|
|
|
e.preventDefault();
|
2020-09-03 17:01:21 +02:00
|
|
|
let full_width = document.getElementById("mainrow").offsetWidth;
|
|
|
|
let left = document.getElementById("left");
|
|
|
|
let right = document.getElementById("right");
|
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
let start_x = e.clientX;
|
2020-09-11 22:46:23 +02:00
|
|
|
let start_width = left.offsetWidth;
|
2020-09-03 17:01:21 +02:00
|
|
|
|
2020-04-24 19:38:21 +02:00
|
|
|
function start_drag(e) {
|
2020-09-03 17:01:21 +02:00
|
|
|
let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
|
2020-10-07 09:33:38 +02:00
|
|
|
// set min chat width to 300px
|
|
|
|
let min_left_width = 300 * 100 / full_width;
|
2020-10-05 11:15:31 +02:00
|
|
|
if (left_width < min_left_width) {
|
|
|
|
return;
|
|
|
|
}
|
2020-09-12 16:34:52 +02:00
|
|
|
left.style.flex = left_width.toString();
|
|
|
|
right.style.flex = (100 - left_width).toString();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
function stop_drag(e) {
|
|
|
|
document.documentElement.removeEventListener(
|
|
|
|
'mousemove', start_drag, false,
|
|
|
|
);
|
|
|
|
document.documentElement.removeEventListener(
|
|
|
|
'mouseup', stop_drag, false,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
document.documentElement.addEventListener(
|
|
|
|
'mousemove', start_drag, false,
|
|
|
|
);
|
|
|
|
document.documentElement.addEventListener(
|
|
|
|
'mouseup', stop_drag, false,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
document.getElementById('resizer').addEventListener('mousedown', chatResizer, false);
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
2020-09-28 20:58:07 +02:00
|
|
|
* @param {unknown} message
|
|
|
|
* @param {string} [level]
|
2020-09-20 14:33:13 +02:00
|
|
|
*/
|
|
|
|
function displayError(message, level) {
|
|
|
|
if(!level)
|
2020-09-28 20:58:07 +02:00
|
|
|
level = "error";
|
2020-09-20 14:33:13 +02:00
|
|
|
var background = 'linear-gradient(to right, #e20a0a, #df2d2d)';
|
|
|
|
var position = 'center';
|
|
|
|
var gravity = 'top';
|
|
|
|
|
|
|
|
switch(level) {
|
2020-09-28 20:58:07 +02:00
|
|
|
case "info":
|
2020-09-20 14:33:13 +02:00
|
|
|
background = 'linear-gradient(to right, #529518, #96c93d)';
|
|
|
|
position = 'right';
|
|
|
|
gravity = 'bottom';
|
|
|
|
break;
|
2020-09-28 20:58:07 +02:00
|
|
|
case "warning":
|
2020-12-08 17:50:29 +01:00
|
|
|
background = "linear-gradient(to right, #bdc511, #c2cf01)";
|
2020-09-20 14:33:13 +02:00
|
|
|
break;
|
2021-07-30 19:38:02 +02:00
|
|
|
case "kicked":
|
|
|
|
level = "error";
|
|
|
|
break;
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
2020-09-20 14:33:13 +02:00
|
|
|
|
|
|
|
/** @ts-ignore */
|
2020-09-01 10:23:35 +02:00
|
|
|
Toastify({
|
2020-09-20 14:33:13 +02:00
|
|
|
text: message,
|
|
|
|
duration: 4000,
|
|
|
|
close: true,
|
|
|
|
position: position,
|
|
|
|
gravity: gravity,
|
2022-02-16 18:56:21 +01:00
|
|
|
style: {
|
|
|
|
background: background,
|
|
|
|
},
|
2020-09-20 14:33:13 +02:00
|
|
|
className: level,
|
2020-09-01 10:23:35 +02:00
|
|
|
}).showToast();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
2020-09-28 20:58:07 +02:00
|
|
|
* @param {unknown} message
|
2020-09-20 14:33:13 +02:00
|
|
|
*/
|
2020-04-24 19:38:21 +02:00
|
|
|
function displayWarning(message) {
|
2020-09-28 20:58:07 +02:00
|
|
|
return displayError(message, "warning");
|
2020-09-03 17:01:21 +02:00
|
|
|
}
|
|
|
|
|
2020-09-20 14:33:13 +02:00
|
|
|
/**
|
2020-09-28 20:58:07 +02:00
|
|
|
* @param {unknown} message
|
2020-09-20 14:33:13 +02:00
|
|
|
*/
|
2020-09-03 17:01:21 +02:00
|
|
|
function displayMessage(message) {
|
2020-09-28 20:58:07 +02:00
|
|
|
return displayError(message, "info");
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
2020-11-24 16:29:19 +01:00
|
|
|
let connecting = false;
|
|
|
|
|
|
|
|
document.getElementById('userform').onsubmit = async function(e) {
|
2020-04-24 19:38:21 +02:00
|
|
|
e.preventDefault();
|
2020-11-24 16:29:19 +01:00
|
|
|
if(connecting)
|
|
|
|
return;
|
|
|
|
connecting = true;
|
|
|
|
try {
|
2021-11-01 00:15:20 +01:00
|
|
|
await serverConnect();
|
2020-11-24 16:29:19 +01:00
|
|
|
} finally {
|
|
|
|
connecting = false;
|
|
|
|
}
|
2020-11-24 17:08:33 +01:00
|
|
|
|
2020-12-01 00:26:14 +01:00
|
|
|
if(getInputElement('presentboth').checked)
|
|
|
|
presentRequested = 'both';
|
|
|
|
else if(getInputElement('presentmike').checked)
|
|
|
|
presentRequested = 'mike';
|
|
|
|
else
|
|
|
|
presentRequested = null;
|
2020-11-30 21:28:45 +01:00
|
|
|
|
|
|
|
getInputElement('presentoff').checked = true;
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-04-24 19:38:21 +02:00
|
|
|
|
|
|
|
document.getElementById('disconnectbutton').onclick = function(e) {
|
2020-08-11 17:09:31 +02:00
|
|
|
serverConnection.close();
|
2020-11-24 17:36:52 +01:00
|
|
|
closeNav();
|
2020-05-10 21:18:07 +02:00
|
|
|
};
|
2020-04-24 19:38:21 +02:00
|
|
|
|
2020-08-27 21:17:46 +02:00
|
|
|
function openNav() {
|
|
|
|
document.getElementById("sidebarnav").style.width = "250px";
|
|
|
|
}
|
|
|
|
|
|
|
|
function closeNav() {
|
|
|
|
document.getElementById("sidebarnav").style.width = "0";
|
|
|
|
}
|
|
|
|
|
|
|
|
document.getElementById('sidebarCollapse').onclick = function(e) {
|
|
|
|
document.getElementById("left-sidebar").classList.toggle("active");
|
|
|
|
document.getElementById("mainrow").classList.toggle("full-width-active");
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('openside').onclick = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
let sidewidth = document.getElementById("sidebarnav").style.width;
|
|
|
|
if (sidewidth !== "0px" && sidewidth !== "") {
|
|
|
|
closeNav();
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
openNav();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('clodeside').onclick = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
closeNav();
|
|
|
|
};
|
|
|
|
|
2020-09-09 20:26:19 +02:00
|
|
|
document.getElementById('collapse-video').onclick = function(e) {
|
2020-09-01 10:23:35 +02:00
|
|
|
e.preventDefault();
|
2021-03-26 20:18:20 +01:00
|
|
|
setVisibility('collapse-video', false);
|
|
|
|
setVisibility('show-video', true);
|
|
|
|
hideVideo(true);
|
2020-09-01 10:23:35 +02:00
|
|
|
};
|
|
|
|
|
2021-03-26 20:18:20 +01:00
|
|
|
document.getElementById('show-video').onclick = function(e) {
|
2020-09-01 10:23:35 +02:00
|
|
|
e.preventDefault();
|
2021-03-26 20:18:20 +01:00
|
|
|
setVisibility('video-container', true);
|
|
|
|
setVisibility('collapse-video', true);
|
|
|
|
setVisibility('show-video', false);
|
2020-09-09 20:26:19 +02:00
|
|
|
};
|
|
|
|
|
2020-10-07 09:33:38 +02:00
|
|
|
document.getElementById('close-chat').onclick = function(e) {
|
2021-03-26 20:18:20 +01:00
|
|
|
e.preventDefault();
|
|
|
|
setVisibility('left', false);
|
|
|
|
setVisibility('show-chat', true);
|
|
|
|
resizePeers();
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('show-chat').onclick = function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
setVisibility('left', true);
|
|
|
|
setVisibility('show-chat', false);
|
|
|
|
resizePeers();
|
2020-10-07 09:33:38 +02:00
|
|
|
};
|
|
|
|
|
2020-09-18 11:28:13 +02:00
|
|
|
async function serverConnect() {
|
2020-11-24 16:29:19 +01:00
|
|
|
if(serverConnection && serverConnection.socket)
|
|
|
|
serverConnection.close();
|
2020-08-11 17:09:31 +02:00
|
|
|
serverConnection = new ServerConnection();
|
|
|
|
serverConnection.onconnected = gotConnected;
|
2022-03-22 18:22:18 +01:00
|
|
|
serverConnection.onpeerconnection = onPeerConnection;
|
2020-08-11 17:09:31 +02:00
|
|
|
serverConnection.onclose = gotClose;
|
|
|
|
serverConnection.ondownstream = gotDownStream;
|
|
|
|
serverConnection.onuser = gotUser;
|
2020-12-01 22:42:06 +01:00
|
|
|
serverConnection.onjoined = gotJoined;
|
2020-08-11 17:09:31 +02:00
|
|
|
serverConnection.onchat = addToChatbox;
|
2020-12-28 01:42:26 +01:00
|
|
|
serverConnection.onusermessage = gotUserMessage;
|
|
|
|
|
2020-09-18 11:28:13 +02:00
|
|
|
let url = `ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`;
|
|
|
|
try {
|
|
|
|
await serverConnection.connect(url);
|
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
displayError(e.message ? e.message : "Couldn't connect to " + url);
|
|
|
|
}
|
2020-08-11 17:09:31 +02:00
|
|
|
}
|
|
|
|
|
2021-10-26 22:22:48 +02:00
|
|
|
async function start() {
|
2021-10-26 19:36:05 +02:00
|
|
|
group = decodeURIComponent(
|
|
|
|
location.pathname.replace(/^\/[a-z]*\//, '').replace(/\/$/, '')
|
|
|
|
);
|
2022-02-18 20:27:53 +01:00
|
|
|
|
2021-10-26 22:22:48 +02:00
|
|
|
/** @type {Object} */
|
|
|
|
try {
|
|
|
|
let r = await fetch(".status.json")
|
|
|
|
if(!r.ok)
|
|
|
|
throw new Error(`${r.status} ${r.statusText}`);
|
|
|
|
groupStatus = await r.json()
|
|
|
|
} catch(e) {
|
|
|
|
console.error(e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-18 20:27:53 +01:00
|
|
|
let parms = new URLSearchParams(window.location.search);
|
|
|
|
if(window.location.search)
|
|
|
|
window.history.replaceState(null, '', window.location.pathname);
|
2021-10-26 22:22:48 +02:00
|
|
|
setTitle(groupStatus.displayName || capitalise(group));
|
2022-02-18 20:27:53 +01:00
|
|
|
|
2021-01-13 13:57:05 +01:00
|
|
|
addFilters();
|
2020-09-18 17:57:37 +02:00
|
|
|
setMediaChoices(false).then(e => reflectSettings());
|
2020-06-09 18:05:16 +02:00
|
|
|
|
2022-02-18 20:27:53 +01:00
|
|
|
if(parms.has('token')) {
|
|
|
|
token = parms.get('token');
|
|
|
|
await serverConnect();
|
2022-02-19 23:58:31 +01:00
|
|
|
} else if(groupStatus.authPortal) {
|
|
|
|
window.location.href = groupStatus.authPortal;
|
2022-02-18 20:27:53 +01:00
|
|
|
} else {
|
|
|
|
let container = document.getElementById("login-container");
|
|
|
|
container.classList.remove('invisible');
|
|
|
|
}
|
2021-05-14 18:04:18 +02:00
|
|
|
setViewportHeight();
|
2020-04-24 19:38:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
start();
|