WebRTC signaling server using Express & 'websocket' packages - javascript

I am working with the following code from Rob Manson's Getting Started with WebRTC. It's implemented using Node.js. The code starts a real-time video call, and I have got it running as expected between 2 tabs in a web browser. I am simply trying to modify this so that it uses Express instead of the 'http' package.
The problem I am having is that there is no video getting displayed in my version. The 'Caller' works as expected, but then the 'Callee' stalls at the "One moment please...connecting your call..." message. There are no errors detected in the browser console or my terminal, and having spent a day trying to resolve the issue I still have little idea where I am going wrong.
Here is the original signalling server file:
// useful libs
var http = require("http");
var fs = require("fs");
var websocket = require("websocket").server;
// general variables
var port = 8000;
var webrtc_clients = [];
var webrtc_discussions = {};
// web server functions
var http_server = http.createServer(function(request, response) {
var matches = undefined;
if (matches = request.url.match("^/images/(.*)")) {
var path = process.cwd()+"/images/"+matches[1];
fs.readFile(path, function(error, data) {
if (error) {
log_error(error);
} else {
response.end(data);
}
});
} else {
response.end(page);
}
});
http_server.listen(port, function() {
log_comment("server listening (port "+port+")");
});
var page = undefined;
fs.readFile("basic_video_call.html", function(error, data) {
if (error) {
log_error(error);
} else {
page = data;
}
});
// web socket functions
var websocket_server = new websocket({
httpServer: http_server
});
websocket_server.on("request", function(request) {
log_comment("new request ("+request.origin+")");
var connection = request.accept(null, request.origin);
log_comment("new connection ("+connection.remoteAddress+")");
webrtc_clients.push(connection);
connection.id = webrtc_clients.length-1;
connection.on("message", function(message) {
if (message.type === "utf8") {
log_comment("got message "+message.utf8Data);
var signal = undefined;
try { signal = JSON.parse(message.utf8Data); } catch(e) { };
if (signal) {
if (signal.type === "join" && signal.token !== undefined) {
try {
if (webrtc_discussions[signal.token] === undefined) {
webrtc_discussions[signal.token] = {};
}
} catch(e) { };
try {
webrtc_discussions[signal.token][connection.id] = true;
} catch(e) { };
} else if (signal.token !== undefined) {
try {
Object.keys(webrtc_discussions[signal.token]).forEach(function(id) {
if (id != connection.id) {
webrtc_clients[id].send(message.utf8Data, log_error);
}
});
} catch(e) { };
} else {
log_comment("invalid signal: "+message.utf8Data);
}
} else {
log_comment("invalid signal: "+message.utf8Data);
}
}
});
connection.on("close", function(connection) {
log_comment("connection closed ("+connection.remoteAddress+")");
Object.keys(webrtc_discussions).forEach(function(token) {
Object.keys(webrtc_discussions[token]).forEach(function(id) {
if (id === connection.id) {
delete webrtc_discussions[token][id];
}
});
});
});
});
// utility functions
function log_error(error) {
if (error !== "Connection closed" && error !== undefined) {
log_comment("ERROR: "+error);
}
}
function log_comment(comment) {
console.log((new Date())+" "+comment);
}
And here is my modified file. Just the first bit has been changed:
// useful libs
var http = require("http");
var fs = require("fs");
var websocket = require("websocket").server;
var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');
// general variables
var hostname = 'localhost';
var port = 8000;
var webrtc_clients = [];
var webrtc_discussions = {};
var expressApp = express();
expressApp.use(morgan('dev'));
var myRouter = express.Router();
myRouter.use(bodyParser.json());
// web server functions
myRouter.route('/').all(function(request,response,next) {
var matches = undefined;
if (matches = request.url.match("^/images/(.*)")) {
var path = process.cwd() +"/images/"+matches[1];
debugger;
console.log("PATH: " + path);
fs.readFile(path, function(error, data) {
if (error) {
log_error(error);
} else {
response.end(data);
}
});
} else {
response.end(page);
}
});
//////////////////////
expressApp.use('/',myRouter);
expressApp.listen(port, hostname, function(){
console.log(`Server running at http://${hostname}:${port}/`);
});
/////////////////////// **I CHANGED NOTHING BELOW HERE** ////////////
var page = undefined;
fs.readFile("basic_video_call.html", function(error, data) {
if (error) {
log_error(error);
} else {
page = data;
}
});
// web socket functions
var websocket_server = new websocket({
httpServer: expressApp
});
websocket_server.on("request", function(request) {
log_comment("new request ("+request.origin+")");
var connection = request.accept(null, request.origin);
log_comment("new connection ("+connection.remoteAddress+")");
webrtc_clients.push(connection);
connection.id = webrtc_clients.length-1;
connection.on("message", function(message) {
if (message.type === "utf8") {
log_comment("got message "+message.utf8Data);
var signal = undefined;
try { signal = JSON.parse(message.utf8Data); } catch(e) { };
if (signal) {
if (signal.type === "join" && signal.token !== undefined) {
try {
if (webrtc_discussions[signal.token] === undefined) {
webrtc_discussions[signal.token] = {};
}
} catch(e) { };
try {
webrtc_discussions[signal.token][connection.id] = true;
} catch(e) { };
} else if (signal.token !== undefined) {
try {
Object.keys(webrtc_discussions[signal.token]).forEach(function(id) {
if (id != connection.id) {
webrtc_clients[id].send(message.utf8Data, log_error);
}
});
} catch(e) { };
} else {
log_comment("invalid signal: "+message.utf8Data);
}
} else {
log_comment("invalid signal: "+message.utf8Data);
}
}
});
connection.on("close", function(connection) {
log_comment("connection closed ("+connection.remoteAddress+")");
Object.keys(webrtc_discussions).forEach(function(token) {
Object.keys(webrtc_discussions[token]).forEach(function(id) {
if (id === connection.id) {
delete webrtc_discussions[token][id];
}
});
});
});
});
// utility functions
function log_error(error) {
if (error !== "Connection closed" && error !== undefined) {
log_comment("ERROR: "+error);
}
}
function log_comment(comment) {
console.log((new Date())+" "+comment);
}
Also, here is the code that handles the WebRTC call, which I haven't changed:
<!DOCTYPE html>
<html>
<head>
<script>
/*
webrtc_polyfill.js by Rob Manson
NOTE: Based on adapter.js by Adam Barth
The MIT License
Copyright (c) 2010-2013 Rob Manson, http://buildAR.com. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
var webrtc_capable = true;
var rtc_peer_connection = null;
var rtc_session_description = null;
var get_user_media = null;
var connect_stream_to_src = null;
var stun_server = "stun.l.google.com:19302";
if (navigator.getUserMedia) { // WebRTC 1.0 standard compliant browser
rtc_peer_connection = RTCPeerConnection;
rtc_session_description = RTCSessionDescription;
get_user_media = navigator.getUserMedia.bind(navigator);
connect_stream_to_src = function(media_stream, media_element) {
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21606
media_element.srcObject = media_stream;
media_element.play();
};
} else if (navigator.mediaDevices.getUserMedia) { // early firefox webrtc implementation
rtc_peer_connection = mozRTCPeerConnection;
rtc_session_description = mozRTCSessionDescription;
get_user_media = navigator.mozGetUserMedia.bind(navigator);
connect_stream_to_src = function(media_stream, media_element) {
media_element.mozSrcObject = media_stream;
media_element.play();
};
stun_server = "74.125.31.127:19302";
} else if (navigator.webkitGetUserMedia) { // early webkit webrtc implementation
rtc_peer_connection = webkitRTCPeerConnection;
rtc_session_description = RTCSessionDescription;
get_user_media = navigator.webkitGetUserMedia.bind(navigator);
connect_stream_to_src = function(media_stream, media_element) {
media_element.src = webkitURL.createObjectURL(media_stream);
};
} else {
alert("This browser does not support WebRTC - visit WebRTC.org for more info");
webrtc_capable = false;
}
</script>
<script>
/*
basic_video_call.js by Rob Manson
The MIT License
Copyright (c) 2010-2013 Rob Manson, http://buildAR.com. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
var call_token; // unique token for this call
var signaling_server; // signaling server for this call
var peer_connection; // peer connection object
function start() {
// create the WebRTC peer connection object
peer_connection = new rtc_peer_connection({ // RTCPeerConnection configuration
"iceServers": [ // information about ice servers
{ "url": "stun:"+stun_server }, // stun server info
]
});
// generic handler that sends any ice candidates to the other peer
peer_connection.onicecandidate = function (ice_event) {
if (ice_event.candidate) {
signaling_server.send(
JSON.stringify({
token:call_token,
type: "new_ice_candidate",
candidate: ice_event.candidate ,
})
);
}
};
// display remote video streams when they arrive using local <video> MediaElement
peer_connection.onaddstream = function (event) {
connect_stream_to_src(event.stream, document.getElementById("remote_video"));
// hide placeholder and show remote video
document.getElementById("loading_state").style.display = "none";
document.getElementById("open_call_state").style.display = "block";
};
// setup stream from the local camera
setup_video();
// setup generic connection to the signaling server using the WebSocket API
signaling_server = new WebSocket("ws://localhost:8000");
if (document.location.hash === "" || document.location.hash === undefined) { // you are the Caller
// create the unique token for this call
var token = Math.round(Math.random()*100);
call_token = "#"+token;
// set location.hash to the unique token for this call
document.location.hash = token;
signaling_server.onopen = function() {
// setup caller signal handler
signaling_server.onmessage = caller_signal_handler;
// tell the signaling server you have joined the call
signaling_server.send(
JSON.stringify({
token:call_token,
type:"join",
})
);
}
document.title = "You are the Caller";
document.getElementById("loading_state").innerHTML = "Ready for a call...ask your friend to visit:<br/><br/>"+document.location;
} else { // you have a hash fragment so you must be the Callee
// get the unique token for this call from location.hash
call_token = document.location.hash;
signaling_server.onopen = function() {
// setup caller signal handler
signaling_server.onmessage = callee_signal_handler;
// tell the signaling server you have joined the call
signaling_server.send(
JSON.stringify({
token:call_token,
type:"join",
})
);
// let the caller know you have arrived so they can start the call
signaling_server.send(
JSON.stringify({
token:call_token,
type:"callee_arrived",
})
);
}
document.title = "You are the Callee";
document.getElementById("loading_state").innerHTML = "One moment please...connecting your call...";
}
}
/* functions used above are defined below */
// handler to process new descriptions
function new_description_created(description) {
peer_connection.setLocalDescription(
description,
function () {
signaling_server.send(
JSON.stringify({
token:call_token,
type:"new_description",
sdp:description
})
);
},
log_error
);
}
// handle signals as a caller
function caller_signal_handler(event) {
var signal = JSON.parse(event.data);
if (signal.type === "callee_arrived") {
peer_connection.createOffer(
new_description_created,
log_error
);
} else if (signal.type === "new_ice_candidate") {
peer_connection.addIceCandidate(
new RTCIceCandidate(signal.candidate)
);
} else if (signal.type === "new_description") {
peer_connection.setRemoteDescription(
new rtc_session_description(signal.sdp),
function () {
if (peer_connection.remoteDescription.type == "answer") {
// extend with your own custom answer handling here
}
},
log_error
);
} else {
// extend with your own signal types here
}
}
// handle signals as a callee
function callee_signal_handler(event) {
var signal = JSON.parse(event.data);
if (signal.type === "new_ice_candidate") {
peer_connection.addIceCandidate(
new RTCIceCandidate(signal.candidate)
);
} else if (signal.type === "new_description") {
peer_connection.setRemoteDescription(
new rtc_session_description(signal.sdp),
function () {
if (peer_connection.remoteDescription.type == "offer") {
peer_connection.createAnswer(new_description_created, log_error);
}
},
log_error
);
} else {
// extend with your own signal types here
}
}
// setup stream from the local camera
function setup_video() {
get_user_media(
{
"audio": true, // request access to local microphone
"video": true // request access to local camera
//"video": {mandatory: {minHeight:8, maxHeight:8, minWidth:8, maxWidth:8}}
},
function (local_stream) { // success callback
// display preview from the local camera & microphone using local <video> MediaElement
connect_stream_to_src(local_stream, document.getElementById("local_video"));
// add local camera stream to peer_connection ready to be sent to the remote peer
peer_connection.addStream(local_stream);
},
log_error
);
}
// generic error handler
function log_error(error) {
console.log(error);
}
</script>
<style>
html, body {
padding: 0px;
margin: 0px;
font-family: "Arial","Helvetica",sans-serif;
}
#loading_state {
position: absolute;
top: 45%;
left: 0px;
width: 100%;
font-size: 20px;
text-align: center;
}
#open_call_state {
display: none;
}
#local_video {
position: absolute;
top: 10px;
left: 10px;
width: 160px;
height: 120px;
background: #333333;
}
#remote_video {
position: absolute;
top: 0px;
left: 0px;
width: 1024px;
height: 768px;
background: #999999;
}
</style>
</head>
<body onload="start()">
<div id="loading_state">
loading...
</div>
<div id="open_call_state">
<video id="remote_video"></video>
<video id="local_video"></video>
</div>
</body>
</html>
I am also open to solutions that don't use Express but that still support authentication in WebRTC. Many thanks for your help.

Well, I discovered the solution. It simply involved replacing this line:
expressApp.listen(port, hostname, function() {
console.log(`Server running at http://${hostname}:${port}/`);
}
With this one:
var webServer = http.createServer(expressApp).listen(8000);
The reason this worked is somewhat explained in this thread. Apologies for wasting anyone's time with a trivial problem.

Related

How to use html5 websockets within sharedworkers

After checking similar questions on stackoverflow I did not find anything much helpful for what I want to do in my project. Reading and researching I successfully made the application work having multiple connections to my Ratchet PHP websocket server, but I noticed every time the user reloaded a page or opened a link in a new tab, the client websocket got disconnected and then reconnected again.
So, I wonder how to get only one persistent connection to a WebSocket Server, for multiple users, in a web application using a Sharedworker.
What I have in the client side is this:
<script>
$(document).ready(function($) {
let socket = new WebSocket("ws://realtime:8090");
socket.onopen = function(e) {
console.log("Browser client connected to websocket server");
socket.send("Greetings from the browser!");
};
socket.onmessage = function(event) {
console.log('Data received from server: ' + event.data);
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
}
else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
console.log('Connection closed unexpectedly.');
}
};
socket.onerror = function(error) {
alert(error.message);
};
});
</script>
Ok after reading, researching and trying different things and code samples, I came to this solution:
The client side (browser) should have the connection to a Sharedworker.
The sharedworker is a separated javascript file containing the core of the sharedworker and whatever other JS code that needs to be executed within it.
I first tested the sharedworker to work fine with the browser tabs, counting the number of opened tabs per user and sharing messages to one user, and then to a group of users.
Once the communication between the browser and the Sharedworker passed those tests I added the websocket code to the body of the Sharedworker JS file.
In the end, the client side (browser) looks like this:
<script>
$(document).ready(function($) {
var currentUser = "{{ Auth::user()->name }}";
let worker = new SharedWorker('worker.js');
worker.port.start();
worker.port.postMessage({
action: 'connect',
username: currentUser
});
worker.port.onmessage = function(message) {
console.log(message.data);
};
});
</script>
The Sharedworker looks like this:
// All this code is executed only once, until the onconnect() function.
//---------------------------------------------------------------------
// The array AllPorts contains objects with the format {user:<string>, port:<MessagePort>}
let AllPorts = [];
var socket = new WebSocket("ws://ssa:8090");
// Called when the WebSocket Server accepts the connection.
socket.onopen = function(e) {
//
};
// Event handler fired when the WebSocket Server sends a message to this client.
socket.onmessage = function(e) {
var message = JSON.parse(e.data);
// This loop sends a message to each tab opened by the given user.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == message.to) {
AllPorts[i].port.postMessage(message.msg);
}
}
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log('Connection closed normally');
}
else {
console.log('Connection closed unexpectedly.');
}
};
socket.onerror = function(error) {
console.log(error.message);
};
// This event handler is fired every time a new tab is opened on the web browser.
onconnect = function(ev) {
let port = ev.ports[0];
port.onmessage = function(e) {
console.log(e.data.action);
let currentUser = e.data.username;
let userIsConnected = false;
switch (e.data.action) {
case "connect":
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
// Add new connected tab to AllPorts array.
AllPorts.push({user: currentUser, port: port});
if (!userIsConnected) {
// New users are added to the list of the WebSocket Server.
setTimeout(() => {
socket.send(JSON.stringify({action: 'connect', username: currentUser}));
}, 600);
}
break;
case "close":
console.log(AllPorts);
var index;
// This is also executed when the user reloads the Tab.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].port == port) {
index = i;
currentUser = AllPorts[i].user;
}
}
AllPorts.splice(index, 1);
userIsConnected = false;
// Check for any connected tab.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
if (!userIsConnected) {
// User doen't have more tabs opened. Remove user from WebSocket Server.
socket.send(JSON.stringify({action: 'disconnect', username: currentUser}));
}
break;
case "notify":
// Check if given user is connected.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
if (userIsConnected) {
socket.send(JSON.stringify({action: 'notify', to: currentUser, message: e.data.message}));
}
} // switch
} // port.onmessage
} // onconnect

peer.js webrtc >> changing stream in runtime

I am developing a cross-plattform application with peer.js and webrtc.
I am using cordova, crosswalk.
Additionaly I am using the webrtc adapter (https://github.com/webrtc/adapter)
My code is based on the webrtc-crosswalk sample. (https://github.com/crosswalk-project/crosswalk-samples)
I want to change the videosource of the stream without creating a new call.
My approche is to remove the tracks of the stream and add the new tracks of the other camera.
The result is that the local video shows the right content, but the callee's remote video freezes.
Probably I am doing a very basic mistake, but i can't find a solution.
I am looking forward to your answers and solutions.
My main codefile is attached.
//Notwendig, um die Dialogfunktion zu aktivieren
document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
console.log(navigator.notification);
// Now safe to use device APIs
}
document.addEventListener('DOMContentLoaded', function () {
// PeerJS server location
var SERVER_IP = '172.20.37.147';
var SERVER_PORT = 9000;
// DOM elements manipulated as user interacts with the app
var messageBox = document.querySelector('#messages');
var callerIdEntry = document.querySelector('#caller-id');
var connectBtn = document.querySelector('#connect');
var recipientIdEntry = document.querySelector('#recipient-id');
var dialBtn = document.querySelector('#dial');
var remoteVideo = document.querySelector('#remote-video');
var localVideo = document.querySelector('#local-video');
var cameraTurn = document.querySelector('#camera_turn');
var stop = document.querySelector('#stop');
// the default facing direction
var dir = "environment";
// the ID set for this client
var callerId = null;
// PeerJS object, instantiated when this client connects with its
// caller ID
var peer = null;
// the local video stream captured with getUserMedia()
var localStream = null;
// DOM utilities
var makePara = function (text) {
var p = document.createElement('p');
p.innerText = text;
return p;
};
var addMessage = function (para) {
if (messageBox.firstChild) {
messageBox.insertBefore(para, messageBox.firstChild);
}
else {
messageBox.appendChild(para);
}
};
var logError = function (text) {
var p = makePara('ERROR: ' + text);
p.style.color = 'red';
addMessage(p);
};
var logMessage = function (text) {
addMessage(makePara(text));
};
// get the local video and audio stream and show preview in the
// "LOCAL" video element
// successCb: has the signature successCb(stream); receives
// the local video stream as an argument
var getLocalStream = function (successCb, ask = true) {
if (localStream && successCb) {
successCb(localStream);
}
else {
navigator.mediaDevices.getUserMedia({ audio: true, video: { facingMode: dir } })
.then(function (stream) {
if (localStream == null) {
/* use the stream */
localStream = stream;
}
else {
stream.getTracks().forEach(function (track) {
localStream.addTrack(track);
});
}
localVideo.src = window.URL.createObjectURL(localStream);
if (successCb) {
successCb(stream);
}
})
.catch(function (err) {
/* handle the error */
logError('failed to access local camera');
logError(err.message);
});
}
};
// set the "REMOTE" video element source
var showRemoteStream = function (stream) {
remoteVideo.src = window.URL.createObjectURL(stream);
};
// set caller ID and connect to the PeerJS server
var connect = function () {
callerId = callerIdEntry.value;
if (!callerId) {
logError('please set caller ID first');
return;
}
try {
// create connection to the ID server
peer = new Peer(callerId, { host: SERVER_IP, port: SERVER_PORT });
// hack to get around the fact that if a server connection cannot
// be established, the peer and its socket property both still have
// open === true; instead, listen to the wrapped WebSocket
// and show an error if its readyState becomes CLOSED
peer.socket._socket.onclose = function () {
logError('no connection to server');
peer = null;
};
// get local stream ready for incoming calls once the wrapped
// WebSocket is open
peer.socket._socket.onopen = function () {
getLocalStream();
};
// handle events representing incoming calls
peer.on('call', answer);
}
catch (e) {
peer = null;
logError('error while connecting to server');
}
};
// make an outgoing call
var dial = function () {
if (!peer) {
logError('please connect first');
return;
}
if (!localStream) {
logError('could not start call as there is no local camera');
return
}
var recipientId = recipientIdEntry.value;
if (!recipientId) {
logError('could not start call as no recipient ID is set');
return;
}
getLocalStream(function (stream) {
logMessage('outgoing call initiated');
var call = peer.call(recipientId, stream);
call.on('stream', showRemoteStream);
call.on('error', function (e) {
logError('error with call');
logError(e.message);
});
});
};
// answer an incoming call
var answer = function (call) {
if (!peer) {
logError('cannot answer a call without a connection');
return;
}
if (!localStream) {
logError('could not answer call as there is no localStream ready');
return;
}
//Asks user to answer the call
navigator.notification.confirm(
"Receive a call?",
function (buttonIndex) {
if (buttonIndex === 1) {
//user clicked "yes"
logMessage('incoming call answered');
call.on('stream', showRemoteStream);
call.answer(localStream);
}
else {
//user clicked "no"
logMessage('incoming call denied');
}
}
,
'Incoming Call',
['Yes', 'No']
);
};
function turnDirection() {
if (dir === "user")
return "environment";
else
return "user";
}
var turnCamera = function (call) {
dir = turnDirection();
localStream.getTracks().forEach(function (track) {
track.stop();
localStream.removeTrack(track);
});
getLocalStream(false);
};
var stopCall = function (call) { };
// wire up button events
connectBtn.addEventListener('click', connect);
dialBtn.addEventListener('click', dial);
cameraTurn.addEventListener('click', turnCamera);
stop.addEventListener('click', stopCall);
});
If you remove and then add a new track to a PeerConnection you need to renegotiate the offer-answer to get it working. I will recommend you to use the replaceTrack API to avoid the re-negotiation problem while changing the camera input.

Websockets: send messages and notifications to all clients except sender

I am developing chat based on websockets and webrtc. I would like to send messages to all connected users except sender but I cannot find suitable solution. To be more specific, I would like to send notifications to other connected users that new user has joined to the chat.
I am trying to give a unique ID to every connected user, but the first assigned ID is re-writed by every new user and I cannot diferentiate users.
Server:
// list of users
var CLIENTS=[];
var id;
// web server is using 8081 port
var webSocketServer = new WebSocketServer.Server({ port: 8081 });
// check if connection is established
webSocketServer.on('connection', function(ws) {
id = Math.random();
CLIENTS[id] = ws;
CLIENTS.push(ws);
ws.on('message', function(message) {
console.log('received: %s', message);
var received = JSON.parse(message);
if(received.type == "login"){
ws.send(message); // send message to itself
/* *********************************************************** */
/* *** Here I trying to check if message comes from sender *** */
sendNotes(JSON.stringify({
user: received.name,
type: "notes"
}), ws, id);
/* *********************************************************** */
}else if(received.type == "message"){
sendAll(message); // broadcast messages to everyone including sender
}
});
ws.on('close', function() {
console.log('user ' + CLIENTS[ws] + ' left chat');
delete CLIENTS[ws];
});
});
function sendNotes(message, ws, id) {
console.log('sendNotes : ', id);
if (CLIENTS[id] !== ws) {
console.log('IF : ', message);
for (var i = 0; i < CLIENTS.length; i++) {
CLIENTS[i].send(message);
}
}else{
console.log('ELSE : ', message);
}
}
function sendAll(message) {
for (var i=0; i < CLIENTS.length; i++) {
CLIENTS[i].send(message); // broadcast messages to everyone including sender
}
}
Client:
loginButton.addEventListener("click", function(){
name = usernameInput.value;
if(name.length > 0){
socket.send(JSON.stringify({
type: "login",
name: name
}));
}
});
function sendData() {
var data = dataChannelSend.value;
var userName = document.getElementById('greetingUser').innerHTML;
socket.send(JSON.stringify({
username : userName, // fetch user name from browser, after login
type : "message",
message : data
}));
}
socket.onmessage = function(message) {
var envelope = JSON.parse(message.data);
switch(envelope.type) {
case "login":
onLogin(envelope);
break;
case "message":
showMessage(envelope);
break;
}
};
I would highly appreciate If you could give me any hint. Thanks
Here is a very simple way of sending to everyone connected except the sender.
Create a broadcast function on your webSocketServer instance that will
take two params.
...
var webSocketServer = new WebSocketServer.Server({ port: 8081 });
...
/*
* method: broadcast
* #data: the data you wanna send
* #sender: which client/ws/socket is sending
*/
webSocketServer.broadcast = function(data, sender) {
webSocketServer.clients.forEach(function(client) {
if (client !== sender) {
client.send(data)
}
})
}
...
// On your message callback.
ws.on('message', function(message) {
...
// Note that we're passing the (ws) here
webSocketServer.broadcast(message, ws);
})
That's it, the broadcast method will send to each connected client
except the one who is sending.
Ok, so we are now storing the CLIENTS in a way that allows us to uniquely identify each client that is connecting, and store arbitrary information about them for later retrieval.
The code below will send the "notes" message to all clients, and THEN add the newly connecting client to the "all clients" list.
SERVER.JS:
var http = require('http'),
Static = require('node-static'),
WebSocketServer = new require('ws'),
// list of users
/*
We are now storing client data like this:
CLIENTS = {
uniqueRandomClientID: {
socket: {}, // The socket that this client is connected on
clientDetails: { // Any details you might wish to store about this client
username: "",
etc: "etc"
}
}
};
So now to get at the socket for a client, it'll be: CLIENTS[uniqueRandomClientID].socket.
Or to show a client's username, it'll be: CLIENTS[uniqueRandomClientID].clientDetails.username.
You might want to write a 'getClientByUsername' function that iterates the CLIENTS array and returns the client with that username.
*/
CLIENTS = {},
// web server is using 8081 port
webSocketServer = new WebSocketServer.Server({ port: 8081 });
// check if connection is established
webSocketServer.on('connection', function(ws) {
console.log('connection is established');
// Now using a randomly generated ID to reference a client. Probably should be better than Math.random :D
var wsID = Math.floor(Math.random() * 1000);
ws.on('message', function(message) {
console.log('received: %s', message);
var received = JSON.parse(message);
if(received.type == "login"){
// If a client with this login name doesnt exist already, its a new client
if(!CLIENTS[wsID]) {
doBroadcast(
{
"newuser": received.name,
type: "notes"
}
);
// Now add this new client to the list
CLIENTS[wsID] = {
socket: ws,
clientDetails: {
username: received.name
}
};
}
} else if(received.type == "message") {
doBroadcast(message); // broadcast messages to everyone including sender
}
});
ws.on('close', function(_event) {
if(CLIENTS[wsID]) {
console.log('user ' + CLIENTS[wsID].clientDetails.username + ' left chat');
delete CLIENTS[wsID];
}
});
/*
* Added this to 'catch' errors rather than just red dump to console. I've never actually done anything with this myself (I *like* red text in my console), but I know this handler should be here :P
*/
ws.on('error', function(_error) {
console.log("error!");
console.log(_error);
});
/*
* Send an object to a client
*
* #param WebSocketClient _to - The client you want to send to (generally an index in the CLIENTS array, i.e CLIENTS["bobsusername123"]
* #param Object _message - A stringifyable JSON object. Complex ones can screw things up, but your basic key/value pairs are usually fine to send.
*/
function doSend(_to, _message) {
_to.send(JSON.stringify(_message));
};
// Added broadcast function to replace sendAll
// Notice how it JSON stringifies the data before sending
/*
* Broadcast a message to all clients
*
* #param Object _message - A stringifyable JSON object. Complex ones can screw things up, but your basic key/value pairs are usually fine to send.
*/
function doBroadcast(_message) {
for(var client in CLIENTS) {
if(!CLIENTS.hasOwnProperty(client)) continue;
doSend(CLIENTS[client].socket, _message);
}
};
});
var fileServer = new Static.Server('.');
http.createServer(function (req, res) {
fileServer.server(req, res);
}).listen(8080, function(){
console.log("Server is listening 8080 port.");
});
console.log("Server is running on 8080 and 8081 ports");
MY CLIENT.JS (for your reference):
var loginButton = document.getElementById("loginbutton"),
usernameInput = document.getElementById("usernameInput");
var SocketClient = function(_uri, _callbacks) {
this.uri = _uri;
this.callbacks = _callbacks;
};
SocketClient.prototype = {
send: function(_message) {
this.socket.send(_message);
},
connect: function() {
try {
this.socket = new WebSocket("ws://" + this.uri);
} catch(e) { return false; }
for(var callback in this.callbacks) {
if(!this.callbacks.hasOwnProperty(callback)) continue;
this.socket["on" + callback] = this.callbacks[callback];
}
return true;
}
};
var socketClient = new SocketClient(
"127.0.0.1:8081",
{
open: function() {
console.log("connected.");
},
message: function(_message) {
console.log("received data:");
console.log(_message);
},
close: function() {
console.log("closed.");
},
error: function(_error) {
console.log("error: ");
console.log(_error);
}
}
);
socketClient.connect();
loginButton.addEventListener("click", function(){
name = usernameInput.value;
if(name.length > 0){
socketClient.send(JSON.stringify({
type: "login",
name: name
}));
}
});
AND THE CLIENT.HTML TO GO WITH IT:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<input type="text" id="usernameInput"/>
<button type="button" id="loginbutton">Login</button>
<script src="client.js"></script>
</body>
</html>
Ive tested this with NWJS v0.12.3 running the server and Firefox on the client.
This should work
const WebSocket = require('ws');
// Websocket variables
const wss = new WebSocket.Server({
port: 3000
});
console.log('Websocket active on port 3000...');
// New WebSocket Connection
wss.on('connection', function connection(ws) {
console.log('new connection')
// On Message Received
ws.on('message', function incoming(message) {
console.log(message)
// Send To Everyone Except Sender
wss.clients.forEach(function(client) {
if (client !== ws) client.send(message);
});
});
});

Communicate with the serial port from client web browser.

In my web application(sencha extjs 5) I have a user requirement to read/write data to the client PC serial port.
I am aware of the client browser can not access local machine hardware without installing some binaries on the local machine(Native app, Windows Service, etc..).
I have seen the same question is discussed few years back in stackoverflow forums. But I need to know what is the best way of doing this today with the available technologies?
Using Web Serial API. I am using this to ONLY read the data from my Weight Scale with RS232 Serial Interface
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Serial</title>
</head>
<body>
<div class="serial-scale-div">
<button class="btn" id="connect-to-serial">Connect with Serial Device</button>
</div>
<button id="get-serial-messages">Get serial messages</button>
<div id="serial-messages-container">
<div class="message"></div>
</div>
<script>
"use strict";
class SerialScaleController {
constructor() {
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
}
async init() {
if ('serial' in navigator) {
try {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });
this.reader = port.readable.getReader();
let signals = await port.getSignals();
console.log(signals);
}
catch (err) {
console.error('There was an error opening the serial port:', err);
}
}
else {
console.error('Web serial doesn\'t seem to be enabled in your browser. Try enabling it by visiting:');
console.error('chrome://flags/#enable-experimental-web-platform-features');
console.error('opera://flags/#enable-experimental-web-platform-features');
console.error('edge://flags/#enable-experimental-web-platform-features');
}
}
async read() {
try {
const readerData = await this.reader.read();
console.log(readerData)
return this.decoder.decode(readerData.value);
}
catch (err) {
const errorMessage = `error reading data: ${err}`;
console.error(errorMessage);
return errorMessage;
}
}
}
const serialScaleController = new SerialScaleController();
const connect = document.getElementById('connect-to-serial');
const getSerialMessages = document.getElementById('get-serial-messages');
connect.addEventListener('pointerdown', () => {
serialScaleController.init();
});
getSerialMessages.addEventListener('pointerdown', async () => {
getSerialMessage();
});
async function getSerialMessage() {
document.querySelector("#serial-messages-container .message").innerText += await serialScaleController.read()
}
</script>
</body>
</html>
Checkout this demo and this code for a more descriptive example.
You might need to turn on the Serial API feature on your browser.
Following is the quote from References
As you can imagine, this is API is only supported by modern Chromium
based desktop browsers right now (April 2020) but hopefully support
will improve in the near future. At this moment you need to enable
your browser's Experimental Web Platform Features, just copy and paste
the right URL:
chrome://flags/#enable-experimental-web-platform-features
opera://flags/#enable-experimental-web-platform-features
edge://flags/#enable-experimental-web-platform-features
References:
https://dev.to/unjavascripter/the-amazing-powers-of-the-web-web-serial-api-3ilc
https://github.com/UnJavaScripter/web-serial-example
Well, One way to do this is develop a chrome app. You can use chrome.serial API.
https://developer.chrome.com/apps/serial
Sample Code,
In your manifest.json,
{
"name": "Serial Sample",
"description": "Read/Write from/to serial port.",
"version": "1.0",
"manifest_version": 2,
"permissions": ["serial"],
"app": {
"background": {
"scripts": ["background.js"]
}
}
}
In your background.js,
const DEVICE_PATH = 'COM1';
const serial = chrome.serial;
var dataRecieved="";
/* Interprets an ArrayBuffer as UTF-8 encoded string data. */
var ab2str = function(buf) {
var bufView = new Uint8Array(buf);
var encodedString = String.fromCharCode.apply(null, bufView);
return decodeURIComponent(escape(encodedString));
};
/* Converts a string to UTF-8 encoding in a Uint8Array; returns the array buffer. */
var str2ab = function(str) {
var encodedString = unescape(encodeURIComponent(str));
var bytes = new Uint8Array(encodedString.length);
for (var i = 0; i < encodedString.length; ++i) {
bytes[i] = encodedString.charCodeAt(i);
}
return bytes.buffer;
};
var SerialConnection = function() {
this.connectionId = -1;
this.lineBuffer = "";
this.boundOnReceive = this.onReceive.bind(this);
this.boundOnReceiveError = this.onReceiveError.bind(this);
this.onConnect = new chrome.Event();
this.onReadLine = new chrome.Event();
this.onError = new chrome.Event();
};
SerialConnection.prototype.onConnectComplete = function(connectionInfo) {
if (!connectionInfo) {
log("Connection failed.");
return;
}
this.connectionId = connectionInfo.connectionId;
chrome.serial.onReceive.addListener(this.boundOnReceive);
chrome.serial.onReceiveError.addListener(this.boundOnReceiveError);
this.onConnect.dispatch();
};
SerialConnection.prototype.onReceive = function(receiveInfo) {
if (receiveInfo.connectionId !== this.connectionId) {
return;
}
this.lineBuffer += ab2str(receiveInfo.data);
var index;
while ((index = this.lineBuffer.indexOf('\n')) >= 0) {
var line = this.lineBuffer.substr(0, index + 1);
this.onReadLine.dispatch(line);
this.lineBuffer = this.lineBuffer.substr(index + 1);
}
};
SerialConnection.prototype.onReceiveError = function(errorInfo) {
if (errorInfo.connectionId === this.connectionId) {
this.onError.dispatch(errorInfo.error);
}
};
SerialConnection.prototype.connect = function(path) {
serial.connect(path, this.onConnectComplete.bind(this))
};
SerialConnection.prototype.send = function(msg) {
if (this.connectionId < 0) {
throw 'Invalid connection';
}
serial.send(this.connectionId, str2ab(msg), function() {});
};
SerialConnection.prototype.disconnect = function() {
if (this.connectionId < 0) {
throw 'Invalid connection';
}
serial.disconnect(this.connectionId, function() {});
};
var connection = new SerialConnection();
connection.onConnect.addListener(function() {
//console.log('connected to: ' + DEVICE_PATH);
});
connection.onReadLine.addListener(function (line) {
//Serial port data recieve event.
dataRecieved = dataRecieved +line;
});
connection.connect(DEVICE_PATH);
Once you create the chrome app to communicate with the serial port the next thing is to allow your external web page to communicate with the chrome app using JavaScript.
For this on your manifest.json file add,
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
This will allow external webpage on your example.com domain communicate with your chrome app.
In your webpage,
// The ID of the extension we want to talk to.
var editorExtensionId = "nboladondmajlaalmcdupihoilpcketyl";
// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId,
{ data: "data to pass to the chrome app" },
function (response)
{
alert(response);
});
In your chrome app,
chrome.runtime.onMessageExternal.addListener(
function (request, sender, sendResponse) {
sendResponse("Send serial port data to the web page");
});
https://developer.chrome.com/apps/messaging
I set up a website and a simple example for running a serial terminal in your browser. You should host it on a https server.
The serial terminal features are now available in chrome 88.
Live demo https://www.SerialTerminal.com
Full source.
https://github.com/mmiscool/serialTerminal.com/blob/main/index.html

XMPP web client (using strophe.js) taking time to connect with ejabberd server

I have implemented an XMPP client for JavaScript (using strophe.js), for which I am referring to the book Professional XMPP with JavaScript and jQuery by Jack Moffitt.
The book has a great explanation on working of XMPP JavaScript client using "strophe.js".
With my code, the XMPP web client is taking 6 seconds to connect with the XMPP server (ejabberd using BOSH), which is not desirable for my application.
Can some one explain why this is happening?
My XMPP client code is as below:
var Arthur = {
connection: null,
jid:"20147001#localhost",
password: "XXXX2014",
handle_message: function (message) {
if ($(message).attr('from')) {
if($(message).attr('from').match(/^abcd007#localhost/)){
console.log("inside message received");
var body = $(message).find('body').contents();
var span = $("<span></span>");
body.each(function () {
if (document.importNode) {
$(document.importNode(this, true)).appendTo(span);
console.log(span);
console.log(span.text());
}
});
notiReceived(span.text());
console.log("afte notiReceived executed");
}
if($(message).attr('from').match(/^xyz2014#localhost/)){
console.log("inside message received");
var body1 = $(message).find('body').contents();
var span1 = $("<span></span>");
body1.each(function () {
if (document.importNode) {
$(document.importNode(this, true)).appendTo(span1);
//console.log(span.find('text'));
console.log(span1);
console.log(span1.text());
}
});
notiReceived(span1.text());
console.log("afte notiReceived executed");
}
}
return true;
}
};
function sendPushNotification(to,operation,request_id,message){
if(to=="citizen"){
var myObject = new Object();
myObject.FROM="Executive";
myObject.FUNCTION=operation;
myObject.MESSAGE = message;
myObject.REQUESTID= request_id;
console.log("inside citizen::"+myObject);
var myString = JSON.stringify(myObject);
$(this).val('');
var msg = $msg({to: 'abcd007#localhost', type: 'chat'})
.c('body').t(myString);
console.log("inside keypress data is::"+msg);
Arthur.connection.send(msg);
}
if(to=="technician"){
var myObject1 = new Object();
myObject1.FROM="Executive";
myObject1.FUNCTION=operation;
myObject1.MESSAGE = "Check Request Status";
myObject1.REQUESTID= request_id;
console.log("inside technician:"+myObject1);
var myString1 = JSON.stringify(myObject1);
$(this).val('');
var msg1 = $msg({to: 'xyz2014#localhost', type: 'chat'})
.c('body').t(myString1);
console.log("after msg send to technician"+msg1);
Arthur.connection.send(msg1);
}
};
function connected() {
console.log("inside connected");
Arthur.connection.addHandler(Arthur.handle_message,
null, "message", "chat");
console.log("inside connected");
Arthur.connection.send($pres());
};
function disconnected() {
console.log("disconnected");
Arthur.connection = null;
};
function disconnect(){
Arthur.connection.disconnect();
};
function connectXMPP() {
var conn = new Strophe.Connection(
hosturl.URL+":5280/http-bind");
console.log("inside connection");
conn.connect(Arthur.jid, Arthur.password, function (status) {
if (status === Strophe.Status.CONNECTED) {
connected();
console.log("connected");
} else if (status === Strophe.Status.DISCONNECTED) {
disconnected();
}
});
Arthur.connection = conn;
};
I don't know whether this is the case here, but in general when connections take long to established, the cause is often a host name resolution issue.
For instance, if a host has both an IPv4 and IPv6 address, but there is no IPv6 connectivity yet IPv6 is tried first, then you may get a delay until the IPv6 connection attempt has timed out.
To examine whether domain name resolution is the cause, you might want to take a look at the network traffic using a tool such as Wireshark.

Categories