You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.js 51KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570
  1. /* jshint -W117 */
  2. /* application specific logic */
  3. var connection = null;
  4. var focus = null;
  5. var activecall = null;
  6. var RTC = null;
  7. var nickname = null;
  8. var sharedKey = '';
  9. var roomUrl = null;
  10. var ssrc2jid = {};
  11. /**
  12. * Indicates whether ssrc is camera video or desktop stream.
  13. * FIXME: remove those maps
  14. */
  15. var ssrc2videoType = {};
  16. var videoSrcToSsrc = {};
  17. var localVideoSrc = null;
  18. var flipXLocalVideo = true;
  19. var isFullScreen = false;
  20. var toolbarTimeout = null;
  21. var currentVideoWidth = null;
  22. var currentVideoHeight = null;
  23. /**
  24. * Method used to calculate large video size.
  25. * @type {function()}
  26. */
  27. var getVideoSize;
  28. /* window.onbeforeunload = closePageWarning; */
  29. function init() {
  30. RTC = setupRTC();
  31. if (RTC === null) {
  32. window.location.href = 'webrtcrequired.html';
  33. return;
  34. } else if (RTC.browser !== 'chrome') {
  35. window.location.href = 'chromeonly.html';
  36. return;
  37. }
  38. connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
  39. if (nickname) {
  40. connection.emuc.addDisplayNameToPresence(nickname);
  41. }
  42. if (connection.disco) {
  43. // for chrome, add multistream cap
  44. }
  45. connection.jingle.pc_constraints = RTC.pc_constraints;
  46. if (config.useIPv6) {
  47. // https://code.google.com/p/webrtc/issues/detail?id=2828
  48. if (!connection.jingle.pc_constraints.optional) connection.jingle.pc_constraints.optional = [];
  49. connection.jingle.pc_constraints.optional.push({googIPv6: true});
  50. }
  51. var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
  52. connection.connect(jid, document.getElementById('password').value, function (status) {
  53. if (status === Strophe.Status.CONNECTED) {
  54. console.log('connected');
  55. if (config.useStunTurn) {
  56. connection.jingle.getStunAndTurnCredentials();
  57. }
  58. obtainAudioAndVideoPermissions(function(){
  59. getUserMediaWithConstraints( ['audio'], audioStreamReady,
  60. function(error){
  61. console.error('failed to obtain audio stream - stop', error);
  62. });
  63. });
  64. document.getElementById('connect').disabled = true;
  65. } else {
  66. console.log('status', status);
  67. }
  68. });
  69. }
  70. /**
  71. * HTTPS only:
  72. * We first ask for audio and video combined stream in order to get permissions and not to ask twice.
  73. * Then we dispose the stream and continue with separate audio, video streams(required for desktop sharing).
  74. */
  75. function obtainAudioAndVideoPermissions(callback){
  76. // This makes sense only on https sites otherwise we'll be asked for permissions every time
  77. if(location.protocol !== 'https:') {
  78. callback();
  79. return;
  80. }
  81. // Get AV
  82. getUserMediaWithConstraints(
  83. ['audio', 'video'],
  84. function(avStream) {
  85. avStream.stop();
  86. callback();
  87. },
  88. function(error){
  89. console.error('failed to obtain audio/video stream - stop', error);
  90. });
  91. }
  92. function audioStreamReady(stream) {
  93. change_local_audio(stream);
  94. if(RTC.browser !== 'firefox') {
  95. getUserMediaWithConstraints( ['video'], videoStreamReady, videoStreamFailed, config.resolution || '360' );
  96. } else {
  97. doJoin();
  98. }
  99. }
  100. function videoStreamReady(stream) {
  101. change_local_video(stream, true);
  102. doJoin();
  103. }
  104. function videoStreamFailed(error) {
  105. console.warn("Failed to obtain video stream - continue anyway", error);
  106. doJoin();
  107. }
  108. function doJoin() {
  109. var roomnode = null;
  110. var path = window.location.pathname;
  111. var roomjid;
  112. // determinde the room node from the url
  113. // TODO: just the roomnode or the whole bare jid?
  114. if (config.getroomnode && typeof config.getroomnode === 'function') {
  115. // custom function might be responsible for doing the pushstate
  116. roomnode = config.getroomnode(path);
  117. } else {
  118. /* fall back to default strategy
  119. * this is making assumptions about how the URL->room mapping happens.
  120. * It currently assumes deployment at root, with a rewrite like the
  121. * following one (for nginx):
  122. location ~ ^/([a-zA-Z0-9]+)$ {
  123. rewrite ^/(.*)$ / break;
  124. }
  125. */
  126. if (path.length > 1) {
  127. roomnode = path.substr(1).toLowerCase();
  128. } else {
  129. roomnode = Math.random().toString(36).substr(2, 20);
  130. window.history.pushState('VideoChat',
  131. 'Room: ' + roomnode, window.location.pathname + roomnode);
  132. }
  133. }
  134. roomjid = roomnode + '@' + config.hosts.muc;
  135. if (config.useNicks) {
  136. var nick = window.prompt('Your nickname (optional)');
  137. if (nick) {
  138. roomjid += '/' + nick;
  139. } else {
  140. roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
  141. }
  142. } else {
  143. roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0,8);
  144. }
  145. connection.emuc.doJoin(roomjid);
  146. }
  147. function change_local_audio(stream) {
  148. connection.jingle.localAudio = stream;
  149. RTC.attachMediaStream($('#localAudio'), stream);
  150. document.getElementById('localAudio').autoplay = true;
  151. document.getElementById('localAudio').volume = 0;
  152. }
  153. function change_local_video(stream, flipX) {
  154. connection.jingle.localVideo = stream;
  155. var localVideo = document.createElement('video');
  156. localVideo.id = 'localVideo_'+stream.id;
  157. localVideo.autoplay = true;
  158. localVideo.volume = 0; // is it required if audio is separated ?
  159. localVideo.oncontextmenu = function () { return false; };
  160. var localVideoContainer = document.getElementById('localVideoWrapper');
  161. localVideoContainer.appendChild(localVideo);
  162. var localVideoSelector = $('#' + localVideo.id);
  163. // Add click handler
  164. localVideoSelector.click(function () { handleVideoThumbClicked(localVideo.src); } );
  165. // Add stream ended handler
  166. stream.onended = function () {
  167. localVideoContainer.removeChild(localVideo);
  168. checkChangeLargeVideo(localVideo.src);
  169. };
  170. // Flip video x axis if needed
  171. flipXLocalVideo = flipX;
  172. if(flipX) {
  173. localVideoSelector.addClass("flipVideoX");
  174. }
  175. // Attach WebRTC stream
  176. RTC.attachMediaStream(localVideoSelector, stream);
  177. localVideoSrc = localVideo.src;
  178. updateLargeVideo(localVideoSrc, 0);
  179. }
  180. $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
  181. function waitForRemoteVideo(selector, sid, ssrc) {
  182. if(selector.removed) {
  183. console.warn("media removed before had started", selector);
  184. return;
  185. }
  186. var sess = connection.jingle.sessions[sid];
  187. if (data.stream.id === 'mixedmslabel') return;
  188. var videoTracks = data.stream.getVideoTracks();
  189. console.log("waiting..", videoTracks, selector[0]);
  190. if (videoTracks.length === 0 || selector[0].currentTime > 0) {
  191. RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
  192. // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
  193. // in order to get rid of too many maps
  194. if(ssrc) {
  195. videoSrcToSsrc[sel.attr('src')] = ssrc;
  196. } else {
  197. console.warn("No ssrc given for video", sel);
  198. }
  199. $(document).trigger('callactive.jingle', [selector, sid]);
  200. console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
  201. } else {
  202. setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250);
  203. }
  204. }
  205. var sess = connection.jingle.sessions[sid];
  206. var thessrc;
  207. // look up an associated JID for a stream id
  208. if (data.stream.id.indexOf('mixedmslabel') === -1) {
  209. var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
  210. ssrclines = ssrclines.filter(function (line) {
  211. return line.indexOf('mslabel:' + data.stream.label) !== -1;
  212. });
  213. if (ssrclines.length) {
  214. thessrc = ssrclines[0].substring(7).split(' ')[0];
  215. // ok to overwrite the one from focus? might save work in colibri.js
  216. console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
  217. if (ssrc2jid[thessrc]) {
  218. data.peerjid = ssrc2jid[thessrc];
  219. }
  220. }
  221. }
  222. var container;
  223. var remotes = document.getElementById('remoteVideos');
  224. if (data.peerjid) {
  225. container = document.getElementById(
  226. 'participant_' + Strophe.getResourceFromJid(data.peerjid));
  227. if (!container) {
  228. console.warn('no container for', data.peerjid);
  229. // create for now...
  230. // FIXME: should be removed
  231. container = addRemoteVideoContainer(
  232. 'participant_' + Strophe.getResourceFromJid(data.peerjid));
  233. } else {
  234. //console.log('found container for', data.peerjid);
  235. }
  236. } else {
  237. if (data.stream.id !== 'mixedmslabel') {
  238. console.error('can not associate stream', data.stream.id, 'with a participant');
  239. // We don't want to add it here since it will cause troubles
  240. return;
  241. }
  242. // FIXME: for the mixed ms we dont need a video -- currently
  243. container = document.createElement('span');
  244. container.className = 'videocontainer';
  245. remotes.appendChild(container);
  246. Util.playSoundNotification('userJoined');
  247. }
  248. var isVideo = data.stream.getVideoTracks().length > 0;
  249. var vid = isVideo ? document.createElement('video') : document.createElement('audio');
  250. var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;
  251. vid.id = id;
  252. vid.autoplay = true;
  253. vid.oncontextmenu = function () { return false; };
  254. container.appendChild(vid);
  255. // TODO: make mixedstream display:none via css?
  256. if (id.indexOf('mixedmslabel') !== -1) {
  257. container.id = 'mixedstream';
  258. $(container).hide();
  259. }
  260. var sel = $('#' + id);
  261. sel.hide();
  262. RTC.attachMediaStream(sel, data.stream);
  263. if(isVideo) {
  264. waitForRemoteVideo(sel, sid, thessrc);
  265. }
  266. data.stream.onended = function () {
  267. console.log('stream ended', this.id);
  268. // Mark video as removed to cancel waiting loop(if video is removed before has started)
  269. sel.removed = true;
  270. sel.remove();
  271. var audioCount = $('#'+container.id+'>audio').length;
  272. var videoCount = $('#'+container.id+'>video').length;
  273. if(!audioCount && !videoCount) {
  274. console.log("Remove whole user");
  275. // Remove whole container
  276. container.remove();
  277. Util.playSoundNotification('userLeft');
  278. resizeThumbnails();
  279. }
  280. checkChangeLargeVideo(vid.src);
  281. };
  282. // Add click handler
  283. sel.click(function () { handleVideoThumbClicked(vid.src); });
  284. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  285. if (isVideo
  286. && data.peerjid && sess.peerjid === data.peerjid &&
  287. data.stream.getVideoTracks().length === 0 &&
  288. connection.jingle.localVideo.getVideoTracks().length > 0) {
  289. window.setTimeout(function() {
  290. sendKeyframe(sess.peerconnection);
  291. }, 3000);
  292. }
  293. });
  294. function handleVideoThumbClicked(videoSrc) {
  295. $(document).trigger("video.selected", [false]);
  296. updateLargeVideo(videoSrc, 1);
  297. $('audio').each(function (idx, el) {
  298. if (el.id.indexOf('mixedmslabel') !== -1) {
  299. el.volume = 0;
  300. el.volume = 1;
  301. }
  302. });
  303. }
  304. /**
  305. * Checks if removed video is currently displayed and tries to display another one instead.
  306. * @param removedVideoSrc src stream identifier of the video.
  307. */
  308. function checkChangeLargeVideo(removedVideoSrc){
  309. if (removedVideoSrc === $('#largeVideo').attr('src')) {
  310. // this is currently displayed as large
  311. // pick the last visible video in the row
  312. // if nobody else is left, this picks the local video
  313. var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
  314. if(!pick) {
  315. console.info("Last visible video no longer exists");
  316. pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
  317. }
  318. // mute if localvideo
  319. if (pick) {
  320. updateLargeVideo(pick.src, pick.volume);
  321. } else {
  322. console.warn("Failed to elect large video");
  323. }
  324. }
  325. }
  326. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  327. function sendKeyframe(pc) {
  328. console.log('sendkeyframe', pc.iceConnectionState);
  329. if (pc.iceConnectionState !== 'connected') return; // safe...
  330. pc.setRemoteDescription(
  331. pc.remoteDescription,
  332. function () {
  333. pc.createAnswer(
  334. function (modifiedAnswer) {
  335. pc.setLocalDescription(modifiedAnswer,
  336. function () {
  337. },
  338. function (error) {
  339. console.log('triggerKeyframe setLocalDescription failed', error);
  340. }
  341. );
  342. },
  343. function (error) {
  344. console.log('triggerKeyframe createAnswer failed', error);
  345. }
  346. );
  347. },
  348. function (error) {
  349. console.log('triggerKeyframe setRemoteDescription failed', error);
  350. }
  351. );
  352. }
  353. function demonstrateabug(pc) {
  354. // funny way of doing mute. the subsequent offer contains things like rtcp-mux
  355. // and triggers all new ice candidates (ice restart)
  356. // this code is here to demonstrate a bug
  357. pc.createOffer(
  358. function (offer) {
  359. console.log(offer);
  360. var sdp = new SDP(offer.sdp);
  361. if (sdp.media.length > 1) {
  362. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  363. sdp.raw = sdp.session + sdp.media.join('');
  364. offer.sdp = sdp.raw;
  365. pc.setLocalDescription(offer,
  366. function () {
  367. console.log('mute SLD ok');
  368. },
  369. function(error) {
  370. console.log('mute SLD error');
  371. }
  372. );
  373. }
  374. },
  375. function (error) {
  376. console.warn(error);
  377. },
  378. {mandatory: {OfferToReceiveAudio: true, OfferToReceiveVideo: false}}
  379. );
  380. }
  381. // really mute video, i.e. dont even send black frames
  382. function muteVideo(pc, unmute) {
  383. // FIXME: this probably needs another of those lovely state safeguards...
  384. // which checks for iceconn == connected and sigstate == stable
  385. pc.setRemoteDescription(pc.remoteDescription,
  386. function () {
  387. pc.createAnswer(
  388. function (answer) {
  389. var sdp = new SDP(answer.sdp);
  390. if (sdp.media.length > 1) {
  391. if (unmute)
  392. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  393. else
  394. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  395. sdp.raw = sdp.session + sdp.media.join('');
  396. answer.sdp = sdp.raw;
  397. }
  398. pc.setLocalDescription(answer,
  399. function () {
  400. console.log('mute SLD ok');
  401. },
  402. function(error) {
  403. console.log('mute SLD error');
  404. }
  405. );
  406. },
  407. function (error) {
  408. console.log(error);
  409. }
  410. );
  411. },
  412. function (error) {
  413. console.log('muteVideo SRD error');
  414. }
  415. );
  416. }
  417. $(document).bind('callincoming.jingle', function (event, sid) {
  418. var sess = connection.jingle.sessions[sid];
  419. // TODO: do we check activecall == null?
  420. activecall = sess;
  421. // TODO: check affiliation and/or role
  422. console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
  423. sess.usedrip = true; // not-so-naive trickle ice
  424. sess.sendAnswer();
  425. sess.accept();
  426. });
  427. $(document).bind('callactive.jingle', function (event, videoelem, sid) {
  428. if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
  429. // ignore mixedmslabela0 and v0
  430. videoelem.show();
  431. resizeThumbnails();
  432. updateLargeVideo(videoelem.attr('src'), 1);
  433. showFocusIndicator();
  434. }
  435. });
  436. $(document).bind('callterminated.jingle', function (event, sid, reason) {
  437. // FIXME
  438. });
  439. $(document).bind('setLocalDescription.jingle', function (event, sid) {
  440. // put our ssrcs into presence so other clients can identify our stream
  441. var sess = connection.jingle.sessions[sid];
  442. var newssrcs = {};
  443. var directions = {};
  444. var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
  445. localSDP.media.forEach(function (media) {
  446. var type = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
  447. if (SDPUtil.find_line(media, 'a=ssrc:')) {
  448. // assumes a single local ssrc
  449. var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
  450. newssrcs[type] = ssrc;
  451. directions[type] = (
  452. SDPUtil.find_line(media, 'a=sendrecv')
  453. || SDPUtil.find_line(media, 'a=recvonly')
  454. || SDPUtil.find_line('a=sendonly')
  455. || SDPUtil.find_line('a=inactive')
  456. || 'a=sendrecv' ).substr(2);
  457. }
  458. });
  459. console.log('new ssrcs', newssrcs);
  460. // Have to clear presence map to get rid of removed streams
  461. connection.emuc.clearPresenceMedia();
  462. var i = 0;
  463. Object.keys(newssrcs).forEach(function (mtype) {
  464. i++;
  465. var type = mtype;
  466. // Change video type to screen
  467. if(mtype === 'video' && isUsingScreenStream) {
  468. type = 'screen';
  469. }
  470. connection.emuc.addMediaToPresence(i, type, newssrcs[mtype], directions[mtype]);
  471. });
  472. if (i > 0) {
  473. connection.emuc.sendPresence();
  474. }
  475. });
  476. $(document).bind('joined.muc', function (event, jid, info) {
  477. updateRoomUrl(window.location.href);
  478. document.getElementById('localNick').appendChild(
  479. document.createTextNode(Strophe.getResourceFromJid(jid) + ' (me)')
  480. );
  481. if (Object.keys(connection.emuc.members).length < 1) {
  482. focus = new ColibriFocus(connection, config.hosts.bridge);
  483. }
  484. if (focus && config.etherpad_base) {
  485. Etherpad.init();
  486. }
  487. showFocusIndicator();
  488. // Once we've joined the muc show the toolbar
  489. showToolbar();
  490. var displayName = '';
  491. if (info.displayName)
  492. displayName = info.displayName + ' (me)';
  493. showDisplayName('localVideoContainer', displayName);
  494. });
  495. $(document).bind('entered.muc', function (event, jid, info, pres) {
  496. console.log('entered', jid, info);
  497. console.log('is focus?' + focus ? 'true' : 'false');
  498. var videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  499. var container = addRemoteVideoContainer(videoSpanId);
  500. if (info.displayName)
  501. showDisplayName(videoSpanId, info.displayName);
  502. var nickfield = document.createElement('span');
  503. nickfield.className = "nick";
  504. nickfield.appendChild(document.createTextNode(Strophe.getResourceFromJid(jid)));
  505. container.appendChild(nickfield);
  506. resizeThumbnails();
  507. if (focus !== null) {
  508. // FIXME: this should prepare the video
  509. if (focus.confid === null) {
  510. console.log('make new conference with', jid);
  511. focus.makeConference(Object.keys(connection.emuc.members));
  512. } else {
  513. console.log('invite', jid, 'into conference');
  514. focus.addNewParticipant(jid);
  515. }
  516. }
  517. else if (sharedKey) {
  518. updateLockButton();
  519. }
  520. $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
  521. //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
  522. // Fixme: direction and video types are unhandled here(maybe something more)
  523. ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
  524. });
  525. });
  526. $(document).bind('left.muc', function (event, jid) {
  527. console.log('left', jid);
  528. connection.jingle.terminateByJid(jid);
  529. var container = document.getElementById('participant_' + Strophe.getResourceFromJid(jid));
  530. if (container) {
  531. // hide here, wait for video to close before removing
  532. $(container).hide();
  533. resizeThumbnails();
  534. }
  535. if (focus === null && connection.emuc.myroomjid === connection.emuc.list_members[0]) {
  536. console.log('welcome to our new focus... myself');
  537. focus = new ColibriFocus(connection, config.hosts.bridge);
  538. if (Object.keys(connection.emuc.members).length > 0) {
  539. focus.makeConference(Object.keys(connection.emuc.members));
  540. }
  541. $(document).trigger('focusechanged.muc', [focus]);
  542. }
  543. else if (focus && Object.keys(connection.emuc.members).length === 0) {
  544. console.log('everyone left');
  545. // FIXME: closing the connection is a hack to avoid some
  546. // problemswith reinit
  547. disposeConference();
  548. focus = new ColibriFocus(connection, config.hosts.bridge);
  549. }
  550. if (connection.emuc.getPrezi(jid)) {
  551. $(document).trigger('presentationremoved.muc', [jid, connection.emuc.getPrezi(jid)]);
  552. }
  553. });
  554. $(document).bind('presence.muc', function (event, jid, info, pres) {
  555. // Remove old ssrcs coming from the jid
  556. Object.keys(ssrc2jid).forEach(function(ssrc){
  557. if(ssrc2jid[ssrc] == jid){
  558. delete ssrc2jid[ssrc];
  559. }
  560. if(ssrc2videoType == jid){
  561. delete ssrc2videoType[ssrc];
  562. }
  563. });
  564. $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
  565. //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
  566. var ssrcV = ssrc.getAttribute('ssrc');
  567. ssrc2jid[ssrcV] = jid;
  568. var type = ssrc.getAttribute('type');
  569. ssrc2videoType[ssrcV] = type;
  570. // might need to update the direction if participant just went from sendrecv to recvonly
  571. if (type === 'video' || type === 'screen') {
  572. var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
  573. switch(ssrc.getAttribute('direction')) {
  574. case 'sendrecv':
  575. el.show();
  576. break;
  577. case 'recvonly':
  578. el.hide();
  579. // FIXME: Check if we have to change large video
  580. //checkChangeLargeVideo(el);
  581. break;
  582. }
  583. }
  584. });
  585. if (info.displayName) {
  586. if (jid === connection.emuc.myroomjid)
  587. showDisplayName('localVideoContainer', info.displayName + ' (me)');
  588. else
  589. showDisplayName('participant_' + Strophe.getResourceFromJid(jid), info.displayName);
  590. }
  591. });
  592. $(document).bind('passwordrequired.muc', function (event, jid) {
  593. console.log('on password required', jid);
  594. $.prompt('<h2>Password required</h2>' +
  595. '<input id="lockKey" type="text" placeholder="shared key" autofocus>',
  596. {
  597. persistent: true,
  598. buttons: { "Ok": true , "Cancel": false},
  599. defaultButton: 1,
  600. loaded: function(event) {
  601. document.getElementById('lockKey').focus();
  602. },
  603. submit: function(e,v,m,f){
  604. if(v)
  605. {
  606. var lockKey = document.getElementById('lockKey');
  607. if (lockKey.value !== null)
  608. {
  609. setSharedKey(lockKey.value);
  610. connection.emuc.doJoin(jid, lockKey.value);
  611. }
  612. }
  613. }
  614. });
  615. });
  616. /**
  617. * Updates the large video with the given new video source.
  618. */
  619. function updateLargeVideo(newSrc, vol) {
  620. console.log('hover in', newSrc);
  621. if ($('#largeVideo').attr('src') != newSrc) {
  622. var isVisible = $('#largeVideo').is(':visible');
  623. $('#largeVideo').fadeOut(300, function () {
  624. $(this).attr('src', newSrc);
  625. // Screen stream is already rotated
  626. var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
  627. var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
  628. if (flipX && videoTransform !== 'scaleX(-1)') {
  629. document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
  630. }
  631. else if (!flipX && videoTransform === 'scaleX(-1)') {
  632. document.getElementById('largeVideo').style.webkitTransform = "none";
  633. }
  634. // Change the way we'll be measuring large video
  635. getVideoSize = isVideoSrcDesktop(newSrc) ? getVideoSizeFit : getVideoSizeCover;
  636. if (isVisible)
  637. $(this).fadeIn(300);
  638. });
  639. }
  640. }
  641. /**
  642. * Checks if video identified by given src is desktop stream.
  643. * @param videoSrc eg. blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395
  644. * @returns {boolean}
  645. */
  646. function isVideoSrcDesktop(videoSrc){
  647. // FIXME: fix this mapping mess...
  648. // figure out if large video is desktop stream or just a camera
  649. var isDesktop = false;
  650. if(localVideoSrc === videoSrc) {
  651. // local video
  652. isDesktop = isUsingScreenStream;
  653. } else {
  654. // Do we have associations...
  655. var videoSsrc = videoSrcToSsrc[videoSrc];
  656. if(videoSsrc) {
  657. var videoType = ssrc2videoType[videoSsrc];
  658. if(videoType) {
  659. // Finally there...
  660. isDesktop = videoType === 'screen';
  661. } else {
  662. console.error("No video type for ssrc: " + videoSsrc);
  663. }
  664. } else {
  665. console.error("No ssrc for src: " + videoSrc);
  666. }
  667. }
  668. return isDesktop;
  669. }
  670. /**
  671. * Shows/hides the large video.
  672. */
  673. function setLargeVideoVisible(isVisible) {
  674. if (isVisible) {
  675. $('#largeVideo').css({visibility:'visible'});
  676. $('#watermark').css({visibility:'visible'});
  677. }
  678. else {
  679. $('#largeVideo').css({visibility:'hidden'});
  680. $('#watermark').css({visibility:'hidden'});
  681. }
  682. }
  683. function getConferenceHandler() {
  684. return focus ? focus : activecall;
  685. }
  686. function toggleVideo() {
  687. if (!(connection && connection.jingle.localVideo)) return;
  688. var sess = getConferenceHandler();
  689. if (sess) {
  690. sess.toggleVideoMute(
  691. function(isMuted){
  692. if(isMuted) {
  693. $('#video').removeClass("fa fa-video-camera fa-lg");
  694. $('#video').addClass("fa fa-video-camera no-fa-video-camera fa-lg");
  695. } else {
  696. $('#video').removeClass("fa fa-video-camera no-fa-video-camera fa-lg");
  697. $('#video').addClass("fa fa-video-camera fa-lg");
  698. }
  699. }
  700. );
  701. }
  702. var sess = focus || activecall;
  703. if (!sess) {
  704. return;
  705. }
  706. sess.pendingop = ismuted ? 'unmute' : 'mute';
  707. sess.modifySources();
  708. }
  709. function toggleAudio() {
  710. if (!(connection && connection.jingle.localAudio)) return;
  711. var localAudio = connection.jingle.localAudio;
  712. for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
  713. localAudio.getAudioTracks()[idx].enabled = !localAudio.getAudioTracks()[idx].enabled;
  714. }
  715. }
  716. /**
  717. * Positions the large video.
  718. *
  719. * @param videoWidth the stream video width
  720. * @param videoHeight the stream video height
  721. */
  722. var positionLarge = function(videoWidth, videoHeight) {
  723. var videoSpaceWidth = $('#videospace').width();
  724. var videoSpaceHeight = window.innerHeight;
  725. var videoSize = getVideoSize( videoWidth,
  726. videoHeight,
  727. videoSpaceWidth,
  728. videoSpaceHeight);
  729. var largeVideoWidth = videoSize[0];
  730. var largeVideoHeight = videoSize[1];
  731. var videoPosition = getVideoPosition( largeVideoWidth,
  732. largeVideoHeight,
  733. videoSpaceWidth,
  734. videoSpaceHeight);
  735. var horizontalIndent = videoPosition[0];
  736. var verticalIndent = videoPosition[1];
  737. positionVideo( $('#largeVideo'),
  738. largeVideoWidth,
  739. largeVideoHeight,
  740. horizontalIndent, verticalIndent);
  741. };
  742. /**
  743. * Returns an array of the video horizontal and vertical indents,
  744. * so that if fits its parent.
  745. *
  746. * @return an array with 2 elements, the horizontal indent and the vertical
  747. * indent
  748. */
  749. var getVideoPosition = function ( videoWidth,
  750. videoHeight,
  751. videoSpaceWidth,
  752. videoSpaceHeight) {
  753. // Parent height isn't completely calculated when we position the video in
  754. // full screen mode and this is why we use the screen height in this case.
  755. // Need to think it further at some point and implement it properly.
  756. var isFullScreen = document.fullScreen
  757. || document.mozFullScreen
  758. || document.webkitIsFullScreen;
  759. if (isFullScreen)
  760. videoSpaceHeight = window.innerHeight;
  761. var horizontalIndent = (videoSpaceWidth - videoWidth)/2;
  762. var verticalIndent = (videoSpaceHeight - videoHeight)/2;
  763. return [horizontalIndent, verticalIndent];
  764. };
  765. /**
  766. * Returns an array of the video dimensions, so that it covers the screen.
  767. * It leaves no empty areas, but some parts of the video might not be visible.
  768. *
  769. * @return an array with 2 elements, the video width and the video height
  770. */
  771. function getVideoSizeCover(videoWidth,
  772. videoHeight,
  773. videoSpaceWidth,
  774. videoSpaceHeight) {
  775. if (!videoWidth)
  776. videoWidth = currentVideoWidth;
  777. if (!videoHeight)
  778. videoHeight = currentVideoHeight;
  779. var aspectRatio = videoWidth / videoHeight;
  780. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  781. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  782. if (availableWidth / aspectRatio < videoSpaceHeight) {
  783. availableHeight = videoSpaceHeight;
  784. availableWidth = availableHeight*aspectRatio;
  785. }
  786. if (availableHeight*aspectRatio < videoSpaceWidth) {
  787. availableWidth = videoSpaceWidth;
  788. availableHeight = availableWidth / aspectRatio;
  789. }
  790. return [availableWidth, availableHeight];
  791. }
  792. /**
  793. * Returns an array of the video dimensions, so that it keeps it's aspect ratio and fits available area with it's
  794. * larger dimension. This method ensures that whole video will be visible and can leave empty areas.
  795. *
  796. * @return an array with 2 elements, the video width and the video height
  797. */
  798. function getVideoSizeFit(videoWidth,
  799. videoHeight,
  800. videoSpaceWidth,
  801. videoSpaceHeight) {
  802. if (!videoWidth)
  803. videoWidth = currentVideoWidth;
  804. if (!videoHeight)
  805. videoHeight = currentVideoHeight;
  806. var aspectRatio = videoWidth / videoHeight;
  807. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  808. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  809. if (availableWidth / aspectRatio >= videoSpaceHeight) {
  810. availableHeight = videoSpaceHeight;
  811. availableWidth = availableHeight*aspectRatio;
  812. }
  813. if (availableHeight*aspectRatio >= videoSpaceWidth) {
  814. availableWidth = videoSpaceWidth;
  815. availableHeight = availableWidth / aspectRatio;
  816. }
  817. return [availableWidth, availableHeight];
  818. }
  819. /**
  820. * Sets the size and position of the given video element.
  821. *
  822. * @param video the video element to position
  823. * @param width the desired video width
  824. * @param height the desired video height
  825. * @param horizontalIndent the left and right indent
  826. * @param verticalIndent the top and bottom indent
  827. */
  828. function positionVideo( video,
  829. width,
  830. height,
  831. horizontalIndent,
  832. verticalIndent) {
  833. video.width(width);
  834. video.height(height);
  835. video.css({ top: verticalIndent + 'px',
  836. bottom: verticalIndent + 'px',
  837. left: horizontalIndent + 'px',
  838. right: horizontalIndent + 'px'});
  839. }
  840. var resizeLargeVideoContainer = function () {
  841. Chat.resizeChat();
  842. var availableHeight = window.innerHeight;
  843. var availableWidth = Util.getAvailableVideoWidth();
  844. if (availableWidth < 0 || availableHeight < 0) return;
  845. $('#videospace').width(availableWidth);
  846. $('#videospace').height(availableHeight);
  847. $('#largeVideoContainer').width(availableWidth);
  848. $('#largeVideoContainer').height(availableHeight);
  849. resizeThumbnails();
  850. };
  851. function resizeThumbnails() {
  852. // Calculate the available height, which is the inner window height minus
  853. // 39px for the header minus 2px for the delimiter lines on the top and
  854. // bottom of the large video, minus the 36px space inside the remoteVideos
  855. // container used for highlighting shadow.
  856. var availableHeight = 100;
  857. var numvids = $('#remoteVideos>span:visible').length;
  858. // Remove the 1px borders arround videos and the chat width.
  859. var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
  860. var availableWidth = availableWinWidth / numvids;
  861. var aspectRatio = 16.0 / 9.0;
  862. var maxHeight = Math.min(160, availableHeight);
  863. availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
  864. if (availableHeight < availableWidth / aspectRatio) {
  865. availableWidth = Math.floor(availableHeight * aspectRatio);
  866. }
  867. // size videos so that while keeping AR and max height, we have a nice fit
  868. $('#remoteVideos').height(availableHeight);
  869. $('#remoteVideos>span').width(availableWidth);
  870. $('#remoteVideos>span').height(availableHeight);
  871. }
  872. $(document).ready(function () {
  873. Chat.init();
  874. // Set the defaults for prompt dialogs.
  875. jQuery.prompt.setDefaults({persistent: false});
  876. // Set default desktop sharing method
  877. setDesktopSharing(config.desktopSharing);
  878. // By default we cover the whole screen with video
  879. getVideoSize = getVideoSizeCover;
  880. resizeLargeVideoContainer();
  881. $(window).resize(function () {
  882. resizeLargeVideoContainer();
  883. positionLarge();
  884. });
  885. // Listen for large video size updates
  886. document.getElementById('largeVideo')
  887. .addEventListener('loadedmetadata', function(e){
  888. currentVideoWidth = this.videoWidth;
  889. currentVideoHeight = this.videoHeight;
  890. positionLarge(currentVideoWidth, currentVideoHeight);
  891. });
  892. if (!$('#settings').is(':visible')) {
  893. console.log('init');
  894. init();
  895. } else {
  896. loginInfo.onsubmit = function (e) {
  897. if (e.preventDefault) e.preventDefault();
  898. $('#settings').hide();
  899. init();
  900. };
  901. }
  902. });
  903. $(window).bind('beforeunload', function () {
  904. if (connection && connection.connected) {
  905. // ensure signout
  906. $.ajax({
  907. type: 'POST',
  908. url: config.bosh,
  909. async: false,
  910. cache: false,
  911. contentType: 'application/xml',
  912. data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
  913. success: function (data) {
  914. console.log('signed out');
  915. console.log(data);
  916. },
  917. error: function (XMLHttpRequest, textStatus, errorThrown) {
  918. console.log('signout error', textStatus + ' (' + errorThrown + ')');
  919. }
  920. });
  921. }
  922. disposeConference();
  923. });
  924. function disposeConference() {
  925. var handler = getConferenceHandler();
  926. if(handler && handler.peerconnection) {
  927. // FIXME: probably removing streams is not required and close() should be enough
  928. if(connection.jingle.localAudio) {
  929. handler.peerconnection.removeStream(connection.jingle.localAudio);
  930. }
  931. if(connection.jingle.localVideo) {
  932. handler.peerconnection.removeStream(connection.jingle.localVideo);
  933. }
  934. handler.peerconnection.close();
  935. }
  936. focus = null;
  937. activecall = null;
  938. }
  939. function dump(elem, filename){
  940. elem = elem.parentNode;
  941. elem.download = filename || 'meetlog.json';
  942. elem.href = 'data:application/json;charset=utf-8,\n';
  943. var data = {};
  944. if (connection.jingle) {
  945. Object.keys(connection.jingle.sessions).forEach(function (sid) {
  946. var session = connection.jingle.sessions[sid];
  947. if (session.peerconnection && session.peerconnection.updateLog) {
  948. // FIXME: should probably be a .dump call
  949. data["jingle_" + session.sid] = {
  950. updateLog: session.peerconnection.updateLog,
  951. stats: session.peerconnection.stats,
  952. url: window.location.href}
  953. ;
  954. }
  955. });
  956. }
  957. metadata = {};
  958. metadata.time = new Date();
  959. metadata.url = window.location.href;
  960. metadata.ua = navigator.userAgent;
  961. if (connection.logger) {
  962. metadata.xmpp = connection.logger.log;
  963. }
  964. data.metadata = metadata;
  965. elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
  966. return false;
  967. }
  968. /**
  969. * Changes the style class of the element given by id.
  970. */
  971. function buttonClick(id, classname) {
  972. $(id).toggleClass(classname); // add the class to the clicked element
  973. }
  974. /**
  975. * Opens the lock room dialog.
  976. */
  977. function openLockDialog() {
  978. // Only the focus is able to set a shared key.
  979. if (focus === null) {
  980. if (sharedKey)
  981. $.prompt("This conversation is currently protected by a shared secret key.",
  982. {
  983. title: "Secrect key",
  984. persistent: false
  985. });
  986. else
  987. $.prompt("This conversation isn't currently protected by a secret key. Only the owner of the conference could set a shared key.",
  988. {
  989. title: "Secrect key",
  990. persistent: false
  991. });
  992. }
  993. else {
  994. if (sharedKey)
  995. $.prompt("Are you sure you would like to remove your secret key?",
  996. {
  997. title: "Remove secrect key",
  998. persistent: false,
  999. buttons: { "Remove": true, "Cancel": false},
  1000. defaultButton: 1,
  1001. submit: function(e,v,m,f){
  1002. if(v)
  1003. {
  1004. setSharedKey('');
  1005. lockRoom(false);
  1006. }
  1007. }
  1008. });
  1009. else
  1010. $.prompt('<h2>Set a secrect key to lock your room</h2>' +
  1011. '<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
  1012. {
  1013. persistent: false,
  1014. buttons: { "Save": true , "Cancel": false},
  1015. defaultButton: 1,
  1016. loaded: function(event) {
  1017. document.getElementById('lockKey').focus();
  1018. },
  1019. submit: function(e,v,m,f){
  1020. if(v)
  1021. {
  1022. var lockKey = document.getElementById('lockKey');
  1023. if (lockKey.value)
  1024. {
  1025. setSharedKey(Util.escapeHtml(lockKey.value));
  1026. lockRoom(true);
  1027. }
  1028. }
  1029. }
  1030. });
  1031. }
  1032. }
  1033. /**
  1034. * Opens the invite link dialog.
  1035. */
  1036. function openLinkDialog() {
  1037. $.prompt('<input id="inviteLinkRef" type="text" value="'
  1038. + encodeURI(roomUrl) + '" onclick="this.select();" readonly>',
  1039. {
  1040. title: "Share this link with everyone you want to invite",
  1041. persistent: false,
  1042. buttons: { "Cancel": false},
  1043. loaded: function(event) {
  1044. document.getElementById('inviteLinkRef').select();
  1045. }
  1046. });
  1047. }
  1048. /**
  1049. * Opens the settings dialog.
  1050. */
  1051. function openSettingsDialog() {
  1052. $.prompt('<h2>Configure your conference</h2>' +
  1053. '<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
  1054. '<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
  1055. 'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
  1056. {
  1057. persistent: false,
  1058. buttons: { "Save": true , "Cancel": false},
  1059. defaultButton: 1,
  1060. loaded: function(event) {
  1061. document.getElementById('lockKey').focus();
  1062. },
  1063. submit: function(e,v,m,f){
  1064. if(v)
  1065. {
  1066. if ($('#initMuted').is(":checked"))
  1067. {
  1068. // it is checked
  1069. }
  1070. if ($('#requireNicknames').is(":checked"))
  1071. {
  1072. // it is checked
  1073. }
  1074. /*
  1075. var lockKey = document.getElementById('lockKey');
  1076. if (lockKey.value)
  1077. {
  1078. setSharedKey(lockKey.value);
  1079. lockRoom(true);
  1080. }
  1081. */
  1082. }
  1083. }
  1084. });
  1085. }
  1086. /**
  1087. * Locks / unlocks the room.
  1088. */
  1089. function lockRoom(lock) {
  1090. if (lock)
  1091. connection.emuc.lockRoom(sharedKey);
  1092. else
  1093. connection.emuc.lockRoom('');
  1094. updateLockButton();
  1095. }
  1096. /**
  1097. * Sets the shared key.
  1098. */
  1099. function setSharedKey(sKey) {
  1100. sharedKey = sKey;
  1101. }
  1102. /**
  1103. * Updates the lock button state.
  1104. */
  1105. function updateLockButton() {
  1106. buttonClick("#lockIcon", "icon-security icon-security-locked");
  1107. }
  1108. /**
  1109. * Hides the toolbar.
  1110. */
  1111. var hideToolbar = function() {
  1112. var isToolbarHover = false;
  1113. $('#header').find('*').each(function(){
  1114. var id = $(this).attr('id');
  1115. if ($("#" + id + ":hover").length > 0) {
  1116. isToolbarHover = true;
  1117. }
  1118. });
  1119. clearTimeout(toolbarTimeout);
  1120. toolbarTimeout = null;
  1121. if (!isToolbarHover) {
  1122. $('#header').hide("slide", { direction: "up", duration: 300});
  1123. }
  1124. else {
  1125. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1126. }
  1127. };
  1128. /**
  1129. * Shows the call main toolbar.
  1130. */
  1131. function showToolbar() {
  1132. if (!$('#header').is(':visible')) {
  1133. $('#header').show("slide", { direction: "up", duration: 300});
  1134. if (toolbarTimeout) {
  1135. clearTimeout(toolbarTimeout);
  1136. toolbarTimeout = null;
  1137. }
  1138. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1139. }
  1140. if (focus != null)
  1141. {
  1142. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
  1143. // $('#settingsButton').css({visibility:"visible"});
  1144. }
  1145. // Show/hide desktop sharing button
  1146. showDesktopSharingButton();
  1147. }
  1148. /**
  1149. * Docks/undocks the toolbar.
  1150. *
  1151. * @param isDock indicates what operation to perform
  1152. */
  1153. function dockToolbar(isDock) {
  1154. if (isDock) {
  1155. // First make sure the toolbar is shown.
  1156. if (!$('#header').is(':visible')) {
  1157. showToolbar();
  1158. }
  1159. // Then clear the time out, to dock the toolbar.
  1160. clearTimeout(toolbarTimeout);
  1161. toolbarTimeout = null;
  1162. }
  1163. else {
  1164. if (!$('#header').is(':visible')) {
  1165. showToolbar();
  1166. }
  1167. else {
  1168. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1169. }
  1170. }
  1171. }
  1172. /**
  1173. * Updates the room invite url.
  1174. */
  1175. function updateRoomUrl(newRoomUrl) {
  1176. roomUrl = newRoomUrl;
  1177. }
  1178. /**
  1179. * Warning to the user that the conference window is about to be closed.
  1180. */
  1181. function closePageWarning() {
  1182. if (focus !== null)
  1183. return "You are the owner of this conference call and you are about to end it.";
  1184. else
  1185. return "You are about to leave this conversation.";
  1186. }
  1187. /**
  1188. * Shows a visual indicator for the focus of the conference.
  1189. * Currently if we're not the owner of the conference we obtain the focus
  1190. * from the connection.jingle.sessions.
  1191. */
  1192. function showFocusIndicator() {
  1193. if (focus !== null) {
  1194. var indicatorSpan = $('#localVideoContainer .focusindicator');
  1195. if (indicatorSpan.children().length === 0)
  1196. {
  1197. createFocusIndicatorElement(indicatorSpan[0]);
  1198. }
  1199. }
  1200. else if (Object.keys(connection.jingle.sessions).length > 0) {
  1201. // If we're only a participant the focus will be the only session we have.
  1202. var session = connection.jingle.sessions[Object.keys(connection.jingle.sessions)[0]];
  1203. var focusId = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
  1204. var focusContainer = document.getElementById(focusId);
  1205. if(!focusContainer) {
  1206. console.error("No focus container!");
  1207. return;
  1208. }
  1209. var indicatorSpan = $('#' + focusId + ' .focusindicator');
  1210. if (!indicatorSpan || indicatorSpan.length === 0) {
  1211. indicatorSpan = document.createElement('span');
  1212. indicatorSpan.className = 'focusindicator';
  1213. focusContainer.appendChild(indicatorSpan);
  1214. createFocusIndicatorElement(indicatorSpan);
  1215. }
  1216. }
  1217. }
  1218. function addRemoteVideoContainer(id) {
  1219. var container = document.createElement('span');
  1220. container.id = id;
  1221. container.className = 'videocontainer';
  1222. var remotes = document.getElementById('remoteVideos');
  1223. remotes.appendChild(container);
  1224. return container;
  1225. }
  1226. /**
  1227. * Creates the element indicating the focus of the conference.
  1228. */
  1229. function createFocusIndicatorElement(parentElement) {
  1230. var focusIndicator = document.createElement('i');
  1231. focusIndicator.className = 'fa fa-star';
  1232. focusIndicator.title = "The owner of this conference";
  1233. parentElement.appendChild(focusIndicator);
  1234. }
  1235. /**
  1236. * Toggles the application in and out of full screen mode
  1237. * (a.k.a. presentation mode in Chrome).
  1238. */
  1239. function toggleFullScreen() {
  1240. var fsElement = document.documentElement;
  1241. if (!document.mozFullScreen && !document.webkitIsFullScreen){
  1242. //Enter Full Screen
  1243. if (fsElement.mozRequestFullScreen) {
  1244. fsElement.mozRequestFullScreen();
  1245. }
  1246. else {
  1247. fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  1248. }
  1249. } else {
  1250. //Exit Full Screen
  1251. if (document.mozCancelFullScreen) {
  1252. document.mozCancelFullScreen();
  1253. } else {
  1254. document.webkitCancelFullScreen();
  1255. }
  1256. }
  1257. }
  1258. /**
  1259. * Shows the display name for the given video.
  1260. */
  1261. function showDisplayName(videoSpanId, displayName) {
  1262. var escDisplayName = Util.escapeHtml(displayName);
  1263. var nameSpan = $('#' + videoSpanId + '>span.displayname');
  1264. // If we already have a display name for this video.
  1265. if (nameSpan.length > 0) {
  1266. var nameSpanElement = nameSpan.get(0);
  1267. if (nameSpanElement.id === 'localDisplayName'
  1268. && $('#localDisplayName').html() !== escDisplayName)
  1269. $('#localDisplayName').html(escDisplayName);
  1270. else
  1271. $('#' + videoSpanId + '_name').html(escDisplayName);
  1272. }
  1273. else {
  1274. var editButton = null;
  1275. if (videoSpanId === 'localVideoContainer') {
  1276. editButton = createEditDisplayNameButton();
  1277. }
  1278. if (escDisplayName.length) {
  1279. nameSpan = document.createElement('span');
  1280. nameSpan.className = 'displayname';
  1281. nameSpan.innerHTML = escDisplayName;
  1282. $('#' + videoSpanId)[0].appendChild(nameSpan);
  1283. }
  1284. if (!editButton) {
  1285. nameSpan.id = videoSpanId + '_name';
  1286. }
  1287. else {
  1288. nameSpan.id = 'localDisplayName';
  1289. $('#' + videoSpanId)[0].appendChild(editButton);
  1290. var editableText = document.createElement('input');
  1291. editableText.className = 'displayname';
  1292. editableText.id = 'editDisplayName';
  1293. if (escDisplayName.length)
  1294. editableText.value
  1295. = escDisplayName.substring(0, escDisplayName.indexOf(' (me)'));
  1296. editableText.setAttribute('style', 'display:none;');
  1297. editableText.setAttribute('placeholder', 'ex. Jane Pink');
  1298. $('#' + videoSpanId)[0].appendChild(editableText);
  1299. $('#localVideoContainer .displayname').bind("click", function(e) {
  1300. e.preventDefault();
  1301. $('#localDisplayName').hide();
  1302. $('#editDisplayName').show();
  1303. $('#editDisplayName').focus();
  1304. $('#editDisplayName').select();
  1305. var inputDisplayNameHandler = function(name) {
  1306. if (nickname !== name) {
  1307. nickname = Util.escapeHtml(name);
  1308. window.localStorage.displayname = nickname;
  1309. connection.emuc.addDisplayNameToPresence(nickname);
  1310. connection.emuc.sendPresence();
  1311. Chat.setChatConversationMode(true);
  1312. }
  1313. if (!$('#localDisplayName').is(":visible")) {
  1314. $('#localDisplayName').html(nickname + " (me)");
  1315. $('#localDisplayName').show();
  1316. $('#editDisplayName').hide();
  1317. }
  1318. };
  1319. $('#editDisplayName').one("focusout", function (e) {
  1320. inputDisplayNameHandler(this.value);
  1321. });
  1322. $('#editDisplayName').on('keydown', function (e) {
  1323. if (e.keyCode === 13) {
  1324. e.preventDefault();
  1325. inputDisplayNameHandler(this.value);
  1326. }
  1327. });
  1328. });
  1329. }
  1330. }
  1331. }
  1332. /**
  1333. * Creates the edit display name button.
  1334. *
  1335. * @returns the edit button
  1336. */
  1337. function createEditDisplayNameButton() {
  1338. var editButton = document.createElement('a');
  1339. editButton.className = 'displayname';
  1340. editButton.innerHTML = '<i class="fa fa-pencil"></i>';
  1341. return editButton;
  1342. }
  1343. /**
  1344. * Resizes and repositions videos in full screen mode.
  1345. */
  1346. $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
  1347. function() {
  1348. resizeLargeVideoContainer();
  1349. positionLarge();
  1350. isFullScreen = document.fullScreen
  1351. || document.mozFullScreen
  1352. || document.webkitIsFullScreen;
  1353. if (isFullScreen) {
  1354. setView("fullscreen");
  1355. }
  1356. else {
  1357. setView("default");
  1358. }
  1359. });
  1360. /**
  1361. * Sets the current view.
  1362. */
  1363. function setView(viewName) {
  1364. // if (viewName == "fullscreen") {
  1365. // document.getElementById('videolayout_fullscreen').disabled = false;
  1366. // document.getElementById('videolayout_default').disabled = true;
  1367. // }
  1368. // else {
  1369. // document.getElementById('videolayout_default').disabled = false;
  1370. // document.getElementById('videolayout_fullscreen').disabled = true;
  1371. // }
  1372. }