Last active
December 16, 2015 03:59
-
-
Save photofroggy/5373407 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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