Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  1. var AudioLevels = require("../audio_levels/AudioLevels");
  2. var Avatar = require("../avatar/Avatar");
  3. var ContactList = require("../side_pannels/contactlist/ContactList");
  4. var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
  5. var UIEvents = require("../../../service/UI/UIEvents");
  6. var RTC = require("../../RTC/RTC");
  7. var RTCBrowserType = require('../../RTC/RTCBrowserType');
  8. var RemoteVideo = require("./RemoteVideo");
  9. var LargeVideo = require("./LargeVideo");
  10. var LocalVideo = require("./LocalVideo");
  11. var remoteVideos = {};
  12. var localVideoThumbnail = null;
  13. var currentDominantSpeaker = null;
  14. var lastNCount = config.channelLastN;
  15. var localLastNCount = config.channelLastN;
  16. var localLastNSet = [];
  17. var lastNEndpointsCache = [];
  18. var lastNPickupJid = null;
  19. var eventEmitter = null;
  20. /**
  21. * Currently focused video jid
  22. * @type {String}
  23. */
  24. var focusedVideoResourceJid = null;
  25. var VideoLayout = (function (my) {
  26. my.init = function (emitter) {
  27. eventEmitter = emitter;
  28. localVideoThumbnail = new LocalVideo(VideoLayout);
  29. VideoLayout.resizeLargeVideoContainer();
  30. LargeVideo.init(VideoLayout, emitter);
  31. };
  32. my.isInLastN = function(resource) {
  33. return lastNCount < 0 // lastN is disabled, return true
  34. || (lastNCount > 0 && lastNEndpointsCache.length == 0) // lastNEndpoints cache not built yet, return true
  35. || (lastNEndpointsCache && lastNEndpointsCache.indexOf(resource) !== -1);
  36. };
  37. my.changeLocalStream = function (stream, isMuted) {
  38. VideoLayout.changeLocalVideo(stream, isMuted);
  39. };
  40. my.changeLocalAudio = function(stream, isMuted) {
  41. if (isMuted)
  42. APP.UI.setAudioMuted(true, true);
  43. APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
  44. var localAudio = document.getElementById('localAudio');
  45. // Writing volume not allowed in IE
  46. if (!RTCBrowserType.isIExplorer()) {
  47. localAudio.autoplay = true;
  48. localAudio.volume = 0;
  49. }
  50. };
  51. my.changeLocalVideo = function(stream, isMuted) {
  52. // Set default display name.
  53. localVideoThumbnail.setDisplayName();
  54. localVideoThumbnail.createConnectionIndicator();
  55. AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
  56. localVideoThumbnail.changeVideo(stream, isMuted);
  57. /* force update if we're currently being displayed */
  58. if (LargeVideo.isCurrentlyOnLarge(APP.xmpp.myResource())) {
  59. LargeVideo.updateLargeVideo(APP.xmpp.myResource(), true);
  60. }
  61. };
  62. my.mucJoined = function () {
  63. var myResourceJid = APP.xmpp.myResource();
  64. localVideoThumbnail.joined(APP.xmpp.myJid());
  65. if (!LargeVideo.getResourceJid())
  66. LargeVideo.updateLargeVideo(myResourceJid, true);
  67. };
  68. /**
  69. * Adds or removes icons for not available camera and microphone.
  70. * @param resourceJid the jid of user
  71. * @param devices available devices
  72. */
  73. my.setDeviceAvailabilityIcons = function (resourceJid, devices) {
  74. if(!devices)
  75. return;
  76. if(!resourceJid)
  77. {
  78. localVideoThumbnail.setDeviceAvailabilityIcons(devices);
  79. }
  80. else
  81. {
  82. if(remoteVideos[resourceJid])
  83. remoteVideos[resourceJid].setDeviceAvailabilityIcons(devices);
  84. }
  85. }
  86. /**
  87. * Checks if removed video is currently displayed and tries to display
  88. * another one instead.
  89. * @param removedVideoSrc src stream identifier of the video.
  90. */
  91. my.updateRemovedVideo = function(resourceJid) {
  92. if (resourceJid === LargeVideo.getResourceJid()) {
  93. var newResourceJid;
  94. // We'll show user's avatar if he is the dominant speaker or if
  95. // his video thumbnail is pinned
  96. if (remoteVideos[resourceJid] &&
  97. resourceJid === focusedVideoResourceJid ||
  98. resourceJid === currentDominantSpeaker) {
  99. newResourceJid = resourceJid;
  100. } else {
  101. // Otherwise select last visible video
  102. newResourceJid = this.electLastVisibleVideo();
  103. }
  104. LargeVideo.updateLargeVideo(newResourceJid);
  105. }
  106. };
  107. my.electLastVisibleVideo = function() {
  108. // pick the last visible video in the row
  109. // if nobody else is left, this picks the local video
  110. var jid;
  111. var videoElem = RTC.getVideoElementName();
  112. var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>' + videoElem);
  113. if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
  114. jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
  115. } else {
  116. console.info("Last visible video no longer exists");
  117. pick = $('#remoteVideos>span[id!="mixedstream"]>' + videoElem);
  118. if (pick.length && APP.RTC.getVideoSrc(pick[0])) {
  119. jid = VideoLayout.getPeerContainerResourceJid(pick[0].parentNode);
  120. } else {
  121. // Try local video
  122. console.info("Fallback to local video...");
  123. jid = APP.xmpp.myResource();
  124. }
  125. }
  126. console.info("electLastVisibleVideo: " + jid);
  127. return jid;
  128. };
  129. my.onRemoteStreamAdded = function (stream) {
  130. if (stream.peerjid) {
  131. VideoLayout.ensurePeerContainerExists(stream.peerjid);
  132. var resourceJid = Strophe.getResourceFromJid(stream.peerjid);
  133. remoteVideos[resourceJid].addRemoteStreamElement(
  134. stream.sid,
  135. stream.getOriginalStream(),
  136. stream.ssrc);
  137. }
  138. };
  139. my.getLargeVideoJid = function () {
  140. return LargeVideo.getResourceJid();
  141. };
  142. /**
  143. * Called when large video update is finished
  144. * @param currentSmallVideo small video currently displayed on large video
  145. */
  146. my.largeVideoUpdated = function (currentSmallVideo) {
  147. // Makes sure that dominant speaker UI
  148. // is enabled only on current small video
  149. localVideoThumbnail.enableDominantSpeaker(
  150. localVideoThumbnail === currentSmallVideo);
  151. Object.keys(remoteVideos).forEach(
  152. function (resourceJid) {
  153. var remoteVideo = remoteVideos[resourceJid];
  154. if (remoteVideo) {
  155. remoteVideo.enableDominantSpeaker(
  156. remoteVideo === currentSmallVideo);
  157. }
  158. }
  159. );
  160. };
  161. my.handleVideoThumbClicked = function(noPinnedEndpointChangedEvent,
  162. resourceJid) {
  163. if(focusedVideoResourceJid) {
  164. var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
  165. if(oldSmallVideo)
  166. oldSmallVideo.focus(false);
  167. }
  168. var smallVideo = VideoLayout.getSmallVideo(resourceJid);
  169. // Unlock current focused.
  170. if (focusedVideoResourceJid === resourceJid)
  171. {
  172. focusedVideoResourceJid = null;
  173. // Enable the currently set dominant speaker.
  174. if (currentDominantSpeaker) {
  175. if(smallVideo && smallVideo.hasVideo())
  176. {
  177. LargeVideo.updateLargeVideo(currentDominantSpeaker);
  178. }
  179. }
  180. if (!noPinnedEndpointChangedEvent) {
  181. eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
  182. }
  183. return;
  184. }
  185. // Lock new video
  186. focusedVideoResourceJid = resourceJid;
  187. // Update focused/pinned interface.
  188. if (resourceJid)
  189. {
  190. if(smallVideo)
  191. smallVideo.focus(true);
  192. if (!noPinnedEndpointChangedEvent) {
  193. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
  194. }
  195. }
  196. // Triggers a "video.selected" event. The "false" parameter indicates
  197. // this isn't a prezi.
  198. $(document).trigger("video.selected", [false]);
  199. LargeVideo.updateLargeVideo(resourceJid);
  200. // Writing volume not allowed in IE
  201. if (!RTCBrowserType.isIExplorer()) {
  202. $('audio').each(function (idx, el) {
  203. el.volume = 0;
  204. el.volume = 1;
  205. });
  206. }
  207. };
  208. /**
  209. * Checks if container for participant identified by given peerJid exists
  210. * in the document and creates it eventually.
  211. *
  212. * @param peerJid peer Jid to check.
  213. *
  214. * @return Returns <tt>true</tt> if the peer container exists,
  215. * <tt>false</tt> - otherwise
  216. */
  217. my.ensurePeerContainerExists = function(peerJid) {
  218. ContactList.ensureAddContact(peerJid);
  219. var resourceJid = Strophe.getResourceFromJid(peerJid);
  220. if(!remoteVideos[resourceJid])
  221. {
  222. remoteVideos[resourceJid] = new RemoteVideo(peerJid, VideoLayout);
  223. // In case this is not currently in the last n we don't show it.
  224. if (localLastNCount
  225. && localLastNCount > 0
  226. && $('#remoteVideos>span').length >= localLastNCount + 2) {
  227. remoteVideos[resourceJid].showPeerContainer('hide');
  228. }
  229. else
  230. VideoLayout.resizeThumbnails();
  231. }
  232. };
  233. my.inputDisplayNameHandler = function (name) {
  234. localVideoThumbnail.inputDisplayNameHandler(name);
  235. };
  236. my.videoactive = function (videoelem, resourceJid) {
  237. console.info(resourceJid + " video is now active");
  238. videoelem.show();
  239. VideoLayout.resizeThumbnails();
  240. // Update the large video to the last added video only if there's no
  241. // current dominant, focused speaker or prezi playing or update it to
  242. // the current dominant speaker.
  243. if ((!focusedVideoResourceJid &&
  244. !currentDominantSpeaker &&
  245. !require("../prezi/Prezi").isPresentationVisible()) ||
  246. focusedVideoResourceJid === resourceJid ||
  247. (resourceJid &&
  248. currentDominantSpeaker === resourceJid)) {
  249. LargeVideo.updateLargeVideo(resourceJid, true);
  250. }
  251. }
  252. /**
  253. * Shows the presence status message for the given video.
  254. */
  255. my.setPresenceStatus = function (resourceJid, statusMsg) {
  256. remoteVideos[resourceJid].setPresenceStatus(statusMsg);
  257. };
  258. /**
  259. * Shows a visual indicator for the moderator of the conference.
  260. */
  261. my.showModeratorIndicator = function () {
  262. var isModerator = APP.xmpp.isModerator();
  263. if (isModerator) {
  264. localVideoThumbnail.createModeratorIndicatorElement();
  265. }
  266. var members = APP.xmpp.getMembers();
  267. Object.keys(members).forEach(function (jid) {
  268. if (Strophe.getResourceFromJid(jid) === 'focus') {
  269. // Skip server side focus
  270. return;
  271. }
  272. var resourceJid = Strophe.getResourceFromJid(jid);
  273. var member = members[jid];
  274. if (member.role === 'moderator') {
  275. remoteVideos[resourceJid].removeRemoteVideoMenu();
  276. remoteVideos[resourceJid].createModeratorIndicatorElement();
  277. } else if (isModerator) {
  278. // We are moderator, but user is not - add menu
  279. if ($('#remote_popupmenu_' + resourceJid).length <= 0) {
  280. remoteVideos[resourceJid].addRemoteVideoMenu();
  281. }
  282. }
  283. });
  284. };
  285. /*
  286. * Shows or hides the audio muted indicator over the local thumbnail video.
  287. * @param {boolean} isMuted
  288. */
  289. my.showLocalAudioIndicator = function(isMuted) {
  290. localVideoThumbnail.showAudioIndicator(isMuted);
  291. };
  292. /**
  293. * Resizes the large video container.
  294. */
  295. my.resizeLargeVideoContainer = function () {
  296. LargeVideo.resize();
  297. VideoLayout.resizeThumbnails();
  298. LargeVideo.position();
  299. };
  300. /**
  301. * Resizes thumbnails.
  302. */
  303. my.resizeThumbnails = function(animate) {
  304. var videoSpaceWidth = $('#remoteVideos').width();
  305. var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
  306. var width = thumbnailSize[0];
  307. var height = thumbnailSize[1];
  308. $('.userAvatar').css('left', (width - height) / 2);
  309. if(animate)
  310. {
  311. $('#remoteVideos').animate({
  312. height: height
  313. },
  314. {
  315. queue: false,
  316. duration: 500
  317. });
  318. $('#remoteVideos>span').animate({
  319. height: height,
  320. width: width
  321. },
  322. {
  323. queue: false,
  324. duration: 500,
  325. complete: function () {
  326. $(document).trigger(
  327. "remotevideo.resized",
  328. [width,
  329. height]);
  330. }
  331. });
  332. }
  333. else
  334. {
  335. // size videos so that while keeping AR and max height, we have a
  336. // nice fit
  337. $('#remoteVideos').height(height);
  338. $('#remoteVideos>span').width(width);
  339. $('#remoteVideos>span').height(height);
  340. $(document).trigger("remotevideo.resized", [width, height]);
  341. }
  342. };
  343. /**
  344. * Calculates the thumbnail size.
  345. *
  346. * @param videoSpaceWidth the width of the video space
  347. */
  348. my.calculateThumbnailSize = function (videoSpaceWidth) {
  349. // Calculate the available height, which is the inner window height minus
  350. // 39px for the header minus 2px for the delimiter lines on the top and
  351. // bottom of the large video, minus the 36px space inside the remoteVideos
  352. // container used for highlighting shadow.
  353. var availableHeight = 100;
  354. var numvids = $('#remoteVideos>span:visible').length;
  355. if (localLastNCount && localLastNCount > 0) {
  356. numvids = Math.min(localLastNCount + 1, numvids);
  357. }
  358. // Remove the 3px borders arround videos and border around the remote
  359. // videos area and the 4 pixels between the local video and the others
  360. //TODO: Find out where the 4 pixels come from and remove them
  361. var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4;
  362. var availableWidth = availableWinWidth / numvids;
  363. var aspectRatio = 16.0 / 9.0;
  364. var maxHeight = Math.min(160, availableHeight);
  365. availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
  366. if (availableHeight < availableWidth / aspectRatio) {
  367. availableWidth = Math.floor(availableHeight * aspectRatio);
  368. }
  369. return [availableWidth, availableHeight];
  370. };
  371. /**
  372. * Returns the corresponding resource jid to the given peer container
  373. * DOM element.
  374. *
  375. * @return the corresponding resource jid to the given peer container
  376. * DOM element
  377. */
  378. my.getPeerContainerResourceJid = function (containerElement) {
  379. var i = containerElement.id.indexOf('participant_');
  380. if (i >= 0)
  381. return containerElement.id.substring(i + 12);
  382. };
  383. my.getPeerVideoSel = function (peerResourceJid) {
  384. return $('#participant_' + peerResourceJid +
  385. '>' + APP.RTC.getVideoElementName());
  386. };
  387. /**
  388. * On contact list item clicked.
  389. */
  390. $(ContactList).bind('contactclicked', function(event, jid) {
  391. if (!jid) {
  392. return;
  393. }
  394. if (jid === APP.xmpp.myJid()) {
  395. $("#localVideoContainer").click();
  396. return;
  397. }
  398. var resource = Strophe.getResourceFromJid(jid);
  399. var videoSel = VideoLayout.getPeerVideoSel(resource);
  400. if (videoSel.length > 0) {
  401. var videoThumb = videoSel[0];
  402. // It is not always the case that a videoThumb exists (if there is
  403. // no actual video).
  404. if (RTC.getVideoSrc(videoThumb)) {
  405. // We have a video src, great! Let's update the large video
  406. // now.
  407. VideoLayout.handleVideoThumbClicked(
  408. false,
  409. Strophe.getResourceFromJid(jid));
  410. } else {
  411. // If we don't have a video src for jid, there's absolutely
  412. // no point in calling handleVideoThumbClicked; Quite
  413. // simply, it won't work because it needs an src to attach
  414. // to the large video.
  415. //
  416. // Instead, we trigger the pinned endpoint changed event to
  417. // let the bridge adjust its lastN set for myjid and store
  418. // the pinned user in the lastNPickupJid variable to be
  419. // picked up later by the lastN changed event handler.
  420. lastNPickupJid = jid;
  421. eventEmitter.emit(UIEvents.PINNED_ENDPOINT,
  422. Strophe.getResourceFromJid(jid));
  423. }
  424. }
  425. });
  426. /**
  427. * On audio muted event.
  428. */
  429. my.onAudioMute = function (jid, isMuted) {
  430. var resourceJid = Strophe.getResourceFromJid(jid);
  431. if (resourceJid === APP.xmpp.myResource()) {
  432. localVideoThumbnail.showAudioIndicator(isMuted);
  433. } else {
  434. VideoLayout.ensurePeerContainerExists(jid);
  435. remoteVideos[resourceJid].showAudioIndicator(isMuted);
  436. if (APP.xmpp.isModerator()) {
  437. remoteVideos[resourceJid].updateRemoteVideoMenu(isMuted);
  438. }
  439. }
  440. };
  441. /**
  442. * On video muted event.
  443. */
  444. my.onVideoMute = function (jid, value) {
  445. if(jid !== APP.xmpp.myJid() && !APP.RTC.muteRemoteVideoStream(jid, value))
  446. return;
  447. if (jid === APP.xmpp.myJid()) {
  448. localVideoThumbnail.showVideoIndicator(value);
  449. } else {
  450. var resource = Strophe.getResourceFromJid(jid);
  451. VideoLayout.ensurePeerContainerExists(jid);
  452. remoteVideos[resource].showVideoIndicator(value);
  453. var el = VideoLayout.getPeerVideoSel(resource);
  454. if (!value)
  455. el.show();
  456. else
  457. el.hide();
  458. }
  459. };
  460. /**
  461. * Display name changed.
  462. */
  463. my.onDisplayNameChanged =
  464. function (jid, displayName, status) {
  465. if (jid === 'localVideoContainer'
  466. || jid === APP.xmpp.myJid()) {
  467. localVideoThumbnail.setDisplayName(displayName);
  468. } else {
  469. VideoLayout.ensurePeerContainerExists(jid);
  470. remoteVideos[Strophe.getResourceFromJid(jid)].setDisplayName(
  471. displayName,
  472. status);
  473. }
  474. };
  475. /**
  476. * On dominant speaker changed event.
  477. */
  478. my.onDominantSpeakerChanged = function (resourceJid) {
  479. // We ignore local user events.
  480. if (resourceJid === APP.xmpp.myResource())
  481. return;
  482. var members = APP.xmpp.getMembers();
  483. // Update the current dominant speaker.
  484. if (resourceJid !== currentDominantSpeaker) {
  485. var currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker);
  486. var newJID = APP.xmpp.findJidFromResource(resourceJid);
  487. if(currentDominantSpeaker && (!members || !members[currentJID] ||
  488. !members[currentJID].displayName)) {
  489. remoteVideos[resourceJid].setDisplayName(null);
  490. }
  491. if(resourceJid && (!members || !members[newJID] ||
  492. !members[newJID].displayName)) {
  493. remoteVideos[resourceJid].setDisplayName(null,
  494. interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME);
  495. }
  496. currentDominantSpeaker = resourceJid;
  497. } else {
  498. return;
  499. }
  500. // Obtain container for new dominant speaker.
  501. var videoSel = VideoLayout.getPeerVideoSel(resourceJid);
  502. // Local video will not have container found, but that's ok
  503. // since we don't want to switch to local video.
  504. if (!focusedVideoResourceJid && videoSel.length)
  505. {
  506. // Update the large video if the video source is already available,
  507. // otherwise wait for the "videoactive.jingle" event.
  508. if (videoSel[0].currentTime > 0) {
  509. LargeVideo.updateLargeVideo(resourceJid);
  510. }
  511. }
  512. };
  513. /**
  514. * On last N change event.
  515. *
  516. * @param lastNEndpoints the list of last N endpoints
  517. * @param endpointsEnteringLastN the list currently entering last N
  518. * endpoints
  519. */
  520. my.onLastNEndpointsChanged = function ( lastNEndpoints,
  521. endpointsEnteringLastN,
  522. stream) {
  523. if (lastNCount !== lastNEndpoints.length)
  524. lastNCount = lastNEndpoints.length;
  525. lastNEndpointsCache = lastNEndpoints;
  526. // Say A, B, C, D, E, and F are in a conference and LastN = 3.
  527. //
  528. // If LastN drops to, say, 2, because of adaptivity, then E should see
  529. // thumbnails for A, B and C. A and B are in E's server side LastN set,
  530. // so E sees them. C is only in E's local LastN set.
  531. //
  532. // If F starts talking and LastN = 3, then E should see thumbnails for
  533. // F, A, B. B gets "ejected" from E's server side LastN set, but it
  534. // enters E's local LastN ejecting C.
  535. // Increase the local LastN set size, if necessary.
  536. if (lastNCount > localLastNCount) {
  537. localLastNCount = lastNCount;
  538. }
  539. // Update the local LastN set preserving the order in which the
  540. // endpoints appeared in the LastN/local LastN set.
  541. var nextLocalLastNSet = lastNEndpoints.slice(0);
  542. for (var i = 0; i < localLastNSet.length; i++) {
  543. if (nextLocalLastNSet.length >= localLastNCount) {
  544. break;
  545. }
  546. var resourceJid = localLastNSet[i];
  547. if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
  548. nextLocalLastNSet.push(resourceJid);
  549. }
  550. }
  551. localLastNSet = nextLocalLastNSet;
  552. var updateLargeVideo = false;
  553. // Handle LastN/local LastN changes.
  554. $('#remoteVideos>span').each(function( index, element ) {
  555. var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
  556. var isReceived = true;
  557. if (resourceJid
  558. && lastNEndpoints.indexOf(resourceJid) < 0
  559. && localLastNSet.indexOf(resourceJid) < 0) {
  560. console.log("Remove from last N", resourceJid);
  561. remoteVideos[resourceJid].showPeerContainer('hide');
  562. isReceived = false;
  563. } else if (resourceJid
  564. && $('#participant_' + resourceJid).is(':visible')
  565. && lastNEndpoints.indexOf(resourceJid) < 0
  566. && localLastNSet.indexOf(resourceJid) >= 0) {
  567. remoteVideos[resourceJid].showPeerContainer('avatar');
  568. isReceived = false;
  569. }
  570. if (!isReceived) {
  571. // resourceJid has dropped out of the server side lastN set, so
  572. // it is no longer being received. If resourceJid was being
  573. // displayed in the large video we have to switch to another
  574. // user.
  575. if (!updateLargeVideo && resourceJid === LargeVideo.getResourceJid()) {
  576. updateLargeVideo = true;
  577. }
  578. }
  579. });
  580. if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
  581. endpointsEnteringLastN = lastNEndpoints;
  582. if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
  583. endpointsEnteringLastN.forEach(function (resourceJid) {
  584. var isVisible = $('#participant_' + resourceJid).is(':visible');
  585. remoteVideos[resourceJid].showPeerContainer('show');
  586. if (!isVisible) {
  587. console.log("Add to last N", resourceJid);
  588. var jid = APP.xmpp.findJidFromResource(resourceJid);
  589. var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
  590. var sel = VideoLayout.getPeerVideoSel(resourceJid);
  591. APP.RTC.attachMediaStream(sel, mediaStream.stream);
  592. if (lastNPickupJid == mediaStream.peerjid) {
  593. // Clean up the lastN pickup jid.
  594. lastNPickupJid = null;
  595. // Don't fire the events again, they've already
  596. // been fired in the contact list click handler.
  597. VideoLayout.handleVideoThumbClicked(
  598. false,
  599. Strophe.getResourceFromJid(mediaStream.peerjid));
  600. updateLargeVideo = false;
  601. }
  602. remoteVideos[resourceJid].waitForPlayback(sel, mediaStream.stream);
  603. }
  604. });
  605. }
  606. // The endpoint that was being shown in the large video has dropped out
  607. // of the lastN set and there was no lastN pickup jid. We need to update
  608. // the large video now.
  609. if (updateLargeVideo) {
  610. var resource, container, src;
  611. var myResource
  612. = APP.xmpp.myResource();
  613. // Find out which endpoint to show in the large video.
  614. for (var i = 0; i < lastNEndpoints.length; i++) {
  615. resource = lastNEndpoints[i];
  616. if (!resource || resource === myResource)
  617. continue;
  618. // videoSrcToSsrc needs to be update for this call to succeed.
  619. LargeVideo.updateLargeVideo(resource);
  620. break;
  621. }
  622. }
  623. };
  624. /**
  625. * Updates local stats
  626. * @param percent
  627. * @param object
  628. */
  629. my.updateLocalConnectionStats = function (percent, object) {
  630. var resolution = null;
  631. if(object.resolution !== null)
  632. {
  633. resolution = object.resolution;
  634. object.resolution = resolution[APP.xmpp.myJid()];
  635. delete resolution[APP.xmpp.myJid()];
  636. }
  637. localVideoThumbnail.updateStatsIndicator(percent, object);
  638. for(var jid in resolution)
  639. {
  640. if(resolution[jid] === null)
  641. continue;
  642. var resourceJid = Strophe.getResourceFromJid(jid);
  643. if(remoteVideos[resourceJid] && remoteVideos[resourceJid].connectionIndicator)
  644. {
  645. remoteVideos[resourceJid].connectionIndicator.updateResolution(resolution[jid]);
  646. }
  647. }
  648. };
  649. /**
  650. * Updates remote stats.
  651. * @param jid the jid associated with the stats
  652. * @param percent the connection quality percent
  653. * @param object the stats data
  654. */
  655. my.updateConnectionStats = function (jid, percent, object) {
  656. var resourceJid = Strophe.getResourceFromJid(jid);
  657. if(remoteVideos[resourceJid])
  658. remoteVideos[resourceJid].updateStatsIndicator(percent, object);
  659. };
  660. /**
  661. * Hides the connection indicator
  662. * @param jid
  663. */
  664. my.hideConnectionIndicator = function (jid) {
  665. remoteVideos[Strophe.getResourceFromJid(jid)].hideConnectionIndicator();
  666. };
  667. /**
  668. * Hides all the indicators
  669. */
  670. my.onStatsStop = function () {
  671. for(var video in remoteVideos)
  672. {
  673. remoteVideos[video].hideIndicator();
  674. }
  675. localVideoThumbnail.hideIndicator();
  676. };
  677. my.participantLeft = function (jid) {
  678. // Unlock large video
  679. var resourceJid = Strophe.getResourceFromJid(jid);
  680. if (focusedVideoResourceJid === resourceJid)
  681. {
  682. console.info("Focused video owner has left the conference");
  683. focusedVideoResourceJid = null;
  684. }
  685. if (currentDominantSpeaker === resourceJid)
  686. {
  687. console.info("Dominant speaker has left the conference");
  688. currentDominantSpeaker = null;
  689. }
  690. var remoteVideo = remoteVideos[resourceJid];
  691. if (remoteVideo) {
  692. remoteVideo.removeConnectionIndicator();
  693. // Remove remote video
  694. delete remoteVideos[resourceJid];
  695. } else {
  696. console.warn("No remote video for " + resourceJid);
  697. }
  698. VideoLayout.resizeThumbnails();
  699. };
  700. my.onVideoTypeChanged = function (jid) {
  701. LargeVideo.onVideoTypeChanged(jid);
  702. };
  703. my.showMore = function (jid) {
  704. if (jid === 'local')
  705. {
  706. localVideoThumbnail.connectionIndicator.showMore();
  707. }
  708. else
  709. {
  710. var remoteVideo = remoteVideos[Strophe.getResourceFromJid(jid)];
  711. if (remoteVideo) {
  712. remoteVideo.connectionIndicator.showMore();
  713. } else {
  714. console.info("Error - no remote video for jid: " + jid);
  715. }
  716. }
  717. };
  718. my.addPreziContainer = function (id) {
  719. return RemoteVideo.createContainer(id);
  720. };
  721. my.setLargeVideoVisible = function (isVisible) {
  722. LargeVideo.setLargeVideoVisible(isVisible);
  723. if(!isVisible && focusedVideoResourceJid)
  724. {
  725. var smallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
  726. if(smallVideo)
  727. smallVideo.focus(false);
  728. smallVideo.showAvatar();
  729. focusedVideoResourceJid = null;
  730. }
  731. };
  732. /**
  733. * Resizes the video area
  734. * @param completeFunction a function to be called when the video space is resized
  735. */
  736. my.resizeVideoArea = function(isVisible, completeFunction) {
  737. LargeVideo.resizeVideoAreaAnimated(isVisible, completeFunction);
  738. VideoLayout.resizeThumbnails(true);
  739. };
  740. my.getSmallVideo = function (resourceJid) {
  741. if(resourceJid == APP.xmpp.myResource())
  742. {
  743. return localVideoThumbnail;
  744. }
  745. else
  746. {
  747. if(!remoteVideos[resourceJid])
  748. return null;
  749. return remoteVideos[resourceJid];
  750. }
  751. };
  752. my.userAvatarChanged = function(resourceJid, thumbUrl)
  753. {
  754. var smallVideo = VideoLayout.getSmallVideo(resourceJid);
  755. if(smallVideo)
  756. smallVideo.avatarChanged(thumbUrl);
  757. else
  758. console.warn(
  759. "Missed avatar update - no small video yet for " + resourceJid);
  760. LargeVideo.updateAvatar(resourceJid, thumbUrl);
  761. };
  762. return my;
  763. }(VideoLayout || {}));
  764. module.exports = VideoLayout;