diff --git a/package-lock.json b/package-lock.json
index e32f2e4c..21c1b757 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,7 @@
"he": "^1.2.0",
"helmet": "^8.1.0",
"httpolyglot": "0.1.2",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"jsdom": "^27.2.0",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.10",
@@ -3538,9 +3538,9 @@
}
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
diff --git a/package.json b/package.json
index 42aafd21..9042a903 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"httpolyglot": "0.1.2",
"jsdom": "^27.2.0",
"jsonwebtoken": "^9.0.2",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"nodemailer": "^7.0.10",
"openai": "^6.8.1",
"qs": "^6.14.0",
diff --git a/public/js/client.js b/public/js/client.js
index dc3de158..768136dd 100644
--- a/public/js/client.js
+++ b/public/js/client.js
@@ -767,8 +767,8 @@ function setButtonsToolTip() {
setTippy(captionClean, 'Clean the messages', 'bottom');
setTippy(captionSaveBtn, 'Save the messages', 'bottom');
setTippy(speechRecognitionIcon, 'Status', 'bottom');
- setTippy(speechRecognitionStart, 'Start', 'top');
- setTippy(speechRecognitionStop, 'Stop', 'top');
+ setTippy(speechRecognitionStart, 'Start caption', 'top');
+ setTippy(speechRecognitionStop, 'Stop caption', 'top');
// Settings
setTippy(mySettingsCloseBtn, 'Close', 'bottom');
setTippy(myPeerNameSetBtn, 'Change name', 'top');
@@ -2246,6 +2246,9 @@ async function handleAddPeer(config) {
await wbUpdate();
playSound('addPeer');
+
+ // Screen reader announcement for peer joined
+ screenReaderAccessibility.announceMessage(`${peer_name} joined the room`);
}
/**
@@ -2750,6 +2753,10 @@ function handleRemovePeer(config) {
playSound('removePeer');
+ // Screen reader announcement for peer left
+ const peer_name = allPeers && allPeers[peer_id] ? allPeers[peer_id]['peer_name'] : 'Participant';
+ screenReaderAccessibility.announceMessage(`${peer_name} left the room`);
+
console.log('ALL PEERS', allPeers);
}
@@ -5204,10 +5211,12 @@ function setShareRoomBtn() {
shareRoomBtn.addEventListener('mouseenter', () => {
if (isMobileDevice || !buttons.main.showShareQr) return;
elemDisplay(qrRoomPopupContainer, true);
+ screenReaderAccessibility.announceMessage('Room QR code displayed');
});
shareRoomBtn.addEventListener('mouseleave', () => {
if (isMobileDevice || !buttons.main.showShareQr) return;
elemDisplay(qrRoomPopupContainer, false);
+ screenReaderAccessibility.announceMessage('Room QR code hidden');
});
}
@@ -7273,6 +7282,9 @@ function handleAudio(e, init, force = null) {
}
setMyAudioStatus(myAudioStatus);
+
+ // Screen reader announcement
+ screenReaderAccessibility.announceMessage(audioStatus ? 'Microphone on' : 'Microphone off');
}
/**
@@ -7340,6 +7352,9 @@ async function handleVideo(e, init, force = null) {
}
setMyVideoStatus(videoStatus);
+
+ // Screen reader announcement
+ screenReaderAccessibility.announceMessage(videoStatus ? 'Camera on' : 'Camera off');
}
/**
@@ -7502,6 +7517,11 @@ async function toggleScreenSharing(init = false) {
}
if (!useVideo) initVideoContainerShow(true);
}
+
+ // Screen reader announcement for starting screen share
+ if (!init) {
+ screenReaderAccessibility.announceMessage('Screen sharing started');
+ }
} else {
// STOP screen sharing
const myScreenWrap = getId('myScreenWrap');
@@ -7535,6 +7555,9 @@ async function toggleScreenSharing(init = false) {
} catch (_) {}
await emitPeerStatus('screen', false, {});
await refreshMyStreamToPeers(undefined, true);
+
+ // Screen reader announcement for stopping screen share
+ screenReaderAccessibility.announceMessage('Screen sharing stopped');
}
// Update init preview when stopping during init
@@ -7568,6 +7591,10 @@ async function toggleScreenSharing(init = false) {
elemDisplay(myVideo, false);
elemDisplay(myVideoAvatarImage, true, 'block');
}
+
+ screenReaderAccessibility.announceMessage(
+ isScreenStreaming ? 'Screen sharing started' : 'Screen sharing stopped'
+ );
} catch (err) {
if (err && err.name === 'NotAllowedError') {
console.error('Screen sharing permission was denied by the user.');
@@ -7704,7 +7731,9 @@ function toggleFullScreen() {
isDocumentOnFullScreen = false;
}
}
- setTippy(fullScreenBtn, isDocumentOnFullScreen ? 'Exit full screen' : 'View full screen', placement);
+ const fullScreenLabel = isDocumentOnFullScreen ? 'Exit full screen' : 'View full screen';
+ setTippy(fullScreenBtn, fullScreenLabel, placement);
+ screenReaderAccessibility.announceMessage(fullScreenLabel);
}
/**
@@ -8226,6 +8255,7 @@ function handleMediaRecorderStart(event) {
if (isMobileDevice) elemDisplay(swapCameraBtn, false);
recStartTs = performance.now();
playSound('recStart');
+ screenReaderAccessibility.announceMessage('Recording started');
}
/**
@@ -8262,6 +8292,7 @@ function handleMediaRecorderStop(event) {
if (isMobileDevice) elemDisplay(swapCameraBtn, true, 'block');
playSound('recStop');
+ screenReaderAccessibility.announceMessage('Recording stopped');
}
/**
@@ -8453,6 +8484,7 @@ function showChatRoomDraggable() {
}
setTippy(chatRoomBtn, 'Close the chat', bottomButtonsPlacement);
+ screenReaderAccessibility.announceMessage('Chat opened');
}
/**
@@ -8475,6 +8507,7 @@ function showCaptionDraggable() {
}
setTippy(captionBtn, 'Close the caption', placement);
+ screenReaderAccessibility.announceMessage('Caption opened');
}
/**
@@ -8799,6 +8832,7 @@ function hideChatRoomAndEmojiPicker() {
isChatRoomVisible = false;
isChatEmojiVisible = false;
setTippy(chatRoomBtn, 'Open the chat', bottomButtonsPlacement);
+ screenReaderAccessibility.announceMessage('Chat closed');
}
/**
@@ -8879,6 +8913,11 @@ function handleDataChannelChat(dataMessage) {
setPeerChatAvatarImgName('left', msgFrom, msgFromAvatar);
appendMessage(msgFrom, leftChatAvatar, 'left', msg, msgPrivate, msgId, msgFrom);
speechInMessages ? speechMessage(true, msgFrom, msg) : playSound('chatMessage');
+
+ // Screen reader announcement for incoming chat message
+ if (!speechInMessages) {
+ screenReaderAccessibility.announceMessage(`New message from ${msgFrom}`);
+ }
}
/**
@@ -9674,12 +9713,14 @@ function hideShowMySettings() {
setTippy(mySettingsBtn, 'Close the settings', bottomButtonsPlacement);
isMySettingsVisible = true;
videoMediaContainer.style.opacity = 0.3;
+ screenReaderAccessibility.announceMessage('Settings opened');
return;
}
elemDisplay(mySettings, false);
setTippy(mySettingsBtn, 'Open the settings', bottomButtonsPlacement);
isMySettingsVisible = false;
videoMediaContainer.style.opacity = 1;
+ screenReaderAccessibility.announceMessage('Settings closed');
}
/**
@@ -9814,6 +9855,9 @@ function handleHideMe(isHideMeActive) {
playSound('on');
}
resizeVideoMedia();
+ screenReaderAccessibility.announceMessage(
+ isHideMeActive ? 'You are hidden from the room' : 'You are visible in the room'
+ );
}
/**
@@ -9847,9 +9891,11 @@ function setMyAudioStatus(status) {
myAudioStatusIcon.className = audioClassName;
// send my audio status to all peers in the room
emitPeerStatus('audio', status);
- setTippy(myAudioStatusIcon, status ? 'My audio is on' : 'My audio is off', 'bottom');
+ const audioStatusLabel = status ? 'My audio is on' : 'My audio is off';
+ setTippy(myAudioStatusIcon, audioStatusLabel, 'bottom');
setTippy(audioBtn, status ? 'Stop the audio' : 'Start the audio', bottomButtonsPlacement);
status ? playSound('on') : playSound('off');
+ screenReaderAccessibility.announceMessage(audioStatusLabel);
}
/**
@@ -9871,8 +9917,10 @@ function setMyVideoStatus(status) {
// send my video status to all peers in the room
emitPeerStatus('video', status);
+ const videoStatusLabel = status ? 'My video is on' : 'My video is off';
+
if (!isMobileDevice) {
- if (myVideoStatusIcon) setTippy(myVideoStatusIcon, status ? 'My video is on' : 'My video is off', 'bottom');
+ if (myVideoStatusIcon) setTippy(myVideoStatusIcon, videoStatusLabel, 'bottom');
setTippy(videoBtn, status ? 'Stop the video' : 'Start the video', bottomButtonsPlacement);
}
@@ -9889,6 +9937,8 @@ function setMyVideoStatus(status) {
]);
playSound('off');
}
+
+ screenReaderAccessibility.announceMessage(videoStatusLabel);
}
/**
@@ -10661,12 +10711,14 @@ function handleRoomStatus(config) {
elemDisplay(lockRoomBtn, false);
elemDisplay(unlockRoomBtn, true);
isRoomLocked = true;
+ screenReaderAccessibility.announceMessage(`${peer_name} locked the room`);
break;
case 'unlock':
userLog('toast', `${icons.user} ${peer_name} \n has 🔓 UNLOCKED the room`, 'top-end');
elemDisplay(unlockRoomBtn, false);
elemDisplay(lockRoomBtn, true);
isRoomLocked = false;
+ screenReaderAccessibility.announceMessage(`${peer_name} unlocked the room`);
break;
case 'checkPassword':
isRoomLocked = true;
@@ -10779,6 +10831,7 @@ function toggleWhiteboard() {
whiteboard.style.top = '50%';
whiteboard.style.left = '50%';
wbIsOpen = !wbIsOpen;
+ screenReaderAccessibility.announceMessage(wbIsOpen ? 'Whiteboard opened' : 'Whiteboard closed');
}
/**
@@ -11774,6 +11827,9 @@ function sendFileInformations(file, peer_id, broadcast = false) {
setTimeout(() => {
sendFileData(peer_id, broadcast);
}, 1000);
+
+ // Screen reader announcement for file sharing
+ screenReaderAccessibility.announceMessage(`Sending file ${fileToSend.name}`);
} else {
userLog('error', 'File dragged not valid or empty.');
}
diff --git a/public/js/screenReader.js b/public/js/screenReader.js
new file mode 100644
index 00000000..f36823c0
--- /dev/null
+++ b/public/js/screenReader.js
@@ -0,0 +1,74 @@
+'use strict';
+
+class ScreenReaderAccessibility {
+ constructor() {
+ this.liveRegion = null;
+ this.initialized = false;
+ }
+
+ /**
+ * Initialize screen reader accessibility
+ */
+ init() {
+ if (this.initialized) return;
+
+ // Create ARIA live region for announcements
+ this.createLiveRegion();
+
+ this.initialized = true;
+ }
+
+ /**
+ * Create ARIA live region for screen reader announcements
+ */
+ createLiveRegion() {
+ if (!this.liveRegion) {
+ this.liveRegion = document.createElement('div');
+ this.liveRegion.setAttribute('role', 'status');
+ this.liveRegion.setAttribute('aria-live', 'polite');
+ this.liveRegion.setAttribute('aria-atomic', 'true');
+ this.liveRegion.style.position = 'absolute';
+ this.liveRegion.style.left = '-10000px';
+ this.liveRegion.style.width = '1px';
+ this.liveRegion.style.height = '1px';
+ this.liveRegion.style.overflow = 'hidden';
+ document.body.appendChild(this.liveRegion);
+ }
+ }
+
+ /**
+ * Announce a message to screen readers
+ * @param {string} message Message to announce
+ */
+ announce(message) {
+ if (!this.liveRegion) return;
+ this.liveRegion.textContent = '';
+ setTimeout(() => {
+ this.liveRegion.textContent = message;
+ }, 100);
+ }
+
+ /**
+ * Announce a generic message
+ * @param {string} message Message to announce
+ * @param {string} priority 'polite' or 'assertive'
+ * polite: Default priority for non-urgent messages.
+ * assertive: Higher priority for urgent messages that require immediate attention.
+ */
+ announceMessage(message, priority = 'polite') {
+ this.liveRegion.setAttribute('aria-live', priority);
+ this.announce(message);
+ }
+}
+
+// Create global instance
+const screenReaderAccessibility = new ScreenReaderAccessibility();
+
+// Auto-initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ screenReaderAccessibility.init();
+ });
+} else {
+ screenReaderAccessibility.init();
+}
diff --git a/public/views/client.html b/public/views/client.html
index 51d33bf2..1e849062 100755
--- a/public/views/client.html
+++ b/public/views/client.html
@@ -1049,6 +1049,7 @@ access to use this app.
+