bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/audio/local-echo/service.js
prlanzarin 9070a651ec fix(audio): local echo not tracking output device changes
Commit 325887e325 split the local echo audio
element from the main audio element to allow concurrent playback without the
risk of interfering with one another.

This introduced a regression where local echo doesn't track output device
changes. The main audio element (i.e. the meeting's audio) is not affected by
this regression.

This commit ensures local echo reacts to output device changes as needed.
2024-09-05 23:57:09 +00:00

153 lines
4.4 KiB
JavaScript

import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
import browserInfo from '/imports/utils/browserInfo';
import logger from '/imports/startup/client/logger';
const LOCAL_MEDIA_TAG = '#local-media';
let audioContext = null;
let sourceContext = null;
let contextDestination = null;
let stubAudioElement = null;
let delayNode = null;
const shouldUseRTCLoopback = () => {
const USE_RTC_LOOPBACK_CHR = window.meetingClientSettings.public.media.localEchoTest.useRtcLoopbackInChromium;
return (browserInfo.isChrome || browserInfo.isEdge) && USE_RTC_LOOPBACK_CHR;
};
const createAudioRTCLoopback = () => new LocalPCLoopback({ audio: true });
const cleanupDelayNode = () => {
if (delayNode) {
delayNode.disconnect();
delayNode = null;
}
if (sourceContext) {
sourceContext.disconnect();
sourceContext = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
if (contextDestination) {
contextDestination.disconnect();
contextDestination = null;
}
if (stubAudioElement) {
stubAudioElement.pause();
stubAudioElement.srcObject = null;
stubAudioElement = null;
}
};
const addDelayNode = (stream) => {
const {
delayTime = 0.5,
maxDelayTime = 2,
} = window.meetingClientSettings.public.media.localEchoTest.delay;
if (stream) {
if (delayNode || audioContext || sourceContext) cleanupDelayNode();
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
// Workaround: attach the stream to a muted stub audio element to be able to play it in
// Chromium-based browsers. See https://bugs.chromium.org/p/chromium/issues/detail?id=933677
stubAudioElement = new Audio();
stubAudioElement.muted = true;
stubAudioElement.srcObject = stream;
// Create a new AudioContext to be able to add a delay to the stream
audioContext = new AudioContext();
sourceContext = audioContext.createMediaStreamSource(stream);
contextDestination = audioContext.createMediaStreamDestination();
// Create a DelayNode to add a delay to the stream
delayNode = new DelayNode(audioContext, { delayTime, maxDelayTime });
// Connect the stream to the DelayNode and then to the MediaStreamDestinationNode
// to be able to play the stream.
sourceContext.connect(delayNode);
delayNode.connect(contextDestination);
delayNode.delayTime.setValueAtTime(delayTime, audioContext.currentTime);
// Play the stream with the delay in the default audio element (local-media)
audioElement.srcObject = contextDestination.stream;
}
};
const deattachEchoStream = () => {
const {
enabled: DELAY_ENABLED = true,
} = window.meetingClientSettings.public.media.localEchoTest.delay;
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
if (DELAY_ENABLED) {
audioElement.muted = false;
cleanupDelayNode();
}
audioElement.pause();
audioElement.srcObject = null;
};
const playEchoStream = async (stream, loopbackAgent = null) => {
const {
enabled: DELAY_ENABLED = true,
} = window.meetingClientSettings.public.media.localEchoTest.delay;
if (stream) {
deattachEchoStream();
let streamToPlay = stream;
if (loopbackAgent) {
// Chromium based browsers need audio to go through PCs for echo cancellation
// to work. See https://bugs.chromium.org/p/chromium/issues/detail?id=687574
try {
await loopbackAgent.start(stream);
streamToPlay = loopbackAgent.loopbackStream;
} catch (error) {
loopbackAgent.stop();
}
}
if (DELAY_ENABLED) {
addDelayNode(streamToPlay);
} else {
// No delay: play the stream in the default audio element (local-media),
// no strings attached.
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
audioElement.srcObject = streamToPlay;
audioElement.muted = false;
audioElement.play();
}
}
};
const setAudioSink = (deviceId) => {
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
if (audioElement.setSinkId) {
audioElement.setSinkId(deviceId).catch((error) => {
logger.warn({
logCode: 'localecho_output_change_error',
extraInfo: {
errorName: error?.name,
errorMessage: error?.message,
deviceId,
},
}, `Error setting audio sink in local echo test: ${error?.name}`);
});
}
};
export default {
shouldUseRTCLoopback,
createAudioRTCLoopback,
deattachEchoStream,
playEchoStream,
setAudioSink,
};