Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

videolayout.js 45KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292
  1. var VideoLayout = (function (my) {
  2. var preMuted = false;
  3. var currentDominantSpeaker = null;
  4. var lastNCount = config.channelLastN;
  5. var lastNEndpointsCache = [];
  6. my.changeLocalAudio = function(stream) {
  7. connection.jingle.localAudio = stream;
  8. RTC.attachMediaStream($('#localAudio'), stream);
  9. document.getElementById('localAudio').autoplay = true;
  10. document.getElementById('localAudio').volume = 0;
  11. if (preMuted) {
  12. toggleAudio();
  13. preMuted = false;
  14. }
  15. };
  16. my.changeLocalVideo = function(stream, flipX) {
  17. connection.jingle.localVideo = stream;
  18. var localVideo = document.createElement('video');
  19. localVideo.id = 'localVideo_' + stream.id;
  20. localVideo.autoplay = true;
  21. localVideo.volume = 0; // is it required if audio is separated ?
  22. localVideo.oncontextmenu = function () { return false; };
  23. var localVideoContainer = document.getElementById('localVideoWrapper');
  24. localVideoContainer.appendChild(localVideo);
  25. // Set default display name.
  26. setDisplayName('localVideoContainer');
  27. AudioLevels.updateAudioLevelCanvas();
  28. var localVideoSelector = $('#' + localVideo.id);
  29. // Add click handler to both video and video wrapper elements in case
  30. // there's no video.
  31. localVideoSelector.click(function () {
  32. VideoLayout.handleVideoThumbClicked(localVideo.src);
  33. });
  34. $('#localVideoContainer').click(function () {
  35. VideoLayout.handleVideoThumbClicked(localVideo.src);
  36. });
  37. // Add hover handler
  38. $('#localVideoContainer').hover(
  39. function() {
  40. VideoLayout.showDisplayName('localVideoContainer', true);
  41. },
  42. function() {
  43. if (!VideoLayout.isLargeVideoVisible()
  44. || localVideo.src !== $('#largeVideo').attr('src'))
  45. VideoLayout.showDisplayName('localVideoContainer', false);
  46. }
  47. );
  48. // Add stream ended handler
  49. stream.onended = function () {
  50. localVideoContainer.removeChild(localVideo);
  51. VideoLayout.updateRemovedVideo(localVideo.src);
  52. };
  53. // Flip video x axis if needed
  54. flipXLocalVideo = flipX;
  55. if (flipX) {
  56. localVideoSelector.addClass("flipVideoX");
  57. }
  58. // Attach WebRTC stream
  59. RTC.attachMediaStream(localVideoSelector, stream);
  60. localVideoSrc = localVideo.src;
  61. VideoLayout.updateLargeVideo(localVideoSrc, 0);
  62. };
  63. /**
  64. * Checks if removed video is currently displayed and tries to display
  65. * another one instead.
  66. * @param removedVideoSrc src stream identifier of the video.
  67. */
  68. my.updateRemovedVideo = function(removedVideoSrc) {
  69. if (removedVideoSrc === $('#largeVideo').attr('src')) {
  70. // this is currently displayed as large
  71. // pick the last visible video in the row
  72. // if nobody else is left, this picks the local video
  73. var pick
  74. = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
  75. .get(0);
  76. if (!pick) {
  77. console.info("Last visible video no longer exists");
  78. pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
  79. if (!pick || !pick.src) {
  80. // Try local video
  81. console.info("Fallback to local video...");
  82. pick = $('#remoteVideos>span>span>video').get(0);
  83. }
  84. }
  85. // mute if localvideo
  86. if (pick) {
  87. VideoLayout.updateLargeVideo(pick.src, pick.volume);
  88. } else {
  89. console.warn("Failed to elect large video");
  90. }
  91. }
  92. };
  93. /**
  94. * Updates the large video with the given new video source.
  95. */
  96. my.updateLargeVideo = function(newSrc, vol) {
  97. console.log('hover in', newSrc);
  98. if ($('#largeVideo').attr('src') != newSrc) {
  99. var isVisible = $('#largeVideo').is(':visible');
  100. $('#largeVideo').fadeOut(300, function () {
  101. var oldSrc = $(this).attr('src');
  102. $(this).attr('src', newSrc);
  103. // Screen stream is already rotated
  104. var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
  105. var videoTransform = document.getElementById('largeVideo')
  106. .style.webkitTransform;
  107. if (flipX && videoTransform !== 'scaleX(-1)') {
  108. document.getElementById('largeVideo').style.webkitTransform
  109. = "scaleX(-1)";
  110. }
  111. else if (!flipX && videoTransform === 'scaleX(-1)') {
  112. document.getElementById('largeVideo').style.webkitTransform
  113. = "none";
  114. }
  115. // Change the way we'll be measuring and positioning large video
  116. var isDesktop = isVideoSrcDesktop(newSrc);
  117. getVideoSize = isDesktop
  118. ? getDesktopVideoSize
  119. : getCameraVideoSize;
  120. getVideoPosition = isDesktop
  121. ? getDesktopVideoPosition
  122. : getCameraVideoPosition;
  123. if (isVisible) {
  124. // Only if the large video is currently visible.
  125. // Disable previous dominant speaker video.
  126. var oldJid = getJidFromVideoSrc(oldSrc);
  127. if (oldJid) {
  128. var oldResourceJid = Strophe.getResourceFromJid(oldJid);
  129. VideoLayout.enableDominantSpeaker(oldResourceJid, false);
  130. }
  131. // Enable new dominant speaker in the remote videos section.
  132. var userJid = getJidFromVideoSrc(newSrc);
  133. if (userJid)
  134. {
  135. var resourceJid = Strophe.getResourceFromJid(userJid);
  136. VideoLayout.enableDominantSpeaker(resourceJid, true);
  137. }
  138. $(this).fadeIn(300);
  139. }
  140. });
  141. }
  142. };
  143. my.handleVideoThumbClicked = function(videoSrc) {
  144. // Restore style for previously focused video
  145. var focusJid = getJidFromVideoSrc(focusedVideoSrc);
  146. var oldContainer = getParticipantContainer(focusJid);
  147. if (oldContainer) {
  148. oldContainer.removeClass("videoContainerFocused");
  149. }
  150. // Unlock current focused.
  151. if (focusedVideoSrc === videoSrc)
  152. {
  153. focusedVideoSrc = null;
  154. var dominantSpeakerVideo = null;
  155. // Enable the currently set dominant speaker.
  156. if (currentDominantSpeaker) {
  157. dominantSpeakerVideo
  158. = $('#participant_' + currentDominantSpeaker + '>video')
  159. .get(0);
  160. if (dominantSpeakerVideo) {
  161. VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1);
  162. }
  163. }
  164. return;
  165. }
  166. // Lock new video
  167. focusedVideoSrc = videoSrc;
  168. // Update focused/pinned interface.
  169. var userJid = getJidFromVideoSrc(videoSrc);
  170. if (userJid)
  171. {
  172. var container = getParticipantContainer(userJid);
  173. container.addClass("videoContainerFocused");
  174. }
  175. // Triggers a "video.selected" event. The "false" parameter indicates
  176. // this isn't a prezi.
  177. $(document).trigger("video.selected", [false]);
  178. VideoLayout.updateLargeVideo(videoSrc, 1);
  179. $('audio').each(function (idx, el) {
  180. if (el.id.indexOf('mixedmslabel') !== -1) {
  181. el.volume = 0;
  182. el.volume = 1;
  183. }
  184. });
  185. };
  186. /**
  187. * Positions the large video.
  188. *
  189. * @param videoWidth the stream video width
  190. * @param videoHeight the stream video height
  191. */
  192. my.positionLarge = function (videoWidth, videoHeight) {
  193. var videoSpaceWidth = $('#videospace').width();
  194. var videoSpaceHeight = window.innerHeight;
  195. var videoSize = getVideoSize(videoWidth,
  196. videoHeight,
  197. videoSpaceWidth,
  198. videoSpaceHeight);
  199. var largeVideoWidth = videoSize[0];
  200. var largeVideoHeight = videoSize[1];
  201. var videoPosition = getVideoPosition(largeVideoWidth,
  202. largeVideoHeight,
  203. videoSpaceWidth,
  204. videoSpaceHeight);
  205. var horizontalIndent = videoPosition[0];
  206. var verticalIndent = videoPosition[1];
  207. positionVideo($('#largeVideo'),
  208. largeVideoWidth,
  209. largeVideoHeight,
  210. horizontalIndent, verticalIndent);
  211. };
  212. /**
  213. * Shows/hides the large video.
  214. */
  215. my.setLargeVideoVisible = function(isVisible) {
  216. var largeVideoJid = getJidFromVideoSrc($('#largeVideo').attr('src'));
  217. var resourceJid = Strophe.getResourceFromJid(largeVideoJid);
  218. if (isVisible) {
  219. $('#largeVideo').css({visibility: 'visible'});
  220. $('.watermark').css({visibility: 'visible'});
  221. VideoLayout.enableDominantSpeaker(resourceJid, true);
  222. }
  223. else {
  224. $('#largeVideo').css({visibility: 'hidden'});
  225. $('.watermark').css({visibility: 'hidden'});
  226. VideoLayout.enableDominantSpeaker(resourceJid, false);
  227. }
  228. };
  229. /**
  230. * Indicates if the large video is currently visible.
  231. *
  232. * @return <tt>true</tt> if visible, <tt>false</tt> - otherwise
  233. */
  234. my.isLargeVideoVisible = function() {
  235. return $('#largeVideo').is(':visible');
  236. };
  237. /**
  238. * Checks if container for participant identified by given peerJid exists
  239. * in the document and creates it eventually.
  240. *
  241. * @param peerJid peer Jid to check.
  242. *
  243. * @return Returns <tt>true</tt> if the peer container exists,
  244. * <tt>false</tt> - otherwise
  245. */
  246. my.ensurePeerContainerExists = function(peerJid) {
  247. ContactList.ensureAddContact(peerJid);
  248. var resourceJid = Strophe.getResourceFromJid(peerJid);
  249. var videoSpanId = 'participant_' + resourceJid;
  250. if ($('#' + videoSpanId).length > 0) {
  251. // If there's been a focus change, make sure we add focus related
  252. // interface!!
  253. if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0)
  254. addRemoteVideoMenu( peerJid,
  255. document.getElementById(videoSpanId));
  256. }
  257. else {
  258. var container
  259. = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
  260. // Set default display name.
  261. setDisplayName(videoSpanId);
  262. var nickfield = document.createElement('span');
  263. nickfield.className = "nick";
  264. nickfield.appendChild(document.createTextNode(resourceJid));
  265. container.appendChild(nickfield);
  266. // In case this is not currently in the last n we don't show it.
  267. if (lastNCount
  268. && lastNCount > 0
  269. && $('#remoteVideos>span').length >= lastNCount + 2) {
  270. showPeerContainer(resourceJid, false);
  271. }
  272. else
  273. VideoLayout.resizeThumbnails();
  274. }
  275. };
  276. my.addRemoteVideoContainer = function(peerJid, spanId) {
  277. var container = document.createElement('span');
  278. container.id = spanId;
  279. container.className = 'videocontainer';
  280. var remotes = document.getElementById('remoteVideos');
  281. // If the peerJid is null then this video span couldn't be directly
  282. // associated with a participant (this could happen in the case of prezi).
  283. if (focus && peerJid != null)
  284. addRemoteVideoMenu(peerJid, container);
  285. remotes.appendChild(container);
  286. AudioLevels.updateAudioLevelCanvas(peerJid);
  287. return container;
  288. };
  289. /**
  290. * Creates an audio or video stream element.
  291. */
  292. my.createStreamElement = function (sid, stream) {
  293. var isVideo = stream.getVideoTracks().length > 0;
  294. var element = isVideo
  295. ? document.createElement('video')
  296. : document.createElement('audio');
  297. var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_')
  298. + sid + '_' + stream.id;
  299. element.id = id;
  300. element.autoplay = true;
  301. element.oncontextmenu = function () { return false; };
  302. return element;
  303. };
  304. my.addRemoteStreamElement
  305. = function (container, sid, stream, peerJid, thessrc) {
  306. var newElementId = null;
  307. var isVideo = stream.getVideoTracks().length > 0;
  308. if (container) {
  309. var streamElement = VideoLayout.createStreamElement(sid, stream);
  310. newElementId = streamElement.id;
  311. container.appendChild(streamElement);
  312. var sel = $('#' + newElementId);
  313. sel.hide();
  314. // If the container is currently visible we attach the stream.
  315. if (!isVideo
  316. || (container.offsetParent !== null && isVideo)) {
  317. RTC.attachMediaStream(sel, stream);
  318. if (isVideo)
  319. waitForRemoteVideo(sel, thessrc, stream);
  320. }
  321. stream.onended = function () {
  322. console.log('stream ended', this);
  323. VideoLayout.removeRemoteStreamElement(stream, container);
  324. if (peerJid)
  325. ContactList.removeContact(peerJid);
  326. };
  327. // Add click handler.
  328. container.onclick = function (event) {
  329. /*
  330. * FIXME It turns out that videoThumb may not exist (if there is
  331. * no actual video).
  332. */
  333. var videoThumb = $('#' + container.id + '>video').get(0);
  334. if (videoThumb)
  335. VideoLayout.handleVideoThumbClicked(videoThumb.src);
  336. event.preventDefault();
  337. return false;
  338. };
  339. // Add hover handler
  340. $(container).hover(
  341. function() {
  342. VideoLayout.showDisplayName(container.id, true);
  343. },
  344. function() {
  345. var videoSrc = null;
  346. if ($('#' + container.id + '>video')
  347. && $('#' + container.id + '>video').length > 0) {
  348. videoSrc = $('#' + container.id + '>video').get(0).src;
  349. }
  350. // If the video has been "pinned" by the user we want to
  351. // keep the display name on place.
  352. if (!VideoLayout.isLargeVideoVisible()
  353. || videoSrc !== $('#largeVideo').attr('src'))
  354. VideoLayout.showDisplayName(container.id, false);
  355. }
  356. );
  357. }
  358. return newElementId;
  359. };
  360. /**
  361. * Removes the remote stream element corresponding to the given stream and
  362. * parent container.
  363. *
  364. * @param stream the stream
  365. * @param container
  366. */
  367. my.removeRemoteStreamElement = function (stream, container) {
  368. if (!container)
  369. return;
  370. var select = null;
  371. var removedVideoSrc = null;
  372. if (stream.getVideoTracks().length > 0) {
  373. select = $('#' + container.id + '>video');
  374. removedVideoSrc = select.get(0).src;
  375. }
  376. else
  377. select = $('#' + container.id + '>audio');
  378. // Remove video source from the mapping.
  379. delete videoSrcToSsrc[removedVideoSrc];
  380. // Mark video as removed to cancel waiting loop(if video is removed
  381. // before has started)
  382. select.removed = true;
  383. select.remove();
  384. var audioCount = $('#' + container.id + '>audio').length;
  385. var videoCount = $('#' + container.id + '>video').length;
  386. if (!audioCount && !videoCount) {
  387. console.log("Remove whole user", container.id);
  388. // Remove whole container
  389. container.remove();
  390. Util.playSoundNotification('userLeft');
  391. VideoLayout.resizeThumbnails();
  392. }
  393. if (removedVideoSrc)
  394. VideoLayout.updateRemovedVideo(removedVideoSrc);
  395. };
  396. /**
  397. * Show/hide peer container for the given resourceJid.
  398. */
  399. function showPeerContainer(resourceJid, isShow) {
  400. var peerContainer = $('#participant_' + resourceJid);
  401. if (!peerContainer)
  402. return;
  403. if (!peerContainer.is(':visible') && isShow)
  404. peerContainer.show();
  405. else if (peerContainer.is(':visible') && !isShow)
  406. peerContainer.hide();
  407. };
  408. /**
  409. * Sets the display name for the given video span id.
  410. */
  411. function setDisplayName(videoSpanId, displayName) {
  412. var nameSpan = $('#' + videoSpanId + '>span.displayname');
  413. var defaultLocalDisplayName = "Me";
  414. var defaultRemoteDisplayName = "Speaker";
  415. // If we already have a display name for this video.
  416. if (nameSpan.length > 0) {
  417. var nameSpanElement = nameSpan.get(0);
  418. if (nameSpanElement.id === 'localDisplayName' &&
  419. $('#localDisplayName').text() !== displayName) {
  420. if (displayName && displayName.length > 0)
  421. $('#localDisplayName').text(displayName + ' (me)');
  422. else
  423. $('#localDisplayName').text(defaultLocalDisplayName);
  424. } else {
  425. if (displayName && displayName.length > 0)
  426. $('#' + videoSpanId + '_name').text(displayName);
  427. else
  428. $('#' + videoSpanId + '_name').text(defaultRemoteDisplayName);
  429. }
  430. } else {
  431. var editButton = null;
  432. nameSpan = document.createElement('span');
  433. nameSpan.className = 'displayname';
  434. $('#' + videoSpanId)[0].appendChild(nameSpan);
  435. if (videoSpanId === 'localVideoContainer') {
  436. editButton = createEditDisplayNameButton();
  437. nameSpan.innerText = defaultLocalDisplayName;
  438. }
  439. else {
  440. nameSpan.innerText = defaultRemoteDisplayName;
  441. }
  442. if (displayName && displayName.length > 0) {
  443. nameSpan.innerText = displayName;
  444. }
  445. if (!editButton) {
  446. nameSpan.id = videoSpanId + '_name';
  447. } else {
  448. nameSpan.id = 'localDisplayName';
  449. $('#' + videoSpanId)[0].appendChild(editButton);
  450. var editableText = document.createElement('input');
  451. editableText.className = 'displayname';
  452. editableText.id = 'editDisplayName';
  453. if (displayName && displayName.length) {
  454. editableText.value
  455. = displayName.substring(0, displayName.indexOf(' (me)'));
  456. }
  457. editableText.setAttribute('style', 'display:none;');
  458. editableText.setAttribute('placeholder', 'ex. Jane Pink');
  459. $('#' + videoSpanId)[0].appendChild(editableText);
  460. $('#localVideoContainer .displayname')
  461. .bind("click", function (e) {
  462. e.preventDefault();
  463. $('#localDisplayName').hide();
  464. $('#editDisplayName').show();
  465. $('#editDisplayName').focus();
  466. $('#editDisplayName').select();
  467. var inputDisplayNameHandler = function (name) {
  468. if (nickname !== name) {
  469. nickname = name;
  470. window.localStorage.displayname = nickname;
  471. connection.emuc.addDisplayNameToPresence(nickname);
  472. connection.emuc.sendPresence();
  473. Chat.setChatConversationMode(true);
  474. }
  475. if (!$('#localDisplayName').is(":visible")) {
  476. if (nickname)
  477. $('#localDisplayName').text(nickname + " (me)");
  478. else
  479. $('#localDisplayName')
  480. .text(defaultLocalDisplayName);
  481. $('#localDisplayName').show();
  482. }
  483. $('#editDisplayName').hide();
  484. };
  485. $('#editDisplayName').one("focusout", function (e) {
  486. inputDisplayNameHandler(this.value);
  487. });
  488. $('#editDisplayName').on('keydown', function (e) {
  489. if (e.keyCode === 13) {
  490. e.preventDefault();
  491. inputDisplayNameHandler(this.value);
  492. }
  493. });
  494. });
  495. }
  496. }
  497. };
  498. /**
  499. * Shows/hides the display name on the remote video.
  500. * @param videoSpanId the identifier of the video span element
  501. * @param isShow indicates if the display name should be shown or hidden
  502. */
  503. my.showDisplayName = function(videoSpanId, isShow) {
  504. var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
  505. if (isShow) {
  506. if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
  507. nameSpan.setAttribute("style", "display:inline-block;");
  508. }
  509. else {
  510. if (nameSpan)
  511. nameSpan.setAttribute("style", "display:none;");
  512. }
  513. };
  514. /**
  515. * Shows the presence status message for the given video.
  516. */
  517. my.setPresenceStatus = function (videoSpanId, statusMsg) {
  518. if (!$('#' + videoSpanId).length) {
  519. // No container
  520. return;
  521. }
  522. var statusSpan = $('#' + videoSpanId + '>span.status');
  523. if (!statusSpan.length) {
  524. //Add status span
  525. statusSpan = document.createElement('span');
  526. statusSpan.className = 'status';
  527. statusSpan.id = videoSpanId + '_status';
  528. $('#' + videoSpanId)[0].appendChild(statusSpan);
  529. statusSpan = $('#' + videoSpanId + '>span.status');
  530. }
  531. // Display status
  532. if (statusMsg && statusMsg.length) {
  533. $('#' + videoSpanId + '_status').text(statusMsg);
  534. statusSpan.get(0).setAttribute("style", "display:inline-block;");
  535. }
  536. else {
  537. // Hide
  538. statusSpan.get(0).setAttribute("style", "display:none;");
  539. }
  540. };
  541. /**
  542. * Shows a visual indicator for the focus of the conference.
  543. * Currently if we're not the owner of the conference we obtain the focus
  544. * from the connection.jingle.sessions.
  545. */
  546. my.showFocusIndicator = function() {
  547. if (focus !== null) {
  548. var indicatorSpan = $('#localVideoContainer .focusindicator');
  549. if (indicatorSpan.children().length === 0)
  550. {
  551. createFocusIndicatorElement(indicatorSpan[0]);
  552. }
  553. }
  554. else if (Object.keys(connection.jingle.sessions).length > 0) {
  555. // If we're only a participant the focus will be the only session we have.
  556. var session
  557. = connection.jingle.sessions
  558. [Object.keys(connection.jingle.sessions)[0]];
  559. var focusId
  560. = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
  561. var focusContainer = document.getElementById(focusId);
  562. if (!focusContainer) {
  563. console.error("No focus container!");
  564. return;
  565. }
  566. var indicatorSpan = $('#' + focusId + ' .focusindicator');
  567. if (!indicatorSpan || indicatorSpan.length === 0) {
  568. indicatorSpan = document.createElement('span');
  569. indicatorSpan.className = 'focusindicator';
  570. focusContainer.appendChild(indicatorSpan);
  571. createFocusIndicatorElement(indicatorSpan);
  572. }
  573. }
  574. };
  575. /**
  576. * Shows video muted indicator over small videos.
  577. */
  578. my.showVideoIndicator = function(videoSpanId, isMuted) {
  579. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  580. if (isMuted === 'false') {
  581. if (videoMutedSpan.length > 0) {
  582. videoMutedSpan.remove();
  583. }
  584. }
  585. else {
  586. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  587. videoMutedSpan = document.createElement('span');
  588. videoMutedSpan.className = 'videoMuted';
  589. if (audioMutedSpan) {
  590. videoMutedSpan.right = '30px';
  591. }
  592. $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
  593. var mutedIndicator = document.createElement('i');
  594. mutedIndicator.className = 'icon-camera-disabled';
  595. Util.setTooltip(mutedIndicator,
  596. "Participant has<br/>stopped the camera.",
  597. "top");
  598. videoMutedSpan.appendChild(mutedIndicator);
  599. }
  600. };
  601. /**
  602. * Shows audio muted indicator over small videos.
  603. */
  604. my.showAudioIndicator = function(videoSpanId, isMuted) {
  605. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  606. if (isMuted === 'false') {
  607. if (audioMutedSpan.length > 0) {
  608. audioMutedSpan.remove();
  609. }
  610. }
  611. else {
  612. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  613. audioMutedSpan = document.createElement('span');
  614. audioMutedSpan.className = 'audioMuted';
  615. Util.setTooltip(audioMutedSpan,
  616. "Participant is muted",
  617. "top");
  618. if (videoMutedSpan) {
  619. audioMutedSpan.right = '30px';
  620. }
  621. $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
  622. var mutedIndicator = document.createElement('i');
  623. mutedIndicator.className = 'icon-mic-disabled';
  624. audioMutedSpan.appendChild(mutedIndicator);
  625. }
  626. };
  627. /**
  628. * Resizes the large video container.
  629. */
  630. my.resizeLargeVideoContainer = function () {
  631. Chat.resizeChat();
  632. var availableHeight = window.innerHeight;
  633. var availableWidth = Util.getAvailableVideoWidth();
  634. if (availableWidth < 0 || availableHeight < 0) return;
  635. $('#videospace').width(availableWidth);
  636. $('#videospace').height(availableHeight);
  637. $('#largeVideoContainer').width(availableWidth);
  638. $('#largeVideoContainer').height(availableHeight);
  639. VideoLayout.resizeThumbnails();
  640. };
  641. /**
  642. * Resizes thumbnails.
  643. */
  644. my.resizeThumbnails = function() {
  645. var videoSpaceWidth = $('#remoteVideos').width();
  646. var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
  647. var width = thumbnailSize[0];
  648. var height = thumbnailSize[1];
  649. // size videos so that while keeping AR and max height, we have a
  650. // nice fit
  651. $('#remoteVideos').height(height);
  652. $('#remoteVideos>span').width(width);
  653. $('#remoteVideos>span').height(height);
  654. $(document).trigger("remotevideo.resized", [width, height]);
  655. };
  656. /**
  657. * Enables the dominant speaker UI.
  658. *
  659. * @param resourceJid the jid indicating the video element to
  660. * activate/deactivate
  661. * @param isEnable indicates if the dominant speaker should be enabled or
  662. * disabled
  663. */
  664. my.enableDominantSpeaker = function(resourceJid, isEnable) {
  665. var displayName = resourceJid;
  666. var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
  667. if (nameSpan.length > 0)
  668. displayName = nameSpan.text();
  669. console.log("UI enable dominant speaker",
  670. displayName,
  671. resourceJid,
  672. isEnable);
  673. var videoSpanId = null;
  674. var videoContainerId = null;
  675. if (resourceJid
  676. === Strophe.getResourceFromJid(connection.emuc.myroomjid)) {
  677. videoSpanId = 'localVideoWrapper';
  678. videoContainerId = 'localVideoContainer';
  679. }
  680. else {
  681. videoSpanId = 'participant_' + resourceJid;
  682. videoContainerId = videoSpanId;
  683. }
  684. videoSpan = document.getElementById(videoContainerId);
  685. if (!videoSpan) {
  686. console.error("No video element for jid", resourceJid);
  687. return;
  688. }
  689. var video = $('#' + videoSpanId + '>video');
  690. if (video && video.length > 0) {
  691. if (isEnable) {
  692. VideoLayout.showDisplayName(videoContainerId, true);
  693. if (!videoSpan.classList.contains("dominantspeaker"))
  694. videoSpan.classList.add("dominantspeaker");
  695. video.css({visibility: 'hidden'});
  696. }
  697. else {
  698. VideoLayout.showDisplayName(videoContainerId, false);
  699. if (videoSpan.classList.contains("dominantspeaker"))
  700. videoSpan.classList.remove("dominantspeaker");
  701. video.css({visibility: 'visible'});
  702. }
  703. }
  704. };
  705. /**
  706. * Gets the selector of video thumbnail container for the user identified by
  707. * given <tt>userJid</tt>
  708. * @param userJid user's Jid for whom we want to get the video container.
  709. */
  710. function getParticipantContainer(userJid)
  711. {
  712. if (!userJid)
  713. return null;
  714. if (userJid === connection.emuc.myroomjid)
  715. return $("#localVideoContainer");
  716. else
  717. return $("#participant_" + Strophe.getResourceFromJid(userJid));
  718. }
  719. /**
  720. * Sets the size and position of the given video element.
  721. *
  722. * @param video the video element to position
  723. * @param width the desired video width
  724. * @param height the desired video height
  725. * @param horizontalIndent the left and right indent
  726. * @param verticalIndent the top and bottom indent
  727. */
  728. function positionVideo(video,
  729. width,
  730. height,
  731. horizontalIndent,
  732. verticalIndent) {
  733. video.width(width);
  734. video.height(height);
  735. video.css({ top: verticalIndent + 'px',
  736. bottom: verticalIndent + 'px',
  737. left: horizontalIndent + 'px',
  738. right: horizontalIndent + 'px'});
  739. }
  740. /**
  741. * Calculates the thumbnail size.
  742. *
  743. * @param videoSpaceWidth the width of the video space
  744. */
  745. my.calculateThumbnailSize = function (videoSpaceWidth) {
  746. // Calculate the available height, which is the inner window height minus
  747. // 39px for the header minus 2px for the delimiter lines on the top and
  748. // bottom of the large video, minus the 36px space inside the remoteVideos
  749. // container used for highlighting shadow.
  750. var availableHeight = 100;
  751. var numvids = 0;
  752. if (lastNCount && lastNCount > 0)
  753. numvids = lastNCount + 1;
  754. else
  755. numvids = $('#remoteVideos>span:visible').length;
  756. // Remove the 3px borders arround videos and border around the remote
  757. // videos area
  758. var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70;
  759. var availableWidth = availableWinWidth / numvids;
  760. var aspectRatio = 16.0 / 9.0;
  761. var maxHeight = Math.min(160, availableHeight);
  762. availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
  763. if (availableHeight < availableWidth / aspectRatio) {
  764. availableWidth = Math.floor(availableHeight * aspectRatio);
  765. }
  766. return [availableWidth, availableHeight];
  767. };
  768. /**
  769. * Returns an array of the video dimensions, so that it keeps it's aspect
  770. * ratio and fits available area with it's larger dimension. This method
  771. * ensures that whole video will be visible and can leave empty areas.
  772. *
  773. * @return an array with 2 elements, the video width and the video height
  774. */
  775. function getDesktopVideoSize(videoWidth,
  776. videoHeight,
  777. videoSpaceWidth,
  778. videoSpaceHeight) {
  779. if (!videoWidth)
  780. videoWidth = currentVideoWidth;
  781. if (!videoHeight)
  782. videoHeight = currentVideoHeight;
  783. var aspectRatio = videoWidth / videoHeight;
  784. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  785. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  786. videoSpaceHeight -= $('#remoteVideos').outerHeight();
  787. if (availableWidth / aspectRatio >= videoSpaceHeight)
  788. {
  789. availableHeight = videoSpaceHeight;
  790. availableWidth = availableHeight * aspectRatio;
  791. }
  792. if (availableHeight * aspectRatio >= videoSpaceWidth)
  793. {
  794. availableWidth = videoSpaceWidth;
  795. availableHeight = availableWidth / aspectRatio;
  796. }
  797. return [availableWidth, availableHeight];
  798. }
  799. /**
  800. * Creates the edit display name button.
  801. *
  802. * @returns the edit button
  803. */
  804. function createEditDisplayNameButton() {
  805. var editButton = document.createElement('a');
  806. editButton.className = 'displayname';
  807. Util.setTooltip(editButton,
  808. 'Click to edit your<br/>display name',
  809. "top");
  810. editButton.innerHTML = '<i class="fa fa-pencil"></i>';
  811. return editButton;
  812. }
  813. /**
  814. * Creates the element indicating the focus of the conference.
  815. *
  816. * @param parentElement the parent element where the focus indicator will
  817. * be added
  818. */
  819. function createFocusIndicatorElement(parentElement) {
  820. var focusIndicator = document.createElement('i');
  821. focusIndicator.className = 'fa fa-star';
  822. parentElement.appendChild(focusIndicator);
  823. Util.setTooltip(parentElement,
  824. "The owner of<br/>this conference",
  825. "top");
  826. }
  827. /**
  828. * Updates the remote video menu.
  829. *
  830. * @param jid the jid indicating the video for which we're adding a menu.
  831. * @param isMuted indicates the current mute state
  832. */
  833. my.updateRemoteVideoMenu = function(jid, isMuted) {
  834. var muteMenuItem
  835. = $('#remote_popupmenu_'
  836. + Strophe.getResourceFromJid(jid)
  837. + '>li>a.mutelink');
  838. var mutedIndicator = "<i class='icon-mic-disabled'></i>";
  839. if (muteMenuItem.length) {
  840. var muteLink = muteMenuItem.get(0);
  841. if (isMuted === 'true') {
  842. muteLink.innerHTML = mutedIndicator + ' Muted';
  843. muteLink.className = 'mutelink disabled';
  844. }
  845. else {
  846. muteLink.innerHTML = mutedIndicator + ' Mute';
  847. muteLink.className = 'mutelink';
  848. }
  849. }
  850. };
  851. /**
  852. * Returns the current dominant speaker resource jid.
  853. */
  854. my.getDominantSpeakerResourceJid = function () {
  855. return currentDominantSpeaker;
  856. };
  857. /**
  858. * Returns the corresponding resource jid to the given peer container
  859. * DOM element.
  860. *
  861. * @return the corresponding resource jid to the given peer container
  862. * DOM element
  863. */
  864. my.getPeerContainerResourceJid = function (containerElement) {
  865. var i = containerElement.id.indexOf('participant_');
  866. if (i >= 0)
  867. return containerElement.id.substring(i + 12);
  868. };
  869. /**
  870. * Adds the remote video menu element for the given <tt>jid</tt> in the
  871. * given <tt>parentElement</tt>.
  872. *
  873. * @param jid the jid indicating the video for which we're adding a menu.
  874. * @param parentElement the parent element where this menu will be added
  875. */
  876. function addRemoteVideoMenu(jid, parentElement) {
  877. var spanElement = document.createElement('span');
  878. spanElement.className = 'remotevideomenu';
  879. parentElement.appendChild(spanElement);
  880. var menuElement = document.createElement('i');
  881. menuElement.className = 'fa fa-angle-down';
  882. menuElement.title = 'Remote user controls';
  883. spanElement.appendChild(menuElement);
  884. // <ul class="popupmenu">
  885. // <li><a href="#">Mute</a></li>
  886. // <li><a href="#">Eject</a></li>
  887. // </ul>
  888. var popupmenuElement = document.createElement('ul');
  889. popupmenuElement.className = 'popupmenu';
  890. popupmenuElement.id
  891. = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
  892. spanElement.appendChild(popupmenuElement);
  893. var muteMenuItem = document.createElement('li');
  894. var muteLinkItem = document.createElement('a');
  895. var mutedIndicator = "<i class='icon-mic-disabled'></i>";
  896. if (!mutedAudios[jid]) {
  897. muteLinkItem.innerHTML = mutedIndicator + 'Mute';
  898. muteLinkItem.className = 'mutelink';
  899. }
  900. else {
  901. muteLinkItem.innerHTML = mutedIndicator + ' Muted';
  902. muteLinkItem.className = 'mutelink disabled';
  903. }
  904. muteLinkItem.onclick = function(){
  905. if ($(this).attr('disabled') != undefined) {
  906. event.preventDefault();
  907. }
  908. var isMute = !mutedAudios[jid];
  909. connection.moderate.setMute(jid, isMute);
  910. popupmenuElement.setAttribute('style', 'display:none;');
  911. if (isMute) {
  912. this.innerHTML = mutedIndicator + ' Muted';
  913. this.className = 'mutelink disabled';
  914. }
  915. else {
  916. this.innerHTML = mutedIndicator + ' Mute';
  917. this.className = 'mutelink';
  918. }
  919. };
  920. muteMenuItem.appendChild(muteLinkItem);
  921. popupmenuElement.appendChild(muteMenuItem);
  922. var ejectIndicator = "<i class='fa fa-eject'></i>";
  923. var ejectMenuItem = document.createElement('li');
  924. var ejectLinkItem = document.createElement('a');
  925. ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
  926. ejectLinkItem.onclick = function(){
  927. connection.moderate.eject(jid);
  928. popupmenuElement.setAttribute('style', 'display:none;');
  929. };
  930. ejectMenuItem.appendChild(ejectLinkItem);
  931. popupmenuElement.appendChild(ejectMenuItem);
  932. }
  933. /**
  934. * On audio muted event.
  935. */
  936. $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
  937. var videoSpanId = null;
  938. if (jid === connection.emuc.myroomjid) {
  939. videoSpanId = 'localVideoContainer';
  940. } else {
  941. VideoLayout.ensurePeerContainerExists(jid);
  942. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  943. }
  944. if (focus) {
  945. mutedAudios[jid] = isMuted;
  946. VideoLayout.updateRemoteVideoMenu(jid, isMuted);
  947. }
  948. if (videoSpanId)
  949. VideoLayout.showAudioIndicator(videoSpanId, isMuted);
  950. });
  951. /**
  952. * On video muted event.
  953. */
  954. $(document).bind('videomuted.muc', function (event, jid, isMuted) {
  955. var videoSpanId = null;
  956. if (jid === connection.emuc.myroomjid) {
  957. videoSpanId = 'localVideoContainer';
  958. } else {
  959. VideoLayout.ensurePeerContainerExists(jid);
  960. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  961. }
  962. if (videoSpanId)
  963. VideoLayout.showVideoIndicator(videoSpanId, isMuted);
  964. });
  965. /**
  966. * Display name changed.
  967. */
  968. $(document).bind('displaynamechanged',
  969. function (event, jid, displayName, status) {
  970. if (jid === 'localVideoContainer'
  971. || jid === connection.emuc.myroomjid) {
  972. setDisplayName('localVideoContainer',
  973. displayName);
  974. } else {
  975. VideoLayout.ensurePeerContainerExists(jid);
  976. setDisplayName(
  977. 'participant_' + Strophe.getResourceFromJid(jid),
  978. displayName,
  979. status);
  980. }
  981. });
  982. /**
  983. * On dominant speaker changed event.
  984. */
  985. $(document).bind('dominantspeakerchanged', function (event, resourceJid) {
  986. // We ignore local user events.
  987. if (resourceJid
  988. === Strophe.getResourceFromJid(connection.emuc.myroomjid))
  989. return;
  990. // Update the current dominant speaker.
  991. if (resourceJid !== currentDominantSpeaker)
  992. currentDominantSpeaker = resourceJid;
  993. else
  994. return;
  995. // Obtain container for new dominant speaker.
  996. var container = document.getElementById(
  997. 'participant_' + resourceJid);
  998. // Local video will not have container found, but that's ok
  999. // since we don't want to switch to local video.
  1000. if (container && !focusedVideoSrc)
  1001. {
  1002. var video = container.getElementsByTagName("video");
  1003. // Update the large video if the video source is already available,
  1004. // otherwise wait for the "videoactive.jingle" event.
  1005. if (video.length && video[0].currentTime > 0)
  1006. VideoLayout.updateLargeVideo(video[0].src);
  1007. }
  1008. });
  1009. /**
  1010. * On last N change event.
  1011. *
  1012. * @param event the event that notified us
  1013. * @param lastNEndpoints the list of last N endpoints
  1014. * @param endpointsEnteringLastN the list currently entering last N
  1015. * endpoints
  1016. */
  1017. $(document).bind('lastnchanged', function ( event,
  1018. lastNEndpoints,
  1019. endpointsEnteringLastN,
  1020. stream) {
  1021. if (lastNCount !== lastNEndpoints.length)
  1022. lastNCount = lastNEndpoints.length;
  1023. lastNEndpointsCache = lastNEndpoints;
  1024. $('#remoteVideos>span').each(function( index, element ) {
  1025. var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
  1026. if (resourceJid
  1027. && lastNEndpoints.length > 0
  1028. && lastNEndpoints.indexOf(resourceJid) < 0) {
  1029. console.log("Remove from last N", resourceJid);
  1030. showPeerContainer(resourceJid, false);
  1031. }
  1032. });
  1033. if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
  1034. endpointsEnteringLastN = lastNEndpoints;
  1035. if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
  1036. endpointsEnteringLastN.forEach(function (resourceJid) {
  1037. if (!$('#participant_' + resourceJid).is(':visible')) {
  1038. console.log("Add to last N", resourceJid);
  1039. showPeerContainer(resourceJid, true);
  1040. mediaStreams.some(function (mediaStream) {
  1041. if (mediaStream.peerjid
  1042. && Strophe.getResourceFromJid(mediaStream.peerjid)
  1043. === resourceJid
  1044. && mediaStream.type === mediaStream.VIDEO_TYPE) {
  1045. var sel = $('#participant_' + resourceJid + '>video');
  1046. RTC.attachMediaStream(sel, mediaStream.stream);
  1047. waitForRemoteVideo(
  1048. sel,
  1049. mediaStream.ssrc,
  1050. mediaStream.stream);
  1051. return true;
  1052. }
  1053. });
  1054. }
  1055. });
  1056. }
  1057. });
  1058. $(document).bind('videoactive.jingle', function (event, videoelem) {
  1059. if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
  1060. // ignore mixedmslabela0 and v0
  1061. videoelem.show();
  1062. VideoLayout.resizeThumbnails();
  1063. var videoParent = videoelem.parent();
  1064. var parentResourceJid = null;
  1065. if (videoParent)
  1066. parentResourceJid
  1067. = VideoLayout.getPeerContainerResourceJid(videoParent[0]);
  1068. // Update the large video to the last added video only if there's no
  1069. // current dominant or focused speaker or update it to the current
  1070. // dominant speaker.
  1071. if ((!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid())
  1072. || (parentResourceJid
  1073. && VideoLayout.getDominantSpeakerResourceJid()
  1074. === parentResourceJid)) {
  1075. VideoLayout.updateLargeVideo(videoelem.attr('src'), 1);
  1076. }
  1077. VideoLayout.showFocusIndicator();
  1078. }
  1079. });
  1080. return my;
  1081. }(VideoLayout || {}));