diff --git a/CHANGELOG.md b/CHANGELOG.md
index 112c970..5ef2a15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
#### v1.1.0 > v1.2.0
+- Add allow writing HLS to disk
- Add audio pan filter
- Add video rotation filter ([#347](https://github.com/datarhei/restreamer/discussions/347))
- Add video h/v flip filter
@@ -27,6 +28,7 @@
- Fix VAAPI encoder
Dependency:
+
- datarhei Core v16.9.0+
#### v1.0.0 > v1.1.0
diff --git a/src/misc/controls/HLS.js b/src/misc/controls/HLS.js
index d97c028..0281590 100644
--- a/src/misc/controls/HLS.js
+++ b/src/misc/controls/HLS.js
@@ -61,6 +61,19 @@ export default function Control(props) {
*/}
+
+
+
+ Where to store the HLS playlist and segments.
+
+
- Master playlist (increases browser/client compatibility)} checked={settings.master_playlist} onChange={handleChange('master_playlist')} />
+ Master playlist (increases browser/client compatibility)}
+ checked={settings.master_playlist}
+ onChange={handleChange('master_playlist')}
+ />
Automatic cleanup of all media data} checked={settings.cleanup} onChange={handleChange('cleanup')} />
diff --git a/src/misc/controls/Source.js b/src/misc/controls/Source.js
index 2c7cae5..3c08432 100644
--- a/src/misc/controls/Source.js
+++ b/src/misc/controls/Source.js
@@ -44,37 +44,29 @@ export default function Control(props) {
const items = [];
- if (props.sources.includes('hls+memfs')) {
- items.push(
-
- );
- }
+ items.push(
+
+ );
- if (props.sources.includes('hls+diskfs')) {
- items.push(
-
- );
- }
+ items.push(
+
+ );
- if (props.sources.includes('rtmp')) {
- items.push(
-
- );
- }
+ items.push(
+
+ );
- if (props.sources.includes('srt')) {
- items.push(
-
- );
- }
+ items.push(
+
+ );
return (
diff --git a/src/utils/restreamer.js b/src/utils/restreamer.js
index a96eb7d..747dedf 100644
--- a/src/utils/restreamer.js
+++ b/src/utils/restreamer.js
@@ -936,16 +936,16 @@ class Restreamer {
return await this._getResources();
}
- // Get all HTTP addresses
- GetHTTPAddresses() {
+ // Get the public HTTP address
+ GetPublicHTTPAddress() {
const config = this.ConfigActive();
const address = (config.http.secure === true ? 'https://' : 'http://') + config.http.host;
- return [address];
+ return address;
}
// Get all RTMP/SRT/SNAPSHOT+MEMFS/HLS+MEMFS addresses
- GetAddresses(what, channelId) {
+ GetPublicAddress(what, channelid) {
const config = this.ConfigActive();
const host = config.hostname;
@@ -963,7 +963,7 @@ class Restreamer {
}
}
- if (what && what === 'rtmp') {
+ if (what === 'rtmp') {
// rtmp/s
const cfg = config.source.network.rtmp;
const port = getPort(cfg.host);
@@ -977,31 +977,56 @@ class Restreamer {
`://${host}${port}` +
(cfg.app.length !== 0 ? cfg.app : '') +
'/' +
- channelId +
+ channelid +
'.stream' +
(cfg.token.length !== 0 ? `?token=${cfg.token}` : '');
- } else if (what && what === 'srt') {
+ } else if (what === 'srt') {
// srt
const cfg = config.source.network.srt;
const port = getPort(cfg.host);
address =
- `srt://${host}${port}/?mode=caller&transtype=live&streamid=#!:m=request,r=${channelId}` +
+ `srt://${host}${port}/?mode=caller&transtype=live&streamid=#!:m=request,r=${channelid}` +
(cfg.token.length !== 0 ? `,token=${cfg.token}` : '') +
(cfg.passphrase.length !== 0 ? `&passphrase=${cfg.passphrase}` : '');
- } else if (what && what === 'snapshot+memfs') {
+ } else if (what === 'snapshot+memfs') {
// snapshot+memfs
const port = getPort(config.source.network.hls.host);
- address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/memfs/${channelId}.jpg`;
- } else {
+ address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/` + this.GetChannelPosterPath(channelid, 'memfs');
+ } else if (what === 'snapshot+diskfs') {
+ // snapshot+diskfs
+ const port = getPort(config.source.network.hls.host);
+
+ address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/` + this.GetChannelPosterPath(channelid, 'diskfs');
+ } else if (what === 'hls+memfs') {
// hls+memfs
const port = getPort(config.source.network.hls.host);
- address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/memfs/${channelId}.m3u8`;
+ address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/` + this.GetChannelManifestPath(channelid, 'memfs');
+ } else if (what === 'hls+diskfs') {
+ // hls+diskfs
+ const port = getPort(config.source.network.hls.host);
+
+ address = (config.http.secure === true ? 'https://' : 'http://') + `${host}${port}/` + this.GetChannelManifestPath(channelid, 'diskfs');
+ } else if (what === 'player') {
+ // player
+ address = (config.http.secure === true ? 'https://' : 'http://') + `${config.http.host}/` + this.GetChannelPlayerPath(channelid);
}
- return [address];
+ return address;
+ }
+
+ // Get the iframe codes for the player
+ GetPublicIframeCode(channelid) {
+ const channel = this.GetChannel(channelid);
+ if (channel === null) {
+ return '';
+ }
+
+ const address = this.GetPublicHTTPAddress();
+
+ return ``;
}
// Channels
@@ -1182,7 +1207,7 @@ class Restreamer {
channelid: channel.channelid,
name: channel.name,
available: channel.available,
- thumbnail: this.Address() + '/' + this.GetChannelPosterUrl(channel.channelid),
+ thumbnail: this.GetChannelAddress('snapshot+memfs', channel.channelid),
egresses: Array.from(channel.egresses.keys()),
});
}
@@ -1201,7 +1226,7 @@ class Restreamer {
channelid: channel.channelid,
name: channel.name,
available: channel.available,
- thumbnail: this.Address() + '/' + this.GetChannelPosterUrl(channel.channelid),
+ thumbnail: this.GetChannelAddress('snapshot+memfs', channel.channelid),
egresses: Array.from(channel.egresses.keys()),
};
}
@@ -1267,16 +1292,46 @@ class Restreamer {
return this.channel.channelid;
}
- // Get the URL for the stream
- GetChannelManifestUrl(channelid) {
- return `memfs/${channelid}.m3u8`;
+ // Get the path for the HLS manifest
+ GetChannelManifestPath(channelid, storage) {
+ if (!storage) {
+ storage = 'memfs';
+ }
+
+ let url = `${channelid}.m3u8`;
+ if (storage === 'memfs') {
+ url = 'memfs/' + url;
+ }
+
+ return url;
}
- // Get the URL for the poster image
- GetChannelPosterUrl(channelid) {
+ // Get the path for the poster image
+ GetChannelPosterPath(channelid, storage) {
return `memfs/${channelid}.jpg`;
}
+ // Get the path for the player
+ GetChannelPlayerPath(channelid) {
+ return `${channelid}.html`;
+ }
+
+ GetChannelAddress(what, channelid) {
+ const address = this.Address();
+
+ if (what === 'hls+memfs') {
+ return `${address}/${this.GetChannelManifestPath(channelid, 'memfs')}`;
+ } else if (what === 'hls+diskfs') {
+ return `${address}/${this.GetChannelManifestPath(channelid, 'diskfs')}`;
+ } else if (what === 'snapshot+memfs') {
+ return `${address}/${this.GetChannelPosterPath(channelid, 'memfs')}`;
+ } else if (what === 'snapshot+diskfs') {
+ return `${address}/${this.GetChannelPosterPath(channelid, 'diskfs')}`;
+ } else if (what === 'player') {
+ return `${address}/${this.GetChannelPlayerPath(channelid)}`;
+ }
+ }
+
// Sessions
async CurrentSessions() {
@@ -1421,59 +1476,6 @@ class Restreamer {
return await this.GetDebug(channel.id);
}
- GetIngestAddresses(channelid) {
- const channel = this.GetChannel(channelid);
- if (channel === null) {
- return [];
- }
-
- const addresses = this.GetHTTPAddresses();
-
- return addresses.map((address) => {
- return `${address}/${channel.channelid}.html`;
- });
- }
-
- // Get the iframe codes for the player
- GetIngestIframeCodes(channelid) {
- const channel = this.GetChannel(channelid);
- if (channel === null) {
- return [];
- }
-
- const addresses = this.GetHTTPAddresses();
-
- const codes = [];
-
- for (let address of addresses) {
- codes.push(
- ``
- );
- }
-
- return codes;
- }
-
- // Get the URL for the HLS manifest
- GetIngestManifestUrl(channelid) {
- return this.GetChannelManifestUrl(channelid);
- }
-
- // Get the URL for poster image
- GetIngestPosterUrl(channelid) {
- return this.GetChannelPosterUrl(channelid);
- }
-
- // Get the URL for poster image
- GetIngestPosterUrlAddresses(channelid) {
- const poster = this.GetChannelPosterUrl(channelid);
- const addresses = this.GetHTTPAddresses();
-
- return addresses.map((address) => {
- return `${address}/${poster}`;
- });
- }
-
// Start the ingest process
async StartIngest(channelid) {
const channel = this.GetChannel(channelid);
@@ -1537,7 +1539,7 @@ class Restreamer {
// Upsert the ingest process
async UpsertIngest(channelid, global, inputs, outputs, control) {
const channel = this.GetChannel(channelid);
- if (channel === null) {
+ if (!channel) {
return [null, { message: 'Unknown channel ID' }];
}
@@ -1602,7 +1604,7 @@ class Restreamer {
}
// Injects a metadata link as title
- const metadata = `${this.GetHTTPAddresses()[0]}/${channel.channelid}/oembed.json`;
+ const metadata = `${this.GetPublicHTTPAddress()}/${channel.channelid}/oembed.json`;
const metadata_options = ['-metadata', `title=${metadata}`, '-metadata', 'service_provider=datarhei-Restreamer'];
output.options.push(...metadata_options);
@@ -1766,7 +1768,8 @@ class Restreamer {
return [null, { message: 'Unknown channel ID' }];
}
- const hlsStore = 'memfs';
+ // Set hls storage endpoint
+ const hlsStorage = control.hls.storage;
const snapshot = {
type: 'ffmpeg',
@@ -1775,7 +1778,7 @@ class Restreamer {
input: [
{
id: 'input_0',
- address: `{${hlsStore}}/${channel.channelid}.m3u8`,
+ address: `{${hlsStorage}}/${channel.channelid}.m3u8`,
options: [],
},
],
@@ -1980,11 +1983,11 @@ class Restreamer {
name: metadata.meta.name,
description: metadata.meta.description,
author_name: metadata.meta.author.name,
- author_url: this.GetIngestAddresses(channelid)[0],
+ author_url: this.GetPublicAddress('player', channelid),
license: metadata.license,
- iframecode: this.GetIngestIframeCodes(channelid)[0],
- poster: this.GetIngestPosterUrl(channelid),
- poster_url: this.GetIngestPosterUrlAddresses(channelid)[0],
+ iframecode: this.GetPublicIframeCode(channelid),
+ poster: this.GetChannelPosterPath(channelid, metadata.control.hls.storage),
+ poster_url: this.GetPublicAddress('snapshot+memfs', channelid),
width: 640,
height: 360,
chromecast: metadata.player.chromecast,
@@ -2026,8 +2029,8 @@ class Restreamer {
const playerConfig = {
...metadata.player,
- source: this.GetIngestManifestUrl(channelid),
- poster: this.GetIngestPosterUrl(channelid),
+ source: this.GetChannelManifestPath(channelid, metadata.control.hls.storage),
+ poster: this.GetChannelPosterPath(channelid, metadata.control.hls.storage),
license: {
license: metadata.license,
title: metadata.meta.name,
@@ -2181,7 +2184,7 @@ class Restreamer {
channel_creator_description: ingestMetadata.meta.author.description,
channel_creator_description_html: ingestMetadata.meta.author.description.replace(/(?:\r\n|\r|\n)/g, '
'),
channel_license: ingestMetadata.license,
- channel_poster: this.GetIngestPosterUrl(item.channelid),
+ channel_poster: this.GetChannelPosterPath(item.channelid, ingestMetadata.control.hls.storage),
channel_width: 640,
channel_height: 360,
};
@@ -2830,9 +2833,20 @@ class Restreamer {
return null;
}
- const regex = /([a-z]+):\/\/[^/]+(?:\/[0-9A-Za-z-_.~/%:=&?]+)?/gm;
+ const regex = /(?:([a-z]+):)?\/[^\s]*/gm;
const replace = (s) => {
- return s.replaceAll(regex, '$1://[anonymized]');
+ return s.replaceAll(regex, (match, scheme) => {
+ if (scheme) {
+ return `${scheme}://[anonymized]`;
+ }
+
+ const pathElm = match.split('/').filter((p) => p.length !== 0);
+ if (pathElm.length < 2) {
+ return match;
+ }
+
+ return `/[anonymized]/${pathElm.pop()}`;
+ });
};
if (p.config) {
diff --git a/src/views/Edit/Sources/Network.js b/src/views/Edit/Sources/Network.js
index d7f6a06..15cae91 100644
--- a/src/views/Edit/Sources/Network.js
+++ b/src/views/Edit/Sources/Network.js
@@ -360,19 +360,19 @@ const getSRTAddress = (host, name, token, passphrase) => {
const getHLS = (config, name) => {
const url = getHLSAddress(config.hls.host, config.hls.credentials, config.hls.name, config.hls.secure);
- return [url];
+ return url;
};
const getRTMP = (config) => {
const url = getRTMPAddress(config.rtmp.host, config.rtmp.app, config.rtmp.name, config.rtmp.token, config.rtmp.secure);
- return [url];
+ return url;
};
const getSRT = (config) => {
const url = getSRTAddress(config.srt.host, config.srt.name, config.srt.token, config.srt.passphrase);
- return [url];
+ return url;
};
const getLocalHLS = (config, name) => {
@@ -639,7 +639,7 @@ function PushHLS(props) {
const classes = useStyles();
const config = props.config;
- const HLSs = getHLS(config);
+ const HLS = getHLS(config);
return (
@@ -650,7 +650,7 @@ function PushHLS(props) {
-
+
@@ -685,7 +685,7 @@ function PushRTMP(props) {
);
} else {
- const RTMPs = getRTMP(config);
+ const RTMP = getRTMP(config);
form = (
@@ -696,7 +696,7 @@ function PushRTMP(props) {
-
+
@@ -734,7 +734,7 @@ function PushSRT(props) {
);
} else {
- const SRTs = getSRT(config);
+ const SRT = getSRT(config);
form = (
@@ -745,7 +745,7 @@ function PushSRT(props) {
-
+
diff --git a/src/views/Edit/Wizard/Sources/InternalHLS.js b/src/views/Edit/Wizard/Sources/InternalHLS.js
index a88bc95..1f71338 100644
--- a/src/views/Edit/Wizard/Sources/InternalHLS.js
+++ b/src/views/Edit/Wizard/Sources/InternalHLS.js
@@ -39,7 +39,7 @@ function Source(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const HLSs = S.func.getHLS(config, settings.push.name);
+ const HLS = S.func.getHLS(config, settings.push.name);
return (
@@ -48,13 +48,11 @@ function Source(props) {
Send stream to this address:
- {HLSs.length !== 0 && (
-
-
-
-
-
- )}
+
+
+
+
+
);
}
diff --git a/src/views/Edit/Wizard/Sources/InternalRTMP.js b/src/views/Edit/Wizard/Sources/InternalRTMP.js
index db09214..4e88a9e 100644
--- a/src/views/Edit/Wizard/Sources/InternalRTMP.js
+++ b/src/views/Edit/Wizard/Sources/InternalRTMP.js
@@ -59,7 +59,7 @@ function Source(props) {
);
}
- const RTMPs = S.func.getRTMP(config, settings.push.name);
+ const RTMP = S.func.getRTMP(config, settings.push.name);
return (
@@ -68,13 +68,11 @@ function Source(props) {
Send stream to this address:
- {RTMPs.length !== 0 && (
-
-
-
-
-
- )}
+
+
+
+
+
);
}
diff --git a/src/views/Edit/Wizard/Sources/InternalSRT.js b/src/views/Edit/Wizard/Sources/InternalSRT.js
index 3793f20..b260ff6 100644
--- a/src/views/Edit/Wizard/Sources/InternalSRT.js
+++ b/src/views/Edit/Wizard/Sources/InternalSRT.js
@@ -59,7 +59,7 @@ function Source(props) {
);
}
- const SRTs = S.func.getSRT(config, settings.push.name);
+ const SRT = S.func.getSRT(config, settings.push.name);
return (
@@ -68,13 +68,11 @@ function Source(props) {
Send stream to this address:
- {SRTs.length !== 0 && (
-
-
-
-
-
- )}
+
+
+
+
+
);
}
diff --git a/src/views/Main/index.js b/src/views/Main/index.js
index 68d14ef..6e86de1 100644
--- a/src/views/Main/index.js
+++ b/src/views/Main/index.js
@@ -82,7 +82,6 @@ export default function Main(props) {
const [$config, setConfig] = React.useState(null);
const navigate = useNavigate();
- const address = props.restreamer.Address() + '/';
useInterval(async () => {
await update();
@@ -282,9 +281,10 @@ export default function Main(props) {
return null;
}
+ const storage = $metadata.control.hls.storage;
const channel = props.restreamer.GetChannel(_channelid);
- const manifest = props.restreamer.GetIngestManifestUrl(_channelid);
- const poster = props.restreamer.GetIngestPosterUrl(_channelid);
+ const manifest = props.restreamer.GetChannelAddress('hls+' + storage, _channelid);
+ const poster = props.restreamer.GetChannelAddress('snapshot+' + storage, _channelid);
let title = Main channel;
if (channel && channel.name && channel.name.length !== 0) {
@@ -380,7 +380,7 @@ export default function Main(props) {
)}
{$state.state === 'connected' && (
-
+
)}
@@ -398,7 +398,7 @@ export default function Main(props) {
variant="outlined"
color="default"
size="small"
- value={props.restreamer.GetAddresses('hls+memfs', _channelid)}
+ value={props.restreamer.GetPublicAddress('hls+' + storage, _channelid)}
>
HLS
@@ -407,7 +407,7 @@ export default function Main(props) {
variant="outlined"
color="default"
size="small"
- value={props.restreamer.GetAddresses('rtmp', _channelid)}
+ value={props.restreamer.GetPublicAddress('rtmp', _channelid)}
>
RTMP
@@ -417,7 +417,7 @@ export default function Main(props) {
variant="outlined"
color="default"
size="small"
- value={props.restreamer.GetAddresses('srt', _channelid)}
+ value={props.restreamer.GetPublicAddress('srt', _channelid)}
>
SRT
@@ -426,7 +426,7 @@ export default function Main(props) {
variant="outlined"
color="default"
size="small"
- value={props.restreamer.GetAddresses('snapshot+memfs', _channelid)}
+ value={props.restreamer.GetPublicAddress('snapshot+memfs', _channelid)}
>
Snapshot
diff --git a/src/views/Publication/Player.js b/src/views/Publication/Player.js
index fd397ec..b33143f 100644
--- a/src/views/Publication/Player.js
+++ b/src/views/Publication/Player.js
@@ -64,15 +64,12 @@ export default function Edit(props) {
const { channelid: _channelid } = useParams();
const { i18n } = useLingui();
const address = props.restreamer.Address();
- const iframeCodes = props.restreamer.GetIngestIframeCodes(_channelid);
- const manifest = props.restreamer.GetIngestManifestUrl(_channelid);
- const poster = props.restreamer.GetIngestPosterUrl(_channelid);
const timeout = React.useRef();
const notify = React.useContext(NotifyContext);
const [$player] = React.useState('videojs-public');
const [$ready, setReady] = React.useState(false);
const [$state, setState] = React.useState('disconnected');
- const [$data, setData] = React.useState({});
+ const [$metadata, setMetadata] = React.useState({});
const [$settings, setSettings] = React.useState({});
const [$tab, setTab] = React.useState('embed');
const [$revision, setRevision] = React.useState(0);
@@ -104,7 +101,7 @@ export default function Edit(props) {
return;
}
- setData(proc.metadata);
+ setMetadata(proc.metadata);
setState(proc.progress.state);
setSettings(props.restreamer.InitPlayerSettings(proc.metadata.player));
@@ -244,12 +241,12 @@ export default function Edit(props) {
const handleDone = async () => {
setSaving(true);
- const data = {
- ...$data,
+ const metadata = {
+ ...$metadata,
player: $settings,
};
- await props.restreamer.SetIngestMetadata(_channelid, data);
+ await props.restreamer.SetIngestMetadata(_channelid, metadata);
await props.restreamer.UpdatePlayer(_channelid);
setSaving(false);
@@ -279,6 +276,12 @@ export default function Edit(props) {
return null;
}
+ const storage = $metadata.control.hls.storage;
+ const manifest = props.restreamer.GetChannelAddress('hls+' + storage, _channelid);
+ const poster = props.restreamer.GetChannelAddress('snapshot+' + storage, _channelid);
+ const playerAddress = props.restreamer.GetPublicAddress('player');
+ const iframeCode = props.restreamer.GetPublicIframeCode(_channelid);
+
return (
@@ -299,10 +302,10 @@ export default function Edit(props) {
- Player URL} value={address + '/' + _channelid + '.html'} />
+ Player URL} value={playerAddress} />
- iframe code} value={iframeCodes.join('\n')} />
+ iframe code} value={iframeCode} />