From df5bf80ba164708e6f96ad06aab6e39b749d032f Mon Sep 17 00:00:00 2001 From: AlexanderWagnerDev Date: Tue, 14 Jul 2026 22:06:59 +0200 Subject: [PATCH 01/20] docs: align README with code-accurate implementation status Replace marketing-style feature claims with what is actually wired into the live session path versus parser-only library code. --- README.md | 71 ++++++++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d3ec429..8137904 100644 --- a/README.md +++ b/README.md @@ -12,45 +12,44 @@ Focused on RTMP/E-RTMP only. SQLite-backed. JSON stats. Nginx-compatible XML. ## Project Status -`librtmp2-server` is currently **Alpha** software. +`librtmp2-server` is **alpha** software: the HTTP/API/DB layer is usable, but RTMP behaviour comes entirely from the embedded [`librtmp2`](https://github.com/OpenRTMP/librtmp2) crate — see that repo's [implementation status](https://github.com/OpenRTMP/librtmp2#implementation-status) for protocol details. -Implemented in this repository: +### Implemented in this repository -- **Integrated RTMP listener** — listens on the configured `RTMP_BIND` address through the Rust `librtmp2` server implementation -- **RTMPS listener** — when `TLS_ENABLED=true`, a second listener on `RTMPS_BIND` accepts RTMPS *alongside* the plaintext RTMP listener -- **OBS / FFmpeg publishing path** — publish requests are routed through the DB-backed stream-key validation layer -- **Play / publish authentication** — separate `publish_key` and `play_key` validation -- **SQLite persistence** — streams, publishers, players, and stats are stored in a database -- **Live publisher/player tracking** — connection state is mirrored into the database -- **JSON stats** — `/stats?key=***` clean modern JSON -- **Nginx-RTMP XML** — `/stats-nginx?key=***` for existing tools -- **REST API** — Stream CRUD, Bearer token auth -- **Docker-ready** — Lightweight Alpine container +- **RTMP listener** on `RTMP_BIND` via `librtmp2` +- **RTMPS listener** on `RTMPS_BIND` when `TLS_ENABLED=true` (alongside plaintext RTMP) +- **OBS / FFmpeg publish path** with DB-backed `publish_key` validation +- **Play authentication** via `play_key` +- **Publisher → player relay** (same `(app, stream)` route) +- **SQLite persistence** — streams, publishers, players, stats +- **JSON stats** — `/stats?key=***` +- **Nginx-RTMP XML** — `/stats-nginx?key=***` +- **REST API** — stream CRUD, Bearer token auth +- **Docker** — Alpine-based images on GHCR -Still under active development: +### Protocol behaviour inherited from librtmp2 (not reimplemented here) -- RTMPS/TLS production readiness -- Protocol completeness and compatibility hardening -- Performance optimization -- Production hardening -- Additional E-RTMP functionality +- Live H.264/AAC and Enhanced-RTMP **passthrough** ingest (HEVC/AV1 from FFmpeg/OBS) +- Late player join gets cached **legacy** H.264/AAC sequence headers + last keyframe only — not Enhanced-RTMP sequence starts +- Publisher `onMetaData` is parsed for stats; **not** forwarded to players +- No VOD commands (`pause` / `seek`), no E-RTMP v2 session negotiation, no nginx-rtmp feature parity (HLS, exec, push relay, recording) -The server is not intended to be presented as a drop-in production replacement for `nginx-rtmp` yet. Test your OBS/FFmpeg workflow before using it for critical streams. +Test your OBS/FFmpeg workflow before using this for critical streams. It is not a drop-in replacement for `nginx-rtmp`. --- ## Features -- **Integrated RTMP listener** — built on the Rust `librtmp2` implementation -- **RTMP + RTMPS at the same time** — RTMPS is an additional listener, not a mode switch; plaintext RTMP keeps working when TLS is enabled -- **SQLite persistence** — Streams, publishers, players, stats all in a DB +Everything below is implemented **in this repo**. Wire-protocol limits are defined by `librtmp2` (see link above). + +- **Integrated RTMP listener** — built on the `librtmp2` crate +- **RTMP + RTMPS at the same time** — RTMPS is an additional listener, not a mode switch +- **SQLite persistence** — streams, publishers, players, stats - **Unique keys per stream** — `publish_key`, `play_key`, `stats_key` -- **Privacy by design** — No one can see streams/stats without the exact key -- **JSON stats** — `/stats?key=***` clean modern JSON -- **Nginx-RTMP XML** — `/stats-nginx?key=***` for existing tools -- **REST API** — Stream CRUD, Bearer token auth -- **Docker-ready** — Lightweight Alpine container -- **Alpha quality** — Interfaces and implementation details may still change +- **Privacy by design** — no public stream list without keys +- **JSON + Nginx-compatible XML stats** +- **REST API** — stream CRUD, Bearer token auth +- **Docker-ready** — lightweight Alpine container --- @@ -69,16 +68,15 @@ connections on both, and forwards connect/publish/play/close events into the DB-backed [`RtmpEventHandler`](src/rtmp_bridge.rs) bridge while also updating codec/stats data. -Both `librtmp2-server` and `librtmp2` are under active development and should be -considered Alpha software. +Both `librtmp2-server` and `librtmp2` are alpha; API and protocol details may still change. ```text OBS / FFmpeg / App │ ▼ librtmp2-server (Rust) - ├── RTMP Listener (port 1935) ← integrated via librtmp2 (Alpha), always on - ├── RTMPS Listener (port 1936) ← alongside RTMP, only when TLS_ENABLED=true + ├── RTMP Listener (port 1935) ← librtmp2, always on + ├── RTMPS Listener (port 1936) ← librtmp2, when TLS_ENABLED=true ├── SQLite (streams, publishers, players, stats) ├── HTTP API (port 8080, axum) │ ├── /api/v1/streams CRUD @@ -88,12 +86,9 @@ OBS / FFmpeg / App │ ▼ librtmp2 (Rust) - ├── Handshake - ├── Chunking - ├── AMF - ├── RTMP Commands - ├── E-RTMP v1 - └── E-RTMP v2 + ├── Live publish/play relay + ├── Legacy RTMP session core + └── E-RTMP passthrough + parser modules (see librtmp2 README) ``` --- From 3e57d44e4cc6aae6083d4f096fb2ab21b997ad59 Mon Sep 17 00:00:00 2001 From: AlexanderWagnerDev Date: Tue, 14 Jul 2026 22:49:29 +0200 Subject: [PATCH 02/20] chore: bump librtmp2 to 0.4.0 (0.1.5) Pin librtmp2 0.4.0 via git until crates.io release, update inherited protocol docs, and fix Frame initializer in RTMP HTTP E2E test. Co-authored-by: Cursor --- AGENTS.md | 2 +- CHANGELOG.md | 15 ++++++++++++++- Cargo.lock | 7 +++---- Cargo.toml | 4 ++-- README.md | 9 +++++---- tests/rtmp_http_e2e.rs | 9 +-------- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 164c216..61585bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** the server depends on `librtmp2 = "0.3.1"` from **crates.io**, not the sibling `../librtmp2` checkout. Editing the local `librtmp2` repo has no effect here unless you add a `[patch]`/path override. +- **Dependency source:** the server pins `librtmp2 = "0.4.0"` (currently via git rev until the crate is on crates.io). It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9794a20..2f82ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ begin at `1.0.0`. ## [Unreleased] +## [0.1.5] — 2026-07-14 + +### Changed +- Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation, + multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek). + Until `0.4.0` is on crates.io the lockfile resolves it from git + (`OpenRTMP/librtmp2` @ `d064938`); switch back to a crates.io version pin after + release. +- Update README protocol notes to match inherited `librtmp2` 0.4.0 behaviour. +- Adjust the RTMP HTTP E2E test `Frame` initializer for the appended `track_id` + field in `librtmp2::types::Frame`. + ## [0.1.4] — 2026-07-13 ### Fixed @@ -186,7 +198,8 @@ plaintext RTMP and RTMPS. ### Planned - REST API enhancements for server management -[Unreleased]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.4...HEAD +[Unreleased]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.5...HEAD +[0.1.5]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.4...v0.1.5 [0.1.4]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.3...v0.1.4 [0.1.3]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/OpenRTMP/librtmp2-server/compare/v0.1.1...v0.1.2 diff --git a/Cargo.lock b/Cargo.lock index dc7c851..4794d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,9 +867,8 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b69e88de9dfd598389e513f421e1fe9cd8d1a0d980a810acbed84297f32cfb" +version = "0.4.0" +source = "git+https://github.com/OpenRTMP/librtmp2?rev=d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4#d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4" dependencies = [ "cc", "libc", @@ -880,7 +879,7 @@ dependencies = [ [[package]] name = "librtmp2-server" -version = "0.1.4" +version = "0.1.5" dependencies = [ "axum", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ddd3b6c..a76483e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librtmp2-server" -version = "0.1.4" +version = "0.1.5" edition = "2024" rust-version = "1.95" license = "MIT" @@ -15,7 +15,7 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -librtmp2 = { version = "0.3.1", features = ["tls"] } +librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } rusqlite = { version = "0.40", features = ["bundled"] } diff --git a/README.md b/README.md index 8137904..ed86fba 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,11 @@ Focused on RTMP/E-RTMP only. SQLite-backed. JSON stats. Nginx-compatible XML. ### Protocol behaviour inherited from librtmp2 (not reimplemented here) -- Live H.264/AAC and Enhanced-RTMP **passthrough** ingest (HEVC/AV1 from FFmpeg/OBS) -- Late player join gets cached **legacy** H.264/AAC sequence headers + last keyframe only — not Enhanced-RTMP sequence starts -- Publisher `onMetaData` is parsed for stats; **not** forwarded to players -- No VOD commands (`pause` / `seek`), no E-RTMP v2 session negotiation, no nginx-rtmp feature parity (HLS, exec, push relay, recording) +- Live H.264/AAC and Enhanced-RTMP **passthrough** ingest (HEVC/AV1/Opus, multitrack when publishers send it) +- Late player join gets cached codec sequence headers (legacy + Enhanced-RTMP) and last keyframe; `onMetaData` is replayed to late joiners +- Legacy RTMP commands (`pause`, `seek`, `receiveAudio`/`receiveVideo`, `closeStream`) are handled in the protocol layer +- E-RTMP v2 connect capability negotiation and multitrack relay live in `librtmp2`; this server does not expose per-track IDs in the HTTP API yet +- No nginx-rtmp feature parity (HLS, exec, push relay, recording) Test your OBS/FFmpeg workflow before using this for critical streams. It is not a drop-in replacement for `nginx-rtmp`. diff --git a/tests/rtmp_http_e2e.rs b/tests/rtmp_http_e2e.rs index 41b31f3..6c5c67c 100644 --- a/tests/rtmp_http_e2e.rs +++ b/tests/rtmp_http_e2e.rs @@ -100,18 +100,11 @@ fn http_create_stream_then_rtmp_publish_and_play() { let frame = Frame { frame_type: FrameType::Video, timestamp: 0, - composition_time: 0, size: data.len() as u32, data: data.as_ptr(), - audio_codec: Default::default(), - audio_sample_rate: 0, - audio_channels: 0, - audio_bit_depth: 0, - audio_fourcc: Default::default(), video_codec: VideoCodec::H264, - video_fourcc: Default::default(), video_frame_type: 1, - is_metadata: 0, + ..Default::default() }; publisher.send_frame(&frame)?; From ab85c76c0e5572d2fa17d9d692425063235834f0 Mon Sep 17 00:00:00 2001 From: AlexanderWagnerDev Date: Tue, 14 Jul 2026 22:56:14 +0200 Subject: [PATCH 03/20] chore: track librtmp2 0.4.0 via git branch until crates.io Replace rev pin with branch feat/ertmp-v2-multitrack-0.4.0. Document registry-only dependency after librtmp2 merge and crates.io publish. Co-authored-by: Cursor --- AGENTS.md | 2 +- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- Cargo.toml | 4 +++- README.md | 8 ++++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 61585bb..c883d98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** the server pins `librtmp2 = "0.4.0"` (currently via git rev until the crate is on crates.io). It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. +- **Dependency source:** `librtmp2 = "0.4.0"` is resolved from git branch `feat/ertmp-v2-multitrack-0.4.0` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f82ba4..11d21b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ begin at `1.0.0`. ## [Unreleased] +### Changed +- Track `librtmp2` 0.4.0 from git branch `feat/ertmp-v2-multitrack-0.4.0` instead of + a fixed commit rev (follows ongoing librtmp2 PR work until release). + +> **After `librtmp2` 0.4.0 is merged and on crates.io:** in `Cargo.toml` remove the +> `git` / `branch` keys and depend on the registry only, e.g. +> `librtmp2 = { version = "0.4.0", features = ["tls"] }`, then run +> `cargo update -p librtmp2` and commit the updated `Cargo.lock`. + ## [0.1.5] — 2026-07-14 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 4794d43..a08cb75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" version = "0.4.0" -source = "git+https://github.com/OpenRTMP/librtmp2?rev=d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4#d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4" +source = "git+https://github.com/OpenRTMP/librtmp2?branch=feat%2Fertmp-v2-multitrack-0.4.0#aae745baad3caf1b22e3568cc0530ca6e7d46a23" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index a76483e..71539c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "d0649380ba053b49bf7f1b189fc1d4d1a2d18ce4", features = ["tls"] } +# Until librtmp2 0.4.0 is merged and published on crates.io, track the feature branch. +# After release, replace with: librtmp2 = { version = "0.4.0", features = ["tls"] } +librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", branch = "feat/ertmp-v2-multitrack-0.4.0", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } rusqlite = { version = "0.40", features = ["bundled"] } diff --git a/README.md b/README.md index ed86fba..44e2b05 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,14 @@ OBS / FFmpeg / App ### Compile +`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pulls it from the git branch `feat/ertmp-v2-multitrack-0.4.0` (see `Cargo.toml`). After merge and crates.io release, change the dependency to a registry-only line: + +```toml +librtmp2 = { version = "0.4.0", features = ["tls"] } +``` + +Then run `cargo update -p librtmp2` and commit the updated lockfile. + ```bash git clone https://github.com/OpenRTMP/librtmp2-server.git cd librtmp2-server From 042c91d98ceac57c4f2fa7e5507743b2dbbfe8df Mon Sep 17 00:00:00 2001 From: AlexanderWagnerDev Date: Tue, 14 Jul 2026 23:17:37 +0200 Subject: [PATCH 04/20] chore: refresh librtmp2 lock to latest 0.4.0 branch (617d3d2) Co-authored-by: Cursor --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a08cb75..25b6c5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" version = "0.4.0" -source = "git+https://github.com/OpenRTMP/librtmp2?branch=feat%2Fertmp-v2-multitrack-0.4.0#aae745baad3caf1b22e3568cc0530ca6e7d46a23" +source = "git+https://github.com/OpenRTMP/librtmp2?branch=feat%2Fertmp-v2-multitrack-0.4.0#617d3d253101616789d71afda8e6fe91e54844f9" dependencies = [ "cc", "libc", From 021226f568ed834027a9b853e5956b773e45f2c5 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 00:59:11 +0200 Subject: [PATCH 05/20] fix: pin librtmp2 to validated PR head --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 71539c5..6035b30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,9 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -# Until librtmp2 0.4.0 is merged and published on crates.io, track the feature branch. -# After release, replace with: librtmp2 = { version = "0.4.0", features = ["tls"] } -librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", branch = "feat/ertmp-v2-multitrack-0.4.0", features = ["tls"] } +# Until librtmp2 0.4.0 is published on crates.io, pin the exact validated +# commit from librtmp2 PR #128 so builds stay reproducible. +librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "ad021f97631cd2033abaee43e711bc406ca10c17", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } rusqlite = { version = "0.40", features = ["bundled"] } From abd57d7a77c23af17d4de05d2e7c3875b1daed2b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:59:30 +0000 Subject: [PATCH 06/20] Update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 25b6c5d..04b52f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" version = "0.4.0" -source = "git+https://github.com/OpenRTMP/librtmp2?branch=feat%2Fertmp-v2-multitrack-0.4.0#617d3d253101616789d71afda8e6fe91e54844f9" +source = "git+https://github.com/OpenRTMP/librtmp2?rev=ad021f97631cd2033abaee43e711bc406ca10c17#ad021f97631cd2033abaee43e711bc406ca10c17" dependencies = [ "cc", "libc", From cb703a88cbfdfc3a03ee5ee97736fd87b6f2f0ea Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 00:59:37 +0200 Subject: [PATCH 07/20] chore: stage librtmp2 dependency refresh --- scripts/apply_pr82_dependency_fix.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 scripts/apply_pr82_dependency_fix.py diff --git a/scripts/apply_pr82_dependency_fix.py b/scripts/apply_pr82_dependency_fix.py new file mode 100644 index 0000000..6385306 --- /dev/null +++ b/scripts/apply_pr82_dependency_fix.py @@ -0,0 +1,40 @@ +from pathlib import Path + +REV = "ad021f97631cd2033abaee43e711bc406ca10c17" +SHORT = REV[:7] + + +def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + count = text.count(old) + if count != 1: + raise RuntimeError(f"{path}: expected one match, found {count}: {old!r}") + file.write_text(text.replace(old, new, 1)) + + +replace_once( + "README.md", + "`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pulls it from the git branch `feat/ertmp-v2-multitrack-0.4.0` (see `Cargo.toml`). After merge and crates.io release, change the dependency to a registry-only line:", + f"`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the validated commit `{SHORT}` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:", +) + +replace_once( + "AGENTS.md", + "- **Dependency source:** `librtmp2 = \"0.4.0\"` is resolved from git branch `feat/ertmp-v2-multitrack-0.4.0` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = \"0.4.0\", features = [\"tls\"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.", + f"- **Dependency source:** `librtmp2 = \"0.4.0\"` is pinned to validated git commit `{SHORT}` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = {{ version = \"0.4.0\", features = [\"tls\"] }}`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.", +) + +replace_once( + "CHANGELOG.md", + "### Changed\n- Track `librtmp2` 0.4.0 from git branch `feat/ertmp-v2-multitrack-0.4.0` instead of\n a fixed commit rev (follows ongoing librtmp2 PR work until release).", + f"### Changed\n- Pin `librtmp2` 0.4.0 to validated commit `{SHORT}` from librtmp2 PR #128 instead of\n following the moving feature branch, keeping server builds reproducible until release.", +) + +replace_once( + "CHANGELOG.md", + " (`OpenRTMP/librtmp2` @ `d064938`); switch back to a crates.io version pin after\n release.", + f" (`OpenRTMP/librtmp2` @ `{SHORT}`); switch back to a crates.io version pin after\n release.", +) + +print(f"Updated documentation for librtmp2 {REV}") From 5d9081c0f33482dad3e87873ba6ee78912a99c51 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 00:59:47 +0200 Subject: [PATCH 08/20] ci: validate librtmp2 dependency refresh --- .../workflows/apply-pr82-dependency-fix.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/apply-pr82-dependency-fix.yml diff --git a/.github/workflows/apply-pr82-dependency-fix.yml b/.github/workflows/apply-pr82-dependency-fix.yml new file mode 100644 index 0000000..9d81576 --- /dev/null +++ b/.github/workflows/apply-pr82-dependency-fix.yml @@ -0,0 +1,62 @@ +name: Apply PR 82 dependency fix + +on: + push: + branches: + - chore/librtmp2-0.4.0 + paths: + - Cargo.toml + - scripts/apply_pr82_dependency_fix.py + - .github/workflows/apply-pr82-dependency-fix.yml + +permissions: + contents: write + +jobs: + apply-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out PR branch + uses: actions/checkout@v4 + with: + ref: chore/librtmp2-0.4.0 + fetch-depth: 0 + + - name: Install Rust 1.95 + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + components: rustfmt, clippy + + - name: Update documentation + run: python3 scripts/apply_pr82_dependency_fix.py + + - name: Regenerate lockfile + run: | + cargo update -p librtmp2 + grep -F 'ad021f97631cd2033abaee43e711bc406ca10c17' Cargo.lock + + - name: Format check + run: cargo fmt --all --check + + - name: Unit tests + run: cargo test + + - name: E2E tests + run: cargo test --features test-support --test rtmp_http_e2e -- --test-threads=1 + + - name: Compile benchmarks + run: cargo check --benches --features test-support + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Commit validated changes + run: | + rm scripts/apply_pr82_dependency_fix.py + rm .github/workflows/apply-pr82-dependency-fix.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "fix: pin server to validated librtmp2 revision" + git push origin HEAD:chore/librtmp2-0.4.0 From 9ad2b558a9c1bd5ed1497968a2f6eacdd7d242ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:01:11 +0000 Subject: [PATCH 09/20] fix: pin server to validated librtmp2 revision --- .../workflows/apply-pr82-dependency-fix.yml | 62 ------------------- AGENTS.md | 2 +- CHANGELOG.md | 6 +- README.md | 2 +- scripts/apply_pr82_dependency_fix.py | 40 ------------ 5 files changed, 5 insertions(+), 107 deletions(-) delete mode 100644 .github/workflows/apply-pr82-dependency-fix.yml delete mode 100644 scripts/apply_pr82_dependency_fix.py diff --git a/.github/workflows/apply-pr82-dependency-fix.yml b/.github/workflows/apply-pr82-dependency-fix.yml deleted file mode 100644 index 9d81576..0000000 --- a/.github/workflows/apply-pr82-dependency-fix.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Apply PR 82 dependency fix - -on: - push: - branches: - - chore/librtmp2-0.4.0 - paths: - - Cargo.toml - - scripts/apply_pr82_dependency_fix.py - - .github/workflows/apply-pr82-dependency-fix.yml - -permissions: - contents: write - -jobs: - apply-and-test: - runs-on: ubuntu-latest - steps: - - name: Check out PR branch - uses: actions/checkout@v4 - with: - ref: chore/librtmp2-0.4.0 - fetch-depth: 0 - - - name: Install Rust 1.95 - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.95.0 - components: rustfmt, clippy - - - name: Update documentation - run: python3 scripts/apply_pr82_dependency_fix.py - - - name: Regenerate lockfile - run: | - cargo update -p librtmp2 - grep -F 'ad021f97631cd2033abaee43e711bc406ca10c17' Cargo.lock - - - name: Format check - run: cargo fmt --all --check - - - name: Unit tests - run: cargo test - - - name: E2E tests - run: cargo test --features test-support --test rtmp_http_e2e -- --test-threads=1 - - - name: Compile benchmarks - run: cargo check --benches --features test-support - - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - - name: Commit validated changes - run: | - rm scripts/apply_pr82_dependency_fix.py - rm .github/workflows/apply-pr82-dependency-fix.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "fix: pin server to validated librtmp2 revision" - git push origin HEAD:chore/librtmp2-0.4.0 diff --git a/AGENTS.md b/AGENTS.md index c883d98..749f1c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** `librtmp2 = "0.4.0"` is resolved from git branch `feat/ertmp-v2-multitrack-0.4.0` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. +- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to validated git commit `ad021f9` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d21b9..66b8aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ begin at `1.0.0`. ## [Unreleased] ### Changed -- Track `librtmp2` 0.4.0 from git branch `feat/ertmp-v2-multitrack-0.4.0` instead of - a fixed commit rev (follows ongoing librtmp2 PR work until release). +- Pin `librtmp2` 0.4.0 to validated commit `ad021f9` from librtmp2 PR #128 instead of + following the moving feature branch, keeping server builds reproducible until release. > **After `librtmp2` 0.4.0 is merged and on crates.io:** in `Cargo.toml` remove the > `git` / `branch` keys and depend on the registry only, e.g. @@ -28,7 +28,7 @@ begin at `1.0.0`. - Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation, multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek). Until `0.4.0` is on crates.io the lockfile resolves it from git - (`OpenRTMP/librtmp2` @ `d064938`); switch back to a crates.io version pin after + (`OpenRTMP/librtmp2` @ `ad021f9`); switch back to a crates.io version pin after release. - Update README protocol notes to match inherited `librtmp2` 0.4.0 behaviour. - Adjust the RTMP HTTP E2E test `Frame` initializer for the appended `track_id` diff --git a/README.md b/README.md index 44e2b05..b2f7307 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ OBS / FFmpeg / App ### Compile -`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pulls it from the git branch `feat/ertmp-v2-multitrack-0.4.0` (see `Cargo.toml`). After merge and crates.io release, change the dependency to a registry-only line: +`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the validated commit `ad021f9` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line: ```toml librtmp2 = { version = "0.4.0", features = ["tls"] } diff --git a/scripts/apply_pr82_dependency_fix.py b/scripts/apply_pr82_dependency_fix.py deleted file mode 100644 index 6385306..0000000 --- a/scripts/apply_pr82_dependency_fix.py +++ /dev/null @@ -1,40 +0,0 @@ -from pathlib import Path - -REV = "ad021f97631cd2033abaee43e711bc406ca10c17" -SHORT = REV[:7] - - -def replace_once(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - count = text.count(old) - if count != 1: - raise RuntimeError(f"{path}: expected one match, found {count}: {old!r}") - file.write_text(text.replace(old, new, 1)) - - -replace_once( - "README.md", - "`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pulls it from the git branch `feat/ertmp-v2-multitrack-0.4.0` (see `Cargo.toml`). After merge and crates.io release, change the dependency to a registry-only line:", - f"`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the validated commit `{SHORT}` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:", -) - -replace_once( - "AGENTS.md", - "- **Dependency source:** `librtmp2 = \"0.4.0\"` is resolved from git branch `feat/ertmp-v2-multitrack-0.4.0` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = \"0.4.0\", features = [\"tls\"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.", - f"- **Dependency source:** `librtmp2 = \"0.4.0\"` is pinned to validated git commit `{SHORT}` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = {{ version = \"0.4.0\", features = [\"tls\"] }}`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.", -) - -replace_once( - "CHANGELOG.md", - "### Changed\n- Track `librtmp2` 0.4.0 from git branch `feat/ertmp-v2-multitrack-0.4.0` instead of\n a fixed commit rev (follows ongoing librtmp2 PR work until release).", - f"### Changed\n- Pin `librtmp2` 0.4.0 to validated commit `{SHORT}` from librtmp2 PR #128 instead of\n following the moving feature branch, keeping server builds reproducible until release.", -) - -replace_once( - "CHANGELOG.md", - " (`OpenRTMP/librtmp2` @ `d064938`); switch back to a crates.io version pin after\n release.", - f" (`OpenRTMP/librtmp2` @ `{SHORT}`); switch back to a crates.io version pin after\n release.", -) - -print(f"Updated documentation for librtmp2 {REV}") From ffc56211a931479a70c223c7c07f52dcb5bc852b Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 01:02:06 +0200 Subject: [PATCH 10/20] docs: clarify validated librtmp2 revision --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6035b30..8dc8a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -# Until librtmp2 0.4.0 is published on crates.io, pin the exact validated -# commit from librtmp2 PR #128 so builds stay reproducible. +# Until librtmp2 0.4.0 is published on crates.io, pin the exact PR #128 head +# that passed librtmp2's tests, E2E, ABI, interop, sanitizer, fuzz, and benchmark checks. librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "ad021f97631cd2033abaee43e711bc406ca10c17", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } From a76a527e94c94cd7e152ade7b9f5a8c0ce5a3939 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 02:21:46 +0200 Subject: [PATCH 11/20] docs: update 0.1.5 changelog date --- CHANGELOG.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b8aad..02af48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,16 +13,7 @@ begin at `1.0.0`. ## [Unreleased] -### Changed -- Pin `librtmp2` 0.4.0 to validated commit `ad021f9` from librtmp2 PR #128 instead of - following the moving feature branch, keeping server builds reproducible until release. - -> **After `librtmp2` 0.4.0 is merged and on crates.io:** in `Cargo.toml` remove the -> `git` / `branch` keys and depend on the registry only, e.g. -> `librtmp2 = { version = "0.4.0", features = ["tls"] }`, then run -> `cargo update -p librtmp2` and commit the updated `Cargo.lock`. - -## [0.1.5] — 2026-07-14 +## [0.1.5] — 2026-07-15 ### Changed - Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation, @@ -31,8 +22,8 @@ begin at `1.0.0`. (`OpenRTMP/librtmp2` @ `ad021f9`); switch back to a crates.io version pin after release. - Update README protocol notes to match inherited `librtmp2` 0.4.0 behaviour. -- Adjust the RTMP HTTP E2E test `Frame` initializer for the appended `track_id` - field in `librtmp2::types::Frame`. +- Update the RTMP HTTP E2E test `Frame` initializer for the expanded 0.4.0 + `librtmp2::types::Frame` shape, including the optional multitrack `track_id`. ## [0.1.4] — 2026-07-13 From ab216d37e2ab89781c795e85550fd7f116401e46 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 02:29:33 +0200 Subject: [PATCH 12/20] chore: add one-shot dependency refresh for PR 82 --- .github/workflows/update-pr82-librtmp2.yml | 163 +++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .github/workflows/update-pr82-librtmp2.yml diff --git a/.github/workflows/update-pr82-librtmp2.yml b/.github/workflows/update-pr82-librtmp2.yml new file mode 100644 index 0000000..51b1b3b --- /dev/null +++ b/.github/workflows/update-pr82-librtmp2.yml @@ -0,0 +1,163 @@ +name: Update PR 82 librtmp2 dependency + +on: + push: + branches: + - chore/librtmp2-0.4.0 + paths: + - .github/workflows/update-pr82-librtmp2.yml + +permissions: + contents: write + +jobs: + update-dependency: + runs-on: ubuntu-latest + steps: + - name: Check out PR branch + uses: actions/checkout@v4 + with: + ref: chore/librtmp2-0.4.0 + fetch-depth: 0 + + - name: Install project Rust toolchain + run: | + rustup toolchain install 1.95.0 --profile minimal + rustup override set 1.95.0 + + - name: Resolve dependency source and update files + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + latest_rev="$(gh api repos/OpenRTMP/librtmp2/pulls/128 --jq .head.sha)" + export LATEST_REV="$latest_rev" + + if python - <<'PY' + import json + import urllib.error + import urllib.request + + request = urllib.request.Request( + "https://crates.io/api/v1/crates/librtmp2/0.4.0", + headers={"User-Agent": "OpenRTMP/librtmp2-server dependency updater"}, + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + data = json.load(response) + except urllib.error.HTTPError as error: + if error.code == 404: + raise SystemExit(1) + raise + + version = data.get("version", {}).get("num") + raise SystemExit(0 if version == "0.4.0" else 1) + PY + then + export LIBRTMP2_PUBLISHED=1 + else + export LIBRTMP2_PUBLISHED=0 + fi + + python - <<'PY' + import os + import re + from pathlib import Path + + latest = os.environ["LATEST_REV"] + short = latest[:7] + published = os.environ["LIBRTMP2_PUBLISHED"] == "1" + + cargo = Path("Cargo.toml") + text = cargo.read_text() + pattern = re.compile( + r'# Until librtmp2 0\.4\.0 is published on crates\.io, pin the exact PR #128 head\n' + r'# that passed librtmp2\'s tests, E2E, ABI, interop, sanitizer, fuzz, and benchmark checks\.\n' + r'librtmp2 = \{ version = "0\.4\.0", git = "https://github\.com/OpenRTMP/librtmp2", rev = "[0-9a-f]{40}", features = \["tls"\] \}' + ) + if published: + replacement = 'librtmp2 = { version = "0.4.0", features = ["tls"] }' + else: + replacement = ( + '# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head.\n' + f'librtmp2 = {{ version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "{latest}", features = ["tls"] }}' + ) + text, count = pattern.subn(replacement, text) + if count != 1: + raise SystemExit("Cargo.toml dependency block did not match exactly once") + cargo.write_text(text) + + agents = Path("AGENTS.md") + text = agents.read_text() + line_pattern = re.compile(r'- \*\*Dependency source:\*\*.*\n') + if published: + line = '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n' + else: + line = f'- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `{short}` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = {{ version = "0.4.0", features = ["tls"] }}`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n' + text, count = line_pattern.subn(line, text) + if count != 1: + raise SystemExit("AGENTS.md dependency line did not match exactly once") + agents.write_text(text) + + changelog = Path("CHANGELOG.md") + text = changelog.read_text() + block_pattern = re.compile( + r' Until `0\.4\.0` is on crates\.io the lockfile resolves it from git\n' + r' \(`OpenRTMP/librtmp2` @ `[0-9a-f]{7}`\); switch back to a crates\.io version pin after\n' + r' release\.' + ) + if published: + block = ' The dependency now resolves from the published crates.io release.' + else: + block = ( + ' Until `0.4.0` is on crates.io the lockfile resolves it from git\n' + f' (`OpenRTMP/librtmp2` @ `{short}`); switch back to a crates.io version pin after\n' + ' release.' + ) + text, count = block_pattern.subn(block, text) + if count != 1: + raise SystemExit("CHANGELOG.md dependency block did not match exactly once") + changelog.write_text(text) + + readme = Path("README.md") + text = readme.read_text() + build_pattern = re.compile( + r'`librtmp2` 0\.4\.0 is not on crates\.io yet\. Until it is published, this repo pins the validated commit `[0-9a-f]{7}` from librtmp2 PR #128 \(see `Cargo\.toml`\) so builds remain reproducible\. After merge and crates\.io release, change the dependency to a registry-only line:\n\n' + r'```toml\n' + r'librtmp2 = \{ version = "0\.4\.0", features = \["tls"\] \}\n' + r'```\n\n' + r'Then run `cargo update -p librtmp2` and commit the updated lockfile\.' + ) + if published: + build = '`librtmp2` 0.4.0 is resolved from crates.io with the `tls` feature.' + else: + build = ( + f'`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `{short}` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:\n\n' + '```toml\n' + 'librtmp2 = { version = "0.4.0", features = ["tls"] }\n' + '```\n\n' + 'Then run `cargo update -p librtmp2` and commit the updated lockfile.' + ) + text, count = build_pattern.subn(build, text) + if count != 1: + raise SystemExit("README.md dependency block did not match exactly once") + readme.write_text(text) + PY + + cargo update -p librtmp2 + cargo check --locked + + - name: Commit dependency update and remove helper workflow + run: | + set -euo pipefail + rm .github/workflows/update-pr82-librtmp2.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Cargo.toml Cargo.lock AGENTS.md CHANGELOG.md README.md .github/workflows/update-pr82-librtmp2.yml + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git commit -m "fix: refresh librtmp2 dependency source" + git push origin HEAD:chore/librtmp2-0.4.0 From bb6180de9dfbd98987f0c88754476c610a9b1b39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:29:51 +0000 Subject: [PATCH 13/20] Update Cargo.lock --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04b52f0..4d76d8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1608,9 +1608,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", From 9c8e69b599bb4036ed603ccf309c00e4bd921d03 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 02:31:41 +0200 Subject: [PATCH 14/20] fix: simplify PR 82 dependency refresh --- .github/workflows/update-pr82-librtmp2.yml | 156 +++++++++------------ 1 file changed, 66 insertions(+), 90 deletions(-) diff --git a/.github/workflows/update-pr82-librtmp2.yml b/.github/workflows/update-pr82-librtmp2.yml index 51b1b3b..fe017fa 100644 --- a/.github/workflows/update-pr82-librtmp2.yml +++ b/.github/workflows/update-pr82-librtmp2.yml @@ -14,50 +14,40 @@ jobs: update-dependency: runs-on: ubuntu-latest steps: - - name: Check out PR branch - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: ref: chore/librtmp2-0.4.0 fetch-depth: 0 - - name: Install project Rust toolchain - run: | - rustup toolchain install 1.95.0 --profile minimal - rustup override set 1.95.0 - - - name: Resolve dependency source and update files + - name: Refresh librtmp2 source env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - - latest_rev="$(gh api repos/OpenRTMP/librtmp2/pulls/128 --jq .head.sha)" - export LATEST_REV="$latest_rev" + export LATEST_REV="$(gh api repos/OpenRTMP/librtmp2/pulls/128 --jq .head.sha)" if python - <<'PY' import json import urllib.error import urllib.request - request = urllib.request.Request( + req = urllib.request.Request( "https://crates.io/api/v1/crates/librtmp2/0.4.0", - headers={"User-Agent": "OpenRTMP/librtmp2-server dependency updater"}, + headers={"User-Agent": "OpenRTMP/librtmp2-server"}, ) try: - with urllib.request.urlopen(request, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: data = json.load(response) except urllib.error.HTTPError as error: if error.code == 404: raise SystemExit(1) raise - - version = data.get("version", {}).get("num") - raise SystemExit(0 if version == "0.4.0" else 1) + raise SystemExit(0 if data.get("version", {}).get("num") == "0.4.0" else 1) PY then - export LIBRTMP2_PUBLISHED=1 + export PUBLISHED=1 else - export LIBRTMP2_PUBLISHED=0 + export PUBLISHED=0 fi python - <<'PY' @@ -67,97 +57,83 @@ jobs: latest = os.environ["LATEST_REV"] short = latest[:7] - published = os.environ["LIBRTMP2_PUBLISHED"] == "1" + published = os.environ["PUBLISHED"] == "1" cargo = Path("Cargo.toml") - text = cargo.read_text() - pattern = re.compile( - r'# Until librtmp2 0\.4\.0 is published on crates\.io, pin the exact PR #128 head\n' - r'# that passed librtmp2\'s tests, E2E, ABI, interop, sanitizer, fuzz, and benchmark checks\.\n' + cargo_text = cargo.read_text() + dep_re = re.compile( + r'(?:# Until librtmp2 0\.4\.0 is published on crates\.io, pin[^\n]*\n' + r'(?:# [^\n]*\n)?)?' r'librtmp2 = \{ version = "0\.4\.0", git = "https://github\.com/OpenRTMP/librtmp2", rev = "[0-9a-f]{40}", features = \["tls"\] \}' ) if published: - replacement = 'librtmp2 = { version = "0.4.0", features = ["tls"] }' + dep = 'librtmp2 = { version = "0.4.0", features = ["tls"] }' else: - replacement = ( + dep = ( '# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head.\n' f'librtmp2 = {{ version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "{latest}", features = ["tls"] }}' ) - text, count = pattern.subn(replacement, text) - if count != 1: - raise SystemExit("Cargo.toml dependency block did not match exactly once") - cargo.write_text(text) - - agents = Path("AGENTS.md") - text = agents.read_text() - line_pattern = re.compile(r'- \*\*Dependency source:\*\*.*\n') - if published: - line = '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n' - else: - line = f'- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `{short}` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = {{ version = "0.4.0", features = ["tls"] }}`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n' - text, count = line_pattern.subn(line, text) - if count != 1: - raise SystemExit("AGENTS.md dependency line did not match exactly once") - agents.write_text(text) - - changelog = Path("CHANGELOG.md") - text = changelog.read_text() - block_pattern = re.compile( - r' Until `0\.4\.0` is on crates\.io the lockfile resolves it from git\n' - r' \(`OpenRTMP/librtmp2` @ `[0-9a-f]{7}`\); switch back to a crates\.io version pin after\n' - r' release\.' - ) - if published: - block = ' The dependency now resolves from the published crates.io release.' - else: - block = ( - ' Until `0.4.0` is on crates.io the lockfile resolves it from git\n' - f' (`OpenRTMP/librtmp2` @ `{short}`); switch back to a crates.io version pin after\n' - ' release.' - ) - text, count = block_pattern.subn(block, text) - if count != 1: - raise SystemExit("CHANGELOG.md dependency block did not match exactly once") - changelog.write_text(text) + cargo_text, n = dep_re.subn(dep, cargo_text, count=1) + if n != 1: + raise SystemExit("Cargo.toml dependency entry was not updated") + cargo.write_text(cargo_text) - readme = Path("README.md") - text = readme.read_text() - build_pattern = re.compile( - r'`librtmp2` 0\.4\.0 is not on crates\.io yet\. Until it is published, this repo pins the validated commit `[0-9a-f]{7}` from librtmp2 PR #128 \(see `Cargo\.toml`\) so builds remain reproducible\. After merge and crates\.io release, change the dependency to a registry-only line:\n\n' - r'```toml\n' - r'librtmp2 = \{ version = "0\.4\.0", features = \["tls"\] \}\n' - r'```\n\n' - r'Then run `cargo update -p librtmp2` and commit the updated lockfile\.' - ) if published: - build = '`librtmp2` 0.4.0 is resolved from crates.io with the `tls` feature.' + replacements = { + "AGENTS.md": ( + re.compile(r'- \*\*Dependency source:\*\*.*\n'), + '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n', + ), + "CHANGELOG.md": ( + re.compile(r' Until `0\.4\.0` is on crates\.io the lockfile resolves it from git\n \(`OpenRTMP/librtmp2` @ `[0-9a-f]{7}`\); switch back to a crates\.io version pin after\n release\.'), + ' The dependency now resolves from the published crates.io release.', + ), + "README.md": ( + re.compile(r'`librtmp2` 0\.4\.0 is not on crates\.io yet\..*?Then run `cargo update -p librtmp2` and commit the updated lockfile\.', re.S), + '`librtmp2` 0.4.0 is resolved from crates.io with the `tls` feature.', + ), + } + for name, (pattern, replacement) in replacements.items(): + path = Path(name) + text, n = pattern.subn(replacement, path.read_text(), count=1) + if n != 1: + raise SystemExit(f"{name} dependency documentation was not updated") + path.write_text(text) else: - build = ( - f'`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `{short}` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:\n\n' - '```toml\n' - 'librtmp2 = { version = "0.4.0", features = ["tls"] }\n' - '```\n\n' - 'Then run `cargo update -p librtmp2` and commit the updated lockfile.' + for name in ("AGENTS.md", "CHANGELOG.md", "README.md"): + path = Path(name) + text = path.read_text() + text = re.sub(r'`[0-9a-f]{7}`(?=[^\n]*(?:PR #128|crates\.io|lockfile|commit))', f'`{short}`', text) + text = text.replace("validated commit", "current commit") + text = text.replace("pinned to validated git commit", "pinned to the current PR #128 head") + path.write_text(text) + + lock = Path("Cargo.lock") + lock_text = lock.read_text() + lock_text, n = re.subn( + r'(git\+https://github\.com/OpenRTMP/librtmp2\?rev=)[0-9a-f]{40}(#[0-9a-f]{40})', + rf'\g<1>{latest}#{latest}', + lock_text, + count=1, ) - text, count = build_pattern.subn(build, text) - if count != 1: - raise SystemExit("README.md dependency block did not match exactly once") - readme.write_text(text) + if n != 1: + raise SystemExit("Cargo.lock librtmp2 source was not updated") + lock.write_text(lock_text) PY - cargo update -p librtmp2 - cargo check --locked + if [ "$PUBLISHED" = 1 ]; then + rustup update stable + cargo update -p librtmp2 + fi + + grep -F "$LATEST_REV" Cargo.toml Cargo.lock || [ "$PUBLISHED" = 1 ] - - name: Commit dependency update and remove helper workflow + - name: Commit result and remove helper run: | set -euo pipefail rm .github/workflows/update-pr82-librtmp2.yml git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add Cargo.toml Cargo.lock AGENTS.md CHANGELOG.md README.md .github/workflows/update-pr82-librtmp2.yml - if git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - git commit -m "fix: refresh librtmp2 dependency source" + git commit -m "fix: refresh librtmp2 dependency pin" git push origin HEAD:chore/librtmp2-0.4.0 From 43b84cd1f3f783092794b0e113591a6c16f11e39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:31:50 +0000 Subject: [PATCH 15/20] fix: refresh librtmp2 dependency pin --- .github/workflows/update-pr82-librtmp2.yml | 139 --------------------- AGENTS.md | 2 +- CHANGELOG.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 5 +- README.md | 2 +- 6 files changed, 6 insertions(+), 146 deletions(-) delete mode 100644 .github/workflows/update-pr82-librtmp2.yml diff --git a/.github/workflows/update-pr82-librtmp2.yml b/.github/workflows/update-pr82-librtmp2.yml deleted file mode 100644 index fe017fa..0000000 --- a/.github/workflows/update-pr82-librtmp2.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Update PR 82 librtmp2 dependency - -on: - push: - branches: - - chore/librtmp2-0.4.0 - paths: - - .github/workflows/update-pr82-librtmp2.yml - -permissions: - contents: write - -jobs: - update-dependency: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: chore/librtmp2-0.4.0 - fetch-depth: 0 - - - name: Refresh librtmp2 source - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - export LATEST_REV="$(gh api repos/OpenRTMP/librtmp2/pulls/128 --jq .head.sha)" - - if python - <<'PY' - import json - import urllib.error - import urllib.request - - req = urllib.request.Request( - "https://crates.io/api/v1/crates/librtmp2/0.4.0", - headers={"User-Agent": "OpenRTMP/librtmp2-server"}, - ) - try: - with urllib.request.urlopen(req, timeout=30) as response: - data = json.load(response) - except urllib.error.HTTPError as error: - if error.code == 404: - raise SystemExit(1) - raise - raise SystemExit(0 if data.get("version", {}).get("num") == "0.4.0" else 1) - PY - then - export PUBLISHED=1 - else - export PUBLISHED=0 - fi - - python - <<'PY' - import os - import re - from pathlib import Path - - latest = os.environ["LATEST_REV"] - short = latest[:7] - published = os.environ["PUBLISHED"] == "1" - - cargo = Path("Cargo.toml") - cargo_text = cargo.read_text() - dep_re = re.compile( - r'(?:# Until librtmp2 0\.4\.0 is published on crates\.io, pin[^\n]*\n' - r'(?:# [^\n]*\n)?)?' - r'librtmp2 = \{ version = "0\.4\.0", git = "https://github\.com/OpenRTMP/librtmp2", rev = "[0-9a-f]{40}", features = \["tls"\] \}' - ) - if published: - dep = 'librtmp2 = { version = "0.4.0", features = ["tls"] }' - else: - dep = ( - '# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head.\n' - f'librtmp2 = {{ version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "{latest}", features = ["tls"] }}' - ) - cargo_text, n = dep_re.subn(dep, cargo_text, count=1) - if n != 1: - raise SystemExit("Cargo.toml dependency entry was not updated") - cargo.write_text(cargo_text) - - if published: - replacements = { - "AGENTS.md": ( - re.compile(r'- \*\*Dependency source:\*\*.*\n'), - '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.\n', - ), - "CHANGELOG.md": ( - re.compile(r' Until `0\.4\.0` is on crates\.io the lockfile resolves it from git\n \(`OpenRTMP/librtmp2` @ `[0-9a-f]{7}`\); switch back to a crates\.io version pin after\n release\.'), - ' The dependency now resolves from the published crates.io release.', - ), - "README.md": ( - re.compile(r'`librtmp2` 0\.4\.0 is not on crates\.io yet\..*?Then run `cargo update -p librtmp2` and commit the updated lockfile\.', re.S), - '`librtmp2` 0.4.0 is resolved from crates.io with the `tls` feature.', - ), - } - for name, (pattern, replacement) in replacements.items(): - path = Path(name) - text, n = pattern.subn(replacement, path.read_text(), count=1) - if n != 1: - raise SystemExit(f"{name} dependency documentation was not updated") - path.write_text(text) - else: - for name in ("AGENTS.md", "CHANGELOG.md", "README.md"): - path = Path(name) - text = path.read_text() - text = re.sub(r'`[0-9a-f]{7}`(?=[^\n]*(?:PR #128|crates\.io|lockfile|commit))', f'`{short}`', text) - text = text.replace("validated commit", "current commit") - text = text.replace("pinned to validated git commit", "pinned to the current PR #128 head") - path.write_text(text) - - lock = Path("Cargo.lock") - lock_text = lock.read_text() - lock_text, n = re.subn( - r'(git\+https://github\.com/OpenRTMP/librtmp2\?rev=)[0-9a-f]{40}(#[0-9a-f]{40})', - rf'\g<1>{latest}#{latest}', - lock_text, - count=1, - ) - if n != 1: - raise SystemExit("Cargo.lock librtmp2 source was not updated") - lock.write_text(lock_text) - PY - - if [ "$PUBLISHED" = 1 ]; then - rustup update stable - cargo update -p librtmp2 - fi - - grep -F "$LATEST_REV" Cargo.toml Cargo.lock || [ "$PUBLISHED" = 1 ] - - - name: Commit result and remove helper - run: | - set -euo pipefail - rm .github/workflows/update-pr82-librtmp2.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.toml Cargo.lock AGENTS.md CHANGELOG.md README.md .github/workflows/update-pr82-librtmp2.yml - git commit -m "fix: refresh librtmp2 dependency pin" - git push origin HEAD:chore/librtmp2-0.4.0 diff --git a/AGENTS.md b/AGENTS.md index 749f1c8..9826604 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to validated git commit `ad021f9` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. +- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `fea7e2e` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 02af48b..1b8287c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ begin at `1.0.0`. - Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation, multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek). Until `0.4.0` is on crates.io the lockfile resolves it from git - (`OpenRTMP/librtmp2` @ `ad021f9`); switch back to a crates.io version pin after + (`OpenRTMP/librtmp2` @ `fea7e2e`); switch back to a crates.io version pin after release. - Update README protocol notes to match inherited `librtmp2` 0.4.0 behaviour. - Update the RTMP HTTP E2E test `Frame` initializer for the expanded 0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 4d76d8b..7ede564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" version = "0.4.0" -source = "git+https://github.com/OpenRTMP/librtmp2?rev=ad021f97631cd2033abaee43e711bc406ca10c17#ad021f97631cd2033abaee43e711bc406ca10c17" +source = "git+https://github.com/OpenRTMP/librtmp2?rev=fea7e2e9d472e651b14be23c9feb63ca47bb3a5b#fea7e2e9d472e651b14be23c9feb63ca47bb3a5b" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 8dc8a52..45f9cd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,8 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -# Until librtmp2 0.4.0 is published on crates.io, pin the exact PR #128 head -# that passed librtmp2's tests, E2E, ABI, interop, sanitizer, fuzz, and benchmark checks. -librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "ad021f97631cd2033abaee43e711bc406ca10c17", features = ["tls"] } +# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head. +librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "fea7e2e9d472e651b14be23c9feb63ca47bb3a5b", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } rusqlite = { version = "0.40", features = ["bundled"] } diff --git a/README.md b/README.md index b2f7307..ab0b9de 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ OBS / FFmpeg / App ### Compile -`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the validated commit `ad021f9` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line: +`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `fea7e2e` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line: ```toml librtmp2 = { version = "0.4.0", features = ["tls"] } From 69144050c5514b31fa1779179797828e5c10efc7 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 02:32:58 +0200 Subject: [PATCH 16/20] chore: clarify reproducible librtmp2 pin --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 45f9cd3..e1eda6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head. +# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head for reproducible builds. librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "fea7e2e9d472e651b14be23c9feb63ca47bb3a5b", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } From 7ce4f51fa5437791cfd7b23907ff96159f070dce Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 15:36:38 +0200 Subject: [PATCH 17/20] fix: reject legacy short access keys --- src/db.rs | 78 +++++++++++++++++++++--------- src/http.rs | 16 +----- src/keygen.rs | 38 +++++++++++++++ src/rtmp_bridge.rs | 118 ++++++++++++++++++++++++++------------------- 4 files changed, 163 insertions(+), 87 deletions(-) diff --git a/src/db.rs b/src/db.rs index ff5abf2..ea9dd9c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -452,6 +452,9 @@ impl Db { crate::log_error!("stream_find_by: rejected disallowed column '{column}'"); return DbLookup::Failed; } + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -472,6 +475,9 @@ impl Db { /// belongs to a disabled/pending-delete stream, so the RTMP auth-failure /// rate limiter only counts the former as a credential mismatch. pub fn stream_find_by_publish_key_any(&self, key: &str) -> DbLookup { + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -655,6 +661,9 @@ impl Db { } pub fn viewer_find_by_play_key(&self, key: &str) -> DbLookup { + if !crate::keygen::is_valid_access_key(key) { + return DbLookup::Missing; + } let conn = self.conn.lock(); map_optional(conn.query_row( &format!( @@ -1125,9 +1134,9 @@ mod tests { id: id.to_string(), name: format!("{id} name"), app: "live".to_string(), - publish_key: pub_key.to_string(), - play_key: play_key.to_string(), - stats_key: stats_key.to_string(), + publish_key: crate::keygen::test_pad_access_key(pub_key), + play_key: crate::keygen::test_pad_access_key(play_key), + stats_key: crate::keygen::test_pad_access_key(stats_key), enabled: true, created_at: now_ts(), } @@ -1177,18 +1186,18 @@ mod tests { assert_eq!(got.name, "stream1 name"); assert_eq!( - match db.stream_find_by_publish_key("pub_key_123") { + match db.stream_find_by_publish_key(&s.publish_key) { DbLookup::Ok(s) => s.id, _ => panic!("publish key lookup failed"), }, "stream1" ); assert!(matches!( - db.viewer_find_by_play_key("pl_key_456"), + db.viewer_find_by_play_key(&s.play_key), DbLookup::Ok(_) )); assert!(matches!( - db.stream_find_by_stats_key("st_key_789"), + db.stream_find_by_stats_key(&s.stats_key), DbLookup::Ok(_) )); assert!(matches!( @@ -1202,13 +1211,8 @@ mod tests { #[test] fn publishers_players_and_stats() { let db = Db::open(":memory:").unwrap(); - db.stream_add(&sample_stream( - "stream1", - "pub_key_123", - "pl_key_456", - "st_key_789", - )) - .unwrap(); + let s = sample_stream("stream1", "pub_key_123", "pl_key_456", "st_key_789"); + db.stream_add(&s).unwrap(); let mut p = Publisher { id: "pub1".to_string(), @@ -1231,7 +1235,7 @@ mod tests { assert!(db.publisher_try_acquire(&p)); assert_eq!(db.publisher_list(Some("stream1")).len(), 1); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_key_456") else { + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; let player = Player { @@ -1380,7 +1384,10 @@ mod tests { }); // Simulate on_close for pub1: find by publish_key -> stream_id -> list. - let DbLookup::Ok(found) = db.stream_find_by_publish_key("pub_key_1") else { + let DbLookup::Ok(stream1) = db.stream_get("stream1") else { + panic!("stream not found"); + }; + let DbLookup::Ok(found) = db.stream_find_by_publish_key(&stream1.publish_key) else { panic!("publish key not found"); }; let mut pubs = db.publisher_list(Some(&found.id)); @@ -1401,14 +1408,9 @@ mod tests { #[test] fn player_try_acquire_enforces_per_key_connection_cap() { let db = Db::open(":memory:").unwrap(); - db.stream_add(&sample_stream( - "stream1", - "pub_key_123", - "pl_key_456", - "st_key_789", - )) - .unwrap(); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_key_456") else { + let s = sample_stream("stream1", "pub_key_123", "pl_key_456", "st_key_789"); + db.stream_add(&s).unwrap(); + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; @@ -1434,4 +1436,34 @@ mod tests { }; assert!(!db.player_try_acquire(&overflow)); } + + #[test] + fn legacy_short_access_keys_are_rejected_at_lookup() { + let db = Db::open(":memory:").unwrap(); + let s = Stream { + id: "legacy".to_string(), + name: "Legacy".to_string(), + app: "live".to_string(), + publish_key: "a".to_string(), + play_key: "b".to_string(), + stats_key: "c".to_string(), + enabled: true, + created_at: now_ts(), + }; + assert!(db.stream_add(&s).is_ok()); + + assert!(matches!( + db.stream_find_by_publish_key("a"), + DbLookup::Missing + )); + assert!(matches!( + db.stream_find_by_publish_key_any("a"), + DbLookup::Missing + )); + assert!(matches!(db.viewer_find_by_play_key("b"), DbLookup::Missing)); + assert!(matches!( + db.stream_find_by_stats_key("c"), + DbLookup::Missing + )); + } } diff --git a/src/http.rs b/src/http.rs index 1a59b8b..abcffcd 100644 --- a/src/http.rs +++ b/src/http.rs @@ -206,21 +206,9 @@ fn is_valid_stream_key_part(value: &str) -> bool { && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) } -/// Minimum length for operator-supplied publish/play/stats keys. Shorter custom -/// keys are trivially brute-forced over the unrate-limited RTMP auth path. -const MIN_ACCESS_KEY_LEN: usize = 32; - /// Publish/play/stats keys: safe ASCII, no slashes, minimum entropy via length. fn is_valid_access_key(value: &str) -> bool { - if value.len() < MIN_ACCESS_KEY_LEN || value.len() > 63 { - return false; - } - let mut chars = value.chars(); - let Some(first) = chars.next() else { - return false; - }; - first.is_ascii_alphanumeric() - && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) + crate::keygen::is_valid_access_key(value) } fn trim_optional_string(value: Option) -> Option { @@ -249,7 +237,7 @@ fn resolve_or_generate_access_key( } } -const ACCESS_KEY_VALIDATION_MSG: &str = "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; +const ACCESS_KEY_VALIDATION_MSG: &str = crate::keygen::ACCESS_KEY_VALIDATION_MSG; fn access_keys_must_be_unique(keys: &[&str]) -> bool { let mut seen = HashSet::with_capacity(keys.len()); diff --git a/src/keygen.rs b/src/keygen.rs index ef87a32..e871cd9 100644 --- a/src/keygen.rs +++ b/src/keygen.rs @@ -18,6 +18,37 @@ pub const PREFIX_STATS_KEY: &str = "sts_"; /// Prefix for configured viewer-slot row ids (panel-managed play access). pub const PREFIX_VIEWER_ID: &str = "vi_"; +/// Minimum length for publish/play/stats keys used at runtime. Shorter keys stored +/// in legacy databases are rejected on RTMP and public stats paths. +pub const MIN_ACCESS_KEY_LEN: usize = 32; + +/// Publish/play/stats keys: safe ASCII, no slashes, minimum entropy via length. +pub fn is_valid_access_key(value: &str) -> bool { + if value.len() < MIN_ACCESS_KEY_LEN || value.len() > 63 { + return false; + } + let mut chars = value.chars(); + let Some(first) = chars.next() else { + return false; + }; + first.is_ascii_alphanumeric() + && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) +} + +pub const ACCESS_KEY_VALIDATION_MSG: &str = "Key must be 32-63 characters, start with a letter or number, and use only letters, numbers, dots, underscores, or hyphens"; + +#[cfg(test)] +pub fn test_pad_access_key(value: &str) -> String { + if is_valid_access_key(value) { + return value.to_string(); + } + let mut padded = value.to_string(); + while padded.len() < MIN_ACCESS_KEY_LEN { + padded.push('x'); + } + padded.chars().take(63).collect() +} + fn keygen_with_entropy(prefix: &str, entropy_bytes: usize) -> Result { let mut rnd = vec![0u8; entropy_bytes]; SysRng @@ -65,4 +96,11 @@ mod tests { assert_eq!(token.len(), API_TOKEN_ENTROPY_BYTES * 2); assert!(token.chars().all(|c| c.is_ascii_hexdigit())); } + + #[test] + fn access_key_validation_enforces_minimum_length() { + assert!(is_valid_access_key("live.main_1_with_sufficient_length_ok")); + assert!(!is_valid_access_key("too_short")); + assert!(!is_valid_access_key("a")); + } } diff --git a/src/rtmp_bridge.rs b/src/rtmp_bridge.rs index 76a7925..c4c409b 100644 --- a/src/rtmp_bridge.rs +++ b/src/rtmp_bridge.rs @@ -894,17 +894,23 @@ mod tests { id: id.to_string(), name: format!("{id} name"), app: "live".to_string(), - publish_key: pub_key.to_string(), - play_key: play_key.to_string(), - stats_key: format!("st_{id}"), + publish_key: crate::keygen::test_pad_access_key(pub_key), + play_key: crate::keygen::test_pad_access_key(play_key), + stats_key: crate::keygen::test_pad_access_key(&format!("stats_{id}")), enabled: true, created_at: crate::db::now_ts(), } } - fn add_stream_with_player(db: &Db, id: &str, pub_key: &str, play_key: &str) { - db.stream_add(&sample_stream(id, pub_key, play_key)) - .unwrap(); + fn add_stream_with_player( + db: &Db, + id: &str, + pub_key: &str, + play_key: &str, + ) -> crate::db::Stream { + let s = sample_stream(id, pub_key, play_key); + db.stream_add(&s).unwrap(); + s } #[test] @@ -1057,23 +1063,23 @@ mod tests { #[test] fn publish_and_play_reject_stream_marked_for_deletion() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let deleted = Arc::new(Mutex::new(HashSet::new())); deleted.lock().insert("s1".to_string()); let bridge = DbRtmpBridge::new(db, deleted); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_err()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_err()); bridge.on_close(1); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_play(2, "live", "pl_k").is_err()); + assert!(bridge.authorize_play(2, "live", &s.play_key).is_err()); } #[test] fn publish_with_valid_key_for_disabled_stream_does_not_burn_auth_budget() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); assert!(db.stream_disable("s1").unwrap()); let bridge = test_bridge(db); let ip = "203.0.113.8:1935"; @@ -1084,7 +1090,11 @@ mod tests { // a brute-force guess would. for conn in 0..(RTMP_AUTH_MAX_FAILURES as u64 + 2) { bridge.on_connect(conn, ip); - assert!(bridge.authorize_publish(conn, "live", "pub_k").is_err()); + assert!( + bridge + .authorize_publish(conn, "live", &s.publish_key) + .is_err() + ); bridge.on_close(conn); } @@ -1102,33 +1112,37 @@ mod tests { #[test] fn publish_rejects_key_for_wrong_app() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "other", "pub_k").is_err()); + assert!( + bridge + .authorize_publish(1, "other", &s.publish_key) + .is_err() + ); assert_eq!(db.publisher_list(Some("s1")).len(), 0); } #[test] fn play_rejects_key_for_wrong_app() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "other", "pl_k").is_err()); + assert!(bridge.authorize_play(1, "other", &s.play_key).is_err()); assert_eq!(db.player_list(Some("s1")).len(), 0); } #[test] fn publish_then_close_deactivates_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); assert_eq!(db.publisher_list(Some("s1")).len(), 1); bridge.on_close(1); @@ -1138,14 +1152,14 @@ mod tests { #[test] fn close_only_affects_its_own_connection() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(2, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(2, "live", &s2.publish_key).is_ok()); bridge.on_close(1); assert_eq!(db.publisher_list(Some("s1")).len(), 0); @@ -1155,24 +1169,24 @@ mod tests { #[test] fn authorize_publish_rejects_second_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(2, "live", "pub_k").is_err()); + assert!(bridge.authorize_publish(2, "live", &s.publish_key).is_err()); } #[test] fn on_connect_preserves_prior_authorize_publish_state() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); // publish callback can run during poll() before the poll-loop on_connect. - assert!(bridge.authorize_publish(1, "live", "pub_k").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s.publish_key).is_ok()); bridge.on_connect(1, "127.0.0.1:1000"); assert!(bridge.has_publisher(1)); @@ -1192,13 +1206,13 @@ mod tests { #[test] fn authorize_publish_switching_streams_deactivates_prior_publisher() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(1, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(1, "live", &s2.publish_key).is_ok()); assert_eq!(db.publisher_list(Some("s1")).len(), 0); assert_eq!(db.publisher_list(Some("s2")).len(), 1); @@ -1207,16 +1221,20 @@ mod tests { #[test] fn authorize_publish_failed_switch_keeps_prior_publisher_active() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); bridge.on_connect(2, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_publish(2, "live", "pub_k2").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_publish(2, "live", &s2.publish_key).is_ok()); - assert!(bridge.authorize_publish(1, "live", "pub_k2").is_err()); + assert!( + bridge + .authorize_publish(1, "live", &s2.publish_key) + .is_err() + ); assert!(bridge.has_publisher(1)); assert_eq!(db.publisher_list(Some("s1")).len(), 1); @@ -1226,13 +1244,13 @@ mod tests { #[test] fn on_play_switching_streams_deactivates_prior_player() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "live", "pl_k1").is_ok()); - assert!(bridge.authorize_play(1, "live", "pl_k2").is_ok()); + assert!(bridge.authorize_play(1, "live", &s1.play_key).is_ok()); + assert!(bridge.authorize_play(1, "live", &s2.play_key).is_ok()); assert_eq!(db.player_list(Some("s1")).len(), 0); assert_eq!(db.player_list(Some("s2")).len(), 1); @@ -1244,17 +1262,17 @@ mod tests { #[test] fn player_replacement_stats_reset_is_not_consumed_by_publisher_stats() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); - add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); + let s1 = add_stream_with_player(&db, "s1", "pub_k1", "pl_k1"); + let s2 = add_stream_with_player(&db, "s2", "pub_k2", "pl_k2"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_publish(1, "live", "pub_k1").is_ok()); - assert!(bridge.authorize_play(1, "live", "pl_k1").is_ok()); + assert!(bridge.authorize_publish(1, "live", &s1.publish_key).is_ok()); + assert!(bridge.authorize_play(1, "live", &s1.play_key).is_ok()); bridge.update_publisher_stats(1, 1_000, "avc1", "mp4a", PublisherStreamMetadata::default()); bridge.update_player_stats(1, 2_000); - assert!(bridge.authorize_play(1, "live", "pl_k2").is_ok()); + assert!(bridge.authorize_play(1, "live", &s2.play_key).is_ok()); bridge.update_publisher_stats(1, 1_500, "avc1", "mp4a", PublisherStreamMetadata::default()); { @@ -1273,11 +1291,11 @@ mod tests { #[test] fn update_player_stats_persists_bytes_out() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); bridge.on_connect(1, "127.0.0.1:1000"); - assert!(bridge.authorize_play(1, "live", "pl_k").is_ok()); + assert!(bridge.authorize_play(1, "live", &s.play_key).is_ok()); bridge.update_player_stats(1, 4096); let players = db.player_list(Some("s1")); @@ -1288,19 +1306,19 @@ mod tests { #[test] fn play_rejects_when_connection_cap_reached() { let db = Arc::new(Db::open(":memory:").unwrap()); - add_stream_with_player(&db, "s1", "pub_k", "pl_k"); + let s = add_stream_with_player(&db, "s1", "pub_k", "pl_k"); let bridge = test_bridge(Arc::clone(&db)); - let DbLookup::Ok(viewer) = db.viewer_find_by_play_key("pl_k") else { + let DbLookup::Ok(viewer) = db.viewer_find_by_play_key(&s.play_key) else { panic!("viewer not found"); }; for conn in 1..=crate::db::MAX_CONNECTIONS_PER_PLAY_KEY as u64 { bridge.on_connect(conn, "127.0.0.1:1000"); - assert!(bridge.authorize_play(conn, "live", "pl_k").is_ok()); + assert!(bridge.authorize_play(conn, "live", &s.play_key).is_ok()); } bridge.on_connect(99, "127.0.0.1:1000"); - assert!(bridge.authorize_play(99, "live", "pl_k").is_err()); + assert!(bridge.authorize_play(99, "live", &s.play_key).is_err()); assert_eq!( db.player_list(Some(&viewer.stream_id)) .iter() From 0e025b2d653a70ef4c1fde89ce6548b68153c6ab Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 17:06:52 +0200 Subject: [PATCH 18/20] chore: apply crates.io librtmp2 0.4.0 migration --- .../workflows/apply-librtmp2-crates-0.4.0.yml | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/apply-librtmp2-crates-0.4.0.yml diff --git a/.github/workflows/apply-librtmp2-crates-0.4.0.yml b/.github/workflows/apply-librtmp2-crates-0.4.0.yml new file mode 100644 index 0000000..fd370a2 --- /dev/null +++ b/.github/workflows/apply-librtmp2-crates-0.4.0.yml @@ -0,0 +1,81 @@ +name: Apply crates.io librtmp2 0.4.0 migration + +on: + push: + branches: + - chore/librtmp2-0.4.0 + paths: + - .github/workflows/apply-librtmp2-crates-0.4.0.yml + +permissions: + contents: write + +jobs: + migrate: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Check out PR branch + uses: actions/checkout@v4 + with: + ref: chore/librtmp2-0.4.0 + fetch-depth: 0 + + - name: Install Rust 1.95 + run: | + rustup toolchain install 1.95.0 --profile minimal + rustup override set 1.95.0 + + - name: Switch librtmp2 to crates.io + shell: python + run: | + from pathlib import Path + + def replace_exact(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + if old not in text: + raise SystemExit(f"expected text not found in {path}") + file.write_text(text.replace(old, new, 1)) + + replace_exact( + "Cargo.toml", + '# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head for reproducible builds.\nlibrtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "fea7e2e9d472e651b14be23c9feb63ca47bb3a5b", features = ["tls"] }', + 'librtmp2 = { version = "0.4.0", features = ["tls"] }', + ) + + replace_exact( + "Cargo.lock", + 'source = "git+https://github.com/OpenRTMP/librtmp2?rev=fea7e2e9d472e651b14be23c9feb63ca47bb3a5b#fea7e2e9d472e651b14be23c9feb63ca47bb3a5b"\ndependencies = [', + 'source = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "1cedd2ff740b4512de3108f3e4b7c797163854f2ee914c8ff16e67dd450f182b"\ndependencies = [', + ) + + replace_exact( + "AGENTS.md", + '- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `fea7e2e` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.', + '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature enabled. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.', + ) + + replace_exact( + "CHANGELOG.md", + '- Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation,\n multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek).\n Until `0.4.0` is on crates.io the lockfile resolves it from git\n (`OpenRTMP/librtmp2` @ `fea7e2e`); switch back to a crates.io version pin after\n release.', + '- Bump the `librtmp2` dependency to the crates.io release **0.4.0** (E-RTMP v2 connect negotiation,\n multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek).', + ) + + replace_exact( + "README.md", + '`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `fea7e2e` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:\n\n```toml\nlibrtmp2 = { version = "0.4.0", features = ["tls"] }\n```\n\nThen run `cargo update -p librtmp2` and commit the updated lockfile.', + '`librtmp2-server` uses the published `librtmp2` 0.4.0 release from crates.io:\n\n```toml\nlibrtmp2 = { version = "0.4.0", features = ["tls"] }\n```', + ) + + - name: Verify locked registry build + run: cargo check --locked + + - name: Commit migration and remove helper workflow + run: | + rm .github/workflows/apply-librtmp2-crates-0.4.0.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Cargo.toml Cargo.lock AGENTS.md CHANGELOG.md README.md .github/workflows/apply-librtmp2-crates-0.4.0.yml + git commit -m "chore: use crates.io librtmp2 0.4.0" + git push origin HEAD:chore/librtmp2-0.4.0 From d23067acf6df94a7830179a0c180b8b7d3a95731 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:07:37 +0000 Subject: [PATCH 19/20] chore: use crates.io librtmp2 0.4.0 --- .../workflows/apply-librtmp2-crates-0.4.0.yml | 81 ------------------- AGENTS.md | 2 +- CHANGELOG.md | 5 +- Cargo.lock | 3 +- Cargo.toml | 3 +- README.md | 4 +- 6 files changed, 6 insertions(+), 92 deletions(-) delete mode 100644 .github/workflows/apply-librtmp2-crates-0.4.0.yml diff --git a/.github/workflows/apply-librtmp2-crates-0.4.0.yml b/.github/workflows/apply-librtmp2-crates-0.4.0.yml deleted file mode 100644 index fd370a2..0000000 --- a/.github/workflows/apply-librtmp2-crates-0.4.0.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Apply crates.io librtmp2 0.4.0 migration - -on: - push: - branches: - - chore/librtmp2-0.4.0 - paths: - - .github/workflows/apply-librtmp2-crates-0.4.0.yml - -permissions: - contents: write - -jobs: - migrate: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - name: Check out PR branch - uses: actions/checkout@v4 - with: - ref: chore/librtmp2-0.4.0 - fetch-depth: 0 - - - name: Install Rust 1.95 - run: | - rustup toolchain install 1.95.0 --profile minimal - rustup override set 1.95.0 - - - name: Switch librtmp2 to crates.io - shell: python - run: | - from pathlib import Path - - def replace_exact(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - if old not in text: - raise SystemExit(f"expected text not found in {path}") - file.write_text(text.replace(old, new, 1)) - - replace_exact( - "Cargo.toml", - '# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head for reproducible builds.\nlibrtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "fea7e2e9d472e651b14be23c9feb63ca47bb3a5b", features = ["tls"] }', - 'librtmp2 = { version = "0.4.0", features = ["tls"] }', - ) - - replace_exact( - "Cargo.lock", - 'source = "git+https://github.com/OpenRTMP/librtmp2?rev=fea7e2e9d472e651b14be23c9feb63ca47bb3a5b#fea7e2e9d472e651b14be23c9feb63ca47bb3a5b"\ndependencies = [', - 'source = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "1cedd2ff740b4512de3108f3e4b7c797163854f2ee914c8ff16e67dd450f182b"\ndependencies = [', - ) - - replace_exact( - "AGENTS.md", - '- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `fea7e2e` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.', - '- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature enabled. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally.', - ) - - replace_exact( - "CHANGELOG.md", - '- Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation,\n multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek).\n Until `0.4.0` is on crates.io the lockfile resolves it from git\n (`OpenRTMP/librtmp2` @ `fea7e2e`); switch back to a crates.io version pin after\n release.', - '- Bump the `librtmp2` dependency to the crates.io release **0.4.0** (E-RTMP v2 connect negotiation,\n multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek).', - ) - - replace_exact( - "README.md", - '`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `fea7e2e` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line:\n\n```toml\nlibrtmp2 = { version = "0.4.0", features = ["tls"] }\n```\n\nThen run `cargo update -p librtmp2` and commit the updated lockfile.', - '`librtmp2-server` uses the published `librtmp2` 0.4.0 release from crates.io:\n\n```toml\nlibrtmp2 = { version = "0.4.0", features = ["tls"] }\n```', - ) - - - name: Verify locked registry build - run: cargo check --locked - - - name: Commit migration and remove helper workflow - run: | - rm .github/workflows/apply-librtmp2-crates-0.4.0.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.toml Cargo.lock AGENTS.md CHANGELOG.md README.md .github/workflows/apply-librtmp2-crates-0.4.0.yml - git commit -m "chore: use crates.io librtmp2 0.4.0" - git push origin HEAD:chore/librtmp2-0.4.0 diff --git a/AGENTS.md b/AGENTS.md index 9826604..f3e9487 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** `librtmp2 = "0.4.0"` is pinned to the current PR #128 head `fea7e2e` until the crate is on crates.io. After merge + publish, switch `Cargo.toml` to a registry-only pin (`librtmp2 = { version = "0.4.0", features = ["tls"] }`) and refresh `Cargo.lock`. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. +- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature enabled. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8287c..4e9bf8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,8 @@ begin at `1.0.0`. ## [0.1.5] — 2026-07-15 ### Changed -- Bump the pinned `librtmp2` dependency to **0.4.0** (E-RTMP v2 connect negotiation, +- Bump the `librtmp2` dependency to the crates.io release **0.4.0** (E-RTMP v2 connect negotiation, multitrack relay, Enhanced-RTMP init-cache/onMetaData replay, legacy pause/seek). - Until `0.4.0` is on crates.io the lockfile resolves it from git - (`OpenRTMP/librtmp2` @ `fea7e2e`); switch back to a crates.io version pin after - release. - Update README protocol notes to match inherited `librtmp2` 0.4.0 behaviour. - Update the RTMP HTTP E2E test `Frame` initializer for the expanded 0.4.0 `librtmp2::types::Frame` shape, including the optional multitrack `track_id`. diff --git a/Cargo.lock b/Cargo.lock index 7ede564..1a6b1b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,8 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "librtmp2" version = "0.4.0" -source = "git+https://github.com/OpenRTMP/librtmp2?rev=fea7e2e9d472e651b14be23c9feb63ca47bb3a5b#fea7e2e9d472e651b14be23c9feb63ca47bb3a5b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cedd2ff740b4512de3108f3e4b7c797163854f2ee914c8ff16e67dd450f182b" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index e1eda6d..b9021e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,7 @@ name = "librtmp2-server" path = "src/main.rs" [dependencies] -# Until librtmp2 0.4.0 is published on crates.io, pin the exact current PR #128 head for reproducible builds. -librtmp2 = { version = "0.4.0", git = "https://github.com/OpenRTMP/librtmp2", rev = "fea7e2e9d472e651b14be23c9feb63ca47bb3a5b", features = ["tls"] } +librtmp2 = { version = "0.4.0", features = ["tls"] } axum = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal", "time", "sync"] } rusqlite = { version = "0.40", features = ["bundled"] } diff --git a/README.md b/README.md index ab0b9de..d01b3ba 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,12 @@ OBS / FFmpeg / App ### Compile -`librtmp2` 0.4.0 is not on crates.io yet. Until it is published, this repo pins the current commit `fea7e2e` from librtmp2 PR #128 (see `Cargo.toml`) so builds remain reproducible. After merge and crates.io release, change the dependency to a registry-only line: +`librtmp2-server` uses the published `librtmp2` 0.4.0 release from crates.io: ```toml librtmp2 = { version = "0.4.0", features = ["tls"] } ``` -Then run `cargo update -p librtmp2` and commit the updated lockfile. - ```bash git clone https://github.com/OpenRTMP/librtmp2-server.git cd librtmp2-server From 870e3becf8619b3d247463dc7e5582e55438af01 Mon Sep 17 00:00:00 2001 From: Alexander Wagner Date: Wed, 15 Jul 2026 17:08:58 +0200 Subject: [PATCH 20/20] docs: clarify crates.io dependency source --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f3e9487..e0d0f5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Non-obvious notes for this environment: - **Toolchain:** `Cargo.toml` sets `edition = "2024"`, `rust-version = "1.95"`. The VM snapshot ships stable Rust 1.97 as the default `rustup` toolchain; the older system Rust (1.83) will not compile this crate. -- **Dependency source:** `librtmp2 = "0.4.0"` resolves from crates.io with the `tls` feature enabled. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. +- **Dependency source:** `librtmp2 = { version = "0.4.0", features = ["tls"] }` resolves from crates.io. It does **not** use the sibling `../librtmp2` checkout unless you add a `[patch]` override locally. - **SQLite is bundled** via `rusqlite` `bundled` feature — no system SQLite needed. - **Running (dev):** `LRTMP2_DB=./server.db ./target/debug/librtmp2-server -v`. RTMP listens on `:1935`, HTTP API on `:8080`. Runtime files (`server.db*`) are untracked and must not be committed. - **API token:** generated once on first start and stored in SQLite (printed to stderr). To use a known token instead, export a real `LRTMP2_API_TOKEN` **process env var before first startup** — the `.env` loader deliberately ignores `LRTMP2_API_TOKEN` in the file. The token is only re-read from env while seeding a fresh DB, so delete `server.db*` to re-seed.