Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

Thumbnail.js 38KB

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