diff --git a/Cargo.lock b/Cargo.lock index 43c0bcc..24dcc2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,15 +20,15 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.17.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" +checksum = "e25ae84c0260bdf5df07796d7cc4882460de26a2b406ec0e6c42461a723b271b" [[package]] name = "accesskit_atspi_common" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" +checksum = "29bd41de2e54451a8ca0dd95ebf45b54d349d29ebceb7f20be264eee14e3d477" dependencies = [ "accesskit", "accesskit_consumer", @@ -40,20 +40,19 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" +checksum = "8bfae7c152994a31dc7d99b8eeac7784a919f71d1b306f4b83217e110fd3824c" dependencies = [ "accesskit", "hashbrown 0.15.2", - "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.18.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" +checksum = "692dd318ff8a7a0ffda67271c4bd10cf32249656f4e49390db0b26ca92b095f2" dependencies = [ "accesskit", "accesskit_consumer", @@ -65,9 +64,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" +checksum = "c5f7474c36606d0fe4f438291d667bae7042ea2760f506650ad2366926358fc8" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -83,24 +82,23 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.24.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" +checksum = "70a042b62c9c05bf7b616f015515c17d2813f3ba89978d6f4fc369735d60700a" dependencies = [ "accesskit", "accesskit_consumer", "hashbrown 0.15.2", - "paste", "static_assertions", - "windows 0.58.0", - "windows-core 0.58.0", + "windows 0.61.1", + "windows-core 0.61.0", ] [[package]] name = "accesskit_winit" -version = "0.23.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" +checksum = "5c1f0d3d13113d8857542a4f8d1a1c24d1dc1527b77aee8426127f4901588708" dependencies = [ "accesskit", "accesskit_macos", @@ -201,6 +199,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "android-build" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9994787facbc3375d2b510024117d11fa98087be537ac878033892193bbb33d2" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "android-properties" version = "0.2.2" @@ -363,6 +370,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -441,6 +466,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -543,9 +579,9 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "atspi" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" dependencies = [ "atspi-common", "atspi-connection", @@ -554,9 +590,9 @@ dependencies = [ [[package]] name = "atspi-common" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" dependencies = [ "enumflags2", "serde", @@ -570,9 +606,9 @@ dependencies = [ [[package]] name = "atspi-connection" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" dependencies = [ "atspi-common", "atspi-proxies", @@ -582,14 +618,13 @@ dependencies = [ [[package]] name = "atspi-proxies" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" dependencies = [ "atspi-common", "serde", "zbus", - "zvariant", ] [[package]] @@ -722,21 +757,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.5.1" @@ -1044,36 +1064,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "codemap" version = "0.1.3" @@ -1132,14 +1122,10 @@ dependencies = [ "gstreamer-rtsp", "gstreamer-rtsp-server", "gstreamer-video", - "http 0.1.0", "ipnetwork", "log", - "m3u8-rs", "mdns-sd", "pnet_datalink", - "quickcheck", - "quickcheck_macros", "slint", "tokio", "tokio-stream", @@ -1220,9 +1206,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1254,25 +1240,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.0", - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-graphics-types 0.2.0", "foreign-types", "libc", ] -[[package]] -name = "core-graphics-helmer-fork" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics-types" version = "0.1.3" @@ -1291,18 +1264,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.0", - "core-foundation 0.10.0", + "core-foundation 0.10.1", "libc", ] [[package]] name = "core-text" -version = "20.1.0" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" dependencies = [ - "core-foundation 0.9.4", - "core-graphics 0.23.2", + "core-foundation 0.10.1", + "core-graphics 0.24.0", "foreign-types", "libc", ] @@ -1372,15 +1345,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.2" @@ -1436,32 +1400,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "ctor-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" -[[package]] -name = "ctrlc" -version = "3.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" -dependencies = [ - "nix 0.29.0", - "windows-sys 0.59.0", -] - [[package]] name = "cursor-icon" version = "1.1.0" @@ -1474,17 +1418,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" -[[package]] -name = "dbus" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" -dependencies = [ - "libc", - "libdbus-sys", - "winapi", -] - [[package]] name = "derive_more" version = "2.0.1" @@ -1517,16 +1450,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -1672,16 +1595,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "log", - "regex", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -1790,9 +1703,9 @@ dependencies = [ [[package]] name = "femtovg" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9bec3fb78abd18f7bbbde01f22f467c47c5a9c043e791802f82da0cf16066d1" +checksum = "ffdf29af13b81d7562ccd57624242b2ed56edc2c83dad25f13bd74d87e7fa8d9" dependencies = [ "bitflags 2.9.0", "bytemuck", @@ -2125,16 +2038,6 @@ dependencies = [ "system-deps 6.2.2", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -2466,16 +2369,6 @@ dependencies = [ "system-deps 7.0.3", ] -[[package]] -name = "gst-plugin-version-helper" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5e874f1660252fd2ec81c602066df3633b3a6fcbe2b196f7f93c27cf069b2a" -dependencies = [ - "chrono", - "toml_edit 0.22.25", -] - [[package]] name = "gstreamer" version = "0.23.5" @@ -2958,38 +2851,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "http" -version = "0.1.0" -dependencies = [ - "ureq", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - [[package]] name = "i-slint-backend-android-activity" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41362c0585c71180f42a2f3f9c072b9b6d0e96875d981e5d2260febf8af4a128" +checksum = "d5deff9cd4f5cd38d8df7e78e8f54ba436fb4d6e398573b61c51b6fe52f7f67d" dependencies = [ "android-activity 0.5.2", "android-activity 0.6.0", + "android-build", "i-slint-core", "i-slint-renderer-skia", "jni", @@ -2999,9 +2869,9 @@ dependencies = [ [[package]] name = "i-slint-backend-linuxkms" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c3e06854468e614d43e747bbc8d3ce9a44eec6a9e8312ef16d4890da97fcc8" +checksum = "836f600e0d24264f927d0b868c42a30fbd6c1a16bd807a51e7132aba5ef5bca6" dependencies = [ "bytemuck", "calloop 0.14.2", @@ -3013,16 +2883,16 @@ dependencies = [ "i-slint-renderer-femtovg", "input", "memmap2", - "nix 0.29.0", + "nix 0.30.1", "raw-window-handle", "xkbcommon", ] [[package]] name = "i-slint-backend-qt" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "354fcbbc568076e6ba1571fc60ef1d1a275a01126435531bba321b4ace1c2cd0" +checksum = "0482606395bc733e485ecd24c84c1073df7f61a8cc626d0dbf604ed5c575e732" dependencies = [ "const-field-offset", "cpp", @@ -3039,9 +2909,9 @@ dependencies = [ [[package]] name = "i-slint-backend-selector" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65058bf40640e94529bb27c4121e237d2565aa1883721806caeaa1509cc326a3" +checksum = "891b24f1323ea2f8791667b960620532d95a76fd9008ac67d2826e1153889978" dependencies = [ "cfg-if", "i-slint-backend-linuxkms", @@ -3054,9 +2924,9 @@ dependencies = [ [[package]] name = "i-slint-backend-winit" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249a3416f38884de18d0bb0e1589cd95ea10b1b2b8f7ea38e06e7aac320f9edf" +checksum = "f2b87c1fb7bcdd6cf0c93dfd2de2c33b0c21eca625ec8aafee714edac5bd9f2b" dependencies = [ "accesskit", "accesskit_winit", @@ -3092,9 +2962,9 @@ dependencies = [ [[package]] name = "i-slint-common" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c5f2411fd0cd44b39f6aff036c6fbcbcc3eecd119a90d5908a90075d256664" +checksum = "748b5c0e292b2263fbd5b2855df207f4f6e068382668c55526037ac0ff5ba038" dependencies = [ "cfg-if", "derive_more", @@ -3105,9 +2975,9 @@ dependencies = [ [[package]] name = "i-slint-compiler" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becea2773938b8299809cb1f1f0350295bc98e105fe175c53686ba063d7b2b9d" +checksum = "dae46edba3b6875f66765d4074ecf4d65b8cbf1e7a0f2b4581e23e72bd8f1157" dependencies = [ "by_address", "codemap", @@ -3135,9 +3005,9 @@ dependencies = [ [[package]] name = "i-slint-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "846dca8f8ef03821df932755381d1a29c5be3981eb269c5b235aba5012665a5a" +checksum = "13599d9d81dba95796c9bf7c9f9f179bf955cdf240f886a442eb460f57b17ba1" dependencies = [ "auto_enums", "bitflags 2.9.0", @@ -3183,9 +3053,9 @@ dependencies = [ [[package]] name = "i-slint-core-macros" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31573d7b7000d377ef6b64621c22a72ac39213899bd0569c05d748e6cb994aee" +checksum = "2633fb6f59e2184db69c0b3323ad8c324b169aa8d24999a67f2b5cc2c2324b8e" dependencies = [ "quote", "serde_json", @@ -3194,13 +3064,13 @@ dependencies = [ [[package]] name = "i-slint-renderer-femtovg" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "620dd18c88c30e2423831a65be881a4a557b5b84d1700fbdff719849b36430d4" +checksum = "e63ad7c680b0d205a2bc19f3974fcb9bc567ab786dbc34553e30aa8b1dffb9f9" dependencies = [ "cfg-if", "const-field-offset", - "core-foundation 0.9.4", + "core-foundation 0.10.1", "core-text", "derive_more", "dwrote", @@ -3224,9 +3094,9 @@ dependencies = [ [[package]] name = "i-slint-renderer-skia" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0115066b82729a225d28d015318f33ad2ae09f32942cb5046544a7f0cefdee3" +checksum = "e63a3aa935a803f1e59e1fb04bc49f93bc23a0612cd49afa7dc9533613e992aa" dependencies = [ "bytemuck", "cfg-if", @@ -3474,15 +3344,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" version = "2.9.0" @@ -3725,15 +3586,6 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "libdbus-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" -dependencies = [ - "pkg-config", -] - [[package]] name = "libfuzzer-sys" version = "0.4.9" @@ -3958,25 +3810,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "m3u8-rs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03cd3335fb5f2447755d45cda9c70f76013626a9db44374973791b0926a86c3" -dependencies = [ - "chrono", - "nom", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -4161,9 +3994,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -4194,15 +4027,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -4275,27 +4099,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -4629,24 +4432,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.7" @@ -4734,29 +4519,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.11", - "smallvec", - "windows-targets 0.52.6", -] - [[package]] name = "paste" version = "1.0.15" @@ -5077,38 +4839,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", - "serde", ] [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", + "serde", ] [[package]] -name = "quickcheck" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" -dependencies = [ - "env_logger 0.8.4", - "log", - "rand 0.8.5", -] - -[[package]] -name = "quickcheck_macros" -version = "1.0.0" +name = "quick-xml" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "memchr", ] [[package]] @@ -5280,7 +5029,7 @@ dependencies = [ "anyhow", "clap", "common", - "env_logger 0.11.8", + "env_logger", "fcast-lib", "futures", "gethostname 1.0.1", @@ -5479,42 +5228,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scap" -version = "0.0.8" -dependencies = [ - "anyhow", - "cocoa", - "core-graphics-helmer-fork", - "dbus", - "libc", - "log", - "objc", - "pipewire", - "rand 0.9.1", - "screencapturekit", - "screencapturekit-sys", - "sysinfo", - "tao-core-video-sys", - "windows 0.58.0", - "windows-capture", - "x11", - "xcb", -] - -[[package]] -name = "scap-gstreamer" -version = "0.1.0" -dependencies = [ - "crossbeam-channel", - "ctrlc", - "gst-plugin-version-helper", - "gstreamer", - "gstreamer-base", - "gstreamer-video", - "scap", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -5533,29 +5246,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "screencapturekit" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" -dependencies = [ - "screencapturekit-sys", -] - -[[package]] -name = "screencapturekit-sys" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" -dependencies = [ - "block", - "dispatch", - "objc", - "objc-foundation", - "objc_id", - "once_cell", -] - [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -5580,17 +5270,21 @@ name = "sender" version = "0.1.0" dependencies = [ "anyhow", + "ashpd", "common", "crossbeam-channel", - "env_logger 0.11.8", + "env_logger", "fcast-lib", "flume", "gstreamer", "log", "mdns-sd", - "scap-gstreamer", + "pipewire", "slint", "slint-build", + "tokio", + "x11", + "xcb", ] [[package]] @@ -5645,17 +5339,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -5703,9 +5386,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skia-bindings" -version = "0.84.0" +version = "0.86.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b612a544c0cc0da82271eac1c40b6b055fe3c5aa20bb7b3922f830c777d9aff0" +checksum = "a2bf215f640b53293844d441e93448b437ca4937595f60e3317fbb03d7ac6783" dependencies = [ "bindgen 0.71.1", "cc", @@ -5720,9 +5403,9 @@ dependencies = [ [[package]] name = "skia-safe" -version = "0.84.0" +version = "0.86.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2629d473f8bdbe35fc227d80d8efe9a7db538a409be8beb19e5cd3153d10b0ef" +checksum = "e372258f52414e04de007326fa497581617c9fa872a3225dca5e42212723c426" dependencies = [ "bitflags 2.9.0", "lazy_static", @@ -5741,9 +5424,9 @@ dependencies = [ [[package]] name = "slint" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6197c948ea3b1ae0c1a42a746ad84539135fb11e5c66094d98a222154edd51" +checksum = "fad0f31b974d0d69db787cc4f8b2965806f4991b0b47bfcb40538ae714fd9448" dependencies = [ "const-field-offset", "i-slint-backend-android-activity", @@ -5762,9 +5445,9 @@ dependencies = [ [[package]] name = "slint-build" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ad3d6c8d620bfbb474ee301ebf99ba9dab3d2f86c0f8d75e52d403bb08acd6" +checksum = "cb7076e05474941b35df7e2d03c393a3c3b4e4855dd93f0280451002df07e474" dependencies = [ "derive_more", "i-slint-compiler", @@ -5774,9 +5457,9 @@ dependencies = [ [[package]] name = "slint-macros" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63ac3ed6d216651bb679d5905dafe8158965db591aa1996d1d5a5c95ebf5e51" +checksum = "66b5db8df61e3766111c8e42a957af8502b2fc1814f87eacc3a19252ae964a5c" dependencies = [ "i-slint-compiler", "proc-macro2", @@ -5979,7 +5662,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -6014,21 +5696,6 @@ dependencies = [ "libc", ] -[[package]] -name = "sysinfo" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "windows 0.52.0", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -6055,18 +5722,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "tao-core-video-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "objc", -] - [[package]] name = "tar" version = "0.4.44" @@ -6106,23 +5761,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "testkit" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "common", - "crossbeam-channel", - "env_logger 0.11.8", - "fcast-lib", - "flume", - "log", - "mdns-sd", - "slint", - "slint-build", -] - [[package]] name = "text-size" version = "1.1.1" @@ -6401,12 +6039,6 @@ dependencies = [ "serde", ] -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - [[package]] name = "udev" version = "0.9.3" @@ -6496,31 +6128,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "ureq" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" -dependencies = [ - "base64", - "log", - "percent-encoding", - "ureq-proto", - "utf-8", -] - -[[package]] -name = "ureq-proto" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" -dependencies = [ - "base64", - "http 1.3.1", - "httparse", - "log", -] - [[package]] name = "url" version = "2.5.4" @@ -6530,6 +6137,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -6559,12 +6167,6 @@ dependencies = [ "xmlwriter", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf16_iter" version = "1.0.5" @@ -6608,9 +6210,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vtable" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b14a049c8d5d1ff811a00f65ac1454335487ed769a943c7bad89ead3573335" +checksum = "753be81c38dff787d177b5939af1fa16f72f0d0d21a6b7d74ae56e29cd26f2a6" dependencies = [ "const-field-offset", "portable-atomic", @@ -6620,9 +6222,9 @@ dependencies = [ [[package]] name = "vtable-macro" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8502f961cf2f1359fed21a70f67c831ccb3ab9e4c0b4dd3ad40387fbe8875db" +checksum = "8cfcf6171aa2b0f85718ca5888ca32f6edf61d1849f8e4b3786ad890e5b68f68" dependencies = [ "proc-macro2", "quote", @@ -6891,26 +6493,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.60.0" @@ -6937,21 +6519,6 @@ dependencies = [ "windows-numerics 0.2.0", ] -[[package]] -name = "windows-capture" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16" -dependencies = [ - "clap", - "ctrlc", - "parking_lot", - "rayon", - "thiserror 2.0.12", - "windows 0.61.1", - "windows-future 0.2.0", -] - [[package]] name = "windows-collections" version = "0.1.1" @@ -6970,28 +6537,6 @@ dependencies = [ "windows-core 0.61.0", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.60.1" @@ -6999,9 +6544,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ "windows-implement 0.59.0", - "windows-interface 0.59.1", + "windows-interface", "windows-link", - "windows-result 0.3.2", + "windows-result", "windows-strings 0.3.1", ] @@ -7012,9 +6557,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-interface", "windows-link", - "windows-result 0.3.2", + "windows-result", "windows-strings 0.4.0", ] @@ -7038,17 +6583,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "windows-implement" version = "0.59.0" @@ -7071,17 +6605,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "windows-interface" version = "0.59.1" @@ -7119,15 +6642,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.2" @@ -7137,16 +6651,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -7559,16 +7063,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xkbcommon" version = "0.8.0" @@ -7646,13 +7140,12 @@ dependencies = [ [[package]] name = "zbus" -version = "4.4.0" +version = "5.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -7663,20 +7156,16 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", + "futures-lite", "hex", - "nix 0.29.0", + "nix 0.30.1", "ordered-stream", - "rand 0.8.5", "serde", "serde_repr", - "sha1", - "static_assertions", "tracing", "uds_windows", - "windows-sys 0.52.0", - "xdg-home", + "windows-sys 0.59.0", + "winnow 0.7.7", "zbus_macros", "zbus_names", "zvariant", @@ -7684,9 +7173,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +checksum = "a22426b1bc2aca91de97772506f0655fa373448e6010d79d5d5880915c388409" dependencies = [ "zbus_xml", "zvariant", @@ -7694,9 +7183,9 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +checksum = "100ffec29ed51859052f4563061abe35557acb56ba574510571f8398efc70a29" dependencies = [ "proc-macro2", "quote", @@ -7708,35 +7197,38 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.101", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", + "winnow 0.7.7", "zvariant", ] [[package]] name = "zbus_xml" -version = "4.0.0" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" dependencies = [ - "quick-xml 0.30.0", + "quick-xml 0.36.2", "serde", "static_assertions", "zbus_names", @@ -7852,22 +7344,24 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.2.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", + "url", + "winnow 0.7.7", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", @@ -7878,11 +7372,14 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", + "serde", + "static_assertions", "syn 2.0.101", + "winnow 0.7.7", ] diff --git a/Cargo.toml b/Cargo.toml index d16f895..cec8772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "fcast-lib", "common", "receiver", "sender", "scap", "scap-gstreamer", "http", "android-sender", "testkit"] +members = [ "fcast-lib", "common", "receiver", "sender", "android-sender"] [workspace.dependencies] gst-video = { package = "gstreamer-video", version = "0.23.5" } @@ -13,8 +13,8 @@ env_logger = "0.11.6" serde = { version = "1", features = ["derive"] } serde_json = "1" rand = "0.9.0" -slint = "1.11.0" -slint-build = "1.11.0" +slint = "1.12.0" +slint-build = "1.12.0" anyhow = "1.0.98" futures = "0.3" tokio-stream = "0.1.17" diff --git a/README.md b/README.md index 64d9543..3503757 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,14 @@ The current platform support matrix looks like this: #### Desktop -| |Linux (Wayland) |Linux (X11) |Windows |MacOS | -|------------|-----------------|-------------|---------|----------| -|OMSender |Yes |Yes |Yes |No | -|OMReceiver |Yes |Yes |Yes |Untested | +| |Linux (Wayland) |Linux (X11) |Windows |MacOS | +|------------|-----------------|-------------|--------------|----------| +|OMSender |Yes |Yes |~~Yes~~ No^1 |No^1 | +|OMReceiver |Yes |Yes |Yes |Untested | -OMSender can cast to other FCast receivers as well. +1: Support is planned + +~~OMSender can cast to other FCast receivers as well.~~ OMReceiver is also an FCast receiver. diff --git a/android-sender/build.rs b/android-sender/build.rs index 7ad2cb6..cdb35a4 100644 --- a/android-sender/build.rs +++ b/android-sender/build.rs @@ -77,7 +77,6 @@ fn main() { cargo_link!("gstaudio-1.0"); cargo_link!("gstapp-1.0"); cargo_link!("gstrtp-1.0"); - cargo_link!("gstwebrtc-1.0"); const DEFAULT_CLANG_VERSION: &str = "20"; let clang_version = diff --git a/assets/icons/reload.svg b/assets/icons/reload.svg new file mode 100644 index 0000000..a898623 --- /dev/null +++ b/assets/icons/reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/Cargo.toml b/common/Cargo.toml index 96c5261..86501fa 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -18,12 +18,10 @@ gst-rtsp = { package = "gstreamer-rtsp", version = "0.23.5", optional = true} gst-rtsp-server = { package = "gstreamer-rtsp-server", version = "0.23.5", optional = true} gio = { version = "0.20.9", optional = true } -m3u8-rs = { version = "6.0.0", optional = true } futures = { workspace = true, optional = true } tokio-stream = { workspace = true, optional = true } mdns-sd = { workspace = true, optional = true } -http = { path = "../http", optional = true } fcast-lib = { path = "../fcast-lib", optional = true } crossbeam-channel = { version = "0.5.15", optional = true } @@ -42,11 +40,5 @@ glutin_glx_sys = { version = "0.6.1", optional = true } [features] video = ["gst", "gst-app", "gst-video", "gst-gl", "gst-gl-egl", "gst-gl-x11", "glutin_egl_sys", "glutin_glx_sys",] -sender = ["gst", "gst-video", "gio", "m3u8-rs", "gst-pbutils", - "http", "crossbeam-channel", "gst-app", "futures", - "fcast-lib", "tokio-stream", "mdns-sd", "gst-gl-egl", "gst-gl-x11", - "glutin_egl_sys", "glutin_glx_sys", "gst-rtsp", "gst-rtsp-server"] - -[dev-dependencies] -quickcheck = "1" -quickcheck_macros = "1" \ No newline at end of file +sender = ["gst", "gst-video", "gio", "gst-pbutils", "crossbeam-channel", "gst-app", "futures", + "fcast-lib", "tokio-stream", "mdns-sd", "gst-rtsp", "gst-rtsp-server",] \ No newline at end of file diff --git a/common/src/sender/audio.rs b/common/src/sender/audio.rs new file mode 100644 index 0000000..7412a22 --- /dev/null +++ b/common/src/sender/audio.rs @@ -0,0 +1,29 @@ +// TODO: try pipewiredeviceprovider and use pulsedeviceprovider as fallback +// TODO: monitor for changes +#[cfg(target_os = "linux")] +pub fn get_pulse_dev() -> anyhow::Result { + use anyhow::bail; + use gst::prelude::*; + + let provider = gst::DeviceProviderFactory::by_name("pulsedeviceprovider").ok_or( + anyhow::anyhow!("Could not find pulse device provider factory"), + )?; + + provider.start()?; + let devices = provider.devices(); + provider.stop(); + + for device in devices { + if !device.has_classes("Audio/Sink") { + continue; + } + let Some(props) = device.properties() else { + continue; + }; + if props.get::("is-default") == Ok(true) { + return Ok(device); + } + } + + bail!("No device found") +} diff --git a/common/src/sender/mod.rs b/common/src/sender/mod.rs index 7a0e7b3..cc45a59 100644 --- a/common/src/sender/mod.rs +++ b/common/src/sender/mod.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "linux")] +pub mod audio; pub mod discovery; pub mod pipeline; pub mod session; diff --git a/common/src/sender/pipeline.rs b/common/src/sender/pipeline.rs index e6b0d42..9916b28 100644 --- a/common/src/sender/pipeline.rs +++ b/common/src/sender/pipeline.rs @@ -17,7 +17,7 @@ #[cfg(target_os = "android")] use super::transmission::rtp::RtpSink; -use super::transmission::{self, TransmissionSink, hls::HlsSink}; +use super::transmission::{self, TransmissionSink}; use anyhow::Result; use fcast_lib::models::PlayMessage; #[cfg(target_os = "android")] @@ -28,6 +28,7 @@ use log::error; #[cfg(target_os = "android")] use std::future::Future; use std::net::IpAddr; +use std::str::FromStr; pub use transmission::init; @@ -37,13 +38,19 @@ pub enum Event { Error, } +pub enum SourceConfig { + AudioVideo { + video: gst::Element, + audio: gst::Element, + }, + Video(gst::Element), + Audio(gst::Element), +} + #[cfg(not(target_os = "android"))] pub struct Pipeline { inner: gst::Pipeline, - tx_sink: Option>, - tee: gst::Element, - preview_queue: gst::Element, - preview_appsink: gst::Element, + tx_sink: Box, } #[cfg(target_os = "android")] @@ -171,58 +178,69 @@ impl Pipeline { }) } + fn setup_video_source(pipeline: &gst::Pipeline, src: gst::Element) -> Result { + // TODO: needed? + let videoflip = gst::ElementFactory::make("videoflip") + .property_from_str("video-direction", "auto") + .build()?; + let videorate = gst::ElementFactory::make("videorate") + .property("skip-to-first", true) + .build()?; + let capsfilter = gst::ElementFactory::make("capsfilter") + .property("caps", gst::Caps::from_str("video/x-raw,framerate=30/1")?) + .build()?; + + // pipeline.add_many([&src, &videorate, &capsfilter])?; + // gst::Element::link_many([&src, &videorate, &capsfilter])?; + + pipeline.add_many([&src, &videoflip, &videorate, &capsfilter])?; + gst::Element::link_many([&src, &videoflip, &videorate, &capsfilter])?; + + Ok(capsfilter) + } + + fn setup_audio_source(pipeline: &gst::Pipeline, src: gst::Element) -> Result { + let capsfilter = gst::ElementFactory::make("capsfilter") + .property( + "caps", + gst::Caps::from_str("audio/x-raw,channels=2,rate=48000")?, + ) + .build()?; + + pipeline.add_many([&src, &capsfilter])?; + gst::Element::link_many([&src, &capsfilter])?; + + Ok(capsfilter) + } + #[cfg(not(target_os = "android"))] - pub fn new(preview_appsink: gst::Element, mut on_event: E, on_sources: S) -> Result + pub fn new_rtsp(mut on_event: E, source: SourceConfig) -> Result where E: FnMut(Event) + Send + Clone + 'static, - S: Fn(&[gst::glib::Value]) -> Option + Send + Sync + 'static, { - let scapsrc = gst::ElementFactory::make("scapsrc") - .property("perform-internal-preroll", true) - .build()?; - let tee = gst::ElementFactory::make("tee").build()?; - let preview_queue = gst::ElementFactory::make("queue") - .name("preview_queue") - .property("max-size-time", 0u64) - .property("max-size-buffers", 0u32) - .property("max-size-bytes", 0u32) - .property_from_str("leaky", "downstream") - .property("silent", true) // Don't emit signals, can give better perf. - .build()?; + use crate::sender::transmission::rtsp::RtspSink; let pipeline = gst::Pipeline::new(); - let tx_sink = None::>; - - // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/3993 - scapsrc.static_pad("src").unwrap().add_probe( - gst::PadProbeType::QUERY_UPSTREAM.union(gst::PadProbeType::PUSH), - |_pad, info| match info.query_mut().map(|query| query.view_mut()) { - Some(gst::QueryViewMut::Latency(latency)) => { - let (_live, min, max) = latency.result(); - latency.set(false, min, max); - gst::PadProbeReturn::Handled - } - _ => gst::PadProbeReturn::Pass, + let source = match source { + SourceConfig::AudioVideo { video, audio } => SourceConfig::AudioVideo { + video: Self::setup_video_source(&pipeline, video)?, + audio: Self::setup_audio_source(&pipeline, audio)?, }, - ); - - scapsrc.connect("select-source", false, on_sources); - - pipeline.add_many([&scapsrc, &tee, &preview_queue, &preview_appsink])?; - gst::Element::link_many([&scapsrc, &tee])?; - gst::Element::link_many([&preview_queue, &preview_appsink])?; + SourceConfig::Video(video) => { + SourceConfig::Video(Self::setup_video_source(&pipeline, video)?) + } + SourceConfig::Audio(audio) => { + SourceConfig::Audio(Self::setup_audio_source(&pipeline, audio)?) + } + }; - let tee_preview_pad = tee - .request_pad_simple("src_%u") - .map_or_else(|| Err(anyhow::anyhow!("`request_pad_simple()` failed")), Ok)?; - let queue_preview_pad = preview_queue - .static_pad("sink") - .ok_or(anyhow::anyhow!("preview_queue is missing static sink pad"))?; - tee_preview_pad.link(&queue_preview_pad)?; + let rtsp = RtspSink::new(&pipeline, source, 3000)?; + let p = Self { + inner: pipeline.clone(), + tx_sink: Box::new(rtsp), + }; - // Start the pipeline in background thread because `scapsrc` initialization will block until - // the user selects the input source. let _ = std::thread::spawn({ let bus = pipeline .bus() @@ -236,9 +254,11 @@ impl Pipeline { debug!("Failed to upgrade pipeline before starting"); return; }; - debug!("Starting pipeline"); + debug!("Starting pipeline..."); if let Err(err) = pipeline.set_state(gst::State::Playing) { error!("Failed to start pipeline: {err}"); + } else { + debug!("Pipeline started"); } } @@ -278,20 +298,11 @@ impl Pipeline { } }); - Ok(Self { - inner: pipeline, - tx_sink, - tee, - preview_queue, - preview_appsink, - }) + Ok(p) } pub fn playing(&mut self) -> Result<()> { - match &mut self.tx_sink { - Some(sink) => sink.playing(), - None => Ok(()), - } + self.tx_sink.playing() } #[cfg(target_os = "android")] @@ -308,27 +319,7 @@ impl Pipeline { #[cfg(not(target_os = "android"))] pub fn shutdown(&mut self) -> Result<()> { self.inner.set_state(gst::State::Null)?; - - self.preview_queue.unlink(&self.preview_appsink); - self.inner.remove(&self.preview_appsink)?; - - if let Some(sink) = &mut self.tx_sink { - sink.shutdown(); - } - - Ok(()) - } - - #[cfg(not(target_os = "android"))] - pub fn add_hls_sink(&mut self, port: u16) -> Result<()> { - let tee_pad = self - .tee - .request_pad_simple("src_%u") - .ok_or(anyhow::anyhow!("`request_pad_simple()` failed"))?; - let hls = HlsSink::new(&self.inner, tee_pad, port)?; - self.tx_sink = Some(Box::new(hls)); - - debug!("Added HLS sink"); + self.tx_sink.shutdown(); Ok(()) } @@ -348,20 +339,6 @@ impl Pipeline { Ok(()) } - #[cfg(not(target_os = "android"))] - pub fn add_rtsp_sink(&mut self, port: u16) -> Result<()> { - let tee_pad = self - .tee - .request_pad_simple("src_%u") - .ok_or(anyhow::anyhow!("`request_pad_simple()` failed"))?; - let rtsp = transmission::rtsp::RtspSink::new(tee_pad, &self.inner, port)?; - self.tx_sink = Some(Box::new(rtsp)); - - debug!("Added RTSP sink"); - - Ok(()) - } - #[cfg(target_os = "android")] pub fn add_rtp_sink(&mut self, port: u16, receiver_addr: IpAddr) -> Result<()> { let appsrc_pad = self @@ -376,26 +353,9 @@ impl Pipeline { Ok(()) } - pub fn remove_transmission_sink(&mut self) -> Result<()> { - if let Some(sink) = &mut self.tx_sink { - sink.shutdown(); - sink.unlink(&self.inner)?; - } - - self.tx_sink = None; - - debug!("Removed transmission sink"); - - Ok(()) - } - /// Get the message that should be sent to a receiver to consume the stream if a transmission /// sink is present pub fn get_play_msg(&self, addr: IpAddr) -> Option { - if let Some(sink) = &self.tx_sink { - sink.get_play_msg(addr) - } else { - None - } + self.tx_sink.get_play_msg(addr) } } diff --git a/common/src/sender/transmission/hls/fake_file_writer.rs b/common/src/sender/transmission/hls/fake_file_writer.rs deleted file mode 100644 index a2ade82..0000000 --- a/common/src/sender/transmission/hls/fake_file_writer.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use crossbeam_channel::Sender; - -use gst::{glib, subclass::prelude::ObjectSubclassIsExt}; - -pub enum Request { - Delete, - Add(Vec), -} - -pub struct ChannelElement { - pub location: String, - pub request: Request, -} - -mod imp { - use std::cell::RefCell; - - use gio::subclass::prelude::*; - - use super::*; - - #[derive(Default)] - pub struct FakeFileWriter { - pub location: RefCell, - pub data: RefCell>, - pub tx: RefCell>>, - } - - impl ObjectImpl for FakeFileWriter {} - - #[glib::object_subclass] - impl ObjectSubclass for FakeFileWriter { - const NAME: &'static str = "FakeFileWriter"; - type Type = super::FakeFileWriter; - type ParentType = gio::OutputStream; - } - - impl OutputStreamImpl for FakeFileWriter { - fn write( - &self, - buffer: &[u8], - _cancellable: Option<&gio::Cancellable>, - ) -> Result { - self.data.borrow_mut().extend_from_slice(buffer); - Ok(buffer.len()) - } - - fn close(&self, _cancellable: Option<&gio::Cancellable>) -> Result<(), glib::Error> { - match (*self.tx.borrow_mut()).take() { - Some(tx) => { - let location = self.location.borrow().clone(); - let data = self.data.borrow().clone(); - if let Err(err) = tx.send(ChannelElement { - location, - request: Request::Add(data), - }) { - log::debug!("Failed to send fake file: {err}"); - } - Ok(()) - } - None => Err(glib::Error::new(glib::FileError::Failed, "Missing tx")), - } - } - - fn flush(&self, _cancellable: Option<&gio::Cancellable>) -> Result<(), glib::Error> { - Ok(()) - } - - fn splice( - &self, - input_stream: &gio::InputStream, - flags: gio::OutputStreamSpliceFlags, - cancellable: Option<&gio::Cancellable>, - ) -> Result { - self.parent_splice(input_stream, flags, cancellable) - } - } -} - -glib::wrapper! { - pub struct FakeFileWriter(ObjectSubclass) @extends gio::OutputStream; -} - -impl FakeFileWriter { - pub fn new(location: String, tx: Sender) -> Self { - let self_: Self = glib::Object::new(); - - let imp = self_.imp(); - - *imp.location.borrow_mut() = location; - *imp.tx.borrow_mut() = Some(tx); - - self_ - } -} diff --git a/common/src/sender/transmission/hls/mod.rs b/common/src/sender/transmission/hls/mod.rs deleted file mode 100644 index 6e54f02..0000000 --- a/common/src/sender/transmission/hls/mod.rs +++ /dev/null @@ -1,460 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use anyhow::{Result, anyhow}; -use fake_file_writer::FakeFileWriter; -use fcast_lib::models::PlayMessage; -use gst::{glib, prelude::*}; -use log::{debug, error, trace}; -use m3u8_rs::{MasterPlaylist, VariantStream}; -use std::io::{Read, Write}; -use std::net::IpAddr; -use std::str::FromStr; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; -use std::{collections::HashMap, path::PathBuf}; -use url_utils::decode_path; - -use super::TransmissionSink; - -mod fake_file_writer; -mod url_utils; - -const HLS_MIME_TYPE: &str = "application/vnd.apple.mpegurl"; - -fn serve_dir( - base: PathBuf, - server_port: u16, - file_rx: crossbeam_channel::Receiver, - fin: Arc, -) -> Result<()> { - let listener = std::net::TcpListener::bind(format!("[::]:{server_port}"))?; - listener.set_nonblocking(true)?; - - debug!("HTTP server listening on {:?}", listener.local_addr()); - - let mut files: HashMap> = HashMap::new(); - - let mut request_buf = Vec::::new(); - let mut response_buf = Vec::::new(); - - loop { - if fin.load(std::sync::atomic::Ordering::Acquire) { - break; - } - - match listener.accept() { - Ok((mut stream, _)) => { - request_buf.clear(); - response_buf.clear(); - listener.set_nonblocking(false)?; - - let mut buf = [0; 4096]; - loop { - let bytes_read = stream.read(&mut buf)?; - request_buf.extend_from_slice(&buf); - if bytes_read < buf.len() { - break; - } - } - - let request = http::Request::parse(&request_buf)?; - - if request.start_line.method != http::RequestMethod::Get { - let response = http::Response { - start_line: http::ResponseStartLine { - version: http::HttpVersion::One, - status: http::StatusCode::NotImplemented, - }, - headers: vec![], - body: None, - }; - response.serialize_into(&mut response_buf); - stream.write_all(&response_buf)?; - continue; - } - - let Ok(uri) = decode_path(request.start_line.target.trim_start_matches("/")) else { - error!("Failed to decode path {}", request.start_line.target); - let response = http::Response { - start_line: http::ResponseStartLine { - version: http::HttpVersion::One, - status: http::StatusCode::InternalServerError, - }, - headers: vec![], - body: None, - }; - response.serialize_into(&mut response_buf); - stream.write_all(&response_buf)?; - continue; - }; - - let mut base_path = base.clone(); - base_path.push(&uri); - - let key = base_path.to_string_lossy().to_string(); - - match files.get(&key) { - Some(file_contents) => { - let response = http::Response { - start_line: http::ResponseStartLine { - version: http::HttpVersion::One, - status: http::StatusCode::Ok, - }, - headers: vec![ - http::Header::new("Content-Type", "application/octet-stream"), - http::Header::new( - "Content-Length", - file_contents.len().to_string(), - ), - ], - body: Some(file_contents), - }; - response.serialize_into(&mut response_buf); - stream.write_all(&response_buf)?; - } - None => { - error!("File not found: {}", base_path.display()); - let response = http::Response { - start_line: http::ResponseStartLine { - version: http::HttpVersion::One, - status: http::StatusCode::NotFound, - }, - headers: vec![], - body: None, - }; - response.serialize_into(&mut response_buf); - stream.write_all(&response_buf)?; - } - } - - listener.set_nonblocking(true)?; - } - Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err.into()), - _ => (), - } - - match file_rx.try_recv() { - Ok(v) => match v.request { - fake_file_writer::Request::Delete => { - files.remove(&v.location.replace('\\', "/")); - } - fake_file_writer::Request::Add(vec) => { - files.insert(v.location.replace('\\', "/"), vec); - } - }, - Err(err) if err.is_disconnected() => return Err(err.into()), - _ => (), - } - - std::thread::sleep(Duration::from_millis(25)); - } - - debug!("Quitting http server"); - - Ok(()) -} - -fn get_codec_name(sink: &gst::Element) -> Result { - let pad = sink - .static_pad("sink") - .ok_or(anyhow!("Failed to get static sink pad"))?; - let caps = pad - .sticky_event::(0) - .ok_or(anyhow!("Failed to get caps from sink pad"))?; - Ok(gst_pbutils::codec_utils_caps_get_mime_codec(caps.caps())?.to_string()) -} - -pub struct HlsSink { - src_pad: gst::Pad, - queue_pad: gst::Pad, - queue: gst::Element, - convert: gst::Element, - scale: gst::Element, - capsfilter: gst::Element, - pub server_port: u16, - main_path: PathBuf, - enc: gst::Element, - enc_caps: gst::Element, - sink: gst::Element, - write_playlist: bool, - file_tx: crossbeam_channel::Sender, - server_fin: Arc, -} - -impl HlsSink { - pub fn new( - pipeline: &gst::Pipeline, - src_pad: gst::Pad, - port: u16, - ) -> Result { - let queue = gst::ElementFactory::make("queue") - .name("sink_queue") - .property("silent", true) - .build()?; - - let convert = gst::ElementFactory::make("videoconvert") - .name("sink_convert") - .build()?; - let scale = gst::ElementFactory::make("videoscale") - .name("sink_scale") - .build()?; - let capsfilter = gst::ElementFactory::make("capsfilter") - .name("sink_capsfilter") - .property( - "caps", - gst::Caps::from_str("video/x-raw,width=(int)[16,8192,2],height=(int)[16,8192,2]")?, - ) - .build()?; - - let enc = gst::ElementFactory::make("x264enc") - .property("bframes", 0u32) - .property("bitrate", 1024 * 4u32) - .property("key-int-max", i32::MAX as u32) - .property_from_str("tune", "zerolatency") - .property_from_str("speed-preset", "superfast") - .build()?; - let enc_caps = gst::ElementFactory::make("capsfilter") - .property( - "caps", - gst::Caps::builder("video/x-h264") - .field("profile", "main") - .field("framerate", gst::Fraction::new(0, 1)) - .build(), - ) - .build()?; - - let base_path = PathBuf::from("/"); - - let (file_tx, file_rx) = crossbeam_channel::bounded::(10); - - let server_fin = Arc::new(AtomicBool::new(false)); - let server_port = port; - std::thread::spawn({ - let server_fin = Arc::clone(&server_fin); - let base_path = base_path.clone(); - move || { - if let Err(err) = serve_dir(base_path, server_port, file_rx, server_fin) { - error!("Error occured serving directory: {err}"); - } - } - }); - - let mut manifest_path = base_path.clone(); - manifest_path.push("manifest.m3u8"); - - let mut path = base_path.clone(); - path.push("video"); - - let mut playlist_location = path.clone(); - playlist_location.push("manifest.m3u8"); - - let mut init_location = path.clone(); - init_location.push("init_%30d.mp4"); - - let mut location = path.clone(); - location.push("segment_%05d.m4s"); - - let sink = gst::ElementFactory::make("hlscmafsink") - .name("hls_sink") - .property("target-duration", 1u32) - .property("playlist-location", playlist_location.to_str().unwrap()) - .property("init-location", init_location.to_str().unwrap()) - .property("location", location.to_str().unwrap()) - .property("enable-program-date-time", true) - .property("sync", true) - .property("latency", 0u64) - .build()?; - - let file_tx_clone = file_tx.clone(); - sink.connect_closure( - "get-init-stream", - false, - glib::closure!(move |sink: &gst::Element, location: &str| { - trace!("{}, writing init segment to {location}", sink.name()); - FakeFileWriter::new(location.to_string(), file_tx_clone.clone()) - .upcast::() - }), - ); - - let file_tx_clone = file_tx.clone(); - sink.connect_closure( - "get-fragment-stream", - false, - glib::closure!(move |sink: &gst::Element, location: &str| { - trace!("{}, writing segment to {location}", sink.name()); - FakeFileWriter::new(location.to_string(), file_tx_clone.clone()) - .upcast::() - }), - ); - - let file_tx_clone = file_tx.clone(); - sink.connect_closure( - "delete-fragment", - false, - glib::closure!(move |sink: &gst::Element, location: &str| { - trace!("{}, removing segment {location}", sink.name()); - file_tx_clone - .send(fake_file_writer::ChannelElement { - location: location.to_string(), - request: fake_file_writer::Request::Delete, - }) - .is_ok() - }), - ); - - let file_tx_clone = file_tx.clone(); - sink.connect_closure( - "get-playlist-stream", - false, - glib::closure!(move |sink: &gst::Element, location: &str| { - trace!("{}, writing playlist to {location}", sink.name()); - FakeFileWriter::new(location.to_string(), file_tx_clone.clone()) - .upcast::() - }), - ); - - let elems = [ - &queue, - &convert, - &scale, - &capsfilter, - &enc, - &enc_caps, - &sink, - ]; - - pipeline.add_many(elems)?; - - gst::Element::link_many(elems)?; - - for elem in elems { - elem.sync_state_with_parent()?; - } - - let queue_pad = queue - .static_pad("sink") - .map_or_else(|| Err(glib::bool_error!("`static_pad()` failed")), Ok)?; - src_pad - .link(&queue_pad) - .map_err(|err| glib::bool_error!("{err}"))?; - - Ok(Self { - src_pad, - queue_pad, - queue, - convert, - scale, - capsfilter, - server_port, - main_path: manifest_path, - enc, - enc_caps, - sink, - write_playlist: true, - file_tx, - server_fin, - }) - } - - pub fn write_manifest_file(&mut self) -> Result<()> { - if !self.write_playlist { - return Ok(()); - } - - let video_codec = get_codec_name(&self.sink)?; - - let variants = vec![VariantStream { - uri: "video/manifest.m3u8".to_string(), - codecs: Some(video_codec), - ..Default::default() - }]; - - let playlist = MasterPlaylist { - version: Some(6), - variants, - ..Default::default() - }; - - debug!("Writing master manifest to {}", self.main_path.display()); - - let mut buf = Vec::new(); - playlist.write_to(&mut buf)?; - - self.file_tx.send(fake_file_writer::ChannelElement { - location: self.main_path.to_string_lossy().to_string(), - request: fake_file_writer::Request::Add(buf), - })?; - - self.write_playlist = false; - - Ok(()) - } -} - -impl TransmissionSink for HlsSink { - fn get_play_msg(&self, addr: IpAddr) -> Option { - Some(PlayMessage { - container: HLS_MIME_TYPE.to_owned(), - url: Some(format!( - "http://{}:{}/manifest.m3u8", - super::addr_to_url_string(addr), - self.server_port, - )), - content: None, - time: Some(0.0), - speed: Some(1.0), - headers: None, - }) - } - - fn playing(&mut self) -> Result<()> { - self.write_manifest_file()?; - Ok(()) - } - - fn shutdown(&mut self) { - self.server_fin - .store(true, std::sync::atomic::Ordering::Release); - } - - fn unlink(&mut self, pipeline: &gst::Pipeline) -> Result<(), glib::error::BoolError> { - let block = super::block_downstream(&self.src_pad)?; - self.src_pad.unlink(&self.queue_pad)?; - self.src_pad.remove_probe(block); - - let elems = [ - &self.queue, - &self.convert, - &self.scale, - &self.capsfilter, - &self.enc, - &self.enc_caps, - &self.sink, - ]; - - pipeline.remove_many(elems)?; - - for elem in elems { - elem.set_state(gst::State::Null) - .map_err(|err| glib::bool_error!("{err}"))?; - } - - Ok(()) - } -} diff --git a/common/src/sender/transmission/hls/url_utils.rs b/common/src/sender/transmission/hls/url_utils.rs deleted file mode 100644 index 813ac92..0000000 --- a/common/src/sender/transmission/hls/url_utils.rs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -#[inline] -const fn hex_decode(hex: char) -> u8 { - match hex { - '0' => 0x0, - '1' => 0x1, - '2' => 0x2, - '3' => 0x3, - '4' => 0x4, - '5' => 0x5, - '6' => 0x6, - '7' => 0x7, - '8' => 0x8, - '9' => 0x9, - 'A' => 0xA, - 'B' => 0xB, - 'C' => 0xC, - 'D' => 0xD, - 'E' => 0xE, - 'F' => 0xF, - _ => unreachable!(), - } -} - -#[inline] -const fn percent_decode(n1: char, n2: char) -> u8 { - (hex_decode(n1) << 4) + hex_decode(n2) -} - -pub fn decode_path(s: &str) -> Result { - let mut res: Vec = Vec::new(); - let s = s.bytes().collect::>(); - let mut i = 0; - while i < s.len() { - match s[i] { - b'%' if i + 2 < s.len() => { - let decoded = percent_decode(s[i + 1] as char, s[i + 2] as char); - i += 3; - res.push(decoded); - } - b'%' => panic!("Invalid percent encoded value"), - _ => { - res.push(s[i]); - i += 1; - } - } - } - - String::from_utf8(res) -} - -#[cfg(test)] -mod tests { - use quickcheck_macros::quickcheck; - - use super::*; - - #[inline] - const fn hex_encode(b: u8) -> char { - match b { - 0x0 => '0', - 0x1 => '1', - 0x2 => '2', - 0x3 => '3', - 0x4 => '4', - 0x5 => '5', - 0x6 => '6', - 0x7 => '7', - 0x8 => '8', - 0x9 => '9', - 0xA => 'A', - 0xB => 'B', - 0xC => 'C', - 0xD => 'D', - 0xE => 'E', - 0xF => 'F', - _ => unreachable!(), - } - } - - #[inline] - const fn percent_encode(b: u8) -> [char; 3] { - ['%', hex_encode(b >> 4), hex_encode(b & 0x0F)] - } - - /// Percent-encode a given path to valid URL. - /// - /// Does not encode '/'. - fn encode_path(s: &str) -> String { - let mut res = String::new(); - - for ch in s.chars() { - let mut buf = [0; 4]; - - let enc = ch.encode_utf8(&mut buf); - - if enc.len() == 1 { - match ch { - '!' | '#' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ':' | ';' - | '=' | '?' | '@' | '[' | ']' | ' ' | '\"' | '%' | '-' | '.' | '<' | '>' - | '\\' | '^' | '_' | '`' | '{' | '|' | '}' | '~' => { - let encoded = percent_encode(buf[0]); - for c in encoded { - res.push(c); - } - } - _ => res.push(ch), - } - } else - /* Handle non ASCII */ - { - let len = enc.len(); - for b in buf[0..len].iter() { - for c in percent_encode(*b) { - res.push(c) - } - } - } - } - - res - } - - #[test] - fn percent_encoding() { - assert_eq!(percent_encode(0x00), ['%', '0', '0']); - assert_eq!(percent_encode(0xF0), ['%', 'F', '0']); - assert_eq!(percent_encode(0x0F), ['%', '0', 'F']); - assert_eq!(percent_encode(0xAB), ['%', 'A', 'B']); - } - - #[test] - fn percent_decoding() { - assert_eq!(percent_decode('0', '0'), 0x00); - assert_eq!(percent_decode('F', '0'), 0xF0); - assert_eq!(percent_decode('0', 'F'), 0x0F); - assert_eq!(percent_decode('A', 'B'), 0xAB); - } - - #[test] - fn encode_no_reserved() { - assert_eq!(encode_path("helloworld"), "helloworld".to_owned()); - } - - #[test] - fn encode_reserved() { - assert_eq!(encode_path("!"), "%21".to_owned()); - assert_eq!( - encode_path("Hello, World!"), - "Hello%2C%20World%21".to_owned() - ); - } - - #[test] - fn encode_utf8() { - // import urllib.parse;urllib.parse.quote_plus("æøå") - assert_eq!(encode_path("æøå"), "%C3%A6%C3%B8%C3%A5".to_owned()); - } - - #[test] - fn decode_no_encoded() { - assert_eq!(decode_path("helloworld"), Ok("helloworld".to_owned())); - } - - #[test] - fn decode_encoded() { - assert_eq!(decode_path("%21"), Ok("!".to_owned())); - assert_eq!( - decode_path("Hello%2C%20World%21"), - Ok("Hello, World!".to_owned()) - ); - assert_eq!(decode_path("%20%20%20%20"), Ok(" ".to_owned())); - } - - #[test] - fn decode_encoded_utf8() { - assert_eq!(decode_path("%C3%A6%C3%B8%C3%A5"), Ok("æøå".to_owned())); - } - - #[quickcheck] - // QUICKCHECK_TESTS=100000 cargo test - fn encode_decode(s: String) -> bool { - let res = decode_path(&encode_path(&s)); - Ok(s) == res - } -} diff --git a/common/src/sender/transmission/mod.rs b/common/src/sender/transmission/mod.rs index b0a47e0..0b91474 100644 --- a/common/src/sender/transmission/mod.rs +++ b/common/src/sender/transmission/mod.rs @@ -18,23 +18,11 @@ use std::net::IpAddr; use fcast_lib::models::PlayMessage; -use gst::glib; -pub mod hls; #[cfg(target_os = "android")] pub mod rtp; pub mod rtsp; -fn block_downstream(pad: &gst::Pad) -> Result { - use gst::prelude::*; - pad.add_probe(gst::PadProbeType::BLOCK_DOWNSTREAM, |_, _| { - gst::PadProbeReturn::Ok - }) - .ok_or(glib::bool_error!( - "Failed to add BLOCK_DOWNSTREAM pad probe" - )) -} - fn addr_to_url_string(addr: IpAddr) -> String { match addr { IpAddr::V4(ipv4_addr) => ipv4_addr.to_string(), @@ -55,7 +43,4 @@ pub trait TransmissionSink: Send { /// Perform any necessary shutdown procedures fn shutdown(&mut self); - - /// Remove the sink's elements from the pipeline and unlink them from the source - fn unlink(&mut self, pipeline: &gst::Pipeline) -> Result<(), glib::error::BoolError>; } diff --git a/common/src/sender/transmission/rtp/mod.rs b/common/src/sender/transmission/rtp.rs similarity index 84% rename from common/src/sender/transmission/rtp/mod.rs rename to common/src/sender/transmission/rtp.rs index 9c0ae61..c03991c 100644 --- a/common/src/sender/transmission/rtp/mod.rs +++ b/common/src/sender/transmission/rtp.rs @@ -162,34 +162,4 @@ impl TransmissionSink for RtpSink { } fn shutdown(&mut self) {} - - fn unlink(&mut self, pipeline: &gst::Pipeline) -> Result<(), glib::error::BoolError> { - let block = super::block_downstream(&self.src_pad)?; - let queue_sink_pad = self.queue.static_pad("sink").ok_or(glib::bool_error!( - "Failed to get static sink pad from queue" - ))?; - self.src_pad.unlink(&queue_sink_pad)?; - self.src_pad.remove_probe(block); - - let elems = [ - &self.queue, - &self.convert, - &self.scale, - &self.capsfilter, - &self.enc, - &self.enc_caps, - &self.queue2, - &self.pay, - &self.sink, - ]; - - pipeline.remove_many(elems)?; - - for elem in elems { - elem.set_state(gst::State::Null) - .map_err(|err| glib::bool_error!("{err}"))?; - } - - Ok(()) - } } diff --git a/common/src/sender/transmission/rtsp/mod.rs b/common/src/sender/transmission/rtsp.rs similarity index 50% rename from common/src/sender/transmission/rtsp/mod.rs rename to common/src/sender/transmission/rtsp.rs index 0463944..7a9afff 100644 --- a/common/src/sender/transmission/rtsp/mod.rs +++ b/common/src/sender/transmission/rtsp.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with OpenMirroring. If not, see . +use crate::sender::pipeline::SourceConfig; + use super::TransmissionSink; use gst::glib; @@ -24,25 +26,14 @@ pub struct RtspSink { server: RTSPServer, id: Option, main_loop: glib::MainLoop, - src_pad: gst::Pad, - videointersink: gst::Element, } impl RtspSink { - pub fn new(src_pad: gst::Pad, pipeline: &gst::Pipeline, port: u16) -> anyhow::Result { - let videointersink = gst::ElementFactory::make("intervideosink").build()?; - - pipeline.add(&videointersink)?; - - let src_pad_block = super::block_downstream(&src_pad)?; - let intersink_pad = videointersink.static_pad("sink").ok_or(anyhow::anyhow!( - "Failed to get static sink pad from intersink" - ))?; - src_pad.link(&intersink_pad)?; - src_pad.remove_probe(src_pad_block); - - videointersink.sync_state_with_parent()?; - + pub fn new( + pipeline: &gst::Pipeline, + source_config: SourceConfig, + port: u16, + ) -> anyhow::Result { let server = RTSPServer::new(); server.set_service(&format!("{port}")); @@ -52,15 +43,45 @@ impl RtspSink { let factory = RTSPMediaFactory::default(); factory.set_shared(true); - // factory.set_launch("( intervideosrc ! video/x-raw,framerate=25/1 ! videoconvert \ - // ! queue ! vp8enc deadline=1 keyframe-max-dist=2000 keyframe-mode=disabled \ - // error-resilient=default lag-in-frames=0 buffer-initial-size=20 buffer-optimal-size=30 buffer-size=75 \ - // ! rtpvp8pay name=pay0 )"); - // NOTE: "superfast" speed-preset seems to be fine. "ultrafast" yields no noticeable difference in latency - factory.set_launch("( intervideosrc ! video/x-raw,framerate=25/1 ! videoconvert ! videoscale \ - ! video/x-raw,width=(int)[16,8192,2],height=(int)[16,8192,2] \ - ! queue ! x264enc tune=zerolatency speed-preset=superfast b-adapt=false key-int-max=2250 \ - ! video/x-h264,profile=baseline ! rtph264pay config-interval=-1 name=pay0 )"); + + match source_config { + SourceConfig::AudioVideo { video, audio } => { + let video_sink = gst::ElementFactory::make("intervideosink").build()?; + let audio_sink = gst::ElementFactory::make("interaudiosink").build()?; + pipeline.add_many([&video_sink, &audio_sink])?; + video.link(&video_sink)?; + audio.link(&audio_sink)?; + factory.set_launch( + "( intervideosrc ! queue ! videoconvert ! videoscale \ + ! video/x-raw,width=(int)[16,8192,2],height=(int)[16,8192,2] \ + ! queue ! x264enc tune=zerolatency speed-preset=ultrafast b-adapt=false key-int-max=2250 \ + ! video/x-h264,profile=baseline ! rtph264pay config-interval=-1 name=pay0 \ + interaudiosrc ! queue ! audioconvert ! audioresample ! audio/x-raw,rate=48000 \ + ! queue ! opusenc ! rtpopuspay name=pay1 )" + ); + } + SourceConfig::Video(video) => { + let video_sink = gst::ElementFactory::make("intervideosink").build()?; + pipeline.add(&video_sink)?; + video.link(&video_sink)?; + factory.set_launch( + "( intervideosrc ! queue ! videoconvert ! videoscale \ + ! video/x-raw,width=(int)[16,8192,2],height=(int)[16,8192,2] \ + ! queue ! x264enc tune=zerolatency speed-preset=ultrafast b-adapt=false key-int-max=2250 \ + ! video/x-h264,profile=baseline ! rtph264pay config-interval=-1 name=pay0 )" + ); + } + SourceConfig::Audio(audio) => { + let audio_sink = gst::ElementFactory::make("interaudiosink").build()?; + pipeline.add(&audio_sink)?; + audio.link(&audio_sink)?; + factory.set_launch( + "( interaudiosrc ! queue ! audioconvert ! audioresample ! audio/x-raw,rate=48000 \ + ! queue ! opusenc ! rtpopuspay name=pay0 )" + ); + } + } + factory.set_latency(0); mounts.add_factory("/", factory); @@ -76,12 +97,12 @@ impl RtspSink { }); } + // pipeline.debug_to_dot_file(gst::DebugGraphDetails::all(), "rtsp-pipeline"); + Ok(Self { server, id: Some(id), main_loop, - src_pad, - videointersink, }) } } @@ -89,7 +110,7 @@ impl RtspSink { impl TransmissionSink for RtspSink { fn get_play_msg(&self, addr: std::net::IpAddr) -> Option { Some(fcast_lib::models::PlayMessage { - container: "video/x-rtsp".to_owned(), + container: "application/x-rtsp".to_owned(), url: Some(format!( "rtsp://{}:{}/", super::addr_to_url_string(addr), @@ -112,24 +133,4 @@ impl TransmissionSink for RtspSink { id.remove(); } } - - fn unlink(&mut self, pipeline: &gst::Pipeline) -> Result<(), gio::glib::error::BoolError> { - let block = super::block_downstream(&self.src_pad)?; - let intersink_pad = self - .videointersink - .static_pad("sink") - .ok_or(glib::bool_error!( - "Failed to get static sink pad from intersink" - ))?; - self.src_pad.unlink(&intersink_pad)?; - self.src_pad.remove_probe(block); - - pipeline.remove(&self.videointersink)?; - - self.videointersink - .set_state(gst::State::Null) - .map_err(|err| glib::bool_error!("{err}"))?; - - Ok(()) - } } diff --git a/http/Cargo.toml b/http/Cargo.toml deleted file mode 100644 index 4acbaa6..0000000 --- a/http/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "http" -version = "0.1.0" -edition = "2021" - -[dev-dependencies] -ureq = { version = "3.0.8", default-features = false } diff --git a/http/src/lib.rs b/http/src/lib.rs deleted file mode 100644 index 6b0a4cf..0000000 --- a/http/src/lib.rs +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::{error::Error, fmt::Display, io::Read, net::TcpStream}; - -#[derive(Debug, PartialEq, Eq)] -pub enum HttpError { - StartLineEmpty, - InvalidRequestMethod, - StartLineMissingTarget, - InvalidRequestTarget, - StartLineMissingVersion, - StartLineInvalidVersion, - InvalidHeaderKey, - InvalidHeaderValue, - Read, - InvalidRequest, - InvalidContentLength, -} - -impl Error for HttpError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - None - } -} - -impl Display for HttpError { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum RequestMethod { - Get, - Post, - Head, - Delete, - Put, - Connect, - Options, - Trace, - Patch, -} - -impl RequestMethod { - pub fn from_slice(method: &[u8]) -> Option { - match method { - b"GET" => Some(Self::Get), - b"POST" => Some(Self::Post), - b"HEAD" => Some(Self::Head), - b"DELETE" => Some(Self::Delete), - b"PUT" => Some(Self::Put), - b"CONNECT" => Some(Self::Connect), - b"OPTIONS" => Some(Self::Options), - b"TRACE" => Some(Self::Trace), - b"PATCH" => Some(Self::Patch), - _ => None, - } - } - - pub fn to_str(&self) -> &'static str { - match self { - RequestMethod::Get => "GET", - RequestMethod::Post => "POST", - RequestMethod::Head => "HEAD", - RequestMethod::Delete => "DELETE", - RequestMethod::Put => "PUT", - RequestMethod::Connect => "CONNECT", - RequestMethod::Options => "OPTIONS", - RequestMethod::Trace => "TRACE", - RequestMethod::Patch => "PATCH", - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum HttpVersion { - ZeroDotNine, - One, - OneDotOne, - Two, - Three, -} - -impl HttpVersion { - pub fn from_slice(version: &[u8]) -> Option { - match version { - b"HTTP/0.9" => Some(Self::ZeroDotNine), - b"HTTP/1.0" => Some(Self::One), - b"HTTP/1.1" => Some(Self::OneDotOne), - b"HTTP/2" => Some(Self::Two), - b"HTTP/3" => Some(Self::Three), - _ => None, - } - } - - pub fn to_str(&self) -> &'static str { - match self { - HttpVersion::ZeroDotNine => "HTTP/0.9", - HttpVersion::One => "HTTP/1.0", - HttpVersion::OneDotOne => "HTTP/1.1", - HttpVersion::Two => "HTTP/2", - HttpVersion::Three => "HTTP/3", - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct RequestStartLine { - pub method: RequestMethod, - pub target: String, - pub version: HttpVersion, -} - -impl RequestStartLine { - /// Takes in a start line buffer with the "\r\n" suffix removed and returns the parsed value - pub fn parse(buf: &[u8]) -> Result { - if buf.is_empty() { - return Err(HttpError::StartLineEmpty); - } - - let mut i = 0; - // Method - while i < buf.len() && buf[i] != b' ' { - i += 1; - } - - let method = - RequestMethod::from_slice(&buf[0..i]).ok_or(HttpError::InvalidRequestMethod)?; - - i += 1; - - if i >= buf.len() { - return Err(HttpError::StartLineMissingTarget); - } - - let mut j = i; - // Target - while j < buf.len() && buf[j] != b' ' { - j += 1; - } - - let target = - String::from_utf8(buf[i..j].to_vec()).map_err(|_| HttpError::InvalidRequestTarget)?; - - j += 1; - - if j >= buf.len() { - return Err(HttpError::StartLineMissingVersion); - } - - let version = - HttpVersion::from_slice(&buf[j..]).ok_or(HttpError::StartLineInvalidVersion)?; - - Ok(Self { - method, - target, - version, - }) - } -} - -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Header { - pub key: String, - pub value: String, -} - -impl Header { - pub fn new(key: impl Into, value: impl Into) -> Self { - Self { - key: key.into(), - value: value.into(), - } - } - - /// Takes in a header buffer with the "\r\n" suffix removed and returns the parsed value - pub fn parse(header: &[u8]) -> Result { - let mut i = 0; - // Key - while i < header.len() && header[i] != b':' && header[i] != b' ' { - i += 1; - } - - if header[i] == b' ' { - return Err(HttpError::InvalidHeaderKey); - } - - let key = - String::from_utf8(header[0..i].to_vec()).map_err(|_| HttpError::InvalidHeaderKey)?; - - if key.is_empty() { - return Err(HttpError::InvalidHeaderKey); - } - - i += 2; - - // Check that there is a space between the : and the - if i > header.len() || header[i - 1] != b' ' { - return Err(HttpError::InvalidHeaderValue); - } - - let value = - String::from_utf8(header[i..].to_vec()).map_err(|_| HttpError::InvalidHeaderValue)?; - - if value.is_empty() { - return Err(HttpError::InvalidHeaderValue); - } - - Ok(Self { key, value }) - } - - pub fn serialize_with_crlf_into(&self, buf: &mut Vec) { - // let mut buf = Vec::new(); - buf.extend_from_slice(self.key.as_bytes()); - buf.extend_from_slice(b": "); - buf.extend_from_slice(self.value.as_bytes()); - buf.extend_from_slice(b"\r\n"); - // buf - } -} - -#[inline] -fn extract_line(buf: &[u8]) -> Option<&[u8]> { - let mut i = 0; - while i < buf.len() && buf[i] != b'\r' { - i += 1; - } - - if i + 1 >= buf.len() || buf[i + 1] != b'\n' { - return None; - } - - Some(&buf[0..i]) -} - -#[derive(Debug, PartialEq, Eq)] -pub struct Request { - pub start_line: RequestStartLine, - pub headers: Vec
, - pub body: Option>, -} - -impl Request { - pub fn parse(buf: &[u8]) -> Result { - let start_line_buf = extract_line(buf).ok_or(HttpError::InvalidRequest)?; - let start_line = RequestStartLine::parse(start_line_buf)?; - - let mut headers: Vec
= Vec::new(); - let mut content_length: Option = None; - - let mut i = start_line_buf.len() + 2; - while i < buf.len() && buf[i] != b'\r' { - let header_buf = extract_line(&buf[i..]).ok_or(HttpError::InvalidRequest)?; - let header = Header::parse(header_buf)?; - - if header.key.as_str() == "Content-Length" { - content_length = Some( - header - .value - .parse::() - .map_err(|_| HttpError::InvalidContentLength)?, - ); - } - - headers.push(header); - - i += header_buf.len() + 2; - } - - if i + 1 >= buf.len() || buf[i + 1] != b'\n' { - return Err(HttpError::InvalidRequest); - } - - i += 2; - - let mut body = None; - - if let Some(content_length) = content_length { - if content_length > 0 { - if i + content_length > buf.len() { - return Err(HttpError::InvalidContentLength); - } - - body = Some(buf[i..i + content_length].to_vec()); - } - } - - Ok(Self { - start_line, - headers, - body, - }) - } - - pub fn read_from_tcp_stream(stream: &mut TcpStream) -> Result { - let mut request_buf = Vec::new(); - let mut buf = [0; 4096]; - loop { - let bytes_read = stream.read(&mut buf).map_err(|_| HttpError::Read)?; - request_buf.extend_from_slice(&buf[0..bytes_read]); - if bytes_read < buf.len() { - break; - } - } - Self::parse(&request_buf) - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum StatusCode { - Ok, - BadRequest, - NotFound, - InternalServerError, - NotImplemented, -} - -impl StatusCode { - pub fn to_str(&self) -> &'static str { - match self { - StatusCode::Ok => "200 OK", - StatusCode::BadRequest => "400 Bad Request", - StatusCode::NotFound => "404 Not Found", - StatusCode::InternalServerError => "500 Internal Server Error", - StatusCode::NotImplemented => "501 Not Implemented", - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ResponseStartLine { - pub version: HttpVersion, - pub status: StatusCode, -} - -impl ResponseStartLine { - pub fn serialize_with_crlf_into(&self, buf: &mut Vec) { - // let mut buf = Vec::new(); - buf.extend_from_slice(self.version.to_str().as_bytes()); - buf.push(b' '); - buf.extend_from_slice(self.status.to_str().as_bytes()); - buf.extend_from_slice(b"\r\n"); - - // buf - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct Response<'a> { - pub start_line: ResponseStartLine, - pub headers: Vec
, - pub body: Option<&'a [u8]>, -} - -impl Response<'_> { - pub fn serialize_into(&self, buf: &mut Vec) { - // let mut buf = Vec::new(); - self.start_line.serialize_with_crlf_into(buf); - // buf.extend_from_slice(&self.start_line.serialize_with_crlf()); - - for header in &self.headers { - header.serialize_with_crlf_into(buf); - // buf.extend_from_slice(&header.serialize_with_crlf()); - } - - buf.extend_from_slice(b"\r\n"); - - if let Some(body) = &self.body { - buf.extend_from_slice(body); - } - - // buf - } -} - -#[cfg(test)] -mod tests { - use std::io::Write; - - use super::*; - - macro_rules! req_start_line { - ($method:ident, $target:expr, $version:ident) => { - RequestStartLine { - method: RequestMethod::$method, - target: $target.to_owned(), - version: HttpVersion::$version, - } - }; - } - - macro_rules! header { - ($key:expr, $value:expr) => { - Header { - key: $key.to_owned(), - value: $value.to_owned(), - } - }; - } - - macro_rules! request { - ($start_line:expr, $headers:expr, $body:expr) => { - Request { - start_line: $start_line, - headers: $headers, - body: $body, - } - }; - } - - #[test] - fn parse_start_line() { - assert_eq!( - RequestStartLine::parse(b"GET / HTTP/0.9"), - Ok(req_start_line!(Get, "/", ZeroDotNine)) - ); - assert_eq!( - RequestStartLine::parse(b"POST / HTTP/0.9"), - Ok(req_start_line!(Post, "/", ZeroDotNine)) - ); - assert_eq!( - RequestStartLine::parse(b"GET /index.html HTTP/1.0"), - Ok(req_start_line!(Get, "/index.html", One)) - ); - } - - #[test] - fn parse_start_line_empty() { - assert_eq!(RequestStartLine::parse(b""), Err(HttpError::StartLineEmpty)); - } - - #[test] - fn parse_start_missing_target() { - assert_eq!( - RequestStartLine::parse(b"GET"), - Err(HttpError::StartLineMissingTarget) - ); - } - - #[test] - fn parse_start_missing_version() { - assert_eq!( - RequestStartLine::parse(b"GET /"), - Err(HttpError::StartLineMissingVersion) - ); - } - - #[test] - fn parse_start_line_invalid_method() { - assert_eq!( - RequestStartLine::parse(b"POKE / HTTP/1.0"), - Err(HttpError::InvalidRequestMethod) - ); - assert_eq!( - RequestStartLine::parse(b"OPTION / HTTP/1.0"), - Err(HttpError::InvalidRequestMethod) - ); - assert_eq!( - RequestStartLine::parse(b"get / HTTP/1.0"), - Err(HttpError::InvalidRequestMethod) - ); - } - - #[test] - fn parse_start_line_invalid_version() { - assert_eq!( - RequestStartLine::parse(b"GET / HTTP/0.8"), - Err(HttpError::StartLineInvalidVersion) - ); - assert_eq!( - RequestStartLine::parse(b"GET / HTTP/1.8"), - Err(HttpError::StartLineInvalidVersion) - ); - assert_eq!( - RequestStartLine::parse(b"GET / HTTP"), - Err(HttpError::StartLineInvalidVersion) - ); - assert_eq!( - RequestStartLine::parse(b"GET / 1234"), - Err(HttpError::StartLineInvalidVersion) - ); - } - - #[test] - fn parse_header() { - assert_eq!(Header::parse(b"Key: Value"), Ok(header!("Key", "Value")),); - assert_eq!( - Header::parse(b"Content-Length: 100"), - Ok(header!("Content-Length", "100")), - ); - assert_eq!( - Header::parse(b"Host: example.com"), - Ok(header!("Host", "example.com")), - ); - } - - #[test] - fn parse_header_invalid_header_key() { - assert_eq!( - Header::parse(b"Key : Value"), - Err(HttpError::InvalidHeaderKey), - ); - assert_eq!(Header::parse(b": Value"), Err(HttpError::InvalidHeaderKey),); - } - - #[test] - fn parse_header_invalid_value() { - assert_eq!( - Header::parse(b"Key:Value"), - Err(HttpError::InvalidHeaderValue), - ); - assert_eq!(Header::parse(b"Key: "), Err(HttpError::InvalidHeaderValue),); - } - - #[test] - fn parse_request() { - assert_eq!( - Request::parse( - b"GET / HTTP/1.0\r\n\ - Content-Length: 0\r\n\ - \r\n" - ), - Ok(request!( - req_start_line!(Get, "/", One), - vec![header!("Content-Length", "0")], - None - )) - ); - assert_eq!( - Request::parse( - b"GET / HTTP/1.0\r\n\ - Content-Length: 0\r\n\ - Key: Value\r\n\ - \r\n" - ), - Ok(request!( - req_start_line!(Get, "/", One), - vec![header!("Content-Length", "0"), header!("Key", "Value"),], - None - )) - ); - assert_eq!( - Request::parse( - b"POST / HTTP/1.0\r\n\ - Content-Length: 11\r\n\ - Content-Type: application/json\r\n\ - \r\n\ - {\"json\": 1}" - ), - Ok(request!( - req_start_line!(Post, "/", One), - vec![ - header!("Content-Length", "11"), - header!("Content-Type", "application/json") - ], - Some(b"{\"json\": 1}".to_vec()) - )) - ); - } - - #[test] - fn parse_request_invalid_content_length() { - assert_eq!( - Request::parse( - b"GET / HTTP/1.0\r\n\ - Content-Length: 1\r\n\ - \r\n" - ), - Err(HttpError::InvalidContentLength) - ); - assert_eq!( - Request::parse( - b"GET / HTTP/1.0\r\n\ - Content-Length: -1\r\n\ - \r\n" - ), - Err(HttpError::InvalidContentLength) - ); - } - - #[test] - fn serialize_header() { - { - let mut buf = Vec::new(); - Header::parse(b"Key: Value") - .unwrap() - .serialize_with_crlf_into(&mut buf); - assert_eq!(&buf, b"Key: Value\r\n"); - } - { - let mut buf = Vec::new(); - Header::parse(b"Content-Length: 123456") - .unwrap() - .serialize_with_crlf_into(&mut buf); - assert_eq!(&buf, b"Content-Length: 123456\r\n"); - } - } - - #[test] - fn serialize_response_start_line() { - { - let mut buf = Vec::new(); - ResponseStartLine { - version: HttpVersion::OneDotOne, - status: StatusCode::Ok, - } - .serialize_with_crlf_into(&mut buf); - assert_eq!(&buf, b"HTTP/1.1 200 OK\r\n"); - } - { - let mut buf = Vec::new(); - ResponseStartLine { - version: HttpVersion::ZeroDotNine, - status: StatusCode::InternalServerError, - } - .serialize_with_crlf_into(&mut buf); - assert_eq!(&buf, b"HTTP/0.9 500 Internal Server Error\r\n"); - } - } - - #[test] - fn serialize_response() { - { - let mut buf = Vec::new(); - Response { - start_line: ResponseStartLine { - version: HttpVersion::One, - status: StatusCode::Ok, - }, - headers: vec![header!("Content-Length", "0")], - body: None, - } - .serialize_into(&mut buf); - assert_eq!( - &buf, - b"HTTP/1.0 200 OK\r\n\ - Content-Length: 0\r\n\ - \r\n" - ); - } - { - let mut buf = Vec::new(); - Response { - start_line: ResponseStartLine { - version: HttpVersion::One, - status: StatusCode::Ok, - }, - headers: vec![header!("Content-Length", "10")], - body: Some(b"AAAAAAAAAA"), - } - .serialize_into(&mut buf); - assert_eq!( - &buf, - b"HTTP/1.0 200 OK\r\n\ - Content-Length: 10\r\n\ - \r\n\ - AAAAAAAAAA" - ); - } - } - - fn headers_are_same(h1: &[Header], h2: &[Header]) -> bool { - if h1.len() != h2.len() { - return false; - } - - let mut h1_set = std::collections::HashSet::new(); - - for h in h1 { - h1_set.insert(h); - } - - for h in h2 { - if !h1_set.contains(h) { - return false; - } - } - - true - } - - #[test] - fn headers_are_actually_same() { - { - let h1 = vec![header!("1", "1"), header!("2", "2"), header!("3", "3")]; - let h2 = vec![header!("1", "1"), header!("2", "2"), header!("3", "3")]; - assert!(headers_are_same(&h1, &h2)); - } - { - let h1 = vec![header!("2", "2"), header!("3", "3"), header!("1", "1")]; - let h2 = vec![header!("1", "1"), header!("2", "2"), header!("3", "3")]; - assert!(headers_are_same(&h1, &h2)); - } - } - - #[test] - fn headers_are_not_same() { - { - let h1 = vec![header!("1", "2"), header!("2", "2"), header!("3", "3")]; - let h2 = vec![header!("1", "1"), header!("2", "2"), header!("3", "3")]; - assert!(!headers_are_same(&h1, &h2)); - } - { - let h1 = vec![header!("3", "3"), header!("1", "1")]; - let h2 = vec![header!("1", "1"), header!("2", "2"), header!("3", "3")]; - assert!(!headers_are_same(&h1, &h2)); - } - } - - #[test] - fn read_from_tcp_stream_basic() { - let (ready_tx, ready_rx) = std::sync::mpsc::channel(); - let handle = std::thread::spawn(move || { - let listener = std::net::TcpListener::bind("127.0.0.1:26969").unwrap(); - - ready_tx.send(()).unwrap(); - - let (mut stream, _) = listener.accept().unwrap(); - - let request = Request::read_from_tcp_stream(&mut stream).unwrap(); - - assert_eq!( - request.start_line, - RequestStartLine { - method: RequestMethod::Get, - target: "/".to_string(), - version: HttpVersion::One, - } - ); - assert!(headers_are_same( - &request.headers, - &[ - header!("user-agent", "test"), - header!("accept", "*/*"), - header!("host", "127.0.0.1:26969"), - ] - )); - assert!(request.body.is_none()); - - let response = Response { - start_line: ResponseStartLine { - version: HttpVersion::One, - status: StatusCode::Ok, - }, - headers: vec![header!("Content-Length", "0")], - body: None, - }; - - let mut response_buf = Vec::new(); - response.serialize_into(&mut response_buf); - - // stream.write_all(&response.serialize()).unwrap(); - stream.write_all(&response_buf).unwrap(); - - stream.shutdown(std::net::Shutdown::Both).unwrap(); - }); - - ready_rx.recv().unwrap(); - - let respone = ureq::get("http://127.0.0.1:26969/") - .version(ureq::http::Version::HTTP_10) - .header("user-agent", "test") - .call() - .unwrap(); - - assert_eq!(respone.status(), ureq::http::StatusCode::OK); - assert_eq!(respone.version(), ureq::http::Version::HTTP_10); - - handle.join().unwrap(); - } -} diff --git a/scap-gstreamer/Cargo.toml b/scap-gstreamer/Cargo.toml deleted file mode 100644 index 40f0656..0000000 --- a/scap-gstreamer/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "scap-gstreamer" -version = "0.1.0" -authors = ["Marcus L. Hanestad "] -repository = "https://github.com/malba124/scap-gstreamer" -license = "GPL-3.0-or-later" -edition = "2021" -description = "Scap screencast plugin for GStreamer" - -[dependencies] -gst = { workspace = true } -gst_base = { package = "gstreamer-base", version = "0.23.5" } -gst-video = { workspace = true } -scap = { path = "../scap" } -crossbeam-channel = { workspace = true } - -[build-dependencies] -gst-plugin-version-helper = "0.8.2" - -[dev-dependencies] -ctrlc = "3.4.5" - -[lib] -name = "scapgst" -crate-type = ["cdylib", "rlib"] -path = "src/lib.rs" diff --git a/scap-gstreamer/README.md b/scap-gstreamer/README.md deleted file mode 100644 index 7618cc2..0000000 --- a/scap-gstreamer/README.md +++ /dev/null @@ -1 +0,0 @@ -Fork of [https://github.com/MAlba124/scap-gstreamer](https://github.com/MAlba124/scap-gstreamer) diff --git a/scap-gstreamer/build.rs b/scap-gstreamer/build.rs deleted file mode 100644 index 8328ca0..0000000 --- a/scap-gstreamer/build.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -fn main() { - gst_plugin_version_helper::info() -} diff --git a/scap-gstreamer/src/lib.rs b/scap-gstreamer/src/lib.rs deleted file mode 100644 index 9f660e8..0000000 --- a/scap-gstreamer/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2024-2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use gst::glib; - -mod scapsrc; - -fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { - scapsrc::register(plugin)?; - Ok(()) -} - -gst::plugin_define!( - scapgst, - env!("CARGO_PKG_DESCRIPTION"), - plugin_init, - concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), - "MIT/X11", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_REPOSITORY"), - env!("BUILD_REL_DATE") -); diff --git a/scap-gstreamer/src/scapsrc/imp.rs b/scap-gstreamer/src/scapsrc/imp.rs deleted file mode 100644 index d625d80..0000000 --- a/scap-gstreamer/src/scapsrc/imp.rs +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright (C) 2024-2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::sync::Arc; -use std::sync::LazyLock; -use std::sync::Mutex; - -use gst::glib; -use gst::prelude::*; - -use gst_base::prelude::BaseSrcExt; -use gst_base::subclass::base_src::CreateSuccess; -use gst_base::subclass::prelude::*; -use gst_video::VideoFrameExt; -use scap::capturer::Capturer; -use scap::capturer::Pts; -use scap::frame::FrameInfo; - -const DEFAULT_FPS: u32 = 25; -const DEFAULT_SHOW_CURSOR: bool = true; -const DEFAULT_PERFORM_INTERNAL_PREROLL: bool = false; - -static CAT: LazyLock = LazyLock::new(|| { - gst::DebugCategory::new( - "scapsrc", - gst::DebugColorFlags::empty(), - Some("Scap screencast source"), - ) -}); - -enum Event { - NewCaps(gst::Caps), - Frame(gst::Buffer), -} - -#[inline] -const fn frame_format_to_gst(format: &scap::frame::FrameFormat) -> gst_video::VideoFormat { - match format { - scap::frame::FrameFormat::RGBx => gst_video::VideoFormat::Rgbx, - scap::frame::FrameFormat::XBGR => gst_video::VideoFormat::Xbgr, - scap::frame::FrameFormat::BGRx => gst_video::VideoFormat::Bgrx, - scap::frame::FrameFormat::BGRA => gst_video::VideoFormat::Bgra, - scap::frame::FrameFormat::RGBA => gst_video::VideoFormat::Rgba, - } -} - -struct Settings { - pub show_cursor: bool, - pub fps: u32, - pub perform_internal_preroll: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - show_cursor: DEFAULT_SHOW_CURSOR, - fps: DEFAULT_FPS, - perform_internal_preroll: DEFAULT_PERFORM_INTERNAL_PREROLL, - } - } -} - -#[derive(Default)] -struct State { - info: Option, - width: i32, - height: i32, -} - -pub struct ScapSrc { - settings: Mutex, - capturer: Mutex>, - state: Arc>, - event_rx: Mutex>>, - buffer_pool: Arc>, -} - -impl Default for ScapSrc { - fn default() -> Self { - Self { - settings: Mutex::new(Default::default()), - capturer: Mutex::new(None), - state: Arc::new(Mutex::new(Default::default())), - event_rx: Mutex::new(None), - buffer_pool: Arc::new(Mutex::new(gst_video::VideoBufferPool::new().upcast())), - } - } -} - -#[glib::object_subclass] -impl ObjectSubclass for ScapSrc { - const NAME: &'static str = "ScapSrc"; - type Type = super::ScapSrc; - type ParentType = gst_base::PushSrc; -} - -impl ObjectImpl for ScapSrc { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: LazyLock> = LazyLock::new(|| { - vec![ - glib::ParamSpecUInt::builder("fps") - .nick("Frames per second") - .blurb("Rate to capture screen at") - .minimum(1) - .default_value(DEFAULT_FPS) - .mutable_ready() - .build(), - glib::ParamSpecBoolean::builder("show-cursor") - .nick("Show cursor") - .blurb("Whether to capture the cursor or not") - .default_value(DEFAULT_SHOW_CURSOR) - .mutable_ready() - .build(), - glib::ParamSpecBoolean::builder("perform-internal-preroll") - .nick("Perform internal preroll") - .blurb("Pull one frame from the capture source before format negotiation") - .default_value(DEFAULT_PERFORM_INTERNAL_PREROLL) - .mutable_ready() - .build(), - ] - }); - - &PROPERTIES - } - - fn constructed(&self) { - self.parent_constructed(); - - let obj = self.obj(); - obj.set_live(true); - obj.set_format(gst::Format::Time); - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "fps" => { - let mut settings = self.settings.lock().unwrap(); - let new_fps = value.get().expect("type checked upstream"); - - gst::info!( - CAT, - imp = self, - "fps was changed from `{}` to `{}`", - settings.fps, - new_fps - ); - - settings.fps = new_fps; - } - "show-cursor" => { - let mut settings = self.settings.lock().unwrap(); - let new_show_cursor = value.get().expect("type checked upstream"); - - gst::info!( - CAT, - imp = self, - "show-cursor was changed from `{}` to `{}`", - settings.show_cursor, - new_show_cursor - ); - - settings.show_cursor = new_show_cursor; - } - "perform-internal-preroll" => { - let mut settings = self.settings.lock().unwrap(); - let new_perf_internal_preroll = value.get().expect("type checked upstream"); - - gst::info!( - CAT, - imp = self, - "perform-internal-preroll was changed from `{}` to `{}`", - settings.perform_internal_preroll, - new_perf_internal_preroll, - ); - - settings.perform_internal_preroll = new_perf_internal_preroll; - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "fps" => { - let settings = self.settings.lock().unwrap(); - settings.fps.to_value() - } - "show-cursor" => { - let settings = self.settings.lock().unwrap(); - settings.show_cursor.to_value() - } - "perform-internal-preroll" => { - let settings = self.settings.lock().unwrap(); - settings.perform_internal_preroll.to_value() - } - _ => unimplemented!(), - } - } - - fn signals() -> &'static [glib::subclass::Signal] { - static SIGNALS: LazyLock> = LazyLock::new(|| { - vec![glib::subclass::Signal::builder("select-source") - .param_types([Vec::::static_type()]) - .return_type::() - .build()] - }); - - SIGNALS.as_ref() - } -} - -impl GstObjectImpl for ScapSrc {} - -impl ElementImpl for ScapSrc { - fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { - static ELEMENT_METADATA: LazyLock = LazyLock::new(|| { - gst::subclass::ElementMetadata::new( - "Scap screencast source", - "Source/Video", - "Scap screencast source", - "Marcus L. Hanestad ", - ) - }); - - Some(&*ELEMENT_METADATA) - } - - fn pad_templates() -> &'static [gst::PadTemplate] { - static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { - let caps = gst_video::VideoCapsBuilder::new() - .format_list([ - gst_video::VideoFormat::Rgbx, - gst_video::VideoFormat::Xbgr, - gst_video::VideoFormat::Bgrx, - gst_video::VideoFormat::Bgrx, - gst_video::VideoFormat::Bgra, - ]) - .build(); - let src_pad_template = gst::PadTemplate::new( - "src", - gst::PadDirection::Src, - gst::PadPresence::Always, - &caps, - ) - .unwrap(); - - vec![src_pad_template] - }); - - &PAD_TEMPLATES - } - - fn change_state( - &self, - transition: gst::StateChange, - ) -> Result { - gst::debug!(CAT, imp = self, "State transition: {transition:?}"); - - let mut res = self.parent_change_state(transition)?; - - match transition { - gst::StateChange::ReadyToPaused => res = gst::StateChangeSuccess::NoPreroll, - gst::StateChange::PausedToPlaying => { - let mut capturer = self.capturer.lock().unwrap(); - match &mut *capturer { - Some(c) => c.start_capture(), - None => { - gst::error!(CAT, imp = self, "Capturer is missing"); - return Err(gst::StateChangeError); - } - } - gst::info!(CAT, imp = self, "Capturing engine was started"); - } - _ => (), - } - - Ok(res) - } -} - -impl BaseSrcImpl for ScapSrc { - fn start(&self) -> Result<(), gst::ErrorMessage> { - let mut capturer = self.capturer.lock().unwrap(); - let settings = self.settings.lock().unwrap(); - - if let Some(mut capturer) = capturer.take() { - gst::debug!(CAT, imp = self, "Capturer exists, stopping"); - capturer.stop_capture(); - } - - let targets = match scap::get_all_targets() { - Ok(t) => t, - Err(err) => return Err(gst::error_msg!(gst::LibraryError::Init, ["{err}"])), - }; - if targets.is_empty() { - gst::error!(CAT, imp = self, "No sources available to capture"); - return Err(gst::error_msg!( - gst::LibraryError::Init, - ["No sources available to capture"] - )); - } - - let source_idx = self.obj().emit_by_name::( - "select-source", - &[&targets.iter().map(|t| t.title()).collect::>()], - ); - - let (event_tx, event_rx) = crossbeam_channel::bounded::(2); - - let event_tx_clone = event_tx.clone(); - let on_format_changed = move |new_format: FrameInfo| { - let gst_v_format = frame_format_to_gst(&new_format.format); - let new_video_info = - gst_video::VideoInfo::builder(gst_v_format, new_format.width, new_format.height) - .build() - .unwrap(); - - gst::debug!(CAT, "Got new format: {new_video_info:?}"); - - let new_caps = new_video_info.to_caps().unwrap(); - - event_tx_clone.send(Event::NewCaps(new_caps)).unwrap(); - }; - - let buffer_pool_clone = Arc::clone(&self.buffer_pool); - let state_clone = Arc::clone(&self.state); - let on_frame = move |pts: Pts, data: &[u8]| { - if event_tx.is_full() { - return; - } - - let buffer_pool = buffer_pool_clone.lock().unwrap(); - let Ok(buffer) = buffer_pool.acquire_buffer(None) else { - gst::error!(CAT, "Failed to acquire buffer"); - return; - }; - drop(buffer_pool); - - let state = state_clone.lock().unwrap(); - let mut vframe = - gst_video::VideoFrame::from_buffer_writable(buffer, state.info.as_ref().unwrap()) - .unwrap(); - drop(state); - - let dest_stride = vframe.plane_stride()[0] as usize; - let dest = vframe.plane_data_mut(0).unwrap(); - - for (dest, src) in dest - .chunks_exact_mut(dest_stride) - .zip(data.chunks_exact(dest_stride)) - { - dest[..dest_stride].copy_from_slice(&src[..dest_stride]); - } - - let mut buffer = vframe.into_buffer(); - buffer - .get_mut() - .unwrap() - .set_pts(gst::ClockTime::from_nseconds(pts)); - - if let Err(err) = event_tx.try_send(Event::Frame(buffer)) { - gst::fixme!(CAT, "Failed to send frame: {err}"); - } - }; - - let mut new_capturer = Capturer::build( - scap::capturer::Options { - fps: settings.fps, - show_cursor: settings.show_cursor, - show_highlight: true, - target: Some(targets[source_idx as usize].clone()), - output_resolution: scap::capturer::Resolution::Captured, - }, - Box::new(on_format_changed), - Box::new(on_frame), - ) - .map_err(|err| gst::error_msg!(gst::LibraryError::Init, ["{err}"]))?; - - if settings.perform_internal_preroll { - // deadlock prevention - drop(settings); - - gst::info!(CAT, imp = self, "Performing internal preroll"); - new_capturer.start_capture(); - match event_rx.recv().map_err(|err| { - gst::error_msg!(gst::LibraryError::Init, ["Failed to get format: {err}"]) - })? { - Event::NewCaps(caps) => self.obj().set_caps(&caps).map_err(|_| { - gst::error_msg!( - gst::LibraryError::Init, - ["Failed to set caps while performing internal preroll"] - ) - })?, - Event::Frame(_) => unreachable!(), - } - } - - let mut rx = self.event_rx.lock().unwrap(); - *rx = Some(event_rx); - - *capturer = Some(new_capturer); - - gst::debug!(CAT, imp = self, "Capturer created"); - - Ok(()) - } - - fn stop(&self) -> Result<(), gst::ErrorMessage> { - let mut capturer = self.capturer.lock().unwrap().take().ok_or(gst::error_msg!( - gst::LibraryError::Shutdown, - ["Missing capturer"] - ))?; - - capturer.stop_capture(); - - Ok(()) - } - - fn set_caps(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> { - let info = gst_video::VideoInfo::from_caps(caps).map_err(|_| { - gst::loggable_error!(CAT, "Failed to build `VideoInfo` from caps {}", caps) - })?; - - gst::debug!(CAT, imp = self, "Configuring for caps: {}", caps); - - let mut buffer_pool = self.buffer_pool.lock().unwrap(); - let config = buffer_pool.config(); - let params = config.params().unwrap(); - let now_size = info.size(); - if params.0 != Some(info.to_caps()?) || params.1 as usize != now_size { - buffer_pool.set_active(false)?; - let new_pool = gst_video::VideoBufferPool::new(); - let mut config = new_pool.config(); - config.set_params(Some(&info.to_caps()?), info.size() as u32, 0, 0); - new_pool.set_config(config)?; - new_pool.set_active(true)?; - *buffer_pool = new_pool.upcast(); - } - - let (new_width, new_height) = (info.width(), info.height()); - self.obj().set_blocksize(4 * new_width * new_height); - - let mut state = self.state.lock().unwrap(); - state.info = Some(info); - state.width = new_width as i32; - state.height = new_height as i32; - - Ok(()) - } - - fn query(&self, query: &mut gst::QueryRef) -> bool { - use gst::QueryViewMut; - let settings = self.settings.lock().unwrap(); - - match query.view_mut() { - QueryViewMut::Caps(q) if settings.perform_internal_preroll => { - gst::debug!(CAT, imp = self, "Returning caps"); - let state = self.state.lock().unwrap(); - if let Some(info) = &state.info.as_ref() { - q.set_result(Some(&info.to_caps().unwrap())); - true - } else { - false - } - } - _ => { - drop(settings); - BaseSrcImplExt::parent_query(self, query) - } - } - } -} - -impl PushSrcImpl for ScapSrc { - fn create(&self, _: Option<&mut gst::BufferRef>) -> Result { - let rx = self.event_rx.lock().map_err(|_| gst::FlowError::Error)?; - let rx = rx.as_ref().ok_or(gst::FlowError::Error)?; - - loop { - match rx.recv().map_err(|_| gst::FlowError::Error)? { - Event::NewCaps(caps) => self - .obj() - .set_caps(&caps) - .map_err(|_| gst::FlowError::Error)?, - Event::Frame(buffer) => return Ok(CreateSuccess::NewBuffer(buffer)), - } - } - } -} diff --git a/scap-gstreamer/src/scapsrc/mod.rs b/scap-gstreamer/src/scapsrc/mod.rs deleted file mode 100644 index f096da2..0000000 --- a/scap-gstreamer/src/scapsrc/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2024-2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use gst::glib; -use gst::prelude::*; - -mod imp; - -glib::wrapper! { - pub struct ScapSrc(ObjectSubclass) @extends gst_base::PushSrc, gst_base::BaseSrc, gst::Element, gst::Object; -} - -pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { - gst::Element::register( - Some(plugin), - "scapsrc", - gst::Rank::NONE, - ScapSrc::static_type(), - ) -} diff --git a/scap/Cargo.toml b/scap/Cargo.toml deleted file mode 100644 index 52f9c22..0000000 --- a/scap/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "scap" -description = "Modern, high-performance screen capture library for Rust. Cross-platform." -version = "0.0.8" -edition = "2021" -license = "GPL-3.0-or-later" -authors = [ - "Siddharth ", - "Pranav ", - "Marcus L. Hanestad ", -] -readme = "README.md" -keywords = ["screen", "recording", "video", "capture", "media"] -categories = ["graphics", "multimedia", "multimedia::video"] - -[dependencies] -log = { workspace = true } -anyhow = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -windows-capture = "1.3.6" -windows = { version = "0.58", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_UI_HiDpi", - "Win32_UI_WindowsAndMessaging", -] } - -[target.'cfg(target_os = "macos")'.dependencies] -sysinfo = "0.30.0" -tao-core-video-sys = "0.2.0" -core-graphics-helmer-fork = "0.24.0" -screencapturekit = "0.2.8" -screencapturekit-sys = "0.2.8" -cocoa = "0.25.0" -objc = "0.2.7" - -[target.'cfg(target_os = "linux")'.dependencies] -pipewire = "0.8.0" -dbus = "0.9.7" -rand = { workspace = true } -xcb = { version = "1.4.0", features = ["randr", "xlib_xcb", "xfixes", "shm"] } -x11 = "2.21.0" -libc = "0.2" \ No newline at end of file diff --git a/scap/README.md b/scap/README.md deleted file mode 100644 index 2727310..0000000 --- a/scap/README.md +++ /dev/null @@ -1 +0,0 @@ -Fork of [https://github.com/CapSoftware/scap](https://github.com/CapSoftware/scap). diff --git a/scap/src/capturer/engine/linux/mod.rs b/scap/src/capturer/engine/linux/mod.rs deleted file mode 100644 index df5c024..0000000 --- a/scap/src/capturer/engine/linux/mod.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::env; - -use log::debug; -use wayland::WaylandCapturer; -use x11::X11Capturer; - -use anyhow::{bail, Result}; - -use crate::capturer::{OnFrameCb, Options}; - -use super::OnFormatChangedCb; - -mod wayland; -mod x11; - -pub trait LinuxCapturerImpl: Send + Sync { - fn start(&mut self); - fn stop(&mut self); -} - -pub struct LinuxCapturer { - pub imp: Box, -} - -impl LinuxCapturer { - pub fn new( - options: Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, - ) -> Result { - if env::var("WAYLAND_DISPLAY").is_ok() { - debug!("On wayland"); - Ok(Self { - imp: Box::new(WaylandCapturer::new(options, on_format_changed, on_frame)?), - }) - } else if env::var("DISPLAY").is_ok() { - debug!("On X11"); - Ok(Self { - imp: Box::new(X11Capturer::new(options, on_format_changed, on_frame)?), - }) - } else { - bail!("Unsupported platform. Could not detect Wayland or X11 displays") - } - } -} - -pub fn create_capturer( - options: Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, -) -> Result { - LinuxCapturer::new(options, on_format_changed, on_frame) -} diff --git a/scap/src/capturer/engine/linux/wayland.rs b/scap/src/capturer/engine/linux/wayland.rs deleted file mode 100644 index f953247..0000000 --- a/scap/src/capturer/engine/linux/wayland.rs +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::{ - mem::size_of, - sync::{ - atomic::{AtomicBool, AtomicU8}, - mpsc::{sync_channel, SyncSender}, - Arc, - }, - thread::JoinHandle, - time::Duration, -}; - -use log::{debug, error, warn}; -use pipewire::{self as pw, spa::buffer::DataType}; -use pw::{ - context::Context, - main_loop::MainLoop, - properties::properties, - spa::{ - self, - param::{ - format::{FormatProperties, MediaSubtype, MediaType}, - video::VideoFormat, - ParamType, - }, - pod::{Pod, Property}, - sys::{ - spa_buffer, spa_meta_header, SPA_META_Header, SPA_PARAM_META_size, SPA_PARAM_META_type, - }, - utils::{Direction, SpaTypes}, - }, - stream::{StreamRef, StreamState}, -}; - -use anyhow::Result; - -use crate::{ - capturer::{OnFormatChangedCb, OnFrameCb, Options}, - frame::FrameInfo, - targets::get_main_display, -}; - -use super::LinuxCapturerImpl; - -struct ListenerUserData { - pub stream_state_changed_to_error: Arc, - pub on_format_changed: OnFormatChangedCb, - pub on_frame: OnFrameCb, - pub base_time: Option, - pub format_height: usize, -} - -fn serialize_obj(obj: spa::pod::Object) -> Vec { - spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &spa::pod::Value::Object(obj), - ) - .unwrap() - .0 - .into_inner() -} - -fn param_changed_callback( - stream: &StreamRef, - user_data: &mut ListenerUserData, - id: u32, - param: Option<&Pod>, -) { - let Some(param) = param else { - return; - }; - if id != spa::param::ParamType::Format.as_raw() { - return; - } - let (media_type, media_subtype) = match spa::param::format_utils::parse_format(param) { - Ok(v) => v, - Err(_) => return, - }; - - if media_type != MediaType::Video || media_subtype != MediaSubtype::Raw { - return; - } - - let mut format = spa::param::video::VideoInfoRaw::new(); - format.parse(param).unwrap(); - - let color_format = match format.format() { - VideoFormat::RGBx => crate::frame::FrameFormat::RGBx, - VideoFormat::xBGR => crate::frame::FrameFormat::XBGR, - VideoFormat::BGRx => crate::frame::FrameFormat::BGRx, - VideoFormat::RGBA => crate::frame::FrameFormat::RGBA, - VideoFormat::BGRA => crate::frame::FrameFormat::BGRA, - _ => unreachable!(), - }; - - let new_info = FrameInfo { - format: color_format, - width: format.size().width, - height: format.size().height, - }; - - user_data.format_height = format.size().height as usize; - - debug!("New video info: {new_info:?}"); - - (user_data.on_format_changed)(new_info); - - let buf_obj = spa::pod::object!( - spa::utils::SpaTypes::ObjectParamBuffers, - spa::param::ParamType::Buffers, - spa::pod::Property { - key: spa::sys::SPA_PARAM_BUFFERS_dataType, - flags: spa::pod::PropertyFlags::empty(), - value: spa::pod::Value::Int( - (1 << spa::buffer::DataType::MemFd.as_raw()) - | (1 << spa::buffer::DataType::MemPtr.as_raw()) - ) - } - ); - - let buf_values = serialize_obj(buf_obj); - - let metas_obj = spa::pod::object!( - SpaTypes::ObjectParamMeta, - ParamType::Meta, - Property::new( - SPA_PARAM_META_type, - spa::pod::Value::Id(spa::utils::Id(SPA_META_Header)) - ), - Property::new( - SPA_PARAM_META_size, - spa::pod::Value::Int(size_of::() as i32) - ), - ); - - let metas_values = serialize_obj(metas_obj); - - let mut params = [ - spa::pod::Pod::from_bytes(&buf_values).unwrap(), - spa::pod::Pod::from_bytes(&metas_values).unwrap(), - ]; - - stream.update_params(&mut params).unwrap(); -} - -fn state_changed_callback( - _stream: &StreamRef, - user_data: &mut ListenerUserData, - _old: StreamState, - new: StreamState, -) { - if let StreamState::Error(e) = new { - error!("State changed to error({e})"); - user_data - .stream_state_changed_to_error - .store(true, std::sync::atomic::Ordering::SeqCst); - } -} - -unsafe fn get_timestamp(buffer: *mut spa_buffer) -> i64 { - let n_metas = (*buffer).n_metas; - if n_metas > 0 { - let mut meta_ptr = (*buffer).metas; - let metas_end = (*buffer).metas.wrapping_add(n_metas as usize); - while meta_ptr != metas_end { - if (*meta_ptr).type_ == SPA_META_Header { - let meta_header: &mut spa_meta_header = - &mut *((*meta_ptr).data as *mut spa_meta_header); - return meta_header.pts; - } - meta_ptr = meta_ptr.wrapping_add(1); - } - 0 - } else { - 0 - } -} - -fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { - let buffer = unsafe { stream.dequeue_raw_buffer() }; - if !buffer.is_null() { - 'outer: { - let buffer = unsafe { (*buffer).buffer }; - if buffer.is_null() { - break 'outer; - } - - let mut timestamp = unsafe { get_timestamp(buffer) } as u64; - - if let Some(base_time) = user_data.base_time { - timestamp -= base_time; - } else { - user_data.base_time = Some(timestamp); - timestamp = 0; - } - - let n_datas = unsafe { (*buffer).n_datas }; - if n_datas < 1 { - break 'outer; - } - - let datas = unsafe { (*buffer).datas as *mut spa::buffer::Data }; - if datas.is_null() { - error!("Data is null"); - break 'outer; - } - let data = unsafe { std::slice::from_raw_parts_mut(datas, n_datas as usize) }; - let data = &mut data[0]; - match data.type_() { - DataType::MemPtr => { - (user_data.on_frame)(timestamp, data.data().unwrap()); - } - DataType::MemFd => { - let fd: std::os::fd::RawFd = data.as_raw().fd as i32; - let offset = data.chunk().offset() as i64; - let stride = data.chunk().stride(); - let size = user_data.format_height * stride as usize; - - let ptr = unsafe { - libc::mmap( - std::ptr::null_mut(), - size, - libc::PROT_READ, - libc::MAP_SHARED, - fd, - offset, - ) - }; - - let data = unsafe { std::slice::from_raw_parts(ptr as *mut u8, size) }; - - (user_data.on_frame)(timestamp, data); - - unsafe { - libc::munmap(ptr, size); - } - } - _ => warn!( - "Got data of type: `{:?}`, ignoring because it's unsupported", - data.type_() - ), - } - } - } else { - error!("Out of buffers"); - } - - unsafe { stream.queue_raw_buffer(buffer) }; -} - -fn pipewire_capturer( - options: Options, - ready_sender: &SyncSender, - stream_id: u32, - capturer_state: Arc, - stream_state_changed_to_error: Arc, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, -) -> Result<()> { - pw::init(); - - let mainloop = MainLoop::new(None)?; - let context = Context::new(&mainloop)?; - let core = context.connect(None)?; - - let user_data = ListenerUserData { - stream_state_changed_to_error: Arc::clone(&stream_state_changed_to_error), - on_format_changed, - on_frame, - base_time: None, - format_height: 0, - }; - - let stream = pw::stream::Stream::new( - &core, - "scap", - properties! { - *pw::keys::MEDIA_TYPE => "Video", - *pw::keys::MEDIA_CATEGORY => "Capture", - *pw::keys::MEDIA_ROLE => "Screen", - }, - )?; - - let _listener = stream - .add_local_listener_with_user_data(user_data) - .state_changed(state_changed_callback) - .param_changed(param_changed_callback) - .process(process_callback) - .register()?; - - let fmt_obj = spa::pod::object!( - spa::utils::SpaTypes::ObjectParamFormat, - spa::param::ParamType::EnumFormat, - spa::pod::property!(FormatProperties::MediaType, Id, MediaType::Video), - spa::pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw), - spa::pod::property!( - FormatProperties::VideoFormat, - Choice, - Enum, - Id, - spa::param::video::VideoFormat::RGBA, // First element is discarded? - spa::param::video::VideoFormat::RGBA, - spa::param::video::VideoFormat::RGBx, - spa::param::video::VideoFormat::BGRx, - spa::param::video::VideoFormat::xBGR, - spa::param::video::VideoFormat::BGRA, - ), - spa::pod::property!( - FormatProperties::VideoSize, - Choice, - Range, - Rectangle, - spa::utils::Rectangle { - // Default - width: 128, - height: 128, - }, - spa::utils::Rectangle { - // Min - width: 1, - height: 1, - }, - spa::utils::Rectangle { - // Max - width: 4096, - height: 4096, - } - ), - spa::pod::property!( - FormatProperties::VideoMaxFramerate, - Choice, - Range, - Fraction, - spa::utils::Fraction { - // Default - num: options.fps, - denom: 1, - }, - spa::utils::Fraction { - // Min - num: 0, - denom: 1, - }, - spa::utils::Fraction { - // Max - num: options.fps, - denom: 1, - } - ), - ); - - let fmt_values = serialize_obj(fmt_obj); - - let mut params = [spa::pod::Pod::from_bytes(&fmt_values).unwrap()]; - - stream.connect( - Direction::Input, - Some(stream_id), - pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, - &mut params, - )?; - - ready_sender.send(true)?; - - while capturer_state.load(std::sync::atomic::Ordering::SeqCst) == 0 { - std::thread::sleep(Duration::from_millis(10)); - } - - let pw_loop = mainloop.loop_(); - - // User has called Capturer::start() and we start the main loop - while capturer_state.load(std::sync::atomic::Ordering::SeqCst) == 1 - && /* If the stream state got changed to `Error`, we exit. TODO: tell user that we exited */ - !stream_state_changed_to_error.load(std::sync::atomic::Ordering::SeqCst) - { - pw_loop.iterate(Duration::from_millis(100)); - } - - debug!("Finished"); - - Ok(()) -} - -pub struct WaylandCapturer { - capturer_join_handle: Option>>, - capturer_state: Arc, - stream_state_changed_to_error: Arc, - // The pipewire stream is deleted when the connection is dropped. - // That's why we keep it alive - _connection: Arc>, -} - -impl WaylandCapturer { - pub fn new( - options: Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, - ) -> Result { - let capturer_state = Arc::new(AtomicU8::new(0)); - let stream_state_changed_to_error = Arc::new(AtomicBool::new(false)); - - let (stream_id, connection) = match &options.target { - Some(target) => match target { - crate::Target::Display(display) => match &display.raw { - crate::targets::LinuxDisplay::Wayland { connection } => { - let stream_id = display.id; - (stream_id, Arc::clone(connection)) - } - crate::targets::LinuxDisplay::X11 { .. } => unreachable!(), - }, - _ => unreachable!(), - }, - None => { - let target = get_main_display()?; - match target.raw { - crate::targets::LinuxDisplay::Wayland { connection } => { - let stream_id = target.id; - (stream_id, Arc::clone(&connection)) - } - crate::targets::LinuxDisplay::X11 { .. } => unreachable!(), - } - } - }; - - let capturer_state_clone = Arc::clone(&capturer_state); - let stream_state_changed_to_error_clone = Arc::clone(&stream_state_changed_to_error); - let (ready_sender, ready_recv) = sync_channel(1); - let capturer_join_handle = std::thread::spawn(move || { - let res = pipewire_capturer( - options, - &ready_sender, - stream_id, - capturer_state_clone, - stream_state_changed_to_error_clone, - on_format_changed, - on_frame, - ); - if res.is_err() { - ready_sender.send(false)?; - } - res - }); - - if !ready_recv.recv()? { - panic!("Failed to setup capturer"); - } - - Ok(Self { - capturer_join_handle: Some(capturer_join_handle), - _connection: connection, - capturer_state, - stream_state_changed_to_error, - }) - } -} - -impl LinuxCapturerImpl for WaylandCapturer { - fn start(&mut self) { - self.capturer_state - .store(1, std::sync::atomic::Ordering::SeqCst); - } - - fn stop(&mut self) { - self.capturer_state - .store(2, std::sync::atomic::Ordering::SeqCst); - if let Some(handle) = self.capturer_join_handle.take() { - if let Err(e) = handle.join().unwrap() { - error!("Error occured capturing: {e}"); - } - } - self.capturer_state - .store(0, std::sync::atomic::Ordering::SeqCst); - self.stream_state_changed_to_error - .store(false, std::sync::atomic::Ordering::SeqCst); - } -} diff --git a/scap/src/capturer/engine/linux/x11.rs b/scap/src/capturer/engine/linux/x11.rs deleted file mode 100644 index 6860553..0000000 --- a/scap/src/capturer/engine/linux/x11.rs +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -// TODO: on gnome when a window is captured and made fullscreen/not fullscreen it crashes. - -use std::{ - sync::{ - atomic::{AtomicU8, Ordering}, - Arc, - }, - thread::JoinHandle, -}; - -use anyhow::{bail, Result}; - -use log::error; -use xcb::{x, Xid}; - -use crate::{ - capturer::{OnFormatChangedCb, OnFrameCb, Options}, - frame::{self, FrameInfo}, - targets::{linux::get_default_x_display, LinuxDisplay, LinuxWindow}, - Target, -}; - -use super::LinuxCapturerImpl; - -struct ShmBuf { - pub id: u32, - data: *mut u8, - pub size: usize, -} - -impl ShmBuf { - pub fn null() -> Self { - Self { - id: 0, - data: std::ptr::null_mut(), - size: 0, - } - } - - pub fn new(size: usize) -> Self { - // Last 9 bits is access permissions - let id = unsafe { libc::shmget(libc::IPC_PRIVATE, size, libc::IPC_CREAT | 0o6_0_0) }; - - if id == -1 { - todo!(); - } - - let data = unsafe { libc::shmat(id, std::ptr::null(), 0) as *mut u8 }; - - if data.is_null() { - todo!(); - } - - Self { - id: id as u32, - data, - size, - } - } - - pub fn slice(&self) -> &[u8] { - unsafe { std::slice::from_raw_parts(self.data, self.size) } - } - - pub fn slice_mut(&mut self) -> &mut [u8] { - unsafe { std::slice::from_raw_parts_mut(self.data, self.size) } - } -} - -impl Drop for ShmBuf { - fn drop(&mut self) { - unsafe { - libc::shmdt(self.data as *const libc::c_void); - libc::shmctl(self.id as i32, libc::IPC_RMID, std::ptr::null_mut()); - } - } -} - -fn current_time_nanos() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64 -} - -pub struct X11Capturer { - capturer_join_handle: Option>>, - capturer_state: Arc, -} - -#[allow(clippy::too_many_arguments)] -fn draw_cursor( - conn: &xcb::Connection, - img: &mut [u8], - win_x: i16, - win_y: i16, - win_width: i16, - win_height: i16, - is_win: bool, - win: &xcb::x::Window, -) -> Result<(), xcb::Error> { - let cursor_image_cookie = conn.send_request(&xcb::xfixes::GetCursorImage {}); - let cursor_image = conn.wait_for_reply(cursor_image_cookie)?; - - let win_x = win_x as i32; - let win_y = win_y as i32; - - let win_width = win_width as i32; - let win_height = win_height as i32; - - let mut cursor_x = cursor_image.x() as i32 - cursor_image.xhot() as i32; - let mut cursor_y = cursor_image.y() as i32 - cursor_image.yhot() as i32; - if is_win { - let disp = conn.get_raw_dpy(); - let mut ncursor_x = 0; - let mut ncursor_y = 0; - let mut child_return = 0; - if unsafe { - x11::xlib::XTranslateCoordinates( - disp, - x11::xlib::XDefaultRootWindow(disp), - win.resource_id() as u64, - cursor_image.x() as i32, - cursor_image.y() as i32, - &mut ncursor_x, - &mut ncursor_y, - &mut child_return, - ) - } == 0 - { - return Ok(()); - } - cursor_x = ncursor_x - cursor_image.xhot() as i32; - cursor_y = ncursor_y - cursor_image.yhot() as i32; - } - - if cursor_x >= win_width + win_x - || cursor_y >= win_height + win_y - || cursor_x < win_x - || cursor_y < win_y - { - return Ok(()); - } - - let x = cursor_x.max(win_x); - let y = cursor_y.max(win_y); - - let w = ((cursor_x + cursor_image.width() as i32).min(win_x + win_width) - x) as u32; - let h = ((cursor_y + cursor_image.height() as i32).min(win_y + win_height) - y) as u32; - - let c_off = (x - cursor_x) as u32; - let i_off: i32 = x - win_x; - - let stride: u32 = 4; - let mut cursor_idx: u32 = ((y - cursor_y) * cursor_image.width() as i32) as u32; - let mut image_idx: u32 = ((y - win_y) * win_width * stride as i32) as u32; - - for _ in 0..h { - cursor_idx += c_off; - image_idx += i_off as u32 * stride; - for _ in 0..w { - let cursor_pix = cursor_image.cursor_image()[cursor_idx as usize]; - let r = (cursor_pix & 0xFF) as u8; - let g = ((cursor_pix >> 8) & 0xFF) as u8; - let b = ((cursor_pix >> 16) & 0xFF) as u8; - let a = (cursor_pix >> 24) & 0xFF; - - let i = image_idx as usize; - if a == 0xFF { - img[i] = r; - img[i + 1] = g; - img[i + 2] = b; - } else if a > 0 { - let a = 255 - a; - img[i] = r + ((img[i] as u32 * a + 255 / 2) / 255) as u8; - img[i + 1] = g + ((img[i + 1] as u32 * a + 255 / 2) / 255) as u8; - img[i + 2] = b + ((img[i + 2] as u32 * a + 255 / 2) / 255) as u8; - } - - cursor_idx += 1; - image_idx += stride; - } - cursor_idx += cursor_image.width() as u32 - w - c_off; - image_idx += (win_width - w as i32 - i_off) as u32 * stride; - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn grab( - conn: &xcb::Connection, - target: &Target, - show_cursor: bool, - base_time: u64, - current_frame_info: &mut Option, - on_format_changed: &mut OnFormatChangedCb, - on_frame: &mut OnFrameCb, - shm_seg: &xcb::shm::Seg, - shm_buf: &mut ShmBuf, -) -> Result<(), xcb::Error> { - let (x, y, width, height, window, is_win) = match &target { - Target::Window(win) => { - let LinuxWindow::X11 { raw_handle } = win.raw else { - unreachable!(); - }; - let geom_cookie = conn.send_request(&x::GetGeometry { - drawable: x::Drawable::Window(raw_handle), - }); - let geom = conn.wait_for_reply(geom_cookie)?; - (0, 0, geom.width(), geom.height(), raw_handle, true) - } - Target::Display(disp) => { - let LinuxDisplay::X11 { - raw_handle, - width, - height, - x_offset, - y_offset, - } = disp.raw - else { - unreachable!(); - }; - (x_offset, y_offset, width, height, raw_handle, false) - } - }; - - let frame_size = width as usize * height as usize * 4; - - if shm_buf.size != frame_size { - if shm_buf.size != 0 { - conn.send_and_check_request(&xcb::shm::Detach { shmseg: *shm_seg })?; - } - - *shm_buf = ShmBuf::new(frame_size); - - conn.send_and_check_request(&xcb::shm::Attach { - shmseg: *shm_seg, - shmid: shm_buf.id, - read_only: false, - })?; - } - - let img_cookie = conn.send_request(&xcb::shm::GetImage { - drawable: x::Drawable::Window(window), - x, - y, - width, - height, - plane_mask: u32::MAX, - format: x::ImageFormat::ZPixmap as u8, - shmseg: *shm_seg, - offset: 0, - }); - - let img = conn.wait_for_reply(img_cookie)?; - - if img.size() as usize != shm_buf.size { - todo!(); - } - - let display_time = current_time_nanos() - base_time; - - if show_cursor { - draw_cursor( - conn, - shm_buf.slice_mut(), - x, - y, - width as i16, - height as i16, - is_win, - &window, - )?; - } - - let frame_info = FrameInfo { - format: frame::FrameFormat::BGRx, - width: width as u32, - height: height as u32, - }; - - if let Some(current_frame_info) = current_frame_info { - if *current_frame_info != frame_info { - (on_format_changed)(frame_info); - } - } else { - (on_format_changed)(frame_info); - } - - (on_frame)(display_time, shm_buf.slice()); - - Ok(()) -} - -fn query_xfixes_version(conn: &xcb::Connection) -> Result<(), xcb::Error> { - let cookie = conn.send_request(&xcb::xfixes::QueryVersion { - client_major_version: xcb::xfixes::MAJOR_VERSION, - client_minor_version: xcb::xfixes::MINOR_VERSION, - }); - let _ = conn.wait_for_reply(cookie)?; - Ok(()) -} - -impl X11Capturer { - pub fn new( - options: Options, - mut on_format_changed: OnFormatChangedCb, - mut on_frame: OnFrameCb, - ) -> Result { - let (conn, screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( - &[ - xcb::Extension::RandR, - xcb::Extension::XFixes, - xcb::Extension::Shm, - ], - &[], - )?; - query_xfixes_version(&conn)?; - let setup = conn.get_setup(); - let Some(screen) = setup.roots().nth(screen_num as usize) else { - bail!("Failed to get setup root"); - }; - - let target = match &options.target { - Some(t) => t.clone(), - None => Target::Display(get_default_x_display(&conn, screen)?), - }; - - let framerate = options.fps as f32; - let show_cursor = options.show_cursor; - let capturer_state = Arc::new(AtomicU8::new(0)); - let capturer_state_clone = Arc::clone(&capturer_state); - - let jh = std::thread::spawn(move || { - while capturer_state_clone.load(Ordering::Acquire) == 0 { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - let shm_seg = conn.generate_id::(); - let mut shm_buf = ShmBuf::null(); - - let base_time = current_time_nanos(); - let mut current_format_info = None; - let frame_time = std::time::Duration::from_secs_f32(1.0 / framerate); - while capturer_state_clone.load(Ordering::Acquire) == 1 { - let start = std::time::Instant::now(); - - // log::debug!("Grabbing..."); - - grab( - &conn, - &target, - show_cursor, - base_time, - &mut current_format_info, - &mut on_format_changed, - &mut on_frame, - &shm_seg, - &mut shm_buf, - )?; - - let elapsed = start.elapsed(); - if elapsed < frame_time { - std::thread::sleep(frame_time - start.elapsed()); - } - } - - Ok(()) - }); - - Ok(Self { - capturer_state, - capturer_join_handle: Some(jh), - }) - } -} - -impl LinuxCapturerImpl for X11Capturer { - fn start(&mut self) { - self.capturer_state.store(1, Ordering::Release); - } - - fn stop(&mut self) { - self.capturer_state.store(2, Ordering::Release); - if let Some(handle) = self.capturer_join_handle.take() { - if let Err(e) = handle.join().expect("Failed to join capturer thread") { - error!("Error occured capturing: {e}"); - } - } - } -} diff --git a/scap/src/capturer/engine/mac/apple_sys.rs b/scap/src/capturer/engine/mac/apple_sys.rs deleted file mode 100644 index 349f268..0000000 --- a/scap/src/capturer/engine/mac/apple_sys.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![allow(non_upper_case_globals)] - -pub use screencapturekit_sys::os_types::base::*; - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __CFDictionary { - _unused: [u8; 0], -} - -pub type CFDictionaryRef = *const __CFDictionary; -pub type CFTypeRef = *const ::std::os::raw::c_void; -pub type CFIndex = ::std::os::raw::c_long; -pub type CFNumberType = CFIndex; -pub type Boolean = ::std::os::raw::c_uchar; - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __CFNumber { - _unused: [u8; 0], -} - -#[allow(non_camel_case_types)] -pub type id = *mut objc::runtime::Object; - -#[repr(transparent)] -#[derive(Debug, Copy, Clone)] -pub struct NSString(pub id); - -pub type CFNumberRef = *const __CFNumber; -pub type SCStreamFrameInfo = NSString; -extern "C" { - pub fn CFDictionaryGetValue( - theDict: CFDictionaryRef, - key: *const ::std::os::raw::c_void, - ) -> *const ::std::os::raw::c_void; - pub fn CFNumberGetValue( - number: CFNumberRef, - theType: CFNumberType, - valuePtr: *mut ::std::os::raw::c_void, - ) -> Boolean; - pub fn CMTimeGetSeconds(time: CMTime) -> Float64; - pub static SCStreamFrameInfoStatus: SCStreamFrameInfo; -} -pub const CFNumberType_kCFNumberSInt64Type: CFNumberType = 4; -pub type NSInteger = ::std::os::raw::c_long; -pub type SCFrameStatus = NSInteger; -pub const SCFrameStatus_SCFrameStatusComplete: SCFrameStatus = 0; diff --git a/scap/src/capturer/engine/mac/mod.rs b/scap/src/capturer/engine/mac/mod.rs deleted file mode 100644 index a739589..0000000 --- a/scap/src/capturer/engine/mac/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::sync::atomic::AtomicBool; -use std::sync::mpsc; -use std::{cmp, sync::Arc}; - -use pixelformat::get_pts_in_nanoseconds; -use screencapturekit::{ - cm_sample_buffer::CMSampleBuffer, - sc_content_filter::{InitParams, SCContentFilter}, - sc_error_handler::StreamErrorHandler, - sc_output_handler::{SCStreamOutputType, StreamOutput}, - sc_shareable_content::SCShareableContent, - sc_stream::SCStream, - sc_stream_configuration::{PixelFormat, SCStreamConfiguration}, - sc_types::SCFrameStatus, -}; -use screencapturekit_sys::os_types::base::{CMTime, CMTimeScale}; -use screencapturekit_sys::os_types::geometry::{CGPoint, CGRect, CGSize}; - -use crate::frame::{Frame, FrameType}; -use crate::targets::Target; -use crate::{ - capturer::{Area, Options, Point, Resolution, Size}, - frame::BGRAFrame, - targets, -}; - -use super::ChannelItem; - -mod apple_sys; -mod pixel_buffer; -mod pixelformat; - -pub use pixel_buffer::PixelBuffer; - -struct ErrorHandler { - error_flag: Arc, -} - -impl StreamErrorHandler for ErrorHandler { - fn on_error(&self) { - error!("Screen capture error occurred."); - self.error_flag - .store(true, std::sync::atomic::Ordering::Relaxed); - } -} - -pub struct Capturer { - pub tx: mpsc::Sender, -} - -impl Capturer { - pub fn new(tx: mpsc::Sender) -> Self { - Capturer { tx } - } -} - -impl StreamOutput for Capturer { - fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) { - self.tx.send((sample, of_type)).unwrap_or(()); - } -} - -pub fn create_capturer( - options: &Options, - tx: mpsc::Sender, - error_flag: Arc, -) -> SCStream { - // If no target is specified, capture the main display - let target = options - .target - .clone() - .unwrap_or_else(|| Target::Display(targets::get_main_display())); - - let sc_shareable_content = SCShareableContent::current(); - - let params = match target { - Target::Window(window) => { - // Get SCWindow from window id - let sc_window = sc_shareable_content - .windows - .into_iter() - .find(|sc_win| sc_win.window_id == window.id) - .unwrap(); - - // Return a DesktopIndependentWindow - // https://developer.apple.com/documentation/screencapturekit/sccontentfilter/3919804-init - InitParams::DesktopIndependentWindow(sc_window) - } - Target::Display(display) => { - // Get SCDisplay from display id - let sc_display = sc_shareable_content - .displays - .into_iter() - .find(|sc_dis| sc_dis.display_id == display.id) - .unwrap(); - - match &options.excluded_targets { - None => InitParams::Display(sc_display), - Some(excluded_targets) => { - let excluded_windows = sc_shareable_content - .windows - .into_iter() - .filter(|window| { - excluded_targets - .iter() - .any(|excluded_target| match excluded_target { - Target::Window(excluded_window) => { - excluded_window.id == window.window_id - } - _ => false, - }) - }) - .collect(); - - InitParams::DisplayExcludingWindows(sc_display, excluded_windows) - } - } - } - }; - - let filter = SCContentFilter::new(params); - - let crop_area = get_crop_area(options); - - let source_rect = CGRect { - origin: CGPoint { - x: crop_area.origin.x, - y: crop_area.origin.y, - }, - size: CGSize { - width: crop_area.size.width, - height: crop_area.size.height, - }, - }; - - let pixel_format = match options.output_type { - FrameType::YUVFrame => PixelFormat::YCbCr420v, - FrameType::BGR0 => PixelFormat::ARGB8888, - FrameType::RGB => PixelFormat::ARGB8888, - FrameType::BGRAFrame => PixelFormat::ARGB8888, - }; - - let [width, height] = get_output_frame_size(options); - - let stream_config = SCStreamConfiguration { - width, - height, - source_rect, - pixel_format, - shows_cursor: options.show_cursor, - minimum_frame_interval: CMTime { - value: 1, - timescale: options.fps as CMTimeScale, - epoch: 0, - flags: 1, - }, - ..Default::default() - }; - - let mut stream = SCStream::new(filter, stream_config, ErrorHandler { error_flag }); - stream.add_output(Capturer::new(tx), SCStreamOutputType::Screen); - - stream -} - -pub fn get_output_frame_size(options: &Options) -> [u32; 2] { - let target = options - .target - .clone() - .unwrap_or_else(|| Target::Display(targets::get_main_display())); - - let scale_factor = targets::get_scale_factor(&target); - let source_rect = get_crop_area(options); - - // Calculate the output height & width based on the required resolution - // Output width and height need to be multiplied by scale (or dpi) - let mut output_width = (source_rect.size.width as u32) * (scale_factor as u32); - let mut output_height = (source_rect.size.height as u32) * (scale_factor as u32); - // 1200x800 - match options.output_resolution { - Resolution::Captured => {} - _ => { - let [resolved_width, resolved_height] = options - .output_resolution - .value((source_rect.size.width as f32) / (source_rect.size.height as f32)); - // 1280 x 853 - output_width = cmp::min(output_width, resolved_width); - output_height = cmp::min(output_height, resolved_height); - } - } - - output_width -= output_width % 2; - output_height -= output_height % 2; - - [output_width, output_height] -} - -pub fn get_crop_area(options: &Options) -> Area { - let target = options - .target - .clone() - .unwrap_or_else(|| Target::Display(targets::get_main_display())); - - let (width, height) = targets::get_target_dimensions(&target); - - options - .crop_area - .as_ref() - .map(|val| { - let input_width = val.size.width + (val.size.width % 2.0); - let input_height = val.size.height + (val.size.height % 2.0); - - Area { - origin: Point { - x: val.origin.x, - y: val.origin.y, - }, - size: Size { - width: input_width, - height: input_height, - }, - } - }) - .unwrap_or_else(|| Area { - origin: Point { x: 0.0, y: 0.0 }, - size: Size { - width: width as f64, - height: height as f64, - }, - }) -} - -pub fn process_sample_buffer( - sample: CMSampleBuffer, - of_type: SCStreamOutputType, - output_type: FrameType, -) -> Option { - if let SCStreamOutputType::Screen = of_type { - let frame_status = &sample.frame_status; - - match frame_status { - SCFrameStatus::Complete | SCFrameStatus::Started => unsafe { - return Some(match output_type { - FrameType::YUVFrame => { - let yuvframe = pixelformat::create_yuv_frame(sample).unwrap(); - Frame::YUVFrame(yuvframe) - } - FrameType::RGB => { - let rgbframe = pixelformat::create_rgb_frame(sample).unwrap(); - Frame::RGB(rgbframe) - } - FrameType::BGR0 => { - let bgrframe = pixelformat::create_bgr_frame(sample).unwrap(); - Frame::BGR0(bgrframe) - } - FrameType::BGRAFrame => { - let bgraframe = pixelformat::create_bgra_frame(sample).unwrap(); - Frame::BGRA(bgraframe) - } - }); - }, - SCFrameStatus::Idle => { - // Quick hack - just send an empty frame, and the caller can figure out how to handle it - if let FrameType::BGRAFrame = output_type { - return Some(Frame::BGRA(BGRAFrame { - display_time: get_pts_in_nanoseconds(&sample), - width: 0, - height: 0, - data: vec![], - })); - } - } - _ => {} - } - } - - None -} diff --git a/scap/src/capturer/engine/mac/pixel_buffer.rs b/scap/src/capturer/engine/mac/pixel_buffer.rs deleted file mode 100644 index ae0b4aa..0000000 --- a/scap/src/capturer/engine/mac/pixel_buffer.rs +++ /dev/null @@ -1,214 +0,0 @@ -use core::slice; -use core_video_sys::{ - CVPixelBufferGetBaseAddress, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRow, - CVPixelBufferGetBytesPerRowOfPlane, CVPixelBufferGetHeight, CVPixelBufferGetHeightOfPlane, - CVPixelBufferGetPlaneCount, CVPixelBufferGetWidth, CVPixelBufferGetWidthOfPlane, - CVPixelBufferLockBaseAddress, CVPixelBufferRef, CVPixelBufferUnlockBaseAddress, -}; -use screencapturekit::{cm_sample_buffer::CMSampleBuffer, sc_types::SCFrameStatus}; -use screencapturekit_sys::cm_sample_buffer_ref::CMSampleBufferGetImageBuffer; -use std::{ops::Deref, sync::mpsc}; - -use crate::capturer::{engine::ChannelItem, RawCapturer}; - -pub struct PixelBuffer { - display_time: u64, - width: usize, - height: usize, - bytes_per_row: usize, - buffer: CMSampleBuffer, -} - -impl PixelBuffer { - pub fn display_time(&self) -> u64 { - self.display_time - } - - pub fn width(&self) -> usize { - self.width - } - - pub fn height(&self) -> usize { - self.height - } - - pub fn buffer(&self) -> &CMSampleBuffer { - &self.buffer - } - - pub fn bytes_per_row(&self) -> usize { - self.bytes_per_row - } - - pub fn data(&self) -> PixelBufferData { - unsafe { - let pixel_buffer = sample_buffer_to_pixel_buffer(&self.buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - let base_address = CVPixelBufferGetBaseAddress(pixel_buffer); - - PixelBufferData { - buffer: pixel_buffer, - data: slice::from_raw_parts( - base_address as *mut _, - self.bytes_per_row * self.height, - ), - } - } - } - - pub fn planes(&self) -> Vec { - unsafe { - let pixel_buffer = sample_buffer_to_pixel_buffer(&self.buffer); - let count = CVPixelBufferGetPlaneCount(pixel_buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - (0..count) - .map(|i| Plane { - buffer: pixel_buffer, - width: CVPixelBufferGetWidthOfPlane(pixel_buffer, i), - height: CVPixelBufferGetHeightOfPlane(pixel_buffer, i), - bytes_per_row: CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, i), - index: i, - }) - .collect() - } - } - - pub(crate) fn new(item: ChannelItem) -> Option { - unsafe { - let display_time = pixel_buffer_display_time(&item.0); - let pixel_buffer = sample_buffer_to_pixel_buffer(&item.0); - let (width, height) = pixel_buffer_bounds(pixel_buffer); - - match item.0.frame_status { - SCFrameStatus::Complete | SCFrameStatus::Started | SCFrameStatus::Idle => { - Some(Self { - display_time, - width, - height, - bytes_per_row: pixel_buffer_bytes_per_row(pixel_buffer), - buffer: item.0, - }) - } - _ => None, - } - } - } -} - -impl Into for PixelBuffer { - fn into(self) -> CMSampleBuffer { - self.buffer - } -} - -#[derive(Debug)] -pub struct Plane { - buffer: CVPixelBufferRef, - index: usize, - width: usize, - height: usize, - bytes_per_row: usize, -} - -impl Plane { - pub fn width(&self) -> usize { - self.width - } - - pub fn height(&self) -> usize { - self.height - } - - pub fn bytes_per_row(&self) -> usize { - self.bytes_per_row - } - - pub fn data(&self) -> PixelBufferData { - unsafe { - CVPixelBufferLockBaseAddress(self.buffer, 0); - - let base_address = CVPixelBufferGetBaseAddressOfPlane(self.buffer, self.index); - - PixelBufferData { - buffer: self.buffer, - data: slice::from_raw_parts( - base_address as *mut _, - self.bytes_per_row * self.height, - ), - } - } - } -} - -pub struct PixelBufferData<'a> { - buffer: CVPixelBufferRef, - data: &'a [u8], -} - -impl<'a> Deref for PixelBufferData<'a> { - type Target = [u8]; - - fn deref(&self) -> &'a Self::Target { - self.data - } -} - -impl<'a> Drop for PixelBufferData<'a> { - fn drop(&mut self) { - unsafe { CVPixelBufferUnlockBaseAddress(self.buffer, 0) }; - } -} - -impl RawCapturer<'_> { - #[cfg(target_os = "macos")] - pub fn get_next_pixel_buffer(&self) -> Result { - use std::time::Duration; - - let capturer = &self.capturer; - - loop { - let error_flag = capturer - .engine - .error_flag - .load(std::sync::atomic::Ordering::Relaxed); - if error_flag { - return Err(mpsc::RecvError); - } - - let res = match capturer.rx.recv_timeout(Duration::from_millis(10)) { - Ok(v) => Ok(v), - Err(mpsc::RecvTimeoutError::Timeout) => continue, - Err(mpsc::RecvTimeoutError::Disconnected) => Err(mpsc::RecvError), - }?; - - if let Some(frame) = PixelBuffer::new(res) { - return Ok(frame); - } - } - } -} - -pub unsafe fn sample_buffer_to_pixel_buffer(sample_buffer: &CMSampleBuffer) -> CVPixelBufferRef { - let buffer_ref = &(*sample_buffer.sys_ref); - - CMSampleBufferGetImageBuffer(buffer_ref) as CVPixelBufferRef -} - -pub unsafe fn pixel_buffer_bounds(pixel_buffer: CVPixelBufferRef) -> (usize, usize) { - let width = CVPixelBufferGetWidth(pixel_buffer); - let height = CVPixelBufferGetHeight(pixel_buffer); - - (width, height) -} - -pub unsafe fn pixel_buffer_bytes_per_row(pixel_buffer: CVPixelBufferRef) -> usize { - CVPixelBufferGetBytesPerRow(pixel_buffer) -} - -pub unsafe fn pixel_buffer_display_time(sample_buffer: &CMSampleBuffer) -> u64 { - sample_buffer.sys_ref.get_presentation_timestamp().value as u64 -} diff --git a/scap/src/capturer/engine/mac/pixelformat.rs b/scap/src/capturer/engine/mac/pixelformat.rs deleted file mode 100644 index 949e4f5..0000000 --- a/scap/src/capturer/engine/mac/pixelformat.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::{mem, slice}; - -use screencapturekit::cm_sample_buffer::CMSampleBuffer; -use screencapturekit_sys::cm_sample_buffer_ref::CMSampleBufferGetSampleAttachmentsArray; - -use super::{ - apple_sys::*, - pixel_buffer::{pixel_buffer_bounds, sample_buffer_to_pixel_buffer}, -}; -use crate::frame::{ - convert_bgra_to_rgb, get_cropped_data, remove_alpha_channel, BGRAFrame, BGRFrame, RGBFrame, - YUVFrame, -}; -use core_graphics_helmer_fork::display::{CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef}; -use core_video_sys::{ - CVPixelBufferGetBaseAddress, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRow, - CVPixelBufferGetBytesPerRowOfPlane, CVPixelBufferLockBaseAddress, - CVPixelBufferUnlockBaseAddress, -}; - -// Returns a frame's presentation timestamp in nanoseconds since an arbitrary start time. -// This is typically yielded from a monotonic clock started on system boot. -pub fn get_pts_in_nanoseconds(sample_buffer: &CMSampleBuffer) -> u64 { - let pts = sample_buffer.sys_ref.get_presentation_timestamp(); - - let seconds = unsafe { CMTimeGetSeconds(pts) }; - - (seconds * 1_000_000_000.).trunc() as u64 -} - -pub unsafe fn create_yuv_frame(sample_buffer: CMSampleBuffer) -> Option { - // Check that the frame status is complete - let buffer_ref = &(*sample_buffer.sys_ref); - { - let attachments = CMSampleBufferGetSampleAttachmentsArray(buffer_ref, 0); - if attachments.is_null() || CFArrayGetCount(attachments as CFArrayRef) == 0 { - return None; - } - let attachment = CFArrayGetValueAtIndex(attachments as CFArrayRef, 0) as CFDictionaryRef; - let frame_status_ref = CFDictionaryGetValue( - attachment as CFDictionaryRef, - SCStreamFrameInfoStatus.0 as _, - ) as CFTypeRef; - if frame_status_ref.is_null() { - return None; - } - let mut frame_status: i64 = 0; - let result = CFNumberGetValue( - frame_status_ref as _, - CFNumberType_kCFNumberSInt64Type, - mem::transmute(&mut frame_status), - ); - if result == 0 { - return None; - } - if frame_status != SCFrameStatus_SCFrameStatusComplete { - return None; - } - } - - let display_time = get_pts_in_nanoseconds(&sample_buffer); - let pixel_buffer = sample_buffer_to_pixel_buffer(&sample_buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - let (width, height) = pixel_buffer_bounds(pixel_buffer); - if width == 0 || height == 0 { - return None; - } - - let luminance_bytes_address = CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0); - let luminance_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0); - let luminance_bytes = slice::from_raw_parts( - luminance_bytes_address as *mut u8, - height * luminance_stride, - ) - .to_vec(); - - let chrominance_bytes_address = CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1); - let chrominance_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1); - let chrominance_bytes = slice::from_raw_parts( - chrominance_bytes_address as *mut u8, - height * chrominance_stride / 2, - ) - .to_vec(); - - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - - YUVFrame { - display_time, - width: width as i32, - height: height as i32, - luminance_bytes, - luminance_stride: luminance_stride as i32, - chrominance_bytes, - chrominance_stride: chrominance_stride as i32, - } - .into() -} - -pub unsafe fn create_bgr_frame(sample_buffer: CMSampleBuffer) -> Option { - let pixel_buffer = sample_buffer_to_pixel_buffer(&sample_buffer); - let display_time = get_pts_in_nanoseconds(&sample_buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - let (width, height) = pixel_buffer_bounds(pixel_buffer); - if width == 0 || height == 0 { - return None; - } - - let base_address = CVPixelBufferGetBaseAddress(pixel_buffer); - let bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer); - - let data = slice::from_raw_parts(base_address as *mut u8, bytes_per_row * height).to_vec(); - - let cropped_data = get_cropped_data( - data, - (bytes_per_row / 4) as i32, - height as i32, - width as i32, - ); - - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - - Some(BGRFrame { - display_time, - width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu - height: height as i32, - data: remove_alpha_channel(cropped_data), - }) -} - -pub unsafe fn create_bgra_frame(sample_buffer: CMSampleBuffer) -> Option { - let pixel_buffer = sample_buffer_to_pixel_buffer(&sample_buffer); - let display_time = get_pts_in_nanoseconds(&sample_buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - let (width, height) = pixel_buffer_bounds(pixel_buffer); - if width == 0 || height == 0 { - return None; - } - - let base_address = CVPixelBufferGetBaseAddress(pixel_buffer); - let bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer); - - let mut data: Vec = vec![]; - - for i in 0..height { - let start = (base_address as *mut u8).wrapping_add(i * bytes_per_row); - data.extend_from_slice(slice::from_raw_parts(start, 4 * width)); - } - - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - - Some(BGRAFrame { - display_time, - width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu - height: height as i32, - data, - }) -} - -pub unsafe fn create_rgb_frame(sample_buffer: CMSampleBuffer) -> Option { - let pixel_buffer = sample_buffer_to_pixel_buffer(&sample_buffer); - let display_time = get_pts_in_nanoseconds(&sample_buffer); - - CVPixelBufferLockBaseAddress(pixel_buffer, 0); - - let (width, height) = pixel_buffer_bounds(pixel_buffer); - if width == 0 || height == 0 { - return None; - } - - let base_address = CVPixelBufferGetBaseAddress(pixel_buffer); - let bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer); - - let data = slice::from_raw_parts(base_address as *mut u8, bytes_per_row * height).to_vec(); - - let cropped_data = get_cropped_data( - data, - (bytes_per_row / 4) as i32, - height as i32, - width as i32, - ); - - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - - Some(RGBFrame { - display_time, - width: width as i32, // width does not give accurate results - https://stackoverflow.com/questions/19587185/cvpixelbuffergetbytesperrow-for-cvimagebufferref-returns-unexpected-wrong-valu - height: height as i32, - data: convert_bgra_to_rgb(cropped_data), - }) -} diff --git a/scap/src/capturer/engine/mod.rs b/scap/src/capturer/engine/mod.rs deleted file mode 100644 index 499071f..0000000 --- a/scap/src/capturer/engine/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use anyhow::Result; - -use super::{OnFormatChangedCb, OnFrameCb, Options}; - -#[cfg(target_os = "macos")] -pub mod mac; - -#[cfg(target_os = "windows")] -mod win; - -#[cfg(target_os = "linux")] -pub(crate) mod linux; - -#[cfg(target_os = "macos")] -pub type ChannelItem = ( - screencapturekit::cm_sample_buffer::CMSampleBuffer, - screencapturekit::sc_output_handler::SCStreamOutputType, -); - -pub struct Engine { - #[cfg(target_os = "macos")] - mac: screencapturekit::sc_stream::SCStream, - #[cfg(target_os = "macos")] - error_flag: std::sync::Arc, - - #[cfg(target_os = "windows")] - win: win::WCStream, - - #[cfg(target_os = "linux")] - linux: linux::LinuxCapturer, -} - -impl Engine { - pub fn new( - options: Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, - ) -> Result { - #[cfg(target_os = "macos")] - { - let error_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let mac = mac::create_capturer(options, tx, error_flag.clone()); - - Ok(Engine { mac, error_flag }) - } - - #[cfg(target_os = "windows")] - { - let win = win::create_capturer(&options, on_format_changed, on_frame); - Ok(Engine { win }) - } - - #[cfg(target_os = "linux")] - { - let linux = linux::create_capturer(options, on_format_changed, on_frame)?; - Ok(Engine { linux }) - } - } - - pub fn start(&mut self) { - #[cfg(target_os = "macos")] - { - // self.mac.add_output(Capturer::new(tx)); - self.mac.start_capture().expect("Failed to start capture"); - } - - #[cfg(target_os = "windows")] - { - self.win.start_capture(); - } - - #[cfg(target_os = "linux")] - { - self.linux.imp.start(); - } - } - - pub fn stop(&mut self) { - #[cfg(target_os = "macos")] - { - self.mac.stop_capture().expect("Failed to stop capture"); - } - - #[cfg(target_os = "windows")] - { - self.win.stop_capture(); - } - - #[cfg(target_os = "linux")] - { - self.linux.imp.stop(); - } - } - - // pub fn process_channel_item(&self, data: ChannelItem) -> Option { - // #[cfg(target_os = "macos")] - // { - // mac::process_sample_buffer(data.0, data.1, self.options.output_type) - // } - // #[cfg(not(target_os = "macos"))] - // Some(data) - // } -} diff --git a/scap/src/capturer/engine/win/mod.rs b/scap/src/capturer/engine/win/mod.rs deleted file mode 100644 index f2d1caa..0000000 --- a/scap/src/capturer/engine/win/mod.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::{ - capturer::{OnFormatChangedCb, OnFrameCb, Options}, - targets::{self, Target}, -}; -use log::debug; -use windows_capture::capture::Context; -use windows_capture::{ - capture::{CaptureControl, GraphicsCaptureApiHandler}, - frame::Frame as WCFrame, - graphics_capture_api::InternalCaptureControl, - monitor::Monitor as WCMonitor, - settings::{ColorFormat, CursorCaptureSettings, DrawBorderSettings, Settings as WCSettings}, - window::Window as WCWindow, -}; - -struct Capturer { - info: crate::frame::FrameInfo, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, - base_time: u64, -} - -#[derive(Clone)] -enum Settings { - Window(WCSettings), - Display(WCSettings), -} - -pub struct WCStream { - settings: Settings, - capture_control: Option>>, -} - -impl GraphicsCaptureApiHandler for Capturer { - type Flags = FlagStruct; - type Error = Box; - - fn new(context: Context) -> Result { - let mut on_format_changed = context.flags.on_format_changed.lock().unwrap(); - let mut on_frame = context.flags.on_frame.lock().unwrap(); - - Ok(Self { - info: crate::frame::FrameInfo { - format: crate::frame::FrameFormat::BGRA, - width: 0, - height: 0, - }, - on_format_changed: on_format_changed.take().unwrap(), - on_frame: on_frame.take().unwrap(), - base_time: 0, - }) - } - - fn on_frame_arrived( - &mut self, - frame: &mut WCFrame, - _: InternalCaptureControl, - ) -> Result<(), Self::Error> { - let (width, height) = (frame.width(), frame.height()); - - let this_info = crate::frame::FrameInfo { - format: crate::frame::FrameFormat::BGRA, - width, - height, - }; - - if self.info != this_info { - (self.on_format_changed)(this_info.clone()); - self.info = this_info; - } - - let mut pts = frame.timespan().Duration as u64 * 100; - if self.base_time == 0 { - self.base_time = pts; - } - pts -= self.base_time; - let mut frame_buffer = frame.buffer().unwrap(); - let raw_frame_buffer = frame_buffer.as_raw_buffer(); - (self.on_frame)(pts, raw_frame_buffer); - - Ok(()) - } - - fn on_closed(&mut self) -> Result<(), Self::Error> { - debug!("Closed"); - Ok(()) - } -} - -impl WCStream { - pub fn start_capture(&mut self) { - if self.capture_control.is_some() { - return; - } - - let cc = match &self.settings { - Settings::Display(st) => Capturer::start_free_threaded(st.to_owned()).unwrap(), - Settings::Window(st) => Capturer::start_free_threaded(st.to_owned()).unwrap(), - }; - - self.capture_control = Some(cc) - } - - pub fn stop_capture(&mut self) { - let capture_control = self.capture_control.take().unwrap(); - let _ = capture_control.stop(); - } -} - -#[derive(Clone)] -struct FlagStruct { - pub on_format_changed: std::sync::Arc>>, - pub on_frame: std::sync::Arc>>, -} - -pub fn create_capturer( - options: &Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, -) -> WCStream { - let target = options - .target - .clone() - .unwrap_or_else(|| Target::Display(targets::get_main_display().unwrap())); - - let color_format = ColorFormat::Bgra8; - - let show_cursor = match options.show_cursor { - true => CursorCaptureSettings::WithCursor, - false => CursorCaptureSettings::WithoutCursor, - }; - - let settings = match target { - Target::Display(display) => Settings::Display(WCSettings::new( - WCMonitor::from_raw_hmonitor(display.raw_handle.0), - show_cursor, - DrawBorderSettings::Default, - color_format, - FlagStruct { - on_format_changed: std::sync::Arc::new(std::sync::Mutex::new(Some( - on_format_changed, - ))), - on_frame: std::sync::Arc::new(std::sync::Mutex::new(Some(on_frame))), - }, - )), - Target::Window(window) => Settings::Window(WCSettings::new( - WCWindow::from_raw_hwnd(window.raw_handle.0), - show_cursor, - DrawBorderSettings::Default, - color_format, - FlagStruct { - on_format_changed: std::sync::Arc::new(std::sync::Mutex::new(Some( - on_format_changed, - ))), - on_frame: std::sync::Arc::new(std::sync::Mutex::new(Some(on_frame))), - }, - )), - }; - - WCStream { - settings, - capture_control: None, - } -} diff --git a/scap/src/capturer/mod.rs b/scap/src/capturer/mod.rs deleted file mode 100644 index a282228..0000000 --- a/scap/src/capturer/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -pub mod engine; - -use anyhow::{bail, Result}; - -use crate::{frame::FrameInfo, has_permission, is_supported, targets::Target}; - -/// Presentation timestamp -pub type Pts = u64; - -pub type OnFormatChangedCb = Box; -pub type OnFrameCb = Box; - -#[derive(Debug, Clone, Copy, Default)] -pub enum Resolution { - _480p, - _720p, - _1080p, - _1440p, - _2160p, - _4320p, - - #[default] - Captured, -} - -#[allow(dead_code)] -impl Resolution { - fn value(&self, aspect_ratio: f32) -> [u32; 2] { - match *self { - Resolution::_480p => [640, (640_f32 / aspect_ratio).floor() as u32], - Resolution::_720p => [1280, (1280_f32 / aspect_ratio).floor() as u32], - Resolution::_1080p => [1920, (1920_f32 / aspect_ratio).floor() as u32], - Resolution::_1440p => [2560, (2560_f32 / aspect_ratio).floor() as u32], - Resolution::_2160p => [3840, (3840_f32 / aspect_ratio).floor() as u32], - Resolution::_4320p => [7680, (7680_f32 / aspect_ratio).floor() as u32], - Resolution::Captured => { - panic!(".value should not be called when Resolution type is Captured") - } - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct Point { - pub x: f64, - pub y: f64, -} - -#[derive(Debug, Default, Clone)] -pub struct Size { - pub width: f64, - pub height: f64, -} -#[derive(Debug, Default, Clone)] -pub struct Area { - pub origin: Point, - pub size: Size, -} - -/// Options passed to the screen capturer -#[derive(Debug, Default, Clone)] -pub struct Options { - pub fps: u32, - pub show_cursor: bool, - pub show_highlight: bool, - pub target: Option, - pub output_resolution: Resolution, -} - -/// Screen capturer class -pub struct Capturer { - engine: engine::Engine, -} - -impl Capturer { - /// Build a new [Capturer] instance with the provided options - pub fn build( - options: Options, - on_format_changed: OnFormatChangedCb, - on_frame: OnFrameCb, - ) -> Result { - if !is_supported() { - bail!("Unsupported platform"); - } - - if !has_permission() { - bail!("Permission not granted"); - } - - let engine = engine::Engine::new(options, on_format_changed, on_frame)?; - - Ok(Capturer { engine }) - } - - /// Start capturing the frames - pub fn start_capture(&mut self) { - self.engine.start(); - } - - /// Stop the capturer - pub fn stop_capture(&mut self) { - self.engine.stop(); - } -} diff --git a/scap/src/frame.rs b/scap/src/frame.rs deleted file mode 100644 index 69c6ee7..0000000 --- a/scap/src/frame.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -// TODO: dma buf (https://docs.pipewire.org/page_dma_buf.html) - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum FrameFormat { - RGBx, - XBGR, - BGRx, - BGRA, - RGBA, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FrameInfo { - pub format: FrameFormat, - pub width: u32, - pub height: u32, - // TODO: rotation -} diff --git a/scap/src/lib.rs b/scap/src/lib.rs deleted file mode 100644 index bbeab33..0000000 --- a/scap/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -// TODO: handle transforms -// TODO: on error callback - -pub mod capturer; -pub mod frame; -mod targets; -mod utils; - -pub use targets::get_all_targets; -pub use targets::Target; -pub(crate) use utils::has_permission; -pub(crate) use utils::is_supported; - -#[cfg(target_os = "macos")] -pub mod engine { - pub use crate::capturer::engine::mac; -} diff --git a/scap/src/targets/linux/mod.rs b/scap/src/targets/linux/mod.rs deleted file mode 100644 index 697412d..0000000 --- a/scap/src/targets/linux/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::{ - ffi::{CStr, CString, NulError}, - sync::{Arc, Mutex}, -}; - -use anyhow::{bail, Context, Result}; - -use crate::targets::{self, LinuxDisplay, LinuxWindow}; - -use super::{Display, Target}; - -use x11::xlib::{XFreeStringList, XGetTextProperty, XTextProperty, XmbTextPropertyToTextList}; -use xcb::{ - randr::{GetCrtcInfo, GetOutputInfo, GetOutputPrimary, GetScreenResources}, - x::{self, GetPropertyReply, Screen}, - Xid, -}; - -fn get_atom(conn: &xcb::Connection, atom_name: &str) -> Result { - let cookie = conn.send_request(&x::InternAtom { - only_if_exists: true, - name: atom_name.as_bytes(), - }); - Ok(conn.wait_for_reply(cookie)?.atom()) -} - -fn get_property( - conn: &xcb::Connection, - win: x::Window, - prop: x::Atom, - typ: x::Atom, - length: u32, -) -> Result { - let cookie = conn.send_request(&x::GetProperty { - delete: false, - window: win, - property: prop, - r#type: typ, - long_offset: 0, - long_length: length, - }); - conn.wait_for_reply(cookie) -} - -fn decode_compound_text( - conn: &xcb::Connection, - value: &[u8], - client: &xcb::x::Window, - ttype: xcb::x::Atom, -) -> Result { - let display = conn.get_raw_dpy(); - assert!(!display.is_null()); - - let c_string = CString::new(value.to_vec())?; - let mut text_prop = XTextProperty { - value: std::ptr::null_mut(), - encoding: 0, - format: 0, - nitems: 0, - }; - let res = unsafe { - XGetTextProperty( - display, - client.resource_id() as u64, - &mut text_prop, - x::ATOM_WM_NAME.resource_id() as u64, - ) - }; - if res == 0 || text_prop.nitems == 0 { - return Ok(String::from("n/a")); - } - - let xname = XTextProperty { - value: c_string.as_ptr() as *mut u8, - encoding: ttype.resource_id() as u64, - format: 8, - nitems: text_prop.nitems, - }; - let mut list: *mut *mut i8 = std::ptr::null_mut(); - let mut count: i32 = 0; - let result = unsafe { XmbTextPropertyToTextList(display, &xname, &mut list, &mut count) }; - if result < 1 || list.is_null() || count < 1 { - Ok(String::from("n/a")) - } else { - let title = unsafe { CStr::from_ptr(*list).to_string_lossy().into_owned() }; - unsafe { XFreeStringList(list) }; - Ok(title) - } -} - -fn get_x11_targets() -> Result> { - let (conn, _screen_num) = - xcb::Connection::connect_with_xlib_display_and_extensions(&[xcb::Extension::RandR], &[])?; - let setup = conn.get_setup(); - let screens = setup.roots(); - - let wm_client_list = get_atom(&conn, "_NET_CLIENT_LIST")?; - assert!(wm_client_list != x::ATOM_NONE, "EWMH not supported"); - - let atom_net_wm_name = get_atom(&conn, "_NET_WM_NAME")?; - let atom_text = get_atom(&conn, "TEXT")?; - let atom_utf8_string = get_atom(&conn, "UTF8_STRING")?; - let atom_compound_text = get_atom(&conn, "COMPOUND_TEXT")?; - - let mut targets = Vec::new(); - for screen in screens { - let window_list = get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100)?; - - for client in window_list.value::() { - let cr = get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096)?; - if !cr.value::().is_empty() { - targets.push(Target::Window(crate::targets::Window { - id: 0, - title: String::from_utf8(cr.value().to_vec()) - .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))?, - raw: LinuxWindow::X11 { - raw_handle: *client, - }, - })); - continue; - } - - let reply = get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096)?; - let value: &[u8] = reply.value(); - if !value.is_empty() { - let ttype = reply.r#type(); - let title = - if ttype == x::ATOM_STRING || ttype == atom_utf8_string || ttype == atom_text { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - } else if ttype == atom_compound_text { - decode_compound_text(&conn, value, client, ttype) - .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))? - } else { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - }; - - targets.push(Target::Window(crate::targets::Window { - id: 0, - title, - raw: LinuxWindow::X11 { - raw_handle: *client, - }, - })); - continue; - } - targets.push(Target::Window(crate::targets::Window { - id: 0, - title: String::from("n/a"), - raw: LinuxWindow::X11 { - raw_handle: *client, - }, - })); - } - - let resources = conn.send_request(&GetScreenResources { - window: screen.root(), - }); - let resources = conn.wait_for_reply(resources)?; - for output in resources.outputs() { - let info = conn.send_request(&GetOutputInfo { - output: *output, - config_timestamp: 0, - }); - let info = conn.wait_for_reply(info)?; - if info.connection() == xcb::randr::Connection::Connected { - let crtc = info.crtc(); - let crtc_info = conn.send_request(&GetCrtcInfo { - crtc, - config_timestamp: 0, - }); - let crtc_info = conn.wait_for_reply(crtc_info)?; - let title = String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); - targets.push(Target::Display(crate::targets::Display { - id: crtc.resource_id(), - title, - raw: LinuxDisplay::X11 { - width: crtc_info.width(), - height: crtc_info.height(), - x_offset: crtc_info.x(), - y_offset: crtc_info.y(), - raw_handle: screen.root(), - }, - })); - } - } - } - - Ok(targets) -} - -pub fn get_all_targets() -> Result> { - if std::env::var("WAYLAND_DISPLAY").is_ok() { - Ok(vec![targets::Target::Display(get_main_display()?)]) - } else if std::env::var("DISPLAY").is_ok() { - get_x11_targets() - } else { - bail!("Unsupported platform. Could not detect Wayland or X11 displays") - } -} - -pub(crate) fn get_default_x_display(conn: &xcb::Connection, screen: &Screen) -> Result { - let primary_display_cookie = conn.send_request(&GetOutputPrimary { - window: screen.root(), - }); - let primary_display = conn.wait_for_reply(primary_display_cookie)?; - let info_cookie = conn.send_request(&GetOutputInfo { - output: primary_display.output(), - config_timestamp: 0, - }); - let info = conn.wait_for_reply(info_cookie)?; - let crtc = info.crtc(); - let crtc_info_cookie = conn.send_request(&GetCrtcInfo { - crtc, - config_timestamp: 0, - }); - let crtc_info = conn.wait_for_reply(crtc_info_cookie)?; - Ok(Display { - id: crtc.resource_id(), - title: String::from_utf8(info.name().to_vec()).unwrap_or(String::from("default")), - raw: LinuxDisplay::X11 { - width: crtc_info.width(), - height: crtc_info.height(), - x_offset: crtc_info.x(), - y_offset: crtc_info.y(), - raw_handle: screen.root(), - }, - }) -} - -mod portal; -pub fn get_main_display() -> Result { - if std::env::var("WAYLAND_DISPLAY").is_ok() { - let connection = - dbus::blocking::Connection::new_session().expect("Failed to create dbus connection"); - let stream_id = portal::ScreenCastPortal::new(&connection) - .show_cursor(true) - .context("Unsupported cursor mode")? - .create_stream() - .context("Failed to get screencast stream")? - .pw_node_id(); - Ok(Display { - id: stream_id, - title: "Display".to_owned(), - raw: LinuxDisplay::Wayland { - connection: Arc::new(Mutex::new(connection)), - }, - }) - } else if std::env::var("DISPLAY").is_ok() { - let (conn, screen_num) = - xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[])?; - let setup = conn.get_setup(); - let Some(screen) = setup.roots().nth(screen_num as usize) else { - bail!("Unable to get x11 root"); - }; - get_default_x_display(&conn, screen) - } else { - bail!("Unsupported platform. Could not detect Wayland or X11 displays") - } -} diff --git a/scap/src/targets/linux/portal.rs b/scap/src/targets/linux/portal.rs deleted file mode 100644 index 8cc2185..0000000 --- a/scap/src/targets/linux/portal.rs +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::{ - sync::{atomic::AtomicBool, Arc, Mutex}, - time::Duration, -}; - -use anyhow::{bail, Result}; - -use dbus::{ - arg::{self, PropMap, RefArg, Variant}, - blocking::{Connection, Proxy}, - message::MatchRule, - strings::{BusName, Interface}, -}; - -// This code was autogenerated with `dbus-codegen-rust -d org.freedesktop.portal.Desktop -p /org/freedesktop/portal/desktop -f org.freedesktop.portal.ScreenCast`, see https://github.com/diwic/dbus-rs -// { -use dbus::blocking; - -#[allow(unused)] -trait OrgFreedesktopPortalScreenCast { - fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; - fn select_sources( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result, dbus::Error>; - fn start( - &self, - session_handle: dbus::Path, - parent_window: &str, - options: arg::PropMap, - ) -> Result, dbus::Error>; - fn open_pipe_wire_remote( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result; - fn available_source_types(&self) -> Result; - fn available_cursor_modes(&self) -> Result; - fn version(&self) -> Result; -} - -impl> OrgFreedesktopPortalScreenCast - for blocking::Proxy<'_, C> -{ - fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "CreateSession", - (options,), - ) - .map(|r: (dbus::Path<'static>,)| r.0) - } - - fn select_sources( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "SelectSources", - (session_handle, options), - ) - .map(|r: (dbus::Path<'static>,)| r.0) - } - - fn start( - &self, - session_handle: dbus::Path, - parent_window: &str, - options: arg::PropMap, - ) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "Start", - (session_handle, parent_window, options), - ) - .map(|r: (dbus::Path<'static>,)| r.0) - } - - fn open_pipe_wire_remote( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "OpenPipeWireRemote", - (session_handle, options), - ) - .map(|r: (arg::OwnedFd,)| r.0) - } - - fn available_source_types(&self) -> Result { - ::get( - self, - "org.freedesktop.portal.ScreenCast", - "AvailableSourceTypes", - ) - } - - fn available_cursor_modes(&self) -> Result { - ::get( - self, - "org.freedesktop.portal.ScreenCast", - "AvailableCursorModes", - ) - } - - fn version(&self) -> Result { - ::get( - self, - "org.freedesktop.portal.ScreenCast", - "version", - ) - } -} -// } - -// This code was autogenerated with `dbus-codegen-rust --file org.freedesktop.portal.Request.xml`, see https://github.com/diwic/dbus-rs -// { -#[allow(unused)] -trait OrgFreedesktopPortalRequest { - fn close(&self) -> Result<(), dbus::Error>; -} - -#[derive(Debug)] -pub struct OrgFreedesktopPortalRequestResponse { - pub response: u32, - pub results: arg::PropMap, -} - -impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { - fn append(&self, i: &mut arg::IterAppend) { - arg::RefArg::append(&self.response, i); - arg::RefArg::append(&self.results, i); - } -} - -impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { - fn read(i: &mut arg::Iter) -> Result { - Ok(OrgFreedesktopPortalRequestResponse { - response: i.read()?, - results: i.read()?, - }) - } -} - -impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { - const NAME: &'static str = "Response"; - const INTERFACE: &'static str = "org.freedesktop.portal.Request"; -} - -impl> OrgFreedesktopPortalRequest - for blocking::Proxy<'_, C> -{ - fn close(&self) -> Result<(), dbus::Error> { - self.method_call("org.freedesktop.portal.Request", "Close", ()) - } -} -// } - -type Response = Option; - -#[derive(Debug)] -#[allow(dead_code)] -pub struct StreamVardict { - id: Option, - position: Option<(i32, i32)>, - size: Option<(i32, i32)>, - source_type: Option, - mapping_id: Option, -} - -#[derive(Debug)] -#[allow(unused)] -pub struct Stream(u32, StreamVardict); - -impl Stream { - pub fn pw_node_id(&self) -> u32 { - self.0 - } - - pub fn from_dbus(stream: &Variant>) -> Option { - let mut stream = stream.as_iter()?.next()?.as_iter()?; - let pipewire_node_id = stream.next()?.as_iter()?.next()?.as_u64()?; - - Some(Self( - pipewire_node_id as u32, - StreamVardict { - id: None, - position: None, - size: None, - source_type: None, - mapping_id: None, - }, - )) - } -} - -macro_rules! match_response { - ( $code:expr ) => { - match $code { - 0 => {} - 1 => bail!("User cancelled the interaction"), - 2 => bail!("The user interaction was ended in some other way"), - _ => unreachable!(), - } - }; -} - -pub struct ScreenCastPortal<'a> { - proxy: Proxy<'a, &'a Connection>, - token: String, - cursor_mode: u32, -} - -impl<'a> ScreenCastPortal<'a> { - pub fn new(connection: &'a Connection) -> Self { - let proxy = connection.with_proxy( - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - Duration::from_secs(4), - ); - - let token = format!("scap_{}", rand::random::()); - - Self { - proxy, - token, - cursor_mode: 1, - } - } - - fn create_session_args(&self) -> arg::PropMap { - let mut map = arg::PropMap::new(); - map.insert( - String::from("handle_token"), - Variant(Box::new(self.token.clone())), - ); - map.insert( - String::from("session_handle_token"), - Variant(Box::new(self.token.clone())), - ); - map - } - - fn select_sources_args(&self) -> Result { - let mut map = arg::PropMap::new(); - map.insert( - String::from("handle_token"), - Variant(Box::new(self.token.clone())), - ); - map.insert( - String::from("types"), - Variant(Box::new(self.proxy.available_source_types()?)), - ); - map.insert(String::from("multiple"), Variant(Box::new(false))); - map.insert( - String::from("cursor_mode"), - Variant(Box::new(self.cursor_mode)), - ); - Ok(map) - } - - fn handle_req_response( - connection: &Connection, - path: dbus::Path<'static>, - iterations: usize, - timeout: Duration, - response: Arc>, - ) -> Result<(), dbus::Error> { - let got_response = Arc::new(AtomicBool::new(false)); - let got_response_clone = Arc::clone(&got_response); - - let mut rule = MatchRule::new(); - rule.path = Some(path); - rule.msg_type = Some(dbus::MessageType::Signal); - rule.sender = Some(BusName::from("org.freedesktop.portal.Desktop")); - rule.interface = Some(Interface::from("org.freedesktop.portal.Request")); - connection.add_match( - rule, - move |res: OrgFreedesktopPortalRequestResponse, _chuh, _msg| { - let mut response = response.lock().expect("Failed to lock response mutex"); - *response = Some(res); - got_response_clone.store(true, std::sync::atomic::Ordering::Relaxed); - false - }, - )?; - - for _ in 0..iterations { - connection.process(timeout)?; - - if got_response.load(std::sync::atomic::Ordering::Relaxed) { - break; - } - } - - Ok(()) - } - - fn create_session(&self) -> Result { - let request_handle = self.proxy.create_session(self.create_session_args())?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 100, - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock().unwrap().take() { - match_response!(res.response); - match res - .results - .get("session_handle") - .map(|h| h.0.as_str().map(String::from)) - { - Some(h) => { - let p = dbus::Path::from(match h { - Some(p) => p, - None => bail!("Invalid session_handle received"), - }); - - return Ok(p); - } - None => bail!("Did not get session handle"), - } - } - - bail!("Did not get response") - } - - fn select_sources(&self, session_handle: dbus::Path) -> Result<()> { - let request_handle = self - .proxy - .select_sources(session_handle, self.select_sources_args()?)?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 1200, // Wait 2 min - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock().unwrap().take() { - match_response!(res.response); - return Ok(()); - } - - bail!("Did not get response") - } - - fn start(&self, session_handle: dbus::Path) -> Result { - let request_handle = self.proxy.start(session_handle, "", PropMap::new())?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 100, // Wait 10 s - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock().unwrap().take() { - match_response!(res.response); - match res.results.get("streams") { - Some(s) => match Stream::from_dbus(s) { - Some(s) => return Ok(s), - None => bail!("Failed to extract stream properties"), - }, - None => bail!("Did not get any streams"), - } - } - - bail!("Did not get response") - } - - pub fn create_stream(&self) -> Result { - let session_handle = self.create_session()?; - self.select_sources(session_handle.clone())?; - self.start(session_handle) - } - - pub fn show_cursor(mut self, mode: bool) -> Result { - let available_modes = self.proxy.available_cursor_modes()?; - if mode && available_modes & 2 == 2 { - self.cursor_mode = 2; - return Ok(self); - } - if !mode && available_modes & 1 == 1 { - self.cursor_mode = 1; - return Ok(self); - } - - bail!("Unsupported cursor mode") - } -} diff --git a/scap/src/targets/mac/mod.rs b/scap/src/targets/mac/mod.rs deleted file mode 100644 index 0b80684..0000000 --- a/scap/src/targets/mac/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -use cocoa::appkit::{NSApp, NSScreen}; -use cocoa::base::{id, nil}; -use cocoa::foundation::{NSRect, NSString, NSUInteger}; -use core_graphics_helmer_fork::display::{CGDirectDisplayID, CGDisplay, CGMainDisplayID}; -use core_graphics_helmer_fork::window::CGWindowID; -use objc::{msg_send, sel, sel_impl}; -use screencapturekit::sc_shareable_content::SCShareableContent; - -use super::{Display, Target}; - -fn get_display_name(display_id: CGDirectDisplayID) -> String { - unsafe { - // Get all screens - let screens: id = NSScreen::screens(nil); - let count: u64 = msg_send![screens, count]; - - for i in 0..count { - let screen: id = msg_send![screens, objectAtIndex: i]; - let device_description: id = msg_send![screen, deviceDescription]; - let display_id_number: id = msg_send![device_description, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")]; - let display_id_number: u32 = msg_send![display_id_number, unsignedIntValue]; - - if display_id_number == display_id { - let localized_name: id = msg_send![screen, localizedName]; - let name: *const i8 = msg_send![localized_name, UTF8String]; - return std::ffi::CStr::from_ptr(name) - .to_string_lossy() - .into_owned(); - } - } - - format!("Unknown Display {}", display_id) - } -} - -pub fn get_all_targets() -> Vec { - let mut targets: Vec = Vec::new(); - - let content = SCShareableContent::current(); - - // Add displays to targets - for display in content.displays { - let id: CGDirectDisplayID = display.display_id; - let raw_handle = CGDisplay::new(id); - let title = get_display_name(id); - - let target = Target::Display(super::Display { - id, - title, - raw_handle, - }); - - targets.push(target); - } - - // Add windows to targets - for window in content.windows { - if window.title.is_some() { - let id = window.window_id; - let title = window.title.expect("Window title not found"); - let raw_handle: CGWindowID = id; - - let target = Target::Window(super::Window { - id, - title, - raw_handle, - }); - targets.push(target); - } - } - - targets -} - -pub fn get_main_display() -> Display { - let id = unsafe { CGMainDisplayID() }; - let title = get_display_name(id); - - Display { - id, - title, - raw_handle: CGDisplay::new(id), - } -} - -pub fn get_scale_factor(target: &Target) -> f64 { - match target { - Target::Window(window) => unsafe { - let cg_win_id = window.raw_handle; - let ns_app: id = NSApp(); - let ns_window: id = msg_send![ns_app, windowWithWindowNumber: cg_win_id as NSUInteger]; - let scale_factor: f64 = msg_send![ns_window, backingScaleFactor]; - scale_factor - }, - Target::Display(display) => { - let mode = display.raw_handle.display_mode().unwrap(); - (mode.pixel_width() / mode.width()) as f64 - } - } -} - -pub fn get_target_dimensions(target: &Target) -> (u64, u64) { - match target { - Target::Window(window) => unsafe { - let cg_win_id = window.raw_handle; - let ns_app: id = NSApp(); - let ns_window: id = msg_send![ns_app, windowWithWindowNumber: cg_win_id as NSUInteger]; - let frame: NSRect = msg_send![ns_window, frame]; - (frame.size.width as u64, frame.size.height as u64) - }, - Target::Display(display) => { - let mode = display.raw_handle.display_mode().unwrap(); - (mode.width(), mode.height()) - } - } -} diff --git a/scap/src/targets/mod.rs b/scap/src/targets/mod.rs deleted file mode 100644 index 02946eb..0000000 --- a/scap/src/targets/mod.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use anyhow::Result; -use std::fmt::Debug; - -#[cfg(target_os = "linux")] -use std::sync::{Arc, Mutex}; - -#[cfg(target_os = "macos")] -mod mac; - -#[cfg(target_os = "windows")] -mod win; - -#[cfg(target_os = "linux")] -pub(crate) mod linux; - -#[cfg(target_os = "linux")] -#[derive(Debug, Clone)] -pub(crate) enum LinuxWindow { - #[allow(dead_code)] - Wayland, - X11 { - raw_handle: xcb::x::Window, - }, -} - -#[cfg(target_os = "linux")] -#[derive(Clone)] -pub(crate) enum LinuxDisplay { - Wayland { - connection: Arc>, - }, - X11 { - raw_handle: xcb::x::Window, - width: u16, - height: u16, - x_offset: i16, - y_offset: i16, - }, -} - -#[cfg(target_os = "linux")] -impl Debug for LinuxDisplay { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LinuxDisplay::Wayland { .. } => f - .debug_struct("LinuxDisplay::Wayland") - .field("connection", &"{...}") - .finish(), - LinuxDisplay::X11 { - raw_handle, - width, - height, - x_offset, - y_offset, - } => f - .debug_struct("LinuxDisplay::X11") - .field("raw_handle", &raw_handle) - .field("width", width) - .field("height", height) - .field("x_offset", x_offset) - .field("y_offset", y_offset) - .finish(), - } - } -} - -#[derive(Debug, Clone)] -pub struct Window { - pub id: u32, - pub title: String, - - #[cfg(target_os = "windows")] - pub raw_handle: windows::Win32::Foundation::HWND, - - #[cfg(target_os = "macos")] - pub raw_handle: core_graphics_helmer_fork::window::CGWindowID, - - #[cfg(target_os = "linux")] - pub(crate) raw: LinuxWindow, -} - -#[derive(Debug, Clone)] -pub struct Display { - pub id: u32, - pub title: String, - - #[cfg(target_os = "windows")] - pub raw_handle: windows::Win32::Graphics::Gdi::HMONITOR, - - #[cfg(target_os = "macos")] - pub raw_handle: core_graphics_helmer_fork::display::CGDisplay, - - #[cfg(target_os = "linux")] - pub(crate) raw: LinuxDisplay, -} - -#[derive(Debug, Clone)] -pub enum Target { - Window(Window), - Display(Display), -} - -impl Target { - pub fn title(&self) -> String { - match self { - Target::Window(window) => window.title.clone(), - Target::Display(display) => display.title.clone(), - } - } -} - -/// Returns a list of targets that can be captured -pub fn get_all_targets() -> Result> { - #[cfg(target_os = "macos")] - return mac::get_all_targets(); - - #[cfg(target_os = "windows")] - return win::get_all_targets(); - - #[cfg(target_os = "linux")] - return linux::get_all_targets(); -} - -#[allow(dead_code)] -pub fn get_main_display() -> Result { - #[cfg(target_os = "macos")] - return mac::get_main_display(); - - #[cfg(target_os = "windows")] - return win::get_main_display(); - - #[cfg(target_os = "linux")] - return linux::get_main_display(); -} diff --git a/scap/src/targets/win/mod.rs b/scap/src/targets/win/mod.rs deleted file mode 100644 index 6f4efac..0000000 --- a/scap/src/targets/win/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::{Display, Target}; -use windows::Win32::{Foundation::HWND, Graphics::Gdi::HMONITOR}; -use windows_capture::{monitor::Monitor, window::Window}; - -use anyhow::{Context, Result}; - -pub fn get_all_targets() -> Result> { - let mut targets: Vec = Vec::new(); - - // Add displays to targets - let displays = Monitor::enumerate().context("Failed to enumerate monitors")?; - for display in displays { - let id = display.as_raw_hmonitor() as u32; - let title = display - .device_name() - .context("Failed to get monitor name")?; - - let target = Target::Display(super::Display { - id, - title, - raw_handle: HMONITOR(display.as_raw_hmonitor()), - }); - targets.push(target); - } - - // Add windows to targets - let windows = Window::enumerate().context("Failed to enumerate windows")?; - for window in windows { - let id = window.as_raw_hwnd() as u32; - let title = window.title().unwrap().to_string(); - - let target = Target::Window(super::Window { - id, - title, - raw_handle: HWND(window.as_raw_hwnd()), - }); - targets.push(target); - } - - Ok(targets) -} - -pub fn get_main_display() -> Result { - let display = Monitor::primary().context("Failed to get primary monitor")?; - let id = display.as_raw_hmonitor() as u32; - - Ok(Display { - id, - title: display - .device_name() - .context("Failed to get monitor name")?, - raw_handle: HMONITOR(display.as_raw_hmonitor()), - }) -} diff --git a/scap/src/utils/mac/mod.rs b/scap/src/utils/mac/mod.rs deleted file mode 100644 index ae3adc4..0000000 --- a/scap/src/utils/mac/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use core_graphics_helmer_fork::access::ScreenCaptureAccess; -use sysinfo::System; - -pub fn has_permission() -> bool { - ScreenCaptureAccess.preflight() -} - -pub fn request_permission() -> bool { - ScreenCaptureAccess.request() -} - -pub fn is_supported() -> bool { - let os_version = System::os_version() - .expect("Failed to get macOS version") - .as_bytes() - .to_vec(); - - let min_version: Vec = "12.3\n".as_bytes().to_vec(); - - os_version >= min_version -} diff --git a/scap/src/utils/mod.rs b/scap/src/utils/mod.rs deleted file mode 100644 index 55098d7..0000000 --- a/scap/src/utils/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -#[cfg(target_os = "macos")] -mod mac; - -#[cfg(target_os = "windows")] -mod win; - -/// Checks if process has permission to capture the screen -pub fn has_permission() -> bool { - #[cfg(target_os = "macos")] - return mac::has_permission(); - - #[cfg(any(target_os = "windows", target_os = "linux"))] - return true; -} - -/// Prompts user to grant screen capturing permission to current process -#[allow(dead_code)] -pub fn request_permission() -> bool { - #[cfg(target_os = "macos")] - return mac::request_permission(); - - #[cfg(any(target_os = "windows", target_os = "linux"))] - return true; -} - -/// Checks if scap is supported on the current system -pub fn is_supported() -> bool { - #[cfg(target_os = "macos")] - return mac::is_supported(); - - #[cfg(target_os = "windows")] - return win::is_supported(); - - #[cfg(target_os = "linux")] - return true; -} diff --git a/scap/src/utils/win/mod.rs b/scap/src/utils/win/mod.rs deleted file mode 100644 index 810d051..0000000 --- a/scap/src/utils/win/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -use windows_capture::graphics_capture_api::GraphicsCaptureApi; - -pub fn is_supported() -> bool { - GraphicsCaptureApi::is_supported().expect("Failed to check support") -} diff --git a/sender/Cargo.toml b/sender/Cargo.toml index d73fff9..6e8bb3e 100644 --- a/sender/Cargo.toml +++ b/sender/Cargo.toml @@ -4,17 +4,23 @@ version = "0.1.0" edition = "2024" [dependencies] -env_logger = { workspace = true } -log = { workspace = true } -gst = { workspace = true } -common = { path = "../common", features = ["video", "sender"] } +env_logger.workspace = true +log.workspace = true +gst.workspace = true +common = { path = "../common", features = ["sender"] } fcast-lib = { path = "../fcast-lib" } -scap-gstreamer = { path = "../scap-gstreamer" } -slint = { version = "1.11.0", default-features = false, features = ["accessibility", "backend-winit", "compat-1-2", "renderer-femtovg", "std"] } -anyhow = { workspace = true } -crossbeam-channel = { workspace = true } -mdns-sd = { version = "0.13.9", default-features = false, features = ["log"] } -flume = { version = "0.11.1", default-features = false, features = ["eventual-fairness"] } +slint.workspace = true +anyhow.workspace = true +crossbeam-channel.workspace = true +mdns-sd = "0.13.9" +flume = "0.11.1" +tokio.workspace = true [build-dependencies] -slint-build = { workspace = true } \ No newline at end of file +slint-build.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +ashpd = { version = "0.11.0", default-features = false, features = ["async-std"] } # `tokio` breaks slint +xcb = { version = "1.5.0", features = ["randr", "xlib_xcb"] } +x11 = "2.21.0" +pipewire = "0.8.0" \ No newline at end of file diff --git a/sender/src/main.rs b/sender/src/main.rs index 8c87d7f..6a18c95 100644 --- a/sender/src/main.rs +++ b/sender/src/main.rs @@ -15,34 +15,342 @@ // You should have received a copy of the GNU General Public License // along with OpenMirroring. If not, see . -use anyhow::Result; +use anyhow::{Context, Result, bail}; use common::sender::session::{Session, SessionEvent}; -use common::video::opengl::SlintOpenGLSink; use fcast_lib::packet::Packet; use log::{debug, error, trace}; +use std::cell::Cell; +use std::ffi::CString; +use std::ffi::{CStr, NulError}; +use std::os::fd::AsRawFd; +use std::rc::Rc; +use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, atomic}; -use std::thread::sleep; use std::time::{Duration, Instant}; +use tokio::runtime::Runtime; use std::net::{IpAddr, SocketAddr}; -use std::rc::Rc; -use common::sender::pipeline; +#[cfg(target_os = "linux")] +use ashpd::desktop::{ + PersistMode, + screencast::{CursorMode, Screencast, SourceType}, +}; + +#[cfg(target_os = "linux")] +use x11::xlib::{XFreeStringList, XGetTextProperty, XTextProperty, XmbTextPropertyToTextList}; +#[cfg(target_os = "linux")] +use xcb::{ + Xid, + randr::{GetCrtcInfo, GetOutputInfo, GetScreenResources}, + x::{self, GetPropertyReply}, +}; + +use common::sender::pipeline::{self, SourceConfig}; slint::include_modules!(); pub type ProducerId = String; #[derive(Debug)] -pub enum Event { +pub enum AudioSource { + #[cfg(target_os = "linux")] + Pipewire { name: String, id: u32 }, +} + +impl AudioSource { + #[cfg(target_os = "linux")] + pub fn display_name(&self) -> String { + match self { + AudioSource::Pipewire { name, .. } => name.clone(), + } + } +} + +#[cfg(target_os = "linux")] +pub fn get_audio_devices() -> anyhow::Result> { + use std::cell::RefCell; + + let mainloop = + pipewire::main_loop::MainLoop::new(None).context("failed to create PipeWire main loop")?; + let context = + pipewire::context::Context::new(&mainloop).context("failed to create PipeWire context")?; + let core = context + .connect(None) + .context("failed to connect to PipeWire core")?; + let registry = core + .get_registry() + .context("failed to get PipeWire registry")?; + + let done = Rc::new(Cell::new(false)); + let done_clone = done.clone(); + let loop_clone = mainloop.clone(); + let pending = core.sync(0).expect("sync failed"); + let pw_sources = Rc::new(RefCell::new(Vec::new())); + let pw_sources_clone = pw_sources.clone(); + + let _listener_core = core + .add_listener_local() + .done(move |id, seq| { + if id == pipewire::core::PW_ID_CORE && seq == pending { + done_clone.set(true); + loop_clone.quit(); + } + }) + .register(); + let _listener_reg = registry + .add_listener_local() + .global(move |global| { + let props = global.props.as_ref().unwrap(); + let Some(mut name) = props + .get(&pipewire::keys::NODE_DESCRIPTION) + .or_else(|| props.get(&pipewire::keys::NODE_NICK)) + .or_else(|| props.get(&pipewire::keys::NODE_NAME)) + .map(|n| n.to_string()) + else { + return; + }; + + let Some(media_class) = props.get(&pipewire::keys::MEDIA_CLASS) else { + return; + }; + + if !media_class.contains("Audio") { + return; + } + + if media_class == "Audio/Source" { + name.push_str(" (source)"); + } else if media_class == "Audio/Sink" { + name.push_str(" (sink)"); + } + + if props + .get(&pipewire::keys::MEDIA_CLASS) + .map(|class| class.contains("Audio")) + != Some(true) + { + debug!("PipeWire object `{name}` is not audio class"); + return; + } + + pw_sources_clone.borrow_mut().push(AudioSource::Pipewire { + name, + id: global.id, + }); + }) + .register(); + + while !done.get() { + mainloop.run(); + } + + Ok(pw_sources.take()) +} + +#[derive(Debug)] +enum VideoSource { + #[cfg(target_os = "linux")] + PipeWire { + node_id: u32, + #[allow(dead_code)] + fd: i32, // TODO: does not work, why not? + }, + #[cfg(target_os = "linux")] + XWindow { id: u32, name: String }, + #[cfg(target_os = "linux")] + XDisplay { + id: u32, + width: u16, + height: u16, + x_offset: i16, + y_offset: i16, + name: String, + }, +} + +impl VideoSource { + #[cfg(target_os = "linux")] + pub fn display_name(&self) -> String { + match self { + VideoSource::PipeWire { .. } => "PipeWire Video Source".to_owned(), + VideoSource::XWindow { name, .. } => name.clone(), + VideoSource::XDisplay { name, .. } => name.clone(), + } + } +} + +#[cfg(target_os = "linux")] +fn get_atom(conn: &xcb::Connection, atom_name: &str) -> Result { + let cookie = conn.send_request(&x::InternAtom { + only_if_exists: true, + name: atom_name.as_bytes(), + }); + Ok(conn.wait_for_reply(cookie)?.atom()) +} + +#[cfg(target_os = "linux")] +fn get_property( + conn: &xcb::Connection, + win: x::Window, + prop: x::Atom, + typ: x::Atom, + length: u32, +) -> Result { + let cookie = conn.send_request(&x::GetProperty { + delete: false, + window: win, + property: prop, + r#type: typ, + long_offset: 0, + long_length: length, + }); + conn.wait_for_reply(cookie) +} + +#[cfg(target_os = "linux")] +fn decode_compound_text( + conn: &xcb::Connection, + value: &[u8], + client: &xcb::x::Window, + ttype: xcb::x::Atom, +) -> Result { + let display = conn.get_raw_dpy(); + assert!(!display.is_null()); + + let c_string = CString::new(value.to_vec())?; + let mut text_prop = XTextProperty { + value: std::ptr::null_mut(), + encoding: 0, + format: 0, + nitems: 0, + }; + let res = unsafe { + XGetTextProperty( + display, + client.resource_id() as u64, + &mut text_prop, + x::ATOM_WM_NAME.resource_id() as u64, + ) + }; + if res == 0 || text_prop.nitems == 0 { + return Ok(String::from("n/a")); + } + + let xname = XTextProperty { + value: c_string.as_ptr() as *mut u8, + encoding: ttype.resource_id() as u64, + format: 8, + nitems: text_prop.nitems, + }; + let mut list: *mut *mut i8 = std::ptr::null_mut(); + let mut count: i32 = 0; + let result = unsafe { XmbTextPropertyToTextList(display, &xname, &mut list, &mut count) }; + if result < 1 || list.is_null() || count < 1 { + Ok(String::from("n/a")) + } else { + let title = unsafe { CStr::from_ptr(*list).to_string_lossy().into_owned() }; + unsafe { XFreeStringList(list) }; + Ok(title) + } +} + +#[cfg(target_os = "linux")] +fn get_x11_targets(conn: &xcb::Connection) -> Result> { + let setup = conn.get_setup(); + let screens = setup.roots(); + + let wm_client_list = get_atom(conn, "_NET_CLIENT_LIST")?; + assert!(wm_client_list != x::ATOM_NONE, "EWMH not supported"); + + let atom_net_wm_name = get_atom(conn, "_NET_WM_NAME")?; + let atom_text = get_atom(conn, "TEXT")?; + let atom_utf8_string = get_atom(conn, "UTF8_STRING")?; + let atom_compound_text = get_atom(conn, "COMPOUND_TEXT")?; + + let mut targets = Vec::new(); + for screen in screens { + let window_list = get_property(conn, screen.root(), wm_client_list, x::ATOM_NONE, 100)?; + + for client in window_list.value::() { + let cr = get_property(conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096)?; + if !cr.value::().is_empty() { + targets.push(VideoSource::XWindow { + id: client.resource_id(), + name: String::from_utf8(cr.value().to_vec()) + .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))?, + }); + continue; + } + + let reply = get_property(conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096)?; + let value: &[u8] = reply.value(); + if !value.is_empty() { + let ttype = reply.r#type(); + let title = + if ttype == x::ATOM_STRING || ttype == atom_utf8_string || ttype == atom_text { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + } else if ttype == atom_compound_text { + decode_compound_text(conn, value, client, ttype) + .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))? + } else { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + }; + + targets.push(VideoSource::XWindow { + id: client.resource_id(), + name: title, + }); + continue; + } + targets.push(VideoSource::XWindow { + id: client.resource_id(), + name: "n/a".to_owned(), + }); + } + + let resources = conn.send_request(&GetScreenResources { + window: screen.root(), + }); + let resources = conn.wait_for_reply(resources)?; + for output in resources.outputs() { + let info = conn.send_request(&GetOutputInfo { + output: *output, + config_timestamp: 0, + }); + let info = conn.wait_for_reply(info)?; + if info.connection() == xcb::randr::Connection::Connected { + let crtc = info.crtc(); + let crtc_info = conn.send_request(&GetCrtcInfo { + crtc, + config_timestamp: 0, + }); + let crtc_info = conn.wait_for_reply(crtc_info)?; + let title = String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); + targets.push(VideoSource::XDisplay { + name: title, + id: screen.root().resource_id(), + width: crtc_info.width(), + height: crtc_info.height(), + x_offset: crtc_info.x(), + y_offset: crtc_info.y(), + }); + } + } + } + + Ok(targets) +} + +#[derive(Debug)] +enum Event { StartCast { addr_idx: usize, port: i32, + video_idx: Option, + audio_idx: Option, }, StopCast, - Sources(Vec), - SelectSource(usize), Packet(fcast_lib::packet::Packet), ReceiverAvailable { name: String, @@ -54,102 +362,173 @@ pub enum Event { PipelineFinished, PipelineIsPlaying, DisconnectedFromReceiver, + VideosAvailable(Vec), + AudiosAvailable(Vec), + ReloadVideoSources, + ReloadAudioSources, + Quit, +} + +enum FetchEvent { + Fetch, Quit, } struct Application { - pipeline: pipeline::Pipeline, + pipeline: Option, ui_weak: slint::Weak, event_tx: crossbeam_channel::Sender, - select_source_tx: crossbeam_channel::Sender, - selected_source: bool, receivers: Vec<(ReceiverItem, SocketAddr)>, - appsink: gst::Element, addresses: Vec, mdns: mdns_sd::ServiceDaemon, mdns_receiver: flume::Receiver, session: Session, should_play: bool, selected_addr_idx: usize, + video_sources: Vec, + audio_sources: Vec, + video_source_fetcher_tx: tokio::sync::mpsc::Sender, + audio_source_fetcher_tx: tokio::sync::mpsc::Sender, } impl Application { + /// Must be called from a tokio runtime. pub fn new( ui_weak: slint::Weak, event_tx: crossbeam_channel::Sender, - appsink: gst::Element, ) -> Result { - let (select_source_tx, pipeline) = Self::new_pipeline(event_tx.clone(), appsink.clone())?; - let mdns = mdns_sd::ServiceDaemon::new()?; let mdns_receiver = mdns.browse("_fcast._tcp.local.")?; + let (video_source_fetcher_tx, mut video_source_fetcher_rx) = tokio::sync::mpsc::channel(10); + let (audio_source_fetcher_tx, mut audio_source_fetcher_rx) = tokio::sync::mpsc::channel(10); + + #[cfg(target_os = "linux")] + { + let event_tx = event_tx.clone(); + tokio::spawn(async move { + let mut _proxy = None; + let mut _session = None; + let mut conn = None; + enum WindowingSystem { + Wayland, + X11, + } + + let winsys = if std::env::var("WAYLAND_DISPLAY").is_ok() { + WindowingSystem::Wayland + } else if std::env::var("DISPLAY").is_ok() { + conn = Some(xcb::Connection::connect(None).unwrap().0); + WindowingSystem::X11 + } else { + panic!("Unsupported windowing system!"); + // TODO: tell user or something + }; + + loop { + let Some(event) = video_source_fetcher_rx.recv().await else { + error!("Failed to receive new video source fetcher event"); + break; + }; + + match (event, &winsys) { + (FetchEvent::Fetch, WindowingSystem::Wayland) => { + let new_proxy = Screencast::new().await.unwrap(); + let new_session = new_proxy.create_session().await.unwrap(); + new_proxy + .select_sources( + &new_session, + CursorMode::Embedded, + SourceType::Monitor | SourceType::Window, + false, + None, + PersistMode::DoNot, + ) + .await + .unwrap(); + + let response = new_proxy + .start(&new_session, None) + .await + .unwrap() + .response() + .unwrap(); + let stream = response.streams().first().unwrap(); + let fd = new_proxy + .open_pipe_wire_remote(&new_session) + .await + .unwrap() + .as_raw_fd(); + event_tx + .send(Event::VideosAvailable(vec![VideoSource::PipeWire { + node_id: stream.pipe_wire_node_id(), + fd, + }])) + .unwrap(); + + _proxy = Some(new_proxy); + _session = Some(new_session); + } + (FetchEvent::Fetch, WindowingSystem::X11) => { + let Some(xconn) = conn.as_ref() else { + error!("No xcb connection available"); + continue; + }; + + let sources = get_x11_targets(xconn).unwrap(); + event_tx.send(Event::VideosAvailable(sources)).unwrap(); + } + (FetchEvent::Quit, _) => break, + } + } + + debug!("Video source fetch loop quit"); + }); + } + + #[cfg(target_os = "linux")] + { + let event_tx = event_tx.clone(); + tokio::spawn(async move { + loop { + let Some(event) = audio_source_fetcher_rx.recv().await else { + error!("Failed to receive new video source fetcher event"); + break; + }; + + match event { + FetchEvent::Fetch => { + match get_audio_devices() { + Ok(devs) => event_tx.send(Event::AudiosAvailable(devs)).unwrap(), + Err(err) => error!("Failed to get default pulse device: {err}"), + }; + } + FetchEvent::Quit => break, + } + } + + debug!("Audio source fetch loop quit"); + }); + } + Ok(Self { - pipeline, + pipeline: None, ui_weak, event_tx, - select_source_tx, - selected_source: false, receivers: Vec::new(), - appsink, addresses: Vec::new(), mdns, mdns_receiver, session: Session::default(), should_play: false, selected_addr_idx: 0, + video_sources: Vec::new(), + audio_sources: Vec::new(), + video_source_fetcher_tx, + audio_source_fetcher_tx, }) } - fn new_pipeline( - event_tx: crossbeam_channel::Sender, - appsink: gst::Element, - ) -> Result<(crossbeam_channel::Sender, pipeline::Pipeline)> { - let (selected_tx, selected_rx) = crossbeam_channel::bounded::(1); - - let pipeline = pipeline::Pipeline::new( - appsink, - { - let event_tx = event_tx.clone(); - let pipeline_has_finished = std::sync::Arc::new(AtomicBool::new(false)); - move |event| { - let event_tx = event_tx.clone(); - let pipeline_has_finished = std::sync::Arc::clone(&pipeline_has_finished); - match event { - pipeline::Event::PipelineIsPlaying => { - event_tx.send(crate::Event::PipelineIsPlaying).unwrap(); - } - pipeline::Event::Eos => { - if !pipeline_has_finished.load(Ordering::Acquire) { - event_tx.send(crate::Event::PipelineFinished).unwrap(); - pipeline_has_finished.store(true, Ordering::Release); - } - } - pipeline::Event::Error => { - if !pipeline_has_finished.load(Ordering::Acquire) { - event_tx.send(crate::Event::PipelineFinished).unwrap(); - pipeline_has_finished.store(true, Ordering::Release); - } - } - } - } - }, - { - let selected_rx = std::sync::Arc::new(std::sync::Mutex::new(selected_rx)); - move |vals| { - let sources = vals[1].get::>().unwrap(); - event_tx.send(Event::Sources(sources)).unwrap(); - let selected_rx = selected_rx.lock().unwrap(); - let res = selected_rx.recv().unwrap() as u64; - use gst::prelude::*; - Some(res.to_value()) - } - }, - )?; - - Ok((selected_tx, pipeline)) - } - fn disconnect_receiver(&mut self) -> Result<()> { for r in self.receivers.iter_mut() { if r.0.state == ReceiverState::Connected || r.0.state == ReceiverState::Connecting { @@ -162,7 +541,12 @@ impl Application { if let Err(err) = self.session.disconnect() { error!("Error occured when disconnecting from receiver: {err}"); } - self.pipeline.remove_transmission_sink()?; + + if let Some(mut pipeline) = self.pipeline.take() { + if let Err(err) = pipeline.shutdown() { + error!("Failed to shutdown pipeline: {err}"); + } + } self.ui_weak.upgrade_in_event_loop(|ui| { ui.invoke_receiver_disconnected(); @@ -290,9 +674,17 @@ impl Application { } /// Returns `true` if the event loop should quit - fn handle_event(&mut self, event: Event) -> Result { + async fn handle_event(&mut self, event: Event) -> Result { + debug!("Handling event: {event:?}"); + match event { - Event::StartCast { addr_idx, port } => { + Event::StartCast { + addr_idx, + port, + video_idx, + audio_idx, + } => { + // TODO let port = { if port < 1 || port > u16::MAX as i32 { error!("Port ({port}) is not in the valid port range"); @@ -302,44 +694,140 @@ impl Application { }; self.selected_addr_idx = addr_idx; + let video_src = match video_idx { + Some(video_idx) => { + let Some(video_src) = self.video_sources.get(video_idx) else { + error!( + "Failed to get video source, video_idx ({video_idx}) > video_sources.len ({})", + self.video_sources.len() + ); + return Ok(false); + }; + match video_src { + #[cfg(target_os = "linux")] + VideoSource::PipeWire { node_id, .. } => Some( + gst::ElementFactory::make("pipewiresrc") + .property("path", node_id.to_string()) + .build()?, + ), + #[cfg(target_os = "linux")] + VideoSource::XWindow { id, .. } => Some( + gst::ElementFactory::make("ximagesrc") + .property("xid", *id as u64) + .property("use-damage", false) + .build()?, + ), + #[cfg(target_os = "linux")] + VideoSource::XDisplay { + id, + width, + height, + x_offset, + y_offset, + .. + } => Some( + gst::ElementFactory::make("ximagesrc") + .property("xid", *id as u64) + .property("startx", *x_offset as u32) + .property("starty", *y_offset as u32) + .property("endx", (*x_offset as u32) + (*width as u32) - 1) + .property("endy", (*y_offset as u32) + (*height as u32) - 1) + .property("use-damage", false) + .build()?, + ), + } + } + None => None, + }; + + let audio_src = match audio_idx { + Some(audio_idx) => { + let Some(audio_src) = self.audio_sources.get(audio_idx) else { + error!( + "Failed to get audio source, audio_idx ({audio_idx}) > audio_sources.len ({})", + self.audio_sources.len() + ); + return Ok(false); + }; + match audio_src { + #[cfg(target_os = "linux")] + AudioSource::Pipewire { id, .. } => Some( + gst::ElementFactory::make("pipewiresrc") + .property("path", id.to_string()) + .build()?, + ), + } + } + None => None, + }; + + let source_config = match (video_src, audio_src) { + (Some(video), Some(audio)) => SourceConfig::AudioVideo { video, audio }, + (Some(video), None) => SourceConfig::Video(video), + (None, Some(audio)) => SourceConfig::Audio(audio), + _ => bail!("must have at least one source"), + }; + for r in self.receivers.iter_mut() { if r.0.state != ReceiverState::Connected { continue; } - if r.0.name.starts_with("OpenMirroring") { - self.pipeline.add_rtsp_sink(port)?; - - let addr = match self.addresses.get(self.selected_addr_idx) { - Some(addr) => addr, - None => { - error!( - "Address ({}) is out of bounds in the addresses list", - self.selected_addr_idx, - ); - return Ok(false); + debug!("Adding RTSP pipeline"); + self.pipeline = Some(pipeline::Pipeline::new_rtsp( + { + let event_tx = self.event_tx.clone(); + let pipeline_has_finished = Arc::new(AtomicBool::new(false)); + move |event| { + let event_tx = event_tx.clone(); + let pipeline_has_finished = Arc::clone(&pipeline_has_finished); + match event { + pipeline::Event::PipelineIsPlaying => { + event_tx.send(crate::Event::PipelineIsPlaying).unwrap(); + } + pipeline::Event::Eos => { + if !pipeline_has_finished.load(Ordering::Relaxed) { + event_tx.send(crate::Event::PipelineFinished).unwrap(); + pipeline_has_finished.store(true, Ordering::Relaxed); + } + } + pipeline::Event::Error => { + if !pipeline_has_finished.load(Ordering::Relaxed) { + event_tx.send(crate::Event::PipelineFinished).unwrap(); + pipeline_has_finished.store(true, Ordering::Relaxed); + } + } + } } - }; + }, + source_config, + )?); - let Some(play_msg) = self.pipeline.get_play_msg(*addr) else { - error!("Could not get stream uri"); + let addr = match self.addresses.get(self.selected_addr_idx) { + Some(addr) => addr, + None => { + error!( + "Address ({}) is out of bounds in the addresses list", + self.selected_addr_idx, + ); return Ok(false); - }; + } + }; - debug!("Sending play message: {play_msg:?}"); + let Some(play_msg) = self.pipeline.as_ref().unwrap().get_play_msg(*addr) else { + error!("Could not get stream uri"); + return Ok(false); + }; - self.send_packet_to_receiver(Packet::Play(play_msg))?; + debug!("Sending play message: {play_msg:?}"); - self.ui_weak.upgrade_in_event_loop(|ui| { - ui.invoke_cast_started(); - })?; + self.send_packet_to_receiver(Packet::Play(play_msg))?; - return Ok(false); - } else { - self.pipeline.add_hls_sink(port)?; - } + self.ui_weak.upgrade_in_event_loop(|ui| { + ui.invoke_cast_started(); + })?; - break; + return Ok(false); } self.ui_weak.upgrade_in_event_loop(|ui| { @@ -353,32 +841,12 @@ impl Application { self.ui_weak.upgrade_in_event_loop(|ui| { ui.invoke_cast_stopped(); })?; - self.pipeline.remove_transmission_sink()?; - } - Event::Sources(sources) => { - debug!("Available sources: {sources:?}"); - if sources.len() == 1 { - debug!("One source available, auto selecting"); - self.event_tx.send(Event::SelectSource(0))?; - } else { - self.ui_weak.upgrade_in_event_loop(move |ui| { - let model = Rc::new(slint::VecModel::::from( - sources - .iter() - .map(|s| s.into()) - .collect::>(), - )); - ui.set_sources_model(model.into()); - })?; + if let Some(mut pipeline) = self.pipeline.take() { + if let Err(err) = pipeline.shutdown() { + error!("Failed to shutdown pipeline: {err}"); + } } } - Event::SelectSource(idx) => { - self.select_source_tx.send(idx)?; - self.selected_source = true; - self.ui_weak.upgrade_in_event_loop(|ui| { - ui.set_has_source(true); - })?; - } Event::Packet(packet) => { trace!("Unhandled packet: {packet:?}"); } @@ -401,13 +869,17 @@ impl Application { } } Event::DisconnectReceiver => self.disconnect_receiver()?, - Event::ChangeSource | Event::PipelineFinished => { - self.shutdown_pipeline_and_create_new_and_update_ui()?; - } + Event::ChangeSource | Event::PipelineFinished => error!("TODO"), Event::PipelineIsPlaying => { - self.pipeline.playing()?; + if let Some(pipeline) = self.pipeline.as_mut() { + if let Err(err) = pipeline.playing() { + error!("Failed to run playing on pipeline: {err}"); + // TODO: set pipeline to None? + } + } if self.should_play { + self.should_play = false; let addr = match self.addresses.get(self.selected_addr_idx) { Some(addr) => addr, None => { @@ -419,7 +891,11 @@ impl Application { } }; - let Some(play_msg) = self.pipeline.get_play_msg(*addr) else { + let Some(pipeline) = self.pipeline.as_ref() else { + error!("Should play but missing pipeline"); + return Ok(false); + }; + let Some(play_msg) = pipeline.get_play_msg(*addr) else { error!("Could not get stream uri"); return Ok(false); }; @@ -431,7 +907,6 @@ impl Application { self.ui_weak.upgrade_in_event_loop(|ui| { ui.invoke_cast_started(); })?; - self.should_play = false; } } Event::DisconnectedFromReceiver => { @@ -445,20 +920,77 @@ impl Application { })?; } Event::Quit => return Ok(true), + Event::VideosAvailable(sources) => { + self.video_sources = sources; + self.update_video_sources_in_ui()?; + } + Event::AudiosAvailable(sources) => { + self.audio_sources = sources; + self.update_audio_sources_in_ui()?; + } + Event::ReloadVideoSources => { + self.video_source_fetcher_tx.send(FetchEvent::Fetch).await? + } + Event::ReloadAudioSources => { + self.audio_source_fetcher_tx.send(FetchEvent::Fetch).await? + } } Ok(false) } - pub fn run_event_loop(mut self, event_rx: crossbeam_channel::Receiver) -> Result<()> { + fn update_audio_sources_in_ui(&self) -> Result<()> { + let audio_dev_names = self + .audio_sources + .iter() + .map(|dev| slint::SharedString::from(dev.display_name().as_str())) + .collect::>(); + self.ui_weak.upgrade_in_event_loop(move |ui| { + ui.set_audio_sources_model( + Rc::new(slint::VecModel::::from_slice( + &audio_dev_names, + )) + .into(), + ); + })?; + + Ok(()) + } + + fn update_video_sources_in_ui(&self) -> Result<()> { + let video_dev_names = self + .video_sources + .iter() + .map(|dev| slint::SharedString::from(dev.display_name().as_str())) + .collect::>(); + self.ui_weak.upgrade_in_event_loop(move |ui| { + ui.set_video_sources_model( + Rc::new(slint::VecModel::::from_slice( + &video_dev_names, + )) + .into(), + ); + })?; + + Ok(()) + } + + pub async fn run_event_loop( + mut self, + event_rx: crossbeam_channel::Receiver, + ) -> Result<()> { self.update_addresses_and_in_ui()?; const ADDR_UPDATE_INTERVAL: Duration = Duration::from_secs(5); let mut prev_addr_update = Instant::now(); + self.video_source_fetcher_tx.send(FetchEvent::Fetch).await?; + self.audio_source_fetcher_tx.send(FetchEvent::Fetch).await?; + + // TODO: do all async loop { match event_rx.try_recv() { Ok(event) => { - if self.handle_event(event)? { + if self.handle_event(event).await? { break; } } @@ -536,41 +1068,21 @@ impl Application { self.update_addresses_and_in_ui()?; } - sleep(Duration::from_millis(25)); + tokio::time::sleep(Duration::from_millis(25)).await; } - debug!("Quitting"); + debug!("Quitting event loop"); - if !self.selected_source { - debug!("Source is not selected, sending fake"); - self.select_source_tx.send(0)?; + if let Some(mut pipeline) = self.pipeline.take() { + if let Err(err) = pipeline.shutdown() { + error!("Failed to shutdown pipeline: {err}"); + } } - self.pipeline.shutdown()?; - self.mdns.shutdown()?; - Ok(()) - } - - fn shutdown_pipeline_and_create_new_and_update_ui(&mut self) -> Result<()> { - if !self.selected_source { - self.select_source_tx.send(0)?; - } - - self.pipeline.shutdown()?; - - let (new_select_srouce_tx, new_pipeline) = - Self::new_pipeline(self.event_tx.clone(), self.appsink.clone())?; - - self.pipeline = new_pipeline; - self.select_source_tx = new_select_srouce_tx; - self.selected_source = false; - - self.ui_weak.upgrade_in_event_loop(|ui| { - ui.set_has_source(false); - ui.set_sources_model(Rc::new(slint::VecModel::::default()).into()); - })?; + self.video_source_fetcher_tx.send(FetchEvent::Quit).await?; + self.audio_source_fetcher_tx.send(FetchEvent::Quit).await?; Ok(()) } @@ -579,78 +1091,28 @@ impl Application { fn main() -> Result<()> { env_logger::Builder::from_default_env() .filter_module("sender", common::default_log_level()) - .filter_module("scap", common::default_log_level()) .filter_module("common", common::default_log_level()) .filter_module("mdns_sd", common::default_log_level()) .init(); - slint::BackendSelector::new() - .backend_name("winit".into()) - .require_opengl() - .select()?; - gst::init()?; - scapgst::plugin_register_static()?; common::sender::pipeline::init()?; - let (event_tx, event_rx) = crossbeam_channel::bounded::(100); + let runtime = Runtime::new()?; - // This sink is used in every consecutively created pipelines - let mut slint_sink = SlintOpenGLSink::new()?; - let slint_appsink = slint_sink.video_sink(); + let (event_tx, event_rx) = crossbeam_channel::bounded::(100); let ui = MainWindow::new()?; slint::set_xdg_app_id("com.github.malba124.OpenMirroring.sender")?; - let gotten_gl = Arc::new(AtomicBool::new(false)); - - ui.window().set_rendering_notifier({ - let ui_weak = ui.as_weak(); - let gotten_gl = Arc::clone(&gotten_gl); - - move |state, graphics_api| match state { - slint::RenderingState::RenderingSetup => { - let ui_weak = ui_weak.clone(); - slint_sink - .connect(graphics_api, move || { - ui_weak - .upgrade_in_event_loop(move |ui| { - ui.window().request_redraw(); - }) - .ok(); - }) - .unwrap(); - gotten_gl.store(true, atomic::Ordering::Release); - } - slint::RenderingState::BeforeRendering => { - if let Some(next_frame) = slint_sink.fetch_next_frame() { - if let Some(ui) = ui_weak.upgrade() { - ui.set_preview_frame(next_frame); - } else { - error!("Failed to upgrade ui_weak"); - } - } - } - slint::RenderingState::RenderingTeardown => { - slint_sink.deactivate_and_pause().unwrap(); - } - _ => (), - } - })?; - - let event_loop_jh = std::thread::spawn({ + let event_loop_jh = runtime.spawn({ let ui_weak = ui.as_weak(); let event_tx = event_tx.clone(); - move || { - // We need to wait until the preview sink has gotten it's required GL contexts, - // if not, creating a pipeline would fail - while !gotten_gl.load(atomic::Ordering::Acquire) { - std::thread::sleep(Duration::from_millis(25)); - } - - Application::new(ui_weak, event_tx, slint_appsink) + async move { + Application::new(ui_weak, event_tx) .unwrap() .run_event_loop(event_rx) + .await .unwrap(); } }); @@ -662,13 +1124,6 @@ fn main() -> Result<()> { }}; } - event_handler!(event_tx, |event_tx: crossbeam_channel::Sender| { - ui.on_select_source(move |idx| { - assert!(idx >= 0, "Invalid select source index"); - event_tx.send(Event::SelectSource(idx as usize)).unwrap(); - }); - }); - event_handler!(event_tx, |event_tx: crossbeam_channel::Sender| { ui.on_connect_receiver(move |receiver| { event_tx @@ -678,11 +1133,21 @@ fn main() -> Result<()> { }); event_handler!(event_tx, |event_tx: crossbeam_channel::Sender| { - ui.on_start_cast(move |(addr_idx, port)| { + ui.on_start_cast(move |(addr_idx, port), video_idx, audio_idx| { event_tx .send(Event::StartCast { addr_idx: addr_idx as usize, port, + video_idx: if video_idx >= 0 { + Some(video_idx as usize) + } else { + None + }, + audio_idx: if audio_idx >= 0 { + Some(audio_idx as usize) + } else { + None + }, }) .unwrap(); }); @@ -725,11 +1190,24 @@ fn main() -> Result<()> { }); }); - ui.run()?; + event_handler!(event_tx, |event_tx: crossbeam_channel::Sender| { + ui.on_reload_video_sources(move || { + event_tx.send(Event::ReloadVideoSources).unwrap(); + }); + }); - event_tx.send(Event::Quit).unwrap(); + event_handler!(event_tx, |event_tx: crossbeam_channel::Sender| { + ui.on_reload_audio_sources(move || { + event_tx.send(Event::ReloadAudioSources).unwrap(); + }); + }); - event_loop_jh.join().unwrap(); + ui.run()?; + + runtime.block_on(async move { + event_tx.send(Event::Quit).unwrap(); + event_loop_jh.await.unwrap(); + }); Ok(()) } diff --git a/sender/ui/main.slint b/sender/ui/main.slint index dbd5c03..e0ceee0 100644 --- a/sender/ui/main.slint +++ b/sender/ui/main.slint @@ -15,11 +15,10 @@ // You should have received a copy of the GNU General Public License // along with OpenMirroring. If not, see . -import { VerticalBox, HorizontalBox, Button, ComboBox, Palette, Spinner } from "std-widgets.slint"; +import { VerticalBox, HorizontalBox, Button, ComboBox, Palette, Spinner, Switch } from "std-widgets.slint"; import { Settings } from "settings.slint"; import { CastDialog, ReceiverItem } from "../../ui-common/cast-dialog.slint"; -import { VideoPreview } from "video-preview.slint"; import { ToolTipArea } from "../../ui-common/tool-tip-area.slint"; export component MainWindow inherits Window { @@ -28,18 +27,18 @@ export component MainWindow inherits Window { preferred-width: 800px; preferred-height: 500px; - callback select-source(int); callback connect-receiver(string); - callback start-cast({addr-idx: int, port: int}); + callback start-cast({addr-idx: int, port: int}, video_idx: int, audio_idx: int); callback stop-cast(); callback disconnect-receiver(); callback change-source(); callback add-receiver-manually(name: string, addr: string, port: string); + callback reload-video-sources(); + callback reload-audio-sources(); in property has-source: false; - property preview-enable: true; - in property preview-frame <=> preview-image.source; - in property <[string]> sources-model: []; + in property <[string]> video-sources-model: []; + in property <[string]> audio-sources-model: []; in property <[ReceiverItem]> receivers-model: []; in property <[string]> addresses-model: []; @@ -69,18 +68,72 @@ export component MainWindow inherits Window { casting = false; } - // Maybe have vertical layout when the root width is, say 800 px, horizontal otherwise VerticalBox { - preview := VideoPreview { - visible: has-source; - show-preview: true; - background: Palette.background; - height: has-source ? 60% : 0%; - preview-image := Image { - visible <=> parent.show-preview; - width: 100%; - height: parent.height; - image-fit: contain; + alignment: center; + + VerticalBox { + video-switch := Switch { + enabled: video-sources-model.length > 0; + text: "Video"; + checked: true; + } + + HorizontalBox { + Text { + vertical-alignment: center; + text: "Source:"; + opacity: video-switch.checked ? 1.0 : 0.6; + animate opacity { duration: 150ms; } + } + + video-combo := ComboBox { + enabled <=> video-switch.checked; + model <=> video-sources-model; + } + + ToolTipArea { + text: "Reload"; + Button { + icon: @image-url("../../assets/icons/reload.svg"); + colorize-icon: true; + clicked => { + reload-video-sources(); + } + } + } + } + } + + VerticalBox { + audio-switch := Switch { + enabled: audio-sources-model.length > 0; + text: "Audio"; + checked: false; + } + + HorizontalBox { + Text { + vertical-alignment: center; + text: "Source:"; + opacity: audio-switch.checked ? 1.0 : 0.6; + animate opacity { duration: 150ms; } + } + + audio-combo := ComboBox { + enabled <=> audio-switch.checked; + model <=> audio-sources-model; + } + + ToolTipArea { + text: "Reload"; + Button { + icon: @image-url("../../assets/icons/reload.svg"); + colorize-icon: true; + clicked => { + reload-audio-sources(); + } + } + } } } @@ -112,26 +165,29 @@ export component MainWindow inherits Window { } } - if has-source && !casting && !starting: ToolTipArea { + if !casting && !starting: ToolTipArea { text: "Start casting"; Button { - enabled: receiver-is-connected; + enabled: receiver-is-connected && (video-switch.checked || audio-switch.checked); icon: @image-url("../../assets/icons/play.svg"); colorize-icon: true; clicked => { - start-cast(settings.currently-selected-address()); + start-cast( + settings.currently-selected-address(), + video-switch.checked ? video-combo.current-index : -1, + audio-switch.checked ? audio-combo.current-index : -1); } } } - if has-source && starting: ToolTipArea { + if starting: ToolTipArea { text: "Casting is starting..."; Spinner { indeterminate: true; } } - if has-source && casting: ToolTipArea { + if casting: ToolTipArea { text: "Stop casting"; Button { icon: @image-url("../../assets/icons/stop.svg"); @@ -142,7 +198,7 @@ export component MainWindow inherits Window { } } - if has-source: ToolTipArea { + ToolTipArea { text: "Open casting dialog"; Button { icon: @image-url("../../assets/icons/cast.svg"); @@ -154,21 +210,6 @@ export component MainWindow inherits Window { } } } - - if !has-source: HorizontalBox { - alignment: center; - source-combo := ComboBox { - model <=> sources-model; - } - - Button { - text: "Select source"; - enabled: sources-model.length > 0; - clicked => { - select-source(source-combo.current-index); - } - } - } } } @@ -186,7 +227,7 @@ export component MainWindow inherits Window { settings := Settings { visible: false; width: root.width >= 500px ? 500px : root.width; - border-radius: root.width >= 500px ? 8px: 0px; + border-radius: root.width >= 500px ? 8px : 0px; stream-addresses <=> addresses-model; closed => { @@ -201,7 +242,7 @@ export component MainWindow inherits Window { receivers-model <=> receivers-model; width: root.width >= 500px ? 500px : root.width; root-height <=> root.height; - border-radius: root.width >= 500px ? 8px: 0px; + border-radius: root.width >= 500px ? 8px : 0px; connect(receiver) => { connect-receiver(receiver); diff --git a/sender/ui/video-preview.slint b/sender/ui/video-preview.slint deleted file mode 100644 index 1ccea2b..0000000 --- a/sender/ui/video-preview.slint +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -import { Palette, Switch, HorizontalBox } from "std-widgets.slint"; - -export component VideoPreview inherits Rectangle { - in-out property show-preview; - - ta := TouchArea { - pointer-event(event) => { - if event.button == PointerEventButton.right && event.kind == PointerEventKind.down { - dialog.show(); - } - } - } - - @children - - Rectangle { - visible: !show-preview; - - Text { - text: "Preview disabled"; - } - } - - dialog := PopupWindow { - x: ta.mouse-x; - y: ta.mouse-y; - - Rectangle { - background: Palette.alternate-background; - border-radius: 8px; - - Dialog { - HorizontalBox { - Text { - vertical-alignment: center; - text: "Show preview"; - } - Switch { - checked: show-preview; - toggled => { - show-preview = !show-preview; - } - } - } - } - } - } -} diff --git a/testkit/Cargo.toml b/testkit/Cargo.toml deleted file mode 100644 index b9ba9f5..0000000 --- a/testkit/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "testkit" -version = "0.1.0" -edition = "2024" - -[dependencies] -slint = { workspace = true } -anyhow = { workspace = true } -crossbeam-channel = { workspace = true } -mdns-sd = { version = "0.13.9", default-features = false, features = ["logging"] } -flume = { version = "0.11.1", default-features = false } -log = { workspace = true } -env_logger = { workspace = true } -fcast-lib = { path = "../fcast-lib" } -chrono = "0.4.41" -common = { path = "../common", features = ["sender"] } - -[build-dependencies] -slint-build = { workspace = true } \ No newline at end of file diff --git a/testkit/README.md b/testkit/README.md deleted file mode 100644 index 0adddd4..0000000 --- a/testkit/README.md +++ /dev/null @@ -1,3 +0,0 @@ -![FCast Test Kit Demo](../assets/testkit_demo.png) - -This is a utility program that makes comunicating with FCast receivers easy. diff --git a/testkit/build.rs b/testkit/build.rs deleted file mode 100644 index ab78911..0000000 --- a/testkit/build.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -fn main() { - slint_build::compile("ui/main.slint").unwrap(); -} diff --git a/testkit/src/main.rs b/testkit/src/main.rs deleted file mode 100644 index 9f9df5d..0000000 --- a/testkit/src/main.rs +++ /dev/null @@ -1,530 +0,0 @@ -// Copyright (C) 2025 Marcus L. Hanestad -// -// This file is part of OpenMirroring. -// -// OpenMirroring is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// OpenMirroring is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with OpenMirroring. If not, see . - -use std::{net::SocketAddr, rc::Rc, thread::sleep, time::Duration}; - -use anyhow::Result; -use common::sender::session::{Session, SessionEvent}; -use crossbeam_channel::Receiver; -use fcast_lib::packet::Packet; -use log::{debug, error}; -use mdns_sd::ServiceEvent; -use slint::{Model, Weak}; - -slint::include_modules!(); - -enum Event { - Quit, - ConnectReceiver(String), - ReceiverAvailable { - name: String, - addresses: Vec, - }, - DisconnectReceiver, - SendPacket(Packet), -} - -struct Application { - ui_weak: Weak, - receivers: Vec<(ReceiverItem, SocketAddr)>, - session: Session, -} - -impl Application { - pub fn new(ui_weak: Weak) -> Self { - Self { - ui_weak, - receivers: Vec::new(), - session: Session::default(), - } - } - - fn receivers_contains(&self, x: &str) -> Option { - for (idx, y) in self.receivers.iter().enumerate() { - if y.0.name == x { - return Some(idx); - } - } - - None - } - - /// Returns the state for all non connecting/connected receivers. - fn receivers_general_state(&self) -> ReceiverState { - for r in &self.receivers { - if r.0.state != ReceiverState::Connectable { - return ReceiverState::Inactive; - } - } - - ReceiverState::Connectable - } - - fn update_receivers_in_ui(&mut self) -> Result<()> { - let g_state = self.receivers_general_state(); - for r in &mut self.receivers { - if r.0.state != ReceiverState::Connecting && r.0.state != ReceiverState::Connected { - r.0.state = g_state; - } - } - - let receivers = self - .receivers - .iter() - .map(|r| r.0.clone()) - .collect::>(); - self.ui_weak.upgrade_in_event_loop(move |ui| { - let model = Rc::new(slint::VecModel::::from_iter( - receivers.into_iter(), - )); - ui.set_receivers_model(model.into()); - })?; - - Ok(()) - } - - fn push_message(&self, direction: MessageDirection, msg: String) -> Result<()> { - self.ui_weak.upgrade_in_event_loop(move |ui| { - let messages_model = ui.get_messages(); - let messages_model = messages_model - .as_any() - .downcast_ref::>>() - .unwrap(); - let messages_model = messages_model.source_model(); - messages_model.push(Message { - direction, - text: msg.into(), - time: format!("{}", chrono::Local::now().format("%H:%M:%S")).into(), - }) - })?; - - Ok(()) - } - - fn add_receiver(&mut self, name: String, addrs: Vec) -> Result<()> { - if let Some(idx) = self.receivers_contains(&name) { - self.receivers[idx].1 = addrs[0]; - } else { - self.receivers.push(( - ReceiverItem { - name: name.into(), - state: self.receivers_general_state(), - }, - addrs[0], - )); - } - - self.update_receivers_in_ui()?; - - Ok(()) - } - - pub fn run_event_loop(mut self, event_rx: Receiver) -> Result<()> { - // Set the message model to be what we want so no errors occur later - self.ui_weak.upgrade_in_event_loop(|ui| { - let model = slint::VecModel::from(Vec::::new()); - let reverse_model = slint::ReverseModel::new(model); - ui.set_messages(Rc::new(reverse_model).into()); - })?; - - let mdns = mdns_sd::ServiceDaemon::new()?; - let mdns_receiver = mdns.browse("_fcast._tcp.local.")?; - - loop { - match event_rx.try_recv() { - Ok(event) => match event { - Event::Quit => break, - Event::ConnectReceiver(name) => { - if let Some(idx) = self.receivers_contains(&name) { - self.receivers[idx].0.state = ReceiverState::Connecting; - - self.push_message( - MessageDirection::Info, - format!("Attempting to connect to {}", self.receivers[idx].0.name), - )?; - - let addr = self.receivers[idx].1; - self.session.connect(addr); - - self.update_receivers_in_ui()?; - } else { - error!("No receiver `{name}` available"); - } - } - Event::ReceiverAvailable { name, addresses } => { - self.add_receiver(name, addresses)?; - } - Event::DisconnectReceiver => { - if let Err(err) = self.session.disconnect() { - error!("Failed to disconnect from receiver: {err}"); - } - - for r in &mut self.receivers { - if r.0.state == ReceiverState::Connected - || r.0.state == ReceiverState::Connecting - { - r.0.state = ReceiverState::Connectable; - self.update_receivers_in_ui()?; - break; - } - } - - self.push_message( - MessageDirection::Info, - "Disconnected from receiver".to_owned(), - )?; - } - Event::SendPacket(packet) => { - if self.session.is_connected() { - self.push_message(MessageDirection::Out, format!("{packet:?}"))?; - if let Err(err) = self.session.send_packet(packet) { - error!("Failed to send packet: {err}"); - if let Err(err) = self.session.disconnect() { - error!("Failed to disconnect from receiver: {err}"); - } - } - } - } - }, - Err(err) => { - if err == crossbeam_channel::TryRecvError::Disconnected { - return Err(err.into()); - } - } - } - - match mdns_receiver.try_recv() { - Ok(event) => match event { - ServiceEvent::ServiceResolved(service_info) => { - let port = service_info.get_port(); - let addrs = service_info - .get_addresses() - .iter() - .map(|a| SocketAddr::new(*a, port)) - .collect::>(); - let mut name = service_info.get_fullname().to_owned(); - if let Some(stripped) = name.strip_suffix("._fcast._tcp.local.") { - name = stripped.to_owned(); - } - debug!("Receiver available: {}", name); - self.add_receiver(name, addrs)?; - } - ServiceEvent::ServiceRemoved(_, mut fullname) => { - if let Some(stripped) = fullname.strip_suffix("._fcast._tcp.local.") { - fullname = stripped.to_owned(); - } - if let Some(idx) = self.receivers_contains(&fullname) { - debug!("Receiver unavailable: {fullname}"); - self.receivers.remove(idx); - self.update_receivers_in_ui()?; - } - } - _ => (), - }, - Err(err) => { - if err == flume::TryRecvError::Disconnected { - return Err(err.into()); - } - } - } - - match self.session.poll_event() { - Ok(Some(event)) => match event { - SessionEvent::Packet(packet) => { - self.push_message(MessageDirection::In, format!("{packet:?}"))?; - if packet == Packet::Ping { - let packet = Packet::Pong; - self.push_message(MessageDirection::Out, format!("{packet:?}"))?; - if let Err(err) = self.session.send_packet(packet) { - error!("Failed to send packet: {err}"); - if let Err(err) = self.session.disconnect() { - error!("Failed to disconnect from receiver: {err}"); - } - } - } - } - SessionEvent::Connected => { - debug!("Successfully connected to receiver"); - - self.push_message( - MessageDirection::Info, - "Successfully connected to receiver".to_owned(), - )?; - - for r in &mut self.receivers { - if r.0.state == ReceiverState::Connecting { - r.0.state = ReceiverState::Connected; - break; - } - } - - self.update_receivers_in_ui()?; - } - }, - Err(err) => { - error!("Failed to poll session event: {err}"); - self.session.disconnect()?; - } - _ => (), - } - - sleep(Duration::from_millis(25)); - } - - mdns.shutdown()?; - - Ok(()) - } -} - -fn main() { - env_logger::Builder::from_default_env() - .filter_module("testkit", log::LevelFilter::Debug) - .filter_module("mdns_sd", log::LevelFilter::Debug) - .init(); - - let ui = MainWindow::new().unwrap(); - - let (event_tx, event_rx) = crossbeam_channel::bounded(10); - - let ui_weak = ui.as_weak(); - - { - let event_tx = event_tx.clone(); - ui.on_connect_receiver(move |name| { - event_tx - .send(Event::ConnectReceiver(name.to_string())) - .unwrap(); - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_disconnect_receiver(move || { - event_tx.send(Event::DisconnectReceiver).unwrap(); - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_add_receiver_manually(move |name, addr, port| { - let parsed_addr = match format!("{addr}:{port}").parse::() { - Ok(a) => a, - Err(err) => { - // TODO: show in UI - error!("Failed to parse manually added receiver socket address: {err}"); - return; - } - }; - event_tx - .send(Event::ReceiverAvailable { - name: name.to_string(), - addresses: vec![parsed_addr], - }) - .unwrap(); - }); - } - - macro_rules! simple_packet { - ($ui:expr, $on:ident, $event_tx:expr, $packet:ident) => {{ - let event_tx = $event_tx.clone(); - $ui.$on(move || { - event_tx.send(Event::SendPacket(Packet::$packet)).unwrap(); - }); - }}; - } - - simple_packet!(ui, on_send_none, event_tx, None); - simple_packet!(ui, on_send_pause, event_tx, Pause); - simple_packet!(ui, on_send_resume, event_tx, Resume); - simple_packet!(ui, on_send_stop, event_tx, Stop); - simple_packet!(ui, on_send_ping, event_tx, Ping); - simple_packet!(ui, on_send_pong, event_tx, Pong); - - { - let event_tx = event_tx.clone(); - ui.on_send_play(move |container, url, content, time, speed| { - use fcast_lib::models::PlayMessage; - if container.is_empty() { - error!("`container` is required but it's empty"); - } - - let message = PlayMessage { - container: container.to_string(), - url: if url.is_empty() { - None - } else { - Some(url.to_string()) - }, - content: if content.is_empty() { - None - } else { - Some(content.to_string()) - }, - time: time.parse::().ok(), - speed: speed.parse::().ok(), - headers: None, - }; - - event_tx - .send(Event::SendPacket(Packet::Play(message))) - .unwrap(); - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_seek(move |time| { - if let Ok(time) = time.parse::() { - let message = fcast_lib::models::SeekMessage { time }; - event_tx - .send(Event::SendPacket(Packet::Seek(message))) - .unwrap(); - } else { - error!("`{time}` is not a valid timestamp"); - } - }); - } - - fn current_time_millis() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64 - } - - { - let event_tx = event_tx.clone(); - ui.on_send_playback_update(move |time, duration, state, speed| { - let Ok(time) = time.parse::() else { - error!("`{time}` is not a valid timestamp"); - return; - }; - let Ok(duration) = duration.parse::() else { - error!("`{duration}` is not a valid timestamp"); - return; - }; - let Ok(speed) = speed.parse::() else { - error!("`{speed}` is not a valid float"); - return; - }; - - let state = match state.as_str() { - "Idle" => fcast_lib::models::PlaybackState::Idle, - "Playing" => fcast_lib::models::PlaybackState::Playing, - "Paused" => fcast_lib::models::PlaybackState::Paused, - _ => { - error!("Invalid state: {state}"); - return; - } - }; - - let message = fcast_lib::models::PlaybackUpdateMessage { - generation: current_time_millis(), - time, - duration, - state, - speed, - }; - - event_tx - .send(Event::SendPacket(Packet::PlaybackUpdate(message))) - .unwrap(); - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_volume_update(move |volume| { - if let Ok(volume) = volume.parse::() { - let message = fcast_lib::models::VolumeUpdateMessage { - generation: current_time_millis(), - volume, - }; - event_tx - .send(Event::SendPacket(Packet::VolumeUpdate(message))) - .unwrap(); - } else { - error!("`{volume}` is not a valid volume"); - } - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_set_volume(move |volume| { - if let Ok(volume) = volume.parse::() { - let message = fcast_lib::models::SetVolumeMessage { volume }; - event_tx - .send(Event::SendPacket(Packet::SetVolume(message))) - .unwrap(); - } else { - error!("`{volume}` is not a valid volume"); - } - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_playback_error(move |err_message| { - let message = fcast_lib::models::PlaybackErrorMessage { - message: err_message.to_string(), - }; - event_tx - .send(Event::SendPacket(Packet::PlaybackError(message))) - .unwrap(); - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_set_speed(move |speed| { - if let Ok(speed) = speed.parse::() { - let message = fcast_lib::models::SetSpeedMessage { speed }; - event_tx - .send(Event::SendPacket(Packet::SetSpeed(message))) - .unwrap(); - } else { - error!("`{speed}` is not a valid speed"); - } - }); - } - - { - let event_tx = event_tx.clone(); - ui.on_send_version(move |version| { - if let Ok(version) = version.parse::() { - let message = fcast_lib::models::VersionMessage { version }; - event_tx - .send(Event::SendPacket(Packet::Version(message))) - .unwrap(); - } else { - error!("`{version}` is not a valid version"); - } - }); - } - - let app_thread_jh = std::thread::spawn(move || { - Application::new(ui_weak).run_event_loop(event_rx).unwrap(); - }); - - ui.run().unwrap(); - - event_tx.send(Event::Quit).unwrap(); - - app_thread_jh.join().unwrap(); -} diff --git a/testkit/ui/main.slint b/testkit/ui/main.slint deleted file mode 100644 index 4743189..0000000 --- a/testkit/ui/main.slint +++ /dev/null @@ -1,346 +0,0 @@ -import { ListView, VerticalBox, HorizontalBox, TabWidget, Button, ComboBox, LineEdit } from "std-widgets.slint"; -import { CastDialog, ReceiverItem } from "../../ui-common/cast-dialog.slint"; -import { LabeledLineEdit } from "../../ui-common/input.slint"; - -export enum MessageDirection { - In, - Out, - Info, -} - -export struct Message { - direction: MessageDirection, - time: string, - text: string, -} - -export component MainWindow inherits Window { - title: "FCast Test Kit"; - - preferred-width: 800px; - preferred-height: 500px; - - callback connect-receiver(string); - callback disconnect-receiver(); - callback add-receiver-manually(name: string, addr: string, port: string); - - callback send-none(); - callback send-pause(); - callback send-resume(); - callback send-stop(); - callback send-ping(); - callback send-pong(); - callback send-play(container: string, url: string, content: string, time: string, speed: string); - callback send-seek(time: string); - callback send-playback-update(time: string, duration: string, state: string, speed: string); - callback send-volume-update(volume: string); - callback send-set-volume(volume: string); - callback send-playback-error(message: string); - callback send-set-speed(speed: string); - callback send-version(version: string); - - in property <[Message]> messages; - in property <[ReceiverItem]> receivers-model; - - property play-container; - property play-url; - property play-content; - property play-time; - property play-speed; - property seek-time; - property pb-update-time; - property pb-update-duration; - property pb-update-state; - property pb-update-speed; - property volume-update-volume; - property set-volume-volume; - property pb-error-message; - property set-speed-speed; - property version-version; - - pure function color_from_message_direction(dir: MessageDirection) -> color { - dir == MessageDirection.In ? #ADDFFF : dir == MessageDirection.Out ? #FFFCE0 : #FFFFFF; - } - - // TODO: resizable panels - VerticalLayout { - width: 100%; - height: 100%; - hl := HorizontalLayout { - VerticalBox { - width: 40%; - Button { - icon: @image-url("../../assets/icons/cast.svg"); - colorize-icon: true; - clicked => { - cast-dialog.visible = true; - shadow.visible = true; - } - } - } - - VerticalLayout { - width: 60%; - HorizontalBox { - operation-cb := ComboBox { - model: [ - "None", - "Play", - "Pause", - "Resume", - "Stop", - "Seek", - "PlaybackUpdate", - "VolumeUpdate", - "SetVolume", - "PlaybackError", - "SetSpeed", - "Version", - "Ping", - "Pong", - ]; - } - - Button { - icon: @image-url("../../assets/icons/play.svg"); - colorize-icon: true; - clicked => { - if operation-cb.current-value == "None" { - send-none(); - } - if operation-cb.current-value == "Play" { - send-play(play-container, play-url, play-content, play-time, play-speed); - } - if operation-cb.current-value == "Pause" { - send-pause(); - } - if operation-cb.current-value == "Resume" { - send-resume(); - } - if operation-cb.current-value == "Stop" { - send-stop(); - } - if operation-cb.current-value == "Seek" { - send-seek(seek-time); - } - if operation-cb.current-value == "PlaybackUpdate" { - send-playback-update(pb-update-time, pb-update-duration, pb-update-state, pb-update-speed); - } - if operation-cb.current-value == "VolumeUpdate" { - send-volume-update(volume-update-volume); - } - if operation-cb.current-value == "SetVolume" { - send-set-volume(set-volume-volume); - } - if operation-cb.current-value == "PlaybackError" { - send-playback-error(pb-error-message); - } - if operation-cb.current-value == "SetSpeed" { - send-set-speed(set-speed-speed); - } - if operation-cb.current-value == "Version" { - send-version(version-version); - } - if operation-cb.current-value == "Ping" { - send-ping(); - } - if operation-cb.current-value == "Pong" { - send-pong(); - } - } - } - } - - if operation-cb.current-value == "Play": VerticalBox { - // TODO: presets? - LabeledLineEdit { - label: "Container:"; - placeholder-text: "e.g. video/mp4"; - text <=> play-container; - } - - LabeledLineEdit { - label: "Url:"; - text <=> play-url; - } - - LabeledLineEdit { - label: "Content:"; - text <=> play-content; - } - - LabeledLineEdit { - label: "Time:"; - text <=> play-time; - } - - HorizontalBox { - Text { - text: "Speed:"; - vertical-alignment: center; - } - - ComboBox { - current-index: 3; // Default to 1x - current-value <=> play-speed; - model: [ - "0.25", - "0.5", - "0.75", - "1", - "1.5", - "1.75", - "2", - ]; - } - } - // TODO: headers - } - if operation-cb.current-value == "Seek": VerticalBox { - LabeledLineEdit { - label: "Time:"; - text <=> seek-time; - } - } - if operation-cb.current-value == "PlaybackUpdate": VerticalBox { - LabeledLineEdit { - label: "Time:"; - text <=> pb-update-time; - } - - LabeledLineEdit { - label: "Duration:"; - text <=> pb-update-duration; - } - - HorizontalBox { - Text { - text: "State:"; - vertical-alignment: center; - } - - ComboBox { - model: [ - "Idle", - "Playing", - "Paused", - ]; - current-value <=> pb-update-state; - } - } - - LabeledLineEdit { - label: "Speed:"; - text <=> pb-update-speed; - } - } - if operation-cb.current-value == "VolumeUpdate": VerticalBox { - LabeledLineEdit { - label: "Volume:"; - text <=> volume-update-volume; - } - } - if operation-cb.current-value == "SetVolume": VerticalBox { - LabeledLineEdit { - label: "Volume:"; - text <=> set-volume-volume; - } - } - if operation-cb.current-value == "PlaybackError": VerticalBox { - LabeledLineEdit { - label: "Message:"; - text <=> pb-error-message; - } - } - if operation-cb.current-value == "SetSpeed": VerticalBox { - LabeledLineEdit { - label: "Speed:"; - text <=> set-speed-speed; - } - } - if operation-cb.current-value == "Version": VerticalBox { - LabeledLineEdit { - label: "Version:"; - text <=> version-version; - } - } - } - } - - VerticalBox { - alignment: space-between; - ListView { - height: 100%; - for m in messages: Rectangle { - border-width: 1px; - border-color: whitesmoke; - background: color_from_message_direction(m.direction); - HorizontalBox { - if m.direction == MessageDirection.In: Image { - source: @image-url("../../assets/icons/arrow_left.svg"); - height: 15px; - width: 15px; - } - if m.direction == MessageDirection.Out: Image { - source: @image-url("../../assets/icons/arrow_right.svg"); - height: 15px; - width: 15px; - } - if m.direction == MessageDirection.Info: Image { - source: @image-url("../../assets/icons/info.svg"); - height: 15px; - width: 9px; - } - Text { - vertical-alignment: center; - text: m.time; - } - - Text { - vertical-alignment: center; - text: m.text; - wrap: word-wrap; - } - } - } - } - } - } - - shadow := Rectangle { - visible: false; - width: 100%; - height: 100%; - background: #000000A0; - - TouchArea { - // Consume all interactions so that no elements behind the settings menu can be accessed - } - } - - cast-dialog := CastDialog { - visible: false; - - receivers-model <=> receivers-model; - width: root.width >= 500px ? 500px : root.width; - root-height <=> root.height; - border-radius: root.width >= 500px ? 8px : 0px; - - connect(receiver) => { - connect-receiver(receiver); - } - - disconnect() => { - disconnect-receiver(); - } - - add-receiver-manually(n, a, p) => { - add-receiver-manually(n, a, p); - } - - closed => { - self.visible = false; - shadow.visible = false; - } - } -} diff --git a/ui-common/input.slint b/ui-common/input.slint index 3f6b0df..23e4375 100644 --- a/ui-common/input.slint +++ b/ui-common/input.slint @@ -23,7 +23,7 @@ export component NetworkPortInput { port >= 1 && port <= 65535 } - VerticalBox { + VerticalLayout { width: 100%; input := LabeledLineEdit {