restreamer-ui-v2/src/views/Settings.js

2257 lines
65 KiB
JavaScript

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 Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import settingsImage from '../assets/images/settings.png';
import BoxText from '../misc/BoxText';
import BoxTextarea from '../misc/BoxTextarea';
import Checkbox from '../misc/Checkbox';
import Dialog from '../misc/modals/Dialog';
import Env from '../misc/Env';
import H from '../utils/help';
import NotifyContext from '../contexts/Notify';
import Paper from '../misc/Paper';
import PaperHeader from '../misc/PaperHeader';
import PaperContent from '../misc/PaperContent';
import PaperFooter from '../misc/PaperFooter';
import PaperThumb from '../misc/PaperThumb';
import Password from '../misc/Password';
import Select from '../misc/Select';
import TabPanel from '../misc/TabPanel';
import TabsVerticalGrid from '../misc/TabsVerticalGrid';
import Textarea from '../misc/Textarea';
import TextField from '../misc/TextField';
import useInterval from '../hooks/useInterval';
const useStyles = makeStyles((theme) => ({
inlineEnv: {
float: 'right',
},
}));
const configValues = {
update_check: {
tab: 'general',
set: (config, value) => {
config.update_check = !config.update_check;
},
unset: (config) => {
delete config.update_check;
},
validate: (config) => {
return null;
},
},
id: {
tab: 'service',
set: (config, value) => {
config.id = value;
},
unset: (config) => {
delete config.id;
},
validate: (config) => {
return null;
},
},
name: {
tab: 'service',
set: (config, value) => {
config.name = value;
},
unset: (config) => {
delete config.name;
},
validate: (config) => {
return null;
},
},
'log.level': {
tab: 'logging',
set: (config, value) => {
config.log.level = value;
},
unset: (config) => {
delete config.log.level;
},
validate: (config) => {
return null;
},
},
'log.max_lines': {
tab: 'logging',
set: (config, value) => {
config.log.max_lines = value;
},
unset: (config) => {
delete config.log.max_lines;
},
validate: (config) => {
return null;
},
},
address: {
tab: 'network',
set: (config, value) => {
config.address = value;
},
unset: (config) => {
delete config.address;
},
validate: (config) => {
return null;
},
},
'host.name': {
tab: 'network',
set: (config, value) => {
config.host.name = value;
},
unset: (config) => {
delete config.host.name;
},
validate: (config) => {
if (config.tls.auto === false) {
return null;
}
const names = toArray(config.host.name, ',').filter((name) => {
if (name.match(/([0-9]+\.)+/) !== null) {
return true;
}
if (name.match(/[:/]/) !== null) {
return true;
}
return false;
});
if (names.length !== 0) {
return 'Only domains names are allowed';
}
return null;
},
},
'tls.address': {
tab: 'network',
set: (config, value) => {
config.tls.address = value;
},
unset: (config) => {
delete config.tls.address;
},
validate: (config) => {
return null;
},
},
'tls.auto': {
tab: 'network',
set: (config, value) => {
config.tls.auto = !config.tls.auto;
},
unset: (config) => {
delete config.tls.auto;
},
validate: (config) => {
return null;
},
},
'api.auth.enable': {
tab: 'auth',
set: (config, value) => {
config.api.auth.enable = !config.api.auth.enable;
},
unset: (config) => {
delete config.api.auth.enable;
},
validate: (config) => {
return null;
},
},
'api.auth.username': {
tab: 'auth',
set: (config, value) => {
config.api.auth.username = value;
},
unset: (config) => {
delete config.api.auth.username;
},
validate: (config) => {
return null;
},
},
'api.auth.password': {
tab: 'auth',
set: (config, value) => {
config.api.auth.password = value;
},
unset: (config) => {
delete config.api.auth.password;
},
validate: (config) => {
return null;
},
},
'rtmp.enable': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.enable = !config.rtmp.enable;
},
unset: (config) => {
delete config.rtmp.enable;
},
validate: (config) => {
return null;
},
},
'rtmp.enable_tls': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.enable_tls = !config.rtmp.enable_tls;
},
unset: (config) => {
delete config.rtmp.enable_tls;
},
validate: (config) => {
return null;
},
},
'rtmp.address': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.address = value;
},
unset: (config) => {
delete config.rtmp.address;
},
validate: (config) => {
return null;
},
},
'rtmp.address_tls': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.address_tls = value;
},
unset: (config) => {
delete config.rtmp.address_tls;
},
validate: (config) => {
return null;
},
},
'rtmp.app': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.app = value;
},
unset: (config) => {
delete config.rtmp.app;
},
validate: (config) => {
return null;
},
},
'rtmp.token': {
tab: 'rtmp',
set: (config, value) => {
config.rtmp.token = value;
},
unset: (config) => {
delete config.rtmp.token;
},
validate: (config) => {
return null;
},
},
'ffmpeg.log.max_lines': {
tab: 'logging',
set: (config, value) => {
config.ffmpeg.log.max_lines = value;
},
unset: (config) => {
delete config.ffmpeg.log.max_lines;
},
validate: (config) => {
return null;
},
},
'ffmpeg.log.max_history': {
tab: 'logging',
set: (config, value) => {
config.ffmpeg.log.max_history = value;
},
unset: (config) => {
delete config.ffmpeg.log.max_history;
},
validate: (config) => {
return null;
},
},
'srt.enable': {
tab: 'srt',
set: (config, value) => {
config.srt.enable = !config.srt.enable;
},
unset: (config) => {
delete config.srt.enable;
},
validate: (config) => {
return null;
},
},
'srt.address': {
tab: 'srt',
set: (config, value) => {
config.srt.address = value;
},
unset: (config) => {
delete config.srt.address;
},
validate: (config) => {
return null;
},
},
'srt.passphrase': {
tab: 'srt',
set: (config, value) => {
config.srt.passphrase = value;
},
unset: (config) => {
delete config.srt.passphrase;
},
validate: (config) => {
return null;
},
},
'srt.token': {
tab: 'srt',
set: (config, value) => {
config.srt.token = value;
},
unset: (config) => {
delete config.srt.token;
},
validate: (config) => {
return null;
},
},
'storage.cors.allow_all': {
tab: 'storage',
set: (config, value) => {
config.storage.cors.allow_all = !config.storage.cors.allow_all;
},
unset: (config) => {
return;
},
validate: (config) => {
return null;
},
},
'storage.cors.origins': {
tab: 'storage',
set: (config, value) => {
config.storage.cors.origins = value;
},
unset: (config) => {
delete config.storage.cors.origins;
},
validate: (config) => {
return null;
},
},
'storage.disk.max_size_mbytes': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.max_size_mbytes = value;
},
unset: (config) => {
delete config.storage.disk.max_size_mbytes;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.enable': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.enable = !config.storage.disk.cache.enable;
},
unset: (config) => {
delete config.storage.disk.cache.enable;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.max_size_mbytes': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.max_size_mbytes = value;
},
unset: (config) => {
delete config.storage.disk.cache.max_size_mbytes;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.ttl_seconds': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.ttl_seconds = value;
},
unset: (config) => {
delete config.storage.disk.cache.ttl_seconds;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.max_file_size_mbytes': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.max_file_size_mbytes = value;
},
unset: (config) => {
delete config.storage.disk.cache.max_file_size_mbytes;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.types.allow': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.types.allow = value;
},
unset: (config) => {
delete config.storage.disk.cache.types.allow;
},
validate: (config) => {
return null;
},
},
'storage.disk.cache.types.block': {
tab: 'storage',
set: (config, value) => {
config.storage.disk.cache.types.block = value;
},
unset: (config) => {
delete config.storage.disk.cache.types.block;
},
validate: (config) => {
return null;
},
},
'storage.memory.auth.enable': {
tab: 'storage',
set: (config, value) => {
config.storage.memory.auth.enable = !config.storage.memory.auth.enable;
},
unset: (config) => {
delete config.storage.memory.auth.enable;
},
validate: (config) => {
return null;
},
},
'storage.memory.auth.username': {
tab: 'storage',
set: (config, value) => {
config.storage.memory.auth.username = value;
},
unset: (config) => {
delete config.storage.memory.auth.username;
},
validate: (config) => {
return null;
},
},
'storage.memory.auth.password': {
tab: 'storage',
set: (config, value) => {
config.storage.memory.auth.password = value;
},
unset: (config) => {
delete config.storage.memory.auth.password;
},
validate: (config) => {
return null;
},
},
'storage.memory.max_size_mbytes': {
tab: 'storage',
set: (config, value) => {
config.storage.memory.max_size_mbytes = value;
},
unset: (config) => {
delete config.storage.memory.max_size_mbytes;
},
validate: (config) => {
return null;
},
},
'storage.memory.purge': {
tab: 'storage',
set: (config, value) => {
config.storage.memory.purge = !config.storage.memory.purge;
},
unset: (config) => {
delete config.storage.memory.purge;
},
validate: (config) => {
return null;
},
},
'sessions.enable': {
tab: 'playback',
set: (config, value) => {
config.sessions.enable = !config.sessions.enable;
},
unset: (config) => {
delete config.sessions.enable;
},
validate: (config) => {
return null;
},
},
'sessions.ip_ignorelist': {
tab: 'playback',
set: (config, value) => {
config.sessions.ip_ignorelist = value;
},
unset: (config) => {
delete config.sessions.ip_ignorelist;
},
validate: (config) => {
return null;
},
},
'sessions.session_timeout_sec': {
tab: 'playback',
set: (config, value) => {
config.sessions.session_timeout_sec = value;
},
unset: (config) => {
delete config.sessions.session_timeout_sec;
},
validate: (config) => {
return null;
},
},
'sessions.persist': {
tab: 'playback',
set: (config, value) => {
config.sessions.persist = !config.sessions.persist;
},
unset: (config) => {
delete config.sessions.persist;
},
validate: (config) => {
return null;
},
},
'sessions.max_bitrate_mbit': {
tab: 'network',
set: (config, value) => {
config.sessions.max_bitrate_mbit = value;
},
unset: (config) => {
delete config.sessions.max_bitrate_mbit;
},
validate: (config) => {
return null;
},
},
'sessions.max_sessions': {
tab: 'network',
set: (config, value) => {
config.sessions.max_sessions = value;
},
unset: (config) => {
delete config.sessions.max_sessions;
},
validate: (config) => {
return null;
},
},
'service.enable': {
tab: 'service',
set: (config, value) => {
config.service.enable = !config.service.enable;
},
unset: (config) => {
delete config.service.enable;
},
validate: (config) => {
return null;
},
},
'service.token': {
tab: 'service',
set: (config, value) => {
config.service.token = value;
},
unset: (config) => {
delete config.service.token;
},
validate: (config) => {
return null;
},
},
};
const RETRIES = 60;
function ErrorTab(props) {
let { label, errors, ...other } = props;
if (errors === true) {
label = (
<Stack direction="row" spacing={1} alignItems="center">
<WarningIcon color="error" />
<Typography>{label}</Typography>
</Stack>
);
}
return <Tab label={label} {...other} />;
}
function ErrorBox(props) {
const messages = props.messages.filter((m) => props.configvalue === '' || m.configvalue === props.configvalue);
if (messages.length === 0) {
return null;
}
return (
<BoxText color="danger">
<Typography variant="body2" gutterBottom>
{messages.map((e, i) => (
<span key={i}>
{e.error}
<br />
</span>
))}
</Typography>
</BoxText>
);
}
ErrorBox.defaultProps = {
configvalue: '',
messages: [],
};
const toArray = (val, separator) => {
return val
.split(separator)
.map((l) => l.trim())
.filter((l) => l.length !== 0);
};
const toInt = (val) => {
if (typeof val === 'string') {
return val.length === 0 ? 0 : parseInt(val);
}
return val;
};
export default function Settings(props) {
const classes = useStyles();
const { i18n } = useLingui();
const navigate = useNavigate();
const { tab: _tab } = useParams();
const notify = React.useContext(NotifyContext);
const [$config, setConfig] = React.useState({
ready: false,
modified: false,
data: null,
overrides: [],
created_at: null,
loaded_at: null,
updated_at: null,
outdated: false,
core: '',
service: false,
});
const [$expert, setExpert] = React.useState(props.restreamer.IsExpert());
const [$updates, setUpdates] = React.useState({
has: props.restreamer.HasUpdates(),
want: props.restreamer.CheckForUpdates(),
});
const [$tab, setTab] = React.useState(_tab ? _tab : 'general');
const [$tabs, setTabs] = React.useState({
general: { errors: false, messages: [] }, // messages is an array of objects: {configvalue: '', error: ''}
network: { errors: false, messages: [] },
auth: { errors: false, messages: [] },
playback: { errors: false, messages: [] },
storage: { errors: false, messages: [] },
rtmp: { errors: false, messages: [] },
srt: { errors: false, messages: [] },
logging: { errors: false, messages: [] },
service: { errors: false, messages: [] },
});
const [$logdata, setLogdata] = React.useState('');
const logTimer = React.useRef();
const [$reloadKey, setReloadKey] = React.useState('');
const [$dialogs, setDialogs] = React.useState({
restart: false,
saved: false,
});
const [$saving, setSaving] = React.useState(false);
const [$restart, setRestart] = React.useState({
restarting: false,
timeout: false,
changes: {
tls: false,
port: false,
},
});
React.useEffect(() => {
(async () => {
await load();
})();
return () => {
clearInterval(logTimer.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useInterval(() => {
setUpdates({
...$updates,
has: props.restreamer.HasUpdates(),
});
}, 1000 * 2);
const load = async () => {
setReloadKey(props.restreamer.CreatedAt().toISOString());
const data = await props.restreamer.Config();
let config = null;
let overrides = [];
let outdated = false;
if (data !== null) {
config = JSON.parse(JSON.stringify(data.config));
config.host.name = config.host.name.join(', ');
config.storage.cors.allow_all = false;
if (config.storage.cors.origins.length === 1 && config.storage.cors.origins[0] === '*') {
config.storage.cors.allow_all = true;
}
config.storage.cors.origins = config.storage.cors.origins.join('\n');
config.storage.disk.cache.types.allow = config.storage.disk.cache.types.allow.join('\n');
config.storage.disk.cache.types.block = config.storage.disk.cache.types.block.join('\n');
config.sessions.ip_ignorelist = config.sessions.ip_ignorelist.join('\n');
if (config.tls.enable === false) {
config.tls.auto = false;
}
config.address = config.address.split(':').join('');
config.tls.address = config.tls.address.split(':').join('');
config.rtmp.address = config.rtmp.address.split(':').join('');
config.rtmp.address_tls = config.rtmp.address_tls.split(':').join('');
config.srt.address = config.srt.address.split(':').join('');
if (config.tls.auto === true) {
config.tls.enable = true;
}
overrides = data.overrides;
if (data.loaded_at < data.updated_at) {
outdated = true;
}
}
setConfig({
...$config,
ready: true,
modified: false,
data: config,
overrides: overrides,
outdated: outdated,
core: '',
service: props.restreamer.HasService(),
});
};
const handleExpertMode = () => {
props.restreamer.SetExpert(!$expert);
setExpert(!$expert);
};
const handleCheckForUpdates = () => {
props.restreamer.SetCheckForUpdates(!$updates.want);
setUpdates({
...$updates,
want: !$updates.want,
});
};
const handleChange = (what) => (event) => {
const value = event.target.value;
const config = $config.data;
if (!(what in configValues)) {
console.warn(`config value "${what}" is not a valid configValue`);
return;
}
configValues[what].set(config, value);
const tabs = $tabs;
// Create tab if it doesn't exist
const tab = configValues[what].tab;
if (!(tab in tabs)) {
tabs[tab] = {
errors: false,
messages: [],
};
}
// Remove all error for a config value
tabs[tab].messages = tabs[tab].messages.filter((m) => m.configvalue !== what);
if (tabs[tab].messages.length === 0) {
tabs[tab].errors = false;
}
// Validate current config value and add error if validation fails
const err = configValues[what].validate(config);
if (err !== null) {
tabs[tab].errors = true;
tabs[tab].messages.push({
configvalue: what,
error: err,
});
}
setTabs({
...$tabs,
...tabs,
});
setConfig({
...$config,
data: config,
modified: true,
});
};
const handleCoreConfig = (event) => {
const value = event.target.value;
setConfig({
...$config,
core: value,
modified: true,
});
};
const handleChangeTab = async (event, value) => {
if (value === 'logging') {
await updateLogdata();
logTimer.current = setInterval(async () => {
await updateLogdata();
}, 1000);
} else {
clearInterval(logTimer.current);
}
setTab(value);
};
const updateLogdata = async () => {
const logdata = await props.restreamer.Log();
setLogdata(logdata.join('\n'));
};
const handleSave = async () => {
setSaving(true);
let config = null;
if ($config.core.length === 0) {
// Poor man's deep clone, but that's OK because we have only basic data types
config = JSON.parse(JSON.stringify($config.data));
config.log.max_lines = toInt(config.log.max_lines);
config.host.name = toArray(config.host.name, ',');
config.storage.cors.origins = toArray(config.storage.cors.origins, '\n');
if (config.storage.cors.allow_all === true) {
config.storage.cors.origins = ['*'];
}
delete config.storage.cors.allow_all;
config.storage.disk.cache.types.allow = toArray(config.storage.disk.cache.types.allow, '\n');
config.storage.disk.cache.types.block = toArray(config.storage.disk.cache.types.block, '\n');
config.sessions.ip_ignorelist = toArray(config.sessions.ip_ignorelist, '\n');
config.ffmpeg.log.max_lines = toInt(config.ffmpeg.log.max_lines);
config.ffmpeg.log.max_history = toInt(config.ffmpeg.log.max_history);
config.storage.disk.max_size_mbytes = toInt(config.storage.disk.max_size_mbytes);
config.storage.disk.cache.max_size_mbytes = toInt(config.storage.disk.cache.max_size_mbytes);
config.storage.disk.cache.ttl_seconds = toInt(config.storage.disk.cache.ttl_seconds);
config.storage.disk.cache.max_file_size_mbytes = toInt(config.storage.disk.cache.max_file_size_mbytes);
config.storage.memory.max_size_mbytes = toInt(config.storage.memory.max_size_mbytes);
config.sessions.session_timeout_sec = toInt(config.sessions.session_timeout_sec);
config.sessions.max_bitrate_mbit = toInt(config.sessions.max_bitrate_mbit);
config.sessions.max_sessions = toInt(config.sessions.max_sessions);
config.address = ':' + config.address;
config.tls.address = ':' + config.tls.address;
config.rtmp.address = ':' + config.rtmp.address;
config.rtmp.address_tls = ':' + config.rtmp.address_tls;
config.rtmp.app = !config.rtmp.app.startsWith('/') ? '/' + config.rtmp.app : config.rtmp.app;
config.srt.address = ':' + config.srt.address;
if (config.tls.auto === true) {
config.tls.enable = true;
} else {
config.tls.enable = false;
}
for (let what of $config.overrides) {
if (!(what in configValues)) {
continue;
}
configValues[what].unset(config);
}
} else {
try {
config = JSON.parse($config.core);
} catch (e) {
const tabs = {};
tabs['service'] = {
errors: true,
messages: [
{
configvalue: 'coreconfig',
error: e.message,
},
],
};
setTabs({
...$tabs,
...tabs,
});
setSaving(false);
return;
}
}
let outdated = $config.outdated;
const [, err] = await props.restreamer.ConfigSet(config);
if (err !== null) {
if (err.code === 404) {
notify.Dispatch('error', 'save:settings', i18n._(t`API endpoint not found. Settings not saved.`));
} else if (err.code === 409) {
notify.Dispatch('error', 'save:settings', i18n._(t`There were some errors in the settings. Settings not saved.`));
} else {
notify.Dispatch('error', 'save:settings', i18n._(t`There was a problem storing the settings. Settings not saved.`));
}
const tabs = {};
const ucfirst = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
if (err.code === 409) {
for (let errorfield in err.message) {
if (!(errorfield in configValues)) {
continue;
}
const tab = configValues[errorfield].tab;
if (!(tab in tabs)) {
tabs[tab] = {
errors: true,
messages: [],
};
}
for (let m of err.message[errorfield]) {
tabs[tab].messages.push({
configvalue: errorfield,
error: ucfirst(m),
});
}
}
} else if (err.code === 400) {
tabs['service'] = {
errors: true,
messages: [
{
configvalue: 'coreconfig',
error: err.message,
},
],
};
}
setTabs({
...$tabs,
...tabs,
});
} else {
notify.Dispatch('success', 'save:settings', i18n._(t`Settings saved. All changes will be applied after restarting the application.`));
const tabs = {};
for (let t in $tabs) {
tabs[t] = { errors: false, messages: [] };
}
setTabs({
...$tabs,
...tabs,
});
outdated = true;
handleSavedDialog();
}
setConfig({
...$config,
modified: false,
outdated: outdated,
core: err === null ? '' : $config.core,
});
setSaving(false);
};
const handleSavedDialog = () => {
setDialogs({
...$dialogs,
saved: !$dialogs.saved,
});
};
const handleReloadDialog = () => {
setDialogs({
...$dialogs,
restart: !$dialogs.restart,
});
};
const handleReload = async () => {
setDialogs({
...$dialogs,
saved: false,
restart: false,
});
setRestart({
...$restart,
restarting: true,
timeout: false,
});
const res = await props.restreamer.ConfigReload();
if (res === false) {
notify.Dispatch('error', 'restart', i18n._(t`Restarting the application failed.`));
setRestart({
...$restart,
restarting: false,
});
return;
}
const waitFor = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
props.restreamer.IgnoreAPIErrors(true);
let restarted = false;
for (let retries = 0; retries <= RETRIES; retries++) {
await waitFor(1000);
let currentKey = $reloadKey;
const about = await props.restreamer.About();
if (about === null) {
// API is not yet available
continue;
}
if (about.id.length === 0) {
// The API requires a login and after the restart the token
// is not valid anymore. This means the restart happened.
restarted = true;
break;
}
let createdAt = about.created_at;
if (createdAt !== null) {
currentKey = createdAt.toISOString();
}
if ($reloadKey !== currentKey) {
// The API tells us that it changed from before. This means
// the restart happened.
restarted = true;
break;
}
}
if (restarted === false) {
setRestart({
...$restart,
restarting: true,
timeout: true,
});
//notify.Dispatch('error', 'restart', i18n._(t`Restarting the application failed.`));
return false;
}
await props.restreamer.Validate();
await props.restreamer.Login($config.data.api.auth.username, $config.data.api.auth.password);
window.location.reload();
return true;
};
const handleAbort = () => {
navigate(-1);
};
const handleHelp = (topic) => () => {
H('settings-' + topic);
};
const env = (what) => {
if ($config.overrides.indexOf(what) !== -1) {
return true;
}
return false;
};
if ($config.ready === false) {
return null;
}
if ($config.data === null) {
return (
<Paper xs={8} sm={6} md={6}>
<PaperHeader title={<Trans>Error</Trans>} />
<PaperContent>
<Trans>Unable to load the config.</Trans>
</PaperContent>
<PaperFooter
buttonsRight={
<Button variant="outlined" color="primary" onClick={() => window.location.reload()}>
<Trans>Retry</Trans>
</Button>
}
/>
</Paper>
);
}
const config = $config.data;
let title = <Trans>Settings</Trans>;
if ($expert === true) {
title = <Trans>Settings (expert mode)</Trans>;
}
return (
<React.Fragment>
<Paper xs={12} md={10}>
<PaperHeader title={title} onAbort={handleAbort} onHelp={handleHelp($tab)} />
<Grid container spacing={1}>
<TabsVerticalGrid>
<Tabs orientation="vertical" variant="scrollable" value={$tab} onChange={handleChangeTab}>
<ErrorTab className="tab" label={<Trans>General</Trans>} value="general" errors={$tabs.general.errors} />
{$config.service === true && (
<ErrorTab className="tab" label={<Trans>Service</Trans>} value="service" errors={$tabs.service.errors} />
)}
<ErrorTab className="tab" label={<Trans>Network</Trans>} value="network" errors={$tabs.network.errors} />
<ErrorTab className="tab" label={<Trans>Authorization</Trans>} value="auth" errors={$tabs.auth.errors} />
{$expert === true && <ErrorTab className="tab" label={<Trans>Playback</Trans>} value="playback" errors={$tabs.playback.errors} />}
{$expert === true && <ErrorTab className="tab" label={<Trans>Storage</Trans>} value="storage" errors={$tabs.storage.errors} />}
<ErrorTab className="tab" label={<Trans>RTMP</Trans>} value="rtmp" errors={$tabs.rtmp.errors} />
<ErrorTab className="tab" label={<Trans>SRT</Trans>} value="srt" errors={$tabs.srt.errors} />
{$expert === true && <ErrorTab className="tab" label={<Trans>Logging</Trans>} value="logging" errors={$tabs.logging.errors} />}
</Tabs>
<TabPanel value={$tab} index="general" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Grid item xs={12}>
<PaperThumb image={settingsImage} title="Welcome to Restreamer v2" height="200px" />
</Grid>
</Grid>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>General</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>All important system settings.</Trans>
</Typography>
</Grid>
{$tabs.general.errors && (
<Grid item xs={12}>
<ErrorBox messages={$tabs.general.messages} />
</Grid>
)}
<Grid item xs={12}>
<Checkbox label={<Trans>Check for updates</Trans>} checked={$updates.want} onChange={handleCheckForUpdates} />
{$updates.has === true && (
<BoxText color="success">
<Typography variant="inherit" color="inherit" width="100%">
<Link
color="inherit"
style={{ textDecoration: 'underline', cursor: 'pointer' }}
onClick={handleHelp('update-link')}
target="_blank"
>
<Trans>There are updates available. Here you get more information.</Trans>
</Link>
</Typography>
</BoxText>
)}
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Send anonymous metrics (helps us for future development)</Trans>}
checked={config.update_check}
disabled={env('update_check')}
onChange={handleChange('update_check')}
/>
{env('update_check') && <Env />}
<ErrorBox configvalue="update_check" messages={$tabs.general.messages} />
</Grid>
<Grid item xs={12}>
<Checkbox label={<Trans>Expert mode</Trans>} checked={$expert} onChange={handleExpertMode} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Button variant="outlined" color="secondary" onClick={handleReloadDialog}>
<Trans>Restart</Trans>
</Button>
{$config.outdated === true && (
<Typography variant="caption">
<Trans>The application is using an older version of the settings.</Trans>
</Typography>
)}
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="service" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Service</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>Setting for connection to the service.</Trans>{' '}
<Link color="secondary" href="https://service.datarhei.com" target="_blank">
<Trans>More about the service</Trans>
</Link>
.
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Restreamer Service</Trans>}
checked={config.service.enable}
disabled={env('service.enable')}
onChange={handleChange('service.enable')}
/>{' '}
{env('service.enable') && <Env />}
<ErrorBox configvalue="service.enable" messages={$tabs.service.messages} />
</Grid>
<Grid item xs={12}>
<TextField label={<Trans>Node ID</Trans>} env={env('id')} disabled value={config.id} onChange={handleChange('id')} />
<Typography variant="caption">
<Trans>Unique ident on the service.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Name</Trans>}
env={env('name')}
disabled={env('name') || !config.service.enable}
value={config.name}
onChange={handleChange('name')}
/>
<ErrorBox configvalue="name" messages={$tabs.service.messages} />
<Typography variant="caption">
<Trans>Human readable name on the service.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Token</Trans>}
env={env('service.token')}
disabled={env('service.token') || !config.service.enable}
value={config.service.token}
onChange={handleChange('service.token')}
/>
<ErrorBox configvalue="service.token" messages={$tabs.service.messages} />
<Typography variant="caption">
<Trans>Service token for monitoring.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
disabled={!config.service.enable}
label={<Trans>Config</Trans>}
value={$config.core}
onChange={handleCoreConfig}
/>
<ErrorBox configvalue="coreconfig" messages={$tabs.service.messages} />
<Typography variant="caption">
<Trans>Custom JSON config for datarhei Core.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="network" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Network</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Address</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Public domain/s</Trans>}
env={env('host.name')}
disabled={env('host.name')}
value={config.host.name}
onChange={handleChange('host.name')}
/>
<ErrorBox configvalue="host.name" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>
The public reachable domain name of the host this Restreamer is running on. Separate multiple domain names by a
comma.
</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>HTTP port</Trans>}
env={env('address')}
disabled={env('address')}
value={config.address}
onChange={handleChange('address')}
/>
<ErrorBox configvalue="address" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>Address to listen on for HTTP requests.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>HTTPS port</Trans>}
env={env('tls.address')}
disabled={env('tls.address') || !config.tls.auto}
value={config.tls.address}
onChange={handleChange('tls.address')}
/>
<ErrorBox configvalue="tls.address" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>Address to listen on for HTTPS requests.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>HTTPS (SSL/TLS)</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Let's Encrypt certification</Trans>}
checked={config.tls.auto}
disabled={env('tls.auto') || config.host.name.length === 0}
onChange={handleChange('tls.auto')}
/>{' '}
{env('tls.auto') && <Env />}
<ErrorBox configvalue="tls.auto" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>Let's Encrypt requires one or more public domain names and an accessible port 80/TCP.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Bandwidth control</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={12}>
<TextField
label={<Trans>Maximum viewers</Trans>}
env={env('sessions.max_sessions')}
disabled={env('sessions.max_sessions')}
value={config.sessions.max_sessions}
onChange={handleChange('sessions.max_sessions')}
/>
<ErrorBox configvalue="sessions.max_sessions" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>
Sets a viewer limit for HLS sessions. If the limit is exceeded, HLS viewers receive the HTTP status code 509
(Bandwidth Limit Exceeded). 0 is unlimited.
</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={12}>
<TextField
label={<Trans>Maximum bandwidth Mbit/s</Trans>}
env={env('sessions.max_bitrate_mbit')}
disabled={env('sessions.max_bitrate_mbit')}
value={config.sessions.max_bitrate_mbit}
onChange={handleChange('sessions.max_bitrate_mbit')}
/>
<ErrorBox configvalue="sessions.max_bitrate_mbit" messages={$tabs.network.messages} />
<Typography variant="caption">
<Trans>
Sets a bandwidth limit in Mbit per second for outgoing HLS data transfer. All services, such as RTMP and outgoing
processes, are included in the calculation. If the bandwidth is exceeded, HLS viewers receive the HTTP status code
509 (Bandwidth Limit Exceeded). 0 is unlimited.
</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="auth" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Authorization</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Basic</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Login/JWT authorization</Trans>}
checked={config.api.auth.enable}
// prob: interface enforces auth.
// disabled={env('api.auth.enable')}
disabled
onChange={handleChange('api.auth.enable')}
/>{' '}
{env('api.auth.enable') && <Env />}
<ErrorBox configvalue="api.auth.enable" messages={$tabs.auth.messages} />
<Typography variant="caption">
<Trans>Enabling authorization is strongly advised. Otherwise, anybody can access this instance.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
env={env('api.auth.username')}
disabled={env('api.auth.username') || !config.api.auth.enable}
label={<Trans>Username</Trans>}
value={config.api.auth.username}
onChange={handleChange('api.auth.username')}
/>
<ErrorBox configvalue="api.auth.username" messages={$tabs.auth.messages} />
<Typography variant="caption">
<Trans>Username for authorization.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Password
env={env('api.auth.password')}
disabled={env('api.auth.password') || !config.api.auth.enable}
label={<Trans>Password</Trans>}
value={config.api.auth.password}
onChange={handleChange('api.auth.password')}
/>
<ErrorBox configvalue="api.auth.password" messages={$tabs.auth.messages} />
<Typography variant="caption">
<Trans>Password for authorization.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="playback" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Playback</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Security</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Allow all referrer</Trans>}
checked={config.storage.cors.allow_all}
disabled={env('storage.cors.origins')}
onChange={handleChange('storage.cors.allow_all')}
/>{' '}
{env('storage.cors.allow_all') && <Env />}
</Grid>
<Grid item xs={12}>
<TextField
multiline
rows={5}
label="CORS: Access-Control-Allow-Origin"
value={config.storage.cors.origins}
env={env('storage.cors.origins')}
disabled={env('storage.cors.origins') || config.storage.cors.allow_all}
onChange={handleChange('storage.cors.origins')}
/>
<ErrorBox configvalue="storage.cors.origins" messages={$tabs.playback.messages} />
<Typography variant="caption">
<Trans>One referrer per line, e.g. http://www.example.com</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Statistics</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>HLS statistic for the In-memory storage</Trans>}
checked={config.sessions.enable}
disabled={env('sessions.enable')}
onChange={handleChange('sessions.enable')}
/>{' '}
{env('sessions.enable') && <Env />}
<ErrorBox configvalue="sessions.enable" messages={$tabs.playback.messages} />
<Typography variant="caption">
<Trans>Allow counting how many viewers the stream has.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
multiline
rows={5}
label={<Trans>Ignore IP ranges</Trans>}
env={env('sessions.ip_ignorelist')}
disabled={env('sessions.ip_ignorelist') || !config.sessions.enable}
value={config.sessions.ip_ignorelist}
onChange={handleChange('sessions.ip_ignorelist')}
/>
<ErrorBox configvalue="sessions.ip_ignorelist" messages={$tabs.playback.messages} />
<Typography variant="caption">
<Trans>
List of IP ranges in CIDR notation, e.g., 127.0.0.1/32, that the statistics will not recordone IP range per line.
Leave empty to record all sessions.
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
type="num"
label={<Trans>Maximum viewer idle time (Seconds)</Trans>}
env={env('sessions.session_timeout_sec')}
disabled={env('sessions.session_timeout_sec') || !config.sessions.enable}
value={config.sessions.session_timeout_sec}
onChange={handleChange('sessions.session_timeout_sec')}
/>
<ErrorBox configvalue="sessions.session_timeout_sec" messages={$tabs.playback.messages} />
<Typography variant="caption">
<Trans>Time until an inactive viewer connection is treated as closed.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Persist viewer statistics</Trans>}
checked={config.sessions.persist}
disabled={env('sessions.persist') || !config.sessions.enable}
onChange={handleChange('sessions.persist')}
/>{' '}
{env('sessions.persist') && <Env />}
<ErrorBox configvalue="sessions.persist" messages={$tabs.playback.messages} />
<Typography variant="caption">
<Trans>Stores the viewer statistics to the disk.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="storage" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Storage</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>In-memory</Trans>
</Typography>
<Typography variant="body1">
<Trans>Settings for /memfs path.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Write protection</Trans>}
checked={config.storage.memory.auth.enable}
env={env('storage.memory.auth.enable')}
disabled={env('storage.memory.auth.enable')}
onChange={handleChange('storage.memory.auth.enable')}
/>
<ErrorBox configvalue="storage.memory.auth.enable" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Enabling basic auth is strongly advised. Otherwise, anybody could write data to /memfs.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>Username</Trans>}
env={env('storage.memory.auth.username')}
disabled={env('storage.memory.auth.username') || !config.storage.memory.auth.enable}
value={config.storage.memory.auth.username}
onChange={handleChange('storage.memory.auth.username')}
/>
<ErrorBox configvalue="storage.memory.auth.username" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Username for authorization.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Password
label={<Trans>Password</Trans>}
env={env('storage.memory.auth.password')}
disabled={env('storage.memory.auth.password') || !config.storage.memory.auth.enable}
value={config.storage.memory.auth.password}
onChange={handleChange('storage.memory.auth.password')}
/>
<ErrorBox configvalue="storage.memory.auth.password" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Password for authorization.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Maximum size (Megabytes)</Trans>}
env={env('storage.memory.max_size_mbytes')}
disabled={env('storage.memory.max_size_mbytes')}
value={config.storage.memory.max_size_mbytes}
onChange={handleChange('storage.memory.max_size_mbytes')}
/>
<ErrorBox configvalue="storage.memory.max_size_mbytes" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Maximum allowed megabytes of RAM for /memfs, 0 for unlimited.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Remove the oldest entries if the /memfs is full</Trans>}
checked={config.storage.memory.purge}
disabled={env('storage.memory.purge') || toInt(config.storage.memory.max_size_mbytes) <= 0}
onChange={handleChange('storage.memory.purge')}
/>{' '}
{env('storage.memory.purge') && <Env />}
<ErrorBox configvalue="storage.memory.purge" messages={$tabs.storage.messages} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Disk</Trans>
</Typography>
<Typography variant="body1">
<Trans>Settings for /data path. The access is protected by </Trans>{' '}
<Link
color="secondary"
href="#/settings/auth"
onClick={() => {
setTab('auth');
}}
>
login (JWT)
</Link>
.
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Maximum size (Megabytes)</Trans>}
value={config.storage.disk.max_size_mbytes}
env={env('storage.memory.max_size_mbytes')}
disabled={env('storage.disk.max_size_mbytes')}
onChange={handleChange('storage.disk.max_size_mbytes')}
/>
<ErrorBox configvalue="storage.disk.max_size_mbytes" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Maximum allowed megabytes to consume from hard disk. 0 for unlimited.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Disk cache</Trans>
</Typography>
<Typography variant="body1">
<Trans>Cache for files on /data.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Disk cache</Trans>}
checked={config.storage.disk.cache.enable}
disabled={env('storage.disk.cache.enable')}
onChange={handleChange('storage.disk.cache.enable')}
/>{' '}
{env('storage.disk.cache.enable') && <Env />}
<ErrorBox configvalue="storage.disk.cache.enable" messages={$tabs.storage.messages} />
</Grid>
<Grid item xs={12}>
<TextField
label={<Trans>Maximum size (Megabytes)</Trans>}
env={env('storage.disk.cache.max_size_mbytes')}
disabled={env('storage.disk.cache.max_size_mbytes') || !config.storage.disk.cache.enable}
value={config.storage.disk.cache.max_size_mbytes}
onChange={handleChange('storage.disk.cache.max_size_mbytes')}
/>
<ErrorBox configvalue="storage.disk.cache.max_size_mbytes" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Maximum allowed cache size, 0 for unlimited.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>Cache time (Seconds)</Trans>}
env={env('storage.disk.cache.ttl_seconds')}
disabled={env('storage.disk.cache.ttl_seconds') || !config.storage.disk.cache.enable}
value={config.storage.disk.cache.ttl_seconds}
onChange={handleChange('storage.disk.cache.ttl_seconds')}
/>
<ErrorBox configvalue="storage.disk.cache.ttl_seconds" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Seconds to keep files in cache.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>Maximum file size (Megabytes)</Trans>}
env={env('storage.disk.cache.max_file_size_mbytes')}
disabled={env('storage.disk.cache.max_file_size_mbytes') || !config.storage.disk.cache.enable}
value={config.storage.disk.cache.max_file_size_mbytes}
onChange={handleChange('storage.disk.cache.max_file_size_mbytes')}
/>
<ErrorBox configvalue="storage.disk.cache.max_file_size_mbytes" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>Maximum file size to put in cache.</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
multiline
rows={5}
label={<Trans>Cache types</Trans>}
env={env('storage.disk.cache.types.allow')}
disabled={env('storage.disk.cache.types.allow') || !config.storage.disk.cache.enable}
value={config.storage.disk.cache.types.allow}
onChange={handleChange('storage.disk.cache.types.allow')}
/>
<ErrorBox configvalue="storage.disk.cache.types.allow" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>List of file extensions to cache (e.g. ".html"), one per line. Leave empty to cache all file types.</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
multiline
rows={5}
label={<Trans>Block cache types</Trans>}
env={env('storage.disk.cache.types.block')}
disabled={env('storage.disk.cache.types.block') || !config.storage.disk.cache.enable}
value={config.storage.disk.cache.types.block}
onChange={handleChange('storage.disk.cache.types.block')}
/>
<ErrorBox configvalue="storage.disk.cache.types.block" messages={$tabs.storage.messages} />
<Typography variant="caption">
<Trans>List of file extensions not to cache (e.g. ".m3u8"), one per line. Leave empty for none.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="rtmp" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>RTMP</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>RTMP server</Trans>}
checked={config.rtmp.enable}
disabled={env('rtmp.enable') || config.rtmp.enable_tls}
onChange={handleChange('rtmp.enable')}
/>{' '}
{env('rtmp.enable') && <Env style={{ marginRight: '2em' }} />}
<ErrorBox configvalue="rtmp.enable" messages={$tabs.rtmp.messages} />
<Checkbox
label={<Trans>RTMPS server</Trans>}
checked={config.rtmp.enable_tls}
disabled={env('rtmp.enable_tls')}
onChange={handleChange('rtmp.enable_tls')}
/>{' '}
{env('rtmp.enable_tls') && <Env />}
<ErrorBox configvalue="rtmp.enable_tls" messages={$tabs.rtmp.messages} />
{config.rtmp.enable_tls && !config.tls.auto && (
<Typography variant="caption">
<Trans>Requires activation</Trans>{' '}
<Link
color="secondary"
href="#/settings/network"
onClick={() => {
setTab('network');
}}
>
TLS/HTTPS
</Link>
.
</Typography>
)}
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={6} md={3}>
<TextField
label={<Trans>RTMP Port</Trans>}
env={env('rtmp.address')}
disabled={env('rtmp.address') || (!config.rtmp.enable && !config.rtmp.enable_tls)}
value={config.rtmp.address}
onChange={handleChange('rtmp.address')}
/>
<ErrorBox configvalue="rtmp.address" messages={$tabs.rtmp.messages} />
<Typography variant="caption">
<Trans>RTMP server listen address.</Trans>
</Typography>
</Grid>
<Grid item xs={6} md={3}>
<TextField
label={<Trans>RTMPS Port</Trans>}
env={env('rtmp.address_tls')}
disabled={env('rtmp.address_tls') || !config.rtmp.enable_tls || !config.tls.auto}
value={config.rtmp.address_tls}
onChange={handleChange('rtmp.address_tls')}
/>
<ErrorBox configvalue="rtmp.address_tls" messages={$tabs.rtmp.messages} />
<Typography variant="caption">
<Trans>RTMPS server listen address.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={<Trans>App</Trans>}
env={env('rtmp.app')}
disabled={env('rtmp.app') || !config.rtmp.enable}
value={config.rtmp.app}
onChange={handleChange('rtmp.app')}
/>
<ErrorBox configvalue="rtmp.app" messages={$tabs.rtmp.messages} />
<Typography variant="caption">
<Trans>RTMP app for publishing.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Password
label={<Trans>Token</Trans>}
env={env('rtmp.token')}
disabled={env('rtmp.token') || !config.rtmp.enable}
value={config.rtmp.token}
onChange={handleChange('rtmp.token')}
/>
<ErrorBox configvalue="rtmp.token" messages={$tabs.rtmp.messages} />
<Typography variant="caption">
<Trans>RTMP token for publishing and playing. The token is the value of the URL query parameter 'token.'</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="srt" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>SRT</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>SRT server</Trans>}
checked={config.srt.enable}
disabled={env('srt.enable')}
onChange={handleChange('srt.enable')}
/>{' '}
{env('srt.enable') && <Env style={{ marginRight: '2em' }} />}
<ErrorBox configvalue="srt.enable" messages={$tabs.srt.messages} />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={6} md={4}>
<TextField
label={<Trans>Port</Trans>}
env={env('srt.address')}
disabled={env('srt.address') || !config.srt.enable}
value={config.srt.address}
onChange={handleChange('srt.address')}
/>
<ErrorBox configvalue="srt.address" messages={$tabs.srt.messages} />
<Typography variant="caption">
<Trans>SRT server listen address.</Trans>
</Typography>
</Grid>
<Grid item xs={6} md={8}>
<Password
label={<Trans>Token</Trans>}
env={env('srt.token')}
disabled={env('srt.token') || !config.srt.enable}
value={config.srt.token}
onChange={handleChange('srt.token')}
/>
<ErrorBox configvalue="srt.token" messages={$tabs.srt.messages} />
<Typography variant="caption">
<Trans>SRT token for publishing and playing. The token is the value of the streamid parameter 'token.'</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Password
label={<Trans>Passphrase</Trans>}
env={env('srt.passphrase')}
disabled={env('srt.passphrase') || !config.srt.enable}
value={config.srt.passphrase}
onChange={handleChange('srt.passphrase')}
inputProps={{ maxLength: 79 }}
error={config.srt.passphrase && config.srt.passphrase.length < 10}
helperText={
config.srt.passphrase && config.srt.passphrase.length < 10 ? (
<Trans>Passphrase must be between 10 and 79 characters long</Trans>
) : (
false
)
}
/>
<ErrorBox configvalue="srt.passphrase" messages={$tabs.srt.messages} />
<Typography variant="caption">
<Trans>Passphrase for SRT encryption.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="logging" className="panel">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Logging</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>System</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<BoxTextarea style={{ paddingTop: '.2em', paddingBottom: '1em' }}>
<Textarea
rows={17}
value={$logdata}
scrollTo="bottom"
readOnly
allowModal
allowCopy
allowDownload={true}
downloadName="core.log"
/>
</BoxTextarea>
</Grid>
<Grid item xs={6}>
<Select
label={<Trans>Log level</Trans>}
value={config.log.level}
disabled={env('log.level')}
onChange={handleChange('log.level')}
>
<MenuItem value="silent">
Silent
{env('log.level') && (
<div className={classes.inlineEnv}>
<Env />
</div>
)}
</MenuItem>
<MenuItem value="error">
Error
{env('log.level') && (
<div className={classes.inlineEnv}>
<Env />
</div>
)}
</MenuItem>
<MenuItem value="warn">
Warn
{env('log.level') && (
<div className={classes.inlineEnv}>
<Env />
</div>
)}
</MenuItem>
<MenuItem value="info">
Info
{env('log.level') && (
<div className={classes.inlineEnv}>
<Env />
</div>
)}
</MenuItem>
<MenuItem value="debug">
Debug
{env('loglevel') && (
<div className={classes.inlineEnv}>
<Env />
</div>
)}
</MenuItem>
</Select>
<ErrorBox configvalue="log.level" messages={$tabs.logging.messages} />
<Typography variant="caption">
<Trans>Level of system protocol.</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label={<Trans>Maximum log lines</Trans>}
value={config.log.max_lines}
env={env('log.max_lines')}
disabled={env('log.max_lines')}
onChange={handleChange('log.max_lines')}
/>
<ErrorBox configvalue="log.max_lines" messages={$tabs.logging.messages} />
<Typography variant="caption">
<Trans>Number of log lines to keep.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>FFmpeg</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label={<Trans>Maximum log lines</Trans>}
value={config.ffmpeg.log.max_lines}
env={env('ffmpeg.log.max_lines')}
disabled={env('ffmpeg.log.max_lines')}
onChange={handleChange('ffmpeg.log.max_lines')}
/>
<ErrorBox configvalue="ffmpeg.log.max_lines" messages={$tabs.logging.messages} />
<Typography variant="caption">
<Trans>Number of log lines to keep.</Trans>
</Typography>
</Grid>
<Grid item xs={6}>
<TextField
label={<Trans>Maximum log histroy</Trans>}
value={config.ffmpeg.log.max_history}
env={env('ffmpeg.log.max_history')}
disabled={env('ffmpeg.log.max_history')}
onChange={handleChange('ffmpeg.log.max_history')}
/>
<ErrorBox configvalue="ffmpeg.log.max_history" messages={$tabs.logging.messages} />
<Typography variant="caption">
<Trans>Number of logs to keep for each process.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
</TabsVerticalGrid>
</Grid>
<PaperFooter
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleAbort}>
<Trans>Abort</Trans>
</Button>
}
buttonsRight={
<Button variant="outlined" color="primary" disabled={!$config.modified} onClick={handleSave}>
<Trans>Save</Trans>
</Button>
}
/>
</Paper>
<Backdrop open={$saving}>
<CircularProgress color="inherit" />
</Backdrop>
<Backdrop open={$restart.restarting}>
<Paper xs={4} sm={4} md={4}>
<PaperHeader title={<Trans>Restarting</Trans>} />
<PaperContent>
{$restart.timeout === false ? (
<React.Fragment>
<Typography variant="body1">
<Trans>Restarting Restreamer Core ...</Trans>
</Typography>
<LinearProgress sx={{ mt: '1em' }} />
</React.Fragment>
) : (
<React.Fragment>
<Typography variant="body1">
<Trans>Reconnecting to Restreamer Core failed for the last {RETRIES} seconds.</Trans>
</Typography>
<Typography variant="body1" sx={{ mt: '1em' }}>
<Trans>
If you enabled Let's Encrypt TLS it might take some time to acquire the certificates. Make sure that Restreamer Core is
reachable via port 80 from the internet. Please check the console log of Restreamer Core.
</Trans>
</Typography>
<Typography variant="body1" sx={{ mt: '1em' }}>
<Trans>
If you changed the ports, it might be that Restreamer Core restarted already, but it is now available on a different
port.
</Trans>
</Typography>
</React.Fragment>
)}
</PaperContent>
<PaperFooter
buttonsRight={
<Button variant="outlined" color="primary" disabled={!$restart.timeout} onClick={() => window.location.reload()}>
<Trans>Reload</Trans>
</Button>
}
/>
</Paper>
</Backdrop>
<Dialog
open={$dialogs.saved}
title={<Trans>Restart required</Trans>}
onClose={handleSavedDialog}
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleSavedDialog}>
<Trans>Abort</Trans>
</Button>
}
buttonsRight={
<Button variant="outlined" color="secondary" onClick={handleReload}>
<Trans>Restart</Trans>
</Button>
}
>
<Typography variant="body1">
<Trans>
You have changed the configuration. In order for the changes to take effect, you have to restart the application. Do you want to restart
now?
</Trans>
</Typography>
</Dialog>
<Dialog
open={$dialogs.restart}
title={<Trans>Restart</Trans>}
onClose={handleReloadDialog}
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleReloadDialog}>
<Trans>Abort</Trans>
</Button>
}
buttonsRight={
<Button variant="outlined" color="secondary" onClick={handleReload}>
<Trans>Restart</Trans>
</Button>
}
>
<Typography variant="body1">
<Trans>Do you really want to restart the application now?</Trans>
</Typography>
</Dialog>
</React.Fragment>
);
}
Settings.defaultProps = {
restreamer: null,
};
Settings.propTypes = {
restreamer: PropTypes.object.isRequired,
};