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, +};