import React from 'react';
import { useNavigate } from 'react-router-dom';
import SemverSatisfies from 'semver/functions/satisfies';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import urlparser from 'url-parse';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Icon from '@mui/icons-material/AccountTree';
import MenuItem from '@mui/material/MenuItem';
import RefreshIcon from '@mui/icons-material/Refresh';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import WarningIcon from '@mui/icons-material/Warning';
import BoxTextarea from '../../../misc/BoxTextarea';
import BoxText from '../../../misc/BoxText';
import Checkbox from '../../../misc/Checkbox';
import FormInlineButton from '../../../misc/FormInlineButton';
import MultiSelect from '../../../misc/MultiSelect';
import MultiSelectOption from '../../../misc/MultiSelectOption';
import Password from '../../../misc/Password';
import Select from '../../../misc/Select';
import Textarea from '../../../misc/Textarea';
const useStyles = makeStyles((theme) => ({
gridContainer: {
marginTop: '0.5em',
},
}));
const initSettings = (initialSettings, config) => {
if (!initialSettings) {
initialSettings = {};
}
const settings = {
mode: 'pull',
address: '',
username: '',
password: '',
push: {},
rtsp: {},
http: {},
general: {},
...initialSettings,
};
settings.push = {
type: 'rtmp',
name: config.channelid,
...settings.push,
};
settings.rtsp = {
udp: false,
stimeout: 5000000,
...settings.rtsp,
};
settings.http = {
readNative: true,
forceFramerate: false,
framerate: 25,
userAgent: '',
referer: '',
http_proxy: '',
...settings.http,
};
settings.general = {
analyzeduration: 5000000,
analyzeduration_rtmp: 3000000,
analyzeduration_http: 20000000,
probesize: 5000000,
max_probe_packets: 2500,
fflags: ['genpts'],
thread_queue_size: 512,
copyts: false,
start_at_zero: false,
use_wallclock_as_timestamps: false,
avoid_negative_ts: 'auto',
...settings.general,
};
return settings;
};
const initConfig = (initialConfig) => {
if (!initialConfig) {
initialConfig = {};
}
const config = {
rtmp: {},
srt: {},
hls: {},
channelid: 'external',
...initialConfig,
};
config.rtmp = {
enabled: false,
secure: false,
host: 'localhost',
local: 'localhost',
app: '',
token: '',
...config.rtmp,
};
config.srt = {
enabled: false,
host: 'localhost',
local: 'localhost',
token: '',
passphrase: '',
...config.srt,
};
config.hls = {
secure: false,
host: 'localhost',
local: 'localhost',
credentials: '',
...config.hls,
};
return config;
};
const initSkills = (initialSkills) => {
if (!initialSkills) {
initialSkills = {};
}
const skills = {
ffmpeg: {},
formats: {},
protocols: {},
...initialSkills,
};
skills.ffmpeg = {
version: '0.0.0',
...skills.ffmpeg,
};
skills.formats = {
demuxers: [],
...skills.formats,
};
skills.protocols = {
input: [],
...skills.protocols,
};
if (skills.formats.demuxers.includes('rtsp')) {
if (!skills.protocols.input.includes('rtsp')) {
skills.protocols.input.push('rtsp');
}
if (skills.protocols.input.includes('tls')) {
if (!skills.protocols.input.includes('rtsps')) {
skills.protocols.input.push('rtsps');
}
}
}
return skills;
};
const createInputs = (settings, config, skills) => {
settings = initSettings(settings);
config = initConfig(config);
skills = initSkills(skills);
let ffmpeg_version = 4;
if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) {
ffmpeg_version = 5;
}
const input = {
address: '',
options: [],
};
if (settings.mode === 'push') {
let name = settings.push.name;
if (settings.push.type === 'hls') {
if (name === 'none') {
name = config.channelid;
}
input.address = getLocalHLS(name);
} else if (settings.push.type === 'rtmp') {
if (name === config.channelid) {
name += '.stream';
}
input.address = getLocalRTMP(name);
} else if (settings.push.type === 'srt') {
if (name === config.channelid) {
name += '.stream';
}
input.address = getLocalSRT(name);
} else {
input.address = '';
}
} else {
input.address = settings.address;
}
// registrate protocol by address
const protocol = getProtocolClass(input.address);
// general settings (pull/push)
if (settings.general.fflags.length !== 0) {
input.options.push('-fflags', '+' + settings.general.fflags.join('+'));
}
input.options.push('-thread_queue_size', settings.general.thread_queue_size);
if (settings.general.probesize !== 5000000) {
input.options.push('-probesize', settings.general.probesize);
}
if (settings.general.max_probe_packets !== 2500) {
input.options.push('-max_probe_packets', settings.general.max_probe_packets);
}
if (settings.general.copyts) {
input.options.push('-copyts');
}
if (settings.general.start_at_zero) {
input.options.push('-start_at_zero');
}
if (settings.general.use_wallclock_as_timestamps) {
input.options.push('-use_wallclock_as_timestamps', '1');
}
if (ffmpeg_version === 5 && settings.general.avoid_negative_ts !== 'auto') {
input.options.push('-avoid_negative_ts', settings.general.avoid_negative_ts);
}
// general settings > analyzeduration by protocol
//
// old settings:
// analyzeduration: 20s for http and 3s for rtmp streams
if (settings.mode === 'push') {
if (settings.push.type === 'hls') {
if (settings.general.analyzeduration_http !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration_http);
}
} else if (settings.push.type === 'rtmp') {
if (settings.general.analyzeduration_rtmp !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration_rtmp);
}
} else if (settings.push.type === 'srt') {
if (settings.general.analyzeduration !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration);
}
}
} else {
if (protocol === 'http') {
if (settings.general.analyzeduration_http !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration_http);
}
} else if (protocol === 'rtmp') {
if (settings.general.analyzeduration_rtmp !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration_rtmp);
}
} else {
if (settings.general.analyzeduration !== 5000000) {
input.options.push('-analyzeduration', settings.general.analyzeduration);
}
}
}
if (settings.mode === 'pull') {
input.address = addUsernamePassword(input.address, settings.username, settings.password);
if (protocol === 'rtsp') {
if (ffmpeg_version === 4) {
input.options.push('-stimeout', settings.rtsp.stimeout);
} else {
input.options.push('-timeout', settings.rtsp.stimeout);
}
if (settings.rtsp.udp === true) {
input.options.push('-rtsp_transport', 'udp');
} else {
input.options.push('-rtsp_transport', 'tcp');
}
} else if (protocol === 'http') {
if (settings.http.readNative === true) {
input.options.push('-re');
}
if (settings.http.forceFramerate === true) {
input.options.push('-r', settings.http.framerate);
}
if (settings.http.userAgent.length !== 0) {
input.options.push('-user_agent', settings.http.userAgent);
}
if (settings.http.referer.length !== 0) {
input.options.push('-referer', settings.http.referer);
}
if (settings.http.http_proxy.length !== 0) {
input.options.push('-http_proxy', settings.http.http_proxy);
}
}
}
/*
if (skills.protocols.input.includes('playout')) {
if (protocol === 'http' || protocol === 'rtmp' || protocol === 'rtsp') {
if (!input.address.startsWith('playout:')) {
input.address = 'playout:' + input.address;
}
input.options.push('-playout_audio', '1');
}
}
*/
return [input];
};
const addUsernamePassword = (address, username, password) => {
if (username.length === 0 && password.length === 0) {
return address;
}
if (isAuthProtocol(address) === false) {
return address;
}
const url = urlparser(address, {});
if (username.length !== 0) {
url.set('username', username);
}
if (password.length !== 0) {
url.set('password', password);
}
return url.toString();
};
const getProtocol = (url) => {
const re = new RegExp('^([a-z][a-z0-9.+-:]*)://', 'i');
const matches = url.match(re);
if (matches === null || matches.length === 0) {
return '';
}
return matches[1];
};
const getProtocolClass = (url) => {
if (typeof url !== 'string') {
url = '';
}
const protocol = getProtocol(url);
if (protocol.length === 0) {
return '';
}
if (/rtmp(e|s|t)?/.test(protocol) === true) {
return 'rtmp';
} else if (/https?/.test(protocol) === true) {
return 'http';
} else if (/mms(t|h)/.test(protocol) === true) {
return 'mms';
} else if (/rtsps?/.test(protocol) === true) {
return 'rtsp';
}
return protocol;
};
const isAuthProtocol = (url) => {
const protocolClass = getProtocolClass(url);
// eslint-disable-next-line default-case
switch (protocolClass) {
case 'amqp':
case 'ftp':
case 'http':
case 'icecast':
case 'mms':
case 'rtmp':
case 'sftp':
case 'rtsp':
return true;
}
return false;
};
const isSupportedProtocol = (url, supportedProtocols) => {
const protocol = getProtocol(url);
if (protocol.length === 0) {
return false;
}
if (!supportedProtocols.includes(protocol)) {
return false;
}
return true;
};
const getHLSAddress = (host, credentials, name, secure) => {
// Test for IPv6 addresses and put brackets around
let url = 'http' + (secure ? 's' : '') + '://' + (credentials.length !== 0 ? credentials + '@' : '') + host + '/memfs/' + name + '.m3u8';
return url;
};
const getRTMPAddress = (host, app, name, token, secure) => {
let url = 'rtmp' + (secure ? 's' : '') + '://' + host + app + '/' + name + '.stream';
if (token.length !== 0) {
url += '?token=' + encodeURIComponent(token);
}
return url;
};
const getSRTAddress = (host, name, token, passphrase, publish) => {
let url =
'srt' +
'://' +
host +
'?mode=caller&transtype=live&streamid=' +
name +
'.stream,mode:' +
(publish ? 'publish' : 'request') +
(token.length !== 0 ? ',token:' + encodeURIComponent(token) : '');
if (passphrase.length !== 0) {
url += '&passphrase=' + encodeURIComponent(passphrase);
}
return url;
};
const getHLS = (config, name) => {
const url = getHLSAddress(config.hls.host, config.hls.credentials, config.channelid, config.hls.secure);
return url;
};
const getRTMP = (config) => {
const url = getRTMPAddress(config.rtmp.host, config.rtmp.app, config.channelid, config.rtmp.token, config.rtmp.secure);
return url;
};
const getSRT = (config) => {
const url = getSRTAddress(config.srt.host, config.channelid, config.srt.token, config.srt.passphrase, true);
return url;
};
const getLocalHLS = (config, name) => {
let url = getHLSAddress(config.hls.local, '', config.channelid, false);
return url;
};
const getLocalRTMP = (name) => {
return '{rtmp,name=' + name + '}';
};
const getLocalSRT = (name) => {
return '{srt,name=' + name + ',mode=request}';
};
const isValidURL = (address) => {
const protocol = getProtocolClass(address);
if (protocol.length === 0) {
return false;
}
return true;
};
function AdvancedSettings(props) {
const settings = props.settings;
const protocolClass = getProtocolClass(settings.address);
return (
}>
Advanced settings
{protocolClass === 'rtsp' && (
RTSP
UDP transport} checked={settings.rtsp.udp} onChange={props.onChange('rtsp', 'udp')} />
Socket timeout (microseconds)}
value={settings.rtsp.stimeout}
onChange={props.onChange('rtsp', 'stimeout')}
/>
)}
{protocolClass === 'http' && (
HTTP and HTTPS
Read input at native speed}
checked={settings.http.readNative}
onChange={props.onChange('http', 'readNative')}
/>
Force input framerate}
checked={settings.http.forceFramerate}
onChange={props.onChange('http', 'forceFramerate')}
/>
{settings.http.forceFramerate === true && (
Framerate}
value={settings.http.framerate}
onChange={props.onChange('http', 'framerate')}
/>
)}
)}
General
Mininum {32}, default {5000000}
Default {2500}
Default {5000000} ({5} seconds)
copyts} checked={settings.general.copyts} onChange={props.onChange('general', 'copyts')} />
start_at_zero}
checked={settings.general.start_at_zero}
onChange={props.onChange('general', 'start_at_zero')}
/>
use_wallclock_as_timestamps}
checked={settings.general.use_wallclock_as_timestamps}
onChange={props.onChange('general', 'use_wallclock_as_timestamps')}
/>
);
}
function Pull(props) {
const classes = useStyles();
const settings = props.settings;
const authProtocol = isAuthProtocol(settings.address);
const validURL = isValidURL(settings.address);
const supportedProtocol = isSupportedProtocol(settings.address, props.skills.protocols.input);
return (
Enter the address of your network source:
Address}
placeholder="rtsp://ip:port/path"
value={settings.address}
onChange={props.onChange('', 'address')}
/>
Supports HTTP (HLS, DASH), RTP, RTSP, RTMP, SRT and more.
{validURL === true && (
{!supportedProtocol ? (
This protocol is unknown or not supported by the available FFmpeg binary.
) : (
{authProtocol && (
Username}
value={settings.username}
onChange={props.onChange('', 'username')}
/>
Username for the device.
Password}
value={settings.password}
onChange={props.onChange('', 'password')}
/>
Password for the device.
)}
)}
)}
Probe
);
}
function Push(props) {
const classes = useStyles();
const settings = props.settings;
//const supportsHLS = isSupportedProtocol('http://', props.skills.protocols.input);
const supportsRTMP = isSupportedProtocol('rtmp://', props.skills.protocols.input);
const supportsSRT = isSupportedProtocol('srt://', props.skills.protocols.input);
if (!supportsRTMP && !supportsSRT) {
return (
The available FFmpeg binary doesn't support any of the required protocols.
);
}
return (
{settings.push.type === 'rtmp' && }
{settings.push.type === 'hls' && }
{settings.push.type === 'srt' && }
);
}
Push.defaultProps = {
knownDevices: [],
settings: {},
config: {},
skills: null,
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function PushHLS(props) {
const classes = useStyles();
const config = props.config;
const HLS = getHLS(config);
return (
Send stream to this address:
Probe
);
}
function PushRTMP(props) {
const { i18n } = useLingui();
const classes = useStyles();
const navigate = useNavigate();
const config = props.config;
let form = null;
if (config.rtmp.enabled === false) {
form = (
RTMP server is not enabled
);
} else {
const RTMP = getRTMP(config);
const filteredDevices = props.knownDevices.filter((device) => device.media === 'rtmp');
const options = filteredDevices.map((device) => {
return (
);
});
options.unshift(
,
);
options.push(
,
);
form = (
} onClick={props.onRefresh} sx={{ float: 'right' }}>
Refresh
{props.settings.push.name === config.channelid && (
Address:
)}
Probe
);
}
return form;
}
PushRTMP.defaultProps = {
knownDevices: [],
settings: {},
config: {},
skills: null,
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function PushSRT(props) {
const { i18n } = useLingui();
const classes = useStyles();
const navigate = useNavigate();
const config = props.config;
let form = null;
if (config.srt.enabled === false) {
form = (
SRT server is not enabled
);
} else {
const SRT = getSRT(config);
const filteredDevices = props.knownDevices.filter((device) => device.media === 'srt');
const options = filteredDevices.map((device) => {
return (
);
});
options.unshift(
,
);
options.push(
,
);
form = (
} onClick={props.onRefresh} sx={{ float: 'right' }}>
Refresh
{props.settings.push.name === config.channelid && (
Address:
)}
Probe
);
}
return form;
}
PushSRT.defaultProps = {
knownDevices: [],
settings: {},
config: {},
skills: null,
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
};
function Source(props) {
const classes = useStyles();
const { i18n } = useLingui();
const config = initConfig(props.config);
const settings = initSettings(props.settings, config);
const skills = initSkills(props.skills);
const handleChange = (section, what) => (event) => {
const value = event.target.value;
if (section === 'http') {
if (['readNative', 'forceFramerate'].includes(what)) {
settings.http[what] = !settings.http[what];
} else {
settings.http[what] = value;
}
} else if (section === 'rtsp') {
if (['udp'].includes(what)) {
settings.rtsp[what] = !settings.rtsp[what];
} else {
settings.rtsp[what] = value;
}
} else if (section === 'general') {
if (['copyts', 'start_at_zero', 'use_wallclock_as_timestamps'].includes(what)) {
settings.general[what] = !settings.general[what];
} else {
settings.general[what] = value;
}
} else if (section === 'push') {
settings.push[what] = value;
if (what === 'type') {
settings.push.name = config.channelid;
}
} else {
settings[what] = value;
}
props.onChange({
...settings,
});
};
const handleProbe = () => {
props.onProbe(settings, createInputs(settings, config, skills));
};
const handleRefresh = () => {
props.onRefresh();
};
return (
{settings.mode === 'pull' ? (
) : (
)}
);
}
Source.defaultProps = {
knownDevices: [],
settings: {},
config: {},
skills: null,
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
};
function SourceIcon(props) {
return ;
}
const id = 'network';
const name = Network source;
const capabilities = ['audio', 'video'];
const ffversion = '^4.1.0 || ^5.0.0';
const func = {
initSettings,
initConfig,
initSkills,
createInputs,
getProtocolClass,
getHLS,
getRTMP,
getSRT,
isValidURL,
isAuthProtocol,
isSupportedProtocol,
};
export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func };