Source: jsxc.lib.webrtc.js

  1. /* global MediaStreamTrack, File */
  2. /* jshint -W020 */
  3. /**
  4. * WebRTC namespace for jsxc.
  5. *
  6. * @namespace jsxc.webrtc
  7. */
  8. jsxc.webrtc = {
  9. /** strophe connection */
  10. conn: null,
  11. /** local video stream */
  12. localStream: null,
  13. /** remote video stream */
  14. remoteStream: null,
  15. /** jid of the last caller */
  16. last_caller: null,
  17. /** should we auto accept incoming calls? */
  18. AUTO_ACCEPT: false,
  19. /** required disco features for video call */
  20. 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'],
  21. /** required disco features for file transfer */
  22. reqFileFeatures: ['urn:xmpp:jingle:1', 'urn:xmpp:jingle:apps:file-transfer:3'],
  23. /** bare jid to current jid mapping */
  24. chatJids: {},
  25. CONST: {
  26. NS: {
  27. EXTDISCO: 'urn:xmpp:extdisco:2'
  28. }
  29. },
  30. /**
  31. * Initialize webrtc plugin.
  32. *
  33. * @private
  34. * @memberOf jsxc.webrtc
  35. */
  36. init: function() {
  37. var self = jsxc.webrtc;
  38. // shortcut
  39. self.conn = jsxc.xmpp.conn;
  40. if (!self.conn.jingle) {
  41. jsxc.error('No jingle plugin found!');
  42. return;
  43. }
  44. var manager = self.conn.jingle.manager;
  45. $(document).on('message.jsxc', self.onMessage);
  46. $(document).on('presence.jsxc', self.onPresence);
  47. $(document).on('mediafailure.jingle', self.onMediaFailure);
  48. manager.on('incoming', $.proxy(self.onIncoming, self));
  49. // @REVIEW those events could be session based
  50. manager.on('terminated', $.proxy(self.onTerminated, self));
  51. manager.on('ringing', $.proxy(self.onCallRinging, self));
  52. manager.on('receivedFile', $.proxy(self.onReceivedFile, self));
  53. manager.on('sentFile', function(sess, metadata) {
  54. jsxc.debug('sent ' + metadata.hash);
  55. });
  56. // @REVIEW those events could be session based
  57. manager.on('peerStreamAdded', $.proxy(self.onRemoteStreamAdded, self));
  58. manager.on('peerStreamRemoved', $.proxy(self.onRemoteStreamRemoved, self));
  59. manager.on('log:*', function(level, msg) {
  60. jsxc.debug('[JINGLE][' + level + ']', msg);
  61. });
  62. if (self.conn.caps) {
  63. $(document).on('caps.strophe', self.onCaps);
  64. }
  65. self.setupIceServers();
  66. },
  67. onConnected: function() {
  68. //Request new credentials after login
  69. jsxc.storage.removeUserItem('iceValidity');
  70. },
  71. onDisconnected: function() {
  72. var self = jsxc.webrtc;
  73. $(document).off('message.jsxc', self.onMessage);
  74. $(document).off('presence.jsxc', self.onPresence);
  75. $(document).off('mediafailure.jingle', self.onMediaFailure);
  76. $(document).off('caps.strophe', self.onCaps);
  77. },
  78. setupIceServers: function() {
  79. var self = jsxc.webrtc;
  80. var ttl = (jsxc.storage.getUserItem('iceValidity') || 0) - (new Date()).getTime();
  81. // validity from jsxc < 2.1.0 is invalid
  82. if (jsxc.storage.getUserItem('iceConfig')) {
  83. jsxc.storage.removeUserItem('iceConfig');
  84. ttl = -1;
  85. }
  86. var url = jsxc.options.get('RTCPeerConfig').url || jsxc.options.turnCredentialsPath;
  87. var peerConfig = jsxc.options.get('RTCPeerConfig');
  88. var domain = self.conn.domain;
  89. if (ttl > 0) {
  90. // credentials valid
  91. self.conn.jingle.setICEServers(peerConfig.iceServers);
  92. window.setTimeout(jsxc.webrtc.setupIceServers, ttl + 500);
  93. } else if (jsxc.xmpp.conn.caps.hasFeatureByJid(domain, self.CONST.NS.EXTDISCO)) {
  94. self.getIceServersByExternalDisco();
  95. } else if (typeof url === 'string' && url.length > 0) {
  96. self.getIceServersByUrl(url);
  97. } else {
  98. self.conn.jingle.setICEServers(peerConfig.iceServers);
  99. }
  100. },
  101. getIceServersByExternalDisco: function() {
  102. var iq = $iq({
  103. type: 'get',
  104. to: jsxc.xmpp.conn.domain
  105. }).c('services', {
  106. xmlns: 'urn:xmpp:extdisco:1'
  107. });
  108. jsxc.xmpp.conn.sendIQ(iq, parseExtDiscoResponse, function(err) {
  109. console.warn('getting turn credentials failed', err);
  110. });
  111. function parseExtDiscoResponse(res) {
  112. jsxc.debug('ice servers receiving by xmpp extdisco');
  113. var iceServers = [];
  114. var minTtl = 86400;
  115. $(res).find('>services>service').each(function(idx, el) {
  116. el = $(el);
  117. var serverItem = {};
  118. switch (el.attr('type')) {
  119. case 'stun':
  120. case 'stuns':
  121. serverItem.urls = el.attr('type') + ':' + el.attr('host');
  122. if (el.attr('port')) {
  123. serverItem.urls += ':' + el.attr('port');
  124. }
  125. break;
  126. case 'turn':
  127. case 'turns':
  128. if (el.attr('username')) {
  129. serverItem.username = el.attr('username');
  130. }
  131. serverItem.urls = el.attr('type') + ':' + el.attr('host');
  132. if (el.attr('port') && el.attr('port') !== '3478') {
  133. serverItem.urls += ':' + el.attr('port');
  134. }
  135. if (el.attr('transport') && el.attr('transport') !== 'udp') {
  136. serverItem.urls += '?transport=' + el.attr('transport');
  137. }
  138. if (el.attr('password')) {
  139. serverItem.credential = el.attr('password');
  140. }
  141. if (el.attr('ttl') && el.attr('ttl') < minTtl) {
  142. minTtl = el.attr('ttl');
  143. }
  144. break;
  145. }
  146. if (serverItem.urls) {
  147. iceServers.push(serverItem);
  148. }
  149. });
  150. if (iceServers.length > 0) {
  151. jsxc.webrtc.setIceServers(iceServers, minTtl);
  152. } else {
  153. jsxc.warn('Found no valid ICE server configuration');
  154. }
  155. }
  156. },
  157. getIceServersByUrl: function(url) {
  158. var self = jsxc.webrtc;
  159. $.ajax(url, {
  160. async: true,
  161. xhrFields: {
  162. withCredentials: jsxc.options.get('RTCPeerConfig').withCredentials
  163. },
  164. success: function(data) {
  165. var ttl = data.ttl || 3600;
  166. var iceServers = data.iceServers;
  167. if (!iceServers && data.url) {
  168. // parse deprecated (v2.1.0) syntax
  169. jsxc.warn('Received RTCPeer configuration is deprecated. Use now RTCPeerConfig.url.');
  170. iceServers = [{
  171. urls: data.url
  172. }];
  173. if (data.username) {
  174. iceServers[0].username = data.username;
  175. }
  176. if (data.credential) {
  177. iceServers[0].credential = data.credential;
  178. }
  179. }
  180. if (iceServers && iceServers.length > 0) {
  181. // url as parameter is deprecated
  182. var url = iceServers[0].url && iceServers[0].url.length > 0;
  183. var urls = iceServers[0].urls && iceServers[0].urls.length > 0;
  184. if (urls || url) {
  185. self.setIceServers(iceServers, ttl);
  186. } else {
  187. jsxc.warn('No valid url found in first ice object.');
  188. }
  189. }
  190. },
  191. dataType: 'json'
  192. });
  193. },
  194. setIceServers: function(iceServers, ttl) {
  195. jsxc.debug('set ice servers');
  196. var peerConfig = jsxc.options.get('RTCPeerConfig');
  197. peerConfig.iceServers = iceServers;
  198. jsxc.options.set('RTCPeerConfig', peerConfig);
  199. jsxc.webrtc.conn.jingle.setICEServers(iceServers);
  200. jsxc.storage.setUserItem('iceValidity', (new Date()).getTime() + 1000 * ttl);
  201. window.setTimeout(jsxc.webrtc.setupIceServers, ttl + 500);
  202. },
  203. /**
  204. * Return list of capable resources.
  205. *
  206. * @memberOf jsxc.webrtc
  207. * @param jid
  208. * @param {(string|string[])} features list of required features
  209. * @returns {Array}
  210. */
  211. getCapableRes: function(jid, features) {
  212. var self = jsxc.webrtc;
  213. var bid = jsxc.jidToBid(jid);
  214. var res = Object.keys(jsxc.storage.getUserItem('res', bid) || {}) || [];
  215. if (!features) {
  216. return res;
  217. } else if (typeof features === 'string') {
  218. features = [features];
  219. }
  220. var available = [];
  221. $.each(res, function(i, r) {
  222. if (self.conn.caps.hasFeatureByJid(bid + '/' + r, features)) {
  223. available.push(r);
  224. }
  225. });
  226. return available;
  227. },
  228. /**
  229. * Add "video" button to window menu.
  230. *
  231. * @private
  232. * @memberOf jsxc.webrtc
  233. * @param event
  234. * @param win jQuery window object
  235. */
  236. initWindow: function(event, win) {
  237. var self = jsxc.webrtc;
  238. if (win.hasClass('jsxc_groupchat')) {
  239. return;
  240. }
  241. jsxc.debug('webrtc.initWindow');
  242. if (!self.conn) {
  243. $(document).one('attached.jsxc', function() {
  244. self.initWindow(null, win);
  245. });
  246. return;
  247. }
  248. // Add video call icon
  249. var div = $('<div>').addClass('jsxc_video');
  250. win.find('.jsxc_tools .jsxc_settings').after(div);
  251. var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
  252. var browserDetails = self.conn.jingle.RTC.browserDetails || {};
  253. var browser = browserDetails.browser;
  254. var version = browserDetails.version;
  255. if (screenMediaExtension[browser] || jsxc.storage.getItem('debug') || (browser === 'firefox' && version >= 52)) {
  256. // Add screen sharing button if extension is available or we are in debug mode
  257. var a = $('<a>');
  258. a.text($.t('Share_screen'));
  259. a.addClass('jsxc_shareScreen jsxc_video');
  260. a.attr('href', '#');
  261. win.find('.jsxc_settings .jsxc_menu li:last').after($('<li>').append(a));
  262. }
  263. self.updateIcon(win.data('bid'));
  264. },
  265. /**
  266. * Enable or disable "video" icon and assign full jid.
  267. *
  268. * @memberOf jsxc.webrtc
  269. * @param bid CSS conform jid
  270. */
  271. updateIcon: function(bid) {
  272. jsxc.debug('Update icon', bid);
  273. var self = jsxc.webrtc;
  274. if (bid === jsxc.jidToBid(self.conn.jid)) {
  275. return;
  276. }
  277. var win = jsxc.gui.window.get(bid);
  278. var jid = win.data('jid');
  279. var ls = jsxc.storage.getUserItem('buddy', bid);
  280. if (typeof jid !== 'string') {
  281. if (ls && typeof ls.jid === 'string') {
  282. jid = ls.jid;
  283. } else {
  284. jsxc.debug('[webrtc] Could not update icon, because could not find jid for ' + bid);
  285. return;
  286. }
  287. }
  288. var res = Strophe.getResourceFromJid(jid);
  289. var el = win.find('.jsxc_video');
  290. var capableRes = self.getCapableRes(jid, self.reqVideoFeatures);
  291. var targetRes = res;
  292. if (targetRes === null) {
  293. $.each(jsxc.storage.getUserItem('buddy', bid).res || [], function(index, val) {
  294. if (capableRes.indexOf(val) > -1) {
  295. targetRes = val;
  296. return false;
  297. }
  298. });
  299. jid = jid + '/' + targetRes;
  300. }
  301. el.off('click');
  302. if (capableRes.indexOf(targetRes) > -1) {
  303. el.click(function() {
  304. if ($(this).hasClass('jsxc_shareScreen')) {
  305. self.startScreenSharing(jid);
  306. } else {
  307. self.startCall(jid);
  308. }
  309. });
  310. el.removeClass('jsxc_disabled');
  311. el.attr('title', $.t('Start_video_call'));
  312. } else {
  313. el.addClass('jsxc_disabled');
  314. el.attr('title', $.t('Video_call_not_possible'));
  315. }
  316. },
  317. /**
  318. * Check if full jid changed.
  319. *
  320. * @private
  321. * @memberOf jsxc.webrtc
  322. * @param e
  323. * @param from full jid
  324. */
  325. onMessage: function(e, from) {
  326. var self = jsxc.webrtc;
  327. var bid = jsxc.jidToBid(from);
  328. jsxc.debug('webrtc.onmessage', from);
  329. if (self.chatJids[bid] !== from) {
  330. self.updateIcon(bid);
  331. self.chatJids[bid] = from;
  332. }
  333. },
  334. /**
  335. * Update icon on presence.
  336. *
  337. * @memberOf jsxc.webrtc
  338. * @param ev
  339. * @param status
  340. * @private
  341. */
  342. onPresence: function(ev, jid, status, presence) {
  343. var self = jsxc.webrtc;
  344. if ($(presence).find('c[xmlns="' + Strophe.NS.CAPS + '"]').length === 0) {
  345. jsxc.debug('webrtc.onpresence', jid);
  346. self.updateIcon(jsxc.jidToBid(jid));
  347. }
  348. },
  349. /**
  350. * Display status message to user.
  351. *
  352. * @memberOf jsxc.webrtc
  353. * @param txt message
  354. * @param d duration in ms
  355. */
  356. setStatus: function(txt, d) {
  357. var status = $('.jsxc_webrtc .jsxc_status');
  358. var duration = (typeof d === 'undefined' || d === null) ? 4000 : d;
  359. jsxc.debug('[Webrtc]', txt);
  360. if (status.html()) {
  361. // attach old messages
  362. txt = status.html() + '<br />' + txt;
  363. }
  364. status.html(txt);
  365. status.css({
  366. 'margin-left': '-' + (status.width() / 2) + 'px',
  367. opacity: 0,
  368. display: 'block'
  369. });
  370. status.stop().animate({
  371. opacity: 1
  372. });
  373. clearTimeout(status.data('timeout'));
  374. if (duration === 0) {
  375. return;
  376. }
  377. var to = setTimeout(function() {
  378. status.stop().animate({
  379. opacity: 0
  380. }, function() {
  381. status.html('');
  382. });
  383. }, duration);
  384. status.data('timeout', to);
  385. },
  386. /**
  387. * Update "video" button if we receive cap information.
  388. *
  389. * @private
  390. * @memberOf jsxc.webrtc
  391. * @param event
  392. * @param jid
  393. */
  394. onCaps: function(event, jid) {
  395. var self = jsxc.webrtc;
  396. if (jsxc.gui.roster.loaded) {
  397. self.updateIcon(jsxc.jidToBid(jid));
  398. } else {
  399. $(document).on('cloaded.roster.jsxc', function() {
  400. self.updateIcon(jsxc.jidToBid(jid));
  401. });
  402. }
  403. },
  404. /**
  405. * Called if media failes.
  406. *
  407. * @private
  408. * @memberOf jsxc.webrtc
  409. */
  410. onMediaFailure: function(ev, err) {
  411. var self = jsxc.webrtc;
  412. var msg;
  413. err = err || {};
  414. self.setStatus('media failure');
  415. switch (err.name) {
  416. case 'NotAllowedError':
  417. case 'PERMISSION_DENIED':
  418. msg = $.t('PermissionDeniedError');
  419. break;
  420. case 'HTTPS_REQUIRED':
  421. case 'EXTENSION_UNAVAILABLE':
  422. msg = $.t(err.name);
  423. break;
  424. default:
  425. msg = $.t(err.name) !== err.name ? $.t(err.name) : $.t('UNKNOWN_ERROR');
  426. }
  427. jsxc.gui.window.postMessage({
  428. bid: jsxc.jidToBid(jsxc.webrtc.last_caller),
  429. direction: jsxc.Message.SYS,
  430. msg: $.t('Media_failure') + ': ' + msg + ' (' + err.name + ').'
  431. });
  432. jsxc.gui.dialog.close();
  433. jsxc.debug('media failure: ' + err.name);
  434. },
  435. /**
  436. * Process incoming jingle offer.
  437. *
  438. * @param {BaseSession} session
  439. */
  440. onIncoming: function(session) {
  441. var self = jsxc.webrtc;
  442. var type = (session.constructor) ? session.constructor.name : null;
  443. if (type === 'FileTransferSession') {
  444. self.onIncomingFileTransfer(session);
  445. } else if (type === 'MediaSession') {
  446. var reqMedia = false;
  447. $.each(session.pc.remoteDescription.contents, function() {
  448. if (this.senders === 'both') {
  449. reqMedia = true;
  450. }
  451. });
  452. session.call = reqMedia;
  453. if (reqMedia) {
  454. self.onIncomingCall(session);
  455. } else {
  456. self.onIncomingStream(session);
  457. }
  458. } else {
  459. jsxc.warn('Unknown session type.');
  460. }
  461. },
  462. /**
  463. * Process incoming stream offer.
  464. *
  465. * @param {MediaSession} session
  466. */
  467. onIncomingStream: function(session) {
  468. jsxc.debug('incoming stream from ' + session.peerID);
  469. var self = jsxc.webrtc;
  470. var bid = jsxc.jidToBid(session.peerID);
  471. session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
  472. self.postScreenMessage(bid, $.t('Incoming_stream'), session.sid);
  473. // display notification
  474. jsxc.notification.notify($.t('Incoming_stream'), $.t('from_sender', {
  475. sender: bid
  476. }));
  477. // send signal to partner
  478. session.ring();
  479. jsxc.webrtc.last_caller = session.peerID;
  480. if (jsxc.webrtc.AUTO_ACCEPT) {
  481. acceptIncomingStream(session);
  482. return;
  483. }
  484. var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
  485. noClose: true
  486. });
  487. dialog.find('.jsxc_accept').click(function() {
  488. $(document).trigger('accept.call.jsxc');
  489. acceptIncomingStream(session);
  490. });
  491. dialog.find('.jsxc_reject').click(function() {
  492. jsxc.gui.dialog.close();
  493. $(document).trigger('reject.call.jsxc');
  494. session.decline();
  495. });
  496. function acceptIncomingStream(session) {
  497. jsxc.gui.dialog.close();
  498. jsxc.gui.showVideoWindow(session.peerID);
  499. session.accept();
  500. }
  501. },
  502. /**
  503. * Process incoming file offer.
  504. *
  505. * @param {FileSession} session
  506. */
  507. onIncomingFileTransfer: function(session) {
  508. jsxc.debug('incoming file transfer from ' + session.peerID);
  509. var buddylist = jsxc.storage.getUserItem('buddylist') || [];
  510. var bid = jsxc.jidToBid(session.peerID);
  511. if (buddylist.indexOf(bid) > -1) {
  512. //Accept file transfers only from contacts
  513. session.accept();
  514. var message = jsxc.gui.window.postMessage({
  515. _uid: session.sid + ':msg',
  516. bid: bid,
  517. direction: jsxc.Message.IN,
  518. attachment: {
  519. name: session.receiver.metadata.name,
  520. type: session.receiver.metadata.type || 'application/octet-stream'
  521. }
  522. });
  523. session.receiver.on('progress', function(sent, size) {
  524. jsxc.gui.window.updateProgress(message, sent, size);
  525. });
  526. }
  527. },
  528. /**
  529. * Called on incoming call.
  530. *
  531. * @private
  532. * @memberOf jsxc.webrtc
  533. * @param {MediaSession} session
  534. */
  535. onIncomingCall: function(session) {
  536. jsxc.debug('incoming call from ' + session.peerID);
  537. var self = jsxc.webrtc;
  538. var bid = jsxc.jidToBid(session.peerID);
  539. session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
  540. self.postCallMessage(bid, $.t('Incoming_call'), session.sid);
  541. // display notification
  542. jsxc.notification.notify($.t('Incoming_call'), $.t('from_sender', {
  543. sender: bid
  544. }));
  545. // send signal to partner
  546. session.ring();
  547. jsxc.webrtc.last_caller = session.peerID;
  548. if (jsxc.webrtc.AUTO_ACCEPT) {
  549. self.acceptIncomingCall(session);
  550. return;
  551. }
  552. var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('incomingCall', bid), {
  553. noClose: true
  554. });
  555. dialog.find('.jsxc_accept').click(function() {
  556. self.acceptIncomingCall(session);
  557. });
  558. dialog.find('.jsxc_reject').click(function() {
  559. jsxc.gui.dialog.close();
  560. $(document).trigger('reject.call.jsxc');
  561. session.decline();
  562. });
  563. },
  564. /**
  565. * Called on incoming call.
  566. *
  567. * @private
  568. * @memberOf jsxc.webrtc
  569. * @param {MediaSession} session
  570. */
  571. acceptIncomingCall: function(session) {
  572. $(document).trigger('accept.call.jsxc');
  573. var self = jsxc.webrtc;
  574. jsxc.switchEvents({
  575. 'mediaready.jingle': function(ev, stream) {
  576. self.setStatus('Accept call');
  577. self.localStream = stream;
  578. self.conn.jingle.localStream = stream;
  579. var dialog = jsxc.gui.showVideoWindow(session.peerID);
  580. dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
  581. session.addStream(stream);
  582. session.accept();
  583. },
  584. 'mediafailure.jingle': function() {
  585. session.decline();
  586. }
  587. });
  588. self.reqUserMedia();
  589. },
  590. /**
  591. * Process jingle termination event.
  592. *
  593. * @param {BaseSession} session
  594. * @param {Object} reason Reason for termination
  595. */
  596. onTerminated: function(session, reason) {
  597. var self = jsxc.webrtc;
  598. var type = (session.constructor) ? session.constructor.name : null;
  599. if (type === 'MediaSession') {
  600. self.onCallTerminated(session, reason);
  601. }
  602. },
  603. /**
  604. * Called if call is terminated.
  605. *
  606. * @private
  607. * @memberOf jsxc.webrtc
  608. * @param {BaseSession} session
  609. * @param {Object} reason Reason for termination
  610. */
  611. onCallTerminated: function(session, reason) {
  612. var self = jsxc.webrtc;
  613. self.setStatus('call terminated ' + session.peerID + (reason && reason.condition ? reason.condition : ''));
  614. var bid = jsxc.jidToBid(session.peerID);
  615. if (self.localStream) {
  616. // stop local stream
  617. if (typeof self.localStream.getTracks === 'function') {
  618. var tracks = self.localStream.getTracks();
  619. tracks.forEach(function(track) {
  620. track.stop();
  621. });
  622. } else if (typeof self.localStream.stop === 'function') {
  623. self.localStream.stop();
  624. } else {
  625. jsxc.warn('Could not stop local stream');
  626. }
  627. }
  628. // @REVIEW necessary?
  629. if ($('.jsxc_remotevideo').length) {
  630. $('.jsxc_remotevideo')[0].src = "";
  631. }
  632. if ($('.jsxc_localvideo').length) {
  633. $('.jsxc_localvideo')[0].src = "";
  634. }
  635. self.conn.jingle.localStream = null;
  636. self.localStream = null;
  637. self.remoteStream = null;
  638. jsxc.gui.closeVideoWindow();
  639. // Close incoming call dialog and stop ringing
  640. jsxc.gui.dialog.close();
  641. $(document).trigger('reject.call.jsxc');
  642. $(document).off('error.jingle');
  643. var msg = (reason && reason.condition ? (': ' + $.t('jingle_reason_' + reason.condition)) : '') + '.';
  644. if (session.call) {
  645. msg = $.t('Call_terminated') + msg;
  646. jsxc.webrtc.postCallMessage(bid, msg, session.sid);
  647. } else {
  648. msg = $.t('Stream_terminated') + msg;
  649. jsxc.webrtc.postScreenMessage(bid, msg, session.sid);
  650. }
  651. },
  652. /**
  653. * Remote station is ringing.
  654. *
  655. * @private
  656. * @memberOf jsxc.webrtc
  657. */
  658. onCallRinging: function() {
  659. this.setStatus('ringing...', 0);
  660. $('.jsxc_videoContainer').removeClass('jsxc_establishing').addClass('jsxc_ringing');
  661. },
  662. /**
  663. * Called if we receive a remote stream.
  664. *
  665. * @private
  666. * @memberOf jsxc.webrtc
  667. * @param {BaseSession} session
  668. * @param {Object} stream
  669. */
  670. onRemoteStreamAdded: function(session, stream) {
  671. var self = jsxc.webrtc;
  672. self.setStatus('Remote stream for session ' + session.sid + ' added.');
  673. self.remoteStream = stream;
  674. var isVideoDevice = stream.getVideoTracks().length > 0;
  675. var isAudioDevice = stream.getAudioTracks().length > 0;
  676. self.setStatus(isVideoDevice ? 'Use remote video device.' : 'No remote video device');
  677. self.setStatus(isAudioDevice ? 'Use remote audio device.' : 'No remote audio device');
  678. if ($('.jsxc_remotevideo').length) {
  679. self.attachMediaStream($('#jsxc_webrtc .jsxc_remotevideo'), stream);
  680. $('#jsxc_webrtc .jsxc_' + (isVideoDevice ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
  681. }
  682. },
  683. /**
  684. * Attach media stream to element.
  685. *
  686. * @memberOf jsxc.webrtc
  687. * @param element {Element|jQuery}
  688. * @param stream {mediastream}
  689. */
  690. attachMediaStream: function(element, stream) {
  691. var el = (element instanceof jQuery) ? element.get(0) : element;
  692. el.srcObject = stream;
  693. $(element).show();
  694. },
  695. /**
  696. * Called if the remote stream was removed.
  697. *
  698. * @private
  699. * @meberOf jsxc.webrtc
  700. * @param {BaseSession} session
  701. */
  702. onRemoteStreamRemoved: function(session) {
  703. this.setStatus('Remote stream for ' + session.jid + ' removed.');
  704. //@TODO clean up
  705. },
  706. /**
  707. * Display information according to the connection state.
  708. *
  709. * @private
  710. * @memberOf jsxc.webrtc
  711. * @param {BaseSession} session
  712. * @param {String} state
  713. */
  714. onIceConnectionStateChanged: function(session, state) {
  715. var self = jsxc.webrtc;
  716. jsxc.debug('connection state for ' + session.sid, state);
  717. if (state === 'connected') {
  718. $('#jsxc_webrtc .jsxc_deviceAvailable').show();
  719. } else if (state === 'failed') {
  720. jsxc.gui.window.postMessage({
  721. bid: jsxc.jidToBid(session.peerID),
  722. direction: jsxc.Message.SYS,
  723. msg: $.t('ICE_connection_failure')
  724. });
  725. session.end('failed-transport');
  726. $(document).trigger('callterminated.jingle');
  727. } else if (state === 'interrupted') {
  728. self.setStatus($.t('Connection_interrupted'));
  729. }
  730. },
  731. /**
  732. * Start a call to the specified jid.
  733. *
  734. * @memberOf jsxc.webrtc
  735. * @param {String} jid full jid
  736. * @param {String[]} um requested user media
  737. */
  738. startCall: function(jid, um) {
  739. var self = jsxc.webrtc;
  740. if (Strophe.getResourceFromJid(jid) === null) {
  741. jsxc.debug('We need a full jid');
  742. return;
  743. }
  744. self.last_caller = jid;
  745. jsxc.switchEvents({
  746. 'mediaready.jingle': function(ev, stream) {
  747. jsxc.debug('media ready for outgoing call');
  748. self.initiateOutgoingCall(jid, stream);
  749. },
  750. 'mediafailure.jingle': function() {
  751. jsxc.gui.dialog.close();
  752. }
  753. });
  754. self.reqUserMedia(um);
  755. },
  756. /**
  757. * Start jingle session to jid with stream.
  758. *
  759. * @param {String} jid
  760. * @param {Object} stream
  761. */
  762. initiateOutgoingCall: function(jid, stream) {
  763. var self = jsxc.webrtc;
  764. self.localStream = stream;
  765. self.conn.jingle.localStream = stream;
  766. var dialog = jsxc.gui.showVideoWindow(jid);
  767. dialog.find('.jsxc_videoContainer').addClass('jsxc_establishing');
  768. self.setStatus('Initiate call');
  769. // @REVIEW session based?
  770. $(document).one('error.jingle', function(ev, sid, error) {
  771. if (error && error.source !== 'offer') {
  772. return;
  773. }
  774. setTimeout(function() {
  775. jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
  776. }, 500);
  777. });
  778. var session = self.conn.jingle.initiate(jid);
  779. // flag session as call
  780. session.call = true;
  781. session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
  782. self.postCallMessage(jsxc.jidToBid(jid), $.t('Call_started'), session.sid);
  783. },
  784. /**
  785. * Hang up the current call.
  786. *
  787. * @memberOf jsxc.webrtc
  788. */
  789. hangUp: function(reason, text) {
  790. if (jsxc.webrtc.conn.jingle.manager && !$.isEmptyObject(jsxc.webrtc.conn.jingle.manager.peers)) {
  791. jsxc.webrtc.conn.jingle.terminate(null, reason, text);
  792. } else {
  793. jsxc.gui.closeVideoWindow();
  794. }
  795. // @TODO check event
  796. $(document).trigger('callterminated.jingle');
  797. },
  798. /**
  799. * Start outgoing screen sharing session.
  800. *
  801. * @param {String} jid
  802. */
  803. startScreenSharing: function(jid) {
  804. var self = this;
  805. if (Strophe.getResourceFromJid(jid) === null) {
  806. jsxc.debug('We need a full jid');
  807. return;
  808. }
  809. self.last_caller = jid;
  810. jsxc.switchEvents({
  811. 'mediaready.jingle': function(ev, stream) {
  812. self.initiateScreenSharing(jid, stream);
  813. },
  814. 'mediafailure.jingle': function(ev, err) {
  815. jsxc.gui.dialog.close();
  816. var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
  817. var screenMediaExtension = jsxc.options.get('screenMediaExtension') || {};
  818. if (screenMediaExtension[browser] &&
  819. (err.name === 'EXTENSION_UNAVAILABLE' || (err.name === 'NotAllowedError' && browser === 'firefox'))) {
  820. // post download link after explanation
  821. setTimeout(function() {
  822. jsxc.gui.window.postMessage({
  823. bid: jsxc.jidToBid(jid),
  824. direction: jsxc.Message.SYS,
  825. msg: $.t('Install_extension') + screenMediaExtension[browser]
  826. });
  827. }, 500);
  828. }
  829. }
  830. });
  831. self.reqUserMedia(['screen']);
  832. },
  833. /**
  834. * Initiate outgoing (one-way) jingle session to jid with stream.
  835. *
  836. * @param {String} jid
  837. * @param {Object} stream
  838. */
  839. initiateScreenSharing: function(jid, stream) {
  840. var self = jsxc.webrtc;
  841. var bid = jsxc.jidToBid(jid);
  842. jsxc.webrtc.localStream = stream;
  843. jsxc.webrtc.conn.jingle.localStream = stream;
  844. var container = jsxc.gui.showMinimizedVideoWindow();
  845. container.addClass('jsxc_establishing');
  846. self.setStatus('Initiate stream');
  847. $(document).one('error.jingle', function(e, sid, error) {
  848. if (error && error.source !== 'offer') {
  849. return;
  850. }
  851. setTimeout(function() {
  852. jsxc.gui.showAlert("Sorry, we couldn't establish a connection. Maybe your buddy is offline.");
  853. }, 500);
  854. });
  855. var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
  856. var browserVersion = self.conn.jingle.RTC.webrtcDetectedVersion;
  857. var constraints;
  858. if ((browserVersion < 33 && browser === 'firefox') || browser === 'chrome') {
  859. constraints = {
  860. mandatory: {
  861. 'OfferToReceiveAudio': false,
  862. 'OfferToReceiveVideo': false
  863. }
  864. };
  865. } else {
  866. constraints = {
  867. 'offerToReceiveAudio': false,
  868. 'offerToReceiveVideo': false
  869. };
  870. }
  871. var session = self.conn.jingle.initiate(jid, undefined, constraints);
  872. session.call = false;
  873. session.on('change:connectionState', $.proxy(self.onIceConnectionStateChanged, self));
  874. // @REVIEW also for calls?
  875. session.on('accepted', function() {
  876. self.onSessionAccepted(session);
  877. });
  878. self.postScreenMessage(bid, $.t('Stream_started'), session.sid);
  879. },
  880. /**
  881. * Session was accepted by other peer.
  882. *
  883. * @param {BaseSession} session
  884. */
  885. onSessionAccepted: function(session) {
  886. var self = jsxc.webrtc;
  887. $('.jsxc_videoContainer').removeClass('jsxc_ringing');
  888. self.postScreenMessage(jsxc.jidToBid(session.peerID), $.t('Connection_accepted'), session.sid);
  889. },
  890. /**
  891. * Request media from local user.
  892. *
  893. * @memberOf jsxc.webrtc
  894. */
  895. reqUserMedia: function(um) {
  896. if (this.localStream) {
  897. $(document).trigger('mediaready.jingle', [this.localStream]);
  898. return;
  899. }
  900. um = um || ['video', 'audio'];
  901. jsxc.gui.dialog.open(jsxc.gui.template.get('allowMediaAccess'), {
  902. noClose: true
  903. });
  904. if (um.indexOf('screen') >= 0) {
  905. jsxc.webrtc.getScreenMedia();
  906. } else if (typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' &&
  907. typeof navigator.mediaDevices.enumerateDevices !== 'undefined') {
  908. navigator.mediaDevices.enumerateDevices()
  909. .then(filterUserMedia)
  910. .catch(function(err) {
  911. jsxc.warn(err.name + ": " + err.message);
  912. });
  913. } else if (typeof MediaStreamTrack !== 'undefined' && typeof MediaStreamTrack.getSources !== 'undefined') {
  914. // @deprecated in chrome since v56
  915. MediaStreamTrack.getSources(filterUserMedia);
  916. } else {
  917. jsxc.webrtc.getUserMedia(um);
  918. }
  919. function filterUserMedia(devices) {
  920. var availableDevices = devices.map(function(device) {
  921. return device.kind;
  922. });
  923. um = um.filter(function(el) {
  924. return availableDevices.indexOf(el) !== -1 || availableDevices.indexOf(el + 'input') !== -1;
  925. });
  926. if (um.length) {
  927. jsxc.webrtc.getUserMedia(um);
  928. } else {
  929. jsxc.warn('No audio/video device available.');
  930. }
  931. }
  932. },
  933. /**
  934. * Get user media from local browser.
  935. *
  936. * @memberOf jsxc.webrtc
  937. */
  938. getUserMedia: function(um) {
  939. var self = jsxc.webrtc;
  940. var constraints = {};
  941. if (um.indexOf('video') > -1) {
  942. constraints.video = true;
  943. }
  944. if (um.indexOf('audio') > -1) {
  945. constraints.audio = true;
  946. }
  947. try {
  948. self.conn.jingle.getUserMedia(constraints, self.userMediaCallback);
  949. } catch (e) {
  950. jsxc.error('GUM failed: ', e);
  951. $(document).trigger('mediafailure.jingle');
  952. }
  953. },
  954. userMediaCallback: function(err, stream) {
  955. if (err) {
  956. jsxc.warn('Failed to get access to local media. Error ', err);
  957. $(document).trigger('mediafailure.jingle', [err]);
  958. } else if (stream) {
  959. jsxc.debug('onUserMediaSuccess');
  960. $(document).trigger('mediaready.jingle', [stream]);
  961. }
  962. },
  963. /**
  964. * Get screen media from local browser.
  965. *
  966. * @memberOf jsxc.webrtc
  967. */
  968. getScreenMedia: function() {
  969. var self = jsxc.webrtc;
  970. jsxc.debug('get screen media');
  971. self.conn.jingle.getScreenMedia(self.screenMediaCallback);
  972. },
  973. screenMediaCallback: function(err, stream) {
  974. if (err) {
  975. $(document).trigger('mediafailure.jingle', [err]);
  976. return;
  977. }
  978. if (stream) {
  979. jsxc.debug('onScreenMediaSuccess');
  980. $(document).trigger('mediaready.jingle', [stream]);
  981. }
  982. },
  983. screenMediaAvailable: function() {
  984. var self = jsxc.webrtc;
  985. var browser = self.conn.jingle.RTC.webrtcDetectedBrowser;
  986. // test if chrome extension for this domain is available
  987. var chrome = !!sessionStorage.getScreenMediaJSExtensionId && browser === 'chrome';
  988. // the ff extension from {@link https://github.com/otalk/getScreenMedia}
  989. // does not provide any possibility to determine if it is installed or not.
  990. // Starting with Firefox 52 {@link https://www.mozilla.org/en-US/firefox/52.0a2/auroranotes/}
  991. // no extension is needed anyway.
  992. var firefox = browser === 'firefox';
  993. return chrome || firefox;
  994. },
  995. /**
  996. * Make a snapshot from a video stream and display it.
  997. *
  998. * @memberOf jsxc.webrtc
  999. * @param video Video stream
  1000. */
  1001. snapshot: function(video) {
  1002. if (!video) {
  1003. jsxc.debug('Missing video element');
  1004. }
  1005. $('.jsxc_snapshotbar p').remove();
  1006. var canvas = $('<canvas/>').css('display', 'none').appendTo('body').attr({
  1007. width: video.width(),
  1008. height: video.height()
  1009. }).get(0);
  1010. var ctx = canvas.getContext('2d');
  1011. ctx.drawImage(video[0], 0, 0);
  1012. var img = $('<img/>');
  1013. var url = null;
  1014. try {
  1015. url = canvas.toDataURL('image/jpeg');
  1016. } catch (err) {
  1017. jsxc.warn('Error', err);
  1018. return;
  1019. }
  1020. img[0].src = url;
  1021. var link = $('<a/>').attr({
  1022. target: '_blank',
  1023. href: url
  1024. });
  1025. link.append(img);
  1026. $('.jsxc_snapshotbar').append(link);
  1027. canvas.remove();
  1028. },
  1029. /**
  1030. * Send file to full jid via jingle.
  1031. *
  1032. * @memberOf jsxc.webrtc
  1033. * @param {string} jid full jid
  1034. * @param {file} file
  1035. * @return {object} session
  1036. */
  1037. sendFile: function(jid, file) {
  1038. jsxc.debug('Send file via webrtc');
  1039. var self = jsxc.webrtc;
  1040. if (!Strophe.getResourceFromJid(jid)) {
  1041. jsxc.warn('Require full jid to send file via webrtc');
  1042. return;
  1043. }
  1044. var sess = self.conn.jingle.manager.createFileTransferSession(jid);
  1045. sess.on('change:sessionState', function() {
  1046. jsxc.debug('Session state', sess.state);
  1047. });
  1048. sess.on('change:connectionState', function() {
  1049. jsxc.debug('Connection state', sess.connectionState);
  1050. });
  1051. sess.start(file);
  1052. return sess;
  1053. },
  1054. /**
  1055. * Display received file.
  1056. *
  1057. * @memberOf jsxc.webrtc
  1058. * @param {object} sess
  1059. * @param {File} file
  1060. * @param {object} metadata file metadata
  1061. */
  1062. onReceivedFile: function(sess, file, metadata) {
  1063. jsxc.debug('file received', metadata);
  1064. if (!FileReader) {
  1065. return;
  1066. }
  1067. var reader = new FileReader();
  1068. var type;
  1069. if (!metadata.type) {
  1070. // detect file type via file extension, because XEP-0234 v0.14
  1071. // does not send any type
  1072. var ext = metadata.name.replace(/.+\.([a-z0-9]+)$/i, '$1').toLowerCase();
  1073. switch (ext) {
  1074. case 'jpg':
  1075. case 'jpeg':
  1076. case 'png':
  1077. case 'gif':
  1078. case 'svg':
  1079. type = 'image/' + ext.replace(/^jpg$/, 'jpeg');
  1080. break;
  1081. case 'mp3':
  1082. case 'wav':
  1083. type = 'audio/' + ext;
  1084. break;
  1085. case 'pdf':
  1086. type = 'application/pdf';
  1087. break;
  1088. case 'txt':
  1089. type = 'text/' + ext;
  1090. break;
  1091. default:
  1092. type = 'application/octet-stream';
  1093. }
  1094. } else {
  1095. type = metadata.type;
  1096. }
  1097. reader.onload = function(ev) {
  1098. // modify element with uid metadata.actualhash
  1099. jsxc.gui.window.postMessage({
  1100. _uid: sess.sid + ':msg',
  1101. bid: jsxc.jidToBid(sess.peerID),
  1102. direction: jsxc.Message.IN,
  1103. attachment: {
  1104. name: metadata.name,
  1105. type: type,
  1106. size: metadata.size,
  1107. data: ev.target.result
  1108. }
  1109. });
  1110. };
  1111. if (!file.type) {
  1112. // file type should be handled in lib
  1113. file = new File([file], metadata.name, {
  1114. type: type
  1115. });
  1116. }
  1117. reader.readAsDataURL(file);
  1118. }
  1119. };
  1120. jsxc.webrtc.postCallMessage = function(bid, msg, uid) {
  1121. jsxc.gui.window.postMessage({
  1122. _uid: uid,
  1123. bid: bid,
  1124. direction: jsxc.Message.SYS,
  1125. msg: ':telephone_receiver: ' + msg
  1126. });
  1127. };
  1128. jsxc.webrtc.postScreenMessage = function(bid, msg, uid) {
  1129. jsxc.gui.window.postMessage({
  1130. _uid: uid,
  1131. bid: bid,
  1132. direction: jsxc.Message.SYS,
  1133. msg: ':computer: ' + msg
  1134. });
  1135. };
  1136. jsxc.gui.showMinimizedVideoWindow = function() {
  1137. var self = jsxc.webrtc;
  1138. // needed to trigger complete.dialog.jsxc
  1139. jsxc.gui.dialog.close();
  1140. var videoContainer = $('<div/>');
  1141. videoContainer.addClass('jsxc_videoContainer jsxc_minimized');
  1142. videoContainer.appendTo('body');
  1143. videoContainer.draggable({
  1144. containment: "parent"
  1145. });
  1146. var videoElement = $('<video class="jsxc_localvideo" autoplay=""></video>');
  1147. videoElement.appendTo(videoContainer);
  1148. videoElement[0].muted = true;
  1149. videoElement[0].volume = 0;
  1150. if (self.localStream) {
  1151. self.attachMediaStream(videoElement, self.localStream);
  1152. }
  1153. videoContainer.append('<div class="jsxc_controlbar"><div><div class="jsxc_hangUp jsxc_videoControl"></div></div></div></div>');
  1154. videoContainer.find('.jsxc_hangUp').click(function() {
  1155. jsxc.webrtc.hangUp('success');
  1156. });
  1157. videoContainer.click(function() {
  1158. videoContainer.find('.jsxc_controlbar').toggleClass('jsxc_visible');
  1159. });
  1160. return videoContainer;
  1161. };
  1162. /**
  1163. * Display window for video call.
  1164. *
  1165. * @memberOf jsxc.gui
  1166. */
  1167. jsxc.gui.showVideoWindow = function(jid) {
  1168. var self = jsxc.webrtc;
  1169. // needed to trigger complete.dialog.jsxc
  1170. jsxc.gui.dialog.close();
  1171. $('body').append(jsxc.gui.template.get('videoWindow'));
  1172. // mute own video element to avoid echoes
  1173. $('#jsxc_webrtc .jsxc_localvideo')[0].muted = true;
  1174. $('#jsxc_webrtc .jsxc_localvideo')[0].volume = 0;
  1175. var rv = $('#jsxc_webrtc .jsxc_remotevideo');
  1176. var lv = $('#jsxc_webrtc .jsxc_localvideo');
  1177. lv.draggable({
  1178. containment: "parent"
  1179. });
  1180. if (self.localStream) {
  1181. self.attachMediaStream(lv, self.localStream);
  1182. }
  1183. var w_dialog = $('#jsxc_webrtc').width();
  1184. var w_remote = rv.width();
  1185. // fit in video
  1186. if (w_remote > w_dialog) {
  1187. var scale = w_dialog / w_remote;
  1188. var new_h = rv.height() * scale;
  1189. var new_w = w_dialog;
  1190. var vc = $('#jsxc_webrtc .jsxc_videoContainer');
  1191. rv.height(new_h);
  1192. rv.width(new_w);
  1193. vc.height(new_h);
  1194. vc.width(new_w);
  1195. lv.height(lv.height() * scale);
  1196. lv.width(lv.width() * scale);
  1197. }
  1198. if (self.remoteStream) {
  1199. self.attachMediaStream(rv, self.remoteStream);
  1200. $('#jsxc_webrtc .jsxc_' + (self.remoteStream.getVideoTracks().length > 0 ? 'remotevideo' : 'noRemoteVideo')).addClass('jsxc_deviceAvailable');
  1201. }
  1202. var win = jsxc.gui.window.open(jsxc.jidToBid(jid));
  1203. win.find('.slimScrollDiv').resizable('disable');
  1204. jsxc.gui.window.resize(win, {
  1205. size: {
  1206. width: $('#jsxc_webrtc .jsxc_chatarea').width(),
  1207. height: $('#jsxc_webrtc .jsxc_chatarea').height()
  1208. }
  1209. }, true);
  1210. $('#jsxc_webrtc .jsxc_chatarea ul').append(win.detach());
  1211. $('#jsxc_webrtc .jsxc_hangUp').click(function() {
  1212. jsxc.webrtc.hangUp('success');
  1213. });
  1214. $('#jsxc_webrtc .jsxc_fullscreen').click(function() {
  1215. if ($.support.fullscreen) {
  1216. // Reset position of localvideo
  1217. $(document).one('disabled.fullscreen', function() {
  1218. lv.removeAttr('style');
  1219. });
  1220. $('#jsxc_webrtc .jsxc_videoContainer').fullscreen();
  1221. }
  1222. });
  1223. $('#jsxc_webrtc .jsxc_videoContainer').click(function() {
  1224. $('#jsxc_webrtc .jsxc_controlbar').toggleClass('jsxc_visible');
  1225. });
  1226. return $('#jsxc_webrtc');
  1227. };
  1228. jsxc.gui.closeVideoWindow = function() {
  1229. var win = $('#jsxc_webrtc .jsxc_chatarea > ul > li');
  1230. if (win.length > 0) {
  1231. $('#jsxc_windowList > ul').prepend(win.detach());
  1232. win.find('.slimScrollDiv').resizable('enable');
  1233. jsxc.gui.window.resize(win);
  1234. }
  1235. $('#jsxc_webrtc, .jsxc_videoContainer').remove();
  1236. };
  1237. $.extend(jsxc.CONST, {
  1238. KEYCODE_ENTER: 13,
  1239. KEYCODE_ESC: 27
  1240. });
  1241. $(document).ready(function() {
  1242. $(document).on('init.window.jsxc', jsxc.webrtc.initWindow);
  1243. $(document).on('attached.jsxc', jsxc.webrtc.init);
  1244. $(document).on('disconnected.jsxc', jsxc.webrtc.onDisconnected);
  1245. $(document).on('connected.jsxc', jsxc.webrtc.onConnected);
  1246. });