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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. // @flow
  2. import { AtlasKitThemeProvider } from '@atlaskit/theme';
  3. import React, { Component } from 'react';
  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. } from '../../../base/participants';
  14. import { connect } from '../../../base/redux';
  15. import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
  16. import { ConnectionIndicator } from '../../../connection-indicator';
  17. import { DisplayName } from '../../../display-name';
  18. import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
  19. import { PresenceLabel } from '../../../presence-status';
  20. import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
  21. import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
  22. const JitsiTrackEvents = JitsiMeetJS.events.track;
  23. declare var interfaceConfig: Object;
  24. /**
  25. * The type of the React {@code Component} state of {@link Thumbnail}.
  26. */
  27. type State = {
  28. /**
  29. * The current audio level value for the Thumbnail.
  30. */
  31. audioLevel: number,
  32. /**
  33. * The current volume setting for the Thumbnail.
  34. */
  35. volume: ?number
  36. };
  37. /**
  38. * The type of the React {@code Component} props of {@link Thumbnail}.
  39. */
  40. type Props = {
  41. /**
  42. * The audio track related to the participant.
  43. */
  44. _audioTrack: ?Object,
  45. /**
  46. * Disable/enable the auto hide functionality for the connection indicator.
  47. */
  48. _connectionIndicatorAutoHideEnabled: boolean,
  49. /**
  50. * Disable/enable the connection indicator.
  51. */
  52. _connectionIndicatorDisabled: boolean,
  53. /**
  54. * The current layout of the filmstrip.
  55. */
  56. _currentLayout: string,
  57. /**
  58. * The default display name for the local participant.
  59. */
  60. _defaultLocalDisplayName: string,
  61. /**
  62. * Indicates whether the profile functionality is disabled.
  63. */
  64. _disableProfile: boolean,
  65. /**
  66. * The height of the Thumbnail.
  67. */
  68. _height: number,
  69. /**
  70. * The aspect ratio of the Thumbnail in percents.
  71. */
  72. _heightToWidthPercent: number,
  73. /**
  74. * Disable/enable the dominant speaker indicator.
  75. */
  76. _isDominantSpeakerDisabled: boolean,
  77. /**
  78. * The size of the icon of indicators.
  79. */
  80. _indicatorIconSize: number,
  81. /**
  82. * An object with information about the participant related to the thumbnaul.
  83. */
  84. _participant: Object,
  85. /**
  86. * The number of participants in the call.
  87. */
  88. _participantCount: number,
  89. /**
  90. * Indicates whether the "start silent" mode is enabled.
  91. */
  92. _startSilent: Boolean,
  93. /**
  94. * The video track that will be displayed in the thumbnail.
  95. */
  96. _videoTrack: ?Object,
  97. /**
  98. * The width of the thumbnail.
  99. */
  100. _width: number,
  101. /**
  102. * The redux dispatch function.
  103. */
  104. dispatch: Function,
  105. /**
  106. * Indicates whether the thumbnail is hovered or not.
  107. */
  108. isHovered: ?boolean,
  109. /**
  110. * The ID of the participant related to the thumbnail.
  111. */
  112. participantID: ?string
  113. };
  114. /**
  115. * Implements a thumbnail.
  116. *
  117. * @extends Component
  118. */
  119. class Thumbnail extends Component<Props, State> {
  120. /**
  121. * Initializes a new Thumbnail instance.
  122. *
  123. * @param {Object} props - The read-only React Component props with which
  124. * the new instance is to be initialized.
  125. */
  126. constructor(props: Props) {
  127. super(props);
  128. this.state = {
  129. audioLevel: 0,
  130. volume: undefined
  131. };
  132. this._updateAudioLevel = this._updateAudioLevel.bind(this);
  133. this._onVolumeChange = this._onVolumeChange.bind(this);
  134. this._onAudioElementReferenceChanged = this._onAudioElementReferenceChanged.bind(this);
  135. }
  136. /**
  137. * Starts listening for audio level updates after the initial render.
  138. *
  139. * @inheritdoc
  140. * @returns {void}
  141. */
  142. componentDidMount() {
  143. this._listenForAudioUpdates();
  144. }
  145. /**
  146. * Stops listening for audio level updates on the old track and starts
  147. * listening instead on the new track.
  148. *
  149. * @inheritdoc
  150. * @returns {void}
  151. */
  152. componentDidUpdate(prevProps: Props) {
  153. if (prevProps._audioTrack !== this.props._audioTrack) {
  154. this._stopListeningForAudioUpdates(prevProps._audioTrack);
  155. this._listenForAudioUpdates();
  156. this._updateAudioLevel(0);
  157. }
  158. }
  159. /**
  160. * Unsubscribe from audio level updates.
  161. *
  162. * @inheritdoc
  163. * @returns {void}
  164. */
  165. componentWillUnmount() {
  166. this._stopListeningForAudioUpdates(this.props._audioTrack);
  167. }
  168. /**
  169. * Starts listening for audio level updates from the library.
  170. *
  171. * @private
  172. * @returns {void}
  173. */
  174. _listenForAudioUpdates() {
  175. const { _audioTrack } = this.props;
  176. if (_audioTrack) {
  177. const { jitsiTrack } = _audioTrack;
  178. jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
  179. }
  180. }
  181. /**
  182. * Stops listening to further updates from the passed track.
  183. *
  184. * @param {Object} audioTrack - The track.
  185. * @private
  186. * @returns {void}
  187. */
  188. _stopListeningForAudioUpdates(audioTrack) {
  189. if (audioTrack) {
  190. const { jitsiTrack } = audioTrack;
  191. jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
  192. }
  193. }
  194. _updateAudioLevel: (number) => void;
  195. /**
  196. * Updates the internal state of the last know audio level. The level should
  197. * be between 0 and 1, as the level will be used as a percentage out of 1.
  198. *
  199. * @param {number} audioLevel - The new audio level for the track.
  200. * @private
  201. * @returns {void}
  202. */
  203. _updateAudioLevel(audioLevel) {
  204. this.setState({
  205. audioLevel
  206. });
  207. }
  208. /**
  209. * Returns an object with the styles for thumbnail.
  210. *
  211. * @returns {Object} - The styles for the thumbnail.
  212. */
  213. _getStyles(): Object {
  214. const { _height, _heightToWidthPercent, _currentLayout } = this.props;
  215. let styles;
  216. switch (_currentLayout) {
  217. case LAYOUTS.TILE_VIEW:
  218. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  219. const avatarSize = _height / 2;
  220. styles = {
  221. avatarContainer: {
  222. height: `${avatarSize}px`,
  223. width: `${avatarSize}px`
  224. }
  225. };
  226. break;
  227. }
  228. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
  229. styles = {
  230. avatarContainer: {
  231. height: '50%',
  232. width: `${_heightToWidthPercent / 2}%`
  233. }
  234. };
  235. break;
  236. }
  237. }
  238. return styles;
  239. }
  240. /**
  241. * Renders a fake participant (youtube video) thumbnail.
  242. *
  243. * @param {string} id - The id of the participant.
  244. * @returns {ReactElement}
  245. */
  246. _renderFakeParticipant() {
  247. const { _participant } = this.props;
  248. const { id } = _participant;
  249. return (
  250. <>
  251. <img
  252. className = 'sharedVideoAvatar'
  253. src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
  254. <div className = 'displayNameContainer'>
  255. <DisplayName
  256. elementID = 'sharedVideoContainer_name'
  257. participantID = { id } />
  258. </div>
  259. </>
  260. );
  261. }
  262. /**
  263. * Renders the top toolbar of the thumbnail.
  264. *
  265. * @returns {Component}
  266. */
  267. _renderTopToolbar() {
  268. const {
  269. _connectionIndicatorAutoHideEnabled,
  270. _connectionIndicatorDisabled,
  271. _currentLayout,
  272. _isDominantSpeakerDisabled,
  273. _indicatorIconSize: iconSize,
  274. _participant,
  275. _participantCount,
  276. isHovered
  277. } = this.props;
  278. const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
  279. const { id, local = false, dominantSpeaker = false } = _participant;
  280. const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
  281. let statsPopoverPosition, tooltipPosition;
  282. switch (_currentLayout) {
  283. case LAYOUTS.TILE_VIEW:
  284. statsPopoverPosition = 'right top';
  285. tooltipPosition = 'right';
  286. break;
  287. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  288. statsPopoverPosition = 'left top';
  289. tooltipPosition = 'left';
  290. break;
  291. default:
  292. statsPopoverPosition = 'top center';
  293. tooltipPosition = 'top';
  294. }
  295. return (
  296. <div>
  297. <AtlasKitThemeProvider mode = 'dark'>
  298. { _connectionIndicatorDisabled
  299. ? null
  300. : <ConnectionIndicator
  301. alwaysVisible = { showConnectionIndicator }
  302. enableStatsDisplay = { true }
  303. iconSize = { iconSize }
  304. isLocalVideo = { local }
  305. participantId = { id }
  306. statsPopoverPosition = { statsPopoverPosition } />
  307. }
  308. <RaisedHandIndicator
  309. iconSize = { iconSize }
  310. participantId = { id }
  311. tooltipPosition = { tooltipPosition } />
  312. { showDominantSpeaker && _participantCount > 2
  313. ? <DominantSpeakerIndicator
  314. iconSize = { iconSize }
  315. tooltipPosition = { tooltipPosition } />
  316. : null }
  317. </AtlasKitThemeProvider>
  318. </div>);
  319. }
  320. /**
  321. * Renders the avatar.
  322. *
  323. * @returns {ReactElement}
  324. */
  325. _renderAvatar() {
  326. const { _participant } = this.props;
  327. const { id } = _participant;
  328. const styles = this._getStyles();
  329. return (
  330. <div
  331. className = 'avatar-container'
  332. style = { styles.avatarContainer }>
  333. <Avatar
  334. className = 'userAvatar'
  335. participantId = { id } />
  336. </div>
  337. );
  338. }
  339. /**
  340. * Renders the local participant's thumbnail.
  341. *
  342. * @returns {ReactElement}
  343. */
  344. _renderLocalParticipant() {
  345. const {
  346. _defaultLocalDisplayName,
  347. _disableProfile,
  348. _participant,
  349. _videoTrack
  350. } = this.props;
  351. const { id } = _participant || {};
  352. const { audioLevel = 0 } = this.state;
  353. return (
  354. <>
  355. <div className = 'videocontainer__background' />
  356. <span id = 'localVideoWrapper'>
  357. <VideoTrack
  358. id = 'localVideo_container'
  359. videoTrack = { _videoTrack } />
  360. </span>
  361. <div className = 'videocontainer__toolbar'>
  362. <StatusIndicators participantID = { id } />
  363. </div>
  364. <div className = 'videocontainer__toptoolbar'>
  365. { this._renderTopToolbar() }
  366. </div>
  367. <div className = 'videocontainer__hoverOverlay' />
  368. <div className = 'displayNameContainer'>
  369. <DisplayName
  370. allowEditing = { !_disableProfile }
  371. displayNameSuffix = { _defaultLocalDisplayName }
  372. elementID = 'localDisplayName'
  373. participantID = { id } />
  374. </div>
  375. { this._renderAvatar() }
  376. <span className = 'audioindicator-container'>
  377. <AudioLevelIndicator audioLevel = { audioLevel } />
  378. </span>
  379. </>
  380. );
  381. }
  382. /**
  383. * Renders a remote participant's 'thumbnail.
  384. *
  385. * @returns {ReactElement}
  386. */
  387. _renderRemoteParticipant() {
  388. const {
  389. _audioTrack,
  390. _participant,
  391. _startSilent
  392. } = this.props;
  393. const { id } = _participant;
  394. const { audioLevel = 0, volume = 1 } = this.state;
  395. // hide volume when in silent mode
  396. const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
  397. const { jitsiTrack } = _audioTrack ?? {};
  398. const audioTrackId = jitsiTrack && jitsiTrack.getId();
  399. return (
  400. <>
  401. {
  402. _audioTrack
  403. ? <AudioTrack
  404. audioTrack = { _audioTrack }
  405. id = { `remoteAudio_${audioTrackId}` }
  406. muted = { _startSilent }
  407. onAudioElementReferenceChanged = { this._onAudioElementReferenceChanged }
  408. volume = { this.state.volume } />
  409. : null
  410. }
  411. <div className = 'videocontainer__background' />
  412. <div className = 'videocontainer__toptoolbar'>
  413. { this._renderTopToolbar() }
  414. </div>
  415. <div className = 'videocontainer__toolbar'>
  416. <StatusIndicators participantID = { id } />
  417. </div>
  418. <div className = 'videocontainer__hoverOverlay' />
  419. <div className = 'displayNameContainer'>
  420. <DisplayName
  421. elementID = { `participant_${id}_name` }
  422. participantID = { id } />
  423. </div>
  424. { this._renderAvatar() }
  425. <div className = 'presence-label-container'>
  426. <PresenceLabel
  427. className = 'presence-label'
  428. participantID = { id } />
  429. </div>
  430. <span className = 'remotevideomenu'>
  431. <AtlasKitThemeProvider mode = 'dark'>
  432. <RemoteVideoMenuTriggerButton
  433. initialVolumeValue = { volume }
  434. onVolumeChange = { onVolumeChange }
  435. participantID = { id } />
  436. </AtlasKitThemeProvider>
  437. </span>
  438. <span className = 'audioindicator-container'>
  439. <AudioLevelIndicator audioLevel = { audioLevel } />
  440. </span>
  441. </>
  442. );
  443. }
  444. _onAudioElementReferenceChanged: Object => void;
  445. /**
  446. * Handles audio element references changes by receiving some properties from the audio element.
  447. *
  448. * @param {Obejct} audioElementProps - Properties of the audio element.
  449. * @returns {void}
  450. */
  451. _onAudioElementReferenceChanged({ volume }) {
  452. if (this.state.volume !== volume) {
  453. this.setState({ volume });
  454. }
  455. }
  456. _onVolumeChange: number => void;
  457. /**
  458. * Handles volume changes.
  459. *
  460. * @param {number} value - The new value for the volume.
  461. * @returns {void}
  462. */
  463. _onVolumeChange(value) {
  464. this.setState({ volume: value });
  465. }
  466. /**
  467. * Implements React's {@link Component#render()}.
  468. *
  469. * @inheritdoc
  470. * @returns {ReactElement}
  471. */
  472. render() {
  473. const { _participant } = this.props;
  474. if (!_participant) {
  475. return null;
  476. }
  477. const { isFakeParticipant, local = false } = _participant;
  478. if (local) {
  479. return this._renderLocalParticipant();
  480. }
  481. if (isFakeParticipant) {
  482. return this._renderFakeParticipant();
  483. }
  484. return this._renderRemoteParticipant();
  485. }
  486. }
  487. /**
  488. * Maps (parts of) the redux state to the associated props for this component.
  489. *
  490. * @param {Object} state - The Redux state.
  491. * @param {Object} ownProps - The own props of the component.
  492. * @private
  493. * @returns {Props}
  494. */
  495. function _mapStateToProps(state, ownProps): Object {
  496. const { participantID } = ownProps;
  497. // Only the local participant won't have id for the time when the conference is not yet joined.
  498. const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
  499. const isLocal = participant?.local ?? true;
  500. const _videoTrack = isLocal
  501. ? getLocalVideoTrack(state['features/base/tracks'])
  502. : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
  503. const _audioTrack = isLocal
  504. ? getLocalAudioTrack(state['features/base/tracks'])
  505. : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
  506. const _currentLayout = getCurrentLayout(state);
  507. let size = {};
  508. const { startSilent, disableProfile = false } = state['features/base/config'];
  509. const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
  510. switch (_currentLayout) {
  511. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  512. const {
  513. horizontalViewDimensions = {
  514. local: {},
  515. remote: {}
  516. }
  517. } = state['features/filmstrip'];
  518. const { local, remote } = horizontalViewDimensions;
  519. const { width, height } = isLocal ? local : remote;
  520. size = {
  521. _width: width,
  522. _height: height
  523. };
  524. break;
  525. }
  526. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  527. size = {
  528. _heightToWidthPercent: isLocal
  529. ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
  530. : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
  531. };
  532. break;
  533. case LAYOUTS.TILE_VIEW: {
  534. const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
  535. size = {
  536. _width: width,
  537. _height: height
  538. };
  539. break;
  540. }
  541. }
  542. return {
  543. _audioTrack,
  544. _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  545. _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  546. _currentLayout,
  547. _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
  548. _disableProfile: disableProfile,
  549. _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
  550. _indicatorIconSize: NORMAL,
  551. _participant: participant,
  552. _participantCount: getParticipantCount(state),
  553. _startSilent: Boolean(startSilent),
  554. _videoTrack,
  555. ...size
  556. };
  557. }
  558. export default connect(_mapStateToProps)(Thumbnail);