问题描述
我正在尝试为WebRTC视频聊天构建可访问的音频指示器。它基本上应该显示您正在讲话时的声音大小。这是隔离的代码(对于CodesandBox,您需要安装styled-components
)。
import React,{ useEffect,useRef,useState } from "react";
import styled,{ css } from "styled-components";
import "./styles.css";
function useInterval(callback,delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
},[callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick,delay);
return () => clearInterval(id);
}
},[delay]);
}
const VolumeMeterWrapper = styled.div`
align-items: center;
justify-content: center;
background: rgba(0,0.75);
display: flex;
height: 32px;
width: 32px;
`;
const VolumeBar = styled.div`
background: #25a968;
border-radius: 1px;
height: 4px;
margin: 0 2px;
width: 8px;
transition: all 0.2s linear;
transform: ${({ level }) => css`scaleY(${level + 1})`};
`;
function VolumeMeter({ level }) {
return (
<VolumeMeterWrapper>
<VolumeBar level={level > 3 ? ((level - 2) * 5) / 4 : 0} />
<VolumeBar level={level} />
<VolumeBar level={level > 3 ? ((level - 2) * 5) / 4 : 0} />
</VolumeMeterWrapper>
);
}
export default function App() {
const [level,setLevel] = useState(0);
const [count,setCount] = useState(0);
useInterval(() => {
setCount((c) => c + 1);
if (count % 10 > 4) {
setLevel((l) => l - 1);
} else {
setLevel((l) => l + 1);
}
},200);
return (
<div className="App">
<h1>Hello CodeSandBox</h1>
<h2>Start editing to see some magic happen!</h2>
Level: {level}
<VolumeMeter level={level} />
</div>
);
}
在此示例中,您没有看到真实的音频输入。 “级别”伪造了该人会说话的声音。
您将如何使此类访问变得容易?您甚至需要这样做吗(因为检查各种提供程序的UI并没有显示任何特殊标签或aria标签)?
解决方法
前言
这很有趣!
首先道歉,该示例有点混乱,因为我尝试在过程的几个部分中为您提供选择,如果没有任何意义,请告诉我!
应该很容易整理并变成可重用的组件。
答案
宣布的实际部分很简单,您只需要在页面上放置visually hidden的aria-live
段落,并更新其中的文本即可。
<p class="visually-hidden" aria-live="assertive">
//update the text in here and it will be announced.
</p>
更难的是要获得良好的屏幕阅读器界面和播音员体验。
我如何处理公告
我最终要做的是获取一段时间内的平均值和峰值音量,并根据这两个参数发布一条消息。
如果音量超过95(假设音量达到100,则为任意数字)或始终高于80,那么我会宣布麦克风太大声。
如果平均音量低于40,那么我会宣布麦克风太安静。
如果平均音量低于10,则我认为麦克风无法正常工作或他们没有说话。
否则,我宣布音量正常。
由于我显然模拟了波动的音量水平,因此实际数字可能会有所不同,因此需要对这些数字进行一些调整。
我要做的另一件事是确保aria-live
区域仅每2秒更新一次。但是,我的屏幕阅读器播音员的速度很慢,因此您大概可以摆脱1200毫秒的干扰。
这是为了避免广播员队列过多。
或者,您可以将其设置为更大的值(例如10秒),因为它对于整个通话期间的持续监视很有用。如果您决定这样做,则将播音员设置为aria-live="polite"
,以免打扰其他屏幕阅读器播报。
我想到的进一步改进
我没有实现此功能,但是有两件事可以使它变得更准确(如果在整个通话过程中都无法使用,则可以减少烦人),如果您希望将其用作整个通话过程中提供持续监控工具。
首先,我将丢弃小于10的任何值作为音量,而仅取大于10的音量平均值。
这将更能指示用户积极讲话时的实际音量水平。
第二,如果有人在讲话,我将放弃所有音量级别的公告。您可能希望用户此时保持安静,所以不要告诉他们他们的麦克风很安静!另外,您真的不希望在其他人聊天时发出通知。
我尝试过的另一种方式
我确实尝试过每500毫秒将音量宣布为一个整数值,但是由于这是快照,所以我感觉发生的情况并不十分准确。这就是为什么我要平均音量。
然后我意识到您可以获得平均值50,但在100(剪切)时达到峰值,因此也为支票增加了峰值量。
其他事情
1。重点宣布
我这样做是为了让您在关注EQ图级别的事物时(不知道该怎么称呼!呵呵),它最初是在聚焦时宣布的。
我意识到作为切换按钮更好,以便用户可以随意打开和关闭它。
最好具有一个切换开关,因为它可以让您在收听公告时调小音量,也可以让其他所有人根据自己的喜好打开和关闭
2。通过平均值并使用短语来宣布
取平均值然后宣布一个短语而不是一个数字的另一个好处是,如果指示器保持打开状态,则用户体验会很好。
它仅在aria-live
区域发生变化时发出通知,因此只要音量保持在可接受的水平,它就不会发出任何通知。显然,如果他们停止讲话,它将宣布“麦克风太安静”,但再次声明一次。
3。喜欢减少运动
患有前庭疾病(对运动障碍的敏感性)的使用者可能会被铁棒分散注意力。
因此,我完全为那些用户隐藏了EQ。
现在,因为默认情况下我已关闭EQ,所以没有必要。但是,如果您决定默认情况下启用EQ,则可以使用prefers-reduced-motion
媒体查询来设置慢得多的动画速度等。这就是为什么我将其保留下来,以作为如何使用它的示例(这是我使EQ专注于工作而不是进行拨动时的一个突出问题,因此不再需要。)
我认为实现此目标的最容易方法是默认情况下关闭EQ。这还可以帮助注意力不集中的人轻松地被铁杆分散注意力,并让癫痫/癫痫症患者的页面安全,因为铁杆可以在一秒钟内“闪” 3次以上。 >
4。更改按钮文本。
打开EQ时,我利用了visually-hidden
类。
我更改了EQ中的文本,然后在视觉上将其隐藏,以便屏幕阅读器用户在聚焦EQ时仍然可以获得有用的按钮文本,但其他所有人只能看到EQ栏。
示例
忽略JS的第一部分,这只是我制作假EQ的伪劣方法。
我添加了一个非常大的注释块来标记您需要的实际零件的开始。它从JS中的65行开始。
还有一些内容可供您参考,以供您选择其他方式(在CSS中重点关注并喜欢减少运动),因此有些混乱。
任何问题都可以问。
//just making the random bars work,ignore this except for globalVars.currentVolume which is defined in the next section as that is what we use to announce the volume.
function randomInt(min,max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function removeClass(selector,className){
var el = document.querySelectorAll(selector);
for (var i = 0; i < el.length; i++) {
el[i].classList.remove(className);
}
}
function addClass(selector,className){
var el = document.querySelectorAll(selector);
for (var i = 0; i < el.length; i++) {
el[i].classList.add(className);
}
}
var generateRandomVolume = function(){
var stepHeight = 0.8;
globalVars.currentVolume = randomInt(0,100);
setBars(globalVars.currentVolume * stepHeight);
setTimeout(generateRandomVolume,randomInt(102,250));
return;
}
function setBars(volume){
var bar1 = document.querySelector('.bar1');
var bar2 = document.querySelector('.bar2');
var bar3 = document.querySelector('.bar3');
var smallHeightProportion = 0.75;
var smallHeight = volume * smallHeightProportion;
bar1.style.height = smallHeight + "px";
bar2.style.height = volume + "px";
bar3.style.height = smallHeight + "px";
//console.log(globalVars.currentVolume);
if(globalVars.currentVolume < 80){
addClass('.bar','green');
removeClass('.bar','orange');
removeClass('.bar','red');
}else if(globalVars.currentVolume >= 90){
addClass('.bar','red');
removeClass('.bar','green');
}else{
addClass('.bar','green');
}
}
window.addEventListener('load',function() {
setTimeout(generateRandomVolume,250);
});
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////// ACTUAL ANSWER ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//actual code you need,sorry it is a bit messy but you should be able to clean it up (and make it "Reactified")
// global variables,obviously only the currentVolume needs to be global so it can be used by both the bars and the announcer
var globalVars = {};
globalVars.currentVolume = 0;
globalVars.volumes = [];
globalVars.averageVolume = 0;
globalVars.announcementsOn = false;
//globalVars.indicatorFocused = false;
var audioIndicator = document.getElementById('audio-indicator');
var liveRegion = document.querySelector('.audio-indicator-announce');
var buttonText = document.querySelector('.button-text');
var announceTimeout;
//adjust the speech interval,2 seconds felt right for me but I have a slow announcer speed,I would imagine real screen reader users could handle about 1200ms update times easily.
var config = {};
config.speakInterval = 2000;
//push volume level every 100ms for use in getting averages
window.addEventListener('load',function() {
setInterval(sampleVolume,100);
});
var sampleVolume = function(){
globalVars.volumes.push(globalVars.currentVolume);
}
audioIndicator.addEventListener('click',function(e) {
toggleActive();
});
audioIndicator.addEventListener('keyup',function(e){
if (e.keyCode === 13) {
toggleActive();
}
});
function toggleActive(){
if(!audioIndicator.classList.contains('on')) {
audioIndicator.classList.add('on');
announceTimeout = setTimeout(announceVolumeInfo,config.speakInterval);
liveRegion.innerHTML = "announcing your microphone volume is now on";
buttonText.classList.add('visually-hidden');
buttonText.innerHTML = 'Mic<span class="visually-hidden">rophone</span> Level indicator on (click to turn off)';
console.log("SPEAK:","announcing your microphone volume is on");
}else{
audioIndicator.classList.remove('on');
clearTimeout(announceTimeout);
liveRegion.innerHTML = "announcing your microphone volume is now off";
buttonText.classList.remove('visually-hidden');
buttonText.innerHTML = 'Mic<span class="visually-hidden">rophone</span> Level indicator off (click to turn on)';
console.log("SPEAK:","announcing your microphone volume is off");
}
}
//switch on the announcer - deprecated idea,instead used toggle switch
//audioIndicator.addEventListener('focus',(e) => {
// setTimeout(announceVolumeInfo,config.speakInterval);
//});
//we take the average over the speakInterval. We also take the peak so we can see if the users microphone is clipping.
function getVolumeInfo(){
var samples = globalVars.volumes.length;
var totalVol = 0;
var avgVol,peakVol = 0;
var sample;
for(var x = 0; x < samples; x++){
sample = globalVars.volumes[x]
totalVol += sample;
if(sample > peakVol){
peakVol = sample;
}
}
globalVars.volumes = [];
var volumes = {};
volumes.average = totalVol / samples;
volumes.peak = peakVol;
return volumes;
}
var announceVolumeInfo = function(){
var volumeInfo = getVolumeInfo();
updateLiveRegion (volumeInfo.average,volumeInfo.peak);
//part of deprecated idea of announcing only on focus
//if(document.activeElement == document.getElementById('audio-indicator')){
// setTimeout(announceVolumeInfo,config.speakInterval);
//}
if(audioIndicator.classList.contains('on')) {
announceTimeout = setTimeout(announceVolumeInfo,config.speakInterval);
}
}
//we announce using this function,if you just want to read the current volume this can be as simple as "liveRegion.innerHTML = globalVars.currentVolume"
var updateLiveRegion = function(avgVolume,peak){
var speak = "Your microphone is ";
//doing it this way has a few advantages detailed in the post.
if(peak > 95 || avgVolume > 80){
speak = speak + "too loud";
}else if(avgVolume < 40){
speak = speak + "too quiet";
}else if(avgVolume < 10){
speak = speak + "not working or you are not talking";
}else{
speak = speak + "at a good volume";
}
console.log("SPEAK:",speak);
console.log("average volume:",avgVolume,"peak volume:",peak);
liveRegion.innerHTML = speak;
//optionally you could just read out the current volume level and that would do away with the need for tracking averages etc..
//liveRegion.innerHTML = "Your microphone volums level is " + globalVars.currentVolume;
}
#audio-indicator{
width: 100px;
height: 100px;
position: relative;
background-color: #333;
}
/** make sure we have a focus indicator,if you decide to make the item interactive with the mouse then also add a different hover state. **/
#audio-indicator:focus{
outline: 2px solid #666;
outline-offset: 3px;
}
#audio-indicator:hover{
background-color: #666;
cursor: pointer;
}
/*************we simply hide the indicator if the user has indicated that they do not want to see animations**********/
@media (prefers-reduced-motion) {
#audio-indicator{
display: none;
}
}
/***********my visually hidden class for hiding content visually but still making it screen reader accessible,preferable to sr-only etc as futureproofed and better compatibility********/
.visually-hidden {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6,IE7 - a 0 height clip,off to the bottom right of the visible 1px box */
clip: rect(1px,1px,1px); /*maybe deprecated but we need to support legacy browsers */
clip-path: inset(50%); /*modern browsers,clip-path works inwards from each corner*/
white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}
#audio-indicator .bar{
display: none;
}
#audio-indicator.on .bar{
display: block;
}
.bar,.button-text{
position: absolute;
top: 50%;
left: 50%;
min-height: 2px;
width: 25%;
transition: all 0.1s linear;
}
.button-text{
width: 90px;
transform: translate(-50%,-50%);
text-align: center;
color: #fff;
}
.bar1{
transform: translate(-175%,-50%);
}
.bar2{
transform: translate(-50%,-50%);
}
.bar3{
transform: translate(75%,-50%);
}
.green{
background-color: green;
}
.orange{
background-color: orange;
}
.red{
background-color: red;
}
<a href="#">dummy link for focus</a>
<div class="audio-indicator-announce visually-hidden" aria-live="assertive">
</div>
<div id="audio-indicator" tabindex="0">
<div class="button-text">Mic<span class="visually-hidden">rophone</span> Level indicator off (click to turn on)</div>
<div class="bar bar1"></div>
<div class="bar bar2"></div>
<div class="bar bar3"></div>
</div>
<a href="#">dummy link for focus</a>
最终思想
虽然上述方法可行,但问题是“从UX角度来看,情商图是否相关/良好的体验?”。
您必须进行用户测试才能找出答案。
最好使用类似于Zoom的方法(稍作调整并可以访问界面)。
- 在通话之前(或通话期间),用户可以先收听预先录制的声音。这使他们可以设置自己的音量级别。
- 然后允许用户讲话,然后播放其音频,以便他们可以检查其麦克风的级别/麦克风是否正常工作。
- 允许用户打开“自动调整我的麦克风音量”并使用与我正在使用的平均值类似的方法,同时在通话过程中自动调整音量。
- 您还可以使用屏幕阅读器选项,该选项使系统可以宣布是否正在自动调整其音量。
- 如果需要自动调节音量,您还可以在麦克风图标/按钮上有一个视觉指示器,该指示器显示向上或向下箭头。
这显然只是一个想法,您可能有一个很好的用EQ图做事的用例,就像我说的那样!