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: