openvibers/openrestream Commits

OpenRestream: the product at openre.stream is OpenRestream (owner, 2026-10-09) (#26)

OP OpenVibers committed 9 Oct 2026 · bd5b26eab3 · 58 files, +271 −271

Every name people read says OpenRestream instead of OpenRe or OpenRe.Stream, and repository paths say
OpenVibers/OpenRestream (renamed on GitHub the same day). Ids are unchanged (openre, openre.*, ov_openre).

Changes

ddiff --git a/.env.example b/.env.example
iindex def50ea..51e1d49 100644
---- a/.env.example
++++ b/.env.example
@@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════
--# OpenRe.Stream — Environment Configuration
++# OpenRestream — Environment Configuration
# Copy to .env for development. In production this is /etc/openvibe/openre.env (0600), read by
# every unit (openre-api, openre-session-coordinator, openre-rtmp-ingest@, openre-restream-worker@);
# the units set NODE_ENV, PORT and OPENRE_RELEASE themselves.
@@@ -22,16 +22,16 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
# Pin the Network public key instead of fetching /api/.well-known/jwks (PEM, \n escapes allowed).
#OV_NETWORK_PUBLIC_KEY=
# OAuth client `openre`: browser sign-in for the UI (redirect https://openre.stream/auth/callback)
--# and OpenRe's own client-credentials tokens (Events, Media).
++# and OpenRestream's own client-credentials tokens (Events, Media).
OV_OAUTH_CLIENT_ID=openre
OV_OAUTH_CLIENT_SECRET=
#OV_OAUTH_REDIRECT_URI=https://openre.stream/auth/callback
# A browser's Network JWT is accepted when its aud contains one of these.
#OPENRE_USER_AUDIENCES=openvibe.openre,openvibe.network
--# ── Store (PostgreSQL, shared by every OpenRe process) ────────
++# ── Store (PostgreSQL, shared by every OpenRestream process) ────────
# PostgreSQL (ADR-035): DATABASE_URL serves through PgBouncer, DATABASE_DIRECT_URL (the owner) runs migrations/ at boot;
--# OpenVibe.Host `roles/data/add-service.sh openre` writes both. Every OpenRe process shares the database. Without
++# OpenVibe.Host `roles/data/add-service.sh openre` writes both. Every OpenRestream process shares the database. Without
# DATABASE_URL, development runs on an embedded PGlite database in data/pglite (OPENRE_PGLITE_DIR): one process only.
DATABASE_URL=
DATABASE_DIRECT_URL=
ddiff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
iindex d19c822..9bd13b9 100644
---- a/.github/workflows/ci.yml
++++ b/.github/workflows/ci.yml
@@@ -6,7 +6,7 @@ on:
jobs:
# install, node --check, npm test and openvibe-contracts-check: the shared test job (Track Q). ADR-035: PostgreSQL
# 18 + PgBouncer (transaction mode) + Valkey 9 containers (openvibe-sdk scripts/test-services.sh); npm test runs every
-- # file on PGlite, test:pg runs them all through PgBouncer, with the tests that start several OpenRe processes.
++ # file on PGlite, test:pg runs them all through PgBouncer, with the tests that start several OpenRestream processes.
test:
uses: OpenVibers/OpenVibe.Shared/.github/workflows/test.yml@423fe519b5fe9594cc81b670e5ba5cb9f21f6176
with:
ddiff --git a/README.md b/README.md
iindex 0633806..f3928ba 100644
---- a/README.md
++++ b/README.md
@@@ -1,4 +1,4 @@
--# OpenRe.Stream
++# OpenRestream
> Ingest and restream: stream definitions, keys, sessions, transport workers, outputs and output health.
@@@ -9,7 +9,7 @@
## Purpose
--The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live keeps the creator/channel product and observes sessions through OpenRe APIs and events; transport workers survive Live deploys **and OpenRe API deploys**. This is the permanent fix for "restarting Live drops live RTMP streams" (hazard H1).
++The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live keeps the creator/channel product and observes sessions through OpenRestream APIs and events; transport workers survive Live deploys **and OpenRestream API deploys**. This is the permanent fix for "restarting Live drops live RTMP streams" (hazard H1).
## Owns
@@@ -33,15 +33,15 @@ The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live
- OpenVibe.Events (event relay; optional: rows wait in the outbox)
- OpenVibe.Media (recording requests; optional: sessions work without it)
- OpenVibe.Contracts v0.76.0 (ids, problem+json, capability checks), OpenVibe.SDK v0.26.0 (`db`, PostgreSQL outbox, token client, per-actor limits, the `service` kit), OpenVibe.Shared v2.5.0 (Frame, readiness), pinned by release tarball
--- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): the store every OpenRe process shares, and the API's per-actor limit counters (optional)
++- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): the store every OpenRestream process shares, and the API's per-actor limit counters (optional)
- OpenVibe.Live (`live.lineage.resolve`: which channel a Live-linked stream belongs to)
- node-media-server 2.7.4 (the RTMP session code Live runs) and the system ffmpeg
## What is ported, per protocol
--| Transport | Live source | In OpenRe | State |
++| Transport | Live source | In OpenRestream | State |
|---|---|---|---|
--| RTMP ingest | `server/streaming/rtmp-server.js` (node-media-server) | `workers/rtmp-ingest.js` — same NMS session code; OpenRe's own keys; publish renamed to `/live/<session id>` so no key reaches a play URL; loopback RTMP play + HTTP-FLV per generation; SO_REUSEPORT drain | **ported, tested with real ffmpeg** |
++| RTMP ingest | `server/streaming/rtmp-server.js` (node-media-server) | `workers/rtmp-ingest.js` — same NMS session code; OpenRestream's own keys; publish renamed to `/live/<session id>` so no key reaches a play URL; loopback RTMP play + HTTP-FLV per generation; SO_REUSEPORT drain | **ported, tested with real ffmpeg** |
| Restream (RTMP source) | `server/streaming/restream-manager.js` | `workers/restream/*` — same ffmpeg arguments (codec copy from HTTP-FLV), Twitch→RTMPS, Kick `/app`, SRT options, live ACK by `-progress`, backoff, rapid-crash circuit breaker, destination cooldown | **ported, tested with real ffmpeg** |
| Restream from a JSMPEG source | same | `jsmpegArgs()` — re-encodes the worker's MPEG-TS data tap (`http://127.0.0.1:<tapPort>/tap/<session>.ts`) | **ported, tested with real ffmpeg** |
| Restream from a WebRTC source | same | `webrtcArgs()`/`rtpInputArgs()` — re-encodes a mediasoup PlainRTP consumer the owning worker sends to ffmpeg's ports (SDP written by the worker's egress API) | **ported, tested with real ffmpeg + mediasoup** |
@@@ -49,7 +49,7 @@ The ingest/restream control plane and runtime extracted from OpenVibe.Live. Live
| WebRTC (WHIP ingest + SFU + viewer signaling) | `whip-handler.js` + `webrtc-sfu.js` + `broadcast-server.js` (mediasoup; one worker owns all of it) | `workers/webrtc.js` + `workers/webrtc/{sfu,sdp,signaling}.js` — WHIP (RFC 9725) admission by key, one mediasoup Router per session, `/b/<key>` broadcaster + `/w/<id>` viewer signaling, loopback RTP/SDP egress for restream/recording/thumbnails; SO_REUSEPORT drain | **ported, tested with real mediasoup** |
| Live thumbnails | `media-proxy/live-thumbs.js` | `workers/thumbnails.js` — the owning worker grabs a frame from its own source (FLV, MPEG-TS tap or a PlainRTP consumer) and uploads it to Media (object ns `live`); the session carries `thumbnail_url` | **ported, tested against a stub Media** |
| JSMPEG | `jsmpeg-relay.js` | `workers/jsmpeg.js` — MPEG-TS POST admission by key in path, WebSocket viewers by playback id, loopback MPEG-TS data tap for restream; SO_REUSEPORT drain | **ported, tested with real ffmpeg** |
--| OAuth-linked destinations (per go-live Twitch/YouTube key refresh, YouTube broadcast creation) | `restream-manager._refreshDestFromConnection` | no (Live owns platform OAuth); OpenRe pushes to the stored key | **not ported** |
++| OAuth-linked destinations (per go-live Twitch/YouTube key refresh, YouTube broadcast creation) | `restream-manager._refreshDestFromConnection` | no (Live owns platform OAuth); OpenRestream pushes to the stored key | **not ported** |
| Viewer counts from platforms, chat relay, PowerChat | restream-manager / integrations | no (product features, stay in Live) | not in scope |
## Processes
@@@ -66,7 +66,7 @@ openre-api (4500) openre-session-coordinator openre-rtmp-inges
- **Generations.** Each worker process registers as generation N+1 of its kind. When a newer generation is `ready`, the coordinator marks older ones `draining` (deadline `OPENRE_DRAIN_MAX_MS`, default 24 h). A draining RTMP worker closes its public listener (the kernel sends new encoder connections to the new generation through the shared port) and keeps its publishers; a draining restream worker keeps its outputs. Idle → the process exits 0 by itself. At the deadline, remaining sessions are ended (encoders reconnect to the newest generation) and remaining outputs are handed over.
- **Leases.** Every heartbeat (2 s) renews the worker and the lease of every session it owns in one transaction. A worker silent for `OPENRE_WORKER_LEASE_MS` (15 s) is `lost`: its sessions fail (`openre.session.failed`), its outputs go back to `pending` for another worker. A lost worker that wakes up drops its transports and exits.
- **Isolation.** An output only ever changes its own row. A dead destination fails (`openre.output.failed`, destination cooldown 15 min → 24 h) and the session stays live (`test/rtmp-e2e.test.js`, `test/sessions.test.js`, `test/coordinator.test.js`).
--- **Store.** PostgreSQL 18 through PgBouncer (`ov_openre` on the host's data role, ADR-035; schema in [migrations/](migrations/)), shared by every OpenRe process. Every transaction is SERIALIZABLE (`server/store/index.js`): an ingest worker admitting a publish and the coordinator failing a lost worker's session conflict, and PostgreSQL makes one of them retry. Admission is async, so the RTMP worker admits a publish before node-media-server's own publish handling runs. Valkey holds the API's per-actor limit counters.
++- **Store.** PostgreSQL 18 through PgBouncer (`ov_openre` on the host's data role, ADR-035; schema in [migrations/](migrations/)), shared by every OpenRestream process. Every transaction is SERIALIZABLE (`server/store/index.js`): an ingest worker admitting a publish and the coordinator failing a lost worker's session conflict, and PostgreSQL makes one of them retry. Admission is async, so the RTMP worker admits a publish before node-media-server's own publish handling runs. Valkey holds the API's per-actor limit counters.
## Running it
@@@ -96,7 +96,7 @@ Called elsewhere, as the service principal `openre` (client credentials from Net
| OpenVibe.Live | `live.lineage.resolve` | the channel a Live-linked stream definition belongs to |
| OpenVibe.Media | the `live` tenant key today (`MEDIA_API_KEY`); `media.object.upload` once `OPENRE_MEDIA_AUTH=service` | recording requests |
--The full grant list, including what Live needs to call OpenRe, is under "Grants the lead adds in
++The full grant list, including what Live needs to call OpenRestream, is under "Grants the lead adds in
Network" below.
## Auth
@@@ -126,7 +126,7 @@ The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposa
## Events
--Written to `event_outbox` in the same transaction as the change (openvibe-sdk `createPgOutbox`); the coordinator's relay publishes them with OpenRe's service token. Envelopes validate against `events.event-envelope@1`; `visibility: internal`; no payload ever carries an ingest key or a destination key.
++Written to `event_outbox` in the same transaction as the change (openvibe-sdk `createPgOutbox`); the coordinator's relay publishes them with OpenRestream's service token. Envelopes validate against `events.event-envelope@1`; `visibility: internal`; no payload ever carries an ingest key or a destination key.
| Type | Subject | When |
|---|---|---|
@@@ -140,7 +140,7 @@ Written to `event_outbox` in the same transaction as the change (openvibe-sdk `c
## Playback
--`GET /api/v1/sessions/:id/playback` returns a descriptor: `flv.internal_url` (loopback HTTP-FLV on the worker that holds the session — for services on this host), `flv.public_url` (`https://openre.stream/play/<session>.flv`, proxied by the API for non-private streams), `rtmp.internal_url` (loopback RTMP play, what Media records from). A **jsmpeg** session's descriptor is `jsmpeg: { ws_url, width, height }` (`ws_url` is the worker's public WebSocket path carrying the session's playback id, never the key; `width`/`height` come from the ffmpeg POST path), plus `jsmpeg.tap_internal_url`, the loopback MPEG-TS tap the restream worker reads. A **webrtc** session's descriptor is `webrtc: { signaling_url, announced_ip }` (`signaling_url` is `ws(s)://<host>:<port>/w/<session id>`, the viewer signaling endpoint keyed by playback id, never the key; Live's watch page runs the mediasoup-client `sfu-viewer-*` flow against it). Every descriptor also carries `thumbnail_url` (the Media object the worker's latest grabbed frame was uploaded to, or `null`) and the session API reports `viewers` (connected consumers / relay sockets, from the worker heartbeat); `hls` is `null` (not produced). Live's player keeps using its own `/api/streams/rtmp-proxy/:id.flv`, which for an OpenRe session proxies `flv.internal_url` (see the Live patch). **AI taps** (decision 6: they stay in Live and read OpenRe's sources, never a Live-local ingest): an **rtmp** session's `flv.internal_url` (loopback HTTP-FLV) and a **webrtc** session's RTP descriptor on the owning worker's loopback egress API — `GET /rtp/<session id>/describe` (the codec facts, e.g. for Media's RTP ingest) and `GET /rtp/<session id>?vport=<p>&aport=<p>` (the SDP a pulling ffmpeg reads, a PlainRTP consumer per producer). Both are loopback-only, reachable by services on the worker's host.
++`GET /api/v1/sessions/:id/playback` returns a descriptor: `flv.internal_url` (loopback HTTP-FLV on the worker that holds the session — for services on this host), `flv.public_url` (`https://openre.stream/play/<session>.flv`, proxied by the API for non-private streams), `rtmp.internal_url` (loopback RTMP play, what Media records from). A **jsmpeg** session's descriptor is `jsmpeg: { ws_url, width, height }` (`ws_url` is the worker's public WebSocket path carrying the session's playback id, never the key; `width`/`height` come from the ffmpeg POST path), plus `jsmpeg.tap_internal_url`, the loopback MPEG-TS tap the restream worker reads. A **webrtc** session's descriptor is `webrtc: { signaling_url, announced_ip }` (`signaling_url` is `ws(s)://<host>:<port>/w/<session id>`, the viewer signaling endpoint keyed by playback id, never the key; Live's watch page runs the mediasoup-client `sfu-viewer-*` flow against it). Every descriptor also carries `thumbnail_url` (the Media object the worker's latest grabbed frame was uploaded to, or `null`) and the session API reports `viewers` (connected consumers / relay sockets, from the worker heartbeat); `hls` is `null` (not produced). Live's player keeps using its own `/api/streams/rtmp-proxy/:id.flv`, which for an OpenRestream session proxies `flv.internal_url` (see the Live patch). **AI taps** (decision 6: they stay in Live and read OpenRestream's sources, never a Live-local ingest): an **rtmp** session's `flv.internal_url` (loopback HTTP-FLV) and a **webrtc** session's RTP descriptor on the owning worker's loopback egress API — `GET /rtp/<session id>/describe` (the codec facts, e.g. for Media's RTP ingest) and `GET /rtp/<session id>?vport=<p>&aport=<p>` (the SDP a pulling ffmpeg reads, a PlainRTP consumer per producer). Both are loopback-only, reachable by services on the worker's host.
## Recording
@@@ -160,15 +160,15 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
| Port | Owner | Until |
|---|---|---|
--| 1935/tcp | Live's in-process RTMP (node-media-server) | Live's RTMP ingest is retired (after the last slot has run on OpenRe for two weeks, ADR-009) |
++| 1935/tcp | Live's in-process RTMP (node-media-server) | Live's RTMP ingest is retired (after the last slot has run on OpenRestream for two weeks, ADR-009) |
| 9935/tcp (loopback) | Live's HTTP-FLV | same |
--| **1936/tcp** | OpenRe `openre-rtmp-ingest` (all generations, SO_REUSEPORT) | permanent: URLs handed out as `rtmp://ingest.openre.stream:1936/live` keep working |
--| 19360–19399/tcp (loopback) | OpenRe per-generation RTMP play + HTTP-FLV | permanent |
--| **9736/tcp** | OpenRe `openre-jsmpeg` (all generations, SO_REUSEPORT): MPEG-TS POST + WS viewers | permanent: `http://ingest.openre.stream:9736/<key>/<w>/<h>/` and `ws://…/<session>` |
--| 19710–19749/tcp (loopback) | OpenRe per-generation JSMPEG MPEG-TS data tap | permanent |
--| **9936/tcp** | OpenRe `openre-webrtc` (all generations, SO_REUSEPORT): WHIP + `/b/<key>` and `/w/<id>` signaling (Live's WHIP is on its app port 3000) | permanent: `http(s)://ingest.openre.stream/whip/<key>` and `ws(s)://ingest.openre.stream/<b\|w>/…` |
--| **10200–10300/udp** | OpenRe `openre-webrtc` mediasoup RTC (never Live's 10000–10100) | permanent: open at the provider edge, `MEDIASOUP_ANNOUNCED_IP` must be reachable |
--| 19810–19849/tcp (loopback) | OpenRe per-generation WebRTC RTP/SDP egress API (restream, Media's RTP recorder, thumbnails) | permanent |
++| **1936/tcp** | OpenRestream `openre-rtmp-ingest` (all generations, SO_REUSEPORT) | permanent: URLs handed out as `rtmp://ingest.openre.stream:1936/live` keep working |
++| 19360–19399/tcp (loopback) | OpenRestream per-generation RTMP play + HTTP-FLV | permanent |
++| **9736/tcp** | OpenRestream `openre-jsmpeg` (all generations, SO_REUSEPORT): MPEG-TS POST + WS viewers | permanent: `http://ingest.openre.stream:9736/<key>/<w>/<h>/` and `ws://…/<session>` |
++| 19710–19749/tcp (loopback) | OpenRestream per-generation JSMPEG MPEG-TS data tap | permanent |
++| **9936/tcp** | OpenRestream `openre-webrtc` (all generations, SO_REUSEPORT): WHIP + `/b/<key>` and `/w/<id>` signaling (Live's WHIP is on its app port 3000) | permanent: `http(s)://ingest.openre.stream/whip/<key>` and `ws(s)://ingest.openre.stream/<b\|w>/…` |
++| **10200–10300/udp** | OpenRestream `openre-webrtc` mediasoup RTC (never Live's 10000–10100) | permanent: open at the provider edge, `MEDIASOUP_ANNOUNCED_IP` must be reachable |
++| 19810–19849/tcp (loopback) | OpenRestream per-generation WebRTC RTP/SDP egress API (restream, Media's RTP recorder, thumbnails) | permanent |
| 4500/tcp (loopback) | `openre-api` | permanent |
The switch to 1935: once Live no longer listens on 1935, set `OPENRE_RTMP_EXTRA_PORTS=1935` and deploy a worker generation (`deploy.sh workers`); from then on `rtmp://ingest.openre.stream/live` works too, and 1936 keeps working. `ingest.openre.stream` is a DNS-only record (RTMP cannot go through Cloudflare's proxy); open 1936/tcp at the host firewall and the provider edge.
@@@ -187,7 +187,7 @@ sudo deploy/scripts/deploy.sh workers # deploy-legacy.sh: starts a
sudo deploy/scripts/deploy.sh status # deploy-legacy.sh
```
--- ovhost refuses an API restart while an ingest session is open (`--wait-idle` holds it, `--force` goes ahead), records every attempt (`ovhost releases openre`), prunes releases beyond five but never one a worker generation runs from, and announces the release. When ovhost is missing, too old or does not deploy OpenRe with `release-layout`, the wrapper runs [deploy/scripts/deploy-legacy.sh](deploy/scripts/deploy-legacy.sh), the previous script, unchanged (`OVHOST_LEGACY=1` forces it). `workers`, `status` and `prune` always run it.
++- ovhost refuses an API restart while an ingest session is open (`--wait-idle` holds it, `--force` goes ahead), records every attempt (`ovhost releases openre`), prunes releases beyond five but never one a worker generation runs from, and announces the release. When ovhost is missing, too old or does not deploy OpenRestream with `release-layout`, the wrapper runs [deploy/scripts/deploy-legacy.sh](deploy/scripts/deploy-legacy.sh), the previous script, unchanged (`OVHOST_LEGACY=1` forces it). `workers`, `status` and `prune` always run it.
- An **API deploy** never restarts a worker unit (no unit depends on another). Viewers of `openre.stream/play/…` reconnect; encoders, restreams and recordings do not notice.
- A **worker deploy** starts `openre-rtmp-ingest@<sha>`, `openre-restream-worker@<sha>` and `openre-jsmpeg@<sha>`; older instances are disabled (not stopped) and exit on their own when drained. Never `systemctl restart` a worker instance during a broadcast; `systemctl stop` starts a drain and waits up to 30 min (`TimeoutStopSec`), then kills.
@@@ -203,17 +203,17 @@ sudo deploy/scripts/deploy.sh status # deploy-legacy.sh
[docs/live-patch.diff](docs/live-patch.diff) is the Live side, made against OpenVibe.Live `f11f809` and verified there (applies with `git apply`; Live's `npm test` 63/63 on Node 22.22.1). It is applied in Live (`f0ca18b`) and deployed in production, switched off: `OPENRE_URL` is not set in Live's env. **Switch off (the default) changes nothing**: with `OPENRE_URL` unset, or a slot on `'live'`, every changed code path returns what it returned before (`test/openre-switch.test.js` in the patch). The only observable difference is additive: owner responses built from `managed_streams` rows carry the two new columns (`ingest_authority: 'live'`, `openre_stream_id: null`). It adds:
--- `server/openre/openre-client.js` — service-token client for OpenRe (streams by `external_ref`, create, rotate, sessions, playback descriptors).
++- `server/openre/openre-client.js` — service-token client for OpenRestream (streams by `external_ref`, create, rotate, sessions, playback descriptors).
- `managed_streams.ingest_authority` (`'live'` default | `'openre'`) + `openre_stream_id`, and an `openre_sessions` projection table (additive, in `initDb`).
--- Live's RTMP `prePublish` refuses a slot key when the slot is on `'openre'`, and a personal key (`users.stream_key`) when any of the user's slots is (a stream can never be ingested twice, and Live never pushes the same destinations as OpenRe). Only RTMP moves; WHIP/browser/JSMPEG for that slot stay on Live.
--- Go Live / stream-key UI for `'openre'` slots: `GET /api/streams/managed` returns the slot without its Live key; `GET /api/streams/managed/:id/profile` and `GET /api/streams/:id/endpoint` show OpenRe's RTMP URL and a key hint (never a key); `POST /api/streams/managed/:id/regenerate-key` rotates on OpenRe and shows the new key once; `GET /api/streams/:id/rtmp-status` reads the mirror. Small fallbacks in `broadcast.js` / `broadcast-workspace.js` display the hint.
--- `POST /internal/openre-events` — OpenVibe.Events delivery endpoint (SDK `parseDelivery` + `createInbox`, consumer `live-openre-mirror`): `openre.session.started` creates or attaches the slot's `streams` row (control config, go-live notifications, Live's own `live.stream.started`), `ended`/`failed` end it and stop the RobotStreamer bridge, chat relays and AI bots as Live's own unpublish does; revision-ordered, exactly once; only for switched slots whose definition has `mirror_to_live` (consent). A 30 s reconcile asks OpenRe about mirrored live sessions (heartbeat refresh; ends rows OpenRe ended). Live's stale-stream cleanup skips mirrored sessions OpenRe confirmed in the last 30 min, and Live's boot-time restream resume skips them.
--- Playback: `GET /api/streams/rtmp-proxy/:id.flv` proxies the session's `flv.internal_url` from OpenRe's playback descriptor (validated as a loopback `…/live/ses_….flv` URL), so Live's player is unchanged.
++- Live's RTMP `prePublish` refuses a slot key when the slot is on `'openre'`, and a personal key (`users.stream_key`) when any of the user's slots is (a stream can never be ingested twice, and Live never pushes the same destinations as OpenRestream). Only RTMP moves; WHIP/browser/JSMPEG for that slot stay on Live.
++- Go Live / stream-key UI for `'openre'` slots: `GET /api/streams/managed` returns the slot without its Live key; `GET /api/streams/managed/:id/profile` and `GET /api/streams/:id/endpoint` show OpenRestream's RTMP URL and a key hint (never a key); `POST /api/streams/managed/:id/regenerate-key` rotates on OpenRestream and shows the new key once; `GET /api/streams/:id/rtmp-status` reads the mirror. Small fallbacks in `broadcast.js` / `broadcast-workspace.js` display the hint.
++- `POST /internal/openre-events` — OpenVibe.Events delivery endpoint (SDK `parseDelivery` + `createInbox`, consumer `live-openre-mirror`): `openre.session.started` creates or attaches the slot's `streams` row (control config, go-live notifications, Live's own `live.stream.started`), `ended`/`failed` end it and stop the RobotStreamer bridge, chat relays and AI bots as Live's own unpublish does; revision-ordered, exactly once; only for switched slots whose definition has `mirror_to_live` (consent). A 30 s reconcile asks OpenRestream about mirrored live sessions (heartbeat refresh; ends rows OpenRestream ended). Live's stale-stream cleanup skips mirrored sessions OpenRestream confirmed in the last 30 min, and Live's boot-time restream resume skips them.
++- Playback: `GET /api/streams/rtmp-proxy/:id.flv` proxies the session's `flv.internal_url` from OpenRestream's playback descriptor (validated as a loopback `…/live/ses_….flv` URL), so Live's player is unchanged.
- Restream routes refuse (409 + `manage_url`) edits/start/stop for destinations of `'openre'` slots: those restreams run and are managed on openre.stream.
--- Admin: `GET /api/admin/openre/status`, `PUT /api/admin/openre/managed/:id/ingest-authority {authority, force}` — `'openre'` needs the slot offline, an RTMP slot (or `force`), the owner's canonical subject; it finds or creates the OpenRe definition and, in one transaction, flips the slot and rotates Live's own key for it. `'live'` flips back.
++- Admin: `GET /api/admin/openre/status`, `PUT /api/admin/openre/managed/:id/ingest-authority {authority, force}` — `'openre'` needs the slot offline, an RTMP slot (or `force`), the owner's canonical subject; it finds or creates the OpenRestream definition and, in one transaction, flips the slot and rotates Live's own key for it. `'live'` flips back.
- Env: `OPENRE_URL`, `OPENRE_PUBLIC_URL`, `OPENRE_EVENTS_SECRET` (in Live's `.env.example`). Unsetting `OPENRE_URL` is the emergency rollback for every switched slot at once.
--Not in the patch (follow-ups): live thumbnails, AI audio/vision taps and the RobotStreamer publisher for OpenRe sessions (they read Live's local HTTP-FLV by key; they can read `flv.internal_url` for an rtmp session and the worker's loopback RTP descriptor — `GET /rtp/<session id>/describe` and `?vport=&aport=` — for a webrtc one); Stream Manager destination editing through OpenRe's API (today: a link to openre.stream); platform viewer counts for OpenRe outputs.
++Not in the patch (follow-ups): live thumbnails, AI audio/vision taps and the RobotStreamer publisher for OpenRestream sessions (they read Live's local HTTP-FLV by key; they can read `flv.internal_url` for an rtmp session and the worker's loopback RTP descriptor — `GET /rtp/<session id>/describe` and `?vport=&aport=` — for a webrtc one); Stream Manager destination editing through OpenRestream's API (today: a link to openre.stream); platform viewer counts for OpenRestream outputs.
## Migration
@@@ -221,7 +221,7 @@ Not in the patch (follow-ups): live thumbnails, AI audio/vision taps and the Rob
## Cutover runbook
--ADR-009: one protocol at a time (RTMP → WHIP → JSMPEG → SFU), per slot, behind the switch, in a maintenance window agreed with the broadcaster; every key that existed before is rotated at the RTMP cutover; Live's ingest code stays until the last protocol has run on OpenRe for two weeks.
++ADR-009: one protocol at a time (RTMP → WHIP → JSMPEG → SFU), per slot, behind the switch, in a maintenance window agreed with the broadcaster; every key that existed before is rotated at the RTMP cutover; Live's ingest code stays until the last protocol has run on OpenRestream for two weeks.
### RTMP (ready)
@@@ -241,7 +241,7 @@ the checks and the rollback for every step. In short:
`scripts/cutover-preflight.js` checks, read-only, what each step needs: the release, the service,
the env, the bind, DNS, the port from outside, the database, Live's env, the Events subscription and
--one slot. Status on 2026-09-23: OpenRe is deployed (`655b98a10aaa`) and DNS is done. The release with
++one slot. Status on 2026-09-23: OpenRestream is deployed (`655b98a10aaa`) and DNS is done. The release with
`6dc78a5`, the public bind, the provider port, Live's settings and the subscription are not.
### JSMPEG (worker ready)
@@@ -256,13 +256,13 @@ one slot. Status on 2026-09-23: OpenRe is deployed (`655b98a10aaa`) and DNS is d
| Criterion (ADR-009 / plan W7) | Evidence |
|---|---|
--| OpenRe ingests and restreams without visiting Live | `test/rtmp-e2e.test.js`: real ffmpeg publish → OpenRe worker → ffmpeg restream to an RTMP sink; no Live process involved |
--| Deploying the OpenRe API does not end worker-owned transports | `test/api-restart.test.js` (API process SIGTERM + restart while a worker process holds a live session; lease keeps renewing) and `test/rtmp-e2e.test.js` (same with a real encoder, restream and FLV playback afterwards) |
++| OpenRestream ingests and restreams without visiting Live | `test/rtmp-e2e.test.js`: real ffmpeg publish → OpenRestream worker → ffmpeg restream to an RTMP sink; no Live process involved |
++| Deploying the OpenRestream API does not end worker-owned transports | `test/api-restart.test.js` (API process SIGTERM + restart while a worker process holds a live session; lease keeps renewing) and `test/rtmp-e2e.test.js` (same with a real encoder, restream and FLV playback afterwards) |
| New sessions route to the newest ready generation; old workers drain | `test/rtmp-e2e.test.js` (second RTMP worker process: new publish on generation 2, generation 1 keeps its session, closes its listener and exits 0 when it ends), `test/coordinator.test.js` |
| A destination failure never terminates the source session | `test/rtmp-e2e.test.js` (dead destination fails, session and encoder stay), `test/sessions.test.js`, `test/coordinator.test.js` (lost restream worker) |
| Recording finalisation stays in Media | `test/recording-events.test.js`, `test/rtmp-e2e.test.js` (Media is asked to create/ingest/finalise; Media's ffmpeg pulls the loopback URL) |
| Every stream key that existed before is rotated | `test/migration.test.js` (old keys never imported; new keys unseen), Live patch rotates Live's own slot key at the switch; checklist step for personal keys |
--| Deploying Live during a broadcast does not interrupt transport or recording | by construction (no OpenRe process talks to Live); proven for real only once a slot runs on OpenRe — not claimed here (every Live restart still stops Live's own restreams) |
++| Deploying Live during a broadcast does not interrupt transport or recording | by construction (no OpenRestream process talks to Live); proven for real only once a slot runs on OpenRestream — not claimed here (every Live restart still stops Live's own restreams) |
| Switch off = no Live behaviour change | Live's full `npm test` with the patch applied, plus `test/openre-switch.test.js` |
## Launch rule
ddiff --git a/STATUS.json b/STATUS.json
iindex fd2ec1f..d00dfff 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -1,5 +1,5 @@
{
-- "repository": "OpenVibers/OpenRe.Stream",
++ "repository": "OpenVibers/OpenRestream",
"stage": "alpha",
"code": true,
"wave": "W7",
@@@ -27,7 +27,7 @@
"openre.output.write"
],
"capabilitiesRegistered": true,
-- "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",
++ "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 OpenRestream 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).",
"features": [
ddiff --git a/deploy/scripts/deploy-legacy.sh b/deploy/scripts/deploy-legacy.sh
iindex 33d46ce..8a7afaf 100755
---- a/deploy/scripts/deploy-legacy.sh
++++ b/deploy/scripts/deploy-legacy.sh
@@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
--# OpenRe.Stream deploy (run on the host as root; the lead runs it, never CI).
++# OpenRestream deploy (run on the host as root; the lead runs it, never CI).
#
# deploy/scripts/deploy.sh release [<git-ref>] check out <ref> (default origin/main) into
# /opt/openre.stream/releases/<sha>, npm ci
@@@ -20,7 +20,7 @@
set -euo pipefail
ROOT=/opt/openre.stream
--REPO=https://github.com/OpenVibers/OpenRe.Stream.git
++REPO=https://github.com/OpenVibers/OpenRestream.git
API=http://127.0.0.1:4500
NODE_BIN=${NODE_BIN:-node}
ddiff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh
iindex bbfc9a8..ba3650a 100755
---- a/deploy/scripts/deploy.sh
++++ b/deploy/scripts/deploy.sh
@@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
--# OpenRe.Stream deploy: a thin wrapper around `ovhost deploy openre` (OpenVibe.Host, strategy
++# OpenRestream deploy: a thin wrapper around `ovhost deploy openre` (OpenVibe.Host, strategy
# release-layout; roadmap WS-N task 11; OpenVibe.Host docs/deploy-strategies.md). Run on the host as root.
#
# deploy/scripts/deploy.sh [deploy [<ref>]] ovhost deploy openre [--to <ref>]: release + api in one
ddiff --git a/deploy/systemd/openre-api.service b/deploy/systemd/openre-api.service
iindex 5db4736..bd4459e 100644
---- a/deploy/systemd/openre-api.service
++++ b/deploy/systemd/openre-api.service
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream API and UI (openre-api): definitions, keys, destinations, sessions, playback proxy
++Description=OpenRestream API and UI (openre-api): definitions, keys, destinations, sessions, playback proxy
# The API owns no transport. Restarting it (every API deploy) never touches the transport
# workers: they are separate units with no dependency on this one, in either direction.
After=network.target postgresql.service pgbouncer.service valkey-server.service
ddiff --git a/deploy/systemd/[email protected] b/deploy/systemd/[email protected]
iindex 22c95e8..d47ced1 100644
---- a/deploy/systemd/[email protected]
++++ b/deploy/systemd/[email protected]
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream JSMPEG ingest+relay worker, release %i
++Description=OpenRestream JSMPEG ingest+relay worker, release %i
# One instance per release (instance name = the release directory). A worker deploy STARTS a new
# instance; the coordinator then drains the older generation, which closes its public listener,
# keeps its sessions until they leave (or OPENRE_DRAIN_MAX_MS), and exits 0 by itself.
ddiff --git a/deploy/systemd/[email protected] b/deploy/systemd/[email protected]
iindex b0e6ec0..0abf8ee 100644
---- a/deploy/systemd/[email protected]
++++ b/deploy/systemd/[email protected]
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream restream worker (ffmpeg per destination), release %i
++Description=OpenRestream restream worker (ffmpeg per destination), release %i
# One instance per release (instance name = the release directory). A worker deploy STARTS a new
# instance; the coordinator gives it every new output and drains the older generation, whose
# outputs run until their sessions end (or OPENRE_DRAIN_MAX_MS: then they move over) before it exits 0.
ddiff --git a/deploy/systemd/[email protected] b/deploy/systemd/[email protected]
iindex 4f42e17..8cb2029 100644
---- a/deploy/systemd/[email protected]
++++ b/deploy/systemd/[email protected]
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream RTMP ingest worker, release %i
++Description=OpenRestream RTMP ingest worker, release %i
# One instance per release (instance name = the release directory). A worker deploy STARTS a new
# instance; the coordinator then drains the older generation, which closes its public listener,
# keeps its publishers until they leave (or OPENRE_DRAIN_MAX_MS), and exits 0 by itself.
ddiff --git a/deploy/systemd/openre-session-coordinator.service b/deploy/systemd/openre-session-coordinator.service
iindex 7949b69..b5a480a 100644
---- a/deploy/systemd/openre-session-coordinator.service
++++ b/deploy/systemd/openre-session-coordinator.service
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream session coordinator: worker leases and generations, output assignment, recording requests, event relay
++Description=OpenRestream session coordinator: worker leases and generations, output assignment, recording requests, event relay
# Stateless between ticks (everything is in the database): restarting it never ends a session.
After=network.target postgresql.service pgbouncer.service valkey-server.service
Wants=postgresql.service pgbouncer.service valkey-server.service
ddiff --git a/deploy/systemd/[email protected] b/deploy/systemd/[email protected]
iindex 5115643..491e3a2 100644
---- a/deploy/systemd/[email protected]
++++ b/deploy/systemd/[email protected]
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenRe.Stream WebRTC worker (WHIP ingest + mediasoup SFU + viewer signaling), release %i
++Description=OpenRestream WebRTC worker (WHIP ingest + mediasoup SFU + viewer signaling), release %i
# One instance per release (instance name = the release directory). A worker deploy STARTS a new
# instance; the coordinator then drains the older generation, which closes its public listener,
# keeps its sessions until they leave (or OPENRE_DRAIN_MAX_MS), and exits 0 by itself.
ddiff --git a/docs/cutover.md b/docs/cutover.md
iindex cb3f22a..ae7555c 100644
---- a/docs/cutover.md
++++ b/docs/cutover.md
@@@ -1,13 +1,13 @@
--# RTMP cutover runbook (OpenRe.Stream)
++# RTMP cutover runbook (OpenRestream)
--The order of work that moves RTMP ingest for Live's stream slots from Live to OpenRe.Stream, one slot
++The order of work that moves RTMP ingest for Live's stream slots from Live to OpenRestream, one slot
at a time (ADR-009). Every step says who does it, how to check it, and how to undo it.
- **OWNER** steps need the owner: the provider firewall, a Live admin session in the browser, the
broadcasters, and a real restream destination.
- **AGENT** steps are commands on the host (`ssh openvibe-ovh`, then `sudo`).
- Every command, path and env name below was checked against the code at the commit that added this
-- file: OpenRe `deploy/scripts/deploy.sh`, `server/config.js`, `scripts/*`; Live `server/openre/*`,
++ file: OpenRestream `deploy/scripts/deploy.sh`, `server/config.js`, `scripts/*`; Live `server/openre/*`,
`deploy/scripts/deploy.sh`; Events `server/api/subscriptions.js`. The production facts (paths,
grants, subscriptions, listeners) were read on the host on 2026-09-23, without changing anything.
@@@ -47,7 +47,7 @@ Everything below runs on the host. The scripts ship with the release (`scripts/`
| `cutover-preflight.js` | read-only checks: `release service env bind dns port db live-env events [slot]` (`--only`, `--skip`, `--slot <id>`, `--json`, `--strict`, `--expect-release <sha>`, `--probe-url`, `--host-ip`) | nothing |
| `subscribe-live-events.js` | Live's Events subscription `openre.session.*` → Live, **as Live** (reads `/etc/openvibe/live.env`); `--dry-run`, `--disable`, `--enable` | one subscription in Events |
| `migrate-from-live.js` | imports slots and destinations from a Live **snapshot** (dry run unless `--apply`) | the PostgreSQL store (`ov_openre`) |
--| `set-definition-state.js` | disables or re-enables one slot's OpenRe definition, **as Live** (per-slot rollback) | the PostgreSQL store (`ov_openre`) via the API |
++| `set-definition-state.js` | disables or re-enables one slot's OpenRestream definition, **as Live** (per-slot rollback) | the PostgreSQL store (`ov_openre`) via the API |
Shell helpers for the session (paste once per SSH session):
@@@ -55,7 +55,7 @@ Shell helpers for the session (paste once per SSH session):
SHA=<the 12-character release id printed by step A2>
# read-only preflight from that release
pf() { sudo node /opt/openre.stream/releases/$SHA/scripts/cutover-preflight.js --expect-release "$SHA" "$@"; }
--# run a command in the current release with OpenRe's production env, AS THE SERVICE USER (ubuntu).
++# run a command in the current release with OpenRestream's production env, AS THE SERVICE USER (ubuntu).
openre_as_service() { sudo bash -c "set -a; . /etc/openvibe/openre.env; set +a; export NODE_ENV=production OPENRE_ENV_FILE=/nonexistent; cd /opt/openre.stream/current && sudo -E -u ubuntu $1"; }
DEPLOY=/opt/openre.stream/releases/$SHA/deploy/scripts/deploy.sh
```
@@@ -198,7 +198,7 @@ Then repeat the off-host port check from A4.
### A7. Rehearsal with a test slot and a real restream destination (OWNER + AGENT)
--Use a **dedicated test account**, not the owner's own. Once any of a user's slots is on OpenRe, Live
++Use a **dedicated test account**, not the owner's own. Once any of a user's slots is on OpenRestream, Live
refuses that user's personal RTMP key (`refusesLiveIngest`).
1. **OWNER:** sign in to Live once with the test account, so it gets a canonical subject. Create an
@@@ -211,7 +211,7 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).
agent the slot id.
2. **AGENT:** snapshot, then migrate that slot (commands in B2), then `pf --only slot --slot <id>`.
3. **OWNER:** switch it with `await api('/admin/openre/managed/<id>/ingest-authority', { method: 'PUT', body: { authority: 'openre' } })`.
-- On the Go Live page, press **Regenerate stream key**. The OpenRe key is shown once. In OBS, set
++ On the Go Live page, press **Regenerate stream key**. The OpenRestream key is shown once. In OBS, set
the server to `rtmp://ingest.openre.stream:1936/live` and paste the key. Start streaming.
4. **Check (AGENT and OWNER):**
- The channel page on openvibe.live shows the stream, and the player works (Live proxies
@@@ -225,10 +225,10 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).
`sudo -u ubuntu bash -c "set -a; . /etc/openvibe/events.env; set +a; psql \"\$DATABASE_URL\" -c \"SELECT d.status, count(*) FROM deliveries d JOIN subscriptions s ON s.id = d.subscription_id WHERE s.consumer = 'live' AND s.topic_pattern = 'openre.session.*' GROUP BY 1\""`
should all be `delivered`.
5. **During the broadcast (AGENT):**
-- - `sudo $DEPLOY api $SHA`: the stream, the restream and the recording do not drop. OpenRe
++ - `sudo $DEPLOY api $SHA`: the stream, the restream and the recording do not drop. OpenRestream
playback viewers reconnect.
- `sudo /opt/openvibe.live/deploy/scripts/deploy.sh --restart` (no `--wait-idle`: the rehearsal
-- itself is live on Live). First check `/api/streams` for anyone else live. The OpenRe session,
++ itself is live on Live). First check `/api/streams` for anyone else live. The OpenRestream session,
the restream and the recording keep running. After the restart, Live's 30 s reconcile keeps
the channel row live.
- `deploy.sh workers` exercises a generation rollover only with a **newer** release (a new
@@@ -254,7 +254,7 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).
the same change, before `nginx -t`.
- (b) Tell broadcasters that destinations are frozen at the cutover until (a).
- **Hazard H4, the leaked keys.** 97 of 97 Live stream keys are identical to the pre-leak backup.
-- Each slot's cutover rotates that slot's Live key (at the switch) and issues a new OpenRe key that
++ Each slot's cutover rotates that slot's Live key (at the switch) and issues a new OpenRestream key that
only the broadcaster sees. It does not rotate the broadcaster's personal key (`users.stream_key`),
so step B5 does that. For slots that will not be cut over within days, rotate on Live now: the
broadcaster presses Regenerate on the Go Live page for each slot, and Regenerate on the dashboard
@@@ -278,7 +278,7 @@ sudo chown ubuntu:ubuntu "$SNAP" && sudo chmod 600 "$SNAP" # it holds every Liv
openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID" # dry run
openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID --apply" # import + checklist
sudo rm -f "$SNAP"
--pf --only slot --slot $ID # OpenRe <id> (active, mirrored); N destination(s), held ones counted
++pf --only slot --slot $ID # OpenRestream <id> (active, mirrored); N destination(s), held ones counted
```
- The migration imports the slot as a definition with a **new key that nobody has seen**. Old keys
@@@ -287,7 +287,7 @@ pf --only slot --slot $ID # OpenRe <id> (active, mirrored); N destination(
(disabled, with the reason recorded in `migration_map`). It is idempotent.
- The printed checklist (`--checklist <file>` writes it to a file) is the broadcaster-facing list
for B3 to B6.
--- **Undo:** nothing to undo on Live. The OpenRe definition does nothing until the switch. To be
++- **Undo:** nothing to undo on Live. The OpenRestream definition does nothing until the switch. To be
tidy, `sudo node /opt/openre.stream/current/scripts/set-definition-state.js --slot $ID --state disabled`.
### B3. Switch (OWNER, admin session)
@@@ -297,15 +297,15 @@ await api('/admin/openre/managed/<ID>/ingest-authority', { method: 'PUT', body:
// → { ingest_authority: 'openre', openre_stream_id, rtmp_url: 'rtmp://ingest.openre.stream:1936/live', live_key_rotated: true }
```
--In one transaction, Live points the slot at the OpenRe definition that B2 created and rotates its
++In one transaction, Live points the slot at the OpenRestream definition that B2 created and rotates its
own copy of the slot key. From this moment Live refuses the old key. It also refuses the owner's
--personal key for RTMP, because the owner now has a slot on OpenRe. A 409 means one of: the slot is
++personal key for RTMP, because the owner now has a slot on OpenRestream. A 409 means one of: the slot is
live on Live, it is not an RTMP slot, or the owner has no subject. Fix the cause. Do not use `force`
for a non-RTMP slot.
### B4. New key (broadcaster)
--On the Go Live page for that slot, press **Regenerate stream key**. OpenRe rotates the key and shows
++On the Go Live page for that slot, press **Regenerate stream key**. OpenRestream rotates the key and shows
it once. In OBS, set Server to `rtmp://ingest.openre.stream:1936/live` and Stream Key to that key.
### B5. Personal key (broadcaster; H4)
@@@ -316,7 +316,7 @@ On the dashboard, press Regenerate for the personal stream key (`POST /api/auth/
### B6. Test broadcast (broadcaster, AGENT, OWNER)
The broadcaster goes live. Check the same things as A7 step 4. `sudo $DEPLOY status` shows the
--session on the newest generation, and `await api('/admin/openre/status')` lists it. In the OpenRe DB,
++session on the newest generation, and `await api('/admin/openre/status')` lists it. In the OpenRestream DB,
the outputs are `live` and any held destinations are not running.
### B-rollback (per slot)
@@@ -324,12 +324,12 @@ the outputs are `live` and any held destinations are not running.
1. The broadcaster stops streaming. The slot must be offline.
2. **OWNER:** `await api('/admin/openre/managed/<ID>/ingest-authority', { method: 'PUT', body: { authority: 'live' } })`.
3. **AGENT:** `sudo node /opt/openre.stream/current/scripts/set-definition-state.js --slot <ID> --state disabled`.
-- OpenRe then refuses the key the broadcaster got in B4, so the slot's destinations can never be
++ OpenRestream then refuses the key the broadcaster got in B4, so the slot's destinations can never be
pushed twice.
4. **Broadcaster:** press Regenerate on the Go Live page. The Live key was rotated at the switch.
Point OBS back at the Live RTMP URL shown there.
--To switch the slot to OpenRe again later, run `set-definition-state.js --slot <ID> --state active`
++To switch the slot to OpenRestream again later, run `set-definition-state.js --slot <ID> --state active`
first, then B3 to B6.
**All slots at once (emergency):** undo A5. Remove `OPENRE_URL`, or restore `live.env.pre-openre`,
@@@ -339,7 +339,7 @@ switch, so each broadcaster regenerates on the Go Live page.
## JSMPEG protocol cutover (after RTMP)
JSMPEG reuses the RTMP runbook above, with one **protocol precondition** (brief §4.5): the slot's
--`managed_streams.protocol` must be `jsmpeg` and its OpenRe definition carries the `jsmpeg` protocol.
++`managed_streams.protocol` must be `jsmpeg` and its OpenRestream definition carries the `jsmpeg` protocol.
Live keeps its own `jsmpeg-relay.js` (9710/9711) until the last JSMPEG slot is cut over.
### J-A. Once, before the first JSMPEG slot (AGENT)
@@@ -401,9 +401,9 @@ done **once** (the worker generation and its ports), then per slot like every pr
generations drain and exit by themselves.
- **Media grants.** Thumbnails upload as Media objects under namespace `live` (Media grant
`media.object.upload` ns `live`); RTP recording uses `ingestRtpStart`/`ingestRtpStop` (Media ports
-- 12000–12199, already allocated by Media). Confirm both for OpenRe's token before switching a slot.
++ 12000–12199, already allocated by Media). Confirm both for OpenRestream's token before switching a slot.
- **Live:** add `webrtc` to `OPENRE_PROTOCOLS` in `server/openre/authority.js` **only when the
-- `openre-webrtc` generation is ready**, and point the broadcast/watch pages at the OpenRe signaling
++ `openre-webrtc` generation is ready**, and point the broadcast/watch pages at the OpenRestream signaling
endpoints (`sfu-*` messages are unchanged; the page reads `playback.webrtc.signaling_url`).
### W-B1–W-B6 per slot (OWNER + broadcaster + AGENT)
@@@ -465,7 +465,7 @@ the steps in this order: the worker refuses to start without `MEDIASOUP_ANNOUNCE
themselves. Check `systemctl status 'openre-webrtc@*'` and
`sudo journalctl -u 'openre-webrtc@<sha>.service' -n 50` (it logs the refusal if
`MEDIASOUP_ANNOUNCED_IP` is missing).
--6. **Smoke (AGENT, from off the host too).** A WHIP POST with a key OpenRe never issued must be
++6. **Smoke (AGENT, from off the host too).** A WHIP POST with a key OpenRestream never issued must be
refused by the worker, not by nginx:
```
curl -sS -o /dev/null -D - -X POST -H 'content-type: application/sdp' --data-binary $'v=0\r\n' \
@@@ -474,9 +474,9 @@ the steps in this order: the worker refuses to start without `MEDIASOUP_ANNOUNCE
PASS: TLS verifies, `409` with `x-whip-error: unknown_key`. A `502`/`504` means the worker is not
listening on 9936; `x-whip-error: generation not taking sessions` means it is not ready yet; a
`404` means the vhost did not load.
--7. **Prerequisite for robot video.** OpenVibe.Bot must publish with keys OpenRe issued
++7. **Prerequisite for robot video.** OpenVibe.Bot must publish with keys OpenRestream issued
(`POST /api/v1/streams`, rotated with `POST /api/v1/streams/:id/keys/rotate`): Bot mints its own
-- keys today and OpenRe refuses them exactly like the smoke key. That is Bot job R5
++ keys today and OpenRestream refuses them exactly like the smoke key. That is Bot job R5
(`bot-t15-r5-openvibe-bot-a`); until it ships, steps 1–6 work and a robot's camera is refused.
**Rollback.** Nothing here touches RTMP, JSMPEG or the API.
@@@ -500,14 +500,14 @@ the steps in this order: the worker refuses to start without `MEDIASOUP_ANNOUNCE
and Media calls are off. Writes, `/play/` and sign-in answer 503.
- Update `STATUS.json` and README "Status" after each phase.
- Each slot's Live RTMP key stays in Live's database until Live's RTMP ingest is retired. That
-- happens after the last slot has run on OpenRe for two weeks (ADR-009, register C-40). Port 1935
-- moves to OpenRe then (`OPENRE_RTMP_EXTRA_PORTS=1935`, `deploy.sh workers`).
++ happens after the last slot has run on OpenRestream for two weeks (ADR-009, register C-40). Port 1935
++ moves to OpenRestream then (`OPENRE_RTMP_EXTRA_PORTS=1935`, `deploy.sh workers`).
## Order of actions
| # | Who | Action | Check |
|---|---|---|---|
--| A1 | OWNER | push OpenRe `main` (and OpenVibe.Host) | `git log origin/main` |
++| A1 | OWNER | push OpenRestream `main` (and OpenVibe.Host) | `git log origin/main` |
| A2 | AGENT | `deploy.sh release origin/main`, `deploy.sh api <sha>` | `pf --only release,env,db`, `/api/ready` |
| A3 | AGENT | `OPENRE_RTMP_BIND=0.0.0.0`, `deploy.sh workers <sha>` | `pf --only service,env,bind` |
| A4 | **OWNER** | open 1936/tcp at the provider edge | off-host `--only port` PASS |
ddiff --git a/docs/live-patch.diff b/docs/live-patch.diff
iindex addd796..6b9bcea 100644
---- a/docs/live-patch.diff
++++ b/docs/live-patch.diff
@@@ -7,7 +7,7 @@ index 5b3d8ac..b30f33b 100644
# OAuth client above (grant events.event.publish). Unset = off. EVENTS_PUBLISH=off also disables it.
#EVENTS_URL=http://127.0.0.1:4300
+
--+# OpenRe.Stream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot
+++# OpenRestream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot
+# is ingested by Live exactly as before. When set, an admin can switch one slot at a time
+# (PUT /api/admin/openre/managed/:id/ingest-authority); only RTMP moves. Live's service principal
+# (the OAuth client above) needs openre.stream.read, openre.stream.write, openre.key.rotate and
@@@ -28,7 +28,7 @@ index 5ecdf41..1ebe063 100644
_wsState.selectedMs = { ..._wsState.selectedMs, ...data.managed_stream };
}
_wsState.streamKey = data.stream_key || null;
--+ // OpenRe-ingested slot: no key in the profile, only a hint (Regenerate shows a new key once).
+++ // OpenRestream-ingested slot: no key in the profile, only a hint (Regenerate shows a new key once).
+ _wsState.streamKeyHint = data.stream_key_hint || null;
_wsState.profile = data.broadcast_settings || {};
_wsState.profile.vibeCoding = _wsNormalizeVibeSettings(_wsReadStoredVibeSettings(_wsState.profile));
@@@ -59,7 +59,7 @@ index f0db572..4610588 100644
const ep = data.endpoint || {};
const rtmpUrl = ep.rtmpUrl || `rtmp://${location.hostname}:1935/live`;
- const streamKey = ep.streamKey || data.stream_key || 'N/A';
--+ // An OpenRe-ingested slot never returns its key here, only a hint (Regenerate shows a new key once).
+++ // An OpenRestream-ingested slot never returns its key here, only a hint (Regenerate shows a new key once).
+ const streamKey = ep.streamKey || data.stream_key || ep.streamKeyHint || 'N/A';
document.getElementById('bc-rtmp-url').textContent = rtmpUrl;
document.getElementById('bc-rtmp-key').textContent = streamKey;
@@@ -72,7 +72,7 @@ index e8f5903..60c20bf 100644
database.exec('CREATE INDEX IF NOT EXISTS idx_vibe_events_stream ON vibe_coding_events(stream_id)');
} catch (e) { console.warn('[DB] vibe_coding migration:', e.message); }
--+ // OpenRe ingest switch (roadmap Wave 7): managed_streams.ingest_authority ('live' by default)
+++ // OpenRestream ingest switch (roadmap Wave 7): managed_streams.ingest_authority ('live' by default)
+ // and the openre_sessions mirror. Additive only; see server/openre/schema.js.
+ try { require('../openre/schema').ensure(database); } catch (e) { console.warn('[DB] openre schema:', e.message); }
+
@@@ -87,7 +87,7 @@ index 63c0969..c88df08 100644
function hasActiveLiveFeed(stream) {
if (!stream) return false;
--+ // OpenRe.Stream holds the ingest (a mirrored session it confirmed recently): not stale.
+++ // OpenRestream holds the ingest (a mirrored session it confirmed recently): not stale.
+ if (require('./openre/mirror').hasLiveSession(stream.id)) return true;
const streamKey = getStreamKey(stream);
if (stream.protocol === 'rtmp') {
@@@ -96,7 +96,7 @@ index 63c0969..c88df08 100644
// OpenVibe.Media → Live webhook (vod.ready / clip.ready …). Mounted BEFORE the
// /internal router because it authenticates with an HMAC signature, not X-Internal-Key.
app.post('/internal/media-webhook', require('./media-proxy/webhook'));
--+// OpenVibe.Events → Live: OpenRe session lifecycle mirrored into `streams` (signed delivery,
+++// OpenVibe.Events → Live: OpenRestream session lifecycle mirrored into `streams` (signed delivery,
+// OPENRE_EVENTS_SECRET; server/openre/mirror.js). Also before /internal (no X-Internal-Key).
+app.post('/internal/openre-events', require('./openre/mirror').webhookHandler);
@@@ -114,7 +114,7 @@ index 63c0969..c88df08 100644
// 5b. Resume enabled restreams for streams that survived the restart.
// WHIP/RTMP broadcasters have no browser session to re-start them manually.
for (const stream of db.getLiveStreams()) {
--+ // OpenRe restreams its own sessions; Live must not start a second push for them.
+++ // OpenRestream restreams its own sessions; Live must not start a second push for them.
+ if (require('./openre/mirror').ownsStream(stream.id)) continue;
restreamManager.resumeForStream(stream.id, stream.user_id, {
protocol: stream.protocol,
@@@ -123,8 +123,8 @@ index 63c0969..c88df08 100644
// 6e. Start periodic viewer count polling for restream destinations
restreamManager.startViewerCountPolling();
--+ // 6g. OpenRe mirror reconcile (only runs when OPENRE_URL is set; idle with nothing mirrored).
--+ try { require('./openre/mirror').start(); } catch (e) { console.warn('[OpenRe] mirror reconcile not started:', e.message); }
+++ // 6g. OpenRestream mirror reconcile (only runs when OPENRE_URL is set; idle with nothing mirrored).
+++ try { require('./openre/mirror').start(); } catch (e) { console.warn('[OpenRestream] mirror reconcile not started:', e.message); }
+
// 6f. VOD storage/offload is owned by OpenVibe.Media now — nothing to start here.
@@@ -139,14 +139,14 @@ index 0000000..fa4b4ab
+/**
+ * The per-slot ingest switch: managed_streams.ingest_authority = 'live' (default) | 'openre'.
+ *
--+ * 'openre' means OpenRe.Stream ingests this slot's RTMP: Live's RTMP server refuses the slot's
--+ * key (so a stream can never be ingested twice), the Go Live UI shows OpenRe's ingest URL and
--+ * rotates OpenRe's key, and the slot's sessions reach `streams` through the mirror (mirror.js).
+++ * 'openre' means OpenRestream ingests this slot's RTMP: Live's RTMP server refuses the slot's
+++ * key (so a stream can never be ingested twice), the Go Live UI shows OpenRestream's ingest URL and
+++ * rotates OpenRestream's key, and the slot's sessions reach `streams` through the mirror (mirror.js).
+ * Only RTMP moves (OPENRE_PROTOCOLS); WHIP, browser/WebRTC and JSMPEG stay on Live until
--+ * OpenRe carries them.
+++ * OpenRestream carries them.
+ *
+ * Switch off = zero behaviour change: every helper here returns what Live did before unless the
--+ * slot says 'openre' AND OpenRe is configured (openre-client enabled()).
+++ * slot says 'openre' AND OpenRestream is configured (openre-client enabled()).
+ */
+const crypto = require('crypto');
+const db = require('../db/database');
@@@ -163,7 +163,7 @@ index 0000000..fa4b4ab
+ return id ? db.getManagedStreamById(id) : null;
+}
+
--+/** authorityOf(slotById(id)) === 'openre', without reading the slot when OpenRe is not configured. */
+++/** authorityOf(slotById(id)) === 'openre', without reading the slot when OpenRestream is not configured. */
+function slotIsOpenre(id) {
+ if (!id || !client.enabled()) return false;
+ return authorityOf(slotById(id)) === 'openre';
@@@ -171,7 +171,7 @@ index 0000000..fa4b4ab
+
+/**
+ * Live's RTMP publish handler asks this before accepting a key. A slot key is refused when its slot
--+ * is on OpenRe; a personal key (users.stream_key, no slot) is refused when any of the user's slots is,
+++ * is on OpenRestream; a personal key (users.stream_key, no slot) is refused when any of the user's slots is,
+ * because Live would attach it to the user's live rows and push the user's destinations twice.
+ */
+function refusesLiveIngest({ managedStream, user = null, protocol = 'rtmp' }) {
@@@ -197,7 +197,7 @@ index 0000000..fa4b4ab
+}
+
+/**
--+ * What the Go Live UI shows for an OpenRe slot: OpenRe's RTMP URL and the hint of its current key
+++ * What the Go Live UI shows for an OpenRestream slot: OpenRestream's RTMP URL and the hint of its current key
+ * (the key itself is only ever shown once, by a rotation).
+ */
+async function ingestFor(slot, subject) {
@@@ -210,15 +210,15 @@ index 0000000..fa4b4ab
+ rtmp_url: rtmp ? rtmp.url : null,
+ stream_key_hint: rtmp && rtmp.key_hint
+ ? `Shown once: press Regenerate for a new key (current key ends in …${rtmp.key_hint})`
--+ : 'Press Regenerate to get your OpenRe stream key',
+++ : 'Press Regenerate to get your OpenRestream stream key',
+ openre_manage_url: client.manageUrl(stream ? stream.id : slot.openre_stream_id),
+ };
+}
+
--+/** Regenerate on an OpenRe slot: OpenRe rotates, the new key is returned once. */
+++/** Regenerate on an OpenRestream slot: OpenRestream rotates, the new key is returned once. */
+async function rotateFor(slot, subject) {
+ const streamId = slot.openre_stream_id || (await client.streamForSlot(slot.id) || {}).id;
--+ if (!streamId) throw new client.OpenReError('this slot has no OpenRe stream yet', 409);
+++ if (!streamId) throw new client.OpenReError('this slot has no OpenRestream stream yet', 409);
+ const r = await client.rotateKey(streamId, { subject });
+ return { stream_key: r.key.key, stream_key_managed_by: 'openre', rtmp_url: r.ingest && r.ingest.rtmp ? r.ingest.rtmp.url : null };
+}
@@@ -231,8 +231,8 @@ index 0000000..fa4b4ab
+
+/**
+ * The admin switch (PUT /api/admin/openre/managed/:id/ingest-authority).
--+ * 'openre': needs OpenRe configured, the slot offline on Live, the owner's canonical subject;
--+ * finds or creates the OpenRe definition, then (one transaction) flips the slot and
+++ * 'openre': needs OpenRestream configured, the slot offline on Live, the owner's canonical subject;
+++ * finds or creates the OpenRestream definition, then (one transaction) flips the slot and
+ * rotates Live's own key for it, so no key that existed before works anywhere.
+ * 'live': flips back (rollback). Live's key was rotated at the switch: the broadcaster
+ * regenerates it on the Go Live page.
@@@ -243,15 +243,15 @@ index 0000000..fa4b4ab
+ if (!['live', 'openre'].includes(authority)) return { status: 400, error: "authority must be 'live' or 'openre'" };
+ if (authority === 'live') {
+ db.run("UPDATE managed_streams SET ingest_authority = 'live', updated_at = CURRENT_TIMESTAMP WHERE id = ?", [slot.id]);
--+ return { status: 200, body: { managed_stream_id: slot.id, ingest_authority: 'live', next: 'The broadcaster regenerates the stream key on the Go Live page (the Live key was rotated when the slot moved to OpenRe).' } };
+++ return { status: 200, body: { managed_stream_id: slot.id, ingest_authority: 'live', next: 'The broadcaster regenerates the stream key on the Go Live page (the Live key was rotated when the slot moved to OpenRestream).' } };
+ }
--+ if (!client.enabled()) return { status: 409, error: 'OpenRe is not configured on this Live (OPENRE_URL, OV_OAUTH_CLIENT_SECRET)' };
+++ if (!client.enabled()) return { status: 409, error: 'OpenRestream is not configured on this Live (OPENRE_URL, OV_OAUTH_CLIENT_SECRET)' };
+ if (slot.ingest_authority === 'openre') return { status: 200, body: { managed_stream_id: slot.id, ingest_authority: 'openre', openre_stream_id: slot.openre_stream_id, unchanged: true } };
+ const live = db.get('SELECT id FROM streams WHERE managed_stream_id = ? AND is_live = 1 LIMIT 1', [slot.id]);
+ if (live) return { status: 409, error: `Slot ${slot.id} is live on Live (stream ${live.id}); switch it in a maintenance window, while it is offline` };
+ const method = String(slot.streaming_method || '').toLowerCase();
+ if (!force && slot.protocol !== 'rtmp' && !['rtmp', 'obs'].includes(method)) {
--+ return { status: 409, error: `Slot ${slot.id} is a ${slot.protocol}${method ? `/${method}` : ''} slot; OpenRe only ingests RTMP today (pass force to switch anyway)` };
+++ return { status: 409, error: `Slot ${slot.id} is a ${slot.protocol}${method ? `/${method}` : ''} slot; OpenRestream only ingests RTMP today (pass force to switch anyway)` };
+ }
+ const subject = require('../auth/identity-sync').subjectOf(slot.user_id);
+ if (!subject) return { status: 409, error: 'The owner has no canonical subject yet (they need to sign in to Live once)' };
@@@ -263,7 +263,7 @@ index 0000000..fa4b4ab
+ }
+ const newLiveKey = crypto.randomBytes(20).toString('hex');
+ const flipped = db.getDb().transaction(() => {
--+ // Checked again here: a Live publish may have started while OpenRe was being asked.
+++ // Checked again here: a Live publish may have started while OpenRestream was being asked.
+ if (db.get('SELECT id FROM streams WHERE managed_stream_id = ? AND is_live = 1 LIMIT 1', [slot.id])) return false;
+ db.run("UPDATE managed_streams SET ingest_authority = 'openre', openre_stream_id = ?, stream_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [stream.id, newLiveKey, slot.id]);
+ return true;
@@@ -277,7 +277,7 @@ index 0000000..fa4b4ab
+ openre_stream_id: stream.id,
+ rtmp_url: stream.ingest && stream.ingest.rtmp ? stream.ingest.rtmp.url : null,
+ live_key_rotated: true,
--+ next: 'The broadcaster presses Regenerate on the Go Live page (OpenRe issues the key) and pastes the new server and key into OBS.',
+++ next: 'The broadcaster presses Regenerate on the Go Live page (OpenRestream issues the key) and pastes the new server and key into OBS.',
+ },
+ };
+}
@@@ -291,19 +291,19 @@ index 0000000..5a32b1c
@@ -0,0 +1,216 @@
+'use strict';
+/**
--+ * OpenRe sessions → Live's `streams` table (roadmap Wave 7, ADR-009: Live observes sessions through
--+ * OpenRe events and APIs). Watch pages, discovery, go-live notifications and analytics keep working
--+ * because an OpenRe-ingested session becomes an ordinary live `streams` row.
+++ * OpenRestream sessions → Live's `streams` table (roadmap Wave 7, ADR-009: Live observes sessions through
+++ * OpenRestream events and APIs). Watch pages, discovery, go-live notifications and analytics keep working
+++ * because an OpenRestream-ingested session becomes an ordinary live `streams` row.
+ *
+ * POST /internal/openre-events the endpoint of Live's OpenVibe.Events subscription (topic
+ * openre.session.*), signed with OPENRE_EVENTS_SECRET. Each event
+ * is applied exactly once (openvibe-sdk inbox, consumer
+ * 'live-openre-mirror'), in revision order per session.
--+ * reconcile (every 30 s) for mirrored live sessions only: asks OpenRe for the session,
--+ * keeps the row's heartbeat fresh, ends it if OpenRe says it ended
+++ * reconcile (every 30 s) for mirrored live sessions only: asks OpenRestream for the session,
+++ * keeps the row's heartbeat fresh, ends it if OpenRestream says it ended
+ * (covers a missed event). Does nothing when no row is mirrored.
+ *
--+ * Consent: a session is mirrored only when its slot is switched to OpenRe on Live AND the OpenRe
+++ * Consent: a session is mirrored only when its slot is switched to OpenRestream on Live AND the OpenRestream
+ * definition says mirror_to_live (plan §15.10 "explicit visibility and consent").
+ */
+const db = require('../db/database');
@@@ -312,7 +312,7 @@ index 0000000..5a32b1c
+
+const CONSUMER = 'live-openre-mirror';
+const RECONCILE_MS = 30000;
--+// A mirrored session OpenRe has not confirmed for this long is left to Live's stale cleanup.
+++// A mirrored session OpenRestream has not confirmed for this long is left to Live's stale cleanup.
+const CONFIRM_WINDOW_MIN = 30;
+let inbox = null;
+let timer = null;
@@@ -391,11 +391,11 @@ index 0000000..5a32b1c
+ const channel = db.getChannelByUserId(user.id);
+ const configId = slot.control_config_id || (channel && channel.active_control_config_id);
+ if (configId) db.applyConfigToStream(configId, streamId);
--+ } catch (e) { console.warn('[OpenRe] control config for mirrored stream failed:', e.message); }
+++ } catch (e) { console.warn('[OpenRestream] control config for mirrored stream failed:', e.message); }
+ const stream = db.getStreamById(streamId) || { id: streamId };
--+ try { require('../streaming/golive-notify').notifyFollowersGoLive(user, stream); } catch (e) { console.warn('[OpenRe] go-live notify failed:', e.message); }
+++ try { require('../streaming/golive-notify').notifyFollowersGoLive(user, stream); } catch (e) { console.warn('[OpenRestream] go-live notify failed:', e.message); }
+ try { require('../streaming/live-events').announceGoLive(stream, user); } catch { /* */ }
--+ console.log(`[OpenRe] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);
+++ console.log(`[OpenRestream] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);
+ },
+ };
+ }
@@@ -419,7 +419,7 @@ index 0000000..5a32b1c
+ try { require('../integrations/robotstreamer-service').stopForStream(streamId); } catch { /* */ }
+ try { require('../integrations/chat-relay-service').stopForStream(streamId); } catch { /* */ }
+ try { require('../integrations/ai-chatbot-service').stopForStream(streamId); } catch { /* */ }
--+ console.log(`[OpenRe] session ${sessionId} ${state}: stream ${streamId} ended`);
+++ console.log(`[OpenRestream] session ${sessionId} ${state}: stream ${streamId} ended`);
+ },
+ };
+}
@@@ -441,20 +441,20 @@ index 0000000..5a32b1c
+ after = r.after || null;
+ return r.outcome;
+ });
--+ if (!result.duplicate && after) { try { after(); } catch (e) { console.warn('[OpenRe] post-commit step failed:', e.message); } }
+++ if (!result.duplicate && after) { try { after(); } catch (e) { console.warn('[OpenRestream] post-commit step failed:', e.message); } }
+ } catch (err) {
--+ console.error('[OpenRe] event apply failed:', err.message);
+++ console.error('[OpenRestream] event apply failed:', err.message);
+ return res.status(500).json({ error: 'apply failed' }); // Events retries
+ }
+ return res.status(204).end();
+}
+
--+/** Stream id → the mirrored OpenRe session (or null). */
+++/** Stream id → the mirrored OpenRestream session (or null). */
+function sessionForStream(streamId) {
+ try { return db.get('SELECT * FROM openre_sessions WHERE stream_id = ? ORDER BY updated_at DESC LIMIT 1', [streamId]) || null; } catch { return null; }
+}
+
--+/** Does OpenRe hold a live ingest for this stream (confirmed recently)? Used by Live's stale cleanup. */
+++/** Does OpenRestream hold a live ingest for this stream (confirmed recently)? Used by Live's stale cleanup. */
+function hasLiveSession(streamId) {
+ try {
+ return Boolean(db.get(`SELECT 1 FROM openre_sessions WHERE stream_id = ? AND state = 'live'
@@@ -462,7 +462,7 @@ index 0000000..5a32b1c
+ } catch { return false; }
+}
+
--+/** Does OpenRe own this stream at all (so Live must not start its own restream/recording for it)? */
+++/** Does OpenRestream own this stream at all (so Live must not start its own restream/recording for it)? */
+function ownsStream(streamId) {
+ return Boolean(sessionForStream(streamId));
+}
@@@ -473,14 +473,14 @@ index 0000000..5a32b1c
+ if (!rows.length || !client.enabled()) return 0;
+ let n = 0;
+ for (const r of rows) {
--+ // Live ended the row itself (End Stream, or stale cleanup while OpenRe was unreachable):
+++ // Live ended the row itself (End Stream, or stale cleanup while OpenRestream was unreachable):
+ // stop tracking it rather than keeping a live mirror of an offline stream.
+ const row = r.stream_id ? db.getStreamById(r.stream_id) : null;
+ if (row && !row.is_live) { db.run("UPDATE openre_sessions SET state = 'detached', updated_at = CURRENT_TIMESTAMP WHERE session_id = ?", [r.session_id]); continue; }
+ let s;
+ try { s = await client.getSession(r.session_id); } catch (err) {
+ if (err.status === 404) s = { state: 'failed', revision: r.revision + 1, ended_at: null };
--+ else continue; // OpenRe unreachable: keep the last known state (bounded by CONFIRM_WINDOW_MIN)
+++ else continue; // OpenRestream unreachable: keep the last known state (bounded by CONFIRM_WINDOW_MIN)
+ }
+ if (s.state === 'live' || s.state === 'starting') {
+ db.run("UPDATE openre_sessions SET confirmed_at = CURRENT_TIMESTAMP WHERE session_id = ?", [r.session_id]);
@@@ -497,7 +497,7 @@ index 0000000..5a32b1c
+
+function start() {
+ if (timer || !client.enabled()) return;
--+ timer = setInterval(() => { reconcileOnce().catch(err => console.warn('[OpenRe] reconcile failed:', err.message)); }, RECONCILE_MS);
+++ timer = setInterval(() => { reconcileOnce().catch(err => console.warn('[OpenRestream] reconcile failed:', err.message)); }, RECONCILE_MS);
+ if (timer.unref) timer.unref();
+}
+
@@@ -513,8 +513,8 @@ index 0000000..6862412
@@ -0,0 +1,127 @@
+'use strict';
+/**
--+ * OpenRe.Stream client (roadmap Wave 7). Live asks OpenRe for stream definitions, ingest URLs,
--+ * key rotation, sessions and playback descriptors; it never touches an OpenRe worker.
+++ * OpenRestream client (roadmap Wave 7). Live asks OpenRestream for stream definitions, ingest URLs,
+++ * key rotation, sessions and playback descriptors; it never touches an OpenRestream worker.
+ *
+ * Auth: Live's Network service principal (OV_OAUTH_CLIENT_ID / OV_OAUTH_CLIENT_SECRET),
+ * client-credentials tokens for audience openvibe.openre. Grants Live needs on openre:
@@@ -522,7 +522,7 @@ index 0000000..6862412
+ * Calls made for a streamer carry X-OV-Subject: <their usr_ subject>.
+ *
+ * Env:
--+ * OPENRE_URL OpenRe API, host-local (http://127.0.0.1:4500). Unset = integration off:
+++ * OPENRE_URL OpenRestream API, host-local (http://127.0.0.1:4500). Unset = integration off:
+ * every slot behaves exactly as before, whatever its ingest_authority.
+ * OPENRE_PUBLIC_URL for links to the standalone UI (default https://openre.stream)
+ * OPENRE_EVENTS_SECRET the signing secret of Live's OpenVibe.Events subscription to openre.*
@@@ -543,7 +543,7 @@ index 0000000..6862412
+ };
+}
+
--+/** True when Live is configured to talk to OpenRe at all. */
+++/** True when Live is configured to talk to OpenRestream at all. */
+function enabled() {
+ const s = settings();
+ return Boolean(s.url && s.clientSecret);
@@@ -563,7 +563,7 @@ index 0000000..6862412
+}
+
+async function request(method, path, { body, subject, timeoutMs = TIMEOUT_MS } = {}) {
--+ if (!enabled()) throw new OpenReError('OpenRe integration is not configured (OPENRE_URL, OV_OAUTH_CLIENT_SECRET)', 0);
+++ if (!enabled()) throw new OpenReError('OpenRestream integration is not configured (OPENRE_URL, OV_OAUTH_CLIENT_SECRET)', 0);
+ const s = settings();
+ const headers = { Accept: 'application/json', Authorization: `Bearer ${await tokenClient().getToken()}` };
+ if (subject) headers['X-OV-Subject'] = subject;
@@@ -574,15 +574,15 @@ index 0000000..6862412
+ res = await fetch(`${s.url}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) });
+ text = await res.text().catch(() => '');
+ } catch (err) {
--+ throw new OpenReError(`OpenRe unreachable (${method} ${path}): ${err.message}`, 0);
+++ throw new OpenReError(`OpenRestream unreachable (${method} ${path}): ${err.message}`, 0);
+ }
+ let json = null;
+ if (text) { try { json = JSON.parse(text); } catch { json = null; } }
--+ if (!res.ok) throw new OpenReError((json && (json.detail || json.error)) || `OpenRe ${res.status} on ${method} ${path}`, res.status, json);
+++ if (!res.ok) throw new OpenReError((json && (json.detail || json.error)) || `OpenRestream ${res.status} on ${method} ${path}`, res.status, json);
+ return json;
+}
+
--+/** The OpenRe stream definition serving a Live slot, or null. */
+++/** The OpenRestream stream definition serving a Live slot, or null. */
+async function streamForSlot(managedStreamId) {
+ const r = await request('GET', `/api/v1/streams?external_ref=${encodeURIComponent(`live:managed_stream:${managedStreamId}`)}`);
+ return (r && r.streams && r.streams[0]) || null;
@@@ -646,8 +646,8 @@ index 0000000..836fd8c
@@ -0,0 +1,49 @@
+'use strict';
+/**
--+ * Admin routes for the OpenRe ingest switch (mounted at /api/admin/openre, admin only). The lead
--+ * flips a slot during a maintenance window agreed with the broadcaster (OpenRe.Stream README,
+++ * Admin routes for the OpenRestream ingest switch (mounted at /api/admin/openre, admin only). The lead
+++ * flips a slot during a maintenance window agreed with the broadcaster (OpenRestream README,
+ * "RTMP cutover runbook").
+ *
+ * GET /api/admin/openre/status configuration + switched slots
@@@ -684,11 +684,11 @@ index 0000000..836fd8c
+ try {
+ const r = await authority.setAuthority(id, String((req.body && req.body.authority) || ''), { force: Boolean(req.body && req.body.force) });
+ if (r.error) return res.status(r.status).json({ error: r.error });
--+ console.log(`[OpenRe] admin ${req.user.username} set slot ${id} ingest authority to ${r.body.ingest_authority}`);
+++ console.log(`[OpenRestream] admin ${req.user.username} set slot ${id} ingest authority to ${r.body.ingest_authority}`);
+ return res.status(r.status).json(r.body);
+ } catch (err) {
--+ console.error('[OpenRe] switch failed:', err.message);
--+ return res.status(err.status && err.status < 500 ? 409 : 502).json({ error: `OpenRe: ${err.message}` });
+++ console.error('[OpenRestream] switch failed:', err.message);
+++ return res.status(err.status && err.status < 500 ? 409 : 502).json({ error: `OpenRestream: ${err.message}` });
+ }
+});
+
@@@ -701,11 +701,11 @@ index 0000000..c0c0d85
@@ -0,0 +1,36 @@
+'use strict';
+/**
--+ * OpenRe ingest switch — Live-side schema (roadmap Wave 7, ADR-009). Additive and idempotent:
+++ * OpenRestream ingest switch — Live-side schema (roadmap Wave 7, ADR-009). Additive and idempotent:
+ *
+ * managed_streams.ingest_authority 'live' (default) | 'openre' who ingests RTMP for this slot
--+ * managed_streams.openre_stream_id the OpenRe stream definition serving the slot (std_…)
--+ * openre_sessions Live's projection of OpenRe ingest sessions it mirrors into
+++ * managed_streams.openre_stream_id the OpenRestream stream definition serving the slot (std_…)
+++ * openre_sessions Live's projection of OpenRestream ingest sessions it mirrors into
+ * `streams` (rebuilt from openre.session.* events; the
+ * revision guard makes out-of-order delivery harmless)
+ *
@@@ -744,7 +744,7 @@ index 4b5f8e4..4ac081b 100644
const router = express.Router();
+/**
--+ * A slot ingested by OpenRe.Stream (ingest_authority = 'openre') restreams from OpenRe: its
+++ * A slot ingested by OpenRestream (ingest_authority = 'openre') restreams from OpenRestream: its
+ * destinations are managed there (openre.stream), so Live refuses to edit or run them here rather
+ * than letting two systems push the same stream. Slots on Live's own ingest are unaffected.
+ */
@@@ -753,7 +753,7 @@ index 4b5f8e4..4ac081b 100644
+ if (!openre.slotIsOpenre(managedStreamId)) return false;
+ const slot = openre.slotById(managedStreamId);
+ res.status(409).json({
--+ error: 'Restreaming for this stream slot is managed on OpenRe.Stream',
+++ error: 'Restreaming for this stream slot is managed on OpenRestream',
+ managed_by: 'openre',
+ manage_url: require('../openre/openre-client').manageUrl(slot && slot.openre_stream_id),
+ });
@@@ -849,12 +849,12 @@ index 70a0857..2c5d856 100644
restream_destinations: restreamDestinations,
- });
+ };
--+ // OpenRe ingests this slot: its RTMP server and key come from OpenRe (the key is only
+++ // OpenRestream ingests this slot: its RTMP server and key come from OpenRestream (the key is only
+ // ever shown by Regenerate). Slots on Live's own ingest get exactly the response above.
+ if (openreAuthority.authorityOf(ms) === 'openre') {
+ const subject = require('../auth/identity-sync').subjectOf(ms.user_id);
+ Object.assign(body, { stream_key: null }, await openreAuthority.ingestFor(ms, subject).catch((err) => ({
--+ ingest_authority: 'openre', stream_key_managed_by: 'openre', rtmp_url: null, stream_key_hint: `OpenRe is unreachable (${err.message})`,
+++ ingest_authority: 'openre', stream_key_managed_by: 'openre', rtmp_url: null, stream_key_hint: `OpenRestream is unreachable (${err.message})`,
+ })));
+ }
+ res.json(body);
@@@ -883,14 +883,14 @@ index 70a0857..2c5d856 100644
return res.status(403).json({ error: 'Not your managed stream' });
}
--+ // OpenRe ingests this slot: OpenRe rotates its key (the old one stops working at once) and
+++ // OpenRestream ingests this slot: OpenRestream rotates its key (the old one stops working at once) and
+ // the new one is shown here, once.
+ if (openreAuthority.authorityOf(ms) === 'openre') {
+ try {
+ const subject = require('../auth/identity-sync').subjectOf(ms.user_id);
+ return res.json(await openreAuthority.rotateFor(ms, subject));
+ } catch (err) {
--+ return res.status(502).json({ error: `Could not rotate the key on OpenRe: ${err.message}` });
+++ return res.status(502).json({ error: `Could not rotate the key on OpenRestream: ${err.message}` });
+ }
+ }
+
@@@ -911,13 +911,13 @@ index 70a0857..2c5d856 100644
...(whipUrlWarning ? { whipUrlWarning } : {}),
};
+ } else if (stream.protocol === 'rtmp' && openreAuthority.slotIsOpenre(stream.managed_stream_id)) {
--+ // OpenRe ingests this slot: its server URL, the hint of its key (never the key).
+++ // OpenRestream ingests this slot: its server URL, the hint of its key (never the key).
+ const slot = openreAuthority.slotById(stream.managed_stream_id);
+ const ingest = await openreAuthority.ingestFor(slot, require('../auth/identity-sync').subjectOf(slot.user_id)).catch(() => ({}));
+ endpoint = {
+ rtmpUrl: ingest.rtmp_url || null,
+ streamKey: null,
--+ streamKeyHint: ingest.stream_key_hint || 'Press Regenerate to get your OpenRe stream key',
+++ streamKeyHint: ingest.stream_key_hint || 'Press Regenerate to get your OpenRestream stream key',
+ keyManagedBy: 'openre',
+ flvUrl: `/api/streams/rtmp-proxy/${stream.id}.flv`,
+ };
@@@ -955,8 +955,8 @@ index 70a0857..2c5d856 100644
+ let url;
+ const mirrored = openreMirror.sessionForStream(stream.id);
+ if (mirrored) {
--+ // An OpenRe session: pull HTTP-FLV from the OpenRe worker that holds it, as named by
--+ // OpenRe's playback descriptor (a loopback URL on this host; no key in it).
+++ // An OpenRestream session: pull HTTP-FLV from the OpenRestream worker that holds it, as named by
+++ // OpenRestream's playback descriptor (a loopback URL on this host; no key in it).
+ if (mirrored.state !== 'live') return res.status(404).end();
+ const pb = await require('../openre/openre-client').playback(mirrored.session_id).catch(() => null);
+ if (!pb || !pb.flv || !/^http:\/\/127\.0\.0\.1:\d+\/live\/ses_[0-9A-Z]+\.flv$/.test(pb.flv.internal_url)) return res.status(502).end();
@@@ -968,7 +968,7 @@ index 70a0857..2c5d856 100644
+ url = `http://127.0.0.1:${nmsPort}/live/${flvKey}.flv`;
+ }
--+ if (req.destroyed) return undefined; // the viewer left while OpenRe was asked
+++ if (req.destroyed) return undefined; // the viewer left while OpenRestream was asked
const http = require('http');
const upstream = http.get(url, (nmsRes) => {
if (nmsRes.statusCode !== 200) {
@@@ -980,11 +980,11 @@ index fb8ba5c..c857f5b 100644
return;
}
--+ // OpenRe.Stream ingests this slot (managed_streams.ingest_authority = 'openre'): refuse
+++ // OpenRestream ingests this slot (managed_streams.ingest_authority = 'openre'): refuse
+ // here so one stream can never be ingested twice. Slots on 'live' (the default) are
+ // untouched. See server/openre/authority.js.
+ if (require('../openre/authority').refusesLiveIngest({ managedStream, user: managedStream ? null : resolvedUser, protocol: 'rtmp' })) {
--+ console.log(`[RTMP] Rejected: ${managedStream ? `slot ${managedStream.id}` : `personal key of ${resolvedUser.username}`} is ingested by OpenRe`);
+++ console.log(`[RTMP] Rejected: ${managedStream ? `slot ${managedStream.id}` : `personal key of ${resolvedUser.username}`} is ingested by OpenRestream`);
+ const session = this.nms.getSession(id);
+ if (session) session.reject();
+ return;
@@@ -1001,9 +1001,9 @@ index 0000000..6ffdc33
@@ -0,0 +1,231 @@
+'use strict';
+
--+// OpenRe ingest switch (roadmap Wave 7, ADR-009). With OPENRE_URL unset, or a slot on 'live' (the
+++// OpenRestream ingest switch (roadmap Wave 7, ADR-009). With OPENRE_URL unset, or a slot on 'live' (the
+// default), nothing changes. With a slot switched to 'openre': Live's RTMP refuses its key, the
--+// Go Live helpers show OpenRe's URL and rotate OpenRe's key, and OpenRe session events (signed
+++// Go Live helpers show OpenRestream's URL and rotate OpenRestream's key, and OpenRestream session events (signed
+// Events deliveries, applied once, in revision order) become ordinary `streams` rows.
+
+const assert = require('assert');
@@@ -1100,7 +1100,7 @@ index 0000000..6ffdc33
+ assert.strictEqual(authority.serializeSlot(slot701), slot701, 'the same object: responses unchanged');
+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: slot701 }), false);
+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: null, user: { id: 601 } }), false);
--+ // Even a slot marked 'openre' behaves as before while OpenRe is not configured.
+++ // Even a slot marked 'openre' behaves as before while OpenRestream is not configured.
+ d.prepare("UPDATE managed_streams SET ingest_authority = 'openre' WHERE id = 701").run();
+ const marked = db.getManagedStreamById(701);
+ assert.strictEqual(authority.authorityOf(marked), 'live');
@@@ -1150,7 +1150,7 @@ index 0000000..6ffdc33
+ assert.strictEqual(db.getManagedStreamByStreamKey('livekey701aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), undefined, "Live's old key is rotated away");
+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: switched }), true, "Live's RTMP refuses the slot");
+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: switched, protocol: 'whip' }), false, 'only RTMP moved');
--+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: null, user: { id: 602 } }), false, 'personal keys of users without an OpenRe slot stay on Live');
+++ assert.strictEqual(authority.refusesLiveIngest({ managedStream: null, user: { id: 602 } }), false, 'personal keys of users without an OpenRestream slot stay on Live');
+ assert.strictEqual(authority.refusesLiveIngest({ managedStream: null, user: { id: 601 } }), true, 'a personal key cannot double-ingest a switched channel');
+ const shown = authority.serializeSlot(switched);
+ assert.strictEqual(shown.stream_key, null);
@@@ -1180,7 +1180,7 @@ index 0000000..6ffdc33
+ assert.strictEqual(mirror.hasLiveSession(streamId), true, 'stale cleanup leaves it alone');
+ assert.strictEqual(mirror.ownsStream(streamId), true, 'Live does not resume its own restreams for it');
+
--+ // Reconcile: OpenRe says live → heartbeat refreshed; says ended → row ended.
+++ // Reconcile: OpenRestream says live → heartbeat refreshed; says ended → row ended.
+ openreSessions[SES(4)] = { id: SES(4), state: 'live', revision: 2 };
+ d.prepare("UPDATE streams SET last_heartbeat = datetime('now', '-10 minutes') WHERE id = ?").run(streamId);
+ await mirror.reconcileOnce();
ddiff --git a/docs/service-manifest-proposal.json b/docs/service-manifest-proposal.json
iindex 2be2ca3..9253b95 100644
---- a/docs/service-manifest-proposal.json
++++ b/docs/service-manifest-proposal.json
@@@ -4,9 +4,9 @@
"openvibe-contracts": ">=0.13.0 <1.0.0"
},
"id": "openre",
-- "name": "OpenRe.Stream",
++ "name": "OpenRestream",
"status": "alpha",
-- "repository": "OpenVibers/OpenRe.Stream",
++ "repository": "OpenVibers/OpenRestream",
"domains": [
"openre.stream",
"ingest.openre.stream"
ddiff --git a/package.json b/package.json
iindex a7dddc5..7651bdb 100644
---- a/package.json
++++ b/package.json
@@@ -2,7 +2,7 @@
"name": "openre-stream",
"version": "0.1.0",
"private": true,
-- "description": "OpenRe.Stream: ingest and restream authority for the OpenVibe network. Stream definitions, hashed ingest keys, ingest sessions, transport workers with generations and leases, restream outputs with health and logs, recording requests to OpenVibe.Media.",
++ "description": "OpenRestream: ingest and restream authority for the OpenVibe network. Stream definitions, hashed ingest keys, ingest sessions, transport workers with generations and leases, restream outputs with health and logs, recording requests to OpenVibe.Media.",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
@@@ -23,7 +23,7 @@
],
"author": "OpenVibers",
"license": "AGPL-3.0-only",
-- "repository": "github:OpenVibers/OpenRe.Stream",
++ "repository": "github:OpenVibers/OpenRestream",
"dependencies": {
"better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
ddiff --git a/scripts/cutover-preflight.js b/scripts/cutover-preflight.js
iindex 653523f..f1789a1 100644
---- a/scripts/cutover-preflight.js
++++ b/scripts/cutover-preflight.js
@@@ -15,13 +15,13 @@
* bind OPENRE_RTMP_BIND is public and the RTMP port really listens on a public address
* dns the ingest host resolves (A/AAAA) only to this host's addresses
* port the ingest port answers an RTMP handshake from outside (see "port" below)
-- * db OpenRe's PostgreSQL database (DATABASE_URL of openre.env, one READ ONLY transaction): no data
++ * db OpenRestream's PostgreSQL database (DATABASE_URL of openre.env, one READ ONLY transaction): no data
* checksum failure, no foreign key left unvalidated, the main tables' counts
* live-env /etc/openvibe/live.env has OPENRE_URL, OPENRE_EVENTS_SECRET (32+ chars) and
* OV_OAUTH_CLIENT_SECRET, and the running Live process was started with them
* events Events has Live's openre.session.* subscription to /internal/openre-events, enabled,
* with the secret Live holds (compared by hash, never printed), and no dead deliveries
-- * slot only with --slot <id>: the Live slot and its OpenRe definition, for the per-slot step;
++ * slot only with --slot <id>: the Live slot and its OpenRestream definition, for the per-slot step;
* --slot all (or --all-slots): every Live slot, one slot:<id> line each, same rules
*
* "port": from the host itself a connection to its own public address never crosses the provider
@@@ -29,7 +29,7 @@
* that is not the host (a checkout with `npm ci` is enough), or pass --probe-url with a probe you
* trust (GET, `{host}` and `{port}` replaced, JSON answer with a boolean `open` or `reachable`).
*
-- * OpenRe and Events serve from PostgreSQL (ADR-035): their databases are read through the DATABASE_URL of their env
++ * OpenRestream and Events serve from PostgreSQL (ADR-035): their databases are read through the DATABASE_URL of their env
* files (--openre-db / --events-db take env:<file>, a postgres:// URL or a SQLite file); Live's is still its SQLite file.
*
* Nothing is written anywhere: files are opened read-only and PostgreSQL is read in READ ONLY transactions, no secret value is
@@@ -379,7 +379,7 @@ async function checkLiveEnv(ctx) {
else if (env.OPENRE_URL.replace(/\/+$/, '') !== opts.openreUrl) problems.push(`OPENRE_URL = ${env.OPENRE_URL}, expected ${opts.openreUrl}`);
if (!env.OPENRE_EVENTS_SECRET) problems.push('OPENRE_EVENTS_SECRET not set');
else if (env.OPENRE_EVENTS_SECRET.length < 32) problems.push('OPENRE_EVENTS_SECRET shorter than 32 characters (Events refuses it)');
-- if (!env.OV_OAUTH_CLIENT_SECRET) problems.push('OV_OAUTH_CLIENT_SECRET not set (Live cannot get OpenRe tokens)');
++ if (!env.OV_OAUTH_CLIENT_SECRET) problems.push('OV_OAUTH_CLIENT_SECRET not set (Live cannot get OpenRestream tokens)');
const notes = [];
if (!env.OPENRE_PUBLIC_URL) notes.push('OPENRE_PUBLIC_URL unset (default https://openre.stream)');
// The running process: systemd loaded the env file when Live started. Names only.
@@@ -447,15 +447,15 @@ async function checkSlot(ctx, id = Number(ctx.opts.slot)) {
if (live.user && live.user.is_banned) problems.push('owner is banned');
if (!live.subject) problems.push('owner has no canonical subject (they sign in to Live once)');
if (live.liveNow) problems.push(`live on Live right now (stream ${live.liveNow.id}): wait for the window`);
-- if (s.protocol !== 'rtmp' && !['rtmp', 'obs'].includes(String(s.streaming_method || '').toLowerCase())) problems.push('not an RTMP slot (OpenRe carries RTMP only)');
-- if (openre.defs.length > 1) problems.push(`${openre.defs.length} OpenRe definitions reference it`);
++ if (s.protocol !== 'rtmp' && !['rtmp', 'obs'].includes(String(s.streaming_method || '').toLowerCase())) problems.push('not an RTMP slot (OpenRestream carries RTMP only)');
++ if (openre.defs.length > 1) problems.push(`${openre.defs.length} OpenRestream definitions reference it`);
const def = openre.defs[0];
if (def) {
-- facts.push(`OpenRe ${def.id} (${def.state}${def.mirror_to_live ? ', mirrored' : ', NOT mirrored to Live'})`);
++ facts.push(`OpenRestream ${def.id} (${def.state}${def.mirror_to_live ? ', mirrored' : ', NOT mirrored to Live'})`);
const held = openre.dests.filter((d) => d.hold_reason).length;
facts.push(`${openre.dests.length} destination(s)${held ? `, ${held} held` : ''}`);
-- if (s.openre_stream_id && s.openre_stream_id !== def.id) problems.push(`Live points at ${s.openre_stream_id}, OpenRe has ${def.id}`);
-- if (openre.openSessions) facts.push(`${openre.openSessions} open OpenRe session(s)`);
++ if (s.openre_stream_id && s.openre_stream_id !== def.id) problems.push(`Live points at ${s.openre_stream_id}, OpenRestream has ${def.id}`);
++ if (openre.openSessions) facts.push(`${openre.openSessions} open OpenRestream session(s)`);
} else facts.push(openre.slotMap ? `migration_map: ${openre.slotMap.status}${openre.slotMap.reason ? ` (${openre.slotMap.reason})` : ''}` : 'not migrated yet');
if (problems.length) return ['FAIL', `${facts.join('; ')}: ${problems.join('; ')}`];
return ['PASS', facts.join('; ')];
ddiff --git a/scripts/migrate-from-live.js b/scripts/migrate-from-live.js
iindex 1fe27f6..20b7601 100755
---- a/scripts/migrate-from-live.js
++++ b/scripts/migrate-from-live.js
@@@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
/**
-- * Import Live's stream slots and restream destinations into OpenRe, from a read-only snapshot of
++ * Import Live's stream slots and restream destinations into OpenRestream, from a read-only snapshot of
* Live's database, and print the per-channel RTMP cutover checklist.
*
* node scripts/migrate-from-live.js --live-db /path/to/live-snapshot.db # dry run
@@@ -9,7 +9,7 @@
* ... [--slots 12,31] [--checklist /path/to/checklist.md]
*
* Take the snapshot with `sqlite3 /opt/openvibe.live/data/live.db ".backup /tmp/live-snapshot.db"`
-- * (Live's DB_PATH in production; never point this at the live file). Uses the OpenRe environment
++ * (Live's DB_PATH in production; never point this at the live file). Uses the OpenRestream environment
* (.env or OPENRE_ENV_FILE) for its own database and OPENRE_SECRETS_KEY; in production set
* OPENRE_ENV_FILE=/etc/openvibe/openre.env and NODE_ENV=production (a unit sets NODE_ENV itself)
* — docs/cutover.md has the command. Prints no secret: old keys are not imported and the new
ddiff --git a/scripts/set-definition-state.js b/scripts/set-definition-state.js
iindex 5789b73..add6c32 100644
---- a/scripts/set-definition-state.js
++++ b/scripts/set-definition-state.js
@@@ -1,10 +1,10 @@
#!/usr/bin/env node
'use strict';
/**
-- * Disable (or re-enable) the OpenRe stream definition of one Live slot, AS LIVE (docs/cutover.md,
++ * Disable (or re-enable) the OpenRestream stream definition of one Live slot, AS LIVE (docs/cutover.md,
* per-slot rollback). A disabled definition's keys are refused at the RTMP handshake
-- * (`stream_disabled`), so after a slot goes back to Live nobody can publish to OpenRe with the key
-- * the broadcaster got at the cutover, and OpenRe never pushes that slot's destinations. Running
++ * (`stream_disabled`), so after a slot goes back to Live nobody can publish to OpenRestream with the key
++ * the broadcaster got at the cutover, and OpenRestream never pushes that slot's destinations. Running
* sessions are not ended (Live holds no openre.session.end grant): do it with the slot offline.
*
* sudo node /opt/openre.stream/current/scripts/set-definition-state.js --slot 12 --state disabled
@@@ -38,9 +38,9 @@ async function run({ slot, state, env, fetchImpl = globalThis.fetch, log = conso
const ref = `live:managed_stream:${slot}`;
const found = await fetchImpl(`${openre}/api/v1/streams?external_ref=${encodeURIComponent(ref)}`, { headers, signal: AbortSignal.timeout(15000) });
const list = await readJson(found);
-- if (!found.ok) throw new Error(`OpenRe answered ${found.status}: ${list.code || ''} ${list.detail || ''}`.trim());
++ if (!found.ok) throw new Error(`OpenRestream answered ${found.status}: ${list.code || ''} ${list.detail || ''}`.trim());
const d = (list.streams || [])[0];
-- if (!d) throw new Error(`OpenRe has no stream definition for ${ref}`);
++ if (!d) throw new Error(`OpenRestream has no stream definition for ${ref}`);
if (d.state === state) {
log(`${d.id} (${ref}) is already ${state}`);
return { id: d.id, state, changed: false, open_session: d.session ? d.session.id : null };
@@@ -52,7 +52,7 @@ async function run({ slot, state, env, fetchImpl = globalThis.fetch, log = conso
signal: AbortSignal.timeout(15000),
});
const b = await readJson(r);
-- if (!r.ok) throw new Error(`OpenRe answered ${r.status}: ${b.code || ''} ${b.detail || ''}`.trim());
++ if (!r.ok) throw new Error(`OpenRestream answered ${r.status}: ${b.code || ''} ${b.detail || ''}`.trim());
log(`${d.id} (${ref}): ${d.state} → ${b.stream ? b.stream.state : state}${d.session ? `; session ${d.session.id} is still ${d.session.state} (not ended by this)` : ''}`);
return { id: d.id, state: b.stream ? b.stream.state : state, changed: true, open_session: d.session ? d.session.id : null };
}
ddiff --git a/scripts/subscribe-live-events.js b/scripts/subscribe-live-events.js
iindex 69b1b5b..93a0d39 100644
---- a/scripts/subscribe-live-events.js
++++ b/scripts/subscribe-live-events.js
@@@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
/**
-- * Live's OpenVibe.Events subscription to OpenRe's session events (docs/cutover.md step A5):
++ * Live's OpenVibe.Events subscription to OpenRestream's session events (docs/cutover.md step A5):
* topic `openre.session.*` → Live's `POST /internal/openre-events`, signed with the secret Live holds
* as OPENRE_EVENTS_SECRET. Events names the consumer after the calling service, so this runs AS LIVE:
* it reads Live's env file (Live's OAuth client, grant [live, events.subscription.manage,
ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex 36f3363..c0a2ba1 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * OpenRe API v1 (openre-api). Every route checks one capability for service callers; owners
++ * OpenRestream API v1 (openre-api). Every route checks one capability for service callers; owners
* (Network user JWT) act on their own streams. Errors are RFC 9457 problem+json.
*
* GET /api/v1/streams openre.stream.read list (owner, ?external_ref=live:managed_stream:12)
ddiff --git a/server/config.js b/server/config.js
iindex 7079da1..e4a440b 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * OpenRe.Stream configuration. Everything comes from the environment (.env in development,
++ * OpenRestream configuration. Everything comes from the environment (.env in development,
* /etc/openvibe/openre.env in production); .env.example documents every name.
*
* load(env) is pure so tests (and every worker process) can build a config without touching
@@@ -75,7 +75,7 @@ function load(env = process.env) {
// ── Ingest (RTMP) ───────────────────────────────────────
rtmp: {
// Public ingest port. Live's in-process ingest owns 1935 until the RTMP cutover
-- // (README "Port plan"); OpenRe listens on 1936 until then.
++ // (README "Port plan"); OpenRestream listens on 1936 until then.
port: rtmpPort,
// Further public ports served by every worker, e.g. 1935 once Live's own RTMP ingest is
// retired (README "Port plan"): URLs handed out with :1936 keep working forever.
@@@ -97,7 +97,7 @@ function load(env = process.env) {
// ── Ingest (JSMPEG) ─────────────────────────────────────
jsmpeg: {
// Public port for both the MPEG-TS HTTP POST from the broadcaster's ffmpeg and the
-- // WebSocket viewers. Live's relay owns 9710/9711 until the JSMPEG cutover; OpenRe
++ // WebSocket viewers. Live's relay owns 9710/9711 until the JSMPEG cutover; OpenRestream
// listens on 9736 until then, SO_REUSEPORT so two generations can listen during a drain.
port: jsmpegPort,
bindHost: env.OPENRE_JSMPEG_BIND || '0.0.0.0',
@@@ -113,11 +113,11 @@ function load(env = process.env) {
// ── Ingest (WebRTC: WHIP + the mediasoup SFU + viewer signaling) ──
// One worker kind 'webrtc' owns WHIP ingest, the mediasoup producers/consumers and viewer
// signaling (T4 decision 1: mediasoup is single-process, so ingest and consumption cannot be
-- // split). Live's own SFU keeps UDP 10000–10100 until the WebRTC cutover; OpenRe listens on
++ // split). Live's own SFU keeps UDP 10000–10100 until the WebRTC cutover; OpenRestream listens on
// 10200–10300 (decision 3) so both stacks run side by side.
webrtc: {
// Public HTTP port: WHIP (POST/PATCH/DELETE /whip/…) and the WS signaling endpoints
-- // (broadcaster and viewer). Live's WHIP lives on its app port 3000; OpenRe brings its own.
++ // (broadcaster and viewer). Live's WHIP lives on its app port 3000; OpenRestream brings its own.
// SO_REUSEPORT so two generations can listen during a drain.
port: webrtcPort,
bindHost: env.OPENRE_WEBRTC_BIND || '0.0.0.0',
ddiff --git a/server/db.js b/server/db.js
iindex 4bc3041..b18af68 100644
---- a/server/db.js
++++ b/server/db.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * OpenRe's durable store: one PostgreSQL database (ov_openre, ADR-035) shared by every OpenRe process.
++ * OpenRestream's durable store: one PostgreSQL database (ov_openre, ADR-035) shared by every OpenRestream process.
*
* openre-api definitions, keys, destinations (owner writes)
* openre-session-coordinator worker liveness, drain, output assignment, recordings, event relay
@@@ -21,7 +21,7 @@ const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');
/**
* The serving handle: DATABASE_URL through PgBouncer, after the migrations ran as the owner (DATABASE_DIRECT_URL).
* Without DATABASE_URL (development, tests), an embedded PGlite database: in OPENRE_PGLITE_DIR or data/pglite, or in
-- * memory with { memory: true }. PGlite is one process: run the other OpenRe processes against PostgreSQL.
++ * memory with { memory: true }. PGlite is one process: run the other OpenRestream processes against PostgreSQL.
*/
async function openDb(config, { memory = false, log = console, registry } = {}) {
const quiet = { ...log, info() {}, log() {} };
ddiff --git a/server/destination-url.js b/server/destination-url.js
iindex 7fcced0..19c88f6 100644
---- a/server/destination-url.js
++++ b/server/destination-url.js
@@@ -9,7 +9,7 @@
* user:password@, no whitespace or control characters, at most 2048 characters
* - the host may not be (or resolve to) loopback, private, link-local, CGNAT, multicast or
* unspecified addresses, unless OPENRE_DEST_ALLOW_PRIVATE is on (tests, a restream box on the
-- * operator's LAN). Live allowed private hosts; OpenRe does not by default, and the migration
++ * operator's LAN). Live allowed private hosts; OpenRestream does not by default, and the migration
* holds such destinations with a recorded reason instead of importing them enabled.
*/
const dns = require('dns');
ddiff --git a/server/discovery.js b/server/discovery.js
iindex 16a5e9c..894d61d 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -6,7 +6,7 @@
* GET /robots.txt welcomes search and AI crawlers, keeps the signed-in owner pages, sign-in,
* the API and playback out of the index, and always names the sitemap
* GET /sitemap.xml the public, server-rendered pages (server/ui/routes.js) only
-- * GET /llms.txt what OpenRe.Stream does today, in plain language
++ * GET /llms.txt what OpenRestream does today, in plain language
*
* Read-only and viewer-independent: built from the config (the public base URL) and nothing else.
*/
@@@ -31,25 +31,25 @@ 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: '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.',
++ name: 'OpenRestream',
++ 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. OpenRestream 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. OpenRe itself serves a JSON/REST API and a small server-rendered owner UI at openre.stream. The service is alpha.',
++ 'OpenRestream is the ingest and restream authority behind OpenVibe.Live: channels, discovery and watch pages stay on openvibe.live. OpenRestream itself serves a JSON/REST API and a small server-rendered owner UI at openre.stream. The service is alpha.',
'',
'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.',
++ '- With an encoder such as OBS: RTMP to rtmp://ingest.openre.stream:1936/live with the stream key OpenRestream 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 OpenRestream 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 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.',
++ '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: OpenRestream 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.',
].join('\n'),
sections: [
{ title: 'Start here', links: [
-- { title: 'OpenRe.Stream home', url: abs('/') },
++ { title: 'OpenRestream home', url: abs('/') },
{ title: 'What shipped', url: abs('/updates') },
-- { title: 'Source code', url: 'https://github.com/OpenVibers/OpenRe.Stream' },
++ { title: 'Source code', url: 'https://github.com/OpenVibers/OpenRestream' },
] },
{ title: 'Machine-readable', links: [
{ title: 'Sitemap', url: abs('/sitemap.xml') },
ddiff --git a/server/events.js b/server/events.js
iindex 8a1f564..1914753 100644
---- a/server/events.js
++++ b/server/events.js
@@@ -1,9 +1,9 @@
'use strict';
/**
-- * OpenRe → OpenVibe.Events through the openvibe-sdk transactional outbox (ADR-004).
++ * OpenRestream → OpenVibe.Events through the openvibe-sdk transactional outbox (ADR-004).
*
-- * Every OpenRe process can enqueue (the row commits in the same transaction as the change
-- * it describes); only the session coordinator runs the relay that publishes rows with OpenRe's
++ * Every OpenRestream process can enqueue (the row commits in the same transaction as the change
++ * it describes); only the session coordinator runs the relay that publishes rows with OpenRestream's
* service token (audience openvibe.events, capability events.event.publish). Events down, Network
* down or no credentials yet: rows wait and are retried; nothing in the transport path waits.
*
ddiff --git a/server/ids.js b/server/ids.js
iindex 3a368a6..d044e80 100644
---- a/server/ids.js
++++ b/server/ids.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * OpenRe identifiers: <prefix>_<ULID> (the ULID from openvibe-contracts ids, so they sort by
-- * creation time like every other OpenVibe id). Prefixes are OpenRe's own entity types.
++ * OpenRestream identifiers: <prefix>_<ULID> (the ULID from openvibe-contracts ids, so they sort by
++ * creation time like every other OpenVibe id). Prefixes are OpenRestream's own entity types.
*/
const { ids } = require('openvibe-contracts');
@@@ -20,7 +20,7 @@ const RE = Object.fromEntries(Object.entries(PREFIX).map(([k, p]) => [k, new Reg
function newId(kind, now) {
const p = PREFIX[kind];
-- if (!p) throw new TypeError(`no OpenRe id prefix for "${kind}"`);
++ if (!p) throw new TypeError(`no OpenRestream id prefix for "${kind}"`);
return `${p}_${ids.ulid(now)}`;
}
ddiff --git a/server/index.js b/server/index.js
iindex 483575b..5ab83a1 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -48,7 +48,7 @@ async function start({ config, clock, fetchImpl = globalThis.fetch, log = consol
if (require.main === module) {
require('dotenv').config({ path: process.env.OPENRE_ENV_FILE || path.join(process.cwd(), '.env') });
start().then((h) => {
-- // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's OpenRe.Stream entry): the keys poller stops, requests
++ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's OpenRestream entry): the keys poller stops, requests
// in flight get 8 s, then Valkey and the database close in that order. A database close failure exits 1;
// past the 10 s deadline the process also exits 1. Workers are separate processes and keep running.
gracefulStop({
ddiff --git a/server/lineage.js b/server/lineage.js
iindex a462cab..ab1c24f 100644
---- a/server/lineage.js
++++ b/server/lineage.js
@@@ -1,8 +1,8 @@
'use strict';
/**
-- * Live lineage for OpenRe definitions (roadmap D20): which OpenVibe.Live channel a stream belongs to,
++ * Live lineage for OpenRestream definitions (roadmap D20): which OpenVibe.Live channel a stream belongs to,
* answered by Live's canonical resolver (GET /internal/lineage/resolve, capability live.lineage.resolve,
-- * OpenRe's Network service token for audience openvibe.live) instead of a mapping of OpenRe's own.
++ * OpenRestream's Network service token for audience openvibe.live) instead of a mapping of OpenRestream's own.
* A definition is asked about when it mirrors into Live or points at a Live slot (external ref
* live:managed_stream), with its owner subject and that slot id; display names are never sent.
*
ddiff --git a/server/media-client.js b/server/media-client.js
iindex ce29809..452ae04 100644
---- a/server/media-client.js
++++ b/server/media-client.js
@@@ -10,7 +10,7 @@
* DELETE /api/v1/:app/vods/:id → drop an empty shell / an ephemeral clips-only recording
*
* The rtmp_url is the owning worker's loopback play URL (rtmp://127.0.0.1:<port>/live/<session id>):
-- * no ingest key ever leaves OpenRe. Media records and finalises; OpenRe only asks.
++ * no ingest key ever leaves OpenRestream. Media records and finalises; OpenRestream only asks.
*
* Auth: config.media.auth === 'key' sends the tenant app key (MEDIA_API_KEY), which Media's VOD
* routes accept today. 'service' sends a Network service token (audience openvibe.media); Media's
ddiff --git a/server/migrate-live.js b/server/migrate-live.js
iindex 8e6eefa..040db8a 100644
---- a/server/migrate-live.js
++++ b/server/migrate-live.js
@@@ -1,13 +1,13 @@
'use strict';
/**
* Import OpenVibe.Live stream slots (managed_streams) and their restream destinations from a
-- * read-only snapshot of Live's database into OpenRe (scripts/migrate-from-live.js is the CLI).
++ * read-only snapshot of Live's database into OpenRestream (scripts/migrate-from-live.js is the CLI).
*
* Rules (ADR-009, plan §8.1):
* - Live's database is opened read-only; nothing is written to it.
-- * - Stream keys are NEVER imported. Every imported slot gets a new OpenRe key that nobody has
++ * - Stream keys are NEVER imported. Every imported slot gets a new OpenRestream key that nobody has
* seen; the streamer gets a usable key by rotating (Live's "Regenerate" once the slot is
-- * switched to OpenRe, or openre.stream). Every key that existed before the cutover is dead
++ * switched to OpenRestream, or openre.stream). Every key that existed before the cutover is dead
* at the cutover.
* - Every source row is imported, held with a reason, or excluded with a reason, and recorded in
* migration_map. Re-running is idempotent: imported rows are skipped, held rows are retried.
@@@ -165,7 +165,7 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
continue;
}
const v = validateDestinationUrl(d.server_url, { allowPrivate: config.outputs.allowPrivateHosts });
-- const hold = !v.ok ? `server URL refused by OpenRe's destination rules: ${v.error}` : null;
++ const hold = !v.ok ? `server URL refused by OpenRestream's destination rules: ${v.error}` : null;
const input = {
platform: ['youtube', 'twitch', 'kick', 'custom'].includes(d.platform) ? d.platform : 'custom',
name: d.name || null,
@@@ -198,7 +198,7 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
count(hold ? 'held' : 'imported');
}
if (own.some(d => d.connection_id) || unbound.some(d => d.connection_id)) {
-- ch.notes.push('OAuth-linked destination(s): Live refreshed their ingest key (and created the YouTube broadcast) on every go-live; OpenRe pushes to the key stored at import. Check the platform key is a persistent one, or re-enter it on openre.stream after the cutover.');
++ ch.notes.push('OAuth-linked destination(s): Live refreshed their ingest key (and created the YouTube broadcast) on every go-live; OpenRestream pushes to the key stored at import. Check the platform key is a persistent one, or re-enter it on openre.stream after the cutover.');
}
}
return report;
@@@ -207,7 +207,7 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
/** Markdown checklist per channel, for the lead and the broadcaster. Contains no secret. */
function checklist(report, { openreUrl = 'https://openre.stream', rtmpUrl } = {}) {
const lines = [];
-- lines.push(`# OpenRe RTMP cutover checklist${report.apply ? '' : ' (dry run: nothing was written)'}`);
++ lines.push(`# OpenRestream RTMP cutover checklist${report.apply ? '' : ' (dry run: nothing was written)'}`);
lines.push('');
lines.push(`Imported ${report.counts.imported}, held ${report.counts.held}, excluded ${report.counts.excluded}, already done ${report.counts.skipped}.`);
lines.push('');
ddiff --git a/server/secrets.js b/server/secrets.js
iindex 355f01b..2ecadff 100644
---- a/server/secrets.js
++++ b/server/secrets.js
@@@ -2,7 +2,7 @@
/**
* Two kinds of secret, handled differently:
*
-- * Ingest keys (what a streamer pastes into OBS) are OpenRe's own credentials. Only their SHA-256
++ * Ingest keys (what a streamer pastes into OBS) are OpenRestream's own credentials. Only their SHA-256
* is stored; the plain key exists once, in the response that created or rotated it. 256 bits of
* randomness make a plain digest safe (no dictionary to try), and lookup stays one indexed read,
* which matters because the RTMP worker authenticates synchronously inside the publish handshake.
ddiff --git a/server/store/definitions.js b/server/store/definitions.js
iindex 2c780b0..0fded58 100644
---- a/server/store/definitions.js
++++ b/server/store/definitions.js
@@@ -234,7 +234,7 @@ function createDefinitions({ db, config, events, clock }) {
/**
* The RTMP (or later WHIP) worker's check, synchronous by design: a publish handshake is
-- * answered inside node-media-server's prePublish handler. Only OpenRe's own hashed keys count:
++ * answered inside node-media-server's prePublish handler. Only OpenRestream's own hashed keys count:
* this never looks at any other service's database.
*/
async function resolveIngestKey(plain, protocol) {
ddiff --git a/server/store/index.js b/server/store/index.js
iindex 9ab3244..6c972b0 100644
---- a/server/store/index.js
++++ b/server/store/index.js
@@@ -14,7 +14,7 @@ const { createOutputs } = require('./outputs');
const { createRecordings } = require('./recordings');
/**
-- * The handle every store gets: its transactions are SERIALIZABLE. Several OpenRe processes change the same rows (a
++ * The handle every store gets: its transactions are SERIALIZABLE. Several OpenRestream processes change the same rows (a
* publish admitted by ingest while the coordinator fails a lost worker's session); PostgreSQL detects the conflict
* and the SDK runs the loser again (40001 is retried).
*/
ddiff --git a/server/store/sessions.js b/server/store/sessions.js
iindex e55993c..94dbcb7 100644
---- a/server/store/sessions.js
++++ b/server/store/sessions.js
@@@ -191,7 +191,7 @@ function createSessions({ db, config, events, clock, definitions, workers }) {
}
/**
-- * Where to watch a session. Internal URLs are loopback addresses on the OpenRe host (Live and
++ * Where to watch a session. Internal URLs are loopback addresses on the OpenRestream host (Live and
* Media run there too); public_url is served by openre-api's /play proxy.
*/
async function playback(session) {
ddiff --git a/server/ui/layout.js b/server/ui/layout.js
iindex 4b86c83..305878b 100644
---- a/server/ui/layout.js
++++ b/server/ui/layout.js
@@@ -6,7 +6,7 @@ const ovServe = require('openvibe-shared/serve');
* pinned openvibe-shared release) the way OpenVibe.Community renders it.
*/
const NETWORK_URL = 'https://openvibe.network';
--const SITE = 'OpenRe.Stream';
++const SITE = 'OpenRestream';
function esc(v) {
return String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
@@@ -34,7 +34,7 @@ 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}}
`;
--const DESCRIPTION = 'OpenRe.Stream: stream definitions, ingest keys, sessions and restream outputs for the OpenVibe network.';
++const DESCRIPTION = 'OpenRestream: 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`;
@@@ -51,7 +51,7 @@ function renderPage({ title, body, user, canonicalPath = '/', robots = 'noindex,
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
};
const foot = { service: 'openre', variant: 'compact', mount: '#ov-footer', brandName: SITE, legalBase: config.liveUrl, updates: '/updates',
-- links: [{ heading: SITE, items: [{ name: 'Streams', url: '/streams' }, { name: 'Source code', url: 'https://github.com/OpenVibers/OpenRe.Stream' }] }] };
++ links: [{ heading: SITE, items: [{ name: 'Streams', url: '/streams' }, { name: 'Source code', url: 'https://github.com/OpenVibers/OpenRestream' }] }] };
let footerSsr = '';
try { footerSsr = require('openvibe-shared/footer').ssr({ service: 'openre', variant: 'compact', updates: '/updates' }); } catch { footerSsr = '<footer id="ov-footer"></footer>'; }
let icon = '';
@@@ -74,7 +74,7 @@ ${styles.map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">
</head>
<body>
<div id="navbar-mount"></div>
--<noscript><nav class="ore" style="padding:8px 16px"><a href="/">OpenRe.Stream</a> · <a href="/streams">Streams</a> · <a href="/sessions">Sessions</a> · ${user ? '<a href="/auth/logout">Sign out</a>' : '<a href="/auth/login">Sign in</a>'}</nav></noscript>
++<noscript><nav class="ore" style="padding:8px 16px"><a href="/">OpenRestream</a> · <a href="/streams">Streams</a> · <a href="/sessions">Sessions</a> · ${user ? '<a href="/auth/logout">Sign out</a>' : '<a href="/auth/login">Sign in</a>'}</nav></noscript>
<main id="main" class="ore">
${body}
</main>
ddiff --git a/server/ui/routes.js b/server/ui/routes.js
iindex 28eda6a..c981906 100644
---- a/server/ui/routes.js
++++ b/server/ui/routes.js
@@@ -3,7 +3,7 @@
* The standalone UI (openre.stream). Server-rendered, useful without JavaScript, forms post back
* to the same process and go through the same store rules as the API.
*
-- * / what OpenRe is, and the signed-in owner's streams
++ * / what OpenRestream is, and the signed-in owner's streams
* /streams definitions + create
* /streams/:id ingest URL, keys (rotate: the new key is shown once), settings,
* destinations (add, edit, delete, test, start, stop), sessions
@@@ -43,7 +43,7 @@ function createUiRouter({ rt, auth, limits = null }) {
if (req.caller.kind === 'user' && req.caller.subject) return true;
page(req, res, {
title: 'Sign in', canonicalPath: req.path,
-- body: `<h1>Sign in</h1><p>OpenRe.Stream uses your OpenVibe account.</p><p><a href="/auth/login?next=${encodeURIComponent(req.originalUrl)}">Sign in with OpenVibe</a></p>`,
++ body: `<h1>Sign in</h1><p>OpenRestream uses your OpenVibe account.</p><p><a href="/auth/login?next=${encodeURIComponent(req.originalUrl)}">Sign in with OpenVibe</a></p>`,
}, 401);
return false;
}
@@@ -84,17 +84,17 @@ function createUiRouter({ rt, auth, limits = null }) {
// ── Pages ─────────────────────────────────────────────────
-- // What shipped on OpenRe.Stream: the shared update log every OpenVibe site has.
-- router.get('/updates', (req, res) => page(req, res, { canonicalPath: '/updates', robots: 'index,follow', title: 'What shipped on OpenRe.Stream', body: frame.updatesBody({ service: 'openre', siteName: 'OpenRe.Stream' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));
++ // What shipped on OpenRestream: the shared update log every OpenVibe site has.
++ router.get('/updates', (req, res) => page(req, res, { canonicalPath: '/updates', robots: 'index,follow', title: 'What shipped on OpenRestream', body: frame.updatesBody({ service: 'openre', siteName: 'OpenRestream' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));
router.get('/', async (req, res) => {
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', 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.',
++ 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 OpenRestream.',
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' })}`,
++${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRestream' })}`,
});
});
@@@ -106,9 +106,9 @@ ${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRe.Stream'
function frontPage(cfg) {
const live = cfg.liveUrl;
return showcase.hero({
-- eyebrow: 'OpenRe.Stream · alpha',
++ eyebrow: 'OpenRestream · 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.',
++ lede: 'Open OpenVibe.Live, press Go Live and allow your camera: nothing to install, and no follower or subscriber threshold. OpenRestream 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' },
@@@ -116,7 +116,7 @@ ${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRe.Stream'
],
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',
++ title: 'What OpenRestream 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.' },
@@@ -162,7 +162,7 @@ ${frame.shipped({ service: 'openre', title: 'Recently shipped on OpenRe.Stream'
page(req, res, {
title: heading, canonicalPath: `/streams/${definition.id}`,
body: `<h1>${esc(heading)}</h1>
--<div class="card"><p><strong>Copy the key now.</strong> OpenRe stores only a hash of it; this page is the only time it is shown.</p>
++<div class="card"><p><strong>Copy the key now.</strong> OpenRestream stores only a hash of it; this page is the only time it is shown.</p>
<label>Server (OBS: Settings → Stream → Custom)</label><code class="secret">${esc(ep.rtmp ? ep.rtmp.url : '')}</code>
<label>Stream key</label><code class="secret">${esc(key.key)}</code></div>
<p><a href="/streams/${esc(definition.id)}">Continue to the stream</a></p>`,
@@@ -217,7 +217,7 @@ ${dests.length ? `<table><tr><th>Destination</th><th>Output</th><th>Health</th><
<div><label>Recording visibility</label><select name="recording_visibility">${['public', 'unlisted', 'private'].map(m => `<option${d.recording_visibility === m ? ' selected' : ''}>${m}</option>`).join('')}</select></div>
<div><label>Playback</label><select name="playback_visibility">${['public', 'unlisted', 'private'].map(m => `<option${d.playback_visibility === m ? ' selected' : ''}>${m}</option>`).join('')}</select></div>
<div><label>State</label><select name="state">${['active', 'disabled'].map(m => `<option${d.state === m ? ' selected' : ''}>${m}</option>`).join('')}</select></div></div>
--<label><input type="checkbox" name="mirror_to_live" value="1"${d.mirror_to_live ? ' checked' : ''}> Mirror sessions into my OpenVibe.Live channel (consent; Live also has to switch this slot to OpenRe)</label>
++<label><input type="checkbox" name="mirror_to_live" value="1"${d.mirror_to_live ? ' checked' : ''}> Mirror sessions into my OpenVibe.Live channel (consent; Live also has to switch this slot to OpenRestream)</label>
<button type="submit">Save</button>`)}</div>
<h2>Sessions</h2>${sessionTable(sessions)}`,
});
ddiff --git a/test/api-restart.test.js b/test/api-restart.test.js
iindex a623bfb..77683e7 100644
---- a/test/api-restart.test.js
++++ b/test/api-restart.test.js
@@@ -1,5 +1,5 @@
'use strict';
--// ADR-009 acceptance: "Deploying the OpenRe API does not end worker-owned transports."
++// ADR-009 acceptance: "Deploying the OpenRestream API does not end worker-owned transports."
// The API runs as its own process; a fake worker process owns a live session. The API is stopped
// (SIGTERM, as systemd does on a deploy), the session stays live and its lease keeps being renewed
// by the worker; a new API process starts and serves the same live session.
@@@ -10,7 +10,7 @@ const { load } = require('../server/config');
const { openRuntime } = require('../server/store');
if (!multiProcess()) {
-- console.log('api-restart: skipped (OpenRe processes sharing one database need PostgreSQL: npm run test:pg)');
++ console.log('api-restart: skipped (OpenRestream processes sharing one database need PostgreSQL: npm run test:pg)');
process.exit(0);
}
const t = suite('api-restart');
ddiff --git a/test/bind-switch.test.js b/test/bind-switch.test.js
iindex 328765d..86cb375 100644
---- a/test/bind-switch.test.js
++++ b/test/bind-switch.test.js
@@@ -12,7 +12,7 @@ const { localAddresses, rtmpProbe } = require('../scripts/cutover-preflight');
const { tmpDir, testEnv, child, waitFor, freePort, suite, silent, multiProcess } = require('./helpers');
if (!multiProcess()) {
-- console.log('bind-switch: skipped (OpenRe processes sharing one database need PostgreSQL: npm run test:pg)');
++ console.log('bind-switch: skipped (OpenRestream processes sharing one database need PostgreSQL: npm run test:pg)');
process.exit(0);
}
const t = suite('bind-switch');
ddiff --git a/test/destinations-ui.test.js b/test/destinations-ui.test.js
iindex 9f88fe9..353bc99 100644
---- a/test/destinations-ui.test.js
++++ b/test/destinations-ui.test.js
@@@ -53,7 +53,7 @@ t('the UI works without JavaScript: landing, sign-in prompt, streams', async ()
api = await bootApi();
const home = await request(api.base, 'GET', '/');
assert.strictEqual(home.status, 200);
-- assert.match(home.text, /OpenRe\.Stream/);
++ assert.match(home.text, /OpenRestream/);
assert.match(home.text, /theme-loader\.js/);
assert.match(home.text, /<noscript><nav/);
const signin = await request(api.base, 'GET', '/streams');
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex ed9f820..0e5568e 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -1,7 +1,7 @@
'use strict';
// The discovery routes (server/discovery.js) answer with the right content types: robots.txt
// names the sitemap, sitemap.xml lists the public pages including /, and llms.txt describes what
--// OpenRe.Stream does today. All three come from openvibe-shared/seo, the shared toolkit.
++// OpenRestream does today. All three come from openvibe-shared/seo, the shared toolkit.
const assert = require('assert');
const seo = require('openvibe-shared/seo');
const { bootApi, request, suite } = require('./helpers');
@@@ -41,10 +41,10 @@ t('llms.txt is plain text, leads with browser go-live and says what runs today',
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(r.text.startsWith('# OpenRestream\n'), 'starts with the site heading');
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/);
++ assert.match(r.text, /WHIP, RFC 9725\) and JSMPEG ingest are ported to OpenRestream and tested, but their workers do not run in production yet/);
} finally {
await api.close();
}
ddiff --git a/test/helpers.js b/test/helpers.js
iindex 7fcf590..c0e9d1e 100644
---- a/test/helpers.js
++++ b/test/helpers.js
@@@ -4,7 +4,7 @@
* manual clock, runtime/API boot helpers, child-process helpers and a tiny sequential runner.
*
* Databases (ADR-035): one per test directory, migrated. npm test: PGlite in memory, so one process (a test that
-- * spawns OpenRe processes sharing it says it is skipped); npm run test:pg (OPENRE_TEST_STORE=pg): a schema of its own
++ * spawns OpenRestream processes sharing it says it is skipped); npm run test:pg (OPENRE_TEST_STORE=pg): a schema of its own
* on the PostgreSQL + PgBouncer containers, whose URLs every spawned process of the directory gets.
*/
const crypto = require('crypto');
@@@ -68,7 +68,7 @@ async function testDb(dir) {
return await databases.get(dir);
}
--/** Several OpenRe processes on one database need PostgreSQL (PGlite is one process). */
++/** Several OpenRestream processes on one database need PostgreSQL (PGlite is one process). */
const multiProcess = () => STORE === 'pg';
/** A handle the test keeps: a runtime's or an API's close() leaves it open for the next one on the directory. */
ddiff --git a/test/jsmpeg-e2e.test.js b/test/jsmpeg-e2e.test.js
iindex 4c3c8d3..f888ac1 100644
---- a/test/jsmpeg-e2e.test.js
++++ b/test/jsmpeg-e2e.test.js
@@@ -22,7 +22,7 @@ if (!hasFfmpeg()) {
process.exit(0);
}
if (!multiProcess()) {
-- console.log('jsmpeg-e2e: skipped (the OpenRe processes share one database: PostgreSQL only, npm run test:pg)');
++ console.log('jsmpeg-e2e: skipped (the OpenRestream processes share one database: PostgreSQL only, npm run test:pg)');
process.exit(0);
}
const t = suite('jsmpeg-e2e');
@@@ -90,7 +90,7 @@ let pubA;
let sink;
let viewer;
--t('setup: stubs and the four OpenRe processes', async () => {
++t('setup: stubs and the four OpenRestream processes', async () => {
const network = await stubServer((req, res, body) => {
if (req.url === '/oauth/token') {
const p = new URLSearchParams(body);
@@@ -249,7 +249,7 @@ t('a killed worker fails its session after the lease expires', async () => {
assert.match(s.failure_reason, /lease|lost|expired/i);
});
--t('no OpenRe process crashed along the way, and no key reached a log', () => {
++t('no OpenRestream process crashed along the way, and no key reached a log', () => {
assert.strictEqual(coordinator.exitCode, null, `coordinator exited:\n${coordinator.output}`);
assert.strictEqual(api.exitCode, null, `api exited:\n${api.output}`);
for (const p of procs) {
ddiff --git a/test/lineage.test.js b/test/lineage.test.js
iindex 25ed916..af29f2b 100644
---- a/test/lineage.test.js
++++ b/test/lineage.test.js
@@@ -1,6 +1,6 @@
'use strict';
// Live lineage (D20): the coordinator asks OpenVibe.Live's canonical resolver which channel a Live-linked
--// definition belongs to, with OpenRe's service token; session events carry the answer.
++// definition belongs to, with OpenRestream's service token; session events carry the answer.
const assert = require('assert');
const { runtime, manualClock, suite, OWNER, outboxEnvelopes, silent } = require('./helpers');
const { createCoordinator } = require('../server/coordinator');
@@@ -31,7 +31,7 @@ const RESOLVED = { status: 'resolved', channel: { id: '17', slug: 'goosely', own
t('a Live-linked definition is resolved with its owner and slot; an unlinked one is never asked about', async () => {
const { rt, lineage, asked } = await setup(RESOLVED);
const linked = (await rt.store.definitions.create({ owner_subject: OWNER, mirror_to_live: true, external_refs: [{ service: 'live', type: 'managed_stream', id: '12' }] })).definition;
-- await rt.store.definitions.create({ owner_subject: OWNER, title: 'OpenRe only' });
++ await rt.store.definitions.create({ owner_subject: OWNER, title: 'OpenRestream only' });
const out = await lineage.refresh();
assert.deepStrictEqual(out, { checked: 1, resolved: 1 });
assert.deepStrictEqual(asked, [{ owner_subject: OWNER, slot_id: '12' }], 'owner subject and slot id, never a display name');
ddiff --git a/test/migration.test.js b/test/migration.test.js
iindex 2f6f3c3..9120030 100644
---- a/test/migration.test.js
++++ b/test/migration.test.js
@@@ -148,7 +148,7 @@ t('the CLI runs a dry run against a snapshot and prints the checklist', async ()
const out = execFileSync(process.execPath, [path.join(ROOT, 'scripts', 'migrate-from-live.js'), '--live-db', file], {
env: { ...process.env, ...env, OPENRE_ENV_FILE: '/nonexistent' }, cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
});
-- assert.match(out, /OpenRe RTMP cutover checklist \(dry run/);
++ assert.match(out, /OpenRestream RTMP cutover checklist \(dry run/);
assert.match(out, /@twoslots/);
});
ddiff --git a/test/preflight.test.js b/test/preflight.test.js
iindex 235a197..0806e2f 100644
---- a/test/preflight.test.js
++++ b/test/preflight.test.js
@@@ -1,6 +1,6 @@
'use strict';
// scripts/cutover-preflight.js against a mocked host: files, commands, DNS, interfaces, the port
--// probe and /api/ready are fakes; OpenRe's database is a real PostgreSQL one (PGlite) read in READ ONLY transactions,
++// probe and /api/ready are fakes; OpenRestream's database is a real PostgreSQL one (PGlite) read in READ ONLY transactions,
// Events' and Live's are real SQLite files opened read-only (Events' production PostgreSQL has the same tables).
// Covers the host as it is today (2026-09-23: everything the cutover still needs fails), a host
// ready for the first slot, each failure on its own, the off-host port check, the probe URL, the
@@@ -27,7 +27,7 @@ const LIVE_PID = 4242;
async function makeDbs() {
const dir = tmpDir();
-- // OpenRe's database: PGlite in memory, reached as the target 'pglite:openre' (host() below hands it out).
++ // OpenRestream's database: PGlite in memory, reached as the target 'pglite:openre' (host() below hands it out).
const openrePath = 'pglite:openre';
const o = await openDb(load({ NODE_ENV: 'test' }), { memory: true });
const now = Date.now();
@@@ -380,7 +380,7 @@ t('slot: offline RTMP slot with a subject and a migrated definition passes; live
const dbs = await makeDbs();
let r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));
assert.strictEqual(r.slot.status, 'PASS', r.slot.detail);
-- assert.match(r.slot.detail, /slot 12 \(@rehearsal, rtmp\/obs\) authority live; OpenRe str_1 \(active, mirrored\); 0 destination\(s\)/);
++ assert.match(r.slot.detail, /slot 12 \(@rehearsal, rtmp\/obs\) authority live; OpenRestream str_1 \(active, mirrored\); 0 destination\(s\)/);
const l = new Database(dbs.livePath);
await l.prepare('INSERT INTO streams (managed_stream_id, is_live) VALUES (12, 1)').run();
await l.prepare('DELETE FROM linked_accounts').run();
ddiff --git a/test/recording-events.test.js b/test/recording-events.test.js
iindex 87e066d..642c2fe 100644
---- a/test/recording-events.test.js
++++ b/test/recording-events.test.js
@@@ -1,6 +1,6 @@
'use strict';
// Recording requests (Media contract as Live's recorder uses it) and the event relay (outbox →
--// OpenVibe.Events with OpenRe's service token), with stub Media / Network / Events servers.
++// OpenVibe.Events with OpenRestream's service token), with stub Media / Network / Events servers.
const assert = require('assert');
const http = require('http');
const { runtime, manualClock, suite, OWNER, silent, outboxTypes } = require('./helpers');
ddiff --git a/test/rtmp-e2e.test.js b/test/rtmp-e2e.test.js
iindex 6ffe0f6..0541f0a 100644
---- a/test/rtmp-e2e.test.js
++++ b/test/rtmp-e2e.test.js
@@@ -3,7 +3,7 @@
// openre-restream-worker and openre-session-coordinator as separate child processes, a stub
// OpenVibe.Network (token endpoint), a stub OpenVibe.Events and a stub OpenVibe.Media.
//
--// Proves: ingest with OpenRe's own keys (wrong and duplicate publishers refused), restream to a
++// Proves: ingest with OpenRestream's own keys (wrong and duplicate publishers refused), restream to a
// working destination while a dead destination fails without touching the session, recording
// requested from Media with a key-free loopback URL that Media can actually pull, an API restart
// during the broadcast that ends nothing, HTTP-FLV playback through the new API, a worker
@@@ -22,7 +22,7 @@ if (!hasFfmpeg()) {
}
if (!multiProcess()) {
-- console.log('rtmp-e2e: skipped (the four OpenRe processes share one database: PostgreSQL only, npm run test:pg)');
++ console.log('rtmp-e2e: skipped (the four OpenRestream processes share one database: PostgreSQL only, npm run test:pg)');
process.exit(0);
}
const t = suite('rtmp-e2e');
@@@ -74,7 +74,7 @@ let pubA;
let sessionA;
let sink;
--t('setup: stubs and the four OpenRe processes', async () => {
++t('setup: stubs and the four OpenRestream processes', async () => {
const network = await stubServer((req, res, body) => {
if (req.url === '/oauth/token') {
const p = new URLSearchParams(body);
@@@ -263,10 +263,10 @@ t('events reached OpenVibe.Events through the outbox relay, without keys', async
assert.deepStrictEqual(started.payload.external_refs.map(r => `${r.service}:${r.type}:${r.id}`), ['live:managed_stream:5', 'live:user:42']);
});
--t('no OpenRe process crashed along the way', () => {
++t('no OpenRestream process crashed along the way', () => {
assert.strictEqual(coordinator.exitCode, null, `coordinator exited:\n${coordinator.output}`);
assert.strictEqual(api.exitCode, null, `api exited:\n${api.output}`);
-- assert.strictEqual(ingest2.exitCode, null, `generation 2 exited:\n${ingest2.output}`); // No ingest or destination key in any OpenRe process log.
++ assert.strictEqual(ingest2.exitCode, null, `generation 2 exited:\n${ingest2.output}`); // No ingest or destination key in any OpenRestream process log.
for (const p of procs) {
if (!p.output) continue;
assert.ok(!p.output.includes(keyA), 'no ingest key in logs');
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
iindex 136c1d1..0cc56b8 100644
---- a/test/service-kit.test.js
++++ b/test/service-kit.test.js
@@@ -1,10 +1,10 @@
'use strict';
/**
-- * openvibe-sdk/service in OpenRe.Stream (plan T1, lane A): the API's SIGTERM/SIGINT shutdown is the kit's
++ * openvibe-sdk/service in OpenRestream (plan T1, lane A): the API's SIGTERM/SIGINT shutdown is the kit's
* gracefulStop, not a hand-written handler. The keys poller stops, requests in flight drain for 8 s, then the
* store closes in today's order (Valkey, then the database); a database close failure exits 1,
* a clean stop exits 0, and past the 10 s deadline the process exits 1. The static half reads server/index.js;
-- * the behavioural half drives gracefulStop with the same options OpenRe passes, exits stubbed.
++ * the behavioural half drives gracefulStop with the same options OpenRestream passes, exits stubbed.
*/
const assert = require('assert');
const fs = require('fs');
ddiff --git a/test/webrtc.test.js b/test/webrtc.test.js
iindex 5872fea..82390ae 100644
---- a/test/webrtc.test.js
++++ b/test/webrtc.test.js
@@@ -28,7 +28,7 @@ function fixtureOffer() {
].join('\r\n');
}
--t('config: the WebRTC ports never collide with Live and the RTC range is OpenRe\'s own', () => {
++t('config: the WebRTC ports never collide with Live and the RTC range is OpenRestream\'s own', () => {
const c = load({});
assert.strictEqual(c.webrtc.port, 9936);
assert.strictEqual(c.webrtc.media.minPort, 10200);
@@@ -58,7 +58,7 @@ t('WHIP SDP: parse, DTLS role, RTP parameters and the answer', () => {
assert.strictEqual(rtp.mid, '0');
const transport = { iceParameters: { usernameFragment: 'uf', password: 'pw' }, iceCandidates: [{ foundation: '1', protocol: 'udp', priority: 1, ip: '203.0.113.7', port: 10200, type: 'host' }], dtlsParameters: { fingerprints: [{ algorithm: 'sha-256', value: 'AA:BB' }] } };
-- const answer = sdp.buildSdpAnswer(transport, offer, { video: { rtpParameters: rtp } }, { serverName: 'OpenRe' });
++ const answer = sdp.buildSdpAnswer(transport, offer, { video: { rtpParameters: rtp } }, { serverName: 'OpenRestream' });
const parsedAnswer = sdp.parse(answer);
assert.strictEqual(parsedAnswer.media[0].direction, 'recvonly');
assert.strictEqual(parsedAnswer.media[0].setup, 'passive');
ddiff --git a/workers/restream-worker.js b/workers/restream-worker.js
iindex 61c3fbf..0bf9983 100644
---- a/workers/restream-worker.js
++++ b/workers/restream-worker.js
@@@ -31,7 +31,7 @@ function createRestreamWorker({ rt, log = console, exit = (code) => process.exit
},
});
-- /** The loopback source URL for a session's protocol, or null when OpenRe cannot restream it yet. */
++ /** The loopback source URL for a session's protocol, or null when OpenRestream cannot restream it yet. */
async function inputUrlFor(session) {
if (session.protocol === 'rtmp') {
const pb = await store.sessions.playback(session);
@@@ -73,7 +73,7 @@ function createRestreamWorker({ rt, log = console, exit = (code) => process.exit
egressBase = `http://127.0.0.1:${ep.egressPort}`;
} else {
input = await inputUrlFor(session);
-- if (!input) { await store.outputs.report(row.id, { state: 'failed', last_error: `restream from ${session.protocol} sessions is not supported by OpenRe yet`, ended_at: Date.now() }, { workerId: runtime.me.id }); continue; }
++ if (!input) { await store.outputs.report(row.id, { state: 'failed', last_error: `restream from ${session.protocol} sessions is not supported by OpenRestream yet`, ended_at: Date.now() }, { workerId: runtime.me.id }); continue; }
}
// Let the ingest settle before pulling (Live waits 3 s for node-media-server's FLV).
if (session.live_at && Date.now() - session.live_at < config.outputs.startDelayMs) continue;
ddiff --git a/workers/restream/ffmpeg-args.js b/workers/restream/ffmpeg-args.js
iindex 5aa69fe..6194c58 100644
---- a/workers/restream/ffmpeg-args.js
++++ b/workers/restream/ffmpeg-args.js
@@@ -3,7 +3,7 @@
* FFmpeg argument building for restream outputs, ported from OpenVibe.Live
* server/streaming/restream-manager.js (quality presets, destination URL normalisation, SRT,
* output flags, friendly errors). Kept byte-for-byte equivalent where it matters so a stream that
-- * restreams correctly from Live restreams the same way from OpenRe.
++ * restreams correctly from Live restreams the same way from OpenRestream.
*
* RTMP → destination is a codec copy (zero CPU); a JSMPEG source (the worker's MPEG-TS data tap,
* read over HTTP) is re-encoded with encodingArgs(). A WebRTC source is mediasoup PlainRTP read
ddiff --git a/workers/rtmp-ingest.js b/workers/rtmp-ingest.js
iindex fb4229f..b6a193e 100644
---- a/workers/rtmp-ingest.js
++++ b/workers/rtmp-ingest.js
@@@ -13,7 +13,7 @@
* published in its worker row, so consumers always reach the process that holds the
* publisher.
*
-- * Authentication happens inside the publish handshake, synchronously, against OpenRe's hashed
++ * Authentication happens inside the publish handshake, synchronously, against OpenRestream's hashed
* keys only. An accepted publish is renamed from /live/<key> to /live/<session id> before
* node-media-server registers it, so the key never appears in any play URL, log or descriptor.
*
ddiff --git a/workers/runtime.js b/workers/runtime.js
iindex 63341d2..1c4d2c9 100644
---- a/workers/runtime.js
++++ b/workers/runtime.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * The worker interface every OpenRe transport worker implements (RTMP ingest, restream, and the
++ * The worker interface every OpenRestream transport worker implements (RTMP ingest, restream, and the
* WHIP/WebRTC/SFU/JSMPEG kinds as they are ported):
*
* const w = createWorkerRuntime({ rt, kind, log, hooks });
ddiff --git a/workers/thumbnails.js b/workers/thumbnails.js
iindex d1f2c90..1c4440f 100644
---- a/workers/thumbnails.js
++++ b/workers/thumbnails.js
@@@ -4,7 +4,7 @@
* and grabs one frame from it every interval, then uploads it to Media as an object under the `live`
* namespace (grant media.object.upload ns live); the object's public URL becomes the session's
* thumbnail_url and travels on the session state and its openre.session.* events. Live can no longer
-- * read a local FLV/SFU/relay for OpenRe-owned sessions, so the frame has to come from here.
++ * read a local FLV/SFU/relay for OpenRestream-owned sessions, so the frame has to come from here.
*
* The grabber is protocol-agnostic: the caller passes the ffmpeg *input* arguments for its source
* (an RTMP/HTTP-FLV URL, the JSMPEG data tap, or an SDP file for a PlainRTP consumer) and this module
ddiff --git a/workers/webrtc/sdp.js b/workers/webrtc/sdp.js
iindex cfdb83e..d66e2ca 100644
---- a/workers/webrtc/sdp.js
++++ b/workers/webrtc/sdp.js
@@@ -2,7 +2,7 @@
/**
* WHIP SDP <-> mediasoup bridging, ported from OpenVibe.Live server/streaming/whip-handler.js
* (offer validation, DTLS parameter extraction, RTP parameter extraction against the router's
-- * capabilities, and the answer builder). The Live-specific stream-row code is not ported: OpenRe
++ * capabilities, and the answer builder). The Live-specific stream-row code is not ported: OpenRestream
* validates the offer, negotiates codecs and builds the answer, and nothing here touches a database.
*
* `sdp-transform` is required lazily so the module (and the whole worker) can be loaded — and its
@@@ -187,7 +187,7 @@ function extractRtpParameters(media, routerCapabilities, mediaIndex = 0) {
* BUNDLE group (RFC 8843 §7.3.3), so a browser publisher whose extra section (a data channel, a
* codec the router lacks) is rejected is not refused by its own SDP parser.
*/
--function buildSdpAnswer(transport, offerSdp, producersByKind, { serverName = 'OpenRe.Stream', fallbackAddress = '127.0.0.1' } = {}) {
++function buildSdpAnswer(transport, offerSdp, producersByKind, { serverName = 'OpenRestream', fallbackAddress = '127.0.0.1' } = {}) {
const { iceParameters, iceCandidates, dtlsParameters } = transport;
const fingerprint = selectDtlsFingerprint(dtlsParameters.fingerprints);
const setup = getDtlsSetupAttribute(offerSdp) === 'passive' ? 'active' : 'passive';
@@@ -316,7 +316,7 @@ function buildIceFragment(iceParameters, mirror = {}) {
* listens on; the worker's PlainRTP consumer is already sending to it.
*/
function buildEgressSdp({ video, audio } = {}) {
-- const lines = ['v=0', 'o=- 0 0 IN IP4 127.0.0.1', 's=OpenRe.Stream WebRTC egress', 'c=IN IP4 127.0.0.1', 't=0 0'];
++ const lines = ['v=0', 'o=- 0 0 IN IP4 127.0.0.1', 's=OpenRestream WebRTC egress', 'c=IN IP4 127.0.0.1', 't=0 0'];
const section = (type, c, port) => {
const pt = c.payloadType;
const codecName = (c.mimeType || `${type}/VP8`).split('/')[1];
ddiff --git a/workers/webrtc/sfu.js b/workers/webrtc/sfu.js
iindex 76c1fc8..d199ee5 100644
---- a/workers/webrtc/sfu.js
++++ b/workers/webrtc/sfu.js
@@@ -1,14 +1,14 @@
'use strict';
/**
-- * The mediasoup SFU, one Router per OpenRe session, ported from OpenVibe.Live
++ * The mediasoup SFU, one Router per OpenRestream session, ported from OpenVibe.Live
* server/streaming/webrtc-sfu.js. The differences that matter:
*
-- * - a "room" is an OpenRe ingest session id (not Live's `stream-<id>`), so a router's lifetime
++ * - a "room" is an OpenRestream ingest session id (not Live's `stream-<id>`), so a router's lifetime
* is exactly the session's;
* - consumer/producer/transport bookkeeping is unchanged, so the viewer signaling and the
* PlainRTP egress paths behave like Live's;
* - PlainRTP consumers (restream, Media's RTP recording, thumbnails) send to an explicit remote
-- * ip:port the caller chose, because on OpenRe the consumer side (ffmpeg) runs in another process.
++ * ip:port the caller chose, because on OpenRestream the consumer side (ffmpeg) runs in another process.
*
* `mediasoup` is required lazily: the module loads (and its turn-free helpers run) on a host where
* the native dependency is not installed, and `createSfu().available` is false there. The worker
ddiff --git a/workers/webrtc/signaling.js b/workers/webrtc/signaling.js
iindex 7b58cb5..7962f6e 100644
---- a/workers/webrtc/signaling.js
++++ b/workers/webrtc/signaling.js
@@@ -2,7 +2,7 @@
/**
* WS signaling for WebRTC, modelled on the producer and viewer halves of OpenVibe.Live's
* server/streaming/broadcast-server.js so Live's broadcast and watch pages can point their
-- * mediasoup-client at OpenRe:
++ * mediasoup-client at OpenRestream:
*
* ws://<host>:<port>/b/<ingest key> the broadcaster (Live's role: produce into the SFU)
* ws://<host>:<port>/w/<session playback id> a viewer (consume; never carries the key)
@@@ -129,7 +129,7 @@ function createSignaling({ sfu, iceServers, admit, endSession, log = console })
try { adm = await admit(decodeURIComponent(bMatch[1])); } catch (err) { adm = { error: err.message }; }
if (!adm || adm.error) {
log.log(`[webrtc] broadcast refused (${adm && adm.error}) from ${socket.remoteAddress || '?'}`);
-- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nX-OpenRe-Error: ' + (adm && adm.error || 'refused') + '\r\n\r\n');
++ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nX-OpenRestream-Error: ' + (adm && adm.error || 'refused') + '\r\n\r\n');
socket.destroy();
return true;
}