Merge remote-tracking branch 'upstream/master'

Conflicts (resolved manually):
	css/main.css
	css/videolayout_default.css
	index.html
This commit is contained in:
luciash 2016-04-04 15:33:38 +02:00
commit bb04d4574f
28 changed files with 977 additions and 266 deletions

1
.gitignore vendored
View File

@ -6,5 +6,6 @@ node_modules
deploy-local.sh
libs/app.bundle.*
libs/lib-jitsi-meet*
libs/external_connect.js
all.css
.remote-sync.json

View File

@ -31,8 +31,9 @@ deploy-appbundle:
deploy-lib-jitsi-meet:
cp $(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map $(DEPLOY_DIR)
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
$(DEPLOY_DIR)
deploy-css:
(cd css; cat $(CSS_FILES)) | $(CLEANCSS) > css/all.css

61
app.js
View File

@ -1,4 +1,4 @@
/* global $, JitsiMeetJS, config */
/* global $, JitsiMeetJS, config, getRoomName */
/* application specific logic */
import "babel-polyfill";
@ -23,43 +23,42 @@ import API from './modules/API/API';
import UIEvents from './service/UI/UIEvents';
/**
* Builds and returns the room name.
*/
function buildRoomName () {
let path = window.location.pathname;
let roomName;
let roomName = getRoomName();
// determinde the room node from the url
// TODO: just the roomnode or the whole bare jid?
if (config.getroomnode && typeof config.getroomnode === 'function') {
// custom function might be responsible for doing the pushstate
roomName = config.getroomnode(path);
} else {
/* fall back to default strategy
* this is making assumptions about how the URL->room mapping happens.
* It currently assumes deployment at root, with a rewrite like the
* following one (for nginx):
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
*/
if (path.length > 1) {
roomName = path.substr(1).toLowerCase();
} else {
let word = RoomnameGenerator.generateRoomWithoutSeparator();
roomName = word.toLowerCase();
window.history.pushState(
'VideoChat', `Room: ${word}`, window.location.pathname + word
);
}
if(!roomName) {
let word = RoomnameGenerator.generateRoomWithoutSeparator();
roomName = word.toLowerCase();
window.history.pushState(
'VideoChat', `Room: ${word}`, window.location.pathname + word
);
}
return roomName;
}
const APP = {
// Used by do_external_connect.js if we receive the attach data after
// connect was already executed. status property can be "initialized",
// "ready" or "connecting". We are interested in "ready" status only which
// means that connect was executed but we have to wait for the attach data.
// In status "ready" handler property will be set to a function that will
// finish the connect process when the attach data or error is received.
connect: {
status: "initialized",
handler: null
},
// Used for automated performance tests
performanceTimes: {
"index.loaded": window.indexLoadedTime
},
UI,
settings,
conference,
connection: null,
API,
init () {
this.keyboardshortcut =
@ -104,8 +103,9 @@ function obtainConfigAndInit() {
// Get config result callback
function(success, error) {
if (success) {
console.log("(TIME) configuration fetched:\t",
window.performance.now());
var now = APP.performanceTimes["configuration.fetched"] =
window.performance.now();
console.log("(TIME) configuration fetched:\t", now);
init();
} else {
// Show obtain config error,
@ -124,7 +124,8 @@ function obtainConfigAndInit() {
$(document).ready(function () {
console.log("(TIME) document ready:\t", window.performance.now());
var now = APP.performanceTimes["document.ready"] = window.performance.now();
console.log("(TIME) document ready:\t", now);
URLProcessor.setConfigParametersFromUrl();
APP.init();

View File

@ -328,7 +328,7 @@ export default {
]);
}).then(([tracks, con]) => {
console.log('initialized with %s local tracks', tracks.length);
connection = con;
APP.connection = connection = con;
this._createRoom(tracks);
this.isDesktopSharingEnabled =
JitsiMeetJS.isDesktopSharingEnabled();
@ -567,7 +567,7 @@ export default {
_getConferenceOptions() {
let options = config;
if(config.enableRecording) {
if(config.enableRecording && !config.recordingType) {
options.recordingType = (config.hosts &&
(typeof config.hosts.jirecon != "undefined"))?
"jirecon" : "colibri";
@ -848,7 +848,8 @@ export default {
APP.UI.changeDisplayName(id, displayName);
});
room.on(ConferenceEvents.RECORDING_STATE_CHANGED, (status, error) => {
room.on(ConferenceEvents.RECORDER_STATE_CHANGED, (status, error) => {
console.log("Received recorder status change: ", status, error);
if(status == "error") {
console.error(error);
return;
@ -1008,15 +1009,8 @@ export default {
// Starts or stops the recording for the conference.
APP.UI.addListener(UIEvents.RECORDING_TOGGLE, (predefinedToken) => {
if (predefinedToken) {
room.toggleRecording({token: predefinedToken});
return;
}
APP.UI.requestRecordingToken().then((token) => {
room.toggleRecording({token: token});
});
APP.UI.addListener(UIEvents.RECORDING_TOGGLED, (options) => {
room.toggleRecording(options);
});
APP.UI.addListener(UIEvents.SUBJECT_CHANGED, (topic) => {
@ -1116,7 +1110,7 @@ export default {
Commands.SHARED_VIDEO, ({value, attributes}, id) => {
if (attributes.state === 'stop') {
APP.UI.stopSharedVideo(id);
APP.UI.stopSharedVideo(id, attributes);
} else if (attributes.state === 'start') {
APP.UI.showSharedVideo(id, value, attributes);
} else if (attributes.state === 'playing'

View File

@ -5,6 +5,42 @@ import LoginDialog from './modules/UI/authentication/LoginDialog';
const ConnectionEvents = JitsiMeetJS.events.connection;
const ConnectionErrors = JitsiMeetJS.errors.connection;
/**
* Checks if we have data to use attach instead of connect. If we have the data
* executes attach otherwise check if we have to wait for the data. If we have
* to wait for the attach data we are setting handler to APP.connect.handler
* which is going to be called when the attach data is received otherwise
* executes connect.
*
* @param {string} [id] user id
* @param {string} [password] password
* @param {string} [roomName] the name of the conference.
*/
function checkForAttachParametersAndConnect(id, password, connection) {
if(window.XMPPAttachInfo){
APP.connect.status = "connecting";
// When connection optimization is not deployed or enabled the default
// value will be window.XMPPAttachInfo.status = "error"
// If the connection optimization is deployed and enabled and there is
// a failure the value will be window.XMPPAttachInfo.status = "error"
if(window.XMPPAttachInfo.status === "error") {
connection.connect({id, password});
return;
}
var attachOptions = window.XMPPAttachInfo.data;
if(attachOptions) {
connection.attach(attachOptions);
} else {
connection.connect({id, password});
}
} else {
APP.connect.status = "ready";
APP.connect.handler = checkForAttachParametersAndConnect.bind(null,
id, password, connection);
}
}
/**
* Try to open connection using provided credentials.
* @param {string} [id]
@ -18,7 +54,8 @@ function connect(id, password, roomName) {
let connectionConfig = config;
connectionConfig.bosh += '?room=' + roomName;
let connection = new JitsiMeetJS.JitsiConnection(null, null, config);
let connection
= new JitsiMeetJS.JitsiConnection(null, config.token, config);
return new Promise(function (resolve, reject) {
connection.addEventListener(
@ -50,7 +87,7 @@ function connect(id, password, roomName) {
reject(err);
}
connection.connect({id, password});
checkForAttachParametersAndConnect(id, password, connection);
});
}

View File

@ -0,0 +1,80 @@
/* global config, getRoomName, getConfigParamsFromUrl */
/* global createConnectionExternally */
/**
* Implements extrnal connect using createConnectionExtenally function defined
* in external_connect.js for Jitsi Meet. Parses the room name and token from
* the url and executes createConnectionExtenally.
*
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet you should use this
* file as reference only because the implementation is Jitsi Meet specific.
*
* NOTE: For optimal results this file should be included right after
* exrnal_connect.js.
*/
/**
* Gets the token from the URL.
*/
function buildToken(){
var params = getConfigParamsFromUrl();
return params["config.token"] || config.token;
}
/**
* Executes createConnectionExternally function.
*/
(function () {
// FIXME: Add implementation for changing that config from the url for
// consistency
var url = config.externalConnectUrl;
/**
* Check if connect from connection.js was executed and executes the handler
* that is going to finish the connect work.
*/
function checkForConnectHandlerAndConnect() {
if(window.APP && window.APP.connect.status === "ready") {
window.APP.connect.handler();
}
}
function error_callback(error){
if(error) //error=undefined if external connect is disabled.
console.warn(error);
// Sets that global variable to be used later by connect method in
// connection.js
window.XMPPAttachInfo = {
status: "error"
};
checkForConnectHandlerAndConnect();
}
if(!url || !window.createConnectionExternally) {
error_callback();
return;
}
var room_name = getRoomName();
if(!room_name) {
error_callback();
return;
}
url += "?room=" + room_name;
var token = buildToken();
if(token)
url += "&token=" + token;
createConnectionExternally(url, function(connectionInfo) {
// Sets that global variable to be used later by connect method in
// connection.js
window.XMPPAttachInfo = {
status: "success",
data: connectionInfo
};
checkForConnectHandlerAndConnect();
}, error_callback);
})();

View File

@ -1,4 +1,5 @@
#chatspace {
display: none;
background-color: black;
border-left: 1px solid #424242;
}

View File

@ -1,4 +1,5 @@
#contactlist {
display: none;
background-color: black;
cursor: default;
}

View File

@ -129,21 +129,6 @@ html, body{
-moz-transition: all .5s ease-in-out;
transition: all .5s ease-in-out;
}
/*#ffde00*/
#toolbar_button_record.active {
-webkit-text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
-moz-text-shadow: 1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
}
a.button:hover,
a.bottomToolbarButton:hover {
@ -157,15 +142,6 @@ a.bottomToolbarButton:hover {
color: #636363;
}
.header_button_separator {
display: inline-block;
position:relative;
top: 5px;
width: 1px;
height: 20px;
background: #373737;
}
.bottom_button_separator {
display: inline-block;
position: relative;
@ -248,19 +224,24 @@ form {
position: absolute;
}
div.feedbackButton {
background-color: rgba(0, 0, 0, 0.5);
#feedbackButtonDiv {
display: none;
position: absolute;
background-color: rgba(0,0,0,.50);
border-radius: 50%;
bottom: 0;
font-size: 1em;
height: 3em;
left: 0;
overflow: hidden;
position: absolute;
text-align: center;
transition: all 0.2s ease-in-out 0s;
width: 3em;
z-index: 100;
transition: all 2s ease-in-out;
}
#feedbackButtonDiv.hidden {
bottom: -246px;
}
div.feedbackButton:hover {
@ -299,7 +280,7 @@ div.feedbackButton:hover {
}
.active {
color: #00ccff;
background-color: #00ccff;
}
.bottomToolbar_span>span {

View File

@ -1,4 +1,5 @@
#settingsmenu {
display: none;
background: black;
color: #00ccff;
overflow-y: auto;

View File

@ -504,3 +504,33 @@
color: rgba(255,255,255,.5);
z-index: 10000;
}
.centeredVideoLabel {
display: none;
position: absolute;
bottom: 45%;
top: auto;
right: auto;
left: auto;
line-height: 28px;
height: 28px;
width: auto;
padding: 5px;
margin-right: auto;
margin-left: auto;
background: rgba(0,0,0,.5);
color: #FFF;
z-index: 10000;
border-radius: 2px;
-webkit-transition: all 2s 2s linear;
transition: all 2s 2s linear;
}
.moveToCorner {
top: 5px;
right: 50px; /*leave free space for the HD label*/
margin-right: 0px;
margin-left: auto;
background: rgba(0,0,0,.3);
color: rgba(255,255,255,.5);
}

View File

@ -9,3 +9,4 @@ sounds /usr/share/jitsi-meet/
fonts /usr/share/jitsi-meet/
images /usr/share/jitsi-meet/
lang /usr/share/jitsi-meet/
connection_optimization /usr/share/jitsi-meet/

View File

@ -4,6 +4,17 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<!--#include virtual="title.html" -->
<script>
window.indexLoadedTime = window.performance.now();
console.log("(TIME) index.html loaded:\t", indexLoadedTime);
</script>
<script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="utils.js?v=1"></script>
<!--#include virtual="connection_optimization/connection_optimization.html" -->
<script src="connection_optimization/do_external_connect.js?v=1"></script>
<script src="interface_config.js?v=6"></script>
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script>
<link rel="icon" type="image/png" href="/images/favicon.ico?v=1"/>
<meta property="og:title" content="Jitsi Meet"/>
<meta property="og:image" content="/images/jitsilogo.png?v=1"/>
@ -14,11 +25,6 @@
<meta itemprop="image" content="/images/jitsilogo.png?v=1"/>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="css/all.css"/>
<script>console.log("(TIME) index.html loaded:\t", window.performance.now());</script>
<script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="interface_config.js?v=6"></script>
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script>
<!--#include virtual="plugin.head.html" -->
</head>
<body>
@ -138,14 +144,14 @@
</span>
<a class="button icon-microphone" id="toolbar_button_mute" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="mutePopover" data-i18n="[content]toolbar.mute" content="Mute / Unmute"></a>
<a class="button icon-camera" id="toolbar_button_camera" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="toggleVideoPopover" data-i18n="[content]toolbar.videomute" content="Start / stop camera"></a>
<a class="button icon-recEnable" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.record" content="Record" style="display: none"></a>
<a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="bottom" style="display: none"></a>
<a class="button icon-security" id="toolbar_button_security" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.lock" content="Lock / unlock room"></a>
<a class="button icon-link" id="toolbar_button_link" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.invite" content="Invite others"></a>
<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-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 fa fa-share-alt-square" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="bottom" content="Share a YouTube 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>
@ -176,6 +182,7 @@
</div>
<span id="videoConnectionMessage"></span>
<span id="videoResolutionLabel">HD</span>
<span id="recordingLabel" class="centeredVideoLabel"></span>
</div>
<div id="remoteVideos">
@ -271,7 +278,7 @@
</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">
<div id="feedbackButtonDiv">
<a id="feedbackButton" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]feedback"><i class="fa fa-heart"></i></a>
</div>
</div>

View File

@ -51,12 +51,11 @@
"mute": "Mute / Unmute",
"videomute": "Start / stop camera",
"authenticate": "Authenticate",
"record": "Record",
"lock": "Lock / unlock room",
"invite": "Invite others",
"chat": "Open / close chat",
"etherpad": "Shared document",
"sharedvideo": "Shared video",
"sharedvideo": "Share a YouTube video",
"sharescreen": "Share screen",
"fullscreen": "Enter / Exit Full Screen",
"sip": "Call SIP number",
@ -83,10 +82,10 @@
"title": "SETTINGS",
"update": "Update",
"name": "Name",
"startAudioMuted": "start without audio",
"startVideoMuted": "start without video",
"selectCamera": "select camera",
"selectMic": "select microphone",
"startAudioMuted": "Start without audio",
"startVideoMuted": "Start without video",
"selectCamera": "Select camera",
"selectMic": "Select microphone",
"followMe": "Enable follow me"
},
"videothumbnail":
@ -179,6 +178,7 @@
"joinAgain": "Join again",
"Share": "Share",
"Save": "Save",
"recording": "Recording",
"recordingToken": "Enter recording token",
"Dial": "Dial",
"sipMsg": "Enter SIP number",
@ -206,7 +206,14 @@
"firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
"feedbackQuestion": "How was your call?",
"thankYou": "Thank you for using __appName__!",
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?"
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
"liveStreaming": "Live Streaming",
"streamKey": "Stream name/key",
"startLiveStreaming": "Start live streaming",
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
"stopLiveStreaming": "Stop live streaming",
"stopRecording": "Stop recording"
},
"email":
{
@ -254,8 +261,20 @@
},
"recording":
{
"toaster": "Currently recording!",
"pending": "Your recording will start as soon as another participant joins",
"on": "Recording has been started"
"pending": "Recording waiting for a participant to join...",
"on": "Recording",
"off": "Recording stopped",
"failedToStart": "Recording failed to start",
"buttonTooltip": "Start / stop recording"
},
"liveStreaming":
{
"pending": "Starting Live Stream...",
"on": "Live Streaming",
"off": "Live Streaming Stopped",
"unavailable": "The live streaming service is currently unavailable. Please try again later.",
"failedToStart": "Live streaming failed to start",
"buttonTooltip": "Start / stop live stream",
"streamIdRequired": "Please fill in the stream id in order to launch the live streaming."
}
}

View File

@ -16,7 +16,6 @@
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
@ -25,6 +24,15 @@ import FilmStrip from './UI/videolayout/FilmStrip';
*/
const _COMMAND = "follow-me";
/**
* The timeout after which a follow-me command that has been received will be
* ignored if not consumed.
*
* @type {number} in seconds
* @private
*/
const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
/**
* Represents the set of {FollowMe}-related states (properties and their
* respective values) which are to be followed by a participant. {FollowMe}
@ -112,6 +120,7 @@ class FollowMe {
constructor (conference, UI) {
this._conference = conference;
this._UI = UI;
this.nextOnStageTimer = 0;
// The states of the local participant which are to be followed (by the
// remote participants when the local participant is in her right to
@ -127,6 +136,29 @@ class FollowMe {
this._onFollowMeCommand.bind(this));
}
/**
* Sets the current state of all follow-me properties, which will fire a
* localPropertyChangeEvent and trigger a send of the follow-me command.
* @private
*/
_setFollowMeInitialState() {
this._filmStripToggled.bind(this, this._UI.isFilmStripVisible());
var pinnedId = VideoLayout.getPinnedId();
var isPinned = false;
var smallVideo;
if (pinnedId) {
isPinned = true;
smallVideo = VideoLayout.getSmallVideo(pinnedId);
}
this._nextOnStage(smallVideo, isPinned);
this._sharedDocumentToggled
.bind(this, this._UI.getSharedDocumentManager().isVisible());
}
/**
* Adds listeners for the UI states of the local participant which are
* to be followed (by the remote participants). A non-moderator (very
@ -171,9 +203,10 @@ class FollowMe {
* to disable it
*/
enableFollowMe (enable) {
this.isEnabled = enable;
if (this.isEnabled)
if (enable) {
this._setFollowMeInitialState();
this._addFollowMeListeners();
}
else
this._removeFollowMeListeners();
}
@ -201,7 +234,7 @@ class FollowMe {
}
/**
* Changes the nextOnPage property value.
* Changes the nextOnStage property value.
*
* @param smallVideo the {SmallVideo} that was pinned or unpinned
* @param isPinned indicates if the given {SmallVideo} was pinned or
@ -265,6 +298,7 @@ class FollowMe {
// 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))
@ -284,6 +318,13 @@ class FollowMe {
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
}
/**
* Process a film strip open / close event received from FOLLOW-ME
* command.
* @param filmStripVisible indicates if the film strip has been shown or
* hidden
* @private
*/
_onFilmStripVisible(filmStripVisible) {
if (typeof filmStripVisible !== 'undefined') {
// XXX The Command(s) API doesn't preserve the types (of
@ -296,25 +337,41 @@ class FollowMe {
// 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);
if (filmStripVisible !== this._UI.isFilmStripVisible())
this._UI.eventEmitter.emit(UIEvents.TOGGLE_FILM_STRIP);
}
}
/**
* Process the id received from a FOLLOW-ME command.
* @param id the identifier of the next participant to show on stage or
* undefined if we're clearing the stage (we're unpining all pined and we
* rely on dominant speaker events)
* @private
*/
_onNextOnStage(id) {
var clickId = null;
if(typeof id !== 'undefined' && !VideoLayout.isPinned(id))
var pin;
if(typeof id !== 'undefined' && !VideoLayout.isPinned(id)) {
clickId = id;
else if (typeof id == 'undefined' && VideoLayout.getPinnedId())
pin = true;
}
else if (typeof id == 'undefined' && VideoLayout.getPinnedId()) {
clickId = VideoLayout.getPinnedId();
pin = false;
}
if (clickId)
VideoLayout.handleVideoThumbClicked(clickId);
this._pinVideoThumbnailById(clickId, pin);
}
/**
* Process a shared document open / close event received from FOLLOW-ME
* command.
* @param sharedDocumentVisible indicates if the shared document has been
* opened or closed
* @private
*/
_onSharedDocumentVisible(sharedDocumentVisible) {
if (typeof sharedDocumentVisible !== 'undefined') {
// XXX The Command(s) API doesn't preserve the types (of
@ -328,6 +385,41 @@ class FollowMe {
this._UI.getSharedDocumentManager().toggleEtherpad();
}
}
/**
* Pins / unpins the video thumbnail given by clickId.
*
* @param clickId the identifier of the video thumbnail to pin or unpin
* @param pin {true} to pin, {false} to unpin
* @private
*/
_pinVideoThumbnailById(clickId, pin) {
var self = this;
var smallVideo = VideoLayout.getSmallVideo(clickId);
// If the SmallVideo for the given clickId exists we proceed with the
// pin/unpin.
if (smallVideo) {
this.nextOnStageTimer = 0;
clearTimeout(this.nextOnStageTimout);
if (pin && !VideoLayout.isPinned(clickId)
|| !pin && VideoLayout.isPinned(clickId))
VideoLayout.handleVideoThumbClicked(clickId);
}
// If there's no SmallVideo object for the given id, lets wait and see
// if it's going to be created in the next 30sec.
else {
this.nextOnStageTimout = setTimeout(function () {
if (self.nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
self.nextOnStageTimer = 0;
return;
}
this.nextOnStageTimer++;
self._pinVideoThumbnailById(clickId, pin);
}, 1000);
}
}
}
export default FollowMe;

View File

@ -1,4 +1,5 @@
/* global $, APP, config, interfaceConfig */
import UIEvents from "../../service/UI/UIEvents";
/*
* Created by Yana Stamcheva on 2/10/15.
@ -60,6 +61,14 @@ var constructDetailedFeedbackHtml = function() {
*/
var feedbackWindowCallback = null;
/**
* Shows / hides the feedback button.
* @private
*/
function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
/**
* Defines all methods in connection to the Feedback window.
*
@ -73,17 +82,23 @@ var Feedback = {
feedbackScore: -1,
/**
* Initialise the Feedback functionality.
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function () {
init: function (emitter) {
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
$("div.feedbackButton").css("display", "block");
$("#feedbackButtonDiv").css("display", "block");
$("#feedbackButton").click(function (event) {
Feedback.openFeedbackWindow();
});
// Show / hide the feedback button whenever the film strip is
// shown / hidden.
emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
_toggleFeedbackIcon();
});
},
/**
* Indicates if the feedback functionality is enabled.

View File

@ -14,6 +14,7 @@ import UIEvents from "../../service/UI/UIEvents";
import CQEvents from '../../service/connectionquality/CQEvents';
import EtherpadManager from './etherpad/Etherpad';
import SharedVideoManager from './shared_video/SharedVideo';
import Recording from "./recording/Recording";
import VideoLayout from "./videolayout/VideoLayout";
import FilmStrip from "./videolayout/FilmStrip";
@ -251,7 +252,7 @@ UI.initConference = function () {
Toolbar.checkAutoEnableDesktopSharing();
if(!interfaceConfig.filmStripOnly) {
Feedback.init();
Feedback.init(eventEmitter);
}
// FollowMe attempts to copy certain aspects of the moderator's UI into the
@ -363,12 +364,16 @@ UI.start = function () {
bindEvents();
sharedVideoManager = new SharedVideoManager(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
$("#videospace").mousemove(function () {
return ToolbarToggler.showToolbar();
});
setupToolbars();
setupChat();
// Initialise the recording module.
if (config.enableRecording)
Recording.init(eventEmitter, config.recordingType);
// Display notice message at the top of the toolbar
if (config.noticeMessage) {
$('#noticeText').text(config.noticeMessage);
@ -566,15 +571,15 @@ UI.updateLocalRole = function (isModerator) {
VideoLayout.showModeratorIndicator();
Toolbar.showSipCallButton(isModerator);
Toolbar.showRecordingButton(isModerator);
Toolbar.showSharedVideoButton(isModerator);
Recording.showRecordingButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
Toolbar.checkAutoRecord();
Recording.checkAutoRecord();
}
};
@ -622,6 +627,14 @@ UI.toggleFilmStrip = function () {
self.toggleFilmStrip.apply(self, arguments);
};
/**
* Indicates if the film strip is currently visible or not.
* @returns {true} if the film strip is currently visible, otherwise
*/
UI.isFilmStripVisible = function () {
return FilmStrip.isFilmStripVisible();
};
/**
* Toggles chat panel.
*/
@ -977,37 +990,8 @@ UI.requestFeedback = function () {
});
};
/**
* Request recording token from the user.
* @returns {Promise}
*/
UI.requestRecordingToken = function () {
let msg = APP.translation.generateTranslationHTML("dialog.recordingToken");
let token = APP.translation.translateString("dialog.token");
return new Promise(function (resolve, reject) {
messageHandler.openTwoButtonDialog(
null, null, null,
`<h2>${msg}</h2>
<input name="recordingToken" type="text"
data-i18n="[placeholder]dialog.token"
placeholder="${token}" autofocus>`,
false, "dialog.Save",
function (e, v, m, f) {
if (v && f.recordingToken) {
resolve(UIUtil.escapeHtml(f.recordingToken));
} else {
reject();
}
},
null,
function () { },
':input:first'
);
});
};
UI.updateRecordingState = function (state) {
Toolbar.updateRecordingState(state);
Recording.updateRecordingState(state);
};
UI.notifyTokenAuthFailed = function () {
@ -1139,7 +1123,7 @@ UI.updateSharedVideo = function (id, url, attributes) {
*/
UI.stopSharedVideo = function (id, attributes) {
if (sharedVideoManager)
sharedVideoManager.stopSharedVideo(id);
sharedVideoManager.stopSharedVideo(id, attributes);
};
module.exports = UI;

View File

@ -0,0 +1,357 @@
/* global APP, $, config, interfaceConfig */
/*
* 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 UIUtil from '../util/UIUtil';
/**
* Indicates if the recording button should be enabled.
*
* @returns {boolean} {true} if the
* @private
*/
function _isRecordingButtonEnabled() {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1
&& config.enableRecording;
}
/**
* Request live stream token from the user.
* @returns {Promise}
*/
function _requestLiveStreamId() {
const msg = APP.translation.generateTranslationHTML("dialog.liveStreaming");
const token = APP.translation.translateString("dialog.streamKey");
const cancelButton
= APP.translation.generateTranslationHTML("dialog.Cancel");
const backButton = APP.translation.generateTranslationHTML("dialog.Back");
const startStreamingButton
= APP.translation.generateTranslationHTML("dialog.startLiveStreaming");
const streamIdRequired
= APP.translation.generateTranslationHTML(
"liveStreaming.streamIdRequired");
return new Promise(function (resolve, reject) {
let dialog = APP.UI.messageHandler.openDialogWithStates({
state0: {
html:
`<h2>${msg}</h2>
<input name="streamId" type="text"
data-i18n="[placeholder]dialog.streamKey"
placeholder="${token}" autofocus>`,
persistent: false,
buttons: [
{title: cancelButton, value: false},
{title: startStreamingButton, value: true}
],
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
e.preventDefault();
if (v) {
if (f.streamId && f.streamId.length > 0) {
resolve(UIUtil.escapeHtml(f.streamId));
dialog.close();
return;
}
else {
dialog.goToState('state1');
return false;
}
} else {
reject();
dialog.close();
return false;
}
}
},
state1: {
html: `<h2>${msg}</h2> ${streamIdRequired}`,
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');
}
}
}
});
});
}
/**
* Request recording token from the user.
* @returns {Promise}
*/
function _requestRecordingToken () {
let msg = APP.translation.generateTranslationHTML("dialog.recordingToken");
let token = APP.translation.translateString("dialog.token");
return new Promise(function (resolve, reject) {
APP.UI.messageHandler.openTwoButtonDialog(
null, null, null,
`<h2>${msg}</h2>
<input name="recordingToken" type="text"
data-i18n="[placeholder]dialog.token"
placeholder="${token}" autofocus>`,
false, "dialog.Save",
function (e, v, m, f) {
if (v && f.recordingToken) {
resolve(UIUtil.escapeHtml(f.recordingToken));
} else {
reject();
}
},
null,
function () { },
':input:first'
);
});
}
function _showStopRecordingPrompt (recordingType) {
var title;
var message;
var buttonKey;
if (recordingType === "jibri") {
title = "dialog.liveStreaming";
message = "dialog.stopStreamingWarning";
buttonKey = "dialog.stopLiveStreaming";
}
else {
title = "dialog.recording";
message = "dialog.stopRecordingWarning";
buttonKey = "dialog.stopRecording";
}
return new Promise(function (resolve, reject) {
APP.UI.messageHandler.openTwoButtonDialog(
title,
null,
message,
null,
false,
buttonKey,
function(e,v,m,f) {
if (v) {
resolve();
} else {
reject();
}
}
);
});
}
function moveToCorner(selector, move) {
let moveToCornerClass = "moveToCorner";
if (move && !selector.hasClass(moveToCornerClass))
selector.addClass(moveToCornerClass);
else
selector.removeClass(moveToCornerClass);
}
var Status = {
ON: "on",
OFF: "off",
AVAILABLE: "available",
UNAVAILABLE: "unavailable",
PENDING: "pending"
};
var Recording = {
/**
* Initializes the recording UI.
*/
init (emitter, recordingType) {
this.eventEmitter = emitter;
// Use recorder states directly from the library.
this.currentState = Status.UNAVAILABLE;
this.initRecordingButton(recordingType);
},
initRecordingButton(recordingType) {
let selector = $('#toolbar_button_record');
if (recordingType === 'jibri') {
this.baseClass = "fa fa-play-circle";
this.recordingOnKey = "liveStreaming.on";
this.recordingOffKey = "liveStreaming.off";
this.recordingPendingKey = "liveStreaming.pending";
this.failedToStartKey = "liveStreaming.failedToStart";
this.recordingButtonTooltip = "liveStreaming.buttonTooltip";
}
else {
this.baseClass = "icon-recEnable";
this.recordingOnKey = "recording.on";
this.recordingOffKey = "recording.off";
this.recordingPendingKey = "recording.pending";
this.failedToStartKey = "recording.failedToStart";
this.recordingButtonTooltip = "recording.buttonTooltip";
}
selector.addClass(this.baseClass);
selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip);
selector.attr("content",
APP.translation.translateString(this.recordingButtonTooltip));
var self = this;
selector.click(function () {
switch (self.currentState) {
case Status.ON:
case Status.PENDING: {
_showStopRecordingPrompt(recordingType).then(() =>
self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED));
break;
}
case Status.AVAILABLE:
case Status.OFF: {
if (recordingType === 'jibri')
_requestLiveStreamId().then((streamId) => {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{streamId: streamId});
});
else {
if (self.predefinedToken) {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{token: self.predefinedToken});
return;
}
_requestRecordingToken().then((token) => {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{token: token});
});
}
break;
}
default: {
APP.UI.messageHandler.openMessageDialog(
"dialog.liveStreaming",
"liveStreaming.unavailable"
);
}
}
});
},
// Shows or hides the 'recording' button.
showRecordingButton (show) {
if (_isRecordingButtonEnabled() && show) {
$('#toolbar_button_record').css({display: "inline-block"});
} else {
$('#toolbar_button_record').css({display: "none"});
}
},
updateRecordingState(recordingState) {
// I'm the recorder, so I don't want to see any UI related to states.
if (config.iAmRecorder)
return;
// If there's no state change, we ignore the update.
if (this.currentState === recordingState)
return;
this.setRecordingButtonState(recordingState);
},
// Sets the state of the recording button
setRecordingButtonState (recordingState) {
let buttonSelector = $('#toolbar_button_record');
let labelSelector = $('#recordingLabel');
// TODO: handle recording state=available
if (recordingState === Status.ON) {
buttonSelector.removeClass(this.baseClass);
buttonSelector.addClass(this.baseClass + " active");
labelSelector.attr("data-i18n", this.recordingOnKey);
moveToCorner(labelSelector, true, 3000);
labelSelector
.text(APP.translation.translateString(this.recordingOnKey));
} else if (recordingState === Status.OFF
|| recordingState === Status.UNAVAILABLE) {
// We don't want to do any changes if this is
// an availability change.
if (this.currentState !== Status.ON
&& this.currentState !== Status.PENDING)
return;
buttonSelector.removeClass(this.baseClass + " active");
buttonSelector.addClass(this.baseClass);
moveToCorner(labelSelector, false);
let messageKey;
if (this.currentState === Status.PENDING)
messageKey = this.failedToStartKey;
else
messageKey = this.recordingOffKey;
labelSelector.attr("data-i18n", messageKey);
labelSelector.text(APP.translation.translateString(messageKey));
setTimeout(function(){
$('#recordingLabel').css({display: "none"});
}, 5000);
}
else if (recordingState === Status.PENDING) {
buttonSelector.removeClass(this.baseClass + " active");
buttonSelector.addClass(this.baseClass);
moveToCorner(labelSelector, false);
labelSelector
.attr("data-i18n", this.recordingPendingKey);
labelSelector
.text(APP.translation.translateString(
this.recordingPendingKey));
}
this.currentState = recordingState;
// We don't show the label for available state.
if (recordingState !== Status.AVAILABLE
&& !labelSelector.is(":visible"))
labelSelector.css({display: "inline-block"});
},
// checks whether recording is enabled and whether we have params
// to start automatically recording
checkAutoRecord () {
if (_isRecordingButtonEnabled && config.autoRecord) {
this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
this.predefinedToken);
}
}
};
export default Recording;

View File

@ -44,7 +44,8 @@ export default class SharedVideoManager {
if(APP.conference.isLocalId(this.from)) {
showStopVideoPropmpt().then(() =>
this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop'));
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop'));
} else {
messageHandler.openMessageDialog(
"dialog.shareVideoTitle",
@ -64,12 +65,18 @@ export default class SharedVideoManager {
if (this.isSharedVideoShown)
return;
this.isSharedVideoShown = true;
// the video url
this.url = url;
// the owner of the video
this.from = id;
//listen for local audio mute events
this.localAudioMutedListener = this.localAudioMuted.bind(this);
this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener);
// This code loads the IFrame Player API code asynchronously.
var tag = document.createElement('script');
@ -82,7 +89,7 @@ export default class SharedVideoManager {
// 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;
this.initialAttributes = attributes;
var self = this;
if(self.isPlayerAPILoaded)
@ -91,7 +98,7 @@ export default class SharedVideoManager {
window.onYouTubeIframeAPIReady = function() {
self.isPlayerAPILoaded = true;
let showControls = APP.conference.isLocalId(self.from) ? 1 : 0;
new YT.Player('sharedVideoIFrame', {
let p = new YT.Player('sharedVideoIFrame', {
height: '100%',
width: '100%',
videoId: self.url,
@ -108,6 +115,18 @@ export default class SharedVideoManager {
'onError': onPlayerError
}
});
// add listener for volume changes
p.addEventListener(
"onVolumeChange", "onVolumeChange");
if (APP.conference.isLocalId(self.from)){
// adds progress listener that will be firing events
// while we are paused and we change the progress of the
// video (seeking forward or backward on the video)
p.addEventListener(
"onVideoProgress", "onVideoProgress");
}
};
window.onPlayerStateChange = function(event) {
@ -130,6 +149,32 @@ export default class SharedVideoManager {
}
};
/**
* Track player progress while paused.
* @param event
*/
window.onVideoProgress = function (event) {
let state = event.target.getPlayerState();
if (state == YT.PlayerState.PAUSED) {
self.updateCheck(true);
}
};
/**
* Gets notified for volume state changed.
* @param event
*/
window.onVolumeChange = function (event) {
self.updateCheck();
// let's check, if player is not muted lets mute locally
if(event.data.volume > 0 && !event.data.muted
&& !APP.conference.isLocalAudioMuted()){
self.emitter.emit(UIEvents.AUDIO_MUTED, true);
self.notifyUserComfortableMicMute(true);
}
};
window.onPlayerReady = function(event) {
let player = event.target;
// do not relay on autoplay as it is not sending all of the events
@ -144,12 +189,16 @@ export default class SharedVideoManager {
self.sharedVideo = new SharedVideoContainer(
{url, iframe, player});
//prevents pausing participants not sharing the video
// to pause the video
if (!APP.conference.isLocalId(self.from)) {
$("#sharedVideo").css("pointer-events","none");
}
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)) {
@ -157,17 +206,12 @@ export default class SharedVideoManager {
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);
console.error("Error in the player:", event.data);
// store the error player, so we can remove it
self.errorInPlayer = event.target;
};
}
@ -184,13 +228,15 @@ export default class SharedVideoManager {
if (attributes.state == 'playing') {
this.processTime(player, attributes);
this.processTime(player, attributes, playerPaused);
// lets check the volume
if (attributes.volume !== undefined &&
player.getVolume() != attributes.volume) {
player.getVolume() != attributes.volume
&& APP.conference.isLocalAudioMuted()) {
player.setVolume(attributes.volume);
console.info("Player change of volume:" + attributes.volume);
this.notifyUserComfortableVideoMute(false);
}
if(playerPaused)
@ -200,7 +246,9 @@ export default class SharedVideoManager {
// if its not paused, pause it
player.pauseVideo();
this.processTime(player, attributes);
this.processTime(player, attributes, true);
} else if (attributes.state == 'stop') {
this.stopSharedVideo(this.from);
}
}
@ -208,9 +256,16 @@ export default class SharedVideoManager {
* 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
* @param forceSeek whether seek should be forced
*/
processTime (player, attributes)
processTime (player, attributes, forceSeek)
{
if(forceSeek) {
console.info("Player seekTo:", attributes.time);
player.seekTo(attributes.time);
return;
}
// check received time and current time
let currentPosition = player.getCurrentTime();
let diff = Math.abs(attributes.time - currentPosition);
@ -230,8 +285,10 @@ export default class SharedVideoManager {
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)
// or there is still no player defined or we are stopped
// (in a process of stopping)
if(!APP.conference.isLocalId(this.from) || !this.player
|| !this.isSharedVideoShown)
return;
let state = this.player.getPlayerState();
@ -281,18 +338,31 @@ export default class SharedVideoManager {
* 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) {
stopSharedVideo (id, attributes) {
if (!this.isSharedVideoShown)
return;
if(this.from !== id)
return;
if(!this.player){
// if there is no error in the player till now,
// store the initial attributes
if (!this.errorInPlayer) {
this.initialAttributes = attributes;
return;
}
}
if(this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.emitter.removeListener(UIEvents.AUDIO_MUTED,
this.localAudioMutedListener);
this.localAudioMutedListener = null;
VideoLayout.removeParticipantContainer(this.url);
VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
@ -300,12 +370,68 @@ export default class SharedVideoManager {
VideoLayout.removeLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE);
this.player.destroy();
this.player = null;
if(this.player) {
this.player.destroy();
this.player = null;
} // if there is an error in player, remove that instance
else if (this.errorInPlayer) {
this.errorInPlayer.destroy();
this.errorInPlayer = null;
}
// revert to original behavior (prevents pausing
// for participants not sharing the video to pause it)
$("#sharedVideo").css("pointer-events","auto");
});
this.url = null;
this.isSharedVideoShown = false;
this.initialAttributes = null;
}
/**
* Receives events for local audio mute/unmute by local user.
* @param muted boolena whether it is muted or not.
*/
localAudioMuted (muted) {
if(!this.player)
return;
if(muted)
return;
// if we are un-muting and player is not muted, lets muted
// to not pollute the conference
if(this.player.getVolume() > 0 || !this.player.isMuted()){
this.player.setVolume(0);
this.notifyUserComfortableVideoMute(true);
}
}
/**
* Notifies user for muting its audio due to video is unmuted.
* @param show boolean, show or hide the notification
*/
notifyUserComfortableMicMute (show) {
if(show) {
this.notifyUserComfortableVideoMute(false);
console.log("Your audio was muted to enjoy the video");
}
else
console.log("Hide notification local audio muted");
}
/**
* Notifies user for muting the video due to audio is unmuted.
* @param show boolean, show or hide the notification
*/
notifyUserComfortableVideoMute (show) {
if(show) {
this.notifyUserComfortableMicMute(false);
console.log(
"Your shared video was muted in order to speak freely!");
}
else
console.log("Hide notification share video muted");
}
}
@ -333,6 +459,7 @@ class SharedVideoContainer extends LargeContainer {
self.bodyBackground = document.body.style.background;
document.body.style.background = 'black';
this.$iframe.css({opacity: 1});
ToolbarToggler.dockToolbar(true);
resolve();
});
});
@ -340,6 +467,7 @@ class SharedVideoContainer extends LargeContainer {
hide () {
let self = this;
ToolbarToggler.dockToolbar(false);
return new Promise(resolve => {
this.$iframe.fadeOut(300, () => {
document.body.style.background = self.bodyBackground;

View File

@ -6,7 +6,6 @@ import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
import UIEvents from '../../../service/UI/UIEvents';
let roomUrl = null;
let recordingToaster = null;
let emitter = null;
@ -43,51 +42,6 @@ function openLinkDialog () {
);
}
// Sets the state of the recording button
function setRecordingButtonState (recordingState) {
let selector = $('#toolbar_button_record');
if (recordingState === 'on') {
selector.removeClass("icon-recEnable");
selector.addClass("icon-recEnable active");
$("#largeVideo").toggleClass("videoMessageFilter", true);
let recordOnKey = "recording.on";
$('#videoConnectionMessage').attr("data-i18n", recordOnKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
setTimeout(function(){
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
}, 1500);
recordingToaster = messageHandler.notify(
null, "recording.toaster", null,
null, null,
{timeOut: 0, closeButton: null, tapToDismiss: false}
);
} else if (recordingState === 'off') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
if (recordingToaster) {
messageHandler.remove(recordingToaster);
}
} else if (recordingState === 'pending') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", true);
let recordPendingKey = "recording.pending";
$('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
$('#videoConnectionMessage').css({display: "block"});
}
}
const buttonHandlers = {
"toolbar_button_mute": function () {
if (APP.conference.audioMuted) {
@ -107,10 +61,6 @@ const buttonHandlers = {
emitter.emit(UIEvents.VIDEO_MUTED, true);
}
},
"toolbar_button_record": function () {
AnalyticsAdapter.sendEvent('toolbar.recording.toggled');
emitter.emit(UIEvents.RECORDING_TOGGLE);
},
"toolbar_button_security": function () {
emitter.emit(UIEvents.ROOM_LOCK_CLICKED);
},
@ -250,7 +200,8 @@ const Toolbar = {
*/
unlockLockButton () {
if ($("#toolbar_button_security").hasClass("icon-security-locked"))
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
UIUtil.buttonClick("#toolbar_button_security",
"icon-security icon-security-locked");
},
/**
@ -258,7 +209,8 @@ const Toolbar = {
*/
lockLockButton () {
if ($("#toolbar_button_security").hasClass("icon-security"))
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
UIUtil.buttonClick("#toolbar_button_security",
"icon-security icon-security-locked");
},
/**
@ -279,15 +231,6 @@ const Toolbar = {
}
},
// Shows or hides the 'recording' button.
showRecordingButton (show) {
if (UIUtil.isButtonEnabled('recording') && show) {
$('#toolbar_button_record').css({display: "inline-block"});
} else {
$('#toolbar_button_record').css({display: "none"});
}
},
// Shows or hides the 'shared video' button.
showSharedVideoButton () {
if (UIUtil.isButtonEnabled('sharedvideo')
@ -298,14 +241,6 @@ const Toolbar = {
}
},
// checks whether recording is enabled and whether we have params
// to start automatically recording
checkAutoRecord () {
if (UIUtil.isButtonEnabled('recording') && config.autoRecord) {
emitter.emit(UIEvents.RECORDING_TOGGLE, UIUtil.escapeHtml(config.autoRecordToken));
}
},
// checks whether desktop sharing is enabled and whether
// we have params to start automatically sharing
checkAutoEnableDesktopSharing () {
@ -383,10 +318,6 @@ const Toolbar = {
}
},
updateRecordingState (state) {
setRecordingButtonState(state);
},
/**
* Marks video icon as muted or not.
* @param {boolean} muted if icon should look like muted or not

View File

@ -59,7 +59,8 @@ const ToolbarToggler = {
* Shows the main toolbar.
*/
showToolbar () {
if (interfaceConfig.filmStripOnly) {
// if we are a recorder we do not want to show the toolbar
if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
return;
}
let header = $("#header");

View File

@ -123,11 +123,7 @@
},
isButtonEnabled: function (name) {
var isEnabled = interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
if (name === 'recording') {
return isEnabled && config.enableRecording;
}
return isEnabled;
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
},
hideDisabledButtons: function (mappings) {

View File

@ -141,8 +141,10 @@ SmallVideo.createStreamElement = function (stream) {
element.id = SmallVideo.getStreamElementID(stream);
element.onplay = function () {
var now = APP.performanceTimes["video.render"]
= window.performance.now();
console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t",
window.performance.now());
now);
};
element.oncontextmenu = function () { return false; };
@ -315,11 +317,21 @@ SmallVideo.prototype.selectVideoElement = function () {
return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
};
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
*
* @param isFocused indicates if the thumbnail should be focused/pinned or not
*/
SmallVideo.prototype.focus = function(isFocused) {
if(!isFocused) {
this.container.classList.remove("videoContainerFocused");
} else {
this.container.classList.add("videoContainerFocused");
var focusedCssClass = "videoContainerFocused";
var isFocusClassEnabled = $(this.container).hasClass(focusedCssClass);
if (!isFocused && isFocusClassEnabled) {
$(this.container).removeClass(focusedCssClass);
}
else if (isFocused && !isFocusClassEnabled) {
$(this.container).addClass(focusedCssClass);
}
};

View File

@ -1,18 +1,6 @@
/* global $, $iq, config, interfaceConfig */
/* global $, $iq, config, interfaceConfig, getConfigParamsFromUrl */
var configUtils = require('./Util');
var params = {};
function getConfigParamsFromUrl() {
if (!location.hash)
return {};
var hash = location.hash.substr(1);
var result = {};
hash.split("&").forEach(function (part) {
var item = part.split("=");
result[item[0]] = JSON.parse(
decodeURIComponent(item[1]).replace(/\\&/, "&"));
});
return result;
}
params = getConfigParamsFromUrl();
@ -62,4 +50,4 @@ var URLProcessor = {
}
};
module.exports = URLProcessor;
module.exports = URLProcessor;

View File

@ -24,7 +24,7 @@ var defaultOptions = {
fallbackOnEmpty: true,
useDataAttrOptions: true,
app: interfaceConfig.APP_NAME,
getAsync: false,
getAsync: true,
defaultValueFromContent: false,
customLoad: function(lng, ns, options, done) {
var resPath = "lang/__ns__-__lng__.json";

View File

@ -62,7 +62,7 @@ export default {
CONTACT_CLICKED: "UI.contact_clicked",
HANGUP: "UI.hangup",
LOGOUT: "UI.logout",
RECORDING_TOGGLE: "UI.recording_toggle",
RECORDING_TOGGLED: "UI.recording_toggled",
SIP_DIAL: "UI.sip_dial",
SUBJECT_CHANGED: "UI.subject_changed",
VIDEO_DEVICE_CHANGED: "UI.video_device_changed",

52
utils.js Normal file
View File

@ -0,0 +1,52 @@
/* global config */
/**
* Defines some utility methods that are used before the other JS files are
* loaded.
*/
/**
* Builds and returns the room name.
*/
function getRoomName () {
var path = window.location.pathname;
var roomName;
// determinde the room node from the url
// TODO: just the roomnode or the whole bare jid?
if (config.getroomnode && typeof config.getroomnode === 'function') {
// custom function might be responsible for doing the pushstate
roomName = config.getroomnode(path);
} else {
/* fall back to default strategy
* this is making assumptions about how the URL->room mapping happens.
* It currently assumes deployment at root, with a rewrite like the
* following one (for nginx):
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
*/
if (path.length > 1) {
roomName = path.substr(1).toLowerCase();
}
}
return roomName;
}
/**
* Parses the hash parameters from the URL and returns them as a JS object.
*/
function getConfigParamsFromUrl() {
if (!location.hash)
return {};
var hash = location.hash.substr(1);
var result = {};
hash.split("&").forEach(function (part) {
var item = part.split("=");
result[item[0]] = JSON.parse(
decodeURIComponent(item[1]).replace(/\\&/, "&"));
});
return result;
}