2021-03-15 15:32:51 +01:00
// @flow
import * as PAGES from 'constants/pages' ;
2021-03-26 06:57:41 +01:00
import * as ICONS from 'constants/icons' ;
2021-03-15 15:58:21 +01:00
import * as PUBLISH _MODES from 'constants/publish_types' ;
2021-04-14 17:56:45 +02:00
import I18nMessage from 'component/i18nMessage' ;
2021-03-15 15:32:51 +01:00
import React from 'react' ;
import Page from 'component/page' ;
import Spinner from 'component/spinner' ;
import Button from 'component/button' ;
import ChannelSelector from 'component/channelSelector' ;
import Yrbl from 'component/yrbl' ;
2021-10-17 10:36:14 +02:00
import Lbry from 'lbry' ;
2021-03-15 15:32:51 +01:00
import { toHex } from 'util/hex' ;
import { FormField } from 'component/common/form' ;
2021-03-15 15:58:21 +01:00
import CopyableText from 'component/copyableText' ;
import Card from 'component/common/card' ;
import ClaimList from 'component/claimList' ;
2021-03-26 06:57:41 +01:00
import usePersistedState from 'effects/use-persisted-state' ;
2021-07-27 22:35:22 +02:00
import { LIVESTREAM _RTMP _URL } from 'constants/livestream' ;
2022-03-15 03:38:45 +01:00
import { ENABLE _NO _SOURCE _CLAIMS , CHANNEL _STAKED _LEVEL _LIVESTREAM } from 'config' ;
2021-03-15 15:32:51 +01:00
type Props = {
2021-11-08 07:27:14 +01:00
hasChannels : boolean ,
2021-03-15 15:32:51 +01:00
fetchingChannels : boolean ,
activeChannelClaim : ? ChannelClaim ,
2021-03-24 01:49:29 +01:00
pendingClaims : Array < Claim > ,
2021-04-14 17:56:45 +02:00
doNewLivestream : ( string ) => void ,
2021-04-23 05:04:11 +02:00
fetchNoSourceClaims : ( string ) => void ,
2021-12-16 22:59:13 +01:00
myLivestreamClaims : Array < StreamClaim > ,
2021-04-23 05:04:11 +02:00
fetchingLivestreams : boolean ,
channelId : ? string ,
channelName : ? string ,
2022-03-09 19:05:37 +01:00
user : ? User ,
activeChannelStakedLevel : number ,
odyseeMembership : string ,
2021-03-15 15:32:51 +01:00
} ;
export default function LivestreamSetupPage ( props : Props ) {
2021-03-30 01:05:18 +02:00
const LIVESTREAM _CLAIM _POLL _IN _MS = 60000 ;
2021-04-23 05:04:11 +02:00
const {
2021-11-08 07:27:14 +01:00
hasChannels ,
2021-04-23 05:04:11 +02:00
fetchingChannels ,
activeChannelClaim ,
pendingClaims ,
doNewLivestream ,
fetchNoSourceClaims ,
myLivestreamClaims ,
fetchingLivestreams ,
channelId ,
channelName ,
2022-03-09 19:05:37 +01:00
user ,
odyseeMembership ,
activeChannelStakedLevel ,
2021-04-23 05:04:11 +02:00
} = props ;
2021-03-15 15:32:51 +01:00
const [ sigData , setSigData ] = React . useState ( { signature : undefined , signing _ts : undefined } ) ;
2021-04-23 05:04:11 +02:00
const [ showHelp , setShowHelp ] = usePersistedState ( 'livestream-help-seen' , true ) ;
2021-03-15 15:32:51 +01:00
2021-04-23 05:04:11 +02:00
const hasLivestreamClaims = Boolean ( myLivestreamClaims . length || pendingClaims . length ) ;
2021-03-15 15:32:51 +01:00
2022-03-09 19:05:37 +01:00
const hasEnoughLBCToStream = activeChannelStakedLevel >= CHANNEL _STAKED _LEVEL _LIVESTREAM ;
const { odysee _live _disabled : liveDisabled , odysee _live _enabled : liveEnabled } = user || { } ;
const livestreamEnabled = Boolean (
ENABLE _NO _SOURCE _CLAIMS && user && ! liveDisabled && ( liveEnabled || odyseeMembership || hasEnoughLBCToStream )
) ;
let reasonAllowedToStream = '' ;
if ( odyseeMembership ) {
reasonAllowedToStream = 'you purchased Odysee Premium' ;
} else if ( liveEnabled ) {
reasonAllowedToStream = 'your livestreaming was turned on manually' ;
} else if ( hasEnoughLBCToStream ) {
reasonAllowedToStream = 'you have enough staked LBC' ;
}
2021-03-30 01:05:18 +02:00
function createStreamKey ( ) {
2021-04-23 05:04:11 +02: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-30 01:05:18 +02:00
}
const streamKey = createStreamKey ( ) ;
2021-04-23 05:04:11 +02:00
const pendingLength = pendingClaims . length ;
const totalLivestreamClaims = pendingClaims . concat ( myLivestreamClaims ) ;
2021-03-26 06:57:41 +01:00
const helpText = (
< div className = "section__subtitle" >
< p >
{ _ _ (
2021-04-14 06:06:11 +02: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 22:59:13 +01: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 06:57:41 +01:00
) } { ' ' }
{ _ _ (
2021-03-26 18:11:10 +01: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 06:57:41 +01: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 >
< li > { _ _ ( ` Keyframes: 1 ` ) } < / li >
< li > { _ _ ( ` Profile: High ` ) } < / li >
< li > { _ _ ( ` Tune: Zerolatency ` ) } < / li >
< / ul >
2021-03-26 18:11:10 +01:00
< p >
2021-04-14 06:06:11 +02:00
{ _ _ ( ` If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work. ` ) }
2021-03-26 18:11:10 +01:00
< / p >
2021-12-16 22:59:13 +01:00
< p > { _ _ ( ` For streaming from your mobile device, we recommend PRISM Live Studio from the app store. ` ) } < / p >
2021-03-26 18:11:10 +01:00
< p >
2021-06-25 03:19:17 +02: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 18:11:10 +01:00
< / p >
2021-04-14 06:06:11 +02:00
< p > { _ _ ( ` Click Save, then confirm, and you are done! ` ) } < / p >
2021-03-26 06:57:41 +01:00
< p >
{ _ _ (
2021-04-14 06:06:11 +02: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 06:57:41 +01:00
) }
< / p >
< / div >
) ;
2021-03-30 01:05:18 +02:00
2021-03-15 15:32:51 +01:00
React . useEffect ( ( ) => {
2021-04-23 05:04:11 +02: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 15:32:51 +01:00
} )
2021-04-23 05:04:11 +02:00
. catch ( ( error ) => {
setSigData ( { signature : null , signing _ts : null } ) ;
2021-04-14 17:56:45 +02:00
} ) ;
}
2021-04-23 05:04:11 +02:00
} , [ channelName , channelId , setSigData ] ) ;
2021-04-14 17:56:45 +02:00
React . useEffect ( ( ) => {
let checkClaimsInterval ;
2021-04-23 05:04:11 +02:00
if ( ! channelId ) return ;
2021-04-14 17:56:45 +02:00
2021-03-30 01:05:18 +02:00
if ( ! checkClaimsInterval ) {
2021-04-23 05:04:11 +02:00
fetchNoSourceClaims ( channelId ) ;
checkClaimsInterval = setInterval ( ( ) => fetchNoSourceClaims ( channelId ) , LIVESTREAM _CLAIM _POLL _IN _MS ) ;
2021-03-30 01:05:18 +02:00
}
return ( ) => {
if ( checkClaimsInterval ) {
clearInterval ( checkClaimsInterval ) ;
}
} ;
2021-04-23 05:04:11 +02:00
} , [ channelId , pendingLength , fetchNoSourceClaims ] ) ;
2021-03-15 15:32:51 +01:00
2021-12-16 22:59:13 +01: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 >
{ ! hideBtn && (
< Button
button = "primary"
iconRight = { ICONS . ADD }
onClick = { ( ) => doNewLivestream ( ` / $ / ${ PAGES . UPLOAD } ?type= ${ PUBLISH _MODES . LIVESTREAM . toLowerCase ( ) } ` ) }
label = { _ _ ( 'Create or Schedule a New Stream' ) }
/ >
) }
< / div >
) ;
} ;
2021-03-15 15:32:51 +01:00
return (
< Page >
2022-03-09 19:05:37 +01:00
{ /* no livestreaming privs because no premium membership */ }
{ ! livestreamEnabled && ! odyseeMembership && (
2022-03-15 03:38:45 +01:00
< div style = { { marginTop : '11px' } } >
< h2 style = { { marginBottom : '15px' } } >
{ _ _ ( 'To stream on Odysee, please join Odysee Premium or have 50 Credits as support on your channel' ) }
< / h2 >
2022-03-09 19:05:37 +01:00
< Button
button = "primary"
label = { _ _ ( 'Join Odysee Premium' ) }
icon = { ICONS . FINANCE }
navigate = { ` / $ / ${ PAGES . ODYSEE _MEMBERSHIP } ` }
className = "membership_button"
2022-03-15 03:38:45 +01:00
style = { { maxWidth : '238px' } }
2022-03-09 19:05:37 +01:00
/ >
2021-03-15 15:32:51 +01:00
< / div >
) }
2022-03-09 19:05:37 +01:00
{ /* show livestreaming frontend */ }
{ livestreamEnabled && (
< div className = "card-stack" >
{ /* getting channel data */ }
{ fetchingChannels && (
< div className = "main--empty" >
< Spinner delayed / >
2021-03-15 15:32:51 +01:00
< / div >
2022-03-09 19:05:37 +01:00
) }
2021-03-15 15:32:51 +01:00
2022-03-09 19:05:37 +01:00
{ /* no channels yet */ }
{ ! fetchingChannels && ! hasChannels && (
< Yrbl
type = "happy"
title = { _ _ ( "You haven't created a channel yet, let's fix that!" ) }
actions = {
< div className = "section__actions" >
< Button button = "primary" navigate = { ` / $ / ${ PAGES . CHANNEL _NEW } ` } label = { _ _ ( 'Create A Channel' ) } / >
< / div >
2021-12-16 22:59:13 +01:00
}
/ >
2022-03-09 19:05:37 +01:00
) }
{ /* channel selector */ }
{ ! fetchingChannels && (
< >
< div className = "section__actions--between" >
< ChannelSelector hideAnon / >
< / div >
< / >
) }
{ /* getting livestreams */ }
{ fetchingLivestreams && ! fetchingChannels && ! hasLivestreamClaims && (
< div className = "main--empty" >
< Spinner delayed / >
< / div >
) }
{ ! fetchingChannels && channelId && (
< >
2021-03-15 15:58:21 +01:00
< Card
2022-03-09 19:05:37 +01:00
titleActions = {
< Button
button = "close"
icon = { showHelp ? ICONS . UP : ICONS . DOWN }
onClick = { ( ) => setShowHelp ( ! showHelp ) }
/ >
}
title = { _ _ ( 'Go Live on Odysee' ) }
subtitle = {
< > { _ _ ( ` Congratulations, you have access to livestreaming because ${ reasonAllowedToStream } ! ` ) } < / >
2021-03-15 15:58:21 +01:00
}
2022-03-09 19:05:37 +01:00
actions = { showHelp && helpText }
2021-03-15 15:32:51 +01:00
/ >
2022-03-09 19:05:37 +01:00
{ streamKey && totalLivestreamClaims . length > 0 && (
< Card
className = "section"
title = { _ _ ( 'Your stream key' ) }
actions = {
< >
< CopyableText
primaryButton
name = "stream-server"
label = { _ _ ( 'Stream server' ) }
copyable = { LIVESTREAM _RTMP _URL }
snackMessage = { _ _ ( 'Copied stream server URL.' ) }
/ >
< CopyableText
primaryButton
enableInputMask
name = "livestream-key"
label = { _ _ ( 'Stream key (can be reused)' ) }
copyable = { streamKey }
snackMessage = { _ _ ( 'Copied stream key.' ) }
/ >
< / >
}
/ >
) }
{ totalLivestreamClaims . length > 0 ? (
< >
{ Boolean ( pendingClaims . length ) && (
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your pending livestreams uploads' ) }
uris = { pendingClaims . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
) }
{ Boolean ( myLivestreamClaims . length ) && (
< >
{ Boolean ( upcomingStreams . length ) && (
< div className = "section" >
< ClaimList
header = { < ListHeader title = { _ _ ( 'Your Scheduled Livestreams' ) } / > }
uris = { upcomingStreams . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
) }
2021-12-16 22:59:13 +01:00
< div className = "section" >
< ClaimList
2022-03-09 19:05:37 +01:00
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 ) }
2021-12-16 22:59:13 +01:00
/ >
< / div >
2022-03-09 19:05:37 +01: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 = { ( ) =>
doNewLivestream ( ` / $ / ${ PAGES . UPLOAD } ?type= ${ PUBLISH _MODES . LIVESTREAM . toLowerCase ( ) } ` )
2021-12-16 22:59:13 +01:00
}
2022-03-09 19:05:37 +01:00
label = { _ _ ( 'Create A Livestream' ) }
/ >
< Button
button = "alt"
onClick = { ( ) => {
fetchNoSourceClaims ( channelId ) ;
} }
label = { _ _ ( 'Check again...' ) }
2021-12-16 22:59:13 +01:00
/ >
< / div >
2022-03-09 19:05:37 +01:00
}
2021-03-15 15:58:21 +01:00
/ >
2022-03-09 19:05:37 +01:00
) }
2021-03-15 15:58:21 +01:00
2022-03-09 19:05:37 +01:00
{ /* Debug Stuff */ }
{ streamKey && false && activeChannelClaim && (
< div style = { { marginTop : 'var(--spacing-l)' } } >
< h3 > Debug Info < / h3 >
2021-03-15 15:58:21 +01:00
2022-03-09 19:05:37 +01:00
{ /* Channel ID */ }
< FormField
name = { 'channelId' }
label = { 'Channel ID' }
type = { 'text' }
defaultValue = { activeChannelClaim . claim _id }
readOnly
/ >
2021-03-15 15:58:21 +01:00
2022-03-09 19:05:37 +01:00
{ /* Signature */ }
< FormField
name = { 'signature' }
label = { 'Signature' }
type = { 'text' }
defaultValue = { sigData . signature }
readOnly
/ >
2021-03-15 15:58:21 +01:00
2022-03-09 19:05:37 +01:00
{ /* Signature TS */ }
< FormField
name = { 'signaturets' }
label = { 'Signature Timestamp' }
type = { 'text' }
defaultValue = { sigData . signing _ts }
readOnly
/ >
{ /* Hex Data */ }
< FormField
name = { 'datahex' }
label = { 'Hex Data' }
type = { 'text' }
defaultValue = { toHex ( activeChannelClaim . name ) }
readOnly
/ >
{ /* Channel Public Key */ }
< FormField
name = { 'channelpublickey' }
label = { 'Public Key' }
type = { 'text' }
defaultValue = { activeChannelClaim . value . public _key }
readOnly
/ >
< / div >
) }
< / >
) }
< / div >
) }
2021-03-15 15:32:51 +01:00
< / Page >
) ;
}