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.

SmallVideo.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /* global $, APP, interfaceConfig */
  2. /* eslint-disable no-unused-vars */
  3. import { AtlasKitThemeProvider } from '@atlaskit/theme';
  4. import Logger from 'jitsi-meet-logger';
  5. import React from 'react';
  6. import ReactDOM from 'react-dom';
  7. import { I18nextProvider } from 'react-i18next';
  8. import { Provider } from 'react-redux';
  9. import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
  10. import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
  11. import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
  12. import { i18next } from '../../../react/features/base/i18n';
  13. import { MEDIA_TYPE } from '../../../react/features/base/media';
  14. import {
  15. getLocalParticipant,
  16. getParticipantById,
  17. getParticipantCount,
  18. getPinnedParticipant,
  19. pinParticipant
  20. } from '../../../react/features/base/participants';
  21. import {
  22. getLocalVideoTrack,
  23. getTrackByMediaTypeAndParticipant,
  24. isLocalTrackMuted,
  25. isRemoteTrackMuted
  26. } from '../../../react/features/base/tracks';
  27. import { ConnectionIndicator } from '../../../react/features/connection-indicator';
  28. import { DisplayName } from '../../../react/features/display-name';
  29. import {
  30. DominantSpeakerIndicator,
  31. isVideoPlayable,
  32. RaisedHandIndicator,
  33. StatusIndicators
  34. } from '../../../react/features/filmstrip';
  35. import {
  36. LAYOUTS,
  37. getCurrentLayout,
  38. setTileView,
  39. shouldDisplayTileView
  40. } from '../../../react/features/video-layout';
  41. /* eslint-enable no-unused-vars */
  42. const logger = Logger.getLogger(__filename);
  43. /**
  44. * Display mode constant used when video is being displayed on the small video.
  45. * @type {number}
  46. * @constant
  47. */
  48. const DISPLAY_VIDEO = 0;
  49. /**
  50. * Display mode constant used when the user's avatar is being displayed on
  51. * the small video.
  52. * @type {number}
  53. * @constant
  54. */
  55. const DISPLAY_AVATAR = 1;
  56. /**
  57. * Display mode constant used when neither video nor avatar is being displayed
  58. * on the small video. And we just show the display name.
  59. * @type {number}
  60. * @constant
  61. */
  62. const DISPLAY_BLACKNESS_WITH_NAME = 2;
  63. /**
  64. * Display mode constant used when video is displayed and display name
  65. * at the same time.
  66. * @type {number}
  67. * @constant
  68. */
  69. const DISPLAY_VIDEO_WITH_NAME = 3;
  70. /**
  71. * Display mode constant used when neither video nor avatar is being displayed
  72. * on the small video. And we just show the display name.
  73. * @type {number}
  74. * @constant
  75. */
  76. const DISPLAY_AVATAR_WITH_NAME = 4;
  77. /**
  78. *
  79. */
  80. export default class SmallVideo {
  81. /**
  82. * Constructor.
  83. */
  84. constructor() {
  85. this.videoIsHovered = false;
  86. this.videoType = undefined;
  87. // Bind event handlers so they are only bound once for every instance.
  88. this.updateView = this.updateView.bind(this);
  89. this._onContainerClick = this._onContainerClick.bind(this);
  90. }
  91. /**
  92. * Returns the identifier of this small video.
  93. *
  94. * @returns the identifier of this small video
  95. */
  96. getId() {
  97. return this.id;
  98. }
  99. /**
  100. * Indicates if this small video is currently visible.
  101. *
  102. * @return <tt>true</tt> if this small video isn't currently visible and
  103. * <tt>false</tt> - otherwise.
  104. */
  105. isVisible() {
  106. return this.$container.is(':visible');
  107. }
  108. /**
  109. * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
  110. */
  111. bindHoverHandler() {
  112. // Add hover handler
  113. this.$container.hover(
  114. () => {
  115. this.videoIsHovered = true;
  116. this.renderThumbnail(true);
  117. this.updateView();
  118. },
  119. () => {
  120. this.videoIsHovered = false;
  121. this.renderThumbnail(false);
  122. this.updateView();
  123. }
  124. );
  125. }
  126. /**
  127. * Renders the thumbnail.
  128. */
  129. renderThumbnail() {
  130. // Should be implemented by in subclasses.
  131. }
  132. /**
  133. * This is an especially interesting function. A naive reader might think that
  134. * it returns this SmallVideo's "video" element. But it is much more exciting.
  135. * It first finds this video's parent element using jquery, then uses a utility
  136. * from lib-jitsi-meet to extract the video element from it (with two more
  137. * jquery calls), and finally uses jquery again to encapsulate the video element
  138. * in an array. This last step allows (some might prefer "forces") users of
  139. * this function to access the video element via the 0th element of the returned
  140. * array (after checking its length of course!).
  141. */
  142. selectVideoElement() {
  143. return $($(this.container).find('video')[0]);
  144. }
  145. /**
  146. * Enables / disables the css responsible for focusing/pinning a video
  147. * thumbnail.
  148. *
  149. * @param isFocused indicates if the thumbnail should be focused/pinned or not
  150. */
  151. focus(isFocused) {
  152. const focusedCssClass = 'videoContainerFocused';
  153. const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
  154. if (!isFocused && isFocusClassEnabled) {
  155. this.$container.removeClass(focusedCssClass);
  156. } else if (isFocused && !isFocusClassEnabled) {
  157. this.$container.addClass(focusedCssClass);
  158. }
  159. }
  160. /**
  161. *
  162. */
  163. hasVideo() {
  164. return this.selectVideoElement().length !== 0;
  165. }
  166. /**
  167. * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
  168. * being displayed on the "large video".
  169. *
  170. * @return {boolean} <tt>true</tt> if the user is displayed on the large video
  171. * or <tt>false</tt> otherwise.
  172. */
  173. isCurrentlyOnLargeVideo() {
  174. return APP.store.getState()['features/large-video']?.participantId === this.id;
  175. }
  176. /**
  177. * Checks whether there is a playable video stream available for the user
  178. * associated with this <tt>SmallVideo</tt>.
  179. *
  180. * @return {boolean} <tt>true</tt> if there is a playable video stream available
  181. * or <tt>false</tt> otherwise.
  182. */
  183. isVideoPlayable() {
  184. return isVideoPlayable(APP.store.getState(), this.id);
  185. }
  186. /**
  187. * Determines what should be display on the thumbnail.
  188. *
  189. * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
  190. * or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
  191. */
  192. selectDisplayMode(input) {
  193. if (!input.tileViewActive && input.isScreenSharing) {
  194. return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
  195. } else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
  196. // Display name is always and only displayed when user is on the stage
  197. return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
  198. } else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
  199. // check hovering and change state to video with name
  200. return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
  201. }
  202. // check hovering and change state to avatar with name
  203. return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
  204. }
  205. /**
  206. * Computes information that determine the display mode.
  207. *
  208. * @returns {Object}
  209. */
  210. computeDisplayModeInput() {
  211. let isScreenSharing = false;
  212. let connectionStatus;
  213. const state = APP.store.getState();
  214. const id = this.id;
  215. const participant = getParticipantById(state, id);
  216. const isLocal = participant?.local ?? true;
  217. const tracks = state['features/base/tracks'];
  218. const videoTrack
  219. = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
  220. if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
  221. isScreenSharing = videoTrack?.videoType === 'desktop';
  222. connectionStatus = participant.connectionStatus;
  223. }
  224. return {
  225. isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
  226. isHovered: this._isHovered(),
  227. isAudioOnly: APP.conference.isAudioOnly(),
  228. tileViewActive: shouldDisplayTileView(state),
  229. isVideoPlayable: this.isVideoPlayable(),
  230. hasVideo: Boolean(this.selectVideoElement().length),
  231. connectionStatus,
  232. canPlayEventReceived: this._canPlayEventReceived,
  233. videoStream: Boolean(videoTrack),
  234. isScreenSharing,
  235. videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
  236. };
  237. }
  238. /**
  239. * Checks whether current video is considered hovered. Currently it is hovered
  240. * if the mouse is over the video, or if the connection
  241. * indicator is shown(hovered).
  242. * @private
  243. */
  244. _isHovered() {
  245. return this.videoIsHovered;
  246. }
  247. /**
  248. * Updates the css classes of the thumbnail based on the current state.
  249. */
  250. updateView() {
  251. this.$container.removeClass((index, classNames) =>
  252. classNames.split(' ').filter(name => name.startsWith('display-')));
  253. const oldDisplayMode = this.displayMode;
  254. let displayModeString = '';
  255. const displayModeInput = this.computeDisplayModeInput();
  256. // Determine whether video, avatar or blackness should be displayed
  257. this.displayMode = this.selectDisplayMode(displayModeInput);
  258. switch (this.displayMode) {
  259. case DISPLAY_AVATAR_WITH_NAME:
  260. displayModeString = 'avatar-with-name';
  261. this.$container.addClass('display-avatar-with-name');
  262. break;
  263. case DISPLAY_BLACKNESS_WITH_NAME:
  264. displayModeString = 'blackness-with-name';
  265. this.$container.addClass('display-name-on-black');
  266. break;
  267. case DISPLAY_VIDEO:
  268. displayModeString = 'video';
  269. this.$container.addClass('display-video');
  270. break;
  271. case DISPLAY_VIDEO_WITH_NAME:
  272. displayModeString = 'video-with-name';
  273. this.$container.addClass('display-name-on-video');
  274. break;
  275. case DISPLAY_AVATAR:
  276. default:
  277. displayModeString = 'avatar';
  278. this.$container.addClass('display-avatar-only');
  279. break;
  280. }
  281. if (this.displayMode !== oldDisplayMode) {
  282. logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
  283. }
  284. if (this.displayMode !== DISPLAY_VIDEO
  285. && this.displayMode !== DISPLAY_VIDEO_WITH_NAME
  286. && displayModeInput.tileViewActive
  287. && displayModeInput.isScreenSharing
  288. && !displayModeInput.isAudioOnly) {
  289. // send the event
  290. sendAnalytics(createScreenSharingIssueEvent({
  291. source: 'thumbnail',
  292. ...displayModeInput
  293. }));
  294. }
  295. }
  296. /**
  297. * Shows or hides the dominant speaker indicator.
  298. * @param show whether to show or hide.
  299. */
  300. showDominantSpeakerIndicator(show) {
  301. // Don't create and show dominant speaker indicator if
  302. // DISABLE_DOMINANT_SPEAKER_INDICATOR is true
  303. if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
  304. return;
  305. }
  306. if (!this.container) {
  307. logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
  308. return;
  309. }
  310. this.$container.toggleClass('active-speaker', show);
  311. }
  312. /**
  313. * Initalizes any browser specific properties. Currently sets the overflow
  314. * property for Qt browsers on Windows to hidden, thus fixing the following
  315. * problem:
  316. * Some browsers don't have full support of the object-fit property for the
  317. * video element and when we set video object-fit to "cover" the video
  318. * actually overflows the boundaries of its container, so it's important
  319. * to indicate that the "overflow" should be hidden.
  320. *
  321. * Setting this property for all browsers will result in broken audio levels,
  322. * which makes this a temporary solution, before reworking audio levels.
  323. */
  324. initBrowserSpecificProperties() {
  325. const userAgent = window.navigator.userAgent;
  326. if (userAgent.indexOf('QtWebEngine') > -1
  327. && (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
  328. this.$container.css('overflow', 'hidden');
  329. }
  330. }
  331. /**
  332. * Cleans up components on {@code SmallVideo} and removes itself from the DOM.
  333. *
  334. * @returns {void}
  335. */
  336. remove() {
  337. logger.log('Remove thumbnail', this.id);
  338. this._unmountThumbnail();
  339. // Remove whole container
  340. if (this.container.parentNode) {
  341. this.container.parentNode.removeChild(this.container);
  342. }
  343. }
  344. /**
  345. * Helper function for re-rendering multiple react components of the small
  346. * video.
  347. *
  348. * @returns {void}
  349. */
  350. rerender() {
  351. this.updateView();
  352. }
  353. /**
  354. * Callback invoked when the thumbnail is clicked and potentially trigger
  355. * pinning of the participant.
  356. *
  357. * @param {MouseEvent} event - The click event to intercept.
  358. * @private
  359. * @returns {void}
  360. */
  361. _onContainerClick(event) {
  362. const triggerPin = this._shouldTriggerPin(event);
  363. if (event.stopPropagation && triggerPin) {
  364. event.stopPropagation();
  365. event.preventDefault();
  366. }
  367. if (triggerPin) {
  368. this.togglePin();
  369. }
  370. return false;
  371. }
  372. /**
  373. * Returns whether or not a click event is targeted at certain elements which
  374. * should not trigger a pin.
  375. *
  376. * @param {MouseEvent} event - The click event to intercept.
  377. * @private
  378. * @returns {boolean}
  379. */
  380. _shouldTriggerPin(event) {
  381. // TODO Checking the classes is a workround to allow events to bubble into
  382. // the DisplayName component if it was clicked. React's synthetic events
  383. // will fire after jQuery handlers execute, so stop propogation at this
  384. // point will prevent DisplayName from getting click events. This workaround
  385. // should be removeable once LocalVideo is a React Component because then
  386. // the components share the same eventing system.
  387. const $source = $(event.target || event.srcElement);
  388. return $source.parents('.displayNameContainer').length === 0
  389. && $source.parents('.popover').length === 0
  390. && !event.target.classList.contains('popover');
  391. }
  392. /**
  393. * Pins the participant displayed by this thumbnail or unpins if already pinned.
  394. *
  395. * @returns {void}
  396. */
  397. togglePin() {
  398. const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
  399. const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
  400. APP.store.dispatch(pinParticipant(participantIdToPin));
  401. }
  402. /**
  403. * Unmounts the thumbnail.
  404. */
  405. _unmountThumbnail() {
  406. ReactDOM.unmountComponentAtNode(this.container);
  407. }
  408. /**
  409. * Sets the size of the thumbnail.
  410. */
  411. _setThumbnailSize() {
  412. const layout = getCurrentLayout(APP.store.getState());
  413. const heightToWidthPercent = 100
  414. / (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
  415. switch (layout) {
  416. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
  417. this.$container.css('padding-top', `${heightToWidthPercent}%`);
  418. break;
  419. }
  420. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  421. const state = APP.store.getState();
  422. const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
  423. const size = this.isLocal ? local : remote;
  424. if (typeof size !== 'undefined') {
  425. const { height, width } = size;
  426. this.$container.css({
  427. height: `${height}px`,
  428. 'min-height': `${height}px`,
  429. 'min-width': `${width}px`,
  430. width: `${width}px`
  431. });
  432. }
  433. break;
  434. }
  435. case LAYOUTS.TILE_VIEW: {
  436. const state = APP.store.getState();
  437. const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
  438. if (typeof thumbnailSize !== 'undefined') {
  439. const { height, width } = thumbnailSize;
  440. this.$container.css({
  441. height: `${height}px`,
  442. 'min-height': `${height}px`,
  443. 'min-width': `${width}px`,
  444. width: `${width}px`
  445. });
  446. }
  447. break;
  448. }
  449. }
  450. }
  451. }