WebRTC one to one without signaling server

To make peer to peer connection users should exchange so called sdp which determines a way to exchange data directly between browsers without servers

Usually in all demos there is websocket server which is used to handle this exchange but for better understanding of how it works we are going to exchange this by hands

How WebRTC connection established

Note that this is kind of simplified (there is no exchange of ice candidates) but in general it is how it works

demo

So from initiator side we are:

const offer = await connection.createOffer()
await connection.setLocalDescription(offer)

Then sending this offer to peer and on his side we are:

await connection.setRemoteDescription(offer)
const answer = await connection.createAnswer()
await connection.setLocalDescription(answer)

Then sending this answer back to initiator and on his side we are:

await connection.setRemoteDescription(answer)

Note that I have missed ice candidates exchanges here to reduce example size and give better understanding of what is going on

And here is full code

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>webrtc</title>
</head>
<body>
    <script>
        let channel = null

        const connection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] })

        connection.ondatachannel = event => {
            console.log('ondatachannel')
            channel = event.channel
            // channel.onopen = event => console.log('onopen', event);
            // channel.onmessage = event => console.log('onmessage', event);
            channel.onmessage = event => alert(event.data)
        }

        connection.onconnectionstatechange = event => document.getElementById("connectionState").innerText = connection.connectionState // console.log('onconnectionstatechange', connection.connectionState)
        connection.oniceconnectionstatechange = event => document.getElementById("iceConnectionState").innerText = connection.iceConnectionState // console.log('oniceconnectionstatechange', connection.iceConnectionState)

        async function step_1_initiator_create_offer() {
            channel = connection.createDataChannel('data')
            // channel.onopen = event => console.log('onopen', event)
            // channel.onmessage = event => console.log('onmessage', event)
            channel.onmessage = event => alert(event.data)

            connection.onicecandidate = event => {
                // console.log('onicecandidate', event)
                if (!event.candidate) {
                    document.getElementById("createdOffer").value = JSON.stringify(connection.localDescription)
                    document.getElementById("createdOffer").hidden = false
                }
            }

            const offer = await connection.createOffer()
            await connection.setLocalDescription(offer)
        }

        async function step_2_accept_remote_offer() {
            const offer = JSON.parse(document.getElementById("remoteOffer").value)
            await connection.setRemoteDescription(offer)
        }

        async function step_3_create_answer() {
            connection.onicecandidate = event => {
                // console.log('onicecandidate', event)
                if (!event.candidate) {
                    document.getElementById("createdAnswer").value = JSON.stringify(connection.localDescription)
                    document.getElementById("createdAnswer").hidden = false
                }
            }
            
            const answer = await connection.createAnswer()
            await connection.setLocalDescription(answer)
        }

        async function step_4_accept_answer() {
            const answer = JSON.parse(document.getElementById("remoteAnswer").value)
            await connection.setRemoteDescription(answer)
        }

        async function send_text() {
            const text = document.getElementById("text").value

            channel.send(text)
        }
    </script>

    <table width="100%" border="1">
        <tr>
            <th>#</th>
            <th>initiator</th>
            <th>peer</th>
        </tr>
        <tr>
            <td>step 1</td>
            <td>
                <input type="button" value="create offer" onclick="step_1_initiator_create_offer()">
                <input id="createdOffer" type="text" hidden>
            </td>
            <td></td>
        </tr>
        <tr>
            <td>step 2</td>
            <td></td>
            <td>
                <input id="remoteOffer" type="text" placeholder="offer from initiator">
                <input type="button" value="accept offer" onclick="step_2_accept_remote_offer()">
            </td>
        </tr>
        <tr>
            <td>step 3</td>
            <td></td>
            <td>
                <input type="button" value="create answer" onclick="step_3_create_answer()">
                <input id="createdAnswer" type="text" hidden>
            </td>
        </tr>
        <tr>
            <td>step 4</td>
            <td>
                <input id="remoteAnswer" type="text" placeholder="answer from peer">
                <input type="button" value="accept answer" onclick="step_4_accept_answer()">
            </td>
            <td></td>
        </tr>
    </table>
    <hr>
    <input id="text" type="text">
    <input type="button" value="send" onclick="send_text()">
    <hr>
    <table border="1">
        <tr>
            <th colspan="2">connection</th>
        </tr>
        <tr>
            <th>connectionState</th>
            <td id="connectionState">unknown</td>
        </tr>
        <tr>
            <th>iceConnectionState</th>
            <td id="iceConnectionState">unknown</td>
        </tr>
    </table>
</body>
</html>