webrtc / samples

WebRTC Web demos and samples
https://webrtc.github.io/samples
BSD 3-Clause "New" or "Revised" License
13.94k stars 5.71k forks source link

Send SDP answer without signalling server #1615

Closed AlexeyBoiko closed 1 year ago

AlexeyBoiko commented 1 year ago

Please read first!

Please use discuss-webrtc for general technical discussions and questions. If you have found an issue/bug with the native libwebrtc SDK or a browser's behaviour around WebRTC please create an issue in the relevant bug tracker. You can find more information on how to submit a bug and do so in the right place here

Note: If the checkboxes above are not checked (which you do after the issue is posted), the issue will be closed.

Browser affected

Version 115.0.5790.171 (Official Build) (64-bit)

Description

Is it possible to send SDP answer over RTCPeerConnection without signaling server? Exmaple 2 shows what I am asking for.

Steps to reproduce

Example 1. WebRTC communication, when SDP answer send over signaling server.

LOCAL                 Singnaling    REMOTE
peer                  service       peer
  |                      |             |
create SDP offer         |             |
send SDP offer---------->|------------>|
  |                      |     setRemoteSdp(SDP offer)
  |                      |     create SDP answer
  |<---------------------|<----send SDP answer <======
  |                      |             |
setRemoteSdp(SDP answer) |             |
create ICE candidate 1   |             |
send ICE candidate 1---->|------------>|
  |                      |     addIceCandidate(ICE 1)
create ICE candidate 2   |             |
send ICE candidate 2---->|------------>|
  |                            addIceCandidate(ICE 2)
  |                                    |
  |                            select best ICE
  |<---------------------------send REMOTE ICE over WebRTC
  |                                    |
  |<-------Peer2Peer over WebRTC------>|

It is mock signaling server:

const signaling = {
    /** @param {string} senderId,  @param {RTCSessionDescription} sdp */
    sdpSend: (senderId, sdp) => {
        console.log(`${senderId} SDP send`);
        document.dispatchEvent(new CustomEvent('sdp', {detail: {senderId, sdp} }))
    },

    /** @param {string} clientId,  @param {(sdp:RTCSessionDescription)=>void} callBack */
    sdpOnGet: (clientId, callBack) =>
        document.addEventListener('sdp', evt => {
            if (evt.detail.senderId !== clientId) {
                console.log(`${clientId} SDP onGet`);
                callBack(evt.detail.sdp)
            }
        }),

    /** @param {string} senderId,  @param {RTCIceCandidate} candidate */
    iceCandidateSend: (senderId, candidate) => {
        console.log(`${senderId} iceCandidate send`);
        document.dispatchEvent(new CustomEvent('ice', {detail: {senderId, candidate} }))
    },

    /** @param {string} clientId,  @param {(candidate:RTCIceCandidate)=>void} callBack */
    iceCandidateOnGet: (clientId, callBack) =>
        document.addEventListener('ice', evt => {
            if (evt.detail.senderId !== clientId) {
                console.log(`${clientId} iceCandidate onGet`);
                callBack(evt.detail.candidate);
            }
        })
};

Here is working code

//
// remote peer
{
    const remoteClientId = 'REMOTE';

    const remoteCon = new RTCPeerConnection();
    remoteCon.ondatachannel = evt => {
        console.log('REMOTE ondatachannel');
        const channel = evt.channel;
        channel.onopen = (event) => {
            console.log('REMOTE channel onopen');

            const msg = 'Hi from remote';
            console.log(`REMOTE message send: ${msg}`);
            channel.send(msg);
        };
        channel.onmessage = (event) => {
            console.log('REMOTE onmessage:', event.data);
        };
    };

    signaling.iceCandidateOnGet(remoteClientId, async candidate => 
        await remoteCon.addIceCandidate(candidate));

    signaling.sdpOnGet(remoteClientId, async sdpOffer => {
        await remoteCon.setRemoteDescription(sdpOffer);
        await remoteCon.setLocalDescription();
        signaling.sdpSend(remoteClientId, remoteCon.localDescription);
    });
}

//
// local peer
{
    const localClientId = 'LOCAL';

    const localCon = new RTCPeerConnection();
    const localChannel = localCon.createDataChannel('chat');
    localChannel.onopen = evt => {
        console.log('LOCAL channel onopen');

        const msg = 'Hi from local';
        console.log(`LOCAL message send: ${msg}`);
        localChannel.send(msg);
    };
    localChannel.onmessage = evt =>
        console.log('LOCAL onmessage:', evt.data);  

    signaling.sdpOnGet(localClientId, async sdpRemote => {
        await localCon.setRemoteDescription(sdpRemote);

        localCon.onicecandidate = evt => {
            if (evt.candidate) {
                signaling.iceCandidateSend(localClientId, evt.candidate);
            }
        }
    });

    // SDP
    // creates and sets SDP
    await localCon.setLocalDescription();
    signaling.sdpSend(localClientId, localCon.localDescription);
}

Console ouput:

LOCAL SDP send
REMOTE SDP onGet
REMOTE SDP send
LOCAL SDP onGet
LOCAL iceCandidate send
REMOTE iceCandidate onGet
LOCAL channel onopen
LOCAL message send: Hi from local
REMOTE ondatachannel
REMOTE channel onopen
REMOTE message send: Hi from remote
REMOTE onmessage: Hi from local
LOCAL onmessage: Hi from remote

Example 2. WebRTC communication, when SDP answer send over WebRTC What I am asking for.

As I understand: LOCAL peer can create SDP, collect all ICE Candidates, and send all at once to REMOTE peer over Signaling Server. REMOTE peer know SDP offer and ICE Candidates -> can send SDP answer over WebRTC.

LOCAL                 Singnaling    REMOTE
peer                  service       peer
  |                      |             |
create SDP offer         |             |
create ICE candidate 1   |             |
create ICE candidate 2   |             |
  |                      |             |
send SDP, ICE1 , ICE 2-->|------------>|
  |                            setRemoteSdp(SDP offer)
  |                            create SDP answer
  |                            addIceCandidate(ICE 1)
  |                            addIceCandidate(ICE 2)
  |                            select best ICE
  |<---------------------------send SDP answer, ICEs over WebRTC
  |                                    |
  |<-------Peer2Peer over WebRTC------>|

singnaling service:

const signaling = {
    /** @param {string} senderId,  @param {RTCSessionDescription} sdp, @param {RTCIceCandidate} iceCandidates */
    sdpAndIceChannelsSend: (senderId, sdp, iceCandidates) => {
        console.log(`${senderId} SDP and ICECandidates send`);
        document.dispatchEvent(new CustomEvent('sdpAndIce', {detail: { senderId, sdp, iceCandidates } }));
    },

    /** @param {string} clientId,  @param {(sdp:RTCSessionDescription, iceCandidates:RTCIceCandidate[])=>void} callBack */
    sdpAndIceChannelsOnGet: (clientId, callBack) =>
        document.addEventListener('sdpAndIce', evt => {
            if (evt.detail.senderId !== clientId) {
                console.log(`${clientId} SDP and iceCandidates onGet`);
                callBack(evt.detail.sdp, evt.detail.iceCandidates);
            }
        }),
};

Not working code

//
// remote peer
{
    const remoteClientId = 'REMOTE';

    const remoteCon = new RTCPeerConnection();
    remoteCon.ondatachannel = evt => {
        console.log('REMOTE ondatachannel');
        const channel = evt.channel;
        channel.onopen = (event) => {
            console.log('REMOTE channel onopen');

            // send message to LOCAL
            const msg = 'Hi from remote';
            console.log(`REMOTE message send: ${msg}`);
            channel.send(msg);
        };
        channel.onmessage = (event) => {
            console.log('REMOTE onmessage:', event.data);
        };
    };

    signaling.sdpAndIceChannelsOnGet(remoteClientId, async (sdpOffer, iceCandidates) => {
        await remoteCon.setRemoteDescription(sdpOffer);
        await remoteCon.setLocalDescription();

        for(const iceCandidate of iceCandidates)
            await remoteCon.addIceCandidate(iceCandidate);
    });
}

//
// local peer
{
    const localClientId = 'LOCAL';

    const localCon = new RTCPeerConnection();
    const localChannel = localCon.createDataChannel('chat');
    localChannel.onopen = evt => {
        console.log('LOCAL channel onopen');

        // send message to REMOTE
        const msg = 'Hi from local';
        console.log(`LOCAL message send: ${msg}`);
        localChannel.send(msg);
    };
    localChannel.onmessage = evt =>
        console.log('LOCAL onmessage:', evt.data);  

    // SDP
    // creates and sets SDP
    await localCon.setLocalDescription();

    const iceCandidates = [];
    localCon.onicecandidate = evt => {
        if (evt.candidate) {
            iceCandidates.push(evt.candidate);
        } else {
            // all ice candidates getted
            // send SDP and iceCandidates to REMOTE
            signaling.sdpAndIceChannelsSend(localClientId,
                localCon.localDescription, iceCandidates);
        }
    }
}

Console ouput:

LOCAL SDP and ICECandidates send
REMOTE SDP and iceCandidates onGet

Expected results

Example of WebRTC connection setup without sendind SDP aswer over signaling server.

Actual results

Can't setup WebRTC connection without sendind SDP aswer over signaling server.

alvestrand commented 1 year ago

As I understand:

LOCAL peer can create SDP, collect all ICE Candidates, and send all at once to REMOTE peer over Signaling Server. REMOTE peer know SDP offer and ICE Candidates -> can send SDP answer over WebRTC.

No, it doesn't work that way. The REMOTE peer knows the SDP offer and the ICE candidates, so it can send ICE candidates to the LOCAL peer. But those ICE candidates do not contain the SDP - in particular, they can't send the FINGERPRINT attribute which is critical to establishing the DTLS connection.

People have speculated about the possibility of including the necessary information in the ICE handshake, but so far, nobody (as far as I know) has published a working example of this approach, or undertaken the necessary work of standardization that would be needed to make it part of the official WebRTC spec.

AlexeyBoiko commented 1 year ago

@alvestrand thank you. Could please clatify:

Imagin text chat app. Users can connect to chat by link. To send messages to all users every user must have Peer2Peer WebRTC connection with all users. It is many-to-many. When new user connect to chat, all current users must receive the new user's SDP. The only way current users can get new user's SDP is by using Signling Server.

Thus: every user must have alive socket connection with Signling Server. Socket connection must be alive all time user in chat. If all users have full time alive socket connection - what's the benefit of WebRTC ?

alvestrand commented 1 year ago

That's a completely different question.

Yes, if you want the signaling server to be able to send information about new users, the clients must have a connection open to the signaling server. (This connection can be a WebRTC connection if you want it to be.)

WebRTC permits, but does not require, direct media and datachannel connections between the participants.

On Mon, Aug 14, 2023 at 7:51 AM Alexey Boyko @.***> wrote:

@alvestrand https://github.com/alvestrand thank you. Could please clatify:

Imagin text chat app. Users can connect to chat by link. To send messages to all users every user must have Peer2Peer WebRTC connection with all users. It is many-to-many. When new user connect to chat, all current users must receive the new user's SDP. The only way current users can get new user's SDP is by using Signling Server.

Thus: every user must have alive socket connection with Signling Server. Socket connection must be alive all time user in chat. If all users have full time alive socket connection - what's the benefit of WebRTC ?

— Reply to this email directly, view it on GitHub https://github.com/webrtc/samples/issues/1615#issuecomment-1676720068, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADVM7JPLG4OCM6EQOG47RLXVG4F5ANCNFSM6AAAAAA3O3FV7M . You are receiving this because you were mentioned.Message ID: @.***>

fippo commented 1 year ago

The answer is simple: for that use-case WebRTC offers little benefit. Those come when you add requirements like file transfer and voice/video chat which the signaling/chat server can't handle for capacity reasons.

AlexeyBoiko commented 1 year ago

@alvestrand @fippo thank you. I know my questions is not an issue report. But it is the only place where I can get answers.

Code below exchange only SDP between peers. It does not send ICE candidates. And its working,

LOCAL                      Singnaling    REMOTE
peer                       server       peer
  |                          |             |
create SDP offer             |             |
  |                          |             |
create ICE candidate 1       |             |
create ICE candidate 2       |             |
(just subscribe              |             |
onicecandidate,              |             |
wait for last ICE            |             |
don't send ICEs)             |             |
  |                          |             |
send SDP only without ICEs-->|------------>|
  |                          |     setRemoteSdp(SDP offer)
  |                          |     create SDP answer
  |<-------------------------|<----send SDP answer without ICEs
setRemoteSdp(SDP answer)                   |
  |                                        |
  |<---------Peer2Peer over WebRTC-------->|

Signaling server mock

const signaling = {
    /** @param {string} senderId,  @param {RTCSessionDescription} sdp */
    sdpAndIceChannelsSend: (senderId, sdp) => {
        console.log(`${senderId} SDP send`);
        document.dispatchEvent(new CustomEvent('sdp', {detail: { senderId, sdp } }));
    },

    /** @param {string} clientId,  @param {(sdp:RTCSessionDescription)=>void} callBack */
    sdpAndIceChannelsOnGet: (clientId, callBack) =>
        document.addEventListener('sdp', evt => {
            if (evt.detail.senderId !== clientId) {
                console.log(`${clientId} SDP onGet`);
                callBack(evt.detail.sdp);
            }
        }),
};

code:

//
// remote peer
{
    const remoteClientId = 'REMOTE';

    const remoteCon = new RTCPeerConnection();
    remoteCon.ondatachannel = evt => {
        console.log('REMOTE ondatachannel');
        const channel = evt.channel;
        channel.onopen = (event) => {
            console.log('REMOTE channel onopen');

            // send message to LOCAL
            const msg = 'Hi from remote';
            console.log(`REMOTE message send: ${msg}`);
            channel.send(msg);
        };
        channel.onmessage = (event) => {
            console.log('REMOTE onmessage:', event.data);
        };
    };

    signaling.sdpAndIceChannelsOnGet(remoteClientId, async (sdpOffer, iceCandidates) => {
        await remoteCon.setRemoteDescription(sdpOffer);
        await remoteCon.setLocalDescription();

        signaling.sdpAndIceChannelsSend(remoteClientId, remoteCon.localDescription);
    });
}

//
// local peer
{
    const localClientId = 'LOCAL';

    const localCon = new RTCPeerConnection();
    const localChannel = localCon.createDataChannel('chat');
    localChannel.onopen = evt => {
        console.log('LOCAL channel onopen');

        // send message to REMOTE
        const msg = 'Hi from local';
        console.log(`LOCAL message send: ${msg}`);
        localChannel.send(msg);
    };
    localChannel.onmessage = evt =>
        console.log('LOCAL onmessage:', evt.data);  

    // SDP
    // creates and sets SDP
    await localCon.setLocalDescription();

    signaling.sdpAndIceChannelsOnGet(localClientId, async sdpAnswer =>
        await localCon.setRemoteDescription(sdpAnswer));

    localCon.onicecandidate = evt => {
        if (!evt.candidate) {
            // all ice candidates getted            
            signaling.sdpAndIceChannelsSend(localClientId, localCon.localDescription);
        }
    };
}

Console ouput:

LOCAL SDP send
REMOTE SDP onGet
REMOTE SDP send
LOCAL SDP onGet
LOCAL channel onopen
LOCAL message send: Hi from local
REMOTE ondatachannel
REMOTE channel onopen
REMOTE message send: Hi from remote
REMOTE onmessage: Hi from local
LOCAL onmessage: Hi from remote

Why this code work if ICEs don't exchange?