Merge remote-tracking branch 'upstream/master'
Conflicts (resolved manually): css/videolayout_default.css
This commit is contained in:
commit
6a162b6e52
1
Makefile
1
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
152
conference.js
152
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -70,4 +70,5 @@ var config = {
|
|||
'During that time service will not be available. ' +
|
||||
'Apologise for inconvenience.',*/
|
||||
disableThirdPartyRequests: false,
|
||||
minHDHeight: 540
|
||||
};
|
||||
|
|
|
@ -95,8 +95,6 @@
|
|||
#unreadMessages {
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
left: 46%;
|
||||
top: 27%
|
||||
}
|
||||
|
||||
#bottomUnreadMessages {
|
||||
|
|
|
@ -63,9 +63,6 @@
|
|||
.icon-exit-full-screen:before {
|
||||
content: "\e60e";
|
||||
}
|
||||
.icon-prezi:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
|
54
css/main.css
54
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <object/> 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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
11
index.html
11
index.html
|
@ -144,8 +144,8 @@
|
|||
<a class="button icon-chat" id="toolbar_button_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="bottom" data-i18n="[content]toolbar.chat" content="Open / close chat">
|
||||
<span id="unreadMessages"></span>
|
||||
</a>
|
||||
<a class="button icon-prezi" id="toolbar_button_prezi" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.prezi" content="Share Prezi"></a>
|
||||
<a class="button icon-share-doc" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared document" data-i18n="[content]toolbar.etherpad"></a>
|
||||
<a class="button fa fa-share-alt-square" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared Video" data-i18n="[content]toolbar.sharedvideo" style="display: none"></a>
|
||||
<a class="button icon-share-desktop" id="toolbar_button_desktopsharing" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="toggleDesktopSharingPopover" content="Share screen" data-i18n="[content]toolbar.sharescreen" style="display: none"></a>
|
||||
<a class="button icon-full-screen" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="bottom" content="Enter / Exit Full Screen" data-i18n="[content]toolbar.fullscreen"></a>
|
||||
<a class="button icon-telephone" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="bottom" content="Call SIP number" data-i18n="[content]toolbar.sip" style="display: none"></a>
|
||||
|
@ -156,11 +156,11 @@
|
|||
</div>
|
||||
<div id="subject"></div>
|
||||
</div>
|
||||
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||
<div id="videospace">
|
||||
|
||||
<div id="largeVideoContainer" class="videocontainer">
|
||||
<div id="presentation"></div>
|
||||
<div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
|
||||
<div id="etherpad"></div>
|
||||
<a target="_new"><div class="watermark leftwatermark"></div></a>
|
||||
<a target="_new"><div class="watermark rightwatermark"></div></a>
|
||||
|
@ -175,6 +175,7 @@
|
|||
<video id="largeVideo" muted="true" autoplay></video>
|
||||
</div>
|
||||
<span id="videoConnectionMessage"></span>
|
||||
<span id="videoResolutionLabel">HD</span>
|
||||
</div>
|
||||
|
||||
<div id="remoteVideos">
|
||||
|
@ -262,6 +263,12 @@
|
|||
<select id="selectMic"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="followMeOptions">
|
||||
<label class = "followMeLabel">
|
||||
<input type="checkbox" id="followMeCheckBox">
|
||||
<span data-i18n="settings.followMe"></span>
|
||||
</label>
|
||||
</div>
|
||||
<a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
|
||||
</div>
|
||||
<div class="feedbackButton">
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 <b>__room__ </b> 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",
|
||||
|
|
|
@ -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;
|
156
modules/UI/UI.js
156
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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: `
|
||||
<h2>${title}</h2>
|
||||
<input name="preziUrl" type="text"
|
||||
data-i18n="[placeholder]defaultPreziLink"
|
||||
data-i18n-options="${JSON.stringify(i18nOptions)}"
|
||||
placeholder="${defaultUrl}" autofocus>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: shareButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (!v) {
|
||||
reject('cancelled');
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let preziUrl = f.preziUrl;
|
||||
if (!preziUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl));
|
||||
|
||||
if (!isPreziLink(urlValue)) {
|
||||
dialog.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve(urlValue);
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
html: `<h2>${title}</h2> ${linkError}`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: backButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 0) {
|
||||
reject();
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.goToState('state0');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export const PreziContainerType = "prezi";
|
||||
|
||||
/**
|
||||
* Container for Prezi iframe.
|
||||
*/
|
||||
class PreziContainer extends LargeContainer {
|
||||
|
||||
constructor ({preziId, isMy, slide, onSlideChanged}) {
|
||||
super();
|
||||
this.reloadBtn = $('#reloadPresentation');
|
||||
|
||||
let preziPlayer = new PreziPlayer(
|
||||
'presentation', {
|
||||
preziId,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
controls: isMy,
|
||||
debug: true
|
||||
}
|
||||
);
|
||||
this.preziPlayer = preziPlayer;
|
||||
this.$iframe = $(preziPlayer.iframe);
|
||||
|
||||
this.$iframe.attr('id', preziId);
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) {
|
||||
console.log("prezi status", value);
|
||||
if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) {
|
||||
preziPlayer.flyToStep(slide);
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) {
|
||||
console.log("event value", value);
|
||||
onSlideChanged(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Prezi slide.
|
||||
* @param {number} slide slide to show
|
||||
*/
|
||||
goToSlide (slide) {
|
||||
if (this.preziPlayer.getCurrentStep() === slide) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preziPlayer.flyToStep(slide);
|
||||
|
||||
let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps();
|
||||
if (!animationStepsArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) {
|
||||
this.preziPlayer.flyToStep(slide, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide "reload presentation" button.
|
||||
* @param {boolean} show
|
||||
*/
|
||||
showReloadBtn (show) {
|
||||
this.reloadBtn.css('display', show ? 'inline-block' : 'none');
|
||||
}
|
||||
|
||||
show () {
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeIn(300, () => {
|
||||
this.$iframe.css({opacity: 1});
|
||||
ToolbarToggler.dockToolbar(true);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hide () {
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeOut(300, () => {
|
||||
this.$iframe.css({opacity: 0});
|
||||
this.showReloadBtn(false);
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onHoverIn () {
|
||||
let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width();
|
||||
|
||||
this.showReloadBtn(true);
|
||||
this.reloadBtn.css('right', rightOffset);
|
||||
}
|
||||
|
||||
onHoverOut (event) {
|
||||
let e = event.toElement || event.relatedTarget;
|
||||
|
||||
if (e && e.id != 'reloadPresentation' && e.id != 'header') {
|
||||
this.showReloadBtn(false);
|
||||
}
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight) {
|
||||
let height = containerHeight - FilmStrip.getFilmStripHeight();
|
||||
|
||||
let width = containerWidth;
|
||||
|
||||
if (height < width / aspectRatio) {
|
||||
width = Math.floor(height * aspectRatio);
|
||||
}
|
||||
|
||||
this.$iframe.width(width).height(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Prezi frame.
|
||||
*/
|
||||
close () {
|
||||
this.showReloadBtn(false);
|
||||
this.preziPlayer.destroy();
|
||||
this.$iframe.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager of Prezi frames.
|
||||
*/
|
||||
export default class PreziManager {
|
||||
constructor (emitter) {
|
||||
this.emitter = emitter;
|
||||
|
||||
this.userId = null;
|
||||
this.url = null;
|
||||
this.prezi = null;
|
||||
|
||||
$("#reloadPresentationLink").click(this.reloadPresentation.bind(this));
|
||||
}
|
||||
|
||||
get isPresenting () {
|
||||
return !!this.userId;
|
||||
}
|
||||
|
||||
get isMyPrezi () {
|
||||
return this.userId === APP.conference.localId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is currently sharing.
|
||||
* @param {string} id user id to check for
|
||||
*/
|
||||
isSharing (id) {
|
||||
return this.userId === id;
|
||||
}
|
||||
|
||||
handlePreziButtonClicked () {
|
||||
if (!this.isPresenting) {
|
||||
requestPreziLink().then(
|
||||
url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0),
|
||||
err => console.error('PREZI CANCELED', err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMyPrezi) {
|
||||
proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI));
|
||||
} else {
|
||||
notifyOtherIsSharingPrezi();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload current Prezi frame.
|
||||
*/
|
||||
reloadPresentation () {
|
||||
if (!this.prezi) {
|
||||
return;
|
||||
}
|
||||
let iframe = this.prezi.$iframe[0];
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Prezi. Create new Prezi if there is no Prezi yet.
|
||||
* @param {string} id owner id
|
||||
* @param {string} url Prezi url
|
||||
* @param {number} slide slide to show
|
||||
*/
|
||||
showPrezi (id, url, slide) {
|
||||
if (!this.isPresenting) {
|
||||
this.createPrezi(id, url, slide);
|
||||
}
|
||||
|
||||
if (this.userId === id && this.url === url) {
|
||||
this.prezi.goToSlide(slide);
|
||||
} else {
|
||||
console.error(this.userId, id);
|
||||
console.error(this.url, url);
|
||||
throw new Error("unexpected presentation change");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Prezi frame..
|
||||
* @param {string} id owner id
|
||||
* @param {string} url Prezi url
|
||||
* @param {number} slide slide to show
|
||||
*/
|
||||
createPrezi (id, url, slide) {
|
||||
console.log("presentation added", url);
|
||||
|
||||
this.userId = id;
|
||||
this.url = url;
|
||||
|
||||
let preziId = getPresentationId(url);
|
||||
let elementId = `participant_${id}_${preziId}`;
|
||||
|
||||
this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId));
|
||||
VideoLayout.resizeThumbnails();
|
||||
this.$thumb.css({
|
||||
'background-image': 'url(../images/avatarprezi.png)'
|
||||
}).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true));
|
||||
|
||||
this.prezi = new PreziContainer({
|
||||
preziId,
|
||||
isMy: this.isMyPrezi,
|
||||
slide,
|
||||
onSlideChanged: newSlide => {
|
||||
if (this.isMyPrezi) {
|
||||
this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi);
|
||||
VideoLayout.showLargeVideoContainer(PreziContainerType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Prezi.
|
||||
* @param {string} id owner id
|
||||
*/
|
||||
removePrezi (id) {
|
||||
if (this.userId !== id) {
|
||||
throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`);
|
||||
}
|
||||
|
||||
this.$thumb.remove();
|
||||
this.$thumb = null;
|
||||
|
||||
// wait until Prezi is hidden, then remove it
|
||||
VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => {
|
||||
console.log("presentation removed", this.url);
|
||||
|
||||
VideoLayout.removeLargeVideoContainer(PreziContainerType);
|
||||
|
||||
this.userId = null;
|
||||
this.url = null;
|
||||
this.prezi.close();
|
||||
this.prezi = null;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,290 +0,0 @@
|
|||
/* jshint -W101 */
|
||||
|
||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
PreziPlayer.API_VERSION = 1;
|
||||
PreziPlayer.CURRENT_STEP = 'currentStep';
|
||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||
PreziPlayer.STATUS_LOADING = 'loading';
|
||||
PreziPlayer.STATUS_READY = 'ready';
|
||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||
PreziPlayer.domain = "https://prezi.com";
|
||||
PreziPlayer.path = "/player/";
|
||||
PreziPlayer.players = {};
|
||||
PreziPlayer.binded_methods = ['changesHandler'];
|
||||
|
||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||
for(var i=0; i<optionArray.length; i++) {
|
||||
var optionSet = optionArray[i];
|
||||
new PreziPlayer(optionSet.id, optionSet);
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.messageReceived = function(event){
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
/*jshint -W004 */
|
||||
function PreziPlayer(id, options) {
|
||||
/*jshint +W004 */
|
||||
var params, paramString = "", _this = this;
|
||||
if (PreziPlayer.players[id]){
|
||||
PreziPlayer.players[id].destroy();
|
||||
}
|
||||
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
|
||||
var method_name = PreziPlayer.binded_methods[i];
|
||||
_this[method_name] = __bind(_this[method_name], _this);
|
||||
}
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
this.values = {'status': PreziPlayer.STATUS_LOADING};
|
||||
this.values[PreziPlayer.CURRENT_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_OBJECT] = null;
|
||||
this.callbacks = [];
|
||||
this.id = id;
|
||||
this.embedTo = document.getElementById(id);
|
||||
if (!this.embedTo) {
|
||||
throw "The element id is not available.";
|
||||
}
|
||||
this.iframe = document.createElement('iframe');
|
||||
params = [
|
||||
{ name: 'oid', value: options.preziId },
|
||||
{ name: 'explorable', value: options.explorable ? 1 : 0 },
|
||||
{ name: 'controls', value: options.controls ? 1 : 0 }
|
||||
];
|
||||
for (i=0; i<params.length; i++) {
|
||||
var param = params[i];
|
||||
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
|
||||
}
|
||||
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
|
||||
this.iframe.frameBorder = 0;
|
||||
this.iframe.scrolling = "no";
|
||||
this.iframe.width = options.width || 640;
|
||||
this.iframe.height = options.height || 480;
|
||||
this.embedTo.innerHTML = '';
|
||||
// JITSI: IN CASE SOMETHING GOES WRONG.
|
||||
try {
|
||||
this.embedTo.appendChild(this.iframe);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("CATCH ERROR");
|
||||
}
|
||||
|
||||
// JITSI: Increase interval from 200 to 500, which fixes prezi
|
||||
// crashes for us.
|
||||
this.initPollInterval = setInterval(function(){
|
||||
_this.sendMessage({'action': 'init'});
|
||||
}, 500);
|
||||
PreziPlayer.players[id] = this;
|
||||
}
|
||||
|
||||
PreziPlayer.prototype.changesHandler = function(message) {
|
||||
var key, value, j, item;
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
for (key in message.data) {
|
||||
if (message.data.hasOwnProperty(key)){
|
||||
value = message.data[key];
|
||||
this.values[key] = value;
|
||||
for (j=0; j<this.callbacks.length; j++) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === key + "Change"){
|
||||
item.callback({type: item.event, value: value});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.destroy = function() {
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
this.embedTo.innerHTML = '';
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.sendMessage = function(message) {
|
||||
if (this.options.debug === true) {
|
||||
if (console && console.log) console.log('sent', message);
|
||||
}
|
||||
message.version = PreziPlayer.API_VERSION;
|
||||
message.id = this.id;
|
||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToNextStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToNextStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToPrevStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||
var obj = this;
|
||||
// check animation_step
|
||||
if (animation_step > 0 &&
|
||||
obj.values.animationCountOnSteps &&
|
||||
obj.values.animationCountOnSteps[step] <= animation_step) {
|
||||
animation_step = obj.values.animationCountOnSteps[step];
|
||||
}
|
||||
// jump to animation steps by calling flyToNextStep()
|
||||
function doAnimationSteps() {
|
||||
if (obj.values.isMoving) {
|
||||
setTimeout(doAnimationSteps, 100); // wait until the flight ends
|
||||
return;
|
||||
}
|
||||
while (animation_step-- > 0) {
|
||||
obj.flyToNextStep(); // do the animation steps
|
||||
}
|
||||
}
|
||||
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
|
||||
// jump to the step
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToStep', step]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToObject', objectId]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['startAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.stop = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['stopAutoPlay']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['pauseAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentStep = function() {
|
||||
return this.values.currentStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||
return this.values.currentAnimationStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentObject = function() {
|
||||
return this.values.currentObject;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStatus = function() {
|
||||
return this.values.status;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.isPlaying = function() {
|
||||
return this.values.isAutoPlaying;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStepCount = function() {
|
||||
return this.values.stepCount;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||
return this.values.animationCountOnSteps;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getTitle = function() {
|
||||
return this.values.title;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||
for (var parameter in dims) {
|
||||
this.iframe[parameter] = dims[parameter];
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getDimensions = function() {
|
||||
return {
|
||||
width: parseInt(this.iframe.width, 10),
|
||||
height: parseInt(this.iframe.height, 10)
|
||||
};
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.on = function(event, callback) {
|
||||
this.callbacks.push({
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.off = function(event, callback) {
|
||||
var j, item;
|
||||
if (event === undefined) {
|
||||
this.callbacks = [];
|
||||
}
|
||||
j = this.callbacks.length;
|
||||
while (j--) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === event && (callback === undefined || item.callback === callback)){
|
||||
this.callbacks.splice(j, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||
}
|
||||
|
||||
window.PreziPlayer = PreziPlayer;
|
||||
|
||||
export default PreziPlayer;
|
|
@ -0,0 +1,582 @@
|
|||
/* global $, APP, YT, onPlayerReady, onPlayerStateChange, onPlayerError */
|
||||
|
||||
import messageHandler from '../util/MessageHandler';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import VideoLayout from "../videolayout/VideoLayout";
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
import FilmStrip from '../videolayout/FilmStrip';
|
||||
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||
|
||||
export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
|
||||
|
||||
/**
|
||||
* Example shared video link.
|
||||
* @type {string}
|
||||
*/
|
||||
const defaultSharedVideoLink = "https://www.youtube.com/watch?v=xNXN7CZk8X0";
|
||||
const updateInterval = 5000; // milliseconds
|
||||
/**
|
||||
* Manager of shared video.
|
||||
*/
|
||||
export default class SharedVideoManager {
|
||||
constructor (emitter) {
|
||||
this.emitter = emitter;
|
||||
this.isSharedVideoShown = false;
|
||||
this.isPlayerAPILoaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts shared video by asking user for url, or if its already working
|
||||
* asks whether the user wants to stop sharing the video.
|
||||
*/
|
||||
toggleSharedVideo () {
|
||||
if(!this.isSharedVideoShown) {
|
||||
requestVideoLink().then(
|
||||
url => this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, url, 'start'),
|
||||
err => console.error('SHARED VIDEO CANCELED', err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(APP.conference.isLocalId(this.from)) {
|
||||
showStopVideoPropmpt().then(() =>
|
||||
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop'));
|
||||
} else {
|
||||
messageHandler.openMessageDialog(
|
||||
"dialog.shareVideoTitle",
|
||||
"dialog.alreadySharedVideoMsg"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the player component and starts the checking function
|
||||
* that will be sending updates, if we are the one shared the video
|
||||
* @param id the id of the sender of the command
|
||||
* @param url the video url
|
||||
* @param attributes
|
||||
*/
|
||||
showSharedVideo (id, url, attributes) {
|
||||
if (this.isSharedVideoShown)
|
||||
return;
|
||||
|
||||
// the video url
|
||||
this.url = url;
|
||||
|
||||
// the owner of the video
|
||||
this.from = id;
|
||||
|
||||
// This code loads the IFrame Player API code asynchronously.
|
||||
var tag = document.createElement('script');
|
||||
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
// sometimes we receive errors like player not defined
|
||||
// or player.pauseVideo is not a function
|
||||
// we need to operate with player after start playing
|
||||
// self.player will be defined once it start playing
|
||||
// and will process any initial attributes if any
|
||||
this.initialAttributes = null;
|
||||
|
||||
var self = this;
|
||||
if(self.isPlayerAPILoaded)
|
||||
window.onYouTubeIframeAPIReady();
|
||||
else
|
||||
window.onYouTubeIframeAPIReady = function() {
|
||||
self.isPlayerAPILoaded = true;
|
||||
let showControls = APP.conference.isLocalId(self.from) ? 1 : 0;
|
||||
new YT.Player('sharedVideoIFrame', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
videoId: self.url,
|
||||
playerVars: {
|
||||
'origin': location.origin,
|
||||
'fs': '0',
|
||||
'autoplay': 0,
|
||||
'controls': showControls,
|
||||
'rel' : 0
|
||||
},
|
||||
events: {
|
||||
'onReady': onPlayerReady,
|
||||
'onStateChange': onPlayerStateChange,
|
||||
'onError': onPlayerError
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.onPlayerStateChange = function(event) {
|
||||
if (event.data == YT.PlayerState.PLAYING) {
|
||||
self.playerPaused = false;
|
||||
|
||||
self.player = event.target;
|
||||
|
||||
if(self.initialAttributes)
|
||||
{
|
||||
self.processAttributes(
|
||||
self.player, self.initialAttributes, self.playerPaused);
|
||||
self.initialAttributes = null;
|
||||
}
|
||||
|
||||
self.updateCheck();
|
||||
} else if (event.data == YT.PlayerState.PAUSED) {
|
||||
self.playerPaused = true;
|
||||
self.updateCheck(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.onPlayerReady = function(event) {
|
||||
let player = event.target;
|
||||
// do not relay on autoplay as it is not sending all of the events
|
||||
// in onPlayerStateChange
|
||||
player.playVideo();
|
||||
|
||||
let thumb = new SharedVideoThumb(self.url);
|
||||
thumb.setDisplayName(player.getVideoData().title);
|
||||
VideoLayout.addParticipantContainer(self.url, thumb);
|
||||
|
||||
let iframe = player.getIframe();
|
||||
self.sharedVideo = new SharedVideoContainer(
|
||||
{url, iframe, player});
|
||||
|
||||
VideoLayout.addLargeVideoContainer(
|
||||
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
|
||||
VideoLayout.handleVideoThumbClicked(self.url);
|
||||
|
||||
self.isSharedVideoShown = true;
|
||||
|
||||
// If we are sending the command and we are starting the player
|
||||
// we need to continuously send the player current time position
|
||||
if(APP.conference.isLocalId(self.from)) {
|
||||
self.intervalId = setInterval(
|
||||
self.updateCheck.bind(self),
|
||||
updateInterval);
|
||||
}
|
||||
|
||||
if(self.player)
|
||||
self.processAttributes(
|
||||
self.player, attributes, self.playerPaused);
|
||||
else {
|
||||
self.initialAttributes = attributes;
|
||||
}
|
||||
};
|
||||
|
||||
window.onPlayerError = function(event) {
|
||||
console.error("Error in the player:" + event.data);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process attributes, whether player needs to be paused or seek.
|
||||
* @param player the player to operate over
|
||||
* @param attributes the attributes with the player state we want
|
||||
* @param playerPaused current saved state for the player
|
||||
*/
|
||||
processAttributes (player, attributes, playerPaused)
|
||||
{
|
||||
if(!attributes)
|
||||
return;
|
||||
|
||||
if (attributes.state == 'playing') {
|
||||
|
||||
this.processTime(player, attributes);
|
||||
|
||||
// lets check the volume
|
||||
if (attributes.volume !== undefined &&
|
||||
player.getVolume() != attributes.volume) {
|
||||
player.setVolume(attributes.volume);
|
||||
console.info("Player change of volume:" + attributes.volume);
|
||||
}
|
||||
|
||||
if(playerPaused)
|
||||
player.playVideo();
|
||||
|
||||
} else if (attributes.state == 'pause') {
|
||||
// if its not paused, pause it
|
||||
player.pauseVideo();
|
||||
|
||||
this.processTime(player, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for time in attributes and if needed seek in current player
|
||||
* @param player the player to operate over
|
||||
* @param attributes the attributes with the player state we want
|
||||
*/
|
||||
processTime (player, attributes)
|
||||
{
|
||||
// check received time and current time
|
||||
let currentPosition = player.getCurrentTime();
|
||||
let diff = Math.abs(attributes.time - currentPosition);
|
||||
|
||||
// if we drift more than the interval for checking
|
||||
// sync, the interval is in milliseconds
|
||||
if(diff > updateInterval/1000) {
|
||||
console.info("Player seekTo:", attributes.time,
|
||||
" current time is:", currentPosition, " diff:", diff);
|
||||
player.seekTo(attributes.time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks current state of the player and fire an event with the values.
|
||||
*/
|
||||
updateCheck(sendPauseEvent)
|
||||
{
|
||||
// ignore update checks if we are not the owner of the video
|
||||
// or there is still no player defined
|
||||
if(!APP.conference.isLocalId(this.from) || !this.player)
|
||||
return;
|
||||
|
||||
let state = this.player.getPlayerState();
|
||||
// if its paused and haven't been pause - send paused
|
||||
if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
|
||||
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
|
||||
this.url, 'pause', this.player.getCurrentTime());
|
||||
}
|
||||
// if its playing and it was paused - send update with time
|
||||
// if its playing and was playing just send update with time
|
||||
else if (state === YT.PlayerState.PLAYING) {
|
||||
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
|
||||
this.url, 'playing',
|
||||
this.player.getCurrentTime(),
|
||||
this.player.isMuted() ? 0 : this.player.getVolume());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates video, if its not playing and needs starting or
|
||||
* if its playing and needs to be paysed
|
||||
* @param id the id of the sender of the command
|
||||
* @param url the video url
|
||||
* @param attributes
|
||||
*/
|
||||
updateSharedVideo (id, url, attributes) {
|
||||
// if we are sending the event ignore
|
||||
if(APP.conference.isLocalId(this.from)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.isSharedVideoShown) {
|
||||
this.showSharedVideo(id, url, attributes);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.player)
|
||||
this.initialAttributes = attributes;
|
||||
else {
|
||||
this.processAttributes(this.player, attributes, this.playerPaused);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop shared video if it is currently showed. If the user started the
|
||||
* shared video is the one in the id (called when user
|
||||
* left and we want to remove video if the user sharing it left).
|
||||
* @param id the id of the sender of the command
|
||||
*/
|
||||
stopSharedVideo (id) {
|
||||
if (!this.isSharedVideoShown)
|
||||
return;
|
||||
|
||||
if(this.from !== id)
|
||||
return;
|
||||
|
||||
if(this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
VideoLayout.removeParticipantContainer(this.url);
|
||||
|
||||
VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
|
||||
.then(() => {
|
||||
VideoLayout.removeLargeVideoContainer(
|
||||
SHARED_VIDEO_CONTAINER_TYPE);
|
||||
|
||||
this.player.destroy();
|
||||
this.player = null;
|
||||
});
|
||||
|
||||
this.url = null;
|
||||
this.isSharedVideoShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for shared video iframe.
|
||||
*/
|
||||
class SharedVideoContainer extends LargeContainer {
|
||||
|
||||
constructor ({url, iframe, player}) {
|
||||
super();
|
||||
|
||||
this.$iframe = $(iframe);
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
get $video () {
|
||||
return this.$iframe;
|
||||
}
|
||||
|
||||
show () {
|
||||
let self = this;
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeIn(300, () => {
|
||||
self.bodyBackground = document.body.style.background;
|
||||
document.body.style.background = 'black';
|
||||
this.$iframe.css({opacity: 1});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hide () {
|
||||
let self = this;
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeOut(300, () => {
|
||||
document.body.style.background = self.bodyBackground;
|
||||
this.$iframe.css({opacity: 0});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onHoverIn () {
|
||||
ToolbarToggler.showToolbar();
|
||||
}
|
||||
|
||||
get id () {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight) {
|
||||
let height = containerHeight - FilmStrip.getFilmStripHeight();
|
||||
|
||||
let width = containerWidth;
|
||||
|
||||
this.$iframe.width(width).height(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} do not switch on dominant speaker event if on stage.
|
||||
*/
|
||||
stayOnStage () {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function SharedVideoThumb (url)
|
||||
{
|
||||
this.id = url;
|
||||
|
||||
this.url = url;
|
||||
this.setVideoType(SHARED_VIDEO_CONTAINER_TYPE);
|
||||
this.videoSpanId = "sharedVideoContainer";
|
||||
this.container = this.createContainer(this.videoSpanId);
|
||||
this.container.onclick = this.videoClick.bind(this);
|
||||
|
||||
SmallVideo.call(this, VideoLayout);
|
||||
this.isVideoMuted = true;
|
||||
}
|
||||
SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
|
||||
SharedVideoThumb.prototype.constructor = SharedVideoThumb;
|
||||
|
||||
/**
|
||||
* hide display name
|
||||
*/
|
||||
|
||||
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function () {};
|
||||
|
||||
SharedVideoThumb.prototype.avatarChanged = function () {};
|
||||
|
||||
SharedVideoThumb.prototype.createContainer = function (spanId) {
|
||||
var container = document.createElement('span');
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
// add the avatar
|
||||
var avatar = document.createElement('img');
|
||||
avatar.id = 'avatar_' + this.id;
|
||||
avatar.className = 'sharedVideoAvatar';
|
||||
avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
|
||||
container.appendChild(avatar);
|
||||
|
||||
var remotes = document.getElementById('remoteVideos');
|
||||
return remotes.appendChild(container);
|
||||
};
|
||||
|
||||
/**
|
||||
* The thumb click handler.
|
||||
*/
|
||||
SharedVideoThumb.prototype.videoClick = function () {
|
||||
VideoLayout.handleVideoThumbClicked(this.url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes RemoteVideo from the page.
|
||||
*/
|
||||
SharedVideoThumb.prototype.remove = function () {
|
||||
console.log("Remove shared video thumb", this.id);
|
||||
|
||||
// Make sure that the large video is updated if are removing its
|
||||
// corresponding small video.
|
||||
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
||||
|
||||
// Remove whole container
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the display name for the thumb.
|
||||
*/
|
||||
SharedVideoThumb.prototype.setDisplayName = function(displayName) {
|
||||
if (!this.container) {
|
||||
console.warn( "Unable to set displayName - " + this.videoSpanId +
|
||||
" does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
|
||||
|
||||
// If we already have a display name for this video.
|
||||
if (nameSpan.length > 0) {
|
||||
if (displayName && displayName.length > 0) {
|
||||
$('#' + this.videoSpanId + '_name').text(displayName);
|
||||
}
|
||||
} else {
|
||||
nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'displayname';
|
||||
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
|
||||
|
||||
if (displayName && displayName.length > 0)
|
||||
$(nameSpan).text(displayName);
|
||||
nameSpan.id = this.videoSpanId + '_name';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if given string is youtube url.
|
||||
* @param {string} url string to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function getYoutubeLink(url) {
|
||||
let p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;//jshint ignore:line
|
||||
return (url.match(p)) ? RegExp.$1 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if he want to close shared video.
|
||||
*/
|
||||
function showStopVideoPropmpt() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
messageHandler.openTwoButtonDialog(
|
||||
"dialog.removeSharedVideoTitle",
|
||||
null,
|
||||
"dialog.removeSharedVideoMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if (v) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for shared video url to share with others.
|
||||
* Dialog validates client input to allow only youtube urls.
|
||||
*/
|
||||
function requestVideoLink() {
|
||||
let i18n = APP.translation;
|
||||
const title = i18n.generateTranslationHTML("dialog.shareVideoTitle");
|
||||
const cancelButton = i18n.generateTranslationHTML("dialog.Cancel");
|
||||
const shareButton = i18n.generateTranslationHTML("dialog.Share");
|
||||
const backButton = i18n.generateTranslationHTML("dialog.Back");
|
||||
const linkError
|
||||
= i18n.generateTranslationHTML("dialog.shareVideoLinkError");
|
||||
const i18nOptions = {url: defaultSharedVideoLink};
|
||||
const defaultUrl = i18n.translateString("defaultLink", i18nOptions);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
let dialog = messageHandler.openDialogWithStates({
|
||||
state0: {
|
||||
html: `
|
||||
<h2>${title}</h2>
|
||||
<input name="sharedVideoUrl" type="text"
|
||||
data-i18n="[placeholder]defaultLink"
|
||||
data-i18n-options="${JSON.stringify(i18nOptions)}"
|
||||
placeholder="${defaultUrl}"
|
||||
autofocus>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: shareButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (!v) {
|
||||
reject('cancelled');
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let sharedVideoUrl = f.sharedVideoUrl;
|
||||
if (!sharedVideoUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlValue = encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
|
||||
let yVideoId = getYoutubeLink(urlValue);
|
||||
if (!yVideoId) {
|
||||
dialog.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve(yVideoId);
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
html: `<h2>${title}</h2> ${linkError}`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: backButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 0) {
|
||||
reject();
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.goToState('state0');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ function toggle (object, selector, onOpenComplete,
|
|||
function resizeVideoArea(isSidePanelVisible, completeFunction) {
|
||||
VideoLayout.resizeVideoArea(!isSidePanelVisible,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
completeFunction);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ function setVisualNotification(show) {
|
|||
var leftIndent = (UIUtil.getTextWidth(chatButtonElement) -
|
||||
UIUtil.getTextWidth(unreadMsgElement)) / 2;
|
||||
var topIndent = (UIUtil.getTextHeight(chatButtonElement) -
|
||||
UIUtil.getTextHeight(unreadMsgElement)) / 2 - 3;
|
||||
UIUtil.getTextHeight(unreadMsgElement)) / 2 - 5;
|
||||
|
||||
unreadMsgElement.setAttribute(
|
||||
'style',
|
||||
|
|
|
@ -89,6 +89,14 @@ export default {
|
|||
);
|
||||
});
|
||||
|
||||
// FOLLOW ME
|
||||
$("#followMeOptions").change(function () {
|
||||
let isFollowMeEnabled = $("#followMeCheckBox").is(":checked");
|
||||
emitter.emit(
|
||||
UIEvents.FOLLOW_ME_ENABLED,
|
||||
isFollowMeEnabled
|
||||
);
|
||||
});
|
||||
|
||||
// LANGUAGES BOX
|
||||
let languagesBox = $("#languages_selectbox");
|
||||
|
@ -135,6 +143,19 @@ export default {
|
|||
$("#startVideoMuted").attr("checked", startVideoMuted);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides the follow me options in the settings dialog.
|
||||
*
|
||||
* @param {boolean} show {true} to show those options, {false} to hide them
|
||||
*/
|
||||
showFollowMeOptions (show) {
|
||||
if (show) {
|
||||
$("#followMeOptions").css("display", "block");
|
||||
} else {
|
||||
$("#followMeOptions").css("display", "none");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if settings menu is visible or not.
|
||||
* @returns {boolean}
|
||||
|
|
|
@ -122,14 +122,14 @@ const buttonHandlers = {
|
|||
AnalyticsAdapter.sendEvent('toolbar.chat.toggled');
|
||||
emitter.emit(UIEvents.TOGGLE_CHAT);
|
||||
},
|
||||
"toolbar_button_prezi": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.prezi.clicked');
|
||||
emitter.emit(UIEvents.PREZI_CLICKED);
|
||||
},
|
||||
"toolbar_button_etherpad": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
|
||||
emitter.emit(UIEvents.ETHERPAD_CLICKED);
|
||||
},
|
||||
"toolbar_button_sharedvideo": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.sharedvideo.clicked');
|
||||
emitter.emit(UIEvents.SHARED_VIDEO_CLICKED);
|
||||
},
|
||||
"toolbar_button_desktopsharing": function () {
|
||||
if (APP.conference.isSharingScreen) {
|
||||
AnalyticsAdapter.sendEvent('toolbar.screen.disabled');
|
||||
|
@ -188,7 +188,6 @@ const defaultToolbarButtons = {
|
|||
'security': '#toolbar_button_security',
|
||||
'invite': '#toolbar_button_link',
|
||||
'chat': '#toolbar_button_chat',
|
||||
'prezi': '#toolbar_button_prezi',
|
||||
'etherpad': '#toolbar_button_etherpad',
|
||||
'fullscreen': '#toolbar_button_fullScreen',
|
||||
'settings': '#toolbar_button_settings',
|
||||
|
@ -246,15 +245,6 @@ const Toolbar = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables and enables some of the buttons.
|
||||
*/
|
||||
setupButtonsFromConfig () {
|
||||
if (!UIUtil.isButtonEnabled('prezi')) {
|
||||
$("#toolbar_button_prezi").css({display: "none"});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unlocks the lock button state.
|
||||
*/
|
||||
|
@ -298,6 +288,16 @@ const Toolbar = {
|
|||
}
|
||||
},
|
||||
|
||||
// Shows or hides the 'shared video' button.
|
||||
showSharedVideoButton () {
|
||||
if (UIUtil.isButtonEnabled('sharedvideo')
|
||||
&& config.disableThirdPartyRequests !== true) {
|
||||
$('#toolbar_button_sharedvideo').css({display: "inline-block"});
|
||||
} else {
|
||||
$('#toolbar_button_sharedvideo').css({display: "none"});
|
||||
}
|
||||
},
|
||||
|
||||
// checks whether recording is enabled and whether we have params
|
||||
// to start automatically recording
|
||||
checkAutoRecord () {
|
||||
|
|
|
@ -124,9 +124,7 @@
|
|||
|
||||
isButtonEnabled: function (name) {
|
||||
var isEnabled = interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
|
||||
if (name === 'prezi') {
|
||||
return isEnabled && !config.disablePrezi;
|
||||
} else if (name === 'recording') {
|
||||
if (name === 'recording') {
|
||||
return isEnabled && config.enableRecording;
|
||||
}
|
||||
return isEnabled;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global APP, $ */
|
||||
/* global APP, $, config */
|
||||
/* jshint -W101 */
|
||||
import JitsiPopover from "../util/JitsiPopover";
|
||||
import VideoLayout from "./VideoLayout";
|
||||
|
||||
/**
|
||||
* Constructs new connection indicator.
|
||||
|
@ -14,6 +15,7 @@ function ConnectionIndicator(videoContainer, id) {
|
|||
this.bitrate = null;
|
||||
this.showMoreValue = false;
|
||||
this.resolution = null;
|
||||
this.isResolutionHD = null;
|
||||
this.transport = [];
|
||||
this.popover = null;
|
||||
this.id = id;
|
||||
|
@ -292,7 +294,6 @@ ConnectionIndicator.prototype.remove = function() {
|
|||
*/
|
||||
ConnectionIndicator.prototype.updateConnectionQuality =
|
||||
function (percent, object) {
|
||||
|
||||
if (percent === null) {
|
||||
this.connectionIndicatorContainer.style.display = "none";
|
||||
this.popover.forceHide();
|
||||
|
@ -316,6 +317,10 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
|||
ConnectionIndicator.connectionQualityValues[quality];
|
||||
}
|
||||
}
|
||||
if (object.isResolutionHD) {
|
||||
this.isResolutionHD = object.isResolutionHD;
|
||||
}
|
||||
this.updateResolutionIndicator();
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
|
@ -325,6 +330,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
|||
*/
|
||||
ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
||||
this.resolution = resolution;
|
||||
this.updateResolutionIndicator();
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
|
@ -354,4 +360,29 @@ ConnectionIndicator.prototype.hideIndicator = function () {
|
|||
this.popover.forceHide();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the resolution indicator.
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateResolutionIndicator = function () {
|
||||
|
||||
if (this.id !== null
|
||||
&& VideoLayout.isCurrentlyOnLarge(this.id)) {
|
||||
|
||||
let showResolutionLabel = false;
|
||||
|
||||
if (this.isResolutionHD !== null)
|
||||
showResolutionLabel = this.isResolutionHD;
|
||||
else if (this.resolution !== null) {
|
||||
let resolutions = this.resolution || {};
|
||||
Object.keys(resolutions).map(function (ssrc) {
|
||||
let {width, height} = resolutions[ssrc];
|
||||
if (height >= config.minHDHeight)
|
||||
showResolutionLabel = true;
|
||||
});
|
||||
}
|
||||
|
||||
VideoLayout.updateResolutionLabel(showResolutionLabel);
|
||||
}
|
||||
};
|
||||
|
||||
export default ConnectionIndicator;
|
||||
|
|
|
@ -1,16 +1,44 @@
|
|||
/* global $, APP, interfaceConfig, config*/
|
||||
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
const thumbAspectRatio = 16.0 / 9.0;
|
||||
const thumbAspectRatio = 1 / 1;
|
||||
|
||||
const FilmStrip = {
|
||||
init () {
|
||||
/**
|
||||
*
|
||||
* @param eventEmitter the {EventEmitter} through which {FilmStrip} is to
|
||||
* emit/fire {UIEvents} (such as {UIEvents.TOGGLED_FILM_STRIP}).
|
||||
*/
|
||||
init (eventEmitter) {
|
||||
this.filmStrip = $('#remoteVideos');
|
||||
this.eventEmitter = eventEmitter;
|
||||
},
|
||||
|
||||
toggleFilmStrip () {
|
||||
/**
|
||||
* Toggles the visibility of the film strip.
|
||||
*
|
||||
* @param visible optional {Boolean} which specifies the desired visibility
|
||||
* of the film strip. If not specified, the visibility will be flipped
|
||||
* (i.e. toggled); otherwise, the visibility will be set to the specified
|
||||
* value.
|
||||
*/
|
||||
toggleFilmStrip (visible) {
|
||||
if (typeof visible === 'boolean'
|
||||
&& this.isFilmStripVisible() == visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filmStrip.toggleClass("hidden");
|
||||
|
||||
// Emit/fire UIEvents.TOGGLED_FILM_STRIP.
|
||||
var eventEmitter = this.eventEmitter;
|
||||
if (eventEmitter) {
|
||||
eventEmitter.emit(
|
||||
UIEvents.TOGGLED_FILM_STRIP,
|
||||
this.isFilmStripVisible());
|
||||
}
|
||||
},
|
||||
|
||||
isFilmStripVisible () {
|
||||
|
@ -44,11 +72,7 @@ const FilmStrip = {
|
|||
* that we want to take into account when calculating the film strip width.
|
||||
*/
|
||||
calculateThumbnailSize (isSideBarVisible) {
|
||||
// Calculate the available height, which is the inner window height
|
||||
// minus 39px for the header minus 2px for the delimiter lines on the
|
||||
// top and bottom of the large video, minus the 36px space inside the
|
||||
// remoteVideos container used for highlighting shadow.
|
||||
let availableHeight = 100;
|
||||
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
||||
|
||||
let numvids = this.getThumbs(true).length;
|
||||
|
||||
|
@ -80,17 +104,17 @@ const FilmStrip = {
|
|||
let maxHeight
|
||||
// If the MAX_HEIGHT property hasn't been specified
|
||||
// we have the static value.
|
||||
= Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 160,
|
||||
= Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
|
||||
availableHeight);
|
||||
|
||||
availableHeight
|
||||
= Math.min( maxHeight,
|
||||
availableWidth / thumbAspectRatio,
|
||||
window.innerHeight - 18);
|
||||
= Math.min( maxHeight, window.innerHeight - 18);
|
||||
|
||||
if (availableHeight < availableWidth / thumbAspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * thumbAspectRatio);
|
||||
if (availableHeight < availableWidth) {
|
||||
availableWidth = availableHeight;
|
||||
}
|
||||
else
|
||||
availableHeight = availableWidth;
|
||||
|
||||
return {
|
||||
thumbWidth: availableWidth,
|
||||
|
|
|
@ -38,4 +38,27 @@ export default class LargeContainer {
|
|||
*/
|
||||
onHoverOut (e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update video stream.
|
||||
* @param {JitsiTrack?} stream new stream
|
||||
* @param {string} videoType video type
|
||||
*/
|
||||
setStream (stream, videoType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide user avatar.
|
||||
* @param {boolean} show
|
||||
*/
|
||||
showAvatar (show) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether current container needs to be switched on dominant speaker event
|
||||
* when the container is on stage.
|
||||
* @return {boolean}
|
||||
*/
|
||||
stayOnStage () {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,10 @@ import FilmStrip from './FilmStrip';
|
|||
import Avatar from "../avatar/Avatar";
|
||||
import {createDeferred} from '../../util/helpers';
|
||||
|
||||
const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
|
||||
const FADE_DURATION_MS = 300;
|
||||
|
||||
export const VIDEO_CONTAINER_TYPE = "camera";
|
||||
|
||||
/**
|
||||
* Get stream id.
|
||||
* @param {JitsiTrack?} stream
|
||||
|
@ -150,8 +151,6 @@ function getDesktopVideoPosition(videoWidth,
|
|||
return { horizontalIndent, verticalIndent };
|
||||
}
|
||||
|
||||
export const VideoContainerType = "video";
|
||||
|
||||
/**
|
||||
* Container for user video.
|
||||
*/
|
||||
|
@ -175,6 +174,8 @@ class VideoContainer extends LargeContainer {
|
|||
this.$avatar = $('#dominantSpeaker');
|
||||
this.$wrapper = $('#largeVideoWrapper');
|
||||
|
||||
this.avatarHeight = $("#dominantSpeakerAvatar").height();
|
||||
|
||||
// This does not work with Temasys plugin - has to be a property to be
|
||||
// copied between new <object> elements
|
||||
//this.$video.on('play', onPlay);
|
||||
|
@ -245,7 +246,7 @@ class VideoContainer extends LargeContainer {
|
|||
containerWidth, containerHeight);
|
||||
|
||||
// update avatar position
|
||||
let top = containerHeight / 2 - avatarSize / 4 * 3;
|
||||
let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
|
||||
|
||||
this.$avatar.css('top', top);
|
||||
|
||||
|
@ -332,6 +333,10 @@ class VideoContainer extends LargeContainer {
|
|||
}
|
||||
|
||||
hide () {
|
||||
// as the container is hidden/replaced by another container
|
||||
// hide its avatar
|
||||
this.showAvatar(false);
|
||||
|
||||
// its already hidden
|
||||
if (!this.isVisible) {
|
||||
return Promise.resolve();
|
||||
|
@ -345,6 +350,13 @@ class VideoContainer extends LargeContainer {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} switch on dominant speaker event if on stage.
|
||||
*/
|
||||
stayOnStage () {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -354,9 +366,13 @@ export default class LargeVideoManager {
|
|||
constructor () {
|
||||
this.containers = {};
|
||||
|
||||
this.state = VideoContainerType;
|
||||
this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
|
||||
this.addContainer(VideoContainerType, this.videoContainer);
|
||||
this.state = VIDEO_CONTAINER_TYPE;
|
||||
this.videoContainer = new VideoContainer(
|
||||
() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// use the same video container to handle and desktop tracks
|
||||
this.addContainer("desktop", this.videoContainer);
|
||||
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
|
@ -368,22 +384,26 @@ export default class LargeVideoManager {
|
|||
});
|
||||
|
||||
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
|
||||
let leftWatermarkDiv = this.$container.find("div.watermark.leftwatermark");
|
||||
let leftWatermarkDiv
|
||||
= this.$container.find("div.watermark.leftwatermark");
|
||||
|
||||
leftWatermarkDiv.css({display: 'block'});
|
||||
|
||||
leftWatermarkDiv.parent().attr('href', interfaceConfig.JITSI_WATERMARK_LINK);
|
||||
leftWatermarkDiv.parent().attr(
|
||||
'href', interfaceConfig.JITSI_WATERMARK_LINK);
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
|
||||
let rightWatermarkDiv = this.$container.find("div.watermark.rightwatermark");
|
||||
let rightWatermarkDiv
|
||||
= this.$container.find("div.watermark.rightwatermark");
|
||||
|
||||
rightWatermarkDiv.css({
|
||||
display: 'block',
|
||||
backgroundImage: 'url(images/rightwatermark.png)'
|
||||
});
|
||||
|
||||
rightWatermarkDiv.parent().attr('href', interfaceConfig.BRAND_WATERMARK_LINK);
|
||||
rightWatermarkDiv.parent().attr(
|
||||
'href', interfaceConfig.BRAND_WATERMARK_LINK);
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_POWERED_BY) {
|
||||
|
@ -413,7 +433,8 @@ export default class LargeVideoManager {
|
|||
}
|
||||
|
||||
get id () {
|
||||
return this.videoContainer.id;
|
||||
let container = this.getContainer(this.state);
|
||||
return container.id;
|
||||
}
|
||||
|
||||
scheduleLargeVideoUpdate () {
|
||||
|
@ -430,16 +451,22 @@ export default class LargeVideoManager {
|
|||
this.newStreamData = null;
|
||||
|
||||
console.info("hover in %s", id);
|
||||
this.state = VideoContainerType;
|
||||
this.videoContainer.setStream(stream, videoType);
|
||||
this.state = videoType;
|
||||
let container = this.getContainer(this.state);
|
||||
container.setStream(stream, videoType);
|
||||
|
||||
// change the avatar url on large
|
||||
this.updateAvatar(Avatar.getAvatarUrl(id));
|
||||
|
||||
let isVideoMuted = stream ? stream.isMuted() : true;
|
||||
// If we the continer 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 isVideoMuted = false;
|
||||
if (videoType == VIDEO_CONTAINER_TYPE)
|
||||
isVideoMuted = stream ? stream.isMuted() : true;
|
||||
|
||||
// show the avatar on large if needed
|
||||
this.videoContainer.showAvatar(isVideoMuted);
|
||||
container.showAvatar(isVideoMuted);
|
||||
|
||||
let promise;
|
||||
|
||||
|
@ -449,7 +476,7 @@ export default class LargeVideoManager {
|
|||
this.showWatermark(true);
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = this.videoContainer.show();
|
||||
promise = container.show();
|
||||
}
|
||||
|
||||
// resolve updateLargeVideo promise after everything is done
|
||||
|
@ -457,7 +484,8 @@ export default class LargeVideoManager {
|
|||
|
||||
return promise;
|
||||
}).then(() => {
|
||||
// after everything is done check again if there are any pending new streams.
|
||||
// after everything is done check again if there are any pending
|
||||
// new streams.
|
||||
this.updateInProcess = false;
|
||||
this.scheduleLargeVideoUpdate();
|
||||
});
|
||||
|
@ -529,7 +557,8 @@ export default class LargeVideoManager {
|
|||
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
|
||||
*/
|
||||
enableVideoProblemFilter (enable) {
|
||||
this.videoContainer.$video.toggleClass("videoProblemFilter", enable);
|
||||
let container = this.getContainer(this.state);
|
||||
container.$video.toggleClass("videoProblemFilter", enable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -600,7 +629,7 @@ export default class LargeVideoManager {
|
|||
}
|
||||
|
||||
let oldContainer = this.containers[this.state];
|
||||
if (this.state === VideoContainerType) {
|
||||
if (this.state === VIDEO_CONTAINER_TYPE) {
|
||||
this.showWatermark(false);
|
||||
}
|
||||
oldContainer.hide();
|
||||
|
@ -609,7 +638,7 @@ export default class LargeVideoManager {
|
|||
let container = this.getContainer(type);
|
||||
|
||||
return container.show().then(() => {
|
||||
if (type === VideoContainerType) {
|
||||
if (type === VIDEO_CONTAINER_TYPE) {
|
||||
this.showWatermark(true);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ function LocalVideo(VideoLayout, emitter) {
|
|||
this.videoSpanId = "localVideoContainer";
|
||||
this.container = $("#localVideoContainer").get(0);
|
||||
this.bindHoverHandler();
|
||||
this.VideoLayout = VideoLayout;
|
||||
this.flipX = true;
|
||||
this.isLocal = true;
|
||||
this.emitter = emitter;
|
||||
|
@ -22,7 +21,7 @@ function LocalVideo(VideoLayout, emitter) {
|
|||
return APP.conference.localId;
|
||||
}
|
||||
});
|
||||
SmallVideo.call(this);
|
||||
SmallVideo.call(this, VideoLayout);
|
||||
}
|
||||
|
||||
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
|
@ -159,7 +158,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
|||
if (event.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.VideoLayout.handleVideoThumbClicked(true, this.id);
|
||||
this.VideoLayout.handleVideoThumbClicked(this.id);
|
||||
};
|
||||
|
||||
let localVideoContainerSelector = $('#localVideoContainer');
|
||||
|
@ -192,7 +191,10 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
|||
|
||||
let endedHandler = () => {
|
||||
localVideoContainer.removeChild(localVideo);
|
||||
this.VideoLayout.updateRemovedVideo(this.id);
|
||||
// when removing only the video element and we are on stage
|
||||
// update the stage
|
||||
if(this.VideoLayout.isCurrentlyOnLarge(this.id))
|
||||
this.VideoLayout.updateLargeVideo(this.id);
|
||||
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
};
|
||||
stream.on(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
|
|
|
@ -11,14 +11,13 @@ function RemoteVideo(id, VideoLayout, emitter) {
|
|||
this.id = id;
|
||||
this.emitter = emitter;
|
||||
this.videoSpanId = `participant_${id}`;
|
||||
this.VideoLayout = VideoLayout;
|
||||
SmallVideo.call(this, VideoLayout);
|
||||
this.addRemoteVideoContainer();
|
||||
this.connectionIndicator = new ConnectionIndicator(this, id);
|
||||
this.setDisplayName();
|
||||
this.bindHoverHandler();
|
||||
this.flipX = false;
|
||||
this.isLocal = false;
|
||||
SmallVideo.call(this);
|
||||
}
|
||||
|
||||
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
|
@ -157,8 +156,10 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
|
|||
console.info((isVideo ? "Video" : "Audio") +
|
||||
" removed " + this.id, select);
|
||||
|
||||
if (isVideo)
|
||||
this.VideoLayout.updateRemovedVideo(this.id);
|
||||
// when removing only the video element and we are on stage
|
||||
// update the stage
|
||||
if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
|
||||
this.VideoLayout.updateLargeVideo(this.id);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -169,7 +170,7 @@ RemoteVideo.prototype.remove = function () {
|
|||
this.removeConnectionIndicator();
|
||||
// Make sure that the large video is updated if are removing its
|
||||
// corresponding small video.
|
||||
this.VideoLayout.updateRemovedVideo(this.id);
|
||||
this.VideoLayout.updateAfterThumbRemoved(this.id);
|
||||
// Remove whole container
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
|
@ -221,7 +222,7 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
|||
|
||||
// ignore click if it was done in popup menu
|
||||
if ($(source).parents('.popupmenu').length === 0) {
|
||||
this.VideoLayout.handleVideoThumbClicked(false, this.id);
|
||||
this.VideoLayout.handleVideoThumbClicked(this.id);
|
||||
}
|
||||
|
||||
// On IE we need to populate this handler on video <object>
|
||||
|
|
|
@ -5,12 +5,13 @@ import UIUtil from "../util/UIUtil";
|
|||
|
||||
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
|
||||
|
||||
function SmallVideo() {
|
||||
function SmallVideo(VideoLayout) {
|
||||
this.isMuted = false;
|
||||
this.hasAvatar = false;
|
||||
this.isVideoMuted = false;
|
||||
this.videoStream = null;
|
||||
this.audioStream = null;
|
||||
this.VideoLayout = VideoLayout;
|
||||
}
|
||||
|
||||
function setVisibility(selector, show) {
|
||||
|
@ -19,6 +20,14 @@ function setVisibility(selector, show) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of this small video.
|
||||
*
|
||||
* @returns the identifier of this small video
|
||||
*/
|
||||
SmallVideo.prototype.getId = function () {
|
||||
return this.id;
|
||||
};
|
||||
|
||||
/* Indicates if this small video is currently visible.
|
||||
*
|
||||
|
@ -362,14 +371,7 @@ SmallVideo.prototype.updateView = function () {
|
|||
}
|
||||
setVisibility(avatar, showAvatar);
|
||||
|
||||
var showDisplayName = !showVideo && !showAvatar;
|
||||
|
||||
if (showDisplayName) {
|
||||
this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
|
||||
}
|
||||
else {
|
||||
this.showDisplayName(false);
|
||||
}
|
||||
this.showDisplayName(!showVideo && !showAvatar);
|
||||
};
|
||||
|
||||
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
|
||||
|
|
|
@ -9,15 +9,14 @@ import UIEvents from "../../../service/UI/UIEvents";
|
|||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
import RemoteVideo from "./RemoteVideo";
|
||||
import LargeVideoManager, {VideoContainerType} from "./LargeVideo";
|
||||
import {PreziContainerType} from '../prezi/Prezi';
|
||||
import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
|
||||
import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
|
||||
import LocalVideo from "./LocalVideo";
|
||||
import PanelToggler from "../side_pannels/SidePanelToggler";
|
||||
|
||||
const RTCUIUtil = JitsiMeetJS.util.RTCUIHelper;
|
||||
|
||||
var remoteVideos = {};
|
||||
var remoteVideoTypes = {};
|
||||
var localVideoThumbnail = null;
|
||||
|
||||
var currentDominantSpeaker = null;
|
||||
|
@ -32,7 +31,7 @@ var eventEmitter = null;
|
|||
* Currently focused video jid
|
||||
* @type {String}
|
||||
*/
|
||||
var focusedVideoResourceJid = null;
|
||||
var pinnedId = null;
|
||||
|
||||
/**
|
||||
* On contact list item clicked.
|
||||
|
@ -50,7 +49,7 @@ function onContactClicked (id) {
|
|||
if (remoteVideo.hasVideoStarted()) {
|
||||
// We have a video src, great! Let's update the large video
|
||||
// now.
|
||||
VideoLayout.handleVideoThumbClicked(false, id);
|
||||
VideoLayout.handleVideoThumbClicked(id);
|
||||
} else {
|
||||
|
||||
// If we don't have a video src for jid, there's absolutely
|
||||
|
@ -64,7 +63,7 @@ function onContactClicked (id) {
|
|||
// picked up later by the lastN changed event handler.
|
||||
|
||||
lastNPickupId = id;
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, id);
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +93,11 @@ var VideoLayout = {
|
|||
init (emitter) {
|
||||
eventEmitter = emitter;
|
||||
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
||||
// sets default video type of local video
|
||||
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
|
||||
// if we do not resize the thumbs here, if there is no video device
|
||||
// the local video thumb maybe one pixel
|
||||
this.resizeThumbnails(false, true, false);
|
||||
|
||||
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||
this.lastNCount = config.channelLastN;
|
||||
|
@ -195,23 +199,22 @@ var VideoLayout = {
|
|||
/**
|
||||
* Checks if removed video is currently displayed and tries to display
|
||||
* another one instead.
|
||||
* Uses focusedID if any or dominantSpeakerID if any,
|
||||
* otherwise elects new video, in this order.
|
||||
*/
|
||||
updateRemovedVideo (id) {
|
||||
updateAfterThumbRemoved (id) {
|
||||
if (!this.isCurrentlyOnLarge(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newId;
|
||||
|
||||
// We'll show user's avatar if he is the dominant speaker or if
|
||||
// his video thumbnail is pinned
|
||||
if (remoteVideos[id] && (id === focusedVideoResourceJid
|
||||
|| id === currentDominantSpeaker)) {
|
||||
newId = id;
|
||||
} else {
|
||||
// Otherwise select last visible video
|
||||
if (pinnedId)
|
||||
newId = pinnedId;
|
||||
else if (currentDominantSpeaker)
|
||||
newId = currentDominantSpeaker;
|
||||
else // Otherwise select last visible video
|
||||
newId = this.electLastVisibleVideo();
|
||||
}
|
||||
|
||||
this.updateLargeVideo(newId);
|
||||
},
|
||||
|
@ -278,26 +281,39 @@ var VideoLayout = {
|
|||
/**
|
||||
* Return the type of the remote video.
|
||||
* @param id the id for the remote video
|
||||
* @returns the video type video or screen.
|
||||
* @returns {String} the video type video or screen.
|
||||
*/
|
||||
getRemoteVideoType (id) {
|
||||
return remoteVideoTypes[id];
|
||||
let smallVideo = VideoLayout.getSmallVideo(id);
|
||||
return smallVideo ? smallVideo.getVideoType() : null;
|
||||
},
|
||||
|
||||
handleVideoThumbClicked (noPinnedEndpointChangedEvent,
|
||||
resourceJid) {
|
||||
if(focusedVideoResourceJid) {
|
||||
var oldSmallVideo
|
||||
= VideoLayout.getSmallVideo(focusedVideoResourceJid);
|
||||
isPinned (id) {
|
||||
return (pinnedId) ? (id === pinnedId) : false;
|
||||
},
|
||||
|
||||
getPinnedId () {
|
||||
return pinnedId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the click on a video thumbnail.
|
||||
*
|
||||
* @param id the identifier of the video thumbnail
|
||||
*/
|
||||
handleVideoThumbClicked (id) {
|
||||
if(pinnedId) {
|
||||
var oldSmallVideo = VideoLayout.getSmallVideo(pinnedId);
|
||||
if (oldSmallVideo && !interfaceConfig.filmStripOnly)
|
||||
oldSmallVideo.focus(false);
|
||||
}
|
||||
|
||||
var smallVideo = VideoLayout.getSmallVideo(resourceJid);
|
||||
// Unlock current focused.
|
||||
if (focusedVideoResourceJid === resourceJid)
|
||||
var smallVideo = VideoLayout.getSmallVideo(id);
|
||||
|
||||
// Unpin if currently pinned.
|
||||
if (pinnedId === id)
|
||||
{
|
||||
focusedVideoResourceJid = null;
|
||||
pinnedId = null;
|
||||
// Enable the currently set dominant speaker.
|
||||
if (currentDominantSpeaker) {
|
||||
if(smallVideo && smallVideo.hasVideo()) {
|
||||
|
@ -305,44 +321,45 @@ var VideoLayout = {
|
|||
}
|
||||
}
|
||||
|
||||
if (!noPinnedEndpointChangedEvent) {
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
|
||||
}
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock new video
|
||||
focusedVideoResourceJid = resourceJid;
|
||||
pinnedId = id;
|
||||
|
||||
// Update focused/pinned interface.
|
||||
if (resourceJid) {
|
||||
if (id) {
|
||||
if (smallVideo && !interfaceConfig.filmStripOnly)
|
||||
smallVideo.focus(true);
|
||||
|
||||
if (!noPinnedEndpointChangedEvent) {
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
|
||||
}
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
|
||||
}
|
||||
|
||||
this.updateLargeVideo(resourceJid);
|
||||
this.updateLargeVideo(id);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Checks if container for participant identified by given id exists
|
||||
* in the document and creates it eventually.
|
||||
*
|
||||
* @return Returns <tt>true</tt> if the peer container exists,
|
||||
* <tt>false</tt> - otherwise
|
||||
* Creates a remote video for participant for the given id.
|
||||
* @param id the id of the participant to add
|
||||
* @param {SmallVideo} smallVideo optional small video instance to add as a
|
||||
* remote video, if undefined RemoteVideo will be created
|
||||
*/
|
||||
addParticipantContainer (id) {
|
||||
let remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
|
||||
addParticipantContainer (id, smallVideo) {
|
||||
let remoteVideo;
|
||||
if(smallVideo)
|
||||
remoteVideo = smallVideo;
|
||||
else
|
||||
remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
|
||||
remoteVideos[id] = remoteVideo;
|
||||
|
||||
let videoType = remoteVideoTypes[id];
|
||||
if (videoType) {
|
||||
remoteVideo.setVideoType(videoType);
|
||||
let videoType = VideoLayout.getRemoteVideoType(id);
|
||||
if (!videoType) {
|
||||
// make video type the default one (camera)
|
||||
videoType = VIDEO_CONTAINER_TYPE;
|
||||
}
|
||||
remoteVideo.setVideoType(videoType);
|
||||
|
||||
// In case this is not currently in the last n we don't show it.
|
||||
if (localLastNCount && localLastNCount > 0 &&
|
||||
|
@ -361,13 +378,13 @@ var VideoLayout = {
|
|||
false, false, false, function() {$(videoelem).show();});
|
||||
|
||||
// Update the large video to the last added video only if there's no
|
||||
// current dominant, focused speaker or prezi playing or update it to
|
||||
// current dominant, focused speaker or update it to
|
||||
// the current dominant speaker.
|
||||
if ((!focusedVideoResourceJid &&
|
||||
if ((!pinnedId &&
|
||||
!currentDominantSpeaker &&
|
||||
!this.isLargeContainerTypeVisible(PreziContainerType)) ||
|
||||
focusedVideoResourceJid === resourceJid ||
|
||||
(resourceJid &&
|
||||
this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
|
||||
pinnedId === resourceJid ||
|
||||
(!pinnedId && resourceJid &&
|
||||
currentDominantSpeaker === resourceJid)) {
|
||||
this.updateLargeVideo(resourceJid, true);
|
||||
}
|
||||
|
@ -522,7 +539,9 @@ var VideoLayout = {
|
|||
// since we don't want to switch to local video.
|
||||
// Update the large video if the video source is already available,
|
||||
// otherwise wait for the "videoactive.jingle" event.
|
||||
if (!focusedVideoResourceJid && remoteVideo.hasVideoStarted()) {
|
||||
if (!pinnedId
|
||||
&& remoteVideo.hasVideoStarted()
|
||||
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
},
|
||||
|
@ -639,11 +658,7 @@ var VideoLayout = {
|
|||
// Clean up the lastN pickup id.
|
||||
lastNPickupId = null;
|
||||
|
||||
// Don't fire the events again, they've already
|
||||
// been fired in the contact list click handler.
|
||||
VideoLayout.handleVideoThumbClicked(
|
||||
false,
|
||||
resourceJid);
|
||||
VideoLayout.handleVideoThumbClicked(resourceJid);
|
||||
|
||||
updateLargeVideo = false;
|
||||
}
|
||||
|
@ -730,9 +745,9 @@ var VideoLayout = {
|
|||
|
||||
removeParticipantContainer (id) {
|
||||
// Unlock large video
|
||||
if (focusedVideoResourceJid === id) {
|
||||
if (pinnedId === id) {
|
||||
console.info("Focused video owner has left the conference");
|
||||
focusedVideoResourceJid = null;
|
||||
pinnedId = null;
|
||||
}
|
||||
|
||||
if (currentDominantSpeaker === id) {
|
||||
|
@ -754,12 +769,11 @@ var VideoLayout = {
|
|||
},
|
||||
|
||||
onVideoTypeChanged (id, newVideoType) {
|
||||
if (remoteVideoTypes[id] === newVideoType) {
|
||||
if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("Peer video type changed: ", id, newVideoType);
|
||||
remoteVideoTypes[id] = newVideoType;
|
||||
|
||||
var smallVideo;
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
|
@ -773,8 +787,8 @@ var VideoLayout = {
|
|||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
smallVideo.setVideoType(newVideoType);
|
||||
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
|
@ -793,10 +807,6 @@ var VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
addRemoteVideoContainer (id) {
|
||||
return RemoteVideo.createContainer(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the video area.
|
||||
*
|
||||
|
@ -804,10 +814,11 @@ var VideoLayout = {
|
|||
* @param forceUpdate indicates that hidden thumbnails will be shown
|
||||
* @param completeFunction a function to be called when the video area is
|
||||
* resized.
|
||||
*/resizeVideoArea (isSideBarVisible,
|
||||
forceUpdate = false,
|
||||
animate = false,
|
||||
completeFunction = null) {
|
||||
*/
|
||||
resizeVideoArea (isSideBarVisible,
|
||||
forceUpdate = false,
|
||||
animate = false,
|
||||
completeFunction = null) {
|
||||
|
||||
if (largeVideo) {
|
||||
largeVideo.updateContainerSize(isSideBarVisible);
|
||||
|
@ -888,7 +899,15 @@ var VideoLayout = {
|
|||
},
|
||||
|
||||
isLargeVideoVisible () {
|
||||
return this.isLargeContainerTypeVisible(VideoContainerType);
|
||||
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {LargeContainer} the currently displayed container on large
|
||||
* video.
|
||||
*/
|
||||
getCurrentlyOnLargeContainer () {
|
||||
return largeVideo.getContainer(largeVideo.state);
|
||||
},
|
||||
|
||||
isCurrentlyOnLarge (id) {
|
||||
|
@ -903,7 +922,8 @@ var VideoLayout = {
|
|||
let currentId = largeVideo.id;
|
||||
|
||||
if (!isOnLarge || forceUpdate) {
|
||||
if (id !== currentId) {
|
||||
let videoType = this.getRemoteVideoType(id);
|
||||
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
|
||||
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
|
||||
}
|
||||
if (currentId) {
|
||||
|
@ -912,7 +932,6 @@ var VideoLayout = {
|
|||
|
||||
let smallVideo = this.getSmallVideo(id);
|
||||
|
||||
let videoType = this.getRemoteVideoType(id);
|
||||
largeVideo.updateLargeVideo(
|
||||
id,
|
||||
smallVideo.videoStream,
|
||||
|
@ -952,8 +971,27 @@ var VideoLayout = {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// if !show then use default type - large video
|
||||
return largeVideo.showContainer(show ? type : VideoContainerType);
|
||||
let currentId = largeVideo.id;
|
||||
if(currentId) {
|
||||
var oldSmallVideo = this.getSmallVideo(currentId);
|
||||
}
|
||||
|
||||
let containerTypeToShow = type;
|
||||
// if we are hiding a container and there is focusedVideo
|
||||
// (pinned remote video) use its video type,
|
||||
// if not then use default type - large video
|
||||
if (!show) {
|
||||
if(pinnedId)
|
||||
containerTypeToShow = this.getRemoteVideoType(pinnedId);
|
||||
else
|
||||
containerTypeToShow = VIDEO_CONTAINER_TYPE;
|
||||
}
|
||||
|
||||
return largeVideo.showContainer(containerTypeToShow)
|
||||
.then(() => {
|
||||
if(oldSmallVideo)
|
||||
oldSmallVideo && oldSmallVideo.updateView();
|
||||
});
|
||||
},
|
||||
|
||||
isLargeContainerTypeVisible (type) {
|
||||
|
@ -962,10 +1000,31 @@ var VideoLayout = {
|
|||
|
||||
/**
|
||||
* Returns the id of the current video shown on large.
|
||||
* Currently used by tests (troture).
|
||||
* Currently used by tests (torture).
|
||||
*/
|
||||
getLargeVideoID () {
|
||||
return largeVideo.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the the current video shown on large.
|
||||
* Currently used by tests (torture).
|
||||
*/
|
||||
getLargeVideo () {
|
||||
return largeVideo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the resolution label, indicating to the user that the large
|
||||
* video stream is currently HD.
|
||||
*/
|
||||
updateResolutionLabel(isResolutionHD) {
|
||||
let videoResolutionLabel = $("#videoResolutionLabel");
|
||||
|
||||
if (isResolutionHD && !videoResolutionLabel.is(":visible"))
|
||||
videoResolutionLabel.css({display: "block"});
|
||||
else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
|
||||
videoResolutionLabel.css({display: "none"});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -20,11 +20,14 @@ export default {
|
|||
START_MUTED_CHANGED: "UI.start_muted_changed",
|
||||
AUDIO_MUTED: "UI.audio_muted",
|
||||
VIDEO_MUTED: "UI.video_muted",
|
||||
PREZI_CLICKED: "UI.prezi_clicked",
|
||||
SHARE_PREZI: "UI.share_prezi",
|
||||
PREZI_SLIDE_CHANGED: "UI.prezi_slide_changed",
|
||||
STOP_SHARING_PREZI: "UI.stop_sharing_prezi",
|
||||
ETHERPAD_CLICKED: "UI.etherpad_clicked",
|
||||
SHARED_VIDEO_CLICKED: "UI.start_shared_video",
|
||||
/**
|
||||
* Updates shared video with params: url, state, time(optional)
|
||||
* Where url is the video link, state is stop/start/pause and time is the
|
||||
* current video playing time.
|
||||
*/
|
||||
UPDATE_SHARED_VIDEO: "UI.update_shared_video",
|
||||
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
|
||||
USER_INVITED: "UI.user_invited",
|
||||
USER_KICKED: "UI.user_kicked",
|
||||
|
@ -34,14 +37,39 @@ export default {
|
|||
TOGGLE_CHAT: "UI.toggle_chat",
|
||||
TOGGLE_SETTINGS: "UI.toggle_settings",
|
||||
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
|
||||
/**
|
||||
* Notifies that a command to toggle the film strip has been issued. The
|
||||
* event may optionally specify a {Boolean} (primitive) value to assign to
|
||||
* the visibility of the film strip (i.e. the event may act as a setter).
|
||||
* The very toggling of the film strip may or may not occurred at the time
|
||||
* of the receipt of the event depending on the position of the receiving
|
||||
* event listener in relation to the event listener which carries out the
|
||||
* command to toggle the film strip.
|
||||
*
|
||||
* @see {TOGGLED_FILM_STRIP}
|
||||
*/
|
||||
TOGGLE_FILM_STRIP: "UI.toggle_film_strip",
|
||||
/**
|
||||
* Notifies that the film strip was (actually) toggled. The event supplies
|
||||
* a {Boolean} (primitive) value indicating the visibility of the film
|
||||
* strip after the toggling (at the time of the event emission).
|
||||
*
|
||||
* @see {TOGGLE_FILM_STRIP}
|
||||
*/
|
||||
TOGGLED_FILM_STRIP: "UI.toggled_film_strip",
|
||||
TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
|
||||
TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
|
||||
CONTACT_CLICKED: "UI.contact_clicked",
|
||||
HANGUP: "UI.hangup",
|
||||
LOGOUT: "UI.logout",
|
||||
RECORDING_TOGGLE: "UI.recording_toggle",
|
||||
SIP_DIAL: "UI.sip_dial",
|
||||
SUBEJCT_CHANGED: "UI.subject_changed",
|
||||
SUBJECT_CHANGED: "UI.subject_changed",
|
||||
VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
|
||||
AUDIO_DEVICE_CHANGED: "UI.audio_device_changed"
|
||||
AUDIO_DEVICE_CHANGED: "UI.audio_device_changed",
|
||||
/**
|
||||
* Notifies interested listeners that the follow-me feature is enabled or
|
||||
* disabled.
|
||||
*/
|
||||
FOLLOW_ME_ENABLED: "UI.follow_me_enabled"
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue