diff --git a/Cargo.lock b/Cargo.lock index dd6ef5ffe..f7c031212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" -version = "0.6.0-rc.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.2.0-rc.4", - "inout 0.2.0-rc.6", + "crypto-common 0.1.7", + "generic-array", ] [[package]] @@ -42,13 +42,13 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686ba04dc80c816104c96cd7782b748f6ad58c5dd4ee619ff3258cf68e83d54" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes 0.9.0-rc.1", - "cipher 0.5.0-rc.1", + "aes 0.8.4", + "cipher 0.4.4", "ctr", "ghash", "subtle", @@ -56,12 +56,11 @@ dependencies = [ [[package]] name = "aes-kw" -version = "0.3.0-rc.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02eaa2d54d0fad0116e4b1efb65803ea0bf059ce970a67cd49718d87e807cb51" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" dependencies = [ - "aes 0.9.0-rc.1", - "const-oid 0.10.1", + "aes 0.8.4", ] [[package]] @@ -91,7 +90,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2477554ebf38aea815a9c4729100cfc32f766876c45b9c9c38ef221b9d1a703" dependencies = [ - "axum 0.8.8", + "axum 0.8.7", "axum-extra", "bytes 1.11.0", "cfg-if", @@ -122,6 +121,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstyle" version = "1.0.13" @@ -160,9 +165,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -170,14 +175,14 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 1.0.69", ] [[package]] name = "asn1-rs-derive" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", @@ -198,9 +203,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", @@ -285,15 +290,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -371,9 +367,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core 0.5.5", "base64 0.22.1", @@ -447,7 +443,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum 0.8.8", + "axum 0.8.7", "axum-core 0.5.5", "bytes 1.11.0", "form_urlencoded", @@ -722,12 +718,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - [[package]] name = "byteorder" version = "1.5.0" @@ -776,6 +766,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -796,9 +792,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.50" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -860,13 +856,40 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "inout 0.1.4", ] @@ -876,7 +899,6 @@ version = "0.5.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" dependencies = [ - "block-buffer 0.11.0-rc.5", "crypto-common 0.2.0-rc.4", "inout 0.2.0-rc.6", ] @@ -892,6 +914,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + [[package]] name = "cmake" version = "0.1.57" @@ -973,10 +1020,40 @@ dependencies = [ ] [[package]] -name = "critical-section" -version = "1.2.0" +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] [[package]] name = "crossbeam" @@ -1034,27 +1111,53 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1e6e5492f8f0830c37f301f6349e0dac8b2466e4fe89eef90e9eef906cd046" +dependencies = [ + "crypto-common 0.1.7", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.7.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4113edbc9f68c0a64d5b911f803eb245d04bb812680fd56776411f69c670f3e0" dependencies = [ - "hybrid-array", "num-traits", "rand_core 0.9.3", "serdect", "subtle", - "zeroize", ] [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1065,7 +1168,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" dependencies = [ "hybrid-array", - "rand_core 0.9.3", ] [[package]] @@ -1078,47 +1180,13 @@ dependencies = [ "subtle", ] -[[package]] -name = "crypto-primes" -version = "0.7.0-pre.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f2523fbb68811c8710829417ad488086720a6349e337c38d12fa81e09e50bf" -dependencies = [ - "crypto-bigint", - "libm", - "rand_core 0.9.3", -] - -[[package]] -name = "cryptoki" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781357a7779a8e92ea985121bbf379a9adf0777f44ab6392efc6abd5aa9b67db" -dependencies = [ - "bitflags 1.3.2", - "cryptoki-sys", - "libloading", - "log", - "paste", - "secrecy", -] - -[[package]] -name = "cryptoki-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "753e27d860277930ae9f394c119c8c70303236aab0ffab1d51f3d207dbb2bc4b" -dependencies = [ - "libloading", -] - [[package]] name = "ctr" -version = "0.10.0-rc.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e41d01c6f73b9330177f5cf782ae5b581b5f2c7840e298e0275ceee5001434" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.5.0-rc.1", + "cipher 0.4.4", ] [[package]] @@ -1128,20 +1196,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ "dispatch2", - "nix", + "nix 0.30.1", "windows-sys 0.61.2", ] [[package]] name = "curve25519-dalek" -version = "5.0.0-pre.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.11.0-rc.3", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -1194,26 +1262,15 @@ dependencies = [ "const-oid 0.9.6", "der_derive", "flagset", - "pem-rfc7468 0.7.0", - "zeroize", -] - -[[package]] -name = "der" -version = "0.8.0-rc.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" -dependencies = [ - "const-oid 0.10.1", - "pem-rfc7468 1.0.0-rc.3", + "pem-rfc7468", "zeroize", ] [[package]] name = "der-parser" -version = "10.0.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", @@ -1254,6 +1311,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "des" version = "0.9.0-rc.1" @@ -1270,7 +1336,6 @@ dependencies = [ "anyhow", "async-trait", "aws-lc-rs", - "bytes 1.11.0", "camino", "ceviche", "ctrlc", @@ -1282,7 +1347,6 @@ dependencies = [ "expect-test", "futures", "hex", - "http-client-proxy", "ironrdp", "notify-debouncer-mini", "parking_lot", @@ -1291,13 +1355,12 @@ dependencies = [ "rustls-pemfile 2.2.0", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "tap", "thiserror 2.0.17", "tokio 1.48.0", "tokio-rustls", "tracing", - "url", "uuid", "win-api-wrappers", "windows 0.61.3", @@ -1324,7 +1387,7 @@ dependencies = [ "anyhow", "argon2", "async-trait", - "axum 0.8.8", + "axum 0.8.7", "axum-extra", "backoff", "bitflags 2.10.0", @@ -1347,15 +1410,14 @@ dependencies = [ "hex", "hostname 0.4.2", "http-body-util", - "http-client-proxy", "hyper 1.8.1", "hyper-util", - "ironrdp-acceptor", - "ironrdp-connector", + "ironrdp-acceptor 0.6.0", + "ironrdp-connector 0.6.0", "ironrdp-core", - "ironrdp-pdu", + "ironrdp-pdu 0.5.0", "ironrdp-rdcleanpath", - "ironrdp-tokio", + "ironrdp-tokio 0.6.0", "jmux-proxy", "job-queue", "job-queue-libsql", @@ -1368,7 +1430,7 @@ dependencies = [ "parking_lot", "pcap-file", "picky", - "picky-krb", + "picky-krb 0.12.0", "pin-project-lite 0.2.16", "portpicker", "proptest", @@ -1380,7 +1442,7 @@ dependencies = [ "serde-querystring", "serde_json", "serde_urlencoded", - "sha2 0.10.9", + "sha2", "smol_str", "sysevent", "sysevent-codes", @@ -1453,7 +1515,7 @@ dependencies = [ "aide", "anyhow", "async-trait", - "axum 0.8.8", + "axum 0.8.7", "base16ct 0.2.0", "base64 0.22.1", "bb8", @@ -1475,7 +1537,7 @@ dependencies = [ "serde", "serde_json", "sha1 0.10.6", - "sha2 0.10.9", + "sha2", "tokio 1.48.0", "tokio-postgres", "tower 0.5.2", @@ -1587,7 +1649,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common 0.1.6", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] @@ -1760,24 +1823,23 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.17.0-rc.7" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ab355ec063f7a110eb627471058093aba00eb7f4e70afbd15e696b79d1077b" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.8.0-rc.9", - "digest 0.11.0-rc.3", + "der", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", - "spki 0.8.0-rc.4", - "zeroize", + "spki", ] [[package]] name = "ed25519" -version = "3.0.0-rc.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef49c0b20c0ad088893ad2a790a29c06a012b3f05bcfc66661fd22a94b32129" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -1785,14 +1847,15 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-pre.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.9.3", - "sha2 0.11.0-rc.2", + "rand_core 0.6.4", + "serde", + "sha2", "subtle", "zeroize", ] @@ -1805,21 +1868,20 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" -version = "0.14.0-rc.15" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3be87c458d756141f3b6ee188828132743bf90c7d14843e2835d6443e5fb03" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.3.0", - "crypto-bigint", - "digest 0.11.0-rc.3", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", "ff", + "generic-array", "group", "hkdf", - "hybrid-array", - "once_cell", - "pem-rfc7468 1.0.0-rc.3", + "pem-rfc7468", "pkcs8", - "rand_core 0.9.3", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1839,18 +1901,6 @@ dependencies = [ "winreg 0.55.0", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2 1.0.103", - "quote 1.0.42", - "syn 2.0.111", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1923,19 +1973,19 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ff" -version = "0.14.0-pre.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d42dd26f5790eda47c1a2158ea4120e32c35ddc9a7743c98a292accc01b54ef3" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.6.4", "subtle", ] [[package]] name = "fiat-crypto" -version = "0.3.0" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" @@ -1956,7 +2006,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", - "libz-sys", "miniz_oxide", ] @@ -2153,12 +2202,13 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2190,10 +2240,11 @@ dependencies = [ [[package]] name = "ghash" -version = "0.6.0-rc.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f88107cb02ed63adcc4282942e60c4d09d80208d33b360ce7c729ce6dae1739" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ + "opaque-debug", "polyval", ] @@ -2205,12 +2256,12 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" -version = "0.14.0-pre.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff6a0b2dd4b981b1ae9e3e6830ab146771f3660d31d57bafd9018805a91b0f1" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.9.3", + "rand_core 0.6.4", "subtle", ] @@ -2253,12 +2304,14 @@ dependencies = [ ] [[package]] -name = "hash32" -version = "0.2.1" +name = "half" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "byteorder", + "cfg-if", + "crunchy", + "zerocopy 0.8.31", ] [[package]] @@ -2316,25 +2369,6 @@ dependencies = [ "http 1.4.0", ] -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "spin 0.9.8", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -2353,59 +2387,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring 0.17.14", - "thiserror 2.0.17", - "tinyvec", - "tokio 1.48.0", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.2", - "resolv-conf", - "smallvec", - "thiserror 2.0.17", - "tokio 1.48.0", - "tracing", -] - [[package]] name = "hkdf" -version = "0.13.0-rc.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ef30358b03ca095a5b910547f4f8d4b9f163e4057669c5233ef595b1ecf008" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.13.0-rc.2", + "hmac 0.12.1", ] [[package]] @@ -2512,20 +2500,6 @@ dependencies = [ "pin-project-lite 0.2.16", ] -[[package]] -name = "http-client-proxy" -version = "0.0.0" -dependencies = [ - "anyhow", - "ipnet", - "parking_lot", - "proxy_cfg", - "reqwest", - "rstest", - "tracing", - "url", -] - [[package]] name = "http-range-header" version = "0.3.1" @@ -2562,9 +2536,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" dependencies = [ - "subtle", "typenum", - "zeroize", ] [[package]] @@ -2623,7 +2595,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-native-certs", "rustls-pki-types", "tokio 1.48.0", @@ -2928,82 +2900,117 @@ dependencies = [ [[package]] name = "ironrdp" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c225751e8fbfaaaac5572a80e25d0a0921e9cf408c55509526161b5609157c" +version = "0.5.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ - "ironrdp-acceptor", + "ironrdp-acceptor 0.1.0", "ironrdp-server", ] [[package]] name = "ironrdp-acceptor" -version = "0.8.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "ironrdp-async 0.1.0", + "ironrdp-connector 0.1.0", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", + "tracing", +] + +[[package]] +name = "ironrdp-acceptor" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c18abf50681dda6ea22ac3a812a385ee915a4a69512c775c2358541e89fdd2" +checksum = "a7bbe1fd9a54d5e9669e4006f4840ea89339cebff2a7fb345dc925b17547925b" dependencies = [ - "ironrdp-async", - "ironrdp-connector", + "ironrdp-async 0.6.0", + "ironrdp-connector 0.6.0", "ironrdp-core", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-pdu 0.5.0", + "ironrdp-svc 0.4.1", "tracing", ] [[package]] name = "ironrdp-ainput" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea4a1141844614f392a6da307e96eba951f77bd14bb641303a36ea38d48e12f" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ "bitflags 2.10.0", - "ironrdp-core", "ironrdp-dvc", + "ironrdp-pdu 0.1.0", "num-derive", "num-traits", ] [[package]] name = "ironrdp-async" -version = "0.8.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "bytes 1.11.0", + "ironrdp-connector 0.1.0", + "ironrdp-pdu 0.1.0", + "tracing", +] + +[[package]] +name = "ironrdp-async" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62813c05253206699b2c8e44e268908dafd9668e07bb46ff262ee5b42d13e8cd" +checksum = "724ce488772b7850f6307b4d82559d87dadb7afdf816a35f6cf6e5a989a716f0" dependencies = [ "bytes 1.11.0", - "ironrdp-connector", + "ironrdp-connector 0.6.0", "ironrdp-core", - "ironrdp-pdu", + "ironrdp-pdu 0.5.0", "tracing", ] [[package]] name = "ironrdp-cliprdr" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9b4286b677b65b57bb1d5c4453ef46c1cf3858f30515b2016465978b4b1e2b" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ "bitflags 2.10.0", - "ironrdp-core", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", + "thiserror 1.0.69", "tracing", ] [[package]] name = "ironrdp-connector" -version = "0.8.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "ironrdp-error 0.1.0", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", + "rand_core 0.6.4", + "sspi 0.11.1", + "tracing", + "url", + "winapi", +] + +[[package]] +name = "ironrdp-connector" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a8d5c1b8f167bbd9c935b08a4d3b592fe0a163ded7e4cc8880d471f06b3e2fa" +checksum = "0cb98a29bdd7ef95b490050ddabe4ddd816e527ea0df9f25cc7a9a30d2584dc1" dependencies = [ "ironrdp-core", - "ironrdp-error", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-error 0.1.3", + "ironrdp-pdu 0.5.0", + "ironrdp-svc 0.4.1", "picky", - "picky-asn1-der", - "picky-asn1-x509", - "rand 0.9.2", - "sspi", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.14.4", + "rand_core 0.6.4", + "sspi 0.16.1", "tracing", "url", ] @@ -3014,35 +3021,36 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2db60a59716a84d09040d29c9e75e81545842510fccb0934c09b28e78b46680" dependencies = [ - "ironrdp-error", + "ironrdp-error 0.1.3", ] [[package]] name = "ironrdp-displaycontrol" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ea49ab58df70f3cfda6c2772c598603df02a7bfb0ddc84e563e25cd7c4e340" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ - "ironrdp-core", "ironrdp-dvc", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", "tracing", ] [[package]] name = "ironrdp-dvc" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf251c27aed3f73d4f25e86b1bee1c9d82145ec665de8317edbf2da7c42ebe8" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ - "ironrdp-core", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", "slab", "tracing", ] +[[package]] +name = "ironrdp-error" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" + [[package]] name = "ironrdp-error" version = "0.1.3" @@ -3051,140 +3059,168 @@ checksum = "4a9d7794e854eef2f13fdf79c8502bcc567a75a15fd0522885f37739386a4cef" [[package]] name = "ironrdp-graphics" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a039574abd3303506ee93c1376fcc9ce13de660c98a38ca9b716b45c369dff8d" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ "bit_field", "bitflags 2.10.0", "bitvec", "byteorder", - "ironrdp-core", - "ironrdp-pdu", + "ironrdp-error 0.1.0", + "ironrdp-pdu 0.1.0", + "lazy_static", "num-derive", "num-traits", - "yuv", + "thiserror 1.0.69", ] [[package]] name = "ironrdp-pdu" -version = "0.7.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "bit_field", + "bitflags 2.10.0", + "byteorder", + "der-parser", + "ironrdp-error 0.1.0", + "md-5", + "num-bigint", + "num-derive", + "num-integer", + "num-traits", + "pkcs1", + "sha1 0.10.6", + "tap", + "thiserror 1.0.69", + "x509-cert", +] + +[[package]] +name = "ironrdp-pdu" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409b96a94ca1fad1bfaa41789611bbb5efc112503b27b0513a1feb243e49eb60" +checksum = "cc69c5d6ad3399965e0d3762886857f5861d4d854efe8d2bfc3462eb2b2b555a" dependencies = [ "bit_field", "bitflags 2.10.0", "byteorder", "der-parser", "ironrdp-core", - "ironrdp-error", - "md-5 0.10.6", + "ironrdp-error 0.1.3", + "md-5", "num-bigint", "num-derive", "num-integer", "num-traits", - "pkcs1 0.7.5", + "pkcs1", "sha1 0.10.6", "tap", - "thiserror 2.0.17", + "thiserror 1.0.69", "x509-cert", ] [[package]] name = "ironrdp-rdcleanpath" -version = "0.2.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e9011b8f48be356a51c75757a09075af787601a03542815424da493d2d2757" +checksum = "ac3f5401de43e86384ac0f7f356af8c0bdc321671853f76095da5d480d6998e0" dependencies = [ - "der 0.7.10", + "der", ] [[package]] name = "ironrdp-rdpsnd" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29897b54e358d7ad309937832dbcc71cff310dc223fa70cfc5d66db8c61b3a6b" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ "bitflags 2.10.0", - "ironrdp-core", - "ironrdp-pdu", - "ironrdp-svc", + "ironrdp-pdu 0.1.0", + "ironrdp-svc 0.1.0", "tracing", ] [[package]] name = "ironrdp-server" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a15046611c443c9484b230ca7daf489a14b588c4cd6ff558228cc23c142297" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" dependencies = [ "anyhow", "async-trait", - "bytes 1.11.0", - "ironrdp-acceptor", + "ironrdp-acceptor 0.1.0", "ironrdp-ainput", - "ironrdp-async", "ironrdp-cliprdr", - "ironrdp-core", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-graphics", - "ironrdp-pdu", + "ironrdp-pdu 0.1.0", "ironrdp-rdpsnd", - "ironrdp-svc", - "ironrdp-tokio", - "qoicoubeh", - "rayon", - "rustls-pemfile 2.2.0", + "ironrdp-svc 0.1.0", + "ironrdp-tokio 0.1.0", "tokio 1.48.0", "tokio-rustls", "tracing", - "x509-cert", - "zstd-safe", ] [[package]] name = "ironrdp-svc" -version = "0.6.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-pdu 0.1.0", +] + +[[package]] +name = "ironrdp-svc" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef43a3ea966070b0e12a3f49ffb863c80311bd15f26c2b3681622c85e70d729" +checksum = "98959b1f0f4e9ae705880c73d604ad8f8ebf99feb2e33507092773c4b091c76c" dependencies = [ "bitflags 2.10.0", "ironrdp-core", - "ironrdp-pdu", + "ironrdp-pdu 0.5.0", ] [[package]] name = "ironrdp-tokio" -version = "0.8.0" +version = "0.1.0" +source = "git+https://github.com/Devolutions/IronRDP?rev=2e1a9ac88e38e7d92d893007bc25d0a05c365861#2e1a9ac88e38e7d92d893007bc25d0a05c365861" +dependencies = [ + "bytes 1.11.0", + "ironrdp-async 0.1.0", + "tokio 1.48.0", +] + +[[package]] +name = "ironrdp-tokio" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af190b161daba5d88c614bbf5915fdb586e9a28cb4b938aaac7abf473a1109b" +checksum = "6a5815ae4dd7866a6730efb653281406a77fd1f5426d77dd959fc04e3512410f" dependencies = [ "bytes 1.11.0", - "ironrdp-async", - "ironrdp-connector", - "reqwest", - "sspi", + "ironrdp-async 0.6.0", "tokio 1.48.0", - "url", ] [[package]] -name = "iso7816" -version = "0.1.4" +name = "is-terminal" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "heapless", + "hermit-abi", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "iso7816-tlv" -version = "0.4.4" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "untrusted 0.9.0", + "either", ] [[package]] @@ -3229,13 +3265,13 @@ dependencies = [ "mcp-proxy", "native-tls", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "proptest", "proxy-http", "proxy-socks", "proxy-types", "proxy_cfg", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-native-certs", "rustls-pemfile 2.2.0", "seahorse", @@ -3339,10 +3375,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02a8789fa3ae967c245095183b29a7414b930d110d8b7fd4dffc5d366120ce0" dependencies = [ "argon2", - "picky-asn1", - "picky-asn1-der", - "picky-asn1-x509", - "picky-krb", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.15.2", + "picky-krb 0.12.0", "thiserror 2.0.17", "time", "tracing", @@ -3351,9 +3387,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.0-rc.0" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -3383,6 +3419,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "lazycell" @@ -3458,9 +3497,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" @@ -3624,17 +3663,6 @@ dependencies = [ "zerocopy 0.7.35", ] -[[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3758,16 +3786,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "md-5" -version = "0.11.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ec86664728010f574d67ef01aec964e6f1299241a3402857c1a8a390a62478" -dependencies = [ - "cfg-if", - "digest 0.11.0-rc.3", -] - [[package]] name = "md4" version = "0.10.2" @@ -3858,23 +3876,6 @@ dependencies = [ "tokio 1.48.0", ] -[[package]] -name = "moka" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "multibase" version = "0.9.2" @@ -3915,7 +3916,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -3923,6 +3924,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + [[package]] name = "netlink-packet-core" version = "0.8.1" @@ -3932,6 +3944,20 @@ dependencies = [ "paste", ] +[[package]] +name = "netlink-packet-route" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "log", + "netlink-packet-core 0.7.0", + "netlink-packet-utils", +] + [[package]] name = "netlink-packet-route" version = "0.26.0" @@ -3941,19 +3967,31 @@ dependencies = [ "bitflags 2.10.0", "libc", "log", - "netlink-packet-core", + "netlink-packet-core 0.8.1", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.12.0" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes 1.11.0", "futures", "log", - "netlink-packet-core", + "netlink-packet-core 0.7.0", "netlink-sys", "thiserror 2.0.17", ] @@ -3973,9 +4011,9 @@ dependencies = [ [[package]] name = "network-interface" -version = "2.0.5" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +checksum = "5e79101e6efcffacab279462884a7eebf65ea5f4ac2cc727b60c715a9aa04722" dependencies = [ "cc", "libc", @@ -4007,7 +4045,7 @@ dependencies = [ "futures-util", "ipconfig", "mdns-sd", - "netlink-packet-route", + "netlink-packet-route 0.26.0", "network-interface", "network-scanner-net", "network-scanner-proto", @@ -4073,6 +4111,17 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.30.1" @@ -4145,15 +4194,15 @@ checksum = "50f0690370ba64c23218c7eaf146b8d84b21b265bbad0dafb19b38c92327ef35" dependencies = [ "bitflags 2.10.0", "ironrdp-core", - "ironrdp-error", + "ironrdp-error 0.1.3", "uuid", ] [[package]] name = "ntapi" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] @@ -4177,6 +4226,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -4203,6 +4269,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4210,6 +4287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4279,10 +4357,18 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" @@ -4316,12 +4402,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - [[package]] name = "openssl-sys" version = "0.9.111" @@ -4336,44 +4416,40 @@ dependencies = [ [[package]] name = "p256" -version = "0.14.0-pre.11" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b374901df34ee468167a58e2a49e468cb059868479cafebeb804f6b855423d" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", "elliptic-curve", - "primefield", "primeorder", - "sha2 0.11.0-rc.2", + "sha2", ] [[package]] name = "p384" -version = "0.14.0-pre.11" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701032b3730df6b882496d6cee8221de0ce4bc11ddc64e6d89784aa5b8a6de30" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ "ecdsa", "elliptic-curve", - "fiat-crypto", - "primefield", "primeorder", - "sha2 0.11.0-rc.2", + "sha2", ] [[package]] name = "p521" -version = "0.14.0-pre.11" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ba29c2906eb5c89a8c411c4f11243ee4e5517ee7d71d9a13fedc877a6057b1" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "base16ct 0.3.0", + "base16ct 0.2.0", "ecdsa", "elliptic-curve", - "primefield", "primeorder", - "rand_core 0.9.3", - "sha2 0.11.0-rc.2", + "rand_core 0.6.4", + "sha2", ] [[package]] @@ -4416,6 +4492,17 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "sha1 0.10.6", +] + [[package]] name = "pbkdf2" version = "0.13.0-rc.1" @@ -4461,15 +4548,6 @@ dependencies = [ "base64ct", ] -[[package]] -name = "pem-rfc7468" -version = "1.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -4536,74 +4614,57 @@ dependencies = [ [[package]] name = "picky" -version = "7.0.0-rc.20" +version = "7.0.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdc52be663aebd70d7006ae305c87eb32a2b836d6c2f26f7e384f845d80b621" +checksum = "83be360ca0cc8659abfbda932098e606fe52fa129508b92f0ce2998c00679170" dependencies = [ - "aead", - "aes 0.9.0-rc.1", + "aes 0.8.4", "aes-gcm", "aes-kw", "base64 0.22.1", - "block-buffer 0.11.0-rc.5", - "block-padding 0.4.0-rc.4", - "cbc 0.2.0-rc.1", - "cipher 0.5.0-rc.1", - "crypto-bigint", - "crypto-common 0.2.0-rc.4", - "crypto-primes", - "ctr", - "curve25519-dalek", - "der 0.8.0-rc.9", - "des", - "digest 0.11.0-rc.3", - "ecdsa", - "ed25519", + "cbc 0.1.2", + "des 0.8.1", + "digest 0.10.7", "ed25519-dalek", - "elliptic-curve", - "ff", - "ghash", - "group", "hex", - "hkdf", - "hmac 0.13.0-rc.2", + "hmac 0.12.1", "http 1.4.0", - "inout 0.2.0-rc.6", - "keccak", - "md-5 0.11.0-rc.2", + "md-5", + "num-bigint-dig", "p256", "p384", "p521", - "pbkdf2", - "pem-rfc7468 1.0.0-rc.3", - "picky-asn1", - "picky-asn1-der", - "picky-asn1-x509", - "pkcs1 0.8.0-rc.4", - "pkcs8", - "polyval", - "primefield", - "primeorder", - "rand 0.9.2", - "rand_core 0.9.3", + "pbkdf2 0.12.2", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.14.4", + "rand 0.8.5", + "rand_core 0.6.4", "rc2", - "rfc6979", "rsa", - "sec1", "serde", "serde_json", - "sha1 0.11.0-rc.2", - "sha2 0.11.0-rc.2", + "sha1 0.10.6", + "sha2", "sha3", - "signature", - "spki 0.8.0-rc.4", - "thiserror 2.0.17", + "thiserror 1.0.69", "time", - "universal-hash", "x25519-dalek", "zeroize", ] +[[package]] +name = "picky-asn1" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" +dependencies = [ + "oid", + "serde", + "serde_bytes", + "time", +] + [[package]] name = "picky-asn1" version = "0.10.1" @@ -4619,31 +4680,119 @@ dependencies = [ [[package]] name = "picky-asn1-der" -version = "0.5.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b491eb61603cba1ad5c6be0269883538f8d74136c35e3641a840fb0fbcd41efc" +checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" dependencies = [ - "picky-asn1", + "picky-asn1 0.8.0", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-der" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dccb53c26f70c082e008818f524bd45d057069517b047bd0c0ee062d6d7d7f2" +dependencies = [ + "picky-asn1 0.10.1", "serde", "serde_bytes", ] [[package]] name = "picky-asn1-x509" -version = "0.15.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c97cd14d567a17755910fa8718277baf39d08682a980b1b1a4b4da7d0bc61a04" +checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" +dependencies = [ + "base64 0.21.7", + "oid", + "picky-asn1 0.8.0", + "picky-asn1-der 0.4.1", + "serde", + "widestring 1.2.1", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f974c1b3348705c23887c4f3b90947b9f23566db8b032e48af59c91f888f6f" dependencies = [ "base64 0.22.1", - "crypto-bigint", + "num-bigint-dig", "oid", - "picky-asn1", - "picky-asn1-der", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", "serde", "widestring 1.2.1", "zeroize", ] +[[package]] +name = "picky-asn1-x509" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c97cd14d567a17755910fa8718277baf39d08682a980b1b1a4b4da7d0bc61a04" +dependencies = [ + "base64 0.22.1", + "oid", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "serde", +] + +[[package]] +name = "picky-krb" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71cf61ebe6e657a81bcac31f9d61d52c23a1fd517b0dad77b915a7d3d15d2e8" +dependencies = [ + "aes 0.8.4", + "byteorder", + "cbc 0.1.2", + "crypto", + "des 0.8.1", + "hmac 0.12.1", + "num-bigint-dig", + "oid", + "pbkdf2 0.12.2", + "picky-asn1 0.8.0", + "picky-asn1-der 0.4.1", + "picky-asn1-x509 0.12.0", + "rand 0.8.5", + "serde", + "sha1 0.10.6", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "picky-krb" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45ffe5f2122cdda5e9059ab837a65ba1b77729db43fc1500f2fce6b27070eab" +dependencies = [ + "aes 0.8.4", + "byteorder", + "cbc 0.1.2", + "crypto", + "des 0.8.1", + "hmac 0.12.1", + "num-bigint-dig", + "oid", + "pbkdf2 0.12.2", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.14.4", + "rand 0.8.5", + "serde", + "sha1 0.10.6", + "thiserror 1.0.69", + "uuid", +] + [[package]] name = "picky-krb" version = "0.12.0" @@ -4656,17 +4805,17 @@ dependencies = [ "byteorder", "cbc 0.2.0-rc.1", "cipher 0.5.0-rc.1", - "crypto-bigint", + "crypto-bigint 0.7.0-rc.8", "crypto-common 0.2.0-rc.4", - "des", + "des 0.9.0-rc.1", "digest 0.11.0-rc.3", "hmac 0.13.0-rc.2", "inout 0.2.0-rc.6", "oid", - "pbkdf2", - "picky-asn1", - "picky-asn1-der", - "picky-asn1-x509", + "pbkdf2 0.13.0-rc.1", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.15.2", "rand 0.9.2", "serde", "sha1 0.11.0-rc.2", @@ -4738,35 +4887,54 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.10", - "spki 0.7.3", + "der", + "pkcs8", + "spki", ] [[package]] -name = "pkcs1" -version = "0.8.0-rc.4" +name = "pkcs8" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.8.0-rc.9", - "spki 0.8.0-rc.4", + "der", + "spki", ] [[package]] -name = "pkcs8" -version = "0.11.0-rc.7" +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93eac55f10aceed84769df670ea4a32d2ffad7399400d41ee1c13b1cd8e1b478" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ - "der 0.8.0-rc.9", - "spki 0.8.0-rc.4", + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] [[package]] name = "polling" @@ -4800,20 +4968,21 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.0-rc.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffd40cc99d0fbb02b4b3771346b811df94194bc103983efa0203c8893755085" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", + "opaque-debug", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portpicker" @@ -4835,10 +5004,10 @@ dependencies = [ "bytes 1.11.0", "fallible-iterator 0.2.0", "hmac 0.12.1", - "md-5 0.10.6", + "md-5", "memchr", "rand 0.9.2", - "sha2 0.10.9", + "sha2", "stringprep", ] @@ -4915,24 +5084,11 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "primefield" -version = "0.14.0-pre.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcd4a163053332fd93f39b81c133e96a98567660981654579c90a99062fbf5" -dependencies = [ - "crypto-bigint", - "ff", - "rand_core 0.9.3", - "subtle", - "zeroize", -] - [[package]] name = "primeorder" -version = "0.14.0-pre.9" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c36e8766fcd270fa9c665b9dc364f570695f5a59240949441b077a397f15b74" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] @@ -5024,7 +5180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools", + "itertools 0.12.1", "proc-macro2 1.0.103", "quote 1.0.42", "syn 2.0.111", @@ -5097,15 +5253,6 @@ dependencies = [ "winreg 0.55.0", ] -[[package]] -name = "qoicoubeh" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba" -dependencies = [ - "bytemuck", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -5124,7 +5271,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.36", + "rustls 0.23.35", "socket2 0.6.1", "thiserror 2.0.17", "tokio 1.48.0", @@ -5144,7 +5291,7 @@ dependencies = [ "rand 0.9.2", "ring 0.17.14", "rustc-hash 2.1.1", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -5287,11 +5434,11 @@ dependencies = [ [[package]] name = "rc2" -version = "0.9.0-pre.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03621ac292cc723def9e0fd0eb9573b1df8d6a9ee7ad637fe94dfc153705f3c" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" dependencies = [ - "cipher 0.5.0-rc.1", + "cipher 0.4.4", ] [[package]] @@ -5378,7 +5525,6 @@ checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" dependencies = [ "base64 0.22.1", "bytes 1.11.0", - "futures-channel", "futures-core", "futures-util", "h2 0.4.12", @@ -5393,7 +5539,7 @@ dependencies = [ "percent-encoding", "pin-project-lite 0.2.16", "quinn", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-native-certs", "rustls-pki-types", "serde", @@ -5413,12 +5559,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - [[package]] name = "retour" version = "0.4.0-alpha.4" @@ -5437,11 +5577,11 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.5.0-rc.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369f9c4f79388704648e7bcb92749c0d6cf4397039293a9b747694fa4fb4bae" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.13.0-rc.2", + "hmac 0.12.1", "subtle", ] @@ -5476,20 +5616,21 @@ dependencies = [ [[package]] name = "rsa" -version = "0.10.0-rc.9" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8955ab399f6426998fde6b76ae27233cce950705e758a6c17afd2f6d0e5d52" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ - "const-oid 0.10.1", - "crypto-bigint", - "crypto-primes", - "digest 0.11.0-rc.3", - "pkcs1 0.8.0-rc.4", + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", "pkcs8", - "rand_core 0.9.3", - "sha1 0.11.0-rc.2", + "rand_core 0.6.4", + "sha1 0.10.6", "signature", - "spki 0.8.0-rc.4", + "spki", "subtle", "zeroize", ] @@ -5526,18 +5667,18 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.19.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ee907fdcec9200d13b9cdb64dfc8179cb4ac16ead6ae0ac76333dc41981fc" +checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5" dependencies = [ - "futures-channel", - "futures-util", + "futures", "log", - "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-core 0.7.0", + "netlink-packet-route 0.19.0", + "netlink-packet-utils", "netlink-proto", "netlink-sys", - "nix", + "nix 0.27.1", "thiserror 1.0.69", "tokio 1.48.0", ] @@ -5612,9 +5753,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", @@ -5632,18 +5773,18 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c742cb7d8e43daae2dd9ca4b1da442b4500a461ce1c84249e6ac99b4bc12562e" dependencies = [ - "rustls 0.23.36", - "sha2 0.10.9", + "rustls 0.23.35", + "sha2", "windows-sys 0.59.0", ] [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -5794,26 +5935,18 @@ checksum = "d25679a62f678e7485f21abdea76f91a15322a0fe51efea791fd9d124f1c473c" [[package]] name = "sec1" -version = "0.8.0-rc.10" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dff52f6118bc9f0ac974a54a639d499ac26a6cad7a6e39bc0990c19625e793b" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.3.0", - "der 0.8.0-rc.9", - "hybrid-array", + "base16ct 0.2.0", + "der", + "generic-array", + "pkcs8", "subtle", "zeroize", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -5932,15 +6065,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", + "ryu", "serde", "serde_core", - "zmij", ] [[package]] @@ -5960,7 +6093,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6" dependencies = [ - "axum 0.8.8", + "axum 0.8.7", "futures", "percent-encoding", "serde", @@ -6044,24 +6177,13 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.11.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.11.0-rc.3", -] - [[package]] name = "sha3" -version = "0.11.0-rc.3" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2103ca0e6f4e9505eae906de5e5883e06fc3b2232fb5d6914890c7bbcb62f478" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.11.0-rc.3", + "digest 0.10.7", "keccak", ] @@ -6091,12 +6213,12 @@ dependencies = [ [[package]] name = "signature" -version = "3.0.0-rc.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc280a6ff65c79fbd6622f64d7127f32b85563bca8c53cd2e9141d6744a9056d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.11.0-rc.3", - "rand_core 0.9.3", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -6140,9 +6262,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smol_str" -version = "0.3.5" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" dependencies = [ "borsh", "serde_core", @@ -6190,84 +6312,93 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.10", + "der", ] [[package]] -name = "spki" -version = "0.8.0-rc.4" +name = "sspi" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +checksum = "18d31fab47d9290be28a8d027c8428756826f1d4fe1e5ba0f51d24f52c568e21" dependencies = [ - "base64ct", - "der 0.8.0-rc.9", + "async-dnssd", + "async-recursion", + "bitflags 2.10.0", + "byteorder", + "cfg-if", + "crypto-mac", + "futures", + "hmac 0.12.1", + "lazy_static", + "md-5", + "md4", + "num-bigint-dig", + "num-derive", + "num-traits", + "oid", + "picky", + "picky-asn1 0.8.0", + "picky-asn1-der 0.4.1", + "picky-asn1-x509 0.12.0", + "picky-krb 0.8.0", + "rand 0.8.5", + "serde", + "serde_derive", + "sha1 0.10.6", + "sha2", + "time", + "tokio 1.48.0", + "tracing", + "url", + "uuid", + "winapi", + "windows 0.51.1", + "windows-sys 0.48.0", + "winreg 0.51.0", + "zeroize", ] [[package]] name = "sspi" -version = "0.18.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f73fe6be958ae27fa8e982d9acc42d16f34eb74714d95bb53015528667cae4" +checksum = "523f6a99e26c1e6476a424d54bbda5354a01ee7f18b9d93dc48a8fd45ae8189b" dependencies = [ "async-dnssd", "async-recursion", "bitflags 2.10.0", - "block-buffer 0.11.0-rc.5", "byteorder", "cfg-if", - "crypto-bigint", - "crypto-common 0.2.0-rc.4", "crypto-mac", - "crypto-primes", - "cryptoki", - "curve25519-dalek", - "der 0.8.0-rc.9", - "digest 0.11.0-rc.3", - "ed25519-dalek", - "ff", "futures", - "getrandom 0.3.4", - "group", - "hickory-proto", - "hickory-resolver", - "hmac 0.13.0-rc.2", - "md-5 0.11.0-rc.2", + "hmac 0.12.1", + "lazy_static", + "md-5", "md4", + "num-bigint-dig", "num-derive", "num-traits", "oid", - "p256", - "p384", - "p521", - "pem-rfc7468 1.0.0-rc.3", "picky", - "picky-asn1", - "picky-asn1-der", - "picky-asn1-x509", - "picky-krb", - "pkcs1 0.8.0-rc.4", - "pkcs8", - "portpicker", - "primefield", - "primeorder", - "rand 0.9.2", - "reqwest", + "picky-asn1 0.10.1", + "picky-asn1-der 0.5.2", + "picky-asn1-x509 0.14.4", + "picky-krb 0.11.0", + "rand 0.8.5", "rsa", - "rustls 0.23.36", - "rustls-native-certs", + "rustls 0.23.35", "serde", - "sha1 0.11.0-rc.2", - "sha2 0.11.0-rc.2", - "signature", - "spki 0.8.0-rc.4", + "serde_derive", + "sha1 0.10.6", + "sha2", "time", "tokio 1.48.0", "tracing", "url", "uuid", - "windows 0.62.2", - "windows-registry 0.6.1", - "winscard", + "windows 0.61.3", + "windows-registry 0.5.3", + "windows-sys 0.60.2", "zeroize", ] @@ -6426,12 +6557,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tap" version = "1.0.1" @@ -6557,33 +6682,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", - "js-sys", "libc", "num-conv", "num_threads", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -6614,6 +6738,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -6754,7 +6888,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.35", "tokio 1.48.0", ] @@ -6771,10 +6905,12 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ + "async-stream", + "bytes 1.11.0", "futures-core", "tokio 1.48.0", "tokio-stream", @@ -6789,7 +6925,7 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-native-certs", "rustls-pki-types", "tokio 1.48.0", @@ -6812,9 +6948,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes 1.11.0", "futures-core", @@ -7296,7 +7432,7 @@ dependencies = [ "log", "native-tls", "rand 0.9.2", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.17", @@ -7414,11 +7550,11 @@ checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[package]] name = "universal-hash" -version = "0.6.0-rc.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.2.0-rc.4", + "crypto-common 0.1.7", "subtle", ] @@ -7450,7 +7586,7 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls 0.23.36", + "rustls 0.23.35", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -7458,15 +7594,14 @@ dependencies = [ [[package]] name = "url" -version = "2.5.8" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -7542,8 +7677,9 @@ name = "video-streamer" version = "0.0.0" dependencies = [ "anyhow", - "axum 0.8.8", + "axum 0.8.7", "cadeau", + "criterion", "ebml-iterable", "futures", "futures-util", @@ -7837,27 +7973,25 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", + "windows-core 0.51.1", + "windows-targets 0.48.5", ] [[package]] name = "windows" -version = "0.62.2" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", ] [[package]] @@ -7870,12 +8004,12 @@ dependencies = [ ] [[package]] -name = "windows-collections" -version = "0.3.2" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-core 0.62.2", + "windows-targets 0.48.5", ] [[package]] @@ -7912,18 +8046,7 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", + "windows-threading", ] [[package]] @@ -7970,16 +8093,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - [[package]] name = "windows-registry" version = "0.5.3" @@ -8164,15 +8277,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8374,35 +8478,22 @@ dependencies = [ [[package]] name = "winreg" -version = "0.55.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] -name = "winscard" -version = "0.2.5" +name = "winreg" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b6ec4e6176df62589d1ac9950f6295be87ca06ee61a7c9a579a2bcc80efe34" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" dependencies = [ - "bitflags 2.10.0", - "crypto-bigint", - "flate2", - "iso7816", - "iso7816-tlv", - "num-derive", - "num-traits", - "picky", - "picky-asn1-x509", - "rand_core 0.9.3", - "rsa", - "sha1 0.11.0-rc.2", - "time", - "tracing", - "uuid", + "cfg-if", + "windows-sys 0.59.0", ] [[package]] @@ -8428,12 +8519,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "3.0.0-pre.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a45998121837fd8c92655d2334aa8f3e5ef0645cdfda5b321b13760c548fd55" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.9.3", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -8445,8 +8536,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", "tls_codec", ] @@ -8482,15 +8573,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "yuv" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f1bad143caadcfcaec93039dc9c40a30fc86f23d9e7cc03764a39fe51d9d43" -dependencies = [ - "num-traits", -] - [[package]] name = "zerocopy" version = "0.7.35" @@ -8605,28 +8687,3 @@ dependencies = [ "quote 1.0.42", "syn 2.0.111", ] - -[[package]] -name = "zmij" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Justfile b/Justfile new file mode 100644 index 000000000..17c3852a9 --- /dev/null +++ b/Justfile @@ -0,0 +1,15 @@ +set shell := ["powershell", "-NoProfile", "-Command"] + +# Usage: +# just test video-streamer +# just test # defaults to video-streamer +# +# Notes: +# - Requires `DGATEWAY_LIB_XMF_PATH` to point to `xmf.dll` for video streaming tests. +# - Writes logs to `.llm/test-.log`. +test streamer="video-streamer": + @$ErrorActionPreference = 'Continue'; $llm = Join-Path (Get-Location) '.llm'; New-Item -ItemType Directory -Force -Path $llm | Out-Null; $log = Join-Path $llm ('test-' + '{{streamer}}' + '.log'); $env:RUST_LOG = 'video_streamer=info,webm_stream_correctness=info'; $env:RUST_BACKTRACE = '1'; $env:CARGO_TARGET_DIR = Join-Path $env:TEMP ('cargo-target-' + '{{streamer}}' + '-test'); if ('{{streamer}}' -eq 'video-streamer') { cmd /c "cargo test -p video-streamer --test webm_stream_correctness -- --ignored --nocapture 2>&1" | Tee-Object -FilePath $log; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } else { throw ('Unknown streamer: ' + '{{streamer}}' + ' (supported: video-streamer)') } + +# Convenience alias (avoids confusion with positional params). +test-streamer: + @just test video-streamer diff --git a/crates/video-streamer/Cargo.toml b/crates/video-streamer/Cargo.toml index bb1eb2785..a6cf082b5 100644 --- a/crates/video-streamer/Cargo.toml +++ b/crates/video-streamer/Cargo.toml @@ -5,6 +5,12 @@ authors = ["Devolutions Inc. "] edition = "2024" publish = false +[features] +# Enables extra per-frame logging and diagnostics. +perf-diagnostics = [] +# Enables internal helpers used by Criterion benchmarks. +bench = ["perf-diagnostics"] + [dependencies] anyhow = "1.0" futures-util = { version = "0.3", features = ["sink"] } @@ -24,7 +30,8 @@ ebml-iterable = "0.6" webm-iterable = "0.6" [dev-dependencies] -tracing-subscriber = "0.3" +criterion = "0.5" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio = { version = "1.45", features = [ "io-util", "rt", @@ -40,3 +47,7 @@ transport = { path = "../transport" } [lints] workspace = true + +[[bench]] +name = "vpx_reencode" +harness = false diff --git a/crates/video-streamer/README.md b/crates/video-streamer/README.md new file mode 100644 index 000000000..816778271 --- /dev/null +++ b/crates/video-streamer/README.md @@ -0,0 +1,57 @@ +# video-streamer + +This crate takes an unseekable WebM recording (typically from Chrome CaptureStream) and rewrites it into a “fresh” WebM stream that can start playing immediately. +It does this by parsing the incoming WebM, finding the correct cut point, and re-encoding frames so the output stream begins with a keyframe and valid headers. + +## Prerequisites + +This crate relies on `cadeau` and its XMF backend for VP8/VP9 decode+encode. +If you want to override which XMF implementation is used at runtime, set `DGATEWAY_LIB_XMF_PATH` to an `xmf.dll` path before running tests or benches. + +Example: + +`$env:DGATEWAY_LIB_XMF_PATH = 'D:\library\cadeau\xmf.dll'` + +## Tests + +Run all tests: + +`cargo test -p video-streamer` + +Run the WebM streaming correctness suite: + +`cargo test -p video-streamer --test webm_stream_correctness -- --nocapture` + +Some tests are marked `#[ignore]` because they require large local assets or are intended for local investigation. +To run ignored tests: + +`cargo test -p video-streamer -- --ignored --nocapture` + +Test assets live under `testing-assets\`. + +## Logging and diagnostics + +Most detailed diagnostics are compiled out by default to keep production logs clean. +To include extra diagnostics, build with `perf-diagnostics`: + +`cargo test -p video-streamer --features perf-diagnostics -- --nocapture` + +Then set `RUST_LOG` as needed. +Example: + +`$env:RUST_LOG = 'video_streamer=trace'` + +## Benchmarks + +The main benchmark is `benches\vpx_reencode.rs`. +Run it with: + +`cargo bench -p video-streamer --bench vpx_reencode --features bench -- --nocapture` + +Benchmark output is intentionally quiet by default. +To print detailed per-run results, set `VIDEO_STREAMER_BENCH_VERBOSE`: + +`$env:VIDEO_STREAMER_BENCH_VERBOSE = '1'` + +If you want to correlate benchmark results with internal timing, also enable `perf-diagnostics` (the `bench` feature enables it). +This is intentionally a build-time gate so production logs stay clean. diff --git a/crates/video-streamer/benches/vpx_reencode.rs b/crates/video-streamer/benches/vpx_reencode.rs new file mode 100644 index 000000000..67d27b470 --- /dev/null +++ b/crates/video-streamer/benches/vpx_reencode.rs @@ -0,0 +1,114 @@ +use std::io::{self, Write as _}; +use std::time::{Duration, Instant}; + +use criterion::{Criterion, criterion_group, criterion_main}; + +fn verbose_bench_logging_enabled() -> bool { + let value = std::env::var_os("VIDEO_STREAMER_BENCH_VERBOSE"); + value.is_some_and(|v| !v.is_empty() && v != "0") +} + +fn maybe_log_line(message: impl std::fmt::Display) { + if !verbose_bench_logging_enabled() { + return; + } + + let _ = writeln!(io::stdout(), "{message}"); +} + +fn bench_reencode_first_500_tags(c: &mut Criterion) { + let xmf_initialized = { + let Ok(path) = std::env::var("DGATEWAY_LIB_XMF_PATH") else { + maybe_log_line("DGATEWAY_LIB_XMF_PATH not set; skipping benchmarks"); + return; + }; + + // SAFETY: This is how the project loads XMF elsewhere. + if let Err(e) = unsafe { cadeau::xmf::init(&path) } { + maybe_log_line(format_args!( + "failed to initialize XMF from DGATEWAY_LIB_XMF_PATH={path}: {e:#}" + )); + return; + } + + true + }; + if !xmf_initialized { + return; + } + + let input = { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir.join("testing-assets").join("uncued-recording.webm") + }; + let mut group = c.benchmark_group("vpx_reencode"); + // Criterion requires sample_size >= 10. + group.sample_size(10); + group.warm_up_time(Duration::from_secs(1)); + group.measurement_time(Duration::from_secs(5)); + + group.bench_function("reencode_first_500_tags_uncued_recording", |b| { + // Keep per-iteration work bounded; Criterion may run many iterations per sample. + let per_iter_deadline = Duration::from_millis(200); + + b.iter_custom(|iters| { + let start = Instant::now(); + let mut tags_processed_total: u64 = 0; + let mut bytes_written_total: u64 = 0; + let mut frames_reencoded_total: u64 = 0; + let mut input_media_span_ms_total: u64 = 0; + let mut timed_out_any = false; + + for _ in 0..iters { + let stats = + video_streamer::bench_support::reencode_first_tags_from_path_until_deadline( + &input, + video_streamer::StreamingConfig { + encoder_threads: video_streamer::config::CpuCount::new(1), + }, + 500, + per_iter_deadline, + ) + .expect("reencode failed"); + + tags_processed_total += stats.tags_processed as u64; + bytes_written_total += stats.bytes_written as u64; + frames_reencoded_total += stats.frames_reencoded as u64; + input_media_span_ms_total += stats.input_media_span_ms as u64; + timed_out_any |= stats.timed_out; + + criterion::black_box(stats); + } + + let elapsed = start.elapsed(); + let elapsed_secs = elapsed.as_secs_f64().max(1e-9); + let tags_per_sec = (tags_processed_total as f64) / elapsed_secs; + let bytes_per_sec = (bytes_written_total as f64) / elapsed_secs; + let frames_per_sec = (frames_reencoded_total as f64) / elapsed_secs; + let media_ms_per_sec = (input_media_span_ms_total as f64) / elapsed_secs; + + maybe_log_line(format_args!( + "[LibVPx-Performance-Hypothesis] iters={} elapsed_ms={} per_iter_deadline_ms={} frames_total={} frames_per_sec={:.2} input_media_ms_total={} input_media_ms_per_sec={:.2} tags_total={} tags_per_sec={:.2} bytes_total={} bytes_per_sec={:.2} timed_out_any={}", + iters, + elapsed.as_millis(), + per_iter_deadline.as_millis(), + frames_reencoded_total, + frames_per_sec, + input_media_span_ms_total, + media_ms_per_sec, + tags_processed_total, + tags_per_sec, + bytes_written_total, + bytes_per_sec, + timed_out_any, + )); + + elapsed + }); + }); + + group.finish(); +} + +criterion_group!(benches, bench_reencode_first_500_tags); +criterion_main!(benches); diff --git a/crates/video-streamer/src/bench_support.rs b/crates/video-streamer/src/bench_support.rs new file mode 100644 index 000000000..1ee6ff4c6 --- /dev/null +++ b/crates/video-streamer/src/bench_support.rs @@ -0,0 +1,204 @@ +use std::io::{Seek, Write}; +use std::path::Path; +use std::time::{Duration, Instant}; + +use anyhow::Context as _; +use webm_iterable::WebmIterator; +use webm_iterable::errors::TagIteratorError; +use webm_iterable::matroska_spec::{Block, Master, MatroskaSpec, SimpleBlock}; + +use crate::StreamingConfig; +use crate::reopenable::Reopenable; +use crate::streamer::iter::{IteratorError, WebmPositionedIterator}; +use crate::streamer::tag_writers::{EncodeWriterConfig, HeaderWriter, WriterResult}; + +#[derive(Debug, Clone)] +pub struct ReencodeBenchStats { + pub wall: Duration, + pub tags_processed: u64, + pub frames_reencoded: u64, + pub input_media_span_ms: u64, + pub chunks_written: u64, + pub bytes_written: u64, + pub timed_out: bool, +} + +#[derive(Default)] +struct CountingWriter { + chunks: u64, + bytes: u64, +} + +impl Write for CountingWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.chunks += 1; + self.bytes += buf.len() as u64; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +/// Bench helper: re-encode the first `max_tags` relevant tags from a WebM file and measure wall-clock time. +/// +/// Notes: +/// - This uses the same WebM parsing + VPX decoder/encoder path as `webm_stream`, but writes output to a counting sink. +/// - Requires XMF to be initialized by the caller (e.g. `cadeau::xmf::init(...)`). +pub fn reencode_first_tags( + input_stream: R, + config: StreamingConfig, + max_tags: u64, +) -> anyhow::Result +where + R: std::io::Read + Seek + Reopenable, +{ + reencode_first_tags_until_deadline(input_stream, config, max_tags, None) +} + +/// Bench helper: re-encode up to `max_tags`, but stop early if `max_wall` elapses. +/// +/// This is intended for "sane timeout" local diagnosis: it prevents benches from running forever on slow machines. +pub fn reencode_first_tags_until_deadline( + input_stream: R, + config: StreamingConfig, + max_tags: u64, + max_wall: Option, +) -> anyhow::Result +where + R: std::io::Read + Seek + Reopenable, +{ + let started_at = Instant::now(); + + let mut webm_itr = WebmPositionedIterator::new(WebmIterator::new( + input_stream, + &[MatroskaSpec::BlockGroup(Master::Start)], + )); + + let mut headers = vec![]; + while let Some(tag) = webm_itr.next() { + let tag = tag?; + if matches!(tag, MatroskaSpec::Cluster(Master::Start)) { + break; + } + headers.push(tag); + } + + let encode_writer_config = EncodeWriterConfig::try_from((headers.as_slice(), &config))?; + + let mut sink = CountingWriter::default(); + let mut header_writer = HeaderWriter::new(&mut sink); + for header in &headers { + header_writer.write(header)?; + } + + let (mut encode_writer, _marker) = header_writer.into_encoded_writer(encode_writer_config)?; + + let mut tags_processed: u64 = 0; + let mut cluster_timestamp: Option = None; + let mut first_input_block_absolute_time: Option = None; + let mut last_input_block_absolute_time: Option = None; + let mut frames_reencoded: u64 = 0; + let mut timed_out = false; + while tags_processed < max_tags { + if let Some(max_wall) = max_wall + && max_wall <= started_at.elapsed() + { + timed_out = true; + break; + } + + match webm_itr.next() { + Some(Ok(tag)) => { + match &tag { + MatroskaSpec::Timestamp(timestamp) => cluster_timestamp = Some(*timestamp), + MatroskaSpec::SimpleBlock(data) => { + if let Some(cluster_timestamp) = cluster_timestamp { + let simple_block = SimpleBlock::try_from(data)?; + let abs_ms_i64 = i64::try_from(cluster_timestamp) + .context("cluster timestamp does not fit in i64")? + .checked_add(i64::from(simple_block.timestamp)) + .context("block absolute timestamp overflow")?; + let abs_ms = + u64::try_from(abs_ms_i64).context("block absolute timestamp does not fit in u64")?; + first_input_block_absolute_time.get_or_insert(abs_ms); + last_input_block_absolute_time = Some(abs_ms); + frames_reencoded += 1; + } + } + MatroskaSpec::BlockGroup(Master::Full(children)) => { + if let Some(cluster_timestamp) = cluster_timestamp { + let raw_block = children.iter().find_map(|t| match t { + MatroskaSpec::Block(block) => Some(block), + _ => None, + }); + + if let Some(raw_block) = raw_block { + let block = Block::try_from(raw_block)?; + let abs_ms_i64 = i64::try_from(cluster_timestamp) + .context("cluster timestamp does not fit in i64")? + .checked_add(i64::from(block.timestamp)) + .context("block absolute timestamp overflow")?; + let abs_ms = u64::try_from(abs_ms_i64) + .context("block absolute timestamp does not fit in u64")?; + first_input_block_absolute_time.get_or_insert(abs_ms); + last_input_block_absolute_time = Some(abs_ms); + frames_reencoded += 1; + } + } + } + _ => {} + } + + tags_processed += 1; + match encode_writer.write(tag)? { + WriterResult::Continue => {} + } + } + Some(Err(IteratorError::InnerError(TagIteratorError::UnexpectedEOF { .. }))) => break, + Some(Err(e)) => return Err(e).context("webm iterator error"), + None => break, + } + } + + let input_media_span_ms = last_input_block_absolute_time + .zip(first_input_block_absolute_time) + .map(|(last, first)| last.saturating_sub(first)) + .unwrap_or(0); + + Ok(ReencodeBenchStats { + wall: started_at.elapsed(), + tags_processed, + frames_reencoded, + input_media_span_ms, + chunks_written: sink.chunks, + bytes_written: sink.bytes, + timed_out, + }) +} + +/// Bench helper: open a WebM file from disk and re-encode. +/// +/// Notes: +/// - This is a convenience wrapper for Criterion benches. +pub fn reencode_first_tags_from_path( + input_path: &Path, + config: StreamingConfig, + max_tags: u64, +) -> anyhow::Result { + let file = crate::streamer::reopenable_file::ReOpenableFile::open(input_path) + .with_context(|| format!("failed to open {}", input_path.display()))?; + reencode_first_tags(file, config, max_tags) +} + +pub fn reencode_first_tags_from_path_until_deadline( + input_path: &Path, + config: StreamingConfig, + max_tags: u64, + max_wall: Duration, +) -> anyhow::Result { + let file = crate::streamer::reopenable_file::ReOpenableFile::open(input_path) + .with_context(|| format!("failed to open {}", input_path.display()))?; + reencode_first_tags_until_deadline(file, config, max_tags, Some(max_wall)) +} diff --git a/crates/video-streamer/src/lib.rs b/crates/video-streamer/src/lib.rs index f62e7be4d..e568689a0 100644 --- a/crates/video-streamer/src/lib.rs +++ b/crates/video-streamer/src/lib.rs @@ -1,3 +1,28 @@ +// Compile-time gated diagnostics to keep production logs clean. +#[cfg(feature = "perf-diagnostics")] +macro_rules! perf_trace { + ($($tt:tt)*) => { + tracing::trace!($($tt)*) + }; +} + +#[cfg(not(feature = "perf-diagnostics"))] +macro_rules! perf_trace { + ($($tt:tt)*) => {}; +} + +#[cfg(feature = "perf-diagnostics")] +macro_rules! perf_debug { + ($($tt:tt)*) => { + tracing::debug!($($tt)*) + }; +} + +#[cfg(not(feature = "perf-diagnostics"))] +macro_rules! perf_debug { + ($($tt:tt)*) => {}; +} + pub mod config; pub mod debug; pub mod reopenable; @@ -14,3 +39,6 @@ pub use streamer::reopenable_file::ReOpenableFile; pub use streamer::signal_writer::SignalWriter; #[rustfmt::skip] pub use streamer::webm_stream; + +#[cfg(feature = "bench")] +pub mod bench_support; diff --git a/crates/video-streamer/src/streamer/channel_writer.rs b/crates/video-streamer/src/streamer/channel_writer.rs index 3913773f4..29af214bf 100644 --- a/crates/video-streamer/src/streamer/channel_writer.rs +++ b/crates/video-streamer/src/streamer/channel_writer.rs @@ -1,4 +1,6 @@ use std::io; +#[cfg(feature = "perf-diagnostics")] +use std::sync::atomic::{AtomicU64, Ordering}; #[derive(Debug)] pub(crate) enum ChannelWriterError { @@ -15,25 +17,74 @@ impl std::error::Error for ChannelWriterError {} pub(crate) struct ChannelWriter { writer: tokio::sync::mpsc::Sender>, + #[cfg(feature = "perf-diagnostics")] + writes_count: AtomicU64, + #[cfg(feature = "perf-diagnostics")] + total_bytes_written: AtomicU64, } impl ChannelWriter { pub(crate) fn new() -> (Self, ChannelWriterReceiver) { + perf_trace!("ChannelWriter::new - creating channel with capacity 10"); let (sender, receiver) = tokio::sync::mpsc::channel(10); - (Self { writer: sender }, ChannelWriterReceiver { receiver }) + ( + Self { + writer: sender, + #[cfg(feature = "perf-diagnostics")] + writes_count: AtomicU64::new(0), + #[cfg(feature = "perf-diagnostics")] + total_bytes_written: AtomicU64::new(0), + }, + ChannelWriterReceiver { + receiver, + #[cfg(feature = "perf-diagnostics")] + reads_count: AtomicU64::new(0), + #[cfg(feature = "perf-diagnostics")] + total_bytes_read: AtomicU64::new(0), + }, + ) } } impl io::Write for ChannelWriter { fn write(&mut self, buf: &[u8]) -> io::Result { - self.writer - .blocking_send(buf.to_vec()) - .map_err(|_| io::Error::other(ChannelWriterError::ChannelClosed))?; + let buf_len = buf.len(); + #[cfg(feature = "perf-diagnostics")] + { + let write_num = self.writes_count.fetch_add(1, Ordering::Relaxed) + 1; + perf_trace!(write_num, buf_len, "ChannelWriter::write - sending to channel"); + } - Ok(buf.len()) + self.writer.blocking_send(buf.to_vec()).map_err(|_| { + #[cfg(feature = "perf-diagnostics")] + { + let total_bytes = self.total_bytes_written.load(Ordering::Relaxed); + perf_debug!( + write_num = self.writes_count.load(Ordering::Relaxed), + total_bytes, + buf_len, + "ChannelWriter::write failed - channel closed" + ); + } + io::Error::other(ChannelWriterError::ChannelClosed) + })?; + + #[cfg(feature = "perf-diagnostics")] + { + let total = self.total_bytes_written.fetch_add(buf_len as u64, Ordering::Relaxed) + buf_len as u64; + perf_trace!( + write_num = self.writes_count.load(Ordering::Relaxed), + buf_len, + total_bytes_written = total, + "ChannelWriter::write completed" + ); + } + + Ok(buf_len) } fn flush(&mut self) -> io::Result<()> { + perf_trace!("ChannelWriter::flush called (no-op)"); Ok(()) } } @@ -41,10 +92,47 @@ impl io::Write for ChannelWriter { #[derive(Debug)] pub(crate) struct ChannelWriterReceiver { receiver: tokio::sync::mpsc::Receiver>, + #[cfg(feature = "perf-diagnostics")] + reads_count: AtomicU64, + #[cfg(feature = "perf-diagnostics")] + total_bytes_read: AtomicU64, } impl ChannelWriterReceiver { pub(crate) async fn recv(&mut self) -> Option> { - self.receiver.recv().await + #[cfg(feature = "perf-diagnostics")] + { + let read_num = self.reads_count.fetch_add(1, Ordering::Relaxed) + 1; + perf_trace!(read_num, "ChannelWriterReceiver::recv - waiting for data"); + } + + let result = self.receiver.recv().await; + + #[cfg(feature = "perf-diagnostics")] + { + let read_num = self.reads_count.load(Ordering::Relaxed); + match &result { + Some(data) => { + let data_len = data.len(); + let total = self.total_bytes_read.fetch_add(data_len as u64, Ordering::Relaxed) + data_len as u64; + perf_trace!( + read_num, + data_len, + total_bytes_read = total, + "ChannelWriterReceiver::recv - received data" + ); + } + None => { + let total = self.total_bytes_read.load(Ordering::Relaxed); + perf_trace!( + read_num, + total_bytes_read = total, + "ChannelWriterReceiver::recv - channel closed (None)" + ); + } + } + } + + result } } diff --git a/crates/video-streamer/src/streamer/iter.rs b/crates/video-streamer/src/streamer/iter.rs index 27cce1561..75a337702 100644 --- a/crates/video-streamer/src/streamer/iter.rs +++ b/crates/video-streamer/src/streamer/iter.rs @@ -138,7 +138,11 @@ where } Ok(false) => {} Ok(true) => { - trace!(last_tag_position = self.previous_emitted_tag_postion, last_key_frame_info =?self.last_key_frame_info, "Key Frame Found"); + perf_trace!( + last_tag_position = self.previous_emitted_tag_postion, + last_key_frame_info = ?self.last_key_frame_info, + "Key Frame Found" + ); match self.last_key_frame_info { LastKeyFrameInfo::NotMet { cluster_timestamp, @@ -197,7 +201,7 @@ where } pub(crate) fn rollback_to_last_successful_tag(&mut self) -> anyhow::Result<()> { - debug!( + perf_debug!( last_tag_position = self.previous_emitted_tag_postion, "Rolling back to last successful tag" ); diff --git a/crates/video-streamer/src/streamer/mod.rs b/crates/video-streamer/src/streamer/mod.rs index 95cee4b82..3642270b4 100644 --- a/crates/video-streamer/src/streamer/mod.rs +++ b/crates/video-streamer/src/streamer/mod.rs @@ -80,7 +80,7 @@ pub fn webm_stream( ); let mut header_writer = HeaderWriter::new(chunk_writer); - debug!(?headers); + perf_debug!(?headers); for header in &headers { header_writer.write(header)?; } @@ -115,7 +115,7 @@ pub fn webm_stream( CorruptedFileError::InvalidTagData { .. }, )))) | None => { - trace!("End of file reached or invalid tag data hit, retrying"); + perf_trace!("End of file reached or invalid tag data hit, retrying"); if retry_count >= MAX_RETRY_COUNT { anyhow::bail!("reached max retry count, the webm iterator cannot proceed with the current streaming file"); } diff --git a/crates/video-streamer/src/streamer/tag_writers.rs b/crates/video-streamer/src/streamer/tag_writers.rs index caf336498..3eb3af597 100644 --- a/crates/video-streamer/src/streamer/tag_writers.rs +++ b/crates/video-streamer/src/streamer/tag_writers.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "perf-diagnostics")] +use std::time::Instant; + use anyhow::Context; use cadeau::xmf::vpx::{VpxCodec, VpxDecoder, VpxEncoder}; use webm_iterable::errors::TagWriterError; @@ -5,11 +8,17 @@ use webm_iterable::matroska_spec::{Master, MatroskaSpec, SimpleBlock}; use webm_iterable::{WebmWriter, WriteOptions}; use super::block_tag::VideoBlock; +use super::channel_writer::ChannelWriterError; use crate::StreamingConfig; use crate::debug::mastroka_spec_name; const VPX_EFLAG_FORCE_KF: u32 = 0x00000001; +#[cfg(feature = "perf-diagnostics")] +fn duration_as_millis_u64(duration: std::time::Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + fn write_unknown_sized_element(writer: &mut WebmWriter, tag: &MatroskaSpec) -> Result<(), TagWriterError> where T: std::io::Write, @@ -54,6 +63,7 @@ where } } +#[derive(Debug, Clone, Copy)] enum CutBlockState { HaventMet, AtCutBlock, @@ -93,6 +103,13 @@ where encoder: VpxEncoder, decoder: VpxDecoder, cut_block_state: CutBlockState, + + #[cfg(feature = "perf-diagnostics")] + stream_start: Instant, + #[cfg(feature = "perf-diagnostics")] + last_report_at: Instant, + #[cfg(feature = "perf-diagnostics")] + frames_reencoded: u64, } /// A token type that enforces the one-time transition of cut block state. @@ -103,12 +120,41 @@ where T: std::io::Write, { fn new(config: EncodeWriterConfig, writer: HeaderWriter) -> anyhow::Result<(Self, CutBlockHitMarker)> { + perf_trace!( + width = config.width, + height = config.height, + threads = config.threads, + codec = ?config.codec, + "CutClusterWriter::new - building VPX decoder" + ); + let decoder = VpxDecoder::builder() .threads(config.threads) .width(config.width) .height(config.height) .codec(config.codec) - .build()?; + .build() + .inspect_err(|error| { + error!( + error = %error, + width = config.width, + height = config.height, + threads = config.threads, + codec = ?config.codec, + "VpxDecoder build failed" + ); + })?; + + perf_trace!( + width = config.width, + height = config.height, + threads = config.threads, + codec = ?config.codec, + bitrate = 256 * 1024, + timebase_num = 1, + timebase_den = 1000, + "CutClusterWriter::new - building VPX encoder" + ); let encoder = VpxEncoder::builder() .timebase_num(1) @@ -118,7 +164,20 @@ where .height(config.height) .threads(config.threads) .bitrate(256 * 1024) - .build()?; + .build() + .inspect_err(|error| { + error!( + error = %error, + width = config.width, + height = config.height, + threads = config.threads, + codec = ?config.codec, + bitrate = 256 * 1024, + "VpxEncoder build failed - this is likely VpxCodecInvalidParam" + ); + })?; + + perf_trace!("CutClusterWriter created successfully - decoder and encoder initialized"); let HeaderWriter { writer } = writer; Ok(( @@ -128,6 +187,12 @@ where encoder, decoder, cut_block_state: CutBlockState::HaventMet, + #[cfg(feature = "perf-diagnostics")] + stream_start: Instant::now(), + #[cfg(feature = "perf-diagnostics")] + last_report_at: Instant::now(), + #[cfg(feature = "perf-diagnostics")] + frames_reencoded: 0, }, CutBlockHitMarker, )) @@ -144,22 +209,47 @@ where { #[instrument(skip(self, tag))] pub(crate) fn write(&mut self, tag: MatroskaSpec) -> anyhow::Result { + let tag_name = mastroka_spec_name(&tag); + perf_trace!( + tag_name = %tag_name, + cluster_timestamp = ?self.cluster_timestamp, + cut_block_state = ?self.cut_block_state, + "CutClusterWriter::write called" + ); + match tag { MatroskaSpec::Timestamp(timestamp) => { + perf_trace!( + timestamp, + previous_cluster_timestamp = ?self.cluster_timestamp, + "Updating cluster_timestamp" + ); self.cluster_timestamp = Some(timestamp); return Ok(WriterResult::Continue); } - MatroskaSpec::BlockGroup(Master::Full(_)) | MatroskaSpec::SimpleBlock(_) => {} + MatroskaSpec::BlockGroup(Master::Full(_)) | MatroskaSpec::SimpleBlock(_) => { + perf_trace!(tag_name = %tag_name, "Processing block tag"); + } MatroskaSpec::BlockGroup(Master::End) | MatroskaSpec::BlockGroup(Master::Start) => { + error!( + tag_name = %tag_name, + "Unsupported BlockGroup Start/End tag received" + ); // If this happens, check the webm iterator cache tag parameter on new function anyhow::bail!("blockGroup start and end tags are not supported"); } _ => { + perf_trace!(tag_name = %tag_name, "Skipping non-block tag"); return Ok(WriterResult::Continue); } } let video_block = VideoBlock::new(tag, self.cluster_timestamp)?; + perf_trace!( + block_timestamp = video_block.timestamp, + cluster_timestamp = ?self.cluster_timestamp, + "VideoBlock created" + ); self.process_current_block(&video_block)?; @@ -167,39 +257,176 @@ where } fn reencode(&mut self, video_block: &VideoBlock, is_key_frame: bool) -> anyhow::Result>> { + let timestamp = video_block.timestamp; + let flags = if is_key_frame { VPX_EFLAG_FORCE_KF } else { 0 }; + + #[cfg(feature = "perf-diagnostics")] + let decode_started_at = Instant::now(); + perf_trace!( + timestamp, + is_key_frame, + flags, + "Reencode: getting frame from video block" + ); + let frame = video_block.get_frame()?; - self.decoder.decode(&frame)?; + let frame_size = frame.len(); + perf_trace!(frame_size, timestamp, "Reencode: decoding frame"); + + self.decoder.decode(&frame).inspect_err(|error| { + error!( + error = %error, + frame_size, + timestamp, + "VPX decoder.decode() failed" + ); + })?; + { - let image = self.decoder.next_frame()?; - self.encoder.encode_frame( - &image, - video_block.timestamp.into(), - 30, - if is_key_frame { VPX_EFLAG_FORCE_KF } else { 0 }, - )?; + #[cfg(feature = "perf-diagnostics")] + let decode_ms = duration_as_millis_u64(decode_started_at.elapsed()); + #[cfg(feature = "perf-diagnostics")] + let encode_started_at = Instant::now(); + let image = self.decoder.next_frame().inspect_err(|error| { + error!(error = %error, timestamp, "VPX decoder.next_frame() failed"); + })?; + + perf_trace!( + timestamp, + is_key_frame, + flags, + duration = 30, + "Reencode: encoding frame" + ); + + self.encoder + .encode_frame(&image, video_block.timestamp.into(), 30, flags) + .inspect_err(|error| { + error!( + error = %error, + timestamp, + is_key_frame, + flags, + "VPX encoder.encode_frame() failed - likely VpxCodecInvalidParam" + ); + })?; + + #[cfg(feature = "perf-diagnostics")] + { + let encode_ms = duration_as_millis_u64(encode_started_at.elapsed()); + let wall_elapsed_ms = duration_as_millis_u64(self.stream_start.elapsed()); + self.frames_reencoded += 1; + + // PERF-HYPOTHESIS: This log is intended to prove/disprove whether decode+encode throughput is too slow + // to follow the recording in near real-time. + if encode_ms >= 50 || self.frames_reencoded.is_multiple_of(30) { + info!( + prefix = "[LibVPx-Performance-Hypothesis]", + frames_reencoded = self.frames_reencoded, + wall_elapsed_ms, + decode_ms, + encode_ms, + force_kf = (flags & VPX_EFLAG_FORCE_KF) != 0, + input_frame_bytes = frame_size, + "Reencode timing" + ); + } else { + perf_trace!( + prefix = "[LibVPx-Performance-Hypothesis]", + frames_reencoded = self.frames_reencoded, + wall_elapsed_ms, + decode_ms, + encode_ms, + force_kf = (flags & VPX_EFLAG_FORCE_KF) != 0, + input_frame_bytes = frame_size, + "Reencode timing" + ); + } + } } - let frame = self.encoder.next_frame()?; + + let frame = self.encoder.next_frame().inspect_err(|error| { + error!(error = %error, timestamp, "VPX encoder.next_frame() failed"); + })?; + + perf_trace!( + timestamp, + output_frame_size = frame.as_ref().map(|f| f.len()), + "Reencode completed" + ); Ok(frame) } + #[cfg(feature = "perf-diagnostics")] + fn maybe_report_realtime_ratio(&mut self, current_block_absolute_time: u64, media_advanced_ms: u64) { + // Report at most once per second to keep logs readable. + if self.last_report_at.elapsed().as_secs_f32() < 1.0 { + return; + } + self.last_report_at = Instant::now(); + + let wall_elapsed_ms = duration_as_millis_u64(self.stream_start.elapsed()); + let ratio = if wall_elapsed_ms == 0 { + 0.0 + } else { + media_advanced_ms as f64 / wall_elapsed_ms as f64 + }; + + info!( + prefix = "[LibVPx-Performance-Hypothesis]", + wall_elapsed_ms, + current_block_absolute_time, + media_advanced_ms, + realtime_ratio = ratio, + frames_reencoded = self.frames_reencoded, + "Stream advancement" + ); + } + + #[cfg(not(feature = "perf-diagnostics"))] + fn maybe_report_realtime_ratio(&mut self, _current_block_absolute_time: u64, _media_advanced_ms: u64) { + let _ = &self.cut_block_state; + } + fn process_current_block(&mut self, current_video_block: &VideoBlock) -> anyhow::Result<()> { + #[cfg(feature = "perf-diagnostics")] + let block_timestamp = current_video_block.timestamp; + perf_trace!( + block_timestamp, + cut_block_state = ?self.cut_block_state, + "Processing current block" + ); + let frame = self.reencode(current_video_block, true)?; let Some(frame) = frame else { + perf_trace!(block_timestamp, "No frame available from encoder - skipping"); // No frame available from the encoder, proceed to the next return Ok(()); }; + #[cfg(feature = "perf-diagnostics")] + let frame_size = frame.len(); + perf_trace!(block_timestamp, frame_size, "Frame available from encoder"); + let block = match self.cut_block_state { CutBlockState::HaventMet => { + perf_trace!(block_timestamp, "State is HaventMet - not writing block yet"); return Ok(()); } CutBlockState::AtCutBlock => { + let cut_block_absolute_time = current_video_block.absolute_timestamp()?; + perf_trace!( + block_timestamp, + cut_block_absolute_time, + "State AtCutBlock - starting new cluster at time 0" + ); self.start_new_cluster(0)?; self.cut_block_state = CutBlockState::Met { - cut_block_absolute_time: current_video_block.absolute_timestamp()?, + cut_block_absolute_time, last_cluster_relative_time: 0, }; + perf_trace!(cut_block_absolute_time, "State transition: AtCutBlock -> Met"); SimpleBlock::new_uncheked(&frame, 1, 0, false, None, false, true) } @@ -209,7 +436,22 @@ where } => { let current_block_absolute_time = current_video_block.absolute_timestamp()?; let cluster_relative_timestamp = current_block_absolute_time - cut_block_absolute_time; + + self.maybe_report_realtime_ratio(current_block_absolute_time, cluster_relative_timestamp); + + perf_trace!( + current_block_absolute_time, + cut_block_absolute_time, + cluster_relative_timestamp, + "Processing block in Met state" + ); + if self.should_write_new_cluster(current_block_absolute_time) { + perf_trace!( + current_block_absolute_time, + cluster_relative_timestamp, + "Starting new cluster due to timestamp overflow" + ); self.start_new_cluster(cluster_relative_timestamp)?; self.cut_block_state = CutBlockState::Met { @@ -217,46 +459,75 @@ where last_cluster_relative_time: cluster_relative_timestamp, }; } - let relative_timestamp = current_video_block.absolute_timestamp()? - - cut_block_absolute_time - - self - .last_cluster_relative_time() - .context("missing last cluster relative time")?; + let last_cluster_rel = self + .last_cluster_relative_time() + .context("missing last cluster relative time")?; + let relative_timestamp = + current_video_block.absolute_timestamp()? - cut_block_absolute_time - last_cluster_rel; - trace!( - relative_timestamp, + perf_trace!( relative_timestamp, cut_block_absolute_time, current_block_absolute_timestamp = current_video_block.absolute_timestamp()?, - last_cluster_relative_time = self - .last_cluster_relative_time() - .context("missing last cluster relative time")?, + last_cluster_relative_time = last_cluster_rel, + "Calculated block relative timestamp" ); - let timestamp = i16::try_from(relative_timestamp)?; + + let timestamp = i16::try_from(relative_timestamp).inspect_err(|error| { + error!( + error = %error, + relative_timestamp, + "Failed to convert relative_timestamp to i16 - overflow" + ); + })?; SimpleBlock::new_uncheked(&frame, 1, timestamp, false, None, false, true) } }; + perf_trace!(block_timestamp, "Writing block to output"); self.write_block(block)?; Ok(()) } fn write_block(&mut self, block: SimpleBlock<'_>) -> anyhow::Result<()> { + perf_trace!("write_block - converting SimpleBlock to MatroskaSpec"); let block: MatroskaSpec = block.into(); - self.writer.write(&block)?; + if let Err(e) = self.writer.write(&block) { + // When the client disconnects or we are shutting down, the destination channel is closed. + // This is normal control flow and is handled at a higher level. + if let TagWriterError::WriteError { source } = &e + && source.kind() == std::io::ErrorKind::Other + && source + .get_ref() + .and_then(|inner| inner.downcast_ref::()) + .is_some_and(|inner| matches!(inner, &ChannelWriterError::ChannelClosed)) + { + perf_trace!("write_block aborted - destination channel closed"); + return Err(e.into()); + } + + error!(error = %e, "write_block failed"); + return Err(e.into()); + } + perf_trace!("write_block completed successfully"); Ok(()) } fn start_new_cluster(&mut self, time: u64) -> anyhow::Result<()> { + perf_trace!(time, is_first_cluster = (time == 0), "start_new_cluster called"); + if time != 0 { + perf_trace!("Writing Cluster::End for previous cluster"); self.writer.write(&MatroskaSpec::Cluster(Master::End))?; } let cluster_start = MatroskaSpec::Cluster(Master::Start); let timestamp = MatroskaSpec::Timestamp(time); + perf_trace!(time, "Writing Cluster::Start and Timestamp"); write_unknown_sized_element(&mut self.writer, &cluster_start)?; self.writer.write(×tamp)?; self.update_cluster_time(time); + perf_trace!(time, "start_new_cluster completed"); Ok(()) } @@ -303,7 +574,16 @@ where } pub(crate) fn mark_cut_block_hit(&mut self, _marker: CutBlockHitMarker) { + perf_trace!( + previous_state = ?format!("{:?}", match &self.cut_block_state { + CutBlockState::HaventMet => "HaventMet", + CutBlockState::AtCutBlock => "AtCutBlock", + CutBlockState::Met { .. } => "Met", + }), + "mark_cut_block_hit called - transitioning to AtCutBlock" + ); self.cut_block_state = CutBlockState::AtCutBlock; + perf_trace!("Cut block state is now AtCutBlock"); } } @@ -326,38 +606,75 @@ impl TryFrom<(Headers<'_>, &StreamingConfig)> for EncodeWriterConfig { let mut height = None; let mut codec = None; + perf_trace!( + headers_count = value.len(), + encoder_threads = config.encoder_threads.value, + "EncodeWriterConfig::try_from - parsing headers" + ); + for header in value { match header { - MatroskaSpec::CodecID(codec_id) => match codec_id.as_str() { - "V_VP8" | "vp8" => { - codec = Some(VpxCodec::VP8); + MatroskaSpec::CodecID(codec_id) => { + perf_trace!(codec_id = %codec_id, "Found CodecID header"); + match codec_id.as_str() { + "V_VP8" | "vp8" => { + perf_trace!("Codec identified as VP8"); + codec = Some(VpxCodec::VP8); + } + "V_VP9" | "vp9" => { + perf_trace!("Codec identified as VP9"); + codec = Some(VpxCodec::VP9); + } + _ => { + error!(codec_id = %codec_id, "Unknown codec in headers"); + anyhow::bail!("unknown codec: {}", codec_id); + } } - "V_VP9" | "vp9" => codec = Some(VpxCodec::VP9), - _ => { - anyhow::bail!("unknown codec: {}", codec_id); - } - }, + } MatroskaSpec::PixelWidth(w) => { + perf_trace!(width = w, "Found PixelWidth header"); width = Some(*w); } MatroskaSpec::PixelHeight(h) => { + perf_trace!(height = h, "Found PixelHeight header"); height = Some(*h); } _ => {} } } + perf_trace!( + width = ?width, + height = ?height, + codec = ?codec, + "Header parsing complete - creating config" + ); + + let threads = config + .encoder_threads + .value + .try_into() + .context("invalid thread count")?; + + let final_width = width.map(u32::try_from).context("no width specified in headers")??; + let final_height = height.map(u32::try_from).context("no height specified in headers")??; + let final_codec = codec.context("no codec specified in headers")?; + let config = EncodeWriterConfig { - threads: config - .encoder_threads - .value - .try_into() - .context("invalid thread count")?, - width: width.map(u32::try_from).context("no width specified")??, - height: height.map(u32::try_from).context("no height specified")??, - codec: codec.context("no codec specified")?, + threads, + width: final_width, + height: final_height, + codec: final_codec, }; + perf_debug!( + width = config.width, + height = config.height, + threads = config.threads, + codec = ?config.codec, + "EncodeWriterConfig created from headers" + ); + Ok(config) } } diff --git a/crates/video-streamer/testing-assets/uncued-recording.webm b/crates/video-streamer/testing-assets/uncued-recording.webm new file mode 100644 index 000000000..351618a12 Binary files /dev/null and b/crates/video-streamer/testing-assets/uncued-recording.webm differ diff --git a/crates/video-streamer/tests/common/mod.rs b/crates/video-streamer/tests/common/mod.rs new file mode 100644 index 000000000..917271ff8 --- /dev/null +++ b/crates/video-streamer/tests/common/mod.rs @@ -0,0 +1 @@ +pub(crate) mod vpx_kf_spike; diff --git a/crates/video-streamer/tests/common/vpx_kf_spike.rs b/crates/video-streamer/tests/common/vpx_kf_spike.rs new file mode 100644 index 000000000..dbe9944a2 --- /dev/null +++ b/crates/video-streamer/tests/common/vpx_kf_spike.rs @@ -0,0 +1,166 @@ +use std::convert::TryInto as _; +use std::io::BufReader; + +use cadeau::xmf::vpx::{VpxCodec, is_key_frame}; +use webm_iterable::WebmIterator; +use webm_iterable::errors::TagIteratorError; +use webm_iterable::matroska_spec::{Block, Master, MatroskaSpec, SimpleBlock}; + +#[derive(Debug, Clone)] +pub(crate) struct FrameAt { + pub(crate) abs_ms: u64, + pub(crate) is_key_frame: bool, + pub(crate) data: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct VpxStreamConfig { + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) codec: VpxCodec, +} + +pub(crate) fn parse_vpx_config_from_headers(headers: &[MatroskaSpec]) -> anyhow::Result { + let mut width: Option = None; + let mut height: Option = None; + let mut codec: Option = None; + + for header in headers { + match header { + MatroskaSpec::CodecID(codec_id) => { + codec = Some(match codec_id.as_str() { + "V_VP8" | "vp8" => VpxCodec::VP8, + "V_VP9" | "vp9" => VpxCodec::VP9, + _ => anyhow::bail!("unknown codec: {codec_id}"), + }); + } + MatroskaSpec::PixelWidth(w) => width = Some((*w).try_into()?), + MatroskaSpec::PixelHeight(h) => height = Some((*h).try_into()?), + _ => {} + } + } + + Ok(VpxStreamConfig { + width: width.ok_or_else(|| anyhow::anyhow!("missing PixelWidth in headers"))?, + height: height.ok_or_else(|| anyhow::anyhow!("missing PixelHeight in headers"))?, + codec: codec.ok_or_else(|| anyhow::anyhow!("missing CodecID in headers"))?, + }) +} + +pub(crate) fn read_headers_and_frames_until( + path: &std::path::Path, + end_ms: u64, +) -> anyhow::Result<(VpxStreamConfig, Vec)> { + let file = std::fs::File::open(path)?; + let mut itr = WebmIterator::new(BufReader::new(file), &[]); + itr.emit_master_end_when_eof(false); + + let mut headers = Vec::::new(); + let mut in_cluster = false; + let mut current_cluster_ts: Option = None; + let mut frames = Vec::::new(); + + for tag in &mut itr { + match tag { + Ok(MatroskaSpec::Cluster(Master::Start)) => { + in_cluster = true; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Cluster(Master::End)) => { + in_cluster = false; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Timestamp(ts)) => { + if in_cluster { + current_cluster_ts = Some(ts); + } else { + headers.push(MatroskaSpec::Timestamp(ts)); + } + } + Ok(tag @ MatroskaSpec::BlockGroup(_)) + | Ok(tag @ MatroskaSpec::SimpleBlock(_)) + | Ok(tag @ MatroskaSpec::CodecID(_)) + | Ok(tag @ MatroskaSpec::PixelWidth(_)) + | Ok(tag @ MatroskaSpec::PixelHeight(_)) => { + if !in_cluster { + headers.push(tag); + continue; + } + + let Some(cluster_ts) = current_cluster_ts else { + continue; + }; + + let (block_ts, is_kf, frame) = match &tag { + MatroskaSpec::SimpleBlock(data) => { + let sb = SimpleBlock::try_from(data)?; + let mut frames = sb.read_frame_data()?; + if frames.len() != 1 { + anyhow::bail!("laced SimpleBlock not supported (frames={})", frames.len()); + } + let frame = frames.pop().expect("len checked").data.to_vec(); + (sb.timestamp, sb.keyframe, frame) + } + MatroskaSpec::BlockGroup(Master::Full(children)) => { + let raw_block = children + .iter() + .find_map(|t| if let MatroskaSpec::Block(b) = t { Some(b) } else { None }) + .ok_or_else(|| anyhow::anyhow!("BlockGroup missing Block"))?; + let block = Block::try_from(raw_block)?; + let mut frames = block.read_frame_data()?; + if frames.len() != 1 { + anyhow::bail!("laced BlockGroup not supported (frames={})", frames.len()); + } + let frame = frames.pop().expect("len checked").data.to_vec(); + let is_kf = is_key_frame(&frame); + (block.timestamp, is_kf, frame) + } + _ => continue, + }; + + let cluster_ts_i64 = + i64::try_from(cluster_ts).map_err(|_| anyhow::anyhow!("cluster timestamp does not fit in i64"))?; + let abs_ms_i64 = cluster_ts_i64 + .checked_add(i64::from(block_ts)) + .ok_or_else(|| anyhow::anyhow!("block timestamp underflow/overflow"))?; + let abs_ms: u64 = abs_ms_i64 + .try_into() + .map_err(|_| anyhow::anyhow!("block absolute timestamp is negative: {abs_ms_i64}"))?; + + frames.push(FrameAt { + abs_ms, + is_key_frame: is_kf, + data: frame, + }); + + if end_ms <= abs_ms { + break; + } + } + Ok(other) => { + if !in_cluster { + headers.push(other); + } + } + Err(TagIteratorError::UnexpectedEOF { .. }) => break, + Err(e) => return Err(e.into()), + } + } + + let cfg = parse_vpx_config_from_headers(&headers)?; + Ok((cfg, frames)) +} + +pub(crate) fn find_cut_indices(frames: &[FrameAt], cut_at_ms: u64) -> anyhow::Result<(usize, usize)> { + let t20_idx = frames + .iter() + .position(|f| f.abs_ms >= cut_at_ms) + .ok_or_else(|| anyhow::anyhow!("no frame at/after cut_at_ms={cut_at_ms}"))?; + + let key_idx = (0..=t20_idx) + .rev() + .find(|&i| frames[i].is_key_frame) + .ok_or_else(|| anyhow::anyhow!("no keyframe found at/before cut point"))?; + + Ok((key_idx, t20_idx)) +} diff --git a/crates/video-streamer/tests/support/mod.rs b/crates/video-streamer/tests/support/mod.rs new file mode 100644 index 000000000..b5673b592 --- /dev/null +++ b/crates/video-streamer/tests/support/mod.rs @@ -0,0 +1,461 @@ +#![allow(dead_code)] + +use std::io::Cursor; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use anyhow::Context as _; +use futures::{Sink, Stream}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::sync::{Notify, Semaphore, broadcast, mpsc, oneshot}; +use tokio_util::bytes::Bytes; +use transport::{WsReadMsg, WsStream}; +use webm_iterable::WebmIterator; +use webm_iterable::errors::TagIteratorError; +use webm_iterable::matroska_spec::{Master, MatroskaSpec}; + +pub(crate) struct InMemoryWs { + rx: mpsc::UnboundedReceiver>, + tx: mpsc::UnboundedSender>, + pending_out: Vec, +} + +impl Stream for InMemoryWs { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + match Pin::new(&mut this.rx).poll_recv(cx) { + Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(WsReadMsg::Payload(Bytes::from(msg))))), + Poll::Ready(None) => Poll::Ready(Some(Ok(WsReadMsg::Close))), + Poll::Pending => Poll::Pending, + } + } +} + +impl Sink> for InMemoryWs { + type Error = std::io::Error; + + fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: Vec) -> Result<(), Self::Error> { + let this = self.get_mut(); + // `WsStream` maps each `AsyncWrite::poll_write` call to a `Sink::start_send` call. + // `tokio_util::codec::Framed` may split a single logical message across multiple writes, + // so we must coalesce writes and only publish the message on `poll_flush`. + this.pending_out.extend_from_slice(&item); + Ok(()) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + if this.pending_out.is_empty() { + return Poll::Ready(Ok(())); + } + + let msg = std::mem::take(&mut this.pending_out); + this.tx + .send(msg) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "in-memory ws receiver dropped"))?; + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +pub(crate) fn init_tracing() { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); +} + +pub(crate) fn global_stream_test_semaphore() -> &'static Semaphore { + static SEM: std::sync::OnceLock = std::sync::OnceLock::new(); + SEM.get_or_init(|| Semaphore::new(1)) +} + +pub(crate) fn asset_path(file_name: &str) -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + std::fs::canonicalize(manifest_dir.join("testing-assets").join(file_name)) + .unwrap_or_else(|e| panic!("failed to resolve asset path: {e:#}")) +} + +pub(crate) fn maybe_init_xmf() -> bool { + use cadeau::xmf; + + let Ok(path) = std::env::var("DGATEWAY_LIB_XMF_PATH") else { + tracing::warn!("Skipping test: DGATEWAY_LIB_XMF_PATH is not set"); + return false; + }; + + let Ok(canonical) = std::fs::canonicalize(&path) else { + tracing::warn!(%path, "Skipping test: DGATEWAY_LIB_XMF_PATH does not exist"); + return false; + }; + + let Some(path_str) = canonical.to_str() else { + tracing::warn!(path = %canonical.display(), "Skipping test: DGATEWAY_LIB_XMF_PATH is not valid UTF-8"); + return false; + }; + + // SAFETY: This is how the project loads XMF elsewhere (service startup / examples). + if let Err(error) = unsafe { xmf::init(path_str) } { + tracing::warn!(%error, %path_str, "Skipping test: failed to initialize XMF"); + return false; + } + + true +} + +pub(crate) fn extract_cluster_timestamps(webm_bytes: &[u8]) -> anyhow::Result> { + let mut itr = WebmIterator::new(Cursor::new(webm_bytes), &[]); + let mut saw_cluster = false; + let mut saw_timestamp = false; + let mut in_cluster = false; + let mut timestamps = Vec::::new(); + + for tag in &mut itr { + match tag { + Ok(MatroskaSpec::Cluster(Master::Start)) => { + saw_cluster = true; + in_cluster = true; + } + Ok(MatroskaSpec::Cluster(Master::End)) => { + in_cluster = false; + } + Ok(MatroskaSpec::Timestamp(ts)) => { + if in_cluster { + saw_timestamp = true; + timestamps.push(ts); + } + } + Ok(_) => {} + Err(TagIteratorError::UnexpectedEOF { .. }) => break, + Err(_) => break, + } + } + + if saw_cluster && saw_timestamp { + Ok(timestamps) + } else { + Ok(Vec::new()) + } +} + +pub(crate) fn extract_block_absolute_timestamps_ms(webm_bytes: &[u8]) -> anyhow::Result> { + use std::convert::TryInto as _; + + let mut itr = WebmIterator::new(Cursor::new(webm_bytes), &[]); + let mut in_cluster = false; + let mut current_cluster_ts: Option = None; + let mut out = Vec::::new(); + + for tag in &mut itr { + match tag { + Ok(MatroskaSpec::Cluster(Master::Start)) => { + in_cluster = true; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Cluster(Master::End)) => { + in_cluster = false; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Timestamp(ts)) => { + if in_cluster { + current_cluster_ts = Some(ts); + } + } + Ok(tag @ MatroskaSpec::SimpleBlock(_)) => { + if !in_cluster { + continue; + } + let Some(cluster_ts) = current_cluster_ts else { + continue; + }; + let simple_block: webm_iterable::matroska_spec::SimpleBlock<'_> = (&tag).try_into()?; + let cluster_ts_i64 = i64::try_from(cluster_ts).context("cluster timestamp does not fit in i64")?; + out.push(cluster_ts_i64 + i64::from(simple_block.timestamp)); + } + Ok(_) => {} + Err(TagIteratorError::UnexpectedEOF { .. }) => break, + Err(_) => break, + } + } + + Ok(out) +} + +pub(crate) fn extract_first_last_block_absolute_timestamps_ms_from_reader( + reader: R, +) -> anyhow::Result<(Option, Option, u64)> { + use std::convert::TryInto as _; + + let mut itr = WebmIterator::new(reader, &[]); + let mut in_cluster = false; + let mut current_cluster_ts: Option = None; + let mut first: Option = None; + let mut last: Option = None; + let mut count: u64 = 0; + + for tag in &mut itr { + match tag { + Ok(MatroskaSpec::Cluster(Master::Start)) => { + in_cluster = true; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Cluster(Master::End)) => { + in_cluster = false; + current_cluster_ts = None; + } + Ok(MatroskaSpec::Timestamp(ts)) => { + if in_cluster { + current_cluster_ts = Some(ts); + } + } + Ok(tag @ MatroskaSpec::SimpleBlock(_)) => { + if !in_cluster { + continue; + } + let Some(cluster_ts) = current_cluster_ts else { + continue; + }; + let simple_block: webm_iterable::matroska_spec::SimpleBlock<'_> = (&tag).try_into()?; + let cluster_ts_i64 = i64::try_from(cluster_ts).context("cluster timestamp does not fit in i64")?; + let abs = cluster_ts_i64 + i64::from(simple_block.timestamp); + first.get_or_insert(abs); + last = Some(abs); + count += 1; + } + Ok(_) => {} + Err(TagIteratorError::UnexpectedEOF { .. }) => break, + Err(_) => break, + } + } + + Ok((first, last, count)) +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct LiveWriteConfig { + pub(crate) chunk_size: usize, + pub(crate) delay: Duration, + pub(crate) pause_after_bytes: Option, + pub(crate) pause: Duration, + pub(crate) notify_every_n_writes: usize, + pub(crate) initial_burst_bytes: u64, +} + +impl Default for LiveWriteConfig { + fn default() -> Self { + Self { + chunk_size: 64 * 1024, + delay: Duration::from_millis(15), + pause_after_bytes: None, + pause: Duration::from_secs(0), + notify_every_n_writes: 1, + initial_burst_bytes: 0, + } + } +} + +#[derive(Debug)] +pub(crate) struct StreamHarness { + pub(crate) client_tx: mpsc::UnboundedSender>, + pub(crate) server_rx: mpsc::UnboundedReceiver>, + pub(crate) shutdown: Arc, + pub(crate) stream_task: tokio::task::JoinHandle>, + pub(crate) writer_task: tokio::task::JoinHandle>, +} + +pub(crate) fn unique_temp_dir(prefix: &str) -> PathBuf { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_nanos(); + std::env::temp_dir().join(format!("{prefix}-{}-{now}", std::process::id())) +} + +pub(crate) async fn spawn_live_file_writer( + asset: PathBuf, + dest: PathBuf, + written_tx: broadcast::Sender<()>, + cfg: LiveWriteConfig, +) -> tokio::task::JoinHandle> { + let mut src = tokio::fs::File::open(&asset) + .await + .unwrap_or_else(|e| panic!("failed to open asset {}: {e:#}", asset.display())); + + let mut dst_opts = tokio::fs::OpenOptions::new(); + dst_opts.create(true).write(true).truncate(true); + #[cfg(windows)] + { + dst_opts.share_mode(0x00000002 | 0x00000001 | 0x00000004); + } + let mut dest_file = dst_opts + .open(&dest) + .await + .unwrap_or_else(|e| panic!("failed to create temp input {}: {e:#}", dest.display())); + + tokio::spawn(async move { + let mut buf = vec![0u8; cfg.chunk_size]; + let mut total_written: u64 = 0; + let mut writes: usize = 0; + let mut paused = false; + + loop { + if !paused + && cfg + .pause_after_bytes + .is_some_and(|threshold| total_written >= threshold) + && cfg.pause > Duration::from_secs(0) + { + paused = true; + tokio::time::sleep(cfg.pause).await; + } + + let n = src.read(&mut buf).await?; + if n == 0 { + break; + } + + dest_file.write_all(&buf[..n]).await?; + dest_file.flush().await?; + total_written += n as u64; + writes += 1; + + if cfg.notify_every_n_writes != 0 && writes.is_multiple_of(cfg.notify_every_n_writes) { + let _ = written_tx.send(()); + } + + if cfg.delay > Duration::from_secs(0) + && (cfg.initial_burst_bytes == 0 || total_written >= cfg.initial_burst_bytes) + { + tokio::time::sleep(cfg.delay).await; + } + } + + let _ = written_tx.send(()); + Ok(()) + }) +} + +pub(crate) async fn spawn_stream_harness( + asset: PathBuf, + write_cfg: LiveWriteConfig, + encoder_threads: usize, +) -> StreamHarness { + spawn_stream_harness_delayed_start(asset, write_cfg, encoder_threads, Duration::from_secs(0)).await +} + +pub(crate) async fn spawn_stream_harness_delayed_start( + asset: PathBuf, + write_cfg: LiveWriteConfig, + encoder_threads: usize, + start_after: Duration, +) -> StreamHarness { + let tmp_dir = unique_temp_dir("video-streamer-webm_stream_harness"); + std::fs::create_dir_all(&tmp_dir).expect("create temp dir"); + let tmp_webm_path = tmp_dir.join("live_input.webm"); + + let (written_tx, _) = broadcast::channel::<()>(16); + let writer_task = spawn_live_file_writer(asset, tmp_webm_path.clone(), written_tx.clone(), write_cfg).await; + + if start_after > Duration::from_secs(0) { + tokio::time::sleep(start_after).await; + } + + let input = match video_streamer::ReOpenableFile::open(&tmp_webm_path) { + Ok(f) => f, + Err(e) => panic!("failed to open temp input {}: {e:#}", tmp_webm_path.display()), + }; + + let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded_channel::>(); + let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded_channel::>(); + + let server_ws = WsStream::new(InMemoryWs { + rx: client_to_server_rx, + tx: server_to_client_tx, + pending_out: Vec::new(), + }); + + let shutdown = Arc::new(Notify::new()); + let shutdown_for_stream = Arc::clone(&shutdown); + + let runtime_handle = tokio::runtime::Handle::current(); + let when_new_chunk_appended = move || { + let (tx, rx) = oneshot::channel(); + let mut r = written_tx.subscribe(); + runtime_handle.spawn(async move { + let _ = r.recv().await; + let _ = tx.send(()); + }); + rx + }; + + let stream_task = tokio::task::spawn_blocking(move || { + video_streamer::webm_stream( + server_ws, + input, + shutdown_for_stream, + video_streamer::StreamingConfig { + encoder_threads: video_streamer::config::CpuCount::new(encoder_threads), + }, + when_new_chunk_appended, + ) + }); + + StreamHarness { + client_tx: client_to_server_tx, + server_rx: server_to_client_rx, + shutdown, + stream_task, + writer_task, + } +} + +pub(crate) fn parse_server_message(msg: &[u8]) -> (u8, &[u8]) { + let Some((type_code, payload)) = msg.split_first() else { + panic!("received empty server message"); + }; + (*type_code, payload) +} + +pub(crate) async fn recv_server_message( + server_rx: &mut mpsc::UnboundedReceiver>, + timeout: Duration, +) -> Option> { + tokio::time::timeout(timeout, server_rx.recv()).await.ok().flatten() +} + +pub(crate) async fn shutdown_and_join(h: StreamHarness) { + shutdown_and_join_with_timeout(h, Duration::from_secs(20)).await; +} + +pub(crate) async fn shutdown_and_join_with_timeout(h: StreamHarness, timeout: Duration) { + h.shutdown.notify_waiters(); + drop(h.client_tx); + + let mut stream_task = h.stream_task; + match tokio::time::timeout(timeout, &mut stream_task).await { + Ok(joined) => { + joined + .expect("webm_stream task panicked") + .unwrap_or_else(|e| panic!("webm_stream returned error: {e:#}")); + } + Err(_) => { + stream_task.abort(); + } + } + + let _ = h.writer_task.await; +} diff --git a/crates/video-streamer/tests/vpx_kf_spike_repro.rs b/crates/video-streamer/tests/vpx_kf_spike_repro.rs new file mode 100644 index 000000000..6da2188ab --- /dev/null +++ b/crates/video-streamer/tests/vpx_kf_spike_repro.rs @@ -0,0 +1,155 @@ +use std::time::Instant; + +use cadeau::xmf::vpx::{VpxDecoder, VpxEncoder}; + +mod support; +use support::*; + +mod common; +use common::vpx_kf_spike::*; + +const VPX_EFLAG_FORCE_KF: u32 = 0x00000001; + +#[test] +#[ignore] +/// Minimal VPX-only reproduction of the production "progress stall" symptom. +/// +/// What this test is checking: +/// - Given an uncued, non-seekable WebM, the streamer "attaches" at ~20s and must cut into the live stream. +/// - To make the cut decodable, it rolls back to the closest preceding keyframe (K_closest), +/// decodes K_closest..T20 to establish reference state, then force-keyframe re-encodes frames from T20 onward. +/// - The performance regression we want to reproduce is that encoding becomes extremely slow after some number +/// of consecutive force-keyframes, which makes wall clock advance while output timeline barely advances. +/// +/// How this test checks it: +/// - Parses WebM tags to locate T20s and K_closest using Cluster/Timestamp + Block timestamps. +/// - Decodes frames from K_closest..T20 (decode-only warmup). +/// - Then decodes+encodes every subsequent frame as a keyframe, logging per-second samples and slow encodes. +/// +/// References: +/// - [WebM: Muxing Guidelines][webm-muxing-guidelines] +/// - [Matroska: SimpleBlock][matroska-simpleblock] +/// +/// [webm-muxing-guidelines]: https://www.webmproject.org/docs/container/#muxing-guidelines +/// [matroska-simpleblock]: https://www.matroska.org/technical/elements.html#simpleblock +fn vpx_force_kf_spike_min_repro_attach_20s() { + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let asset = asset_path("uncued-recording.webm"); + let cut_at_ms = 20_000u64; + let end_ms = 26_000u64; + let threads: u32 = 20; + + let (cfg, frames) = read_headers_and_frames_until(&asset, end_ms) + .unwrap_or_else(|e| panic!("failed to read frames from {}: {e:#}", asset.display())); + let (key_idx, t20_idx) = find_cut_indices(&frames, cut_at_ms).expect("failed to find cut indices"); + + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + asset = %asset.display(), + width = cfg.width, + height = cfg.height, + codec = ?cfg.codec, + threads, + frames_total = frames.len(), + key_idx, + t20_idx, + key_abs_ms = frames[key_idx].abs_ms, + t20_abs_ms = frames[t20_idx].abs_ms, + "KF spike repro setup" + ); + + let mut decoder = VpxDecoder::builder() + .threads(threads) + .width(cfg.width) + .height(cfg.height) + .codec(cfg.codec) + .build() + .expect("build decoder"); + + let mut encoder = VpxEncoder::builder() + .timebase_num(1) + .timebase_den(1000) + .codec(cfg.codec) + .width(cfg.width) + .height(cfg.height) + .threads(threads) + .bitrate(256 * 1024) + .build() + .expect("build encoder"); + + let started_at = Instant::now(); + + for (i, f) in frames.iter().enumerate().take(t20_idx).skip(key_idx) { + decoder + .decode(&f.data) + .unwrap_or_else(|e| panic!("decode warmup failed at idx={i}: {e:#}")); + let _ = decoder + .next_frame() + .unwrap_or_else(|e| panic!("next_frame warmup failed at idx={i}: {e:#}")); + } + + let mut frames_encoded: u64 = 0; + let mut max_encode_ms: u64 = 0; + let mut last_logged_sec: Option = None; + + for (i, f) in frames.iter().enumerate().skip(t20_idx) { + let decode_started_at = Instant::now(); + decoder + .decode(&f.data) + .unwrap_or_else(|e| panic!("decode failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let image = decoder + .next_frame() + .unwrap_or_else(|e| panic!("next_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let decode_ms = u64::try_from(decode_started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + + let encode_started_at = Instant::now(); + let pts = i64::try_from(f.abs_ms).unwrap_or(i64::MAX); + encoder + .encode_frame(&image, pts, 30, VPX_EFLAG_FORCE_KF) + .unwrap_or_else(|e| panic!("encode_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let encode_ms = u64::try_from(encode_started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + max_encode_ms = max_encode_ms.max(encode_ms); + frames_encoded += 1; + + let _ = encoder + .next_frame() + .unwrap_or_else(|e| panic!("encoder.next_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + + let sec = f.abs_ms / 1000; + let should_sample = matches!(sec, 20..=26) && last_logged_sec != Some(sec); + let wall_elapsed_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + if should_sample || encode_ms >= 50 { + last_logged_sec = Some(sec); + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + idx = i, + abs_ms = f.abs_ms, + second = sec, + frames_encoded, + wall_elapsed_ms, + decode_ms, + encode_ms, + max_encode_ms, + force_kf = true, + input_frame_bytes = f.data.len(), + "KF spike sample" + ); + } + + if end_ms <= f.abs_ms { + break; + } + } + + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + frames_encoded, + max_encode_ms, + wall_elapsed_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX), + "KF spike repro done" + ); +} diff --git a/crates/video-streamer/tests/vpx_kf_spike_repro_first_kf_only.rs b/crates/video-streamer/tests/vpx_kf_spike_repro_first_kf_only.rs new file mode 100644 index 000000000..ab1ff173d --- /dev/null +++ b/crates/video-streamer/tests/vpx_kf_spike_repro_first_kf_only.rs @@ -0,0 +1,154 @@ +use std::time::Instant; + +use cadeau::xmf::vpx::{VpxDecoder, VpxEncoder}; + +mod support; +use support::*; + +mod common; +use common::vpx_kf_spike::*; + +const VPX_EFLAG_FORCE_KF: u32 = 0x00000001; + +#[test] +#[ignore] +/// Control experiment for the KF-spike reproduction. +/// +/// What this test is checking: +/// - Same attach-at-20s cut workflow as `vpx_kf_spike_repro.rs`, but we only force the first post-cut frame (T20) as a keyframe. +/// - All subsequent frames are encoded without `VPX_EFLAG_FORCE_KF`. +/// +/// Expected outcome: +/// - If the regression is specifically triggered by *consecutive forced keyframes*, this variant should be much faster +/// (encode_ms should stay low and wall time should be close to media time). +/// +/// References: +/// - [WebM: Muxing Guidelines][webm-muxing-guidelines] +/// - [Matroska: SimpleBlock][matroska-simpleblock] +/// +/// [webm-muxing-guidelines]: https://www.webmproject.org/docs/container/#muxing-guidelines +/// [matroska-simpleblock]: https://www.matroska.org/technical/elements.html#simpleblock +fn vpx_force_only_first_cut_frame_min_repro_attach_20s() { + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let asset = asset_path("uncued-recording.webm"); + let cut_at_ms = 20_000u64; + let end_ms = 26_000u64; + let threads: u32 = 20; + + let (cfg, frames) = read_headers_and_frames_until(&asset, end_ms) + .unwrap_or_else(|e| panic!("failed to read frames from {}: {e:#}", asset.display())); + let (key_idx, t20_idx) = find_cut_indices(&frames, cut_at_ms).expect("failed to find cut indices"); + + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + asset = %asset.display(), + width = cfg.width, + height = cfg.height, + codec = ?cfg.codec, + threads, + frames_total = frames.len(), + key_idx, + t20_idx, + key_abs_ms = frames[key_idx].abs_ms, + t20_abs_ms = frames[t20_idx].abs_ms, + "First-KF-only repro setup" + ); + + let mut decoder = VpxDecoder::builder() + .threads(threads) + .width(cfg.width) + .height(cfg.height) + .codec(cfg.codec) + .build() + .expect("build decoder"); + + let mut encoder = VpxEncoder::builder() + .timebase_num(1) + .timebase_den(1000) + .codec(cfg.codec) + .width(cfg.width) + .height(cfg.height) + .threads(threads) + .bitrate(256 * 1024) + .build() + .expect("build encoder"); + + let started_at = Instant::now(); + + for (i, f) in frames.iter().enumerate().take(t20_idx).skip(key_idx) { + decoder + .decode(&f.data) + .unwrap_or_else(|e| panic!("decode warmup failed at idx={i}: {e:#}")); + let _ = decoder + .next_frame() + .unwrap_or_else(|e| panic!("next_frame warmup failed at idx={i}: {e:#}")); + } + + let mut frames_encoded: u64 = 0; + let mut max_encode_ms: u64 = 0; + let mut last_logged_sec: Option = None; + + for (i, f) in frames.iter().enumerate().skip(t20_idx) { + let force_kf = i == t20_idx; + let flags = if force_kf { VPX_EFLAG_FORCE_KF } else { 0 }; + + let decode_started_at = Instant::now(); + decoder + .decode(&f.data) + .unwrap_or_else(|e| panic!("decode failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let image = decoder + .next_frame() + .unwrap_or_else(|e| panic!("next_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let decode_ms = u64::try_from(decode_started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + + let encode_started_at = Instant::now(); + let pts = i64::try_from(f.abs_ms).unwrap_or(i64::MAX); + encoder + .encode_frame(&image, pts, 30, flags) + .unwrap_or_else(|e| panic!("encode_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + let encode_ms = u64::try_from(encode_started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + max_encode_ms = max_encode_ms.max(encode_ms); + frames_encoded += 1; + + let _ = encoder + .next_frame() + .unwrap_or_else(|e| panic!("encoder.next_frame failed at idx={i} abs_ms={}: {e:#}", f.abs_ms)); + + let sec = f.abs_ms / 1000; + let should_sample = matches!(sec, 20..=26) && last_logged_sec != Some(sec); + let wall_elapsed_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + if should_sample || encode_ms >= 50 { + last_logged_sec = Some(sec); + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + idx = i, + abs_ms = f.abs_ms, + second = sec, + frames_encoded, + wall_elapsed_ms, + decode_ms, + encode_ms, + max_encode_ms, + force_kf, + input_frame_bytes = f.data.len(), + "First-KF-only sample" + ); + } + + if end_ms <= f.abs_ms { + break; + } + } + + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + frames_encoded, + max_encode_ms, + wall_elapsed_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX), + "First-KF-only repro done" + ); +} diff --git a/crates/video-streamer/tests/webm_stream_correctness.rs b/crates/video-streamer/tests/webm_stream_correctness.rs new file mode 100644 index 000000000..d7fe78f7a --- /dev/null +++ b/crates/video-streamer/tests/webm_stream_correctness.rs @@ -0,0 +1,381 @@ +use std::time::Duration; + +mod support; +use support::*; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that `webm_stream` (no-cues input) eventually emits a stream whose first emitted Cluster timeline starts at 0. +/// +/// How: +/// - Simulate a live-growing `.webm` file by appending bytes in small chunks. +/// - Drive the protocol with `Start` then repeated `Pull`. +/// - Collect `ServerMessage::Chunk` payload bytes and parse Cluster timestamps from the output. +/// - Assert that the first observed Cluster timestamp is exactly 0 (the cut point timeline reset contract). +/// +/// References: +/// - [WebM: Muxing Guidelines][webm-muxing-guidelines] +/// - [Matroska: Cluster][matroska-cluster] +/// +/// [webm-muxing-guidelines]: https://www.webmproject.org/docs/container/#muxing-guidelines +/// [matroska-cluster]: https://www.matroska.org/technical/elements.html#cluster +async fn timeline_starts_at_zero_after_cut_uncued_recording() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let mut saw_metadata = false; + let mut collected = Vec::::new(); + let mut first_ts: Option = None; + + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(180) { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + + let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(10)).await else { + continue; + }; + let (ty, payload) = parse_server_message(&msg); + + match ty { + 0 => { + collected.extend_from_slice(payload); + if let Ok(timestamps) = extract_cluster_timestamps(&collected) + && let Some(&ts0) = timestamps.first() + { + first_ts.get_or_insert(ts0); + if ts0 == 0 { + break; + } + } + } + 1 => saw_metadata = true, + 2 => panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)), + 3 => break, + other => panic!("unknown server message type code: {other}"), + } + } + + assert!(saw_metadata, "never received metadata"); + assert_eq!(first_ts, Some(0), "never observed first cluster timestamp 0"); + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that the emitted SimpleBlock timeline is monotonic and makes forward progress. +/// +/// How: +/// - Drive the stream long enough to observe a number of SimpleBlocks. +/// - Parse Cluster timestamp + SimpleBlock relative timestamp, and compute absolute timestamps in milliseconds. +/// - Assert absolute timestamps are monotonic non-decreasing and that they advance by at least 500ms. +/// +/// References: +/// - [Matroska: SimpleBlock][matroska-simpleblock] +/// +/// [matroska-simpleblock]: https://www.matroska.org/technical/elements.html#simpleblock +async fn block_timestamps_monotonic_and_advances_uncued_recording() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let mut collected = Vec::::new(); + let mut abs_ts: Vec = Vec::new(); + + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(60) { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + + let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(10)).await else { + continue; + }; + let (ty, payload) = parse_server_message(&msg); + + if ty == 2 { + panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)); + } + if ty != 0 { + continue; + } + + collected.extend_from_slice(payload); + abs_ts = extract_block_absolute_timestamps_ms(&collected).unwrap_or_default(); + if abs_ts.len() >= 10 { + break; + } + } + + assert!(abs_ts.len() >= 2, "did not observe enough block timestamps"); + let first = abs_ts[0]; + let mut last = first; + for &ts in &abs_ts[1..] { + assert!(ts >= last, "block absolute timestamps went backwards: {last} -> {ts}"); + last = ts; + } + assert!( + last - first >= 100, + "block timestamps did not advance: first={first} last={last}" + ); + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that a `Start` message triggers exactly one metadata message (until `Start` is sent again). +/// +/// How: +/// - Send `Start` and then read messages for a short time window. +/// - Assert the first metadata is received. +/// - Continue pulling and assert no additional metadata arrives without another `Start`. +async fn start_sends_metadata_once() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let mut metadata_count = 0u32; + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(10) { + let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(2)).await else { + continue; + }; + let (ty, payload) = parse_server_message(&msg); + match ty { + 1 => metadata_count += 1, + 2 => panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)), + _ => {} + } + if metadata_count >= 1 { + break; + } + } + + assert_eq!(metadata_count, 1, "expected one metadata message after Start"); + + for _ in 0..20 { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + if let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(2)).await { + let (ty, payload) = parse_server_message(&msg); + if ty == 1 { + panic!("unexpected additional metadata without Start"); + } + if ty == 2 { + panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)); + } + } + } + + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that `Pull` without `Start` does not crash the stream and does not send `ServerMessage::Error`. +/// +/// How: +/// - Never send `Start`. +/// - Send several `Pull` messages and accept any output except `Error`. +/// - This pins current behavior while allowing the implementation to decide whether metadata is required. +async fn pull_without_start_does_not_error_or_hang() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + let mut saw_error = false; + + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(10) { + if h.client_tx.send(vec![1]).is_err() { + break; + } + if let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_millis(500)).await { + let (ty, _payload) = parse_server_message(&msg); + if ty == 2 { + saw_error = true; + break; + } + } + } + + assert!(!saw_error, "received ServerMessage::Error without Start"); + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that when the live input stops growing temporarily (EOF), the stream can recover when growth resumes. +/// +/// How: +/// - Writer appends for a while, then pauses, then resumes appending. +/// - Client keeps issuing `Pull` and expects to eventually receive chunks both before and after the pause. +/// - This validates the EOF wait + rollback + continue path. +async fn pause_then_resume_recovers_from_eof_wait() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let write_cfg = LiveWriteConfig { + pause_after_bytes: Some(256 * 1024), + pause: Duration::from_secs(3), + ..LiveWriteConfig::default() + }; + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), write_cfg, 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let mut got_any_chunk = false; + let mut got_chunk_after_stall = false; + let mut stalled_once = false; + + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(60) { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + + match recv_server_message(&mut h.server_rx, Duration::from_millis(600)).await { + Some(msg) => { + let (ty, payload) = parse_server_message(&msg); + if ty == 2 { + panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)); + } + if ty == 0 { + if got_any_chunk && stalled_once { + got_chunk_after_stall = true; + break; + } + got_any_chunk = true; + } + } + None => { + if got_any_chunk { + stalled_once = true; + } + } + } + } + + assert!(got_any_chunk, "never received any chunk"); + assert!( + got_chunk_after_stall, + "never observed a chunk after a stall (pause/resume)" + ); + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that slow pulling does not break the stream (no errors, still produces chunks). +/// +/// How: +/// - Send `Start` then issue `Pull` at a low rate. +/// - Assert we still receive at least one chunk and never an `Error`. +async fn slow_pull_still_produces_chunks() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let mut got_chunk = false; + for _ in 0..20 { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + if let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(3)).await { + let (ty, payload) = parse_server_message(&msg); + if ty == 2 { + panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)); + } + if ty == 0 { + got_chunk = true; + } + } + tokio::time::sleep(Duration::from_millis(300)).await; + } + + assert!(got_chunk, "never received any chunk with slow pull"); + shutdown_and_join(h).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore] +/// Tests that a client disconnect mid-stream causes `webm_stream` to exit cleanly. +/// +/// How: +/// - Start streaming and pull until at least one chunk is received. +/// - Drop the client sender (simulating a disconnect). +/// - Assert the streaming task exits within a timeout and returns `Ok(())`. +async fn client_disconnect_exits_cleanly() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let mut h = spawn_stream_harness(asset_path("uncued-recording.webm"), LiveWriteConfig::default(), 1).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let started_at = tokio::time::Instant::now(); + while started_at.elapsed() < Duration::from_secs(30) { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + if let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(2)).await { + let (ty, payload) = parse_server_message(&msg); + if ty == 2 { + panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)); + } + if ty == 0 { + break; + } + } + } + + drop(h.client_tx); + h.shutdown.notify_waiters(); + + tokio::time::timeout(Duration::from_secs(20), h.stream_task) + .await + .expect("timeout waiting for webm_stream to exit") + .expect("webm_stream task panicked") + .unwrap_or_else(|e| panic!("webm_stream returned error: {e:#}")); + + let _ = h.writer_task.await; +} diff --git a/crates/video-streamer/tests/webm_stream_perf.rs b/crates/video-streamer/tests/webm_stream_perf.rs new file mode 100644 index 000000000..1fc7b0403 --- /dev/null +++ b/crates/video-streamer/tests/webm_stream_perf.rs @@ -0,0 +1,137 @@ +use std::io::{BufReader, Write as _}; +use std::time::Duration; + +mod support; +use support::*; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore] +/// Reproduces the production performance symptom where wall clock advances but output media time barely moves. +/// +/// This is intentionally a local-only perf reproduction: +/// - The input is an uncued, non-seekable WebM "recording" that grows over time. +/// - The stream attaches after 20s and cuts near the live tail (keyframe rollback + timeline reset). +/// - The test measures output timeline advancement using WebM timestamps (Cluster/Timestamp + SimpleBlock timestamp). +/// +/// References: +/// - [WebM: Muxing Guidelines][webm-muxing-guidelines] +/// - [Matroska: Cluster][matroska-cluster] +/// - [Matroska: SimpleBlock][matroska-simpleblock] +/// +/// [webm-muxing-guidelines]: https://www.webmproject.org/docs/container/#muxing-guidelines +/// [matroska-cluster]: https://www.matroska.org/technical/elements.html#cluster +/// [matroska-simpleblock]: https://www.matroska.org/technical/elements.html#simpleblock +async fn webm_stream_progress_stall_attach_at_20s() { + let _permit = global_stream_test_semaphore() + .acquire() + .await + .expect("failed to acquire global test semaphore"); + init_tracing(); + if !maybe_init_xmf() { + return; + } + + let asset = asset_path("uncued-recording.webm"); + let asset_duration = Duration::from_secs(38); + let start_after = Duration::from_secs(20); + let run_for = Duration::from_secs(20); + let encoder_threads = 20usize; + + let asset_len = std::fs::metadata(&asset) + .unwrap_or_else(|e| panic!("failed to stat asset {}: {e:#}", asset.display())) + .len(); + + let write_cfg = { + let mut cfg = LiveWriteConfig::default(); + let chunks = asset_len.div_ceil(cfg.chunk_size as u64).max(1); + let asset_duration_ms = u64::try_from(asset_duration.as_millis()).unwrap_or(u64::MAX); + let per_chunk_ms = asset_duration_ms.div_ceil(chunks).max(1); + cfg.delay = Duration::from_millis(per_chunk_ms); + cfg.initial_burst_bytes = 0; + cfg.notify_every_n_writes = 1; + cfg + }; + + let out_dir = unique_temp_dir("video-streamer-webm_stream_perf"); + std::fs::create_dir_all(&out_dir).expect("create perf output dir"); + let out_path = out_dir.join("shadow_output.webm"); + let mut out_file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&out_path) + .unwrap_or_else(|e| panic!("failed to create output {}: {e:#}", out_path.display())); + + let mut h = spawn_stream_harness_delayed_start(asset.clone(), write_cfg, encoder_threads, start_after).await; + assert!(h.client_tx.send(vec![0]).is_ok(), "failed to send Start"); + + let started_at = tokio::time::Instant::now(); + let mut pulls_sent: u64 = 0; + let mut chunks_received: u64 = 0; + let mut bytes_received: u64 = 0; + + while started_at.elapsed() < run_for { + assert!(h.client_tx.send(vec![1]).is_ok(), "failed to send Pull"); + pulls_sent += 1; + + let Some(msg) = recv_server_message(&mut h.server_rx, Duration::from_secs(3)).await else { + continue; + }; + let (ty, payload) = parse_server_message(&msg); + match ty { + 0 => { + out_file + .write_all(payload) + .unwrap_or_else(|e| panic!("failed to write output: {e:#}")); + bytes_received += payload.len() as u64; + chunks_received += 1; + } + 1 => {} + 2 => panic!("received ServerMessage::Error: {}", String::from_utf8_lossy(payload)), + 3 => break, + _ => {} + } + } + + shutdown_and_join_with_timeout(h, Duration::from_secs(10)).await; + out_file + .flush() + .unwrap_or_else(|e| panic!("failed to flush output {}: {e:#}", out_path.display())); + drop(out_file); + + let wall_elapsed_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX); + let file = std::fs::File::open(&out_path) + .unwrap_or_else(|e| panic!("failed to open output {}: {e:#}", out_path.display())); + let (first, last, blocks) = extract_first_last_block_absolute_timestamps_ms_from_reader(BufReader::new(file)) + .unwrap_or_else(|e| panic!("failed to parse output timestamps: {e:#}")); + + let media_advanced_ms = match (first, last) { + (Some(f), Some(l)) => u64::try_from((l - f).max(0)).unwrap_or(0), + _ => 0, + }; + let ratio = if wall_elapsed_ms == 0 { + 0.0 + } else { + media_advanced_ms as f64 / wall_elapsed_ms as f64 + }; + + tracing::info!( + prefix = "[LibVPx-Performance-Hypothesis]", + asset = %asset.display(), + out = %out_path.display(), + encoder_threads, + pulls_sent, + chunks_received, + bytes_received, + blocks, + wall_elapsed_ms, + media_advanced_ms, + realtime_ratio = ratio, + "Perf reproduction summary" + ); + + assert!( + ratio < 0.2, + "did not reproduce progress stall: realtime_ratio={ratio} wall_elapsed_ms={wall_elapsed_ms} media_advanced_ms={media_advanced_ms}" + ); +}