Selaa lähdekoodia

feat(local-video): convert to react (#1705)

* feat(local-video): convert to react

- Create a VideoTrack component for displaying a video element.
  This mirrors native also having a VideoTrack component.
- The VideoTrack component does not let React update it to prevent
  the video element from re-rendering, which could cause flickers
  and would not work with temasys's overriding of the video element.
- VideoTrack extends AbstractVideoTrack to mirror native
  implementation and to get the dispatch of the onplaying event.
- Remove the onclick handler on the video element. Honestly, I
  didn't get it to work, and did not try, but it is also unnecessary
  because another handler already exists on the video wrapper.

* ref(device-selection): VideoInputPreview uses VideoTrack to show video

* squash into conversion: change css selectors

* squash into conversion: mix in abstract props

* squash into conversion: change shouldComponentUpdate check

* squash: update comment about why triggerOnPlayingUpdate is used
master
virtuacoplenny 8 vuotta sitten
vanhempi
commit
244de8096f

+ 4
- 4
css/_videolayout_default.scss Näytä tiedosto

@@ -157,8 +157,8 @@
157 157
     -o-transform: scale(-1, 1);
158 158
 }
159 159
 
160
-#localVideoWrapper>video,
161
-#localVideoWrapper>object {
160
+#localVideoWrapper video,
161
+#localVideoWrapper object {
162 162
     border-radius: $borderRadius !important;
163 163
     cursor: hand;
164 164
     object-fit: cover;
@@ -183,8 +183,8 @@
183 183
 
184 184
 #sharedVideo,
185 185
 #etherpad,
186
-#localVideoWrapper>video,
187
-#localVideoWrapper>object,
186
+#localVideoWrapper video,
187
+#localVideoWrapper object,
188 188
 #localVideoWrapper,
189 189
 #largeVideoWrapper,
190 190
 #largeVideoWrapper>video,

+ 28
- 18
modules/UI/videolayout/LocalVideo.js Näytä tiedosto

@@ -1,11 +1,18 @@
1 1
 /* global $, config, interfaceConfig, APP, JitsiMeetJS */
2
+
3
+/* eslint-disable no-unused-vars */
4
+import React, { Component } from 'react';
5
+import ReactDOM from 'react-dom';
6
+import { Provider } from 'react-redux';
7
+
8
+import { VideoTrack } from '../../../react/features/base/media';
9
+/* eslint-enable no-unused-vars */
10
+
2 11
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3 12
 
4
-import UIUtil from "../util/UIUtil";
5 13
 import UIEvents from "../../../service/UI/UIEvents";
6 14
 import SmallVideo from "./SmallVideo";
7 15
 
8
-const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper;
9 16
 const TrackEvents = JitsiMeetJS.events.track;
10 17
 
11 18
 function LocalVideo(VideoLayout, emitter) {
@@ -85,31 +92,34 @@ LocalVideo.prototype.changeVideo = function (stream) {
85 92
     localVideoContainerSelector.off('click');
86 93
     localVideoContainerSelector.on('click', localVideoClick);
87 94
 
88
-    let localVideo = document.createElement('video');
89
-    localVideo.id = this.localVideoId = 'localVideo_' + stream.getId();
90
-
91
-    RTCUIUtils.setAutoPlay(localVideo, true);
92
-    RTCUIUtils.setVolume(localVideo, 0);
95
+    this.localVideoId = 'localVideo_' + stream.getId();
93 96
 
94 97
     var localVideoContainer = document.getElementById('localVideoWrapper');
95
-    // Put the new video always in front
96
-    UIUtil.prependChild(localVideoContainer, localVideo);
97 98
 
98
-    // Add click handler to both video and video wrapper elements in case
99
-    // there's no video.
100
-
101
-    // onclick has to be used with Temasys plugin
102
-    localVideo.onclick = localVideoClick;
99
+    /* jshint ignore:start */
100
+    ReactDOM.render(
101
+        <Provider store = { APP.store }>
102
+            <VideoTrack
103
+                id = { this.localVideoId }
104
+                videoTrack = {{ jitsiTrack: stream }} />
105
+        </Provider>,
106
+        localVideoContainer
107
+    );
108
+    /* jshint ignore:end */
103 109
 
104 110
     let isVideo = stream.videoType != "desktop";
105 111
     this._enableDisableContextMenu(isVideo);
106 112
     this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false);
107 113
 
108
-    // Attach WebRTC stream
109
-    localVideo = stream.attach(localVideo);
110
-
111 114
     let endedHandler = () => {
112
-        localVideoContainer.removeChild(localVideo);
115
+        // Only remove if there is no video and not a transition state.
116
+        // Previous non-react logic created a new video element with each track
117
+        // removal whereas react reuses the video component so it could be the
118
+        // stream ended but a new one is being used.
119
+        if (this.videoStream.isEnded()) {
120
+            ReactDOM.unmountComponentAtNode(localVideoContainer);
121
+        }
122
+
113 123
         // when removing only the video element and we are on stage
114 124
         // update the stage
115 125
         if (this.isCurrentlyOnLargeVideo()) {

+ 26
- 1
react/features/base/media/components/AbstractVideoTrack.js Näytä tiedosto

@@ -12,6 +12,18 @@ import { Video } from './_';
12 12
  * @abstract
13 13
  */
14 14
 export default class AbstractVideoTrack extends Component {
15
+    /**
16
+     * Default values for AbstractVideoTrack component's properties.
17
+     *
18
+     * @static
19
+     */
20
+    static defaultProps = {
21
+        /**
22
+         * Dispatch an action when the video starts playing.
23
+         */
24
+        triggerOnPlayingUpdate: true
25
+    };
26
+
15 27
     /**
16 28
      * AbstractVideoTrack component's property types.
17 29
      *
@@ -19,7 +31,18 @@ export default class AbstractVideoTrack extends Component {
19 31
      */
20 32
     static propTypes = {
21 33
         dispatch: React.PropTypes.func,
34
+
35
+        /**
36
+         * Whether or not the store should be updated about the playing status
37
+         * of the video. Defaults to true. One use case for setting this prop
38
+         * to false is using multiple locals streams from the same video source,
39
+         * such as when previewing video. In those cases, the store may have no
40
+         * need to be updated about the existence or state of the stream.
41
+         */
42
+        triggerOnPlayingUpdate: React.PropTypes.bool,
43
+
22 44
         videoTrack: React.PropTypes.object,
45
+
23 46
         waitForVideoStarted: React.PropTypes.bool,
24 47
 
25 48
         /**
@@ -117,7 +140,9 @@ export default class AbstractVideoTrack extends Component {
117 140
     _onVideoPlaying() {
118 141
         const videoTrack = this.props.videoTrack;
119 142
 
120
-        if (videoTrack && !videoTrack.videoStarted) {
143
+        if (this.props.triggerOnPlayingUpdate
144
+            && videoTrack
145
+            && !videoTrack.videoStarted) {
121 146
             this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack));
122 147
         }
123 148
     }

+ 202
- 0
react/features/base/media/components/web/VideoTrack.js Näytä tiedosto

@@ -0,0 +1,202 @@
1
+import React from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import AbstractVideoTrack from '../AbstractVideoTrack';
5
+
6
+/**
7
+ * Component that renders a video element for a passed in video track.
8
+ *
9
+ * @extends AbstractVideoTrack
10
+ */
11
+class VideoTrack extends AbstractVideoTrack {
12
+    /**
13
+     * Default values for {@code VideoTrack} component's properties.
14
+     *
15
+     * @static
16
+     */
17
+    static defaultProps = {
18
+        ...AbstractVideoTrack.defaultProps,
19
+
20
+        className: '',
21
+
22
+        id: ''
23
+    };
24
+
25
+    /**
26
+     * {@code VideoTrack} component's property types.
27
+     *
28
+     * @static
29
+     */
30
+    static propTypes = {
31
+        ...AbstractVideoTrack.propTypes,
32
+
33
+        /**
34
+         * CSS classes to add to the video element.
35
+         */
36
+        className: React.PropTypes.string,
37
+
38
+        /**
39
+         * The value of the id attribute of the video. Used by the torture tests
40
+         * to locate video elements.
41
+         */
42
+        id: React.PropTypes.string
43
+    };
44
+
45
+    /**
46
+     * Initializes a new VideoTrack instance.
47
+     *
48
+     * @param {Object} props - The read-only properties with which the new
49
+     * instance is to be initialized.
50
+     */
51
+    constructor(props) {
52
+        super(props);
53
+
54
+        /**
55
+         * The internal reference to the DOM/HTML element intended for
56
+         * displaying a video. This element may be an HTML video element or a
57
+         * temasys video object.
58
+         *
59
+         * @private
60
+         * @type {HTMLVideoElement|Object}
61
+         */
62
+        this._videoElement = null;
63
+
64
+
65
+        // Bind event handlers so they are only bound once for every instance.
66
+        this._setVideoElement = this._setVideoElement.bind(this);
67
+    }
68
+
69
+    /**
70
+     * Invokes the library for rendering the video on initial display. Sets the
71
+     * volume level to zero to ensure no sound plays.
72
+     *
73
+     * @inheritdoc
74
+     * @returns {void}
75
+     */
76
+    componentDidMount() {
77
+        // Add these attributes directly onto the video element so temasys can
78
+        // use them when converting the video to an object.
79
+        this._videoElement.volume = 0;
80
+        this._videoElement.onplaying = this._onVideoPlaying;
81
+
82
+        this._attachTrack(this.props.videoTrack);
83
+    }
84
+
85
+    /**
86
+     * Remove any existing associations between the current video track and the
87
+     * component's video element.
88
+     *
89
+     * @inheritdoc
90
+     * @returns {void}
91
+     */
92
+    componentWillUnmount() {
93
+        this._detachTrack(this.props.videoTrack);
94
+    }
95
+
96
+    /**
97
+     * Updates the video display only if a new track is added. This component's
98
+     * updating is blackboxed from React to prevent re-rendering of video
99
+     * element, as the lib uses track.attach(videoElement) instead. Also,
100
+     * re-rendering cannot be used with temasys, which replaces video elements
101
+     * with an object.
102
+     *
103
+     * @inheritdoc
104
+     * @returns {boolean} - False is always returned to blackbox this component.
105
+     * from React.
106
+     */
107
+    shouldComponentUpdate(nextProps) {
108
+        const currentJitsiTrack = this.props.videoTrack
109
+            && this.props.videoTrack.jitsiTrack;
110
+        const nextJitsiTrack = nextProps.videoTrack
111
+            && nextProps.videoTrack.jitsiTrack;
112
+
113
+        if (currentJitsiTrack !== nextJitsiTrack) {
114
+            this._detachTrack(this.props.videoTrack);
115
+            this._attachTrack(nextProps.videoTrack);
116
+        }
117
+
118
+        return false;
119
+    }
120
+
121
+    /**
122
+     * Renders the video element.
123
+     *
124
+     * @override
125
+     * @returns {ReactElement}
126
+     */
127
+    render() {
128
+        // The wrapping div is necessary because temasys will replace the video
129
+        // with an object but react will keep expecting the video element. The
130
+        // div gives a constant element for react to keep track of.
131
+        return (
132
+            <div>
133
+                <video
134
+                    autoPlay = { true }
135
+                    className = { this.props.className }
136
+                    id = { this.props.id }
137
+                    ref = { this._setVideoElement } />
138
+            </div>
139
+        );
140
+    }
141
+
142
+    /**
143
+     * Calls into the passed in track to associate the track with the
144
+     * component's video element and render video.
145
+     *
146
+     * @param {Object} videoTrack - The redux representation of the
147
+     * {@code JitsiLocalTrack}.
148
+     * @private
149
+     * @returns {void}
150
+     */
151
+    _attachTrack(videoTrack) {
152
+        if (!videoTrack || !videoTrack.jitsiTrack) {
153
+            return;
154
+        }
155
+
156
+        const updatedVideoElement
157
+            = videoTrack.jitsiTrack.attach(this._videoElement);
158
+
159
+        // Sets the instance variable for the video element again as the element
160
+        // maybe have been replaced with a new object by temasys.
161
+        this._setVideoElement(updatedVideoElement);
162
+    }
163
+
164
+    /**
165
+     * Removes the association to the component's video element from the passed
166
+     * in redux representation of jitsi video track to stop the track from
167
+     * rendering. With temasys, the video element must still be visible for
168
+     * detaching to complete.
169
+     *
170
+     * @param {Object} videoTrack -  The redux representation of the
171
+     * {@code JitsiLocalTrack}.
172
+     * @private
173
+     * @returns {void}
174
+     */
175
+    _detachTrack(videoTrack) {
176
+        // Detach the video element from the track only if it has already
177
+        // been attached. This accounts for a special case with temasys
178
+        // where if detach is being called before attach, the video
179
+        // element is converted to Object without updating this
180
+        // component's reference to the video element.
181
+        if (this._videoElement
182
+            && videoTrack
183
+            && videoTrack.jitsiTrack
184
+            && videoTrack.jitsiTrack.containers.includes(this._videoElement)) {
185
+            videoTrack.jitsiTrack.detach(this._videoElement);
186
+        }
187
+    }
188
+
189
+    /**
190
+     * Sets an instance variable for the component's video element so it can be
191
+     * referenced later for attaching and detaching a JitsiLocalTrack.
192
+     *
193
+     * @param {Object} element - DOM element for the component's video display.
194
+     * @private
195
+     * @returns {void}
196
+     */
197
+    _setVideoElement(element) {
198
+        this._videoElement = element;
199
+    }
200
+}
201
+
202
+export default connect()(VideoTrack);

+ 1
- 0
react/features/base/media/components/web/index.js Näytä tiedosto

@@ -1 +1,2 @@
1 1
 export { default as Audio } from './Audio';
2
+export { default as VideoTrack } from './VideoTrack';

+ 13
- 211
react/features/device-selection/components/VideoInputPreview.js Näytä tiedosto

@@ -1,6 +1,6 @@
1 1
 import React, { Component } from 'react';
2 2
 
3
-import { translate } from '../../base/i18n';
3
+import { VideoTrack } from '../../base/media';
4 4
 
5 5
 const VIDEO_ERROR_CLASS = 'video-preview-has-error';
6 6
 
@@ -23,86 +23,12 @@ class VideoInputPreview extends Component {
23 23
          */
24 24
         error: React.PropTypes.string,
25 25
 
26
-        /**
27
-         * Invoked to obtain translated strings.
28
-         */
29
-        t: React.PropTypes.func,
30
-
31 26
         /**
32 27
          * The JitsiLocalTrack to display.
33 28
          */
34 29
         track: React.PropTypes.object
35 30
     };
36 31
 
37
-    /**
38
-     * Initializes a new VideoInputPreview instance.
39
-     *
40
-     * @param {Object} props - The read-only React Component props with which
41
-     * the new instance is to be initialized.
42
-     */
43
-    constructor(props) {
44
-        super(props);
45
-
46
-        /**
47
-         * The internal reference to the DOM/HTML element intended for showing
48
-         * error messages.
49
-         *
50
-         * @private
51
-         * @type {HTMLDivElement}
52
-         */
53
-        this._errorElement = null;
54
-
55
-        /**
56
-         * The internal reference to topmost DOM/HTML element backing the React
57
-         * {@code Component}. Accessed directly for toggling a classname to
58
-         * indicate an error is present so styling can be changed to display it.
59
-         *
60
-         * @private
61
-         * @type {HTMLDivElement}
62
-         */
63
-        this._rootElement = null;
64
-
65
-        /**
66
-         * The internal reference to the DOM/HTML element intended for
67
-         * displaying a video. This element may be an HTML video element or a
68
-         * temasys video object.
69
-         *
70
-         * @private
71
-         * @type {HTMLVideoElement|Object}
72
-         */
73
-        this._videoElement = null;
74
-
75
-        // Bind event handlers so they are only bound once for every instance.
76
-        this._setErrorElement = this._setErrorElement.bind(this);
77
-        this._setRootElement = this._setRootElement.bind(this);
78
-        this._setVideoElement = this._setVideoElement.bind(this);
79
-    }
80
-
81
-    /**
82
-     * Invokes the library for rendering the video on initial display.
83
-     *
84
-     * @inheritdoc
85
-     * @returns {void}
86
-     */
87
-    componentDidMount() {
88
-        if (this.props.error) {
89
-            this._updateErrorView(this.props.error);
90
-        } else {
91
-            this._attachTrack(this.props.track);
92
-        }
93
-    }
94
-
95
-    /**
96
-     * Remove any existing associations between the current previewed track and
97
-     * the component's video element.
98
-     *
99
-     * @inheritdoc
100
-     * @returns {void}
101
-     */
102
-    componentWillUnmount() {
103
-        this._detachTrack(this.props.track);
104
-    }
105
-
106 32
     /**
107 33
      * Implements React's {@link Component#render()}.
108 34
      *
@@ -110,146 +36,22 @@ class VideoInputPreview extends Component {
110 36
      * @returns {ReactElement}
111 37
      */
112 38
     render() {
39
+        const { error } = this.props;
40
+        const errorClass = error ? VIDEO_ERROR_CLASS : '';
41
+        const className = `video-input-preview ${errorClass}`;
42
+
113 43
         return (
114
-            <div
115
-                className = 'video-input-preview'
116
-                ref = { this._setRootElement }>
117
-                <video
118
-                    autoPlay = { true }
44
+            <div className = { className }>
45
+                <VideoTrack
119 46
                     className = 'video-input-preview-display flipVideoX'
120
-                    ref = { this._setVideoElement } />
121
-                <div
122
-                    className = 'video-input-preview-error'
123
-                    ref = { this._setErrorElement } />
47
+                    triggerOnPlayingUpdate = { false }
48
+                    videoTrack = {{ jitsiTrack: this.props.track }} />
49
+                <div className = 'video-input-preview-error'>
50
+                    { error || '' }
51
+                </div>
124 52
             </div>
125 53
         );
126 54
     }
127
-
128
-    /**
129
-     * Only update when the deviceId has changed. This component is somewhat
130
-     * black-boxed from React's rendering so lib-jitsi-meet can instead handle
131
-     * updating of the video preview, which takes browser differences into
132
-     * consideration. For example, temasys's video object must be visible to
133
-     * update the displayed track, but React's re-rendering could potentially
134
-     * remove the video object from the page.
135
-     *
136
-     * @inheritdoc
137
-     * @returns {void}
138
-     */
139
-    shouldComponentUpdate(nextProps) {
140
-        const hasNewTrack = nextProps.track !== this.props.track;
141
-
142
-        if (hasNewTrack || nextProps.error) {
143
-            this._detachTrack(this.props.track);
144
-            this._updateErrorView(nextProps.error);
145
-        }
146
-
147
-        // Never attempt to show the new track if there is an error present.
148
-        if (hasNewTrack && !nextProps.error) {
149
-            this._attachTrack(nextProps.track);
150
-        }
151
-
152
-        return false;
153
-    }
154
-
155
-    /**
156
-     * Calls into the passed in track to associate the track with the
157
-     * component's video element and render video. Also sets the instance
158
-     * variable for the video element as the element the track attached to,
159
-     * which could be an Object if on a temasys supported browser.
160
-     *
161
-     * @param {JitsiLocalTrack} track - The library's track model which will be
162
-     * displayed.
163
-     * @private
164
-     * @returns {void}
165
-     */
166
-    _attachTrack(track) {
167
-        if (!track) {
168
-            return;
169
-        }
170
-
171
-        const updatedVideoElement = track.attach(this._videoElement);
172
-
173
-        this._setVideoElement(updatedVideoElement);
174
-    }
175
-
176
-    /**
177
-     * Removes the association to the component's video element from the passed
178
-     * in JitsiLocalTrack to stop the track from rendering. With temasys, the
179
-     * video element must still be visible for detaching to complete.
180
-     *
181
-     * @param {JitsiLocalTrack} track - The library's track model which needs
182
-     * to stop previewing in the video element.
183
-     * @private
184
-     * @returns {void}
185
-     */
186
-    _detachTrack(track) {
187
-        // Detach the video element from the track only if it has already
188
-        // been attached. This accounts for a special case with temasys
189
-        // where if detach is being called before attach, the video
190
-        // element is converted to Object without updating this
191
-        // component's reference to the video element.
192
-        if (this._videoElement
193
-            && track
194
-            && track.containers.includes(this._videoElement)) {
195
-            track.detach(this._videoElement);
196
-        }
197
-    }
198
-
199
-    /**
200
-     * Sets an instance variable for the component's element intended for
201
-     * displaying error messages. The element will be accessed directly to
202
-     * display an error message.
203
-     *
204
-     * @param {Object} element - DOM element intended for displaying errors.
205
-     * @private
206
-     * @returns {void}
207
-     */
208
-    _setErrorElement(element) {
209
-        this._errorElement = element;
210
-    }
211
-
212
-    /**
213
-     * Sets the component's root element.
214
-     *
215
-     * @param {Object} element - The highest DOM element in the component.
216
-     * @private
217
-     * @returns {void}
218
-     */
219
-    _setRootElement(element) {
220
-        this._rootElement = element;
221
-    }
222
-
223
-    /**
224
-     * Sets an instance variable for the component's video element so it can be
225
-     * referenced later for attaching and detaching a JitsiLocalTrack.
226
-     *
227
-     * @param {Object} element - DOM element for the component's video display.
228
-     * @private
229
-     * @returns {void}
230
-     */
231
-    _setVideoElement(element) {
232
-        this._videoElement = element;
233
-    }
234
-
235
-    /**
236
-     * Adds or removes a class to the component's parent node to indicate an
237
-     * error has occurred. Also sets the error text.
238
-     *
239
-     * @param {string} error - The error message to display. If falsy, error
240
-     * message display will be hidden.
241
-     * @private
242
-     * @returns {void}
243
-     */
244
-    _updateErrorView(error) {
245
-        if (error) {
246
-            this._rootElement.classList.add(VIDEO_ERROR_CLASS);
247
-        } else {
248
-            this._rootElement.classList.remove(VIDEO_ERROR_CLASS);
249
-        }
250
-
251
-        this._errorElement.innerText = error || '';
252
-    }
253 55
 }
254 56
 
255
-export default translate(VideoInputPreview);
57
+export default VideoInputPreview;

Loading…
Peruuta
Tallenna