623 lines
23 KiB
JavaScript
623 lines
23 KiB
JavaScript
function TraceablePeerConnection(ice_config, constraints) {
|
|
var self = this;
|
|
var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
|
|
this.peerconnection = new RTCPeerconnection(ice_config, constraints);
|
|
this.updateLog = [];
|
|
this.stats = {};
|
|
this.statsinterval = null;
|
|
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
|
|
|
/**
|
|
* Array of ssrcs that will be added on next modifySources call.
|
|
* @type {Array}
|
|
*/
|
|
this.addssrc = [];
|
|
/**
|
|
* Array of ssrcs that will be added on next modifySources call.
|
|
* @type {Array}
|
|
*/
|
|
this.removessrc = [];
|
|
/**
|
|
* Pending operation that will be done during modifySources call.
|
|
* Currently 'mute'/'unmute' operations are supported.
|
|
*
|
|
* @type {String}
|
|
*/
|
|
this.pendingop = null;
|
|
|
|
/**
|
|
* Flag indicates that peer connection stream have changed and modifySources should proceed.
|
|
* @type {boolean}
|
|
*/
|
|
this.switchstreams = false;
|
|
|
|
// override as desired
|
|
this.trace = function (what, info) {
|
|
//console.warn('WTRACE', what, info);
|
|
self.updateLog.push({
|
|
time: new Date(),
|
|
type: what,
|
|
value: info || ""
|
|
});
|
|
};
|
|
this.onicecandidate = null;
|
|
this.peerconnection.onicecandidate = function (event) {
|
|
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
};
|
|
this.onaddstream = null;
|
|
this.peerconnection.onaddstream = function (event) {
|
|
self.trace('onaddstream', event.stream.id);
|
|
if (self.onaddstream !== null) {
|
|
self.onaddstream(event);
|
|
}
|
|
};
|
|
this.onremovestream = null;
|
|
this.peerconnection.onremovestream = function (event) {
|
|
self.trace('onremovestream', event.stream.id);
|
|
if (self.onremovestream !== null) {
|
|
self.onremovestream(event);
|
|
}
|
|
};
|
|
this.onsignalingstatechange = null;
|
|
this.peerconnection.onsignalingstatechange = function (event) {
|
|
self.trace('onsignalingstatechange', self.signalingState);
|
|
if (self.onsignalingstatechange !== null) {
|
|
self.onsignalingstatechange(event);
|
|
}
|
|
};
|
|
this.oniceconnectionstatechange = null;
|
|
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
self.trace('oniceconnectionstatechange', self.iceConnectionState);
|
|
if (self.oniceconnectionstatechange !== null) {
|
|
self.oniceconnectionstatechange(event);
|
|
}
|
|
};
|
|
this.onnegotiationneeded = null;
|
|
this.peerconnection.onnegotiationneeded = function (event) {
|
|
self.trace('onnegotiationneeded');
|
|
if (self.onnegotiationneeded !== null) {
|
|
self.onnegotiationneeded(event);
|
|
}
|
|
};
|
|
self.ondatachannel = null;
|
|
this.peerconnection.ondatachannel = function (event) {
|
|
self.trace('ondatachannel', event);
|
|
if (self.ondatachannel !== null) {
|
|
self.ondatachannel(event);
|
|
}
|
|
};
|
|
if (!navigator.mozGetUserMedia && this.maxstats) {
|
|
this.statsinterval = window.setInterval(function() {
|
|
self.peerconnection.getStats(function(stats) {
|
|
var results = stats.result();
|
|
for (var i = 0; i < results.length; ++i) {
|
|
//console.log(results[i].type, results[i].id, results[i].names())
|
|
var now = new Date();
|
|
results[i].names().forEach(function (name) {
|
|
var id = results[i].id + '-' + name;
|
|
if (!self.stats[id]) {
|
|
self.stats[id] = {
|
|
startTime: now,
|
|
endTime: now,
|
|
values: [],
|
|
times: []
|
|
};
|
|
}
|
|
self.stats[id].values.push(results[i].stat(name));
|
|
self.stats[id].times.push(now.getTime());
|
|
if (self.stats[id].values.length > self.maxstats) {
|
|
self.stats[id].values.shift();
|
|
self.stats[id].times.shift();
|
|
}
|
|
self.stats[id].endTime = now;
|
|
});
|
|
}
|
|
});
|
|
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
dumpSDP = function(description) {
|
|
return 'type: ' + description.type + '\r\n' + description.sdp;
|
|
}
|
|
|
|
if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
|
|
TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
|
|
TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
|
|
TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { return this.peerconnection.localDescription; });
|
|
TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { return this.peerconnection.remoteDescription; });
|
|
}
|
|
|
|
TraceablePeerConnection.prototype.addStream = function (stream) {
|
|
this.trace('addStream', stream.id);
|
|
this.peerconnection.addStream(stream);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.removeStream = function (stream) {
|
|
this.trace('removeStream', stream.id);
|
|
this.peerconnection.removeStream(stream);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
|
this.trace('createDataChannel', label, opts);
|
|
return this.peerconnection.createDataChannel(label, opts);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('setLocalDescription', dumpSDP(description));
|
|
this.peerconnection.setLocalDescription(description,
|
|
function () {
|
|
self.trace('setLocalDescriptionOnSuccess');
|
|
successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('setLocalDescriptionOnFailure', err);
|
|
failureCallback(err);
|
|
}
|
|
);
|
|
/*
|
|
if (this.statsinterval === null && this.maxstats > 0) {
|
|
// start gathering stats
|
|
}
|
|
*/
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('setRemoteDescription', dumpSDP(description));
|
|
this.peerconnection.setRemoteDescription(description,
|
|
function () {
|
|
self.trace('setRemoteDescriptionOnSuccess');
|
|
successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('setRemoteDescriptionOnFailure', err);
|
|
failureCallback(err);
|
|
}
|
|
);
|
|
/*
|
|
if (this.statsinterval === null && this.maxstats > 0) {
|
|
// start gathering stats
|
|
}
|
|
*/
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.hardMuteVideo = function (muted) {
|
|
this.pendingop = muted ? 'mute' : 'unmute';
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) {
|
|
if (!this.addssrc[channel]) {
|
|
this.addssrc[channel] = '';
|
|
}
|
|
this.addssrc[channel] += ssrcLines;
|
|
}
|
|
|
|
TraceablePeerConnection.prototype.addSource = function (elem) {
|
|
console.log('addssrc', new Date().getTime());
|
|
console.log('ice', this.iceConnectionState);
|
|
var sdp = new SDP(this.remoteDescription.sdp);
|
|
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
|
|
|
var self = this;
|
|
$(elem).each(function (idx, content) {
|
|
var name = $(content).attr('name');
|
|
var lines = '';
|
|
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
tmp.each(function () {
|
|
var ssrc = $(this).attr('ssrc');
|
|
if(mySdp.containsSSRC(ssrc)){
|
|
/**
|
|
* This happens when multiple participants change their streams at the same time and
|
|
* ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
|
|
* addssrc are scheduled for update IQ. See
|
|
*/
|
|
console.warn("Got add stream request for my own ssrc: "+ssrc);
|
|
return;
|
|
}
|
|
$(this).find('>parameter').each(function () {
|
|
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
|
if ($(this).attr('value') && $(this).attr('value').length)
|
|
lines += ':' + $(this).attr('value');
|
|
lines += '\r\n';
|
|
});
|
|
});
|
|
sdp.media.forEach(function(media, idx) {
|
|
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
|
return;
|
|
sdp.media[idx] += lines;
|
|
self.enqueueAddSsrc(idx, lines);
|
|
});
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
});
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) {
|
|
if (!this.removessrc[channel]){
|
|
this.removessrc[channel] = '';
|
|
}
|
|
this.removessrc[channel] += ssrcLines;
|
|
}
|
|
|
|
TraceablePeerConnection.prototype.removeSource = function (elem) {
|
|
console.log('removessrc', new Date().getTime());
|
|
console.log('ice', this.iceConnectionState);
|
|
var sdp = new SDP(this.remoteDescription.sdp);
|
|
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
|
|
|
var self = this;
|
|
$(elem).each(function (idx, content) {
|
|
var name = $(content).attr('name');
|
|
var lines = '';
|
|
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
tmp.each(function () {
|
|
var ssrc = $(this).attr('ssrc');
|
|
// This should never happen, but can be useful for bug detection
|
|
if(mySdp.containsSSRC(ssrc)){
|
|
console.error("Got remove stream request for my own ssrc: "+ssrc);
|
|
return;
|
|
}
|
|
$(this).find('>parameter').each(function () {
|
|
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
|
if ($(this).attr('value') && $(this).attr('value').length)
|
|
lines += ':' + $(this).attr('value');
|
|
lines += '\r\n';
|
|
});
|
|
});
|
|
sdp.media.forEach(function(media, idx) {
|
|
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
|
return;
|
|
sdp.media[idx] += lines;
|
|
self.enqueueRemoveSsrc(idx, lines);
|
|
});
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
});
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.modifySources = function(successCallback) {
|
|
var self = this;
|
|
if (this.signalingState == 'closed') return;
|
|
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
|
|
// There is nothing to do since scheduled job might have been executed by another succeeding call
|
|
if(successCallback){
|
|
successCallback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// FIXME: this is a big hack
|
|
// https://code.google.com/p/webrtc/issues/detail?id=2688
|
|
if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) {
|
|
console.warn('modifySources not yet', this.signalingState, this.iceConnectionState);
|
|
this.wait = true;
|
|
window.setTimeout(function() { self.modifySources(successCallback); }, 250);
|
|
return;
|
|
}
|
|
if (this.wait) {
|
|
window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
|
|
this.wait = false;
|
|
return;
|
|
}
|
|
|
|
// Reset switch streams flag
|
|
this.switchstreams = false;
|
|
|
|
var sdp = new SDP(this.remoteDescription.sdp);
|
|
|
|
// add sources
|
|
this.addssrc.forEach(function(lines, idx) {
|
|
sdp.media[idx] += lines;
|
|
});
|
|
this.addssrc = [];
|
|
|
|
// remove sources
|
|
this.removessrc.forEach(function(lines, idx) {
|
|
lines = lines.split('\r\n');
|
|
lines.pop(); // remove empty last element;
|
|
lines.forEach(function(line) {
|
|
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
|
|
});
|
|
});
|
|
this.removessrc = [];
|
|
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
|
|
function() {
|
|
|
|
if(self.signalingState == 'closed') {
|
|
console.error("createAnswer attempt on closed state");
|
|
return;
|
|
}
|
|
|
|
self.createAnswer(
|
|
function(modifiedAnswer) {
|
|
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
|
|
if (self.pendingop !== null) {
|
|
var sdp = new SDP(modifiedAnswer.sdp);
|
|
if (sdp.media.length > 1) {
|
|
switch(self.pendingop) {
|
|
case 'mute':
|
|
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
|
|
break;
|
|
case 'unmute':
|
|
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
|
break;
|
|
}
|
|
sdp.raw = sdp.session + sdp.media.join('');
|
|
modifiedAnswer.sdp = sdp.raw;
|
|
}
|
|
self.pendingop = null;
|
|
}
|
|
|
|
// FIXME: pushing down an answer while ice connection state
|
|
// is still checking is bad...
|
|
//console.log(self.peerconnection.iceConnectionState);
|
|
|
|
// trying to work around another chrome bug
|
|
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
|
|
self.setLocalDescription(modifiedAnswer,
|
|
function() {
|
|
//console.log('modified setLocalDescription ok');
|
|
if(successCallback){
|
|
successCallback();
|
|
}
|
|
},
|
|
function(error) {
|
|
console.error('modified setLocalDescription failed', error);
|
|
}
|
|
);
|
|
},
|
|
function(error) {
|
|
console.error('modified answer failed', error);
|
|
}
|
|
);
|
|
},
|
|
function(error) {
|
|
console.error('modify failed', error);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.close = function () {
|
|
this.trace('stop');
|
|
if (this.statsinterval !== null) {
|
|
window.clearInterval(this.statsinterval);
|
|
this.statsinterval = null;
|
|
}
|
|
this.peerconnection.close();
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
|
|
var self = this;
|
|
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
|
|
this.peerconnection.createOffer(
|
|
function (offer) {
|
|
self.trace('createOfferOnSuccess', dumpSDP(offer));
|
|
successCallback(offer);
|
|
},
|
|
function(err) {
|
|
self.trace('createOfferOnFailure', err);
|
|
failureCallback(err);
|
|
},
|
|
constraints
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
|
|
var self = this;
|
|
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
|
|
this.peerconnection.createAnswer(
|
|
function (answer) {
|
|
self.trace('createAnswerOnSuccess', dumpSDP(answer));
|
|
successCallback(answer);
|
|
},
|
|
function(err) {
|
|
self.trace('createAnswerOnFailure', err);
|
|
failureCallback(err);
|
|
},
|
|
constraints
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
|
|
this.peerconnection.addIceCandidate(candidate);
|
|
/* maybe later
|
|
this.peerconnection.addIceCandidate(candidate,
|
|
function () {
|
|
self.trace('addIceCandidateOnSuccess');
|
|
successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('addIceCandidateOnFailure', err);
|
|
failureCallback(err);
|
|
}
|
|
);
|
|
*/
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
|
|
if (navigator.mozGetUserMedia) {
|
|
// ignore for now...
|
|
} else {
|
|
this.peerconnection.getStats(callback);
|
|
}
|
|
};
|
|
|
|
// mozilla chrome compat layer -- very similar to adapter.js
|
|
function setupRTC() {
|
|
var RTC = null;
|
|
if (navigator.mozGetUserMedia) {
|
|
console.log('This appears to be Firefox');
|
|
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
|
if (version >= 22) {
|
|
RTC = {
|
|
peerconnection: mozRTCPeerConnection,
|
|
browser: 'firefox',
|
|
getUserMedia: navigator.mozGetUserMedia.bind(navigator),
|
|
attachMediaStream: function (element, stream) {
|
|
element[0].mozSrcObject = stream;
|
|
element[0].play();
|
|
},
|
|
pc_constraints: {}
|
|
};
|
|
if (!MediaStream.prototype.getVideoTracks)
|
|
MediaStream.prototype.getVideoTracks = function () { return []; };
|
|
if (!MediaStream.prototype.getAudioTracks)
|
|
MediaStream.prototype.getAudioTracks = function () { return []; };
|
|
RTCSessionDescription = mozRTCSessionDescription;
|
|
RTCIceCandidate = mozRTCIceCandidate;
|
|
}
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
console.log('This appears to be Chrome');
|
|
RTC = {
|
|
peerconnection: webkitRTCPeerConnection,
|
|
browser: 'chrome',
|
|
getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
|
|
attachMediaStream: function (element, stream) {
|
|
element.attr('src', webkitURL.createObjectURL(stream));
|
|
},
|
|
// DTLS should now be enabled by default but..
|
|
pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
|
|
};
|
|
if (navigator.userAgent.indexOf('Android') != -1) {
|
|
RTC.pc_constraints = {}; // disable DTLS on Android
|
|
}
|
|
if (!webkitMediaStream.prototype.getVideoTracks) {
|
|
webkitMediaStream.prototype.getVideoTracks = function () {
|
|
return this.videoTracks;
|
|
};
|
|
}
|
|
if (!webkitMediaStream.prototype.getAudioTracks) {
|
|
webkitMediaStream.prototype.getAudioTracks = function () {
|
|
return this.audioTracks;
|
|
};
|
|
}
|
|
}
|
|
if (RTC === null) {
|
|
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
|
|
}
|
|
return RTC;
|
|
}
|
|
|
|
function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps, desktopStream) {
|
|
var constraints = {audio: false, video: false};
|
|
|
|
if (um.indexOf('video') >= 0) {
|
|
constraints.video = {mandatory: {}};// same behaviour as true
|
|
}
|
|
if (um.indexOf('audio') >= 0) {
|
|
constraints.audio = {};// same behaviour as true
|
|
}
|
|
if (um.indexOf('screen') >= 0) {
|
|
constraints.video = {
|
|
mandatory: {
|
|
chromeMediaSource: "screen",
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3
|
|
}
|
|
};
|
|
}
|
|
if (um.indexOf('desktop') >= 0) {
|
|
constraints.video = {
|
|
mandatory: {
|
|
chromeMediaSource: "desktop",
|
|
chromeMediaSourceId: desktopStream,
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we are running on Android device
|
|
var isAndroid = navigator.userAgent.indexOf('Android') != -1;
|
|
|
|
if (resolution && !constraints.video || isAndroid) {
|
|
constraints.video = {mandatory: {}};// same behaviour as true
|
|
}
|
|
// see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
|
|
switch (resolution) {
|
|
// 16:9 first
|
|
case '1080':
|
|
case 'fullhd':
|
|
constraints.video.mandatory.minWidth = 1920;
|
|
constraints.video.mandatory.minHeight = 1080;
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
break;
|
|
case '720':
|
|
case 'hd':
|
|
constraints.video.mandatory.minWidth = 1280;
|
|
constraints.video.mandatory.minHeight = 720;
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
break;
|
|
case '360':
|
|
constraints.video.mandatory.minWidth = 640;
|
|
constraints.video.mandatory.minHeight = 360;
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
break;
|
|
case '180':
|
|
constraints.video.mandatory.minWidth = 320;
|
|
constraints.video.mandatory.minHeight = 180;
|
|
constraints.video.mandatory.minAspectRatio = 1.77;
|
|
break;
|
|
// 4:3
|
|
case '960':
|
|
constraints.video.mandatory.minWidth = 960;
|
|
constraints.video.mandatory.minHeight = 720;
|
|
break;
|
|
case '640':
|
|
case 'vga':
|
|
constraints.video.mandatory.minWidth = 640;
|
|
constraints.video.mandatory.minHeight = 480;
|
|
break;
|
|
case '320':
|
|
constraints.video.mandatory.minWidth = 320;
|
|
constraints.video.mandatory.minHeight = 240;
|
|
break;
|
|
default:
|
|
if (isAndroid) {
|
|
constraints.video.mandatory.minWidth = 320;
|
|
constraints.video.mandatory.minHeight = 240;
|
|
constraints.video.mandatory.maxFrameRate = 15;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
|
|
if (!constraints.video) constraints.video = {mandatory: {}};//same behaviour as true
|
|
constraints.video.optional = [{bandwidth: bandwidth}];
|
|
}
|
|
if (fps) { // for some cameras it might be necessary to request 30fps
|
|
// so they choose 30fps mjpg over 10fps yuy2
|
|
if (!constraints.video) constraints.video = {mandatory: {}};// same behaviour as tru;
|
|
constraints.video.mandatory.minFrameRate = fps;
|
|
}
|
|
|
|
try {
|
|
RTC.getUserMedia(constraints,
|
|
function (stream) {
|
|
console.log('onUserMediaSuccess');
|
|
success_callback(stream);
|
|
},
|
|
function (error) {
|
|
console.warn('Failed to get access to local media. Error ', error);
|
|
if(failure_callback) {
|
|
failure_callback(error);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error('GUM failed: ', e);
|
|
if(failure_callback) {
|
|
failure_callback(e);
|
|
}
|
|
}
|
|
}
|