Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

Thumbnail.tsx 40KB

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