diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e3e8c0b..2d065a4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - lint .flatpak: - image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-45' + image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-46' variables: FLATPAK_BUILD_DIR: _build MANIFEST_PATH: build-aux/org.pipewire.Helvum.json diff --git a/Cargo.lock b/Cargo.lock index 4876527..eabb933 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width", + "yansi-term", +] + [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -25,16 +48,17 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bindgen" -version = "0.66.1" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.0", + "annotate-snippets", + "bitflags", "cexpr", "clang-sys", + "itertools", "lazy_static", "lazycell", - "peeking_take_while", "proc-macro2", "quote", "regex", @@ -43,12 +67,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.4.0" @@ -57,23 +75,22 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "cairo-rs" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0466dfa8c0ee78deef390c274ad756801e0a6dbb86c5ef0924a298c5761c4d" +checksum = "2650f66005301bd33cc486dec076e1293c4cecf768bc7ba9bf5d2b1be339b99c" dependencies = [ - "bitflags 2.4.0", + "bitflags", "cairo-sys-rs", "glib", "libc", - "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +checksum = "fd3bb3119664efbd78b5e6c93957447944f16bdbced84c17a9f41c7829b81e64" dependencies = [ "glib-sys", "libc", @@ -125,6 +142,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -140,19 +166,52 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "field-offset" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset 0.9.0", + "memoffset", "rustc_version", ] @@ -221,22 +280,21 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.18.0" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc9c2ed73a81d556b65d08879ba4ee58808a6b1927ce915262185d6d547c6f3" +checksum = "f6a23f8a0b5090494fd04924662d463f8386cc678dd3915015a838c1a3679b92" dependencies = [ "gdk-pixbuf-sys", "gio", "glib", "libc", - "once_cell", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "3dcbd04c1b2c4834cc008b4828bc917d062483b88d26effde6342e5622028f96" dependencies = [ "gio-sys", "glib-sys", @@ -247,9 +305,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6" +checksum = "9100b25604183f2fd97f55ef087fae96ab4934d7215118a35303e422688e6e4b" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -262,9 +320,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.7.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0" +checksum = "d0b76874c40bb8d1c7d03a7231e23ac75fa577a456cd53af32ec17ec8f121626" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -279,9 +337,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.18.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57052f84e8e5999b258e8adf8f5f2af0ac69033864936b8b6838321db2f759b1" +checksum = "c64947d08d7fbb03bf8ad1f25a8ac6cf4329bc772c9b7e5abe7bf9493c81194f" dependencies = [ "futures-channel", "futures-core", @@ -290,7 +348,6 @@ dependencies = [ "gio-sys", "glib", "libc", - "once_cell", "pin-project-lite", "smallvec", "thiserror", @@ -298,24 +355,24 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "winapi", + "windows-sys", ] [[package]] name = "glib" -version = "0.18.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" +checksum = "01e191cc1af1f35b9699213107068cd3fe05d9816275ac118dc785a0dd8faebf" dependencies = [ - "bitflags 2.4.0", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -328,20 +385,18 @@ dependencies = [ "libc", "log", "memchr", - "once_cell", "smallvec", "thiserror", ] [[package]] name = "glib-macros" -version = "0.18.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" +checksum = "9972bb91643d589c889654693a4f1d07697fdcb5d104b5c44fb68649ba1bf68d" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro-crate", - "proc-macro-error", "proc-macro2", "quote", "syn 2.0.37", @@ -349,9 +404,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4" dependencies = [ "libc", "system-deps", @@ -365,9 +420,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979" dependencies = [ "glib-sys", "libc", @@ -376,9 +431,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" +checksum = "99e4d388e96c5f29e2b2f67045d229ddf826d0a8d6d282f94ed3b34452222c91" dependencies = [ "glib", "graphene-sys", @@ -387,9 +442,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" +checksum = "236ed66cc9b18d8adf233716f75de803d0bf6fc806f60d14d948974a12e240d0" dependencies = [ "glib-sys", "libc", @@ -399,9 +454,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e" +checksum = "c65036fc8f99579e8cb37b12487969b707ab23ec8ab953682ff347cbd15d396e" dependencies = [ "cairo-rs", "gdk4", @@ -414,9 +469,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55" +checksum = "bd24c814379f9c3199dc53e52253ee8d0f657eae389ab282c330505289d24738" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -430,9 +485,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842" +checksum = "aa82753b8c26277e4af1446c70e35b19aad4fb794a7b143859e7eeb9a4025d83" dependencies = [ "cairo-rs", "field-offset", @@ -451,9 +506,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.7.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f" +checksum = "40300bf071d2fcd4c94eacc09e84ec6fe73129d2ceb635cf7e55b026b5443567" dependencies = [ "anyhow", "proc-macro-crate", @@ -465,9 +520,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8" +checksum = "0db1b104138f087ccdc81d2c332de5dd049b89de3d384437cc1093b17cd2da18" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -494,14 +549,22 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "helvum" version = "0.5.1" dependencies = [ + "async-channel", "glib", "libadwaita", "libc", "log", + "natord", "once_cell", "pipewire", ] @@ -516,6 +579,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -530,9 +602,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libadwaita" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c" +checksum = "91b4990248b9e1ec5e72094a2ccaea70ec3809f88f6fd52192f2af306b87c5d9" dependencies = [ "gdk-pixbuf", "gdk4", @@ -546,9 +618,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a" +checksum = "23a748e4e92be1265cd9e93d569c0b5dfc7814107985aa6743d670ab281ea1a8" dependencies = [ "gdk4-sys", "gio-sys", @@ -578,11 +650,11 @@ dependencies = [ [[package]] name = "libspa" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0434617020ddca18b86067912970c55410ca654cdafd775480322f50b857a8c4" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" dependencies = [ - "bitflags 2.4.0", + "bitflags", "cc", "convert_case", "cookie-factory", @@ -595,9 +667,9 @@ dependencies = [ [[package]] name = "libspa-sys" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e70ca3f3e70f858ef363046d06178c427b4e0b63d210c95fd87d752679d345" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" dependencies = [ "bindgen", "cc", @@ -612,18 +684,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -641,16 +704,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "nix" -version = "0.26.4" +name = "natord" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cfg-if", "libc", - "memoffset 0.7.1", - "pin-utils", ] [[package]] @@ -665,28 +732,27 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pango" -version = "0.18.0" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a9e54b831d033206160096b825f2070cf5fda7e35167b1c01e9e774f9202d1" +checksum = "b1264d13deb823cc652f26cfe59afb1ec4b9db2a5bd27c41b738c879cc1bfaa1" dependencies = [ "gio", "glib", "libc", - "once_cell", "pango-sys", ] [[package]] name = "pango-sys" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +checksum = "f52ef6a881c19fbfe3b1484df5cad411acaaba29dbec843941c3110d19f340ea" dependencies = [ "glib-sys", "gobject-sys", @@ -695,10 +761,10 @@ dependencies = [ ] [[package]] -name = "peeking_take_while" -version = "0.1.2" +name = "parking" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "pin-project-lite" @@ -714,12 +780,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipewire" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d009c8dd65e890b515a71950f7e4c801523b8894ff33863a40830bf762e9e9" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" dependencies = [ "anyhow", - "bitflags 2.4.0", + "bitflags", "libc", "libspa", "libspa-sys", @@ -731,9 +797,9 @@ dependencies = [ [[package]] name = "pipewire-sys" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "890c084e7b737246cb4799c86b71a0e4da536031ff7473dd639eba9f95039f64" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" dependencies = [ "bindgen", "libspa-sys", @@ -742,18 +808,17 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "once_cell", - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -894,9 +959,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" @@ -927,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ "cfg-expr", - "heck", + "heck 0.4.1", "pkg-config", "toml", "version-compare", @@ -968,14 +1033,14 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -993,6 +1058,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1005,6 +1081,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "version-compare" version = "0.1.1" @@ -1039,6 +1121,72 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winnow" version = "0.5.15" @@ -1047,3 +1195,12 @@ checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] + +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml index e72f4fd..a8d2599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,14 @@ categories = ["gui", "multimedia"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -pipewire = "0.7.1" -adw = { version = "0.5", package = "libadwaita", features = ["v1_4"] } -glib = { version = "0.18", features = ["log"] } +pipewire = "0.8.0" +adw = { version = "0.6", package = "libadwaita", features = ["v1_4"] } +glib = { version = "0.19", features = ["log"] } +async-channel = "2.2" log = "0.4.11" -once_cell = "1.7.2" +once_cell = "1.19" libc = "0.2" +natord = "1.0.9" diff --git a/README.md b/README.md index 7ed5ec0..bbc39fe 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ $ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.fl Then install the required flatpak platform and SDK, if you dont have them already: ```shell -$ flatpak install org.gnome.{Platform,Sdk}//45 org.freedesktop.Sdk.Extension.rust-stable//23.08 org.freedesktop.Sdk.Extension.llvm16//23.08 +$ flatpak install org.gnome.{Platform,Sdk}//46 org.freedesktop.Sdk.Extension.rust-stable//23.08 org.freedesktop.Sdk.Extension.llvm16//23.08 ``` To compile and install as a flatpak, clone the project, change to the project directory, and run: diff --git a/build-aux/org.pipewire.Helvum.json b/build-aux/org.pipewire.Helvum.json index 8716d17..2ac2a07 100644 --- a/build-aux/org.pipewire.Helvum.json +++ b/build-aux/org.pipewire.Helvum.json @@ -1,7 +1,7 @@ { "id": "org.pipewire.Helvum", "runtime": "org.gnome.Platform", - "runtime-version": "45", + "runtime-version": "46", "sdk": "org.gnome.Sdk", "sdk-extensions": [ "org.freedesktop.Sdk.Extension.rust-stable", diff --git a/src/application.rs b/src/application.rs index 0e4dbc0..9daf385 100644 --- a/src/application.rs +++ b/src/application.rs @@ -16,11 +16,12 @@ use adw::{ gio, - glib::{self, clone, Receiver}, + glib::{self, clone}, gtk, prelude::*, subclass::prelude::*, }; +use log::error; use pipewire::channel::Sender; use crate::{graph_manager::GraphManager, ui, GtkMessage, PipewireMessage}; @@ -30,11 +31,14 @@ static APP_ID: &str = "org.pipewire.Helvum"; static VERSION: &str = env!("CARGO_PKG_VERSION"); static AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); +const DEFAULT_REMOTE_NAME: &str = "Default Remote"; + mod imp { use super::*; + use std::cell::OnceCell; + use adw::subclass::prelude::AdwApplicationImpl; - use once_cell::unsync::OnceCell; #[derive(Default)] pub struct Application { @@ -130,6 +134,31 @@ mod imp { about_window.present(); } + + pub(super) fn setup_options(&self, pw_sender: Sender) { + let obj = &*self.obj(); + + obj.add_main_option( + "socket", + glib::char::Char::from(b's'), + glib::OptionFlags::NONE, + glib::OptionArg::String, + "PipeWire socket to connect", + Some("PATH"), + ); + + let current_remote_label = obj.imp().window.current_remote_label(); + obj.connect_handle_local_options(clone!(@strong pw_sender => move |_, opts| { + match opts.lookup::("socket") { + Ok(p) => { + current_remote_label.set_label(p.as_deref().unwrap_or(DEFAULT_REMOTE_NAME)); + pw_sender.send(GtkMessage::Connect(p)).unwrap(); + }, + Err(e) => error!("Invalid socket path: {e}"), + } + -1 + })); + } } } @@ -143,7 +172,7 @@ impl Application { /// Create the view. /// This will set up the entire user interface and prepare it for being run. pub(super) fn new( - gtk_receiver: Receiver, + gtk_receiver: async_channel::Receiver, pw_sender: Sender, ) -> Self { let app: Application = glib::Object::builder() @@ -152,6 +181,8 @@ impl Application { let imp = app.imp(); + imp.setup_options(pw_sender.clone()); + imp.graph_manager .set(GraphManager::new( &imp.window.graph(), diff --git a/src/graph_manager.rs b/src/graph_manager.rs index 34b2868..3a569a6 100644 --- a/src/graph_manager.rs +++ b/src/graph_manager.rs @@ -21,11 +21,11 @@ use pipewire::channel::Sender as PwSender; use crate::{ui::graph::GraphView, GtkMessage, PipewireMessage}; mod imp { + use glib::closure_local; + use super::*; - use std::{cell::RefCell, collections::HashMap}; - - use once_cell::unsync::OnceCell; + use std::{cell::OnceCell, cell::RefCell, collections::HashMap}; use crate::{ui::graph, MediaType, NodeType}; @@ -53,36 +53,61 @@ mod imp { impl ObjectImpl for GraphManager {} impl GraphManager { - pub fn attach_receiver(&self, receiver: glib::Receiver) { - receiver.attach(None, glib::clone!( - @weak self as imp => @default-return glib::ControlFlow::Continue, - move |msg| { - match msg { - PipewireMessage::NodeAdded { id, name, node_type } => imp.add_node(id, name.as_str(), node_type), - PipewireMessage::NodeNameChanged { id, name, media_name } => imp.node_name_changed(id, &name, &media_name), - PipewireMessage::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction), - PipewireMessage::PortFormatChanged { id, media_type } => imp.port_media_type_changed(id, media_type), - PipewireMessage::LinkAdded { - id, port_from, port_to, active, media_type - } => imp.add_link(id, port_from, port_to, active, media_type), - PipewireMessage::LinkStateChanged { id, active } => imp.link_state_changed(id, active), - PipewireMessage::LinkFormatChanged { id, media_type } => imp.link_format_changed(id, media_type), - PipewireMessage::NodeRemoved { id } => imp.remove_node(id), - PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id), - PipewireMessage::LinkRemoved { id } => imp.remove_link(id), - PipewireMessage::Connecting => { - imp.obj().connection_banner().set_revealed(true); - } - PipewireMessage::Connected => { - imp.obj().connection_banner().set_revealed(false); - }, - PipewireMessage::Disconnected => { - imp.clear(); - }, - }; - glib::ControlFlow::Continue - } - )); + pub async fn receive(&self, receiver: async_channel::Receiver) { + loop { + let Ok(msg) = receiver.recv().await else { + continue; + }; + match msg { + PipewireMessage::NodeAdded { + id, + name, + node_type, + } => self.add_node(id, name.as_str(), node_type), + PipewireMessage::NodeNameChanged { + id, + name, + media_name, + } => self.node_name_changed(id, &name, &media_name), + PipewireMessage::NodePropsChanged { id, volume, mute } => { + self.node_props_changed(id, volume, mute); + } + PipewireMessage::PortAdded { + id, + node_id, + name, + direction, + } => self.add_port(id, name.as_str(), node_id, direction), + PipewireMessage::PortFormatChanged { id, media_type } => { + self.port_media_type_changed(id, media_type) + } + PipewireMessage::LinkAdded { + id, + port_from, + port_to, + active, + media_type, + } => self.add_link(id, port_from, port_to, active, media_type), + PipewireMessage::LinkStateChanged { id, active } => { + self.link_state_changed(id, active) + } + PipewireMessage::LinkFormatChanged { id, media_type } => { + self.link_format_changed(id, media_type) + } + PipewireMessage::NodeRemoved { id } => self.remove_node(id), + PipewireMessage::PortRemoved { id, node_id } => self.remove_port(id, node_id), + PipewireMessage::LinkRemoved { id } => self.remove_link(id), + PipewireMessage::Connecting => { + self.obj().connection_banner().set_revealed(true); + } + PipewireMessage::Connected => { + self.obj().connection_banner().set_revealed(false); + } + PipewireMessage::Disconnected => { + self.clear(); + } + }; + } } /// Add a new node to the view. @@ -90,6 +115,31 @@ mod imp { log::info!("Adding node to graph: id {}", id); let node = graph::Node::new(name, id); + let sender = self.pw_sender.get().expect("pw_sender shoud be set"); + let s = sender.clone(); + node.connect_closure( + "volume-changed", + false, + closure_local!(move |n: graph::Node, volume: f32| { + s.send(crate::GtkMessage::NodeVolumeChanged { + id: n.pipewire_id(), + volume, + }) + .expect("Failed to send message"); + }), + ); + let s = sender.clone(); + node.connect_closure( + "mute-changed", + false, + closure_local!(move |n: graph::Node, mute: bool| { + s.send(crate::GtkMessage::NodeMuteChanged { + id: n.pipewire_id(), + mute, + }) + .expect("Failed to send message"); + }), + ); self.items.borrow_mut().insert(id, node.clone().upcast()); @@ -130,7 +180,13 @@ mod imp { } /// Add a new port to the view. - fn add_port(&self, id: u32, name: &str, node_id: u32, direction: pipewire::spa::Direction) { + fn add_port( + &self, + id: u32, + name: &str, + node_id: u32, + direction: pipewire::spa::utils::Direction, + ) { log::info!("Adding port to graph: id {}", id); let mut items = self.items.borrow_mut(); @@ -273,7 +329,11 @@ mod imp { link.set_active(active); } - fn link_format_changed(&self, id: u32, media_type: pipewire::spa::format::MediaType) { + fn link_format_changed( + &self, + id: u32, + media_type: pipewire::spa::param::format::MediaType, + ) { let items = self.items.borrow(); let Some(link) = items.get(&id) else { @@ -315,6 +375,26 @@ mod imp { self.items.borrow_mut().clear(); self.obj().graph().clear(); } + + fn node_props_changed(&self, id: u32, volume: Option, mute: Option) { + let items = self.items.borrow(); + + let Some(node) = items.get(&id) else { + log::warn!("Node (id: {id}) for changed name not found in graph manager"); + return; + }; + let Some(node) = node.dynamic_cast_ref::() else { + log::warn!("Graph Manager item under node (id: {id}) is not a node"); + return; + }; + + if let Some(vol) = volume { + node.set_node_volume(vol as f64) + } + if let Some(mute) = mute { + node.set_mute(mute); + } + } } } @@ -322,19 +402,23 @@ glib::wrapper! { pub struct GraphManager(ObjectSubclass); } +async fn receive(graph_manager: GraphManager, receiver: async_channel::Receiver) { + graph_manager.imp().receive(receiver).await +} + impl GraphManager { pub fn new( graph: &GraphView, connection_banner: &adw::Banner, sender: PwSender, - receiver: glib::Receiver, + receiver: async_channel::Receiver, ) -> Self { let res: Self = glib::Object::builder() .property("graph", graph) .property("connection-banner", connection_banner) .build(); - res.imp().attach_receiver(receiver); + glib::MainContext::default().spawn_local(receive(res.clone(), receiver)); assert!( res.imp().pw_sender.set(sender).is_ok(), "Should be able to set pw_sender)" diff --git a/src/main.rs b/src/main.rs index 610c175..710f728 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,15 +20,28 @@ mod pipewire_connection; mod ui; use adw::{gtk, prelude::*}; -use pipewire::spa::{format::MediaType, Direction}; +use pipewire::spa::{param::format::MediaType, utils::Direction}; /// Messages sent by the GTK thread to notify the pipewire thread. #[derive(Debug, Clone)] pub enum GtkMessage { /// Toggle a link between the two specified ports. - ToggleLink { port_from: u32, port_to: u32 }, + ToggleLink { + port_from: u32, + port_to: u32, + }, + /// Connect to PipeWire service. + Connect(Option), /// Quit the event loop and let the thread finish. Terminate, + NodeVolumeChanged { + id: u32, + volume: f32, + }, + NodeMuteChanged { + id: u32, + mute: bool, + }, } /// Messages sent by the pipewire thread to notify the GTK thread. @@ -44,6 +57,11 @@ pub enum PipewireMessage { name: String, media_name: String, }, + NodePropsChanged { + id: u32, + volume: Option, + mute: Option, + }, PortAdded { id: u32, node_id: u32, @@ -120,7 +138,7 @@ fn main() -> Result<(), Box> { // Start the pipewire thread with channels in both directions. - let (gtk_sender, gtk_receiver) = glib::MainContext::channel(glib::Priority::DEFAULT); + let (gtk_sender, gtk_receiver) = async_channel::unbounded(); let (pw_sender, pw_receiver) = pipewire::channel::channel(); let pw_thread = std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver)); diff --git a/src/pipewire_connection/libspa_ext.rs b/src/pipewire_connection/libspa_ext.rs new file mode 100644 index 0000000..2932be7 --- /dev/null +++ b/src/pipewire_connection/libspa_ext.rs @@ -0,0 +1,191 @@ +// FIXME: use libspa methods as soon as they are available + +use std::{ + borrow::BorrowMut, + mem::MaybeUninit, + os::raw::c_void, + pin::Pin, + ptr::{null, null_mut}, +}; + +use pipewire::spa::{ + pod::{builder::Builder, Pod}, + sys::{ + spa_pod, spa_pod_array, spa_pod_array_body, spa_pod_frame, spa_pod_object, + spa_pod_object_body, spa_pod_prop, spa_pod_prop_first, spa_pod_prop_is_inside, + spa_pod_prop_next, + }, +}; + +#[derive(Clone)] +pub struct PodPropsIter<'a> { + size: u32, + body: &'a spa_pod_object_body, + cur_prop: *const spa_pod_prop, +} + +impl<'a> PodPropsIter<'a> { + fn new(pod: &'a Pod) -> Self { + unsafe { + let raw_pod = pod.as_raw_ptr(); + let size = (*raw_pod).size; + let obj = raw_pod as *const spa_pod_object; + let body = &(*obj).body; + let cur_prop = null(); + Self { + size, + body, + cur_prop, + } + } + } +} + +pub struct PodPropKV<'a> { + k: u32, + v: &'a Pod, +} +impl<'a> PodPropKV<'a> { + pub fn key(&self) -> u32 { + self.k + } + pub fn value(&self) -> &'a Pod { + self.v + } +} + +impl<'a> Iterator for PodPropsIter<'a> { + type Item = PodPropKV<'a>; + fn next(&mut self) -> Option { + unsafe { + if self.cur_prop.is_null() { + self.cur_prop = spa_pod_prop_first(self.body); + } else { + self.cur_prop = spa_pod_prop_next(self.cur_prop); + } + if spa_pod_prop_is_inside(self.body, self.size, self.cur_prop) { + let inner_pod = Pod::from_raw(&(*self.cur_prop).value); + Some(PodPropKV { + k: (*self.cur_prop).key, + v: inner_pod, + }) + } else { + None + } + } + } +} + +#[derive(Clone)] +pub struct PodArrayIter<'a> { + pod: &'a Pod, + size: u32, + body: *const spa_pod_array_body, + iter: *mut spa_pod, +} +impl<'a> PodArrayIter<'a> { + fn new(pod: &'a Pod) -> Self { + assert!(pod.is_array()); + unsafe { + let raw_pod = pod.as_raw_ptr(); + let arr = raw_pod as *const spa_pod_array; + let body = &(*arr).body as *const spa_pod_array_body; + let size = (*raw_pod).size; + let iter = null_mut(); + Self { + pod, + body, + size, + iter, + } + } + } +} +impl<'a> Iterator for PodArrayIter<'a> { + type Item = &'a Pod; + fn next(&mut self) -> Option { + unsafe { + if self.iter.is_null() { + self.iter = self + .pod + .as_raw_ptr() + .byte_offset(size_of::() as isize); + } else { + self.iter = self.iter.byte_offset((*self.body).child.size as isize); + } + // FIXME: replace with spa_ptrinside in gnome47 platform + let p1 = self.body as *const c_void; + let p2 = self.iter as *const c_void; + let s1 = self.size as usize; + let s2 = (*self.body).child.size as usize; + if (*self.body).child.size > 0 + && p1 <= p2 && s2 <= s1 && p2.offset_from(p1) as usize <= s1 - s2 + { + Some(pipewire::spa::pod::Pod::from_raw(self.iter)) + } else { + None + } + } + } +} + +pub trait PodExt { + fn iter_props(&self) -> PodPropsIter; + fn iter_array(&self) -> PodArrayIter; +} +impl PodExt for Pod { + fn iter_props(&self) -> PodPropsIter { + PodPropsIter::new(self) + } + fn iter_array(&self) -> PodArrayIter { + PodArrayIter::new(self) + } +} + +// FIXME: Replace as soon as libspa fixes its builder API +pub struct FrameWrapper { + _inner: Pin>>, +} +pub trait BuilderExt { + fn push_object_frame(&mut self, type_: u32, id: u32) -> Result; + fn push_array_frame(&mut self) -> Result; + fn pop_frame(&mut self, frame: &mut FrameWrapper) -> &Pod; +} +impl<'d> BuilderExt for Builder<'d> { + fn push_object_frame(&mut self, type_: u32, id: u32) -> Result { + unsafe { + let mut obj_frame = FrameWrapper { + _inner: Box::pin(MaybeUninit::zeroed()), + }; + if self + .push_object(obj_frame._inner.borrow_mut(), type_, id) + .is_err() + { + Err(()) + } else { + Ok(obj_frame) + } + } + } + fn push_array_frame(&mut self) -> Result { + unsafe { + let mut array_frame = FrameWrapper { + _inner: Box::pin(MaybeUninit::zeroed()), + }; + if self.push_array(array_frame._inner.borrow_mut()).is_err() { + Err(()) + } else { + Ok(array_frame) + } + } + } + fn pop_frame(&mut self, frame: &mut FrameWrapper) -> &Pod { + unsafe { + let pod_ptr = pipewire::spa::sys::spa_pod_builder_pop( + self.as_raw_ptr(), + frame._inner.borrow_mut().assume_init_mut(), + ) as *const pipewire::spa::sys::spa_pod; + Pod::from_raw(pod_ptr) + } + } +} diff --git a/src/pipewire_connection/mod.rs b/src/pipewire_connection/mod.rs index 768f737..c1ce36d 100644 --- a/src/pipewire_connection/mod.rs +++ b/src/pipewire_connection/mod.rs @@ -14,31 +14,30 @@ // // SPDX-License-Identifier: GPL-3.0-only +mod libspa_ext; mod state; -use std::{ - cell::{Cell, RefCell}, - collections::HashMap, - rc::Rc, - time::Duration, -}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration}; use adw::glib::{self, clone}; +use libspa_ext::{BuilderExt, PodExt as _}; use log::{debug, error, info, warn}; use pipewire::{ + context::Context, + core::{Core, PW_ID_CORE}, keys, - link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState}, - node::{Node, NodeInfo, NodeListener}, - port::{Port, PortChangeMask, PortInfo, PortListener}, - prelude::*, - properties, + link::{Link, LinkChangeMask, LinkInfoRef, LinkListener, LinkState}, + main_loop::MainLoop, + node::{Node, NodeInfoRef, NodeListener}, + port::{Port, PortChangeMask, PortInfoRef, PortListener}, + properties::{properties, Properties}, registry::{GlobalObject, Registry}, spa::{ param::{ParamInfoFlags, ParamType}, - ForeignDict, SpaResult, + sys::{SPA_PARAM_Props, SPA_PROP_channelVolumes, SPA_PROP_mute, SPA_TYPE_OBJECT_Props}, + utils::{dict::DictRef, result::SpaResult, SpaTypes}, }, types::ObjectType, - Context, Core, MainLoop, }; use crate::{GtkMessage, MediaType, NodeType, PipewireMessage}; @@ -48,6 +47,7 @@ enum ProxyItem { Node { _proxy: Node, _listener: NodeListener, + _channels: Option, }, Port { proxy: Port, @@ -59,44 +59,78 @@ enum ProxyItem { }, } +struct LoopState { + is_stopped: bool, + props: Properties, +} + +impl LoopState { + fn handle_message(&mut self, msg: GtkMessage) -> bool { + match msg { + GtkMessage::Terminate => self.is_stopped = true, + GtkMessage::Connect(remote) => match remote { + Some(s) => self.props.insert(*keys::REMOTE_NAME, s), + None => self.props.remove(*keys::REMOTE_NAME), + }, + _ => return false, + } + true + } +} + /// The "main" function of the pipewire thread. pub(super) fn thread_main( - gtk_sender: glib::Sender, + gtk_sender: async_channel::Sender, mut pw_receiver: pipewire::channel::Receiver, ) { - let mainloop = MainLoop::new().expect("Failed to create mainloop"); + let mainloop = MainLoop::new(None).expect("Failed to create mainloop"); let context = Rc::new(Context::new(&mainloop).expect("Failed to create context")); - let is_stopped = Rc::new(Cell::new(false)); + let loop_state = Rc::new(RefCell::new(LoopState { + is_stopped: false, + props: properties! { + "media.category" => "Manager", + }, + })); let mut is_connecting = false; - while !is_stopped.get() { + // Wait PipeWire service to connect from command line arguments. + let receiver = pw_receiver.attach(mainloop.loop_(), { + clone!(@strong mainloop, @strong loop_state => move |msg| + if loop_state.borrow_mut().handle_message(msg) { + mainloop.quit(); + } + ) + }); + mainloop.run(); + pw_receiver = receiver.deattach(); + + while !loop_state.borrow().is_stopped { // Try to connect - let core = match context.connect(Some(properties! { - "media.category" => "Manager" - })) { + let props = loop_state.borrow().props.clone(); + let core = match context.connect(Some(props)) { Ok(core) => Rc::new(core), Err(_) => { if !is_connecting { is_connecting = true; gtk_sender - .send(PipewireMessage::Connecting) + .send_blocking(PipewireMessage::Connecting) .expect("Failed to send message"); } // If connection is failed, try to connect again in 200ms let interval = Some(Duration::from_millis(200)); - let timer = mainloop.add_timer(clone!(@strong mainloop => move |_| { - mainloop.quit(); - })); + let timer = mainloop + .loop_() + .add_timer(clone!(@strong mainloop => move |_| { + mainloop.quit(); + })); timer.update_timer(interval, None).into_result().unwrap(); - let receiver = pw_receiver.attach(&mainloop, { - clone!(@strong mainloop, @strong is_stopped => move |msg| - if let GtkMessage::Terminate = msg { - // main thread requested stop - is_stopped.set(true); + let receiver = pw_receiver.attach(mainloop.loop_(), { + clone!(@strong mainloop, @strong loop_state => move |msg| + if loop_state.borrow_mut().handle_message(msg) { mainloop.quit(); } ) @@ -112,7 +146,7 @@ pub(super) fn thread_main( if is_connecting { is_connecting = false; gtk_sender - .send(PipewireMessage::Connected) + .send_blocking(PipewireMessage::Connected) .expect("Failed to send message"); } @@ -122,33 +156,54 @@ pub(super) fn thread_main( let proxies = Rc::new(RefCell::new(HashMap::new())); let state = Rc::new(RefCell::new(State::new())); - let receiver = pw_receiver.attach(&mainloop, { - clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong is_stopped => move |msg| match msg { + let receiver = pw_receiver.attach(mainloop.loop_(), { + clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong loop_state, @strong proxies => move |msg| match msg { GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, ®istry, &state), - GtkMessage::Terminate => { - // main thread requested stop - is_stopped.set(true); + GtkMessage::Terminate | GtkMessage::Connect(_) => { + loop_state.borrow_mut().handle_message(msg); mainloop.quit(); } + GtkMessage::NodeVolumeChanged { id, volume } => { + let proxies = proxies.borrow(); + let Some(ProxyItem::Node { _proxy: node, _channels: channels, .. }) = proxies.get(&id) else { + error!("Received info on unknown node with id {id}"); + return; + }; + if let Some(channels) = channels { + set_node_param_props(node, vec![volume; *channels], None) + } else { + warn!("Volume change triggered on a node with no known channels"); + } + } + GtkMessage::NodeMuteChanged { id, mute } => { + let proxies = proxies.borrow(); + let Some(ProxyItem::Node { _proxy: node, .. }) = proxies.get(&id) else { + error!("Received info on unknown node with id {id}"); + return; + }; + set_node_param_props(node, vec![], Some(mute)) + } }) }); - let gtk_sender = gtk_sender.clone(); - let _listener = core.add_listener_local() - .error(clone!(@strong mainloop, @strong gtk_sender, @strong is_stopped => move |id, _seq, res, message| { - if id != pipewire::PW_ID_CORE { - return; - } + let _listener = core + .add_listener_local() + .error( + clone!(@strong mainloop, @strong gtk_sender => move |id, _seq, res, message| { + if id != PW_ID_CORE { + return; + } - if res == -libc::EPIPE { - gtk_sender.send(PipewireMessage::Disconnected) - .expect("Failed to send message"); - mainloop.quit(); - } else { - let serr = SpaResult::from_c(res).into_result().unwrap_err(); - error!("Pipewire Core received error {serr}: {message}"); - } - })) + if res == -libc::EPIPE { + gtk_sender.send_blocking(PipewireMessage::Disconnected) + .expect("Failed to send message"); + mainloop.quit(); + } else { + let serr = SpaResult::from_c(res).into_result().unwrap_err(); + error!("Pipewire Core received error {serr}: {message}"); + } + }), + ) .register(); let _listener = registry @@ -163,9 +218,9 @@ pub(super) fn thread_main( } } )) - .global_remove(clone!(@strong proxies, @strong state => move |id| { + .global_remove(clone!(@strong gtk_sender, @strong proxies, @strong state => move |id| { if let Some(item) = state.borrow_mut().remove(id) { - gtk_sender.send(match item { + gtk_sender.send_blocking(match item { Item::Node { .. } => PipewireMessage::NodeRemoved {id}, Item::Port { node_id } => PipewireMessage::PortRemoved {id, node_id}, Item::Link { .. } => PipewireMessage::LinkRemoved {id}, @@ -183,11 +238,15 @@ pub(super) fn thread_main( mainloop.run(); pw_receiver = receiver.deattach(); + + gtk_sender + .send_blocking(PipewireMessage::Disconnected) + .expect("Failed to send message"); } } /// Get the nicest possible name for the node, using a fallback chain of possible name attributes -fn get_node_name(props: &ForeignDict) -> &str { +fn get_node_name(props: &DictRef) -> &str { props .get(&keys::NODE_DESCRIPTION) .or_else(|| props.get(&keys::NODE_NICK)) @@ -197,8 +256,8 @@ fn get_node_name(props: &ForeignDict) -> &str { /// Handle a new node being added fn handle_node( - node: &GlobalObject, - sender: &glib::Sender, + node: &GlobalObject<&DictRef>, + sender: &async_channel::Sender, registry: &Rc, proxies: &Rc>>, state: &Rc>, @@ -233,7 +292,7 @@ fn handle_node( state.borrow_mut().insert(node.id, Item::Node); sender - .send(PipewireMessage::NodeAdded { + .send_blocking(PipewireMessage::NodeAdded { id: node.id, name, node_type, @@ -241,11 +300,18 @@ fn handle_node( .expect("Failed to send message"); let proxy: Node = registry.bind(node).expect("Failed to bind to node proxy"); + proxy.subscribe_params(&[ParamType::Props]); + let id = node.id; let listener = proxy .add_listener_local() .info(clone!(@strong sender, @strong proxies => move |info| { handle_node_info(info, &sender, &proxies); })) + .param(clone!(@strong sender, @strong proxies => move |_: i32, param_type: pipewire::spa::param::ParamType, _: u32, _:u32, pod: Option<&pipewire::spa::pod::Pod>| { + if let Some(pod) = pod { + handle_node_params(id, param_type, pod, &sender, &proxies); + } + })) .register(); proxies.borrow_mut().insert( @@ -253,13 +319,14 @@ fn handle_node( ProxyItem::Node { _proxy: proxy, _listener: listener, + _channels: None, }, ); } fn handle_node_info( - info: &NodeInfo, - sender: &glib::Sender, + info: &NodeInfoRef, + sender: &async_channel::Sender, proxies: &Rc>>, ) { debug!("Received node info: {:?}", info); @@ -276,7 +343,7 @@ fn handle_node_info( let name = get_node_name(props).to_string(); sender - .send(PipewireMessage::NodeNameChanged { + .send_blocking(PipewireMessage::NodeNameChanged { id, name, media_name: media_name.to_string(), @@ -285,10 +352,93 @@ fn handle_node_info( } } +fn handle_node_params( + id: u32, + param_type: pipewire::spa::param::ParamType, + pod: &pipewire::spa::pod::Pod, + sender: &async_channel::Sender, + proxies: &Rc>>, +) { + debug!( + "Received node params of type {:?}: {:?}", + param_type, + pod.type_() + ); + + let mut proxies = proxies.borrow_mut(); + let Some(ProxyItem::Node { + _channels: channels, + .. + }) = proxies.get_mut(&id) + else { + error!("Received info on unknown node with id {id}"); + return; + }; + + if pod.type_() == SpaTypes::Object { + let mut volume = None; + let mut mute = None; + for prop in pod.iter_props() { + if prop.key() == SPA_PROP_channelVolumes && prop.value().is_array() { + let mut iter = prop.value().iter_array().filter(|p| p.is_float()); + *channels = Some(iter.clone().count()); + // We're only interested in the "master" volume of the node, so take first + // channel volume as volume + volume = iter.next().map(|p| { + p.get_float() + .expect("values in POD array should have been float") + // Calculate cubic volume value from linear + .cbrt() + }); + } else if prop.key() == SPA_PROP_mute && prop.value().is_bool() { + mute = prop.value().get_bool().ok(); + } + } + sender + .send_blocking(PipewireMessage::NodePropsChanged { id, volume, mute }) + .expect("Failed to send message"); + } +} + +fn set_node_param_props(node: &Node, channel_volumes: Vec, mute: Option) { + let mut buffer = vec![0u8; 1024]; + let mut builder = pipewire::spa::pod::builder::Builder::new(&mut buffer); + let mut obj_frame = builder + .push_object_frame(SPA_TYPE_OBJECT_Props, SPA_PARAM_Props) + .expect("Failed to push param props object to builder"); + + if let Some(m) = mute { + builder + .add_prop(SPA_PROP_mute, 0) + .expect("Failed to add mute prop"); + builder + .add_bool(m) + .expect("Failed to add value for mute prop"); + } + + if !channel_volumes.is_empty() { + builder + .add_prop(SPA_PROP_channelVolumes, 0) + .expect("Failed to add channelVolumes prop"); + let mut array_frame = builder + .push_array_frame() + .expect("Failed to push array value for channelVolumes prop"); + for v in channel_volumes { + // Volume is cubic, but we display in linear + builder + .add_float(v * v * v) + .expect("Failed to add array entry for channelVolumes prop"); + } + builder.pop_frame(&mut array_frame); + } + + let pod = builder.pop_frame(&mut obj_frame); + node.set_param(ParamType::Props, 0, pod); +} /// Handle a new port being added fn handle_port( - port: &GlobalObject, - sender: &glib::Sender, + port: &GlobalObject<&DictRef>, + sender: &async_channel::Sender, registry: &Rc, proxies: &Rc>>, state: &Rc>, @@ -319,10 +469,10 @@ fn handle_port( } fn handle_port_info( - info: &PortInfo, + info: &PortInfoRef, proxies: &Rc>>, state: &Rc>, - sender: &glib::Sender, + sender: &async_channel::Sender, ) { debug!("Received port info: {:?}", info); @@ -363,7 +513,7 @@ fn handle_port_info( } sender - .send(PipewireMessage::PortAdded { + .send_blocking(PipewireMessage::PortAdded { id, node_id, name, @@ -376,7 +526,7 @@ fn handle_port_info( fn handle_port_enum_format( port_id: u32, param: Option<&pipewire::spa::pod::Pod>, - sender: &glib::Sender, + sender: &async_channel::Sender, ) { let media_type = param .and_then(|param| pipewire::spa::param::format_utils::parse_format(param).ok()) @@ -384,7 +534,7 @@ fn handle_port_enum_format( .unwrap_or(MediaType::Unknown); sender - .send(PipewireMessage::PortFormatChanged { + .send_blocking(PipewireMessage::PortFormatChanged { id: port_id, media_type, }) @@ -393,8 +543,8 @@ fn handle_port_enum_format( /// Handle a new link being added fn handle_link( - link: &GlobalObject, - sender: &glib::Sender, + link: &GlobalObject<&DictRef>, + sender: &async_channel::Sender, registry: &Rc, proxies: &Rc>>, state: &Rc>, @@ -422,9 +572,9 @@ fn handle_link( } fn handle_link_info( - info: &LinkInfo, + info: &LinkInfoRef, state: &Rc>, - sender: &glib::Sender, + sender: &async_channel::Sender, ) { debug!("Received link info: {:?}", info); @@ -435,7 +585,7 @@ fn handle_link_info( // Info was an update - figure out if we should notify the gtk thread if info.change_mask().contains(LinkChangeMask::STATE) { sender - .send(PipewireMessage::LinkStateChanged { + .send_blocking(PipewireMessage::LinkStateChanged { id, active: matches!(info.state(), LinkState::Active), }) @@ -443,7 +593,7 @@ fn handle_link_info( } if info.change_mask().contains(LinkChangeMask::FORMAT) { sender - .send(PipewireMessage::LinkFormatChanged { + .send_blocking(PipewireMessage::LinkFormatChanged { id, media_type: get_link_media_type(info), }) @@ -457,7 +607,7 @@ fn handle_link_info( state.insert(id, Item::Link { port_from, port_to }); sender - .send(PipewireMessage::LinkAdded { + .send_blocking(PipewireMessage::LinkAdded { id, port_from, port_to, @@ -495,7 +645,7 @@ fn toggle_link( .get_node_of_port(port_to) .expect("Requested port not in state"); - if let Err(e) = core.create_object::( + if let Err(e) = core.create_object::( "link-factory", &properties! { "link.output.node" => node_from.to_string(), @@ -510,7 +660,7 @@ fn toggle_link( } } -fn get_link_media_type(link_info: &LinkInfo) -> MediaType { +fn get_link_media_type(link_info: &LinkInfoRef) -> MediaType { let media_type = link_info .format() .and_then(|format| pipewire::spa::param::format_utils::parse_format(format).ok()) diff --git a/src/ui/graph/graph_view.rs b/src/ui/graph/graph_view.rs index 5825dae..0898b38 100644 --- a/src/ui/graph/graph_view.rs +++ b/src/ui/graph/graph_view.rs @@ -42,8 +42,8 @@ mod imp { use adw::gtk::gdk::{self}; use log::warn; use once_cell::sync::Lazy; - use pipewire::spa::format::MediaType; - use pipewire::spa::Direction; + use pipewire::spa::param::format::MediaType; + use pipewire::spa::utils::Direction; pub struct Colors { audio: gdk::RGBA, @@ -787,7 +787,10 @@ impl GraphView { 420.0 }; - let y = imp + // FIXME: We are currently using a constant 180 for the height of the nodes, this could cause problems for nodes with a lot of ports. + let node_height = 180.0; + + let mut column = imp .nodes .borrow() .iter() @@ -800,11 +803,24 @@ impl GraphView { // Only look for other nodes that have a similar x coordinate (x - x2).abs() < 50.0 }) - .max_by(|y1, y2| { - // Get max in column - y1.partial_cmp(y2).unwrap_or(Ordering::Equal) + .collect::>(); + column + .sort_unstable_by(|(_, a_y), (_, b_y)| a_y.partial_cmp(b_y).unwrap_or(Ordering::Less)); + let y = column + .windows(2) + .map(|w| { + // Calculate space between this node and the next + let (a_y, b_y) = (w[0].1, w[1].1); + let diff_next = b_y - a_y; + (a_y, diff_next) }) - .map_or(20_f32, |(_x, y)| y + 120.0); + .find(|(_y, diff_next)| *diff_next >= 2.0 * node_height) + .map_or( + // If we didn't find enough space between nodes, append to bottom + column.last().map_or(20_f32, |(_x, y)| y + node_height), + // Put new node after below the node we found + |(y, _)| y + node_height, + ); imp.nodes.borrow_mut().insert(node, Point::new(x, y)); } diff --git a/src/ui/graph/link.rs b/src/ui/graph/link.rs index 140c74c..eb1343b 100644 --- a/src/ui/graph/link.rs +++ b/src/ui/graph/link.rs @@ -15,7 +15,7 @@ // SPDX-License-Identifier: GPL-3.0-only use adw::{glib, prelude::*, subclass::prelude::*}; -use pipewire::spa::format::MediaType; +use pipewire::spa::param::format::MediaType; use super::Port; diff --git a/src/ui/graph/node.rs b/src/ui/graph/node.rs index a3f1afb..6ba26e1 100644 --- a/src/ui/graph/node.rs +++ b/src/ui/graph/node.rs @@ -15,16 +15,19 @@ // SPDX-License-Identifier: GPL-3.0-only use adw::{glib, gtk, prelude::*, subclass::prelude::*}; -use pipewire::spa::Direction; +use pipewire::spa::utils::Direction; use super::Port; mod imp { + use glib::subclass::Signal; + use super::*; use std::{ cell::{Cell, RefCell}, collections::HashSet, + sync::OnceLock, }; #[derive(glib::Properties, gtk::CompositeTemplate, Default)] @@ -55,6 +58,22 @@ mod imp { #[template_child] pub(super) media_name: TemplateChild, #[template_child] + pub(super) node_mute_button: TemplateChild, + #[property( + name = "node-volume", type = f64, + get = |this: &Self| { + let val = this.node_volume.value(); + val/100.0 + }, + set = |this: &Self, val : f64| { + this.node_volume.set_value(val*100.0); + self.node_volume.set_visible(true); + self.node_mute_button.set_visible(true); + } + )] + #[template_child] + pub(super) node_volume: TemplateChild, + #[template_child] pub(super) separator: TemplateChild, #[template_child] pub(super) port_grid: TemplateChild, @@ -91,6 +110,11 @@ mod imp { // Display a grab cursor when the mouse is over the label so the user knows the node can be dragged. self.node_name .set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref()); + + self.node_volume + .set_adjustment(>k::Adjustment::new(0.0, 0.0, 100.0, 0.5, 5.0, 0.0)); + + self.set_up_volume_ctl(); } fn dispose(&self) { @@ -98,6 +122,20 @@ mod imp { child.unparent(); } } + + fn signals() -> &'static [Signal] { + static SIGNALS: OnceLock> = OnceLock::new(); + SIGNALS.get_or_init(|| { + vec![ + Signal::builder("volume-changed") + .param_types([f32::static_type()]) + .build(), + Signal::builder("mute-changed") + .param_types([bool::static_type()]) + .build(), + ] + }) + } } impl WidgetImpl for Node {} @@ -129,8 +167,8 @@ mod imp { _ => unreachable!(), }); - ports_out.sort_unstable_by_key(|port| port.name()); - ports_in.sort_unstable_by_key(|port| port.name()); + ports_out.sort_unstable_by(|&a, &b| natord::compare(&a.name(), &b.name())); + ports_in.sort_unstable_by(|&a, &b| natord::compare(&a.name(), &b.name())); // In case no ports have been added to the port, hide the seperator as it is not needed self.separator @@ -144,6 +182,28 @@ mod imp { self.port_grid.attach(port, 1, i.try_into().unwrap(), 1, 1); } } + + fn set_up_volume_ctl(&self) { + let pipewire_id = self.pipewire_id.clone(); + let obj = self.obj().clone(); + self.node_volume.connect_change_value(move |_, _, val| { + let percent = format!("{:.3}%", val.to_string()); + log::info!("Changing volume of {} to {}", pipewire_id.get(), percent); + obj.emit_by_name::<()>("volume-changed", &[&(val as f32 / 100.0f32)]); + glib::Propagation::Proceed + }); + + let pipewire_id = self.pipewire_id.clone(); + let obj = self.obj().clone(); + self.node_mute_button.connect_clicked(move |button| { + let is_muted = button + .icon_name() + .expect("icon_name for mute button should be set") + == "audio-volume-muted"; + log::info!("Changing mute of {} to {}", pipewire_id.get(), !is_muted); + obj.emit_by_name::<()>("mute-changed", &[&(!is_muted)]); + }); + } } } @@ -174,4 +234,12 @@ impl Node { log::warn!("Tried to remove non-existant port widget from node"); } } + + pub fn set_mute(&self, is_muted: bool) { + self.imp().node_mute_button.set_icon_name(if is_muted { + "audio-volume-muted" + } else { + "audio-volume-high" + }); + } } diff --git a/src/ui/graph/node.ui b/src/ui/graph/node.ui index 9a80c56..de9fb4d 100644 --- a/src/ui/graph/node.ui +++ b/src/ui/graph/node.ui @@ -41,6 +41,27 @@ + + + horizontal + + + audio-volume-high + false + + + + + horizontal + true + right + 0 + false + true + + + + diff --git a/src/ui/graph/port.rs b/src/ui/graph/port.rs index 13a5d9b..c11f310 100644 --- a/src/ui/graph/port.rs +++ b/src/ui/graph/port.rs @@ -21,17 +21,17 @@ use adw::{ prelude::*, subclass::prelude::*, }; -use pipewire::spa::Direction; +use pipewire::spa::utils::Direction; use super::PortHandle; mod imp { use super::*; - use std::cell::Cell; + use std::cell::{Cell, OnceCell}; - use once_cell::{sync::Lazy, unsync::OnceCell}; - use pipewire::spa::{format::MediaType, Direction}; + use once_cell::sync::Lazy; + use pipewire::spa::{param::format::MediaType, utils::Direction}; /// Graphical representation of a pipewire port. #[derive(gtk::CompositeTemplate, glib::Properties)] diff --git a/src/ui/window.rs b/src/ui/window.rs index e1e410b..7c10354 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -15,6 +15,9 @@ mod imp { #[property(type = adw::Banner, get = |_| self.connection_banner.clone())] pub connection_banner: TemplateChild, #[template_child] + #[property(type = gtk::Label, get = |_| self.current_remote_label.clone())] + pub current_remote_label: TemplateChild, + #[template_child] #[property(type = graph::GraphView, get = |_| self.graph.clone())] pub graph: TemplateChild, } diff --git a/src/ui/window.ui b/src/ui/window.ui index c1df524..0af42b6 100644 --- a/src/ui/window.ui +++ b/src/ui/window.ui @@ -13,11 +13,40 @@