Checkpoint audio-only Skald fork work

This commit is contained in:
Storm Dragon
2026-05-18 13:06:57 -04:00
parent a8ada950d5
commit 965347cad4
48 changed files with 1080 additions and 3651 deletions
-93
View File
@@ -1,93 +0,0 @@
// Copyright (c) 2024 by Juliusz Chroboczek.
// 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.
'use strict';
let imageSegmenter;
async function loadImageSegmenter(model) {
let module = await import('/third-party/tasks-vision/vision_bundle.mjs');
let vision = await module.FilesetResolver.forVisionTasks(
"/third-party/tasks-vision/wasm"
);
return await module.ImageSegmenter.createFromOptions(vision, {
baseOptions: {
modelAssetPath: model,
},
outputCategoryMask: true,
outputConfidenceMasks: false,
runningMode: 'VIDEO',
});
}
async function foregroundMask(bitmap, timestamp) {
if(!(bitmap instanceof ImageBitmap))
throw new Error('Bad type for worker data');
try {
let width = bitmap.width;
let height = bitmap.height;
let p = new Promise((resolve, reject) =>
imageSegmenter.segmentForVideo(
bitmap, timestamp,
result => resolve(result),
));
let result = await p;
/** @type{Uint8Array} */
let mask = result.categoryMask.getAsUint8Array();
let id = new ImageData(width, height);
for(let i = 0; i < mask.length; i++)
id.data[4 * i + 3] = mask[i];
result.close();
let ib = await createImageBitmap(id);
return {
bitmap: bitmap,
mask: ib,
};
} catch(e) {
bitmap.close();
throw(e);
}
}
onmessage = async e => {
try {
let data = e.data;
if(data.model) {
if(imageSegmenter)
throw new Error("image segmenter already initialised");
imageSegmenter = await loadImageSegmenter(data.model);
if(!imageSegmenter)
throw new Error("loadImageSegmenter returned null");
postMessage(null);
} else if(data.bitmap) {
if(imageSegmenter == null)
throw new Error("image segmenter not initialised");
let mask = await foregroundMask(data.bitmap, data.timestamp);
postMessage(mask, [mask.bitmap, mask.mask]);
} else {
throw new Error("unexpected message type");
}
} catch(e) {
postMessage(e);
}
}
+4 -4
View File
@@ -24,9 +24,9 @@ document.getElementById('passwordform').onsubmit = async function(e) {
e.preventDefault();
let parms = new URLSearchParams(window.location.search);
let group = parms.get('group');
if(!group) {
displayError("Couldn't determine group");
let hall = parms.get('hall');
if(!hall) {
displayError("Couldn't determine hall");
return;
}
let user = parms.get('username');
@@ -44,7 +44,7 @@ document.getElementById('passwordform').onsubmit = async function(e) {
}
try {
await setPassword(group, user, false, new1, old);
await setPassword(hall, user, false, new1, old);
document.getElementById('old').value = '';
document.getElementById('new1').value = '';
document.getElementById('new2').value = '';
+1 -6
View File
@@ -7,15 +7,10 @@
</head>
<body>
<div><button id="start">Start</button></p>
<div><button id="start">Start</button></div>
<p id="status"></p>
<p id="error"></p>
<div><button id="show" disabled>Show/hide yourself</button></div>
<div id="videos"></div>
<div id="chat"></div>
<script src="/protocol.js" defer></script>
+12 -177
View File
@@ -1,4 +1,4 @@
/* Skald client example. */
/* Skald client example - audio-only. */
/**
* The main function.
@@ -6,7 +6,7 @@
* @param {string} url
*/
async function start(url) {
// fetch the group information
// fetch the hall information
let r = await fetch(url + ".status");
if(!r.ok) {
throw new Error(`${r.status} ${r.statusText}`);
@@ -23,7 +23,7 @@ async function start(url) {
if(token) {
serverConnect(status, token);
} else if(status.authPortal) {
window.location.href = groupStatus.authPortal
window.location.href = status.authPortal
return;
} else {
serverConnect(status, null);
@@ -33,7 +33,7 @@ async function start(url) {
/**
* Display the connection status.
*
* @parm {string} status
* @param {string} status
*/
function displayStatus(status) {
let c = document.getElementById('status');
@@ -43,38 +43,29 @@ function displayStatus(status) {
/**
* Connect to the server.
*
* @parm {Object} status
* @parm {string} token
* @param {Object} status
* @param {string} token
*/
function serverConnect(status, token) {
// create the connection to the server
let conn = new ServerConnection();
conn.onconnected = async function() {
displayStatus('Connected');
let creds = token ?
{type: 'token', token: token} :
{type: 'password', password: ''};
// join the group and wait for the onjoined callback
await this.join("public", "example-user", creds);
};
conn.onchat = onChat;
conn.onusermessage = onUserMessage;
conn.ondownstream = onDownStream;
conn.onclose = function() {
displayStatus('Disconnected');
}
conn.onjoined = onJoined;
// connect and wait for the onconnected callback
conn.connect(status.endpoint);
}
/**
* Called whenever we receive a chat message.
*
* @this {ServerConnection}
* @parm {string} username
* @parm {string} message
*/
function onChat(id, dest, username, time, privileged, history, kind, message) {
let p = document.createElement('p');
@@ -85,11 +76,6 @@ function onChat(id, dest, username, time, privileged, history, kind, message) {
/**
* Called whenever we receive a user message.
*
* @this {ServerConnection}
* @parm {string} username
* @parm {string} message
* @parm {string} kind
*/
function onUserMessage(id, dest, username, time, privileged, kind, error, message) {
switch(kind) {
@@ -113,57 +99,13 @@ function onUserMessage(id, dest, username, time, privileged, kind, error, messag
}
}
/**
* Find the camera stream, if any.
*
* @parm {string} conn
* @returns {Stream}
* Called when we join or leave a hall.
*/
function cameraStream(conn) {
for(let id in conn.up) {
let s = conn.up[id];
if(s.label === 'camera')
return s;
}
return null;
}
/**
* Enable or disable the show/hide button.
*
* @parm{ServerConnection} conn
* @parm{boolean} enable
*/
function enableShow(conn, enable) {
let b = /** @type{HTMLButtonElement} */(document.getElementById('show'));
if(enable) {
b.onclick = function() {
let s = cameraStream(conn);
if(!s)
showCamera(conn);
else
hide(conn, s);
}
b.disabled = false;
} else {
b.disabled = true;
b.onclick = null;
}
}
/**
* Called when we join or leave a group.
*
* @this {ServerConnection}
* @parm {string} kind
* @parm {string} message}
*/
async function onJoined(kind, group, perms, status, data, error, message) {
async function onJoined(kind, hall, perms, status, data, error, message) {
switch(kind) {
case 'fail':
displayError(message);
enableShow(this, false);
this.close();
break;
case 'redirect':
@@ -172,15 +114,13 @@ async function onJoined(kind, group, perms, status, data, error, message) {
return;
case 'leave':
displayStatus('Connected');
enableShow(this, false);
this.close();
break;
case 'join':
case 'change':
displayStatus(`Connected as ${this.username} in group ${this.group}.`);
enableShow(this, true);
// request videos from the server
this.request({'': ['audio', 'video']});
displayStatus(`Connected as ${this.username} in hall ${this.hall}.`);
// request audio from the server
this.request({'': ['audio']});
break;
default:
displayError(`Unexpected state ${kind}.`);
@@ -189,115 +129,10 @@ async function onJoined(kind, group, perms, status, data, error, message) {
}
}
/**
* Create a video element. We encode the stream's id in the element's id
* in order to avoid having a global hash table that maps ids to video
* elements.
*
* @parm {string} id
* @returns {HTMLVideoElement}
*/
function makeVideoElement(id) {
let v = document.createElement('video');
v.id = 'video-' + id;
let container = document.getElementById('videos');
container.appendChild(v);
return v;
}
/**
* Find the video element that shows a given id.
*
* @parm {string} id
* @returns {HTMLVideoElement}
*/
function getVideoElement(id) {
let v = document.getElementById('video-' + id);
return /** @type{HTMLVideoElement} */(v);
}
/**
* Enable the camera and broadcast yourself to the group.
*
* @parm {ServerConnection} conn
*/
async function showCamera(conn) {
let ms = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
/* Send the new stream to the server */
let s = conn.newUpStream();
s.label = 'camera';
s.setStream(ms);
let v = makeVideoElement(s.localId);
s.onclose = function(replace) {
s.stream.getTracks().forEach(t => t.stop());
v.srcObject = null;
v.parentNode.removeChild(v);
}
function addTrack(t) {
t.oneneded = function(e) {
ms.onaddtrack = null;
s.onremovetrack = null;
s.close();
}
s.pc.addTransceiver(t, {
direction: 'sendonly',
streams: [ms],
});
}
// Make sure all future tracks are added.
s.onaddtrack = function(e) {
addTrack(e.track);
}
// Add any existing tracks.
ms.getTracks().forEach(addTrack);
// Connect the MediaStream to the video element and start playing.
v.srcObject = ms;
v.muted = true;
v.play();
}
/**
* Stop broadcasting.
*
* @parm {ServerConnection} conn
* @parm {Stream} s
*/
async function hide(conn, s) {
s.stream.getTracks().forEach(t => t.stop());
s.close();
}
/**
* Called when the server pushes a stream.
*
* @this {ServerConnection}
* @parm {Stream} c
*/
function onDownStream(s) {
s.onclose = function(replace) {
let v = getVideoElement(s.localId);
v.srcObject = null;
v.parentNode.removeChild(v);
}
s.ondowntrack = function(track, transceiver, stream) {
let v = getVideoElement(s.localId);
if(v.srcObject !== stream)
v.srcObject = stream;
}
let v = makeVideoElement(s.localId);
v.srcObject = s.stream;
v.play();
}
/**
* Display an error message.
*
* @parm {string} message
* @param {string} message
*/
function displayError(message) {
document.getElementById('error').textContent = message;
+1 -1
View File
@@ -23,7 +23,7 @@
<p id="errormessage"></p>
<div id="public-halls" class="groups">
<div id="public-halls" class="halls">
<h2>Public halls</h2>
<table id="public-halls-table"></table>
+2 -2
View File
@@ -10,10 +10,10 @@ body {
height: 12px;
}
.groups {
.halls {
}
.nogroups {
.nohalls {
display: none;
}
+21 -21
View File
@@ -23,17 +23,17 @@
document.getElementById('hallform').onsubmit = async function(e) {
e.preventDefault();
clearError();
let groupinput = document.getElementById('hall')
let hallInput = document.getElementById('hall')
let button = document.getElementById('submitbutton');
let group = groupinput.value.trim();
if(group === '')
let hall = hallInput.value.trim();
if(hall === '')
return;
let url = '/hall/' + group + '/';
let url = '/hall/' + hall + '/';
let statusUrl = url + '.status.json';
try {
groupinput.disabled = true;
hallInput.disabled = true;
button.disabled = true;
try {
let resp = await fetch(statusUrl, {
@@ -41,7 +41,7 @@ document.getElementById('hallform').onsubmit = async function(e) {
});
if(!resp.ok) {
if(resp.status === 404)
displayError('No such group');
displayError('No such hall');
else
displayError(`The server said: ${resp.status} ${resp.statusText}`);
return;
@@ -51,7 +51,7 @@ document.getElementById('hallform').onsubmit = async function(e) {
return;
}
} finally {
groupinput.disabled = false;
hallInput.disabled = false;
button.disabled = false;
}
@@ -90,38 +90,38 @@ async function listPublicHalls() {
l = await r.json();
} catch(e) {
table.textContent = `Couldn't fetch halls: ${e}`;
div.classList.remove('nogroups');
div.classList.add('groups');
div.classList.remove('nohalls');
div.classList.add('halls');
return;
}
if (l.length === 0) {
table.textContent = '(No halls found.)';
div.classList.remove('groups');
div.classList.add('nogroups');
div.classList.remove('halls');
div.classList.add('nohalls');
return;
}
div.classList.remove('nogroups');
div.classList.add('groups');
div.classList.remove('nohalls');
div.classList.add('halls');
for(let i = 0; i < l.length; i++) {
let group = l[i];
let hall = l[i];
let tr = document.createElement('tr');
let td = document.createElement('td');
let a = document.createElement('a');
a.textContent = group.displayName || group.name;
a.href = group.location;
a.textContent = hall.displayName || hall.name;
a.href = hall.location;
td.appendChild(a);
tr.appendChild(td);
let td2 = document.createElement('td');
if(group.description)
td2.textContent = group.description;
if(hall.description)
td2.textContent = hall.description;
tr.appendChild(td2);
let td3 = document.createElement('td');
if(!group.redirect) {
let locked = group.locked ? ', locked' : '';
td3.textContent = `(${group.clientCount} clients${locked})`;
if(!hall.redirect) {
let locked = hall.locked ? ', locked' : '';
td3.textContent = `(${hall.clientCount} clients${locked})`;
} else {
td3.textContent = '(remote)';
}
+55 -55
View File
@@ -154,133 +154,133 @@ async function updateObject(url, values, etag) {
}
/**
* listGroups returns the list of groups.
* listHalls returns the list of halls.
*
* @returns {Promise<Array<string>>}
*/
async function listGroups() {
async function listHalls() {
return await listObjects('/skald-api/v0/.halls/');
}
/**
* getGroup returns the sanitised description of the given group.
* getHall returns the sanitised description of the given hall.
*
* @param {string} group
* @param {string} hall
* @param {string} [etag]
* @returns {Promise<Object>}
*/
async function getGroup(group, etag) {
return await getObject(`/skald-api/v0/.halls/${group}`, etag);
async function getHall(hall, etag) {
return await getObject(`/skald-api/v0/.halls/${hall}`, etag);
}
/**
* createGroup creates a group. It fails if the group already exists.
* createHall creates a hall. It fails if the hall already exists.
*
* @param {string} group
* @param {string} hall
* @param {Object} [values]
*/
async function createGroup(group, values) {
return await createObject(`/skald-api/v0/.halls/${group}`, values);
async function createHall(hall, values) {
return await createObject(`/skald-api/v0/.halls/${hall}`, values);
}
/**
* deleteGroup deletes a group.
* deleteHall deletes a hall.
*
* @param {string} group
* @param {string} hall
* @param {string} [etag]
*/
async function deleteGroup(group, etag) {
return await deleteObject(`/skald-api/v0/.halls/${group}`, etag);
async function deleteHall(hall, etag) {
return await deleteObject(`/skald-api/v0/.halls/${hall}`, etag);
}
/**
* updateGroup modifies a hall definition.
* updateHall modifies a hall definition.
* Any fields present in values are overriden, any fields absent in values
* are left unchanged.
*
* @param {string} group
* @param {string} hall
* @param {Object} values
* @param {string} [etag]
*/
async function updateGroup(group, values, etag) {
return await updateObject(`/skald-api/v0/.halls/${group}`, values);
async function updateHall(hall, values, etag) {
return await updateObject(`/skald-api/v0/.halls/${hall}`, values);
}
/**
* listUsers lists the users in a given group.
* listUsers lists the users in a given hall.
*
* @param {string} group
* @param {string} hall
* @returns {Promise<Array<string>>}
*/
async function listUsers(group) {
return await listObjects(`/skald-api/v0/.halls/${group}/.users/`);
async function listUsers(hall) {
return await listObjects(`/skald-api/v0/.halls/${hall}/.users/`);
}
/**
* userURL returns the URL for a user entry
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {boolean} wildcard
*/
function userURL(group, user, wildcard) {
function userURL(hall, user, wildcard) {
if(wildcard)
return `/skald-api/v0/.halls/${group}/.wildcard-user`;
return `/skald-api/v0/.halls/${hall}/.wildcard-user`;
else if(user === "")
return `/skald-api/v0/.halls/${group}/.empty-user`;
return `/skald-api/v0/.halls/${hall}/.empty-user`;
else
return `/skald-api/v0/.halls/${group}/.users/${user}`
return `/skald-api/v0/.halls/${hall}/.users/${user}`
}
/**
* getUser returns a given user entry.
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {boolean} wildcard
* @param {string} [etag]
* @returns {Promise<Object>}
*/
async function getUser(group, user, wildcard, etag) {
return await getObject(userURL(group, user, wildcard), etag);
async function getUser(hall, user, wildcard, etag) {
return await getObject(userURL(hall, user, wildcard), etag);
}
/**
* createUser creates a new user entry. It fails if the user already
* exists.
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {boolean} wildcard
* @param {Object} values
*/
async function createUser(group, user, wildcard, values) {
return await createObject(userURL(group, user, wildcard), values);
async function createUser(hall, user, wildcard, values) {
return await createObject(userURL(hall, user, wildcard), values);
}
/**
* deleteUser deletes a user.
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {boolean} wildcard
* @param {string} [etag]
*/
async function deleteUser(group, user, wildcard, etag) {
return await deleteObject(userURL(group, user, wildcard), etag);
async function deleteUser(hall, user, wildcard, etag) {
return await deleteObject(userURL(hall, user, wildcard), etag);
}
/**
* updateUser modifies a given user entry.
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {Object} values
* @param {boolean} wildcard
* @param {string} [etag]
*/
async function updateUser(group, user, wildcard, values, etag) {
return await updateObject(userURL(group, user, wildcard), values, etag);
async function updateUser(hall, user, wildcard, values, etag) {
return await updateObject(userURL(hall, user, wildcard), values, etag);
}
/**
@@ -288,13 +288,13 @@ async function updateUser(group, user, wildcard, values, etag) {
* If oldpassword is provided, then it is used for authentication instead
* of the browser's normal mechanism.
*
* @param {string} group
* @param {string} hall
* @param {string} user
* @param {boolean} wildcard
* @param {string} password
* @param {string} [oldpassword]
*/
async function setPassword(group, user, wildcard, password, oldpassword) {
async function setPassword(hall, user, wildcard, password, oldpassword) {
let options = {
method: 'POST',
headers: {
@@ -308,42 +308,42 @@ async function setPassword(group, user, wildcard, password, oldpassword) {
`Basic ${btoa(user + ':' + oldpassword)}`
}
let r = await fetch(userURL(group, user, wildcard) + '/.password', options);
let r = await fetch(userURL(hall, user, wildcard) + '/.password', options);
if(!r.ok)
throw httpError(r);
}
/**
* listTokens lists the tokens for a given group.
* listTokens lists the tokens for a given hall.
*
* @param {string} group
* @param {string} hall
* @returns {Promise<Array<string>>}
*/
async function listTokens(group) {
return await listObjects(`/skald-api/v0/.halls/${group}/.tokens/`);
async function listTokens(hall) {
return await listObjects(`/skald-api/v0/.halls/${hall}/.tokens/`);
}
/**
* getToken returns a given token.
*
* @param {string} group
* @param {string} hall
* @param {string} token
* @param {string} [etag]
* @returns {Promise<Object>}
*/
async function getToken(group, token, etag) {
return await getObject(`/skald-api/v0/.halls/${group}/.tokens/${token}`,
async function getToken(hall, token, etag) {
return await getObject(`/skald-api/v0/.halls/${hall}/.tokens/${token}`,
etag);
}
/**
* createToken creates a new token and returns its name
*
* @param {string} group
* @param {string} hall
* @param {Object} template
* @returns {Promise<string>}
*/
async function createToken(group, template) {
async function createToken(hall, template) {
let options = {
method: 'POST',
headers: {
@@ -353,7 +353,7 @@ async function createToken(group, template) {
}
let r = await fetch(
`/skald-api/v0/.halls/${group}/.tokens/`,
`/skald-api/v0/.halls/${hall}/.tokens/`,
options);
if(!r.ok)
throw httpError(r);
@@ -366,13 +366,13 @@ async function createToken(group, template) {
/**
* updateToken modifies a token.
*
* @param {string} group
* @param {string} hall
* @param {Object} token
*/
async function updateToken(group, token, etag) {
async function updateToken(hall, token, etag) {
if(!token.token)
throw new Error("Unnamed token");
return await updateObject(
`/skald-api/v0/.halls/${group}/.tokens/${token.token}`,
`/skald-api/v0/.halls/${hall}/.tokens/${token.token}`,
token, etag);
}
+32 -32
View File
@@ -83,11 +83,11 @@ function ServerConnection() {
*/
this.id = newRandomId();
/**
* The group that we have joined, or null if we haven't joined yet.
* The hall that we have joined, or null if we haven't joined yet.
*
* @type {string}
*/
this.group = null;
this.hall = null;
/**
* The username we joined as.
*
@@ -95,7 +95,7 @@ function ServerConnection() {
*/
this.username = null;
/**
* The set of users in this group, including ourself.
* The set of users in this hall, including ourself.
*
* @type {Object<string,user>}
*/
@@ -186,19 +186,19 @@ function ServerConnection() {
*/
this.onpeerconnection = null;
/**
* onuser is called whenever a user in the group changes. The users
* onuser is called whenever a user in the hall changes. The users
* array has already been updated.
*
* @type{(this: ServerConnection, id: string, kind: string) => void}
*/
this.onuser = null;
/**
* onjoined is called whenever we join or leave a group or whenever the
* permissions we have in a group change.
* onjoined is called whenever we join or leave a hall or whenever the
* permissions we have in a hall change.
*
* kind is one of 'join', 'fail', 'change' or 'leave'.
*
* @type{(this: ServerConnection, kind: string, group: string, permissions: Array<string>, status: Object<string,any>, data: Object<string,any>, error: string, message: string) => void}
* @type{(this: ServerConnection, kind: string, hall: string, permissions: Array<string>, status: Object<string,any>, data: Object<string,any>, error: string, message: string) => void}
*/
this.onjoined = null;
/**
@@ -262,7 +262,7 @@ function ServerConnection() {
* @property {Array<string>} [permissions]
* @property {Object<string,any>} [status]
* @property {Object<string,any>} [data]
* @property {string} [group]
* @property {string} [hall]
* @property {unknown} [value]
* @property {boolean} [noecho]
* @property {string|number} [time]
@@ -365,9 +365,9 @@ ServerConnection.prototype.connect = function(url) {
if(sc.onuser)
sc.onuser.call(sc, id, 'delete');
}
if(sc.group && sc.onjoined)
sc.onjoined.call(sc, 'leave', sc.group, [], {}, {}, '', '');
sc.group = null;
if(sc.hall && sc.onjoined)
sc.onjoined.call(sc, 'leave', sc.hall, [], {}, {}, '', '');
sc.hall = null;
sc.username = null;
if(sc.pingHandler) {
clearInterval(sc.pingHandler);
@@ -432,19 +432,19 @@ ServerConnection.prototype.connect = function(url) {
sc.permissions = [];
sc.rtcConfiguration = null;
} else if(m.kind === 'join' || m.kind == 'change') {
if(m.kind === 'join' && sc.group) {
throw new Error('Joined multiple groups');
} else if(m.kind === 'change' && m.group != sc.group) {
console.warn('join(change) for inconsistent group');
if(m.kind === 'join' && sc.hall) {
throw new Error('Joined multiple halls');
} else if(m.kind === 'change' && m.hall != sc.hall) {
console.warn('join(change) for inconsistent hall');
break;
}
sc.group = m.group;
sc.hall = m.hall;
sc.username = m.username;
sc.permissions = m.permissions || [];
sc.rtcConfiguration = m.rtcConfiguration || null;
}
if(sc.onjoined)
sc.onjoined.call(sc, m.kind, m.group,
sc.onjoined.call(sc, m.kind, m.hall,
m.permissions || [],
m.status, m.data,
m.error || null, m.value || null);
@@ -546,19 +546,19 @@ function parseTime(value) {
}
/**
* join requests to join a group. The onjoined callback will be called
* join requests to join a hall. The onjoined callback will be called
* when we've effectively joined.
*
* @param {string} group - The name of the group to join.
* @param {string} hall - The name of the hall to join.
* @param {string} username - the username to join as.
* @param {string|Object} credentials - password or authServer.
* @param {Object<string,any>} [data] - the initial associated data.
*/
ServerConnection.prototype.join = async function(group, username, credentials, data) {
ServerConnection.prototype.join = async function(hall, username, credentials, data) {
let m = {
type: 'join',
kind: 'join',
group: group,
hall: hall,
};
if(typeof username !== 'undefined' && username !== null)
m.username = username;
@@ -629,16 +629,16 @@ ServerConnection.prototype.join = async function(group, username, credentials, d
};
/**
* leave leaves a group. The onjoined callback will be called when we've
* leave leaves a hall. The onjoined callback will be called when we've
* effectively left.
*
* @param {string} group - The name of the group to join.
* @param {string} hall - The name of the hall to join.
*/
ServerConnection.prototype.leave = function(group) {
ServerConnection.prototype.leave = function(hall) {
this.send({
type: 'join',
kind: 'leave',
group: group,
hall: hall,
});
};
@@ -646,8 +646,8 @@ ServerConnection.prototype.leave = function(group) {
* request sets the list of requested tracks
*
* @param {Object<string,Array<string>>} what
* - A dictionary that maps labels to a sequence of 'audio', 'video'
* or 'video-low. An entry with an empty label '' provides the default.
* - A dictionary that maps labels to a sequence of 'audio'
* . An entry with an empty label '' provides the default.
*/
ServerConnection.prototype.request = function(what) {
this.send({
@@ -809,14 +809,14 @@ ServerConnection.prototype.userMessage = function(kind, dest, value, noecho) {
};
/**
* groupAction sends a request to act on the current group.
* hallAction sends a request to act on the current hall.
*
* @param {string} kind
* @param {any} [data]
*/
ServerConnection.prototype.groupAction = function(kind, data) {
ServerConnection.prototype.hallAction = function(kind, data) {
this.send({
type: 'groupaction',
type: 'hallaction',
source: this.id,
kind: kind,
username: this.username,
@@ -1459,7 +1459,7 @@ Stream.prototype.restartIce = function () {
* request sets the list of tracks. If this is not called, or called with
* a null argument, then the default is provided by ServerConnection.request.
*
* @param {Array<string>} what - a sequence of 'audio', 'video' or 'video-low'.
* @param {Array<string>} what - a sequence of 'audio' .
*/
Stream.prototype.request = function(what) {
let c = this;
@@ -1501,7 +1501,7 @@ Stream.prototype.updateStats = async function() {
if(stid && r.type === 'outbound-rtp') {
let id = stid;
// Firefox doesn't implement rid, use ssrc
// to discriminate simulcast tracks.
// to discriminate RTP tracks.
id = id + '-' + r.ssrc;
if(!('bytesSent' in r))
continue;
-192
View File
@@ -466,17 +466,9 @@ textarea.form-reply {
white-space: pre-wrap;
}
.video-container {
height: calc(var(--vh, 1vh) * 100 - 56px);
position: relative;
background: rgba(0, 0, 0, 0.91);
/* Display only when showing video */
display: block;
}
.chat-btn {
display: block;
/*on top of video peers*/
z-index: 1002;
font-size: 1.8em;
position: absolute;
@@ -492,37 +484,9 @@ textarea.form-reply {
text-shadow: 0px 0px 1px #b3adad;
}
.collapse-video {
left: inherit;
right: 30px;
}
.video-controls, .top-video-controls {
position: absolute;
width: 100%;
left: 0;
bottom: 25px;
text-align: center;
color: #eaeaea;
font-size: 1.1em;
opacity: 0;
height: 32px;
}
.video-controls:after, .top-video-controls:after {
clear: both;
display: table;
content: " ";
}
.top-video-controls {
text-align: right;
bottom: inherit;
top: 0;
line-height: 1.1;
font-size: 1.3em;
text-shadow: 1px 1px 2px rgb(90 86 86);
}
.controls-button {
padding: 3px 10px;
@@ -544,54 +508,14 @@ textarea.form-reply {
background: linear-gradient(180deg, rgb(0 0 0 / 20%) 0%, rgb(0 0 0 / 50%) 0%, rgb(0 0 0 / 70%) 100%);
}
.peer:hover > .video-controls, .peer:hover > .top-video-controls {
opacity: 1;
}
.video-controls span, .top-video-controls span {
margin-right: 20px;
transition: opacity .7s ease-out;
opacity: 1;
cursor: pointer;
}
.video-controls span:last-child, .top-video-controls span:last-child {
margin-right: 0;
}
.video-controls span:hover, .top-video-controls span:hover {
opacity: .8;
transition: opacity .5s ease-out;
}
.top-video-controls .video-stop{
display: flex;
width: 1.5em;
height: 1.5em;
background: rgba(0,0,0,0.5);
border-radius: 50%;
justify-content: center;
align-items: center;
color: #eaeaea;
}
.video-controls .volume {
display: inline-block;
text-align: center;
}
.video-controls .video-play {
font-size: 0.85em;
}
.video-controls .video-stop {
color: #d03e3e;
}
.video-controls span.disabled, .video-controls span.disabled:hover, .top-video-controls span.disabled:hover{
opacity: .2;
color: #c8c8c8
}
.volume-mute {
vertical-align: middle;
@@ -609,10 +533,6 @@ textarea.form-reply {
transition: opacity .5s ease-out;
}
.video-controls .volume:hover {
--ov: 1;
--dv: inline;
}
.mobile-container {
display: block !important;
@@ -794,38 +714,16 @@ h1 {
width: 5.8em;
}
#videoselect {
text-align-last: center;
margin-right: 0.4em;
}
#audioselect {
text-align-last: center;
}
#sharebutton, #unsharebutton {
white-space: nowrap;
}
#unsharebutton {
margin-right: 0.4em;
}
#filterselect {
text-align-last: center;
margin-right: 0.4em;
}
#sendselect {
text-align-last: center;
margin-right: 0.4em;
}
#simulcastselect {
text-align-last: center;
margin-right: 0.4em;
}
#requestselect {
text-align-last: center;
}
@@ -910,58 +808,6 @@ h1 {
cursor: ew-resize;
}
#peers {
padding: 10px;
display: grid;
grid-template-columns: repeat(1, 1fr);
grid-template-rows: repeat(1, auto);
row-gap: 5px;
column-gap: 10px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
z-index: 1000;
background-size: cover;
overflow: hidden;
vertical-align: top!important;
}
.peer {
margin-top: auto;
margin-bottom: auto;
position: relative;
border: 2px solid rgba(0,0,0,0);
background: #80808014;
}
.peer-active {
border: 2px solid #610a86;
}
.peer-hidden {
display: none;
}
.media {
width: 100%;
max-height: calc(var(--vh, 1vh) * 100 - 76px);
padding-bottom: 20px;
object-fit: contain;
}
.media-failed {
filter: grayscale(0.5) contrast(0.5);
}
.mirror {
transform: scaleX(-1);
}
#inputform {
width: 100%;
}
@@ -1066,20 +912,6 @@ legend {
list-style: none;
}
.show-video {
position: absolute;
right: 30px;
bottom: 120px;
color: white;
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 150%;
border-radius: 30px;
background: #600aa0;
box-shadow: 4px 4px 7px 1px rgba(0,0,0,0.16);
}
.blink {
-ms-animation: blink 1.0s linear infinite;
@@ -1210,9 +1042,6 @@ header .collapse:hover {
content: "\f130";
}
#users > div.user-status-camera::after {
content: "\f030";
}
.close-icon {
font: normal 1em/1 Arial, sans-serif;
@@ -1223,11 +1052,6 @@ header .collapse:hover {
/* END Sidebar Left */
@media only screen and (min-width: 1025px) {
.coln-right .collapse-video, .coln-right .show-video {
display: none;
}
}
@media only screen and (max-width: 1024px) {
#presentbutton, #unpresentbutton {
@@ -1259,18 +1083,7 @@ header .collapse:hover {
display: none;
}
.video-container {
position: fixed;
height: calc(var(--vh, 1vh) * 100 - 56px);
top: 56px;
right: 0;
left: 0;
margin-bottom: 60px;
}
.top-video-controls {
opacity: 1;
}
.login-container {
position: fixed;
@@ -1304,7 +1117,6 @@ header .collapse:hover {
#left-sidebar.active {
min-width: 200px;
max-width: 200px;
/* on top of video peers */
z-index: 1002;
}
@@ -1336,10 +1148,6 @@ header .collapse:hover {
line-height: 36px;
}
#peers {
padding: 3px;
}
#resizer {
display: none;
}
+6 -95
View File
@@ -37,12 +37,12 @@
<ul class="nav-menu">
<li>
<button id="presentbutton" class="invisible btn btn-success" aria-label="Enable camera and microphone">
<button id="presentbutton" class="invisible btn btn-success" aria-label="Enable microphone">
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text"> Enable</span>
</button>
</li>
<li>
<button id="unpresentbutton" class="invisible btn btn-cancel" aria-label="Disable camera and microphone">
<button id="unpresentbutton" class="invisible btn btn-cancel" aria-label="Disable microphone">
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text"> Disable</span>
</button>
</li>
@@ -52,12 +52,7 @@
<span class="nav-text">Mute</span>
</button>
</li>
<li>
<button id="sharebutton" class="invisible nav-link nav-button" aria-label="Share screen">
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
<span class="nav-text">Share Screen</span>
</button>
</li>
<li>
<button class="nav-button nav-link nav-more" id="openside" aria-label="More options" aria-haspopup="true">
<span><i class="fas fa-ellipsis-v" aria-hidden="true"></i></span>
@@ -87,20 +82,9 @@
</div>
<div id="resizer" tabindex="0" role="separator" aria-label="Chat resize handle" aria-orientation="vertical"></div>
<div class="coln-right" id="right">
<button class="show-video blink invisible" id="show-video" aria-label="Show video">
<i class="fas fa-exchange-alt" aria-hidden="true"></i>
</button>
<button class="chat-btn show-chat invisible" id="show-chat" aria-label="Show chat">
<i class="far fa-comment-alt icon-chat" aria-hidden="true"></i>
</button>
<button class="chat-btn collapse-video invisible" id="collapse-video" aria-label="Hide video and show chat">
<i class="far fa-comment-alt icon-chat" aria-hidden="true"></i>
</button>
<div class="video-container invisible" id="video-container">
<div id="expand-video" class="expand-video">
<div id="peers"></div>
</div>
</div>
<div id="captions-container" class="invisible">
<div id="captions"></div>
</div>
@@ -128,10 +112,6 @@
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
<label for="presentmike">Microphone</label>
</p>
<p class="switch-radio">
<input id="presentboth" type="radio" name="presentradio" value="both"/>
<label for="presentboth">Camera and microphone</label>
</p>
</div>
</fieldset>
<div class="clear"></div>
@@ -175,25 +155,10 @@
<div id="mediaoptions" class="invisible">
<fieldset>
<legend>Media Options</legend>
<label for="videoselect" class="sidenav-label-first">Camera:</label>
<select id="videoselect" class="select select-inline">
<option value="">off</option>
</select>
<label for="audioselect" class="sidenav-label">Microphone:</label>
<label for="audioselect" class="sidenav-label-first">Microphone:</label>
<select id="audioselect" class="select select-inline">
<option value="">off</option>
</select>
<form>
<input id="mirrorbox" type="checkbox" checked/>
<label for="mirrorbox">Mirror view</label>
</form>
<form>
<input id="blackboardbox" type="checkbox"/>
<label for="blackboardbox">Blackboard mode</label>
</form>
<form>
<input id="preprocessingbox" type="checkbox" checked/>
@@ -211,13 +176,6 @@
<fieldset>
<legend>Other Settings</legend>
<form id="filterform">
<label for="filterselect" class="sidenav-label-first">Filter:</label>
<select id="filterselect" class="select select-inline">
<option value="" selected>none</option>
</select>
</form>
<form id="sendform">
<label for="sendselect" class="sidenav-label-first">Send:</label>
<select id="sendselect" class="select select-inline">
@@ -228,23 +186,11 @@
</select>
</form>
<form id="simulcastform">
<label for="simulcastselect" class="sidenav-label-first">Simulcast:</label>
<select id="simulcastselect" class="select select-inline">
<option value="off">off</option>
<option value="auto" selected>auto</option>
<option value="on">on</option>
</select>
</form>
<form id="requestform">
<label for="requestselect" class="sidenav-label">Receive:</label>
<select id="requestselect" class="select select-inline">
<option value="">nothing</option>
<option value="audio">audio only</option>
<option value="screenshare">screenshare only</option>
<option value="everything-low">low quality</option>
<option value="everything" selected>everything</option>
<option value="audio" selected>audio only</option>
</select>
</form>
@@ -252,46 +198,11 @@
<input id="activitybox" type="checkbox"/>
<label for="activitybox">Activity detection</label>
</form>
<form>
<input id="displayallbox" type="checkbox"/>
<label for="displayallbox">Display audio-only users</label>
</form>
</fieldset>
</div>
</aside>
<div id="videocontrols-template" class="invisible">
<div class="video-controls vc-overlay">
<div class="controls-button controls-left">
<button class="video-play" aria-label="Play video">
<i class="fas fa-play" aria-hidden="true"></i>
</button>
<span class="volume">
<button class="volume-mute" aria-label="Mute volume">
<i class="fas fa-volume-up" aria-hidden="true"></i>
</button>
<input class="volume-slider" type="range" max="100" value="100" min="0" step="5" aria-label="Volume">
</span>
</div>
<div class="controls-button controls-right">
<button class="pip" aria-label="Picture in picture">
<i class="far fa-clone" aria-hidden="true"></i>
</button>
<button class="fullscreen" aria-label="Fullscreen">
<i class="fas fa-expand" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div id="topvideocontrols-template" class="invisible">
<div class="top-video-controls">
<div class="controls-button controls-right">
<button class="close-icon video-stop" aria-label="Stop video">
</button>
</div>
</div>
</div>
<dialog id="invite-dialog">
<form method="dialog">
+115 -1319
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -36,23 +36,23 @@ async function listStats() {
}
if(l.length === 0) {
table.textContent = '(No group found.)';
table.textContent = '(No hall found.)';
return;
}
for(let i = 0; i < l.length; i++)
formatGroup(table, l[i]);
formatHall(table, l[i]);
}
function formatGroup(table, group) {
function formatHall(table, hall) {
let tr = document.createElement('tr');
let td = document.createElement('td');
td.textContent = group.name;
td.textContent = hall.name;
tr.appendChild(td);
table.appendChild(tr);
if(group.clients) {
for(let i = 0; i < group.clients.length; i++) {
let client = group.clients[i];
if(hall.clients) {
for(let i = 0; i < hall.clients.length; i++) {
let client = hall.clients[i];
let tr2 = document.createElement('tr');
tr2.appendChild(document.createElement('td'));
let td2 = document.createElement('td');