浏览代码

fix(device-selection): do not reuse tracks in previews

Device selection has live previews that reuse the current local
audio and video tracks for the sake of internet explorer. This
means when the local video was muted, device selection would
show a muted message. It is preferred to show a live preview
even when muted.

The changes include:
- Passing device ids into DeviceSelectionDialog, not tracks.
- Setting default selected devices to use for live previews.
- Removing all checks in DeviceSelectionDialog involving local tracks.
- Catching and displaying errors when creating a live video preview.
master
Leonard Kim 8 年前
父节点
当前提交
929bc8b8b9

+ 3
- 5
css/modals/device-selection/_device-selection.scss 查看文件

@@ -79,7 +79,7 @@
79 79
                 border-radius: 3px;
80 80
             }
81 81
 
82
-            .video-input-preview-muted {
82
+            .video-input-preview-error {
83 83
                 color: $participantNameColor;
84 84
                 display: none;
85 85
                 left: 0;
@@ -89,12 +89,10 @@
89 89
                 top: 50%;
90 90
             }
91 91
 
92
-            &.video-muted {
93
-                /* TOFIX: to be removed when we move out from muted preview */
92
+            &.video-preview-has-error {
94 93
                 background: black;
95
-                /* TOFIX-END */
96 94
 
97
-                .video-input-preview-muted {
95
+                .video-input-preview-error {
98 96
                     display: block;
99 97
                 }
100 98
             }

+ 1
- 1
lang/main.json 查看文件

@@ -429,9 +429,9 @@
429 429
         "speakerTime": "Speaker Time"
430 430
     },
431 431
     "deviceSelection": {
432
-        "currentlyVideoMuted": "Video is currently muted",
433 432
         "deviceSettings": "Device settings",
434 433
         "noPermission": "Permission not granted",
434
+        "previewUnavailable": "Preview unavailable",
435 435
         "selectADevice": "Select a device",
436 436
         "testAudio": "Test sound"
437 437
     },

+ 3
- 6
react/features/device-selection/actions.js 查看文件

@@ -12,16 +12,13 @@ import { DeviceSelectionDialog } from './components';
12 12
  * @returns {Function}
13 13
  */
14 14
 export function openDeviceSelectionDialog() {
15
-    return (dispatch, getState) => {
15
+    return dispatch => {
16 16
         JitsiMeetJS.mediaDevices.isDeviceListAvailable()
17 17
             .then(isDeviceListAvailable => {
18
-                const state = getState();
19
-                const conference = state['features/base/conference'].conference;
20
-
21 18
                 dispatch(openDialog(DeviceSelectionDialog, {
19
+                    currentAudioInputId: APP.settings.getMicDeviceId(),
22 20
                     currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
23
-                    currentAudioTrack: conference.getLocalAudioTrack(),
24
-                    currentVideoTrack: conference.getLocalVideoTrack(),
21
+                    currentVideoInputId: APP.settings.getCameraDeviceId(),
25 22
                     disableAudioInputChange:
26 23
                         !JitsiMeetJS.isMultipleAudioInputSupported(),
27 24
                     disableDeviceChange: !isDeviceListAvailable

+ 174
- 245
react/features/device-selection/components/DeviceSelectionDialog.js 查看文件

@@ -34,29 +34,25 @@ class DeviceSelectionDialog extends Component {
34 34
          * All known audio and video devices split by type. This prop comes from
35 35
          * the app state.
36 36
          */
37
-        _devices: React.PropTypes.object,
37
+        _availableDevices: React.PropTypes.object,
38 38
 
39 39
         /**
40
-         * Device id for the current audio output device.
40
+         * Device id for the current audio input device. This device will be set
41
+         * as the default audio input device to preview.
41 42
          */
42
-        currentAudioOutputId: React.PropTypes.string,
43
+        currentAudioInputId: React.PropTypes.string,
43 44
 
44 45
         /**
45
-         * JitsiLocalTrack for the current local audio.
46
-         *
47
-         * JitsiLocalTracks for the current audio and video, if any, should be
48
-         * passed in for re-use in the previews. This is needed for Internet
49
-         * Explorer, which cannot get multiple tracks from the same device, even
50
-         * across tabs.
46
+         * Device id for the current audio output device. This device will be
47
+         * set as the default audio output device to preview.
51 48
          */
52
-        currentAudioTrack: React.PropTypes.object,
49
+        currentAudioOutputId: React.PropTypes.string,
53 50
 
54 51
         /**
55
-         * JitsiLocalTrack for the current local video.
56
-         *
57
-         * Needed for reuse. See comment for propTypes.currentAudioTrack.
52
+         * Device id for the current video input device. This device will be set
53
+         * as the default video input device to preview.
58 54
          */
59
-        currentVideoTrack: React.PropTypes.object,
55
+        currentVideoInputId: React.PropTypes.string,
60 56
 
61 57
         /**
62 58
          * Whether or not the audio selector can be interacted with. If true,
@@ -78,12 +74,12 @@ class DeviceSelectionDialog extends Component {
78 74
         dispatch: React.PropTypes.func,
79 75
 
80 76
         /**
81
-         * Whether or not new audio input source can be selected.
77
+         * Whether or not a new audio input source can be selected.
82 78
          */
83 79
         hasAudioPermission: React.PropTypes.bool,
84 80
 
85 81
         /**
86
-         * Whether or not new video input sources can be selected.
82
+         * Whether or not a new video input sources can be selected.
87 83
          */
88 84
         hasVideoPermission: React.PropTypes.bool,
89 85
 
@@ -117,15 +113,40 @@ class DeviceSelectionDialog extends Component {
117 113
     constructor(props) {
118 114
         super(props);
119 115
 
116
+        const { _availableDevices } = this.props;
117
+
120 118
         this.state = {
121
-            // JitsiLocalTracks to use for live previewing.
119
+            // JitsiLocalTrack to use for live previewing of audio input.
122 120
             previewAudioTrack: null,
121
+
122
+            // JitsiLocalTrack to use for live previewing of video input.
123 123
             previewVideoTrack: null,
124 124
 
125
-            // Device ids to keep track of new selections.
126
-            videInput: null,
127
-            audioInput: null,
128
-            audioOutput: null
125
+            // An message describing a problem with obtaining a video preview.
126
+            previewVideoTrackError: null,
127
+
128
+            // The audio input device id to show as selected by default.
129
+            selectedAudioInputId: this.props.currentAudioInputId || '',
130
+
131
+            // The audio output device id to show as selected by default.
132
+            selectedAudioOutputId: this.props.currentAudioOutputId || '',
133
+
134
+            // The video input device id to show as selected by default.
135
+            // FIXME: On temasys, without a device selected and put into local
136
+            // storage as the default device to use, the current video device id
137
+            // is a blank string. This is because the library gets a local video
138
+            // track and then maps the track's device id by matching the track's
139
+            // label to the MediaDeviceInfos returned from enumerateDevices. In
140
+            // WebRTC, the track label is expected to return the camera device
141
+            // label. However, temasys video track labels refer to track id, not
142
+            // device label, so the library cannot match the track to a device.
143
+            // The workaround of defaulting to the first videoInput available
144
+            // is re-used from the previous device settings implementation.
145
+            selectedVideoInputId: this.props.currentVideoInputId
146
+                || (_availableDevices.videoInput
147
+                    && _availableDevices.videoInput[0]
148
+                    && _availableDevices.videoInput[0].deviceId)
149
+                || ''
129 150
         };
130 151
 
131 152
         // Preventing closing while cleaning up previews is important for
@@ -134,16 +155,29 @@ class DeviceSelectionDialog extends Component {
134 155
         // closure until cleanup is complete ensures no errors in the process.
135 156
         this._isClosing = false;
136 157
 
158
+        // Bind event handlers so they are only bound once for every instance.
137 159
         this._closeModal = this._closeModal.bind(this);
138
-        this._getAndSetAudioOutput = this._getAndSetAudioOutput.bind(this);
139
-        this._getAndSetAudioTrack = this._getAndSetAudioTrack.bind(this);
140
-        this._getAndSetVideoTrack = this._getAndSetVideoTrack.bind(this);
141 160
         this._onCancel = this._onCancel.bind(this);
142 161
         this._onSubmit = this._onSubmit.bind(this);
162
+        this._updateAudioOutput = this._updateAudioOutput.bind(this);
163
+        this._updateAudioInput = this._updateAudioInput.bind(this);
164
+        this._updateVideoInput = this._updateVideoInput.bind(this);
143 165
     }
144 166
 
145 167
     /**
146
-     * Clean up any preview tracks that might not have been cleaned up already.
168
+     * Sets default device choices so a choice is pre-selected in the dropdowns
169
+     * and live previews are created.
170
+     *
171
+     * @inheritdoc
172
+     */
173
+    componentDidMount() {
174
+        this._updateAudioOutput(this.state.selectedAudioOutputId);
175
+        this._updateAudioInput(this.state.selectedAudioInputId);
176
+        this._updateVideoInput(this.state.selectedVideoInputId);
177
+    }
178
+
179
+    /**
180
+     * Disposes preview tracks that might not already be disposed.
147 181
      *
148 182
      * @inheritdoc
149 183
      */
@@ -173,8 +207,8 @@ class DeviceSelectionDialog extends Component {
173 207
                     <div className = 'device-selection-column column-video'>
174 208
                         <div className = 'device-selection-video-container'>
175 209
                             <VideoInputPreview
176
-                                track = { this.state.previewVideoTrack
177
-                                    || this.props.currentVideoTrack } />
210
+                                error = { this.state.previewVideoTrackError }
211
+                                track = { this.state.previewVideoTrack } />
178 212
                         </div>
179 213
                         { this._renderAudioInputPreview() }
180 214
                     </div>
@@ -197,17 +231,10 @@ class DeviceSelectionDialog extends Component {
197 231
      * promise can be for video cleanup and another for audio cleanup.
198 232
      */
199 233
     _attemptPreviewTrackCleanup() {
200
-        const cleanupPromises = [];
201
-
202
-        if (!this._isPreviewingCurrentVideoTrack()) {
203
-            cleanupPromises.push(this._disposeVideoPreview());
204
-        }
205
-
206
-        if (!this._isPreviewingCurrentAudioTrack()) {
207
-            cleanupPromises.push(this._disposeAudioPreview());
208
-        }
209
-
210
-        return cleanupPromises;
234
+        return Promise.all([
235
+            this._disposeVideoPreview(),
236
+            this._disposeAudioPreview()
237
+        ]);
211 238
     }
212 239
 
213 240
     /**
@@ -243,147 +270,7 @@ class DeviceSelectionDialog extends Component {
243 270
     }
244 271
 
245 272
     /**
246
-     * Callback invoked when a new audio output device has been selected.
247
-     * Updates the internal state of the user's selection.
248
-     *
249
-     * @param {string} deviceId - The id of the chosen audio output device.
250
-     * @private
251
-     * @returns {void}
252
-     */
253
-    _getAndSetAudioOutput(deviceId) {
254
-        this.setState({
255
-            audioOutput: deviceId
256
-        });
257
-    }
258
-
259
-    /**
260
-     * Callback invoked when a new audio input device has been selected.
261
-     * Updates the internal state of the user's selection as well as the audio
262
-     * track that should display in the preview. Will reuse the current local
263
-     * audio track if it has been selected.
264
-     *
265
-     * @param {string} deviceId - The id of the chosen audio input device.
266
-     * @private
267
-     * @returns {void}
268
-     */
269
-    _getAndSetAudioTrack(deviceId) {
270
-        this.setState({
271
-            audioInput: deviceId
272
-        }, () => {
273
-            const cleanupPromise = this._isPreviewingCurrentAudioTrack()
274
-                ? Promise.resolve() : this._disposeAudioPreview();
275
-
276
-            if (this._isCurrentAudioTrack(deviceId)) {
277
-                cleanupPromise
278
-                    .then(() => {
279
-                        this.setState({
280
-                            previewAudioTrack: this.props.currentAudioTrack
281
-                        });
282
-                    });
283
-            } else {
284
-                cleanupPromise
285
-                    .then(() => createLocalTrack('audio', deviceId))
286
-                    .then(jitsiLocalTrack => {
287
-                        this.setState({
288
-                            previewAudioTrack: jitsiLocalTrack
289
-                        });
290
-                    });
291
-            }
292
-        });
293
-    }
294
-
295
-    /**
296
-     * Callback invoked when a new video input device has been selected. Updates
297
-     * the internal state of the user's selection as well as the video track
298
-     * that should display in the preview. Will reuse the current local video
299
-     * track if it has been selected.
300
-     *
301
-     * @param {string} deviceId - The id of the chosen video input device.
302
-     * @private
303
-     * @returns {void}
304
-     */
305
-    _getAndSetVideoTrack(deviceId) {
306
-        this.setState({
307
-            videoInput: deviceId
308
-        }, () => {
309
-            const cleanupPromise = this._isPreviewingCurrentVideoTrack()
310
-                ? Promise.resolve() : this._disposeVideoPreview();
311
-
312
-            if (this._isCurrentVideoTrack(deviceId)) {
313
-                cleanupPromise
314
-                    .then(() => {
315
-                        this.setState({
316
-                            previewVideoTrack: this.props.currentVideoTrack
317
-                        });
318
-                    });
319
-            } else {
320
-                cleanupPromise
321
-                    .then(() => createLocalTrack('video', deviceId))
322
-                    .then(jitsiLocalTrack => {
323
-                        this.setState({
324
-                            previewVideoTrack: jitsiLocalTrack
325
-                        });
326
-                    });
327
-            }
328
-        });
329
-    }
330
-
331
-    /**
332
-     * Utility function for determining if the current local audio track has the
333
-     * passed in device id.
334
-     *
335
-     * @param {string} deviceId - The device id to match against.
336
-     * @private
337
-     * @returns {boolean} True if the device id is being used by the local audio
338
-     * track.
339
-     */
340
-    _isCurrentAudioTrack(deviceId) {
341
-        return this.props.currentAudioTrack
342
-            && this.props.currentAudioTrack.getDeviceId() === deviceId;
343
-    }
344
-
345
-    /**
346
-     * Utility function for determining if the current local video track has the
347
-     * passed in device id.
348
-     *
349
-     * @param {string} deviceId - The device id to match against.
350
-     * @private
351
-     * @returns {boolean} True if the device id is being used by the local
352
-     * video track.
353
-     */
354
-    _isCurrentVideoTrack(deviceId) {
355
-        return this.props.currentVideoTrack
356
-            && this.props.currentVideoTrack.getDeviceId() === deviceId;
357
-    }
358
-
359
-    /**
360
-     * Utility function for detecting if the current audio preview track is not
361
-     * the currently used audio track.
362
-     *
363
-     * @private
364
-     * @returns {boolean} True if the current audio track is being used for
365
-     * the preview.
366
-     */
367
-    _isPreviewingCurrentAudioTrack() {
368
-        return !this.state.previewAudioTrack
369
-            || this.state.previewAudioTrack === this.props.currentAudioTrack;
370
-    }
371
-
372
-    /**
373
-     * Utility function for detecting if the current video preview track is not
374
-     * the currently used video track.
375
-     *
376
-     * @private
377
-     * @returns {boolean} True if the current video track is being used as the
378
-     * preview.
379
-     */
380
-    _isPreviewingCurrentVideoTrack() {
381
-        return !this.state.previewVideoTrack
382
-            || this.state.previewVideoTrack === this.props.currentVideoTrack;
383
-    }
384
-
385
-    /**
386
-     * Cleans existing preview tracks and signal to closeDeviceSelectionDialog.
273
+     * Disposes preview tracks and signals to close DeviceSelectionDialog.
387 274
      *
388 275
      * @private
389 276
      * @returns {boolean} Returns false to prevent closure until cleanup is
@@ -406,7 +293,7 @@ class DeviceSelectionDialog extends Component {
406 293
     }
407 294
 
408 295
     /**
409
-     * Identify changes to the preferred input/output devices and perform
296
+     * Identifies changes to the preferred input/output devices and perform
410 297
      * necessary cleanup and requests to use those devices. Closes the modal
411 298
      * after cleanup and device change requests complete.
412 299
      *
@@ -421,32 +308,26 @@ class DeviceSelectionDialog extends Component {
421 308
 
422 309
         this._isClosing = true;
423 310
 
424
-        const deviceChangePromises = [];
425
-
426
-        if (this.state.videoInput && !this._isPreviewingCurrentVideoTrack()) {
427
-            const changeVideoPromise = this._disposeVideoPreview()
428
-                .then(() => {
429
-                    this.props.dispatch(setVideoInputDevice(
430
-                        this.state.videoInput));
431
-                });
432
-
433
-            deviceChangePromises.push(changeVideoPromise);
434
-        }
435
-
436
-        if (this.state.audioInput && !this._isPreviewingCurrentAudioTrack()) {
437
-            const changeAudioPromise = this._disposeAudioPreview()
438
-                .then(() => {
439
-                    this.props.dispatch(setAudioInputDevice(
440
-                        this.state.audioInput));
441
-                });
442
-
443
-            deviceChangePromises.push(changeAudioPromise);
444
-        }
445
-
446
-        if (this.state.audioOutput
447
-            && this.state.audioOutput !== this.props.currentAudioOutputId) {
448
-            this.props.dispatch(setAudioOutputDevice(this.state.audioOutput));
449
-        }
311
+        const deviceChangePromises = this._attemptPreviewTrackCleanup()
312
+            .then(() => {
313
+                if (this.state.selectedVideoInputId
314
+                        !== this.props.currentVideoInputId) {
315
+                    this.props.dispatch(
316
+                        setVideoInputDevice(this.state.selectedVideoInputId));
317
+                }
318
+
319
+                if (this.state.selectedAudioInputId
320
+                        !== this.props.currentAudioInputId) {
321
+                    this.props.dispatch(
322
+                        setAudioInputDevice(this.state.selectedAudioInputId));
323
+                }
324
+
325
+                if (this.state.selectedAudioOutputId
326
+                        !== this.props.currentAudioOutputId) {
327
+                    this.props.dispatch(
328
+                        setAudioOutputDevice(this.state.selectedAudioOutputId));
329
+                }
330
+            });
450 331
 
451 332
         Promise.all(deviceChangePromises)
452 333
             .then(this._closeModal)
@@ -470,8 +351,7 @@ class DeviceSelectionDialog extends Component {
470 351
 
471 352
         return (
472 353
             <AudioInputPreview
473
-                track = { this.state.previewAudioTrack
474
-                    || this.props.currentAudioTrack } />
354
+                track = { this.state.previewAudioTrack } />
475 355
         );
476 356
     }
477 357
 
@@ -489,8 +369,7 @@ class DeviceSelectionDialog extends Component {
489 369
 
490 370
         return (
491 371
             <AudioOutputPreview
492
-                deviceId = { this.state.audioOutput
493
-                    || this.props.currentAudioOutputId } />
372
+                deviceId = { this.state.selectedAudioOutputId } />
494 373
         );
495 374
     }
496 375
 
@@ -515,70 +394,120 @@ class DeviceSelectionDialog extends Component {
515 394
      * @returns {Array<ReactElement>} DeviceSelector instances.
516 395
      */
517 396
     _renderSelectors() {
518
-        const availableDevices = this.props._devices;
519
-        const currentAudioId = this.state.audioInput
520
-            || (this.props.currentAudioTrack
521
-                && this.props.currentAudioTrack.getDeviceId());
522
-        const currentAudioOutId = this.state.audioOutput
523
-            || this.props.currentAudioOutputId;
524
-
525
-        // FIXME: On temasys, without a device selected and put into local
526
-        // storage as the default device to use, the current video device id is
527
-        // a blank string. This is because the library gets a local video track
528
-        // and then maps the track's device id by matching the track's label to
529
-        // the MediaDeviceInfos returned from enumerateDevices. In WebRTC, the
530
-        // track label is expected to return the camera device label. However,
531
-        // temasys video track labels refer to track id, not device label, so
532
-        // the library cannot match the track to a device. The workaround of
533
-        // defaulting to the first videoInput available has been re-used from
534
-        // the previous device settings implementation.
535
-        const currentVideoId = this.state.videoInput
536
-            || (this.props.currentVideoTrack
537
-                && this.props.currentVideoTrack.getDeviceId())
538
-            || (availableDevices.videoInput[0]
539
-                && availableDevices.videoInput[0].deviceId)
540
-            || ''; // DeviceSelector expects a string for prop selectedDeviceId.
541
-
397
+        const { _availableDevices } = this.props;
542 398
         const configurations = [
543 399
             {
544
-                devices: availableDevices.videoInput,
400
+                devices: _availableDevices.videoInput,
545 401
                 hasPermission: this.props.hasVideoPermission,
546 402
                 icon: 'icon-camera',
547 403
                 isDisabled: this.props.disableDeviceChange,
548 404
                 key: 'videoInput',
549 405
                 label: 'settings.selectCamera',
550
-                onSelect: this._getAndSetVideoTrack,
551
-                selectedDeviceId: currentVideoId
406
+                onSelect: this._updateVideoInput,
407
+                selectedDeviceId: this.state.selectedVideoInputId
552 408
             },
553 409
             {
554
-                devices: availableDevices.audioInput,
410
+                devices: _availableDevices.audioInput,
555 411
                 hasPermission: this.props.hasAudioPermission,
556 412
                 icon: 'icon-microphone',
557 413
                 isDisabled: this.props.disableAudioInputChange
558 414
                     || this.props.disableDeviceChange,
559 415
                 key: 'audioInput',
560 416
                 label: 'settings.selectMic',
561
-                onSelect: this._getAndSetAudioTrack,
562
-                selectedDeviceId: currentAudioId
417
+                onSelect: this._updateAudioInput,
418
+                selectedDeviceId: this.state.selectedAudioInputId
563 419
             }
564 420
         ];
565 421
 
566 422
         if (!this.props.hideAudioOutputSelect) {
567 423
             configurations.push({
568
-                devices: availableDevices.audioOutput,
424
+                devices: _availableDevices.audioOutput,
569 425
                 hasPermission: this.props.hasAudioPermission
570 426
                     || this.props.hasVideoPermission,
571 427
                 icon: 'icon-volume',
572 428
                 isDisabled: this.props.disableDeviceChange,
573 429
                 key: 'audioOutput',
574 430
                 label: 'settings.selectAudioOutput',
575
-                onSelect: this._getAndSetAudioOutput,
576
-                selectedDeviceId: currentAudioOutId
431
+                onSelect: this._updateAudioOutput,
432
+                selectedDeviceId: this.state.selectedAudioOutputId
577 433
             });
578 434
         }
579 435
 
580 436
         return configurations.map(this._renderSelector);
581 437
     }
438
+
439
+    /**
440
+     * Callback invoked when a new audio input device has been selected. Updates
441
+     * the internal state of the user's selection as well as the audio track
442
+     * that should display in the preview.
443
+     *
444
+     * @param {string} deviceId - The id of the chosen audio input device.
445
+     * @private
446
+     * @returns {void}
447
+     */
448
+    _updateAudioInput(deviceId) {
449
+        this.setState({
450
+            selectedAudioInputId: deviceId
451
+        }, () => {
452
+            this._disposeAudioPreview()
453
+                .then(() => createLocalTrack('audio', deviceId))
454
+                .then(jitsiLocalTrack => {
455
+                    this.setState({
456
+                        previewAudioTrack: jitsiLocalTrack
457
+                    });
458
+                })
459
+                .catch(() => {
460
+                    this.setState({
461
+                        previewAudioTrack: null
462
+                    });
463
+                });
464
+        });
465
+    }
466
+
467
+    /**
468
+     * Callback invoked when a new audio output device has been selected.
469
+     * Updates the internal state of the user's selection.
470
+     *
471
+     * @param {string} deviceId - The id of the chosen audio output device.
472
+     * @private
473
+     * @returns {void}
474
+     */
475
+    _updateAudioOutput(deviceId) {
476
+        this.setState({
477
+            selectedAudioOutputId: deviceId
478
+        });
479
+    }
480
+
481
+    /**
482
+     * Callback invoked when a new video input device has been selected. Updates
483
+     * the internal state of the user's selection as well as the video track
484
+     * that should display in the preview.
485
+     *
486
+     * @param {string} deviceId - The id of the chosen video input device.
487
+     * @private
488
+     * @returns {void}
489
+     */
490
+    _updateVideoInput(deviceId) {
491
+        this.setState({
492
+            selectedVideoInputId: deviceId
493
+        }, () => {
494
+            this._disposeVideoPreview()
495
+                .then(() => createLocalTrack('video', deviceId))
496
+                .then(jitsiLocalTrack => {
497
+                    this.setState({
498
+                        previewVideoTrack: jitsiLocalTrack,
499
+                        previewVideoTrackError: null
500
+                    });
501
+                })
502
+                .catch(() => {
503
+                    this.setState({
504
+                        previewVideoTrack: null,
505
+                        previewVideoTrackError:
506
+                            this.props.t('deviceSelection.previewUnavailable')
507
+                    });
508
+                });
509
+        });
510
+    }
582 511
 }
583 512
 
584 513
 /**
@@ -588,12 +517,12 @@ class DeviceSelectionDialog extends Component {
588 517
  * @param {Object} state - The Redux state.
589 518
  * @private
590 519
  * @returns {{
591
- *     _devices: Object
520
+ *     _availableDevices: Object
592 521
  * }}
593 522
  */
594 523
 function _mapStateToProps(state) {
595 524
     return {
596
-        _devices: state['features/base/devices']
525
+        _availableDevices: state['features/base/devices']
597 526
     };
598 527
 }
599 528
 

+ 77
- 25
react/features/device-selection/components/VideoInputPreview.js 查看文件

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
2 2
 
3 3
 import { translate } from '../../base/i18n';
4 4
 
5
-const VIDEO_MUTE_CLASS = 'video-muted';
5
+const VIDEO_ERROR_CLASS = 'video-preview-has-error';
6 6
 
7 7
 /**
8 8
  * React component for displaying video. This component defers to lib-jitsi-meet
@@ -17,12 +17,18 @@ class VideoInputPreview extends Component {
17 17
      * @static
18 18
      */
19 19
     static propTypes = {
20
+        /**
21
+         * An error message to display instead of a preview. Displaying an error
22
+         * will take priority over displaying a video preview.
23
+         */
24
+        error: React.PropTypes.string,
25
+
20 26
         /**
21 27
          * Invoked to obtain translated strings.
22 28
          */
23 29
         t: React.PropTypes.func,
24 30
 
25
-        /*
31
+        /**
26 32
          * The JitsiLocalTrack to display.
27 33
          */
28 34
         track: React.PropTypes.object
@@ -37,9 +43,37 @@ class VideoInputPreview extends Component {
37 43
     constructor(props) {
38 44
         super(props);
39 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
+         */
40 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
+         */
41 73
         this._videoElement = null;
42 74
 
75
+        // Bind event handlers so they are only bound once for every instance.
76
+        this._setErrorElement = this._setErrorElement.bind(this);
43 77
         this._setRootElement = this._setRootElement.bind(this);
44 78
         this._setVideoElement = this._setVideoElement.bind(this);
45 79
     }
@@ -51,7 +85,11 @@ class VideoInputPreview extends Component {
51 85
      * @returns {void}
52 86
      */
53 87
     componentDidMount() {
54
-        this._attachTrack(this.props.track);
88
+        if (this.props.error) {
89
+            this._updateErrorView(this.props.error);
90
+        } else {
91
+            this._attachTrack(this.props.track);
92
+        }
55 93
     }
56 94
 
57 95
     /**
@@ -80,9 +118,9 @@ class VideoInputPreview extends Component {
80 118
                     autoPlay = { true }
81 119
                     className = 'video-input-preview-display flipVideoX'
82 120
                     ref = { this._setVideoElement } />
83
-                <div className = 'video-input-preview-muted'>
84
-                    { this.props.t('deviceSelection.currentlyVideoMuted') }
85
-                </div>
121
+                <div
122
+                    className = 'video-input-preview-error'
123
+                    ref = { this._setErrorElement } />
86 124
             </div>
87 125
         );
88 126
     }
@@ -99,8 +137,15 @@ class VideoInputPreview extends Component {
99 137
      * @returns {void}
100 138
      */
101 139
     shouldComponentUpdate(nextProps) {
102
-        if (nextProps.track !== this.props.track) {
140
+        const hasNewTrack = nextProps.track !== this.props.track;
141
+
142
+        if (hasNewTrack || nextProps.error) {
103 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) {
104 149
             this._attachTrack(nextProps.track);
105 150
         }
106 151
 
@@ -123,17 +168,9 @@ class VideoInputPreview extends Component {
123 168
             return;
124 169
         }
125 170
 
126
-        // Do not attempt to display a preview if the track is muted, as the
127
-        // library will simply return a falsy value for the element anyway.
128
-        if (track.isMuted()) {
129
-            this._showMuteOverlay(true);
130
-        } else {
131
-            this._showMuteOverlay(false);
171
+        const updatedVideoElement = track.attach(this._videoElement);
132 172
 
133
-            const updatedVideoElement = track.attach(this._videoElement);
134
-
135
-            this._setVideoElement(updatedVideoElement);
136
-        }
173
+        this._setVideoElement(updatedVideoElement);
137 174
     }
138 175
 
139 176
     /**
@@ -159,6 +196,19 @@ class VideoInputPreview extends Component {
159 196
         }
160 197
     }
161 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
+
162 212
     /**
163 213
      * Sets the component's root element.
164 214
      *
@@ -183,20 +233,22 @@ class VideoInputPreview extends Component {
183 233
     }
184 234
 
185 235
     /**
186
-     * Adds or removes a class to the component's parent node to indicate mute
187
-     * status.
236
+     * Adds or removes a class to the component's parent node to indicate an
237
+     * error has occurred. Also sets the error text.
188 238
      *
189
-     * @param {boolean} shouldShow - True if the mute class should be added and
190
-     * false if the class should be removed.
239
+     * @param {string} error - The error message to display. If falsy, error
240
+     * message display will be hidden.
191 241
      * @private
192 242
      * @returns {void}
193 243
      */
194
-    _showMuteOverlay(shouldShow) {
195
-        if (shouldShow) {
196
-            this._rootElement.classList.add(VIDEO_MUTE_CLASS);
244
+    _updateErrorView(error) {
245
+        if (error) {
246
+            this._rootElement.classList.add(VIDEO_ERROR_CLASS);
197 247
         } else {
198
-            this._rootElement.classList.remove(VIDEO_MUTE_CLASS);
248
+            this._rootElement.classList.remove(VIDEO_ERROR_CLASS);
199 249
         }
250
+
251
+        this._errorElement.innerText = error || '';
200 252
     }
201 253
 }
202 254
 

正在加载...
取消
保存