2021-03-15 10:32:51 -04:00
// @flow
import * as PAGES from 'constants/pages' ;
2021-03-26 01:57:41 -04:00
import * as ICONS from 'constants/icons' ;
2022-07-11 16:12:37 +02:00
import { useHistory } from 'react-router' ;
2021-04-14 11:56:45 -04:00
import I18nMessage from 'component/i18nMessage' ;
2021-03-15 10:32:51 -04:00
import React from 'react' ;
import Page from 'component/page' ;
import Button from 'component/button' ;
import Yrbl from 'component/yrbl' ;
2021-10-17 16:36:14 +08:00
import Lbry from 'lbry' ;
2021-03-15 10:32:51 -04:00
import { toHex } from 'util/hex' ;
import { FormField } from 'component/common/form' ;
2021-03-15 10:58:21 -04:00
import CopyableText from 'component/copyableText' ;
import Card from 'component/common/card' ;
import ClaimList from 'component/claimList' ;
2021-07-27 16:35:22 -04:00
import { LIVESTREAM _RTMP _URL } from 'constants/livestream' ;
2022-03-31 01:06:30 -04:00
import { ENABLE _NO _SOURCE _CLAIMS } from 'config' ;
2022-07-11 16:12:37 +02:00
import classnames from 'classnames' ;
import LivestreamForm from 'component/publish/livestream/livestreamForm' ;
import Icon from 'component/common/icon' ;
import { useIsMobile } from 'effects/use-screensize' ;
import YrblWalletEmpty from 'component/yrblWalletEmpty' ;
2021-03-15 10:32:51 -04:00
type Props = {
2021-11-08 14:27:14 +08:00
hasChannels : boolean ,
2021-03-15 10:32:51 -04:00
fetchingChannels : boolean ,
activeChannelClaim : ? ChannelClaim ,
2021-03-23 20:49:29 -04:00
pendingClaims : Array < Claim > ,
2021-04-14 11:56:45 -04:00
doNewLivestream : ( string ) => void ,
2021-04-22 23:04:11 -04:00
fetchNoSourceClaims : ( string ) => void ,
2022-07-11 16:12:37 +02:00
clearPublish : ( ) => void ,
2021-12-16 15:59:13 -06:00
myLivestreamClaims : Array < StreamClaim > ,
2021-04-22 23:04:11 -04:00
channelId : ? string ,
channelName : ? string ,
2022-03-09 19:05:37 +01:00
user : ? User ,
2022-07-11 16:12:37 +02:00
balance : number ,
editingURI : ? string ,
2021-03-15 10:32:51 -04:00
} ;
export default function LivestreamSetupPage ( props : Props ) {
2021-03-29 19:05:18 -04:00
const LIVESTREAM _CLAIM _POLL _IN _MS = 60000 ;
2021-04-22 23:04:11 -04:00
const {
2021-11-08 14:27:14 +08:00
hasChannels ,
2021-04-22 23:04:11 -04:00
fetchingChannels ,
activeChannelClaim ,
pendingClaims ,
doNewLivestream ,
fetchNoSourceClaims ,
2022-07-11 16:12:37 +02:00
clearPublish ,
2021-04-22 23:04:11 -04:00
myLivestreamClaims ,
channelId ,
channelName ,
2022-03-09 19:05:37 +01:00
user ,
2022-07-11 16:12:37 +02:00
balance ,
editingURI ,
2021-04-22 23:04:11 -04:00
} = props ;
2021-03-15 10:32:51 -04:00
2022-07-11 16:12:37 +02:00
const isMobile = useIsMobile ( ) ;
const {
location : { search } ,
} = useHistory ( ) ;
const urlParams = new URLSearchParams ( search ) ;
const urlTab = urlParams . get ( 't' ) ;
const urlSource = urlParams . get ( 's' ) ;
2021-03-15 10:32:51 -04:00
const [ sigData , setSigData ] = React . useState ( { signature : undefined , signing _ts : undefined } ) ;
2022-03-31 01:06:30 -04:00
const { odysee _live _disabled : liveDisabled } = user || { } ;
2021-03-15 10:32:51 -04:00
2022-03-31 01:06:30 -04:00
const livestreamEnabled = Boolean ( ENABLE _NO _SOURCE _CLAIMS && user && ! liveDisabled ) ;
2022-03-09 19:05:37 +01:00
2022-07-11 16:12:37 +02:00
const [ isClear , setIsClear ] = React . useState ( false ) ;
2021-03-29 19:05:18 -04:00
function createStreamKey ( ) {
2021-04-22 23:04:11 -04:00
if ( ! channelId || ! channelName || ! sigData . signature || ! sigData . signing _ts ) return null ;
return ` ${ channelId } ?d= ${ toHex ( channelName ) } &s= ${ sigData . signature } &t= ${ sigData . signing _ts } ` ;
2021-03-29 19:05:18 -04:00
}
2022-07-11 16:12:37 +02:00
const formTitle = ! editingURI ? _ _ ( 'Go Live' ) : _ _ ( 'Edit Livestream' ) ;
2021-03-29 19:05:18 -04:00
const streamKey = createStreamKey ( ) ;
2021-04-22 23:04:11 -04:00
const pendingLength = pendingClaims . length ;
const totalLivestreamClaims = pendingClaims . concat ( myLivestreamClaims ) ;
2021-03-26 01:57:41 -04:00
const helpText = (
< div className = "section__subtitle" >
< p >
{ _ _ (
2021-04-14 00:06:11 -04:00
` 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. `
) } { ' ' }
{ _ _ (
2021-12-16 15:59:13 -06:00
` Scheduled livestreams will appear at the top of your channel page and for your followers. Regular livestreams will only appear once you are actually live. `
2021-03-26 01:57:41 -04:00
) } { ' ' }
{ _ _ (
2021-03-26 13:11:10 -04:00
` 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. `
2021-03-26 01:57:41 -04:00
) }
< / p >
< p > { _ _ ( ` To ensure the best streaming experience with OBS, open settings -> output ` ) } < / p >
< p > { _ _ ( ` Select advanced mode from the dropdown at the top. ` ) } < / p >
< p > { _ _ ( ` Ensure the following settings are selected under the streaming tab: ` ) } < / p >
< ul >
< li > { _ _ ( ` Bitrate: 1000 to 2500 kbps ` ) } < / li >
2022-04-20 15:48:45 -04:00
< li > { _ _ ( ` Keyframes: 2 ` ) } < / li >
2021-03-26 01:57:41 -04:00
< li > { _ _ ( ` Profile: High ` ) } < / li >
< li > { _ _ ( ` Tune: Zerolatency ` ) } < / li >
< / ul >
2021-03-26 13:11:10 -04:00
< p >
2021-04-14 00:06:11 -04:00
{ _ _ ( ` If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work. ` ) }
2021-03-26 13:11:10 -04:00
< / p >
2021-12-16 15:59:13 -06:00
< p > { _ _ ( ` For streaming from your mobile device, we recommend PRISM Live Studio from the app store. ` ) } < / p >
2021-03-26 13:11:10 -04:00
< p >
2021-06-25 09:19:17 +08:00
{ _ _ (
` After your stream: \ nClick 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). `
) }
2021-03-26 13:11:10 -04:00
< / p >
2021-04-14 00:06:11 -04:00
< p > { _ _ ( ` Click Save, then confirm, and you are done! ` ) } < / p >
2021-03-26 01:57:41 -04:00
< p >
{ _ _ (
2021-04-14 00:06:11 -04:00
` 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. `
2021-03-26 01:57:41 -04:00
) }
< / p >
< / div >
) ;
2021-03-29 19:05:18 -04:00
2022-07-11 16:12:37 +02:00
function createNewLivestream ( ) {
setTab ( 'Publish' ) ;
doNewLivestream ( ` / $ / ${ PAGES . UPLOAD } ` ) ;
}
2021-03-15 10:32:51 -04:00
React . useEffect ( ( ) => {
2021-04-22 23:04:11 -04:00
// ensure we have a channel
if ( channelId && channelName ) {
Lbry . channel _sign ( {
channel _id : channelId ,
hexdata : toHex ( channelName ) ,
} )
. then ( ( data ) => {
setSigData ( data ) ;
2021-03-15 10:32:51 -04:00
} )
2021-04-22 23:04:11 -04:00
. catch ( ( error ) => {
setSigData ( { signature : null , signing _ts : null } ) ;
2021-04-14 11:56:45 -04:00
} ) ;
}
2021-04-22 23:04:11 -04:00
} , [ channelName , channelId , setSigData ] ) ;
2021-04-14 11:56:45 -04:00
React . useEffect ( ( ) => {
let checkClaimsInterval ;
2021-04-22 23:04:11 -04:00
if ( ! channelId ) return ;
2021-04-14 11:56:45 -04:00
2021-03-29 19:05:18 -04:00
if ( ! checkClaimsInterval ) {
2021-04-22 23:04:11 -04:00
fetchNoSourceClaims ( channelId ) ;
checkClaimsInterval = setInterval ( ( ) => fetchNoSourceClaims ( channelId ) , LIVESTREAM _CLAIM _POLL _IN _MS ) ;
2021-03-29 19:05:18 -04:00
}
return ( ) => {
if ( checkClaimsInterval ) {
clearInterval ( checkClaimsInterval ) ;
}
} ;
2021-04-22 23:04:11 -04:00
} , [ channelId , pendingLength , fetchNoSourceClaims ] ) ;
2021-03-15 10:32:51 -04:00
2021-12-16 15:59:13 -06:00
const filterPending = ( claims : Array < StreamClaim > ) => {
return claims . filter ( ( claim ) => {
return ! pendingClaims . some ( ( pending ) => pending . permanent _url === claim . permanent _url ) ;
} ) ;
} ;
const upcomingStreams = filterPending ( myLivestreamClaims ) . filter ( ( claim ) => {
return Number ( claim . value . release _time ) * 1000 > Date . now ( ) ;
} ) ;
const pastStreams = filterPending ( myLivestreamClaims ) . filter ( ( claim ) => {
return Number ( claim . value . release _time ) * 1000 <= Date . now ( ) ;
} ) ;
type HeaderProps = {
title : string ,
hideBtn ? : boolean ,
} ;
const ListHeader = ( props : HeaderProps ) => {
const { title , hideBtn = false } = props ;
return (
< div className = { 'w-full flex items-center justify-between' } >
< span > { title } < / span >
2022-07-11 16:12:37 +02:00
{ ! hideBtn && ! isMobile && (
2021-12-16 15:59:13 -06:00
< Button
button = "primary"
iconRight = { ICONS . ADD }
2022-07-11 16:12:37 +02:00
onClick = { ( ) => createNewLivestream ( ) }
2021-12-16 15:59:13 -06:00
label = { _ _ ( 'Create or Schedule a New Stream' ) }
/ >
) }
< / div >
) ;
} ;
2022-07-11 16:12:37 +02:00
const [ tab , setTab ] = React . useState ( urlTab || 'Publish' ) ;
2022-03-17 22:41:10 +08:00
2022-07-11 16:12:37 +02:00
React . useEffect ( ( ) => {
if ( editingURI ) {
setTab ( 'Publish' ) ;
}
} , [ editingURI ] ) ;
2022-03-31 01:46:09 -04:00
2022-07-11 16:12:37 +02:00
React . useEffect ( ( ) => {
if ( urlTab ) {
setTab ( urlTab ) ;
}
} , [ urlTab ] ) ;
2021-03-15 10:32:51 -04:00
2022-07-11 16:12:37 +02:00
const HeaderMenu = ( e ) => {
return (
< >
< Button
key = { 'Publish' }
iconSize = { 18 }
label = { 'Publish' }
button = "alt"
onClick = { ( ) => {
setTab ( 'Publish' ) ;
} }
disabled = { e . disabled }
className = { classnames ( 'button-toggle' , { 'button-toggle--active' : tab === 'Publish' } ) }
/ >
< Button
key = { 'Setup' }
iconSize = { 18 }
label = { 'Local Setup' }
button = "alt"
onClick = { ( ) => {
setTab ( 'Setup' ) ;
} }
disabled = { e . disabled || e . isEditing }
className = { classnames ( 'button-toggle' , { 'button-toggle--active' : tab === 'Setup' } ) }
/ >
< / >
) ;
} ;
2022-03-09 19:05:37 +01:00
2022-07-11 16:12:37 +02:00
function resetForm ( ) {
clearPublish ( ) ;
setTab ( 'Publish' ) ;
}
return (
< Page className = "uploadPage-wrapper" >
{ balance < 0.01 && < YrblWalletEmpty / > }
< h1 className = "page__title" >
< Icon icon = { ICONS . VIDEO } / >
< label >
{ formTitle }
{ ! isClear && < Button onClick = { ( ) => resetForm ( ) } icon = { ICONS . REFRESH } button = "primary" label = "Clear" / > }
< / label >
< / h1 >
< HeaderMenu disabled = { balance < 0.01 } isEditing = { editingURI } / >
{ tab === 'Setup' && (
< div className = { editingURI ? 'disabled' : '' } >
{ /* livestreaming disabled */ }
{ ! livestreamEnabled && (
< div style = { { marginTop : '11px' } } >
< h2 style = { { marginBottom : '15px' } } >
{ _ _ ( 'This account has livestreaming disabled, please reach out to hello@odysee.com for assistance.' ) }
< / h2 >
2022-03-09 19:05:37 +01:00
< / div >
) }
2022-07-11 16:12:37 +02:00
{ /* show livestreaming frontend */ }
{ livestreamEnabled && (
< div className = "card-stack" >
{ /* no channels yet */ }
{ ! fetchingChannels && ! hasChannels && (
< Yrbl
type = "happy"
title = { _ _ ( "You haven't created a channel yet, let's fix that!" ) }
2022-03-09 19:05:37 +01:00
actions = {
2022-07-11 16:12:37 +02:00
< div className = "section__actions" >
< Button button = "primary" navigate = { ` / $ / ${ PAGES . CHANNEL _NEW } ` } label = { _ _ ( 'Create A Channel' ) } / >
< / div >
2022-03-09 19:05:37 +01:00
}
/ >
) }
2022-07-11 16:12:37 +02:00
{ ! fetchingChannels && channelId && (
2022-03-09 19:05:37 +01:00
< >
2022-07-11 16:12:37 +02:00
< Card
className = { classnames ( 'section card--livestream-key' , {
disabled : ! streamKey || totalLivestreamClaims . length === 0 ,
} ) }
actions = {
< >
< CopyableText
primaryButton
enableInputMask = { ! streamKey || totalLivestreamClaims . length === 0 }
name = "stream-server"
label = { _ _ ( 'Stream server' ) }
copyable = { LIVESTREAM _RTMP _URL }
snackMessage = { _ _ ( 'Copied stream server URL.' ) }
disabled = { ! streamKey || totalLivestreamClaims . length === 0 }
/ >
< CopyableText
primaryButton
enableInputMask
name = "livestream-key"
label = { _ _ ( 'Stream key (can be reused)' ) }
copyable = { ! streamKey || totalLivestreamClaims . length === 0 ? LIVESTREAM _RTMP _URL : streamKey }
snackMessage = { _ _ ( 'Copied stream key.' ) }
/ >
< / >
}
/ >
{ totalLivestreamClaims . length > 0 ? (
2022-03-09 19:05:37 +01:00
< >
2022-07-11 16:12:37 +02:00
{ Boolean ( pendingClaims . length ) && (
< div className = "section card--livestream-past" >
2022-03-09 19:05:37 +01:00
< ClaimList
2022-07-11 16:12:37 +02:00
header = { _ _ ( 'Your pending livestreams uploads' ) }
uris = { pendingClaims . map ( ( claim ) => claim . permanent _url ) }
2022-03-09 19:05:37 +01:00
/ >
< / div >
) }
2022-07-11 16:12:37 +02:00
{ Boolean ( myLivestreamClaims . length ) && (
< >
{ Boolean ( upcomingStreams . length ) && (
< div className = "section" >
< ClaimList
header = { < ListHeader title = { _ _ ( 'Your Scheduled Livestreams' ) } / > }
uris = { upcomingStreams . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
) }
< div className = "section card--livestream-past" >
< ClaimList
header = {
< ListHeader
title = { _ _ ( 'Your Past Livestreams' ) }
hideBtn = { Boolean ( upcomingStreams . length ) }
/ >
}
empty = {
< I18nMessage
tokens = { {
check _again : (
< Button
button = "link"
onClick = { ( ) => fetchNoSourceClaims ( channelId ) }
label = { _ _ ( 'Check again' ) }
/ >
) ,
} }
>
Nothing here yet . % check _again %
< / I18nMessage >
}
uris = { pastStreams . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
< / >
) }
2022-03-09 19:05:37 +01:00
< / >
2022-07-11 16:12:37 +02:00
) : (
< Yrbl
className = "livestream__publish-intro"
title = { _ _ ( 'No livestream publishes found' ) }
subtitle = { _ _ (
'You need to upload your livestream details before you can go live. Please note: Replays must be published manually after your stream via the Update button on the livestream.'
) }
actions = {
< div className = "section__actions" >
< Button
button = "primary"
onClick = { ( ) => createNewLivestream ( ) }
label = { _ _ ( 'Create A Livestream' ) }
/ >
< Button
button = "alt"
onClick = { ( ) => {
fetchNoSourceClaims ( channelId ) ;
} }
label = { _ _ ( 'Check again...' ) }
/ >
< / div >
}
/ >
2022-03-09 19:05:37 +01:00
) }
2022-07-11 16:12:37 +02:00
< Card className = "card--livestream-instructions" title = "Instructions" actions = { helpText } / >
2021-03-15 10:58:21 -04:00
2022-07-11 16:12:37 +02:00
{ /* Debug Stuff */ }
{ streamKey && false && activeChannelClaim && (
< div style = { { marginTop : 'var(--spacing-l)' } } >
< h3 > Debug Info < / h3 >
2021-03-15 10:58:21 -04:00
2022-07-11 16:12:37 +02:00
{ /* Channel ID */ }
< FormField
name = { 'channelId' }
label = { 'Channel ID' }
type = { 'text' }
defaultValue = { activeChannelClaim . claim _id }
readOnly
/ >
2021-03-15 10:58:21 -04:00
2022-07-11 16:12:37 +02:00
{ /* Signature */ }
< FormField
name = { 'signature' }
label = { 'Signature' }
type = { 'text' }
defaultValue = { sigData . signature }
readOnly
/ >
2022-03-09 19:05:37 +01:00
2022-07-11 16:12:37 +02:00
{ /* Signature TS */ }
< FormField
name = { 'signaturets' }
label = { 'Signature Timestamp' }
type = { 'text' }
defaultValue = { sigData . signing _ts }
readOnly
/ >
2022-03-09 19:05:37 +01:00
2022-07-11 16:12:37 +02:00
{ /* Hex Data */ }
< FormField
name = { 'datahex' }
label = { 'Hex Data' }
type = { 'text' }
defaultValue = { toHex ( activeChannelClaim . name ) }
readOnly
/ >
{ /* Channel Public Key */ }
< FormField
name = { 'channelpublickey' }
label = { 'Public Key' }
type = { 'text' }
defaultValue = { activeChannelClaim . value . public _key }
readOnly
/ >
< / div >
) }
< / >
2022-03-09 19:05:37 +01:00
) }
2022-07-11 16:12:37 +02:00
< / div >
2022-03-09 19:05:37 +01:00
) }
< / div >
) }
2022-07-11 16:12:37 +02:00
{ tab === 'Publish' && (
< LivestreamForm setClearStatus = { setIsClear } disabled = { balance < 0.01 } urlSource = { urlSource } / >
) }
2021-03-15 10:32:51 -04:00
< / Page >
) ;
}