1

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

Jonathan
  • 21
  • 7
  • please look here https://stackoverflow.com/questions/58429603/onicecandidate-is-never-call-after-setremotedescription-webrtc-android – PRAJIN PRAKASH Oct 19 '21 at 14:23
  • 1
    It would be more helpful if you posted the version that you are running currently of both firefox and chrome. Also, you probably already did this, but, try disabling all your extensions that you have installed. – Marcelo Lacerda Oct 19 '21 at 14:25
  • 1
    Thanks @MarceloLacerda The phone I tested it on is only used for development so it has no extensions. – Jonathan Oct 20 '21 at 01:04
  • @PRAJINPRAKASH I've already checked out that link but it is a different issue. They never set the local description so it was not working anywhere. – Jonathan Oct 20 '21 at 01:12
  • As my experience, you need not to convert the ICE candidate to JSON. – The KNVB Oct 20 '21 at 04:08
  • 1
    Your WebRTC implementation is updated, you may refer the following link to update your WebRTC implementation. https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation – The KNVB Oct 20 '21 at 04:10
  • @TheKNVB Thank you, I need to convert it to JSON cause I'm storing it on firebase, also that won't help because 'candidates found' is never logged so the event is never fired in the first place. Also I read through the guide you provided and couldn't find any semantic differences in my code. – Jonathan Oct 20 '21 at 05:03
  • It turns out this is a known bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1115068, I managed to reproduce the exact bug listed in that link the same way they did. The phone I tested it on was Android 11 as well but not beta so this bug must've slipped through the cracks. – Jonathan Oct 21 '21 at 03:02

1 Answers1

1

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.

Jonathan
  • 21
  • 7
  • This solved it for me. Thanks! – alexward1230 Nov 27 '21 at 09:24
  • do we have any other ways around this? Looks like we can no longer set our targetSdk to 29 (or below) for google play uploads. So this solution wont work if you need to publish to google play. Any ideas? Ive tried to go to GeckoView but its been a lot of work :( – alexward1230 Dec 11 '21 at 21:32
  • I have not found anything, it is definitely a bug though and google already knows about it so they should fix it in the future. – Jonathan Dec 12 '21 at 11:36