[RN] Implement streaming on mobile
This commit is contained in:
parent
453c4b99dc
commit
5aee082bf9
|
@ -467,7 +467,7 @@
|
|||
"serviceName": "Live Streaming service",
|
||||
"signIn": "Sign in with Google",
|
||||
"signInCTA": "Sign in or enter your live stream key from YouTube.",
|
||||
"start": "Start a livestream",
|
||||
"start": "Start a live stream",
|
||||
"streamIdHelp": "What's this?",
|
||||
"unavailableTitle": "Live Streaming unavailable"
|
||||
},
|
||||
|
|
|
@ -29,9 +29,20 @@ export const RECORDING_SESSION_UPDATED = Symbol('RECORDING_SESSION_UPDATED');
|
|||
*
|
||||
* {
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* streamType: string,
|
||||
* uid: ?number
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
= Symbol('SET_PENDING_RECORDING_NOTIFICATION_UID');
|
||||
|
||||
/**
|
||||
* Sets the stream key last used by the user for later reuse.
|
||||
*
|
||||
* {
|
||||
* type: SET_STREAM_KEY,
|
||||
* streamKey: string
|
||||
* }
|
||||
*/
|
||||
export const SET_STREAM_KEY = Symbol('SET_STREAM_KEY');
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
hideNotification,
|
||||
showErrorNotification,
|
||||
|
@ -9,7 +11,8 @@ import {
|
|||
import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_STREAM_KEY
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
|
@ -29,35 +32,37 @@ export function clearRecordingSessions() {
|
|||
* Signals that the pending recording notification should be removed from the
|
||||
* screen.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream (e.g. file or stream).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hidePendingRecordingNotification() {
|
||||
export function hidePendingRecordingNotification(streamType: string) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const { pendingNotificationUid } = getState()['features/recording'];
|
||||
const { pendingNotificationUids } = getState()['features/recording'];
|
||||
const pendingNotificationUid = pendingNotificationUids[streamType];
|
||||
|
||||
if (pendingNotificationUid) {
|
||||
dispatch(hideNotification(pendingNotificationUid));
|
||||
dispatch(setPendingRecordingNotificationUid());
|
||||
dispatch(
|
||||
_setPendingRecordingNotificationUid(
|
||||
undefined, streamType));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets UID of the the pending recording notification to use it when hinding
|
||||
* the notification is necessary, or unsets it when
|
||||
* undefined (or no param) is passed.
|
||||
* Sets the stream key last used by the user for later reuse.
|
||||
*
|
||||
* @param {?number} uid - The UID of the notification.
|
||||
* @param {string} streamKey - The stream key to set.
|
||||
* redux.
|
||||
* @returns {{
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* uid: number
|
||||
* type: SET_STREAM_KEY,
|
||||
* streamKey: string
|
||||
* }}
|
||||
*/
|
||||
export function setPendingRecordingNotificationUid(uid: ?number) {
|
||||
export function setLiveStreamKey(streamKey: string) {
|
||||
return {
|
||||
type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
uid
|
||||
type: SET_STREAM_KEY,
|
||||
streamKey
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,20 +70,29 @@ export function setPendingRecordingNotificationUid(uid: ?number) {
|
|||
* Signals that the pending recording notification should be shown on the
|
||||
* screen.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream (e.g. file or stream).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showPendingRecordingNotification() {
|
||||
export function showPendingRecordingNotification(streamType: string) {
|
||||
return (dispatch: Function) => {
|
||||
const showNotificationAction = showNotification({
|
||||
const isLiveStreaming
|
||||
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
const dialogProps = isLiveStreaming ? {
|
||||
descriptionKey: 'liveStreaming.pending',
|
||||
titleKey: 'dialog.liveStreaming'
|
||||
} : {
|
||||
descriptionKey: 'recording.pending',
|
||||
isDismissAllowed: false,
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
const showNotificationAction = showNotification({
|
||||
isDismissAllowed: false,
|
||||
...dialogProps
|
||||
});
|
||||
|
||||
dispatch(showNotificationAction);
|
||||
|
||||
dispatch(setPendingRecordingNotificationUid(
|
||||
showNotificationAction.uid));
|
||||
dispatch(_setPendingRecordingNotificationUid(
|
||||
showNotificationAction.uid, streamType));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -96,13 +110,21 @@ export function showRecordingError(props: Object) {
|
|||
* Signals that the stopped recording notification should be shown on the
|
||||
* screen for a given period.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream (e.g. file or stream).
|
||||
* @returns {showNotification}
|
||||
*/
|
||||
export function showStoppedRecordingNotification() {
|
||||
return showNotification({
|
||||
export function showStoppedRecordingNotification(streamType: string) {
|
||||
const isLiveStreaming
|
||||
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
const dialogProps = isLiveStreaming ? {
|
||||
descriptionKey: 'liveStreaming.off',
|
||||
titleKey: 'dialog.liveStreaming'
|
||||
} : {
|
||||
descriptionKey: 'recording.off',
|
||||
titleKey: 'dialog.recording'
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
return showNotification(dialogProps, 2500);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -127,3 +149,25 @@ export function updateRecordingSessionData(session: Object) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets UID of the the pending streaming notification to use it when hinding
|
||||
* the notification is necessary, or unsets it when undefined (or no param) is
|
||||
* passed.
|
||||
*
|
||||
* @param {?number} uid - The UID of the notification.
|
||||
* redux.
|
||||
* @param {string} streamType - The type of the stream (e.g. file or stream).
|
||||
* @returns {{
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* streamType: string,
|
||||
* uid: number
|
||||
* }}
|
||||
*/
|
||||
function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
|
||||
return {
|
||||
type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
streamType,
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
|
@ -1,128 +1,32 @@
|
|||
// @flow
|
||||
|
||||
import Spinner from '@atlaskit/spinner';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
|
||||
import googleApi from '../../googleApi';
|
||||
|
||||
import AbstractStartLiveStreamDialog, {
|
||||
_mapStateToProps,
|
||||
GOOGLE_API_STATES,
|
||||
type Props
|
||||
} from './AbstractStartLiveStreamDialog';
|
||||
import BroadcastsDropdown from './BroadcastsDropdown';
|
||||
import GoogleSignInButton from './GoogleSignInButton';
|
||||
import StreamKeyForm from './StreamKeyForm';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* An enumeration of the different states the Google API can be in while
|
||||
* interacting with {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
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
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link StartLiveStreamDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* The ID for the Google web client application used for making stream key
|
||||
* related requests.
|
||||
*/
|
||||
_googleApiApplicationClientID: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of
|
||||
* {@link StartLiveStreamDialog}.
|
||||
*/
|
||||
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
|
||||
};
|
||||
|
||||
/**
|
||||
* A React Component for requesting a YouTube stream key to use for live
|
||||
* streaming of the current conference.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StartLiveStreamDialog extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
class StartLiveStreamDialog
|
||||
extends AbstractStartLiveStreamDialog {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StartLiveStreamDialog} instance.
|
||||
|
@ -133,91 +37,17 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
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;
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
|
||||
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
|
||||
this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
|
||||
this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onYouTubeBroadcastIDSelected
|
||||
= this._onYouTubeBroadcastIDSelected.bind(this);
|
||||
|
||||
this._renderDialogContent = this._renderDialogContent.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 React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _googleApiApplicationClientID } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancelTitleKey = 'dialog.Cancel'
|
||||
okTitleKey = 'dialog.startLiveStreaming'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'liveStreaming.start'
|
||||
width = { 'small' }>
|
||||
<div className = 'live-stream-dialog'>
|
||||
{ _googleApiApplicationClientID
|
||||
? this._renderYouTubePanel() : null }
|
||||
<StreamKeyForm
|
||||
helpURL = { interfaceConfig.LIVE_STREAMING_HELP_LINK }
|
||||
onChange = { this._onStreamKeyChange }
|
||||
value = { this.state.streamKey } />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onInitializeGoogleApi: () => Object;
|
||||
_onInitializeGoogleApi: () => Promise<*>;
|
||||
|
||||
/**
|
||||
* Loads the Google web client application used for fetching stream keys.
|
||||
|
@ -247,22 +77,7 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
_onGetYouTubeBroadcasts: () => Object;
|
||||
_onGetYouTubeBroadcasts: () => Promise<*>;
|
||||
|
||||
/**
|
||||
* Asks the user to sign in, if not already signed in, and then requests a
|
||||
|
@ -322,59 +137,7 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
.then(() => this._onGetYouTubeBroadcasts());
|
||||
}
|
||||
|
||||
_onStreamKeyChange: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to update the {@code StartLiveStreamDialog} component's
|
||||
* display of the entered YouTube stream key.
|
||||
*
|
||||
* @param {Object} event - DOM Event for value change.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStreamKeyChange(event) {
|
||||
this._setStateIfMounted({
|
||||
streamKey: event.target.value,
|
||||
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, streamKey, selectedBoundStreamID } = this.state;
|
||||
|
||||
if (!streamKey) {
|
||||
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: streamKey
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
_onStreamKeyChange: string => void;
|
||||
|
||||
_onYouTubeBroadcastIDSelected: (string) => Object;
|
||||
|
||||
|
@ -452,6 +215,27 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
return (firstError && firstError.reason) || null;
|
||||
}
|
||||
|
||||
_renderDialogContent: () => React$Component<*>
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderDialogContent() {
|
||||
const { _googleApiApplicationClientID } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'live-stream-dialog'>
|
||||
{ _googleApiApplicationClientID
|
||||
? this._renderYouTubePanel() : null }
|
||||
<StreamKeyForm
|
||||
onChange = { this._onStreamKeyChange }
|
||||
value = { this.state.streamKey || this.props._streamKey } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a React Element for authenticating with the Google web client.
|
||||
*
|
||||
|
@ -537,6 +321,8 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_setStateIfMounted: Object => void
|
||||
|
||||
/**
|
||||
* Returns the error message to display for the current error state.
|
||||
*
|
||||
|
@ -559,40 +345,6 @@ class StartLiveStreamDialog extends Component<Props, State> {
|
|||
|
||||
return <div className = 'google-error'>{ text }</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props of
|
||||
* {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _googleApiApplicationClientID: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_googleApiApplicationClientID:
|
||||
state['features/base/config'].googleApiApplicationClientID
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
|
@ -1,36 +1,12 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
import AbstractStopLiveStreamDialog, {
|
||||
_mapStateToProps
|
||||
} from './AbstractStopLiveStreamDialog';
|
||||
|
||||
/**
|
||||
* A React Component for confirming the participant wishes to stop the currently
|
||||
|
@ -38,73 +14,16 @@ type Props = {
|
|||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StopLiveStreamDialog 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);
|
||||
}
|
||||
class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
* Renders the platform specific {@code Dialog} content.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.stopLiveStreaming'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.liveStreaming'
|
||||
width = 'small'>
|
||||
{ this.props.t('dialog.stopStreamingWarning') }
|
||||
</Dialog>
|
||||
);
|
||||
_renderDialogContent() {
|
||||
return this.props.t('dialog.stopStreamingWarning');
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
||||
|
||||
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,42 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import { FieldTextStateless } from '@atlaskit/field-text';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import AbstractStreamKeyForm, {
|
||||
type Props
|
||||
} from './AbstractStreamKeyForm';
|
||||
|
||||
/**
|
||||
* A React Component for entering a key for starting a YouTube live stream.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StreamKeyForm extends Component {
|
||||
/**
|
||||
* {@code StreamKeyForm} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The URL to the page with more information for manually finding the
|
||||
* stream key for a YouTube broadcast.
|
||||
*/
|
||||
helpURL: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Callback invoked when the entered stream key has changed.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The stream key value to display as having been entered so far.
|
||||
*/
|
||||
value: PropTypes.string
|
||||
};
|
||||
class StreamKeyForm extends AbstractStreamKeyForm {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StreamKeyForm} instance.
|
||||
|
@ -44,11 +22,10 @@ class StreamKeyForm extends Component {
|
|||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code StreamKeyForm} instance with.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._onOpenHelp = this._onOpenHelp.bind(this);
|
||||
}
|
||||
|
||||
|
@ -59,7 +36,7 @@ class StreamKeyForm extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { value, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'stream-key-form'>
|
||||
|
@ -69,12 +46,12 @@ class StreamKeyForm extends Component {
|
|||
isSpellCheckEnabled = { false }
|
||||
label = { t('dialog.streamKey') }
|
||||
name = 'streamId'
|
||||
okDisabled = { !this.props.value }
|
||||
okDisabled = { !value }
|
||||
onChange = { this._onInputChange }
|
||||
placeholder = { t('liveStreaming.enterStreamKey') }
|
||||
shouldFitContainer = { true }
|
||||
type = 'text'
|
||||
value = { this.props.value } />
|
||||
value = { this.state.value } />
|
||||
{ this.props.helpURL
|
||||
? <div className = 'form-footer'>
|
||||
<a
|
||||
|
@ -89,17 +66,9 @@ class StreamKeyForm extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the value of the input field has updated through
|
||||
* user input.
|
||||
*
|
||||
* @param {Object} event - DOM Event for value change.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onInputChange(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
_onInputChange: Object => void
|
||||
|
||||
_onOpenHelp: () => void
|
||||
|
||||
/**
|
||||
* Opens a new tab with information on how to manually locate a YouTube
|
||||
|
@ -109,7 +78,7 @@ class StreamKeyForm extends Component {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelp() {
|
||||
window.open(this.props.helpURL, 'noopener');
|
||||
window.open(this.helpURL, 'noopener');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
});
|
|
@ -107,9 +107,7 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
|
|||
}
|
||||
|
||||
if (typeof visible === 'undefined') {
|
||||
const visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
|
||||
|
||||
visible = visibleButtons.has('recording')
|
||||
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('recording')
|
||||
&& (abstractProps.visible || _fileRecordingsDisabledTooltipKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
|
||||
export {
|
||||
LiveStreamButton,
|
||||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog
|
||||
} from './LiveStream';
|
||||
export {
|
||||
RecordButton,
|
||||
StartRecordingDialog,
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* The Google API scopes to request access to for streaming.
|
||||
*
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
export const GOOGLE_API_SCOPES = [
|
||||
'https://www.googleapis.com/auth/youtube.readonly'
|
||||
];
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when a recording or live streaming
|
||||
* session is stopped.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { GOOGLE_API_SCOPES } from './constants';
|
||||
|
||||
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
|
||||
const GOOGLE_API_SCOPES = [
|
||||
'https://www.googleapis.com/auth/youtube.readonly'
|
||||
].join(' ');
|
||||
|
||||
/**
|
||||
* A promise for dynamically loading the Google API Client Library.
|
||||
|
@ -68,7 +67,7 @@ const googleApi = {
|
|||
setTimeout(() => {
|
||||
api.client.init({
|
||||
clientId,
|
||||
scope: GOOGLE_API_SCOPES
|
||||
scope: GOOGLE_API_SCOPES.join(' ')
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
|
|
@ -104,24 +104,31 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||
case RECORDING_SESSION_UPDATED: {
|
||||
const updatedSessionData
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
|
||||
if (updatedSessionData.mode === JitsiRecordingConstants.mode.FILE) {
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
if (updatedSessionData.status === PENDING
|
||||
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
|
||||
dispatch(
|
||||
showPendingRecordingNotification(updatedSessionData.mode));
|
||||
} else if (updatedSessionData.status !== PENDING) {
|
||||
dispatch(
|
||||
hidePendingRecordingNotification(updatedSessionData.mode));
|
||||
|
||||
if (updatedSessionData.status === PENDING
|
||||
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
|
||||
dispatch(showPendingRecordingNotification());
|
||||
} else if (updatedSessionData.status !== PENDING) {
|
||||
dispatch(hidePendingRecordingNotification());
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)
|
||||
&& updatedSessionData.mode
|
||||
=== JitsiRecordingConstants.mode.FILE) {
|
||||
dispatch(playSound(RECORDING_ON_SOUND_ID));
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
dispatch(
|
||||
showStoppedRecordingNotification(
|
||||
updatedSessionData.mode));
|
||||
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)) {
|
||||
dispatch(playSound(RECORDING_ON_SOUND_ID));
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
if (updatedSessionData.mode
|
||||
=== JitsiRecordingConstants.mode.FILE) {
|
||||
dispatch(stopSound(RECORDING_ON_SOUND_ID));
|
||||
dispatch(playSound(RECORDING_OFF_SOUND_ID));
|
||||
dispatch(showStoppedRecordingNotification());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,33 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
import { PersistenceRegistry } from '../base/storage';
|
||||
import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_STREAM_KEY
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
pendingNotificationUids: {},
|
||||
sessionDatas: []
|
||||
};
|
||||
|
||||
/**
|
||||
* The name of the Redux store this feature stores its state in.
|
||||
*/
|
||||
const STORE_NAME = 'features/recording';
|
||||
|
||||
/**
|
||||
* Sets up the persistence of the feature {@code recording}.
|
||||
*/
|
||||
PersistenceRegistry.register(STORE_NAME, {
|
||||
streamKey: true
|
||||
}, DEFAULT_STATE);
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/recording.
|
||||
*/
|
||||
ReducerRegistry.register('features/recording',
|
||||
ReducerRegistry.register(STORE_NAME,
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
|
||||
|
@ -29,10 +44,23 @@ ReducerRegistry.register('features/recording',
|
|||
_updateSessionDatas(state.sessionDatas, action.sessionData)
|
||||
};
|
||||
|
||||
case SET_PENDING_RECORDING_NOTIFICATION_UID:
|
||||
case SET_PENDING_RECORDING_NOTIFICATION_UID: {
|
||||
const pendingNotificationUids = {
|
||||
...state.pendingNotificationUids
|
||||
};
|
||||
|
||||
pendingNotificationUids[action.streamType] = action.uid;
|
||||
|
||||
return {
|
||||
...state,
|
||||
pendingNotificationUid: action.uid
|
||||
pendingNotificationUids
|
||||
};
|
||||
}
|
||||
|
||||
case SET_STREAM_KEY:
|
||||
return {
|
||||
...state,
|
||||
streamKey: action.streamKey
|
||||
};
|
||||
|
||||
default:
|
||||
|
|
|
@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
|||
import { BottomSheet, hideDialog } from '../../../base/dialog';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||
import { RecordButton } from '../../../recording';
|
||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||
import { RoomLockButton } from '../../../room-lock';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
|
@ -70,6 +70,7 @@ class OverflowMenu extends Component<Props> {
|
|||
<AudioOnlyButton { ...buttonProps } />
|
||||
<RoomLockButton { ...buttonProps } />
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<PictureInPictureButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
|
|
|
@ -11,11 +11,9 @@ import {
|
|||
} from '../../../analytics';
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipants,
|
||||
isLocalParticipantModerator,
|
||||
participantUpdated
|
||||
} from '../../../base/participants';
|
||||
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
|
||||
|
@ -30,10 +28,8 @@ import {
|
|||
} from '../../../invite';
|
||||
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
|
||||
import {
|
||||
RecordButton,
|
||||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog,
|
||||
getActiveSession
|
||||
LiveStreamButton,
|
||||
RecordButton
|
||||
} from '../../../recording';
|
||||
import {
|
||||
SETTINGS_TABS,
|
||||
|
@ -123,22 +119,6 @@ type Props = {
|
|||
*/
|
||||
_isGuest: boolean,
|
||||
|
||||
/**
|
||||
* The tooltip key to use when live streaming is disabled. Or undefined
|
||||
* if non to be shown and the button to be hidden.
|
||||
*/
|
||||
_liveStreamingDisabledTooltipKey: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the live streaming feature is enabled for use.
|
||||
*/
|
||||
_liveStreamingEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The current live streaming session, if any.
|
||||
*/
|
||||
_liveStreamingSession: ?Object,
|
||||
|
||||
/**
|
||||
* The ID of the local participant.
|
||||
*/
|
||||
|
@ -230,8 +210,6 @@ class Toolbox extends Component<Props> {
|
|||
= this._onToolbarToggleEtherpad.bind(this);
|
||||
this._onToolbarToggleFullScreen
|
||||
= this._onToolbarToggleFullScreen.bind(this);
|
||||
this._onToolbarToggleLiveStreaming
|
||||
= this._onToolbarToggleLiveStreaming.bind(this);
|
||||
this._onToolbarToggleProfile
|
||||
= this._onToolbarToggleProfile.bind(this);
|
||||
this._onToolbarToggleRaiseHand
|
||||
|
@ -476,22 +454,6 @@ class Toolbox extends Component<Props> {
|
|||
this.props.dispatch(setFullScreen(fullScreen));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to show a dialog for starting or stopping a live
|
||||
* streaming session.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleLiveStreaming() {
|
||||
const { _liveStreamingSession } = this.props;
|
||||
const dialogToDisplay = _liveStreamingSession
|
||||
? StopLiveStreamDialog : StartLiveStreamDialog;
|
||||
|
||||
this.props.dispatch(
|
||||
openDialog(dialogToDisplay, { session: _liveStreamingSession }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to show or hide the profile edit panel.
|
||||
*
|
||||
|
@ -790,25 +752,6 @@ class Toolbox extends Component<Props> {
|
|||
this._doToggleFullScreen();
|
||||
}
|
||||
|
||||
_onToolbarToggleLiveStreaming: () => void;
|
||||
|
||||
/**
|
||||
* Starts the process for enabling or disabling live streaming.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleLiveStreaming() {
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'livestreaming.button',
|
||||
{
|
||||
'is_streaming': Boolean(this.props._liveStreamingSession),
|
||||
type: JitsiRecordingConstants.mode.STREAM
|
||||
}));
|
||||
|
||||
this._doToggleLiveStreaming();
|
||||
}
|
||||
|
||||
_onToolbarToggleProfile: () => void;
|
||||
|
||||
/**
|
||||
|
@ -919,43 +862,6 @@ class Toolbox extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an {@code OverflowMenuItem} to start or stop live streaming of
|
||||
* the current conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLiveStreamingButton() {
|
||||
const {
|
||||
_liveStreamingDisabledTooltipKey,
|
||||
_liveStreamingEnabled,
|
||||
_liveStreamingSession,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const translationKey = _liveStreamingSession
|
||||
? 'dialog.stopLiveStreaming'
|
||||
: 'dialog.startLiveStreaming';
|
||||
|
||||
return (
|
||||
<OverflowMenuItem
|
||||
accessibilityLabel
|
||||
= { t('dialog.accessibilityLabel.liveStreaming') }
|
||||
disabled = { !_liveStreamingEnabled }
|
||||
elementAfter = {
|
||||
<span className = 'beta-tag'>
|
||||
{ t('recording.beta') }
|
||||
</span>
|
||||
}
|
||||
icon = 'icon-public'
|
||||
key = 'livestreaming'
|
||||
onClick = { this._onToolbarToggleLiveStreaming }
|
||||
text = { t(translationKey) }
|
||||
tooltip = { t(_liveStreamingDisabledTooltipKey) } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list elements of the overflow menu.
|
||||
*
|
||||
|
@ -969,8 +875,6 @@ class Toolbox extends Component<Props> {
|
|||
_feedbackConfigured,
|
||||
_fullScreen,
|
||||
_isGuest,
|
||||
_liveStreamingDisabledTooltipKey,
|
||||
_liveStreamingEnabled,
|
||||
_sharingVideo,
|
||||
t
|
||||
} = this.props;
|
||||
|
@ -997,9 +901,9 @@ class Toolbox extends Component<Props> {
|
|||
text = { _fullScreen
|
||||
? t('toolbar.exitFullScreen')
|
||||
: t('toolbar.enterFullScreen') } />,
|
||||
(_liveStreamingEnabled || _liveStreamingDisabledTooltipKey)
|
||||
&& this._shouldShowButton('livestreaming')
|
||||
&& this._renderLiveStreamingButton(),
|
||||
<LiveStreamButton
|
||||
key = 'livestreaming'
|
||||
showLabel = { true } />,
|
||||
<RecordButton
|
||||
key = 'record'
|
||||
showLabel = { true } />,
|
||||
|
@ -1086,7 +990,6 @@ function _mapStateToProps(state) {
|
|||
callStatsID,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
let { liveStreamingEnabled } = state['features/base/config'];
|
||||
const sharedVideoStatus = state['features/shared-video'].status;
|
||||
const { current } = state['features/side-panel'];
|
||||
const {
|
||||
|
@ -1102,10 +1005,6 @@ function _mapStateToProps(state) {
|
|||
const dialOutEnabled = isDialOutEnabled(state);
|
||||
|
||||
let desktopSharingDisabledTooltipKey;
|
||||
let liveStreamingDisabledTooltipKey;
|
||||
|
||||
liveStreamingEnabled
|
||||
= isLocalParticipantModerator(state) && liveStreamingEnabled;
|
||||
|
||||
if (state['features/base/config'].enableFeaturesBasedOnToken) {
|
||||
// we enable desktop sharing if any participant already have this
|
||||
|
@ -1122,27 +1021,6 @@ function _mapStateToProps(state) {
|
|||
desktopSharingDisabledTooltipKey
|
||||
= 'dialog.shareYourScreenDisabled';
|
||||
}
|
||||
|
||||
// we enable recording if the local participant have this
|
||||
// feature enabled
|
||||
const { features = {} } = localParticipant;
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
|
||||
liveStreamingEnabled
|
||||
= liveStreamingEnabled && String(features.livestreaming) === 'true';
|
||||
|
||||
// if the feature is disabled on purpose, do no show it, no tooltip
|
||||
if (!liveStreamingEnabled
|
||||
&& String(features.livestreaming) !== 'disabled') {
|
||||
// button and tooltip
|
||||
if (isGuest) {
|
||||
liveStreamingDisabledTooltipKey
|
||||
= 'dialog.liveStreamingDisabledForGuestTooltip';
|
||||
} else {
|
||||
liveStreamingDisabledTooltipKey
|
||||
= 'dialog.liveStreamingDisabledTooltip';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1158,10 +1036,6 @@ function _mapStateToProps(state) {
|
|||
iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
|
||||
_isGuest: state['features/base/jwt'].isGuest,
|
||||
_fullScreen: fullScreen,
|
||||
_liveStreamingDisabledTooltipKey: liveStreamingDisabledTooltipKey,
|
||||
_liveStreamingEnabled: liveStreamingEnabled,
|
||||
_liveStreamingSession:
|
||||
getActiveSession(state, JitsiRecordingConstants.mode.STREAM),
|
||||
_localParticipantID: localParticipant.id,
|
||||
_overflowMenuVisible: overflowMenuVisible,
|
||||
_raisedHand: localParticipant.raisedHand,
|
||||
|
|
Loading…
Reference in New Issue