diff --git a/conference.js b/conference.js
index 447859c5b..618b230a2 100644
--- a/conference.js
+++ b/conference.js
@@ -21,16 +21,6 @@ const TrackErrors = JitsiMeetJS.errors.track;
let room, connection, localAudio, localVideo, roomLocker;
-/**
- * Known custom conference commands.
- */
-const Commands = {
- CONNECTION_QUALITY: "stats",
- EMAIL: "email",
- ETHERPAD: "etherpad",
- SHARED_VIDEO: "shared-video"
-};
-
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
/**
@@ -52,10 +42,11 @@ function connect(roomName) {
/**
* Share email with other users.
+ * @param emailCommand the email command
* @param {string} email new email
*/
-function sendEmail (email) {
- room.sendCommand(Commands.EMAIL, {
+function sendEmail (emailCommand, email) {
+ room.sendCommand(emailCommand, {
value: email,
attributes: {
id: room.myUserId()
@@ -507,6 +498,61 @@ export default {
getLogs () {
return room.getLogs();
},
+
+ /**
+ * 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: {
+ /**
+ * Known custom conference commands.
+ */
+ defaults: {
+ CONNECTION_QUALITY: "stats",
+ EMAIL: "email",
+ ETHERPAD: "etherpad",
+ SHARED_VIDEO: "shared-video",
+ CUSTOM_ROLE: "custom-role"
+ },
+ /**
+ * 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);
+ }
+ },
+
_createRoom (localTracks) {
room = connection.initJitsiConference(APP.conference.roomName,
this._getConferenceOptions());
@@ -522,7 +568,7 @@ export default {
this._room = room; // FIXME do not use this
let email = APP.settings.getEmail();
- email && sendEmail(email);
+ email && sendEmail(this.commands.defaults.EMAIL, email);
let nick = APP.settings.getDisplayName();
if (config.useNicks && !nick) {
@@ -534,50 +580,6 @@ 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 && !config.recordingType) {
@@ -916,7 +918,8 @@ export default {
APP.UI.updateLocalStats(percent, stats);
// send local stats to other users
- room.sendCommandOnce(Commands.CONNECTION_QUALITY, {
+ room.sendCommandOnce(this.commands.defaults.CONNECTION_QUALITY,
+ {
children: ConnectionQuality.convertToMUCStats(stats),
attributes: {
xmlns: 'http://jitsi.org/jitmeet/stats'
@@ -926,8 +929,9 @@ export default {
);
// listen to remote stats
- room.addCommandListener(Commands.CONNECTION_QUALITY,(values, from) => {
- ConnectionQuality.updateRemoteStats(from, values);
+ room.addCommandListener(this.commands.defaults.CONNECTION_QUALITY,
+ (values, from) => {
+ ConnectionQuality.updateRemoteStats(from, values);
});
ConnectionQuality.addListener(CQEvents.REMOTESTATS_UPDATED,
@@ -935,7 +939,7 @@ export default {
APP.UI.updateRemoteStats(id, percent, stats);
});
- room.addCommandListener(Commands.ETHERPAD, ({value}) => {
+ room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
APP.UI.initEtherpad(value);
});
@@ -948,9 +952,9 @@ export default {
APP.settings.setEmail(email);
APP.UI.setUserAvatar(room.myUserId(), email);
- sendEmail(email);
+ sendEmail(this.commands.defaults.EMAIL, email);
});
- room.addCommandListener(Commands.EMAIL, (data) => {
+ room.addCommandListener(this.commands.defaults.EMAIL, (data) => {
APP.UI.setUserAvatar(data.attributes.id, data.value);
});
@@ -1095,8 +1099,8 @@ export default {
// 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, {
+ room.removeCommand(this.commands.defaults.SHARED_VIDEO);
+ room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
value: url,
attributes: {
state: state,
@@ -1108,8 +1112,8 @@ export default {
else {
// in case of paused, in order to allow late users to join
// paused
- room.removeCommand(Commands.SHARED_VIDEO);
- room.sendCommand(Commands.SHARED_VIDEO, {
+ room.removeCommand(this.commands.defaults.SHARED_VIDEO);
+ room.sendCommand(this.commands.defaults.SHARED_VIDEO, {
value: url,
attributes: {
state: state,
@@ -1120,7 +1124,7 @@ export default {
}
});
room.addCommandListener(
- Commands.SHARED_VIDEO, ({value, attributes}, id) => {
+ this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => {
if (attributes.state === 'stop') {
APP.UI.stopSharedVideo(id, attributes);
diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js
index 457385112..92f79bc37 100644
--- a/modules/UI/Feedback.js
+++ b/modules/UI/Feedback.js
@@ -69,6 +69,21 @@ function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
+/**
+ * Shows / hides the feedback button.
+ * @param {show} set to {true} to show the feedback button or to {false}
+ * to hide it
+ * @private
+ */
+function _showFeedbackButton (show) {
+ var feedbackButton = $("#feedbackButtonDiv");
+
+ if (show)
+ feedbackButton.css("display", "block");
+ else
+ feedbackButton.css("display", "none");
+}
+
/**
* Defines all methods in connection to the Feedback window.
*
@@ -85,11 +100,15 @@ var Feedback = {
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function (emitter) {
+ // Initialise to enabled.
+ this.enabled = true;
+
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
- $("#feedbackButtonDiv").css("display", "block");
+
+ _showFeedbackButton(true);
$("#feedbackButton").click(function (event) {
Feedback.openFeedbackWindow();
});
@@ -100,13 +119,22 @@ var Feedback = {
_toggleFeedbackIcon();
});
},
+ /**
+ * Enables/ disabled the feedback feature.
+ */
+ enableFeedback: function (enable) {
+ if (this.enabled !== enable)
+ _showFeedbackButton(enable);
+ this.enabled = enable;
+ },
+
/**
* Indicates if the feedback functionality is enabled.
*
* @return true if the feedback functionality is enabled, false otherwise.
*/
isEnabled: function() {
- return APP.conference.isCallstatsEnabled();
+ return this.enabled && APP.conference.isCallstatsEnabled();
},
/**
* Opens the feedback window.
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 97cdddf82..d9d46deb4 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -29,6 +29,8 @@ var JitsiPopover = require("./util/JitsiPopover");
var Feedback = require("./Feedback");
import FollowMe from "../FollowMe";
+import Recorder from "../recorder/Recorder";
+
var eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
@@ -42,7 +44,8 @@ let followMeHandler;
* Prompt user for nickname.
*/
function promptDisplayName() {
- let nickRequiredMsg = APP.translation.translateString("dialog.displayNameRequired");
+ let nickRequiredMsg
+ = APP.translation.translateString("dialog.displayNameRequired");
let defaultNickMsg = APP.translation.translateString(
"defaultNickname", {name: "Jane Pink"}
);
@@ -110,8 +113,8 @@ function setupToolbars() {
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
function toggleFullScreen () {
- let isNotFullScreen = !document.fullscreenElement && // alternative standard method
-
+ // alternative standard method
+ let isNotFullScreen = !document.fullscreenElement &&
!document.mozFullScreenElement && // current working methods
!document.webkitFullscreenElement &&
!document.msFullscreenElement;
@@ -124,7 +127,8 @@ function toggleFullScreen () {
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
- document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ document.documentElement
+ .webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
if (document.exitFullscreen) {
@@ -238,6 +242,11 @@ UI.initConference = function () {
//if local role changes buttons state will be again updated
UI.updateLocalRole(false);
+ // Initialise the recorder handler. We're doing this explicitly before
+ // calling showToolbar, because the recorder may want to disable all
+ // toolbars.
+ new Recorder(APP.conference, UI);
+
// Once we've joined the muc show the toolbar
ToolbarToggler.showToolbar();
diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js
index 38e65504e..23a660b29 100644
--- a/modules/UI/recording/Recording.js
+++ b/modules/UI/recording/Recording.js
@@ -133,6 +133,13 @@ function _requestRecordingToken () {
});
}
+/**
+ * Shows a prompt dialog to the user when they have toggled off the recording.
+ *
+ * @param recordingType the recording type
+ * @returns {Promise}
+ * @private
+ */
function _showStopRecordingPrompt (recordingType) {
var title;
var message;
@@ -167,6 +174,12 @@ function _showStopRecordingPrompt (recordingType) {
});
}
+/**
+ * Moves the element given by {selector} to the top right corner of the screen.
+ * @param selector the selector for the element to move
+ * @param move {true} to move the element, {false} to move it back to its intial
+ * position
+ */
function moveToCorner(selector, move) {
let moveToCornerClass = "moveToCorner";
@@ -176,6 +189,12 @@ function moveToCorner(selector, move) {
selector.removeClass(moveToCornerClass);
}
+/**
+ * The status of the recorder.
+ * FIXME: Those constants should come from the library.
+ * @type {{ON: string, OFF: string, AVAILABLE: string,
+ * UNAVAILABLE: string, PENDING: string}}
+ */
var Status = {
ON: "on",
OFF: "off",
@@ -184,6 +203,11 @@ var Status = {
PENDING: "pending"
};
+/**
+ * Manages the recording user interface and user experience.
+ * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
+ * setRecordingButtonState, checkAutoRecord}}
+ */
var Recording = {
/**
* Initializes the recording UI.
@@ -196,6 +220,9 @@ var Recording = {
this.initRecordingButton(recordingType);
},
+ /**
+ * Initialise the recording button.
+ */
initRecordingButton(recordingType) {
let selector = $('#toolbar_button_record');
@@ -261,7 +288,10 @@ var Recording = {
});
},
- // Shows or hides the 'recording' button.
+ /**
+ * Shows or hides the 'recording' button.
+ * @param show {true} to show the recording button, {false} to hide it
+ */
showRecordingButton (show) {
if (_isRecordingButtonEnabled() && show) {
$('#toolbar_button_record').css({display: "inline-block"});
@@ -270,6 +300,10 @@ var Recording = {
}
},
+ /**
+ * Updates the recording state UI.
+ * @param recordingState gives us the current recording state
+ */
updateRecordingState(recordingState) {
// I'm the recorder, so I don't want to see any UI related to states.
if (config.iAmRecorder)
@@ -282,7 +316,10 @@ var Recording = {
this.setRecordingButtonState(recordingState);
},
- // Sets the state of the recording button
+ /**
+ * Sets the state of the recording button.
+ * @param recordingState gives us the current recording state
+ */
setRecordingButtonState (recordingState) {
let buttonSelector = $('#toolbar_button_record');
let labelSelector = $('#recordingLabel');
diff --git a/modules/UI/toolbars/BottomToolbar.js b/modules/UI/toolbars/BottomToolbar.js
index eae97444a..4ef781203 100644
--- a/modules/UI/toolbars/BottomToolbar.js
+++ b/modules/UI/toolbars/BottomToolbar.js
@@ -12,8 +12,27 @@ const defaultBottomToolbarButtons = {
const BottomToolbar = {
init () {
this.toolbar = $('#bottomToolbar');
- },
+ // The bottom toolbar is enabled by default.
+ this.enabled = true;
+ },
+ /**
+ * Enables / disables the bottom toolbar.
+ * @param {e} set to {true} to enable the bottom toolbar or {false}
+ * to disable it
+ */
+ enable (e) {
+ this.enabled = e;
+ if (!e && this.isVisible())
+ this.hide(false);
+ },
+ /**
+ * Indicates if the bottom toolbar is currently enabled.
+ * @return {this.enabled}
+ */
+ isEnabled() {
+ return this.enabled;
+ },
setupListeners (emitter) {
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js
index 03e76ba67..7a95487ee 100644
--- a/modules/UI/toolbars/Toolbar.js
+++ b/modules/UI/toolbars/Toolbar.js
@@ -171,6 +171,9 @@ function showSipNumberInput () {
const Toolbar = {
init (eventEmitter) {
emitter = eventEmitter;
+ // The toolbar is enabled by default.
+ this.enabled = true;
+ this.toolbarSelector = $("#header");
UIUtil.hideDisabledButtons(defaultToolbarButtons);
@@ -178,14 +181,31 @@ const Toolbar = {
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
);
},
-
+ /**
+ * Enables / disables the toolbar.
+ * @param {e} set to {true} to enable the toolbar or {false}
+ * to disable it
+ */
+ enable (e) {
+ this.enabled = e;
+ if (!e && this.isVisible())
+ this.hide(false);
+ },
+ /**
+ * Indicates if the bottom toolbar is currently enabled.
+ * @return {this.enabled}
+ */
+ isEnabled() {
+ return this.enabled;
+ },
/**
* Updates the room invite url.
*/
updateRoomUrl (newRoomUrl) {
roomUrl = newRoomUrl;
- // If the invite dialog has been already opened we update the information.
+ // If the invite dialog has been already opened we update the
+ // information.
let inviteLink = document.getElementById('inviteLinkRef');
if (inviteLink) {
inviteLink.value = roomUrl;
@@ -244,14 +264,16 @@ const Toolbar = {
// checks whether desktop sharing is enabled and whether
// we have params to start automatically sharing
checkAutoEnableDesktopSharing () {
- if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) {
+ if (UIUtil.isButtonEnabled('desktop')
+ && config.autoEnableDesktopSharing) {
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
}
},
// Shows or hides SIP calls button
showSipCallButton (show) {
- if (APP.conference.sipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
+ if (APP.conference.sipGatewayEnabled()
+ && UIUtil.isButtonEnabled('sip') && show) {
$('#toolbar_button_sip').css({display: "inline-block"});
} else {
$('#toolbar_button_sip').css({display: "none"});
@@ -331,7 +353,51 @@ const Toolbar = {
* @param {boolean} muted if icon should look like muted or not
*/
markAudioIconAsMuted (muted) {
- $('#toolbar_button_mute').toggleClass("icon-microphone", !muted).toggleClass("icon-mic-disabled", muted);
+ $('#toolbar_button_mute').toggleClass("icon-microphone",
+ !muted).toggleClass("icon-mic-disabled", muted);
+ },
+
+ /**
+ * Indicates if the toolbar is currently hovered.
+ * @return {true} if the toolbar is currently hovered, {false} otherwise
+ */
+ isHovered() {
+ this.toolbarSelector.find('*').each(function () {
+ let id = $(this).attr('id');
+ if ($(`#${id}:hover`).length > 0) {
+ return true;
+ }
+ });
+ if ($("#bottomToolbar:hover").length > 0) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Returns true if this toolbar is currently visible, or false otherwise.
+ * @return true if currently visible, false - otherwise
+ */
+ isVisible() {
+ return this.toolbarSelector.is(":visible");
+ },
+
+ /**
+ * Hides the toolbar with animation or not depending on the animate
+ * parameter.
+ */
+ hide() {
+ this.toolbarSelector.hide(
+ "slide", { direction: "up", duration: 300});
+ },
+
+ /**
+ * Shows the toolbar with animation or not depending on the animate
+ * parameter.
+ */
+ show() {
+ this.toolbarSelector.show(
+ "slide", { direction: "up", duration: 300});
}
};
diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js
index ac13c6910..09bff508f 100644
--- a/modules/UI/toolbars/ToolbarToggler.js
+++ b/modules/UI/toolbars/ToolbarToggler.js
@@ -2,6 +2,7 @@
import UIUtil from '../util/UIUtil';
import BottomToolbar from './BottomToolbar';
+import Toolbar from './Toolbar';
import FilmStrip from '../videolayout/FilmStrip.js';
let toolbarTimeoutObject;
@@ -16,10 +17,6 @@ function showDesktopSharingButton() {
}
}
-function isToolbarVisible () {
- return $('#header').is(':visible');
-}
-
/**
* Hides the toolbar.
*/
@@ -28,25 +25,13 @@ function hideToolbar() {
return;
}
- let header = $("#header");
- let isToolbarHover = false;
- header.find('*').each(function () {
- let id = $(this).attr('id');
- if ($(`#${id}:hover`).length > 0) {
- isToolbarHover = true;
- }
- });
- if ($("#bottomToolbar:hover").length > 0) {
- isToolbarHover = true;
- }
-
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
- if (isToolbarHover) {
+ if (Toolbar.isHovered()) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else {
- header.hide("slide", { direction: "up", duration: 300});
+ Toolbar.hide();
$('#subject').animate({top: "-=40"}, 300);
if (!FilmStrip.isFilmStripVisible()) {
BottomToolbar.hide(true);
@@ -59,18 +44,23 @@ const ToolbarToggler = {
* Shows the main toolbar.
*/
showToolbar () {
- // if we are a recorder we do not want to show the toolbar
- if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
+ if (interfaceConfig.filmStripOnly) {
return;
}
- let header = $("#header");
- if (!header.is(':visible') || !BottomToolbar.isVisible()) {
- header.show("slide", { direction: "up", duration: 300});
- $('#subject').animate({top: "+=40"}, 300);
- if (!BottomToolbar.isVisible()) {
- BottomToolbar.show(true);
- }
+ var updateTimeout = false;
+ if (Toolbar.isEnabled() && !Toolbar.isVisible()) {
+ Toolbar.show();
+ $('#subject').animate({top: "+=40"}, 300);
+ updateTimeout = true;
+ }
+
+ if (BottomToolbar.isEnabled() && !BottomToolbar.isVisible()) {
+ BottomToolbar.show(true);
+ updateTimeout = true;
+ }
+
+ if (updateTimeout) {
if (toolbarTimeoutObject) {
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
@@ -89,13 +79,13 @@ const ToolbarToggler = {
* @param isDock indicates what operation to perform
*/
dockToolbar (isDock) {
- if (interfaceConfig.filmStripOnly) {
+ if (interfaceConfig.filmStripOnly || !Toolbar.isEnabled()) {
return;
}
if (isDock) {
// First make sure the toolbar is shown.
- if (!isToolbarVisible()) {
+ if (!Toolbar.isVisible()) {
this.showToolbar();
}
@@ -103,7 +93,7 @@ const ToolbarToggler = {
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
} else {
- if (isToolbarVisible()) {
+ if (Toolbar.isVisible()) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else {
this.showToolbar();
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index f40d2e9be..a809aaebb 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -50,7 +50,25 @@ SmallVideo.prototype.showDisplayName = function(isShow) {
}
};
+/**
+ * Enables / disables the device availability icons for this small video.
+ * @param {enable} set to {true} to enable and {false} to disable
+ */
+SmallVideo.prototype.enableDeviceAvailabilityIcons = function (enable) {
+ if (typeof enable === "undefined")
+ return;
+
+ this.deviceAvailabilityIconsEnabled = enable;
+};
+
+/**
+ * Sets the device "non" availability icons.
+ * @param devices the devices, which will be checked for availability
+ */
SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
+ if (!this.deviceAvailabilityIconsEnabled)
+ return;
+
if(!this.container)
return;
diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js
index 772e05f37..7c94da9db 100644
--- a/modules/UI/videolayout/VideoLayout.js
+++ b/modules/UI/videolayout/VideoLayout.js
@@ -196,6 +196,25 @@ var VideoLayout = {
video.setDeviceAvailabilityIcons(devices);
},
+ /**
+ * Enables/disables device availability icons for the given participant id.
+ * The default value is {true}.
+ * @param id the identifier of the participant
+ * @param enable {true} to enable device availability icons
+ */
+ enableDeviceAvailabilityIcons (id, enable) {
+ let video;
+ if (APP.conference.isLocalId(id)) {
+ video = localVideoThumbnail;
+ }
+ else if (remoteVideos[id]) {
+ video = remoteVideos[id];
+ }
+
+ if (video)
+ video.enableDeviceAvailabilityIcons(enable);
+ },
+
/**
* Checks if removed video is currently displayed and tries to display
* another one instead.
diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js
new file mode 100644
index 000000000..9f2f7d870
--- /dev/null
+++ b/modules/recorder/Recorder.js
@@ -0,0 +1,100 @@
+/* global config, APP */
+/*
+ * 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 VideoLayout from '../UI/videolayout/VideoLayout';
+import Feedback from '../UI/Feedback.js';
+import Toolbar from '../UI/toolbars/Toolbar';
+import BottomToolbar from '../UI/toolbars/BottomToolbar';
+
+const _RECORDER_CUSTOM_ROLE = "recorder-role";
+
+class Recorder {
+ /**
+ * Initializes a new {Recorder} instance.
+ *
+ * @param conference the {conference} which is to transport
+ * {Recorder}-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;
+
+ // If I am a recorder then I publish my recorder custom role to notify
+ // everyone.
+ if (config.iAmRecorder) {
+ VideoLayout.enableDeviceAvailabilityIcons(conference.localId, true);
+ this._publishMyRecorderRole();
+ Feedback.enableFeedback(false);
+ Toolbar.enable(false);
+ BottomToolbar.enable(false);
+ }
+
+ // Listen to "CUSTOM_ROLE" commands.
+ this._conference.commands.addCommandListener(
+ this._conference.commands.defaults.CUSTOM_ROLE,
+ this._onCustomRoleCommand.bind(this));
+ }
+
+ /**
+ * Publish the recorder custom role.
+ * @private
+ */
+ _publishMyRecorderRole () {
+ var conference = this._conference;
+
+ var commands = conference.commands;
+
+ commands.removeCommand(commands.defaults.CUSTOM_ROLE);
+ var self = this;
+ commands.sendCommandOnce(
+ commands.defaults.CUSTOM_ROLE,
+ {
+ attributes: {
+ recorderRole: true
+ }
+ });
+ }
+
+ /**
+ * Notifies this instance about a &qout;Custom Role&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.
+ */
+ _onCustomRoleCommand ({ 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'
+ || this._conference.isLocalId(id)
+ || !attributes.recorderRole)
+ return;
+
+ var isRecorder = (attributes.recorderRole == 'true');
+
+ if (isRecorder)
+ VideoLayout.enableDeviceAvailabilityIcons(id, isRecorder);
+ }
+}
+
+export default Recorder;