Create livestream page and generate signed streamkey

This commit is contained in:
DispatchCommit 2021-03-01 18:07:10 -08:00
parent a443caba71
commit 464f530264
5 changed files with 192 additions and 5 deletions

View file

@ -100,6 +100,7 @@ const Header = (props: Props) => {
const hasBackout = Boolean(backout); const hasBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
const notificationsEnabled = (user && user.experimental_ui) || false; const notificationsEnabled = (user && user.experimental_ui) || false;
const livestreamEnabled = (user && user.experimental_ui) || false;
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url; const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
// Sign out if they click the "x" when they are on the password prompt // Sign out if they click the "x" when they are on the password prompt
@ -280,6 +281,7 @@ const Header = (props: Props) => {
openSignOutModal={openSignOutModal} openSignOutModal={openSignOutModal}
email={email} email={email}
signOut={signOut} signOut={signOut}
livestreamEnabled={livestreamEnabled}
/> />
</div> </div>
)} )}
@ -333,6 +335,7 @@ type HeaderMenuButtonProps = {
openSignOutModal: () => void, openSignOutModal: () => void,
email: ?string, email: ?string,
signOut: () => void, signOut: () => void,
livestreamEnabled: boolean,
}; };
function HeaderMenuButtons(props: HeaderMenuButtonProps) { function HeaderMenuButtons(props: HeaderMenuButtonProps) {
@ -346,6 +349,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
openSignOutModal, openSignOutModal,
email, email,
signOut, signOut,
livestreamEnabled,
} = props; } = props;
return ( return (
@ -374,10 +378,15 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
<Icon aria-hidden icon={ICONS.CHANNEL} /> <Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')} {__('New Channel')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.GO_LIVE}`)}>
{/* Go Live Button for LiveStreaming */}
{(livestreamEnabled) &&(
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
<Icon aria-hidden icon={ICONS.VIDEO} /> <Icon aria-hidden icon={ICONS.VIDEO} />
{__('Go Live')} {__('Go Live')}
</MenuItem> </MenuItem>
)}
</MenuList> </MenuList>
</Menu> </Menu>
)} )}

View file

@ -36,6 +36,7 @@ import PasswordResetPage from 'page/passwordReset';
import PasswordSetPage from 'page/passwordSet'; import PasswordSetPage from 'page/passwordSet';
import SignInVerifyPage from 'page/signInVerify'; import SignInVerifyPage from 'page/signInVerify';
import ChannelsPage from 'page/channels'; import ChannelsPage from 'page/channels';
import LiveStreamPage from 'page/livestream';
import EmbedWrapperPage from 'page/embedWrapper'; import EmbedWrapperPage from 'page/embedWrapper';
import TopPage from 'page/top'; import TopPage from 'page/top';
import Welcome from 'page/welcome'; import Welcome from 'page/welcome';
@ -275,6 +276,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} /> <PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />

View file

@ -48,4 +48,4 @@ exports.BUY = 'buy';
exports.CHANNEL_NEW = 'channel/new'; exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications'; exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube'; exports.YOUTUBE_SYNC = 'youtube';
exports.GO_LIVE = 'livestream'; exports.LIVESTREAM = 'livestream';

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
import CreatorDashboardPage from './view';
const select = state => ({
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
activeChannelClaim: selectActiveChannelClaim(state),
});
export default connect(select, { doSetActiveChannel })(CreatorDashboardPage);

163
ui/page/livestream/view.jsx Normal file
View file

@ -0,0 +1,163 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import Page from 'component/page';
import Spinner from 'component/spinner';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import Yrbl from 'component/yrbl';
import { Lbry } from 'lbry-redux';
import { toHex } from '../../util/hex';
import ClaimPreview from '../../component/claimPreview';
import { FormField } from '../../component/common/form';
type Props = {
channels: Array<ChannelClaim>,
fetchingChannels: boolean,
activeChannelClaim: ?ChannelClaim,
};
export default function CreatorDashboardPage(props: Props) {
const { channels, fetchingChannels, activeChannelClaim } = props;
const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined });
const hasChannels = channels && channels.length > 0;
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
const streamKey = createStreamKey();
React.useEffect(() => {
if (activeChannelClaimStr) {
const channelClaim = JSON.parse(activeChannelClaimStr);
// ensure we have a channel
if (channelClaim.claim_id) {
Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
})
.then((data) => {
console.log(data);
setSigData(data);
})
.catch((error) => {
setSigData({ signature: null, signing_ts: null });
console.error(error);
});
}
}
}, [ activeChannelClaimStr, setSigData ]);
function createStreamKey() {
if (!activeChannelClaim || !sigData.signature || !sigData.signing_ts) return null;
return `${activeChannelClaim.claim_id}?sig=${sigData.signature}&ts=${sigData.signing_ts}`;
}
/******/
const LIVE_STREAM_TAG = 'odysee-livestream';
const [isFetching, setIsFetching] = React.useState(true);
const [isLive, setIsLive] = React.useState(false);
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
React.useEffect(() => {
if (!activeChannelClaimStr) return;
const channelClaim = JSON.parse(activeChannelClaimStr);
Lbry.claim_search({
channel_ids: [channelClaim.claim_id],
any_tags: [LIVE_STREAM_TAG],
claim_type: ['stream'],
})
.then((res) => {
if (res && res.items && res.items.length > 0) {
const claim = res.items[0];
setLivestreamClaim(claim);
} else {
setIsFetching(false);
}
})
.catch(() => {
setIsFetching(false);
});
}, [activeChannelClaimStr]);
return (
<Page>
{fetchingChannels && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
{!fetchingChannels && !hasChannels && (
<Yrbl
type="happy"
title={__("You haven't created a channel yet, let's fix that!")}
actions={
<div className="section__actions">
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
</div>
}
/>
)}
{!fetchingChannels && activeChannelClaim && (
<React.Fragment>
{/* Channel Selector */}
<ChannelSelector hideAnon />
{/* Display StreamKey */}
{ streamKey
? (<div>
{/* Stream Server Address */}
<FormField
name={'livestreamServer'}
label={'Stream Server'}
type={'text'}
defaultValue={'rtmp://stream.odysee.com/live'}
readOnly
/>
{/* Stream Key */}
<FormField
name={'livestreamKey'}
label={'Stream Key'}
type={'text'}
defaultValue={streamKey}
readOnly
/>
</div>)
: (
<div>
<div style={{marginBottom: '2rem'}}>{JSON.stringify(activeChannelClaim)}</div>
{ sigData &&
<div>{JSON.stringify(sigData)}</div>
}
</div>
)
}
{/* Stream Claim(s) */}
{ livestreamClaim ? (
<div style={{marginTop: 'var(--spacing-l)'}}>
<h4>Your LiveStream Claims</h4>
<ClaimPreview uri={livestreamClaim.permanent_url} />
</div>
) : (
<div style={{marginTop: 'var(--spacing-l)'}}>
<div>You must first publish a livestream claim before your stream will be visible!</div>
<div>TODO: add a button for this</div>
</div>
)}
{activeChannelClaim &&
<div>Public Key: {activeChannelClaim.value.public_key}</div>
}
</React.Fragment>
)}
</Page>
);
}