Pārlūkot izejas kodu

Merge pull request #657 from tsareg/switch_tracks_on_device_list_change

Switch local audio and video track when list of available devices changes
master
yanas 9 gadus atpakaļ
vecāks
revīzija
29f06bbb77

+ 250
- 13
conference.js Parādīt failu

@@ -22,6 +22,7 @@ const TrackEvents = JitsiMeetJS.events.track;
22 22
 const TrackErrors = JitsiMeetJS.errors.track;
23 23
 
24 24
 let room, connection, localAudio, localVideo, roomLocker;
25
+let currentAudioInputDevices, currentVideoInputDevices;
25 26
 
26 27
 import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
27 28
 
@@ -170,16 +171,25 @@ function hangup (requestFeedback = false) {
170 171
 
171 172
 /**
172 173
  * Create local tracks of specified types.
173
- * @param {string[]} devices required track types ('audio', 'video' etc.)
174
+ * @param {string[]} devices - required track types ('audio', 'video' etc.)
175
+ * @param {string|null} [cameraDeviceId] - camera device id, if undefined - one
176
+ *      from settings will be used
177
+ * @param {string|null} [micDeviceId] - microphone device id, if undefined - one
178
+ *      from settings will be used
174 179
  * @returns {Promise<JitsiLocalTrack[]>}
175 180
  */
176
-function createLocalTracks (...devices) {
181
+function createLocalTracks (devices, cameraDeviceId, micDeviceId) {
177 182
     return JitsiMeetJS.createLocalTracks({
178 183
         // copy array to avoid mutations inside library
179 184
         devices: devices.slice(0),
180 185
         resolution: config.resolution,
181
-        cameraDeviceId: APP.settings.getCameraDeviceId(),
182
-        micDeviceId: APP.settings.getMicDeviceId(),
186
+        cameraDeviceId: typeof cameraDeviceId === 'undefined'
187
+            || cameraDeviceId === null
188
+                ? APP.settings.getCameraDeviceId()
189
+                : cameraDeviceId,
190
+        micDeviceId: typeof micDeviceId === 'undefined' || micDeviceId === null
191
+            ? APP.settings.getMicDeviceId()
192
+            : micDeviceId,
183 193
         // adds any ff fake device settings if any
184 194
         firefox_fake_device: config.firefox_fake_device
185 195
     }).catch(function (err) {
@@ -188,6 +198,17 @@ function createLocalTracks (...devices) {
188 198
     });
189 199
 }
190 200
 
201
+/**
202
+ * Stores lists of current 'audioinput' and 'videoinput' devices
203
+ * @param {MediaDeviceInfo[]} devices
204
+ */
205
+function setCurrentMediaDevices(devices) {
206
+    currentAudioInputDevices = devices.filter(
207
+        d => d.kind === 'audioinput');
208
+    currentVideoInputDevices = devices.filter(
209
+        d => d.kind === 'videoinput');
210
+}
211
+
191 212
 class ConferenceConnector {
192 213
     constructor(resolve, reject) {
193 214
         this._resolve = resolve;
@@ -327,6 +348,7 @@ export default {
327 348
      * @returns {Promise}
328 349
      */
329 350
     init(options) {
351
+        let self = this;
330 352
         this.roomName = options.roomName;
331 353
         JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE);
332 354
 
@@ -356,9 +378,9 @@ export default {
356 378
         return JitsiMeetJS.init(config).then(() => {
357 379
             return Promise.all([
358 380
                 // try to retrieve audio and video
359
-                createLocalTracks('audio', 'video')
381
+                createLocalTracks(['audio', 'video'])
360 382
                 // if failed then try to retrieve only audio
361
-                    .catch(() => createLocalTracks('audio'))
383
+                    .catch(() => createLocalTracks(['audio']))
362 384
                 // if audio also failed then just return empty array
363 385
                     .catch(() => []),
364 386
                 connect(options.roomName)
@@ -370,15 +392,53 @@ export default {
370 392
             this.isDesktopSharingEnabled =
371 393
                 JitsiMeetJS.isDesktopSharingEnabled();
372 394
 
395
+            // if user didn't give access to mic or camera or doesn't have
396
+            // them at all, we disable corresponding toolbar buttons
397
+            if (!tracks.find((t) => t.isAudioTrack())) {
398
+                APP.UI.disableMicrophoneButton();
399
+            }
400
+
401
+            if (!tracks.find((t) => t.isVideoTrack())) {
402
+                APP.UI.disableCameraButton();
403
+            }
404
+
373 405
             // update list of available devices
374 406
             if (JitsiMeetJS.mediaDevices.isDeviceListAvailable() &&
375 407
                 JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) {
376
-                JitsiMeetJS.mediaDevices.enumerateDevices(
377
-                    APP.UI.onAvailableDevicesChanged);
408
+                JitsiMeetJS.mediaDevices.enumerateDevices(function(devices) {
409
+                    // Ugly way to synchronize real device IDs with local
410
+                    // storage and settings menu. This is a workaround until
411
+                    // getConstraints() method will be implemented in browsers.
412
+                    if (localAudio) {
413
+                        localAudio._setRealDeviceIdFromDeviceList(devices);
414
+                        APP.settings.setMicDeviceId(localAudio.getDeviceId());
415
+                    }
416
+
417
+                    if (localVideo) {
418
+                        localVideo._setRealDeviceIdFromDeviceList(devices);
419
+                        APP.settings.setCameraDeviceId(
420
+                            localVideo.getDeviceId());
421
+                    }
422
+
423
+                    setCurrentMediaDevices(devices);
424
+
425
+                    APP.UI.onAvailableDevicesChanged(devices);
426
+                });
378 427
 
379 428
                 JitsiMeetJS.mediaDevices.addEventListener(
380 429
                     JitsiMeetJS.events.mediaDevices.DEVICE_LIST_CHANGED,
381
-                    APP.UI.onAvailableDevicesChanged);
430
+                    (devices) => {
431
+                        // Just defer callback until other event callbacks are
432
+                        // processed.
433
+                        window.setTimeout(() => {
434
+                            checkLocalDevicesAfterDeviceListChanged(devices)
435
+                                .then(() => {
436
+                                    setCurrentMediaDevices(devices);
437
+
438
+                                    APP.UI.onAvailableDevicesChanged(devices);
439
+                                });
440
+                        }, 0);
441
+                    });
382 442
             }
383 443
             if (config.iAmRecorder)
384 444
                 this.recorder = new Recorder();
@@ -388,6 +448,180 @@ export default {
388 448
             return new Promise((resolve, reject) => {
389 449
                 (new ConferenceConnector(resolve, reject)).connect();
390 450
             });
451
+
452
+            function checkAudioOutputDeviceAfterDeviceListChanged(newDevices) {
453
+                if (!JitsiMeetJS.mediaDevices
454
+                        .isDeviceChangeAvailable('output')) {
455
+                    return;
456
+                }
457
+
458
+                var selectedAudioOutputDeviceId =
459
+                        APP.settings.getAudioOutputDeviceId(),
460
+                    availableAudioOutputDevices = newDevices.filter(d => {
461
+                        return d.kind === 'audiooutput';
462
+                    });
463
+
464
+                if (selectedAudioOutputDeviceId !== 'default' &&
465
+                    !availableAudioOutputDevices.find(d =>
466
+                    d.deviceId === selectedAudioOutputDeviceId)) {
467
+                    APP.settings.setAudioOutputDeviceId('default');
468
+                }
469
+            }
470
+
471
+            function checkLocalDevicesAfterDeviceListChanged(newDevices) {
472
+                // Event handler can be fire before direct enumerateDevices()
473
+                // call, so handle this situation here.
474
+                if (!currentAudioInputDevices && !currentVideoInputDevices) {
475
+                    setCurrentMediaDevices(newDevices);
476
+                }
477
+
478
+                checkAudioOutputDeviceAfterDeviceListChanged(newDevices);
479
+
480
+                let availableAudioInputDevices = newDevices.filter(
481
+                        d => d.kind === 'audioinput'),
482
+                    availableVideoInputDevices = newDevices.filter(
483
+                        d => d.kind === 'videoinput'),
484
+                    selectedAudioInputDeviceId = APP.settings.getMicDeviceId(),
485
+                    selectedVideoInputDeviceId =
486
+                        APP.settings.getCameraDeviceId(),
487
+                    selectedAudioInputDevice = availableAudioInputDevices.find(
488
+                        d => d.deviceId === selectedAudioInputDeviceId),
489
+                    selectedVideoInputDevice = availableVideoInputDevices.find(
490
+                        d => d.deviceId === selectedVideoInputDeviceId),
491
+                    tracksToCreate = [],
492
+                    micIdToUse = null,
493
+                    cameraIdToUse = null;
494
+
495
+                // Here we handle case when no device was initially plugged, but
496
+                // then it's connected OR new device was connected when previous
497
+                // track has ended.
498
+                if (!localAudio || localAudio.disposed || localAudio.isEnded()){
499
+                    if (availableAudioInputDevices.length
500
+                        && availableAudioInputDevices[0].label !== '') {
501
+                        tracksToCreate.push('audio');
502
+                        micIdToUse = availableAudioInputDevices[0].deviceId;
503
+                    } else {
504
+                        APP.UI.disableMicrophoneButton();
505
+                    }
506
+                }
507
+
508
+                if ((!localVideo || localVideo.disposed || localVideo.isEnded())
509
+                    && !self.isSharingScreen){
510
+                    if (availableVideoInputDevices.length
511
+                        && availableVideoInputDevices[0].label !== '') {
512
+                        tracksToCreate.push('video');
513
+                        cameraIdToUse = availableVideoInputDevices[0].deviceId;
514
+                    } else {
515
+                        APP.UI.disableCameraButton();
516
+                    }
517
+                }
518
+
519
+                if (localAudio && !localAudio.disposed && !localAudio.isEnded()
520
+                    && selectedAudioInputDevice
521
+                    && selectedAudioInputDeviceId !== localAudio.getDeviceId()
522
+                    && tracksToCreate.indexOf('audio') === -1) {
523
+                    tracksToCreate.push('audio');
524
+                    micIdToUse = selectedAudioInputDeviceId;
525
+                }
526
+
527
+                if (localVideo && !localVideo.disposed && !localVideo.isEnded()
528
+                    && selectedVideoInputDevice
529
+                    && selectedVideoInputDeviceId !== localVideo.getDeviceId()
530
+                    && tracksToCreate.indexOf('video') === -1
531
+                    && !self.isSharingScreen) {
532
+                    tracksToCreate.push('video');
533
+                    cameraIdToUse = selectedVideoInputDeviceId;
534
+                }
535
+
536
+                if (tracksToCreate.length) {
537
+                    return createNewTracks(
538
+                        tracksToCreate, cameraIdToUse, micIdToUse);
539
+                } else {
540
+                    return Promise.resolve();
541
+                }
542
+
543
+                function createNewTracks(type, cameraDeviceId, micDeviceId) {
544
+                    return createLocalTracks(type, cameraDeviceId, micDeviceId)
545
+                        .then(onTracksCreated)
546
+                        .catch(() => {
547
+                            // if we tried to create both audio and video tracks
548
+                            // at once and failed, let's try again only with
549
+                            // audio. Such situation may happen in case if we
550
+                            // granted access only to microphone, but not to
551
+                            // camera.
552
+                            if (type.indexOf('audio') !== -1
553
+                                && type.indexOf('video') !== -1) {
554
+                                return createLocalTracks(['audio'], null,
555
+                                    micDeviceId);
556
+                            }
557
+
558
+                        })
559
+                        .then(onTracksCreated)
560
+                        .catch(() => {
561
+                            // if we tried to create both audio and video tracks
562
+                            // at once and failed, let's try again only with
563
+                            // video. Such situation may happen in case if we
564
+                            // granted access only to camera, but not to
565
+                            // microphone.
566
+                            if (type.indexOf('audio') !== -1
567
+                                && type.indexOf('video') !== -1) {
568
+                                return createLocalTracks(['video'],
569
+                                    cameraDeviceId,
570
+                                    null);
571
+                            }
572
+                        })
573
+                        .then(onTracksCreated)
574
+                        .catch(() => {
575
+                            // can't do anything in this case, so just ignore;
576
+                        });
577
+                }
578
+
579
+                function onTracksCreated(tracks) {
580
+                    return Promise.all((tracks || []).map(track => {
581
+                        if (track.isAudioTrack()) {
582
+                            let audioWasMuted = self.audioMuted;
583
+
584
+                            return self.useAudioStream(track).then(() => {
585
+                                console.log('switched local audio');
586
+
587
+                                // If we plugged-in new device (and switched to
588
+                                // it), but video was muted before, or we
589
+                                // unplugged current device and selected new
590
+                                // one, then mute new video track.
591
+                                if (audioWasMuted ||
592
+                                    currentAudioInputDevices.length >
593
+                                    availableAudioInputDevices.length) {
594
+                                    muteLocalAudio(true);
595
+                                }
596
+                            });
597
+                        } else if (track.isVideoTrack()) {
598
+                            let videoWasMuted = self.videoMuted;
599
+
600
+                            return self.useVideoStream(track).then(() => {
601
+                                console.log('switched local video');
602
+
603
+                                // TODO: maybe make video large if we
604
+                                // are not in conference yet
605
+
606
+                                // If we plugged-in new device (and switched to
607
+                                // it), but video was muted before, or we
608
+                                // unplugged current device and selected new
609
+                                // one, then mute new video track.
610
+                                if (videoWasMuted ||
611
+                                    (currentVideoInputDevices.length >
612
+                                    availableVideoInputDevices.length)) {
613
+                                    muteLocalVideo(true);
614
+                                }
615
+                            });
616
+                        } else {
617
+                            console.error("Ignored not an audio nor a "
618
+                                + "video track: ", track);
619
+
620
+                            return Promise.resolve();
621
+                        }
622
+                    }));
623
+                }
624
+            }
391 625
         });
392 626
     },
393 627
     /**
@@ -667,6 +901,8 @@ export default {
667 901
                 this.isSharingScreen = false;
668 902
             }
669 903
 
904
+            stream.videoType === 'camera' && APP.UI.enableCameraButton();
905
+
670 906
             APP.UI.setVideoMuted(this.localId, this.videoMuted);
671 907
 
672 908
             APP.UI.updateDesktopSharingButtons();
@@ -701,6 +937,7 @@ export default {
701 937
                 this.audioMuted = false;
702 938
             }
703 939
 
940
+            APP.UI.enableMicrophoneButton();
704 941
             APP.UI.setAudioMuted(this.localId, this.audioMuted);
705 942
         });
706 943
     },
@@ -719,7 +956,7 @@ export default {
719 956
         this.videoSwitchInProgress = true;
720 957
 
721 958
         if (shareScreen) {
722
-            createLocalTracks('desktop').then(([stream]) => {
959
+            createLocalTracks(['desktop']).then(([stream]) => {
723 960
                 stream.on(
724 961
                     TrackEvents.LOCAL_TRACK_STOPPED,
725 962
                     () => {
@@ -767,7 +1004,7 @@ export default {
767 1004
                 );
768 1005
             });
769 1006
         } else {
770
-            createLocalTracks('video').then(
1007
+            createLocalTracks(['video']).then(
771 1008
                 ([stream]) => this.useVideoStream(stream)
772 1009
             ).then(() => {
773 1010
                 this.videoSwitchInProgress = false;
@@ -1118,7 +1355,7 @@ export default {
1118 1355
             UIEvents.VIDEO_DEVICE_CHANGED,
1119 1356
             (cameraDeviceId) => {
1120 1357
                 APP.settings.setCameraDeviceId(cameraDeviceId);
1121
-                createLocalTracks('video').then(([stream]) => {
1358
+                createLocalTracks(['video']).then(([stream]) => {
1122 1359
                     this.useVideoStream(stream);
1123 1360
                     console.log('switched local video device');
1124 1361
                 });
@@ -1129,7 +1366,7 @@ export default {
1129 1366
             UIEvents.AUDIO_DEVICE_CHANGED,
1130 1367
             (micDeviceId) => {
1131 1368
                 APP.settings.setMicDeviceId(micDeviceId);
1132
-                createLocalTracks('audio').then(([stream]) => {
1369
+                createLocalTracks(['audio']).then(([stream]) => {
1133 1370
                     this.useAudioStream(stream);
1134 1371
                     console.log('switched local audio device');
1135 1372
                 });

+ 4
- 0
css/main.css Parādīt failu

@@ -58,6 +58,10 @@ html, body{
58 58
     vertical-align: middle;
59 59
 }
60 60
 
61
+.button[disabled] {
62
+    opacity: 0.5;
63
+}
64
+
61 65
 .toolbar_span>span {
62 66
     display: inline-block;
63 67
     position: absolute;

+ 6
- 2
lang/main.json Parādīt failu

@@ -66,7 +66,9 @@
66 66
         "dialpad": "Show dialpad",
67 67
         "sharedVideoMutedPopup": "Your shared video has been muted so<br/>that you can talk to the other participants.",
68 68
         "micMutedPopup": "Your microphone has been muted so that you<br/>would fully enjoy your shared video.",
69
-        "unableToUnmutePopup": "You cannot un-mute while the shared video is on."
69
+        "unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
70
+        "cameraDisabled": "Camera is not available",
71
+        "micDisabled": "Microphone is not available"
70 72
     },
71 73
     "bottomtoolbar": {
72 74
         "chat": "Open / close chat",
@@ -90,7 +92,9 @@
90 92
         "selectCamera": "Select camera",
91 93
         "selectMic": "Select microphone",
92 94
         "selectAudioOutput": "Select audio output",
93
-        "followMe": "Enable follow me"
95
+        "followMe": "Enable follow me",
96
+        "noDevice": "None",
97
+        "noPermission": "Permission to use device is not granted"
94 98
     },
95 99
     "videothumbnail":
96 100
     {

+ 28
- 0
modules/UI/UI.js Parādīt failu

@@ -1142,4 +1142,32 @@ UI.onSharedVideoStop = function (id, attributes) {
1142 1142
         sharedVideoManager.onSharedVideoStop(id, attributes);
1143 1143
 };
1144 1144
 
1145
+/**
1146
+ * Disables camera toolbar button.
1147
+ */
1148
+UI.disableCameraButton = function () {
1149
+    Toolbar.markVideoIconAsDisabled(true);
1150
+};
1151
+
1152
+/**
1153
+ * Enables camera toolbar button.
1154
+ */
1155
+UI.enableCameraButton = function () {
1156
+    Toolbar.markVideoIconAsDisabled(false);
1157
+};
1158
+
1159
+/**
1160
+ * Disables microphone toolbar button.
1161
+ */
1162
+UI.disableMicrophoneButton = function () {
1163
+    Toolbar.markAudioIconAsDisabled(true);
1164
+};
1165
+
1166
+/**
1167
+ * Enables microphone toolbar button.
1168
+ */
1169
+UI.enableMicrophoneButton = function () {
1170
+    Toolbar.markAudioIconAsDisabled(false);
1171
+};
1172
+
1145 1173
 module.exports = UI;

+ 79
- 46
modules/UI/side_pannels/settings/SettingsMenu.js Parādīt failu

@@ -30,10 +30,16 @@ function generateLanguagesOptions(items, currentLang) {
30 30
  * Generate html select options for available physical devices.
31 31
  * @param {{ deviceId, label }[]} items available devices
32 32
  * @param {string} [selectedId] id of selected device
33
+ * @param {boolean} permissionGranted if permission to use selected device type
34
+ *      is granted
33 35
  * @returns {string}
34 36
  */
35
-function generateDevicesOptions(items, selectedId) {
36
-    return items.map(function (item) {
37
+function generateDevicesOptions(items, selectedId, permissionGranted) {
38
+    if (!permissionGranted && items.length) {
39
+        return '<option data-i18n="settings.noPermission"></option>';
40
+    }
41
+
42
+    var options = items.map(function (item) {
37 43
         let attrs = {
38 44
             value: item.deviceId
39 45
         };
@@ -44,7 +50,13 @@ function generateDevicesOptions(items, selectedId) {
44 50
 
45 51
         let attrsStr = UIUtil.attrsToString(attrs);
46 52
         return `<option ${attrsStr}>${item.label}</option>`;
47
-    }).join('\n');
53
+    });
54
+
55
+    if (!items.length) {
56
+        options.unshift('<option data-i18n="settings.noDevice"></option>');
57
+    }
58
+
59
+    return options.join('');
48 60
 }
49 61
 
50 62
 
@@ -111,26 +123,30 @@ export default {
111 123
 
112 124
 
113 125
         // DEVICES LIST
114
-        this.changeDevicesList([]);
115
-        $('#selectCamera').change(function () {
116
-            let cameraDeviceId = $(this).val();
117
-            if (cameraDeviceId !== Settings.getCameraDeviceId()) {
118
-                emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId);
119
-            }
120
-        });
121
-        $('#selectMic').change(function () {
122
-            let micDeviceId = $(this).val();
123
-            if (micDeviceId !== Settings.getMicDeviceId()) {
124
-                emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId);
125
-            }
126
-        });
127
-        $('#selectAudioOutput').change(function () {
128
-            let audioOutputDeviceId = $(this).val();
129
-            if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) {
130
-                emitter.emit(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
131
-                    audioOutputDeviceId);
132
-            }
133
-        });
126
+        if (JitsiMeetJS.mediaDevices.isDeviceListAvailable() &&
127
+            JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) {
128
+            this.changeDevicesList([]);
129
+
130
+            $('#selectCamera').change(function () {
131
+                let cameraDeviceId = $(this).val();
132
+                if (cameraDeviceId !== Settings.getCameraDeviceId()) {
133
+                    emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId);
134
+                }
135
+            });
136
+            $('#selectMic').change(function () {
137
+                let micDeviceId = $(this).val();
138
+                if (micDeviceId !== Settings.getMicDeviceId()) {
139
+                    emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId);
140
+                }
141
+            });
142
+            $('#selectAudioOutput').change(function () {
143
+                let audioOutputDeviceId = $(this).val();
144
+                if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) {
145
+                    emitter.emit(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
146
+                        audioOutputDeviceId);
147
+                }
148
+            });
149
+        }
134 150
     },
135 151
 
136 152
     /**
@@ -193,41 +209,58 @@ export default {
193 209
      * @param {{ deviceId, label, kind }[]} devices list of available devices
194 210
      */
195 211
     changeDevicesList (devices) {
196
-        let $devicesOptions =  $('#devicesOptions');
197
-
198
-        if (!devices.length) {
199
-            $devicesOptions.hide();
200
-            return;
201
-        }
202
-
203 212
         let $selectCamera= $('#selectCamera'),
204 213
             $selectMic = $('#selectMic'),
205 214
             $selectAudioOutput = $('#selectAudioOutput'),
206 215
             $selectAudioOutputParent = $selectAudioOutput.parent();
207 216
 
208
-        let audio = devices.filter(device => device.kind === 'audioinput');
209
-        let video = devices.filter(device => device.kind === 'videoinput');
210
-        let audioOutput = devices
211
-            .filter(device => device.kind === 'audiooutput');
217
+        let audio = devices.filter(device => device.kind === 'audioinput'),
218
+            video = devices.filter(device => device.kind === 'videoinput'),
219
+            audioOutput = devices
220
+                .filter(device => device.kind === 'audiooutput'),
221
+            selectedAudioDevice = audio.find(
222
+                d => d.deviceId === Settings.getMicDeviceId()) || audio[0],
223
+            selectedVideoDevice = video.find(
224
+                d => d.deviceId === Settings.getCameraDeviceId()) || video[0],
225
+            selectedAudioOutputDevice = audioOutput.find(
226
+                    d => d.deviceId === Settings.getAudioOutputDeviceId()),
227
+            videoPermissionGranted =
228
+                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video'),
229
+            audioPermissionGranted =
230
+                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio');
212 231
 
213
-        $selectCamera.html(
214
-            generateDevicesOptions(video, Settings.getCameraDeviceId())
215
-        );
216
-        $selectMic.html(
217
-            generateDevicesOptions(audio, Settings.getMicDeviceId())
218
-        );
232
+        $selectCamera
233
+            .html(generateDevicesOptions(
234
+                video,
235
+                selectedVideoDevice ? selectedVideoDevice.deviceId : '',
236
+                videoPermissionGranted))
237
+            .prop('disabled', !video.length || !videoPermissionGranted);
219 238
 
220
-        if (audioOutput.length &&
221
-            JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
222
-            $selectAudioOutput.html(
223
-                generateDevicesOptions(audioOutput,
224
-                    Settings.getAudioOutputDeviceId()));
239
+        $selectMic
240
+            .html(generateDevicesOptions(
241
+                audio,
242
+                selectedAudioDevice ? selectedAudioDevice.deviceId : '',
243
+                audioPermissionGranted))
244
+            .prop('disabled', !audio.length || !audioPermissionGranted);
245
+
246
+        if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
247
+            $selectAudioOutput
248
+                .html(generateDevicesOptions(
249
+                    audioOutput,
250
+                    selectedAudioOutputDevice
251
+                        ? selectedAudioOutputDevice.deviceId
252
+                        : 'default',
253
+                    videoPermissionGranted || audioPermissionGranted))
254
+                .prop('disabled', !audioOutput.length ||
255
+                    (!videoPermissionGranted && !audioPermissionGranted));
225 256
 
226 257
             $selectAudioOutputParent.show();
227 258
         } else {
228 259
             $selectAudioOutputParent.hide();
229 260
         }
230 261
 
231
-        $devicesOptions.show();
262
+        $('#devicesOptions').show();
263
+
264
+        APP.translation.translateElement($('#settingsmenu option'));
232 265
     }
233 266
 };

+ 49
- 1
modules/UI/toolbars/Toolbar.js Parādīt failu

@@ -191,7 +191,9 @@ const Toolbar = {
191 191
         UIUtil.hideDisabledButtons(defaultToolbarButtons);
192 192
 
193 193
         Object.keys(buttonHandlers).forEach(
194
-            buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
194
+            buttonId => $(`#${buttonId}`).click(function(event) {
195
+                !$(this).prop('disabled') && buttonHandlers[buttonId](event);
196
+            })
195 197
         );
196 198
     },
197 199
     /**
@@ -361,6 +363,29 @@ const Toolbar = {
361 363
         $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted);
362 364
     },
363 365
 
366
+    /**
367
+     * Marks video icon as disabled or not.
368
+     * @param {boolean} disabled if icon should look like disabled or not
369
+     */
370
+    markVideoIconAsDisabled (disabled) {
371
+        var $btn = $('#toolbar_button_camera');
372
+
373
+        $btn
374
+            .prop("disabled", disabled)
375
+            .attr("data-i18n", disabled
376
+                ? "[content]toolbar.cameraDisabled"
377
+                : "[content]toolbar.videomute")
378
+            .attr("shortcut", disabled ? "" : "toggleVideoPopover");
379
+
380
+        disabled
381
+            ? $btn.attr("disabled", "disabled")
382
+            : $btn.removeAttr("disabled");
383
+
384
+        APP.translation.translateElement($btn);
385
+
386
+        disabled && this.markVideoIconAsMuted(disabled);
387
+    },
388
+
364 389
     /**
365 390
      * Marks audio icon as muted or not.
366 391
      * @param {boolean} muted if icon should look like muted or not
@@ -370,6 +395,29 @@ const Toolbar = {
370 395
             !muted).toggleClass("icon-mic-disabled", muted);
371 396
     },
372 397
 
398
+    /**
399
+     * Marks audio icon as disabled or not.
400
+     * @param {boolean} disabled if icon should look like disabled or not
401
+     */
402
+    markAudioIconAsDisabled (disabled) {
403
+        var $btn = $('#toolbar_button_mute');
404
+
405
+        $btn
406
+            .prop("disabled", disabled)
407
+            .attr("data-i18n", disabled
408
+                ? "[content]toolbar.micDisabled"
409
+                : "[content]toolbar.mute")
410
+            .attr("shortcut", disabled ? "" : "mutePopover");
411
+
412
+        disabled
413
+            ? $btn.attr("disabled", "disabled")
414
+            : $btn.removeAttr("disabled");
415
+
416
+        APP.translation.translateElement($btn);
417
+
418
+        disabled && this.markAudioIconAsMuted(disabled);
419
+    },
420
+
373 421
     /**
374 422
      * Indicates if the toolbar is currently hovered.
375 423
      * @return {true} if the toolbar is currently hovered, {false} otherwise

+ 11
- 8
modules/settings/Settings.js Parādīt failu

@@ -43,12 +43,15 @@ if (supportsLocalStorage()) {
43 43
         window.localStorage.welcomePageDisabled || false
44 44
     );
45 45
 
46
-    var audioOutputDeviceId = window.localStorage.audioOutputDeviceId;
47
-
48
-    if (typeof audioOutputDeviceId !== 'undefined' && audioOutputDeviceId !==
49
-            JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
50
-        JitsiMeetJS.mediaDevices.setAudioOutputDevice(
51
-            window.localStorage.audioOutputDeviceId).catch((ex) => {
46
+    // Currently audio output device change is supported only in Chrome and
47
+    // default output always has 'default' device ID
48
+    var audioOutputDeviceId = window.localStorage.audioOutputDeviceId
49
+        || 'default';
50
+
51
+    if (audioOutputDeviceId !==
52
+        JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
53
+        JitsiMeetJS.mediaDevices.setAudioOutputDevice(audioOutputDeviceId)
54
+            .catch((ex) => {
52 55
                 console.error('failed to set audio output device from local ' +
53 56
                     'storage', ex);
54 57
             });
@@ -166,10 +169,10 @@ export default {
166 169
     /**
167 170
      * Set device id of the audio output device which is currently in use.
168 171
      * Empty string stands for default device.
169
-     * @param {string} newId new audio output device id
172
+     * @param {string} newId='default' - new audio output device id
170 173
      * @returns {Promise}
171 174
      */
172
-    setAudioOutputDeviceId: function (newId = '') {
175
+    setAudioOutputDeviceId: function (newId = 'default') {
173 176
         return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
174 177
             .then(() => window.localStorage.audioOutputDeviceId = newId);
175 178
     },

Notiek ielāde…
Atcelt
Saglabāt