Feat publish replays on master (#5863)
* provide livestream replay publish via url
This commit is contained in:
parent
9f3d779cf2
commit
989126c603
24 changed files with 674 additions and 150 deletions
|
@ -34,6 +34,7 @@ ENABLE_NO_SOURCE_CLAIMS=false
|
||||||
ENABLE_PREROLL_ADS=false
|
ENABLE_PREROLL_ADS=false
|
||||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
||||||
CHANNEL_STAKED_LEVEL_LIVESTREAM=5
|
CHANNEL_STAKED_LEVEL_LIVESTREAM=5
|
||||||
|
WEB_PUBLISH_SIZE_LIMIT_GB=4
|
||||||
|
|
||||||
# OG
|
# OG
|
||||||
OG_TITLE_SUFFIX=| lbry.tv
|
OG_TITLE_SUFFIX=| lbry.tv
|
||||||
|
|
|
@ -39,6 +39,7 @@ const config = {
|
||||||
ENABLE_PREROLL_ADS: process.env.ENABLE_PREROLL_ADS === 'true',
|
ENABLE_PREROLL_ADS: process.env.ENABLE_PREROLL_ADS === 'true',
|
||||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS: process.env.CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS,
|
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS: process.env.CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS,
|
||||||
CHANNEL_STAKED_LEVEL_LIVESTREAM: process.env.CHANNEL_STAKED_LEVEL_LIVESTREAM,
|
CHANNEL_STAKED_LEVEL_LIVESTREAM: process.env.CHANNEL_STAKED_LEVEL_LIVESTREAM,
|
||||||
|
WEB_PUBLISH_SIZE_LIMIT_GB: process.env.WEB_PUBLISH_SIZE_LIMIT_GB,
|
||||||
SIMPLE_SITE: process.env.SIMPLE_SITE === 'true',
|
SIMPLE_SITE: process.env.SIMPLE_SITE === 'true',
|
||||||
SHOW_ADS: process.env.SHOW_ADS === 'true',
|
SHOW_ADS: process.env.SHOW_ADS === 'true',
|
||||||
PINNED_URI_1: process.env.PINNED_URI_1,
|
PINNED_URI_1: process.env.PINNED_URI_1,
|
||||||
|
|
22
flow-typed/livestream.js
vendored
Normal file
22
flow-typed/livestream.js
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type LivestreamReplayItem = {
|
||||||
|
data: {
|
||||||
|
claimId: string,
|
||||||
|
deleted: boolean,
|
||||||
|
deletedAt: ?string,
|
||||||
|
ffprobe: any,
|
||||||
|
fileDuration: number, // decimal? float? string?
|
||||||
|
fileType: string,
|
||||||
|
fileLocation: string,
|
||||||
|
fileSize: number,
|
||||||
|
key: string,
|
||||||
|
published: boolean,
|
||||||
|
publishedAt: ?string,
|
||||||
|
service: string,
|
||||||
|
thumbnails: Array<string>,
|
||||||
|
uploadedAt: string, // Date?
|
||||||
|
},
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
declare type LivestreamReplayData = Array<LivestreamReplayItem>;
|
11
flow-typed/subscription.js
vendored
11
flow-typed/subscription.js
vendored
|
@ -1,15 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
|
||||||
import {
|
|
||||||
DOWNLOADED,
|
|
||||||
DOWNLOADING,
|
|
||||||
NOTIFY_ONLY,
|
|
||||||
VIEW_ALL,
|
|
||||||
VIEW_LATEST_FIRST,
|
|
||||||
SUGGESTED_TOP_BID,
|
|
||||||
SUGGESTED_TOP_SUBSCRIBED,
|
|
||||||
SUGGESTED_FEATURED,
|
|
||||||
} from 'constants/subscriptions';
|
|
||||||
|
|
||||||
declare type Subscription = {
|
declare type Subscription = {
|
||||||
channelName: string, // @CryptoCandor,
|
channelName: string, // @CryptoCandor,
|
||||||
|
|
|
@ -1778,5 +1778,10 @@
|
||||||
"Publishing...": "Publishing...",
|
"Publishing...": "Publishing...",
|
||||||
"Collection": "Collection",
|
"Collection": "Collection",
|
||||||
"More from %claim_name%": "More from %claim_name%",
|
"More from %claim_name%": "More from %claim_name%",
|
||||||
|
"Upload that unlabeled video you found behind the TV in 1991": "Upload that unlabeled video you found behind the TV in 1991",
|
||||||
|
"Select Replay": "Select Replay",
|
||||||
|
"Craft an epic post clearly explaining... whatever.": "Craft an epic post clearly explaining... whatever.",
|
||||||
|
"%viewer_count% currently %viewer_state%": "%viewer_count% currently %viewer_state%",
|
||||||
|
"Choose Replay": "Choose Replay",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ type Props = {
|
||||||
nag?: Node,
|
nag?: Node,
|
||||||
smallTitle?: boolean,
|
smallTitle?: boolean,
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
children?: any, // not sure how this works
|
children?: Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Card(props: Props) {
|
export default function Card(props: Props) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { SIMPLE_SITE, SITE_NAME, ENABLE_FILE_REACTIONS } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
|
@ -114,25 +113,13 @@ function FileActions(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
icon={ICONS.EDIT}
|
icon={ICONS.EDIT}
|
||||||
label={__('Edit')}
|
label={isLivestreamClaim ? __('Update') : __('Edit')}
|
||||||
navigate={`/$/${PAGES.UPLOAD}${isLivestreamClaim ? `?type=${PUBLISH_MODES.LIVESTREAM}` : ''}`}
|
navigate={`/$/${PAGES.UPLOAD}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
prepareEdit(claim, editUri, fileInfo);
|
prepareEdit(claim, editUri, fileInfo);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{claimIsMine && isLivestreamClaim && (
|
|
||||||
<Button
|
|
||||||
className="button--file-action"
|
|
||||||
icon={ICONS.PUBLISH}
|
|
||||||
label={__('Publish Replay')}
|
|
||||||
navigate={`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.FILE}`}
|
|
||||||
onClick={() => {
|
|
||||||
prepareEdit(claim, editUri, fileInfo);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDelete && (
|
{showDelete && (
|
||||||
<Button
|
<Button
|
||||||
title={__('Remove from your library')}
|
title={__('Remove from your library')}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { BITWAVE_API } from 'constants/livestream';
|
import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
@ -35,7 +35,7 @@ export default function LivestreamLink(props: Props) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function fetchIsStreaming() {
|
function fetchIsStreaming() {
|
||||||
// $FlowFixMe Bitwave's API can handle garbage
|
// $FlowFixMe Bitwave's API can handle garbage
|
||||||
fetch(`${BITWAVE_API}/${livestreamChannelId}`)
|
fetch(`${BITWAVE_LIVE_API}/${livestreamChannelId}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res && res.success && res.data && res.data.live) {
|
if (res && res.success && res.data && res.data.live) {
|
||||||
|
|
|
@ -5,15 +5,17 @@ import {
|
||||||
makeSelectPublishFormValue,
|
makeSelectPublishFormValue,
|
||||||
doUpdatePublishForm,
|
doUpdatePublishForm,
|
||||||
doClearPublish,
|
doClearPublish,
|
||||||
|
makeSelectClaimIsStreamPlaceholder,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { selectFfmpegStatus } from 'redux/selectors/settings';
|
import { selectFfmpegStatus } from 'redux/selectors/settings';
|
||||||
import PublishPage from './view';
|
import PublishPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state, props) => ({
|
||||||
name: makeSelectPublishFormValue('name')(state),
|
name: makeSelectPublishFormValue('name')(state),
|
||||||
title: makeSelectPublishFormValue('title')(state),
|
title: makeSelectPublishFormValue('title')(state),
|
||||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
|
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
|
||||||
optimize: makeSelectPublishFormValue('optimize')(state),
|
optimize: makeSelectPublishFormValue('optimize')(state),
|
||||||
isStillEditing: selectIsStillEditing(state),
|
isStillEditing: selectIsStillEditing(state),
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
|
@ -22,12 +24,13 @@ const select = state => ({
|
||||||
size: makeSelectPublishFormValue('fileSize')(state),
|
size: makeSelectPublishFormValue('fileSize')(state),
|
||||||
duration: makeSelectPublishFormValue('fileDur')(state),
|
duration: makeSelectPublishFormValue('fileDur')(state),
|
||||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||||
|
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
clearPublish: () => dispatch(doClearPublish()),
|
clearPublish: () => dispatch(doClearPublish()),
|
||||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||||
showToast: message => dispatch(doToast({ message, isError: true })),
|
showToast: (message) => dispatch(doToast({ message, isError: true })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(PublishPage);
|
export default connect(select, perform)(PublishPage);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { SITE_NAME } from 'config';
|
import { SITE_NAME, WEB_PUBLISH_SIZE_LIMIT_GB } from 'config';
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
@ -14,6 +14,11 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
|
import CopyableText from 'component/copyableText';
|
||||||
|
import Empty from 'component/common/empty';
|
||||||
|
import moment from 'moment';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
|
@ -35,9 +40,17 @@ type Props = {
|
||||||
size: number,
|
size: number,
|
||||||
duration: number,
|
duration: number,
|
||||||
isVid: boolean,
|
isVid: boolean,
|
||||||
|
subtitle: string,
|
||||||
setPublishMode: (string) => void,
|
setPublishMode: (string) => void,
|
||||||
setPrevFileText: (string) => void,
|
setPrevFileText: (string) => void,
|
||||||
header: Node,
|
header: Node,
|
||||||
|
livestreamData: LivestreamReplayData,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
|
checkLivestreams: (string, ?string, ?string) => void,
|
||||||
|
channelId: string,
|
||||||
|
channelSignature: { signature?: string, signing_ts?: string },
|
||||||
|
isCheckingLivestreams: boolean,
|
||||||
|
setWaitForFile: (boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishFile(props: Props) {
|
function PublishFile(props: Props) {
|
||||||
|
@ -63,29 +76,60 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode,
|
setPublishMode,
|
||||||
setPrevFileText,
|
setPrevFileText,
|
||||||
header,
|
header,
|
||||||
|
livestreamData,
|
||||||
|
isLivestreamClaim,
|
||||||
|
subtitle,
|
||||||
|
checkLivestreams,
|
||||||
|
channelId,
|
||||||
|
channelSignature,
|
||||||
|
isCheckingLivestreams,
|
||||||
|
setWaitForFile,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const SOURCE_NONE = 'none';
|
||||||
|
const SOURCE_SELECT = 'select';
|
||||||
|
const SOURCE_UPLOAD = 'upload';
|
||||||
|
|
||||||
|
const RECOMMENDED_BITRATE = 6000000;
|
||||||
|
const TV_PUBLISH_SIZE_LIMIT_BYTES = WEB_PUBLISH_SIZE_LIMIT_GB * 1073741824;
|
||||||
|
const TV_PUBLISH_SIZE_LIMIT_GB_STR = String(WEB_PUBLISH_SIZE_LIMIT_GB);
|
||||||
|
|
||||||
|
const PROCESSING_MB_PER_SECOND = 0.5;
|
||||||
|
const MINUTES_THRESHOLD = 30;
|
||||||
|
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
|
||||||
|
const MARKDOWN_FILE_EXTENSIONS = ['txt', 'md', 'markdown'];
|
||||||
|
const sizeInMB = Number(size) / 1000000;
|
||||||
|
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||||
const ffmpegAvail = ffmpegStatus.available;
|
const ffmpegAvail = ffmpegStatus.available;
|
||||||
const [oversized, setOversized] = useState(false);
|
const [oversized, setOversized] = useState(false);
|
||||||
const [currentFile, setCurrentFile] = useState(null);
|
const [currentFile, setCurrentFile] = useState(null);
|
||||||
const [currentFileType, setCurrentFileType] = useState(null);
|
const [currentFileType, setCurrentFileType] = useState(null);
|
||||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||||
|
|
||||||
const RECOMMENDED_BITRATE = 6000000;
|
|
||||||
const TV_PUBLISH_SIZE_LIMIT: number = 4294967296;
|
|
||||||
const TV_PUBLISH_SIZE_LIMIT_STR_GB = '4';
|
|
||||||
const UPLOAD_SIZE_MESSAGE = __(
|
const UPLOAD_SIZE_MESSAGE = __(
|
||||||
'%SITE_NAME% uploads are limited to %limit% GB. Download the app for unrestricted publishing.',
|
'%SITE_NAME% uploads are limited to %limit% GB. Download the app for unrestricted publishing.',
|
||||||
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_STR_GB }
|
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
|
||||||
);
|
);
|
||||||
const PROCESSING_MB_PER_SECOND = 0.5;
|
|
||||||
const MINUTES_THRESHOLD = 30;
|
|
||||||
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
|
|
||||||
const MARKDOWN_FILE_EXTENSIONS = ['txt', 'md', 'markdown'];
|
|
||||||
|
|
||||||
const sizeInMB = Number(size) / 1000000;
|
const fileSelectorModes = [
|
||||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
{ label: __('Choose Replay'), actionName: SOURCE_SELECT, icon: ICONS.MENU },
|
||||||
|
{ label: __('Upload'), actionName: SOURCE_UPLOAD, icon: ICONS.PUBLISH },
|
||||||
|
{ label: __('None'), actionName: SOURCE_NONE },
|
||||||
|
];
|
||||||
|
|
||||||
|
const livestreamDataStr = JSON.stringify(livestreamData);
|
||||||
|
const hasLivestreamData = livestreamData && Boolean(livestreamData.length);
|
||||||
|
const showSourceSelector = isLivestreamClaim;
|
||||||
|
|
||||||
|
const [fileSelectSource, setFileSelectSource] = useState(
|
||||||
|
IS_WEB && showSourceSelector ? SOURCE_SELECT : SOURCE_UPLOAD
|
||||||
|
);
|
||||||
|
// const [showFileUpdate, setShowFileUpdate] = useState(false);
|
||||||
|
const [selectedFileIndex, setSelectedFileIndex] = useState(null);
|
||||||
|
const PAGE_SIZE = 4;
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const totalPages =
|
||||||
|
hasLivestreamData && livestreamData.length > PAGE_SIZE ? Math.ceil(livestreamData.length / PAGE_SIZE) : 1;
|
||||||
|
|
||||||
// Reset filePath if publish mode changed
|
// Reset filePath if publish mode changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -93,9 +137,42 @@ function PublishFile(props: Props) {
|
||||||
if (currentFileType !== 'text/markdown' && !isStillEditing) {
|
if (currentFileType !== 'text/markdown' && !isStillEditing) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
}
|
}
|
||||||
|
} else if (mode === PUBLISH_MODES.LIVESTREAM) {
|
||||||
|
updatePublishForm({ filePath: '' });
|
||||||
}
|
}
|
||||||
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
||||||
|
|
||||||
|
// set default file source to select if necessary
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasLivestreamData && isLivestreamClaim) {
|
||||||
|
setWaitForFile(true);
|
||||||
|
setFileSelectSource(SOURCE_SELECT);
|
||||||
|
} else if (isLivestreamClaim) {
|
||||||
|
setFileSelectSource(SOURCE_NONE);
|
||||||
|
}
|
||||||
|
}, [hasLivestreamData, isLivestreamClaim, setFileSelectSource]);
|
||||||
|
|
||||||
|
const normalizeUrlForProtocol = (url) => {
|
||||||
|
if (url.startsWith('https://')) {
|
||||||
|
return url;
|
||||||
|
} else {
|
||||||
|
if (url.startsWith('http://')) {
|
||||||
|
return url;
|
||||||
|
} else {
|
||||||
|
return `https://${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// update remoteUrl when replay selected
|
||||||
|
useEffect(() => {
|
||||||
|
const livestreamData = JSON.parse(livestreamDataStr);
|
||||||
|
if (selectedFileIndex !== null && livestreamData && livestreamData.length) {
|
||||||
|
updatePublishForm({
|
||||||
|
remoteFileUrl: normalizeUrlForProtocol(livestreamData[selectedFileIndex].data.fileLocation),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedFileIndex, updatePublishForm, livestreamDataStr]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filePath || filePath === '') {
|
if (!filePath || filePath === '') {
|
||||||
setCurrentFile('');
|
setCurrentFile('');
|
||||||
|
@ -121,6 +198,10 @@ function PublishFile(props: Props) {
|
||||||
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePaginateReplays(page) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
function getBitrate(size, duration) {
|
function getBitrate(size, duration) {
|
||||||
const s = Number(size);
|
const s = Number(size);
|
||||||
const d = Number(duration);
|
const d = Number(duration);
|
||||||
|
@ -154,7 +235,7 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessage() {
|
function getUploadMessage() {
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
if (oversized) {
|
if (oversized) {
|
||||||
return (
|
return (
|
||||||
|
@ -186,6 +267,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!isStillEditing && name) {
|
if (!!isStillEditing && name) {
|
||||||
|
if (isLivestreamClaim) {
|
||||||
|
return (
|
||||||
|
<p className="help">{__('You can upload your own recording or select a replay when your stream is over')}</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
|
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
|
||||||
|
@ -198,7 +284,7 @@ function PublishFile(props: Props) {
|
||||||
<p className="help">
|
<p className="help">
|
||||||
{__(
|
{__(
|
||||||
'For video content, use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming. %SITE_NAME% uploads are restricted to %limit% GB.',
|
'For video content, use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming. %SITE_NAME% uploads are restricted to %limit% GB.',
|
||||||
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_STR_GB }
|
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
|
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -225,6 +311,27 @@ function PublishFile(props: Props) {
|
||||||
return newName.replace(INVALID_URI_CHARS, '-');
|
return newName.replace(INVALID_URI_CHARS, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFileSource(source) {
|
||||||
|
if (source === SOURCE_NONE) {
|
||||||
|
// clear files and remotes...
|
||||||
|
// https://github.com/lbryio/lbry-desktop/issues/5855
|
||||||
|
// publish is trying to use one field to share html file blob and string and such
|
||||||
|
// $FlowFixMe
|
||||||
|
handleFileChange(false);
|
||||||
|
updatePublishForm({ remoteFileUrl: undefined });
|
||||||
|
} else if (source === SOURCE_UPLOAD) {
|
||||||
|
updatePublishForm({ remoteFileUrl: undefined });
|
||||||
|
} else if (source === SOURCE_SELECT) {
|
||||||
|
// $FlowFixMe
|
||||||
|
handleFileChange(false);
|
||||||
|
if (selectedFileIndex !== null) {
|
||||||
|
updatePublishForm({ remoteFileUrl: livestreamData[selectedFileIndex].data.fileLocation });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFileSelectSource(source);
|
||||||
|
setWaitForFile(source !== SOURCE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
function handleTitleChange(event) {
|
function handleTitleChange(event) {
|
||||||
const title = event.target.value;
|
const title = event.target.value;
|
||||||
// Update title
|
// Update title
|
||||||
|
@ -247,7 +354,11 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!file) {
|
if (!file) {
|
||||||
updatePublishForm({ filePath: '', name: '' });
|
if (isStillEditing) {
|
||||||
|
updatePublishForm({ filePath: '' });
|
||||||
|
} else {
|
||||||
|
updatePublishForm({ filePath: '', name: '' });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +412,7 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
// we only need to enforce file sizes on 'web'
|
// we only need to enforce file sizes on 'web'
|
||||||
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT) {
|
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT_BYTES) {
|
||||||
setOversized(true);
|
setOversized(true);
|
||||||
showToast(__(UPLOAD_SIZE_MESSAGE));
|
showToast(__(UPLOAD_SIZE_MESSAGE));
|
||||||
updatePublishForm({ filePath: '', name: '' });
|
updatePublishForm({ filePath: '', name: '' });
|
||||||
|
@ -326,7 +437,7 @@ function PublishFile(props: Props) {
|
||||||
updatePublishForm(publishFormParams);
|
updatePublishForm(publishFormParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPublishFile = mode === PUBLISH_MODES.FILE;
|
const showFileUpload = mode === PUBLISH_MODES.FILE || (mode === PUBLISH_MODES.LIVESTREAM && hasLivestreamData);
|
||||||
const isPublishPost = mode === PUBLISH_MODES.POST;
|
const isPublishPost = mode === PUBLISH_MODES.POST;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -334,7 +445,7 @@ function PublishFile(props: Props) {
|
||||||
className={disabled || balance === 0 ? 'card--disabled' : ''}
|
className={disabled || balance === 0 ? 'card--disabled' : ''}
|
||||||
title={
|
title={
|
||||||
<div>
|
<div>
|
||||||
{header}
|
{header} {/* display mode buttons from parent */}
|
||||||
{publishing && <Spinner type={'small'} />}
|
{publishing && <Spinner type={'small'} />}
|
||||||
{inProgress && (
|
{inProgress && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -343,10 +454,10 @@ function PublishFile(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
subtitle={isStillEditing && __('You are currently editing your upload.')}
|
subtitle={subtitle || (isStillEditing && __('You are currently editing your upload.'))}
|
||||||
actions={
|
actions={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<PublishName />
|
<PublishName uri={uri} />
|
||||||
<FormField
|
<FormField
|
||||||
type="text"
|
type="text"
|
||||||
name="content_title"
|
name="content_title"
|
||||||
|
@ -356,28 +467,161 @@ function PublishFile(props: Props) {
|
||||||
value={title}
|
value={title}
|
||||||
onChange={handleTitleChange}
|
onChange={handleTitleChange}
|
||||||
/>
|
/>
|
||||||
{isPublishFile && (
|
{/* Decide whether to show file upload or replay selector */}
|
||||||
|
{/* @if TARGET='web' */}
|
||||||
|
<>
|
||||||
|
{showSourceSelector && (
|
||||||
|
<fieldset-section>
|
||||||
|
<div className="section__actions--between section__actions--align-bottom">
|
||||||
|
<div>
|
||||||
|
<label>{__('Add replay video')}</label>
|
||||||
|
<div className="button-group">
|
||||||
|
{fileSelectorModes.map((fmode) => (
|
||||||
|
<Button
|
||||||
|
key={fmode.label}
|
||||||
|
icon={fmode.icon || undefined}
|
||||||
|
iconSize={18}
|
||||||
|
label={fmode.label}
|
||||||
|
button="alt"
|
||||||
|
onClick={() => {
|
||||||
|
// $FlowFixMe
|
||||||
|
handleFileSource(fmode.actionName);
|
||||||
|
}}
|
||||||
|
className={classnames('button-toggle', {
|
||||||
|
'button-toggle--active': fileSelectSource === fmode.actionName,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fileSelectSource === SOURCE_SELECT && (
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
label={__('Check for Replays')}
|
||||||
|
disabled={isCheckingLivestreams}
|
||||||
|
icon={ICONS.REFRESH}
|
||||||
|
onClick={() =>
|
||||||
|
checkLivestreams(channelId, channelSignature.signature, channelSignature.signing_ts)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset-section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fileSelectSource === SOURCE_UPLOAD && showFileUpload && (
|
||||||
|
<>
|
||||||
|
<FileSelector
|
||||||
|
label={__('Video file')}
|
||||||
|
disabled={disabled}
|
||||||
|
currentPath={currentFile}
|
||||||
|
onFileChosen={handleFileChange}
|
||||||
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
|
accept="video/mp4,video/x-m4v,video/*"
|
||||||
|
placeholder={__('Select video file to upload')}
|
||||||
|
/>
|
||||||
|
{getUploadMessage()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fileSelectSource === SOURCE_SELECT && showFileUpload && hasLivestreamData && !isCheckingLivestreams && (
|
||||||
|
<>
|
||||||
|
<fieldset-section>
|
||||||
|
<label>{__('Select Replay')}</label>
|
||||||
|
<div className="table__wrapper">
|
||||||
|
<table className="table table--livestream-data">
|
||||||
|
<tbody>
|
||||||
|
{livestreamData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE).map((item, i) => (
|
||||||
|
<tr
|
||||||
|
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
|
||||||
|
key={item.id}
|
||||||
|
className={classnames('livestream__data-row', {
|
||||||
|
'livestream__data-row--selected': selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<FormField
|
||||||
|
type="radio"
|
||||||
|
checked={selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i}
|
||||||
|
label={null}
|
||||||
|
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
|
||||||
|
className="livestream__data-row-radio"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="livestream_thumb_container">
|
||||||
|
{item.data.thumbnails.slice(0, 3).map((thumb) => (
|
||||||
|
<img key={thumb} className="livestream___thumb" src={thumb} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{`${Math.floor(item.data.fileDuration / 60)} ${
|
||||||
|
Math.floor(item.data.fileDuration / 60) > 1 ? __('minutes') : __('minute')
|
||||||
|
}`}
|
||||||
|
<div className="table__item-label">
|
||||||
|
{`${moment(item.data.uploadedAt).from(moment())}`}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<CopyableText
|
||||||
|
primaryButton
|
||||||
|
copyable={normalizeUrlForProtocol(item.data.fileLocation)}
|
||||||
|
snackMessage={__('Url copied.')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</fieldset-section>
|
||||||
|
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
||||||
|
<fieldset-section>
|
||||||
|
<ReactPaginate
|
||||||
|
pageCount={totalPages}
|
||||||
|
pageRangeDisplayed={2}
|
||||||
|
previousLabel="‹"
|
||||||
|
nextLabel="›"
|
||||||
|
activeClassName="pagination__item--selected"
|
||||||
|
pageClassName="pagination__item"
|
||||||
|
previousClassName="pagination__item pagination__item--previous"
|
||||||
|
nextClassName="pagination__item pagination__item--next"
|
||||||
|
breakClassName="pagination__item pagination__item--break"
|
||||||
|
marginPagesDisplayed={2}
|
||||||
|
onPageChange={(e) => handlePaginateReplays(e.selected + 1)}
|
||||||
|
forcePage={currentPage - 1}
|
||||||
|
initialPage={currentPage - 1}
|
||||||
|
containerClassName="pagination"
|
||||||
|
/>
|
||||||
|
</fieldset-section>
|
||||||
|
</fieldset-group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fileSelectSource === SOURCE_SELECT && showFileUpload && !hasLivestreamData && !isCheckingLivestreams && (
|
||||||
|
<div className="main--empty empty">
|
||||||
|
<Empty text={__('No replays found.')} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{fileSelectSource === SOURCE_SELECT && showFileUpload && isCheckingLivestreams && (
|
||||||
|
<div className="main--empty empty">
|
||||||
|
<Spinner small />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
{/* @endif */}
|
||||||
|
{/* @if TARGET='app' */}
|
||||||
|
{showFileUpload && (
|
||||||
<FileSelector
|
<FileSelector
|
||||||
label={__('File')}
|
label={__('File')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
currentPath={currentFile}
|
currentPath={currentFile}
|
||||||
onFileChosen={handleFileChange}
|
onFileChosen={handleFileChange}
|
||||||
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
|
accept="video/mp4,video/x-m4v,video/*"
|
||||||
|
placeholder={__('Select video file to upload')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showFileUpload && (
|
||||||
{isPublishPost && (
|
|
||||||
<PostEditor
|
|
||||||
label={__('Post --[noun, markdown post tab button]--')}
|
|
||||||
uri={uri}
|
|
||||||
disabled={disabled}
|
|
||||||
fileMimeType={fileMimeType}
|
|
||||||
setPrevFileText={setPrevFileText}
|
|
||||||
setCurrentFileType={setCurrentFileType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isPublishFile && getMessage()}
|
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
{isPublishFile && (
|
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={userOptimize}
|
checked={userOptimize}
|
||||||
|
@ -387,7 +631,7 @@ function PublishFile(props: Props) {
|
||||||
name="optimize"
|
name="optimize"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isPublishFile && !ffmpegAvail && (
|
{showFileUpload && !ffmpegAvail && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
@ -398,7 +642,7 @@ function PublishFile(props: Props) {
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isPublishFile && Boolean(size) && ffmpegAvail && optimize && isVid && (
|
{showFileUpload && Boolean(size) && ffmpegAvail && optimize && isVid && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
@ -412,6 +656,16 @@ function PublishFile(props: Props) {
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
{isPublishPost && (
|
||||||
|
<PostEditor
|
||||||
|
label={__('Post --[noun, markdown post tab button]--')}
|
||||||
|
uri={uri}
|
||||||
|
disabled={disabled}
|
||||||
|
fileMimeType={fileMimeType}
|
||||||
|
setPrevFileText={setPrevFileText}
|
||||||
|
setCurrentFileType={setCurrentFileType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -12,7 +12,11 @@ import {
|
||||||
doPrepareEdit,
|
doPrepareEdit,
|
||||||
doCheckPublishNameAvailability,
|
doCheckPublishNameAvailability,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
|
selectMyChannelClaims,
|
||||||
|
makeSelectClaimIsStreamPlaceholder,
|
||||||
|
makeSelectPublishFormValue,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import { doPublishDesktop } from 'redux/actions/publish';
|
import { doPublishDesktop } from 'redux/actions/publish';
|
||||||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
||||||
import {
|
import {
|
||||||
|
@ -22,27 +26,40 @@ import {
|
||||||
selectActiveChannelStakedLevel,
|
selectActiveChannelStakedLevel,
|
||||||
} from 'redux/selectors/app';
|
} from 'redux/selectors/app';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import PublishPage from './view';
|
import PublishPage from './view';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
import { selectUser } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => {
|
||||||
...selectPublishFormValues(state),
|
const myClaimForUri = selectMyClaimForUri(state);
|
||||||
user: selectUser(state),
|
const permanentUrl = (myClaimForUri && myClaimForUri.permanent_url) || '';
|
||||||
// The winning claim for a short lbry uri
|
const isPostClaim = makeSelectFileRenderModeForUri(permanentUrl)(state) === RENDER_MODES.MARKDOWN;
|
||||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
|
||||||
// My previously published claims under this short lbry uri
|
return {
|
||||||
myClaimForUri: selectMyClaimForUri(state),
|
...selectPublishFormValues(state),
|
||||||
// If I clicked the "edit" button, have I changed the uri?
|
user: selectUser(state),
|
||||||
// Need this to make it easier to find the source on previously published content
|
// The winning claim for a short lbry uri
|
||||||
isStillEditing: selectIsStillEditing(state),
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
isResolvingUri: selectIsResolvingPublishUris(state),
|
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(permanentUrl)(state),
|
||||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
isPostClaim,
|
||||||
modal: selectModal(state),
|
permanentUrl,
|
||||||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
// My previously published claims under this short lbry uri
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
myClaimForUri,
|
||||||
incognito: selectIncognito(state),
|
// If I clicked the "edit" button, have I changed the uri?
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
// Need this to make it easier to find the source on previously published content
|
||||||
});
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
|
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
|
||||||
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
|
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||||
|
modal: selectModal(state),
|
||||||
|
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||||
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
incognito: selectIncognito(state),
|
||||||
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES, Lbry } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelSelect from 'component/channelSelector';
|
import ChannelSelect from 'component/channelSelector';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -27,6 +27,8 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
import { toHex } from 'util/hex';
|
||||||
|
import { BITWAVE_REPLAY_API } from 'constants/livestream';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -82,6 +84,10 @@ type Props = {
|
||||||
incognito: boolean,
|
incognito: boolean,
|
||||||
user: ?User,
|
user: ?User,
|
||||||
activeChannelStakedLevel: number,
|
activeChannelStakedLevel: number,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
|
isPostClaim: boolean,
|
||||||
|
permanentUrl: ?string,
|
||||||
|
remoteUrl: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishForm(props: Props) {
|
function PublishForm(props: Props) {
|
||||||
|
@ -114,28 +120,69 @@ function PublishForm(props: Props) {
|
||||||
incognito,
|
incognito,
|
||||||
user,
|
user,
|
||||||
activeChannelStakedLevel,
|
activeChannelStakedLevel,
|
||||||
|
isLivestreamClaim,
|
||||||
|
isPostClaim,
|
||||||
|
permanentUrl,
|
||||||
|
remoteUrl,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { replace, location } = useHistory();
|
const { replace, location } = useHistory();
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const urlParams = new URLSearchParams(location.search);
|
||||||
const uploadType = urlParams.get('type');
|
const TYPE_PARAM = 'type';
|
||||||
const livestreamEnabled =
|
const uploadType = urlParams.get(TYPE_PARAM);
|
||||||
|
const enableLivestream =
|
||||||
ENABLE_NO_SOURCE_CLAIMS &&
|
ENABLE_NO_SOURCE_CLAIMS &&
|
||||||
user &&
|
user &&
|
||||||
!user.odysee_live_disabled &&
|
!user.odysee_live_disabled &&
|
||||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
|
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const MODES = livestreamEnabled
|
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
|
||||||
? Object.values(PUBLISH_MODES)
|
if (editingURI) {
|
||||||
: Object.values(PUBLISH_MODES).filter((mode) => mode !== PUBLISH_MODES.LIVESTREAM);
|
if (isPostClaim) {
|
||||||
|
return mode === PUBLISH_MODES.POST;
|
||||||
|
} else if (isLivestreamClaim) {
|
||||||
|
return mode === PUBLISH_MODES.LIVESTREAM && enableLivestream;
|
||||||
|
} else {
|
||||||
|
return mode === PUBLISH_MODES.FILE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode === PUBLISH_MODES.LIVESTREAM) {
|
||||||
|
return enableLivestream;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const MODE_TO_I18N_STR = {
|
const MODE_TO_I18N_STR = {
|
||||||
[PUBLISH_MODES.FILE]: SIMPLE_SITE ? 'Video' : 'File',
|
[PUBLISH_MODES.FILE]: SIMPLE_SITE ? 'Video' : 'File',
|
||||||
[PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--',
|
[PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--',
|
||||||
[PUBLISH_MODES.LIVESTREAM]: 'Livestream --[noun, livestream tab button]--',
|
[PUBLISH_MODES.LIVESTREAM]: 'Livestream --[noun, livestream tab button]--',
|
||||||
};
|
};
|
||||||
// Component state
|
|
||||||
const [mode, setMode] = React.useState(uploadType || PUBLISH_MODES.FILE);
|
const [mode, setMode] = React.useState(uploadType || PUBLISH_MODES.FILE);
|
||||||
|
const [isCheckingLivestreams, setCheckingLivestreams] = React.useState(false);
|
||||||
|
|
||||||
|
let customSubtitle;
|
||||||
|
if (mode === PUBLISH_MODES.LIVESTREAM || isLivestreamClaim) {
|
||||||
|
if (isLivestreamClaim) {
|
||||||
|
customSubtitle = __('Update your livestream');
|
||||||
|
} else {
|
||||||
|
customSubtitle = __('Prepare an upcoming livestream');
|
||||||
|
}
|
||||||
|
} else if (mode === PUBLISH_MODES.POST || isPostClaim) {
|
||||||
|
if (isPostClaim) {
|
||||||
|
customSubtitle = __('Edit your post');
|
||||||
|
} else {
|
||||||
|
customSubtitle = __('Craft an epic post clearly explaining... whatever.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (editingURI) {
|
||||||
|
customSubtitle = __('Update your video');
|
||||||
|
} else {
|
||||||
|
customSubtitle = __('Upload that unlabeled video you found behind the TV in 1991');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [autoSwitchMode, setAutoSwitchMode] = React.useState(true);
|
const [autoSwitchMode, setAutoSwitchMode] = React.useState(true);
|
||||||
|
|
||||||
// Used to check if the url name has changed:
|
// Used to check if the url name has changed:
|
||||||
|
@ -145,20 +192,27 @@ function PublishForm(props: Props) {
|
||||||
const [fileEdited, setFileEdited] = React.useState(false);
|
const [fileEdited, setFileEdited] = React.useState(false);
|
||||||
const [prevFileText, setPrevFileText] = React.useState('');
|
const [prevFileText, setPrevFileText] = React.useState('');
|
||||||
|
|
||||||
|
const [waitForFile, setWaitForFile] = useState(false);
|
||||||
|
const [livestreamData, setLivestreamData] = React.useState([]);
|
||||||
|
const [signedMessage, setSignedMessage] = React.useState({ signature: undefined, signing_ts: undefined });
|
||||||
|
const signedMessageStr = JSON.stringify(signedMessage);
|
||||||
const TAGS_LIMIT = 5;
|
const TAGS_LIMIT = 5;
|
||||||
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
|
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
|
||||||
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
|
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
|
||||||
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
|
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
|
||||||
const isInProgress = filePath || editingURI || name || title;
|
const isInProgress = filePath || editingURI || name || title;
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
|
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
|
||||||
// Editing content info
|
// Editing content info
|
||||||
const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined;
|
|
||||||
const fileMimeType =
|
const fileMimeType =
|
||||||
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
|
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
|
||||||
? myClaimForUri.value.source.media_type
|
? myClaimForUri.value.source.media_type
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const claimChannelId = myClaimForUri && myClaimForUri.signing_channel && myClaimForUri.signing_channel.claim_id;
|
||||||
|
|
||||||
const nameEdited = isStillEditing && name !== prevName;
|
const nameEdited = isStillEditing && name !== prevName;
|
||||||
|
|
||||||
|
const waitingForFile = waitForFile && !remoteUrl && !filePath;
|
||||||
// If they are editing, they don't need a new file chosen
|
// If they are editing, they don't need a new file chosen
|
||||||
const formValidLessFile =
|
const formValidLessFile =
|
||||||
name &&
|
name &&
|
||||||
|
@ -173,11 +227,33 @@ function PublishForm(props: Props) {
|
||||||
|
|
||||||
const formValid = isOverwritingExistingClaim
|
const formValid = isOverwritingExistingClaim
|
||||||
? false
|
? false
|
||||||
: editingURI && !filePath
|
: editingURI && !filePath // if we're editing we don't need a file
|
||||||
? isStillEditing && formValidLessFile
|
? isStillEditing && formValidLessFile && !waitingForFile
|
||||||
: formValidLessFile;
|
: formValidLessFile;
|
||||||
|
|
||||||
const [previewing, setPreviewing] = React.useState(false);
|
const [previewing, setPreviewing] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (activeChannelClaimStr) {
|
||||||
|
const channelClaim = JSON.parse(activeChannelClaimStr);
|
||||||
|
const message = 'get-claim-id-replays';
|
||||||
|
setSignedMessage({ signature: null, signing_ts: null });
|
||||||
|
// ensure we have a channel
|
||||||
|
if (channelClaim.claim_id) {
|
||||||
|
Lbry.channel_sign({
|
||||||
|
channel_id: channelClaim.claim_id,
|
||||||
|
hexdata: toHex(message),
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
setSignedMessage(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSignedMessage({ signature: null, signing_ts: null });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChannelClaimStr, setSignedMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -186,12 +262,37 @@ function PublishForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [modal]);
|
}, [modal]);
|
||||||
|
|
||||||
const isLivestream = mode === PUBLISH_MODES.LIVESTREAM;
|
// move this to lbryinc OR to a file under ui, and/or provide a standardized livestreaming config.
|
||||||
|
function fetchLivestreams(channelId, signature, timestamp) {
|
||||||
|
setCheckingLivestreams(true);
|
||||||
|
fetch(`${BITWAVE_REPLAY_API}/${channelId}?signature=${signature || ''}&signing_ts=${timestamp || ''}`) // claimChannelId
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (!res || !res.data) {
|
||||||
|
setLivestreamData([]);
|
||||||
|
}
|
||||||
|
setLivestreamData(res.data);
|
||||||
|
setCheckingLivestreams(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setLivestreamData([]);
|
||||||
|
setCheckingLivestreams(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const signedMessage = JSON.parse(signedMessageStr);
|
||||||
|
if (claimChannelId && isLivestreamClaim && signedMessage.signature) {
|
||||||
|
fetchLivestreams(claimChannelId, signedMessage.signature, signedMessage.signing_ts);
|
||||||
|
}
|
||||||
|
}, [claimChannelId, isLivestreamClaim, signedMessageStr]);
|
||||||
|
|
||||||
|
const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM;
|
||||||
let submitLabel;
|
let submitLabel;
|
||||||
if (publishing) {
|
if (publishing) {
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
submitLabel = __('Saving...');
|
submitLabel = __('Saving...');
|
||||||
} else if (isLivestream) {
|
} else if (isLivestreamMode) {
|
||||||
submitLabel = __('Creating...');
|
submitLabel = __('Creating...');
|
||||||
} else {
|
} else {
|
||||||
submitLabel = __('Uploading...');
|
submitLabel = __('Uploading...');
|
||||||
|
@ -201,7 +302,7 @@ function PublishForm(props: Props) {
|
||||||
} else {
|
} else {
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
submitLabel = __('Save');
|
submitLabel = __('Save');
|
||||||
} else if (isLivestream) {
|
} else if (isLivestreamMode) {
|
||||||
submitLabel = __('Create');
|
submitLabel = __('Create');
|
||||||
} else {
|
} else {
|
||||||
submitLabel = __('Upload');
|
submitLabel = __('Upload');
|
||||||
|
@ -221,7 +322,7 @@ function PublishForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [thumbnail, resetThumbnailStatus]);
|
}, [thumbnail, resetThumbnailStatus]);
|
||||||
|
|
||||||
// Save current name of the editing claim
|
// Save previous name of the editing claim
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStillEditing && (!prevName || !prevName.trim() === '')) {
|
if (isStillEditing && (!prevName || !prevName.trim() === '')) {
|
||||||
if (name !== prevName) {
|
if (name !== prevName) {
|
||||||
|
@ -263,17 +364,18 @@ function PublishForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [name, activeChannelName, resolveUri, updatePublishForm, checkAvailability]);
|
}, [name, activeChannelName, resolveUri, updatePublishForm, checkAvailability]);
|
||||||
|
|
||||||
|
// because publish editingUri is channel_short/claim_long and we don't have that, resolve it.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// because editingURI is lbry://channel_short/claim_long and that particular shape won't map to the claimId yet
|
|
||||||
if (editingURI) {
|
if (editingURI) {
|
||||||
resolveUri(editingURI);
|
resolveUri(editingURI);
|
||||||
}
|
}
|
||||||
}, [editingURI, resolveUri]);
|
}, [editingURI, resolveUri]);
|
||||||
|
|
||||||
|
// set isMarkdownPost in publish form if so, also update isLivestreamPublish
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updatePublishForm({
|
updatePublishForm({
|
||||||
isMarkdownPost: mode === PUBLISH_MODES.POST,
|
isMarkdownPost: mode === PUBLISH_MODES.POST,
|
||||||
isLivestreamPublish: isLivestream,
|
isLivestreamPublish: mode === PUBLISH_MODES.LIVESTREAM,
|
||||||
});
|
});
|
||||||
}, [mode, updatePublishForm]);
|
}, [mode, updatePublishForm]);
|
||||||
|
|
||||||
|
@ -282,14 +384,15 @@ function PublishForm(props: Props) {
|
||||||
updatePublishForm({ channel: undefined });
|
updatePublishForm({ channel: undefined });
|
||||||
|
|
||||||
// Anonymous livestreams aren't supported
|
// Anonymous livestreams aren't supported
|
||||||
if (isLivestream) {
|
if (isLivestreamMode) {
|
||||||
setMode(PUBLISH_MODES.FILE);
|
setMode(PUBLISH_MODES.FILE);
|
||||||
}
|
}
|
||||||
} else if (activeChannelName) {
|
} else if (activeChannelName) {
|
||||||
updatePublishForm({ channel: activeChannelName });
|
updatePublishForm({ channel: activeChannelName });
|
||||||
}
|
}
|
||||||
}, [activeChannelName, incognito, updatePublishForm]);
|
}, [activeChannelName, incognito, updatePublishForm, isLivestreamMode]);
|
||||||
|
|
||||||
|
// set mode based on urlParams 'type'
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _uploadType = uploadType && uploadType.toLowerCase();
|
const _uploadType = uploadType && uploadType.toLowerCase();
|
||||||
|
|
||||||
|
@ -310,19 +413,24 @@ function PublishForm(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// LiveStream publish
|
// LiveStream publish
|
||||||
if (_uploadType === PUBLISH_MODES.LIVESTREAM.toLowerCase() && livestreamEnabled) {
|
if (_uploadType === PUBLISH_MODES.LIVESTREAM.toLowerCase()) {
|
||||||
setMode(PUBLISH_MODES.LIVESTREAM);
|
if (enableLivestream) {
|
||||||
|
setMode(PUBLISH_MODES.LIVESTREAM);
|
||||||
|
} else {
|
||||||
|
setMode(PUBLISH_MODES.FILE);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to standard file publish
|
// Default to standard file publish
|
||||||
setMode(PUBLISH_MODES.FILE);
|
setMode(PUBLISH_MODES.FILE);
|
||||||
}, [uploadType, livestreamEnabled]);
|
}, [uploadType, enableLivestream]);
|
||||||
|
|
||||||
|
// if we have a type urlparam, update it? necessary?
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uploadType) return;
|
if (!uploadType) return;
|
||||||
const newParams = new URLSearchParams();
|
const newParams = new URLSearchParams();
|
||||||
newParams.set('type', mode.toLowerCase());
|
newParams.set(TYPE_PARAM, mode.toLowerCase());
|
||||||
replace({ search: newParams.toString() });
|
replace({ search: newParams.toString() });
|
||||||
}, [mode, uploadType]);
|
}, [mode, uploadType]);
|
||||||
|
|
||||||
|
@ -391,7 +499,7 @@ function PublishForm(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Publish file
|
// Publish file
|
||||||
if (mode === PUBLISH_MODES.FILE || isLivestream) {
|
if (mode === PUBLISH_MODES.FILE || isLivestreamMode) {
|
||||||
runPublish = true;
|
runPublish = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,19 +536,26 @@ function PublishForm(props: Props) {
|
||||||
// Editing claim uri
|
// Editing claim uri
|
||||||
return (
|
return (
|
||||||
<div className="card-stack">
|
<div className="card-stack">
|
||||||
<ChannelSelect hideAnon={isLivestream} disabled={disabled} />
|
<ChannelSelect hideAnon={isLivestreamMode} disabled={disabled} />
|
||||||
|
|
||||||
<PublishFile
|
<PublishFile
|
||||||
uri={uri}
|
uri={permanentUrl}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
fileMimeType={fileMimeType}
|
fileMimeType={fileMimeType}
|
||||||
disabled={disabled || publishing}
|
disabled={disabled || publishing}
|
||||||
inProgress={isInProgress}
|
inProgress={isInProgress}
|
||||||
setPublishMode={setMode}
|
setPublishMode={setMode}
|
||||||
setPrevFileText={setPrevFileText}
|
setPrevFileText={setPrevFileText}
|
||||||
|
livestreamData={livestreamData}
|
||||||
|
subtitle={customSubtitle}
|
||||||
|
setWaitForFile={setWaitForFile}
|
||||||
|
isCheckingLivestreams={isCheckingLivestreams}
|
||||||
|
checkLivestreams={fetchLivestreams}
|
||||||
|
channelId={claimChannelId}
|
||||||
|
channelSignature={signedMessage}
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
{MODES.map((modeName) => (
|
{AVAILABLE_MODES.map((modeName) => (
|
||||||
<Button
|
<Button
|
||||||
key={String(modeName)}
|
key={String(modeName)}
|
||||||
icon={modeName}
|
icon={modeName}
|
||||||
|
@ -460,7 +575,7 @@ function PublishForm(props: Props) {
|
||||||
{!publishing && (
|
{!publishing && (
|
||||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||||
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
|
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
|
||||||
<Card actions={<SelectThumbnail />} />
|
<Card actions={<SelectThumbnail livestreamdData={livestreamData} />} />
|
||||||
<TagsSelect
|
<TagsSelect
|
||||||
suggestMature={!SIMPLE_SITE}
|
suggestMature={!SIMPLE_SITE}
|
||||||
disableAutoFocus
|
disableAutoFocus
|
||||||
|
@ -489,7 +604,7 @@ function PublishForm(props: Props) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PublishBid disabled={isStillEditing || formDisabled} />
|
<PublishBid disabled={isStillEditing || formDisabled} />
|
||||||
{!isLivestream && <PublishPrice disabled={formDisabled} />}
|
{!isLivestreamMode && <PublishPrice disabled={formDisabled} />}
|
||||||
<PublishAdditionalOptions disabled={formDisabled} />
|
<PublishAdditionalOptions disabled={formDisabled} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -511,7 +626,7 @@ function PublishForm(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<p className="help">
|
<p className="help">
|
||||||
{!formDisabled && !formValid ? (
|
{!formDisabled && !formValid ? (
|
||||||
<PublishFormErrors mode={mode} />
|
<PublishFormErrors mode={mode} waitForFile={waitingForFile} />
|
||||||
) : (
|
) : (
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
|
|
@ -12,14 +12,26 @@ type Props = {
|
||||||
filePath: ?string,
|
filePath: ?string,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
uploadThumbnailStatus: string,
|
uploadThumbnailStatus: string,
|
||||||
|
waitForFile: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishFormErrors(props: Props) {
|
function PublishFormErrors(props: Props) {
|
||||||
const { name, title, bid, bidError, editingURI, filePath, isStillEditing, uploadThumbnailStatus } = props;
|
const {
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
bid,
|
||||||
|
bidError,
|
||||||
|
editingURI,
|
||||||
|
filePath,
|
||||||
|
isStillEditing,
|
||||||
|
uploadThumbnailStatus,
|
||||||
|
waitForFile,
|
||||||
|
} = props;
|
||||||
// These are extra help
|
// These are extra help
|
||||||
// If there is an error it will be presented as an inline error as well
|
// If there is an error it will be presented as an inline error as well
|
||||||
return (
|
return (
|
||||||
<div className="error__text">
|
<div className="error__text">
|
||||||
|
{waitForFile && <div>{__('Choose a replay file, or select None')}</div>}
|
||||||
{!title && <div>{__('A title is required')}</div>}
|
{!title && <div>{__('A title is required')}</div>}
|
||||||
{!name && <div>{__('A URL is required')}</div>}
|
{!name && <div>{__('A URL is required')}</div>}
|
||||||
{!isNameValid(name, false) && INVALID_NAME_ERROR}
|
{!isNameValid(name, false) && INVALID_NAME_ERROR}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const BITWAVE_EMBED_URL = 'https://bitwave.tv/odysee';
|
export const BITWAVE_EMBED_URL = 'https://bitwave.tv/odysee';
|
||||||
export const BITWAVE_API = 'https://api.bitwave.tv/v1/odysee/live';
|
export const BITWAVE_LIVE_API = 'https://api.bitwave.tv/v1/odysee/live';
|
||||||
|
export const BITWAVE_REPLAY_API = 'https://api.bitwave.tv/v1/replays/odysee';
|
||||||
|
|
|
@ -6,22 +6,29 @@ import {
|
||||||
selectPublishFormValues,
|
selectPublishFormValues,
|
||||||
selectIsStillEditing,
|
selectIsStillEditing,
|
||||||
selectMyChannelClaims,
|
selectMyChannelClaims,
|
||||||
|
makeSelectClaimIsStreamPlaceholder,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings';
|
import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doPublishDesktop } from 'redux/actions/publish';
|
import { doPublishDesktop } from 'redux/actions/publish';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state, props) => {
|
||||||
...selectPublishFormValues(state),
|
const editingUri = makeSelectPublishFormValue('editingURI')(state);
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
return {
|
||||||
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
|
...selectPublishFormValues(state),
|
||||||
publishing: makeSelectPublishFormValue('publishing')(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
isStillEditing: selectIsStillEditing(state),
|
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||||
ffmpegStatus: selectFfmpegStatus(state),
|
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
|
||||||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
publishing: makeSelectPublishFormValue('publishing')(state),
|
||||||
});
|
remoteFile: makeSelectPublishFormValue('remoteFileUrl')(state),
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
ffmpegStatus: selectFfmpegStatus(state),
|
||||||
|
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||||
|
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(editingUri)(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),
|
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),
|
||||||
|
|
|
@ -43,6 +43,8 @@ type Props = {
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
publishSuccess: boolean,
|
publishSuccess: boolean,
|
||||||
publishing: boolean,
|
publishing: boolean,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
|
remoteFile: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
// class ModalPublishPreview extends React.PureComponent<Props> {
|
// class ModalPublishPreview extends React.PureComponent<Props> {
|
||||||
|
@ -74,10 +76,14 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
publishing,
|
publishing,
|
||||||
publish,
|
publish,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
isLivestreamClaim,
|
||||||
|
remoteFile,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const livestream =
|
const livestream =
|
||||||
|
(uri && isLivestreamClaim) ||
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
previewResponse.outputs[0] && previewResponse.outputs[0].value && !previewResponse.outputs[0].value.source;
|
(previewResponse.outputs[0] && previewResponse.outputs[0].value && !previewResponse.outputs[0].value.source);
|
||||||
// leave the confirm modal up if we're not going straight to upload/reflecting
|
// leave the confirm modal up if we're not going straight to upload/reflecting
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -122,7 +128,11 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
||||||
let modalTitle;
|
let modalTitle;
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
modalTitle = __('Confirm Edit');
|
if (livestream) {
|
||||||
|
modalTitle = __('Confirm Update');
|
||||||
|
} else {
|
||||||
|
modalTitle = __('Confirm Edit');
|
||||||
|
}
|
||||||
} else if (livestream) {
|
} else if (livestream) {
|
||||||
modalTitle = __('Create Livestream');
|
modalTitle = __('Create Livestream');
|
||||||
} else {
|
} else {
|
||||||
|
@ -208,6 +218,7 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
<table className="table table--condensed table--publish-preview">
|
<table className="table table--condensed table--publish-preview">
|
||||||
<tbody>
|
<tbody>
|
||||||
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
|
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
|
||||||
|
{livestream && remoteFile && createRow(__('Replay'), __('Remote File Selected'))}
|
||||||
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
|
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
|
||||||
{createRow(__('Title'), title)}
|
{createRow(__('Title'), title)}
|
||||||
{createRow(__('Description'), descriptionValue)}
|
{createRow(__('Description'), descriptionValue)}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { BITWAVE_API } from 'constants/livestream';
|
import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import LivestreamLayout from 'component/livestreamLayout';
|
import LivestreamLayout from 'component/livestreamLayout';
|
||||||
|
@ -56,7 +56,7 @@ export default function LivestreamPage(props: Props) {
|
||||||
let interval;
|
let interval;
|
||||||
function checkIsLive() {
|
function checkIsLive() {
|
||||||
// $FlowFixMe Bitwave's API can handle garbage
|
// $FlowFixMe Bitwave's API can handle garbage
|
||||||
fetch(`${BITWAVE_API}/${livestreamChannelId}`)
|
fetch(`${BITWAVE_LIVE_API}/${livestreamChannelId}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res || !res.data) {
|
if (!res || !res.data) {
|
||||||
|
|
|
@ -55,9 +55,11 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
const helpText = (
|
const helpText = (
|
||||||
<div className="section__subtitle">
|
<div className="section__subtitle">
|
||||||
<p>
|
<p>
|
||||||
{__(`Create a Livestream by first submitting your Livestream details and waiting for approval confirmation.`)}{' '}
|
|
||||||
{__(
|
{__(
|
||||||
`The livestream will not be visible on your channel until you are live, but you can share the URL in advance.`
|
`Create a Livestream by first submitting your livestream details and waiting for approval confirmation. This can be done well in advance and will take a few minutes.`
|
||||||
|
)}{' '}
|
||||||
|
{__(
|
||||||
|
`The livestream will not be visible on your channel page until you are live, but you can share the URL in advance.`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{__(
|
{__(
|
||||||
`Once the your livestream is confirmed, configure your streaming software (OBS, Restream, etc) and input the server URL along with the stream key in it.`
|
`Once the your livestream is confirmed, configure your streaming software (OBS, Restream, etc) and input the server URL along with the stream key in it.`
|
||||||
|
@ -73,25 +75,18 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
<li>{__(`Tune: Zerolatency`)}</li>
|
<li>{__(`Tune: Zerolatency`)}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{__(`If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.`)}
|
||||||
`If using other livestreaming software, make sure the bitrate is below 5000 kbps or the stream will not work.`
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{__(`After your stream:
|
||||||
`Please note: You'll need to record your own stream through your software if you plan to share it afterward. You can also delete it if you prefer not to upload the copy.`
|
Click the Update button on the content page. This will allow you to select a replay or upload your own edited MP4. Replays are limited to 4 hours and may take a few minutes to show (use the Check For Replays button).`)}
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>{__(`Click Save, then confirm, and you are done!`)}</p>
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{__(
|
||||||
`In the near future, this manual step will be removed and you will be able to share the stream right after its finished without needing to record it yourself.`
|
`Note: If you don't plan on publishing your replay, you'll want to delete your livestream and then start with a fresh one next time.`
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
{__(`After your livestream:
|
|
||||||
Click the Publish Replay button. This will allow you to edit details before sharing on Odysee. Be sure to select the saved mp4 file you recorded.`)}
|
|
||||||
</p>
|
|
||||||
<p>{__(`Click Save and you are done!`)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,10 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
|
||||||
const noFileParam = !filePath || filePath === NO_FILE;
|
const noFileParam = !filePath || filePath === NO_FILE;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const editingUri = makeSelectPublishFormValue('editingURI')(state) || '';
|
const editingUri = makeSelectPublishFormValue('editingURI')(state) || '';
|
||||||
|
const remoteUrl = makeSelectPublishFormValue('remoteFileUrl')(state);
|
||||||
const claim = makeSelectClaimForUri(editingUri)(state) || {};
|
const claim = makeSelectClaimForUri(editingUri)(state) || {};
|
||||||
const hasSourceFile = claim.value && claim.value.source;
|
const hasSourceFile = claim.value && claim.value.source;
|
||||||
const redirectToLivestream = noFileParam && !hasSourceFile;
|
const redirectToLivestream = noFileParam && !hasSourceFile && !remoteUrl;
|
||||||
|
|
||||||
const publishSuccess = (successResponse, lbryFirstError) => {
|
const publishSuccess = (successResponse, lbryFirstError) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
@import 'component/form-field';
|
@import 'component/form-field';
|
||||||
@import 'component/header';
|
@import 'component/header';
|
||||||
@import 'component/icon';
|
@import 'component/icon';
|
||||||
@import 'component/livestream';
|
|
||||||
@import 'component/main';
|
@import 'component/main';
|
||||||
@import 'component/markdown-editor';
|
@import 'component/markdown-editor';
|
||||||
@import 'component/markdown-preview';
|
@import 'component/markdown-preview';
|
||||||
|
@ -54,6 +53,7 @@
|
||||||
@import 'component/status-bar';
|
@import 'component/status-bar';
|
||||||
@import 'component/syntax-highlighter';
|
@import 'component/syntax-highlighter';
|
||||||
@import 'component/table';
|
@import 'component/table';
|
||||||
|
@import 'component/livestream';
|
||||||
@import 'component/tabs';
|
@import 'component/tabs';
|
||||||
@import 'component/tooltip';
|
@import 'component/tooltip';
|
||||||
@import 'component/txo-list';
|
@import 'component/txo-list';
|
||||||
|
|
|
@ -205,3 +205,84 @@
|
||||||
.livestream__publish-intro {
|
.livestream__publish-intro {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table--livestream-data {
|
||||||
|
td:nth-of-type(1) {
|
||||||
|
max-width: 4rem;
|
||||||
|
}
|
||||||
|
td:nth-of-type(2) {
|
||||||
|
min-width: 8.5rem;
|
||||||
|
}
|
||||||
|
td:nth-of-type(3) {
|
||||||
|
width: 4rem;
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
td:nth-of-type(4) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
td:nth-of-type(1) {
|
||||||
|
max-width: 4rem;
|
||||||
|
}
|
||||||
|
td:nth-of-type(2) {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
td:nth-of-type(3) {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
td:nth-of-type(4) {
|
||||||
|
width: 100%;
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream_thumb_container {
|
||||||
|
height: 4rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream___thumb {
|
||||||
|
padding: 0 var(--spacing-xxs);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__data-row {
|
||||||
|
cursor: pointer;
|
||||||
|
.radio {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&:nth-child(n) {
|
||||||
|
&.livestream__data-row--selected {
|
||||||
|
background-color: var(--color-input-toggle-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
td {
|
||||||
|
.radio {
|
||||||
|
label::before {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-input-toggle-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-input-bg-selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,12 +40,28 @@
|
||||||
td {
|
td {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
color: var(--color-text-help);
|
||||||
|
padding-bottom: var(--spacing-xxxs);
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding-left: var(--spacing-xs) !important;
|
||||||
|
padding-right: var(--spacing-xs) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table--fixed {
|
.table--fixed {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,6 +172,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section__actions--align-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.section__actions--no-margin {
|
.section__actions--no-margin {
|
||||||
@extend .section__actions;
|
@extend .section__actions;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
|
@ -42,8 +42,10 @@ export default function apiPublishCallViaWeb(
|
||||||
if (fileField) {
|
if (fileField) {
|
||||||
body.append('file', fileField);
|
body.append('file', fileField);
|
||||||
params.file_path = '__POST_FILE__';
|
params.file_path = '__POST_FILE__';
|
||||||
|
delete params['remote_url'];
|
||||||
} else if (remoteUrl) {
|
} else if (remoteUrl) {
|
||||||
body.append('remote_url', remoteUrl);
|
body.append('remote_url', remoteUrl);
|
||||||
|
delete params['remote_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonPayload = JSON.stringify({
|
const jsonPayload = JSON.stringify({
|
||||||
|
|
Loading…
Reference in a new issue