Browse Source

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
master
Avram Tudor 3 years ago
parent
commit
e421a119e1
No account linked to committer's email address

+ 1
- 0
android/sdk/build.gradle View File

74
     implementation project(':react-native-sound')
74
     implementation project(':react-native-sound')
75
     implementation project(':react-native-splash-screen')
75
     implementation project(':react-native-splash-screen')
76
     implementation project(':react-native-svg')
76
     implementation project(':react-native-svg')
77
+    implementation project(':react-native-video')
77
     implementation project(':react-native-webrtc')
78
     implementation project(':react-native-webrtc')
78
     implementation project(':react-native-webview')
79
     implementation project(':react-native-webview')
79
 
80
 

+ 1
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java View File

191
                 new com.reactnativecommunity.webview.RNCWebViewPackage(),
191
                 new com.reactnativecommunity.webview.RNCWebViewPackage(),
192
                 new com.rnimmersive.RNImmersivePackage(),
192
                 new com.rnimmersive.RNImmersivePackage(),
193
                 new com.zmxv.RNSound.RNSoundPackage(),
193
                 new com.zmxv.RNSound.RNSoundPackage(),
194
+                new com.brentvatne.react.ReactVideoPackage(),
194
                 new ReactPackageAdapter() {
195
                 new ReactPackageAdapter() {
195
                     @Override
196
                     @Override
196
                     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
197
                     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {

+ 2
- 0
android/settings.gradle View File

27
 project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
27
 project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
28
 include ':react-native-svg'
28
 include ':react-native-svg'
29
 project(':react-native-svg').projectDir = new File(rootProject.projectDir, 	'../node_modules/react-native-svg/android')
29
 project(':react-native-svg').projectDir = new File(rootProject.projectDir, 	'../node_modules/react-native-svg/android')
30
+include ':react-native-video'
31
+project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
30
 include ':react-native-webrtc'
32
 include ':react-native-webrtc'
31
 project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
33
 project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
32
 include ':react-native-webview'
34
 include ':react-native-webview'

+ 1
- 0
ios/Podfile View File

60
   pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
60
   pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
61
   pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
61
   pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
62
   pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
62
   pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
63
+  pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'
63
   pod 'react-native-webview', :path => '../node_modules/react-native-webview'
64
   pod 'react-native-webview', :path => '../node_modules/react-native-webview'
64
   pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
65
   pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
65
   pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
66
   pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'

+ 10
- 1
ios/Podfile.lock View File

288
     - React
288
     - React
289
   - react-native-splash-screen (3.2.0):
289
   - react-native-splash-screen (3.2.0):
290
     - React
290
     - React
291
+  - react-native-video (5.1.1):
292
+    - React-Core
293
+    - react-native-video/Video (= 5.1.1)
294
+  - react-native-video/Video (5.1.1):
295
+    - React-Core
291
   - react-native-webrtc (1.89.3):
296
   - react-native-webrtc (1.89.3):
292
     - React-Core
297
     - React-Core
293
   - react-native-webview (11.0.2):
298
   - react-native-webview (11.0.2):
394
   - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
399
   - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
395
   - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
400
   - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
396
   - react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
401
   - react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
402
+  - react-native-video (from `../node_modules/react-native-video/react-native-video.podspec`)
397
   - react-native-webrtc (from `../node_modules/react-native-webrtc`)
403
   - react-native-webrtc (from `../node_modules/react-native-webrtc`)
398
   - react-native-webview (from `../node_modules/react-native-webview`)
404
   - react-native-webview (from `../node_modules/react-native-webview`)
399
   - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
405
   - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
478
     :path: "../node_modules/@react-native-community/slider"
484
     :path: "../node_modules/@react-native-community/slider"
479
   react-native-splash-screen:
485
   react-native-splash-screen:
480
     :path: "../node_modules/react-native-splash-screen"
486
     :path: "../node_modules/react-native-splash-screen"
487
+  react-native-video:
488
+    :path: "../node_modules/react-native-video/react-native-video.podspec"
481
   react-native-webrtc:
489
   react-native-webrtc:
482
     :path: "../node_modules/react-native-webrtc"
490
     :path: "../node_modules/react-native-webrtc"
483
   react-native-webview:
491
   react-native-webview:
559
   react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
567
   react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
560
   react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
568
   react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
561
   react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
569
   react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
570
+  react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
562
   react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
571
   react-native-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
563
   react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
572
   react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
564
   React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
573
   React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
580
   RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
589
   RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
581
   Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
590
   Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
582
 
591
 
583
-PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49
592
+PODFILE CHECKSUM: f4db44d934caeae7212dbaa33abe62ed164363e8
584
 
593
 
585
 COCOAPODS: 1.10.1
594
 COCOAPODS: 1.10.1

+ 41
- 5
package-lock.json View File

7539
         }
7539
         }
7540
       }
7540
       }
7541
     },
7541
     },
7542
+    "eme-encryption-scheme-polyfill": {
7543
+      "version": "2.0.3",
7544
+      "resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz",
7545
+      "integrity": "sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg=="
7546
+    },
7542
     "emoji-regex": {
7547
     "emoji-regex": {
7543
       "version": "6.5.1",
7548
       "version": "6.5.1",
7544
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
7549
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
8244
     "events": {
8249
     "events": {
8245
       "version": "3.1.0",
8250
       "version": "3.1.0",
8246
       "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
8251
       "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
8247
-      "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
8252
+      "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
8253
+      "dev": true
8248
     },
8254
     },
8249
     "eventsource": {
8255
     "eventsource": {
8250
       "version": "1.0.7",
8256
       "version": "1.0.7",
11016
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
11022
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
11017
       "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
11023
       "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
11018
     },
11024
     },
11025
+    "keymirror": {
11026
+      "version": "0.1.1",
11027
+      "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
11028
+      "integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
11029
+    },
11019
     "killable": {
11030
     "killable": {
11020
       "version": "1.0.1",
11031
       "version": "1.0.1",
11021
       "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
11032
       "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
15143
         "whatwg-url-without-unicode": "8.0.0-3"
15154
         "whatwg-url-without-unicode": "8.0.0-3"
15144
       }
15155
       }
15145
     },
15156
     },
15157
+    "react-native-video": {
15158
+      "version": "5.1.1",
15159
+      "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.1.1.tgz",
15160
+      "integrity": "sha512-zee8gRUrjPWRoZSEBiMebClqu1iAuCQNLjzqpmXFrRWEoJj7azM3BPqLQWJgsnfLiYUYGySeApC/G60THM5+tw==",
15161
+      "requires": {
15162
+        "keymirror": "^0.1.1",
15163
+        "prop-types": "^15.7.2",
15164
+        "shaka-player": "^2.5.9"
15165
+      }
15166
+    },
15146
     "react-native-watch-connectivity": {
15167
     "react-native-watch-connectivity": {
15147
       "version": "0.4.3",
15168
       "version": "0.4.3",
15148
       "resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz",
15169
       "resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz",
15192
       }
15213
       }
15193
     },
15214
     },
15194
     "react-native-youtube-iframe": {
15215
     "react-native-youtube-iframe": {
15195
-      "version": "1.2.3",
15196
-      "resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
15197
-      "integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
15216
+      "version": "2.1.1",
15217
+      "resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.1.1.tgz",
15218
+      "integrity": "sha512-vnLzA5zcnMwa1gMqGfvkjaE82NW1Nd2Up4Q1OUz6IKm69xSG/9/m4APZ5fCN8UMhy6lH95iagd497J7jwEwz3w==",
15198
       "requires": {
15219
       "requires": {
15199
-        "events": "^3.0.0"
15220
+        "events": "^3.2.0"
15221
+      },
15222
+      "dependencies": {
15223
+        "events": {
15224
+          "version": "3.3.0",
15225
+          "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
15226
+          "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
15227
+        }
15200
       }
15228
       }
15201
     },
15229
     },
15202
     "react-node-resolver": {
15230
     "react-node-resolver": {
16070
         "safe-buffer": "^5.0.1"
16098
         "safe-buffer": "^5.0.1"
16071
       }
16099
       }
16072
     },
16100
     },
16101
+    "shaka-player": {
16102
+      "version": "2.5.22",
16103
+      "resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.22.tgz",
16104
+      "integrity": "sha512-PAoeNLUQ/hT/9dY7QvNFgIiDtXSqbYVFuXXtLHh7ytVVqTvI/p4HLwfYShiR+sE/sbsDOr9D5l9D/ztLPhxgtw==",
16105
+      "requires": {
16106
+        "eme-encryption-scheme-polyfill": "^2.0.1"
16107
+      }
16108
+    },
16073
     "shallow-clone": {
16109
     "shallow-clone": {
16074
       "version": "0.1.2",
16110
       "version": "0.1.2",
16075
       "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
16111
       "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",

+ 2
- 1
package.json View File

87
     "react-native-svg": "12.1.0",
87
     "react-native-svg": "12.1.0",
88
     "react-native-svg-transformer": "0.14.3",
88
     "react-native-svg-transformer": "0.14.3",
89
     "react-native-url-polyfill": "1.2.0",
89
     "react-native-url-polyfill": "1.2.0",
90
+    "react-native-video": "5.1.1",
90
     "react-native-watch-connectivity": "0.4.3",
91
     "react-native-watch-connectivity": "0.4.3",
91
     "react-native-webrtc": "1.89.3",
92
     "react-native-webrtc": "1.89.3",
92
     "react-native-webview": "11.0.2",
93
     "react-native-webview": "11.0.2",
93
-    "react-native-youtube-iframe": "1.2.3",
94
+    "react-native-youtube-iframe": "2.1.1",
94
     "react-redux": "7.1.0",
95
     "react-redux": "7.1.0",
95
     "react-textarea-autosize": "8.3.0",
96
     "react-textarea-autosize": "8.3.0",
96
     "react-transition-group": "2.4.0",
97
     "react-transition-group": "2.4.0",

+ 6
- 6
react/features/base/participants/components/ParticipantView.native.js View File

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 import { Text, View } from 'react-native';
4
 import { Text, View } from 'react-native';
5
 
5
 
6
-import { YoutubeLargeVideo } from '../../../shared-video/components';
6
+import { SharedVideo } from '../../../shared-video/components/native';
7
 import { Avatar } from '../../avatar';
7
 import { Avatar } from '../../avatar';
8
 import { translate } from '../../i18n';
8
 import { translate } from '../../i18n';
9
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
9
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
208
                 ? this.props.testHintId
208
                 ? this.props.testHintId
209
                 : `org.jitsi.meet.Participant#${this.props.participantId}`;
209
                 : `org.jitsi.meet.Participant#${this.props.participantId}`;
210
 
210
 
211
-        const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
211
+        const renderSharedVideo = _isFakeParticipant && !disableVideo;
212
 
212
 
213
         return (
213
         return (
214
             <Container
214
             <Container
215
-                onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
215
+                onClick = { renderVideo || renderSharedVideo ? undefined : onPress }
216
                 style = {{
216
                 style = {{
217
                     ...styles.participantView,
217
                     ...styles.participantView,
218
                     ...this.props.style
218
                     ...this.props.style
221
 
221
 
222
                 <TestHint
222
                 <TestHint
223
                     id = { testHintId }
223
                     id = { testHintId }
224
-                    onPress = { renderYoutubeLargeVideo ? undefined : onPress }
224
+                    onPress = { renderSharedVideo ? undefined : onPress }
225
                     value = '' />
225
                     value = '' />
226
 
226
 
227
-                { renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
227
+                { renderSharedVideo && <SharedVideo /> }
228
 
228
 
229
                 { !_isFakeParticipant && renderVideo
229
                 { !_isFakeParticipant && renderVideo
230
                     && <VideoTrack
230
                     && <VideoTrack
234
                         zOrder = { this.props.zOrder }
234
                         zOrder = { this.props.zOrder }
235
                         zoomEnabled = { this.props.zoomEnabled } /> }
235
                         zoomEnabled = { this.props.zoomEnabled } /> }
236
 
236
 
237
-                { !renderYoutubeLargeVideo && !renderVideo
237
+                { !renderSharedVideo && !renderVideo
238
                     && <View style = { styles.avatarContainer }>
238
                     && <View style = { styles.avatarContainer }>
239
                         <Avatar
239
                         <Avatar
240
                             participantId = { this.props.participantId }
240
                             participantId = { this.props.participantId }

+ 25
- 0
react/features/shared-video/components/AbstractSharedVideoDialog.js View File

3
 import { Component } from 'react';
3
 import { Component } from 'react';
4
 import type { Dispatch } from 'redux';
4
 import type { Dispatch } from 'redux';
5
 
5
 
6
+import { getYoutubeId } from '../functions';
7
+
6
 /**
8
 /**
7
  * The type of the React {@code Component} props of
9
  * The type of the React {@code Component} props of
8
  * {@link AbstractSharedVideoDialog}.
10
  * {@link AbstractSharedVideoDialog}.
42
     }
44
     }
43
 
45
 
44
     _onSetVideoLink: string => boolean;
46
     _onSetVideoLink: string => boolean;
47
+
48
+    /**
49
+     * Validates the entered video link by extracting the id and dispatches it.
50
+     *
51
+     * It returns a boolean to comply the Dialog behaviour:
52
+     *     {@code true} - the dialog should be closed.
53
+     *     {@code false} - the dialog should be left open.
54
+     *
55
+    * @param {string} link - The entered video link.
56
+     * @returns {boolean}
57
+     */
58
+    _onSetVideoLink(link: string) {
59
+        if (!link || !link.trim()) {
60
+            return false;
61
+        }
62
+
63
+        const youtubeId = getYoutubeId(link);
64
+        const { onPostSubmit } = this.props;
65
+
66
+        onPostSubmit(youtubeId || link);
67
+
68
+        return true;
69
+    }
45
 }
70
 }

+ 261
- 0
react/features/shared-video/components/native/AbstractVideoManager.js View File

1
+/* @flow */
2
+/* eslint-disable no-invalid-this */
3
+
4
+import throttle from 'lodash/throttle';
5
+import { PureComponent } from 'react';
6
+
7
+import { getCurrentConference } from '../../../base/conference';
8
+import { getLocalParticipant } from '../../../base/participants';
9
+import { setSharedVideoStatus } from '../../actions.any';
10
+import { PLAYBACK_STATUSES } from '../../constants';
11
+
12
+/**
13
+ * Return true if the diffenrece between the two timees is larger than 5.
14
+ *
15
+ * @param {number} newTime - The current time.
16
+ * @param {number} previousTime - The previous time.
17
+ * @private
18
+ * @returns {boolean}
19
+*/
20
+function shouldSeekToPosition(newTime, previousTime) {
21
+    return Math.abs(newTime - previousTime) > 5;
22
+}
23
+
24
+/**
25
+ * The type of the React {@link Component} props of {@link AbstractVideoManager}.
26
+ */
27
+export type Props = {
28
+
29
+    /**
30
+     * The current coference
31
+     */
32
+    _conference: Object,
33
+
34
+    /**
35
+     * Is the video shared by the local user.
36
+     *
37
+     * @private
38
+     */
39
+    _isOwner: boolean,
40
+
41
+    /**
42
+     * The shared video owner id
43
+     */
44
+    _ownerId: string,
45
+
46
+    /**
47
+     * The shared video status
48
+     */
49
+     _status: string,
50
+
51
+    /**
52
+     * Seek time in seconds.
53
+     *
54
+     */
55
+    _time: number,
56
+
57
+    /**
58
+     * The video url
59
+     */
60
+     _videoUrl: string,
61
+
62
+    /**
63
+     * The Redux dispatch function.
64
+     */
65
+     dispatch: Function,
66
+
67
+    /**
68
+      * The player's height
69
+    */
70
+    height: number,
71
+
72
+    /**
73
+      * The video id
74
+    */
75
+    videoId: string,
76
+
77
+    /**
78
+      * The player's width
79
+    */
80
+    width: number
81
+}
82
+
83
+/**
84
+ * Manager of shared video.
85
+ */
86
+class AbstractVideoManager extends PureComponent<Props> {
87
+    throttledFireUpdateSharedVideoEvent: Function;
88
+
89
+    /**
90
+     * Initializes a new instance of AbstractVideoManager.
91
+     *
92
+     * @returns {void}
93
+     */
94
+    constructor() {
95
+        super();
96
+
97
+        this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000);
98
+    }
99
+
100
+    /**
101
+     * Implements React Component's componentDidMount.
102
+     *
103
+     * @inheritdoc
104
+     */
105
+    componentDidMount() {
106
+        this.processUpdatedProps();
107
+    }
108
+
109
+    /**
110
+     * Implements React Component's componentDidUpdate.
111
+     *
112
+     * @inheritdoc
113
+     */
114
+    componentDidUpdate() {
115
+        this.processUpdatedProps();
116
+    }
117
+
118
+    /**
119
+     * Implements React Component's componentWillUnmount.
120
+     *
121
+     * @inheritdoc
122
+     */
123
+    componentWillUnmount() {
124
+        if (this.dispose) {
125
+            this.dispose();
126
+        }
127
+    }
128
+
129
+    /**
130
+     * Processes new properties.
131
+     *
132
+     * @returns {void}
133
+     */
134
+    async processUpdatedProps() {
135
+        const { _status, _time, _isOwner } = this.props;
136
+
137
+        if (_isOwner) {
138
+            return;
139
+        }
140
+
141
+        const playerTime = await this.getTime();
142
+
143
+        if (shouldSeekToPosition(_time, playerTime)) {
144
+            this.seek(_time);
145
+        }
146
+
147
+        if (this.getPlaybackStatus() !== _status) {
148
+            if (_status === PLAYBACK_STATUSES.PLAYING) {
149
+                this.play();
150
+            } else if (_status === PLAYBACK_STATUSES.PAUSED) {
151
+                this.pause();
152
+            }
153
+        }
154
+    }
155
+
156
+    /**
157
+     * Handle video playing.
158
+     *
159
+     * @returns {void}
160
+     */
161
+    onPlay() {
162
+        this.fireUpdateSharedVideoEvent();
163
+    }
164
+
165
+    /**
166
+     * Handle video paused.
167
+     *
168
+     * @returns {void}
169
+     */
170
+    onPause() {
171
+        this.fireUpdateSharedVideoEvent();
172
+    }
173
+
174
+    /**
175
+     * Dispatches an update action for the shared video.
176
+     *
177
+     * @returns {void}
178
+     */
179
+    async fireUpdateSharedVideoEvent() {
180
+        const { _isOwner } = this.props;
181
+
182
+        if (!_isOwner) {
183
+            return;
184
+        }
185
+
186
+        const status = this.getPlaybackStatus();
187
+
188
+        if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
189
+            return;
190
+        }
191
+
192
+        const time = await this.getTime();
193
+
194
+        const {
195
+            _ownerId,
196
+            _videoUrl,
197
+            dispatch
198
+        } = this.props;
199
+
200
+        dispatch(setSharedVideoStatus({
201
+            videoUrl: _videoUrl,
202
+            status,
203
+            time,
204
+            ownerId: _ownerId
205
+        }));
206
+    }
207
+
208
+    /**
209
+     * Seeks video to provided time
210
+     * @param {number} time
211
+     */
212
+    seek: (time: number) => void;
213
+
214
+    /**
215
+     * Indicates the playback state of the video
216
+     */
217
+    getPlaybackStatus: () => boolean;
218
+
219
+    /**
220
+     * Plays video
221
+     */
222
+    play: () => void;
223
+
224
+    /**
225
+     * Pauses video
226
+     */
227
+    pause: () => void;
228
+
229
+    /**
230
+     * Retrieves current time
231
+     */
232
+    getTime: () => number;
233
+
234
+    /**
235
+     * Disposes current video player
236
+     */
237
+    dispose: () => void;
238
+}
239
+
240
+
241
+export default AbstractVideoManager;
242
+
243
+/**
244
+ * Maps part of the Redux store to the props of this component.
245
+ *
246
+ * @param {Object} state - The Redux state.
247
+ * @returns {Props}
248
+ */
249
+export function _mapStateToProps(state: Object): $Shape<Props> {
250
+    const { ownerId, status, time, videoUrl } = state['features/shared-video'];
251
+    const localParticipant = getLocalParticipant(state);
252
+
253
+    return {
254
+        _conference: getCurrentConference(state),
255
+        _isOwner: ownerId === localParticipant.id,
256
+        _ownerId: ownerId,
257
+        _status: status,
258
+        _time: time,
259
+        _videoUrl: videoUrl
260
+    };
261
+}

+ 168
- 0
react/features/shared-video/components/native/SharedVideo.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { View } from 'react-native';
5
+
6
+import { getLocalParticipant } from '../../../base/participants';
7
+import { connect } from '../../../base/redux';
8
+import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
9
+import { setToolboxVisible } from '../../../toolbox/actions';
10
+
11
+import VideoManager from './VideoManager';
12
+import YoutubeVideoManager from './YoutubeVideoManager';
13
+import styles from './styles';
14
+
15
+type Props = {
16
+
17
+    /**
18
+     * The Redux dispatch function.
19
+     */
20
+     dispatch: Function,
21
+
22
+    /**
23
+     * Is the video shared by the local user.
24
+     *
25
+     * @private
26
+     */
27
+    isOwner: boolean,
28
+
29
+    /**
30
+     * True if in landscape mode.
31
+     *
32
+     * @private
33
+     */
34
+     isWideScreen: boolean,
35
+
36
+    /**
37
+     * The available player width
38
+     */
39
+    playerHeight: number,
40
+
41
+    /**
42
+     * The available player width
43
+     */
44
+    playerWidth: number,
45
+
46
+    /**
47
+     * The shared video url
48
+     */
49
+     videoUrl: string,
50
+}
51
+
52
+/**
53
+ * Implements a React {@link Component} which represents the large video (a.k.a.
54
+ * the conference participant who is on the local stage) on Web/React.
55
+ *
56
+ * @extends Component
57
+ */
58
+class SharedVideo extends Component<Props> {
59
+    /**
60
+     * Initializes a new {@code SharedVideo} instance.
61
+     *
62
+     * @param {Object} props - The properties.
63
+     */
64
+    constructor(props: Props) {
65
+        super(props);
66
+
67
+        this.setWideScreenMode(props.isWideScreen);
68
+    }
69
+
70
+    /**
71
+     * Implements React's {@link Component#componentDidUpdate()}.
72
+     *
73
+     * @inheritdoc
74
+     * @returns {void}
75
+     */
76
+    componentDidUpdate(prevProps: Props) {
77
+        const { isWideScreen } = this.props;
78
+
79
+        if (isWideScreen !== prevProps.isWideScreen) {
80
+            this.setWideScreenMode(isWideScreen);
81
+        }
82
+    }
83
+
84
+    /**
85
+     * Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise.
86
+     *
87
+     * @param {isWideScreen} isWideScreen - Whether the screen is wide.
88
+     * @private
89
+     * @returns {void}
90
+    */
91
+    setWideScreenMode(isWideScreen) {
92
+        this.props.dispatch(setToolboxVisible(!isWideScreen));
93
+    }
94
+
95
+    /**
96
+     * Implements React's {@link Component#render()}.
97
+     *
98
+     * @inheritdoc
99
+     * @returns {React$Element}
100
+     */
101
+    render() {
102
+        const {
103
+            isOwner,
104
+            playerHeight,
105
+            playerWidth,
106
+            videoUrl
107
+        } = this.props;
108
+
109
+        if (!videoUrl) {
110
+            return null;
111
+        }
112
+
113
+        return (
114
+            <View
115
+                pointerEvents = { isOwner ? 'auto' : 'none' }
116
+                style = { styles.videoContainer } >
117
+                {videoUrl.match(/http/)
118
+                    ? (
119
+                        <VideoManager
120
+                            height = { playerHeight }
121
+                            videoId = { videoUrl }
122
+                            width = { playerWidth } />
123
+                    ) : (
124
+                        <YoutubeVideoManager
125
+                            height = { playerHeight }
126
+                            videoId = { videoUrl }
127
+                            width = { playerWidth } />
128
+                    )
129
+                }
130
+            </View>
131
+        );
132
+    }
133
+}
134
+
135
+/**
136
+ * Maps (parts of) the Redux state to the associated LargeVideo props.
137
+ *
138
+ * @param {Object} state - The Redux state.
139
+ * @private
140
+ * @returns {Props}
141
+ */
142
+function _mapStateToProps(state) {
143
+    const { ownerId, videoUrl } = state['features/shared-video'];
144
+    const { aspectRatio, clientHeight, clientWidth } = state['features/base/responsive-ui'];
145
+
146
+    const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
147
+    const localParticipant = getLocalParticipant(state);
148
+
149
+    let playerHeight, playerWidth;
150
+
151
+    if (isWideScreen) {
152
+        playerHeight = clientHeight;
153
+        playerWidth = playerHeight * 16 / 9;
154
+    } else {
155
+        playerWidth = clientWidth;
156
+        playerHeight = playerWidth * 9 / 16;
157
+    }
158
+
159
+    return {
160
+        isOwner: ownerId === localParticipant.id,
161
+        isWideScreen,
162
+        playerHeight,
163
+        playerWidth,
164
+        videoUrl
165
+    };
166
+}
167
+
168
+export default connect(_mapStateToProps)(SharedVideo);

+ 25
- 32
react/features/shared-video/components/native/SharedVideoDialog.js View File

4
 
4
 
5
 import { InputDialog } from '../../../base/dialog';
5
 import { InputDialog } from '../../../base/dialog';
6
 import { connect } from '../../../base/redux';
6
 import { connect } from '../../../base/redux';
7
-import { defaultMobileSharedVideoLink } from '../../constants';
8
-import { getYoutubeId } from '../../functions';
7
+import { defaultSharedVideoLink } from '../../constants';
9
 import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
8
 import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
10
 
9
 
11
 /**
10
 /**
12
  * Implements a component to render a display name prompt.
11
  * Implements a component to render a display name prompt.
13
  */
12
  */
14
 class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
13
 class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
14
+    /**
15
+     * Instantiates a new component.
16
+     *
17
+     * @inheritdoc
18
+     */
19
+    constructor(props) {
20
+        super(props);
21
+
22
+        this._onSubmitValue = this._onSubmitValue.bind(this);
23
+    }
24
+
25
+    _onSubmitValue: () => boolean;
26
+
27
+    /**
28
+     * Callback to be invoked when the value of the link input is submitted.
29
+     *
30
+     * @param {string} value - The entered video link.
31
+     * @returns {boolean}
32
+     */
33
+    _onSubmitValue(value) {
34
+        return super._onSetVideoLink(value);
35
+    }
15
 
36
 
16
     /**
37
     /**
17
      * Implements React's {@link Component#render()}.
38
      * Implements React's {@link Component#render()}.
22
         return (
43
         return (
23
             <InputDialog
44
             <InputDialog
24
                 contentKey = 'dialog.shareVideoTitle'
45
                 contentKey = 'dialog.shareVideoTitle'
25
-                onSubmit = { this._onSetVideoLink }
46
+                onSubmit = { this._onSubmitValue }
26
                 textInputProps = {{
47
                 textInputProps = {{
27
-                    placeholder: defaultMobileSharedVideoLink
48
+                    placeholder: defaultSharedVideoLink
28
                 }} />
49
                 }} />
29
         );
50
         );
30
     }
51
     }
31
-
32
-    /**
33
-     * Validates the entered video link by extracting the id and dispatches it.
34
-     *
35
-     * It returns a boolean to comply the Dialog behaviour:
36
-     *     {@code true} - the dialog should be closed.
37
-     *     {@code false} - the dialog should be left open.
38
-     *
39
-     * @param {string} link - The entered video link.
40
-     * @returns {boolean}
41
-     */
42
-    _onSetVideoLink(link: string) {
43
-        if (!link || !link.trim()) {
44
-            return false;
45
-        }
46
-
47
-        const videoId = getYoutubeId(link);
48
-
49
-        if (videoId) {
50
-            const { onPostSubmit } = this.props;
51
-
52
-            onPostSubmit && onPostSubmit(videoId);
53
-
54
-            return true;
55
-        }
56
-
57
-        return false;
58
-    }
59
 }
52
 }
60
 
53
 
61
 export default connect()(SharedVideoDialog);
54
 export default connect()(SharedVideoDialog);

+ 187
- 0
react/features/shared-video/components/native/VideoManager.js View File

1
+import Logger from 'jitsi-meet-logger';
2
+import React from 'react';
3
+import Video from 'react-native-video';
4
+
5
+import { connect } from '../../../base/redux';
6
+import { PLAYBACK_STATUSES } from '../../constants';
7
+
8
+import AbstractVideoManager, {
9
+    _mapStateToProps,
10
+    Props
11
+} from './AbstractVideoManager';
12
+
13
+const logger = Logger.getLogger(__filename);
14
+
15
+/**
16
+ * Manager of shared video.
17
+ */
18
+class VideoManager extends AbstractVideoManager<Props> {
19
+    /**
20
+     * Initializes a new VideoManager instance.
21
+     *
22
+     * @param {Object} props - This component's props.
23
+     *
24
+     * @returns {void}
25
+     */
26
+    constructor(props) {
27
+        super(props);
28
+
29
+        this.state = {
30
+            currentTime: 0,
31
+            paused: false
32
+        };
33
+
34
+        this.playerRef = React.createRef();
35
+        this.onPlaybackRateChange = this.onPlaybackRateChange.bind(this);
36
+        this.onProgress = this.onProgress.bind(this);
37
+    }
38
+
39
+    /**
40
+     * Retrieves the current player ref.
41
+     */
42
+    get player() {
43
+        return this.playerRef.current;
44
+    }
45
+
46
+    /**
47
+     * Indicates the playback state of the video.
48
+     *
49
+     * @returns {string}
50
+     */
51
+    getPlaybackStatus() {
52
+        let status;
53
+
54
+        if (this.state.paused) {
55
+            status = PLAYBACK_STATUSES.PAUSED;
56
+        } else {
57
+            status = PLAYBACK_STATUSES.PLAYING;
58
+        }
59
+
60
+        return status;
61
+    }
62
+
63
+    /**
64
+     * Retrieves current time.
65
+     *
66
+     * @returns {number}
67
+     */
68
+    getTime() {
69
+        return this.state.currentTime;
70
+    }
71
+
72
+    /**
73
+     * Seeks video to provided time.
74
+     *
75
+     * @param {number} time - The time to seek to.
76
+     *
77
+     * @returns {void}
78
+     */
79
+    seek(time) {
80
+        if (this.player) {
81
+            this.player.seek(time);
82
+        }
83
+    }
84
+
85
+    /**
86
+     * Plays video.
87
+     *
88
+     * @returns {void}
89
+     */
90
+    play() {
91
+        this.setState({
92
+            paused: false
93
+        });
94
+    }
95
+
96
+    /**
97
+     * Pauses video.
98
+     *
99
+     * @returns {void}
100
+     */
101
+    pause() {
102
+        this.setState({
103
+            paused: true
104
+        });
105
+    }
106
+
107
+    /**
108
+     * Handles playback rate changed event.
109
+     *
110
+     * @param {Object} options.playbackRate - Playback rate: 1 - playing, 0 - paused, other - slowed down / sped up.
111
+     * @returns {void}
112
+     */
113
+    onPlaybackRateChange({ playbackRate }) {
114
+        if (playbackRate === 0) {
115
+            this.setState({
116
+                paused: true
117
+            }, () => {
118
+                this.onPause();
119
+            });
120
+        }
121
+
122
+        if (playbackRate === 1) {
123
+            this.setState({
124
+                paused: false
125
+            }, () => {
126
+                this.onPlay();
127
+            });
128
+        }
129
+    }
130
+
131
+    /**
132
+     * Handles progress updarte event.
133
+     *
134
+     * @param {Object} options - Progress event options.
135
+     * @returns {void}
136
+     */
137
+    onProgress(options) {
138
+        this.setState({ currentTime: options.currentTime });
139
+        this.throttledFireUpdateSharedVideoEvent();
140
+    }
141
+
142
+    /**
143
+     * Retrieves video tag params.
144
+     *
145
+     * @returns {void}
146
+     */
147
+    getPlayerOptions() {
148
+        const { _isOwner, videoId, width, height } = this.props;
149
+        const { paused } = this.state;
150
+
151
+        const options = {
152
+            paused,
153
+            progressUpdateInterval: 5000,
154
+            resizeMode: 'cover',
155
+            style: {
156
+                height,
157
+                width
158
+            },
159
+            source: { uri: videoId },
160
+            controls: _isOwner,
161
+            pictureInPicture: false,
162
+            onProgress: this.onProgress,
163
+            onError: event => {
164
+                logger.error('Error in the player:', event);
165
+            }
166
+        };
167
+
168
+        if (_isOwner) {
169
+            options.onPlaybackRateChange = this.onPlaybackRateChange;
170
+        }
171
+
172
+        return options;
173
+    }
174
+
175
+    /**
176
+     * Implements React Component's render.
177
+     *
178
+     * @inheritdoc
179
+     */
180
+    render() {
181
+        return (<Video
182
+            ref = { this.playerRef }
183
+            { ...this.getPlayerOptions() } />);
184
+    }
185
+}
186
+
187
+export default connect(_mapStateToProps)(VideoManager);

+ 0
- 441
react/features/shared-video/components/native/YoutubeLargeVideo.js View File

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

+ 193
- 0
react/features/shared-video/components/native/YoutubeVideoManager.js View File

1
+import React from 'react';
2
+import Video from 'react-native-youtube-iframe';
3
+
4
+import { connect } from '../../../base/redux';
5
+import { PLAYBACK_STATUSES } from '../../constants';
6
+
7
+import AbstractVideoManager, {
8
+    _mapStateToProps,
9
+    Props
10
+} from './AbstractVideoManager';
11
+
12
+/**
13
+ * Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
14
+ *
15
+ * @private
16
+ */
17
+const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len
18
+
19
+/**
20
+ * Manager of youtube shared video.
21
+ */
22
+class YoutubeVideoManager extends AbstractVideoManager<Props> {
23
+    /**
24
+     * Initializes a new VideoManager instance.
25
+     *
26
+     * @param {Object} props - This component's props.
27
+     *
28
+     * @returns {void}
29
+     */
30
+    constructor(props) {
31
+        super(props);
32
+
33
+        this.state = {
34
+            paused: false
35
+        };
36
+
37
+        this.playerRef = React.createRef();
38
+        this._onReady = this._onReady.bind(this);
39
+        this._onChangeState = this._onChangeState.bind(this);
40
+    }
41
+
42
+    /**
43
+     * Retrieves the current player ref.
44
+     */
45
+    get player() {
46
+        return this.playerRef.current;
47
+    }
48
+
49
+    /**
50
+     * Indicates the playback state of the video.
51
+     *
52
+     * @returns {string}
53
+     */
54
+    getPlaybackStatus() {
55
+        let status;
56
+
57
+        if (this.state.paused) {
58
+            status = PLAYBACK_STATUSES.PAUSED;
59
+        } else {
60
+            status = PLAYBACK_STATUSES.PLAYING;
61
+        }
62
+
63
+        return status;
64
+    }
65
+
66
+    /**
67
+     * Retrieves current time.
68
+     *
69
+     * @returns {number}
70
+     */
71
+    getTime() {
72
+        return this.player?.getCurrentTime();
73
+    }
74
+
75
+    /**
76
+     * Seeks video to provided time.
77
+     *
78
+     * @param {number} time - The time to seek to.
79
+     *
80
+     * @returns {void}
81
+     */
82
+    seek(time) {
83
+        if (this.player) {
84
+            this.player.seekTo(time);
85
+        }
86
+    }
87
+
88
+    /**
89
+     * Plays video.
90
+     *
91
+     * @returns {void}
92
+     */
93
+    play() {
94
+        this.setState({
95
+            paused: false
96
+        });
97
+    }
98
+
99
+    /**
100
+     * Pauses video.
101
+     *
102
+     * @returns {void}
103
+     */
104
+    pause() {
105
+        this.setState({
106
+            paused: true
107
+        });
108
+    }
109
+
110
+    /**
111
+     * Handles state change event.
112
+     *
113
+     * @param {string} event - State event.
114
+     * @returns {void}
115
+     */
116
+    _onChangeState(event) {
117
+        if (event === 'paused') {
118
+            this.setState({
119
+                paused: true
120
+            }, () => {
121
+                this.onPause();
122
+            });
123
+        }
124
+
125
+        if (event === 'playing') {
126
+            this.setState({
127
+                paused: false
128
+            }, () => {
129
+                this.onPlay();
130
+            });
131
+        }
132
+    }
133
+
134
+    /**
135
+     * Handles onReady event.
136
+     *
137
+     * @returns {void}
138
+     */
139
+    _onReady() {
140
+        this.setState({
141
+            paused: false
142
+        });
143
+    }
144
+
145
+    /**
146
+     * Retrieves video tag params.
147
+     *
148
+     * @returns {void}
149
+     */
150
+    getPlayerOptions() {
151
+        const { _isOwner, videoId, width, height } = this.props;
152
+
153
+        const options = {
154
+            height,
155
+            initialPlayerParams: {
156
+                controls: _isOwner,
157
+                modestbranding: true,
158
+                preventFullScreen: true
159
+            },
160
+            play: !this.state.paused,
161
+            ref: this.playerRef,
162
+            videoId,
163
+            volume: 50,
164
+            webViewProps: {
165
+                bounces: false,
166
+                mediaPlaybackRequiresUserAction: false,
167
+                scrollEnabled: false,
168
+                userAgent: webviewUserAgent
169
+            },
170
+            width
171
+        };
172
+
173
+        if (_isOwner) {
174
+            options.onChangeState = this._onChangeState;
175
+            options.onReady = this._onReady;
176
+        }
177
+
178
+        return options;
179
+    }
180
+
181
+    /**
182
+     * Implements React Component's render.
183
+     *
184
+     * @inheritdoc
185
+     */
186
+    render() {
187
+        return (<Video
188
+            ref = { this.playerRef }
189
+            { ...this.getPlayerOptions() } />);
190
+    }
191
+}
192
+
193
+export default connect(_mapStateToProps)(YoutubeVideoManager);

+ 1
- 1
react/features/shared-video/components/native/index.js View File

2
 
2
 
3
 export { default as SharedVideoButton } from './SharedVideoButton';
3
 export { default as SharedVideoButton } from './SharedVideoButton';
4
 export { default as SharedVideoDialog } from './SharedVideoDialog';
4
 export { default as SharedVideoDialog } from './SharedVideoDialog';
5
-export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';
5
+export { default as SharedVideo } from './SharedVideo';
6
 
6
 

+ 1
- 1
react/features/shared-video/components/native/styles.js View File

4
  * The style of toolbar buttons.
4
  * The style of toolbar buttons.
5
  */
5
  */
6
 export default {
6
 export default {
7
-    youtubeVideoContainer: {
7
+    videoContainer: {
8
         alignItems: 'center',
8
         alignItems: 'center',
9
         flex: 1,
9
         flex: 1,
10
         flexDirection: 'column',
10
         flexDirection: 'column',

+ 12
- 21
react/features/shared-video/components/web/AbstractVideoManager.js View File

2
 /* eslint-disable no-invalid-this */
2
 /* eslint-disable no-invalid-this */
3
 import Logger from 'jitsi-meet-logger';
3
 import Logger from 'jitsi-meet-logger';
4
 import throttle from 'lodash/throttle';
4
 import throttle from 'lodash/throttle';
5
-import { Component } from 'react';
5
+import { PureComponent } from 'react';
6
 
6
 
7
 import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
7
 import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
8
 import { getCurrentConference } from '../../../base/conference';
8
 import { getCurrentConference } from '../../../base/conference';
13
 import { dockToolbox } from '../../../toolbox/actions.web';
13
 import { dockToolbox } from '../../../toolbox/actions.web';
14
 import { muteLocal } from '../../../video-menu/actions.any';
14
 import { muteLocal } from '../../../video-menu/actions.any';
15
 import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
15
 import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
16
-export const PLAYBACK_STATES = {
17
-    PLAYING: 'playing',
18
-    PAUSED: 'pause',
19
-    STOPPED: 'stop'
20
-};
16
+import { PLAYBACK_STATUSES } from '../../constants';
21
 
17
 
22
 const logger = Logger.getLogger(__filename);
18
 const logger = Logger.getLogger(__filename);
23
 
19
 
34
 }
30
 }
35
 
31
 
36
 /**
32
 /**
37
- * The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
33
+ * The type of the React {@link PureComponent} props of {@link AbstractVideoManager}.
38
  */
34
  */
39
 export type Props = {
35
 export type Props = {
40
 
36
 
115
 /**
111
 /**
116
  * Manager of shared video.
112
  * Manager of shared video.
117
  */
113
  */
118
-class AbstractVideoManager extends Component<Props> {
114
+class AbstractVideoManager extends PureComponent<Props> {
119
     throttledFireUpdateSharedVideoEvent: Function;
115
     throttledFireUpdateSharedVideoEvent: Function;
120
 
116
 
121
     /**
117
     /**
190
             this.seek(_time);
186
             this.seek(_time);
191
         }
187
         }
192
 
188
 
193
-        if (this.getPlaybackState() !== _status) {
194
-            if (_status === PLAYBACK_STATES.PLAYING) {
189
+        if (this.getPlaybackStatus() !== _status) {
190
+            if (_status === PLAYBACK_STATUSES.PLAYING) {
195
                 this.play();
191
                 this.play();
196
             }
192
             }
197
 
193
 
198
-            if (_status === PLAYBACK_STATES.PAUSED) {
194
+            if (_status === PLAYBACK_STATUSES.PAUSED) {
199
                 this.pause();
195
                 this.pause();
200
             }
196
             }
201
         }
197
         }
270
      * @returns {void}
266
      * @returns {void}
271
      */
267
      */
272
     fireUpdatePlayingVideoEvent() {
268
     fireUpdatePlayingVideoEvent() {
273
-        if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) {
269
+        if (this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING) {
274
             this.fireUpdateSharedVideoEvent();
270
             this.fireUpdateSharedVideoEvent();
275
         }
271
         }
276
     }
272
     }
287
             return;
283
             return;
288
         }
284
         }
289
 
285
 
290
-        const status = this.getPlaybackState();
286
+        const status = this.getPlaybackStatus();
291
 
287
 
292
-        if (!Object.values(PLAYBACK_STATES).includes(status)) {
288
+        if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
293
             return;
289
             return;
294
         }
290
         }
295
 
291
 
317
      * currently on.
313
      * currently on.
318
      */
314
      */
319
     isSharedVideoVolumeOn() {
315
     isSharedVideoVolumeOn() {
320
-        return this.getPlaybackState() === PLAYBACK_STATES.PLAYING
316
+        return this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING
321
                 && !this.isMuted()
317
                 && !this.isMuted()
322
                 && this.getVolume() > 0;
318
                 && this.getVolume() > 0;
323
     }
319
     }
347
     /**
343
     /**
348
      * Indicates the playback state of the video
344
      * Indicates the playback state of the video
349
      */
345
      */
350
-    getPlaybackState: () => boolean;
346
+    getPlaybackStatus: () => boolean;
351
 
347
 
352
     /**
348
     /**
353
      * Indicates whether the video is muted
349
      * Indicates whether the video is muted
359
      */
355
      */
360
     getVolume: () => number;
356
     getVolume: () => number;
361
 
357
 
362
-    /**
363
-      * Sets current volume
364
-    */
365
-    setVolume: (value: number) => void;
366
-
367
     /**
358
     /**
368
      * Plays video
359
      * Plays video
369
      */
360
      */

+ 1
- 25
react/features/shared-video/components/web/SharedVideoDialog.js View File

8
 import { getFieldValue } from '../../../base/react';
8
 import { getFieldValue } from '../../../base/react';
9
 import { connect } from '../../../base/redux';
9
 import { connect } from '../../../base/redux';
10
 import { defaultSharedVideoLink } from '../../constants';
10
 import { defaultSharedVideoLink } from '../../constants';
11
-import { getYoutubeId } from '../../functions';
12
 import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
11
 import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
13
 
12
 
14
 /**
13
 /**
60
      * @returns {boolean}
59
      * @returns {boolean}
61
      */
60
      */
62
     _onSubmitValue() {
61
     _onSubmitValue() {
63
-        return this._onSetVideoLink(this.state.value);
62
+        return super._onSetVideoLink(this.state.value);
64
     }
63
     }
65
 
64
 
66
     /**
65
     /**
94
         );
93
         );
95
     }
94
     }
96
 
95
 
97
-    /**
98
-     * Validates the entered video link by extracting the id and dispatches it.
99
-     *
100
-     * It returns a boolean to comply the Dialog behaviour:
101
-     *     {@code true} - the dialog should be closed.
102
-     *     {@code false} - the dialog should be left open.
103
-     *
104
-    * @param {string} link - The entered video link.
105
-     * @returns {boolean}
106
-     */
107
-    _onSetVideoLink(link: string) {
108
-        if (!link || !link.trim()) {
109
-            return false;
110
-        }
111
-
112
-        const youtubeId = getYoutubeId(link);
113
-        const { onPostSubmit } = this.props;
114
-
115
-        onPostSubmit(youtubeId || link);
116
-
117
-        return true;
118
-    }
119
-
120
     _onChange: Object => void;
96
     _onChange: Object => void;
121
 }
97
 }
122
 
98
 

+ 6
- 19
react/features/shared-video/components/web/VideoManager.js View File

1
 import React from 'react';
1
 import React from 'react';
2
 
2
 
3
 import { connect } from '../../../base/redux';
3
 import { connect } from '../../../base/redux';
4
+import { PLAYBACK_STATUSES } from '../../constants';
4
 
5
 
5
 import AbstractVideoManager, {
6
 import AbstractVideoManager, {
6
     _mapDispatchToProps,
7
     _mapDispatchToProps,
7
     _mapStateToProps,
8
     _mapStateToProps,
8
-    PLAYBACK_STATES,
9
     Props
9
     Props
10
 } from './AbstractVideoManager';
10
 } from './AbstractVideoManager';
11
 
11
 
39
      *
39
      *
40
      * @returns {string}
40
      * @returns {string}
41
      */
41
      */
42
-    getPlaybackState() {
43
-        let state;
42
+    getPlaybackStatus() {
43
+        let status;
44
 
44
 
45
         if (!this.player) {
45
         if (!this.player) {
46
             return;
46
             return;
47
         }
47
         }
48
 
48
 
49
         if (this.player.paused) {
49
         if (this.player.paused) {
50
-            state = PLAYBACK_STATES.PAUSED;
50
+            status = PLAYBACK_STATUSES.PAUSED;
51
         } else {
51
         } else {
52
-            state = PLAYBACK_STATES.PLAYING;
52
+            status = PLAYBACK_STATUSES.PLAYING;
53
         }
53
         }
54
 
54
 
55
-        return state;
55
+        return status;
56
     }
56
     }
57
 
57
 
58
     /**
58
     /**
73
         return this.player?.volume;
73
         return this.player?.volume;
74
     }
74
     }
75
 
75
 
76
-    /**
77
-     * Sets player volume.
78
-     *
79
-     * @param {number} value - The volume.
80
-     *
81
-     * @returns {void}
82
-     */
83
-    setVolume(value) {
84
-        if (this.player) {
85
-            this.player.volume = value;
86
-        }
87
-    }
88
-
89
     /**
76
     /**
90
      * Retrieves current time.
77
      * Retrieves current time.
91
      *
78
      *

+ 7
- 18
react/features/shared-video/components/web/YoutubeVideoManager.js View File

3
 import YouTube from 'react-youtube';
3
 import YouTube from 'react-youtube';
4
 
4
 
5
 import { connect } from '../../../base/redux';
5
 import { connect } from '../../../base/redux';
6
+import { PLAYBACK_STATUSES } from '../../constants';
6
 
7
 
7
 import AbstractVideoManager, {
8
 import AbstractVideoManager, {
8
     _mapDispatchToProps,
9
     _mapDispatchToProps,
9
-    _mapStateToProps,
10
-    PLAYBACK_STATES
10
+    _mapStateToProps
11
 } from './AbstractVideoManager';
11
 } from './AbstractVideoManager';
12
 
12
 
13
 /**
13
 /**
34
      *
34
      *
35
      * @returns {string}
35
      * @returns {string}
36
      */
36
      */
37
-    getPlaybackState() {
38
-        let state;
37
+    getPlaybackStatus() {
38
+        let status;
39
 
39
 
40
         if (!this.player) {
40
         if (!this.player) {
41
             return;
41
             return;
44
         const playerState = this.player.getPlayerState();
44
         const playerState = this.player.getPlayerState();
45
 
45
 
46
         if (playerState === YouTube.PlayerState.PLAYING) {
46
         if (playerState === YouTube.PlayerState.PLAYING) {
47
-            state = PLAYBACK_STATES.PLAYING;
47
+            status = PLAYBACK_STATUSES.PLAYING;
48
         }
48
         }
49
 
49
 
50
         if (playerState === YouTube.PlayerState.PAUSED) {
50
         if (playerState === YouTube.PlayerState.PAUSED) {
51
-            state = PLAYBACK_STATES.PAUSED;
51
+            status = PLAYBACK_STATUSES.PAUSED;
52
         }
52
         }
53
 
53
 
54
-        return state;
54
+        return status;
55
     }
55
     }
56
 
56
 
57
     /**
57
     /**
72
         return this.player?.getVolume();
72
         return this.player?.getVolume();
73
     }
73
     }
74
 
74
 
75
-    /**
76
-     * Sets player volume.
77
-     *
78
-     * @param {number} value - The volume.
79
-     *
80
-     * @returns {void}
81
-     */
82
-    setVolume(value) {
83
-        return this.player?.setVolume(value);
84
-    }
85
-
86
     /**
75
     /**
87
      * Retrieves current time.
76
      * Retrieves current time.
88
      *
77
      *

+ 9
- 5
react/features/shared-video/constants.js View File

6
  */
6
  */
7
 export const defaultSharedVideoLink = 'Youtube link or direct video link';
7
 export const defaultSharedVideoLink = 'Youtube link or direct video link';
8
 
8
 
9
-/**
10
- * Mobile example for a youtube video
11
- */
12
-export const defaultMobileSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
13
-
14
 /**
9
 /**
15
  * Fixed name of the video player fake participant.
10
  * Fixed name of the video player fake participant.
16
  * @type {string}
11
  * @type {string}
29
  * @type {string}
24
  * @type {string}
30
  */
25
  */
31
 export const SHARED_VIDEO = 'shared-video';
26
 export const SHARED_VIDEO = 'shared-video';
27
+
28
+/**
29
+ * Available playback statuses
30
+ */
31
+export const PLAYBACK_STATUSES = {
32
+    PLAYING: 'playing',
33
+    PAUSED: 'pause',
34
+    STOPPED: 'stop'
35
+};

Loading…
Cancel
Save