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 Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import * as helper from './helper';
import * as M from '../../utils/metadata';
import useInterval from '../../hooks/useInterval';
import BoxText from '../../misc/BoxText';
import DebugModal from '../../misc/modals/Debug';
import Dialog from '../../misc/modals/Dialog';
import EncodingSelect from '../../misc/EncodingSelect';
import FilterSelect from '../../misc/FilterSelect';
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 Process from './Process';
import ProcessControl from '../../misc/controls/Process';
import ProcessModal from '../../misc/modals/Process';
import Services from './Services';
import SourceControl from '../../misc/controls/Source';
import TabContent from './TabContent';
import TabPanel from '../../misc/TabPanel';
import TabsVerticalGrid from '../../misc/TabsVerticalGrid';
const useStyles = makeStyles((theme) => ({
gridContainer: {
marginTop: '0.5em',
marginBottom: '1em',
},
link: {
marginLeft: 10,
wordWrap: 'anywhere',
},
}));
export default function Edit(props) {
const classes = useStyles();
const { i18n } = useLingui();
const { channelid: _channelid, service: _service, index: _index } = useParams();
const id = props.restreamer.GetEgressId(_service, _index);
const navigate = useNavigate();
const notify = React.useContext(NotifyContext);
const [$ready, setReady] = React.useState(false);
const [$settings, setSettings] = React.useState(M.getDefaultEgressMetadata());
const [$sources, setSources] = React.useState([]);
const [$localSources, setLocalSources] = React.useState([]);
const [$tab, setTab] = React.useState('general');
const [$progress, setProgress] = React.useState({});
const [$processDetails, setProcessDetails] = React.useState({
open: false,
data: {
prelude: [],
log: [],
},
});
const processLogTimer = React.useRef();
const [$processDebug, setProcessDebug] = React.useState({
open: false,
data: '',
});
const [$unsavedChanges, setUnsavedChanges] = React.useState(false);
const [$skills, setSkills] = React.useState(null);
const [$metadata, setMetadata] = React.useState({
name: '',
description: '',
license: '',
});
const [$deleteDialog, setDeleteDialog] = React.useState(false);
const [$saving, setSaving] = React.useState(false);
const [$service, setService] = React.useState(null);
const [$serviceSkills, setServiceSkills] = React.useState(null);
const [$invalid, setInvalid] = React.useState('');
useInterval(async () => {
await update(false);
}, 1000);
React.useEffect(() => {
(async () => {
await update(true);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if ($invalid.length !== 0) {
navigate($invalid, { replace: true });
}
}, [navigate, $invalid]);
const update = async (isFirst) => {
const channelid = props.restreamer.SelectChannel(_channelid);
if (channelid === '' || channelid !== _channelid) {
setInvalid('/');
return;
}
const proc = await props.restreamer.GetEgress(_channelid, id, ['state']);
if (proc === null) {
notify.Dispatch('warning', 'notfound:egress:' + _service, i18n._(t`Publication service not found`));
setInvalid(`/${_channelid}`);
return;
}
setProgress(proc.progress);
if (isFirst === true) {
const s = Services.Get(_service);
if (s === null) {
notify.Dispatch('warning', 'notfound:egress:' + _service, i18n._(t`Publication service not found`));
setInvalid(`/${_channelid}/`);
return null;
}
setService(s);
const skills = await props.restreamer.Skills();
setSkills(skills);
const serviceSkills = helper.conflateServiceSkills(s.requires, skills);
setServiceSkills(serviceSkills);
const ingest = await props.restreamer.GetIngestMetadata(_channelid);
setMetadata({
...$metadata,
name: ingest.meta.name,
description: ingest.meta.description,
license: ingest.license,
});
const localSources = [];
localSources.push('hls+' + ingest.control.hls.storage);
if (ingest.control.rtmp.enable) {
localSources.push('rtmp');
}
if (ingest.control.srt.enable) {
localSources.push('srt');
}
setLocalSources(localSources);
const sources = helper.createSourcesFromStreams(ingest.streams);
setSources(sources);
const settings = await props.restreamer.GetEgressMetadata(_channelid, id);
const profiles = settings.profiles;
profiles[0].video = helper.preselectProfile(profiles[0].video, 'video', ingest.streams, serviceSkills.codecs.video, skills);
profiles[0].audio = helper.preselectProfile(profiles[0].audio, 'audio', ingest.streams, serviceSkills.codecs.audio, skills);
settings.profiles = profiles;
settings.streams = M.createOutputStreams(sources, profiles, false);
setSettings(settings);
setReady(true);
}
};
const handleServiceAction = async (action) => {
let state = 'disconnected';
if (action === 'connect') {
await props.restreamer.StartEgress(_channelid, id);
state = 'connecting';
} else if (action === 'disconnect') {
await props.restreamer.StopEgress(_channelid, id);
state = 'disconnecting';
} else if (action === 'reconnect') {
await props.restreamer.StopEgress(_channelid, id);
await props.restreamer.StartEgress(_channelid, id);
state = 'connecting';
}
setProgress({
...$progress,
state: state,
});
};
const handleServiceChange = (outputs, settings) => {
if (!Array.isArray(outputs)) {
outputs = [outputs];
}
setSettings({
...$settings,
outputs: outputs,
settings: settings,
});
setUnsavedChanges(true);
};
const handleEncoding = (type) => (encoder, decoder, automatic) => {
const profiles = $settings.profiles;
profiles[0][type].encoder = encoder;
profiles[0][type].decoder = decoder;
const streams = M.createOutputStreams($sources, profiles, false);
let outputs = $settings.outputs;
if ('createOutputs' in $service) {
outputs = $service.createOutputs($settings.settings, $serviceSkills, $metadata, streams);
}
setSettings({
...$settings,
profiles: profiles,
streams: streams,
outputs: outputs,
});
if (!automatic) {
setUnsavedChanges(true);
}
};
const handleFilter = (type) => (filter, automatic) => {
const profiles = $settings.profiles;
profiles[0][type].filter = filter;
setSettings({
...$settings,
profiles: profiles,
});
if (!automatic) {
setUnsavedChanges(true);
}
};
const handleServiceDone = async () => {
setSaving(true);
const [global, inputs, outputs] = helper.createInputsOutputs($sources, $settings.profiles, $settings.outputs, false);
if (inputs.length === 0 || outputs.length === 0) {
setSaving(false);
notify.Dispatch('error', 'save:egress:' + _service, i18n._(t`The input profile is not complete. Please define a video and audio source.`));
return;
}
const [, err] = await props.restreamer.UpdateEgress(_channelid, id, global, inputs, outputs, $settings.control);
if (err !== null) {
setSaving(false);
notify.Dispatch('error', 'save:egress:' + _service, i18n._(t`Failed to store publication service (${err.message})`));
return;
}
await props.restreamer.SetEgressMetadata(_channelid, id, $settings);
setSaving(false);
notify.Dispatch('success', 'save:egress:' + _service, i18n._(t`The settings for "${$settings.name}" have been saved`));
setUnsavedChanges(false);
};
const handleServiceName = (event) => {
const name = event.target.value;
setSettings({
...$settings,
name: name,
});
setUnsavedChanges(true);
};
const handleControlChange = (what) => (control, automatic) => {
setSettings({
...$settings,
control: {
...$settings.control,
[what]: control,
},
});
if (automatic === false) {
setUnsavedChanges(true);
}
};
const handleServiceDeleteDialog = () => {
setDeleteDialog(!$deleteDialog);
};
const handleServiceDelete = async () => {
setSaving(true);
const res = await props.restreamer.DeleteEgress(_channelid, id);
if (res === false) {
setSaving(false);
notify.Dispatch('warning', 'delete:egress:' + _service, i18n._(t`The publication service "${$settings.name}" could not be deleted`));
return;
}
setSaving(false);
notify.Dispatch('success', 'delete:egress:' + _service, i18n._(t`The publication service "${$settings.name}" has been deleted`));
navigate(`/${_channelid}`);
};
const handleAbort = () => {
navigate(`/${_channelid}/`);
};
const handleChangeTab = (event, value) => {
setTab(value);
};
const handleHelp = (topic) => () => {
if (!topic) {
H('publication-' + $tab);
return;
}
H(topic);
};
const handleProcessDetails = async (event) => {
event.preventDefault();
const open = !$processDetails.open;
let logdata = {
prelude: [],
log: [],
};
if (open === true) {
const data = await props.restreamer.GetEgressLog(_channelid, id);
if (data !== null) {
logdata = data;
}
processLogTimer.current = setInterval(async () => {
await updateProcessDetailsLog();
}, 1000);
} else {
clearInterval(processLogTimer.current);
}
setProcessDetails({
...$processDetails,
open: open,
data: logdata,
});
};
const updateProcessDetailsLog = async () => {
const data = await props.restreamer.GetEgressLog(_channelid, id);
if (data !== null) {
setProcessDetails({
...$processDetails,
open: true,
data: data,
});
}
};
const handleProcessDebug = async (event) => {
event.preventDefault();
const show = !$processDebug.open;
let data = '';
if (show === true) {
const debug = await props.restreamer.GetEgressDebug(_channelid, id);
data = JSON.stringify(debug, null, 2);
}
setProcessDebug({
...$processDebug,
open: show,
data: data,
});
};
if ($ready === false) {
return null;
}
const ServiceControl = $service.component;
const title = $settings.name.length === 0 ? $service.name : $settings.name;
return (
Edit: {title}
}
onAbort={handleAbort}
onHelp={handleHelp()}
/>
General} value="general" />
Source & Encoding} value="encoding" />
Process control} value="process" />
{$service.description}
Service name}
value={$settings.name}
onChange={handleServiceName}
/>
Process
{$unsavedChanges === true && (
You have unsaved changes. Please save them before you can control the service again.
)}
{$unsavedChanges === false && (
Process details
Process report
)}
Source & Encoding
Source
Select RTMP or SRT (if enabled) for less latency.
Encoding
Passthrough (copy) should only be disabled if necessary. Each encoding requires additional CPU/GPU resources.
Video settings
{$settings.profiles[0].video.encoder.coder !== 'copy' && (
)}
Audio settings
{$settings.profiles[0].audio.encoder.coder !== 'copy' && (
)}
Close
}
buttonsRight={
}
/>
Process details}
progress={$progress}
logdata={$processDetails.data}
onHelp={handleHelp('process-details')}
/>
Process report}
data={$processDebug.data}
onHelp={handleHelp('process-report')}
/>
);
}
Edit.defaultProps = {
restreamer: null,
};
Edit.propTypes = {
restreamer: PropTypes.object.isRequired,
};