Latest code. Working voice, still needs some ui cleanup and accessibility fixes but in decent shape.

This commit is contained in:
Storm Dragon
2026-05-19 21:47:31 -04:00
parent 01ce7529eb
commit 6275e3adef
19 changed files with 855 additions and 775 deletions
+6 -4
View File
@@ -70,7 +70,8 @@
</button>
<h2 id="chat-history-heading" class="sr-only">Chat History</h2>
<div id="box"></div>
<div id="chat-announcements" aria-live="polite" aria-atomic="true" class="sr-only"></div>
<div id="chat-announcements" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
<div id="audio-streams" aria-hidden="true"></div>
<div class="reply">
<form id="inputform">
<textarea id="input" class="form-reply" aria-label="Message or /command"></textarea>
@@ -105,11 +106,11 @@
<legend>Enable at start:</legend>
<div class="present-switch">
<p class="switch-radio">
<input id="presentoff" type="radio" name="presentradio" value="" checked/>
<input id="presentoff" type="radio" name="presentradio" value=""/>
<label for="presentoff">Nothing</label>
</p>
<p class="switch-radio">
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
<input id="presentmike" type="radio" name="presentradio" value="mike" checked/>
<label for="presentmike">Microphone</label>
</p>
</div>
@@ -157,7 +158,8 @@
<legend>Media Options</legend>
<label for="audioselect" class="sidenav-label-first">Microphone:</label>
<select id="audioselect" class="select select-inline">
<option value="">off</option>
<option value="default">default microphone</option>
<option value="off">off</option>
</select>
<form>
+83 -66
View File
@@ -800,7 +800,7 @@ let mediaChoicesDone = false;
* Populate the media choices menu.
*
* Since media names might not be available before we call
* getDisplayMedia, we call this function twice, the second time in order
* getUserMedia, we call this function twice, the second time in order
* to update the menu with user-readable labels.
*
* @param{boolean} done
@@ -818,10 +818,17 @@ async function setMediaChoices(done) {
return;
}
let cn = 1, mn = 1;
let mn = 1;
devices.forEach(d => {
let label = d.label;
if(d.kind === 'audioinput' && d.deviceId) {
if(!label)
label = `Microphone ${mn}`;
addSelectOption(getSelectElement('audioselect'),
label, d.deviceId);
mn++;
}
});
mediaChoicesDone = done;
@@ -1031,9 +1038,18 @@ async function addLocalMedia(localId) {
let settings = getSettings();
/** @type{boolean|MediaTrackConstraints} */
let audio = settings.audio ? {deviceId: settings.audio} : false;
let audio = false;
if(settings.audio === 'default') {
audio = true;
} else if(settings.audio && settings.audio !== 'off') {
audio = {deviceId: settings.audio};
}
if(!audio) {
displayMessage('Please choose a microphone before enabling audio.');
return;
}
if(audio) {
if(audio !== true) {
if(!settings.preprocessing) {
audio.echoCancellation = false;
audio.noiseSuppression = false;
@@ -1080,17 +1096,6 @@ async function addLocalMedia(localId) {
}
setButtonsVisibility();
}
/**
* @param {File} file
*/
async function addFileMedia(file) {
displayError('Local file playback is not supported in audio-only mode');
}
/**
* @param {MediaStream} s
*/
@@ -1182,6 +1187,26 @@ async function setMedia(c) {
if(!stream)
return;
c.userdata.media = stream;
let media = /** @type {HTMLAudioElement} */
(document.getElementById('media-' + c.localId));
if(!media) {
media = document.createElement('audio');
media.id = 'media-' + c.localId;
media.autoplay = true;
media.muted = !!c.up;
media.classList.add('media');
let container = document.getElementById('audio-streams');
if(!container)
throw new Error("Couldn't find audio-streams");
container.appendChild(media);
}
if(media.srcObject !== stream)
media.srcObject = stream;
setMediaStatus(c);
}
@@ -1201,8 +1226,13 @@ function showHideMedia(c, elt) {
* @param {Stream} c
*/
function resetMedia(c) {
// Audio-only: no media elements to reset
return;
let media = /** @type {HTMLAudioElement} */
(document.getElementById('media-' + c.localId));
if(!media) {
console.error("Resetting unknown media element")
return;
}
media.srcObject = media.srcObject;
}
/**
@@ -1254,6 +1284,12 @@ function setVolumeButton(muted, button, slider) {
* @param {string} localId
*/
function delMedia(localId) {
let media = document.getElementById('media-' + localId);
if(media) {
if(media instanceof HTMLMediaElement)
media.srcObject = null;
media.remove();
}
setButtonsVisibility();
}
@@ -1274,12 +1310,20 @@ function setMediaStatus(c) {
}
if(good) {
media.classList.remove('media-failed');
if(c.userdata.play) {
if(media instanceof HTMLMediaElement)
if(media instanceof HTMLMediaElement) {
if(!c.up && !c.userdata.playing) {
c.userdata.playing = true;
media.play().catch(e => {
c.userdata.playing = false;
console.error(e);
displayError(e);
});
} else if(c.userdata.play) {
media.play().catch(e => {
console.error(e);
displayError(e);
});
}
delete(c.userdata.play);
}
} else {
@@ -1500,8 +1544,6 @@ function userMenu(elt) {
inviteMenu();
}});
}
if(serverConnection.permissions.indexOf('present') >= 0 && canFile())
items.push({label: 'Broadcast file', onClick: presentFile});
items.push({label: 'Restart media', onClick: renegotiateStreams});
} else {
items.push({label: 'Send file', onClick: () => {
@@ -2239,6 +2281,20 @@ function formatTime(time) {
/** @type {lastMessage} */
let lastMessage = {};
/**
* @param {string} text
*/
function announceChat(text) {
let announcement = document.getElementById('chat-announcements');
if(!announcement)
return;
announcement.textContent = '';
window.setTimeout(() => {
announcement.textContent = text;
}, 20);
}
/**
* @param {string} id
* @param {string} peerId
@@ -2359,11 +2415,11 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
// Announce new messages to screen readers (but not history)
if(!history) {
let announcement = document.getElementById('chat-announcements');
if(announcement) {
let messageText = typeof message === 'string' ? message : body.textContent;
announcement.textContent = nick ? `${nick}: ${messageText}` : messageText;
}
let messageText = typeof message === 'string' ? message : body.textContent;
let speaker = nick;
if(serverConnection && peerId === serverConnection.id)
speaker = 'You';
announceChat(speaker ? `${speaker}: ${messageText}` : messageText);
}
return;
@@ -3014,45 +3070,6 @@ commands.unraise = {
}
}
/** @returns {boolean} */
function canFile() {
return false;
}
function presentFile() {
let input = document.createElement('input');
input.type = 'file';
input.accept="audio/*";
input.onchange = function(e) {
if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this');
let files = this.files;
for(let i = 0; i < files.length; i++) {
addFileMedia(files[i]).catch(e => {
console.error(e);
displayError(e);
});
}
};
input.click();
}
commands.presentfile = {
description: 'broadcast an audio file',
f: (c, r) => {
presentFile();
},
predicate: () => {
if(!canFile())
return 'Your browser does not support presenting arbitrary files';
if(!serverConnection || !serverConnection.permissions ||
serverConnection.permissions.indexOf('present') < 0)
return 'You are not authorised to present.';
return null;
}
};
/**
* @param {string} id
*/
@@ -3355,7 +3372,7 @@ document.getElementById('loginform').onsubmit = async function(e) {
presentRequested = 'mike';
else
presentRequested = null;
getInputElement('presentoff').checked = true;
getInputElement('presentmike').checked = true;
// Connect to the server, gotConnected will join.
serverConnect();
-9
View File
@@ -99,15 +99,6 @@ function formatTrack(table, track) {
tr.appendChild(document.createElement('td'));
tr.appendChild(document.createElement('td'));
let td = document.createElement('td');
let layer = '';
if(track.sid || track.maxSid)
layer = layer + `s${track.sid}/${track.maxSid}`;
if(track.tid || track.maxTid) {
if(layer !== '')
layer = layer + '+';
layer = layer + `t${track.tid}/${track.maxTid}`;
}
td.textContent = layer;
tr.appendChild(td);
let td2 = document.createElement('td');
if(track.maxBitrate)