Documentation

Everything you need to embed librtmp2 in a media pipeline, run the reference server, or deploy the full stack with Docker and the web panel.

Active development. All three projects (librtmp2, librtmp2-server, librtmp2-server-panel) remain pre-1.0. APIs, Docker images, and configuration may evolve between releases. Pin tested versions and validate your OBS/FFmpeg workflow before relying on it for critical streams. Use crates.io and GitHub Releases as the source of truth for current versions.

Getting Started

librtmp2 is a protocol library — it decodes RTMP and Enhanced RTMP (E-RTMP v1/v2) on the wire and calls back into your application. It deliberately contains no HTTP server, no authentication policy, and no stream storage. If you want a ready-to-run endpoint instead of embedding the crate, use librtmp2-server.

Add librtmp2 from crates.io to your Cargo.toml. While librtmp2 remains on 0.x, use a loose 0.x range when you want regular updates and refresh the lockfile for security fixes (RTMPS/TLS via the default tls feature):

[dependencies.librtmp2]
version = "0"

cargo update -p librtmp2   # pull newest 0.x from crates.io

For reproducible builds, replace the loose range with the exact release you tested. Use the version shown on crates.io rather than copying a “current” version number from this page.

There is no version = "latest" in Cargo — Cargo.lock always pins the resolved version until you run cargo update.

Or clone and build locally for development (requires Rust 1.93+):

git clone https://github.com/OpenRTMP/librtmp2.git
cd librtmp2
cargo build --release
cargo test

Minimal server example — listen, accept publish, log frames (examples/minimal_server.rs):

use librtmp2::server::Server;
use librtmp2::types::*;

fn on_frame(frame: &Frame) {
    println!("frame: size={}", frame.size);
}

let config = ServerConfig {
    max_connections: 16,
    chunk_size: 4096,
    tls_enabled: 0,
    tls_cert_file: std::ptr::null(),
    tls_key_file: std::ptr::null(),
    tls_ca_file: std::ptr::null(),
    tls_insecure: 0,
};

let mut server = Server::new(config)?;
server.on_frame_cb = Some(on_frame);
server.listen("0.0.0.0:1935")?;

while running {
    server.poll(100)?;
}

For sanitizer builds during development:

RUSTFLAGS="-Z sanitizer=address" cargo test
RUSTFLAGS="-Z sanitizer=undefined" cargo test

Connection State Machine

Every connection moves through a fixed set of states as the handshake, capability negotiation, and stream lifecycle progress:

TCP_ACCEPTED → HANDSHAKE → CONNECTED → [CAPS_NEGOTIATED] → APP_CONNECTED → STREAM_CREATED → PUBLISHING | PLAYING → CLOSING → CLOSED

CAPS_NEGOTIATED is the E-RTMP v2 capability exchange step between CONNECTED and APP_CONNECTED. Classic RTMP and E-RTMP v1 peers skip it entirely.

Host Callbacks

The library never touches storage, auth, or transcoding — it decodes the wire and calls back into your application. In Rust, assign callbacks on the Server or Client object (e.g. server.on_publish_cb = Some(...)):

CallbackFired when
on_connectpeer completes the RTMP connect command
on_publisha publisher requests a stream key — return false to reject
on_playa viewer requests playback of a stream — return false to reject
on_framea bounds-checked audio/video/script Frame is ready
on_closethe connection is tearing down, for any reason

Layer Reference

Ingest flows bottom-up through nine layers before reaching your callbacks. See the architecture overview on the homepage for the full diagram. Key directories in src/:

DirectoryResponsibility
core/alloc hook, growable buffers, byte helpers, logging, errors
handshake/C0/C1/C2 ↔ S0/S1/S2, partial-read buffering, version detection
chunk/chunk_reader/writer, per-csid chunk_state, SetChunkSize/Abort
message/reassembled message dispatch & AMF command decode/encode
amf/AMF0 (mandatory) and AMF3 (optional)
flv/FLV audio/video/script tag parsing
ertmp/E-RTMP v1 (ExVideo/ExAudio, FourCC, HDR) + v2 (capsEx, reconnect, multitrack, ModEx)
session/connection object, state machine, stream bookkeeping
server/ & client/accept loop / per-connection poll · outbound connect & publish/play

librtmp2-server

librtmp2-server is the reference RTMP/E-RTMP media server built on top of librtmp2. It is a separate repository and binary — the library itself stays free of any server loop, socket policy, or storage decisions.

Unlike a bare librtmp2 integration, the server adds application-layer features:

FeatureWhat it does
RTMP listeneraccepts publishers and players on RTMP_BIND (default 0.0.0.0:1935) via the integrated librtmp2 server
RTMPS listeneroptional second TLS listener on RTMPS_BIND (1936) alongside plaintext RTMP when TLS_ENABLED=true
SQLite persistencestreams, publishers, players, and stats stored in LRTMP2_DB
Per-stream keysauto-generated publish_key, play_key, and stats_key — no public stream list without the exact key
REST APIstream CRUD on /api/v1/streams with Bearer token auth (axum, port 8080)
Stats endpoints/stats?key=<stats_key> (JSON) and /stats-nginx?key=<stats_key> (nginx-rtmp-compatible XML)
Frame relayforwards publisher frames to all matching players, GOP-aware
HA clusteringoptional multi-node mode (CLUSTER_ENABLED, off by default) with OpenRaft state replication and a media mesh — see HA clustering

Build and run natively (standalone):

git clone https://github.com/OpenRTMP/librtmp2-server.git
cd librtmp2-server
cargo build --release
cp .env.example .env
LRTMP2_DB=./server.db ./target/release/librtmp2-server

On first startup the server generates an API bearer token, stores it in SQLite, and prints it once to stderr. Use that token for Authorization: Bearer <token> on REST API calls.

Publish with OBS: Server rtmp://<host>/live, Stream Key = the publish_key returned by POST /api/v1/streams.

curl -X POST http://localhost:8080/api/v1/streams \
  -H "Authorization: Bearer <api-token>" \
  -H "Content-Type: application/json" \
  -d '{"id":"mystream","name":"My Live Stream","app":"live"}'

HA clustering

From server 0.2.0, librtmp2-server can run as a multi-node cluster. Clustering is off by default; standalone behavior is unchanged when CLUSTER_ENABLED=false. Published Docker images build with the cluster Cargo feature; runtime still defaults to standalone.

Architecture in short:

PlaneDefault portRole
Control1940OpenRaft RPC, join/admin, heartbeats, StatsProxy
Media1941Inter-node frame relay, subscribe, init-cache
RTMP / HTTP1935 / 8080Client publish/play and admin API (unchanged)
  • One SQLite DB per node; durable stream/viewer/token/ownership mutations go through Raft.
  • No central media proxy and no mandatory Postgres/Redis for the cluster plane.
  • Publisher ownership uses epoch fencing; players on non-owner nodes receive media via the mesh.
  • Shared-secret peer auth; optional mTLS for control and media.

Minimal bootstrap (first voter):

CLUSTER_ENABLED=true
CLUSTER_NODE_ID=1
CLUSTER_BOOTSTRAP=true
CLUSTER_SECRET=<long-random-secret>
CLUSTER_ADVERTISE_ADDR=10.0.0.1:1940
CLUSTER_MEDIA_ADVERTISE_ADDR=10.0.0.1:1941

Additional nodes join with an empty database and CLUSTER_JOIN=<existing-control-addr>, then promote learners to voters via POST /api/v1/cluster/nodes/{id}/promote.

Authenticated cluster APIs include GET /api/v1/cluster, /nodes, /streams, plus drain/resume/promote/remove. Full configuration, limitations, and operator notes live in the server repo's docs/clustering.md and the site guide Run an HA RTMP cluster.

Native builds that need clustering must compile with the feature:

cargo build --release --features cluster

librtmp2-server-panel

librtmp2-server-panel is a Flask web UI that talks to the server's REST API. It does not implement RTMP itself — it manages streams, copies URLs, and polls live stats.

FeatureDescription
Stream managementcreate and delete streams via /api/v1/streams
One-click copypublish URL, stream key, play URL, and stats URL
Live statsbitrate, resolution, codec, uptime, RTT polled from /stats?key=...
Cluster UIwhen health reports cluster.enabled=true: quorum overview, node drain/resume/remove, stream owner/epoch placement
Login gateoptional admin login (REQUIRE_LOGIN=True by default)
SecurityCSRF protection, rate limiting (Redis-backed in Docker), encrypted key display

The panel does not participate in Raft. Point it at any healthy synchronized node; durable admin writes are forwarded inside the cluster. Standalone servers hide the Cluster navigation automatically.

Key environment variables (see .env.example in the panel repo):

VariableDescription
LRTMP2_API_URLserver HTTP API base URL (internal; e.g. http://openrtmp-server:8080 in Docker)
LRTMP2_API_TOKENBearer token — must match the server's API token
LRTMP2_DOMAINpublic host/IP for RTMP URLs shown to users
LRTMP2_STATS_URLbrowser-reachable stats URL (defaults to LRTMP2_API_URL)
PASSWORD / SECRET_KEYpanel login password and Flask session secret

Open the panel at http://localhost:8000 after starting (default credentials in .env.example: user admin).

Docker Deployment

Both server and panel publish prebuilt images to GitHub Container Registry. See also the download page for copy-paste commands.

Server only

Quickest path — auto-generated API token on first start:

docker network create openrtmp   # shared with the panel examples below

# For RTMPS (TLS_ENABLED=true), also publish -p 1936:1936.
docker run -d \
  --name librtmp2-server \
  --network openrtmp \
  -p 1935:1935 \
  -p 8080:8080 \
  -v librtmp2-server-data:/data \
  ghcr.io/openrtmp/librtmp2-server:latest

docker logs librtmp2-server   # copy API token from first-start output

Available tags: latest, beta, alpha, and pinned versions (for example a concrete release tag from GitHub Releases).

Panel only (docker run)

Image: ghcr.io/openrtmp/librtmp2-server-panel. Connect to an existing server on the same Docker network (container name librtmp2-server):

docker network create openrtmp   # skip if you created it above

docker run -d \
  --name librtmp2-server-panel \
  --network openrtmp \
  -p 8000:8000 \
  -e LRTMP2_API_URL=http://librtmp2-server:8080 \
  -e LRTMP2_STATS_URL=http://localhost:8080 \
  -e LRTMP2_API_TOKEN=<token-from-server-logs> \
  -e LRTMP2_DOMAIN=localhost \
  -e PASSWORD=<panel-password> \
  -e SECRET_KEY=<random-secret> \
  ghcr.io/openrtmp/librtmp2-server-panel:latest

Server on the host instead of Docker? Use LRTMP2_API_URL=http://host.docker.internal:8080 (Windows/macOS).

Full stack (docker run)

Server + panel + Redis without Compose. Set a shared API token before the first server start:

export LRTMP2_API_TOKEN=$(openssl rand -hex 32)
export PANEL_PASSWORD="$(openssl rand -base64 24 | tr -d '\n')"
export PANEL_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
printf 'Save this panel password: %s\n' "${PANEL_PASSWORD}"

docker network create openrtmp

docker run -d --name librtmp2-panel-redis --network openrtmp redis:7-alpine

docker run -d \
  --name librtmp2-server \
  --network openrtmp \
  -p 1935:1935 -p 8080:8080 \
  -e LRTMP2_API_TOKEN=$LRTMP2_API_TOKEN \
  -e LRTMP2_DB=/data/server.db \
  -v librtmp2-server-data:/data \
  ghcr.io/openrtmp/librtmp2-server:latest

docker run -d \
  --name librtmp2-server-panel \
  --network openrtmp \
  -p 8000:8000 \
  -e LRTMP2_API_URL=http://librtmp2-server:8080 \
  -e LRTMP2_STATS_URL=http://localhost:8080 \
  -e LRTMP2_API_TOKEN=$LRTMP2_API_TOKEN \
  -e LRTMP2_DOMAIN=localhost \
  -e PASSWORD=$PANEL_PASSWORD \
  -e SECRET_KEY=$PANEL_SECRET \
  -e RATELIMIT_STORAGE_URI=redis://librtmp2-panel-redis:6379/0 \
  ghcr.io/openrtmp/librtmp2-server-panel:latest

Full stack (docker compose)

The panel repo's compose.quickstart.yml runs the same three services from the published images. Set secrets in .env before the first start so the server seeds the shared API token:

git clone https://github.com/OpenRTMP/librtmp2-server-panel.git
cd librtmp2-server-panel
cp .env.example .env
# Set LRTMP2_API_TOKEN, PASSWORD, SECRET_KEY, LRTMP2_DOMAIN
docker compose -f compose.quickstart.yml up -d

The repo's default docker-compose.yml instead builds the server from a sibling ../librtmp2-server checkout; use it only for source development.

Ports exposed by default (from librtmp2-server-panel/docker-compose.yml and librtmp2-server/.env.example):

PortService
1935RTMP ingest / playback (RTMP_BIND)
1936RTMPS ingest / playback (RTMPS_BIND) — only when TLS_ENABLED=true; not exposed in the default compose file (uncomment 1936:1936 there)
1940Cluster control plane (CLUSTER_BIND) — only when CLUSTER_ENABLED=true
1941Cluster media mesh (CLUSTER_MEDIA_BIND) — only when clustering is enabled
8080HTTP API, stats, health check (HTTP_BIND)
8000Web panel

To enable RTMPS alongside plaintext RTMP, set LRTMP2_TLS_ENABLED=true (or TLS_ENABLED=true in .env), mount cert/key files, expose port 1936, and set RTMPS_BIND=0.0.0.0:1936 as in librtmp2-server/docker-compose.yml. The panel shows rtmps:// URLs only when GET /api/v1/health reports rtmps_enabled: true (and uses LRTMP2_RTMPS_PORT, default 1936).

For a multi-node cluster, expose 1940 and 1941 between peers, set the CLUSTER_* variables described under HA clustering, and keep each node on its own SQLite volume. See the clustering guide for bootstrap and join steps.

API & Versioning

Only the public librtmp2 crate interface is the intended stable API surface. Everything under src/**/* that is not pub may change freely between releases.

librtmp2 follows SemVer and remains on 0.x during active development. For 0.x releases, minor versions may include breaking API changes; 1.0.0 marks the first stable public API. Pin the crates.io release you tested if you depend on a specific API shape.

librtmp2-server and librtmp2-server-panel are also pre-1.0 — REST API shapes, environment variable names, and Docker images may evolve. Prefer GitHub Releases and image tags over hard-coding versions from this page.