浏览代码

feat(recording): use google api to get stream key (#2481)

* feat(recording): use google api to get stream key

* squash: renaming pass

* squash: return full load promise

* sqush: use google api state enum

* squash: workaround for lib not loading

* another new design...

* increase timeout workaround for gapi load issue

* styling pass

* tweak copy

* squash: auto select first broadcast
master
virtuacoplenny 6 年前
父节点
当前提交
823481dc1d

+ 1
- 1
config.js 查看文件

@@ -333,7 +333,6 @@ var config = {
333 333
         // userRegion: "asia"
334 334
     }
335 335
 
336
-
337 336
     // List of undocumented settings used in jitsi-meet
338 337
     /**
339 338
      alwaysVisibleToolbar
@@ -353,6 +352,7 @@ var config = {
353 352
      etherpad_base
354 353
      externalConnectUrl
355 354
      firefox_fake_device
355
+     googleApiApplicationClientID
356 356
      iAmRecorder
357 357
      iAmSipGateway
358 358
      peopleSearchQueryTypes

+ 78
- 0
css/_recording.scss 查看文件

@@ -1,3 +1,81 @@
1 1
 .recordingSpinner {
2 2
     vertical-align: top;
3 3
 }
4
+
5
+.live-stream-dialog {
6
+    /**
7
+     * Set font-size to be consistent with Atlaskit FieldText.
8
+     */
9
+    font-size: 14px;
10
+
11
+    .broadcast-dropdown,
12
+    .broadcast-dropdown-trigger {
13
+        text-align: left;
14
+    }
15
+
16
+    .form-footer {
17
+        text-align: right;
18
+    }
19
+
20
+    .live-stream-cta {
21
+        a {
22
+            cursor: pointer;
23
+        }
24
+    }
25
+
26
+    .google-api {
27
+        margin-top: 10px;
28
+        min-height: 36px;
29
+        text-align: center;
30
+        width: 100%;
31
+    }
32
+
33
+    /**
34
+     * The Google sign in button must follow Google's design guidelines.
35
+     * See: https://developers.google.com/identity/branding-guidelines
36
+     */
37
+    .google-sign-in {
38
+        background-color: #4285f4;
39
+        border-radius: 2px;
40
+        cursor: pointer;
41
+        display: inline-flex;
42
+        font-family: Roboto, arial, sans-serif;
43
+        font-size: 14px;
44
+        padding: 1px;
45
+
46
+        .google-cta {
47
+            color: white;
48
+            display: inline-block;
49
+            /**
50
+             * Hack the line height for vertical centering of text.
51
+             */
52
+            line-height: 32px;
53
+            margin: 0 15px;
54
+        }
55
+
56
+        .google-logo {
57
+            background-color: white;
58
+            border-radius: 2px;
59
+            display: inline-block;
60
+            padding: 8px;
61
+            height: 18px;
62
+            width: 18px;
63
+        }
64
+    }
65
+
66
+    .google-panel {
67
+        align-items: center;
68
+        border-bottom: 2px solid rgba(0, 0, 0, 0.3);
69
+        display: flex;
70
+        flex-direction: column;
71
+        padding-bottom: 10px;
72
+    }
73
+
74
+    .stream-key-form {
75
+        .helper-link {
76
+            display: inline-block;
77
+            cursor: pointer;
78
+            margin-top: 5px;
79
+        }
80
+    }
81
+}

+ 11
- 0
images/googleLogo.svg 查看文件

@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+
3
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 48 48" class="abcRioButtonSvg">
4
+  <g>
5
+    <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
6
+    <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
7
+    <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
8
+    <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
9
+    <path fill="none" d="M0 0h48v48H0z"></path>
10
+  </g>
11
+</svg>

+ 11
- 4
lang/main.json 查看文件

@@ -285,8 +285,8 @@
285 285
         "thankYou": "Thank you for using __appName__!",
286 286
         "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
287 287
         "liveStreaming": "Live Streaming",
288
-        "streamKey": "Stream name/key",
289
-        "startLiveStreaming": "Start live streaming",
288
+        "streamKey": "Live stream key",
289
+        "startLiveStreaming": "Go live now",
290 290
         "stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
291 291
         "stopRecordingWarning": "Are you sure you would like to stop the recording?",
292 292
         "stopLiveStreaming": "Stop live streaming",
@@ -396,14 +396,21 @@
396 396
         "busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
397 397
         "busyTitle": "All streamers are currently busy",
398 398
         "buttonTooltip": "Start / Stop Live Stream",
399
+        "changeSignIn": "Switch accounts.",
400
+        "choose": "Choose a live stream",
401
+        "chooseCTA": "Choose a streaming option. You're currently logged in as __email__.",
402
+        "enterStreamKey": "Enter your YouTube live stream key here.",
399 403
         "error": "Live Streaming failed. Please try again.",
404
+        "errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.",
400 405
         "failedToStart": "Live Streaming failed to start",
401 406
         "off": "Live Streaming stopped",
402 407
         "on": "Live Streaming",
403 408
         "pending": "Starting Live Stream...",
404 409
         "serviceName": "Live Streaming service",
405
-        "streamIdRequired": "Please fill in the stream id in order to launch the Live Streaming.",
406
-        "streamIdHelp": "Where do I find this?",
410
+        "signIn": "Sign in with Google",
411
+        "signInCTA": "Sign in or enter your live stream key from YouTube.",
412
+        "start": "Start a livestream",
413
+        "streamIdHelp": "What's this?",
407 414
         "unavailableTitle": "Live Streaming unavailable"
408 415
     },
409 416
     "videoSIPGW":

+ 17
- 99
modules/UI/recording/Recording.js 查看文件

@@ -20,6 +20,7 @@ import UIEvents from '../../../service/UI/UIEvents';
20 20
 import UIUtil from '../util/UIUtil';
21 21
 import VideoLayout from '../videolayout/VideoLayout';
22 22
 
23
+import { openDialog } from '../../../react/features/base/dialog';
23 24
 import {
24 25
     JitsiRecordingStatus
25 26
 } from '../../../react/features/base/lib-jitsi-meet';
@@ -31,6 +32,8 @@ import {
31 32
 import { setToolboxEnabled } from '../../../react/features/toolbox';
32 33
 import { setNotificationsEnabled } from '../../../react/features/notifications';
33 34
 import {
35
+    StartLiveStreamDialog,
36
+    StopLiveStreamDialog,
34 37
     hideRecordingLabel,
35 38
     updateRecordingState
36 39
 } from '../../../react/features/recording';
@@ -102,91 +105,11 @@ function _isRecordingButtonEnabled() {
102 105
  * @returns {Promise}
103 106
  */
104 107
 function _requestLiveStreamId() {
105
-    const cancelButton
106
-        = APP.translation.generateTranslationHTML('dialog.Cancel');
107
-    const backButton = APP.translation.generateTranslationHTML('dialog.Back');
108
-    const startStreamingButton
109
-        = APP.translation.generateTranslationHTML('dialog.startLiveStreaming');
110
-    const streamIdRequired
111
-        = APP.translation.generateTranslationHTML(
112
-            'liveStreaming.streamIdRequired');
113
-    const streamIdHelp
114
-        = APP.translation.generateTranslationHTML(
115
-            'liveStreaming.streamIdHelp');
116
-
117
-    return new Promise((resolve, reject) => {
118
-        dialog = APP.UI.messageHandler.openDialogWithStates({
119
-            state0: {
120
-                titleKey: 'dialog.liveStreaming',
121
-                html:
122
-                    `<input  class="input-control"
123
-                    name="streamId" type="text"
124
-                    data-i18n="[placeholder]dialog.streamKey"
125
-                    autofocus><div style="text-align: right">
126
-                    <a class="helper-link" target="_new"
127
-                    href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">${
128
-    streamIdHelp
129
-}</a></div>`,
130
-                persistent: false,
131
-                buttons: [
132
-                    { title: cancelButton,
133
-                        value: false },
134
-                    { title: startStreamingButton,
135
-                        value: true }
136
-                ],
137
-                focus: ':input:first',
138
-                defaultButton: 1,
139
-                submit(e, v, m, f) { // eslint-disable-line max-params
140
-                    e.preventDefault();
141
-
142
-                    if (v) {
143
-                        if (f.streamId && f.streamId.length > 0) {
144
-                            resolve(UIUtil.escapeHtml(f.streamId));
145
-                            dialog.close();
146
-
147
-                            return;
148
-                        }
149
-                        dialog.goToState('state1');
150
-
151
-                        return false;
152
-
153
-                    }
154
-                    reject(APP.UI.messageHandler.CANCEL);
155
-                    dialog.close();
156
-
157
-                    return false;
158
-
159
-                }
160
-            },
161
-
162
-            state1: {
163
-                titleKey: 'dialog.liveStreaming',
164
-                html: streamIdRequired,
165
-                persistent: false,
166
-                buttons: [
167
-                    { title: cancelButton,
168
-                        value: false },
169
-                    { title: backButton,
170
-                        value: true }
171
-                ],
172
-                focus: ':input:first',
173
-                defaultButton: 1,
174
-                submit(e, v) {
175
-                    e.preventDefault();
176
-                    if (v === 0) {
177
-                        reject(APP.UI.messageHandler.CANCEL);
178
-                        dialog.close();
179
-                    } else {
180
-                        dialog.goToState('state0');
181
-                    }
182
-                }
183
-            }
184
-        }, {
185
-            close() {
186
-                dialog = null;
187
-            }
188
-        });
189
-    });
108
+    return new Promise((resolve, reject) =>
109
+        APP.store.dispatch(openDialog(StartLiveStreamDialog, {
110
+            onCancel: reject,
111
+            onSubmit: resolve
112
+        })));
190 113
 }
191 114
 
192 115
 /**
@@ -232,25 +155,20 @@ function _requestRecordingToken() {
232 155
  * @private
233 156
  */
234 157
 function _showStopRecordingPrompt(recordingType) {
235
-    let title;
236
-    let message;
237
-    let buttonKey;
238
-
239 158
     if (recordingType === 'jibri') {
240
-        title = 'dialog.liveStreaming';
241
-        message = 'dialog.stopStreamingWarning';
242
-        buttonKey = 'dialog.stopLiveStreaming';
243
-    } else {
244
-        title = 'dialog.recording';
245
-        message = 'dialog.stopRecordingWarning';
246
-        buttonKey = 'dialog.stopRecording';
159
+        return new Promise((resolve, reject) => {
160
+            APP.store.dispatch(openDialog(StopLiveStreamDialog, {
161
+                onCancel: reject,
162
+                onSubmit: resolve
163
+            }));
164
+        });
247 165
     }
248 166
 
249 167
     return new Promise((resolve, reject) => {
250 168
         dialog = APP.UI.messageHandler.openTwoButtonDialog({
251
-            titleKey: title,
252
-            msgKey: message,
253
-            leftButtonKey: buttonKey,
169
+            titleKey: 'dialog.recording',
170
+            msgKey: 'dialog.stopRecordingWarning',
171
+            leftButtonKey: 'dialog.stopRecording',
254 172
             submitFunction: (e, v) => (v ? resolve : reject)(),
255 173
             closeFunction: () => {
256 174
                 dialog = null;

+ 0
- 0
react/features/recording/components/LiveStream/BroadcastsDropdown.native.js 查看文件


+ 168
- 0
react/features/recording/components/LiveStream/BroadcastsDropdown.web.js 查看文件

@@ -0,0 +1,168 @@
1
+import {
2
+    DropdownItem,
3
+    DropdownItemGroup,
4
+    DropdownMenuStateless
5
+} from '@atlaskit/dropdown-menu';
6
+import React, { PureComponent } from 'react';
7
+import PropTypes from 'prop-types';
8
+
9
+import { translate } from '../../../base/i18n';
10
+
11
+/**
12
+ * A dropdown to select a YouTube broadcast.
13
+ *
14
+ * @extends Component
15
+ */
16
+class BroadcastsDropdown extends PureComponent {
17
+    /**
18
+     * Default values for {@code StreamKeyForm} component's properties.
19
+     *
20
+     * @static
21
+     */
22
+    static defaultProps = {
23
+        broadcasts: []
24
+    };
25
+
26
+    /**
27
+     * {@code BroadcastsDropdown} component's property types.
28
+     */
29
+    static propTypes = {
30
+        /**
31
+         * Broadcasts available for selection. Each broadcast item should be an
32
+         * object with a title for display in the dropdown and a boundStreamID
33
+         * to return in the {@link onBroadcastSelected} callback.
34
+         */
35
+        broadcasts: PropTypes.array,
36
+
37
+        /**
38
+         * Callback invoked when an item in the dropdown is selected. The
39
+         * selected broadcast's boundStreamID will be passed back.
40
+         */
41
+        onBroadcastSelected: PropTypes.func,
42
+
43
+        /**
44
+         * The boundStreamID of the broadcast that should display as selected in
45
+         * the dropdown.
46
+         */
47
+        selectedBroadcastID: PropTypes.string,
48
+
49
+        /**
50
+         * Invoked to obtain translated strings.
51
+         */
52
+        t: PropTypes.func
53
+    };
54
+
55
+    /**
56
+     * The initial state of a {@code StreamKeyForm} instance.
57
+     *
58
+     * @type {{
59
+     *     isDropdownOpen: boolean
60
+     * }}
61
+     */
62
+    state = {
63
+        isDropdownOpen: false
64
+    };
65
+
66
+    /**
67
+     * Initializes a new {@code BroadcastsDropdown} instance.
68
+     *
69
+     * @param {Props} props - The React {@code Component} props to initialize
70
+     * the new {@code BroadcastsDropdown} instance with.
71
+     */
72
+    constructor(props) {
73
+        super(props);
74
+
75
+        // Bind event handlers so they are only bound once per instance.
76
+        this._onDropdownOpenChange = this._onDropdownOpenChange.bind(this);
77
+        this._onSelect = this._onSelect.bind(this);
78
+    }
79
+
80
+    /**
81
+     * Implements React's {@link Component#render()}.
82
+     *
83
+     * @inheritdoc
84
+     * @returns {ReactElement}
85
+     */
86
+    render() {
87
+        const { broadcasts, selectedBroadcastID, t } = this.props;
88
+
89
+        const dropdownItems = broadcasts.map(broadcast =>
90
+            // eslint-disable-next-line react/jsx-wrap-multilines
91
+            <DropdownItem
92
+                key = { broadcast.boundStreamID }
93
+                // eslint-disable-next-line react/jsx-no-bind
94
+                onClick = { () => this._onSelect(broadcast.boundStreamID) }>
95
+                { broadcast.title }
96
+            </DropdownItem>
97
+        );
98
+        const selected = this.props.broadcasts.find(
99
+            broadcast => broadcast.boundStreamID === selectedBroadcastID);
100
+        const triggerText = (selected && selected.title)
101
+            || t('liveStreaming.choose');
102
+
103
+        return (
104
+            <div className = 'broadcast-dropdown'>
105
+                <DropdownMenuStateless
106
+                    isOpen = { this.state.isDropdownOpen }
107
+                    onItemActivated = { this._onSelect }
108
+                    onOpenChange = { this._onDropdownOpenChange }
109
+                    shouldFitContainer = { true }
110
+                    trigger = { triggerText }
111
+                    triggerButtonProps = {{
112
+                        className: 'broadcast-dropdown-trigger',
113
+                        shouldFitContainer: true
114
+                    }}
115
+                    triggerType = 'button'>
116
+                    <DropdownItemGroup>
117
+                        { dropdownItems }
118
+                    </DropdownItemGroup>
119
+                </DropdownMenuStateless>
120
+            </div>
121
+        );
122
+    }
123
+
124
+    /**
125
+     * Transforms the passed in broadcasts into an array of objects that can
126
+     * be parsed by {@code DropdownMenuStateless}.
127
+     *
128
+     * @param {Array<Object>} broadcasts - The YouTube broadcasts to display.
129
+     * @private
130
+     * @returns {Array<Object>}
131
+     */
132
+    _formatBroadcasts(broadcasts) {
133
+        return broadcasts.map(broadcast => {
134
+            return {
135
+                content: broadcast.title,
136
+                value: broadcast
137
+            };
138
+        });
139
+    }
140
+
141
+    /**
142
+     * Sets the dropdown to be displayed or not based on the passed in event.
143
+     *
144
+     * @param {Object} dropdownEvent - The event passed from
145
+     * {@code DropdownMenuStateless} indicating if the dropdown should be open
146
+     * or closed.
147
+     * @private
148
+     * @returns {void}
149
+     */
150
+    _onDropdownOpenChange(dropdownEvent) {
151
+        this.setState({
152
+            isDropdownOpen: dropdownEvent.isOpen
153
+        });
154
+    }
155
+
156
+    /**
157
+     * Callback invoked when an item has been clicked in the dropdown menu.
158
+     *
159
+     * @param {Object} boundStreamID - The bound stream ID for the selected
160
+     * broadcast.
161
+     * @returns {void}
162
+     */
163
+    _onSelect(boundStreamID) {
164
+        this.props.onBroadcastSelected(boundStreamID);
165
+    }
166
+}
167
+
168
+export default translate(BroadcastsDropdown);

+ 0
- 0
react/features/recording/components/LiveStream/GoogleSignInButton.native.js 查看文件


+ 47
- 0
react/features/recording/components/LiveStream/GoogleSignInButton.web.js 查看文件

@@ -0,0 +1,47 @@
1
+import PropTypes from 'prop-types';
2
+import React, { Component } from 'react';
3
+
4
+/**
5
+ * A React Component showing a button to sign in with Google.
6
+ *
7
+ * @extends Component
8
+ */
9
+export default class GoogleSignInButton extends Component {
10
+    /**
11
+     * {@code GoogleSignInButton} component's property types.
12
+     *
13
+     * @static
14
+     */
15
+    static propTypes = {
16
+        /**
17
+         * The callback to invoke when the button is clicked.
18
+         */
19
+        onClick: PropTypes.func,
20
+
21
+        /**
22
+         * The text to display in the button.
23
+         */
24
+        text: PropTypes.string
25
+    };
26
+
27
+    /**
28
+     * Implements React's {@link Component#render()}.
29
+     *
30
+     * @inheritdoc
31
+     * @returns {ReactElement}
32
+     */
33
+    render() {
34
+        return (
35
+            <div
36
+                className = 'google-sign-in'
37
+                onClick = { this.props.onClick }>
38
+                <img
39
+                    className = 'google-logo'
40
+                    src = 'images/googleLogo.svg' />
41
+                <div className = 'google-cta'>
42
+                    { this.props.text }
43
+                </div>
44
+            </div>
45
+        );
46
+    }
47
+}

+ 0
- 0
react/features/recording/components/LiveStream/StartLiveStreamDialog.native.js 查看文件


+ 462
- 0
react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js 查看文件

@@ -0,0 +1,462 @@
1
+/* globals APP, interfaceConfig */
2
+
3
+import Spinner from '@atlaskit/spinner';
4
+import PropTypes from 'prop-types';
5
+import React, { Component } from 'react';
6
+import { connect } from 'react-redux';
7
+
8
+import { Dialog } from '../../../base/dialog';
9
+import { translate } from '../../../base/i18n';
10
+
11
+import googleApi from '../../googleApi';
12
+
13
+import BroadcastsDropdown from './BroadcastsDropdown';
14
+import GoogleSignInButton from './GoogleSignInButton';
15
+import StreamKeyForm from './StreamKeyForm';
16
+
17
+/**
18
+ * An enumeration of the different states the Google API can be in while
19
+ * interacting with {@code StartLiveStreamDialog}.
20
+  *
21
+ * @private
22
+ * @type {Object}
23
+ */
24
+const GOOGLE_API_STATES = {
25
+    /**
26
+     * The state in which the Google API still needs to be loaded.
27
+     */
28
+    NEEDS_LOADING: 0,
29
+
30
+    /**
31
+     * The state in which the Google API is loaded and ready for use.
32
+     */
33
+    LOADED: 1,
34
+
35
+    /**
36
+     * The state in which a user has been logged in through the Google API.
37
+     */
38
+    SIGNED_IN: 2,
39
+
40
+    /**
41
+     * The state in which the Google API encountered an error either loading
42
+     * or with an API request.
43
+     */
44
+    ERROR: 3
45
+};
46
+
47
+/**
48
+ * A React Component for requesting a YouTube stream key to use for live
49
+ * streaming of the current conference.
50
+ *
51
+ * @extends Component
52
+ */
53
+class StartLiveStreamDialog extends Component {
54
+    /**
55
+     * {@code StartLiveStreamDialog} component's property types.
56
+     *
57
+     * @static
58
+     */
59
+    static propTypes = {
60
+        /**
61
+         * The ID for the Google web client application used for making stream
62
+         * key related requests.
63
+         */
64
+        _googleApiApplicationClientID: PropTypes.string,
65
+
66
+        /**
67
+         * Callback to invoke when the dialog is dismissed without submitting a
68
+         * stream key.
69
+         */
70
+        onCancel: PropTypes.func,
71
+
72
+        /**
73
+         * Callback to invoke when a stream key is submitted for use.
74
+         */
75
+        onSubmit: PropTypes.func,
76
+
77
+        /**
78
+         * Invoked to obtain translated strings.
79
+         */
80
+        t: PropTypes.func
81
+    };
82
+
83
+    /**
84
+     * {@code StartLiveStreamDialog} component's local state.
85
+     *
86
+     * @property {boolean} googleAPIState - The current state of interactions
87
+     * with the Google API. Determines what Google related UI should display.
88
+     * @property {Object[]|undefined} broadcasts - Details about the broadcasts
89
+     * available for use for the logged in Google user's YouTube account.
90
+     * @property {string} googleProfileEmail - The email of the user currently
91
+     * logged in to the Google web client application.
92
+     * @property {string} streamKey - The selected or entered stream key to use
93
+     * for YouTube live streaming.
94
+     */
95
+    state = {
96
+        broadcasts: undefined,
97
+        googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
98
+        googleProfileEmail: '',
99
+        selectedBroadcastID: undefined,
100
+        streamKey: ''
101
+    };
102
+
103
+    /**
104
+     * Initializes a new {@code StartLiveStreamDialog} instance.
105
+     *
106
+     * @param {Props} props - The React {@code Component} props to initialize
107
+     * the new {@code StartLiveStreamDialog} instance with.
108
+     */
109
+    constructor(props) {
110
+        super(props);
111
+
112
+        /**
113
+         * Instance variable used to flag whether the component is or is not
114
+         * mounted. Used as a hack to avoid setting state on an unmounted
115
+         * component.
116
+         *
117
+         * @private
118
+         * @type {boolean}
119
+         */
120
+        this._isMounted = false;
121
+
122
+        // Bind event handlers so they are only bound once per instance.
123
+        this._onCancel = this._onCancel.bind(this);
124
+        this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
125
+        this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
126
+        this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
127
+        this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
128
+        this._onSubmit = this._onSubmit.bind(this);
129
+        this._onYouTubeBroadcastIDSelected
130
+            = this._onYouTubeBroadcastIDSelected.bind(this);
131
+    }
132
+
133
+    /**
134
+     * Implements {@link Component#componentDidMount()}. Invoked immediately
135
+     * after this component is mounted.
136
+     *
137
+     * @inheritdoc
138
+     * @returns {void}
139
+     */
140
+    componentDidMount() {
141
+        this._isMounted = true;
142
+
143
+        if (this.props._googleApiApplicationClientID) {
144
+            this._onInitializeGoogleApi();
145
+        }
146
+    }
147
+
148
+    /**
149
+     * Implements React's {@link Component#componentWillUnmount()}. Invoked
150
+     * immediately before this component is unmounted and destroyed.
151
+     *
152
+     * @inheritdoc
153
+     */
154
+    componentWillUnmount() {
155
+        this._isMounted = false;
156
+    }
157
+
158
+    /**
159
+     * Implements React's {@link Component#render()}.
160
+     *
161
+     * @inheritdoc
162
+     * @returns {ReactElement}
163
+     */
164
+    render() {
165
+        const { _googleApiApplicationClientID } = this.props;
166
+
167
+        return (
168
+            <Dialog
169
+                cancelTitleKey = 'dialog.Cancel'
170
+                okTitleKey = 'dialog.startLiveStreaming'
171
+                onCancel = { this._onCancel }
172
+                onSubmit = { this._onSubmit }
173
+                titleKey = 'liveStreaming.start'
174
+                width = { 'small' }>
175
+                <div className = 'live-stream-dialog'>
176
+                    { _googleApiApplicationClientID
177
+                        ? this._renderYouTubePanel() : null }
178
+                    <StreamKeyForm
179
+                        helpURL = { interfaceConfig.LIVE_STREAMING_HELP_LINK }
180
+                        onChange = { this._onStreamKeyChange }
181
+                        value = { this.state.streamKey } />
182
+                </div>
183
+            </Dialog>
184
+        );
185
+    }
186
+
187
+    /**
188
+     * Loads the Google web client application used for fetching stream keys.
189
+     * If the user is already logged in, then a request for available YouTube
190
+     * broadcasts is also made.
191
+     *
192
+     * @private
193
+     * @returns {Promise}
194
+     */
195
+    _onInitializeGoogleApi() {
196
+        return googleApi.get()
197
+            .then(() => googleApi.initializeClient(
198
+                this.props._googleApiApplicationClientID))
199
+            .then(() => this._setStateIfMounted({
200
+                googleAPIState: GOOGLE_API_STATES.LOADED
201
+            }))
202
+            .then(() => googleApi.isSignedIn())
203
+            .then(isSignedIn => {
204
+                if (isSignedIn) {
205
+                    return this._onGetYouTubeBroadcasts();
206
+                }
207
+            })
208
+            .catch(() => {
209
+                this._setStateIfMounted({
210
+                    googleAPIState: GOOGLE_API_STATES.ERROR
211
+                });
212
+            });
213
+    }
214
+
215
+    /**
216
+     * Invokes the passed in {@link onCancel} callback and closes
217
+     * {@code StartLiveStreamDialog}.
218
+     *
219
+     * @private
220
+     * @returns {boolean} True is returned to close the modal.
221
+     */
222
+    _onCancel() {
223
+        this.props.onCancel(APP.UI.messageHandler.CANCEL);
224
+
225
+        return true;
226
+    }
227
+
228
+    /**
229
+     * Asks the user to sign in, if not already signed in, and then requests a
230
+     * list of the user's YouTube broadcasts.
231
+     *
232
+     * @private
233
+     * @returns {Promise}
234
+     */
235
+    _onGetYouTubeBroadcasts() {
236
+        return googleApi.get()
237
+            .then(() => googleApi.signInIfNotSignedIn())
238
+            .then(() => googleApi.getCurrentUserProfile())
239
+            .then(profile => {
240
+                this._setStateIfMounted({
241
+                    googleProfileEmail: profile.getEmail(),
242
+                    googleAPIState: GOOGLE_API_STATES.SIGNED_IN
243
+                });
244
+            })
245
+            .then(() => googleApi.requestAvailableYouTubeBroadcasts())
246
+            .then(response => {
247
+                const broadcasts = response.result.items.map(item => {
248
+                    return {
249
+                        title: item.snippet.title,
250
+                        boundStreamID: item.contentDetails.boundStreamId,
251
+                        status: item.status.lifeCycleStatus
252
+                    };
253
+                });
254
+
255
+                this._setStateIfMounted({
256
+                    broadcasts
257
+                });
258
+
259
+                if (broadcasts.length === 1 && !this.state.streamKey) {
260
+                    const broadcast = broadcasts[0];
261
+
262
+                    this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
263
+                }
264
+            })
265
+            .catch(response => {
266
+                // Only show an error if an external request was made with the
267
+                // Google api. Do not error if the login in canceled.
268
+                if (response && response.result) {
269
+                    this._setStateIfMounted({
270
+                        googleAPIState: GOOGLE_API_STATES.ERROR
271
+                    });
272
+                }
273
+            });
274
+    }
275
+
276
+    /**
277
+     * Forces the Google web client application to prompt for a sign in, such as
278
+     * when changing account, and will then fetch available YouTube broadcasts.
279
+     *
280
+     * @private
281
+     * @returns {Promise}
282
+     */
283
+    _onRequestGoogleSignIn() {
284
+        return googleApi.showAccountSelection()
285
+            .then(() => this._setStateIfMounted({ broadcasts: undefined }))
286
+            .then(() => this._onGetYouTubeBroadcasts());
287
+    }
288
+
289
+    /**
290
+     * Callback invoked to update the {@code StartLiveStreamDialog} component's
291
+     * display of the entered YouTube stream key.
292
+     *
293
+     * @param {Object} event - DOM Event for value change.
294
+     * @private
295
+     * @returns {void}
296
+     */
297
+    _onStreamKeyChange(event) {
298
+        this._setStateIfMounted({
299
+            streamKey: event.target.value,
300
+            selectedBroadcastID: undefined
301
+        });
302
+    }
303
+
304
+    /**
305
+     * Invokes the passed in {@link onSubmit} callback with the entered stream
306
+     * key, and then closes {@code StartLiveStreamDialog}.
307
+     *
308
+     * @private
309
+     * @returns {boolean} False if no stream key is entered to preventing
310
+     * closing, true to close the modal.
311
+     */
312
+    _onSubmit() {
313
+        if (!this.state.streamKey) {
314
+            return false;
315
+        }
316
+
317
+        this.props.onSubmit(this.state.streamKey);
318
+
319
+        return true;
320
+    }
321
+
322
+    /**
323
+     * Fetches the stream key for a YouTube broadcast and updates the internal
324
+     * state to display the associated stream key as being entered.
325
+     *
326
+     * @param {string} boundStreamID - The bound stream ID associated with the
327
+     * broadcast from which to get the stream key.
328
+     * @private
329
+     * @returns {Promise}
330
+     */
331
+    _onYouTubeBroadcastIDSelected(boundStreamID) {
332
+        return googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
333
+            .then(response => {
334
+                const found = response.result.items[0];
335
+                const streamKey = found.cdn.ingestionInfo.streamName;
336
+
337
+                this._setStateIfMounted({
338
+                    streamKey,
339
+                    selectedBroadcastID: boundStreamID
340
+                });
341
+            });
342
+    }
343
+
344
+    /**
345
+     * Renders a React Element for authenticating with the Google web client.
346
+     *
347
+     * @private
348
+     * @returns {ReactElement}
349
+     */
350
+    _renderYouTubePanel() {
351
+        const { t } = this.props;
352
+        const {
353
+            broadcasts,
354
+            googleProfileEmail,
355
+            selectedBroadcastID
356
+        } = this.state;
357
+
358
+        let googleContent, helpText;
359
+
360
+        switch (this.state.googleAPIState) {
361
+        case GOOGLE_API_STATES.LOADED:
362
+            googleContent = ( // eslint-disable-line no-extra-parens
363
+                <GoogleSignInButton
364
+                    onClick = { this._onGetYouTubeBroadcasts }
365
+                    text = { t('liveStreaming.signIn') } />
366
+            );
367
+            helpText = t('liveStreaming.signInCTA');
368
+
369
+            break;
370
+
371
+        case GOOGLE_API_STATES.SIGNED_IN:
372
+            googleContent = ( // eslint-disable-line no-extra-parens
373
+                <BroadcastsDropdown
374
+                    broadcasts = { broadcasts }
375
+                    onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
376
+                    selectedBroadcastID = { selectedBroadcastID } />
377
+            );
378
+
379
+            /**
380
+             * FIXME: Ideally this help text would be one translation string
381
+             * that also accepts the anchor. This can be done using the Trans
382
+             * component of react-i18next but I couldn't get it working...
383
+             */
384
+            helpText = ( // eslint-disable-line no-extra-parens
385
+                <div>
386
+                    { `${t('liveStreaming.chooseCTA',
387
+                        { email: googleProfileEmail })} ` }
388
+                    <a onClick = { this._onRequestGoogleSignIn }>
389
+                        { t('liveStreaming.changeSignIn') }
390
+                    </a>
391
+                </div>
392
+            );
393
+
394
+            break;
395
+
396
+        case GOOGLE_API_STATES.ERROR:
397
+            googleContent = ( // eslint-disable-line no-extra-parens
398
+                <GoogleSignInButton
399
+                    onClick = { this._onRequestGoogleSignIn }
400
+                    text = { t('liveStreaming.signIn') } />
401
+            );
402
+            helpText = t('liveStreaming.errorAPI');
403
+
404
+            break;
405
+
406
+        case GOOGLE_API_STATES.NEEDS_LOADING:
407
+        default:
408
+            googleContent = ( // eslint-disable-line no-extra-parens
409
+                <Spinner
410
+                    isCompleting = { false }
411
+                    size = 'medium' />
412
+            );
413
+
414
+            break;
415
+        }
416
+
417
+        return (
418
+            <div className = 'google-panel'>
419
+                <div className = 'live-stream-cta'>
420
+                    { helpText }
421
+                </div>
422
+                <div className = 'google-api'>
423
+                    { googleContent }
424
+                </div>
425
+            </div>
426
+        );
427
+    }
428
+
429
+    /**
430
+     * Updates the internal state if the component is still mounted. This is a
431
+     * workaround for all the state setting that occurs after ajax.
432
+     *
433
+     * @param {Object} newState - The new state to merge into the existing
434
+     * state.
435
+     * @private
436
+     * @returns {void}
437
+     */
438
+    _setStateIfMounted(newState) {
439
+        if (this._isMounted) {
440
+            this.setState(newState);
441
+        }
442
+    }
443
+}
444
+
445
+/**
446
+ * Maps (parts of) the redux state to the React {@code Component} props of
447
+ * {@code StartLiveStreamDialog}.
448
+ *
449
+ * @param {Object} state - The redux state.
450
+ * @protected
451
+ * @returns {{
452
+ *     _googleApiApplicationClientID: string
453
+ * }}
454
+ */
455
+function _mapStateToProps(state) {
456
+    return {
457
+        _googleApiApplicationClientID:
458
+            state['features/base/config'].googleApiApplicationClientID
459
+    };
460
+}
461
+
462
+export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

+ 0
- 0
react/features/recording/components/LiveStream/StopLiveStreamDialog.native.js 查看文件


+ 82
- 0
react/features/recording/components/LiveStream/StopLiveStreamDialog.web.js 查看文件

@@ -0,0 +1,82 @@
1
+import PropTypes from 'prop-types';
2
+import React, { Component } from 'react';
3
+
4
+import { Dialog } from '../../../base/dialog';
5
+import { translate } from '../../../base/i18n';
6
+
7
+/**
8
+ * A React Component for confirming the participant wishes to stop the currently
9
+ * active live stream of the conference.
10
+ *
11
+ * @extends Component
12
+ */
13
+class StopLiveStreamDialog extends Component {
14
+    /**
15
+     * {@code StopLiveStreamDialog} component's property types.
16
+     *
17
+     * @static
18
+     */
19
+    static propTypes = {
20
+        /**
21
+         * Callback to invoke when the dialog is dismissed without confirming
22
+         * the live stream should be stopped.
23
+         */
24
+        onCancel: PropTypes.func,
25
+
26
+        /**
27
+         * Callback to invoke when confirming the live stream should be stopped.
28
+         */
29
+        onSubmit: PropTypes.func,
30
+
31
+        /**
32
+         * Invoked to obtain translated strings.
33
+         */
34
+        t: PropTypes.func
35
+    };
36
+
37
+    /**
38
+     * Initializes a new {@code StopLiveStreamDialog} instance.
39
+     *
40
+     * @param {Object} props - The read-only properties with which the new
41
+     * instance is to be initialized.
42
+     */
43
+    constructor(props) {
44
+        super(props);
45
+
46
+        // Bind event handler so it is only bound once for every instance.
47
+        this._onSubmit = this._onSubmit.bind(this);
48
+    }
49
+
50
+    /**
51
+     * Implements React's {@link Component#render()}.
52
+     *
53
+     * @inheritdoc
54
+     * @returns {ReactElement}
55
+     */
56
+    render() {
57
+        return (
58
+            <Dialog
59
+                okTitleKey = 'dialog.stopLiveStreaming'
60
+                onCancel = { this.props.onCancel }
61
+                onSubmit = { this._onSubmit }
62
+                titleKey = 'dialog.liveStreaming'
63
+                width = 'small'>
64
+                { this.props.t('dialog.stopStreamingWarning') }
65
+            </Dialog>
66
+        );
67
+    }
68
+
69
+    /**
70
+     * Callback invoked when stopping of live streaming is confirmed.
71
+     *
72
+     * @private
73
+     * @returns {boolean} True to close the modal.
74
+     */
75
+    _onSubmit() {
76
+        this.props.onSubmit();
77
+
78
+        return true;
79
+    }
80
+}
81
+
82
+export default translate(StopLiveStreamDialog);

+ 0
- 0
react/features/recording/components/LiveStream/StreamKeyForm.native.js 查看文件


+ 115
- 0
react/features/recording/components/LiveStream/StreamKeyForm.web.js 查看文件

@@ -0,0 +1,115 @@
1
+import { FieldTextStateless } from '@atlaskit/field-text';
2
+import PropTypes from 'prop-types';
3
+import React, { Component } from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+
7
+/**
8
+ * A React Component for entering a key for starting a YouTube live stream.
9
+ *
10
+ * @extends Component
11
+ */
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
+    };
40
+
41
+    /**
42
+     * Initializes a new {@code StreamKeyForm} instance.
43
+     *
44
+     * @param {Props} props - The React {@code Component} props to initialize
45
+     * the new {@code StreamKeyForm} instance with.
46
+     */
47
+    constructor(props) {
48
+        super(props);
49
+
50
+        // Bind event handlers so they are only bound once per instance.
51
+        this._onInputChange = this._onInputChange.bind(this);
52
+        this._onOpenHelp = this._onOpenHelp.bind(this);
53
+    }
54
+
55
+    /**
56
+     * Implements React's {@link Component#render()}.
57
+     *
58
+     * @inheritdoc
59
+     * @returns {ReactElement}
60
+     */
61
+    render() {
62
+        const { t } = this.props;
63
+
64
+        return (
65
+            <div className = 'stream-key-form'>
66
+                <FieldTextStateless
67
+                    autoFocus = { true }
68
+                    compact = { true }
69
+                    label = { t('dialog.streamKey') }
70
+                    name = 'streamId'
71
+                    okDisabled = { !this.props.value }
72
+                    onChange = { this._onInputChange }
73
+                    placeholder = { t('liveStreaming.enterStreamKey') }
74
+                    shouldFitContainer = { true }
75
+                    type = 'text'
76
+                    value = { this.props.value } />
77
+                { this.props.helpURL
78
+                    ? <div className = 'form-footer'>
79
+                        <a
80
+                            className = 'helper-link'
81
+                            onClick = { this._onOpenHelp }>
82
+                            { t('liveStreaming.streamIdHelp') }
83
+                        </a>
84
+                    </div>
85
+                    : null
86
+                }
87
+            </div>
88
+        );
89
+    }
90
+
91
+    /**
92
+     * Callback invoked when the value of the input field has updated through
93
+     * user input.
94
+     *
95
+     * @param {Object} event - DOM Event for value change.
96
+     * @private
97
+     * @returns {void}
98
+     */
99
+    _onInputChange(event) {
100
+        this.props.onChange(event);
101
+    }
102
+
103
+    /**
104
+     * Opens a new tab with information on how to manually locate a YouTube
105
+     * broadcast stream key.
106
+     *
107
+     * @private
108
+     * @returns {void}
109
+     */
110
+    _onOpenHelp() {
111
+        window.open(this.props.helpURL, 'noopener');
112
+    }
113
+}
114
+
115
+export default translate(StreamKeyForm);

+ 2
- 0
react/features/recording/components/LiveStream/index.js 查看文件

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

+ 1
- 0
react/features/recording/components/index.js 查看文件

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

+ 230
- 0
react/features/recording/googleApi.js 查看文件

@@ -0,0 +1,230 @@
1
+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
+
6
+/**
7
+ * A promise for dynamically loading the Google API Client Library.
8
+ *
9
+ * @private
10
+ * @type {Promise}
11
+ */
12
+let googleClientLoadPromise;
13
+
14
+/**
15
+ * A singleton for loading and interacting with the Google API.
16
+ */
17
+const googleApi = {
18
+    /**
19
+     * Obtains Google API Client Library, loading the library dynamically if
20
+     * needed.
21
+     *
22
+     * @returns {Promise}
23
+     */
24
+    get() {
25
+        const globalGoogleApi = this._getGoogleApiClient();
26
+
27
+        if (!globalGoogleApi) {
28
+            return this.load();
29
+        }
30
+
31
+        return Promise.resolve(globalGoogleApi);
32
+    },
33
+
34
+    /**
35
+     * Gets the profile for the user signed in to the Google API Client Library.
36
+     *
37
+     * @returns {Promise}
38
+     */
39
+    getCurrentUserProfile() {
40
+        return this.get()
41
+            .then(() => this.isSignedIn())
42
+            .then(isSignedIn => {
43
+                if (!isSignedIn) {
44
+                    return null;
45
+                }
46
+
47
+                return this._getGoogleApiClient()
48
+                    .auth2.getAuthInstance()
49
+                    .currentUser.get()
50
+                    .getBasicProfile();
51
+            });
52
+    },
53
+
54
+    /**
55
+     * Sets the Google Web Client ID used for authenticating with Google and
56
+     * making Google API requests.
57
+     *
58
+     * @param {string} clientId - The client ID to be used with the API library.
59
+     * @returns {Promise}
60
+     */
61
+    initializeClient(clientId) {
62
+        return this.get()
63
+            .then(api => new Promise((resolve, reject) => {
64
+                // setTimeout is used as a workaround for api.client.init not
65
+                // resolving consistently when the Google API Client Library is
66
+                // loaded asynchronously. See:
67
+                // github.com/google/google-api-javascript-client/issues/399
68
+                setTimeout(() => {
69
+                    api.client.init({
70
+                        clientId,
71
+                        scope: GOOGLE_API_SCOPES
72
+                    })
73
+                    .then(resolve)
74
+                    .catch(reject);
75
+                }, 500);
76
+            }));
77
+    },
78
+
79
+    /**
80
+     * Checks whether a user is currently authenticated with Google through an
81
+     * initialized Google API Client Library.
82
+     *
83
+     * @returns {Promise}
84
+     */
85
+    isSignedIn() {
86
+        return this.get()
87
+            .then(api => Boolean(api
88
+                && api.auth2
89
+                && api.auth2.getAuthInstance
90
+                && api.auth2.getAuthInstance().isSignedIn
91
+                && api.auth2.getAuthInstance().isSignedIn.get()));
92
+    },
93
+
94
+    /**
95
+     * Generates a script tag and downloads the Google API Client Library.
96
+     *
97
+     * @returns {Promise}
98
+     */
99
+    load() {
100
+        if (googleClientLoadPromise) {
101
+            return googleClientLoadPromise;
102
+        }
103
+
104
+        googleClientLoadPromise = new Promise((resolve, reject) => {
105
+            const scriptTag = document.createElement('script');
106
+
107
+            scriptTag.async = true;
108
+            scriptTag.addEventListener('error', () => {
109
+                scriptTag.remove();
110
+
111
+                googleClientLoadPromise = null;
112
+
113
+                reject();
114
+            });
115
+            scriptTag.addEventListener('load', resolve);
116
+            scriptTag.type = 'text/javascript';
117
+
118
+            scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL;
119
+
120
+            document.head.appendChild(scriptTag);
121
+        })
122
+            .then(() => new Promise((resolve, reject) =>
123
+                this._getGoogleApiClient().load('client:auth2', {
124
+                    callback: resolve,
125
+                    onerror: reject
126
+                })))
127
+            .then(() => this._getGoogleApiClient());
128
+
129
+        return googleClientLoadPromise;
130
+    },
131
+
132
+    /**
133
+     * Executes a request for a list of all YouTube broadcasts associated with
134
+     * user currently signed in to the Google API Client Library.
135
+     *
136
+     * @returns {Promise}
137
+     */
138
+    requestAvailableYouTubeBroadcasts() {
139
+        const url = this._getURLForLiveBroadcasts();
140
+
141
+        return this.get()
142
+            .then(api => api.client.request(url));
143
+    },
144
+
145
+    /**
146
+     * Executes a request to get all live streams associated with a broadcast
147
+     * in YouTube.
148
+     *
149
+     * @param {string} boundStreamID - The bound stream ID associated with a
150
+     * broadcast in YouTube.
151
+     * @returns {Promise}
152
+     */
153
+    requestLiveStreamsForYouTubeBroadcast(boundStreamID) {
154
+        const url = this._getURLForLiveStreams(boundStreamID);
155
+
156
+        return this.get()
157
+            .then(api => api.client.request(url));
158
+    },
159
+
160
+    /**
161
+     * Prompts the participant to sign in to the Google API Client Library, even
162
+     * if already signed in.
163
+     *
164
+     * @returns {Promise}
165
+     */
166
+    showAccountSelection() {
167
+        return this.get()
168
+            .then(api => api.auth2.getAuthInstance().signIn());
169
+    },
170
+
171
+    /**
172
+     * Prompts the participant to sign in to the Google API Client Library, if
173
+     * not already signed in.
174
+     *
175
+     * @returns {Promise}
176
+     */
177
+    signInIfNotSignedIn() {
178
+        return this.get()
179
+            .then(() => this.isSignedIn())
180
+            .then(isSignedIn => {
181
+                if (!isSignedIn) {
182
+                    return this.showAccountSelection();
183
+                }
184
+            });
185
+    },
186
+
187
+    /**
188
+     * Returns the global Google API Client Library object. Direct use of this
189
+     * method is discouraged; instead use the {@link get} method.
190
+     *
191
+     * @private
192
+     * @returns {Object|undefined}
193
+     */
194
+    _getGoogleApiClient() {
195
+        return window.gapi;
196
+    },
197
+
198
+    /**
199
+     * Returns the URL to the Google API endpoint for retrieving the currently
200
+     * signed in user's YouTube broadcasts.
201
+     *
202
+     * @private
203
+     * @returns {string}
204
+     */
205
+    _getURLForLiveBroadcasts() {
206
+        return [
207
+            'https://content.googleapis.com/youtube/v3/liveBroadcasts',
208
+            '?broadcastType=persistent',
209
+            '&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus'
210
+        ].join('');
211
+    },
212
+
213
+    /**
214
+     * Returns the URL to the Google API endpoint for retrieving the live
215
+     * streams associated with a YouTube broadcast's bound stream.
216
+     *
217
+     * @param {string} boundStreamID - The bound stream ID associated with a
218
+     * broadcast in YouTube.
219
+     * @returns {string}
220
+     */
221
+    _getURLForLiveStreams(boundStreamID) {
222
+        return [
223
+            'https://content.googleapis.com/youtube/v3/liveStreams',
224
+            '?part=id%2Csnippet%2Ccdn%2Cstatus',
225
+            `&id=${boundStreamID}`
226
+        ].join('');
227
+    }
228
+};
229
+
230
+export default googleApi;

正在加载...
取消
保存