Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebRTC connectionState stuck at "new" - Safari only, works in Chrome and FF

I'm having trouble connecting to my local peer as remote with WebRTC video and audio. This issue is only happening in Safari on desktop and iOS. On Chrome and Firefox the issue is non-existant.

I'm assuming it has something to do with the fact that in Safari, it always asks if you want to allow audio/video but I'm not sure. That's just the only difference I can make out between the browsers. Even after selecting 'allow', the issue persists.

Reproduction steps:

  • In Chrome, open the initial local connection with audio/video
  • In Safari, open the remote connection and choose to enable audio/video

Result:

  • Local connection never makes an offer and the connectionState of the remote (Safari) gets stuck as new. See the following RTCPeerConnection object:

rtcSafari

Here is the exact same object via the exact same steps, but in Chrome or Firefox:

rtcChrome

Edit:

After more testing, I've found the following:

  • Below format: (First Connection) > (Second Connection)

  • Chrome > Chrome: Works

  • Chrome > Firefox: Works

  • Chrome > Safari: Doesn't work

  • Safari > Chrome: Works

  • Safari > Safari: Works

The issue doesn't seem to exist when using Safari for both sides of the connection...only when Safari is used as the secondary connection.

Here is my code:

import h from './helpers.js';

document.getElementById('close-chat').addEventListener('click', (e) => {
    document.querySelector('#right').style.display = "none";
});

document.getElementById('open-chat').addEventListener('click', (e) => {
    document.querySelector('#right').style.display = "flex";
});

window.addEventListener('load', () => {
    sessionStorage.setItem('connected', 'false');

    const room = h.getParam('room');
    const user = h.getParam('user');

    sessionStorage.setItem('username', user);

    const username = sessionStorage.getItem('username');

    if (!room) {
        document.querySelector('#room-create').attributes.removeNamedItem('hidden');
    }

    else if (!username) {
        document.querySelector('#username-set').attributes.removeNamedItem('hidden');
    }

    else {
        let commElem = document.getElementsByClassName('room-comm');

        for (let i = 0; i < commElem.length; i++) {
            commElem[i].attributes.removeNamedItem('hidden');
        }

        var pc = [];

        let socket = io('/stream');

        var socketId = '';
        var myStream = '';
        var screen = '';

        // Get user video by default
        getAndSetUserStream();

        socket.on('connect', () => {
            console.log('Connected');

            sessionStorage.setItem('remoteConnected', 'false');
            h.connectedChat();
            setTimeout(h.establishingChat, 3000);
            setTimeout(h.oneMinChat, 60000);
            setTimeout(h.twoMinChat, 120000);
            setTimeout(h.threeMinChat, 180000);
            setTimeout(h.fourMinChat, 240000);
            setTimeout(h.fiveMinChat, 300000);

            // Set socketId
            socketId = socket.io.engine.id;

            socket.emit('subscribe', {
                room: room,
                socketId: socketId
            });

            socket.on('new user', (data) => {
                // OG user gets log when new user joins here.
                console.log('New User');
                console.log(data);

                socket.emit('newUserStart', { to: data.socketId, sender: socketId });
                pc.push(data.socketId);
                init(true, data.socketId);
            });

            socket.on('newUserStart', (data) => {
                console.log('New User Start');
                console.log(data);

                pc.push(data.sender);
                init(false, data.sender);
            });

            socket.on('ice candidates', async (data) => {
                console.log('Ice Candidates:');
                console.log(data);

                data.candidate ? await pc[data.sender].addIceCandidate(new RTCIceCandidate(data.candidate)) : '';
            });

            socket.on('sdp', async (data) => {
                console.log('SDP:');
                console.log(data);

                if (data.description.type === 'offer') {
                    data.description ? await pc[data.sender].setRemoteDescription(new RTCSessionDescription(data.description)) : '';

                    h.getUserFullMedia().then(async (stream) => {
                        if (!document.getElementById('local').srcObject) {
                            h.setLocalStream(stream);
                        }

                        // Save my stream
                        myStream = stream;

                        stream.getTracks().forEach((track) => {
                            pc[data.sender].addTrack(track, stream);
                        });

                        let answer = await pc[data.sender].createAnswer();

                        await pc[data.sender].setLocalDescription(answer);

                        socket.emit('sdp', { description: pc[data.sender].localDescription, to: data.sender, sender: socketId });
                    }).catch((e) => {
                        console.error(e);
                    });
                }

                else if (data.description.type === 'answer') {
                    await pc[data.sender].setRemoteDescription(new RTCSessionDescription(data.description));
                }
            });

            socket.on('chat', (data) => {
                h.addChat(data, 'remote');
            });
        });

        function getAndSetUserStream() {
            console.log('Get and set user stream.');

            h.getUserFullMedia({ audio: true, video: true }).then((stream) => {
                // Save my stream
                myStream = stream;

                h.setLocalStream(stream);
            }).catch((e) => {
                console.error(`stream error: ${e}`);
            });
        }

        function sendMsg(msg) {
            let data = {
                room: room,
                msg: msg,
                sender: username
            };

            // Emit chat message
            socket.emit('chat', data);

            // Add localchat
            h.addChat(data, 'local');
        }

        function init(createOffer, partnerName) {
            console.log('P1:');
            console.log(partnerName);

            pc[partnerName] = new RTCPeerConnection(h.getIceServer());

            console.log('P2:');
            console.log(pc[partnerName]);

            if (screen && screen.getTracks().length) {
                console.log('Screen:');
                console.log(screen);

                screen.getTracks().forEach((track) => {
                    pc[partnerName].addTrack(track, screen); // Should trigger negotiationneeded event
                });
            }

            else if (myStream) {
                console.log('myStream:');
                console.log(myStream);

                myStream.getTracks().forEach((track) => {
                    pc[partnerName].addTrack(track, myStream); // Should trigger negotiationneeded event
                });
            }

            else {
                h.getUserFullMedia().then((stream) => {
                    console.log('Stream:');
                    console.log(stream);

                    // Save my stream
                    myStream = stream;

                    stream.getTracks().forEach((track) => {
                        console.log('Tracks:');
                        console.log(track);

                        pc[partnerName].addTrack(track, stream); // Should trigger negotiationneeded event
                    });

                    h.setLocalStream(stream);
                }).catch((e) => {
                    console.error(`stream error: ${e}`);
                });
            }

            // Create offer
            if (createOffer) {
                console.log('Create Offer');

                pc[partnerName].onnegotiationneeded = async () => {
                    let offer = await pc[partnerName].createOffer();

                    console.log('Offer:');
                    console.log(offer);

                    await pc[partnerName].setLocalDescription(offer);

                    console.log('Partner Details:');
                    console.log(pc[partnerName]);

                    socket.emit('sdp', { description: pc[partnerName].localDescription, to: partnerName, sender: socketId });
                };
            }

            // Send ice candidate to partnerNames
            pc[partnerName].onicecandidate = ({ candidate }) => {
                console.log('Send ICE Candidates:');
                console.log(candidate);

                socket.emit('ice candidates', { candidate: candidate, to: partnerName, sender: socketId });
            };

            // Add
            pc[partnerName].ontrack = (e) => {
                console.log('Adding partner video...');

                let str = e.streams[0];
                if (document.getElementById(`${partnerName}-video`)) {
                    document.getElementById(`${partnerName}-video`).srcObject = str;
                }

                else {
                    // Video elem
                    let newVid = document.createElement('video');
                    newVid.id = `${partnerName}-video`;
                    newVid.srcObject = str;
                    newVid.autoplay = true;
                    newVid.className = 'remote-video';
                    newVid.playsInline = true;
                    newVid.controls = true;

                    // Put div in main-section elem
                    document.getElementById('left').appendChild(newVid);

                    const video = document.getElementsByClassName('remote-video');
                }
            };

            pc[partnerName].onconnectionstatechange = (d) => {
                console.log('Connection State:');
                console.log(pc[partnerName].iceConnectionState);

                switch (pc[partnerName].iceConnectionState) {
                    case 'new':
                        console.log('New connection...!');
                        break;
                    case 'checking':
                        console.log('Checking connection...!');
                        break;
                    case 'connected':
                        console.log('Connected with dispensary!');
                        sessionStorage.setItem('remoteConnected', 'true');
                        h.establishedChat();
                        break;
                    case 'disconnected':
                        console.log('Disconnected');
                        sessionStorage.setItem('connected', 'false');
                        sessionStorage.setItem('remoteConnected', 'false');
                        h.disconnectedChat();
                        h.closeVideo(partnerName);
                        break;
                    case 'failed':
                        console.log('Failed');
                        sessionStorage.setItem('connected', 'false');
                        sessionStorage.setItem('remoteConnected', 'false');
                        h.disconnectedChat();
                        h.closeVideo(partnerName);
                        break;
                    case 'closed':
                        console.log('Closed');
                        sessionStorage.setItem('connected', 'false');
                        sessionStorage.setItem('remoteConnected', 'false');
                        h.disconnectedChat();
                        h.closeVideo(partnerName);
                        break;
                }
            };

            pc[partnerName].onsignalingstatechange = (d) => {
                switch (pc[partnerName].signalingState) {
                    case 'closed':
                        console.log("Signalling state is 'closed'");
                        h.closeVideo(partnerName);
                        break;
                }
            };
        }

        // Chat textarea
        document.getElementById('chat-input').addEventListener('keypress', (e) => {
            if (e.which === 13 && (e.target.value.trim())) {
                e.preventDefault();

                sendMsg(e.target.value);

                setTimeout(() => {
                    e.target.value = '';
                }, 50);
            }
        });
    }
});

like image 664
Joe Berthelot Avatar asked Nov 07 '22 01:11

Joe Berthelot


1 Answers

It would be helpful to see the console logs from a failed (stuck in the "new" state) Safari run.

One possibility is that Safari isn't doing the full ice candidate gathering. As Phillip Hancke noted, seeing the SDP would help figure out if that's happening. As would seeing the console logs. In the past, Safari has had various quirks and bugs related to candidate gathering.

One way to force Safari to gather candidates is to explicitly set offerToReceiveAudio and offerToReceiveVideo:

await pc[partnerName].createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
like image 119
Kwindla Kramer Avatar answered Nov 12 '22 18:11

Kwindla Kramer