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 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) => {
if (!initialSettings) {
initialSettings = {};
}
const settings = {
mode: 'pull',
address: '',
username: '',
password: '',
push: {},
rtsp: {},
http: {},
general: {},
...initialSettings,
};
settings.push = {
type: 'rtmp',
...settings.push,
};
settings.rtsp = {
udp: false,
stimeout: 5000000,
...settings.rtsp,
};
settings.http = {
readNative: true,
forceFramerate: false,
framerate: 25,
userAgent: '',
...settings.http,
};
settings.general = {
fflags: ['genpts'],
thread_queue_size: 512,
...settings.general,
};
return settings;
};
const initConfig = (initialConfig) => {
if (!initialConfig) {
initialConfig = {};
}
const config = {
rtmp: {},
srt: {},
hls: {},
...initialConfig,
};
config.rtmp = {
enabled: false,
secure: false,
host: 'localhost',
local: 'localhost',
app: '',
token: '',
name: 'external',
...config.rtmp,
};
config.srt = {
enabled: false,
host: 'localhost',
local: 'localhost',
token: '',
passphrase: '',
name: 'external',
...config.srt,
};
config.hls = {
secure: false,
host: 'localhost',
local: 'localhost',
credentials: '',
name: 'external',
...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');
}
}
return skills;
};
const createInputs = (settings, config, skills) => {
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.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.mode === 'push') {
if (settings.push.type === 'hls') {
input.address = getLocalHLS(config);
input.options.push('-analyzeduration', '20000000');
} else if (settings.push.type === 'rtmp') {
input.address = getLocalRTMP(config);
input.options.push('-analyzeduration', '3000000');
} else if (settings.push.type === 'srt') {
input.address = getLocalSRT(config);
} else {
input.address = '';
}
} else {
input.address = settings.address;
}
const protocol = getProtocolClass(input.address);
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 === 'rtmp') {
input.options.push('-analyzeduration', '3000000');
} else if (protocol === 'http') {
input.options.push('-analyzeduration', '20000000');
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 (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';
}
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 +
',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.hls.name, config.hls.secure);
return url;
};
const getRTMP = (config) => {
const url = getRTMPAddress(config.rtmp.host, config.rtmp.app, config.rtmp.name, config.rtmp.token, config.rtmp.secure);
return url;
};
const getSRT = (config) => {
const url = getSRTAddress(config.srt.host, config.srt.name, config.srt.token, config.srt.passphrase, true);
return url;
};
const getLocalHLS = (config, name) => {
let url = getHLSAddress(config.hls.local, '', config.hls.name, false);
return url;
};
const getLocalRTMP = (config) => {
return getRTMPAddress(config.rtmp.local, config.rtmp.app, config.rtmp.name, config.rtmp.token, false);
};
const getLocalSRT = (config) => {
return getSRTAddress(config.srt.local, config.srt.name, config.srt.token, config.srt.passphrase, false);
};
const isValidURL = (address) => {
const protocol = getProtocolClass(address);
if (protocol.length === 0) {
return false;
}
return true;
};
function Pull(props) {
const classes = useStyles();
const settings = props.settings;
const protocolClass = getProtocolClass(settings.address);
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.
)}
}>
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
)}
)}
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' && }
);
}
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 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);
form = (
Send stream to this address:
Probe
);
}
return form;
}
function PushSRT(props) {
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);
form = (
Send stream to this address:
Probe
);
}
return form;
}
function Source(props) {
const classes = useStyles();
const { i18n } = useLingui();
const settings = initSettings(props.settings);
const config = initConfig(props.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 ([].includes(what)) {
settings.general[what] = !settings.general[what];
} else {
settings.general[what] = value;
}
} else if (section === 'push') {
settings.push[what] = value;
} else {
settings[what] = value;
}
props.onChange({
...settings,
});
};
const handleProbe = () => {
props.onProbe(settings, createInputs(settings, config, skills));
};
return (
{settings.mode === 'pull' ? (
) : (
)}
);
}
Source.defaultProps = {
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 };