diff --git a/overlay/src/views/Edit/index.js b/overlay/src/views/Edit/index.js
new file mode 100644
index 0000000..02de4ed
--- /dev/null
+++ b/overlay/src/views/Edit/index.js
@@ -0,0 +1,707 @@
+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 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 WHEPControl from '../../misc/controls/WHEP';
+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(props) {
+ 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 = props.restreamer.SelectChannel(_channelid);
+ if (channelid === '' || channelid !== _channelid) {
+ setInvalid(true);
+ return;
+ }
+
+ const proc = await props.restreamer.GetIngestProgress(_channelid);
+ setProcess(proc);
+
+ let metadata = await props.restreamer.GetIngestMetadata(_channelid);
+ setData({
+ ...$data,
+ ...metadata,
+ });
+
+ const skills = await props.restreamer.Skills();
+ setSkills(skills);
+
+ const config = await props.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 props.restreamer.StopIngest(_channelid);
+ stopped = stopped ? await props.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 props.restreamer.RefreshSkills();
+
+ const skills = await props.restreamer.Skills();
+ setSkills(skills);
+ };
+
+ const handleSourceStore = async (name, data) => {
+ return await props.restreamer.UploadData('', name, data);
+ };
+
+ const handleSourceProbe = async (inputs) => {
+ let [res, err] = await props.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 props.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 props.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 props.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 player
+ res = await props.restreamer.UpdatePlayer(_channelid);
+ if (res === false) {
+ notify.Dispatch('warning', 'save:ingest', i18n._(t`Failed to update the player`));
+ }
+
+ // Create/update the playersite
+ res = await props.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 props.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 = Main Source;
+ if ($data.meta.name.length !== '') {
+ title = $data.meta.name;
+ }
+
+ return (
+
+
+
+ Edit: {title}
+
+ }
+ onAbort={handleAbort}
+ onHelp={handleHelp}
+ />
+
+
+
+ General} value="general" />
+ Processing & Control} value="control" />
+ Meta information} value="meta" />
+ License} value="license" />
+
+
+
+
+
+
+
+
+ General
+
+
+
+
+
+ Edit the audio and video sources for the live stream. Add a description, and set your desired content license.
+
+
+
+ {$state.editing === false && (
+
+
+
+
+ Use the wizard () for a quick and easy setup, or edit (
+ ) the sources directly in custom mode.
+
+
+
+
+ )}
+
+
+
+
+ {$state.editing === false ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Processing & Control
+
+
+
+
+
+
+
+ HLS output
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RTMP output
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SRT output
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WebRTC output (WHEP)
+
+
+
+
+
+
+
+
+
+
+ Snapshot
+
+
+
+
+
+
+
+
+
+
+ Process
+
+
+
+
+
+
+
+
+
+
+ Limits
+
+
+
+
+
+
+
+
+
+
+
+ Metadata
+
+
+
+
+ Briefly describe what the audience will see during the live stream.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ License
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+ }
+ buttonsRight={
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+ );
+}
+
+Edit.defaultProps = {
+ restreamer: null,
+};
+
+Edit.propTypes = {
+ restreamer: PropTypes.object.isRequired,
+};