restreamer-ui-v2/src/RestreamerUI.js
2022-11-09 21:20:06 +01:00

562 lines
13 KiB
JavaScript

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 * as M from './utils/metadata';
import Restreamer from './utils/restreamer';
import Router from './Router';
import Views from './views';
import { UI as Version } from './version';
import Changelog from './misc/Changelog';
import SemverGt from 'semver/functions/gt';
import SemverValid from 'semver/functions/valid';
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 [$metadata, setMetadata] = React.useState({});
const [$changelog, setChangelog] = React.useState({
open: false,
current: '',
previous: '',
});
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();
await checkChangelog();
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 checkChangelog = async () => {
let showChangelog = true;
if (restreamer.current.IsConnected() === true) {
let metadata = await restreamer.current.GetMetadata(false);
const channels = await restreamer.current.ListChannels();
let current = Version.replace('restreamer-', '');
let previous = '';
if (SemverValid(current) === null) {
showChangelog = false;
}
if (metadata === null) {
if (channels.length === 1) {
const progress = await restreamer.current.GetIngestProgress(channels[0].channelid);
if (progress.valid === false) {
// assume fresh installation
metadata = M.initMetadata(metadata);
await restreamer.current.SetMetadata({
...metadata,
bundle: {
...metadata.bundle,
version: current,
},
});
return false;
}
}
}
metadata = M.initMetadata(metadata);
if ('version' in metadata.bundle) {
if (SemverValid(metadata.bundle.version) !== null) {
previous = metadata.bundle.version;
}
}
if (showChangelog === true) {
if (SemverValid(previous) === null) {
previous = '';
}
if (previous.length !== 0) {
if (!SemverGt(current, previous)) {
showChangelog = false;
}
}
}
setMetadata({
...$metadata,
...metadata,
});
setChangelog({
...$changelog,
open: showChangelog,
current: current,
previous: previous,
});
}
return showChangelog;
};
const handleCloseChangelog = async () => {
await restreamer.current.SetMetadata({
...$metadata,
bundle: {
...$metadata.bundle,
version: $changelog.current,
},
});
setChangelog({
...$changelog,
open: false,
});
};
const handleLogin = async (username, password) => {
const connected = await restreamer.current.Login(username, password);
await checkChangelog();
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,
},
},
version: 3,
};
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 (
<Backdrop open={true}>
<CircularProgress color="inherit" />
</Backdrop>
);
}
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 = null;
if ($state.initialized === false) {
view = <Views.Initializing />;
} else {
if ($state.valid === false) {
view = <Views.Invalid address={restreamer.current.Address()} />;
} else if ($state.connected === false) {
view = (
<Views.Login
onLogin={handleLogin}
auths={restreamer.current.Auths()}
hasService={$state.service}
address={restreamer.current.Address()}
onAuth0={handleAuth0}
/>
);
} else if ($state.compatibility.compatible === false) {
if ($state.compatibility.core.compatible === false) {
view = <Views.Incompatible type="core" have={$state.compatibility.core.have} want={$state.compatibility.core.want} />;
} else if ($state.compatibility.ffmpeg.compatible === false) {
view = <Views.Incompatible type="ffmpeg" have={$state.compatibility.ffmpeg.have} want={$state.compatibility.ffmpeg.want} />;
}
} else if ($state.password === true) {
view = (
<Views.Password
onReset={handlePasswordReset}
username={restreamer.current.ConfigValue('api.auth.username')}
usernameOverride={restreamer.current.ConfigOverrides('api.auth.username')}
password={restreamer.current.ConfigValue('api.auth.password')}
passwordOverride={restreamer.current.ConfigOverrides('api.auth.password')}
/>
);
} else {
view = <Router restreamer={restreamer.current} />;
resources = handleResources;
}
}
const expand = $state.connected && $state.compatibility.compatible && !$state.password;
return (
<I18n>
<NotifyProvider value={{ Dispatch: notify }}>
<Grid container direction="column" justifyContent="flex-start" alignItems="stretch" spacing={0}>
<Grid className={classes.MainHeader}>
<Header
expand={expand}
showPlayersite={$state.ingest}
showSettings={$state.compatibility.compatible}
hasUpdates={$state.updates}
hasService={$state.service}
onChannel={handleChannelList}
onPlayersite={handlePlayersite}
onSettings={handleSettings}
onLogout={handleLogout}
/>
</Grid>
<Grid item className={classes.MainContent}>
<Grid container className="MainContent-container" justifyContent="center" alignItems="center" spacing={0}>
<Grid item sm={1}></Grid>
<Grid item xs={12} sm={10} className="MainContent-item">
{view}
</Grid>
<Grid item sm={1}></Grid>
</Grid>
</Grid>
</Grid>
<Footer expand={$state.connected} app={app} version={version} name={name} resources={resources} />
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={$snack.open}
autoHideDuration={6000}
onClose={handleCloseSnack}
>
<Alert variant="filled" elevation={6} onClose={handleCloseSnack} severity={$snack.severity}>
{$snack.message}
</Alert>
</Snackbar>
{expand && (
<ChannelList
open={$channelList.open}
channels={$channelList.channels}
channelid={$channelList.channelid}
onClose={handleCloseChannelList}
onClick={handleSelectChannel}
onAdd={handleAddChannel}
onState={handleStateChannel}
/>
)}
{expand && $changelog.open && (
<Changelog open={$changelog.open} onClose={handleCloseChangelog} current={$changelog.current} previous={$changelog.previous} />
)}
</NotifyProvider>
</I18n>
);
}
RestreamerUI.defaultProps = {
address: '',
};