Add basic image loop input source
This commit is contained in:
parent
245f69cdcb
commit
c316e36c1d
@ -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
|
||||
|
||||
@ -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 '';
|
||||
},
|
||||
};
|
||||
|
||||
@ -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>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
219
src/views/Edit/Sources/VideoLoop.js
Normal file
219
src/views/Edit/Sources/VideoLoop.js
Normal 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 };
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user