2024-11-05 16:40:21 +01:00

711 lines
19 KiB
JavaScript

import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import EditIcon from '@mui/icons-material/Edit';
import Grid from '@mui/material/Grid';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import * as M from '../../utils/metadata';
import sourceThumb from '../../assets/images/livesource.png';
import Dialog from '../../misc/modals/Dialog';
import H from '../../utils/help';
import HLSControl from '../../misc/controls/HLS';
import LicenseControl from '../../misc/controls/License';
import LimitsControl from '../../misc/controls/Limits';
import MetadataControl from '../../misc/controls/Metadata';
import NotifyContext from '../../contexts/Notify';
import Paper from '../../misc/Paper';
import PaperHeader from '../../misc/PaperHeader';
import PaperFooter from '../../misc/PaperFooter';
import PaperThumb from '../../misc/PaperThumb';
import PreviewControl from '../../misc/controls/Preview';
import ProcessControl from '../../misc/controls/Process';
import Profile from './Profile';
import ProfileSummary from './ProfileSummary';
import RTMPControl from '../../misc/controls/RTMP';
import SnapshotControl from '../../misc/controls/Snapshot';
import SRTControl from '../../misc/controls/SRT';
import TabPanel from '../../misc/TabPanel';
import TabsVerticalGrid from '../../misc/TabsVerticalGrid';
const useStyles = makeStyles((theme) => ({
wizardButtonElement: {
display: 'flex',
alignItems: 'left',
},
wizardButton: {
marginLeft: '1em',
padding: ' 0em 2em 0em 2em',
},
link: {
color: theme.palette.common.white,
},
inlineIcon: {
marginBottom: '-.2rem',
},
}));
export default function Edit({ restreamer = null }) {
const classes = useStyles();
const { i18n } = useLingui();
const navigate = useNavigate();
const { channelid: _channelid, tab: _tab } = useParams();
const notify = React.useContext(NotifyContext);
const [$tab, setTab] = React.useState(_tab ? _tab : 'general');
const [$state, setState] = React.useState({
editing: false,
edit: '',
complete: false,
saving: false,
});
const [$data, setData] = React.useState(M.getDefaultIngestMetadata());
const [$skills, setSkills] = React.useState({});
const [$config, setConfig] = React.useState({});
const [$process, setProcess] = React.useState({});
const [$ready, setReady] = React.useState(false);
const [$deleteDialog, setDeleteDialog] = React.useState(false);
const [$editDialog, setEditDialog] = React.useState({
open: false,
target: '',
what: '',
});
const [$invalid, setInvalid] = React.useState(false);
React.useEffect(() => {
(async () => {
await load();
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if ($invalid === true) {
navigate('/', { replace: true });
}
}, [navigate, $invalid]);
const load = async () => {
const channelid = restreamer.SelectChannel(_channelid);
if (channelid === '' || channelid !== _channelid) {
setInvalid(true);
return;
}
const proc = await restreamer.GetIngestProgress(_channelid);
setProcess(proc);
let metadata = await restreamer.GetIngestMetadata(_channelid);
setData({
...$data,
...metadata,
});
const skills = await restreamer.Skills();
setSkills(skills);
const config = await restreamer.ConfigActive();
setConfig(config);
const complete = M.validateProfile(metadata.sources, metadata.profiles[0]);
const state = {
complete: complete,
};
if (metadata.sources.length === 0) {
state.editing = true;
state.edit = 'video';
}
setState({
...$state,
...state,
});
setReady(true);
};
const handleChangeTab = (event, value) => {
setTab(value);
};
const handleSourceEditDialog = (target) => (what) => {
if ($process.order === 'start') {
setEditDialog({
...$editDialog,
open: true,
target: target,
what: what,
});
return;
}
if (target === 'wizard') {
handleWizard();
} else {
handleSourceEdit(what);
}
};
const handleSourceEditDialogAbort = () => {
setEditDialog({
...$editDialog,
open: false,
target: '',
what: '',
});
};
const handleSourceEditDialogDone = async () => {
let stopped = false;
stopped = await restreamer.StopIngest(_channelid);
stopped = stopped ? await restreamer.StopIngestSnapshot(_channelid) : false;
const target = $editDialog.target;
const what = $editDialog.what;
setEditDialog({
...$editDialog,
open: false,
target: '',
what: '',
});
if (stopped === false) {
notify.Dispatch('error', 'edit:ingest', t`Failed to stop process`);
return;
}
if (target === 'wizard') {
handleWizard();
} else {
handleSourceEdit(what);
}
};
const handleSourceEdit = (what) => {
setState({
...$state,
editing: true,
edit: what,
});
};
const handleSkillsRefresh = async () => {
await restreamer.RefreshSkills();
const skills = await restreamer.Skills();
setSkills(skills);
};
const handleSourceStore = async (name, data, onprogress) => {
return await restreamer.UploadData('', name, data, onprogress);
};
const handleSourceProbe = async (inputs) => {
let [res, err] = await restreamer.Probe(_channelid, inputs);
if (err !== null) {
res = {
streams: [],
log: [err.message],
};
}
return res;
};
const handleSourceDone = (sources, profile) => {
const complete = M.validateProfile(sources, profile);
let streams = [];
if (complete === true) {
streams = M.createOutputStreams(sources, [profile]);
}
setData({
...$data,
sources: sources,
profiles: [profile],
streams: streams,
});
setState({
...$state,
editing: false,
complete: complete,
});
};
const handleSourceAbort = () => {
setState({
...$state,
editing: false,
});
};
const handleWizard = () => {
navigate(`/${_channelid}/edit/wizard`);
};
const handleControlChange = (what) => (settings) => {
const control = {
...$data.control,
[what]: settings,
};
setData({
...$data,
control: control,
});
};
const handleMetadataChange = (settings) => {
setData({
...$data,
meta: settings,
});
};
const handleLicenseChange = (license) => {
setData({
...$data,
license: license,
});
};
const handleDone = async () => {
setState({
...$state,
saving: true,
});
const save = async () => {
const sources = $data.sources;
const profiles = $data.profiles;
const control = $data.control;
const [global, inputs, outputs] = M.createInputsOutputs(sources, profiles, true);
if (inputs.length === 0 || outputs.length === 0) {
notify.Dispatch('error', 'save:ingest', i18n._(t`The input profile is not complete. Please define a video and audio source.`));
return false;
}
// Create/update the ingest
let [, err] = await restreamer.UpsertIngest(_channelid, global, inputs, outputs, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest process (${err.message})`));
return false;
}
// Save the metadata
let res = await restreamer.SetIngestMetadata(_channelid, $data);
if (res === false) {
notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to save ingest metadata`));
}
// Create/update the ingest snapshot process
[, err] = await restreamer.UpsertIngestSnapshot(_channelid, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest snapshot process (${err.message})`));
}
// Create/update the ingest preview process
[, err] = await restreamer.UpsertIngestPreview(_channelid, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest preview process (${err.message})`));
}
// Create/update the player
res = await restreamer.UpdatePlayer(_channelid);
if (res === false) {
notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to update the player`));
}
// Create/update the playersite
res = await restreamer.UpdatePlayersite();
if (res === false) {
notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to update the playersite`));
}
return true;
};
const res = await save();
setState({
...$state,
saving: false,
});
if (res === false) {
return;
}
notify.Dispatch('success', 'save:ingest', i18n._(t`Channel "${$data.meta.name}" saved`));
navigate(`/${_channelid}/`);
};
const handleAbort = () => {
navigate(`/${_channelid}/`);
};
const handleChannelDeleteDialog = () => {
setDeleteDialog(!$deleteDialog);
};
const handleChannelDelete = async () => {
setState({
...$state,
saving: true,
});
const res = await restreamer.DeleteChannel(_channelid);
if (res === false) {
setState({
...$state,
saving: false,
});
notify.Dispatch('warning', 'delete:ingest', i18n._(t`The channel "${$data.meta.name}" could not be deleted`));
return;
}
setState({
...$state,
saving: false,
});
notify.Dispatch('success', 'delete:ingest', i18n._(t`The channel "${$data.meta.name}" has been deleted`));
navigate('/');
};
const handleHelp = () => {
H('edit-' + $tab);
};
if ($ready === false) {
return null;
}
let title = <Trans>Main Source</Trans>;
if ($data.meta.name.length !== '') {
title = $data.meta.name;
}
return (
<React.Fragment>
<Paper xs={12} md={10}>
<PaperHeader
title={
<React.Fragment>
<Trans>Edit</Trans>: {title}
</React.Fragment>
}
onAbort={handleAbort}
onHelp={handleHelp}
/>
<Grid container spacing={1}>
<TabsVerticalGrid>
<Tabs orientation="vertical" variant="scrollable" value={$tab} onChange={handleChangeTab}>
<Tab className="tab" label={<Trans>General</Trans>} value="general" />
<Tab className="tab" label={<Trans>Processing &amp; Control</Trans>} value="control" />
<Tab className="tab" label={<Trans>Meta information</Trans>} value="meta" />
<Tab className="tab" label={<Trans>License</Trans>} value="license" />
</Tabs>
<TabPanel value={$tab} index="general" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<PaperThumb image={sourceThumb} title="General" />
</Grid>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>General</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>
Edit the audio and video sources for the live stream. Add a description, and set your desired content license.
</Trans>
</Typography>
</Grid>
{$state.editing === false && (
<Grid item xs={12}>
<div className={classes.wizardButtonElement}>
<Typography>
<Trans>
Use the wizard (<AutoFixHighIcon className={classes.inlineIcon} />) for a quick and easy setup, or edit (
<EditIcon className={classes.inlineIcon} />) the sources directly in custom mode.
</Trans>
</Typography>
</div>
</Grid>
)}
<Grid item xs={12}>
<Divider />
</Grid>
</Grid>
{$state.editing === false ? (
<ProfileSummary
sources={$data.sources}
profile={$data.profiles[0]}
onWizard={handleSourceEditDialog('wizard')}
onEdit={handleSourceEditDialog('extended')}
/>
) : (
<Profile
skills={$skills}
sources={$data.sources}
profile={$data.profiles[0]}
config={$config.source}
startWith={$state.edit}
onProbe={handleSourceProbe}
onRefresh={handleSkillsRefresh}
onDone={handleSourceDone}
onAbort={handleSourceAbort}
onStore={handleSourceStore}
/>
)}
</TabPanel>
<TabPanel value={$tab} index="control" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Processing &amp; Control</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>HLS output</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<HLSControl settings={$data.control.hls} onChange={handleControlChange('hls')} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>RTMP output</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<RTMPControl
settings={$data.control.rtmp}
enabled={$config.source.network.rtmp.enabled}
onChange={handleControlChange('rtmp')}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} display={{ xs: 'block', md: 'none' }}>
<Divider />
</Grid>
<Grid item xs={12} md={6}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>SRT output</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<SRTControl
settings={$data.control.srt}
enabled={$config.source.network.srt.enabled}
onChange={handleControlChange('srt')}
/>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Snapshot</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<SnapshotControl settings={$data.control.snapshot} onChange={handleControlChange('snapshot')} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Player Playback</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<PreviewControl
availableEncoders={$skills.encoders.video}
settings={$data.control.preview}
onChange={handleControlChange('preview')}
/>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Process</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<ProcessControl settings={$data.control.process} onChange={handleControlChange('process')} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Limits</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<LimitsControl settings={$data.control.limits} onChange={handleControlChange('limits')} />
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="meta" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Metadata</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>Briefly describe what the audience will see during the live stream.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<MetadataControl settings={$data.meta} onChange={handleMetadataChange} />
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="license" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>License</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography>
<Trans>
Use your copyright and choose the right image licence. Whether free for all or highly restricted. Briefly discuss
what others are allowed to do with your image.
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<LicenseControl license={$data.license} onChange={handleLicenseChange} />
</Grid>
</Grid>
</TabPanel>
</TabsVerticalGrid>
</Grid>
<PaperFooter
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleAbort}>
<Trans>Close</Trans>
</Button>
}
buttonsRight={
<React.Fragment>
<Button
variant="outlined"
color="primary"
disabled={$state.editing === true || $state.complete === false || $state.saving === true}
onClick={handleDone}
>
<Trans>Save</Trans>
</Button>
<Button variant="outlined" color="secondary" onClick={handleChannelDeleteDialog}>
<Trans>Delete</Trans>
</Button>
</React.Fragment>
}
/>
</Paper>
<Dialog
open={$editDialog.open}
onClose={handleSourceEditDialogAbort}
title={<Trans>Do you want to disconnect "{$data.meta.name}"?</Trans>}
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleSourceEditDialogAbort}>
<Trans>Abort</Trans>
</Button>
}
buttonsRight={
<Button variant="outlined" color="secondary" onClick={handleSourceEditDialogDone}>
<Trans>Disconnect &amp; Continue</Trans>
</Button>
}
>
<Typography>
<Trans>This source cannot be edited while it is in use. To continue, you have to disconnect the source.</Trans>
</Typography>
</Dialog>
<Dialog
open={$deleteDialog}
onClose={handleChannelDeleteDialog}
title={<Trans>Do you want to delete "{$data.meta.name}"?</Trans>}
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleChannelDeleteDialog}>
<Trans>Abort</Trans>
</Button>
}
buttonsRight={
<Button variant="outlined" color="secondary" onClick={handleChannelDelete}>
<Trans>Delete</Trans>
</Button>
}
>
<Typography>
<Trans>The deletion of this channel can not be recovered. All publications of this channel will be removed.</Trans>
</Typography>
</Dialog>
<Backdrop open={$state.saving}>
<CircularProgress color="inherit" />
</Backdrop>
</React.Fragment>
);
}
Edit.propTypes = {
restreamer: PropTypes.object.isRequired,
};