您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

Thumbnail.js 15KB

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