diff --git a/lang/main.json b/lang/main.json
index 24199d190..bf769362e 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -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 get it from here!",
"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."
}
}
diff --git a/modules/FollowMe.js b/modules/FollowMe.js
index 0d5086809..a4b18f812 100644
--- a/modules/FollowMe.js
+++ b/modules/FollowMe.js
@@ -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;
diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js
index 463538bd9..457385112 100644
--- a/modules/UI/Feedback.js
+++ b/modules/UI/Feedback.js
@@ -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.
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 4e4a9d99f..97cdddf82 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -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,
- `
${msg}
- `,
- 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;
diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js
new file mode 100644
index 000000000..38e65504e
--- /dev/null
+++ b/modules/UI/recording/Recording.js
@@ -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:
+ `