mirror of https://github.com/jitsi/jitsi-meet
parent
453c4b99dc
commit
5aee082bf9
@ -0,0 +1,114 @@ |
||||
// @flow
|
||||
|
||||
import { openDialog } from '../../../base/dialog'; |
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; |
||||
import { |
||||
isLocalParticipantModerator, |
||||
getLocalParticipant |
||||
} from '../../../base/participants'; |
||||
import { |
||||
AbstractButton, |
||||
type AbstractButtonProps |
||||
} from '../../../base/toolbox'; |
||||
|
||||
import { getActiveSession } from '../../functions'; |
||||
|
||||
import StartLiveStreamDialog from './StartLiveStreamDialog'; |
||||
import StopLiveStreamDialog from './StopLiveStreamDialog'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link AbstractLiveStreamButton}. |
||||
*/ |
||||
export type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* True if there is a running active live stream, false otherwise. |
||||
*/ |
||||
_isLiveStreamRunning: boolean, |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The i18n translate function. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* An abstract class of a button for starting and stopping live streaming. |
||||
*/ |
||||
export default class AbstractLiveStreamButton<P: Props> |
||||
extends AbstractButton<P, *> { |
||||
accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming'; |
||||
label = 'dialog.startLiveStreaming'; |
||||
toggledLabel = 'dialog.stopLiveStreaming'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { _isLiveStreamRunning, dispatch } = this.props; |
||||
|
||||
dispatch(openDialog( |
||||
_isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog |
||||
)); |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether this button is in toggled state or not. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isToggled() { |
||||
return this.props._isLiveStreamRunning; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated props for the |
||||
* {@code AbstractLiveStreamButton} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @param {Props} ownProps - The own props of the Component. |
||||
* @private |
||||
* @returns {{ |
||||
* _isLiveStreamRunning: boolean, |
||||
* visible: boolean |
||||
* }} |
||||
*/ |
||||
export function _mapStateToProps(state: Object, ownProps: Props) { |
||||
let { visible } = ownProps; |
||||
|
||||
if (typeof visible === 'undefined') { |
||||
// If the containing component provides the visible prop, that is one
|
||||
// above all, but if not, the button should be autonomus and decide on
|
||||
// its own to be visible or not.
|
||||
const isModerator = isLocalParticipantModerator(state); |
||||
const { |
||||
enableFeaturesBasedOnToken, |
||||
liveStreamingEnabled |
||||
} = state['features/base/config']; |
||||
const { features = {} } = getLocalParticipant(state); |
||||
|
||||
visible = isModerator |
||||
&& liveStreamingEnabled |
||||
&& (!enableFeaturesBasedOnToken |
||||
|| String(features.livestreaming) === 'true'); |
||||
} |
||||
|
||||
return { |
||||
_isLiveStreamRunning: Boolean( |
||||
getActiveSession(state, JitsiRecordingConstants.mode.STREAM)), |
||||
visible |
||||
}; |
||||
} |
@ -0,0 +1,336 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { |
||||
createRecordingDialogEvent, |
||||
sendAnalytics |
||||
} from '../../../analytics'; |
||||
import { Dialog } from '../../../base/dialog'; |
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link AbstractStartLiveStreamDialog}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* The {@code JitsiConference} for the current conference. |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* The ID for the Google client application used for making stream key |
||||
* related requests. |
||||
*/ |
||||
_googleApiApplicationClientID: string, |
||||
|
||||
/** |
||||
* The live stream key that was used before. |
||||
*/ |
||||
_streamKey: string, |
||||
|
||||
/** |
||||
* The Redux dispatch function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of |
||||
* {@link AbstractStartLiveStreamDialog}. |
||||
*/ |
||||
export type State = { |
||||
|
||||
/** |
||||
* Details about the broadcasts available for use for the logged in Google |
||||
* user's YouTube account. |
||||
*/ |
||||
broadcasts: ?Array<Object>, |
||||
|
||||
/** |
||||
* The error type, as provided by Google, for the most recent error |
||||
* encountered by the Google API. |
||||
*/ |
||||
errorType: ?string, |
||||
|
||||
/** |
||||
* The current state of interactions with the Google API. Determines what |
||||
* Google related UI should display. |
||||
*/ |
||||
googleAPIState: number, |
||||
|
||||
/** |
||||
* The email of the user currently logged in to the Google web client |
||||
* application. |
||||
*/ |
||||
googleProfileEmail: string, |
||||
|
||||
/** |
||||
* The boundStreamID of the broadcast currently selected in the broadcast |
||||
* dropdown. |
||||
*/ |
||||
selectedBoundStreamID: ?string, |
||||
|
||||
/** |
||||
* The selected or entered stream key to use for YouTube live streaming. |
||||
*/ |
||||
streamKey: string |
||||
}; |
||||
|
||||
/** |
||||
* An enumeration of the different states the Google API can be in while |
||||
* interacting with {@code StartLiveStreamDialog}. |
||||
* |
||||
* @private |
||||
* @type {Object} |
||||
*/ |
||||
export const GOOGLE_API_STATES = { |
||||
/** |
||||
* The state in which the Google API still needs to be loaded. |
||||
*/ |
||||
NEEDS_LOADING: 0, |
||||
|
||||
/** |
||||
* The state in which the Google API is loaded and ready for use. |
||||
*/ |
||||
LOADED: 1, |
||||
|
||||
/** |
||||
* The state in which a user has been logged in through the Google API. |
||||
*/ |
||||
SIGNED_IN: 2, |
||||
|
||||
/** |
||||
* The state in which the Google API encountered an error either loading |
||||
* or with an API request. |
||||
*/ |
||||
ERROR: 3 |
||||
}; |
||||
|
||||
/** |
||||
* Implements an abstract class for the StartLiveStreamDialog on both platforms. |
||||
* |
||||
* NOTE: Google log-in is not supported for mobile yet for later implementation |
||||
* but the abstraction of its properties are already present in this abstract |
||||
* class. |
||||
*/ |
||||
export default class AbstractStartLiveStreamDialog |
||||
extends Component<Props, State> { |
||||
_isMounted: boolean; |
||||
|
||||
/** |
||||
* Constructor of the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
broadcasts: undefined, |
||||
errorType: undefined, |
||||
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING, |
||||
googleProfileEmail: '', |
||||
selectedBoundStreamID: undefined, |
||||
streamKey: '' |
||||
}; |
||||
|
||||
/** |
||||
* Instance variable used to flag whether the component is or is not |
||||
* mounted. Used as a hack to avoid setting state on an unmounted |
||||
* component. |
||||
* |
||||
* @private |
||||
* @type {boolean} |
||||
*/ |
||||
this._isMounted = false; |
||||
|
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onStreamKeyChange = this._onStreamKeyChange.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements {@link Component#componentDidMount()}. Invoked immediately |
||||
* after this component is mounted. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
this._isMounted = true; |
||||
|
||||
if (this.props._googleApiApplicationClientID) { |
||||
this._onInitializeGoogleApi(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentWillUnmount()}. Invoked |
||||
* immediately before this component is unmounted and destroyed. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
this._isMounted = false; |
||||
} |
||||
|
||||
/** |
||||
* Implements {@code Component}'s render. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Dialog |
||||
cancelTitleKey = 'dialog.Cancel' |
||||
okTitleKey = 'dialog.startLiveStreaming' |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'liveStreaming.start' |
||||
width = { 'small' }> |
||||
{ |
||||
this._renderDialogContent() |
||||
} |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_onCancel: () => boolean; |
||||
|
||||
/** |
||||
* Invokes the passed in {@link onCancel} callback and closes |
||||
* {@code StartLiveStreamDialog}. |
||||
* |
||||
* @private |
||||
* @returns {boolean} True is returned to close the modal. |
||||
*/ |
||||
_onCancel() { |
||||
sendAnalytics(createRecordingDialogEvent('start', 'cancel.button')); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Asks the user to sign in, if not already signed in, and then requests a |
||||
* list of the user's YouTube broadcasts. |
||||
* |
||||
* NOTE: To be implemented by platforms. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_onGetYouTubeBroadcasts: () => Promise<*>; |
||||
|
||||
/** |
||||
* Loads the Google client application used for fetching stream keys. |
||||
* If the user is already logged in, then a request for available YouTube |
||||
* broadcasts is also made. |
||||
*/ |
||||
_onInitializeGoogleApi: () => Object; |
||||
|
||||
_onStreamKeyChange: string => void; |
||||
|
||||
/** |
||||
* Callback invoked to update the {@code StartLiveStreamDialog} component's |
||||
* display of the entered YouTube stream key. |
||||
* |
||||
* @param {string} streamKey - The stream key entered in the field. |
||||
* changed text. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onStreamKeyChange(streamKey) { |
||||
this._setStateIfMounted({ |
||||
streamKey, |
||||
selectedBoundStreamID: undefined |
||||
}); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
|
||||
/** |
||||
* Invokes the passed in {@link onSubmit} callback with the entered stream |
||||
* key, and then closes {@code StartLiveStreamDialog}. |
||||
* |
||||
* @private |
||||
* @returns {boolean} False if no stream key is entered to preventing |
||||
* closing, true to close the modal. |
||||
*/ |
||||
_onSubmit() { |
||||
const { broadcasts, selectedBoundStreamID } = this.state; |
||||
const key = this.state.streamKey || this.props._streamKey; |
||||
|
||||
if (!key) { |
||||
return false; |
||||
} |
||||
|
||||
let selectedBroadcastID = null; |
||||
|
||||
if (selectedBoundStreamID) { |
||||
const selectedBroadcast = broadcasts && broadcasts.find( |
||||
broadcast => broadcast.boundStreamID === selectedBoundStreamID); |
||||
|
||||
selectedBroadcastID = selectedBroadcast && selectedBroadcast.id; |
||||
} |
||||
|
||||
sendAnalytics( |
||||
createRecordingDialogEvent('start', 'confirm.button')); |
||||
|
||||
this.props._conference.startRecording({ |
||||
broadcastId: selectedBroadcastID, |
||||
mode: JitsiRecordingConstants.mode.STREAM, |
||||
streamId: key |
||||
}); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Updates the internal state if the component is still mounted. This is a |
||||
* workaround for all the state setting that occurs after ajax. |
||||
* |
||||
* @param {Object} newState - The new state to merge into the existing |
||||
* state. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setStateIfMounted(newState) { |
||||
if (this._isMounted) { |
||||
this.setState(newState); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Renders the platform specific dialog content. |
||||
* |
||||
* @returns {React$Component} |
||||
*/ |
||||
_renderDialogContent: () => React$Component<*> |
||||
} |
||||
|
||||
/** |
||||
* Maps part of the Redux state to the component's props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {{ |
||||
* _conference: Object, |
||||
* _googleApiApplicationClientID: string, |
||||
* _streamKey: string |
||||
* }} |
||||
*/ |
||||
export function _mapStateToProps(state: Object) { |
||||
return { |
||||
_conference: state['features/base/conference'].conference, |
||||
_googleApiApplicationClientID: |
||||
state['features/base/config'].googleApiApplicationClientID, |
||||
_streamKey: state['features/recording'].streamKey |
||||
}; |
||||
} |
@ -0,0 +1,119 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { Dialog } from '../../../base/dialog'; |
||||
import { |
||||
createRecordingDialogEvent, |
||||
sendAnalytics |
||||
} from '../../../analytics'; |
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; |
||||
|
||||
import { getActiveSession } from '../../functions'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link StopLiveStreamDialog}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The {@code JitsiConference} for the current conference. |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* The redux representation of the live stremaing to be stopped. |
||||
*/ |
||||
_session: Object, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* A React Component for confirming the participant wishes to stop the currently |
||||
* active live stream of the conference. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
export default class AbstractStopLiveStreamDialog extends Component<Props> { |
||||
/** |
||||
* Initializes a new {@code StopLiveStreamDialog} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Dialog |
||||
okTitleKey = 'dialog.stopLiveStreaming' |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'dialog.liveStreaming' |
||||
width = 'small'> |
||||
{ this._renderDialogContent() } |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_onSubmit: () => boolean; |
||||
|
||||
/** |
||||
* Callback invoked when stopping of live streaming is confirmed. |
||||
* |
||||
* @private |
||||
* @returns {boolean} True to close the modal. |
||||
*/ |
||||
_onSubmit() { |
||||
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button')); |
||||
|
||||
const { _session } = this.props; |
||||
|
||||
if (_session) { |
||||
this.props._conference.stopRecording(_session.id); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Function to be implemented by the platform specific implementations. |
||||
* |
||||
* @private |
||||
* @returns {React$Component<*>} |
||||
*/ |
||||
_renderDialogContent: () => React$Component<*> |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the React {@code Component} props of |
||||
* {@code StopLiveStreamDialog}. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _conference: Object, |
||||
* _session: Object |
||||
* }} |
||||
*/ |
||||
export function _mapStateToProps(state: Object) { |
||||
return { |
||||
_conference: state['features/base/conference'].conference, |
||||
_session: getActiveSession(state, JitsiRecordingConstants.mode.STREAM) |
||||
}; |
||||
} |
@ -0,0 +1,107 @@ |
||||
// @flow
|
||||
|
||||
import { Component } from 'react'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
/** |
||||
* The live streaming help link to display. On web it comes from |
||||
* interfaceConfig, but we don't have that on mobile. |
||||
* |
||||
* FIXME: This is in props now to prepare for the Redux-based interfaceConfig |
||||
*/ |
||||
const LIVE_STREAMING_HELP_LINK = 'https://jitsi.org/live'; |
||||
|
||||
/** |
||||
* The props of the component. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* Callback invoked when the entered stream key has changed. |
||||
*/ |
||||
onChange: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* The stream key value to display as having been entered so far. |
||||
*/ |
||||
value: string |
||||
}; |
||||
|
||||
/** |
||||
* The state of the component. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* The value entered in the field. |
||||
*/ |
||||
value: string |
||||
} |
||||
|
||||
/** |
||||
* An abstract React Component for entering a key for starting a YouTube live |
||||
* stream. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
export default class AbstractStreamKeyForm extends Component<Props, State> { |
||||
helpURL: string; |
||||
|
||||
/** |
||||
* Constructor for the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
value: props.value |
||||
}; |
||||
|
||||
this.helpURL = (typeof interfaceConfig !== 'undefined' |
||||
&& interfaceConfig.LIVE_STREAMING_HELP_LINK) |
||||
|| LIVE_STREAMING_HELP_LINK; |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onInputChange = this._onInputChange.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements {@code Component}'s componentWillReceiveProps. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillReceiveProps(newProps: Props) { |
||||
this.setState({ |
||||
value: newProps.value |
||||
}); |
||||
} |
||||
|
||||
_onInputChange: Object => void |
||||
|
||||
/** |
||||
* Callback invoked when the value of the input field has updated through |
||||
* user input. This forwards the value (string only, even if it was a dom |
||||
* event) to the onChange prop provided to the component. |
||||
* |
||||
* @param {Object | string} change - DOM Event for value change or the |
||||
* changed text. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onInputChange(change) { |
||||
const value = typeof change === 'object' ? change.target.value : change; |
||||
|
||||
this.setState({ |
||||
value |
||||
}); |
||||
this.props.onChange(value); |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import AbstractLiveStreamButton, { |
||||
_mapStateToProps, |
||||
type Props |
||||
} from './AbstractLiveStreamButton'; |
||||
|
||||
/** |
||||
* An implementation of a button for starting and stopping live streaming. |
||||
*/ |
||||
class LiveStreamButton extends AbstractLiveStreamButton<Props> { |
||||
iconName = 'public'; |
||||
toggledIconName = 'public'; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(LiveStreamButton)); |
@ -0,0 +1,122 @@ |
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { getLocalParticipant } from '../../../base/participants'; |
||||
|
||||
import AbstractLiveStreamButton, { |
||||
_mapStateToProps as _abstractMapStateToProps, |
||||
type Props as AbstractProps |
||||
} from './AbstractLiveStreamButton'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
type Props = AbstractProps & { |
||||
|
||||
/** |
||||
* True if the button should be disabled, false otherwise. |
||||
* |
||||
* NOTE: On web, if the feature is not disabled on purpose, then we still |
||||
* show the button but disabled and with a tooltip rendered on it, |
||||
* explaining why it's not available. |
||||
*/ |
||||
_disabled: boolean, |
||||
|
||||
/** |
||||
* Tooltip for the button when it's disabled in a certain way. |
||||
*/ |
||||
_liveStreamDisabledTooltipKey: ?string |
||||
} |
||||
|
||||
/** |
||||
* An implementation of a button for starting and stopping live streaming. |
||||
*/ |
||||
class LiveStreamButton extends AbstractLiveStreamButton<Props> { |
||||
iconName = 'icon-public'; |
||||
toggledIconName = 'icon-public'; |
||||
|
||||
/** |
||||
* Constructor of the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.tooltip = props._liveStreamDisabledTooltipKey; |
||||
} |
||||
|
||||
/** |
||||
* Implements {@code Component}'s componentWillReceiveProps. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillReceiveProps(newProps: Props) { |
||||
this.tooltip = newProps._liveStreamDisabledTooltipKey; |
||||
} |
||||
|
||||
/** |
||||
* Helper function to be implemented by subclasses, which must return a |
||||
* boolean value indicating if this button is disabled or not. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isDisabled() { |
||||
return this.props._disabled; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated props for the |
||||
* {@code LiveStreamButton} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @param {Props} ownProps - The own props of the Component. |
||||
* @private |
||||
* @returns {{ |
||||
* _conference: Object, |
||||
* _isLiveStreamRunning: boolean, |
||||
* _disabled: boolean, |
||||
* visible: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state: Object, ownProps: Props) { |
||||
const abstractProps = _abstractMapStateToProps(state, ownProps); |
||||
const localParticipant = getLocalParticipant(state); |
||||
const { features = {} } = localParticipant; |
||||
let { visible } = ownProps; |
||||
|
||||
let _disabled = false; |
||||
let _liveStreamDisabledTooltipKey; |
||||
|
||||
if (!abstractProps.visible |
||||
&& String(features.livestreaming) !== 'disabled') { |
||||
_disabled = true; |
||||
|
||||
// button and tooltip
|
||||
if (state['features/base/jwt'].isGuest) { |
||||
_liveStreamDisabledTooltipKey |
||||
= 'dialog.liveStreamingDisabledForGuestTooltip'; |
||||
} else { |
||||
_liveStreamDisabledTooltipKey |
||||
= 'dialog.liveStreamingDisabledTooltip'; |
||||
} |
||||
} |
||||
|
||||
if (typeof visible === 'undefined') { |
||||
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('livestreaming') |
||||
&& (abstractProps.visible || _liveStreamDisabledTooltipKey); |
||||
} |
||||
|
||||
return { |
||||
...abstractProps, |
||||
_disabled, |
||||
_liveStreamDisabledTooltipKey, |
||||
visible |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(LiveStreamButton)); |
@ -0,0 +1,92 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { View } from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import { setLiveStreamKey } from '../../actions'; |
||||
|
||||
import AbstractStartLiveStreamDialog, { |
||||
_mapStateToProps, |
||||
type Props |
||||
} from './AbstractStartLiveStreamDialog'; |
||||
import StreamKeyForm from './StreamKeyForm'; |
||||
|
||||
/** |
||||
* A React Component for requesting a YouTube stream key to use for live |
||||
* streaming of the current conference. |
||||
*/ |
||||
class StartLiveStreamDialog extends AbstractStartLiveStreamDialog { |
||||
/** |
||||
* Constructor of the component. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this); |
||||
this._onStreamKeyChangeNative |
||||
= this._onStreamKeyChangeNative.bind(this); |
||||
this._renderDialogContent = this._renderDialogContent.bind(this); |
||||
} |
||||
|
||||
_onInitializeGoogleApi: () => Promise<*> |
||||
|
||||
/** |
||||
* Loads the Google client application used for fetching stream keys. |
||||
* If the user is already logged in, then a request for available YouTube |
||||
* broadcasts is also made. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_onInitializeGoogleApi() { |
||||
// This is a placeholder method for the Google feature.
|
||||
return Promise.resolve(); |
||||
} |
||||
|
||||
_onStreamKeyChange: string => void |
||||
|
||||
_onStreamKeyChangeNative: string => void; |
||||
|
||||
/** |
||||
* Callback to handle stream key changes. |
||||
* |
||||
* FIXME: This is a temporary method to store the streaming key on mobile |
||||
* for easier use, until the Google sign-in is implemented. We don't store |
||||
* the key on web for security reasons (e.g. we don't want to have the key |
||||
* stored if the used signed out). |
||||
* |
||||
* @private |
||||
* @param {string} streamKey - The new key value. |
||||
* @returns {void} |
||||
*/ |
||||
_onStreamKeyChangeNative(streamKey) { |
||||
this.props.dispatch(setLiveStreamKey(streamKey)); |
||||
this._onStreamKeyChange(streamKey); |
||||
} |
||||
|
||||
_renderDialogContent: () => React$Component<*> |
||||
|
||||
/** |
||||
* Renders the platform specific dialog content. |
||||
* |
||||
* @returns {React$Component} |
||||
*/ |
||||
_renderDialogContent() { |
||||
return ( |
||||
<View> |
||||
<StreamKeyForm |
||||
onChange = { this._onStreamKeyChangeNative } |
||||
value = { this.props._streamKey } /> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog)); |
@ -0,0 +1,41 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Text, View } from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import styles from '../styles'; |
||||
|
||||
import AbstractStopLiveStreamDialog, { |
||||
_mapStateToProps |
||||
} from './AbstractStopLiveStreamDialog'; |
||||
|
||||
/** |
||||
* A React Component for confirming the participant wishes to stop the currently |
||||
* active live stream of the conference. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class StopLiveStreamDialog extends AbstractStopLiveStreamDialog { |
||||
|
||||
/** |
||||
* Renders the platform specific {@code Dialog} content. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
_renderDialogContent() { |
||||
return ( |
||||
<View style = { styles.messageContainer }> |
||||
<Text> |
||||
{ |
||||
this.props.t('dialog.stopStreamingWarning') |
||||
} |
||||
</Text> |
||||
</View> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(StopLiveStreamDialog)); |
@ -0,0 +1,86 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { Linking, Text, TextInput, TouchableOpacity, View } from 'react-native'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
import AbstractStreamKeyForm, { |
||||
type Props |
||||
} from './AbstractStreamKeyForm'; |
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* A React Component for entering a key for starting a YouTube live stream. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class StreamKeyForm extends AbstractStreamKeyForm { |
||||
/** |
||||
* Initializes a new {@code StreamKeyForm} instance. |
||||
* |
||||
* @param {Props} props - The React {@code Component} props to initialize |
||||
* the new {@code StreamKeyForm} instance with. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onOpenHelp = this._onOpenHelp.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
|
||||
return ( |
||||
<View style = { styles.streamKeyFormWrapper }> |
||||
<Text style = { styles.streamKeyInputLabel }> |
||||
{ |
||||
t('dialog.streamKey') |
||||
} |
||||
</Text> |
||||
<TextInput |
||||
onChangeText = { this._onInputChange } |
||||
placeholder = { t('liveStreaming.enterStreamKey') } |
||||
style = { styles.streamKeyInput } |
||||
value = { this.state.value } /> |
||||
<TouchableOpacity |
||||
onPress = { this._onOpenHelp } |
||||
style = { styles.streamKeyHelp } > |
||||
<Text> |
||||
{ |
||||
t('liveStreaming.streamIdHelp') |
||||
} |
||||
</Text> |
||||
</TouchableOpacity> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_onInputChange: Object => void |
||||
|
||||
_onOpenHelp: () => void |
||||
|
||||
/** |
||||
* Opens the information link on how to manually locate a YouTube broadcast |
||||
* stream key. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onOpenHelp() { |
||||
const { helpURL } = this; |
||||
|
||||
if (typeof helpURL === 'string') { |
||||
Linking.openURL(helpURL); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export default translate(StreamKeyForm); |
@ -1,2 +1,3 @@ |
||||
export { default as LiveStreamButton } from './LiveStreamButton'; |
||||
export { default as StartLiveStreamDialog } from './StartLiveStreamDialog'; |
||||
export { default as StopLiveStreamDialog } from './StopLiveStreamDialog'; |
||||
|
@ -0,0 +1,28 @@ |
||||
// @flow
|
||||
|
||||
import { BoxModel, createStyleSheet } from '../../../base/styles'; |
||||
|
||||
/** |
||||
* The styles of the React {@code Components} of LiveStream. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
|
||||
streamKeyFormWrapper: { |
||||
flexDirection: 'column', |
||||
padding: BoxModel.padding |
||||
}, |
||||
|
||||
streamKeyHelp: { |
||||
alignSelf: 'flex-end' |
||||
}, |
||||
|
||||
streamKeyInput: { |
||||
alignSelf: 'stretch', |
||||
height: 50 |
||||
}, |
||||
|
||||
streamKeyInputLabel: { |
||||
alignSelf: 'flex-start' |
||||
} |
||||
|
||||
}); |
Loading…
Reference in new issue