/* global MediaStreamTrack, File */
/* jshint -W020 */
/**
* WebRTC namespace for jsxc.
*
* @namespace jsxc.webrtc
*/
jsxc.webrtc = {
/** strophe connection */
conn: null,
/** local video stream */
localStream: null,
/** remote video stream */
remoteStream: null,
/** jid of the last caller */
last_caller: null,
/** should we auto accept incoming calls? */
AUTO_ACCEPT: false,
/** required disco features for video call */
reqVideoFeatures: ['urn:xmpp:jingle:apps:rtp:video', 'urn:xmpp:jingle:apps:rtp:audio', 'urn:xmpp:jingle:transports:ice-udp:1', 'urn:xmpp:jingle:apps:dtls:0'],
/** required disco features for file transfer */
reqFileFeatures: ['urn:xmpp:jingle:1', 'urn:xmpp:jingle:apps:file-transfer:3'],
/** bare jid to current jid mapping */
chatJids: {},
/**
* Initialize webrtc plugin.
*
* @private
* @memberOf jsxc.webrtc
*/
init: function() {
var self = jsxc.webrtc;
// shortcut
self.conn = jsxc.xmpp.conn;
if (!self.conn.jingle) {
jsxc.error('No jingle plugin found!');
return;
}
var manager = self.conn.jingle.manager;
$(document).on('message.jsxc', self.onMessage);
$(document).on('presence.jsxc', self.onPresence);
$(document).on('mediafailure.jingle', self.onMediaFailure);
manager.on('incoming', $.proxy(self.onIncoming, self));
// @REVIEW those events could be session based
manager.on('terminated', $.proxy(self.onTerminated, self));
manager.on('ringing', $.proxy(self.onCallRinging, self));
manager.on('receivedFile', $.proxy(self.onReceivedFile, self));
manager.on('sentFile', function(sess, metadata) {
jsxc.debug('sent ' + metadata.hash);
});
// @REVIEW those events could be session based
manager.on('peerStreamAdded', $.proxy(self.onRemoteStreamAdded, self));
manager.on('peerStreamRemoved', $.proxy(self.onRemoteStreamRemoved, self));
manager.on('log:*', function(level, msg) {
jsxc.debug('[JINGLE][' + level + ']', msg);
});
if (self.conn.caps) {
$(document).on('caps.strophe', self.onCaps);
}
var url = jsxc.options.get('RTCPeerConfig').url || jsxc.options.turnCredentialsPath;
var peerConfig = jsxc.options.get('RTCPeerConfig');
if (typeof url === 'string' && url.length > 0) {
self.getTurnCrendentials(url);
} else {
if (jsxc.storage.getUserItem('iceValidity')) {
// old ice validity found. Clean up.
jsxc.storage.removeUserItem('iceValidity');
// Replace saved servers with the once passed to jsxc
peerConfig.iceServers = jsxc.options.RTCPeerConfig.iceServers;
jsxc.options.set('RTCPeerConfig', peerConfig);
}
self.conn.jingle.setICEServers(peerConfig.iceServers);
}
},
onConnected: function() {
//Request new credentials after login
jsxc.storage.removeUserItem('iceValidity');
},
onDisconnected: function() {
var self = jsxc.webrtc;
$(document).off('message.jsxc', self.onMessage);
$(document).off('presence.jsxc', self.onPresence);
$(document).off('mediafailure.jingle', self.onMediaFailure);
$(document).off('caps.strophe', self.onCaps);
},
/**
* Checks if cached configuration is valid and if necessary update it.
*
* @memberOf jsxc.webrtc
* @param {string} [url]
*/
getTurnCrendentials: function(url) {
var self = jsxc.webrtc;
url = url || jsxc.options.get('RTCPeerConfig').url || jsxc.options.turnCredentialsPath;
var ttl = (jsxc.storage.getUserItem('iceValidity') || 0) - (new Date()).getTime();
// validity from jsxc < 2.1.0 is invalid
if (jsxc.storage.getUserItem('iceConfig')) {
jsxc.storage.removeUserItem('iceConfig');
ttl = -1;
}
if (ttl > 0) {
// credentials valid
self.conn.jingle.setICEServers(jsxc.options.get('RTCPeerConfig').iceServers);
window.setTimeout(jsxc.webrtc.getTurnCrendentials, ttl + 500);
return;
}
$.ajax(url, {
async: true,
xhrFields: {
withCredentials: jsxc.options.get('RTCPeerConfig').withCredentials
},
success: function(data) {
var ttl = data.ttl || 3600;
var iceServers = data.iceServers;
if (!iceServers && data.url) {
// parse deprecated (v2.1.0) syntax
jsxc.warn('Received RTCPeer configuration is deprecated. Use now RTCPeerConfig.url.');
iceServers = [{
urls: data.url
}];
if (data.username) {
iceServers[0].username = data.username;
}
if (data.credential) {
iceServers[0].credential = data.credential;
}
}
if (iceServers && iceServers.length > 0) {
// url as parameter is deprecated
var url = iceServers[0].url && iceServers[0].url.length > 0;
var urls = iceServers[0].urls && iceServers[0].urls.length > 0;
if (urls || url) {
jsxc.debug('ice servers received');
var peerConfig = jsxc.options.get('RTCPeerConfig');
peerConfig.iceServers = iceServers;
jsxc.options.set('RTCPeerConfig', peerConfig);
self.conn.jingle.setICEServers(iceServers);
jsxc.storage.setUserItem('iceValidity', (new Date()).getTime() + 1000 * ttl);
} else {
jsxc.warn('No valid url found in first ice object.');
}
}
},
dataType: 'json'
});
},
/**
* Return list of capable resources.
*
* @memberOf jsxc.webrtc
* @param jid
* @param {(string|string[])} features list of required features
* @returns {Array}
*/
getCapableRes: function(jid, features) {
var self = jsxc.webrtc;
var bid = jsxc.jidToBid(jid);
var res = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
if (!features) {
return res;
} else if (typeof features === 'string') {
features = [features];
}
var available = [];
$.each(res, function(i, r) {
if (self.conn.caps.hasFeatureByJid(bid + '/' + r, features)) {
available.push(r);
}
});
return available;
},
/**
* Add "video" button to window menu.
*
* @private
* @memberOf jsxc.webrtc
* @param event
* @param win jQuery window object
*/
initWindow: function(event, win) {
var self = jsxc.webrtc;
if (win.hasClass('jsxc_groupchat')) {
return;
}
jsxc.debug('webrtc.initWindow');
if (!self.conn) {
$(document).one('attached.jsxc', function() {
self.initWindow(null, win);
});
return;
}
// Add video call icon
var div = $('<div>').addClass('jsxc_video');
win.find('.jsxc_tools .jsxc_settings').after(div);
var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
var browserDetails = self.conn.jingle.RTC.browserDetails || {};
var browser = browserDetails.browser;
var version = browserDetails.version;
if (screenMediaExtension[browser] || jsxc.storage.getItem('debug') || (browser === 'firefox' && version >= 52)) {
// Add screen sharing button if extension is available or we are in debug mode
var a = $('<a>');
a.text($.t('Share_screen'));
a.addClass('jsxc_shareScreen jsxc_video');
a.attr('href', '#');
win.find('.jsxc_settings .jsxc_menu li:last').after($('<li>').append(a));
}
self.updateIcon(win.data('bid'));
},
/**
* Enable or disable "video" icon and assign full jid.
*
* @memberOf jsxc.webrtc
* @param bid CSS conform jid
*/
updateIcon: function(bid) {
jsxc.debug('Update icon', bid);
var self = jsxc.webrtc;
if (bid === jsxc.jidToBid(self.conn.jid)) {
return;
}
var win = jsxc.gui.window.get(bid);
var jid = win.data('jid');
var ls = jsxc.storage.getUserItem('buddy', bid);
if (typeof jid !== 'string') {
if (ls && typeof ls.jid === 'string') {
jid = ls.jid;
} else {
jsxc.debug('[webrtc] Could not update icon, because could not find jid for ' + bid);
return;
}
}
var res = Strophe.getResourceFromJid(jid);
var el = win.find('.jsxc_video');
var capableRes = self.getCapableRes(jid, self.reqVideoFeatures);
var targetRes = res;
if (targetRes === null) {
$.each(jsxc.storage.getUserItem('buddy', bid).res || [], function(index, val) {
if (capableRes.indexOf(val) > -1) {
targetRes = val;
return false;
}
});
jid = jid + '/' + targetRes;
}
el.off('click');
if (capableRes.indexOf(targetRes) > -1) {
el.click(function() {
if ($(this).hasClass('jsxc_shareScreen')) {
self.startScreenSharing(jid);
} else {
self.startCall(jid);
}
});
el.removeClass('jsxc_disabled');
el.attr('title', $.t('Start_video_call'));
} else {
el.addClass('jsxc_disabled');
el.attr('title', $.t('Video_call_not_possible'));
}
},
/**
* Check if full jid changed.
*
* @private
* @memberOf jsxc.webrtc
* @param e
* @param from full jid
*/
onMessage: function(e, from) {
var self = jsxc.webrtc;
var bid = jsxc.jidToBid(from);
jsxc.debug('webrtc.onmessage', from);
if (self.chatJids[bid] !== from) {
self.updateIcon(bid);
self.chatJids[bid] = from;
}
},
/**
* Update icon on presence.
*
* @memberOf jsxc.webrtc
* @param ev
* @param status
* @private
*/
onPresence: function(ev, jid, status, presence) {
var self = jsxc.webrtc;
if ($(presence).find('c[xmlns="' + Strophe.NS.CAPS + '"]').length === 0) {
jsxc.debug('webrtc.onpresence', jid);
self.updateIcon(jsxc.jidToBid(jid));
}
},
/**
* Display status message to user.
*
* @memberOf jsxc.webrtc
* @param txt message
* @param d duration in ms
*/
setStatus: function(txt, d) {
var status = $('.jsxc_webrtc .jsxc_status');
var duration = (typeof d === 'undefined' || d === null) ? 4000 : d;
jsxc.debug('[Webrtc]', txt);
if (status.html()) {
// attach old messages
txt = status.html() + '<br />' + txt;
}
status.html(txt);
status.css({
'margin-left': '-' + (status.width() / 2) + 'px',
opacity: 0,
display: 'block'
});
status.stop().animate({
opacity: 1
});
clearTimeout(status.data('timeout'));
if (duration === 0) {
return;
}
var to = setTimeout(function() {
status.stop().animate({
opacity: 0
}, function() {
status.html('');
});
}, duration);
status.data('timeout', to);
},
/**
* Update "video" button if we receive cap information.
*
* @private
* @memberOf jsxc.webrtc
* @param event
* @param jid
*/
onCaps: function(event, jid) {
var self = jsxc.webrtc;
if (jsxc.gui.roster.loaded) {
self.updateIcon(jsxc.jidToBid(jid));
} else {
$(document).on('cloaded.roster.jsxc', function() {
self.updateIcon(jsxc.jidToBid(jid));
});
}
},
/**
* Called if media failes.
*
* @private
* @memberOf jsxc.webrtc
*/
onMediaFailure: function(ev, err) {
var self = jsxc.webrtc;
var msg;
err = err || {};
self.setStatus('media failure');
switch (err.name) {
case 'NotAllowedError':
case 'PERMISSION_DENIED':
msg = $.t('PermissionDeniedError');
break;
case 'HTTPS_REQUIRED':
case 'EXTENSION_UNAVAILABLE':
msg = $.t(err.name);
break;
default:
msg = $.t(err.name) !== err.name ? $.t(err.name) : $.t('UNKNOWN_ERROR');
}
jsxc.gui.window.postMessage({
bid: jsxc.jidToBid(jsxc.webrtc.last_caller),
direction: jsxc.Message.SYS,
msg: $.t('Media_failure') + ': ' + msg + ' (' + err.name + ').'
});
jsxc.gui.dialog.close();
jsxc.debug('media failure: ' + err.name);
},
/**
* Process incoming jingle offer.
*
* @param {BaseSession} session
*/
onIncoming: function(session) {
var self = jsxc.webrtc;
var type = (session.constructor) ? session.constructor.name : null;
if (type === 'FileTransferSession') {
self.onIncomingFileTransfer(session);
} else if (type === 'MediaSession') {
var reqMedia = false;
$.each(session.pc.remoteDescription.contents, function() {
if (this.senders === 'both') {
reqMedia = true;
}
});
session.call = reqMedia;
if (reqMedia) {
self.onIncomingCall(session);
} else {
self.onIncomingStream(session);
}
} else {
jsxc.warn('Unknown session type.');
}
},
/**
* Process incoming stream offer.
*
* @param {MediaSession} session
*/
onIncomingStream: function(session) {
jsxc.debug('incoming stream from ' + session.peerID);
var self = jsxc.webrtc;
var bid = jsxc.jidToBid(session.peerID);
session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
self.postScreenMessage(bid, $.t('Incoming_stream'), session.sid);
// display notification
jsxc.notification.notify($.t('Incoming_stream'), $.t('from_sender', {
sender: bid
}));
// send signal to partner
session.ring();
jsxc.webrtc.last_caller = session.peerID;
if (jsxc.webrtc.AUTO_ACCEPT) {
acceptIncomingStream(session);
return;
}
var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
noClose: true
});
dialog.find('.jsxc_accept').click(function() {
$(document).trigger('accept.call.jsxc');
acceptIncomingStream(session);
});
dialog.find('.jsxc_reject').click(function() {
jsxc.gui.dialog.close();
$(document).trigger('reject.call.jsxc');
session.decline();
});
function acceptIncomingStream(session) {
jsxc.gui.dialog.close();
jsxc.gui.showVideoWindow(session.peerID);
session.accept();
}
},
/**
* Process incoming file offer.
*
* @param {FileSession} session
*/
onIncomingFileTransfer: function(session) {
jsxc.debug('incoming file transfer from ' + session.peerID);
var buddylist = jsxc.storage.getUserItem('buddylist') || [];
var bid = jsxc.jidToBid(session.peerID);
if (buddylist.indexOf(bid) > -1) {
//Accept file transfers only from contacts
session.accept();
var message = jsxc.gui.window.postMessage({
_uid: session.sid + ':msg',
bid: bid,
direction: jsxc.Message.IN,
attachment: {
name: session.receiver.metadata.name,
type: session.receiver.metadata.type || 'application/octet-stream'
}
});
session.receiver.on('progress', function(sent, size) {
jsxc.gui.window.updateProgress(message, sent, size);
});
}
},
/**
* Called on incoming call.
*
* @private
* @memberOf jsxc.webrtc
* @param {MediaSession} session
*/
onIncomingCall: function(session) {
jsxc.debug('incoming call from ' + session.peerID);
var self = jsxc.webrtc;
var bid = jsxc.jidToBid(session.peerID);
session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
self.postCallMessage(bid, $.t('Incoming_call'), session.sid);
// display notification
jsxc.notification.notify($.t('Incoming_call'), $.t('from_sender', {
sender: bid
}));
// send signal to partner
session.ring();
jsxc.webrtc.last_caller = session.peerID;
if (jsxc.webrtc.AUTO_ACCEPT) {
self.acceptIncomingCall(session);
return;
}
var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
noClose: true
});
dialog.find('.jsxc_accept').click(function() {
self.acceptIncomingCall(session);
});
dialog.find('.jsxc_reject').click(function() {
jsxc.gui.dialog.close();
$(document).trigger('reject.call.jsxc');
session.decline();
});
},
/**
* Called on incoming call.
*
* @private
* @memberOf jsxc.webrtc
* @param {MediaSession} session
*/
acceptIncomingCall: function(session) {
$(document).trigger('accept.call.jsxc');
var self = jsxc.webrtc;
jsxc.switchEvents({
'mediaready.jingle': function(ev, stream) {
self.setStatus('Accept call');
self.localStream = stream;
self.conn.jingle.localStream = stream;
var dialog = jsxc.gui.showVideoWindow(session.peerID);
dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
session.addStream(stream);
session.accept();
},
'mediafailure.jingle': function() {
session.decline();
}
});
self.reqUserMedia();
},
/**
* Process jingle termination event.
*
* @param {BaseSession} session
* @param {Object} reason Reason for termination
*/
onTerminated: function(session, reason) {
var self = jsxc.webrtc;
var type = (session.constructor) ? session.constructor.name : null;
if (type === 'MediaSession') {
self.onCallTerminated(session, reason);
}
},
/**
* Called if call is terminated.
*
* @private
* @memberOf jsxc.webrtc
* @param {BaseSession} session
* @param {Object} reason Reason for termination
*/
onCallTerminated: function(session, reason) {
var self = jsxc.webrtc;
self.setStatus('call terminated ' + session.peerID + (reason && reason.condition ? reason.condition : ''));
var bid = jsxc.jidToBid(session.peerID);
if (self.localStream) {
// stop local stream
if (typeof self.localStream.getTracks === 'function') {
var tracks = self.localStream.getTracks();
tracks.forEach(function(track) {
track.stop();
});
} else if (typeof self.localStream.stop === 'function') {
self.localStream.stop();
} else {
jsxc.warn('Could not stop local stream');
}
}
// @REVIEW necessary?
if ($('.jsxc_remotevideo').length) {
$('.jsxc_remotevideo')[0].src = "";
}
if ($('.jsxc_localvideo').length) {
$('.jsxc_localvideo')[0].src = "";
}
self.conn.jingle.localStream = null;
self.localStream = null;
self.remoteStream = null;
jsxc.gui.closeVideoWindow();
// Close incoming call dialog and stop ringing
jsxc.gui.dialog.close();
$(document).trigger('reject.call.jsxc');
$(document).off('error.jingle');
var msg = (reason && reason.condition ? (': ' + $.t('jingle_reason_' + reason.condition)) : '') + '.';
if (session.call) {
msg = $.t('Call_terminated') + msg;
jsxc.webrtc.postCallMessage(bid, msg, session.sid);
} else {
msg = $.t('Stream_terminated') + msg;
jsxc.webrtc.postScreenMessage(bid, msg, session.sid);
}
},
/**
* Remote station is ringing.
*
* @private
* @memberOf jsxc.webrtc
*/
onCallRinging: function() {
this.setStatus('ringing...', 0);
$('.jsxc_videoContainer').removeClass('jsxc_establishing').addClass('jsxc_ringing');
},
/**
* Called if we receive a remote stream.
*
* @private
* @memberOf jsxc.webrtc
* @param {BaseSession} session
* @param {Object} stream
*/
onRemoteStreamAdded: function(session, stream) {
var self = jsxc.webrtc;
self.setStatus('Remote stream for session ' + session.sid + ' added.');
self.remoteStream = stream;
var isVideoDevice = stream.getVideoTracks().length > 0;
var isAudioDevice = stream.getAudioTracks().length > 0;
self.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
self.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
if ($('.jsxc_remotevideo').length) {
self.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
$('#jsxc_webrtc .jsxc_' + (isVideoDevice ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
}
},
/**
* Attach media stream to element.
*
* @memberOf jsxc.webrtc
* @param element {Element|jQuery}
* @param stream {mediastream}
*/
attachMediaStream: function(element, stream) {
var el = (element instanceof jQuery) ? element.get(0) : element;
el.srcObject = stream;
$(element).show();
},
/**
* Called if the remote stream was removed.
*
* @private
* @meberOf jsxc.webrtc
* @param {BaseSession} session
*/
onRemoteStreamRemoved: function(session) {
this.setStatus('Remote stream for ' + session.jid + ' removed.');
//@TODO clean up
},
/**
* Display information according to the connection state.
*
* @private
* @memberOf jsxc.webrtc
* @param {BaseSession} session
* @param {String} state
*/
onIceConnectionStateChanged: function(session, state) {
var self = jsxc.webrtc;
jsxc.debug('connection state for ' + session.sid, state);
if (state === 'connected') {
$('#jsxc_webrtc .jsxc_deviceAvailable').show();
} else if (state === 'failed') {
jsxc.gui.window.postMessage({
bid: jsxc.jidToBid(session.peerID),
direction: jsxc.Message.SYS,
msg: $.t('ICE_connection_failure')
});
session.end('failed-transport');
$(document).trigger('callterminated.jingle');
} else if (state === 'interrupted') {
self.setStatus($.t('Connection_interrupted'));
}
},
/**
* Start a call to the specified jid.
*
* @memberOf jsxc.webrtc
* @param {String} jid full jid
* @param {String[]} um requested user media
*/
startCall: function(jid, um) {
var self = jsxc.webrtc;
if (Strophe.getResourceFromJid(jid) === null) {
jsxc.debug('We need a full jid');
return;
}
self.last_caller = jid;
jsxc.switchEvents({
'mediaready.jingle': function(ev, stream) {
jsxc.debug('media ready for outgoing call');
self.initiateOutgoingCall(jid, stream);
},
'mediafailure.jingle': function() {
jsxc.gui.dialog.close();
}
});
self.reqUserMedia(um);
},
/**
* Start jingle session to jid with stream.
*
* @param {String} jid
* @param {Object} stream
*/
initiateOutgoingCall: function(jid, stream) {
var self = jsxc.webrtc;
self.localStream = stream;
self.conn.jingle.localStream = stream;
var dialog = jsxc.gui.showVideoWindow(jid);
dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
self.setStatus('Initiate call');
// @REVIEW session based?
$(document).one('error.jingle', function(ev, sid, error) {
if (error && error.source !== 'offer') {
return;
}
setTimeout(function() {
jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
}, 500);
});
var session = self.conn.jingle.initiate(jid);
// flag session as call
session.call = true;
session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
self.postCallMessage(jsxc.jidToBid(jid), $.t('Call_started'), session.sid);
},
/**
* Hang up the current call.
*
* @memberOf jsxc.webrtc
*/
hangUp: function(reason, text) {
if (jsxc.webrtc.conn.jingle.manager && !$.isEmptyObject(jsxc.webrtc.conn.jingle.manager.peers)) {
jsxc.webrtc.conn.jingle.terminate(null, reason, text);
} else {
jsxc.gui.closeVideoWindow();
}
// @TODO check event
$(document).trigger('callterminated.jingle');
},
/**
* Start outgoing screen sharing session.
*
* @param {String} jid
*/
startScreenSharing: function(jid) {
var self = this;
if (Strophe.getResourceFromJid(jid) === null) {
jsxc.debug('We need a full jid');
return;
}
self.last_caller = jid;
jsxc.switchEvents({
'mediaready.jingle': function(ev, stream) {
self.initiateScreenSharing(jid, stream);
},
'mediafailure.jingle': function(ev, err) {
jsxc.gui.dialog.close();
var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
if (screenMediaExtension[browser] &&
(err.name === 'EXTENSION_UNAVAILABLE' || (err.name === 'NotAllowedError' && browser === 'firefox'))) {
// post download link after explanation
setTimeout(function() {
jsxc.gui.window.postMessage({
bid: jsxc.jidToBid(jid),
direction: jsxc.Message.SYS,
msg: $.t('Install_extension') + screenMediaExtension[browser]
});
}, 500);
}
}
});
self.reqUserMedia(['screen']);
},
/**
* Initiate outgoing (one-way) jingle session to jid with stream.
*
* @param {String} jid
* @param {Object} stream
*/
initiateScreenSharing: function(jid, stream) {
var self = jsxc.webrtc;
var bid = jsxc.jidToBid(jid);
jsxc.webrtc.localStream = stream;
jsxc.webrtc.conn.jingle.localStream = stream;
var container = jsxc.gui.showMinimizedVideoWindow();
container.addClass('jsxc_establishing');
self.setStatus('Initiate stream');
$(document).one('error.jingle', function(e, sid, error) {
if (error && error.source !== 'offer') {
return;
}
setTimeout(function() {
jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
}, 500);
});
var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
var browserVersion = self.conn.jingle.RTC.webrtcDetectedVersion;
var constraints;
if ((browserVersion < 33 && browser === 'firefox') || browser === 'chrome') {
constraints = {
mandatory: {
'OfferToReceiveAudio': false,
'OfferToReceiveVideo': false
}
};
} else {
constraints = {
'offerToReceiveAudio': false,
'offerToReceiveVideo': false
};
}
var session = self.conn.jingle.initiate(jid, undefined, constraints);
session.call = false;
session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
// @REVIEW also for calls?
session.on('accepted', function() {
self.onSessionAccepted(session);
});
self.postScreenMessage(bid, $.t('Stream_started'), session.sid);
},
/**
* Session was accepted by other peer.
*
* @param {BaseSession} session
*/
onSessionAccepted: function(session) {
var self = jsxc.webrtc;
$('.jsxc_videoContainer').removeClass('jsxc_ringing');
self.postScreenMessage(jsxc.jidToBid(session.peerID), $.t('Connection_accepted'), session.sid);
},
/**
* Request media from local user.
*
* @memberOf jsxc.webrtc
*/
reqUserMedia: function(um) {
if (this.localStream) {
$(document).trigger('mediaready.jingle', [this.localStream]);
return;
}
um = um || ['video', 'audio'];
jsxc.gui.dialog.open(jsxc.gui.template.get('allowMediaAccess'), {
noClose: true
});
if (um.indexOf('screen') >= 0) {
jsxc.webrtc.getScreenMedia();
} else if (typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' &&
typeof navigator.mediaDevices.enumerateDevices !== 'undefined') {
navigator.mediaDevices.enumerateDevices()
.then(filterUserMedia)
.catch(function(err) {
jsxc.warn(err.name + ": " + err.message);
});
} else if (typeof MediaStreamTrack !== 'undefined' && typeof MediaStreamTrack.getSources !== 'undefined') {
// @deprecated in chrome since v56
MediaStreamTrack.getSources(filterUserMedia);
} else {
jsxc.webrtc.getUserMedia(um);
}
function filterUserMedia(devices) {
var availableDevices = devices.map(function(device) {
return device.kind;
});
um = um.filter(function(el) {
return availableDevices.indexOf(el) !== -1 || availableDevices.indexOf(el + 'input') !== -1;
});
if (um.length) {
jsxc.webrtc.getUserMedia(um);
} else {
jsxc.warn('No audio/video device available.');
}
}
},
/**
* Get user media from local browser.
*
* @memberOf jsxc.webrtc
*/
getUserMedia: function(um) {
var self = jsxc.webrtc;
var constraints = {};
if (um.indexOf('video') > -1) {
constraints.video = true;
}
if (um.indexOf('audio') > -1) {
constraints.audio = true;
}
try {
self.conn.jingle.getUserMedia(constraints, self.userMediaCallback);
} catch (e) {
jsxc.error('GUM failed: ', e);
$(document).trigger('mediafailure.jingle');
}
},
userMediaCallback: function(err, stream) {
if (err) {
jsxc.warn('Failed to get access to local media. Error ', err);
$(document).trigger('mediafailure.jingle', [err]);
} else if (stream) {
jsxc.debug('onUserMediaSuccess');
$(document).trigger('mediaready.jingle', [stream]);
}
},
/**
* Get screen media from local browser.
*
* @memberOf jsxc.webrtc
*/
getScreenMedia: function() {
var self = jsxc.webrtc;
jsxc.debug('get screen media');
self.conn.jingle.getScreenMedia(self.screenMediaCallback);
},
screenMediaCallback: function(err, stream) {
if (err) {
$(document).trigger('mediafailure.jingle', [err]);
return;
}
if (stream) {
jsxc.debug('onScreenMediaSuccess');
$(document).trigger('mediaready.jingle', [stream]);
}
},
screenMediaAvailable: function() {
var self = jsxc.webrtc;
var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
// test if chrome extension for this domain is available
var chrome = !!sessionStorage.getScreenMediaJSExtensionId && browser === 'chrome';
// the ff extension from {@link https://github.com/otalk/getScreenMedia}
// does not provide any possibility to determine if it is installed or not.
// Starting with Firefox 52 {@link https://www.mozilla.org/en-US/firefox/52.0a2/auroranotes/}
// no extension is needed anyway.
var firefox = browser === 'firefox';
return chrome || firefox;
},
/**
* Make a snapshot from a video stream and display it.
*
* @memberOf jsxc.webrtc
* @param video Video stream
*/
snapshot: function(video) {
if (!video) {
jsxc.debug('Missing video element');
}
$('.jsxc_snapshotbar p').remove();
var canvas = $('<canvas/>').css('display', 'none').appendTo('body').attr({
width: video.width(),
height: video.height()
}).get(0);
var ctx = canvas.getContext('2d');
ctx.drawImage(video[0], 0, 0);
var img = $('<img/>');
var url = null;
try {
url = canvas.toDataURL('image/jpeg');
} catch (err) {
jsxc.warn('Error', err);
return;
}
img[0].src = url;
var link = $('<a/>').attr({
target: '_blank',
href: url
});
link.append(img);
$('.jsxc_snapshotbar').append(link);
canvas.remove();
},
/**
* Send file to full jid via jingle.
*
* @memberOf jsxc.webrtc
* @param {string} jid full jid
* @param {file} file
* @return {object} session
*/
sendFile: function(jid, file) {
jsxc.debug('Send file via webrtc');
var self = jsxc.webrtc;
if (!Strophe.getResourceFromJid(jid)) {
jsxc.warn('Require full jid to send file via webrtc');
return;
}
var sess = self.conn.jingle.manager.createFileTransferSession(jid);
sess.on('change:sessionState', function() {
jsxc.debug('Session state', sess.state);
});
sess.on('change:connectionState', function() {
jsxc.debug('Connection state', sess.connectionState);
});
sess.start(file);
return sess;
},
/**
* Display received file.
*
* @memberOf jsxc.webrtc
* @param {object} sess
* @param {File} file
* @param {object} metadata file metadata
*/
onReceivedFile: function(sess, file, metadata) {
jsxc.debug('file received', metadata);
if (!FileReader) {
return;
}
var reader = new FileReader();
var type;
if (!metadata.type) {
// detect file type via file extension, because XEP-0234 v0.14
// does not send any type
var ext = metadata.name.replace(/.+\.([a-z0-9]+)$/i, '$1').toLowerCase();
switch (ext) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
case 'svg':
type = 'image/' + ext.replace(/^jpg$/, 'jpeg');
break;
case 'mp3':
case 'wav':
type = 'audio/' + ext;
break;
case 'pdf':
type = 'application/pdf';
break;
case 'txt':
type = 'text/' + ext;
break;
default:
type = 'application/octet-stream';
}
} else {
type = metadata.type;
}
reader.onload = function(ev) {
// modify element with uid metadata.actualhash
jsxc.gui.window.postMessage({
_uid: sess.sid + ':msg',
bid: jsxc.jidToBid(sess.peerID),
direction: jsxc.Message.IN,
attachment: {
name: metadata.name,
type: type,
size: metadata.size,
data: ev.target.result
}
});
};
if (!file.type) {
// file type should be handled in lib
file = new File([file], metadata.name, {
type: type
});
}
reader.readAsDataURL(file);
}
};
jsxc.webrtc.postCallMessage = function(bid, msg, uid) {
jsxc.gui.window.postMessage({
_uid: uid,
bid: bid,
direction: jsxc.Message.SYS,
msg: ':telephone_receiver: ' + msg
});
};
jsxc.webrtc.postScreenMessage = function(bid, msg, uid) {
jsxc.gui.window.postMessage({
_uid: uid,
bid: bid,
direction: jsxc.Message.SYS,
msg: ':computer: ' + msg
});
};
jsxc.gui.showMinimizedVideoWindow = function() {
var self = jsxc.webrtc;
// needed to trigger complete.dialog.jsxc
jsxc.gui.dialog.close();
var videoContainer = $('<div/>');
videoContainer.addClass('jsxc_videoContainer jsxc_minimized');
videoContainer.appendTo('body');
videoContainer.draggable({
containment: "parent"
});
var videoElement = $('<video class="jsxc_localvideo" autoplay=""></video>');
videoElement.appendTo(videoContainer);
videoElement[0].muted = true;
videoElement[0].volume = 0;
if (self.localStream) {
self.attachMediaStream(videoElement, self.localStream);
}
videoContainer.append('<div class="jsxc_controlbar"><div><div class="jsxc_hangUp jsxc_videoControl"></div></div></div></div>');
videoContainer.find('.jsxc_hangUp').click(function() {
jsxc.webrtc.hangUp('success');
});
videoContainer.click(function() {
videoContainer.find('.jsxc_controlbar').toggleClass('jsxc_visible');
});
return videoContainer;
};
/**
* Display window for video call.
*
* @memberOf jsxc.gui
*/
jsxc.gui.showVideoWindow = function(jid) {
var self = jsxc.webrtc;
// needed to trigger complete.dialog.jsxc
jsxc.gui.dialog.close();
$('body').append(jsxc.gui.template.get('videoWindow'));
// mute own video element to avoid echoes
$('#jsxc_webrtc .jsxc_localvideo')[0].muted = true;
$('#jsxc_webrtc .jsxc_localvideo')[0].volume = 0;
var rv = $('#jsxc_webrtc .jsxc_remotevideo');
var lv = $('#jsxc_webrtc .jsxc_localvideo');
lv.draggable({
containment: "parent"
});
if (self.localStream) {
self.attachMediaStream(lv, self.localStream);
}
var w_dialog = $('#jsxc_webrtc').width();
var w_remote = rv.width();
// fit in video
if (w_remote > w_dialog) {
var scale = w_dialog / w_remote;
var new_h = rv.height() * scale;
var new_w = w_dialog;
var vc = $('#jsxc_webrtc .jsxc_videoContainer');
rv.height(new_h);
rv.width(new_w);
vc.height(new_h);
vc.width(new_w);
lv.height(lv.height() * scale);
lv.width(lv.width() * scale);
}
if (self.remoteStream) {
self.attachMediaStream(rv, self.remoteStream);
$('#jsxc_webrtc .jsxc_' + (self.remoteStream.getVideoTracks().length > 0 ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
}
var win = jsxc.gui.window.open(jsxc.jidToBid(jid));
win.find('.slimScrollDiv').resizable('disable');
jsxc.gui.window.resize(win, {
size: {
width: $('#jsxc_webrtc .jsxc_chatarea').width(),
height: $('#jsxc_webrtc .jsxc_chatarea').height()
}
}, true);
$('#jsxc_webrtc .jsxc_chatarea ul').append(win.detach());
$('#jsxc_webrtc .jsxc_hangUp').click(function() {
jsxc.webrtc.hangUp('success');
});
$('#jsxc_webrtc .jsxc_fullscreen').click(function() {
if ($.support.fullscreen) {
// Reset position of localvideo
$(document).one('disabled.fullscreen', function() {
lv.removeAttr('style');
});
$('#jsxc_webrtc .jsxc_videoContainer').fullscreen();
}
});
$('#jsxc_webrtc .jsxc_videoContainer').click(function() {
$('#jsxc_webrtc .jsxc_controlbar').toggleClass('jsxc_visible');
});
return $('#jsxc_webrtc');
};
jsxc.gui.closeVideoWindow = function() {
var win = $('#jsxc_webrtc .jsxc_chatarea > ul > li');
if (win.length > 0) {
$('#jsxc_windowList > ul').prepend(win.detach());
win.find('.slimScrollDiv').resizable('enable');
jsxc.gui.window.resize(win);
}
$('#jsxc_webrtc, .jsxc_videoContainer').remove();
};
$.extend(jsxc.CONST, {
KEYCODE_ENTER: 13,
KEYCODE_ESC: 27
});
$(document).ready(function() {
$(document).on('init.window.jsxc', jsxc.webrtc.initWindow);
$(document).on('attached.jsxc', jsxc.webrtc.init);
$(document).on('disconnected.jsxc', jsxc.webrtc.onDisconnected);
$(document).on('connected.jsxc', jsxc.webrtc.onConnected);
});