Add basic image loop input source

This commit is contained in:
Ingo Oppermann 2023-11-06 16:34:47 +01:00
parent 245f69cdcb
commit c316e36c1d
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
6 changed files with 263 additions and 24 deletions

View File

@ -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

View File

@ -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}
/>
</Grid>
{$videoProbe.status !== 'none' && (
@ -457,6 +462,7 @@ export default function Source(props) {
onSelect={handleSourceChange}
onChange={handleSourceSettingsChange}
onRefresh={handleRefresh}
onStore={handleStore}
/>
</Grid>
{$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 '';
},
};

View File

@ -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) {
<Typography>{s.name}</Typography>
</div>
</Button>
</Grid>
</Grid>,
);
}

View File

@ -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 = <Trans>Please select a file to upload.</Trans>;
break;
case 'mimetype':
message = (
<Trans>
The selected file type ({err.actual}) is not allowed. Allowed file types are {err.allowed}
</Trans>
);
break;
case 'size':
message = (
<Trans>
The selected file is too big (<Filesize bytes={err.actual} />
). Only <Filesize bytes={err.allowed} /> are allowed.
</Trans>
);
break;
case 'read':
message = <Trans>There was an error during upload: {err.message}</Trans>;
break;
default:
message = <Trans>Unknown upload error</Trans>;
}
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 (
<React.Fragment>
<Grid container alignItems="flex-start" spacing={2} className={classes.gridContainer}>
<Grid item xs={12} md={9}>
<TextField
variant="outlined"
fullWidth
id="logo-url"
label={<Trans>Image path</Trans>}
value={settings.address}
onChange={handleChange('address')}
/>
</Grid>
<Grid item xs={12} md={3}>
<UploadButton
label={<Trans>Upload</Trans>}
acceptTypes={imageTypes}
onStart={handleUploadStart}
onError={handleUploadError(<Trans>Uploading the image failed</Trans>)}
onUpload={handleImageUpload}
/>
</Grid>
<Grid item xs={12}>
<FormInlineButton onClick={handleProbe} disabled={!settings.address.length}>
<Trans>Probe</Trans>
</FormInlineButton>
</Grid>
</Grid>
<Backdrop open={$saving}>
<CircularProgress color="inherit" />
</Backdrop>
<Dialog
open={$error.open}
title={$error.title}
onClose={hideUploadError}
buttonsRight={
<Button variant="outlined" color="primary" onClick={hideUploadError}>
<Trans>OK</Trans>
</Button>
}
>
<Typography variant="body1">{$error.message}</Typography>
</Dialog>
</React.Fragment>
);
}
Source.defaultProps = {
knownDevices: [],
settings: {},
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onRefresh: function () {},
onStore: function (name, data) {
return '';
},
};
function SourceIcon(props) {
return <Icon style={{ color: '#FFF' }} {...props} />;
}
const id = 'videoloop';
const name = <Trans>Loop</Trans>;
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 };

View File

@ -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;

View File

@ -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}
/>
)}
</TabPanel>