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.
This commit is contained in:
Saúl Ibarra Corretgé 2017-04-05 17:14:26 +02:00 committed by Leonard Kim
parent 1bcdbd1d96
commit 9ba3a1c4ff
28 changed files with 836 additions and 273 deletions

View File

@ -1085,6 +1085,43 @@ export default {
}); });
}, },
/**
* Triggers a tooltip to display when a feature was attempted to be used
* while in audio only mode.
*
* @param {string} featureName - The name of the feature that attempted to
* toggle.
* @private
* @returns {void}
*/
_displayAudioOnlyTooltip(featureName) {
let tooltipElementId = null;
switch (featureName) {
case 'screenShare':
tooltipElementId = '#screenshareWhileAudioOnly';
break;
case 'videoMute':
tooltipElementId = '#unmuteWhileAudioOnly';
break;
}
if (tooltipElementId) {
APP.UI.showToolbar(6000);
APP.UI.showCustomToolbarPopup(
tooltipElementId, true, 5000);
}
},
/**
* Returns whether or not the conference is currently in audio only mode.
*
* @returns {boolean}
*/
isAudioOnly() {
return Boolean(
APP.store.getState()['features/base/conference'].audioOnly);
},
videoSwitchInProgress: false, videoSwitchInProgress: false,
toggleScreenSharing(shareScreen = !this.isSharingScreen) { toggleScreenSharing(shareScreen = !this.isSharingScreen) {
@ -1097,6 +1134,11 @@ export default {
return; return;
} }
if (this.isAudioOnly()) {
this._displayAudioOnlyTooltip('screenShare');
return;
}
this.videoSwitchInProgress = true; this.videoSwitchInProgress = true;
let externalInstallation = false; let externalInstallation = false;
@ -1400,6 +1442,10 @@ export default {
} }
}); });
APP.UI.addListener(
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
() => this._displayAudioOnlyTooltip('videoMute'));
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
(smallVideo, isPinned) => { (smallVideo, isPinned) => {
let smallVideoId = smallVideo.getId(); let smallVideoId = smallVideo.getId();
@ -1512,7 +1558,14 @@ export default {
}); });
APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio); APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio);
APP.UI.addListener(UIEvents.VIDEO_MUTED, muteLocalVideo); APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
if (this.isAudioOnly() && !muted) {
this._displayAudioOnlyTooltip('videoMute');
return;
}
muteLocalVideo(muted);
});
room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED, room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED,
(stats) => { (stats) => {
@ -1661,6 +1714,14 @@ export default {
micDeviceId: null micDeviceId: null
}) })
.then(([stream]) => { .then(([stream]) => {
if (this.isAudioOnly()) {
return stream.mute()
.then(() => stream);
}
return stream;
})
.then(stream => {
this.useVideoStream(stream); this.useVideoStream(stream);
logger.log('switched local video device'); logger.log('switched local video device');
APP.settings.setCameraDeviceId(cameraDeviceId, true); APP.settings.setCameraDeviceId(cameraDeviceId, true);
@ -1707,6 +1768,18 @@ export default {
} }
); );
APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
muteLocalVideo(audioOnly);
// Immediately update the UI by having remote videos and the large
// video update themselves instead of waiting for some other event
// to cause the update, usually PARTICIPANT_CONN_STATUS_CHANGED.
// There is no guarantee another event will trigger the update
// immediately and in all situations, for example because a remote
// participant is having connection trouble so no status changes.
APP.UI.updateAllVideos();
});
APP.UI.addListener( APP.UI.addListener(
UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this) UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
); );

View File

@ -46,9 +46,6 @@
.icon-download:before { .icon-download:before {
content: "\e902"; content: "\e902";
} }
.icon-dialpad:before {
content: "\e61c";
}
.icon-edit:before { .icon-edit:before {
content: "\e907"; content: "\e907";
} }
@ -142,3 +139,12 @@
.icon-presentation:before { .icon-presentation:before {
content: "\e603"; content: "\e603";
} }
.icon-dialpad:before {
content: "\e925";
}
.icon-visibility:before {
content: "\e923";
}
.icon-visibility-off:before {
content: "\e924";
}

View File

@ -71,6 +71,10 @@
&.icon-microphone { &.icon-microphone {
@extend .icon-mic-disabled; @extend .icon-mic-disabled;
} }
&.icon-visibility {
@extend .icon-visibility-off;
}
} }
&.unclickable { &.unclickable {
@ -170,7 +174,7 @@
width: $defaultToolbarSize; width: $defaultToolbarSize;
-webkit-transform: translateX(-100%); -webkit-transform: translateX(-100%);
.button.toggled:not(.icon-raised-hand) { .button.toggled:not(.icon-raised-hand):not(.button-active) {
background: $toolbarSelectBackground; background: $toolbarSelectBackground;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;

View File

@ -115,6 +115,12 @@
visibility: hidden; visibility: hidden;
z-index: $zindex2; z-index: $zindex2;
} }
&.audio-only {
.videoThumbnailProblemFilter {
filter: none;
}
}
} }
#localVideoWrapper { #localVideoWrapper {
@ -489,19 +495,31 @@
0px 0px 1px rgba(0,0,0,0.3); 0px 0px 1px rgba(0,0,0,0.3);
} }
.audio-only-label {
cursor: default;
display: flex;
height: auto;
justify-content: center;
z-index: $centeredVideoLabelZ;
}
.audio-only-label,
.video-state-indicator { .video-state-indicator {
background: $videoStateIndicatorBackground; background: $videoStateIndicatorBackground;
color: $videoStateIndicatorColor; color: $videoStateIndicatorColor;
font-size: 13px; font-size: 13px;
height: 40px;
line-height: 20px; line-height: 20px;
text-align: center; text-align: center;
min-width: 40px; min-width: 40px;
height: 40px;
padding: 10px 5px; padding: 10px 5px;
border-radius: 50%; border-radius: 50%;
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
} }
.video-state-indicator {
height: 40px;
}
#videoResolutionLabel, #videoResolutionLabel,
.centeredVideoLabel { .centeredVideoLabel {

Binary file not shown.

View File

@ -11,7 +11,6 @@
<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" /> <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" />
<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" /> <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" />
<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" /> <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" />
<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" />
<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" /> <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" />
<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" /> <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" />
<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" /> <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 @@
<glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" /> <glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
<glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" /> <glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
<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" /> <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" />
<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" />
<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" />
<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" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

520
fonts/selection.json Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -14,6 +14,11 @@
"defaultNickname": "ex. Jane Pink", "defaultNickname": "ex. Jane Pink",
"defaultLink": "e.g. __url__", "defaultLink": "e.g. __url__",
"callingName": "__name__", "callingName": "__name__",
"audioOnly": {
"audioOnly": "Audio only",
"featureToggleDisabled": "Toggling of __feature__ is disabled while in audio only mode",
"howToDisable": "Audio only mode is currently enabled. Click the audio only button in the toolbar to disable the feature."
},
"userMedia": { "userMedia": {
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.", "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.", "chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
@ -92,6 +97,7 @@
"rejoinKeyTitle": "Rejoin" "rejoinKeyTitle": "Rejoin"
}, },
"toolbar": { "toolbar": {
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"mute": "Mute / Unmute", "mute": "Mute / Unmute",
"videomute": "Start / Stop camera", "videomute": "Start / Stop camera",
"authenticate": "Authenticate", "authenticate": "Authenticate",

View File

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

View File

@ -11,6 +11,7 @@ import AudioLevels from "../audio_levels/AudioLevels";
const ParticipantConnectionStatus const ParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus; = JitsiMeetJS.constants.participantConnectionStatus;
const DESKTOP_CONTAINER_TYPE = 'desktop';
/** /**
* Manager for all Large containers. * Manager for all Large containers.
@ -33,7 +34,7 @@ export default class LargeVideoManager {
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer); this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle desktop tracks // use the same video container to handle desktop tracks
this.addContainer("desktop", this.videoContainer); this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
@ -103,6 +104,8 @@ export default class LargeVideoManager {
preUpdate.then(() => { preUpdate.then(() => {
const { id, stream, videoType, resolve } = this.newStreamData; const { id, stream, videoType, resolve } = this.newStreamData;
const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE;
this.newStreamData = null; this.newStreamData = null;
logger.info("hover in %s", id); logger.info("hover in %s", id);
@ -120,9 +123,7 @@ export default class LargeVideoManager {
// If the container is VIDEO_CONTAINER_TYPE, we need to check // If the container is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted // its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false // in rest of the cases it is false
let showAvatar let showAvatar = isVideoFromCamera && (!stream || stream.isMuted());
= (videoType === VIDEO_CONTAINER_TYPE)
&& (!stream || stream.isMuted());
// If the user's connection is disrupted then the avatar will be // If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if // displayed in case we have no video image cached. That is if
@ -130,12 +131,20 @@ export default class LargeVideoManager {
// the video was not rendered, before the connection has failed. // the video was not rendered, before the connection has failed.
const isConnectionActive = this._isConnectionActive(id); const isConnectionActive = this._isConnectionActive(id);
if (videoType === VIDEO_CONTAINER_TYPE if (isVideoFromCamera
&& !isConnectionActive && !isConnectionActive
&& (isUserSwitch || !container.wasVideoRendered)) { && (isUserSwitch || !container.wasVideoRendered)) {
showAvatar = true; showAvatar = true;
} }
// If audio only mode is enabled, always show the avatar for
// videos from another participant.
if (APP.conference.isAudioOnly()
&& (isVideoFromCamera
|| videoType === DESKTOP_CONTAINER_TYPE)) {
showAvatar = true;
}
let promise; let promise;
// do not show stream if video is muted // do not show stream if video is muted
@ -159,7 +168,11 @@ export default class LargeVideoManager {
// Make sure no notification about remote failure is shown as // Make sure no notification about remote failure is shown as
// its UI conflicts with the one for local connection interrupted. // its UI conflicts with the one for local connection interrupted.
const isConnected = APP.conference.isConnectionInterrupted() // For the purposes of UI indicators, audio only is considered as
// an "active" connection.
const isConnected
= APP.conference.isAudioOnly()
|| APP.conference.isConnectionInterrupted()
|| isConnectionActive; || isConnectionActive;
// when isHavingConnectivityIssues, state can be inactive, // when isHavingConnectivityIssues, state can be inactive,

View File

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

View File

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

View File

@ -956,6 +956,24 @@ var VideoLayout = {
return largeVideo && largeVideo.id === id; return largeVideo && largeVideo.id === id;
}, },
/**
* Triggers an update of remote video and large video displays so they may
* pick up any state changes that have occurred elsewhere.
*
* @returns {void}
*/
updateAllVideos() {
const displayedUserId = this.getLargeVideoID();
if (displayedUserId) {
this.updateLargeVideo(displayedUserId, true);
}
Object.keys(remoteVideos).forEach(video => {
remoteVideos[video].updateView();
});
},
updateLargeVideo (id, forceUpdate) { updateLargeVideo (id, forceUpdate) {
if (!largeVideo) { if (!largeVideo) {
return; return;

View File

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

View File

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

View File

@ -0,0 +1,104 @@
import React, { Component } from 'react';
import UIUtil from '../../../../modules/UI/util/UIUtil';
import { translate } from '../../base/i18n';
/**
* React {@code Component} for displaying a message to indicate audio only mode
* is active and for triggering a tooltip to provide more information about
* audio only mode.
*
* @extends Component
*/
export class AudioOnlyLabel extends Component {
/**
* {@code AudioOnlyLabel}'s property types.
*
* @static
*/
static propTypes = {
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
}
/**
* Initializes a new {@code AudioOnlyLabel} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The internal reference to the DOM/HTML element at the top of the
* React {@code Component}'s DOM/HTML hierarchy. It is necessary for
* setting a tooltip to display when hovering over the component.
*
* @private
* @type {HTMLDivElement}
*/
this._rootElement = null;
// Bind event handlers so they are only bound once for every instance.
this._setRootElement = this._setRootElement.bind(this);
}
/**
* Sets a tooltip on the component to display on hover.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._setTooltip();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'audio-only-label'
ref = { this._setRootElement }>
<i className = 'icon-visibility-off' />
</div>
);
}
/**
* Sets the instance variable for the component's root element so it can be
* accessed directly.
*
* @param {HTMLDivElement} element - The topmost DOM element of the
* component's DOM/HTML hierarchy.
* @private
* @returns {void}
*/
_setRootElement(element) {
this._rootElement = element;
}
/**
* Sets the tooltip on the component's root element.
*
* @private
* @returns {void}
*/
_setTooltip() {
UIUtil.setTooltip(
this._rootElement,
'audioOnly.howToDisable',
'left'
);
}
}
export default translate(AudioOnlyLabel);

View File

@ -0,0 +1,58 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import AudioOnlyLabel from './AudioOnlyLabel';
/**
* Component responsible for displaying a label that indicates some state of the
* current conference. The AudioOnlyLabel component will be displayed when the
* conference is in audio only mode.
*/
export class StatusLabel extends Component {
/**
* StatusLabel component's property types.
*
* @static
*/
static propTypes = {
/**
* The redux store representation of the current conference.
*/
_conference: React.PropTypes.object
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
if (!this.props._conference.audioOnly) {
return null;
}
return (
<div className = 'moveToCorner'>
<AudioOnlyLabel />
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated StatusLabel's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _conference: Object,
* }}
*/
function _mapStateToProps(state) {
return {
_conference: state['features/base/conference']
};
}
export default connect(_mapStateToProps)(StatusLabel);

View File

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

View File

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

View File

@ -0,0 +1,103 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { toggleAudioOnly } from '../../base/conference';
import ToolbarButton from './ToolbarButton';
/**
* React {@code Component} for toggling audio only mode.
*
* @extends Component
*/
class AudioOnlyButton extends Component {
/**
* {@code AudioOnlyButton}'s property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not audio only mode is enabled.
*/
_audioOnly: React.PropTypes.bool,
/**
* Invoked to toggle audio only mode.
*/
dispatch: React.PropTypes.func,
/**
* From which side the button tooltip should appear.
*/
tooltipPosition: React.PropTypes.string
}
/**
* Initializes a new {@code AudioOnlyButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const buttonConfiguration = {
buttonName: 'audioonly',
classNames: [ 'button', 'icon-visibility' ],
enabled: true,
id: 'toolbar_button_audioonly',
tooltipKey: 'toolbar.audioonly'
};
if (this.props._audioOnly) {
buttonConfiguration.classNames.push('toggled button-active');
}
return (
<ToolbarButton
button = { buttonConfiguration }
onClick = { this._onClick }
tooltipPosition = { this.props.tooltipPosition } />
);
}
/**
* Dispatches an action to toggle audio only mode.
*
* @private
* @returns {void}
*/
_onClick() {
this.props.dispatch(toggleAudioOnly());
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AudioOnlyButton}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state) {
return {
_audioOnly: state['features/base/conference'].audioOnly
};
}
export default connect(_mapStateToProps)(AudioOnlyButton);

View File

@ -121,6 +121,17 @@ class Toolbar extends Component {
_renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>, _renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>,
index: number): Array<ReactElement<*>> { index: number): Array<ReactElement<*>> {
const [ key, button ] = keyValuePair; const [ key, button ] = keyValuePair;
if (button.component) {
acc.push(
<button.component
key = { key }
tooltipPosition = { this.props.tooltipPosition } />
);
return acc;
}
const { splitterIndex, tooltipPosition } = this.props; const { splitterIndex, tooltipPosition } = this.props;
if (splitterIndex && index === splitterIndex) { if (splitterIndex && index === splitterIndex) {

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import UIEvents from '../../../service/UI/UIEvents';
import { openInviteDialog } from '../invite'; import { openInviteDialog } from '../invite';
import { AudioOnlyButton } from './components';
declare var APP: Object; declare var APP: Object;
declare var config: Object; declare var config: Object;
declare var JitsiMeetJS: Object; declare var JitsiMeetJS: Object;
@ -42,6 +44,14 @@ function _showSIPNumberInput() {
* All toolbar buttons' descriptors. * All toolbar buttons' descriptors.
*/ */
export default { export default {
/**
* The descriptor of the audio only toolbar button. Defers actual
* descriptor implementation to the {@code AudioOnlyButton} component.
*/
audioonly: {
component: AudioOnlyButton
},
/** /**
* The descriptor of the camera toolbar button. * The descriptor of the camera toolbar button.
*/ */
@ -59,9 +69,23 @@ export default {
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, true); APP.UI.emitEvent(UIEvents.VIDEO_MUTED, true);
} }
}, },
popups: [
{
className: 'loginmenu',
dataAttr: 'audioOnly.featureToggleDisabled',
dataInterpolate: { feature: 'video mute' },
id: 'unmuteWhileAudioOnly'
}
],
shortcut: 'V', shortcut: 'V',
shortcutAttr: 'toggleVideoPopover', shortcutAttr: 'toggleVideoPopover',
shortcutFunc() { shortcutFunc() {
if (APP.conference.isAudioOnly()) {
APP.UI.emitEvent(UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY);
return;
}
JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled'); JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled');
APP.conference.toggleVideoMuted(); APP.conference.toggleVideoMuted();
}, },
@ -137,6 +161,14 @@ export default {
} }
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING); APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
}, },
popups: [
{
className: 'loginmenu',
dataAttr: 'audioOnly.featureToggleDisabled',
dataInterpolate: { feature: 'screen sharing' },
id: 'screenshareWhileAudioOnly'
}
],
shortcut: 'D', shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover', shortcutAttr: 'toggleDesktopSharingPopover',
shortcutFunc() { shortcutFunc() {

View File

@ -20,6 +20,7 @@ export default {
START_MUTED_CHANGED: "UI.start_muted_changed", START_MUTED_CHANGED: "UI.start_muted_changed",
AUDIO_MUTED: "UI.audio_muted", AUDIO_MUTED: "UI.audio_muted",
VIDEO_MUTED: "UI.video_muted", VIDEO_MUTED: "UI.video_muted",
VIDEO_UNMUTING_WHILE_AUDIO_ONLY: "UI.video_unmuting_while_audio_only",
ETHERPAD_CLICKED: "UI.etherpad_clicked", ETHERPAD_CLICKED: "UI.etherpad_clicked",
SHARED_VIDEO_CLICKED: "UI.start_shared_video", SHARED_VIDEO_CLICKED: "UI.start_shared_video",
/** /**
@ -33,6 +34,10 @@ export default {
TOGGLE_FULLSCREEN: "UI.toogle_fullscreen", TOGGLE_FULLSCREEN: "UI.toogle_fullscreen",
FULLSCREEN_TOGGLED: "UI.fullscreen_toggled", FULLSCREEN_TOGGLED: "UI.fullscreen_toggled",
AUTH_CLICKED: "UI.auth_clicked", AUTH_CLICKED: "UI.auth_clicked",
/**
* Notifies that the audio only mode was toggled.
*/
TOGGLE_AUDIO_ONLY: "UI.toggle_audioonly",
TOGGLE_CHAT: "UI.toggle_chat", TOGGLE_CHAT: "UI.toggle_chat",
TOGGLE_SETTINGS: "UI.toggle_settings", TOGGLE_SETTINGS: "UI.toggle_settings",
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list", TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",