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 = (
{label}
);
}
return ;
}
function ErrorBox(props) {
const messages = props.messages.filter((m) => props.configvalue === '' || m.configvalue === props.configvalue);
if (messages.length === 0) {
return null;
}
return (
{messages.map((e, i) => (
{e.error}
))}
);
}
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 (
Error} />
Unable to load the config.
window.location.reload()}>
Retry
}
/>
);
}
const config = $config.data;
let title = Settings;
if ($expert === true) {
title = Settings (expert mode);
}
return (
General} value="general" errors={$tabs.general.errors} />
{$config.service === true && (
Service} value="service" errors={$tabs.service.errors} />
)}
Network} value="network" errors={$tabs.network.errors} />
Authorization} value="auth" errors={$tabs.auth.errors} />
{$expert === true && Playback} value="playback" errors={$tabs.playback.errors} />}
{$expert === true && Storage} value="storage" errors={$tabs.storage.errors} />}
RTMP} value="rtmp" errors={$tabs.rtmp.errors} />
SRT} value="srt" errors={$tabs.srt.errors} />
{$expert === true && Logging} value="logging" errors={$tabs.logging.errors} />}
General
All important system settings.
{$tabs.general.errors && (
)}
Check for updates} checked={$updates.want} onChange={handleCheckForUpdates} />
{$updates.has === true && (
There are updates available. Here you get more information.
)}
Send anonymous metrics (helps us for future development)}
checked={config.update_check}
disabled={env('update_check')}
onChange={handleChange('update_check')}
/>
{env('update_check') && }
Expert mode} checked={$expert} onChange={handleExpertMode} />
{$config.outdated === true && (
The application is using an older version of the settings.
)}
Service
Setting for connection to the service.{' '}
More about the service
.
Restreamer Service}
checked={config.service.enable}
disabled={env('service.enable')}
onChange={handleChange('service.enable')}
/>{' '}
{env('service.enable') && }
Node ID} env={env('id')} disabled value={config.id} onChange={handleChange('id')} />
Unique ident on the service.
Name}
env={env('name')}
disabled={env('name') || !config.service.enable}
value={config.name}
onChange={handleChange('name')}
/>
Human readable name on the service.
Token}
env={env('service.token')}
disabled={env('service.token') || !config.service.enable}
value={config.service.token}
onChange={handleChange('service.token')}
/>
Service token for monitoring.
Config}
value={$config.core}
onChange={handleCoreConfig}
/>
Custom JSON config for datarhei Core.
Network
Address
Public domain/s}
env={env('host.name')}
disabled={env('host.name')}
value={config.host.name}
onChange={handleChange('host.name')}
/>
The public reachable domain name of the host this Restreamer is running on. Separate multiple domain names by a
comma.
HTTP port}
env={env('address')}
disabled={env('address')}
value={config.address}
onChange={handleChange('address')}
/>
Address to listen on for HTTP requests.
HTTPS port}
env={env('tls.address')}
disabled={env('tls.address') || !config.tls.auto}
value={config.tls.address}
onChange={handleChange('tls.address')}
/>
Address to listen on for HTTPS requests.
HTTPS (SSL/TLS)
Let's Encrypt certification}
checked={config.tls.auto}
disabled={env('tls.auto') || config.host.name.length === 0}
onChange={handleChange('tls.auto')}
/>{' '}
{env('tls.auto') && }
Let's Encrypt requires one or more public domain names and an accessible port 80/TCP.
Bandwidth control
Maximum viewers}
env={env('sessions.max_sessions')}
disabled={env('sessions.max_sessions')}
value={config.sessions.max_sessions}
onChange={handleChange('sessions.max_sessions')}
/>
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.
Maximum bandwidth Mbit/s}
env={env('sessions.max_bitrate_mbit')}
disabled={env('sessions.max_bitrate_mbit')}
value={config.sessions.max_bitrate_mbit}
onChange={handleChange('sessions.max_bitrate_mbit')}
/>
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.
Authorization
Basic
Login/JWT authorization}
checked={config.api.auth.enable}
// prob: interface enforces auth.
// disabled={env('api.auth.enable')}
disabled
onChange={handleChange('api.auth.enable')}
/>{' '}
{env('api.auth.enable') && }
Enabling authorization is strongly advised. Otherwise, anybody can access this instance.
Username}
value={config.api.auth.username}
onChange={handleChange('api.auth.username')}
/>
Username for authorization.
Password}
value={config.api.auth.password}
onChange={handleChange('api.auth.password')}
/>
Password for authorization.
Playback
Security
Allow all referrer}
checked={config.storage.cors.allow_all}
disabled={env('storage.cors.origins')}
onChange={handleChange('storage.cors.allow_all')}
/>{' '}
{env('storage.cors.allow_all') && }
One referrer per line, e.g. http://www.example.com
Statistics
HLS statistic for the In-memory storage}
checked={config.sessions.enable}
disabled={env('sessions.enable')}
onChange={handleChange('sessions.enable')}
/>{' '}
{env('sessions.enable') && }
Allow counting how many viewers the stream has.
Ignore IP ranges}
env={env('sessions.ip_ignorelist')}
disabled={env('sessions.ip_ignorelist') || !config.sessions.enable}
value={config.sessions.ip_ignorelist}
onChange={handleChange('sessions.ip_ignorelist')}
/>
List of IP ranges in CIDR notation, e.g., 127.0.0.1/32, that the statistics will not record—one IP range per line.
Leave empty to record all sessions.
Maximum viewer idle time (Seconds)}
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')}
/>
Time until an inactive viewer connection is treated as closed.
Persist viewer statistics}
checked={config.sessions.persist}
disabled={env('sessions.persist') || !config.sessions.enable}
onChange={handleChange('sessions.persist')}
/>{' '}
{env('sessions.persist') && }
Stores the viewer statistics to the disk.
Storage
In-memory
Settings for /memfs path.
Write protection}
checked={config.storage.memory.auth.enable}
env={env('storage.memory.auth.enable')}
disabled={env('storage.memory.auth.enable')}
onChange={handleChange('storage.memory.auth.enable')}
/>
Enabling basic auth is strongly advised. Otherwise, anybody could write data to /memfs.
Username}
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')}
/>
Username for authorization.
Password}
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')}
/>
Password for authorization.
Maximum size (Megabytes)}
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')}
/>
Maximum allowed megabytes of RAM for /memfs, 0 for unlimited.
Remove the oldest entries if the /memfs is full}
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') && }
Disk
Settings for /data path. The access is protected by {' '}
{
setTab('auth');
}}
>
login (JWT)
.
Maximum size (Megabytes)}
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')}
/>
Maximum allowed megabytes to consume from hard disk. 0 for unlimited.
Disk cache
Cache for files on /data.
Disk cache}
checked={config.storage.disk.cache.enable}
disabled={env('storage.disk.cache.enable')}
onChange={handleChange('storage.disk.cache.enable')}
/>{' '}
{env('storage.disk.cache.enable') && }
Maximum size (Megabytes)}
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')}
/>
Maximum allowed cache size, 0 for unlimited.
Cache time (Seconds)}
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')}
/>
Seconds to keep files in cache.
Maximum file size (Megabytes)}
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')}
/>
Maximum file size to put in cache.
Cache types}
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')}
/>
List of file extensions to cache (e.g. ".html"), one per line. Leave empty to cache all file types.
Block cache types}
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')}
/>
List of file extensions not to cache (e.g. ".m3u8"), one per line. Leave empty for none.
RTMP
RTMP server}
checked={config.rtmp.enable}
disabled={env('rtmp.enable') || config.rtmp.enable_tls}
onChange={handleChange('rtmp.enable')}
/>{' '}
{env('rtmp.enable') && }
RTMPS server}
checked={config.rtmp.enable_tls}
disabled={env('rtmp.enable_tls')}
onChange={handleChange('rtmp.enable_tls')}
/>{' '}
{env('rtmp.enable_tls') && }
{config.rtmp.enable_tls && !config.tls.auto && (
Requires activation{' '}
{
setTab('network');
}}
>
TLS/HTTPS
.
)}
RTMP Port}
env={env('rtmp.address')}
disabled={env('rtmp.address') || (!config.rtmp.enable && !config.rtmp.enable_tls)}
value={config.rtmp.address}
onChange={handleChange('rtmp.address')}
/>
RTMP server listen address.
RTMPS Port}
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')}
/>
RTMPS server listen address.
App}
env={env('rtmp.app')}
disabled={env('rtmp.app') || !config.rtmp.enable}
value={config.rtmp.app}
onChange={handleChange('rtmp.app')}
/>
RTMP app for publishing.
Token}
env={env('rtmp.token')}
disabled={env('rtmp.token') || !config.rtmp.enable}
value={config.rtmp.token}
onChange={handleChange('rtmp.token')}
/>
RTMP token for publishing and playing. The token is the value of the URL query parameter 'token.'
SRT
SRT server}
checked={config.srt.enable}
disabled={env('srt.enable')}
onChange={handleChange('srt.enable')}
/>{' '}
{env('srt.enable') && }
Port}
env={env('srt.address')}
disabled={env('srt.address') || !config.srt.enable}
value={config.srt.address}
onChange={handleChange('srt.address')}
/>
SRT server listen address.
Token}
env={env('srt.token')}
disabled={env('srt.token') || !config.srt.enable}
value={config.srt.token}
onChange={handleChange('srt.token')}
/>
SRT token for publishing and playing. The token is the value of the streamid parameter 'token.'
Passphrase}
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 ? (
Passphrase must be between 10 and 79 characters long
) : (
false
)
}
/>
Passphrase for SRT encryption.
Logging
System
Level of system protocol.
Maximum log lines}
value={config.log.max_lines}
env={env('log.max_lines')}
disabled={env('log.max_lines')}
onChange={handleChange('log.max_lines')}
/>
Number of log lines to keep.
FFmpeg
Maximum log lines}
value={config.ffmpeg.log.max_lines}
env={env('ffmpeg.log.max_lines')}
disabled={env('ffmpeg.log.max_lines')}
onChange={handleChange('ffmpeg.log.max_lines')}
/>
Number of log lines to keep.
Maximum log histroy}
value={config.ffmpeg.log.max_history}
env={env('ffmpeg.log.max_history')}
disabled={env('ffmpeg.log.max_history')}
onChange={handleChange('ffmpeg.log.max_history')}
/>
Number of logs to keep for each process.
Abort
}
buttonsRight={
}
/>
Restarting} />
{$restart.timeout === false ? (
Restarting Restreamer Core ...
) : (
Reconnecting to Restreamer Core failed for the last {RETRIES} seconds.
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.
If you changed the ports, it might be that Restreamer Core restarted already, but it is now available on a different
port.
)}
window.location.reload()}>
Reload
}
/>
);
}
Settings.defaultProps = {
restreamer: null,
};
Settings.propTypes = {
restreamer: PropTypes.object.isRequired,
};