Fix Recording regression caused by 'React Toolbar'

Saúl Ibarra Corretgé reported that Recording shows an error dialog
stating "There was an error connecting to your camera". Hristo Terezov
and Yana Stamcheva traced that the problem originates in
da4425b5c0
and, more specifically, is caused by a different order of execution due
to the move of the invocation of the function Recording.init.

The solution is to bring back the execution location of Recording.init.
This commit is contained in:
Lyubo Marinov 2017-04-12 14:06:56 -05:00
parent 0316450ee2
commit ae06a6ce41
7 changed files with 208 additions and 158 deletions

View File

@ -322,6 +322,10 @@ UI.start = function () {
$("#videoconference_page").mousemove(debouncedShowToolbar); $("#videoconference_page").mousemove(debouncedShowToolbar);
// Initialise the recording module.
if (config.enableRecording) {
Recording.init(eventEmitter, config.recordingType);
}
// Initialize side panels // Initialize side panels
SidePanels.init(eventEmitter); SidePanels.init(eventEmitter);
} else { } else {

View File

@ -21,7 +21,7 @@ import UIUtil from '../util/UIUtil';
import VideoLayout from '../videolayout/VideoLayout'; import VideoLayout from '../videolayout/VideoLayout';
import Feedback from '../feedback/Feedback.js'; import Feedback from '../feedback/Feedback.js';
import { hideToolbox } from '../../../react/features/toolbox'; import { setToolboxEnabled } from '../../../react/features/toolbox';
/** /**
* The dialog for user input. * The dialog for user input.
@ -35,8 +35,10 @@ let dialog = null;
* @private * @private
*/ */
function _isRecordingButtonEnabled() { function _isRecordingButtonEnabled() {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1 return (
&& config.enableRecording && APP.conference.isRecordingSupported(); interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1
&& config.enableRecording
&& APP.conference.isRecordingSupported());
} }
/** /**
@ -129,7 +131,7 @@ function _requestLiveStreamId() {
* Request recording token from the user. * Request recording token from the user.
* @returns {Promise} * @returns {Promise}
*/ */
function _requestRecordingToken () { function _requestRecordingToken() {
let titleKey = "dialog.recordingToken"; let titleKey = "dialog.recordingToken";
let messageString = ( let messageString = (
`<input name="recordingToken" type="text" `<input name="recordingToken" type="text"
@ -164,7 +166,7 @@ function _requestRecordingToken () {
* @returns {Promise} * @returns {Promise}
* @private * @private
*/ */
function _showStopRecordingPrompt (recordingType) { function _showStopRecordingPrompt(recordingType) {
var title; var title;
var message; var message;
var buttonKey; var buttonKey;
@ -179,19 +181,13 @@ function _showStopRecordingPrompt (recordingType) {
buttonKey = "dialog.stopRecording"; buttonKey = "dialog.stopRecording";
} }
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
dialog = APP.UI.messageHandler.openTwoButtonDialog({ dialog = APP.UI.messageHandler.openTwoButtonDialog({
titleKey: title, titleKey: title,
msgKey: message, msgKey: message,
leftButtonKey: buttonKey, leftButtonKey: buttonKey,
submitFunction: function(e,v) { submitFunction: (e, v) => (v ? resolve : reject)(),
if (v) { closeFunction: () => {
resolve();
} else {
reject();
}
},
closeFunction: function () {
dialog = null; dialog = null;
} }
}); });
@ -250,35 +246,12 @@ var Recording = {
/** /**
* Initializes the recording UI. * Initializes the recording UI.
*/ */
init (emitter, recordingType) { init(eventEmitter, recordingType) {
this.eventEmitter = emitter; this.eventEmitter = eventEmitter;
this.recordingType = recordingType;
this.updateRecordingState(APP.conference.getRecordingState()); this.updateRecordingState(APP.conference.getRecordingState());
this.initRecordingButton(recordingType);
// If I am a recorder then I publish my recorder custom role to notify
// everyone.
if (config.iAmRecorder) {
VideoLayout.enableDeviceAvailabilityIcons(
APP.conference.getMyUserId(), false);
VideoLayout.setLocalVideoVisible(false);
Feedback.enableFeedback(false);
APP.store.dispatch(hideToolbox());
APP.UI.messageHandler.enableNotifications(false);
APP.UI.messageHandler.enablePopups(false);
}
},
/**
* Initialise the recording button.
*/
initRecordingButton(recordingType) {
let selector = $('#toolbar_button_record');
let button = selector.get(0);
UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
if (recordingType === 'jibri') { if (recordingType === 'jibri') {
this.baseClass = "fa fa-play-circle"; this.baseClass = "fa fa-play-circle";
this.recordingTitle = "dialog.liveStreaming"; this.recordingTitle = "dialog.liveStreaming";
@ -304,101 +277,44 @@ var Recording = {
this.recordingBusy = "liveStreaming.busy"; this.recordingBusy = "liveStreaming.busy";
} }
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
// toolbar_button_record may not exist yet.
$(document).on(
'click',
'#toolbar_button_record',
ev => this._onToolbarButtonClick(ev));
// If I am a recorder then I publish my recorder custom role to notify
// everyone.
if (config.iAmRecorder) {
VideoLayout.enableDeviceAvailabilityIcons(
APP.conference.getMyUserId(), false);
VideoLayout.setLocalVideoVisible(false);
Feedback.enableFeedback(false);
APP.store.dispatch(setToolboxEnabled(false));
APP.UI.messageHandler.enableNotifications(false);
APP.UI.messageHandler.enablePopups(false);
}
},
/**
* Initialise the recording button.
*/
initRecordingButton() {
const selector = $('#toolbar_button_record');
UIUtil.setTooltip(selector, 'liveStreaming.buttonTooltip', 'right');
selector.addClass(this.baseClass); selector.addClass(this.baseClass);
selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip); selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip);
APP.translation.translateElement(selector); APP.translation.translateElement(selector);
var self = this;
selector.click(function () {
if (dialog)
return;
JitsiMeetJS.analytics.sendEvent('recording.clicked');
switch (self.currentState) {
case Status.ON:
case Status.RETRYING:
case Status.PENDING: {
_showStopRecordingPrompt(recordingType).then(
() => {
self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
JitsiMeetJS.analytics.sendEvent(
'recording.stopped');
},
() => {});
break;
}
case Status.AVAILABLE:
case Status.OFF: {
if (recordingType === 'jibri')
_requestLiveStreamId().then((streamId) => {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{streamId: streamId});
JitsiMeetJS.analytics.sendEvent(
'recording.started');
}).catch(
reason => {
if (reason !== APP.UI.messageHandler.CANCEL)
logger.error(reason);
else
JitsiMeetJS.analytics.sendEvent(
'recording.canceled');
}
);
else {
if (self.predefinedToken) {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{token: self.predefinedToken});
JitsiMeetJS.analytics.sendEvent(
'recording.started');
return;
}
_requestRecordingToken().then((token) => {
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
{token: token});
JitsiMeetJS.analytics.sendEvent(
'recording.started');
}).catch(
reason => {
if (reason !== APP.UI.messageHandler.CANCEL)
logger.error(reason);
else
JitsiMeetJS.analytics.sendEvent(
'recording.canceled');
}
);
}
break;
}
case Status.BUSY: {
dialog = APP.UI.messageHandler.openMessageDialog(
self.recordingTitle,
self.recordingBusy,
null,
function () {
dialog = null;
}
);
break;
}
default: {
dialog = APP.UI.messageHandler.openMessageDialog(
self.recordingTitle,
self.recordingUnavailable,
null,
function () {
dialog = null;
}
);
}
}
});
}, },
/** /**
* Shows or hides the 'recording' button. * Shows or hides the 'recording' button.
* @param show {true} to show the recording button, {false} to hide it * @param show {true} to show the recording button, {false} to hide it
*/ */
showRecordingButton (show) { showRecordingButton(show) {
let shouldShow = show && _isRecordingButtonEnabled(); let shouldShow = show && _isRecordingButtonEnabled();
let id = 'toolbar_button_record'; let id = 'toolbar_button_record';
@ -425,7 +341,7 @@ var Recording = {
* Sets the state of the recording button. * Sets the state of the recording button.
* @param recordingState gives us the current recording state * @param recordingState gives us the current recording state
*/ */
updateRecordingUI (recordingState) { updateRecordingUI(recordingState) {
let oldState = this.currentState; let oldState = this.currentState;
this.currentState = recordingState; this.currentState = recordingState;
@ -491,7 +407,7 @@ var Recording = {
}, },
// checks whether recording is enabled and whether we have params // checks whether recording is enabled and whether we have params
// to start automatically recording // to start automatically recording
checkAutoRecord () { checkAutoRecord() {
if (_isRecordingButtonEnabled && config.autoRecord) { if (_isRecordingButtonEnabled && config.autoRecord) {
this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken); this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED, this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
@ -514,6 +430,90 @@ var Recording = {
APP.translation.translateElement(labelSelector); APP.translation.translateElement(labelSelector);
}, },
/**
* Handles {@code click} on {@code toolbar_button_record}.
*
* @returns {void}
*/
_onToolbarButtonClick() {
if (dialog) {
return;
}
JitsiMeetJS.analytics.sendEvent('recording.clicked');
switch (this.currentState) {
case Status.ON:
case Status.RETRYING:
case Status.PENDING: {
_showStopRecordingPrompt(this.recordingType).then(
() => {
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
JitsiMeetJS.analytics.sendEvent('recording.stopped');
},
() => {});
break;
}
case Status.AVAILABLE:
case Status.OFF: {
if (this.recordingType === 'jibri')
_requestLiveStreamId().then(streamId => {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED,
{ streamId });
JitsiMeetJS.analytics.sendEvent('recording.started');
}).catch(reason => {
if (reason !== APP.UI.messageHandler.CANCEL)
logger.error(reason);
else
JitsiMeetJS.analytics.sendEvent('recording.canceled');
});
else {
if (this.predefinedToken) {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED,
{ token: this.predefinedToken });
JitsiMeetJS.analytics.sendEvent('recording.started');
return;
}
_requestRecordingToken().then((token) => {
this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED,
{ token });
JitsiMeetJS.analytics.sendEvent('recording.started');
}).catch(reason => {
if (reason !== APP.UI.messageHandler.CANCEL)
logger.error(reason);
else
JitsiMeetJS.analytics.sendEvent('recording.canceled');
});
}
break;
}
case Status.BUSY: {
dialog = APP.UI.messageHandler.openMessageDialog(
this.recordingTitle,
this.recordingBusy,
null,
() => {
dialog = null;
}
);
break;
}
default: {
dialog = APP.UI.messageHandler.openMessageDialog(
this.recordingTitle,
this.recordingUnavailable,
null,
() => {
dialog = null;
}
);
}
}
},
/** /**
* Sets the toggled state of the recording toolbar button. * Sets the toggled state of the recording toolbar button.
* *

View File

@ -157,11 +157,13 @@ const IndicatorFontSizes = {
* @param position the position of the tooltip in relation to the element * @param position the position of the tooltip in relation to the element
*/ */
setTooltip(element, key, position) { setTooltip(element, key, position) {
if (element !== null) { if (element) {
element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]); const selector = element.jquery ? element : $(element);
element.setAttribute('data-i18n', '[content]' + key);
APP.translation.translateElement($(element)); selector.attr('data-tooltip', TOOLTIP_POSITIONS[position]);
selector.attr('data-i18n', `[content]${key}`);
APP.translation.translateElement(selector);
} }
}, },

View File

@ -21,16 +21,6 @@ export const CLEAR_TOOLBOX_TIMEOUT = Symbol('CLEAR_TOOLBOX_TIMEOUT');
export const SET_DEFAULT_TOOLBOX_BUTTONS export const SET_DEFAULT_TOOLBOX_BUTTONS
= Symbol('SET_DEFAULT_TOOLBOX_BUTTONS'); = Symbol('SET_DEFAULT_TOOLBOX_BUTTONS');
/**
* The type of the action which sets the permanent visibility of the Toolbox.
*
* {
* type: SET_TOOLBOX_ALWAYS_VISIBLE,
* alwaysVisible: boolean
* }
*/
export const SET_TOOLBOX_ALWAYS_VISIBLE = Symbol('SET_TOOLBOX_ALWAYS_VISIBLE');
/** /**
* The type of the action which sets the conference subject. * The type of the action which sets the conference subject.
* *
@ -73,6 +63,26 @@ export const SET_TOOLBAR_BUTTON = Symbol('SET_TOOLBAR_BUTTON');
*/ */
export const SET_TOOLBAR_HOVERED = Symbol('SET_TOOLBAR_HOVERED'); export const SET_TOOLBAR_HOVERED = Symbol('SET_TOOLBAR_HOVERED');
/**
* The type of the action which sets the permanent visibility of the Toolbox.
*
* {
* type: SET_TOOLBOX_ALWAYS_VISIBLE,
* alwaysVisible: boolean
* }
*/
export const SET_TOOLBOX_ALWAYS_VISIBLE = Symbol('SET_TOOLBOX_ALWAYS_VISIBLE');
/**
* The type of the (redux) action which enables/disables the Toolbox.
*
* {
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }
*/
export const SET_TOOLBOX_ENABLED = Symbol('SET_TOOLBOX_ENABLED');
/** /**
* The type of the action which sets a new Toolbox visibility timeout and its * The type of the action which sets a new Toolbox visibility timeout and its
* delay. * delay.

View File

@ -5,11 +5,12 @@ import type { Dispatch } from 'redux-thunk';
import { import {
CLEAR_TOOLBOX_TIMEOUT, CLEAR_TOOLBOX_TIMEOUT,
SET_DEFAULT_TOOLBOX_BUTTONS, SET_DEFAULT_TOOLBOX_BUTTONS,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_SUBJECT, SET_SUBJECT,
SET_SUBJECT_SLIDE_IN, SET_SUBJECT_SLIDE_IN,
SET_TOOLBAR_BUTTON, SET_TOOLBAR_BUTTON,
SET_TOOLBAR_HOVERED, SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT, SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS, SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE SET_TOOLBOX_VISIBLE
@ -168,6 +169,22 @@ export function setToolboxAlwaysVisible(alwaysVisible: boolean): Object {
/* eslint-disable flowtype/space-before-type-colon */ /* eslint-disable flowtype/space-before-type-colon */
/**
* Enables/disables the toolbox.
*
* @param {boolean} enabled - True to enable the toolbox or false to disable it.
* @returns {{
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }}
*/
export function setToolboxEnabled(enabled: boolean): Object {
return {
type: SET_TOOLBOX_ENABLED,
enabled
};
}
/** /**
* Dispatches an action which sets new timeout and clears the previous one. * Dispatches an action which sets new timeout and clears the previous one.
* *

View File

@ -3,8 +3,8 @@
import Recording from '../../../modules/UI/recording/Recording'; import Recording from '../../../modules/UI/recording/Recording';
import SideContainerToggler import SideContainerToggler
from '../../../modules/UI/side_pannels/SideContainerToggler'; from '../../../modules/UI/side_pannels/SideContainerToggler';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../../../modules/UI/util/UIUtil'; import UIUtil from '../../../modules/UI/util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import { import {
clearToolboxTimeout, clearToolboxTimeout,
@ -171,14 +171,11 @@ export function showDialPadButton(show: boolean): Function {
*/ */
export function showRecordingButton(): Function { export function showRecordingButton(): Function {
return (dispatch: Dispatch<*>) => { return (dispatch: Dispatch<*>) => {
const eventEmitter = APP.UI.eventEmitter; dispatch(setToolbarButton('recording', {
const buttonName = 'recording';
dispatch(setToolbarButton(buttonName, {
hidden: false hidden: false
})); }));
Recording.init(eventEmitter, config.recordingType); Recording.initRecordingButton();
}; };
} }
@ -234,9 +231,14 @@ export function showSIPCallButton(show: boolean): Function {
export function showToolbox(timeout: number = 0): Object { export function showToolbox(timeout: number = 0): Object {
return (dispatch: Dispatch<*>, getState: Function) => { return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState(); const state = getState();
const { alwaysVisible, timeoutMS, visible } = state['features/toolbox']; const {
alwaysVisible,
enabled,
timeoutMS,
visible
} = state['features/toolbox'];
if (!visible) { if (enabled && !visible) {
dispatch(setToolboxVisible(true)); dispatch(setToolboxVisible(true));
dispatch(setSubjectSlideIn(true)); dispatch(setSubjectSlideIn(true));

View File

@ -5,11 +5,12 @@ import { ReducerRegistry } from '../base/redux';
import { import {
CLEAR_TOOLBOX_TIMEOUT, CLEAR_TOOLBOX_TIMEOUT,
SET_DEFAULT_TOOLBOX_BUTTONS, SET_DEFAULT_TOOLBOX_BUTTONS,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_SUBJECT, SET_SUBJECT,
SET_SUBJECT_SLIDE_IN, SET_SUBJECT_SLIDE_IN,
SET_TOOLBAR_BUTTON, SET_TOOLBAR_BUTTON,
SET_TOOLBAR_HOVERED, SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT, SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS, SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE SET_TOOLBOX_VISIBLE
@ -51,6 +52,14 @@ function _getInitialState() {
*/ */
alwaysVisible: false, alwaysVisible: false,
/**
* The indicator which determines whether the Toolbox is enabled. For
* example, modules/UI/recording/Recording.js disables the Toolbox.
*
* @type {boolean}
*/
enabled: true,
/** /**
* The indicator which determines whether a Toolbar in the Toolbox is * The indicator which determines whether a Toolbar in the Toolbox is
* hovered. * hovered.
@ -132,12 +141,6 @@ ReducerRegistry.register(
}; };
} }
case SET_TOOLBOX_ALWAYS_VISIBLE:
return {
...state,
alwaysVisible: action.alwaysVisible
};
case SET_SUBJECT: case SET_SUBJECT:
return { return {
...state, ...state,
@ -159,6 +162,18 @@ ReducerRegistry.register(
hovered: action.hovered hovered: action.hovered
}; };
case SET_TOOLBOX_ALWAYS_VISIBLE:
return {
...state,
alwaysVisible: action.alwaysVisible
};
case SET_TOOLBOX_ENABLED:
return {
...state,
enabled: action.enabled
};
case SET_TOOLBOX_TIMEOUT: case SET_TOOLBOX_TIMEOUT:
return { return {
...state, ...state,