diff --git a/Cargo.lock b/Cargo.lock index 87f73d106..d1106bacb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,12 +35,12 @@ dependencies = [ [[package]] name = "annotate-snippets" -version = "0.9.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ - "unicode-width 0.1.14", - "yansi-term", + "anstyle", + "unicode-width", ] [[package]] @@ -121,31 +121,16 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.0" +name = "atomic_refcell" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] -name = "bindgen" -version = "0.69.5" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "annotate-snippets", - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.87", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bindgen" @@ -153,6 +138,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ + "annotate-snippets", "bitflags 2.9.4", "cexpr", "clang-sys", @@ -162,9 +148,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -199,9 +185,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -225,17 +211,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon 0.12.16", + "nom 7.1.3", ] [[package]] @@ -245,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", - "target-lexicon 0.13.2", + "target-lexicon", ] [[package]] @@ -302,7 +278,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -336,24 +312,24 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width", "windows-sys 0.61.1", ] [[package]] name = "convert_case" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ "unicode-segmentation", ] [[package]] name = "convert_case" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" dependencies = [ "unicode-segmentation", ] @@ -363,9 +339,6 @@ name = "cookie-factory" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] [[package]] name = "crossterm" @@ -412,7 +385,7 @@ dependencies = [ "convert_case 0.7.1", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -463,14 +436,14 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -492,12 +465,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -555,21 +528,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -577,7 +535,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -597,12 +554,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -611,7 +562,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -638,13 +589,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -659,7 +606,64 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gio-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.1", +] + +[[package]] +name = "glib" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" +dependencies = [ + "bitflags 2.9.4", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "glib-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" +dependencies = [ + "libc", + "system-deps", ] [[package]] @@ -668,6 +672,139 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gobject-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7ba7a2584e31927b7fec6a32737b57dc991b55253c9bb7c2c8eddb5a4cb345" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools 0.14.0", + "kstring", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "pastey", + "pin-project-lite", + "smallvec", + "thiserror 2.0.17", +] + +[[package]] +name = "gstreamer-app" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" +dependencies = [ + "futures-core", + "futures-sink", + "glib", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-audio" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8" +dependencies = [ + "cfg-if", + "glib", + "gstreamer", + "gstreamer-audio-sys", + "gstreamer-base", + "libc", + "smallvec", +] + +[[package]] +name = "gstreamer-audio-sys" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377" +dependencies = [ + "cfg-if", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -686,12 +823,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - [[package]] name = "indexmap" version = "2.11.4" @@ -714,15 +845,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -747,6 +869,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -758,16 +904,13 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "kstring" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] [[package]] name = "libc" @@ -794,8 +937,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae4417fe6c528d48e1982b8fc7fdd9999013065cb8b4978369c2e4fea69ad4df" dependencies = [ - "bindgen 0.72.1", - "system-deps 7.0.5", + "bindgen", + "system-deps", ] [[package]] @@ -810,30 +953,30 @@ dependencies = [ [[package]] name = "libspa" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" dependencies = [ "bitflags 2.9.4", "cc", - "convert_case 0.6.0", + "convert_case 0.8.0", "cookie-factory", "libc", "libspa-sys", - "nix 0.27.1", - "nom", - "system-deps 6.2.2", + "nix 0.30.1", + "nom 8.0.0", + "system-deps", ] [[package]] name = "libspa-sys" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" dependencies = [ - "bindgen 0.69.5", + "bindgen", "cc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -850,11 +993,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -944,9 +1086,15 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + [[package]] name = "nb" version = "1.1.0" @@ -978,17 +1126,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -1024,6 +1161,43 @@ 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 = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.4" @@ -1043,7 +1217,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1058,11 +1232,20 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "option-operations" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689" +dependencies = [ + "pastey", +] + [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1070,17 +1253,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1095,30 +1284,30 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipewire" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" dependencies = [ "anyhow", "bitflags 2.9.4", "libc", "libspa", "libspa-sys", - "nix 0.27.1", + "nix 0.30.1", "once_cell", "pipewire-sys", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] name = "pipewire-sys" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" dependencies = [ - "bindgen 0.69.5", + "bindgen", "libspa-sys", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -1127,6 +1316,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1164,12 +1368,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1254,9 +1458,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -1304,7 +1508,7 @@ checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1338,16 +1542,10 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.87", + "syn 2.0.106", "unicode-ident", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -1457,7 +1655,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1581,6 +1779,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1600,35 +1804,22 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr 0.15.8", - "heck", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - [[package]] name = "system-deps" version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "cfg-expr 0.20.3", + "cfg-expr", "heck", "pkg-config", "toml 0.8.23", @@ -1641,12 +1832,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "target-lexicon" version = "0.13.2" @@ -1698,7 +1883,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1709,7 +1894,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] [[package]] @@ -1826,15 +2011,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unsafe-libyaml" @@ -2074,6 +2253,9 @@ dependencies = [ "alsa", "clap", "env_logger", + "gstreamer", + "gstreamer-app", + "gstreamer-audio", "log", "pipewire", "rand", @@ -2246,11 +2428,20 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -2276,7 +2467,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -2298,7 +2489,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2364,7 +2555,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.4", ] [[package]] @@ -2394,10 +2585,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -2514,13 +2706,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.4", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wyz" @@ -2541,31 +2730,22 @@ dependencies = [ "toml 0.9.7", ] -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] - [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.106", ] diff --git a/vhost-device-sound/CHANGELOG.md b/vhost-device-sound/CHANGELOG.md index d11f4a5e1..12bd12d35 100644 --- a/vhost-device-sound/CHANGELOG.md +++ b/vhost-device-sound/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added +- [[#876]](https://github.com/rust-vmm/vhost-device/pull/876) Add GStreamer audio backend support - [[#806]](https://github.com/rust-vmm/vhost-device/pull/806) Add controls field in VirtioSoundConfig - [[#746]](https://github.com/rust-vmm/vhost-device/pull/746) Add new sampling rates 12000Hz and 24000Hz @@ -14,6 +15,10 @@ - [[#808]](https://github.com/rust-vmm/vhost-device/pull/808) pipewire: Fix rand module imports - [[#884]](https://github.com/rust-vmm/vhost-device/pull/884) vhost-device-sound/pipewire: fix wrong format +### Limitations + +- GStreamer backend: 20-bit PCM formats (VIRTIO_SND_PCM_FMT_S20/U20) are not directly supported by GStreamer and are automatically converted to 24/32-bit formats + ### Deprecated ## v0.2.0 diff --git a/vhost-device-sound/Cargo.toml b/vhost-device-sound/Cargo.toml index 53630e14d..c00c30a23 100644 --- a/vhost-device-sound/Cargo.toml +++ b/vhost-device-sound/Cargo.toml @@ -12,9 +12,10 @@ edition = "2021" [features] xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"] -default = ["alsa-backend", "pw-backend"] +default = ["alsa-backend", "pw-backend", "gst-backend"] alsa-backend = ["dep:alsa"] pw-backend = ["pw"] +gst-backend = ["dep:gst", "dep:gst-app", "dep:gst-audio"] [dependencies] clap = { version = "4.5", features = ["derive"] } @@ -32,6 +33,9 @@ vmm-sys-util = "0.14" [target.'cfg(target_env = "gnu")'.dependencies] alsa = { version = "0.10", optional = true } pw = { package = "pipewire", version = "0.9.2", optional = true } +gst = { package = "gstreamer", version = "0.24.2", optional = true, features = ["v1_24"] } +gst-app = { package = "gstreamer-app", version = "0.24.2", optional = true, features = ["v1_24"] } +gst-audio = {package = "gstreamer-audio", version = "0.24.2", optional = true, features = ["v1_24"] } [dev-dependencies] rstest = "0.26.1" diff --git a/vhost-device-sound/README.md b/vhost-device-sound/README.md index a06356ee5..92d7aca2a 100644 --- a/vhost-device-sound/README.md +++ b/vhost-device-sound/README.md @@ -16,7 +16,7 @@ generated with help2man target/debug/vhost-device-sound |mandoc vhost-user Unix domain socket path --backend - audio backend to be used [possible values: null, pipewire, alsa] + audio backend to be used [possible values: null, pipewire, alsa, gstreamer] -h, --help Print help diff --git a/vhost-device-sound/src/args.rs b/vhost-device-sound/src/args.rs index 7cd51d5e2..5cda63cc6 100644 --- a/vhost-device-sound/src/args.rs +++ b/vhost-device-sound/src/args.rs @@ -25,4 +25,7 @@ pub enum BackendType { Pipewire, #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] Alsa, + #[cfg(all(feature = "gst-backend", target_env = "gnu"))] + #[value(name = "gstreamer")] + GStreamer, } diff --git a/vhost-device-sound/src/audio_backends.rs b/vhost-device-sound/src/audio_backends.rs index 25a940846..a56e7e8ee 100644 --- a/vhost-device-sound/src/audio_backends.rs +++ b/vhost-device-sound/src/audio_backends.rs @@ -8,10 +8,15 @@ mod null; #[cfg(all(feature = "pw-backend", target_env = "gnu"))] mod pipewire; +#[cfg(all(feature = "gst-backend", target_env = "gnu"))] +mod gstreamer; + use std::sync::{Arc, RwLock}; #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] use self::alsa::AlsaBackend; +#[cfg(all(feature = "gst-backend", target_env = "gnu"))] +use self::gstreamer::GStreamerBackend; use self::null::NullBackend; #[cfg(all(feature = "pw-backend", target_env = "gnu"))] use self::pipewire::PwBackend; @@ -61,6 +66,12 @@ pub fn alloc_audio_backend( } #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] BackendType::Alsa => Ok(Box::new(AlsaBackend::new(streams))), + #[cfg(all(feature = "gst-backend", target_env = "gnu"))] + BackendType::GStreamer => { + Ok(Box::new(GStreamerBackend::new(streams).map_err(|err| { + crate::Error::UnexpectedAudioBackendError(err.into()) + })?)) + } } } diff --git a/vhost-device-sound/src/audio_backends/gstreamer.rs b/vhost-device-sound/src/audio_backends/gstreamer.rs new file mode 100644 index 000000000..f8eb02d71 --- /dev/null +++ b/vhost-device-sound/src/audio_backends/gstreamer.rs @@ -0,0 +1,1174 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use gst::{glib::Error as GlibError, prelude::*, Pipeline}; +use gst_app; +use gst_audio::{AudioFormat, AudioInfo}; +use thiserror::Error as ThisError; + +use super::AudioBackend; +use crate::{ + stream::{Error as StreamError, PCMState, PcmParams}, + virtio_sound::{ + VirtioSndPcmSetParams, VIRTIO_SND_PCM_FMT_A_LAW, VIRTIO_SND_PCM_FMT_FLOAT, + VIRTIO_SND_PCM_FMT_FLOAT64, VIRTIO_SND_PCM_FMT_MU_LAW, VIRTIO_SND_PCM_FMT_S16, + VIRTIO_SND_PCM_FMT_S18_3, VIRTIO_SND_PCM_FMT_S20, VIRTIO_SND_PCM_FMT_S20_3, + VIRTIO_SND_PCM_FMT_S24, VIRTIO_SND_PCM_FMT_S24_3, VIRTIO_SND_PCM_FMT_S32, + VIRTIO_SND_PCM_FMT_S8, VIRTIO_SND_PCM_FMT_U16, VIRTIO_SND_PCM_FMT_U18_3, + VIRTIO_SND_PCM_FMT_U20, VIRTIO_SND_PCM_FMT_U20_3, VIRTIO_SND_PCM_FMT_U24, + VIRTIO_SND_PCM_FMT_U24_3, VIRTIO_SND_PCM_FMT_U32, VIRTIO_SND_PCM_FMT_U8, + VIRTIO_SND_PCM_RATE_11025, VIRTIO_SND_PCM_RATE_12000, VIRTIO_SND_PCM_RATE_16000, + VIRTIO_SND_PCM_RATE_176400, VIRTIO_SND_PCM_RATE_192000, VIRTIO_SND_PCM_RATE_22050, + VIRTIO_SND_PCM_RATE_24000, VIRTIO_SND_PCM_RATE_32000, VIRTIO_SND_PCM_RATE_384000, + VIRTIO_SND_PCM_RATE_44100, VIRTIO_SND_PCM_RATE_48000, VIRTIO_SND_PCM_RATE_5512, + VIRTIO_SND_PCM_RATE_64000, VIRTIO_SND_PCM_RATE_8000, VIRTIO_SND_PCM_RATE_88200, + VIRTIO_SND_PCM_RATE_96000, + }, + Direction, Error, Result, Stream, +}; + +/// Error type for the Gstreamer backend +#[derive(Debug, ThisError)] +pub enum GstError { + #[error("Failed to initialize GStreamer: {0}")] + InitError(GlibError), +} + +pub struct GStreamerBackendIn { + pipeline: Pipeline, +} + +impl GStreamerBackendIn { + pub fn new( + caps: &gst::Caps, + stream_id: u32, + streams: Arc>>, + ) -> Result { + // Create the input pipeline + let pipeline = gst::Pipeline::with_name("audio_input_pipeline"); + + let autoaudiosrc = + gst::ElementFactory::make_with_name("autoaudiosrc", Some("autoaudiosrc")) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + let appsink = gst_app::AppSink::builder() + .name("audio_appsink") + .caps(caps) + .build(); + + pipeline + .add_many([&autoaudiosrc, appsink.upcast_ref()]) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + gst::Element::link_many([&autoaudiosrc, appsink.upcast_ref()]) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + appsink.set_callbacks( + gst_app::AppSinkCallbacks::builder() + .new_sample(move |appsink| { + log::debug!("AppSink new sample for stream {stream_id}"); + + // get sample and buffer + let sample = match appsink.pull_sample() { + Ok(sample) => sample, + Err(err) => { + log::error!("Failed to pull sample: {err:?}"); + return Err(gst::FlowError::Eos); + } + }; + + let buffer = sample.buffer().ok_or_else(|| { + log::error!("Failed to get buffer from sample"); + gst::FlowError::Error + })?; + + let map = match buffer.map_readable() { + Ok(map) => map, + Err(err) => { + log::error!("Failed to map buffer: {err:?}"); + return Err(gst::FlowError::Error); + } + }; + + let slice = map.as_slice(); + let mut n_samples = slice.len(); + let mut start = 0; + + let mut stream_params = streams.write().unwrap(); + let stream = match stream_params.get_mut(stream_id as usize) { + Some(s) => s, + None => { + log::error!("Stream {stream_id} not found in appsink callback"); + return Err(gst::FlowError::Error); + } + }; + + while n_samples > 0 { + let Some(request) = stream.requests.front_mut() else { + log::debug!("No request available for input stream {stream_id}"); + return Err(gst::FlowError::Eos); + }; + + let avail = request.len().saturating_sub(request.pos); + let n_bytes = n_samples.min(avail); + + let p = &slice[start..start + n_bytes]; + + let written = request + .write_input(p) + .expect("Failed to write input to guest") + as usize; + + if written == 0 { + log::debug!("Wrote 0 bytes, breaking"); + break; + } + + n_samples -= written; + start += written; + + if request.pos >= request.len() { + stream.requests.pop_front(); + } + } + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + Ok(Self { pipeline }) + } +} + +pub struct GStreamerBackendOut { + pipeline: Pipeline, +} + +impl GStreamerBackendOut { + pub fn new( + caps: &gst::Caps, + stream_id: u32, + streams: Arc>>, + ) -> Result { + // Create the output pipeline + let pipeline = gst::Pipeline::with_name("audio_output_pipeline"); + + let appsrc = gst_app::AppSrc::builder() + .name("audio_appsrc") + .caps(caps) + .build(); + + let autoaudiosink = + gst::ElementFactory::make_with_name("autoaudiosink", Some("autoaudiosink")) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + pipeline + .add_many([appsrc.upcast_ref(), &autoaudiosink]) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + gst::Element::link_many([appsrc.upcast_ref(), &autoaudiosink]) + .map_err(|e| Error::UnexpectedAudioBackendError(e.into()))?; + + appsrc.set_callbacks( + gst_app::AppSrcCallbacks::builder() + .need_data(move |appsrc, _| { + log::debug!("AppSrc need data for stream {stream_id}"); + let mut stream_params = streams.write().unwrap(); + let stream = stream_params + .get_mut(stream_id as usize) + .expect("Stream does not exist"); + let Some(request) = stream.requests.front_mut() else { + return; + }; + + // Check if the request has data to read + let avail = request.len().saturating_sub(request.pos); + if avail == 0 { + stream.requests.pop_front(); + return; + } + + // push data to appsrc + let period_bytes = stream.params.period_bytes.to_native() as usize; + let to_send = avail.min(period_bytes); + + let mut buffer = match gst::Buffer::with_size(to_send) { + Ok(buf) => buf, + Err(e) => { + log::error!("Failed to create buffer: {e:?}"); + return; + } + }; + + { + let buffer = match buffer.get_mut() { + Some(buf) => buf, + None => { + log::error!("Failed to get mutable buffer reference"); + return; + } + }; + let mut map = match buffer.map_writable() { + Ok(map) => map, + Err(e) => { + log::error!("Failed to map buffer: {e:?}"); + return; + } + }; + let slice = map.as_mut_slice(); + + // copy data from request to buffer + let written = request + .read_output(slice) + .expect("Failed to read output buffer from guest"); + + request.pos += written as usize; + if request.pos >= request.len() { + stream.requests.pop_front(); + } + } + + // push data to appsrc + if let Err(err) = appsrc.push_buffer(buffer) { + log::error!("Failed to push buffer: {err}"); + } + }) + .build(), + ); + + Ok(Self { pipeline }) + } +} + +pub struct GStreamerBackend { + stream_params: Arc>>, + stream_in: RwLock>, + stream_out: RwLock>, +} + +impl GStreamerBackend { + pub fn new(stream_params: Arc>>) -> std::result::Result { + // init GStreamer + log::debug!("Initializing GStreamer backend"); + + gst::init().map_err(GstError::InitError)?; + + Ok(Self { + stream_params, + stream_in: RwLock::new(HashMap::new()), + stream_out: RwLock::new(HashMap::new()), + }) + } + + #[cfg(target_endian = "little")] + pub fn set_format(&self, params: &PcmParams) -> Result { + let format = match params.format { + VIRTIO_SND_PCM_FMT_MU_LAW => AudioFormat::Encoded, + VIRTIO_SND_PCM_FMT_A_LAW => AudioFormat::Encoded, + VIRTIO_SND_PCM_FMT_S8 => AudioFormat::S8, + VIRTIO_SND_PCM_FMT_U8 => AudioFormat::U8, + VIRTIO_SND_PCM_FMT_S16 => AudioFormat::S16le, + VIRTIO_SND_PCM_FMT_U16 => AudioFormat::U16le, + VIRTIO_SND_PCM_FMT_S18_3 => AudioFormat::S18le, + VIRTIO_SND_PCM_FMT_U18_3 => AudioFormat::U18le, + VIRTIO_SND_PCM_FMT_S20_3 => AudioFormat::S20le, + VIRTIO_SND_PCM_FMT_U20_3 => AudioFormat::U20le, + VIRTIO_SND_PCM_FMT_S24_3 => AudioFormat::S24le, + VIRTIO_SND_PCM_FMT_U24_3 => AudioFormat::U24le, + VIRTIO_SND_PCM_FMT_S20 => { + log::warn!("20-bit format not directly supported, using 24/32-bit format"); + AudioFormat::S2432le + } + VIRTIO_SND_PCM_FMT_U20 => { + log::warn!("20-bit format not directly supported, using 24/32-bit format"); + AudioFormat::U2432le + } + VIRTIO_SND_PCM_FMT_S24 => AudioFormat::S2432le, + VIRTIO_SND_PCM_FMT_U24 => AudioFormat::U2432le, + VIRTIO_SND_PCM_FMT_S32 => AudioFormat::S32le, + VIRTIO_SND_PCM_FMT_U32 => AudioFormat::U32le, + VIRTIO_SND_PCM_FMT_FLOAT => AudioFormat::F32le, + VIRTIO_SND_PCM_FMT_FLOAT64 => AudioFormat::F64le, + _ => AudioFormat::Unknown, + }; + Ok(format) + } + + #[cfg(target_endian = "big")] + pub fn set_format(&self, params: &PcmParams) -> Result { + let format = match params.format { + VIRTIO_SND_PCM_FMT_MU_LAW => AudioFormat::Encoded, + VIRTIO_SND_PCM_FMT_A_LAW => AudioFormat::Encoded, + VIRTIO_SND_PCM_FMT_S8 => AudioFormat::S8, + VIRTIO_SND_PCM_FMT_U8 => AudioFormat::U8, + VIRTIO_SND_PCM_FMT_S16 => AudioFormat::S16le, + VIRTIO_SND_PCM_FMT_U16 => AudioFormat::U16le, + VIRTIO_SND_PCM_FMT_S18_3 => AudioFormat::S18le, + VIRTIO_SND_PCM_FMT_U18_3 => AudioFormat::U18le, + VIRTIO_SND_PCM_FMT_S20_3 => AudioFormat::S20le, + VIRTIO_SND_PCM_FMT_U20_3 => AudioFormat::U20le, + VIRTIO_SND_PCM_FMT_S24_3 => AudioFormat::S24le, + VIRTIO_SND_PCM_FMT_U24_3 => AudioFormat::U24le, + VIRTIO_SND_PCM_FMT_S20 => { + log::warn!("20-bit format not directly supported, using 24/32-bit format"); + AudioFormat::S2432be + } + VIRTIO_SND_PCM_FMT_U20 => { + log::warn!("20-bit format not directly supported, using 24/32-bit format"); + AudioFormat::U2432be + } + VIRTIO_SND_PCM_FMT_S24 => AudioFormat::S2432be, + VIRTIO_SND_PCM_FMT_U24 => AudioFormat::U2432be, + VIRTIO_SND_PCM_FMT_S32 => AudioFormat::S32be, + VIRTIO_SND_PCM_FMT_U32 => AudioFormat::U32be, + VIRTIO_SND_PCM_FMT_FLOAT => AudioFormat::F32be, + VIRTIO_SND_PCM_FMT_FLOAT64 => AudioFormat::F64be, + _ => AudioFormat::Unknown, + }; + + Ok(format) + } + + pub fn create_caps(&self, params: &PcmParams) -> Result { + let channels = u32::from(params.channels); + + let format = self.set_format(params).map_err(|e| { + log::error!("Failed to set audio format: {e}"); + Error::UnexpectedAudioBackendConfiguration + })?; + + let rate = match params.rate { + VIRTIO_SND_PCM_RATE_5512 => 5512, + VIRTIO_SND_PCM_RATE_8000 => 8000, + VIRTIO_SND_PCM_RATE_11025 => 11025, + VIRTIO_SND_PCM_RATE_16000 => 16000, + VIRTIO_SND_PCM_RATE_22050 => 22050, + VIRTIO_SND_PCM_RATE_32000 => 32000, + VIRTIO_SND_PCM_RATE_44100 => 44100, + VIRTIO_SND_PCM_RATE_48000 => 48000, + VIRTIO_SND_PCM_RATE_64000 => 64000, + VIRTIO_SND_PCM_RATE_88200 => 88200, + VIRTIO_SND_PCM_RATE_96000 => 96000, + VIRTIO_SND_PCM_RATE_176400 => 176400, + VIRTIO_SND_PCM_RATE_192000 => 192000, + VIRTIO_SND_PCM_RATE_384000 => 384000, + VIRTIO_SND_PCM_RATE_12000 => 12000, + VIRTIO_SND_PCM_RATE_24000 => 24000, + _ => 44100, + }; + + log::debug!("Creating caps for PCM stream: {format} {rate} {channels}"); + + let caps = match params.format { + VIRTIO_SND_PCM_FMT_MU_LAW => gst::Caps::builder("audio/x-mulaw") + .field("rate", rate) + .field("channels", channels) + .build(), + VIRTIO_SND_PCM_FMT_A_LAW => gst::Caps::builder("audio/x-alaw") + .field("rate", rate) + .field("channels", channels) + .build(), + _ => { + let audio_info = + AudioInfo::builder(format, rate, channels) + .build() + .map_err(|e| { + log::error!("Failed to create AudioInfo: {e}"); + Error::UnexpectedAudioBackendConfiguration + })?; + + audio_info.to_caps().map_err(|e| { + log::error!("Failed to create caps from AudioInfo: {e}"); + Error::UnexpectedAudioBackendConfiguration + })? + } + }; + + Ok(caps) + } +} + +impl AudioBackend for GStreamerBackend { + fn write(&self, stream_id: u32) -> Result<()> { + if stream_id >= self.stream_params.read().unwrap().len() as u32 { + log::error!( + "Received DoWork action for stream id {} but there are only {} PCM streams.", + stream_id, + self.stream_params.read().unwrap().len() + ); + return Err(Error::StreamWithIdNotFound(stream_id)); + } + if !matches!( + self.stream_params.read().unwrap()[stream_id as usize].state, + PCMState::Start | PCMState::Prepare + ) { + return Err(Error::Stream(crate::stream::Error::InvalidState( + "write", + self.stream_params.read().unwrap()[stream_id as usize].state, + ))); + } + Ok(()) + } + + fn read(&self, stream_id: u32) -> Result<()> { + if !matches!( + self.stream_params.read().unwrap()[stream_id as usize].state, + PCMState::Start | PCMState::Prepare + ) { + return Err(Error::Stream(crate::stream::Error::InvalidState( + "read", + self.stream_params.read().unwrap()[stream_id as usize].state, + ))); + } + Ok(()) + } + + fn set_parameters(&self, stream_id: u32, request: VirtioSndPcmSetParams) -> Result<()> { + log::debug!("Setting parameters for stream {stream_id}"); + + let stream_clone = self.stream_params.clone(); + let mut stream_params = stream_clone.write().unwrap(); + if let Some(st) = stream_params.get_mut(stream_id as usize) { + if let Err(err) = st.state.set_parameters() { + log::error!("Stream {stream_id} set_parameters {err}"); + return Err(Error::Stream(err)); + } else if !st.supports_format(request.format) || !st.supports_rate(request.rate) { + return Err(Error::UnexpectedAudioBackendConfiguration); + } else { + st.params.features = request.features; + st.params.buffer_bytes = request.buffer_bytes; + st.params.period_bytes = request.period_bytes; + st.params.channels = request.channels; + st.params.format = request.format; + st.params.rate = request.rate; + } + } else { + return Err(Error::StreamWithIdNotFound(stream_id)); + } + log::debug!("Stream parameters after set: {stream_params:?}"); + + Ok(()) + } + + fn prepare(&self, stream_id: u32) -> Result<()> { + log::debug!("Preparing stream {stream_id}"); + self.stream_params + .write() + .unwrap() + .get_mut(stream_id as usize) + .ok_or(Error::StreamWithIdNotFound(stream_id))? + .state + .prepare() + .inspect_err(|err| log::error!("Stream {stream_id} prepare {err}")) + .map_err(Error::Stream)?; + + log::debug!("Stream {stream_id} prepared successfully"); + let stream_params = self.stream_params.read().unwrap(); + let params = &stream_params[stream_id as usize].params; + let mut stream_in = self.stream_in.write().unwrap(); + let mut stream_out = self.stream_out.write().unwrap(); + + if let Some(stream) = stream_in.remove(&stream_id) { + if let Err(err) = stream.pipeline.set_state(gst::State::Null) { + log::error!("Failed to set pipeline to Null state: {err}"); + return Err(Error::Stream(StreamError::CouldNotDisconnectStream)); + } + } + + if let Some(stream) = stream_out.remove(&stream_id) { + if let Err(err) = stream.pipeline.set_state(gst::State::Null) { + log::error!("Failed to set pipeline to Null state: {err}"); + return Err(Error::Stream(StreamError::CouldNotDisconnectStream)); + } + } + + let caps = self.create_caps(params)?; + + let streams = self.stream_params.clone(); + + let direction = stream_params[stream_id as usize].direction; + + if direction == Direction::Input { + let pipeline_in = GStreamerBackendIn::new(&caps, stream_id, streams)?; + stream_in.insert(stream_id, pipeline_in); + } else if direction == Direction::Output { + let pipeline_out = GStreamerBackendOut::new(&caps, stream_id, streams)?; + stream_out.insert(stream_id, pipeline_out); + } + Ok(()) + } + + fn release(&self, stream_id: u32) -> Result<()> { + log::debug!("Releasing stream {stream_id}"); + self.stream_params + .write() + .unwrap() + .get_mut(stream_id as usize) + .ok_or(Error::StreamWithIdNotFound(stream_id))? + .state + .release() + .inspect_err(|err| log::error!("Stream {stream_id} release {err}")) + .map_err(Error::Stream)?; + + let stream = &mut self.stream_params.write().unwrap(); + let direction = stream[stream_id as usize].direction; + + if direction == Direction::Input { + let mut stream_in = self.stream_in.write().unwrap(); + let pipeline_in = stream_in + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_in.pipeline.set_state(gst::State::Null) { + log::error!("Failed to set pipeline in to Null state: {err}"); + return Err(Error::Stream(StreamError::CouldNotDisconnectStream)); + } + stream_in.remove(&stream_id); + } else if direction == Direction::Output { + let mut stream_out = self.stream_out.write().unwrap(); + let pipeline_out = stream_out + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_out.pipeline.set_state(gst::State::Null) { + log::error!("Failed to set pipeline out to Null state: {err}"); + return Err(Error::Stream(StreamError::CouldNotDisconnectStream)); + } + stream_out.remove(&stream_id); + } + + // clear requests for the stream + std::mem::take(&mut stream[stream_id as usize].requests); + + Ok(()) + } + + fn start(&self, stream_id: u32) -> Result<()> { + log::debug!("Starting stream {stream_id}"); + self.stream_params + .write() + .unwrap() + .get_mut(stream_id as usize) + .ok_or(Error::StreamWithIdNotFound(stream_id))? + .state + .start() + .inspect_err(|err| log::error!("Stream {stream_id} start {err}")) + .map_err(Error::Stream)?; + + let stream = self.stream_params.read().unwrap(); + let direction = stream[stream_id as usize].direction; + + if direction == Direction::Input { + let stream_in = self.stream_in.read().unwrap(); + let pipeline_in = stream_in + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_in.pipeline.set_state(gst::State::Playing) { + log::error!("Failed to set pipeline in to Playing state: {err}"); + return Err(Error::Stream(StreamError::CouldNotStartStream)); + } + } else if direction == Direction::Output { + let stream_out = self.stream_out.read().unwrap(); + let pipeline_out = stream_out + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_out.pipeline.set_state(gst::State::Playing) { + log::error!("Failed to set pipeline out to Playing state: {err}"); + return Err(Error::Stream(StreamError::CouldNotStartStream)); + } + } + + Ok(()) + } + + fn stop(&self, stream_id: u32) -> Result<()> { + log::debug!("Stopping stream {stream_id}"); + self.stream_params + .write() + .unwrap() + .get_mut(stream_id as usize) + .ok_or(Error::StreamWithIdNotFound(stream_id))? + .state + .stop() + .inspect_err(|err| log::error!("Stream {stream_id} start {err}")) + .map_err(Error::Stream)?; + + let stream = self.stream_params.read().unwrap(); + let direction = stream[stream_id as usize].direction; + + if direction == Direction::Input { + let stream_in = self.stream_in.read().unwrap(); + let pipeline_in = stream_in + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_in.pipeline.set_state(gst::State::Paused) { + log::error!("Failed to set pipeline in to Paused state: {err}"); + return Err(Error::Stream(StreamError::CouldNotStopStream)); + } + } else if direction == Direction::Output { + let stream_out = self.stream_out.read().unwrap(); + let pipeline_out = stream_out + .get(&stream_id) + .ok_or(Error::StreamWithIdNotFound(stream_id))?; + if let Err(err) = pipeline_out.pipeline.set_state(gst::State::Paused) { + log::error!("Failed to set pipeline out to Paused state: {err}"); + return Err(Error::Stream(StreamError::CouldNotStopStream)); + } + } + Ok(()) + } + + #[cfg(test)] + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(test)] +pub mod test_utils; + +#[cfg(test)] +mod tests { + use rusty_fork::rusty_fork_test; + + use super::{test_utils::GStreamerTestHarness, *}; + use crate::{stream::Stream, virtio_sound::*}; + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_backend_success() { + crate::init_logger(); + let stream = Stream { + direction: Direction::Input, + ..Default::default() + }; + + let stream_params = Arc::new(RwLock::new(vec![stream])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test input stream lifecycle + let request = VirtioSndPcmSetParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + gst_backend.set_parameters(0, request).unwrap(); + gst_backend.prepare(0).unwrap(); + gst_backend.start(0).unwrap(); + gst_backend.write(0).unwrap(); + gst_backend.read(0).unwrap(); + gst_backend.stop(0).unwrap(); + gst_backend.release(0).unwrap(); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_backend_invalid_stream_id() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + let result = gst_backend.write(1); + assert!(result.is_err()); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_invalid_stream() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + let request = VirtioSndPcmSetParams::default(); + let res = gst_backend.set_parameters(0, request); + assert_eq!( + res.unwrap_err().to_string(), + Error::StreamWithIdNotFound(0).to_string() + ); + + for res in [ + gst_backend.prepare(0), + gst_backend.start(0), + gst_backend.stop(0), + ] { + assert_eq!( + res.unwrap_err().to_string(), + Error::StreamWithIdNotFound(0).to_string() + ); + } + + let res = gst_backend.release(0); + assert_eq!( + res.unwrap_err().to_string(), + Error::StreamWithIdNotFound(0).to_string() + ); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_invalid_state_transitions() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test invalid state transitions + assert!(gst_backend.start(0).is_err()); + assert!(gst_backend.stop(0).is_err()); + assert!(gst_backend.release(0).is_err()); + + let request = VirtioSndPcmSetParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + gst_backend.set_parameters(0, request).unwrap(); + gst_backend.prepare(0).unwrap(); + + assert!(gst_backend.stop(0).is_err()); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_invalid_parameters() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + let unsupported_request = VirtioSndPcmSetParams { + format: VIRTIO_SND_PCM_FMT_IMA_ADPCM, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + assert!(gst_backend.set_parameters(0, unsupported_request).is_err()); + + // Test unsupported rate + let unsupported_rate_request = VirtioSndPcmSetParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_5512, + channels: 2, + ..Default::default() + }; + assert!(gst_backend.set_parameters(0, unsupported_rate_request).is_err()); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_mu_law_a_law_caps() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test μ-law format + let mu_law_params = PcmParams { + format: VIRTIO_SND_PCM_FMT_MU_LAW, + rate: VIRTIO_SND_PCM_RATE_8000, + channels: 1, + ..Default::default() + }; + let caps = gst_backend.create_caps(&mu_law_params).unwrap(); + let caps_str = caps.to_string(); + assert!(caps_str.contains("audio/x-mulaw")); + + // Test A-law format + let a_law_params = PcmParams { + format: VIRTIO_SND_PCM_FMT_A_LAW, + rate: VIRTIO_SND_PCM_RATE_8000, + channels: 1, + ..Default::default() + }; + let caps = gst_backend.create_caps(&a_law_params).unwrap(); + let caps_str = caps.to_string(); + assert!(caps_str.contains("audio/x-alaw")); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_caps_creation() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test different audio formats + let test_cases = vec![ + (VIRTIO_SND_PCM_FMT_S16, VIRTIO_SND_PCM_RATE_44100, 2), + (VIRTIO_SND_PCM_FMT_S32, VIRTIO_SND_PCM_RATE_48000, 1), + (VIRTIO_SND_PCM_FMT_FLOAT, VIRTIO_SND_PCM_RATE_96000, 6), + (VIRTIO_SND_PCM_FMT_S24, VIRTIO_SND_PCM_RATE_192000, 4), + ]; + + for (format, rate, channels) in test_cases { + let params = PcmParams { + format, + rate, + channels, + ..Default::default() + }; + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + + // Verify caps structure + let structure = caps.structure(0).unwrap(); + assert!(structure.has_field("channels")); + assert!(structure.has_field("rate")); + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_all_supported_formats() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test all supported audio formats + let formats = vec![ + VIRTIO_SND_PCM_FMT_S8, + VIRTIO_SND_PCM_FMT_U8, + VIRTIO_SND_PCM_FMT_S16, + VIRTIO_SND_PCM_FMT_U16, + VIRTIO_SND_PCM_FMT_S18_3, + VIRTIO_SND_PCM_FMT_U18_3, + VIRTIO_SND_PCM_FMT_S20_3, + VIRTIO_SND_PCM_FMT_U20_3, + VIRTIO_SND_PCM_FMT_S24_3, + VIRTIO_SND_PCM_FMT_U24_3, + VIRTIO_SND_PCM_FMT_S20, + VIRTIO_SND_PCM_FMT_U20, + VIRTIO_SND_PCM_FMT_S24, + VIRTIO_SND_PCM_FMT_U24, + VIRTIO_SND_PCM_FMT_S32, + VIRTIO_SND_PCM_FMT_U32, + VIRTIO_SND_PCM_FMT_FLOAT, + VIRTIO_SND_PCM_FMT_FLOAT64, + ]; + + for format in formats { + let params = PcmParams { + format, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_ne!(audio_format, AudioFormat::Unknown); + + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_all_supported_rates() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test all supported sample rates + let rates = vec![ + VIRTIO_SND_PCM_RATE_5512, + VIRTIO_SND_PCM_RATE_8000, + VIRTIO_SND_PCM_RATE_11025, + VIRTIO_SND_PCM_RATE_12000, + VIRTIO_SND_PCM_RATE_16000, + VIRTIO_SND_PCM_RATE_22050, + VIRTIO_SND_PCM_RATE_24000, + VIRTIO_SND_PCM_RATE_32000, + VIRTIO_SND_PCM_RATE_44100, + VIRTIO_SND_PCM_RATE_48000, + VIRTIO_SND_PCM_RATE_64000, + VIRTIO_SND_PCM_RATE_88200, + VIRTIO_SND_PCM_RATE_96000, + VIRTIO_SND_PCM_RATE_176400, + VIRTIO_SND_PCM_RATE_192000, + VIRTIO_SND_PCM_RATE_384000, + ]; + + for rate in rates { + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate, + channels: 2, + ..Default::default() + }; + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + + let structure = caps.structure(0).unwrap(); + assert!(structure.has_field("rate")); + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_unknown_format() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test unknown format (using invalid format value) + let params = PcmParams { + format: 255, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_eq!(audio_format, AudioFormat::Unknown); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_unknown_rate() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: 255, + channels: 2, + ..Default::default() + }; + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + + let structure = caps.structure(0).unwrap(); + let rate: i32 = structure.get("rate").unwrap(); + assert_eq!(rate, 44100); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_multiple_prepare_release_cycles() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + let request = VirtioSndPcmSetParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + + // Test multiple prepare/release cycles + for _ in 0..3 { + gst_backend.set_parameters(0, request).unwrap(); + gst_backend.prepare(0).unwrap(); + gst_backend.release(0).unwrap(); + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_different_channel_counts() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test different channel counts + let channel_counts = vec![1, 2, 4, 6, 8]; + + for channels in channel_counts { + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels, + ..Default::default() + }; + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + + let structure = caps.structure(0).unwrap(); + let caps_channels: i32 = structure.get("channels").unwrap(); + assert_eq!(caps_channels, i32::from(channels)) + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + rusty_fork_test! { + #[test] + fn test_gstreamer_all_3byte_formats() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test all 3-byte formats + let formats_3byte = vec![ + VIRTIO_SND_PCM_FMT_S18_3, + VIRTIO_SND_PCM_FMT_U18_3, + VIRTIO_SND_PCM_FMT_S20_3, + VIRTIO_SND_PCM_FMT_U20_3, + VIRTIO_SND_PCM_FMT_S24_3, + VIRTIO_SND_PCM_FMT_U24_3, + ]; + + for format in formats_3byte { + let params = PcmParams { + format, + rate: VIRTIO_SND_PCM_RATE_48000, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_ne!(audio_format, AudioFormat::Unknown); + + let caps = gst_backend.create_caps(¶ms).unwrap(); + assert!(!caps.is_empty()); + } + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + #[cfg(target_endian = "little")] + rusty_fork_test! { + #[test] + fn test_gstreamer_little_endian_formats() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test little endian specific format mapping + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_eq!(audio_format, AudioFormat::S16le); + + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_FLOAT, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_eq!(audio_format, AudioFormat::F32le); + } + } + + // `GStreamerTestHarness` modifies the process's environment, so this test should + // be executed on a forked process. + #[cfg(target_endian = "big")] + rusty_fork_test! { + #[test] + fn test_gstreamer_big_endian_formats() { + crate::init_logger(); + let stream_params = Arc::new(RwLock::new(vec![Stream::default()])); + + let _test_harness = GStreamerTestHarness::new(); + + let gst_backend = GStreamerBackend::new(stream_params).unwrap(); + + // Test big endian specific format mapping + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_eq!(audio_format, AudioFormat::S16be); + + let params = PcmParams { + format: VIRTIO_SND_PCM_FMT_FLOAT, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + ..Default::default() + }; + let audio_format = gst_backend.set_format(¶ms).unwrap(); + assert_eq!(audio_format, AudioFormat::F32be); + } + } +} diff --git a/vhost-device-sound/src/audio_backends/gstreamer/test_utils.rs b/vhost-device-sound/src/audio_backends/gstreamer/test_utils.rs new file mode 100644 index 000000000..3e9f728c5 --- /dev/null +++ b/vhost-device-sound/src/audio_backends/gstreamer/test_utils.rs @@ -0,0 +1,68 @@ +use std::env; + +use tempfile::TempDir; + +/// A test harness that sets up an isolated environment for GStreamer tests. +/// Unlike PipeWire, GStreamer is a library and doesn't need a daemon. +/// We only need to isolate runtime dirs and registry files. +pub struct GStreamerTestHarness { + _runtime_dir: TempDir, + old_runtime_dir: Option, + old_gst_registry: Option, + old_gst_debug: Option, +} + +impl GStreamerTestHarness { + /// Create a new isolated GStreamer test environment. + pub fn new() -> Self { + let tmpdir = tempfile::tempdir().expect("Failed to create temp dir for GStreamer"); + + let old_runtime_dir = env::var("XDG_RUNTIME_DIR").ok(); + let old_gst_registry = env::var("GST_REGISTRY").ok(); + let old_gst_debug = env::var("GST_DEBUG").ok(); + + log::debug!("old_runtime_dir: {:?}", old_runtime_dir); + log::debug!("old_gst_registry: {:?}", old_gst_registry); + log::debug!("old_gst_debug: {:?}", old_gst_debug); + + env::set_var("XDG_RUNTIME_DIR", tmpdir.path()); + env::set_var("GST_REGISTRY", tmpdir.path().join("gst-registry.bin")); + env::set_var("GST_DEBUG", "ERROR"); + + log::debug!( + "Started isolated GStreamer test environment at {:?}", + tmpdir.path() + ); + + Self { + _runtime_dir: tmpdir, + old_runtime_dir, + old_gst_registry, + old_gst_debug, + } + } +} + +impl Drop for GStreamerTestHarness { + fn drop(&mut self) { + if let Some(val) = &self.old_runtime_dir { + env::set_var("XDG_RUNTIME_DIR", val); + } else { + env::remove_var("XDG_RUNTIME_DIR"); + } + + if let Some(val) = &self.old_gst_registry { + env::set_var("GST_REGISTRY", val); + } else { + env::remove_var("GST_REGISTRY"); + } + + if let Some(val) = &self.old_gst_debug { + env::set_var("GST_DEBUG", val); + } else { + env::remove_var("GST_DEBUG"); + } + + log::debug!("Isolated GStreamer environment cleaned up"); + } +} diff --git a/vhost-device-sound/src/main.rs b/vhost-device-sound/src/main.rs index 283f80204..330e7cacf 100644 --- a/vhost-device-sound/src/main.rs +++ b/vhost-device-sound/src/main.rs @@ -55,6 +55,10 @@ mod tests { all(feature = "alsa-backend", target_env = "gnu"), case::alsa("alsa", BackendType::Alsa) )] + #[cfg_attr( + all(feature = "gst-backend", target_env = "gnu"), + case::gstreamer("gstreamer", BackendType::GStreamer) + )] fn test_cli_backend_arg(#[case] backend_name: &str, #[case] backend: BackendType) { let args: SoundArgs = Parser::parse_from([ "", diff --git a/vhost-device-sound/src/stream.rs b/vhost-device-sound/src/stream.rs index 3b59032ae..a97ad5949 100644 --- a/vhost-device-sound/src/stream.rs +++ b/vhost-device-sound/src/stream.rs @@ -29,6 +29,12 @@ pub enum Error { DescriptorWriteFailed, #[error("Could not disconnect stream")] CouldNotDisconnectStream, + #[cfg(feature = "gst-backend")] + #[error("Could not start stream")] + CouldNotStartStream, + #[cfg(feature = "gst-backend")] + #[error("Could not stop stream")] + CouldNotStopStream, } type Result = std::result::Result; @@ -323,7 +329,8 @@ impl Request { } #[inline] - /// Returns the length of the sound data [`virtio_queue::desc::RawDescriptor`]. + /// Returns the length of the sound data + /// [`virtio_queue::desc::RawDescriptor`]. pub const fn len(&self) -> usize { self.len }