diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 97f41c7..4554a46 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -69,6 +69,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -120,6 +138,17 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -132,6 +161,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -307,6 +345,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -361,6 +442,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -376,6 +463,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -503,6 +599,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" @@ -543,6 +645,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -726,10 +834,11 @@ dependencies = [ [[package]] name = "chat-app-desktop" -version = "0.10.1" +version = "0.10.2" dependencies = [ "base64 0.22.1", "dryoc", + "image", "livekit", "serde", "serde_json", @@ -743,6 +852,8 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tokio", + "windows 0.58.0", + "xcap", ] [[package]] @@ -807,6 +918,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -892,6 +1009,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" @@ -958,6 +1088,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1230,6 +1379,17 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "der" version = "0.7.10" @@ -1623,6 +1783,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1682,12 +1862,47 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -2155,6 +2370,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -2400,6 +2625,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2659,7 +2895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -2777,6 +3013,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "1.9.3" @@ -2819,6 +3095,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "iota-crypto" version = "0.23.2" @@ -2911,6 +3198,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -3100,6 +3396,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -3130,6 +3432,15 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libflate" version = "2.3.0" @@ -3154,6 +3465,16 @@ dependencies = [ "rle-decode-fast", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -3388,6 +3709,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3454,6 +3784,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-5" version = "0.10.6" @@ -3521,6 +3861,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.2" @@ -3536,7 +3886,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -3623,6 +3973,21 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify-rust" version = "4.15.0" @@ -3637,6 +4002,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3659,6 +4043,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3679,6 +4074,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4084,6 +4490,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pbjson" version = "0.6.0" @@ -4431,6 +4843,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -4585,6 +5010,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "prost" version = "0.12.6" @@ -4658,6 +5102,36 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -4868,12 +5342,82 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -5061,6 +5605,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -5780,6 +6330,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -6284,6 +6843,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -6319,7 +6892,7 @@ dependencies = [ "bitflags 2.11.1", "block2", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", "dispatch2", "dlopen2", @@ -6472,7 +7045,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -6862,6 +7435,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -7227,7 +7814,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -7470,6 +8057,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -7791,8 +8389,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -7817,6 +8415,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "whoami" version = "1.6.1" @@ -7886,6 +8490,26 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.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.61.3" @@ -7908,14 +8532,39 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "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.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -7927,8 +8576,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -7945,6 +8594,28 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -7956,6 +8627,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -7989,6 +8682,24 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.4" @@ -8007,6 +8718,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[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.4.2" @@ -8613,12 +9334,47 @@ dependencies = [ "rustix", ] +[[package]] +name = "xcap" +version = "0.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1107223d8283abdd9f22bad27cf36562ef7d3941d82360c75c303656b7dfcb66" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "dbus", + "image", + "log", + "percent-encoding", + "sysinfo", + "thiserror 1.0.69", + "windows 0.58.0", + "xcb", +] + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + [[package]] name = "xkeysym" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.2" @@ -8897,6 +9653,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 35c52c9..80445a1 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -28,11 +28,35 @@ serde_json = "1" dryoc = { version = "0.7", default-features = false, features = ["serde"] } base64 = "0.22" +# PNG encoding for screen-source thumbnails returned by the +# `enumerate_screen_sources` command. `default-features = false` skips the +# image-format decoders we don't use (jpeg, gif, webp, …) — keeps the +# thumbnail command at ~200KB extra binary size. +image = { version = "0.25", default-features = false, features = ["png"] } + +# Cross-platform screen + window enumeration and capture. Replaces direct +# Win32 GDI / macOS CoreGraphics / X11 calls with a small uniform API so +# the enumerate-sources command has one code path. The crate pulls in +# platform-specific backends automatically (~1.5MB binary growth on +# Windows). Marked optional so non-desktop targets don't compile it. +xcap = "0.0.14" + [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-global-shortcut = "2" tauri-plugin-updater = "2" tauri-plugin-window-state = "2" +# Windows-only screen-source enumeration + thumbnail capture. Pulled in +# only on Windows so macOS + Linux builds stay slim. The enumerate command +# returns stub-empty on non-Windows until we add native equivalents. +[target."cfg(target_os = \"windows\")".dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_UI_HiDpi", + "Win32_UI_WindowsAndMessaging", +] } + # LiveKit client SDK — lives behind the `rust-livekit` feature flag so the # baseline build stays unaffected while the JS-SDK path is still the # default. Pulls libwebrtc-rs which adds ~20MB to the binary and ~5-10min diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index c99f0aa..79de4a4 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod crypto; +mod screen_sources; #[cfg(feature = "rust-livekit")] mod livekit_bridge; @@ -91,6 +92,7 @@ pub fn run() { crypto::crypto_box_seal, crypto::crypto_box_seal_open, crypto::crypto_pwhash, + screen_sources::enumerate_screen_sources, ]) .plugin(tauri_plugin_notification::init()); @@ -107,6 +109,7 @@ pub fn run() { crypto::crypto_box_seal, crypto::crypto_box_seal_open, crypto::crypto_pwhash, + screen_sources::enumerate_screen_sources, livekit_bridge::livekit_connect, livekit_bridge::livekit_disconnect, livekit_bridge::livekit_send_data, diff --git a/apps/desktop/src-tauri/src/screen_sources.rs b/apps/desktop/src-tauri/src/screen_sources.rs new file mode 100644 index 0000000..72aed8d --- /dev/null +++ b/apps/desktop/src-tauri/src/screen_sources.rs @@ -0,0 +1,191 @@ +// Source enumeration for the Discord-style screen-share picker. Returns a +// flat list of screens + windows with small PNG thumbnails so the JS +// picker can render a grid without the browser's native picker. +// +// The `id` field is emitted in Chromium's internal desktopCapturer format +// ("screen::0" / "window::0") so the JS side can try to pass it +// straight into getUserMedia's `chromeMediaSourceId` constraint. That +// bypass may or may not be accepted by WebView2 depending on the version +// — if it isn't, the JS falls back to the ordinary getDisplayMedia flow +// and at least the user has seen an informed preview first. +// +// xcap abstracts the platform-specific capture APIs (Windows GDI + DXGI, +// macOS CoreGraphics/ScreenCaptureKit, X11) so the code here stays flat. +// Thumbnails are captured at native resolution, then letterbox-downscaled +// to fit a 320×180 box to keep the base64 payload small. + +use base64::Engine; +use image::{ImageBuffer, Rgba}; +use serde::Serialize; + +const THUMB_MAX_W: u32 = 320; +const THUMB_MAX_H: u32 = 180; + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScreenSource { + /// Chromium-format source id — stable within one enumeration call. + pub id: String, + /// Human-readable label for the picker (monitor name or window title). + pub name: String, + /// Discriminator for the grid grouping. + pub kind: &'static str, + /// Base64-encoded PNG, no data-URL prefix. None when the capture + /// fails (minimised window, permission-denied surface, transient + /// race). The UI renders a name-only card in that case. + pub thumbnail_png: Option, + /// Native width of the full-res source — mostly informational, used + /// by the UI for aspect-ratio styling of the card. + pub width: u32, + pub height: u32, +} + +#[tauri::command] +pub fn enumerate_screen_sources() -> Result, String> { + let mut out: Vec = Vec::new(); + append_monitors(&mut out); + append_windows(&mut out); + Ok(out) +} + +// --------------------------------------------------------------------------- +// Monitors +// --------------------------------------------------------------------------- + +fn append_monitors(out: &mut Vec) { + let monitors = match xcap::Monitor::all() { + Ok(m) => m, + Err(err) => { + eprintln!("xcap Monitor::all failed: {err}"); + return; + } + }; + for (idx, m) in monitors.iter().enumerate() { + let name = monitor_label(m, idx); + let width = m.width(); + let height = m.height(); + // xcap exposes an OS-level monitor id; pass it through as the + // middle component so the JS side can correlate repeat enumerations. + let raw_id = m.id(); + let id = format!("screen:{raw_id}:0"); + let thumb = capture_monitor_thumbnail(m); + out.push(ScreenSource { + id, + name, + kind: "screen", + thumbnail_png: thumb, + width, + height, + }); + } +} + +fn monitor_label(m: &xcap::Monitor, idx: usize) -> String { + let name = m.name(); + if name.is_empty() { + format!("Bildschirm {}", idx + 1) + } else { + name.to_string() + } +} + +fn capture_monitor_thumbnail(m: &xcap::Monitor) -> Option { + let image = m.capture_image().ok()?; + encode_scaled_png(image) +} + +// --------------------------------------------------------------------------- +// Windows +// --------------------------------------------------------------------------- + +fn append_windows(out: &mut Vec) { + let windows = match xcap::Window::all() { + Ok(w) => w, + Err(err) => { + eprintln!("xcap Window::all failed: {err}"); + return; + } + }; + for w in windows.iter() { + if !is_shareable_window(w) { + continue; + } + let title = w.title(); + if title.trim().is_empty() { + continue; + } + let width = w.width(); + let height = w.height(); + let raw_id = w.id(); + let id = format!("window:{raw_id}:0"); + let thumb = capture_window_thumbnail(w); + out.push(ScreenSource { + id, + name: title.to_string(), + kind: "window", + thumbnail_png: thumb, + width, + height, + }); + } +} + +fn is_shareable_window(w: &xcap::Window) -> bool { + if w.is_minimized() { + return false; + } + let width = w.width(); + let height = w.height(); + // Tooltips, invisible tray-helpers, etc. sit at or near zero size — + // they'd clutter the picker grid and usually can't be captured anyway. + if width < 80 || height < 60 { + return false; + } + true +} + +fn capture_window_thumbnail(w: &xcap::Window) -> Option { + let image = w.capture_image().ok()?; + encode_scaled_png(image) +} + +// --------------------------------------------------------------------------- +// Scaling + encoding +// --------------------------------------------------------------------------- + +// Letterbox-shrink the captured image so the long edge is at most +// THUMB_MAX_W / THUMB_MAX_H. Keeps aspect ratio, skips upscaling entirely +// (tiny windows stay their captured size). Returns a base64 PNG string or +// None if encoding fails. +fn encode_scaled_png(src: ImageBuffer, Vec>) -> Option { + let (w, h) = src.dimensions(); + if w == 0 || h == 0 { + return None; + } + let scale = (THUMB_MAX_W as f32 / w as f32) + .min(THUMB_MAX_H as f32 / h as f32) + .min(1.0); + let scaled = if scale < 1.0 { + let new_w = ((w as f32) * scale).round().max(1.0) as u32; + let new_h = ((h as f32) * scale).round().max(1.0) as u32; + image::imageops::resize(&src, new_w, new_h, image::imageops::FilterType::Triangle) + } else { + src + }; + let mut buf: Vec = Vec::new(); + let encoder = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + image::codecs::png::CompressionType::Fast, + image::codecs::png::FilterType::Adaptive, + ); + use image::ImageEncoder; + encoder + .write_image( + scaled.as_raw(), + scaled.width(), + scaled.height(), + image::ExtendedColorType::Rgba8, + ) + .ok()?; + Some(base64::engine::general_purpose::STANDARD.encode(&buf)) +} diff --git a/apps/desktop/src/components/InCallPanel.tsx b/apps/desktop/src/components/InCallPanel.tsx index 8d791d9..ecbd934 100644 --- a/apps/desktop/src/components/InCallPanel.tsx +++ b/apps/desktop/src/components/InCallPanel.tsx @@ -18,7 +18,7 @@ import { FocusIcon, GridIcon, LockIcon, MaximizeIcon, SpinnerIcon, UsersIcon } f import { ParticipantsPopover, type ParticipantRow } from './ParticipantsPopover'; import { ParticipantVolumeMenu } from './ParticipantVolumeMenu'; import { ScreenShareContextMenu } from './ScreenShareContextMenu'; -import { ScreenShareDialog } from './ScreenShareDialog'; +import { ScreenSourcePicker } from './ScreenSourcePicker'; import { ScreenShareViewer } from './ScreenShareViewer'; import { SoundboardPanel } from './SoundboardPanel'; @@ -84,7 +84,7 @@ export function InCallPanel({ conversation }: Props) { const { session } = useAuth(); const myId = session?.user.id ?? null; const activeSpeakers = useActiveSpeakers(room); - const [shareDialogOpen, setShareDialogOpen] = useState(false); + const [pickerOpen, setPickerOpen] = useState(false); const [soundboardOpen, setSoundboardOpen] = useState(false); const [participantsOpen, setParticipantsOpen] = useState(false); const [volumeMenu, setVolumeMenu] = useState< @@ -212,19 +212,19 @@ export function InCallPanel({ conversation }: Props) { video={isCameraEnabled} deafened={isDeafened} onToggleMute={toggleMute} - // 1-click share uses last-saved preset + displaySurface. Right-click - // opens the quality picker for users who want to change settings - // before starting — matches Discord's "Go Live" vs quick-share split. + // Click opens the Discord-style source picker (thumbnails + quality + + // audio). Clicking again while a share is live stops it. Right-click + // also opens the picker in case the user wants to swap sources. onToggleShare={() => { if (isScreenSharing) { void stopScreenShare(); } else { - void startScreenShare(); + setPickerOpen(true); } }} onShareContextMenu={(e) => { e.preventDefault(); - if (!isScreenSharing) setShareDialogOpen(true); + if (!isScreenSharing) setPickerOpen(true); }} onToggleVideo={() => void toggleCamera()} onToggleDeafen={toggleDeafen} @@ -412,9 +412,9 @@ export function InCallPanel({ conversation }: Props) { - setShareDialogOpen(false)} + setPickerOpen(false)} onStart={async (opts) => { await startScreenShare(opts); }} diff --git a/apps/desktop/src/components/ScreenShareDialog.tsx b/apps/desktop/src/components/ScreenShareDialog.tsx deleted file mode 100644 index f87f4fb..0000000 --- a/apps/desktop/src/components/ScreenShareDialog.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - type DisplaySurfaceHint, - getPresetParams, - getScreenShareSettings, - PRESET_ORDER, - type ScreenSharePreset, - updateScreenShareSettings, -} from '../lib/screenShareSettings'; -import { - MonitorShareIcon, - SpinnerIcon, - XIcon, -} from './icons'; - -interface Props { - open: boolean; - onClose: () => void; - onStart: (opts: { - preset: ScreenSharePreset; - displaySurface: DisplaySurfaceHint; - framerate: number | null; - }) => Promise; -} - -const FRAMERATE_OPTIONS: ReadonlyArray<{ value: number | null; label: string }> = [ - { value: null, label: 'Preset-Standard' }, - { value: 15, label: '15 fps' }, - { value: 30, label: '30 fps' }, - { value: 60, label: '60 fps' }, -]; - -// Discord-style pre-share dialog. The OS still owns the final source picker -// (browser/OS limitation — only Chrome/Edge plus a native plugin can enumerate -// windows from JS), but we pre-filter with the `displaySurface` hint and lock -// in quality + framerate up-front so the user doesn't have to re-open the -// system picker to adjust them mid-call. -export function ScreenShareDialog({ open, onClose, onStart }: Props) { - const { t } = useTranslation(['app']); - const initial = getScreenShareSettings(); - const [surface, setSurface] = useState(initial.displaySurface); - const [preset, setPreset] = useState(initial.preset); - const [framerate, setFramerate] = useState(initial.framerateOverride); - const [includeAudio, setIncludeAudio] = useState(initial.includeSystemAudio); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - - if (!open) return null; - - async function handleStart() { - setBusy(true); - setError(null); - try { - // Persist the audio choice alongside the other picker prefs so the - // upstream startScreenShare picks it up on its settings read. - updateScreenShareSettings({ includeSystemAudio: includeAudio }); - await onStart({ preset, displaySurface: surface, framerate }); - onClose(); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : 'screenshare failed'); - } finally { - setBusy(false); - } - } - - const presetParams = getPresetParams(preset); - const effectiveFps = framerate ?? presetParams.framerate; - - return ( -
-
e.stopPropagation()} - className="flex w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-line bg-surface-3 shadow-xl" - > -
-
- -

- {t('app:call.share_dialog_title', { defaultValue: 'Bildschirm teilen' })} -

-
- -
- -
-
-

- {t('app:call.share_surface', { defaultValue: 'Quelle' })} -

-
- setSurface(null)} - label={t('app:call.share_any', { defaultValue: 'Alle anzeigen' })} - sub={t('app:call.share_any_sub', { defaultValue: 'Bildschirm + Fenster' })} - /> - setSurface('monitor')} - label={t('app:call.share_monitor', { defaultValue: 'Bildschirm' })} - sub={t('app:call.share_monitor_sub', { defaultValue: 'Ganzer Monitor' })} - /> - setSurface('window')} - label={t('app:call.share_window', { defaultValue: 'Fenster' })} - sub={t('app:call.share_window_sub', { defaultValue: 'Einzelnes Fenster' })} - /> -
-
- -
-

- {t('app:call.share_quality', { defaultValue: 'Qualität' })} -

- -
- -
-

- {t('app:call.share_fps', { defaultValue: 'Bildrate' })} -

-
- {FRAMERATE_OPTIONS.map((opt) => ( - - ))} -
-

- {t('app:call.share_fps_effective', { - defaultValue: 'Effektiv: {{fps}} fps', - fps: effectiveFps, - })} -

-
- -
- -
- - {error && ( -

- {error} -

- )} - -

- {t('app:call.share_hint', { - defaultValue: - 'Nach "Teilen starten" öffnet das Betriebssystem den Quellen-Picker. Qualität + Bildrate werden bereits angewendet.', - })} -

-
- -
- - -
-
-
- ); -} - -function SurfaceOption({ - active, - onClick, - label, - sub, -}: { - active: boolean; - onClick: () => void; - label: string; - sub: string; -}) { - return ( - - ); -} diff --git a/apps/desktop/src/components/ScreenSourcePicker.tsx b/apps/desktop/src/components/ScreenSourcePicker.tsx new file mode 100644 index 0000000..1da4c79 --- /dev/null +++ b/apps/desktop/src/components/ScreenSourcePicker.tsx @@ -0,0 +1,319 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + type DisplaySurfaceHint, + getPresetParams, + getScreenShareSettings, + PRESET_ORDER, + type ScreenSharePreset, + updateScreenShareSettings, +} from '../lib/screenShareSettings'; +import { + enumerateScreenSources, + type ScreenSource, + thumbnailDataUrl, +} from '../lib/screenSources'; +import { MonitorShareIcon, SpinnerIcon, XIcon } from './icons'; + +interface Props { + open: boolean; + onClose: () => void; + /** Parent handles the actual share start. `sourceId` is null when the user + * clicks "Teilen" without picking a specific source — fallback to the + * OS-level getDisplayMedia picker. */ + onStart: (opts: { + sourceId: string | null; + preset: ScreenSharePreset; + displaySurface: DisplaySurfaceHint; + framerate: number | null; + includeAudio: boolean; + }) => Promise; +} + +// Discord-style picker. Replaces the old form-field dialog with a thumbnail +// grid sourced from the Rust `enumerate_screen_sources` command. Clicking a +// thumbnail stashes its Chromium-format id; the parent then attempts a +// `chromeMediaSourceId`-constrained getUserMedia call. If WebView2 ignores +// the constraint (it may), the fallback OS picker still runs — but at least +// the user already saw + chose from a real preview first. +export function ScreenSourcePicker({ open, onClose, onStart }: Props) { + const { t } = useTranslation(['app']); + const initial = getScreenShareSettings(); + const [sources, setSources] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [preset, setPreset] = useState(initial.preset); + const [includeAudio, setIncludeAudio] = useState(initial.includeSystemAudio); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + // Re-enumerate every time the picker opens so closed windows + new ones + // stay accurate. A previous stale list would surface sources the user + // can't actually share anymore. + useEffect(() => { + if (!open) { + setSources(null); + setSelectedId(null); + setError(null); + return; + } + let cancelled = false; + void (async () => { + const list = await enumerateScreenSources(); + if (cancelled) return; + setSources(list); + })(); + return () => { + cancelled = true; + }; + }, [open]); + + if (!open) return null; + + const screens = sources?.filter((s) => s.kind === 'screen') ?? []; + const windows = sources?.filter((s) => s.kind === 'window') ?? []; + const hasAny = (sources?.length ?? 0) > 0; + + async function handleStart(): Promise { + setBusy(true); + setError(null); + try { + // Persist user's audio + preset choice so subsequent shares start with + // the same prefs when they skip the picker. The picker itself stays + // as the entry for future starts (right-click on share button also + // opens it — see InCallPanel wiring). + updateScreenShareSettings({ preset, includeSystemAudio: includeAudio }); + // Infer a displaySurface hint from the selection so the fallback OS + // picker jumps to the right tab when our direct-publish path is + // rejected by WebView2. + const selected = sources?.find((s) => s.id === selectedId) ?? null; + const hint: DisplaySurfaceHint = + selected?.kind === 'screen' + ? 'monitor' + : selected?.kind === 'window' + ? 'window' + : null; + await onStart({ + sourceId: selected?.id ?? null, + preset, + displaySurface: hint, + framerate: null, + includeAudio, + }); + onClose(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'screenshare failed'); + } finally { + setBusy(false); + } + } + + return ( +
+
e.stopPropagation()} + className="flex max-h-[88vh] w-full max-w-[860px] flex-col overflow-hidden rounded-2xl border border-line bg-surface-3 shadow-xl" + > +
+
+ +

+ {t('app:call.share_dialog_title', { defaultValue: 'Bildschirm teilen' })} +

+
+ +
+ +
+ {sources === null ? ( +
+ + + {t('app:call.share_loading', { defaultValue: 'Lade Quellen…' })} + +
+ ) : !hasAny ? ( +
+ + + {t('app:call.share_no_sources', { + defaultValue: + 'Keine Quellen-Previews verfügbar. Klick auf „Teilen" öffnet den System-Picker.', + })} + +
+ ) : ( +
+ {screens.length > 0 && ( + + )} + {windows.length > 0 && ( + + )} +
+ )} +
+ +
+
+ + +
+ {error && ( +

+ {error} +

+ )} +
+ + +
+
+
+
+ ); +} + +function SourceSection({ + title, + sources, + selectedId, + onSelect, +}: { + title: string; + sources: ScreenSource[]; + selectedId: string | null; + onSelect: (id: string) => void; +}) { + return ( +
+

+ {title} +

+
+ {sources.map((src) => ( + onSelect(src.id)} + /> + ))} +
+
+ ); +} + +function SourceCard({ + source, + selected, + onClick, +}: { + source: ScreenSource; + selected: boolean; + onClick: () => void; +}) { + const thumb = thumbnailDataUrl(source); + return ( + + ); +} diff --git a/apps/desktop/src/context/CallContext.tsx b/apps/desktop/src/context/CallContext.tsx index 97e8be6..058b577 100644 --- a/apps/desktop/src/context/CallContext.tsx +++ b/apps/desktop/src/context/CallContext.tsx @@ -1130,6 +1130,11 @@ export function CallProvider({ children }: { children: ReactNode }) { preset: ScreenSharePreset; displaySurface: DisplaySurfaceHint; framerate: number | null; + /** Chromium-format source id from our custom picker. When set, we + * try to capture that exact source via `chromeMediaSourceId` + * instead of the OS-level getDisplayMedia picker. Falls back to + * getDisplayMedia if WebView2 rejects the constraint. */ + sourceId: string | null; }>, ) => { const r = roomRef.current; @@ -1153,6 +1158,96 @@ export function CallProvider({ children }: { children: ReactNode }) { const ssParams = getPresetParams(preset); const fps = framerateOverride ?? ssParams.framerate; + const sourceId = overrides?.sourceId ?? null; + + // Direct-publish path when our custom picker supplied a Chromium- + // format source id. Bypasses the OS picker so the user shares exactly + // the window/monitor they clicked in the grid. getUserMedia with the + // legacy chromeMediaSourceId constraint is not in the MediaStream + // spec but is honoured by Chromium / WebView2. If it throws we fall + // through to setScreenShareEnabled and let the OS picker run. + if (sourceId) { + try { + const { Track: LkTrack } = await import('livekit-client'); + const maxWidth = ssParams.dims?.width ?? 3840; + const maxHeight = ssParams.dims?.height ?? 2160; + // Cast chains: browsers expose the legacy constraint via + // `MediaTrackConstraints.mandatory` which isn't in lib.dom. + const videoConstraints = { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + maxWidth, + maxHeight, + maxFrameRate: fps, + }, + } as unknown as MediaTrackConstraints; + const audioConstraints: MediaTrackConstraints | false = settings.includeSystemAudio + ? ({ + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + }, + } as unknown as MediaTrackConstraints) + : false; + const stream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: videoConstraints, + }); + const videoMst = stream.getVideoTracks()[0]; + const audioMst = stream.getAudioTracks()[0]; + if (!videoMst) { + stream.getTracks().forEach((t) => t.stop()); + throw new Error('no video track from chromeMediaSource'); + } + // Pass raw MediaStreamTracks — `publishTrack` wraps them in the + // right Local*Track internally and the publishDefaults on the + // Room handle VP9 codec + screenShareEncoding caps. Passing the + // raw tracks also sidesteps a type incompatibility between + // livekit-client's Local*Track and our exactOptionalPropertyTypes + // setting. + const videoPub = await lp.publishTrack(videoMst, { + source: LkTrack.Source.ScreenShare, + videoCodec: 'vp9', + }); + // Stop the publish when the OS revokes capture (user hit the + // OS "Stop sharing" banner, or closed the window we were sharing). + videoMst.addEventListener('ended', () => { + void (async () => { + try { + if (videoPub.track) await lp.unpublishTrack(videoPub.track); + } catch { + /* already unpublished */ + } + setIsScreenSharing(false); + })(); + }); + if (audioMst) { + const audioPub = await lp.publishTrack(audioMst, { + source: LkTrack.Source.ScreenShareAudio, + }); + audioMst.addEventListener('ended', () => { + void (async () => { + try { + if (audioPub.track) await lp.unpublishTrack(audioPub.track); + } catch { + /* ignore */ + } + })(); + }); + } + setIsScreenSharing(true); + return; + } catch (err: unknown) { + // WebView2 / browser rejected the legacy constraint. Fall through + // to the normal OS picker path below so the user still gets a + // working share instead of a hard error. + console.warn( + 'direct screen-share via chromeMediaSourceId failed; falling back to getDisplayMedia', + err, + ); + } + } try { await lp.setScreenShareEnabled(true, { diff --git a/apps/desktop/src/lib/screenSources.ts b/apps/desktop/src/lib/screenSources.ts new file mode 100644 index 0000000..82d42d4 --- /dev/null +++ b/apps/desktop/src/lib/screenSources.ts @@ -0,0 +1,38 @@ +// Frontend wrapper for the Rust `enumerate_screen_sources` command. Falls +// back to an empty list outside the Tauri runtime so a browser-only dev +// build (pnpm vite:dev in Chrome without Tauri) degrades gracefully to +// "nothing to show" rather than throwing. + +import { isTauriRuntime } from './globalShortcut'; + +export type ScreenSourceKind = 'screen' | 'window'; + +export interface ScreenSource { + /** Chromium-format source id ("screen::0" / "window::0"). */ + id: string; + name: string; + kind: ScreenSourceKind; + /** Base64-encoded PNG without a data-URL prefix. Null when capture failed. */ + thumbnailPng: string | null; + width: number; + height: number; +} + +export async function enumerateScreenSources(): Promise { + if (!isTauriRuntime()) return []; + try { + const { invoke } = await import('@tauri-apps/api/core'); + const raw = await invoke('enumerate_screen_sources'); + return raw ?? []; + } catch (err: unknown) { + console.warn('enumerate_screen_sources failed', err); + return []; + } +} + +// Data-URL helper — picker tiles bind `src={thumbnailDataUrl(src)}` so the +// thumbnail bytes never leave the component's render pass. +export function thumbnailDataUrl(src: ScreenSource): string | null { + if (!src.thumbnailPng) return null; + return 'data:image/png;base64,' + src.thumbnailPng; +}