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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  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._onInitialVolumeSet = this._onInitialVolumeSet.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 indicators of the thumbnail.
  264. *
  265. * @returns {Component}
  266. */
  267. _renderTopIndicators() {
  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-start';
  285. tooltipPosition = 'right';
  286. break;
  287. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  288. statsPopoverPosition = 'left-start';
  289. tooltipPosition = 'left';
  290. break;
  291. default:
  292. statsPopoverPosition = 'auto';
  293. tooltipPosition = 'top';
  294. }
  295. return (
  296. <div>
  297. <AtlasKitThemeProvider mode = 'dark'>
  298. { !_connectionIndicatorDisabled
  299. && <ConnectionIndicator
  300. alwaysVisible = { showConnectionIndicator }
  301. enableStatsDisplay = { true }
  302. iconSize = { iconSize }
  303. isLocalVideo = { local }
  304. participantId = { id }
  305. statsPopoverPosition = { statsPopoverPosition } />
  306. }
  307. <RaisedHandIndicator
  308. iconSize = { iconSize }
  309. participantId = { id }
  310. tooltipPosition = { tooltipPosition } />
  311. { showDominantSpeaker && _participantCount > 2
  312. && <DominantSpeakerIndicator
  313. iconSize = { iconSize }
  314. tooltipPosition = { tooltipPosition } />
  315. }
  316. </AtlasKitThemeProvider>
  317. </div>);
  318. }
  319. /**
  320. * Renders the avatar.
  321. *
  322. * @returns {ReactElement}
  323. */
  324. _renderAvatar() {
  325. const { _participant } = this.props;
  326. const { id } = _participant;
  327. const styles = this._getStyles();
  328. return (
  329. <div
  330. className = 'avatar-container'
  331. style = { styles.avatarContainer }>
  332. <Avatar
  333. className = 'userAvatar'
  334. participantId = { id } />
  335. </div>
  336. );
  337. }
  338. /**
  339. * Renders the local participant's thumbnail.
  340. *
  341. * @returns {ReactElement}
  342. */
  343. _renderLocalParticipant() {
  344. const {
  345. _defaultLocalDisplayName,
  346. _disableProfile,
  347. _participant,
  348. _videoTrack
  349. } = this.props;
  350. const { id } = _participant || {};
  351. const { audioLevel } = this.state;
  352. return (
  353. <>
  354. <div className = 'videocontainer__background' />
  355. <span id = 'localVideoWrapper'>
  356. <VideoTrack
  357. id = 'localVideo_container'
  358. videoTrack = { _videoTrack } />
  359. </span>
  360. <div className = 'videocontainer__toolbar'>
  361. <StatusIndicators participantID = { id } />
  362. </div>
  363. <div className = 'videocontainer__toptoolbar'>
  364. { this._renderTopIndicators() }
  365. </div>
  366. <div className = 'videocontainer__hoverOverlay' />
  367. <div className = 'displayNameContainer'>
  368. <DisplayName
  369. allowEditing = { !_disableProfile }
  370. displayNameSuffix = { _defaultLocalDisplayName }
  371. elementID = 'localDisplayName'
  372. participantID = { id } />
  373. </div>
  374. { this._renderAvatar() }
  375. <span className = 'audioindicator-container'>
  376. <AudioLevelIndicator audioLevel = { audioLevel } />
  377. </span>
  378. </>
  379. );
  380. }
  381. /**
  382. * Renders a remote participant's 'thumbnail.
  383. *
  384. * @returns {ReactElement}
  385. */
  386. _renderRemoteParticipant() {
  387. const {
  388. _audioTrack,
  389. _participant,
  390. _startSilent
  391. } = this.props;
  392. const { id } = _participant;
  393. const { audioLevel, volume } = this.state;
  394. // hide volume when in silent mode
  395. const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
  396. const jitsiTrack = _audioTrack?.jitsiTrack;
  397. const audioTrackId = jitsiTrack && jitsiTrack.getId();
  398. return (
  399. <>
  400. {
  401. _audioTrack
  402. ? <AudioTrack
  403. audioTrack = { _audioTrack }
  404. id = { `remoteAudio_${audioTrackId || ''}` }
  405. muted = { _startSilent }
  406. onInitialVolumeSet = { this._onInitialVolumeSet }
  407. volume = { this.state.volume } />
  408. : null
  409. }
  410. <div className = 'videocontainer__background' />
  411. <div className = 'videocontainer__toptoolbar'>
  412. { this._renderTopIndicators() }
  413. </div>
  414. <div className = 'videocontainer__toolbar'>
  415. <StatusIndicators participantID = { id } />
  416. </div>
  417. <div className = 'videocontainer__hoverOverlay' />
  418. <div className = 'displayNameContainer'>
  419. <DisplayName
  420. elementID = { `participant_${id}_name` }
  421. participantID = { id } />
  422. </div>
  423. { this._renderAvatar() }
  424. <div className = 'presence-label-container'>
  425. <PresenceLabel
  426. className = 'presence-label'
  427. participantID = { id } />
  428. </div>
  429. <span className = 'remotevideomenu'>
  430. <AtlasKitThemeProvider mode = 'dark'>
  431. <RemoteVideoMenuTriggerButton
  432. initialVolumeValue = { volume }
  433. onVolumeChange = { onVolumeChange }
  434. participantID = { id } />
  435. </AtlasKitThemeProvider>
  436. </span>
  437. <span className = 'audioindicator-container'>
  438. <AudioLevelIndicator audioLevel = { audioLevel } />
  439. </span>
  440. </>
  441. );
  442. }
  443. _onInitialVolumeSet: Object => void;
  444. /**
  445. * A handler for the initial volume value of the audio element.
  446. *
  447. * @param {number} volume - Properties of the audio element.
  448. * @returns {void}
  449. */
  450. _onInitialVolumeSet(volume) {
  451. if (this.state.volume !== volume) {
  452. this.setState({ volume });
  453. }
  454. }
  455. _onVolumeChange: number => void;
  456. /**
  457. * Handles volume changes.
  458. *
  459. * @param {number} value - The new value for the volume.
  460. * @returns {void}
  461. */
  462. _onVolumeChange(value) {
  463. this.setState({ volume: value });
  464. }
  465. /**
  466. * Implements React's {@link Component#render()}.
  467. *
  468. * @inheritdoc
  469. * @returns {ReactElement}
  470. */
  471. render() {
  472. const { _participant } = this.props;
  473. if (!_participant) {
  474. return null;
  475. }
  476. const { isFakeParticipant, local } = _participant;
  477. if (local) {
  478. return this._renderLocalParticipant();
  479. }
  480. if (isFakeParticipant) {
  481. return this._renderFakeParticipant();
  482. }
  483. return this._renderRemoteParticipant();
  484. }
  485. }
  486. /**
  487. * Maps (parts of) the redux state to the associated props for this component.
  488. *
  489. * @param {Object} state - The Redux state.
  490. * @param {Object} ownProps - The own props of the component.
  491. * @private
  492. * @returns {Props}
  493. */
  494. function _mapStateToProps(state, ownProps): Object {
  495. const { participantID } = ownProps;
  496. // Only the local participant won't have id for the time when the conference is not yet joined.
  497. const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
  498. const isLocal = participant?.local ?? true;
  499. const _videoTrack = isLocal
  500. ? getLocalVideoTrack(state['features/base/tracks'])
  501. : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
  502. const _audioTrack = isLocal
  503. ? getLocalAudioTrack(state['features/base/tracks'])
  504. : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
  505. const _currentLayout = getCurrentLayout(state);
  506. let size = {};
  507. const { startSilent, disableProfile = false } = state['features/base/config'];
  508. const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
  509. switch (_currentLayout) {
  510. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  511. const {
  512. horizontalViewDimensions = {
  513. local: {},
  514. remote: {}
  515. }
  516. } = state['features/filmstrip'];
  517. const { local, remote } = horizontalViewDimensions;
  518. const { width, height } = isLocal ? local : remote;
  519. size = {
  520. _width: width,
  521. _height: height
  522. };
  523. break;
  524. }
  525. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
  526. size = {
  527. _heightToWidthPercent: isLocal
  528. ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
  529. : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
  530. };
  531. break;
  532. case LAYOUTS.TILE_VIEW: {
  533. const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
  534. size = {
  535. _width: width,
  536. _height: height
  537. };
  538. break;
  539. }
  540. }
  541. return {
  542. _audioTrack,
  543. _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
  544. _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
  545. _currentLayout,
  546. _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
  547. _disableProfile: disableProfile,
  548. _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
  549. _indicatorIconSize: NORMAL,
  550. _participant: participant,
  551. _participantCount: getParticipantCount(state),
  552. _startSilent: Boolean(startSilent),
  553. _videoTrack,
  554. ...size
  555. };
  556. }
  557. export default connect(_mapStateToProps)(Thumbnail);