Ingress#
Ingress brings media from outside into a Room. A streamer pushing RTMP from OBS, a WHIP endpoint publishing over WebRTC, a video file pulled from a URL, an IP camera: Ingress takes what arrives, transcodes it (or relays it untouched) and publishes it into the Room as a regular participant, so everyone subscribes to it like any other track.
Your application server creates the Ingress up front. The API returns a URL and, for push inputs, a stream key. Whoever is broadcasting points their encoder at those.
Input types#
| Input | What it is |
|---|---|
RTMP_INPUT |
OpenVidu exposes an RTMP endpoint. The broadcaster pushes to the returned rtmp:// URL using the returned stream_key. Always transcoded |
WHIP_INPUT |
OpenVidu exposes a WHIP endpoint, so the broadcaster publishes over WebRTC. The only input that can skip transcoding |
URL_INPUT |
OpenVidu pulls media from a URL you supply, rather than waiting to be pushed to. Supports HLS streams and media files (MP4, MOV, MKV/WebM, OGG, MP3, M4A) |
The full enum is IngressInput .
IP cameras
RTSP cameras are ingested through URL_INPUT, passing the camera's rtsp:// URL. There is a worked example in eight languages under IP Cameras.
Push and pull workflows#
The two families have different lifecycles, and this is the first thing to get right.
A push Ingress (RTMP_INPUT, WHIP_INPUT) waits for the broadcaster:
- You create the Ingress. It returns a URL and a stream key.
- Your user configures those in their streaming software and starts streaming.
- Ingress transcodes the incoming media, or forwards it unchanged when transcoding is disabled.
- Ingress joins the Room and publishes the media.
- When the broadcaster disconnects, the Ingress participant leaves the Room. The Ingress itself stays valid in a disconnected state, so the same URL and stream key can be reused for the next session.
A pull Ingress (URL_INPUT) starts on its own:
- You create the Ingress, and Ingress immediately starts fetching and transcoding the media.
- Ingress joins the Room and publishes it.
- When the media has been fully consumed, or you call
DeleteIngress, the participant leaves the Room.
Creating an Ingress#
Ingress are created by making a request to the Ingress API from your application server. Requests to the Ingress API require a token with the ingressAdmin grant (any LiveKit server SDK automatically generates it from your LIVEKIT_API_KEY and LIVEKIT_API_SECRET).
The example below creates an RTMP Ingress that publishes into "my-room":
Using LiveKit Node SDK
import { IngressClient, IngressInput } from 'livekit-server-sdk';
const ingressClient = new IngressClient('https://my-openvidu-host', 'api-key', 'api-secret');
const ingress = await ingressClient.createIngress(IngressInput.RTMP_INPUT, {
name: 'my-ingress',
roomName: 'my-room',
participantIdentity: 'my-participant',
participantName: 'My Participant'
});
// ingress.url and ingress.streamKey are the endpoint the broadcaster pushes to
Using LiveKit Go SDK
import (
"context"
livekit "github.com/livekit/protocol/livekit"
lksdk "github.com/livekit/server-sdk-go/v2"
)
ingressClient := lksdk.NewIngressClient(
"https://my-openvidu-host",
"api-key",
"api-secret",
)
ingressRequest := &livekit.CreateIngressRequest{
InputType: livekit.IngressInput_RTMP_INPUT,
Name: "my-ingress",
RoomName: "my-room",
ParticipantIdentity: "my-participant",
ParticipantName: "My Participant",
}
ingressInfo, err := ingressClient.CreateIngress(context.Background(), ingressRequest)
// ingressInfo.Url and ingressInfo.StreamKey are the endpoint the broadcaster pushes to
Using LiveKit Ruby SDK
require 'livekit'
ingressClient = LiveKit::IngressServiceClient.new("https://my-openvidu-host", api_key: "api-key", api_secret: "api-secret")
response = ingressClient.create_ingress(
:RTMP_INPUT,
name: "my-ingress",
room_name: "my-room",
participant_identity: "my-participant",
participant_name: "My Participant"
)
ingressInfo = response.data
# ingressInfo.url and ingressInfo.stream_key are the endpoint the broadcaster pushes to
Using LiveKit Kotlin SDK
import io.livekit.server.IngressServiceClient;
import livekit.LivekitIngress.IngressInfo;
import livekit.LivekitIngress.IngressInput;
IngressServiceClient ingressService = IngressServiceClient.createClient("https://my-openvidu-host", "api-key", "api-secret");
IngressInfo ingressInfo = ingressService.createIngress(
"my-ingress", // Ingress name
"my-room", // Room name
"my-participant", // Ingress participant identity
"My Participant", // Ingress participant name
IngressInput.RTMP_INPUT // Ingress input type
).execute().body();
// ingressInfo.getUrl() and ingressInfo.getStreamKey() are the endpoint the broadcaster pushes to
Using LiveKit Python SDK
from livekit.api import LiveKitAPI, CreateIngressRequest, IngressInput
lkapi = LiveKitAPI(
url="https://my-openvidu-host", api_key="api-key", api_secret="api-secret"
)
request = CreateIngressRequest(
input_type=IngressInput.RTMP_INPUT,
name="my-ingress",
room_name="my-room",
participant_identity="my-participant",
participant_name="My Participant",
)
ingress_info = await lkapi.ingress.create_ingress(request)
# ingress_info.url and ingress_info.stream_key are the endpoint the broadcaster pushes to
Using LiveKit Rust SDK
use livekit_api::services::ingress::{CreateIngressOptions, IngressClient};
use livekit_protocol::IngressInput;
let ingress_client = IngressClient::with_api_key(
"https://my-openvidu-host",
"api-key",
"api-secret",
);
let ingress_info = ingress_client.create_ingress(
IngressInput::RtmpInput,
CreateIngressOptions {
name: "my-ingress".to_string(),
room_name: "my-room".to_string(),
participant_identity: "my-participant".to_string(),
participant_name: "My Participant".to_string(),
..Default::default()
}).await?;
// ingress_info.url and ingress_info.stream_key are the endpoint the broadcaster pushes to
Using LiveKit PHP SDK
<?php
use Agence104\LiveKit\IngressServiceClient;
use Livekit\IngressInput;
$ingressClient = new IngressServiceClient("https://my-openvidu-host", "api-key", "api-secret");
$ingressInfo = $ingressClient->createIngress(
IngressInput::RTMP_INPUT,
"my-ingress", // Ingress name
"my-room", // Room name
"my-participant", // Ingress participant identity
"My Participant" // Ingress participant name
);
// $ingressInfo->getUrl() and $ingressInfo->getStreamKey() are the endpoint the broadcaster pushes to
Using LiveKit .NET SDK
using Livekit.Server.Sdk.Dotnet;
IngressServiceClient ingressClient = new IngressServiceClient(
"https://my-openvidu-host",
"api-key",
"api-secret"
);
var ingressInfo = await ingressClient.CreateIngress(new CreateIngressRequest
{
InputType = IngressInput.RtmpInput,
Name = "my-ingress",
RoomName = "my-room",
ParticipantIdentity = "my-participant",
ParticipantName = "My Participant",
});
// ingressInfo.Url and ingressInfo.StreamKey are the endpoint the broadcaster pushes to
If your backend technology does not have its own SDK, you have two options:
-
Call the Server API directly.
CreateIngressis a POST to/twirp/livekit.Ingress/CreateIngress, authenticated with a token carrying theingressAdmingrant:curl -X POST 'https://my-openvidu-host/twirp/livekit.Ingress/CreateIngress' \ -H 'Authorization: Bearer <TOKEN>' \ -H 'Content-Type: application/json' \ -d '{ "input_type": "RTMP_INPUT", "name": "my-ingress", "room_name": "my-room", "participant_identity": "my-participant", "participant_name": "My Participant" }' -
Use the livekit-cli :
Create a file at
ingress.jsonwith the following content:{ "input_type": "RTMP_INPUT", "name": "my-ingress", "room_name": "my-room", "participant_identity": "my-participant", "participant_name": "My Participant" }Then run the following commands:
The response is an IngressInfo . For push inputs it carries the url and stream_key the broadcaster points their encoder at.
These are the fields of the create request:
| Field | Notes |
|---|---|
input_type |
One of the three input types above |
url |
Where to pull media from. URL_INPUT only |
name |
Your own label for this Ingress |
room_name |
The Room to publish into |
participant_identity |
Identity the Ingress publishes as. Same uniqueness rules as any participant identity |
participant_name |
Display name of the publishing participant |
participant_metadata |
Metadata attached to the publishing participant |
enable_transcoding |
Whether to re-encode the incoming media. See Transcoding |
audio |
IngressAudioOptions : track name, source, and a preset or explicit options |
video |
IngressVideoOptions : the same, for video |
enabled |
Defaults to true. Set it to false to reject new connection attempts without deleting the Ingress |
Transcoding#
Ingress can re-encode incoming media before publishing it, so that every subscriber can consume it. When it transcodes, it publishes simulcast layers by default, which is what lets viewers on poor connections get a lower layer.
Whether it transcodes depends on the input:
- RTMP and URL inputs are always transcoded. They arrive as a single non-simulcast stream that has to be re-encoded to be useful in a Room.
- WHIP forwards media unmodified by default. WHIP is already WebRTC, so relaying it straight through gives the lowest possible latency. The cost is that the broadcaster's own encoder settings are what every subscriber gets, so it should publish simulcast itself. Set
enable_transcodingtotruewhen it cannot.
Presets#
The simplest way to configure transcoding is a preset, which fixes codec, dimensions, framerate, bitrate and, for video, the whole set of simulcast layers.
Video presets (IngressVideoEncodingPreset ), all H.264:
| Preset | Resolution | FPS | Main-layer bitrate | Layers |
|---|---|---|---|---|
H264_720P_30FPS_3_LAYERS |
1280×720 | 30 | 1900 kbps | 3 |
H264_1080P_30FPS_3_LAYERS |
1920×1080 | 30 | 3500 kbps | 3 |
H264_540P_25FPS_2_LAYERS |
960×540 | 25 | 1000 kbps | 2 |
H264_720P_30FPS_1_LAYER |
1280×720 | 30 | 1900 kbps | 1 |
H264_1080P_30FPS_1_LAYER |
1920×1080 | 30 | 3500 kbps | 1 |
H264_720P_30FPS_3_LAYERS_HIGH_MOTION |
1280×720 | 30 | 2500 kbps | 3 |
H264_1080P_30FPS_3_LAYERS_HIGH_MOTION |
1920×1080 | 30 | 4500 kbps | 3 |
H264_540P_25FPS_2_LAYERS_HIGH_MOTION |
960×540 | 25 | 1300 kbps | 2 |
H264_720P_30FPS_1_LAYER_HIGH_MOTION |
1280×720 | 30 | 2500 kbps | 1 |
H264_1080P_30FPS_1_LAYER_HIGH_MOTION |
1920×1080 | 30 | 4500 kbps | 1 |
The HIGH_MOTION variants spend more bitrate at the same resolution. Reach for them when the source is hard to encode, such as sport or gameplay. For a static presentation or a talking head the standard presets are enough.
Audio presets (IngressAudioEncodingPreset ):
| Preset | Codec |
|---|---|
OPUS_STEREO_96KBPS |
OPUS, 2 channels, 96 kbps |
OPUS_MONO_64KBS |
OPUS, 1 channel, 64 kbps |
Custom encoding options#
If no preset fits, set explicit options instead of a preset. Note that with custom video options you define the simulcast layers yourself: when layers is empty, Ingress publishes a single layer plus the usual half and quarter dimensions.
| Options | Fields |
|---|---|
IngressVideoEncodingOptions |
video_codec, frame_rate and layers, an array of VideoLayer (quality, width, height, bitrate) |
IngressAudioEncodingOptions |
audio_codec, bitrate, channels and disable_dtx |
Configuring the encoder#
Whatever software the broadcaster uses, it needs the same two values from the IngressInfo : the url and the stream_key.
- OBS Studio: in Settings > Stream, pick the
Custom...service, put theurlin Server and thestream_keyin Stream Key. -
FFmpeg: append the stream key to the URL and publish with the
flvmuxer. -
GStreamer: supports both RTMP (
rtmp2sink) and WHIP (whipsink), the latter requiring thenicesink,webrtcbinandwhipsinkplugins.
Full command lines for each: Encoder configuration .
Ingress lifecycle#
Managing an Ingress#
| Operation | What it does |
|---|---|
CreateIngress |
Creates one and returns its URL and stream key |
ListIngress |
Lists Ingress, filtered by Room or by ingress_id |
UpdateIngress |
Changes an existing Ingress: Room, participant details, options, enabled. Only a reusable Ingress (RTMP, WHIP) can be updated |
DeleteIngress |
Removes one, disconnecting it from the Room |
IngressInfo#
Every operation above returns an IngressInfo , and so does every Ingress webhook. It describes the Ingress and how to connect to it:
{
"ingressId": "IN_AbCdEfGhIjKl",
"name": "my-ingress",
"streamKey": "GHtwLbmMChLR",
"url": "rtmp://my-openvidu-host:1935/x",
"inputType": "RTMP_INPUT",
"enableTranscoding": true,
"roomName": "my-room",
"participantIdentity": "my-participant",
"participantName": "My Participant",
"reusable": true,
"enabled": true,
"state": {
"status": "ENDPOINT_PUBLISHING",
"roomId": "RM_GmENxWJemFqL",
"startedAt": "1755640000000000000"
}
}
The fields that matter most:
ingressId: the identifier every operation above takes.urlandstreamKey: what the broadcaster configures. Treat the stream key as a credential.reusable: whether the endpoint accepts a new session after one ends. True for push inputs, which is what lets a broadcaster reconnect to the same URL.state: describes current endpoint status, errors, and input media state. It is an object of typeIngressStateand carries the most important information about the media actually arriving for the Ingress endpoint.
Webhooks#
Two webhook events track an Ingress, both carrying the full ingressInfo:
| Event | Fires when |
|---|---|
ingress_started |
The Ingress began publishing into the Room |
ingress_ended |
It stopped |
An Ingress publishing into a Room also produces ordinary participant and track events: a participant_joined for the Ingress participant, and a track_published per track.
Related#
- IP Cameras: RTSP ingest in eight languages.
- Stream ingestion: choosing between the input types.
- Access tokens reference: the
ingressAdmingrant gates these operations. - Webhooks reference: the Ingress events and their payloads.
- Egress reference: media in the other direction.