From 87f6d27fb244ea8f5973186d5dc6a456a0aa7f15 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Tue, 2 Aug 2022 10:27:18 +0200 Subject: [PATCH] feat(liveStreamting) add configuration to customize streaming dialog --- config.js | 20 +++++-- interface_config.js | 5 +- react/features/base/config/configType.ts | 7 +++ react/features/base/config/configWhitelist.js | 1 + react/features/base/config/reducer.ts | 20 +++++++ react/features/google-api/actions.js | 6 ++- .../LiveStream/AbstractLiveStreamButton.js | 8 +-- .../LiveStream/AbstractStreamKeyForm.js | 53 +++++++++++++------ .../components/LiveStream/constants.js | 11 ++++ .../components/LiveStream/functions.js | 27 ++++++++++ .../LiveStream/native/StreamKeyForm.js | 31 ++++++++--- .../LiveStream/web/StreamKeyForm.js | 14 ++--- 12 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 react/features/recording/components/LiveStream/functions.js diff --git a/config.js b/config.js index e13c98ea3..171737ede 100644 --- a/config.js +++ b/config.js @@ -310,9 +310,6 @@ var config = { // DEPRECATED. Use recordingService.sharingEnabled instead. // fileRecordingsServiceSharingEnabled: false, - // Whether to enable live streaming or not. - // liveStreamingEnabled: false, - // Local recording configuration. // localRecording: { // // Whether to disable local recording or not. @@ -325,6 +322,23 @@ var config = { // disableSelfRecording: false, // }, + // Customize the Live Streaming dialog. Can be modified for a non-YouTube provider. + // liveStreaming: { + // // Whether to enable live streaming or not. + // enabled: false, + // // Terms link + // termsLink: 'https://www.youtube.com/t/terms', + // // Data privacy link + // dataPrivacyLink: 'https://policies.google.com/privacy', + // // RegExp string that validates the stream key input field + // validatorRegExpString: '^(?:[a-zA-Z0-9]{4}(?:-(?!$)|$)){4}', + // // Documentation reference for the live streaming feature. + // helpLink: 'https://jitsi.org/live' + // }, + + // DEPRECATED. Use liveStreaming.enabled instead. + // liveStreamingEnabled: false, + // DEPRECATED. Use transcription.enabled instead. // transcribingEnabled: false, diff --git a/interface_config.js b/interface_config.js index 3a9a4fd0f..4158fff37 100644 --- a/interface_config.js +++ b/interface_config.js @@ -90,7 +90,6 @@ var interfaceConfig = { JITSI_WATERMARK_LINK: 'https://jitsi.org', LANG_DETECTION: true, // Allow i18n to detect the system language - LIVE_STREAMING_HELP_LINK: 'https://jitsi.org/live', // Documentation reference for the live streaming feature. LOCAL_THUMBNAIL_RATIO: 16 / 9, // 16:9 /** @@ -248,6 +247,10 @@ var interfaceConfig = { // Moved to config.js as `toolbarConfig.initialTimeout`. // INITIAL_TOOLBAR_TIMEOUT: 20000, + // Please use `liveStreaming.helpLink` from config.js + // Documentation reference for the live streaming feature. + // LIVE_STREAMING_HELP_LINK: 'https://jitsi.org/live', + // Moved to config.js as `toolbarConfig.alwaysVisible`. // TOOLBAR_ALWAYS_VISIBLE: false, diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index dac82b7d2..113c453e3 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -332,6 +332,13 @@ export interface IConfig { lastNLimits?: { [key: number]: number; }; + liveStreaming?: { + dataPrivacyLink?: string; + enabled?: boolean; + helpLink?: string; + termsLink?: string; + validatorRegExpString?: string; + }; liveStreamingEnabled?: boolean; localRecording?: { disable?: boolean; diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index 4b8277ec4..6b1c1aa64 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -183,6 +183,7 @@ export default [ 'iceTransportPolicy', 'ignoreStartMuted', 'inviteAppName', + 'liveStreaming', 'liveStreamingEnabled', 'localRecording', 'localSubject', diff --git a/react/features/base/config/reducer.ts b/react/features/base/config/reducer.ts index 5c9706321..986c6f045 100644 --- a/react/features/base/config/reducer.ts +++ b/react/features/base/config/reducer.ts @@ -420,6 +420,26 @@ function _translateLegacyConfig(oldValue: IConfig) { }; } + newValue.liveStreaming = newValue.liveStreaming || {}; + + // Migrate config.liveStreamingEnabled + if (oldValue.liveStreamingEnabled !== undefined) { + newValue.liveStreaming = { + ...newValue.liveStreaming, + enabled: oldValue.liveStreamingEnabled + }; + } + + // Migrate interfaceConfig.LIVE_STREAMING_HELP_LINK + if (oldValue.liveStreaming === undefined + && typeof interfaceConfig === 'object' + && interfaceConfig.hasOwnProperty('LIVE_STREAMING_HELP_LINK')) { + newValue.liveStreaming = { + ...newValue.liveStreaming, + helpLink: interfaceConfig.LIVE_STREAMING_HELP_LINK + }; + } + return newValue; } diff --git a/react/features/google-api/actions.js b/react/features/google-api/actions.js index cc9d371ab..8114e3fd5 100644 --- a/react/features/google-api/actions.js +++ b/react/features/google-api/actions.js @@ -3,6 +3,7 @@ import type { Dispatch } from 'redux'; import { getShareInfoText } from '../invite'; +import { getLiveStreaming } from '../recording/components/LiveStream/functions'; import { SET_GOOGLE_API_PROFILE, @@ -36,15 +37,16 @@ export function loadGoogleAPI() { googleApi.get() .then(() => { const { - liveStreamingEnabled, enableCalendarIntegration, googleApiApplicationClientID } = getState()['features/base/config']; + const liveStreaming = getLiveStreaming(getState()); + if (getState()['features/google-api'].googleAPIState === GOOGLE_API_STATES.NEEDS_LOADING) { return googleApi.initializeClient( - googleApiApplicationClientID, liveStreamingEnabled, enableCalendarIntegration); + googleApiApplicationClientID, liveStreaming.enabled, enableCalendarIntegration); } return Promise.resolve(); diff --git a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js index b671969fc..cfb78f2a0 100644 --- a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js +++ b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js @@ -12,6 +12,8 @@ import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions'; import { FEATURES } from '../../../jaas/constants'; import { getActiveSession } from '../../functions'; +import { getLiveStreaming } from './functions'; + /** * The type of the React {@code Component} props of @@ -141,12 +143,12 @@ export function _mapStateToProps(state: Object, ownProps: Props) { // its own to be visible or not. const isModerator = isLocalParticipantModerator(state); const { - enableFeaturesBasedOnToken, - liveStreamingEnabled + enableFeaturesBasedOnToken } = state['features/base/config']; + const liveStreaming = getLiveStreaming(state); const { features = {} } = getLocalParticipant(state); - visible = isModerator && liveStreamingEnabled; + visible = isModerator && liveStreaming.enabled; if (enableFeaturesBasedOnToken) { visible = visible && String(features.livestreaming) === 'true'; diff --git a/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js b/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js index 636e4a22a..25e004576 100644 --- a/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js +++ b/react/features/recording/components/LiveStream/AbstractStreamKeyForm.js @@ -3,21 +3,35 @@ import debounce from 'lodash/debounce'; import { Component } from 'react'; -declare var interfaceConfig: Object; +import { getLiveStreaming } from './functions'; -/** - * The live streaming help link to display. On web it comes from - * interfaceConfig, but we don't have that on mobile. - * - * FIXME: This is in props now to prepare for the Redux-based interfaceConfig. - */ -const LIVE_STREAMING_HELP_LINK = 'https://jitsi.org/live'; + +export type LiveStreaming = { + enabled: boolean, + helpLink: string, // Documentation reference for the live streaming feature. + termsLink: string, // Terms link + dataPrivacyLink: string, // Data privacy link + validatorRegExpString: string // RegExp string that validates the stream key input field +} + +export type LiveStreamingProps = { + enabled: boolean, + helpURL: string, + termsURL: string, + dataPrivacyURL: string, + streamLinkRegexp: RegExp +} /** * The props of the component. */ export type Props = { + /** + * The live streaming dialog properties. + */ + _liveStreaming: LiveStreamingProps, + /** * Callback invoked when the entered stream key has changed. */ @@ -54,7 +68,7 @@ type State = { */ export default class AbstractStreamKeyForm extends Component { - helpURL: string; + _debouncedUpdateValidationErrorVisibility: Function; /** @@ -70,10 +84,6 @@ export default class AbstractStreamKeyForm && !this._validateStreamKey(this.props.value) }; - this.helpURL = (typeof interfaceConfig !== 'undefined' - && interfaceConfig.LIVE_STREAMING_HELP_LINK) - || LIVE_STREAMING_HELP_LINK; - this._debouncedUpdateValidationErrorVisibility = debounce( this._updateValidationErrorVisibility.bind(this), 800, @@ -150,9 +160,22 @@ export default class AbstractStreamKeyForm */ _validateStreamKey(streamKey = '') { const trimmedKey = streamKey.trim(); - const fourGroupsDashSeparated = /^(?:[a-zA-Z0-9]{4}(?:-(?!$)|$)){4}/; - const match = fourGroupsDashSeparated.exec(trimmedKey); + const match = this.props._liveStreaming.streamLinkRegexp.exec(trimmedKey); return Boolean(match); } } + +/** + * Maps part of the Redux state to the component's props. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _liveStreaming: LiveStreamingProps + * }} + */ +export function _mapStateToProps(state: Object) { + return { + _liveStreaming: getLiveStreaming(state) + }; +} diff --git a/react/features/recording/components/LiveStream/constants.js b/react/features/recording/components/LiveStream/constants.js index 2c7ef8b47..1f83107b9 100644 --- a/react/features/recording/components/LiveStream/constants.js +++ b/react/features/recording/components/LiveStream/constants.js @@ -13,3 +13,14 @@ export const YOUTUBE_LIVE_DASHBOARD_URL = 'https://www.youtube.com/live_dashboar * The URL for YouTube terms and conditions. */ export const YOUTUBE_TERMS_URL = 'https://www.youtube.com/t/terms'; + +/** + * The live streaming help link to display. + */ +export const JITSI_LIVE_STREAMING_HELP_LINK = 'https://jitsi.org/live'; + +/** + * The YouTube stream link RegExp. + */ +export const FOUR_GROUPS_DASH_SEPARATED = /^(?:[a-zA-Z0-9]{4}(?:-(?!$)|$)){4}/; + diff --git a/react/features/recording/components/LiveStream/functions.js b/react/features/recording/components/LiveStream/functions.js new file mode 100644 index 000000000..dbc4015db --- /dev/null +++ b/react/features/recording/components/LiveStream/functions.js @@ -0,0 +1,27 @@ +import { + FOUR_GROUPS_DASH_SEPARATED, + GOOGLE_PRIVACY_POLICY, + JITSI_LIVE_STREAMING_HELP_LINK, + YOUTUBE_TERMS_URL +} from './constants'; + +/** + * Get the live streaming options. + * + * @param {Object} state - The global state. + * @returns {LiveStreaming} + */ +export function getLiveStreaming(state: Object) { + const { liveStreaming = {} } = state['features/base/config']; + + const regexp = liveStreaming.validatorRegExpString + && new RegExp(liveStreaming.validatorRegExpString); + + return { + enabled: Boolean(liveStreaming.enabled), + helpURL: liveStreaming.helpLink || JITSI_LIVE_STREAMING_HELP_LINK, + termsURL: liveStreaming.termsLink || YOUTUBE_TERMS_URL, + dataPrivacyURL: liveStreaming.dataPrivacyLink || GOOGLE_PRIVACY_POLICY, + streamLinkRegexp: regexp || FOUR_GROUPS_DASH_SEPARATED + }; +} diff --git a/react/features/recording/components/LiveStream/native/StreamKeyForm.js b/react/features/recording/components/LiveStream/native/StreamKeyForm.js index 5fbea74df..47a07dfeb 100644 --- a/react/features/recording/components/LiveStream/native/StreamKeyForm.js +++ b/react/features/recording/components/LiveStream/native/StreamKeyForm.js @@ -10,7 +10,9 @@ import { StyleType } from '../../../../base/styles'; import AbstractStreamKeyForm, { type Props as AbstractProps } from '../AbstractStreamKeyForm'; -import { GOOGLE_PRIVACY_POLICY, YOUTUBE_TERMS_URL } from '../constants'; +import { getLiveStreaming } from '../functions'; + +import styles, { PLACEHOLDER_COLOR } from './styles'; type Props = AbstractProps & { @@ -20,8 +22,6 @@ type Props = AbstractProps & { _dialogStyles: StyleType }; -import styles, { PLACEHOLDER_COLOR } from './styles'; - /** * A React Component for entering a key for starting a YouTube live stream. * @@ -148,7 +148,7 @@ class StreamKeyForm extends AbstractStreamKeyForm { * @returns {void} */ _onOpenGooglePrivacyPolicy() { - Linking.openURL(GOOGLE_PRIVACY_POLICY); + Linking.openURL(this.props._liveStreaming.dataPrivacyURL); } _onOpenHelp: () => void; @@ -161,7 +161,7 @@ class StreamKeyForm extends AbstractStreamKeyForm { * @returns {void} */ _onOpenHelp() { - const { helpURL } = this; + const helpURL = this.props._liveStreaming.helpURL; if (typeof helpURL === 'string') { Linking.openURL(helpURL); @@ -177,8 +177,25 @@ class StreamKeyForm extends AbstractStreamKeyForm { * @returns {void} */ _onOpenYoutubeTerms() { - Linking.openURL(YOUTUBE_TERMS_URL); + Linking.openURL(this.props._liveStreaming.termsURL); } } -export default translate(connect(_abstractMapStateToProps)(StreamKeyForm)); +/** + * Maps (parts of) the redux state to the associated props for the + * {@code StreamKeyForm} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _liveStreaming: LiveStreamingProps + * }} + */ +function _mapStateToProps(state: Object) { + return { + ..._abstractMapStateToProps(state), + _liveStreaming: getLiveStreaming(state) + }; +} + +export default translate(connect(_mapStateToProps)(StreamKeyForm)); diff --git a/react/features/recording/components/LiveStream/web/StreamKeyForm.js b/react/features/recording/components/LiveStream/web/StreamKeyForm.js index 61b79cff9..5b8ba8679 100644 --- a/react/features/recording/components/LiveStream/web/StreamKeyForm.js +++ b/react/features/recording/components/LiveStream/web/StreamKeyForm.js @@ -4,10 +4,10 @@ import { FieldTextStateless } from '@atlaskit/field-text'; import React from 'react'; import { translate } from '../../../../base/i18n'; +import { connect } from '../../../../base/redux'; import AbstractStreamKeyForm, { - type Props + type Props, _mapStateToProps } from '../AbstractStreamKeyForm'; -import { GOOGLE_PRIVACY_POLICY, YOUTUBE_TERMS_URL } from '../constants'; /** * A React Component for entering a key for starting a YouTube live stream. @@ -62,7 +62,7 @@ class StreamKeyForm extends AbstractStreamKeyForm { : null } - { this.helpURL + { this.props._liveStreaming.helpURL ? { { t('liveStreaming.youtubeTerms') } { t('liveStreaming.googlePrivacyPolicy') } @@ -106,7 +106,7 @@ class StreamKeyForm extends AbstractStreamKeyForm { * @returns {void} */ _onOpenHelp() { - window.open(this.helpURL, '_blank', 'noopener'); + window.open(this.props._liveStreaming.helpURL, '_blank', 'noopener'); } _onOpenHelpKeyPress: () => void; @@ -128,4 +128,4 @@ class StreamKeyForm extends AbstractStreamKeyForm { } } -export default translate(StreamKeyForm); +export default translate(connect(_mapStateToProps)(StreamKeyForm));