Initial commit.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
body {
|
||||
font: 14px "Lato", Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
.signature {
|
||||
border-top: solid;
|
||||
margin-top: 2em;
|
||||
padding-top: 0em;
|
||||
border-width: thin;
|
||||
clear: both;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>SFU</title>
|
||||
<link rel="stylesheet" href="/common.css">
|
||||
<link rel="stylesheet" href="/mainpage.css">
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>SFU</h1>
|
||||
|
||||
<form id="groupform">
|
||||
<label for="group">Group:</label>
|
||||
<input id="group" type="text" name="group"/>
|
||||
<input type="submit" value="Join"/><br/>
|
||||
</form>
|
||||
|
||||
<div id="public-groups" class="groups">
|
||||
<h2>Public groups</h2>
|
||||
|
||||
<table id="public-groups-table"></table>
|
||||
</div>
|
||||
|
||||
<footer class="signature"><p><a href="https://www.irif.fr/~jch/software/sfu/">Unnamed SFU</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a></footer>
|
||||
|
||||
<script src="/mainpage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.groups {
|
||||
}
|
||||
|
||||
.nogroups {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2019-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';
|
||||
|
||||
document.getElementById('groupform').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
let group = document.getElementById('group').value.trim();
|
||||
|
||||
location.href = '/group/' + group;
|
||||
}
|
||||
|
||||
async function listPublicGroups() {
|
||||
let div = document.getElementById('public-groups');
|
||||
let table = document.getElementById('public-groups-table');
|
||||
|
||||
let l;
|
||||
try {
|
||||
l = await (await fetch('/public-groups.json')).json();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
l = [];
|
||||
}
|
||||
|
||||
if (l.length === 0) {
|
||||
table.textContent = '(No groups found.)';
|
||||
div.classList.remove('groups');
|
||||
div.classList.add('nogroups');
|
||||
return;
|
||||
}
|
||||
|
||||
div.classList.remove('nogroups');
|
||||
div.classList.add('groups');
|
||||
|
||||
for(let i = 0; i < l.length; i++) {
|
||||
let group = l[i];
|
||||
let tr = document.createElement('tr');
|
||||
let td = document.createElement('td');
|
||||
let a = document.createElement('a');
|
||||
a.textContent = group.name;
|
||||
a.href = '/group/' + encodeURIComponent(group.name);
|
||||
td.appendChild(a);
|
||||
tr.appendChild(td);
|
||||
let td2 = document.createElement('td');
|
||||
td2.textContent = `(${group.clientCount} clients)`;
|
||||
tr.appendChild(td2);
|
||||
table.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
listPublicGroups();
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#header {
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
#statdiv {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
#errspan {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.connected {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.userform {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.userform-invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disconnect-invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.noerror {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#users {
|
||||
width: 5%;
|
||||
margin-left: 2%;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
#anonymous-users {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#chatbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#chat {
|
||||
display: flex;
|
||||
width: 20%;
|
||||
margin-left: 0.3em;
|
||||
border: 1px solid;
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
#inputform {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#box {
|
||||
height: 95%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message, message-me {
|
||||
margin: 0 0.5em 0 0.5em;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
line-height: 1.5;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.message-me-asterisk {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.message-me-user {
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.message-me-content {
|
||||
}
|
||||
|
||||
#input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#inputbutton {
|
||||
background-color: white;
|
||||
border: none;
|
||||
margin-right: 0.2em;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#inputbutton:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#resizer {
|
||||
width: 8px;
|
||||
margin-left: -8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#resizer:hover {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
#peers {
|
||||
margin-left: 1%;
|
||||
margin-right: 1%;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.peer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
max-height: 50%;
|
||||
}
|
||||
|
||||
.media {
|
||||
height: 400px;
|
||||
margin: auto;
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
height: 2em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#inputform {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#input {
|
||||
width: 85%;
|
||||
border: 1px solid;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>SFU</title>
|
||||
<link rel="stylesheet" type="text/css" href="/common.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/sfu.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 id="title">SFU</h1>
|
||||
|
||||
<div id="header">
|
||||
<div id="statdiv">
|
||||
<span id="statspan"></span>
|
||||
<form id="userform" class="userform">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" name="username"
|
||||
autocomplete="username"/>
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" type="password" name="password"
|
||||
autocomplete="current-password"/>
|
||||
<input id="connectbutton" type="submit" value="Connect" disabled/>
|
||||
</form>
|
||||
<input id="disconnectbutton" class="disconnect-invisible"
|
||||
type="submit" value="Disconnect"/>
|
||||
<span id="errspan"></span>
|
||||
</div>
|
||||
|
||||
<div id="optionsdiv">
|
||||
<label for="presenterbox">Present:</label>
|
||||
<input id="presenterbox" type="checkbox"/>
|
||||
<label for="sharebox">Share screen:</label>
|
||||
<input id="sharebox" type="checkbox"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="users"></div>
|
||||
<div id="chat">
|
||||
<div id="chatbox">
|
||||
<div id="box"></div>
|
||||
<form id="inputform">
|
||||
<textarea id="input"></textarea>
|
||||
<input id="inputbutton" type="submit" value="➤"/>
|
||||
</form>
|
||||
</div>
|
||||
<div id="resizer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="peers"></div>
|
||||
</div>
|
||||
|
||||
<script src="/sfu.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
+794
@@ -0,0 +1,794 @@
|
||||
// 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';
|
||||
|
||||
let myid;
|
||||
|
||||
let group;
|
||||
|
||||
let socket;
|
||||
|
||||
let up = {}, down = {};
|
||||
|
||||
let iceServers = [];
|
||||
|
||||
function toHex(array) {
|
||||
let a = new Uint8Array(array);
|
||||
let s = '';
|
||||
function hex(x) {
|
||||
let h = x.toString(16);
|
||||
if(h.length < 2)
|
||||
h = '0' + h;
|
||||
return h;
|
||||
}
|
||||
return a.reduce((x, y) => x + hex(y), '');
|
||||
}
|
||||
|
||||
function randomid() {
|
||||
let a = new Uint8Array(16);
|
||||
crypto.getRandomValues(a);
|
||||
return toHex(a);
|
||||
}
|
||||
|
||||
function Connection(id, pc) {
|
||||
this.id = id;
|
||||
this.label = null;
|
||||
this.pc = pc;
|
||||
this.stream = null;
|
||||
this.iceCandidates = [];
|
||||
}
|
||||
|
||||
Connection.prototype.close = function() {
|
||||
this.pc.close();
|
||||
send({
|
||||
type: 'close',
|
||||
id: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
function setUserPass(username, password) {
|
||||
window.sessionStorage.setItem(
|
||||
'userpass',
|
||||
JSON.stringify({username: username, password: password}),
|
||||
);
|
||||
}
|
||||
|
||||
function getUserPass() {
|
||||
let userpass = window.sessionStorage.getItem('userpass');
|
||||
if(!userpass)
|
||||
return null;
|
||||
return JSON.parse(userpass);
|
||||
}
|
||||
|
||||
function getUsername() {
|
||||
let userpass = getUserPass();
|
||||
if(!userpass)
|
||||
return null;
|
||||
return userpass.username;
|
||||
}
|
||||
|
||||
function setConnected(connected) {
|
||||
let statspan = document.getElementById('statspan');
|
||||
let userform = document.getElementById('userform');
|
||||
let diconnect = document.getElementById('disconnectbutton');
|
||||
if(connected) {
|
||||
statspan.textContent = 'Connected';
|
||||
statspan.classList.remove('disconnected');
|
||||
statspan.classList.add('connected');
|
||||
userform.classList.add('userform-invisible');
|
||||
userform.classList.remove('userform');
|
||||
disconnectbutton.classList.remove('disconnect-invisible');
|
||||
} else {
|
||||
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');
|
||||
userform.classList.add('userform');
|
||||
userform.classList.remove('userform-invisible');
|
||||
disconnectbutton.classList.add('disconnect-invisible');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('presenterbox').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
setLocalMedia();
|
||||
}
|
||||
|
||||
document.getElementById('sharebox').onchange = function(e) {
|
||||
e.preventDefault();
|
||||
setShareMedia();
|
||||
}
|
||||
|
||||
let localMediaId = null;
|
||||
|
||||
async function setLocalMedia() {
|
||||
if(!getUserPass())
|
||||
return;
|
||||
|
||||
if(!document.getElementById('presenterbox').checked) {
|
||||
if(localMediaId) {
|
||||
up[localMediaId].close();
|
||||
delete(up[localMediaId]);
|
||||
delMedia(localMediaId)
|
||||
localMediaId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(!localMediaId) {
|
||||
let constraints = {audio: true, video: true};
|
||||
let opts = {video: true, audio: true};
|
||||
let stream = null;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
localMediaId = await newUpStream();
|
||||
|
||||
let c = up[localMediaId];
|
||||
c.stream = stream;
|
||||
stream.getTracks().forEach(t => {
|
||||
c.pc.addTrack(t, stream);
|
||||
});
|
||||
await setMedia(localMediaId);
|
||||
}
|
||||
}
|
||||
|
||||
let shareMediaId = null;
|
||||
|
||||
async function setShareMedia() {
|
||||
if(!getUserPass())
|
||||
return;
|
||||
|
||||
if(!document.getElementById('sharebox').checked) {
|
||||
if(shareMediaId) {
|
||||
up[shareMediaId].close();
|
||||
delete(up[shareMediaId]);
|
||||
delMedia(shareMediaId)
|
||||
shareMediaId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!shareMediaId) {
|
||||
let constraints = {audio: true, video: true};
|
||||
let opts = {video: true, audio: true};
|
||||
let stream = null;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
shareMediaId = await newUpStream();
|
||||
|
||||
let c = up[shareMediaId];
|
||||
c.stream = stream;
|
||||
stream.getTracks().forEach(t => {
|
||||
c.pc.addTrack(t, stream);
|
||||
});
|
||||
await setMedia(shareMediaId);
|
||||
}
|
||||
}
|
||||
|
||||
function setMedia(id) {
|
||||
let mine = true;
|
||||
let c = up[id];
|
||||
if(!c) {
|
||||
c = down[id];
|
||||
mine = false;
|
||||
}
|
||||
if(!c)
|
||||
throw new Error('Unknown connection');
|
||||
|
||||
let peersdiv = document.getElementById('peers');
|
||||
|
||||
let div = document.getElementById('peer-' + id);
|
||||
if(!div) {
|
||||
div = document.createElement('div');
|
||||
div.id = 'peer-' + id;
|
||||
div.classList.add('peer');
|
||||
peersdiv.appendChild(div);
|
||||
}
|
||||
|
||||
let media = document.getElementById('media-' + id);
|
||||
if(!media) {
|
||||
media = document.createElement('video');
|
||||
media.id = 'media-' + id;
|
||||
media.classList.add('media');
|
||||
media.autoplay = true;
|
||||
media.playsinline = true;
|
||||
media.controls = true;
|
||||
if(mine)
|
||||
media.muted = true;
|
||||
div.appendChild(media);
|
||||
}
|
||||
|
||||
let label = document.getElementById('label-' + id);
|
||||
if(!label) {
|
||||
label = document.createElement('div');
|
||||
label.id = 'label-' + id;
|
||||
label.classList.add('label');
|
||||
div.appendChild(label)
|
||||
}
|
||||
|
||||
media.srcObject = c.stream;
|
||||
setLabel(id);
|
||||
}
|
||||
|
||||
function delMedia(id) {
|
||||
let mediadiv = document.getElementById('peers');
|
||||
let peer = document.getElementById('peer-' + id);
|
||||
let media = document.getElementById('media-' + id);
|
||||
|
||||
media.srcObject = null;
|
||||
mediadiv.removeChild(peer);
|
||||
}
|
||||
|
||||
function setLabel(id) {
|
||||
let label = document.getElementById('label-' + id);
|
||||
if(!label)
|
||||
return;
|
||||
let l = down[id] ? down[id].label : null;
|
||||
label.textContent = l ? l : '';
|
||||
}
|
||||
|
||||
function serverConnect() {
|
||||
if(socket) {
|
||||
socket.close(1000, 'Reconnecting');
|
||||
socket = null;
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
try {
|
||||
socket = new WebSocket(
|
||||
`ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`,
|
||||
);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
setConnected(false);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.onerror = function(e) {
|
||||
console.error(e);
|
||||
reject(e.error ? e.error : e);
|
||||
};
|
||||
socket.onopen = function(e) {
|
||||
resetUsers();
|
||||
setConnected(true);
|
||||
let up = getUserPass();
|
||||
send({
|
||||
type: 'handshake',
|
||||
id: myid,
|
||||
group: group,
|
||||
username: up.username,
|
||||
password: up.password,
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
socket.onclose = function(e) {
|
||||
setConnected(false);
|
||||
document.getElementById('presenterbox').checked = false;
|
||||
setLocalMedia();
|
||||
document.getElementById('sharebox').checked = false;
|
||||
setShareMedia();
|
||||
reject(new Error('websocket close ' + e.code + ' ' + e.reason));
|
||||
};
|
||||
socket.onmessage = function(e) {
|
||||
let m = JSON.parse(e.data);
|
||||
switch(m.type) {
|
||||
case 'offer':
|
||||
gotOffer(m.id, m.offer);
|
||||
break;
|
||||
case 'answer':
|
||||
gotAnswer(m.id, m.answer);
|
||||
break;
|
||||
case 'close':
|
||||
gotClose(m.id);
|
||||
break;
|
||||
case 'ice':
|
||||
gotICE(m.id, m.candidate);
|
||||
break;
|
||||
case 'maxbitrate':
|
||||
setMaxBitrate(m.id, m.audiorate, m.videorate);
|
||||
break;
|
||||
case 'label':
|
||||
gotLabel(m.id, m.value);
|
||||
break;
|
||||
case 'user':
|
||||
gotUser(m.id, m.username, m.del);
|
||||
break;
|
||||
case 'chat':
|
||||
addToChatbox(m.id, m.username, m.value, m.me);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unexpected server message', m.type);
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function gotOffer(id, offer) {
|
||||
let c = down[id];
|
||||
if(!c) {
|
||||
let pc = new RTCPeerConnection({
|
||||
iceServers: iceServers,
|
||||
});
|
||||
c = new Connection(id, pc);
|
||||
down[id] = c;
|
||||
|
||||
c.pc.onicecandidate = function(e) {
|
||||
if(!e.candidate)
|
||||
return;
|
||||
send({type: 'ice',
|
||||
id: id,
|
||||
candidate: e.candidate,
|
||||
});
|
||||
}
|
||||
|
||||
c.pc.ontrack = function(e) {
|
||||
c.stream = e.streams[0];
|
||||
setMedia(id);
|
||||
}
|
||||
}
|
||||
|
||||
await c.pc.setRemoteDescription(offer);
|
||||
await addIceCandidates(c);
|
||||
let answer = await c.pc.createAnswer();
|
||||
if(!answer)
|
||||
throw new Error("Didn't create answer")
|
||||
await c.pc.setLocalDescription(answer);
|
||||
send({
|
||||
type: 'answer',
|
||||
id: id,
|
||||
answer: answer,
|
||||
});
|
||||
}
|
||||
|
||||
function gotLabel(id, label) {
|
||||
let c = down[id];
|
||||
if(!c)
|
||||
throw new Error('Got label for unknown id');
|
||||
|
||||
c.label = label;
|
||||
setLabel(id);
|
||||
}
|
||||
|
||||
async function gotAnswer(id, answer) {
|
||||
let c = up[id];
|
||||
if(!c)
|
||||
throw new Error('unknown up stream');
|
||||
await c.pc.setRemoteDescription(answer);
|
||||
await addIceCandidates(c);
|
||||
}
|
||||
|
||||
function gotClose(id) {
|
||||
let c = down[id];
|
||||
if(!c)
|
||||
throw new Error('unknown down stream');
|
||||
delete(down[id]);
|
||||
c.close();
|
||||
delMedia(id);
|
||||
}
|
||||
|
||||
async function gotICE(id, candidate) {
|
||||
let conn = up[id];
|
||||
if(!conn)
|
||||
conn = down[id];
|
||||
if(!conn)
|
||||
throw new Error('unknown stream');
|
||||
if(conn.pc.remoteDescription)
|
||||
await conn.pc.addIceCandidate(candidate).catch(console.warn);
|
||||
else
|
||||
conn.iceCandidates.push(candidate)
|
||||
}
|
||||
|
||||
let maxaudiorate, maxvideorate;
|
||||
|
||||
async function setMaxBitrate(id, audio, video) {
|
||||
let conn = up[id];
|
||||
if(!conn)
|
||||
throw new Error("Setting bitrate of unknown id");
|
||||
|
||||
let senders = conn.pc.getSenders();
|
||||
for(let i = 0; i < senders.length; i++) {
|
||||
let s = senders[i];
|
||||
if(!s.track)
|
||||
return;
|
||||
let p = s.getParameters();
|
||||
let bitrate;
|
||||
if(s.track.kind == 'audio')
|
||||
bitrate = audio;
|
||||
else if(s.track.kind == 'video')
|
||||
bitrate = video;
|
||||
for(let j = 0; j < p.encodings.length; j++) {
|
||||
let e = p.encodings[j];
|
||||
if(bitrate)
|
||||
e.maxBitrate = bitrate;
|
||||
else
|
||||
delete(e.maxBitrate);
|
||||
await s.setParameters(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addIceCandidates(conn) {
|
||||
let promises = []
|
||||
conn.iceCandidates.forEach(c => {
|
||||
promises.push(conn.pc.addIceCandidate(c).catch(console.warn));
|
||||
});
|
||||
conn.iceCandidates = [];
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
function send(m) {
|
||||
if(!m)
|
||||
throw(new Error('Sending null message'));
|
||||
return socket.send(JSON.stringify(m))
|
||||
}
|
||||
|
||||
let users = {};
|
||||
|
||||
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 anon = document.getElementById('anonymous-users');
|
||||
let user = document.createElement('div');
|
||||
user.id = 'user-' + id;
|
||||
user.textContent = name ? name : '(anon)';
|
||||
div.appendChild(user);
|
||||
}
|
||||
|
||||
function delUser(id, name) {
|
||||
if(!name)
|
||||
name = null;
|
||||
if(!id in users)
|
||||
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]);
|
||||
}
|
||||
|
||||
function gotUser(id, name, del) {
|
||||
if(del)
|
||||
delUser(id, name);
|
||||
else
|
||||
addUser(id, name);
|
||||
}
|
||||
|
||||
const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\+~#=?]+[-a-zA-Z0-9@:%/_\+~#=]/g;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let lastMessage = {};
|
||||
|
||||
function addToChatbox(peerId, nick, message, me){
|
||||
let container = document.createElement('div');
|
||||
container.classList.add('message');
|
||||
if(!me) {
|
||||
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);
|
||||
}
|
||||
|
||||
document.getElementById('box').appendChild(container);
|
||||
|
||||
if(box.scrollHeight > box.clientHeight) {
|
||||
box.scrollTop = box.scrollHeight - box.clientHeight;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
let username = getUsername();
|
||||
if(!username) {
|
||||
displayError("Sorry, you're anonymous, you cannot chat");
|
||||
return;
|
||||
}
|
||||
|
||||
let input = document.getElementById('input');
|
||||
let data = input.value;
|
||||
input.value = '';
|
||||
|
||||
let message, me;
|
||||
|
||||
if(data === '')
|
||||
return;
|
||||
|
||||
if(data.charAt(0) === '/') {
|
||||
if(data.charAt(1) === '/') {
|
||||
message = data.substring(1);
|
||||
me = false;
|
||||
} else {
|
||||
let space, cmd, rest;
|
||||
space = data.indexOf(' ');
|
||||
if(space < 0) {
|
||||
cmd = data;
|
||||
rest = '';
|
||||
} else {
|
||||
cmd = data.slice(0, space);
|
||||
rest = data.slice(space + 1).trim();
|
||||
}
|
||||
|
||||
switch(cmd) {
|
||||
case '/nick':
|
||||
setNick(rest);
|
||||
storeNick(rest);
|
||||
return;
|
||||
case '/me':
|
||||
message = rest;
|
||||
me = true;
|
||||
break;
|
||||
default:
|
||||
displayError('Uknown command ' + cmd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message = data;
|
||||
me = false;
|
||||
}
|
||||
|
||||
addToChatbox(myid, username, message, me);
|
||||
send({
|
||||
type: 'chat',
|
||||
username: username,
|
||||
value: message,
|
||||
me: me,
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function chatResizer(e) {
|
||||
e.preventDefault();
|
||||
let chat = document.getElementById('chat');
|
||||
let start_x = e.clientX;
|
||||
let start_width = parseFloat(
|
||||
document.defaultView.getComputedStyle(chat).width.replace('px', ''),
|
||||
);
|
||||
let inputbutton = document.getElementById('inputbutton');
|
||||
function start_drag(e) {
|
||||
let width = start_width + e.clientX - start_x;
|
||||
if(width < 40)
|
||||
inputbutton.style.display = 'none';
|
||||
else
|
||||
inputbutton.style.display = 'inline';
|
||||
chat.style.width = width + 'px';
|
||||
}
|
||||
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);
|
||||
|
||||
async function newUpStream() {
|
||||
let id = randomid();
|
||||
if(up[id])
|
||||
throw new Error('Eek!');
|
||||
let pc = new RTCPeerConnection({
|
||||
iceServers: iceServers,
|
||||
});
|
||||
if(!pc)
|
||||
throw new Error("Couldn't create peer connection")
|
||||
up[id] = new Connection(id, pc);
|
||||
|
||||
pc.onnegotiationneeded = e => negotiate(id);
|
||||
|
||||
pc.onicecandidate = function(e) {
|
||||
if(!e.candidate)
|
||||
return;
|
||||
send({type: 'ice',
|
||||
id: id,
|
||||
candidate: e.candidate,
|
||||
});
|
||||
}
|
||||
|
||||
pc.ontrack = console.error;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function negotiate(id) {
|
||||
let c = up[id];
|
||||
if(!c)
|
||||
throw new Error('unknown connection');
|
||||
let offer = await c.pc.createOffer({});
|
||||
if(!offer)
|
||||
throw(new Error("Didn't create offer"));
|
||||
await c.pc.setLocalDescription(offer);
|
||||
send({
|
||||
type: 'offer',
|
||||
id: id,
|
||||
offer: offer,
|
||||
});
|
||||
}
|
||||
|
||||
let errorTimeout = null;
|
||||
|
||||
function setErrorTimeout(ms) {
|
||||
if(errorTimeout) {
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = null;
|
||||
}
|
||||
if(ms) {
|
||||
errorTimeout = setTimeout(clearError, ms);
|
||||
}
|
||||
}
|
||||
|
||||
function displayError(message) {
|
||||
let errspan = document.getElementById('errspan');
|
||||
errspan.textContent = message;
|
||||
errspan.classList.remove('noerror');
|
||||
errspan.classList.add('error');
|
||||
setErrorTimeout(8000);
|
||||
}
|
||||
|
||||
function displayWarning(message) {
|
||||
// don't overwrite real errors
|
||||
if(!errorTimeout)
|
||||
return displayError(message);
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
let errspan = document.getElementById('errspan');
|
||||
errspan.textContent = '';
|
||||
errspan.classList.remove('error');
|
||||
errspan.classList.add('noerror');
|
||||
setErrorTimeout(null);
|
||||
}
|
||||
|
||||
async function getIceServers() {
|
||||
let r = await fetch('/ice-servers.json');
|
||||
if(!r.ok)
|
||||
throw new Error("Couldn't fetch ICE servers: " +
|
||||
r.status + ' ' + r.statusText);
|
||||
let servers = await r.json();
|
||||
if(!(servers instanceof Array))
|
||||
throw new Error("couldn't parse ICE servers");
|
||||
iceServers = servers;
|
||||
}
|
||||
|
||||
async function doConnect() {
|
||||
await serverConnect();
|
||||
await setLocalMedia();
|
||||
await setShareMedia();
|
||||
}
|
||||
|
||||
document.getElementById('userform').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
let username = document.getElementById('username').value.trim();
|
||||
let password = document.getElementById('password').value;
|
||||
setUserPass(username, password);
|
||||
await doConnect();
|
||||
}
|
||||
|
||||
document.getElementById('disconnectbutton').onclick = function(e) {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
|
||||
function start() {
|
||||
group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
|
||||
let title = document.getElementById('title');
|
||||
if(group !== '')
|
||||
title.textContent = group.charAt(0).toUpperCase() + group.slice(1);
|
||||
|
||||
myid = randomid();
|
||||
|
||||
getIceServers().catch(console.error).then(c => {
|
||||
document.getElementById('connectbutton').disabled = false;
|
||||
}).then(c => {
|
||||
let userpass = getUserPass();
|
||||
if(userpass)
|
||||
doConnect();
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
Reference in New Issue
Block a user