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:
parent
1bcdbd1d96
commit
9ba3a1c4ff
|
@ -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,
|
||||
toggleScreenSharing(shareScreen = !this.isSharingScreen) {
|
||||
|
@ -1097,6 +1134,11 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.isAudioOnly()) {
|
||||
this._displayAudioOnlyTooltip('screenShare');
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoSwitchInProgress = true;
|
||||
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,
|
||||
(smallVideo, isPinned) => {
|
||||
let smallVideoId = smallVideo.getId();
|
||||
|
@ -1512,7 +1558,14 @@ export default {
|
|||
});
|
||||
|
||||
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,
|
||||
(stats) => {
|
||||
|
@ -1661,6 +1714,14 @@ export default {
|
|||
micDeviceId: null
|
||||
})
|
||||
.then(([stream]) => {
|
||||
if (this.isAudioOnly()) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => {
|
||||
this.useVideoStream(stream);
|
||||
logger.log('switched local video device');
|
||||
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(
|
||||
UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
|
||||
);
|
||||
|
|
|
@ -26,119 +26,125 @@
|
|||
}
|
||||
|
||||
.icon-mic-camera-combined:before {
|
||||
content: "\e903";
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-feedback:before {
|
||||
content: "\e91d";
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon-toggle-filmstrip:before {
|
||||
content: "\e91c";
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon-avatar:before {
|
||||
content: "\e901";
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-hangup:before {
|
||||
content: "\e905";
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-chat:before {
|
||||
content: "\e906";
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-dialpad:before {
|
||||
content: "\e61c";
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-edit:before {
|
||||
content: "\e907";
|
||||
content: "\e907";
|
||||
}
|
||||
.icon-share-doc:before {
|
||||
content: "\e908";
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-telephone:before {
|
||||
content: "\e909";
|
||||
content: "\e909";
|
||||
}
|
||||
.icon-kick:before {
|
||||
content: "\e904";
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-menu-up:before {
|
||||
content: "\e91f";
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon-menu-down:before {
|
||||
content: "\e920";
|
||||
content: "\e920";
|
||||
}
|
||||
.icon-full-screen:before {
|
||||
content: "\e90b";
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-exit-full-screen:before {
|
||||
content: "\e90c";
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-star-full:before {
|
||||
content: "\e90a";
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-security:before {
|
||||
content: "\e90d";
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon-security-locked:before {
|
||||
content: "\e90e";
|
||||
content: "\e90e";
|
||||
}
|
||||
.icon-reload:before {
|
||||
content: "\e90f";
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon-microphone:before {
|
||||
content: "\e910";
|
||||
content: "\e910";
|
||||
}
|
||||
.icon-mic-empty:before {
|
||||
content: "\e911";
|
||||
content: "\e911";
|
||||
}
|
||||
.icon-mic-disabled:before {
|
||||
content: "\e912";
|
||||
content: "\e912";
|
||||
}
|
||||
.icon-raised-hand:before {
|
||||
content: "\e91e";
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon-contactList:before {
|
||||
content: "\e91b";
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\e913";
|
||||
content: "\e913";
|
||||
}
|
||||
.icon-shared-video:before {
|
||||
content: "\e914";
|
||||
content: "\e914";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\e915";
|
||||
content: "\e915";
|
||||
}
|
||||
.icon-star:before {
|
||||
content: "\e916";
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-switch-camera:before {
|
||||
content: "\e921";
|
||||
content: "\e921";
|
||||
}
|
||||
.icon-share-desktop:before {
|
||||
content: "\e917";
|
||||
content: "\e917";
|
||||
}
|
||||
.icon-camera:before {
|
||||
content: "\e918";
|
||||
content: "\e918";
|
||||
}
|
||||
.icon-camera-disabled:before {
|
||||
content: "\e919";
|
||||
content: "\e919";
|
||||
}
|
||||
.icon-volume:before {
|
||||
content: "\e91a";
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon-connection-lost:before {
|
||||
content: "\e900";
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-connection:before {
|
||||
content: "\e61a";
|
||||
content: "\e61a";
|
||||
}
|
||||
.icon-recDisable:before {
|
||||
content: "\e613";
|
||||
content: "\e613";
|
||||
}
|
||||
.icon-recEnable:before {
|
||||
content: "\e614";
|
||||
content: "\e614";
|
||||
}
|
||||
.icon-presentation:before {
|
||||
content: "\e603";
|
||||
content: "\e603";
|
||||
}
|
||||
.icon-dialpad:before {
|
||||
content: "\e925";
|
||||
}
|
||||
.icon-visibility:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon-visibility-off:before {
|
||||
content: "\e924";
|
||||
}
|
|
@ -71,6 +71,10 @@
|
|||
&.icon-microphone {
|
||||
@extend .icon-mic-disabled;
|
||||
}
|
||||
|
||||
&.icon-visibility {
|
||||
@extend .icon-visibility-off;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
|
@ -170,7 +174,7 @@
|
|||
width: $defaultToolbarSize;
|
||||
-webkit-transform: translateX(-100%);
|
||||
|
||||
.button.toggled:not(.icon-raised-hand) {
|
||||
.button.toggled:not(.icon-raised-hand):not(.button-active) {
|
||||
background: $toolbarSelectBackground;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -115,6 +115,12 @@
|
|||
visibility: hidden;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.audio-only {
|
||||
.videoThumbnailProblemFilter {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
|
@ -489,19 +495,31 @@
|
|||
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 {
|
||||
background: $videoStateIndicatorBackground;
|
||||
color: $videoStateIndicatorColor;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 10px 5px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.video-state-indicator {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#videoResolutionLabel,
|
||||
.centeredVideoLabel {
|
||||
|
|
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
|
@ -11,7 +11,6 @@
|
|||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
|
||||
<glyph unicode="" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
|
||||
<glyph unicode="" 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="" 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="" 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="" 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>
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -38,7 +38,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
|||
//main toolbar
|
||||
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
|
||||
//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
|
||||
* All of them should be in TOOLBAR_BUTTONS
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"defaultNickname": "ex. Jane Pink",
|
||||
"defaultLink": "e.g. __url__",
|
||||
"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": {
|
||||
"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.",
|
||||
|
@ -92,6 +97,7 @@
|
|||
"rejoinKeyTitle": "Rejoin"
|
||||
},
|
||||
"toolbar": {
|
||||
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / Stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -11,6 +11,7 @@ import AudioLevels from "../audio_levels/AudioLevels";
|
|||
|
||||
const ParticipantConnectionStatus
|
||||
= JitsiMeetJS.constants.participantConnectionStatus;
|
||||
const DESKTOP_CONTAINER_TYPE = 'desktop';
|
||||
|
||||
/**
|
||||
* Manager for all Large containers.
|
||||
|
@ -33,7 +34,7 @@ export default class LargeVideoManager {
|
|||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// 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.height = 0;
|
||||
|
@ -103,6 +104,8 @@ export default class LargeVideoManager {
|
|||
|
||||
preUpdate.then(() => {
|
||||
const { id, stream, videoType, resolve } = this.newStreamData;
|
||||
const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE;
|
||||
|
||||
this.newStreamData = null;
|
||||
|
||||
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
|
||||
// its stream whether exist and is muted to set isVideoMuted
|
||||
// in rest of the cases it is false
|
||||
let showAvatar
|
||||
= (videoType === VIDEO_CONTAINER_TYPE)
|
||||
&& (!stream || stream.isMuted());
|
||||
let showAvatar = isVideoFromCamera && (!stream || stream.isMuted());
|
||||
|
||||
// If the user's connection is disrupted then the avatar will be
|
||||
// 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.
|
||||
const isConnectionActive = this._isConnectionActive(id);
|
||||
|
||||
if (videoType === VIDEO_CONTAINER_TYPE
|
||||
if (isVideoFromCamera
|
||||
&& !isConnectionActive
|
||||
&& (isUserSwitch || !container.wasVideoRendered)) {
|
||||
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;
|
||||
|
||||
// do not show stream if video is muted
|
||||
|
@ -159,8 +168,12 @@ export default class LargeVideoManager {
|
|||
|
||||
// Make sure no notification about remote failure is shown as
|
||||
// its UI conflicts with the one for local connection interrupted.
|
||||
const isConnected = APP.conference.isConnectionInterrupted()
|
||||
|| isConnectionActive;
|
||||
// For the purposes of UI indicators, audio only is considered as
|
||||
// an "active" connection.
|
||||
const isConnected
|
||||
= APP.conference.isAudioOnly()
|
||||
|| APP.conference.isConnectionInterrupted()
|
||||
|| isConnectionActive;
|
||||
|
||||
// when isHavingConnectivityIssues, state can be inactive,
|
||||
// interrupted or restoring. We show different message for
|
||||
|
|
|
@ -556,6 +556,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
|
|||
* @inheritDoc
|
||||
*/
|
||||
RemoteVideo.prototype.updateView = function () {
|
||||
$(this.container).toggleClass('audio-only', APP.conference.isAudioOnly());
|
||||
|
||||
this.updateConnectionStatusIndicator();
|
||||
|
||||
|
|
|
@ -459,7 +459,9 @@ SmallVideo.prototype.selectDisplayMode = function() {
|
|||
// Display name is always and only displayed when user is on the stage
|
||||
if (this.isCurrentlyOnLargeVideo()) {
|
||||
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
|
||||
return this._isHovered() ?
|
||||
DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
||||
|
|
|
@ -956,6 +956,24 @@ var VideoLayout = {
|
|||
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) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global APP */
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { CONNECTION_ESTABLISHED } from '../connection';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
|
@ -149,6 +151,12 @@ function _setAudioOnly(store, next, action) {
|
|||
// Mute local video
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { connect, disconnect } from '../../base/connection';
|
|||
import { DialogContainer } from '../../base/dialog';
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { OverlayContainer } from '../../overlay';
|
||||
import { StatusLabel } from '../../status-label';
|
||||
import { Toolbox } from '../../toolbox';
|
||||
import { HideNotificationBarStyle } from '../../unsupported-browser';
|
||||
|
||||
|
@ -95,6 +96,7 @@ class Conference extends Component {
|
|||
<span
|
||||
className = 'video-state-indicator moveToCorner'
|
||||
id = 'videoResolutionLabel'>HD</span>
|
||||
<StatusLabel />
|
||||
<span
|
||||
className
|
||||
= 'video-state-indicator centeredVideoLabel'
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1 @@
|
|||
export { default as StatusLabel } from './StatusLabel';
|
|
@ -0,0 +1 @@
|
|||
export * from './components';
|
|
@ -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);
|
|
@ -121,6 +121,17 @@ class Toolbar extends Component {
|
|||
_renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>,
|
||||
index: number): Array<ReactElement<*>> {
|
||||
const [ key, button ] = keyValuePair;
|
||||
|
||||
if (button.component) {
|
||||
acc.push(
|
||||
<button.component
|
||||
key = { key }
|
||||
tooltipPosition = { this.props.tooltipPosition } />
|
||||
);
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
const { splitterIndex, tooltipPosition } = this.props;
|
||||
|
||||
if (splitterIndex && index === splitterIndex) {
|
||||
|
|
|
@ -185,7 +185,7 @@ class ToolbarButton extends AbstractToolbarButton {
|
|||
gravity = popup.dataAttrPosition;
|
||||
}
|
||||
|
||||
const title = this.props.t(popup.dataAttr);
|
||||
const title = this.props.t(popup.dataAttr, popup.dataInterpolate);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { default as AudioOnlyButton } from './AudioOnlyButton';
|
||||
export { default as Toolbox } from './Toolbox';
|
||||
|
|
|
@ -6,6 +6,8 @@ import UIEvents from '../../../service/UI/UIEvents';
|
|||
|
||||
import { openInviteDialog } from '../invite';
|
||||
|
||||
import { AudioOnlyButton } from './components';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
@ -42,6 +44,14 @@ function _showSIPNumberInput() {
|
|||
* All toolbar buttons' descriptors.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -59,9 +69,23 @@ export default {
|
|||
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, true);
|
||||
}
|
||||
},
|
||||
popups: [
|
||||
{
|
||||
className: 'loginmenu',
|
||||
dataAttr: 'audioOnly.featureToggleDisabled',
|
||||
dataInterpolate: { feature: 'video mute' },
|
||||
id: 'unmuteWhileAudioOnly'
|
||||
}
|
||||
],
|
||||
shortcut: 'V',
|
||||
shortcutAttr: 'toggleVideoPopover',
|
||||
shortcutFunc() {
|
||||
if (APP.conference.isAudioOnly()) {
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled');
|
||||
APP.conference.toggleVideoMuted();
|
||||
},
|
||||
|
@ -137,6 +161,14 @@ export default {
|
|||
}
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
|
||||
},
|
||||
popups: [
|
||||
{
|
||||
className: 'loginmenu',
|
||||
dataAttr: 'audioOnly.featureToggleDisabled',
|
||||
dataInterpolate: { feature: 'screen sharing' },
|
||||
id: 'screenshareWhileAudioOnly'
|
||||
}
|
||||
],
|
||||
shortcut: 'D',
|
||||
shortcutAttr: 'toggleDesktopSharingPopover',
|
||||
shortcutFunc() {
|
||||
|
|
|
@ -20,6 +20,7 @@ export default {
|
|||
START_MUTED_CHANGED: "UI.start_muted_changed",
|
||||
AUDIO_MUTED: "UI.audio_muted",
|
||||
VIDEO_MUTED: "UI.video_muted",
|
||||
VIDEO_UNMUTING_WHILE_AUDIO_ONLY: "UI.video_unmuting_while_audio_only",
|
||||
ETHERPAD_CLICKED: "UI.etherpad_clicked",
|
||||
SHARED_VIDEO_CLICKED: "UI.start_shared_video",
|
||||
/**
|
||||
|
@ -33,6 +34,10 @@ export default {
|
|||
TOGGLE_FULLSCREEN: "UI.toogle_fullscreen",
|
||||
FULLSCREEN_TOGGLED: "UI.fullscreen_toggled",
|
||||
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_SETTINGS: "UI.toggle_settings",
|
||||
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
|
||||
|
|
Loading…
Reference in New Issue