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
+
+
+
+
+
+
+
+
+ );
+}
+
+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}
/>
)}