diff --git a/Makefile b/Makefile
index 735923fbc..12624e6e0 100644
--- a/Makefile
+++ b/Makefile
@@ -47,5 +47,6 @@ source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r analytics.js external_api.js favicon.ico fonts images index.html interface_config.js libs plugin.*html sounds title.html unsupported_browser.html LICENSE config.js lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \
+ cp css/unsupported_browser.css source_package/jitsi-meet/css && \
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
rm -rf source_package
diff --git a/README.md b/README.md
index 7081f2772..07a4f8ddb 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [J
You can also try it out yourself at https://meet.jit.si .
-Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad and remote presentations with Prezi.
+Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad.
## Installation
diff --git a/conference.js b/conference.js
index 529a6c1aa..90d8bc007 100644
--- a/conference.js
+++ b/conference.js
@@ -28,10 +28,11 @@ const Commands = {
CONNECTION_QUALITY: "stats",
EMAIL: "email",
ETHERPAD: "etherpad",
- PREZI: "prezi",
- STOP_PREZI: "stop-prezi"
+ SHARED_VIDEO: "shared-video"
};
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
+
/**
* Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use
@@ -258,6 +259,10 @@ class ConferenceConnector {
APP.UI.notifyFocusLeft();
break;
+ case ConferenceErrors.CONFERENCE_MAX_USERS:
+ connection.disconnect();
+ APP.UI.notifyMaxUsersLimitReached();
+ break;
default:
this._handleConferenceFailed(err, ...params);
}
@@ -397,6 +402,15 @@ export default {
listMembersIds () {
return room.getParticipants().map(p => p.getId());
},
+ /**
+ * Checks whether the participant identified by id is a moderator.
+ * @id id to search for participant
+ * @return {boolean} whether the participant is moderator
+ */
+ isParticipantModerator (id) {
+ let user = room.getParticipantById(id);
+ return user && user.isModerator();
+ },
/**
* Check if SIP is supported.
* @returns {boolean}
@@ -506,6 +520,51 @@ export default {
this._setupListeners();
},
+
+ /**
+ * Exposes a Command(s) API on this instance. It is necessitated by (1) the
+ * desire to keep room private to this instance and (2) the need of other
+ * modules to send and receive commands to and from participants.
+ * Eventually, this instance remains in control with respect to the
+ * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
+ * JitsiConference) is to be used in the implementation of the Command(s)
+ * API of this instance.
+ */
+ commands: {
+ /**
+ * Receives notifications from other participants about commands aka
+ * custom events (sent by sendCommand or sendCommandOnce methods).
+ * @param command {String} the name of the command
+ * @param handler {Function} handler for the command
+ */
+ addCommandListener () {
+ room.addCommandListener.apply(room, arguments);
+ },
+ /**
+ * Removes command.
+ * @param name {String} the name of the command.
+ */
+ removeCommand () {
+ room.removeCommand.apply(room, arguments);
+ },
+ /**
+ * Sends command.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommand () {
+ room.sendCommand.apply(room, arguments);
+ },
+ /**
+ * Sends command one time.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommandOnce () {
+ room.sendCommandOnce.apply(room, arguments);
+ },
+ },
+
_getConferenceOptions() {
let options = config;
if(config.enableRecording) {
@@ -687,7 +746,7 @@ export default {
console.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
APP.UI.removeUser(id, user.getDisplayName());
- APP.UI.stopPrezi(id);
+ APP.UI.stopSharedVideo(id);
});
@@ -866,35 +925,6 @@ export default {
APP.UI.initEtherpad(value);
});
- room.addCommandListener(Commands.PREZI, ({value, attributes}) => {
- APP.UI.showPrezi(attributes.id, value, attributes.slide);
- });
-
- room.addCommandListener(Commands.STOP_PREZI, ({attributes}) => {
- APP.UI.stopPrezi(attributes.id);
- });
-
- APP.UI.addListener(UIEvents.SHARE_PREZI, (url, slide) => {
- console.log('Sharing Prezi %s slide %s', url, slide);
- room.removeCommand(Commands.PREZI);
- room.sendCommand(Commands.PREZI, {
- value: url,
- attributes: {
- id: room.myUserId(),
- slide
- }
- });
- });
-
- APP.UI.addListener(UIEvents.STOP_SHARING_PREZI, () => {
- room.removeCommand(Commands.PREZI);
- room.sendCommandOnce(Commands.STOP_PREZI, {
- attributes: {
- id: room.myUserId()
- }
- });
- });
-
APP.UI.addListener(UIEvents.EMAIL_CHANGED, (email = '') => {
email = email.trim();
@@ -1011,8 +1041,20 @@ export default {
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
room.selectParticipant(id);
});
- APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
- room.pinParticipant(id);
+
+ APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
+ var smallVideoId = smallVideo.getId();
+
+ if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
+ && !APP.conference.isLocalId(smallVideoId))
+ if (isPinned)
+ room.pinParticipant(smallVideoId);
+ // When the library starts supporting multiple pins we would
+ // pass the isPinned parameter together with the identifier,
+ // but currently we send null to indicate that we unpin the
+ // last pinned.
+ else
+ room.pinParticipant(null);
});
APP.UI.addListener(
@@ -1040,5 +1082,47 @@ export default {
APP.UI.addListener(
UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
);
+
+ APP.UI.addListener(UIEvents.UPDATE_SHARED_VIDEO,
+ (url, state, time, volume) => {
+ // send start and stop commands once, and remove any updates
+ // that had left
+ if (state === 'stop' || state === 'start' || state === 'playing') {
+ room.removeCommand(Commands.SHARED_VIDEO);
+ room.sendCommandOnce(Commands.SHARED_VIDEO, {
+ value: url,
+ attributes: {
+ state: state,
+ time: time,
+ volume: volume
+ }
+ });
+ }
+ else {
+ // in case of paused, in order to allow late users to join
+ // paused
+ room.removeCommand(Commands.SHARED_VIDEO);
+ room.sendCommand(Commands.SHARED_VIDEO, {
+ value: url,
+ attributes: {
+ state: state,
+ time: time,
+ volume: volume
+ }
+ });
+ }
+ });
+ room.addCommandListener(
+ Commands.SHARED_VIDEO, ({value, attributes}, id) => {
+
+ if (attributes.state === 'stop') {
+ APP.UI.stopSharedVideo(id);
+ } else if (attributes.state === 'start') {
+ APP.UI.showSharedVideo(id, value, attributes);
+ } else if (attributes.state === 'playing'
+ || attributes.state === 'pause') {
+ APP.UI.updateSharedVideo(id, value, attributes);
+ }
+ });
}
};
diff --git a/config.js b/config.js
index 4dda7b85f..8f25c6b76 100644
--- a/config.js
+++ b/config.js
@@ -70,4 +70,5 @@ var config = {
'During that time service will not be available. ' +
'Apologise for inconvenience.',*/
disableThirdPartyRequests: false,
+ minHDHeight: 540
};
diff --git a/css/chat.css b/css/chat.css
index 1cf801c0e..65a7a068f 100644
--- a/css/chat.css
+++ b/css/chat.css
@@ -95,8 +95,6 @@
#unreadMessages {
font-size: 8px;
position: absolute;
- left: 46%;
- top: 27%
}
#bottomUnreadMessages {
diff --git a/css/font.css b/css/font.css
index d7a1d82ed..076d668ac 100644
--- a/css/font.css
+++ b/css/font.css
@@ -63,9 +63,6 @@
.icon-exit-full-screen:before {
content: "\e60e";
}
-.icon-prezi:before {
- content: "\e60c";
-}
.icon-link:before {
content: "\e600";
}
diff --git a/css/main.css b/css/main.css
index 9ae59adb3..463018499 100644
--- a/css/main.css
+++ b/css/main.css
@@ -38,16 +38,6 @@ html, body{
position: relative;
}
-#toolbar a.button::after {
- content: '';
- display: inline-block;
- position: absolute;
- left: 40px;
- width: 1px;
- height: 20px;
- background: #373737;
-}
-
#toolbar a.button:last-child::after {
content: none;
}
@@ -56,13 +46,16 @@ html, body{
display: inline-block;
position: relative;
color: #FFFFFF;
- top: 0;
- padding: 10px 0;
+ top:0px;
+ padding-top: 10px;
width: 38px;
+ height: 28px;
cursor: pointer;
text-align: center;
text-shadow: 0 1px 0 rgba(255,255,255,.3), 0 -1px 0 rgba(0,0,0,.6);
z-index: 1;
+ font-size: 1.22em !important;
+ vertical-align: middle;
}
.toolbar_span>span {
@@ -103,13 +96,13 @@ html, body{
#numberOfParticipants {
position: absolute;
top: 0px;
- right: -1;
+ right: -1px;
color: white;
width: 13px;
height: 13px;
line-height: 13px;
font-weight: bold;
- border-radius: 2px;
+ border-radius: 1px;
font-size: 11px;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
@@ -118,6 +111,19 @@ html, body{
color: #00ccff;
}
+#toolbar {
+ display:inline-block;
+ position:relative;
+ top:5px;
+ margin-left:auto;
+ margin-right:auto;
+ height:38px;
+ width:auto;
+ background-color: rgba(0,0,0,0.5);
+ border-radius: 1px;
+ pointer-events: auto;
+}
+
#toolbar_button_record {
-webkit-transition: all .5s ease-in-out;
-moz-transition: all .5s ease-in-out;
@@ -141,13 +147,10 @@ html, body{
a.button:hover,
a.bottomToolbarButton:hover {
- top: 0px;
cursor: pointer;
background: rgba(255, 255, 255, 0.1);
- border-radius: 6px;
- background-clip: padding-box;
- -webkit-border-radius: 6px;
- -webkit-background-clip: padding-box;
+ border-radius: 1px;
+ -webkit-border-radius: 1px;
}
.no-fa-video-camera, .fa-microphone-slash {
@@ -157,7 +160,7 @@ a.bottomToolbarButton:hover {
.header_button_separator {
display: inline-block;
position:relative;
- top: 5;
+ top: 5px;
width: 1px;
height: 20px;
background: #373737;
@@ -166,7 +169,7 @@ a.bottomToolbarButton:hover {
.bottom_button_separator {
display: inline-block;
position: relative;
- left: 5;
+ left: 5px;
width: 20px;
height: 1px;
background: #373737;
@@ -210,7 +213,7 @@ button {
height: 35px;
padding: 0 1em 0 2em;
position: relative;
- border-radius: 3px;
+ border-radius: 1px;
font-weight: bold;
color: #fff;
line-height: 35px;
@@ -273,10 +276,9 @@ div.feedbackButton:hover {
margin-right: 5px;
bottom: 40px;
width: 29px;
- border-radius: 6px;
+ border-radius: 1px;
color: #FFF;
- border: 1px solid rgba(256, 256, 256, 0.2);
- background: rgba(0,0,0,0.8);
+ background: rgba(0,0,0,0.5);
z-index: 6; /*+1 from #remoteVideos*/
}
@@ -350,7 +352,7 @@ div.feedbackButton:hover {
}
#toast-container.notification-bottom-right {
- bottom: 120px;
+ bottom: 140px;
right: 5px;
}
diff --git a/css/popup_menu.css b/css/popup_menu.css
index 300d95f4b..041e04229 100644
--- a/css/popup_menu.css
+++ b/css/popup_menu.css
@@ -13,7 +13,7 @@ ul.popupmenu {
width: 100px;
background-color: rgba(0,0,0,0.9);
border: 1px solid rgba(256, 256, 256, 0.2);
- border-radius:8px;
+ border-radius:3px;
}
ul.popupmenu:after {
@@ -31,7 +31,7 @@ ul.popupmenu li {
ul.popupmenu li:hover {
background-color: rgba(256, 256, 256, .2);
- border-radius:6px;
+ border-radius:3px;
}
/*Link Appearance*/
diff --git a/css/settingsmenu.css b/css/settingsmenu.css
index c990dfb4a..1cd63203e 100644
--- a/css/settingsmenu.css
+++ b/css/settingsmenu.css
@@ -43,29 +43,25 @@
cursor: pointer;
}
-
-#startMutedOptions {
+#startMutedOptions,
+#followMeOptions {
padding-left: 10%;
text-indent: -10%;
-
margin-top: 10px;
-
display: none; /* hide by default */
-
/* clearfix */
overflow: auto;
zoom: 1;
}
-#startAudioMuted {
+#startAudioMuted,
+#startVideoMuted,
+#followMeCheckBox {
width: 13px !important;
}
-#startVideoMuted {
- width: 13px !important;
-}
-
-.startMutedLabel {
+.startMutedLabel,
+.followMeLabel {
width: 94%;
float: left;
cursor: pointer;
diff --git a/css/videolayout_default.css b/css/videolayout_default.css
index f298d05be..1e02c4aa4 100644
--- a/css/videolayout_default.css
+++ b/css/videolayout_default.css
@@ -21,6 +21,7 @@
width: auto;
height: auto !important;
z-index: 5;
+ font-size: 0pt; /*!!!Removes the gap between the local video container and the remote videos.*/
}
#remoteVideos.hidden {
@@ -30,18 +31,21 @@
.videocontainer {
position: relative;
+ margin-left: auto;
+ margin-right: auto;
+ text-align: center;
}
#remoteVideos .videocontainer {
display: none;
background-color: black;
background-size: contain;
- border-radius:8px;
- border: 2px solid #212425;
- margin-right: 3px;
+ border-radius:1px;
+ border: 1px solid #212425;
+ /*margin-right: 1px;*/
}
-#remoteVideos .videocontainer:hover,
+/*#remoteVideos .videocontainer:hover,*/
#remoteVideos .videocontainer.videoContainerFocused {
cursor: hand;
/* transform:scale(1.08, 1.08);
@@ -55,25 +59,21 @@
}
#remoteVideos .videocontainer:hover {
- box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF;
- border: 2px solid #FFFFFF;
+ border: 1px solid #c1c1c1;
}
#remoteVideos .videocontainer.videoContainerFocused {
box-shadow: inset 0 0 28px #006d91;
- border: 2px solid #006d91;
+ border: 1px solid #006d91;
}
#remoteVideos .videocontainer.videoContainerFocused:hover {
- box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91;
- border: 2px solid #FFFFFF;
+ box-shadow: inset 0 0 5px #c1c1c1, 0 0 10px #c1c1c1, inset 0 0 60px #006d91;
+ border: 1px solid #c1c1c1;
}
#localVideoWrapper {
display:inline-block;
- -webkit-mask-box-image: url(../images/videomask.svg);
- border-radius:4px !important;
- border: 0px !important;
}
/* With TemasysWebRTC plugin element is used
@@ -81,7 +81,8 @@
#remoteVideos .videocontainer>video,
#remoteVideos .videocontainer>object {
cursor: hand;
- border-radius:4px;
+ border-radius:1px;
+ object-fit: cover;
}
.flipVideoX {
@@ -94,7 +95,8 @@
#localVideoWrapper>video,
#localVideoWrapper>object {
cursor: hand;
- border-radius:4px !important;
+ border-radius:1px !important;
+ object-fit: cover;
}
#largeVideo,
@@ -111,6 +113,7 @@
}
#presentation,
+#sharedVideo,
#etherpad,
#localVideoWrapper>video,
#localVideoWrapper>object,
@@ -173,7 +176,7 @@
overflow: hidden;
white-space: nowrap;
z-index: 2;
- border-radius:20px;
+ border-radius:3px;
}
.videocontainer>span.status {
@@ -192,7 +195,7 @@
overflow: hidden;
white-space: nowrap;
z-index: 2;
- border-radius:20px;
+ border-radius:3px;
}
.connectionindicator
@@ -390,14 +393,14 @@
display: inline-block;
position: absolute;
z-index: 0;
- border-radius:10px;
+ border-radius:1px;
pointer-events: none;
}
#dominantSpeaker {
visibility: hidden;
- width: 150px;
- height: 150px;
+ width: 300px;
+ height: 300px;
margin: auto;
overflow: hidden;
position: relative;
@@ -416,21 +419,28 @@
}
#dominantSpeakerAvatar {
- width: 100px;
- height: 100px;
- top: 25px;
+ width: 200px;
+ height: 200px;
+ top: 50px;
margin: auto;
position: relative;
- border-radius: 50px;
+ border-radius: 100px;
z-index: 3;
visibility: inherit;
+ background-color: #000000;
}
.userAvatar {
height: 100%;
position: absolute;
- left: 35px;
- border-radius: 200px;
+ left: 0;
+ border-radius: 2px;
+}
+
+.sharedVideoAvatar {
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
}
.noMic {
@@ -483,3 +493,14 @@
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
}
+
+#videoResolutionLabel {
+ display: none;
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ background: rgba(0,0,0,.5);
+ padding: 10px;
+ color: rgba(255,255,255,.5);
+ z-index: 10000;
+}
diff --git a/css/welcome_page.css b/css/welcome_page.css
index 0f1a11031..3f38f6e49 100644
--- a/css/welcome_page.css
+++ b/css/welcome_page.css
@@ -42,11 +42,11 @@
}
#enter_room_form {
- border-radius: 10px;
+ border-radius: 1px;
background-color: #FFFFFF;
border: none;
- -moz-border-radius: 10px;
- -webkit-border-radius: 10px;
+ -moz-border-radius: 1px;
+ -webkit-border-radius: 1px;
-webkit-appearance: none;
box-shadow: none;
}
@@ -78,8 +78,8 @@
width: 73px;
height: 45px;
background-color: #16a8fe;
- moz-border-radius: 10px;
- -webkit-border-radius: 10px;
+ moz-border-radius: 1px;
+ -webkit-border-radius: 1px;
color: #ffffff;
font-weight: 600;
border: none;
diff --git a/index.html b/index.html
index 5aa91fe89..848e8fe88 100644
--- a/index.html
+++ b/index.html
@@ -144,8 +144,8 @@
-
+
@@ -156,11 +156,11 @@
-
+
@@ -175,6 +175,7 @@
+ HD
@@ -262,6 +263,12 @@
+
+
+
diff --git a/interface_config.js b/interface_config.js
index aae97d279..9d0f05a62 100644
--- a/interface_config.js
+++ b/interface_config.js
@@ -1,6 +1,6 @@
var interfaceConfig = {
CANVAS_EXTRA: 104,
- CANVAS_RADIUS: 7,
+ CANVAS_RADIUS: 0,
SHADOW_COLOR: '#ffffff',
INITIAL_TOOLBAR_TIMEOUT: 20000,
TOOLBAR_TIMEOUT: 4000,
@@ -14,9 +14,8 @@ var interfaceConfig = {
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
APP_NAME: "Jitsi Meet",
INVITATION_POWERED_BY: true,
- DOMINANT_SPEAKER_AVATAR_SIZE: 100,
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
- 'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad',
+ 'recording', 'security', 'invite', 'chat', 'etherpad', 'sharedvideo',
'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',
'contacts'],
// Determines how the video would fit the screen. 'both' would fit the whole
@@ -30,5 +29,5 @@ var interfaceConfig = {
filmStripOnly: false,
RANDOM_AVATAR_URL_PREFIX: false,
RANDOM_AVATAR_URL_SUFFIX: false,
- FILM_STRIP_MAX_HEIGHT: 160
+ FILM_STRIP_MAX_HEIGHT: 120
};
diff --git a/lang/main.json b/lang/main.json
index c2863f3b5..24199d190 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -9,7 +9,7 @@
"me": "me",
"speaker": "Speaker",
"defaultNickname": "ex. __name__",
- "defaultPreziLink": "e.g. __url__",
+ "defaultLink": "e.g. __url__",
"welcomepage":{
"go": "GO",
"roomname": "Enter room name",
@@ -55,8 +55,8 @@
"lock": "Lock / unlock room",
"invite": "Invite others",
"chat": "Open / close chat",
- "prezi": "Share Prezi",
"etherpad": "Shared document",
+ "sharedvideo": "Shared video",
"sharescreen": "Share screen",
"fullscreen": "Enter / Exit Full Screen",
"sip": "Call SIP number",
@@ -86,7 +86,8 @@
"startAudioMuted": "start without audio",
"startVideoMuted": "start without video",
"selectCamera": "select camera",
- "selectMic": "select microphone"
+ "selectMic": "select microphone",
+ "followMe": "Enable follow me"
},
"videothumbnail":
{
@@ -146,6 +147,7 @@
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
"bridgeUnavailable": "Jitsi Videobridge is currently unavailable. Please try again later!",
"jicofoUnavailable": "Jicofo is currently unavailable. Please try again later!",
+ "maxUsersLimitReached": "The limit for maximum number of participants in the conference has been reached. The conference is full. Please try again later!",
"lockTitle": "Lock failed",
"lockMessage": "Failed to lock the conference.",
"warning": "Warning",
@@ -159,11 +161,12 @@
"defaultError": "There was some kind of error",
"passwordRequired": "Password required",
"Ok": "Ok",
- "removePreziTitle": "Remove Prezi",
- "removePreziMsg": "Are you sure you would like to remove your Prezi?",
- "sharePreziTitle": "Share a Prezi",
- "sharePreziMsg": "Another participant is already sharing a Prezi. This conference allows only one Prezi at a time.",
"Remove": "Remove",
+ "shareVideoTitle": "Share a video",
+ "shareVideoLinkError": "Please provide a correct youtube link.",
+ "removeSharedVideoTitle": "Remove shared video",
+ "removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
+ "alreadySharedVideoMsg": "Another participant is already sharing video. This conference allows only one shared video at a time.",
"WaitingForHost": "Waiting for the host ...",
"WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"IamHost": "I am the host",
@@ -175,7 +178,6 @@
"hungUp": "You hung up",
"joinAgain": "Join again",
"Share": "Share",
- "preziLinkError": "Please provide a correct prezi link.",
"Save": "Save",
"recordingToken": "Enter recording token",
"Dial": "Dial",
diff --git a/modules/FollowMe.js b/modules/FollowMe.js
new file mode 100644
index 000000000..0d5086809
--- /dev/null
+++ b/modules/FollowMe.js
@@ -0,0 +1,333 @@
+/*
+ * Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UIEvents from '../service/UI/UIEvents';
+import VideoLayout from './UI/videolayout/VideoLayout';
+import FilmStrip from './UI/videolayout/FilmStrip';
+
+/**
+ * The (name of the) command which transports the state (represented by
+ * {State} for the local state at the time of this writing) of a {FollowMe}
+ * (instance) between participants.
+ */
+const _COMMAND = "follow-me";
+
+/**
+ * Represents the set of {FollowMe}-related states (properties and their
+ * respective values) which are to be followed by a participant. {FollowMe}
+ * will send {_COMMAND} whenever a property of {State} changes (if the local
+ * participant is in her right to issue such a command, of course).
+ */
+class State {
+ /**
+ * Initializes a new {State} instance.
+ *
+ * @param propertyChangeCallback {Function} which is to be called when a
+ * property of the new instance has its value changed from an old value
+ * into a (different) new value. The function is supplied with the name of
+ * the property, the old value of the property before the change, and the
+ * new value of the property after the change.
+ */
+ constructor (propertyChangeCallback) {
+ this._propertyChangeCallback = propertyChangeCallback;
+ }
+
+ get filmStripVisible () { return this._filmStripVisible; }
+
+ set filmStripVisible (b) {
+ var oldValue = this._filmStripVisible;
+ if (oldValue !== b) {
+ this._filmStripVisible = b;
+ this._firePropertyChange('filmStripVisible', oldValue, b);
+ }
+ }
+
+ get nextOnStage() { return this._nextOnStage; }
+
+ set nextOnStage(id) {
+ var oldValue = this._nextOnStage;
+ if (oldValue !== id) {
+ this._nextOnStage = id;
+ this._firePropertyChange('nextOnStage', oldValue, id);
+ }
+ }
+
+ get sharedDocumentVisible () { return this._sharedDocumentVisible; }
+
+ set sharedDocumentVisible (b) {
+ var oldValue = this._sharedDocumentVisible;
+ if (oldValue !== b) {
+ this._sharedDocumentVisible = b;
+ this._firePropertyChange('sharedDocumentVisible', oldValue, b);
+ }
+ }
+
+ /**
+ * Invokes {_propertyChangeCallback} to notify it that {property} had its
+ * value changed from {oldValue} to {newValue}.
+ *
+ * @param property the name of the property which had its value changed
+ * from {oldValue} to {newValue}
+ * @param oldValue the value of {property} before the change
+ * @param newValue the value of {property} after the change
+ */
+ _firePropertyChange (property, oldValue, newValue) {
+ var propertyChangeCallback = this._propertyChangeCallback;
+ if (propertyChangeCallback)
+ propertyChangeCallback(property, oldValue, newValue);
+ }
+}
+
+/**
+ * Represents the "Follow Me" feature which enables a moderator to
+ * (partially) control the user experience/interface (e.g. film strip
+ * visibility) of (other) non-moderator particiapnts.
+ *
+ * @author Lyubomir Marinov
+ */
+class FollowMe {
+ /**
+ * Initializes a new {FollowMe} instance.
+ *
+ * @param conference the {conference} which is to transport
+ * {FollowMe}-related information between participants
+ * @param UI the {UI} which is the source (model/state) to be sent to
+ * remote participants if the local participant is the moderator or the
+ * destination (model/state) to receive from the remote moderator if the
+ * local participant is not the moderator
+ */
+ constructor (conference, UI) {
+ this._conference = conference;
+ this._UI = UI;
+
+ // The states of the local participant which are to be followed (by the
+ // remote participants when the local participant is in her right to
+ // issue such commands).
+ this._local = new State(this._localPropertyChange.bind(this));
+
+ // Listen to "Follow Me" commands. I'm not sure whether a moderator can
+ // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
+ // possible, then it may be easiest to always listen to commands. The
+ // listener will validate received commands before acting on them.
+ conference.commands.addCommandListener(
+ _COMMAND,
+ this._onFollowMeCommand.bind(this));
+ }
+
+ /**
+ * Adds listeners for the UI states of the local participant which are
+ * to be followed (by the remote participants). A non-moderator (very
+ * likely) can become a moderator so it may be easiest to always track
+ * the states of interest.
+ * @private
+ */
+ _addFollowMeListeners () {
+ this.filmStripEventHandler = this._filmStripToggled.bind(this);
+ this._UI.addListener(UIEvents.TOGGLED_FILM_STRIP,
+ this.filmStripEventHandler);
+
+ var self = this;
+ this.pinnedEndpointEventHandler = function (smallVideo, isPinned) {
+ self._nextOnStage(smallVideo, isPinned);
+ };
+ this._UI.addListener(UIEvents.PINNED_ENDPOINT,
+ this.pinnedEndpointEventHandler);
+
+ this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
+ this._UI.addListener( UIEvents.TOGGLED_SHARED_DOCUMENT,
+ this.sharedDocEventHandler);
+ }
+
+ /**
+ * Removes all follow me listeners.
+ * @private
+ */
+ _removeFollowMeListeners () {
+ this._UI.removeListener(UIEvents.TOGGLED_FILM_STRIP,
+ this.filmStripEventHandler);
+ this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
+ this.sharedDocEventHandler);
+ this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
+ this.pinnedEndpointEventHandler);
+ }
+
+ /**
+ * Enables or disabled the follow me functionality
+ *
+ * @param enable {true} to enable the follow me functionality, {false} -
+ * to disable it
+ */
+ enableFollowMe (enable) {
+ this.isEnabled = enable;
+ if (this.isEnabled)
+ this._addFollowMeListeners();
+ else
+ this._removeFollowMeListeners();
+ }
+
+ /**
+ * Notifies this instance that the (visibility of the) film strip was
+ * toggled (in the user interface of the local participant).
+ *
+ * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
+ * a result of the toggle) or {false} if the film strip was hidden
+ */
+ _filmStripToggled (filmStripVisible) {
+ this._local.filmStripVisible = filmStripVisible;
+ }
+
+ /**
+ * Notifies this instance that the (visibility of the) shared document was
+ * toggled (in the user interface of the local participant).
+ *
+ * @param sharedDocumentVisible {Boolean} {true} if the shared document was
+ * shown (as a result of the toggle) or {false} if it was hidden
+ */
+ _sharedDocumentToggled (sharedDocumentVisible) {
+ this._local.sharedDocumentVisible = sharedDocumentVisible;
+ }
+
+ /**
+ * Changes the nextOnPage property value.
+ *
+ * @param smallVideo the {SmallVideo} that was pinned or unpinned
+ * @param isPinned indicates if the given {SmallVideo} was pinned or
+ * unpinned
+ * @private
+ */
+ _nextOnStage (smallVideo, isPinned) {
+ if (!this._conference.isModerator)
+ return;
+
+ var nextOnStage = null;
+ if(isPinned)
+ nextOnStage = smallVideo.getId();
+
+ this._local.nextOnStage = nextOnStage;
+ }
+
+ /**
+ * Sends the follow-me command, when a local property change occurs.
+ *
+ * @param property the property name
+ * @param oldValue the old value
+ * @param newValue the new value
+ * @private
+ */
+ _localPropertyChange (property, oldValue, newValue) {
+ // Only a moderator is allowed to send commands.
+ var conference = this._conference;
+ if (!conference.isModerator)
+ return;
+
+ var commands = conference.commands;
+ // XXX The "Follow Me" command represents a snapshot of all states
+ // which are to be followed so don't forget to removeCommand before
+ // sendCommand!
+ commands.removeCommand(_COMMAND);
+ var self = this;
+ commands.sendCommandOnce(
+ _COMMAND,
+ {
+ attributes: {
+ filmStripVisible: self._local.filmStripVisible,
+ nextOnStage: self._local.nextOnStage,
+ sharedDocumentVisible: self._local.sharedDocumentVisible
+ }
+ });
+ }
+
+ /**
+ * Notifies this instance about a &qout;Follow Me&qout; command (delivered
+ * by the Command(s) API of {this._conference}).
+ *
+ * @param attributes the attributes {Object} carried by the command
+ * @param id the identifier of the participant who issued the command. A
+ * notable idiosyncrasy of the Command(s) API to be mindful of here is that
+ * the command may be issued by the local participant.
+ */
+ _onFollowMeCommand ({ attributes }, id) {
+ // We require to know who issued the command because (1) only a
+ // moderator is allowed to send commands and (2) a command MUST be
+ // issued by a defined commander.
+ if (typeof id === 'undefined')
+ return;
+ // The Command(s) API will send us our own commands and we don't want
+ // to act upon them.
+ if (this._conference.isLocalId(id))
+ return;
+
+ if (!this._conference.isParticipantModerator(id))
+ {
+ console.warn('Received follow-me command ' +
+ 'not from moderator');
+ return;
+ }
+
+ // Applies the received/remote command to the user experience/interface
+ // of the local participant.
+ this._onFilmStripVisible(attributes.filmStripVisible);
+ this._onNextOnStage(attributes.nextOnStage);
+ this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
+ }
+
+ _onFilmStripVisible(filmStripVisible) {
+ if (typeof filmStripVisible !== 'undefined') {
+ // XXX The Command(s) API doesn't preserve the types (of
+ // attributes, at least) at the time of this writing so take into
+ // account that what originated as a Boolean may be a String on
+ // receipt.
+ filmStripVisible = (filmStripVisible == 'true');
+
+ // FIXME The UI (module) very likely doesn't (want to) expose its
+ // eventEmitter as a public field. I'm not sure at the time of this
+ // writing whether calling UI.toggleFilmStrip() is acceptable (from
+ // a design standpoint) either.
+ if (filmStripVisible !== FilmStrip.isFilmStripVisible())
+ this._UI.eventEmitter.emit(
+ UIEvents.TOGGLE_FILM_STRIP,
+ filmStripVisible);
+ }
+ }
+
+ _onNextOnStage(id) {
+
+ var clickId = null;
+ if(typeof id !== 'undefined' && !VideoLayout.isPinned(id))
+ clickId = id;
+ else if (typeof id == 'undefined' && VideoLayout.getPinnedId())
+ clickId = VideoLayout.getPinnedId();
+
+ if (clickId)
+ VideoLayout.handleVideoThumbClicked(clickId);
+ }
+
+ _onSharedDocumentVisible(sharedDocumentVisible) {
+ if (typeof sharedDocumentVisible !== 'undefined') {
+ // XXX The Command(s) API doesn't preserve the types (of
+ // attributes, at least) at the time of this writing so take into
+ // account that what originated as a Boolean may be a String on
+ // receipt.
+ sharedDocumentVisible = (sharedDocumentVisible == 'true');
+
+ if (sharedDocumentVisible
+ !== this._UI.getSharedDocumentManager().isVisible())
+ this._UI.getSharedDocumentManager().toggleEtherpad();
+ }
+ }
+}
+
+export default FollowMe;
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index e68113ff2..4e4a9d99f 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -12,8 +12,8 @@ import PanelToggler from "./side_pannels/SidePanelToggler";
import UIUtil from "./util/UIUtil";
import UIEvents from "../../service/UI/UIEvents";
import CQEvents from '../../service/connectionquality/CQEvents';
-import PreziManager from './prezi/Prezi';
import EtherpadManager from './etherpad/Etherpad';
+import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from "./videolayout/VideoLayout";
import FilmStrip from "./videolayout/FilmStrip";
@@ -27,11 +27,15 @@ var messageHandler = UI.messageHandler;
var JitsiPopover = require("./util/JitsiPopover");
var Feedback = require("./Feedback");
+import FollowMe from "../FollowMe";
+
var eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
-let preziManager;
let etherpadManager;
+let sharedVideoManager;
+
+let followMeHandler;
/**
* Prompt user for nickname.
@@ -96,7 +100,6 @@ function setupChat() {
*/
function setupToolbars() {
Toolbar.init(eventEmitter);
- Toolbar.setupButtonsFromConfig();
BottomToolbar.setupListeners(eventEmitter);
}
@@ -230,6 +233,10 @@ UI.initConference = function () {
// Add myself to the contact list.
ContactList.addContact(id);
+ //update default button states before showing the toolbar
+ //if local role changes buttons state will be again updated
+ UI.updateLocalRole(false);
+
// Once we've joined the muc show the toolbar
ToolbarToggler.showToolbar();
@@ -246,6 +253,12 @@ UI.initConference = function () {
if(!interfaceConfig.filmStripOnly) {
Feedback.init();
}
+
+ // FollowMe attempts to copy certain aspects of the moderator's UI into the
+ // other participants' UI. Consequently, it needs (1) read and write access
+ // to the UI (depending on the moderator role of the local participant) and
+ // (2) APP.conference as means of communication between the participants.
+ followMeHandler = new FollowMe(APP.conference, UI);
};
UI.mucJoined = function () {
@@ -256,9 +269,6 @@ UI.mucJoined = function () {
* Setup some UI event listeners.
*/
function registerListeners() {
- UI.addListener(UIEvents.PREZI_CLICKED, function () {
- preziManager.handlePreziButtonClicked();
- });
UI.addListener(UIEvents.ETHERPAD_CLICKED, function () {
if (etherpadManager) {
@@ -266,6 +276,12 @@ function registerListeners() {
}
});
+ UI.addListener(UIEvents.SHARED_VIDEO_CLICKED, function () {
+ if (sharedVideoManager) {
+ sharedVideoManager.toggleSharedVideo();
+ }
+ });
+
UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen);
UI.addListener(UIEvents.TOGGLE_CHAT, UI.toggleChat);
@@ -276,7 +292,15 @@ function registerListeners() {
UI.addListener(UIEvents.TOGGLE_CONTACT_LIST, UI.toggleContactList);
- UI.addListener(UIEvents.TOGGLE_FILM_STRIP, UI.toggleFilmStrip);
+ UI.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
+ UI.toggleFilmStrip();
+ VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
+ });
+
+ UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
+ if (followMeHandler)
+ followMeHandler.enableFollowMe(isEnabled);
+ });
}
/**
@@ -326,7 +350,7 @@ UI.start = function () {
registerListeners();
BottomToolbar.init();
- FilmStrip.init();
+ FilmStrip.init(eventEmitter);
VideoLayout.init(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
@@ -337,7 +361,7 @@ UI.start = function () {
ContactList.init(eventEmitter);
bindEvents();
- preziManager = new PreziManager(eventEmitter);
+ sharedVideoManager = new SharedVideoManager(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
$("#videospace").mousemove(function () {
@@ -465,10 +489,19 @@ UI.initEtherpad = function (name) {
return;
}
console.log('Etherpad is enabled');
- etherpadManager = new EtherpadManager(config.etherpad_base, name);
+ etherpadManager
+ = new EtherpadManager(config.etherpad_base, name, eventEmitter);
Toolbar.showEtherpadButton();
};
+/**
+ * Returns the shared document manager object.
+ * @return {EtherpadManager} the shared document manager object
+ */
+UI.getSharedDocumentManager = function () {
+ return etherpadManager;
+};
+
/**
* Show user on UI.
* @param {string} id user id
@@ -485,11 +518,11 @@ UI.addUser = function (id, displayName) {
config.startAudioMuted > APP.conference.membersCount)
UIUtil.playSoundNotification('userJoined');
- // Configure avatar
- UI.setUserAvatar(id);
-
// Add Peer's container
VideoLayout.addParticipantContainer(id);
+
+ // Configure avatar
+ UI.setUserAvatar(id);
};
/**
@@ -534,7 +567,9 @@ UI.updateLocalRole = function (isModerator) {
Toolbar.showSipCallButton(isModerator);
Toolbar.showRecordingButton(isModerator);
+ Toolbar.showSharedVideoButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
+ SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
@@ -583,7 +618,8 @@ UI.toggleSmileys = function () {
* Toggles film strip.
*/
UI.toggleFilmStrip = function () {
- FilmStrip.toggleFilmStrip();
+ var self = FilmStrip;
+ self.toggleFilmStrip.apply(self, arguments);
};
/**
@@ -671,10 +707,26 @@ UI.setVideoMuted = function (id, muted) {
}
};
+/**
+ * Adds a listener that would be notified on the given type of event.
+ *
+ * @param type the type of the event we're listening for
+ * @param listener a function that would be called when notified
+ */
UI.addListener = function (type, listener) {
eventEmitter.on(type, listener);
};
+/**
+ * Removes the given listener for the given type of event.
+ *
+ * @param type the type of the event we're listening for
+ * @param listener the listener we want to remove
+ */
+UI.removeListener = function (type, listener) {
+ eventEmitter.removeListener(type, listener);
+};
+
UI.clickOnVideo = function (videoNumber) {
var remoteVideos = $(".videocontainer:not(#mixedstream)");
if (remoteVideos.length > videoNumber) {
@@ -732,6 +784,22 @@ UI.notifyConnectionFailed = function (stropheErrorMsg) {
);
};
+
+/**
+ * Notify user that maximum users limit has been reached.
+ */
+UI.notifyMaxUsersLimitReached = function () {
+ var title = APP.translation.generateTranslationHTML(
+ "dialog.error");
+
+ var message = APP.translation.generateTranslationHTML(
+ "dialog.maxUsersLimitReached");
+
+ messageHandler.openDialog(
+ title, message, true, {}, function (e, v, m, f) { return false; }
+ );
+};
+
/**
* Notify user that he was automatically muted when joned the conference.
*/
@@ -998,26 +1066,6 @@ UI.updateAuthInfo = function (isAuthEnabled, login) {
}
};
-/**
- * Show Prezi from the user.
- * @param {string} userId user id
- * @param {string} url Prezi url
- * @param {number} slide slide to show
- */
-UI.showPrezi = function (userId, url, slide) {
- preziManager.showPrezi(userId, url, slide);
-};
-
-/**
- * Stop showing Prezi from the user.
- * @param {string} userId user id
- */
-UI.stopPrezi = function (userId) {
- if (preziManager.isSharing(userId)) {
- preziManager.removePrezi(userId);
- }
-};
-
UI.onStartMutedChanged = function (startAudioMuted, startVideoMuted) {
SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
};
@@ -1038,6 +1086,14 @@ UI.getLargeVideoID = function () {
return VideoLayout.getLargeVideoID();
};
+/**
+ * Returns the current video shown on large.
+ * Currently used by tests (torture).
+ */
+UI.getLargeVideo = function () {
+ return VideoLayout.getLargeVideo();
+};
+
/**
* Shows dialog with a link to FF extension.
*/
@@ -1054,4 +1110,36 @@ UI.updateDevicesAvailability = function (id, devices) {
VideoLayout.setDeviceAvailabilityIcons(id, devices);
};
+/**
+ * Show shared video.
+ * @param {string} id the id of the sender of the command
+ * @param {string} url video url
+ * @param {string} attributes
+*/
+UI.showSharedVideo = function (id, url, attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.showSharedVideo(id, url, attributes);
+};
+
+/**
+ * Update shared video.
+ * @param {string} id the id of the sender of the command
+ * @param {string} url video url
+ * @param {string} attributes
+ */
+UI.updateSharedVideo = function (id, url, attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.updateSharedVideo(id, url, attributes);
+};
+
+/**
+ * Stop showing shared video.
+ * @param {string} id the id of the sender of the command
+ * @param {string} attributes
+ */
+UI.stopSharedVideo = function (id, attributes) {
+ if (sharedVideoManager)
+ sharedVideoManager.stopSharedVideo(id);
+};
+
module.exports = UI;
diff --git a/modules/UI/audio_levels/AudioLevels.js b/modules/UI/audio_levels/AudioLevels.js
index 7e1bc68dd..68568710d 100644
--- a/modules/UI/audio_levels/AudioLevels.js
+++ b/modules/UI/audio_levels/AudioLevels.js
@@ -8,13 +8,16 @@ const LOCAL_LEVEL = 'local';
let ASDrawContext = null;
let audioLevelCanvasCache = {};
+let dominantSpeakerAudioElement = null;
-function initDominantSpeakerAudioLevels() {
- let ASRadius = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE / 2;
- let ASCenter = (interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
+function initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
+ let ASRadius = dominantSpeakerAvatarSize / 2;
+ let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
// Draw a circle.
+ ASDrawContext.beginPath();
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
+ ASDrawContext.closePath();
// Add a shadow around the circle
ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
@@ -90,14 +93,14 @@ function getShadowLevel (audioLevel) {
let shadowLevel = 0;
if (audioLevel <= 0.3) {
- shadowLevel
- = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
+ shadowLevel = Math.round(
+ interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
} else if (audioLevel <= 0.6) {
- shadowLevel
- = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
+ shadowLevel = Math.round(
+ interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
} else {
- shadowLevel
- = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
+ shadowLevel = Math.round(
+ interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
@@ -124,8 +127,18 @@ function getVideoSpanId(id) {
const AudioLevels = {
init () {
- ASDrawContext = $('#dominantSpeakerAudioLevel')[0].getContext('2d');
- initDominantSpeakerAudioLevels();
+ dominantSpeakerAudioElement = $('#dominantSpeakerAudioLevel')[0];
+ ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
+
+ let parentContainer = $("#dominantSpeaker");
+ let dominantSpeakerWidth = parentContainer.width();
+ let dominantSpeakerHeight = parentContainer.height();
+
+ dominantSpeakerAudioElement.width = dominantSpeakerWidth;
+ dominantSpeakerAudioElement.height = dominantSpeakerHeight;
+
+ let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
+ initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
},
/**
@@ -155,8 +168,10 @@ const AudioLevels = {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
- audioLevelCanvas.style.bottom = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
- audioLevelCanvas.style.left = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
+ audioLevelCanvas.style.bottom
+ = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
+ audioLevelCanvas.style.left
+ = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
videoSpan.appendChild(audioLevelCanvas);
@@ -213,7 +228,10 @@ const AudioLevels = {
return;
}
- ASDrawContext.clearRect(0, 0, 300, 300);
+ ASDrawContext.clearRect(0, 0,
+ dominantSpeakerAudioElement.width,
+ dominantSpeakerAudioElement.height);
+
if (!audioLevel) {
return;
}
diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js
index 31b489d43..ac01d21f5 100644
--- a/modules/UI/avatar/Avatar.js
+++ b/modules/UI/avatar/Avatar.js
@@ -48,14 +48,21 @@ export default {
}
avatarId = MD5.hexdigest(avatarId.trim().toLowerCase());
- // Default to using gravatar.
- let urlPref = 'https://www.gravatar.com/avatar/';
- let urlSuf = "?d=wavatar&size=100";
- if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
+ let urlPref = null;
+ let urlSuf = null;
+ if (!random) {
+ urlPref = 'https://www.gravatar.com/avatar/';
+ urlSuf = "?d=wavatar&size=200";
+ }
+ else if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
}
+ else {
+ urlPref = 'https://robohash.org/';
+ urlSuf = ".png?size=200x200";
+ }
return urlPref + avatarId + urlSuf;
}
diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js
index f5f58f921..58315c9a9 100644
--- a/modules/UI/etherpad/Etherpad.js
+++ b/modules/UI/etherpad/Etherpad.js
@@ -3,6 +3,7 @@
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
+import UIEvents from "../../../service/UI/UIEvents";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import FilmStrip from '../videolayout/FilmStrip';
@@ -52,12 +53,13 @@ const DEFAULT_WIDTH = 640;
*/
const DEFAULT_HEIGHT = 480;
-const EtherpadContainerType = "etherpad";
+const ETHERPAD_CONTAINER_TYPE = "etherpad";
/**
* Container for Etherpad iframe.
*/
class Etherpad extends LargeContainer {
+
constructor (domain, name) {
super();
@@ -110,9 +112,11 @@ class Etherpad extends LargeContainer {
show () {
const $iframe = $(this.iframe);
const $container = $(this.container);
+ let self = this;
return new Promise(resolve => {
$iframe.fadeIn(300, function () {
+ self.bodyBackground = document.body.style.background;
document.body.style.background = '#eeeeee';
$iframe.css({visibility: 'visible'});
$container.css({zIndex: 2});
@@ -124,6 +128,7 @@ class Etherpad extends LargeContainer {
hide () {
const $iframe = $(this.iframe);
const $container = $(this.container);
+ document.body.style.background = this.bodyBackground;
return new Promise(resolve => {
$iframe.fadeOut(300, function () {
@@ -133,19 +138,27 @@ class Etherpad extends LargeContainer {
});
});
}
+
+ /**
+ * @return {boolean} do not switch on dominant speaker event if on stage.
+ */
+ stayOnStage () {
+ return true;
+ }
}
/**
* Manager of the Etherpad frame.
*/
export default class EtherpadManager {
- constructor (domain, name) {
+ constructor (domain, name, eventEmitter) {
if (!domain || !name) {
throw new Error("missing domain or name");
}
this.domain = domain;
this.name = name;
+ this.eventEmitter = eventEmitter;
this.etherpad = null;
}
@@ -153,13 +166,17 @@ export default class EtherpadManager {
return !!this.etherpad;
}
+ isVisible() {
+ return VideoLayout.isLargeContainerTypeVisible(ETHERPAD_CONTAINER_TYPE);
+ }
+
/**
* Create new Etherpad frame.
*/
openEtherpad () {
this.etherpad = new Etherpad(this.domain, this.name);
VideoLayout.addLargeVideoContainer(
- EtherpadContainerType,
+ ETHERPAD_CONTAINER_TYPE,
this.etherpad
);
}
@@ -173,10 +190,12 @@ export default class EtherpadManager {
this.openEtherpad();
}
- let isVisible = VideoLayout.isLargeContainerTypeVisible(
- EtherpadContainerType
- );
+ let isVisible = this.isVisible();
- VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible);
+ VideoLayout.showLargeVideoContainer(
+ ETHERPAD_CONTAINER_TYPE, !isVisible);
+
+ this.eventEmitter
+ .emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
}
}
diff --git a/modules/UI/prezi/Prezi.js b/modules/UI/prezi/Prezi.js
deleted file mode 100644
index da2b41cb5..000000000
--- a/modules/UI/prezi/Prezi.js
+++ /dev/null
@@ -1,448 +0,0 @@
-/* global $, APP */
-/* jshint -W101 */
-
-import VideoLayout from "../videolayout/VideoLayout";
-import LargeContainer from '../videolayout/LargeContainer';
-import PreziPlayer from './PreziPlayer';
-import UIUtil from '../util/UIUtil';
-import UIEvents from '../../../service/UI/UIEvents';
-import messageHandler from '../util/MessageHandler';
-import ToolbarToggler from "../toolbars/ToolbarToggler";
-import SidePanelToggler from "../side_pannels/SidePanelToggler";
-import FilmStrip from '../videolayout/FilmStrip';
-
-/**
- * Example of Prezi link.
- */
-const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi";
-const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i;
-/**
- * Default aspect ratio for Prezi frame.
- */
-const aspectRatio = 16.0 / 9.0;
-
-/**
- * Default Prezi frame width.
- */
-const DEFAULT_WIDTH = 640;
-/**
- * Default Prezi frame height.
- */
-const DEFAULT_HEIGHT = 480;
-
-/**
- * Indicates if the given string is an alphanumeric string.
- * Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
- * purpose of checking URIs.
- * @param {string} unsafeText string to check
- * @returns {boolean}
- */
-function isAlphanumeric(unsafeText) {
- return alphanumRegex.test(unsafeText);
-}
-
-/**
- * Returns the presentation id from the given url.
- * @param {string} url Prezi link
- * @returns {string} presentation id
- */
-function getPresentationId (url) {
- let presId = url.substring(url.indexOf("prezi.com/") + 10);
- return presId.substring(0, presId.indexOf('/'));
-}
-
-/**
- * Checks if given string is Prezi url.
- * @param {string} url string to check.
- * @returns {boolean}
- */
-function isPreziLink(url) {
- if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
- return false;
- }
-
- let presId = url.substring(url.indexOf("prezi.com/") + 10);
- if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
- return false;
- }
-
- return true;
-}
-
-/**
- * Notify user that other user if already sharing Prezi.
- */
-function notifyOtherIsSharingPrezi() {
- messageHandler.openMessageDialog(
- "dialog.sharePreziTitle",
- "dialog.sharePreziMsg"
- );
-}
-
-/**
- * Ask user if he want to close Prezi he's sharing.
- */
-function proposeToClosePrezi() {
- return new Promise(function (resolve, reject) {
- messageHandler.openTwoButtonDialog(
- "dialog.removePreziTitle",
- null,
- "dialog.removePreziMsg",
- null,
- false,
- "dialog.Remove",
- function(e,v,m,f) {
- if (v) {
- resolve();
- } else {
- reject();
- }
- }
- );
-
- });
-}
-
-/**
- * Ask user for Prezi url to share with others.
- * Dialog validates client input to allow only Prezi urls.
- */
-function requestPreziLink() {
- const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle");
- const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
- const shareButton = APP.translation.generateTranslationHTML("dialog.Share");
- const backButton = APP.translation.generateTranslationHTML("dialog.Back");
- const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError");
- const i18nOptions = {url: defaultPreziLink};
- const defaultUrl = APP.translation.translateString(
- "defaultPreziLink", i18nOptions
- );
-
- return new Promise(function (resolve, reject) {
- let dialog = messageHandler.openDialogWithStates({
- state0: {
- html: `
-