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
|
||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
||||
CHANNEL_STAKED_LEVEL_LIVESTREAM=5
|
||||
WEB_PUBLISH_SIZE_LIMIT_GB=4
|
||||
|
||||
# OG
|
||||
OG_TITLE_SUFFIX=| lbry.tv
|
||||
|
|
|
@ -39,6 +39,7 @@ const config = {
|
|||
ENABLE_PREROLL_ADS: process.env.ENABLE_PREROLL_ADS === 'true',
|
||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS: process.env.CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS,
|
||||
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',
|
||||
SHOW_ADS: process.env.SHOW_ADS === 'true',
|
||||
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
|
||||
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 = {
|
||||
channelName: string, // @CryptoCandor,
|
||||
|
|
|
@ -1778,5 +1778,10 @@
|
|||
"Publishing...": "Publishing...",
|
||||
"Collection": "Collection",
|
||||
"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--"
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ type Props = {
|
|||
nag?: Node,
|
||||
smallTitle?: boolean,
|
||||
onClick?: () => void,
|
||||
children?: any, // not sure how this works
|
||||
children?: Node,
|
||||
};
|
||||
|
||||
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 MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
|
@ -114,25 +113,13 @@ function FileActions(props: Props) {
|
|||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.EDIT}
|
||||
label={__('Edit')}
|
||||
navigate={`/$/${PAGES.UPLOAD}${isLivestreamClaim ? `?type=${PUBLISH_MODES.LIVESTREAM}` : ''}`}
|
||||
label={isLivestreamClaim ? __('Update') : __('Edit')}
|
||||
navigate={`/$/${PAGES.UPLOAD}`}
|
||||
onClick={() => {
|
||||
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 && (
|
||||
<Button
|
||||
title={__('Remove from your library')}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { BITWAVE_API } from 'constants/livestream';
|
||||
import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||
import React from 'react';
|
||||
import Card from 'component/common/card';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
|
@ -35,7 +35,7 @@ export default function LivestreamLink(props: Props) {
|
|||
React.useEffect(() => {
|
||||
function fetchIsStreaming() {
|
||||
// $FlowFixMe Bitwave's API can handle garbage
|
||||
fetch(`${BITWAVE_API}/${livestreamChannelId}`)
|
||||
fetch(`${BITWAVE_LIVE_API}/${livestreamChannelId}`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res && res.success && res.data && res.data.live) {
|
||||
|
|
|
@ -5,15 +5,17 @@ import {
|
|||
makeSelectPublishFormValue,
|
||||
doUpdatePublishForm,
|
||||
doClearPublish,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
} from 'lbry-redux';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectFfmpegStatus } from 'redux/selectors/settings';
|
||||
import PublishPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
const select = (state, props) => ({
|
||||
name: makeSelectPublishFormValue('name')(state),
|
||||
title: makeSelectPublishFormValue('title')(state),
|
||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
|
||||
optimize: makeSelectPublishFormValue('optimize')(state),
|
||||
isStillEditing: selectIsStillEditing(state),
|
||||
balance: selectBalance(state),
|
||||
|
@ -22,12 +24,13 @@ const select = state => ({
|
|||
size: makeSelectPublishFormValue('fileSize')(state),
|
||||
duration: makeSelectPublishFormValue('fileDur')(state),
|
||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
const perform = (dispatch) => ({
|
||||
clearPublish: () => dispatch(doClearPublish()),
|
||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||
showToast: message => dispatch(doToast({ message, isError: true })),
|
||||
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||
showToast: (message) => dispatch(doToast({ message, isError: true })),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PublishPage);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { SITE_NAME } from 'config';
|
||||
import { SITE_NAME, WEB_PUBLISH_SIZE_LIMIT_GB } from 'config';
|
||||
import type { Node } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
@ -14,6 +14,11 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||
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 = {
|
||||
uri: ?string,
|
||||
|
@ -35,9 +40,17 @@ type Props = {
|
|||
size: number,
|
||||
duration: number,
|
||||
isVid: boolean,
|
||||
subtitle: string,
|
||||
setPublishMode: (string) => void,
|
||||
setPrevFileText: (string) => void,
|
||||
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) {
|
||||
|
@ -63,29 +76,60 @@ function PublishFile(props: Props) {
|
|||
setPublishMode,
|
||||
setPrevFileText,
|
||||
header,
|
||||
livestreamData,
|
||||
isLivestreamClaim,
|
||||
subtitle,
|
||||
checkLivestreams,
|
||||
channelId,
|
||||
channelSignature,
|
||||
isCheckingLivestreams,
|
||||
setWaitForFile,
|
||||
} = 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 [oversized, setOversized] = useState(false);
|
||||
const [currentFile, setCurrentFile] = useState(null);
|
||||
const [currentFileType, setCurrentFileType] = useState(null);
|
||||
const [optimizeAvail, setOptimizeAvail] = useState(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 = __(
|
||||
'%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 secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||
const fileSelectorModes = [
|
||||
{ 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
|
||||
useEffect(() => {
|
||||
|
@ -93,9 +137,42 @@ function PublishFile(props: Props) {
|
|||
if (currentFileType !== 'text/markdown' && !isStillEditing) {
|
||||
updatePublishForm({ filePath: '' });
|
||||
}
|
||||
} else if (mode === PUBLISH_MODES.LIVESTREAM) {
|
||||
updatePublishForm({ filePath: '' });
|
||||
}
|
||||
}, [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(() => {
|
||||
if (!filePath || filePath === '') {
|
||||
setCurrentFile('');
|
||||
|
@ -121,6 +198,10 @@ function PublishFile(props: Props) {
|
|||
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
||||
}
|
||||
|
||||
function handlePaginateReplays(page) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
|
||||
function getBitrate(size, duration) {
|
||||
const s = Number(size);
|
||||
const d = Number(duration);
|
||||
|
@ -154,7 +235,7 @@ function PublishFile(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function getMessage() {
|
||||
function getUploadMessage() {
|
||||
// @if TARGET='web'
|
||||
if (oversized) {
|
||||
return (
|
||||
|
@ -186,6 +267,11 @@ function PublishFile(props: Props) {
|
|||
}
|
||||
|
||||
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 (
|
||||
<p className="help">
|
||||
{__("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">
|
||||
{__(
|
||||
'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" />
|
||||
</p>
|
||||
|
@ -225,6 +311,27 @@ function PublishFile(props: Props) {
|
|||
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) {
|
||||
const title = event.target.value;
|
||||
// Update title
|
||||
|
@ -247,7 +354,11 @@ function PublishFile(props: Props) {
|
|||
|
||||
// select file, start to select a new one, then cancel
|
||||
if (!file) {
|
||||
updatePublishForm({ filePath: '', name: '' });
|
||||
if (isStillEditing) {
|
||||
updatePublishForm({ filePath: '' });
|
||||
} else {
|
||||
updatePublishForm({ filePath: '', name: '' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -301,7 +412,7 @@ function PublishFile(props: Props) {
|
|||
|
||||
// @if TARGET='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);
|
||||
showToast(__(UPLOAD_SIZE_MESSAGE));
|
||||
updatePublishForm({ filePath: '', name: '' });
|
||||
|
@ -326,7 +437,7 @@ function PublishFile(props: Props) {
|
|||
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;
|
||||
|
||||
return (
|
||||
|
@ -334,7 +445,7 @@ function PublishFile(props: Props) {
|
|||
className={disabled || balance === 0 ? 'card--disabled' : ''}
|
||||
title={
|
||||
<div>
|
||||
{header}
|
||||
{header} {/* display mode buttons from parent */}
|
||||
{publishing && <Spinner type={'small'} />}
|
||||
{inProgress && (
|
||||
<div>
|
||||
|
@ -343,10 +454,10 @@ function PublishFile(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
}
|
||||
subtitle={isStillEditing && __('You are currently editing your upload.')}
|
||||
subtitle={subtitle || (isStillEditing && __('You are currently editing your upload.'))}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<PublishName />
|
||||
<PublishName uri={uri} />
|
||||
<FormField
|
||||
type="text"
|
||||
name="content_title"
|
||||
|
@ -356,28 +467,161 @@ function PublishFile(props: Props) {
|
|||
value={title}
|
||||
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
|
||||
label={__('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')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
{showFileUpload && (
|
||||
<FormField
|
||||
type="checkbox"
|
||||
checked={userOptimize}
|
||||
|
@ -387,7 +631,7 @@ function PublishFile(props: Props) {
|
|||
name="optimize"
|
||||
/>
|
||||
)}
|
||||
{isPublishFile && !ffmpegAvail && (
|
||||
{showFileUpload && !ffmpegAvail && (
|
||||
<p className="help">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
|
@ -398,7 +642,7 @@ function PublishFile(props: Props) {
|
|||
</I18nMessage>
|
||||
</p>
|
||||
)}
|
||||
{isPublishFile && Boolean(size) && ffmpegAvail && optimize && isVid && (
|
||||
{showFileUpload && Boolean(size) && ffmpegAvail && optimize && isVid && (
|
||||
<p className="help">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
|
@ -412,6 +656,16 @@ function PublishFile(props: Props) {
|
|||
</p>
|
||||
)}
|
||||
{/* @endif */}
|
||||
{isPublishPost && (
|
||||
<PostEditor
|
||||
label={__('Post --[noun, markdown post tab button]--')}
|
||||
uri={uri}
|
||||
disabled={disabled}
|
||||
fileMimeType={fileMimeType}
|
||||
setPrevFileText={setPrevFileText}
|
||||
setCurrentFileType={setCurrentFileType}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,11 @@ import {
|
|||
doPrepareEdit,
|
||||
doCheckPublishNameAvailability,
|
||||
SETTINGS,
|
||||
selectMyChannelClaims,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
makeSelectPublishFormValue,
|
||||
} from 'lbry-redux';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import { doPublishDesktop } from 'redux/actions/publish';
|
||||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
||||
import {
|
||||
|
@ -22,27 +26,40 @@ import {
|
|||
selectActiveChannelStakedLevel,
|
||||
} from 'redux/selectors/app';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import PublishPage from './view';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state) => ({
|
||||
...selectPublishFormValues(state),
|
||||
user: selectUser(state),
|
||||
// The winning claim for a short lbry uri
|
||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||
// My previously published claims under this short lbry uri
|
||||
myClaimForUri: selectMyClaimForUri(state),
|
||||
// If I clicked the "edit" button, have I changed the uri?
|
||||
// Need this to make it easier to find the source on previously published content
|
||||
isStillEditing: selectIsStillEditing(state),
|
||||
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||
modal: selectModal(state),
|
||||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
});
|
||||
const select = (state) => {
|
||||
const myClaimForUri = selectMyClaimForUri(state);
|
||||
const permanentUrl = (myClaimForUri && myClaimForUri.permanent_url) || '';
|
||||
const isPostClaim = makeSelectFileRenderModeForUri(permanentUrl)(state) === RENDER_MODES.MARKDOWN;
|
||||
|
||||
return {
|
||||
...selectPublishFormValues(state),
|
||||
user: selectUser(state),
|
||||
// The winning claim for a short lbry uri
|
||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(permanentUrl)(state),
|
||||
isPostClaim,
|
||||
permanentUrl,
|
||||
// My previously published claims under this short lbry uri
|
||||
myClaimForUri,
|
||||
// If I clicked the "edit" button, have I changed the uri?
|
||||
// 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) => ({
|
||||
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 React, { useEffect } from 'react';
|
||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES, Lbry } from 'lbry-redux';
|
||||
import Button from 'component/button';
|
||||
import ChannelSelect from 'component/channelSelector';
|
||||
import classnames from 'classnames';
|
||||
|
@ -27,6 +27,8 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||
import { useHistory } from 'react-router';
|
||||
import Spinner from 'component/spinner';
|
||||
import { toHex } from 'util/hex';
|
||||
import { BITWAVE_REPLAY_API } from 'constants/livestream';
|
||||
|
||||
// @if TARGET='app'
|
||||
import fs from 'fs';
|
||||
|
@ -82,6 +84,10 @@ type Props = {
|
|||
incognito: boolean,
|
||||
user: ?User,
|
||||
activeChannelStakedLevel: number,
|
||||
isLivestreamClaim: boolean,
|
||||
isPostClaim: boolean,
|
||||
permanentUrl: ?string,
|
||||
remoteUrl: ?string,
|
||||
};
|
||||
|
||||
function PublishForm(props: Props) {
|
||||
|
@ -114,28 +120,69 @@ function PublishForm(props: Props) {
|
|||
incognito,
|
||||
user,
|
||||
activeChannelStakedLevel,
|
||||
isLivestreamClaim,
|
||||
isPostClaim,
|
||||
permanentUrl,
|
||||
remoteUrl,
|
||||
} = props;
|
||||
|
||||
const { replace, location } = useHistory();
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const uploadType = urlParams.get('type');
|
||||
const livestreamEnabled =
|
||||
const TYPE_PARAM = 'type';
|
||||
const uploadType = urlParams.get(TYPE_PARAM);
|
||||
const enableLivestream =
|
||||
ENABLE_NO_SOURCE_CLAIMS &&
|
||||
user &&
|
||||
!user.odysee_live_disabled &&
|
||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
|
||||
// $FlowFixMe
|
||||
const MODES = livestreamEnabled
|
||||
? Object.values(PUBLISH_MODES)
|
||||
: Object.values(PUBLISH_MODES).filter((mode) => mode !== PUBLISH_MODES.LIVESTREAM);
|
||||
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
|
||||
if (editingURI) {
|
||||
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 = {
|
||||
[PUBLISH_MODES.FILE]: SIMPLE_SITE ? 'Video' : 'File',
|
||||
[PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--',
|
||||
[PUBLISH_MODES.LIVESTREAM]: 'Livestream --[noun, livestream tab button]--',
|
||||
};
|
||||
// Component state
|
||||
|
||||
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);
|
||||
|
||||
// Used to check if the url name has changed:
|
||||
|
@ -145,20 +192,27 @@ function PublishForm(props: Props) {
|
|||
const [fileEdited, setFileEdited] = React.useState(false);
|
||||
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 fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
|
||||
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
|
||||
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
|
||||
const isInProgress = filePath || editingURI || name || title;
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
|
||||
// Editing content info
|
||||
const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined;
|
||||
const fileMimeType =
|
||||
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
|
||||
? myClaimForUri.value.source.media_type
|
||||
: undefined;
|
||||
const claimChannelId = myClaimForUri && myClaimForUri.signing_channel && myClaimForUri.signing_channel.claim_id;
|
||||
|
||||
const nameEdited = isStillEditing && name !== prevName;
|
||||
|
||||
const waitingForFile = waitForFile && !remoteUrl && !filePath;
|
||||
// If they are editing, they don't need a new file chosen
|
||||
const formValidLessFile =
|
||||
name &&
|
||||
|
@ -173,11 +227,33 @@ function PublishForm(props: Props) {
|
|||
|
||||
const formValid = isOverwritingExistingClaim
|
||||
? false
|
||||
: editingURI && !filePath
|
||||
? isStillEditing && formValidLessFile
|
||||
: editingURI && !filePath // if we're editing we don't need a file
|
||||
? isStillEditing && formValidLessFile && !waitingForFile
|
||||
: formValidLessFile;
|
||||
|
||||
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(() => {
|
||||
if (!modal) {
|
||||
setTimeout(() => {
|
||||
|
@ -186,12 +262,37 @@ function PublishForm(props: Props) {
|
|||
}
|
||||
}, [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;
|
||||
if (publishing) {
|
||||
if (isStillEditing) {
|
||||
submitLabel = __('Saving...');
|
||||
} else if (isLivestream) {
|
||||
} else if (isLivestreamMode) {
|
||||
submitLabel = __('Creating...');
|
||||
} else {
|
||||
submitLabel = __('Uploading...');
|
||||
|
@ -201,7 +302,7 @@ function PublishForm(props: Props) {
|
|||
} else {
|
||||
if (isStillEditing) {
|
||||
submitLabel = __('Save');
|
||||
} else if (isLivestream) {
|
||||
} else if (isLivestreamMode) {
|
||||
submitLabel = __('Create');
|
||||
} else {
|
||||
submitLabel = __('Upload');
|
||||
|
@ -221,7 +322,7 @@ function PublishForm(props: Props) {
|
|||
}
|
||||
}, [thumbnail, resetThumbnailStatus]);
|
||||
|
||||
// Save current name of the editing claim
|
||||
// Save previous name of the editing claim
|
||||
useEffect(() => {
|
||||
if (isStillEditing && (!prevName || !prevName.trim() === '')) {
|
||||
if (name !== prevName) {
|
||||
|
@ -263,17 +364,18 @@ function PublishForm(props: Props) {
|
|||
}
|
||||
}, [name, activeChannelName, resolveUri, updatePublishForm, checkAvailability]);
|
||||
|
||||
// because publish editingUri is channel_short/claim_long and we don't have that, resolve it.
|
||||
useEffect(() => {
|
||||
// because editingURI is lbry://channel_short/claim_long and that particular shape won't map to the claimId yet
|
||||
if (editingURI) {
|
||||
resolveUri(editingURI);
|
||||
}
|
||||
}, [editingURI, resolveUri]);
|
||||
|
||||
// set isMarkdownPost in publish form if so, also update isLivestreamPublish
|
||||
useEffect(() => {
|
||||
updatePublishForm({
|
||||
isMarkdownPost: mode === PUBLISH_MODES.POST,
|
||||
isLivestreamPublish: isLivestream,
|
||||
isLivestreamPublish: mode === PUBLISH_MODES.LIVESTREAM,
|
||||
});
|
||||
}, [mode, updatePublishForm]);
|
||||
|
||||
|
@ -282,14 +384,15 @@ function PublishForm(props: Props) {
|
|||
updatePublishForm({ channel: undefined });
|
||||
|
||||
// Anonymous livestreams aren't supported
|
||||
if (isLivestream) {
|
||||
if (isLivestreamMode) {
|
||||
setMode(PUBLISH_MODES.FILE);
|
||||
}
|
||||
} else if (activeChannelName) {
|
||||
updatePublishForm({ channel: activeChannelName });
|
||||
}
|
||||
}, [activeChannelName, incognito, updatePublishForm]);
|
||||
}, [activeChannelName, incognito, updatePublishForm, isLivestreamMode]);
|
||||
|
||||
// set mode based on urlParams 'type'
|
||||
useEffect(() => {
|
||||
const _uploadType = uploadType && uploadType.toLowerCase();
|
||||
|
||||
|
@ -310,19 +413,24 @@ function PublishForm(props: Props) {
|
|||
return;
|
||||
}
|
||||
// LiveStream publish
|
||||
if (_uploadType === PUBLISH_MODES.LIVESTREAM.toLowerCase() && livestreamEnabled) {
|
||||
setMode(PUBLISH_MODES.LIVESTREAM);
|
||||
if (_uploadType === PUBLISH_MODES.LIVESTREAM.toLowerCase()) {
|
||||
if (enableLivestream) {
|
||||
setMode(PUBLISH_MODES.LIVESTREAM);
|
||||
} else {
|
||||
setMode(PUBLISH_MODES.FILE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to standard file publish
|
||||
setMode(PUBLISH_MODES.FILE);
|
||||
}, [uploadType, livestreamEnabled]);
|
||||
}, [uploadType, enableLivestream]);
|
||||
|
||||
// if we have a type urlparam, update it? necessary?
|
||||
useEffect(() => {
|
||||
if (!uploadType) return;
|
||||
const newParams = new URLSearchParams();
|
||||
newParams.set('type', mode.toLowerCase());
|
||||
newParams.set(TYPE_PARAM, mode.toLowerCase());
|
||||
replace({ search: newParams.toString() });
|
||||
}, [mode, uploadType]);
|
||||
|
||||
|
@ -391,7 +499,7 @@ function PublishForm(props: Props) {
|
|||
}
|
||||
}
|
||||
// Publish file
|
||||
if (mode === PUBLISH_MODES.FILE || isLivestream) {
|
||||
if (mode === PUBLISH_MODES.FILE || isLivestreamMode) {
|
||||
runPublish = true;
|
||||
}
|
||||
|
||||
|
@ -428,19 +536,26 @@ function PublishForm(props: Props) {
|
|||
// Editing claim uri
|
||||
return (
|
||||
<div className="card-stack">
|
||||
<ChannelSelect hideAnon={isLivestream} disabled={disabled} />
|
||||
<ChannelSelect hideAnon={isLivestreamMode} disabled={disabled} />
|
||||
|
||||
<PublishFile
|
||||
uri={uri}
|
||||
uri={permanentUrl}
|
||||
mode={mode}
|
||||
fileMimeType={fileMimeType}
|
||||
disabled={disabled || publishing}
|
||||
inProgress={isInProgress}
|
||||
setPublishMode={setMode}
|
||||
setPrevFileText={setPrevFileText}
|
||||
livestreamData={livestreamData}
|
||||
subtitle={customSubtitle}
|
||||
setWaitForFile={setWaitForFile}
|
||||
isCheckingLivestreams={isCheckingLivestreams}
|
||||
checkLivestreams={fetchLivestreams}
|
||||
channelId={claimChannelId}
|
||||
channelSignature={signedMessage}
|
||||
header={
|
||||
<>
|
||||
{MODES.map((modeName) => (
|
||||
{AVAILABLE_MODES.map((modeName) => (
|
||||
<Button
|
||||
key={String(modeName)}
|
||||
icon={modeName}
|
||||
|
@ -460,7 +575,7 @@ function PublishForm(props: Props) {
|
|||
{!publishing && (
|
||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
|
||||
<Card actions={<SelectThumbnail />} />
|
||||
<Card actions={<SelectThumbnail livestreamdData={livestreamData} />} />
|
||||
<TagsSelect
|
||||
suggestMature={!SIMPLE_SITE}
|
||||
disableAutoFocus
|
||||
|
@ -489,7 +604,7 @@ function PublishForm(props: Props) {
|
|||
/>
|
||||
|
||||
<PublishBid disabled={isStillEditing || formDisabled} />
|
||||
{!isLivestream && <PublishPrice disabled={formDisabled} />}
|
||||
{!isLivestreamMode && <PublishPrice disabled={formDisabled} />}
|
||||
<PublishAdditionalOptions disabled={formDisabled} />
|
||||
</div>
|
||||
)}
|
||||
|
@ -511,7 +626,7 @@ function PublishForm(props: Props) {
|
|||
</div>
|
||||
<p className="help">
|
||||
{!formDisabled && !formValid ? (
|
||||
<PublishFormErrors mode={mode} />
|
||||
<PublishFormErrors mode={mode} waitForFile={waitingForFile} />
|
||||
) : (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
|
|
|
@ -12,14 +12,26 @@ type Props = {
|
|||
filePath: ?string,
|
||||
isStillEditing: boolean,
|
||||
uploadThumbnailStatus: string,
|
||||
waitForFile: boolean,
|
||||
};
|
||||
|
||||
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
|
||||
// If there is an error it will be presented as an inline error as well
|
||||
return (
|
||||
<div className="error__text">
|
||||
{waitForFile && <div>{__('Choose a replay file, or select None')}</div>}
|
||||
{!title && <div>{__('A title is required')}</div>}
|
||||
{!name && <div>{__('A URL is required')}</div>}
|
||||
{!isNameValid(name, false) && INVALID_NAME_ERROR}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
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,
|
||||
selectIsStillEditing,
|
||||
selectMyChannelClaims,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
SETTINGS,
|
||||
} from 'lbry-redux';
|
||||
import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doPublishDesktop } from 'redux/actions/publish';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
|
||||
const select = (state) => ({
|
||||
...selectPublishFormValues(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
|
||||
publishing: makeSelectPublishFormValue('publishing')(state),
|
||||
isStillEditing: selectIsStillEditing(state),
|
||||
ffmpegStatus: selectFfmpegStatus(state),
|
||||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const editingUri = makeSelectPublishFormValue('editingURI')(state);
|
||||
|
||||
return {
|
||||
...selectPublishFormValues(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||
publishSuccess: makeSelectPublishFormValue('publishSuccess')(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) => ({
|
||||
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),
|
||||
|
|
|
@ -43,6 +43,8 @@ type Props = {
|
|||
myChannels: ?Array<ChannelClaim>,
|
||||
publishSuccess: boolean,
|
||||
publishing: boolean,
|
||||
isLivestreamClaim: boolean,
|
||||
remoteFile: string,
|
||||
};
|
||||
|
||||
// class ModalPublishPreview extends React.PureComponent<Props> {
|
||||
|
@ -74,10 +76,14 @@ const ModalPublishPreview = (props: Props) => {
|
|||
publishing,
|
||||
publish,
|
||||
closeModal,
|
||||
isLivestreamClaim,
|
||||
remoteFile,
|
||||
} = props;
|
||||
|
||||
const livestream =
|
||||
(uri && isLivestreamClaim) ||
|
||||
// $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
|
||||
// @if TARGET='web'
|
||||
React.useEffect(() => {
|
||||
|
@ -122,7 +128,11 @@ const ModalPublishPreview = (props: Props) => {
|
|||
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
||||
let modalTitle;
|
||||
if (isStillEditing) {
|
||||
modalTitle = __('Confirm Edit');
|
||||
if (livestream) {
|
||||
modalTitle = __('Confirm Update');
|
||||
} else {
|
||||
modalTitle = __('Confirm Edit');
|
||||
}
|
||||
} else if (livestream) {
|
||||
modalTitle = __('Create Livestream');
|
||||
} else {
|
||||
|
@ -208,6 +218,7 @@ const ModalPublishPreview = (props: Props) => {
|
|||
<table className="table table--condensed table--publish-preview">
|
||||
<tbody>
|
||||
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
|
||||
{livestream && remoteFile && createRow(__('Replay'), __('Remote File Selected'))}
|
||||
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
|
||||
{createRow(__('Title'), title)}
|
||||
{createRow(__('Description'), descriptionValue)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { BITWAVE_API } from 'constants/livestream';
|
||||
import { BITWAVE_LIVE_API } from 'constants/livestream';
|
||||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import LivestreamLayout from 'component/livestreamLayout';
|
||||
|
@ -56,7 +56,7 @@ export default function LivestreamPage(props: Props) {
|
|||
let interval;
|
||||
function checkIsLive() {
|
||||
// $FlowFixMe Bitwave's API can handle garbage
|
||||
fetch(`${BITWAVE_API}/${livestreamChannelId}`)
|
||||
fetch(`${BITWAVE_LIVE_API}/${livestreamChannelId}`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res || !res.data) {
|
||||
|
|
|
@ -55,9 +55,11 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
const helpText = (
|
||||
<div className="section__subtitle">
|
||||
<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.`
|
||||
|
@ -73,25 +75,18 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
<li>{__(`Tune: Zerolatency`)}</li>
|
||||
</ul>
|
||||
<p>
|
||||
{__(
|
||||
`If using other livestreaming software, make sure the bitrate is below 5000 kbps or the stream will not work.`
|
||||
)}
|
||||
{__(`If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.`)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
`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.`
|
||||
)}
|
||||
{__(`After your stream:
|
||||
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>{__(`Click Save, then confirm, and you are done!`)}</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>
|
||||
{__(`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>
|
||||
);
|
||||
|
||||
|
|
|
@ -29,9 +29,10 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
|
|||
const noFileParam = !filePath || filePath === NO_FILE;
|
||||
const state = getState();
|
||||
const editingUri = makeSelectPublishFormValue('editingURI')(state) || '';
|
||||
const remoteUrl = makeSelectPublishFormValue('remoteFileUrl')(state);
|
||||
const claim = makeSelectClaimForUri(editingUri)(state) || {};
|
||||
const hasSourceFile = claim.value && claim.value.source;
|
||||
const redirectToLivestream = noFileParam && !hasSourceFile;
|
||||
const redirectToLivestream = noFileParam && !hasSourceFile && !remoteUrl;
|
||||
|
||||
const publishSuccess = (successResponse, lbryFirstError) => {
|
||||
const state = getState();
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
@import 'component/form-field';
|
||||
@import 'component/header';
|
||||
@import 'component/icon';
|
||||
@import 'component/livestream';
|
||||
@import 'component/main';
|
||||
@import 'component/markdown-editor';
|
||||
@import 'component/markdown-preview';
|
||||
|
@ -54,6 +53,7 @@
|
|||
@import 'component/status-bar';
|
||||
@import 'component/syntax-highlighter';
|
||||
@import 'component/table';
|
||||
@import 'component/livestream';
|
||||
@import 'component/tabs';
|
||||
@import 'component/tooltip';
|
||||
@import 'component/txo-list';
|
||||
|
|
|
@ -205,3 +205,84 @@
|
|||
.livestream__publish-intro {
|
||||
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 {
|
||||
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 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
}
|
||||
|
||||
.table--fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
|
|
@ -172,6 +172,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section__actions--align-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.section__actions--no-margin {
|
||||
@extend .section__actions;
|
||||
margin-top: 0;
|
||||
|
|
|
@ -42,8 +42,10 @@ export default function apiPublishCallViaWeb(
|
|||
if (fileField) {
|
||||
body.append('file', fileField);
|
||||
params.file_path = '__POST_FILE__';
|
||||
delete params['remote_url'];
|
||||
} else if (remoteUrl) {
|
||||
body.append('remote_url', remoteUrl);
|
||||
delete params['remote_url'];
|
||||
}
|
||||
|
||||
const jsonPayload = JSON.stringify({
|
||||
|
|
Loading…
Reference in a new issue