1ada7dc57e
Output device changes aren't working in 2.6's echo test when artifical delay is on due to the fact that the feedback audio is being played via the WebAudio context rather the the HTMLMediaElement. Since output device change works via HTMLMediaElement's setSinkId, it's basically a no-op. This commit fixes the issue by piping the AudioContext destination through the main audio element rather than using WebAudio directly for playback. An additional stub media element (muted) is added to circumvent one of Chrome's WebAudio issue. The alternative would be to use AudioContext's setSinkId, but it isn't supported by Firefox (setSinkId enabled) and Chrome < 110. This should work with FF (setSinkId enabled) and a wide array of Chromium versions.
122 lines
3.7 KiB
JavaScript
122 lines
3.7 KiB
JavaScript
import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
|
|
import browserInfo from '/imports/utils/browserInfo';
|
|
|
|
const MEDIA_TAG = Meteor.settings.public.media.mediaTag;
|
|
const USE_RTC_LOOPBACK_CHR = Meteor.settings.public.media.localEchoTest.useRtcLoopbackInChromium;
|
|
const {
|
|
enabled: DELAY_ENABLED = true,
|
|
delayTime = 0.5,
|
|
maxDelayTime = 2,
|
|
} = Meteor.settings.public.media.localEchoTest.delay;
|
|
|
|
let audioContext = null;
|
|
let sourceContext = null;
|
|
let contextDestination = null;
|
|
let stubAudioElement = null;
|
|
let delayNode = null;
|
|
|
|
const useRTCLoopback = () => (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) => {
|
|
if (stream) {
|
|
if (delayNode || audioContext || sourceContext) cleanupDelayNode();
|
|
const audioElement = document.querySelector(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 (remote-media)
|
|
audioElement.srcObject = contextDestination.stream;
|
|
}
|
|
};
|
|
|
|
const deattachEchoStream = () => {
|
|
const audioElement = document.querySelector(MEDIA_TAG);
|
|
|
|
if (DELAY_ENABLED) {
|
|
audioElement.muted = false;
|
|
cleanupDelayNode();
|
|
}
|
|
|
|
audioElement.pause();
|
|
audioElement.srcObject = null;
|
|
};
|
|
|
|
const playEchoStream = async (stream, loopbackAgent = null) => {
|
|
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 (remote-media),
|
|
// no strings attached.
|
|
const audioElement = document.querySelector(MEDIA_TAG);
|
|
audioElement.srcObject = streamToPlay;
|
|
audioElement.muted = false;
|
|
audioElement.play();
|
|
}
|
|
}
|
|
};
|
|
|
|
export default {
|
|
useRTCLoopback,
|
|
createAudioRTCLoopback,
|
|
deattachEchoStream,
|
|
playEchoStream,
|
|
};
|