Compare commits

...

4 commits

Author SHA1 Message Date
DispatchCommit
af90085b26 bookmarking changes with this commit 2021-03-05 14:14:23 -08:00
DispatchCommit
1a8e7b0973 Add channel name hex data to streamkey
Also adds individual debug fields to help when debugging a channel verify currently
2021-03-03 18:55:08 -08:00
DispatchCommit
464f530264 Create livestream page and generate signed streamkey 2021-03-01 18:07:10 -08:00
DispatchCommit
a443caba71 Add Go Live to header dropdown 2021-02-26 08:12:32 -08:00
8 changed files with 297 additions and 15 deletions

View file

@ -1185,6 +1185,7 @@
"Download the app to track files you've viewed and downloaded.": "Download the app to track files you've viewed and downloaded.",
"Create a New Channel": "Create a New Channel",
"Create a new channel": "Create a new channel",
"Go Live": "Go Live",
"Thumbnail source": "Thumbnail source",
"Cover source": "Cover source",
"Your changes will be live in a few minutes": "Your changes will be live in a few minutes",

View file

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

View file

@ -17,9 +17,10 @@ import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
import { selectModal, selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import PublishPage from './view';
import PublishForm from './view';
import { selectUser } from 'redux/selectors/user';
const select = state => ({
const select = (state) => ({
...selectPublishFormValues(state),
// The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state),
@ -34,16 +35,17 @@ const select = state => ({
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
user: selectUser(state),
});
const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
const perform = (dispatch) => ({
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
resolveUri: (uri) => dispatch(doResolveUri(uri)),
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
checkAvailability: name => dispatch(doCheckPublishNameAvailability(name)),
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
});
export default connect(select, perform)(PublishPage);
export default connect(select, perform)(PublishForm);

View file

@ -25,6 +25,7 @@ import SelectThumbnail from 'component/selectThumbnail';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
import * as PUBLISH_MODES from 'constants/publish_types';
import { FormField } from 'component/common/form';
// @if TARGET='app'
import fs from 'fs';
@ -72,14 +73,14 @@ type Props = {
balance: number,
isStillEditing: boolean,
clearPublish: () => void,
resolveUri: string => void,
resolveUri: (string) => void,
scrollToTop: () => void,
prepareEdit: (claim: any, uri: string) => void,
resetThumbnailStatus: () => void,
amountNeededForTakeover: ?number,
// Add back type
updatePublishForm: any => void,
checkAvailability: string => void,
updatePublishForm: (any) => void,
checkAvailability: (string) => void,
ytSignupPending: boolean,
modal: { id: string, modalProps: {} },
enablePublishPreview: boolean,
@ -124,8 +125,12 @@ function PublishForm(props: Props) {
enablePublishPreview,
activeChannelClaim,
incognito,
user,
isLivestreamPublish,
} = props;
const isLivestreamCreator = (user && user.experimental_ui) || false;
const TAGS_LIMIT = 5;
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
@ -332,6 +337,38 @@ function PublishForm(props: Props) {
<div className="card-stack">
<ChannelSelect disabled={disabled} />
{/* Some LiveStream Publishing Hack Sean made */}
{isLivestreamCreator && (
<div className="livestream__creator-message livestream__publish-checkbox">
<h4>Hello brave beta tester,</h4>
<p>
Check this box if you have entered video information for your livestream. It doesn't matter what file you
choose for now, just make the sure the title, description, and tags are correct. Everything else is setup!
</p>
<p>
When you edit this file, there will be another checkbox to turn this back into a regular video so it can be
listed on your channel's page.
</p>
<FormField
type={isStillEditing ? 'radio' : 'checkbox'}
label={__('This is for my livestream')}
name="is_livestream_checkbox"
checked={isLivestreamPublish}
onChange={(e) => updatePublishForm({ isLivestreamPublish: e.target.checked })}
/>
{isStillEditing && (
<FormField
type="radio"
label={'I am done livestreaming'}
name="is_livestream_checkbox_done"
checked={!isLivestreamPublish}
onChange={(e) => updatePublishForm({ isLivestreamPublish: !e.target.checked })}
/>
)}
</div>
)}
<PublishFile
uri={uri}
mode={mode}
@ -373,17 +410,17 @@ function PublishForm(props: Props) {
"Add tags that are relevant to your content so those who're looking for it can find it more easily. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated."
)}
placeholder={__('gaming, crypto')}
onSelect={newTags => {
onSelect={(newTags) => {
const validatedTags = [];
newTags.forEach(newTag => {
if (!tags.some(tag => tag.name === newTag.name)) {
newTags.forEach((newTag) => {
if (!tags.some((tag) => tag.name === newTag.name)) {
validatedTags.push(newTag);
}
});
updatePublishForm({ tags: [...tags, ...validatedTags] });
}}
onRemove={clickedTag => {
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
onRemove={(clickedTag) => {
const newTags = tags.slice().filter((tag) => tag.name !== clickedTag.name);
updatePublishForm({ tags: newTags });
}}
tagsChosen={tags}

View file

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

View file

@ -48,3 +48,4 @@ exports.BUY = 'buy';
exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube';
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);

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

@ -0,0 +1,213 @@
// @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}?d=${toHex(activeChannelClaim.name)}&s=${sigData.signature}&t=${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)'}}>
<h3>Your LiveStream Claims</h3>
<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>
)}
{/* Debug Stuff */}
{ streamKey && (
<div style={{marginTop: 'var(--spacing-l)'}}>
<h3>Debug Info</h3>
{/* Channel ID */}
<FormField
name={'channelId'}
label={'Channel ID'}
type={'text'}
defaultValue={activeChannelClaim.claim_id}
readOnly
/>
{/* Signature */}
<FormField
name={'signature'}
label={'Signature'}
type={'text'}
defaultValue={sigData.signature}
readOnly
/>
{/* Signature TS */}
<FormField
name={'signaturets'}
label={'Signature Timestamp'}
type={'text'}
defaultValue={sigData.signing_ts}
readOnly
/>
{/* Hex Data */}
<FormField
name={'datahex'}
label={'Hex Data'}
type={'text'}
defaultValue={toHex(activeChannelClaim.name)}
readOnly
/>
{/* Channel Public Key */}
<FormField
name={'channelpublickey'}
label={'Public Key'}
type={'text'}
defaultValue={activeChannelClaim.value.public_key}
readOnly
/>
</div>
)}
</React.Fragment>
)}
</Page>
);
}