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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305
  1. /* eslint-disable lines-around-comment */
  2. import { withStyles } from '@material-ui/styles';
  3. import clsx from 'clsx';
  4. import debounce from 'lodash/debounce';
  5. import React, { Component } from 'react';
  6. // @ts-ignore
  7. import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
  8. // @ts-ignore
  9. import { Avatar } from '../../../base/avatar';
  10. // @ts-ignore
  11. import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../../../base/config';
  12. import { isMobileBrowser } from '../../../base/environment/utils';
  13. import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
  14. // @ts-ignore
  15. import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
  16. import { pinParticipant } from '../../../base/participants/actions';
  17. import {
  18. getLocalParticipant,
  19. getParticipantByIdOrUndefined,
  20. hasRaisedHand
  21. } from '../../../base/participants/functions';
  22. import { Participant } from '../../../base/participants/reducer';
  23. import { connect } from '../../../base/redux/functions';
  24. import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
  25. // @ts-ignore
  26. import { isTestModeEnabled } from '../../../base/testing';
  27. import {
  28. getLocalAudioTrack,
  29. getLocalVideoTrack,
  30. getTrackByMediaTypeAndParticipant,
  31. getVirtualScreenshareParticipantTrack,
  32. updateLastTrackVideoMediaEvent,
  33. trackStreamingStatusChanged
  34. // @ts-ignore
  35. } from '../../../base/tracks';
  36. // @ts-ignore
  37. import { getVideoObjectPosition } from '../../../face-landmarks/functions';
  38. // @ts-ignore
  39. import { hideGif, showGif } from '../../../gifs/actions';
  40. // @ts-ignore
  41. import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
  42. // @ts-ignore
  43. import { PresenceLabel } from '../../../presence-status';
  44. // @ts-ignore
  45. import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
  46. // @ts-ignore
  47. import { togglePinStageParticipant } from '../../actions';
  48. import {
  49. DISPLAY_MODE_TO_CLASS_NAME,
  50. DISPLAY_VIDEO,
  51. FILMSTRIP_TYPE,
  52. SHOW_TOOLBAR_CONTEXT_MENU_AFTER,
  53. THUMBNAIL_TYPE,
  54. VIDEO_TEST_EVENTS
  55. } from '../../constants';
  56. import {
  57. computeDisplayModeFromInput,
  58. getActiveParticipantsIds,
  59. getDisplayModeInput,
  60. getThumbnailTypeFromLayout,
  61. isVideoPlayable,
  62. isStageFilmstripAvailable,
  63. showGridInVerticalView
  64. // @ts-ignore
  65. } from '../../functions';
  66. // @ts-ignore
  67. import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
  68. import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
  69. import ThumbnailTopIndicators from './ThumbnailTopIndicators';
  70. // @ts-ignore
  71. import VirtualScreenshareParticipant from './VirtualScreenshareParticipant';
  72. declare let interfaceConfig: any;
  73. /**
  74. * The type of the React {@code Component} state of {@link Thumbnail}.
  75. */
  76. export type State = {
  77. /**
  78. * Indicates that the canplay event has been received.
  79. */
  80. canPlayEventReceived: boolean,
  81. /**
  82. * The current display mode of the thumbnail.
  83. */
  84. displayMode: number,
  85. /**
  86. * Indicates whether the thumbnail is hovered or not.
  87. */
  88. isHovered: boolean,
  89. /**
  90. * Whether popover is visible or not.
  91. */
  92. popoverVisible: boolean
  93. };
  94. /**
  95. * The type of the React {@code Component} props of {@link Thumbnail}.
  96. */
  97. export type Props = {
  98. /**
  99. * The audio track related to the participant.
  100. */
  101. _audioTrack?: Object,
  102. /**
  103. * Indicates whether the local video flip feature is disabled or not.
  104. */
  105. _disableLocalVideoFlip: boolean,
  106. /**
  107. * Indicates whether enlargement of tiles to fill the available space is disabled.
  108. */
  109. _disableTileEnlargement: boolean,
  110. /**
  111. * URL of GIF sent by this participant, null if there's none.
  112. */
  113. _gifSrc?: string,
  114. /**
  115. * The height of the Thumbnail.
  116. */
  117. _height: number,
  118. /**
  119. * Whether or not the participant is displayed on the stage filmstrip.
  120. * Used to hide the video from the vertical filmstrip.
  121. */
  122. _isActiveParticipant: boolean,
  123. /**
  124. * Indicates whether audio only mode is enabled.
  125. */
  126. _isAudioOnly: boolean,
  127. /**
  128. * Indicates whether the participant associated with the thumbnail is displayed on the large video.
  129. */
  130. _isCurrentlyOnLargeVideo: boolean,
  131. /**
  132. * Disable/enable the dominant speaker indicator.
  133. */
  134. _isDominantSpeakerDisabled: boolean,
  135. /**
  136. * Indicates whether the thumbnail should be hidden or not.
  137. */
  138. _isHidden: boolean,
  139. /**
  140. * Whether we are currently running in a mobile browser.
  141. */
  142. _isMobile: boolean,
  143. /**
  144. * Whether we are currently running in a mobile browser in portrait orientation.
  145. */
  146. _isMobilePortrait: boolean,
  147. /**
  148. * Indicates whether the participant is screen sharing.
  149. */
  150. _isScreenSharing: boolean,
  151. /**
  152. * Indicates whether testing mode is enabled.
  153. */
  154. _isTestModeEnabled: boolean,
  155. /**
  156. * Indicates whether the video associated with the thumbnail is playable.
  157. */
  158. _isVideoPlayable: boolean,
  159. /**
  160. * Indicates whether the participant is a virtual screen share participant. This prop is behind the
  161. * sourceNameSignaling feature flag.
  162. */
  163. _isVirtualScreenshareParticipant: boolean,
  164. /**
  165. * The current local video flip setting.
  166. */
  167. _localFlipX: boolean,
  168. /**
  169. * An object with information about the participant related to the thumbnail.
  170. */
  171. _participant: Participant,
  172. /**
  173. * Whether or not the participant has the hand raised.
  174. */
  175. _raisedHand: boolean,
  176. /**
  177. * Whether source name signaling is enabled.
  178. */
  179. _sourceNameSignalingEnabled: boolean,
  180. /**
  181. * Whether or not the current layout is stage filmstrip layout.
  182. */
  183. _stageFilmstripLayout: boolean,
  184. /**
  185. * Whether or not the participants are displayed on stage.
  186. * (and not screensharing or shared video; used to determine
  187. * whether or not the display the participant video in the vertical filmstrip).
  188. */
  189. _stageParticipantsVisible: boolean,
  190. /**
  191. * The type of thumbnail to display.
  192. */
  193. _thumbnailType: string,
  194. /**
  195. * The video object position for the participant.
  196. */
  197. _videoObjectPosition: string,
  198. /**
  199. * The video track that will be displayed in the thumbnail.
  200. */
  201. _videoTrack?: any,
  202. /**
  203. * The width of the thumbnail.
  204. */
  205. _width: number,
  206. /**
  207. * An object containing CSS classes.
  208. */
  209. classes: any,
  210. /**
  211. * The redux dispatch function.
  212. */
  213. dispatch: Function,
  214. /**
  215. * The type of filmstrip the tile is displayed in.
  216. */
  217. filmstripType: string,
  218. /**
  219. * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
  220. */
  221. horizontalOffset: number,
  222. /**
  223. * The ID of the participant related to the thumbnail.
  224. */
  225. participantID?: string,
  226. /**
  227. * Styles that will be set to the Thumbnail's main span element.
  228. */
  229. style?: any,
  230. /**
  231. * The width of the thumbnail. Used for expanding the width of the thumbnails on last row in case
  232. * there is empty space.
  233. */
  234. width?: number
  235. };
  236. const defaultStyles = (theme: any) => {
  237. return {
  238. indicatorsContainer: {
  239. position: 'absolute',
  240. padding: `${theme.spacing(1)}px`,
  241. zIndex: 10,
  242. width: '100%',
  243. boxSizing: 'border-box',
  244. display: 'flex',
  245. left: 0,
  246. '&.tile-view-mode': {
  247. padding: `${theme.spacing(2)}px`
  248. }
  249. },
  250. indicatorsTopContainer: {
  251. top: 0,
  252. justifyContent: 'space-between'
  253. },
  254. indicatorsBottomContainer: {
  255. bottom: 0
  256. },
  257. indicatorsBackground: {
  258. backgroundColor: 'rgba(0, 0, 0, 0.7)',
  259. borderRadius: '4px',
  260. display: 'flex',
  261. alignItems: 'center',
  262. maxWidth: '100%',
  263. overflow: 'hidden',
  264. '&:not(:empty)': {
  265. padding: '2px'
  266. },
  267. '& > *:not(:last-child)': {
  268. marginRight: '4px'
  269. },
  270. '&:not(.top-indicators) > span:last-child': {
  271. marginRight: '6px'
  272. }
  273. },
  274. containerBackground: {
  275. position: 'absolute',
  276. top: 0,
  277. left: 0,
  278. height: '100%',
  279. width: '100%',
  280. borderRadius: '4px',
  281. backgroundColor: theme.palette.ui02
  282. },
  283. borderIndicator: {
  284. position: 'absolute',
  285. width: '100%',
  286. height: '100%',
  287. zIndex: 9,
  288. borderRadius: '4px'
  289. },
  290. borderIndicatorOnTop: {
  291. zIndex: 11
  292. },
  293. activeSpeaker: {
  294. '& .active-speaker-indicator': {
  295. boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
  296. }
  297. },
  298. raisedHand: {
  299. '& .raised-hand-border': {
  300. boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
  301. }
  302. },
  303. gif: {
  304. position: 'absolute',
  305. width: '100%',
  306. height: '100%',
  307. zIndex: 11,
  308. display: 'flex',
  309. justifyContent: 'center',
  310. alignItems: 'center',
  311. overflow: 'hidden',
  312. backgroundColor: theme.palette.ui02,
  313. '& img': {
  314. maxWidth: '100%',
  315. maxHeight: '100%',
  316. objectFit: 'contain',
  317. flexGrow: '1'
  318. }
  319. }
  320. };
  321. };
  322. /**
  323. * Implements a thumbnail.
  324. *
  325. * @augments Component
  326. */
  327. class Thumbnail extends Component<Props, State> {
  328. /**
  329. * The long touch setTimeout handler.
  330. */
  331. timeoutHandle?: number;
  332. /**
  333. * Timeout used to detect double tapping.
  334. * It is active while user has tapped once.
  335. */
  336. _firstTap?: number;
  337. /**
  338. * Initializes a new Thumbnail instance.
  339. *
  340. * @param {Object} props - The read-only React Component props with which
  341. * the new instance is to be initialized.
  342. */
  343. constructor(props: Props) {
  344. super(props);
  345. const state = {
  346. canPlayEventReceived: false,
  347. displayMode: DISPLAY_VIDEO,
  348. popoverVisible: false,
  349. isHovered: false
  350. };
  351. this.state = {
  352. ...state,
  353. displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state))
  354. };
  355. this.timeoutHandle = undefined;
  356. this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
  357. this._onCanPlay = this._onCanPlay.bind(this);
  358. this._onClick = this._onClick.bind(this);
  359. this._onMouseEnter = this._onMouseEnter.bind(this);
  360. this._onMouseMove = debounce(this._onMouseMove.bind(this), 100, {
  361. leading: true,
  362. trailing: false
  363. });
  364. this._onMouseLeave = this._onMouseLeave.bind(this);
  365. this._onTestingEvent = this._onTestingEvent.bind(this);
  366. this._onTouchStart = this._onTouchStart.bind(this);
  367. this._onTouchEnd = this._onTouchEnd.bind(this);
  368. this._onTouchMove = this._onTouchMove.bind(this);
  369. this._showPopover = this._showPopover.bind(this);
  370. this._hidePopover = this._hidePopover.bind(this);
  371. this._onGifMouseEnter = this._onGifMouseEnter.bind(this);
  372. this._onGifMouseLeave = this._onGifMouseLeave.bind(this);
  373. this.handleTrackStreamingStatusChanged = this.handleTrackStreamingStatusChanged.bind(this);
  374. }
  375. /**
  376. * Starts listening for track streaming status updates after the initial render.
  377. *
  378. * @inheritdoc
  379. * @returns {void}
  380. */
  381. componentDidMount() {
  382. this._onDisplayModeChanged();
  383. // Listen to track streaming status changed event to keep it updated.
  384. // TODO: after converting this component to a react function component,
  385. // use a custom hook to update local track streaming status.
  386. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  387. if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
  388. _videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  389. this.handleTrackStreamingStatusChanged);
  390. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  391. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  392. }
  393. }
  394. /**
  395. * Remove listeners for track streaming status update.
  396. *
  397. * @inheritdoc
  398. * @returns {void}
  399. */
  400. componentWillUnmount() {
  401. // TODO: after converting this component to a react function component,
  402. // use a custom hook to update local track streaming status.
  403. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  404. if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
  405. _videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  406. this.handleTrackStreamingStatusChanged);
  407. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  408. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  409. }
  410. }
  411. /**
  412. * Stops listening for track streaming status updates on the old track and starts
  413. * listening instead on the new track.
  414. *
  415. * @inheritdoc
  416. * @returns {void}
  417. */
  418. componentDidUpdate(prevProps: Props, prevState: State) {
  419. if (prevState.displayMode !== this.state.displayMode) {
  420. this._onDisplayModeChanged();
  421. }
  422. // TODO: after converting this component to a react function component,
  423. // use a custom hook to update local track streaming status.
  424. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  425. if (_sourceNameSignalingEnabled
  426. && prevProps._videoTrack?.jitsiTrack?.getSourceName() !== _videoTrack?.jitsiTrack?.getSourceName()) {
  427. if (prevProps._videoTrack && !prevProps._videoTrack.local) {
  428. prevProps._videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  429. this.handleTrackStreamingStatusChanged);
  430. dispatch(trackStreamingStatusChanged(prevProps._videoTrack.jitsiTrack,
  431. prevProps._videoTrack.jitsiTrack.getTrackStreamingStatus()));
  432. }
  433. if (_videoTrack && !_videoTrack.local) {
  434. _videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  435. this.handleTrackStreamingStatusChanged);
  436. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  437. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  438. }
  439. }
  440. }
  441. /**
  442. * Handle track streaming status change event by
  443. * by dispatching an action to update track streaming status for the given track in app state.
  444. *
  445. * @param {JitsiTrack} jitsiTrack - The track with streaming status updated.
  446. * @param {JitsiTrackStreamingStatus} streamingStatus - The updated track streaming status.
  447. * @returns {void}
  448. */
  449. handleTrackStreamingStatusChanged(jitsiTrack: any, streamingStatus: any) {
  450. this.props.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
  451. }
  452. /**
  453. * Handles display mode changes.
  454. *
  455. * @returns {void}
  456. */
  457. _onDisplayModeChanged() {
  458. const input = getDisplayModeInput(this.props, this.state);
  459. this._maybeSendScreenSharingIssueEvents(input);
  460. }
  461. /**
  462. * Sends screen sharing issue event if an issue is detected.
  463. *
  464. * @param {Object} input - The input used to compute the thumbnail display mode.
  465. * @returns {void}
  466. */
  467. _maybeSendScreenSharingIssueEvents(input: any) {
  468. const {
  469. _isAudioOnly,
  470. _isScreenSharing,
  471. _thumbnailType
  472. } = this.props;
  473. const { displayMode } = this.state;
  474. const isTileType = _thumbnailType === THUMBNAIL_TYPE.TILE;
  475. if (!(DISPLAY_VIDEO === displayMode)
  476. && isTileType
  477. && _isScreenSharing
  478. && !_isAudioOnly) {
  479. sendAnalytics(createScreenSharingIssueEvent({
  480. source: 'thumbnail',
  481. ...input
  482. }));
  483. }
  484. }
  485. /**
  486. * Implements React's {@link Component#getDerivedStateFromProps()}.
  487. *
  488. * @inheritdoc
  489. */
  490. static getDerivedStateFromProps(props: Props, prevState: State) {
  491. if (!props._videoTrack && prevState.canPlayEventReceived) {
  492. const newState = {
  493. ...prevState,
  494. canPlayEventReceived: false
  495. };
  496. return {
  497. ...newState,
  498. displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, newState))
  499. };
  500. }
  501. const newDisplayMode = computeDisplayModeFromInput(getDisplayModeInput(props, prevState));
  502. if (newDisplayMode !== prevState.displayMode) {
  503. return {
  504. ...prevState,
  505. displayMode: newDisplayMode
  506. };
  507. }
  508. return null;
  509. }
  510. /**
  511. * Clears the first click timeout.
  512. *
  513. * @returns {void}
  514. */
  515. _clearDoubleClickTimeout() {
  516. clearTimeout(this._firstTap);
  517. this._firstTap = undefined;
  518. }
  519. /**
  520. * Shows popover.
  521. *
  522. * @private
  523. * @returns {void}
  524. */
  525. _showPopover() {
  526. this.setState({
  527. popoverVisible: true
  528. });
  529. }
  530. /**
  531. * Hides popover.
  532. *
  533. * @private
  534. * @returns {void}
  535. */
  536. _hidePopover() {
  537. const { _thumbnailType } = this.props;
  538. if (_thumbnailType === THUMBNAIL_TYPE.VERTICAL) {
  539. this.setState({
  540. isHovered: false
  541. });
  542. }
  543. this.setState({
  544. popoverVisible: false
  545. });
  546. }
  547. /**
  548. * Returns an object with the styles for thumbnail.
  549. *
  550. * @returns {Object} - The styles for the thumbnail.
  551. */
  552. _getStyles(): any {
  553. const { canPlayEventReceived } = this.state;
  554. const {
  555. _disableTileEnlargement,
  556. _height,
  557. _isVirtualScreenshareParticipant,
  558. _isHidden,
  559. _isScreenSharing,
  560. _participant,
  561. _thumbnailType,
  562. _videoObjectPosition,
  563. _videoTrack,
  564. _width,
  565. horizontalOffset,
  566. style
  567. } = this.props;
  568. const isTileType = _thumbnailType === THUMBNAIL_TYPE.TILE;
  569. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  570. const track = jitsiVideoTrack?.track;
  571. const isPortraitVideo = ((track && track.getSettings()?.aspectRatio) || 1) < 1;
  572. let styles: {
  573. avatar: Object,
  574. thumbnail: any,
  575. video: Object
  576. } = {
  577. thumbnail: {},
  578. avatar: {},
  579. video: {}
  580. };
  581. const avatarSize = Math.min(_height / 2, _width - 30);
  582. let { left } = style || {};
  583. if (typeof left === 'number' && horizontalOffset) {
  584. left += horizontalOffset;
  585. }
  586. let videoStyles: any = null;
  587. const doNotStretchVideo = (isPortraitVideo && isTileType)
  588. || _disableTileEnlargement
  589. || _isScreenSharing;
  590. if (canPlayEventReceived || _participant.local || _isVirtualScreenshareParticipant) {
  591. videoStyles = {
  592. objectFit: doNotStretchVideo ? 'contain' : 'cover'
  593. };
  594. } else {
  595. videoStyles = {
  596. display: 'none'
  597. };
  598. }
  599. if (videoStyles.objectFit === 'cover') {
  600. videoStyles.objectPosition = _videoObjectPosition;
  601. }
  602. styles = {
  603. thumbnail: {
  604. ...style,
  605. left,
  606. height: `${_height}px`,
  607. minHeight: `${_height}px`,
  608. minWidth: `${_width}px`,
  609. width: `${_width}px`
  610. },
  611. avatar: {
  612. height: `${avatarSize}px`,
  613. width: `${avatarSize}px`
  614. },
  615. video: videoStyles
  616. };
  617. if (_isHidden) {
  618. styles.thumbnail.display = 'none';
  619. }
  620. return styles;
  621. }
  622. /**
  623. * On click handler.
  624. *
  625. * @returns {void}
  626. */
  627. _onClick() {
  628. const { _participant, dispatch, _stageFilmstripLayout } = this.props;
  629. const { id, pinned } = _participant;
  630. if (_stageFilmstripLayout) {
  631. dispatch(togglePinStageParticipant(id));
  632. } else {
  633. dispatch(pinParticipant(pinned ? null : id));
  634. }
  635. }
  636. /**
  637. * Mouse enter handler.
  638. *
  639. * @returns {void}
  640. */
  641. _onMouseEnter() {
  642. this.setState({ isHovered: true });
  643. }
  644. /**
  645. * Mouse move handler.
  646. *
  647. * @returns {void}
  648. */
  649. _onMouseMove() {
  650. if (!this.state.isHovered) {
  651. // Workaround for the use case where the layout changes (for example the participant pane is closed)
  652. // and as a result the mouse appears on top of the thumbnail. In these use cases the mouse enter
  653. // event on the thumbnail is not triggered in Chrome.
  654. this.setState({ isHovered: true });
  655. }
  656. }
  657. /**
  658. * Mouse leave handler.
  659. *
  660. * @returns {void}
  661. */
  662. _onMouseLeave() {
  663. this.setState({ isHovered: false });
  664. }
  665. /**
  666. * Handler for touch start.
  667. *
  668. * @returns {void}
  669. */
  670. _onTouchStart() {
  671. this.timeoutHandle = window.setTimeout(this._showPopover, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
  672. if (this._firstTap) {
  673. this._clearDoubleClickTimeout();
  674. this._onClick();
  675. return;
  676. }
  677. this._firstTap = window.setTimeout(this._clearDoubleClickTimeout, 300);
  678. }
  679. /**
  680. * Cancel showing popover context menu after x milliseconds if the no. Of milliseconds is not reached yet,
  681. * or just clears the timeout.
  682. *
  683. * @returns {void}
  684. */
  685. _onTouchEnd() {
  686. clearTimeout(this.timeoutHandle);
  687. }
  688. /**
  689. * Cancel showing Context menu after x milliseconds if the number of milliseconds is not reached
  690. * before a touch move(drag), or just clears the timeout.
  691. *
  692. * @returns {void}
  693. */
  694. _onTouchMove() {
  695. clearTimeout(this.timeoutHandle);
  696. }
  697. /**
  698. * Renders a fake participant (youtube video) thumbnail.
  699. *
  700. * @param {string} id - The id of the participant.
  701. * @returns {ReactElement}
  702. */
  703. _renderFakeParticipant() {
  704. const { _isMobile, _participant: { avatarURL } } = this.props;
  705. const styles = this._getStyles();
  706. const containerClassName = this._getContainerClassName();
  707. return (
  708. <span
  709. className = { containerClassName }
  710. id = 'sharedVideoContainer'
  711. onClick = { this._onClick }
  712. { ...(_isMobile ? {} : {
  713. onMouseEnter: this._onMouseEnter,
  714. onMouseMove: this._onMouseMove,
  715. onMouseLeave: this._onMouseLeave
  716. }) }
  717. style = { styles.thumbnail }>
  718. {avatarURL ? (
  719. <img
  720. className = 'sharedVideoAvatar'
  721. src = { avatarURL } />
  722. )
  723. : this._renderAvatar(styles.avatar)}
  724. </span>
  725. );
  726. }
  727. /**
  728. * Renders the avatar.
  729. *
  730. * @param {Object} styles - The styles that will be applied to the avatar.
  731. * @returns {ReactElement}
  732. */
  733. _renderAvatar(styles: Object) {
  734. const { _participant } = this.props;
  735. const { id } = _participant;
  736. return (
  737. <div
  738. className = 'avatar-container'
  739. style = { styles }>
  740. <Avatar
  741. className = 'userAvatar'
  742. participantId = { id } />
  743. </div>
  744. );
  745. }
  746. /**
  747. * Returns the container class name.
  748. *
  749. * @returns {string} - The class name that will be used for the container.
  750. */
  751. _getContainerClassName() {
  752. let className = 'videocontainer';
  753. const { displayMode } = this.state;
  754. const {
  755. _isDominantSpeakerDisabled,
  756. _participant,
  757. _raisedHand,
  758. _thumbnailType,
  759. classes
  760. } = this.props;
  761. className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
  762. if (_raisedHand) {
  763. className += ` ${classes.raisedHand}`;
  764. }
  765. if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
  766. className += ` ${classes.activeSpeaker} dominant-speaker`;
  767. }
  768. if (_thumbnailType !== THUMBNAIL_TYPE.TILE && _participant?.pinned) {
  769. className += ' videoContainerFocused';
  770. }
  771. return className;
  772. }
  773. /**
  774. * Keep showing the GIF for the current participant.
  775. *
  776. * @returns {void}
  777. */
  778. _onGifMouseEnter() {
  779. const { dispatch, _participant: { id } } = this.props;
  780. dispatch(showGif(id));
  781. }
  782. /**
  783. * Keep showing the GIF for the current participant.
  784. *
  785. * @returns {void}
  786. */
  787. _onGifMouseLeave() {
  788. const { dispatch, _participant: { id } } = this.props;
  789. dispatch(hideGif(id));
  790. }
  791. /**
  792. * Renders GIF.
  793. *
  794. * @returns {Component}
  795. */
  796. _renderGif() {
  797. const { _gifSrc, classes } = this.props;
  798. return _gifSrc && (
  799. <div className = { classes.gif }>
  800. <img
  801. alt = 'GIF'
  802. src = { _gifSrc } />
  803. </div>
  804. );
  805. }
  806. /**
  807. * Canplay event listener.
  808. *
  809. * @param {SyntheticEvent} event - The event.
  810. * @returns {void}
  811. */
  812. _onCanPlay(event: any) {
  813. this.setState({ canPlayEventReceived: true });
  814. const {
  815. _isTestModeEnabled,
  816. _videoTrack
  817. } = this.props;
  818. if (_videoTrack && _isTestModeEnabled) {
  819. this._onTestingEvent(event);
  820. }
  821. }
  822. /**
  823. * Event handler for testing events.
  824. *
  825. * @param {SyntheticEvent} event - The event.
  826. * @returns {void}
  827. */
  828. _onTestingEvent(event: any) {
  829. const {
  830. _videoTrack,
  831. dispatch
  832. } = this.props;
  833. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  834. dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
  835. }
  836. /**
  837. * Renders a remote participant's 'thumbnail.
  838. *
  839. * @param {boolean} local - Whether or not it's the local participant.
  840. * @returns {ReactElement}
  841. */
  842. _renderParticipant(local = false) {
  843. const {
  844. _audioTrack,
  845. _disableLocalVideoFlip,
  846. _gifSrc,
  847. _isMobile,
  848. _isMobilePortrait,
  849. _isScreenSharing,
  850. _isTestModeEnabled,
  851. _localFlipX,
  852. _participant,
  853. _thumbnailType,
  854. _videoTrack,
  855. classes,
  856. filmstripType
  857. } = this.props;
  858. const { id } = _participant || {};
  859. const { isHovered, popoverVisible } = this.state;
  860. const styles = this._getStyles();
  861. let containerClassName = this._getContainerClassName();
  862. const videoTrackClassName
  863. = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
  864. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  865. const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
  866. const videoEventListeners: any = {};
  867. if (local) {
  868. if (_isMobilePortrait) {
  869. styles.thumbnail.height = styles.thumbnail.width;
  870. containerClassName = `${containerClassName} self-view-mobile-portrait`;
  871. }
  872. } else {
  873. if (_videoTrack && _isTestModeEnabled) {
  874. VIDEO_TEST_EVENTS.forEach(attribute => {
  875. videoEventListeners[attribute] = this._onTestingEvent;
  876. });
  877. }
  878. videoEventListeners.onCanPlay = this._onCanPlay;
  879. }
  880. const video = _videoTrack && <VideoTrack
  881. className = { local ? videoTrackClassName : '' }
  882. eventHandlers = { videoEventListeners }
  883. id = { local ? 'localVideo_container' : `remoteVideo_${videoTrackId || ''}` }
  884. muted = { local ? undefined : true }
  885. style = { styles.video }
  886. videoTrack = { _videoTrack } />;
  887. return (
  888. <span
  889. className = { containerClassName }
  890. id = { local
  891. ? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
  892. : `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
  893. }
  894. { ...(_isMobile
  895. ? {
  896. onTouchEnd: this._onTouchEnd,
  897. onTouchMove: this._onTouchMove,
  898. onTouchStart: this._onTouchStart
  899. }
  900. : {
  901. onClick: this._onClick,
  902. onMouseEnter: this._onMouseEnter,
  903. onMouseMove: this._onMouseMove,
  904. onMouseLeave: this._onMouseLeave
  905. }
  906. ) }
  907. style = { styles.thumbnail }>
  908. {!_gifSrc && (local
  909. ? <span id = 'localVideoWrapper'>{video}</span>
  910. : video)}
  911. <div className = { classes.containerBackground } />
  912. <div
  913. className = { clsx(classes.indicatorsContainer,
  914. classes.indicatorsTopContainer,
  915. _thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
  916. ) }>
  917. <ThumbnailTopIndicators
  918. hidePopover = { this._hidePopover }
  919. indicatorsClassName = { classes.indicatorsBackground }
  920. isHovered = { isHovered }
  921. local = { local }
  922. participantId = { id }
  923. popoverVisible = { popoverVisible }
  924. showPopover = { this._showPopover }
  925. thumbnailType = { _thumbnailType } />
  926. </div>
  927. <div
  928. className = { clsx(classes.indicatorsContainer,
  929. classes.indicatorsBottomContainer,
  930. _thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
  931. ) }>
  932. <ThumbnailBottomIndicators
  933. className = { classes.indicatorsBackground }
  934. isVirtualScreenshareParticipant = { false }
  935. local = { local }
  936. participantId = { id }
  937. thumbnailType = { _thumbnailType } />
  938. </div>
  939. {!_gifSrc && this._renderAvatar(styles.avatar) }
  940. { !local && (
  941. <div className = 'presence-label-container'>
  942. <PresenceLabel
  943. className = 'presence-label'
  944. participantID = { id } />
  945. </div>
  946. )}
  947. <ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
  948. {this._renderGif()}
  949. <div
  950. className = { clsx(classes.borderIndicator,
  951. _gifSrc && classes.borderIndicatorOnTop,
  952. 'raised-hand-border') } />
  953. <div
  954. className = { clsx(classes.borderIndicator,
  955. _gifSrc && classes.borderIndicatorOnTop,
  956. 'active-speaker-indicator') } />
  957. {_gifSrc && (
  958. <div
  959. className = { clsx(classes.borderIndicator, classes.borderIndicatorOnTop) }
  960. onMouseEnter = { this._onGifMouseEnter }
  961. onMouseLeave = { this._onGifMouseLeave } />
  962. )}
  963. </span>
  964. );
  965. }
  966. /**
  967. * Implements React's {@link Component#render()}.
  968. *
  969. * @inheritdoc
  970. * @returns {ReactElement}
  971. */
  972. render() {
  973. const { _participant, _isVirtualScreenshareParticipant } = this.props;
  974. if (!_participant) {
  975. return null;
  976. }
  977. const { isFakeParticipant, local } = _participant;
  978. if (local) {
  979. return this._renderParticipant(true);
  980. }
  981. if (isFakeParticipant) {
  982. return this._renderFakeParticipant();
  983. }
  984. if (_isVirtualScreenshareParticipant) {
  985. const { isHovered } = this.state;
  986. const { _videoTrack, _isMobile, classes, _thumbnailType } = this.props;
  987. return (
  988. <VirtualScreenshareParticipant
  989. classes = { classes }
  990. containerClassName = { this._getContainerClassName() }
  991. isHovered = { isHovered }
  992. isLocal = { local }
  993. isMobile = { _isMobile }
  994. onClick = { this._onClick }
  995. onMouseEnter = { this._onMouseEnter }
  996. onMouseLeave = { this._onMouseLeave }
  997. onMouseMove = { this._onMouseMove }
  998. onTouchEnd = { this._onTouchEnd }
  999. onTouchMove = { this._onTouchMove }
  1000. onTouchStart = { this._onTouchStart }
  1001. participantId = { _participant.id }
  1002. styles = { this._getStyles() }
  1003. thumbnailType = { _thumbnailType }
  1004. videoTrack = { _videoTrack } />
  1005. );
  1006. }
  1007. return this._renderParticipant();
  1008. }
  1009. }
  1010. /**
  1011. * Maps (parts of) the redux state to the associated props for this component.
  1012. *
  1013. * @param {Object} state - The Redux state.
  1014. * @param {Object} ownProps - The own props of the component.
  1015. * @private
  1016. * @returns {Props}
  1017. */
  1018. function _mapStateToProps(state: any, ownProps: any): Object {
  1019. const { participantID, filmstripType = FILMSTRIP_TYPE.MAIN } = ownProps;
  1020. const participant = getParticipantByIdOrUndefined(state, participantID);
  1021. const id = participant?.id;
  1022. const isLocal = participant?.local ?? true;
  1023. const tracks = state['features/base/tracks'];
  1024. const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
  1025. let _videoTrack;
  1026. if (sourceNameSignalingEnabled && participant?.isVirtualScreenshareParticipant) {
  1027. _videoTrack = getVirtualScreenshareParticipantTrack(tracks, id);
  1028. } else {
  1029. _videoTrack = isLocal
  1030. ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
  1031. }
  1032. const _audioTrack = isLocal
  1033. ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
  1034. const _currentLayout = getCurrentLayout(state);
  1035. let size: any = {};
  1036. let _isMobilePortrait = false;
  1037. const {
  1038. defaultLocalDisplayName,
  1039. disableLocalVideoFlip,
  1040. disableTileEnlargement,
  1041. iAmRecorder,
  1042. iAmSipGateway
  1043. } = state['features/base/config'];
  1044. const { localFlipX } = state['features/base/settings'];
  1045. const _isMobile = isMobileBrowser();
  1046. const activeParticipants = getActiveParticipantsIds(state);
  1047. const tileType = getThumbnailTypeFromLayout(_currentLayout, filmstripType);
  1048. switch (tileType) {
  1049. case THUMBNAIL_TYPE.VERTICAL:
  1050. case THUMBNAIL_TYPE.HORIZONTAL: {
  1051. const {
  1052. horizontalViewDimensions = {
  1053. local: {},
  1054. remote: {}
  1055. },
  1056. verticalViewDimensions = {
  1057. local: {},
  1058. remote: {},
  1059. gridView: {}
  1060. }
  1061. } = state['features/filmstrip'];
  1062. const _verticalViewGrid = showGridInVerticalView(state);
  1063. const { local, remote }
  1064. = tileType === THUMBNAIL_TYPE.VERTICAL
  1065. ? verticalViewDimensions : horizontalViewDimensions;
  1066. const { width, height } = (isLocal ? local : remote) ?? {};
  1067. size = {
  1068. _width: width,
  1069. _height: height
  1070. };
  1071. if (_verticalViewGrid) {
  1072. const { width: _width, height: _height } = verticalViewDimensions.gridView.thumbnailSize;
  1073. size = {
  1074. _width,
  1075. _height
  1076. };
  1077. }
  1078. _isMobilePortrait = _isMobile && state['features/base/responsive-ui'].aspectRatio === ASPECT_RATIO_NARROW;
  1079. break;
  1080. }
  1081. case THUMBNAIL_TYPE.TILE: {
  1082. const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
  1083. const {
  1084. stageFilmstripDimensions = {
  1085. thumbnailSize: {}
  1086. },
  1087. screenshareFilmstripDimensions
  1088. } = state['features/filmstrip'];
  1089. size = {
  1090. _width: thumbnailSize?.width,
  1091. _height: thumbnailSize?.height
  1092. };
  1093. if (filmstripType === FILMSTRIP_TYPE.STAGE) {
  1094. const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
  1095. size = {
  1096. _width,
  1097. _height
  1098. };
  1099. } else if (filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
  1100. const { width: _width, height: _height } = screenshareFilmstripDimensions.thumbnailSize;
  1101. size = {
  1102. _width,
  1103. _height
  1104. };
  1105. }
  1106. break;
  1107. }
  1108. }
  1109. if (ownProps.width) {
  1110. size._width = ownProps.width;
  1111. }
  1112. const { gifUrl: gifSrc } = getGifForParticipant(state, id);
  1113. const mode = getGifDisplayMode(state);
  1114. const participantId = isLocal ? getLocalParticipant(state).id : participantID;
  1115. return {
  1116. _audioTrack,
  1117. _currentLayout,
  1118. _defaultLocalDisplayName: defaultLocalDisplayName,
  1119. _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
  1120. _disableTileEnlargement: Boolean(disableTileEnlargement),
  1121. _isActiveParticipant: activeParticipants.find((pId: string) => pId === participantId),
  1122. _isHidden: isLocal && iAmRecorder && !iAmSipGateway,
  1123. _isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
  1124. _isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
  1125. _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
  1126. _isMobile,
  1127. _isMobilePortrait,
  1128. _isScreenSharing: _videoTrack?.videoType === 'desktop',
  1129. _isTestModeEnabled: isTestModeEnabled(state),
  1130. _isVideoPlayable: id && isVideoPlayable(state, id),
  1131. _isVirtualScreenshareParticipant: sourceNameSignalingEnabled && participant?.isVirtualScreenshareParticipant,
  1132. _localFlipX: Boolean(localFlipX),
  1133. _multipleVideoSupport: getMultipleVideoSupportFeatureFlag(state),
  1134. _participant: participant,
  1135. _raisedHand: hasRaisedHand(participant),
  1136. _stageFilmstripLayout: isStageFilmstripAvailable(state),
  1137. _stageParticipantsVisible: _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW,
  1138. _thumbnailType: tileType,
  1139. _videoObjectPosition: getVideoObjectPosition(state, participant?.id),
  1140. _videoTrack,
  1141. ...size,
  1142. _gifSrc: mode === 'chat' ? null : gifSrc,
  1143. _sourceNameSignalingEnabled: sourceNameSignalingEnabled
  1144. };
  1145. }
  1146. // @ts-ignore
  1147. export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail));