I've looked through countless guides and articles about react-native-webrtc and, surprisingly, I was not able to find a solution to my problem. Everything else works just fine (audio calls and video calls), however for some reason, during video calls specifically, the audio is played back through the built-in earpiece, not the speaker.
I've found some other solutions for 3rd party libraries, such as setSpeakerPhoneOn or setForceSpeakerPhoneOn from react-native-incall-manager, but I don't want to install a whole separate library for just one feature. I've tried looking into their source code, but there was little that I could gather that would help me.
How can I toggle the speaker with only the react-native-webrtc library?
Here is how the caller's call is initiated:
const CallerVideoScreen = React.memo(() => {
const room = useUnit($room);
const iceCandidates = useUnit($iceCandidates) // empty array until target user gathers all candidates from their side;
const turnCredentials = useUnit($turnCredentials) // generate on my own ICE server;
const [localStream, setLocalStream] = React.useState(null);
const [remoteStream, setRemoteStream] = React.useState(null);
const [cachedLocalPC, setCachedLocalPC] = React.useState(null);
React.useEffect(() => {
setScreenHeaderComponent({
payload: {
screenHeaderConfig: {
component: null,
props: null,
previous: {
component: EScreenHeaderComponentName.ChatHeader,
props: {},
},
},
},
});
startLocalStream();
}, []);
React.useEffect(() => {
if (localStream && room?.id && turnCredentials) {
startCall();
return () => {
setLocalStream(null);
setRemoteStream(null);
setCachedLocalPC(null);
};
}
}, [localStream, room?.id, turnCredentials]);
const startLocalStream = async () => {
const devices = await mediaDevices.enumerateDevices();
const videoSourceId = devices.find(
(device) => device.kind === 'videoinput' && device.facing === 'front',
);
if (videoSourceId) {
const newStream = await mediaDevices.getUserMedia({
audio: true,
video: {
mandatory: {
minWidth: 500,
minHeight: 300,
minFrameRate: 30,
},
facingMode: 'user',
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
});
setLocalStream(newStream);
}
};
const startCall = async () => {
if (localStream) {
const localPC = new RTCPeerConnection(
iceServerConfig({ turnCredentials }),
);
localStream.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
localPC.addEventListener('icecandidate', (e) => {
if (!e.candidate) {
return;
}
// store candidate
storeIceCandidate({ payload: e.candidate.toJSON() });
});
localPC.ontrack = (e) => {
const newStream = new MediaStream();
e.streams[0].getTracks().forEach((track) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = await localPC.createOffer({});
await localPC.setLocalDescription(offer);
// signal offer to target user
updateCallRoomOffer({ payload: { offer } });
setCachedLocalPC(localPC);
}
};
React.useEffect(() => {
if (cachedLocalPC && room?.answer) {
if (!cachedLocalPC.currentRemoteDescription) {
// intercept answer to offer and set description
const rtcSessionDescription = new RTCSessionDescription(room.answer);
cachedLocalPC.setRemoteDescription(rtcSessionDescription);
} else {
// log error
}
}
}, [cachedLocalPC, room?.answer]);
React.useEffect(() => {
if (cachedLocalPC && iceCandidates.length) {
iceCandidates.forEach(({ candidate, sdpMLineIndex, sdpMid }) => {
cachedLocalPC.addIceCandidate(
new RTCIceCandidate({ candidate, sdpMLineIndex, sdpMid }),
);
});
}
}, [cachedLocalPC, iceCandidates]);
return (
<RTCView
style={styles.rctView}
streamURL={remoteStream?.toURL() ?? ''}
objectFit={'cover'}
/>
);
});