Here is the code.
sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
localConnection.ondatachannel = receiveChannelCallback;
localConnection.onicecandidate = e => {
console.log('candidates found');
e.candidate && offerCandidates.add(e.candidate.toJSON());
};
var offerDescription = await localConnection.createOffer();
await localConnection.setLocalDescription(offerDescription);
I have confirmed it works on all desktop browsers and firefox on android but onicecandidate is never called on chrome for android or native webview.
Also I updated chrome, webview and android itself and the problem still persists.
Edit: I tried it on another phone running chrome version 84.0.4147.89 and it works perfectly. The version that has the issue is 94.0.4606.85.
I downgraded chrome to version 87.0.4280.141 and now it is working but sadly downgrading the webview didn't help which is the end use case.
My theory is that it is a bug or a security issue on the new versions. In any case here is the full code just to make sure.
import './firebase/firebase.js';
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: ""
};
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
var connectButton = null;
var disconnectButton = null;
var sendButton = null;
var messageInputBox = null;
var receiveBox = null;
const servers = {
iceServers: [
{
urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'],
},
],
iceCandidatePoolSize: 10,
};
const localConnection = new RTCPeerConnection(servers);
var calldoc;
var offerCandidates;
var answerCandidates;
var sendChannel = null; // RTCDataChannel for the local (sender)
var receiveChannel = null; // RTCDataChannel for the remote (receiver)
var answerInput = null;
var answerButton = null;
var connected = false;
var id = null;
var dataConstraint;
function startup() {
connectButton = document.getElementById('connectButton');
disconnectButton = document.getElementById('disconnectButton');
sendButton = document.getElementById('sendButton');
messageInputBox = document.getElementById('message');
receiveBox = document.getElementById('receivebox');
answerInput = document.getElementById('answerID');
answerButton = document.getElementById('answerButton');
// Set event listeners for user interface widgets
answerButton.addEventListener('click', listenForConnection, false);
connectButton.addEventListener('click', connectPeers, false);
disconnectButton.addEventListener('click', disconnectPeers, false);
sendButton.addEventListener('click', sendMessage, false);
}
function onicecandidate (e) {
console.log('candidates found');
e.candidate && offerCandidates.add(e.candidate.toJSON());
};
export async function connectPeers() {
// Create the local connection and its event listeners
calldoc = firebase.firestore().collection('calls').doc();
// Create the data channel and establish its event listeners
dataConstraint = null;
sendChannel = localConnection.createDataChannel("sendChannel", dataConstraint);
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
localConnection.ondatachannel = receiveChannelCallback;
localConnection.onicecandidate = onicecandidate;
id = calldoc.id;
offerCandidates = calldoc.collection('offerCandidates');
answerCandidates = calldoc.collection('answerCandidates');
var offerDescription = await localConnection.createOffer();
await localConnection.setLocalDescription(offerDescription);
const offer = {
sdp: offerDescription.sdp,
type: offerDescription.type,
};
await calldoc.set({offer});
calldoc.onSnapshot((snapshot) => {
const data = snapshot.data();
if (data !== null) {
if (!localConnection.currentRemoteDescription && data.answer) {
const answerDescription = new RTCSessionDescription(data.answer);
localConnection.setRemoteDescription(answerDescription);
}
}
});
answerCandidates.onSnapshot(snapshot => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
const candidate = new RTCIceCandidate(change.doc.data());
localConnection.addIceCandidate(candidate);
console.log("found answer");
connected = true;
}
});
});
}
async function listenForConnection() {
calldoc = firebase.firestore().collection('calls').doc(answerInput.value);
answerCandidates = calldoc.collection('answerCandidates');
localConnection.onicecandidate = event => {
event.candidate && answerCandidates.add(event.candidate.toJSON());
};
// Create the data channel and establish its event listeners
sendChannel = localConnection.createDataChannel("receiveChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
localConnection.ondatachannel = receiveChannelCallback;
const cdata = (await calldoc.get()).data();
const offerDescription = cdata.offer;
await localConnection.setRemoteDescription(new
RTCSessionDescription(offerDescription));
const answerDescription = await localConnection.createAnswer();
await localConnection.setLocalDescription(answerDescription);
const answer = {
type: answerDescription.type,
sdp: answerDescription.sdp,
};
await calldoc.update({ answer });
offerCandidates.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
console.log(change)
if (change.type === 'added') {
let data = change.doc.data();
localConnection.addIceCandidate(new RTCIceCandidate(data));
}
});
});
}
// Handle errors attempting to create a description;
function handleCreateDescriptionError(error) {
console.log("Unable to create an offer: " + error.toString());
}
// Handle successful addition of the ICE candidate
// on the "local" end of the connection.
function handleLocalAddCandidateSuccess() {
connectButton.disabled = true;
}
// Handle successful addition of the ICE candidate
// on the "remote" end of the connection.
function handleRemoteAddCandidateSuccess() {
disconnectButton.disabled = false;
}
// Handle an error that occurs during addition of ICE candidate.
function handleAddCandidateError() {
console.log("Oh noes! addICECandidate failed!");
}
// Handles clicks on the "Send" button by transmitting
export function sendMessage() {
if (connected === false) {
return
}
var message = messageInputBox.value;
sendChannel.send(message);
messageInputBox.value = "";
messageInputBox.focus();
}
// Handle status changes on the local end of the data
function handleSendChannelStatusChange(event) {
console.log('on open fired???');
if (sendChannel) {
var state = sendChannel.readyState;
if (state === "open") {
messageInputBox.disabled = false;
messageInputBox.focus();
sendButton.disabled = false;
disconnectButton.disabled = false;
connectButton.disabled = true;
} else {
messageInputBox.disabled = true;
sendButton.disabled = true;
connectButton.disabled = false;
disconnectButton.disabled = true;
}
}
}
// Called when the connection opens and the data
// channel is ready to be connected to the remote.
function receiveChannelCallback(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
}
// Handle onmessage events for the receiving channel.
// These are the data messages sent by the sending channel.
function handleReceiveMessage(event) {
var el = document.createElement("p");
var txtNode = document.createTextNode(event.data);
el.appendChild(txtNode);
receiveBox.appendChild(el);
}
// Handle status changes on the receiver's channel.
function handleReceiveChannelStatusChange(event) {
if (receiveChannel) {
console.log("Receive channel's status has changed to " +
receiveChannel.readyState);
}
// Here you would do stuff that needs to be done
// when the channel's status changes.
}
/
function disconnectPeers() {
// Close the RTCDataChannels if they're open.
sendChannel.close();
receiveChannel.close();
// Close the RTCPeerConnections
localConnection.close();
remoteConnection.close();
sendChannel = null;
receiveChannel = null;
localConnection = null;
remoteConnection = null;
// Update user interface elements
connectButton.disabled = false;
disconnectButton.disabled = true;
sendButton.disabled = true;
messageInputBox.value = "";
messageInputBox.disabled = true;
}
window.addEventListener('load', startup, false);
After a long time I found the answer it is a bug in the new chrome, the solution is to build the app for Android 10, not 11.