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
Changes
@@@ -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)@@@ -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:3000NoDelay=true# Node's libuv expects a non-blocking descriptor.NonBlocking=true@@@ -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-ERRORThere 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 \```textOpenVibe.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.@@@ -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.');}@@@ -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'),
@@ -32,10 +32,10 @@ This repository contains the OpenVibe.Live server, browser assets, and runtime c
@@ -32,10 +32,10 @@ This repository contains the OpenVibe.Live server, browser assets, and runtime c
### Streaming support
### 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).
- optional WebRTC SFU via `mediasoup` (ports 11000-11300 in production).
- JSMPEG relay (TLS relay ports 9710-9789 via nginx stream).
- 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.
- real-time broadcast and control channels.
### Media (VODs / clips / pastes / thumbnails)
### Media (VODs / clips / pastes / thumbnails)
@@ -15,7 +15,9 @@ Description=OpenVibe.Live HTTP socket
@@ -15,7 +15,9 @@ Description=OpenVibe.Live HTTP socket
# and connections arriving in that gap were refused. The socket must outlive every restart.
# and connections arriving in that gap were refused. The socket must outlive every restart.
[Socket]
[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
NoDelay=true
# Node's libuv expects a non-blocking descriptor.
# Node's libuv expects a non-blocking descriptor.
NonBlocking=true
NonBlocking=true
@@ -7,7 +7,7 @@ OpenVibe.Live accepts live video over **WHIP** (WebRTC-HTTP Ingestion Protocol,
@@ -7,7 +7,7 @@ OpenVibe.Live accepts live video over **WHIP** (WebRTC-HTTP Ingestion Protocol,
## Endpoint
## Endpoint
```
```
-POST https://ingest.openvibe.live/whip/{streamKey}
+POST https://whip.openvibe.live/whip/{streamKey}
Content-Type: application/sdp
Content-Type: application/sdp
```
```
@@ -76,7 +76,7 @@ Access-Control-Expose-Headers: Location, X-WHIP-ERROR
@@ -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:
There are no cookies involved, so `credentials` is never needed — call `fetch()` normally. You can verify from a shell:
```bash
```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 'Origin: https://my-static-site.example' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
-H 'Access-Control-Request-Headers: content-type'
@@ -92,7 +92,7 @@ curl -si -X OPTIONS https://ingest.openvibe.live/whip/x \
@@ -92,7 +92,7 @@ curl -si -X OPTIONS https://ingest.openvibe.live/whip/x \
```text
```text
OpenVibe.Live WHIP ingest — browser publisher requirements
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).
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.
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.
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.
@@ -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>
<video id="preview" autoplay muted playsinline></video>
<button id="go">Go live</button>
<button id="go">Go live</button>
<script>
<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;
let pc, resourceUrl;
async function goLive() {
async function goLive() {
@@ -161,7 +161,7 @@ Things worth knowing:
@@ -161,7 +161,7 @@ Things worth knowing:
## Other clients
## Other clients
- **OBS Studio (30+):** *Settings → Stream → Service: WHIP*, Server = the WHIP URL, Bearer Token = your stream key.
- **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.
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.
@@ -242,7 +242,7 @@ app.set('trust proxy', 2); // Two hops: Cloudflare → nginx → Node
@@ -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
// 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
// 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'.
// 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
// 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
// in start(), which runs after this middleware is built — a value captured here would be
@@ -1282,25 +1282,8 @@ async function start() {
@@ -1282,25 +1282,8 @@ async function start() {
// ignore malformed host
// 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)) {
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.');
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.');
}
}
@@ -258,9 +258,15 @@ assert.ok(
@@ -258,9 +258,15 @@ assert.ok(
indexSrc.includes('TURN server:') && indexSrc.includes('not configured (STUN-only'),
indexSrc.includes('TURN server:') && indexSrc.includes('not configured (STUN-only'),
'server/index.js must log TURN configuration status at startup'
'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(
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(
assert.ok(
indexSrc.includes('WHIP_PUBLIC_URL is using http:// in production'),
indexSrc.includes('WHIP_PUBLIC_URL is using http:// in production'),