Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

StartLiveStreamDialog.web.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // @flow
  2. import Spinner from '@atlaskit/spinner';
  3. import React from 'react';
  4. import { connect } from 'react-redux';
  5. import { translate } from '../../../base/i18n';
  6. import googleApi from '../../googleApi';
  7. import AbstractStartLiveStreamDialog, {
  8. _mapStateToProps,
  9. GOOGLE_API_STATES,
  10. type Props
  11. } from './AbstractStartLiveStreamDialog';
  12. import BroadcastsDropdown from './BroadcastsDropdown';
  13. import GoogleSignInButton from './GoogleSignInButton';
  14. import StreamKeyForm from './StreamKeyForm';
  15. declare var interfaceConfig: Object;
  16. /**
  17. * A React Component for requesting a YouTube stream key to use for live
  18. * streaming of the current conference.
  19. *
  20. * @extends Component
  21. */
  22. class StartLiveStreamDialog
  23. extends AbstractStartLiveStreamDialog {
  24. /**
  25. * Initializes a new {@code StartLiveStreamDialog} instance.
  26. *
  27. * @param {Props} props - The React {@code Component} props to initialize
  28. * the new {@code StartLiveStreamDialog} instance with.
  29. */
  30. constructor(props: Props) {
  31. super(props);
  32. // Bind event handlers so they are only bound once per instance.
  33. this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
  34. this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
  35. this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
  36. this._onYouTubeBroadcastIDSelected
  37. = this._onYouTubeBroadcastIDSelected.bind(this);
  38. this._renderDialogContent = this._renderDialogContent.bind(this);
  39. }
  40. _onInitializeGoogleApi: () => Promise<*>;
  41. /**
  42. * Loads the Google web client application used for fetching stream keys.
  43. * If the user is already logged in, then a request for available YouTube
  44. * broadcasts is also made.
  45. *
  46. * @private
  47. * @returns {Promise}
  48. */
  49. _onInitializeGoogleApi() {
  50. return googleApi.get()
  51. .then(() => googleApi.initializeClient(
  52. this.props._googleApiApplicationClientID))
  53. .then(() => this._setStateIfMounted({
  54. googleAPIState: GOOGLE_API_STATES.LOADED
  55. }))
  56. .then(() => googleApi.isSignedIn())
  57. .then(isSignedIn => {
  58. if (isSignedIn) {
  59. return this._onGetYouTubeBroadcasts();
  60. }
  61. })
  62. .catch(() => {
  63. this._setStateIfMounted({
  64. googleAPIState: GOOGLE_API_STATES.ERROR
  65. });
  66. });
  67. }
  68. _onGetYouTubeBroadcasts: () => Promise<*>;
  69. /**
  70. * Asks the user to sign in, if not already signed in, and then requests a
  71. * list of the user's YouTube broadcasts.
  72. *
  73. * @private
  74. * @returns {Promise}
  75. */
  76. _onGetYouTubeBroadcasts() {
  77. return googleApi.get()
  78. .then(() => googleApi.signInIfNotSignedIn())
  79. .then(() => googleApi.getCurrentUserProfile())
  80. .then(profile => {
  81. this._setStateIfMounted({
  82. googleProfileEmail: profile.getEmail(),
  83. googleAPIState: GOOGLE_API_STATES.SIGNED_IN
  84. });
  85. })
  86. .then(() => googleApi.requestAvailableYouTubeBroadcasts())
  87. .then(response => {
  88. const broadcasts = this._parseBroadcasts(response.result.items);
  89. this._setStateIfMounted({
  90. broadcasts
  91. });
  92. if (broadcasts.length === 1 && !this.state.streamKey) {
  93. const broadcast = broadcasts[0];
  94. this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
  95. }
  96. })
  97. .catch(response => {
  98. // Only show an error if an external request was made with the
  99. // Google api. Do not error if the login in canceled.
  100. if (response && response.result) {
  101. this._setStateIfMounted({
  102. errorType: this._parseErrorFromResponse(response),
  103. googleAPIState: GOOGLE_API_STATES.ERROR
  104. });
  105. }
  106. });
  107. }
  108. _onRequestGoogleSignIn: () => Object;
  109. /**
  110. * Forces the Google web client application to prompt for a sign in, such as
  111. * when changing account, and will then fetch available YouTube broadcasts.
  112. *
  113. * @private
  114. * @returns {Promise}
  115. */
  116. _onRequestGoogleSignIn() {
  117. return googleApi.showAccountSelection()
  118. .then(() => this._setStateIfMounted({ broadcasts: undefined }))
  119. .then(() => this._onGetYouTubeBroadcasts());
  120. }
  121. _onStreamKeyChange: string => void;
  122. _onYouTubeBroadcastIDSelected: (string) => Object;
  123. /**
  124. * Fetches the stream key for a YouTube broadcast and updates the internal
  125. * state to display the associated stream key as being entered.
  126. *
  127. * @param {string} boundStreamID - The bound stream ID associated with the
  128. * broadcast from which to get the stream key.
  129. * @private
  130. * @returns {Promise}
  131. */
  132. _onYouTubeBroadcastIDSelected(boundStreamID) {
  133. return googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
  134. .then(response => {
  135. const broadcasts = response.result.items;
  136. const streamName = broadcasts
  137. && broadcasts[0]
  138. && broadcasts[0].cdn.ingestionInfo.streamName;
  139. const streamKey = streamName || '';
  140. this._setStateIfMounted({
  141. streamKey,
  142. selectedBoundStreamID: boundStreamID
  143. });
  144. });
  145. }
  146. _parseBroadcasts: (Array<Object>) => Array<Object>;
  147. /**
  148. * Takes in a list of broadcasts from the YouTube API, removes dupes,
  149. * removes broadcasts that cannot get a stream key, and parses the
  150. * broadcasts into flat objects.
  151. *
  152. * @param {Array} broadcasts - Broadcast descriptions as obtained from
  153. * calling the YouTube API.
  154. * @private
  155. * @returns {Array} An array of objects describing each unique broadcast.
  156. */
  157. _parseBroadcasts(broadcasts) {
  158. const parsedBroadcasts = {};
  159. for (let i = 0; i < broadcasts.length; i++) {
  160. const broadcast = broadcasts[i];
  161. const boundStreamID = broadcast.contentDetails.boundStreamId;
  162. if (boundStreamID && !parsedBroadcasts[boundStreamID]) {
  163. parsedBroadcasts[boundStreamID] = {
  164. boundStreamID,
  165. id: broadcast.id,
  166. status: broadcast.status.lifeCycleStatus,
  167. title: broadcast.snippet.title
  168. };
  169. }
  170. }
  171. return Object.values(parsedBroadcasts);
  172. }
  173. /**
  174. * Searches in a Google API error response for the error type.
  175. *
  176. * @param {Object} response - The Google API response that may contain an
  177. * error.
  178. * @private
  179. * @returns {string|null}
  180. */
  181. _parseErrorFromResponse(response) {
  182. const result = response.result;
  183. const error = result.error;
  184. const errors = error && error.errors;
  185. const firstError = errors && errors[0];
  186. return (firstError && firstError.reason) || null;
  187. }
  188. _renderDialogContent: () => React$Component<*>
  189. /**
  190. * Renders the platform specific dialog content.
  191. *
  192. * @returns {React$Component}
  193. */
  194. _renderDialogContent() {
  195. const { _googleApiApplicationClientID } = this.props;
  196. return (
  197. <div className = 'live-stream-dialog'>
  198. { _googleApiApplicationClientID
  199. ? this._renderYouTubePanel() : null }
  200. <StreamKeyForm
  201. onChange = { this._onStreamKeyChange }
  202. value = { this.state.streamKey || this.props._streamKey } />
  203. </div>
  204. );
  205. }
  206. /**
  207. * Renders a React Element for authenticating with the Google web client.
  208. *
  209. * @private
  210. * @returns {ReactElement}
  211. */
  212. _renderYouTubePanel() {
  213. const { t } = this.props;
  214. const {
  215. broadcasts,
  216. googleProfileEmail,
  217. selectedBoundStreamID
  218. } = this.state;
  219. let googleContent, helpText;
  220. switch (this.state.googleAPIState) {
  221. case GOOGLE_API_STATES.LOADED:
  222. googleContent = ( // eslint-disable-line no-extra-parens
  223. <GoogleSignInButton
  224. onClick = { this._onGetYouTubeBroadcasts }
  225. text = { t('liveStreaming.signIn') } />
  226. );
  227. helpText = t('liveStreaming.signInCTA');
  228. break;
  229. case GOOGLE_API_STATES.SIGNED_IN:
  230. googleContent = ( // eslint-disable-line no-extra-parens
  231. <BroadcastsDropdown
  232. broadcasts = { broadcasts }
  233. onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
  234. selectedBoundStreamID = { selectedBoundStreamID } />
  235. );
  236. /**
  237. * FIXME: Ideally this help text would be one translation string
  238. * that also accepts the anchor. This can be done using the Trans
  239. * component of react-i18next but I couldn't get it working...
  240. */
  241. helpText = ( // eslint-disable-line no-extra-parens
  242. <div>
  243. { `${t('liveStreaming.chooseCTA',
  244. { email: googleProfileEmail })} ` }
  245. <a onClick = { this._onRequestGoogleSignIn }>
  246. { t('liveStreaming.changeSignIn') }
  247. </a>
  248. </div>
  249. );
  250. break;
  251. case GOOGLE_API_STATES.ERROR:
  252. googleContent = ( // eslint-disable-line no-extra-parens
  253. <GoogleSignInButton
  254. onClick = { this._onRequestGoogleSignIn }
  255. text = { t('liveStreaming.signIn') } />
  256. );
  257. helpText = this._getGoogleErrorMessageToDisplay();
  258. break;
  259. case GOOGLE_API_STATES.NEEDS_LOADING:
  260. default:
  261. googleContent = ( // eslint-disable-line no-extra-parens
  262. <Spinner
  263. isCompleting = { false }
  264. size = 'medium' />
  265. );
  266. break;
  267. }
  268. return (
  269. <div className = 'google-panel'>
  270. <div className = 'live-stream-cta'>
  271. { helpText }
  272. </div>
  273. <div className = 'google-api'>
  274. { googleContent }
  275. </div>
  276. </div>
  277. );
  278. }
  279. _setStateIfMounted: Object => void
  280. /**
  281. * Returns the error message to display for the current error state.
  282. *
  283. * @private
  284. * @returns {string} The error message to display.
  285. */
  286. _getGoogleErrorMessageToDisplay() {
  287. let text;
  288. switch (this.state.errorType) {
  289. case 'liveStreamingNotEnabled':
  290. text = this.props.t(
  291. 'liveStreaming.errorLiveStreamNotEnabled',
  292. { email: this.state.googleProfileEmail });
  293. break;
  294. default:
  295. text = this.props.t('liveStreaming.errorAPI');
  296. break;
  297. }
  298. return <div className = 'google-error'>{ text }</div>;
  299. }
  300. }
  301. export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));