diff --git a/conference.js b/conference.js index 85d7d3071..2bf68c48a 100644 --- a/conference.js +++ b/conference.js @@ -28,6 +28,7 @@ import { redirectWithStoredParams, reloadWithStoredParams } from './react/features/app'; +import { updateRecordingState } from './react/features/recording'; import EventEmitter from 'events'; @@ -1851,6 +1852,12 @@ export default { APP.store.dispatch(dominantSpeakerChanged(id)); }); + room.on(JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED, + (from, liveStreamViewURL) => + APP.store.dispatch(updateRecordingState({ + liveStreamViewURL + }))); + if (!interfaceConfig.filmStripOnly) { room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => { APP.UI.markVideoInterrupted(true); diff --git a/css/modals/invite/_info.scss b/css/modals/invite/_info.scss index b5c849fb5..5fa66f917 100644 --- a/css/modals/invite/_info.scss +++ b/css/modals/invite/_info.scss @@ -59,7 +59,8 @@ } } - .info-dialog-conference-url { + .info-dialog-conference-url, + .info-dialog-live-stream-url { width: max-content; width: -moz-max-content; width: -webkit-max-content; @@ -81,8 +82,8 @@ font-size: 16px; } - .info-dialog-invite-link, - .info-dialog-invite-link:hover { + .info-dialog-url-text, + .info-dialog-url-text:hover { color: inherit; cursor: inherit; } diff --git a/lang/main.json b/lang/main.json index d0a415515..0de3da582 100644 --- a/lang/main.json +++ b/lang/main.json @@ -516,9 +516,11 @@ "dialInConferenceID": "PIN:", "dialInNotSupported": "Sorry, dialing in is currently not suppported.", "genericError": "Whoops, something went wrong.", + "inviteLiveStream": "To view the live stream of this meeting, click this link: __url__", "invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#", "invitePhoneAlternatives": "To view more phone numbers, click this link: __url__", "inviteURL": "To join the video meeting, click this link: __url__", + "liveStreamURL": "Live stream:", "moreNumbers": "More numbers", "noNumbers": "No dial-in numbers.", "noPassword": "None", diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index af6deb155..537b414ac 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -109,7 +109,10 @@ function _requestLiveStreamId() { return new Promise((resolve, reject) => APP.store.dispatch(openDialog(StartLiveStreamDialog, { onCancel: reject, - onSubmit: resolve + onSubmit: (streamId, broadcastId) => resolve({ + broadcastId, + streamId + }) }))); } @@ -257,7 +260,6 @@ const Recording = { * @param recordingState gives us the current recording state */ updateRecordingUI(recordingState) { - const oldState = this.currentState; this.currentState = recordingState; @@ -388,10 +390,13 @@ const Recording = { case JitsiRecordingStatus.OFF: { if (this.recordingType === 'jibri') { _requestLiveStreamId() - .then(streamId => { + .then(({ broadcastId, streamId }) => { this.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, - { streamId }); + { + broadcastId, + streamId + }); // The confirm button on the start recording dialog was // clicked diff --git a/react/features/invite/components/InfoDialogButton.web.js b/react/features/invite/components/InfoDialogButton.web.js index 57826919a..815847de2 100644 --- a/react/features/invite/components/InfoDialogButton.web.js +++ b/react/features/invite/components/InfoDialogButton.web.js @@ -35,12 +35,9 @@ class InfoDialogButton extends Component { static propTypes = { /** - * Phone numbers for dialing into the conference. + * The redux state representing the dial-in numbers feature. */ - _dialInNumbers: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.array - ]), + _dialIn: PropTypes.object, /** * Whether or not the {@code InfoDialog} should display automatically @@ -48,6 +45,11 @@ class InfoDialogButton extends Component { */ _disableAutoShow: PropTypes.bool, + /** + * The URL for a currently active live broadcast + */ + _liveStreamViewURL: PropTypes.string, + /** * The number of real participants in the call. If in a lonely call, * the {@code InfoDialog} will be automatically shown. @@ -117,7 +119,7 @@ class InfoDialogButton extends Component { this._maybeAutoShowDialog(); }, INFO_DIALOG_AUTO_SHOW_TIMEOUT); - if (!this.props._dialInNumbers) { + if (!this.props._dialIn.numbers) { this.props.dispatch(updateDialInNumbers()); } } @@ -150,7 +152,7 @@ class InfoDialogButton extends Component { * @returns {ReactElement} */ render() { - const { t } = this.props; + const { _dialIn, _liveStreamViewURL, t } = this.props; const { showDialog } = this.state; const iconClass = `icon-info ${showDialog ? 'toggled' : ''}`; @@ -158,7 +160,10 @@ class InfoDialogButton extends Component {
} + } isOpen = { showDialog } onClose = { this._onDialogClose } position = { 'top right' }> @@ -215,16 +220,18 @@ class InfoDialogButton extends Component { * @param {Object} state - The Redux state. * @private * @returns {{ - * _dialInNumbers: Array, - * _disableAutoShow: bolean, + * _dialIn: Object, + * _disableAutoShow: boolean, + * _liveStreamViewURL: string, * _participantCount: number, * _toolboxVisible: boolean * }} */ function _mapStateToProps(state) { return { - _dialInNumbers: state['features/invite'].numbers, + _dialIn: state['features/invite'], _disableAutoShow: state['features/base/config'].iAmRecorder, + _liveStreamViewURL: state['features/recording'].liveStreamViewURL, _participantCount: getParticipantCount(state['features/base/participants']), _toolboxVisible: state['features/toolbox'].visible diff --git a/react/features/invite/components/info-dialog/InfoDialog.web.js b/react/features/invite/components/info-dialog/InfoDialog.web.js index fa8302d3e..068ed0ed6 100644 --- a/react/features/invite/components/info-dialog/InfoDialog.web.js +++ b/react/features/invite/components/info-dialog/InfoDialog.web.js @@ -10,8 +10,6 @@ import { getLocalParticipant } from '../../../base/participants'; -import { updateDialInNumbers } from '../../actions'; - import DialInNumber from './DialInNumber'; import PasswordForm from './PasswordForm'; @@ -24,15 +22,6 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); * @extends Component */ class InfoDialog extends Component { - /** - * Default values for {@code InfoDialog} component's properties. - * - * @static - */ - static defaultProps = { - autoUpdateNumbers: true - }; - /** * {@code InfoDialog} component's property types. * @@ -57,11 +46,6 @@ class InfoDialog extends Component { */ _conferenceName: PropTypes.string, - /** - * The redux state representing the dial-in numbers feature. - */ - _dialIn: PropTypes.object, - /** * The current url of the conference to be copied onto the clipboard. */ @@ -79,17 +63,20 @@ class InfoDialog extends Component { _password: PropTypes.string, /** - * Whether or not this component should make a request for dial-in - * numbers. If false, this component will rely on an outside source - * updating and passing in numbers through the _dialIn prop. + * The object representing the dialIn feature. */ - autoUpdateNumbers: PropTypes.bool, + dialIn: PropTypes.object, /** * Invoked to open a dialog for adding participants to the conference. */ dispatch: PropTypes.func, + /** + * The current known URL for a live stream in progress. + */ + liveStreamViewURL: PropTypes.string, + /** * Callback invoked when the dialog should be closed. */ @@ -129,7 +116,7 @@ class InfoDialog extends Component { constructor(props) { super(props); - const { defaultCountry, numbers } = props._dialIn; + const { defaultCountry, numbers } = props.dialIn; if (numbers) { this.state.phoneNumber @@ -147,7 +134,7 @@ class InfoDialog extends Component { this._copyElement = null; // Bind event handlers so they are only bound once for every instance. - this._onClickInviteURL = this._onClickInviteURL.bind(this); + this._onClickURLText = this._onClickURLText.bind(this); this._onCopyInviteURL = this._onCopyInviteURL.bind(this); this._onPasswordRemove = this._onPasswordRemove.bind(this); this._onPasswordSubmit = this._onPasswordSubmit.bind(this); @@ -156,20 +143,6 @@ class InfoDialog extends Component { this._setCopyElement = this._setCopyElement.bind(this); } - /** - * Implements {@link Component#componentDidMount()}. Invoked immediately - * after this component is mounted. Requests dial-in numbers if not - * already known. - * - * @inheritdoc - * @returns {void} - */ - componentDidMount() { - if (!this.state.phoneNumber && this.props.autoUpdateNumbers) { - this.props.dispatch(updateDialInNumbers()); - } - } - /** * Implements React's {@link Component#componentWillReceiveProps()}. Invoked * before this mounted component receives new props. @@ -182,8 +155,8 @@ class InfoDialog extends Component { this.setState({ passwordEditEnabled: false }); } - if (!this.state.phoneNumber && nextProps._dialIn.numbers) { - const { defaultCountry, numbers } = nextProps._dialIn; + if (!this.state.phoneNumber && nextProps.dialIn.numbers) { + const { defaultCountry, numbers } = nextProps.dialIn; this.setState({ phoneNumber: @@ -199,7 +172,7 @@ class InfoDialog extends Component { * @returns {ReactElement} */ render() { - const { onMouseOver, t } = this.props; + const { liveStreamViewURL, onMouseOver, t } = this.props; return (
  + onClick = { this._onClickURLText } > { this._getURLToDisplay() } @@ -231,6 +204,7 @@ class InfoDialog extends Component {
{ this._renderDialInDisplay() }
+ { liveStreamViewURL && this._renderLiveStreamURL() }
+ + { t('info.liveStreamURL') } + +   + + + { liveStreamViewURL } + + +
+ ); + } + /** * Returns whether or not dial-in related UI should be displayed. * @@ -497,7 +507,7 @@ class InfoDialog extends Component { * @returns {boolean} */ _shouldDisplayDialIn() { - const { conferenceID, numbers, numbersEnabled } = this.props._dialIn; + const { conferenceID, numbers, numbersEnabled } = this.props.dialIn; const { phoneNumber } = this.state; return Boolean( @@ -531,7 +541,6 @@ class InfoDialog extends Component { * _canEditPassword: boolean, * _conference: Object, * _conferenceName: string, - * _dialIn: Object, * _inviteURL: string, * _locked: string, * _password: string @@ -558,7 +567,6 @@ function _mapStateToProps(state) { _canEditPassword: canEditPassword, _conference: conference, _conferenceName: room, - _dialIn: state['features/invite'], _inviteURL: getInviteURL(state), _locked: locked, _password: password diff --git a/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js b/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js index fd4f1aafb..f7b0bc631 100644 --- a/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js +++ b/react/features/recording/components/LiveStream/BroadcastsDropdown.web.js @@ -44,7 +44,7 @@ class BroadcastsDropdown extends PureComponent { * The boundStreamID of the broadcast that should display as selected in * the dropdown. */ - selectedBroadcastID: PropTypes.string, + selectedBoundStreamID: PropTypes.string, /** * Invoked to obtain translated strings. @@ -84,7 +84,7 @@ class BroadcastsDropdown extends PureComponent { * @returns {ReactElement} */ render() { - const { broadcasts, selectedBroadcastID, t } = this.props; + const { broadcasts, selectedBoundStreamID, t } = this.props; const dropdownItems = broadcasts.map(broadcast => // eslint-disable-next-line react/jsx-wrap-multilines @@ -96,7 +96,7 @@ class BroadcastsDropdown extends PureComponent { ); const selected = this.props.broadcasts.find( - broadcast => broadcast.boundStreamID === selectedBroadcastID); + broadcast => broadcast.boundStreamID === selectedBoundStreamID); const triggerText = (selected && selected.title) || t('liveStreaming.choose'); diff --git a/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js b/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js index a3b70a4d1..7f0c71045 100644 --- a/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js +++ b/react/features/recording/components/LiveStream/StartLiveStreamDialog.web.js @@ -89,6 +89,8 @@ class StartLiveStreamDialog extends Component { * 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} selectedBoundStreamID - The boundStreamID of the + * broadcast currently selected in the broadcast dropdown. * @property {string} streamKey - The selected or entered stream key to use * for YouTube live streaming. */ @@ -96,7 +98,7 @@ class StartLiveStreamDialog extends Component { broadcasts: undefined, googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING, googleProfileEmail: '', - selectedBroadcastID: undefined, + selectedBoundStreamID: undefined, streamKey: '' }; @@ -291,7 +293,7 @@ class StartLiveStreamDialog extends Component { _onStreamKeyChange(event) { this._setStateIfMounted({ streamKey: event.target.value, - selectedBroadcastID: undefined + selectedBoundStreamID: undefined }); } @@ -304,11 +306,22 @@ class StartLiveStreamDialog extends Component { * closing, true to close the modal. */ _onSubmit() { - if (!this.state.streamKey) { + const { streamKey, selectedBoundStreamID } = this.state; + + if (!streamKey) { return false; } - this.props.onSubmit(this.state.streamKey); + let selectedBroadcastID = null; + + if (selectedBoundStreamID) { + const selectedBroadcast = this.state.broadcasts.find( + broadcast => broadcast.boundStreamID === selectedBoundStreamID); + + selectedBroadcastID = selectedBroadcast && selectedBroadcast.id; + } + + this.props.onSubmit(streamKey, selectedBroadcastID); return true; } @@ -333,7 +346,7 @@ class StartLiveStreamDialog extends Component { this._setStateIfMounted({ streamKey, - selectedBroadcastID: boundStreamID + selectedBoundStreamID: boundStreamID }); }); } @@ -358,6 +371,7 @@ class StartLiveStreamDialog extends Component { if (boundStreamID && !parsedBroadcasts[boundStreamID]) { parsedBroadcasts[boundStreamID] = { boundStreamID, + id: broadcast.id, status: broadcast.status.lifeCycleStatus, title: broadcast.snippet.title }; @@ -378,7 +392,7 @@ class StartLiveStreamDialog extends Component { const { broadcasts, googleProfileEmail, - selectedBroadcastID + selectedBoundStreamID } = this.state; let googleContent, helpText; @@ -399,7 +413,7 @@ class StartLiveStreamDialog extends Component { + selectedBoundStreamID = { selectedBoundStreamID } /> ); /**