Merge pull request #1509 from virtuacoplenny/lenny/web-audio-only
Audio only mode for web
This commit is contained in:
commit
166fb1d13f
|
@ -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,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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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).
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1 @@
|
|||
export { default as VideoStatusLabel } from './VideoStatusLabel';
|
|
@ -0,0 +1 @@
|
|||
export * from './components';
|
|
@ -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