librtmp2 0.4.2

librtmp2 — RTMP/RTMPS protocol library
Documentation
# Changelog

All notable changes to this project will be documented in this file.

> ⚠️ **Alpha software.** `librtmp2` is in active early development. It has **no
> fixed, stable release version yet** — everything below is pre-release (alpha)
> and the API/ABI may change at any time without notice. Pin to a specific git
> commit if you depend on it.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
While in alpha the project stays on `0.x`; semantic-versioning guarantees only
begin at `1.0.0`.

## [Unreleased]

## [0.4.2] — 2026-07-21

### Security
- `Client::publish()` and `Client::play()` now bound their blocking AMF
  exchange with the configured connect-timeout wall-clock deadline instead
  of passing `None`, closing a stall window where a malicious server could
  hold the caller for up to ~650 seconds (64 recv polls × 10s) after connect
  succeeded.
- AMF route strings (connect `app`, publish/play stream names) are now
  decoded with strict UTF-8 validation and the command is rejected on
  failure; invalid UTF-8 previously collapsed to an empty string via
  `unwrap_or`, letting distinct wire-level names collide onto the same
  `(app, stream)` relay route.
- `read_string_checked()` now rejects embedded NUL bytes instead of copying
  them verbatim and letting `decode_route_amf_string()`'s NUL-sentinel scan
  truncate the value later, which let distinct invalid route values collapse
  onto the same app/stream route — undermining the UTF-8 collision guard
  above.
- The server session layer now rejects empty connect `app` names and empty
  publish/play stream names, and gates `onMetaData`/script relay to players
  and the stream cache on `on_media_cb` being registered.

### Fixed
- `bytes_received` is now tracked as `u64` (was `u32`), so WindowAckSize
  pacing stays correct once a connection passes 4 GiB of inbound data
  instead of wrapping.
- The client's Aggregate-message play path now passes sub-tag slices
  directly to `on_frame_cb` instead of cloning each sub-tag into a new
  `Vec`.

## [0.4.1] — 2026-07-18

### Changed
- `Client::connect()` now enforces a single wall-clock deadline across DNS
  resolution, TCP/TLS connect, the RTMP handshake, and the AMF
  `connect`/`createStream` exchange (previously only DNS/TCP connect were
  bounded). The default budget is unchanged (`TCP_CONNECT_TIMEOUT_SECS`, 10s);
  callers on slower or more loaded hosts can raise it with the new
  `Client::set_connect_timeout()`.

### Fixed
- Capped chained ModEx extension unwrapping at 32 layers to prevent CPU
  amplification from deeply nested wrappers on media frames.
- `Conn` now tracks the exact `PublishRouteRegistry` key it claimed and
  releases that same key on `FCUnpublish`/`closeStream`/play takeover,
  instead of re-deriving a route key from the current `relay_key` at release
  time. Fixes stale route ownership (blocking other publishers) and stale
  `stream_cache` entries when a host integrator pins `relay_key` after the
  initial publish claim.
- `publish()` now clears `Stream.is_playing` (mirroring what `play()` already
  does for `is_publishing`), so a play→publish switch on the same connection
  is correctly observable by hosts polling stream role flags.

## [0.4.0] — 2026-07-15

### Added
- Server-side E-RTMP v2 connect negotiation on the live session path:
  `fourCcList`, numeric `capsEx` capability bitmask, `videoFourCcInfoMap`,
  `reconnect`, and NetConnection `_error` responses when capability negotiation
  fails.
- Multitrack media support (E-RTMP v2 `AudioPacketType::Multitrack` /
  `VideoPacketType::Multitrack`): opaque relay of full multitrack messages,
  per-track `on_frame_cb` delivery with `Frame.track_id`, and init-cache replay
  of multitrack sequence-start headers to late-joining players.
- Enhanced init-frame classification via `exvideo`/`exaudio` parsers (HEVC, AV1,
  Opus, AAC) in `media/init_cache.rs`, replacing legacy nibble-only detection.
- `onMetaData` script caching in `StreamCache` and replay to players that join
  after the publisher has already sent metadata.
- Client receive path for AMF0/AMF3 `onMetaData` and RTMP Aggregate messages
  (`0x16`), unpacking sub-tags into the normal A/V and script frame callbacks.
- Legacy RTMP commands on the server session path: `pause`, `seek`,
  `receiveAudio`, `receiveVideo`, and `closeStream`.
- User Control message handling for `StreamBegin`, `StreamEOF`, and
  `SetBufferLength`; AMF3 Shared Object messages are accepted as no-ops.
- `examples/dump_frames.rs` — play a stream and print one line per received frame
  (type, timestamp, size, codec details) for debugging live publishers.
- libFuzzer harnesses under `fuzz/` for chunk reading, handshake parsing, AMF0
  skipping, E-RTMP parsers, and RTMP control-message decoders.
- CI jobs: `.github/workflows/sanitizers.yml` (ASan unit tests, overflow-check
  unit tests, ASan example builds) and `.github/workflows/fuzz.yml` (scheduled
  libFuzzer smoke runs).

### Changed
- The built-in relay fan-out budget is configurable through
  `Server::max_relay_sends_per_poll` (default: 4096 sends per poll). The first
  eligible frame in a poll is always processed even when its audience exceeds
  the budget, preventing an oversized fan-out frame from being re-queued forever.
- Connect AMF helpers now parse and write E-RTMP v2 capability representations,
  including wildcard FourCC entries, numeric `capsEx`, and per-codec
  `videoFourCcInfoMap` masks. The built-in client continues to connect without
  advertising capabilities; negotiation is active on the server session path.
- `Frame` now carries populated codec/header fields (`video_fourcc`, `audio_fourcc`,
  composition time, etc.) and optional `track_id` for multitrack callbacks.
- Corrected E-RTMP audio/video packet-type constants for sequence-end,
  multichannel, and multitrack values.
- Removed `docs/roadmap.md`; release status lives in `README.md` and
  `CHANGELOG.md`.

### Fixed
- ModEx prefix bytes are normalized only when the ModEx capability was negotiated
  and the leading bytes form an unambiguous ModEx wrapper; enhanced
  ExVideo/ExAudio, legacy AAC, and multitrack tags are preserved. Relay always
  forwards the original payload.
- Aggregate processing preserves relative sub-tag timestamps for A/V and metadata,
  rejects malformed or truncated payloads, and routes script tags through the
  normal metadata path.
- Re-enabling `receiveAudio` or `receiveVideo` schedules init-cache replay;
  paused players and disabled media types are filtered during relay.
- Enhanced `CodedFramesX` keyframes and multitrack sequence headers are classified
  correctly for late-join cache replay.
- Complex RTMP handshake (digest/HMAC) is not implemented; peers requesting it
  still receive a legacy simple S1/S2 response so ffmpeg and similar clients connect.

### Security
- Bound E-RTMP capability blobs to 4 KiB and Aggregate messages to 4096 sub-tags
  on both server and client receive paths; stream-cache resource accounting now
  includes metadata and per-track headers.

## [0.3.1] — 2026-07-13

### Fixed
- `Client::connect()` DNS resolution now respects the TCP connect deadline
  instead of blocking indefinitely in the system resolver; lookups run on a
  single shared worker thread with a bounded job queue, and worker startup
  failures return `ErrorCode::Internal` instead of panicking.
- `send_frame_payload()` and `lrtmp2_client_send_frame` now service inbound
  RTMP UserControl ping requests before sending media, so in-tree clients
  stay connected when the server enforces ping timeouts.
- Ping responses issued while draining inbound messages during `poll()` or
  publishing now use nonblocking `try_send` instead of blocking
  `Transport::send()`, so a zero-timeout poll cannot stall for up to 10
  seconds on a peer that stops reading.
- `service_inbound_nonblocking()` now honors the same per-poll byte and
  message budgets as `poll()`, and stops reading on transient EAGAIN instead
  of spinning until the socket blocks.
- Server-side ping RTT tracking starts only after the ping has fully left
  `send_buffer`; unflushed pings queued behind a slow reader no longer start
  the timeout early, and pings stuck unflushed longer than `PING_TIMEOUT`
  now close the connection. RTT timing now tracks the ping's own queued byte
  range instead of waiting for the entire `send_buffer` to drain, so prompt
  ping responses are not discarded when later media remains queued.
- `Client::poll()` now works while publishing to service inbound pings and
  retry queued ping responses after transient `EAGAIN`, and the shared DNS
  worker is re-created after a transient thread-spawn failure instead of
  permanently caching the error in a `OnceLock`.
- Publishing clients now poll for socket writability (`POLLOUT`) when queued
  pong bytes remain after a transient `EAGAIN`, so idle publishers can flush
  keepalive responses without sitting out the full read timeout.
- Publishing clients also poll `POLLOUT` when the send queue backs up during
  media upload, and preserve the correct TLS poll direction when flushing
  queued pong bytes after a transient `EAGAIN`.
- Nonblocking publish sends now propagate `try_flush_send_buffer()` errors
  instead of dropping flush failures on the floor.

### Security
- Inbound peers that open TCP but never complete the AMF connect exchange
  (including partial legacy handshake bytes) are now closed after a 10-second
  setup deadline instead of holding a connection slot until
  `max_connections` is reached.
- Server connections with unanswered or stale outbound RTMP pings are now
  closed instead of accumulating indefinitely in `pending_pings`.
- DNS lookups abandoned after the connect deadline no longer spawn unbounded
  detached resolver threads, and the shared resolver job queue is capped so
  wedged lookups cannot grow heap usage without bound.

## [0.3.0] — 2026-07-12

### Fixed
- `lrtmp2_client_create()` silently ignored `ServerConfig.tls_ca_file` and
  `ServerConfig.tls_insecure` — the client always verified `rtmps://` peers
  against only the system trust store regardless of what those fields were
  set to, even though the ABI documented them as controlling client TLS
  verification. `Client`/`Transport::connect_tls` now honor a caller-supplied
  CA bundle or an explicit opt-out of verification.
- `Transport::connect_tls()`'s `ca_file` option now *replaces* the trust
  store instead of adding the caller's CA bundle to the system default trust
  store — a custom CA is meant to restrict which peers are trusted, not
  merely extend the existing set.
- `insecure = true` no longer loads the system's default CA verify paths at
  all, so `rtmps://` connections with verification intentionally disabled no
  longer fail on hosts without a usable default CA store.
- `lrtmp2_client_create()` now rejects (returns NULL for) a non-UTF-8
  `tls_ca_file` path instead of silently discarding it and falling back to
  default verification.

### Changed
- `Transport::connect_tls()` (Rust-only API, not part of the FFI ABI) gained
  two new parameters (`ca_file: Option<&str>`, `insecure: bool`) to support
  the fix above. Bumped the minor version per this crate's pre-1.0
  versioning policy (breaking Rust API change, ABI/FFI surface unaffected).

## [0.2.1] — 2026-07-10

### Added
- `lrtmp2_tls_supported()` runtime capability check exported to FFI
- Parse `onMetaData` from RTMP data messages into `Conn` fields (duration, width, height, framerate, videodatarate, audiodatarate, codec info)

### Fixed
- `flv::audio_tag` / `video_tag` / `script_tag` parsers now reset the
  caller-owned tag struct at the start of every `parse()` call, so switching
  between codecs mid-stream (or a shorter value following a longer one, e.g.
  a script tag name) no longer leaves stale fields from a previous parse
- CodeQL invalid-pointer alert in FFI `server_create` test resolved
- Borrow checker errors in AMF0 `skip_value_depth`
- `onMetaData` parsing scope and lifecycle per review feedback

### Security
- Cap and copy FFI frame payloads before sending and reject an oversized
  `frame.size`; ignore inbound media whose `msg_stream_id` doesn't match
  the current stream; retain `on_frame_cb` payloads in connection-scoped
  scratch buffers instead of a shared one
- Add per-poll (256 KiB) and per-command-wait (256 KiB) byte budgets to the
  RTMP client's recv path, mirroring the server's existing fairness cap, so
  a malicious server can no longer monopolize the embedder's event-loop
  thread or force hundreds of megabytes through the AMF connect handshake
- `lrtmp2_server_create` now substitutes a default `max_connections` (256)
  when the FFI caller passes a zero-initialized `ServerConfig` (e.g. via
  `calloc`/`{0}`), which previously disabled all connection limiting; an
  explicit negative value continues to mean "unlimited", matching
  `Server::new`

### Documentation
- Improved `onMetaData` parsing robustness per CodeRabbit/Codex reviews

## [0.2.0] — 2026-07-10

### Added
- RTMP Aggregate message (`0x16`) handling: aggregate-framed audio/video
  from a publisher is now unpacked and relayed through the normal
  media-frame path instead of being silently dropped

### Fixed
- `Client::connect()` now actually accepts `rtmps://` URLs via the new
  `Transport::connect_tls()` (dialing over TLS and verifying the server
  certificate against the system trust store) — previously only the
  server side implemented RTMPS, despite the 0.1.0 notes describing
  client-side `rtmps://` support; `parse_rtmp_url()` now recognizes the
  scheme and defaults to port 443
- `flv::audio_tag` / `video_tag` / `script_tag` parsers now reset the
  caller-owned tag struct at the start of every `parse()` call, so switching
  between codecs mid-stream (or a shorter value following a longer one, e.g.
  a script tag name) no longer leaves stale fields from a previous parse
- The RTMP client's inbound recv budget accounting no longer discards an
  already-read chunk once it slightly exceeds the remaining budget; the read
  itself is now capped at the remaining budget so bytes belonging to the
  response being waited for are never dropped
- `Client::poll()` no longer risks blocking in `poll(2)` for the full
  timeout when TLS already has decrypted plaintext buffered internally from
  a previous budget-limited drain
- FFI `lrtmp2_client_send_frame` and `Client::send_frame_payload` now
  enforce the max client frame-size cap consistently on every call path,
  and reject a stale/non-owned frame pointer when the client isn't in the
  `Publishing` state, instead of only checking on one of two paths
- The server poll loop keeps draining a connection's already-buffered
  messages (up to 3 extra passes) when a batch exceeds the per-recv message
  budget, instead of stalling until the peer happens to send more bytes
- The RTMPS client now checks TLS support before dialing, instead of
  opening a plaintext TCP connection first even in a
  `--no-default-features` build that given an `rtmps://` URL would only
  reject after connecting
- The client's blocking read helpers now poll for write-readiness (not
  just read-readiness) when a TLS read reports `WANT_WRITE` during
  renegotiation, instead of stalling until timeout on the wrong direction
- The client's TLS handshake is now bounded by the same 10s timeout used
  elsewhere in the transport, instead of blocking indefinitely if a peer
  stalls mid-handshake
- Retry `poll(2)` on `EINTR` in the client's transport-readiness wait
  helper instead of surfacing it as a hard I/O error and aborting the
  caller's read/handshake

### Security
- Cap and copy FFI frame payloads before sending and reject an oversized
  `frame.size`; ignore inbound media whose `msg_stream_id` doesn't match
  the current stream; retain `on_frame_cb` payloads in connection-scoped
  scratch buffers instead of a shared one
- Add per-poll (256 KiB) and per-command-wait (256 KiB) byte budgets to the
  RTMP client's recv path, mirroring the server's existing fairness cap, so
  a malicious server can no longer monopolize the embedder's event-loop
  thread or force hundreds of megabytes through the AMF connect handshake
- `lrtmp2_server_create` now substitutes a default `max_connections` (256)
  when the FFI caller passes a zero-initialized `ServerConfig` (e.g. via
  `calloc`/`{0}`), which previously disabled all connection limiting; an
  explicit negative value continues to mean "unlimited", matching
  `Server::new`

## [0.1.1] — 2026-07-08

### Fixed
- Cap per-connection recv drain in `process_connections()` to 256 KiB per
  poll pass, preventing a peer that keeps its kernel recv buffer full from
  starving other sessions in the single-threaded poll loop

### Documentation
- Update docs.rs badge to track the latest published version

## [0.1.0] — 2026-07-08

First tagged pre-release. `librtmp2` is a Rust crate (built via Cargo as
`cdylib`/`staticlib`/`lib`) exposing both an idiomatic Rust API and an
FFI-compatible `extern "C"` layer for consumption from C, Go, Python, PHP,
and others.

### Added
- TLS / RTMPS support via OpenSSL, enabled by default through the `tls`
  Cargo feature (`cargo build --no-default-features` for a zero-dependency,
  plaintext-only build)
- Transport abstraction shared by plaintext RTMP and TLS so the layers above
  never branch on the wire type
- Server-side TLS termination and client-side `rtmps://` connect with SNI
  and certificate verification
- `lrtmp2_tls_supported()` runtime capability check
- Legacy RTMP protocol support (handshake, chunk, message, AMF0)
- Enhanced RTMP v1 support (ExVideo/ExAudio headers, FourCC registry, HDR/colorInfo)
- Enhanced RTMP v2 support (capsEx, reconnect, multitrack, ModEx)
- Full server API with callbacks (`on_connect`, `on_publish`, `on_play`, `on_frame`, `on_close`)
- Full client API with publish/play flows
- Frame API supporting audio, video, script, and metadata types
- H.264, H.265, AV1, and legacy video codec support
- AAC, Opus, MP3, G.711 audio codec support
- Example programs: `minimal_server`, `minimal_client`, `play_pull`, `ffmpeg_ingest`
- Inline unit tests throughout `src/`, an end-to-end loopback integration
  test (`tests/server_client_loopback.rs`), and interop shell scripts
  (`tests/interop/`)
- ABI baseline tooling (`scripts/abi-baseline.sh`) for `0.x` compatibility checks
- Automated ABI compliance checks in CI (`cargo-semver-checks` via `.github/workflows/abi-check.yml`)

### Security
- Bounds-checked parsers for all network-provided length fields
- Constant-time RNG for handshake
- Safe handling of unknown E-RTMP v2 ModEx types (degrades to NOP, not panic)

### Documentation
- `CLAUDE.md` with build commands and architecture guide
- `docs/abi-policy.md` with ABI compliance checklist
- Protocol mapping documents for legacy, E-RTMP v1, and E-RTMP v2
- `CONTRIBUTING.md` guidelines

[Unreleased]: https://github.com/OpenRTMP/librtmp2/compare/v0.4.2...HEAD
[0.4.2]: https://github.com/OpenRTMP/librtmp2/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/OpenRTMP/librtmp2/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/OpenRTMP/librtmp2/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/OpenRTMP/librtmp2/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/OpenRTMP/librtmp2/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/OpenRTMP/librtmp2/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/OpenRTMP/librtmp2/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/OpenRTMP/librtmp2/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/OpenRTMP/librtmp2/releases/tag/v0.1.0