Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

VideoLayout.js 34KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  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 lastNEndpoints the list of last N endpoints
  597. * @param endpointsEnteringLastN the list currently entering last N
  598. * endpoints
  599. */
  600. onLastNEndpointsChanged (lastNEndpoints, endpointsEnteringLastN) {
  601. Object.keys(remoteVideos).forEach(
  602. id => {
  603. if (lastNEndpoints.length > 0
  604. && lastNEndpoints.indexOf(id) < 0
  605. || endpointsEnteringLastN.length > 0
  606. && endpointsEnteringLastN.indexOf(id) > 0) {
  607. let remoteVideo = (id) ? remoteVideos[id] : null;
  608. if (remoteVideo) {
  609. remoteVideo.updateView();
  610. if (remoteVideo.isCurrentlyOnLargeVideo())
  611. this.updateLargeVideo(id);
  612. }
  613. }
  614. });
  615. },
  616. /**
  617. * Updates local stats
  618. * @param percent
  619. * @param object
  620. */
  621. updateLocalConnectionStats (percent, object) {
  622. const { framerate, resolution } = object;
  623. object.resolution = resolution[APP.conference.getMyUserId()];
  624. object.framerate = framerate[APP.conference.getMyUserId()];
  625. localVideoThumbnail.updateStatsIndicator(percent, object);
  626. Object.keys(resolution).forEach(function (id) {
  627. if (APP.conference.isLocalId(id)) {
  628. return;
  629. }
  630. let resolutionValue = resolution[id];
  631. let remoteVideo = remoteVideos[id];
  632. if (resolutionValue && remoteVideo) {
  633. remoteVideo.updateResolution(resolutionValue);
  634. }
  635. });
  636. Object.keys(framerate).forEach(function (id) {
  637. if (APP.conference.isLocalId(id)) {
  638. return;
  639. }
  640. const framerateValue = framerate[id];
  641. const remoteVideo = remoteVideos[id];
  642. if (framerateValue && remoteVideo) {
  643. remoteVideo.updateFramerate(framerateValue);
  644. }
  645. });
  646. },
  647. /**
  648. * Updates remote stats.
  649. * @param id the id associated with the stats
  650. * @param percent the connection quality percent
  651. * @param object the stats data
  652. */
  653. updateConnectionStats (id, percent, object) {
  654. let remoteVideo = remoteVideos[id];
  655. if (remoteVideo) {
  656. remoteVideo.updateStatsIndicator(percent, object);
  657. }
  658. },
  659. /**
  660. * Hides the connection indicator
  661. * @param id
  662. */
  663. hideConnectionIndicator (id) {
  664. let remoteVideo = remoteVideos[id];
  665. if (remoteVideo)
  666. remoteVideo.hideConnectionIndicator();
  667. },
  668. /**
  669. * Hides all the indicators
  670. */
  671. hideStats () {
  672. for (var video in remoteVideos) {
  673. let remoteVideo = remoteVideos[video];
  674. if (remoteVideo)
  675. remoteVideo.hideIndicator();
  676. }
  677. localVideoThumbnail.hideIndicator();
  678. },
  679. removeParticipantContainer (id) {
  680. // Unlock large video
  681. if (pinnedId === id) {
  682. logger.info("Focused video owner has left the conference");
  683. pinnedId = null;
  684. }
  685. if (currentDominantSpeaker === id) {
  686. logger.info("Dominant speaker has left the conference");
  687. currentDominantSpeaker = null;
  688. }
  689. var remoteVideo = remoteVideos[id];
  690. if (remoteVideo) {
  691. // Remove remote video
  692. logger.info("Removing remote video: " + id);
  693. delete remoteVideos[id];
  694. remoteVideo.remove();
  695. } else {
  696. logger.warn("No remote video for " + id);
  697. }
  698. VideoLayout.resizeThumbnails();
  699. },
  700. onVideoTypeChanged (id, newVideoType) {
  701. if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
  702. return;
  703. }
  704. logger.info("Peer video type changed: ", id, newVideoType);
  705. var smallVideo;
  706. if (APP.conference.isLocalId(id)) {
  707. if (!localVideoThumbnail) {
  708. logger.warn("Local video not ready yet");
  709. return;
  710. }
  711. smallVideo = localVideoThumbnail;
  712. } else if (remoteVideos[id]) {
  713. smallVideo = remoteVideos[id];
  714. } else {
  715. return;
  716. }
  717. smallVideo.setVideoType(newVideoType);
  718. if (this.isCurrentlyOnLarge(id)) {
  719. this.updateLargeVideo(id, true);
  720. }
  721. },
  722. showMore (id) {
  723. if (id === 'local') {
  724. localVideoThumbnail.connectionIndicator.showMore();
  725. } else {
  726. let remoteVideo = remoteVideos[id];
  727. if (remoteVideo) {
  728. remoteVideo.connectionIndicator.showMore();
  729. } else {
  730. logger.info("Error - no remote video for id: " + id);
  731. }
  732. }
  733. },
  734. /**
  735. * Resizes the video area.
  736. *
  737. * @param forceUpdate indicates that hidden thumbnails will be shown
  738. * @param completeFunction a function to be called when the video area is
  739. * resized.
  740. */
  741. resizeVideoArea (forceUpdate = false,
  742. animate = false,
  743. completeFunction = null) {
  744. if (largeVideo) {
  745. largeVideo.updateContainerSize();
  746. largeVideo.resize(animate);
  747. }
  748. // Calculate available width and height.
  749. let availableHeight = window.innerHeight;
  750. let availableWidth = UIUtil.getAvailableVideoWidth();
  751. if (availableWidth < 0 || availableHeight < 0) {
  752. return;
  753. }
  754. // Resize the thumbnails first.
  755. this.resizeThumbnails(false, forceUpdate);
  756. // Resize the video area element.
  757. $('#videospace').animate({
  758. right: window.innerWidth - availableWidth,
  759. width: availableWidth,
  760. height: availableHeight
  761. }, {
  762. queue: false,
  763. duration: animate ? 500 : 1,
  764. complete: completeFunction
  765. });
  766. },
  767. getSmallVideo (id) {
  768. if (APP.conference.isLocalId(id)) {
  769. return localVideoThumbnail;
  770. } else {
  771. return remoteVideos[id];
  772. }
  773. },
  774. changeUserAvatar (id, avatarUrl) {
  775. var smallVideo = VideoLayout.getSmallVideo(id);
  776. if (smallVideo) {
  777. smallVideo.avatarChanged(avatarUrl);
  778. } else {
  779. logger.warn(
  780. "Missed avatar update - no small video yet for " + id
  781. );
  782. }
  783. if (this.isCurrentlyOnLarge(id)) {
  784. largeVideo.updateAvatar(avatarUrl);
  785. }
  786. },
  787. /**
  788. * Indicates that the video has been interrupted.
  789. */
  790. onVideoInterrupted () {
  791. if (largeVideo) {
  792. largeVideo.onVideoInterrupted();
  793. }
  794. },
  795. /**
  796. * Indicates that the video has been restored.
  797. */
  798. onVideoRestored () {
  799. if (largeVideo) {
  800. largeVideo.onVideoRestored();
  801. }
  802. },
  803. isLargeVideoVisible () {
  804. return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
  805. },
  806. /**
  807. * @return {LargeContainer} the currently displayed container on large
  808. * video.
  809. */
  810. getCurrentlyOnLargeContainer () {
  811. return largeVideo.getContainer(largeVideo.state);
  812. },
  813. isCurrentlyOnLarge (id) {
  814. return largeVideo && largeVideo.id === id;
  815. },
  816. updateLargeVideo (id, forceUpdate) {
  817. if (!largeVideo) {
  818. return;
  819. }
  820. let isOnLarge = this.isCurrentlyOnLarge(id);
  821. let currentId = largeVideo.id;
  822. if (!isOnLarge || forceUpdate) {
  823. let videoType = this.getRemoteVideoType(id);
  824. // FIXME video type is not the same thing as container type
  825. if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
  826. eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
  827. }
  828. let smallVideo = this.getSmallVideo(id);
  829. let oldSmallVideo;
  830. if (currentId) {
  831. oldSmallVideo = this.getSmallVideo(currentId);
  832. }
  833. smallVideo.waitForResolutionChange();
  834. if (oldSmallVideo)
  835. oldSmallVideo.waitForResolutionChange();
  836. largeVideo.updateLargeVideo(
  837. id,
  838. smallVideo.videoStream,
  839. videoType
  840. ).then(function() {
  841. // update current small video and the old one
  842. smallVideo.updateView();
  843. oldSmallVideo && oldSmallVideo.updateView();
  844. }, function () {
  845. // use clicked other video during update, nothing to do.
  846. });
  847. } else if (currentId) {
  848. let currentSmallVideo = this.getSmallVideo(currentId);
  849. currentSmallVideo.updateView();
  850. }
  851. },
  852. addLargeVideoContainer (type, container) {
  853. largeVideo && largeVideo.addContainer(type, container);
  854. },
  855. removeLargeVideoContainer (type) {
  856. largeVideo && largeVideo.removeContainer(type);
  857. },
  858. /**
  859. * @returns Promise
  860. */
  861. showLargeVideoContainer (type, show) {
  862. if (!largeVideo) {
  863. return Promise.reject();
  864. }
  865. let isVisible = this.isLargeContainerTypeVisible(type);
  866. if (isVisible === show) {
  867. return Promise.resolve();
  868. }
  869. let currentId = largeVideo.id;
  870. if(currentId) {
  871. var oldSmallVideo = this.getSmallVideo(currentId);
  872. }
  873. let containerTypeToShow = type;
  874. // if we are hiding a container and there is focusedVideo
  875. // (pinned remote video) use its video type,
  876. // if not then use default type - large video
  877. if (!show) {
  878. if(pinnedId)
  879. containerTypeToShow = this.getRemoteVideoType(pinnedId);
  880. else
  881. containerTypeToShow = VIDEO_CONTAINER_TYPE;
  882. }
  883. return largeVideo.showContainer(containerTypeToShow)
  884. .then(() => {
  885. if(oldSmallVideo)
  886. oldSmallVideo && oldSmallVideo.updateView();
  887. });
  888. },
  889. isLargeContainerTypeVisible (type) {
  890. return largeVideo && largeVideo.state === type;
  891. },
  892. /**
  893. * Returns the id of the current video shown on large.
  894. * Currently used by tests (torture).
  895. */
  896. getLargeVideoID () {
  897. return largeVideo.id;
  898. },
  899. /**
  900. * Returns the the current video shown on large.
  901. * Currently used by tests (torture).
  902. */
  903. getLargeVideo () {
  904. return largeVideo;
  905. },
  906. /**
  907. * Updates the resolution label, indicating to the user that the large
  908. * video stream is currently HD.
  909. */
  910. updateResolutionLabel(isResolutionHD) {
  911. let id = 'videoResolutionLabel';
  912. UIUtil.setVisible(id, isResolutionHD);
  913. },
  914. /**
  915. * Sets the flipX state of the local video.
  916. * @param {boolean} true for flipped otherwise false;
  917. */
  918. setLocalFlipX (val) {
  919. this.localFlipX = val;
  920. },
  921. getEventEmitter() {return eventEmitter;},
  922. /**
  923. * Handles user's features changes.
  924. */
  925. onUserFeaturesChanged (user) {
  926. let video = this.getSmallVideo(user.getId());
  927. if (!video) {
  928. return;
  929. }
  930. this._setRemoteControlProperties(user, video);
  931. },
  932. /**
  933. * Sets the remote control properties (checks whether remote control
  934. * is supported and executes remoteVideo.setRemoteControlSupport).
  935. * @param {JitsiParticipant} user the user that will be checked for remote
  936. * control support.
  937. * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
  938. * will be set.
  939. */
  940. _setRemoteControlProperties (user, remoteVideo) {
  941. APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
  942. remoteVideo.setRemoteControlSupport(result));
  943. },
  944. /**
  945. * Returns the wrapper jquery selector for the largeVideo
  946. * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
  947. */
  948. getLargeVideoWrapper() {
  949. return this.getCurrentlyOnLargeContainer().$wrapper;
  950. }
  951. };
  952. export default VideoLayout;