Skip to content

Instantly share code, notes, and snippets.

@photofroggy
Last active December 16, 2015 03:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save photofroggy/5373407 to your computer and use it in GitHub Desktop.
Save photofroggy/5373407 to your computer and use it in GitHub Desktop.
/**
* dAmn video extension.
*/
var dVideo = {};
dVideo.VERSION = '0.0.1';
dVideo.STATE = 'pre-alpha';
dVideo.extension = function( client ) {
if( !dVideo.RTC.PeerConnection )
return;
var init = function ( ) {
client.bds.provides.push( 'PEER' );
// Event bindings
client.bind( 'BDS.PEER.REQUEST', function( event ) { handle.peer.request( event ); } );
client.bind( 'BDS.PEER.ACK', function( event ) { handle.peer.ack( event ); } );
client.bind( 'BDS.PEER.REJECT', function( event ) { handle.peer.reject( event ); } );
client.bind( 'BDS.PEER.ACCEPT', function( event ) { handle.peer.accept( event ); } );
client.bind( 'BDS.PEER.OPEN', function( event ) { handle.peer.open( event ); } );
client.bind( 'BDS.PEER.END', function( event ) { handle.peer.end( event ); } );
client.bind( 'BDS.PEER.OFFER', function( event ) { handle.peer.offer( event ); } );
client.bind( 'BDS.PEER.ANSWER', function( event ) { handle.peer.answer( event ); } );
client.bind( 'BDS.PEER.CLOSE', function( event ) { handle.peer.close( event ); } );
client.ui.control.add_button({
label: '',
icon: 'camera',
title: 'Start a video call.',
href: '#call',
handler: function( ) {
dVideo.create_phone( client );
}
});
};
/**
* Handle commands!
*/
var cmds = {
};
/**
* Handle events.
*/
var handle = {
peer: {
request: function( event ) {
if( event.sns[0] != '@' )
return;
var user = event.param[0];
var pns = event.param[1];
// Away or ignored
if( client.ui.umuted.indexOf( user.toLowerCase() ) != -1 ) {
client.npmsg(event.ns, 'BDS:PEER:REJECT:' + pns + ',' + user + ',You have been blocked');
return;
}
if( client.away.on ) {
client.npmsg(event.ns, 'BDS:PEER:REJECT:'+pns+','+user+',Away; ' + client.away.reason);
return;
}
if( dVideo.phone ) {
if( dVideo.phone.call != null ) {
if( !dVideo.phone.call.group ) {
client.npmsg( event.ns, 'BDS:PEER:REJECT:' + pns + ',' + user + ',Already in a call' );
return;
}
}
} else {
dVideo.create_phone( client );
}
client.npmsg(event.ns, 'BDS:PEER:ACK:' + pns + ',' + user);
// Tell the user about the call.
dVideo.phone.incoming( pns, user, event );
},
// Don't really need to do anything here
// Unless we set a timeout for requests
ack: function( event ) {},
reject: function( event ) {
if( event.sns[0] != '@' )
return;
// dVideo.phone.call.close();
// dVideo.phone.call = null;
},
accept: function( event ) {
if( event.sns[0] != '@' )
return;
if( !dVideo.phone.call )
return;
var call = dVideo.phone.call;
var pns = event.param[0];
var user = event.param[1];
var chan = event.param[2] || event.ns;
if( user.toLowerCase() != client.settings.username.toLowerCase() )
return;
var peer = dVideo.phone.call.new_peer( pns, event.user );
if( !peer ) {
return;
}
peer.conn.ready(
function( ) {
dVideo.signal.offer( peer );
}
);
},
open: function( event ) {},
end: function( event ) {},
offer: function( event ) {
if( event.sns[0] != '@' )
return;
if( !dVideo.phone )
return;
if( !dVideo.phone.call )
return;
var call = dVideo.phone.call;
var pns = event.param[0];
var user = event.param[1];
var target = event.param[2];
var offer = new dVideo.RTC.SessionDescription( JSON.parse( event.param.slice(3).join(',') ) );
if( target.toLowerCase() != client.settings.username.toLowerCase() )
return;
// Away or ignored
if( client.ui.umuted.indexOf( user.toLowerCase() ) != -1 ) {
dVideo.signal.reject( user, 'You have been blocked' );
return;
}
if( client.away.on ) {
dVideo.signal.reject( user, 'Away, reason: ' + client.away.reason );
return;
}
var peer = call.peer( user );
if( !peer ) {
if( !call.group )
return;
peer = call.new_peer( pns, user );
}
peer.conn.ready(
function( ) {
dVideo.signal.answer( peer );
},
offer
);
},
answer: function( event ) {
if( event.sns[0] != '@' )
return;
if( !dVideo.phone )
return;
if( !dVideo.phone.call )
return;
var call = dVideo.phone.call;
var pns = event.param[0];
var user = event.param[1];
var target = event.param[2];
var offer = new dVideo.RTC.SessionDescription( JSON.parse( event.param.slice(3).join(',') ) );
if( target.toLowerCase() != client.settings.username.toLowerCase() )
return;
var peer = call.peer( user );
if( !peer )
return;
peer.conn.open(
function( ) {
// console.log('> connected to new peer ' + peer.user);
},
offer
);
},
close: function( event ) {},
},
};
init();
};
/**
* Array containing bots used to manage calls.
*
* We could have Botdom doing this, but I dunno. Maybe a different bot would be
* a better idea.
*/
dVideo.bots = [ 'botdom', 'damnphone' ];
/**
* Options for peer candidates.
*/
dVideo.peer_options = {
iceServers: [
{ url: 'stun:stun.l.google.com:19302' }
]
};
/**
* webRTC objects
*/
dVideo.RTC = {
PeerConnection: null,
SessionDescription: null,
IceCandidate: null,
}
dVideo._gum = function() {};
dVideo.getUserMedia = function( options, success, error ) {
return dVideo._gum( options, success, error );
};
if( window.mozRTCPeerConnection ) {
dVideo.RTC.PeerConnection = mozRTCPeerConnection;
dVideo.RTC.SessionDescription = mozRTCSessionDescription;
dVideo.RTC.IceCandidate = mozRTCIceCandidate;
dVideo._gum = function( options, success, error ) {
return navigator.mozGetUserMedia( options, success, error );
};
}
if( window.webkitRTCPeerConnection ) {
dVideo.RTC.PeerConnection = webkitRTCPeerConnection;
dVideo._gum = function( options, success, error ) {
return navigator.webkitGetUserMedia( options, success, error );
};
}
if( window.RTCPeerConnection ) {
dVideo.RTC.PeerConnection = RTCPeerConnection;
dVideo._gum = function( options, success, error ) {
return navigator.getUserMedia( options, success, error );
};
}
if( window.RTCSessionDescription ) {
dVideo.RTC.SessionDescription = RTCSessionDescription;
dVideo.RTC.IceCandidate = RTCIceCandidate;
}
/**
* Holds a reference to the phone.
*/
dVideo.phone = null;
/**
* Signaling channel object
*/
dVideo.signal = null;
/**
* Object detailing the local peer.
*/
dVideo.local = {};
dVideo.local.stream = null;
dVideo.local.url = null;
/**
* Objects detailing remote peers.
*/
dVideo.remote = {};
dVideo.remote._empty = {
video: null,
audio: null,
conn: null
};
/**
* Current channel stuff.
*/
dVideo.chan = {};
dVideo.chan.group = false;
dVideo.chan.calls = [];
/**
* Create a signaling channel.
* @method create_signaling_channel
* @param client {Object} Reference to a client
* @param bds {String} dAmn channel used for bds commands
* @param pns {String} Peer namespace associated with the signals
* @param ns {String} Channel associated with the connection, if any
*/
dVideo.create_signaling_channel = function( client, bds, pns, ns ) {
var user = client.settings.username;
var nse = ns ? ',' + ns : '';
dVideo.signal = {
request: function( ) {
client.npmsg( bds, 'BDS:PEER:REQUEST:' + user + ',' + pns + ',' + nse );
},
accept: function( auser ) {
client.npmsg( bds, 'BDS:PEER:ACCEPT:' + pns + ',' + auser + nse );
},
offer: function( peer ) {
client.npmsg( bds, 'BDS:PEER:OFFER:' + pns + ',' + user + ',' + peer.user + ',' + JSON.stringify( peer.conn.offer ) );
},
answer: function( peer ) {
client.npmsg( bds, 'BDS:PEER:ANSWER:' + pns + ',' + user + ',' + peer.user + ',' + JSON.stringify( peer.conn.offer ) );
},
reject: function( ruser, reason ) {
reason = reason ? ',' + reason : '';
client.npmsg( bds, 'BDS:PEER:REJECT:' + pns + ',' + ruser + reason );
},
close: function( cuser ) {
client.npmsg( bds, 'BDS:PEER:CLOSE:' + pns + ',' + ( cuser || user ) );
},
list: function( channel ) {
channel = channel ? ':' + channel : '';
client.npmsg( bds, 'BDS:PEER:LIST' + channel );
}
};
};
/**
* Create a new video phone.
*/
dVideo.create_phone = function( client ) {
dVideo.phone = new dVideo.Phone( client );
};
/**
* Video phone
*
* @class dVideo.Phone
* @constructor
* @param client {Object} Reference to the wsc client
*/
dVideo.Phone = function( client ) {
// SET PROPERTIES
this.call = null;
this.client = client;
this.stream = null;
this.url = null;
this.view = null;
this.build();
};
/**
* Build the phone UI.
*
* @method build
*/
dVideo.Phone.prototype.build = function( ) {
this.client.ui.view.append('<div class="phone"></div>');
this.view = this.client.ui.view.find('div.phone');
};
/**
* Start or join a call.
* @method dial
* @param bds {String} dAmn channel being used for BDS commands
* @param pns {String} Peer namespace for the call
* @param [ns=bds] {String} dAmn channel for the call
* @param [host=pns.user] {String} Host of the call
* @return {Object} Reference to the current call
*/
dVideo.Phone.prototype.dial = function( bds, pns, ns, host ) {
if( this.call != null )
return this.call;
ns = ns || bds;
this.call = new dVideo.Phone.Call( this, bds, ns, pns, host );
dVideo.signal.request( );
return this.call;
};
/**
* Receive an incoming peer connection.
*
* @method incoming
* @param pns {String} Peer connection namespace
* @param user {String} User of the connection
* @param event {Object} Event data
*/
dVideo.Phone.prototype.incoming = function( pns, user, event ) {
var client = this.client;
if( this.call == null ) {
var pnotice = client.ui.pager.notice({
'ref': 'call-' + user,
'icon': '<img src="' + wsc.dAmn.avatar.src(user,
client.channel(event.ns).info.members[user].usericon) + '" />',
'heading': user + ' calling...',
'content': user + ' is calling you.',
'buttons': {
'answer': {
'ref': 'answer',
'target': 'answer',
'label': 'Answer',
'title': 'Answer the call',
'click': function( ) {
client.ui.pager.remove_notice( pnotice );
dVideo.phone.answer( event.ns, event.param[2] || event.ns, pns, user );
return false;
}
},
'reject': {
'ref': 'reject',
'target': 'reject',
'label': 'Reject',
'title': 'Reject the call',
'click': function( ) {
client.npmsg(event.ns, 'BDS:PEER:REJECT:'+pns+',' + user);
client.ui.pager.remove_notice( pnotice );
return false;
}
}
}
}, true );
pnotice.onclose = function( ) {
client.npmsg( event.ns, 'BDS:PEER:REJECT:' + event.user );
};
return;
}
var peer = this.call.new_peer( pns, user );
var call = this.call;
if( !peer ) {
dVideo.signal.reject( user, 'Permission denied' );
return;
}
/**
* Usually we have this:
*
* <a> BDS:PEER:REQUEST:a,pchat:a:b
* <b> BDS:PEER:ACK:a,pchat:a:b
* <b> BDS:PEER:ACCEPT:pchat:a:b,a
* <a> BDS:PEER:OFFER:pchat:a:b,a,b,[offer]
* <b> BDS:PEER:ANSWER:pchat:a:b,b,a,[answer]
*
* But for group calls we do this when the bot connects you to another
* user in a group call:
*
* <bot> BDS:PEER:REQUEST:user,bdsc:botlab-meeting,chat:botlab
* <you> BDS:PEER:OFFER:bdsc:botlab-meeting,you,user,[offer]
* <bot> BDS:PEER:ANSWER:bdsc:botlab-meeting,user,you,[answer]
*
* Similarly, the client should be able to respond to offers on the other
* side of this, which will simply run as follows:
*
* <bot> BDS:PEER:OFFER:bdsc:botlab-meeting,you,user,[offer]
* <user> BDS:PEER:ANSWER:bdsc:botlab-meeting,user,you,[answer]
*
* Request is not needed on this side of things. Make it so.
*/
if( !this.call.group ) {
dVideo.signal.accept( user );
return;
}
peer.conn.ready(
function( ) {
dVideo.signal.offer( peer );
}
);
};
/**
* Answer a call.
* @method answer
* @param bds {String} dAmn channel being used for bds messages
* @param ns {String} dAmn channel associated with the call
* @param pns {String} Peer namespace for the call
* @param user {String} User for the call
*/
dVideo.Phone.prototype.answer = function( bds, ns, pns, user ) {
this.call = new dVideo.Phone.Call( this, bds, ns, pns, user );
var peer = this.call.new_peer( pns, user );
if( !peer ) {
dVideo.signal.reject( user, 'Permission denied' );
this.call.close();
this.call = null;
return;
}
dVideo.signal.accept( user );
};
/**
* Call object. Maybe a bit over the top here.
* @class dVideo.Phone.Call
* @constructor
* @param phone {Object} Phone the call is being made on
* @param bds {String} dAmn channel being used for bds messages
* @param ns {String} dAmn channel the call is connected to
* @param pns {String} Peer namespace the call is associated with
* @param [user=pns.user] {String} User who started the call
*/
dVideo.Phone.Call = function( phone, bds, ns, pns, user ) {
this.phone = phone;
this.bds = bds;
this.pns = pns;
this.ns = ns;
this.user = '';
this.peers = {};
this.spns = this.pns.split('-');
this.ans = this.spns.shift();
this.rns = this.spns.join(' ');
this.group = dVideo.bots.indexOf( this.ns.substr( 1 ) ) != -1;
this.user = user || this.spns[0].substr(1);
this.dans = phone.client.deform_ns( this.ans );
dVideo.create_signaling_channel( this.phone.client, bds, pns, ns );
dVideo.getUserMedia(
{ video: true, audio: true },
function( stream ) {
dVideo.phone.url = URL.createObjectURL( stream );
dVideo.phone.stream = stream;
}
);
};
/**
* Close the call.
* @method close
*/
dVideo.Phone.Call.prototype.close = function( ) {
for( var p in this.peers ) {
if( !this.peers.hasOwnProperty( p ) )
continue;
this.peers[p].conn.close();
}
};
/**
* Add a new peer to the call.
* @method new_peer
* @param pns {String} Peer namespace for the call
* @param user {String} Name of the peer
* @return {Object} New peer connection object or null if failed
*/
dVideo.Phone.Call.prototype.new_peer = function( pns, user ) {
if( this.pns != pns )
return null;
if( !this.group ) {
if( this.dans.substr(1).toLowerCase() != user.toLowerCase() )
return null;
}
var peer = {
user: user,
conn: dVideo.peer_connection(),
stream: null,
url: null
};
this.peers[user] = peer;
return peer;
};
/**
* Get a peer.
* @method peer
* @param peer {String} Name of the peer
* @return {Object} Peer connection object or null
*/
dVideo.Phone.Call.prototype.peer = function( peer ) {
return this.peers[peer] || null;
};
/**
* lol
*
* Make a peer connection.
*/
dVideo.peer_connection = function( remote ) {
if( !dVideo.RTC.PeerConnection )
return null;
return new dVideo.PeerConnection( remote );
};
/**
* Our own wrapper for RTCPeerConnection objects.
*
* Because boilerplate? Yeah, that.
*
* @class dVideo.PeerConnection
* @constructor
* @param [remote_offer=null] {String} Descriptor for a remote offer.
*/
dVideo.PeerConnection = function( remote_offer ) {
this.pc = new dVideo.RTC.PeerConnection( dVideo.peer_options );
this.offer = '';
this.remote_offer = remote_offer || null;
this.responding = this.remote_offer != null;
this.bindings();
if( this.remote_offer )
this.set_remote_description( this.remote_offer );
};
/**
* Set up event bindings for the peer connection.
* @method bindings
*/
dVideo.PeerConnection.prototype.bindings = function( ) {
var pc = this;
// For those things that still do things in ice candidate mode or whatever.
this.pc.onicecandidate = function( candidate ) {
console.log('candidate',candidate);
//pc.pc.addIceCandidate( candidate );
};
// Stub event handler
var stub = function() {};
this.onready = stub;
this.onopen = stub;
};
/**
* Ready the connection.
*
* Callback fired when the connection is ready to be opened. IE, when a local
* offer is set. Signalling channels should be used to transfer offer information.
*
* If a remote offer is provided, then the object generates an answer for the
* offer.
*
* @method ready
* @param onready {Function} Callback to fire when the connection is ready
* @param [remote=null] {String} Descriptor for a remote offer
*/
dVideo.PeerConnection.prototype.ready = function( onready, remote ) {
this.onready = onready || this.onready;
this.remote_offer = remote || this.remote_offer;
this.responding = this.remote_offer != null;
if( this.responding ) {
var onopen = this.onopen;
var pc = this;
this.onopen = function( ) {
pc.answer();
pc.onopen = onopen;
};
this.set_remote_description( this.remote_offer );
return;
}
this.create_offer();
};
/**
* Open a connection to a remote peer.
*
* @method open
* @param onopen {Function} Callback to fire when the connection is open
* @param [offer=null] {String} Descriptor for the remote connection
*/
dVideo.PeerConnection.prototype.open = function( onopen, offer ) {
if( !this.offer )
return;
this.remote_offer = offer || this.remote_offer;
this.onopen = onopen;
if( !this.remote_offer )
return;
this.set_remote_description( this.remote_offer );
};
/**
* Close a connection
* @method close
*/
dVideo.PeerConnection.prototype.close = function( ) {
this.pc.close();
};
/**
* Method usually called on errors.
* @method onerror
*/
dVideo.PeerConnection.prototype.onerror = function( err ) {
console.log( '>> Got an error:', '"', err.message, '"', err );
};
/**
* Create an offer for a connection.
*
* Helper method.
* @method create_offer
*/
dVideo.PeerConnection.prototype.create_offer = function( ) {
var pc = this;
this.pc.createOffer(
function( description ) { pc.offer_created( description ); },
function( err ) { pc.onerror( err ); }
);
};
/**
* An offer has been created! Set it as our local description.
* @method offer_created
* @param description {String} Descriptor for the offer.
*/
dVideo.PeerConnection.prototype.offer_created = function( description ) {
this.offer = description;
var pc = this;
this.pc.setLocalDescription( this.offer , function( ) { pc.local_description_set(); }, this.onerror );
};
/**
* Set the descriptor for the remote connection.
* @method set_remote_description
* @param description {String} Descriptor for the remote connection
*/
dVideo.PeerConnection.prototype.set_remote_description = function( description ) {
this.remote_offer = description;
var pc = this;
this.pc.setRemoteDescription( this.remote_offer , function( ) { pc.remote_description_set(); }, this.onerror );
};
/**
* A local description as been set. Handle it!
* @method local_description_set
*/
dVideo.PeerConnection.prototype.local_description_set = function( ) {
this.onready();
};
/**
* A local description as been set. Handle it!
* @method remote_description_set
*/
dVideo.PeerConnection.prototype.remote_description_set = function( ) {
this.onopen();
};
/**
* Create an answer for a remote offer.
* @method answer
*/
dVideo.PeerConnection.prototype.answer = function( ) {
var pc = this;
this.responding = true;
this.pc.createAnswer(
function( answer ) { pc.answer_created( answer ); },
function( err ) { pc.onerror( err ); }
);
};
/**
* Answer has been created. Send away, or something.
* @method answer_created
* @param answer {String} Descriptor for answer.
*/
dVideo.PeerConnection.prototype.answer_created = function( answer ) {
this.offer = answer;
var pc = this;
this.pc.setLocalDescription( this.offer,
function( ) { pc.local_description_set(); },
function( err ) { pc.onerror( err ); }
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment