Ver código fonte

feat(conference): add audio only mode

Audio only mode can be used to save bandwidth. In this mode local video is muted
and last N is set to 0, thus disabling all remote video.

When this mode is enabled avatars are shown.
j8
Saúl Ibarra Corretgé 8 anos atrás
pai
commit
9ba3a1c4ff

+ 74
- 1
conference.js Ver arquivo

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

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

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

+ 19
- 1
css/_videolayout_default.scss Ver arquivo

@@ -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,19 +495,31 @@
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;
503 518
     box-sizing: border-box;
504 519
 }
520
+.video-state-indicator {
521
+    height: 40px;
522
+}
505 523
 
506 524
 #videoResolutionLabel,
507 525
 .centeredVideoLabel {

BIN
fonts/jitsi.eot Ver arquivo


+ 3
- 1
fonts/jitsi.svg Ver arquivo

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


BIN
fonts/jitsi.woff Ver arquivo


+ 303
- 217
fonts/selection.json
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 1
interface_config.js Ver arquivo

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

+ 6
- 0
lang/main.json Ver arquivo

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

+ 8
- 0
modules/UI/UI.js Ver arquivo

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

+ 20
- 7
modules/UI/videolayout/LargeVideoManager.js Ver arquivo

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

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

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

+ 18
- 0
modules/UI/videolayout/VideoLayout.js Ver arquivo

@@ -956,6 +956,24 @@ var VideoLayout = {
956 956
         return largeVideo && largeVideo.id === id;
957 957
     },
958 958
 
959
+    /**
960
+     * Triggers an update of remote video and large video displays so they may
961
+     * pick up any state changes that have occurred elsewhere.
962
+     *
963
+     * @returns {void}
964
+     */
965
+    updateAllVideos() {
966
+        const displayedUserId = this.getLargeVideoID();
967
+
968
+        if (displayedUserId) {
969
+            this.updateLargeVideo(displayedUserId, true);
970
+        }
971
+
972
+        Object.keys(remoteVideos).forEach(video => {
973
+            remoteVideos[video].updateView();
974
+        });
975
+    },
976
+
959 977
     updateLargeVideo (id, forceUpdate) {
960 978
         if (!largeVideo) {
961 979
             return;

+ 8
- 0
react/features/base/conference/middleware.js Ver arquivo

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

+ 2
- 0
react/features/conference/components/Conference.web.js Ver arquivo

@@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection';
7 7
 import { DialogContainer } from '../../base/dialog';
8 8
 import { Watermarks } from '../../base/react';
9 9
 import { OverlayContainer } from '../../overlay';
10
+import { StatusLabel } from '../../status-label';
10 11
 import { Toolbox } from '../../toolbox';
11 12
 import { HideNotificationBarStyle } from '../../unsupported-browser';
12 13
 
@@ -95,6 +96,7 @@ class Conference extends Component {
95 96
                         <span
96 97
                             className = 'video-state-indicator moveToCorner'
97 98
                             id = 'videoResolutionLabel'>HD</span>
99
+                        <StatusLabel />
98 100
                         <span
99 101
                             className
100 102
                                 = 'video-state-indicator centeredVideoLabel'

+ 104
- 0
react/features/status-label/components/AudioOnlyLabel.js Ver arquivo

@@ -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'
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);

+ 58
- 0
react/features/status-label/components/StatusLabel.js Ver arquivo

@@ -0,0 +1,58 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import AudioOnlyLabel from './AudioOnlyLabel';
5
+
6
+/**
7
+ * Component responsible for displaying a label that indicates some state of the
8
+ * current conference. The AudioOnlyLabel component will be displayed when the
9
+ * conference is in audio only mode.
10
+ */
11
+export class StatusLabel extends Component {
12
+    /**
13
+     * StatusLabel component's property types.
14
+     *
15
+     * @static
16
+     */
17
+    static propTypes = {
18
+        /**
19
+         * The redux store representation of the current conference.
20
+         */
21
+        _conference: React.PropTypes.object
22
+    }
23
+
24
+    /**
25
+     * Implements React's {@link Component#render()}.
26
+     *
27
+     * @inheritdoc
28
+     * @returns {ReactElement|null}
29
+     */
30
+    render() {
31
+        if (!this.props._conference.audioOnly) {
32
+            return null;
33
+        }
34
+
35
+        return (
36
+            <div className = 'moveToCorner'>
37
+                <AudioOnlyLabel />
38
+            </div>
39
+        );
40
+    }
41
+}
42
+
43
+/**
44
+ * Maps (parts of) the Redux state to the associated StatusLabel's props.
45
+ *
46
+ * @param {Object} state - The Redux state.
47
+ * @private
48
+ * @returns {{
49
+ *     _conference: Object,
50
+ * }}
51
+ */
52
+function _mapStateToProps(state) {
53
+    return {
54
+        _conference: state['features/base/conference']
55
+    };
56
+}
57
+
58
+export default connect(_mapStateToProps)(StatusLabel);

+ 1
- 0
react/features/status-label/components/index.js Ver arquivo

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

+ 1
- 0
react/features/status-label/index.js Ver arquivo

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

+ 103
- 0
react/features/toolbox/components/AudioOnlyButton.js Ver arquivo

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

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

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

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

@@ -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() {

+ 5
- 0
service/UI/UIEvents.js Ver arquivo

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

Carregando…
Cancelar
Salvar