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' ;
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-04-14 17:56:45 +02:00
import usePrevious from 'effects/use-previous' ;
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-04-14 17:56:45 +02:00
doNewLivestream : ( string ) => void ,
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-14 17:56:45 +02:00
const { channels , fetchingChannels , activeChannelClaim , pendingClaims , doNewLivestream } = 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-30 01:05:18 +02: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-30 01:05:18 +02: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 )
)
: [ ] ;
2021-04-14 17:56:45 +02:00
const [ localPending , setLocalPending ] = React . useState ( [ ] ) ; //
const localPendingStr = JSON . stringify ( localPending ) ;
const pendingLivestreamClaimsStr = JSON . stringify ( pendingLiveStreamClaims ) ;
const prevPendingLiveStreamClaimStr = usePrevious ( pendingLivestreamClaimsStr ) ;
const liveStreamClaimsStr = JSON . stringify ( livestreamClaims ) ;
const prevLiveStreamClaimsStr = JSON . stringify ( liveStreamClaimsStr ) ;
2021-03-30 01:05:18 +02:00
const pendingLength = pendingLiveStreamClaims . length ;
const totalLivestreamClaims = pendingLiveStreamClaims . concat ( livestreamClaims ) ;
2021-04-14 17:56:45 +02:00
const activeChannelId = activeChannelClaim && activeChannelClaim . claim _id ;
const localPendingForChannel = localPending . filter (
( claim ) => claim . signing _channel && claim . signing _channel . claim _id === activeChannelId
) ;
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. `
) } { ' ' }
{ _ _ (
` The livestream will not be visible on your channel page until you are live, but you can share the URL in advance. `
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 >
< p >
2021-04-14 06:06:11 +02:00
{ _ _ ( ` 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 ) . ` )}
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 ( ( ) => {
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 ] ) ;
2021-04-14 17:56:45 +02:00
// The following 2 effects handle the time between pending disappearing and claim_search being able to find it.
// We'll maintain our own pending list:
// add to it when there are new things in pending
// remove items only when our claim_search finds it
2021-03-15 15:32:51 +01:00
React . useEffect ( ( ) => {
2021-04-14 17:56:45 +02:00
// add to localPending when pending changes
const localPending = JSON . parse ( localPendingStr ) ;
const pendingLivestreamClaims = JSON . parse ( pendingLivestreamClaimsStr ) ;
if (
pendingLiveStreamClaims !== prevPendingLiveStreamClaimStr ||
( pendingLivestreamClaims . length && ! localPending . length )
) {
const prevPendingLivestreamClaims = prevPendingLiveStreamClaimStr
? JSON . parse ( prevPendingLiveStreamClaimStr )
: [ ] ;
const pendingClaimIds = pendingLivestreamClaims . map ( ( claim ) => claim . claim _id ) ;
const prevPendingClaimIds = prevPendingLivestreamClaims . map ( ( claim ) => claim . claim _id ) ;
const newLocalPending = [ ] ;
if ( pendingClaimIds . length > prevPendingClaimIds . length ) {
pendingLivestreamClaims . forEach ( ( pendingClaim ) => {
if ( ! localPending . some ( ( lClaim ) => lClaim . claim _id === pendingClaim . claim _id ) ) {
newLocalPending . push ( pendingClaim ) ;
}
} ) ;
setLocalPending ( localPending . concat ( newLocalPending ) ) ;
}
}
} , [ pendingLivestreamClaimsStr , prevPendingLiveStreamClaimStr , localPendingStr , setLocalPending ] ) ;
React . useEffect ( ( ) => {
// remove from localPending when livestreamClaims found
const localPending = JSON . parse ( localPendingStr ) ;
if ( liveStreamClaimsStr !== prevLiveStreamClaimsStr && localPending . length ) {
const livestreamClaims = JSON . parse ( liveStreamClaimsStr ) ;
setLocalPending (
localPending . filter ( ( pending ) => ! livestreamClaims . some ( ( claim ) => claim . claim _id === pending . claim _id ) )
) ;
}
} , [ liveStreamClaimsStr , prevLiveStreamClaimsStr , localPendingStr , setLocalPending ] ) ;
2021-03-15 15:32:51 +01:00
2021-04-14 17:56:45 +02:00
const checkLivestreams = React . useCallback (
function checkLivestreamClaims ( channelClaimId , setLivestreamClaims , setSpin ) {
2021-03-30 01:05:18 +02:00
Lbry . claim _search ( {
2021-04-14 17:56:45 +02:00
channel _ids : [ channelClaimId ] ,
2021-03-30 01:05:18 +02:00
has _no _source : true ,
claim _type : [ 'stream' ] ,
2021-04-14 17:56:45 +02:00
include _purchase _receipt : true ,
2021-03-15 15:32:51 +01:00
} )
2021-03-30 01:05:18 +02:00
. then ( ( res ) => {
if ( res && res . items && res . items . length > 0 ) {
setLivestreamClaims ( res . items . reverse ( ) ) ;
} else {
setLivestreamClaims ( [ ] ) ;
}
setSpin ( false ) ;
} )
. catch ( ( ) => {
setLivestreamClaims ( [ ] ) ;
setSpin ( false ) ;
} ) ;
2021-04-14 17:56:45 +02:00
} ,
[ activeChannelId ]
) ;
React . useEffect ( ( ) => {
let checkClaimsInterval ;
if ( ! activeChannelClaimStr ) return ;
const channelClaim = JSON . parse ( activeChannelClaimStr ) ;
2021-03-30 01:05:18 +02:00
if ( ! checkClaimsInterval ) {
2021-04-14 17:56:45 +02:00
checkLivestreams ( channelClaim . claim _id , setLivestreamClaims , setSpin ) ;
checkClaimsInterval = setInterval (
( ) => checkLivestreams ( channelClaim . claim _id , setLivestreamClaims , setSpin ) ,
LIVESTREAM _CLAIM _POLL _IN _MS
) ;
2021-03-30 01:05:18 +02:00
}
return ( ) => {
if ( checkClaimsInterval ) {
clearInterval ( checkClaimsInterval ) ;
}
} ;
2021-04-14 17:56:45 +02:00
} , [ activeChannelClaimStr , pendingLength , setSpin , checkLivestreams ] ) ;
2021-03-15 15:32:51 +01:00
return (
< Page >
2021-03-30 01:05:18 +02: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-04-14 17:56:45 +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-04-14 17:56:45 +02:00
< >
{ Boolean ( localPendingForChannel . length ) && (
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your pending livestream uploads' ) }
uris = { localPendingForChannel . map ( ( claim ) => claim . permanent _url ) }
/ >
< / div >
) }
{ Boolean ( livestreamClaims . length ) && (
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your livestream uploads' ) }
empty = {
< I18nMessage
tokens = { {
check _again : (
< Button
button = "link"
onClick = { ( ) => checkLivestreams ( activeChannelId , setLivestreamClaims , setSpin ) }
label = { _ _ ( 'Check again' ) }
/ >
) ,
} }
>
Nothing here yet . % check _again %
< / I18nMessage >
}
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' ) }
2021-04-14 17:56:45 +02:00
subtitle = { _ _ (
'You need to upload your livestream details before you can go live. If you already created one in this channel, it should appear soon.'
) }
2021-03-15 15:58:21 +01:00
actions = {
< div className = "section__actions" >
< Button
button = "primary"
2021-04-14 17:56:45 +02:00
onClick = { ( ) =>
doNewLivestream ( ` / $ / ${ PAGES . UPLOAD } ?type= ${ PUBLISH _MODES . LIVESTREAM . toLowerCase ( ) } ` )
}
2021-03-15 15:58:21 +01:00
label = { _ _ ( 'Create A Livestream' ) }
/ >
2021-04-14 17:56:45 +02:00
< Button
button = "alt"
onClick = { ( ) => {
setSpin ( true ) ;
checkLivestreams ( activeChannelId , setLivestreamClaims , setSpin ) ;
} }
label = { _ _ ( 'Check again...' ) }
/ >
2021-03-15 15:58:21 +01:00
< / 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 >
) ;
}