From e421a119e1c85e8041b66d9dc8176dc5e967f0c6 Mon Sep 17 00:00:00 2001 From: Avram Tudor Date: Mon, 2 Aug 2021 15:55:52 +0300 Subject: [PATCH] feat(share-video) Allow sharing direct video links (mp4 etc) on mobile (#9511) * feat(share-video) Allow sharing direct video links (mp4 etc) on mobile * fix linting * code review --- android/sdk/build.gradle | 1 + .../meet/sdk/ReactInstanceManagerHolder.java | 1 + android/settings.gradle | 2 + ios/Podfile | 1 + ios/Podfile.lock | 11 +- package-lock.json | 46 +- package.json | 3 +- .../components/ParticipantView.native.js | 12 +- .../components/AbstractSharedVideoDialog.js | 25 + .../components/native/AbstractVideoManager.js | 261 +++++++++++ .../components/native/SharedVideo.js | 168 +++++++ .../components/native/SharedVideoDialog.js | 57 +-- .../components/native/VideoManager.js | 187 ++++++++ .../components/native/YoutubeLargeVideo.js | 441 ------------------ .../components/native/YoutubeVideoManager.js | 193 ++++++++ .../shared-video/components/native/index.js | 2 +- .../shared-video/components/native/styles.js | 2 +- .../components/web/AbstractVideoManager.js | 33 +- .../components/web/SharedVideoDialog.js | 26 +- .../components/web/VideoManager.js | 25 +- .../components/web/YoutubeVideoManager.js | 25 +- react/features/shared-video/constants.js | 14 +- 22 files changed, 960 insertions(+), 576 deletions(-) create mode 100644 react/features/shared-video/components/native/AbstractVideoManager.js create mode 100644 react/features/shared-video/components/native/SharedVideo.js create mode 100644 react/features/shared-video/components/native/VideoManager.js delete mode 100644 react/features/shared-video/components/native/YoutubeLargeVideo.js create mode 100644 react/features/shared-video/components/native/YoutubeVideoManager.js diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index db2042893c..0aa458e821 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation project(':react-native-sound') implementation project(':react-native-splash-screen') implementation project(':react-native-svg') + implementation project(':react-native-video') implementation project(':react-native-webrtc') implementation project(':react-native-webview') diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java index 84b2031152..4e51c03b93 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java @@ -191,6 +191,7 @@ class ReactInstanceManagerHolder { new com.reactnativecommunity.webview.RNCWebViewPackage(), new com.rnimmersive.RNImmersivePackage(), new com.zmxv.RNSound.RNSoundPackage(), + new com.brentvatne.react.ReactVideoPackage(), new ReactPackageAdapter() { @Override public List createNativeModules(ReactApplicationContext reactContext) { diff --git a/android/settings.gradle b/android/settings.gradle index 307b1313f1..88945b1c4f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -27,6 +27,8 @@ include ':react-native-splash-screen' project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android') include ':react-native-svg' project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') +include ':react-native-video' +project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') include ':react-native-webrtc' project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android') include ':react-native-webview' diff --git a/ios/Podfile b/ios/Podfile index b10ad9fcf4..1e2e18062e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -60,6 +60,7 @@ target 'JitsiMeetSDK' do pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo' pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider' pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen' + pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec' pod 'react-native-webview', :path => '../node_modules/react-native-webview' pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc' pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b9245d3ce6..75d48bec44 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -288,6 +288,11 @@ PODS: - React - react-native-splash-screen (3.2.0): - React + - react-native-video (5.1.1): + - React-Core + - react-native-video/Video (= 5.1.1) + - react-native-video/Video (5.1.1): + - React-Core - react-native-webrtc (1.89.3): - React-Core - react-native-webview (11.0.2): @@ -394,6 +399,7 @@ DEPENDENCIES: - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) + - react-native-video (from `../node_modules/react-native-video/react-native-video.podspec`) - react-native-webrtc (from `../node_modules/react-native-webrtc`) - react-native-webview (from `../node_modules/react-native-webview`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -478,6 +484,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/slider" react-native-splash-screen: :path: "../node_modules/react-native-splash-screen" + react-native-video: + :path: "../node_modules/react-native-video/react-native-video.podspec" react-native-webrtc: :path: "../node_modules/react-native-webrtc" react-native-webview: @@ -559,6 +567,7 @@ SPEC CHECKSUMS: react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93 react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 + react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8 react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87 React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9 @@ -580,6 +589,6 @@ SPEC CHECKSUMS: RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3 Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c -PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49 +PODFILE CHECKSUM: f4db44d934caeae7212dbaa33abe62ed164363e8 COCOAPODS: 1.10.1 diff --git a/package-lock.json b/package-lock.json index 8375b4f526..e6bce38dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7539,6 +7539,11 @@ } } }, + "eme-encryption-scheme-polyfill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz", + "integrity": "sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg==" + }, "emoji-regex": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", @@ -8244,7 +8249,8 @@ "events": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==" + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true }, "eventsource": { "version": "1.0.7", @@ -11016,6 +11022,11 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" }, + "keymirror": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz", + "integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU=" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -15143,6 +15154,16 @@ "whatwg-url-without-unicode": "8.0.0-3" } }, + "react-native-video": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.1.1.tgz", + "integrity": "sha512-zee8gRUrjPWRoZSEBiMebClqu1iAuCQNLjzqpmXFrRWEoJj7azM3BPqLQWJgsnfLiYUYGySeApC/G60THM5+tw==", + "requires": { + "keymirror": "^0.1.1", + "prop-types": "^15.7.2", + "shaka-player": "^2.5.9" + } + }, "react-native-watch-connectivity": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz", @@ -15192,11 +15213,18 @@ } }, "react-native-youtube-iframe": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz", - "integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.1.1.tgz", + "integrity": "sha512-vnLzA5zcnMwa1gMqGfvkjaE82NW1Nd2Up4Q1OUz6IKm69xSG/9/m4APZ5fCN8UMhy6lH95iagd497J7jwEwz3w==", "requires": { - "events": "^3.0.0" + "events": "^3.2.0" + }, + "dependencies": { + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + } } }, "react-node-resolver": { @@ -16070,6 +16098,14 @@ "safe-buffer": "^5.0.1" } }, + "shaka-player": { + "version": "2.5.22", + "resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.22.tgz", + "integrity": "sha512-PAoeNLUQ/hT/9dY7QvNFgIiDtXSqbYVFuXXtLHh7ytVVqTvI/p4HLwfYShiR+sE/sbsDOr9D5l9D/ztLPhxgtw==", + "requires": { + "eme-encryption-scheme-polyfill": "^2.0.1" + } + }, "shallow-clone": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", diff --git a/package.json b/package.json index efe02f9d20..eecbdee1e8 100644 --- a/package.json +++ b/package.json @@ -87,10 +87,11 @@ "react-native-svg": "12.1.0", "react-native-svg-transformer": "0.14.3", "react-native-url-polyfill": "1.2.0", + "react-native-video": "5.1.1", "react-native-watch-connectivity": "0.4.3", "react-native-webrtc": "1.89.3", "react-native-webview": "11.0.2", - "react-native-youtube-iframe": "1.2.3", + "react-native-youtube-iframe": "2.1.1", "react-redux": "7.1.0", "react-textarea-autosize": "8.3.0", "react-transition-group": "2.4.0", diff --git a/react/features/base/participants/components/ParticipantView.native.js b/react/features/base/participants/components/ParticipantView.native.js index 3c930abee9..268365b726 100644 --- a/react/features/base/participants/components/ParticipantView.native.js +++ b/react/features/base/participants/components/ParticipantView.native.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { Text, View } from 'react-native'; -import { YoutubeLargeVideo } from '../../../shared-video/components'; +import { SharedVideo } from '../../../shared-video/components/native'; import { Avatar } from '../../avatar'; import { translate } from '../../i18n'; import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet'; @@ -208,11 +208,11 @@ class ParticipantView extends Component { ? this.props.testHintId : `org.jitsi.meet.Participant#${this.props.participantId}`; - const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo; + const renderSharedVideo = _isFakeParticipant && !disableVideo; return ( { - { renderYoutubeLargeVideo && } + { renderSharedVideo && } { !_isFakeParticipant && renderVideo && { zOrder = { this.props.zOrder } zoomEnabled = { this.props.zoomEnabled } /> } - { !renderYoutubeLargeVideo && !renderVideo + { !renderSharedVideo && !renderVideo && extends Component < Props, } _onSetVideoLink: string => boolean; + + /** + * Validates the entered video link by extracting the id and dispatches it. + * + * It returns a boolean to comply the Dialog behaviour: + * {@code true} - the dialog should be closed. + * {@code false} - the dialog should be left open. + * + * @param {string} link - The entered video link. + * @returns {boolean} + */ + _onSetVideoLink(link: string) { + if (!link || !link.trim()) { + return false; + } + + const youtubeId = getYoutubeId(link); + const { onPostSubmit } = this.props; + + onPostSubmit(youtubeId || link); + + return true; + } } diff --git a/react/features/shared-video/components/native/AbstractVideoManager.js b/react/features/shared-video/components/native/AbstractVideoManager.js new file mode 100644 index 0000000000..07f12f36a3 --- /dev/null +++ b/react/features/shared-video/components/native/AbstractVideoManager.js @@ -0,0 +1,261 @@ +/* @flow */ +/* eslint-disable no-invalid-this */ + +import throttle from 'lodash/throttle'; +import { PureComponent } from 'react'; + +import { getCurrentConference } from '../../../base/conference'; +import { getLocalParticipant } from '../../../base/participants'; +import { setSharedVideoStatus } from '../../actions.any'; +import { PLAYBACK_STATUSES } from '../../constants'; + +/** + * Return true if the diffenrece between the two timees is larger than 5. + * + * @param {number} newTime - The current time. + * @param {number} previousTime - The previous time. + * @private + * @returns {boolean} +*/ +function shouldSeekToPosition(newTime, previousTime) { + return Math.abs(newTime - previousTime) > 5; +} + +/** + * The type of the React {@link Component} props of {@link AbstractVideoManager}. + */ +export type Props = { + + /** + * The current coference + */ + _conference: Object, + + /** + * Is the video shared by the local user. + * + * @private + */ + _isOwner: boolean, + + /** + * The shared video owner id + */ + _ownerId: string, + + /** + * The shared video status + */ + _status: string, + + /** + * Seek time in seconds. + * + */ + _time: number, + + /** + * The video url + */ + _videoUrl: string, + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /** + * The player's height + */ + height: number, + + /** + * The video id + */ + videoId: string, + + /** + * The player's width + */ + width: number +} + +/** + * Manager of shared video. + */ +class AbstractVideoManager extends PureComponent { + throttledFireUpdateSharedVideoEvent: Function; + + /** + * Initializes a new instance of AbstractVideoManager. + * + * @returns {void} + */ + constructor() { + super(); + + this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000); + } + + /** + * Implements React Component's componentDidMount. + * + * @inheritdoc + */ + componentDidMount() { + this.processUpdatedProps(); + } + + /** + * Implements React Component's componentDidUpdate. + * + * @inheritdoc + */ + componentDidUpdate() { + this.processUpdatedProps(); + } + + /** + * Implements React Component's componentWillUnmount. + * + * @inheritdoc + */ + componentWillUnmount() { + if (this.dispose) { + this.dispose(); + } + } + + /** + * Processes new properties. + * + * @returns {void} + */ + async processUpdatedProps() { + const { _status, _time, _isOwner } = this.props; + + if (_isOwner) { + return; + } + + const playerTime = await this.getTime(); + + if (shouldSeekToPosition(_time, playerTime)) { + this.seek(_time); + } + + if (this.getPlaybackStatus() !== _status) { + if (_status === PLAYBACK_STATUSES.PLAYING) { + this.play(); + } else if (_status === PLAYBACK_STATUSES.PAUSED) { + this.pause(); + } + } + } + + /** + * Handle video playing. + * + * @returns {void} + */ + onPlay() { + this.fireUpdateSharedVideoEvent(); + } + + /** + * Handle video paused. + * + * @returns {void} + */ + onPause() { + this.fireUpdateSharedVideoEvent(); + } + + /** + * Dispatches an update action for the shared video. + * + * @returns {void} + */ + async fireUpdateSharedVideoEvent() { + const { _isOwner } = this.props; + + if (!_isOwner) { + return; + } + + const status = this.getPlaybackStatus(); + + if (!Object.values(PLAYBACK_STATUSES).includes(status)) { + return; + } + + const time = await this.getTime(); + + const { + _ownerId, + _videoUrl, + dispatch + } = this.props; + + dispatch(setSharedVideoStatus({ + videoUrl: _videoUrl, + status, + time, + ownerId: _ownerId + })); + } + + /** + * Seeks video to provided time + * @param {number} time + */ + seek: (time: number) => void; + + /** + * Indicates the playback state of the video + */ + getPlaybackStatus: () => boolean; + + /** + * Plays video + */ + play: () => void; + + /** + * Pauses video + */ + pause: () => void; + + /** + * Retrieves current time + */ + getTime: () => number; + + /** + * Disposes current video player + */ + dispose: () => void; +} + + +export default AbstractVideoManager; + +/** + * Maps part of the Redux store to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {Props} + */ +export function _mapStateToProps(state: Object): $Shape { + const { ownerId, status, time, videoUrl } = state['features/shared-video']; + const localParticipant = getLocalParticipant(state); + + return { + _conference: getCurrentConference(state), + _isOwner: ownerId === localParticipant.id, + _ownerId: ownerId, + _status: status, + _time: time, + _videoUrl: videoUrl + }; +} diff --git a/react/features/shared-video/components/native/SharedVideo.js b/react/features/shared-video/components/native/SharedVideo.js new file mode 100644 index 0000000000..b8f05630ed --- /dev/null +++ b/react/features/shared-video/components/native/SharedVideo.js @@ -0,0 +1,168 @@ +// @flow + +import React, { Component } from 'react'; +import { View } from 'react-native'; + +import { getLocalParticipant } from '../../../base/participants'; +import { connect } from '../../../base/redux'; +import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui'; +import { setToolboxVisible } from '../../../toolbox/actions'; + +import VideoManager from './VideoManager'; +import YoutubeVideoManager from './YoutubeVideoManager'; +import styles from './styles'; + +type Props = { + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /** + * Is the video shared by the local user. + * + * @private + */ + isOwner: boolean, + + /** + * True if in landscape mode. + * + * @private + */ + isWideScreen: boolean, + + /** + * The available player width + */ + playerHeight: number, + + /** + * The available player width + */ + playerWidth: number, + + /** + * The shared video url + */ + videoUrl: string, +} + +/** + * Implements a React {@link Component} which represents the large video (a.k.a. + * the conference participant who is on the local stage) on Web/React. + * + * @extends Component + */ +class SharedVideo extends Component { + /** + * Initializes a new {@code SharedVideo} instance. + * + * @param {Object} props - The properties. + */ + constructor(props: Props) { + super(props); + + this.setWideScreenMode(props.isWideScreen); + } + + /** + * Implements React's {@link Component#componentDidUpdate()}. + * + * @inheritdoc + * @returns {void} + */ + componentDidUpdate(prevProps: Props) { + const { isWideScreen } = this.props; + + if (isWideScreen !== prevProps.isWideScreen) { + this.setWideScreenMode(isWideScreen); + } + } + + /** + * Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise. + * + * @param {isWideScreen} isWideScreen - Whether the screen is wide. + * @private + * @returns {void} + */ + setWideScreenMode(isWideScreen) { + this.props.dispatch(setToolboxVisible(!isWideScreen)); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {React$Element} + */ + render() { + const { + isOwner, + playerHeight, + playerWidth, + videoUrl + } = this.props; + + if (!videoUrl) { + return null; + } + + return ( + + {videoUrl.match(/http/) + ? ( + + ) : ( + + ) + } + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated LargeVideo props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Props} + */ +function _mapStateToProps(state) { + const { ownerId, videoUrl } = state['features/shared-video']; + const { aspectRatio, clientHeight, clientWidth } = state['features/base/responsive-ui']; + + const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE; + const localParticipant = getLocalParticipant(state); + + let playerHeight, playerWidth; + + if (isWideScreen) { + playerHeight = clientHeight; + playerWidth = playerHeight * 16 / 9; + } else { + playerWidth = clientWidth; + playerHeight = playerWidth * 9 / 16; + } + + return { + isOwner: ownerId === localParticipant.id, + isWideScreen, + playerHeight, + playerWidth, + videoUrl + }; +} + +export default connect(_mapStateToProps)(SharedVideo); diff --git a/react/features/shared-video/components/native/SharedVideoDialog.js b/react/features/shared-video/components/native/SharedVideoDialog.js index 58b854f39a..ae21c48465 100644 --- a/react/features/shared-video/components/native/SharedVideoDialog.js +++ b/react/features/shared-video/components/native/SharedVideoDialog.js @@ -4,14 +4,35 @@ import React from 'react'; import { InputDialog } from '../../../base/dialog'; import { connect } from '../../../base/redux'; -import { defaultMobileSharedVideoLink } from '../../constants'; -import { getYoutubeId } from '../../functions'; +import { defaultSharedVideoLink } from '../../constants'; import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog'; /** * Implements a component to render a display name prompt. */ class SharedVideoDialog extends AbstractSharedVideoDialog<*> { + /** + * Instantiates a new component. + * + * @inheritdoc + */ + constructor(props) { + super(props); + + this._onSubmitValue = this._onSubmitValue.bind(this); + } + + _onSubmitValue: () => boolean; + + /** + * Callback to be invoked when the value of the link input is submitted. + * + * @param {string} value - The entered video link. + * @returns {boolean} + */ + _onSubmitValue(value) { + return super._onSetVideoLink(value); + } /** * Implements React's {@link Component#render()}. @@ -22,40 +43,12 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> { return ( ); } - - /** - * Validates the entered video link by extracting the id and dispatches it. - * - * It returns a boolean to comply the Dialog behaviour: - * {@code true} - the dialog should be closed. - * {@code false} - the dialog should be left open. - * - * @param {string} link - The entered video link. - * @returns {boolean} - */ - _onSetVideoLink(link: string) { - if (!link || !link.trim()) { - return false; - } - - const videoId = getYoutubeId(link); - - if (videoId) { - const { onPostSubmit } = this.props; - - onPostSubmit && onPostSubmit(videoId); - - return true; - } - - return false; - } } export default connect()(SharedVideoDialog); diff --git a/react/features/shared-video/components/native/VideoManager.js b/react/features/shared-video/components/native/VideoManager.js new file mode 100644 index 0000000000..1e70b839dd --- /dev/null +++ b/react/features/shared-video/components/native/VideoManager.js @@ -0,0 +1,187 @@ +import Logger from 'jitsi-meet-logger'; +import React from 'react'; +import Video from 'react-native-video'; + +import { connect } from '../../../base/redux'; +import { PLAYBACK_STATUSES } from '../../constants'; + +import AbstractVideoManager, { + _mapStateToProps, + Props +} from './AbstractVideoManager'; + +const logger = Logger.getLogger(__filename); + +/** + * Manager of shared video. + */ +class VideoManager extends AbstractVideoManager { + /** + * Initializes a new VideoManager instance. + * + * @param {Object} props - This component's props. + * + * @returns {void} + */ + constructor(props) { + super(props); + + this.state = { + currentTime: 0, + paused: false + }; + + this.playerRef = React.createRef(); + this.onPlaybackRateChange = this.onPlaybackRateChange.bind(this); + this.onProgress = this.onProgress.bind(this); + } + + /** + * Retrieves the current player ref. + */ + get player() { + return this.playerRef.current; + } + + /** + * Indicates the playback state of the video. + * + * @returns {string} + */ + getPlaybackStatus() { + let status; + + if (this.state.paused) { + status = PLAYBACK_STATUSES.PAUSED; + } else { + status = PLAYBACK_STATUSES.PLAYING; + } + + return status; + } + + /** + * Retrieves current time. + * + * @returns {number} + */ + getTime() { + return this.state.currentTime; + } + + /** + * Seeks video to provided time. + * + * @param {number} time - The time to seek to. + * + * @returns {void} + */ + seek(time) { + if (this.player) { + this.player.seek(time); + } + } + + /** + * Plays video. + * + * @returns {void} + */ + play() { + this.setState({ + paused: false + }); + } + + /** + * Pauses video. + * + * @returns {void} + */ + pause() { + this.setState({ + paused: true + }); + } + + /** + * Handles playback rate changed event. + * + * @param {Object} options.playbackRate - Playback rate: 1 - playing, 0 - paused, other - slowed down / sped up. + * @returns {void} + */ + onPlaybackRateChange({ playbackRate }) { + if (playbackRate === 0) { + this.setState({ + paused: true + }, () => { + this.onPause(); + }); + } + + if (playbackRate === 1) { + this.setState({ + paused: false + }, () => { + this.onPlay(); + }); + } + } + + /** + * Handles progress updarte event. + * + * @param {Object} options - Progress event options. + * @returns {void} + */ + onProgress(options) { + this.setState({ currentTime: options.currentTime }); + this.throttledFireUpdateSharedVideoEvent(); + } + + /** + * Retrieves video tag params. + * + * @returns {void} + */ + getPlayerOptions() { + const { _isOwner, videoId, width, height } = this.props; + const { paused } = this.state; + + const options = { + paused, + progressUpdateInterval: 5000, + resizeMode: 'cover', + style: { + height, + width + }, + source: { uri: videoId }, + controls: _isOwner, + pictureInPicture: false, + onProgress: this.onProgress, + onError: event => { + logger.error('Error in the player:', event); + } + }; + + if (_isOwner) { + options.onPlaybackRateChange = this.onPlaybackRateChange; + } + + return options; + } + + /** + * Implements React Component's render. + * + * @inheritdoc + */ + render() { + return (