binary: axum signaling + DashMap session store + browser test client
Four routes on axum 0.7 per spec §4.1: POST /v1/sessions (mint), POST /v1/sessions/:id/offer (str0m-native SDP accept), DELETE /v1/sessions/:id (close), GET / (static HTML client). Session store is a DashMap<ChannelId, Arc<Mutex<RtcSession>>> (spec §4.5); one tokio task drives all session poll loops — per-session tasks would pre-pave the wrong pattern for step 4's dedicated thread. Graceful shutdown drops the DashMap on Ctrl-C / SIGTERM. Integration test exercises the REST surface; manual browser e2e per README §6.5.
This commit is contained in:
574
Cargo.lock
generated
574
Cargo.lock
generated
@@ -23,6 +23,15 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.7"
|
||||
@@ -68,6 +77,23 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "audiopus_sys"
|
||||
version = "0.2.2"
|
||||
@@ -108,6 +134,73 @@ dependencies = [
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
@@ -120,6 +213,12 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -221,6 +320,12 @@ 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"
|
||||
@@ -240,6 +345,20 @@ 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",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
@@ -335,6 +454,16 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
@@ -353,12 +482,30 @@ version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
@@ -416,6 +563,92 @@ dependencies = [
|
||||
"r-efi 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -477,24 +710,65 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[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.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -514,6 +788,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -572,6 +855,29 @@ dependencies = [
|
||||
"audiopus_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[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 = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -581,6 +887,12 @@ dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
@@ -690,6 +1002,32 @@ dependencies = [
|
||||
"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 = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -720,6 +1058,24 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "rutster"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dashmap",
|
||||
"rutster-call-model",
|
||||
"rutster-media",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rutster-call-model"
|
||||
version = "0.0.0"
|
||||
@@ -750,6 +1106,18 @@ version = "0.0.0"
|
||||
name = "rutster-tap"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[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.10.0"
|
||||
@@ -807,12 +1175,67 @@ dependencies = [
|
||||
"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 = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
@@ -825,6 +1248,22 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -899,6 +1338,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
@@ -950,6 +1395,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
@@ -981,12 +1435,69 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -1010,6 +1521,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1041,12 +1582,24 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[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.4+wasi-0.2.12"
|
||||
@@ -1101,6 +1654,21 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
@@ -1170,3 +1738,9 @@ name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/rutster",
|
||||
"crates/rutster-call-model",
|
||||
"crates/rutster-media",
|
||||
"crates/rutster-signaling-sip",
|
||||
|
||||
@@ -73,7 +73,7 @@ use str0m::media::Mid;
|
||||
/// contract on `handle_input` + `poll_output`). `RwLock`'s read-mode
|
||||
/// would be useless because str0m takes `&mut Rtc`. `Mutex` it is.
|
||||
pub struct RtcSession {
|
||||
pub(crate) channel: Channel,
|
||||
pub channel: Channel,
|
||||
pub(crate) rtc: Rtc,
|
||||
pub(crate) decoder: OpusDecoder,
|
||||
pub(crate) encoder: OpusEncoder,
|
||||
|
||||
28
crates/rutster/Cargo.toml
Normal file
28
crates/rutster/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
# crates/rutster/Cargo.toml
|
||||
[package]
|
||||
name = "rutster"
|
||||
version = "0.0.0"
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Rutster binary: axum signaling + media driver + static browser test client (slice 1)."
|
||||
|
||||
[dependencies]
|
||||
rutster-call-model = { path = "../rutster-call-model" }
|
||||
rutster-media = { path = "../rutster-media" }
|
||||
axum = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tower = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "rutster"
|
||||
path = "src/main.rs"
|
||||
26
crates/rutster/src/lib.rs
Normal file
26
crates/rutster/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! # rutster (lib) — slice-1 binary library target
|
||||
//!
|
||||
//! Thin library surface re-exporting the modules the `main.rs` binary
|
||||
//! and the `tests/api_integration.rs` integration test both need.
|
||||
//!
|
||||
//! # Why a `lib.rs` *and* a `main.rs`?
|
||||
//!
|
||||
//! Pure binary crates (`src/main.rs` only) do not surface items via the
|
||||
//! crate's external path (`rutster::routes::router`) — Cargo's
|
||||
//! integration tests (`tests/*.rs`) link against the *library* target,
|
||||
//! not the binary target. With `pub mod` declared only in `main.rs`,
|
||||
//! `use rutster::routes::router` in the integration test fails to
|
||||
//! resolve (`E0433`). The library/binary split (the standard Cargo
|
||||
//! pattern: a crate compiles both `lib.rs` → `librutster` and `main.rs`
|
||||
//! → `rutster`, with the binary linking the library) makes the same
|
||||
//! `pub mod` declarations visible to both consumers. `main.rs` then
|
||||
//! imports them via `use rutster::...` paths exactly as the brief's
|
||||
//! Step 6 sketches.
|
||||
//!
|
||||
//! ## Architecture refs
|
||||
//!
|
||||
//! - [slice-1 spec §4](../../docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md)
|
||||
//! - [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) — fused vertical.
|
||||
|
||||
pub mod routes;
|
||||
pub mod session_map;
|
||||
66
crates/rutster/src/main.rs
Normal file
66
crates/rutster/src/main.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! # rutster — slice-1 binary
|
||||
//!
|
||||
//! axum signaling server + the media-core poll driver + a static HTML
|
||||
//! test client (spec §4). Binds `0.0.0.0:8080` plaintext — no TLS in
|
||||
//! slice 1 (out of scope per §1.2). DTLS-SRTP is mandatory on the media
|
||||
//! surface (str0m handles natively); TLS on the HTTP surface lands with
|
||||
//! the deployment posture in step 5.
|
||||
//!
|
||||
//! ## Architecture refs
|
||||
//!
|
||||
//! - [slice-1 spec §4](../../docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md)
|
||||
//! - [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) — fused vertical.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use rutster::routes::router;
|
||||
use rutster::session_map::AppState;
|
||||
use tracing::info;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "rutster=info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let state = AppState::new();
|
||||
state.clone().spawn_poll_task().await;
|
||||
|
||||
let addr: SocketAddr = "0.0.0.0:8080".parse().expect("valid addr");
|
||||
info!(%addr, "listening");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, router(state))
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Ctrl-C / SIGTERM handler (spec §4.5). Dropping the AppState drops the
|
||||
/// DashMap, which drops every RtcSession, which str0m sees as a closed
|
||||
/// peer — browsers get a dead peer connection. Acceptable for the dev
|
||||
/// loop; no in-flight call preservation story in slice 1.
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("installed ctrl-c handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let sigterm = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("installed SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let sigterm = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => info!("received Ctrl-C, shutting down"),
|
||||
_ = sigterm => info!("received SIGTERM, shutting down"),
|
||||
}
|
||||
}
|
||||
107
crates/rutster/src/routes.rs
Normal file
107
crates/rutster/src/routes.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! # HTTP routes (spec §4.1, §4.3)
|
||||
//!
|
||||
//! Four routes on axum 0.7:
|
||||
//! - `POST /v1/sessions` → `{ "session_id": "<uuid>" }`.
|
||||
//! - `POST /v1/sessions/:id/offer` (`Content-Type: application/sdp` req
|
||||
//! + response) → core returns the SDP answer.
|
||||
//! - `DELETE /v1/sessions/:id` → tear down.
|
||||
//! - `GET /` → serve the static HTML test client.
|
||||
//!
|
||||
//! No authn/authz, no TLS, no multi-tenancy — all deferred per spec §1.2.
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::session_map::AppState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SessionCreated {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
/// POST /v1/sessions — mint a fresh RtcSession (spec §4.1).
|
||||
pub async fn create_session(State(state): State<AppState>) -> Response {
|
||||
match state.create_session() {
|
||||
Ok(id) => {
|
||||
let body = Json(SessionCreated {
|
||||
session_id: id.0.to_string(),
|
||||
});
|
||||
(StatusCode::OK, body).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "session create failed");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /v1/sessions/:id/offer — accept browser SDP offer, return answer
|
||||
/// (spec §4.1). Non-trickle: the offer body carries all browser ICE
|
||||
/// candidates; the answer carries the core's candidates (filled natively
|
||||
/// by str0m 0.21's sdp_api().accept_offer).
|
||||
pub async fn post_offer(
|
||||
State(state): State<AppState>,
|
||||
Path(id_str): Path<String>,
|
||||
body: String,
|
||||
) -> Response {
|
||||
let Ok(id_uuid) = Uuid::parse_str(&id_str) else {
|
||||
return (StatusCode::NOT_FOUND, "bad session id").into_response();
|
||||
};
|
||||
let id = rutster_call_model::ChannelId(id_uuid);
|
||||
let Some(session_arc) = state.get(id) else {
|
||||
return (StatusCode::NOT_FOUND, "no such session").into_response();
|
||||
};
|
||||
let mut s = session_arc.lock().await;
|
||||
match s.accept_offer(&body) {
|
||||
Ok(answer_sdp) => (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/sdp")],
|
||||
answer_sdp,
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "SDP accept failed");
|
||||
StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DELETE /v1/sessions/:id — tear down (spec §4.1, §4.5).
|
||||
pub async fn delete_session(State(state): State<AppState>, Path(id_str): Path<String>) -> Response {
|
||||
let Ok(id_uuid) = Uuid::parse_str(&id_str) else {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
let id = rutster_call_model::ChannelId(id_uuid);
|
||||
state.close(id).await;
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
|
||||
/// GET / — serve the static browser test client (spec §4.4).
|
||||
pub async fn index() -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
include_str!("../static/index.html"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Build the axum router.
|
||||
pub fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
// `POST /v1/sessions` creates; `DELETE /v1/sessions/:id` destroys
|
||||
// (note the `:id` — deleting the collection root has no meaning and
|
||||
// would extract a missing `:id` path parameter, so the two routes
|
||||
// live at different paths, not chained via `.delete(...)` on the
|
||||
// collection route as axum's method chaining would suggest).
|
||||
.route("/v1/sessions", post(create_session))
|
||||
.route("/v1/sessions/:id", axum::routing::delete(delete_session))
|
||||
.route("/v1/sessions/:id/offer", post(post_offer))
|
||||
.with_state(state)
|
||||
}
|
||||
121
crates/rutster/src/session_map.rs
Normal file
121
crates/rutster/src/session_map.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! # Session store + poll-driver (spec §4.5)
|
||||
//!
|
||||
//! `DashMap<ChannelId, RtcSession>` holds active sessions; the `ChannelId`
|
||||
//! (UUID newtype from `rutster-call-model`) IS the session id surfaced in
|
||||
//! the REST API. A single tokio task drives all sessions' poll loops (a
|
||||
//! per-session task would clutter the runtime and pre-pave the wrong
|
||||
//! pattern for the step-4 dedicated thread — spec §4.5).
|
||||
//!
|
||||
//! # Concurrency note
|
||||
//!
|
||||
//! `DashMap` shards its inner `HashMap` so concurrent gets/puts across
|
||||
//! different `ChannelId`s don't contend. We iterate per-shard inside the
|
||||
//! poll task to drive each session; entries marked `Closed` are removed.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use rutster_call_model::{ChannelId, ChannelState};
|
||||
use rutster_media::{RtcSession, RtcSessionError};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// The application state shared across axum handlers + the poll task.
|
||||
///
|
||||
/// # Why `Arc` (and not bare)
|
||||
///
|
||||
/// axum clones the state into every handler. `Arc` is the standard way
|
||||
/// to share `DashMap` + `Mutex` owned state across these clones cheaply
|
||||
/// (a single heap allocation, refcount-bumped per clone). Without `Arc`,
|
||||
/// every handler would move its own copy — and `DashMap` is not `Copy`.
|
||||
///
|
||||
/// # Why a separate `poll_running` `Mutex`
|
||||
///
|
||||
/// The poll loop is one task; we don't want two. The Mutex guards a
|
||||
/// once-only spawn: `spawn_poll_task` checks-and-sets it under the mutex.
|
||||
/// `Mutex` (not `RwLock`) because the only operation is "take it once."
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub sessions: Arc<DashMap<ChannelId, Arc<Mutex<RtcSession>>>>,
|
||||
pub poll_running: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sessions: Arc::new(DashMap::new()),
|
||||
poll_running: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint a fresh `RtcSession`, store it under its `ChannelId`, return the id.
|
||||
pub fn create_session(&self) -> Result<ChannelId, RtcSessionError> {
|
||||
let session = RtcSession::new()?;
|
||||
let id = session.channel_id();
|
||||
self.sessions.insert(id, Arc::new(Mutex::new(session)));
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Look up a session by id (returns the clone of the Arc-wrapped Mutex).
|
||||
pub fn get(&self, id: ChannelId) -> Option<Arc<Mutex<RtcSession>>> {
|
||||
self.sessions.get(&id).map(|r| r.clone())
|
||||
}
|
||||
|
||||
/// Transition to Closing then drop the entry (spec §4.1 — DELETE).
|
||||
pub async fn close(&self, id: ChannelId) {
|
||||
if let Some((_id, session_arc)) = self.sessions.remove(&id) {
|
||||
let mut s = session_arc.lock().await;
|
||||
s.channel.state = ChannelState::Closing;
|
||||
s.channel.state = ChannelState::Closed;
|
||||
info!(channel_id = %id, "session closed via DELETE");
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the single poll task for all sessions (idempotent).
|
||||
pub async fn spawn_poll_task(self) {
|
||||
let mut running = self.poll_running.lock().await;
|
||||
if *running {
|
||||
return;
|
||||
}
|
||||
*running = true;
|
||||
drop(running);
|
||||
|
||||
let state = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(10));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let now = Instant::now();
|
||||
drive_all_sessions(&state, now).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// One iteration of "drive every active session." Removes closed entries.
|
||||
async fn drive_all_sessions(state: &AppState, now: Instant) {
|
||||
// Collect ids first to avoid holding the DashMap shard during the
|
||||
// async poll (which would block other handlers mutating the same shard).
|
||||
let ids: Vec<ChannelId> = state.sessions.iter().map(|r| *r.key()).collect();
|
||||
for id in ids {
|
||||
let session_arc = match state.sessions.get(&id) {
|
||||
Some(r) => r.clone(),
|
||||
None => continue,
|
||||
};
|
||||
let mut s = session_arc.lock().await;
|
||||
let _ = s.run_poll_once(now); // hot-path match-and-continue inside
|
||||
if s.is_closed() {
|
||||
drop(s);
|
||||
state.sessions.remove(&id);
|
||||
debug!(channel_id = %id, "session evicted after close");
|
||||
}
|
||||
}
|
||||
}
|
||||
79
crates/rutster/static/index.html
Normal file
79
crates/rutster/static/index.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Rutster slice-1 — WebRTC loopback</title>
|
||||
<style>
|
||||
body { font: 14px/1.4 system-ui, sans-serif; max-width: 60ch; margin: 2rem auto; }
|
||||
pre { background: #f4f4f4; padding: 1rem; overflow: auto; }
|
||||
button { font: inherit; padding: 0.4rem 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rutster slice-1 — WebRTC loopback</h1>
|
||||
<p>Speak; you should hear yourself back within ~200 ms.</p>
|
||||
<button id="start">Start call</button>
|
||||
<button id="mute" disabled>Mute mic</button>
|
||||
<button id="hangup" disabled>Hang up</button>
|
||||
<pre id="log"></pre>
|
||||
<audio id="audio" autoplay></audio>
|
||||
|
||||
<script>
|
||||
const log = (s) => { document.getElementById('log').textContent += s + '\n'; };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let pc, sessionId, localStream;
|
||||
|
||||
$('start').onclick = async () => {
|
||||
$('start').disabled = true;
|
||||
$('mute').disabled = false;
|
||||
$('hangup').disabled = false;
|
||||
localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
// No STUN (host candidates only — spec §4.4, zero-dependency dev loop).
|
||||
pc = new RTCPeerConnection({ iceServers: [] });
|
||||
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
|
||||
|
||||
pc.oniceconnectionstatechange = () => log('ICE: ' + pc.iceConnectionState);
|
||||
pc.onconnectionstatechange = () => log('conn: ' + pc.connectionState);
|
||||
pc.ontrack = (e) => { $('audio').srcObject = e.streams[0]; };
|
||||
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
// Non-trickle: wait for ICE gathering to complete so all candidates
|
||||
// travel in the SDP offer POST.
|
||||
await new Promise(r => {
|
||||
if (pc.iceGatheringState === 'complete') r();
|
||||
else { pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') r(); }; };
|
||||
});
|
||||
|
||||
const resp = await fetch('/v1/sessions', { method: 'POST' });
|
||||
const { session_id } = await resp.json();
|
||||
sessionId = session_id;
|
||||
log('session: ' + session_id);
|
||||
|
||||
const ans = await fetch(`/v1/sessions/${session_id}/offer`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/sdp' },
|
||||
body: pc.localDescription.sdp,
|
||||
});
|
||||
const answerSdp = await ans.text();
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
||||
};
|
||||
|
||||
$('mute').onclick = () => {
|
||||
const track = localStream.getAudioTracks()[0];
|
||||
track.enabled = !track.enabled;
|
||||
$('mute').textContent = track.enabled ? 'Mute mic' : 'Unmute mic';
|
||||
};
|
||||
|
||||
$('hangup').onclick = async () => {
|
||||
await fetch(`/v1/sessions/${sessionId}`, { method: 'DELETE' });
|
||||
pc.close();
|
||||
$('hangup').disabled = true;
|
||||
$('mute').disabled = true;
|
||||
$('start').disabled = false;
|
||||
log('hung up');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
49
crates/rutster/tests/api_integration.rs
Normal file
49
crates/rutster/tests/api_integration.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Integration test for the slice-1 REST surface (spec §4.1, §6.4).
|
||||
//!
|
||||
//! Spins up the axum app on an ephemeral port, then exercises the API
|
||||
//! contract: POST /v1/sessions → JSON { session_id }; GET / serves a
|
||||
//! text/html page. We do NOT exercise the WebRTC handshake here (that
|
||||
//! needs a real peer); the manual e2e plan in README.md covers it.
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use rutster::session_map::AppState;
|
||||
use tower::ServiceExt; // enables `oneshot` on the Router for sync tests
|
||||
|
||||
#[tokio::test]
|
||||
async fn post_v1_sessions_returns_a_session_id() {
|
||||
let app = rutster::routes::router(AppState::new());
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/v1/sessions")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert!(v["session_id"].is_string(), "response has session_id");
|
||||
assert_eq!(v["session_id"].as_str().unwrap().len(), 36); // UUID v4
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_root_serves_html() {
|
||||
let app = rutster::routes::router(AppState::new());
|
||||
let resp = app
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get("content-type")
|
||||
.map(|v| v.to_str().unwrap()),
|
||||
Some("text/html; charset=utf-8")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user