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
This commit is contained in:
parent
619acaca24
commit
e421a119e1
|
@ -74,6 +74,7 @@ dependencies {
|
||||||
implementation project(':react-native-sound')
|
implementation project(':react-native-sound')
|
||||||
implementation project(':react-native-splash-screen')
|
implementation project(':react-native-splash-screen')
|
||||||
implementation project(':react-native-svg')
|
implementation project(':react-native-svg')
|
||||||
|
implementation project(':react-native-video')
|
||||||
implementation project(':react-native-webrtc')
|
implementation project(':react-native-webrtc')
|
||||||
implementation project(':react-native-webview')
|
implementation project(':react-native-webview')
|
||||||
|
|
||||||
|
|
|
@ -191,6 +191,7 @@ class ReactInstanceManagerHolder {
|
||||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||||
new com.rnimmersive.RNImmersivePackage(),
|
new com.rnimmersive.RNImmersivePackage(),
|
||||||
new com.zmxv.RNSound.RNSoundPackage(),
|
new com.zmxv.RNSound.RNSoundPackage(),
|
||||||
|
new com.brentvatne.react.ReactVideoPackage(),
|
||||||
new ReactPackageAdapter() {
|
new ReactPackageAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
|
|
@ -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')
|
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
|
||||||
include ':react-native-svg'
|
include ':react-native-svg'
|
||||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
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'
|
include ':react-native-webrtc'
|
||||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||||
include ':react-native-webview'
|
include ':react-native-webview'
|
||||||
|
|
|
@ -60,6 +60,7 @@ target 'JitsiMeetSDK' do
|
||||||
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
|
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-slider', :path => '../node_modules/@react-native-community/slider'
|
||||||
pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
|
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-webview', :path => '../node_modules/react-native-webview'
|
||||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||||
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
|
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
|
||||||
|
|
|
@ -288,6 +288,11 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- react-native-splash-screen (3.2.0):
|
- react-native-splash-screen (3.2.0):
|
||||||
- React
|
- 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-native-webrtc (1.89.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-webview (11.0.2):
|
- react-native-webview (11.0.2):
|
||||||
|
@ -394,6 +399,7 @@ DEPENDENCIES:
|
||||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||||
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
- 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-webrtc (from `../node_modules/react-native-webrtc`)
|
||||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||||
|
@ -478,6 +484,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/@react-native-community/slider"
|
:path: "../node_modules/@react-native-community/slider"
|
||||||
react-native-splash-screen:
|
react-native-splash-screen:
|
||||||
:path: "../node_modules/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:
|
react-native-webrtc:
|
||||||
:path: "../node_modules/react-native-webrtc"
|
:path: "../node_modules/react-native-webrtc"
|
||||||
react-native-webview:
|
react-native-webview:
|
||||||
|
@ -559,6 +567,7 @@ SPEC CHECKSUMS:
|
||||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||||
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
||||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||||
|
react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
|
||||||
react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
|
react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
|
||||||
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
||||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||||
|
@ -580,6 +589,6 @@ SPEC CHECKSUMS:
|
||||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49
|
PODFILE CHECKSUM: f4db44d934caeae7212dbaa33abe62ed164363e8
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.10.1
|
||||||
|
|
|
@ -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": {
|
"emoji-regex": {
|
||||||
"version": "6.5.1",
|
"version": "6.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
|
||||||
|
@ -8244,7 +8249,8 @@
|
||||||
"events": {
|
"events": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
"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": {
|
"eventsource": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
@ -11016,6 +11022,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
||||||
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
||||||
},
|
},
|
||||||
|
"keymirror": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
@ -15143,6 +15154,16 @@
|
||||||
"whatwg-url-without-unicode": "8.0.0-3"
|
"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": {
|
"react-native-watch-connectivity": {
|
||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz",
|
"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": {
|
"react-native-youtube-iframe": {
|
||||||
"version": "1.2.3",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.1.1.tgz",
|
||||||
"integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
|
"integrity": "sha512-vnLzA5zcnMwa1gMqGfvkjaE82NW1Nd2Up4Q1OUz6IKm69xSG/9/m4APZ5fCN8UMhy6lH95iagd497J7jwEwz3w==",
|
||||||
"requires": {
|
"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": {
|
"react-node-resolver": {
|
||||||
|
@ -16070,6 +16098,14 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"shallow-clone": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
|
||||||
|
|
|
@ -87,10 +87,11 @@
|
||||||
"react-native-svg": "12.1.0",
|
"react-native-svg": "12.1.0",
|
||||||
"react-native-svg-transformer": "0.14.3",
|
"react-native-svg-transformer": "0.14.3",
|
||||||
"react-native-url-polyfill": "1.2.0",
|
"react-native-url-polyfill": "1.2.0",
|
||||||
|
"react-native-video": "5.1.1",
|
||||||
"react-native-watch-connectivity": "0.4.3",
|
"react-native-watch-connectivity": "0.4.3",
|
||||||
"react-native-webrtc": "1.89.3",
|
"react-native-webrtc": "1.89.3",
|
||||||
"react-native-webview": "11.0.2",
|
"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-redux": "7.1.0",
|
||||||
"react-textarea-autosize": "8.3.0",
|
"react-textarea-autosize": "8.3.0",
|
||||||
"react-transition-group": "2.4.0",
|
"react-transition-group": "2.4.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
import { YoutubeLargeVideo } from '../../../shared-video/components';
|
import { SharedVideo } from '../../../shared-video/components/native';
|
||||||
import { Avatar } from '../../avatar';
|
import { Avatar } from '../../avatar';
|
||||||
import { translate } from '../../i18n';
|
import { translate } from '../../i18n';
|
||||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||||
|
@ -208,11 +208,11 @@ class ParticipantView extends Component<Props> {
|
||||||
? this.props.testHintId
|
? this.props.testHintId
|
||||||
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
||||||
|
|
||||||
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
|
const renderSharedVideo = _isFakeParticipant && !disableVideo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
|
onClick = { renderVideo || renderSharedVideo ? undefined : onPress }
|
||||||
style = {{
|
style = {{
|
||||||
...styles.participantView,
|
...styles.participantView,
|
||||||
...this.props.style
|
...this.props.style
|
||||||
|
@ -221,10 +221,10 @@ class ParticipantView extends Component<Props> {
|
||||||
|
|
||||||
<TestHint
|
<TestHint
|
||||||
id = { testHintId }
|
id = { testHintId }
|
||||||
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
|
onPress = { renderSharedVideo ? undefined : onPress }
|
||||||
value = '' />
|
value = '' />
|
||||||
|
|
||||||
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
|
{ renderSharedVideo && <SharedVideo /> }
|
||||||
|
|
||||||
{ !_isFakeParticipant && renderVideo
|
{ !_isFakeParticipant && renderVideo
|
||||||
&& <VideoTrack
|
&& <VideoTrack
|
||||||
|
@ -234,7 +234,7 @@ class ParticipantView extends Component<Props> {
|
||||||
zOrder = { this.props.zOrder }
|
zOrder = { this.props.zOrder }
|
||||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||||
|
|
||||||
{ !renderYoutubeLargeVideo && !renderVideo
|
{ !renderSharedVideo && !renderVideo
|
||||||
&& <View style = { styles.avatarContainer }>
|
&& <View style = { styles.avatarContainer }>
|
||||||
<Avatar
|
<Avatar
|
||||||
participantId = { this.props.participantId }
|
participantId = { this.props.participantId }
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { getYoutubeId } from '../functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link AbstractSharedVideoDialog}.
|
* {@link AbstractSharedVideoDialog}.
|
||||||
|
@ -42,4 +44,27 @@ export default class AbstractSharedVideoDialog<S: *> extends Component < Props,
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSetVideoLink: string => boolean;
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Props> {
|
||||||
|
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<Props> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
|
@ -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<Props> {
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<View
|
||||||
|
pointerEvents = { isOwner ? 'auto' : 'none' }
|
||||||
|
style = { styles.videoContainer } >
|
||||||
|
{videoUrl.match(/http/)
|
||||||
|
? (
|
||||||
|
<VideoManager
|
||||||
|
height = { playerHeight }
|
||||||
|
videoId = { videoUrl }
|
||||||
|
width = { playerWidth } />
|
||||||
|
) : (
|
||||||
|
<YoutubeVideoManager
|
||||||
|
height = { playerHeight }
|
||||||
|
videoId = { videoUrl }
|
||||||
|
width = { playerWidth } />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
|
@ -4,14 +4,35 @@ import React from 'react';
|
||||||
|
|
||||||
import { InputDialog } from '../../../base/dialog';
|
import { InputDialog } from '../../../base/dialog';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { defaultMobileSharedVideoLink } from '../../constants';
|
import { defaultSharedVideoLink } from '../../constants';
|
||||||
import { getYoutubeId } from '../../functions';
|
|
||||||
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a component to render a display name prompt.
|
* Implements a component to render a display name prompt.
|
||||||
*/
|
*/
|
||||||
class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
|
@ -22,40 +43,12 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||||
return (
|
return (
|
||||||
<InputDialog
|
<InputDialog
|
||||||
contentKey = 'dialog.shareVideoTitle'
|
contentKey = 'dialog.shareVideoTitle'
|
||||||
onSubmit = { this._onSetVideoLink }
|
onSubmit = { this._onSubmitValue }
|
||||||
textInputProps = {{
|
textInputProps = {{
|
||||||
placeholder: defaultMobileSharedVideoLink
|
placeholder: defaultSharedVideoLink
|
||||||
}} />
|
}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
export default connect()(SharedVideoDialog);
|
||||||
|
|
|
@ -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<Props> {
|
||||||
|
/**
|
||||||
|
* 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 (<Video
|
||||||
|
ref = { this.playerRef }
|
||||||
|
{ ...this.getPlayerOptions() } />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_mapStateToProps)(VideoManager);
|
|
@ -1,441 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { Component, createRef } from 'react';
|
|
||||||
import { View } from 'react-native';
|
|
||||||
import YoutubePlayer from 'react-native-youtube-iframe';
|
|
||||||
|
|
||||||
import { getLocalParticipant } from '../../../base/participants';
|
|
||||||
import { connect } from '../../../base/redux';
|
|
||||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
|
|
||||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
|
||||||
import { setSharedVideoStatus } from '../../actions.native';
|
|
||||||
|
|
||||||
import styles from './styles';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
|
|
||||||
*/
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the youtube controls on the player.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_enableControls: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the video shared by the local user.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_isOwner: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID of the participant (to be) depicted by LargeVideo.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_isPlaying: string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true when the status is set to stop and the view should not react to further changes.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_isStopped: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if in landscape mode.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_isWideScreen: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The id of the participant sharing the video.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_ownerId: string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The height of the player.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_playerHeight: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The width of the player.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_playerWidth: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek time in seconds.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_seek: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The status of the player.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_status: string,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redux dispatch function.
|
|
||||||
*/
|
|
||||||
dispatch: Function,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Youtube id of the video to be played.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
youtubeId: string
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Implements a React {@code Component} for showing a youtube video.
|
|
||||||
*
|
|
||||||
* @extends Component
|
|
||||||
*/
|
|
||||||
class YoutubeLargeVideo extends Component<Props, *> {
|
|
||||||
/**
|
|
||||||
* Saves a handle to the timer for seek time updates,
|
|
||||||
* so that it can be cancelled when the component unmounts.
|
|
||||||
*/
|
|
||||||
intervalId: ?IntervalID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A React ref to the HTML element containing the {@code YoutubePlayer} instance.
|
|
||||||
*/
|
|
||||||
playerRef: Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a new {@code YoutubeLargeVideo} instance.
|
|
||||||
*
|
|
||||||
* @param {Object} props - The read-only properties with which the new
|
|
||||||
* instance is to be initialized.
|
|
||||||
*/
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.playerRef = createRef();
|
|
||||||
|
|
||||||
this._onReady = this._onReady.bind(this);
|
|
||||||
this._onChangeState = this._onChangeState.bind(this);
|
|
||||||
|
|
||||||
this.setWideScreenMode(props._isWideScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seeks to the new time if the difference between the new one and the current is larger than 5 seconds.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
const playerRef = this.playerRef.current;
|
|
||||||
const { _isWideScreen, _seek } = this.props;
|
|
||||||
|
|
||||||
if (_seek !== prevProps._seek) {
|
|
||||||
playerRef && playerRef.getCurrentTime().then(time => {
|
|
||||||
if (shouldSeekToPosition(_seek, time)) {
|
|
||||||
playerRef && playerRef.seekTo(_seek);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isWideScreen !== prevProps._isWideScreen) {
|
|
||||||
this.setWideScreenMode(_isWideScreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the interval for saving the seek time to redux every 5 seconds.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.intervalId = setInterval(() => {
|
|
||||||
this.saveRefTime();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the interval.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearInterval(this.intervalId);
|
|
||||||
this.intervalId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the YoutubeLargeVideo element.
|
|
||||||
*
|
|
||||||
* @override
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
_enableControls,
|
|
||||||
_isPlaying,
|
|
||||||
_playerHeight,
|
|
||||||
_playerWidth,
|
|
||||||
youtubeId
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
pointerEvents = { _enableControls ? 'auto' : 'none' }
|
|
||||||
style = { styles.youtubeVideoContainer } >
|
|
||||||
<YoutubePlayer
|
|
||||||
height = { _playerHeight }
|
|
||||||
initialPlayerParams = {{
|
|
||||||
controls: _enableControls,
|
|
||||||
modestbranding: true,
|
|
||||||
preventFullScreen: true
|
|
||||||
}}
|
|
||||||
/* eslint-disable react/jsx-no-bind */
|
|
||||||
onChangeState = { this._onChangeState }
|
|
||||||
/* eslint-disable react/jsx-no-bind */
|
|
||||||
onReady = { this._onReady }
|
|
||||||
play = { _isPlaying }
|
|
||||||
playbackRate = { 1 }
|
|
||||||
ref = { this.playerRef }
|
|
||||||
videoId = { youtubeId }
|
|
||||||
volume = { 50 }
|
|
||||||
webViewProps = {{
|
|
||||||
bounces: false,
|
|
||||||
mediaPlaybackRequiresUserAction: false,
|
|
||||||
scrollEnabled: false,
|
|
||||||
userAgent: webviewUserAgent
|
|
||||||
}}
|
|
||||||
width = { _playerWidth } />
|
|
||||||
</View>);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onReady: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when the player is ready to play the video.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_onReady() {
|
|
||||||
if (this.props?._isOwner) {
|
|
||||||
this.onVideoReady(
|
|
||||||
this.props.youtubeId,
|
|
||||||
this.playerRef.current && this.playerRef.current.getCurrentTime(),
|
|
||||||
this.props._ownerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onChangeState: (status: string) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when the state of the player changes.
|
|
||||||
*
|
|
||||||
* @param {string} status - The new status of the player.
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_onChangeState(status) {
|
|
||||||
this.playerRef?.current && this.playerRef.current.getCurrentTime().then(time => {
|
|
||||||
const {
|
|
||||||
_isOwner,
|
|
||||||
_isPlaying,
|
|
||||||
_isStopped,
|
|
||||||
_ownerId,
|
|
||||||
_seek,
|
|
||||||
youtubeId
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (shouldSetNewStatus(_isStopped, _isOwner, status, _isPlaying, time, _seek)) {
|
|
||||||
this.onVideoChangeEvent(youtubeId, status, time, _ownerId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls onVideoChangeEvent with the refTime.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
saveRefTime() {
|
|
||||||
const { youtubeId, _status, _ownerId } = this.props;
|
|
||||||
|
|
||||||
this.playerRef.current && this.playerRef.current.getCurrentTime().then(time => {
|
|
||||||
this.onVideoChangeEvent(youtubeId, _status, time, _ownerId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches the video status, time and ownerId if the status is playing or paused.
|
|
||||||
*
|
|
||||||
* @param {string} videoUrl - The youtube id of the video.
|
|
||||||
* @param {string} status - The status of the player.
|
|
||||||
* @param {number} time - The seek time.
|
|
||||||
* @param {string} ownerId - The id of the participant sharing the video.
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onVideoChangeEvent(videoUrl, status, time, ownerId) {
|
|
||||||
if (![ 'playing', 'paused' ].includes(status)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.dispatch(setSharedVideoStatus({
|
|
||||||
videoUrl,
|
|
||||||
status: translateStatus(status),
|
|
||||||
time,
|
|
||||||
ownerId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches the 'playing' as video status, time and ownerId.
|
|
||||||
*
|
|
||||||
* @param {string} videoUrl - The youtube id of the video.
|
|
||||||
* @param {number} time - The seek time.
|
|
||||||
* @param {string} ownerId - The id of the participant sharing the video.
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onVideoReady(videoUrl, time, ownerId) {
|
|
||||||
time.then(t => this.props.dispatch(setSharedVideoStatus({
|
|
||||||
videoUrl,
|
|
||||||
status: 'playing',
|
|
||||||
time: t,
|
|
||||||
ownerId
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable max-params */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the user is the owner and
|
|
||||||
* the status has changed or the seek time difference from the previous set is larger than 5 seconds.
|
|
||||||
*
|
|
||||||
* @param {boolean} isStopped - Once the status was set to stop, all the other statuses should be ignored.
|
|
||||||
* @param {boolean} isOwner - Whether the local user is sharing the video.
|
|
||||||
* @param {string} status - The new status.
|
|
||||||
* @param {boolean} isPlaying - Whether the component is playing at the moment.
|
|
||||||
* @param {number} newTime - The new seek time.
|
|
||||||
* @param {number} previousTime - The old seek time.
|
|
||||||
* @private
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function shouldSetNewStatus(isStopped, isOwner, status, isPlaying, newTime, previousTime) {
|
|
||||||
if (isStopped) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOwner || status === 'buffering') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldSeekToPosition(newTime, previousTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props.
|
|
||||||
*
|
|
||||||
* @param {Object} state - Redux state.
|
|
||||||
* @private
|
|
||||||
* @returns {Props}
|
|
||||||
*/
|
|
||||||
function _mapStateToProps(state) {
|
|
||||||
const { ownerId, status, time } = state['features/shared-video'];
|
|
||||||
const localParticipant = getLocalParticipant(state);
|
|
||||||
const responsiveUi = state['features/base/responsive-ui'];
|
|
||||||
const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;
|
|
||||||
const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
|
|
||||||
|
|
||||||
let playerHeight, playerWidth;
|
|
||||||
|
|
||||||
if (isWideScreen) {
|
|
||||||
playerHeight = screenHeight;
|
|
||||||
playerWidth = playerHeight * 16 / 9;
|
|
||||||
} else {
|
|
||||||
playerWidth = screenWidth;
|
|
||||||
playerHeight = playerWidth * 9 / 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
_enableControls: ownerId === localParticipant.id,
|
|
||||||
_isOwner: ownerId === localParticipant.id,
|
|
||||||
_isPlaying: status === 'playing',
|
|
||||||
_isStopped: status === 'stop',
|
|
||||||
_isWideScreen: isWideScreen,
|
|
||||||
_ownerId: ownerId,
|
|
||||||
_playerHeight: playerHeight,
|
|
||||||
_playerWidth: playerWidth,
|
|
||||||
_seek: time,
|
|
||||||
_status: status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In case the status is 'paused', it is translated to 'pause' to match the web functionality.
|
|
||||||
*
|
|
||||||
* @param {string} status - The status of the shared video.
|
|
||||||
* @private
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function translateStatus(status) {
|
|
||||||
if (status === 'paused') {
|
|
||||||
return 'pause';
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(_mapStateToProps)(YoutubeLargeVideo);
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Video from 'react-native-youtube-iframe';
|
||||||
|
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import { PLAYBACK_STATUSES } from '../../constants';
|
||||||
|
|
||||||
|
import AbstractVideoManager, {
|
||||||
|
_mapStateToProps,
|
||||||
|
Props
|
||||||
|
} from './AbstractVideoManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager of youtube shared video.
|
||||||
|
*/
|
||||||
|
class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||||
|
/**
|
||||||
|
* Initializes a new VideoManager instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - This component's props.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
paused: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.playerRef = React.createRef();
|
||||||
|
this._onReady = this._onReady.bind(this);
|
||||||
|
this._onChangeState = this._onChangeState.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.player?.getCurrentTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeks video to provided time.
|
||||||
|
*
|
||||||
|
* @param {number} time - The time to seek to.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
seek(time) {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.seekTo(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays video.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
play() {
|
||||||
|
this.setState({
|
||||||
|
paused: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses video.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
pause() {
|
||||||
|
this.setState({
|
||||||
|
paused: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles state change event.
|
||||||
|
*
|
||||||
|
* @param {string} event - State event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onChangeState(event) {
|
||||||
|
if (event === 'paused') {
|
||||||
|
this.setState({
|
||||||
|
paused: true
|
||||||
|
}, () => {
|
||||||
|
this.onPause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'playing') {
|
||||||
|
this.setState({
|
||||||
|
paused: false
|
||||||
|
}, () => {
|
||||||
|
this.onPlay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles onReady event.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onReady() {
|
||||||
|
this.setState({
|
||||||
|
paused: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves video tag params.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
getPlayerOptions() {
|
||||||
|
const { _isOwner, videoId, width, height } = this.props;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
height,
|
||||||
|
initialPlayerParams: {
|
||||||
|
controls: _isOwner,
|
||||||
|
modestbranding: true,
|
||||||
|
preventFullScreen: true
|
||||||
|
},
|
||||||
|
play: !this.state.paused,
|
||||||
|
ref: this.playerRef,
|
||||||
|
videoId,
|
||||||
|
volume: 50,
|
||||||
|
webViewProps: {
|
||||||
|
bounces: false,
|
||||||
|
mediaPlaybackRequiresUserAction: false,
|
||||||
|
scrollEnabled: false,
|
||||||
|
userAgent: webviewUserAgent
|
||||||
|
},
|
||||||
|
width
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_isOwner) {
|
||||||
|
options.onChangeState = this._onChangeState;
|
||||||
|
options.onReady = this._onReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React Component's render.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (<Video
|
||||||
|
ref = { this.playerRef }
|
||||||
|
{ ...this.getPlayerOptions() } />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_mapStateToProps)(YoutubeVideoManager);
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
export { default as SharedVideoButton } from './SharedVideoButton';
|
export { default as SharedVideoButton } from './SharedVideoButton';
|
||||||
export { default as SharedVideoDialog } from './SharedVideoDialog';
|
export { default as SharedVideoDialog } from './SharedVideoDialog';
|
||||||
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';
|
export { default as SharedVideo } from './SharedVideo';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* The style of toolbar buttons.
|
* The style of toolbar buttons.
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
youtubeVideoContainer: {
|
videoContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable no-invalid-this */
|
/* eslint-disable no-invalid-this */
|
||||||
import Logger from 'jitsi-meet-logger';
|
import Logger from 'jitsi-meet-logger';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { Component } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
|
import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
|
||||||
import { getCurrentConference } from '../../../base/conference';
|
import { getCurrentConference } from '../../../base/conference';
|
||||||
|
@ -13,11 +13,7 @@ import { showWarningNotification } from '../../../notifications/actions';
|
||||||
import { dockToolbox } from '../../../toolbox/actions.web';
|
import { dockToolbox } from '../../../toolbox/actions.web';
|
||||||
import { muteLocal } from '../../../video-menu/actions.any';
|
import { muteLocal } from '../../../video-menu/actions.any';
|
||||||
import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
|
import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
|
||||||
export const PLAYBACK_STATES = {
|
import { PLAYBACK_STATUSES } from '../../constants';
|
||||||
PLAYING: 'playing',
|
|
||||||
PAUSED: 'pause',
|
|
||||||
STOPPED: 'stop'
|
|
||||||
};
|
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
const logger = Logger.getLogger(__filename);
|
||||||
|
|
||||||
|
@ -34,7 +30,7 @@ function shouldSeekToPosition(newTime, previousTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
|
* The type of the React {@link PureComponent} props of {@link AbstractVideoManager}.
|
||||||
*/
|
*/
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
|
||||||
|
@ -115,7 +111,7 @@ export type Props = {
|
||||||
/**
|
/**
|
||||||
* Manager of shared video.
|
* Manager of shared video.
|
||||||
*/
|
*/
|
||||||
class AbstractVideoManager extends Component<Props> {
|
class AbstractVideoManager extends PureComponent<Props> {
|
||||||
throttledFireUpdateSharedVideoEvent: Function;
|
throttledFireUpdateSharedVideoEvent: Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -190,12 +186,12 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
this.seek(_time);
|
this.seek(_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getPlaybackState() !== _status) {
|
if (this.getPlaybackStatus() !== _status) {
|
||||||
if (_status === PLAYBACK_STATES.PLAYING) {
|
if (_status === PLAYBACK_STATUSES.PLAYING) {
|
||||||
this.play();
|
this.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_status === PLAYBACK_STATES.PAUSED) {
|
if (_status === PLAYBACK_STATUSES.PAUSED) {
|
||||||
this.pause();
|
this.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +266,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
fireUpdatePlayingVideoEvent() {
|
fireUpdatePlayingVideoEvent() {
|
||||||
if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) {
|
if (this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING) {
|
||||||
this.fireUpdateSharedVideoEvent();
|
this.fireUpdateSharedVideoEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,9 +283,9 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = this.getPlaybackState();
|
const status = this.getPlaybackStatus();
|
||||||
|
|
||||||
if (!Object.values(PLAYBACK_STATES).includes(status)) {
|
if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +313,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
* currently on.
|
* currently on.
|
||||||
*/
|
*/
|
||||||
isSharedVideoVolumeOn() {
|
isSharedVideoVolumeOn() {
|
||||||
return this.getPlaybackState() === PLAYBACK_STATES.PLAYING
|
return this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING
|
||||||
&& !this.isMuted()
|
&& !this.isMuted()
|
||||||
&& this.getVolume() > 0;
|
&& this.getVolume() > 0;
|
||||||
}
|
}
|
||||||
|
@ -347,7 +343,7 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Indicates the playback state of the video
|
* Indicates the playback state of the video
|
||||||
*/
|
*/
|
||||||
getPlaybackState: () => boolean;
|
getPlaybackStatus: () => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether the video is muted
|
* Indicates whether the video is muted
|
||||||
|
@ -359,11 +355,6 @@ class AbstractVideoManager extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
getVolume: () => number;
|
getVolume: () => number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets current volume
|
|
||||||
*/
|
|
||||||
setVolume: (value: number) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays video
|
* Plays video
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { translate } from '../../../base/i18n';
|
||||||
import { getFieldValue } from '../../../base/react';
|
import { getFieldValue } from '../../../base/react';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { defaultSharedVideoLink } from '../../constants';
|
import { defaultSharedVideoLink } from '../../constants';
|
||||||
import { getYoutubeId } from '../../functions';
|
|
||||||
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +59,7 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
_onSubmitValue() {
|
_onSubmitValue() {
|
||||||
return this._onSetVideoLink(this.state.value);
|
return super._onSetVideoLink(this.state.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,29 +93,6 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onChange: Object => void;
|
_onChange: Object => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { PLAYBACK_STATUSES } from '../../constants';
|
||||||
|
|
||||||
import AbstractVideoManager, {
|
import AbstractVideoManager, {
|
||||||
_mapDispatchToProps,
|
_mapDispatchToProps,
|
||||||
_mapStateToProps,
|
_mapStateToProps,
|
||||||
PLAYBACK_STATES,
|
|
||||||
Props
|
Props
|
||||||
} from './AbstractVideoManager';
|
} from './AbstractVideoManager';
|
||||||
|
|
||||||
|
@ -39,20 +39,20 @@ class VideoManager extends AbstractVideoManager<Props> {
|
||||||
*
|
*
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getPlaybackState() {
|
getPlaybackStatus() {
|
||||||
let state;
|
let status;
|
||||||
|
|
||||||
if (!this.player) {
|
if (!this.player) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.player.paused) {
|
if (this.player.paused) {
|
||||||
state = PLAYBACK_STATES.PAUSED;
|
status = PLAYBACK_STATUSES.PAUSED;
|
||||||
} else {
|
} else {
|
||||||
state = PLAYBACK_STATES.PLAYING;
|
status = PLAYBACK_STATUSES.PLAYING;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,19 +73,6 @@ class VideoManager extends AbstractVideoManager<Props> {
|
||||||
return this.player?.volume;
|
return this.player?.volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets player volume.
|
|
||||||
*
|
|
||||||
* @param {number} value - The volume.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
setVolume(value) {
|
|
||||||
if (this.player) {
|
|
||||||
this.player.volume = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves current time.
|
* Retrieves current time.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,11 +3,11 @@ import React from 'react';
|
||||||
import YouTube from 'react-youtube';
|
import YouTube from 'react-youtube';
|
||||||
|
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { PLAYBACK_STATUSES } from '../../constants';
|
||||||
|
|
||||||
import AbstractVideoManager, {
|
import AbstractVideoManager, {
|
||||||
_mapDispatchToProps,
|
_mapDispatchToProps,
|
||||||
_mapStateToProps,
|
_mapStateToProps
|
||||||
PLAYBACK_STATES
|
|
||||||
} from './AbstractVideoManager';
|
} from './AbstractVideoManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,8 +34,8 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||||
*
|
*
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getPlaybackState() {
|
getPlaybackStatus() {
|
||||||
let state;
|
let status;
|
||||||
|
|
||||||
if (!this.player) {
|
if (!this.player) {
|
||||||
return;
|
return;
|
||||||
|
@ -44,14 +44,14 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||||
const playerState = this.player.getPlayerState();
|
const playerState = this.player.getPlayerState();
|
||||||
|
|
||||||
if (playerState === YouTube.PlayerState.PLAYING) {
|
if (playerState === YouTube.PlayerState.PLAYING) {
|
||||||
state = PLAYBACK_STATES.PLAYING;
|
status = PLAYBACK_STATUSES.PLAYING;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerState === YouTube.PlayerState.PAUSED) {
|
if (playerState === YouTube.PlayerState.PAUSED) {
|
||||||
state = PLAYBACK_STATES.PAUSED;
|
status = PLAYBACK_STATUSES.PAUSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,17 +72,6 @@ class YoutubeVideoManager extends AbstractVideoManager<Props> {
|
||||||
return this.player?.getVolume();
|
return this.player?.getVolume();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets player volume.
|
|
||||||
*
|
|
||||||
* @param {number} value - The volume.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
setVolume(value) {
|
|
||||||
return this.player?.setVolume(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves current time.
|
* Retrieves current time.
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,11 +6,6 @@
|
||||||
*/
|
*/
|
||||||
export const defaultSharedVideoLink = 'Youtube link or direct video link';
|
export const defaultSharedVideoLink = 'Youtube link or direct video link';
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile example for a youtube video
|
|
||||||
*/
|
|
||||||
export const defaultMobileSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixed name of the video player fake participant.
|
* Fixed name of the video player fake participant.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -29,3 +24,12 @@ export const YOUTUBE_PLAYER_PARTICIPANT_NAME = 'YouTube';
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
export const SHARED_VIDEO = 'shared-video';
|
export const SHARED_VIDEO = 'shared-video';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available playback statuses
|
||||||
|
*/
|
||||||
|
export const PLAYBACK_STATUSES = {
|
||||||
|
PLAYING: 'playing',
|
||||||
|
PAUSED: 'pause',
|
||||||
|
STOPPED: 'stop'
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue