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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. /* global config, APP, $, interfaceConfig, JitsiMeetJS */
  2. /* jshint -W101 */
  3. import AudioLevels from "../audio_levels/AudioLevels";
  4. import Avatar from "../avatar/Avatar";
  5. import BottomToolbar from "../toolbars/BottomToolbar";
  6. import FilmStrip from "./FilmStrip";
  7. import UIEvents from "../../../service/UI/UIEvents";
  8. import UIUtil from "../util/UIUtil";
  9. import RemoteVideo from "./RemoteVideo";
  10. import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
  11. import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
  12. import LocalVideo from "./LocalVideo";
  13. import PanelToggler from "../side_pannels/SidePanelToggler";
  14. const RTCUIUtil = JitsiMeetJS.util.RTCUIHelper;
  15. var remoteVideos = {};
  16. var localVideoThumbnail = null;
  17. var currentDominantSpeaker = null;
  18. var localLastNCount = config.channelLastN;
  19. var localLastNSet = [];
  20. var lastNEndpointsCache = [];
  21. var lastNPickupId = null;
  22. var eventEmitter = null;
  23. /**
  24. * Currently focused video jid
  25. * @type {String}
  26. */
  27. var pinnedId = null;
  28. /**
  29. * On contact list item clicked.
  30. */
  31. function onContactClicked (id) {
  32. if (APP.conference.isLocalId(id)) {
  33. $("#localVideoContainer").click();
  34. return;
  35. }
  36. let remoteVideo = remoteVideos[id];
  37. if (remoteVideo && remoteVideo.hasVideo()) {
  38. // It is not always the case that a videoThumb exists (if there is
  39. // no actual video).
  40. if (remoteVideo.hasVideoStarted()) {
  41. // We have a video src, great! Let's update the large video
  42. // now.
  43. VideoLayout.handleVideoThumbClicked(id);
  44. } else {
  45. // If we don't have a video src for jid, there's absolutely
  46. // no point in calling handleVideoThumbClicked; Quite
  47. // simply, it won't work because it needs an src to attach
  48. // to the large video.
  49. //
  50. // Instead, we trigger the pinned endpoint changed event to
  51. // let the bridge adjust its lastN set for myjid and store
  52. // the pinned user in the lastNPickupId variable to be
  53. // picked up later by the lastN changed event handler.
  54. lastNPickupId = id;
  55. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
  56. }
  57. }
  58. }
  59. /**
  60. * Returns the corresponding resource id to the given peer container
  61. * DOM element.
  62. *
  63. * @return the corresponding resource id to the given peer container
  64. * DOM element
  65. */
  66. function getPeerContainerResourceId (containerElement) {
  67. if (localVideoThumbnail.container === containerElement) {
  68. return localVideoThumbnail.id;
  69. }
  70. let i = containerElement.id.indexOf('participant_');
  71. if (i >= 0) {
  72. return containerElement.id.substring(i + 12);
  73. }
  74. }
  75. let largeVideo;
  76. var VideoLayout = {
  77. init (emitter) {
  78. eventEmitter = emitter;
  79. localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
  80. // sets default video type of local video
  81. localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
  82. // if we do not resize the thumbs here, if there is no video device
  83. // the local video thumb maybe one pixel
  84. this.resizeThumbnails(false, true, false);
  85. emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
  86. this.lastNCount = config.channelLastN;
  87. },
  88. initLargeVideo (isSideBarVisible) {
  89. largeVideo = new LargeVideoManager();
  90. largeVideo.updateContainerSize(isSideBarVisible);
  91. AudioLevels.init();
  92. },
  93. setAudioLevel(id, lvl) {
  94. if (!largeVideo) {
  95. return;
  96. }
  97. AudioLevels.updateAudioLevel(
  98. id, lvl, largeVideo.id
  99. );
  100. },
  101. isInLastN (resource) {
  102. return this.lastNCount < 0 || // lastN is disabled
  103. // lastNEndpoints cache not built yet
  104. (this.lastNCount > 0 && !lastNEndpointsCache.length) ||
  105. (lastNEndpointsCache &&
  106. lastNEndpointsCache.indexOf(resource) !== -1);
  107. },
  108. changeLocalAudio (stream) {
  109. let localAudio = document.getElementById('localAudio');
  110. localAudio = stream.attach(localAudio);
  111. // Now when Temasys plugin is converting also <audio> elements to
  112. // plugin's <object>s, in current layout it will capture click events
  113. // before it reaches the local video object. We hide it here in order
  114. // to prevent that.
  115. //if (RTCBrowserType.isIExplorer()) {
  116. // The issue is not present on Safari. Also if we hide it in Safari
  117. // then the local audio track will have 'enabled' flag set to false
  118. // which will result in audio mute issues
  119. // $(localAudio).hide();
  120. localAudio.width = 1;
  121. localAudio.height = 1;
  122. //}
  123. },
  124. changeLocalVideo (stream) {
  125. // Set default display name.
  126. localVideoThumbnail.setDisplayName();
  127. localVideoThumbnail.createConnectionIndicator();
  128. let localId = APP.conference.localId;
  129. this.onVideoTypeChanged(localId, stream.videoType);
  130. let {thumbWidth, thumbHeight} = this.resizeThumbnails(false, true);
  131. AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
  132. if (!stream.isMuted()) {
  133. localVideoThumbnail.changeVideo(stream);
  134. }
  135. /* force update if we're currently being displayed */
  136. if (this.isCurrentlyOnLarge(localId)) {
  137. this.updateLargeVideo(localId, true);
  138. }
  139. },
  140. /**
  141. * Get's the localID of the conference and set it to the local video
  142. * (small one). This needs to be called as early as possible, when muc is
  143. * actually joined. Otherwise events can come with information like email
  144. * and setting them assume the id is already set.
  145. */
  146. mucJoined () {
  147. if (largeVideo && !largeVideo.id) {
  148. this.updateLargeVideo(APP.conference.localId, true);
  149. }
  150. },
  151. /**
  152. * Adds or removes icons for not available camera and microphone.
  153. * @param resourceJid the jid of user
  154. * @param devices available devices
  155. */
  156. setDeviceAvailabilityIcons (id, devices) {
  157. if (APP.conference.isLocalId(id)) {
  158. localVideoThumbnail.setDeviceAvailabilityIcons(devices);
  159. return;
  160. }
  161. let video = remoteVideos[id];
  162. if (!video) {
  163. return;
  164. }
  165. video.setDeviceAvailabilityIcons(devices);
  166. },
  167. /**
  168. * Enables/disables device availability icons for the given participant id.
  169. * The default value is {true}.
  170. * @param id the identifier of the participant
  171. * @param enable {true} to enable device availability icons
  172. */
  173. enableDeviceAvailabilityIcons (id, enable) {
  174. let video;
  175. if (APP.conference.isLocalId(id)) {
  176. video = localVideoThumbnail;
  177. }
  178. else if (remoteVideos[id]) {
  179. video = remoteVideos[id];
  180. }
  181. if (video)
  182. video.enableDeviceAvailabilityIcons(enable);
  183. },
  184. /**
  185. * Checks if removed video is currently displayed and tries to display
  186. * another one instead.
  187. * Uses focusedID if any or dominantSpeakerID if any,
  188. * otherwise elects new video, in this order.
  189. */
  190. updateAfterThumbRemoved (id) {
  191. if (!this.isCurrentlyOnLarge(id)) {
  192. return;
  193. }
  194. let newId;
  195. if (pinnedId)
  196. newId = pinnedId;
  197. else if (currentDominantSpeaker)
  198. newId = currentDominantSpeaker;
  199. else // Otherwise select last visible video
  200. newId = this.electLastVisibleVideo();
  201. this.updateLargeVideo(newId);
  202. },
  203. electLastVisibleVideo () {
  204. // pick the last visible video in the row
  205. // if nobody else is left, this picks the local video
  206. let thumbs = FilmStrip.getThumbs(true).filter('[id!="mixedstream"]');
  207. let lastVisible = thumbs.filter(':visible:last');
  208. if (lastVisible.length) {
  209. let id = getPeerContainerResourceId(lastVisible[0]);
  210. if (remoteVideos[id]) {
  211. console.info("electLastVisibleVideo: " + id);
  212. return id;
  213. }
  214. // The RemoteVideo was removed (but the DOM elements may still
  215. // exist).
  216. }
  217. console.info("Last visible video no longer exists");
  218. thumbs = FilmStrip.getThumbs();
  219. if (thumbs.length) {
  220. let id = getPeerContainerResourceId(thumbs[0]);
  221. if (remoteVideos[id]) {
  222. console.info("electLastVisibleVideo: " + id);
  223. return id;
  224. }
  225. // The RemoteVideo was removed (but the DOM elements may
  226. // still exist).
  227. }
  228. // Go with local video
  229. console.info("Fallback to local video...");
  230. let id = APP.conference.localId;
  231. console.info("electLastVisibleVideo: " + id);
  232. return id;
  233. },
  234. onRemoteStreamAdded (stream) {
  235. let id = stream.getParticipantId();
  236. let remoteVideo = remoteVideos[id];
  237. if (!remoteVideo)
  238. return;
  239. remoteVideo.addRemoteStreamElement(stream);
  240. // if track is muted make sure we reflect that
  241. if(stream.isMuted())
  242. {
  243. if(stream.getType() === "audio")
  244. this.onAudioMute(stream.getParticipantId(), true);
  245. else
  246. this.onVideoMute(stream.getParticipantId(), true);
  247. }
  248. },
  249. onRemoteStreamRemoved (stream) {
  250. let id = stream.getParticipantId();
  251. let remoteVideo = remoteVideos[id];
  252. if (remoteVideo) { // remote stream may be removed after participant left the conference
  253. remoteVideo.removeRemoteStreamElement(stream);
  254. }
  255. },
  256. /**
  257. * Return the type of the remote video.
  258. * @param id the id for the remote video
  259. * @returns {String} the video type video or screen.
  260. */
  261. getRemoteVideoType (id) {
  262. let smallVideo = VideoLayout.getSmallVideo(id);
  263. return smallVideo ? smallVideo.getVideoType() : null;
  264. },
  265. isPinned (id) {
  266. return (pinnedId) ? (id === pinnedId) : false;
  267. },
  268. getPinnedId () {
  269. return pinnedId;
  270. },
  271. /**
  272. * Handles the click on a video thumbnail.
  273. *
  274. * @param id the identifier of the video thumbnail
  275. */
  276. handleVideoThumbClicked (id) {
  277. if(pinnedId) {
  278. var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
  279. if (oldSmallVideo && !interfaceConfig.filmStripOnly)
  280. oldSmallVideo.focus(false);
  281. }
  282. var smallVideo = VideoLayout.getSmallVideo(id);
  283. // Unpin if currently pinned.
  284. if (pinnedId === id)
  285. {
  286. pinnedId = null;
  287. // Enable the currently set dominant speaker.
  288. if (currentDominantSpeaker) {
  289. if(smallVideo && smallVideo.hasVideo()) {
  290. this.updateLargeVideo(currentDominantSpeaker);
  291. }
  292. }
  293. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
  294. return;
  295. }
  296. // Lock new video
  297. pinnedId = id;
  298. // Update focused/pinned interface.
  299. if (id) {
  300. if (smallVideo && !interfaceConfig.filmStripOnly)
  301. smallVideo.focus(true);
  302. eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
  303. }
  304. this.updateLargeVideo(id);
  305. },
  306. /**
  307. * Creates a participant container for the given id and smallVideo.
  308. *
  309. * @param id the id of the participant to add
  310. * @param {SmallVideo} smallVideo optional small video instance to add as a
  311. * remote video, if undefined RemoteVideo will be created
  312. */
  313. addParticipantContainer (id, smallVideo) {
  314. let remoteVideo;
  315. if(smallVideo)
  316. remoteVideo = smallVideo;
  317. else
  318. remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
  319. remoteVideos[id] = remoteVideo;
  320. let videoType = VideoLayout.getRemoteVideoType(id);
  321. if (!videoType) {
  322. // make video type the default one (camera)
  323. videoType = VIDEO_CONTAINER_TYPE;
  324. }
  325. remoteVideo.setVideoType(videoType);
  326. // In case this is not currently in the last n we don't show it.
  327. if (localLastNCount && localLastNCount > 0 &&
  328. FilmStrip.getThumbs().length >= localLastNCount + 2) {
  329. remoteVideo.showPeerContainer('hide');
  330. } else {
  331. VideoLayout.resizeThumbnails(false, true);
  332. }
  333. },
  334. videoactive (videoelem, resourceJid) {
  335. console.info(resourceJid + " video is now active", videoelem);
  336. VideoLayout.resizeThumbnails(
  337. false, false, false, function() {$(videoelem).show();});
  338. // Update the large video to the last added video only if there's no
  339. // current dominant, focused speaker or update it to
  340. // the current dominant speaker.
  341. if ((!pinnedId &&
  342. !currentDominantSpeaker &&
  343. this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
  344. pinnedId === resourceJid ||
  345. (!pinnedId && resourceJid &&
  346. currentDominantSpeaker === resourceJid) ||
  347. /* Playback started while we're on the stage - may need to update
  348. video source with the new stream */
  349. this.isCurrentlyOnLarge(resourceJid)) {
  350. this.updateLargeVideo(resourceJid, true);
  351. }
  352. },
  353. /**
  354. * Shows the presence status message for the given video.
  355. */
  356. setPresenceStatus (id, statusMsg) {
  357. remoteVideos[id].setPresenceStatus(statusMsg);
  358. },
  359. /**
  360. * Shows a visual indicator for the moderator of the conference.
  361. * On local or remote participants.
  362. */
  363. showModeratorIndicator () {
  364. let isModerator = APP.conference.isModerator;
  365. if (isModerator) {
  366. localVideoThumbnail.createModeratorIndicatorElement();
  367. }
  368. APP.conference.listMembers().forEach(function (member) {
  369. let id = member.getId();
  370. if (member.isModerator()) {
  371. remoteVideos[id].removeRemoteVideoMenu();
  372. remoteVideos[id].createModeratorIndicatorElement();
  373. } else if (isModerator) {
  374. // We are moderator, but user is not - add menu
  375. if ($(`#remote_popupmenu_${id}`).length <= 0) {
  376. remoteVideos[id].addRemoteVideoMenu();
  377. }
  378. }
  379. });
  380. },
  381. /*
  382. * Shows or hides the audio muted indicator over the local thumbnail video.
  383. * @param {boolean} isMuted
  384. */
  385. showLocalAudioIndicator (isMuted) {
  386. localVideoThumbnail.showAudioIndicator(isMuted);
  387. },
  388. /**
  389. * Resizes thumbnails.
  390. */
  391. resizeThumbnails ( animate = false,
  392. forceUpdate = false,
  393. isSideBarVisible = null,
  394. onComplete = null) {
  395. isSideBarVisible
  396. = (isSideBarVisible !== null)
  397. ? isSideBarVisible : PanelToggler.isVisible();
  398. let {thumbWidth, thumbHeight}
  399. = FilmStrip.calculateThumbnailSize(isSideBarVisible);
  400. $('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2);
  401. FilmStrip.resizeThumbnails(thumbWidth, thumbHeight,
  402. animate, forceUpdate)
  403. .then(function () {
  404. BottomToolbar.resizeToolbar(thumbWidth, thumbHeight);
  405. AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
  406. if (onComplete && typeof onComplete === "function")
  407. onComplete();
  408. });
  409. return {thumbWidth, thumbHeight};
  410. },
  411. /**
  412. * On audio muted event.
  413. */
  414. onAudioMute (id, isMuted) {
  415. if (APP.conference.isLocalId(id)) {
  416. localVideoThumbnail.showAudioIndicator(isMuted);
  417. } else {
  418. remoteVideos[id].showAudioIndicator(isMuted);
  419. if (APP.conference.isModerator) {
  420. remoteVideos[id].updateRemoteVideoMenu(isMuted);
  421. }
  422. }
  423. },
  424. /**
  425. * On video muted event.
  426. */
  427. onVideoMute (id, value) {
  428. if (APP.conference.isLocalId(id)) {
  429. localVideoThumbnail.setMutedView(value);
  430. } else {
  431. var remoteVideo = remoteVideos[id];
  432. remoteVideo.setMutedView(value);
  433. }
  434. if (this.isCurrentlyOnLarge(id)) {
  435. // large video will show avatar instead of muted stream
  436. this.updateLargeVideo(id, true);
  437. }
  438. },
  439. /**
  440. * Display name changed.
  441. */
  442. onDisplayNameChanged (id, displayName, status) {
  443. if (id === 'localVideoContainer' ||
  444. APP.conference.isLocalId(id)) {
  445. localVideoThumbnail.setDisplayName(displayName);
  446. } else {
  447. remoteVideos[id].setDisplayName(displayName, status);
  448. }
  449. },
  450. /**
  451. * On dominant speaker changed event.
  452. */
  453. onDominantSpeakerChanged (id) {
  454. if (id === currentDominantSpeaker) {
  455. return;
  456. }
  457. let oldSpeakerRemoteVideo = remoteVideos[currentDominantSpeaker];
  458. // We ignore local user events, but just unmark remote user as dominant
  459. // while we are talking
  460. if (APP.conference.isLocalId(id)) {
  461. if(oldSpeakerRemoteVideo)
  462. {
  463. oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false);
  464. localVideoThumbnail.updateDominantSpeakerIndicator(true);
  465. currentDominantSpeaker = null;
  466. }
  467. return;
  468. }
  469. let remoteVideo = remoteVideos[id];
  470. if (!remoteVideo) {
  471. return;
  472. }
  473. // Update the current dominant speaker.
  474. remoteVideo.updateDominantSpeakerIndicator(true);
  475. localVideoThumbnail.updateDominantSpeakerIndicator(false);
  476. // let's remove the indications from the remote video if any
  477. if (oldSpeakerRemoteVideo) {
  478. oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false);
  479. }
  480. currentDominantSpeaker = id;
  481. // Local video will not have container found, but that's ok
  482. // since we don't want to switch to local video.
  483. // Update the large video if the video source is already available,
  484. // otherwise wait for the "videoactive.jingle" event.
  485. if (!pinnedId
  486. && remoteVideo.hasVideoStarted()
  487. && !this.getCurrentlyOnLargeContainer().stayOnStage()) {
  488. this.updateLargeVideo(id);
  489. }
  490. },
  491. /**
  492. * On last N change event.
  493. *
  494. * @param lastNEndpoints the list of last N endpoints
  495. * @param endpointsEnteringLastN the list currently entering last N
  496. * endpoints
  497. */
  498. onLastNEndpointsChanged (lastNEndpoints, endpointsEnteringLastN) {
  499. if (this.lastNCount !== lastNEndpoints.length)
  500. this.lastNCount = lastNEndpoints.length;
  501. lastNEndpointsCache = lastNEndpoints;
  502. // Say A, B, C, D, E, and F are in a conference and LastN = 3.
  503. //
  504. // If LastN drops to, say, 2, because of adaptivity, then E should see
  505. // thumbnails for A, B and C. A and B are in E's server side LastN set,
  506. // so E sees them. C is only in E's local LastN set.
  507. //
  508. // If F starts talking and LastN = 3, then E should see thumbnails for
  509. // F, A, B. B gets "ejected" from E's server side LastN set, but it
  510. // enters E's local LastN ejecting C.
  511. // Increase the local LastN set size, if necessary.
  512. if (this.lastNCount > localLastNCount) {
  513. localLastNCount = this.lastNCount;
  514. }
  515. // Update the local LastN set preserving the order in which the
  516. // endpoints appeared in the LastN/local LastN set.
  517. var nextLocalLastNSet = lastNEndpoints.slice(0);
  518. for (var i = 0; i < localLastNSet.length; i++) {
  519. if (nextLocalLastNSet.length >= localLastNCount) {
  520. break;
  521. }
  522. var resourceJid = localLastNSet[i];
  523. if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
  524. nextLocalLastNSet.push(resourceJid);
  525. }
  526. }
  527. localLastNSet = nextLocalLastNSet;
  528. var updateLargeVideo = false;
  529. // Handle LastN/local LastN changes.
  530. FilmStrip.getThumbs().each(( index, element ) => {
  531. var resourceJid = getPeerContainerResourceId(element);
  532. var smallVideo = remoteVideos[resourceJid];
  533. // We do not want to process any logic for our own(local) video
  534. // because the local participant is never in the lastN set.
  535. // The code of this function might detect that the local participant
  536. // has been dropped out of the lastN set and will update the large
  537. // video
  538. // Detected from avatar tests, where lastN event override
  539. // local video pinning
  540. if(APP.conference.isLocalId(resourceJid))
  541. return;
  542. var isReceived = true;
  543. if (resourceJid &&
  544. lastNEndpoints.indexOf(resourceJid) < 0 &&
  545. localLastNSet.indexOf(resourceJid) < 0) {
  546. console.log("Remove from last N", resourceJid);
  547. if (smallVideo)
  548. smallVideo.showPeerContainer('hide');
  549. else if (!APP.conference.isLocalId(resourceJid))
  550. console.error("No remote video for: " + resourceJid);
  551. isReceived = false;
  552. } else if (resourceJid &&
  553. smallVideo.isVisible() &&
  554. lastNEndpoints.indexOf(resourceJid) < 0 &&
  555. localLastNSet.indexOf(resourceJid) >= 0) {
  556. if (smallVideo)
  557. smallVideo.showPeerContainer('avatar');
  558. else if (!APP.conference.isLocalId(resourceJid))
  559. console.error("No remote video for: " + resourceJid);
  560. isReceived = false;
  561. }
  562. if (!isReceived) {
  563. // resourceJid has dropped out of the server side lastN set, so
  564. // it is no longer being received. If resourceJid was being
  565. // displayed in the large video we have to switch to another
  566. // user.
  567. if (!updateLargeVideo &&
  568. this.isCurrentlyOnLarge(resourceJid)) {
  569. updateLargeVideo = true;
  570. }
  571. }
  572. });
  573. if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
  574. endpointsEnteringLastN = lastNEndpoints;
  575. if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
  576. endpointsEnteringLastN.forEach(function (resourceJid) {
  577. var remoteVideo = remoteVideos[resourceJid];
  578. if (remoteVideo)
  579. remoteVideo.showPeerContainer('show');
  580. if (!remoteVideo.isVisible()) {
  581. console.log("Add to last N", resourceJid);
  582. remoteVideo.addRemoteStreamElement(remoteVideo.videoStream);
  583. if (lastNPickupId == resourceJid) {
  584. // Clean up the lastN pickup id.
  585. lastNPickupId = null;
  586. VideoLayout.handleVideoThumbClicked(resourceJid);
  587. updateLargeVideo = false;
  588. }
  589. remoteVideo.waitForPlayback(
  590. remoteVideo.selectVideoElement()[0],
  591. remoteVideo.videoStream);
  592. }
  593. });
  594. }
  595. // The endpoint that was being shown in the large video has dropped out
  596. // of the lastN set and there was no lastN pickup jid. We need to update
  597. // the large video now.
  598. if (updateLargeVideo) {
  599. var resource;
  600. // Find out which endpoint to show in the large video.
  601. for (i = 0; i < lastNEndpoints.length; i++) {
  602. resource = lastNEndpoints[i];
  603. if (!resource || APP.conference.isLocalId(resource))
  604. continue;
  605. // videoSrcToSsrc needs to be update for this call to succeed.
  606. this.updateLargeVideo(resource);
  607. break;
  608. }
  609. }
  610. },
  611. /**
  612. * Updates local stats
  613. * @param percent
  614. * @param object
  615. */
  616. updateLocalConnectionStats (percent, object) {
  617. let resolutions = object.resolution;
  618. object.resolution = resolutions[APP.conference.localId];
  619. localVideoThumbnail.updateStatsIndicator(percent, object);
  620. Object.keys(resolutions).forEach(function (id) {
  621. if (APP.conference.isLocalId(id)) {
  622. return;
  623. }
  624. let resolution = resolutions[id];
  625. let remoteVideo = remoteVideos[id];
  626. if (resolution && remoteVideo) {
  627. remoteVideo.updateResolution(resolution);
  628. }
  629. });
  630. },
  631. /**
  632. * Updates remote stats.
  633. * @param id the id associated with the stats
  634. * @param percent the connection quality percent
  635. * @param object the stats data
  636. */
  637. updateConnectionStats (id, percent, object) {
  638. if (remoteVideos[id]) {
  639. remoteVideos[id].updateStatsIndicator(percent, object);
  640. }
  641. },
  642. /**
  643. * Hides the connection indicator
  644. * @param id
  645. */
  646. hideConnectionIndicator (id) {
  647. remoteVideos[id].hideConnectionIndicator();
  648. },
  649. /**
  650. * Hides all the indicators
  651. */
  652. hideStats () {
  653. for(var video in remoteVideos) {
  654. remoteVideos[video].hideIndicator();
  655. }
  656. localVideoThumbnail.hideIndicator();
  657. },
  658. removeParticipantContainer (id) {
  659. // Unlock large video
  660. if (pinnedId === id) {
  661. console.info("Focused video owner has left the conference");
  662. pinnedId = null;
  663. }
  664. if (currentDominantSpeaker === id) {
  665. console.info("Dominant speaker has left the conference");
  666. currentDominantSpeaker = null;
  667. }
  668. var remoteVideo = remoteVideos[id];
  669. if (remoteVideo) {
  670. // Remove remote video
  671. console.info("Removing remote video: " + id);
  672. delete remoteVideos[id];
  673. remoteVideo.remove();
  674. } else {
  675. console.warn("No remote video for " + id);
  676. }
  677. VideoLayout.resizeThumbnails();
  678. },
  679. onVideoTypeChanged (id, newVideoType) {
  680. if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
  681. return;
  682. }
  683. console.info("Peer video type changed: ", id, newVideoType);
  684. var smallVideo;
  685. if (APP.conference.isLocalId(id)) {
  686. if (!localVideoThumbnail) {
  687. console.warn("Local video not ready yet");
  688. return;
  689. }
  690. smallVideo = localVideoThumbnail;
  691. } else if (remoteVideos[id]) {
  692. smallVideo = remoteVideos[id];
  693. } else {
  694. return;
  695. }
  696. smallVideo.setVideoType(newVideoType);
  697. if (this.isCurrentlyOnLarge(id)) {
  698. this.updateLargeVideo(id, true);
  699. }
  700. },
  701. showMore (id) {
  702. if (id === 'local') {
  703. localVideoThumbnail.connectionIndicator.showMore();
  704. } else {
  705. let remoteVideo = remoteVideos[id];
  706. if (remoteVideo) {
  707. remoteVideo.connectionIndicator.showMore();
  708. } else {
  709. console.info("Error - no remote video for id: " + id);
  710. }
  711. }
  712. },
  713. /**
  714. * Resizes the video area.
  715. *
  716. * @param isSideBarVisible indicates if the side bar is currently visible
  717. * @param forceUpdate indicates that hidden thumbnails will be shown
  718. * @param completeFunction a function to be called when the video area is
  719. * resized.
  720. */
  721. resizeVideoArea (isSideBarVisible,
  722. forceUpdate = false,
  723. animate = false,
  724. completeFunction = null) {
  725. if (largeVideo) {
  726. largeVideo.updateContainerSize(isSideBarVisible);
  727. largeVideo.resize(animate);
  728. }
  729. // Calculate available width and height.
  730. let availableHeight = window.innerHeight;
  731. let availableWidth = UIUtil.getAvailableVideoWidth(isSideBarVisible);
  732. if (availableWidth < 0 || availableHeight < 0) {
  733. return;
  734. }
  735. // Resize the thumbnails first.
  736. this.resizeThumbnails(false, forceUpdate, isSideBarVisible);
  737. // Resize the video area element.
  738. $('#videospace').animate({
  739. right: window.innerWidth - availableWidth,
  740. width: availableWidth,
  741. height: availableHeight
  742. }, {
  743. queue: false,
  744. duration: animate ? 500 : 1,
  745. complete: completeFunction
  746. });
  747. },
  748. getSmallVideo (id) {
  749. if (APP.conference.isLocalId(id)) {
  750. return localVideoThumbnail;
  751. } else {
  752. return remoteVideos[id];
  753. }
  754. },
  755. changeUserAvatar (id, avatarUrl) {
  756. var smallVideo = VideoLayout.getSmallVideo(id);
  757. if (smallVideo) {
  758. smallVideo.avatarChanged(avatarUrl);
  759. } else {
  760. console.warn(
  761. "Missed avatar update - no small video yet for " + id
  762. );
  763. }
  764. if (this.isCurrentlyOnLarge(id)) {
  765. largeVideo.updateAvatar(avatarUrl);
  766. }
  767. },
  768. /**
  769. * Indicates that the video has been interrupted.
  770. */
  771. onVideoInterrupted () {
  772. this.enableVideoProblemFilter(true);
  773. let reconnectingKey = "connection.RECONNECTING";
  774. $('#videoConnectionMessage')
  775. .attr("data-i18n", reconnectingKey)
  776. .text(APP.translation.translateString(reconnectingKey))
  777. .css({display: "block"});
  778. },
  779. /**
  780. * Indicates that the video has been restored.
  781. */
  782. onVideoRestored () {
  783. this.enableVideoProblemFilter(false);
  784. $('#videoConnectionMessage').css({display: "none"});
  785. },
  786. enableVideoProblemFilter (enable) {
  787. if (!largeVideo) {
  788. return;
  789. }
  790. largeVideo.enableVideoProblemFilter(enable);
  791. },
  792. isLargeVideoVisible () {
  793. return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
  794. },
  795. /**
  796. * @return {LargeContainer} the currently displayed container on large
  797. * video.
  798. */
  799. getCurrentlyOnLargeContainer () {
  800. return largeVideo.getContainer(largeVideo.state);
  801. },
  802. isCurrentlyOnLarge (id) {
  803. return largeVideo && largeVideo.id === id;
  804. },
  805. updateLargeVideo (id, forceUpdate) {
  806. if (!largeVideo) {
  807. return;
  808. }
  809. let isOnLarge = this.isCurrentlyOnLarge(id);
  810. let currentId = largeVideo.id;
  811. if (!isOnLarge || forceUpdate) {
  812. let videoType = this.getRemoteVideoType(id);
  813. if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
  814. eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
  815. }
  816. if (currentId) {
  817. var oldSmallVideo = this.getSmallVideo(currentId);
  818. }
  819. let smallVideo = this.getSmallVideo(id);
  820. largeVideo.updateLargeVideo(
  821. id,
  822. smallVideo.videoStream,
  823. videoType
  824. ).then(function() {
  825. // update current small video and the old one
  826. smallVideo.updateView();
  827. oldSmallVideo && oldSmallVideo.updateView();
  828. }, function () {
  829. // use clicked other video during update, nothing to do.
  830. });
  831. } else if (currentId) {
  832. let currentSmallVideo = this.getSmallVideo(currentId);
  833. currentSmallVideo.updateView();
  834. }
  835. },
  836. addLargeVideoContainer (type, container) {
  837. largeVideo && largeVideo.addContainer(type, container);
  838. },
  839. removeLargeVideoContainer (type) {
  840. largeVideo && largeVideo.removeContainer(type);
  841. },
  842. /**
  843. * @returns Promise
  844. */
  845. showLargeVideoContainer (type, show) {
  846. if (!largeVideo) {
  847. return Promise.reject();
  848. }
  849. let isVisible = this.isLargeContainerTypeVisible(type);
  850. if (isVisible === show) {
  851. return Promise.resolve();
  852. }
  853. let currentId = largeVideo.id;
  854. if(currentId) {
  855. var oldSmallVideo = this.getSmallVideo(currentId);
  856. }
  857. let containerTypeToShow = type;
  858. // if we are hiding a container and there is focusedVideo
  859. // (pinned remote video) use its video type,
  860. // if not then use default type - large video
  861. if (!show) {
  862. if(pinnedId)
  863. containerTypeToShow = this.getRemoteVideoType(pinnedId);
  864. else
  865. containerTypeToShow = VIDEO_CONTAINER_TYPE;
  866. }
  867. return largeVideo.showContainer(containerTypeToShow)
  868. .then(() => {
  869. if(oldSmallVideo)
  870. oldSmallVideo && oldSmallVideo.updateView();
  871. });
  872. },
  873. isLargeContainerTypeVisible (type) {
  874. return largeVideo && largeVideo.state === type;
  875. },
  876. /**
  877. * Returns the id of the current video shown on large.
  878. * Currently used by tests (torture).
  879. */
  880. getLargeVideoID () {
  881. return largeVideo.id;
  882. },
  883. /**
  884. * Returns the the current video shown on large.
  885. * Currently used by tests (torture).
  886. */
  887. getLargeVideo () {
  888. return largeVideo;
  889. },
  890. /**
  891. * Updates the resolution label, indicating to the user that the large
  892. * video stream is currently HD.
  893. */
  894. updateResolutionLabel(isResolutionHD) {
  895. let videoResolutionLabel = $("#videoResolutionLabel");
  896. if (isResolutionHD && !videoResolutionLabel.is(":visible"))
  897. videoResolutionLabel.css({display: "block"});
  898. else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
  899. videoResolutionLabel.css({display: "none"});
  900. }
  901. };
  902. export default VideoLayout;