2023-11-03 16:05:56 +01:00

580 lines
16 KiB
JavaScript

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 = <Trans>Please select a file to upload.</Trans>;
break;
case 'mimetype':
message = (
<Trans>
The selected file type ({err.actual}) is not allowed. Allowed file types are {err.allowed}
</Trans>
);
break;
case 'size':
message = (
<Trans>
The selected file is too big (<Filesize bytes={err.actual} />
). Only <Filesize bytes={err.allowed} /> are allowed.
</Trans>
);
break;
case 'read':
message = <Trans>There was an error during upload: {err.message}</Trans>;
break;
default:
message = <Trans>Unknown upload error</Trans>;
}
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 (
<React.Fragment>
<Paper xs={12} md={10}>
<PaperHeader title={<Trans>EDIT: Player</Trans>} onAbort={handleAbort} onHelp={handleHelp} />
<Grid container spacing={1} className={classes.gridContainer}>
<Grid item xs={12}>
<Grid container spacing={0} className={classes.playerL1}>
<Grid item xs={12} className={classes.playerL2}>
{$state !== 'connected' ? (
<Grid container direction="column" className={classes.playerL3} justifyContent="center" alignItems="center" spacing={1}>
<Grid item>
<Typography variant="h2">
<Trans>No video</Trans>
</Typography>
</Grid>
</Grid>
) : (
<Player
key={$revision}
type={$player}
source={manifest}
autoplay={$settings.autoplay}
mute={$settings.mute}
poster={poster}
logo={logo}
colors={$settings.color}
statistics={$settings.statistics}
controls
/>
)}
</Grid>
</Grid>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item xs={12}>
<TabsHorizontal value={$tab} onChange={handleChangeTab}>
<Tab className="tab" label={<Trans>Embed</Trans>} value="embed" />
<Tab className="tab" label={<Trans>Logo</Trans>} value="logo" />
<Tab className="tab" label={<Trans>Poster</Trans>} value="poster" />
<Tab className="tab" label={<Trans>Playback</Trans>} value="playback" />
</TabsHorizontal>
<TabPanel value={$tab} index="embed">
<Grid container spacing={2}>
<Grid item xs={12}>
<TextFieldCopy label={<Trans>Player URL</Trans>} value={playerAddress} />
</Grid>
<Grid item xs={12}>
<TextFieldCopy label={<Trans>iframe code</Trans>} value={iframeCode} />
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="colors">
<Grid container spacing={2}>
<Grid item xs={6}>
<TextField
variant="outlined"
fullWidth
id="color-1"
label={<Trans>Seekbar color</Trans>}
value={$settings.color.seekbar}
onChange={handleChange('seekbar', 'color')}
/>
</Grid>
<Grid item xs={6}>
<TextField
variant="outlined"
fullWidth
id="color-2"
label={<Trans>Button color</Trans>}
value={$settings.color.buttons}
onChange={handleChange('buttons', 'color')}
/>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="logo">
<Grid container spacing={2}>
<Grid item xs={12} md={9}>
<TextField
variant="outlined"
fullWidth
id="logo-url"
label={<Trans>Image URL</Trans>}
value={$settings.logo.image}
onChange={handleChange('image', 'logo')}
/>
</Grid>
<Grid item xs={12} md={3}>
<UploadButton
label={<Trans>Upload</Trans>}
acceptTypes={logoImageTypes}
onStart={handleUploadStart}
onError={handleUploadError(<Trans>Uploading the logo failed</Trans>)}
onUpload={handleLogoUpload}
/>
</Grid>
<Grid item xs={12} md={4}>
<Select
variant="outlined"
fullWidth
label={<Trans>Position</Trans>}
value={$settings.logo.position}
onChange={handleChange('position', 'logo')}
>
<MenuItem value="top-left">Top-Left</MenuItem>
<MenuItem value="top-right">Top-Right</MenuItem>
<MenuItem value="bottom-left">Bottom-Left</MenuItem>
<MenuItem value="bottom-right">Bottom-Right</MenuItem>
</Select>
</Grid>
<Grid item xs={12} md={8}>
<TextField
variant="outlined"
fullWidth
id="logo-link"
label={<Trans>Link</Trans>}
value={$settings.logo.link}
onChange={handleChange('link', 'logo')}
/>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="poster">
<Grid container spacing={2}>
<Grid item xs={12} md={9}>
<TextField
variant="outlined"
fullWidth
id="poster-url"
label={<Trans>Poster image URL</Trans>}
value={$settings.poster}
onChange={handleChange('poster')}
/>
</Grid>
<Grid item xs={12} md={3}>
<UploadButton
label={<Trans>Upload</Trans>}
acceptTypes={posterImageTypes}
onStart={handleUploadStart}
onError={handleUploadError(<Trans>Uploading the poster failed</Trans>)}
onUpload={handlePosterUpload}
/>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="statistic">
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
variant="outlined"
fullWidth
id="ga-id"
label={<Trans>Google Analytics ID</Trans>}
value={$settings.gaAccount}
onChange={handleChange('account', 'ga')}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
variant="outlined"
fullWidth
id="ga-name"
label={<Trans>Google Analytics Tracker Name</Trans>}
value={$settings.gaName}
onChange={handleChange('name', 'ga')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Enable nerd statistics</Trans>}
checked={$settings.statistics}
onChange={handleChange('statistics')}
/>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="playback">
<Grid container spacing={2}>
<Grid item xs={12}>
<Checkbox label={<Trans>Autoplay</Trans>} checked={$settings.autoplay} onChange={handleChange('autoplay')} />
<Checkbox label={<Trans>Mute</Trans>} checked={$settings.mute} onChange={handleChange('mute')} />
<Checkbox label={<Trans>Chromecast</Trans>} checked={$settings.chromecast} onChange={handleChange('chromecast')} />
<Checkbox label={<Trans>AirPlay</Trans>} checked={$settings.airplay} onChange={handleChange('airplay')} />
</Grid>
</Grid>
</TabPanel>
</Grid>
</Grid>
<PaperFooter
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleAbort}>
<Trans>Close</Trans>
</Button>
}
buttonsRight={
<React.Fragment>
<Button variant="outlined" color="primary" onClick={handleDone}>
<Trans>Save</Trans>
</Button>
{$settings.logo.image && $tab === 'logo' && (
<Button variant="outlined" color="secondary" onClick={handleLogoReset}>
<Trans>Reset logo</Trans>
</Button>
)}
{$settings.poster && $tab === 'poster' && (
<Button variant="outlined" color="secondary" onClick={handlePosterReset}>
<Trans>Reset poster</Trans>
</Button>
)}
</React.Fragment>
}
/>
</Paper>
<Backdrop open={$saving}>
<CircularProgress color="inherit" />
</Backdrop>
<Dialog
open={$error.open}
title={$error.title}
onClose={hideUploadError}
buttonsRight={
<Button variant="outlined" color="primary" onClick={hideUploadError}>
<Trans>OK</Trans>
</Button>
}
>
<Typography variant="body1">{$error.message}</Typography>
</Dialog>
</React.Fragment>
);
}
Edit.defaultProps = {
restreamer: null,
};