Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

YoutubeLargeVideo.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. // @flow
  2. import React, { Component, createRef } from 'react';
  3. import { View } from 'react-native';
  4. import YoutubePlayer from 'react-native-youtube-iframe';
  5. import { getLocalParticipant } from '../../../base/participants';
  6. import { connect } from '../../../base/redux';
  7. import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
  8. import { setToolboxVisible } from '../../../toolbox/actions';
  9. import { setSharedVideoStatus } from '../../actions.native';
  10. import { getYoutubeId } from '../../functions';
  11. import styles from './styles';
  12. /**
  13. * Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
  14. *
  15. * @private
  16. */
  17. const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len
  18. /**
  19. * The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
  20. */
  21. type Props = {
  22. /**
  23. * Display the youtube controls on the player.
  24. *
  25. * @private
  26. */
  27. _enableControls: boolean,
  28. /**
  29. * Is the video shared by the local user.
  30. *
  31. * @private
  32. */
  33. _isOwner: boolean,
  34. /**
  35. * The ID of the participant (to be) depicted by LargeVideo.
  36. *
  37. * @private
  38. */
  39. _isPlaying: string,
  40. /**
  41. * Set to true when the status is set to stop and the view should not react to further changes.
  42. *
  43. * @private
  44. */
  45. _isStopped: boolean,
  46. /**
  47. * True if in landscape mode.
  48. *
  49. * @private
  50. */
  51. _isWideScreen: boolean,
  52. /**
  53. * The id of the participant sharing the video.
  54. *
  55. * @private
  56. */
  57. _ownerId: string,
  58. /**
  59. * The height of the player.
  60. *
  61. * @private
  62. */
  63. _playerHeight: number,
  64. /**
  65. * The width of the player.
  66. *
  67. * @private
  68. */
  69. _playerWidth: number,
  70. /**
  71. * Seek time in seconds.
  72. *
  73. * @private
  74. */
  75. _seek: number,
  76. /**
  77. * The status of the player.
  78. *
  79. * @private
  80. */
  81. _status: string,
  82. /**
  83. * The Redux dispatch function.
  84. */
  85. dispatch: Function,
  86. /**
  87. * Youtube url of the video to be played.
  88. *
  89. * @private
  90. */
  91. youtubeUrl: string
  92. };
  93. /**
  94. *
  95. * Implements a React {@code Component} for showing a youtube video.
  96. *
  97. * @extends Component
  98. */
  99. class YoutubeLargeVideo extends Component<Props, *> {
  100. /**
  101. * Saves a handle to the timer for seek time updates,
  102. * so that it can be cancelled when the component unmounts.
  103. */
  104. intervalId: ?IntervalID;
  105. /**
  106. * A React ref to the HTML element containing the {@code YoutubePlayer} instance.
  107. */
  108. playerRef: Object;
  109. /**
  110. * Initializes a new {@code YoutubeLargeVideo} instance.
  111. *
  112. * @param {Object} props - The read-only properties with which the new
  113. * instance is to be initialized.
  114. */
  115. constructor(props: Props) {
  116. super(props);
  117. this.playerRef = createRef();
  118. this._onReady = this._onReady.bind(this);
  119. this._onChangeState = this._onChangeState.bind(this);
  120. this.setWideScreenMode(props._isWideScreen);
  121. }
  122. /**
  123. * Seeks to the new time if the difference between the new one and the current is larger than 5 seconds.
  124. *
  125. * @inheritdoc
  126. * @returns {void}
  127. */
  128. componentDidUpdate(prevProps: Props) {
  129. const playerRef = this.playerRef.current;
  130. const { _isWideScreen, _seek } = this.props;
  131. if (_seek !== prevProps._seek) {
  132. playerRef && playerRef.getCurrentTime().then(time => {
  133. if (shouldSeekToPosition(_seek, time)) {
  134. playerRef && playerRef.seekTo(_seek);
  135. }
  136. });
  137. }
  138. if (_isWideScreen !== prevProps._isWideScreen) {
  139. this.setWideScreenMode(_isWideScreen);
  140. }
  141. }
  142. /**
  143. * Sets the interval for saving the seek time to redux every 5 seconds.
  144. *
  145. * @inheritdoc
  146. * @returns {void}
  147. */
  148. componentDidMount() {
  149. this.intervalId = setInterval(() => {
  150. this.saveRefTime();
  151. }, 5000);
  152. }
  153. /**
  154. * Clears the interval.
  155. *
  156. * @inheritdoc
  157. * @returns {void}
  158. */
  159. componentWillUnmount() {
  160. clearInterval(this.intervalId);
  161. this.intervalId = null;
  162. }
  163. /**
  164. * Renders the YoutubeLargeVideo element.
  165. *
  166. * @override
  167. * @returns {ReactElement}
  168. */
  169. render() {
  170. const {
  171. _enableControls,
  172. _isPlaying,
  173. _playerHeight,
  174. _playerWidth,
  175. youtubeUrl
  176. } = this.props;
  177. return (
  178. <View
  179. pointerEvents = { _enableControls ? 'auto' : 'none' }
  180. style = { styles.youtubeVideoContainer } >
  181. <YoutubePlayer
  182. height = { _playerHeight }
  183. initialPlayerParams = {{
  184. controls: _enableControls,
  185. modestbranding: true,
  186. preventFullScreen: true
  187. }}
  188. /* eslint-disable react/jsx-no-bind */
  189. onChangeState = { this._onChangeState }
  190. /* eslint-disable react/jsx-no-bind */
  191. onReady = { this._onReady }
  192. play = { _isPlaying }
  193. playbackRate = { 1 }
  194. ref = { this.playerRef }
  195. videoId = { getYoutubeId(youtubeUrl) }
  196. volume = { 50 }
  197. webViewProps = {{
  198. bounces: false,
  199. mediaPlaybackRequiresUserAction: false,
  200. scrollEnabled: false,
  201. userAgent: webviewUserAgent
  202. }}
  203. width = { _playerWidth } />
  204. </View>);
  205. }
  206. _onReady: () => void;
  207. /**
  208. * Callback invoked when the player is ready to play the video.
  209. *
  210. * @private
  211. * @returns {void}
  212. */
  213. _onReady() {
  214. if (this.props?._isOwner) {
  215. this.onVideoReady(
  216. this.props.youtubeUrl,
  217. this.playerRef.current && this.playerRef.current.getCurrentTime(),
  218. this.props._ownerId);
  219. }
  220. }
  221. _onChangeState: (status: string) => void;
  222. /**
  223. * Callback invoked when the state of the player changes.
  224. *
  225. * @param {string} status - The new status of the player.
  226. * @private
  227. * @returns {void}
  228. */
  229. _onChangeState(status) {
  230. this.playerRef?.current && this.playerRef.current.getCurrentTime().then(time => {
  231. const {
  232. _isOwner,
  233. _isPlaying,
  234. _isStopped,
  235. _ownerId,
  236. _seek,
  237. youtubeUrl
  238. } = this.props;
  239. if (shouldSetNewStatus(_isStopped, _isOwner, status, _isPlaying, time, _seek)) {
  240. this.onVideoChangeEvent(youtubeUrl, status, time, _ownerId);
  241. }
  242. });
  243. }
  244. /**
  245. * Calls onVideoChangeEvent with the refTime.
  246. *
  247. * @private
  248. * @returns {void}
  249. */
  250. saveRefTime() {
  251. const { youtubeUrl, _status, _ownerId } = this.props;
  252. this.playerRef.current && this.playerRef.current.getCurrentTime().then(time => {
  253. this.onVideoChangeEvent(youtubeUrl, _status, time, _ownerId);
  254. });
  255. }
  256. /**
  257. * Dispatches the video status, time and ownerId if the status is playing or paused.
  258. *
  259. * @param {string} videoUrl - The youtube id of the video.
  260. * @param {string} status - The status of the player.
  261. * @param {number} time - The seek time.
  262. * @param {string} ownerId - The id of the participant sharing the video.
  263. * @private
  264. * @returns {void}
  265. */
  266. onVideoChangeEvent(videoUrl, status, time, ownerId) {
  267. if (![ 'playing', 'paused' ].includes(status)) {
  268. return;
  269. }
  270. this.props.dispatch(setSharedVideoStatus({
  271. videoUrl,
  272. status: translateStatus(status),
  273. time,
  274. ownerId
  275. }));
  276. }
  277. /**
  278. * Dispatches the 'playing' as video status, time and ownerId.
  279. *
  280. * @param {string} videoUrl - The youtube id of the video.
  281. * @param {number} time - The seek time.
  282. * @param {string} ownerId - The id of the participant sharing the video.
  283. * @private
  284. * @returns {void}
  285. */
  286. onVideoReady(videoUrl, time, ownerId) {
  287. time.then(t => this.props.dispatch(setSharedVideoStatus({
  288. videoUrl,
  289. status: 'playing',
  290. time: t,
  291. ownerId
  292. })));
  293. }
  294. /**
  295. * Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise.
  296. *
  297. * @param {isWideScreen} isWideScreen - Whether the screen is wide.
  298. * @private
  299. * @returns {void}
  300. */
  301. setWideScreenMode(isWideScreen) {
  302. this.props.dispatch(setToolboxVisible(!isWideScreen));
  303. }
  304. }
  305. /* eslint-disable max-params */
  306. /**
  307. * Return true if the user is the owner and
  308. * the status has changed or the seek time difference from the previous set is larger than 5 seconds.
  309. *
  310. * @param {boolean} isStopped - Once the status was set to stop, all the other statuses should be ignored.
  311. * @param {boolean} isOwner - Whether the local user is sharing the video.
  312. * @param {string} status - The new status.
  313. * @param {boolean} isPlaying - Whether the component is playing at the moment.
  314. * @param {number} newTime - The new seek time.
  315. * @param {number} previousTime - The old seek time.
  316. * @private
  317. * @returns {boolean}
  318. */
  319. function shouldSetNewStatus(isStopped, isOwner, status, isPlaying, newTime, previousTime) {
  320. if (isStopped) {
  321. return false;
  322. }
  323. if (!isOwner || status === 'buffering') {
  324. return false;
  325. }
  326. if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) {
  327. return true;
  328. }
  329. return shouldSeekToPosition(newTime, previousTime);
  330. }
  331. /**
  332. * Return true if the diffenrece between the two timees is larger than 5.
  333. *
  334. * @param {number} newTime - The current time.
  335. * @param {number} previousTime - The previous time.
  336. * @private
  337. * @returns {boolean}
  338. */
  339. function shouldSeekToPosition(newTime, previousTime) {
  340. return Math.abs(newTime - previousTime) > 5;
  341. }
  342. /**
  343. * Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props.
  344. *
  345. * @param {Object} state - Redux state.
  346. * @private
  347. * @returns {Props}
  348. */
  349. function _mapStateToProps(state) {
  350. const { ownerId, status, time } = state['features/shared-video'];
  351. const localParticipant = getLocalParticipant(state);
  352. const responsiveUi = state['features/base/responsive-ui'];
  353. const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;
  354. const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
  355. let playerHeight, playerWidth;
  356. if (isWideScreen) {
  357. playerHeight = screenHeight;
  358. playerWidth = playerHeight * 16 / 9;
  359. } else {
  360. playerWidth = screenWidth;
  361. playerHeight = playerWidth * 9 / 16;
  362. }
  363. return {
  364. _enableControls: ownerId === localParticipant.id,
  365. _isOwner: ownerId === localParticipant.id,
  366. _isPlaying: status === 'playing',
  367. _isStopped: status === 'stop',
  368. _isWideScreen: isWideScreen,
  369. _ownerId: ownerId,
  370. _playerHeight: playerHeight,
  371. _playerWidth: playerWidth,
  372. _seek: time,
  373. _status: status
  374. };
  375. }
  376. /**
  377. * In case the status is 'paused', it is translated to 'pause' to match the web functionality.
  378. *
  379. * @param {string} status - The status of the shared video.
  380. * @private
  381. * @returns {string}
  382. */
  383. function translateStatus(status) {
  384. if (status === 'paused') {
  385. return 'pause';
  386. }
  387. return status;
  388. }
  389. export default connect(_mapStateToProps)(YoutubeLargeVideo);