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')} /> Do you want to delete {title}?} buttonsLeft={ } buttonsRight={ } > Deleting a publication service cannot be reversed. The publication stops immediately. ); } Edit.defaultProps = { restreamer: null, }; Edit.propTypes = { restreamer: PropTypes.object.isRequired, };