Browse Source

Merge pull request #1509 from virtuacoplenny/lenny/web-audio-only

Audio only mode for web
master
yanas 8 years ago
parent
commit
166fb1d13f
37 changed files with 1177 additions and 574 deletions
  1. 74
    1
      conference.js
  2. 48
    42
      css/_font.scss
  3. 5
    1
      css/_toolbars.scss
  4. 17
    2
      css/_videolayout_default.scss
  5. 3
    5
      css/modals/device-selection/_device-selection.scss
  6. BIN
      fonts/jitsi.eot
  7. 3
    1
      fonts/jitsi.svg
  8. BIN
      fonts/jitsi.ttf
  9. BIN
      fonts/jitsi.woff
  10. 303
    217
      fonts/selection.json
  11. 1
    1
      interface_config.js
  12. 7
    1
      lang/main.json
  13. 8
    0
      modules/UI/UI.js
  14. 5
    1
      modules/UI/videolayout/ConnectionIndicator.js
  15. 20
    7
      modules/UI/videolayout/LargeVideoManager.js
  16. 1
    0
      modules/UI/videolayout/RemoteVideo.js
  17. 3
    1
      modules/UI/videolayout/SmallVideo.js
  18. 19
    13
      modules/UI/videolayout/VideoLayout.js
  19. 11
    0
      react/features/base/conference/actionTypes.js
  20. 18
    0
      react/features/base/conference/actions.js
  21. 8
    0
      react/features/base/conference/middleware.js
  22. 23
    1
      react/features/base/conference/reducer.js
  23. 2
    3
      react/features/conference/components/Conference.web.js
  24. 3
    6
      react/features/device-selection/actions.js
  25. 174
    245
      react/features/device-selection/components/DeviceSelectionDialog.js
  26. 77
    25
      react/features/device-selection/components/VideoInputPreview.js
  27. 103
    0
      react/features/toolbox/components/AudioOnlyButton.js
  28. 11
    0
      react/features/toolbox/components/Toolbar.web.js
  29. 1
    1
      react/features/toolbox/components/ToolbarButton.web.js
  30. 1
    0
      react/features/toolbox/components/index.js
  31. 32
    0
      react/features/toolbox/defaultToolbarButtons.js
  32. 104
    0
      react/features/video-status-label/components/AudioOnlyLabel.js
  33. 16
    0
      react/features/video-status-label/components/HDVideoLabel.js
  34. 69
    0
      react/features/video-status-label/components/VideoStatusLabel.js
  35. 1
    0
      react/features/video-status-label/components/index.js
  36. 1
    0
      react/features/video-status-label/index.js
  37. 5
    0
      service/UI/UIEvents.js

+ 74
- 1
conference.js View File

@@ -1085,6 +1085,43 @@ export default {
1085 1085
             });
1086 1086
     },
1087 1087
 
1088
+    /**
1089
+     * Triggers a tooltip to display when a feature was attempted to be used
1090
+     * while in audio only mode.
1091
+     *
1092
+     * @param {string} featureName - The name of the feature that attempted to
1093
+     * toggle.
1094
+     * @private
1095
+     * @returns {void}
1096
+     */
1097
+    _displayAudioOnlyTooltip(featureName) {
1098
+        let tooltipElementId = null;
1099
+
1100
+        switch (featureName) {
1101
+        case 'screenShare':
1102
+            tooltipElementId = '#screenshareWhileAudioOnly';
1103
+            break;
1104
+        case 'videoMute':
1105
+            tooltipElementId = '#unmuteWhileAudioOnly';
1106
+            break;
1107
+        }
1108
+
1109
+        if (tooltipElementId) {
1110
+            APP.UI.showToolbar(6000);
1111
+            APP.UI.showCustomToolbarPopup(
1112
+                tooltipElementId, true, 5000);
1113
+        }
1114
+    },
1115
+
1116
+    /**
1117
+     * Returns whether or not the conference is currently in audio only mode.
1118
+     *
1119
+     * @returns {boolean}
1120
+     */
1121
+    isAudioOnly() {
1122
+        return Boolean(
1123
+            APP.store.getState()['features/base/conference'].audioOnly);
1124
+    },
1088 1125
 
1089 1126
     videoSwitchInProgress: false,
1090 1127
     toggleScreenSharing(shareScreen = !this.isSharingScreen) {
@@ -1097,6 +1134,11 @@ export default {
1097 1134
             return;
1098 1135
         }
1099 1136
 
1137
+        if (this.isAudioOnly()) {
1138
+            this._displayAudioOnlyTooltip('screenShare');
1139
+            return;
1140
+        }
1141
+
1100 1142
         this.videoSwitchInProgress = true;
1101 1143
         let externalInstallation = false;
1102 1144
 
@@ -1400,6 +1442,10 @@ export default {
1400 1442
                 }
1401 1443
             });
1402 1444
 
1445
+            APP.UI.addListener(
1446
+                UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
1447
+                () => this._displayAudioOnlyTooltip('videoMute'));
1448
+
1403 1449
             APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
1404 1450
             (smallVideo, isPinned) => {
1405 1451
                 let smallVideoId = smallVideo.getId();
@@ -1512,7 +1558,14 @@ export default {
1512 1558
         });
1513 1559
 
1514 1560
         APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio);
1515
-        APP.UI.addListener(UIEvents.VIDEO_MUTED, muteLocalVideo);
1561
+        APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
1562
+            if (this.isAudioOnly() && !muted) {
1563
+                this._displayAudioOnlyTooltip('videoMute');
1564
+                return;
1565
+            }
1566
+
1567
+            muteLocalVideo(muted);
1568
+        });
1516 1569
 
1517 1570
         room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED,
1518 1571
             (stats) => {
@@ -1661,6 +1714,14 @@ export default {
1661 1714
                     micDeviceId: null
1662 1715
                 })
1663 1716
                 .then(([stream]) => {
1717
+                    if (this.isAudioOnly()) {
1718
+                        return stream.mute()
1719
+                            .then(() => stream);
1720
+                    }
1721
+
1722
+                    return stream;
1723
+                })
1724
+                .then(stream => {
1664 1725
                     this.useVideoStream(stream);
1665 1726
                     logger.log('switched local video device');
1666 1727
                     APP.settings.setCameraDeviceId(cameraDeviceId, true);
@@ -1707,6 +1768,18 @@ export default {
1707 1768
             }
1708 1769
         );
1709 1770
 
1771
+        APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
1772
+            muteLocalVideo(audioOnly);
1773
+
1774
+            // Immediately update the UI by having remote videos and the large
1775
+            // video update themselves instead of waiting for some other event
1776
+            // to cause the update, usually PARTICIPANT_CONN_STATUS_CHANGED.
1777
+            // There is no guarantee another event will trigger the update
1778
+            // immediately and in all situations, for example because a remote
1779
+            // participant is having connection trouble so no status changes.
1780
+            APP.UI.updateAllVideos();
1781
+        });
1782
+
1710 1783
         APP.UI.addListener(
1711 1784
             UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
1712 1785
         );

+ 48
- 42
css/_font.scss View File

@@ -26,119 +26,125 @@
26 26
 }
27 27
 
28 28
 .icon-mic-camera-combined:before {
29
-  content: "\e903";
29
+    content: "\e903";
30 30
 }
31 31
 .icon-feedback:before {
32
-  content: "\e91d";
32
+    content: "\e91d";
33 33
 }
34 34
 .icon-toggle-filmstrip:before {
35
-  content: "\e91c";
35
+    content: "\e91c";
36 36
 }
37 37
 .icon-avatar:before {
38
-  content: "\e901";
38
+    content: "\e901";
39 39
 }
40 40
 .icon-hangup:before {
41
-  content: "\e905";
41
+    content: "\e905";
42 42
 }
43 43
 .icon-chat:before {
44
-  content: "\e906";
44
+    content: "\e906";
45 45
 }
46 46
 .icon-download:before {
47
-  content: "\e902";
48
-}
49
-.icon-dialpad:before {
50
-  content: "\e61c";
47
+    content: "\e902";
51 48
 }
52 49
 .icon-edit:before {
53
-  content: "\e907";
50
+    content: "\e907";
54 51
 }
55 52
 .icon-share-doc:before {
56
-  content: "\e908";
53
+    content: "\e908";
57 54
 }
58 55
 .icon-telephone:before {
59
-  content: "\e909";
56
+    content: "\e909";
60 57
 }
61 58
 .icon-kick:before {
62
-  content: "\e904";
59
+    content: "\e904";
63 60
 }
64 61
 .icon-menu-up:before {
65
-  content: "\e91f";
62
+    content: "\e91f";
66 63
 }
67 64
 .icon-menu-down:before {
68
-  content: "\e920";
65
+    content: "\e920";
69 66
 }
70 67
 .icon-full-screen:before {
71
-  content: "\e90b";
68
+    content: "\e90b";
72 69
 }
73 70
 .icon-exit-full-screen:before {
74
-  content: "\e90c";
71
+    content: "\e90c";
75 72
 }
76 73
 .icon-star-full:before {
77
-  content: "\e90a";
74
+    content: "\e90a";
78 75
 }
79 76
 .icon-security:before {
80
-  content: "\e90d";
77
+    content: "\e90d";
81 78
 }
82 79
 .icon-security-locked:before {
83
-  content: "\e90e";
80
+    content: "\e90e";
84 81
 }
85 82
 .icon-reload:before {
86
-  content: "\e90f";
83
+    content: "\e90f";
87 84
 }
88 85
 .icon-microphone:before {
89
-  content: "\e910";
86
+    content: "\e910";
90 87
 }
91 88
 .icon-mic-empty:before {
92
-  content: "\e911";
89
+    content: "\e911";
93 90
 }
94 91
 .icon-mic-disabled:before {
95
-  content: "\e912";
92
+    content: "\e912";
96 93
 }
97 94
 .icon-raised-hand:before {
98
-  content: "\e91e";
95
+    content: "\e91e";
99 96
 }
100 97
 .icon-contactList:before {
101
-  content: "\e91b";
98
+    content: "\e91b";
102 99
 }
103 100
 .icon-link:before {
104
-  content: "\e913";
101
+    content: "\e913";
105 102
 }
106 103
 .icon-shared-video:before {
107
-  content: "\e914";
104
+    content: "\e914";
108 105
 }
109 106
 .icon-settings:before {
110
-  content: "\e915";
107
+    content: "\e915";
111 108
 }
112 109
 .icon-star:before {
113
-  content: "\e916";
110
+    content: "\e916";
114 111
 }
115 112
 .icon-switch-camera:before {
116
-  content: "\e921";
113
+    content: "\e921";
117 114
 }
118 115
 .icon-share-desktop:before {
119
-  content: "\e917";
116
+    content: "\e917";
120 117
 }
121 118
 .icon-camera:before {
122
-  content: "\e918";
119
+    content: "\e918";
123 120
 }
124 121
 .icon-camera-disabled:before {
125
-  content: "\e919";
122
+    content: "\e919";
126 123
 }
127 124
 .icon-volume:before {
128
-  content: "\e91a";
125
+    content: "\e91a";
129 126
 }
130 127
 .icon-connection-lost:before {
131
-  content: "\e900";
128
+    content: "\e900";
132 129
 }
133 130
 .icon-connection:before {
134
-  content: "\e61a";
131
+    content: "\e61a";
135 132
 }
136 133
 .icon-recDisable:before {
137
-  content: "\e613";
134
+    content: "\e613";
138 135
 }
139 136
 .icon-recEnable:before {
140
-  content: "\e614";
137
+    content: "\e614";
141 138
 }
142 139
 .icon-presentation:before {
143
-  content: "\e603";
144
-}
140
+    content: "\e603";
141
+}
142
+.icon-dialpad:before {
143
+    content: "\e925";
144
+}
145
+.icon-visibility:before {
146
+    content: "\e923";
147
+}
148
+.icon-visibility-off:before {
149
+    content: "\e924";
150
+}

+ 5
- 1
css/_toolbars.scss View File

@@ -71,6 +71,10 @@
71 71
         &.icon-microphone {
72 72
             @extend .icon-mic-disabled;
73 73
         }
74
+
75
+        &.icon-visibility {
76
+            @extend .icon-visibility-off;
77
+        }
74 78
     }
75 79
 
76 80
     &.unclickable {
@@ -170,7 +174,7 @@
170 174
         width: $defaultToolbarSize;
171 175
         -webkit-transform: translateX(-100%);
172 176
 
173
-        .button.toggled:not(.icon-raised-hand) {
177
+        .button.toggled:not(.icon-raised-hand):not(.button-active) {
174 178
             background: $toolbarSelectBackground;
175 179
             cursor: pointer;
176 180
             text-decoration: none;

+ 17
- 2
css/_videolayout_default.scss View File

@@ -115,6 +115,12 @@
115 115
         visibility: hidden;
116 116
         z-index: $zindex2;
117 117
     }
118
+
119
+    &.audio-only {
120
+        .videoThumbnailProblemFilter {
121
+            filter: none;
122
+        }
123
+    }
118 124
 }
119 125
 
120 126
 #localVideoWrapper {
@@ -489,14 +495,23 @@
489 495
                     0px 0px 1px rgba(0,0,0,0.3);
490 496
 }
491 497
 
498
+.audio-only-label {
499
+    cursor: default;
500
+    display: flex;
501
+    height: auto;
502
+    justify-content: center;
503
+    z-index: $centeredVideoLabelZ;
504
+}
505
+
506
+.audio-only-label,
492 507
 .video-state-indicator {
493 508
     background: $videoStateIndicatorBackground;
494 509
     color: $videoStateIndicatorColor;
495 510
     font-size: 13px;
511
+    height: 40px;
496 512
     line-height: 20px;
497 513
     text-align: center;
498 514
     min-width: 40px;
499
-    height: 40px;
500 515
     padding: 10px 5px;
501 516
     border-radius: 50%;
502 517
     position: absolute;
@@ -505,13 +520,13 @@
505 520
 
506 521
 #videoResolutionLabel,
507 522
 .centeredVideoLabel {
508
-    display: none;
509 523
     z-index: $centeredVideoLabelZ;
510 524
 }
511 525
 
512 526
 .centeredVideoLabel {
513 527
     bottom: 45%;
514 528
     border-radius: 2px;
529
+    display: none;
515 530
     -webkit-transition: all 2s 2s linear;
516 531
     transition: all 2s 2s linear;
517 532
 

+ 3
- 5
css/modals/device-selection/_device-selection.scss View File

@@ -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
             }

BIN
fonts/jitsi.eot View File


+ 3
- 1
fonts/jitsi.svg View File

@@ -11,7 +11,6 @@
11 11
 <glyph unicode="&#xe613;" glyph-name="recDisable" horiz-adv-x="1140" d="M1123.444 1003.015c-23.593 26.481-64.131 28.989-90.74 5.395l-1008.269-893.436c-26.609-23.468-28.991-64.131-5.46-90.676 12.674-14.306 30.308-21.649 48.126-21.649 15.123 0 30.372 5.401 42.544 16.195l130.045 115.22c90.743-81.844 210.569-132.165 342.473-132.101 282.816 0.061 510.913 227.969 511.287 510.972 0.126 109.934-34.682 211.367-93.499 294.72l118.088 104.625c26.483 23.526 28.997 64.129 5.404 90.735zM944.422 513.818c0.128-200.922-161.896-363.201-362.509-362.952-87.56 0.123-167.573 31.151-230.061 82.569l331.277 293.509v-73.176c1.071-60.993 32.696-92.18 94.944-93.692 61.997 1.512 93.686 32.763 95.131 93.756v41.096h-72.227v-47.499c0.251-4.642-0.564-10.607-2.511-17.949-1.25-3.261-3.448-6.020-6.525-8.093-3.197-2.572-7.845-3.828-13.868-3.828-10.543 0.31-17.132 4.268-19.827 11.921-1.068 3.512-1.947 6.905-2.508 10.163-0.254 2.887-0.377 5.532-0.377 7.786v143.511l42.477 37.634c0.215-0.432 0.452-0.851 0.63-1.303 1.947-6.467 2.762-12.799 2.511-19.076v-36.772h72.227v30.121c-0.246 31.245-9.086 54.699-26.363 70.447l40.711 36.069c35.787-56.055 56.803-122.585 56.867-194.244zM239.795 395.47c-12.613 37.023-19.827 76.557-19.827 117.913-0.19 200.236 161.584 362.009 361.945 362.135 56.853 0 110.313-13.302 158.133-36.398l117.846 104.421c-79.444 50.952-173.758 80.817-275.292 80.948-283.377 0.181-511.354-227.729-511.789-511.675-0.126-79.567 18.636-154.679 51.137-221.882l117.848 104.538zM388.576 690.020h-97.514v-249.057l72.23 64.070v0.689h0.815l117.72 104.418c0 0.564 0.123 0.94 0.123 1.509 0.753 53.898-30.369 80.069-93.374 78.37zM405.959 625.517c1.942-2.767 3.074-6.469 3.323-11.112 0.312-4.452 0.438-9.6 0.438-15.246 0.251-10.916-0.689-19.83-2.949-26.985-2.952-7.594-10.983-11.357-24.159-11.357h-19.325v74.043h15.31c7.842 0 13.865-0.683 18.072-2.19 4.397-1.573 7.468-3.953 9.29-7.153z" />
12 12
 <glyph unicode="&#xe614;" glyph-name="recEnable" horiz-adv-x="1142" d="M581.278 1025.708c284.857-0.19 514.807-230.517 514.427-514.997-0.378-285.047-230.073-514.553-514.869-514.615-284.541-0.062-515.311 230.517-514.933 514.422 0.439 285.936 230.009 515.439 515.375 515.19zM580.579 875.756c-201.764-0.123-364.666-163.032-364.478-364.663 0-202.018 162.524-364.735 364.478-364.984 202.018-0.316 365.174 163.030 365.048 365.423-0.252 201.767-163.156 364.35-365.048 364.224zM287.698 688.907h98.196c63.442 1.767 94.785-24.518 94.027-78.863 0.254-19.081-2.211-34.882-7.456-47.521-6.005-12.508-18.706-21.988-38.167-28.181v-0.819c28.373-6.259 43.031-23.573 43.981-51.946v-57.689c0-11.247 0.254-22.813 0.758-34.756 0.819-12.005 3.033-20.979 6.696-27.043h-71.846c-3.727 6.064-6.128 15.038-7.14 27.043-1.012 11.943-1.454 23.509-1.138 34.756v52.321c0 9.603-2.214 16.553-6.573 20.979-4.675 4.107-12.701 6.19-24.012 6.19h-14.599v-141.291h-72.73v326.82zM360.428 558.861h19.463c13.271 0 21.359 3.794 24.331 11.375 2.276 7.204 3.221 16.304 2.969 27.171 0 5.815-0.126 10.867-0.442 15.418-0.252 4.675-1.392 8.404-3.352 11.247-1.831 3.157-4.926 5.561-9.352 7.14-4.233 1.454-10.299 2.211-18.2 2.211h-15.418v-74.564zM498.372 688.907h162.082v-62.687h-89.35v-65.587h78.103v-62.685h-78.103v-73.11h92.822v-62.749h-165.557v326.818zM682.507 599.999c0.316 31.782 9.416 55.542 27.425 71.407 17.44 15.29 40.185 22.936 68.181 22.936 28.247 0 51.119-7.646 68.623-23 17.82-15.798 26.92-39.623 27.171-71.407v-30.333h-72.73v37.031c0.254 6.192-0.57 12.639-2.527 19.209-1.264 3.157-3.475 5.938-6.573 8.214-3.221 1.515-7.898 2.404-13.964 2.404-10.615-0.316-17.249-3.855-19.967-10.618-2.211-6.573-3.223-13.017-2.907-19.209v-161.956c0-2.273 0.126-4.865 0.38-7.772 0.568-3.411 1.454-6.824 2.527-10.233 2.717-7.775 9.352-11.756 19.967-12.007 6.067 0 10.744 1.261 13.964 3.791 3.098 2.15 5.309 4.867 6.573 8.216 1.96 7.33 2.782 13.33 2.527 18.007v47.837h72.73v-41.328c-1.451-61.547-33.364-93.015-95.794-94.469-62.685 1.454-94.53 32.922-95.607 94.343v148.937z" />
13 13
 <glyph unicode="&#xe61a;" glyph-name="connection" horiz-adv-x="1444" d="M3.881 210.835h220.26v-210.835h-220.26v210.835zM308.817 414.143h220.27v-414.143h-220.27v414.143zM613.764 617.412h220.268v-617.412h-220.268v617.412zM918.685 820.715h220.265v-820.715h-220.265v820.715zM1223.629 1024h220.263v-1024h-220.263v1024z" />
14
-<glyph unicode="&#xe61c;" glyph-name="dialpad" horiz-adv-x="1026" d="M74.418 881.299h239.304v-228.491h-239.304v228.491zM393.455 881.299h239.304v-228.491h-239.304v228.491zM712.494 881.299h239.263v-228.491h-239.263v228.491zM74.418 562.265h239.304v-228.555h-239.304v228.555zM393.455 562.265h239.304v-228.555h-239.304v228.555zM712.494 562.265h239.263v-228.555h-239.263v228.555zM74.418 243.166h239.304v-228.465h-239.304v228.465zM393.455 243.166h239.304v-228.465h-239.304v228.465zM712.494 243.166h239.263v-228.465h-239.263v228.465z" />
15 14
 <glyph unicode="&#xe900;" glyph-name="connection-lost" horiz-adv-x="1414" d="M0 299.153h196.337v-187.951h-196.337v187.951zM271.842 480.372h196.337v-369.169h-196.337v369.169zM543.656 661.562h196.337v-550.36h-196.337v550.36zM815.47 842.766v-731.564h119.56c-14.589 33.025-23.125 71.503-23.232 111.943 0.132 86.42 38.697 163.851 99.656 216.468l0.348 403.153h-196.332zM1087.292 1024v-533.672c28.874 10.572 62.222 16.73 97.009 16.825 35.717-0.129 69.823-6.614 101.322-18.371l-1.999 535.218h-196.332zM1192.868 439.852c-0.009 0-0.020 0-0.031 0-122.247 0-221.351-98.447-221.372-219.896 0-0.007 0-0.014 0-0.021 0-121.467 99.111-219.935 221.372-219.935 0.011 0 0.021 0 0.032 0 122.248 0.014 221.345 98.477 221.345 219.935 0 0.007 0 0.013 0 0.020-0.021 121.441-99.11 219.883-221.345 219.897zM1194.706 372.607c87.601-0.006 158.614-69.787 158.614-155.866 0-0.006 0-0.012 0-0.019-0.022-86.062-71.026-155.822-158.614-155.828-87.588 0.006-158.593 69.766-158.615 155.826 0 0.007 0 0.014 0 0.020 0 86.079 71.013 155.86 158.613 155.866zM1286.795 355.682l48.348-52.528-236.375-217.567-48.348 52.528 236.375 217.567z" />
16 15
 <glyph unicode="&#xe901;" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
17 16
 <glyph unicode="&#xe902;" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
@@ -46,4 +45,7 @@
46 45
 <glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
47 46
 <glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
48 47
 <glyph unicode="&#xe921;" glyph-name="switch-camera" d="M640 362l150 150-150 150v-108h-256v108l-150-150 150-150v108h256v-108zM854 854c46 0 84-40 84-86v-512c0-46-38-86-84-86h-684c-46 0-84 40-84 86v512c0 46 38 86 84 86h136l78 84h256l78-84h136z" />
48
+<glyph unicode="&#xe923;" glyph-name="visibility" d="M512 640c70 0 128-58 128-128s-58-128-128-128-128 58-128 128 58 128 128 128zM512 298c118 0 214 96 214 214s-96 214-214 214-214-96-214-214 96-214 214-214zM512 832c214 0 396-132 470-320-74-188-256-320-470-320s-396 132-470 320c74 188 256 320 470 320z" />
49
+<glyph unicode="&#xe924;" glyph-name="visibility-off" d="M506 640h6c70 0 128-58 128-128v-8zM322 606c-14-28-24-60-24-94 0-118 96-214 214-214 34 0 66 10 94 24l-66 66c-8-2-18-4-28-4-70 0-128 58-128 128 0 10 2 20 4 28zM86 842l54 54 756-756-54-54c-47.968 47.365-96.266 94.401-144 142-58-24-120-36-186-36-214 0-396 132-470 320 34 84 90 156 160 212-39.017 38.983-77.307 78.693-116 118zM512 726c-28 0-54-6-78-16l-92 92c52 20 110 30 170 30 214 0 394-132 468-320-32-80-82-148-146-202l-124 124c10 24 16 50 16 78 0 118-96 214-214 214z" />
50
+<glyph unicode="&#xe925;" glyph-name="dialpad" d="M512 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 810c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86zM256 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 214c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86z" />
49 51
 </font></defs></svg>

BIN
fonts/jitsi.ttf View File


BIN
fonts/jitsi.woff View File


+ 303
- 217
fonts/selection.json
File diff suppressed because it is too large
View File


+ 1
- 1
interface_config.js View File

@@ -38,7 +38,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
38 38
         //main toolbar
39 39
         'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
40 40
         //extended toolbar
41
-        'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'sip', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
41
+        'profile', 'contacts', 'chat', 'audioonly', 'recording', 'etherpad', 'sharedvideo', 'sip', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
42 42
     /**
43 43
      * Main Toolbar Buttons
44 44
      * All of them should be in TOOLBAR_BUTTONS

+ 7
- 1
lang/main.json View File

@@ -14,6 +14,11 @@
14 14
     "defaultNickname": "ex. Jane Pink",
15 15
     "defaultLink": "e.g. __url__",
16 16
     "callingName": "__name__",
17
+    "audioOnly": {
18
+        "audioOnly": "Audio only",
19
+        "featureToggleDisabled": "Toggling of __feature__ is disabled while in audio only mode",
20
+        "howToDisable": "Audio only mode is currently enabled. Click the audio only button in the toolbar to disable the feature."
21
+    },
17 22
     "userMedia": {
18 23
       "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
19 24
       "chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
@@ -92,6 +97,7 @@
92 97
         "rejoinKeyTitle": "Rejoin"
93 98
     },
94 99
     "toolbar": {
100
+        "audioonly": "Enable / Disable audio only mode (saves bandwidth)",
95 101
         "mute": "Mute / Unmute",
96 102
         "videomute": "Start / Stop camera",
97 103
         "authenticate": "Authenticate",
@@ -423,9 +429,9 @@
423 429
         "speakerTime": "Speaker Time"
424 430
     },
425 431
     "deviceSelection": {
426
-        "currentlyVideoMuted": "Video is currently muted",
427 432
         "deviceSettings": "Device settings",
428 433
         "noPermission": "Permission not granted",
434
+        "previewUnavailable": "Preview unavailable",
429 435
         "selectADevice": "Select a device",
430 436
         "testAudio": "Test sound"
431 437
     },

+ 8
- 0
modules/UI/UI.js View File

@@ -711,6 +711,14 @@ UI.setVideoMuted = function (id, muted) {
711 711
     }
712 712
 };
713 713
 
714
+/**
715
+ * Triggers an update of remote video and large video displays so they may pick
716
+ * up any state changes that have occurred elsewhere.
717
+ *
718
+ * @returns {void}
719
+ */
720
+UI.updateAllVideos = () => VideoLayout.updateAllVideos();
721
+
714 722
 /**
715 723
  * Adds a listener that would be notified on the given type of event.
716 724
  *

+ 5
- 1
modules/UI/videolayout/ConnectionIndicator.js View File

@@ -1,5 +1,9 @@
1 1
 /* global $, APP, config */
2 2
 /* jshint -W101 */
3
+import {
4
+    setLargeVideoHDStatus
5
+} from '../../../react/features/base/conference';
6
+
3 7
 import JitsiPopover from "../util/JitsiPopover";
4 8
 import VideoLayout from "./VideoLayout";
5 9
 import UIUtil from "../util/UIUtil";
@@ -478,7 +482,7 @@ ConnectionIndicator.prototype.updateResolutionIndicator = function () {
478 482
                 });
479 483
         }
480 484
 
481
-        VideoLayout.updateResolutionLabel(showResolutionLabel);
485
+        APP.store.dispatch(setLargeVideoHDStatus(showResolutionLabel));
482 486
     }
483 487
 };
484 488
 

+ 20
- 7
modules/UI/videolayout/LargeVideoManager.js View File

@@ -11,6 +11,7 @@ import AudioLevels from "../audio_levels/AudioLevels";
11 11
 
12 12
 const ParticipantConnectionStatus
13 13
     = JitsiMeetJS.constants.participantConnectionStatus;
14
+const DESKTOP_CONTAINER_TYPE = 'desktop';
14 15
 
15 16
 /**
16 17
  * Manager for all Large containers.
@@ -33,7 +34,7 @@ export default class LargeVideoManager {
33 34
         this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
34 35
 
35 36
         // use the same video container to handle desktop tracks
36
-        this.addContainer("desktop", this.videoContainer);
37
+        this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
37 38
 
38 39
         this.width = 0;
39 40
         this.height = 0;
@@ -103,6 +104,8 @@ export default class LargeVideoManager {
103 104
 
104 105
         preUpdate.then(() => {
105 106
             const { id, stream, videoType, resolve } = this.newStreamData;
107
+            const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE;
108
+
106 109
             this.newStreamData = null;
107 110
 
108 111
             logger.info("hover in %s", id);
@@ -120,9 +123,7 @@ export default class LargeVideoManager {
120 123
             // If the container is VIDEO_CONTAINER_TYPE, we need to check
121 124
             // its stream whether exist and is muted to set isVideoMuted
122 125
             // in rest of the cases it is false
123
-            let showAvatar
124
-                = (videoType === VIDEO_CONTAINER_TYPE)
125
-                    && (!stream || stream.isMuted());
126
+            let showAvatar = isVideoFromCamera && (!stream || stream.isMuted());
126 127
 
127 128
             // If the user's connection is disrupted then the avatar will be
128 129
             // displayed in case we have no video image cached. That is if
@@ -130,12 +131,20 @@ export default class LargeVideoManager {
130 131
             // the video was not rendered, before the connection has failed.
131 132
             const isConnectionActive = this._isConnectionActive(id);
132 133
 
133
-            if (videoType === VIDEO_CONTAINER_TYPE
134
+            if (isVideoFromCamera
134 135
                     && !isConnectionActive
135 136
                     && (isUserSwitch || !container.wasVideoRendered)) {
136 137
                 showAvatar = true;
137 138
             }
138 139
 
140
+            // If audio only mode is enabled, always show the avatar for
141
+            // videos from another participant.
142
+            if (APP.conference.isAudioOnly()
143
+                && (isVideoFromCamera
144
+                    || videoType === DESKTOP_CONTAINER_TYPE)) {
145
+                showAvatar = true;
146
+            }
147
+
139 148
             let promise;
140 149
 
141 150
             // do not show stream if video is muted
@@ -159,8 +168,12 @@ export default class LargeVideoManager {
159 168
 
160 169
             // Make sure no notification about remote failure is shown as
161 170
             // its UI conflicts with the one for local connection interrupted.
162
-            const isConnected = APP.conference.isConnectionInterrupted()
163
-                                || isConnectionActive;
171
+            // For the purposes of UI indicators, audio only is considered as
172
+            // an "active" connection.
173
+            const isConnected
174
+                = APP.conference.isAudioOnly()
175
+                    || APP.conference.isConnectionInterrupted()
176
+                    || isConnectionActive;
164 177
 
165 178
             // when isHavingConnectivityIssues, state can be inactive,
166 179
             // interrupted or restoring. We show different message for

+ 1
- 0
modules/UI/videolayout/RemoteVideo.js View File

@@ -556,6 +556,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
556 556
  * @inheritDoc
557 557
  */
558 558
 RemoteVideo.prototype.updateView = function () {
559
+    $(this.container).toggleClass('audio-only', APP.conference.isAudioOnly());
559 560
 
560 561
     this.updateConnectionStatusIndicator();
561 562
 

+ 3
- 1
modules/UI/videolayout/SmallVideo.js View File

@@ -459,7 +459,9 @@ SmallVideo.prototype.selectDisplayMode = function() {
459 459
     // Display name is always and only displayed when user is on the stage
460 460
     if (this.isCurrentlyOnLargeVideo()) {
461 461
         return DISPLAY_BLACKNESS_WITH_NAME;
462
-    } else if (this.isVideoPlayable() && this.selectVideoElement().length) {
462
+    } else if (this.isVideoPlayable()
463
+        && this.selectVideoElement().length
464
+        && !APP.conference.isAudioOnly()) {
463 465
         // check hovering and change state to video with name
464 466
         return this._isHovered() ?
465 467
             DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;

+ 19
- 13
modules/UI/videolayout/VideoLayout.js View File

@@ -176,9 +176,7 @@ var VideoLayout = {
176 176
         let localId = APP.conference.getMyUserId();
177 177
         this.onVideoTypeChanged(localId, stream.videoType);
178 178
 
179
-        if (!stream.isMuted()) {
180
-            localVideoThumbnail.changeVideo(stream);
181
-        }
179
+        localVideoThumbnail.changeVideo(stream);
182 180
 
183 181
         /* force update if we're currently being displayed */
184 182
         if (this.isCurrentlyOnLarge(localId)) {
@@ -956,6 +954,24 @@ var VideoLayout = {
956 954
         return largeVideo && largeVideo.id === id;
957 955
     },
958 956
 
957
+    /**
958
+     * Triggers an update of remote video and large video displays so they may
959
+     * pick up any state changes that have occurred elsewhere.
960
+     *
961
+     * @returns {void}
962
+     */
963
+    updateAllVideos() {
964
+        const displayedUserId = this.getLargeVideoID();
965
+
966
+        if (displayedUserId) {
967
+            this.updateLargeVideo(displayedUserId, true);
968
+        }
969
+
970
+        Object.keys(remoteVideos).forEach(video => {
971
+            remoteVideos[video].updateView();
972
+        });
973
+    },
974
+
959 975
     updateLargeVideo (id, forceUpdate) {
960 976
         if (!largeVideo) {
961 977
             return;
@@ -1062,16 +1078,6 @@ var VideoLayout = {
1062 1078
         return largeVideo;
1063 1079
     },
1064 1080
 
1065
-    /**
1066
-     * Updates the resolution label, indicating to the user that the large
1067
-     * video stream is currently HD.
1068
-     */
1069
-    updateResolutionLabel(isResolutionHD) {
1070
-        let id = 'videoResolutionLabel';
1071
-
1072
-        UIUtil.setVisible(id, isResolutionHD);
1073
-    },
1074
-
1075 1081
     /**
1076 1082
      * Sets the flipX state of the local video.
1077 1083
      * @param {boolean} true for flipped otherwise false;

+ 11
- 0
react/features/base/conference/actionTypes.js View File

@@ -93,6 +93,17 @@ export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
93 93
 export const _SET_AUDIO_ONLY_VIDEO_MUTED
94 94
     = Symbol('_SET_AUDIO_ONLY_VIDEO_MUTED');
95 95
 
96
+/**
97
+ * The type of (redux) action to set whether or not the displayed large video is
98
+ * in high-definition.
99
+ *
100
+ * {
101
+ *     type: SET_LARGE_VIDEO_HD_STATUS,
102
+ *     isLargeVideoHD: boolean
103
+ * }
104
+ */
105
+export const SET_LARGE_VIDEO_HD_STATUS = Symbol('SET_LARGE_VIDEO_HD_STATUS');
106
+
96 107
 /**
97 108
  * The type of redux action which sets the video channel's lastN (value).
98 109
  *

+ 18
- 0
react/features/base/conference/actions.js View File

@@ -20,6 +20,7 @@ import {
20 20
     LOCK_STATE_CHANGED,
21 21
     SET_AUDIO_ONLY,
22 22
     _SET_AUDIO_ONLY_VIDEO_MUTED,
23
+    SET_LARGE_VIDEO_HD_STATUS,
23 24
     SET_LASTN,
24 25
     SET_PASSWORD,
25 26
     SET_PASSWORD_FAILED,
@@ -358,6 +359,23 @@ export function _setAudioOnlyVideoMuted(muted: boolean) {
358 359
     };
359 360
 }
360 361
 
362
+/**
363
+ * Action to set whether or not the currently displayed large video is in
364
+ * high-definition.
365
+ *
366
+ * @param {boolean} isLargeVideoHD - True if the large video is high-definition.
367
+ * @returns {{
368
+ *     type: SET_LARGE_VIDEO_HD_STATUS,
369
+ *     isLargeVideoHD: boolean
370
+ * }}
371
+ */
372
+export function setLargeVideoHDStatus(isLargeVideoHD) {
373
+    return {
374
+        type: SET_LARGE_VIDEO_HD_STATUS,
375
+        isLargeVideoHD
376
+    };
377
+}
378
+
361 379
 /**
362 380
  * Sets the video channel's last N (value) of the current conference. A value of
363 381
  * undefined shall be used to reset it to the default value.

+ 8
- 0
react/features/base/conference/middleware.js View File

@@ -1,4 +1,6 @@
1 1
 /* global APP */
2
+import UIEvents from '../../../../service/UI/UIEvents';
3
+
2 4
 import { CONNECTION_ESTABLISHED } from '../connection';
3 5
 import {
4 6
     getLocalParticipant,
@@ -149,6 +151,12 @@ function _setAudioOnly(store, next, action) {
149 151
     // Mute local video
150 152
     store.dispatch(_setAudioOnlyVideoMuted(audioOnly));
151 153
 
154
+    if (typeof APP !== 'undefined') {
155
+        // TODO This should be a temporary solution that lasts only until
156
+        // video tracks and all ui is moved into react/redux on the web.
157
+        APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly);
158
+    }
159
+
152 160
     return result;
153 161
 }
154 162
 

+ 23
- 1
react/features/base/conference/reducer.js View File

@@ -11,6 +11,7 @@ import {
11 11
     LOCK_STATE_CHANGED,
12 12
     SET_AUDIO_ONLY,
13 13
     _SET_AUDIO_ONLY_VIDEO_MUTED,
14
+    SET_LARGE_VIDEO_HD_STATUS,
14 15
     SET_PASSWORD,
15 16
     SET_ROOM
16 17
 } from './actionTypes';
@@ -43,6 +44,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
43 44
     case _SET_AUDIO_ONLY_VIDEO_MUTED:
44 45
         return _setAudioOnlyVideoMuted(state, action);
45 46
 
47
+    case SET_LARGE_VIDEO_HD_STATUS:
48
+        return _setLargeVideoHDStatus(state, action);
49
+
46 50
     case SET_PASSWORD:
47 51
         return _setPassword(state, action);
48 52
 
@@ -238,7 +242,10 @@ function _lockStateChanged(state, action) {
238 242
  * reduction of the specified action.
239 243
  */
240 244
 function _setAudioOnly(state, action) {
241
-    return set(state, 'audioOnly', action.audioOnly);
245
+    return assign(state, {
246
+        audioOnly: action.audioOnly,
247
+        isLargeVideoHD: action.audioOnly ? false : state.isLargeVideoHD
248
+    });
242 249
 }
243 250
 
244 251
 /**
@@ -256,6 +263,21 @@ function _setAudioOnlyVideoMuted(state, action) {
256 263
     return set(state, 'audioOnlyVideoMuted', action.muted);
257 264
 }
258 265
 
266
+/**
267
+ * Reduces a specific Redux action SET_LARGE_VIDEO_HD_STATUS of the feature
268
+ * base/conference.
269
+ *
270
+ * @param {Object} state - The Redux state of the feature base/conference.
271
+ * @param {Action} action - The Redux action SET_LARGE_VIDEO_HD_STATUS to
272
+ * reduce.
273
+ * @private
274
+ * @returns {Object} The new state of the feature base/conference after the
275
+ * reduction of the specified action.
276
+ */
277
+function _setLargeVideoHDStatus(state, action) {
278
+    return set(state, 'isLargeVideoHD', action.isLargeVideoHD);
279
+}
280
+
259 281
 /**
260 282
  * Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
261 283
  *

+ 2
- 3
react/features/conference/components/Conference.web.js View File

@@ -9,6 +9,7 @@ import { Watermarks } from '../../base/react';
9 9
 import { OverlayContainer } from '../../overlay';
10 10
 import { Toolbox } from '../../toolbox';
11 11
 import { HideNotificationBarStyle } from '../../unsupported-browser';
12
+import { VideoStatusLabel } from '../../video-status-label';
12 13
 
13 14
 declare var $: Function;
14 15
 declare var APP: Object;
@@ -92,9 +93,7 @@ class Conference extends Component {
92 93
                                 muted = 'true' />
93 94
                         </div>
94 95
                         <span id = 'localConnectionMessage' />
95
-                        <span
96
-                            className = 'video-state-indicator moveToCorner'
97
-                            id = 'videoResolutionLabel'>HD</span>
96
+                        <VideoStatusLabel />
98 97
                         <span
99 98
                             className
100 99
                                 = 'video-state-indicator centeredVideoLabel'

+ 3
- 6
react/features/device-selection/actions.js View File

@@ -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 View File

@@ -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 View File

@@ -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
 

+ 103
- 0
react/features/toolbox/components/AudioOnlyButton.js View File

@@ -0,0 +1,103 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { toggleAudioOnly } from '../../base/conference';
5
+
6
+import ToolbarButton from './ToolbarButton';
7
+
8
+/**
9
+ * React {@code Component} for toggling audio only mode.
10
+ *
11
+ * @extends Component
12
+ */
13
+class AudioOnlyButton extends Component {
14
+    /**
15
+     * {@code AudioOnlyButton}'s property types.
16
+     *
17
+     * @static
18
+     */
19
+    static propTypes = {
20
+        /**
21
+         * Whether or not audio only mode is enabled.
22
+         */
23
+        _audioOnly: React.PropTypes.bool,
24
+
25
+        /**
26
+         * Invoked to toggle audio only mode.
27
+         */
28
+        dispatch: React.PropTypes.func,
29
+
30
+        /**
31
+         * From which side the button tooltip should appear.
32
+         */
33
+        tooltipPosition: React.PropTypes.string
34
+    }
35
+
36
+    /**
37
+     * Initializes a new {@code AudioOnlyButton} instance.
38
+     *
39
+     * @param {Object} props - The read-only properties with which the new
40
+     * instance is to be initialized.
41
+     */
42
+    constructor(props) {
43
+        super(props);
44
+
45
+        // Bind event handlers so they are only bound once for every instance.
46
+        this._onClick = this._onClick.bind(this);
47
+    }
48
+
49
+    /**
50
+     * Implements React's {@link Component#render()}.
51
+     *
52
+     * @inheritdoc
53
+     * @returns {ReactElement}
54
+     */
55
+    render() {
56
+        const buttonConfiguration = {
57
+            buttonName: 'audioonly',
58
+            classNames: [ 'button', 'icon-visibility' ],
59
+            enabled: true,
60
+            id: 'toolbar_button_audioonly',
61
+            tooltipKey: 'toolbar.audioonly'
62
+        };
63
+
64
+        if (this.props._audioOnly) {
65
+            buttonConfiguration.classNames.push('toggled button-active');
66
+        }
67
+
68
+        return (
69
+            <ToolbarButton
70
+                button = { buttonConfiguration }
71
+                onClick = { this._onClick }
72
+                tooltipPosition = { this.props.tooltipPosition } />
73
+        );
74
+    }
75
+
76
+    /**
77
+     * Dispatches an action to toggle audio only mode.
78
+     *
79
+     * @private
80
+     * @returns {void}
81
+     */
82
+    _onClick() {
83
+        this.props.dispatch(toggleAudioOnly());
84
+    }
85
+}
86
+
87
+/**
88
+ * Maps (parts of) the Redux state to the associated {@code AudioOnlyButton}'s
89
+ * props.
90
+ *
91
+ * @param {Object} state - The Redux state.
92
+ * @private
93
+ * @returns {{
94
+ *     _audioOnly: boolean
95
+ * }}
96
+ */
97
+function _mapStateToProps(state) {
98
+    return {
99
+        _audioOnly: state['features/base/conference'].audioOnly
100
+    };
101
+}
102
+
103
+export default connect(_mapStateToProps)(AudioOnlyButton);

+ 11
- 0
react/features/toolbox/components/Toolbar.web.js View File

@@ -121,6 +121,17 @@ class Toolbar extends Component {
121 121
     _renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>,
122 122
                          index: number): Array<ReactElement<*>> {
123 123
         const [ key, button ] = keyValuePair;
124
+
125
+        if (button.component) {
126
+            acc.push(
127
+                <button.component
128
+                    key = { key }
129
+                    tooltipPosition = { this.props.tooltipPosition } />
130
+            );
131
+
132
+            return acc;
133
+        }
134
+
124 135
         const { splitterIndex, tooltipPosition } = this.props;
125 136
 
126 137
         if (splitterIndex && index === splitterIndex) {

+ 1
- 1
react/features/toolbox/components/ToolbarButton.web.js View File

@@ -185,7 +185,7 @@ class ToolbarButton extends AbstractToolbarButton {
185 185
                 gravity = popup.dataAttrPosition;
186 186
             }
187 187
 
188
-            const title = this.props.t(popup.dataAttr);
188
+            const title = this.props.t(popup.dataAttr, popup.dataInterpolate);
189 189
 
190 190
             return (
191 191
                 <div

+ 1
- 0
react/features/toolbox/components/index.js View File

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

+ 32
- 0
react/features/toolbox/defaultToolbarButtons.js View File

@@ -6,6 +6,8 @@ import UIEvents from '../../../service/UI/UIEvents';
6 6
 
7 7
 import { openInviteDialog } from '../invite';
8 8
 
9
+import { AudioOnlyButton } from './components';
10
+
9 11
 declare var APP: Object;
10 12
 declare var config: Object;
11 13
 declare var JitsiMeetJS: Object;
@@ -42,6 +44,14 @@ function _showSIPNumberInput() {
42 44
  * All toolbar buttons' descriptors.
43 45
  */
44 46
 export default {
47
+    /**
48
+     * The descriptor of the audio only toolbar button. Defers actual
49
+     * descriptor implementation to the {@code AudioOnlyButton} component.
50
+     */
51
+    audioonly: {
52
+        component: AudioOnlyButton
53
+    },
54
+
45 55
     /**
46 56
      * The descriptor of the camera toolbar button.
47 57
      */
@@ -59,9 +69,23 @@ export default {
59 69
                 APP.UI.emitEvent(UIEvents.VIDEO_MUTED, true);
60 70
             }
61 71
         },
72
+        popups: [
73
+            {
74
+                className: 'loginmenu',
75
+                dataAttr: 'audioOnly.featureToggleDisabled',
76
+                dataInterpolate: { feature: 'video mute' },
77
+                id: 'unmuteWhileAudioOnly'
78
+            }
79
+        ],
62 80
         shortcut: 'V',
63 81
         shortcutAttr: 'toggleVideoPopover',
64 82
         shortcutFunc() {
83
+            if (APP.conference.isAudioOnly()) {
84
+                APP.UI.emitEvent(UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY);
85
+
86
+                return;
87
+            }
88
+
65 89
             JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled');
66 90
             APP.conference.toggleVideoMuted();
67 91
         },
@@ -137,6 +161,14 @@ export default {
137 161
             }
138 162
             APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
139 163
         },
164
+        popups: [
165
+            {
166
+                className: 'loginmenu',
167
+                dataAttr: 'audioOnly.featureToggleDisabled',
168
+                dataInterpolate: { feature: 'screen sharing' },
169
+                id: 'screenshareWhileAudioOnly'
170
+            }
171
+        ],
140 172
         shortcut: 'D',
141 173
         shortcutAttr: 'toggleDesktopSharingPopover',
142 174
         shortcutFunc() {

+ 104
- 0
react/features/video-status-label/components/AudioOnlyLabel.js View File

@@ -0,0 +1,104 @@
1
+import React, { Component } from 'react';
2
+
3
+import UIUtil from '../../../../modules/UI/util/UIUtil';
4
+
5
+import { translate } from '../../base/i18n';
6
+
7
+/**
8
+ * React {@code Component} for displaying a message to indicate audio only mode
9
+ * is active and for triggering a tooltip to provide more information about
10
+ * audio only mode.
11
+ *
12
+ * @extends Component
13
+ */
14
+export class AudioOnlyLabel extends Component {
15
+    /**
16
+     * {@code AudioOnlyLabel}'s property types.
17
+     *
18
+     * @static
19
+     */
20
+    static propTypes = {
21
+        /**
22
+         * Invoked to obtain translated strings.
23
+         */
24
+        t: React.PropTypes.func
25
+    }
26
+
27
+    /**
28
+     * Initializes a new {@code AudioOnlyLabel} instance.
29
+     *
30
+     * @param {Object} props - The read-only properties with which the new
31
+     * instance is to be initialized.
32
+     */
33
+    constructor(props) {
34
+        super(props);
35
+
36
+        /**
37
+         * The internal reference to the DOM/HTML element at the top of the
38
+         * React {@code Component}'s DOM/HTML hierarchy. It is necessary for
39
+         * setting a tooltip to display when hovering over the component.
40
+         *
41
+         * @private
42
+         * @type {HTMLDivElement}
43
+         */
44
+        this._rootElement = null;
45
+
46
+        // Bind event handlers so they are only bound once for every instance.
47
+        this._setRootElement = this._setRootElement.bind(this);
48
+    }
49
+
50
+    /**
51
+     * Sets a tooltip on the component to display on hover.
52
+     *
53
+     * @inheritdoc
54
+     * @returns {void}
55
+     */
56
+    componentDidMount() {
57
+        this._setTooltip();
58
+    }
59
+
60
+    /**
61
+     * Implements React's {@link Component#render()}.
62
+     *
63
+     * @inheritdoc
64
+     * @returns {ReactElement}
65
+     */
66
+    render() {
67
+        return (
68
+            <div
69
+                className = 'audio-only-label moveToCorner'
70
+                ref = { this._setRootElement }>
71
+                <i className = 'icon-visibility-off' />
72
+            </div>
73
+        );
74
+    }
75
+
76
+    /**
77
+     * Sets the instance variable for the component's root element so it can be
78
+     * accessed directly.
79
+     *
80
+     * @param {HTMLDivElement} element - The topmost DOM element of the
81
+     * component's DOM/HTML hierarchy.
82
+     * @private
83
+     * @returns {void}
84
+     */
85
+    _setRootElement(element) {
86
+        this._rootElement = element;
87
+    }
88
+
89
+    /**
90
+     * Sets the tooltip on the component's root element.
91
+     *
92
+     * @private
93
+     * @returns {void}
94
+     */
95
+    _setTooltip() {
96
+        UIUtil.setTooltip(
97
+            this._rootElement,
98
+            'audioOnly.howToDisable',
99
+            'left'
100
+        );
101
+    }
102
+}
103
+
104
+export default translate(AudioOnlyLabel);

+ 16
- 0
react/features/video-status-label/components/HDVideoLabel.js View File

@@ -0,0 +1,16 @@
1
+import React from 'react';
2
+
3
+/**
4
+ * A functional React {@code Component} for showing an HD status label.
5
+ *
6
+ * @returns {ReactElement}
7
+ */
8
+export default function HDVideoLabel() {
9
+    return (
10
+        <span
11
+            className = 'video-state-indicator moveToCorner'
12
+            id = 'videoResolutionLabel'>
13
+            HD
14
+        </span>
15
+    );
16
+}

+ 69
- 0
react/features/video-status-label/components/VideoStatusLabel.js View File

@@ -0,0 +1,69 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import AudioOnlyLabel from './AudioOnlyLabel';
5
+import HDVideoLabel from './HDVideoLabel';
6
+
7
+/**
8
+ * React {@code Component} responsible for displaying a label that indicates
9
+ * the displayed video state of the current conference. {@code AudioOnlyLabel}
10
+ * will display when the conference is in audio only mode. {@code HDVideoLabel}
11
+ * will display if not in audio only mode and a high-definition large video is
12
+ * being displayed.
13
+ */
14
+export class VideoStatusLabel extends Component {
15
+    /**
16
+     * {@code VideoStatusLabel}'s property types.
17
+     *
18
+     * @static
19
+     */
20
+    static propTypes = {
21
+        /**
22
+         * Whether or not the conference is in audio only mode.
23
+         */
24
+        _audioOnly: React.PropTypes.bool,
25
+
26
+        /**
27
+         * Whether or not a high-definition large video is displayed.
28
+         */
29
+        _largeVideoHD: React.PropTypes.bool
30
+    }
31
+
32
+    /**
33
+     * Implements React's {@link Component#render()}.
34
+     *
35
+     * @inheritdoc
36
+     * @returns {ReactElement|null}
37
+     */
38
+    render() {
39
+        if (this.props._audioOnly) {
40
+            return <AudioOnlyLabel />;
41
+        } else if (this.props._largeVideoHD) {
42
+            return <HDVideoLabel />;
43
+        }
44
+
45
+        return null;
46
+    }
47
+}
48
+
49
+/**
50
+ * Maps (parts of) the Redux state to the associated {@code VideoStatusLabel}'s
51
+ * props.
52
+ *
53
+ * @param {Object} state - The Redux state.
54
+ * @private
55
+ * @returns {{
56
+ *     _audioOnly: boolean,
57
+ *     _largeVideoHD: boolean
58
+ * }}
59
+ */
60
+function _mapStateToProps(state) {
61
+    const { audioOnly, isLargeVideoHD } = state['features/base/conference'];
62
+
63
+    return {
64
+        _audioOnly: audioOnly,
65
+        _largeVideoHD: isLargeVideoHD
66
+    };
67
+}
68
+
69
+export default connect(_mapStateToProps)(VideoStatusLabel);

+ 1
- 0
react/features/video-status-label/components/index.js View File

@@ -0,0 +1 @@
1
+export { default as VideoStatusLabel } from './VideoStatusLabel';

+ 1
- 0
react/features/video-status-label/index.js View File

@@ -0,0 +1 @@
1
+export * from './components';

+ 5
- 0
service/UI/UIEvents.js View File

@@ -20,6 +20,7 @@ export default {
20 20
     START_MUTED_CHANGED: "UI.start_muted_changed",
21 21
     AUDIO_MUTED: "UI.audio_muted",
22 22
     VIDEO_MUTED: "UI.video_muted",
23
+    VIDEO_UNMUTING_WHILE_AUDIO_ONLY: "UI.video_unmuting_while_audio_only",
23 24
     ETHERPAD_CLICKED: "UI.etherpad_clicked",
24 25
     SHARED_VIDEO_CLICKED: "UI.start_shared_video",
25 26
     /**
@@ -33,6 +34,10 @@ export default {
33 34
     TOGGLE_FULLSCREEN: "UI.toogle_fullscreen",
34 35
     FULLSCREEN_TOGGLED: "UI.fullscreen_toggled",
35 36
     AUTH_CLICKED: "UI.auth_clicked",
37
+    /**
38
+     * Notifies that the audio only mode was toggled.
39
+     */
40
+    TOGGLE_AUDIO_ONLY: "UI.toggle_audioonly",
36 41
     TOGGLE_CHAT: "UI.toggle_chat",
37 42
     TOGGLE_SETTINGS: "UI.toggle_settings",
38 43
     TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",

Loading…
Cancel
Save