1

I created a simple peer-to-peer app using NodeJS and WebRTC for something like a one-to-many livestreaming application.

So far it is working on my localhost but when I deployed the app on a production VM server on Google Cloud Platform, I can't create a DataChannel using peer.createDataChannel(). Or at least that is the issue that I see because it is not throwing any errors.

server.js

const port = process.env.PORT || 80;

const express = require('express');
const bodyParser = require('body-parser');
const webrtc = require('wrtc');

const app = express();

const status = {
    offline: 'offline',
    online: 'online',
    streaming: 'streaming'
};

let hostStream;
let hostChannel;
let channelData = {
    status: status.offline,
    message: null
};

app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.post('/broadcast', async ({ body }, res) => {
    try {
        let peer = new webrtc.RTCPeerConnection({
            iceServers: [
                {
                    urls: "stun:stun.stunprotocol.org"
                }
            ]
        });

        peer.ontrack = (e) => handleTrackEvent(e, peer);
        peer.ondatachannel = (e) => handleHostDataChannelEvent(e);

        let desc = new webrtc.RTCSessionDescription(body.sdp);
        await peer.setRemoteDescription(desc);

        let answer = await peer.createAnswer();
        await peer.setLocalDescription(answer);

        let payload = {
            sdp: peer.localDescription,
            status: channelData.status
        };

        res.json(payload);
    } catch (e) {
        console.log(e);
    }
});

function handleTrackEvent(e, peer) {
    hostStream = e.streams[0];
}

function handleHostDataChannelEvent(e) {
    let channel = e.channel;

    channel.onopen = function(event) {
        channelData.message = '[ SERVER ]: Peer-to-peer data channel has been created.';
        channel.send(JSON.stringify(channelData));
        channelData.message = null;
    }
    channel.onmessage = function(event) {
        console.log(event.data);
    }

    hostChannel = channel;
}

app.listen(port, () => console.log('[ SERVER ]: Started'));

streamer.js

function createPeer() {
    let peer = new RTCPeerConnection({
        iceServers: [
            {
                urls: "stun:stun.stunprotocol.org"
            }
        ]
    });

    let channel = peer.createDataChannel('host-server');
    channel.onopen = function(event) {
        channel.send('Host: Data Channel Opened');
    }
    channel.onmessage = function(event) {
        let data = JSON.parse(event.data);

        if('status' in data) {
            $('body').removeClass().addClass(data.status);
        }

        if('message' in data && data.message != null) {
            $.toast({
                heading: 'Data Channel',
                text: data.message,
                showHideTransition: 'slide',
                icon: 'info',
                position: 'top-center',
                stack: false
            })
        }
    }

    peer.onnegotiationneeded = () => handleNegotiationNeededEvent(peer);

    return peer;
}

On my localhost, when the host (streamer.js) starts streaming media, the server outputs Host: Data Channel Opened on the console and on the host's browser, I see the toast with a message Server: Peer-to-peer data channel has been created.. However when I try the application on my production server the server doesn't log that on the console and the host's browser doesn't open a toast with the message saying the data channel has been created.

There are no errors on both the browser console nor the server console so I don't really know where the problem is.

bassicplays
  • 328
  • 1
  • 7
  • 21

1 Answers1

2

I do not see the gathering of ice candidates in your code - so it is no surprise your peers cannot establish a connection with each other. Here is the working sample of what your code should look like.

streamer.js:

async function createPeer(configuration) {
  const localCandidates = [];
  // Step 1. Create new RTCPeerConnection
  const peer = new RTCPeerConnection(configuration);
  peer.onconnectionstatechange = (event) => {
    console.log('Connection state:', peer.connectionState);
  };
  peer.onsignalingstatechange = (event) => {
    console.log('Signaling state:', peer.signalingState);
  };
  peer.oniceconnectionstatechange = (event) => {
    console.log('ICE connection state:', peer.iceConnectionState);
  };
  peer.onicegatheringstatechange = (event) => {
    console.log('ICE gathering state:', peer.iceGatheringState);
  };
  // Step 5. Gathering local ICE candidates
  peer.onicecandidate = async (event) => {
    if (event.candidate) {
      localCandidates.push(event.candidate);
      return;
    }
    // Step 6. Send Offer and client candidates to server
    const response = await fetch('/broadcast', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        offer: offer,
        candidates: localCandidates,
      }),
    });
    const {answer, candidates} = await response.json();
    // Step 7. Set remote description with Answer from server
    await peer.setRemoteDescription(answer);
    // Step 8. Add ICE candidates from server
    for (let candidate of candidates) {
      await peer.addIceCandidate(candidate);
    }
  };
  // Step 2. Create new Data channel
  const dataChannel = peer.createDataChannel('host-server');
  dataChannel.onopen = (event) => {
    dataChannel.send('Hello from client!');
  };
  dataChannel.onclose = (event) => {
    console.log('Data channel closed');
  };
  dataChannel.onmessage = (event) => {
    console.log('Data channel message:', event.data);
  };
  // Step 3. Create Offer
  const offer = await peer.createOffer();
  // Step 4. Set local description with Offer from step 3
  await peer.setLocalDescription(offer);
  return peer;
}

const configuration = {
  iceServers: [
    {
      urls: 'stun:global.stun.twilio.com:3478?transport=udp',
    },
  ],
};
// Add turn server to `configuration.iceServers` if needed.
// See more at https://www.twilio.com/docs/stun-turn

createPeer(configuration);

server.js:

const express = require('express');
const bodyParser = require('body-parser');
const webrtc = require('wrtc');

const port = process.env.PORT || 80;
const configuration = {
  iceServers: [
    {
      urls: 'stun:global.stun.twilio.com:3478?transport=udp',
    },
  ],
};
// Add turn server to `configuration.iceServers` if needed.

const app = express();
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.post('/broadcast', async (req, res) => {
  const {offer, candidates} = req.body;
  const localCandidates = [];
  let dataChannel;
  // Step 1. Create new RTCPeerConnection
  const peer = new webrtc.RTCPeerConnection(configuration);
  peer.ondatachannel = (event) => {
    dataChannel = event.channel;
    dataChannel.onopen = (event) => {
      dataChannel.send('Hello from server!');
    };
    dataChannel.onclose = (event) => {
      console.log('Data channel closed');
    };
    dataChannel.onmessage = (event) => {
      console.log('Data channel message:', event.data);
    };
  };
  peer.onconnectionstatechange = (event) => {
    console.log('Connection state:', peer.connectionState);
  };
  peer.onsignalingstatechange = (event) => {
    console.log('Signaling state:', peer.signalingState);
  };
  peer.oniceconnectionstatechange = (event) => {
    console.log('ICE connection state:', peer.iceConnectionState);
  };
  peer.onicegatheringstatechange = (event) => {
    console.log('ICE gathering state:', peer.iceGatheringState);
  };
  peer.onicecandidate = (event) => {
    // Step 6. Gathering local ICE candidates
    if (event.candidate) {
      localCandidates.push(event.candidate);
      return;
    }
    // Step 7. Response with Answer and server candidates
    let payload = {
      answer: peer.localDescription,
      candidates: localCandidates,
    };
    res.json(payload);
  };
  // Step 2. Set remote description with Offer from client
  await peer.setRemoteDescription(offer);
  // Step 3. Create Answer
  let answer = await peer.createAnswer();
  // Step 4. Set local description with Answer from step 3
  await peer.setLocalDescription(answer);
  // Step 5. Add ICE candidates from client
  for (let candidate of candidates) {
    await peer.addIceCandidate(candidate);
  }
});

app.listen(port, () => console.log('Server started on port ' + port));

I found your stun server not fully functional, so I replaced it with another one from Twillio. Also, I added event handlers with which it is easy to track the state of the WebRTC session. You would do well to learn more about WebRTC connection flow, really.

  • I updated my question to include more code from the `server.js` file. Could you take a look? – bassicplays Jan 05 '22 at 19:59
  • Yes, sure. I have updated the answer with code samples. – Denis Barsukov Jan 10 '22 at 09:47
  • Thank you, I did obliterate the VM I had running this app, I will try to recreate one later today if I have the time but thanks for explaining where I was having an issue with. Out of curiosity regarding `twilio`, do I have to create an account with them? Does my stream data go through their servers? I'm assuming it doesn't based on the link you provided but I just want to make sure. – bassicplays Jan 11 '22 at 16:33
  • 1
    You do not need an account for [stun server](https://www.twilio.com/stun-turn). Your stream data go through their server only if you use their turn server. – Denis Barsukov Jan 11 '22 at 20:42
  • This looks to be going in to the right direction. However I still cant view the host's media stream when a peer connects and starts watching. I may have messed up my code. Do you mind including a simple code that does that and not just establishing the gathering of ICE candidates? – bassicplays Jan 12 '22 at 05:10
  • Have updated with step-by-step description. Here is a good guide of p2p application using turn server https://www.twilio.com/blog/2014/12/set-phasers-to-stunturn-getting-started-with-webrtc-using-node-js-socket-io-and-twilios-nat-traversal-service.html – Denis Barsukov Jan 12 '22 at 06:53