You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Thumbnail.js 30KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. // @flow
  2. import React, { Component } from 'react';
  3. import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
  4. import { AudioLevelIndicator } from '../../../audio-level-indicator';
  5. import { Avatar } from '../../../base/avatar';
  6. import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
  7. import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
  8. import AudioTrack from '../../../base/media/components/web/AudioTrack';
  9. import {
  10. getLocalParticipant,
  11. getParticipantById,
  12. getParticipantCount,
  13. pinParticipant
  14. } from '../../../base/participants';
  15. import { connect } from '../../../base/redux';
  16. import { isTestModeEnabled } from '../../../base/testing';
  17. import {
  18. getLocalAudioTrack,
  19. getLocalVideoTrack,
  20. getTrackByMediaTypeAndParticipant,
  21. updateLastTrackVideoMediaEvent
  22. } from '../../../base/tracks';
  23. import { ConnectionIndicator } from '../../../connection-indicator';
  24. import { DisplayName } from '../../../display-name';
  25. import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
  26. import { PresenceLabel } from '../../../presence-status';
  27. import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
  28. import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
  29. import {
  30. DISPLAY_MODE_TO_CLASS_NAME,
  31. DISPLAY_MODE_TO_STRING,
  32. DISPLAY_VIDEO,
  33. DISPLAY_VIDEO_WITH_NAME,
  34. VIDEO_TEST_EVENTS
  35. } from '../../constants';
  36. import { isVideoPlayable, computeDisplayMode } from '../../functions';
  37. import logger from '../../logger';
  38. const JitsiTrackEvents = JitsiMeetJS.events.track;
  39. declare var interfaceConfig: Object;
  40. /**
  41. * The type of the React {@code Component} state of {@link Thumbnail}.
  42. */
  43. export type State = {|
  44. /**
  45. * The current audio level value for the Thumbnail.
  46. */
  47. audioLevel: number,
  48. /**
  49. * Indicates that the canplay event has been received.
  50. */
  51. canPlayEventReceived: boolean,
  52. /**
  53. * The current display mode of the thumbnail.
  54. */
  55. displayMode: number,
  56. /**
  57. * Indicates whether the thumbnail is hovered or not.
  58. */
  59. isHovered: boolean,
  60. /**
  61. * The current volume setting for the Thumbnail.
  62. */
  63. volume: ?number
  64. |};
  65. /**
  66. * The type of the React {@code Component} props of {@link Thumbnail}.
  67. */
  68. export type Props = {|
  69. /**
  70. * The audio track related to the participant.
  71. */
  72. _audioTrack: ?Object,
  73. /**
  74. * Disable/enable the auto hide functionality for the connection indicator.
  75. */
  76. _connectionIndicatorAutoHideEnabled: boolean,
  77. /**
  78. * Disable/enable the connection indicator.
  79. */
  80. _connectionIndicatorDisabled: boolean,
  81. /**
  82. * The current layout of the filmstrip.
  83. */
  84. _currentLayout: string,
  85. /**
  86. * The default display name for the local participant.
  87. */
  88. _defaultLocalDisplayName: string,
  89. /**
  90. * Indicates whether the local video flip feature is disabled or not.
  91. */
  92. _disableLocalVideoFlip: boolean,
  93. /**
  94. * Indicates whether the profile functionality is disabled.
  95. */
  96. _disableProfile: boolean,
  97. /**
  98. * The display mode of the thumbnail.
  99. */
  100. _displayMode: number,
  101. /**
  102. * The height of the Thumbnail.
  103. */
  104. _height: number,
  105. /**
  106. * The aspect ratio of the Thumbnail in percents.
  107. */
  108. _heightToWidthPercent: number,
  109. /**
  110. * Indicates whether the thumbnail should be hidden or not.
  111. */
  112. _isHidden: boolean,
  113. /**
  114. * Indicates whether audio only mode is enabled.
  115. */
  116. _isAudioOnly: boolean,
  117. /**
  118. * Indicates whether the participant associated with the thumbnail is displayed on the large video.
  119. */
  120. _isCurrentlyOnLargeVideo: boolean,
  121. /**
  122. * Indicates whether the participant is screen sharing.
  123. */
  124. _isScreenSharing: boolean,
  125. /**
  126. * Indicates whether the video associated with the thumbnail is playable.
  127. */
  128. _isVideoPlayable: boolean,
  129. /**
  130. * Disable/enable the dominant speaker indicator.
  131. */
  132. _isDominantSpeakerDisabled: boolean,
  133. /**
  134. * Indicates whether testing mode is enabled.
  135. */
  136. _isTestModeEnabled: boolean,
  137. /**
  138. * The size of the icon of indicators.
  139. */
  140. _indicatorIconSize: number,
  141. /**
  142. * The current local video flip setting.
  143. */
  144. _localFlipX: boolean,
  145. /**
  146. * An object with information about the participant related to the thumbnaul.
  147. */
  148. _participant: Object,
  149. /**
  150. * The number of participants in the call.
  151. */
  152. _participantCount: number,
  153. /**
  154. * Indicates whether the "start silent" mode is enabled.
  155. */
  156. _startSilent: Boolean,
  157. /**
  158. * The video track that will be displayed in the thumbnail.
  159. */
  160. _videoTrack: ?Object,
  161. /**
  162. * The width of the thumbnail.
  163. */
  164. _width: number,
  165. /**
  166. * The redux dispatch function.
  167. */
  168. dispatch: Function,
  169. /**
  170. * The ID of the participant related to the thumbnail.
  171. */
  172. participantID: ?string
  173. |};
  174. /**
  175. * Click handler for the display name container.
  176. *
  177. * @param {SyntheticEvent} event - The click event.
  178. * @returns {void}
  179. */
  180. function onClick(event) {
  181. // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
  182. // needs to be stopped.
  183. event.stopPropagation();
  184. }
  185. /**
  186. * Implements a thumbnail.
  187. *
  188. * @extends Component
  189. */
  190. class Thumbnail extends Component<Props, State> {
  191. /**
  192. * Initializes a new Thumbnail instance.
  193. *
  194. * @param {Object} props - The read-only React Component props with which
  195. * the new instance is to be initialized.
  196. */
  197. constructor(props: Props) {
  198. super(props);
  199. const state = {
  200. audioLevel: 0,
  201. canPlayEventReceived: false,
  202. isHovered: false,
  203. volume: undefined,
  204. displayMode: DISPLAY_VIDEO
  205. };
  206. this.state = {
  207. ...state,
  208. displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
  209. };
  210. this._updateAudioLevel = this._updateAudioLevel.bind(this);
  211. this._onCanPlay = this._onCanPlay.bind(this);
  212. this._onClick = this._onClick.bind(this);
  213. this._onVolumeChange = this._onVolumeChange.bind(this);
  214. this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
  215. this._onMouseEnter = this._onMouseEnter.bind(this);
  216. this._onMouseLeave = this._onMouseLeave.bind(this);
  217. this._onTestingEvent = this._onTestingEvent.bind(this);
  218. }
  219. /**
  220. * Starts listening for audio level updates after the initial render.
  221. *
  222. * @inheritdoc
  223. * @returns {void}
  224. */
  225. componentDidMount() {
  226. this._listenForAudioUpdates();
  227. this._onDisplayModeChanged();
  228. }
  229. /**
  230. * Stops listening for audio level updates on the old track and starts
  231. * listening instead on the new track.
  232. *
  233. * @inheritdoc
  234. * @returns {void}
  235. */
  236. componentDidUpdate(prevProps: Props, prevState: State) {
  237. if (prevProps._audioTrack !== this.props._audioTrack) {
  238. this._stopListeningForAudioUpdates(prevProps._audioTrack);
  239. this._listenForAudioUpdates();
  240. this._updateAudioLevel(0);
  241. }
  242. if (prevState.displayMode !== this.state.displayMode) {
  243. this._onDisplayModeChanged();
  244. }
  245. }
  246. /**
  247. * Handles display mode changes.
  248. *
  249. * @returns {void}
  250. */
  251. _onDisplayModeChanged() {
  252. const input = Thumbnail.getDisplayModeInput(this.props, this.state);
  253. const displayModeString = DISPLAY_MODE_TO_STRING[this.state.displayMode];
  254. const id = this.props._participant?.id;
  255. this._maybeSendScreenSharingIssueEvents(input);
  256. logger.debug(`Displaying ${displayModeString} for ${id}, data: [${JSON.stringify(input)}]`);
  257. }
  258. /**
  259. * Sends screen sharing issue event if an issue is detected.
  260. *
  261. * @param {Object} input - The input used to compute the thumbnail display mode.
  262. * @returns {void}
  263. */
  264. _maybeSendScreenSharingIssueEvents(input) {
  265. const {
  266. _currentLayout,
  267. _isAudioOnly,
  268. _isScreenSharing
  269. } = this.props;
  270. const { displayMode } = this.state;
  271. const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
  272. if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode)
  273. && tileViewActive
  274. && _isScreenSharing
  275. && !_isAudioOnly) {
  276. sendAnalytics(createScreenSharingIssueEvent({
  277. source: 'thumbnail',
  278. ...input
  279. }));
  280. }
  281. }
  282. /**
  283. * Implements React's {@link Component#getDerivedStateFromProps()}.
  284. *
  285. * @inheritdoc
  286. */
  287. static getDerivedStateFromProps(props: Props, prevState: State) {
  288. if (!props._videoTrack && prevState.canPlayEventReceived) {
  289. const newState = {
  290. ...prevState,
  291. canPlayEventReceived: false
  292. };
  293. return {
  294. ...newState,
  295. displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState))
  296. };
  297. }
  298. const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState));
  299. if (newDisplayMode !== prevState.displayMode) {
  300. return {
  301. ...prevState,
  302. displayMode: newDisplayMode
  303. };
  304. }
  305. return null;
  306. }
  307. /**
  308. * Extracts information for props and state needed to compute the display mode.
  309. *
  310. * @param {Props} props - The component's props.
  311. * @param {State} state - The component's state.
  312. * @returns {Object}
  313. */
  314. static getDisplayModeInput(props: Props, state: State) {
  315. const {
  316. _currentLayout,
  317. _isAudioOnly,
  318. _isCurrentlyOnLargeVideo,
  319. _isScreenSharing,
  320. _isVideoPlayable,
  321. _participant,
  322. _videoTrack
  323. } = props;
  324. const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
  325. const { canPlayEventReceived, isHovered } = state;
  326. return {
  327. isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
  328. isHovered,
  329. isAudioOnly: _isAudioOnly,
  330. tileViewActive,
  331. isVideoPlayable: _isVideoPlayable,
  332. connectionStatus: _participant?.connectionStatus,
  333. canPlayEventReceived,
  334. videoStream: Boolean(_videoTrack),
  335. isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
  336. isScreenSharing: _isScreenSharing,
  337. videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
  338. };
  339. }
  340. /**
  341. * Unsubscribe from audio level updates.
  342. *
  343. * @inheritdoc
  344. * @returns {void}
  345. */
  346. componentWillUnmount() {
  347. this._stopListeningForAudioUpdates(this.props._audioTrack);
  348. }
  349. /**
  350. * Starts listening for audio level updates from the library.
  351. *
  352. * @private
  353. * @returns {void}
  354. */
  355. _listenForAudioUpdates() {
  356. const { _audioTrack } = this.props;
  357. if (_audioTrack) {
  358. const { jitsiTrack } = _audioTrack;
  359. jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
  360. }
  361. }
  362. /**
  363. * Stops listening to further updates from the passed track.
  364. *
  365. * @param {Object} audioTrack - The track.
  366. * @private
  367. * @returns {void}
  368. */
  369. _stopListeningForAudioUpdates(audioTrack) {
  370. if (audioTrack) {
  371. const { jitsiTrack } = audioTrack;
  372. jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
  373. }
  374. }
  375. _updateAudioLevel: (number) => void;
  376. /**
  377. * Updates the internal state of the last know audio level. The level should
  378. * be between 0 and 1, as the level will be used as a percentage out of 1.
  379. *
  380. * @param {number} audioLevel - The new audio level for the track.
  381. * @private
  382. * @returns {void}
  383. */
  384. _updateAudioLevel(audioLevel) {
  385. this.setState({
  386. audioLevel
  387. });
  388. }
  389. /**
  390. * Returns an object with the styles for thumbnail.
  391. *
  392. * @returns {Object} - The styles for the thumbnail.
  393. */
  394. _getStyles(): Object {
  395. const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
  396. let styles: {
  397. thumbnail: Object,
  398. avatar: Object
  399. } = {
  400. thumbnail: {},
  401. avatar: {}
  402. };
  403. switch (_currentLayout) {
  404. case LAYOUTS.TILE_VIEW:
  405. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  406. const avatarSize = _height / 2;
  407. styles = {
  408. thumbnail: {
  409. height: `${_height}px`,
  410. minHeight: `${_height}px`,
  411. minWidth: `${_width}px`,
  412. width: `${_width}px`
  413. },
  414. avatar: {
  415. height: `${avatarSize}px`,
  416. width: `${avatarSize}px`
  417. }
  418. };
  419. break;
  420. }
  421. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
  422. styles = {
  423. thumbnail: {
  424. paddingTop: `${_heightToWidthPercent}%`
  425. },
  426. avatar: {
  427. height: '50%',
  428. width: `${_heightToWidthPercent / 2}%`
  429. }
  430. };
  431. break;
  432. }
  433. }
  434. if (_isHidden) {
  435. styles.thumbnail.display = 'none';
  436. }
  437. return styles;
  438. }
  439. _onClick: () => void;
  440. /**
  441. * On click handler.
  442. *
  443. * @returns {void}
  444. */
  445. _onClick() {
  446. const { _participant, dispatch } = this.props;
  447. const { id, pinned } = _participant;
  448. dispatch(pinParticipant(pinned ? null : id));
  449. }
  450. _onMouseEnter: () => void;
  451. /**
  452. * Mouse enter handler.
  453. *
  454. * @returns {void}
  455. */
  456. _onMouseEnter() {
  457. this.setState({ isHovered: true });
  458. }
  459. _onMouseLeave: () => void;
  460. /**
  461. * Mouse leave handler.
  462. *
  463. * @returns {void}
  464. */
  465. _onMouseLeave() {
  466. this.setState({ isHovered: false });
  467. }
  468. /**
  469. * Renders a fake participant (youtube video) thumbnail.
  470. *
  471. * @param {string} id - The id of the participant.
  472. * @returns {ReactElement}
  473. */
  474. _renderFakeParticipant() {
  475. const { _participant: { avatarURL } } = this.props;
  476. const styles = this._getStyles();
  477. const containerClassName = this._getContainerClassName();
  478. return (
  479. <span
  480. className = { containerClassName }
  481. id = 'sharedVideoContainer'
  482. onClick = { this._onClick }
  483. onMouseEnter = { this._onMouseEnter }
  484. onMouseLeave = { this._onMouseLeave }
  485. style = { styles.thumbnail }>
  486. {avatarURL ? (
  487. <img
  488. className = 'sharedVideoAvatar'
  489. src = { avatarURL } />
  490. )
  491. : this._renderAvatar(styles.avatar)}
  492. </span>
  493. );
  494. }
  495. /**
  496. * Renders the top indicators of the thumbnail.
  497. *
  498. * @returns {Component}
  499. */
  500. _renderTopIndicators() {
  501. const {
  502. _connectionIndicatorAutoHideEnabled,
  503. _connectionIndicatorDisabled,
  504. _currentLayout,
  505. _isDominantSpeakerDisabled,
  506. _indicatorIconSize: iconSize,
  507. _participant,
  508. _participantCount
  509. } = this.props;
  510. const { isHovered } = this.state;
  511. const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
  512. const { id, local = false, dominantSpeaker = false } = _participant;
  513. const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
  514. let statsPopoverPosition, tooltipPosition;
  515. switch (_currentLayout) {
  516. case LAYOUTS.TILE_VIEW:
  517. statsPopoverPosition = 'right-start';
  518. tooltipPosition = 'right';
  519. break;
  520. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  521. statsPopoverPosition = 'left-start';
  522. tooltipPosition = 'left';
  523. break;
  524. default:
  525. statsPopoverPosition = 'auto';
  526. tooltipPosition = 'top';
  527. }
  528. return (
  529. <div>
  530. { !_connectionIndicatorDisabled
  531. && <ConnectionIndicator
  532. alwaysVisible = { showConnectionIndicator }
  533. enableStatsDisplay = { true }
  534. iconSize = { iconSize }
  535. isLocalVideo = { local }
  536. participantId = { id }
  537. statsPopoverPosition = { statsPopoverPosition } />
  538. }
  539. <RaisedHandIndicator
  540. iconSize = { iconSize }
  541. participantId = { id }
  542. tooltipPosition = { tooltipPosition } />
  543. { showDominantSpeaker && _participantCount > 2
  544. && <DominantSpeakerIndicator
  545. iconSize = { iconSize }
  546. tooltipPosition = { tooltipPosition } />
  547. }
  548. </div>);
  549. }
  550. /**
  551. * Renders the avatar.
  552. *
  553. * @param {Object} styles - The styles that will be applied to the avatar.
  554. * @returns {ReactElement}
  555. */
  556. _renderAvatar(styles) {
  557. const { _participant } = this.props;
  558. const { id } = _participant;
  559. return (
  560. <div
  561. className = 'avatar-container'
  562. style = { styles }>
  563. <Avatar
  564. className = 'userAvatar'
  565. participantId = { id } />
  566. </div>
  567. );
  568. }
  569. /**
  570. * Returns the container class name.
  571. *
  572. * @returns {string} - The class name that will be used for the container.
  573. */
  574. _getContainerClassName() {
  575. let className = 'videocontainer';
  576. const { displayMode } = this.state;
  577. const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
  578. const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
  579. className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
  580. if (_participant?.pinned) {
  581. className += ' videoContainerFocused';
  582. }
  583. if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
  584. className += ' active-speaker';
  585. }
  586. if (_isHidden) {
  587. className += ' hidden';
  588. }
  589. if (isRemoteParticipant && _isAudioOnly) {
  590. className += ' audio-only';
  591. }
  592. return className;
  593. }
  594. /**
  595. * Renders the local participant's thumbnail.
  596. *
  597. * @returns {ReactElement}
  598. */
  599. _renderLocalParticipant() {
  600. const {
  601. _defaultLocalDisplayName,
  602. _disableLocalVideoFlip,
  603. _isScreenSharing,
  604. _localFlipX,
  605. _disableProfile,
  606. _participant,
  607. _videoTrack
  608. } = this.props;
  609. const { id } = _participant || {};
  610. const { audioLevel } = this.state;
  611. const styles = this._getStyles();
  612. const containerClassName = this._getContainerClassName();
  613. const videoTrackClassName
  614. = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
  615. return (
  616. <span
  617. className = { containerClassName }
  618. id = 'localVideoContainer'
  619. onClick = { this._onClick }
  620. onMouseEnter = { this._onMouseEnter }
  621. onMouseLeave = { this._onMouseLeave }
  622. style = { styles.thumbnail }>
  623. <div className = 'videocontainer__background' />
  624. <span id = 'localVideoWrapper'>
  625. <VideoTrack
  626. className = { videoTrackClassName }
  627. id = 'localVideo_container'
  628. videoTrack = { _videoTrack } />
  629. </span>
  630. <div className = 'videocontainer__toolbar'>
  631. <StatusIndicators participantID = { id } />
  632. </div>
  633. <div className = 'videocontainer__toptoolbar'>
  634. { this._renderTopIndicators() }
  635. </div>
  636. <div className = 'videocontainer__hoverOverlay' />
  637. <div
  638. className = 'displayNameContainer'
  639. onClick = { onClick }>
  640. <DisplayName
  641. allowEditing = { !_disableProfile }
  642. displayNameSuffix = { _defaultLocalDisplayName }
  643. elementID = 'localDisplayName'
  644. participantID = { id } />
  645. </div>
  646. { this._renderAvatar(styles.avatar) }
  647. <span className = 'localvideomenu'>
  648. <LocalVideoMenuTriggerButton />
  649. </span>
  650. <span className = 'audioindicator-container'>
  651. <AudioLevelIndicator audioLevel = { audioLevel } />
  652. </span>
  653. </span>
  654. );
  655. }
  656. _onCanPlay: Object => void;
  657. /**
  658. * Canplay event listener.
  659. *
  660. * @param {SyntheticEvent} event - The event.
  661. * @returns {void}
  662. */
  663. _onCanPlay(event) {
  664. this.setState({ canPlayEventReceived: true });
  665. const {
  666. _isTestModeEnabled,
  667. _videoTrack
  668. } = this.props;
  669. if (_videoTrack && _isTestModeEnabled) {
  670. this._onTestingEvent(event);
  671. }
  672. }
  673. _onTestingEvent: Object => void;
  674. /**
  675. * Event handler for testing events.
  676. *
  677. * @param {SyntheticEvent} event - The event.
  678. * @returns {void}
  679. */
  680. _onTestingEvent(event) {
  681. const {
  682. _videoTrack,
  683. dispatch
  684. } = this.props;
  685. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  686. dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
  687. }
  688. /**
  689. * Renders a remote participant's 'thumbnail.
  690. *
  691. * @returns {ReactElement}
  692. */
  693. _renderRemoteParticipant() {
  694. const {
  695. _audioTrack,
  696. _isTestModeEnabled,
  697. _participant,
  698. _startSilent,
  699. _videoTrack
  700. } = this.props;
  701. const { id } = _participant;
  702. const { audioLevel, canPlayEventReceived, volume } = this.state;
  703. const styles = this._getStyles();
  704. const containerClassName = this._getContainerClassName();
  705. // hide volume when in silent mode
  706. const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
  707. const jitsiAudioTrack = _audioTrack?.jitsiTrack;
  708. const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
  709. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  710. const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
  711. const videoEventListeners = {};
  712. if (_videoTrack && _isTestModeEnabled) {
  713. VIDEO_TEST_EVENTS.forEach(attribute => {
  714. videoEventListeners[attribute] = this._onTestingEvent;
  715. });
  716. }
  717. videoEventListeners.onCanPlay = this._onCanPlay;
  718. const videoElementStyle = canPlayEventReceived ? null : {
  719. display: 'none'
  720. };
  721. return (
  722. <span
  723. className = { containerClassName }
  724. id = { `participant_${id}` }
  725. onClick = { this._onClick }
  726. onMouseEnter = { this._onMouseEnter }
  727. onMouseLeave = { this._onMouseLeave }
  728. style = { styles.thumbnail }>
  729. {
  730. _videoTrack && <VideoTrack
  731. eventHandlers = { videoEventListeners }
  732. id = { `remoteVideo_${videoTrackId || ''}` }
  733. muted = { true }
  734. style = { videoElementStyle }
  735. videoTrack = { _videoTrack } />
  736. }
  737. {
  738. _audioTrack && <AudioTrack
  739. audioTrack = { _audioTrack }
  740. id = { `remoteAudio_${audioTrackId || ''}` }
  741. muted = { _startSilent }
  742. onInitialVolumeSet = { this._onInitialVolumeSet }
  743. volume = { volume } />
  744. }
  745. <div className = 'videocontainer__background' />
  746. <div className = 'videocontainer__toptoolbar'>
  747. { this._renderTopIndicators() }
  748. </div>
  749. <div className = 'videocontainer__toolbar'>
  750. <StatusIndicators participantID = { id } />
  751. </div>
  752. <div className = 'videocontainer__hoverOverlay' />
  753. <div className = 'displayNameContainer'>
  754. <DisplayName
  755. elementID = { `participant_${id}_name` }
  756. participantID = { id } />
  757. </div>
  758. { this._renderAvatar(styles.avatar) }
  759. <div className = 'presence-label-container'>
  760. <PresenceLabel
  761. className = 'presence-label'
  762. participantID = { id } />
  763. </div>
  764. <span className = 'remotevideomenu'>
  765. <RemoteVideoMenuTriggerButton
  766. initialVolumeValue = { volume }
  767. onVolumeChange = { onVolumeChange }
  768. participantID = { id } />
  769. </span>
  770. <span className = 'audioindicator-container'>
  771. <AudioLevelIndicator audioLevel = { audioLevel } />
  772. </span>
  773. </span>
  774. );
  775. }
  776. _onInitialVolumeSet: Object => void;
  777. /**
  778. * A handler for the initial volume value of the audio element.
  779. *
  780. * @param {number} volume - Properties of the audio element.
  781. * @returns {void}
  782. */
  783. _onInitialVolumeSet(volume) {
  784. if (this.state.volume !== volume) {
  785. this.setState({ volume });
  786. }
  787. }
  788. _onVolumeChange: number => void;
  789. /**
  790. * Handles volume changes.
  791. *
  792. * @param {number} value - The new value for the volume.
  793. * @returns {void}
  794. */
  795. _onVolumeChange(value) {
  796. this.setState({ volume: value });
  797. }
  798. /**
  799. * Implements React's {@link Component#render()}.
  800. *
  801. * @inheritdoc
  802. * @returns {ReactElement}
  803. */
  804. render() {
  805. const { _participant } = this.props;
  806. if (!_participant) {
  807. return null;
  808. }
  809. const { isFakeParticipant, local } = _participant;
  810. if (local) {
  811. return this._renderLocalParticipant();
  812. }
  813. if (isFakeParticipant) {
  814. return this._renderFakeParticipant();
  815. }
  816. return this._renderRemoteParticipant();
  817. }
  818. }
  819. /**
  820. * Maps (parts of) the redux state to the associated props for this component.
  821. *
  822. * @param {Object} state - The Redux state.
  823. * @param {Object} ownProps - The own props of the component.
  824. * @private
  825. * @returns {Props}
  826. */
  827. function _mapStateToProps(state, ownProps): Object {
  828. const { participantID } = ownProps;
  829. // Only the local participant won't have id for the time when the conference is not yet joined.
  830. const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
  831. const { id } = participant;
  832. const isLocal = participant?.local ?? true;
  833. const tracks = state['features/base/tracks'];
  834. const _videoTrack = isLocal
  835. ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
  836. const _audioTrack = isLocal
  837. ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
  838. const _currentLayout = getCurrentLayout(state);
  839. let size = {};
  840. const {
  841. startSilent,
  842. disableLocalVideoFlip,
  843. disableProfile,
  844. iAmRecorder,
  845. iAmSipGateway
  846. } = state['features/base/config'];
  847. const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
  848. const { localFlipX } = state['features/base/settings'];
  849. switch (_currentLayout) {
  850. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  851. const {
  852. horizontalViewDimensions = {
  853. local: {},
  854. remote: {}
  855. }
  856. } = state['features/filmstrip'];
  857. const { local, remote } = horizontalViewDimensions;
  858. const { width, height } = isLocal ? local : remote;
  859. size = {
  860. _width: width,
  861. _height: height
  862. };
  863. break;
  864. }
  865. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  866. size = {
  867. _heightToWidthPercent: isLocal
  868. ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
  869. : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
  870. };
  871. break;
  872. case LAYOUTS.TILE_VIEW: {
  873. const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
  874. size = {
  875. _width: width,
  876. _height: height
  877. };
  878. break;
  879. }
  880. }
  881. return {
  882. _audioTrack,
  883. _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  884. _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  885. _currentLayout,
  886. _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
  887. _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
  888. _disableProfile: disableProfile,
  889. _isHidden: isLocal && iAmRecorder && !iAmSipGateway,
  890. _isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
  891. _isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
  892. _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
  893. _isScreenSharing: _videoTrack?.videoType === 'desktop',
  894. _isTestModeEnabled: isTestModeEnabled(state),
  895. _isVideoPlayable: isVideoPlayable(state, id),
  896. _indicatorIconSize: NORMAL,
  897. _localFlipX: Boolean(localFlipX),
  898. _participant: participant,
  899. _participantCount: getParticipantCount(state),
  900. _startSilent: Boolean(startSilent),
  901. _videoTrack,
  902. ...size
  903. };
  904. }
  905. export default connect(_mapStateToProps)(Thumbnail);