livqenoDocs

Live StreamingOBS & Encoder Ingest

OBS Settings for Livqeno

Broadcast into a live stream from OBS, vMix, ffmpeg or a hardware encoder, over WHIP or RTMP/RTMPS.

Ingest lets a host go live from OBS, vMix, ffmpeg or a hardware encoder instead of a browser. Use it for produced streams: overlays, scenes, several cameras, or a server-side video source.

Where this fits: your backend creates the stream as usual (Streams & lifecycle), then creates an ingest for it. That gives you a server URL and a stream key to paste into OBS. The encoder joins the stream's room as a host, so RTC viewers, HLS viewers and restream destinations all see it exactly like a browser host.

There are two ways in:

WHIPRTMP / RTMPS
OBS version30 or newerAny
LatencyLowest (WebRTC end to end)Low
AudioOpus, passed straight throughAAC, converted to Opus by Livqeno
Best forOBS on a modern machineOlder encoders, vMix, hardware encoders, ffmpeg

Both need H.264 video with no B-frames. That's the setting people most often get wrong, so it's covered for each encoder below.

1. Create the ingest

One ingest per stream. Choose the protocol when you create it: RTMP (the default, covers both rtmp:// and rtmps://) or WHIP.

const ingress = await livqeno.liveStreams.ingress.create(stream.id, { protocol: 'RTMP' });
 
// Shown only now, and again after reset(). Store it or hand it to the host.
console.log(ingress.rtmpsUrl, ingress.streamKey);
{
  "id": "ing_Q2m8xVb1Lr0aTz",
  "protocol": "RTMP",
  "identity": "alice",
  "keyPrefix": "sk_AbC",
  "autoStart": true,
  "status": "IDLE",
  "reconnectWindowSec": 60,
  "lastError": null,
  "inputVideo": null,
  "inputAudio": null,
  "whipUrl": "https://ingest.livqeno.com/whip/v1",
  "rtmpUrl": "rtmp://rtmp.livqeno.com:1935/live",
  "rtmpsUrl": "rtmps://rtmp.livqeno.com:1936/live",
  "streamKey": "sk_AbC…",
  "connectedAt": null,
  "disconnectedAt": null,
  "createdAt": "2026-10-02T12:00:00.000Z",
  "updatedAt": "2026-10-02T12:00:00.000Z"
}

Options you can pass to create:

FieldDefaultMeans
protocolRTMPRTMP or WHIP. An encoder using the other one is refused.
identityThe stream's HOSTWhich host the encoder publishes as. See OBS takes over.
autoStarttrueStart a CREATED stream automatically when the encoder connects.
reconnectWindowSec60How long a dropped encoder can take to come back. See Reconnects.

The routes are POST /v1/live-streams/{streamId}/ingress (create), GET /v1/live-streams/{streamId}/ingress (status, never the key), POST /v1/live-streams/{streamId}/ingress/reset (new key) and DELETE /v1/live-streams/{streamId}/ingress.

2a. OBS over WHIP (OBS 30+)

Settings → Stream:

SettingValue
ServiceWHIP
ServerwhipUrl from the response, https://ingest.livqeno.com/whip/v1
Bearer TokenstreamKey from the response (sk_…)

Settings → Output (set Output Mode to Advanced, then the Streaming tab):

SettingValue
Video Encoderx264, or NVIDIA NVENC H.264
Rate ControlCBR
Bitrate2500–6000 Kbps
Keyframe Interval2 s
B-frames0 (x264 options: bframes=0; NVENC: Max B-frames 0)

OBS sends Opus audio over WHIP, which Livqeno passes through unchanged.

2b. OBS over RTMP or RTMPS

Settings → Stream:

SettingValue
ServiceCustom...
ServerrtmpsUrl, rtmps://rtmp.livqeno.com:1936/live (or rtmpUrl on port 1935 if your network blocks RTMPS)
Stream KeystreamKey (sk_…)

Prefer RTMPS: it encrypts the stream key on the way in. Plain RTMP is there for networks that block it.

Settings → Output (Advanced mode, Streaming tab):

Settingx264NVIDIA NVENC H.264
Rate ControlCBRCBR
Bitrate2500–6000 Kbps2500–6000 Kbps
Keyframe Interval2 s2 s
CPU Usage Preset / PresetveryfastP5 (the OBS default)
Profilehigh or mainhigh or main
B-framesx264 Options: bframes=0Max B-frames: 0

Settings → Output → Audio: AAC, 128 Kbps. Settings → Audio: Sample Rate 48 kHz. Settings → Video: Output resolution 1920×1080 or lower, 30 FPS or lower.

Livqeno converts AAC to Opus, because browsers can't play AAC over WebRTC.

ffmpeg

Handy for testing, or for server-side sources. Replace sk_… with your key.

RTMPS (the stream key goes after /live/):

Terminal
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -tune zerolatency -pix_fmt yuv420p \
  -bf 0 -g 60 -keyint_min 60 -sc_threshold 0 \
  -b:v 3000k -maxrate 3000k -bufsize 6000k \
  -c:a aac -b:a 128k -ar 48000 -ac 2 \
  -f flv "rtmps://rtmp.livqeno.com:1936/live/sk_…"

WHIP (ffmpeg 8.0 or newer, which added the WHIP muxer; create the ingest with protocol: 'WHIP'):

Terminal
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -tune zerolatency -pix_fmt yuv420p \
  -bf 0 -g 60 -keyint_min 60 -sc_threshold 0 -b:v 3000k \
  -c:a libopus -b:a 128k -ar 48000 -ac 2 \
  -f whip -authorization "sk_…" "https://ingest.livqeno.com/whip/v1"

-g 60 at 30 fps is a 2-second keyframe interval, and -bf 0 turns B-frames off.

What Livqeno accepts

Limit
Video codecH.264 only, no B-frames
Resolution and frame rateUp to 1920×1080 at 30 fps
BitrateUp to 8 Mbps
Keyframe interval1–2 s recommended. Above 4 s is accepted with a warning
AudioAAC (RTMP, converted to Opus) or Opus (WHIP)
Encoders per streamOne

Livqeno passes your video through as-is, without re-encoding it. That keeps latency and cost low, but it means viewers get exactly the one quality you send, and Livqeno can't fix B-frames or ask your encoder for a keyframe. A long keyframe interval means late joiners wait longer for a picture.

Status and webhooks

GET /v1/live-streams/{streamId}/ingress returns status, lastError, and what the encoder is actually sending (inputVideo, inputAudio):

statusMeans
IDLECreated, no encoder has connected
CONNECTINGThe encoder connected and Livqeno is joining the room
PUBLISHINGViewers are receiving the encoder's media
RECONNECTINGThe encoder dropped, and the reconnect window is open
DISCONNECTEDThe encoder left, or didn't come back in time
FAILEDLivqeno refused the media. lastError says why
EventSent whendata
live_stream.ingress_connectedThe encoder started publishingstreamId, ingressId, protocol, at
live_stream.ingress_disconnectedThe encoder left, and didn't reconnect within the windowstreamId, ingressId, protocol, reason, at
live_stream.ingress_failedLivqeno refused the encoder's media (for example B-frames)streamId, ingressId, protocol, reason, at

Reconnects

Encoders drop: Wi-Fi blips, OBS restarts, a laptop sleeps. When that happens, Livqeno keeps the host in the room for reconnectWindowSec (60 seconds by default) and marks the ingest RECONNECTING:

  • Back within the window, with the same key: the stream resumes on the same participant. Viewers see the video pause and continue. Nobody sees a "host left" and "host joined".
  • Not back in time: the host leaves the room, the status becomes DISCONNECTED, and live_stream.ingress_disconnected fires.

Either way, Livqeno doesn't end the stream. Ending it is your call, with end(), the same as for a browser host.

Every reconnect checks the key again, so after a reset() or delete() the old key can't resume, even inside the window.

OBS takes over

By default the encoder publishes as the stream's HOST identity. If that host is also in the room from a browser, the encoder's session replaces the browser's, the same as when one person joins twice. That's usually what you want: the host opens your app to check things, then goes live from OBS.

To keep both (say, a browser host with the OBS feed as a second camera), register a second identity with addHost() first, then pass it as identity when you create the ingest. Either way, the encoder counts as host time for usage, never as a viewer.

With autoStart: true (the default), connecting the encoder to a CREATED stream starts it, exactly like calling start(). With autoStart: false, call start() yourself first. An encoder can't connect to an ENDED stream.

Troubleshooting

When Livqeno refuses an encoder, OBS shows a generic "Failed to connect" message. Where the real reason ends up depends on when the encoder was refused:

  • Refused before any media (the "Rejected" rows): the key or the stream state was wrong, so no session started and lastError is unchanged. Over WHIP, the HTTP response body carries the reason as { "error": "<code>", "message": "…" }. Over RTMP, the publish is refused and the encoder only sees a failed connection, so work through the "Rejected" rows.
  • Refused because of the media itself (the FAILED rows): the ingest's status becomes FAILED with the reason in lastError, and live_stream.ingress_failed fires. Check GET …/ingress.
What you seeCauseFix
Rejected right away, key unknownThe key is wrong, was reset, or the ingest was deletedCopy the current key. If you've lost it, reset() and use the new one
Rejected: protocol mismatchAn RTMP encoder on a WHIP ingest, or the reverseUse the URL that matches the ingest's protocol, or delete and re-create the ingest with the other one
Rejected: already publishingAnother encoder is live on this keyStop the other encoder. One encoder per stream
Rejected: stream endedThe stream is ENDED, which is finalCreate a new stream and ingest
Rejected: stream not liveautoStart is false and the stream is still CREATEDCall start() first, or create the ingest with autoStart: true
Rejected: allowance exhaustedThe project's live-streaming allowance is used upSee Usage
Rejected: concurrency limitAuto-start would make a second live stream for this hostEnd the other stream first
FAILED: B-frames are not supportedThe encoder is sending B-framesx264: add bframes=0 to x264 Options. NVENC: set Max B-frames to 0. ffmpeg: -bf 0
FAILED: no audio track / no video receivedOne of the two is missing. Both are requiredAdd an audio source (even a silent one) and a video source in OBS
FAILED: unsupported codecHEVC, AV1 or another non-H.264 codecSwitch the encoder to H.264
FAILED: resolution, frame rate or bitrate limitAbove 1080p, 30 fps or 8 MbpsLower Output resolution, FPS or Bitrate
Connected, but a warning about the keyframe intervalKeyframe interval above 4 sSet Keyframe Interval to 2 s (-g 60 at 30 fps)
Video stutters or plays out of orderB-frames, on an encoder that doesn't report themSame fix as the B-frames row
RTMPS doesn't connect but RTMP doesPort 1936 is blocked on your networkUse rtmpUrl on port 1935, or another network
Disconnected right after a key resetWorking as intended: reset kicks the old keyPaste the new key into OBS

Next steps