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-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' ;
import { Lbry } from 'lbry-redux' ;
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-03-15 15:32:51 +01:00
type Props = {
channels : Array < ChannelClaim > ,
fetchingChannels : boolean ,
activeChannelClaim : ? ChannelClaim ,
2021-03-24 01:49:29 +01:00
pendingClaims : Array < Claim > ,
2021-03-15 15:32:51 +01:00
} ;
export default function LivestreamSetupPage ( props : Props ) {
2021-03-26 01:46:15 +01:00
const LIVESTREAM _CLAIM _POLL _IN _MS = 60000 ;
2021-03-24 01:49:29 +01:00
const { channels , fetchingChannels , activeChannelClaim , pendingClaims } = props ;
2021-03-15 15:32:51 +01:00
const [ sigData , setSigData ] = React . useState ( { signature : undefined , signing _ts : undefined } ) ;
2021-03-26 06:57:41 +01:00
const [ showHelpTest , setShowHelpTest ] = usePersistedState ( 'livestream-help-seen' , true ) ;
2021-03-26 07:59:36 +01:00
const [ spin , setSpin ] = React . useState ( true ) ;
2021-03-26 01:46:15 +01:00
const [ livestreamClaims , setLivestreamClaims ] = React . useState ( [ ] ) ;
2021-03-15 15:32:51 +01:00
const hasChannels = channels && channels . length > 0 ;
const activeChannelClaimStr = JSON . stringify ( activeChannelClaim ) ;
2021-03-26 01:46:15 +01:00
function createStreamKey ( ) {
if ( ! activeChannelClaim || ! sigData . signature || ! sigData . signing _ts ) return null ;
return ` ${ activeChannelClaim . claim _id } ?d= ${ toHex ( activeChannelClaim . name ) } &s= ${ sigData . signature } &t= ${
sigData . signing _ts
} ` ;
}
const streamKey = createStreamKey ( ) ;
const pendingLiveStreamClaims = pendingClaims
? pendingClaims . filter (
( claim ) =>
// $FlowFixMe
claim . value _type === 'stream' && ! ( claim . value && claim . value . source )
)
: [ ] ;
const pendingLength = pendingLiveStreamClaims . length ;
const totalLivestreamClaims = pendingLiveStreamClaims . concat ( livestreamClaims ) ;
2021-03-26 06:57:41 +01:00
const helpText = (
< div className = "section__subtitle" >
< p >
2021-03-26 18:11:10 +01:00
{ _ _ ( ` Create a Livestream by first submitting your Livestream details and waiting for approval confirmation. ` ) } { ' ' }
2021-03-26 06:57:41 +01:00
{ _ _ (
` The livestream will not be visible on your channel until you are live, but you can share the URL in advance. `
) } { ' ' }
{ _ _ (
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-03-26 23:17:06 +01:00
{ _ _ (
` If using other livestreaming software, make sure the bitrate is below 5000 kbps or the stream will not work. `
) }
2021-03-26 18:11:10 +01:00
< / p >
< p >
{ _ _ (
2021-03-26 23:17:06 +01:00
` 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. `
2021-03-26 18:11:10 +01:00
) }
< / p >
2021-03-26 06:57:41 +01:00
< p >
{ _ _ (
2021-03-26 18:11:10 +01:00
` 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. `
2021-03-26 06:57:41 +01:00
) }
< / p >
< p >
{ _ _ ( ` After your livestream:
2021-03-26 23:17:06 +01:00
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 . ` )}
2021-03-26 06:57:41 +01:00
< / p >
2021-03-26 23:17:06 +01:00
< p > { _ _ ( ` Click Save and you are done! ` ) } < / p >
2021-03-26 06:57:41 +01:00
< / div >
) ;
2021-03-26 01:46:15 +01:00
2021-03-15 15:32:51 +01:00
React . useEffect ( ( ) => {
if ( activeChannelClaimStr ) {
const channelClaim = JSON . parse ( activeChannelClaimStr ) ;
// ensure we have a channel
if ( channelClaim . claim _id ) {
Lbry . channel _sign ( {
channel _id : channelClaim . claim _id ,
hexdata : toHex ( channelClaim . name ) ,
} )
. then ( ( data ) => {
setSigData ( data ) ;
} )
. catch ( ( error ) => {
setSigData ( { signature : null , signing _ts : null } ) ;
} ) ;
}
}
} , [ activeChannelClaimStr , setSigData ] ) ;
React . useEffect ( ( ) => {
2021-03-26 01:46:15 +01:00
let checkClaimsInterval ;
2021-03-15 15:32:51 +01:00
if ( ! activeChannelClaimStr ) return ;
const channelClaim = JSON . parse ( activeChannelClaimStr ) ;
2021-03-26 01:46:15 +01:00
function checkLivestreamClaims ( ) {
Lbry . claim _search ( {
channel _ids : [ channelClaim . claim _id ] ,
has _no _source : true ,
claim _type : [ 'stream' ] ,
2021-03-15 15:32:51 +01:00
} )
2021-03-26 01:46:15 +01:00
. then ( ( res ) => {
if ( res && res . items && res . items . length > 0 ) {
setLivestreamClaims ( res . items . reverse ( ) ) ;
} else {
setLivestreamClaims ( [ ] ) ;
}
setSpin ( false ) ;
} )
. catch ( ( ) => {
setLivestreamClaims ( [ ] ) ;
setSpin ( false ) ;
} ) ;
}
if ( ! checkClaimsInterval ) {
checkLivestreamClaims ( ) ;
checkClaimsInterval = setInterval ( checkLivestreamClaims , LIVESTREAM _CLAIM _POLL _IN _MS ) ;
}
return ( ) => {
if ( checkClaimsInterval ) {
clearInterval ( checkClaimsInterval ) ;
}
} ;
2021-03-26 07:59:36 +01:00
} , [ activeChannelClaimStr , pendingLength , setSpin ] ) ;
2021-03-15 15:32:51 +01:00
return (
< Page >
2021-03-26 01:46:15 +01:00
{ ( fetchingChannels || spin ) && (
2021-03-15 15:32:51 +01:00
< div className = "main--empty" >
< Spinner delayed / >
< / div >
) }
{ ! fetchingChannels && ! hasChannels && (
< Yrbl
type = "happy"
title = { _ _ ( "You haven't created a channel yet, let's fix that!" ) }
actions = {
< div className = "section__actions" >
< Button button = "primary" navigate = { ` / $ / ${ PAGES . CHANNEL _NEW } ` } label = { _ _ ( 'Create A Channel' ) } / >
< / div >
}
/ >
) }
2021-03-26 07:59:36 +01:00
{ ! fetchingChannels && (
< div className = "section__actions--between" >
< ChannelSelector hideAnon / >
< Button button = "link" onClick = { ( ) => setShowHelpTest ( ! showHelpTest ) } label = { _ _ ( 'How does this work?' ) } / >
< / div >
) }
2021-03-15 15:32:51 +01:00
2021-03-26 07:59:36 +01:00
{ spin && ! fetchingChannels && (
< div className = "main--empty" >
< Spinner delayed / >
< / div >
) }
2021-03-15 15:58:21 +01:00
< div className = "card-stack" >
2021-03-26 07:59:36 +01:00
{ ! spin && ! fetchingChannels && activeChannelClaim && (
2021-03-15 15:58:21 +01:00
< >
2021-03-26 06:57:41 +01:00
{ showHelpTest && (
< Card
titleActions = { < Button button = "close" icon = { ICONS . REMOVE } onClick = { ( ) => setShowHelpTest ( false ) } / > }
2021-03-26 08:02:22 +01:00
title = { _ _ ( 'Go Live on Odysee' ) }
subtitle = { _ _ ( ` You're invited to try out our new livestreaming service while in beta! ` ) }
2021-03-26 06:57:41 +01:00
actions = { helpText }
/ >
) }
2021-03-24 01:49:29 +01:00
{ streamKey && totalLivestreamClaims . length > 0 && (
2021-03-15 15:58:21 +01:00
< Card
2021-03-30 05:07:54 +02:00
className = "section"
2021-03-15 15:58:21 +01:00
title = { _ _ ( 'Your stream key' ) }
actions = {
< >
< CopyableText
primaryButton
name = "stream-server"
label = { _ _ ( 'Stream server' ) }
copyable = "rtmp://stream.odysee.com/live"
snackMessage = { _ _ ( 'Copied' ) }
/ >
< CopyableText
primaryButton
name = "livestream-key"
label = { _ _ ( 'Stream key' ) }
copyable = { streamKey }
snackMessage = { _ _ ( 'Copied' ) }
/ >
< / >
}
2021-03-15 15:32:51 +01:00
/ >
2021-03-15 15:58:21 +01:00
) }
2021-03-15 15:32:51 +01:00
2021-03-24 01:49:29 +01:00
{ totalLivestreamClaims . length > 0 ? (
2021-03-30 05:07:54 +02:00
< >
{ Boolean ( pendingLiveStreamClaims . length ) && (
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your pending livestream uploads' ) }
uris = { pendingLiveStreamClaims . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
) }
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your livestream uploads' ) }
uris = { livestreamClaims
. filter ( ( c ) => ! pendingLiveStreamClaims . some ( ( p ) => p . permanent _url === c . permanent _url ) )
. map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
< / >
2021-03-15 15:58:21 +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.' ) }
actions = {
< div className = "section__actions" >
< Button
button = "primary"
navigate = { ` / $ / ${ PAGES . UPLOAD } ?type= ${ PUBLISH _MODES . LIVESTREAM . toLowerCase ( ) } ` }
label = { _ _ ( 'Create A Livestream' ) }
/ >
< / div >
}
2021-03-15 15:32:51 +01:00
/ >
2021-03-15 15:58:21 +01:00
) }
{ /* Debug Stuff */ }
{ streamKey && false && (
< div style = { { marginTop : 'var(--spacing-l)' } } >
< h3 > Debug Info < / h3 >
{ /* Channel ID */ }
< FormField
name = { 'channelId' }
label = { 'Channel ID' }
type = { 'text' }
defaultValue = { activeChannelClaim . claim _id }
readOnly
/ >
{ /* Signature */ }
< FormField
name = { 'signature' }
label = { 'Signature' }
type = { 'text' }
defaultValue = { sigData . signature }
readOnly
/ >
{ /* Signature TS */ }
< FormField
name = { 'signaturets' }
label = { 'Signature Timestamp' }
type = { 'text' }
defaultValue = { sigData . signing _ts }
readOnly
/ >
{ /* Hex Data */ }
< FormField
name = { 'datahex' }
label = { 'Hex Data' }
type = { 'text' }
defaultValue = { toHex ( activeChannelClaim . name ) }
readOnly
/ >
{ /* Channel Public Key */ }
< FormField
name = { 'channelpublickey' }
label = { 'Public Key' }
type = { 'text' }
defaultValue = { activeChannelClaim . value . public _key }
readOnly
/ >
< / div >
) }
< / >
) }
< / div >
2021-03-15 15:32:51 +01:00
< / Page >
) ;
}