eureca.io
Version:
Nodejs transparent bidirectional remote procedure call (RPC) supporting multiple transports : WebRTC, Websocket, engine.io, faye ...etc
260 lines (171 loc) • 9.22 kB
text/typescript
/// <reference path="../EObject.class.ts" />
/// <reference path="../Util.class.ts" />
///
//highly inspired from https://github.com/cjb/serverless-webrtc/
/** @ignore */
declare var webrtc;
declare var require;
module Eureca.Transports.WebRTC {
var webrtc;
if (Eureca.Util.isNodejs) {
try {
webrtc = require('wrtc');
} catch (e) {
//console.error("wrtc module not found : WebRTC support will not be available");
//process.exit(e.code);
webrtc = {unavailable:true, error:e};
}
}
var PeerConnection = Eureca.Util.isNodejs ? webrtc.RTCPeerConnection : window['RTCPeerConnection'] || window['mozRTCPeerConnection'] || window['webkitRTCPeerConnection'];
var SessionDescription = Eureca.Util.isNodejs ? webrtc.RTCSessionDescription : window['RTCSessionDescription'] || window['mozRTCSessionDescription'] || window['webkitRTCSessionDescription'];
export class Peer extends EObject {
public id = Util.randomStr(16);
public pc = null;
private offer = null;
//private answer = null;
public channel = null;
public pendingDataChannels = {};
public dataChannels = {}
public cfg = {
"iceServers": [
{ "urls": "stun:stun.l.google.com:19302" },
{ "urls": 'stun:stun1.l.google.com:19302' }
]
};
//public con = { 'optional': [{ 'DtlsSrtpKeyAgreement': true }] };
public con;
public channelSettings = {
reliable: true,
ordered: true,
maxRetransmits:null
}
constructor(settings: any= {reliable:true}) {
super();
if (webrtc && webrtc.unavailable) {
console.error("wrtc module not found\n");
console.error(" * Please follow instructions here https://github.com/js-platform/node-webrtc to install wrtc\n");
console.error(" * Note : WebRTC is only supported on x64 platforms\n");
process.exit();
}
if (typeof settings.reliable != 'undefined') this.channelSettings.reliable = settings.reliable;
if (typeof settings.maxRetransmits != 'undefined') this.channelSettings.maxRetransmits = settings.maxRetransmits;
if (typeof settings.ordered !== 'undefined') this.channelSettings.ordered = settings.ordered;
}
public makeOffer(callback:Function, failureCallback:Function) {
var __this = this;
var pc = new PeerConnection(this.cfg, this.con);
this.pc = pc;
//this.makeDataChannel();
pc.onsignalingstatechange = this.onsignalingstatechange.bind(this);
pc.oniceconnectionstatechange = this.oniceconnectionstatechange.bind({pc:pc, handler:this});
pc.onicegatheringstatechange = this.onicegatheringstatechange.bind(this);
pc.onicecandidate = function (candidate) {
// Firing this callback with a null candidate indicates that
// trickle ICE gathering has finished, and all the candidates
// are now present in pc.localDescription. Waiting until now
// to create the answer saves us from having to send offer +
// answer + iceCandidates separately.
if (candidate.candidate == null) {
if (typeof callback == 'function') callback(pc);
}
}
// If you don't make a datachannel *before* making your offer (such
// that it's included in the offer), then when you try to make one
// afterwards it just stays in "connecting" state forever. This is
// my least favorite thing about the datachannel API.
var channel = pc.createDataChannel('eureca.io',
{ /**/reliable: __this.channelSettings.reliable, maxRetransmits: __this.channelSettings.maxRetransmits, ordered: __this.channelSettings.ordered });
this.channel = channel;
pc.createOffer()
.then( desc => pc.setLocalDescription(desc), failureCallback)
.then(() => { /*pc.setLocalDescription success*/}, failureCallback)
}
public getAnswer(pastedAnswer) {
var data = typeof pastedAnswer == 'string' ? JSON.parse(pastedAnswer) : pastedAnswer;
var answer = new SessionDescription(data);
this.pc.setRemoteDescription(answer);
}
public getOffer(pastedOffer, request, callback) {
var __this = this;
var data = typeof pastedOffer === 'object' ? pastedOffer : JSON.parse(pastedOffer);
var pc = new PeerConnection(this.cfg, this.con);
//this.pc = pc;
pc.onsignalingstatechange = this.onsignalingstatechange.bind(this);
pc.oniceconnectionstatechange = this.oniceconnectionstatechange.bind({pc:pc, handler:this});
pc.onicegatheringstatechange = this.onicegatheringstatechange.bind(this);
pc.onicecandidate = function (candidate) {
// Firing this callback with a null candidate indicates that
// trickle ICE gathering has finished, and all the candidates
// are now present in pc.localDescription. Waiting until now
// to create the answer saves us from having to send offer +
// answer + iceCandidates separately.
if (candidate.candidate == null) {
if (typeof callback == 'function') callback(pc);
}
}
//var labels = Object.keys(this.dataChannelSettings);
pc.ondatachannel = function (evt) {
var channel = evt.channel;
channel.request = request;
//__this.channel = channel;
var label = channel.label;
__this.pendingDataChannels[label] = channel;
channel.binaryType = 'arraybuffer';
channel.onopen = function () {
__this.dataChannels[label] = channel;
delete __this.pendingDataChannels[label];
__this.trigger('datachannel', channel);
};
};
const offer = new SessionDescription(data);
pc.setRemoteDescription(offer)
.then(() => pc.createAnswer(), __this.doHandleError)
.then(desc => pc.setLocalDescription(desc), __this.doHandleError)
.then(()=>{}, __this.doHandleError);
}
public onsignalingstatechange(state) {
}
private lastState = '';
private stateTimeout;
public oniceconnectionstatechange(state) {
var __this = (<any>this).handler;
var pc = (<any>this).pc;
__this.trigger('stateChange', pc.iceConnectionState);
__this.lastState = pc.iceConnectionState;
if (__this.stateTimeout != undefined)
clearTimeout(__this.stateTimeout);
if (pc.iceConnectionState == 'disconnected' || pc.iceConnectionState == 'failed') {
__this.trigger('disconnected');
}
if (pc.iceConnectionState == 'completed' || pc.iceConnectionState == 'connected') {
// var ackObj = {};
// ackObj[Protocol.signalACK] = 1;
// var maxtries = 10;
// var itv = setInterval(function () {
// maxtries--;
// if (maxtries <= 0) {
// clearInterval(itv);
// __this.doHandleError('Channel readyState failure ');
// return;
// }
// if (__this.channel.readyState == 'open') {
// clearInterval(itv);
// __this.channel.send(JSON.stringify(ackObj));
// }
// }, 500);
//
}
else {
__this.stateTimeout = setTimeout(function () {
__this.trigger('timeout');
}, 5000);
}
}
public onicegatheringstatechange(state) {
//console.info('ice gathering state change:', state);
}
public doHandleError(error) {
this.trigger('error', error);
}
}
}