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 32KB

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