1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-23 00:55:58 +01:00
galene/static/sfu.js

1338 lines
35 KiB
JavaScript
Raw Normal View History

2020-04-24 19:38:21 +02:00
// Copyright (c) 2020 by Juliusz Chroboczek.
// This is not open source software. Copy it, and I'll break into your
// house and tell your three year-old that Santa doesn't exist.
'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} */
let serverConnection;
2020-04-24 19:38:21 +02:00
2020-09-12 16:34:52 +02:00
/**
* @typedef {Object} userpass
* @property {string} username
* @property {string} password
*/
/* Some browsers disable session storage when cookies are disabled,
we fall back to a global variable. */
2020-09-12 16:34:52 +02:00
/**
* @type {userpass}
*/
let fallbackUserPass = null;
2020-09-12 16:34:52 +02:00
/**
* @param {string} username
* @param {string} password
*/
2020-04-24 19:38:21 +02:00
function setUserPass(username, password) {
let userpass = {username: username, password: password};
try {
window.sessionStorage.setItem('userpass', JSON.stringify(userpass));
fallbackUserPass = null;
} catch(e) {
console.warn("Couldn't store password:", e);
2020-09-12 16:34:52 +02:00
fallbackUserPass = userpass;
}
2020-04-24 19:38:21 +02:00
}
2020-09-12 16:34:52 +02:00
/**
* @returns {userpass}
*/
2020-04-24 19:38:21 +02:00
function getUserPass() {
let userpass;
try {
let json = window.sessionStorage.getItem('userpass');
userpass = JSON.parse(json);
} catch(e) {
console.warn("Couldn't retrieve password:", e);
userpass = fallbackUserPass;
}
return userpass || null;
2020-04-24 19:38:21 +02:00
}
2020-09-12 16:34:52 +02:00
/**
* @returns {string}
*/
2020-04-24 19:38:21 +02:00
function getUsername() {
let userpass = getUserPass();
if(!userpass)
return null;
return userpass.username;
}
function showVideo() {
2020-09-09 20:26:19 +02:00
let width = window.innerWidth;
let video_container = document.getElementById('video-container');
video_container.classList.remove('no-video');
2020-09-09 20:26:19 +02:00
if (width <= 768)
document.getElementById('collapse-video').style.display = "block";
}
2020-09-12 16:34:52 +02:00
/**
* @param {boolean} [force]
*/
function hideVideo(force) {
let mediadiv = document.getElementById('peers');
2020-09-12 16:34:52 +02:00
if(mediadiv.childElementCount > 0 && !force)
return;
let video_container = document.getElementById('video-container');
video_container.classList.add('no-video');
2020-09-09 20:26:19 +02:00
}
function closeVideoControls() {
// hide all video buttons used to switch video on mobile layout
document.getElementById('switch-video').style.display = "";
2020-09-09 20:26:19 +02:00
document.getElementById('collapse-video').style.display = "";
}
2020-09-05 12:59:28 +02:00
/**
* @param{boolean} connected
*/
2020-04-24 19:38:21 +02:00
function setConnected(connected) {
let statspan = document.getElementById('statspan');
2020-09-02 15:35:55 +02:00
let userbox = document.getElementById('user');
let connectionbox = document.getElementById('login-container');
2020-04-24 19:38:21 +02:00
if(connected) {
resetUsers();
clearChat();
2020-04-24 19:38:21 +02:00
statspan.textContent = 'Connected';
statspan.classList.remove('disconnected');
statspan.classList.add('connected');
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();
2020-04-24 19:38:21 +02:00
} else {
2020-09-05 12:59:28 +02:00
resetUsers();
2020-04-24 19:38:21 +02:00
let userpass = getUserPass();
document.getElementById('username').value =
userpass ? userpass.username : '';
document.getElementById('password').value =
userpass ? userpass.password : '';
statspan.textContent = 'Disconnected';
statspan.classList.remove('connected');
statspan.classList.add('disconnected');
2020-09-02 15:35:55 +02:00
userbox.classList.add('invisible');
connectionbox.classList.remove('invisible');
clearUsername();
displayError("Disconnected!", "error");
2020-09-09 20:26:19 +02:00
hideVideo();
closeVideoControls();
}
}
function gotConnected() {
setConnected(true);
let up = getUserPass();
this.login(up.username, up.password);
this.join(group);
this.request(document.getElementById('requestselect').value);
}
2020-09-12 16:34:52 +02:00
/**
* @param {number} code
* @param {string} reason
*/
function gotClose(code, reason) {
delUpMediaKind(null);
setConnected(false);
if(code != 1000) {
console.warn('Socket close', code, reason);
}
}
/**
* @param {Stream} c
*/
function gotDownStream(c) {
c.onclose = function() {
delMedia(c.id);
};
c.onerror = function(e) {
console.error(e);
displayError(e);
}
c.ondowntrack = function(track, transceiver, label, stream) {
setMedia(c, false);
}
c.onlabel = function(label) {
setLabel(c);
}
c.onstatus = function(status) {
setMediaStatus(c);
2020-04-24 19:38:21 +02:00
}
2020-09-11 23:20:46 +02:00
c.onstats = gotDownStats;
if(document.getElementById('activitybox').checked)
2020-09-11 23:20:46 +02:00
c.setStatsInterval(activityDetectionInterval);
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`,
);
2020-09-09 20:26:19 +02:00
};
setViewportHeight();
// On resize and orientation change, we update viewport height
addEventListener('resize', setViewportHeight);
addEventListener('orientationchange', setViewportHeight);
document.getElementById('presentbutton').onclick = function(e) {
2020-04-24 19:38:21 +02:00
e.preventDefault();
addLocalMedia();
2020-05-10 21:18:07 +02:00
};
2020-04-24 19:38:21 +02:00
document.getElementById('unpresentbutton').onclick = function(e) {
e.preventDefault();
delUpMediaKind('local');
2020-09-03 20:54:27 +02:00
resizePeers();
};
function changePresentation() {
let id = findUpMedia('local');
if(id) {
addLocalMedia(id);
}
}
/**
* @param {string} id
* @param {boolean} visible
*/
function setVisibility(id, visible) {
let elt = document.getElementById(id);
if(visible)
elt.classList.remove('invisible');
else
elt.classList.add('invisible');
}
function setButtonsVisibility() {
let permissions = serverConnection.permissions;
let local = !!findUpMedia('local');
let share = !!findUpMedia('screenshare')
// don't allow multiple presentations
setVisibility('presentbutton', permissions.present && !local);
setVisibility('unpresentbutton', local);
// allow multiple shared documents
setVisibility('sharebutton', permissions.present);
setVisibility('unsharebutton', share);
setVisibility('mediaoptions', permissions.present);
}
2020-08-24 22:37:48 +02:00
/** @type {boolean} */
2020-06-09 18:05:16 +02:00
let localMute = false;
function toggleLocalMute() {
setLocalMute(!localMute);
}
2020-09-12 16:34:52 +02:00
/**
* @param {boolean} mute
*/
2020-06-09 18:05:16 +02:00
function setLocalMute(mute) {
localMute = mute;
muteLocalTracks(localMute);
let button = document.getElementById('mutebutton');
2020-09-11 10:39:32 +02:00
let icon = button.querySelector("span .fa");
if(localMute){
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-06-09 18:05:16 +02:00
}
document.getElementById('videoselect').onchange = function(e) {
e.preventDefault();
changePresentation();
2020-05-10 21:18:07 +02:00
};
2020-06-09 18:05:16 +02:00
document.getElementById('audioselect').onchange = function(e) {
e.preventDefault();
changePresentation();
2020-05-10 21:18:07 +02:00
};
2020-06-09 18:05:16 +02:00
document.getElementById('mutebutton').onclick = function(e) {
e.preventDefault();
toggleLocalMute();
}
document.getElementById('sharebutton').onclick = function(e) {
2020-04-24 19:38:21 +02:00
e.preventDefault();
addShareMedia();
2020-05-10 21:18:07 +02:00
};
2020-04-24 19:38:21 +02:00
document.getElementById('unsharebutton').onclick = function(e) {
e.preventDefault();
delUpMediaKind('screenshare');
}
/** @returns {number} */
function getMaxVideoThroughput() {
let v = document.getElementById('sendselect').value;
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;
}
}
document.getElementById('sendselect').onchange = async function(e) {
let t = getMaxVideoThroughput();
for(let id in serverConnection.up) {
let c = serverConnection.up[id];
if(c.kind === 'local')
await setMaxVideoThroughput(c, t);
}
}
document.getElementById('requestselect').onchange = function(e) {
2020-05-09 19:39:34 +02:00
e.preventDefault();
serverConnection.request(this.value);
2020-05-10 21:18:07 +02:00
};
2020-05-09 19:39:34 +02:00
const activityDetectionInterval = 200;
2020-09-11 23:20:46 +02:00
const activityDetectionPeriod = 700;
const activityDetectionThreshold = 0.2;
document.getElementById('activitybox').onchange = function(e) {
for(let id in serverConnection.down) {
let c = serverConnection.down[id];
if(this.checked)
2020-09-11 23:20:46 +02:00
c.setStatsInterval(activityDetectionInterval);
else {
setActive(c, false);
c.setStatsInterval(0);
}
}
}
function gotUpStats(stats) {
let c = this;
2020-04-26 19:21:01 +02:00
let text = '';
c.pc.getSenders().forEach(s => {
let tid = s.track && s.track.id;
let stats = tid && c.stats[tid];
2020-09-11 23:20:46 +02:00
let rate = stats && stats['outbound-rtp'] && stats['outbound-rtp'].rate;
if(typeof rate === 'number') {
if(text)
text = text + ' + ';
2020-09-11 23:20:46 +02:00
text = text + Math.round(rate / 1000) + 'kbps';
}
});
setLabel(c, text);
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) {
let peer = document.getElementById('peer-' + c.id);
if(value)
peer.classList.add('peer-active');
else
peer.classList.remove('peer-active');
}
function gotDownStats(stats) {
if(!document.getElementById('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
/**
* @param {string} value
*/
function mapMediaOption(value) {
switch(value) {
case 'default':
return true;
case 'off':
return false;
default:
return {deviceId: value};
}
}
function addSelectOption(select, label, value) {
if(!value)
value = label;
for(let i = 0; i < select.children.length; i++) {
if(select.children[i].value === value) {
return;
}
}
let option = document.createElement('option');
option.value = value;
option.textContent = label;
select.appendChild(option);
}
// media names might not be available before we call getDisplayMedia. So
// we call this lazily.
2020-08-24 22:37:48 +02:00
/** @type {boolean} */
let mediaChoicesDone = false;
async function setMediaChoices() {
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}`;
addSelectOption(document.getElementById('videoselect'),
label, d.deviceId);
cn++;
} else if(d.kind === 'audioinput') {
if(!label)
label = `Microphone ${mn}`;
addSelectOption(document.getElementById('audioselect'),
label, d.deviceId);
mn++;
}
});
mediaChoicesDone = true;
}
/**
* @param {string} [id]
*/
function newUpStream(id) {
2020-08-13 20:16:52 +02:00
let c = serverConnection.newUpStream(id);
c.onstatus = function(status) {
setMediaStatus(c);
}
c.onerror = function(e) {
console.error(e);
displayError(e);
delUpMedia(c);
}
c.onabort = function() {
delUpMedia(c);
}
c.onnegotiationcompleted = function() {
setMaxVideoThroughput(c, getMaxVideoThroughput())
}
return c;
}
/**
* @param {Stream} c
* @param {number} [bps]
*/
async function setMaxVideoThroughput(c, bps) {
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();
if(!p.encodings)
p.encodings = [{}];
p.encodings.forEach(e => {
if(bps > 0)
e.maxBitrate = bps;
else
delete e.maxBitrate;
});
try {
await s.setParameters(p);
} catch(e) {
console.error(e);
}
}
}
/**
* @param {string} [id]
*/
async function addLocalMedia(id) {
2020-04-24 19:38:21 +02:00
if(!getUserPass())
return;
let audio = mapMediaOption(document.getElementById('audioselect').value);
let video = mapMediaOption(document.getElementById('videoselect').value);
let old = id && serverConnection.up[id];
if(!audio && !video) {
if(old)
delUpMedia(old);
return;
}
if(old)
stopUpMedia(old);
let constraints = {audio: audio, video: video};
let stream = null;
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch(e) {
console.error(e);
displayError(e);
if(old)
delUpMedia(old);
return;
2020-04-24 19:38:21 +02:00
}
setMediaChoices();
let c = newUpStream(id);
c.kind = 'local';
c.stream = stream;
stream.getTracks().forEach(t => {
c.labels[t.id] = t.kind
2020-06-09 18:05:16 +02:00
if(t.kind == 'audio' && localMute)
t.enabled = false;
let sender = c.pc.addTrack(t, stream);
});
2020-09-11 23:20:46 +02:00
c.onstats = gotUpStats;
c.setStatsInterval(2000);
await setMedia(c, true);
setButtonsVisibility();
2020-04-24 19:38:21 +02:00
}
async function addShareMedia(setup) {
2020-04-24 19:38:21 +02:00
if(!getUserPass())
return;
let stream = null;
try {
stream = await navigator.mediaDevices.getDisplayMedia({video: true});
} catch(e) {
console.error(e);
displayError(e);
2020-04-24 19:38:21 +02:00
return;
}
let c = newUpStream();
c.kind = 'screenshare';
c.stream = stream;
stream.getTracks().forEach(t => {
let sender = c.pc.addTrack(t, stream);
t.onended = e => {
delUpMedia(c);
};
c.labels[t.id] = 'screenshare';
});
2020-09-11 23:20:46 +02:00
c.onstats = gotUpStats;
c.setStatsInterval(2000);
await setMedia(c, true);
setButtonsVisibility()
}
/**
* @param {Stream} c
*/
function stopUpMedia(c) {
if(!c.stream)
return;
c.stream.getTracks().forEach(t => {
try {
t.stop();
} catch(e) {
}
});
}
/**
* @param {Stream} c
*/
function delUpMedia(c) {
stopUpMedia(c);
2020-08-19 14:39:40 +02:00
try {
delMedia(c.id);
} catch(e) {
console.warn(e);
}
c.close(true);
delete(serverConnection.up[c.id]);
setButtonsVisibility()
}
/**
* delUpMediaKind reoves all up media of the given kind. If kind is
* falseish, it removes all up media.
* @param {string} kind
*/
function delUpMediaKind(kind) {
for(let id in serverConnection.up) {
let c = serverConnection.up[id];
if(kind && c.kind != kind)
continue
c.close(true);
delMedia(id);
delete(serverConnection.up[id]);
2020-04-24 19:38:21 +02:00
}
setButtonsVisibility();
hideVideo();
}
function findUpMedia(kind) {
for(let id in serverConnection.up) {
if(serverConnection.up[id].kind === kind)
return id;
}
return null;
2020-04-24 19:38:21 +02:00
}
2020-06-09 18:05:16 +02:00
function muteLocalTracks(mute) {
if(!serverConnection)
return;
for(let id in serverConnection.up) {
let c = serverConnection.up[id];
2020-06-09 18:05:16 +02:00
if(c.kind === 'local') {
let stream = c.stream;
stream.getTracks().forEach(t => {
if(t.kind === 'audio') {
t.enabled = !mute;
}
});
}
}
}
/**
* @param {Stream} c
* @param {boolean} isUp
*/
function setMedia(c, isUp) {
2020-04-24 19:38:21 +02:00
let peersdiv = document.getElementById('peers');
let div = document.getElementById('peer-' + c.id);
2020-04-24 19:38:21 +02:00
if(!div) {
div = document.createElement('div');
div.id = 'peer-' + c.id;
2020-04-24 19:38:21 +02:00
div.classList.add('peer');
peersdiv.appendChild(div);
}
let media = document.getElementById('media-' + c.id);
2020-04-24 19:38:21 +02:00
if(!media) {
media = document.createElement('video');
media.id = 'media-' + c.id;
2020-04-24 19:38:21 +02:00
media.classList.add('media');
media.autoplay = true;
media.playsinline = true;
media.controls = true;
if(isUp)
2020-04-24 19:38:21 +02:00
media.muted = true;
div.appendChild(media);
}
let label = document.getElementById('label-' + c.id);
2020-04-24 19:38:21 +02:00
if(!label) {
label = document.createElement('div');
label.id = 'label-' + c.id;
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
}
media.srcObject = c.stream;
setLabel(c);
setMediaStatus(c);
2020-05-01 01:22:17 +02:00
showVideo();
2020-05-01 01:22:17 +02:00
resizePeers();
2020-04-24 19:38:21 +02:00
}
2020-09-12 16:34:52 +02:00
/**
* @param {string} id
*/
2020-04-24 19:38:21 +02:00
function delMedia(id) {
let mediadiv = document.getElementById('peers');
let peer = document.getElementById('peer-' + id);
2020-08-19 14:39:40 +02:00
if(!peer)
throw new Error('Removing unknown media');
2020-04-24 19:38:21 +02:00
let media = document.getElementById('media-' + id);
media.srcObject = null;
mediadiv.removeChild(peer);
2020-05-01 01:22:17 +02:00
resizePeers();
hideVideo();
2020-04-24 19:38:21 +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';
let media = document.getElementById('media-' + c.id);
if(!media) {
console.warn('Setting status of unknown media.');
return;
}
if(good)
media.classList.remove('media-failed');
else
media.classList.add('media-failed');
}
/**
* @param {Stream} c
* @param {string} [fallback]
*/
function setLabel(c, fallback) {
let label = document.getElementById('label-' + c.id);
2020-04-24 19:38:21 +02:00
if(!label)
return;
let l = c.label;
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() {
let count =
Object.keys(serverConnection.up).length +
Object.keys(serverConnection.down).length;
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;
let size = 100 / columns;
2020-09-09 20:26:19 +02:00
let container = document.getElementById("video-container")
// Peers div has total padding of 30px, we remove 30 on offsetHeight
let max_video_height = Math.trunc((peers.offsetHeight - 30) / columns);
let media_list = document.getElementsByClassName("media");
2020-09-12 16:34:52 +02:00
for(let i = 0; i < media_list.length; i++)
media_list[i].style['max_height'] = max_video_height + "px";
2020-09-09 20:26:19 +02:00
if (count <= 2 && container.offsetHeight > container.offsetWidth) {
peers.style['grid-template-columns'] = "repeat(1, 1fr)";
} else {
peers.style['grid-template-columns'] = `repeat(${columns}, 1fr)`;
}
2020-05-01 01:22:17 +02:00
}
2020-08-24 22:37:48 +02:00
/** @type{Object.<string,string>} */
2020-04-24 19:38:21 +02:00
let users = {};
/**
* @param {string} id
* @param {string} name
*/
2020-04-24 19:38:21 +02:00
function addUser(id, name) {
if(!name)
name = null;
if(id in users)
throw new Error('Duplicate user id');
users[id] = name;
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");
2020-04-24 19:38:21 +02:00
user.textContent = name ? name : '(anon)';
2020-09-12 15:23:38 +02:00
if(name) {
let us = div.children;
for(let i = 0; i < us.length; i++) {
let child = us[i];
let childname = users[child.id.slice('user-'.length)];
if(!childname || childname > name) {
div.insertBefore(user, child);
return;
}
}
}
2020-04-24 19:38:21 +02:00
div.appendChild(user);
}
/**
* @param {string} id
* @param {string} name
*/
2020-04-24 19:38:21 +02:00
function delUser(id, name) {
if(!name)
name = null;
if(!(id in users))
2020-04-24 19:38:21 +02:00
throw new Error('Unknown user id');
if(users[id] !== name)
throw new Error('Inconsistent user name');
delete(users[id]);
let div = document.getElementById('users');
let user = document.getElementById('user-' + id);
div.removeChild(user);
}
function resetUsers() {
for(let id in users)
delUser(id, users[id]);
}
/**
* @param {string} id
* @param {string} kind
* @param {string} name
*/
2020-08-12 13:51:31 +02:00
function gotUser(id, kind, name) {
switch(kind) {
case 'add':
2020-04-24 19:38:21 +02:00
addUser(id, name);
2020-08-12 13:51:31 +02:00
break;
case 'delete':
delUser(id, name);
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() {
let userpass = getUserPass();
let text = '';
if(userpass && userpass.username)
text = 'as ' + userpass.username;
if(serverConnection.permissions.op && serverConnection.permissions.present)
2020-04-25 18:09:31 +02:00
text = text + ' (op, presenter)';
else if(serverConnection.permissions.op)
2020-04-25 18:09:31 +02:00
text = text + ' (op)';
else if(serverConnection.permissions.present)
2020-04-25 18:09:31 +02:00
text = text + ' (presenter)';
document.getElementById('userspan').textContent = text;
}
function clearUsername() {
document.getElementById('userspan').textContent = '';
}
/**
* @param {Object.<string,boolean>} perms
*/
function gotPermissions(perms) {
2020-04-25 18:09:31 +02:00
displayUsername();
setButtonsVisibility();
if(serverConnection.permissions.present)
2020-09-11 22:46:23 +02:00
displayMessage("Press Present to enable your camera or microphone");
2020-04-25 02:25:51 +02: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
* @returns {Array.<Text|HTMLElement>}
*/
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;
}
2020-09-12 16:34:52 +02:00
/**
* @param {Array.<string>} lines
* @returns {HTMLElement}
*/
2020-04-24 19:38:21 +02:00
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-08-24 22:37:48 +02:00
/**
* @typedef {Object} lastMessage
* @property {string} [nick]
* @property {string} [peerId]
*/
/** @type {lastMessage} */
2020-04-24 19:38:21 +02:00
let lastMessage = {};
2020-08-12 12:17:56 +02:00
function addToChatbox(peerId, nick, kind, message){
2020-09-02 15:35:55 +02:00
let userpass = getUserPass();
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-09-02 15:35:55 +02:00
if (userpass.username === nick) {
container.classList.add('message-sender');
}
2020-08-12 12:17:56 +02:00
if(kind !== 'me') {
2020-04-24 19:38:21 +02:00
let p = formatLines(message.split('\n'));
if (lastMessage.nick !== nick || lastMessage.peerId !== peerId) {
let user = document.createElement('p');
user.textContent = nick;
user.classList.add('message-user');
container.appendChild(user);
}
p.classList.add('message-content');
container.appendChild(p);
lastMessage.nick = nick;
lastMessage.peerId = peerId;
} else {
let asterisk = document.createElement('span');
asterisk.textContent = '*';
asterisk.classList.add('message-me-asterisk');
let user = document.createElement('span');
user.textContent = nick;
user.classList.add('message-me-user');
let content = document.createElement('span');
formatLine(message).forEach(elt => {
content.appendChild(elt);
});
content.classList.add('message-me-content');
container.appendChild(asterisk);
container.appendChild(user);
container.appendChild(content);
container.classList.add('message-me');
delete(lastMessage.nick);
delete(lastMessage.peerId);
}
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;
}
function clearChat() {
2020-04-25 21:16:49 +02:00
lastMessage = {};
document.getElementById('box').textContent = '';
}
/**
* parseCommand splits a string into two space-separated parts. The first
* part may be quoted and may include backslash escapes.
*
* @param {string} line
* @returns {Array.<string>}
*/
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-04-24 19:38:21 +02:00
function handleInput() {
let input = document.getElementById('input');
let data = input.value;
input.value = '';
let message, me;
if(data === '')
return;
if(data[0] === '/') {
if(data.length > 1 && data[1] === '/') {
2020-04-24 19:38:21 +02:00
message = data.substring(1);
me = false;
} else {
let cmd, rest;
let space = data.indexOf(' ');
2020-04-24 19:38:21 +02:00
if(space < 0) {
cmd = data;
rest = '';
} else {
cmd = data.slice(0, space);
rest = data.slice(space + 1);
2020-04-24 19:38:21 +02:00
}
switch(cmd) {
case '/me':
message = rest;
me = true;
break;
2020-04-25 18:43:22 +02:00
case '/leave':
serverConnection.close();
2020-04-25 18:43:22 +02:00
return;
2020-04-30 19:13:10 +02:00
case '/clear':
if(!serverConnection.permissions.op) {
2020-04-30 19:13:10 +02:00
displayError("You're not an operator");
return;
}
serverConnection.groupAction('clearchat');
2020-04-30 19:13:10 +02:00
return;
2020-05-18 15:24:04 +02:00
case '/lock':
case '/unlock':
if(!serverConnection.permissions.op) {
2020-05-18 15:24:04 +02:00
displayError("You're not an operator");
return;
}
2020-09-18 11:40:00 +02:00
serverConnection.groupAction(cmd.slice(1), rest);
2020-05-30 00:23:54 +02:00
return;
case '/record':
case '/unrecord':
if(!serverConnection.permissions.record) {
2020-05-30 00:23:54 +02:00
displayError("You're not allowed to record");
return;
}
serverConnection.groupAction(cmd.slice(1));
2020-05-18 15:24:04 +02:00
return;
2020-04-25 17:36:35 +02:00
case '/op':
case '/unop':
case '/kick':
case '/present':
2020-05-10 21:18:12 +02:00
case '/unpresent': {
if(!serverConnection.permissions.op) {
2020-04-25 18:09:31 +02:00
displayError("You're not an operator");
2020-04-25 17:36:35 +02:00
return;
}
let parsed = parseCommand(rest);
2020-04-25 17:36:35 +02:00
let id;
if(parsed[0] in users) {
id = parsed[0];
2020-04-25 17:36:35 +02:00
} else {
for(let i in users) {
if(users[i] === parsed[0]) {
2020-04-25 17:36:35 +02:00
id = i;
break;
}
}
}
if(!id) {
displayError('Unknown user ' + parsed[0]);
2020-04-25 17:36:35 +02:00
return;
}
serverConnection.userAction(cmd.slice(1), id, parsed[1]);
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
default:
displayError('Uknown command ' + cmd);
return;
}
}
} else {
message = data;
me = false;
}
let username = getUsername();
if(!username) {
displayError("Sorry, you're anonymous, you cannot chat");
return;
}
try {
serverConnection.chat(username, me ? 'me' : '', message);
} 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();
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-04-24 19:38:21 +02:00
function start_drag(e) {
let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
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);
function displayError(message, level, position, gravity) {
var background = "linear-gradient(to right, #e20a0a, #df2d2d)";
if (level === "info") {
background = "linear-gradient(to right, #529518, #96c93d)";
}
if (level === "warning") {
background = "linear-gradient(to right, #edd800, #c9c200)";
2020-04-24 19:38:21 +02:00
}
Toastify({
text: message,
duration: 4000,
close: true,
position: position ? position: 'center',
gravity: gravity ? gravity : 'top',
backgroundColor: background,
className: level,
}).showToast();
2020-04-24 19:38:21 +02:00
}
function displayWarning(message) {
let level = "warning";
return displayError(message, level);
}
function displayMessage(message) {
return displayError(message, "info", "right", "bottom");
2020-04-24 19:38:21 +02:00
}
document.getElementById('userform').onsubmit = function(e) {
2020-04-24 19:38:21 +02:00
e.preventDefault();
let username = document.getElementById('username').value.trim();
let password = document.getElementById('password').value;
setUserPass(username, password);
serverConnect();
2020-05-10 21:18:07 +02:00
};
2020-04-24 19:38:21 +02:00
document.getElementById('disconnectbutton').onclick = function(e) {
serverConnection.close();
2020-09-02 15:35:55 +02:00
let user_box = document.getElementById('userDropdown');
if (user_box.classList.contains("show")) {
user_box.classList.toggle("show");
}
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('user').onclick = function(e) {
2020-09-02 17:55:44 +02:00
e.preventDefault();
2020-08-27 21:17:46 +02:00
document.getElementById("userDropdown").classList.toggle("show");
};
document.getElementById('clodeside').onclick = function(e) {
e.preventDefault();
closeNav();
};
2020-09-09 20:26:19 +02:00
document.getElementById('collapse-video').onclick = function(e) {
e.preventDefault();
2020-09-09 20:26:19 +02:00
let width = window.innerWidth;
if (width <= 768) {
let user_box = document.getElementById('userDropdown');
if (user_box.classList.contains("show")) {
return;
}
// fixed div for small screen
2020-09-09 20:26:19 +02:00
this.style.display = "";
hideVideo(true);
document.getElementById('switch-video').style.display = "block";
}
};
document.getElementById('switch-video').onclick = function(e) {
e.preventDefault();
showVideo();
this.style.display = "";
2020-09-09 20:26:19 +02:00
document.getElementById('collapse-video').style.display = "block";
};
window.onclick = function(event) {
let user_box = document.getElementById('userDropdown');
if (user_box.classList.contains("show") && event.target.id != "user") {
let parent = event.target;
while (parent.id !== "main" && parent.id !== "userDropdown" &&
parent.id !== "user" && parent.tagName !== "body") {
parent = parent.parentNode;
}
if (parent.id !="userDropdown" && parent.id !== "user") {
user_box.classList.toggle("show");
}
}
};
async function serverConnect() {
serverConnection = new ServerConnection();
serverConnection.onconnected = gotConnected;
serverConnection.onclose = gotClose;
serverConnection.ondownstream = gotDownStream;
serverConnection.onuser = gotUser;
serverConnection.onpermissions = gotPermissions;
serverConnection.onchat = addToChatbox;
serverConnection.onclearchat = clearChat;
serverConnection.onusermessage = function(kind, message) {
if(kind === 'error')
displayError(`The server said: ${message}`);
else
displayWarning(`The server said: ${message}`);
}
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-04-24 19:38:21 +02:00
function start() {
group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
2020-04-25 17:43:56 +02:00
let title = group.charAt(0).toUpperCase() + group.slice(1);
if(group !== '') {
document.title = title;
2020-05-10 21:18:07 +02:00
document.getElementById('title').textContent = title;
2020-04-25 17:43:56 +02:00
}
2020-04-24 19:38:21 +02:00
2020-06-09 18:05:16 +02:00
setLocalMute(localMute);
document.getElementById("user").classList.add('invisible');
document.getElementById("login-container").classList.remove('invisible');
2020-04-24 19:38:21 +02:00
}
start();