diff --git a/Cargo.lock b/Cargo.lock index 2652296ec1..621d8d3582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,12 @@ dependencies = [ "x11rb 0.13.1", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -384,9 +390,9 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -419,9 +425,9 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -539,7 +545,7 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", @@ -561,12 +567,12 @@ dependencies = [ "log", "peeking_take_while", "prettyplease", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.98", "which", ] @@ -582,12 +588,12 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -618,7 +624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb15541e888071f64592c0b4364fdff21b7cb0a247f984296699351963a8721" dependencies = [ "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -793,7 +799,7 @@ dependencies = [ "glib 0.18.5", "libc", "once_cell", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -809,13 +815,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.102" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -993,7 +999,7 @@ dependencies = [ "rand 0.8.5", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", "utf16string", "uuid", "x11-clipboard 0.8.1", @@ -1045,6 +1051,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -1129,7 +1150,7 @@ source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc dependencies = [ "directories-next", "serde 1.0.203", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", ] @@ -1164,7 +1185,7 @@ version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-xid 0.2.4", ] @@ -1181,6 +1202,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1200,6 +1231,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1214,6 +1251,18 @@ dependencies = [ "objc2-encode 2.0.0-pre.2", ] +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.22.3" @@ -1275,6 +1324,31 @@ dependencies = [ "libc", ] +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal", + "objc", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1614,7 +1688,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1748,7 +1822,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1800,6 +1874,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dylib_virtual_display" version = "0.1.0" @@ -1809,7 +1889,7 @@ dependencies = [ "lazy_static", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1859,7 +1939,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1877,9 +1957,9 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -1898,9 +1978,9 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2102,7 +2182,7 @@ dependencies = [ "log", "nu-ansi-term", "regex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2111,6 +2191,9 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", + "nanorand", "spin", ] @@ -2186,9 +2269,9 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2353,9 +2436,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2510,8 +2593,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2546,7 +2631,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2614,7 +2699,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2628,7 +2713,7 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -2642,9 +2727,9 @@ dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.2", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2716,7 +2801,7 @@ dependencies = [ "once_cell", "paste", "pretty-hex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2875,9 +2960,9 @@ checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2967,7 +3052,7 @@ dependencies = [ "socket2 0.3.19", "sodiumoxide", "sysinfo", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-native-tls", "tokio-rustls 0.26.0", @@ -3252,7 +3337,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", ] @@ -3384,7 +3469,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", ] @@ -3399,7 +3484,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", "windows-sys 0.45.0", ] @@ -3804,6 +3889,21 @@ dependencies = [ "autocfg 1.3.0", ] +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + [[package]] name = "mime" version = "0.3.17" @@ -3838,6 +3938,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mozjpeg" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55571bce4f12d80ceb4296526e7614f796df72daaaac85f265ab732fa47b7bc9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3626d7942d5b56cc6d47b1c59724c0a976b786fca059c5aaa904aef6324d55" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + [[package]] name = "muda" version = "0.13.5" @@ -3853,7 +3978,7 @@ dependencies = [ "objc", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -3863,6 +3988,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3903,7 +4046,7 @@ dependencies = [ "ndk-sys 0.4.1+23.1.7779620", "num_enum 0.5.11", "raw-window-handle 0.5.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3917,7 +4060,7 @@ dependencies = [ "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum 0.7.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3979,7 +4122,7 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4043,6 +4186,67 @@ dependencies = [ "libc", ] +[[package]] +name = "nokhwa" +version = "0.10.7" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "flume", + "image 0.25.1", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.11", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.1" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "nokhwa-core", + "once_cell", + "windows 0.43.0", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.5" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "bytes", + "image 0.25.1", + "mozjpeg", + "thiserror 2.0.11", +] + [[package]] name = "nom" version = "7.1.3" @@ -4102,7 +4306,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4113,9 +4317,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4191,7 +4395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4203,9 +4407,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 2.0.2", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4440,9 +4644,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4560,7 +4764,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4745,9 +4949,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4861,8 +5065,8 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.68", + "proc-macro2 1.0.93", + "syn 2.0.98", ] [[package]] @@ -4910,7 +5114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "version_check", @@ -4922,7 +5126,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "version_check", ] @@ -4938,9 +5142,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -4954,7 +5158,7 @@ dependencies = [ "bytes", "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4969,7 +5173,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4984,7 +5188,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.61", "which", ] @@ -4994,7 +5198,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e2d30ab1878b2e72d1e2fc23ff5517799c9929e2cf81a8516f9f4dcf2b9cf3" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -5078,7 +5282,7 @@ version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", ] [[package]] @@ -5323,7 +5527,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -5412,6 +5616,15 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -5892,6 +6105,7 @@ dependencies = [ "log", "ndk 0.7.0", "ndk-context", + "nokhwa", "num_cpus", "pkg-config", "quest", @@ -5965,9 +6179,9 @@ version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -5999,9 +6213,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6228,7 +6442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6240,7 +6454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "rustversion", "syn 1.0.109", @@ -6269,18 +6483,18 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.68" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] @@ -6345,7 +6559,7 @@ dependencies = [ "pkg-config", "strum 0.18.0", "strum_macros 0.18.0", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", "version-compare 0.0.10", ] @@ -6418,7 +6632,7 @@ name = "tao-macros" version = "0.1.2" source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6513,7 +6727,16 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -6522,9 +6745,20 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", ] [[package]] @@ -6631,9 +6865,9 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6675,7 +6909,7 @@ checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.61", "tokio", ] @@ -6690,7 +6924,7 @@ dependencies = [ "futures-sink", "futures-util", "pin-project", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-util", ] @@ -6819,9 +7053,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6858,7 +7092,7 @@ dependencies = [ "objc2-foundation", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -7067,6 +7301,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -7129,7 +7383,7 @@ dependencies = [ "dirs 5.0.1", "enquote", "rust-ini", - "thiserror", + "thiserror 1.0.61", "winapi 0.3.9", "winreg 0.11.0", ] @@ -7180,9 +7434,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -7214,9 +7468,9 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7284,7 +7538,7 @@ version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quick-xml 0.34.0", "quote 1.0.36", ] @@ -7463,6 +7717,21 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows" version = "0.44.0" @@ -7547,9 +7816,9 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -7558,9 +7827,9 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -7915,7 +8184,7 @@ dependencies = [ "os_pipe", "rustix 0.38.34", "tempfile", - "thiserror", + "thiserror 1.0.61", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -8094,7 +8363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "syn 1.0.109", @@ -8136,9 +8405,9 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -8147,9 +8416,9 @@ version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -8255,7 +8524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "zvariant_utils", @@ -8267,7 +8536,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 205f30a683..bf769d4945 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -29,8 +29,10 @@ import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; +import 'mobile/pages/view_camera_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; +import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -96,6 +98,7 @@ enum DesktopType { main, remote, fileTransfer, + viewCamera, cm, portForward, } @@ -1750,7 +1753,8 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { await bind.setLocalFlutterOption( k: windowFramePrefix + type.name, v: pos.toString()); - if (type == WindowType.RemoteDesktop && windowId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null) { await _saveSessionWindowPosition( type, windowId, isMaximized, isFullscreen, pos); } @@ -1901,7 +1905,9 @@ Future restoreWindowPosition(WindowType type, String? pos; // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) // Though "open in tabs" is true and the new window restore peer position, it's ok. - if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null && + peerId != null) { final peerPos = bind.mainGetPeerFlutterOptionSync( id: peerId, k: windowFramePrefix + type.name); if (peerPos.isNotEmpty) { @@ -1916,7 +1922,7 @@ Future restoreWindowPosition(WindowType type, debugPrint("no window position saved, ignoring position restoration"); return false; } - if (type == WindowType.RemoteDesktop) { + if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) { if (!isRemotePeerPos && windowId != null) { if (lpos.offsetWidth != null) { lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; @@ -2085,6 +2091,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) { enum UriLinkType { remoteDesktop, fileTransfer, + viewCamera, portForward, rdp, } @@ -2136,6 +2143,11 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--view-camera': + type = UriLinkType.viewCamera; + id = args[i + 1]; + i++; + break; case '--port-forward': type = UriLinkType.portForward; id = args[i + 1]; @@ -2177,6 +2189,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password: password, forceRelay: forceRelay); }); break; + case UriLinkType.viewCamera: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newViewCamera(id!, + password: password, forceRelay: forceRelay); + }); + break; case UriLinkType.portForward: Future.delayed(Duration.zero, () { rustDeskWinManager.newPortForward(id!, false, @@ -2200,7 +2218,14 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { List? urlLinkToCmdArgs(Uri uri) { String? command; String? id; - final options = ["connect", "play", "file-transfer", "port-forward", "rdp"]; + final options = [ + "connect", + "play", + "file-transfer", + "view-camera", + "port-forward", + "rdp" + ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { return []; @@ -2238,6 +2263,8 @@ List? urlLinkToCmdArgs(Uri uri) { connect(Get.context!, id); } else if (optionIndex == 2) { connect(Get.context!, id, isFileTransfer: true); + } else if (optionIndex == 3) { + connect(Get.context!, id, isViewCamera: true); } return null; } @@ -2290,6 +2317,7 @@ List? urlLinkToCmdArgs(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, + required bool isViewCamera, required bool isTcpTunneling, required bool isRDP, bool? forceRelay, @@ -2302,6 +2330,12 @@ connectMainDesktop(String id, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); + } else if (isViewCamera) { + await rustDeskWinManager.newViewCamera(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, @@ -2318,10 +2352,12 @@ connectMainDesktop(String id, /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. +/// If [isViewCamera], starts a session only for view camera. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. connect(BuildContext context, String id, {bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, bool forceRelay = false, @@ -2353,6 +2389,7 @@ connect(BuildContext context, String id, await connectMainDesktop( id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, password: password, @@ -2363,6 +2400,7 @@ connect(BuildContext context, String id, await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, 'password': password, @@ -2400,6 +2438,31 @@ connect(BuildContext context, String id, ), ); } + } else if (isViewCamera) { + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_view_camera.ViewCameraPage( + key: ValueKey(id), + id: id, + toolbarState: ToolbarState(), + password: password, + forceRelay: forceRelay, + isSharedPassword: isSharedPassword, + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ViewCameraPage( + id: id, password: password, isSharedPassword: isSharedPassword), + ), + ); + } } else { if (isWeb) { Navigator.push( @@ -2686,6 +2749,8 @@ String getWindowName({WindowType? overrideType}) { return name; case WindowType.FileTransfer: return "File Transfer - $name"; + case WindowType.ViewCamera: + return "View Camera - $name"; case WindowType.PortForward: return "Port Forward - $name"; case WindowType.RemoteDesktop: @@ -3051,6 +3116,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, 'peer_id': peerId, 'display': i, 'display_count': pi.displays.length, + 'window_type': (kWindowType ?? WindowType.RemoteDesktop).index, }; if (screenRect != null) { args['screen_rect'] = { @@ -3065,12 +3131,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, } setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount, - int? display, Rect? screenRect) async { + WindowType windowType, int? display, Rect? screenRect) async { if (screenRect == null) { // Do not restore window position to new connection if there's a pre-session. // https://github.com/rustdesk/rustdesk/discussions/8825 if (preSessionCount == 0) { - await restoreWindowPosition(WindowType.RemoteDesktop, + await restoreWindowPosition(windowType, windowId: windowId, display: display, peerId: peerId); } } else { diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 6d00edc8d7..367aa8685f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -488,6 +488,7 @@ abstract class BasePeerCard extends StatelessWidget { BuildContext context, String title, { bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, }) { @@ -502,6 +503,7 @@ abstract class BasePeerCard extends StatelessWidget { peer, tab, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, ); @@ -530,6 +532,15 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _viewCameraAction(BuildContext context) { + return _connectCommonAction( + context, + translate('View camera'), + isViewCamera: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -880,6 +891,7 @@ class RecentPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -939,6 +951,7 @@ class FavoritePeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -992,6 +1005,7 @@ class DiscoveredPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1045,6 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1177,6 +1192,7 @@ class MyGroupPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1398,6 +1414,7 @@ class TagPainter extends CustomPainter { void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, {bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false}) async { var password = ''; @@ -1423,6 +1440,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, password: password, isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP); } diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 6667bdf802..36fb453399 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget { class RawTouchGestureDetectorRegion extends StatefulWidget { final Widget child; final FFI ffi; - + final bool isCamera; late final InputModel inputModel = ffi.inputModel; late final FfiModel ffiModel = ffi.ffiModel; RawTouchGestureDetectorRegion({ required this.child, required this.ffi, + this.isCamera = false, }); @override @@ -382,6 +384,7 @@ class _RawTouchGestureDetectorRegionState _scale = d.scale; if (scale != 0) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -402,6 +405,7 @@ class _RawTouchGestureDetectorRegionState return; } if ((isDesktop || isWebDesktop)) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -536,3 +540,46 @@ class RawPointerMouseRegion extends StatelessWidget { ); } } + +class CameraRawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + CameraRawPointerMouseRegion({ + this.onEnter, + this.onExit, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: (evt) { + final offset = evt.position; + double x = offset.dx; + double y = max(0.0, offset.dy); + inputModel.handlePointerDevicePos( + kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault); + }, + onPointerDown: (evt) { + onPointerDown?.call(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + }, + child: MouseRegion( + cursor: MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child, + ), + ); + } +} diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 153121057e..dd957ad00e 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -89,10 +89,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; List v = []; // elevation - if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) { + if (isDefaultConn && + perms['keyboard'] != false && + ffi.elevationModel.showRequestMenu) { v.add( TTextMenu( child: Text(translate('Request Elevation')), @@ -101,7 +104,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // osAccount / osPassword - if (perms['keyboard'] != false) { + if (isDefaultConn && perms['keyboard'] != false) { v.add( TTextMenu( child: Row(children: [ @@ -130,7 +133,9 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // paste - if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) { + if (isDefaultConn && + pi.platform != kPeerPlatformAndroid && + perms['keyboard'] != false) { v.add(TTextMenu( child: Text(translate('Send clipboard keystrokes')), onPressed: () async { @@ -142,43 +147,53 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // reset canvas - if (isMobile) { + if (isDefaultConn && isMobile) { v.add(TTextMenu( child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } connectWithToken( - {required bool isFileTransfer, required bool isTcpTunneling}) { + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false}) { final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); connect(context, id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, connToken: connToken); } // transferFile - if (isDesktop) { + if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => - connectWithToken(isFileTransfer: true, isTcpTunneling: false)), + onPressed: () => connectWithToken(isFileTransfer: true)), + ); + } + // viewCamera + if (isDefaultConn && isDesktop) { + v.add( + TTextMenu( + child: Text(translate('View camera')), + onPressed: () => connectWithToken(isViewCamera: true)), ); } // tcpTunneling - if (isDesktop) { + if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => - connectWithToken(isFileTransfer: false, isTcpTunneling: true)), + onPressed: () => connectWithToken(isTcpTunneling: true)), ); } // note - if (bind - .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") - .isNotEmpty) { + if (isDefaultConn && + bind + .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") + .isNotEmpty) { v.add( TTextMenu( child: Text(translate('Note')), @@ -186,11 +201,12 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // divider - if (isDesktop || isWebDesktop) { + if (isDefaultConn && (isDesktop || isWebDesktop)) { v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); } // ctrlAltDel - if (!ffiModel.viewOnly && + if (isDefaultConn && + !ffiModel.viewOnly && ffiModel.keyboard && (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( @@ -200,7 +216,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // restart - if (perms['restart'] != false && + if (isDefaultConn && + perms['restart'] != false && (pi.platform == kPeerPlatformLinux || pi.platform == kPeerPlatformWindows || pi.platform == kPeerPlatformMacOS)) { @@ -212,7 +229,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // insertLock - if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) { + if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) { v.add( TTextMenu( child: Text(translate('Insert Lock')), @@ -220,7 +237,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // blockUserInput - if (ffi.ffiModel.keyboard && + if (isDefaultConn && + ffi.ffiModel.keyboard && ffi.ffiModel.permissions['block_input'] != false && pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? { @@ -236,12 +254,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // switchSides - if (isDesktop && + if (isDefaultConn && + isDesktop && ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformMacOS && versionCmp(pi.version, '1.2.0') >= 0 && - bind.peerGetDefaultSessionsCount(id: id) == 1) { + bind.peerGetSessionsCount(id: id, isViewCamera: false) == 1) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => @@ -523,6 +542,7 @@ Future> toolbarDisplayToggle( final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; // show quality monitor final option = 'show-quality-monitor'; @@ -535,7 +555,7 @@ Future> toolbarDisplayToggle( }, child: Text(translate('Show quality monitor')))); // mute - if (perms['audio'] != false) { + if (isDefaultConn && perms['audio'] != false) { final option = 'disable-audio'; final value = bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); @@ -556,7 +576,8 @@ Future> toolbarDisplayToggle( final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && bind.mainHasFileClipboard() && pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); - if (ffiModel.keyboard && + if (isDefaultConn && + ffiModel.keyboard && perms['file'] != false && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { final enabled = !ffiModel.viewOnly; @@ -574,7 +595,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Enable file copy and paste')))); } // disable clipboard - if (ffiModel.keyboard && perms['clipboard'] != false) { + if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) { final enabled = !ffiModel.viewOnly; final option = 'disable-clipboard'; var value = @@ -591,7 +612,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Disable clipboard')))); } // lock after session end - if (ffiModel.keyboard && !ffiModel.isPeerAndroid) { + if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) { final enabled = !ffiModel.viewOnly; final option = 'lock-after-session-end'; final value = @@ -656,12 +677,12 @@ Future> toolbarDisplayToggle( child: Text(translate('True color (4:4:4)')))); } - if (isMobile) { + if (isDefaultConn && isMobile) { v.addAll(toolbarKeyboardToggles(ffi)); } // view mode (mobile only, desktop is in keyboard menu) - if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) { + if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) { v.add(TToggleMenu( value: ffiModel.viewOnly, onChanged: (value) async { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 9b8e45aa4a..d5b133bf19 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -27,6 +27,7 @@ const String kPlatformAdditionsAmyuniVirtualDisplays = const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl"; +const String kPlatformAdditionsSupportViewCamera = "support_view_camera"; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; @@ -44,6 +45,7 @@ const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopViewCamera = "view camera"; const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; @@ -58,6 +60,7 @@ const String kWindowConnect = "connect"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; +const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; @@ -97,6 +100,7 @@ const String kOptionEnableKeyboard = "enable-keyboard"; const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; +const String kOptionEnableCamera = "enable-camera"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index dd9e840411..8efc355f31 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -17,7 +17,6 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../models/platform_model.dart'; -import '../widgets/button.dart'; class OnlineStatusWidget extends StatefulWidget { const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) @@ -203,6 +202,8 @@ class _ConnectionPageState extends State final FocusNode _idFocusNode = FocusNode(); final TextEditingController _idEditingController = TextEditingController(); + String selectedConnectionType = 'Connect'; + bool isWindowMinimized = false; final AllPeersLoader _allPeersLoader = AllPeersLoader(); @@ -321,9 +322,10 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. - void onConnect({bool isFileTransfer = false}) { + void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) { var id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); + connect(context, id, + isFileTransfer: isFileTransfer, isViewCamera: isViewCamera); } /// UI for the remote ID TextField. @@ -501,21 +503,64 @@ class _ConnectionPageState extends State ), Padding( padding: const EdgeInsets.only(top: 13.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Button( - isOutline: true, - onTap: () => onConnect(isFileTransfer: true), - text: "Transfer file", + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SizedBox( + height: 28.0, + child: ElevatedButton( + onPressed: () { + onConnect(); + }, + child: Text(translate("Connect")), ), - const SizedBox( - width: 17, + ), + const SizedBox(width: 3), + Container( + height: 28.0, + width: 28.0, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), ), - Button(onTap: onConnect, text: "Connect"), - ], - ), - ) + child: Center( + child: MenuAnchor( + builder: (context, controller, builder) { + return IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + visualDensity: VisualDensity.compact, + icon: controller.isOpen + ? const Icon(Icons.keyboard_arrow_up) + : const Icon(Icons.keyboard_arrow_down), + onPressed: () { + setState(() { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }); + }, + ); + }, + menuChildren: [ + MenuItemButton( + onPressed: () { + onConnect(isFileTransfer: true); + }, + child: Text(translate('Transfer file')), + ), + MenuItemButton( + onPressed: () { + onConnect(isViewCamera: true); + }, + child: Text(translate('View camera')), + ), + ], + ), + ), + ), + ]), + ), ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 94e3257586..7f30a5a63d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -775,6 +775,7 @@ class _DesktopHomePageState extends State await connectMainDesktop( call.arguments['id'], isFileTransfer: call.arguments['isFileTransfer'], + isViewCamera: call.arguments['isViewCamera'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], password: call.arguments['password'], @@ -789,9 +790,15 @@ class _DesktopHomePageState extends State } catch (e) { debugPrint("Failed to parse window id '${call.arguments}': $e"); } - if (windowId != null) { + WindowType? windowType; + try { + windowType = WindowType.values.byName(args[3]); + } catch (e) { + debugPrint("Failed to parse window type '${call.arguments}': $e"); + } + if (windowId != null && windowType != null) { await rustDeskWinManager.moveTabToNewWindow( - windowId, args[1], args[2]); + windowId, args[1], args[2], windowType); } } else if (call.method == kWindowEventOpenMonitorSession) { final args = jsonDecode(call.arguments); @@ -799,9 +806,10 @@ class _DesktopHomePageState extends State final peerId = args['peer_id'] as String; final display = args['display'] as int; final displayCount = args['display_count'] as int; + final windowType = args['window_type'] as int; final screenRect = parseParamScreenRect(args); await rustDeskWinManager.openMonitorSession( - windowId, peerId, display, displayCount, screenRect); + windowId, peerId, display, displayCount, screenRect, windowType); } else if (call.method == kWindowEventRemoteWindowCoords) { final windowId = int.tryParse(call.arguments); if (windowId != null) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index f89381a3ff..ad9a1296ca 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -960,6 +960,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio, enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( context, 'Enable TCP tunneling', kOptionEnableTunnel, enabled: enabled, fakeValue: fakeValue), diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 025e530c2c..644f6c3367 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -269,8 +269,10 @@ class _ConnectionTabPageState extends State { style: style, ), proc: () async { - await DesktopMultiWindow.invokeMethod(kMainWindowId, - kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId'); + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,RemoteDesktop'); cancelFunc(); }, padding: padding, @@ -417,8 +419,8 @@ class _ConnectionTabPageState extends State { await WindowController.fromWindowId(windowId()).setFullscreen(false); stateGlobal.setFullscreen(false, procWnd: false); } - await setNewConnectWindowFrame( - windowId(), id!, prePeerCount, display, screenRect); + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.RemoteDesktop, display, screenRect); Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { await windowOnTop(windowId()); }); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 9a93cb34f1..ec081d5742 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -353,7 +353,9 @@ Widget buildConnectionCard(Client client) { key: ValueKey(client.id), children: [ _CmHeader(client: client), - client.type_() != ClientType.remote || client.disconnected + client.type_() == ClientType.file || + client.type_() == ClientType.portForward || + client.disconnected ? Offstage() : _PrivilegeBoard(client: client), Expanded( @@ -526,7 +528,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: !client.authorized || (client.type_() != ClientType.remote && - client.type_() != ClientType.file), + client.type_() != ClientType.file && + client.type_() != ClientType.camera), child: IconButton( onPressed: () => checkClickTime(client.id, () { if (client.type_() == ClientType.file) { @@ -627,96 +630,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { padding: EdgeInsets.symmetric(horizontal: spacing), mainAxisSpacing: spacing, crossAxisSpacing: spacing, - children: [ - buildPermissionIcon( - client.keyboard, - Icons.keyboard, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "keyboard", enabled: enabled); - setState(() { - client.keyboard = enabled; - }); - }, - translate('Enable keyboard/mouse'), - ), - buildPermissionIcon( - client.clipboard, - Icons.assignment_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "clipboard", enabled: enabled); - setState(() { - client.clipboard = enabled; - }); - }, - translate('Enable clipboard'), - ), - buildPermissionIcon( - client.audio, - Icons.volume_up_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "audio", enabled: enabled); - setState(() { - client.audio = enabled; - }); - }, - translate('Enable audio'), - ), - buildPermissionIcon( - client.file, - Icons.upload_file_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "file", enabled: enabled); - setState(() { - client.file = enabled; - }); - }, - translate('Enable file copy and paste'), - ), - buildPermissionIcon( - client.restart, - Icons.restart_alt_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "restart", enabled: enabled); - setState(() { - client.restart = enabled; - }); - }, - translate('Enable remote restart'), - ), - buildPermissionIcon( - client.recording, - Icons.videocam_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "recording", enabled: enabled); - setState(() { - client.recording = enabled; - }); - }, - translate('Enable recording session'), - ), - // only windows support block input - if (isWindows) - buildPermissionIcon( - client.blockInput, - Icons.block, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, - name: "block_input", - enabled: enabled); - setState(() { - client.blockInput = enabled; - }); - }, - translate('Enable blocking user input'), - ) - ], + children: client.type_() == ClientType.camera + ? [ + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + ] + : [ + buildPermissionIcon( + client.keyboard, + Icons.keyboard, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "keyboard", + enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, + translate('Enable keyboard/mouse'), + ), + buildPermissionIcon( + client.clipboard, + Icons.assignment_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "clipboard", + enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, + translate('Enable clipboard'), + ), + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.file, + Icons.upload_file_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "file", + enabled: enabled); + setState(() { + client.file = enabled; + }); + }, + translate('Enable file copy and paste'), + ), + buildPermissionIcon( + client.restart, + Icons.restart_alt_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "restart", + enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, + translate('Enable remote restart'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + // only windows support block input + if (isWindows) + buildPermissionIcon( + client.blockInput, + Icons.block, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "block_input", + enabled: enabled); + setState(() { + client.blockInput = enabled; + }); + }, + translate('Enable blocking user input'), + ) + ], ), ), ], diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart new file mode 100644 index 0000000000..b06dc86c56 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -0,0 +1,730 @@ +import 'dart:async'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/remote_input.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter_hbb/models/state_model.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/toolbar.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; +import '../widgets/tabbar_widget.dart'; + +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +// Used to skip session close if "move to new window" is clicked. +final Map closeSessionOnDispose = {}; + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage({ + Key? key, + required this.id, + required this.toolbarState, + this.sessionId, + this.tabWindowId, + this.password, + this.display, + this.displays, + this.tabController, + this.connToken, + this.forceRelay, + this.isSharedPassword, + }) : super(key: key) { + initSharedStates(id); + } + + final String id; + final SessionID? sessionId; + final int? tabWindowId; + final int? display; + final List? displays; + final String? password; + final ToolbarState toolbarState; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + final DesktopTabController? tabController; + + FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi; + + @override + State createState() { + final state = _ViewCameraPageState(id); + _lastState.value = state; + return state; + } +} + +class _ViewCameraPageState extends State + with AutomaticKeepAliveClientMixin, MultiWindowListener { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + + var _blockableOverlayState = BlockableOverlayState(); + + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` + // to identify the toolbar instance and its callback function. + int? _instanceIdOnEnterOrLeaveImage4Toolbar; + Function(bool)? _onEnterOrLeaveImage4Toolbar; + + late FFI _ffi; + + SessionID get sessionId => _ffi.sessionId; + + _ViewCameraPageState(String id) { + _initStates(id); + } + + void _initStates(String id) {} + + @override + void initState() { + super.initState(); + _ffi = FFI(widget.sessionId); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + }); + _ffi.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, + connToken: widget.connToken, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isLinux) { + WakelockPlus.enable(); + } + + _ffi.ffiModel.updateEventListener(sessionId, widget.id); + if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); + _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } + + _blockableOverlayState.applyFfi(_ffi); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + stateGlobal.isFocused.value = false; + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (isWindows) { + _isWindowBlur = false; + } + stateGlobal.isFocused.value = true; + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (isWindows) { + _isWindowBlur = false; + } + if (!isLinux) { + WakelockPlus.enable(); + } + } + + // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. + @override + void onWindowMaximize() { + super.onWindowMaximize(); + if (!isLinux) { + WakelockPlus.enable(); + } + } + + @override + void onWindowMinimize() { + super.onWindowMinimize(); + if (!isLinux) { + WakelockPlus.disable(); + } + } + + @override + void onWindowEnterFullScreen() { + super.onWindowEnterFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(true); + } + } + + @override + void onWindowLeaveFullScreen() { + super.onWindowLeaveFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(false); + } + } + + @override + Future dispose() async { + final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; + + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); + _ffi.textureModel.onViewCameraPageDispose(closeSession); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } + DesktopMultiWindow.removeListener(this); + _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.imageModel.disposeImage(); + _ffi.cursorModel.disposeImages(); + _rawKeyFocusNode.dispose(); + await _ffi.close(closeSession: closeSession); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + if (closeSession) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } + if (!isLinux) { + await WakelockPlus.disable(); + } + await Get.delete(tag: widget.id); + removeSharedStates(widget.id); + } + + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (id, func) { + _instanceIdOnEnterOrLeaveImage4Toolbar = id; + _onEnterOrLeaveImage4Toolbar = func; + }, + onEnterOrLeaveImageCleaner: (id) { + // If _instanceIdOnEnterOrLeaveImage4Toolbar != id + // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar. + if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) { + _instanceIdOnEnterOrLeaveImage4Toolbar = null; + _onEnterOrLeaveImage4Toolbar = null; + } + }, + setRemoteState: setState, + ); + + bodyWidget() { + return Stack( + children: [ + Container( + color: kColorCanvas, + child: getBodyForDesktop(context), + ), + Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : () { + if (!_ffi.ffiModel.isPeerAndroid) { + return Offstage(); + } else { + return Obx(() => Offstage( + offstage: _ffi.dialogManager + .mobileActionsOverlayVisible.isFalse, + child: Overlay(initialEntries: [ + makeMobileActionsOverlayEntry( + () => _ffi.dialogManager + .setMobileActionsOverlayVisible(false), + ffi: _ffi, + ) + ]), + )); + } + }(), + // Use Overlay to enable rebuild every time on menu button click. + _ffi.ffiModel.pi.isSet.isTrue + ? Overlay( + initialEntries: [OverlayEntry(builder: remoteToolbar)]) + : remoteToolbar(context), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + ), + ], + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // If the privacy mode(disable physical displays) is switched, + // we should not dismiss the dialog immediately. + if (DateTime.now().difference(togglePrivacyModeTime) > + const Duration(milliseconds: 3000)) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + } + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, _ffi.dialogManager); + return false; + }, + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(true); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + _ffi.inputModel.enterOrLeave(true); + } + } + + void leaveView(PointerExitEvent evt) { + if (_ffi.ffiModel.keyboard) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(false); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + _ffi.inputModel.enterOrLeave(false); + } + } + + Widget _buildRawTouchAndPointerRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawTouchGestureDetectorRegion( + child: _buildRawPointerMouseRegion(child, onEnter, onExit), + ffi: _ffi, + isCamera: true, + ); + } + + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return CameraRawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + + Widget getBodyForDesktop(BuildContext context) { + var paints = [ + MouseRegion(onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, child: LayoutBuilder(builder: (context, constraints) { + final c = Provider.of(context, listen: false); + Future.delayed(Duration.zero, () => c.updateViewStyle()); + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + widget.toolbarState.initShow(sessionId); + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: (child) => _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + })) + ]; + + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawTouchAndPointerRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatefulWidget { + final FFI ffi; + final String id; + final RxBool cursorOverImage; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.ffi, + required this.id, + required this.cursorOverImage, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + bool _lastRemoteCursorMoved = false; + + String get id => widget.id; + RxBool get cursorOverImage => widget.cursorOverImage; + Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; + + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); + return NotificationListener( + onNotification: (notification) { + c.updateScrollPercent(); + return false; + }, + child: Container( + child: _buildCrossScrollbarFromLayout( + context, + _buildListener(paintWidget), + c.size, + paintSize, + c.scrollHorizontal, + c.scrollVertical, + )), + ); + } else { + if (c.size.width > 0 && c.size.height > 0) { + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + isLinux ? c.x.toInt().toDouble() : c.x, + isLinux ? c.y.toInt().toDouble() : c.y, + ), + c.size, + isViewOriginal()) + : _buildScrollAutoNonTextureRender(m, c, s); + return Container(child: _buildListener(paintWidget)); + } else { + return Container(); + } + } + } + + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAutoNonTextureRender( + ImageModel m, CanvasModel c, double s) { + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget.ffi.textureModel + .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); + if (true) { + // both "textureId.value != -1" and "true" seems ok + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * s, + height: displays[i].height * s, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, + Widget child, + Size layoutSize, + Size size, + ScrollController horizontal, + ScrollController vertical, + ) { + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, + ); + } + if (layoutSize.height < size.height) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ); + } + + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart new file mode 100644 index 0000000000..4510949fa2 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -0,0 +1,499 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../models/platform_model.dart'; + +class _MenuTheme { + static const Color blueColor = MyTheme.button; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + +class ViewCameraTabPage extends StatefulWidget { + final Map params; + + const ViewCameraTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ViewCameraTabPageState(params); +} + +class _ViewCameraTabPageState extends State { + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera)); + final contentKey = UniqueKey(); + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; + + String? peerId; + bool _isScreenRectSet = false; + int? _display; + + var connectionMap = RxList.empty(growable: true); + + _ViewCameraTabPageState(Map params) { + RemoteCountState.init(); + peerId = params['id']; + final sessionId = params['session_id']; + final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); + if (peerId != null) { + ConnectionTypeState.init(peerId!); + tabController.onSelected = (id) { + final viewCameraPage = tabController.widget(id); + if (viewCameraPage is ViewCameraPage) { + final ffi = viewCameraPage.ffi; + bind.setCurSessionId(sessionId: ffi.sessionId); + } + WindowController.fromWindowId(params['windowId']) + .setTitle(getWindowNameWithId(id)); + UnreadChatCountState.find(id).value = 0; + }; + tabController.add(TabInfo( + key: peerId!, + label: peerId!, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(peerId), + page: ViewCameraPage( + key: ValueKey(peerId), + id: peerId!, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: params['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: params['connToken'], + forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], + ), + )); + _update_remote_count(); + } + tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); + } + + @override + void initState() { + super.initState(); + + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.ViewCamera, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + selectedBorderColor: MyTheme.accent, + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn; + if (secure && direct) { + msgConn = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + msgConn = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + msgConn = translate("Direct and unencrypted connection"); + } else { + msgConn = translate("Relayed and unencrypted connection"); + } + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue && + e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_toolbar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final sessionId = ffi.sessionId; + final toolbarState = viewCameraPage.toolbarState; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + style: style, + )), + proc: () { + toolbarState.switchShow(sessionId); + cancelFunc(); + }, + padding: padding, + ), + ]); + + if (tabController.state.value.tabs.length > 1) { + final splitAction = MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Move tab to new window'), + style: style, + ), + proc: () async { + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,ViewCamera'); + cancelFunc(); + }, + padding: padding, + ); + menu.insert(1, splitAction); + } + + menu.addAll([ + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () { + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ) + ]); + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + // Keep calling until the window status is hidden. + // + // Workaround for Windows: + // If you click other buttons and close in msgbox within a very short period of time, the close may fail. + // `await WindowController.fromWindowId(windowId()).close();`. + Future loopCloseWindow() async { + int c = 0; + final windowController = WindowController.fromWindowId(windowId()); + while (c < 20 && + tabController.state.value.tabs.isEmpty && + (!await windowController.isHidden())) { + await windowController.close(); + await Future.delayed(Duration(milliseconds: 100)); + c++; + } + } + + loopCloseWindow(); + } + ConnectionTypeState.delete(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + debugPrint( + "[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewViewCamera) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + final prePeerCount = tabController.length; + Future.delayed(Duration.zero, () async { + if (stateGlobal.fullscreen.isTrue) { + await WindowController.fromWindowId(windowId()).setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + } + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.ViewCamera, display, screenRect); + Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { + await windowOnTop(windowId()); + }); + }); + ConnectionTypeState.init(id); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: ViewCameraPage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: args['connToken'], + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = + tabController.jumpToByKeyAndDisplay(id, display, isCamera: true); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as ViewCameraPage; + returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final viewCameraPage = + tabController.state.value.selectedTabInfo.page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } else if (call.method == kWindowEventSetFullscreen) { + stateGlobal.setFullscreen(call.arguments == 'true'); + } + _update_remote_count(); + return returnValue; + } +} diff --git a/flutter/lib/desktop/screen/desktop_view_camera_screen.dart b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart new file mode 100644 index 0000000000..a845b89d01 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopViewCameraScreen extends StatelessWidget { + final Map params; + + DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) { + bind.mainInitInputSource(); + stateGlobal.getInputSource(force: true); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: isLinux ? Colors.transparent : null, + body: ViewCameraTabPage( + params: params, + ), + )); + } +} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index d826ea8c6b..31b8b2263e 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -478,7 +478,10 @@ class _RemoteToolbarState extends State { state: widget.state, setFullscreen: _setFullscreen, )); - toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + // Do not show keyboard for camera connection type. + if (widget.ffi.connType == ConnType.defaultConn) { + toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + } toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); @@ -1043,23 +1046,26 @@ class _DisplayMenuState extends State<_DisplayMenu> { scrollStyle(), imageQuality(), codec(), - _ResolutionsMenu( - id: widget.id, - ffi: widget.ffi, - screenAdjustor: _screenAdjustor, - ), - if (showVirtualDisplayMenu(ffi)) + if (ffi.connType == ConnType.defaultConn) + _ResolutionsMenu( + id: widget.id, + ffi: widget.ffi, + screenAdjustor: _screenAdjustor, + ), + if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn) _SubmenuButton( ffi: widget.ffi, menuChildren: getVirtualDisplayMenuChildren(ffi, id, null), child: Text(translate("Virtual display")), ), - cursorToggles(), + if (ffi.connType == ConnType.defaultConn) cursorToggles(), Divider(), toggles(), ]; // privacy mode - if (ffiModel.keyboard && pi.features.privacyMode) { + if (ffi.connType == ConnType.defaultConn && + ffiModel.keyboard && + pi.features.privacyMode) { final privacyModeState = PrivacyModeState.find(id); final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); @@ -1085,7 +1091,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { ]); } } - menuChildren.add(widget.pluginItem); + if (ffi.connType == ConnType.defaultConn) { + menuChildren.add(widget.pluginItem); + } return menuChildren; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 96ada22c90..c8640e05fb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -51,6 +52,7 @@ enum DesktopTabType { cm, remoteScreen, fileTransfer, + viewCamera, portForward, install, } @@ -179,11 +181,13 @@ class DesktopTabController { jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), callOnSelected: callOnSelected); - bool jumpToByKeyAndDisplay(String key, int display) { + bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) { for (int i = 0; i < state.value.tabs.length; i++) { final tab = state.value.tabs[i]; if (tab.key == key) { - final ffi = (tab.page as RemotePage).ffi; + final ffi = isCamera + ? (tab.page as ViewCameraPage).ffi + : (tab.page as RemotePage).ffi; if (ffi.ffiModel.pi.currentDisplay == display) { return jumpTo(i, callOnSelected: true); } @@ -725,6 +729,7 @@ class WindowActionPanelState extends State { return widget.tabController.state.value.tabs.length > 1 && (widget.tabController.tabType == DesktopTabType.remoteScreen || widget.tabController.tabType == DesktopTabType.fileTransfer || + widget.tabController.tabType == DesktopTabType.viewCamera || widget.tabController.tabType == DesktopTabType.portForward || widget.tabController.tabType == DesktopTabType.cm); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b5a0af7114..f04e142d94 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; @@ -76,6 +77,13 @@ Future main(List args) async { kAppTypeDesktopFileTransfer, ); break; + case WindowType.ViewCamera: + desktopType = DesktopType.viewCamera; + runMultiWindow( + argument, + kAppTypeDesktopViewCamera, + ); + break; case WindowType.PortForward: desktopType = DesktopType.portForward; runMultiWindow( @@ -192,6 +200,12 @@ void runMultiWindow( params: argument, ); break; + case kAppTypeDesktopViewCamera: + draggablePositions.load(); + widget = DesktopViewCameraScreen( + params: argument, + ); + break; case kAppTypeDesktopPortForward: widget = DesktopPortForwardScreen( params: argument, @@ -227,6 +241,19 @@ void runMultiWindow( await restoreWindowPosition(WindowType.FileTransfer, windowId: kWindowId!); break; + case kAppTypeDesktopViewCamera: + // If screen rect is set, the window will be moved to the target screen and then set fullscreen. + if (argument['screen_rect'] == null) { + // display can be used to control the offset of the window. + await restoreWindowPosition( + WindowType.ViewCamera, + windowId: kWindowId!, + peerId: argument['id'] as String?, + // FIXME: fix display index. + display: argument['display'] as int?, + ); + } + break; case kAppTypeDesktopPortForward: await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index efccc5de65..82d7058ab5 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -204,6 +204,7 @@ class WebHomePage extends StatelessWidget { return; } bool isFileTransfer = false; + bool isViewCamera = false; String? id; String? password; for (int i = 0; i < args.length; i++) { @@ -219,6 +220,11 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--view-camera': + isViewCamera = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -228,7 +234,7 @@ class WebHomePage extends StatelessWidget { } } if (id != null) { - connect(context, id, isFileTransfer: isFileTransfer, password: password); + connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password); } } } diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart new file mode 100644 index 0000000000..afd24dc7e0 --- /dev/null +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -0,0 +1,721 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/toolbar.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import '../../common.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/remote_input.dart'; +import '../../models/input_model.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../utils/image.dart'; + +final initText = '1' * 1024; + +// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard. +// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard. +// https://github.com/flutter/flutter/issues/159384 +// https://github.com/flutter/flutter/issues/159383 +void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { + if (isAndroid) { + if (isKeyboardVisible != true) { + // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`. + gFFI.invokeMethod("enable_soft_keyboard", false); + } + } +} + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage( + {Key? key, required this.id, this.password, this.isSharedPassword}) + : super(key: key); + + final String id; + final String? password; + final bool? isSharedPassword; + + @override + State createState() => _ViewCameraPageState(id); +} + +class _ViewCameraPageState extends State + with WidgetsBindingObserver { + Timer? _timer; + bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; + Orientation? _currentOrientation; + double _viewInsetsBottom = 0; + + Timer? _timerDidChangeMetrics; + + final _blockableOverlayState = BlockableOverlayState(); + + final keyboardVisibilityController = KeyboardVisibilityController(); + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _physicalFocusNode = FocusNode(); + var _showEdit = false; // use soft keyboard + + InputModel get inputModel => gFFI.inputModel; + SessionID get sessionId => gFFI.sessionId; + + final TextEditingController _textController = + TextEditingController(text: initText); + + _ViewCameraPageState(String id) { + initSharedStates(id); + gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; + gFFI.dialogManager.loadMobileActionsOverlayVisible(); + } + + @override + void initState() { + super.initState(); + gFFI.ffiModel.updateEventListener(sessionId, widget.id); + gFFI.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + gFFI.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isWeb) { + WakelockPlus.enable(); + } + _physicalFocusNode.requestFocus(); + gFFI.inputModel.listenToMouse(true); + gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); + gFFI.chatModel + .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); + _blockableOverlayState.applyFfi(gFFI); + gFFI.imageModel.addCallbackOnFirstImage((String peerId) { + gFFI.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId)); + if (gFFI.recordingModel.start) { + showToast(translate('Automatically record outgoing sessions')); + } + _disableAndroidSoftKeyboard( + isKeyboardVisible: keyboardVisibilityController.isVisible); + }); + WidgetsBinding.instance.addObserver(this); + } + + @override + Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + gFFI.dialogManager.hideMobileActionsOverlay(store: false); + gFFI.inputModel.listenToMouse(false); + gFFI.imageModel.disposeImage(); + gFFI.cursorModel.disposeImages(); + await gFFI.invokeMethod("enable_soft_keyboard", true); + _mobileFocusNode.dispose(); + _physicalFocusNode.dispose(); + await gFFI.close(); + _timer?.cancel(); + _timerDidChangeMetrics?.cancel(); + gFFI.dialogManager.dismissAll(); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!isWeb) { + await WakelockPlus.disable(); + } + removeSharedStates(widget.id); + // `on_voice_call_closed` should be called when the connection is ended. + // The inner logic of `on_voice_call_closed` will check if the voice call is active. + // Only one client is considered here for now. + gFFI.chatModel.onVoiceCallClosed("End connetion"); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) {} + + @override + void didChangeMetrics() { + // If the soft keyboard is visible and the canvas has been changed(panned or scaled) + // Don't try reset the view style and focus the cursor. + if (gFFI.cursorModel.lastKeyboardIsVisible && + gFFI.canvasModel.isMobileCanvasChanged) { + return; + } + + final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom; + _timerDidChangeMetrics?.cancel(); + _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { + // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. + if (newBottom != _viewInsetsBottom) { + gFFI.canvasModel.mobileFocusCanvasCursor(); + _viewInsetsBottom = newBottom; + } + }); + } + + // to-do: It should be better to use transparent color instead of the bgColor. + // But for now, the transparent color will cause the canvas to be white. + // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay. + // But I don't know why and how to fix it. + Widget emptyOverlay(Color bgColor) => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: bgColor, + ), + ); + + Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty + ? getBottomAppBar() + : Offstage()); + + @override + Widget build(BuildContext context) { + final keyboardIsVisible = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; + + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, gFFI.dialogManager); + return false; + }, + child: Scaffold( + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: keyboardIsVisible + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !keyboardIsVisible, + child: Icon( + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, + color: Colors.white, + ), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (keyboardIsVisible) { + _showEdit = false; + gFFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: Obx(() => Stack( + alignment: Alignment.bottomCenter, + children: [ + gFFI.ffiModel.pi.isSet.isTrue && + gFFI.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay(MyTheme.canvasColor) + : () { + gFFI.ffiModel.tryShowAndroidActionsOverlay(); + return Offstage(); + }(), + _bottomWidget(), + gFFI.ffiModel.pi.isSet.isFalse + ? emptyOverlay(MyTheme.canvasColor) + : Offstage(), + ], + )), + body: Obx( + () => getRawPointerAndKeyBody(Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: kColorCanvas, + child: SafeArea( + child: OrientationBuilder(builder: (ctx, orientation) { + if (_currentOrientation != orientation) { + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); + _currentOrientation = orientation; + gFFI.canvasModel.updateViewStyle(); + }); + } + return Container( + color: MyTheme.canvasColor, + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + isCamera: true, + ), + ); + }), + ), + ); + }) + ], + )), + )), + ); + } + + Widget getRawPointerAndKeyBody(Widget child) { + return CameraRawPointerMouseRegion( + inputModel: inputModel, + // Disable RawKeyFocusScope before the connecting is established. + // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog. + child: gFFI.ffiModel.pi.isSet.isTrue + ? RawKeyFocusScope( + focusNode: _physicalFocusNode, + inputModel: inputModel, + child: child) + : child, + ); + } + + Widget getBottomAppBar() { + return BottomAppBar( + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(sessionId, gFFI.dialogManager); + }, + ), + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + setState(() => _showEdit = false); + showOptions(context, widget.id, gFFI.dialogManager); + }, + ) + ] + + (isWeb + ? [] + : [ + futureBuilder( + future: gFFI.invokeMethod( + "get_value", "KEY_IS_SUPPORT_VOICE_CALL"), + hasData: (isSupportVoiceCall) => IconButton( + color: Colors.white, + icon: isAndroid && isSupportVoiceCall + ? SvgPicture.asset('assets/chat.svg', + colorFilter: ColorFilter.mode( + Colors.white, BlendMode.srcIn)) + : Icon(Icons.message), + onPressed: () => + isAndroid && isSupportVoiceCall + ? showChatOptions(widget.id) + : onPressedTextChat(widget.id), + )) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + setState(() => _showEdit = false); + showActions(widget.id); + }, + ), + ]), + Obx(() => IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: gFFI.ffiModel.waitForFirstImage.isTrue + ? null + : () { + setState(() => _showBar = !_showBar); + }, + )), + ], + ), + ); + } + + Widget getBodyForMobile() { + return Container( + color: MyTheme.canvasColor, + child: Stack(children: () { + final paints = [ + ImagePaint(), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), + SizedBox( + width: 0, + height: 0, + child: !_showEdit + ? Container() + : TextFormField( + textInputAction: TextInputAction.newline, + autocorrect: false, + // Flutter 3.16.9 Android. + // `enableSuggestions` causes secure keyboard to be shown. + // https://github.com/flutter/flutter/issues/139143 + // https://github.com/flutter/flutter/issues/146540 + // enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + controller: _textController, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + // `onChanged` may be called depending on the input method if this widget is wrapped in + // `Focus(onKeyEvent: ..., child: ...)` + // For `Backspace` button in the soft keyboard: + // en/fr input method: + // 1. The button will not trigger `onKeyEvent` if the text field is not empty. + // 2. The button will trigger `onKeyEvent` if the text field is empty. + // ko/zh/ja input method: the button will trigger `onKeyEvent` + // and the event will not popup if `KeyEventResult.handled` is returned. + onChanged: null, + ).workaroundFreezeLinuxMint(), + ), + ]; + return paints; + }())); + } + + Widget getBodyForDesktopWithListener() { + var paints = [ImagePaint()]; + return Container( + color: MyTheme.canvasColor, child: Stack(children: paints)); + } + + List _getMobileActionMenus() { + if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid || + !gFFI.ffiModel.keyboard) { + return []; + } + final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0; + if (!enabled) return []; + return [ + TTextMenu( + child: Text(translate('Back')), + onPressed: () => gFFI.inputModel.onMobileBack(), + ), + TTextMenu( + child: Text(translate('Home')), + onPressed: () => gFFI.inputModel.onMobileHome(), + ), + TTextMenu( + child: Text(translate('Apps')), + onPressed: () => gFFI.inputModel.onMobileApps(), + ), + TTextMenu( + child: Text(translate('Volume up')), + onPressed: () => gFFI.inputModel.onMobileVolumeUp(), + ), + TTextMenu( + child: Text(translate('Volume down')), + onPressed: () => gFFI.inputModel.onMobileVolumeDown(), + ), + TTextMenu( + child: Text(translate('Power')), + onPressed: () => gFFI.inputModel.onMobilePower(), + ), + ]; + } + + void showActions(String id) async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + final mobileActionMenus = _getMobileActionMenus(); + final menus = toolbarControls(context, id, gFFI); + + final List> more = [ + ...mobileActionMenus + .asMap() + .entries + .map((e) => + PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(), + if (mobileActionMenus.isNotEmpty) PopupMenuDivider(), + ...menus + .asMap() + .entries + .map((e) => PopupMenuItem( + child: e.value.getChild(), + value: e.key + mobileActionMenus.length)) + .toList(), + ]; + () async { + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: more, + elevation: 8, + ); + if (index != null) { + if (index < mobileActionMenus.length) { + mobileActionMenus[index].onPressed.call(); + } else if (index < mobileActionMenus.length + more.length) { + menus[index - mobileActionMenus.length].onPressed.call(); + } + } + }(); + } + + onPressedTextChat(String id) { + gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID)); + gFFI.chatModel.toggleChatOverlay(); + } + + showChatOptions(String id) async { + onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId); + onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId); + + makeTextMenu(String label, Widget icon, VoidCallback onPressed, + {TextStyle? labelStyle}) => + TTextMenu( + child: Text(translate(label), style: labelStyle), + trailingIcon: Transform.scale( + scale: (isDesktop || isWebDesktop) ? 0.8 : 1, + child: IgnorePointer( + child: IconButton( + onPressed: null, + icon: icon, + ), + ), + ), + onPressed: onPressed, + ); + + final isInVoice = [ + VoiceCallStatus.waitingForResponse, + VoiceCallStatus.connected + ].contains(gFFI.chatModel.voiceCallStatus.value); + final menus = [ + makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent), + () => onPressedTextChat(widget.id)), + isInVoice + ? makeTextMenu( + 'End voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: + ColorFilter.mode(Colors.redAccent, BlendMode.srcIn), + ), + onPressEndVoiceCall, + labelStyle: TextStyle(color: Colors.redAccent)) + : makeTextMenu( + 'Voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn), + ), + onPressVoiceCall), + ]; + + final menuItems = menus + .asMap() + .entries + .map((e) => PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(); + Future.delayed(Duration.zero, () async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: menuItems, + elevation: 8, + ); + if (index != null && index < menus.length) { + menus[index].onPressed.call(); + } + }); + } +} + +class ImagePaint extends StatelessWidget { + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + var s = c.scale; + final adjust = c.getAdjustY(); + return CustomPaint( + painter: ImagePainter( + image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s), + ); + } +} + +void showOptions( + BuildContext context, String id, OverlayDialogManager dialogManager) async { + var displays = []; + final pi = gFFI.ffiModel.pi; + final image = gFFI.ffiModel.getConnectionImage(); + if (image != null) { + displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + } + if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) { + final cur = pi.currentDisplay; + final children = []; + for (var i = 0; i < pi.displays.length; ++i) { + children.add(InkWell( + onTap: () { + if (i == cur) return; + openMonitorInTheSameTab(i, gFFI, pi); + gFFI.dialogManager.dismissAll(); + }, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).hintColor), + borderRadius: BorderRadius.circular(2), + color: i == cur + ? Theme.of(context).primaryColor.withOpacity(0.6) + : null), + child: Center( + child: Text((i + 1).toString(), + style: TextStyle( + color: i == cur ? Colors.white : Colors.black87, + fontWeight: FontWeight.bold)))))); + } + displays.add(Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: children, + ))); + } + if (displays.isNotEmpty) { + displays.add(const Divider(color: MyTheme.border)); + } + + List> viewStyleRadios = + await toolbarViewStyle(context, id, gFFI); + List> imageQualityRadios = + await toolbarImageQuality(context, id, gFFI); + List> codecRadios = await toolbarCodec(context, id, gFFI); + List displayToggles = + await toolbarDisplayToggle(context, id, gFFI); + + dialogManager.show((setState, close, context) { + var viewStyle = + (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs; + var imageQuality = + (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '') + .obs; + var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs; + final radios = [ + for (var e in viewStyleRadios) + Obx(() => getRadio( + e.child, + e.value, + viewStyle.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) viewStyle.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in imageQualityRadios) + Obx(() => getRadio( + e.child, + e.value, + imageQuality.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) imageQuality.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in codecRadios) + Obx(() => getRadio( + e.child, + e.value, + codec.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) codec.value = v; + } + : null)), + if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), + ]; + + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); + final displayTogglesList = displayToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxToggleValues[e.key].value, + onChanged: e.value.onChanged != null + ? (v) { + e.value.onChanged?.call(v); + if (v != null) rxToggleValues[e.key].value = v; + } + : null, + title: e.value.child))) + .toList(); + final toggles = [ + ...displayTogglesList, + ]; + + var popupDialogMenus = List.empty(growable: true); + if (popupDialogMenus.isNotEmpty) { + popupDialogMenus.add(const Divider(color: MyTheme.border)); + } + + return CustomAlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: displays + radios + popupDialogMenus + toggles), + ); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); +} + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index c6cf55256d..a960491346 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -235,6 +235,17 @@ class TextureModel { } } + onViewCameraPageDispose(bool closeSession) async { + final ffi = parent.target; + if (ffi == null) return; + for (final texture in _pixelbufferRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + for (final texture in _gpuRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + } + ensureControl(int display) { var ctl = _control[display]; if (ctl == null) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 2e7a36fc15..463d5a32b7 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -369,6 +369,7 @@ class InputModel { String? get peerPlatform => parent.target?.ffiModel.pi.platform; bool get isViewOnly => parent.target!.ffiModel.viewOnly; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; + bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; InputModel(this.parent) { sessionId = parent.target!.sessionId; @@ -471,6 +472,7 @@ class InputModel { KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -525,6 +527,7 @@ class InputModel { KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -724,6 +727,7 @@ class InputModel { /// [press] indicates a click event(down and up). void inputKey(String name, {bool? down, bool? press}) { if (!keyboardPerm) return; + if (isViewCamera) return; bind.sessionInputKey( sessionId: sessionId, name: name, @@ -785,6 +789,7 @@ class InputModel { /// Send scroll event with scroll distance [y]. Future scroll(int y) async { + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json @@ -808,6 +813,7 @@ class InputModel { /// Send mouse press event. Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'type': type, 'buttons': button.value}))); @@ -834,6 +840,7 @@ class InputModel { /// Send mouse movement event with distance in [x] and [y]. Future moveMouse(double x, double y) async { if (!keyboardPerm) return; + if (isViewCamera) return; var x2 = x.toInt(); var y2 = y.toInt(); await bind.sessionSendMouse( @@ -857,6 +864,7 @@ class InputModel { _lastScale = 1.0; _stopFling = true; if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanStart, e.position); } @@ -865,6 +873,7 @@ class InputModel { // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform != kPeerPlatformAndroid) { final scale = ((e.scale - _lastScale) * 1000).toInt(); _lastScale = e.scale; @@ -904,6 +913,7 @@ class InputModel { handlePointerEvent('touch', kMouseEventTypePanUpdate, Offset(x.toDouble(), y.toDouble())); } else { + if (isViewCamera) return; bind.sessionSendMouse( sessionId: sessionId, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); @@ -912,6 +922,7 @@ class InputModel { } void _scheduleFling(double x, double y, int delay) { + if (isViewCamera) return; if ((x == 0 && y == 0) || _stopFling) { _fling = false; return; @@ -963,6 +974,7 @@ class InputModel { } void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); return; @@ -994,6 +1006,7 @@ class InputModel { _remoteWindowCoords = []; _windowRect = null; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; @@ -1007,6 +1020,7 @@ class InputModel { void onPointUpImage(PointerUpEvent e) { if (isDesktop) _queryOtherWindowCoords = false; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); @@ -1015,6 +1029,7 @@ class InputModel { void onPointMoveImage(PointerMoveEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (_queryOtherWindowCoords) { Future.delayed(Duration.zero, () async { @@ -1049,6 +1064,7 @@ class InputModel { void onPointerSignalImage(PointerSignalEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx.toInt(); var dy = e.scrollDelta.dy.toInt(); @@ -1146,6 +1162,7 @@ class InputModel { } final evt = PointerEventToRust(kind, type, evtValue).toJson(); + if (isViewCamera) return; bind.sessionSendPointer( sessionId: sessionId, msg: json.encode(modify(evt))); } @@ -1177,6 +1194,7 @@ class InputModel { Offset offset, { bool onExit = false, }) { + if (isViewCamera) return; double x = offset.dx; double y = max(0.0, offset.dy); if (_checkPeerControlProtected(x, y)) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e408e0e2fc..06b5041edb 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -407,7 +407,9 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.sendEmptyDirs(evt); } } else if (name == "record_status") { - if (desktopType == DesktopType.remote || isMobile) { + if (desktopType == DesktopType.remote || + desktopType == DesktopType.viewCamera || + isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); } } else { @@ -501,7 +503,10 @@ class FfiModel with ChangeNotifier { final display = int.parse(evt['display']); if (_pi.currentDisplay != kAllDisplayValue) { - if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) { + if (bind.peerGetSessionsCount( + id: peerId, + isViewCamera: parent.target?.connType == ConnType.viewCamera) > + 1) { if (display != _pi.currentDisplay) { return; } @@ -809,7 +814,10 @@ class FfiModel with ChangeNotifier { _pi.primaryDisplay = currentDisplay; } - if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) { + if (bind.peerGetSessionsCount( + id: peerId, + isViewCamera: parent.target?.connType == ConnType.viewCamera) <= + 1) { _pi.currentDisplay = currentDisplay; } @@ -827,9 +835,11 @@ class FfiModel with ChangeNotifier { sessionId: sessionId, arg: kOptionTouchMode) != ''; } + // FIXME: handle ViewCamera ConnType independently. if (connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); - } else if (connType == ConnType.defaultConn) { + } else if (connType == ConnType.defaultConn || + connType == ConnType.viewCamera) { List newDisplays = []; List displays = json.decode(evt['displays']); for (int i = 0; i < displays.length; ++i) { @@ -859,7 +869,7 @@ class FfiModel with ChangeNotifier { bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: kOptionToggleViewOnly)); } - if (connType == ConnType.defaultConn) { + if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) { final platformAdditions = evt['platform_additions']; if (platformAdditions != null && platformAdditions != '') { try { @@ -2576,7 +2586,7 @@ class ElevationModel with ChangeNotifier { onPortableServiceRunning(bool running) => _running = running; } -enum ConnType { defaultConn, fileTransfer, portForward, rdp } +enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera } /// Flutter state manager and data communication with the Rust core. class FFI { @@ -2651,10 +2661,11 @@ class FFI { ffiModel.waitForImageTimer = null; } - /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. + /// Start with the given [id]. Only transfer file if [isFileTransfer], only view camera if [isViewCamera], only port forward if [isPortForward]. void start( String id, { bool isFileTransfer = false, + bool isViewCamera = false, bool isPortForward = false, bool isRdp = false, String? switchUuid, @@ -2669,9 +2680,15 @@ class FFI { closed = false; auditNote = ''; if (isMobile) mobileReset(); - assert(!(isFileTransfer && isPortForward), 'more than one connect type'); + assert( + (!(isPortForward && isViewCamera)) && + (!(isViewCamera && isPortForward)) && + (!(isPortForward && isFileTransfer)), + 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; + } else if (isViewCamera) { + connType = ConnType.viewCamera; } else if (isPortForward) { connType = ConnType.portForward; } else { @@ -2691,6 +2708,7 @@ class FFI { sessionId: sessionId, id: id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isPortForward: isPortForward, isRdp: isRdp, switchUuid: switchUuid ?? '', @@ -2706,7 +2724,10 @@ class FFI { return; } final addRes = bind.sessionAddExistedSync( - id: id, sessionId: sessionId, displays: Int32List.fromList(displays)); + id: id, + sessionId: sessionId, + displays: Int32List.fromList(displays), + isViewCamera: isViewCamera); if (addRes != '') { debugPrint( 'Unreachable, failed to add existed session to $id, $addRes'); @@ -2717,6 +2738,11 @@ class FFI { if (isDesktop && connType == ConnType.defaultConn) { textureModel.updateCurrentDisplay(display ?? 0); } + // FIXME: separate cameras displays or shift all indices. + if (isDesktop && connType == ConnType.viewCamera) { + // FIXME: currently the default 0 is not used. + textureModel.updateCurrentDisplay(display ?? 0); + } // CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions. // Though the stream is returned immediately, the stream may not be ready. @@ -2993,6 +3019,9 @@ class PeerInfo with ChangeNotifier { bool get isAmyuniIdd => platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd'; + bool get isSupportViewCamera => + platformAdditions[kPlatformAdditionsSupportViewCamera] == true; + Display? tryGetDisplay({int? display}) { if (displays.isEmpty) { return null; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8775764619..2ac5bfe16f 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -791,6 +791,7 @@ class ServerModel with ChangeNotifier { enum ClientType { remote, file, + camera, portForward, } @@ -798,6 +799,7 @@ class Client { int id = 0; // client connections inner count id bool authorized = false; bool isFileTransfer = false; + bool isViewCamera = false; String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app @@ -815,13 +817,15 @@ class Client { RxInt unreadChatMessageCount = 0.obs; - Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, + Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) { id = json['id']; authorized = json['authorized']; isFileTransfer = json['is_file_transfer']; + // TODO: no entry then default. + isViewCamera = json['is_view_camera']; portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; @@ -843,6 +847,7 @@ class Client { data['id'] = id; data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; + data['is_view_camera'] = isViewCamera; data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; @@ -863,6 +868,8 @@ class Client { ClientType type_() { if (isFileTransfer) { return ClientType.file; + } else if (isViewCamera) { + return ClientType.camera; } else if (portForward.isNotEmpty) { return ClientType.portForward; } else { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 70001ffdff..4e848ea7c9 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -11,7 +11,14 @@ import 'package:flutter_hbb/models/input_model.dart'; /// must keep the order // ignore: constant_identifier_names -enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } +enum WindowType { + Main, + RemoteDesktop, + FileTransfer, + ViewCamera, + PortForward, + Unknown +} extension Index on int { WindowType get windowType { @@ -23,6 +30,8 @@ extension Index on int { case 2: return WindowType.FileTransfer; case 3: + return WindowType.ViewCamera; + case 4: return WindowType.PortForward; default: return WindowType.Unknown; @@ -50,31 +59,46 @@ class RustDeskMultiWindowManager { final List _windowActiveCallbacks = List.empty(growable: true); final List _remoteDesktopWindows = List.empty(growable: true); final List _fileTransferWindows = List.empty(growable: true); + final List _viewCameraWindows = List.empty(growable: true); final List _portForwardWindows = List.empty(growable: true); - moveTabToNewWindow(int windowId, String peerId, String sessionId) async { + moveTabToNewWindow(int windowId, String peerId, String sessionId, + WindowType windowType) async { var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType.index, 'id': peerId, 'tab_window_id': windowId, 'session_id': sessionId, }; - await _newSession( - false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, - peerId, - _remoteDesktopWindows, - jsonEncode(params), - ); + if (windowType == WindowType.RemoteDesktop) { + await _newSession( + false, + WindowType.RemoteDesktop, + kWindowEventNewRemoteDesktop, + peerId, + _remoteDesktopWindows, + jsonEncode(params), + ); + } else if (windowType == WindowType.ViewCamera) { + await _newSession( + false, + WindowType.ViewCamera, + kWindowEventNewViewCamera, + peerId, + _viewCameraWindows, + jsonEncode(params), + ); + } } // This function must be called in the main window thread. // Because the _remoteDesktopWindows is managed in that thread. openMonitorSession(int windowId, String peerId, int display, int displayCount, - Rect? screenRect) async { - if (_remoteDesktopWindows.length > 1) { - for (final windowId in _remoteDesktopWindows) { + Rect? screenRect, int windowType) async { + final isCamera = windowType == WindowType.ViewCamera.index; + final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows; + if (windowIDs.length > 1) { + for (final windowId in windowIDs) { if (await DesktopMultiWindow.invokeMethod( windowId, kWindowEventActiveDisplaySession, @@ -91,7 +115,7 @@ class RustDeskMultiWindowManager { ? List.generate(displayCount, (index) => index) : [display]; var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType, 'id': peerId, 'tab_window_id': windowId, 'display': display, @@ -107,10 +131,10 @@ class RustDeskMultiWindowManager { } await _newSession( false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, + windowType.windowType, + isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop, peerId, - _remoteDesktopWindows, + windowIDs, jsonEncode(params), screenRect: screenRect, ); @@ -277,6 +301,27 @@ class RustDeskMultiWindowManager { ); } + Future newViewCamera( + String remoteId, { + String? password, + bool? isSharedPassword, + String? switchUuid, + bool? forceRelay, + String? connToken, + }) async { + return await newSession( + WindowType.ViewCamera, + kWindowEventNewViewCamera, + remoteId, + _viewCameraWindows, + password: password, + forceRelay: forceRelay, + switchUuid: switchUuid, + isSharedPassword: isSharedPassword, + connToken: connToken, + ); + } + Future newPortForward( String remoteId, bool isRDP, { @@ -324,6 +369,8 @@ class RustDeskMultiWindowManager { return _remoteDesktopWindows; case WindowType.FileTransfer: return _fileTransferWindows; + case WindowType.ViewCamera: + return _viewCameraWindows; case WindowType.PortForward: return _portForwardWindows; case WindowType.Unknown: @@ -342,6 +389,9 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: _fileTransferWindows.clear(); break; + case WindowType.ViewCamera: + _viewCameraWindows.clear(); + break; case WindowType.PortForward: _portForwardWindows.clear(); break; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index dba7fc0941..d869cb3100 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -60,7 +60,8 @@ class RustdeskImpl { throw UnimplementedError("hostStopSystemKeyPropagate"); } - int peerGetDefaultSessionsCount({required String id, dynamic hint}) { + int peerGetSessionsCount( + {required String id, required bool isViewCamera, dynamic hint}) { return 0; } @@ -68,6 +69,7 @@ class RustdeskImpl { {required String id, required UuidValue sessionId, required Int32List displays, + required bool isViewCamera, dynamic hint}) { return ''; } @@ -76,6 +78,7 @@ class RustdeskImpl { {required UuidValue sessionId, required String id, required bool isFileTransfer, + required bool isViewCamera, required bool isPortForward, required bool isRdp, required String switchUuid, @@ -90,7 +93,8 @@ class RustdeskImpl { 'id': id, 'password': password, 'is_shared_password': isSharedPassword, - 'isFileTransfer': isFileTransfer + 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera }) ]); } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 21b87b8489..342764b4a6 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -10,10 +10,10 @@ import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; final testClients = [ - Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), - Client(1, false, false, "UserBBBBB", "221123123", true, false, false), - Client(2, false, false, "UserC", "331123123", true, false, false), - Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) + Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false), + Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false), + Client(2, false, false, false, "UserC", "331123123", true, false, false, false), + Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false) ]; /// flutter run -d {platform} -t test/cm_test.dart to test cm diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 529010f160..d336f2d3b1 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -23,6 +23,7 @@ lazy_static = "1.4" hbb_common = { path = "../hbb_common" } webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} +nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } [dependencies.winapi] version = "0.3" diff --git a/libs/scrap/src/common/camera.rs b/libs/scrap/src/common/camera.rs new file mode 100644 index 0000000000..e857423ed8 --- /dev/null +++ b/libs/scrap/src/common/camera.rs @@ -0,0 +1,232 @@ +use std::{ + io, + sync::{Arc, Mutex}, +}; + +use nokhwa::{ + pixel_format::RgbAFormat, + query, + utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType}, + Camera, +}; + +use hbb_common::message_proto::{DisplayInfo, Resolution}; + +#[cfg(feature = "vram")] +use crate::AdapterDevice; + +use crate::common::{bail, ResultType}; +use crate::{Frame, PixelBuffer, Pixfmt, TraitCapturer}; + +pub const PRIMARY_CAMERA_IDX: usize = 0; +lazy_static::lazy_static! { + static ref SYNC_CAMERA_DISPLAYS: Arc>> = Arc::new(Mutex::new(Vec::new())); +} + +pub struct Cameras; + +// pre-condition +pub fn primary_camera_exists() -> bool { + Cameras::exists(PRIMARY_CAMERA_IDX) +} + +impl Cameras { + pub fn all_info() -> ResultType> { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + return Ok(Vec::new()); + + match query(ApiBackend::Auto) { + Ok(cameras) => { + let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap(); + camera_displays.clear(); + // FIXME: nokhwa returns duplicate info for one physical camera on linux for now. + // issue: https://github.com/l1npengtul/nokhwa/issues/171 + // Use only one camera as a temporary hack. + cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + let Some(info) = cameras.first() else { + bail!("No camera found") + }; + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x: 0, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + } else { + let mut x = 0; + for info in &cameras { + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + x += width; + } + } + } + Ok(camera_displays.clone()) + } + Err(e) => { + bail!("Query cameras error: {}", e) + } + } + } + + pub fn exists(index: usize) -> bool { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + return false; + + match query(ApiBackend::Auto) { + Ok(cameras) => index < cameras.len(), + _ => return false, + } + } + + fn create_camera(index: &CameraIndex) -> ResultType { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + bail!("This platform doesn't support camera yet"); + + let result = Camera::new( + index.clone(), + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestResolution), + ); + match result { + Ok(camera) => Ok(camera), + Err(e) => bail!("create camera{} error: {}", index, e), + } + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + let index = CameraIndex::Index(index as u32); + let camera = Self::create_camera(&index)?; + let resolution = camera.resolution(); + Ok(Resolution { + width: resolution.width() as i32, + height: resolution.height() as i32, + ..Default::default() + }) + } + + pub fn get_sync_cameras() -> Vec { + SYNC_CAMERA_DISPLAYS.lock().unwrap().clone() + } + + pub fn get_capturer(current: usize) -> ResultType> { + Ok(Box::new(CameraCapturer::new(current)?)) + } +} + +pub struct CameraCapturer { + camera: Camera, + data: Vec, + last_data: Vec, // for faster compare and copy +} + +impl CameraCapturer { + fn new(current: usize) -> ResultType { + let index = CameraIndex::Index(current as u32); + let camera = Cameras::create_camera(&index)?; + Ok(CameraCapturer { + camera, + data: Vec::new(), + last_data: Vec::new(), + }) + } +} + +impl TraitCapturer for CameraCapturer { + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + // TODO: move this check outside `frame`. + if !self.camera.is_stream_open() { + if let Err(e) = self.camera.open_stream() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera open stream error: {}", e), + )); + } + } + match self.camera.frame() { + Ok(buffer) => { + match buffer.decode_image::() { + Ok(decoded) => { + self.data = decoded.as_raw().to_vec(); + crate::would_block_if_equal(&mut self.last_data, &self.data)?; + // FIXME: macos's PixelBuffer cannot be directly created from bytes slice. + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "windows"))] { + Ok(Frame::PixelBuffer(PixelBuffer::new( + &self.data, + Pixfmt::RGBA, + decoded.width() as usize, + decoded.height() as usize, + ))) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera is not supported on this platform yet"), + )) + } + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame decode error: {}", e), + )), + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame error: {}", e), + )), + } + } + + #[cfg(windows)] + fn is_gdi(&self) -> bool { + false + } + + #[cfg(windows)] + fn set_gdi(&mut self) -> bool { + false + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} +} diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs index ae2f1130fe..242b4fa1b4 100644 --- a/libs/scrap/src/common/dxgi.rs +++ b/libs/scrap/src/common/dxgi.rs @@ -70,23 +70,29 @@ impl TraitCapturer for Capturer { pub struct PixelBuffer<'a> { data: &'a [u8], + pixfmt: Pixfmt, width: usize, height: usize, stride: Vec, } impl<'a> PixelBuffer<'a> { - pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { + pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { let stride0 = data.len() / height; let mut stride = Vec::new(); stride.push(stride0); PixelBuffer { data, + pixfmt, width, height, stride, } } + + pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self { + Self::new(data, Pixfmt::BGRA, width, height) + } } impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { @@ -107,7 +113,7 @@ impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { } fn pixfmt(&self) -> Pixfmt { - Pixfmt::BGRA + self.pixfmt } } @@ -232,7 +238,7 @@ impl CapturerMag { impl TraitCapturer for CapturerMag { fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> { self.inner.frame(&mut self.data)?; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( &self.data, self.inner.get_rect().1, self.inner.get_rect().2, diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index cef718cc10..7b8388fbd5 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -48,6 +48,7 @@ pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer +pub mod camera; pub mod aom; pub mod record; mod vpx; diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index a9d1af85c0..1f5296954d 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -396,7 +396,7 @@ impl Capturer { } else { let width = self.width; let height = self.height; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( self.get_pixelbuffer(timeout)?, width, height, diff --git a/src/client.rs b/src/client.rs index d2ceffd3cc..12ffe9728a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2348,6 +2348,7 @@ impl LoginConfigHandler { show_hidden: !self.get_option("remote_show_hidden").is_empty(), ..Default::default() }), + ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()), ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { host: self.port_forward.0.clone(), port: self.port_forward.1, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 448eac7f96..4f084151f7 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -83,6 +83,7 @@ struct ParsedPeerInfo { platform: String, is_installed: bool, idd_impl: String, + support_view_camera: bool, } impl ParsedPeerInfo { @@ -129,7 +130,10 @@ impl Remote { #[cfg(target_os = "windows")] let _file_clip_context_holder = { // `is_port_forward()` will not reach here, but we still check it for clarity. - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + if !self.handler.is_file_transfer() + && !self.handler.is_port_forward() + && !self.handler.is_view_camera() + { // It is ok to call this function multiple times. ContextSend::enable(true); Some(crate::SimpleCallOnReturn { @@ -152,6 +156,8 @@ impl Remote { let mut received = false; let conn_type = if self.handler.is_file_transfer() { ConnType::FILE_TRANSFER + } else if self.handler.is_view_camera() { + ConnType::VIEW_CAMERA } else { ConnType::default() }; @@ -173,7 +179,7 @@ impl Remote { .set_connected(); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.update_direct(Some(direct)); - if conn_type == ConnType::DEFAULT_CONN { + if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { self.handler .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); } @@ -190,7 +196,8 @@ impl Remote { { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() - || self.handler.is_rdp(); + || self.handler.is_rdp() + || self.handler.is_view_camera(); if !is_conn_not_default { (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); @@ -330,12 +337,12 @@ impl Remote { .set_disconnected(round); #[cfg(not(target_os = "ios"))] - if _set_disconnected_ok { + if !self.handler.is_view_camera() && _set_disconnected_ok { Client::try_stop_clipboard(); } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] - if _set_disconnected_ok { + if !self.handler.is_view_camera() && _set_disconnected_ok { crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } @@ -1176,6 +1183,25 @@ impl Remote { } } + fn check_view_camera_support(&self, peer_version: &str, peer_platform: &str) -> bool { + if self.peer_info.support_view_camera { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.3.9") + && (peer_platform == "Windows" || peer_platform == "Linux") + { + self.handler.msgbox( + "error", + "Download new version", + "upgrade_remote_rustdesk_client_to_{1.3.9}_tip", + "", + ); + } else { + self.handler.on_error("view_camera_unsupported_tip"); + } + return false; + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1230,10 +1256,19 @@ impl Remote { let peer_version = pi.version.clone(); let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); + if self.handler.is_view_camera() { + if !self.check_view_camera_support(&peer_version, &peer_platform) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } self.handler.handle_peer_info(pi); #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + if !(self.handler.is_file_transfer() + || self.handler.is_port_forward() + || self.handler.is_view_camera()) + { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); @@ -1532,6 +1567,9 @@ impl Remote { ); } } + Ok(Permission::Camera) => { + self.handler.set_permission("camera", p.enabled); + } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); } @@ -1773,6 +1811,11 @@ impl Remote { .flatten() .unwrap_or_default() .to_string(); + self.peer_info.support_view_camera = platform_additions + .get("support_view_camera") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); } } diff --git a/src/core_main.rs b/src/core_main.rs index 182a04a162..4e1fc11595 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -53,6 +53,7 @@ pub fn core_main() -> Option> { "--connect", "--play", "--file-transfer", + "--view-camera", "--port-forward", "--rdp", ] @@ -99,7 +100,7 @@ pub fn core_main() -> Option> { } } #[cfg(windows)] - if args.contains(&"--connect".to_string()) { + if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) { hbb_common::platform::windows::start_cpu_performance_monitor(); } #[cfg(feature = "flutter")] @@ -589,7 +590,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { + "--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" | "--rdp" => { authority = Some((&arg.to_string()[2..]).to_owned()); id = args.next(); } diff --git a/src/flutter.rs b/src/flutter.rs index 2bc8066eda..2e86f6893f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1149,8 +1149,14 @@ pub fn session_add_existed( peer_id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> ResultType<()> { - sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id, displays); + let conn_type = if is_view_camera { + ConnType::VIEW_CAMERA + } else { + ConnType::DEFAULT_CONN + }; + sessions::insert_peer_session_id(peer_id, conn_type, session_id, displays); Ok(()) } @@ -1160,11 +1166,13 @@ pub fn session_add_existed( /// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_view_camera` - If the session is used for view camera. /// * `is_port_forward` - If the session is used for port forward. pub fn session_add( session_id: &SessionID, id: &str, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, switch_uuid: &str, @@ -1175,6 +1183,8 @@ pub fn session_add( ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER + } else if is_view_camera { + ConnType::VIEW_CAMERA } else if is_port_forward { if is_rdp { ConnType::RDP diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cc9d304e7c..b997a391ef 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -92,16 +92,22 @@ pub fn host_stop_system_key_propagate(_stopped: bool) { } // This function is only used to count the number of control sessions. -pub fn peer_get_default_sessions_count(id: String) -> SyncReturn { - SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN)) +pub fn peer_get_sessions_count(id: String, is_view_camera: bool) -> SyncReturn { + let conn_type = if is_view_camera { + ConnType::VIEW_CAMERA + } else { + ConnType::DEFAULT_CONN + }; + SyncReturn(sessions::get_session_count(id, conn_type)) } pub fn session_add_existed_sync( id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> SyncReturn { - if let Err(e) = session_add_existed(id.clone(), session_id, displays) { + if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -112,6 +118,7 @@ pub fn session_add_sync( session_id: SessionID, id: String, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, switch_uuid: String, @@ -124,6 +131,7 @@ pub fn session_add_sync( &session_id, &id, is_file_transfer, + is_view_camera, is_port_forward, is_rdp, &switch_uuid, diff --git a/src/ipc.rs b/src/ipc.rs index 5f533cd94a..7e447576b0 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -188,6 +188,7 @@ pub enum Data { Login { id: i32, is_file_transfer: bool, + is_view_camera: bool, peer_id: String, name: String, authorized: bool, @@ -1280,6 +1281,6 @@ mod test { #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); - assert!(std::mem::size_of::() < 96); + assert!(std::mem::size_of::() <= 96); } } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8e7f90f1fa..9261347f47 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "عرض الكاميرا"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index c9b5a6b493..e4016118c6 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Прагляд камеры"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index af5e4c428a..c1a10096bb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Преглед на камерата"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e5b3ea1592..f8d94e05ba 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sense etiquetar"), ("new-version-of-{}-tip", ""), ("Accessible devices", "Dispositius accessibles"), + ("View camera", "Mostra la càmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7ede944cba..6d7320125e 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), ("Accessible devices", "可访问的设备"), + ("View camera", "查看摄像头"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"), + ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), + ("Enable camera", "允许查看摄像头"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 4b070eb2db..77054ec732 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Zobrazit kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1d22ea9a08..70ffaecb81 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Se kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e482085d85..b26b6d6cb7 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("Accessible devices", "Erreichbare Geräte"), + ("View camera", "Kamera anzeigen"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 5f8dae0b12..d5f6ccd69b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Χωρίς ετικέτα"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("Accessible devices", "Προσβάσιμες συσκευές"), + ("View camera", "Προβολή κάμερας"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 5fe3016ca0..3b20a5bf29 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -237,5 +237,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), ("new-version-of-{}-tip", "There is a new version of {} available"), + ("View camera", "View camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), + ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4bcafc03f0..c6d5565af0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Rigardi kameron"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 245e441575..e90d5f8502 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("Accessible devices", ""), + ("View camera", "Ver cámara"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index e40a2a477f..6e901f9a79 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sildistamata"), ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), ("Accessible devices", "Ligipääsetavad seadmed"), + ("View camera", "Vaata kaamerat"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9b7bc9a8ea..2daba04ad6 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Ikusi kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 816fc6c3e8..499635036e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "نمایش دوربین"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً مشتری RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 23c482fc60..207da95b26 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sans étiquette"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), + ("View camera", "Voir la caméra"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à jour le client RustDesk avec la version {} ou une version plus récente sur l'appareil distant"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 705d3bb130..f2d31f0ec6 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 501da1c12a..bc3c289999 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Pregled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 11c9df22be..18a505ffef 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), + ("View camera", "Kamera megtekintése"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Kérjük, frissítse a RustDesk kliens {} vagy újabb verziójára a távoli oldalon!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2666abad76..9c147e688c 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Lihat Kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Silakan perbarui klien RustDesk ke versi {} atau lebih baru di sisi remote!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 43f72fd929..a5e82391fa 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("Accessible devices", "Dispositivi accessibili"), + ("View camera", "Visualizza telecamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 78cc748fe8..ce039515e4 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "カメラを表示"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8a004809c1..17f2a8345a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "태그 없음"), ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), ("Accessible devices", ""), + ("View camera", "카메라 보기"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "원격 측의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c0a07b9d55..a1e2a77b8e 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Камераны Көру"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index e19badc616..57b1922389 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Peržiūrėti kamerą"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index b15eafa760..56f23899b9 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neatzīmēts"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("Accessible devices", "Pieejamas ierīces"), + ("View camera", "Skatīt kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 51855906b8..708832eef0 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Vis kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 529f47257e..c6f5b6f6d4 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("Accessible devices", "Toegankelijke apparaten"), + ("View camera", "Camera bekijken"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3d4dd3ac79..c2476a470e 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Bez etykiety"), ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("Accessible devices", "Dostępne urządzenia"), + ("View camera", "Podgląd kamery"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e1cde5c3cd..ca0b6a7d86 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Ver câmara"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3a64e4fb09..03ab110452 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Visualizar Câmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b2d02ad711..7e6f1c022a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Vezi camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e0c8eaeb14..6ad129f66f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без метки"), ("new-version-of-{}-tip", "Доступна новая версия {}"), ("Accessible devices", "Доступные устройства"), + ("View camera", "Просмотр камеры"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удаленной стороне!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index bb49857aff..dc75d9bc54 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Chene tag"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), + ("View camera", "Mustra càmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "¡Actualice el cliente RustDesk a la versión {} o más reciente en el lado remoto!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index adbd91fcd9..7709bb7b28 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Zobraziť kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index ada51f77c9..c0ef6c1e10 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neoznačeno"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("Accessible devices", ""), + ("View camera", "Pogled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index fd35d84ba6..2161803744 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 47d88fadb7..7a5dfc87b0 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Pregled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f0a20bd5e8..359ac5a9d6 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Visa kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b5edd1f83d..131c6b64f5 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d56fda6d2a..7af6b92d36 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "ดูกล้อง"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4b4bb484f9..b5ea004c98 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Kamerayı görüntüle"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0eb0b543ba..e9ecbce874 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} 可用"), ("Accessible devices", "可存取的裝置"), + ("View camera", "檢視相機"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 用戶端升級到 {} 或更新版本!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index dfe469518f..63810992dc 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без міток"), ("new-version-of-{}-tip", "Доступна нова версія {}"), ("Accessible devices", ""), + ("View camera", "Перегляд камери"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index cd7e6b7dd1..808455a54e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -657,5 +657,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Xem camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), ].iter().cloned().collect(); } diff --git a/src/server.rs b/src/server.rs index 117b700c17..dcb7023f5c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -24,9 +24,11 @@ use hbb_common::{ sodiumoxide::crypto::{box_, sign}, timeout, tokio, ResultType, Stream, }; +use scrap::camera; #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; +use video_service::VideoSource; use crate::ipc::Data; @@ -76,7 +78,6 @@ const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); - pub static ref CONN_COUNT: Arc> = Default::default(); // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // @@ -279,22 +280,53 @@ async fn create_relay_connection_( impl Server { fn is_video_service_name(name: &str) -> bool { - name.starts_with(video_service::NAME) + name.starts_with(VideoSource::Monitor.service_name_prefix()) + || name.starts_with(VideoSource::Camera.service_name_prefix()) + } + + pub fn try_add_primary_camera_service(&mut self) { + if !camera::primary_camera_exists() { + return; + } + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if !self.contains(&primary_camera_name) { + self.add_service(Box::new(video_service::new( + VideoSource::Camera, + camera::PRIMARY_CAMERA_IDX, + ))); + } } pub fn try_add_primay_video_service(&mut self) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); if !self.contains(&primary_video_service_name) { self.add_service(Box::new(video_service::new( + VideoSource::Monitor, *display_service::PRIMARY_DISPLAY_IDX, ))); } } + pub fn add_camera_connection(&mut self, conn: ConnInner) { + if camera::primary_camera_exists() { + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if let Some(s) = self.services.get(&primary_camera_name) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); for s in self.services.values() { let name = s.name(); if Self::is_video_service_name(&name) && name != primary_video_service_name { @@ -307,7 +339,6 @@ impl Server { #[cfg(target_os = "macos")] self.update_enable_retina(); self.connections.insert(conn.id(), conn); - *CONN_COUNT.lock().unwrap() = self.connections.len(); } pub fn remove_connection(&mut self, conn: &ConnInner) { @@ -315,7 +346,6 @@ impl Server { s.on_unsubscribe(conn.id()); } self.connections.remove(&conn.id()); - *CONN_COUNT.lock().unwrap() = self.connections.len(); #[cfg(target_os = "macos")] self.update_enable_retina(); } @@ -361,10 +391,15 @@ impl Server { self.id_count } - pub fn set_video_service_opt(&self, display: Option, opt: &str, value: &str) { + pub fn set_video_service_opt( + &self, + display: Option<(VideoSource, usize)>, + opt: &str, + value: &str, + ) { for (k, v) in self.services.iter() { - if let Some(display) = display { - if k != &video_service::get_service_name(display) { + if let Some((source, display)) = display { + if k != &video_service::get_service_name(source, display) { continue; } } @@ -392,13 +427,14 @@ impl Server { fn capture_displays( &mut self, conn: ConnInner, + source: VideoSource, displays: &[usize], include: bool, exclude: bool, ) { let displays = displays .iter() - .map(|d| video_service::get_service_name(*d)) + .map(|d| video_service::get_service_name(source, *d)) .collect::>(); let keys = self.services.keys().cloned().collect::>(); for name in keys.iter() { diff --git a/src/server/connection.rs b/src/server/connection.rs index 7b1173fd70..95f4b88eff 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -44,6 +44,7 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; +use scrap::camera; use serde_derive::Serialize; use serde_json::{json, value::Value}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -167,6 +168,7 @@ pub enum AuthConnType { Remote, FileTransfer, PortForward, + ViewCamera, } pub struct Connection { @@ -179,6 +181,7 @@ pub struct Connection { timer: crate::RustDeskInterval, file_timer: crate::RustDeskInterval, file_transfer: Option<(String, bool)>, + view_camera: bool, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, @@ -222,6 +225,7 @@ pub struct Connection { portable: PortableState, from_switch: bool, voice_call_request_timestamp: Option, + voice_calling: bool, options_in_login: Option, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: HashSet, @@ -331,6 +335,7 @@ impl Connection { timer: crate::rustdesk_interval(time::interval(SEC30)), file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_transfer: None, + view_camera: false, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, @@ -369,6 +374,7 @@ impl Connection { from_switch: false, audio_sender: None, voice_call_request_timestamp: None, + voice_calling: false, options_in_login: None, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: Default::default(), @@ -533,9 +539,17 @@ impl Connection { conn.send_permission(Permission::Audio, enabled).await; if conn.authorized { if let Some(s) = conn.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - conn.inner.clone(), conn.audio_enabled()); + if conn.is_authed_view_camera_conn() { + if conn.voice_calling || !conn.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } } } } else if &name == "file" { @@ -774,7 +788,7 @@ impl Connection { }); conn.send(msg_out.into()).await; } - if conn.is_authed_remote_conn() { + if conn.is_authed_remote_conn() || conn.view_camera { if let Some(last_test_delay) = conn.last_test_delay { video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis()); } @@ -1189,6 +1203,8 @@ impl Connection { (1, AuthConnType::FileTransfer) } else if self.port_forward_socket.is_some() { (2, AuthConnType::PortForward) + } else if self.view_camera { + (3, AuthConnType::ViewCamera) } else { (0, AuthConnType::Remote) }; @@ -1277,6 +1293,11 @@ impl Connection { platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); } + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + platform_additions.insert("support_view_camera".into(), json!(true)); + } + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] if !platform_additions.is_empty() { pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); @@ -1290,7 +1311,8 @@ impl Connection { return; } #[cfg(target_os = "linux")] - if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { + if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() && !self.view_camera + { let mut msg = "".to_string(); if crate::platform::linux::is_login_screen_wayland() { msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() @@ -1347,6 +1369,29 @@ impl Connection { self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); if self.file_transfer.is_some() { res.set_peer_info(pi); + } else if self.view_camera { + let supported_encoding = scrap::codec::Encoder::supported_encoding(); + self.last_supported_encoding = Some(supported_encoding.clone()); + log::info!("peer info supported_encoding: {:?}", supported_encoding); + pi.encoding = Some(supported_encoding).into(); + + pi.displays = camera::Cameras::all_info().unwrap_or(Vec::new()); + pi.current_display = camera::PRIMARY_CAMERA_IDX as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: camera::Cameras::get_camera_resolution( + pi.current_display as usize, + ) + .ok() + .into_iter() + .collect(), + ..Default::default() + }) + .into(); + } + res.set_peer_info(pi); + self.update_codec_on_login(); } else { let supported_encoding = scrap::codec::Encoder::supported_encoding(); self.last_supported_encoding = Some(supported_encoding.clone()); @@ -1414,15 +1459,31 @@ impl Connection { } else { self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); } + } else if self.view_camera { + if !wait_session_id_confirm { + self.try_sub_camera_displays(); + } + self.keyboard = false; + self.send_permission(Permission::Keyboard, false).await; } else if sub_service { if !wait_session_id_confirm { - self.try_sub_services(); + self.try_sub_monitor_services(); } } } - fn try_sub_services(&mut self) { - let is_remote = self.file_transfer.is_none() && self.port_forward_socket.is_none(); + fn try_sub_camera_displays(&mut self) { + if let Some(s) = self.server.upgrade() { + let mut s = s.write().unwrap(); + + s.try_add_primary_camera_service(); + s.add_camera_connection(self.inner.clone()); + } + } + + fn try_sub_monitor_services(&mut self) { + let is_remote = + self.file_transfer.is_none() && self.port_forward_socket.is_none() && !self.view_camera; if is_remote && !self.services_subed { self.services_subed = true; if let Some(s) = self.server.upgrade() { @@ -1466,7 +1527,7 @@ impl Connection { if let Some(current_sid) = crate::platform::get_current_process_session_id() { if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && sessions.iter().any(|e| e.sid == current_sid) && get_version_number(&self.lr.version) >= get_version_number("1.2.4") @@ -1539,6 +1600,7 @@ impl Connection { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), + is_view_camera: self.view_camera, port_forward: self.port_forward_address.clone(), peer_id, name, @@ -1781,6 +1843,15 @@ impl Connection { } self.file_transfer = Some((ft.dir, ft.show_hidden)); } + Some(login_request::Union::ViewCamera(_vc)) => { + if !Connection::permission(keys::OPTION_ENABLE_CAMERA) { + self.send_login_error("No permission of viewing camera") + .await; + sleep(1.).await; + return false; + } + self.view_camera = true; + } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; @@ -1987,6 +2058,9 @@ impl Connection { match msg.union { #[allow(unused_mut)] Some(message::Union::MouseEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { log::debug!("call_main_service_pointer_input fail:{}", e); @@ -2005,6 +2079,9 @@ impl Connection { self.update_auto_disconnect_timer(); } Some(message::Union::PointerDeviceEvent(pde)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = match pde.union { Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { @@ -2044,6 +2121,9 @@ impl Connection { Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) @@ -2096,6 +2176,9 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(message::Union::KeyEvent(me)) => { + if self.is_authed_view_camera_conn() { + return true; + } if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); @@ -2592,7 +2675,7 @@ impl Connection { let sessions = crate::platform::get_available_sessions(false); if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && current_process_sid != sid && sessions.iter().any(|e| e.sid == sid) @@ -2606,15 +2689,19 @@ impl Connection { if let Some((dir, show_hidden)) = self.delayed_read_dir.take() { self.read_dir(&dir, show_hidden); } + } else if self.view_camera { + self.try_sub_camera_displays(); } else { - self.try_sub_services(); + self.try_sub_monitor_services(); } } } Some(misc::Union::MessageQuery(mq)) => { - if let Some(msg_out) = - video_service::make_display_changed_msg(mq.switch_display as _, None) - { + if let Some(msg_out) = video_service::make_display_changed_msg( + mq.switch_display as _, + None, + self.video_source(), + ) { self.send(msg_out).await; } } @@ -2713,7 +2800,7 @@ impl Connection { video_service::refresh(); self.server.upgrade().map(|s| { s.read().unwrap().set_video_service_opt( - display, + display.map(|d| (self.video_source(), d)), video_service::OPTION_REFRESH, super::service::SERVICE_OPTION_VALUE_TRUE, ); @@ -2743,19 +2830,33 @@ impl Connection { // 1. For compatibility with old versions ( < 1.2.4 ). // 2. Sciter version. // 3. Update `SupportedResolutions`. - if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { + if let Some(msg_out) = + video_service::make_display_changed_msg(self.display_idx, None, self.video_source()) + { self.send(msg_out).await; } } } + fn video_source(&self) -> VideoSource { + if self.view_camera { + VideoSource::Camera + } else { + VideoSource::Monitor + } + } + fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { - let new_service_name = video_service::get_service_name(display_idx); - let old_service_name = video_service::get_service_name(self.display_idx); + let new_service_name = video_service::get_service_name(self.video_source(), display_idx); + let old_service_name = + video_service::get_service_name(self.video_source(), self.display_idx); let mut lock = server.write().unwrap(); if display_idx != *display_service::PRIMARY_DISPLAY_IDX { if !lock.contains(&new_service_name) { - lock.add_service(Box::new(video_service::new(display_idx))); + lock.add_service(Box::new(video_service::new( + self.video_source(), + display_idx, + ))); } } // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. @@ -2790,26 +2891,27 @@ impl Connection { } async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { + let video_source = self.video_source(); if let Some(sever) = self.server.upgrade() { let mut lock = sever.write().unwrap(); for display in add.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } for display in set.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } if !add.is_empty() { - lock.capture_displays(self.inner.clone(), add, true, false); + lock.capture_displays(self.inner.clone(), video_source, add, true, false); } else if !sub.is_empty() { - lock.capture_displays(self.inner.clone(), sub, false, true); + lock.capture_displays(self.inner.clone(), video_source, sub, false, true); } else { - lock.capture_displays(self.inner.clone(), set, true, true); + lock.capture_displays(self.inner.clone(), video_source, set, true, true); } self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; if self.follow_remote_window { @@ -2931,6 +3033,16 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } self.send(msg).await; + self.voice_calling = accepted; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled() && accepted, + ); + } + } } else { log::warn!("Possible a voice call attack."); } @@ -2940,6 +3052,14 @@ impl Connection { crate::audio_service::set_voice_call_input_device(None, true); // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + self.voice_calling = false; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write() + .unwrap() + .subscribe(super::audio_service::NAME, self.inner.clone(), false); + } + } } async fn update_options(&mut self, o: &OptionMessage) { @@ -3016,11 +3136,21 @@ impl Connection { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - self.inner.clone(), - self.audio_enabled(), - ); + if self.is_authed_view_camera_conn() { + if self.voice_calling || !self.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } } } } @@ -3316,6 +3446,7 @@ impl Connection { fn portable_check(&mut self) { if self.portable.is_installed || self.file_transfer.is_some() + || self.view_camera || self.port_forward_socket.is_some() || !self.keyboard { @@ -3463,6 +3594,13 @@ impl Connection { false } + fn is_authed_view_camera_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::ViewCamera; + } + false + } + #[cfg(feature = "unix-file-copy-paste")] async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { let is_stopping_allowed = clip.is_stopping_allowed(); @@ -3966,7 +4104,6 @@ impl Retina { } mod raii { - // CONN_COUNT: remote connection count in fact // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections @@ -4001,7 +4138,7 @@ mod raii { _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); - if conn_type == AuthConnType::Remote { + if conn_type == AuthConnType::Remote || conn_type == AuthConnType::ViewCamera { video_service::VIDEO_QOS .lock() .unwrap() @@ -4024,12 +4161,12 @@ mod raii { .send((conn_count, remote_count))); } - pub fn remote_and_file_conn_count() -> usize { + pub fn non_port_forward_conn_count() -> usize { AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) + .filter(|c| c.1 != AuthConnType::PortForward) .count() } @@ -4112,7 +4249,7 @@ mod raii { impl Drop for AuthedConnID { fn drop(&mut self) { - if self.1 == AuthConnType::Remote { + if self.1 == AuthConnType::Remote || self.1 == AuthConnType::ViewCamera { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); video_service::VIDEO_QOS .lock() diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 98b42a5fac..33f6650189 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -404,6 +404,7 @@ fn no_displays(displays: &Vec) -> bool { } } + #[inline] #[cfg(not(windows))] pub fn try_get_displays() -> ResultType> { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c7f651e9ac..257f4d71b4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option> { } pub fn try_stop_record_cursor_pos() { - let count_lock = CONN_COUNT.lock().unwrap(); - if *count_lock > 0 { + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.1 == AuthConnType::Remote) + .count(); + if remote_count > 0 { return; } RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 47d2f57896..b8cba71dd7 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -717,7 +717,7 @@ pub mod client { } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, self.height, @@ -808,8 +808,13 @@ pub mod client { }, ConnCount(None) => { if !quick_support { - let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); - stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + let remote_count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.1 == crate::server::AuthConnType::Remote) + .count(); + stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); } }, WillClose => { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 5bc58da45d..973adc81cf 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -51,6 +51,7 @@ use scrap::vram::{VRamEncoder, VRamEncoderConfig}; use scrap::Capturer; use scrap::{ aom::AomEncoderConfig, + camera::Cameras, codec::{Encoder, EncoderCfg}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, @@ -65,7 +66,6 @@ use std::{ time::{self, Duration, Instant}, }; -pub const NAME: &'static str = "video"; pub const OPTION_REFRESH: &'static str = "refresh"; lazy_static::lazy_static! { @@ -133,10 +133,34 @@ impl VideoFrameController { } } +#[derive(Clone, Copy, Debug)] +pub enum VideoSource { + Monitor, + Camera, +} + +impl VideoSource { + pub fn service_name_prefix(&self) -> &'static str { + match self { + VideoSource::Monitor => "monitor", + VideoSource::Camera => "camera", + } + } + + pub fn is_monitor(&self) -> bool { + matches!(self, VideoSource::Monitor) + } + + pub fn is_camera(&self) -> bool { + matches!(self, VideoSource::Camera) + } +} + #[derive(Clone)] pub struct VideoService { sp: GenericService, idx: usize, + source: VideoSource, } impl Deref for VideoService { @@ -153,14 +177,15 @@ impl DerefMut for VideoService { } } -pub fn get_service_name(idx: usize) -> String { - format!("{}{}", NAME, idx) +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) } -pub fn new(idx: usize) -> GenericService { +pub fn new(source: VideoSource, idx: usize) -> GenericService { let vs = VideoService { - sp: GenericService::new(get_service_name(idx), true), + sp: GenericService::new(get_service_name(source, idx), true), idx, + source, }; GenericService::run(&vs, run); vs.sp @@ -292,7 +317,10 @@ impl DerefMut for CapturerInfo { } } -fn get_capturer(current: usize, portable_service_running: bool) -> ResultType { +fn get_capturer_monitor( + current: usize, + portable_service_running: bool, +) -> ResultType { #[cfg(target_os = "linux")] { if !is_x11() { @@ -309,6 +337,7 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType ResultType ResultType { + let cameras = camera::Cameras::get_sync_cameras(); + let ncamera = cameras.len(); + if ncamera <= current { + bail!("Failed to get camera {}, cameras len: {}", current, ncamera,); + } + let Some(camera) = cameras.get(current) else { + bail!( + "Camera of index {} doesn't exist or platform not supported", + current + ); + }; + let capturer = camera::Cameras::get_capturer(current)?; + let (width, height) = (camera.width as usize, camera.height as usize); + let origin = (camera.x as i32, camera.y as i32); + let name = &camera.name; + let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + let _capturer_privacy_mode_id = privacy_mode_id; + log::debug!( + "#cameras={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", + ncamera, + current, + &origin, + width, + height, + num_cpus::get_physical(), + num_cpus::get(), + name, + ); + return Ok(CapturerInfo { + origin, + width, + height, + ndisplay: ncamera, + current, + privacy_mode_id, + _capturer_privacy_mode_id: privacy_mode_id, + capturer, + }); +} +fn get_capturer( + source: VideoSource, + current: usize, + portable_service_running: bool, +) -> ResultType { + match source { + VideoSource::Monitor => get_capturer_monitor(current, portable_service_running), + VideoSource::Camera => get_capturer_camera(current), + } +} + fn run(vs: VideoService) -> ResultType<()> { let _raii = Raii::new(vs.idx); // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. @@ -406,7 +486,7 @@ fn run(vs: VideoService) -> ResultType<()> { let display_idx = vs.idx; let sp = vs.sp; - let mut c = get_capturer(display_idx, last_portable_service_running)?; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(windows)] if !scrap::codec::enable_directx_capture() && !c.is_gdi() { log::info!("disable dxgi with option, fall back to gdi"); @@ -452,9 +532,11 @@ fn run(vs: VideoService) -> ResultType<()> { #[cfg(feature = "vram")] c.set_output_texture(encoder.input_texture()); #[cfg(target_os = "android")] - if let Err(e) = check_change_scale(encoder.is_hardware()) { - try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); - bail!(e); + if vs.source.is_monitor() { + if let Err(e) = check_change_scale(encoder.is_hardware()) { + try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); + bail!(e); + } } VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS @@ -503,7 +585,9 @@ fn run(vs: VideoService) -> ResultType<()> { display_idx, )?; if sp.is_option_true(OPTION_REFRESH) { - let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + if vs.source.is_monitor() { + let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + } log::info!("switch to refresh"); bail!("SWITCH"); } @@ -530,7 +614,9 @@ fn run(vs: VideoService) -> ResultType<()> { VRamEncoder::set_fallback_gdi(display_idx, true); bail!("SWITCH"); } - check_privacy_mode_changed(&sp, display_idx, &c)?; + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } #[cfg(windows)] { if crate::platform::windows::desktop_changed() @@ -540,7 +626,7 @@ fn run(vs: VideoService) -> ResultType<()> { } } let now = time::Instant::now(); - if last_check_displays.elapsed().as_millis() > 1000 { + if vs.source.is_monitor() && last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. @@ -635,7 +721,9 @@ fn run(vs: VideoService) -> ResultType<()> { Err(err) => { // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. - try_broadcast_display_changed(&sp, display_idx, &c, true)?; + if vs.source.is_monitor() { + try_broadcast_display_changed(&sp, display_idx, &c, true)?; + } #[cfg(windows)] if !c.is_gdi() { @@ -657,7 +745,9 @@ fn run(vs: VideoService) -> ResultType<()> { let timeout_millis = 3_000u64; let wait_begin = Instant::now(); while wait_begin.elapsed().as_millis() < timeout_millis as _ { - check_privacy_mode_changed(&sp, display_idx, &c)?; + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } frame_controller.try_wait_next(&mut fetched_conn_ids, 300); // break if all connections have received current frame if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { @@ -1004,7 +1094,9 @@ fn try_broadcast_display_changed( (cap.origin.0, cap.origin.1, cap.width, cap.height), ) { log::info!("Display {} changed", display); - if let Some(msg_out) = make_display_changed_msg(display_idx, Some(display)) { + if let Some(msg_out) = + make_display_changed_msg(display_idx, Some(display), VideoSource::Monitor) + { let msg_out = Arc::new(msg_out); sp.send_shared(msg_out.clone()); // switch display may occur before the first video frame, add snapshot to send to new subscribers @@ -1021,10 +1113,16 @@ fn try_broadcast_display_changed( pub fn make_display_changed_msg( display_idx: usize, opt_display: Option, + source: VideoSource, ) -> Option { let display = match opt_display { Some(d) => d, - None => get_display_info(display_idx)?, + None => match source { + VideoSource::Monitor => display_service::get_display_info(display_idx)?, + VideoSource::Camera => camera::Cameras::get_sync_cameras() + .get(display_idx)? + .clone(), + }, }; let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { @@ -1033,13 +1131,24 @@ pub fn make_display_changed_msg( y: display.y, width: display.width, height: display.height, - cursor_embedded: display_service::capture_cursor_embedded(), + cursor_embedded: match source { + VideoSource::Monitor => display_service::capture_cursor_embedded(), + VideoSource::Camera => false, + }, #[cfg(not(target_os = "android"))] resolutions: Some(SupportedResolutions { - resolutions: if display.name.is_empty() { - vec![] - } else { - crate::platform::resolutions(&display.name) + resolutions: match source { + VideoSource::Monitor => { + if display.name.is_empty() { + vec![] + } else { + crate::platform::resolutions(&display.name) + } + } + VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx) + .ok() + .into_iter() + .collect(), }, ..SupportedResolutions::default() }) diff --git a/src/ui/cm.rs b/src/ui/cm.rs index c8c8c657fb..5e56896aff 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -19,6 +19,7 @@ impl InvokeUiCM for SciterHandler { &make_args!( client.id, client.is_file_transfer, + client.is_view_camera, client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 38f5c5c2d8..fc18bdfd30 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -356,7 +356,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -550,7 +550,7 @@ function adjustHeader() { view.on("size", adjustHeader); -// handler.addConnection(0, false, 0, "", "test1", true, false, false, true, true); -// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false, false); -// handler.addConnection(2, false, 0, "", "test3", true, false, false, false, false); +// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true); +// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false); +// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false); // handler.newMessage(0, 'h'); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 8bcc9cc1b8..a1c0369723 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -316,6 +316,7 @@ impl InvokeUiSession for SciterHandler { ConnType::RDP => {} ConnType::PORT_FORWARD => {} ConnType::FILE_TRANSFER => {} + ConnType::VIEW_CAMERA => {} ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); } @@ -557,6 +558,8 @@ impl SciterSession { let conn_type = if cmd.eq("--file-transfer") { ConnType::FILE_TRANSFER + } else if cmd.eq("--view-camera") { + ConnType::VIEW_CAMERA } else if cmd.eq("--port-forward") { ConnType::PORT_FORWARD } else if cmd.eq("--rdp") { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c1e25362a8..b36c37be79 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -47,6 +47,7 @@ pub struct Client { pub authorized: bool, pub disconnected: bool, pub is_file_transfer: bool, + pub is_view_camera: bool, pub port_forward: String, pub name: String, pub peer_id: String, @@ -128,6 +129,7 @@ impl ConnectionManager { &self, id: i32, is_file_transfer: bool, + is_view_camera: bool, port_forward: String, peer_id: String, name: String, @@ -147,6 +149,7 @@ impl ConnectionManager { authorized, disconnected: false, is_file_transfer, + is_view_camera, port_forward, name: name.clone(), peer_id: peer_id.clone(), @@ -402,9 +405,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -672,6 +675,7 @@ pub async fn start_listen( Some(Data::Login { id, is_file_transfer, + is_view_camera, port_forward, peer_id, name, @@ -690,6 +694,7 @@ pub async fn start_listen( cm.add_connection( id, is_file_transfer, + is_view_camera, port_forward, peer_id, name, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cf339f18b8..6898273ea0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -190,6 +190,14 @@ impl Session { .eq(&ConnType::FILE_TRANSFER) } + pub fn is_view_camera(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::VIEW_CAMERA) + } + pub fn is_port_forward(&self) -> bool { let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP