Przeglądaj źródła

[RN] Implement streaming on mobile

master
Bettenbuk Zoltan 7 lat temu
rodzic
commit
5aee082bf9
25 zmienionych plików z 1281 dodań i 600 usunięć
  1. 1
    1
      lang/main.json
  2. 11
    0
      react/features/recording/actionTypes.js
  3. 65
    21
      react/features/recording/actions.js
  4. 114
    0
      react/features/recording/components/LiveStream/AbstractLiveStreamButton.js
  5. 336
    0
      react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js
  6. 119
    0
      react/features/recording/components/LiveStream/AbstractStopLiveStreamDialog.js
  7. 107
    0
      react/features/recording/components/LiveStream/AbstractStreamKeyForm.js
  8. 20
    0
      react/features/recording/components/LiveStream/LiveStreamButton.native.js
  9. 122
    0
      react/features/recording/components/LiveStream/LiveStreamButton.web.js
  10. 92
    0
      react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js
  11. 35
    283
      react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js
  12. 41
    0
      react/features/recording/components/LiveStream/StopLiveStreamDialog.native.js
  13. 7
    88
      react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js
  14. 86
    0
      react/features/recording/components/LiveStream/StreamKeyForm.native.js
  15. 16
    47
      react/features/recording/components/LiveStream/StreamKeyForm.web.js
  16. 1
    0
      react/features/recording/components/LiveStream/index.js
  17. 28
    0
      react/features/recording/components/LiveStream/styles.js
  18. 1
    3
      react/features/recording/components/Recording/RecordButton.web.js
  19. 5
    1
      react/features/recording/components/index.js
  20. 9
    0
      react/features/recording/constants.js
  21. 3
    4
      react/features/recording/googleApi.js
  22. 23
    16
      react/features/recording/middleware.js
  23. 32
    4
      react/features/recording/reducer.js
  24. 2
    1
      react/features/toolbox/components/native/OverflowMenu.js
  25. 5
    131
      react/features/toolbox/components/web/Toolbox.js

+ 1
- 1
lang/main.json Wyświetl plik

@@ -467,7 +467,7 @@
467 467
         "serviceName": "Live Streaming service",
468 468
         "signIn": "Sign in with Google",
469 469
         "signInCTA": "Sign in or enter your live stream key from YouTube.",
470
-        "start": "Start a livestream",
470
+        "start": "Start a live stream",
471 471
         "streamIdHelp": "What's this?",
472 472
         "unavailableTitle": "Live Streaming unavailable"
473 473
     },

+ 11
- 0
react/features/recording/actionTypes.js Wyświetl plik

@@ -29,9 +29,20 @@ export const RECORDING_SESSION_UPDATED = Symbol('RECORDING_SESSION_UPDATED');
29 29
  *
30 30
  * {
31 31
  *     type: SET_PENDING_RECORDING_NOTIFICATION_UID,
32
+ *     streamType: string,
32 33
  *     uid: ?number
33 34
  * }
34 35
  * @public
35 36
  */
36 37
 export const SET_PENDING_RECORDING_NOTIFICATION_UID
37 38
     = Symbol('SET_PENDING_RECORDING_NOTIFICATION_UID');
39
+
40
+/**
41
+ * Sets the stream key last used by the user for later reuse.
42
+ *
43
+ * {
44
+ *     type: SET_STREAM_KEY,
45
+ *     streamKey: string
46
+ * }
47
+ */
48
+export const SET_STREAM_KEY = Symbol('SET_STREAM_KEY');

+ 65
- 21
react/features/recording/actions.js Wyświetl plik

@@ -1,5 +1,7 @@
1 1
 // @flow
2 2
 
3
+import JitsiMeetJS from '../base/lib-jitsi-meet';
4
+
3 5
 import {
4 6
     hideNotification,
5 7
     showErrorNotification,
@@ -9,7 +11,8 @@ import {
9 11
 import {
10 12
     CLEAR_RECORDING_SESSIONS,
11 13
     RECORDING_SESSION_UPDATED,
12
-    SET_PENDING_RECORDING_NOTIFICATION_UID
14
+    SET_PENDING_RECORDING_NOTIFICATION_UID,
15
+    SET_STREAM_KEY
13 16
 } from './actionTypes';
14 17
 
15 18
 /**
@@ -29,35 +32,37 @@ export function clearRecordingSessions() {
29 32
  * Signals that the pending recording notification should be removed from the
30 33
  * screen.
31 34
  *
35
+ * @param {string} streamType - The type of the stream (e.g. file or stream).
32 36
  * @returns {Function}
33 37
  */
34
-export function hidePendingRecordingNotification() {
38
+export function hidePendingRecordingNotification(streamType: string) {
35 39
     return (dispatch: Function, getState: Function) => {
36
-        const { pendingNotificationUid } = getState()['features/recording'];
40
+        const { pendingNotificationUids } = getState()['features/recording'];
41
+        const pendingNotificationUid = pendingNotificationUids[streamType];
37 42
 
38 43
         if (pendingNotificationUid) {
39 44
             dispatch(hideNotification(pendingNotificationUid));
40
-            dispatch(setPendingRecordingNotificationUid());
45
+            dispatch(
46
+                _setPendingRecordingNotificationUid(
47
+                    undefined, streamType));
41 48
         }
42 49
     };
43 50
 }
44 51
 
45 52
 /**
46
- * Sets UID of the the pending recording notification to use it when hinding
47
- * the notification is necessary, or unsets it when
48
- * undefined (or no param) is passed.
53
+ * Sets the stream key last used by the user for later reuse.
49 54
  *
50
- * @param {?number} uid - The UID of the notification.
55
+ * @param {string} streamKey - The stream key to set.
51 56
  * redux.
52 57
  * @returns {{
53
- *     type: SET_PENDING_RECORDING_NOTIFICATION_UID,
54
- *     uid: number
58
+ *     type: SET_STREAM_KEY,
59
+ *     streamKey: string
55 60
  * }}
56 61
  */
57
-export function setPendingRecordingNotificationUid(uid: ?number) {
62
+export function setLiveStreamKey(streamKey: string) {
58 63
     return {
59
-        type: SET_PENDING_RECORDING_NOTIFICATION_UID,
60
-        uid
64
+        type: SET_STREAM_KEY,
65
+        streamKey
61 66
     };
62 67
 }
63 68
 
@@ -65,20 +70,29 @@ export function setPendingRecordingNotificationUid(uid: ?number) {
65 70
  * Signals that the pending recording notification should be shown on the
66 71
  * screen.
67 72
  *
73
+ * @param {string} streamType - The type of the stream (e.g. file or stream).
68 74
  * @returns {Function}
69 75
  */
70
-export function showPendingRecordingNotification() {
76
+export function showPendingRecordingNotification(streamType: string) {
71 77
     return (dispatch: Function) => {
72
-        const showNotificationAction = showNotification({
78
+        const isLiveStreaming
79
+            = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
80
+        const dialogProps = isLiveStreaming ? {
81
+            descriptionKey: 'liveStreaming.pending',
82
+            titleKey: 'dialog.liveStreaming'
83
+        } : {
73 84
             descriptionKey: 'recording.pending',
74
-            isDismissAllowed: false,
75 85
             titleKey: 'dialog.recording'
86
+        };
87
+        const showNotificationAction = showNotification({
88
+            isDismissAllowed: false,
89
+            ...dialogProps
76 90
         });
77 91
 
78 92
         dispatch(showNotificationAction);
79 93
 
80
-        dispatch(setPendingRecordingNotificationUid(
81
-            showNotificationAction.uid));
94
+        dispatch(_setPendingRecordingNotificationUid(
95
+            showNotificationAction.uid, streamType));
82 96
     };
83 97
 }
84 98
 
@@ -96,13 +110,21 @@ export function showRecordingError(props: Object) {
96 110
  * Signals that the stopped recording notification should be shown on the
97 111
  * screen for a given period.
98 112
  *
113
+ * @param {string} streamType - The type of the stream (e.g. file or stream).
99 114
  * @returns {showNotification}
100 115
  */
101
-export function showStoppedRecordingNotification() {
102
-    return showNotification({
116
+export function showStoppedRecordingNotification(streamType: string) {
117
+    const isLiveStreaming
118
+        = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
119
+    const dialogProps = isLiveStreaming ? {
120
+        descriptionKey: 'liveStreaming.off',
121
+        titleKey: 'dialog.liveStreaming'
122
+    } : {
103 123
         descriptionKey: 'recording.off',
104 124
         titleKey: 'dialog.recording'
105
-    }, 2500);
125
+    };
126
+
127
+    return showNotification(dialogProps, 2500);
106 128
 }
107 129
 
108 130
 /**
@@ -127,3 +149,25 @@ export function updateRecordingSessionData(session: Object) {
127 149
         }
128 150
     };
129 151
 }
152
+
153
+/**
154
+ * Sets UID of the the pending streaming notification to use it when hinding
155
+ * the notification is necessary, or unsets it when undefined (or no param) is
156
+ * passed.
157
+ *
158
+ * @param {?number} uid - The UID of the notification.
159
+ * redux.
160
+ * @param {string} streamType - The type of the stream (e.g. file or stream).
161
+ * @returns {{
162
+ *     type: SET_PENDING_RECORDING_NOTIFICATION_UID,
163
+ *     streamType: string,
164
+ *     uid: number
165
+ * }}
166
+ */
167
+function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
168
+    return {
169
+        type: SET_PENDING_RECORDING_NOTIFICATION_UID,
170
+        streamType,
171
+        uid
172
+    };
173
+}

+ 114
- 0
react/features/recording/components/LiveStream/AbstractLiveStreamButton.js Wyświetl plik

@@ -0,0 +1,114 @@
1
+// @flow
2
+
3
+import { openDialog } from '../../../base/dialog';
4
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
5
+import {
6
+    isLocalParticipantModerator,
7
+    getLocalParticipant
8
+} from '../../../base/participants';
9
+import {
10
+    AbstractButton,
11
+    type AbstractButtonProps
12
+} from '../../../base/toolbox';
13
+
14
+import { getActiveSession } from '../../functions';
15
+
16
+import StartLiveStreamDialog from './StartLiveStreamDialog';
17
+import StopLiveStreamDialog from './StopLiveStreamDialog';
18
+
19
+/**
20
+ * The type of the React {@code Component} props of
21
+ * {@link AbstractLiveStreamButton}.
22
+ */
23
+export type Props = AbstractButtonProps & {
24
+
25
+    /**
26
+     * True if there is a running active live stream, false otherwise.
27
+     */
28
+    _isLiveStreamRunning: boolean,
29
+
30
+    /**
31
+     * The redux {@code dispatch} function.
32
+     */
33
+    dispatch: Function,
34
+
35
+    /**
36
+     * The i18n translate function.
37
+     */
38
+    t: Function
39
+};
40
+
41
+/**
42
+ * An abstract class of a button for starting and stopping live streaming.
43
+ */
44
+export default class AbstractLiveStreamButton<P: Props>
45
+    extends AbstractButton<P, *> {
46
+    accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming';
47
+    label = 'dialog.startLiveStreaming';
48
+    toggledLabel = 'dialog.stopLiveStreaming';
49
+
50
+    /**
51
+     * Handles clicking / pressing the button.
52
+     *
53
+     * @override
54
+     * @protected
55
+     * @returns {void}
56
+     */
57
+    _handleClick() {
58
+        const { _isLiveStreamRunning, dispatch } = this.props;
59
+
60
+        dispatch(openDialog(
61
+            _isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
62
+        ));
63
+    }
64
+
65
+    /**
66
+     * Indicates whether this button is in toggled state or not.
67
+     *
68
+     * @override
69
+     * @protected
70
+     * @returns {boolean}
71
+     */
72
+    _isToggled() {
73
+        return this.props._isLiveStreamRunning;
74
+    }
75
+}
76
+
77
+/**
78
+ * Maps (parts of) the redux state to the associated props for the
79
+ * {@code AbstractLiveStreamButton} component.
80
+ *
81
+ * @param {Object} state - The Redux state.
82
+ * @param {Props} ownProps - The own props of the Component.
83
+ * @private
84
+ * @returns {{
85
+ *     _isLiveStreamRunning: boolean,
86
+ *     visible: boolean
87
+ * }}
88
+ */
89
+export function _mapStateToProps(state: Object, ownProps: Props) {
90
+    let { visible } = ownProps;
91
+
92
+    if (typeof visible === 'undefined') {
93
+        // If the containing component provides the visible prop, that is one
94
+        // above all, but if not, the button should be autonomus and decide on
95
+        // its own to be visible or not.
96
+        const isModerator = isLocalParticipantModerator(state);
97
+        const {
98
+            enableFeaturesBasedOnToken,
99
+            liveStreamingEnabled
100
+        } = state['features/base/config'];
101
+        const { features = {} } = getLocalParticipant(state);
102
+
103
+        visible = isModerator
104
+            && liveStreamingEnabled
105
+            && (!enableFeaturesBasedOnToken
106
+                || String(features.livestreaming) === 'true');
107
+    }
108
+
109
+    return {
110
+        _isLiveStreamRunning: Boolean(
111
+            getActiveSession(state, JitsiRecordingConstants.mode.STREAM)),
112
+        visible
113
+    };
114
+}

+ 336
- 0
react/features/recording/components/LiveStream/AbstractStartLiveStreamDialog.js Wyświetl plik

@@ -0,0 +1,336 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import {
6
+    createRecordingDialogEvent,
7
+    sendAnalytics
8
+} from '../../../analytics';
9
+import { Dialog } from '../../../base/dialog';
10
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
11
+
12
+/**
13
+ * The type of the React {@code Component} props of
14
+ * {@link AbstractStartLiveStreamDialog}.
15
+ */
16
+export type Props = {
17
+
18
+    /**
19
+     * The {@code JitsiConference} for the current conference.
20
+     */
21
+    _conference: Object,
22
+
23
+    /**
24
+     * The ID for the Google client application used for making stream key
25
+     * related requests.
26
+     */
27
+    _googleApiApplicationClientID: string,
28
+
29
+    /**
30
+     * The live stream key that was used before.
31
+     */
32
+    _streamKey: string,
33
+
34
+    /**
35
+     * The Redux dispatch function.
36
+     */
37
+    dispatch: Function,
38
+
39
+    /**
40
+     * Invoked to obtain translated strings.
41
+     */
42
+    t: Function
43
+}
44
+
45
+/**
46
+ * The type of the React {@code Component} state of
47
+ * {@link AbstractStartLiveStreamDialog}.
48
+ */
49
+export type State = {
50
+
51
+    /**
52
+     * Details about the broadcasts available for use for the logged in Google
53
+     * user's YouTube account.
54
+     */
55
+    broadcasts: ?Array<Object>,
56
+
57
+    /**
58
+     * The error type, as provided by Google, for the most recent error
59
+     * encountered by the Google API.
60
+     */
61
+    errorType: ?string,
62
+
63
+    /**
64
+     * The current state of interactions with the Google API. Determines what
65
+     * Google related UI should display.
66
+     */
67
+    googleAPIState: number,
68
+
69
+    /**
70
+     * The email of the user currently logged in to the Google web client
71
+     * application.
72
+     */
73
+    googleProfileEmail: string,
74
+
75
+    /**
76
+     * The boundStreamID of the broadcast currently selected in the broadcast
77
+     * dropdown.
78
+     */
79
+    selectedBoundStreamID: ?string,
80
+
81
+    /**
82
+     * The selected or entered stream key to use for YouTube live streaming.
83
+     */
84
+    streamKey: string
85
+};
86
+
87
+/**
88
+ * An enumeration of the different states the Google API can be in while
89
+ * interacting with {@code StartLiveStreamDialog}.
90
+  *
91
+ * @private
92
+ * @type {Object}
93
+ */
94
+export const GOOGLE_API_STATES = {
95
+    /**
96
+     * The state in which the Google API still needs to be loaded.
97
+     */
98
+    NEEDS_LOADING: 0,
99
+
100
+    /**
101
+     * The state in which the Google API is loaded and ready for use.
102
+     */
103
+    LOADED: 1,
104
+
105
+    /**
106
+     * The state in which a user has been logged in through the Google API.
107
+     */
108
+    SIGNED_IN: 2,
109
+
110
+    /**
111
+     * The state in which the Google API encountered an error either loading
112
+     * or with an API request.
113
+     */
114
+    ERROR: 3
115
+};
116
+
117
+/**
118
+ * Implements an abstract class for the StartLiveStreamDialog on both platforms.
119
+ *
120
+ * NOTE: Google log-in is not supported for mobile yet for later implementation
121
+ * but the abstraction of its properties are already present in this abstract
122
+ * class.
123
+ */
124
+export default class AbstractStartLiveStreamDialog
125
+    extends Component<Props, State> {
126
+    _isMounted: boolean;
127
+
128
+    /**
129
+     * Constructor of the component.
130
+     *
131
+     * @inheritdoc
132
+     */
133
+    constructor(props: Props) {
134
+        super(props);
135
+
136
+        this.state = {
137
+            broadcasts: undefined,
138
+            errorType: undefined,
139
+            googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
140
+            googleProfileEmail: '',
141
+            selectedBoundStreamID: undefined,
142
+            streamKey: ''
143
+        };
144
+
145
+        /**
146
+         * Instance variable used to flag whether the component is or is not
147
+         * mounted. Used as a hack to avoid setting state on an unmounted
148
+         * component.
149
+         *
150
+         * @private
151
+         * @type {boolean}
152
+         */
153
+        this._isMounted = false;
154
+
155
+        this._onCancel = this._onCancel.bind(this);
156
+        this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
157
+        this._onSubmit = this._onSubmit.bind(this);
158
+    }
159
+
160
+    /**
161
+     * Implements {@link Component#componentDidMount()}. Invoked immediately
162
+     * after this component is mounted.
163
+     *
164
+     * @inheritdoc
165
+     * @returns {void}
166
+     */
167
+    componentDidMount() {
168
+        this._isMounted = true;
169
+
170
+        if (this.props._googleApiApplicationClientID) {
171
+            this._onInitializeGoogleApi();
172
+        }
173
+    }
174
+
175
+    /**
176
+     * Implements React's {@link Component#componentWillUnmount()}. Invoked
177
+     * immediately before this component is unmounted and destroyed.
178
+     *
179
+     * @inheritdoc
180
+     */
181
+    componentWillUnmount() {
182
+        this._isMounted = false;
183
+    }
184
+
185
+    /**
186
+     * Implements {@code Component}'s render.
187
+     *
188
+     * @inheritdoc
189
+     */
190
+    render() {
191
+        return (
192
+            <Dialog
193
+                cancelTitleKey = 'dialog.Cancel'
194
+                okTitleKey = 'dialog.startLiveStreaming'
195
+                onCancel = { this._onCancel }
196
+                onSubmit = { this._onSubmit }
197
+                titleKey = 'liveStreaming.start'
198
+                width = { 'small' }>
199
+                {
200
+                    this._renderDialogContent()
201
+                }
202
+            </Dialog>
203
+        );
204
+    }
205
+
206
+    _onCancel: () => boolean;
207
+
208
+    /**
209
+     * Invokes the passed in {@link onCancel} callback and closes
210
+     * {@code StartLiveStreamDialog}.
211
+     *
212
+     * @private
213
+     * @returns {boolean} True is returned to close the modal.
214
+     */
215
+    _onCancel() {
216
+        sendAnalytics(createRecordingDialogEvent('start', 'cancel.button'));
217
+
218
+        return true;
219
+    }
220
+
221
+    /**
222
+     * Asks the user to sign in, if not already signed in, and then requests a
223
+     * list of the user's YouTube broadcasts.
224
+     *
225
+     * NOTE: To be implemented by platforms.
226
+     *
227
+     * @private
228
+     * @returns {Promise}
229
+     */
230
+    _onGetYouTubeBroadcasts: () => Promise<*>;
231
+
232
+    /**
233
+     * Loads the Google client application used for fetching stream keys.
234
+     * If the user is already logged in, then a request for available YouTube
235
+     * broadcasts is also made.
236
+     */
237
+    _onInitializeGoogleApi: () => Object;
238
+
239
+    _onStreamKeyChange: string => void;
240
+
241
+    /**
242
+     * Callback invoked to update the {@code StartLiveStreamDialog} component's
243
+     * display of the entered YouTube stream key.
244
+     *
245
+     * @param {string} streamKey - The stream key entered in the field.
246
+     * changed text.
247
+     * @private
248
+     * @returns {void}
249
+     */
250
+    _onStreamKeyChange(streamKey) {
251
+        this._setStateIfMounted({
252
+            streamKey,
253
+            selectedBoundStreamID: undefined
254
+        });
255
+    }
256
+
257
+    _onSubmit: () => boolean;
258
+
259
+    /**
260
+     * Invokes the passed in {@link onSubmit} callback with the entered stream
261
+     * key, and then closes {@code StartLiveStreamDialog}.
262
+     *
263
+     * @private
264
+     * @returns {boolean} False if no stream key is entered to preventing
265
+     * closing, true to close the modal.
266
+     */
267
+    _onSubmit() {
268
+        const { broadcasts, selectedBoundStreamID } = this.state;
269
+        const key = this.state.streamKey || this.props._streamKey;
270
+
271
+        if (!key) {
272
+            return false;
273
+        }
274
+
275
+        let selectedBroadcastID = null;
276
+
277
+        if (selectedBoundStreamID) {
278
+            const selectedBroadcast = broadcasts && broadcasts.find(
279
+                broadcast => broadcast.boundStreamID === selectedBoundStreamID);
280
+
281
+            selectedBroadcastID = selectedBroadcast && selectedBroadcast.id;
282
+        }
283
+
284
+        sendAnalytics(
285
+            createRecordingDialogEvent('start', 'confirm.button'));
286
+
287
+        this.props._conference.startRecording({
288
+            broadcastId: selectedBroadcastID,
289
+            mode: JitsiRecordingConstants.mode.STREAM,
290
+            streamId: key
291
+        });
292
+
293
+        return true;
294
+    }
295
+
296
+    /**
297
+     * Updates the internal state if the component is still mounted. This is a
298
+     * workaround for all the state setting that occurs after ajax.
299
+     *
300
+     * @param {Object} newState - The new state to merge into the existing
301
+     * state.
302
+     * @private
303
+     * @returns {void}
304
+     */
305
+    _setStateIfMounted(newState) {
306
+        if (this._isMounted) {
307
+            this.setState(newState);
308
+        }
309
+    }
310
+
311
+    /**
312
+     * Renders the platform specific dialog content.
313
+     *
314
+     * @returns {React$Component}
315
+     */
316
+    _renderDialogContent: () => React$Component<*>
317
+}
318
+
319
+/**
320
+ * Maps part of the Redux state to the component's props.
321
+ *
322
+ * @param {Object} state - The Redux state.
323
+ * @returns {{
324
+ *     _conference: Object,
325
+ *     _googleApiApplicationClientID: string,
326
+ *     _streamKey: string
327
+ * }}
328
+ */
329
+export function _mapStateToProps(state: Object) {
330
+    return {
331
+        _conference: state['features/base/conference'].conference,
332
+        _googleApiApplicationClientID:
333
+            state['features/base/config'].googleApiApplicationClientID,
334
+        _streamKey: state['features/recording'].streamKey
335
+    };
336
+}

+ 119
- 0
react/features/recording/components/LiveStream/AbstractStopLiveStreamDialog.js Wyświetl plik

@@ -0,0 +1,119 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import {
7
+    createRecordingDialogEvent,
8
+    sendAnalytics
9
+} from '../../../analytics';
10
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
11
+
12
+import { getActiveSession } from '../../functions';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of
16
+ * {@link StopLiveStreamDialog}.
17
+ */
18
+type Props = {
19
+
20
+    /**
21
+     * The {@code JitsiConference} for the current conference.
22
+     */
23
+    _conference: Object,
24
+
25
+    /**
26
+     * The redux representation of the live stremaing to be stopped.
27
+     */
28
+    _session: Object,
29
+
30
+    /**
31
+     * Invoked to obtain translated strings.
32
+     */
33
+    t: Function
34
+};
35
+
36
+/**
37
+ * A React Component for confirming the participant wishes to stop the currently
38
+ * active live stream of the conference.
39
+ *
40
+ * @extends Component
41
+ */
42
+export default class AbstractStopLiveStreamDialog extends Component<Props> {
43
+    /**
44
+     * Initializes a new {@code StopLiveStreamDialog} instance.
45
+     *
46
+     * @param {Object} props - The read-only properties with which the new
47
+     * instance is to be initialized.
48
+     */
49
+    constructor(props: Props) {
50
+        super(props);
51
+
52
+        // Bind event handler so it is only bound once for every instance.
53
+        this._onSubmit = this._onSubmit.bind(this);
54
+    }
55
+
56
+    /**
57
+     * Implements React's {@link Component#render()}.
58
+     *
59
+     * @inheritdoc
60
+     * @returns {ReactElement}
61
+     */
62
+    render() {
63
+        return (
64
+            <Dialog
65
+                okTitleKey = 'dialog.stopLiveStreaming'
66
+                onSubmit = { this._onSubmit }
67
+                titleKey = 'dialog.liveStreaming'
68
+                width = 'small'>
69
+                { this._renderDialogContent() }
70
+            </Dialog>
71
+        );
72
+    }
73
+
74
+    _onSubmit: () => boolean;
75
+
76
+    /**
77
+     * Callback invoked when stopping of live streaming is confirmed.
78
+     *
79
+     * @private
80
+     * @returns {boolean} True to close the modal.
81
+     */
82
+    _onSubmit() {
83
+        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
84
+
85
+        const { _session } = this.props;
86
+
87
+        if (_session) {
88
+            this.props._conference.stopRecording(_session.id);
89
+        }
90
+
91
+        return true;
92
+    }
93
+
94
+    /**
95
+     * Function to be implemented by the platform specific implementations.
96
+     *
97
+     * @private
98
+     * @returns {React$Component<*>}
99
+     */
100
+    _renderDialogContent: () => React$Component<*>
101
+}
102
+
103
+/**
104
+ * Maps (parts of) the redux state to the React {@code Component} props of
105
+ * {@code StopLiveStreamDialog}.
106
+ *
107
+ * @param {Object} state - The redux state.
108
+ * @private
109
+ * @returns {{
110
+ *     _conference: Object,
111
+ *     _session: Object
112
+ * }}
113
+ */
114
+export function _mapStateToProps(state: Object) {
115
+    return {
116
+        _conference: state['features/base/conference'].conference,
117
+        _session: getActiveSession(state, JitsiRecordingConstants.mode.STREAM)
118
+    };
119
+}

+ 107
- 0
react/features/recording/components/LiveStream/AbstractStreamKeyForm.js Wyświetl plik

@@ -0,0 +1,107 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+declare var interfaceConfig: Object;
6
+
7
+/**
8
+ * The live streaming help link to display. On web it comes from
9
+ * interfaceConfig, but we don't have that on mobile.
10
+ *
11
+ * FIXME: This is in props now to prepare for the Redux-based interfaceConfig
12
+ */
13
+const LIVE_STREAMING_HELP_LINK = 'https://jitsi.org/live';
14
+
15
+/**
16
+ * The props of the component.
17
+ */
18
+export type Props = {
19
+
20
+    /**
21
+     * Callback invoked when the entered stream key has changed.
22
+     */
23
+    onChange: Function,
24
+
25
+    /**
26
+     * Invoked to obtain translated strings.
27
+     */
28
+    t: Function,
29
+
30
+    /**
31
+     * The stream key value to display as having been entered so far.
32
+     */
33
+    value: string
34
+};
35
+
36
+/**
37
+ * The state of the component.
38
+ */
39
+type State = {
40
+
41
+    /**
42
+     * The value entered in the field.
43
+     */
44
+    value: string
45
+}
46
+
47
+/**
48
+ * An abstract React Component for entering a key for starting a YouTube live
49
+ * stream.
50
+ *
51
+ * @extends Component
52
+ */
53
+export default class AbstractStreamKeyForm extends Component<Props, State> {
54
+    helpURL: string;
55
+
56
+    /**
57
+     * Constructor for the component.
58
+     *
59
+     * @inheritdoc
60
+     */
61
+    constructor(props: Props) {
62
+        super(props);
63
+
64
+        this.state = {
65
+            value: props.value
66
+        };
67
+
68
+        this.helpURL = (typeof interfaceConfig !== 'undefined'
69
+            && interfaceConfig.LIVE_STREAMING_HELP_LINK)
70
+            || LIVE_STREAMING_HELP_LINK;
71
+
72
+        // Bind event handlers so they are only bound once per instance.
73
+        this._onInputChange = this._onInputChange.bind(this);
74
+    }
75
+
76
+    /**
77
+     * Implements {@code Component}'s componentWillReceiveProps.
78
+     *
79
+     * @inheritdoc
80
+     */
81
+    componentWillReceiveProps(newProps: Props) {
82
+        this.setState({
83
+            value: newProps.value
84
+        });
85
+    }
86
+
87
+    _onInputChange: Object => void
88
+
89
+    /**
90
+     * Callback invoked when the value of the input field has updated through
91
+     * user input. This forwards the value (string only, even if it was a dom
92
+     * event) to the onChange prop provided to the component.
93
+     *
94
+     * @param {Object | string} change - DOM Event for value change or the
95
+     * changed text.
96
+     * @private
97
+     * @returns {void}
98
+     */
99
+    _onInputChange(change) {
100
+        const value = typeof change === 'object' ? change.target.value : change;
101
+
102
+        this.setState({
103
+            value
104
+        });
105
+        this.props.onChange(value);
106
+    }
107
+}

+ 20
- 0
react/features/recording/components/LiveStream/LiveStreamButton.native.js Wyświetl plik

@@ -0,0 +1,20 @@
1
+// @flow
2
+
3
+import { connect } from 'react-redux';
4
+
5
+import { translate } from '../../../base/i18n';
6
+
7
+import AbstractLiveStreamButton, {
8
+    _mapStateToProps,
9
+    type Props
10
+} from './AbstractLiveStreamButton';
11
+
12
+/**
13
+ * An implementation of a button for starting and stopping live streaming.
14
+ */
15
+class LiveStreamButton extends AbstractLiveStreamButton<Props> {
16
+    iconName = 'public';
17
+    toggledIconName = 'public';
18
+}
19
+
20
+export default translate(connect(_mapStateToProps)(LiveStreamButton));

+ 122
- 0
react/features/recording/components/LiveStream/LiveStreamButton.web.js Wyświetl plik

@@ -0,0 +1,122 @@
1
+// @flow
2
+
3
+import { connect } from 'react-redux';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { getLocalParticipant } from '../../../base/participants';
7
+
8
+import AbstractLiveStreamButton, {
9
+    _mapStateToProps as _abstractMapStateToProps,
10
+    type Props as AbstractProps
11
+} from './AbstractLiveStreamButton';
12
+
13
+declare var interfaceConfig: Object;
14
+
15
+type Props = AbstractProps & {
16
+
17
+    /**
18
+     * True if the button should be disabled, false otherwise.
19
+     *
20
+     * NOTE: On web, if the feature is not disabled on purpose, then we still
21
+     * show the button but disabled and with a tooltip rendered on it,
22
+     * explaining why it's not available.
23
+     */
24
+    _disabled: boolean,
25
+
26
+    /**
27
+     * Tooltip for the button when it's disabled in a certain way.
28
+     */
29
+    _liveStreamDisabledTooltipKey: ?string
30
+}
31
+
32
+/**
33
+ * An implementation of a button for starting and stopping live streaming.
34
+ */
35
+class LiveStreamButton extends AbstractLiveStreamButton<Props> {
36
+    iconName = 'icon-public';
37
+    toggledIconName = 'icon-public';
38
+
39
+    /**
40
+     * Constructor of the component.
41
+     *
42
+     * @inheritdoc
43
+     */
44
+    constructor(props: Props) {
45
+        super(props);
46
+
47
+        this.tooltip = props._liveStreamDisabledTooltipKey;
48
+    }
49
+
50
+    /**
51
+     * Implements {@code Component}'s componentWillReceiveProps.
52
+     *
53
+     * @inheritdoc
54
+     */
55
+    componentWillReceiveProps(newProps: Props) {
56
+        this.tooltip = newProps._liveStreamDisabledTooltipKey;
57
+    }
58
+
59
+    /**
60
+     * Helper function to be implemented by subclasses, which must return a
61
+     * boolean value indicating if this button is disabled or not.
62
+     *
63
+     * @override
64
+     * @protected
65
+     * @returns {boolean}
66
+     */
67
+    _isDisabled() {
68
+        return this.props._disabled;
69
+    }
70
+}
71
+
72
+/**
73
+ * Maps (parts of) the redux state to the associated props for the
74
+ * {@code LiveStreamButton} component.
75
+ *
76
+ * @param {Object} state - The Redux state.
77
+ * @param {Props} ownProps - The own props of the Component.
78
+ * @private
79
+ * @returns {{
80
+ *     _conference: Object,
81
+ *     _isLiveStreamRunning: boolean,
82
+ *     _disabled: boolean,
83
+ *     visible: boolean
84
+ * }}
85
+ */
86
+function _mapStateToProps(state: Object, ownProps: Props) {
87
+    const abstractProps = _abstractMapStateToProps(state, ownProps);
88
+    const localParticipant = getLocalParticipant(state);
89
+    const { features = {} } = localParticipant;
90
+    let { visible } = ownProps;
91
+
92
+    let _disabled = false;
93
+    let _liveStreamDisabledTooltipKey;
94
+
95
+    if (!abstractProps.visible
96
+            && String(features.livestreaming) !== 'disabled') {
97
+        _disabled = true;
98
+
99
+        // button and tooltip
100
+        if (state['features/base/jwt'].isGuest) {
101
+            _liveStreamDisabledTooltipKey
102
+                = 'dialog.liveStreamingDisabledForGuestTooltip';
103
+        } else {
104
+            _liveStreamDisabledTooltipKey
105
+                = 'dialog.liveStreamingDisabledTooltip';
106
+        }
107
+    }
108
+
109
+    if (typeof visible === 'undefined') {
110
+        visible = interfaceConfig.TOOLBAR_BUTTONS.includes('livestreaming')
111
+            && (abstractProps.visible || _liveStreamDisabledTooltipKey);
112
+    }
113
+
114
+    return {
115
+        ...abstractProps,
116
+        _disabled,
117
+        _liveStreamDisabledTooltipKey,
118
+        visible
119
+    };
120
+}
121
+
122
+export default translate(connect(_mapStateToProps)(LiveStreamButton));

+ 92
- 0
react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js Wyświetl plik

@@ -0,0 +1,92 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+
9
+import { setLiveStreamKey } from '../../actions';
10
+
11
+import AbstractStartLiveStreamDialog, {
12
+    _mapStateToProps,
13
+    type Props
14
+} from './AbstractStartLiveStreamDialog';
15
+import StreamKeyForm from './StreamKeyForm';
16
+
17
+/**
18
+ * A React Component for requesting a YouTube stream key to use for live
19
+ * streaming of the current conference.
20
+ */
21
+class StartLiveStreamDialog extends AbstractStartLiveStreamDialog {
22
+    /**
23
+     * Constructor of the component.
24
+     *
25
+     * @inheritdoc
26
+     */
27
+    constructor(props: Props) {
28
+        super(props);
29
+
30
+        // Bind event handlers so they are only bound once per instance.
31
+        this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
32
+        this._onStreamKeyChangeNative
33
+            = this._onStreamKeyChangeNative.bind(this);
34
+        this._renderDialogContent = this._renderDialogContent.bind(this);
35
+    }
36
+
37
+    _onInitializeGoogleApi: () => Promise<*>
38
+
39
+    /**
40
+     * Loads the Google client application used for fetching stream keys.
41
+     * If the user is already logged in, then a request for available YouTube
42
+     * broadcasts is also made.
43
+     *
44
+     * @private
45
+     * @returns {Promise}
46
+     */
47
+    _onInitializeGoogleApi() {
48
+        // This is a placeholder method for the Google feature.
49
+        return Promise.resolve();
50
+    }
51
+
52
+    _onStreamKeyChange: string => void
53
+
54
+    _onStreamKeyChangeNative: string => void;
55
+
56
+    /**
57
+     * Callback to handle stream key changes.
58
+     *
59
+     * FIXME: This is a temporary method to store the streaming key on mobile
60
+     * for easier use, until the Google sign-in is implemented. We don't store
61
+     * the key on web for security reasons (e.g. we don't want to have the key
62
+     * stored if the used signed out).
63
+     *
64
+     * @private
65
+     * @param {string} streamKey - The new key value.
66
+     * @returns {void}
67
+     */
68
+    _onStreamKeyChangeNative(streamKey) {
69
+        this.props.dispatch(setLiveStreamKey(streamKey));
70
+        this._onStreamKeyChange(streamKey);
71
+    }
72
+
73
+    _renderDialogContent: () => React$Component<*>
74
+
75
+    /**
76
+     * Renders the platform specific dialog content.
77
+     *
78
+     * @returns {React$Component}
79
+     */
80
+    _renderDialogContent() {
81
+        return (
82
+            <View>
83
+                <StreamKeyForm
84
+                    onChange = { this._onStreamKeyChangeNative }
85
+                    value = { this.props._streamKey } />
86
+            </View>
87
+        );
88
+    }
89
+
90
+}
91
+
92
+export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

+ 35
- 283
react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js Wyświetl plik

@@ -1,128 +1,32 @@
1 1
 // @flow
2 2
 
3 3
 import Spinner from '@atlaskit/spinner';
4
-import React, { Component } from 'react';
4
+import React from 'react';
5 5
 import { connect } from 'react-redux';
6 6
 
7
-import {
8
-    createRecordingDialogEvent,
9
-    sendAnalytics
10
-} from '../../../analytics';
11
-import { Dialog } from '../../../base/dialog';
12 7
 import { translate } from '../../../base/i18n';
13
-import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
14 8
 
15 9
 import googleApi from '../../googleApi';
16 10
 
11
+import AbstractStartLiveStreamDialog, {
12
+    _mapStateToProps,
13
+    GOOGLE_API_STATES,
14
+    type Props
15
+} from './AbstractStartLiveStreamDialog';
17 16
 import BroadcastsDropdown from './BroadcastsDropdown';
18 17
 import GoogleSignInButton from './GoogleSignInButton';
19 18
 import StreamKeyForm from './StreamKeyForm';
20 19
 
21 20
 declare var interfaceConfig: Object;
22 21
 
23
-/**
24
- * An enumeration of the different states the Google API can be in while
25
- * interacting with {@code StartLiveStreamDialog}.
26
-  *
27
- * @private
28
- * @type {Object}
29
- */
30
-const GOOGLE_API_STATES = {
31
-    /**
32
-     * The state in which the Google API still needs to be loaded.
33
-     */
34
-    NEEDS_LOADING: 0,
35
-
36
-    /**
37
-     * The state in which the Google API is loaded and ready for use.
38
-     */
39
-    LOADED: 1,
40
-
41
-    /**
42
-     * The state in which a user has been logged in through the Google API.
43
-     */
44
-    SIGNED_IN: 2,
45
-
46
-    /**
47
-     * The state in which the Google API encountered an error either loading
48
-     * or with an API request.
49
-     */
50
-    ERROR: 3
51
-};
52
-
53
-/**
54
- * The type of the React {@code Component} props of
55
- * {@link StartLiveStreamDialog}.
56
- */
57
-type Props = {
58
-
59
-    /**
60
-     * The {@code JitsiConference} for the current conference.
61
-     */
62
-    _conference: Object,
63
-
64
-    /**
65
-     * The ID for the Google web client application used for making stream key
66
-     * related requests.
67
-     */
68
-    _googleApiApplicationClientID: string,
69
-
70
-    /**
71
-     * Invoked to obtain translated strings.
72
-     */
73
-    t: Function
74
-};
75
-
76
-/**
77
- * The type of the React {@code Component} state of
78
- * {@link StartLiveStreamDialog}.
79
- */
80
-type State = {
81
-
82
-    /**
83
-     * Details about the broadcasts available for use for the logged in Google
84
-     * user's YouTube account.
85
-     */
86
-    broadcasts: ?Array<Object>,
87
-
88
-    /**
89
-     * The error type, as provided by Google, for the most recent error
90
-     * encountered by the Google API.
91
-     */
92
-    errorType: ?string,
93
-
94
-    /**
95
-     * The current state of interactions with the Google API. Determines what
96
-     * Google related UI should display.
97
-     */
98
-    googleAPIState: number,
99
-
100
-    /**
101
-     * The email of the user currently logged in to the Google web client
102
-     * application.
103
-     */
104
-    googleProfileEmail: string,
105
-
106
-    /**
107
-     * The boundStreamID of the broadcast currently selected in the broadcast
108
-     * dropdown.
109
-     */
110
-    selectedBoundStreamID: ?string,
111
-
112
-    /**
113
-     * The selected or entered stream key to use for YouTube live streaming.
114
-     */
115
-    streamKey: string
116
-};
117
-
118 22
 /**
119 23
  * A React Component for requesting a YouTube stream key to use for live
120 24
  * streaming of the current conference.
121 25
  *
122 26
  * @extends Component
123 27
  */
124
-class StartLiveStreamDialog extends Component<Props, State> {
125
-    _isMounted: boolean;
28
+class StartLiveStreamDialog
29
+    extends AbstractStartLiveStreamDialog {
126 30
 
127 31
     /**
128 32
      * Initializes a new {@code StartLiveStreamDialog} instance.
@@ -133,91 +37,17 @@ class StartLiveStreamDialog extends Component<Props, State> {
133 37
     constructor(props: Props) {
134 38
         super(props);
135 39
 
136
-        this.state = {
137
-            broadcasts: undefined,
138
-            errorType: undefined,
139
-            googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
140
-            googleProfileEmail: '',
141
-            selectedBoundStreamID: undefined,
142
-            streamKey: ''
143
-        };
144
-
145
-        /**
146
-         * Instance variable used to flag whether the component is or is not
147
-         * mounted. Used as a hack to avoid setting state on an unmounted
148
-         * component.
149
-         *
150
-         * @private
151
-         * @type {boolean}
152
-         */
153
-        this._isMounted = false;
154
-
155 40
         // Bind event handlers so they are only bound once per instance.
156
-        this._onCancel = this._onCancel.bind(this);
157 41
         this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
158 42
         this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
159 43
         this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
160
-        this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
161
-        this._onSubmit = this._onSubmit.bind(this);
162 44
         this._onYouTubeBroadcastIDSelected
163 45
             = this._onYouTubeBroadcastIDSelected.bind(this);
164
-    }
165
-
166
-    /**
167
-     * Implements {@link Component#componentDidMount()}. Invoked immediately
168
-     * after this component is mounted.
169
-     *
170
-     * @inheritdoc
171
-     * @returns {void}
172
-     */
173
-    componentDidMount() {
174
-        this._isMounted = true;
175
-
176
-        if (this.props._googleApiApplicationClientID) {
177
-            this._onInitializeGoogleApi();
178
-        }
179
-    }
180
-
181
-    /**
182
-     * Implements React's {@link Component#componentWillUnmount()}. Invoked
183
-     * immediately before this component is unmounted and destroyed.
184
-     *
185
-     * @inheritdoc
186
-     */
187
-    componentWillUnmount() {
188
-        this._isMounted = false;
189
-    }
190
-
191
-    /**
192
-     * Implements React's {@link Component#render()}.
193
-     *
194
-     * @inheritdoc
195
-     * @returns {ReactElement}
196
-     */
197
-    render() {
198
-        const { _googleApiApplicationClientID } = this.props;
199 46
 
200
-        return (
201
-            <Dialog
202
-                cancelTitleKey = 'dialog.Cancel'
203
-                okTitleKey = 'dialog.startLiveStreaming'
204
-                onCancel = { this._onCancel }
205
-                onSubmit = { this._onSubmit }
206
-                titleKey = 'liveStreaming.start'
207
-                width = { 'small' }>
208
-                <div className = 'live-stream-dialog'>
209
-                    { _googleApiApplicationClientID
210
-                        ? this._renderYouTubePanel() : null }
211
-                    <StreamKeyForm
212
-                        helpURL = { interfaceConfig.LIVE_STREAMING_HELP_LINK }
213
-                        onChange = { this._onStreamKeyChange }
214
-                        value = { this.state.streamKey } />
215
-                </div>
216
-            </Dialog>
217
-        );
47
+        this._renderDialogContent = this._renderDialogContent.bind(this);
218 48
     }
219 49
 
220
-    _onInitializeGoogleApi: () => Object;
50
+    _onInitializeGoogleApi: () => Promise<*>;
221 51
 
222 52
     /**
223 53
      * Loads the Google web client application used for fetching stream keys.
@@ -247,22 +77,7 @@ class StartLiveStreamDialog extends Component<Props, State> {
247 77
             });
248 78
     }
249 79
 
250
-    _onCancel: () => boolean;
251
-
252
-    /**
253
-     * Invokes the passed in {@link onCancel} callback and closes
254
-     * {@code StartLiveStreamDialog}.
255
-     *
256
-     * @private
257
-     * @returns {boolean} True is returned to close the modal.
258
-     */
259
-    _onCancel() {
260
-        sendAnalytics(createRecordingDialogEvent('start', 'cancel.button'));
261
-
262
-        return true;
263
-    }
264
-
265
-    _onGetYouTubeBroadcasts: () => Object;
80
+    _onGetYouTubeBroadcasts: () => Promise<*>;
266 81
 
267 82
     /**
268 83
      * 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> {
322 137
             .then(() => this._onGetYouTubeBroadcasts());
323 138
     }
324 139
 
325
-    _onStreamKeyChange: () => void;
326
-
327
-    /**
328
-     * Callback invoked to update the {@code StartLiveStreamDialog} component's
329
-     * display of the entered YouTube stream key.
330
-     *
331
-     * @param {Object} event - DOM Event for value change.
332
-     * @private
333
-     * @returns {void}
334
-     */
335
-    _onStreamKeyChange(event) {
336
-        this._setStateIfMounted({
337
-            streamKey: event.target.value,
338
-            selectedBoundStreamID: undefined
339
-        });
340
-    }
341
-
342
-    _onSubmit: () => boolean;
343
-
344
-    /**
345
-     * Invokes the passed in {@link onSubmit} callback with the entered stream
346
-     * key, and then closes {@code StartLiveStreamDialog}.
347
-     *
348
-     * @private
349
-     * @returns {boolean} False if no stream key is entered to preventing
350
-     * closing, true to close the modal.
351
-     */
352
-    _onSubmit() {
353
-        const { broadcasts, streamKey, selectedBoundStreamID } = this.state;
354
-
355
-        if (!streamKey) {
356
-            return false;
357
-        }
358
-
359
-        let selectedBroadcastID = null;
360
-
361
-        if (selectedBoundStreamID) {
362
-            const selectedBroadcast = broadcasts && broadcasts.find(
363
-                broadcast => broadcast.boundStreamID === selectedBoundStreamID);
364
-
365
-            selectedBroadcastID = selectedBroadcast && selectedBroadcast.id;
366
-        }
367
-
368
-        sendAnalytics(createRecordingDialogEvent('start', 'confirm.button'));
369
-
370
-        this.props._conference.startRecording({
371
-            broadcastId: selectedBroadcastID,
372
-            mode: JitsiRecordingConstants.mode.STREAM,
373
-            streamId: streamKey
374
-        });
375
-
376
-        return true;
377
-    }
140
+    _onStreamKeyChange: string => void;
378 141
 
379 142
     _onYouTubeBroadcastIDSelected: (string) => Object;
380 143
 
@@ -452,6 +215,27 @@ class StartLiveStreamDialog extends Component<Props, State> {
452 215
         return (firstError && firstError.reason) || null;
453 216
     }
454 217
 
218
+    _renderDialogContent: () => React$Component<*>
219
+
220
+    /**
221
+     * Renders the platform specific dialog content.
222
+     *
223
+     * @returns {React$Component}
224
+     */
225
+    _renderDialogContent() {
226
+        const { _googleApiApplicationClientID } = this.props;
227
+
228
+        return (
229
+            <div className = 'live-stream-dialog'>
230
+                { _googleApiApplicationClientID
231
+                    ? this._renderYouTubePanel() : null }
232
+                <StreamKeyForm
233
+                    onChange = { this._onStreamKeyChange }
234
+                    value = { this.state.streamKey || this.props._streamKey } />
235
+            </div>
236
+        );
237
+    }
238
+
455 239
     /**
456 240
      * Renders a React Element for authenticating with the Google web client.
457 241
      *
@@ -537,6 +321,8 @@ class StartLiveStreamDialog extends Component<Props, State> {
537 321
         );
538 322
     }
539 323
 
324
+    _setStateIfMounted: Object => void
325
+
540 326
     /**
541 327
      * Returns the error message to display for the current error state.
542 328
      *
@@ -559,40 +345,6 @@ class StartLiveStreamDialog extends Component<Props, State> {
559 345
 
560 346
         return <div className = 'google-error'>{ text }</div>;
561 347
     }
562
-
563
-    /**
564
-     * Updates the internal state if the component is still mounted. This is a
565
-     * workaround for all the state setting that occurs after ajax.
566
-     *
567
-     * @param {Object} newState - The new state to merge into the existing
568
-     * state.
569
-     * @private
570
-     * @returns {void}
571
-     */
572
-    _setStateIfMounted(newState) {
573
-        if (this._isMounted) {
574
-            this.setState(newState);
575
-        }
576
-    }
577
-}
578
-
579
-/**
580
- * Maps (parts of) the redux state to the React {@code Component} props of
581
- * {@code StartLiveStreamDialog}.
582
- *
583
- * @param {Object} state - The redux state.
584
- * @private
585
- * @returns {{
586
- *     _conference: Object,
587
- *     _googleApiApplicationClientID: string
588
- * }}
589
- */
590
-function _mapStateToProps(state) {
591
-    return {
592
-        _conference: state['features/base/conference'].conference,
593
-        _googleApiApplicationClientID:
594
-            state['features/base/config'].googleApiApplicationClientID
595
-    };
596 348
 }
597 349
 
598 350
 export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

+ 41
- 0
react/features/recording/components/LiveStream/StopLiveStreamDialog.native.js Wyświetl plik

@@ -0,0 +1,41 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+
9
+import styles from '../styles';
10
+
11
+import AbstractStopLiveStreamDialog, {
12
+    _mapStateToProps
13
+} from './AbstractStopLiveStreamDialog';
14
+
15
+/**
16
+ * A React Component for confirming the participant wishes to stop the currently
17
+ * active live stream of the conference.
18
+ *
19
+ * @extends Component
20
+ */
21
+class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
22
+
23
+    /**
24
+     * Renders the platform specific {@code Dialog} content.
25
+     *
26
+     * @inheritdoc
27
+     */
28
+    _renderDialogContent() {
29
+        return (
30
+            <View style = { styles.messageContainer }>
31
+                <Text>
32
+                    {
33
+                        this.props.t('dialog.stopStreamingWarning')
34
+                    }
35
+                </Text>
36
+            </View>
37
+        );
38
+    }
39
+}
40
+
41
+export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));

+ 7
- 88
react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js Wyświetl plik

@@ -1,36 +1,12 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
4 3
 import { connect } from 'react-redux';
5 4
 
6
-import { Dialog } from '../../../base/dialog';
7 5
 import { translate } from '../../../base/i18n';
8
-import {
9
-    createRecordingDialogEvent,
10
-    sendAnalytics
11
-} from '../../../analytics';
12 6
 
13
-/**
14
- * The type of the React {@code Component} props of
15
- * {@link StopLiveStreamDialog}.
16
- */
17
-type Props = {
18
-
19
-    /**
20
-     * The {@code JitsiConference} for the current conference.
21
-     */
22
-    _conference: Object,
23
-
24
-    /**
25
-     * The redux representation of the live stremaing to be stopped.
26
-     */
27
-    session: Object,
28
-
29
-    /**
30
-     * Invoked to obtain translated strings.
31
-     */
32
-    t: Function
33
-};
7
+import AbstractStopLiveStreamDialog, {
8
+    _mapStateToProps
9
+} from './AbstractStopLiveStreamDialog';
34 10
 
35 11
 /**
36 12
  * A React Component for confirming the participant wishes to stop the currently
@@ -38,73 +14,16 @@ type Props = {
38 14
  *
39 15
  * @extends Component
40 16
  */
41
-class StopLiveStreamDialog extends Component<Props> {
42
-    /**
43
-     * Initializes a new {@code StopLiveStreamDialog} instance.
44
-     *
45
-     * @param {Object} props - The read-only properties with which the new
46
-     * instance is to be initialized.
47
-     */
48
-    constructor(props: Props) {
49
-        super(props);
50
-
51
-        // Bind event handler so it is only bound once for every instance.
52
-        this._onSubmit = this._onSubmit.bind(this);
53
-    }
17
+class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
54 18
 
55 19
     /**
56
-     * Implements React's {@link Component#render()}.
20
+     * Renders the platform specific {@code Dialog} content.
57 21
      *
58 22
      * @inheritdoc
59
-     * @returns {ReactElement}
60 23
      */
61
-    render() {
62
-        return (
63
-            <Dialog
64
-                okTitleKey = 'dialog.stopLiveStreaming'
65
-                onSubmit = { this._onSubmit }
66
-                titleKey = 'dialog.liveStreaming'
67
-                width = 'small'>
68
-                { this.props.t('dialog.stopStreamingWarning') }
69
-            </Dialog>
70
-        );
24
+    _renderDialogContent() {
25
+        return this.props.t('dialog.stopStreamingWarning');
71 26
     }
72
-
73
-    _onSubmit: () => boolean;
74
-
75
-    /**
76
-     * Callback invoked when stopping of live streaming is confirmed.
77
-     *
78
-     * @private
79
-     * @returns {boolean} True to close the modal.
80
-     */
81
-    _onSubmit() {
82
-        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
83
-
84
-        const { session } = this.props;
85
-
86
-        if (session) {
87
-            this.props._conference.stopRecording(session.id);
88
-        }
89
-
90
-        return true;
91
-    }
92
-}
93
-
94
-/**
95
- * Maps (parts of) the redux state to the React {@code Component} props of
96
- * {@code StopLiveStreamDialog}.
97
- *
98
- * @param {Object} state - The redux state.
99
- * @private
100
- * @returns {{
101
- *     _conference: Object
102
- * }}
103
- */
104
-function _mapStateToProps(state) {
105
-    return {
106
-        _conference: state['features/base/conference'].conference
107
-    };
108 27
 }
109 28
 
110 29
 export default translate(connect(_mapStateToProps)(StopLiveStreamDialog));

+ 86
- 0
react/features/recording/components/LiveStream/StreamKeyForm.native.js Wyświetl plik

@@ -0,0 +1,86 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Linking, Text, TextInput, TouchableOpacity, View } from 'react-native';
5
+
6
+import { translate } from '../../../base/i18n';
7
+
8
+import AbstractStreamKeyForm, {
9
+    type Props
10
+} from './AbstractStreamKeyForm';
11
+import styles from './styles';
12
+
13
+/**
14
+ * A React Component for entering a key for starting a YouTube live stream.
15
+ *
16
+ * @extends Component
17
+ */
18
+class StreamKeyForm extends AbstractStreamKeyForm {
19
+    /**
20
+     * Initializes a new {@code StreamKeyForm} instance.
21
+     *
22
+     * @param {Props} props - The React {@code Component} props to initialize
23
+     * the new {@code StreamKeyForm} instance with.
24
+     */
25
+    constructor(props: Props) {
26
+        super(props);
27
+
28
+        // Bind event handlers so they are only bound once per instance.
29
+        this._onOpenHelp = this._onOpenHelp.bind(this);
30
+    }
31
+
32
+    /**
33
+     * Implements React's {@link Component#render()}.
34
+     *
35
+     * @inheritdoc
36
+     * @returns {ReactElement}
37
+     */
38
+    render() {
39
+        const { t } = this.props;
40
+
41
+        return (
42
+            <View style = { styles.streamKeyFormWrapper }>
43
+                <Text style = { styles.streamKeyInputLabel }>
44
+                    {
45
+                        t('dialog.streamKey')
46
+                    }
47
+                </Text>
48
+                <TextInput
49
+                    onChangeText = { this._onInputChange }
50
+                    placeholder = { t('liveStreaming.enterStreamKey') }
51
+                    style = { styles.streamKeyInput }
52
+                    value = { this.state.value } />
53
+                <TouchableOpacity
54
+                    onPress = { this._onOpenHelp }
55
+                    style = { styles.streamKeyHelp } >
56
+                    <Text>
57
+                        {
58
+                            t('liveStreaming.streamIdHelp')
59
+                        }
60
+                    </Text>
61
+                </TouchableOpacity>
62
+            </View>
63
+        );
64
+    }
65
+
66
+    _onInputChange: Object => void
67
+
68
+    _onOpenHelp: () => void
69
+
70
+    /**
71
+     * Opens the information link on how to manually locate a YouTube broadcast
72
+     * stream key.
73
+     *
74
+     * @private
75
+     * @returns {void}
76
+     */
77
+    _onOpenHelp() {
78
+        const { helpURL } = this;
79
+
80
+        if (typeof helpURL === 'string') {
81
+            Linking.openURL(helpURL);
82
+        }
83
+    }
84
+}
85
+
86
+export default translate(StreamKeyForm);

+ 16
- 47
react/features/recording/components/LiveStream/StreamKeyForm.web.js Wyświetl plik

@@ -1,42 +1,20 @@
1
+// @flow
2
+
1 3
 import { FieldTextStateless } from '@atlaskit/field-text';
2
-import PropTypes from 'prop-types';
3
-import React, { Component } from 'react';
4
+import React from 'react';
4 5
 
5 6
 import { translate } from '../../../base/i18n';
6 7
 
8
+import AbstractStreamKeyForm, {
9
+    type Props
10
+} from './AbstractStreamKeyForm';
11
+
7 12
 /**
8 13
  * A React Component for entering a key for starting a YouTube live stream.
9 14
  *
10 15
  * @extends Component
11 16
  */
12
-class StreamKeyForm extends Component {
13
-    /**
14
-     * {@code StreamKeyForm} component's property types.
15
-     *
16
-     * @static
17
-     */
18
-    static propTypes = {
19
-        /**
20
-         * The URL to the page with more information for manually finding the
21
-         * stream key for a YouTube broadcast.
22
-         */
23
-        helpURL: PropTypes.string,
24
-
25
-        /**
26
-         * Callback invoked when the entered stream key has changed.
27
-         */
28
-        onChange: PropTypes.func,
29
-
30
-        /**
31
-         * Invoked to obtain translated strings.
32
-         */
33
-        t: PropTypes.func,
34
-
35
-        /**
36
-         * The stream key value to display as having been entered so far.
37
-         */
38
-        value: PropTypes.string
39
-    };
17
+class StreamKeyForm extends AbstractStreamKeyForm {
40 18
 
41 19
     /**
42 20
      * Initializes a new {@code StreamKeyForm} instance.
@@ -44,11 +22,10 @@ class StreamKeyForm extends Component {
44 22
      * @param {Props} props - The React {@code Component} props to initialize
45 23
      * the new {@code StreamKeyForm} instance with.
46 24
      */
47
-    constructor(props) {
25
+    constructor(props: Props) {
48 26
         super(props);
49 27
 
50 28
         // Bind event handlers so they are only bound once per instance.
51
-        this._onInputChange = this._onInputChange.bind(this);
52 29
         this._onOpenHelp = this._onOpenHelp.bind(this);
53 30
     }
54 31
 
@@ -59,7 +36,7 @@ class StreamKeyForm extends Component {
59 36
      * @returns {ReactElement}
60 37
      */
61 38
     render() {
62
-        const { t } = this.props;
39
+        const { value, t } = this.props;
63 40
 
64 41
         return (
65 42
             <div className = 'stream-key-form'>
@@ -69,12 +46,12 @@ class StreamKeyForm extends Component {
69 46
                     isSpellCheckEnabled = { false }
70 47
                     label = { t('dialog.streamKey') }
71 48
                     name = 'streamId'
72
-                    okDisabled = { !this.props.value }
49
+                    okDisabled = { !value }
73 50
                     onChange = { this._onInputChange }
74 51
                     placeholder = { t('liveStreaming.enterStreamKey') }
75 52
                     shouldFitContainer = { true }
76 53
                     type = 'text'
77
-                    value = { this.props.value } />
54
+                    value = { this.state.value } />
78 55
                 { this.props.helpURL
79 56
                     ? <div className = 'form-footer'>
80 57
                         <a
@@ -89,17 +66,9 @@ class StreamKeyForm extends Component {
89 66
         );
90 67
     }
91 68
 
92
-    /**
93
-     * Callback invoked when the value of the input field has updated through
94
-     * user input.
95
-     *
96
-     * @param {Object} event - DOM Event for value change.
97
-     * @private
98
-     * @returns {void}
99
-     */
100
-    _onInputChange(event) {
101
-        this.props.onChange(event);
102
-    }
69
+    _onInputChange: Object => void
70
+
71
+    _onOpenHelp: () => void
103 72
 
104 73
     /**
105 74
      * Opens a new tab with information on how to manually locate a YouTube
@@ -109,7 +78,7 @@ class StreamKeyForm extends Component {
109 78
      * @returns {void}
110 79
      */
111 80
     _onOpenHelp() {
112
-        window.open(this.props.helpURL, 'noopener');
81
+        window.open(this.helpURL, 'noopener');
113 82
     }
114 83
 }
115 84
 

+ 1
- 0
react/features/recording/components/LiveStream/index.js Wyświetl plik

@@ -1,2 +1,3 @@
1
+export { default as LiveStreamButton } from './LiveStreamButton';
1 2
 export { default as StartLiveStreamDialog } from './StartLiveStreamDialog';
2 3
 export { default as StopLiveStreamDialog } from './StopLiveStreamDialog';

+ 28
- 0
react/features/recording/components/LiveStream/styles.js Wyświetl plik

@@ -0,0 +1,28 @@
1
+// @flow
2
+
3
+import { BoxModel, createStyleSheet } from '../../../base/styles';
4
+
5
+/**
6
+ * The styles of the React {@code Components} of LiveStream.
7
+ */
8
+export default createStyleSheet({
9
+
10
+    streamKeyFormWrapper: {
11
+        flexDirection: 'column',
12
+        padding: BoxModel.padding
13
+    },
14
+
15
+    streamKeyHelp: {
16
+        alignSelf: 'flex-end'
17
+    },
18
+
19
+    streamKeyInput: {
20
+        alignSelf: 'stretch',
21
+        height: 50
22
+    },
23
+
24
+    streamKeyInputLabel: {
25
+        alignSelf: 'flex-start'
26
+    }
27
+
28
+});

+ 1
- 3
react/features/recording/components/Recording/RecordButton.web.js Wyświetl plik

@@ -107,9 +107,7 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
107 107
     }
108 108
 
109 109
     if (typeof visible === 'undefined') {
110
-        const visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
111
-
112
-        visible = visibleButtons.has('recording')
110
+        visible = interfaceConfig.TOOLBAR_BUTTONS.includes('recording')
113 111
             && (abstractProps.visible || _fileRecordingsDisabledTooltipKey);
114 112
     }
115 113
 

+ 5
- 1
react/features/recording/components/index.js Wyświetl plik

@@ -1,4 +1,8 @@
1
-export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
1
+export {
2
+    LiveStreamButton,
3
+    StartLiveStreamDialog,
4
+    StopLiveStreamDialog
5
+} from './LiveStream';
2 6
 export {
3 7
     RecordButton,
4 8
     StartRecordingDialog,

+ 9
- 0
react/features/recording/constants.js Wyświetl plik

@@ -1,5 +1,14 @@
1 1
 // @flow
2 2
 
3
+/**
4
+ * The Google API scopes to request access to for streaming.
5
+ *
6
+ * @type {Array<string>}
7
+ */
8
+export const GOOGLE_API_SCOPES = [
9
+    'https://www.googleapis.com/auth/youtube.readonly'
10
+];
11
+
3 12
 /**
4 13
  * The identifier of the sound to be played when a recording or live streaming
5 14
  * session is stopped.

+ 3
- 4
react/features/recording/googleApi.js Wyświetl plik

@@ -1,7 +1,6 @@
1
+import { GOOGLE_API_SCOPES } from './constants';
2
+
1 3
 const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
2
-const GOOGLE_API_SCOPES = [
3
-    'https://www.googleapis.com/auth/youtube.readonly'
4
-].join(' ');
5 4
 
6 5
 /**
7 6
  * A promise for dynamically loading the Google API Client Library.
@@ -68,7 +67,7 @@ const googleApi = {
68 67
                 setTimeout(() => {
69 68
                     api.client.init({
70 69
                         clientId,
71
-                        scope: GOOGLE_API_SCOPES
70
+                        scope: GOOGLE_API_SCOPES.join(' ')
72 71
                     })
73 72
                     .then(resolve)
74 73
                     .catch(reject);

+ 23
- 16
react/features/recording/middleware.js Wyświetl plik

@@ -104,24 +104,31 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
104 104
     case RECORDING_SESSION_UPDATED: {
105 105
         const updatedSessionData
106 106
             = getSessionById(getState(), action.sessionData.id);
107
-
108
-        if (updatedSessionData.mode === JitsiRecordingConstants.mode.FILE) {
109
-            const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
110
-
111
-            if (updatedSessionData.status === PENDING
112
-                && (!oldSessionData || oldSessionData.status !== PENDING)) {
113
-                dispatch(showPendingRecordingNotification());
114
-            } else if (updatedSessionData.status !== PENDING) {
115
-                dispatch(hidePendingRecordingNotification());
116
-
117
-                if (updatedSessionData.status === ON
118
-                    && (!oldSessionData || oldSessionData.status !== ON)) {
119
-                    dispatch(playSound(RECORDING_ON_SOUND_ID));
120
-                } else if (updatedSessionData.status === OFF
121
-                    && (!oldSessionData || oldSessionData.status !== OFF)) {
107
+        const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
108
+
109
+        if (updatedSessionData.status === PENDING
110
+            && (!oldSessionData || oldSessionData.status !== PENDING)) {
111
+            dispatch(
112
+                showPendingRecordingNotification(updatedSessionData.mode));
113
+        } else if (updatedSessionData.status !== PENDING) {
114
+            dispatch(
115
+                hidePendingRecordingNotification(updatedSessionData.mode));
116
+
117
+            if (updatedSessionData.status === ON
118
+                && (!oldSessionData || oldSessionData.status !== ON)
119
+                && updatedSessionData.mode
120
+                    === JitsiRecordingConstants.mode.FILE) {
121
+                dispatch(playSound(RECORDING_ON_SOUND_ID));
122
+            } else if (updatedSessionData.status === OFF
123
+                && (!oldSessionData || oldSessionData.status !== OFF)) {
124
+                dispatch(
125
+                    showStoppedRecordingNotification(
126
+                        updatedSessionData.mode));
127
+
128
+                if (updatedSessionData.mode
129
+                        === JitsiRecordingConstants.mode.FILE) {
122 130
                     dispatch(stopSound(RECORDING_ON_SOUND_ID));
123 131
                     dispatch(playSound(RECORDING_OFF_SOUND_ID));
124
-                    dispatch(showStoppedRecordingNotification());
125 132
                 }
126 133
             }
127 134
         }

+ 32
- 4
react/features/recording/reducer.js Wyświetl plik

@@ -1,18 +1,33 @@
1 1
 import { ReducerRegistry } from '../base/redux';
2
+import { PersistenceRegistry } from '../base/storage';
2 3
 import {
3 4
     CLEAR_RECORDING_SESSIONS,
4 5
     RECORDING_SESSION_UPDATED,
5
-    SET_PENDING_RECORDING_NOTIFICATION_UID
6
+    SET_PENDING_RECORDING_NOTIFICATION_UID,
7
+    SET_STREAM_KEY
6 8
 } from './actionTypes';
7 9
 
8 10
 const DEFAULT_STATE = {
11
+    pendingNotificationUids: {},
9 12
     sessionDatas: []
10 13
 };
11 14
 
15
+/**
16
+ * The name of the Redux store this feature stores its state in.
17
+ */
18
+const STORE_NAME = 'features/recording';
19
+
20
+/**
21
+ * Sets up the persistence of the feature {@code recording}.
22
+ */
23
+PersistenceRegistry.register(STORE_NAME, {
24
+    streamKey: true
25
+}, DEFAULT_STATE);
26
+
12 27
 /**
13 28
  * Reduces the Redux actions of the feature features/recording.
14 29
  */
15
-ReducerRegistry.register('features/recording',
30
+ReducerRegistry.register(STORE_NAME,
16 31
     (state = DEFAULT_STATE, action) => {
17 32
         switch (action.type) {
18 33
 
@@ -29,10 +44,23 @@ ReducerRegistry.register('features/recording',
29 44
                     _updateSessionDatas(state.sessionDatas, action.sessionData)
30 45
             };
31 46
 
32
-        case SET_PENDING_RECORDING_NOTIFICATION_UID:
47
+        case SET_PENDING_RECORDING_NOTIFICATION_UID: {
48
+            const pendingNotificationUids = {
49
+                ...state.pendingNotificationUids
50
+            };
51
+
52
+            pendingNotificationUids[action.streamType] = action.uid;
53
+
54
+            return {
55
+                ...state,
56
+                pendingNotificationUids
57
+            };
58
+        }
59
+
60
+        case SET_STREAM_KEY:
33 61
             return {
34 62
                 ...state,
35
-                pendingNotificationUid: action.uid
63
+                streamKey: action.streamKey
36 64
             };
37 65
 
38 66
         default:

+ 2
- 1
react/features/toolbox/components/native/OverflowMenu.js Wyświetl plik

@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
6 6
 import { BottomSheet, hideDialog } from '../../../base/dialog';
7 7
 import { AudioRouteButton } from '../../../mobile/audio-mode';
8 8
 import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
9
-import { RecordButton } from '../../../recording';
9
+import { LiveStreamButton, RecordButton } from '../../../recording';
10 10
 import { RoomLockButton } from '../../../room-lock';
11 11
 
12 12
 import AudioOnlyButton from './AudioOnlyButton';
@@ -70,6 +70,7 @@ class OverflowMenu extends Component<Props> {
70 70
                 <AudioOnlyButton { ...buttonProps } />
71 71
                 <RoomLockButton { ...buttonProps } />
72 72
                 <RecordButton { ...buttonProps } />
73
+                <LiveStreamButton { ...buttonProps } />
73 74
                 <PictureInPictureButton { ...buttonProps } />
74 75
             </BottomSheet>
75 76
         );

+ 5
- 131
react/features/toolbox/components/web/Toolbox.js Wyświetl plik

@@ -11,11 +11,9 @@ import {
11 11
 } from '../../../analytics';
12 12
 import { openDialog } from '../../../base/dialog';
13 13
 import { translate } from '../../../base/i18n';
14
-import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
15 14
 import {
16 15
     getLocalParticipant,
17 16
     getParticipants,
18
-    isLocalParticipantModerator,
19 17
     participantUpdated
20 18
 } from '../../../base/participants';
21 19
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
@@ -30,10 +28,8 @@ import {
30 28
 } from '../../../invite';
31 29
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
32 30
 import {
33
-    RecordButton,
34
-    StartLiveStreamDialog,
35
-    StopLiveStreamDialog,
36
-    getActiveSession
31
+    LiveStreamButton,
32
+    RecordButton
37 33
 } from '../../../recording';
38 34
 import {
39 35
     SETTINGS_TABS,
@@ -123,22 +119,6 @@ type Props = {
123 119
      */
124 120
     _isGuest: boolean,
125 121
 
126
-    /**
127
-     * The tooltip key to use when live streaming is disabled. Or undefined
128
-     * if non to be shown and the button to be hidden.
129
-     */
130
-    _liveStreamingDisabledTooltipKey: boolean,
131
-
132
-    /**
133
-     * Whether or not the live streaming feature is enabled for use.
134
-     */
135
-    _liveStreamingEnabled: boolean,
136
-
137
-    /**
138
-     * The current live streaming session, if any.
139
-     */
140
-    _liveStreamingSession: ?Object,
141
-
142 122
     /**
143 123
      * The ID of the local participant.
144 124
      */
@@ -230,8 +210,6 @@ class Toolbox extends Component<Props> {
230 210
             = this._onToolbarToggleEtherpad.bind(this);
231 211
         this._onToolbarToggleFullScreen
232 212
             = this._onToolbarToggleFullScreen.bind(this);
233
-        this._onToolbarToggleLiveStreaming
234
-            = this._onToolbarToggleLiveStreaming.bind(this);
235 213
         this._onToolbarToggleProfile
236 214
             = this._onToolbarToggleProfile.bind(this);
237 215
         this._onToolbarToggleRaiseHand
@@ -476,22 +454,6 @@ class Toolbox extends Component<Props> {
476 454
         this.props.dispatch(setFullScreen(fullScreen));
477 455
     }
478 456
 
479
-    /**
480
-     * Dispatches an action to show a dialog for starting or stopping a live
481
-     * streaming session.
482
-     *
483
-     * @private
484
-     * @returns {void}
485
-     */
486
-    _doToggleLiveStreaming() {
487
-        const { _liveStreamingSession } = this.props;
488
-        const dialogToDisplay = _liveStreamingSession
489
-            ? StopLiveStreamDialog : StartLiveStreamDialog;
490
-
491
-        this.props.dispatch(
492
-            openDialog(dialogToDisplay, { session: _liveStreamingSession }));
493
-    }
494
-
495 457
     /**
496 458
      * Dispatches an action to show or hide the profile edit panel.
497 459
      *
@@ -790,25 +752,6 @@ class Toolbox extends Component<Props> {
790 752
         this._doToggleFullScreen();
791 753
     }
792 754
 
793
-    _onToolbarToggleLiveStreaming: () => void;
794
-
795
-    /**
796
-     * Starts the process for enabling or disabling live streaming.
797
-     *
798
-     * @private
799
-     * @returns {void}
800
-     */
801
-    _onToolbarToggleLiveStreaming() {
802
-        sendAnalytics(createToolbarEvent(
803
-            'livestreaming.button',
804
-            {
805
-                'is_streaming': Boolean(this.props._liveStreamingSession),
806
-                type: JitsiRecordingConstants.mode.STREAM
807
-            }));
808
-
809
-        this._doToggleLiveStreaming();
810
-    }
811
-
812 755
     _onToolbarToggleProfile: () => void;
813 756
 
814 757
     /**
@@ -919,43 +862,6 @@ class Toolbox extends Component<Props> {
919 862
         );
920 863
     }
921 864
 
922
-    /**
923
-     * Renders an {@code OverflowMenuItem} to start or stop live streaming of
924
-     * the current conference.
925
-     *
926
-     * @private
927
-     * @returns {ReactElement}
928
-     */
929
-    _renderLiveStreamingButton() {
930
-        const {
931
-            _liveStreamingDisabledTooltipKey,
932
-            _liveStreamingEnabled,
933
-            _liveStreamingSession,
934
-            t
935
-        } = this.props;
936
-
937
-        const translationKey = _liveStreamingSession
938
-            ? 'dialog.stopLiveStreaming'
939
-            : 'dialog.startLiveStreaming';
940
-
941
-        return (
942
-            <OverflowMenuItem
943
-                accessibilityLabel
944
-                    = { t('dialog.accessibilityLabel.liveStreaming') }
945
-                disabled = { !_liveStreamingEnabled }
946
-                elementAfter = {
947
-                    <span className = 'beta-tag'>
948
-                        { t('recording.beta') }
949
-                    </span>
950
-                }
951
-                icon = 'icon-public'
952
-                key = 'livestreaming'
953
-                onClick = { this._onToolbarToggleLiveStreaming }
954
-                text = { t(translationKey) }
955
-                tooltip = { t(_liveStreamingDisabledTooltipKey) } />
956
-        );
957
-    }
958
-
959 865
     /**
960 866
      * Renders the list elements of the overflow menu.
961 867
      *
@@ -969,8 +875,6 @@ class Toolbox extends Component<Props> {
969 875
             _feedbackConfigured,
970 876
             _fullScreen,
971 877
             _isGuest,
972
-            _liveStreamingDisabledTooltipKey,
973
-            _liveStreamingEnabled,
974 878
             _sharingVideo,
975 879
             t
976 880
         } = this.props;
@@ -997,9 +901,9 @@ class Toolbox extends Component<Props> {
997 901
                     text = { _fullScreen
998 902
                         ? t('toolbar.exitFullScreen')
999 903
                         : t('toolbar.enterFullScreen') } />,
1000
-            (_liveStreamingEnabled || _liveStreamingDisabledTooltipKey)
1001
-                && this._shouldShowButton('livestreaming')
1002
-                && this._renderLiveStreamingButton(),
904
+            <LiveStreamButton
905
+                key = 'livestreaming'
906
+                showLabel = { true } />,
1003 907
             <RecordButton
1004 908
                 key = 'record'
1005 909
                 showLabel = { true } />,
@@ -1086,7 +990,6 @@ function _mapStateToProps(state) {
1086 990
         callStatsID,
1087 991
         iAmRecorder
1088 992
     } = state['features/base/config'];
1089
-    let { liveStreamingEnabled } = state['features/base/config'];
1090 993
     const sharedVideoStatus = state['features/shared-video'].status;
1091 994
     const { current } = state['features/side-panel'];
1092 995
     const {
@@ -1102,10 +1005,6 @@ function _mapStateToProps(state) {
1102 1005
     const dialOutEnabled = isDialOutEnabled(state);
1103 1006
 
1104 1007
     let desktopSharingDisabledTooltipKey;
1105
-    let liveStreamingDisabledTooltipKey;
1106
-
1107
-    liveStreamingEnabled
1108
-        = isLocalParticipantModerator(state) && liveStreamingEnabled;
1109 1008
 
1110 1009
     if (state['features/base/config'].enableFeaturesBasedOnToken) {
1111 1010
         // we enable desktop sharing if any participant already have this
@@ -1122,27 +1021,6 @@ function _mapStateToProps(state) {
1122 1021
             desktopSharingDisabledTooltipKey
1123 1022
                 = 'dialog.shareYourScreenDisabled';
1124 1023
         }
1125
-
1126
-        // we enable recording if the local participant have this
1127
-        // feature enabled
1128
-        const { features = {} } = localParticipant;
1129
-        const { isGuest } = state['features/base/jwt'];
1130
-
1131
-        liveStreamingEnabled
1132
-            = liveStreamingEnabled && String(features.livestreaming) === 'true';
1133
-
1134
-        // if the feature is disabled on purpose, do no show it, no tooltip
1135
-        if (!liveStreamingEnabled
1136
-            && String(features.livestreaming) !== 'disabled') {
1137
-            // button and tooltip
1138
-            if (isGuest) {
1139
-                liveStreamingDisabledTooltipKey
1140
-                    = 'dialog.liveStreamingDisabledForGuestTooltip';
1141
-            } else {
1142
-                liveStreamingDisabledTooltipKey
1143
-                    = 'dialog.liveStreamingDisabledTooltip';
1144
-            }
1145
-        }
1146 1024
     }
1147 1025
 
1148 1026
     return {
@@ -1158,10 +1036,6 @@ function _mapStateToProps(state) {
1158 1036
             iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
1159 1037
         _isGuest: state['features/base/jwt'].isGuest,
1160 1038
         _fullScreen: fullScreen,
1161
-        _liveStreamingDisabledTooltipKey: liveStreamingDisabledTooltipKey,
1162
-        _liveStreamingEnabled: liveStreamingEnabled,
1163
-        _liveStreamingSession:
1164
-             getActiveSession(state, JitsiRecordingConstants.mode.STREAM),
1165 1039
         _localParticipantID: localParticipant.id,
1166 1040
         _overflowMenuVisible: overflowMenuVisible,
1167 1041
         _raisedHand: localParticipant.raisedHand,

Ładowanie…
Anuluj
Zapisz