rtmp-rs is an async RTMP server and RTMP client for Rust, built on Tokio. Use it to ingest live streams from OBS or FFmpeg, relay them to viewers, pull a stream from another server, or publish to YouTube, Twitch and other RTMPS endpoints. It supports legacy RTMP (H.264 and AAC) as well as Enhanced RTMP v2 with HEVC, AV1, VP9, Opus and FLAC.
The server accepts what real encoders send. Empty app names, timestamp regressions and other quirks are tolerated instead of dropping the connection. A GOP cache gives late joiners a keyframe right away, and slow subscribers lose video frames before they lose audio.
- An RTMP server with stream key routing and pub/sub built in. Publishers and players are matched by stream key with no extra code.
- An RTMP client that can pull a stream or publish one, over plain RTMP or RTMPS (TLS).
- Enhanced RTMP v2 with codec negotiation. HEVC, AV1, VP9 and VP8 video, Opus, FLAC, AC-3 and E-AC-3 audio, plus H.264 and AAC. Older clients fall back to legacy RTMP automatically.
- A
RtmpHandlertrait with optional callbacks for authentication, metadata and per-frame access. Every callback has a default, so you override only the ones you care about. - GOP caching so a viewer joining mid-stream starts on a keyframe instead of waiting for the next one.
- Backpressure handling that drops video for slow subscribers while keeping audio continuous.
- Media payloads passed around as
bytes::Bytes, so a frame is shared between subscribers rather than copied per connection. - Parsers for FLV tags, H.264 NAL units, AAC frames and the Enhanced RTMP packet formats.
cargo add rtmp-rsFor RTMPS, enable the tls feature:
cargo add rtmp-rs --features tlsA working server is a handler struct and a few lines of main:
use rtmp_rs::{RtmpServer, ServerConfig, RtmpHandler, AuthResult};
use rtmp_rs::session::SessionContext;
use rtmp_rs::protocol::message::{ConnectParams, PublishParams};
struct MyHandler;
impl RtmpHandler for MyHandler {
async fn on_connect(&self, _ctx: &SessionContext, params: &ConnectParams) -> AuthResult {
println!("App: {}", params.app);
AuthResult::Accept
}
async fn on_publish(&self, _ctx: &SessionContext, params: &PublishParams) -> AuthResult {
println!("Stream key: {}", params.stream_key);
// Validate the stream key here
AuthResult::Accept
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = RtmpServer::new(ServerConfig::default(), MyHandler);
server.run().await?;
Ok(())
}Then point an encoder at it:
# OBS: Server rtmp://localhost/live, Stream Key test
# FFmpeg
ffmpeg -re -i input.mp4 -c copy -f flv rtmp://localhost/live/test
# Watch it
ffplay rtmp://localhost/live/testuse std::time::Duration;
use rtmp_rs::ServerConfig;
let config = ServerConfig::default()
.bind("0.0.0.0:1935".parse()?)
.max_connections(1000)
.chunk_size(4096)
.connection_timeout(Duration::from_secs(10))
.idle_timeout(Duration::from_secs(60));RtmpServer::run_until takes a shutdown future if you need graceful shutdown.
RtmpPuller connects to a server, plays a stream and hands you parsed frames and raw FLV tags over a channel:
use rtmp_rs::client::{ClientConfig, ClientEvent, RtmpPuller};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ClientConfig::new("rtmp://server/live/stream_key");
let (puller, mut events) = RtmpPuller::new(config);
tokio::spawn(async move {
while let Some(event) = events.recv().await {
match event {
ClientEvent::VideoFrame { timestamp, data } => {
if data.is_keyframe() {
println!("Keyframe at {}ms", timestamp);
}
}
ClientEvent::AudioFrame { timestamp, .. } => {
println!("Audio at {}ms", timestamp);
}
ClientEvent::Disconnected => break,
_ => {}
}
}
});
puller.start().await?;
Ok(())
}RtmpPublisher pushes a stream to a remote server. It is codec-agnostic. You hand it FLV audio tag bodies with timestamps and it does the chunking and the RTMP handshake:
use bytes::Bytes;
use rtmp_rs::client::{ClientConfig, PublishEvent, RtmpPublisher};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ClientConfig::new("rtmp://server/live/stream_key");
let (mut publisher, mut events) = RtmpPublisher::new(config);
tokio::spawn(async move {
while let Some(event) = events.recv().await {
if let PublishEvent::Error(e) = event {
eprintln!("publish error: {e}");
}
}
});
publisher.connect().await?;
// AAC sequence header: FLV audio tag header (0xAF) + packet type 0 + AudioSpecificConfig
let header = Bytes::from(vec![0xAF, 0x00, 0x12, 0x10]);
publisher.send_audio(header, 0).await?;
// Raw AAC frames follow with packet type 1
// publisher.send_audio(Bytes::from(vec![0xAF, 0x01, /* AAC frame */]), timestamp_ms).await?;
publisher.disconnect().await;
Ok(())
}The publisher currently sends audio only. See RtmpPublisher::send_audio for the FLV tag layout per codec.
With the tls feature enabled, any rtmps:// URL uses TLS. The default port is 443. Certificates are checked against the Mozilla root store bundled by webpki-roots, so public endpoints such as YouTube and Twitch work without configuration:
let config = ClientConfig::new("rtmps://a.rtmps.youtube.com:443/live2/STREAM_KEY");To trust a private CA or a self-signed certificate on a server you run, add it as an extra root. The certificate type comes from rustls, so add rustls (or rustls-pki-types) to your own dependencies to name it:
use rustls::pki_types::CertificateDer;
let cert: CertificateDer<'static> = /* load DER */;
let config = ClientConfig::new("rtmps://ingest.internal:1936/live/key")
.tls_root_cert(cert);Extra roots are added on top of the built-in store.
RtmpHandler is where authentication and media processing live. Override what you need and leave the rest:
use rtmp_rs::{RtmpHandler, AuthResult};
use rtmp_rs::session::SessionContext;
use rtmp_rs::protocol::message::PublishParams;
struct AuthHandler;
impl RtmpHandler for AuthHandler {
async fn on_publish(&self, _ctx: &SessionContext, params: &PublishParams) -> AuthResult {
if validate_stream_key(¶ms.stream_key) {
AuthResult::Accept
} else {
AuthResult::Reject("Invalid stream key".into())
}
}
}| Callback | When to use it |
|---|---|
on_connection |
New TCP connection. IP blocklists, rate limiting |
on_handshake_complete |
After the RTMP handshake, before connect |
on_connect |
Validate the app name, read auth tokens from tcUrl |
on_disconnect |
Cleanup and logging |
on_fc_publish |
Early stream key check (OBS sends this before publish) |
on_publish |
Stream key authentication |
on_unpublish |
Publisher went away |
on_play |
Subscriber authorization |
on_pause, on_unpause |
Subscriber paused or resumed |
on_metadata |
Resolution, bitrate, codec from onMetaData |
on_media_tag |
Raw FLV tags, for recording or filtering |
on_video_frame |
H.264 NAL units (legacy RTMP) |
on_audio_frame |
AAC frames (legacy RTMP) |
on_enhanced_video_frame |
HEVC, AV1, VP9, VP8 frames (Enhanced RTMP) |
on_enhanced_audio_frame |
Opus, FLAC, AC-3, E-AC-3 frames (Enhanced RTMP) |
on_keyframe |
GOP boundaries |
rtmp-rs implements Enhanced RTMP v2. The default mode is Auto, which advertises capabilities during connect and falls back to legacy RTMP for clients that do not understand them. OBS 30+ and recent FFmpeg builds can send HEVC and AV1 this way.
| Video | Audio |
|---|---|
| H.264/AVC | AAC |
| H.265/HEVC | Opus |
| AV1 | FLAC |
| VP9 | AC-3 |
| VP8 | E-AC-3 |
use rtmp_rs::{ServerConfig, EnhancedRtmpMode, EnhancedServerCapabilities};
use rtmp_rs::media::fourcc::{VideoFourCc, AudioFourCc};
use rtmp_rs::protocol::enhanced::FourCcCapability;
// Default: Auto mode with the common codecs
let config = ServerConfig::default();
// Require Enhanced RTMP and reject legacy clients
let config = ServerConfig::default()
.enhanced_rtmp(EnhancedRtmpMode::EnhancedOnly);
// Legacy RTMP only
let config = ServerConfig::default()
.enhanced_rtmp(EnhancedRtmpMode::LegacyOnly);
// Advertise a specific codec set
let caps = EnhancedServerCapabilities::minimal()
.with_video_codec(VideoFourCc::Hevc, FourCcCapability::forward())
.with_video_codec(VideoFourCc::Av1, FourCcCapability::forward())
.with_audio_codec(AudioFourCc::Opus, FourCcCapability::forward());
let config = ServerConfig::default()
.enhanced_capabilities(caps);use rtmp_rs::media::{EnhancedVideoData, EnhancedAudioData};
use rtmp_rs::session::StreamContext;
impl RtmpHandler for MyHandler {
async Scan report · 2026-09-11 - ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
report this listing
— log in to report
0 comments
log in to comment.