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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  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 } = this.props;
  476. const { id } = _participant;
  477. const styles = this._getStyles();
  478. const containerClassName = this._getContainerClassName();
  479. return (
  480. <span
  481. className = { containerClassName }
  482. id = 'sharedVideoContainer'
  483. onClick = { this._onClick }
  484. onMouseEnter = { this._onMouseEnter }
  485. onMouseLeave = { this._onMouseLeave }
  486. style = { styles.thumbnail }>
  487. <img
  488. className = 'sharedVideoAvatar'
  489. src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
  490. <div className = 'displayNameContainer'>
  491. <DisplayName
  492. elementID = 'sharedVideoContainer_name'
  493. participantID = { id } />
  494. </div>
  495. </span>
  496. );
  497. }
  498. /**
  499. * Renders the top indicators of the thumbnail.
  500. *
  501. * @returns {Component}
  502. */
  503. _renderTopIndicators() {
  504. const {
  505. _connectionIndicatorAutoHideEnabled,
  506. _connectionIndicatorDisabled,
  507. _currentLayout,
  508. _isDominantSpeakerDisabled,
  509. _indicatorIconSize: iconSize,
  510. _participant,
  511. _participantCount
  512. } = this.props;
  513. const { isHovered } = this.state;
  514. const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
  515. const { id, local = false, dominantSpeaker = false } = _participant;
  516. const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
  517. let statsPopoverPosition, tooltipPosition;
  518. switch (_currentLayout) {
  519. case LAYOUTS.TILE_VIEW:
  520. statsPopoverPosition = 'right-start';
  521. tooltipPosition = 'right';
  522. break;
  523. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  524. statsPopoverPosition = 'left-start';
  525. tooltipPosition = 'left';
  526. break;
  527. default:
  528. statsPopoverPosition = 'auto';
  529. tooltipPosition = 'top';
  530. }
  531. return (
  532. <div>
  533. { !_connectionIndicatorDisabled
  534. && <ConnectionIndicator
  535. alwaysVisible = { showConnectionIndicator }
  536. enableStatsDisplay = { true }
  537. iconSize = { iconSize }
  538. isLocalVideo = { local }
  539. participantId = { id }
  540. statsPopoverPosition = { statsPopoverPosition } />
  541. }
  542. <RaisedHandIndicator
  543. iconSize = { iconSize }
  544. participantId = { id }
  545. tooltipPosition = { tooltipPosition } />
  546. { showDominantSpeaker && _participantCount > 2
  547. && <DominantSpeakerIndicator
  548. iconSize = { iconSize }
  549. tooltipPosition = { tooltipPosition } />
  550. }
  551. </div>);
  552. }
  553. /**
  554. * Renders the avatar.
  555. *
  556. * @param {Object} styles - The styles that will be applied to the avatar.
  557. * @returns {ReactElement}
  558. */
  559. _renderAvatar(styles) {
  560. const { _participant } = this.props;
  561. const { id } = _participant;
  562. return (
  563. <div
  564. className = 'avatar-container'
  565. style = { styles }>
  566. <Avatar
  567. className = 'userAvatar'
  568. participantId = { id } />
  569. </div>
  570. );
  571. }
  572. /**
  573. * Returns the container class name.
  574. *
  575. * @returns {string} - The class name that will be used for the container.
  576. */
  577. _getContainerClassName() {
  578. let className = 'videocontainer';
  579. const { displayMode } = this.state;
  580. const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
  581. const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
  582. className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
  583. if (_participant?.pinned) {
  584. className += ' videoContainerFocused';
  585. }
  586. if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
  587. className += ' active-speaker';
  588. }
  589. if (_isHidden) {
  590. className += ' hidden';
  591. }
  592. if (isRemoteParticipant && _isAudioOnly) {
  593. className += ' audio-only';
  594. }
  595. return className;
  596. }
  597. /**
  598. * Renders the local participant's thumbnail.
  599. *
  600. * @returns {ReactElement}
  601. */
  602. _renderLocalParticipant() {
  603. const {
  604. _defaultLocalDisplayName,
  605. _disableLocalVideoFlip,
  606. _isScreenSharing,
  607. _localFlipX,
  608. _disableProfile,
  609. _participant,
  610. _videoTrack
  611. } = this.props;
  612. const { id } = _participant || {};
  613. const { audioLevel } = this.state;
  614. const styles = this._getStyles();
  615. const containerClassName = this._getContainerClassName();
  616. const videoTrackClassName
  617. = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
  618. return (
  619. <span
  620. className = { containerClassName }
  621. id = 'localVideoContainer'
  622. onClick = { this._onClick }
  623. onMouseEnter = { this._onMouseEnter }
  624. onMouseLeave = { this._onMouseLeave }
  625. style = { styles.thumbnail }>
  626. <div className = 'videocontainer__background' />
  627. <span id = 'localVideoWrapper'>
  628. <VideoTrack
  629. className = { videoTrackClassName }
  630. id = 'localVideo_container'
  631. videoTrack = { _videoTrack } />
  632. </span>
  633. <div className = 'videocontainer__toolbar'>
  634. <StatusIndicators participantID = { id } />
  635. </div>
  636. <div className = 'videocontainer__toptoolbar'>
  637. { this._renderTopIndicators() }
  638. </div>
  639. <div className = 'videocontainer__hoverOverlay' />
  640. <div
  641. className = 'displayNameContainer'
  642. onClick = { onClick }>
  643. <DisplayName
  644. allowEditing = { !_disableProfile }
  645. displayNameSuffix = { _defaultLocalDisplayName }
  646. elementID = 'localDisplayName'
  647. participantID = { id } />
  648. </div>
  649. { this._renderAvatar(styles.avatar) }
  650. <span className = 'localvideomenu'>
  651. <LocalVideoMenuTriggerButton />
  652. </span>
  653. <span className = 'audioindicator-container'>
  654. <AudioLevelIndicator audioLevel = { audioLevel } />
  655. </span>
  656. </span>
  657. );
  658. }
  659. _onCanPlay: Object => void;
  660. /**
  661. * Canplay event listener.
  662. *
  663. * @param {SyntheticEvent} event - The event.
  664. * @returns {void}
  665. */
  666. _onCanPlay(event) {
  667. this.setState({ canPlayEventReceived: true });
  668. const {
  669. _isTestModeEnabled,
  670. _videoTrack
  671. } = this.props;
  672. if (_videoTrack && _isTestModeEnabled) {
  673. this._onTestingEvent(event);
  674. }
  675. }
  676. _onTestingEvent: Object => void;
  677. /**
  678. * Event handler for testing events.
  679. *
  680. * @param {SyntheticEvent} event - The event.
  681. * @returns {void}
  682. */
  683. _onTestingEvent(event) {
  684. const {
  685. _videoTrack,
  686. dispatch
  687. } = this.props;
  688. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  689. dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
  690. }
  691. /**
  692. * Renders a remote participant's 'thumbnail.
  693. *
  694. * @returns {ReactElement}
  695. */
  696. _renderRemoteParticipant() {
  697. const {
  698. _audioTrack,
  699. _isTestModeEnabled,
  700. _participant,
  701. _startSilent,
  702. _videoTrack
  703. } = this.props;
  704. const { id } = _participant;
  705. const { audioLevel, canPlayEventReceived, volume } = this.state;
  706. const styles = this._getStyles();
  707. const containerClassName = this._getContainerClassName();
  708. // hide volume when in silent mode
  709. const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
  710. const jitsiAudioTrack = _audioTrack?.jitsiTrack;
  711. const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
  712. const jitsiVideoTrack = _videoTrack?.jitsiTrack;
  713. const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
  714. const videoEventListeners = {};
  715. if (_videoTrack && _isTestModeEnabled) {
  716. VIDEO_TEST_EVENTS.forEach(attribute => {
  717. videoEventListeners[attribute] = this._onTestingEvent;
  718. });
  719. }
  720. videoEventListeners.onCanPlay = this._onCanPlay;
  721. const videoElementStyle = canPlayEventReceived ? null : {
  722. display: 'none'
  723. };
  724. return (
  725. <span
  726. className = { containerClassName }
  727. id = { `participant_${id}` }
  728. onClick = { this._onClick }
  729. onMouseEnter = { this._onMouseEnter }
  730. onMouseLeave = { this._onMouseLeave }
  731. style = { styles.thumbnail }>
  732. {
  733. _videoTrack && <VideoTrack
  734. eventHandlers = { videoEventListeners }
  735. id = { `remoteVideo_${videoTrackId || ''}` }
  736. muted = { true }
  737. style = { videoElementStyle }
  738. videoTrack = { _videoTrack } />
  739. }
  740. {
  741. _audioTrack && <AudioTrack
  742. audioTrack = { _audioTrack }
  743. id = { `remoteAudio_${audioTrackId || ''}` }
  744. muted = { _startSilent }
  745. onInitialVolumeSet = { this._onInitialVolumeSet }
  746. volume = { volume } />
  747. }
  748. <div className = 'videocontainer__background' />
  749. <div className = 'videocontainer__toptoolbar'>
  750. { this._renderTopIndicators() }
  751. </div>
  752. <div className = 'videocontainer__toolbar'>
  753. <StatusIndicators participantID = { id } />
  754. </div>
  755. <div className = 'videocontainer__hoverOverlay' />
  756. <div className = 'displayNameContainer'>
  757. <DisplayName
  758. elementID = { `participant_${id}_name` }
  759. participantID = { id } />
  760. </div>
  761. { this._renderAvatar(styles.avatar) }
  762. <div className = 'presence-label-container'>
  763. <PresenceLabel
  764. className = 'presence-label'
  765. participantID = { id } />
  766. </div>
  767. <span className = 'remotevideomenu'>
  768. <RemoteVideoMenuTriggerButton
  769. initialVolumeValue = { volume }
  770. onVolumeChange = { onVolumeChange }
  771. participantID = { id } />
  772. </span>
  773. <span className = 'audioindicator-container'>
  774. <AudioLevelIndicator audioLevel = { audioLevel } />
  775. </span>
  776. </span>
  777. );
  778. }
  779. _onInitialVolumeSet: Object => void;
  780. /**
  781. * A handler for the initial volume value of the audio element.
  782. *
  783. * @param {number} volume - Properties of the audio element.
  784. * @returns {void}
  785. */
  786. _onInitialVolumeSet(volume) {
  787. if (this.state.volume !== volume) {
  788. this.setState({ volume });
  789. }
  790. }
  791. _onVolumeChange: number => void;
  792. /**
  793. * Handles volume changes.
  794. *
  795. * @param {number} value - The new value for the volume.
  796. * @returns {void}
  797. */
  798. _onVolumeChange(value) {
  799. this.setState({ volume: value });
  800. }
  801. /**
  802. * Implements React's {@link Component#render()}.
  803. *
  804. * @inheritdoc
  805. * @returns {ReactElement}
  806. */
  807. render() {
  808. const { _participant } = this.props;
  809. if (!_participant) {
  810. return null;
  811. }
  812. const { isFakeParticipant, local } = _participant;
  813. if (local) {
  814. return this._renderLocalParticipant();
  815. }
  816. if (isFakeParticipant) {
  817. return this._renderFakeParticipant();
  818. }
  819. return this._renderRemoteParticipant();
  820. }
  821. }
  822. /**
  823. * Maps (parts of) the redux state to the associated props for this component.
  824. *
  825. * @param {Object} state - The Redux state.
  826. * @param {Object} ownProps - The own props of the component.
  827. * @private
  828. * @returns {Props}
  829. */
  830. function _mapStateToProps(state, ownProps): Object {
  831. const { participantID } = ownProps;
  832. // Only the local participant won't have id for the time when the conference is not yet joined.
  833. const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
  834. const { id } = participant;
  835. const isLocal = participant?.local ?? true;
  836. const tracks = state['features/base/tracks'];
  837. const _videoTrack = isLocal
  838. ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
  839. const _audioTrack = isLocal
  840. ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
  841. const _currentLayout = getCurrentLayout(state);
  842. let size = {};
  843. const {
  844. startSilent,
  845. disableLocalVideoFlip,
  846. disableProfile,
  847. iAmRecorder,
  848. iAmSipGateway
  849. } = state['features/base/config'];
  850. const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
  851. const { localFlipX } = state['features/base/settings'];
  852. switch (_currentLayout) {
  853. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  854. const {
  855. horizontalViewDimensions = {
  856. local: {},
  857. remote: {}
  858. }
  859. } = state['features/filmstrip'];
  860. const { local, remote } = horizontalViewDimensions;
  861. const { width, height } = isLocal ? local : remote;
  862. size = {
  863. _width: width,
  864. _height: height
  865. };
  866. break;
  867. }
  868. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  869. size = {
  870. _heightToWidthPercent: isLocal
  871. ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
  872. : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
  873. };
  874. break;
  875. case LAYOUTS.TILE_VIEW: {
  876. const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
  877. size = {
  878. _width: width,
  879. _height: height
  880. };
  881. break;
  882. }
  883. }
  884. return {
  885. _audioTrack,
  886. _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  887. _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  888. _currentLayout,
  889. _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
  890. _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
  891. _disableProfile: disableProfile,
  892. _isHidden: isLocal && iAmRecorder && !iAmSipGateway,
  893. _isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
  894. _isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
  895. _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
  896. _isScreenSharing: _videoTrack?.videoType === 'desktop',
  897. _isTestModeEnabled: isTestModeEnabled(state),
  898. _isVideoPlayable: isVideoPlayable(state, id),
  899. _indicatorIconSize: NORMAL,
  900. _localFlipX: Boolean(localFlipX),
  901. _participant: participant,
  902. _participantCount: getParticipantCount(state),
  903. _startSilent: Boolean(startSilent),
  904. _videoTrack,
  905. ...size
  906. };
  907. }
  908. export default connect(_mapStateToProps)(Thumbnail);