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

Audio only mode for web
This commit is contained in:
yanas 2017-05-05 11:52:15 -05:00 committed by GitHub
commit 166fb1d13f
37 changed files with 1177 additions and 574 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,
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)
);

View File

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

View File

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

View File

@ -115,6 +115,12 @@
visibility: hidden;
z-index: $zindex2;
}
&.audio-only {
.videoThumbnailProblemFilter {
filter: none;
}
}
}
#localVideoWrapper {
@ -489,14 +495,23 @@
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;
@ -505,13 +520,13 @@
#videoResolutionLabel,
.centeredVideoLabel {
display: none;
z-index: $centeredVideoLabelZ;
}
.centeredVideoLabel {
bottom: 45%;
border-radius: 2px;
display: none;
-webkit-transition: all 2s 2s linear;
transition: all 2s 2s linear;

View File

@ -79,7 +79,7 @@
border-radius: 3px;
}
.video-input-preview-muted {
.video-input-preview-error {
color: $participantNameColor;
display: none;
left: 0;
@ -89,12 +89,10 @@
top: 50%;
}
&.video-muted {
/* TOFIX: to be removed when we move out from muted preview */
&.video-preview-has-error {
background: black;
/* TOFIX-END */
.video-input-preview-muted {
.video-input-preview-error {
display: block;
}
}

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="&#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="&#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="&#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" />
@ -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="&#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="&#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>

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

View File

@ -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",
@ -423,9 +429,9 @@
"speakerTime": "Speaker Time"
},
"deviceSelection": {
"currentlyVideoMuted": "Video is currently muted",
"deviceSettings": "Device settings",
"noPermission": "Permission not granted",
"previewUnavailable": "Preview unavailable",
"selectADevice": "Select a device",
"testAudio": "Test sound"
},

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

View File

@ -1,5 +1,9 @@
/* global $, APP, config */
/* jshint -W101 */
import {
setLargeVideoHDStatus
} from '../../../react/features/base/conference';
import JitsiPopover from "../util/JitsiPopover";
import VideoLayout from "./VideoLayout";
import UIUtil from "../util/UIUtil";
@ -478,7 +482,7 @@ ConnectionIndicator.prototype.updateResolutionIndicator = function () {
});
}
VideoLayout.updateResolutionLabel(showResolutionLabel);
APP.store.dispatch(setLargeVideoHDStatus(showResolutionLabel));
}
};

View File

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

View File

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

View File

@ -176,9 +176,7 @@ var VideoLayout = {
let localId = APP.conference.getMyUserId();
this.onVideoTypeChanged(localId, stream.videoType);
if (!stream.isMuted()) {
localVideoThumbnail.changeVideo(stream);
}
localVideoThumbnail.changeVideo(stream);
/* force update if we're currently being displayed */
if (this.isCurrentlyOnLarge(localId)) {
@ -956,6 +954,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;
@ -1062,16 +1078,6 @@ var VideoLayout = {
return largeVideo;
},
/**
* Updates the resolution label, indicating to the user that the large
* video stream is currently HD.
*/
updateResolutionLabel(isResolutionHD) {
let id = 'videoResolutionLabel';
UIUtil.setVisible(id, isResolutionHD);
},
/**
* Sets the flipX state of the local video.
* @param {boolean} true for flipped otherwise false;

View File

@ -93,6 +93,17 @@ export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
export const _SET_AUDIO_ONLY_VIDEO_MUTED
= Symbol('_SET_AUDIO_ONLY_VIDEO_MUTED');
/**
* The type of (redux) action to set whether or not the displayed large video is
* in high-definition.
*
* {
* type: SET_LARGE_VIDEO_HD_STATUS,
* isLargeVideoHD: boolean
* }
*/
export const SET_LARGE_VIDEO_HD_STATUS = Symbol('SET_LARGE_VIDEO_HD_STATUS');
/**
* The type of redux action which sets the video channel's lastN (value).
*

View File

@ -20,6 +20,7 @@ import {
LOCK_STATE_CHANGED,
SET_AUDIO_ONLY,
_SET_AUDIO_ONLY_VIDEO_MUTED,
SET_LARGE_VIDEO_HD_STATUS,
SET_LASTN,
SET_PASSWORD,
SET_PASSWORD_FAILED,
@ -358,6 +359,23 @@ export function _setAudioOnlyVideoMuted(muted: boolean) {
};
}
/**
* Action to set whether or not the currently displayed large video is in
* high-definition.
*
* @param {boolean} isLargeVideoHD - True if the large video is high-definition.
* @returns {{
* type: SET_LARGE_VIDEO_HD_STATUS,
* isLargeVideoHD: boolean
* }}
*/
export function setLargeVideoHDStatus(isLargeVideoHD) {
return {
type: SET_LARGE_VIDEO_HD_STATUS,
isLargeVideoHD
};
}
/**
* Sets the video channel's last N (value) of the current conference. A value of
* undefined shall be used to reset it to the default value.

View File

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

View File

@ -11,6 +11,7 @@ import {
LOCK_STATE_CHANGED,
SET_AUDIO_ONLY,
_SET_AUDIO_ONLY_VIDEO_MUTED,
SET_LARGE_VIDEO_HD_STATUS,
SET_PASSWORD,
SET_ROOM
} from './actionTypes';
@ -43,6 +44,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case _SET_AUDIO_ONLY_VIDEO_MUTED:
return _setAudioOnlyVideoMuted(state, action);
case SET_LARGE_VIDEO_HD_STATUS:
return _setLargeVideoHDStatus(state, action);
case SET_PASSWORD:
return _setPassword(state, action);
@ -238,7 +242,10 @@ function _lockStateChanged(state, action) {
* reduction of the specified action.
*/
function _setAudioOnly(state, action) {
return set(state, 'audioOnly', action.audioOnly);
return assign(state, {
audioOnly: action.audioOnly,
isLargeVideoHD: action.audioOnly ? false : state.isLargeVideoHD
});
}
/**
@ -256,6 +263,21 @@ function _setAudioOnlyVideoMuted(state, action) {
return set(state, 'audioOnlyVideoMuted', action.muted);
}
/**
* Reduces a specific Redux action SET_LARGE_VIDEO_HD_STATUS of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_LARGE_VIDEO_HD_STATUS to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setLargeVideoHDStatus(state, action) {
return set(state, 'isLargeVideoHD', action.isLargeVideoHD);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

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

View File

@ -12,16 +12,13 @@ import { DeviceSelectionDialog } from './components';
* @returns {Function}
*/
export function openDeviceSelectionDialog() {
return (dispatch, getState) => {
return dispatch => {
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
.then(isDeviceListAvailable => {
const state = getState();
const conference = state['features/base/conference'].conference;
dispatch(openDialog(DeviceSelectionDialog, {
currentAudioInputId: APP.settings.getMicDeviceId(),
currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
currentAudioTrack: conference.getLocalAudioTrack(),
currentVideoTrack: conference.getLocalVideoTrack(),
currentVideoInputId: APP.settings.getCameraDeviceId(),
disableAudioInputChange:
!JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange: !isDeviceListAvailable

View File

@ -34,29 +34,25 @@ class DeviceSelectionDialog extends Component {
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
_devices: React.PropTypes.object,
_availableDevices: React.PropTypes.object,
/**
* Device id for the current audio output device.
* Device id for the current audio input device. This device will be set
* as the default audio input device to preview.
*/
currentAudioInputId: React.PropTypes.string,
/**
* Device id for the current audio output device. This device will be
* set as the default audio output device to preview.
*/
currentAudioOutputId: React.PropTypes.string,
/**
* JitsiLocalTrack for the current local audio.
*
* JitsiLocalTracks for the current audio and video, if any, should be
* passed in for re-use in the previews. This is needed for Internet
* Explorer, which cannot get multiple tracks from the same device, even
* across tabs.
* Device id for the current video input device. This device will be set
* as the default video input device to preview.
*/
currentAudioTrack: React.PropTypes.object,
/**
* JitsiLocalTrack for the current local video.
*
* Needed for reuse. See comment for propTypes.currentAudioTrack.
*/
currentVideoTrack: React.PropTypes.object,
currentVideoInputId: React.PropTypes.string,
/**
* Whether or not the audio selector can be interacted with. If true,
@ -78,12 +74,12 @@ class DeviceSelectionDialog extends Component {
dispatch: React.PropTypes.func,
/**
* Whether or not new audio input source can be selected.
* Whether or not a new audio input source can be selected.
*/
hasAudioPermission: React.PropTypes.bool,
/**
* Whether or not new video input sources can be selected.
* Whether or not a new video input sources can be selected.
*/
hasVideoPermission: React.PropTypes.bool,
@ -117,15 +113,40 @@ class DeviceSelectionDialog extends Component {
constructor(props) {
super(props);
const { _availableDevices } = this.props;
this.state = {
// JitsiLocalTracks to use for live previewing.
// JitsiLocalTrack to use for live previewing of audio input.
previewAudioTrack: null,
// JitsiLocalTrack to use for live previewing of video input.
previewVideoTrack: null,
// Device ids to keep track of new selections.
videInput: null,
audioInput: null,
audioOutput: null
// An message describing a problem with obtaining a video preview.
previewVideoTrackError: null,
// The audio input device id to show as selected by default.
selectedAudioInputId: this.props.currentAudioInputId || '',
// The audio output device id to show as selected by default.
selectedAudioOutputId: this.props.currentAudioOutputId || '',
// The video input device id to show as selected by default.
// FIXME: On temasys, without a device selected and put into local
// storage as the default device to use, the current video device id
// is a blank string. This is because the library gets a local video
// track and then maps the track's device id by matching the track's
// label to the MediaDeviceInfos returned from enumerateDevices. In
// WebRTC, the track label is expected to return the camera device
// label. However, temasys video track labels refer to track id, not
// device label, so the library cannot match the track to a device.
// The workaround of defaulting to the first videoInput available
// is re-used from the previous device settings implementation.
selectedVideoInputId: this.props.currentVideoInputId
|| (_availableDevices.videoInput
&& _availableDevices.videoInput[0]
&& _availableDevices.videoInput[0].deviceId)
|| ''
};
// Preventing closing while cleaning up previews is important for
@ -134,16 +155,29 @@ class DeviceSelectionDialog extends Component {
// closure until cleanup is complete ensures no errors in the process.
this._isClosing = false;
// Bind event handlers so they are only bound once for every instance.
this._closeModal = this._closeModal.bind(this);
this._getAndSetAudioOutput = this._getAndSetAudioOutput.bind(this);
this._getAndSetAudioTrack = this._getAndSetAudioTrack.bind(this);
this._getAndSetVideoTrack = this._getAndSetVideoTrack.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._updateAudioOutput = this._updateAudioOutput.bind(this);
this._updateAudioInput = this._updateAudioInput.bind(this);
this._updateVideoInput = this._updateVideoInput.bind(this);
}
/**
* Clean up any preview tracks that might not have been cleaned up already.
* Sets default device choices so a choice is pre-selected in the dropdowns
* and live previews are created.
*
* @inheritdoc
*/
componentDidMount() {
this._updateAudioOutput(this.state.selectedAudioOutputId);
this._updateAudioInput(this.state.selectedAudioInputId);
this._updateVideoInput(this.state.selectedVideoInputId);
}
/**
* Disposes preview tracks that might not already be disposed.
*
* @inheritdoc
*/
@ -173,8 +207,8 @@ class DeviceSelectionDialog extends Component {
<div className = 'device-selection-column column-video'>
<div className = 'device-selection-video-container'>
<VideoInputPreview
track = { this.state.previewVideoTrack
|| this.props.currentVideoTrack } />
error = { this.state.previewVideoTrackError }
track = { this.state.previewVideoTrack } />
</div>
{ this._renderAudioInputPreview() }
</div>
@ -197,17 +231,10 @@ class DeviceSelectionDialog extends Component {
* promise can be for video cleanup and another for audio cleanup.
*/
_attemptPreviewTrackCleanup() {
const cleanupPromises = [];
if (!this._isPreviewingCurrentVideoTrack()) {
cleanupPromises.push(this._disposeVideoPreview());
}
if (!this._isPreviewingCurrentAudioTrack()) {
cleanupPromises.push(this._disposeAudioPreview());
}
return cleanupPromises;
return Promise.all([
this._disposeVideoPreview(),
this._disposeAudioPreview()
]);
}
/**
@ -243,147 +270,7 @@ class DeviceSelectionDialog extends Component {
}
/**
* Callback invoked when a new audio output device has been selected.
* Updates the internal state of the user's selection.
*
* @param {string} deviceId - The id of the chosen audio output device.
* @private
* @returns {void}
*/
_getAndSetAudioOutput(deviceId) {
this.setState({
audioOutput: deviceId
});
}
/**
* Callback invoked when a new audio input device has been selected.
* Updates the internal state of the user's selection as well as the audio
* track that should display in the preview. Will reuse the current local
* audio track if it has been selected.
*
* @param {string} deviceId - The id of the chosen audio input device.
* @private
* @returns {void}
*/
_getAndSetAudioTrack(deviceId) {
this.setState({
audioInput: deviceId
}, () => {
const cleanupPromise = this._isPreviewingCurrentAudioTrack()
? Promise.resolve() : this._disposeAudioPreview();
if (this._isCurrentAudioTrack(deviceId)) {
cleanupPromise
.then(() => {
this.setState({
previewAudioTrack: this.props.currentAudioTrack
});
});
} else {
cleanupPromise
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewAudioTrack: jitsiLocalTrack
});
});
}
});
}
/**
* Callback invoked when a new video input device has been selected. Updates
* the internal state of the user's selection as well as the video track
* that should display in the preview. Will reuse the current local video
* track if it has been selected.
*
* @param {string} deviceId - The id of the chosen video input device.
* @private
* @returns {void}
*/
_getAndSetVideoTrack(deviceId) {
this.setState({
videoInput: deviceId
}, () => {
const cleanupPromise = this._isPreviewingCurrentVideoTrack()
? Promise.resolve() : this._disposeVideoPreview();
if (this._isCurrentVideoTrack(deviceId)) {
cleanupPromise
.then(() => {
this.setState({
previewVideoTrack: this.props.currentVideoTrack
});
});
} else {
cleanupPromise
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewVideoTrack: jitsiLocalTrack
});
});
}
});
}
/**
* Utility function for determining if the current local audio track has the
* passed in device id.
*
* @param {string} deviceId - The device id to match against.
* @private
* @returns {boolean} True if the device id is being used by the local audio
* track.
*/
_isCurrentAudioTrack(deviceId) {
return this.props.currentAudioTrack
&& this.props.currentAudioTrack.getDeviceId() === deviceId;
}
/**
* Utility function for determining if the current local video track has the
* passed in device id.
*
* @param {string} deviceId - The device id to match against.
* @private
* @returns {boolean} True if the device id is being used by the local
* video track.
*/
_isCurrentVideoTrack(deviceId) {
return this.props.currentVideoTrack
&& this.props.currentVideoTrack.getDeviceId() === deviceId;
}
/**
* Utility function for detecting if the current audio preview track is not
* the currently used audio track.
*
* @private
* @returns {boolean} True if the current audio track is being used for
* the preview.
*/
_isPreviewingCurrentAudioTrack() {
return !this.state.previewAudioTrack
|| this.state.previewAudioTrack === this.props.currentAudioTrack;
}
/**
* Utility function for detecting if the current video preview track is not
* the currently used video track.
*
* @private
* @returns {boolean} True if the current video track is being used as the
* preview.
*/
_isPreviewingCurrentVideoTrack() {
return !this.state.previewVideoTrack
|| this.state.previewVideoTrack === this.props.currentVideoTrack;
}
/**
* Cleans existing preview tracks and signal to closeDeviceSelectionDialog.
* Disposes preview tracks and signals to close DeviceSelectionDialog.
*
* @private
* @returns {boolean} Returns false to prevent closure until cleanup is
@ -406,7 +293,7 @@ class DeviceSelectionDialog extends Component {
}
/**
* Identify changes to the preferred input/output devices and perform
* Identifies changes to the preferred input/output devices and perform
* necessary cleanup and requests to use those devices. Closes the modal
* after cleanup and device change requests complete.
*
@ -421,32 +308,26 @@ class DeviceSelectionDialog extends Component {
this._isClosing = true;
const deviceChangePromises = [];
const deviceChangePromises = this._attemptPreviewTrackCleanup()
.then(() => {
if (this.state.selectedVideoInputId
!== this.props.currentVideoInputId) {
this.props.dispatch(
setVideoInputDevice(this.state.selectedVideoInputId));
}
if (this.state.videoInput && !this._isPreviewingCurrentVideoTrack()) {
const changeVideoPromise = this._disposeVideoPreview()
.then(() => {
this.props.dispatch(setVideoInputDevice(
this.state.videoInput));
});
if (this.state.selectedAudioInputId
!== this.props.currentAudioInputId) {
this.props.dispatch(
setAudioInputDevice(this.state.selectedAudioInputId));
}
deviceChangePromises.push(changeVideoPromise);
}
if (this.state.audioInput && !this._isPreviewingCurrentAudioTrack()) {
const changeAudioPromise = this._disposeAudioPreview()
.then(() => {
this.props.dispatch(setAudioInputDevice(
this.state.audioInput));
});
deviceChangePromises.push(changeAudioPromise);
}
if (this.state.audioOutput
&& this.state.audioOutput !== this.props.currentAudioOutputId) {
this.props.dispatch(setAudioOutputDevice(this.state.audioOutput));
}
if (this.state.selectedAudioOutputId
!== this.props.currentAudioOutputId) {
this.props.dispatch(
setAudioOutputDevice(this.state.selectedAudioOutputId));
}
});
Promise.all(deviceChangePromises)
.then(this._closeModal)
@ -470,8 +351,7 @@ class DeviceSelectionDialog extends Component {
return (
<AudioInputPreview
track = { this.state.previewAudioTrack
|| this.props.currentAudioTrack } />
track = { this.state.previewAudioTrack } />
);
}
@ -489,8 +369,7 @@ class DeviceSelectionDialog extends Component {
return (
<AudioOutputPreview
deviceId = { this.state.audioOutput
|| this.props.currentAudioOutputId } />
deviceId = { this.state.selectedAudioOutputId } />
);
}
@ -515,70 +394,120 @@ class DeviceSelectionDialog extends Component {
* @returns {Array<ReactElement>} DeviceSelector instances.
*/
_renderSelectors() {
const availableDevices = this.props._devices;
const currentAudioId = this.state.audioInput
|| (this.props.currentAudioTrack
&& this.props.currentAudioTrack.getDeviceId());
const currentAudioOutId = this.state.audioOutput
|| this.props.currentAudioOutputId;
// FIXME: On temasys, without a device selected and put into local
// storage as the default device to use, the current video device id is
// a blank string. This is because the library gets a local video track
// and then maps the track's device id by matching the track's label to
// the MediaDeviceInfos returned from enumerateDevices. In WebRTC, the
// track label is expected to return the camera device label. However,
// temasys video track labels refer to track id, not device label, so
// the library cannot match the track to a device. The workaround of
// defaulting to the first videoInput available has been re-used from
// the previous device settings implementation.
const currentVideoId = this.state.videoInput
|| (this.props.currentVideoTrack
&& this.props.currentVideoTrack.getDeviceId())
|| (availableDevices.videoInput[0]
&& availableDevices.videoInput[0].deviceId)
|| ''; // DeviceSelector expects a string for prop selectedDeviceId.
const { _availableDevices } = this.props;
const configurations = [
{
devices: availableDevices.videoInput,
devices: _availableDevices.videoInput,
hasPermission: this.props.hasVideoPermission,
icon: 'icon-camera',
isDisabled: this.props.disableDeviceChange,
key: 'videoInput',
label: 'settings.selectCamera',
onSelect: this._getAndSetVideoTrack,
selectedDeviceId: currentVideoId
onSelect: this._updateVideoInput,
selectedDeviceId: this.state.selectedVideoInputId
},
{
devices: availableDevices.audioInput,
devices: _availableDevices.audioInput,
hasPermission: this.props.hasAudioPermission,
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange,
key: 'audioInput',
label: 'settings.selectMic',
onSelect: this._getAndSetAudioTrack,
selectedDeviceId: currentAudioId
onSelect: this._updateAudioInput,
selectedDeviceId: this.state.selectedAudioInputId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
devices: availableDevices.audioOutput,
devices: _availableDevices.audioOutput,
hasPermission: this.props.hasAudioPermission
|| this.props.hasVideoPermission,
icon: 'icon-volume',
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: this._getAndSetAudioOutput,
selectedDeviceId: currentAudioOutId
onSelect: this._updateAudioOutput,
selectedDeviceId: this.state.selectedAudioOutputId
});
}
return configurations.map(this._renderSelector);
}
/**
* Callback invoked when a new audio input device has been selected. Updates
* the internal state of the user's selection as well as the audio track
* that should display in the preview.
*
* @param {string} deviceId - The id of the chosen audio input device.
* @private
* @returns {void}
*/
_updateAudioInput(deviceId) {
this.setState({
selectedAudioInputId: deviceId
}, () => {
this._disposeAudioPreview()
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewAudioTrack: jitsiLocalTrack
});
})
.catch(() => {
this.setState({
previewAudioTrack: null
});
});
});
}
/**
* Callback invoked when a new audio output device has been selected.
* Updates the internal state of the user's selection.
*
* @param {string} deviceId - The id of the chosen audio output device.
* @private
* @returns {void}
*/
_updateAudioOutput(deviceId) {
this.setState({
selectedAudioOutputId: deviceId
});
}
/**
* Callback invoked when a new video input device has been selected. Updates
* the internal state of the user's selection as well as the video track
* that should display in the preview.
*
* @param {string} deviceId - The id of the chosen video input device.
* @private
* @returns {void}
*/
_updateVideoInput(deviceId) {
this.setState({
selectedVideoInputId: deviceId
}, () => {
this._disposeVideoPreview()
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewVideoTrack: jitsiLocalTrack,
previewVideoTrackError: null
});
})
.catch(() => {
this.setState({
previewVideoTrack: null,
previewVideoTrackError:
this.props.t('deviceSelection.previewUnavailable')
});
});
});
}
}
/**
@ -588,12 +517,12 @@ class DeviceSelectionDialog extends Component {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _devices: Object
* _availableDevices: Object
* }}
*/
function _mapStateToProps(state) {
return {
_devices: state['features/base/devices']
_availableDevices: state['features/base/devices']
};
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { translate } from '../../base/i18n';
const VIDEO_MUTE_CLASS = 'video-muted';
const VIDEO_ERROR_CLASS = 'video-preview-has-error';
/**
* React component for displaying video. This component defers to lib-jitsi-meet
@ -17,12 +17,18 @@ class VideoInputPreview extends Component {
* @static
*/
static propTypes = {
/**
* An error message to display instead of a preview. Displaying an error
* will take priority over displaying a video preview.
*/
error: React.PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func,
/*
/**
* The JitsiLocalTrack to display.
*/
track: React.PropTypes.object
@ -37,9 +43,37 @@ class VideoInputPreview extends Component {
constructor(props) {
super(props);
/**
* The internal reference to the DOM/HTML element intended for showing
* error messages.
*
* @private
* @type {HTMLDivElement}
*/
this._errorElement = null;
/**
* The internal reference to topmost DOM/HTML element backing the React
* {@code Component}. Accessed directly for toggling a classname to
* indicate an error is present so styling can be changed to display it.
*
* @private
* @type {HTMLDivElement}
*/
this._rootElement = null;
/**
* The internal reference to the DOM/HTML element intended for
* displaying a video. This element may be an HTML video element or a
* temasys video object.
*
* @private
* @type {HTMLVideoElement|Object}
*/
this._videoElement = null;
// Bind event handlers so they are only bound once for every instance.
this._setErrorElement = this._setErrorElement.bind(this);
this._setRootElement = this._setRootElement.bind(this);
this._setVideoElement = this._setVideoElement.bind(this);
}
@ -51,7 +85,11 @@ class VideoInputPreview extends Component {
* @returns {void}
*/
componentDidMount() {
this._attachTrack(this.props.track);
if (this.props.error) {
this._updateErrorView(this.props.error);
} else {
this._attachTrack(this.props.track);
}
}
/**
@ -80,9 +118,9 @@ class VideoInputPreview extends Component {
autoPlay = { true }
className = 'video-input-preview-display flipVideoX'
ref = { this._setVideoElement } />
<div className = 'video-input-preview-muted'>
{ this.props.t('deviceSelection.currentlyVideoMuted') }
</div>
<div
className = 'video-input-preview-error'
ref = { this._setErrorElement } />
</div>
);
}
@ -99,8 +137,15 @@ class VideoInputPreview extends Component {
* @returns {void}
*/
shouldComponentUpdate(nextProps) {
if (nextProps.track !== this.props.track) {
const hasNewTrack = nextProps.track !== this.props.track;
if (hasNewTrack || nextProps.error) {
this._detachTrack(this.props.track);
this._updateErrorView(nextProps.error);
}
// Never attempt to show the new track if there is an error present.
if (hasNewTrack && !nextProps.error) {
this._attachTrack(nextProps.track);
}
@ -123,17 +168,9 @@ class VideoInputPreview extends Component {
return;
}
// Do not attempt to display a preview if the track is muted, as the
// library will simply return a falsy value for the element anyway.
if (track.isMuted()) {
this._showMuteOverlay(true);
} else {
this._showMuteOverlay(false);
const updatedVideoElement = track.attach(this._videoElement);
const updatedVideoElement = track.attach(this._videoElement);
this._setVideoElement(updatedVideoElement);
}
this._setVideoElement(updatedVideoElement);
}
/**
@ -159,6 +196,19 @@ class VideoInputPreview extends Component {
}
}
/**
* Sets an instance variable for the component's element intended for
* displaying error messages. The element will be accessed directly to
* display an error message.
*
* @param {Object} element - DOM element intended for displaying errors.
* @private
* @returns {void}
*/
_setErrorElement(element) {
this._errorElement = element;
}
/**
* Sets the component's root element.
*
@ -183,20 +233,22 @@ class VideoInputPreview extends Component {
}
/**
* Adds or removes a class to the component's parent node to indicate mute
* status.
* Adds or removes a class to the component's parent node to indicate an
* error has occurred. Also sets the error text.
*
* @param {boolean} shouldShow - True if the mute class should be added and
* false if the class should be removed.
* @param {string} error - The error message to display. If falsy, error
* message display will be hidden.
* @private
* @returns {void}
*/
_showMuteOverlay(shouldShow) {
if (shouldShow) {
this._rootElement.classList.add(VIDEO_MUTE_CLASS);
_updateErrorView(error) {
if (error) {
this._rootElement.classList.add(VIDEO_ERROR_CLASS);
} else {
this._rootElement.classList.remove(VIDEO_MUTE_CLASS);
this._rootElement.classList.remove(VIDEO_ERROR_CLASS);
}
this._errorElement.innerText = error || '';
}
}

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<*>,
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) {

View File

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

View File

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

View File

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

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 moveToCorner'
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,16 @@
import React from 'react';
/**
* A functional React {@code Component} for showing an HD status label.
*
* @returns {ReactElement}
*/
export default function HDVideoLabel() {
return (
<span
className = 'video-state-indicator moveToCorner'
id = 'videoResolutionLabel'>
HD
</span>
);
}

View File

@ -0,0 +1,69 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import AudioOnlyLabel from './AudioOnlyLabel';
import HDVideoLabel from './HDVideoLabel';
/**
* React {@code Component} responsible for displaying a label that indicates
* the displayed video state of the current conference. {@code AudioOnlyLabel}
* will display when the conference is in audio only mode. {@code HDVideoLabel}
* will display if not in audio only mode and a high-definition large video is
* being displayed.
*/
export class VideoStatusLabel extends Component {
/**
* {@code VideoStatusLabel}'s property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the conference is in audio only mode.
*/
_audioOnly: React.PropTypes.bool,
/**
* Whether or not a high-definition large video is displayed.
*/
_largeVideoHD: React.PropTypes.bool
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
if (this.props._audioOnly) {
return <AudioOnlyLabel />;
} else if (this.props._largeVideoHD) {
return <HDVideoLabel />;
}
return null;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code VideoStatusLabel}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _largeVideoHD: boolean
* }}
*/
function _mapStateToProps(state) {
const { audioOnly, isLargeVideoHD } = state['features/base/conference'];
return {
_audioOnly: audioOnly,
_largeVideoHD: isLargeVideoHD
};
}
export default connect(_mapStateToProps)(VideoStatusLabel);

View File

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

View File

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

View File

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