openvibers/openvibe.live Commits

Live listens on 127.0.0.1:3000 only (0.0.0.0 answered on the public IP, past Cloudflare and nginx); WHIP is advertised on whip.openvibe.live behind Cloudflare and RTMP on DNS-only ingest.openvibe.live (the registry had RTMP on openvibe.live, which Cloudflare does not carry); drop the announced-IP-vs-WHIP-host warning

OP OpenVibers committed 23 Sept 2026 · 047f895ff2 · 5 files, +22 −31


Changes

ddiff --git a/README.md b/README.md
iindex b81497b3..c6dbba5f 100644
---- a/README.md
++++ b/README.md
@@@ -32,10 +32,10 @@ This repository contains the OpenVibe.Live server, browser assets, and runtime c
### Streaming support
--- RTMP ingest using `node-media-server` (ports 1935 / 9935, public host `ingest.openvibe.live`).
++- RTMP ingest using `node-media-server` (ports 1935 / 9935, public host `ingest.openvibe.live`, DNS-only: Cloudflare does not carry RTMP).
- optional WebRTC SFU via `mediasoup` (ports 11000-11300 in production).
- JSMPEG relay (TLS relay ports 9710-9789 via nginx stream).
--- WHIP/HTTP ingestion support (`ingest.openvibe.live`) — open CORS, so even a backend-less web page can publish; see [docs/whip.md → Publishing from a browser](docs/whip.md#publishing-from-a-browser) and the hosted [browser publisher](https://openvibe.live/whip-publisher.html).
++- WHIP/HTTP ingestion support (`whip.openvibe.live`, behind Cloudflare; `ingest.openvibe.live` still answers for older encoder configs) — open CORS, so even a backend-less web page can publish; see [docs/whip.md → Publishing from a browser](docs/whip.md#publishing-from-a-browser) and the hosted [browser publisher](https://openvibe.live/whip-publisher.html).
- real-time broadcast and control channels.
### Media (VODs / clips / pastes / thumbnails)
ddiff --git a/deploy/systemd/openvibe-live.socket b/deploy/systemd/openvibe-live.socket
iindex 7bf8f1a7..eb9f73b3 100644
---- a/deploy/systemd/openvibe-live.socket
++++ b/deploy/systemd/openvibe-live.socket
@@@ -15,7 +15,9 @@ Description=OpenVibe.Live HTTP socket
# and connections arriving in that gap were refused. The socket must outlive every restart.
[Socket]
--ListenStream=0.0.0.0:3000
++# Loopback only: nginx is the only client. 0.0.0.0 left Live answering on the public IP, past
++# Cloudflare and nginx (so past the client-address headers nginx sets), until 2026-09-23.
++ListenStream=127.0.0.1:3000
NoDelay=true
# Node's libuv expects a non-blocking descriptor.
NonBlocking=true
ddiff --git a/docs/whip.md b/docs/whip.md
iindex 2bb393c9..6e2b020b 100644
---- a/docs/whip.md
++++ b/docs/whip.md
@@@ -7,7 +7,7 @@ OpenVibe.Live accepts live video over **WHIP** (WebRTC-HTTP Ingestion Protocol,
## Endpoint
```
--POST https://ingest.openvibe.live/whip/{streamKey}
++POST https://whip.openvibe.live/whip/{streamKey}
Content-Type: application/sdp
```
@@@ -76,7 +76,7 @@ Access-Control-Expose-Headers: Location, X-WHIP-ERROR
There are no cookies involved, so `credentials` is never needed — call `fetch()` normally. You can verify from a shell:
```bash
--curl -si -X OPTIONS https://ingest.openvibe.live/whip/x \
++curl -si -X OPTIONS https://whip.openvibe.live/whip/x \
-H 'Origin: https://my-static-site.example' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
@@@ -92,7 +92,7 @@ curl -si -X OPTIONS https://ingest.openvibe.live/whip/x \
```text
OpenVibe.Live WHIP ingest — browser publisher requirements
--1. POST the SDP offer to https://ingest.openvibe.live/whip/<streamKey> with Content-Type: application/sdp. No cookies, no other auth; CORS is open (*).
++1. POST the SDP offer to https://whip.openvibe.live/whip/<streamKey> with Content-Type: application/sdp. No cookies, no other auth; CORS is open (*).
2. Expect 201; body = SDP answer (application/sdp); Location header = session resource URL (read it — it is exposed via CORS).
3. Offer: one audio + one video m-section, a=setup:actpass, BUNDLE + rtcp-mux, single encoding (no simulcast). Codecs: VP8 or H.264 (42e01f, packetization-mode=1) + Opus 48k. AV1/VP9/HEVC are ignored.
4. No STUN/TURN needed (server is ICE-lite); do not wait for ICE gathering; PATCH/trickle is optional.
@@@ -105,7 +105,7 @@ A complete publisher is about forty lines. This is the core of [`whip-publisher.
<video id="preview" autoplay muted playsinline></video>
<button id="go">Go live</button>
<script>
--const WHIP_URL = 'https://ingest.openvibe.live/whip/<your-stream-key>';
++const WHIP_URL = 'https://whip.openvibe.live/whip/<your-stream-key>';
let pc, resourceUrl;
async function goLive() {
@@@ -161,7 +161,7 @@ Things worth knowing:
## Other clients
- **OBS Studio (30+):** *Settings → Stream → Service: WHIP*, Server = the WHIP URL, Bearer Token = your stream key.
--- **FFmpeg (7.0+):** `ffmpeg -re -i input.mp4 -c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p -c:a libopus -f whip "https://ingest.openvibe.live/whip/<streamKey>"`
--- **GStreamer:** `... ! whipsink whip-endpoint="https://ingest.openvibe.live/whip/<streamKey>"` (needs `gst-plugins-rs`).
++- **FFmpeg (7.0+):** `ffmpeg -re -i input.mp4 -c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p -c:a libopus -f whip "https://whip.openvibe.live/whip/<streamKey>"`
++- **GStreamer:** `... ! whipsink whip-endpoint="https://whip.openvibe.live/whip/<streamKey>"` (needs `gst-plugins-rs`).
The Broadcast page shows ready-to-paste commands for each with your key filled in. See [Broadcasting Guide](broadcasting.md) for the other ingest methods.
ddiff --git a/server/index.js b/server/index.js
iindex e8798575..1f3b897a 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -242,7 +242,7 @@ app.set('trust proxy', 2); // Two hops: Cloudflare → nginx → Node
// RTMP FLV is now proxied same-origin via /api/streams/rtmp-proxy/:id.flv — no external CSP entry needed
// The hosted browser WHIP publisher (/whip-publisher.html) POSTs its SDP offer to the
--// dedicated WHIP host when one is configured (ingest.openvibe.live), which is a different
++// dedicated WHIP host when one is configured (whip.openvibe.live), which is a different
// origin from the page itself and would otherwise be blocked by connect-src 'self'.
// Resolved per request, not at boot: config.whip is (re)filled by the URL-registry refresh
// in start(), which runs after this middleware is built — a value captured here would be
@@@ -1282,25 +1282,8 @@ async function start() {
// ignore malformed host
}
}
-- function hostsEquivalent(a, b) {
-- const normalize = (value) => String(value || '').toLowerCase().replace(/^[\[\]]+/g, '').replace(/[\[\]]+$/g, '');
-- const localHosts = new Set(['localhost', '127.0.0.1', '::1']);
-- const na = normalize(a);
-- const nb = normalize(b);
-- if (na === nb) return true;
-- return localHosts.has(na) && localHosts.has(nb);
-- }
--
-- if (config.whip?.publicUrl && config.mediasoup?.announcedIp) {
-- try {
-- const whipHost = new URL(config.whip.publicUrl).hostname;
-- if (!hostsEquivalent(config.mediasoup.announcedIp, whipHost)) {
-- console.warn('[Server] WARNING: MEDIASOUP_ANNOUNCED_IP does not match WHIP_PUBLIC_URL host. This may cause incorrect ICE candidate advertisement for WHIP/WebRTC.');
-- }
-- } catch (e) {
-- // ignore malformed host
-- }
-- }
++ // No check of MEDIASOUP_ANNOUNCED_IP against the WHIP host: ICE candidates carry the announced
++ // IP whatever host the signaling reached, and whip.openvibe.live is behind Cloudflare.
if (config.nodeEnv === 'production' && config.mediasoup?.announcedIp && ['127.0.0.1', 'localhost', '::1'].includes(config.mediasoup.announcedIp)) {
console.warn('[Server] WARNING: Mediasoup announcedIp is configured as a local address. External WebRTC clients may be unable to connect. Set MEDIASOUP_ANNOUNCED_IP to your public WHIP/WebRTC hostname.');
}
ddiff --git a/test/sfu-viewer-stability.test.js b/test/sfu-viewer-stability.test.js
iindex 9d6e19f5..e0fc3c3b 100644
---- a/test/sfu-viewer-stability.test.js
++++ b/test/sfu-viewer-stability.test.js
@@@ -258,9 +258,15 @@ assert.ok(
indexSrc.includes('TURN server:') && indexSrc.includes('not configured (STUN-only'),
'server/index.js must log TURN configuration status at startup'
);
++// The WHIP host is behind Cloudflare (whip.openvibe.live), so it never resolves to the announced IP;
++// ICE candidates carry the announced IP regardless. Only a local announced IP is worth a warning.
assert.ok(
-- indexSrc.includes('MEDIASOUP_ANNOUNCED_IP does not match WHIP_PUBLIC_URL host'),
-- 'server/index.js must warn when announced IP does not match the public WHIP host'
++ !indexSrc.includes('MEDIASOUP_ANNOUNCED_IP does not match WHIP_PUBLIC_URL host'),
++ 'server/index.js must not compare the announced IP with the (proxied) WHIP host'
++);
++assert.ok(
++ indexSrc.includes('Mediasoup announcedIp is configured as a local address'),
++ 'server/index.js must warn when the announced IP is a local address'
);
assert.ok(
indexSrc.includes('WHIP_PUBLIC_URL is using http:// in production'),