Change Audio Output Device - Javascript
During the meeting, at any point, a participant wishing to switch their output audio device, such as from headphones to speakers, can do so using the below-mentioned methods.
Getting Output device
- To get all the available audio output devices, you can use the
getPlaybackDevices()method of theVideoSDKclass.
const getAudioOutputDevice = async () => {
const speakers = await VideoSDK.getPlaybackDevices();
return speakers;
};
To learn more about the getPlaybackDevices() method check this API reference.
Changing Output Device
- To change the output audio device, you need to set the
sinkIdfor each<audio>element used to render the audio in the meeting.
const setAudioOutputDevice = (deviceId) => {
const audioTags = document.getElementsByTagName("audio");
Array.from(audioTags).forEach((tag) => {
tag.setSinkId(deviceId);
});
};
To learn more about changing the audio output device check this documentation.
Changing Output Device on iOS
-
Starting from iOS 26, the browser lists audio output devices, so the
getPlaybackDevices()method returns the available speakers, and thesetSinkId()method is exposed. However, callingsetSinkId()on an<audio>element which is playing a remote track gets silently ignored, because WebKit renders that audio through the WebRTC engine's pipeline, whichsetSinkId()does not control. -
To switch the speaker on iOS, you have to mix all the remote audio tracks into a single stream using the Web Audio API, and play that mix through a single hidden relay
<audio>element. The relay element plays a plain media stream, so callingsetSinkId()on it works as expected. -
On iOS versions below 26,
setSinkId()is not available andgetPlaybackDevices()does not list any audio output devices, so speaker selection is not possible there. However, iOS routes the audio input and output together at the OS level, so when the user selects a microphone using thechangeMic()method, iOS automatically switches the audio output to the same device. For example, selecting the microphone of a Bluetooth headset also routes the meeting audio to that headset.
Audio Relay Utility
- Create a utility which maintains a single
AudioContext, mixes the remote tracks into aMediaStreamAudioDestinationNode, and plays the mix through the relay<audio>element.
const isIOSDevice =
typeof navigator !== "undefined" &&
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(/Mac/.test(navigator.userAgent) && navigator.maxTouchPoints > 1));
function shouldUseAudioRelay() {
return (
isIOSDevice && typeof HTMLMediaElement.prototype.setSinkId === "function"
);
}
let audioContext = null;
let destinationNode = null;
let relayElement = null;
const connectedTracks = new Map();
function resumeRelay() {
if (!audioContext) return;
if (audioContext.state !== "running") {
audioContext.resume().catch(() => {});
}
if (relayElement.paused) {
relayElement.play().catch(() => {});
}
}
function ensureRelay() {
if (audioContext) return;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
destinationNode = audioContext.createMediaStreamDestination();
relayElement = document.createElement("audio");
relayElement.autoplay = true;
relayElement.playsInline = true;
relayElement.style.display = "none";
relayElement.srcObject = destinationNode.stream;
document.body.appendChild(relayElement);
// iOS suspends the AudioContext until a user gesture and after
// audio-session interruptions (e.g. route changes); revive it on any tap.
document.addEventListener("touchend", resumeRelay, {
capture: true,
passive: true,
});
document.addEventListener("click", resumeRelay, {
capture: true,
passive: true,
});
}
function connectTrackToRelay(track) {
if (!shouldUseAudioRelay() || !track) return () => {};
ensureRelay();
let entry = connectedTracks.get(track);
if (!entry) {
const sourceNode = audioContext.createMediaStreamSource(
new MediaStream([track])
);
sourceNode.connect(destinationNode);
entry = { sourceNode, refCount: 0 };
connectedTracks.set(track, entry);
}
entry.refCount += 1;
resumeRelay();
return () => {
const current = connectedTracks.get(track);
if (!current) return;
current.refCount -= 1;
if (current.refCount <= 0) {
current.sourceNode.disconnect();
connectedTracks.delete(track);
}
};
}
function setRelaySinkId(deviceId) {
if (!shouldUseAudioRelay() || deviceId == null) return;
ensureRelay();
resumeRelay();
relayElement.setSinkId(deviceId).catch((err) => {
console.error("Setting relay speaker device failed", err);
});
}
function teardownRelay() {
if (!audioContext) return;
document.removeEventListener("touchend", resumeRelay, { capture: true });
document.removeEventListener("click", resumeRelay, { capture: true });
connectedTracks.forEach(({ sourceNode }) => sourceNode.disconnect());
connectedTracks.clear();
relayElement.pause();
relayElement.srcObject = null;
relayElement.remove();
relayElement = null;
destinationNode = null;
audioContext.close().catch(() => {});
audioContext = null;
}
Rendering Participant Audio
- While creating the
<audio>element for a participant, mute it when the relay is active, and connect the remote audio track to the relay while setting the media track. TheconnectTrackToRelay()method returns a disconnect function, so store it and call it when the participant leaves, to remove their track from the mix.
const relayDisconnects = new Map();
function createAudioElement(pId) {
let audioElement = document.createElement("audio");
audioElement.setAttribute("autoPlay", "false");
audioElement.setAttribute("playsInline", "true");
audioElement.setAttribute("controls", "false");
audioElement.setAttribute("id", `a-${pId}`);
audioElement.style.display = "none";
// mute the participant's audio element when the relay is active
audioElement.muted = shouldUseAudioRelay();
return audioElement;
}
// setting media track
function setTrack(stream, audioElement, participant, isLocal) {
if (stream.kind == "audio") {
if (isLocal) {
isMicOn = true;
} else {
const mediaStream = new MediaStream();
mediaStream.addTrack(stream.track);
audioElement.srcObject = mediaStream;
audioElement
.play()
.catch((error) => console.error("audioElem.play() failed", error));
// connect the remote audio track to the relay
relayDisconnects.get(participant.id)?.();
relayDisconnects.set(participant.id, connectTrackToRelay(stream.track));
}
}
}
// participant left
meeting.on("participant-left", (participant) => {
// remove the participant's track from the mix
relayDisconnects.get(participant.id)?.();
relayDisconnects.delete(participant.id);
});
Switching the Output Device
- While changing the output audio device, set the
sinkIdon the relay element when the relay is active, and fall back to setting it on each<audio>element on other platforms.
const setAudioOutputDevice = (deviceId) => {
if (shouldUseAudioRelay()) {
setRelaySinkId(deviceId);
return;
}
const audioTags = document.getElementsByTagName("audio");
Array.from(audioTags).forEach((tag) => {
tag.setSinkId(deviceId);
});
};
Releasing the Relay
- When the meeting ends, release the relay so the browser tab stops holding the audio session.
meeting.on("meeting-left", () => {
teardownRelay();
});
Setting Audio Volume
-
To set the audio volume for the meeting, you need to adjust the volume property for each
<audio>element used to render the paricipant audio. -
Value for the
volumeproperty for the<audio>can be between0and1.
const setAudioVolume = (volume) => {
const audioTags = document.getElementsByTagName("audio");
Array.from(audioTags).forEach((tag) => {
tag.volume = volume;
});
};
To learn more about adjusting the audio volume check this documentation.
Got a Question? Ask us on discord

