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"
|
// userRegion: "asia"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// List of undocumented settings used in jitsi-meet
|
// List of undocumented settings used in jitsi-meet
|
||||||
/**
|
/**
|
||||||
alwaysVisibleToolbar
|
alwaysVisibleToolbar
|
||||||
|
@ -353,6 +352,7 @@ var config = {
|
||||||
etherpad_base
|
etherpad_base
|
||||||
externalConnectUrl
|
externalConnectUrl
|
||||||
firefox_fake_device
|
firefox_fake_device
|
||||||
|
googleApiApplicationClientID
|
||||||
iAmRecorder
|
iAmRecorder
|
||||||
iAmSipGateway
|
iAmSipGateway
|
||||||
peopleSearchQueryTypes
|
peopleSearchQueryTypes
|
||||||
|
|
|
@ -1,3 +1,81 @@
|
||||||
.recordingSpinner {
|
.recordingSpinner {
|
||||||
vertical-align: top;
|
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__!",
|
"thankYou": "Thank you for using __appName__!",
|
||||||
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
|
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
|
||||||
"liveStreaming": "Live Streaming",
|
"liveStreaming": "Live Streaming",
|
||||||
"streamKey": "Stream name/key",
|
"streamKey": "Live stream key",
|
||||||
"startLiveStreaming": "Start live streaming",
|
"startLiveStreaming": "Go live now",
|
||||||
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
||||||
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
||||||
"stopLiveStreaming": "Stop live streaming",
|
"stopLiveStreaming": "Stop live streaming",
|
||||||
|
@ -396,14 +396,21 @@
|
||||||
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
|
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
|
||||||
"busyTitle": "All streamers are currently busy",
|
"busyTitle": "All streamers are currently busy",
|
||||||
"buttonTooltip": "Start / Stop Live Stream",
|
"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.",
|
"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",
|
"failedToStart": "Live Streaming failed to start",
|
||||||
"off": "Live Streaming stopped",
|
"off": "Live Streaming stopped",
|
||||||
"on": "Live Streaming",
|
"on": "Live Streaming",
|
||||||
"pending": "Starting Live Stream...",
|
"pending": "Starting Live Stream...",
|
||||||
"serviceName": "Live Streaming service",
|
"serviceName": "Live Streaming service",
|
||||||
"streamIdRequired": "Please fill in the stream id in order to launch the Live Streaming.",
|
"signIn": "Sign in with Google",
|
||||||
"streamIdHelp": "Where do I find this?",
|
"signInCTA": "Sign in or enter your live stream key from YouTube.",
|
||||||
|
"start": "Start a livestream",
|
||||||
|
"streamIdHelp": "What's this?",
|
||||||
"unavailableTitle": "Live Streaming unavailable"
|
"unavailableTitle": "Live Streaming unavailable"
|
||||||
},
|
},
|
||||||
"videoSIPGW":
|
"videoSIPGW":
|
||||||
|
|
|
@ -20,6 +20,7 @@ import UIEvents from '../../../service/UI/UIEvents';
|
||||||
import UIUtil from '../util/UIUtil';
|
import UIUtil from '../util/UIUtil';
|
||||||
import VideoLayout from '../videolayout/VideoLayout';
|
import VideoLayout from '../videolayout/VideoLayout';
|
||||||
|
|
||||||
|
import { openDialog } from '../../../react/features/base/dialog';
|
||||||
import {
|
import {
|
||||||
JitsiRecordingStatus
|
JitsiRecordingStatus
|
||||||
} from '../../../react/features/base/lib-jitsi-meet';
|
} from '../../../react/features/base/lib-jitsi-meet';
|
||||||
|
@ -31,6 +32,8 @@ import {
|
||||||
import { setToolboxEnabled } from '../../../react/features/toolbox';
|
import { setToolboxEnabled } from '../../../react/features/toolbox';
|
||||||
import { setNotificationsEnabled } from '../../../react/features/notifications';
|
import { setNotificationsEnabled } from '../../../react/features/notifications';
|
||||||
import {
|
import {
|
||||||
|
StartLiveStreamDialog,
|
||||||
|
StopLiveStreamDialog,
|
||||||
hideRecordingLabel,
|
hideRecordingLabel,
|
||||||
updateRecordingState
|
updateRecordingState
|
||||||
} from '../../../react/features/recording';
|
} from '../../../react/features/recording';
|
||||||
|
@ -102,91 +105,11 @@ function _isRecordingButtonEnabled() {
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
function _requestLiveStreamId() {
|
function _requestLiveStreamId() {
|
||||||
const cancelButton
|
return new Promise((resolve, reject) =>
|
||||||
= APP.translation.generateTranslationHTML('dialog.Cancel');
|
APP.store.dispatch(openDialog(StartLiveStreamDialog, {
|
||||||
const backButton = APP.translation.generateTranslationHTML('dialog.Back');
|
onCancel: reject,
|
||||||
const startStreamingButton
|
onSubmit: resolve
|
||||||
= 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -232,25 +155,20 @@ function _requestRecordingToken() {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function _showStopRecordingPrompt(recordingType) {
|
function _showStopRecordingPrompt(recordingType) {
|
||||||
let title;
|
|
||||||
let message;
|
|
||||||
let buttonKey;
|
|
||||||
|
|
||||||
if (recordingType === 'jibri') {
|
if (recordingType === 'jibri') {
|
||||||
title = 'dialog.liveStreaming';
|
return new Promise((resolve, reject) => {
|
||||||
message = 'dialog.stopStreamingWarning';
|
APP.store.dispatch(openDialog(StopLiveStreamDialog, {
|
||||||
buttonKey = 'dialog.stopLiveStreaming';
|
onCancel: reject,
|
||||||
} else {
|
onSubmit: resolve
|
||||||
title = 'dialog.recording';
|
}));
|
||||||
message = 'dialog.stopRecordingWarning';
|
});
|
||||||
buttonKey = 'dialog.stopRecording';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
||||||
titleKey: title,
|
titleKey: 'dialog.recording',
|
||||||
msgKey: message,
|
msgKey: 'dialog.stopRecordingWarning',
|
||||||
leftButtonKey: buttonKey,
|
leftButtonKey: 'dialog.stopRecording',
|
||||||
submitFunction: (e, v) => (v ? resolve : reject)(),
|
submitFunction: (e, v) => (v ? resolve : reject)(),
|
||||||
closeFunction: () => {
|
closeFunction: () => {
|
||||||
dialog = null;
|
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';
|
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