diff --git a/conference.js b/conference.js index 90d8bc007..dc2ba0249 100644 --- a/conference.js +++ b/conference.js @@ -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) => { diff --git a/css/main.css b/css/main.css index e3a442e74..a164f8516 100644 --- a/css/main.css +++ b/css/main.css @@ -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 { @@ -298,7 +283,7 @@ div.feedbackButton:hover { } .active { - color: #00ccff; + background-color: #00ccff; } .bottomToolbar_span>span { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 5b585738f..ed00174e4 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -488,4 +488,34 @@ padding: 10px; 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: 4px; + -webkit-transition: all 2s 2s linear; + transition: all 2s 2s linear; +} + +.moveToCorner { + top: 5px; + right: 5px; + margin-right: 0px; + margin-left: auto; + background: rgba(0,0,0,.3); + color: rgba(255,255,255,.5); } \ No newline at end of file diff --git a/index.html b/index.html index b77017611..1e2608db7 100644 --- a/index.html +++ b/index.html @@ -116,7 +116,7 @@ - + @@ -154,6 +154,7 @@ HD +
diff --git a/lang/main.json b/lang/main.json index 24199d190..bcbca615a 100644 --- a/lang/main.json +++ b/lang/main.json @@ -51,7 +51,7 @@ "mute": "Mute / Unmute", "videomute": "Start / stop camera", "authenticate": "Authenticate", - "record": "Record", + "record": "Toggle recording", "lock": "Lock / unlock room", "invite": "Invite others", "chat": "Open / close chat", @@ -179,6 +179,7 @@ "joinAgain": "Join again", "Share": "Share", "Save": "Save", + "recording": "Recording", "recordingToken": "Enter recording token", "Dial": "Dial", "sipMsg": "Enter SIP number", @@ -206,7 +207,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 +262,17 @@ }, "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" + }, + "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" } } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 4e4a9d99f..ce3492332 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"; @@ -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(); } }; @@ -977,37 +982,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 () { diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js new file mode 100644 index 000000000..3c55d749d --- /dev/null +++ b/modules/UI/recording/Recording.js @@ -0,0 +1,289 @@ +/* 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'; + +/** + * Recording. + */ +let recordingToaster = null; + +function _isRecordingButtonEnabled() { + return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1 + && config.enableRecording; +} + +/** + * Request live stream token from the user. + * @returns {Promise} + */ +function _requestLiveStreamId() { + let msg = APP.translation.generateTranslationHTML("dialog.liveStreaming"); + let token = APP.translation.translateString("dialog.streamKey"); + return new Promise(function (resolve, reject) { + APP.UI.messageHandler.openTwoButtonDialog( + null, null, null, + `

${msg}

+ `, + false, "dialog.startLiveStreaming", + function (e, v, m, f) { + if (v && f.streamId) { + resolve(UIUtil.escapeHtml(f.streamId)); + } else { + reject(); + } + }, + null, + function () { }, + ':input:first' + ); + }); +} + +/** + * 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, + `

${msg}

+ `, + 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 Recording = { + /** + * Initializes the recording UI. + */ + init (emitter, recordingType) { + this.eventEmitter = emitter; + + 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"; + } + else { + this.baseClass = "icon-recEnable"; + this.recordingOnKey = "recording.on"; + this.recordingOffKey = "recording.off"; + this.recordingPendingKey = "recording.pending"; + this.failedToStartKey = "recording.failedToStart"; + } + + selector.addClass(this.baseClass); + + var self = this; + selector.click(function () { + console.log("BUTTON CLICKED", self.currentState); + switch (self.currentState) { + case "on": { + _showStopRecordingPrompt(recordingType).then(() => + self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED)); + } + break; + case "available": + case "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) { + // 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 === '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 === 'off' + || recordingState === 'unavailable') { + + // We don't want to do any changes if this is + // an availability change. + if (this.currentState === "available" + || this.currentState === "unavailable") + return; + + buttonSelector.removeClass(this.baseClass + " active"); + buttonSelector.addClass(this.baseClass); + + moveToCorner(labelSelector, false); + let messageKey; + if (this.currentState === "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 === '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; + + if (!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; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 687e91783..03e76ba67 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -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 diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index f34c2544b..ab284805f 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -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) { diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5c091481c..5da292339 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -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",