oxideav-rtmp
Pure-Rust RTMP for the
oxideav framework — accept an
incoming publisher (server / source) or push your own stream to a
remote RTMP server (client / sink). Zero external dependencies,
blocking-thread-per-connection.
Server (accept a publisher)
use ;
let server = bind?;
let req = server.accept?; // blocks until one publisher connects
if req.stream_name != "my-secret-key"
let mut session = req.accept?; // sends NetStream.Publish.Start
while let Some = session.next_packet?
Multi-client variant — one thread per connection:
server.serve?;
Client (push to a remote RTMP server)
use RtmpClient;
let mut client = connect?;
client.send_video_sequence_header?; // AVCDecoderConfigurationRecord
client.send_audio_sequence_header?; // 2-byte AudioSpecificConfig
loop
client.close?;
Scope
-
RTMP (
rtmp://, plain TCP port 1935). No RTMPS yet — wrap ourRead + Writewith rustls if you need it, or request anrtmpsfeature. -
Publish direction only. The server accepts incoming publishers; the client pushes to remote servers. RTMP play (subscribe / pull) is a follow-up.
-
AMF0 command flow is what every commodity ingest endpoint (OBS / Wirecast / nginx-rtmp / libavformat) negotiates. The [
amf3] module ships a complete AMF3 wire-format encoder + decoder — all thirteen markers plus the three reference tables — and AMF3 data / command messages are now routed end-to-end: the server decodesonMetaDatacarried as a type-15 AMF3 data message (per AMF 3 spec §4.1 / AMF 0 spec §3.1, an AMF0 frame switching to AMF3 via theavmplus-object-marker0x11), bridges the AMF3 value graph ontoAmf0Value, and surfaces it through the sameStreamPacket::Metadatapath as AMF0; type-17 AMF3 commands feed the same stream-teardown detection.RtmpClient::send_metadata_amf3emits the AMF3-encoded form for peers on an AMF3 channel. Shared objects, RTMFP, and the Adobe digest-verified handshake remain unimplemented. -
H.264 + AAC are the canonical legacy payloads, plus Enhanced RTMP v1 (Veovera 2023) FourCC video codecs —
hvc1(HEVC / H.265),av01(AV1),vp09(VP9) — and the Enhanced RTMP v2 (Veovera 2026) additions:vp08(VP8),avc1(FourCC-mode AVC / H.264),vvc1(VVC / H.266). Sequence-start (HEVC / VVC / AVCDecoderConfigurationRecord,AV1CodecConfigurationRecord,VPCodecConfigurationRecordfor VP8 + VP9),CodedFrames,CodedFramesX(CTS=0 omitted),SequenceEnd, and the HDRPacketTypeMetadata(colorInfo) frames all round-trip viaflv::parse_video/flv::build_video. SI24compositionTimeOffsetis emitted on the wire for the three NALU-based FourCCs (hvc1/avc1/vvc1) withCodedFramesand stripped forCodedFramesXper the v2 spec. The crate passes through FLV tag bytes verbatim, so additional codecs (MP3, H.263, Speex, …) keep working too. -
Enhanced RTMP v2 (Veovera 2026) FourCC audio codecs:
Opus,fLaC(FLAC),ac-3(AC-3),ec-3(E-AC-3),.mp3,mp4a(AAC, added FourCC signalling).SequenceStart,CodedFrames, andSequenceEndAudioPacketTypes round-trip viaflv::parse_audio/flv::build_audio; bodies are the per-codec shapes called out inenhanced-rtmp-v2.pdf§"ExAudioTagBody" (OpusHeadID-header per RFC 7845 §5.1 for Opus SequenceStart,fLaC + STREAMINFOper Xiph FLAC §7 for FLAC SequenceStart, ATSC sync frames for AC-3 / E-AC-3 CodedFrames, MPEG Layer III frames for MP3, ISO/IEC 14496-3AudioSpecificConfigfor FourCC-AAC SequenceStart). TheMultichannelConfigAudioPacketType is also decoded end-to-end:AudioTag::multichannel_config()lifts the body into the strongly-typedMultichannelConfigview (perenhanced-rtmp-v2.pdf§"ExAudioTagBody" —audioChannelOrder(UI8) | channelCount(UI8) | (audioChannelMapping[UI8] | audioChannelFlags(UI32))), andAudioTag::multichannel_config_tagrebuilds the matching outbound tag.audio_channel/audio_channel_maskpublic submodules name all 24 spec-defined positions (including the 22.2 surround extras from SMPTE ST 2036-2-2008). TheMultitrackAudioPacketType / VideoPacketType is also wired end-to-end: themultitrackType (UB[4]) | realPacketType (UB[4])byte plus the optional shared FourCC (per spec, omitted inManyTracksManyCodecsmode) are consumed inline byparse_audio/parse_video, and the per-track list ((trackFourCc if ManyTracksManyCodecs) | trackId(UI8) | (sizeOfTrack(UI24) if not OneTrack) | body) lifts to a typed [Multitrack { multitrack_type, tracks: Vec<MultitrackTrack> }] onVideoTag::multitrack/AudioTag::multitrack.OneTrack/ManyTracks/ManyTracksManyCodecsall round-trip — including the inner-PacketType-MUST-NOT-be-Multitrack invariant from the spec — via the newVideoTag::multitrack_tag/AudioTag::multitrack_tagbuilders. ReservedmultitrackTypevalues (3..=15) pass through verbatim so a forwarding ingest preserves future modes. -
Graceful session close.
RtmpSession::closeemits aUserControl StreamEOF(stream_id)(RTMP 1.0 §7.1.7) beforeonStatus("NetStream.Unpublish.Success"), flushes the chunk writer, and half-closes the write side. The peer drains every buffered frame plus the EOF + status reply before observing FIN, matching the client-side teardown behaviour. Symmetrically,RtmpClient::poll_eventsurfaces server-originatedUserControl StreamEOF,UserControl StreamBegin,onStatus(...), and_result/_errorreplies as aClientEventenum so a publisher can distinguish a clean server-initiated end-of-stream from an unexpected TCP FIN. Protocol-control plumbing (Set Chunk Size, Window Ack Size, Set Peer Bandwidth, Ping Request → Ping Response) is handled transparently insidepoll_event. -
Injection-robust parser surface. Every public decode entry point — AMF0 (
decode/decode_all), AMF3 (decode/decode_all/decode_data_message), FLV (parse_video/parse_audio), the chunk-stream reader (ChunkReader::read_message), both handshake directions — is fuzzed against deterministic random byte streams (tests/injection_robustness.rs) and against structurally-corrupted RTMP frames (oversize lengths, forged fmt-1 chunks without prior fmt-0 state, truncated handshakes, wrong RTMP version bytes,u32::MAXstrict-array counts). Every call returnsResult, never panics, never spins, and never over-allocates. Stack-overflow protection:amf::MAX_DECODE_DEPTHandamf3::MAX_DECODE_DEPTH(both 64) cap nested-container recursion in both AMF dialects before the call stack runs out, so a forged onMetaData with 2_000 nested Objects surfaces a clean error rather than crashing. -
Enhanced RTMP v2
ModExprelude (Veovera 2026). TheModExpacket-type signal (enhanced-rtmp-v2.pdf§"ExVideoTagHeader" / §"ExAudioTagHeader") is decoded for both audio and video: a chain of size-prefixedmodExDataentries (UI8 + 1length, escaping to0xFF+UI16 + 1for 256..=65536-byte payloads) preceding the FourCC, each terminated by amodExType | next-PacketTypenibble byte. The chain round-trips throughflv::parse_*/flv::build_*via the newVideoTag::mod_ex/AudioTag::mod_ex(Vec<flv::ModEx>) fields, and the real PacketType is recovered from the chain so the packet adapters route a ModEx-prefixed tag transparently. The only subtype defined today,TimestampOffsetNano(abytesToUI24sub-millisecond presentation offset, 0..=999_999 ns), is exposed via{Video,Audio}Tag::timestamp_offset_nano; folding that nanosecond offset into the millisecondPackettimeline is a follow-up.
Pipeline integration (SourceRegistry)
Wire rtmp:// URIs into the workspace's
[oxideav_core::SourceRegistry] so the pipeline executor reads
RTMP streams via the same dispatch as file:// and http(s)://:
use SourceRegistry;
let mut reg = new;
register;
// `rtmp://host:port/app/stream-name` opens a one-shot listener
// that accepts a single publisher and surfaces it as a
// `PacketSource` (audio = stream 0, video = stream 1, both with
// time_base 1/1000). Codec ids are auto-detected from the
// publisher's first audio + video tags (h264, hevc, av1, vp9 in
// Enhanced-RTMP-v1 FourCC mode; vp8, h264 (avc1), vvc added in
// Enhanced-RTMP-v2 FourCC mode; h264, h263, vp6f / vp6a, flashsv /
// flashsv2 for legacy single-byte codec ids; opus, flac, ac3,
// eac3, mp3, aac in Enhanced-RTMP-v2 audio FourCC mode; aac, mp3,
// pcm_*, speex, nellymoser for legacy single-byte audio
// sound-format).
let _src = reg.open?;
The opener is listen-style: each open() binds the URL's
host:port, accepts one publisher, validates the announced
app + stream_name against the URL path (rejects on
mismatch), then hands packets to the registry. For multi-client
service, keep using [RtmpServer::serve] directly.
Reusable building blocks
The lower-level modules are public so callers can compose something non-standard:
amf::{encode, decode, encode_command, Amf0Value}amf3::{encode, decode, decode_all, decode_data_message, encode_all, Amf3Value, Decoder}plusAmf3Value::to_amf0()andamf3::AVMPLUS_OBJECT_MARKERchunk::{ChunkReader, ChunkWriter, Message}handshake::{client_handshake, server_handshake}flv::{parse_video, build_video, parse_audio, build_audio, ModEx}message::build_*— builders for every protocol-control / command message we emit