espruino
Version:
Command Line Interface and library for Communications with Espruino JavaScript Microcontrollers
423 lines (380 loc) • 14.6 kB
JavaScript
/* This is common code for handling connections with WebRTC using Peer.js
It handles both client and server.
This needs:
EspruinoWebIDE/js/libs/peerjs.min.js
* The 'Bridge' connects direct to Bluetooth/Bangle.js - it is the Client
* The 'Client' (Web IDE) is a server
Protocol:
* Both Bridge and Client have servers
* If the 'Bridge' connects to the 'Client', the Client is immediately sent a {t:"serverId",id:"..."} packet
* The Client then closes and connects to the Bridge
* When the Client connects to the Bridge, things are working correctly
Packets Sent:
{ t : "getPorts" }
-> { t : "getPortsResponse", ports: [ {}, ...] }
{ t : "connect", port : port }
-> { t : "connectResponse" }
-> { t : "disconnected" } // device got disconnected
{ t : "disconnect" }
-> { t : "disconnectResponse" }
{ t : "write", data : data }
-> { t : "writeResponse" }
"ping"
-> If bridge doesn't get a ping in WEBRTC_INACTIVITY_TIMEOUT it closes the connection
which we have to do because peer.js doesn't seem to tell us when a connection is closed
TODO:
* The Client (IDE) side needs to be able to remember the Bridge's ID and autoconnect
* We need to have repeated retries for the connection
* Clicking top-left in the bridge to pair should automatically disconnect
* When first connecting (eg localStorage/IDE config empty) it may not work correctly? After a refresh it does
*/
/* Create a connection:
options = {
bridge : bool
// true = we're the one communicating with the Bangle
// false = we're communicating with the user
connectToPeerID : string // if set, we connect to this peer as soon as we start
onStatus : function(msg) // show status message
onPeerID : function(id) // we have a peer ID now
onBridgePeerID : function(id) // CLIENT: the bridge connected to us and gave us its peer ID
onPeerConnected : function // a peer has connected
onPeerDisconnected : function // a peer has disconnected
onGetPorts : function(cb) // BRIDGE: get port list
onVideoStream : function(stream) // CLIENT: We have a video stream - display it!
onPortConnect : function(port, cb) // BRIDGE: start connecting
onPortDisconnect : function(cb) // BRIDGE: close active connection
onPortWrite : function(data, cb) // BRIDGE: send data
onPortReceived : function(data) // CLIENT: Got data from device
onPortDisconnected : function() // CLIENT: Port disconnected
connectVideo : function(stream) // BRIDGE: Start a video connection with the given medias stream
}
Return options with extras added:
{
connect : function(id) // connect to the given peer ID
connectSendPeerId : function(id) // BRIDGE: Connect to the client in order to tell it to connect back to us!
getPorts : function(cb) // get current ports
portConnect : function(port, cb) // CLIENT: Connect to a device
portDisconnect : function(cb) // CLIENT: Disconnect
onPortDisconnected : function() // BRIDGE: Signal client was disconnected
portWrite : function(data, cb) // CLIENT: Send data
onPortReceived : function(data) // BRIDGE: Got data from device
peerId : "..." // The peer ID of *this* peer
connections : [] // list of open connections
connection : undefined // the current open connection (since we are only supporting one connection right now)
}
*/
if (typeof Peer == "undefined") {
try {
if (typeof process != "undefined")
Peer = require("peerjs-on-node").Peer;
global.File = function() { }; // fix for `ReferenceError: File is not defined` in peerjs-on-node
} catch (e) {
console.log('webrtc-connection: require("peerjs-on-node") failed: ', e);
}
}
if (typeof Peer == "undefined") {
console.log("webrtc-connection: Peer.js not loaded - Remote Connection disabled");
throw "No Peer.js implementation available";
}
var peer; // peer.js connection
function webrtcInit(options) {
const WEBRTC_INACTIVITY_TIMEOUT = 10000; // how long before we kill a socket if no acitivity (because close eventys not getting fired!)
const WEBRTC_PING_INTERVAL = 1000; // how often do we send out 'ping' messages?
var callbacks = {}; // list of callbacks
options = options||{};
options.peerId = null;
options.connections = [];
options.connection = undefined; // active connection
var localStorageName = options.bridge ? "WEBRTC_BRIDGE_PEER_ID" : "WEBRTC_CLIENT_PEER_ID";
if ("undefined"!=typeof window &&
window.localStorage &&
window.localStorage.getItem(localStorageName))
options.peerId = window.localStorage.getItem(localStorageName);
peer = new Peer(options.peerId, {
debug: 2
});
var conn;
peer.on('open', function (id) {
// Workaround for peer.reconnect deleting previous id
if (peer.id === null) {
console.log('[WebRTC] Received null id from peer open');
peer.id = options.peerId;
} else {
options.peerId = peer.id;
if ("undefined"!=typeof window &&
window.localStorage &&
window.localStorage.setItem(localStorageName, peer.id));
}
options.peerId = peer.id;
console.log('[WebRTC] Peer ID: ' + peer.id);
if (options.onPeerID)
options.onPeerID(peer.id);
if (options.connectToPeerID)
conn = options.connect(options.connectToPeerID);
});
peer.on('disconnected', function () {
options.onStatus("Connection lost. Reconnecting...");
console.log('[WebRTC] Connection lost. Reconnecting....');
// Workaround for peer.reconnect deleting previous id
peer.id = options.peerId;
peer._lastServerId = options.peerId;
peer.reconnect();
});
peer.on('close', function() {
options.onStatus("Connection destroyed. Please refresh");
console.log('Connection destroyed');
});
peer.on('error', function (err) {
console.log(err);
options.onStatus("ERROR: "+err);
// remove this from our active connections list
if (conn) {
webrtcRemoveConnection(conn);
conn = undefined;
}
});
peer.on('call', function(call) {
if (options.onVideoStream) {
call.on('stream', function(stream) {
// `stream` is the MediaStream of the remote peer.
options.onVideoStream(stream);
});
call.on('close', function() {
// this seems broken https://github.com/peers/peerjs/issues/636
options.onVideoStream(undefined);
});
call.answer(/*mediaStream*/); // answer - not sending a stream back...
}
});
peer.on('connection', function (conn) {
console.log("[WebRTC] Incoming connection");
/* If we're a client, we just want to listen for connections
which will tell us where *we* should connect */
if (!options.bridge) {
console.log("[WebRTC] ServerId: We're a client - wait for a serverId packet");
conn.on('open', function () {
console.log("[WebRTC] ServerId: Connected to: " + conn.peer);
});
conn.on('data', function (data) {
if ("object" == typeof data &&
data.t=="serverId" &&
data.id) {
console.log("[WebRTC] ServerId: Got server ID : "+data.id);
if (options.onBridgePeerID)
options.onBridgePeerID(data.id);
options.connect(data.id);
} else
console.log("[WebRTC] ServerId: Unknown packet!",data);
});
conn.on('close', function () {
console.log("[WebRTC] ServerId: Connection closed");
});
return;
}
// We're a bridge - handle this connection!
console.log("[WebRTC] Connected to: " + conn.peer);
options.onStatus("Connected to: " + conn.peer);
if (options.onPeerConnected) options.onPeerConnected();
webrtcAddHandlers(conn);
});
Object.assign(options, { // ============== COMMON
connect : function(id) {
console.log("[WebRTC] Connecting to "+id+"...");
var conn = peer.connect(id, {
reliable: true
});
webrtcAddHandlers(conn);
return conn;
}
});
function send(data) {
options.connections.forEach(conn => conn.send(data));
}
if (options.bridge) {
Object.assign(options, { // ============== BRIDGE
getPorts : function(cb) { // CLIENT
callbackAddWithTimeout("getPorts", cb, 2000);
send({t:"getPorts"});
},
portConnect : function(port, cb) { // CLIENT
callbackAddWithTimeout("connect", cb, 2000);
send({t:"connect", port:port});
},
portDisconnect : function(cb) { // CLIENT
callbackAddWithTimeout("disconnect", cb, 2000);
send({t:"disconnect"});
},
onPortDisconnected : function() {
send({t:"disconnected"});
},
onPortReceived : function(data) {
send({t:"received", data});
},
portWrite : function(data, cb) { // CLIENT
callbackAddWithTimeout("write", cb, 2000);
send({t:"write", data:data});
},
connectVideo : function (stream) {
if (!webrtc.connections.length) {
console.log("[WebRTC].connectVideo no active connections");
return;
}
webrtc.connections.forEach(c => {
peer.call(c.peer, stream);
});
},
connectSendPeerId : function(id) {
console.log("[WebRTC] ServerId: Connecting to "+id+" send our ID...");
var conn = peer.connect(id, {
reliable: true
});
conn.on('open', function () {
console.log("[WebRTC] ServerId: Connected - send our ID" );
conn.send( {t:"serverId",id:options.peerId});
setTimeout(function() {
console.log("[WebRTC] ServerId: Closing");
conn.close();
}, 1000);
});
}
});
} else {
Object.assign(options, { // ============== CLIENT
getPorts : function(cb) { // CLIENT
callbackAddWithTimeout("getPorts", cb, 1000);
send({t:"getPorts"});
},
portConnect : function(port, cb) { // CLIENT
callbackAddWithTimeout("connect", cb, 10000);
send({t:"connect", port:port});
},
portDisconnect : function(cb) { // CLIENT
callbackAddWithTimeout("disconnect", cb, 2000);
send({t:"disconnect"});
},
portWrite : function(data, cb) { // CLIENT
callbackAddWithTimeout("write", cb, 2000);
send({t:"write", data:data});
},
});
}
// ---------------------------------
function callbackAddWithTimeout(id, cb, timeout) {
var o = { cb : cb };
o.timeout = setTimeout(function() {
console.log("[WebRTC] Timeout for "+id);
o.timeout = undefined;
if (o.cb) o.cb();
o.cb = undefined;
}, timeout);
callbacks[id] = o;
}
function callbackCall(id, data) {
if (!callbacks[id]) return; // no callback set
if (callbacks[id].cb)
callbacks[id].cb(data);
callbacks[id].cb = undefined;
if (callbacks[id].timeout)
clearTimeout(callbacks[id].timeout);
callbacks[id] = undefined;
}
function webrtcRemoveConnection(conn) {
var idx = options.connections.indexOf(conn);
if (idx>=0)
options.connections.splice(idx,1);
}
function webrtcAddHandlers(conn) {
options.connection = conn;
options.connections.push(conn);
function timeoutHandler() {
conn.inactivityTimeout = undefined;
console.log("[WebRTC] No activity on connection - closing "+conn.peer);
conn.close();
}
conn.inactivityTimeout = setTimeout(timeoutHandler, WEBRTC_INACTIVITY_TIMEOUT);
conn.on('open', function () {
console.log("[WebRTC] Connected to: " + conn.peer);
if (options.onPeerConnected) options.onPeerConnected(conn.peer);
conn.keepAliveTimer = setInterval(function() {
conn.send("ping"); // don't send anything useful so we won't have to decode it in webrtcDataHandler
}, WEBRTC_PING_INTERVAL);
});
// Handle incoming data (messages only since this is the signal sender)
conn.on('data', function (data) {
webrtcDataHandler(conn, data, options);
if (conn.inactivityTimeout) clearTimeout(conn.inactivityTimeout);
conn.inactivityTimeout = setTimeout(timeoutHandler, WEBRTC_INACTIVITY_TIMEOUT);
});
conn.on('close', function () {
console.log("[WebRTC] Connection closed on " + conn.peer);
options.onStatus("Disconnected from: " + conn.peer);
options.connection = undefined;
webrtcRemoveConnection(conn);
if (conn.keepAliveTimer) {
clearInterval(conn.keepAliveTimer);
conn.keepAliveTimer = undefined;
}
if (conn.inactivityTimeout) {
clearTimeout(conn.inactivityTimeout);
conn.inactivityTimeout = undefined;
}
if (options.onPeerDisconnected)
options.onPeerDisconnected(conn.peer);
});
}
function webrtcDataHandler(conn, data, options) {
if ("object" != typeof data) return; // ignore ping/etc
console.log("[WebRTC] data " + JSON.stringify(data));
if (options.bridge) switch (data.t) { // ================= BRIDGE
case "getPorts" : // BRIDGE
options.onGetPorts(ports => {
conn.send({t:"getPortsResponse", ports:ports});
});
break;
case "connect" : // BRIDGE
options.onPortConnect(data.port, () => {
conn.send({t:"connectResponse"});
});
break;
case "disconnect" : // BRIDGE
options.onPortDisconnect(data.port, () => {
conn.send({t:"disconnectResponse"});
});
break;
case "write" : // BRIDGE
options.onPortWrite(data.data, () => {
conn.send({t:"writeResponse"});
});
break;
} else switch (data.t) { // ======================CLIENT
case "getPortsResponse" : // CLIENT
data.ports.forEach(port => {
// TODO set port.type so we get a different icon?
if (port.description)
port.description = "REMOTE: "+port.description;
});
callbackCall("getPorts", data.ports);
break;
case "connectResponse" : // CLIENT
callbackCall("connect");
break;
case "disconnected" : // CLIENT
options.onPortDisconnected();
break;
case "disconnectResponse" : // CLIENT
callbackCall("disconnect");
break;
case "writeResponse" : // CLIENT
callbackCall("write");
break;
case "received" : // CLIENT
options.onPortReceived(data.data);
break;
}
}
// -----------------------------------------------------
return options;
}
// Fix for running under Node.js
if (typeof global != "undefined")
global.webrtcInit = webrtcInit;