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:
parent
d82f172db8
commit
2c4a3b0f60
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'> </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'> </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
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue