Twilio = { extend: function(M) { for (var k in M) Twilio[k] = M[k] } };
Twilio.extend((function(){
var VERSION = '1.2';
var files = {}, cache = {};
var require = (function(){
function require(module) {
if (!(module in cache)) {
var path = "lib/" + module + ".js", exports = {};
files[path].apply(exports, [require, exports]);
cache[module] = exports;
}
return cache[module];
};
return require;
})();
files["lib/twilio/connection.js"] = (function(require, exports){ var factories = require("twilio/factories");
var eventEmitter = require("twilio/eventemitter");
var log = require("twilio/log");
var util = require("twilio/util");
var rtc = require("twilio/rtc");
/**
* Constructor for Connections.
*
* @exports Connection as Twilio.Connection
* @memberOf Twilio
* @borrows EventEmitter#addListener as #addListener
* @borrows EventEmitter#fire as #fire
* @borrows EventEmitter#removeListener as #removeListener
* @borrows EventEmitter#hasListener as #hasListener
* @borrows Twilio.mixinLog-log as #log
* @constructor
* @param {object} device The device associated with this connection
* @param {object} message Data to send over the connection
* @param {object} [options]
* @config {string} [chunder="chunder.prod.twilio.com"] Hostname of chunder server
* @config {boolean} [debug=false] Enable debugging
* @config {boolean} [encrypt=false] Encrypt media
* @config {MediaStream} [mediaStream] Use this MediaStream object
* @config {string} [token] The Twilio capabilities JWT
*/
function Connection(device, message, options) {
if (!(this instanceof Connection)) {
return new Connection(device, message, options);
}
this.device = device;
this.message = message || {};
options = options || {};
var defaults = {
logPrefix: "[Connection]",
mediaStreamFactory: rtc.enabled() ? factories.PeerConnection
: factories.MediaStream,
offerSdp: null,
debug: false,
encrypt: false
};
for (var prop in defaults) {
if (prop in options) continue;
options[prop] = defaults[prop];
}
this.options = options;
this.parameters = {};
this._status = this.options["offerSdp"] ? "pending" : "closed";
this.sendHangup = true;
log.mixinLog(this, this.options["logPrefix"]);
this.log.enabled = this.options["debug"];
this.log.warnings = this.options['warnings'];
eventEmitter.mixinEventEmitter(this);
/**
* Reference to the Twilio.MediaStream object.
* @type Twilio.MediaStream
*/
this.mediaStream = this.options["mediaStreamFactory"](
this.options["encrypt"],
this.device,
this.device.options["simplePermissionDialog"]
);
var self = this;
this.mediaStream.onerror = function(e) {
var error = {
code: e.info.code,
message: e.info.message || "Error with mediastream",
info: e.info,
connection: self
};
self.log("Received an error from MediaStream:", e);
self.fire("error", error);
};
this.mediaStream.onopen = function() {
if (self._status == "connecting") {
self._status = "open";
//self.mediaStream.publish("input", "live"); //only for flash
self.mediaStream.attachAudio();
self.mediaStream.play("output"); //only for flash
self.fire("accept", self);
} else {
// call was probably canceled sometime before this
self.mediaStream.close();
}
};
this.mediaStream.onclose = function() {
self._status = "closed";
if (self.device.sounds.disconnect()) {
self.device.soundcache.play("disconnect");
}
self.fire("disconnect", self);
};
this.pstream = this.device.stream;
var onCancel = function(payload) {
var callsid = payload.callsid;
if (self.parameters.CallSid == callsid) {
self.cancel();
self.pstream.removeListener("cancel", onCancel);
}
};
this.pstream.addListener("cancel", onCancel);
}
/**
* @return {string}
*/
Connection.toString = function() {
return "[Twilio.Connection class]";
};
Connection.prototype = {
/**
* @return {string}
*/
toString: function() {
return "[Twilio.Connection instance]";
},
sendDigits: function(digits) {
if (digits.match(/[^0-9*#w]/)) {
throw new util.Exception(
"Illegal character passed into sendDigits");
}
var sequence = [];
for(var i = 0; i < digits.length; i++) {
var dtmf = digits[i] != "w" ? "dtmf" + digits[i] : "";
if (dtmf == "dtmf*") dtmf = "dtmfs";
if (dtmf == "dtmf#") dtmf = "dtmfh";
sequence.push([dtmf, 200, 20]);
}
this.device.soundcache.playseq(sequence);
// send pstream message to send DTMF
if (this.pstream != null && this.pstream.status != "disconnected") {
var payload = { dtmf: digits, callsid: this.parameters.CallSid };
this.pstream.publish("dtmf", payload);
} else {
var error = {
code: payload.error.code || 31000,
message: payload.error.message || "Could not send DTMF: Signaling channel is disconnected",
connection: this
};
this.fire("error", error);
}
},
status: function() {
return this._status;
},
/**
* Mute incoming audio.
*/
mute: function(muteParam) {
if (arguments.length === 0) {
this.log.deprecated('.mute() is deprecated. Please use .mute(true) or .mute(false) to mute or unmute a call instead.');
}
if (typeof muteParam == "function") {
// if handler, register listener
return this.addListener("mute",muteParam);
}
// change state if call results in transition
var origState = this.isMuted();
var self = this;
var callback = function() {
var newState = self.isMuted();
if (origState != newState) {
self.fire("mute",newState,self);
}
}
if (muteParam == false) {
// if explicitly false, unmute connection
this.mediaStream.attachAudio(callback);
} else {
// if undefined or true, mute connection
this.mediaStream.detachAudio(callback);
}
},
/**
* Check if connection is muted
*/
isMuted: function() {
return !this.mediaStream.isAudioAttached();
},
/**
* Unmute (Deprecated)
*/
unmute: function() {
this.log.deprecated('.unmute() is deprecated. Please use .mute(false) to unmute a call instead.');
this.mute(false);
},
accept: function(handler) {
if (typeof handler == "function") {
return this.addListener("accept", handler);
}
var self = this;
this._status = "connecting";
var connect_ = function(err,code) {
if (self._status != "connecting") {
// call must have been canceled
self.mediaStream.close();
return;
}
if (err) {
self._die(err,code);
return;
}
var pairs = [];
for (var key in self.message) {
pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(self.message[key]));
}
var params = pairs.join("&");
if (self.parameters.CallSid) {
self.mediaStream.answerIncomingCall.call(self.mediaStream, self.parameters.CallSid, self.options["offerSdp"]);
} else {
// temporary call sid to be used for outgoing calls
self.outboundConnectionId = util.generateConnectionUUID();
self.mediaStream.makeOutgoingCall.call(self.mediaStream, params, self.outboundConnectionId);
self.pstream.addOneTimeListener("answer", function(payload) {
if (typeof payload.callsid !== 'undefined') {
self.parameters.CallSid = payload.callsid;
self.mediaStream.callSid = payload.callsid;
}
});
}
var onHangup = function(payload) {
/**
* see if callsid passed in message matches either callsid or outbound id
* connection should always have either callsid or outbound id
* if no callsid passed hangup anyways
*/
if (payload.callsid && (self.parameters.CallSid || self.outboundConnectionId)) {
if (payload.callsid != self.parameters.CallSid && payload.callsid != self.outboundConnectionId) {
return;
}
} else if (payload.callsid) {
// hangup is for another connection
return;
}
self.log("Received HANGUP from gateway");
if (payload.error) {
var error = {
code: payload.error.code || 31000,
message: payload.error.message || "Error sent from gateway in HANGUP",
connection: self
};
self.log("Received an error from the gateway:", error);
self.fire("error", error);
}
self.sendHangup = false;
self.disconnect();
self.pstream.removeListener("hangup", onHangup);
};
self.pstream.addListener("hangup", onHangup);
};
this.mediaStream.openHelper(
connect_,
this.device.options["simplePermissionDialog"],
Connection.NO_MIC_LEVEL || 0,
{ showDialog: function() { Device.dialog.show() },
closeDialog: function(accessGranted) {
Device.dialog.hide();
self.device.options["simplePermissionDialog"] = accessGranted;
if (!accessGranted) {
self._die("Access to microphone has been denied");
Device.disconnectAll();
}
}
},
function(x) { self.device.showSettings(x); }
);
},
reject: function(handler) {
if (typeof handler == "function") {
return this.addListener("reject", handler);
}
if (this._status == "pending") {
var payload = { callsid: this.parameters.CallSid }
this.pstream.publish("reject", payload);
this.fire("reject");
}
},
ignore: function(handler) {
if (typeof handler == "function") {
return this.addListener("cancel", handler);
}
if (this._status == "pending") {
this._status = "closed";
this.fire("cancel");
}
},
cancel: function(handler) {
this.log.deprecated('.cancel() is deprecated. Please use .ignore() instead.');
this.ignore(handler);
},
disconnect: function(handler) {
if (typeof handler == "function") {
return this.addListener("disconnect", handler);
}
if (this._status == "open" || this._status == "connecting") {
this.log("Disconnecting...");
// send pstream hangup message
if (this.pstream != null && this.pstream.status != "disconnected" && this.sendHangup) {
var callId = this.parameters.CallSid || this.outboundConnectionId;
if (callId) {
var payload = { callsid: callId };
this.pstream.publish("hangup", payload);
}
}
this.mediaStream.close();
}
},
error: function(handler) {
if (typeof handler == "function") {
return this.addListener("error", handler);
}
},
_die: function(message,code) {
this.fire("error", { message: message, code: code });
}
};
exports.Connection = Connection; });
files["lib/twilio/device.js"] = (function(require, exports){ var factories = require("twilio/factories");
var eventEmitter = require("twilio/eventemitter");
var log = require("twilio/log");
var util = require("twilio/util");
var rtc = require("twilio/rtc");
var Options = require("twilio/options").Options;
var Dialog = require("twilio/dialog").Dialog;
var REG_INTERVAL = 30000;
/**
* Constructor for Device objects.
*
* @exports Device as Twilio.Device
* @memberOf Twilio
* @borrows EventEmitter#addListener as #addListener
* @borrows EventEmitter#fire as #fire
* @borrows EventEmitter#hasListener #hasListener
* @borrows EventEmitter#removeListener as #removeListener
* @borrows Twilio.mixinLog-log as #log
* @constructor
* @param {string} token The Twilio capabilities token
* @param {object} [options]
* @config {boolean} [debug=false]
*/
function Device(token, options) {
if (!(this instanceof Device)) {
return new Device(token, options);
}
if (!token) {
throw new util.Exception("Capability token is not valid or missing.");
}
// copy options
var origOptions = {};
for (i in options) {
origOptions[i] = options[i];
}
var defaults = {
logPrefix: "[Device]",
host: "chunder.twilio.com",
chunderw: "chunderw-gll.twilio.com",
soundCacheFactory: factories.SoundCache,
soundFactory: factories.Sound,
connectionFactory: factories.Connection,
pStreamFactory: factories.PStream,
presenceFactory: factories.Presence,
noRegister: false,
encrypt: false,
simplePermissionDialog: false,
rtc: true,
debug: false,
closeProtection: false,
secureSignaling: true,
warnings: true
};
options = options || {};
for (var prop in defaults) {
if (prop in options) continue;
options[prop] = defaults[prop];
}
this.options = options;
this.token = token;
this._status = "offline";
this.connections = [];
this.sounds = new Options({
incoming: true,
outgoing: true,
disconnect: true
});
if (!this.options["rtc"]) {
rtc.enabled(false);
}
// if flash, use old device
if (!rtc.enabled()) {
return new require("twilio/olddevice").Device(token,origOptions);
}
this.soundcache = this.options["soundCacheFactory"]();
var a = document.createElement("audio");
canPlayMp3 = false;
try {
canPlayMp3 = !!(a.canPlayType && a.canPlayType('audio/mpeg').replace(/no/, ''));
}
catch (e) {
}
canPlayVorbis = false;
try {
canPlayVorbis = !!(a.canPlayType && a.canPlayType('audio/ogg;codecs="vorbis"').replace(/no/, ''));
}
catch (e) {
}
var ext = "mp3";
if (canPlayVorbis && !canPlayMp3) {
ext = "ogg";
}
var urls = {
incoming: "sounds/incoming." + ext, outgoing: "sounds/outgoing." + ext,
disconnect: "sounds/disconnect." + ext,
dtmf1: "sounds/dtmf-1." + ext, dtmf2: "sounds/dtmf-2." + ext,
dtmf3: "sounds/dtmf-3." + ext, dtmf4: "sounds/dtmf-4." + ext,
dtmf5: "sounds/dtmf-5." + ext, dtmf6: "sounds/dtmf-6." + ext,
dtmf7: "sounds/dtmf-7." + ext, dtmf8: "sounds/dtmf-8." + ext,
dtmf9: "sounds/dtmf-9." + ext, dtmf0: "sounds/dtmf-0." + ext,
dtmfs: "sounds/dtmf-star." + ext, dtmfh: "sounds/dtmf-hash." + ext
};
var base = typeof TWILIO_ROOT === "undefined" ? "" : TWILIO_ROOT;
for (var name in urls) {
var sound = this.options["soundFactory"]();
sound.load(base + urls[name]);
this.soundcache.add(name, sound);
}
// Minimum duration for incoming ring
this.soundcache.envelope("incoming", { release: 2000 });
log.mixinLog(this, this.options["logPrefix"]);
this.log.enabled = this.options["debug"];
eventEmitter.mixinEventEmitter(this);
var device = this;
this.addListener("incoming", function(connection) {
connection.addOneTimeListener("accept", function() {
device.soundcache.stop("incoming");
});
connection.addOneTimeListener("cancel", function() {
device.soundcache.stop("incoming");
});
connection.addOneTimeListener("error", function() {
device.soundcache.stop("incoming");
});
connection.addOneTimeListener("reject", function() {
device.soundcache.stop("incoming");
});
if (device.sounds.incoming()) {
device.soundcache.play("incoming", 0, 1000);
}
});
// setup flag for allowing presence for media types
this.mediaPresence = { audio: !this.options["noRegister"] };
// setup stream
this.register(this.token);
var closeProtection = this.options["closeProtection"];
if (closeProtection) {
var confirmClose = function(event) {
if (device._status == "busy") {
var defaultMsg = "A call is currently in-progress. Leaving or reloading this page will end the call.";
var confirmationMsg = closeProtection == true ? defaultMsg : closeProtection;
(event || window.event).returnValue = confirmationMsg;
return confirmationMsg;
}
};
if (window.addEventListener) {
window.addEventListener("beforeunload", confirmClose);
} else if (window.attachEvent) {
window.attachEvent("onbeforeunload", confirmClose);
}
}
// close connections on unload
var onClose = function() {
device.disconnectAll();
}
if (window.addEventListener) {
window.addEventListener("unload", onClose);
} else if (window.attachEvent) {
window.attachEvent("onunload", onClose);
}
}
function makeConnection(device, params, options) {
var defaults = {
encrypt: device.options["encrypt"],
debug: device.options["debug"],
warnings: device.options['warnings']
};
options = options || {};
for (var prop in defaults) {
if (prop in options) continue;
options[prop] = defaults[prop];
}
var connection = device.options["connectionFactory"](device, params, options);
connection.addOneTimeListener("accept", function() {
device._status = "busy";
device.fire("connect", connection);
});
connection.addListener("error", function(error) {
device.fire("error", error);
// Only drop connection from device if it's pending
if (connection.status() != "pending" || connection.status() != "connecting") return;
device._removeConnection(connection);
});
connection.addOneTimeListener("cancel", function() {
device.log("Canceled: " + connection.parameters["CallSid"]);
device._removeConnection(connection);
device.fire("cancel", connection);
});
connection.addOneTimeListener("disconnect", function() {
if (device._status == "busy") device._status = "ready";
device.fire("disconnect", connection);
device._removeConnection(connection);
});
connection.addOneTimeListener("reject", function() {
device.log("Rejected: " + connection.parameters["CallSid"]);
device._removeConnection(connection);
});
return connection;
}
/**
* @return {string}
*/
Device.toString = function() {
return "[Twilio.Device class]";
};
Device.prototype = {
/**
* @return {string}
*/
toString: function() {
return "[Twilio.Device instance]";
},
register: function(token) {
if (this.stream && this.stream.status != "disconnected") {
this.stream.setToken(token);
} else {
this._setupStream();
}
this._setupEventStream(token);
/*
* Presence has nothing to do with incoming capabilities anymore so revisit this when
* presence spec is established.
* Plus this logic is probably wrong for restarting/stoping presenceClient
var tokenIncomingObject = util.objectize(token).scope["client:incoming"];
if (tokenIncomingObject) {
var clientName = tokenIncomingObject.params.clientName;
if (this.presenceClient) {
this.presenceClient.clientName = clientName;
this.presenceClient.start();
} else {
this.presenceClient = this.options["presenceFactory"](clientName,
this,
this.stream,
{ autoStart: true});
}
} else {
if (this.presenceClient) {
this.presenceClient.stop();
this.presenceClient.detach();
this.presenceClient = null;
}
}*/
},
registerPresence: function() {
if (!this.token) {
return;
}
// check token, if incoming capable then set mediaPresence capability to true
var tokenIncomingObject = util.objectize(this.token).scope["client:incoming"];
if (tokenIncomingObject) {
this.mediaPresence.audio = true;
// create the eventstream if needed
if (!this.eventStream) {
this._setupEventStream(this.token);
}
}
this._sendPresence();
},
unregisterPresence: function() {
this.mediaPresence.audio = false;
this._sendPresence();
this._disconnectEventStream();
},
presence: function(handler) {
if (!("client:incoming" in util.objectize(this.token).scope)) return;
this.presenceClient.handlers.push(handler);
// resetup eventstream if the # of handlers went from 0->1
if (this.token && this.presenceClient.handlers.length == 1) {
this._setupEventStream(this.token);
}
},
showSettings: function(showCallback) {
showCallback = showCallback || function() {};
Device.dialog.show(showCallback);
var MediaStream = factories.getClass(
"vendor/mediastream/mediastream", "MediaStream");
MediaStream.showSettings();
// IE9: after showing and hiding the dialog once,
// often we end up with a blank permissions dialog
// the next time around. This makes it show back up
Device.dialog.screen.style.width = "200%";
},
connect: function(params) {
if (typeof params == "function") {
return this.addListener("connect", params);
}
params = params || {};
var connection = makeConnection(this, params);
this.connections.push(connection);
if (this.sounds.outgoing()) {
var self = this;
connection.accept(function() {
self.soundcache.play("outgoing");
});
}
connection.accept();
return connection;
},
disconnectAll: function() {
// Create a copy of connections before iterating, because disconnect
// will trigger callbacks which modify the connections list. At the end
// of the iteration, this.connections should be an empty list.
var connections = [].concat(this.connections);
for (var i = 0; i < connections.length; i++) {
connections[i].disconnect();
}
if (this.connections.length > 0) {
this.log("Connections left pending: " + this.connections.length);
}
},
destroy: function() {
if (this.stream) {
this.stream.destroy();
this.stream = null;
}
//backwards compatibility
this._disconnectEventStream();
if (this.swf && this.swf.disconnect) {
this.swf.disconnect();
}
},
disconnect: function(handler) {
this.addListener("disconnect", handler);
},
incoming: function(handler) {
this.addListener("incoming", handler);
},
offline: function(handler) {
this.addListener("offline", handler);
},
ready: function(handler) {
this.addListener("ready", handler);
},
error: function(handler) {
this.addListener("error", handler);
},
status: function() {
return this._status;
},
activeConnection: function() {
// TODO: fix later, for now just pass back first connection
return this.connections[0];
},
_sendPresence: function() {
this.stream.register(this.mediaPresence);
if (this.mediaPresence.audio) {
this._startRegistrationTimer();
} else {
this._stopRegistrationTimer();
}
},
_startRegistrationTimer: function() {
window.clearTimeout(this.regTimer);
var self = this;
this.regTimer = window.setTimeout( function() {
self._sendPresence();
},REG_INTERVAL);
},
_stopRegistrationTimer: function() {
window.clearTimeout(this.regTimer);
},
_setupStream: function() {
var device = this;
this.log("Setting up PStream");
var streamOptions = {
chunder: this.options["host"],
chunderw: this.options["chunderw"],
debug: this.options["debug"],
secureSignaling: this.options["secureSignaling"]
};
this.stream = this.options["pStreamFactory"](this.token, streamOptions);
this.stream.addListener("connected", function() {
device._sendPresence();
});
this.stream.addListener("ready", function() {
device.log("Stream is ready");
if (device._status == "offline") device._status = "ready";
device.fire("ready", device);
});
this.stream.addListener("offline", function() {
device.log("Stream is offline");
device._status = "offline";
device.fire("offline", device);
});
this.stream.addListener("error", function(payload) {
var error = payload.error;
if (error) {
if (payload.callsid) {
error.connection = device._findConnection(payload.callsid);
}
device.log("Received error: ",error);
device.fire("error", error);
}
});
this.stream.addListener("invite", function(payload) {
if (device._status == "busy") {
device.log("Device busy; ignoring incoming invite");
return;
}
if (!payload["callsid"] || !payload["sdp"]) {
device.fire("error", { message: "Malformed invite from gateway" });
return;
}
var connection = makeConnection(device, {}, { offerSdp: payload["sdp"] });
connection.parameters = payload["parameters"] || {};
connection.parameters["CallSid"] = connection.parameters["CallSid"] || payload["callsid"];
device.connections.push(connection);
device.fire("incoming", connection);
});
},
_setupEventStream: function(token) {
/*
* eventstream for presence backwards compatibility
*/
this.options["eventStreamFactory"] = this.options["eventStreamFactory"] || factories.EventStream;
this.options["eventsScheme"] = this.options["eventsScheme"] || "wss";
this.options["eventsHost"] = this.options["eventsHost"] || "matrix.twilio.com";
var features = [];
var url = null;
if ("client:incoming" in util.objectize(token).scope) {
features.push("publishPresence");
if (this.presenceClient && this.presenceClient.handlers.length > 0) {
features.push("presenceEvents");
}
var makeUrl = function (token, scheme, host, features) {
features = features || [];
var fparams = [];
for (var i = 0; i < features.length; i++) {
fparams.push("feature=" + features[i]);
}
var qparams = [ "AccessToken=" + token ].concat(fparams);
return [
scheme + "://" + host, "2012-02-09",
util.objectize(token).iss,
util.objectize(token).scope["client:incoming"].params.clientName
].join("/") + "?" + qparams.join("&");
}
url = makeUrl(token,
this.options["eventsScheme"],
this.options["eventsHost"],
features);
}
var device = this;
if (!url || !this.mediaPresence.audio) {
this._disconnectEventStream();
return;
}
if (this.eventStream) {
this.eventStream.options["url"] = url;
this.eventStream.reconnect(token);
return;
}
this.log("Registering to eventStream with url: " + url);
var eventStreamOptions = {
logPrefix: "[Matrix]",
debug: this.options["debug"],
url: url
};
this.eventStream = this.options["eventStreamFactory"](token, eventStreamOptions);
this.eventStream.addListener("error", function(error) {
device.log("Received error: ",error);
device.fire("error", error);
})
var clientName = util.objectize(token).scope["client:incoming"].params.clientName;
this.presenceClient = this.options["presenceFactory"](clientName,
this,
this.eventStream,
{ autoStart: true});
},
_disconnectEventStream: function() {
if (this.eventStream) {
this.eventStream.destroy();
if (this.presenceClient) {
this.presenceClient.detach(this.eventStream);
}
this.eventStream = null;
}
this.log("Destroyed eventstream.");
},
_removeConnection: function(connection) {
for (var i = this.connections.length - 1; i >= 0; i--) {
if (connection == this.connections[i]) {
this.connections.splice(i, 1);
}
}
},
_findConnection: function(callsid) {
for (var i = 0; i < this.connections.length; i++) {
var conn = this.connections[i];
if (conn.parameters.CallSid == callsid || conn.outboundConnectionId == callsid) {
return conn;
}
}
}
};
function singletonwrapper(cls) {
var afterSetup = [];
var tasks = [];
var queue = function(task) {
if (cls.instance) return task();
tasks.push(task);
};
var defaultErrorHandler = function(error) {
var err_msg = (error.code ? error.code + ": " : "") + error.message;
if (cls.instance) {
cls.instance.log(err_msg);
}
throw new util.Exception(err_msg);
};
var members = /** @lends Twilio.Device */ {
/**
* Instance of Twilio.Device.
*
* @type Twilio.Device
*/
instance: null,
/**
* @param {string} token
* @param {object} [options]
* @return {Twilio.Device}
*/
setup: function(token, options) {
if (cls.instance) {
cls.instance.log("Found existing Device; using new token but ignoring options");
cls.instance.token = token;
cls.instance.register(token);
} else {
cls.instance = new Device(token, options);
cls.sounds = cls.instance.sounds;
for (var i = 0; i < tasks.length; i++) {
tasks[i]();
}
cls.error(defaultErrorHandler);
}
for (var i = 0; i < afterSetup.length; i++) {
afterSetup[i](token, options);
}
afterSetup = [];
return cls;
},
/**
* Connects to Twilio.
*
* @param {object} parameters
* @return {Twilio.Connection}
*/
connect: function(parameters) {
if (typeof parameters == "function") {
queue(function() {
cls.instance.addListener("connect", parameters);
});
return;
}
if (!cls.instance) {
throw new util.Exception("Run Twilio.Device.setup()");
}
if (cls.instance.connections.length > 0) {
cls.instance.fire("error",
{ message: "A connection is currently active" });
return;
}
return cls.instance.connect(parameters);
},
/**
* @return {Twilio.Device}
*/
disconnectAll: function() {
queue(function() {
cls.instance.disconnectAll();
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
disconnect: function(handler) {
queue(function() {
cls.instance.addListener("disconnect", handler);
});
return cls;
},
status: function() {
return cls.instance._status;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
ready: function(handler) {
queue(function() {
cls.instance.addListener("ready", handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
error: function(handler) {
queue(function() {
if (handler != defaultErrorHandler) {
cls.instance.removeListener("error", defaultErrorHandler);
}
cls.instance.addListener("error", handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
presence: function(handler) {
queue(function() {
cls.instance.presence(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
offline: function(handler) {
queue(function() {
cls.instance.addListener("offline", handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.Device}
*/
incoming: function(handler) {
queue(function() {
cls.instance.addListener("incoming", handler);
});
return cls;
},
/**
* @return {Twilio.Device}
*/
destroy: function() {
if (cls.instance) cls.instance.destroy();
return cls;
},
/**
* @return {Twilio.Device}
*/
cancel: function(handler) {
queue(function() {
cls.instance.addListener("cancel", handler);
});
return cls;
},
showPermissionsDialog: function() {
if (!cls.instance) {
throw new util.Exception("Run Twilio.Device.setup()");
}
cls.instance.showSettings();
},
activeConnection: function() {
if (!cls.instance) {
return null;
}
return cls.instance.activeConnection();
},
__afterSetup: function(callback) {
afterSetup.push(callback);
}
};
for (var method in members) {
cls[method] = members[method];
}
return cls;
}
Device = singletonwrapper(Device);
Device.dialog = factories.Dialog();
exports.Device = Device; });
files["lib/twilio/dialog.js"] = (function(require, exports){ var Dialog = (function() {
function Dialog() {
var screen = document.createElement("div");
var dialog = document.createElement("div");
var close = document.createElement("button");
screen.style.position = "fixed";
screen.style.zIndex = "99999";
screen.style.top = "0";
screen.style.left = "0";
screen.style.width = "1px";
screen.style.height = "1px";
screen.style.overflow = "hidden";
screen.style.visibility = "hidden";
dialog.style.margin = "10% auto 0";
dialog.style.width = "215px";
dialog.style.borderRadius = "8px";
dialog.style.backgroundColor = "#f8f8f8";
dialog.style.border = "8px solid rgb(160, 160, 160)";
var self = this;
var hideFn = function() {
self.hide();
if (self.closeCb) {
self.closeCb.call();
}
};
close.appendChild(document.createTextNode("Close"));
if (window.addEventListener) {
close.addEventListener("click", hideFn, false);
} else {
close.attachEvent("onclick", hideFn);
}
screen.appendChild(dialog);
dialog.appendChild(close);
this.screen = screen;
this.dialog = dialog;
this.close = close;
this.container = null;
this.inserted = false;
this.embed = function() { };
if (document.body) {
document.body.appendChild(screen);
self.inserted = true;
} else {
var self = this;
var fn = function() {
document.body.appendChild(screen);
self.inserted = true;
self.embed();
};
if (window.addEventListener) {
window.addEventListener("load", fn, false);
} else {
window.attachEvent("onload", fn);
}
}
}
Dialog.prototype = {
/**
* Inserts a DOM element into the dialog.
*
* @param {HTMLElement} container Content for the dialog
*/
insert: function(container, embed) {
if (this.container) {
if (this.container == container) {
return;
}
this.dialog.removeChild(this.container);
}
this.container = container;
this.dialog.insertBefore(container, this.close);
this.embed = embed || this.embed();
if (this.inserted) {
this.embed();
}
},
/**
* Shows the dialog.
*/
show: function(closeCb) {
if (closeCb)
this.close.style.display = "";
else
this.close.style.display = "none";
this.closeCb = closeCb;
this.screen.style.width = "100%";
this.screen.style.height = "auto";
this.screen.style.visibility = "visible";
// Firefox uses subpixel units for positioning which is incompatible
// with Flash components: they are visible but unresponsive to user
// inputs. The workaround is to add a subpixel left margin to the flash
// component's container. This is a known bug:
// http://bugs.adobe.com/jira/browse/FP-4656.
var dw = this.dialog.style.width.replace("px", "") | 0,
ww = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
wh = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
this.dialog.style.marginLeft = (((ww - dw) / 2) | 0) + "px";
this.dialog.style.marginTop = ((wh * .1) | 0) + "px";
},
/**
* Hides the dialog.
*/
hide: function() {
this.screen.style.width = "1px";
this.screen.style.height = "1px";
this.screen.style.visibility = "hidden";
}
};
return Dialog;
})();
exports.Dialog = Dialog; });
files["lib/twilio/eventemitter.js"] = (function(require, exports){ /**
* Implementation of indexOf which uses the native indexOf if available.
*
* @param {string} needle
* @param {array} haystack
* @param {boolean} _js Always use the Javascript implementation
*
* @return {number} The position of the needle
*/
function _indexOf(needle, haystack, _js) {
_js = _js || false;
haystack = haystack || [];
if (haystack.length == 0) {
return -1;
}
if (!_js && haystack.indexOf) {
return haystack.indexOf(needle);
}
else {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
return i;
}
}
return -1;
}
}
/**
* A collection of methods that can be mixed in to an object to assist in
* maintaining a mapping of events to event listeners, as well as the ability
* to invoke a class of listeners.
*
* @class
* @inner
* @name EventEmitter
*/
var EventEmitter = {
/** @lends EventEmitter# */
/**
* Registers a callback for an event type.
*
* @param {string} type The class of event
* @param {function} callback A function to call when its event is triggered
*
* @return {EventEmitter}
*/
addListener: function(type, callback) {
if (typeof callback != "function") {
var util = require("twilio/util");
throw new util.Exception(
"Listener must be a function, not " + typeof callback);
}
this.handlers[type] = this.handlers[type] || [];
this.handlers[type].push(callback);
this.log("Registered listener for event type \"" + type + "\"");
return this;
},
/**
* Add a listener that gets called only one time
*/
addOneTimeListener: function(type, callback) {
if (typeof callback != "function") {
var util = require("twilio/util");
throw new util.Exception(
"Listener must be a function, not " + typeof callback);
}
this.oneTimeHandlers[type] = this.oneTimeHandlers[type] || [];
this.oneTimeHandlers[type].push(callback);
this.log("Registered one time listener for event type \"" + type + "\"");
return this;
},
/**
* Is the callback registered with the event type?
*
* @param {string} type The class of event
* @param {function} callback The associated callback
*
* @return {bool} Return true if the callback is associated with the event
*/
hasListener: function(type, callback) {
return _indexOf(callback, this.handlers[type]) != -1;
},
/**
* Removes a callback for an event type.
*
* @param {string} type The class of event
* @param {function} callback The associated callback
*
* @return {EventEmitter}
*/
removeListener: function(type, callback) {
var index = _indexOf(callback, this.handlers[type]);
if (index > -1) {
this.handlers[type].splice(index, 1);
this.log("Removed listener for event type \"" + type + "\"");
}
// remove any instance from one time handlers as well
index = _indexOf(callback, this.oneTimeHandlers[type]);
if (index > -1) {
this.oneTimeHandlers[type].splice(index, 1);
this.log("Removed one time listener for event type \"" + type + "\"");
}
return this;
},
/**
* Invokes all the registered callbacks for an event type.
*
* @param {string} type The class of event
* @param *args Any number of arguments to pass to the callback
*/
fire: function(/* type, *args */) {
var args = Array.prototype.slice.call(arguments, 0);
var type = args.shift();
var handlers = this.handlers[type] || [];
this.log("Invoke listeners for event type \"" + type + "\"");
for (var index = 0; index < handlers.length; index++) {
handlers[index].apply(this, args);
}
// run the one time handlers
handlers = this.oneTimeHandlers[type] || [];
this.oneTimeHandlers[type] = null;
for (var index = 0; index < handlers.length; index++) {
handlers[index].apply(this, args);
}
}
}
/**
* Utility to mix-in the EventEmitter methods to an object.
*
* @exports mixinEventEmitter as Twilio.mixinEventEmitter
* @memberOf Twilio
*
* @param {object} object The object to attach EventEmitter properties to
*
* @return {object} Return the object passed in
*/
function mixinEventEmitter(object) {
object.handlers = object.handlers || {};
object.oneTimeHandlers = object.oneTimeHandlers || {};
object.log = object.log || function() {};
for (var name in EventEmitter) {
if (object[name]) {
continue;
}
object[name] = (function(this_, name) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
return EventEmitter[name].apply(this_, args)
};
})(object, name);
}
}
exports.mixinEventEmitter = mixinEventEmitter; });
files["lib/twilio/eventstream.js"] = (function(require, exports){ var eventEmitter = require("twilio/eventemitter");
var log = require("twilio/log");
var util = require("twilio/util");
var factories = require("twilio/factories");
var Heartbeat = require("twilio/heartbeat").Heartbeat;
function initEvent(object, type) {
if (!object.fire || !object.addListener) {
throw new util.Exception("Object is not event savvy");
}
return function() {
var args = Array.prototype.slice.call(arguments, 0);
if (typeof args[0] == "function") {
return object.addListener(type, args[0]);
} else {
args.unshift(type);
return object.fire.apply(object, args);
}
};
};
function trim(str) {
if (typeof str != "string") return "";
return str.trim
? str.trim()
: str.replace(/^\s+|\s+$/g, "");
}
/**
* Splits a concatenation of multiple JSON strings into a list of JSON strings.
*
* @param string json The string of multiple JSON strings
* @param boolean validate If true, thrown an error on invalid syntax
*
* @return array A list of JSON strings
*/
function splitObjects(json, validate) {
var trimmed = trim(json);
return trimmed.length == 0 ? [] : trimmed.split("\n");
}
/**
* Constructor for EventStream objects.
*
* @exports EventStream as Twilio.EventStream
* @memberOf Twilio
* @borrows EventEmitter#addListener as #addListener
* @borrows EventEmitter#removeListener as #removeListener
* @borrows EventEmitter#fire as #fire
* @borrows EventEmitter#hasListener as #hasListener
* @constructor
* @param {string} token The Twilio capabilities JWT
* @param {object} [options]
* @config {string} [options.swfLocation] Location of WebSocket.swf
* @config {WebSocket} [options.socket] Mock socket
* @config {boolean} [options.reconnect=true] Try to reconnect closed connections
* @config {int} [options.flashTimeout=5000] Time to wait for Flash to initialize
* @config {boolean} [options.debug=false] Enable debugging
*/
function EventStream(token, options) {
if (!(this instanceof EventStream)) {
return new EventStream(token, options);
}
var defaults = {
logPrefix: "[EventStream]",
scheme: "wss",
host: "stream.twilio.com",
reconnect: true,
url: null,
flashTimeout: 5000,
filters: {},
debug: false
};
options = options || {};
for (var prop in defaults) {
if (prop in options) continue;
options[prop] = defaults[prop];
}
this.options = options;
this.token = token || "";
this.handlers = {};
this.status = "offline";
log.mixinLog(this, this.options["logPrefix"]);
this.log.enabled = this.options["debug"];
/**
* A utility to help detect network connection loss.
*
* @type Twilio.Heartbeat
*/
this.heartbeat = new Heartbeat({ "interval": 15 });
eventEmitter.mixinEventEmitter(this);
this._connect();
var events = [
"incoming",
"ready",
"offline",
"sms",
"call",
"twiml",
"error"
];
for (var i = 0; i < events.length; i++) {
this[events[i]] = initEvent(this, events[i]);
}
// The event type publish is an alias for twiml. See the comment in the
// onmessage handler for the websocket for more details.
var self = this;
this.addListener("publish", function(obj) {
self.fire("twiml", obj);
});
}
EventStream.initializingFlash = false;
EventStream.initializeFlash = function(stream) {
if (EventStream.initializingFlash) return;
EventStream.initializingFlash = true;
if (window.WebSocket && window.WebSocket.__initialize) {
WEB_SOCKET_SWF_LOCATION = stream.options["swfLocation"];
window.WebSocket.__initialize();
// We want to inform the user if there is a failure loading
// WebSocket.swf. swfobject.embedSWF has a success/failure callback,
// but it returns true even on a 404 response for the requested .swf.
// http://code.google.com/p/swfobject/issues/detail?id=126#c13.
stream.log("Waiting " + stream.options["flashTimeout"] +
"ms for flash to initialize");
window.setTimeout(function() {
if (!window.WebSocket.__flash) {
stream.log("WebSocket did not initialize");
}
}, stream.options["flashTimeout"]);
}
};
/**
* @return {string}
*/
EventStream.toString = function() {
return "[Twilio.EventStream class]";
};
EventStream.prototype = {
toString: function() {
return "[Twilio.EventStream instance]";
},
destroy: function() {
this.socket.close();
this.options["reconnect"] = false;
return this;
},
publish: function (payload, channel) {
try {
this.socket.send(JSON.stringify({
"rt.message": "publish",
"rt.subchannel": channel,
"rt.payload": payload
}));
}
catch (error) {
this.log("Error while publishing to eventstream. Reconnecting socket.");
if (this.socket) {
this.socket.close();
}
this._tryReconnect();
}
},
_cleanupSocket: function(socket) {
if (socket) {
var noop = function() {};
socket.onopen = function() { socket.close(); };
socket.onmessage = noop;
socket.onerror = noop;
socket.onclose = noop;
if (socket.readyState < 2) {
socket.close();
}
}
},
_connect: function(attempted) {
var self = this;
var url = this._extractUrl();
var attempt = ++attempted || 1;
if (!url) {
this.log("Nothing to do");
return;
}
var oldSocket = this.socket;
this.log("Attempting to connect to " + url + "...");
// _tryReconnect calls this method expecting a new WebSocket each time.
try {
this.socket = factories.WebSocket(url);
} catch (e) {
this.log("Connection to " + url + " failed: " + (e.message || ""));
return;
}
this.socket.onopen = function() {
self.log("Socket opened... sending ready signal");
self._cleanupSocket(oldSocket);
self.socket.send(JSON.stringify({
"rt.message": "listen",
"rt.token": self.token
}));
};
this.socket.onerror = function(me) {
if (me.data) {
self.fire("error", {
message: me.data.message || "",
code: me.data.code || ""
});
} else {
self.log("Received message event:", me);
}
};
this.socket.onmessage = function(message) {
self.heartbeat.beat();
// Return if just keepalive newline
if (message.data == "\n") return;
// Message might contain more than one JSON object.
var objects = splitObjects(message.data);
for (var i = 0; i < objects.length; i++) {
var obj = JSON.parse(objects[i]);
if (obj["rt.message"] == "ready") {
if (self.status != "ready") {
self.status = "ready";
self.fire("ready", self);
}
} else {
// Hurl sends "publish" EventTypes, but our API calls them
// "twiml" events. We want to have all "publish" events
// fire the handlers registered for "twiml". In the
// EventStream constructor, we add a listener for
// "publish", and use that to fire a "twiml" event.
var event_type = obj["rt.message"] || obj["EventType"];
if (event_type == "error") {
var errMessage = obj["message"] || "";
self.log("Connection to " + url + " failed: " + errMessage);
if (/^4/.test(obj["code"])) {
self.options["reconnect"] = false;
} else {
//Attempt to reconnect up to 5 times using exponential random backoff
if (attempt < 5) {
var minBackoff = 30;
var backoffRange = Math.pow(2,attempt)*50;
var backoff = minBackoff + Math.round(Math.random()*backoffRange);
setTimeout(function() {
if (self.socket) {
self.socket.close();
}
self._connect(attempt);
}, backoff);
} else {
self.fire("error", {
message: "Connection to Twilio failed: " + errMessage,
code: obj["code"] || ""
});
}
}
}
if (event_type != "incoming") {
self.fire("incoming", obj);
self.fire(event_type, obj);
} else {
self.fire(event_type, obj);
}
}
}
};
this.socket.onclose = function() {
self._cleanupSocket(oldSocket);
if (self.status != "offline") {
self.log("Gone offline");
self.status = "offline";
self.fire("offline", self);
}
};
this.heartbeat.onsleep = function() {
self.log("Connection heartbeat timed out.");
if (self.socket) {
self.socket.close();
}
self._tryReconnect(5000);
};
},
reconnect: function(token) {
if (this.socket) {
if (this.socket.readyState == 0) {
socket = this.socket;
socket.onopen = function () { socket.close(); }
} else {
this.socket.close();
}
}
this.token = token;
this.options["reconnect"] = true;
this._tryReconnect();
},
_extractUrl: function() {
if (this.options["url"]) {
return this.options["url"];
}
var scopes = util.objectize(this.token).scope;
if (!("stream:subscribe" in scopes)) {
return null;
}
var scope = scopes["stream:subscribe"];
var path = (scope.params && scope.params["path"])
? scope.params["path"]
: "/";
var filters = this.options["filters"];
filters["AccessToken"] = this.token;
return this.options["scheme"] + "://" + this.options["host"]
+ path + "?" + util.urlencode(filters, true);
},
_tryReconnect: function(delay) {
var now = (new Date().getTime() / 1000) | 0;
if (!this.options["reconnect"]
|| this._extractUrl() == null
) {
return;
}
delay = delay || 5000;
this._connect();
var callAgain = (function(self) {
return function() {
self._tryReconnect(delay * 2);
};
})(this);
var checkReady = (function(self) {
return function() {
switch(self.socket.readyState) {
case 0:
window.setTimeout(checkReady,1000);
break;
case 1:
return;
case 2:
case 3:
default:
window.setTimeout(callAgain, delay);
break;
}
};
})(this);
window.setTimeout(checkReady, 5000);
}
};
function singletonwrapper(cls) {
var tasks = [];
var queue = function(task) {
if (cls.instance) return task();
tasks.push(task);
};
var members = /** @lends Twilio.EventStream */ {
/**
* Instance of EventStream.
*
* @type Twilio.EventStream
*/
instance: null,
/**
* Either "offline" or "ready
* @type string
*/
status: "offline",
/**
* @param {string} token
* @param {object} options
* @return {Twilio.EventStream}
*/
setup: function(token, options) {
cls.instance = new cls(token, options);
for (var i = 0; i < tasks.length; i++) {
tasks[i]();
}
cls.ready(function() { cls.status = "ready"; });
cls.offline(function() { cls.status = "offline"; });
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
incoming: function(handler) {
queue(function() {
cls.instance.incoming(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
ready: function(handler) {
queue(function() {
cls.instance.ready(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
offline: function(handler) {
queue(function() {
cls.instance.offline(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
sms: function(handler) {
queue(function() {
cls.instance.sms(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
call: function(handler) {
queue(function() {
cls.instance.call(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
twiml: function(handler) {
queue(function() {
cls.instance.twiml(handler);
});
return cls;
},
/**
* @param {function} handler
* @return {Twilio.EventStream}
*/
error: function(handler) {
queue(function() {
cls.instance.error(handler);
});
return cls;
}
};
for (var name in members) {
cls[name] = members[name];
}
return cls;
}
EventStream = singletonwrapper(EventStream);
exports.EventStream = EventStream; });
files["lib/twilio/factories.js"] = (function(require, exports){ var classes = {};
var callbacks = {};
function create(cls, args) {
if (!cls.apply) return create_(cls, args);
var ctor = function() {};
ctor.prototype = cls.prototype;
var args = Array.prototype.slice.call(args, 0),
child = new ctor;
// Catches "TypeError: DOM object constructor cannot be called as a
// function" in Chrome 29.
try {
var result = cls.apply(child, args);
} catch (e) {
return create_(cls, args);
}
return typeof result === "object" ? result : child;
}
function create_(cls, args) {
var len = args.length;
if (len === 0) return new cls();
else if (len === 1) return new cls(args[0]);
else if (len === 2) return new cls(args[0], args[1]);
else if (len === 3) return new cls(args[0], args[1], args[2]);
else if (len === 4) return new cls(args[0], args[1], args[2], args[3]);
}
function makeFactory(path, name, callback) {
var key = path + "/" + name;
if (callback) callbacks[key] = callback;
return function() {
var cls = classes[key] || (
callbacks[key] ? callbacks[key]() : require(path)[name]);
return create(cls, arguments);
};
}
exports.registerClass = function(path, name, cls) {
classes[path + "/" + name] = cls;
}
exports.getClass = function(path, name) {
var key = path + "/" + name;
return classes[key] || (callbacks[key] ? callbacks[key]() : undefined);
}
exports.Presence = makeFactory("twilio/presence", "Presence");
exports.Device = makeFactory("twilio/device", "Device");
exports.Dialog = makeFactory("twilio/dialog", "Dialog");
exports.Connection = makeFactory("twilio/connection", "Connection");
exports.OldConnection = makeFactory("twilio/oldconnection", "Connection");
exports.EventStream = makeFactory("twilio/eventstream", "EventStream");
exports.PStream = makeFactory("twilio/pstream", "PStream");
exports.SoundCache = makeFactory("twilio/soundcache", "SoundCache");
exports.PeerConnection = makeFactory("twilio/rtc", "PeerConnection");
exports.WSTransport = makeFactory("twilio/wstransport", "WSTransport");
exports.MediaStream = makeFactory("vendor/swfobject",
"swfobject", function() { return swfobject });
exports.Sound = makeFactory("vendor/sound/sound",
"Sound", function() { return Twilio.Sound });
exports.WebSocket = makeFactory("vendor/web-socket-js/websocket",
"WebSocket", function() { return WebSocket });
exports.MediaStream = makeFactory("vendor/mediastream/mediastream",
"MediaStream", function() { return Twilio.MediaStream }); });
files["lib/twilio/heartbeat.js"] = (function(require, exports){ /**
* Heartbeat just wants you to call beat()
every once in a while.
*
*
It initializes a countdown timer that expects a call to
* Hearbeat#beat
every n seconds. If beat()
hasn't
* been called for #interval
seconds, it fires a
* onsleep
event and waits. The next call to beat()
* fires onwakeup
and initializes a new timer.
For example:
* * @example * * >>> hb = new Heartbeat({ * ... interval: 10, * ... onsleep: function() { console.log('Gone to sleep...Zzz...'); }, * ... onwakeup: function() { console.log('Awake already!'); }, * ... }); * * >>> hb.beat(); # then wait 10 seconds * Gone to sleep...Zzz... * >>> hb.beat(); * Awake already! * * @exports Heartbeat as Twilio.Heartbeat * @memberOf Twilio * @constructor * @param {object} opts Options for Heartbeat * @config {int} [interval=10] Seconds between each call tobeat
* @config {function} [onsleep] Callback for sleep events
* @config {function} [onwakeup] Callback for wakeup events
*/
function Heartbeat(opts) {
if (!(this instanceof Heartbeat)) return new Heartbeat(opts);
opts = opts || {};
/** @ignore */
var noop = function() { };
var defaults = {
interval: 10,
now: function() { return new Date().getTime() },
repeat: function(f, t) { return setInterval(f, t) },
stop: function(f, t) { return clearInterval(f, t) },
onsleep: noop,
onwakeup: noop
};
for (var prop in defaults) {
if (prop in opts) continue;
opts[prop] = defaults[prop];
}
/**
* Number of seconds with no beat before sleeping.
* @type number
*/
this.interval = opts.interval;
this.lastbeat = 0;
this.pintvl = null;
/**
* Invoked when this object has not received a call to #beat
* for an elapsed period of time greater than #interval
* seconds.
*
* @event
*/
this.onsleep = opts.onsleep;
/**
* Invoked when this object is sleeping and receives a call to
* #beat
.
*
* @event
*/
this.onwakeup = opts.onwakeup;
this.repeat = opts.repeat;
this.stop = opts.stop;
this.now = opts.now;
}
/**
* @return {string}
*/
Heartbeat.toString = function() {
return "[Twilio.Heartbeat class]";
};
Heartbeat.prototype = {
/**
* @return {string}
*/
toString: function() {
return "[Twilio.Heartbeat instance]";
},
/**
* Keeps the instance awake (by resetting the count down); or if asleep,
* wakes it up.
*/
beat: function() {
this.lastbeat = this.now();
if (this.sleeping()) {
if (this.onwakeup) {
this.onwakeup();
}
var self = this;
this.pintvl = this.repeat.call(
null,
function() { self.check() },
this.interval * 1000
);
}
},
/**
* Goes into a sleep state if the time between now and the last heartbeat
* is greater than or equal to the specified interval
.
*/
check: function() {
var timeidle = this.now() - this.lastbeat;
if (!this.sleeping() && timeidle >= this.interval * 1000) {
if (this.onsleep) {
this.onsleep();
}
this.stop.call(null, this.pintvl);
this.pintvl = null;
}
},
/**
* @return {boolean} True if sleeping
*/
sleeping: function() {
return this.pintvl == null;
}
};
exports.Heartbeat = Heartbeat; });
files["lib/twilio/log.js"] = (function(require, exports){ /**
* Bestow logging powers.
*
* @exports mixinLog as Twilio.mixinLog
* @memberOf Twilio
*
* @param {object} object The object to bestow logging powers to
* @param {string} [prefix] Prefix log messages with this
*
* @return {object} Return the object passed in
*/
function mixinLog(object, prefix) {
/**
* Logs a message or object.
*
* There are a few options available for the log mixin. Imagine an object
* foo
with this function mixed in:
var foo = {};
* Twilio.mixinLog(foo);
*
*
*
* To enable or disable the log: foo.log.enabled = true
To modify the prefix: foo.log.prefix = "Hello"
To use a custom callback instead of console.log
:
* foo.log.handler = function() { ... };
Wrapper around the Flash MediaStreamMain object which encapsulates a * single NetConnection and two NetStream objects. The NetStream objects send * and receive media from a Flash Media Server.
* *The MediaStreamMain object exposes utilities to configure the Microphone * and display the security settings dialog.
* * @constructor */ function MediaStream(encrypt, host) { this.__id = MediaStream.__nextId++; /** @ignore */ var noop = function() { }; /** * Invoked when the NetConnection object successfully connects. * * @function * @event */ this.onopen = noop; /** * Invoked when an error is received. * * @function * @event * @param {object} error An error object */ this.onerror = noop; /** * Invoked when a NetConnection object disconnects. * * @function * @event * @param {object} error An error object */ this.onclose = noop; this.onconnected = noop; this._uri = (encrypt ? "rtmps" : "rtmp") + "://" + host + "/chunder"; this.micAttached = false; MediaStream.__instances[this.__id] = this; } MediaStream.prototype = { openHelper: function(next, simplePermissionDialog, noMicLevel, dialogFn, showSettings) { var self = this; MediaStream.__queue(function() { var noMics = MediaStream.getMicrophones().length == noMicLevel; if (noMics) { next("No microphone is available"); return; } else if (simplePermissionDialog) { if (MediaStream.isMicMuted()) { MediaStream.__queueResponseCB( function(accessGranted) { dialogFn.closeDialog(accessGranted); if (accessGranted) { self.exec("startCall"); self.onopen(self); } }); dialogFn.showDialog(); } else { self.onconnected = function() { self.exec("startCall"); self.onopen(self); } } next(); } else { self.onconnected = function() { self.exec("startCall"); self.onopen(self); } if (!MediaStream.isMicMuted()) next(); else { showSettings(function() { if (MediaStream.isMicMuted()) { next("User denied access to microphone.", 31208); } else next(); }); } } }); }, uri: function() { return this._uri; }, /** * Opens a new connection to the Flash Media Server. Takes an arbitrary * list of parameters, the first of which must be the URI of the * application instance. For example: * * @example mediaStream.open("rtmp://localhost/mycoolapp", ...); * @param *args Arguments to pass to NetStream.connect */ open: function() { var self = this; var args = Array.prototype.slice.call(arguments); MediaStream.__queue(function() { MediaStream.__flash.open.apply(MediaStream.__flash, [self.__id].concat(args)); }); }, /** * Wraps #call on the NetConnection. * * @example mediaStrea.exec("doSomething", "arg1"); * @param *args Arguments to pass to NetStream.call */ exec: function() { var self = this; var args = Array.prototype.slice.call(arguments); MediaStream.__queue(function() { MediaStream.__flash.exec.apply(MediaStream.__flash, [self.__id].concat(args)); }); }, /** * Closes the connection. */ close: function() { var self = this; MediaStream.__queue(function() { MediaStream.__flash.close(self.__id); }); }, /** * Begin receiving media. * * @example mediaStream.play("output"); */ play: function() { var self = this; var args = Array.prototype.slice.call(arguments); MediaStream.__queue(function() { MediaStream.__flash.playStream.apply(MediaStream.__flash, [self.__id].concat(args)); }); }, /** * Begin publishing media. * * @example mediaStream.publish("input", "live"); * * @param {string} name An identifier for the stream. * @param {string} type Defaults to "live". */ publish: function(name, type) { var self = this; MediaStream.__queue(function() { MediaStream.__flash.publish(self.__id, name, type); }); }, /** * Attach a Microphone object to the MediaStream. * * @example mediaStream.attachAudio(); * * @param {function} callback for after the audio is attached * @param {int} index The index of a Microphone, or null. */ attachAudio: function(callback, index) { var self = this; MediaStream.__queue(function() { MediaStream.__flash.attachAudio(self.__id, index); self.micAttached = true; if (callback && typeof callback == "function") { callback(); } }); }, /** * Detach Microphone object from the MediaStream, effectively disabling * audio publishing. * * @example mediaStream.detachAudio(); * * @param {function} callback for after the audio is detached * @param {int} index The index of a Microphone, or null. */ detachAudio: function(callback) { var self = this; MediaStream.__queue(function() { MediaStream.__flash.detachAudio(self.__id); self.micAttached = false; if (callback && typeof callback == "function") { callback(); } }); }, isAudioAttached: function() { return this.micAttached; }, /** * Handle events. */ __handleEvent: function(event) { switch (event.type) { case "gatewayError": case "securityError": case "asyncError": case "ioError": this.onerror.call(this, event); break; case "netStatus": this.__handleNetStatus(event); break; case "callsid": if (typeof this.onCallSid == "function") { this.onCallSid(event.info.callsid); } break; default: break; } }, __handleNetStatus: function(event) { MediaStream.log("Event info code: " + event.info.code); switch (event.info.code) { case "NetConnection.Connect.Failed": case "NetConnection.Connect.Rejected": MediaStream.log("Connection failed or was rejected"); this.onerror.call(this, event); break; case "NetConnection.Connect.Closed": MediaStream.log("Connection closed"); this.onclose.call(this, event); break; case "NetConnection.Connect.Success": MediaStream.log("Connection established"); this.publish("input","live"); this.attachAudio(); this.onconnected(); break; default: MediaStream.log("Unexpected event: " + event.info.code); break; } } }; function defaultLoader(flash, embedCallback) { if (!document.body) { var callback = function() { defaultLoader(flash, embedCallback); }; try { window.addEventListener("load", callback, false); } catch(e) { window.attachEvent("onload", callback); } return; } var container = document.createElement("div"); container.style.position = "absolute"; container.appendChild(flash); document.body.appendChild(container); embedCallback(); }; var classMethods = { /** * Run a task or queue it if __flash is not ready. * * @param {function} task The task to run. */ __queue: function(task) { if (MediaStream.initialized) { task(); } else { MediaStream.__tasks.push(task); } }, /** * Queue tasks to be called when simplePermissionDialog response is recorded * * @param {function} task The task to run. */ __queueResponseCB: function(cb) { MediaStream.__responseCB.push(cb); }, /** * Embed the Flash object and instantiate the MediaStreamMain object. * * @param {array} options Initialization options. */ initialize: function(options) { if (!swfobject.hasFlashPlayerVersion("10.0.0")) throw new util.Exception("Flash Player >= 10.0.0 is required."); if (MediaStream.__flash) return; options = options || {}; options["swfLocation"] = options["swfLocation"] || "MediaStreamMain.swf"; options["domId"] = options["domId"] || "__connectionFlash__"; options["loader"] = options["loader"] || defaultLoader; if (!document.body) { try { window.addEventListener("load", function() { MediaStream.initialize(options); }, false); } catch(e) { window.attachEvent("onload", function() { MediaStream.initialize(options); }); } return; } var flash = document.createElement("div"); flash.id = options["domId"]; options["loader"](flash, function() { MediaStream.__flash = flash; var flashVars = { }; if ("objectEnc" in options) { flashVars["objectEnc"] = options["objectEnc"]; } // MediaStreamMain uses the value of namespace to invoke methods // declared in Javascript land. Some of the methods it invokes are // __onLog which will end up being accessible via // MediaStream.__onLog. flashVars["namespace"] = NS_MEDIASTREAM ? NS_MEDIASTREAM + ".MediaStream" : "MediaStream"; swfobject.embedSWF( options["swfLocation"], options["domId"], "215", "138", "10.0.0", null, flashVars, { hasPriority: true, allowScriptAccess: "always" }, null, function(e) { MediaStream.log("Embed " + (e.success ? "succeeded" : "failed")); } ); }); }, /** * Sets microphone gain. * * @param {int} value Gain amount (0-100) */ setMicrophoneGain: function(value) { MediaStream.__queue(function() { try { MediaStream.__flash.setMicrophoneGain(value); } catch (e) { MediaStream.log(e); } }); }, /** * Sets the microphone. * * @param {int} index Name of microphone */ setMicrophone: function(index, enhanced) { MediaStream.__queue(function() { MediaStream.__flash.setMicrophone(index, enhanced); }); }, /** * The name of the current microphone. * * @return {int} */ getMicrophone: function() { return MediaStream.initialized ? MediaStream.__flash.getMicrophone() : null; }, /** * A list of available Microphones. * * @return {Array} */ getMicrophones: function() { return MediaStream.initialized ? MediaStream.__flash.getMicrophones() : []; }, /** * Sets echo suppression. * * @param {boolean} enabled Uses echo suppression if true */ setUseEchoSuppression: function(enabled) { MediaStream.__queue(function() { try { MediaStream.__flash.setUseEchoSupression(enabled); } catch (e) { MediaStream.log(e); } }); }, /** * Sets silence level. * * This function sets options for the Flash noise gate. The gate is open * when the amount of sound exceeds the level specified by {level}, and the * gate closes when the amount of sound is under the level threshold for an * elapsed time of {{timeout}} milliseconds. * * @param {int} level Amount of sound required to activate the mic * @param {int} timeout Amount of time to wait before mic deactivates */ setSilenceLevel: function(level, timeout) { MediaStream.__queue(function() { try { MediaStream.__flash.setSilenceLevel(level, timeout); } catch (e) { MediaStream.log(e); } }); }, /** * Displays Flash security settings dialog. */ showSettings: function() { MediaStream.__queue(function() { MediaStream.__flash.showSettings(); }); }, /** * Is the microphone muted? * * @return {boolean|null} Returns null if microphone is unavailable */ isMicMuted: function() { try { return MediaStream.__flash.isMicMuted(); } catch (e) { if (e instanceof TypeError) return; throw e; } }, /** * Log a message to the console. * * @param {string} msg The message to display * @param obj Additional object to include in the log * @param {string} method Defaults to log */ log: function(msg, obj, method) { if (!MediaStream.__debug || !window.console) { return; } method = method || "log"; console[method]("[MediaStream] " + msg); if (typeof obj != "undefined") { console[method](obj); } } }; var flashEventHandlers = { __onFlashInitialized: function() { MediaStream.__flash = document.getElementById(MediaStream.__flash.id); setTimeout(function() { MediaStream.initialized = true; MediaStream.__flash.setDebug(MediaStream.__debug); if (/Mac OS X.*Chrome\/2[34]/.test(navigator.userAgent)) { MediaStream.setMicrophone(-1, false); } else { MediaStream.setMicrophone(-1, true); } var mic = MediaStream.getMicrophone(); MediaStream.log(mic ? "Using " + mic : "No mics available"); MediaStream.setMicrophoneGain(75); for (var i = 0; i < MediaStream.__tasks.length; i++) { MediaStream.__tasks[i].call(); } }, 0); }, __onMediaStreamEvent: function() { setTimeout(function() { var events = MediaStream.__flash.dequeueEvents(); for (var i = 0; i < events.length; i++) { try { MediaStream.log("Received event: " + events[i].type); MediaStream .__instances[events[i].mediaStreamId] .__handleEvent(events[i]); } catch (e) { MediaStream.log( "Error while processing " + events[i].type + ": " + e.message, e, "error"); } } }, 0); }, __onLog: function(le) { if (le.level == "error") { MediaStream.log(le.message, undefined, "error"); } else { MediaStream.log(le.message); } }, __onUserResponse: function(accessGranted) { for (var i = 0; i < MediaStream.__responseCB.length; i++) { MediaStream.__responseCB[i](accessGranted); } MediaStream.__responseCB = []; } }; for (var name in classMethods) { MediaStream[name] = classMethods[name]; } for (var name in flashEventHandlers) { MediaStream[name] = flashEventHandlers[name]; } MediaStream.__nextId = 0; MediaStream.__flash = null; MediaStream.__instances = {}; MediaStream.__tasks = []; MediaStream.__responseCB = []; MediaStream.__debug = false; MediaStream.initialized = false; /** @constant */ MediaStream.AMF0 = 0; /** @constant */ MediaStream.AMF3 = 3; ns.MediaStream = MediaStream; })(typeof NS_MEDIASTREAM != "undefined" ? this[NS_MEDIASTREAM] : this); var a = document.createElement("audio"); var forceFlash = true; try { forceFlash = !(a.canPlayType && (a.canPlayType("audio/mpeg").replace(/no/, "") || a.canPlayType('audio/ogg;codecs="vorbis"').replace(/no/, ""))); } catch(e) { } Twilio.Sound.initialize({ swfLocation: TWILIO_ROOT + "SoundMain.swf" , forceFlash: forceFlash }); // We have to call later because we don't yet have the token from which // to extract the subdomain of the SWF URI. This security measure is to // ensure the end-user has an opportunity to deny access to their // microphone. When we find an alternative (e.g. Flash SharedObject), // we should unwrap and remove __afterSetup functionality. var Device = require("twilio/device").Device; Device.__afterSetup(function(token, options) { var rtc = require("twilio/rtc"); if (!rtc.enabled()) { var decode = require("twilio/util").decode; var factories = require("twilio/factories") var MediaStream = factories.getClass("vendor/mediastream/mediastream", "MediaStream"); var yoink = function(token, root) { if (!token) return root; var urlRe = /^(\w+):\/\/([^/]*)(.*)?/; var matches = root.match(urlRe); if (!matches) return root; var prot = matches[1], host = matches[2], path = matches[3]; if (!/twilio.com$/.test(host.split(":")[0])) return root; var iss = decode(token)["iss"]; if (!iss) return root; return prot + "://" + iss.toLowerCase() + "." + host + path; }; MediaStream.initialize({ objectEnc: MediaStream.AMF0, swfLocation: yoink(token, TWILIO_ROOT) + "MediaStreamMain.swf", loader: function(c, f) { Device.dialog.insert(c, f) } }); } }); var exports = require("twilio"); exports.Sound = Twilio.Sound; exports.MediaStream = Twilio.MediaStream; exports._phpjs_atob = Twilio._phpjs_atob; exports._phpjs_btoa = Twilio._phpjs_btoa; return exports; })());