Reputation: 358
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.
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'));
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.
Upvotes: 1
Views: 1070
Reputation:
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.
Upvotes: 2