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:
| WHIP | RTMP / RTMPS | |
|---|---|---|
| OBS version | 30 or newer | Any |
| Latency | Lowest (WebRTC end to end) | Low |
| Audio | Opus, passed straight through | AAC, converted to Opus by Livqeno |
| Best for | OBS on a modern machine | Older 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);from livqeno import CreateIngressParams
ingress = livqeno.live_streams.ingress.create(stream["id"], CreateIngressParams(protocol="RTMP"))
print(ingress["rtmpsUrl"], ingress["streamKey"])curl -X POST "$LIVQENO_API_URL/v1/live-streams/$STREAM_ID/ingress" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"protocol": "RTMP"}'{
"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:
| Field | Default | Means |
|---|---|---|
protocol | RTMP | RTMP or WHIP. An encoder using the other one is refused. |
identity | The stream's HOST | Which host the encoder publishes as. See OBS takes over. |
autoStart | true | Start a CREATED stream automatically when the encoder connects. |
reconnectWindowSec | 60 | How 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:
| Setting | Value |
|---|---|
| Service | WHIP |
| Server | whipUrl from the response, https://ingest.livqeno.com/whip/v1 |
| Bearer Token | streamKey from the response (sk_…) |
Settings → Output (set Output Mode to Advanced, then the Streaming tab):
| Setting | Value |
|---|---|
| Video Encoder | x264, or NVIDIA NVENC H.264 |
| Rate Control | CBR |
| Bitrate | 2500–6000 Kbps |
| Keyframe Interval | 2 s |
| B-frames | 0 (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:
| Setting | Value |
|---|---|
| Service | Custom... |
| Server | rtmpsUrl, rtmps://rtmp.livqeno.com:1936/live (or rtmpUrl on port 1935 if your network blocks RTMPS) |
| Stream Key | streamKey (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):
| Setting | x264 | NVIDIA NVENC H.264 |
|---|---|---|
| Rate Control | CBR | CBR |
| Bitrate | 2500–6000 Kbps | 2500–6000 Kbps |
| Keyframe Interval | 2 s | 2 s |
| CPU Usage Preset / Preset | veryfast | P5 (the OBS default) |
| Profile | high or main | high or main |
| B-frames | x264 Options: bframes=0 | Max 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/):
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'):
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 codec | H.264 only, no B-frames |
| Resolution and frame rate | Up to 1920×1080 at 30 fps |
| Bitrate | Up to 8 Mbps |
| Keyframe interval | 1–2 s recommended. Above 4 s is accepted with a warning |
| Audio | AAC (RTMP, converted to Opus) or Opus (WHIP) |
| Encoders per stream | One |
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):
status | Means |
|---|---|
IDLE | Created, no encoder has connected |
CONNECTING | The encoder connected and Livqeno is joining the room |
PUBLISHING | Viewers are receiving the encoder's media |
RECONNECTING | The encoder dropped, and the reconnect window is open |
DISCONNECTED | The encoder left, or didn't come back in time |
FAILED | Livqeno refused the media. lastError says why |
| Event | Sent when | data |
|---|---|---|
live_stream.ingress_connected | The encoder started publishing | streamId, ingressId, protocol, at |
live_stream.ingress_disconnected | The encoder left, and didn't reconnect within the window | streamId, ingressId, protocol, reason, at |
live_stream.ingress_failed | Livqeno 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, andlive_stream.ingress_disconnectedfires.
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
lastErroris 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
FAILEDrows): the ingest'sstatusbecomesFAILEDwith the reason inlastError, andlive_stream.ingress_failedfires. CheckGET …/ingress.
| What you see | Cause | Fix |
|---|---|---|
| Rejected right away, key unknown | The key is wrong, was reset, or the ingest was deleted | Copy the current key. If you've lost it, reset() and use the new one |
| Rejected: protocol mismatch | An RTMP encoder on a WHIP ingest, or the reverse | Use the URL that matches the ingest's protocol, or delete and re-create the ingest with the other one |
| Rejected: already publishing | Another encoder is live on this key | Stop the other encoder. One encoder per stream |
| Rejected: stream ended | The stream is ENDED, which is final | Create a new stream and ingest |
| Rejected: stream not live | autoStart is false and the stream is still CREATED | Call start() first, or create the ingest with autoStart: true |
| Rejected: allowance exhausted | The project's live-streaming allowance is used up | See Usage |
| Rejected: concurrency limit | Auto-start would make a second live stream for this host | End the other stream first |
FAILED: B-frames are not supported | The encoder is sending B-frames | x264: add bframes=0 to x264 Options. NVENC: set Max B-frames to 0. ffmpeg: -bf 0 |
FAILED: no audio track / no video received | One of the two is missing. Both are required | Add an audio source (even a silent one) and a video source in OBS |
FAILED: unsupported codec | HEVC, AV1 or another non-H.264 codec | Switch the encoder to H.264 |
FAILED: resolution, frame rate or bitrate limit | Above 1080p, 30 fps or 8 Mbps | Lower Output resolution, FPS or Bitrate |
| Connected, but a warning about the keyframe interval | Keyframe interval above 4 s | Set Keyframe Interval to 2 s (-g 60 at 30 fps) |
| Video stutters or plays out of order | B-frames, on an encoder that doesn't report them | Same fix as the B-frames row |
| RTMPS doesn't connect but RTMP does | Port 1936 is blocked on your network | Use rtmpUrl on port 1935, or another network |
| Disconnected right after a key reset | Working as intended: reset kicks the old key | Paste the new key into OBS |
Next steps
- Restream: send the stream on to YouTube or Twitch as well.
- Hosts & co-hosts: register the identity an encoder publishes as.
- Known limitations: what ingest doesn't do yet.