Show the YouTube live stream URL (#2837)

* feat(recording): show the YouTube live stream URL

- From the start live stream dialog, push up the broadcast ID
  of the chosen broadcast. It is assumed the ID can be used to
  create the YouTube link.
- Listen for lib-jitsi-meet to emit updates of the known live
  stream URL, shove it into redux, and have InfoDialog display
  it.

* ref(info): pass in dial in and live stream url

Passing these values in should trigger AtlasKit InlineDialog
to re-render and reposition itself.

* ref(info): use conference existence as trigger for autoshowing dialog

* feat(info): add live stream link to invite copy

* Revert "ref(info): use conference existence as trigger for autoshowing dialog"

This reverts commit 1072102267.

* hidden -> url

* _onClickHiddenURL -> _onClickURLText
This commit is contained in:
virtuacoplenny 2018-04-20 10:28:16 -07:00 committed by bbaldino
parent d82f172db8
commit 2c4a3b0f60
8 changed files with 124 additions and 80 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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",

View File

@ -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

View File

@ -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 {
<div className = 'toolbox-button-wth-dialog'>
<InlineDialog
content = {
<InfoDialog onClose = { this._onDialogClose } /> }
<InfoDialog
dialIn = { _dialIn }
liveStreamViewURL = { _liveStreamViewURL }
onClose = { this._onDialogClose } /> }
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

View File

@ -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 (
<div
@ -221,9 +194,9 @@ class InfoDialog extends Component {
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
<a
className = 'info-dialog-invite-link'
className = 'info-dialog-url-text'
href = { this.props._inviteURL }
onClick = { this._onClickInviteURL } >
onClick = { this._onClickURLText } >
{ this._getURLToDisplay() }
</a>
</span>
@ -231,6 +204,7 @@ class InfoDialog extends Component {
<div className = 'info-dialog-dial-in'>
{ this._renderDialInDisplay() }
</div>
{ liveStreamViewURL && this._renderLiveStreamURL() }
<div className = 'info-dialog-password'>
<PasswordForm
editEnabled = { this.state.passwordEditEnabled }
@ -321,16 +295,24 @@ class InfoDialog extends Component {
* @returns {string}
*/
_getTextToCopy() {
const { t } = this.props;
const { liveStreamViewURL, t } = this.props;
let invite = t('info.inviteURL', {
url: this.props._inviteURL
});
if (liveStreamViewURL) {
const liveStream = t('info.inviteLiveStream', {
url: liveStreamViewURL
});
invite = `${invite}\n${liveStream}`;
}
if (this._shouldDisplayDialIn()) {
const dial = t('info.invitePhone', {
number: this.state.phoneNumber,
conferenceID: this.props._dialIn.conferenceID
conferenceID: this.props.dialIn.conferenceID
});
const moreNumbers = t('info.invitePhoneAlternatives', {
url: this._getDialInfoPageURL()
@ -353,16 +335,16 @@ class InfoDialog extends Component {
}
/**
* Callback invoked when the displayed invite URL link is clicked to prevent
* actual navigation from happening. The invite URL link has an href to
* display "Copy Link Address" in the context menu but otherwise it should
* not behave like a link.
* Callback invoked when a displayed URL link is clicked to prevent actual
* navigation from happening. The URL links have an href to display "Copy
* Link Address" in the context menu but otherwise it should not behave like
* links.
*
* @param {Object} event - The click event from clicking on the link.
* @private
* @returns {void}
*/
_onClickInviteURL(event) {
_onClickURLText(event) {
event.preventDefault();
}
@ -439,7 +421,7 @@ class InfoDialog extends Component {
return (
<div>
<DialInNumber
conferenceID = { this.props._dialIn.conferenceID }
conferenceID = { this.props.dialIn.conferenceID }
phoneNumber = { this.state.phoneNumber } />
<a
className = 'more-numbers'
@ -490,6 +472,34 @@ class InfoDialog extends Component {
: null;
}
/**
* Returns a ReactElement for display a link to the current url of a
* live stream in progress.
*
* @private
* @returns {null|ReactElement}
*/
_renderLiveStreamURL() {
const { liveStreamViewURL, t } = this.props;
return (
<div className = 'info-dialog-live-stream-url'>
<span className = 'info-label'>
{ t('info.liveStreamURL') }
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
<a
className = 'info-dialog-url-text'
href = { liveStreamViewURL }
onClick = { this._onClickURLText } >
{ liveStreamViewURL }
</a>
</span>
</div>
);
}
/**
* 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

View File

@ -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 {
</DropdownItem>
);
const selected = this.props.broadcasts.find(
broadcast => broadcast.boundStreamID === selectedBroadcastID);
broadcast => broadcast.boundStreamID === selectedBoundStreamID);
const triggerText = (selected && selected.title)
|| t('liveStreaming.choose');

View File

@ -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 {
<BroadcastsDropdown
broadcasts = { broadcasts }
onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
selectedBroadcastID = { selectedBroadcastID } />
selectedBoundStreamID = { selectedBoundStreamID } />
);
/**