Skip to main content
Version: 1.x.x

Change Audio Output Device - React

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 the useMediaDevice hook.
import { useMediaDevice } from "@videosdk.live/react-sdk";

const { getPlaybackDevices } = useMediaDevice();

const getAudioOutputDevice = async () => {
const speakers = await getPlaybackDevices();
return speakers;
};
note

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 sinkId for 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);
});
};
note

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 the setSinkId() method is exposed. However, calling setSinkId() on an <audio> element which is playing a remote track gets silently ignored, because WebKit renders that audio through the WebRTC engine's pipeline, which setSinkId() 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 calling setSinkId() on it works as expected.

  • On iOS versions below 26, setSinkId() is not available and getPlaybackDevices() 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 the changeMic() 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 file audioOutputRelay.js which maintains a single AudioContext, mixes the remote tracks into a MediaStreamAudioDestinationNode, and plays the mix through the relay <audio> element.
audioOutputRelay.js
const isIOSDevice =
typeof navigator !== "undefined" &&
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(/Mac/.test(navigator.userAgent) && navigator.maxTouchPoints > 1));

export 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,
});
}

export 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);
}
};
}

export function setRelaySinkId(deviceId) {
if (!shouldUseAudioRelay() || deviceId == null) return;
ensureRelay();
resumeRelay();
relayElement.setSinkId(deviceId).catch((err) => {
console.error("Setting relay speaker device failed", err);
});
}

export 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 rendering the audio of a participant, mute the participant's <audio> element when the relay is active, and connect the remote track to the relay instead. The effect returns the disconnect function, so the track is removed from the mix when the participant leaves or turns their mic off.
import { useParticipant } from "@videosdk.live/react-sdk";
import { useEffect, useRef } from "react";
import {
connectTrackToRelay,
shouldUseAudioRelay,
} from "./audioOutputRelay";

const ParticipantAudioPlayer = ({ participantId }) => {
const { micStream, micOn, isLocal } = useParticipant(participantId);
const micRef = useRef(null);

useEffect(() => {
if (micRef.current) {
if (micOn && micStream) {
const mediaStream = new MediaStream();
mediaStream.addTrack(micStream.track);
micRef.current.srcObject = mediaStream;
micRef.current
.play()
.catch((error) => console.error("audio play() failed", error));
if (!isLocal) {
return connectTrackToRelay(micStream.track);
}
} else {
micRef.current.srcObject = null;
}
}
}, [micStream, micOn, isLocal]);

return (
<audio ref={micRef} autoPlay muted={isLocal || shouldUseAudioRelay()} />
);
};

Switching the Output Device

  • While changing the output audio device, set the sinkId on 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.
const { leave } = useMeeting({
onMeetingLeft: () => {
teardownRelay();
},
});

Sample Implementation

You can find the complete implementation in the VideoSDK React SDK example repository.

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 volume property for the <audio> can be between 0 and 1.

const setAudioVolume = (volume) => {
const audioTags = document.getElementsByTagName("audio");
Array.from(audioTags).forEach((tag) => {
tag.volume = volume;
});
};
note

To learn more about adjusting the audio volume check this documentation.

Got a Question? Ask us on discord