Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

Thumbnail.js 31KB

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