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.

videolayout.js 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. var VideoLayout = (function (my) {
  2. var preMuted = false;
  3. var currentDominantSpeaker = null;
  4. my.changeLocalAudio = function(stream) {
  5. connection.jingle.localAudio = stream;
  6. RTC.attachMediaStream($('#localAudio'), stream);
  7. document.getElementById('localAudio').autoplay = true;
  8. document.getElementById('localAudio').volume = 0;
  9. if (preMuted) {
  10. toggleAudio();
  11. preMuted = false;
  12. }
  13. };
  14. my.changeLocalVideo = function(stream, flipX) {
  15. connection.jingle.localVideo = stream;
  16. var localVideo = document.createElement('video');
  17. localVideo.id = 'localVideo_' + stream.id;
  18. localVideo.autoplay = true;
  19. localVideo.volume = 0; // is it required if audio is separated ?
  20. localVideo.oncontextmenu = function () { return false; };
  21. var localVideoContainer = document.getElementById('localVideoWrapper');
  22. localVideoContainer.appendChild(localVideo);
  23. AudioLevels.updateAudioLevelCanvas();
  24. var localVideoSelector = $('#' + localVideo.id);
  25. // Add click handler to both video and video wrapper elements in case
  26. // there's no video.
  27. localVideoSelector.click(function () {
  28. VideoLayout.handleVideoThumbClicked(localVideo.src);
  29. });
  30. $('#localVideoContainer').click(function () {
  31. VideoLayout.handleVideoThumbClicked(localVideo.src);
  32. });
  33. // Add hover handler
  34. $('#localVideoContainer').hover(
  35. function() {
  36. VideoLayout.showDisplayName('localVideoContainer', true);
  37. },
  38. function() {
  39. if (!VideoLayout.isLargeVideoVisible()
  40. || localVideo.src !== $('#largeVideo').attr('src'))
  41. VideoLayout.showDisplayName('localVideoContainer', false);
  42. }
  43. );
  44. // Add stream ended handler
  45. stream.onended = function () {
  46. localVideoContainer.removeChild(localVideo);
  47. VideoLayout.checkChangeLargeVideo(localVideo.src);
  48. };
  49. // Flip video x axis if needed
  50. flipXLocalVideo = flipX;
  51. if (flipX) {
  52. localVideoSelector.addClass("flipVideoX");
  53. }
  54. // Attach WebRTC stream
  55. RTC.attachMediaStream(localVideoSelector, stream);
  56. localVideoSrc = localVideo.src;
  57. VideoLayout.updateLargeVideo(localVideoSrc, 0);
  58. };
  59. /**
  60. * Checks if removed video is currently displayed and tries to display
  61. * another one instead.
  62. * @param removedVideoSrc src stream identifier of the video.
  63. */
  64. my.checkChangeLargeVideo = function(removedVideoSrc) {
  65. if (removedVideoSrc === $('#largeVideo').attr('src')) {
  66. // this is currently displayed as large
  67. // pick the last visible video in the row
  68. // if nobody else is left, this picks the local video
  69. var pick
  70. = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
  71. .get(0);
  72. if (!pick) {
  73. console.info("Last visible video no longer exists");
  74. pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
  75. if (!pick) {
  76. // Try local video
  77. console.info("Fallback to local video...");
  78. pick = $('#remoteVideos>span>span>video').get(0);
  79. }
  80. }
  81. // mute if localvideo
  82. if (pick) {
  83. VideoLayout.updateLargeVideo(pick.src, pick.volume);
  84. } else {
  85. console.warn("Failed to elect large video");
  86. }
  87. }
  88. };
  89. /**
  90. * Updates the large video with the given new video source.
  91. */
  92. my.updateLargeVideo = function(newSrc, vol) {
  93. console.log('hover in', newSrc);
  94. if ($('#largeVideo').attr('src') != newSrc) {
  95. var isVisible = $('#largeVideo').is(':visible');
  96. $('#largeVideo').fadeOut(300, function () {
  97. var oldSrc = $(this).attr('src');
  98. $(this).attr('src', newSrc);
  99. // Screen stream is already rotated
  100. var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
  101. var videoTransform = document.getElementById('largeVideo')
  102. .style.webkitTransform;
  103. if (flipX && videoTransform !== 'scaleX(-1)') {
  104. document.getElementById('largeVideo').style.webkitTransform
  105. = "scaleX(-1)";
  106. }
  107. else if (!flipX && videoTransform === 'scaleX(-1)') {
  108. document.getElementById('largeVideo').style.webkitTransform
  109. = "none";
  110. }
  111. // Change the way we'll be measuring and positioning large video
  112. var isDesktop = isVideoSrcDesktop(newSrc);
  113. getVideoSize = isDesktop
  114. ? getDesktopVideoSize
  115. : getCameraVideoSize;
  116. getVideoPosition = isDesktop
  117. ? getDesktopVideoPosition
  118. : getCameraVideoPosition;
  119. if (isVisible) {
  120. // Only if the large video is currently visible.
  121. // Disable previous dominant speaker video.
  122. var oldJid = getJidFromVideoSrc(oldSrc);
  123. if (oldJid) {
  124. var oldResourceJid = Strophe.getResourceFromJid(oldJid);
  125. VideoLayout.enableDominantSpeaker(oldResourceJid, false);
  126. }
  127. // Enable new dominant speaker in the remote videos section.
  128. var userJid = getJidFromVideoSrc(newSrc);
  129. if (userJid)
  130. {
  131. var resourceJid = Strophe.getResourceFromJid(userJid);
  132. VideoLayout.enableDominantSpeaker(resourceJid, true);
  133. }
  134. $(this).fadeIn(300);
  135. }
  136. });
  137. }
  138. };
  139. my.handleVideoThumbClicked = function(videoSrc) {
  140. // Restore style for previously focused video
  141. var focusJid = getJidFromVideoSrc(focusedVideoSrc);
  142. var oldContainer = getParticipantContainer(focusJid);
  143. if (oldContainer) {
  144. oldContainer.removeClass("videoContainerFocused");
  145. }
  146. // Unlock current focused.
  147. if (focusedVideoSrc === videoSrc)
  148. {
  149. focusedVideoSrc = null;
  150. var dominantSpeakerVideo = null;
  151. // Enable the currently set dominant speaker.
  152. if (currentDominantSpeaker) {
  153. dominantSpeakerVideo
  154. = $('#participant_' + currentDominantSpeaker + '>video')
  155. .get(0);
  156. if (dominantSpeakerVideo)
  157. VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1);
  158. }
  159. return;
  160. }
  161. // Lock new video
  162. focusedVideoSrc = videoSrc;
  163. // Update focused/pinned interface.
  164. var userJid = getJidFromVideoSrc(videoSrc);
  165. if (userJid)
  166. {
  167. var container = getParticipantContainer(userJid);
  168. container.addClass("videoContainerFocused");
  169. }
  170. // Triggers a "video.selected" event. The "false" parameter indicates
  171. // this isn't a prezi.
  172. $(document).trigger("video.selected", [false]);
  173. VideoLayout.updateLargeVideo(videoSrc, 1);
  174. $('audio').each(function (idx, el) {
  175. if (el.id.indexOf('mixedmslabel') !== -1) {
  176. el.volume = 0;
  177. el.volume = 1;
  178. }
  179. });
  180. };
  181. /**
  182. * Positions the large video.
  183. *
  184. * @param videoWidth the stream video width
  185. * @param videoHeight the stream video height
  186. */
  187. my.positionLarge = function (videoWidth, videoHeight) {
  188. var videoSpaceWidth = $('#videospace').width();
  189. var videoSpaceHeight = window.innerHeight;
  190. var videoSize = getVideoSize(videoWidth,
  191. videoHeight,
  192. videoSpaceWidth,
  193. videoSpaceHeight);
  194. var largeVideoWidth = videoSize[0];
  195. var largeVideoHeight = videoSize[1];
  196. var videoPosition = getVideoPosition(largeVideoWidth,
  197. largeVideoHeight,
  198. videoSpaceWidth,
  199. videoSpaceHeight);
  200. var horizontalIndent = videoPosition[0];
  201. var verticalIndent = videoPosition[1];
  202. positionVideo($('#largeVideo'),
  203. largeVideoWidth,
  204. largeVideoHeight,
  205. horizontalIndent, verticalIndent);
  206. };
  207. /**
  208. * Shows/hides the large video.
  209. */
  210. my.setLargeVideoVisible = function(isVisible) {
  211. var largeVideoJid = getJidFromVideoSrc($('#largeVideo').attr('src'));
  212. var resourceJid = Strophe.getResourceFromJid(largeVideoJid);
  213. if (isVisible) {
  214. $('#largeVideo').css({visibility: 'visible'});
  215. $('.watermark').css({visibility: 'visible'});
  216. VideoLayout.enableDominantSpeaker(resourceJid, true);
  217. }
  218. else {
  219. $('#largeVideo').css({visibility: 'hidden'});
  220. $('.watermark').css({visibility: 'hidden'});
  221. VideoLayout.enableDominantSpeaker(resourceJid, false);
  222. }
  223. };
  224. /**
  225. * Indicates if the large video is currently visible.
  226. *
  227. * @return <tt>true</tt> if visible, <tt>false</tt> - otherwise
  228. */
  229. my.isLargeVideoVisible = function() {
  230. return $('#largeVideo').is(':visible');
  231. };
  232. /**
  233. * Checks if container for participant identified by given peerJid exists
  234. * in the document and creates it eventually.
  235. *
  236. * @param peerJid peer Jid to check.
  237. */
  238. my.ensurePeerContainerExists = function(peerJid) {
  239. var peerResource = Strophe.getResourceFromJid(peerJid);
  240. var videoSpanId = 'participant_' + peerResource;
  241. if ($('#' + videoSpanId).length > 0) {
  242. // If there's been a focus change, make sure we add focus related
  243. // interface!!
  244. if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
  245. addRemoteVideoMenu( peerJid,
  246. document.getElementById(videoSpanId));
  247. return;
  248. }
  249. var container
  250. = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
  251. var nickfield = document.createElement('span');
  252. nickfield.className = "nick";
  253. nickfield.appendChild(document.createTextNode(peerResource));
  254. container.appendChild(nickfield);
  255. VideoLayout.resizeThumbnails();
  256. };
  257. my.addRemoteVideoContainer = function(peerJid, spanId) {
  258. var container = document.createElement('span');
  259. container.id = spanId;
  260. container.className = 'videocontainer';
  261. var remotes = document.getElementById('remoteVideos');
  262. // If the peerJid is null then this video span couldn't be directly
  263. // associated with a participant (this could happen in the case of prezi).
  264. if (focus && peerJid != null)
  265. addRemoteVideoMenu(peerJid, container);
  266. remotes.appendChild(container);
  267. AudioLevels.updateAudioLevelCanvas(peerJid);
  268. return container;
  269. };
  270. /**
  271. * Shows the display name for the given video.
  272. */
  273. my.setDisplayName = function(videoSpanId, displayName) {
  274. var nameSpan = $('#' + videoSpanId + '>span.displayname');
  275. var defaultLocalDisplayName = "Me";
  276. var defaultRemoteDisplayName = "Speaker";
  277. // If we already have a display name for this video.
  278. if (nameSpan.length > 0) {
  279. var nameSpanElement = nameSpan.get(0);
  280. if (nameSpanElement.id === 'localDisplayName' &&
  281. $('#localDisplayName').text() !== displayName) {
  282. if (displayName)
  283. $('#localDisplayName').text(displayName + ' (me)');
  284. else
  285. $('#localDisplayName').text(defaultLocalDisplayName);
  286. } else {
  287. if (displayName)
  288. $('#' + videoSpanId + '_name').text(displayName);
  289. else
  290. $('#' + videoSpanId + '_name').text(defaultRemoteDisplayName);
  291. }
  292. } else {
  293. var editButton = null;
  294. nameSpan = document.createElement('span');
  295. nameSpan.className = 'displayname';
  296. $('#' + videoSpanId)[0].appendChild(nameSpan);
  297. if (videoSpanId === 'localVideoContainer') {
  298. editButton = createEditDisplayNameButton();
  299. nameSpan.innerText = defaultLocalDisplayName;
  300. }
  301. else {
  302. nameSpan.innerText = defaultRemoteDisplayName;
  303. }
  304. if (displayName && displayName.length) {
  305. nameSpan.innerText = displayName;
  306. }
  307. if (!editButton) {
  308. nameSpan.id = videoSpanId + '_name';
  309. } else {
  310. nameSpan.id = 'localDisplayName';
  311. $('#' + videoSpanId)[0].appendChild(editButton);
  312. var editableText = document.createElement('input');
  313. editableText.className = 'displayname';
  314. editableText.id = 'editDisplayName';
  315. if (displayName.length) {
  316. editableText.value
  317. = displayName.substring(0, displayName.indexOf(' (me)'));
  318. }
  319. editableText.setAttribute('style', 'display:none;');
  320. editableText.setAttribute('placeholder', 'ex. Jane Pink');
  321. $('#' + videoSpanId)[0].appendChild(editableText);
  322. $('#localVideoContainer .displayname')
  323. .bind("click", function (e) {
  324. e.preventDefault();
  325. $('#localDisplayName').hide();
  326. $('#editDisplayName').show();
  327. $('#editDisplayName').focus();
  328. $('#editDisplayName').select();
  329. var inputDisplayNameHandler = function (name) {
  330. if (nickname !== name) {
  331. nickname = name;
  332. window.localStorage.displayname = nickname;
  333. connection.emuc.addDisplayNameToPresence(nickname);
  334. connection.emuc.sendPresence();
  335. Chat.setChatConversationMode(true);
  336. }
  337. if (!$('#localDisplayName').is(":visible")) {
  338. if (nickname)
  339. $('#localDisplayName').text(nickname + " (me)");
  340. else
  341. $('#localDisplayName')
  342. .text(defaultLocalDisplayName);
  343. $('#localDisplayName').show();
  344. }
  345. $('#editDisplayName').hide();
  346. };
  347. $('#editDisplayName').one("focusout", function (e) {
  348. inputDisplayNameHandler(this.value);
  349. });
  350. $('#editDisplayName').on('keydown', function (e) {
  351. if (e.keyCode === 13) {
  352. e.preventDefault();
  353. inputDisplayNameHandler(this.value);
  354. }
  355. });
  356. });
  357. }
  358. }
  359. };
  360. /**
  361. * Shows/hides the display name on the remote video.
  362. * @param videoSpanId the identifier of the video span element
  363. * @param isShow indicates if the display name should be shown or hidden
  364. */
  365. my.showDisplayName = function(videoSpanId, isShow) {
  366. // FIX: need to use noConflict of jquery, because apparently we're
  367. // using another library that uses $, which conflics with jquery and
  368. // sometimes objects are null because of that!!!!!!!!!
  369. // http://api.jquery.com/jQuery.noConflict/
  370. var nameSpan = jQuery('#' + videoSpanId + '>span.displayname').get(0);
  371. if (isShow) {
  372. if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
  373. nameSpan.setAttribute("style", "display:inline-block;");
  374. }
  375. else {
  376. if (nameSpan)
  377. nameSpan.setAttribute("style", "display:none;");
  378. }
  379. };
  380. /**
  381. * Shows a visual indicator for the focus of the conference.
  382. * Currently if we're not the owner of the conference we obtain the focus
  383. * from the connection.jingle.sessions.
  384. */
  385. my.showFocusIndicator = function() {
  386. if (focus !== null) {
  387. var indicatorSpan = $('#localVideoContainer .focusindicator');
  388. if (indicatorSpan.children().length === 0)
  389. {
  390. createFocusIndicatorElement(indicatorSpan[0]);
  391. }
  392. }
  393. else if (Object.keys(connection.jingle.sessions).length > 0) {
  394. // If we're only a participant the focus will be the only session we have.
  395. var session
  396. = connection.jingle.sessions
  397. [Object.keys(connection.jingle.sessions)[0]];
  398. var focusId
  399. = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
  400. var focusContainer = document.getElementById(focusId);
  401. if (!focusContainer) {
  402. console.error("No focus container!");
  403. return;
  404. }
  405. var indicatorSpan = $('#' + focusId + ' .focusindicator');
  406. if (!indicatorSpan || indicatorSpan.length === 0) {
  407. indicatorSpan = document.createElement('span');
  408. indicatorSpan.className = 'focusindicator';
  409. focusContainer.appendChild(indicatorSpan);
  410. createFocusIndicatorElement(indicatorSpan);
  411. }
  412. }
  413. };
  414. /**
  415. * Shows video muted indicator over small videos.
  416. */
  417. my.showVideoIndicator = function(videoSpanId, isMuted) {
  418. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  419. if (isMuted === 'false') {
  420. if (videoMutedSpan.length > 0) {
  421. videoMutedSpan.remove();
  422. }
  423. }
  424. else {
  425. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  426. videoMutedSpan = document.createElement('span');
  427. videoMutedSpan.className = 'videoMuted';
  428. if (audioMutedSpan) {
  429. videoMutedSpan.right = '30px';
  430. }
  431. $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
  432. var mutedIndicator = document.createElement('i');
  433. mutedIndicator.className = 'icon-camera-disabled';
  434. Util.setTooltip(mutedIndicator,
  435. "Participant has<br/>stopped the camera.",
  436. "top");
  437. videoMutedSpan.appendChild(mutedIndicator);
  438. }
  439. };
  440. /**
  441. * Shows audio muted indicator over small videos.
  442. */
  443. my.showAudioIndicator = function(videoSpanId, isMuted) {
  444. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  445. if (isMuted === 'false') {
  446. if (audioMutedSpan.length > 0) {
  447. audioMutedSpan.remove();
  448. }
  449. }
  450. else {
  451. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  452. audioMutedSpan = document.createElement('span');
  453. audioMutedSpan.className = 'audioMuted';
  454. Util.setTooltip(audioMutedSpan,
  455. "Participant is muted",
  456. "top");
  457. if (videoMutedSpan) {
  458. audioMutedSpan.right = '30px';
  459. }
  460. $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
  461. var mutedIndicator = document.createElement('i');
  462. mutedIndicator.className = 'icon-mic-disabled';
  463. audioMutedSpan.appendChild(mutedIndicator);
  464. }
  465. };
  466. /**
  467. * Resizes the large video container.
  468. */
  469. my.resizeLargeVideoContainer = function () {
  470. Chat.resizeChat();
  471. var availableHeight = window.innerHeight;
  472. var availableWidth = Util.getAvailableVideoWidth();
  473. if (availableWidth < 0 || availableHeight < 0) return;
  474. $('#videospace').width(availableWidth);
  475. $('#videospace').height(availableHeight);
  476. $('#largeVideoContainer').width(availableWidth);
  477. $('#largeVideoContainer').height(availableHeight);
  478. VideoLayout.resizeThumbnails();
  479. };
  480. /**
  481. * Resizes thumbnails.
  482. */
  483. my.resizeThumbnails = function() {
  484. var videoSpaceWidth = $('#remoteVideos').width();
  485. var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
  486. var width = thumbnailSize[0];
  487. var height = thumbnailSize[1];
  488. // size videos so that while keeping AR and max height, we have a
  489. // nice fit
  490. $('#remoteVideos').height(height);
  491. $('#remoteVideos>span').width(width);
  492. $('#remoteVideos>span').height(height);
  493. $(document).trigger("remotevideo.resized", [width, height]);
  494. };
  495. /**
  496. * Enables the dominant speaker UI.
  497. *
  498. * @param resourceJid the jid indicating the video element to
  499. * activate/deactivate
  500. * @param isEnable indicates if the dominant speaker should be enabled or
  501. * disabled
  502. */
  503. my.enableDominantSpeaker = function(resourceJid, isEnable) {
  504. var displayName = resourceJid;
  505. var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
  506. if (nameSpan.length > 0)
  507. displayName = nameSpan.text();
  508. console.log("UI enable dominant speaker",
  509. displayName,
  510. resourceJid,
  511. isEnable);
  512. var videoSpanId = null;
  513. var videoContainerId = null;
  514. if (resourceJid
  515. === Strophe.getResourceFromJid(connection.emuc.myroomjid)) {
  516. videoSpanId = 'localVideoWrapper';
  517. videoContainerId = 'localVideoContainer';
  518. }
  519. else {
  520. videoSpanId = 'participant_' + resourceJid;
  521. videoContainerId = videoSpanId;
  522. }
  523. videoSpan = document.getElementById(videoContainerId);
  524. if (!videoSpan) {
  525. console.error("No video element for jid", resourceJid);
  526. return;
  527. }
  528. var video = $('#' + videoSpanId + '>video');
  529. if (video && video.length > 0) {
  530. if (isEnable) {
  531. VideoLayout.showDisplayName(videoContainerId, true);
  532. if (!videoSpan.classList.contains("dominantspeaker"))
  533. videoSpan.classList.add("dominantspeaker");
  534. video.css({visibility: 'hidden'});
  535. }
  536. else {
  537. VideoLayout.showDisplayName(videoContainerId, false);
  538. if (videoSpan.classList.contains("dominantspeaker"))
  539. videoSpan.classList.remove("dominantspeaker");
  540. video.css({visibility: 'visible'});
  541. }
  542. }
  543. };
  544. /**
  545. * Gets the selector of video thumbnail container for the user identified by
  546. * given <tt>userJid</tt>
  547. * @param userJid user's Jid for whom we want to get the video container.
  548. */
  549. function getParticipantContainer(userJid)
  550. {
  551. if (!userJid)
  552. return null;
  553. if (userJid === connection.emuc.myroomjid)
  554. return $("#localVideoContainer");
  555. else
  556. return $("#participant_" + Strophe.getResourceFromJid(userJid));
  557. }
  558. /**
  559. * Sets the size and position of the given video element.
  560. *
  561. * @param video the video element to position
  562. * @param width the desired video width
  563. * @param height the desired video height
  564. * @param horizontalIndent the left and right indent
  565. * @param verticalIndent the top and bottom indent
  566. */
  567. function positionVideo(video,
  568. width,
  569. height,
  570. horizontalIndent,
  571. verticalIndent) {
  572. video.width(width);
  573. video.height(height);
  574. video.css({ top: verticalIndent + 'px',
  575. bottom: verticalIndent + 'px',
  576. left: horizontalIndent + 'px',
  577. right: horizontalIndent + 'px'});
  578. }
  579. /**
  580. * Calculates the thumbnail size.
  581. */
  582. my.calculateThumbnailSize = function (videoSpaceWidth) {
  583. // Calculate the available height, which is the inner window height minus
  584. // 39px for the header minus 2px for the delimiter lines on the top and
  585. // bottom of the large video, minus the 36px space inside the remoteVideos
  586. // container used for highlighting shadow.
  587. var availableHeight = 100;
  588. var numvids = $('#remoteVideos>span:visible').length;
  589. // Remove the 3px borders arround videos and border around the remote
  590. // videos area
  591. var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 50;
  592. var availableWidth = availableWinWidth / numvids;
  593. var aspectRatio = 16.0 / 9.0;
  594. var maxHeight = Math.min(160, availableHeight);
  595. availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
  596. if (availableHeight < availableWidth / aspectRatio) {
  597. availableWidth = Math.floor(availableHeight * aspectRatio);
  598. }
  599. return [availableWidth, availableHeight];
  600. };
  601. /**
  602. * Returns an array of the video dimensions, so that it keeps it's aspect
  603. * ratio and fits available area with it's larger dimension. This method
  604. * ensures that whole video will be visible and can leave empty areas.
  605. *
  606. * @return an array with 2 elements, the video width and the video height
  607. */
  608. function getDesktopVideoSize(videoWidth,
  609. videoHeight,
  610. videoSpaceWidth,
  611. videoSpaceHeight) {
  612. if (!videoWidth)
  613. videoWidth = currentVideoWidth;
  614. if (!videoHeight)
  615. videoHeight = currentVideoHeight;
  616. var aspectRatio = videoWidth / videoHeight;
  617. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  618. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  619. videoSpaceHeight -= $('#remoteVideos').outerHeight();
  620. if (availableWidth / aspectRatio >= videoSpaceHeight)
  621. {
  622. availableHeight = videoSpaceHeight;
  623. availableWidth = availableHeight * aspectRatio;
  624. }
  625. if (availableHeight * aspectRatio >= videoSpaceWidth)
  626. {
  627. availableWidth = videoSpaceWidth;
  628. availableHeight = availableWidth / aspectRatio;
  629. }
  630. return [availableWidth, availableHeight];
  631. }
  632. /**
  633. * Creates the edit display name button.
  634. *
  635. * @returns the edit button
  636. */
  637. function createEditDisplayNameButton() {
  638. var editButton = document.createElement('a');
  639. editButton.className = 'displayname';
  640. Util.setTooltip(editButton,
  641. 'Click to edit your<br/>display name',
  642. "top");
  643. editButton.innerHTML = '<i class="fa fa-pencil"></i>';
  644. return editButton;
  645. }
  646. /**
  647. * Creates the element indicating the focus of the conference.
  648. *
  649. * @param parentElement the parent element where the focus indicator will
  650. * be added
  651. */
  652. function createFocusIndicatorElement(parentElement) {
  653. var focusIndicator = document.createElement('i');
  654. focusIndicator.className = 'fa fa-star';
  655. parentElement.appendChild(focusIndicator);
  656. Util.setTooltip(parentElement,
  657. "The owner of<br/>this conference",
  658. "top");
  659. }
  660. /**
  661. * Updates the remote video menu.
  662. *
  663. * @param jid the jid indicating the video for which we're adding a menu.
  664. * @param isMuted indicates the current mute state
  665. */
  666. my.updateRemoteVideoMenu = function(jid, isMuted) {
  667. var muteMenuItem
  668. = $('#remote_popupmenu_'
  669. + Strophe.getResourceFromJid(jid)
  670. + '>li>a.mutelink');
  671. var mutedIndicator = "<i class='icon-mic-disabled'></i>";
  672. if (muteMenuItem.length) {
  673. var muteLink = muteMenuItem.get(0);
  674. if (isMuted === 'true') {
  675. muteLink.innerHTML = mutedIndicator + ' Muted';
  676. muteLink.className = 'mutelink disabled';
  677. }
  678. else {
  679. muteLink.innerHTML = mutedIndicator + ' Mute';
  680. muteLink.className = 'mutelink';
  681. }
  682. }
  683. };
  684. /**
  685. * Returns the current dominant speaker resource jid.
  686. */
  687. my.getDominantSpeakerResourceJid = function () {
  688. return currentDominantSpeaker;
  689. };
  690. /**
  691. * Adds the remote video menu element for the given <tt>jid</tt> in the
  692. * given <tt>parentElement</tt>.
  693. *
  694. * @param jid the jid indicating the video for which we're adding a menu.
  695. * @param parentElement the parent element where this menu will be added
  696. */
  697. function addRemoteVideoMenu(jid, parentElement) {
  698. var spanElement = document.createElement('span');
  699. spanElement.className = 'remotevideomenu';
  700. parentElement.appendChild(spanElement);
  701. var menuElement = document.createElement('i');
  702. menuElement.className = 'fa fa-angle-down';
  703. menuElement.title = 'Remote user controls';
  704. spanElement.appendChild(menuElement);
  705. // <ul class="popupmenu">
  706. // <li><a href="#">Mute</a></li>
  707. // <li><a href="#">Eject</a></li>
  708. // </ul>
  709. var popupmenuElement = document.createElement('ul');
  710. popupmenuElement.className = 'popupmenu';
  711. popupmenuElement.id
  712. = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
  713. spanElement.appendChild(popupmenuElement);
  714. var muteMenuItem = document.createElement('li');
  715. var muteLinkItem = document.createElement('a');
  716. var mutedIndicator = "<i class='icon-mic-disabled'></i>";
  717. if (!mutedAudios[jid]) {
  718. muteLinkItem.innerHTML = mutedIndicator + 'Mute';
  719. muteLinkItem.className = 'mutelink';
  720. }
  721. else {
  722. muteLinkItem.innerHTML = mutedIndicator + ' Muted';
  723. muteLinkItem.className = 'mutelink disabled';
  724. }
  725. muteLinkItem.onclick = function(){
  726. if ($(this).attr('disabled') != undefined) {
  727. event.preventDefault();
  728. }
  729. var isMute = !mutedAudios[jid];
  730. connection.moderate.setMute(jid, isMute);
  731. popupmenuElement.setAttribute('style', 'display:none;');
  732. if (isMute) {
  733. this.innerHTML = mutedIndicator + ' Muted';
  734. this.className = 'mutelink disabled';
  735. }
  736. else {
  737. this.innerHTML = mutedIndicator + ' Mute';
  738. this.className = 'mutelink';
  739. }
  740. };
  741. muteMenuItem.appendChild(muteLinkItem);
  742. popupmenuElement.appendChild(muteMenuItem);
  743. var ejectIndicator = "<i class='fa fa-eject'></i>";
  744. var ejectMenuItem = document.createElement('li');
  745. var ejectLinkItem = document.createElement('a');
  746. ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
  747. ejectLinkItem.onclick = function(){
  748. connection.moderate.eject(jid);
  749. popupmenuElement.setAttribute('style', 'display:none;');
  750. };
  751. ejectMenuItem.appendChild(ejectLinkItem);
  752. popupmenuElement.appendChild(ejectMenuItem);
  753. }
  754. /**
  755. * On audio muted event.
  756. */
  757. $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
  758. var videoSpanId = null;
  759. if (jid === connection.emuc.myroomjid) {
  760. videoSpanId = 'localVideoContainer';
  761. } else {
  762. VideoLayout.ensurePeerContainerExists(jid);
  763. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  764. }
  765. if (focus) {
  766. mutedAudios[jid] = isMuted;
  767. VideoLayout.updateRemoteVideoMenu(jid, isMuted);
  768. }
  769. if (videoSpanId)
  770. VideoLayout.showAudioIndicator(videoSpanId, isMuted);
  771. });
  772. /**
  773. * On video muted event.
  774. */
  775. $(document).bind('videomuted.muc', function (event, jid, isMuted) {
  776. var videoSpanId = null;
  777. if (jid === connection.emuc.myroomjid) {
  778. videoSpanId = 'localVideoContainer';
  779. } else {
  780. VideoLayout.ensurePeerContainerExists(jid);
  781. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  782. }
  783. if (videoSpanId)
  784. VideoLayout.showVideoIndicator(videoSpanId, isMuted);
  785. });
  786. /**
  787. * On dominant speaker changed event.
  788. */
  789. $(document).bind('dominantspeakerchanged', function (event, resourceJid) {
  790. // We ignore local user events.
  791. if (resourceJid
  792. === Strophe.getResourceFromJid(connection.emuc.myroomjid))
  793. return;
  794. // Obtain container for new dominant speaker.
  795. var container = document.getElementById(
  796. 'participant_' + resourceJid);
  797. // Update the current dominant speaker.
  798. if (resourceJid !== currentDominantSpeaker)
  799. currentDominantSpeaker = resourceJid;
  800. else
  801. return;
  802. // Local video will not have container found, but that's ok
  803. // since we don't want to switch to local video.
  804. if (container && !focusedVideoSrc)
  805. {
  806. var video = container.getElementsByTagName("video");
  807. if (video.length)
  808. {
  809. VideoLayout.updateLargeVideo(video[0].src);
  810. }
  811. }
  812. });
  813. return my;
  814. }(VideoLayout || {}));