# 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.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.3.1...HEAD
[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