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.

Filmstrip.js 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // @flow
  2. import React, { PureComponent } from 'react';
  3. import { FlatList, SafeAreaView } from 'react-native';
  4. import { getLocalParticipant } from '../../../base/participants';
  5. import { Platform } from '../../../base/react';
  6. import { connect } from '../../../base/redux';
  7. import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
  8. import { setVisibleRemoteParticipants } from '../../actions';
  9. import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
  10. import LocalThumbnail from './LocalThumbnail';
  11. import Thumbnail from './Thumbnail';
  12. import styles from './styles';
  13. // Immutable reference to avoid re-renders.
  14. const NO_REMOTE_VIDEOS = [];
  15. /**
  16. * Filmstrip component's property types.
  17. */
  18. type Props = {
  19. /**
  20. * Application's aspect ratio.
  21. */
  22. _aspectRatio: Symbol,
  23. _clientWidth: number,
  24. _clientHeight: number,
  25. _localParticipantId: string,
  26. /**
  27. * The participants in the conference.
  28. */
  29. _participants: Array<any>,
  30. /**
  31. * The indicator which determines whether the filmstrip is visible.
  32. */
  33. _visible: boolean,
  34. /**
  35. * Invoked to trigger state changes in Redux.
  36. */
  37. dispatch: Function,
  38. };
  39. /**
  40. * Implements a React {@link Component} which represents the filmstrip on
  41. * mobile/React Native.
  42. *
  43. * @extends Component
  44. */
  45. class Filmstrip extends PureComponent<Props> {
  46. /**
  47. * Whether the local participant should be rendered separately from the
  48. * remote participants i.e. outside of their {@link ScrollView}.
  49. */
  50. _separateLocalThumbnail: boolean;
  51. /**
  52. * The FlatList's viewabilityConfig.
  53. */
  54. _viewabilityConfig: Object;
  55. /**
  56. * Constructor of the component.
  57. *
  58. * @inheritdoc
  59. */
  60. constructor(props) {
  61. super(props);
  62. // XXX Our current design is to have the local participant separate from
  63. // the remote participants. Unfortunately, Android's Video
  64. // implementation cannot accommodate that because remote participants'
  65. // videos appear on top of the local participant's video at times.
  66. // That's because Android's Video utilizes EGL and EGL gives us only two
  67. // practical layers in which we can place our participants' videos:
  68. // layer #0 sits behind the window, creates a hole in the window, and
  69. // there we render the LargeVideo; layer #1 is known as media overlay in
  70. // EGL terms, renders on top of layer #0, and, consequently, is for the
  71. // Filmstrip. With the separate LocalThumnail, we should have left the
  72. // remote participants' Thumbnails in layer #1 and utilized layer #2 for
  73. // LocalThumbnail. Unfortunately, layer #2 is not practical (that's why
  74. // I said we had two practical layers only) because it renders on top of
  75. // everything which in our case means on top of participant-related
  76. // indicators such as moderator, audio and video muted, etc. For now we
  77. // do not have much of a choice but to continue rendering LocalThumbnail
  78. // as any other remote Thumbnail on Android.
  79. this._separateLocalThumbnail = Platform.OS !== 'android';
  80. this._viewabilityConfig = {
  81. itemVisiblePercentThreshold: 30
  82. };
  83. this._keyExtractor = this._keyExtractor.bind(this);
  84. this._getItemLayout = this._getItemLayout.bind(this);
  85. this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
  86. this._renderThumbnail = this._renderThumbnail.bind(this);
  87. }
  88. _keyExtractor: string => string;
  89. /**
  90. * Returns a key for a passed item of the list.
  91. *
  92. * @param {string} item - The user ID.
  93. * @returns {string} - The user ID.
  94. */
  95. _keyExtractor(item) {
  96. return item;
  97. }
  98. /**
  99. * Calculates the width and height of the filmstrip based on the screen size and aspect ratio.
  100. *
  101. * @returns {Object} - The width and the height.
  102. */
  103. _getDimensions() {
  104. const { _aspectRatio, _clientWidth, _clientHeight } = this.props;
  105. const { height, width, margin } = styles.thumbnail;
  106. if (_aspectRatio === ASPECT_RATIO_NARROW) {
  107. return {
  108. height,
  109. width: this._separateLocalThumbnail ? _clientWidth - width - (margin * 2) : _clientWidth
  110. };
  111. }
  112. return {
  113. height: this._separateLocalThumbnail ? _clientHeight - height - (margin * 2) : _clientHeight,
  114. width
  115. };
  116. }
  117. _getItemLayout: (?Array<string>, number) => {length: number, offset: number, index: number};
  118. /**
  119. * Optimization for FlatList. Returns the length, offset and index for an item.
  120. *
  121. * @param {Array<string>} data - The data array with user IDs.
  122. * @param {number} index - The index number of the item.
  123. * @returns {Object}
  124. */
  125. _getItemLayout(data, index) {
  126. const { _aspectRatio } = this.props;
  127. const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
  128. const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
  129. return {
  130. length,
  131. offset: length * index,
  132. index
  133. };
  134. }
  135. _onViewableItemsChanged: Object => void;
  136. /**
  137. * A handler for visible items changes.
  138. *
  139. * @param {Object} data - The visible items data.
  140. * @param {Array<Object>} data.viewableItems - The visible items array.
  141. * @returns {void}
  142. */
  143. _onViewableItemsChanged({ viewableItems = [] }) {
  144. const indexArray: Array<number> = viewableItems.map(i => i.index);
  145. // If the local video placed at the beginning we need to shift the start index of the remoteParticipants array
  146. // with 1 because and in the same time we don't need to adjust the end index because the end index will not be
  147. // included.
  148. const startIndex
  149. = this._separateLocalThumbnail ? Math.min(...indexArray) : Math.max(Math.min(...indexArray) - 1, 0);
  150. const endIndex = Math.max(...indexArray) + (this._separateLocalThumbnail ? 1 : 0);
  151. this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
  152. }
  153. _renderThumbnail: Object => Object;
  154. /**
  155. * Creates React Element to display each participant in a thumbnail.
  156. *
  157. * @private
  158. * @returns {ReactElement}
  159. */
  160. _renderThumbnail({ item /* , index , separators */ }) {
  161. return (
  162. <Thumbnail
  163. key = { item }
  164. participantID = { item } />)
  165. ;
  166. }
  167. /**
  168. * Implements React's {@link Component#render()}.
  169. *
  170. * @inheritdoc
  171. * @returns {ReactElement}
  172. */
  173. render() {
  174. const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
  175. if (!_visible) {
  176. return null;
  177. }
  178. const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
  179. const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
  180. const { height, width } = this._getDimensions();
  181. const { height: thumbnailHeight, width: thumbnailWidth, margin } = styles.thumbnail;
  182. const initialNumToRender = Math.ceil(isNarrowAspectRatio
  183. ? width / (thumbnailWidth + (2 * margin))
  184. : height / (thumbnailHeight + (2 * margin))
  185. );
  186. const participants = this._separateLocalThumbnail ? _participants : [ _localParticipantId, ..._participants ];
  187. return (
  188. <SafeAreaView style = { filmstripStyle }>
  189. {
  190. this._separateLocalThumbnail
  191. && !isNarrowAspectRatio
  192. && <LocalThumbnail />
  193. }
  194. <FlatList
  195. bounces = { false }
  196. data = { participants }
  197. getItemLayout = { this._getItemLayout }
  198. horizontal = { isNarrowAspectRatio }
  199. initialNumToRender = { initialNumToRender }
  200. key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
  201. keyExtractor = { this._keyExtractor }
  202. onViewableItemsChanged = { this._onViewableItemsChanged }
  203. renderItem = { this._renderThumbnail }
  204. showsHorizontalScrollIndicator = { false }
  205. showsVerticalScrollIndicator = { false }
  206. style = { styles.flatListStageView }
  207. viewabilityConfig = { this._viewabilityConfig }
  208. windowSize = { 2 } />
  209. {
  210. this._separateLocalThumbnail && isNarrowAspectRatio
  211. && <LocalThumbnail />
  212. }
  213. </SafeAreaView>
  214. );
  215. }
  216. }
  217. /**
  218. * Maps (parts of) the redux state to the associated {@code Filmstrip}'s props.
  219. *
  220. * @param {Object} state - The redux state.
  221. * @private
  222. * @returns {Props}
  223. */
  224. function _mapStateToProps(state) {
  225. const { enabled, remoteParticipants } = state['features/filmstrip'];
  226. const showRemoteVideos = shouldRemoteVideosBeVisible(state);
  227. const responsiveUI = state['features/base/responsive-ui'];
  228. return {
  229. _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
  230. _clientHeight: responsiveUI.clientHeight,
  231. _clientWidth: responsiveUI.clientWidth,
  232. _localParticipantId: getLocalParticipant(state)?.id,
  233. _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
  234. _visible: enabled && isFilmstripVisible(state)
  235. };
  236. }
  237. export default connect(_mapStateToProps)(Filmstrip);