2022-04-13 09:51:07 +08:00
|
|
|
import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
|
|
|
|
import browserInfo from '/imports/utils/browserInfo';
|
2024-09-06 07:53:33 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
2022-04-13 09:51:07 +08:00
|
|
|
|
2024-06-05 19:26:27 +08:00
|
|
|
const LOCAL_MEDIA_TAG = '#local-media';
|
|
|
|
|
2023-01-24 03:31:53 +08:00
|
|
|
let audioContext = null;
|
|
|
|
let sourceContext = null;
|
2023-03-21 04:38:06 +08:00
|
|
|
let contextDestination = null;
|
|
|
|
let stubAudioElement = null;
|
2023-01-24 03:31:53 +08:00
|
|
|
let delayNode = null;
|
2022-04-13 09:51:07 +08:00
|
|
|
|
2024-06-05 19:26:27 +08:00
|
|
|
const shouldUseRTCLoopback = () => {
|
2024-05-29 21:26:11 +08:00
|
|
|
const USE_RTC_LOOPBACK_CHR = window.meetingClientSettings.public.media.localEchoTest.useRtcLoopbackInChromium;
|
|
|
|
|
|
|
|
return (browserInfo.isChrome || browserInfo.isEdge) && USE_RTC_LOOPBACK_CHR;
|
|
|
|
};
|
|
|
|
|
2022-04-13 09:51:07 +08:00
|
|
|
const createAudioRTCLoopback = () => new LocalPCLoopback({ audio: true });
|
|
|
|
|
2023-01-24 03:31:53 +08:00
|
|
|
const cleanupDelayNode = () => {
|
|
|
|
if (delayNode) {
|
|
|
|
delayNode.disconnect();
|
|
|
|
delayNode = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sourceContext) {
|
|
|
|
sourceContext.disconnect();
|
|
|
|
sourceContext = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (audioContext) {
|
|
|
|
audioContext.close();
|
|
|
|
audioContext = null;
|
|
|
|
}
|
2023-03-21 04:38:06 +08:00
|
|
|
|
|
|
|
if (contextDestination) {
|
|
|
|
contextDestination.disconnect();
|
|
|
|
contextDestination = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stubAudioElement) {
|
|
|
|
stubAudioElement.pause();
|
|
|
|
stubAudioElement.srcObject = null;
|
|
|
|
stubAudioElement = null;
|
|
|
|
}
|
2023-01-24 03:31:53 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const addDelayNode = (stream) => {
|
2024-05-29 21:26:11 +08:00
|
|
|
const {
|
|
|
|
delayTime = 0.5,
|
|
|
|
maxDelayTime = 2,
|
|
|
|
} = window.meetingClientSettings.public.media.localEchoTest.delay;
|
|
|
|
|
2023-01-24 03:31:53 +08:00
|
|
|
if (stream) {
|
|
|
|
if (delayNode || audioContext || sourceContext) cleanupDelayNode();
|
2024-06-05 19:26:27 +08:00
|
|
|
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
|
2023-03-21 04:38:06 +08:00
|
|
|
// 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;
|
2023-01-24 03:31:53 +08:00
|
|
|
|
2023-03-21 04:38:06 +08:00
|
|
|
// Create a new AudioContext to be able to add a delay to the stream
|
2023-01-24 03:31:53 +08:00
|
|
|
audioContext = new AudioContext();
|
|
|
|
sourceContext = audioContext.createMediaStreamSource(stream);
|
2023-03-21 04:38:06 +08:00
|
|
|
contextDestination = audioContext.createMediaStreamDestination();
|
|
|
|
// Create a DelayNode to add a delay to the stream
|
2023-01-24 03:31:53 +08:00
|
|
|
delayNode = new DelayNode(audioContext, { delayTime, maxDelayTime });
|
2023-03-21 04:38:06 +08:00
|
|
|
// Connect the stream to the DelayNode and then to the MediaStreamDestinationNode
|
|
|
|
// to be able to play the stream.
|
2023-01-24 03:31:53 +08:00
|
|
|
sourceContext.connect(delayNode);
|
2023-03-21 04:38:06 +08:00
|
|
|
delayNode.connect(contextDestination);
|
2023-01-24 03:31:53 +08:00
|
|
|
delayNode.delayTime.setValueAtTime(delayTime, audioContext.currentTime);
|
2024-06-05 19:26:27 +08:00
|
|
|
// Play the stream with the delay in the default audio element (local-media)
|
2023-03-21 04:38:06 +08:00
|
|
|
audioElement.srcObject = contextDestination.stream;
|
2023-01-24 03:31:53 +08:00
|
|
|
}
|
|
|
|
};
|
2023-03-21 04:38:06 +08:00
|
|
|
|
2022-04-13 09:51:07 +08:00
|
|
|
const deattachEchoStream = () => {
|
2024-05-29 21:26:11 +08:00
|
|
|
const {
|
|
|
|
enabled: DELAY_ENABLED = true,
|
|
|
|
} = window.meetingClientSettings.public.media.localEchoTest.delay;
|
|
|
|
|
2024-06-05 19:26:27 +08:00
|
|
|
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
|
2023-01-24 03:31:53 +08:00
|
|
|
|
|
|
|
if (DELAY_ENABLED) {
|
|
|
|
audioElement.muted = false;
|
|
|
|
cleanupDelayNode();
|
|
|
|
}
|
|
|
|
|
2022-04-13 09:51:07 +08:00
|
|
|
audioElement.pause();
|
|
|
|
audioElement.srcObject = null;
|
|
|
|
};
|
|
|
|
|
|
|
|
const playEchoStream = async (stream, loopbackAgent = null) => {
|
2024-05-29 21:26:11 +08:00
|
|
|
const {
|
|
|
|
enabled: DELAY_ENABLED = true,
|
|
|
|
} = window.meetingClientSettings.public.media.localEchoTest.delay;
|
|
|
|
|
2022-04-13 09:51:07 +08:00
|
|
|
if (stream) {
|
|
|
|
deattachEchoStream();
|
|
|
|
let streamToPlay = stream;
|
|
|
|
|
|
|
|
if (loopbackAgent) {
|
2023-03-21 04:38:06 +08:00
|
|
|
// 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
|
2022-04-13 09:51:07 +08:00
|
|
|
try {
|
|
|
|
await loopbackAgent.start(stream);
|
|
|
|
streamToPlay = loopbackAgent.loopbackStream;
|
|
|
|
} catch (error) {
|
|
|
|
loopbackAgent.stop();
|
|
|
|
}
|
|
|
|
}
|
2023-01-24 03:31:53 +08:00
|
|
|
|
|
|
|
if (DELAY_ENABLED) {
|
|
|
|
addDelayNode(streamToPlay);
|
2023-03-21 04:38:06 +08:00
|
|
|
} else {
|
2024-06-05 19:26:27 +08:00
|
|
|
// No delay: play the stream in the default audio element (local-media),
|
2023-03-21 04:38:06 +08:00
|
|
|
// no strings attached.
|
2024-06-05 19:26:27 +08:00
|
|
|
const audioElement = document.querySelector(LOCAL_MEDIA_TAG);
|
2023-03-21 04:38:06 +08:00
|
|
|
audioElement.srcObject = streamToPlay;
|
|
|
|
audioElement.muted = false;
|
|
|
|
audioElement.play();
|
2023-01-24 03:31:53 +08:00
|
|
|
}
|
2022-04-13 09:51:07 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-09-06 07:53:33 +08:00
|
|
|
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}`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-04-13 09:51:07 +08:00
|
|
|
export default {
|
2024-06-05 19:26:27 +08:00
|
|
|
shouldUseRTCLoopback,
|
2022-04-13 09:51:07 +08:00
|
|
|
createAudioRTCLoopback,
|
|
|
|
deattachEchoStream,
|
|
|
|
playEchoStream,
|
2024-09-06 07:53:33 +08:00
|
|
|
setAudioSink,
|
2022-04-13 09:51:07 +08:00
|
|
|
};
|