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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. // @flow
  2. import React, { PureComponent } from 'react';
  3. import { Image, View } from 'react-native';
  4. import type { Dispatch } from 'redux';
  5. import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../../../base/config';
  6. import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
  7. import { MEDIA_TYPE, VIDEO_TYPE } from '../../../base/media';
  8. import {
  9. PARTICIPANT_ROLE,
  10. ParticipantView,
  11. getLocalParticipant,
  12. getParticipantByIdOrUndefined,
  13. getParticipantCount,
  14. hasRaisedHand,
  15. isEveryoneModerator,
  16. isScreenShareParticipant,
  17. pinParticipant
  18. } from '../../../base/participants';
  19. import { FakeParticipant } from '../../../base/participants/types';
  20. import { Container } from '../../../base/react';
  21. import { connect } from '../../../base/redux';
  22. import {
  23. getTrackByMediaTypeAndParticipant,
  24. getVideoTrackByParticipant,
  25. trackStreamingStatusChanged
  26. } from '../../../base/tracks';
  27. import { ConnectionIndicator } from '../../../connection-indicator';
  28. import { DisplayNameLabel } from '../../../display-name';
  29. import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
  30. import {
  31. showContextMenuDetails,
  32. showSharedVideoMenu
  33. } from '../../../participants-pane/actions.native';
  34. import { toggleToolboxVisible } from '../../../toolbox/actions.native';
  35. import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
  36. import AudioMutedIndicator from './AudioMutedIndicator';
  37. import ModeratorIndicator from './ModeratorIndicator';
  38. import PinnedIndicator from './PinnedIndicator';
  39. import RaisedHandIndicator from './RaisedHandIndicator';
  40. import ScreenShareIndicator from './ScreenShareIndicator';
  41. import styles, { AVATAR_SIZE } from './styles';
  42. /**
  43. * Thumbnail component's property types.
  44. */
  45. type Props = {
  46. /**
  47. * Whether local audio (microphone) is muted or not.
  48. */
  49. _audioMuted: boolean,
  50. /**
  51. * URL of GIF sent by this participant, null if there's none.
  52. */
  53. _gifSrc: ?string,
  54. /**
  55. * The type of participant if the participant is fake.
  56. */
  57. _fakeParticipant?: FakeParticipant,
  58. /**
  59. * Indicates whether the participant is screen sharing.
  60. */
  61. _isScreenShare: boolean,
  62. /**
  63. * Indicates whether the thumbnail is for a virtual screenshare participant.
  64. */
  65. _isVirtualScreenshare: boolean,
  66. /**
  67. * Indicates whether the participant is local.
  68. */
  69. _local: boolean,
  70. /**
  71. * Shared video local participant owner.
  72. */
  73. _localVideoOwner: boolean,
  74. /**
  75. * The ID of the participant obtain from the participant object in Redux.
  76. *
  77. * NOTE: Generally it should be the same as the participantID prop except the case where the passed
  78. * participantID doesn't correspond to any of the existing participants.
  79. */
  80. _participantId: string,
  81. /**
  82. * Indicates whether the participant is pinned or not.
  83. */
  84. _pinned: boolean,
  85. /**
  86. * Whether or not the participant has the hand raised.
  87. */
  88. _raisedHand: boolean,
  89. /**
  90. * Whether to show the dominant speaker indicator or not.
  91. */
  92. _renderDominantSpeakerIndicator: boolean,
  93. /**
  94. * Whether to show the moderator indicator or not.
  95. */
  96. _renderModeratorIndicator: boolean,
  97. /**
  98. * Whether source name signaling is enabled.
  99. */
  100. _sourceNameSignalingEnabled: boolean,
  101. /**
  102. * The video track that will be displayed in the thumbnail.
  103. */
  104. _videoTrack: ?Object,
  105. /**
  106. * Invoked to trigger state changes in Redux.
  107. */
  108. dispatch: Dispatch<any>,
  109. /**
  110. * The height of the thumnail.
  111. */
  112. height: ?number,
  113. /**
  114. * The ID of the participant related to the thumbnail.
  115. */
  116. participantID: ?string,
  117. /**
  118. * Whether to display or hide the display name of the participant in the thumbnail.
  119. */
  120. renderDisplayName: ?boolean,
  121. /**
  122. * If true, it tells the thumbnail that it needs to behave differently. E.g. React differently to a single tap.
  123. */
  124. tileView?: boolean
  125. };
  126. /**
  127. * React component for video thumbnail.
  128. */
  129. class Thumbnail extends PureComponent<Props> {
  130. /**
  131. * Creates new Thumbnail component.
  132. *
  133. * @param {Props} props - The props of the component.
  134. * @returns {Thumbnail}
  135. */
  136. constructor(props: Props) {
  137. super(props);
  138. this._onClick = this._onClick.bind(this);
  139. this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
  140. this.handleTrackStreamingStatusChanged = this.handleTrackStreamingStatusChanged.bind(this);
  141. }
  142. _onClick: () => void;
  143. /**
  144. * Thumbnail click handler.
  145. *
  146. * @returns {void}
  147. */
  148. _onClick() {
  149. const { _participantId, _pinned, dispatch, tileView } = this.props;
  150. if (tileView) {
  151. dispatch(toggleToolboxVisible());
  152. } else {
  153. dispatch(pinParticipant(_pinned ? null : _participantId));
  154. }
  155. }
  156. _onThumbnailLongPress: () => void;
  157. /**
  158. * Thumbnail long press handler.
  159. *
  160. * @returns {void}
  161. */
  162. _onThumbnailLongPress() {
  163. const { _fakeParticipant, _participantId, _local, _localVideoOwner, dispatch } = this.props;
  164. if (_fakeParticipant && _localVideoOwner) {
  165. dispatch(showSharedVideoMenu(_participantId));
  166. }
  167. if (!_fakeParticipant) {
  168. dispatch(showContextMenuDetails(_participantId, _local));
  169. }
  170. }
  171. /**
  172. * Renders the indicators for the thumbnail.
  173. *
  174. * @returns {ReactElement}
  175. */
  176. _renderIndicators() {
  177. const {
  178. _audioMuted: audioMuted,
  179. _fakeParticipant,
  180. _isScreenShare: isScreenShare,
  181. _isVirtualScreenshare,
  182. _renderModeratorIndicator: renderModeratorIndicator,
  183. _participantId: participantId,
  184. _pinned,
  185. renderDisplayName,
  186. tileView
  187. } = this.props;
  188. const indicators = [];
  189. if (!_fakeParticipant) {
  190. indicators.push(<View
  191. key = 'top-left-indicators'
  192. style = { [
  193. styles.thumbnailTopIndicatorContainer,
  194. styles.thumbnailTopLeftIndicatorContainer
  195. ] }>
  196. { !_isVirtualScreenshare && <ConnectionIndicator participantId = { participantId } /> }
  197. { !_isVirtualScreenshare && <RaisedHandIndicator participantId = { participantId } /> }
  198. {tileView && isScreenShare && (
  199. <View style = { styles.indicatorContainer }>
  200. <ScreenShareIndicator />
  201. </View>
  202. )}
  203. </View>);
  204. indicators.push(<Container
  205. key = 'bottom-indicators'
  206. style = { styles.thumbnailIndicatorContainer }>
  207. <Container style = { (audioMuted || renderModeratorIndicator) && styles.bottomIndicatorsContainer }>
  208. { audioMuted && !_isVirtualScreenshare && <AudioMutedIndicator /> }
  209. { !tileView && _pinned && <PinnedIndicator />}
  210. { renderModeratorIndicator && !_isVirtualScreenshare && <ModeratorIndicator />}
  211. { !tileView && (isScreenShare || _isVirtualScreenshare) && <ScreenShareIndicator /> }
  212. </Container>
  213. {
  214. renderDisplayName && <DisplayNameLabel
  215. contained = { true }
  216. participantId = { participantId } />
  217. }
  218. </Container>);
  219. }
  220. return indicators;
  221. }
  222. /**
  223. * Starts listening for track streaming status updates after the initial render.
  224. *
  225. * @inheritdoc
  226. * @returns {void}
  227. */
  228. componentDidMount() {
  229. // Listen to track streaming status changed event to keep it updated.
  230. // TODO: after converting this component to a react function component,
  231. // use a custom hook to update local track streaming status.
  232. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  233. if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
  234. _videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  235. this.handleTrackStreamingStatusChanged);
  236. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  237. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  238. }
  239. }
  240. /**
  241. * Stops listening for track streaming status updates on the old track and starts listening instead on the new
  242. * track.
  243. *
  244. * @inheritdoc
  245. * @returns {void}
  246. */
  247. componentDidUpdate(prevProps: Props) {
  248. // TODO: after converting this component to a react function component,
  249. // use a custom hook to update local track streaming status.
  250. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  251. if (_sourceNameSignalingEnabled
  252. && prevProps._videoTrack?.jitsiTrack?.getSourceName() !== _videoTrack?.jitsiTrack?.getSourceName()) {
  253. if (prevProps._videoTrack && !prevProps._videoTrack.local) {
  254. prevProps._videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  255. this.handleTrackStreamingStatusChanged);
  256. dispatch(trackStreamingStatusChanged(prevProps._videoTrack.jitsiTrack,
  257. prevProps._videoTrack.jitsiTrack.getTrackStreamingStatus()));
  258. }
  259. if (_videoTrack && !_videoTrack.local) {
  260. _videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  261. this.handleTrackStreamingStatusChanged);
  262. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  263. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  264. }
  265. }
  266. }
  267. /**
  268. * Remove listeners for track streaming status update.
  269. *
  270. * @inheritdoc
  271. * @returns {void}
  272. */
  273. componentWillUnmount() {
  274. // TODO: after converting this component to a react function component,
  275. // use a custom hook to update local track streaming status.
  276. const { _videoTrack, dispatch, _sourceNameSignalingEnabled } = this.props;
  277. if (_sourceNameSignalingEnabled && _videoTrack && !_videoTrack.local) {
  278. _videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
  279. this.handleTrackStreamingStatusChanged);
  280. dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
  281. _videoTrack.jitsiTrack.getTrackStreamingStatus()));
  282. }
  283. }
  284. /**
  285. * Handle track streaming status change event by by dispatching an action to update track streaming status for the
  286. * given track in app state.
  287. *
  288. * @param {JitsiTrack} jitsiTrack - The track with streaming status updated.
  289. * @param {JitsiTrackStreamingStatus} streamingStatus - The updated track streaming status.
  290. * @returns {void}
  291. */
  292. handleTrackStreamingStatusChanged(jitsiTrack, streamingStatus) {
  293. this.props.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
  294. }
  295. /**
  296. * Implements React's {@link Component#render()}.
  297. *
  298. * @inheritdoc
  299. * @returns {ReactElement}
  300. */
  301. render() {
  302. const {
  303. _fakeParticipant,
  304. _gifSrc,
  305. _isScreenShare: isScreenShare,
  306. _isVirtualScreenshare,
  307. _participantId: participantId,
  308. _raisedHand,
  309. _renderDominantSpeakerIndicator,
  310. height,
  311. tileView
  312. } = this.props;
  313. const styleOverrides = tileView ? {
  314. aspectRatio: SQUARE_TILE_ASPECT_RATIO,
  315. flex: 0,
  316. height,
  317. maxHeight: null,
  318. maxWidth: null,
  319. width: null
  320. } : null;
  321. return (
  322. <Container
  323. onClick = { this._onClick }
  324. onLongPress = { this._onThumbnailLongPress }
  325. style = { [
  326. styles.thumbnail,
  327. styleOverrides,
  328. _raisedHand && !_isVirtualScreenshare ? styles.thumbnailRaisedHand : null,
  329. _renderDominantSpeakerIndicator && !_isVirtualScreenshare ? styles.thumbnailDominantSpeaker : null
  330. ] }
  331. touchFeedback = { false }>
  332. {_gifSrc ? <Image
  333. source = {{ uri: _gifSrc }}
  334. style = { styles.thumbnailGif } />
  335. : <>
  336. <ParticipantView
  337. avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
  338. disableVideo = { isScreenShare || _fakeParticipant }
  339. participantId = { participantId }
  340. zOrder = { 1 } />
  341. {
  342. this._renderIndicators()
  343. }
  344. </>
  345. }
  346. </Container>
  347. );
  348. }
  349. }
  350. /**
  351. * Function that maps parts of Redux state tree into component props.
  352. *
  353. * @param {Object} state - Redux state.
  354. * @param {Props} ownProps - Properties of component.
  355. * @returns {Object}
  356. */
  357. function _mapStateToProps(state, ownProps) {
  358. const { ownerId } = state['features/shared-video'];
  359. const tracks = state['features/base/tracks'];
  360. const { participantID, tileView } = ownProps;
  361. const participant = getParticipantByIdOrUndefined(state, participantID);
  362. const localParticipantId = getLocalParticipant(state).id;
  363. const id = participant?.id;
  364. const audioTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
  365. const videoTrack = getVideoTrackByParticipant(state, participant);
  366. const isMultiStreamSupportEnabled = getMultipleVideoSupportFeatureFlag(state);
  367. const isScreenShare = videoTrack?.videoType === VIDEO_TYPE.DESKTOP;
  368. const participantCount = getParticipantCount(state);
  369. const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
  370. const _isEveryoneModerator = isEveryoneModerator(state);
  371. const renderModeratorIndicator = tileView && !_isEveryoneModerator
  372. && participant?.role === PARTICIPANT_ROLE.MODERATOR;
  373. const { gifUrl: gifSrc } = getGifForParticipant(state, id);
  374. const mode = getGifDisplayMode(state);
  375. return {
  376. _audioMuted: audioTrack?.muted ?? true,
  377. _fakeParticipant: participant?.fakeParticipant,
  378. _gifSrc: mode === 'chat' ? null : gifSrc,
  379. _isScreenShare: isScreenShare,
  380. _isVirtualScreenshare: isMultiStreamSupportEnabled && isScreenShareParticipant(participant),
  381. _local: participant?.local,
  382. _localVideoOwner: Boolean(ownerId === localParticipantId),
  383. _participantId: id,
  384. _pinned: participant?.pinned,
  385. _raisedHand: hasRaisedHand(participant),
  386. _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
  387. _renderModeratorIndicator: renderModeratorIndicator,
  388. _sourceNameSignalingEnabled: getSourceNameSignalingFeatureFlag(state),
  389. _videoTrack: videoTrack
  390. };
  391. }
  392. export default connect(_mapStateToProps)(Thumbnail);