From 18decc5fa3045f55ca9b6bd96505c8e70c2cbda7 Mon Sep 17 00:00:00 2001 From: Doloro1978 Date: Sun, 14 Jun 2026 01:06:25 +0100 Subject: [PATCH] rtmp -> whep, is working now --- CLAUDE.md | 103 ++++ Cargo.lock | 1507 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 + index.html | 119 ++++ src/http.rs | 60 ++ src/main.rs | 172 +++++- src/media.rs | 147 +++++ src/rtmp.rs | 52 ++ src/webrtc.rs | 209 +++++++ 9 files changed, 2370 insertions(+), 7 deletions(-) create mode 100644 CLAUDE.md create mode 100644 index.html create mode 100644 src/http.rs create mode 100644 src/media.rs create mode 100644 src/rtmp.rs create mode 100644 src/webrtc.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..417c454 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Build +cargo build + +# Run +cargo run + +# Build (Nix) +nix build + +# Dev shell (provides clang + mold linker) +nix develop +``` + +No tests exist yet. + +## Architecture + +This is an RTMP-to-WHEP bridge: it accepts an RTMP video publish stream and re-streams it to browsers via WebRTC (using the WHEP signaling protocol). + +**Signal flow:** + +``` +OBS/encoder → RTMP (port 8123) → H264Parser → async_broadcast channel + ↓ +Browser ← WebRTC/UDP ← str0m Rtc ← WHEP HTTP (port 5000) +``` + +### RTMP ingestion (port 8123) + +Each incoming TCP connection is handled in a detached smol task: + +1. **Handshake** — reads C0+C1 (1537 bytes) using `rml_rtmp::Handshake`, sends S0+S1+S2, reads C2 (1536 bytes) to complete the handshake. +2. **Session setup** — creates an `rml_rtmp::ServerSession` and writes its initial response bytes to the stream. +3. **Event loop** — reads 4096-byte chunks and calls `rtmp_session.handle_input`. The library returns a mix of `OutboundResponse` packets (written back immediately) and `RaisedEvent` values: + - `ConnectionRequested` → accepted unconditionally. + - `PublishStreamRequested` → accepted only if `stream_key == "test"`, rejected otherwise. On accept, a `StreamSession` holding an `async_broadcast::Sender>` is inserted into the `AppState` `DashMap`. + - `VideoDataReceived` → checked for HEVC (bytes 1–4 == `hvc1`; connection dropped if true), then passed to `H264Parser::parse`. If a frame is returned it is broadcast on the channel. + - `PublishStreamFinished` → the entry is removed from the `DashMap`. + +### H.264 parsing (AVCC → Annex-B) + +`H264Parser` processes the raw `VideoDataReceived` payload bytes: + +- **Byte 0**: upper nibble = frame type (1 = keyframe), lower nibble = codec ID (7 = H.264; anything else is dropped). +- **Byte 1**: AVC packet type — `0` = sequence header (AVCDecoderConfigurationRecord), `1` = NAL unit data. +- **Bytes 2–4**: composition time offset (ignored). +- **Bytes 5+**: payload. + +For packet type `0` the parser walks the `AVCDecoderConfigurationRecord` to cache the raw SPS and PPS byte arrays. + +For packet type `1` the parser converts from AVCC (each NALU preceded by a 4-byte big-endian length) to Annex-B (each NALU preceded by the `00 00 00 01` start code). Before the first NALU of every keyframe it prepends the cached `SPS` and `PPS` in Annex-B form so that str0m's RTP packetizer can bundle them into a STAP-A alongside the IDR NALU. + +### WHEP signaling (port 5000) + +The browser opens a `RTCPeerConnection`, adds a `recvonly` video transceiver, calls `createOffer`, and POSTs the SDP to `POST /whep/test`. + +`tiny_http` (running in an OS thread) receives the request, sends the tuple `("", sdp_body)` on `offer_tx`, then **blocks** on `accept_rx` waiting for the answer SDP. The 201 response with `Content-Type: application/sdp` is sent once the answer arrives. + +### WebRTC negotiation and media loop + +The smol `Webrtc` task receives the offer from `offer_rx`: + +1. Binds a UDP socket to `127.0.0.1:0` — this is the only ICE candidate advertised (host, UDP, loopback). Remote candidates coming from the browser are handled by str0m internally; the socket just needs to be reachable from the browser on the same machine. +2. Builds an `Rtc` with H.264 explicitly configured for payload types 102, 104, and 106 (profiles `0x42e01f`, `0x4d001f`, `0x64001f`). The default H.264 support is disabled first so only these three PTs are offered. +3. Adds a `SendOnly` video media track, then calls `changes.accept_offer(offer_sdp)` to produce the SDP answer. +4. Sends the answer back on `accept_tx` (unblocking the HTTP thread), then detaches a per-connection async loop. + +**Per-connection loop** (`Webrtc::detach_connection`): + +- Calls `rtc.poll_output()` in a tight loop until it returns `Output::Timeout(deadline)`. Each iteration either sends a UDP datagram (`Output::Transmit`) or handles an event: + - `Event::MediaAdded` — iterates the writer's payload params and picks the PT with the highest `profile_level_id`, storing it in `video_pt`. + - `Event::Connected` — sets `connected = true`; media sending begins after this. +- Once connected, on each iteration it lazily subscribes to the `async_broadcast` channel for stream key `"test"` (if not already subscribed), then drains all available frames with `try_recv` and writes each one via `writer.write(pt, now, rtp_time, frame.data)`. The `rtp_time` is constructed from `frame.timestamp_ms` as `MediaTime::from_millis`. +- Then waits with `smol::future::or` for either the str0m deadline or a UDP datagram. Incoming datagrams are fed to `rtc.handle_input(Input::Receive(...))` for ICE/DTLS/RTCP processing; a timeout fires `rtc.handle_input(Input::Timeout(...))`. + +**Module breakdown:** + +- `main.rs` — Entry point. Owns `AppState` (a `DashMap`). Spawns the HTTP and WebRTC tasks, then loops accepting RTMP TCP connections. Each RTMP connection runs the `rml_rtmp` handshake and session, parses H.264 frames via `H264Parser`, and broadcasts them on a per-stream `async_broadcast` channel stored in `AppState`. + +- `src/http.rs` — Blocking `tiny_http` server on port 5000 running in its own OS thread. Handles CORS and WHEP `POST /whep/*` requests. Forwards the SDP offer to the WebRTC task via `smol::channel` and blocks waiting for the SDP answer before responding. + +- `src/webrtc.rs` — Async task (smol) that receives SDP offers, builds a `str0m` `Rtc` instance with H.264 codec config, negotiates the answer, and detaches a per-connection loop. That loop polls `str0m` for output (packets to send), listens on a per-connection UDP socket for incoming DTLS/ICE, and drains the `async_broadcast` video channel to write Annex-B frames into the `str0m` writer. + +- `src/media.rs` — `H264Parser` converts raw RTMP `VideoDataReceived` payloads (AVCC format) to Annex-B. Sequence-header packets (AVC packet type 0) update cached SPS/PPS; NAL unit packets (type 1) prepend SPS+PPS before each keyframe and convert length-prefixed NALUs to start-code NALUs. + +- `src/rtmp.rs` — An early stub for a manual RTMP handshake implementation, unused in the current flow (the actual RTMP handling uses `rml_rtmp` directly in `main.rs`). + +**Key design choices:** + +- Only the hardcoded stream key `"test"` is accepted; other keys are rejected. +- HEVC/H.265 is explicitly rejected at ingestion time. +- `async_broadcast` channels are overflow-enabled (old frames are silently dropped if no subscriber drains fast enough). +- The runtime is `smol` (not tokio). The HTTP server runs in a dedicated OS thread (`std::thread::spawn`) because `tiny_http` is synchronous. +- `str0m` handles RTP packetization, DTLS, and ICE internally; the code only provides Annex-B video bytes and a UDP socket. + +**Test page:** `index.html` — open in a browser to view the stream via WHEP without any extra tooling. diff --git a/Cargo.lock b/Cargo.lock index 99cf324..88aba38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,96 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -133,12 +223,65 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "untrusted", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.2" @@ -152,12 +295,94 @@ dependencies = [ "piper", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -167,12 +392,204 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "dimpl" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7afb6878ee6941d3ee770bd8a391c0c083ee2102a7e8e91a730fb722ef1e46b9" +dependencies = [ + "aes", + "arrayvec", + "aws-lc-rs", + "ccm", + "der", + "log", + "nom 8.0.0", + "once_cell", + "pkcs8", + "rand 0.9.4", + "rcgen", + "sec1", + "signature", + "spki", + "subtle", + "time", + "x509-cert", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -210,6 +627,30 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-core" version = "0.3.32" @@ -235,12 +676,170 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840878b6e30d40e5bda1a7116100f1a18b7bdb91814513b87be80bbfb5d41879" +dependencies = [ + "crc", + "serde", + "str0m-proto", + "subtle", + "tracing", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -253,6 +852,21 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + [[package]] name = "macro_rules_attribute" version = "0.2.2" @@ -269,18 +883,126 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -298,6 +1020,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "polling" version = "3.11.0" @@ -312,13 +1044,214 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "aws-lc-rs", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rml_amf0" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63551cfcd4d1f42733c190e4b58dd268b1eacb73410d9afbf62784aa12cac240" +dependencies = [ + "byteorder", + "thiserror 1.0.69", +] + +[[package]] +name = "rml_rtmp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a354e80eb7aa2a6fed09b3bd25c19bcfd32cf51f81f1219f4ec04f34519989da" +dependencies = [ + "byteorder", + "bytes", + "hmac", + "rand 0.8.6", + "rml_amf0", + "sha2", + "thiserror 1.0.69", +] + [[package]] name = "rtmp-to-whip-simple-server" version = "0.1.0" dependencies = [ + "async-broadcast", + "bytes", + "dashmap", + "futures-lite", "macro_rules_attribute", + "rand 0.10.1", + "rml_rtmp", "smol", "smol-macros", + "str0m", + "tiny_http", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", ] [[package]] @@ -334,6 +1267,116 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctp-proto" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8423ea59db998985015bc5d0145837eab48f60ec449a2dc01f5870499afe0a4" +dependencies = [ + "bytes", + "crc", + "log", + "rand 0.9.4", + "rustc-hash", + "slab", + "thiserror 2.0.18", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures 0.2.17", + "digest", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -344,12 +1387,24 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "smol" version = "2.0.2" @@ -380,6 +1435,293 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "str0m" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca05746700d3621a27d7b99beaf2e724f8940608cbab00c3e4ebd974620668af" +dependencies = [ + "arrayvec", + "base64ct", + "combine", + "dimpl", + "fastrand", + "is", + "sctp-proto", + "serde", + "str0m-aws-lc-rs", + "str0m-proto", + "subtle", + "time", + "tracing", +] + +[[package]] +name = "str0m-aws-lc-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1908a6439b68fd22c275d44cbf4b50b2a75cc45f2b626160d87a194023c18fcd" +dependencies = [ + "aws-lc-rs", + "dimpl", + "str0m-proto", + "time", +] + +[[package]] +name = "str0m-proto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02836118cf7384413d7e8beb8b9ab56a4007d1b6514c11df35150a2e7aef8f1a" +dependencies = [ + "base64ct", + "dimpl", + "fastrand", + "serde", + "subtle", + "time", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -394,3 +1736,168 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 763fcb7..2797068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] +async-broadcast = "0.7.2" +bytes = "1.11.1" +dashmap = "6.2.1" macro_rules_attribute = "0.2.2" +rand = "0.10.1" +rml_rtmp = "0.8.0" +futures-lite = "2" smol = "2.0.2" smol-macros = "0.1.1" +str0m = "0.20.0" +tiny_http = "0.12.0" diff --git a/index.html b/index.html new file mode 100644 index 0000000..7f78316 --- /dev/null +++ b/index.html @@ -0,0 +1,119 @@ + + + + + WHEP Viewer + + + + +
Waiting for stream…
+ + + diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..e05e770 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,60 @@ +use std::error::Error; + +use smol::channel::{Receiver, Sender}; +use tiny_http::{Header, Response, Server, StatusCode}; + +pub struct HttpServer { + pub offer_tx: Sender<(String, String)>, + pub accept_rx: Receiver<(String, String)>, +} + +impl HttpServer { + pub fn start(self) -> Result<(), Box> { + std::thread::spawn(move || { + let server = Server::http("0.0.0.0:5000").unwrap(); + for mut request in server.incoming_requests() { + let cors = + Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(); + let cors_methods = + Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"POST, OPTIONS"[..]) + .unwrap(); + let cors_headers = + Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]) + .unwrap(); + + if *request.method() == tiny_http::Method::Options { + let reply = Response::from_data("") + .with_status_code(StatusCode(204)) + .with_header(cors) + .with_header(cors_methods) + .with_header(cors_headers); + request.respond(reply).unwrap(); + continue; + } + + if request.url().contains("whep") && *request.method() == tiny_http::Method::Post { + let mut body = String::new(); + request.as_reader().read_to_string(&mut body).unwrap(); + println!("HTTP: received offer, body length={}", body.len()); + smol::block_on(self.offer_tx.send(("".to_string(), body))).unwrap(); + let content_type = + Header::from_bytes(&b"Content-Type"[..], &b"application/sdp"[..]).unwrap(); + + let reply = match smol::block_on(self.accept_rx.recv()) { + Ok(accept) => Response::from_data(accept.1) + .with_status_code(StatusCode(201)) + .with_header(content_type) + .with_header(cors) + .with_header(cors_methods) + .with_header(cors_headers), + Err(_) => Response::from_data("") + .with_status_code(StatusCode(500)) + .with_header(content_type), + }; + request.respond(reply).unwrap(); + } + } + }); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 56c49ad..c4c12e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,181 @@ -use std::error::Error; +use std::{error::Error, sync::Arc}; +use async_broadcast::{Receiver, Sender, broadcast}; +use dashmap::DashMap; use macro_rules_attribute::apply; -use smol::{io::AsyncReadExt, net::TcpListener, stream::StreamExt}; +use rml_rtmp::{ + handshake::{Handshake, HandshakeProcessResult, PeerType}, + sessions::{ServerSession, ServerSessionConfig, ServerSessionEvent, ServerSessionResult}, +}; +use smol::{ + io::{AsyncReadExt, AsyncWriteExt}, + lock::Mutex, + net::TcpListener, + stream::StreamExt, +}; use smol_macros::main; +use crate::{ + http::HttpServer, + media::{H264Parser, VideoFrame}, +}; + +mod http; +mod media; +mod rtmp; +mod webrtc; + +struct AppState { + stream_sessions: Arc>, +} + +struct StreamSession { + stream_key: String, + frame_channel: async_broadcast::Sender>, +} + #[apply(main!)] async fn main() -> Result<(), Box> { let listener = TcpListener::bind("0.0.0.0:8123").await?; let mut incoming = listener.incoming(); + let mut appstate = Arc::new(Mutex::new(AppState { + stream_sessions: Arc::new(DashMap::new()), + })); + let (offer_tx, offer_rx) = smol::channel::bounded::<(String, String)>(32); + let (answer_tx, answer_rx) = smol::channel::bounded::<(String, String)>(32); + + let http = HttpServer { + offer_tx, + accept_rx: answer_rx, + }; + + http.start()?; + + let app = appstate.lock().await; + + let webrtc = webrtc::Webrtc { + offer_rx, + accept_tx: answer_tx, + sessions_ref: app.stream_sessions.clone(), + }; + + webrtc.start()?; + + drop(app); while let Some(connection) = incoming.next().await { - let mut stream = connection?; + let stream = connection?; + let appstate = appstate.clone(); + smol::spawn(async move { - let mut buf = vec![0; 1024]; - stream.read(&mut buf).await.unwrap(); - print!("{:#?}", buf); + let mut stream = stream; + let mut server = Handshake::new(PeerType::Server); + let appstate = appstate; + + let mut c0_c1: [u8; 1537] = [0; 1537]; + stream.read_exact(&mut c0_c1).await.unwrap(); + let s0_s1_s2 = server.process_bytes(&c0_c1); + let s0_s1_s2 = match s0_s1_s2 { + Ok(HandshakeProcessResult::InProgress { + response_bytes: bytes, + }) => bytes, + _ => panic!("handshake failed"), + }; + stream.write_all(&s0_s1_s2).await.unwrap(); + let mut c2 = vec![0u8; 1536]; + stream.read_exact(&mut c2).await.unwrap(); + + match server.process_bytes(&c2[..]) { + Ok(HandshakeProcessResult::Completed { .. }) => {} + Ok(HandshakeProcessResult::InProgress { + response_bytes: meow, + }) => stream.write_all(&meow).await.unwrap(), + x => panic!("Unexpected process_bytes response: {:?}", x), + } + + let config = ServerSessionConfig::new(); + let (mut rtmp_session, bytes) = ServerSession::new(config).unwrap(); + for x in bytes { + if let ServerSessionResult::OutboundResponse(packet) = x { + stream.write_all(&packet.bytes).await.unwrap(); + } + } + let (mut video_channel, _video_rx) = broadcast::>(32); + video_channel.set_overflow(true); + let mut parser = H264Parser::new(); + + loop { + let mut buf: [u8; 4096] = [0; 4096]; + let n = stream.read(&mut buf).await.unwrap(); + + let obs_events = rtmp_session.handle_input(&buf[..n]).unwrap(); + for event in obs_events { + match event { + ServerSessionResult::OutboundResponse(packet) => { + stream.write_all(&packet.bytes).await.unwrap(); + } + ServerSessionResult::RaisedEvent(x) => match x { + ServerSessionEvent::PublishStreamFinished { .. } => { + appstate.lock().await.stream_sessions.remove("test"); + } + ServerSessionEvent::PublishStreamRequested { + request_id, + app_name, + stream_key, + .. + } => { + let mut reply = rtmp_session.accept_request(request_id).unwrap(); + if stream_key != "test" { + reply = + rtmp_session.reject_request(request_id, "", "").unwrap(); + } + + let session = StreamSession { + stream_key: stream_key.clone(), + frame_channel: video_channel.clone(), + }; + + appstate + .lock() + .await + .stream_sessions + .insert(stream_key.clone(), session); + + for x in reply { + if let ServerSessionResult::OutboundResponse(y) = x { + stream.write_all(&y.bytes).await.unwrap(); + } + } + } + ServerSessionEvent::ConnectionRequested { request_id, .. } => { + let reply = rtmp_session.accept_request(request_id).unwrap(); + for x in reply { + if let ServerSessionResult::OutboundResponse(y) = x { + stream.write_all(&y.bytes).await.unwrap(); + } + } + } + ServerSessionEvent::VideoDataReceived { + data, timestamp, .. + } => { + if data.len() >= 5 && &data[1..5] == b"hvc1" { + println!("HEVC/H.265 not supported, closing connection"); + return; + } + if let Some(parsed_frame) = parser.parse(&data, timestamp.value) { + video_channel.broadcast(Arc::new(parsed_frame)).await.ok(); + } + } + _ => {} + }, + _ => {} + } + } + } }) - .await; + .detach(); } Ok(()) } + diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..98ef41e --- /dev/null +++ b/src/media.rs @@ -0,0 +1,147 @@ +// Claude slop... im not skilled amount to do this bullshit + +use bytes::Bytes; + +pub struct VideoFrame { + pub data: Bytes, + pub is_keyframe: bool, + pub timestamp_ms: u32, +} + +pub struct AudioFrame { + pub data: Bytes, + pub timestamp_ms: u32, +} + +pub struct H264Parser { + sps: Option>, + pps: Option>, +} + +impl H264Parser { + pub fn new() -> Self { + Self { + sps: None, + pps: None, + } + } + + /// Parse an RTMP VideoDataReceived payload. Returns None for sequence + /// header packets (which carry SPS/PPS but no displayable frame). + pub fn parse(&mut self, bytes: &[u8], timestamp_ms: u32) -> Option { + if bytes.len() < 5 { + return None; + } + + let frame_type = (bytes[0] >> 4) & 0x0F; + let codec_id = bytes[0] & 0x0F; + + if codec_id != 7 { + return None; // not H.264 + } + + let avc_packet_type = bytes[1]; + // bytes[2..5] are the composition time offset — not needed for sending + let payload = &bytes[5..]; + + match avc_packet_type { + 0 => { + self.parse_sequence_header(payload); + None + } + 1 => { + let is_keyframe = frame_type == 1; + let data = self.avcc_to_annexb(payload, is_keyframe)?; + Some(VideoFrame { + data: Bytes::from(data), + is_keyframe, + timestamp_ms, + }) + } + _ => None, + } + } + + fn parse_sequence_header(&mut self, payload: &[u8]) { + // AVCDecoderConfigurationRecord layout: + // [0] configurationVersion + // [1] AVCProfileIndication + // [2] profile_compatibility + // [3] AVCLevelIndication + // [4] 0xFF (lower 2 bits = lengthSizeMinusOne, always 3 meaning 4-byte lengths) + // [5] 0xE0 | numSPS + // [6..] SPS entries: 2-byte length + bytes + // then: numPPS, PPS entries: 2-byte length + bytes + if payload.len() < 7 { + return; + } + + let mut i = 5; + + let num_sps = (payload[i] & 0x1F) as usize; + i += 1; + + for _ in 0..num_sps { + if i + 2 > payload.len() { + return; + } + let len = u16::from_be_bytes([payload[i], payload[i + 1]]) as usize; + i += 2; + if i + len > payload.len() { + return; + } + self.sps = Some(payload[i..i + len].to_vec()); + i += len; + } + + if i >= payload.len() { + return; + } + + let num_pps = payload[i] as usize; + i += 1; + + for _ in 0..num_pps { + if i + 2 > payload.len() { + return; + } + let len = u16::from_be_bytes([payload[i], payload[i + 1]]) as usize; + i += 2; + if i + len > payload.len() { + return; + } + self.pps = Some(payload[i..i + len].to_vec()); + i += len; + } + } + + fn avcc_to_annexb(&self, payload: &[u8], is_keyframe: bool) -> Option> { + let mut out = Vec::new(); + + // Prepend SPS+PPS before every keyframe so str0m's packetizer + // can bundle them into a STAP-A alongside the IDR NALU. + if is_keyframe { + if let (Some(sps), Some(pps)) = (&self.sps, &self.pps) { + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(sps); + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(pps); + } + } + + // Convert each length-prefixed NALU to an Annex B start-code NALU. + let mut i = 0; + while i + 4 <= payload.len() { + let nalu_len = u32::from_be_bytes(payload[i..i + 4].try_into().unwrap()) as usize; + i += 4; + if i + nalu_len > payload.len() { + break; + } + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(&payload[i..i + nalu_len]); + i += nalu_len; + } + + if out.is_empty() { None } else { Some(out) } + } +} diff --git a/src/rtmp.rs b/src/rtmp.rs new file mode 100644 index 0000000..98ac91f --- /dev/null +++ b/src/rtmp.rs @@ -0,0 +1,52 @@ +#[derive(Default, Debug)] +pub struct RtmpSession { + client: RtmpClient, +} + +#[derive(Debug)] +struct RtmpClient { + version: u8, + timestamp: u32, + magic_bytes: [u8; 1536], +} + +impl Default for RtmpClient { + fn default() -> Self { + Self { + magic_bytes: [0; 1536], + version: 0, + timestamp: 0, + } + } +} + +impl RtmpSession { + pub fn consume_handshake(&mut self, bytes: &[u8]) -> Result<(), ()> { + let version = bytes[0]; + let timestamp = u32::from_be_bytes(bytes[1..5].try_into().unwrap()); + let mut random: [u8; 1536] = [0; 1536]; + random.copy_from_slice(&bytes[1..1537]); + + println!("size of rand: {}", random.len()); + + let client = RtmpClient { + version, + timestamp, + magic_bytes: random, + }; + + self.client = client; + + Ok(()) + } + pub fn response(&self) -> [u8; 1537] { + let mut reply: [u8; 1537] = [0; 1537]; + reply[0] = 3; + // reply[1..1537].copy_from_slice(&self.client.magic_bytes); + let mut rand: [u8; 1536] = [0; 1536]; + rand.fill(1); + reply[1..1537].copy_from_slice(&rand); + + reply + } +} diff --git a/src/webrtc.rs b/src/webrtc.rs new file mode 100644 index 0000000..fe99261 --- /dev/null +++ b/src/webrtc.rs @@ -0,0 +1,209 @@ +use dashmap::DashMap; +use smol::{ + channel::{Receiver, Sender}, + net::UdpSocket, +}; +use std::{ + error::Error, + sync::Arc, + time::{Duration, Instant}, +}; + +use str0m::{ + Candidate, Event, IceConnectionState, Input, Output, Rtc, + change::SdpOffer, + media::{MediaKind, MediaTime, Mid}, + net::{Protocol, Receive}, +}; + +use crate::{StreamSession, media::VideoFrame}; + +pub struct Webrtc { + pub offer_rx: Receiver<(String, String)>, + pub accept_tx: Sender<(String, String)>, + pub sessions_ref: Arc>, +} + +impl Webrtc { + pub fn start(self) -> Result<(), Box> { + smol::spawn(async move { + while let Ok(offer) = self.offer_rx.recv().await { + let (stream_key, sdp_body) = offer; + let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let local_addr = socket.local_addr().unwrap(); + + let mut builder = Rtc::builder(); + { + let cc = builder.codec_config(); + cc.enable_h264(false); + cc.add_h264(102.into(), None, true, 0x42e01f); + cc.add_h264(104.into(), None, true, 0x4d001f); + cc.add_h264(106.into(), None, true, 0x64001f); + } + let mut rtc = builder.build(Instant::now()); + + let candidate = Candidate::host(local_addr, Protocol::Udp).unwrap(); + rtc.add_local_candidate(candidate); + + let offer_sdp = SdpOffer::from_sdp_string(&sdp_body).unwrap(); + let mut changes = rtc.sdp_api(); + let mid = changes.add_media( + MediaKind::Video, + str0m::media::Direction::SendOnly, + Some(stream_key.clone()), + Some("video0".to_string()), + None, + ); + let offer_answer = match changes.accept_offer(offer_sdp) { + Ok(a) => a, + Err(e) => { + println!("accept_offer failed: {:?}", e); + continue; + } + }; + let answer_sdp = offer_answer.to_sdp_string(); + + self.accept_tx + .send((stream_key.clone(), answer_sdp)) + .await + .unwrap(); + + let sessions_ref = self.sessions_ref.clone(); + smol::spawn(async move { + Webrtc::detach_connection(socket, rtc, sessions_ref, mid).await; + }) + .detach(); + } + }) + .detach(); + Ok(()) + } + + async fn detach_connection( + socket: UdpSocket, + mut rtc: Rtc, + sessions_ref: Arc>, + _hint_mid: Mid, + ) { + let mut video_mid: Option = None; + let mut video_pt = None; + let mut connected = false; + let mut video_stream: Option>> = None; + + let mut recv_buf = vec![0u8; 65535]; + let local_addr = socket.local_addr().unwrap(); + + loop { + let deadline = loop { + match rtc.poll_output() { + Ok(Output::Timeout(t)) => break t, + Ok(Output::Transmit(t)) => { + if socket.send_to(&t.contents, t.destination).await.is_err() { + return; + } + } + Ok(Output::Event(e)) => match e { + Event::MediaAdded(ma) => { + if ma.kind == MediaKind::Video { + if let Some(writer) = rtc.writer(ma.mid) { + let best = writer + .payload_params() + .max_by_key(|p| { + p.spec().format.profile_level_id.unwrap_or(0) + }); + if let Some(params) = best { + println!("Selected PT {:?}", params.pt()); + video_pt = Some(params.pt()); + video_mid = Some(ma.mid); + } + } + } + } + Event::IceConnectionStateChange(state) => { + println!("ICE state: {:?}", state); + } + Event::Connected => { + println!("DTLS+ICE connected, ready for media"); + connected = true; + } + _ => {} + }, + Err(_) => return, + } + }; + + if connected { + if video_stream.is_none() { + if let Some(session) = sessions_ref.get("test") { + video_stream = Some(session.frame_channel.new_receiver()); + } + } + if let Some(ref mut stream) = video_stream { + // Drain at most 8 frames per loop tick so the UDP socket + // (ICE keepalives, RTCP) is not starved by a backlog. + for _ in 0..8 { + match stream.try_recv() { + Ok(frame) => { + let now = Instant::now(); + // Explicit 90 kHz clock for H.264 RTP timestamps. + let rtp_time = MediaTime::from_90khz(frame.timestamp_ms as u64 * 90); + if let (Some(pt), Some(writer)) = + (video_pt, video_mid.and_then(|m| rtc.writer(m))) + { + if let Err(e) = + writer.write(pt, now, rtp_time, frame.data.to_vec()) + { + println!("write error: {:?}", e); + } + } + } + Err(async_broadcast::TryRecvError::Empty) => break, + Err(async_broadcast::TryRecvError::Closed) => return, + // Overflow means some frames were dropped; the next + // try_recv will give the oldest surviving frame, so + // continue draining rather than breaking. + Err(async_broadcast::TryRecvError::Overflowed(_)) => continue, + } + } + } + } + + // Cap wait to 20 ms so frame delivery stays timely even when + // str0m's deadline is far out. + let wait_until = deadline.min(Instant::now() + Duration::from_millis(20)).max(Instant::now()); + + let input = smol::future::or( + async { + smol::Timer::at(wait_until).await; + None + }, + async { + let (n, from) = socket.recv_from(&mut recv_buf).await.ok()?; + Some((n, from)) + }, + ) + .await; + + match input { + None => { + rtc.handle_input(Input::Timeout(Instant::now())).ok(); + } + Some((n, from)) => { + let data = recv_buf[..n].to_vec(); + if let Ok(contents) = data.as_slice().try_into() { + rtc.handle_input(Input::Receive( + Instant::now(), + Receive { + proto: Protocol::Udp, + source: from, + destination: local_addr, + contents, + }, + )) + .ok(); + } + } + } + } + } +}