diff --git a/src/misc/CircularProgress.js b/src/misc/CircularProgress.js new file mode 100644 index 0000000..a37c936 --- /dev/null +++ b/src/misc/CircularProgress.js @@ -0,0 +1,31 @@ +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; + +export default function Component({ color = 'inherit', value = -1 }) { + if (value < 0) { + return ; + } + + return ( + + + + + {`${Math.round(value)}%`} + + + + ); +} diff --git a/src/misc/Player/videojs.js b/src/misc/Player/videojs.js index 87bf9c8..22c6c0d 100644 --- a/src/misc/Player/videojs.js +++ b/src/misc/Player/videojs.js @@ -3,7 +3,7 @@ import React from 'react'; import Grid from '@mui/material/Grid'; import videojs from 'video.js'; -import overlay from './videojs-overlay.es.js'; +import './videojs-overlay.es.js'; import 'video.js/dist/video-js.min.css'; import './video-js-skin-internal.min.css'; import './video-js-skin-public.min.css'; @@ -28,8 +28,6 @@ export default function VideoJS({ type = 'videojs-internal', options = {}, onRea const videoElement = videoRef.current; if (!videoElement) return; - videojs.registerPlugin('overlay', overlay); - const player = (playerRef.current = videojs(videoElement, options, () => { onReady && onReady(player); })); diff --git a/src/misc/UploadButton.js b/src/misc/UploadButton.js index 181cdfe..96b8693 100644 --- a/src/misc/UploadButton.js +++ b/src/misc/UploadButton.js @@ -63,7 +63,11 @@ export default function UploadButton({ return; } + /* + let streamer = file.stream(); + let reader = new FileReader(); + // read as blob: https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL reader.readAsArrayBuffer(file); reader.onloadend = async () => { if (reader.result === null) { @@ -74,9 +78,11 @@ export default function UploadButton({ }); return; } - - onUpload(reader.result, type.extension, type.mimetype); - }; +*/ + // transformStream in order to count transferred bytes: https://stackoverflow.com/questions/35711724/upload-progress-indicators-for-fetch + // .pipeThrough(progressTrackingStream) + onUpload(file, type.extension, type.mimetype); + //}; }; onStart(); diff --git a/src/utils/api.js b/src/utils/api.js index a5f4f41..b175259 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,3 +1,5 @@ +import { fetch } from './fetch'; + class API { constructor(address) { this.base = '/api'; @@ -277,12 +279,13 @@ class API { return await this._HEAD('/v3/fs/disk' + path); } - async DataPutFile(path, data) { + async DataPutFile(path, data, onprogress = null) { return await this._PUT('/v3/fs/disk' + path, { headers: { 'Content-Type': 'application/data', }, body: data, + onprogress: onprogress, }); } diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 0000000..bf99d27 --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,81 @@ +const fetch = async (url, options = {}) => { + options = { + method: 'GET', + ...options, + }; + + options.method = options.method.toUpperCase(); + + const xhr = new XMLHttpRequest(); + + return new Promise((resolve, reject) => { + xhr.responseType = 'text'; + xhr.onload = () => { + const response = { + ok: false, + headers: { + get: function (key) { + return this.data.get(key.toLowerCase()); + }, + data: xhr + .getAllResponseHeaders() + .split('\r\n') + .reduce((result, current) => { + let [name, value] = current.split(': '); + result.set(name, value); + return result; + }, new Map()), + }, + status: xhr.status, + statusText: xhr.statusText, + data: xhr.response, + json: function () { + return JSON.parse(this.data); + }, + text: function () { + return this.data; + }, + }; + if (xhr.status < 200 || xhr.status >= 300) { + resolve(response); + } else { + response.ok = true; + resolve(response); + } + }; + xhr.onerror = () => { + reject({ + message: 'network error', + }); + }; + if ('onprogress' in options && typeof options.onprogress == 'function') { + const tracker = (event) => { + if (!event.lengthComputable) { + options.onprogress(false, 0, event.loaded); + return; + } + + options.onprogress(true, event.loaded / event.total, event.total); + }; + + if (options.method === 'GET') { + xhr.onprogress = tracker; + } else if (options.method === 'PUT' || options.method === 'POST') { + xhr.upload.onprogress = tracker; + } + } + xhr.open(options.method, url, true); + if ('headers' in options) { + for (const header in options.headers) { + xhr.setRequestHeader(header, options.headers[header]); + } + } + if ('body' in options) { + xhr.send(options.body); + } else { + xhr.send(); + } + }); +}; + +export { fetch }; diff --git a/src/utils/restreamer.js b/src/utils/restreamer.js index 7fd8d40..5f120a8 100644 --- a/src/utils/restreamer.js +++ b/src/utils/restreamer.js @@ -2290,7 +2290,7 @@ class Restreamer { } // Upload channel specific channel data - async UploadData(channelid, name, data) { + async UploadData(channelid, name, data, onprogress = null) { if (channelid.length === 0) { channelid = this.GetCurrentChannelID(); } @@ -2305,7 +2305,7 @@ class Restreamer { const path = `/channels/${channel.channelid}/${name}`; - await this._uploadAssetData(path, data); + await this._uploadAssetData(path, data, onprogress); return path; } @@ -3358,8 +3358,8 @@ class Restreamer { return true; } - async _uploadAssetData(remotePath, data) { - await this._call(this.api.DataPutFile, remotePath, data); + async _uploadAssetData(remotePath, data, onprogress = null) { + await this._call(this.api.DataPutFile, remotePath, data, onprogress); return true; } diff --git a/src/version.js b/src/version.js index b7c73df..9e95f2e 100644 --- a/src/version.js +++ b/src/version.js @@ -1,7 +1,7 @@ import pkg from '../package.json'; const Core = '^16.11.0'; -const FFmpeg = '^5.1.0 || ^6.1.0'; +const FFmpeg = '^5.1.0 || ^6.1.0 || ^7.0.0'; const UI = pkg.bundle ? pkg.bundle : pkg.name + ' v' + pkg.version; const Version = pkg.version; diff --git a/src/views/Edit/Profile.js b/src/views/Edit/Profile.js index b7b3e57..1f7d986 100644 --- a/src/views/Edit/Profile.js +++ b/src/views/Edit/Profile.js @@ -234,8 +234,8 @@ export default function Profile({ setSkillsRefresh(false); }; - const handleStore = async (name, data) => { - return await onStore(name, data); + const handleStore = async (name, data, onprogress) => { + return await onStore(name, data, onprogress); }; const handleEncoding = (type) => (encoder, decoder) => { diff --git a/src/views/Edit/SourceSelect.js b/src/views/Edit/SourceSelect.js index d92a590..2e52cd6 100644 --- a/src/views/Edit/SourceSelect.js +++ b/src/views/Edit/SourceSelect.js @@ -78,8 +78,8 @@ export default function SourceSelect({ await onRefresh(); }; - const handleStore = async (name, data) => { - return await onStore(name, data); + const handleStore = async (name, data, onprogress) => { + return await onStore(name, data, onprogress); }; const handleProbe = async (settings, inputs) => { diff --git a/src/views/Edit/Sources/ALSA.js b/src/views/Edit/Sources/ALSA.js index a2e9fd4..c57a5b1 100644 --- a/src/views/Edit/Sources/ALSA.js +++ b/src/views/Edit/Sources/ALSA.js @@ -156,7 +156,7 @@ function SourceIcon(props) { const id = 'alsa'; const name = ALSA; const capabilities = ['audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/AVFoundation.js b/src/views/Edit/Sources/AVFoundation.js index 1cde83b..45e739f 100644 --- a/src/views/Edit/Sources/AVFoundation.js +++ b/src/views/Edit/Sources/AVFoundation.js @@ -221,7 +221,7 @@ function SourceIcon(props) { const id = 'avfoundation'; const name = AVFoundation; const capabilities = ['audio', 'video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/AudioLoop.js b/src/views/Edit/Sources/AudioLoop.js index c88deca..23cefaf 100644 --- a/src/views/Edit/Sources/AudioLoop.js +++ b/src/views/Edit/Sources/AudioLoop.js @@ -4,12 +4,12 @@ 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 CircularProgress from '../../../misc/CircularProgress'; import Dialog from '../../../misc/modals/Dialog'; import Filesize from '../../../misc/Filesize'; import FormInlineButton from '../../../misc/FormInlineButton'; @@ -63,7 +63,10 @@ function Source({ const classes = useStyles(); settings = initSettings(settings); - const [$saving, setSaving] = React.useState(false); + const [$progress, setProgress] = React.useState({ + enable: false, + value: -1, + }); const [$error, setError] = React.useState({ open: false, title: '', @@ -71,7 +74,15 @@ function Source({ }); const handleFileUpload = async (data, extension, mimetype) => { - const path = await onStore('audioloop.source', data); + const path = await onStore('audioloop.source', data, (computable, progress, total) => { + setProgress((current) => { + return { + ...current, + enable: true, + value: computable ? progress * 100 : -1, + }; + }); + }); onChange({ ...settings, @@ -79,11 +90,17 @@ function Source({ mimetype: mimetype, }); - setSaving(false); + setProgress({ + ...$progress, + enable: false, + }); }; const handleUploadStart = () => { - setSaving(true); + setProgress({ + ...$progress, + enable: true, + }); }; const handleUploadError = (title) => (err) => { @@ -115,7 +132,10 @@ function Source({ message = Unknown upload error; } - setSaving(false); + setProgress({ + ...$progress, + enable: false, + }); showUploadError(title, message); }; @@ -166,8 +186,8 @@ function Source({ - - + + Loop; const capabilities = ['audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/Framebuffer.js b/src/views/Edit/Sources/Framebuffer.js index 8692429..5d31f14 100644 --- a/src/views/Edit/Sources/Framebuffer.js +++ b/src/views/Edit/Sources/Framebuffer.js @@ -130,7 +130,7 @@ function SourceIcon(props) { const id = 'fbdev'; const name = Framebuffer; const capabilities = ['video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/Network.js b/src/views/Edit/Sources/Network.js index 6d9188e..d52758d 100644 --- a/src/views/Edit/Sources/Network.js +++ b/src/views/Edit/Sources/Network.js @@ -1229,7 +1229,7 @@ function SourceIcon(props) { const id = 'network'; const name = Network source; const capabilities = ['audio', 'video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/NoAudio.js b/src/views/Edit/Sources/NoAudio.js index 51f7ec1..8e46997 100644 --- a/src/views/Edit/Sources/NoAudio.js +++ b/src/views/Edit/Sources/NoAudio.js @@ -37,7 +37,7 @@ function SourceIcon(props) { const id = 'noaudio'; const name = No audio; const capabilities = ['audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/Raspicam.js b/src/views/Edit/Sources/Raspicam.js index a35fc04..0d48ede 100644 --- a/src/views/Edit/Sources/Raspicam.js +++ b/src/views/Edit/Sources/Raspicam.js @@ -136,7 +136,7 @@ function SourceIcon(props) { const id = 'raspicam'; const name = Raspberry Pi camera; const capabilities = ['video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/SDP.js b/src/views/Edit/Sources/SDP.js index d9b93d1..32e85d3 100644 --- a/src/views/Edit/Sources/SDP.js +++ b/src/views/Edit/Sources/SDP.js @@ -193,7 +193,7 @@ function SourceIcon(props) { const id = 'sdp'; const name = SDP; const capabilities = ['video', 'audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/V4L.js b/src/views/Edit/Sources/V4L.js index 8f9b044..1a00678 100644 --- a/src/views/Edit/Sources/V4L.js +++ b/src/views/Edit/Sources/V4L.js @@ -145,7 +145,7 @@ function SourceIcon(props) { const id = 'video4linux2'; const name = Hardware device; const capabilities = ['video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/VideoAudio.js b/src/views/Edit/Sources/VideoAudio.js index 084d57d..d49adeb 100644 --- a/src/views/Edit/Sources/VideoAudio.js +++ b/src/views/Edit/Sources/VideoAudio.js @@ -37,7 +37,7 @@ function SourceIcon(props) { const id = 'videoaudio'; const name = Video source; const capabilities = ['audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/VideoLoop.js b/src/views/Edit/Sources/VideoLoop.js index b2db595..8da3198 100644 --- a/src/views/Edit/Sources/VideoLoop.js +++ b/src/views/Edit/Sources/VideoLoop.js @@ -4,12 +4,12 @@ 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 CircularProgress from '../../../misc/CircularProgress'; import Dialog from '../../../misc/modals/Dialog'; import Filesize from '../../../misc/Filesize'; import FormInlineButton from '../../../misc/FormInlineButton'; @@ -71,7 +71,10 @@ function Source({ const classes = useStyles(); settings = initSettings(settings); - const [$saving, setSaving] = React.useState(false); + const [$progress, setProgress] = React.useState({ + enable: false, + value: -1, + }); const [$error, setError] = React.useState({ open: false, title: '', @@ -79,7 +82,15 @@ function Source({ }); const handleFileUpload = async (data, extension, mimetype) => { - const path = await onStore('videoloop.source', data); + const path = await onStore('videoloop.source', data, (computable, progress, total) => { + setProgress((current) => { + return { + ...current, + enable: true, + value: computable ? progress * 100 : -1, + }; + }); + }); onChange({ ...settings, @@ -87,11 +98,17 @@ function Source({ mimetype: mimetype, }); - setSaving(false); + setProgress({ + ...$progress, + enable: false, + }); }; const handleUploadStart = () => { - setSaving(true); + setProgress({ + ...$progress, + enable: true, + }); }; const handleUploadError = (title) => (err) => { @@ -123,7 +140,10 @@ function Source({ message = Unknown upload error; } - setSaving(false); + setProgress({ + ...$progress, + enable: false, + }); showUploadError(title, message); }; @@ -174,8 +194,8 @@ function Source({ - - + + Loop; const capabilities = ['video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/VirtualAudio.js b/src/views/Edit/Sources/VirtualAudio.js index 7fd0b91..a84fa28 100644 --- a/src/views/Edit/Sources/VirtualAudio.js +++ b/src/views/Edit/Sources/VirtualAudio.js @@ -180,7 +180,7 @@ function SourceIcon(props) { const id = 'virtualaudio'; const name = Virtual source; const capabilities = ['audio']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/Sources/VirtualVideo.js b/src/views/Edit/Sources/VirtualVideo.js index b5a9baa..cb88aff 100644 --- a/src/views/Edit/Sources/VirtualVideo.js +++ b/src/views/Edit/Sources/VirtualVideo.js @@ -199,7 +199,7 @@ function SourceIcon(props) { const id = 'virtualvideo'; const name = Virtual source; const capabilities = ['video']; -const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0'; +const ffversion = '^4.1.0 || ^5.0.0 || ^6.1.0 || ^7.0.0'; const func = { initSettings, diff --git a/src/views/Edit/index.js b/src/views/Edit/index.js index cdbea29..2ab3773 100644 --- a/src/views/Edit/index.js +++ b/src/views/Edit/index.js @@ -211,8 +211,8 @@ export default function Edit({ restreamer = null }) { setSkills(skills); }; - const handleSourceStore = async (name, data) => { - return await restreamer.UploadData('', name, data); + const handleSourceStore = async (name, data, onprogress) => { + return await restreamer.UploadData('', name, data, onprogress); }; const handleSourceProbe = async (inputs) => {