import React from 'react'; import makeStyles from '@mui/styles/makeStyles'; import Alert from '@mui/material/Alert'; import Backdrop from '@mui/material/Backdrop'; import CircularProgress from '@mui/material/CircularProgress'; import Grid from '@mui/material/Grid'; import Snackbar from '@mui/material/Snackbar'; import { NotifyProvider } from './contexts/Notify'; import * as auth0 from './utils/auth0'; import useInterval from './hooks/useInterval'; import ChannelList from './misc/ChannelList'; import Footer from './Footer'; import I18n from './I18n'; import Header from './Header'; import Restreamer from './utils/restreamer'; import Router from './Router'; import Views from './views'; const useStyles = makeStyles((theme) => ({ MainHeader: { height: '132px', }, // todo: one layer MainContent: { height: '100%', '& .MainContent-container': { minHeight: 'calc(100vh - 230px)', }, '& .MainContent-item': { maxWidth: '980px', }, }, })); export default function RestreamerUI(props) { const classes = useStyles(); const [$state, setState] = React.useState({ initialized: false, valid: false, connected: false, compatibility: { compatible: false }, ingest: false, password: false, updates: false, service: false, }); const [$ready, setReady] = React.useState(false); const [$snack, setSnack] = React.useState({ open: false, message: '', severity: 'info', }); const [$channelList, setChannelList] = React.useState({ open: false, channelid: '', channels: [], }); const restreamer = React.useRef(null); React.useEffect(() => { (async () => { await handleMount(); })(); return () => {}; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useInterval(() => { setState({ ...$state, updates: restreamer.current.HasUpdates(), }); }, 1000 * 60); const notify = (severity, type, message) => { setSnack({ ...$snack, open: true, message: message, severity: severity, }); if (severity === 'success') { if (type === 'save:ingest') { setState({ ...$state, ingest: true, }); } } else if (severity === 'error') { if (type === 'network') { setState({ ...$state, initialized: true, valid: false, }); } else if (type === 'auth') { (async () => { await handleLogout(); })(); } } }; const handleMount = async () => { restreamer.current = new Restreamer(props.address); restreamer.current.AddListener((event) => { notify(event.severity, event.type, event.message); }); // Try if there's still an auth0 session if (auth0.init() === true) { if (await auth0.isAuthenticated()) { const token = await auth0.getToken(); await restreamer.current.LoginWithToken(token); } else { const result = await auth0.handleRedirectCallback(); if (result.initialized === true) { if (result.error === true) { notify('error', 'auth0', 'Auth0: ' + result.description); } } } } const valid = await restreamer.current.Validate(); setState({ ...$state, initialized: true, valid: valid, connected: restreamer.current.IsConnected(), compatibility: restreamer.current.Compatibility(), ingest: restreamer.current.HasIngest(), password: restreamer.current.Auths().length === 0 && !restreamer.current.ConfigOverrides('api.auth.enable'), updates: restreamer.current.HasUpdates(), service: restreamer.current.HasService(), }); setReady(true); }; const handleLogin = async (username, password) => { const connected = await restreamer.current.Login(username, password); setState({ ...$state, connected: connected, compatibility: restreamer.current.Compatibility(), ingest: restreamer.current.HasIngest(), }); return connected; }; const handleAuth0 = async () => { const token = await auth0.getToken(); const connected = await restreamer.current.LoginWithToken(token); setState({ ...$state, connected: connected, compatibility: restreamer.current.Compatibility(), }); }; const handleLogout = async () => { setState({ ...$state, initialized: false, connected: false, }); restreamer.current.Logout(); if (await auth0.isAuthenticated()) { await auth0.logout(); } restreamer.current.Reset(); const valid = await restreamer.current.Validate(); setState({ ...$state, initialized: true, valid: valid, connected: restreamer.current.IsConnected(), compatibility: restreamer.current.Compatibility(), ingest: restreamer.current.HasIngest(), }); }; const handlePasswordReset = async (username, loginUsername, password, loginPassword) => { const data = { api: { auth: { enable: true, }, }, }; if (username.length !== 0) { data.api.auth.username = username; } if (password.length !== 0) { data.api.auth.password = password; } const [, err] = await restreamer.current.ConfigSet(data); if (err !== null) { notify('error', 'save:settings', `There was an error resetting the password.`); return 'ERROR'; } const res = await restreamer.current.ConfigReload(); if (res === false) { notify('error', 'restart', `Restarting the application failed.`); return 'ERROR'; } restreamer.current.IgnoreAPIErrors(true); const waitFor = (ms) => { return new Promise((resolve) => { setTimeout(resolve, ms); }); }; let restarted = false; const key = restreamer.current.CreatedAt().toISOString(); for (let retries = 0; retries <= 60; retries++) { await waitFor(1000); const about = await restreamer.current.About(); if (about === null) { // Restarted API not yet available continue; } if (about.created_at?.toISOString() === key) { // API did not yet restart continue; } restarted = true; break; } if (restarted === true) { // After the restart the API requires a login and this means the restart happened await restreamer.current.Validate(); await restreamer.current.Login(loginUsername, loginPassword); window.location.reload(); } else { return 'TIMEOUT'; } return 'OK'; }; const handlePlayersite = () => { document.location.hash = '#/playersite'; }; const handleSettings = () => { document.location.hash = '#/settings'; }; const handleChannelList = () => { const channelid = restreamer.current.GetCurrentChannelID(); const channels = restreamer.current.ListChannels(); setChannelList({ ...$channelList, open: true, channelid: channelid, channels: channels, }); }; const handleSelectChannel = (channelid) => { restreamer.current.SelectChannel(channelid); handleChannelList(); document.location.hash = `#/${channelid}`; }; const handleCloseChannelList = () => { setChannelList({ ...$channelList, open: false, }); }; const handleAddChannel = (name) => { const channelid = restreamer.current.CreateChannel(name); restreamer.current.SelectChannel(channelid); setChannelList({ ...$channelList, open: false, }); document.location.hash = `#/${channelid}/edit/wizard`; }; const handleStateChannel = async (channelids) => { const processes = await restreamer.current.ListProcesses(['state'], channelids); const states = {}; for (let p of processes) { states[p.id] = p.progress.state; } return states; }; const handleCloseSnack = () => { setSnack({ ...$snack, open: false, }); }; const handleResources = async () => { return await restreamer.current.Resources(); }; if ($ready === false) { return ( ); } let version = {}; let app = ''; let name = ''; if ($state.initialized === true) { version = restreamer.current.Version(); app = restreamer.current.App(); name = restreamer.current.Name(); } let resources = () => { return null; }; let view = ; if ($state.valid === false) { view = ; } else if ($state.connected === false) { view = ( ); } else if ($state.compatibility.compatible === false) { if ($state.compatibility.core.compatible === false) { view = ; } else if ($state.compatibility.ffmpeg.compatible === false) { view = ; } } else if ($state.password === true) { view = ( ); } else { view = ; resources = handleResources; } const expand = $state.connected && $state.compatibility.compatible && !$state.password; return (
{view}