Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

VideoLayout.js 34KB

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