diff --git a/.gitignore b/.gitignore index 3d10387..95df177 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ Thumbs.db # Sisyphus orchestration artifacts -.sisyphus/ diff --git a/Cargo.lock b/Cargo.lock index ab1624d..43969a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -67,12 +77,68 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ashpd" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3118453e020b8e3e0da25ef9a1d0d51d668874358af11aded9d91a8b9c25f323" +dependencies = [ + "enumflags2", + "futures-util", + "getrandom", + "serde", + "serde_repr", + "tokio", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ + "annotate-snippets", "bitflags", "cexpr", "clang-sys", @@ -91,6 +157,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytemuck" version = "1.25.0" @@ -111,6 +183,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.59" @@ -127,7 +205,17 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", ] [[package]] @@ -136,6 +224,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clang-sys" version = "1.8.1" @@ -193,6 +287,45 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -244,6 +377,39 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -251,9 +417,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "ffmpeg-next" version = "8.1.0" @@ -285,12 +478,101 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -303,6 +585,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -318,12 +624,36 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.184" @@ -340,6 +670,34 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libspa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" +dependencies = [ + "bitflags", + "cc", + "convert_case", + "cookie-factory", + "libc", + "libspa-sys", + "nix", + "nom 8.0.0", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -370,6 +728,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -388,6 +755,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -398,13 +777,22 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -429,18 +817,81 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pipewire" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" +dependencies = [ + "anyhow", + "bitflags", + "libc", + "libspa", + "libspa-sys", + "nix", + "once_cell", + "pipewire-sys", + "thiserror", +] + +[[package]] +name = "pipewire-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -468,6 +919,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "regex" version = "1.12.3" @@ -526,7 +983,82 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", ] [[package]] @@ -575,12 +1107,28 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -598,6 +1146,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -607,6 +1207,73 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tracing" version = "0.1.44" @@ -664,18 +1331,58 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -688,12 +1395,115 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.15" @@ -852,22 +1662,232 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "wl-webrtc" version = "0.1.0" dependencies = [ "anyhow", + "ashpd", "clap", + "crossbeam-channel", "drm", "drm-fourcc", "ffmpeg-next", "libc", + "libspa", "mio", + "pipewire", "signal-hook", "signal-hook-mio", + "tokio", "tracing", "tracing-subscriber", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", ] + +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 6a058a3..091eb8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,8 @@ anyhow = "1" drm = "0.12" drm-fourcc = "2" libc = "0.2" +ashpd = { version = "0.13", features = ["tokio", "screencast"] } +tokio = { version = "1", features = ["rt"] } +pipewire = "0.9" +libspa = "0.9" +crossbeam-channel = "0.5" diff --git a/src/args.rs b/src/args.rs index e3d2992..ca06c04 100644 --- a/src/args.rs +++ b/src/args.rs @@ -39,6 +39,10 @@ pub struct Args { #[arg(short, long)] pub verbose: bool, + /// Capture backend to use: 'screencopy' (wlroots) or 'portal' (KWin/KDE). Auto-detected if omitted + #[arg(long)] + pub backend: Option, + /// Port for WebTransport server (Phase 2, unused in MVP) #[arg(long, default_value_t = 0)] pub port: u16, diff --git a/src/avhw.rs b/src/avhw.rs index 31dd9db..6895325 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -7,7 +7,7 @@ use ffmpeg_next as ff; use ffmpeg_next::ffi; use ffmpeg_next::packet::Mut as _; -use crate::transform::Transform; +use crate::transform::{transpose_if_transform_transposed, Transform}; // --------------------------------------------------------------------------- // AvHwDevCtx @@ -463,6 +463,46 @@ impl EncState { } } +// --------------------------------------------------------------------------- +// Shared encoder creation (used by both wlr-screencopy and portal paths) +// --------------------------------------------------------------------------- + +/// Create a fully configured encoder with VAAPI hardware acceleration. +/// +/// Convenience wrapper around [`EncState::new`] that computes default values +/// for `bitrate` and `gop_size` when not provided, and handles encoder dimension +/// transposition for rotated/transformed outputs. +#[allow(clippy::too_many_arguments)] +pub fn create_encoder( + drm_device: &Path, + output_path: &Path, + width: u32, + height: u32, + fps: u32, + transform: Transform, + bitrate: Option, + gop_size: Option, +) -> Result { + let (enc_w, enc_h) = + transpose_if_transform_transposed(transform, width as i32, height as i32); + let actual_bitrate = bitrate.unwrap_or_else(|| { + 2 * (width as u64) * (height as u64) * (fps as u64) / 100 + }); + let actual_gop_size = gop_size.unwrap_or(fps); + EncState::new( + drm_device, + output_path, + width, + height, + enc_w as u32, + enc_h as u32, + actual_bitrate, + actual_gop_size, + fps, + transform, + ) +} + // --------------------------------------------------------------------------- // Filter graph (inline) // --------------------------------------------------------------------------- @@ -535,31 +575,30 @@ fn build_filter_graph( src_ctx.link(0, &mut scale_ctx, 0); match transform { - Transform::Normal90 | Transform::Normal270 => { + Transform::Normal => { + scale_ctx.link(0, &mut sink_ctx, 0); + } + other => { let transpose = ff::filter::find("transpose_vaapi") .ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?; - let dir_val = match transform { + let dir_val = match other { Transform::Normal90 => "1", + Transform::Normal180 => "4", Transform::Normal270 => "2", - _ => unreachable!(), + Transform::Flipped => "5", + Transform::Flipped90 => "3", + Transform::Flipped180 => "6", + Transform::Flipped270 => "0", + Transform::Normal => unreachable!(), }; - let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?; - // SAFETY: transpose_vaapi needs hw_device_ctx for VAAPI device access. + let mut trans_ctx = + graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?; unsafe { (*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone(); } scale_ctx.link(0, &mut trans_ctx, 0); trans_ctx.link(0, &mut sink_ctx, 0); } - Transform::Normal180 => { - tracing::warn!( - "Normal180 transform detected; rotation correction deferred to follow-up" - ); - scale_ctx.link(0, &mut sink_ctx, 0); - } - _ => { - scale_ctx.link(0, &mut sink_ctx, 0); - } } graph diff --git a/src/backend_detect.rs b/src/backend_detect.rs new file mode 100644 index 0000000..9eadb3c --- /dev/null +++ b/src/backend_detect.rs @@ -0,0 +1,134 @@ +use anyhow::Result; +use wayland_client::globals::registry_queue_init; +use wayland_client::globals::GlobalListContents; +use wayland_client::protocol::wl_registry::{Event, WlRegistry}; +use wayland_client::{Connection, Dispatch, QueueHandle}; + +use crate::args::Args; + +/// Capture backend to use for screen capture. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CaptureBackend { + /// wlroots wlr-screencopy protocol (Sway, Hyprland, etc.) + WlrScreencopy, + /// xdg-desktop-portal with PipeWire (KWin/KDE, GNOME, etc.) + PortalPipeWire, +} + +/// Minimal dispatch type for listing Wayland globals during backend detection. +struct RegistryLs; + +impl Dispatch for RegistryLs { + fn event( + _state: &mut Self, + _registry: &WlRegistry, + _event: Event, + _data: &GlobalListContents, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +/// Detect which capture backend to use. +/// +/// Priority: +/// 1. Explicit `--backend` override from CLI args +/// 2. Auto-detect by checking for `zwlr_screencopy_manager_v1` in Wayland globals +/// +/// The detection Wayland connection is dropped before returning so the actual +/// capture backend can create its own connection without holding two simultaneously. +pub fn detect_backend(args: &Args) -> Result { + // 1. Check explicit override + if let Some(ref backend) = args.backend { + return match backend.as_str() { + "portal" => { + tracing::info!("Backend override: Portal/PipeWire"); + Ok(CaptureBackend::PortalPipeWire) + } + "screencopy" => { + tracing::info!("Backend override: wlr-screencopy"); + Ok(CaptureBackend::WlrScreencopy) + } + other => { + anyhow::bail!( + "Unknown backend '{}'. Use 'screencopy' or 'portal'.", + other + ); + } + }; + } + + // 2. Auto-detect: check if zwlr_screencopy_manager_v1 is available + tracing::info!("Auto-detecting capture backend..."); + let conn = Connection::connect_to_env()?; + let (globals, _queue) = registry_queue_init::(&conn)?; + + let has_screencopy = globals + .contents() + .clone_list() + .iter() + .any(|g| g.interface == "zwlr_screencopy_manager_v1"); + + // Drop the Wayland connection explicitly before returning. + // The screencopy path creates its own connection. Holding two connections + // simultaneously is wasteful and may cause issues on some compositors. + drop(conn); + + if has_screencopy { + tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend"); + Ok(CaptureBackend::WlrScreencopy) + } else { + tracing::info!("No wlr-screencopy support → using Portal/PipeWire backend"); + Ok(CaptureBackend::PortalPipeWire) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_args(backend: Option<&str>) -> Args { + Args { + output: "test.mp4".to_string(), + output_name: None, + fps: 30, + codec: "h264".to_string(), + hw_accel: "vaapi".to_string(), + drm_device: None, + bitrate: None, + gop_size: None, + verbose: false, + backend: backend.map(String::from), + port: 0, + } + } + + #[test] + fn explicit_portal_backend() { + let args = make_args(Some("portal")); + let result = detect_backend(&args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire); + } + + #[test] + fn explicit_screencopy_backend() { + let args = make_args(Some("screencopy")); + let result = detect_backend(&args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CaptureBackend::WlrScreencopy); + } + + #[test] + fn invalid_backend_name_returns_error() { + let args = make_args(Some("magic")); + let result = detect_backend(&args); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Unknown backend 'magic'"), + "Expected error about unknown backend, got: {err}" + ); + } +} diff --git a/src/cap_portal.rs b/src/cap_portal.rs new file mode 100644 index 0000000..645347e --- /dev/null +++ b/src/cap_portal.rs @@ -0,0 +1,411 @@ +use std::os::fd::{FromRawFd, OwnedFd}; +use std::thread::{self, JoinHandle}; + +use anyhow::Result; +use crossbeam_channel::{Receiver, Sender, bounded}; +use tokio::runtime::Runtime; + +use crate::args::Args; + +pub struct PwDmaBufFrame { + pub fd: OwnedFd, + pub offset: u64, + pub stride: u32, + pub modifier: u64, + pub width: u32, + pub height: u32, + pub format: u32, + pub pts: i64, +} + +pub enum PwEvent { + Frame(PwDmaBufFrame), + StreamEnded, + Error(String), +} + +pub enum PwCmd { + Shutdown, +} + +pub struct CapPortal { + cmd_tx: Sender, + frame_rx: Receiver, + pw_thread: Option>, + rt: Runtime, +} + +struct PwThreadCtx { + frame_tx: Sender, + cmd_rx: Receiver, + pw_fd: OwnedFd, + node_id: u32, + fps: u32, +} + +impl CapPortal { + pub fn new(args: &Args) -> Result { + let rt = Runtime::new()?; + + let (pw_fd, node_id) = rt.block_on(async { + Self::setup_portal().await + })?; + + let (frame_tx, frame_rx) = bounded(3); + let (cmd_tx, cmd_rx) = bounded(1); + + let ctx = PwThreadCtx { + frame_tx, + cmd_rx, + pw_fd, + node_id, + fps: args.fps, + }; + + let pw_thread = thread::Builder::new() + .name("pipewire-capture".into()) + .spawn(move || { + pipewire_thread(ctx); + })?; + + Ok(Self { + cmd_tx, + frame_rx, + pw_thread: Some(pw_thread), + rt, + }) + } + + pub fn frame_receiver(&self) -> &Receiver { + &self.frame_rx + } + + async fn setup_portal() -> Result<(OwnedFd, u32)> { + use ashpd::desktop::screencast::{ + CursorMode, Screencast, SelectSourcesOptions, SourceType, + }; + use ashpd::desktop::PersistMode; + + let proxy = Screencast::new().await.map_err(|e| { + anyhow::anyhow!("Failed to create Screencast proxy: {e}") + })?; + + let session = proxy + .create_session(Default::default()) + .await + .map_err(|e| anyhow::anyhow!("Failed to create ScreenCast session: {e}"))?; + + 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), + ) + .await + .map_err(|e| { + anyhow::anyhow!("屏幕共享权限被拒绝 / Screen sharing permission denied: {e}") + })?; + + let response = proxy + .start(&session, None, Default::default()) + .await + .map_err(|e| anyhow::anyhow!("ScreenCast start failed: {e}"))? + .response() + .map_err(|e| anyhow::anyhow!("ScreenCast response error: {e}"))?; + + let stream = response + .streams() + .first() + .ok_or_else(|| anyhow::anyhow!("No streams returned from ScreenCast"))?; + + let node_id = stream.pipe_wire_node_id(); + + let fd = proxy + .open_pipe_wire_remote(&session, Default::default()) + .await + .map_err(|e| anyhow::anyhow!("Failed to open PipeWire remote: {e}"))?; + + tracing::info!("Portal session established: node_id={node_id}"); + + Ok((fd, node_id)) + } +} + +impl Drop for CapPortal { + fn drop(&mut self) { + let _ = self.cmd_tx.send(PwCmd::Shutdown); + + if let Some(handle) = self.pw_thread.take() { + let _ = handle.join(); + } + } +} + +fn pipewire_thread(ctx: PwThreadCtx) { + use pipewire as pw; + use pw::properties::properties; + use pw::stream::{StreamBox, StreamFlags}; + use std::cell::Cell; + use std::rc::Rc; + use pw::spa::param::video::VideoInfoRaw; + + pw::init(); + + let PwThreadCtx { + frame_tx, + cmd_rx, + pw_fd, + node_id, + fps: _fps, + } = ctx; + + let mainloop = match pw::main_loop::MainLoopBox::new(None) { + Ok(ml) => ml, + Err(e) => { + let _ = frame_tx.send(PwEvent::Error(format!("MainLoop::new failed: {e}"))); + return; + } + }; + + let context = match pw::context::ContextBox::new(mainloop.loop_(), None) { + Ok(c) => c, + Err(e) => { + let _ = frame_tx.send(PwEvent::Error(format!("Context::new failed: {e}"))); + return; + } + }; + + let core = match context.connect_fd(pw_fd, None) { + Ok(c) => c, + Err(e) => { + let _ = frame_tx.send(PwEvent::Error(format!("connect_fd failed: {e}"))); + return; + } + }; + + let stream = match StreamBox::new( + &core, + "wl-webrtc", + properties! { + *pw::keys::MEDIA_TYPE => "Video", + *pw::keys::MEDIA_CATEGORY => "Capture", + *pw::keys::MEDIA_ROLE => "Screen", + }, + ) { + Ok(s) => s, + Err(e) => { + let _ = frame_tx.send(PwEvent::Error(format!("Stream::new failed: {e}"))); + return; + } + }; + + // Shared format state: (width, height, drm_fourcc, modifier) + let format_info: Rc>> = + Rc::new(Cell::new(None)); + + let frame_tx_clone = frame_tx.clone(); + let _listener = stream + .add_local_listener::<()>() + .state_changed(move |_, _, old, new| { + tracing::debug!("PipeWire stream state: {old:?} -> {new:?}"); + match new { + pw::stream::StreamState::Error(_) + | pw::stream::StreamState::Unconnected => { + let _ = frame_tx_clone.send(PwEvent::StreamEnded); + } + _ => {} + } + }) + .param_changed({ + let format_info = format_info.clone(); + move |_, _, id, param| { + let Some(param) = param else { return }; + if id != pw::spa::param::ParamType::Format.as_raw() { + return; + } + let mut info = VideoInfoRaw::new(); + if let Err(e) = info.parse(param) { + tracing::warn!("Failed to parse video format: {e}"); + return; + } + let width = info.size().width; + let height = info.size().height; + let drm_format = spa_to_drm_fourcc(info.format()); + let modifier = info.modifier(); + format_info.set(Some((width, height, drm_format, modifier))); + tracing::info!( + "PipeWire format negotiated: {width}x{height}, \ + drm_format={drm_format:#010x}, modifier={modifier:#x}" + ); + } + }) + .process({ + let format_info = format_info.clone(); + let frame_tx = frame_tx.clone(); + move |stream, _| { + let raw_buf = unsafe { stream.dequeue_raw_buffer() }; + if raw_buf.is_null() { + return; + } + + let spa_buf = unsafe { (*raw_buf).buffer }; + if spa_buf.is_null() { + unsafe { stream.queue_raw_buffer(raw_buf) }; + return; + } + + let n_datas = unsafe { (*spa_buf).n_datas }; + let datas_ptr = unsafe { (*spa_buf).datas }; + if n_datas == 0 || datas_ptr.is_null() { + unsafe { stream.queue_raw_buffer(raw_buf) }; + return; + } + + // Access first data item through libspa Data wrapper + let data_ref: &pw::spa::buffer::Data = unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) }; + let fd = data_ref.fd(); + if fd < 0 { + unsafe { stream.queue_raw_buffer(raw_buf) }; + return; + } + + let chunk = data_ref.chunk(); + let offset = chunk.offset() as u64; + let stride = chunk.stride() as u32; + + // Get PTS from SPA_META_Header metadata + let pts: i64 = unsafe { + let mut pts_val: i64 = 0; + let n_metas = (*spa_buf).n_metas; + let metas = (*spa_buf).metas; + if !metas.is_null() { + for i in 0..n_metas { + let meta = &*metas.add(i as usize); + if meta.type_ == libspa::sys::SPA_META_Header + && meta.size as usize >= std::mem::size_of::() + && !meta.data.is_null() + { + let header = &*(meta.data as *const libspa::sys::spa_meta_header); + pts_val = header.pts; + break; + } + } + } + pts_val + }; + + let dup_fd = unsafe { libc::dup(fd) }; + if dup_fd < 0 { + unsafe { stream.queue_raw_buffer(raw_buf) }; + return; + } + + let (width, height, format, modifier) = + format_info.get().unwrap_or((0, 0, 0, 0)); + + let frame = PwDmaBufFrame { + fd: unsafe { OwnedFd::from_raw_fd(dup_fd) }, + offset, + stride, + modifier, + width, + height, + format, + pts, + }; + + let _ = frame_tx.send(PwEvent::Frame(frame)); + unsafe { stream.queue_raw_buffer(raw_buf) }; + } + }) + .register(); + + let mut params: [&pw::spa::pod::Pod; 0] = []; + + if let Err(e) = stream.connect( + pw::spa::utils::Direction::Input, + Some(node_id), + StreamFlags::AUTOCONNECT | StreamFlags::MAP_BUFFERS, + &mut params, + ) { + let _ = frame_tx.send(PwEvent::Error(format!("stream.connect failed: {e}"))); + return; + } + + let loop_ = mainloop.loop_(); + loop_.add_signal_local( + pw::loop_::Signal::SIGINT, + Box::new(|| {}), + ); + loop_.add_signal_local( + pw::loop_::Signal::SIGTERM, + Box::new(|| {}), + ); + + // Store raw pointer as usize so it is Send-safe across threads. + // PipeWire's pw_main_loop_quit is thread-safe by design. + let mainloop_ptr = mainloop.as_raw_ptr() as usize; + let cmd_rx_moved = cmd_rx; + std::thread::spawn(move || { + let _ = cmd_rx_moved.recv(); + // SAFETY: mainloop is still alive on the pipewire thread while we wait + // for cmd_rx, and quit() is thread-safe in PipeWire C API. + unsafe { pipewire::sys::pw_main_loop_quit(mainloop_ptr as *mut _) }; + }); + + mainloop.run(); + + // SAFETY: pipewire has been initialized with pw::init() above and all + // PipeWire resources (mainloop, stream) have been dropped. + unsafe { pw::deinit() }; +} + +const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 { + (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) +} + +fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 { + use libspa::param::video::VideoFormat; + match format { + VideoFormat::BGRA => fourcc(b'B', b'G', b'R', b'A'), + VideoFormat::BGRx => fourcc(b'B', b'G', b'R', b'X'), + VideoFormat::RGBA => fourcc(b'R', b'G', b'B', b'A'), + VideoFormat::RGBx => fourcc(b'R', b'G', b'B', b'X'), + VideoFormat::ARGB => fourcc(b'A', b'R', b'2', b'4'), + VideoFormat::xRGB => fourcc(b'X', b'R', b'2', b'4'), + VideoFormat::ABGR => fourcc(b'A', b'B', b'2', b'4'), + VideoFormat::xBGR => fourcc(b'X', b'B', b'2', b'4'), + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn spa_to_drm_fourcc_bgra() { + use libspa::param::video::VideoFormat; + assert_eq!(spa_to_drm_fourcc(VideoFormat::BGRA), fourcc(b'B', b'G', b'R', b'A')); + } + + #[test] + fn spa_to_drm_fourcc_rgba() { + use libspa::param::video::VideoFormat; + assert_eq!(spa_to_drm_fourcc(VideoFormat::RGBA), fourcc(b'R', b'G', b'B', b'A')); + } + + #[test] + fn spa_to_drm_fourcc_unknown_returns_zero() { + use libspa::param::video::VideoFormat; + assert_eq!(spa_to_drm_fourcc(VideoFormat::Unknown), 0); + } + + #[test] + fn fourcc_encoding() { + assert_eq!(fourcc(b'B', b'G', b'R', b'A'), 0x41524742); + } +} diff --git a/src/main.rs b/src/main.rs index c0e84eb..9b3aba0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,12 @@ use wayland_client::Connection; mod args; mod avhw; +mod backend_detect; +mod cap_portal; mod cap_wlr_screencopy; mod fps_limit; mod state; +mod state_portal; mod transform; use crate::args::Args; @@ -40,6 +43,19 @@ fn main() -> Result<()> { anyhow::bail!("HEVC not supported in MVP. Use --codec h264"); } + let backend = crate::backend_detect::detect_backend(&args)?; + + match backend { + crate::backend_detect::CaptureBackend::WlrScreencopy => { + run_wlr_screencopy(args) + } + crate::backend_detect::CaptureBackend::PortalPipeWire => { + run_portal_pipewire(args) + } + } +} + +fn run_wlr_screencopy(args: Args) -> Result<()> { // Connect to Wayland compositor let conn = Connection::connect_to_env()?; let (gm, mut queue) = registry_queue_init::>(&conn)?; @@ -158,3 +174,58 @@ fn main() -> Result<()> { tracing::info!("Done"); Ok(()) } + +fn run_portal_pipewire(args: Args) -> Result<()> { + use crate::state_portal::StatePortal; + + tracing::info!("Using Portal/PipeWire backend (KWin/KDE/GNOME)"); + + let mut state = StatePortal::new(args)?; + + // Set up signal handling only (no Wayland fd needed) + let mut signals = signal_hook_mio::v1_0::Signals::new(&[ + signal_hook::consts::SIGINT, + signal_hook::consts::SIGTERM, + ])?; + + let mut poll = mio::Poll::new()?; + let mut events = mio::Events::with_capacity(8); + + poll.registry().register( + &mut signals, + mio::Token(1), + mio::Interest::READABLE, + )?; + + let mut running = true; + while running { + poll.poll(&mut events, Some(std::time::Duration::from_millis(10))) + .unwrap_or_else(|e| { + if e.kind() == std::io::ErrorKind::Interrupted { + return; + } + tracing::error!("poll failed: {e}"); + running = false; + }); + + for event in &events { + if event.token() == mio::Token(1) { + tracing::info!("Received quit signal"); + running = false; + } + } + + // Process all available PipeWire frames + while state.poll_and_encode()? {} + + if state.is_errored() { + tracing::error!("Fatal error in portal state machine, exiting"); + running = false; + } + } + + tracing::info!("Shutting down..."); + state.shutdown(); + tracing::info!("Done"); + Ok(()) +} diff --git a/src/state.rs b/src/state.rs index 58e388c..9849cbc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -465,6 +465,8 @@ impl State { let fd_dup = unsafe { libc::dup(obj.fd) }; if fd_dup < 0 { tracing::error!("failed to dup dma-buf fd"); + // wayland-client does not auto-destroy params on Drop. + params.destroy(); self.errored = true; return; } @@ -553,8 +555,27 @@ impl State { } } - pub fn on_copy_fail(&mut self) { + pub fn on_copy_fail(&mut self) + where + S::Frame: Default, + { tracing::error!("compositor copy failed"); + let taken = mem::replace(&mut self.in_flight_surface, InFlightSurface::None); + match taken { + InFlightSurface::CopyQueued { + buffer, + frame, + .. + } => { + drop(buffer); + if let EncConstructionStage::Streaming { cap, .. } = &mut self.stage { + cap.on_done_with_frame(frame); + } + } + other => { + self.in_flight_surface = other; + } + } self.errored = true; } @@ -576,25 +597,19 @@ impl State { }; let (output_info, output, cap, screencopy_manager, dmabuf) = stage_data; let drm_path = self.resolve_drm_path(); - let bitrate = self.args.bitrate.unwrap_or_else(|| { - let fps = self.args.fps as u64; - 2 * (width as u64) * (height as u64) * fps / 100 - }); - let gop_size = self.args.gop_size.unwrap_or(self.args.fps); let fps = self.args.fps; - let (enc_w, enc_h) = - transpose_if_transform_transposed(output_info.transform, width as i32, height as i32); - let enc = match EncState::new( + let bitrate = self.args.bitrate.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), width, height, - enc_w as u32, - enc_h as u32, - bitrate, - gop_size, fps, output_info.transform, + self.args.bitrate, + self.args.gop_size, ) { Ok(enc) => enc, Err(e) => { @@ -1175,21 +1190,24 @@ impl Dispatch for State { } BufferParamsEvent::Failed => { tracing::error!("DMA-BUF buffer creation failed"); - state.errored = true; - match mem::replace(&mut state.in_flight_surface, InFlightSurface::None) { + let taken = mem::replace(&mut state.in_flight_surface, InFlightSurface::None); + match taken { InFlightSurface::CopyQueued { - surface: _, - drm_map: _, - frame: _, buffer, + frame, + .. } => { drop(buffer); + if let EncConstructionStage::Streaming { cap, .. } = &mut state.stage { + cap.on_done_with_frame(frame); + } } other => { state.in_flight_surface = other; } } proxy.destroy(); + state.errored = true; } _ => {} } diff --git a/src/state_portal.rs b/src/state_portal.rs new file mode 100644 index 0000000..f11fc4e --- /dev/null +++ b/src/state_portal.rs @@ -0,0 +1,306 @@ +use std::mem; +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use ffmpeg_next as ff; +use ffmpeg_next::ffi; + +use crate::args::Args; +use crate::avhw::{self, EncState}; +use crate::cap_portal::{CapPortal, PwDmaBufFrame, PwEvent}; +use crate::fps_limit::FpsLimit; +use crate::transform::Transform; + +enum PortalStage { + WaitingForFormat, + Streaming, +} + +pub struct StatePortal { + stage: PortalStage, + enc: Option, + fps_limit: FpsLimit<()>, + cap: CapPortal, + args: Args, + errored: bool, + first_frame: bool, + drm_device: PathBuf, +} + +impl StatePortal { + pub fn new(args: Args) -> Result { + let drm_device = resolve_drm_device(&args)?; + tracing::info!("Using DRM device: {}", drm_device.display()); + + let cap = CapPortal::new(&args)?; + + Ok(Self { + stage: PortalStage::WaitingForFormat, + enc: None, + fps_limit: FpsLimit::new(args.fps), + cap, + args, + errored: false, + first_frame: true, + drm_device, + }) + } + + pub fn poll_and_encode(&mut self) -> Result { + let event = match self.cap.frame_receiver().try_recv() { + Ok(event) => event, + Err(_) => return Ok(false), + }; + + match event { + PwEvent::Frame(frame) => { + match self.stage { + PortalStage::WaitingForFormat => { + tracing::info!( + "First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}", + frame.width, + frame.height, + frame.format, + frame.stride, + frame.modifier + ); + + let enc = avhw::create_encoder( + &self.drm_device, + self.args.output.as_ref(), + frame.width, + frame.height, + self.args.fps, + Transform::Normal, + self.args.bitrate, + self.args.gop_size, + )?; + + self.enc = Some(enc); + self.stage = PortalStage::Streaming; + drop(frame); + } + PortalStage::Streaming => { + self.handle_pw_frame(frame)?; + } + } + } + PwEvent::StreamEnded => { + tracing::warn!("PipeWire stream ended"); + self.errored = true; + } + PwEvent::Error(e) => { + tracing::error!("PipeWire error: {e}"); + self.errored = true; + } + } + + Ok(true) + } + + fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> { + // 1. FPS limiting (first frame bypasses) + if self.first_frame { + self.first_frame = false; + } else { + let now = std::time::Instant::now(); + if self.fps_limit.on_new_frame((), now).is_none() { + return Ok(()); + } + } + + // 2. Build DRM descriptor for DMA-BUF import + let desc = build_drm_descriptor(&frame); + let desc_box = Box::new(desc); + + // 3. Allocate raw DRM_PRIME source frame using Video wrapper + let mut raw_frame = ff::frame::Video::empty(); + unsafe { + let raw_ptr = raw_frame.as_mut_ptr(); + (*raw_ptr).data[0] = Box::into_raw(desc_box) as *mut u8; + (*raw_ptr).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; + (*raw_ptr).width = frame.width as i32; + (*raw_ptr).height = frame.height as i32; + } + + // 4. Get encoder reference + let enc = match self.enc.as_mut() { + Some(e) => e, + None => { + // Recover the Box to prevent memory leak of the descriptor + unsafe { + let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; + if !desc_ptr.is_null() { + let _ = Box::from_raw(desc_ptr); + } + } + bail!("encoder not initialized"); + } + }; + + // 5. Allocate VAAPI hardware target frame + let mut hw_frame = ff::frame::Video::empty(); + let ret = unsafe { + ffi::av_hwframe_get_buffer(enc.frames_rgb().as_ptr(), hw_frame.as_mut_ptr(), 0) + }; + if ret < 0 { + // Recover the Box to prevent memory leak of the descriptor + unsafe { + let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; + if !desc_ptr.is_null() { + let _ = Box::from_raw(desc_ptr); + } + } + bail!("av_hwframe_get_buffer failed: error {ret}"); + } + + // 6. Import DMA-BUF into VAAPI via transfer_data + let ret = unsafe { + ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0) + }; + if ret < 0 { + unsafe { + let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; + if !desc_ptr.is_null() { + let _ = Box::from_raw(desc_ptr); + } + } + if ret == -(ffi::EINVAL as i32) { + bail!( + "VAAPI does not support DMA-BUF modifier 0x{:X}", + frame.modifier + ); + } + bail!("av_hwframe_transfer_data failed: error {ret}"); + } + + // 7. Set PTS + unsafe { + (*hw_frame.as_mut_ptr()).pts = frame.pts; + } + + // 8. Encode + enc.encode_frame(&hw_frame)?; + + // 9. Clean up: recover the Boxed descriptor from raw_frame to prevent leak. + // Video::drop calls av_frame_free which does NOT free data[0]. + unsafe { + let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; + if !desc_ptr.is_null() { + let _ = Box::from_raw(desc_ptr); + } + } + // raw_frame and hw_frame drop here via Video::drop → av_frame_free + + Ok(()) + } + + pub fn flush(&mut self) -> Result<()> { + if let Some(enc) = &mut self.enc { + enc.flush()?; + } + Ok(()) + } + + pub fn shutdown(&mut self) { + if let Err(e) = self.flush() { + tracing::error!("Flush error during shutdown: {e}"); + } + tracing::info!("StatePortal shutdown complete"); + } + + pub fn is_errored(&self) -> bool { + self.errored + } +} + +fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor { + let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() }; + + desc.nb_objects = 1; + desc.objects[0].fd = frame.fd.as_raw_fd(); + desc.objects[0].size = 0; + desc.objects[0].format_modifier = frame.modifier; + + desc.nb_layers = 1; + desc.layers[0].format = frame.format; + desc.layers[0].nb_planes = 1; + desc.layers[0].planes[0].object_index = 0; + desc.layers[0].planes[0].offset = frame.offset as isize; + desc.layers[0].planes[0].pitch = frame.stride as isize; + + desc +} + +use std::os::fd::AsRawFd; + +fn resolve_drm_device(args: &Args) -> Result { + if let Some(ref drm) = args.drm_device { + return Ok(PathBuf::from(drm)); + } + + for render in &["/dev/dri/renderD128", "/dev/dri/renderD129"] { + let path = PathBuf::from(render); + if path.exists() { + return Ok(path); + } + } + + bail!("No DRM render device found. Specify --drm-device.") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::fd::{FromRawFd, OwnedFd}; + + fn make_test_frame() -> PwDmaBufFrame { + // Create a dummy fd from stderr (always valid fd 2) + let fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) }; + PwDmaBufFrame { + fd, + offset: 0, + stride: 1920 * 4, + modifier: 0, // DRM_FORMAT_MOD_LINEAR + width: 1920, + height: 1080, + format: 0x34325258, // XR24 little-endian + pts: 12345, + } + } + + #[test] + fn build_drm_descriptor_single_plane() { + let frame = make_test_frame(); + let desc = build_drm_descriptor(&frame); + + assert_eq!(desc.nb_objects, 1); + assert_eq!(desc.objects[0].format_modifier, 0); + assert_eq!(desc.nb_layers, 1); + assert_eq!(desc.layers[0].format, 0x34325258); + assert_eq!(desc.layers[0].nb_planes, 1); + assert_eq!(desc.layers[0].planes[0].object_index, 0); + assert_eq!(desc.layers[0].planes[0].offset, 0); + assert_eq!(desc.layers[0].planes[0].pitch, 1920 * 4); + } + + #[test] + fn resolve_drm_device_explicit() { + let args = Args { + output: "test.mp4".to_string(), + output_name: None, + fps: 30, + codec: "h264".to_string(), + hw_accel: "vaapi".to_string(), + drm_device: Some("/dev/dri/renderD128".to_string()), + bitrate: None, + gop_size: None, + verbose: false, + backend: None, + port: 0, + }; + let result = resolve_drm_device(&args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), std::path::PathBuf::from("/dev/dri/renderD128")); + } +} diff --git a/src/transform.rs b/src/transform.rs index 69e3e4c..b6bac3b 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -56,12 +56,7 @@ pub fn transform_basis(transform: Transform) -> (i32, i32, i32, i32) { /// new_x = a * x + b * y + offset_x /// new_y = c * x + d * y + offset_y /// ``` -pub fn screen_to_frame( - transform: Transform, - rect: Rect, - frame_w: i32, - frame_h: i32, -) -> Rect { +pub fn screen_to_frame(transform: Transform, rect: Rect, frame_w: i32, frame_h: i32) -> Rect { let (a, b, c, d) = transform_basis(transform); // Compute the offset so that the transformed origin maps correctly. @@ -90,9 +85,10 @@ pub fn screen_to_frame( /// and `(w, h)` unchanged otherwise. pub fn transpose_if_transform_transposed(transform: Transform, w: i32, h: i32) -> (i32, i32) { match transform { - Transform::Normal90 | Transform::Normal270 | Transform::Flipped90 | Transform::Flipped270 => { - (h, w) - } + Transform::Normal90 + | Transform::Normal270 + | Transform::Flipped90 + | Transform::Flipped270 => (h, w), _ => (w, h), } } @@ -161,15 +157,33 @@ mod tests { #[test] fn screen_to_frame_identity_unchanged() { - let rect = Rect { x: 10, y: 20, w: 100, h: 50 }; + let rect = Rect { + x: 10, + y: 20, + w: 100, + h: 50, + }; let result = screen_to_frame(Transform::Normal, rect, 1920, 1080); - assert_eq!(result, Rect { x: 10, y: 20, w: 100, h: 50 }); + assert_eq!( + result, + Rect { + x: 10, + y: 20, + w: 100, + h: 50 + } + ); } #[test] fn screen_to_frame_90_rotates_origin() { // 90° CW: top-left (0,0) in screen should map to bottom-left in frame - let rect = Rect { x: 0, y: 0, w: 100, h: 50 }; + let rect = Rect { + x: 0, + y: 0, + w: 100, + h: 50, + }; let result = screen_to_frame(Transform::Normal90, rect, 1080, 1920); // a=0,b=1,c=-1,d=0 => offset_x=0, offset_y=1920 (c+d=-1<0) // new_x = 0*0 + 1*0 + 0 = 0 @@ -183,7 +197,12 @@ mod tests { #[test] fn screen_to_frame_180_rotates() { - let rect = Rect { x: 100, y: 200, w: 300, h: 400 }; + let rect = Rect { + x: 100, + y: 200, + w: 300, + h: 400, + }; let result = screen_to_frame(Transform::Normal180, rect, 1920, 1080); // a=-1,b=0,c=0,d=-1, offset_x=1920, offset_y=1080 assert_eq!(result.x, -100 + 1920); @@ -194,7 +213,12 @@ mod tests { #[test] fn screen_to_frame_flipped_horizontal() { - let rect = Rect { x: 50, y: 30, w: 200, h: 100 }; + let rect = Rect { + x: 50, + y: 30, + w: 200, + h: 100, + }; let result = screen_to_frame(Transform::Flipped, rect, 1920, 1080); // a=-1,b=0,c=0,d=1, offset_x=1920, offset_y=0 assert_eq!(result.x, -50 + 1920); @@ -207,85 +231,179 @@ mod tests { #[test] fn transpose_normal_no_swap() { - assert_eq!(transpose_if_transform_transposed(Transform::Normal, 1920, 1080), (1920, 1080)); + assert_eq!( + transpose_if_transform_transposed(Transform::Normal, 1920, 1080), + (1920, 1080) + ); } #[test] fn transpose_90_swaps() { - assert_eq!(transpose_if_transform_transposed(Transform::Normal90, 1920, 1080), (1080, 1920)); + assert_eq!( + transpose_if_transform_transposed(Transform::Normal90, 1920, 1080), + (1080, 1920) + ); } #[test] fn transpose_180_no_swap() { - assert_eq!(transpose_if_transform_transposed(Transform::Normal180, 1920, 1080), (1920, 1080)); + assert_eq!( + transpose_if_transform_transposed(Transform::Normal180, 1920, 1080), + (1920, 1080) + ); } #[test] fn transpose_270_swaps() { - assert_eq!(transpose_if_transform_transposed(Transform::Normal270, 1920, 1080), (1080, 1920)); + assert_eq!( + transpose_if_transform_transposed(Transform::Normal270, 1920, 1080), + (1080, 1920) + ); } #[test] fn transpose_flipped_no_swap() { - assert_eq!(transpose_if_transform_transposed(Transform::Flipped, 1920, 1080), (1920, 1080)); + assert_eq!( + transpose_if_transform_transposed(Transform::Flipped, 1920, 1080), + (1920, 1080) + ); } #[test] fn transpose_flipped90_swaps() { - assert_eq!(transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080), (1080, 1920)); + assert_eq!( + transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080), + (1080, 1920) + ); } #[test] fn transpose_flipped180_no_swap() { - assert_eq!(transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080), (1920, 1080)); + assert_eq!( + transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080), + (1920, 1080) + ); } #[test] fn transpose_flipped270_swaps() { - assert_eq!(transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080), (1080, 1920)); + assert_eq!( + transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080), + (1080, 1920) + ); } // ── fit_inside_bounds ───────────────────────────────────────── #[test] fn fit_inside_already_fits() { - let rect = Rect { x: 10, y: 20, w: 100, h: 50 }; + let rect = Rect { + x: 10, + y: 20, + w: 100, + h: 50, + }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!(result, rect); } #[test] fn fit_inside_clips_right_and_bottom() { - let rect = Rect { x: 1800, y: 1000, w: 200, h: 200 }; + let rect = Rect { + x: 1800, + y: 1000, + w: 200, + h: 200, + }; let result = fit_inside_bounds(rect, 1920, 1080); - assert_eq!(result, Rect { x: 1800, y: 1000, w: 120, h: 80 }); + assert_eq!( + result, + Rect { + x: 1800, + y: 1000, + w: 120, + h: 80 + } + ); } #[test] fn fit_inside_clips_negative_origin() { - let rect = Rect { x: -50, y: -30, w: 200, h: 200 }; + let rect = Rect { + x: -50, + y: -30, + w: 200, + h: 200, + }; let result = fit_inside_bounds(rect, 1920, 1080); - assert_eq!(result, Rect { x: 0, y: 0, w: 150, h: 170 }); + assert_eq!( + result, + Rect { + x: 0, + y: 0, + w: 150, + h: 170 + } + ); } #[test] fn fit_inside_completely_out_of_bounds() { - let rect = Rect { x: 2000, y: 2000, w: 100, h: 100 }; + let rect = Rect { + x: 2000, + y: 2000, + w: 100, + h: 100, + }; let result = fit_inside_bounds(rect, 1920, 1080); - assert_eq!(result, Rect { x: 1920, y: 1080, w: 0, h: 0 }); + assert_eq!( + result, + Rect { + x: 1920, + y: 1080, + w: 0, + h: 0 + } + ); } #[test] fn fit_inside_zero_size_rect() { - let rect = Rect { x: 100, y: 100, w: 0, h: 0 }; + let rect = Rect { + x: 100, + y: 100, + w: 0, + h: 0, + }; let result = fit_inside_bounds(rect, 1920, 1080); - assert_eq!(result, Rect { x: 100, y: 100, w: 0, h: 0 }); + assert_eq!( + result, + Rect { + x: 100, + y: 100, + w: 0, + h: 0 + } + ); } #[test] fn fit_inside_zero_bounds() { - let rect = Rect { x: 0, y: 0, w: 100, h: 100 }; + let rect = Rect { + x: 0, + y: 0, + w: 100, + h: 100, + }; let result = fit_inside_bounds(rect, 0, 0); - assert_eq!(result, Rect { x: 0, y: 0, w: 0, h: 0 }); + assert_eq!( + result, + Rect { + x: 0, + y: 0, + w: 0, + h: 0 + } + ); } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 7183fce..f096f5c 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -44,10 +44,7 @@ fn test_rejects_invalid_args() { .output() .expect("failed to execute wl-webrtc with invalid args"); - assert!( - !output.status.success(), - "should reject unrecognized flag" - ); + assert!(!output.status.success(), "should reject unrecognized flag"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.to_lowercase().contains("error") @@ -68,10 +65,7 @@ fn test_rejects_hevc_codec() { .expect("failed to execute wl-webrtc --codec hevc"); // MVP only supports h264; hevc should be rejected. - assert!( - !output.status.success(), - "should reject hevc codec in MVP" - ); + assert!(!output.status.success(), "should reject hevc codec in MVP"); } /// Tests requiring a live Wayland compositor and VAAPI hardware.