From c316e36c1d38e088ac61d75f82f9f73efcde44b6 Mon Sep 17 00:00:00 2001 From: Ingo Oppermann Date: Mon, 6 Nov 2023 16:34:47 +0100 Subject: [PATCH] Add basic image loop input source --- src/utils/restreamer.js | 40 +++-- src/views/Edit/Profile.js | 13 +- src/views/Edit/SourceSelect.js | 8 +- src/views/Edit/Sources/VideoLoop.js | 219 ++++++++++++++++++++++++++++ src/views/Edit/Sources/index.js | 2 + src/views/Edit/index.js | 5 + 6 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 src/views/Edit/Sources/VideoLoop.js diff --git a/src/utils/restreamer.js b/src/utils/restreamer.js index 70d3f1b..3d10e8a 100644 --- a/src/utils/restreamer.js +++ b/src/utils/restreamer.js @@ -507,6 +507,7 @@ class Restreamer { network: [], virtualaudio: [], virtualvideo: [], + videoloop: [], }, sinks: {}, }; @@ -2171,38 +2172,35 @@ class Restreamer { await this._uploadAssetData(`/channels/${channelid}/config.js`, 'var playerConfig = ' + JSON.stringify(playerConfig)); } - // Upload a logo for the selfhosted player - async UploadLogo(channelid, data, extension) { - const channel = this.GetChannel(channelid); - if (channel === null) { - return; + // Upload channel specific channel data + async UploadData(channelid, name, data) { + if (channelid.length === 0) { + channelid = this.GetCurrentChannelID(); } - // sanitize extension - extension = extension.replace(/[^0-9a-z]/gi, ''); + const channel = this.GetChannel(channelid); + if (channel === null) { + return ''; + } - const path = `/channels/${channel.channelid}/logo.${extension}`; + // sanitize name + name = name.replace(/[^0-9a-z.]/gi, ''); + + const path = `/channels/${channel.channelid}/${name}`; await this._uploadAssetData(path, data); return path; } + // Upload a logo for the selfhosted player + async UploadLogo(channelid, data, extension) { + return this.UploadData(channelid, 'logo.' + extension, data); + } + // Upload a poster image for the selfhosted player async UploadPoster(channelid, data, extension) { - const channel = this.GetChannel(channelid); - if (channel === null) { - return; - } - - // sanitize extension - extension = extension.replace(/[^0-9a-z]/gi, ''); - - const path = `/channels/${channel.channelid}/poster.${extension}`; - - await this._uploadAssetData(path, data); - - return path; + return this.UploadData(channelid, 'poster.' + extension, data); } // Playersite diff --git a/src/views/Edit/Profile.js b/src/views/Edit/Profile.js index 816acdb..4f4bfe9 100644 --- a/src/views/Edit/Profile.js +++ b/src/views/Edit/Profile.js @@ -20,7 +20,7 @@ import StreamSelect from './StreamSelect'; import FilterSelect from '../../misc/FilterSelect'; -export default function Source(props) { +export default function Profile(props) { const [$sources, setSources] = React.useState({ video: M.initSource('video', props.sources[0]), audio: M.initSource('audio', props.sources[1]), @@ -202,6 +202,10 @@ export default function Source(props) { setSkillsRefresh(false); }; + const handleStore = async (name, data) => { + return await props.onStore(name, data); + }; + const handleEncoding = (type) => (encoder, decoder) => { const profile = $profile[type]; @@ -342,6 +346,7 @@ export default function Source(props) { onProbe={handleProbe} onChange={handleSourceSettingsChange} onRefresh={handleRefresh} + onStore={handleStore} /> {$videoProbe.status !== 'none' && ( @@ -457,6 +462,7 @@ export default function Source(props) { onSelect={handleSourceChange} onChange={handleSourceSettingsChange} onRefresh={handleRefresh} + onStore={handleStore} /> {$profile.custom.selected === false && $profile.custom.stream >= 0 && ( @@ -603,7 +609,7 @@ export default function Source(props) { ); } -Source.defaultProps = { +Profile.defaultProps = { skills: {}, sources: [], profile: {}, @@ -618,4 +624,7 @@ Source.defaultProps = { }; }, onRefresh: function () {}, + onStore: function (name, data) { + return ''; + }, }; diff --git a/src/views/Edit/SourceSelect.js b/src/views/Edit/SourceSelect.js index 155ec02..e9991ee 100644 --- a/src/views/Edit/SourceSelect.js +++ b/src/views/Edit/SourceSelect.js @@ -68,6 +68,10 @@ export default function SourceSelect(props) { await props.onRefresh(); }; + const handleStore = async (name, data) => { + return await props.onStore(name, data); + }; + const handleProbe = async (settings, inputs) => { await props.onProbe(props.type, $source, settings, inputs); }; @@ -97,6 +101,7 @@ export default function SourceSelect(props) { onChange={handleChange($source)} onProbe={handleProbe} onRefresh={handleRefresh} + onStore={handleStore} /> ); } @@ -129,6 +134,7 @@ SourceSelect.defaultProps = { onSelect: function (type, device) {}, onChange: function (type, device, settings) {}, onRefresh: function () {}, + onStore: function (name, data) {}, }; function Select(props) { @@ -162,7 +168,7 @@ function Select(props) { {s.name} - + , ); } diff --git a/src/views/Edit/Sources/VideoLoop.js b/src/views/Edit/Sources/VideoLoop.js new file mode 100644 index 0000000..782e688 --- /dev/null +++ b/src/views/Edit/Sources/VideoLoop.js @@ -0,0 +1,219 @@ +import React from 'react'; + +import { Trans } 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 Grid from '@mui/material/Grid'; +import Icon from '@mui/icons-material/Cached'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; + +import Dialog from '../../../misc/modals/Dialog'; +import Filesize from '../../../misc/Filesize'; +import FormInlineButton from '../../../misc/FormInlineButton'; +import UploadButton from '../../../misc/UploadButton'; + +const imageTypes = [ + { mimetype: 'image/png', extension: 'png', maxSize: 2 * 1024 * 1024 }, + { mimetype: 'image/jpeg', extension: 'jpg', maxSize: 2 * 1024 * 1024 }, +]; + +const useStyles = makeStyles((theme) => ({ + gridContainer: { + marginTop: '0.5em', + }, +})); + +const initSettings = (initialSettings) => { + if (!initialSettings) { + initialSettings = {}; + } + + const settings = { + address: '', + ...initialSettings, + }; + + return settings; +}; + +const createInputs = (settings) => { + const address = '{diskfs}' + settings.address; + const input = { + address: address, + options: [], + }; + + input.options.push('-loop', '1'); + input.options.push('-framerate', '1'); + input.options.push('-re'); + + return [input]; +}; + +function Source(props) { + const classes = useStyles(); + const settings = initSettings(props.settings); + const [$saving, setSaving] = React.useState(false); + const [$error, setError] = React.useState({ + open: false, + title: '', + message: '', + }); + + const handleChange = (what) => (event) => { + let data = {}; + + data[what] = event.target.value; + + props.onChange({ + ...settings, + ...data, + }); + }; + + const handleImageUpload = async (data, extension) => { + const path = await props.onStore('input.' + extension, data); + + handleChange('address')({ + target: { + value: path, + }, + }); + + setSaving(false); + }; + + const handleUploadStart = () => { + setSaving(true); + }; + + const handleUploadError = (title) => (err) => { + let message = null; + + switch (err.type) { + case 'nofiles': + message = Please select a file to upload.; + break; + case 'mimetype': + message = ( + + The selected file type ({err.actual}) is not allowed. Allowed file types are {err.allowed} + + ); + break; + case 'size': + message = ( + + The selected file is too big ( + ). Only are allowed. + + ); + break; + case 'read': + message = There was an error during upload: {err.message}; + break; + default: + message = Unknown upload error; + } + + setSaving(false); + + showUploadError(title, message); + }; + + const showUploadError = (title, message) => { + setError({ + ...$error, + open: true, + title: title, + message: message, + }); + }; + + const hideUploadError = () => { + setError({ + ...$error, + open: false, + }); + }; + + const handleProbe = () => { + props.onProbe(settings, createInputs(settings)); + }; + + return ( + + + + Image path} + value={settings.address} + onChange={handleChange('address')} + /> + + + Upload} + acceptTypes={imageTypes} + onStart={handleUploadStart} + onError={handleUploadError(Uploading the image failed)} + onUpload={handleImageUpload} + /> + + + + Probe + + + + + + + + OK + + } + > + {$error.message} + + + ); +} + +Source.defaultProps = { + knownDevices: [], + settings: {}, + onChange: function (settings) {}, + onProbe: function (settings, inputs) {}, + onRefresh: function () {}, + onStore: function (name, data) { + return ''; + }, +}; + +function SourceIcon(props) { + return ; +} + +const id = 'videoloop'; +const name = Loop; +const capabilities = ['video']; +const ffversion = '^4.1.0 || ^5.0.0'; + +const func = { + initSettings, + createInputs, +}; + +export { id, name, capabilities, ffversion, SourceIcon as icon, Source as component, func }; diff --git a/src/views/Edit/Sources/index.js b/src/views/Edit/Sources/index.js index ed9e95e..30f4aae 100644 --- a/src/views/Edit/Sources/index.js +++ b/src/views/Edit/Sources/index.js @@ -5,6 +5,7 @@ import * as NoAudio from './NoAudio'; import * as Raspicam from './Raspicam'; import * as Video4Linux from './V4L'; import * as VideoAudio from './VideoAudio'; +import * as VideoLoop from './VideoLoop'; import * as VirtualAudio from './VirtualAudio'; import * as VirtualVideo from './VirtualVideo'; @@ -46,5 +47,6 @@ registry.Register(VirtualAudio); registry.Register(VirtualVideo); registry.Register(NoAudio); registry.Register(VideoAudio); +registry.Register(VideoLoop); export default registry; diff --git a/src/views/Edit/index.js b/src/views/Edit/index.js index 25ab7ef..0f778a5 100644 --- a/src/views/Edit/index.js +++ b/src/views/Edit/index.js @@ -209,6 +209,10 @@ export default function Edit(props) { setSkills(skills); }; + const handleSourceStore = async (name, data) => { + return await props.restreamer.UploadData('', name, data); + }; + const handleSourceProbe = async (inputs) => { let [res, err] = await props.restreamer.Probe(_channelid, inputs); if (err !== null) { @@ -466,6 +470,7 @@ export default function Edit(props) { onRefresh={handleSkillsRefresh} onDone={handleSourceDone} onAbort={handleSourceAbort} + onStore={handleSourceStore} /> )}