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-07-27 22:35:22 +02:00
import { LIVESTREAM _RTMP _URL } from 'constants/livestream' ;
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-04-23 05:04:11 +02:00
fetchNoSourceClaims : ( string ) => void ,
myLivestreamClaims : Array < Claim > ,
fetchingLivestreams : boolean ,
channelId : ? string ,
channelName : ? 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 {
channels ,
fetchingChannels ,
activeChannelClaim ,
pendingClaims ,
doNewLivestream ,
fetchNoSourceClaims ,
myLivestreamClaims ,
fetchingLivestreams ,
channelId ,
channelName ,
} = 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
const hasChannels = channels && channels . length > 0 ;
2021-04-23 05:04:11 +02:00
const hasLivestreamClaims = Boolean ( myLivestreamClaims . length || pendingClaims . length ) ;
2021-03-15 15:32:51 +01:00
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. `
) } { ' ' }
{ _ _ (
` 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-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
return (
< Page >
2021-04-23 05:04:11 +02:00
{ fetchingChannels && (
2021-03-15 15:32:51 +01:00
< div className = "main--empty" >
2021-04-23 18:57:51 +02:00
< Spinner delayed / >
2021-03-15 15:32:51 +01:00
< / 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 / >
2021-04-23 05:04:11 +02:00
< Button button = "link" onClick = { ( ) => setShowHelp ( ! showHelp ) } label = { _ _ ( 'How does this work?' ) } / >
2021-03-26 07:59:36 +01:00
< / div >
) }
2021-03-15 15:32:51 +01:00
2021-04-23 05:04:11 +02:00
{ fetchingLivestreams && ! fetchingChannels && ! hasLivestreamClaims && (
2021-03-26 07:59:36 +01:00
< div className = "main--empty" >
2021-04-23 18:57:51 +02:00
< Spinner delayed / >
2021-03-26 07:59:36 +01:00
< / div >
) }
2021-03-15 15:58:21 +01:00
< div className = "card-stack" >
2021-04-23 18:57:51 +02:00
{ ! fetchingChannels && channelId && (
2021-03-15 15:58:21 +01:00
< >
2021-04-23 05:04:11 +02:00
{ showHelp && (
2021-03-26 06:57:41 +01:00
< Card
2021-04-23 05:04:11 +02:00
titleActions = { < Button button = "close" icon = { ICONS . REMOVE } onClick = { ( ) => setShowHelp ( 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' ) }
2021-07-27 22:35:22 +02:00
copyable = { LIVESTREAM _RTMP _URL }
2021-09-23 11:50:43 +02:00
snackMessage = { _ _ ( 'Copied stream server URL.' ) }
2021-03-15 15:58:21 +01:00
/ >
2021-09-23 11:57:13 +02:00
< CopyableText
2021-03-15 15:58:21 +01:00
primaryButton
2021-09-23 11:50:43 +02:00
enableInputMask
2021-03-15 15:58:21 +01:00
name = "livestream-key"
label = { _ _ ( 'Stream key' ) }
copyable = { streamKey }
2021-09-23 11:50:43 +02:00
snackMessage = { _ _ ( 'Copied stream key.' ) }
2021-03-15 15:58:21 +01:00
/ >
< / >
}
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
< >
2021-04-23 05:04:11 +02:00
{ Boolean ( pendingClaims . length ) && (
2021-04-14 17:56:45 +02:00
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your pending livestream uploads' ) }
2021-04-23 05:04:11 +02:00
uris = { pendingClaims . map ( ( claim ) => claim . permanent _url ) }
2021-04-14 17:56:45 +02:00
/ >
< / div >
) }
2021-04-23 05:04:11 +02:00
{ Boolean ( myLivestreamClaims . length ) && (
2021-04-14 17:56:45 +02:00
< div className = "section" >
< ClaimList
header = { _ _ ( 'Your livestream uploads' ) }
empty = {
< I18nMessage
tokens = { {
check _again : (
< Button
button = "link"
2021-04-23 05:04:11 +02:00
onClick = { ( ) => fetchNoSourceClaims ( channelId ) }
2021-04-14 17:56:45 +02:00
label = { _ _ ( 'Check again' ) }
/ >
) ,
} }
>
Nothing here yet . % check _again %
< / I18nMessage >
}
2021-04-23 05:04:11 +02:00
uris = { myLivestreamClaims
. filter (
( claim ) => ! pendingClaims . some ( ( pending ) => pending . permanent _url === claim . permanent _url )
)
2021-04-14 17:56:45 +02:00
. 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 = { ( ) => {
2021-04-23 05:04:11 +02:00
fetchNoSourceClaims ( channelId ) ;
2021-04-14 17:56:45 +02:00
} }
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 */ }
2021-04-23 05:04:11 +02:00
{ streamKey && false && activeChannelClaim && (
2021-03-15 15:58:21 +01:00
< 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 >
) ;
}