diff --git a/Cargo.lock b/Cargo.lock index 2fae3e7..d162959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,33 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.7.7" @@ -203,6 +230,33 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "bambu-stream" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "env_logger", + "futures-util", + "http-auth", + "lazy_static", + "log", + "maplit", + "nom", + "openh264", + "rcgen", + "rtp", + "rtp-types", + "rustls 0.23.14", + "rustls-pemfile", + "sdl2", + "tokio", + "tokio-rustls 0.26.0", + "url", + "webpki-roots", + "webrtc-util", +] + [[package]] name = "bambulabs" version = "0.1.0" @@ -247,6 +301,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -301,9 +378,20 @@ version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -323,6 +411,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -365,6 +464,15 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.2" @@ -633,6 +741,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -666,6 +780,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -760,6 +897,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -950,6 +1093,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -983,6 +1141,21 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "base64 0.22.1", + "digest", + "hex", + "md-5", + "memchr", + "rand", + "sha2", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1208,6 +1381,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1223,6 +1405,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -1238,12 +1429,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libmdns" version = "0.9.1" @@ -1375,6 +1582,12 @@ dependencies = [ "uuid 1.11.0", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "match_cfg" version = "0.1.0" @@ -1396,6 +1609,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1467,6 +1690,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "moonraker" version = "0.1.0" @@ -1517,6 +1746,12 @@ dependencies = [ "rand", ] +[[package]] +name = "nasm-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4d98d0065f4b1daf164b3eafb11974c94662e5e2396cf03f32d0bb5c17da51" + [[package]] name = "native-tls" version = "0.2.12" @@ -1655,6 +1890,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "openh264" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73724323f3c6953c46e5f1c69df1c909a7c6a9cd5079ffd24998e95460c9ba45" +dependencies = [ + "openh264-sys2", +] + +[[package]] +name = "openh264-sys2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec8a969591260a0cd308e5a793773fbcfcd9ef591206d11de88cecebe119800" +dependencies = [ + "cc", + "nasm-rs", + "walkdir", +] + [[package]] name = "openssl" version = "0.10.66" @@ -1824,6 +2079,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1868,6 +2133,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "portpicker" version = "0.1.1" @@ -1902,6 +2173,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.87" @@ -1951,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "syn", @@ -1976,7 +2257,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.0.0", "rustls 0.23.14", "socket2", "thiserror", @@ -1993,7 +2274,7 @@ dependencies = [ "bytes", "rand", "ring", - "rustc-hash", + "rustc-hash 2.0.0", "rustls 0.23.14", "slab", "thiserror", @@ -2053,6 +2334,19 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -2181,6 +2475,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rtp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6870f09b5db96f8b9e7290324673259fd15519ebb7d55acf8e7eb044a9ead6af" +dependencies = [ + "bytes", + "portable-atomic", + "rand", + "serde", + "thiserror", + "webrtc-util", +] + +[[package]] +name = "rtp-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01b38bb7fd9425628876786934ade84ec5cb63905804f073583e6554d33f9af" +dependencies = [ + "smallvec", + "thiserror", +] + [[package]] name = "rumqttc" version = "0.24.0" @@ -2205,6 +2523,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.0.0" @@ -2244,6 +2568,8 @@ version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -2286,6 +2612,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2303,6 +2630,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.26" @@ -2346,6 +2682,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdl2" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2484,6 +2843,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3264,6 +3634,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + [[package]] name = "version_check" version = "0.9.5" @@ -3279,6 +3655,16 @@ dependencies = [ "atomic-waker", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3390,6 +3776,39 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webrtc-util" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8d9bc631768958ed97b8d68b5d301e63054ae90b09083d43e2fefb939fd77e" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "libc", + "log", + "nix", + "portable-atomic", + "rand", + "thiserror", + "tokio", + "winapi", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3406,6 +3825,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3624,6 +4052,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 4e28324..92f3144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,7 @@ edition = "2021" panic = "abort" [workspace] -members = [ - "bambulabs", - "moonraker" -] +members = ["bambulabs", "moonraker", "bambu-stream"] [features] default = ["bambu", "formlabs", "moonraker", "serial"] @@ -42,7 +39,10 @@ opentelemetry = "0.25.0" opentelemetry-otlp = "0.25.0" opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] } prometheus-client = "0.22.3" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } schemars = { version = "0.8", features = ["chrono", "uuid1", "bigdecimal"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -52,13 +52,30 @@ slog-async = "2.7.0" slog-json = "2.6.1" slog-term = "2.9.1" thiserror = "1.0.64" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net"] } -tokio-serial = { version = "5", optional = true, features = ["tokio-util", "libudev"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "macros", + "time", + "net", +] } +tokio-serial = { version = "5", optional = true, features = [ + "tokio-util", + "libudev", +] } toml = "0.8.19" tracing = "0.1" tracing-opentelemetry = "0.26.0" tracing-slog = "0.3.0" -tracing-subscriber = { version = "0.3.18", features = ["registry", "std", "fmt", "smallvec", "ansi", "tracing-log", "json", "env-filter"] } +tracing-subscriber = { version = "0.3.18", features = [ + "registry", + "std", + "fmt", + "smallvec", + "ansi", + "tracing-log", + "json", + "env-filter", +] } uuid = "1.11.0" [dev-dependencies] diff --git a/bambu-stream/.gitignore b/bambu-stream/.gitignore new file mode 100644 index 0000000..1a4595e --- /dev/null +++ b/bambu-stream/.gitignore @@ -0,0 +1,2 @@ +/target +*.log diff --git a/bambu-stream/Cargo.toml b/bambu-stream/Cargo.toml new file mode 100644 index 0000000..affe4b6 --- /dev/null +++ b/bambu-stream/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bambu-stream" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio-rustls = "0.26.0" +rcgen = { version = "0.13", features = ["pem"] } +tokio = { version = "1.0", features = ["full"] } +futures-util = "0.3.1" +lazy_static = "1.1" +webpki-roots = "0.26" +rustls-pemfile = "2" +env_logger = "0.11.5" +log = "0.4.22" +rustls = "0.23.12" +url = "2.5.2" +anyhow = "1.0.86" +http-auth = "0.1.9" +openh264 = "0.6.2" +sdl2 = "0.37.0" +nom = "7.1.3" +maplit = "1.0.2" +rtp-types = "0.1.1" +rtp = "0.11.0" +bytes = "1.7.1" +webrtc-util = "0.9.0" diff --git a/bambu-stream/NOTES.md b/bambu-stream/NOTES.md new file mode 100644 index 0000000..d19bf74 --- /dev/null +++ b/bambu-stream/NOTES.md @@ -0,0 +1,173 @@ +# RTP + + + +Stuff is sent in network byte order, i.e. big-endian. + +To be able to decrypt RTSP frames in Wireshark, we can use `ffplay`: + +```bash +export SSLKEYLOGFILE=~/ssl-keys.log +ffplay "rtsps://bblp:192190e7@192.168.0.96:322/streaming/live/1" +``` + +Then go into the Wireshark TLS settings and select the log file to use. + +## Payload type 96: "dynamic" + +, +[RFC3551](https://www.rfc-editor.org/rfc/rfc3551.html). + +## Header + +The original diagram in the spec is really annoying as it works in base 10. I want bytes! So here +they are: + +``` + 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|V=2|P|X| CC |M| PT | sequence number | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| timestamp | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| synchronization source (SSRC) identifier | ++=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +| contributing source (CSRC) identifiers | +| .... | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +It seems that most frames apart from a couple at the start begin with `80 60 c3`, then what I +_reckon_ is the low byte of the sequence counter in the next byte. This smells like an RTP header, +although none of the fixed values (like `V=2`) seem to match up. The preceding stuff like +`24 00 05 ac` might be some kind of frame delimiter? Or some custom stuff by the LIVE555 streaming +crap? I dunno. + +# Figuring out the stream format + +The normal `00 00 00 01` NALU packet delimiter doesn't exist in any of the data I've captured from +either my Rust code or `ffplay`. + +A Wireshark dump shows small frames like this from `ffplay`: + +``` +24 00 05 ac +``` + +`05 ac` is `1452` in decimal, which is the length of the next chunk of data captured. Does this mean +that the stream is in fact AVCC? The `24 00` part is still a mystery. + +Lining up `24 00` with the header diagram above we get: + +``` + 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +|V=2|P|X| CC |M| PT | + +Rust print: + 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 + +Each byte mirrored: + 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 +``` + +Which doesn't make much sense... + +What about `80 60`? + +``` + 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +|V=2|P|X| CC |M| PT | + +Rust print: + 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 + +Each byte mirrored: + 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 +``` + +--- + +Every chunk seems to have a common header of `80, 60, 5d, a2, 4a, d4, 97, f9, d4, 9e, 7f, e1` after +the weird 4 byte header. + +--- + +According to : + +> AVCC is best option for: a stored file (with known sizes & offsets). + +But how much do you want to bet that Bambu just threw it into their live streaming setup? The +LIVE555 website implies it prefers streaming files, so maybe that's why. + +First 4 bytes of an AVCC frame are called "extradata" or "sequence header" + + +--- + +From ffmpeg/ffplay in the DESCRIBE response: + +``` +v=0 +o=- 1723111495901673 1 IN IP4 192.168.0.96 +s=rtsp stream server +i=Thu Aug 8 11:04:55 2024 + +t=0 0 +a=tool:LIVE555 Streaming Media v2023.03.30 +a=type:broadcast +a=control:* +a=range:npt=now- +a=x-qt-text-nam:rtsp stream server +a=x-qt-text-inf:Thu Aug 8 11:04:55 2024 + +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +b=AS:17186 +a=rtpmap:96 H264/90000 +a=fmtp:96 packetization-mode=1;profile-level-id=42C01F;sprop-parameter-sets=Z0LAH42NUCSC2TZAAAADAEAAAA8jwiEagA==,aM4xsg== +a=control:track1 +``` + +This is an SDP (`application/sdp`) formatted response. I can test them with VLC: + + +The last little base64 `aM4xsg==` decodes to (hex): + +``` +68,CE,31,B2 +``` + +This is present in the bytes I get in the Rust code! No idea what this means... + +--- + +The longer `Z0LAH42NUCSC2TZAAAADAEAAAA8jwiEagA==` (SPS and PPS?) is: + +``` +67,42,C0,1F,8D,8D,50,24,82,D9,36,40,00,00,03,00,40,00,00,0F,23,C2,21,1A,80 +``` + +which is indeed present as well. + +Some more info here + +The received frame starts like this: + +``` +[ 24, 00, 00, 25 ], 80, 60, 07, 8f, 84, b4, 4d, 39, 5b, 6e, 5f, 94, [ 67, 42, c0, 1f, 8d, 8d, 50, 24, 82, d9, 36, 40, 00, 00, 03, 00, 40, 00, 00, 0f, 23, c2, 21 ] +``` + +--- + +`ffmpeg` says + +``` +Input #0, rtsp, from 'rtsps://bblp:192190e7@192.168.0.96:322/streaming/live/1': + Metadata: + title : rtsp stream server + comment : Sat Aug 10 00:25:18 2024 + Duration: N/A, start: 0.031978, bitrate: N/A + Stream #0:0, 21, 1/90000: Video: h264 (Constrained Baseline), 1 reference frame, yuvj420p(pc, progressive, left), 1168x720, 0/1, 30 fps, 30 tbr, 90k tb +``` + +Profile is `66d` with constrained bit set to 1 diff --git a/bambu-stream/README.md b/bambu-stream/README.md new file mode 100644 index 0000000..0103c29 --- /dev/null +++ b/bambu-stream/README.md @@ -0,0 +1,15 @@ +# Bambu X1 Carbon RTSP streaming + +Authenticates with a Bambu X1 Carbon RTSP stream and shows the frames in an SDL2 window. The printer +IP and access code are currently hard coded. SDL2 can be a bit of a pain to get working on macOS but +[this](https://github.com/embedded-graphics/simulator?tab=readme-ov-file#macos-brew) works well for +me at least. + +```bash +cargo run --release +``` + +`RUST_LOG=debug` can be useful for debugging RTSP auth issues. + +Note that exiting the program doesn't work very well, even though to my knowledge the SDL event loop +isn't blocked. Try using `killall bambu-steam` on Linux. diff --git a/bambu-stream/sample-handshake.txt b/bambu-stream/sample-handshake.txt new file mode 100644 index 0000000..ba0d376 --- /dev/null +++ b/bambu-stream/sample-handshake.txt @@ -0,0 +1,81 @@ +From `ffplay "rtsps://bblp:192190e7@192.168.0.96:322/streaming/live/1"` + +OPTIONS rtsps://192.168.0.96:322/streaming/live/1 RTSP/1.0 +CSeq: 1 +User-Agent: Lavf60.16.100 + +RTSP/1.0 200 OK +CSeq: 1 +Date: Thu, Aug 08 2024 10:57:17 GMT +Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER + +DESCRIBE rtsps://192.168.0.96:322/streaming/live/1 RTSP/1.0 +Accept: application/sdp +CSeq: 2 +User-Agent: Lavf60.16.100 + +RTSP/1.0 401 Unauthorized +CSeq: 2 +Date: Thu, Aug 08 2024 10:57:17 GMT +WWW-Authenticate: Digest realm="LIVE555 Streaming Media", nonce="db860ca0377a9fe8769112644ba5db76" + +DESCRIBE rtsps://192.168.0.96:322/streaming/live/1 RTSP/1.0 +Accept: application/sdp +CSeq: 3 +User-Agent: Lavf60.16.100 +Authorization: Digest username="bblp", realm="LIVE555 Streaming Media", nonce="db860ca0377a9fe8769112644ba5db76", uri="rtsps://192.168.0.96:322/streaming/live/1", response="142ead4ed9c3a58148d54eee6b4d0715" + +RTSP/1.0 200 OK +CSeq: 3 +Date: Thu, Aug 08 2024 10:57:17 GMT +Content-Base: rtsps://192.168.0.96/streaming/live/1/ +Content-Type: application/sdp +Content-Length: 496 + +v=0 +o=- 1723111495901673 1 IN IP4 192.168.0.96 +s=rtsp stream server +i=Thu Aug 8 11:04:55 2024 + +t=0 0 +a=tool:LIVE555 Streaming Media v2023.03.30 +a=type:broadcast +a=control:* +a=range:npt=now- +a=x-qt-text-nam:rtsp stream server +a=x-qt-text-inf:Thu Aug 8 11:04:55 2024 + +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +b=AS:17186 +a=rtpmap:96 H264/90000 +a=fmtp:96 packetization-mode=1;profile-level-id=42C01F;sprop-parameter-sets=Z0LAH42NUCSC2TZAAAADAEAAAA8jwiEagA==,aM4xsg== +a=control:track1 + +SETUP rtsps://192.168.0.96/streaming/live/1/track1 RTSP/1.0 +Transport: RTP/AVP/TCP;unicast;interleaved=0-1 +CSeq: 4 +User-Agent: Lavf60.16.100 +Authorization: Digest username="bblp", realm="LIVE555 Streaming Media", nonce="db860ca0377a9fe8769112644ba5db76", uri="rtsps://192.168.0.96/streaming/live/1/track1", response="5bb1433eef289f517fbcce998eb868ec" + +RTSP/1.0 200 OK +CSeq: 4 +Date: Thu, Aug 08 2024 10:57:17 GMT +Transport: RTP/AVP/TCP;unicast;destination=192.168.0.74;source=192.168.0.96;interleaved=0-1 +Session: 061546E4;timeout=10 + +PLAY rtsps://192.168.0.96/streaming/live/1/ RTSP/1.0 +Range: npt=0.000- +CSeq: 5 +User-Agent: Lavf60.16.100 +Session: 061546E4 +Authorization: Digest username="bblp", realm="LIVE555 Streaming Media", nonce="db860ca0377a9fe8769112644ba5db76", uri="rtsps://192.168.0.96/streaming/live/1/", response="a5d9483ecc37519ee2c1f270b2a38e94" + +// Some binary crap here, maybe video frames that start coming in? + +RTSP/1.0 200 OK +CSeq: 5 +Date: Thu, Aug 08 2024 10:57:17 GMT +Range: npt=0.000- +Session: 061546E4 +RTP-Info: url=rtsps://192.168.0.96/streaming/live/1/track1;seq=61243;rtptime=665680901 diff --git a/bambu-stream/src/main.rs b/bambu-stream/src/main.rs new file mode 100644 index 0000000..d83a673 --- /dev/null +++ b/bambu-stream/src/main.rs @@ -0,0 +1,159 @@ +mod no_auth; +mod rtsps; + +use bytes::Bytes; +use nom::bytes::streaming::take_until; +use openh264::{decoder::Decoder, formats::YUVSource}; +use rtp::packetizer::Depacketizer; +use rtsps::Rtsps; +use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; +use webrtc_util::marshal::{MarshalSize, Unmarshal}; + +/// Parse a packet beginning with a 4 byte header: `0x24, 0x00, len low, len high` and return the payload. +/// +/// Not sure what the `0x2400` represents as I can't find this structure in any of the RTP/H.264 +/// docs, but it delimits every RTP frame. +fn parse_packet<'buf>(i: &'buf [u8]) -> nom::IResult<&'buf [u8], Bytes> { + use nom::{ + bytes::streaming::{tag, take}, + number::streaming::be_u16, + }; + + // Discard any preceding bytes before the start marker + let (i, _before) = take_until(&[0x24u8, 0x00][..])(i)?; + + // Start marker 0x2400 (big endian) + let (i, _marker) = tag([0x24u8, 0x00])(i)?; + + // Data length + let (i, len) = be_u16(i)?; + + let len = usize::from(len); + + // Main data payload + let (i, chunk) = take(len)(i)?; + + Ok((i, Bytes::from(chunk.to_vec()))) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let mut stream = Rtsps::new( + // User is hard coded to `bblp`. Password is printer access code. + "rtsps://bblp:192190e7@192.168.0.96:322/streaming/live/1", + ) + .await?; + + let mut openh264 = Decoder::new()?; + + stream.open_stream("/streaming/live/1").await?; + + for _ in 0..32 { + stream.read_more().await?; + } + + // --- + + let sdl_context = sdl2::init().expect("Error sdl2 init"); + let video_subsystem = sdl_context.video().expect("Error sld2 video subsystem"); + + let window = video_subsystem + .window("Bambu stream", 1168, 720) + .position_centered() + .opengl() + .build()?; + + let mut canvas = window.into_canvas().build()?; + let texture_creator = canvas.texture_creator(); + + let mut texture = texture_creator.create_texture_static(PixelFormatEnum::IYUV, 1168, 720)?; + let mut event_pump = sdl_context.event_pump().expect("Error sld2 event"); + + // --- + + let mut decoder = rtp::codecs::h264::H264Packet::default(); + + let mut b = Vec::new(); + + while let Ok(next_chunk) = stream.read_more().await { + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => { + log::info!("Exiting..."); + + break; + } + _ => {} + } + } + + b.extend_from_slice(next_chunk); + + let (rest, mut chunk) = match parse_packet(&b) { + Ok(res) => res, + Err(nom::Err::Incomplete(needed)) => { + log::debug!("Need {:?} more bytes", needed); + + continue; + } + Err(nom::Err::Error(e)) => { + log::warn!("Nom error {:?}", e); + + continue; + } + Err(nom::Err::Failure(e)) => { + log::error!("Nom failure {:?}", e); + + return Err(anyhow::anyhow!("Packet parse failure: {:?}", e.code)); + } + }; + + log::debug!("Chunk len {}, rest {}", chunk.len(), rest.len()); + + // Strip first successfully parsed chunk from the beginning of the buffer + b = rest.to_vec(); + + if let Ok(p) = rtp::packet::Packet::unmarshal(&mut chunk) { + log::debug!( + "Decoded a packet SN {}, TS {}, PT {} payload {} first 32 bytes: {:02x?}", + p.header.sequence_number, + p.header.timestamp, + p.header.payload_type, + p.marshal_size(), + &p.payload[0..p.payload.len().min(32)] + ); + + match decoder.depacketize(&p.payload) { + Ok(bytes) if !bytes.is_empty() => { + if let Ok(Some(f)) = openh264.decode(&bytes) { + log::info!("Got a frame: {:?}", f.dimensions()); + + let (y_size, u_size, v_size) = f.strides(); + + texture.update_yuv(None, f.y(), y_size, f.u(), u_size, f.v(), v_size)?; + + canvas.clear(); + + canvas + .copy(&texture, None, None) + .expect("Error copying texture"); + + canvas.present(); + } + } + Ok(_) => (), + Err(e) => log::error!("Depacketize error {:?}", e), + } + } else { + log::error!("Failed to unmarshal chunk {}", chunk.len()); + } + } + + Ok(()) +} diff --git a/bambu-stream/src/no_auth.rs b/bambu-stream/src/no_auth.rs new file mode 100644 index 0000000..ca58395 --- /dev/null +++ b/bambu-stream/src/no_auth.rs @@ -0,0 +1,61 @@ +//! FIXME: Dedupe this with the `no_auth` file in `machine-api`. + +/// A no-op implementation of `ServerCertVerifier` that accepts any certificate. +#[derive(Debug)] +pub(crate) struct NoAuth {} + +impl NoAuth { + /// Creates a new `NoAuth` instance. + pub(crate) fn new() -> Self { + Self {} + } +} + +impl rustls::client::danger::ServerCertVerifier for NoAuth { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA1, + rustls::SignatureScheme::ECDSA_SHA1_Legacy, + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::ED448, + ] + } +} diff --git a/bambu-stream/src/rtsps.rs b/bambu-stream/src/rtsps.rs new file mode 100644 index 0000000..0112366 --- /dev/null +++ b/bambu-stream/src/rtsps.rs @@ -0,0 +1,523 @@ +use anyhow::Result; +use core::fmt; +use maplit::btreemap; +use nom::branch::alt; +use nom::bytes::streaming::{tag, take_until}; +use nom::character::complete::not_line_ending; +use nom::character::streaming::{crlf, space1}; +use nom::combinator::map; +use nom::multi::separated_list0; +use nom::sequence::separated_pair; +use nom::IResult; +use rustls::pki_types::ServerName; +use std::collections::BTreeMap; +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt, ErrorKind}; +use tokio::net::TcpStream; +use tokio_rustls::client::TlsStream; +use tokio_rustls::TlsConnector; +use url::Url; + +#[derive(Copy, Clone, Debug)] +pub enum Methods { + Options, + Describe, + Setup, + Play, + Teardown, +} + +impl fmt::Display for Methods { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Methods::Options => f.write_str("OPTIONS"), + Methods::Describe => f.write_str("DESCRIBE"), + Methods::Setup => f.write_str("SETUP"), + Methods::Play => f.write_str("PLAY"), + Methods::Teardown => f.write_str("TEARDOWN"), + } + } +} + +// Don't need this: the methods aren't returned as part of the standard header. We may require this +// if we start parsing the OPTIONS response. +// impl Methods { +// fn parse(i: &[u8]) -> IResult<&[u8], Self> { +// alt(( +// map(tag(b"OPTIONS"), |_| Self::Options), +// map(tag(b"DESCRIBE"), |_| Self::Describe), +// map(tag(b"SETUP"), |_| Self::Setup), +// map(tag(b"PLAY"), |_| Self::Play), +// map(tag(b"TEARDOWN"), |_| Self::Teardown), +// ))(i) +// } +// } + +pub struct Rtsps { + cseq: u32, + tcp_addr: SocketAddr, + pub stream: TlsStream, + auth: Option, + pub frame_buffer: Vec, + temp_buffer: Vec, +} + +impl Rtsps { + pub async fn new(addr: &str) -> Result { + let url = Url::parse(addr); + + let socket_addr = match url.clone() { + Ok(parsed_addr) => parsed_addr.socket_addrs(|| None)?, + Err(e) => panic!("Trying to parse {addr} resulted in {e}"), + }; + + let mut config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(crate::no_auth::NoAuth::new())) + .with_no_client_auth(); + + config.key_log = Arc::new(rustls::KeyLogFile::new()); + + let connector = TlsConnector::from(Arc::new(config)); + + let tcp_stream = TcpStream::connect(socket_addr[0]).await?; + + let domain = ServerName::try_from(url.unwrap().domain().expect("No domain")) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid dnsname"))? + .to_owned(); + + let tcp_stream = connector.connect(domain, tcp_stream).await?; + + log::debug!("Connecting to server at: {}", socket_addr[0]); + + Ok(Self { + tcp_addr: socket_addr[0], + stream: tcp_stream, + auth: None, + cseq: 0, + frame_buffer: Vec::new(), + temp_buffer: vec![0u8; 4096], + }) + } + + pub async fn open_stream(&mut self, path: &str) -> Result<()> { + log::info!("Open stream to {}", path); + + // Send OPTIONS + self.request_response(Methods::Options, BTreeMap::new(), &path).await?; + + // Send DESCRIBE + let _headers = self + .authenticated_request_response(Methods::Describe, BTreeMap::new(), &path) + .await?; + + // Hard code stream location + // TODO: Extract stream path from `Content-Base: rtsps://192.168.0.96/streaming/live/1/` + // header and `a=control:track1` in body + let track_path = format!("{}/track1", path); + + // Send SETUP with `Transport`, etc headers + let headers = self + .authenticated_request_response( + Methods::Setup, + btreemap! { + // Value hard coded from `ffplay` Wireshark capture + "Transport" => "RTP/AVP/TCP;unicast;interleaved=0-1".to_string() + }, + &track_path, + ) + .await?; + + // Extract session token from SETUP response + let session_token = headers + .headers + .get("Session") + // Extract e.g. "061546E4" from "061546E4;timeout=10" + .and_then(|v| v.split_once(';')) + .map(|(session_token, _rest)| session_token.to_string()) + .ok_or_else(|| anyhow::anyhow!("No session token found in response"))?; + + // Hard code range to beginning of stream + let range = "npt=0.000-".to_string(); + + self.frame_buffer.clear(); + + // Send PLAY with session token and range header + let _headers = self + .authenticated_request_response( + Methods::Play, + btreemap! { + "Session" => session_token, + "Range" => range + }, + &path, + ) + .await?; + + Ok(()) + } + + async fn authenticated_request_response( + &mut self, + method: Methods, + extra_headers: BTreeMap<&str, String>, + track: &str, + ) -> Result { + // Attempt request + let headers = self.request_response(method, extra_headers.clone(), track).await?; + + // If unauthorised, authenticate, then try original request again + let headers = if headers.status == RtpStatusCode::Unauthorized { + log::debug!("--> Request is unauthorised"); + + let auth_header = headers + .headers + .get("WWW-Authenticate") + .expect("Needs digest auth, but no digest header present"); + + log::debug!("----> Digest header: {}", auth_header); + + // https://github.com/scottlamb/http-auth/blob/main/examples/reqwest.rs + + let mut pw_client = http_auth::PasswordClient::try_from(auth_header.as_str()).expect("Password client"); + + log::debug!("----> Password client {:?}", pw_client); + + let username = "bblp"; + // TODO: Pass in password from config + let password = "192190e7"; + + let authorization = pw_client + .respond(&http_auth::PasswordParams { + username, + password, + uri: &format!("rtsps://{}{}", self.tcp_addr.ip(), track), + method: &method.to_string(), + body: Some(&[]), + }) + .expect("Respond"); + + // Cache auth for next time + self.auth = Some(authorization.clone()); + + log::debug!("----> Auth reply {:?}", authorization); + + // Make request again, this time with an additional authorization header. + self.request_response(method, extra_headers, track).await? + } else { + headers + }; + + if headers.status == RtpStatusCode::Unauthorized { + Err(anyhow::anyhow!("Failed to authorise")) + } else { + Ok(headers) + } + } + + /// Send a request and return its response headers and body + async fn request_response( + &mut self, + method: Methods, + mut extra_headers: BTreeMap<&str, String>, + track: &str, + ) -> Result { + self.cseq += 1; + + let method_str = method.to_string(); + + log::debug!("Send {} request", method_str); + + let mut headers = btreemap! { + "CSeq" => self.cseq.to_string(), + "User-Agent" => "Machine-Api".to_string(), + }; + + if let Some(auth) = self.auth.as_ref() { + headers.insert("Authorization", auth.clone()); + } + + headers.append(&mut extra_headers); + + let headers = headers + .into_iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join("\r\n"); + + // NOTE: Double newline (\r\n\r\n) delimits header and request body (which is always empty + // for RTP control messages) + let request = format!( + "{} rtsps://{}{} RTSP/1.0\r\n{}\r\n\r\n", + method_str, self.tcp_addr, track, headers + ); + + log::debug!("--> Request\n\n{}", request); + + self.stream.get_mut().0.writable().await?; + + self.stream.write_all(request.as_bytes()).await?; + + let mut buf_size = 0; + let mut arr = [0u8; 4096]; + let mut buf = arr.as_mut_slice(); + + let headers = loop { + match self.stream.read(&mut buf).await { + Ok(n) => { + buf_size += n; + + match RtspResponse::parse(&buf[0..buf_size]) { + Ok((_rest, response)) => { + break response; + } + Err(e) => match e { + nom::Err::Incomplete(more) => { + log::debug!("Waiting for more {:?}", more) + } + nom::Err::Error(e) => { + log::debug!("Error {:?}", e.code); + } + nom::Err::Failure(e) => { + log::error!("Parse failure: {:?}", e.code); + + return Err(anyhow::anyhow!("Response parse error: {:?}", e.code)); + } + }, + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => { + continue; + } + Err(e) => { + return Err(e.into()); + } + } + }; + + log::debug!("--> Response headers\n\n{}", headers); + + if headers.status == RtpStatusCode::NotFound { + return Err(anyhow::anyhow!("Path {} not found", track)); + } + + Ok(headers) + } + + pub async fn read_more(&mut self) -> Result<&[u8]> { + let mut buf = self.temp_buffer.as_mut_slice(); + + let n = self.stream.read(&mut buf).await?; + + let buf = &buf[0..n]; + + // self.frame_buffer.extend(&buf[..]); + + // dbg!(&self.frame_buffer[(self.frame_buffer.len() - 10)..]); + // dbg!(&buf[0..buf.len().min(10)]); + + Ok(buf) + } +} + +// RTSP header begins with this tag +const HEADER_START_TOKEN: &[u8] = b"RTSP/1.0"; +// Header and body are separated by two newlines +const HEADER_END_TOKEN: &[u8] = b"\r\n\r\n"; + +// TODO: More status codes from https://github.com/FFmpeg/FFmpeg/blob/deee00e2eb58710f21c1c8775702930bc4d9f86b/libavformat/rtspdec.c#L47-L58 +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u16)] +enum RtpStatusCode { + Ok = 200u16, + Unauthorized = 401, + NotFound = 404, + Other(u16), +} + +impl RtpStatusCode { + fn parse(i: &[u8]) -> IResult<&[u8], Self> { + alt(( + map(tag(b"200 OK"), |_| Self::Ok), + map(tag(b"401 Unauthorized"), |_| Self::Unauthorized), + map(tag(b"404 Stream Not Found"), |_| Self::NotFound), + map( + separated_pair(nom::character::streaming::u16, space1, not_line_ending), + |(n, _text_status)| Self::Other(n), + ), + ))(i) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct RtspResponse { + status: RtpStatusCode, + headers: BTreeMap, +} + +impl RtspResponse { + fn parse(i: &[u8]) -> IResult<&[u8], Self> { + let (i, _discard_preamble) = take_until(HEADER_START_TOKEN)(i)?; + + let (i, (_start_token, status)) = separated_pair(tag(HEADER_START_TOKEN), space1, RtpStatusCode::parse)(i)?; + + let (i, _) = crlf(i)?; + + let (i, headers) = Self::parse_pairs(i)?; + + Ok((i, Self { status, headers })) + } + + fn parse_pairs(i: &[u8]) -> IResult<&[u8], BTreeMap> { + let (i, headers) = separated_list0( + crlf, + map( + separated_pair( + nom::bytes::complete::take_until(": "), + nom::bytes::complete::tag(": "), + not_line_ending, + ), + |(key, value)| { + // Headers are ASCII (or the bits we care about are anyway), so we don't mind + // losing some fancy characters here + ( + String::from_utf8_lossy(key).trim().to_string(), + String::from_utf8_lossy(value).trim().to_string(), + ) + }, + ), + )(i)?; + + let headers = headers.into_iter().collect::>(); + + let (i, _header_body_separator) = tag(HEADER_END_TOKEN)(i)?; + + Ok((i, headers)) + } +} + +impl fmt::Display for RtspResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let headers = self + .headers + .iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join("\n"); + + write!(f, "{:?}\n{}", self.status, headers) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use maplit::btreemap; + + // Tests that we can parse a header out of packets interleaved with video frames + #[test] + fn parse_header_inside_rtp_stream() { + let preamble_garbage = vec![36u8, 1, 0, 48]; + + let header = b"RTSP/1.0 401 Unauthorized\r\nCSeq: 2\r\nDate: Fri, Aug 09 2024 14:00:40 GMT\r\nWWW-Authenticate: Digest realm=\"LIVE555 Streaming Media\", nonce=\"3b8d6b98cb67fb38af1cd3ae50ec393d\"\r\n\r\n".to_vec(); + + let postamble_garbage = vec![ + 36u8, 1, 0, 48, 128, 200, 0, 6, 116, 243, 37, 127, 234, 96, 159, 202, 2, 208, 111, 239, 158, 100, 214, 132, + 0, 0, 0, 0, 0, 0, 0, 0, 129, 202, 0, 4, 116, 243, 37, 127, 1, 7, + ]; + + let all = vec![preamble_garbage.clone(), header.clone(), postamble_garbage.clone()] + .iter() + .flatten() + .copied() + .collect::>(); + + let parsed = RtspResponse::parse(&all); + + let expected = ( + &postamble_garbage[..], + RtspResponse { + status: RtpStatusCode::Unauthorized, + headers: btreemap! { + "CSeq".to_string() => "2".to_string(), + "Date".to_string() => "Fri, Aug 09 2024 14:00:40 GMT".to_string(), + "WWW-Authenticate".to_string() => "Digest realm=\"LIVE555 Streaming Media\", nonce=\"3b8d6b98cb67fb38af1cd3ae50ec393d\"".to_string() + }, + }, + ); + + assert_eq!(parsed, Ok(expected)); + } + + #[test] + fn find_header_in_garbage() { + let input = &b"$0*mh`fb5.c*mhBL-P001RTSP/1.0 200 OK\r\nCSeq: 6\r\nDate: Fri, Aug 09 2024 15:27:28 GMT\r\nRange: npt=0.000-\r\nSession: A6A00543\r\nRTP-Info: url=rtsps://192.168.0.96/streaming/live/1/track1;seq=61748;rtptime=2162568838\r\n\r\n"[..]; + + let parsed = RtspResponse::parse(input); + + let expected = ( + &[][..], + RtspResponse { + status: RtpStatusCode::Ok, + headers: btreemap! { + "CSeq".to_string() => "6".to_string(), + "Date".to_string() => "Fri, Aug 09 2024 15:27:28 GMT".to_string(), + "Range".to_string() => "npt=0.000-".to_string(), + "Session".to_string() => "A6A00543".to_string(), + "RTP-Info".to_string() => "url=rtsps://192.168.0.96/streaming/live/1/track1;seq=61748;rtptime=2162568838".to_string() + }, + }, + ); + + assert_eq!(parsed, Ok(expected)); + } + + #[test] + fn unknown_status() { + assert_eq!( + RtpStatusCode::parse(b"418 Teapot"), + Ok((&[][..], RtpStatusCode::Other(418))) + ); + } + + #[test] + fn parse_key_values() { + let input = &b"CSeq: 1\r\nDate: Fri, Aug 09 2024 14:46:40 GMT\r\nPublic: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER\r\n\r\n"[..]; + + assert_eq!( + RtspResponse::parse_pairs(input), + Ok(( + &[][..], + btreemap! { + "CSeq".to_string() => "1".to_string(), + "Date".to_string() => "Fri, Aug 09 2024 14:46:40 GMT".to_string(), + "Public".to_string() => "OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER".to_string() + } + )) + ); + } + + #[test] + + fn parse_header() { + let header = &b"RTSP/1.0 401 Unauthorized\r\nCSeq: 2\r\nDate: Fri, Aug 09 2024 14:00:40 GMT\r\nWWW-Authenticate: Digest realm=\"LIVE555 Streaming Media\", nonce=\"3b8d6b98cb67fb38af1cd3ae50ec393d\"\r\n\r\n"[..]; + + let parsed = RtspResponse::parse(header); + + let expected = ( + &[][..], + RtspResponse { + status: RtpStatusCode::Unauthorized, + headers: btreemap! { + "CSeq".to_string() => "2".to_string(), + "Date".to_string() => "Fri, Aug 09 2024 14:00:40 GMT".to_string(), + "WWW-Authenticate".to_string() => "Digest realm=\"LIVE555 Streaming Media\", nonce=\"3b8d6b98cb67fb38af1cd3ae50ec393d\"".to_string() + }, + }, + ); + + assert_eq!(parsed, Ok(expected)); + } +}