瀏覽代碼

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
                 border-radius: 3px;
79
                 border-radius: 3px;
80
             }
80
             }
81
 
81
 
82
-            .video-input-preview-muted {
82
+            .video-input-preview-error {
83
                 color: $participantNameColor;
83
                 color: $participantNameColor;
84
                 display: none;
84
                 display: none;
85
                 left: 0;
85
                 left: 0;
89
                 top: 50%;
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
                 background: black;
93
                 background: black;
95
-                /* TOFIX-END */
96
 
94
 
97
-                .video-input-preview-muted {
95
+                .video-input-preview-error {
98
                     display: block;
96
                     display: block;
99
                 }
97
                 }
100
             }
98
             }

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

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

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

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

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

34
          * All known audio and video devices split by type. This prop comes from
34
          * All known audio and video devices split by type. This prop comes from
35
          * the app state.
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
          * Whether or not the audio selector can be interacted with. If true,
58
          * Whether or not the audio selector can be interacted with. If true,
78
         dispatch: React.PropTypes.func,
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
         hasAudioPermission: React.PropTypes.bool,
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
         hasVideoPermission: React.PropTypes.bool,
84
         hasVideoPermission: React.PropTypes.bool,
89
 
85
 
117
     constructor(props) {
113
     constructor(props) {
118
         super(props);
114
         super(props);
119
 
115
 
116
+        const { _availableDevices } = this.props;
117
+
120
         this.state = {
118
         this.state = {
121
-            // JitsiLocalTracks to use for live previewing.
119
+            // JitsiLocalTrack to use for live previewing of audio input.
122
             previewAudioTrack: null,
120
             previewAudioTrack: null,
121
+
122
+            // JitsiLocalTrack to use for live previewing of video input.
123
             previewVideoTrack: null,
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
         // Preventing closing while cleaning up previews is important for
152
         // Preventing closing while cleaning up previews is important for
134
         // closure until cleanup is complete ensures no errors in the process.
155
         // closure until cleanup is complete ensures no errors in the process.
135
         this._isClosing = false;
156
         this._isClosing = false;
136
 
157
 
158
+        // Bind event handlers so they are only bound once for every instance.
137
         this._closeModal = this._closeModal.bind(this);
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
         this._onCancel = this._onCancel.bind(this);
160
         this._onCancel = this._onCancel.bind(this);
142
         this._onSubmit = this._onSubmit.bind(this);
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
      * @inheritdoc
182
      * @inheritdoc
149
      */
183
      */
173
                     <div className = 'device-selection-column column-video'>
207
                     <div className = 'device-selection-column column-video'>
174
                         <div className = 'device-selection-video-container'>
208
                         <div className = 'device-selection-video-container'>
175
                             <VideoInputPreview
209
                             <VideoInputPreview
176
-                                track = { this.state.previewVideoTrack
177
-                                    || this.props.currentVideoTrack } />
210
+                                error = { this.state.previewVideoTrackError }
211
+                                track = { this.state.previewVideoTrack } />
178
                         </div>
212
                         </div>
179
                         { this._renderAudioInputPreview() }
213
                         { this._renderAudioInputPreview() }
180
                     </div>
214
                     </div>
197
      * promise can be for video cleanup and another for audio cleanup.
231
      * promise can be for video cleanup and another for audio cleanup.
198
      */
232
      */
199
     _attemptPreviewTrackCleanup() {
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
     }
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
      * @private
275
      * @private
389
      * @returns {boolean} Returns false to prevent closure until cleanup is
276
      * @returns {boolean} Returns false to prevent closure until cleanup is
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
      * necessary cleanup and requests to use those devices. Closes the modal
297
      * necessary cleanup and requests to use those devices. Closes the modal
411
      * after cleanup and device change requests complete.
298
      * after cleanup and device change requests complete.
412
      *
299
      *
421
 
308
 
422
         this._isClosing = true;
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
         Promise.all(deviceChangePromises)
332
         Promise.all(deviceChangePromises)
452
             .then(this._closeModal)
333
             .then(this._closeModal)
470
 
351
 
471
         return (
352
         return (
472
             <AudioInputPreview
353
             <AudioInputPreview
473
-                track = { this.state.previewAudioTrack
474
-                    || this.props.currentAudioTrack } />
354
+                track = { this.state.previewAudioTrack } />
475
         );
355
         );
476
     }
356
     }
477
 
357
 
489
 
369
 
490
         return (
370
         return (
491
             <AudioOutputPreview
371
             <AudioOutputPreview
492
-                deviceId = { this.state.audioOutput
493
-                    || this.props.currentAudioOutputId } />
372
+                deviceId = { this.state.selectedAudioOutputId } />
494
         );
373
         );
495
     }
374
     }
496
 
375
 
515
      * @returns {Array<ReactElement>} DeviceSelector instances.
394
      * @returns {Array<ReactElement>} DeviceSelector instances.
516
      */
395
      */
517
     _renderSelectors() {
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
         const configurations = [
398
         const configurations = [
543
             {
399
             {
544
-                devices: availableDevices.videoInput,
400
+                devices: _availableDevices.videoInput,
545
                 hasPermission: this.props.hasVideoPermission,
401
                 hasPermission: this.props.hasVideoPermission,
546
                 icon: 'icon-camera',
402
                 icon: 'icon-camera',
547
                 isDisabled: this.props.disableDeviceChange,
403
                 isDisabled: this.props.disableDeviceChange,
548
                 key: 'videoInput',
404
                 key: 'videoInput',
549
                 label: 'settings.selectCamera',
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
                 hasPermission: this.props.hasAudioPermission,
411
                 hasPermission: this.props.hasAudioPermission,
556
                 icon: 'icon-microphone',
412
                 icon: 'icon-microphone',
557
                 isDisabled: this.props.disableAudioInputChange
413
                 isDisabled: this.props.disableAudioInputChange
558
                     || this.props.disableDeviceChange,
414
                     || this.props.disableDeviceChange,
559
                 key: 'audioInput',
415
                 key: 'audioInput',
560
                 label: 'settings.selectMic',
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
         if (!this.props.hideAudioOutputSelect) {
422
         if (!this.props.hideAudioOutputSelect) {
567
             configurations.push({
423
             configurations.push({
568
-                devices: availableDevices.audioOutput,
424
+                devices: _availableDevices.audioOutput,
569
                 hasPermission: this.props.hasAudioPermission
425
                 hasPermission: this.props.hasAudioPermission
570
                     || this.props.hasVideoPermission,
426
                     || this.props.hasVideoPermission,
571
                 icon: 'icon-volume',
427
                 icon: 'icon-volume',
572
                 isDisabled: this.props.disableDeviceChange,
428
                 isDisabled: this.props.disableDeviceChange,
573
                 key: 'audioOutput',
429
                 key: 'audioOutput',
574
                 label: 'settings.selectAudioOutput',
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
         return configurations.map(this._renderSelector);
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
  * @param {Object} state - The Redux state.
517
  * @param {Object} state - The Redux state.
589
  * @private
518
  * @private
590
  * @returns {{
519
  * @returns {{
591
- *     _devices: Object
520
+ *     _availableDevices: Object
592
  * }}
521
  * }}
593
  */
522
  */
594
 function _mapStateToProps(state) {
523
 function _mapStateToProps(state) {
595
     return {
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
 
2
 
3
 import { translate } from '../../base/i18n';
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
  * React component for displaying video. This component defers to lib-jitsi-meet
8
  * React component for displaying video. This component defers to lib-jitsi-meet
17
      * @static
17
      * @static
18
      */
18
      */
19
     static propTypes = {
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
          * Invoked to obtain translated strings.
27
          * Invoked to obtain translated strings.
22
          */
28
          */
23
         t: React.PropTypes.func,
29
         t: React.PropTypes.func,
24
 
30
 
25
-        /*
31
+        /**
26
          * The JitsiLocalTrack to display.
32
          * The JitsiLocalTrack to display.
27
          */
33
          */
28
         track: React.PropTypes.object
34
         track: React.PropTypes.object
37
     constructor(props) {
43
     constructor(props) {
38
         super(props);
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
         this._rootElement = null;
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
         this._videoElement = null;
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
         this._setRootElement = this._setRootElement.bind(this);
77
         this._setRootElement = this._setRootElement.bind(this);
44
         this._setVideoElement = this._setVideoElement.bind(this);
78
         this._setVideoElement = this._setVideoElement.bind(this);
45
     }
79
     }
51
      * @returns {void}
85
      * @returns {void}
52
      */
86
      */
53
     componentDidMount() {
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
                     autoPlay = { true }
118
                     autoPlay = { true }
81
                     className = 'video-input-preview-display flipVideoX'
119
                     className = 'video-input-preview-display flipVideoX'
82
                     ref = { this._setVideoElement } />
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
             </div>
124
             </div>
87
         );
125
         );
88
     }
126
     }
99
      * @returns {void}
137
      * @returns {void}
100
      */
138
      */
101
     shouldComponentUpdate(nextProps) {
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
             this._detachTrack(this.props.track);
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
             this._attachTrack(nextProps.track);
149
             this._attachTrack(nextProps.track);
105
         }
150
         }
106
 
151
 
123
             return;
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
         }
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
      * Sets the component's root element.
213
      * Sets the component's root element.
164
      *
214
      *
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
      * @private
241
      * @private
192
      * @returns {void}
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
         } else {
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
 

Loading…
取消
儲存