Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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