openvibers/openrestream Commits

OpenRestream: the front page gains OBS settings and questions (FAQPage JSON-LD) (#59)

OP OpenVibers committed 12 Oct 2026 · 92e84b476b · 3 files, +33 −2

openre.stream's front page says more, all of it from what the README states:
- **OBS settings:** a block with the exact server (from `config.rtmp.publicHost`/`publicPort`, `rtmp://ingest.openre.stream:1936/live` in production) and where the key comes from;
- **Questions** (visible as `<details>` and as FAQPage JSON-LD): free and no minimum; RTMP, RTMPS and SRT destinations; no re-encode for an RTMP source (codec copy); going live from a browser on OpenVibe.Live; keys (stream key hashed, destination keys and SRT passphrases AES-256-GCM, shown masked); OpenVibe Live on by default.

Test: `discovery.test.js` checks the FAQPage JSON-LD, that every question appears on the page, and the server line. `destinations-ui` passes.

Changes

ddiff --git a/server/ui/layout.js b/server/ui/layout.js
iindex ec0440e..961f85a 100644
---- a/server/ui/layout.js
++++ b/server/ui/layout.js
@@@ -26,6 +26,8 @@ main.ore{max-width:1040px;margin:0 auto;padding:24px 16px 64px}
.ore h1{font-size:1.6rem;margin:.2em 0 .4em}.ore h2{font-size:1.15rem;margin:1.6em 0 .6em}.ore h3{font-size:1rem;margin:1em 0 .4em}
.ore a{color:#93c5fd}.ore p.muted,.ore .muted{color:var(--ore-muted)}
.ore .card{background:var(--ore-panel);border:1px solid var(--ore-line);border-radius:10px;padding:14px 16px;margin:12px 0}
++.ore-faq details{border-bottom:1px solid var(--ore-line);padding:.75rem 0}.ore-faq details:first-of-type{border-top:1px solid var(--ore-line)}
++.ore-faq summary{cursor:pointer;font-weight:600;list-style-position:inside}.ore-faq summary:hover{color:#93c5fd}.ore-faq details p{margin:.5rem 0 0;color:var(--ore-muted);max-width:46rem}
.ore table{width:100%;border-collapse:collapse;font-size:.92rem}.ore th,.ore td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--ore-line);vertical-align:top}
.ore code,.ore .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88rem;word-break:break-all}
.ore .secret{display:block;background:#0a0f18;border:1px dashed var(--ore-warn);padding:10px;border-radius:8px;user-select:all}
ddiff --git a/server/ui/routes.js b/server/ui/routes.js
iindex 1bdeaef..fa41d6f 100644
---- a/server/ui/routes.js
++++ b/server/ui/routes.js
@@@ -33,6 +33,16 @@ const pill = (state) => `<span class="pill ${esc(state)}">${esc(state)}</span>`;
// one place (server/ui/layout.js clips it to 160 for the meta tags).
const FRONT_PAGE_DESCRIPTION = 'OpenRestream: open restreaming. Go live once from OBS and stream to Twitch, YouTube, Kick and any RTMP or SRT server at the same time, with OpenVibe Live on by default. Free, open source, no follower minimum.';
++// The front page's questions, answered from what OpenRestream does today (README): shown as a list and as FAQPage JSON-LD.
++const FRONT_FAQ = [
++ { q: 'Is OpenRestream free?', a: 'Yes. Any OpenVibe account can restream from the first minute, with no follower, subscriber or eligibility minimum. The code is open source (AGPL-3.0).' },
++ { q: 'Which platforms can I stream to?', a: 'Any platform that takes RTMP, RTMPS or SRT: Twitch, YouTube, Kick and others. Add each destination\'s server and key once; every destination has its own health, logs and retries.' },
++ { q: 'Does OpenRestream re-encode my stream?', a: 'Not for a stream sent from OBS or another RTMP encoder: it is copied to every destination as it arrives, so each platform gets the quality you sent.' },
++ { q: 'Can I go live without OBS?', a: 'Yes. On OpenVibe.Live you can go live from your browser, with nothing to install and no follower minimum.' },
++ { q: 'Are my keys safe?', a: 'Your OpenRestream stream key is shown once and stored only as a hash; rotate it any time. Destination keys and SRT passphrases are encrypted (AES-256-GCM), the API only ever shows their last four characters, and logged command lines are redacted.' },
++ { q: 'Does my stream also appear on OpenVibe Live?', a: 'By default, yes: every stream also goes live on your openvibe.live channel with chat, clips and VODs. Switch it off for any stream.' },
++];
++
function csrfFor(token) { return crypto.createHash('sha256').update(`openre-csrf:${token}`).digest('hex').slice(0, 32); }
function createUiRouter({ rt, auth, limits = null, liveLink = null }) {
@@@ -103,7 +113,7 @@ function createUiRouter({ rt, auth, limits = null, liveLink = null }) {
// Structured data for the front page only: the site is a web application (openre.stream).
jsonLd: [seo.jsonLd.softwareApp({
name: 'OpenRestream', url: `${config.baseUrl}/`, description: FRONT_PAGE_DESCRIPTION, category: 'MultimediaApplication',
-- })],
++ }), seo.jsonLd.faq(FRONT_FAQ)],
body: `${frontPage(config)}
${signedIn ? `<h2>Your streams</h2>${mine.length ? await streamTable(mine) : '<p class="muted">No streams yet.</p>'}<p><a href="/streams">Manage streams</a></p>` : '<p><a href="/auth/login?next=/streams">Sign in with OpenVibe</a> to manage your streams.</p>'}
${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRestream' })}`,
@@@ -115,6 +125,13 @@ ${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRestream' }
* default (owner, 2026-10-09). It says that RTMP ingest and RTMP/RTMPS/SRT restreaming run here, and that going live
* from a browser happens on OpenVibe.Live for now (the WebRTC and JSMPEG workers are ported but not deployed).
*/
++ // What streamers paste into OBS (config.rtmp.publicHost/publicPort): rtmp://<host>[:port]/live.
++ const ingestServer = (cfg) => {
++ const r = cfg.rtmp || {};
++ const host = r.publicHost || 'ingest.openre.stream';
++ return `rtmp://${host}${r.publicPort && Number(r.publicPort) !== 1935 ? `:${r.publicPort}` : ''}/live`;
++ };
++
function frontPage(cfg) {
const live = cfg.liveUrl;
return showcase.hero({
@@@ -145,7 +162,13 @@ ${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRestream' }
{ title: 'Add destinations', text: 'The server and key of each platform you also stream to. Test each one before you go live.' },
{ title: 'Go live from OBS', text: 'Every destination, and your OpenVibe Live channel, shows its health while you stream.' },
],
-- });
++ }) + showcase.code({
++ title: 'OBS settings',
++ lede: 'In OBS, open Settings → Stream, choose Custom, and paste the server and the key from your stream\'s page.',
++ samples: [{ label: 'Settings → Stream', code: `Service: Custom...\nServer: ${ingestServer(cfg)}\nStream Key: (from your stream's page, shown once)` }],
++ }) + `<section class="sc-sec ore-faq" aria-labelledby="ore-faq-h"><h2 id="ore-faq-h">Questions</h2>
++${FRONT_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('\n')}
++</section>`;
}
async function streamTable(list) {
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex e8eb439..d82c02e 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -77,6 +77,12 @@ t('the front page leads with open restreaming, links to Live for browser go-live
assert.strictEqual(app['@type'], 'WebApplication');
assert.strictEqual(app.name, 'OpenRestream');
assert.strictEqual(app.applicationCategory, 'MultimediaApplication');
++ // The questions, as a visible list and as FAQPage JSON-LD; the OBS settings name the ingest server.
++ const all = [...r.text.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/gs)].map((m) => JSON.parse(m[1]));
++ const faq = all.find((x) => x['@type'] === 'FAQPage');
++ assert.ok(faq && faq.mainEntity.length >= 5, 'FAQPage JSON-LD');
++ assert.ok(faq.mainEntity.every((q) => r.text.includes(`<summary>${q.name.replace(/'/g, '&#39;')}</summary>`) || r.text.includes(`<summary>${q.name}</summary>`)), 'every JSON-LD question is on the page');
++ assert.match(r.text, /Server: +rtmp:\/\/[^\s<]+\/live/);
} finally {
await api.close();
}