diff --git a/Cargo.lock b/Cargo.lock index 41665ff..c8823de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -77,6 +98,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ashpd" version = "0.13.10" @@ -85,13 +112,52 @@ checksum = "3118453e020b8e3e0da25ef9a1d0d51d668874358af11aded9d91a8b9c25f323" dependencies = [ "enumflags2", "futures-util", - "getrandom", + "getrandom 0.4.2", "serde", "serde_repr", "tokio", "zbus", ] +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -132,6 +198,41 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "untrusted", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bindgen" version = "0.72.1" @@ -151,6 +252,15 @@ dependencies = [ "syn", ] +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -196,9 +306,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -230,6 +354,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -281,12 +415,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -296,6 +449,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.8.0" @@ -311,6 +470,30 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -326,6 +509,135 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dimpl" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7afb6878ee6941d3ee770bd8a391c0c083ee2102a7e8e91a730fb722ef1e46b9" +dependencies = [ + "aes", + "arrayvec", + "aws-lc-rs", + "ccm", + "der", + "log", + "nom 8.0.0", + "once_cell", + "pkcs8", + "rand", + "rcgen", + "sec1", + "signature", + "spki", + "subtle", + "time", + "x509-cert", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -371,6 +683,12 @@ dependencies = [ "linux-raw-sys 0.6.5", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -478,12 +796,24 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-core" version = "0.3.32" @@ -539,6 +869,39 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -547,7 +910,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -609,6 +972,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840878b6e30d40e5bda1a7116100f1a18b7bdb91814513b87be80bbfb5d41879" +dependencies = [ + "crc", + "serde", + "str0m-proto", + "subtle", + "tracing", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -630,6 +1015,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.98" @@ -670,6 +1065,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + [[package]] name = "libspa" version = "0.9.2" @@ -795,6 +1199,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -805,6 +1243,15 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -817,6 +1264,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -833,6 +1286,15 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -867,12 +1329,37 @@ dependencies = [ "system-deps", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -919,12 +1406,71 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "aws-lc-rs", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.12.3" @@ -960,6 +1506,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -986,12 +1541,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "sctp-proto" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8423ea59db998985015bc5d0145837eab48f60ec449a2dc01f5870499afe0a4" +dependencies = [ + "bytes", + "crc", + "log", + "rand", + "rustc-hash", + "slab", + "thiserror", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "zeroize", +] + [[package]] name = "semver" version = "1.0.28" @@ -1107,6 +1698,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "slab" version = "0.4.12" @@ -1129,12 +1726,75 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "str0m" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca05746700d3621a27d7b99beaf2e724f8940608cbab00c3e4ebd974620668af" +dependencies = [ + "arrayvec", + "base64ct", + "combine", + "dimpl", + "fastrand", + "is", + "sctp-proto", + "serde", + "str0m-aws-lc-rs", + "str0m-proto", + "subtle", + "time", + "tracing", +] + +[[package]] +name = "str0m-aws-lc-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1908a6439b68fd22c275d44cbf4b50b2a75cc45f2b626160d87a194023c18fcd" +dependencies = [ + "aws-lc-rs", + "dimpl", + "str0m-proto", + "time", +] + +[[package]] +name = "str0m-proto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02836118cf7384413d7e8beb8b9ab56a4007d1b6514c11df35150a2e7aef8f1a" +dependencies = [ + "base64ct", + "dimpl", + "fastrand", + "serde", + "subtle", + "time", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -1146,6 +1806,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "system-deps" version = "7.0.8" @@ -1172,7 +1843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -1207,6 +1878,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.52.3" @@ -1331,6 +2033,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "uds_windows" version = "1.2.1" @@ -1366,6 +2074,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1401,6 +2115,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1773,6 +2493,7 @@ dependencies = [ "ashpd", "clap", "crossbeam-channel", + "dirs", "drm", "drm-fourcc", "ffmpeg-next", @@ -1780,8 +2501,10 @@ dependencies = [ "libspa", "mio", "pipewire", + "serde_json", "signal-hook", "signal-hook-mio", + "str0m", "tokio", "tracing", "tracing-subscriber", @@ -1791,6 +2514,45 @@ dependencies = [ "zbus", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "zbus" version = "5.15.0" @@ -1847,6 +2609,32 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 81290c6..15f0785 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,6 @@ tokio = { version = "1", features = ["rt"] } pipewire = { version = "0.9", features = ["v0_3_45"] } libspa = "0.9" crossbeam-channel = "0.5" +str0m = "0.20" +serde_json = "1" +dirs = "6" diff --git a/src/args.rs b/src/args.rs index ca06c04..57275b7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,9 +3,9 @@ use clap::Parser; #[derive(Parser, Debug, Clone)] #[command(name = "wl-webrtc", about = "Wayland screen capture and encoding tool")] pub struct Args { - /// Output file path (e.g., output.mp4, output.mkv) + /// Output file path (e.g., output.mp4, output.mkv). Optional when using --port for WebRTC mode #[arg(short, long)] - pub output: String, + pub output: Option, /// Wayland output name to capture #[arg(long)] @@ -43,7 +43,11 @@ pub struct Args { #[arg(long)] pub backend: Option, - /// Port for WebTransport server (Phase 2, unused in MVP) + /// Port for WebRTC HTTP signaling server; 0 keeps MP4 file output mode #[arg(long, default_value_t = 0)] pub port: u16, + + /// Force re-authorization dialog (ignore saved portal restore token) + #[arg(long)] + pub no_persist: bool, } diff --git a/src/avhw.rs b/src/avhw.rs index 9a73a69..245a783 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -596,13 +596,18 @@ impl EncState { // SwEncState - VAAPI GPU downscale + software H.264 encode // --------------------------------------------------------------------------- +pub enum FrameOutput { + Muxer(ff::format::context::Output), + Channel(crossbeam_channel::Sender>), +} + pub struct SwEncState { hw_dev: AvHwDevCtx, frames_rgb: AvHwFrameCtx, filter_graph: ff::filter::Graph, sws_ctx: *mut ffi::SwsContext, enc_video: ff::codec::encoder::video::Video, - octx: ff::format::context::Output, + output: Option, yuv_frame: *mut ffi::AVFrame, starting_timestamp: Option, frames_written: bool, @@ -651,7 +656,52 @@ impl SwEncState { filter_graph, sws_ctx, enc_video, - octx, + output: Some(FrameOutput::Muxer(octx)), + yuv_frame, + starting_timestamp: None, + frames_written: false, + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_webrtc( + drm_device: &Path, + width: u32, + height: u32, + enc_width: u32, + enc_height: u32, + fps: u32, + bitrate: u64, + gop_size: u32, + tx: crossbeam_channel::Sender>, + ) -> Result { + tracing::info!( + "SwEncState::new_webrtc: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264 -> WebRTC" + ); + + let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?; + let frames_rgb = + AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?; + let filter_graph = build_swenc_filter_graph( + &hw_dev, + &frames_rgb, + width, + height, + enc_width, + enc_height, + fps, + )?; + let sws_ctx = create_nv12_to_yuv420p_sws(enc_width, enc_height)?; + let enc_video = create_software_h264_encoder(enc_width, enc_height, fps, bitrate, gop_size)?; + let yuv_frame = alloc_yuv420p_frame(enc_width, enc_height)?; + + Ok(Self { + hw_dev, + frames_rgb, + filter_graph, + sws_ctx, + enc_video, + output: Some(FrameOutput::Channel(tx)), yuv_frame, starting_timestamp: None, frames_written: false, @@ -704,7 +754,6 @@ impl SwEncState { } } - // SAFETY: Sending a null frame flushes the encoder without transferring ownership. unsafe { let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null()); if ret < 0 && ret != ffi::AVERROR_EOF { @@ -715,9 +764,10 @@ impl SwEncState { self.drain_encoder(start_ts)?; if self.frames_written { - self.octx - .write_trailer() - .map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?; + if let Some(FrameOutput::Muxer(ref mut octx)) = self.output { + octx.write_trailer() + .map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?; + } } Ok(()) @@ -793,25 +843,39 @@ impl SwEncState { bail!("avcodec_receive_packet failed: error {ret}"); } - let enc_tb = self.enc_video.time_base(); - let stream_tb = unsafe { - let streams = (*self.octx.as_ptr()).streams; - let st = *streams.add(0); - ff::Rational::from((*st).time_base) - }; - pkt.rescale_ts(enc_tb, stream_tb); + match self.output { + Some(FrameOutput::Muxer(ref mut octx)) => { + let enc_tb = self.enc_video.time_base(); + let stream_tb = unsafe { + let streams = (*octx.as_ptr()).streams; + let st = *streams.add(0); + ff::Rational::from((*st).time_base) + }; + pkt.rescale_ts(enc_tb, stream_tb); - if let Some(pts) = pkt.pts() { - pkt.set_pts(Some(pts - start_ts)); - } - if let Some(dts) = pkt.dts() { - pkt.set_dts(Some(dts - start_ts)); - } + if let Some(pts) = pkt.pts() { + pkt.set_pts(Some(pts - start_ts)); + } + if let Some(dts) = pkt.dts() { + pkt.set_dts(Some(dts - start_ts)); + } - pkt.set_stream(0); - pkt.write_interleaved(&mut self.octx) - .map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?; - self.frames_written = true; + pkt.set_stream(0); + pkt.write_interleaved(octx) + .map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?; + self.frames_written = true; + } + Some(FrameOutput::Channel(ref tx)) => { + let data: &[u8] = unsafe { + std::slice::from_raw_parts( + (*pkt.as_mut_ptr()).data, + (*pkt.as_mut_ptr()).size as usize, + ) + }; + let _ = tx.send(data.to_vec()); + } + None => {} + } } Ok(()) } @@ -1115,6 +1179,54 @@ fn create_software_h264_muxer( Ok((enc_video, octx)) } +fn create_software_h264_encoder( + width: u32, + height: u32, + fps: u32, + bitrate: u64, + gop_size: u32, +) -> Result { + let codec = ff::encoder::find_by_name("libx264") + .or_else(|| ff::encoder::find_by_name("libopenh264")) + .ok_or_else(|| anyhow::anyhow!("No H.264 software encoder found"))?; + let codec_name = codec.name().to_string(); + + let mut enc = { + let ctx = ff::codec::Context::new_with_codec(codec); + ctx.encoder().video()? + }; + enc.set_width(width); + enc.set_height(height); + enc.set_format(ff::format::Pixel::YUV420P); + enc.set_bit_rate(bitrate as usize); + enc.set_gop(gop_size); + enc.set_time_base(ff::Rational::new(1, fps as i32)); + enc.set_max_b_frames(0); + + if codec_name == "libx264" { + unsafe { + let key = CString::new("preset").unwrap(); + let val = CString::new("ultrafast").unwrap(); + ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0); + let key = CString::new("tune").unwrap(); + let val = CString::new("zerolatency").unwrap(); + ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0); + let key = CString::new("threads").unwrap(); + let val = CString::new("6").unwrap(); + ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0); + let key = CString::new("x264opts").unwrap(); + let val = CString::new("repeat_headers=1").unwrap(); + ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0); + } + } + + let opened = enc + .open() + .map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?; + tracing::info!("WebRTC encoder: {codec_name} {width}x{height} @ {fps}fps {bitrate}bps"); + Ok(opened.0) +} + // --------------------------------------------------------------------------- // Filter graph (inline) // --------------------------------------------------------------------------- diff --git a/src/cap_portal.rs b/src/cap_portal.rs index 9c157f1..d8e8592 100644 --- a/src/cap_portal.rs +++ b/src/cap_portal.rs @@ -12,6 +12,7 @@ // - crossbeam-channel: 高性能有界通道,用于线程间帧传递 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::thread::{self, JoinHandle}; @@ -98,12 +99,10 @@ impl CapPortal { /// 4. 创建 eventfd 对,用于线程安全的关闭信号传递 /// 5. 启动 PipeWire 捕获线程 pub fn new(args: &Args) -> Result { - // 创建独立的 Tokio 运行时,仅用于 setup_portal 中的异步 Portal D-Bus 调用 let rt = Runtime::new()?; - // 通过 Portal 获取 PipeWire 连接 fd 和节点 ID - // block_on 在此处同步等待异步 Portal 调用完成 - let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?; + let no_persist = args.no_persist; + let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal(no_persist).await })?; let (frame_tx, frame_rx) = bounded(16); let (event_tx, event_rx) = bounded(8); @@ -172,44 +171,50 @@ impl CapPortal { /// 5. 打开 PipeWire 远程连接,获取文件描述符 /// /// 返回 (PipeWire fd, node_id),供 PipeWire 线程连接使用 - async fn setup_portal() -> Result<(OwnedFd, u32)> { + async fn setup_portal(no_persist: bool) -> Result<(OwnedFd, u32)> { use ashpd::desktop::screencast::{ CursorMode, Screencast, SelectSourcesOptions, SourceType, }; use ashpd::desktop::PersistMode; - // 创建 Screencast D-Bus 代理,与桌面环境的 Portal 服务通信 let proxy = Screencast::new() .await .map_err(|e| anyhow::anyhow!("Failed to create Screencast proxy: {e}"))?; - // 创建 ScreenCast 会话(每个会话对应一次屏幕录制请求) let session = proxy .create_session(Default::default()) .await .map_err(|e| anyhow::anyhow!("Failed to create ScreenCast session: {e}"))?; - // 配置录制源选择参数: - // - CursorMode::Embedded: 光标嵌入到帧数据中(而非单独的元数据) - // - SourceType::Monitor: 仅捕获显示器(不捕获窗口) - // - multiple: false: 不允许多源选择 - // - PersistMode::DoNot: 不持久化会话(每次需要重新授权) + let version_supported = proxy.version() >= 4; + + let (persist_mode, saved_token) = if !no_persist && version_supported { + let token = load_restore_token(); + if token.is_some() { + tracing::info!("Attempting to restore portal session with saved token"); + } + (PersistMode::ExplicitlyRevoked, token) + } else { + (PersistMode::DoNot, None) + }; + + let mut options = SelectSourcesOptions::default() + .set_cursor_mode(CursorMode::Embedded) + .set_sources(ashpd::enumflags2::BitFlags::from(SourceType::Monitor)) + .set_multiple(false) + .set_persist_mode(persist_mode); + + if let Some(ref token) = saved_token { + options = options.set_restore_token(token.as_str()); + } + proxy - .select_sources( - &session, - SelectSourcesOptions::default() - .set_cursor_mode(CursorMode::Embedded) - .set_sources(ashpd::enumflags2::BitFlags::from(SourceType::Monitor)) - .set_multiple(false) - .set_persist_mode(PersistMode::DoNot), - ) + .select_sources(&session, options) .await .map_err(|e| { - anyhow::anyhow!("屏幕共享权限被拒绝 / Screen sharing permission denied: {e}") + anyhow::anyhow!("Screen sharing permission denied: {e}") })?; - // 启动录制会话,此时桌面环境会弹出权限确认对话框 - // 用户确认后返回包含 PipeWire 流信息的响应 let response = proxy .start(&session, None, Default::default()) .await @@ -217,18 +222,19 @@ impl CapPortal { .response() .map_err(|e| anyhow::anyhow!("ScreenCast response error: {e}"))?; - // 获取返回的第一个(也是唯一的)视频流 - // 每个流对应一个 PipeWire 节点 + if !no_persist && version_supported { + if let Some(new_token) = response.restore_token() { + save_restore_token(new_token); + } + } + let stream = response .streams() .first() .ok_or_else(|| anyhow::anyhow!("No streams returned from ScreenCast"))?; - // 提取 PipeWire 节点 ID,用于后续连接到该节点的视频流 let node_id = stream.pipe_wire_node_id(); - // 打开 PipeWire 远程连接,获取文件描述符 - // 这个 fd 允许直接与 PipeWire 守护进程通信 let fd = proxy .open_pipe_wire_remote(&session, Default::default()) .await @@ -240,6 +246,30 @@ impl CapPortal { } } +fn token_path() -> PathBuf { + let base = dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from("/tmp")); + base.join("wl-webrtc").join("portal-restore-token") +} + +fn load_restore_token() -> Option { + let path = token_path(); + let token = std::fs::read_to_string(&path).ok()?; + let trimmed = token.trim().to_string(); + if trimmed.is_empty() { None } else { Some(trimmed) } +} + +fn save_restore_token(token: &str) { + let path = token_path(); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + match std::fs::write(&path, token) { + Ok(()) => tracing::info!("Saved portal restore token"), + Err(e) => tracing::warn!("Failed to save restore token: {e}"), + } +} + impl Drop for CapPortal { /// 析构时安全关闭 PipeWire 线程 /// diff --git a/src/lib.rs b/src/lib.rs index f71fb0c..22d024d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod fps_limit; pub mod state; pub mod state_portal; pub mod transform; +pub mod webrtc; diff --git a/src/main.rs b/src/main.rs index 074a5ec..9df2698 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod fps_limit; // 帧率限制器 mod state; // wlr-screencopy 后端的主状态机 mod state_portal; // Portal/PipeWire 后端的主状态机 mod transform; // 图像变换(旋转/翻转) +mod webrtc; // WebRTC 传输(str0m Sans-IO) use crate::args::Args; use crate::cap_wlr_screencopy::CapWlrScreencopy; @@ -49,6 +50,7 @@ fn main() -> Result<()> { } else { tracing::Level::INFO }) + .with_writer(std::io::stderr) .init(); tracing::info!("wl-webrtc starting"); @@ -59,6 +61,10 @@ fn main() -> Result<()> { anyhow::bail!("HEVC not supported in MVP. Use --codec h264"); } + if args.output.is_none() && args.port == 0 { + anyhow::bail!("Either --output or --port is required"); + } + // 自动检测当前桌面环境可用的截屏后端 // 会尝试列举 Wayland 全局对象,判断合成器是否支持 wlr-screencopy 协议 let backend = crate::backend_detect::detect_backend(&args)?; diff --git a/src/state.rs b/src/state.rs index c2ab75a..a82a3bd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -613,7 +613,7 @@ impl State { .unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100); let enc = match crate::avhw::create_encoder( &drm_path, - Path::new(&self.args.output), + Path::new(self.args.output.as_deref().expect("output required for MP4 mode")), width, height, fps, diff --git a/src/state_portal.rs b/src/state_portal.rs index 8ab1815..176e466 100644 --- a/src/state_portal.rs +++ b/src/state_portal.rs @@ -8,6 +8,7 @@ use anyhow::{bail, Result}; use crate::args::Args; use crate::avhw::{self, SwEncState}; use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame}; +use crate::webrtc::WebRtcState; /// 门户采集的阶段状态 /// - WaitingForFormat: 等待接收到第一帧 DMA-BUF 以确定视频格式参数 @@ -32,6 +33,10 @@ pub struct StatePortal { start_time: Option, last_stats_time: Option, last_stats_frames: u64, + webrtc: Option, + webrtc_tx: Option>>, + webrtc_rx: Option>>, + webrtc_frames_sent: u64, } impl StatePortal { @@ -48,6 +53,14 @@ impl StatePortal { let cap = CapPortal::new(&args)?; + let (webrtc, webrtc_tx, webrtc_rx) = if args.port > 0 { + let (tx, rx) = crossbeam_channel::bounded(32); + let wrtc = WebRtcState::new(args.port, args.fps)?; + (Some(wrtc), Some(tx), Some(rx)) + } else { + (None, None, None) + }; + Ok(Self { stage: PortalStage::WaitingForFormat, enc: None, @@ -59,6 +72,10 @@ impl StatePortal { start_time: None, last_stats_time: None, last_stats_frames: 0, + webrtc, + webrtc_tx, + webrtc_rx, + webrtc_frames_sent: 0, }) } @@ -68,6 +85,9 @@ impl StatePortal { /// `block=false` 时使用 try_recv 非阻塞检查。 /// 返回 `Ok(true)` 表示已处理事件,`Ok(false)` 表示暂无数据。 pub fn poll_and_encode(&mut self, block: bool) -> Result { + // WebRTC: process signaling, network, and forward encoded frames + self.poll_webrtc()?; + if let Ok(ctrl) = self.cap.event_receiver().try_recv() { match ctrl { PwCtrlEvent::StreamEnded => { @@ -119,19 +139,39 @@ impl StatePortal { let actual_bitrate = self.args.bitrate.unwrap_or_else(|| { 2 * (enc_width as u64) * (enc_height as u64) * (self.args.fps as u64) / 100 }); - let actual_gop_size = self.args.gop_size.unwrap_or(self.args.fps); + let actual_gop_size = self.args.gop_size.unwrap_or_else(|| { + if self.webrtc_tx.is_some() { + (self.args.fps / 2).max(10) + } else { + self.args.fps + } + }); - let enc = avhw::SwEncState::new( - &drm_path, - self.args.output.as_ref(), - frame.width, - frame.height, - enc_width, - enc_height, - self.args.fps, - actual_bitrate, - actual_gop_size, - )?; + let enc = if let Some(ref tx) = self.webrtc_tx { + avhw::SwEncState::new_webrtc( + &drm_path, + frame.width, + frame.height, + enc_width, + enc_height, + self.args.fps, + actual_bitrate, + actual_gop_size, + tx.clone(), + )? + } else { + avhw::SwEncState::new( + &drm_path, + std::path::Path::new(self.args.output.as_deref().expect("output required for MP4 mode")), + frame.width, + frame.height, + enc_width, + enc_height, + self.args.fps, + actual_bitrate, + actual_gop_size, + )? + }; self.enc = Some(enc); self.stage = PortalStage::Streaming; @@ -145,6 +185,9 @@ impl StatePortal { } } + // WebRTC: drain encoded frames produced by this poll before returning. + self.poll_webrtc()?; + Ok(true) } @@ -266,6 +309,29 @@ impl StatePortal { pub fn is_errored(&self) -> bool { self.errored } + + fn poll_webrtc(&mut self) -> Result<()> { + let Some(ref mut wrtc) = self.webrtc else { return Ok(()); }; + + wrtc.handle_signaling()?; + wrtc.poll_and_feed()?; + + if let Some(ref rx) = self.webrtc_rx { + let mut count = 0u32; + while let Ok(data) = rx.try_recv() { + count += 1; + if let Err(e) = wrtc.write_h264_frame(&data, self.webrtc_frames_sent, self.args.fps) { + tracing::debug!("WebRTC write frame error: {e}"); + } + self.webrtc_frames_sent = self.webrtc_frames_sent.saturating_add(1); + } + if count > 0 { + tracing::info!("WebRTC forwarded {count} frames from channel"); + } + } + + Ok(()) + } } impl Drop for StatePortal { diff --git a/src/webrtc.rs b/src/webrtc.rs new file mode 100644 index 0000000..b104572 --- /dev/null +++ b/src/webrtc.rs @@ -0,0 +1,531 @@ +// WebRTC 传输模块 — 使用 str0m (Sans-IO) 将 H.264 编码帧推送到浏览器 +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, UdpSocket}; +use std::time::Instant; + +use anyhow::{bail, Result}; +use str0m::change::SdpOffer; +use str0m::format::Codec; +use str0m::media::{Frequency, MediaKind, MediaTime, Mid, Pt}; +use str0m::net::{Protocol, Receive}; +use str0m::{Candidate, Event, IceConnectionState, Input, Output, Rtc, RtcConfig}; + +// ── 嵌入式 HTML 测试页面 ────────────────────────────────────────────────── + +const HTML_PAGE: &str = r#" + +wl-webrtc P0 + + +
Connecting...
+ +

+
+"#;
+
+// ── WebRTC 状态 ───────────────────────────────────────────────────────────
+
+pub struct WebRtcState {
+    signal_listener: TcpListener,
+    inner: Option,
+    fps: u32,
+}
+
+struct WebRtcInner {
+    rtc: Rtc,
+    socket: UdpSocket,
+    udp_addr: SocketAddr,
+    video_mid: Option,
+    video_pt: Option,
+    connected: bool,
+    need_keyframe: bool,
+    rtp_clock: u32,
+    buf: Vec,
+}
+
+impl WebRtcState {
+    pub fn new(port: u16, fps: u32) -> Result {
+        let signal_listener = TcpListener::bind(format!("0.0.0.0:{port}"))?;
+        signal_listener.set_nonblocking(true)?;
+        tracing::info!("WebRTC signaling on http://0.0.0.0:{port}/");
+
+        Ok(Self {
+            signal_listener,
+            inner: None,
+            fps,
+        })
+    }
+
+    pub fn handle_signaling(&mut self) -> Result {
+        let mut handled = false;
+        loop {
+            let (mut stream, _addr) = match self.signal_listener.accept() {
+                Ok(s) => s,
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
+                Err(e) => bail!("TCP accept error: {e}"),
+            };
+            handled = true;
+            stream.set_nonblocking(true)?;
+
+            let mut req = vec![0u8; 65536];
+            let n = match stream.read(&mut req) {
+                Ok(n) => n,
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
+                Err(e) => {
+                    tracing::warn!("TCP read error: {e}");
+                    continue;
+                }
+            };
+            let req_str = String::from_utf8_lossy(&req[..n]);
+
+            if req_str.starts_with("GET / ")
+                || req_str.starts_with("GET /sdp ")
+                    && !req_str.contains("Content-Type: application/json")
+            {
+                let resp = format!(
+                    "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+                    HTML_PAGE.len(),
+                    HTML_PAGE
+                );
+                let _ = stream.write_all(resp.as_bytes());
+            } else if req_str.starts_with("POST /sdp") {
+                let body = extract_body(&req_str);
+                if body.is_empty() {
+                    let resp = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\nempty body";
+                    let _ = stream.write_all(resp.as_bytes());
+                    continue;
+                }
+
+                match WebRtcInner::new(self.fps)
+                    .and_then(|mut new_inner| {
+                        let answer_json = new_inner.handle_sdp_offer(body.as_bytes())?;
+                        Ok((new_inner, answer_json))
+                    }) {
+                    Ok((new_inner, answer_json)) => {
+                        let replacing = self.inner.is_some();
+                        self.inner = Some(new_inner);
+                        if replacing {
+                            tracing::info!("Replaced WebRTC connection (old dropped)");
+                        } else {
+                            tracing::info!("New WebRTC connection");
+                        }
+
+                        let resp = format!(
+                            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
+                            answer_json.len(),
+                            answer_json
+                        );
+                        let _ = stream.write_all(resp.as_bytes());
+                    }
+                    Err(e) => {
+                        tracing::error!("SDP offer handling failed: {e}");
+                        let resp = format!("HTTP/1.1 500 Error\r\nConnection: close\r\n\r\n{e}");
+                        let _ = stream.write_all(resp.as_bytes());
+                    }
+                }
+            } else {
+                let resp = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n";
+                let _ = stream.write_all(resp.as_bytes());
+            }
+        }
+        Ok(handled)
+    }
+
+    pub fn poll_rtc(&mut self) -> Result<()> {
+        if let Some(inner) = self.inner.as_mut() {
+            if inner.poll_rtc()? {
+                tracing::warn!("WebRTC connection closed/failed; clearing connection state");
+                self.inner = None;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn feed_network(&mut self) -> Result<()> {
+        if let Some(inner) = self.inner.as_mut() {
+            inner.feed_network()?;
+        }
+        Ok(())
+    }
+
+    pub fn poll_and_feed(&mut self) -> Result<()> {
+        self.poll_rtc()?;
+        self.feed_network()?;
+        self.poll_rtc()
+    }
+
+    pub fn write_h264_frame(&mut self, data: &[u8], frame_number: u64, fps: u32) -> Result<()> {
+        if let Some(inner) = self.inner.as_mut() {
+            inner.write_h264_frame(data, frame_number, fps)?;
+        }
+        Ok(())
+    }
+
+    pub fn is_connected(&self) -> bool {
+        self.inner.as_ref().is_some_and(WebRtcInner::is_connected)
+    }
+}
+
+impl WebRtcInner {
+    fn new(fps: u32) -> Result {
+        let _ = fps;
+        let mut rtc = RtcConfig::new().build(Instant::now());
+
+        let socket = UdpSocket::bind("0.0.0.0:0")?;
+        socket.set_nonblocking(true)?;
+        let local_addr = socket.local_addr()?;
+
+        let lan_ip = local_ip().unwrap_or_else(|| {
+            tracing::warn!("Failed to detect LAN IP, falling back to 127.0.0.1");
+            "127.0.0.1".to_string()
+        });
+        let candidate_addr: SocketAddr = format!("{lan_ip}:{}", local_addr.port()).parse()?;
+        let candidate = Candidate::host(candidate_addr, "udp")
+            .map_err(|e| anyhow::anyhow!("candidate: {e}"))?;
+        rtc.add_local_candidate(candidate);
+        tracing::info!("WebRTC UDP: {candidate_addr} (bound 0.0.0.0)");
+
+        Ok(Self {
+            rtc,
+            socket,
+            udp_addr: candidate_addr,
+            video_mid: None,
+            video_pt: None,
+            connected: false,
+            need_keyframe: false,
+            rtp_clock: 0,
+            buf: vec![0u8; 65535],
+        })
+    }
+
+    fn handle_sdp_offer(&mut self, body: &[u8]) -> Result {
+        let offer: SdpOffer = serde_json::from_slice(body)
+            .map_err(|e| anyhow::anyhow!("parse SDP offer: {e}"))?;
+
+        let answer = self
+            .rtc
+            .sdp_api()
+            .accept_offer(offer)
+            .map_err(|e| anyhow::anyhow!("accept_offer: {e}"))?;
+
+        self.need_keyframe = true;
+        tracing::info!("SDP exchange complete, waiting for ICE/DTLS...");
+
+        self.discover_video_params();
+
+        let answer_json =
+            serde_json::to_vec(&answer).map_err(|e| anyhow::anyhow!("serialize answer: {e}"))?;
+
+        String::from_utf8(answer_json).map_err(|e| anyhow::anyhow!("answer utf8: {e}"))
+    }
+
+    fn discover_video_params(&mut self) {
+        for s in ["0", "1", "2", "3"] {
+            let mid: Mid = s.into();
+            if let Some(media) = self.rtc.media(mid) {
+                if media.kind() == MediaKind::Video {
+                    tracing::info!("Found video media: mid={mid}");
+                    self.video_mid = Some(mid);
+                    break;
+                }
+            }
+        }
+
+        if let Some(mid) = self.video_mid {
+            if let Some(writer) = self.rtc.writer(mid) {
+                for pp in writer.payload_params() {
+                    tracing::debug!("Codec: pt={:?} spec={:?}", pp.pt(), pp.spec());
+                    if pp.spec().codec.is_video() && pp.spec().codec == Codec::H264 {
+                        self.video_pt = Some(pp.pt());
+                        tracing::info!("H.264 payload type: {:?}", pp.pt());
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    fn poll_rtc(&mut self) -> Result {
+        loop {
+            match self.rtc.poll_output() {
+                Ok(Output::Transmit(t)) => {
+                    tracing::info!("TX {} bytes -> {}", t.contents.len(), t.destination);
+                    if let Err(e) = self.socket.send_to(&t.contents, t.destination) {
+                        tracing::warn!("UDP send error: {e}");
+                    }
+                }
+                Ok(Output::Event(e)) => {
+                    tracing::info!("RTC event: {e:?}");
+                    match &e {
+                        Event::Connected => {
+                            tracing::info!("WebRTC connected!");
+                            self.connected = true;
+                            self.need_keyframe = true;
+                            self.discover_video_params();
+                        }
+                        Event::IceConnectionStateChange(IceConnectionState::Disconnected) => {
+                            tracing::warn!("WebRTC disconnected");
+                            self.connected = false;
+                        }
+                        Event::MediaAdded(ma) => {
+                            tracing::info!("Media added: mid={:?}", ma.mid);
+                        }
+                        _ => {
+                            tracing::debug!("WebRTC event: {:?}", e);
+                        }
+                    }
+                }
+                Ok(Output::Timeout(_t)) => break,
+                Err(e) => {
+                    tracing::error!("rtc.poll_output error: {e}");
+                    break;
+                }
+            }
+        }
+        Ok(false)
+    }
+
+    fn feed_network(&mut self) -> Result<()> {
+        let mut recv_count = 0u32;
+        loop {
+            match self.socket.recv_from(&mut self.buf) {
+                Ok((n, source)) => {
+                    recv_count += 1;
+                    if recv_count <= 5 {
+                        tracing::info!("UDP recv {} bytes from {}", n, source);
+                    }
+                    let input = Input::Receive(
+                        Instant::now(),
+                        Receive {
+                            proto: Protocol::Udp,
+                            source,
+                            destination: self.udp_addr,
+                            contents: self.buf[..n]
+                                .try_into()
+                                .map_err(|e| anyhow::anyhow!("receive contents: {e}"))?,
+                        },
+                    );
+                    self.rtc
+                        .handle_input(input)
+                        .map_err(|e| anyhow::anyhow!("handle_input({n} bytes from {source}): {e}"))?;
+                }
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
+                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
+                Err(e) => bail!("UDP recv error: {e}"),
+            }
+        }
+
+        self.rtc
+            .handle_input(Input::Timeout(Instant::now()))
+            .map_err(|e| anyhow::anyhow!("handle timeout: {e}"))?;
+
+        Ok(())
+    }
+
+    fn write_h264_frame(&mut self, data: &[u8], frame_number: u64, fps: u32) -> Result<()> {
+        if !self.connected {
+            return Ok(());
+        }
+
+        let mid = match self.video_mid {
+            Some(m) => m,
+            None => {
+                tracing::warn!("write_h264: no video_mid");
+                return Ok(());
+            }
+        };
+        let pt = match self.video_pt {
+            Some(p) => p,
+            None => {
+                tracing::warn!("write_h264: no video_pt");
+                return Ok(());
+            }
+        };
+
+        if self.need_keyframe {
+            if !is_idr_nalu(data) {
+                tracing::debug!(
+                    "write_h264: skipping non-IDR frame ({} bytes), waiting for keyframe",
+                    data.len()
+                );
+                return Ok(());
+            }
+            tracing::info!(
+                "write_h264: got IDR keyframe ({} bytes), starting playback",
+                data.len()
+            );
+            self.need_keyframe = false;
+        }
+
+        let ticks_per_second = 90_000u64;
+        let fps = fps.max(1) as u64;
+        let rtp_timestamp = frame_number.saturating_mul(ticks_per_second) / fps;
+        self.rtp_clock = rtp_timestamp as u32;
+        let rtp_time = MediaTime::new(rtp_timestamp, Frequency::NINETY_KHZ);
+
+        let writer = match self.rtc.writer(mid) {
+            Some(w) => w,
+            None => {
+                tracing::warn!("write_h264: no writer for mid={mid}");
+                return Ok(());
+            }
+        };
+
+        tracing::debug!(
+            "write_h264: {} bytes, pt={:?}, rtp={}",
+            data.len(),
+            pt,
+            self.rtp_clock
+        );
+        writer
+            .write(pt, Instant::now(), rtp_time, data)
+            .map_err(|e| anyhow::anyhow!("writer.write: {e}"))?;
+
+        self.poll_rtc()?;
+
+        Ok(())
+    }
+
+    fn is_connected(&self) -> bool {
+        self.connected
+    }
+}
+
+// ── 工具函数 ──────────────────────────────────────────────────────────────
+
+/// 从 HTTP 请求中提取 body(在 \r\n\r\n 之后)
+fn extract_body(req: &str) -> &str {
+    if let Some(idx) = req.find("\r\n\r\n") {
+        req.get(idx + 4..).unwrap_or("")
+    } else {
+        ""
+    }
+}
+
+fn local_ip() -> Option {
+    std::net::UdpSocket::bind("0.0.0.0:0")
+        .ok()
+        .and_then(|s| {
+            s.connect("1.1.1.1:80").ok()?;
+            let addr = s.local_addr().ok()?;
+            drop(s);
+            let ip = addr.ip().to_string();
+            if ip == "0.0.0.0" || ip.starts_with("127.") {
+                return None;
+            }
+            Some(ip)
+        })
+}
+
+fn is_idr_nalu(data: &[u8]) -> bool {
+    let mut i = 0;
+    while i + 4 < data.len() {
+        if data[i..i + 4] == [0, 0, 0, 1] {
+            let nal_type = data[i + 4] & 0x1F;
+            if nal_type == 5 {
+                return true;
+            }
+            i += 5;
+        } else if i + 3 < data.len() && data[i..i + 3] == [0, 0, 1] {
+            let nal_type = data[i + 3] & 0x1F;
+            if nal_type == 5 {
+                return true;
+            }
+            i += 4;
+        } else {
+            i += 1;
+        }
+    }
+    false
+}