OpenVibe.Live/Docs
Source on GitHub

Restreaming#

How a live OpenVibe stream is forwarded to other platforms, what each path does to the picture, what the broadcaster sees, and how the forwarders are kept in step with the stream's lifecycle.

Two forwarders#

PathModuleDestinationsWhat happens to the video
ffmpeg restreamsserver/streaming/restream-manager.jsTwitch, YouTube, Kick, any RTMP/RTMPS or SRT ingest (restream_destinations rows)RTMP source: codec copy (nothing re-encoded). Browser/WHIP/JSMPEG source: re-encoded to H.264/AAC on the server with the destination's quality preset
RobotStreamer passthroughserver/integrations/rs-passthrough-relay.jsOne robot per stream slot (robotstreamer_integrations)Zero re-encode: the source's RTP is pulled from our mediasoup SFU and written straight into a werift peer connection joined to RobotStreamer's SFU

The RobotStreamer path needs an SFU producer, so it serves browser and OBS/WHIP sources. RTMP and JSMPEG streams get the chat mirror only (robotstreamer-service.js skips the relay for them instead of letting it wait for a producer that never comes).

Latency#

SRT destinations#

A destination whose server URL starts with srt://host:port is sent as MPEG-TS in caller mode (pkt_size=1316). The "stream key" field becomes the SRT streamid; the destination's srt_latency_ms and optional srt_passphrase (10–79 characters, AES per the SRT spec) travel as URL options. The passphrase is stored server-side and never returned to the client (has_srt_passphrase only). Command lines in the log are redacted (server/utils/redact.js masks passphrase=, streamid= and the key segment).

Live acknowledgement and errors (ffmpeg)#

ffmpeg is spawned with -progress pipe:1 -stats_period 1. The first progress block with frame > 0 (or out_time > 0 for a codec copy) is the live signal — it means the ingest accepted the stream and is taking data. Before this the dashboard guessed "Live" after 15 s regardless. If no progress arrives within 20 s the run is killed with a "no response from the ingest" error so the retry machinery and the dashboard see a real failure instead of a "Starting…" that never ends.

The last stderr lines of a failed run are kept raw (session.lastErrorRaw, logged) and mapped to a sentence a streamer can act on (RestreamManager.friendlyFfmpegError): rejected key, unreachable or unresolvable ingest, TLS failure, connection closed by the platform, SRT passphrase mismatch, bad encoder option. Restarts back off exponentially (5 s → 2 min, 30 attempts); a destination that never went live and crashes four times in a row is put in a persisted cooldown (circuit breaker) so it is not hammered on every go-live.

GET /api/restream/status returns, per live stream and destination: status, platform, transport (rtmp/srt), liveAt, uptimeMs, restartAttempts/maxRestartAttempts, nextRestartAt, lastError and the latest progress (fps, bitrate_kbps, speed, dropped, frame). It also carries robotstreamer[streamId] — the passthrough state below.

RobotStreamer passthrough lifecycle#

The relay must never outlive its source: RobotStreamer keeps a robot "live" for exactly as long as our producers exist on its SFU, so a relay left running after the streamer stopped shows RS viewers a live robot with a black picture. The session therefore ends on any of:

  1. producer-removed for the stream's video producer on our SFU (tab closed, room closed);
  2. the stream row going not-live (checked every 5 s as a backstop for end paths that never
    touch the SFU room — stale-heartbeat sweeps, an ingest handler calling db.endStream);
  3. an explicit stop: the broadcaster's Stop button (POST /api/robotstreamer/restream/stop),
    the integration being disabled, or any of the stream-end paths below.

Restarts (RS websocket closed, ICE/DTLS to RS lost, the run throwing, a producer being replaced on reconnect) first re-check that the source is live, then back off 3 s → 30 s and give up after 12 consecutive attempts. A run that stayed live for a minute resets the counter. A session that gave up stays visible as failed with its reason until Retry (POST /api/robotstreamer/restream/start) relaunches it.

rsPassthroughRelay.status(streamId) (in /api/restream/status) reports state (starting | live | restarting | failed), restarts, last_error / last_restart_reason, next_restart_at, uptime_ms, video_codec, has_audio, keyframe_requests, ingest_stalls and RS's own RTCP view of us (rs.loss_pct, rs.rtt_ms, rs.nacks, rs.plis).

Every stream-end path stops the forwarders#

End pathWhereStops
Streamer ends the streamDELETE /api/streams/:idRS, chat relays, AI bots, restreams (via room close), voice channel
Broadcaster socket gone for 60 sbroadcast-server.js grace timer → _stopForwarders()RS, restreams, chat relays, AI bots
OBS stops (RTMP donePublish)index.js rtmpServer.on('unpublish')restreams, RS, chat relays, AI bots
WHIP session endswhip-handler.js endActiveWhipStreamrestreams, RS, chat relays, AI bots, room
No heartbeat for 5 minindex.js stale sweepRS, chat relays, AI bots, restreams, room
Admin force-endDELETE /api/admin/streams/:idRS, restreams, chat relays, AI bots, room, SFU room, voice channel
Process shutdownindex.js SIGTERM handlerrsPassthroughRelay.stopAll() closes each RS peer so RS's viewers get consumerClosed immediately

Dashboard#

The live-controls restream panel (public/js/broadcast.js updateRestreamControlPanel) polls /api/restream/status every 5 s while live and shows, per destination, a badge (Idle / Starting / Live / Error / Failed / Paused) plus a detail line: uptime, fps and bitrate, an encoder-behind warning when speed < 0.95×, dropped frames, the last error, and a countdown to the next retry. The RobotStreamer row is driven by the server's passthrough state when the server relay owns the robot (its Start/Stop reach the server; before, the button only touched the browser's unused publisher). The stream-slot settings form (broadcast-workspace.js) exposes per destination: quality preset, video/audio bitrate, fps, x264 encoder speed, and for srt:// URLs the latency and passphrase.

Tests#

This page is rendered from docs/restreaming.md in the OpenVibe.Live repository. Found a mistake? Edit it there.