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.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. minimumViewTime: 500
  83. };
  84. this._keyExtractor = this._keyExtractor.bind(this);
  85. this._getItemLayout = this._getItemLayout.bind(this);
  86. this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
  87. this._renderThumbnail = this._renderThumbnail.bind(this);
  88. }
  89. _keyExtractor: string => string;
  90. /**
  91. * Returns a key for a passed item of the list.
  92. *
  93. * @param {string} item - The user ID.
  94. * @returns {string} - The user ID.
  95. */
  96. _keyExtractor(item) {
  97. return item;
  98. }
  99. /**
  100. * Calculates the width and height of the filmstrip based on the screen size and aspect ratio.
  101. *
  102. * @returns {Object} - The width and the height.
  103. */
  104. _getDimensions() {
  105. const { _aspectRatio, _clientWidth, _clientHeight } = this.props;
  106. const { height, width, margin } = styles.thumbnail;
  107. if (_aspectRatio === ASPECT_RATIO_NARROW) {
  108. return {
  109. height,
  110. width: this._separateLocalThumbnail ? _clientWidth - width - (margin * 2) : _clientWidth
  111. };
  112. }
  113. return {
  114. height: this._separateLocalThumbnail ? _clientHeight - height - (margin * 2) : _clientHeight,
  115. width
  116. };
  117. }
  118. _getItemLayout: (?Array<string>, number) => {length: number, offset: number, index: number};
  119. /**
  120. * Optimization for FlatList. Returns the length, offset and index for an item.
  121. *
  122. * @param {Array<string>} data - The data array with user IDs.
  123. * @param {number} index - The index number of the item.
  124. * @returns {Object}
  125. */
  126. _getItemLayout(data, index) {
  127. const { _aspectRatio } = this.props;
  128. const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
  129. const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
  130. return {
  131. length,
  132. offset: length * index,
  133. index
  134. };
  135. }
  136. _onViewableItemsChanged: Object => void;
  137. /**
  138. * A handler for visible items changes.
  139. *
  140. * @param {Object} data - The visible items data.
  141. * @param {Array<Object>} data.viewableItems - The visible items array.
  142. * @returns {void}
  143. */
  144. _onViewableItemsChanged({ viewableItems = [] }) {
  145. if (!this._separateLocalThumbnail && viewableItems[0]?.index === 0) {
  146. // Skip the local thumbnail.
  147. viewableItems.shift();
  148. }
  149. if (viewableItems.length === 0) {
  150. // User might be fast-scrolling, it will stabilize.
  151. return;
  152. }
  153. let startIndex = viewableItems[0].index;
  154. let endIndex = viewableItems[viewableItems.length - 1].index;
  155. if (!this._separateLocalThumbnail) {
  156. // We are off by one in the remote participants array.
  157. startIndex -= 1;
  158. endIndex -= 1;
  159. }
  160. this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
  161. }
  162. _renderThumbnail: Object => Object;
  163. /**
  164. * Creates React Element to display each participant in a thumbnail.
  165. *
  166. * @private
  167. * @returns {ReactElement}
  168. */
  169. _renderThumbnail({ item /* , index , separators */ }) {
  170. return (
  171. <Thumbnail
  172. key = { item }
  173. participantID = { item } />)
  174. ;
  175. }
  176. /**
  177. * Implements React's {@link Component#render()}.
  178. *
  179. * @inheritdoc
  180. * @returns {ReactElement}
  181. */
  182. render() {
  183. const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
  184. if (!_visible) {
  185. return null;
  186. }
  187. const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
  188. const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
  189. const { height, width } = this._getDimensions();
  190. const { height: thumbnailHeight, width: thumbnailWidth, margin } = styles.thumbnail;
  191. const initialNumToRender = Math.ceil(isNarrowAspectRatio
  192. ? width / (thumbnailWidth + (2 * margin))
  193. : height / (thumbnailHeight + (2 * margin))
  194. );
  195. const participants = this._separateLocalThumbnail ? _participants : [ _localParticipantId, ..._participants ];
  196. return (
  197. <SafeAreaView style = { filmstripStyle }>
  198. {
  199. this._separateLocalThumbnail
  200. && !isNarrowAspectRatio
  201. && <LocalThumbnail />
  202. }
  203. <FlatList
  204. bounces = { false }
  205. data = { participants }
  206. getItemLayout = { this._getItemLayout }
  207. horizontal = { isNarrowAspectRatio }
  208. initialNumToRender = { initialNumToRender }
  209. key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
  210. keyExtractor = { this._keyExtractor }
  211. onViewableItemsChanged = { this._onViewableItemsChanged }
  212. renderItem = { this._renderThumbnail }
  213. showsHorizontalScrollIndicator = { false }
  214. showsVerticalScrollIndicator = { false }
  215. style = { styles.flatListStageView }
  216. viewabilityConfig = { this._viewabilityConfig }
  217. windowSize = { 2 } />
  218. {
  219. this._separateLocalThumbnail && isNarrowAspectRatio
  220. && <LocalThumbnail />
  221. }
  222. </SafeAreaView>
  223. );
  224. }
  225. }
  226. /**
  227. * Maps (parts of) the redux state to the associated {@code Filmstrip}'s props.
  228. *
  229. * @param {Object} state - The redux state.
  230. * @private
  231. * @returns {Props}
  232. */
  233. function _mapStateToProps(state) {
  234. const { enabled, remoteParticipants } = state['features/filmstrip'];
  235. const showRemoteVideos = shouldRemoteVideosBeVisible(state);
  236. const responsiveUI = state['features/base/responsive-ui'];
  237. return {
  238. _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
  239. _clientHeight: responsiveUI.clientHeight,
  240. _clientWidth: responsiveUI.clientWidth,
  241. _localParticipantId: getLocalParticipant(state)?.id,
  242. _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
  243. _visible: enabled && isFilmstripVisible(state)
  244. };
  245. }
  246. export default connect(_mapStateToProps)(Filmstrip);