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
This commit is contained in:
parent
b5b99301ca
commit
823481dc1d
|
@ -333,7 +333,6 @@ var config = {
|
|||
// userRegion: "asia"
|
||||
}
|
||||
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
alwaysVisibleToolbar
|
||||
|
@ -353,6 +352,7 @@ var config = {
|
|||
etherpad_base
|
||||
externalConnectUrl
|
||||
firefox_fake_device
|
||||
googleApiApplicationClientID
|
||||
iAmRecorder
|
||||
iAmSipGateway
|
||||
peopleSearchQueryTypes
|
||||
|
|
|
@ -1,3 +1,81 @@
|
|||
.recordingSpinner {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.live-stream-dialog {
|
||||
/**
|
||||
* Set font-size to be consistent with Atlaskit FieldText.
|
||||
*/
|
||||
font-size: 14px;
|
||||
|
||||
.broadcast-dropdown,
|
||||
.broadcast-dropdown-trigger {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.live-stream-cta {
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.google-api {
|
||||
margin-top: 10px;
|
||||
min-height: 36px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Google sign in button must follow Google's design guidelines.
|
||||
* See: https://developers.google.com/identity/branding-guidelines
|
||||
*/
|
||||
.google-sign-in {
|
||||
background-color: #4285f4;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-family: Roboto, arial, sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 1px;
|
||||
|
||||
.google-cta {
|
||||
color: white;
|
||||
display: inline-block;
|
||||
/**
|
||||
* Hack the line height for vertical centering of text.
|
||||
*/
|
||||
line-height: 32px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.google-logo {
|
||||
background-color: white;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.google-panel {
|
||||
align-items: center;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stream-key-form {
|
||||
.helper-link {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 48 48" class="abcRioButtonSvg">
|
||||
<g>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<path fill="none" d="M0 0h48v48H0z"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 909 B |
|
@ -285,8 +285,8 @@
|
|||
"thankYou": "Thank you for using __appName__!",
|
||||
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
|
||||
"liveStreaming": "Live Streaming",
|
||||
"streamKey": "Stream name/key",
|
||||
"startLiveStreaming": "Start live streaming",
|
||||
"streamKey": "Live stream key",
|
||||
"startLiveStreaming": "Go live now",
|
||||
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
||||
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
||||
"stopLiveStreaming": "Stop live streaming",
|
||||
|
@ -396,14 +396,21 @@
|
|||
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
|
||||
"busyTitle": "All streamers are currently busy",
|
||||
"buttonTooltip": "Start / Stop Live Stream",
|
||||
"changeSignIn": "Switch accounts.",
|
||||
"choose": "Choose a live stream",
|
||||
"chooseCTA": "Choose a streaming option. You're currently logged in as __email__.",
|
||||
"enterStreamKey": "Enter your YouTube live stream key here.",
|
||||
"error": "Live Streaming failed. Please try again.",
|
||||
"errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.",
|
||||
"failedToStart": "Live Streaming failed to start",
|
||||
"off": "Live Streaming stopped",
|
||||
"on": "Live Streaming",
|
||||
"pending": "Starting Live Stream...",
|
||||
"serviceName": "Live Streaming service",
|
||||
"streamIdRequired": "Please fill in the stream id in order to launch the Live Streaming.",
|
||||
"streamIdHelp": "Where do I find this?",
|
||||
"signIn": "Sign in with Google",
|
||||
"signInCTA": "Sign in or enter your live stream key from YouTube.",
|
||||
"start": "Start a livestream",
|
||||
"streamIdHelp": "What's this?",
|
||||
"unavailableTitle": "Live Streaming unavailable"
|
||||
},
|
||||
"videoSIPGW":
|
||||
|
|
|
@ -20,6 +20,7 @@ import UIEvents from '../../../service/UI/UIEvents';
|
|||
import UIUtil from '../util/UIUtil';
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
|
||||
import { openDialog } from '../../../react/features/base/dialog';
|
||||
import {
|
||||
JitsiRecordingStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
|
@ -31,6 +32,8 @@ import {
|
|||
import { setToolboxEnabled } from '../../../react/features/toolbox';
|
||||
import { setNotificationsEnabled } from '../../../react/features/notifications';
|
||||
import {
|
||||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog,
|
||||
hideRecordingLabel,
|
||||
updateRecordingState
|
||||
} from '../../../react/features/recording';
|
||||
|
@ -102,91 +105,11 @@ function _isRecordingButtonEnabled() {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
function _requestLiveStreamId() {
|
||||
const cancelButton
|
||||
= APP.translation.generateTranslationHTML('dialog.Cancel');
|
||||
const backButton = APP.translation.generateTranslationHTML('dialog.Back');
|
||||
const startStreamingButton
|
||||
= APP.translation.generateTranslationHTML('dialog.startLiveStreaming');
|
||||
const streamIdRequired
|
||||
= APP.translation.generateTranslationHTML(
|
||||
'liveStreaming.streamIdRequired');
|
||||
const streamIdHelp
|
||||
= APP.translation.generateTranslationHTML(
|
||||
'liveStreaming.streamIdHelp');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog = APP.UI.messageHandler.openDialogWithStates({
|
||||
state0: {
|
||||
titleKey: 'dialog.liveStreaming',
|
||||
html:
|
||||
`<input class="input-control"
|
||||
name="streamId" type="text"
|
||||
data-i18n="[placeholder]dialog.streamKey"
|
||||
autofocus><div style="text-align: right">
|
||||
<a class="helper-link" target="_new"
|
||||
href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">${
|
||||
streamIdHelp
|
||||
}</a></div>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{ title: cancelButton,
|
||||
value: false },
|
||||
{ title: startStreamingButton,
|
||||
value: true }
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit(e, v, m, f) { // eslint-disable-line max-params
|
||||
e.preventDefault();
|
||||
|
||||
if (v) {
|
||||
if (f.streamId && f.streamId.length > 0) {
|
||||
resolve(UIUtil.escapeHtml(f.streamId));
|
||||
dialog.close();
|
||||
|
||||
return;
|
||||
}
|
||||
dialog.goToState('state1');
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
reject(APP.UI.messageHandler.CANCEL);
|
||||
dialog.close();
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
titleKey: 'dialog.liveStreaming',
|
||||
html: streamIdRequired,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{ title: cancelButton,
|
||||
value: false },
|
||||
{ title: backButton,
|
||||
value: true }
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit(e, v) {
|
||||
e.preventDefault();
|
||||
if (v === 0) {
|
||||
reject(APP.UI.messageHandler.CANCEL);
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.goToState('state0');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
close() {
|
||||
dialog = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Promise((resolve, reject) =>
|
||||
APP.store.dispatch(openDialog(StartLiveStreamDialog, {
|
||||
onCancel: reject,
|
||||
onSubmit: resolve
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,25 +155,20 @@ function _requestRecordingToken() {
|
|||
* @private
|
||||
*/
|
||||
function _showStopRecordingPrompt(recordingType) {
|
||||
let title;
|
||||
let message;
|
||||
let buttonKey;
|
||||
|
||||
if (recordingType === 'jibri') {
|
||||
title = 'dialog.liveStreaming';
|
||||
message = 'dialog.stopStreamingWarning';
|
||||
buttonKey = 'dialog.stopLiveStreaming';
|
||||
} else {
|
||||
title = 'dialog.recording';
|
||||
message = 'dialog.stopRecordingWarning';
|
||||
buttonKey = 'dialog.stopRecording';
|
||||
return new Promise((resolve, reject) => {
|
||||
APP.store.dispatch(openDialog(StopLiveStreamDialog, {
|
||||
onCancel: reject,
|
||||
onSubmit: resolve
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
||||
titleKey: title,
|
||||
msgKey: message,
|
||||
leftButtonKey: buttonKey,
|
||||
titleKey: 'dialog.recording',
|
||||
msgKey: 'dialog.stopRecordingWarning',
|
||||
leftButtonKey: 'dialog.stopRecording',
|
||||
submitFunction: (e, v) => (v ? resolve : reject)(),
|
||||
closeFunction: () => {
|
||||
dialog = null;
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import {
|
||||
DropdownItem,
|
||||
DropdownItemGroup,
|
||||
DropdownMenuStateless
|
||||
} from '@atlaskit/dropdown-menu';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* A dropdown to select a YouTube broadcast.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class BroadcastsDropdown extends PureComponent {
|
||||
/**
|
||||
* Default values for {@code StreamKeyForm} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
broadcasts: []
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code BroadcastsDropdown} component's property types.
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Broadcasts available for selection. Each broadcast item should be an
|
||||
* object with a title for display in the dropdown and a boundStreamID
|
||||
* to return in the {@link onBroadcastSelected} callback.
|
||||
*/
|
||||
broadcasts: PropTypes.array,
|
||||
|
||||
/**
|
||||
* Callback invoked when an item in the dropdown is selected. The
|
||||
* selected broadcast's boundStreamID will be passed back.
|
||||
*/
|
||||
onBroadcastSelected: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The boundStreamID of the broadcast that should display as selected in
|
||||
* the dropdown.
|
||||
*/
|
||||
selectedBroadcastID: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* The initial state of a {@code StreamKeyForm} instance.
|
||||
*
|
||||
* @type {{
|
||||
* isDropdownOpen: boolean
|
||||
* }}
|
||||
*/
|
||||
state = {
|
||||
isDropdownOpen: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code BroadcastsDropdown} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code BroadcastsDropdown} instance with.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onDropdownOpenChange = this._onDropdownOpenChange.bind(this);
|
||||
this._onSelect = this._onSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { broadcasts, selectedBroadcastID, t } = this.props;
|
||||
|
||||
const dropdownItems = broadcasts.map(broadcast =>
|
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<DropdownItem
|
||||
key = { broadcast.boundStreamID }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { () => this._onSelect(broadcast.boundStreamID) }>
|
||||
{ broadcast.title }
|
||||
</DropdownItem>
|
||||
);
|
||||
const selected = this.props.broadcasts.find(
|
||||
broadcast => broadcast.boundStreamID === selectedBroadcastID);
|
||||
const triggerText = (selected && selected.title)
|
||||
|| t('liveStreaming.choose');
|
||||
|
||||
return (
|
||||
<div className = 'broadcast-dropdown'>
|
||||
<DropdownMenuStateless
|
||||
isOpen = { this.state.isDropdownOpen }
|
||||
onItemActivated = { this._onSelect }
|
||||
onOpenChange = { this._onDropdownOpenChange }
|
||||
shouldFitContainer = { true }
|
||||
trigger = { triggerText }
|
||||
triggerButtonProps = {{
|
||||
className: 'broadcast-dropdown-trigger',
|
||||
shouldFitContainer: true
|
||||
}}
|
||||
triggerType = 'button'>
|
||||
<DropdownItemGroup>
|
||||
{ dropdownItems }
|
||||
</DropdownItemGroup>
|
||||
</DropdownMenuStateless>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the passed in broadcasts into an array of objects that can
|
||||
* be parsed by {@code DropdownMenuStateless}.
|
||||
*
|
||||
* @param {Array<Object>} broadcasts - The YouTube broadcasts to display.
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_formatBroadcasts(broadcasts) {
|
||||
return broadcasts.map(broadcast => {
|
||||
return {
|
||||
content: broadcast.title,
|
||||
value: broadcast
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dropdown to be displayed or not based on the passed in event.
|
||||
*
|
||||
* @param {Object} dropdownEvent - The event passed from
|
||||
* {@code DropdownMenuStateless} indicating if the dropdown should be open
|
||||
* or closed.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDropdownOpenChange(dropdownEvent) {
|
||||
this.setState({
|
||||
isDropdownOpen: dropdownEvent.isOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when an item has been clicked in the dropdown menu.
|
||||
*
|
||||
* @param {Object} boundStreamID - The bound stream ID for the selected
|
||||
* broadcast.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelect(boundStreamID) {
|
||||
this.props.onBroadcastSelected(boundStreamID);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(BroadcastsDropdown);
|
|
@ -0,0 +1,47 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* A React Component showing a button to sign in with Google.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class GoogleSignInButton extends Component {
|
||||
/**
|
||||
* {@code GoogleSignInButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The callback to invoke when the button is clicked.
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The text to display in the button.
|
||||
*/
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className = 'google-sign-in'
|
||||
onClick = { this.props.onClick }>
|
||||
<img
|
||||
className = 'google-logo'
|
||||
src = 'images/googleLogo.svg' />
|
||||
<div className = 'google-cta'>
|
||||
{ this.props.text }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,462 @@
|
|||
/* globals APP, interfaceConfig */
|
||||
|
||||
import Spinner from '@atlaskit/spinner';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import googleApi from '../../googleApi';
|
||||
|
||||
import BroadcastsDropdown from './BroadcastsDropdown';
|
||||
import GoogleSignInButton from './GoogleSignInButton';
|
||||
import StreamKeyForm from './StreamKeyForm';
|
||||
|
||||
/**
|
||||
* An enumeration of the different states the Google API can be in while
|
||||
* interacting with {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const GOOGLE_API_STATES = {
|
||||
/**
|
||||
* The state in which the Google API still needs to be loaded.
|
||||
*/
|
||||
NEEDS_LOADING: 0,
|
||||
|
||||
/**
|
||||
* The state in which the Google API is loaded and ready for use.
|
||||
*/
|
||||
LOADED: 1,
|
||||
|
||||
/**
|
||||
* The state in which a user has been logged in through the Google API.
|
||||
*/
|
||||
SIGNED_IN: 2,
|
||||
|
||||
/**
|
||||
* The state in which the Google API encountered an error either loading
|
||||
* or with an API request.
|
||||
*/
|
||||
ERROR: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* A React Component for requesting a YouTube stream key to use for live
|
||||
* streaming of the current conference.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StartLiveStreamDialog extends Component {
|
||||
/**
|
||||
* {@code StartLiveStreamDialog} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The ID for the Google web client application used for making stream
|
||||
* key related requests.
|
||||
*/
|
||||
_googleApiApplicationClientID: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the dialog is dismissed without submitting a
|
||||
* stream key.
|
||||
*/
|
||||
onCancel: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback to invoke when a stream key is submitted for use.
|
||||
*/
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code StartLiveStreamDialog} component's local state.
|
||||
*
|
||||
* @property {boolean} googleAPIState - The current state of interactions
|
||||
* with the Google API. Determines what Google related UI should display.
|
||||
* @property {Object[]|undefined} broadcasts - Details about the broadcasts
|
||||
* available for use for the logged in Google user's YouTube account.
|
||||
* @property {string} googleProfileEmail - The email of the user currently
|
||||
* logged in to the Google web client application.
|
||||
* @property {string} streamKey - The selected or entered stream key to use
|
||||
* for YouTube live streaming.
|
||||
*/
|
||||
state = {
|
||||
broadcasts: undefined,
|
||||
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
|
||||
googleProfileEmail: '',
|
||||
selectedBroadcastID: undefined,
|
||||
streamKey: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StartLiveStreamDialog} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code StartLiveStreamDialog} instance with.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* Instance variable used to flag whether the component is or is not
|
||||
* mounted. Used as a hack to avoid setting state on an unmounted
|
||||
* component.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._isMounted = false;
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
|
||||
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
|
||||
this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
|
||||
this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onYouTubeBroadcastIDSelected
|
||||
= this._onYouTubeBroadcastIDSelected.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link Component#componentDidMount()}. Invoked immediately
|
||||
* after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
|
||||
if (this.props._googleApiApplicationClientID) {
|
||||
this._onInitializeGoogleApi();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount()}. Invoked
|
||||
* immediately before this component is unmounted and destroyed.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _googleApiApplicationClientID } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancelTitleKey = 'dialog.Cancel'
|
||||
okTitleKey = 'dialog.startLiveStreaming'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'liveStreaming.start'
|
||||
width = { 'small' }>
|
||||
<div className = 'live-stream-dialog'>
|
||||
{ _googleApiApplicationClientID
|
||||
? this._renderYouTubePanel() : null }
|
||||
<StreamKeyForm
|
||||
helpURL = { interfaceConfig.LIVE_STREAMING_HELP_LINK }
|
||||
onChange = { this._onStreamKeyChange }
|
||||
value = { this.state.streamKey } />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Google web client application used for fetching stream keys.
|
||||
* If the user is already logged in, then a request for available YouTube
|
||||
* broadcasts is also made.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onInitializeGoogleApi() {
|
||||
return googleApi.get()
|
||||
.then(() => googleApi.initializeClient(
|
||||
this.props._googleApiApplicationClientID))
|
||||
.then(() => this._setStateIfMounted({
|
||||
googleAPIState: GOOGLE_API_STATES.LOADED
|
||||
}))
|
||||
.then(() => googleApi.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (isSignedIn) {
|
||||
return this._onGetYouTubeBroadcasts();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this._setStateIfMounted({
|
||||
googleAPIState: GOOGLE_API_STATES.ERROR
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the passed in {@link onCancel} callback and closes
|
||||
* {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} True is returned to close the modal.
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.onCancel(APP.UI.messageHandler.CANCEL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user to sign in, if not already signed in, and then requests a
|
||||
* list of the user's YouTube broadcasts.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onGetYouTubeBroadcasts() {
|
||||
return googleApi.get()
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => googleApi.getCurrentUserProfile())
|
||||
.then(profile => {
|
||||
this._setStateIfMounted({
|
||||
googleProfileEmail: profile.getEmail(),
|
||||
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
|
||||
});
|
||||
})
|
||||
.then(() => googleApi.requestAvailableYouTubeBroadcasts())
|
||||
.then(response => {
|
||||
const broadcasts = response.result.items.map(item => {
|
||||
return {
|
||||
title: item.snippet.title,
|
||||
boundStreamID: item.contentDetails.boundStreamId,
|
||||
status: item.status.lifeCycleStatus
|
||||
};
|
||||
});
|
||||
|
||||
this._setStateIfMounted({
|
||||
broadcasts
|
||||
});
|
||||
|
||||
if (broadcasts.length === 1 && !this.state.streamKey) {
|
||||
const broadcast = broadcasts[0];
|
||||
|
||||
this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
// Only show an error if an external request was made with the
|
||||
// Google api. Do not error if the login in canceled.
|
||||
if (response && response.result) {
|
||||
this._setStateIfMounted({
|
||||
googleAPIState: GOOGLE_API_STATES.ERROR
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the Google web client application to prompt for a sign in, such as
|
||||
* when changing account, and will then fetch available YouTube broadcasts.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRequestGoogleSignIn() {
|
||||
return googleApi.showAccountSelection()
|
||||
.then(() => this._setStateIfMounted({ broadcasts: undefined }))
|
||||
.then(() => this._onGetYouTubeBroadcasts());
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to update the {@code StartLiveStreamDialog} component's
|
||||
* display of the entered YouTube stream key.
|
||||
*
|
||||
* @param {Object} event - DOM Event for value change.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStreamKeyChange(event) {
|
||||
this._setStateIfMounted({
|
||||
streamKey: event.target.value,
|
||||
selectedBroadcastID: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the passed in {@link onSubmit} callback with the entered stream
|
||||
* key, and then closes {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} False if no stream key is entered to preventing
|
||||
* closing, true to close the modal.
|
||||
*/
|
||||
_onSubmit() {
|
||||
if (!this.state.streamKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.props.onSubmit(this.state.streamKey);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the stream key for a YouTube broadcast and updates the internal
|
||||
* state to display the associated stream key as being entered.
|
||||
*
|
||||
* @param {string} boundStreamID - The bound stream ID associated with the
|
||||
* broadcast from which to get the stream key.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onYouTubeBroadcastIDSelected(boundStreamID) {
|
||||
return googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
|
||||
.then(response => {
|
||||
const found = response.result.items[0];
|
||||
const streamKey = found.cdn.ingestionInfo.streamName;
|
||||
|
||||
this._setStateIfMounted({
|
||||
streamKey,
|
||||
selectedBroadcastID: boundStreamID
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a React Element for authenticating with the Google web client.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderYouTubePanel() {
|
||||
const { t } = this.props;
|
||||
const {
|
||||
broadcasts,
|
||||
googleProfileEmail,
|
||||
selectedBroadcastID
|
||||
} = this.state;
|
||||
|
||||
let googleContent, helpText;
|
||||
|
||||
switch (this.state.googleAPIState) {
|
||||
case GOOGLE_API_STATES.LOADED:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onGetYouTubeBroadcasts }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
helpText = t('liveStreaming.signInCTA');
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.SIGNED_IN:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<BroadcastsDropdown
|
||||
broadcasts = { broadcasts }
|
||||
onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
|
||||
selectedBroadcastID = { selectedBroadcastID } />
|
||||
);
|
||||
|
||||
/**
|
||||
* FIXME: Ideally this help text would be one translation string
|
||||
* that also accepts the anchor. This can be done using the Trans
|
||||
* component of react-i18next but I couldn't get it working...
|
||||
*/
|
||||
helpText = ( // eslint-disable-line no-extra-parens
|
||||
<div>
|
||||
{ `${t('liveStreaming.chooseCTA',
|
||||
{ email: googleProfileEmail })} ` }
|
||||
<a onClick = { this._onRequestGoogleSignIn }>
|
||||
{ t('liveStreaming.changeSignIn') }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.ERROR:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onRequestGoogleSignIn }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
);
|
||||
helpText = t('liveStreaming.errorAPI');
|
||||
|
||||
break;
|
||||
|
||||
case GOOGLE_API_STATES.NEEDS_LOADING:
|
||||
default:
|
||||
googleContent = ( // eslint-disable-line no-extra-parens
|
||||
<Spinner
|
||||
isCompleting = { false }
|
||||
size = 'medium' />
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'google-panel'>
|
||||
<div className = 'live-stream-cta'>
|
||||
{ helpText }
|
||||
</div>
|
||||
<div className = 'google-api'>
|
||||
{ googleContent }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state if the component is still mounted. This is a
|
||||
* workaround for all the state setting that occurs after ajax.
|
||||
*
|
||||
* @param {Object} newState - The new state to merge into the existing
|
||||
* state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setStateIfMounted(newState) {
|
||||
if (this._isMounted) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props of
|
||||
* {@code StartLiveStreamDialog}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* _googleApiApplicationClientID: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_googleApiApplicationClientID:
|
||||
state['features/base/config'].googleApiApplicationClientID
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));
|
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* A React Component for confirming the participant wishes to stop the currently
|
||||
* active live stream of the conference.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StopLiveStreamDialog extends Component {
|
||||
/**
|
||||
* {@code StopLiveStreamDialog} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Callback to invoke when the dialog is dismissed without confirming
|
||||
* the live stream should be stopped.
|
||||
*/
|
||||
onCancel: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback to invoke when confirming the live stream should be stopped.
|
||||
*/
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StopLiveStreamDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.stopLiveStreaming'
|
||||
onCancel = { this.props.onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.liveStreaming'
|
||||
width = 'small'>
|
||||
{ this.props.t('dialog.stopStreamingWarning') }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when stopping of live streaming is confirmed.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} True to close the modal.
|
||||
*/
|
||||
_onSubmit() {
|
||||
this.props.onSubmit();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(StopLiveStreamDialog);
|
|
@ -0,0 +1,115 @@
|
|||
import { FieldTextStateless } from '@atlaskit/field-text';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* A React Component for entering a key for starting a YouTube live stream.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StreamKeyForm extends Component {
|
||||
/**
|
||||
* {@code StreamKeyForm} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The URL to the page with more information for manually finding the
|
||||
* stream key for a YouTube broadcast.
|
||||
*/
|
||||
helpURL: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Callback invoked when the entered stream key has changed.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The stream key value to display as having been entered so far.
|
||||
*/
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StreamKeyForm} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code StreamKeyForm} instance with.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._onOpenHelp = this._onOpenHelp.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'stream-key-form'>
|
||||
<FieldTextStateless
|
||||
autoFocus = { true }
|
||||
compact = { true }
|
||||
label = { t('dialog.streamKey') }
|
||||
name = 'streamId'
|
||||
okDisabled = { !this.props.value }
|
||||
onChange = { this._onInputChange }
|
||||
placeholder = { t('liveStreaming.enterStreamKey') }
|
||||
shouldFitContainer = { true }
|
||||
type = 'text'
|
||||
value = { this.props.value } />
|
||||
{ this.props.helpURL
|
||||
? <div className = 'form-footer'>
|
||||
<a
|
||||
className = 'helper-link'
|
||||
onClick = { this._onOpenHelp }>
|
||||
{ t('liveStreaming.streamIdHelp') }
|
||||
</a>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the value of the input field has updated through
|
||||
* user input.
|
||||
*
|
||||
* @param {Object} event - DOM Event for value change.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onInputChange(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new tab with information on how to manually locate a YouTube
|
||||
* broadcast stream key.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelp() {
|
||||
window.open(this.props.helpURL, 'noopener');
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(StreamKeyForm);
|
|
@ -0,0 +1,2 @@
|
|||
export { default as StartLiveStreamDialog } from './StartLiveStreamDialog';
|
||||
export { default as StopLiveStreamDialog } from './StopLiveStreamDialog';
|
|
@ -1 +1,2 @@
|
|||
export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
|
||||
export { default as RecordingLabel } from './RecordingLabel';
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
|
||||
const GOOGLE_API_SCOPES = [
|
||||
'https://www.googleapis.com/auth/youtube.readonly'
|
||||
].join(' ');
|
||||
|
||||
/**
|
||||
* A promise for dynamically loading the Google API Client Library.
|
||||
*
|
||||
* @private
|
||||
* @type {Promise}
|
||||
*/
|
||||
let googleClientLoadPromise;
|
||||
|
||||
/**
|
||||
* A singleton for loading and interacting with the Google API.
|
||||
*/
|
||||
const googleApi = {
|
||||
/**
|
||||
* Obtains Google API Client Library, loading the library dynamically if
|
||||
* needed.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get() {
|
||||
const globalGoogleApi = this._getGoogleApiClient();
|
||||
|
||||
if (!globalGoogleApi) {
|
||||
return this.load();
|
||||
}
|
||||
|
||||
return Promise.resolve(globalGoogleApi);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the profile for the user signed in to the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCurrentUserProfile() {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._getGoogleApiClient()
|
||||
.auth2.getAuthInstance()
|
||||
.currentUser.get()
|
||||
.getBasicProfile();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the Google Web Client ID used for authenticating with Google and
|
||||
* making Google API requests.
|
||||
*
|
||||
* @param {string} clientId - The client ID to be used with the API library.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
initializeClient(clientId) {
|
||||
return this.get()
|
||||
.then(api => new Promise((resolve, reject) => {
|
||||
// setTimeout is used as a workaround for api.client.init not
|
||||
// resolving consistently when the Google API Client Library is
|
||||
// loaded asynchronously. See:
|
||||
// github.com/google/google-api-javascript-client/issues/399
|
||||
setTimeout(() => {
|
||||
api.client.init({
|
||||
clientId,
|
||||
scope: GOOGLE_API_SCOPES
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, 500);
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a user is currently authenticated with Google through an
|
||||
* initialized Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
isSignedIn() {
|
||||
return this.get()
|
||||
.then(api => Boolean(api
|
||||
&& api.auth2
|
||||
&& api.auth2.getAuthInstance
|
||||
&& api.auth2.getAuthInstance().isSignedIn
|
||||
&& api.auth2.getAuthInstance().isSignedIn.get()));
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a script tag and downloads the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load() {
|
||||
if (googleClientLoadPromise) {
|
||||
return googleClientLoadPromise;
|
||||
}
|
||||
|
||||
googleClientLoadPromise = new Promise((resolve, reject) => {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
||||
scriptTag.async = true;
|
||||
scriptTag.addEventListener('error', () => {
|
||||
scriptTag.remove();
|
||||
|
||||
googleClientLoadPromise = null;
|
||||
|
||||
reject();
|
||||
});
|
||||
scriptTag.addEventListener('load', resolve);
|
||||
scriptTag.type = 'text/javascript';
|
||||
|
||||
scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL;
|
||||
|
||||
document.head.appendChild(scriptTag);
|
||||
})
|
||||
.then(() => new Promise((resolve, reject) =>
|
||||
this._getGoogleApiClient().load('client:auth2', {
|
||||
callback: resolve,
|
||||
onerror: reject
|
||||
})))
|
||||
.then(() => this._getGoogleApiClient());
|
||||
|
||||
return googleClientLoadPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a request for a list of all YouTube broadcasts associated with
|
||||
* user currently signed in to the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestAvailableYouTubeBroadcasts() {
|
||||
const url = this._getURLForLiveBroadcasts();
|
||||
|
||||
return this.get()
|
||||
.then(api => api.client.request(url));
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a request to get all live streams associated with a broadcast
|
||||
* in YouTube.
|
||||
*
|
||||
* @param {string} boundStreamID - The bound stream ID associated with a
|
||||
* broadcast in YouTube.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestLiveStreamsForYouTubeBroadcast(boundStreamID) {
|
||||
const url = this._getURLForLiveStreams(boundStreamID);
|
||||
|
||||
return this.get()
|
||||
.then(api => api.client.request(url));
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library, even
|
||||
* if already signed in.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
showAccountSelection() {
|
||||
return this.get()
|
||||
.then(api => api.auth2.getAuthInstance().signIn());
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library, if
|
||||
* not already signed in.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
signInIfNotSignedIn() {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return this.showAccountSelection();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the global Google API Client Library object. Direct use of this
|
||||
* method is discouraged; instead use the {@link get} method.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
_getGoogleApiClient() {
|
||||
return window.gapi;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the URL to the Google API endpoint for retrieving the currently
|
||||
* signed in user's YouTube broadcasts.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getURLForLiveBroadcasts() {
|
||||
return [
|
||||
'https://content.googleapis.com/youtube/v3/liveBroadcasts',
|
||||
'?broadcastType=persistent',
|
||||
'&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus'
|
||||
].join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the URL to the Google API endpoint for retrieving the live
|
||||
* streams associated with a YouTube broadcast's bound stream.
|
||||
*
|
||||
* @param {string} boundStreamID - The bound stream ID associated with a
|
||||
* broadcast in YouTube.
|
||||
* @returns {string}
|
||||
*/
|
||||
_getURLForLiveStreams(boundStreamID) {
|
||||
return [
|
||||
'https://content.googleapis.com/youtube/v3/liveStreams',
|
||||
'?part=id%2Csnippet%2Ccdn%2Cstatus',
|
||||
`&id=${boundStreamID}`
|
||||
].join('');
|
||||
}
|
||||
};
|
||||
|
||||
export default googleApi;
|
Loading…
Reference in New Issue