import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useLingui } from '@lingui/react'; import { Trans, t } from '@lingui/macro'; import makeStyles from '@mui/styles/makeStyles'; import Backdrop from '@mui/material/Backdrop'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Grid from '@mui/material/Grid'; import MenuItem from '@mui/material/MenuItem'; import Tab from '@mui/material/Tab'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Checkbox from '../../misc/Checkbox'; import Dialog from '../../misc/modals/Dialog'; import Filesize from '../../misc/Filesize'; import H from '../../utils/help'; import NotifyContext from '../../contexts/Notify'; import Paper from '../../misc/Paper'; import PaperHeader from '../../misc/PaperHeader'; import PaperFooter from '../../misc/PaperFooter'; import Player from '../../misc/Player'; import Select from '../../misc/Select'; import UploadButton from '../../misc/UploadButton'; import TabPanel from '../../misc/TabPanel'; import TabsHorizontal from '../../misc/TabsHorizontal'; import TextFieldCopy from '../../misc/TextFieldCopy'; const useStyles = makeStyles((theme) => ({ gridContainer: { paddingTop: '1em', }, playerL1: { padding: '4px 1px 4px 9px', }, playerL2: { position: 'relative', width: '100%', paddingTop: '56.25%', }, playerL3: { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, backgroundColor: theme.palette.common.black, }, })); const logoImageTypes = [ { mimetype: 'image/gif', extension: 'gif', maxSize: 1 * 1024 * 1024 }, { mimetype: 'image/png', extension: 'png', maxSize: 1 * 1024 * 1024 }, { mimetype: 'image/jpeg', extension: 'jpg', maxSize: 1 * 1024 * 1024 }, { mimetype: 'image/svg+xml', extension: 'svg', maxSize: 1 * 1024 * 1024 }, ]; const posterImageTypes = [ { mimetype: 'image/gif', extension: 'gif', maxSize: 1 * 1024 * 1024 }, { mimetype: 'image/png', extension: 'png', maxSize: 1 * 1024 * 1024 }, { mimetype: 'image/jpeg', extension: 'jpg', maxSize: 1 * 1024 * 1024 }, ]; export default function Edit(props) { const classes = useStyles(); const navigate = useNavigate(); const { channelid: _channelid } = useParams(); const { i18n } = useLingui(); const address = props.restreamer.Address(); const timeout = React.useRef(); const notify = React.useContext(NotifyContext); const [$player] = React.useState('videojs-public'); const [$ready, setReady] = React.useState(false); const [$state, setState] = React.useState('disconnected'); const [$metadata, setMetadata] = React.useState({}); const [$settings, setSettings] = React.useState({}); const [$tab, setTab] = React.useState('embed'); const [$revision, setRevision] = React.useState(0); const [$saving, setSaving] = React.useState(false); const [$error, setError] = React.useState({ open: false, title: '', message: '', }); const [$invalid, setInvalid] = React.useState(''); React.useEffect(() => { (async () => { await mount(); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { if ($invalid.length !== 0) { navigate($invalid, { replace: true }); } }, [navigate, $invalid]); const mount = async () => { const channelid = props.restreamer.SelectChannel(_channelid); if (channelid === '' || channelid !== _channelid) { setInvalid('/'); return; } const proc = await props.restreamer.GetIngest(channelid, ['state', 'metadata']); if (proc === null) { notify.Dispatch('warning', 'notfound:ingest', i18n._(t`Main channel not found`)); setInvalid(`/${_channelid}/`); return; } setMetadata(proc.metadata); setState(proc.progress.state); setSettings(props.restreamer.InitPlayerSettings(proc.metadata.player)); setReady(true); }; const handleChange = (what, section = '') => (event) => { const value = event.target.value; const settings = $settings; if (section === '') { if (['autoplay', 'mute', 'statistics', 'chromecast', 'airplay'].includes(what)) { settings[what] = !settings[what]; } else { settings[what] = value; } } else if (section === 'color') { settings.color[what] = value; } else if (section === 'ga') { settings.ga[what] = value; } else if (section === 'logo') { settings.logo[what] = value; } if (timeout.current !== null) { clearTimeout(timeout.current); timeout.current = null; } timeout.current = setTimeout(() => { timeout.current = null; setRevision($revision + 1); }, 500); setSettings({ ...$settings, ...settings, }); }; const handleLogoUpload = async (data, extension) => { const path = await props.restreamer.UploadLogo(_channelid, data, extension); handleChange( 'image', 'logo', )({ target: { value: path, }, }); setSaving(false); }; const handlePosterUpload = async (data, extension) => { const path = await props.restreamer.UploadPoster(_channelid, data, extension); handleChange('poster')({ target: { value: path, }, }); setSaving(false); }; const handleUploadStart = () => { setSaving(true); }; const handleUploadError = (title) => (err) => { let message = null; switch (err.type) { case 'nofiles': message = Please select a file to upload.; break; case 'mimetype': message = ( The selected file type ({err.actual}) is not allowed. Allowed file types are {err.allowed} ); break; case 'size': message = ( The selected file is too big ( ). Only are allowed. ); break; case 'read': message = There was an error during upload: {err.message}; break; default: message = Unknown upload error; } setSaving(false); showUploadError(title, message); }; const showUploadError = (title, message) => { setError({ ...$error, open: true, title: title, message: message, }); }; const hideUploadError = () => { setError({ ...$error, open: false, }); }; const handleLogoReset = (event) => { // For the cleanup of the core, we need to check the following: // 1. is the image on the core or external? // 2. is the image used somewhere else? // 3. OK via dialog handleChange( 'image', 'logo', )({ target: { value: '', }, }); handleChange( 'position', 'logo', )({ target: { value: 'top-left', }, }); handleChange( 'link', 'logo', )({ target: { value: '', }, }); }; const handlePosterReset = (event) => { // For the cleanup of the core, we need to check the following: // 1. is the image on the core or external? // 2. is the image used somewhere else? // 3. OK via dialog handleChange('poster')({ target: { value: '', }, }); }; const handleDone = async () => { setSaving(true); const metadata = { ...$metadata, player: $settings, }; await props.restreamer.SetIngestMetadata(_channelid, metadata); await props.restreamer.UpdatePlayer(_channelid); setSaving(false); notify.Dispatch('success', 'save:player', i18n._(t`Player settings saved`)); }; const handleChangeTab = (event, value) => { setTab(value); }; const handleAbort = () => { navigate(`/${_channelid}/`); }; const handleHelp = () => { H('player-' + $tab); }; const prepareUrl = (url) => { if (url.length === 0) { return ''; } if (url.match(/^https?:\/\//) === null) { url = address + url; } try { let u = new URL(url); u.searchParams.set('_rscache', Math.random()); return u.href; } catch (e) { return url + '?' + Math.random(); } }; if ($ready === false) { return null; } const storage = $metadata.control.hls.storage; const manifest = props.restreamer.GetChannelAddress('hls+' + storage, _channelid); const poster = $settings.poster ? prepareUrl($settings.poster) : props.restreamer.GetChannelAddress('snapshot+' + storage, _channelid); const playerAddress = props.restreamer.GetPublicAddress('player', _channelid); const iframeCode = props.restreamer.GetPublicIframeCode(_channelid); const logo = { ...$settings.logo, image: prepareUrl($settings.logo.image) }; return ( EDIT: Player} onAbort={handleAbort} onHelp={handleHelp} /> {$state !== 'connected' ? ( No video ) : ( )} Embed} value="embed" /> Logo} value="logo" /> Poster} value="poster" /> Playback} value="playback" /> Player URL} value={playerAddress} /> iframe code} value={iframeCode} /> Seekbar color} value={$settings.color.seekbar} onChange={handleChange('seekbar', 'color')} /> Button color} value={$settings.color.buttons} onChange={handleChange('buttons', 'color')} /> Image URL} value={$settings.logo.image} onChange={handleChange('image', 'logo')} /> Upload} acceptTypes={logoImageTypes} onStart={handleUploadStart} onError={handleUploadError(Uploading the logo failed)} onUpload={handleLogoUpload} /> Link} value={$settings.logo.link} onChange={handleChange('link', 'logo')} /> Poster image URL} value={$settings.poster} onChange={handleChange('poster')} /> Upload} acceptTypes={posterImageTypes} onStart={handleUploadStart} onError={handleUploadError(Uploading the poster failed)} onUpload={handlePosterUpload} /> Google Analytics ID} value={$settings.gaAccount} onChange={handleChange('account', 'ga')} /> Google Analytics Tracker Name} value={$settings.gaName} onChange={handleChange('name', 'ga')} /> Enable nerd statistics} checked={$settings.statistics} onChange={handleChange('statistics')} /> Autoplay} checked={$settings.autoplay} onChange={handleChange('autoplay')} /> Mute} checked={$settings.mute} onChange={handleChange('mute')} /> Chromecast} checked={$settings.chromecast} onChange={handleChange('chromecast')} /> AirPlay} checked={$settings.airplay} onChange={handleChange('airplay')} /> Close } buttonsRight={ {$settings.logo.image && $tab === 'logo' && ( )} {$settings.poster && $tab === 'poster' && ( )} } /> OK } > {$error.message} ); } Edit.defaultProps = { restreamer: null, };