I'm trying to learn how to create an RTCPeerConnection so that I can use the DataChannel API. Here's what I have tried from what I understood:
var client = new mozRTCPeerConnection;
var server = new mozRTCPeerConnection;
client.createOffer(function (description) {
client.setLocalDescription(description);
server.setRemoteDescription(description);
server.createAnswer(function (description) {
server.setLocalDescription(description);
client.setRemoteDescription(description);
var clientChannel = client.createDataChannel("chat");
var serverChannel = server.createDataChannel("chat");
clientChannel.onmessage = serverChannel.onmessage = onmessage;
clientChannel.send("Hello Server!");
serverChannel.send("Hello Client!");
function onmessage(event) {
alert(event.data);
}
});
});
I'm not sure what's going wrong, but I'm assuming that the connection is never established because no messages are being displayed.
Where do I learn more about this? I've already read the Getting Started with WebRTC - HTML5 Rocks tutorial.
I finally got it to work after sifting through a lot of articles: http://jsfiddle.net/LcQzV/
First we create the peer connections:
var media = {};
media.fake = media.audio = true;
var client = new mozRTCPeerConnection;
var server = new mozRTCPeerConnection;
When the client connects to the server it must open a data channel:
client.onconnection = function () {
var channel = client.createDataChannel("chat", {});
channel.onmessage = function (event) {
alert("Server: " + event.data);
};
channel.onopen = function () {
channel.send("Hello Server!");
};
};
When the client creates a data channel the server may respond:
server.ondatachannel = function (channel) {
channel.onmessage = function (event) {
alert("Client: " + event.data);
};
channel.onopen = function () {
channel.send("Hello Client!");
};
};
We need to add a fake audio stream to the client and the server to establish a connection:
navigator.mozGetUserMedia(media, callback, errback);
function callback(fakeAudio) {
server.addStream(fakeAudio);
client.addStream(fakeAudio);
client.createOffer(offer);
}
function errback(error) {
alert(error);
}
The client creates an offer:
function offer(description) {
client.setLocalDescription(description, function () {
server.setRemoteDescription(description, function () {
server.createAnswer(answer);
});
});
}
The server accepts the offer and establishes a connection:
function answer(description) {
server.setLocalDescription(description, function () {
client.setRemoteDescription(description, function () {
var port1 = Date.now();
var port2 = port1 + 1;
client.connectDataConnection(port1, port2);
server.connectDataConnection(port2, port1);
});
});
}
Phew. That took a while to understand.
I've posted a gist that shows setting up a data connection, compatible with both Chrome and Firefox.
The main difference is that where in FF you have to wait until the connection is set up, in Chrome it's just the opposite: it seems you need to create the data connection before any offers are sent back/forth:
var pc1 = new RTCPeerConnection(cfg, con);
if (!pc1.connectDataConnection) setupDC1(); // Chrome...Firefox defers per other answer
The other difference is that Chrome passes an event object to .ondatachannel whereas FF passes just a raw channel:
pc2.ondatachannel = function (e) {
var datachannel = e.channel || e;
Note that you currently need Chrome Nightly started with --enable-data-channels for it to work as well.
Here is a sequence of events I have working today (Feb 2014) in Chrome. This is for a simplified case where peer 1 will stream video to peer 2.
Set up some way for the peers to exchange messages. (The variance in how people accomplish this is what makes different WebRTC code samples so incommensurable, sadly. But mentally, and in your code organization, try to separate this logic out from the rest.)
On each side, set up message handlers for the important signalling messages. You can set them up and leave them up. There are 3 core messages to handle & send:
an ice candidate sent from the other side ==> call addIceCandidate with it
an offer message ==> SetRemoteDescription with it, then make an answer & send it
an answer message ===> SetRemoteDescription with it
On each side, create a new peerconnection object and attach event handlers to it for important events: onicecandidate, onremovestream, onaddstream, etc.
ice candidate ===> send it to other side
stream added ===> attach it to a video element so you can see it
When both peers are present and all the handlers are in place, peer 1 gets a trigger message of some kind to start video capture (using the getUserMedia call)
Once getUserMedia succeeds, we have a stream. Call addStream on the peer 1's peer connection object.
Then -- and only then -- peer 1 makes an offer
Due to the handlers we set up in step 2, peer 2 gets this and sends an answer
Concurrently with this (and somewhat obscurely), the peer connection object starts producing ice candidates. They get sent back and forth between the two peers and handled (steps 2 & 3 above)
Streaming starts by itself, opaquely, as a result of 2 conditions:
offer/answer exchange
ice candidates received, exchanged, and added
I haven't found a way to add video after step 9. When I want to change something, I go back to step 3.
Related
My project works as intended except that I have to refresh the browser every time my keyword list sends something to it to display. I assume it's my inexperience with Expressjs and not creating the route correctly within my websocket? Any help would be appreciated.
Browser
let socket = new WebSocket("ws://localhost:3000");
socket.addEventListener('open', function (event) {
console.log('Connected to WS server')
socket.send('Hello Server!');
});
socket.addEventListener('message', function (e) {
const keywordsList = JSON.parse(e.data);
console.log("Received: '" + e.data + "'");
document.getElementById("keywordsList").innerHTML = e.data;
});
socket.onclose = function(code, reason) {
console.log(code, reason, 'disconnected');
}
socket.onerror = error => {
console.error('failed to connect', error);
};
Server
const ws = require('ws');
const express = require('express');
const keywordsList = require('./app');
const app = express();
const port = 3000;
const wsServer = new ws.Server({ noServer: true });
wsServer.on('connection', function connection(socket) {
socket.send(JSON.stringify(keywordsList));
socket.on('message', message => console.log(message));
});
// `server` is a vanilla Node.js HTTP server, so use
// the same ws upgrade process described here:
// https://www.npmjs.com/package/ws#multiple-servers-sharing-a-single-https-server
const server = app.listen(3000);
server.on('upgrade', (request, socket, head) => {
wsServer.handleUpgrade(request, socket, head, socket => {
wsServer.emit('connection', socket, request);
});
});
In answer to "How to Send and/or Stream array data that is being continually updated to a client" as arrived at in comment.
A possible solution using WebSockets may be to
Create an interface on the server for array updates (if you haven't already) that isolates the array object from arbitrary outside modification and supports a callback when updates are made.
Determine the latency allowed for multiple updates to occur without being pushed. The latency should allow reasonable time for previous network traffic to complete without overloading bandwidth unnecessarily.
When an array update occurs, start a timer if not already running for the latency period .
On timer expiry JSON.stringify the array (to take a snapshot), clear the timer running status, and message the client with the JSON text.
A slightly more complicated method to avoid delaying all push operations would be to immediately push single updates unless they occur within a guard period after the most recent push operation. A timer could then push modifications made during the guard period at the end of the guard period.
Broadcasting
The WebSockets API does not directly support broadcasting the same data to multiple clients. Refer to Server Broadcast in ws documentation for an example of sending data to all connected clients using a forEach loop.
Client side listener
In the client-side message listener
document.getElementById("keywordsList").innerHTML = e.data;
would be better as
document.getElementById("keywordsList").textContent = keywordList;
to both present keywords after decoding from JSON and prevent them ever being treated as HTML.
So I finally figured out what I wanted to accomplish. It sounds straight forward after I learned enough and thought about how to structure the back end of my project.
If you have two websockets running and one needs information from the other, you cannot run them side by side. You need to have one encapsulate the other and then call the websocket INSIDE of the other websocket. This can easily cause problems down the road for other projects since now you have one websocket that won't fire until the other is run but for my project it makes perfect sense since it is locally run and needs all the parts working 100 percent in order to be effective. It took me a long time to understand how to structure the code as such.
I am developing a JavaScript-based web SIP client communicating with Asterisk SIP server.
The SIP client is using JSSIP 3.4.2, I'm testing on Chrome version 80.
Both SIP client and SIP server are behind firewalls. I'm using STUN server stun.l.google.com:19302.
The call is established well, but there's a 40 sec delay between calling the "call" method and establishing a call (starting an RTP session).
Here's the code of SIP UA registration:
// SIP UA registration
var currentUserSipAccount = {
uri: '211',
pwd: 'secret'
};
var sipDomain = 'sip.my-domain.com';
var sipServerUrl = 'wss://' + sipDomain + ':8089/ws';
var socket = new JsSIP.WebSocketInterface(sipServerUrl);
var connectionParams = {};
connectionParams.sockets = [socket];
connectionParams.register = true;
connectionParams.uri = 'sip:' + currentUserSipAccount.uri + '#' + sipDomain;
connectionParams.password = currentUserSipAccount.pwd;
var bwPhone = new JsSIP.UA(connectionParams);
Here's the code of call initiation:
// SIP call
var callNumber = 'sip:233#' + sipDomain;
var callOptions = {
mediaConstraints: {
audio: true, // only audio calls
video: false
},
pcConfig: {
iceServers: [
{'urls': ['stun:stun.l.google.com:19302']}
]
}
};
bwPhone.call(callNumber, callOptions);
I have setup logging of each SIP event and found that the delay is related to the onicegatheringstatechange and onicecandidate events.
Here's the Wireshark log:
Each 10 sec, a STUN request is being sent, followed by an instant response. This happens 4 times.
Here is the browser console log I am getting:
The computer on which I'm doing a call has multiple network interfaces. I see icecandidate events containing two IP addresses, one of them (169.254.128.100) is related to Ethernet and not used, another one (192.168.1.33) is related to WiFi and is used for connecting to Internet.
I also see in the browser console log, that the STUN response is being received within several milliseconds after initiating the call. But after that, JSSIP waits for 40 seconds!
How to avoid this 40 sec delay?
Gathering candidates can be very long, and usually, when the delay is large, the last ice candidate will failed to be found.
To solve your delay, your can control the timeout and abort when you decide. This is an example for a timeout of 5 seconds with jssip:
var myCandidateTimeout = null;
_session.on('icecandidate', function(candidate, ready) {
console.log('getting a candidate' + candidate.candidate.candidate);
if (myCandidateTimeout!=null)
clearTimeout(myCandidateTimeout);
// 5 seconds timeout after the last icecandidate received!
myCandidateTimeout = setTimeout(candidate.ready, 5000);
}
Got some hints in the JSSIP group. To stop gathering for ICE candidates and continue SIP flow, I need to call event.ready() inside the icecandidate event handler.
This code resolved the issue (not sure what is srflx, maybe that's not necessary):
session.on("icecandidate", function (event) {
if (event.candidate.type === "srflx" &&
event.candidate.relatedAddress !== null &&
event.candidate.relatedPort !== null) {
event.ready();
}
});
If you do not plan to create a conference call, then you can do this. (works for me)
session.on("icecandidate", function (event) {
event.ready();
});
Is it possible to terminate a websocket connection from server without closing the entire server? If it is then, how can I achieve it?
Note: I'm using NodeJS as back-end and 'ws' websocket module.
So because of some sort of omission in the documentation regarding ws.close() and ws.terminate() I think the solutions in provided answers won't close the sockets gracefully in some cases, thus keeping them hanging in the Event Loop.
Compare the next two methods of ws package:
ws.close():
Initializes close handshake, sending close frame to the peer and awaiting to receive close frame from the peer, after that sending FIN packet in attempt to perform a clean socket close. When answer received, the socket is destroyed. However, there is a closeTimeout that will destroy socket only as a worst case scenario, and it potentially could keep socket for additional 30 seconds, preventing the graceful exit with your custom timeout:
// ws/lib/WebSocket.js:21
const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly.
ws.terminate():
Forcibly destroys the socket without closing frames or fin packets exchange, and does it instantly, without any timeout.
Hard shutdown
Considering all of the above, the "hard landing" scenario would be as follows:
wss.clients.forEach((socket) => {
// Soft close
socket.close();
process.nextTick(() => {
if ([socket.OPEN, socket.CLOSING].includes(socket.readyState)) {
// Socket still hangs, hard close
socket.terminate();
}
});
});
Soft shutdown
You can give your clients some time to respond, if you could allow yourself to wait for a while (but not 30 seconds):
// First sweep, soft close
wss.clients.forEach((socket) => {
socket.close();
});
setTimeout(() => {
// Second sweep, hard close
// for everyone who's left
wss.clients.forEach((socket) => {
if ([socket.OPEN, socket.CLOSING].includes(socket.readyState)) {
socket.terminate();
}
});
}, 10000);
Important: proper execution of close() method will emit 1000 close code for close event, while terminate() will signal abnormal close with 1006 (MDN WebSocket Close event).
If you want to kick ALL clients without closing the server you can do this:
for(const client of wss.clients)
{
client.close();
}
you can also filter wss.clients too if you want to look for one in particular. If you want to kick a client as part of the connection logic (i.e. it sends bad data etc), you can do this:
let WebSocketServer = require("ws").Server;
let wss = new WebSocketServer ({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.send('something');
ws.close(); // <- this closes the connection from the server
});
and with a basic client
"use strict";
const WebSocket = require("ws");
let ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
console.log("opened");
};
ws.onmessage = (m) => {
console.log(m.data);
};
ws.onclose = () => {
console.log("closed");
};
you'll get:
d:/example/node client
opened
something
closed
According to the ws documentation, you need to call websocket.close() to terminate a connection.
let server = new WebSocketServer(options);
server.on('connection', ws => {
ws.close(); //terminate this connection
});
Just use ws.close() in this way.
var socketServer = new WebSocketServer();
socketServer.on('connection', function (ws) {
ws.close(); //Close connecton for connected client ws
});
If you use var client = net.createConnection() to create the socket you can use client.destroy() to destroy it.
With ws it should be:
var server = new WebSocketServer();
server.on('connection', function (socket) {
// Do something and then
socket.close(); //quit this connection
});
The error I am getting in the browser console (only appears in chrome, no errors in firefox) is Error: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': The ICE candidate could not be added.
I followed a tutorial and was able to get p2p video chat to work using nodejs. Now I am using Flask and python on the server side and angularjs on client side.
Signaling process for two peers is being done with angular-socketio.
console.log("The user connected to the socket");
socket.emit('readyToJoinRoom', {"signal_room": SIGNAL_ROOM});
//Send a first signaling message to anyone listening
//This normally would be on a button click
socket.emit('signal',{"type":"user_joined", "message":"Are you ready for a call?", "room":SIGNAL_ROOM});
socket.forward('signaling_message', $scope);
$scope.$on('socket:signaling_message', function (ev, data) {
displaySignalMessage("Signal received: " + data.type);
// Setup the RTC Peer Connection object
if (!rtcPeerConn) {
startSignaling();
}
if(data.type != "user_joined") {
console.log(data.message);
var message = JSON.parse(data.message);
console.log(message);
if(message.sdp) {
console.log("inside 2nd if statement");
rtcPeerConn.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
// if we received an offer, we need to answer
if(rtcPeerConn.remoteDescription.type === 'offer') {
console.log("inside third if for remoteDescription."); // This never executes, error happens right before this line
rtcPeerConn.createAnswer(sendLocalDesc, logError);
}
}, logError);
}
else {
console.log("addedddddddd ice candidate.");
rtcPeerConn.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
});
Once two people join the room the startSignaling() method is called. It sets the local description and completes 3 ice candidates then I receive an SDP but this is never true if(rtcPeerConn.remoteDescription.type === 'offer') even though it prints the SDP in the console with a type equal to offer. I am not sure why it never goes inside this if statement. I am not sure why I am getting an error. If you have any questions just ask. Thanks for the help.
I think
rtcPeerConn.setRemoteDescription(new RTCSessionDescription(message.sdp),...
will not work because the constructor of RTCSessionDescription needs the information about the type and the sdp. Try:
var desc = new RTCSessionDescription();
desc.sdp = message.sdp;
desc.type = "offer";
rtcPeerConn.setRemoteDescription(desc,.....
I had some issues constructing the RTCSessionDescription from JSON as well.
Hope this helps...
I'm using the ws library for WebSockets in Node.js and
I'm trying this example from the library examples:
var sys = require("sys"),
ws = require("./ws");
ws.createServer(function (websocket) {
websocket.addListener("connect", function (resource) {
// emitted after handshake
sys.debug("connect: " + resource);
// server closes connection after 10s, will also get "close" event
setTimeout(websocket.end, 10 * 1000);
}).addListener("data", function (data) {
// handle incoming data
sys.debug(data);
// send data to client
websocket.write("Thanks!");
}).addListener("close", function () {
// emitted when server or client closes connection
sys.debug("close");
});
}).listen(8080);
All OK. It works, but running 3 clients, for instance, and sending "Hello!" from one will make the server only reply "Thanks!" to the client which sent the message, not to all.
How can I broadcast "Thanks!" to all connected clients when someone sends "Hello!"?
Thanks!
If you want to send out to all clients, you have to keep track of them. Here is a sample:
var sys = require("sys"),
ws = require("./ws");
// # Keep track of all our clients
var clients = [];
ws.createServer(function (websocket) {
websocket.addListener("connect", function (resource) {
// emitted after handshake
sys.debug("connect: " + resource);
// # Add to our list of clients
clients.push(websocket);
// server closes connection after 10s, will also get "close" event
// setTimeout(websocket.end, 10 * 1000);
}).addListener("data", function (data) {
// handle incoming data
sys.debug(data);
// send data to client
// # Write out to all our clients
for(var i = 0; i < clients.length; i++) {
clients[i].write("Thanks!");
}
}).addListener("close", function () {
// emitted when server or client closes connection
sys.debug("close");
for(var i = 0; i < clients.length; i++) {
// # Remove from our connections list so we don't send
// # to a dead socket
if(clients[i] == websocket) {
clients.splice(i);
break;
}
}
});
}).listen(8080);
I was able to get it to broadcast to all clients, but it's not heavily tested for all cases. The general concept should get you started though.
EDIT: By the way I'm not sure what the 10 second close is for so I've commented it out. It's rather useless if you're trying to broadcast to all clients since they'll just keep getting disconnected.
I would recommend you to use socket.io. It has example web-chat functionality out of the box and also provides abstraction layer from the socket technology on client (WebSockets are supported by Safari, Chrome, Opera and Firefox, but disabled in Firefox and Opera now due to security vulnerabilities in ws-protocol).