openvibers/openrestream Commits

OpenRe.Stream: a showcase home that leads with browser go-live; llms.txt says only what runs today. (#14)

OP OpenVibers committed 7 Oct 2026 · 9271a1f9ee · 7 files, +80 −23

The openre.stream front page is now a product home built with the shared showcase kit (openvibe-shared v2.10.1) that leads with going live from a browser with no OBS and no follower minimum, and llms.txt now says only what runs in production.

Correction: llms.txt (shipped in #13) described browser WHIP ingest at ingest.openre.stream and JSMPEG ingest at :9736 as available. Production runs only openre-rtmp-ingest and openre-restream-worker (ovhost health openre, 2026-10-07): the WebRTC and JSMPEG workers are ported and tested but not deployed, and browser go-live runs on OpenVibe.Live. llms.txt now says so, restream lists RTMP and SRT destinations, and recording is for RTMP sessions.

- server/ui/routes.js: frontPage() renders a hero ("Go live from your browser. No OBS, no follower minimum."; Open OpenVibe.Live and press Go Live, then what OpenRe is; actions Go live in your browser -> openvibe.live, Restream with OBS -> /streams, Read the guide -> openvibe.live/docs/go-live-in-your-browser; the alpha note keeps the old page line that WHIP and JSMPEG are still served by Live), a "What OpenRe does" grid (hashed, rotatable keys; several RTMP/SRT destinations with health, logs, backoff; per-stream recording choice; no follower or eligibility threshold) and "Restream with OBS" steps. The signed-in "Your streams" table and shipped feed stay below.
- server/ui/layout.js: renderPage takes a description (the home leads with browser go-live) and openvibe-shared stylesheet names.
- server/discovery.js: llms.txt summary and details corrected as above.
- test/discovery.test.js: llms.txt leads with browser go-live and states the ported-not-deployed workers; a new front page test (hero h1, description, primary action to Live, the alpha note, showcase.css, one h1).
- package.json / lockfile, STATUS.json: openvibe-shared v2.5.0 -> v2.10.1.

Tests: ov test test/discovery.test.js test/destinations-ui.test.js test/asset-cache.test.js: 3 passed.

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex 880ede5..7e20a81 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -8,7 +8,7 @@
"port": 4500,
"rtmpPort": 1936,
"domain": "openre.stream",
-- "contracts": "openvibe-contracts v0.76.0 (openvibe-sdk v0.26.0, openvibe-shared v2.5.0)",
++ "contracts": "openvibe-contracts v0.76.0 (openvibe-sdk v0.26.0, openvibe-shared v2.10.1)",
"protocols": {
"rtmp": "ingest ported (openre-rtmp-ingest)",
"restream": "RTMP source to RTMP/RTMPS/SRT destinations ported (openre-restream-worker)",
@@@ -28,7 +28,7 @@
"livePatch": "applied in Live f0ca18b and deployed; Live wired on 2026-09-23 (OPENRE_URL and the openre.session.* subscription, docs/cutover.md phase A); every Live slot keeps ingest_authority=live until its per-slot cutover (phase B), so no broadcaster uses OpenRe yet",
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy openre` (OpenVibe.Host strategy release-layout since the 2026-09-27 cutover; deploy/scripts/deploy.sh is the wrapper and keeps workers/status/prune): API + coordinator on 127.0.0.1:4500, RTMP ingest public on 0.0.0.0:1936 (ingest.openre.stream, DNS-only; reachable from outside since 2026-09-23), Live wired (OPENRE_URL, openre.session.* subscription), preflight all PASS, restore drill passed. No slot switched yet: every slot is still ingested by Live until the per-slot cutover (docs/cutover.md phase B).",
-- "updated": "2026-09-28",
++ "updated": "2026-10-07",
"features": [
"stream definitions owned by a Network subject, with typed references",
"ingest keys hashed at rest, shown once, rotation with grace",
ddiff --git a/package-lock.json b/package-lock.json
iindex 56e4a9b..70b983b 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -18,7 +18,7 @@
"node-media-server": "2.7.4",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0",
"sdp-transform": "^3.0.0",
"ws": "^8.18.0"
@@@ -1863,9 +1863,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.5.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
-- "integrity": "sha512-fHuUS6SXM7ezatfAyqLm1V8B6puV9YXSnz6aG6XQbWx28qghRCCHeUF3lr4nxiCGdn9SyikYtIyLGL5r1ycp9Q==",
++ "version": "2.10.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
++ "integrity": "sha512-6fUOw22bSvxg0K33BefAyAkNRkETe9euLNT0CHs2mLIvzYR1q38KiHRUQIWj8FESbVANF0pXMojhBz5TeqpMtg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 5b19c97..c7c9d84 100644
---- a/package.json
++++ b/package.json
@@@ -34,7 +34,7 @@
"node-media-server": "2.7.4",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0",
"sdp-transform": "^3.0.0",
"ws": "^8.18.0"
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 5663e1e..16a5e9c 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -32,18 +32,18 @@ function createDiscoveryRoutes({ config }) {
router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders()).send(
seo.llmsTxt({
name: 'OpenRe.Stream',
-- summary: 'Live ingest and restream for the OpenVibe network. Go live from a browser over WHIP (WebRTC) with no OBS or encoder install, or publish with an RTMP encoder such as OBS or an ffmpeg MPEG-TS (JSMPEG) feed; OpenRe carries the session and restreams it to RTMP, RTMPS and SRT destinations.',
++ summary: 'Go live from your browser with no OBS and no follower minimum: on the OpenVibe network you open openvibe.live, press Go Live and allow your camera. OpenRe.Stream is the network\'s ingest and restream service behind it: it takes an RTMP feed from an encoder such as OBS and restreams the session to RTMP and SRT destinations.',
details: [
-- 'OpenRe.Stream is the ingest and restream authority behind OpenVibe.Live: channels, discovery and watch pages stay on openvibe.live, which plays an OpenRe session from its playback descriptor. OpenRe itself serves a JSON/REST API and a small server-rendered owner UI at openre.stream.',
++ 'OpenRe.Stream is the ingest and restream authority behind OpenVibe.Live: channels, discovery and watch pages stay on openvibe.live. OpenRe itself serves a JSON/REST API and a small server-rendered owner UI at openre.stream. The service is alpha.',
'',
-- 'Ingest options, as the code and README describe them today:',
-- '- Browser WebRTC (WHIP, RFC 9725): POST /whip/<key> on ingest.openre.stream, one mediasoup router per session and WebSocket viewer signaling at /w/<session id>. The /b/<key> broadcaster signaling endpoint is what the OpenVibe.Live broadcast page speaks, so a streamer can go live from a browser without OBS.',
-- '- RTMP: any RTMP encoder, e.g. OBS, to rtmp://ingest.openre.stream:1936/live with the stream key shown once when the stream is created.',
-- '- JSMPEG: an ffmpeg MPEG-TS HTTP POST to ingest.openre.stream:9736/<key>/<width>/<height>/ with WebSocket viewers; JSMPEG sessions are not recorded.',
++ 'Going live, as it runs today:',
++ '- From a browser, with nothing to install: on OpenVibe.Live (https://openvibe.live, the Go Live button; guide at https://openvibe.live/docs/go-live-in-your-browser). There is no follower, subscriber or eligibility threshold.',
++ '- With an encoder such as OBS: RTMP to rtmp://ingest.openre.stream:1936/live with the stream key OpenRe shows once when the stream is created. Any signed-in OpenVibe account can create a stream.',
++ '- Browser WebRTC (WHIP, RFC 9725) and JSMPEG ingest are ported to OpenRe and tested, but their workers do not run in production yet; until they do, OpenVibe.Live serves them.',
'',
-- 'Restream: a session can fan out to RTMP, RTMPS and SRT destinations (Twitch, YouTube, Kick or a custom server) with per-output health, logs, backoff and a rapid-crash circuit breaker. Recording: OpenRe asks OpenVibe.Media to record a VOD or clips for RTMP and WebRTC sessions (not JSMPEG). Playback: a live, non-private session is served as HTTP-FLV at https://openre.stream/play/<session id>.flv.',
++ 'Restream: a session can fan out to several RTMP and SRT destinations at once, with per-output health, logs, backoff and a rapid-crash circuit breaker. Recording: OpenRe asks OpenVibe.Media to record a VOD or clips of an RTMP session. Playback: a live, non-private session is served as HTTP-FLV at https://openre.stream/play/<session id>.flv.',
'',
-- 'Ingest keys are random, stored only as a SHA-256 hash and shown once at creation or rotation. There is no follower minimum or eligibility threshold to go live: a signed-in OpenVibe account can create a stream. The service is alpha.',
++ 'Ingest keys are random, stored only as a SHA-256 hash and shown once at creation or rotation.',
].join('\n'),
sections: [
{ title: 'Start here', links: [
ddiff --git a/server/ui/layout.js b/server/ui/layout.js
iindex 941d35f..4b86c83 100644
---- a/server/ui/layout.js
++++ b/server/ui/layout.js
@@@ -34,7 +34,9 @@ main.ore{max-width:1040px;margin:0 auto;padding:24px 16px 64px}
@media (max-width:640px){.ore th:nth-child(n+4),.ore td:nth-child(n+4){display:none}}
`;
--function renderPage({ title, body, user, canonicalPath = '/', robots = 'noindex,nofollow', config }) {
++const DESCRIPTION = 'OpenRe.Stream: stream definitions, ingest keys, sessions and restream outputs for the OpenVibe network.';
++
++function renderPage({ title, body, user, canonicalPath = '/', robots = 'noindex,nofollow', config, description = DESCRIPTION, styles = [] }) {
const pageTitle = title ? `${title} · ${SITE}` : `${SITE} — ingest and restream for OpenVibe`;
const nav = {
service: 'openre',
@@@ -60,12 +62,13 @@ function renderPage({ title, body, user, canonicalPath = '/', robots = 'noindex,
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(pageTitle)}</title>
--<meta name="description" content="OpenRe.Stream: stream definitions, ingest keys, sessions and restream outputs for the OpenVibe network.">
++<meta name="description" content="${esc(description)}">
<meta name="robots" content="${esc(robots)}">
<link rel="canonical" href="${esc(config.baseUrl + canonicalPath)}">
${icon}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<style>${CSS}</style>
++${styles.map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`).join('\n')}
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
</head>
ddiff --git a/server/ui/routes.js b/server/ui/routes.js
iindex 8eb5d39..3671516 100644
---- a/server/ui/routes.js
++++ b/server/ui/routes.js
@@@ -16,6 +16,7 @@
const crypto = require('crypto');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
++const showcase = require('openvibe-shared/showcase');
const express = require('express');
const { renderPage, esc } = require('./layout');
const { StoreError } = require('../store/definitions');
@@@ -87,15 +88,50 @@ function createUiRouter({ rt, auth }) {
const signedIn = req.caller.kind === 'user' && req.caller.subject;
const mine = signedIn ? await store.definitions.list({ owner_subject: req.caller.subject }) : [];
page(req, res, {
-- canonicalPath: '/', robots: 'index,follow',
-- body: `<h1>OpenRe.Stream</h1>
--<p>Ingest and restream for the OpenVibe network: stream definitions with hashed ingest keys, RTMP ingest sessions that run in transport workers separate from any web deploy, restream outputs with health and logs, and recording requests to OpenVibe.Media.</p>
--<p class="muted">Status: alpha. RTMP ingest and RTMP/SRT restreaming work here; WHIP, WebRTC/SFU and JSMPEG are still served by OpenVibe.Live. Channels, discovery and watch pages stay on <a href="${esc(config.liveUrl)}">openvibe.live</a>.</p>
++ canonicalPath: '/', robots: 'index,follow', styles: [showcase.STYLESHEET],
++ description: 'Go live from your browser with no OBS and no follower minimum on OpenVibe.Live, or restream an OBS feed to your other channels with OpenRe.Stream.',
++ 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 OpenRe.Stream' })}`,
});
});
++ /**
++ * The front page (openvibe-shared/showcase). It leads with going live from a browser, which today happens on
++ * OpenVibe.Live; it states that only RTMP ingest and RTMP/SRT restreaming run here, and that WHIP and JSMPEG
++ * are still Live's (the WebRTC and JSMPEG workers are ported but not deployed).
++ */
++ function frontPage(cfg) {
++ const live = cfg.liveUrl;
++ return showcase.hero({
++ eyebrow: 'OpenRe.Stream · alpha',
++ title: 'Go live from your browser.', accent: 'No OBS, no follower minimum.',
++ lede: 'Open OpenVibe.Live, press Go Live and allow your camera: nothing to install, and no follower or subscriber threshold. OpenRe.Stream is the ingest and restream service behind it. With an encoder such as OBS, it takes your RTMP feed and sends the session on to your other channels.',
++ actions: [
++ { label: 'Go live in your browser', href: live, primary: true },
++ { label: 'Restream with OBS', href: '/streams' },
++ { label: 'Read the guide', href: `${live}/docs/go-live-in-your-browser` },
++ ],
++ note: 'Alpha. RTMP ingest and RTMP/SRT restreaming run here today; browser (WHIP) and JSMPEG ingest are still served by OpenVibe.Live. Channels, discovery and watch pages stay on openvibe.live.',
++ }) + showcase.features({
++ title: 'What OpenRe does',
++ items: [
++ { icon: 'ov:stream', title: 'Ingest keys you control', text: 'Create a stream and get its RTMP server and key. The key is shown once, stored only as a hash, and can be rotated any time.' },
++ { icon: 'ov:live', title: 'One stream, several channels', text: 'Send a session to several RTMP or SRT destinations at once, each with its own health, logs and retry with backoff.' },
++ { icon: 'ov:video', title: 'Recording', text: 'Per stream: record a VOD, keep clips only, or record nothing. OpenVibe.Media keeps the recordings.' },
++ { icon: 'ov:check', title: 'Nothing to qualify for', text: 'Any signed-in OpenVibe account can create a stream. There is no follower, subscriber or eligibility threshold.' },
++ ],
++ }) + showcase.steps({
++ title: 'Restream with OBS',
++ items: [
++ { title: 'Sign in', text: 'With your OpenVibe account.', href: '/auth/login?next=/streams' },
++ { title: 'Create a stream', text: 'Copy the RTMP server and the key; the key is shown only once.' },
++ { title: 'Add destinations', text: 'The server and key of each channel you also stream to. Test each one before you go live.' },
++ { title: 'Go live from OBS', text: 'Each destination shows its health and logs while you stream.' },
++ ],
++ });
++ }
++
async function streamTable(list) {
return `<table><tr><th>Stream</th><th>State</th><th>Now</th><th>Linked to</th></tr>${(await Promise.all(list.map(async (d) => {
const open = (await store.sessions.openFor(d.id))[0];
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 80c4d63..ed9f820 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -35,14 +35,32 @@ t('sitemap.xml is XML and lists the public pages, including /', async () => {
}
});
--t('llms.txt is plain text and leads with browser WHIP go-live', async () => {
++t('llms.txt is plain text, leads with browser go-live and says what runs today', async () => {
const api = await bootApi({});
try {
const r = await request(api.base, 'GET', '/llms.txt');
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.ok(r.text.startsWith('# OpenRe.Stream\n'), 'starts with the site heading');
-- assert.ok(/WHIP/.test(r.text) && /browser/i.test(r.text), 'browser WHIP go-live is stated');
++ assert.match(r.text.split('\n').slice(0, 4).join(' '), /Go live from your browser with no OBS and no follower minimum/, 'browser go-live leads');
++ // Only the RTMP ingest and restream workers run in production: WHIP and JSMPEG are ported, not served yet.
++ assert.match(r.text, /WHIP, RFC 9725\) and JSMPEG ingest are ported to OpenRe and tested, but their workers do not run in production yet/);
++ } finally {
++ await api.close();
++ }
++});
++
++t('the front page leads with browser go-live, links to Live for it, and says what runs here', async () => {
++ const api = await bootApi({});
++ try {
++ const r = await request(api.base, 'GET', '/');
++ assert.strictEqual(r.status, 200);
++ assert.match(r.text, /<h1>Go live from your browser\.<span class="sc-accent"> No OBS, no follower minimum\.<\/span><\/h1>/);
++ assert.match(r.text, /<meta name="description" content="Go live from your browser with no OBS and no follower minimum/);
++ assert.ok(r.text.includes(`href="${api.config.liveUrl}"`), 'the primary action opens OpenVibe.Live, where browser go-live runs today');
++ assert.match(r.text, /browser \(WHIP\) and JSMPEG ingest are still served by OpenVibe\.Live/);
++ assert.match(r.text, /href="\/shared\/showcase\.css\?v=[0-9a-f]{12}"/);
++ assert.strictEqual((r.text.match(/<h1[ >]/g) || []).length, 1, 'one h1');
} finally {
await api.close();
}