diff --git a/overlay/src/views/Main/index.js b/overlay/src/views/Main/index.js
new file mode 100644
index 0000000..02ee881
--- /dev/null
+++ b/overlay/src/views/Main/index.js
@@ -0,0 +1,506 @@
+import React from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import { Trans } from '@lingui/macro';
+import makeStyles from '@mui/styles/makeStyles';
+import CircularProgress from '@mui/material/CircularProgress';
+import Grid from '@mui/material/Grid';
+import Link from '@mui/material/Link';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import WarningIcon from '@mui/icons-material/Warning';
+
+import * as M from '../../utils/metadata';
+import { anonymize } from '../../utils/anonymizer';
+import useInterval from '../../hooks/useInterval';
+import ActionButton from '../../misc/ActionButton';
+import CopyButton from '../../misc/CopyButton';
+import DebugModal from '../../misc/modals/Debug';
+import H from '../../utils/help';
+import Paper from '../../misc/Paper';
+import PaperHeader from '../../misc/PaperHeader';
+import Player from '../../misc/Player';
+import Progress from './Progress';
+import Publication from './Publication';
+import ProcessModal from '../../misc/modals/Process';
+import Welcome from '../Welcome';
+import WHEPStatus from './WHEPStatus';
+
+const useStyles = makeStyles((theme) => ({
+ gridContainerL1: {
+ marginBottom: '6em',
+ },
+ gridContainerL2: {
+ paddingTop: '.6em',
+ },
+ link: {
+ marginLeft: 10,
+ },
+ playerL1: {
+ //padding: '4px 1px 4px 8px',
+ paddingTop: 10,
+ paddingLeft: 18
+ },
+ playerL2: {
+ position: 'relative',
+ width: '100%',
+ paddingTop: '56.25%',
+ },
+ playerL3: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ backgroundColor: theme.palette.common.black,
+ },
+ playerWarningIcon: {
+ color: theme.palette.warning.main,
+ fontSize: 'xxx-large',
+ },
+}));
+
+export default function Main(props) {
+ const classes = useStyles();
+ const navigate = useNavigate();
+ const { channelid: _channelid } = useParams();
+ const [$state, setState] = React.useState({
+ ready: false,
+ valid: false,
+ progress: {},
+ state: 'disconnected',
+ onConnect: null,
+ });
+ const [$metadata, setMetadata] = React.useState(M.getDefaultIngestMetadata());
+ const [$processDetails, setProcessDetails] = React.useState({
+ open: false,
+ data: {
+ prelude: [],
+ log: [],
+ },
+ });
+ const processLogTimer = React.useRef();
+ const [$processDebug, setProcessDebug] = React.useState({
+ open: false,
+ data: '',
+ });
+ const [$config, setConfig] = React.useState(null);
+ const [$invalid, setInvalid] = React.useState(false);
+
+ useInterval(async () => {
+ await update();
+ }, 1000);
+
+ React.useEffect(() => {
+ (async () => {
+ await load();
+ await update();
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ React.useEffect(() => {
+ if ($invalid === true) {
+ navigate('/', { replace: true });
+ }
+ }, [navigate, $invalid]);
+
+ const load = async () => {
+ const config = props.restreamer.ConfigActive();
+ setConfig(config);
+
+ const metadata = await props.restreamer.GetIngestMetadata(_channelid);
+ setMetadata({
+ ...$metadata,
+ ...metadata,
+ });
+
+ await update();
+ };
+
+ const update = async () => {
+ const channelid = props.restreamer.SelectChannel(_channelid);
+ if (channelid === '' || channelid !== _channelid) {
+ setInvalid(true);
+ return;
+ }
+
+ const progress = await props.restreamer.GetIngestProgress(_channelid);
+
+ const state = {
+ ...$state,
+ ready: true,
+ valid: progress.valid,
+ progress: progress,
+ state: progress.state,
+ };
+
+ if (state.state === 'connecting') {
+ if (state.onConnect === null) {
+ state.onConnect = async () => {
+ await props.restreamer.StopIngestSnapshot(_channelid);
+ await props.restreamer.StartIngestSnapshot(_channelid);
+ };
+ }
+ } else if (state.state === 'connected') {
+ if (state.onConnect !== null && typeof state.onConnect === 'function') {
+ const onConnect = state.onConnect;
+ setTimeout(async () => {
+ await onConnect();
+ }, 100);
+ state.onConnect = null;
+ }
+ }
+
+ if ($metadata.control.rtmp.enable) {
+ if (!$config.source.network.rtmp.enabled) {
+ state.state = 'error';
+ state.progress.error = 'RTMP server is not enabled, but required.';
+ }
+ } else if ($metadata.control.srt.enable) {
+ if (!$config.source.network.srt.enabled) {
+ state.state = 'error';
+ state.progress.error = 'SRT server is not enabled, but required.';
+ }
+ }
+
+ setState({
+ ...$state,
+ ...state,
+ });
+ };
+
+ const connect = async () => {
+ setState({
+ ...$state,
+ state: 'connecting',
+ onConnect: async () => {
+ await props.restreamer.StopIngestSnapshot(_channelid);
+ await props.restreamer.StartIngestSnapshot(_channelid);
+ },
+ });
+
+ await props.restreamer.StartIngest(_channelid);
+ await props.restreamer.StartIngestSnapshot(_channelid);
+ };
+
+ const disconnect = async () => {
+ setState({
+ ...$state,
+ state: 'disconnecting',
+ });
+
+ await props.restreamer.StopIngestSnapshot(_channelid);
+ await props.restreamer.StopIngest(_channelid);
+
+ await disconnectEgresses();
+ };
+
+ const reconnect = async () => {
+ await disconnect();
+ await connect();
+ };
+
+ const disconnectEgresses = async () => {
+ await props.restreamer.StopAllEgresses(_channelid);
+ };
+
+ const handleProcessDetails = async (event) => {
+ event.preventDefault();
+
+ const open = !$processDetails.open;
+ let logdata = {
+ prelude: [],
+ log: [],
+ };
+
+ if (open === true) {
+ const data = await props.restreamer.GetIngestLog(_channelid);
+ 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.GetIngestLog(_channelid);
+ if (data !== null) {
+ setProcessDetails({
+ ...$processDetails,
+ open: true,
+ data: data,
+ });
+ }
+ };
+
+ const handleProcessDebug = async (event) => {
+ event.preventDefault();
+
+ let data = '';
+
+ if ($processDebug.open === false) {
+ const debug = await props.restreamer.GetIngestDebug(_channelid);
+ data = JSON.stringify(debug, null, 2);
+ }
+
+ setProcessDebug({
+ ...$processDebug,
+ open: !$processDebug.open,
+ data: data,
+ });
+ };
+
+ const handleHelp = (topic) => () => {
+ H(topic);
+ };
+
+ if ($state.ready === false) {
+ return (
+
+
+
+
+
+
+ Retrieving stream data ...
+
+
+
+ );
+ }
+
+ if ($state.valid === false) {
+ return ;
+ }
+
+ const storage = $metadata.control.hls.storage;
+ const channel = props.restreamer.GetChannel(_channelid);
+ const manifest = props.restreamer.GetChannelAddress('hls+' + storage, _channelid);
+ const poster = props.restreamer.GetChannelAddress('snapshot+' + storage, _channelid);
+
+ let title = Main channel;
+ if (channel && channel.name && channel.name.length !== 0) {
+ title = channel.name;
+ }
+
+ return (
+
+
+
+
+ navigate(`/${_channelid}/edit`)} onHelp={handleHelp('main')} />
+
+
+
+
+ {($state.state === 'disconnected' || $state.state === 'disconnecting') && (
+
+
+
+ No video
+
+
+
+ )}
+ {$state.state === 'connecting' && (
+
+
+
+
+
+
+ Connecting ...
+
+
+
+ )}
+ {$state.state === 'error' && (
+
+
+
+
+
+
+ Error: {anonymize($state.progress.error) || 'unknown'}
+
+
+
+
+
+ Please check the{' '}
+
+ process log
+
+
+
+
+ {$state.progress.reconnect !== -1 && (
+
+
+ Reconnecting in {$state.progress.reconnect}s
+
+
+ )}
+ {$state.progress.reconnect === -1 && (
+
+
+ You have to reconnect manually
+
+
+ )}
+
+ )}
+ {$state.state === 'connected' && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ Content URL
+
+
+
+ HLS
+
+ {$metadata.control.rtmp.enable && (
+
+ RTMP
+
+ )}
+ {$metadata.control.srt.enable && (
+
+ SRT
+
+ )}
+ {$metadata.control.webrtc && $metadata.control.webrtc.enable && (
+
+ WHEP
+
+ )}
+
+ Snapshot
+
+
+
+
+ {$metadata.control.webrtc && $metadata.control.webrtc.enable && (
+
+
+
+ WebRTC viewers
+
+
+
+
+ )}
+
+
+
+
+
+ Process details
+
+
+ Process report
+
+
+
+
+
+
+
+
+
+ Process details}
+ progress={$state.progress}
+ logdata={$processDetails.data}
+ onHelp={handleHelp('process-details')}
+ />
+ Process report}
+ data={$processDebug.data}
+ onHelp={handleHelp('process-report')}
+ />
+
+ );
+}
+
+Main.defaultProps = {
+ restreamer: null,
+};