Merge remote-tracking branch 'upstream/master'
# Conflicts: # Cargo.lock # src/server/connection.rs
This commit is contained in:
commit
b522de3b56
4
.github/workflows/bridge.yml
vendored
4
.github/workflows/bridge.yml
vendored
@ -6,8 +6,8 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
|
4
.github/workflows/flutter-build.yml
vendored
4
.github/workflows/flutter-build.yml
vendored
@ -13,8 +13,8 @@ on:
|
||||
env:
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
# for arm64 linux
|
||||
FLUTTER_ELINUX_VERSION: "3.10.6"
|
||||
FLUTTER_ELINUX_COMMIT_ID: "410b3ca42f2cd0c485edf517a1666652bab442d4"
|
||||
|
4
.github/workflows/history.yml
vendored
4
.github/workflows/history.yml
vendored
@ -4,9 +4,9 @@ on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
LLVM_VERSION: "10.0"
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
TAG_NAME: "tmp"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
# vcpkg version: 2022.05.10
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
|
||||
|
2717
Cargo.lock
generated
2717
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -32,6 +32,13 @@ linux_headless = ["pam" ]
|
||||
virtual_display_driver = ["virtual_display"]
|
||||
plugin_framework = []
|
||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||
unix-file-copy-paste = [
|
||||
"dep:x11-clipboard",
|
||||
"dep:x11rb",
|
||||
"dep:percent-encoding",
|
||||
"dep:once_cell",
|
||||
"clipboard/unix-file-copy-paste",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -62,7 +69,7 @@ num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "1.75", features = ["uuid"], optional = true}
|
||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev", branch = "master" }
|
||||
url = { version = "2.3", features = ["serde"] }
|
||||
@ -132,6 +139,10 @@ dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
@ -153,7 +164,6 @@ winapi = { version = "0.3", features = [ "winnt" ] }
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
flutter_rust_bridge_codegen = "1.75"
|
||||
os-version = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -85,6 +85,7 @@ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxc
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
@ -170,6 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: file copy and paste implemention for Windows, Linux, OSX.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: obsolete Sciter UI (deprecated)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
|
||||
|
25
build.py
25
build.py
@ -24,18 +24,21 @@ else:
|
||||
flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
||||
skip_cargo = False
|
||||
|
||||
|
||||
def get_arch() -> str:
|
||||
custom_arch = os.environ.get("ARCH")
|
||||
if custom_arch is None:
|
||||
return "amd64"
|
||||
return custom_arch
|
||||
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def get_version():
|
||||
with open("Cargo.toml", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
@ -123,6 +126,11 @@ def make_parser():
|
||||
action='store_true',
|
||||
help='Build windows portable'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--unix-file-copy-paste',
|
||||
action='store_true',
|
||||
help='Build with unix file copy paste feature'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--flatpak',
|
||||
action='store_true',
|
||||
@ -185,6 +193,7 @@ def download_extract_features(features, res_dir):
|
||||
import re
|
||||
|
||||
proxy = ''
|
||||
|
||||
def req(url):
|
||||
if not proxy:
|
||||
return url
|
||||
@ -196,9 +205,9 @@ def download_extract_features(features, res_dir):
|
||||
|
||||
for (feat, feat_info) in features.items():
|
||||
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
|
||||
includes = [ re.compile(p) for p in includes ]
|
||||
includes = [re.compile(p) for p in includes]
|
||||
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
|
||||
excludes = [ re.compile(p) for p in excludes ]
|
||||
excludes = [re.compile(p) for p in excludes]
|
||||
|
||||
print(f'{feat} download begin')
|
||||
download_filename = feat_info['zip_url'].split('/')[-1]
|
||||
@ -272,6 +281,8 @@ def get_features(args):
|
||||
features.append('flatpak')
|
||||
if args.appimage:
|
||||
features.append('appimage')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
print("features:", features)
|
||||
return features
|
||||
|
||||
@ -350,6 +361,7 @@ def build_flutter_deb(version, features):
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def build_deb_from_folder(version, binary_folder):
|
||||
os.chdir('flutter')
|
||||
system2('mkdir -p tmpdeb/usr/bin/')
|
||||
@ -388,10 +400,12 @@ def build_deb_from_folder(version, binary_folder):
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def build_flutter_dmg(version, features):
|
||||
if not skip_cargo:
|
||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||
system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
||||
system2(
|
||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
||||
# copy dylib
|
||||
system2(
|
||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||
@ -557,7 +571,8 @@ def main():
|
||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
|
||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
|
||||
'''.format(pa))
|
||||
system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
|
||||
system2(
|
||||
'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
|
||||
os.rename('RustDesk %s.dmg' %
|
||||
version, 'rustdesk-%s.dmg' % version)
|
||||
if pa:
|
||||
@ -577,7 +592,7 @@ def main():
|
||||
else:
|
||||
print('Not signed')
|
||||
else:
|
||||
# buid deb package
|
||||
# build deb package
|
||||
system2(
|
||||
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
|
||||
system2('dpkg-deb -R rustdesk.deb tmpdeb')
|
||||
|
40
build.rs
40
build.rs
@ -76,52 +76,12 @@ fn install_oboe() {
|
||||
//cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper");
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
fn gen_flutter_rust_bridge() {
|
||||
if !std::env::var("RUN_FFIGEN").is_ok() {
|
||||
return;
|
||||
}
|
||||
use lib_flutter_rust_bridge_codegen::{
|
||||
config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
|
||||
};
|
||||
let llvm_path = match std::env::var("LLVM_HOME") {
|
||||
Ok(path) => Some(vec![path]),
|
||||
Err(_) => None,
|
||||
};
|
||||
// Tell Cargo that if the given file changes, to rerun this build script.
|
||||
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
|
||||
// Options for frb_codegen
|
||||
let raw_opts = RawOpts {
|
||||
// Path of input Rust code
|
||||
rust_input: vec!["src/flutter_ffi.rs".to_string()],
|
||||
// Path of output generated Dart code
|
||||
dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()],
|
||||
// Path of output generated C header
|
||||
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
|
||||
/// Path to the installed LLVM
|
||||
llvm_path,
|
||||
// for other options use defaults
|
||||
..Default::default()
|
||||
};
|
||||
// get opts from raw opts
|
||||
let configs = config_parse(raw_opts);
|
||||
// generation of rust api for ffi
|
||||
let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
|
||||
for config in configs.iter() {
|
||||
frb_codegen(config, &all_symbols).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
hbb_common::gen_version();
|
||||
install_oboe();
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
// let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
// if target_os == "android" || target_os == "ios" {
|
||||
#[cfg(feature = "flutter")]
|
||||
gen_flutter_rust_bridge();
|
||||
// return;
|
||||
// }
|
||||
#[cfg(all(windows, feature = "inline"))]
|
||||
build_manifest();
|
||||
#[cfg(windows)]
|
||||
|
@ -104,7 +104,7 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:16.2'
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,14 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent for deep linking-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="rustdesk" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
@ -55,7 +55,7 @@ PODS:
|
||||
- SDWebImage (5.15.5):
|
||||
- SDWebImage/Core (= 5.15.5)
|
||||
- SDWebImage/Core (5.15.5)
|
||||
- sqflite (0.0.2):
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.4)
|
||||
@ -65,7 +65,8 @@ PODS:
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
@ -80,8 +81,8 @@ DEPENDENCIES:
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- uni_links (from `.symlinks/plugins/uni_links/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@ -116,30 +117,30 @@ EXTERNAL SOURCES:
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
|
||||
video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
||||
PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
|
||||
|
||||
|
@ -159,7 +159,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -434,7 +434,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -632,7 +632,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@ -722,7 +722,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -24,6 +24,21 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.carriez.rustdesk</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>rustdesk</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
@ -1955,6 +1955,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
String? command;
|
||||
String? id;
|
||||
final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
|
||||
if (uri.authority.isEmpty &&
|
||||
uri.path.split('').every((char) => char == '/')) {
|
||||
return [];
|
||||
@ -1962,18 +1963,33 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
// For compatibility
|
||||
command = '--connect';
|
||||
id = uri.path.substring("/new/".length);
|
||||
} else if (['connect', "play", 'file-transfer', 'port-forward', 'rdp']
|
||||
.contains(uri.authority)) {
|
||||
} else if (options.contains(uri.authority)) {
|
||||
final optionIndex = options.indexOf(uri.authority);
|
||||
command = '--${uri.authority}';
|
||||
if (uri.path.length > 1) {
|
||||
id = uri.path.substring(1);
|
||||
}
|
||||
if (isMobile && id != null) {
|
||||
if (optionIndex == 0 || optionIndex == 1) {
|
||||
connect(Get.context!, id);
|
||||
} else if (optionIndex == 2) {
|
||||
connect(Get.context!, id, isFileTransfer: true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else if (uri.authority.length > 2 && uri.path.length <= 1) {
|
||||
// rustdesk://<connect-id>
|
||||
command = '--connect';
|
||||
id = uri.authority;
|
||||
}
|
||||
|
||||
if (isMobile){
|
||||
if (id != null){
|
||||
connect(Get.context!, id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> args = List.empty(growable: true);
|
||||
if (command != null && id != null) {
|
||||
args.add(command);
|
||||
|
@ -972,7 +972,7 @@ void showRestartRemoteDevice(PeerInfo pi, String id, SessionID sessionId,
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Flexible(
|
||||
child: Text(translate("Restart Remote Device"))
|
||||
child: Text(translate("Restart remote device"))
|
||||
.paddingOnly(left: 10)),
|
||||
]),
|
||||
content: Text(
|
||||
@ -1248,25 +1248,41 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
double fpsInitValue = 30;
|
||||
bool qualitySet = false;
|
||||
bool fpsSet = false;
|
||||
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
bool hideFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
bool hideMoreQuality =
|
||||
(await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.2') < 0;
|
||||
|
||||
setCustomValues({double? quality, double? fps}) async {
|
||||
if (quality != null) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
sessionId: sessionId, value: quality.toInt());
|
||||
print("quality:$quality");
|
||||
}
|
||||
if (fps != null) {
|
||||
fpsSet = true;
|
||||
await bind.sessionSetCustomFps(sessionId: sessionId, fps: fps.toInt());
|
||||
print("fps:$fps");
|
||||
}
|
||||
if (!qualitySet) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
sessionId: sessionId, value: qualityInitValue.toInt());
|
||||
print("qualityInitValue:$qualityInitValue");
|
||||
}
|
||||
if (!fpsSet) {
|
||||
if (!hideFps && !fpsSet) {
|
||||
fpsSet = true;
|
||||
await bind.sessionSetCustomFps(
|
||||
sessionId: sessionId, fps: fpsInitValue.toInt());
|
||||
print("fpsInitValue:$fpsInitValue");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1279,7 +1295,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
|
||||
qualityInitValue =
|
||||
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
||||
if (qualityInitValue < 10 || qualityInitValue > 2000) {
|
||||
if ((hideMoreQuality && qualityInitValue > 100) ||
|
||||
qualityInitValue < 10 ||
|
||||
qualityInitValue > 2000) {
|
||||
qualityInitValue = 50;
|
||||
}
|
||||
// fps
|
||||
@ -1289,20 +1307,14 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||
fpsInitValue = 30;
|
||||
}
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
|
||||
final content = customImageQualityWidget(
|
||||
initQuality: qualityInitValue,
|
||||
initFps: fpsInitValue,
|
||||
setQuality: (v) => setCustomValues(quality: v),
|
||||
setFps: (v) => setCustomValues(fps: v),
|
||||
showFps: !notShowFps);
|
||||
showFps: !hideFps,
|
||||
showMoreQuality: !hideMoreQuality);
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
||||
|
@ -27,45 +27,44 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return isIOS
|
||||
? IOSDraggable (
|
||||
position: position,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMobileAppBar(context),
|
||||
Expanded(
|
||||
child: ChatPage(chatModel: chatModel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
builder: (context, onPanUpdate) {
|
||||
final child =
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: isDesktop
|
||||
? _buildDesktopAppBar(context)
|
||||
: _buildMobileAppBar(context),
|
||||
? IOSDraggable(
|
||||
position: position,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMobileAppBar(context),
|
||||
Expanded(
|
||||
child: ChatPage(chatModel: chatModel),
|
||||
),
|
||||
body: ChatPage(chatModel: chatModel),
|
||||
);
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: child);
|
||||
});
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
builder: (context, onPanUpdate) {
|
||||
final child = Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: isDesktop
|
||||
? _buildDesktopAppBar(context)
|
||||
: _buildMobileAppBar(context),
|
||||
),
|
||||
body: ChatPage(chatModel: chatModel),
|
||||
);
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: child);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMobileAppBar(BuildContext context) {
|
||||
@ -354,14 +353,14 @@ class _DraggableState extends State<Draggable> {
|
||||
}
|
||||
|
||||
class IOSDraggable extends StatefulWidget {
|
||||
const IOSDraggable({
|
||||
Key? key,
|
||||
this.position = Offset.zero,
|
||||
this.chatModel,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
const IOSDraggable(
|
||||
{Key? key,
|
||||
this.position = Offset.zero,
|
||||
this.chatModel,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
|
||||
final Offset position;
|
||||
final ChatModel? chatModel;
|
||||
@ -423,7 +422,7 @@ class _IOSDraggableState extends State<IOSDraggable> {
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkKeyboard();
|
||||
return Stack(
|
||||
@ -439,12 +438,12 @@ class _IOSDraggableState extends State<IOSDraggable> {
|
||||
_chatModel?.setChatWindowPosition(_position);
|
||||
},
|
||||
child: Material(
|
||||
child:
|
||||
Container(
|
||||
width: _width,
|
||||
height: _height,
|
||||
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: widget.builder(context),
|
||||
child: Container(
|
||||
width: _width,
|
||||
height: _height,
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: widget.builder(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -499,6 +498,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -495,7 +495,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('Transfer File'),
|
||||
translate('Transfer file'),
|
||||
isFileTransfer: true,
|
||||
);
|
||||
}
|
||||
@ -505,7 +505,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('TCP Tunneling'),
|
||||
translate('TCP tunneling'),
|
||||
isTcpTunneling: true,
|
||||
);
|
||||
}
|
||||
@ -568,7 +568,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
MenuEntryBase<String> _createShortCutAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Create Desktop Shortcut'),
|
||||
translate('Create desktop shortcut'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
@ -818,7 +818,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
MenuEntryBase<String> _addToAb(Peer peer) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Add to Address Book'),
|
||||
translate('Add to address book'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
|
@ -75,9 +75,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
void initState() {
|
||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
|
||||
? PeerUiType.list
|
||||
: PeerUiType.grid;
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
? PeerUiType.grid
|
||||
: int.parse(uiType) == 1
|
||||
? PeerUiType.tile
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
@ -454,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Add to Address Book'),
|
||||
message: translate('Add to address book'),
|
||||
child: Icon(model.icons[PeerTabIndex.ab.index])),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
);
|
||||
@ -763,8 +765,6 @@ class PeerViewDropdown extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
RelativeRect menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<PeerUiType> types = [PeerUiType.grid, PeerUiType.tile, PeerUiType.list];
|
||||
@ -804,6 +804,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
))));
|
||||
}
|
||||
|
||||
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
child: Tooltip(
|
||||
@ -819,16 +820,14 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
setState(() {
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
});
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () => showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
items: items,
|
||||
elevation: 8,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ customImageQualityWidget(
|
||||
required double initFps,
|
||||
required Function(double) setQuality,
|
||||
required Function(double) setFps,
|
||||
required bool showFps}) {
|
||||
required bool showFps,
|
||||
required bool showMoreQuality}) {
|
||||
if (!showMoreQuality && initQuality > 100) {
|
||||
initQuality = 50;
|
||||
}
|
||||
final qualityValue = initQuality.obs;
|
||||
final fpsValue = initFps.obs;
|
||||
|
||||
@ -69,7 +73,7 @@ customImageQualityWidget(
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
// mobile doesn't have enough space
|
||||
if (!isMobile)
|
||||
if (showMoreQuality && !isMobile)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
@ -85,7 +89,7 @@ customImageQualityWidget(
|
||||
))
|
||||
],
|
||||
)),
|
||||
if (isMobile)
|
||||
if (showMoreQuality && isMobile)
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -160,7 +164,8 @@ customImageQualitySetting() {
|
||||
setFps: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
showFps: true);
|
||||
showFps: true,
|
||||
showMoreQuality: true);
|
||||
}
|
||||
|
||||
Future<bool> setServerConfig(
|
||||
@ -265,7 +270,7 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
|
||||
return [
|
||||
Tooltip(
|
||||
message: translate('Import Server Config'),
|
||||
message: translate('Import server config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
|
||||
),
|
||||
|
@ -133,7 +133,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer File')),
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
@ -141,7 +141,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP Tunneling')),
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
@ -176,7 +176,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Restart Remote Device')),
|
||||
child: Text(translate('Restart remote device')),
|
||||
onPressed: () =>
|
||||
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
|
||||
);
|
||||
@ -191,6 +191,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
}
|
||||
// blockUserInput
|
||||
if (ffi.ffiModel.keyboard &&
|
||||
ffi.ffiModel.permissions['block_input'] != false &&
|
||||
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
|
||||
{
|
||||
v.add(TTextMenu(
|
||||
@ -436,9 +437,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
if (Platform.isWindows &&
|
||||
pi.platform == kPeerPlatformWindows &&
|
||||
perms['file'] != false) {
|
||||
if (perms['file'] != false &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
||||
final option = 'enable-file-transfer';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
@ -547,5 +548,22 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Use all my displays for the remote session'))));
|
||||
}
|
||||
|
||||
// 444
|
||||
final codec_format = ffi.qualityMonitorModel.data.codecFormat;
|
||||
if (versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
(codec_format == "AV1" || codec_format == "VP9")) {
|
||||
final option = 'i444';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||
},
|
||||
child: Text(translate('True color (4:4:4)'))));
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||
const String kPlatformAdditionsHeadless = "headless";
|
||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
|
@ -50,6 +50,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
return list.sublist(0, n);
|
||||
}
|
||||
}
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
|
||||
@ -81,7 +82,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()){
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
super.dispose();
|
||||
@ -157,9 +158,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
peers = await getAllPeers();
|
||||
setState(() {
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
@ -177,148 +178,173 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
message: translate("id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
}
|
||||
else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
alias: '',
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
alias: '',
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
if (int.tryParse(textWithoutSpaces) != null) {
|
||||
textEditingValue = TextEditingValue(
|
||||
text: textWithoutSpaces,
|
||||
selection: textEditingValue.selection,
|
||||
);
|
||||
return [emptyPeer];
|
||||
}
|
||||
else {
|
||||
String textWithoutSpaces = textEditingValue.text.replaceAll(" ", "");
|
||||
if (int.tryParse(textWithoutSpaces) != null) {
|
||||
textEditingValue = TextEditingValue(
|
||||
text: textWithoutSpaces,
|
||||
selection: textEditingValue.selection,
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username.toLowerCase().contains(textToFind) ||
|
||||
peer.hostname.toLowerCase().contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
return peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.hostname
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted,
|
||||
) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idInputFocused.value = fieldFocusNode.hasFocus;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading) {
|
||||
_fetchPeers();
|
||||
}
|
||||
},
|
||||
});
|
||||
final textLength =
|
||||
fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
return Obx(() => TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: fieldFocusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 13)),
|
||||
controller: fieldTextEditingController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onChanged: (v) {
|
||||
_idController.id = v;
|
||||
},
|
||||
));
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
double maxHeight = options.length * 50;
|
||||
maxHeight = maxHeight > 200 ? 200 : maxHeight;
|
||||
|
||||
fieldViewBuilder: (BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode ,
|
||||
VoidCallback onFieldSubmitted,
|
||||
) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idInputFocused.value = fieldFocusNode.hasFocus;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading){
|
||||
_fetchPeers();
|
||||
}
|
||||
});
|
||||
final textLength = fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection = TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
return Obx(() =>
|
||||
TextField(
|
||||
maxLength: 90,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: fieldFocusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor: Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 13)),
|
||||
controller: fieldTextEditingController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onChanged: (v) {
|
||||
_idController.id = v;
|
||||
},
|
||||
));
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<Peer> onSelected, Iterable<Peer> options) {
|
||||
double maxHeight = options.length * 50;
|
||||
maxHeight = maxHeight > 200 ? 200 : maxHeight;
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ClipRRect(
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 319,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: ListView(
|
||||
children: options.map((peer) => AutocompletePeerTile(onSelect: () => onSelected(peer), peer: peer)).toList(),
|
||||
elevation: 4,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 319,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: ListView(
|
||||
children: options
|
||||
.map((peer) => AutocompletePeerTile(
|
||||
onSelect: () =>
|
||||
onSelected(peer),
|
||||
peer: peer))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
@ -329,7 +355,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () => onConnect(isFileTransfer: true),
|
||||
text: "Transfer File",
|
||||
text: "Transfer file",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 17,
|
||||
@ -382,7 +408,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
onTap: () async {
|
||||
await start_service(true);
|
||||
},
|
||||
child: Text(translate("Start Service"),
|
||||
child: Text(translate("Start service"),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: em)))
|
||||
|
@ -336,11 +336,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (!bind.mainIsInstalled()) {
|
||||
return buildInstallCard(
|
||||
"", "install_tip", "Install", bind.mainGotoInstall);
|
||||
return buildInstallCard("", "install_tip", "Install", () async {
|
||||
await rustDeskWinManager.closeAllSubWindows();
|
||||
bind.mainGotoInstall();
|
||||
});
|
||||
} else if (bind.mainIsInstalledLowerVersion()) {
|
||||
return buildInstallCard("Status", "Your installation is lower version.",
|
||||
"Click to upgrade", bind.mainUpdateMe);
|
||||
return buildInstallCard(
|
||||
"Status", "Your installation is lower version.", "Click to upgrade",
|
||||
() async {
|
||||
await rustDeskWinManager.closeAllSubWindows();
|
||||
bind.mainUpdateMe();
|
||||
});
|
||||
}
|
||||
} else if (Platform.isMacOS) {
|
||||
if (!bind.mainIsCanScreenRecording(prompt: false)) {
|
||||
@ -384,13 +390,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final keyShowSelinuxHelpTip = "show-selinux-help-tip";
|
||||
if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
|
||||
LinuxCards.add(buildInstallCard(
|
||||
"Warning", "selinux_tip", "", () async {},
|
||||
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
||||
help: 'Help',
|
||||
link:
|
||||
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
|
||||
closeButton: true,
|
||||
closeOption: keyShowSelinuxHelpTip,
|
||||
"Warning",
|
||||
"selinux_tip",
|
||||
"",
|
||||
() async {},
|
||||
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
||||
help: 'Help',
|
||||
link:
|
||||
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
|
||||
closeButton: true,
|
||||
closeOption: keyShowSelinuxHelpTip,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -418,7 +427,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
|
||||
Widget buildInstallCard(String title, String content, String btnText,
|
||||
GestureTapCallback onPressed,
|
||||
{double marginTop = 20.0, String? help, String? link, bool? closeButton, String? closeOption}) {
|
||||
{double marginTop = 20.0,
|
||||
String? help,
|
||||
String? link,
|
||||
bool? closeButton,
|
||||
String? closeOption}) {
|
||||
void closeCard() async {
|
||||
if (closeOption != null) {
|
||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||
@ -439,89 +452,90 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: marginTop),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 226, 66, 188),
|
||||
Color.fromARGB(255, 244, 114, 124),
|
||||
],
|
||||
)),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: (title.isNotEmpty
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
translate(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
).marginOnly(bottom: 6)),
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
] +
|
||||
(btnText.isNotEmpty
|
||||
? <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FixedWidthButton(
|
||||
width: 150,
|
||||
padding: 8,
|
||||
isOutline: true,
|
||||
text: translate(btnText),
|
||||
textColor: Colors.white,
|
||||
borderColor: Colors.white,
|
||||
textSize: 20,
|
||||
radius: 10,
|
||||
onTap: onPressed,
|
||||
)
|
||||
])
|
||||
]
|
||||
: <Widget>[]) +
|
||||
(help != null
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () async =>
|
||||
await launchUrl(Uri.parse(link!)),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 226, 66, 188),
|
||||
Color.fromARGB(255, 244, 114, 124),
|
||||
],
|
||||
)),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: (title.isNotEmpty
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
translate(help),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
)).marginOnly(top: 6)),
|
||||
]
|
||||
: <Widget>[]))),
|
||||
translate(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
).marginOnly(bottom: 6)),
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
] +
|
||||
(btnText.isNotEmpty
|
||||
? <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FixedWidthButton(
|
||||
width: 150,
|
||||
padding: 8,
|
||||
isOutline: true,
|
||||
text: translate(btnText),
|
||||
textColor: Colors.white,
|
||||
borderColor: Colors.white,
|
||||
textSize: 20,
|
||||
radius: 10,
|
||||
onTap: onPressed,
|
||||
)
|
||||
])
|
||||
]
|
||||
: <Widget>[]) +
|
||||
(help != null
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () async =>
|
||||
await launchUrl(Uri.parse(link!)),
|
||||
child: Text(
|
||||
translate(help),
|
||||
style: TextStyle(
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
)).marginOnly(top: 6)),
|
||||
]
|
||||
: <Widget>[]))),
|
||||
),
|
||||
if (closeButton != null && closeButton == true)
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: closeCard,
|
||||
),
|
||||
onPressed: closeCard,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -632,23 +632,27 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
}).marginOnly(left: _kContentHMargin),
|
||||
Column(
|
||||
children: [
|
||||
_OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard',
|
||||
_OptionCheckBox(context, 'Enable keyboard/mouse', 'enable-keyboard',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard',
|
||||
_OptionCheckBox(context, 'Enable clipboard', 'enable-clipboard',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable File Transfer', 'enable-file-transfer',
|
||||
context, 'Enable file transfer', 'enable-file-transfer',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable Audio', 'enable-audio',
|
||||
_OptionCheckBox(context, 'Enable audio', 'enable-audio',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel',
|
||||
_OptionCheckBox(context, 'Enable TCP tunneling', 'enable-tunnel',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable Remote Restart', 'enable-remote-restart',
|
||||
context, 'Enable remote restart', 'enable-remote-restart',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable Recording Session', 'enable-record-session',
|
||||
context, 'Enable recording session', 'enable-record-session',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
if (Platform.isWindows)
|
||||
_OptionCheckBox(
|
||||
context, 'Enable blocking user input', 'enable-block-input',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable remote configuration modification',
|
||||
'allow-remote-config-modification',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
@ -769,7 +773,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
bool enabled = !locked;
|
||||
return _Card(title: 'Security', children: [
|
||||
shareRdp(context, enabled),
|
||||
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
|
||||
_OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
|
||||
reverse: true, enabled: enabled),
|
||||
...directIp(context),
|
||||
whitelist(),
|
||||
@ -809,7 +813,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
update() => setState(() {});
|
||||
RxBool applyEnabled = false.obs;
|
||||
return [
|
||||
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
|
||||
_OptionCheckBox(context, 'Enable direct IP access', 'direct-server',
|
||||
update: update, enabled: !locked),
|
||||
() {
|
||||
// Simple temp wrapper for PR check
|
||||
@ -1320,6 +1324,7 @@ class _DisplayState extends State<_Display> {
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
|
||||
otherRow('True color (4:4:4)', 'i444'),
|
||||
];
|
||||
if (useTextureRender) {
|
||||
children.add(otherRow('Show displays as individual windows',
|
||||
|
@ -15,7 +15,7 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
@ -91,7 +91,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
@ -104,7 +104,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
_ffi.close();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
@ -1126,10 +1126,11 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
|
||||
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
|
||||
Entry entry, bool isLocal) {
|
||||
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||
.contains(LogicalKeyboardKey.controlLeft);
|
||||
final isCtrlDown = RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.controlLeft) ||
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.controlRight);
|
||||
final isShiftDown =
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft) ||
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftRight);
|
||||
if (isCtrlDown) {
|
||||
if (selectedItems.items.contains(entry)) {
|
||||
selectedItems.remove(entry);
|
||||
|
@ -8,7 +8,7 @@ import 'package:flutter_custom_cursor/cursor_manager.dart'
|
||||
as custom_cursor_manager;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
|
||||
@ -123,7 +123,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
@ -183,7 +183,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
void onWindowMaximize() {
|
||||
super.onWindowMaximize();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +200,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
void onWindowMinimize() {
|
||||
super.onWindowMinimize();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
await Wakelock.disable();
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
await Get.delete<FFI>(tag: widget.id);
|
||||
removeSharedStates(widget.id);
|
||||
|
@ -386,7 +386,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Restart Remote Device'),
|
||||
translate('Restart remote device'),
|
||||
style: style,
|
||||
),
|
||||
proc: () => showRestartRemoteDevice(
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||
@ -482,8 +483,8 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
client.type_() != ClientType.file),
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(client.id, () {
|
||||
if (client.type_() != ClientType.file) {
|
||||
gFFI.chatModel.toggleCMSidePage();
|
||||
if (client.type_() == ClientType.file) {
|
||||
gFFI.chatModel.toggleCMFilePage();
|
||||
} else {
|
||||
gFFI.chatModel
|
||||
.toggleCMChatPage(MessageKey(client.peerId, client.id));
|
||||
@ -519,6 +520,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
Function(bool)? onTap, String tooltipText) {
|
||||
return Tooltip(
|
||||
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
||||
waitDuration: Duration.zero,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? MyTheme.accent : Colors.grey[700],
|
||||
@ -535,7 +537,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -547,9 +548,11 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final crossAxisCount = 4;
|
||||
final spacing = 10.0;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 200.0,
|
||||
height: 160.0,
|
||||
margin: EdgeInsets.all(5.0),
|
||||
padding: EdgeInsets.all(5.0),
|
||||
decoration: BoxDecoration(
|
||||
@ -574,10 +577,10 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
).marginOnly(left: 4.0, bottom: 8.0),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 3,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
mainAxisSpacing: 20.0,
|
||||
crossAxisSpacing: 20.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing),
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
children: [
|
||||
buildPermissionIcon(
|
||||
client.keyboard,
|
||||
@ -589,7 +592,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.keyboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow using keyboard and mouse'),
|
||||
translate('Enable keyboard/mouse'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.clipboard,
|
||||
@ -601,7 +604,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.clipboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow using clipboard'),
|
||||
translate('Enable clipboard'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.audio,
|
||||
@ -613,7 +616,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.audio = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow hearing sound'),
|
||||
translate('Enable audio'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.file,
|
||||
@ -625,7 +628,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.file = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow file copy and paste'),
|
||||
translate('Enable file copy and paste'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.restart,
|
||||
@ -637,7 +640,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.restart = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow remote restart'),
|
||||
translate('Enable remote restart'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.recording,
|
||||
@ -649,8 +652,24 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.recording = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow recording session'),
|
||||
)
|
||||
translate('Enable recording session'),
|
||||
),
|
||||
// only windows support block input
|
||||
if (Platform.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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -975,6 +994,49 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
);
|
||||
}
|
||||
|
||||
iconLabel(CmFileLog item) {
|
||||
switch (item.action) {
|
||||
case CmFileAction.none:
|
||||
return Container();
|
||||
case CmFileAction.localToRemote:
|
||||
case CmFileAction.remoteToLocal:
|
||||
return Column(
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
),
|
||||
Text(item.action == CmFileAction.remoteToLocal
|
||||
? translate('Send')
|
||||
: translate('Receive'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.remove:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Delete'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.createDir:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.create_new_folder,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget statusList() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size(200, double.infinity),
|
||||
@ -983,7 +1045,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
child: Obx(
|
||||
() {
|
||||
final jobTable = gFFI.cmFileModel.currentJobTable;
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
statusListView(List<CmFileLog> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
@ -998,22 +1060,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Column(
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? 0 : pi,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
),
|
||||
Text(item.isRemoteToLocal
|
||||
? translate('Send')
|
||||
: translate('Receive'))
|
||||
],
|
||||
),
|
||||
child: iconLabel(item),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
@ -1048,8 +1095,9 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage:
|
||||
item.state == JobState.inProgress,
|
||||
offstage: !(item.isTransfer() &&
|
||||
item.state !=
|
||||
JobState.inProgress),
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
|
@ -107,7 +107,7 @@ class _ToolbarTheme {
|
||||
static const double dividerHeight = 12.0;
|
||||
|
||||
static const double buttonSize = 32;
|
||||
static const double buttonHMargin = 3;
|
||||
static const double buttonHMargin = 2;
|
||||
static const double buttonVMargin = 6;
|
||||
static const double iconRadius = 8;
|
||||
static const double elevation = 3;
|
||||
@ -125,12 +125,13 @@ class _ToolbarTheme {
|
||||
: EdgeInsets.fromLTRB(6, 14, 6, 14);
|
||||
static const double menuButtonBorderRadius = 3.0;
|
||||
|
||||
static get borderColor =>
|
||||
MyTheme.currentThemeMode() == ThemeMode.light ? bordLight : bordDark;
|
||||
|
||||
static final defaultMenuStyle = MenuStyle(
|
||||
side: MaterialStateProperty.all(BorderSide(
|
||||
width: 1,
|
||||
color: MyTheme.currentThemeMode() == ThemeMode.light
|
||||
? _ToolbarTheme.bordLight
|
||||
: _ToolbarTheme.bordDark,
|
||||
color: borderColor,
|
||||
)),
|
||||
shape: MaterialStatePropertyAll(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_ToolbarTheme.menuBorderRadius))),
|
||||
@ -141,6 +142,19 @@ class _ToolbarTheme {
|
||||
padding: MaterialStatePropertyAll(EdgeInsets.zero),
|
||||
overlayColor: MaterialStatePropertyAll(Colors.transparent),
|
||||
);
|
||||
|
||||
static Widget borderWrapper(Widget child, BorderRadius borderRadius) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef DismissFunc = void Function();
|
||||
@ -420,6 +434,9 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
if (show.isTrue && _dragging.isFalse) {
|
||||
triggerAutoHide();
|
||||
}
|
||||
final borderRadius = BorderRadius.vertical(
|
||||
bottom: Radius.circular(5),
|
||||
);
|
||||
return Align(
|
||||
alignment: FractionalOffset(_fractionX.value, 0),
|
||||
child: Offstage(
|
||||
@ -427,6 +444,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
child: Material(
|
||||
elevation: _ToolbarTheme.elevation,
|
||||
shadowColor: MyTheme.color(context).shadow,
|
||||
borderRadius: borderRadius,
|
||||
child: _DraggableShowHide(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
dragging: _dragging,
|
||||
@ -434,6 +452,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
show: show,
|
||||
setFullscreen: _setFullscreen,
|
||||
setMinimize: _minimize,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -475,13 +494,14 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
}
|
||||
toolbarItems.add(_RecordMenu());
|
||||
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
|
||||
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Material(
|
||||
elevation: _ToolbarTheme.elevation,
|
||||
shadowColor: MyTheme.color(context).shadow,
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
borderRadius: toolbarBorderRadius,
|
||||
color: Theme.of(context)
|
||||
.menuBarTheme
|
||||
.style
|
||||
@ -491,13 +511,15 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Theme(
|
||||
data: themeData(),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
|
||||
...toolbarItems,
|
||||
SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
|
||||
],
|
||||
),
|
||||
child: _ToolbarTheme.borderWrapper(
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
|
||||
...toolbarItems,
|
||||
SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
|
||||
],
|
||||
),
|
||||
toolbarBorderRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -598,14 +620,19 @@ class _MonitorMenu extends StatelessWidget {
|
||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
showMonitorsToolbar ? buildMultiMonitorMenu() : buildMonitorMenu();
|
||||
Widget build(BuildContext context) => showMonitorsToolbar
|
||||
? buildMultiMonitorMenu()
|
||||
: Obx(() => buildMonitorMenu());
|
||||
|
||||
Widget buildMonitorMenu() {
|
||||
final width = SimpleWrapper<double>(0);
|
||||
final monitorsIcon =
|
||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Select Monitor',
|
||||
icon: icon(),
|
||||
icon: monitorsIcon,
|
||||
ffi: ffi,
|
||||
width: width.value,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuStyle: MenuStyle(
|
||||
@ -644,26 +671,37 @@ class _MonitorMenu extends StatelessWidget {
|
||||
child: Text(translate('Show displays as individual windows')));
|
||||
}
|
||||
|
||||
buildOneMonitorButton(i, curDisplay) => Text(
|
||||
'${i + 1}',
|
||||
style: TextStyle(
|
||||
color: i == curDisplay
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> buildMonitorList(bool isMulti) {
|
||||
final List<Widget> monitorList = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
|
||||
getMonitorText(int i) {
|
||||
if (i == kAllDisplayValue) {
|
||||
if (pi.displays.length == 2) {
|
||||
return '1|2';
|
||||
} else {
|
||||
return 'ALL';
|
||||
}
|
||||
} else {
|
||||
return (i + 1).toString();
|
||||
}
|
||||
}
|
||||
|
||||
buildMonitorButton(int i) => Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
|
||||
final isAllMonitors = i == kAllDisplayValue;
|
||||
final width = SimpleWrapper<double>(0);
|
||||
Widget? monitorsIcon;
|
||||
if (isAllMonitors) {
|
||||
monitorsIcon = globalMonitorsWidget(
|
||||
width, Colors.white, _ToolbarTheme.blueColor);
|
||||
}
|
||||
return _IconMenuButton(
|
||||
tooltip: isMulti ? '' : '#${i + 1} monitor',
|
||||
tooltip: isMulti
|
||||
? ''
|
||||
: isAllMonitors
|
||||
? 'all monitors'
|
||||
: '#${i + 1} monitor',
|
||||
hMargin: isMulti ? null : 6,
|
||||
vMargin: isMulti ? null : 12,
|
||||
topLevel: false,
|
||||
@ -673,33 +711,25 @@ class _MonitorMenu extends StatelessWidget {
|
||||
hoverColor: i == display.value
|
||||
? _ToolbarTheme.hoverBlueColor
|
||||
: _ToolbarTheme.hoverInactiveColor,
|
||||
icon: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: _ToolbarTheme.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
getMonitorText(i),
|
||||
style: TextStyle(
|
||||
color: i == display.value
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
width: isAllMonitors ? width.value : null,
|
||||
icon: isAllMonitors
|
||||
? monitorsIcon
|
||||
: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: _ToolbarTheme.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Obx(() => buildOneMonitorButton(i, display.value)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () => onPressed(i, pi),
|
||||
);
|
||||
});
|
||||
@ -713,26 +743,69 @@ class _MonitorMenu extends StatelessWidget {
|
||||
return monitorList;
|
||||
}
|
||||
|
||||
icon() {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
globalMonitorsWidget(
|
||||
SimpleWrapper<double> width, Color activeTextColor, Color activeBgColor) {
|
||||
getMonitors() {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
final rect = ffi.ffiModel.globalDisplaysRect();
|
||||
if (rect == null) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
|
||||
final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
|
||||
final startX = startY;
|
||||
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < pi.displays.length; i++) {
|
||||
final d = pi.displays[i];
|
||||
final fontSize = (d.width * scale < d.height * scale
|
||||
? d.width * scale
|
||||
: d.height * scale) *
|
||||
0.65;
|
||||
children.add(Positioned(
|
||||
left: (d.x - rect.left) * scale + startX,
|
||||
top: (d.y - rect.top) * scale + startY,
|
||||
width: d.width * scale,
|
||||
height: d.height * scale,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey,
|
||||
width: 1.0,
|
||||
),
|
||||
color: display.value == i ? activeBgColor : Colors.white,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${i + 1}',
|
||||
style: TextStyle(
|
||||
color: display.value == i
|
||||
? activeTextColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)),
|
||||
),
|
||||
));
|
||||
}
|
||||
width.value = rect.width * scale + startX * 2;
|
||||
return SizedBox(
|
||||
width: width.value,
|
||||
height: rect.height * scale + startY * 2,
|
||||
child: Stack(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/screen.svg",
|
||||
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
return Text(
|
||||
'${display.value == kAllDisplayValue ? 'A' : '${display.value + 1}'}/${pi.displays.length}',
|
||||
style: const TextStyle(
|
||||
color: _ToolbarTheme.blueColor,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: _ToolbarTheme.buttonSize),
|
||||
getMonitors(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -1150,8 +1223,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
|
||||
final visible =
|
||||
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
|
||||
final visible = ffiModel.keyboard &&
|
||||
(isVirtualDisplay || resolutions.length > 1) &&
|
||||
pi.currentDisplay != kAllDisplayValue;
|
||||
if (!visible) return Offstage();
|
||||
final showOriginalBtn =
|
||||
ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
|
||||
@ -1761,6 +1835,7 @@ class _IconMenuButton extends StatefulWidget {
|
||||
final double? hMargin;
|
||||
final double? vMargin;
|
||||
final bool topLevel;
|
||||
final double? width;
|
||||
const _IconMenuButton({
|
||||
Key? key,
|
||||
this.assetName,
|
||||
@ -1772,6 +1847,7 @@ class _IconMenuButton extends StatefulWidget {
|
||||
this.hMargin,
|
||||
this.vMargin,
|
||||
this.topLevel = true,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -1792,7 +1868,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
|
||||
height: _ToolbarTheme.buttonSize,
|
||||
);
|
||||
var button = SizedBox(
|
||||
width: _ToolbarTheme.buttonSize,
|
||||
width: widget.width ?? _ToolbarTheme.buttonSize,
|
||||
height: _ToolbarTheme.buttonSize,
|
||||
child: MenuItemButton(
|
||||
style: ButtonStyle(
|
||||
@ -1839,18 +1915,20 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final List<Widget> menuChildren;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI ffi;
|
||||
final double? width;
|
||||
|
||||
_IconSubmenuButton(
|
||||
{Key? key,
|
||||
this.svg,
|
||||
this.icon,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildren,
|
||||
required this.ffi,
|
||||
this.menuStyle})
|
||||
: super(key: key);
|
||||
_IconSubmenuButton({
|
||||
Key? key,
|
||||
this.svg,
|
||||
this.icon,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildren,
|
||||
required this.ffi,
|
||||
this.menuStyle,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_IconSubmenuButton> createState() => _IconSubmenuButtonState();
|
||||
@ -1870,7 +1948,7 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
height: _ToolbarTheme.buttonSize,
|
||||
);
|
||||
final button = SizedBox(
|
||||
width: _ToolbarTheme.buttonSize,
|
||||
width: widget.width ?? _ToolbarTheme.buttonSize,
|
||||
height: _ToolbarTheme.buttonSize,
|
||||
child: SubmenuButton(
|
||||
menuStyle: widget.menuStyle ?? _ToolbarTheme.defaultMenuStyle,
|
||||
@ -2016,6 +2094,7 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
final RxDouble fractionX;
|
||||
final RxBool dragging;
|
||||
final RxBool show;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
final Function(bool) setFullscreen;
|
||||
final Function() setMinimize;
|
||||
@ -2028,6 +2107,7 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
required this.show,
|
||||
required this.setFullscreen,
|
||||
required this.setMinimize,
|
||||
required this.borderRadius,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -2164,9 +2244,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
.style
|
||||
?.backgroundColor
|
||||
?.resolve(MaterialState.values.toSet()),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(5),
|
||||
border: Border.all(
|
||||
color: _ToolbarTheme.borderColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: widget.borderRadius,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
|
@ -583,32 +583,19 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
void onWindowClose() async {
|
||||
mainWindowClose() async => await windowManager.hide();
|
||||
notMainWindowClose(WindowController controller) async {
|
||||
if (widget.tabController.length == 0) {
|
||||
debugPrint("close emtpy multiwindow, hide");
|
||||
await controller.hide();
|
||||
await rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||
} else {
|
||||
if (widget.tabController.length != 0) {
|
||||
debugPrint("close not emtpy multiwindow from taskbar");
|
||||
if (Platform.isWindows) {
|
||||
await controller.show();
|
||||
await controller.focus();
|
||||
final res = await widget.onClose?.call() ?? true;
|
||||
if (res) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
// onWindowClose will be called again to hide
|
||||
await WindowController.fromWindowId(kWindowId!).close();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ubuntu22.04 windowOnTop not work from taskbar
|
||||
widget.tabController.clear();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
// onWindowClose will be called again to hide
|
||||
await WindowController.fromWindowId(kWindowId!).close();
|
||||
});
|
||||
if (!res) return;
|
||||
}
|
||||
widget.tabController.clear();
|
||||
}
|
||||
await controller.hide();
|
||||
await rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||
}
|
||||
|
||||
macOSWindowClose(
|
||||
|
@ -156,6 +156,7 @@ void runMobileApp() async {
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
await initUniLinks();
|
||||
}
|
||||
|
||||
void runMultiWindow(
|
||||
|
@ -54,10 +54,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
@ -312,6 +314,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uniLinksSubscription?.cancel();
|
||||
_idController.dispose();
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
|
@ -5,7 +5,7 @@ import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
@ -73,7 +73,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id);
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -81,7 +81,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
model.close().whenComplete(() {
|
||||
gFFI.close();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
});
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@ -60,7 +60,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
@ -88,7 +88,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
await Wakelock.disable();
|
||||
await WakelockPlus.disable();
|
||||
await keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ class ServiceNotRunningNotification extends StatelessWidget {
|
||||
serverModel.toggleService();
|
||||
}
|
||||
},
|
||||
label: Text(translate("Start Service")))
|
||||
label: Text(translate("Start service")))
|
||||
],
|
||||
));
|
||||
}
|
||||
@ -561,7 +561,7 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer File"), serverModel.fileOk,
|
||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
||||
serverModel.toggleFile),
|
||||
hasAudioPermission
|
||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||
|
@ -221,7 +221,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN Discovery')),
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
@ -270,7 +270,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable Recording Session')),
|
||||
title: Text(translate('Enable recording session')),
|
||||
initialValue: _enableRecordSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
@ -407,7 +407,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
enhancementsTiles.add(SettingsTile.switchTile(
|
||||
initialValue: _enableStartOnBoot,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text("${translate('Start on Boot')} (beta)"),
|
||||
Text("${translate('Start on boot')} (beta)"),
|
||||
Text(
|
||||
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
@ -797,6 +797,7 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Touch mode', 'touch-mode'),
|
||||
otherRow('True color (4:4:4)', 'i444'),
|
||||
],
|
||||
),
|
||||
]),
|
||||
|
@ -285,6 +285,10 @@ class ChatModel with ChangeNotifier {
|
||||
await toggleCMSidePage();
|
||||
}
|
||||
|
||||
toggleCMFilePage() async {
|
||||
await toggleCMSidePage();
|
||||
}
|
||||
|
||||
var _togglingCMSidePage = false; // protect order for await
|
||||
toggleCMSidePage() async {
|
||||
if (_togglingCMSidePage) return false;
|
||||
@ -296,6 +300,13 @@ class ChatModel with ChangeNotifier {
|
||||
await windowManager.setSizeAlignment(
|
||||
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
|
||||
} else {
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
final client = parent.target?.serverModel.clients.firstWhereOrNull(
|
||||
(client) => client.id.toString() == currentSelectedTab.key);
|
||||
if (client != null) {
|
||||
client.unreadChatMessageCount.value = 0;
|
||||
}
|
||||
requestChatInputFocus();
|
||||
await windowManager.show();
|
||||
await windowManager.setSizeAlignment(
|
||||
|
@ -10,8 +10,8 @@ import 'file_model.dart';
|
||||
|
||||
class CmFileModel {
|
||||
final WeakReference<FFI> parent;
|
||||
final currentJobTable = RxList<JobProgress>();
|
||||
final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]);
|
||||
final currentJobTable = RxList<CmFileLog>();
|
||||
final _jobTables = HashMap<int, RxList<CmFileLog>>.fromEntries([]);
|
||||
Stopwatch stopwatch = Stopwatch();
|
||||
int _lastElapsed = 0;
|
||||
|
||||
@ -19,14 +19,24 @@ class CmFileModel {
|
||||
|
||||
void updateCurrentClientId(int id) {
|
||||
if (_jobTables[id] == null) {
|
||||
_jobTables[id] = RxList<JobProgress>();
|
||||
_jobTables[id] = RxList<CmFileLog>();
|
||||
}
|
||||
Future.delayed(Duration.zero, () {
|
||||
currentJobTable.value = _jobTables[id]!;
|
||||
});
|
||||
}
|
||||
|
||||
onFileTransferLog(dynamic log) {
|
||||
onFileTransferLog(Map<String, dynamic> evt) {
|
||||
if (evt['transfer'] != null) {
|
||||
_onFileTransfer(evt['transfer']);
|
||||
} else if (evt['remove'] != null) {
|
||||
_onFileRemove(evt['remove']);
|
||||
} else if (evt['create_dir'] != null) {
|
||||
_onDirCreate(evt['create_dir']);
|
||||
}
|
||||
}
|
||||
|
||||
_onFileTransfer(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
if (!stopwatch.isRunning) stopwatch.start();
|
||||
@ -56,9 +66,9 @@ class CmFileModel {
|
||||
debugPrint("jobTable should not be null");
|
||||
return;
|
||||
}
|
||||
JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
|
||||
CmFileLog? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
|
||||
if (job == null) {
|
||||
job = JobProgress();
|
||||
job = CmFileLog();
|
||||
jobTable.add(job);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
@ -68,14 +78,14 @@ class CmFileModel {
|
||||
}
|
||||
}
|
||||
job.id = data.id;
|
||||
job.isRemoteToLocal = data.isRemote;
|
||||
job.action =
|
||||
data.isRemote ? CmFileAction.remoteToLocal : CmFileAction.localToRemote;
|
||||
job.fileName = data.path;
|
||||
job.totalSize = data.totalSize;
|
||||
job.finishedSize = data.finishedSize;
|
||||
if (job.finishedSize > data.totalSize) {
|
||||
job.finishedSize = data.totalSize;
|
||||
}
|
||||
job.isRemoteToLocal = data.isRemote;
|
||||
|
||||
if (job.finishedSize > 0) {
|
||||
if (job.finishedSize < job.totalSize) {
|
||||
@ -99,6 +109,119 @@ class CmFileModel {
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
|
||||
_onFileRemove(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileActionLog data = FileActionLog.fromJson(d);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
return;
|
||||
}
|
||||
int removeUnreadCount = 0;
|
||||
if (data.dir) {
|
||||
bool isChild(String parent, String child) {
|
||||
if (child.startsWith(parent) && child.length > parent.length) {
|
||||
final suffix = child.substring(parent.length);
|
||||
return suffix.startsWith('/') || suffix.startsWith('\\');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeUnreadCount = jobTable
|
||||
.where((e) =>
|
||||
e.action == CmFileAction.remove &&
|
||||
isChild(data.path, e.fileName))
|
||||
.length;
|
||||
jobTable.removeWhere((e) =>
|
||||
e.action == CmFileAction.remove && isChild(data.path, e.fileName));
|
||||
}
|
||||
jobTable.add(CmFileLog()
|
||||
..id = data.id
|
||||
..fileName = data.path
|
||||
..action = CmFileAction.remove
|
||||
..state = JobState.done);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
// Wrong number if unreadCount changes during deletion, which rarely happens
|
||||
RxInt? rx = client?.unreadChatMessageCount;
|
||||
if (rx != null) {
|
||||
if (rx.value >= removeUnreadCount) {
|
||||
rx.value -= removeUnreadCount;
|
||||
}
|
||||
rx.value += 1;
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
_onDirCreate(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileActionLog data = FileActionLog.fromJson(d);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
return;
|
||||
}
|
||||
jobTable.add(CmFileLog()
|
||||
..id = data.id
|
||||
..fileName = data.path
|
||||
..action = CmFileAction.createDir
|
||||
..state = JobState.done);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CmFileAction {
|
||||
none,
|
||||
remoteToLocal,
|
||||
localToRemote,
|
||||
remove,
|
||||
createDir,
|
||||
}
|
||||
|
||||
class CmFileLog {
|
||||
JobState state = JobState.none;
|
||||
var id = 0;
|
||||
var speed = 0.0;
|
||||
var finishedSize = 0;
|
||||
var totalSize = 0;
|
||||
CmFileAction action = CmFileAction.none;
|
||||
var fileName = "";
|
||||
var err = "";
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
String display() {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
return state.display();
|
||||
}
|
||||
|
||||
bool isTransfer() {
|
||||
return action == CmFileAction.remoteToLocal ||
|
||||
action == CmFileAction.localToRemote;
|
||||
}
|
||||
}
|
||||
|
||||
class TransferJobSerdeData {
|
||||
@ -140,3 +263,25 @@ class TransferJobSerdeData {
|
||||
error: d['error'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class FileActionLog {
|
||||
int id = 0;
|
||||
int connId = 0;
|
||||
String path = '';
|
||||
bool dir = false;
|
||||
|
||||
FileActionLog({
|
||||
required this.connId,
|
||||
required this.id,
|
||||
required this.path,
|
||||
required this.dir,
|
||||
});
|
||||
|
||||
FileActionLog.fromJson(dynamic d)
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
id: d['id'] ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
dir: d['dir'] ?? false,
|
||||
);
|
||||
}
|
||||
|
@ -1006,7 +1006,7 @@ extension JobStateDisplay on JobState {
|
||||
case JobState.none:
|
||||
return translate("Waiting");
|
||||
case JobState.inProgress:
|
||||
return translate("Transfer File");
|
||||
return translate("Transfer file");
|
||||
case JobState.done:
|
||||
return translate("Finished");
|
||||
case JobState.error:
|
||||
|
@ -138,8 +138,9 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId = parent.target!.sessionId;
|
||||
}
|
||||
|
||||
Rect? displaysRect() {
|
||||
final displays = _pi.getCurDisplays();
|
||||
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays);
|
||||
Rect? displaysRect() => _getDisplaysRect(_pi.getCurDisplays());
|
||||
Rect? _getDisplaysRect(List<Display> displays) {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
@ -352,7 +353,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
} else if (name == "cm_file_transfer_log") {
|
||||
if (isDesktop) {
|
||||
gFFI.cmFileModel.onFileTransferLog(evt['log']);
|
||||
gFFI.cmFileModel.onFileTransferLog(evt);
|
||||
}
|
||||
} else {
|
||||
debugPrint('Unknown event name: $name');
|
||||
@ -430,15 +431,19 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
handleSwitchDisplay(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
final curDisplay = int.parse(evt['display']);
|
||||
final display = int.parse(evt['display']);
|
||||
|
||||
if (_pi.currentDisplay != kAllDisplayValue) {
|
||||
if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) {
|
||||
if (curDisplay != _pi.currentDisplay) {
|
||||
if (display != _pi.currentDisplay) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_pi.currentDisplay = curDisplay;
|
||||
if (!_pi.isSupportMultiUiSession) {
|
||||
_pi.currentDisplay = display;
|
||||
}
|
||||
// If `isSupportMultiUiSession` is true, the switch display message should not be used to update current display.
|
||||
// It is only used to update the display info.
|
||||
}
|
||||
|
||||
var newDisplay = Display();
|
||||
@ -451,16 +456,24 @@ class FfiModel with ChangeNotifier {
|
||||
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalHeight =
|
||||
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
|
||||
_pi.displays[curDisplay] = newDisplay;
|
||||
_pi.displays[display] = newDisplay;
|
||||
|
||||
updateCurDisplay(sessionId);
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = curDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
||||
updateCurDisplay(sessionId);
|
||||
}
|
||||
|
||||
if (!_pi.isSupportMultiUiSession) {
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = display;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
parent.target?.recordingModel.onSwitchDisplay();
|
||||
handleResolutions(peerId, evt['resolutions']);
|
||||
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
||||
handleResolutions(peerId, evt['resolutions']);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -627,7 +640,9 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||
cachedPeerData.peerInfo = evt;
|
||||
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
||||
// Because this function is asynchronous, there's an "await" in this function.
|
||||
cachedPeerData.peerInfo = {...evt};
|
||||
|
||||
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
||||
bind.mainLoadRecentPeers();
|
||||
@ -666,11 +681,12 @@ class FfiModel with ChangeNotifier {
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else if (connType == ConnType.defaultConn) {
|
||||
_pi.displays = [];
|
||||
List<Display> newDisplays = [];
|
||||
List<dynamic> displays = json.decode(evt['displays']);
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
_pi.displays.add(evtToDisplay(displays[i]));
|
||||
newDisplays.add(evtToDisplay(displays[i]));
|
||||
}
|
||||
_pi.displays.value = newDisplays;
|
||||
_pi.displaysCount.value = _pi.displays.length;
|
||||
if (_pi.currentDisplay < _pi.displays.length) {
|
||||
// now replaced to _updateCurDisplay
|
||||
@ -860,7 +876,7 @@ class FfiModel with ChangeNotifier {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
newDisplays.add(evtToDisplay(displays[i]));
|
||||
}
|
||||
_pi.displays = newDisplays;
|
||||
_pi.displays.value = newDisplays;
|
||||
_pi.displaysCount.value = _pi.displays.length;
|
||||
|
||||
if (_pi.currentDisplay == kAllDisplayValue) {
|
||||
@ -908,11 +924,11 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
||||
} else {
|
||||
try {
|
||||
final updateJson = json.decode(updateData);
|
||||
final updateJson = json.decode(updateData) as Map<String, dynamic>;
|
||||
for (final key in updateJson.keys) {
|
||||
_pi.platformAdditions[key] = updateJson[key];
|
||||
}
|
||||
if (!updateJson.contains(kPlatformAdditionsVirtualDisplays)) {
|
||||
if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) {
|
||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -1845,6 +1861,7 @@ class QualityMonitorData {
|
||||
String? delay;
|
||||
String? targetBitrate;
|
||||
String? codecFormat;
|
||||
String? chroma;
|
||||
}
|
||||
|
||||
class QualityMonitorModel with ChangeNotifier {
|
||||
@ -1898,6 +1915,9 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
if ((evt['codec_format'] as String).isNotEmpty) {
|
||||
_data.codecFormat = evt['codec_format'];
|
||||
}
|
||||
if ((evt['chroma'] as String).isNotEmpty) {
|
||||
_data.chroma = evt['chroma'];
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
//
|
||||
@ -2321,7 +2341,7 @@ class PeerInfo with ChangeNotifier {
|
||||
bool isSupportMultiUiSession = false;
|
||||
int currentDisplay = 0;
|
||||
int primaryDisplay = kInvalidDisplayIndex;
|
||||
List<Display> displays = [];
|
||||
RxList<Display> displays = <Display>[].obs;
|
||||
Features features = Features();
|
||||
List<Resolution> resolutions = [];
|
||||
Map<String, dynamic> platformAdditions = {};
|
||||
|
@ -15,7 +15,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import '../common.dart';
|
||||
import '../generated_bridge.dart';
|
||||
|
||||
class RgbaFrame extends Struct {
|
||||
final class RgbaFrame extends Struct {
|
||||
@Uint32()
|
||||
external int len;
|
||||
external Pointer<Uint8> data;
|
||||
|
@ -22,10 +22,10 @@ class PeerTabModel with ChangeNotifier {
|
||||
int get currentTab => _currentTab;
|
||||
int _currentTab = 0; // index in tabNames
|
||||
List<String> tabNames = [
|
||||
'Recent Sessions',
|
||||
'Recent sessions',
|
||||
'Favorites',
|
||||
'Discovered',
|
||||
'Address Book',
|
||||
'Address book',
|
||||
'Group',
|
||||
];
|
||||
final List<IconData> icons = [
|
||||
|
@ -8,7 +8,7 @@ import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../common.dart';
|
||||
@ -380,7 +380,7 @@ class ServerModel with ChangeNotifier {
|
||||
await bind.mainStartService();
|
||||
updateClientState();
|
||||
if (Platform.isAndroid) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,7 +393,7 @@ class ServerModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
if (!Platform.isLinux) {
|
||||
// current linux is not supported
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -690,6 +690,7 @@ class Client {
|
||||
bool file = false;
|
||||
bool restart = false;
|
||||
bool recording = false;
|
||||
bool blockInput = false;
|
||||
bool disconnected = false;
|
||||
bool fromSwitch = false;
|
||||
bool inVoiceCall = false;
|
||||
@ -713,6 +714,7 @@ class Client {
|
||||
file = json['file'];
|
||||
restart = json['restart'];
|
||||
recording = json['recording'];
|
||||
blockInput = json['block_input'];
|
||||
disconnected = json['disconnected'];
|
||||
fromSwitch = json['from_switch'];
|
||||
inVoiceCall = json['in_voice_call'];
|
||||
@ -733,6 +735,7 @@ class Client {
|
||||
data['file'] = file;
|
||||
data['restart'] = restart;
|
||||
data['recording'] = recording;
|
||||
data['block_input'] = blockInput;
|
||||
data['disconnected'] = disconnected;
|
||||
data['from_switch'] = fromSwitch;
|
||||
return data;
|
||||
|
@ -1,4 +1,4 @@
|
||||
platform :osx, '10.12'
|
||||
platform :osx, '10.14'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
@ -3,7 +3,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- desktop_multi_window (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus_macos (0.0.1):
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_custom_cursor (0.0.1):
|
||||
- FlutterMacOS
|
||||
@ -27,7 +29,10 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_macos (0.0.1):
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.2.0):
|
||||
- FlutterMacOS
|
||||
@ -37,7 +42,8 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||
- desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`)
|
||||
- device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_custom_cursor (from `Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
@ -47,7 +53,8 @@ DEPENDENCIES:
|
||||
- texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`)
|
||||
- uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
|
||||
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||
|
||||
@ -60,8 +67,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
desktop_multi_window:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos
|
||||
device_info_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_custom_cursor:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos
|
||||
FlutterMacOS:
|
||||
@ -80,8 +89,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
|
||||
video_player_avfoundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
window_size:
|
||||
@ -90,21 +101,23 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
|
||||
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
|
||||
PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
|
@ -210,7 +210,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -5,7 +5,7 @@ import desktop_multi_window
|
||||
// import bitsdojo_window_macos
|
||||
|
||||
import desktop_drop
|
||||
import device_info_plus_macos
|
||||
import device_info_plus
|
||||
import flutter_custom_cursor
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
@ -14,7 +14,7 @@ import sqflite
|
||||
// import tray_manager
|
||||
import uni_links_desktop
|
||||
import url_launcher_macos
|
||||
import wakelock_macos
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
import window_size
|
||||
import texture_rgba_renderer
|
||||
@ -35,17 +35,18 @@ class MainFlutterWindow: NSWindow {
|
||||
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
||||
// Register the plugin which you want access from other isolate.
|
||||
// DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin"))
|
||||
// Note: copy below from above RegisterGeneratedPlugins
|
||||
self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin"))
|
||||
DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin"))
|
||||
// TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin"))
|
||||
UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
|
||||
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
|
||||
}
|
||||
|
@ -1,135 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 53;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
ADDEDBA66A6E2 /* Required for static linking */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADDEDBA66A6E1 /* libresolv.tbd */,
|
||||
);
|
||||
name = "Required for static linking";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CA603C4309E122869D176AE5 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CA603C4309E198AF0B5890DB /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADDEDBA66A6E2 /* Required for static linking */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CA603C4309E1D65BC3C892A8 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CA603C4309E122869D176AE5 /* Products */,
|
||||
CA603C4309E198AF0B5890DB /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CA603C4309E1E04653AD465F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
};
|
||||
buildConfigurationList = CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */;
|
||||
compatibilityVersion = "Xcode 11.4";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CA603C4309E1D65BC3C892A8;
|
||||
productRefGroup = CA603C4309E122869D176AE5 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CA608F3F78EE228BE02872F8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
|
||||
CARGO_XCODE_BUILD_MODE = debug;
|
||||
CARGO_XCODE_FEATURES = "";
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
|
||||
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = RustDesk;
|
||||
SDKROOT = macosx;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CA608F3F78EE3CC16B37690B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
|
||||
CARGO_XCODE_BUILD_MODE = release;
|
||||
CARGO_XCODE_FEATURES = "";
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
|
||||
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
|
||||
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
|
||||
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
|
||||
PRODUCT_NAME = RustDesk;
|
||||
SDKROOT = macosx;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-sectcreate",
|
||||
__CGPreLoginApp,
|
||||
__cgpreloginapp,
|
||||
/dev/null,
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CA608F3F78EE3CC16B37690B /* Release */,
|
||||
CA608F3F78EE228BE02872F8 /* Debug */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CA603C4309E1E04653AD465F /* Project object */;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.2.4+39
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0"
|
||||
sdk: '^3.1.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@ -27,31 +27,29 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
ffi: ^2.0.1
|
||||
path_provider: ^2.0.12
|
||||
external_path: ^1.0.1
|
||||
provider: ^6.0.3
|
||||
ffi: ^2.1.0
|
||||
path_provider: ^2.1.1
|
||||
external_path: ^1.0.3
|
||||
provider: ^6.0.5
|
||||
tuple: ^2.0.0
|
||||
wakelock: ^0.6.2
|
||||
# Keep this version for the compatibility of some old systems like win7.
|
||||
device_info_plus: ^4.1.2
|
||||
wakelock_plus: ^1.1.3
|
||||
#firebase_analytics: ^9.1.5
|
||||
package_info_plus: ^3.1.2
|
||||
url_launcher: ^6.0.9
|
||||
package_info_plus: ^4.2.0
|
||||
url_launcher: ^6.2.1
|
||||
toggle_switch: ^2.1.0
|
||||
dash_chat_2:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/Dash-Chat-2
|
||||
draggable_float_widget: ^0.0.2
|
||||
draggable_float_widget: ^0.1.0
|
||||
settings_ui: ^2.0.2
|
||||
flutter_breadcrumb: ^1.0.1
|
||||
http: ^0.13.4
|
||||
http: ^1.1.0
|
||||
qr_code_scanner: ^1.0.0
|
||||
zxing2: ^0.2.0
|
||||
image_picker: ^0.8.5
|
||||
image: ^4.0.17
|
||||
back_button_interceptor: ^6.0.1
|
||||
flutter_rust_bridge: "<1.76.0"
|
||||
flutter_rust_bridge: "1.80.1"
|
||||
window_manager:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/window_manager
|
||||
@ -68,10 +66,7 @@ dependencies:
|
||||
get: ^4.6.5
|
||||
visibility_detector: ^0.4.0+2
|
||||
contextmenu: ^3.0.0
|
||||
desktop_drop:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter-plugins
|
||||
path: ./packages/desktop_drop
|
||||
desktop_drop: ^0.4.4
|
||||
scroll_pos: ^0.4.0
|
||||
debounce_throttle: ^2.0.0
|
||||
file_picker: ^5.1.0
|
||||
@ -105,6 +100,7 @@ dependencies:
|
||||
url: https://github.com/21pages/dynamic_layouts.git
|
||||
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
|
||||
pull_down_button: ^0.9.3
|
||||
device_info_plus: ^9.1.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
@ -113,7 +109,7 @@ dev_dependencies:
|
||||
build_runner: ^2.4.6
|
||||
freezed: ^2.4.2
|
||||
flutter_lints: ^2.0.2
|
||||
ffigen: ^7.2.4
|
||||
ffigen: ^8.0.2
|
||||
|
||||
# rerun: flutter pub run flutter_launcher_icons
|
||||
flutter_icons:
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cargo install flutter_rust_bridge_codegen --version 1.75.3 --features uuid
|
||||
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
|
||||
flutter pub get
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
||||
# call `flutter clean` if cargo build fails
|
||||
|
@ -2,16 +2,48 @@
|
||||
name = "clipboard"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
build = "build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
unix-file-copy-paste = [
|
||||
"dep:x11rb",
|
||||
"dep:x11-clipboard",
|
||||
"dep:rand",
|
||||
"dep:fuser",
|
||||
"dep:libc",
|
||||
"dep:dashmap",
|
||||
"dep:percent-encoding",
|
||||
"dep:utf16string",
|
||||
"dep:once_cell",
|
||||
"dep:cacao"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
lazy_static = "1.4"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
parking_lot = {version = "0.12"}
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
rand = {version = "0.8", optional = true}
|
||||
fuser = {version = "0.13", optional = true}
|
||||
libc = {version = "0.2", optional = true}
|
||||
dashmap = {version ="5.5", optional = true}
|
||||
utf16string = {version = "0.2", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
percent-encoding = {version ="2.3", optional = true}
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}
|
||||
|
@ -3,8 +3,74 @@
|
||||
Copy files and text through network.
|
||||
Main lowlevel logic from [FreeRDP](https://github.com/FreeRDP/FreeRDP).
|
||||
|
||||
To enjoy file copy and paste feature on Linux/OSX,
|
||||
please build with `unix-file-copy-paste` feature.
|
||||
|
||||
TODO: Move this lib to a separate project.
|
||||
|
||||
## How it works
|
||||
|
||||
Terminalogies:
|
||||
|
||||
- cliprdr: this module
|
||||
- local: the endpoint which initiates a file copy events
|
||||
- remote: the endpoint which paste the file copied from `local`
|
||||
|
||||
The main algorithm of copying and pasting files is from
|
||||
[Remote Desktop Protocol: Clipboard Virtual Channel Extension](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPECLIP/%5bMS-RDPECLIP%5d.pdf),
|
||||
and could be concluded as:
|
||||
|
||||
0. local and remote notify each other that it's ready.
|
||||
1. local subscribes/listening to the system's clipboard for file copy
|
||||
2. local once got file copy event, notice the remote
|
||||
3. remote confirms receive and try pulls the file list
|
||||
4. local updates its file-list, the remote flushes pulled file list to the clipboard
|
||||
5. remote OS or desktop manager initiates a paste, making other programs reading
|
||||
clipboard files. Convert those reading requests to RPCs
|
||||
|
||||
- on Windows, all file reading will go through the stream file API
|
||||
- on Linux/OSX, FUSE is used for converting reading requests to RPCs
|
||||
- in case of local clipboard been transferred back
|
||||
and leading to a dead loop,
|
||||
all file copy event pointing at the FUSE directory will be ignored
|
||||
|
||||
6. finishing pasting all files one by one.
|
||||
|
||||
In a perspective of network data transferring:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant l as Local
|
||||
participant r as Remote
|
||||
note over l, r: Initialize
|
||||
l ->> r: Monitor Ready
|
||||
r ->> l: Monitor Ready
|
||||
loop Get clipboard update
|
||||
l ->> r: Format List (I got update)
|
||||
r ->> l: Format List Response (notified)
|
||||
r ->> l: Format Data Request (requests file list)
|
||||
activate l
|
||||
note left of l: Retrive file list from system clipboard
|
||||
l ->> r: Format Data Response (containing file list)
|
||||
deactivate l
|
||||
note over r: Update system clipboard with received file list
|
||||
end
|
||||
loop Some application requests copied files
|
||||
note right of r: application reads file from x to x+y
|
||||
note over r: the file is the a-th file on list
|
||||
r ->> l: File Contents Request (read file a offset x size y)
|
||||
activate l
|
||||
note left of l: Find a-th file on list, read from x to x+y
|
||||
l ->> r: File Contents Response (contents of file a offset x size y)
|
||||
deactivate l
|
||||
end
|
||||
```
|
||||
|
||||
Note: In actual implementation, both sides could play send clipboard update
|
||||
and request file contents.
|
||||
There is no such limitation that only local can update clipboard
|
||||
and copy files to remote.
|
||||
|
||||
## impl
|
||||
|
||||
### windows
|
||||
@ -14,3 +80,82 @@ TODO: Move this lib to a separate project.
|
||||
![A1->B1](./docs/assets/win_A_B.png)
|
||||
|
||||
![B1->A1](./docs/assets/win_B_A.png)
|
||||
|
||||
The protocol was originally designed as an extension of the Windows RDP,
|
||||
so the specific message packages fits windows well.
|
||||
|
||||
When starting cliprdr, a thread is spawn to create a invisible window
|
||||
and to subscribe to OLE clipboard events.
|
||||
The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was
|
||||
set to handle a variaty of events.
|
||||
|
||||
Detailed implementation is shown in pictures above.
|
||||
|
||||
### Linux/OSX
|
||||
|
||||
The Cliprdr Server implementation has mainly 3 parts:
|
||||
|
||||
- Clipboard Client
|
||||
- Local File list
|
||||
- FUSE server
|
||||
|
||||
#### Clipboard Client
|
||||
|
||||
The clipboard client has a thread polling for file urls on clipboard.
|
||||
|
||||
If the client found any updates of file urls,
|
||||
after filtering out those pointing to our FUSE directory or duplicated,
|
||||
send format list directly to remote.
|
||||
|
||||
The cliprdr server also uses clipboard client for setting clipboard,
|
||||
or retrive paths from system.
|
||||
|
||||
#### Local File List
|
||||
|
||||
The local file list is a temperary list of file metadata.
|
||||
When receiving file contents PDU from peer, the server picks
|
||||
out the file requested and open it for reading if necessary.
|
||||
|
||||
Also when receiving Format Data Request PDU from remote asking for file list,
|
||||
the local file list should be rebuilt from file list retrieved from Clipboard Client.
|
||||
|
||||
Some caching and preloading could done on it since applications are likely to read
|
||||
on the list sequentially.
|
||||
|
||||
#### FUSE server
|
||||
|
||||
The FUSE server could convert POSIX file reading request to File Contents
|
||||
Request/Response RPCs.
|
||||
|
||||
When received file list from remote,
|
||||
the FUSE server will figure out the file system tree and rearrange its content.
|
||||
|
||||
#### Groceries
|
||||
|
||||
- The protocol was originally implemented for windows,
|
||||
so paths in PDU will all be converted to DOS formats in UTF-16 LE encoding,
|
||||
and datetimes will be converted to LDAP timestamp instead of
|
||||
unix timestamp
|
||||
|
||||
```text
|
||||
UNIX
|
||||
/usr/bin/rustdesk
|
||||
->
|
||||
DOS
|
||||
\usr\bin\rustdesk
|
||||
```
|
||||
|
||||
- To better fit for preserving permissions on unix-like platforms,
|
||||
a reserved area of FileDescriptor PDU
|
||||
|
||||
- you may notice
|
||||
the mountpoint is still occupied after the application quits.
|
||||
That's because the FUSE server was not mounted with `AUTO_UNMOUNT`.
|
||||
- It's hard to implement gressful shutdown for a multi-processed program
|
||||
- `AUTO_UNMOUNT` was not enabled by default and requires enable
|
||||
`user_allow_other` in configure. Letting users edit such global
|
||||
configuration to use this feature might not be a good idea.
|
||||
- use [`umount()`](https://man7.org/linux/man-pages/man2/umount.2.html)
|
||||
syscall to unmount will also require that option.
|
||||
- we currently directly call [`umount`](https://man7.org/linux/man-pages/man8/umount.8.html)
|
||||
program to unmount dangling FUSE server. It worked perfectly for now.
|
||||
|
@ -1,43 +1,35 @@
|
||||
use cc;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_c_impl() {
|
||||
let mut build = cc::Build::new();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
build.file("src/windows/wf_cliprdr.c");
|
||||
#[cfg(target_os = "linux")]
|
||||
build.file("src/X11/xf_cliprdr.c");
|
||||
#[cfg(target_os = "macos")]
|
||||
build.file("src/OSX/Clipboard.m");
|
||||
|
||||
build.flag_if_supported("-Wno-c++0x-extensions");
|
||||
build.flag_if_supported("-Wno-return-type-c-linkage");
|
||||
build.flag_if_supported("-Wno-invalid-offsetof");
|
||||
build.flag_if_supported("-Wno-unused-parameter");
|
||||
{
|
||||
build.flag_if_supported("-Wno-c++0x-extensions");
|
||||
build.flag_if_supported("-Wno-return-type-c-linkage");
|
||||
build.flag_if_supported("-Wno-invalid-offsetof");
|
||||
build.flag_if_supported("-Wno-unused-parameter");
|
||||
|
||||
if build.get_compiler().is_like_msvc() {
|
||||
build.define("WIN32", "");
|
||||
// build.define("_AMD64_", "");
|
||||
build.flag("-Z7");
|
||||
build.flag("-GR-");
|
||||
// build.flag("-std:c++11");
|
||||
} else {
|
||||
build.flag("-fPIC");
|
||||
// build.flag("-std=c++11");
|
||||
// build.flag("-include");
|
||||
// build.flag(&confdefs_path.to_string_lossy());
|
||||
if build.get_compiler().is_like_msvc() {
|
||||
build.define("WIN32", "");
|
||||
// build.define("_AMD64_", "");
|
||||
build.flag("-Z7");
|
||||
build.flag("-GR-");
|
||||
// build.flag("-std:c++11");
|
||||
} else {
|
||||
build.flag("-fPIC");
|
||||
// build.flag("-std=c++11");
|
||||
// build.flag("-include");
|
||||
// build.flag(&confdefs_path.to_string_lossy());
|
||||
}
|
||||
|
||||
build.compile("mycliprdr");
|
||||
}
|
||||
|
||||
build.compile("mycliprdr");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c");
|
||||
#[cfg(target_os = "linux")]
|
||||
println!("cargo:rerun-if-changed=src/X11/xf_cliprdr.c");
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rerun-if-changed=src/OSX/Clipboard.m");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
build_c_impl();
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
#include "../cliprdr.h"
|
||||
|
||||
void mac_cliprdr_init(CliprdrClientContext *cliprdr)
|
||||
{
|
||||
(void)cliprdr;
|
||||
}
|
||||
|
||||
void mac_cliprdr_uninit(CliprdrClientContext *cliprdr)
|
||||
{
|
||||
(void)cliprdr;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#include "../cliprdr.h"
|
||||
|
||||
void xf_cliprdr_init(CliprdrClientContext* cliprdr)
|
||||
{
|
||||
(void)cliprdr;
|
||||
}
|
||||
|
||||
void xf_cliprdr_uninit( CliprdrClientContext* cliprdr)
|
||||
{
|
||||
(void)cliprdr;
|
||||
}
|
@ -1,573 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(deref_nullptr)]
|
||||
|
||||
use std::{boxed::Box, result::Result};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type size_t = ::std::os::raw::c_ulonglong;
|
||||
pub type __vcrt_bool = bool;
|
||||
pub type wchar_t = ::std::os::raw::c_ushort;
|
||||
|
||||
pub type POINTER_64_INT = ::std::os::raw::c_ulonglong;
|
||||
pub type INT8 = ::std::os::raw::c_schar;
|
||||
pub type PINT8 = *mut ::std::os::raw::c_schar;
|
||||
pub type INT16 = ::std::os::raw::c_short;
|
||||
pub type PINT16 = *mut ::std::os::raw::c_short;
|
||||
pub type INT32 = ::std::os::raw::c_int;
|
||||
pub type PINT32 = *mut ::std::os::raw::c_int;
|
||||
pub type INT64 = ::std::os::raw::c_longlong;
|
||||
pub type PINT64 = *mut ::std::os::raw::c_longlong;
|
||||
pub type UINT8 = ::std::os::raw::c_uchar;
|
||||
pub type PUINT8 = *mut ::std::os::raw::c_uchar;
|
||||
pub type UINT16 = ::std::os::raw::c_ushort;
|
||||
pub type PUINT16 = *mut ::std::os::raw::c_ushort;
|
||||
pub type UINT32 = ::std::os::raw::c_uint;
|
||||
pub type PUINT32 = *mut ::std::os::raw::c_uint;
|
||||
pub type UINT64 = ::std::os::raw::c_ulonglong;
|
||||
pub type PUINT64 = *mut ::std::os::raw::c_ulonglong;
|
||||
pub type LONG32 = ::std::os::raw::c_int;
|
||||
pub type PLONG32 = *mut ::std::os::raw::c_int;
|
||||
pub type ULONG32 = ::std::os::raw::c_uint;
|
||||
pub type PULONG32 = *mut ::std::os::raw::c_uint;
|
||||
pub type DWORD32 = ::std::os::raw::c_uint;
|
||||
pub type PDWORD32 = *mut ::std::os::raw::c_uint;
|
||||
pub type INT_PTR = ::std::os::raw::c_longlong;
|
||||
pub type PINT_PTR = *mut ::std::os::raw::c_longlong;
|
||||
pub type UINT_PTR = ::std::os::raw::c_ulonglong;
|
||||
pub type PUINT_PTR = *mut ::std::os::raw::c_ulonglong;
|
||||
pub type LONG_PTR = ::std::os::raw::c_longlong;
|
||||
pub type PLONG_PTR = *mut ::std::os::raw::c_longlong;
|
||||
pub type ULONG_PTR = ::std::os::raw::c_ulonglong;
|
||||
pub type PULONG_PTR = *mut ::std::os::raw::c_ulonglong;
|
||||
pub type SHANDLE_PTR = ::std::os::raw::c_longlong;
|
||||
pub type HANDLE_PTR = ::std::os::raw::c_ulonglong;
|
||||
pub type UHALF_PTR = ::std::os::raw::c_uint;
|
||||
pub type PUHALF_PTR = *mut ::std::os::raw::c_uint;
|
||||
pub type HALF_PTR = ::std::os::raw::c_int;
|
||||
pub type PHALF_PTR = *mut ::std::os::raw::c_int;
|
||||
pub type SIZE_T = ULONG_PTR;
|
||||
pub type PSIZE_T = *mut ULONG_PTR;
|
||||
pub type SSIZE_T = LONG_PTR;
|
||||
pub type PSSIZE_T = *mut LONG_PTR;
|
||||
pub type DWORD_PTR = ULONG_PTR;
|
||||
pub type PDWORD_PTR = *mut ULONG_PTR;
|
||||
pub type LONG64 = ::std::os::raw::c_longlong;
|
||||
pub type PLONG64 = *mut ::std::os::raw::c_longlong;
|
||||
pub type ULONG64 = ::std::os::raw::c_ulonglong;
|
||||
pub type PULONG64 = *mut ::std::os::raw::c_ulonglong;
|
||||
pub type DWORD64 = ::std::os::raw::c_ulonglong;
|
||||
pub type PDWORD64 = *mut ::std::os::raw::c_ulonglong;
|
||||
pub type KAFFINITY = ULONG_PTR;
|
||||
pub type PKAFFINITY = *mut KAFFINITY;
|
||||
pub type PVOID = *mut ::std::os::raw::c_void;
|
||||
pub type CHAR = ::std::os::raw::c_char;
|
||||
pub type SHORT = ::std::os::raw::c_short;
|
||||
pub type LONG = ::std::os::raw::c_long;
|
||||
pub type WCHAR = wchar_t;
|
||||
pub type PWCHAR = *mut WCHAR;
|
||||
pub type LPWCH = *mut WCHAR;
|
||||
pub type PWCH = *mut WCHAR;
|
||||
pub type LPCWCH = *const WCHAR;
|
||||
pub type PCWCH = *const WCHAR;
|
||||
pub type NWPSTR = *mut WCHAR;
|
||||
pub type LPWSTR = *mut WCHAR;
|
||||
pub type PWSTR = *mut WCHAR;
|
||||
pub type PZPWSTR = *mut PWSTR;
|
||||
pub type PCZPWSTR = *const PWSTR;
|
||||
pub type LPUWSTR = *mut WCHAR;
|
||||
pub type PUWSTR = *mut WCHAR;
|
||||
pub type LPCWSTR = *const WCHAR;
|
||||
pub type PCWSTR = *const WCHAR;
|
||||
pub type PZPCWSTR = *mut PCWSTR;
|
||||
pub type PCZPCWSTR = *const PCWSTR;
|
||||
pub type LPCUWSTR = *const WCHAR;
|
||||
pub type PCUWSTR = *const WCHAR;
|
||||
pub type PZZWSTR = *mut WCHAR;
|
||||
pub type PCZZWSTR = *const WCHAR;
|
||||
pub type PUZZWSTR = *mut WCHAR;
|
||||
pub type PCUZZWSTR = *const WCHAR;
|
||||
pub type PNZWCH = *mut WCHAR;
|
||||
pub type PCNZWCH = *const WCHAR;
|
||||
pub type PUNZWCH = *mut WCHAR;
|
||||
pub type PCUNZWCH = *const WCHAR;
|
||||
pub type PCHAR = *mut CHAR;
|
||||
pub type LPCH = *mut CHAR;
|
||||
pub type PCH = *mut CHAR;
|
||||
pub type LPCCH = *const CHAR;
|
||||
pub type PCCH = *const CHAR;
|
||||
pub type NPSTR = *mut CHAR;
|
||||
pub type LPSTR = *mut CHAR;
|
||||
pub type PSTR = *mut CHAR;
|
||||
pub type PZPSTR = *mut PSTR;
|
||||
pub type PCZPSTR = *const PSTR;
|
||||
pub type LPCSTR = *const CHAR;
|
||||
pub type PCSTR = *const CHAR;
|
||||
pub type PZPCSTR = *mut PCSTR;
|
||||
pub type PCZPCSTR = *const PCSTR;
|
||||
pub type PZZSTR = *mut CHAR;
|
||||
pub type PCZZSTR = *const CHAR;
|
||||
pub type PNZCH = *mut CHAR;
|
||||
pub type PCNZCH = *const CHAR;
|
||||
pub type TCHAR = ::std::os::raw::c_char;
|
||||
pub type PTCHAR = *mut ::std::os::raw::c_char;
|
||||
pub type TBYTE = ::std::os::raw::c_uchar;
|
||||
pub type PTBYTE = *mut ::std::os::raw::c_uchar;
|
||||
pub type LPTCH = LPCH;
|
||||
pub type PTCH = LPCH;
|
||||
pub type LPCTCH = LPCCH;
|
||||
pub type PCTCH = LPCCH;
|
||||
pub type PTSTR = LPSTR;
|
||||
pub type LPTSTR = LPSTR;
|
||||
pub type PUTSTR = LPSTR;
|
||||
pub type LPUTSTR = LPSTR;
|
||||
pub type PCTSTR = LPCSTR;
|
||||
pub type LPCTSTR = LPCSTR;
|
||||
pub type PCUTSTR = LPCSTR;
|
||||
pub type LPCUTSTR = LPCSTR;
|
||||
pub type PZZTSTR = PZZSTR;
|
||||
pub type PUZZTSTR = PZZSTR;
|
||||
pub type PCZZTSTR = PCZZSTR;
|
||||
pub type PCUZZTSTR = PCZZSTR;
|
||||
pub type PZPTSTR = PZPSTR;
|
||||
pub type PNZTCH = PNZCH;
|
||||
pub type PUNZTCH = PNZCH;
|
||||
pub type PCNZTCH = PCNZCH;
|
||||
pub type PCUNZTCH = PCNZCH;
|
||||
pub type PSHORT = *mut SHORT;
|
||||
pub type PLONG = *mut LONG;
|
||||
pub type ULONG = ::std::os::raw::c_ulong;
|
||||
pub type PULONG = *mut ULONG;
|
||||
pub type USHORT = ::std::os::raw::c_ushort;
|
||||
pub type PUSHORT = *mut USHORT;
|
||||
pub type UCHAR = ::std::os::raw::c_uchar;
|
||||
pub type PUCHAR = *mut UCHAR;
|
||||
pub type PSZ = *mut ::std::os::raw::c_char;
|
||||
pub type DWORD = ::std::os::raw::c_ulong;
|
||||
pub type BOOL = ::std::os::raw::c_int;
|
||||
pub type BYTE = ::std::os::raw::c_uchar;
|
||||
pub type WORD = ::std::os::raw::c_ushort;
|
||||
pub type FLOAT = f32;
|
||||
pub type PFLOAT = *mut FLOAT;
|
||||
pub type PBOOL = *mut BOOL;
|
||||
pub type LPBOOL = *mut BOOL;
|
||||
pub type PBYTE = *mut BYTE;
|
||||
pub type LPBYTE = *mut BYTE;
|
||||
pub type PINT = *mut ::std::os::raw::c_int;
|
||||
pub type LPINT = *mut ::std::os::raw::c_int;
|
||||
pub type PWORD = *mut WORD;
|
||||
pub type LPWORD = *mut WORD;
|
||||
pub type LPLONG = *mut ::std::os::raw::c_long;
|
||||
pub type PDWORD = *mut DWORD;
|
||||
pub type LPDWORD = *mut DWORD;
|
||||
pub type LPVOID = *mut ::std::os::raw::c_void;
|
||||
pub type LPCVOID = *const ::std::os::raw::c_void;
|
||||
pub type INT = ::std::os::raw::c_int;
|
||||
pub type UINT = ::std::os::raw::c_uint;
|
||||
pub type PUINT = *mut ::std::os::raw::c_uint;
|
||||
pub type va_list = *mut ::std::os::raw::c_char;
|
||||
|
||||
pub const TRUE: ::std::os::raw::c_int = 1;
|
||||
pub const FALSE: ::std::os::raw::c_int = 0;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_HEADER {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_HEADER = _CLIPRDR_HEADER;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_CAPABILITY_SET {
|
||||
pub capabilitySetType: UINT16,
|
||||
pub capabilitySetLength: UINT16,
|
||||
}
|
||||
pub type CLIPRDR_CAPABILITY_SET = _CLIPRDR_CAPABILITY_SET;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_GENERAL_CAPABILITY_SET {
|
||||
pub capabilitySetType: UINT16,
|
||||
pub capabilitySetLength: UINT16,
|
||||
pub version: UINT32,
|
||||
pub generalFlags: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_GENERAL_CAPABILITY_SET = _CLIPRDR_GENERAL_CAPABILITY_SET;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_CAPABILITIES {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub cCapabilitiesSets: UINT32,
|
||||
pub capabilitySets: *mut CLIPRDR_CAPABILITY_SET,
|
||||
}
|
||||
pub type CLIPRDR_CAPABILITIES = _CLIPRDR_CAPABILITIES;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_MONITOR_READY {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_MONITOR_READY = _CLIPRDR_MONITOR_READY;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_TEMP_DIRECTORY {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub szTempDir: [::std::os::raw::c_char; 520usize],
|
||||
}
|
||||
pub type CLIPRDR_TEMP_DIRECTORY = _CLIPRDR_TEMP_DIRECTORY;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FORMAT {
|
||||
pub formatId: UINT32,
|
||||
pub formatName: *mut ::std::os::raw::c_char,
|
||||
}
|
||||
pub type CLIPRDR_FORMAT = _CLIPRDR_FORMAT;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FORMAT_LIST {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub numFormats: UINT32,
|
||||
pub formats: *mut CLIPRDR_FORMAT,
|
||||
}
|
||||
pub type CLIPRDR_FORMAT_LIST = _CLIPRDR_FORMAT_LIST;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FORMAT_LIST_RESPONSE {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_FORMAT_LIST_RESPONSE = _CLIPRDR_FORMAT_LIST_RESPONSE;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_LOCK_CLIPBOARD_DATA {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub clipDataId: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_LOCK_CLIPBOARD_DATA = _CLIPRDR_LOCK_CLIPBOARD_DATA;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub clipDataId: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_UNLOCK_CLIPBOARD_DATA = _CLIPRDR_UNLOCK_CLIPBOARD_DATA;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FORMAT_DATA_REQUEST {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub requestedFormatId: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_FORMAT_DATA_REQUEST = _CLIPRDR_FORMAT_DATA_REQUEST;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FORMAT_DATA_RESPONSE {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub requestedFormatData: *const BYTE,
|
||||
}
|
||||
pub type CLIPRDR_FORMAT_DATA_RESPONSE = _CLIPRDR_FORMAT_DATA_RESPONSE;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FILE_CONTENTS_REQUEST {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub streamId: UINT32,
|
||||
pub listIndex: UINT32,
|
||||
pub dwFlags: UINT32,
|
||||
pub nPositionLow: UINT32,
|
||||
pub nPositionHigh: UINT32,
|
||||
pub cbRequested: UINT32,
|
||||
pub haveClipDataId: BOOL,
|
||||
pub clipDataId: UINT32,
|
||||
}
|
||||
pub type CLIPRDR_FILE_CONTENTS_REQUEST = _CLIPRDR_FILE_CONTENTS_REQUEST;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _CLIPRDR_FILE_CONTENTS_RESPONSE {
|
||||
pub connID: UINT32,
|
||||
pub msgType: UINT16,
|
||||
pub msgFlags: UINT16,
|
||||
pub dataLen: UINT32,
|
||||
pub streamId: UINT32,
|
||||
pub cbRequested: UINT32,
|
||||
pub requestedData: *const BYTE,
|
||||
}
|
||||
pub type CLIPRDR_FILE_CONTENTS_RESPONSE = _CLIPRDR_FILE_CONTENTS_RESPONSE;
|
||||
pub type CliprdrClientContext = _cliprdr_client_context;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _NOTIFICATION_MESSAGE {
|
||||
pub r#type: UINT32, // 0 - info, 1 - warning, 2 - error
|
||||
pub msg: *const BYTE,
|
||||
pub details: *const BYTE,
|
||||
}
|
||||
pub type NOTIFICATION_MESSAGE = _NOTIFICATION_MESSAGE;
|
||||
pub type pcCliprdrServerCapabilities = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
capabilities: *const CLIPRDR_CAPABILITIES,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientCapabilities = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
capabilities: *const CLIPRDR_CAPABILITIES,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrMonitorReady = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
monitorReady: *const CLIPRDR_MONITOR_READY,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrTempDirectory = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
tempDirectory: *const CLIPRDR_TEMP_DIRECTORY,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcNotifyClipboardMsg =
|
||||
::std::option::Option<unsafe extern "C" fn(connID: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT>;
|
||||
pub type pcCliprdrClientFormatList = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatList: *const CLIPRDR_FORMAT_LIST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFormatList = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatList: *const CLIPRDR_FORMAT_LIST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientFormatListResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFormatListResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientLockClipboardData = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerLockClipboardData = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientUnlockClipboardData = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerUnlockClipboardData = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientFormatDataRequest = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFormatDataRequest = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientFormatDataResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFormatDataResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientFileContentsRequest = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFileContentsRequest = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrClientFileContentsResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
pub type pcCliprdrServerFileContentsResponse = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
context: *mut CliprdrClientContext,
|
||||
fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE,
|
||||
) -> UINT,
|
||||
>;
|
||||
|
||||
// TODO: hide more members of clipboard context
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct _cliprdr_client_context {
|
||||
pub Custom: *mut ::std::os::raw::c_void,
|
||||
pub EnableFiles: BOOL,
|
||||
pub EnableOthers: BOOL,
|
||||
pub IsStopped: BOOL,
|
||||
pub ResponseWaitTimeoutSecs: UINT32,
|
||||
pub ServerCapabilities: pcCliprdrServerCapabilities,
|
||||
pub ClientCapabilities: pcCliprdrClientCapabilities,
|
||||
pub MonitorReady: pcCliprdrMonitorReady,
|
||||
pub TempDirectory: pcCliprdrTempDirectory,
|
||||
pub NotifyClipboardMsg: pcNotifyClipboardMsg,
|
||||
pub ClientFormatList: pcCliprdrClientFormatList,
|
||||
pub ServerFormatList: pcCliprdrServerFormatList,
|
||||
pub ClientFormatListResponse: pcCliprdrClientFormatListResponse,
|
||||
pub ServerFormatListResponse: pcCliprdrServerFormatListResponse,
|
||||
pub ClientLockClipboardData: pcCliprdrClientLockClipboardData,
|
||||
pub ServerLockClipboardData: pcCliprdrServerLockClipboardData,
|
||||
pub ClientUnlockClipboardData: pcCliprdrClientUnlockClipboardData,
|
||||
pub ServerUnlockClipboardData: pcCliprdrServerUnlockClipboardData,
|
||||
pub ClientFormatDataRequest: pcCliprdrClientFormatDataRequest,
|
||||
pub ServerFormatDataRequest: pcCliprdrServerFormatDataRequest,
|
||||
pub ClientFormatDataResponse: pcCliprdrClientFormatDataResponse,
|
||||
pub ServerFormatDataResponse: pcCliprdrServerFormatDataResponse,
|
||||
pub ClientFileContentsRequest: pcCliprdrClientFileContentsRequest,
|
||||
pub ServerFileContentsRequest: pcCliprdrServerFileContentsRequest,
|
||||
pub ClientFileContentsResponse: pcCliprdrClientFileContentsResponse,
|
||||
pub ServerFileContentsResponse: pcCliprdrServerFileContentsResponse,
|
||||
pub LastRequestedFormatId: UINT32,
|
||||
}
|
||||
|
||||
// #[link(name = "user32")]
|
||||
// #[link(name = "ole32")]
|
||||
extern "C" {
|
||||
pub(crate) fn init_cliprdr(context: *mut CliprdrClientContext) -> BOOL;
|
||||
pub(crate) fn uninit_cliprdr(context: *mut CliprdrClientContext) -> BOOL;
|
||||
pub(crate) fn empty_cliprdr(context: *mut CliprdrClientContext, connID: UINT32) -> BOOL;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CliprdrError {
|
||||
#[error("invalid cliprdr name")]
|
||||
CliprdrName,
|
||||
#[error("failed to init cliprdr")]
|
||||
CliprdrInit,
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CliprdrClientContext {
|
||||
pub fn create(
|
||||
enable_files: bool,
|
||||
enable_others: bool,
|
||||
response_wait_timeout_secs: u32,
|
||||
notify_callback: pcNotifyClipboardMsg,
|
||||
client_format_list: pcCliprdrClientFormatList,
|
||||
client_format_list_response: pcCliprdrClientFormatListResponse,
|
||||
client_format_data_request: pcCliprdrClientFormatDataRequest,
|
||||
client_format_data_response: pcCliprdrClientFormatDataResponse,
|
||||
client_file_contents_request: pcCliprdrClientFileContentsRequest,
|
||||
client_file_contents_response: pcCliprdrClientFileContentsResponse,
|
||||
) -> Result<Box<Self>, CliprdrError> {
|
||||
let context = CliprdrClientContext {
|
||||
Custom: 0 as *mut _,
|
||||
EnableFiles: if enable_files { TRUE } else { FALSE },
|
||||
EnableOthers: if enable_others { TRUE } else { FALSE },
|
||||
IsStopped: FALSE,
|
||||
ResponseWaitTimeoutSecs: response_wait_timeout_secs,
|
||||
ServerCapabilities: None,
|
||||
ClientCapabilities: None,
|
||||
MonitorReady: None,
|
||||
TempDirectory: None,
|
||||
NotifyClipboardMsg: notify_callback,
|
||||
ClientFormatList: client_format_list,
|
||||
ServerFormatList: None,
|
||||
ClientFormatListResponse: client_format_list_response,
|
||||
ServerFormatListResponse: None,
|
||||
ClientLockClipboardData: None,
|
||||
ServerLockClipboardData: None,
|
||||
ClientUnlockClipboardData: None,
|
||||
ServerUnlockClipboardData: None,
|
||||
ClientFormatDataRequest: client_format_data_request,
|
||||
ServerFormatDataRequest: None,
|
||||
ClientFormatDataResponse: client_format_data_response,
|
||||
ServerFormatDataResponse: None,
|
||||
ClientFileContentsRequest: client_file_contents_request,
|
||||
ServerFileContentsRequest: None,
|
||||
ClientFileContentsResponse: client_file_contents_response,
|
||||
ServerFileContentsResponse: None,
|
||||
LastRequestedFormatId: 0,
|
||||
};
|
||||
let mut context = Box::new(context);
|
||||
unsafe {
|
||||
if FALSE == init_cliprdr(&mut (*context)) {
|
||||
println!("Failed to init cliprdr");
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
} else {
|
||||
Ok(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CliprdrClientContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if FALSE == uninit_cliprdr(&mut *self) {
|
||||
println!("Failed to uninit cliprdr");
|
||||
} else {
|
||||
println!("Succeeded to uninit cliprdr");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +1,72 @@
|
||||
use crate::cliprdr::*;
|
||||
use hbb_common::log;
|
||||
use hbb_common::{log, ResultType};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::CliprdrServiceContext;
|
||||
|
||||
const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(0)};
|
||||
static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(None)};
|
||||
}
|
||||
|
||||
pub struct ContextSend {
|
||||
addr: Mutex<u64>,
|
||||
addr: Mutex<Option<Box<dyn CliprdrServiceContext>>>,
|
||||
}
|
||||
|
||||
impl ContextSend {
|
||||
#[inline]
|
||||
pub fn is_enabled() -> bool {
|
||||
*CONTEXT_SEND.addr.lock().unwrap() != 0
|
||||
CONTEXT_SEND.addr.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
pub fn set_is_stopped() {
|
||||
let _res = Self::proc(|c| {
|
||||
c.IsStopped = TRUE;
|
||||
0
|
||||
});
|
||||
let _res = Self::proc(|c| c.set_is_stopped().map_err(|e| e.into()));
|
||||
}
|
||||
|
||||
pub fn enable(enabled: bool) {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
if enabled {
|
||||
if *lock == 0 {
|
||||
match crate::create_cliprdr_context(
|
||||
true,
|
||||
false,
|
||||
CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS,
|
||||
) {
|
||||
Ok(context) => {
|
||||
log::info!("clipboard context for file transfer created.");
|
||||
*lock = Box::into_raw(context) as _;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Create clipboard context for file transfer: {}",
|
||||
err.to_string()
|
||||
);
|
||||
}
|
||||
if lock.is_some() {
|
||||
return;
|
||||
}
|
||||
match crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS) {
|
||||
Ok(context) => {
|
||||
log::info!("clipboard context for file transfer created.");
|
||||
*lock = Some(context)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"create clipboard context for file transfer: {}",
|
||||
err.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if *lock != 0 {
|
||||
unsafe {
|
||||
let _ = Box::from_raw(*lock as *mut CliprdrClientContext);
|
||||
}
|
||||
log::info!("clipboard context for file transfer destroyed.");
|
||||
*lock = 0;
|
||||
}
|
||||
} else if let Some(_clp) = lock.take() {
|
||||
*lock = None;
|
||||
log::info!("clipboard context for file transfer destroyed.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn proc<F: FnOnce(&mut CliprdrClientContext) -> u32>(f: F) -> u32 {
|
||||
let lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
if *lock != 0 {
|
||||
unsafe { f(&mut *(*lock as *mut CliprdrClientContext)) }
|
||||
} else {
|
||||
0
|
||||
/// make sure the clipboard context is enabled.
|
||||
pub fn make_sure_enabled() -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?;
|
||||
*lock = Some(ctx);
|
||||
log::info!("clipboard context for file transfer recreated.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>(
|
||||
f: F,
|
||||
) -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
match lock.as_mut() {
|
||||
Some(context) => f(context),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
use cliprdr::*;
|
||||
#[allow(dead_code)]
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
use hbb_common::{allow_err, log};
|
||||
use hbb_common::{
|
||||
allow_err, lazy_static, log,
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex as TokioMutex,
|
||||
@ -8,19 +15,59 @@ use hbb_common::{
|
||||
ResultType,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
boxed::Box,
|
||||
ffi::{CStr, CString},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod cliprdr;
|
||||
pub mod context_send;
|
||||
pub mod platform;
|
||||
pub use context_send::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
/// Ability to handle Clipboard File from remote rustdesk client
|
||||
///
|
||||
/// # Note
|
||||
/// There actually should be 2 parts to implement a useable clipboard file service,
|
||||
/// but this only contains the RPC server part.
|
||||
/// The local listener and transport part is too platform specific to wrap up in typeclasses.
|
||||
pub trait CliprdrServiceContext: Send + Sync {
|
||||
/// set to be stopped
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
|
||||
/// clear the content on clipboard
|
||||
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
|
||||
|
||||
/// run as a server for clipboard RPC
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CliprdrError {
|
||||
#[error("invalid cliprdr name")]
|
||||
CliprdrName,
|
||||
#[error("failed to init cliprdr")]
|
||||
CliprdrInit,
|
||||
#[error("cliprdr out of memory")]
|
||||
CliprdrOutOfMemory,
|
||||
#[error("cliprdr internal error")]
|
||||
ClipboardInternalError,
|
||||
#[error("cliprdr occupied")]
|
||||
ClipboardOccupied,
|
||||
#[error("conversion failure")]
|
||||
ConversionFailure,
|
||||
#[error("failure to read clipboard")]
|
||||
OpenClipboard,
|
||||
#[error("failure to read file metadata or content")]
|
||||
FileError { path: PathBuf, err: std::io::Error },
|
||||
#[error("invalid request")]
|
||||
InvalidRequest { description: String },
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum ClipboardFile {
|
||||
@ -63,6 +110,7 @@ pub enum ClipboardFile {
|
||||
struct MsgChannel {
|
||||
peer_id: String,
|
||||
conn_id: i32,
|
||||
#[allow(dead_code)]
|
||||
sender: UnboundedSender<ClipboardFile>,
|
||||
receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>,
|
||||
}
|
||||
@ -74,19 +122,19 @@ lazy_static::lazy_static! {
|
||||
|
||||
impl ClipboardFile {
|
||||
pub fn is_stopping_allowed(&self) -> bool {
|
||||
match self {
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady
|
||||
| ClipboardFile::FormatList { .. }
|
||||
| ClipboardFile::FormatDataRequest { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
| ClipboardFile::FormatList { .. }
|
||||
| ClipboardFile::FormatDataRequest { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_stopping_allowed_from_peer(&self) -> bool {
|
||||
match self {
|
||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,8 +196,21 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if conn_id == 0 {
|
||||
send_data_to_all(data);
|
||||
} else {
|
||||
send_data_to_channel(conn_id, data);
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
@ -161,608 +222,13 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool {
|
||||
unsafe { TRUE == cliprdr::empty_cliprdr(context, conn_id as u32) }
|
||||
}
|
||||
|
||||
pub fn server_clip_file(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
msg: ClipboardFile,
|
||||
) -> u32 {
|
||||
let mut ret = 0;
|
||||
match msg {
|
||||
ClipboardFile::NotifyCallback { .. } => {
|
||||
// unreachable
|
||||
}
|
||||
ClipboardFile::MonitorReady => {
|
||||
log::debug!("server_monitor_ready called");
|
||||
ret = server_monitor_ready(context, conn_id);
|
||||
log::debug!(
|
||||
"server_monitor_ready called, conn_id {}, return {}",
|
||||
conn_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
log::debug!(
|
||||
"server_format_list called, conn_id {}, format_list: {:?}",
|
||||
conn_id,
|
||||
&format_list
|
||||
);
|
||||
ret = server_format_list(context, conn_id, format_list);
|
||||
log::debug!(
|
||||
"server_format_list called, conn_id {}, return {}",
|
||||
conn_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FormatListResponse { msg_flags } => {
|
||||
log::debug!("server_format_list_response called");
|
||||
ret = server_format_list_response(context, conn_id, msg_flags);
|
||||
log::debug!(
|
||||
"server_format_list_response called, conn_id {}, msg_flags {}, return {}",
|
||||
conn_id,
|
||||
msg_flags,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FormatDataRequest {
|
||||
requested_format_id,
|
||||
} => {
|
||||
log::debug!("server_format_data_request called");
|
||||
ret = server_format_data_request(context, conn_id, requested_format_id);
|
||||
log::debug!(
|
||||
"server_format_data_request called, conn_id {}, requested_format_id {}, return {}",
|
||||
conn_id,
|
||||
requested_format_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
log::debug!("server_format_data_response called");
|
||||
ret = server_format_data_response(context, conn_id, msg_flags, format_data);
|
||||
log::debug!(
|
||||
"server_format_data_response called, conn_id {}, msg_flags: {}, return {}",
|
||||
conn_id,
|
||||
msg_flags,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
have_clip_data_id,
|
||||
clip_data_id,
|
||||
} => {
|
||||
log::debug!("server_file_contents_request called");
|
||||
ret = server_file_contents_request(
|
||||
context,
|
||||
conn_id,
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
have_clip_data_id,
|
||||
clip_data_id,
|
||||
);
|
||||
log::debug!("server_file_contents_request called, conn_id {}, stream_id: {}, list_index {}, dw_flags {}, n_position_low {}, n_position_high {}, cb_requested {}, have_clip_data_id {}, clip_data_id {}, return {}", conn_id,
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
have_clip_data_id,
|
||||
clip_data_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
} => {
|
||||
log::debug!("server_file_contents_response called");
|
||||
ret = server_file_contents_response(
|
||||
context,
|
||||
conn_id,
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
);
|
||||
log::debug!("server_file_contents_response called, conn_id {}, msg_flags {}, stream_id {}, return {}",
|
||||
conn_id,
|
||||
msg_flags,
|
||||
stream_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 {
|
||||
unsafe {
|
||||
let monitor_ready = CLIPRDR_MONITOR_READY {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: 0 as UINT16,
|
||||
dataLen: 0 as UINT32,
|
||||
};
|
||||
if let Some(f) = context.MonitorReady {
|
||||
let ret = f(context, &monitor_ready);
|
||||
ret as u32
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_format_list(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
format_list: Vec<(i32, String)>,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let num_formats = format_list.len() as UINT32;
|
||||
let mut formats = format_list
|
||||
.into_iter()
|
||||
.map(|format| {
|
||||
if format.1.is_empty() {
|
||||
CLIPRDR_FORMAT {
|
||||
formatId: format.0 as UINT32,
|
||||
formatName: 0 as *mut _,
|
||||
}
|
||||
} else {
|
||||
let n = match CString::new(format.1) {
|
||||
Ok(n) => n,
|
||||
Err(_) => CString::new("").unwrap(),
|
||||
};
|
||||
CLIPRDR_FORMAT {
|
||||
formatId: format.0 as UINT32,
|
||||
formatName: n.into_raw(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CLIPRDR_FORMAT>>();
|
||||
|
||||
let format_list = CLIPRDR_FORMAT_LIST {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: 0 as UINT16,
|
||||
dataLen: 0 as UINT32,
|
||||
numFormats: num_formats,
|
||||
formats: formats.as_mut_ptr(),
|
||||
};
|
||||
|
||||
let ret = if let Some(f) = context.ServerFormatList {
|
||||
f(context, &format_list)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
};
|
||||
|
||||
for f in formats {
|
||||
if !f.formatName.is_null() {
|
||||
// retake pointer to free memory
|
||||
let _ = CString::from_raw(f.formatName);
|
||||
}
|
||||
}
|
||||
|
||||
ret as u32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_format_list_response(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
msg_flags: i32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let format_list_response = CLIPRDR_FORMAT_LIST_RESPONSE {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: msg_flags as UINT16,
|
||||
dataLen: 0 as UINT32,
|
||||
};
|
||||
|
||||
if let Some(f) = context.ServerFormatListResponse {
|
||||
f(context, &format_list_response)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_format_data_request(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
requested_format_id: i32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let format_data_request = CLIPRDR_FORMAT_DATA_REQUEST {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: 0 as UINT16,
|
||||
dataLen: 0 as UINT32,
|
||||
requestedFormatId: requested_format_id as UINT32,
|
||||
};
|
||||
if let Some(f) = context.ServerFormatDataRequest {
|
||||
f(context, &format_data_request)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_format_data_response(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
msg_flags: i32,
|
||||
mut format_data: Vec<u8>,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let format_data_response = CLIPRDR_FORMAT_DATA_RESPONSE {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: msg_flags as UINT16,
|
||||
dataLen: format_data.len() as UINT32,
|
||||
requestedFormatData: format_data.as_mut_ptr(),
|
||||
};
|
||||
if let Some(f) = context.ServerFormatDataResponse {
|
||||
f(context, &format_data_response)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_file_contents_request(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
stream_id: i32,
|
||||
list_index: i32,
|
||||
dw_flags: i32,
|
||||
n_position_low: i32,
|
||||
n_position_high: i32,
|
||||
cb_requested: i32,
|
||||
have_clip_data_id: bool,
|
||||
clip_data_id: i32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let file_contents_request = CLIPRDR_FILE_CONTENTS_REQUEST {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: 0 as UINT16,
|
||||
dataLen: 0 as UINT32,
|
||||
streamId: stream_id as UINT32,
|
||||
listIndex: list_index as UINT32,
|
||||
dwFlags: dw_flags as UINT32,
|
||||
nPositionLow: n_position_low as UINT32,
|
||||
nPositionHigh: n_position_high as UINT32,
|
||||
cbRequested: cb_requested as UINT32,
|
||||
haveClipDataId: if have_clip_data_id { TRUE } else { FALSE },
|
||||
clipDataId: clip_data_id as UINT32,
|
||||
};
|
||||
if let Some(f) = context.ServerFileContentsRequest {
|
||||
f(context, &file_contents_request)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_file_contents_response(
|
||||
context: &mut CliprdrClientContext,
|
||||
conn_id: i32,
|
||||
msg_flags: i32,
|
||||
stream_id: i32,
|
||||
mut requested_data: Vec<u8>,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let file_contents_response = CLIPRDR_FILE_CONTENTS_RESPONSE {
|
||||
connID: conn_id as UINT32,
|
||||
msgType: 0 as UINT16,
|
||||
msgFlags: msg_flags as UINT16,
|
||||
dataLen: 4 + requested_data.len() as UINT32,
|
||||
streamId: stream_id as UINT32,
|
||||
cbRequested: requested_data.len() as UINT32,
|
||||
requestedData: requested_data.as_mut_ptr(),
|
||||
};
|
||||
if let Some(f) = context.ServerFileContentsResponse {
|
||||
f(context, &file_contents_response)
|
||||
} else {
|
||||
ERR_CODE_SERVER_FUNCTION_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_cliprdr_context(
|
||||
enable_files: bool,
|
||||
enable_others: bool,
|
||||
response_wait_timeout_secs: u32,
|
||||
) -> ResultType<Box<CliprdrClientContext>> {
|
||||
Ok(CliprdrClientContext::create(
|
||||
enable_files,
|
||||
enable_others,
|
||||
response_wait_timeout_secs,
|
||||
Some(notify_callback),
|
||||
Some(client_format_list),
|
||||
Some(client_format_list_response),
|
||||
Some(client_format_data_request),
|
||||
Some(client_format_data_response),
|
||||
Some(client_file_contents_request),
|
||||
Some(client_file_contents_response),
|
||||
)?)
|
||||
}
|
||||
|
||||
extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT {
|
||||
log::debug!("notify_callback called");
|
||||
let data = unsafe {
|
||||
let msg = &*msg;
|
||||
let details = if msg.details.is_null() {
|
||||
Ok("")
|
||||
} else {
|
||||
CStr::from_ptr(msg.details as _).to_str()
|
||||
};
|
||||
match (CStr::from_ptr(msg.msg as _).to_str(), details) {
|
||||
(Ok(m), Ok(d)) => {
|
||||
let msgtype = format!(
|
||||
"custom-{}-nocancel-nook-hasclose",
|
||||
if msg.r#type == 0 {
|
||||
"info"
|
||||
} else if msg.r#type == 1 {
|
||||
"warn"
|
||||
} else {
|
||||
"error"
|
||||
}
|
||||
);
|
||||
let title = "Clipboard";
|
||||
let text = if d.is_empty() {
|
||||
m.to_string()
|
||||
} else {
|
||||
format!("{} {}", m, d)
|
||||
};
|
||||
ClipboardFile::NotifyCallback {
|
||||
r#type: msgtype,
|
||||
title: title.to_string(),
|
||||
text,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("notify_callback: failed to convert msg");
|
||||
return ERR_CODE_INVALID_PARAMETER;
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[inline]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
send_data(conn_id as _, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_format_list(
|
||||
_context: *mut CliprdrClientContext,
|
||||
clip_format_list: *const CLIPRDR_FORMAT_LIST,
|
||||
) -> UINT {
|
||||
let conn_id;
|
||||
let mut format_list: Vec<(i32, String)> = Vec::new();
|
||||
unsafe {
|
||||
let mut i = 0u32;
|
||||
while i < (*clip_format_list).numFormats {
|
||||
let format_data = &(*(*clip_format_list).formats.offset(i as isize));
|
||||
if format_data.formatName.is_null() {
|
||||
format_list.push((format_data.formatId as i32, "".to_owned()));
|
||||
} else {
|
||||
let format_name = CStr::from_ptr(format_data.formatName).to_str();
|
||||
let format_name = match format_name {
|
||||
Ok(n) => n.to_owned(),
|
||||
Err(_) => {
|
||||
log::warn!("failed to get format name");
|
||||
"".to_owned()
|
||||
}
|
||||
};
|
||||
format_list.push((format_data.formatId as i32, format_name));
|
||||
}
|
||||
// log::debug!("format list item {}: format id: {}, format name: {}", i, format_data.formatId, &format_name);
|
||||
i += 1;
|
||||
}
|
||||
conn_id = (*clip_format_list).connID as i32;
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
log::debug!(
|
||||
"client_format_list called, client id: {}, format_list: {:?}",
|
||||
conn_id,
|
||||
&format_list
|
||||
);
|
||||
let data = ClipboardFile::FormatList { format_list };
|
||||
// no need to handle result here
|
||||
if conn_id == 0 {
|
||||
// msg_channel is used for debug, VEC_MSG_CHANNEL cannot be inspected by the debugger.
|
||||
let msg_channel = VEC_MSG_CHANNEL.read().unwrap();
|
||||
msg_channel
|
||||
.iter()
|
||||
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
|
||||
} else {
|
||||
send_data(conn_id, data);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_format_list_response(
|
||||
_context: *mut CliprdrClientContext,
|
||||
format_list_response: *const CLIPRDR_FORMAT_LIST_RESPONSE,
|
||||
) -> UINT {
|
||||
let conn_id;
|
||||
let msg_flags;
|
||||
unsafe {
|
||||
conn_id = (*format_list_response).connID as i32;
|
||||
msg_flags = (*format_list_response).msgFlags as i32;
|
||||
}
|
||||
log::debug!(
|
||||
"client_format_list_response called, client id: {}, msg_flags: {}",
|
||||
conn_id,
|
||||
msg_flags
|
||||
);
|
||||
let data = ClipboardFile::FormatListResponse { msg_flags };
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_request(
|
||||
_context: *mut CliprdrClientContext,
|
||||
format_data_request: *const CLIPRDR_FORMAT_DATA_REQUEST,
|
||||
) -> UINT {
|
||||
let conn_id;
|
||||
let requested_format_id;
|
||||
unsafe {
|
||||
conn_id = (*format_data_request).connID as i32;
|
||||
requested_format_id = (*format_data_request).requestedFormatId as i32;
|
||||
}
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id,
|
||||
};
|
||||
log::debug!(
|
||||
"client_format_data_request called, conn_id: {}, requested_format_id: {}",
|
||||
conn_id,
|
||||
requested_format_id
|
||||
);
|
||||
// no need to handle result here
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_response(
|
||||
_context: *mut CliprdrClientContext,
|
||||
format_data_response: *const CLIPRDR_FORMAT_DATA_RESPONSE,
|
||||
) -> UINT {
|
||||
let conn_id;
|
||||
let msg_flags;
|
||||
let format_data;
|
||||
unsafe {
|
||||
conn_id = (*format_data_response).connID as i32;
|
||||
msg_flags = (*format_data_response).msgFlags as i32;
|
||||
if (*format_data_response).requestedFormatData.is_null() {
|
||||
format_data = Vec::new();
|
||||
} else {
|
||||
format_data = std::slice::from_raw_parts(
|
||||
(*format_data_response).requestedFormatData,
|
||||
(*format_data_response).dataLen as usize,
|
||||
)
|
||||
.to_vec();
|
||||
}
|
||||
}
|
||||
log::debug!(
|
||||
"client_format_data_response called, client id: {}, msg_flags: {}",
|
||||
conn_id,
|
||||
msg_flags
|
||||
);
|
||||
let data = ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_request(
|
||||
_context: *mut CliprdrClientContext,
|
||||
file_contents_request: *const CLIPRDR_FILE_CONTENTS_REQUEST,
|
||||
) -> UINT {
|
||||
// TODO: support huge file?
|
||||
// if (!cliprdr->hasHugeFileSupport)
|
||||
// {
|
||||
// if (((UINT64)fileContentsRequest->cbRequested + fileContentsRequest->nPositionLow) >
|
||||
// UINT32_MAX)
|
||||
// return ERROR_INVALID_PARAMETER;
|
||||
// if (fileContentsRequest->nPositionHigh != 0)
|
||||
// return ERROR_INVALID_PARAMETER;
|
||||
// }
|
||||
|
||||
let conn_id;
|
||||
let stream_id;
|
||||
let list_index;
|
||||
let dw_flags;
|
||||
let n_position_low;
|
||||
let n_position_high;
|
||||
let cb_requested;
|
||||
let have_clip_data_id;
|
||||
let clip_data_id;
|
||||
unsafe {
|
||||
conn_id = (*file_contents_request).connID as i32;
|
||||
stream_id = (*file_contents_request).streamId as i32;
|
||||
list_index = (*file_contents_request).listIndex as i32;
|
||||
dw_flags = (*file_contents_request).dwFlags as i32;
|
||||
n_position_low = (*file_contents_request).nPositionLow as i32;
|
||||
n_position_high = (*file_contents_request).nPositionHigh as i32;
|
||||
cb_requested = (*file_contents_request).cbRequested as i32;
|
||||
have_clip_data_id = (*file_contents_request).haveClipDataId == TRUE;
|
||||
clip_data_id = (*file_contents_request).clipDataId as i32;
|
||||
}
|
||||
let data = ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
have_clip_data_id,
|
||||
clip_data_id,
|
||||
};
|
||||
log::debug!("client_file_contents_request called, data: {:?}", &data);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_response(
|
||||
_context: *mut CliprdrClientContext,
|
||||
file_contents_response: *const CLIPRDR_FILE_CONTENTS_RESPONSE,
|
||||
) -> UINT {
|
||||
let conn_id;
|
||||
let msg_flags;
|
||||
let stream_id;
|
||||
let requested_data;
|
||||
unsafe {
|
||||
conn_id = (*file_contents_response).connID as i32;
|
||||
msg_flags = (*file_contents_response).msgFlags as i32;
|
||||
stream_id = (*file_contents_response).streamId as i32;
|
||||
if (*file_contents_response).requestedData.is_null() {
|
||||
requested_data = Vec::new();
|
||||
} else {
|
||||
requested_data = std::slice::from_raw_parts(
|
||||
(*file_contents_response).requestedData,
|
||||
(*file_contents_response).cbRequested as usize,
|
||||
)
|
||||
.to_vec();
|
||||
}
|
||||
}
|
||||
let data = ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
};
|
||||
log::debug!(
|
||||
"client_file_contents_response called, conn_id: {}, msg_flags: {}, stream_id: {}",
|
||||
conn_id,
|
||||
msg_flags,
|
||||
stream_id
|
||||
);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
1182
libs/clipboard/src/platform/fuse.rs
Normal file
1182
libs/clipboard/src/platform/fuse.rs
Normal file
File diff suppressed because it is too large
Load Diff
88
libs/clipboard/src/platform/mod.rs
Normal file
88
libs/clipboard/src/platform/mod.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use crate::{CliprdrError, CliprdrServiceContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn create_cliprdr_context(
|
||||
enable_files: bool,
|
||||
enable_others: bool,
|
||||
response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
let boxed =
|
||||
windows::create_cliprdr_context(enable_files, enable_others, response_wait_timeout_secs)?
|
||||
as Box<_>;
|
||||
Ok(boxed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
/// use FUSE for file pasting on these platforms
|
||||
pub mod fuse;
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub mod unix;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn create_cliprdr_context(
|
||||
_enable_files: bool,
|
||||
_enable_others: bool,
|
||||
_response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
{
|
||||
use std::{fs::Permissions, os::unix::prelude::PermissionsExt};
|
||||
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
|
||||
if !_enable_files {
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64);
|
||||
|
||||
let app_name = APP_NAME.read().unwrap().clone();
|
||||
|
||||
let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr");
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
std::fs::create_dir(&mnt_path).ok();
|
||||
std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
log::info!("clear previously mounted cliprdr FUSE");
|
||||
if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() {
|
||||
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
||||
}
|
||||
|
||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?;
|
||||
log::debug!("start cliprdr FUSE");
|
||||
unix_ctx.run().expect("failed to start cliprdr FUSE");
|
||||
|
||||
Ok(Box::new(unix_ctx) as Box<_>)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
struct DummyCliprdrContext {}
|
||||
|
||||
impl CliprdrServiceContext for DummyCliprdrContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
Ok(())
|
||||
}
|
||||
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn server_clip_file(
|
||||
&mut self,
|
||||
_conn_id: i32,
|
||||
_msg: crate::ClipboardFile,
|
||||
) -> Result<(), crate::CliprdrError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
// begin of epoch used by microsoft
|
||||
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
|
||||
const LDAP_EPOCH_DELTA: u64 = 116444772610000000;
|
367
libs/clipboard/src/platform/unix/local_file.rs
Normal file
367
libs/clipboard/src/platform/unix/local_file.rs
Normal file
@ -0,0 +1,367 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Seek},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use utf16string::WString;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA},
|
||||
CliprdrError,
|
||||
};
|
||||
|
||||
/// has valid file attributes
|
||||
const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LocalFile {
|
||||
pub path: PathBuf,
|
||||
|
||||
pub handle: Option<BufReader<File>>,
|
||||
pub offset: AtomicU64,
|
||||
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub last_write_time: SystemTime,
|
||||
pub is_dir: bool,
|
||||
pub perm: u32,
|
||||
pub read_only: bool,
|
||||
pub hidden: bool,
|
||||
pub system: bool,
|
||||
pub archive: bool,
|
||||
pub normal: bool,
|
||||
}
|
||||
|
||||
impl LocalFile {
|
||||
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
let size = mt.len() as u64;
|
||||
let is_dir = mt.is_dir();
|
||||
let read_only = mt.permissions().readonly();
|
||||
let system = false;
|
||||
let hidden = path.to_string_lossy().starts_with('.');
|
||||
let archive = false;
|
||||
let normal = !(is_dir || read_only || system || hidden || archive);
|
||||
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
let perm = mt.permissions().mode();
|
||||
|
||||
let name = path
|
||||
.display()
|
||||
.to_string()
|
||||
.trim_start_matches('/')
|
||||
.replace('/', "\\");
|
||||
|
||||
// NOTE: open files lazily
|
||||
let handle = None;
|
||||
let offset = AtomicU64::new(0);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
path: path.clone(),
|
||||
handle,
|
||||
offset,
|
||||
size,
|
||||
last_write_time,
|
||||
is_dir,
|
||||
read_only,
|
||||
system,
|
||||
hidden,
|
||||
perm,
|
||||
archive,
|
||||
normal,
|
||||
})
|
||||
}
|
||||
pub fn as_bin(&self) -> Vec<u8> {
|
||||
let mut buf = BytesMut::with_capacity(592);
|
||||
|
||||
let read_only_flag = if self.read_only { 0x1 } else { 0 };
|
||||
let hidden_flag = if self.hidden { 0x2 } else { 0 };
|
||||
let system_flag = if self.system { 0x4 } else { 0 };
|
||||
let directory_flag = if self.is_dir { 0x10 } else { 0 };
|
||||
let archive_flag = if self.archive { 0x20 } else { 0 };
|
||||
let normal_flag = if self.normal { 0x80 } else { 0 };
|
||||
|
||||
let file_attributes: u32 = read_only_flag
|
||||
| hidden_flag
|
||||
| system_flag
|
||||
| directory_flag
|
||||
| archive_flag
|
||||
| normal_flag;
|
||||
|
||||
let win32_time = self
|
||||
.last_write_time
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64
|
||||
/ 100
|
||||
+ LDAP_EPOCH_DELTA;
|
||||
|
||||
let size_high = (self.size >> 32) as u32;
|
||||
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
||||
|
||||
let path = self.path.to_string_lossy().to_string();
|
||||
|
||||
let wstr: WString<utf16string::LE> = WString::from(&path);
|
||||
let name = wstr.as_bytes();
|
||||
|
||||
log::trace!(
|
||||
"put file to list: name_len {}, name {}",
|
||||
name.len(),
|
||||
&self.name
|
||||
);
|
||||
|
||||
let flags = FLAGS_FD_SIZE
|
||||
| FLAGS_FD_LAST_WRITE
|
||||
| FLAGS_FD_ATTRIBUTES
|
||||
| FLAGS_FD_PROGRESSUI
|
||||
| FLAGS_FD_UNIX_MODE;
|
||||
|
||||
// flags, 4 bytes
|
||||
buf.put_u32_le(flags);
|
||||
// 32 bytes reserved
|
||||
buf.put(&[0u8; 32][..]);
|
||||
// file attributes, 4 bytes
|
||||
buf.put_u32_le(file_attributes);
|
||||
|
||||
// NOTE: this is not used in windows
|
||||
// in the specification, this is 16 bytes reserved
|
||||
// lets use the last 4 bytes to store the file mode
|
||||
//
|
||||
// 12 bytes reserved
|
||||
buf.put(&[0u8; 12][..]);
|
||||
// file permissions, 4 bytes
|
||||
buf.put_u32_le(self.perm);
|
||||
|
||||
// last write time, 8 bytes
|
||||
buf.put_u64_le(win32_time);
|
||||
// file size (high)
|
||||
buf.put_u32_le(size_high);
|
||||
// file size (low)
|
||||
buf.put_u32_le(size_low);
|
||||
// put name and padding to 520 bytes
|
||||
let name_len = name.len();
|
||||
buf.put(name);
|
||||
buf.put(&vec![0u8; 520 - name_len][..]);
|
||||
|
||||
buf.to_vec()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn load_handle(&mut self) -> Result<(), CliprdrError> {
|
||||
if !self.is_dir && self.handle.is_none() {
|
||||
let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle);
|
||||
reader.fill_buf().map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
self.handle = Some(reader);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||
self.load_handle()?;
|
||||
|
||||
let handle = self.handle.as_mut().unwrap();
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
self.offset.store(new_offset, Ordering::Relaxed);
|
||||
|
||||
// gc file handle
|
||||
if new_offset >= self.size {
|
||||
self.offset.store(0, Ordering::Relaxed);
|
||||
self.handle = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
fn constr_file_lst(
|
||||
path: &PathBuf,
|
||||
file_list: &mut Vec<LocalFile>,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
// prevent fs loop
|
||||
if visited.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
visited.insert(path.clone());
|
||||
|
||||
let local_file = LocalFile::try_open(path)?;
|
||||
file_list.push(local_file);
|
||||
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
if mt.is_dir() {
|
||||
let dir = std::fs::read_dir(path).unwrap();
|
||||
for entry in dir {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
constr_file_lst(&path, file_list, visited)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut file_list = Vec::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
constr_file_lst(path, &mut file_list, &mut visited)?;
|
||||
}
|
||||
Ok(file_list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_list_test {
|
||||
use std::{path::PathBuf, sync::atomic::AtomicU64};
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::{platform::fuse::FileDescription, CliprdrError};
|
||||
|
||||
use super::LocalFile;
|
||||
|
||||
#[inline]
|
||||
fn generate_tree(prefix: &str) -> Vec<LocalFile> {
|
||||
// generate a tree of local files, no handles
|
||||
// - /
|
||||
// |- a.txt
|
||||
// |- b
|
||||
// |- c.txt
|
||||
#[inline]
|
||||
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
|
||||
LocalFile {
|
||||
path: PathBuf::from(path),
|
||||
handle: None,
|
||||
name: name.to_string(),
|
||||
size: 0,
|
||||
offset: AtomicU64::new(0),
|
||||
last_write_time: std::time::SystemTime::UNIX_EPOCH,
|
||||
read_only: false,
|
||||
is_dir,
|
||||
perm: 0o754,
|
||||
hidden: false,
|
||||
system: false,
|
||||
archive: false,
|
||||
normal: false,
|
||||
}
|
||||
}
|
||||
|
||||
let p = prefix;
|
||||
|
||||
let (r_path, a_path, b_path, c_path) = if !prefix.is_empty() {
|
||||
(
|
||||
p.to_string(),
|
||||
format!("{}/a.txt", p),
|
||||
format!("{}/b", p),
|
||||
format!("{}/b/c.txt", p),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
".".to_string(),
|
||||
"a.txt".to_string(),
|
||||
"b".to_string(),
|
||||
"b/c.txt".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let root = generate_file(&r_path, ".", true);
|
||||
let a = generate_file(&a_path, "a.txt", false);
|
||||
let b = generate_file(&b_path, "b", true);
|
||||
let c = generate_file(&c_path, "c.txt", false);
|
||||
|
||||
vec![root, a, b, c]
|
||||
}
|
||||
|
||||
fn as_bin_parse_test(prefix: &str) -> Result<(), CliprdrError> {
|
||||
let tree = generate_tree(prefix);
|
||||
let mut pdu = BytesMut::with_capacity(4 + 592 * tree.len());
|
||||
pdu.put_u32_le(tree.len() as u32);
|
||||
for file in tree {
|
||||
pdu.put(file.as_bin().as_slice());
|
||||
}
|
||||
|
||||
let parsed = FileDescription::parse_file_descriptors(pdu.to_vec(), 0)?;
|
||||
assert_eq!(parsed.len(), 4);
|
||||
|
||||
if !prefix.is_empty() {
|
||||
assert_eq!(parsed[0].name.to_str().unwrap(), format!("{}", prefix));
|
||||
assert_eq!(
|
||||
parsed[1].name.to_str().unwrap(),
|
||||
format!("{}/a.txt", prefix)
|
||||
);
|
||||
assert_eq!(parsed[2].name.to_str().unwrap(), format!("{}/b", prefix));
|
||||
assert_eq!(
|
||||
parsed[3].name.to_str().unwrap(),
|
||||
format!("{}/b/c.txt", prefix)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(parsed[0].name.to_str().unwrap(), ".");
|
||||
assert_eq!(parsed[1].name.to_str().unwrap(), "a.txt");
|
||||
assert_eq!(parsed[2].name.to_str().unwrap(), "b");
|
||||
assert_eq!(parsed[3].name.to_str().unwrap(), "b/c.txt");
|
||||
}
|
||||
|
||||
assert!(parsed[0].perm & 0o777 == 0o754);
|
||||
assert!(parsed[1].perm & 0o777 == 0o754);
|
||||
assert!(parsed[2].perm & 0o777 == 0o754);
|
||||
assert!(parsed[3].perm & 0o777 == 0o754);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_descriptors() -> Result<(), CliprdrError> {
|
||||
as_bin_parse_test("")?;
|
||||
as_bin_parse_test("/")?;
|
||||
as_bin_parse_test("test")?;
|
||||
as_bin_parse_test("/test")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
600
libs/clipboard/src/platform/unix/mod.rs
Normal file
600
libs/clipboard/src/platform/unix/mod.rs
Normal file
@ -0,0 +1,600 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::FileDescription, unix::local_file::construct_file_list},
|
||||
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
||||
};
|
||||
|
||||
use self::local_file::LocalFile;
|
||||
#[cfg(target_os = "linux")]
|
||||
use self::url::{encode_path_to_uri, parse_plain_uri_list};
|
||||
|
||||
use super::fuse::FuseServer;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// clipboard implementation of x11
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// clipboard implementation of macos
|
||||
pub mod ns_clipboard;
|
||||
|
||||
pub mod local_file;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod url;
|
||||
|
||||
// not actual format id, just a placeholder
|
||||
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||
// not actual format id, just a placeholder
|
||||
const FILECONTENTS_FORMAT_ID: i32 = 49267;
|
||||
const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
|
||||
|
||||
lazy_static! {
|
||||
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::from_iter(
|
||||
[
|
||||
(
|
||||
FILEDESCRIPTOR_FORMAT_ID,
|
||||
FILEDESCRIPTORW_FORMAT_NAME.to_string()
|
||||
),
|
||||
(FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string())
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
);
|
||||
}
|
||||
|
||||
fn get_local_format(remote_id: i32) -> Option<String> {
|
||||
REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone())
|
||||
}
|
||||
|
||||
fn add_remote_format(local_name: &str, remote_id: i32) {
|
||||
REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string());
|
||||
}
|
||||
|
||||
trait SysClipboard: Send + Sync {
|
||||
fn start(&self);
|
||||
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
|
||||
fn get_file_list(&self) -> Vec<PathBuf>;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
{
|
||||
use x11::*;
|
||||
let x11_clip = X11Clipboard::new(ignore_path)?;
|
||||
Ok(Box::new(x11_clip) as Box<_>)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
use ns_clipboard::*;
|
||||
let ns_pb = NsPasteboard::new(ignore_path)?;
|
||||
Ok(Box::new(ns_pb) as Box<_>)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ClipboardContext {
|
||||
pub fuse_mount_point: PathBuf,
|
||||
/// stores fuse background session handle
|
||||
fuse_handle: Mutex<Option<fuser::BackgroundSession>>,
|
||||
|
||||
/// a sender of clipboard file contents pdu to fuse server
|
||||
fuse_tx: Sender<ClipboardFile>,
|
||||
fuse_server: Arc<Mutex<FuseServer>>,
|
||||
|
||||
clipboard: Arc<dyn SysClipboard>,
|
||||
local_files: Mutex<Vec<LocalFile>>,
|
||||
}
|
||||
|
||||
impl ClipboardContext {
|
||||
pub fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
|
||||
// assert mount path exists
|
||||
let fuse_mount_point = mount_path.canonicalize().map_err(|e| {
|
||||
log::error!("failed to canonicalize mount path: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
|
||||
let (fuse_server, fuse_tx) = FuseServer::new(timeout);
|
||||
|
||||
let fuse_server = Arc::new(Mutex::new(fuse_server));
|
||||
|
||||
let clipboard = get_sys_clipboard(&fuse_mount_point)?;
|
||||
let clipboard = Arc::from(clipboard) as Arc<_>;
|
||||
let local_files = Mutex::new(vec![]);
|
||||
|
||||
Ok(Self {
|
||||
fuse_mount_point,
|
||||
fuse_server,
|
||||
fuse_tx,
|
||||
fuse_handle: Mutex::new(None),
|
||||
clipboard,
|
||||
local_files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), CliprdrError> {
|
||||
if !self.is_stopped() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut fuse_handle = self.fuse_handle.lock();
|
||||
|
||||
let mount_path = &self.fuse_mount_point;
|
||||
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!(
|
||||
"mounting clipboard FUSE to {}",
|
||||
self.fuse_mount_point.display()
|
||||
);
|
||||
|
||||
let new_handle = fuser::spawn_mount2(
|
||||
FuseServer::client(self.fuse_server.clone()),
|
||||
mount_path,
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
*fuse_handle = Some(new_handle);
|
||||
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("start listening clipboard");
|
||||
clipboard.start();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set clipboard data from file list
|
||||
pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let prefix = self.fuse_mount_point.clone();
|
||||
let paths: Vec<PathBuf> = paths.iter().cloned().map(|p| prefix.join(p)).collect();
|
||||
log::debug!("setting clipboard with paths: {:?}", paths);
|
||||
self.clipboard.set_file_list(&paths)?;
|
||||
log::debug!("clipboard set, paths: {:?}", paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<(), CliprdrError> {
|
||||
let mut file_list = self.local_files.lock();
|
||||
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
let Some(file) = file_list.get(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = file_list.get_mut(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
if offset > file.size {
|
||||
log::error!("invalid reading offset requested from conn: {}", conn_id);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid reading offset requested from conn: {}",
|
||||
conn_id
|
||||
),
|
||||
});
|
||||
}
|
||||
let read_size = if offset + length > file.size {
|
||||
file.size - offset
|
||||
} else {
|
||||
length
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; read_size as usize];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
send_data(conn_id, file_contents_resp);
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn resp_file_contents_fail(conn_id: i32, stream_id: i32) {
|
||||
let resp = ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x2,
|
||||
stream_id,
|
||||
requested_data: vec![],
|
||||
};
|
||||
send_data(conn_id, resp)
|
||||
}
|
||||
|
||||
impl ClipboardContext {
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
self.fuse_handle.lock().is_none()
|
||||
}
|
||||
|
||||
pub fn sync_local_files(&self) -> Result<(), CliprdrError> {
|
||||
let mut local_files = self.local_files.lock();
|
||||
let clipboard_files = self.clipboard.get_file_list();
|
||||
let local_file_list: Vec<PathBuf> = local_files.iter().map(|f| f.path.clone()).collect();
|
||||
if local_file_list == clipboard_files {
|
||||
return Ok(());
|
||||
}
|
||||
let new_files = construct_file_list(&clipboard_files)?;
|
||||
*local_files = new_files;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
log::debug!("serve clipboard file from conn: {}", conn_id);
|
||||
if self.is_stopped() {
|
||||
log::debug!("cliprdr stopped, restart it");
|
||||
self.run()?;
|
||||
}
|
||||
match msg {
|
||||
ClipboardFile::NotifyCallback { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
ClipboardFile::MonitorReady => {
|
||||
log::debug!("server_monitor_ready called");
|
||||
|
||||
self.send_file_list(conn_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
log::debug!("server_format_list called");
|
||||
// filter out "FileGroupDescriptorW" and "FileContents"
|
||||
let fmt_lst: Vec<(i32, String)> = format_list
|
||||
.into_iter()
|
||||
.filter(|(_, name)| {
|
||||
name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME
|
||||
})
|
||||
.collect();
|
||||
if fmt_lst.len() != 2 {
|
||||
log::debug!("no supported formats");
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("supported formats: {:?}", fmt_lst);
|
||||
let file_contents_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap();
|
||||
let file_descriptor_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap();
|
||||
|
||||
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
||||
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
||||
|
||||
// sync file system from peer
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: file_descriptor_id,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FormatListResponse { msg_flags } => {
|
||||
log::debug!("server_format_list_response called");
|
||||
if msg_flags != 0x1 {
|
||||
send_format_list(conn_id)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ClipboardFile::FormatDataRequest {
|
||||
requested_format_id,
|
||||
} => {
|
||||
log::debug!("server_format_data_request called");
|
||||
let Some(format) = get_local_format(requested_format_id) else {
|
||||
log::error!(
|
||||
"got unsupported format data request: id={} from conn={}",
|
||||
requested_format_id,
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if format == FILEDESCRIPTORW_FORMAT_NAME {
|
||||
self.send_file_list(conn_id)?;
|
||||
} else if format == FILECONTENTS_FORMAT_NAME {
|
||||
log::error!(
|
||||
"try to read file contents with FormatDataRequest from conn={}",
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
} else {
|
||||
log::error!(
|
||||
"got unsupported format data request: id={} from conn={}",
|
||||
requested_format_id,
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
log::debug!(
|
||||
"server_format_data_response called, msg_flags={}",
|
||||
msg_flags
|
||||
);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
resp_format_data_failure(conn_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
// this must be a file descriptor format data
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.fuse_server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
log::debug!("load file list: {:?}", paths);
|
||||
self.set_clipboard(&paths)?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsResponse { .. } => {
|
||||
log::debug!("server_file_contents_response called");
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
self.fuse_tx.send(msg).map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
..
|
||||
} => {
|
||||
log::debug!("server_file_contents_request called");
|
||||
let fcr = if dw_flags == 0x1 {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
}
|
||||
} else if dw_flags == 0x2 {
|
||||
let offset = (n_position_high as u64) << 32 | n_position_low as u64;
|
||||
let length = cb_requested as u64;
|
||||
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
offset,
|
||||
length,
|
||||
}
|
||||
} else {
|
||||
log::error!("got invalid FileContentsRequest from conn={}", conn_id);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.serve_file_contents(conn_id, fcr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> {
|
||||
self.sync_local_files()?;
|
||||
|
||||
let file_list = self.local_files.lock();
|
||||
send_file_list(&*file_list, conn_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl CliprdrServiceContext for ClipboardContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
// unmount the fuse
|
||||
if let Some(fuse_handle) = self.fuse_handle.lock().take() {
|
||||
fuse_handle.join();
|
||||
}
|
||||
// we don't stop the clipboard, keep listening in case of restart
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
self.clipboard.set_file_list(&[])?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
self.serve(conn_id, msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn resp_format_data_failure(conn_id: i32) {
|
||||
let data = ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 0x2,
|
||||
format_data: vec![],
|
||||
};
|
||||
send_data(conn_id, data)
|
||||
}
|
||||
|
||||
fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!("send format list to remote, conn={}", conn_id);
|
||||
let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID)
|
||||
.unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string());
|
||||
let fc_format_name =
|
||||
get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string());
|
||||
let format_list = ClipboardFile::FormatList {
|
||||
format_list: vec![
|
||||
(FILEDESCRIPTOR_FORMAT_ID, fd_format_name),
|
||||
(FILECONTENTS_FORMAT_ID, fc_format_name),
|
||||
],
|
||||
};
|
||||
|
||||
send_data(conn_id, format_list);
|
||||
log::debug!("format list to remote dispatched, conn={}", conn_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(files: &[LocalFile]) -> Vec<u8> {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * files.len());
|
||||
data.put_u32_le(files.len() as u32);
|
||||
for file in files.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!(
|
||||
"send file list to remote, conn={}, list={:?}",
|
||||
conn_id,
|
||||
files.iter().map(|f| f.path.display()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let format_data = build_file_list_pdu(files);
|
||||
|
||||
send_data(
|
||||
conn_id,
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 1,
|
||||
format_data,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
97
libs/clipboard/src/platform/unix/ns_clipboard.rs
Normal file
97
libs/clipboard/src/platform/unix/ns_clipboard.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
|
||||
use cacao::pasteboard::{Pasteboard, PasteboardName};
|
||||
use hbb_common::log;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::SysClipboard;
|
||||
|
||||
#[inline]
|
||||
fn wait_file_list() -> Option<Vec<PathBuf>> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.get_file_urls()
|
||||
.ok()
|
||||
.map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.set_files(file_list.to_vec())
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
pub struct NsPasteboard {
|
||||
ignore_path: PathBuf,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl NsPasteboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for NsPasteboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
set_file_list(paths)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
|
||||
loop {
|
||||
let file_list = match wait_file_list() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let filtered = file_list
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
75
libs/clipboard/src/platform/unix/url.rs
Normal file
75
libs/clipboard/src/platform/unix/url.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::CliprdrError;
|
||||
|
||||
// on x11, path will be encode as
|
||||
// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
// url encode and decode is needed
|
||||
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||
|
||||
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String {
|
||||
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
|
||||
.to_string();
|
||||
format!("file://{}", encoded)
|
||||
}
|
||||
|
||||
pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result<PathBuf, CliprdrError> {
|
||||
let encoded_path = encoded_uri.trim_start_matches("file://");
|
||||
let path_str = percent_encoding::percent_decode_str(encoded_path)
|
||||
.decode_utf8()
|
||||
.map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
let path_str = path_str.to_string();
|
||||
|
||||
Ok(Path::new(&path_str).to_path_buf())
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) fn parse_plain_uri_list(v: Vec<u8>) -> Result<Vec<PathBuf>, CliprdrError> {
|
||||
let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
parse_uri_list(&text)
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) fn parse_uri_list(text: &str) -> Result<Vec<PathBuf>, CliprdrError> {
|
||||
let mut list = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
if !line.starts_with("file://") {
|
||||
continue;
|
||||
}
|
||||
let decoded = parse_uri_to_path(line)?;
|
||||
list.push(decoded)
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod uri_test {
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
||||
let uri = super::encode_path_to_uri(&path);
|
||||
assert_eq!(
|
||||
uri,
|
||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
);
|
||||
let convert_back = super::parse_uri_to_path(&uri).unwrap();
|
||||
assert_eq!(path, convert_back);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_list() {
|
||||
let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
"#;
|
||||
let list = super::parse_uri_list(uri_list.into()).unwrap();
|
||||
assert!(list.len() == 2);
|
||||
assert_eq!(list[0], list[1]);
|
||||
}
|
||||
}
|
162
libs/clipboard/src/platform/unix/x11.rs
Normal file
162
libs/clipboard/src/platform/unix/x11.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
|
||||
use hbb_common::log;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use x11_clipboard::Clipboard;
|
||||
use x11rb::protocol::xproto::Atom;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
|
||||
|
||||
static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new();
|
||||
|
||||
fn get_clip() -> Result<&'static Clipboard, CliprdrError> {
|
||||
X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit))
|
||||
}
|
||||
|
||||
pub struct X11Clipboard {
|
||||
ignore_path: PathBuf,
|
||||
text_uri_list: Atom,
|
||||
gnome_copied_files: Atom,
|
||||
nautilus_clipboard: Atom,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl X11Clipboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
let clipboard = get_clip()?;
|
||||
let text_uri_list = clipboard
|
||||
.setter
|
||||
.get_atom("text/uri-list")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
let gnome_copied_files = clipboard
|
||||
.setter
|
||||
.get_atom("x-special/gnome-copied-files")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
let nautilus_clipboard = clipboard
|
||||
.setter
|
||||
.get_atom("x-special/nautilus-clipboard")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
text_uri_list,
|
||||
gnome_copied_files,
|
||||
nautilus_clipboard,
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
|
||||
fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
let prop = get_clip()?.setter.atoms.property;
|
||||
// NOTE:
|
||||
// # why not use `load_wait`
|
||||
// load_wait is likely to wait forever, which is not what we want
|
||||
let res = get_clip()?.load_wait(clip, target, prop);
|
||||
match res {
|
||||
Ok(res) => Ok(res),
|
||||
Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]),
|
||||
Err(x11_clipboard::error::Error::Timeout) => {
|
||||
log::debug!("x11 clipboard get content timeout.");
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("x11 clipboard get content fail: {:?}", e);
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
log::debug!("try to store clipboard content");
|
||||
get_clip()?
|
||||
.store_batch(clip, batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn wait_file_list(&self) -> Result<Option<Vec<PathBuf>>, CliprdrError> {
|
||||
let v = self.load(self.text_uri_list)?;
|
||||
let p = parse_plain_uri_list(v)?;
|
||||
Ok(Some(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for X11Clipboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
|
||||
let uri_list: Vec<String> = paths.iter().map(encode_path_to_uri).collect();
|
||||
let uri_list = uri_list.join("\n");
|
||||
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
||||
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();
|
||||
let batch = vec![
|
||||
(self.text_uri_list, text_uri_list_data),
|
||||
(self.gnome_copied_files, gnome_copied_files_data.clone()),
|
||||
(self.nautilus_clipboard, gnome_copied_files_data),
|
||||
];
|
||||
self.store_batch(batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
// clear cached file list
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
loop {
|
||||
let sth = match self.wait_file_list() {
|
||||
Ok(sth) => sth,
|
||||
Err(e) => {
|
||||
log::warn!("failed to get file list from clipboard: {}", e);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(paths) = sth else {
|
||||
// just sleep
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
};
|
||||
|
||||
let filtered = paths
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
1219
libs/clipboard/src/platform/windows.rs
Normal file
1219
libs/clipboard/src/platform/windows.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ edition = "2018"
|
||||
[dependencies]
|
||||
flexi_logger = { version = "0.25", features = ["async"] }
|
||||
protobuf = { version = "3.2", features = ["with-bytes"] }
|
||||
tokio = { version = "1.28", features = ["full"] }
|
||||
tokio = { version = "=1.28.1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
futures = "0.3"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
|
@ -17,6 +17,11 @@ message YUV {
|
||||
int32 stride = 2;
|
||||
}
|
||||
|
||||
enum Chroma {
|
||||
I420 = 0;
|
||||
I444 = 1;
|
||||
}
|
||||
|
||||
message VideoFrame {
|
||||
oneof union {
|
||||
EncodedVideoFrames vp9s = 6;
|
||||
@ -83,11 +88,20 @@ message Features {
|
||||
bool privacy_mode = 1;
|
||||
}
|
||||
|
||||
message CodecAbility {
|
||||
bool vp8 = 1;
|
||||
bool vp9 = 2;
|
||||
bool av1 = 3;
|
||||
bool h264 = 4;
|
||||
bool h265 = 5;
|
||||
}
|
||||
|
||||
message SupportedEncoding {
|
||||
bool h264 = 1;
|
||||
bool h265 = 2;
|
||||
bool vp8 = 3;
|
||||
bool av1 = 4;
|
||||
CodecAbility i444 = 5;
|
||||
}
|
||||
|
||||
message PeerInfo {
|
||||
@ -512,6 +526,7 @@ message PermissionInfo {
|
||||
File = 4;
|
||||
Restart = 5;
|
||||
Recording = 6;
|
||||
BlockInput = 7;
|
||||
}
|
||||
|
||||
Permission permission = 1;
|
||||
@ -541,6 +556,8 @@ message SupportedDecoding {
|
||||
PreferCodec prefer = 4;
|
||||
int32 ability_vp8 = 5;
|
||||
int32 ability_av1 = 6;
|
||||
CodecAbility i444 = 7;
|
||||
Chroma prefer_chroma = 8;
|
||||
}
|
||||
|
||||
message OptionMessage {
|
||||
|
@ -91,10 +91,11 @@ const CHARS: &[char] = &[
|
||||
];
|
||||
|
||||
pub const RENDEZVOUS_SERVERS: &[&str] = &["rs-ny.rustdesk.com"];
|
||||
pub const PUBLIC_RS_PUB_KEY: &str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
|
||||
|
||||
pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") {
|
||||
Some(key) if !key.is_empty() => key,
|
||||
_ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=",
|
||||
_ => PUBLIC_RS_PUB_KEY,
|
||||
};
|
||||
|
||||
pub const RENDEZVOUS_PORT: i32 = 21116;
|
||||
@ -1229,6 +1230,10 @@ impl PeerConfig {
|
||||
if !mp.contains_key(key) {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
|
||||
}
|
||||
key = "i444";
|
||||
if !mp.contains_key(key) {
|
||||
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from ast import parse
|
||||
import os
|
||||
import optparse
|
||||
from hashlib import md5
|
||||
@ -47,7 +46,7 @@ def write_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||
f.write((len(path)).to_bytes(length=length_count, byteorder='big'))
|
||||
f.write(path)
|
||||
# data length & compressed data
|
||||
f.write((data_length).to_bytes(
|
||||
f.write(data_length.to_bytes(
|
||||
length=length_count, byteorder='big'))
|
||||
f.write(compressed_data)
|
||||
# md5 code
|
||||
@ -65,6 +64,8 @@ def build_portable(output_folder: str):
|
||||
|
||||
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
|
||||
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option("-f", "--folder", dest="folder",
|
||||
|
@ -197,6 +197,7 @@ fn main() {
|
||||
find_package("libyuv");
|
||||
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
|
||||
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
|
||||
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
|
||||
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
|
@ -1,13 +1,20 @@
|
||||
use docopt::Docopt;
|
||||
use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV};
|
||||
use hbb_common::{
|
||||
env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV},
|
||||
log,
|
||||
};
|
||||
use scrap::{
|
||||
aom::{AomDecoder, AomEncoder, AomEncoderConfig},
|
||||
codec::{EncoderApi, EncoderCfg, Quality as Q},
|
||||
Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
|
||||
convert_to_yuv, Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder,
|
||||
VpxEncoderConfig,
|
||||
VpxVideoCodecId::{self, *},
|
||||
STRIDE_ALIGN,
|
||||
};
|
||||
use std::{io::Write, time::Instant};
|
||||
use std::{
|
||||
io::Write,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
// cargo run --package scrap --example benchmark --release --features hwcodec
|
||||
|
||||
@ -15,7 +22,7 @@ const USAGE: &'static str = "
|
||||
Codec benchmark.
|
||||
|
||||
Usage:
|
||||
benchmark [--count=COUNT] [--quality=QUALITY] [--hw-pixfmt=PIXFMT]
|
||||
benchmark [--count=COUNT] [--quality=QUALITY] [--i444]
|
||||
benchmark (-h | --help)
|
||||
|
||||
Options:
|
||||
@ -23,24 +30,17 @@ Options:
|
||||
--count=COUNT Capture frame count [default: 100].
|
||||
--quality=QUALITY Video quality [default: Balanced].
|
||||
Valid values: Best, Balanced, Low.
|
||||
--hw-pixfmt=PIXFMT Hardware codec pixfmt. [default: i420]
|
||||
Valid values: i420, nv12.
|
||||
--i444 I444.
|
||||
";
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Deserialize, Clone, Copy)]
|
||||
struct Args {
|
||||
flag_count: usize,
|
||||
flag_quality: Quality,
|
||||
flag_hw_pixfmt: Pixfmt,
|
||||
flag_i444: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
enum Pixfmt {
|
||||
I420,
|
||||
NV12,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Deserialize, Clone, Copy)]
|
||||
enum Quality {
|
||||
Best,
|
||||
Balanced,
|
||||
@ -54,31 +54,6 @@ fn main() {
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
let quality = args.flag_quality;
|
||||
let yuv_count = args.flag_count;
|
||||
let (yuvs, width, height) = capture_yuv(yuv_count);
|
||||
println!(
|
||||
"benchmark {}x{} quality:{:?}k hw_pixfmt:{:?}",
|
||||
width, height, quality, args.flag_hw_pixfmt
|
||||
);
|
||||
let quality = match quality {
|
||||
Quality::Best => Q::Best,
|
||||
Quality::Balanced => Q::Balanced,
|
||||
Quality::Low => Q::Low,
|
||||
};
|
||||
[VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, quality, yuv_count));
|
||||
test_av1(&yuvs, width, height, quality, yuv_count);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
use hwcodec::AVPixelFormat;
|
||||
let hw_pixfmt = match args.flag_hw_pixfmt {
|
||||
Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
};
|
||||
let yuvs = hw::vpx_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
hw::test(&yuvs, width, height, quality, yuv_count, hw_pixfmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_yuv(yuv_count: usize) -> (Vec<Vec<u8>>, usize, usize) {
|
||||
let mut index = 0;
|
||||
let mut displays = Display::all().unwrap();
|
||||
for i in 0..displays.len() {
|
||||
@ -88,28 +63,45 @@ fn capture_yuv(yuv_count: usize) -> (Vec<Vec<u8>>, usize, usize) {
|
||||
}
|
||||
}
|
||||
let d = displays.remove(index);
|
||||
let mut c = Capturer::new(d, true).unwrap();
|
||||
let mut v = vec![];
|
||||
loop {
|
||||
if let Ok(frame) = c.frame(std::time::Duration::from_millis(30)) {
|
||||
v.push(frame.0.to_vec());
|
||||
print!("\rcapture {}/{}", v.len(), yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
if v.len() == yuv_count {
|
||||
println!();
|
||||
return (v, c.width(), c.height());
|
||||
}
|
||||
}
|
||||
let mut c = Capturer::new(d).unwrap();
|
||||
let width = c.width();
|
||||
let height = c.height();
|
||||
|
||||
println!(
|
||||
"benchmark {}x{} quality:{:?}, i444:{:?}",
|
||||
width, height, quality, args.flag_i444
|
||||
);
|
||||
let quality = match quality {
|
||||
Quality::Best => Q::Best,
|
||||
Quality::Balanced => Q::Balanced,
|
||||
Quality::Low => Q::Low,
|
||||
};
|
||||
[VP8, VP9].map(|codec| {
|
||||
test_vpx(
|
||||
&mut c,
|
||||
codec,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
yuv_count,
|
||||
if codec == VP8 { false } else { args.flag_i444 },
|
||||
)
|
||||
});
|
||||
test_av1(&mut c, width, height, quality, yuv_count, args.flag_i444);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
hw::test(&mut c, width, height, quality, yuv_count);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vpx(
|
||||
c: &mut Capturer,
|
||||
codec_id: VpxVideoCodecId,
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
yuv_count: usize,
|
||||
i444: bool,
|
||||
) {
|
||||
let config = EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: width as _,
|
||||
@ -118,28 +110,53 @@ fn test_vpx(
|
||||
codec: codec_id,
|
||||
keyframe_interval: None,
|
||||
});
|
||||
let mut encoder = VpxEncoder::new(config).unwrap();
|
||||
let mut encoder = VpxEncoder::new(config, i444).unwrap();
|
||||
let mut vpxs = vec![];
|
||||
let start = Instant::now();
|
||||
let mut size = 0;
|
||||
for yuv in yuvs {
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
size += frame.data.len();
|
||||
vpxs.push(frame.data.to_vec());
|
||||
let mut yuv = Vec::new();
|
||||
let mut mid_data = Vec::new();
|
||||
let mut counter = 0;
|
||||
let mut time_sum = Duration::ZERO;
|
||||
loop {
|
||||
match c.frame(std::time::Duration::from_millis(30)) {
|
||||
Ok(frame) => {
|
||||
let tmp_timer = Instant::now();
|
||||
convert_to_yuv(&frame, encoder.yuvfmt(), &mut yuv, &mut mid_data);
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, &yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
size += frame.data.len();
|
||||
vpxs.push(frame.data.to_vec());
|
||||
counter += 1;
|
||||
print!("\r{codec_id:?} {}/{}", counter, yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
for ref frame in encoder.flush().unwrap() {
|
||||
size += frame.data.len();
|
||||
vpxs.push(frame.data.to_vec());
|
||||
counter += 1;
|
||||
print!("\r{codec_id:?} {}/{}", counter, yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
time_sum += tmp_timer.elapsed();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
for ref frame in encoder.flush().unwrap() {
|
||||
size += frame.data.len();
|
||||
vpxs.push(frame.data.to_vec());
|
||||
if counter >= yuv_count {
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(vpxs.len(), yuv_count);
|
||||
println!(
|
||||
"{:?} encode: {:?}, {} byte",
|
||||
codec_id,
|
||||
start.elapsed() / yuv_count as _,
|
||||
time_sum / yuv_count as _,
|
||||
size / yuv_count
|
||||
);
|
||||
|
||||
@ -156,30 +173,58 @@ fn test_vpx(
|
||||
);
|
||||
}
|
||||
|
||||
fn test_av1(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, quality: Q, yuv_count: usize) {
|
||||
fn test_av1(
|
||||
c: &mut Capturer,
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
yuv_count: usize,
|
||||
i444: bool,
|
||||
) {
|
||||
let config = EncoderCfg::AOM(AomEncoderConfig {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
quality,
|
||||
keyframe_interval: None,
|
||||
});
|
||||
let mut encoder = AomEncoder::new(config).unwrap();
|
||||
let mut encoder = AomEncoder::new(config, i444).unwrap();
|
||||
let start = Instant::now();
|
||||
let mut size = 0;
|
||||
let mut av1s = vec![];
|
||||
for yuv in yuvs {
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
size += frame.data.len();
|
||||
av1s.push(frame.data.to_vec());
|
||||
let mut av1s: Vec<Vec<u8>> = vec![];
|
||||
let mut yuv = Vec::new();
|
||||
let mut mid_data = Vec::new();
|
||||
let mut counter = 0;
|
||||
let mut time_sum = Duration::ZERO;
|
||||
loop {
|
||||
match c.frame(std::time::Duration::from_millis(30)) {
|
||||
Ok(frame) => {
|
||||
let tmp_timer = Instant::now();
|
||||
convert_to_yuv(&frame, encoder.yuvfmt(), &mut yuv, &mut mid_data);
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, &yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
size += frame.data.len();
|
||||
av1s.push(frame.data.to_vec());
|
||||
counter += 1;
|
||||
print!("\rAV1 {}/{}", counter, yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
time_sum += tmp_timer.elapsed();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
if counter >= yuv_count {
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(av1s.len(), yuv_count);
|
||||
println!(
|
||||
"AV1 encode: {:?}, {} byte",
|
||||
start.elapsed() / yuv_count as _,
|
||||
time_sum / yuv_count as _,
|
||||
size / yuv_count
|
||||
);
|
||||
let mut decoder = AomDecoder::new().unwrap();
|
||||
@ -193,165 +238,101 @@ fn test_av1(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, quality: Q, yuv_co
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
mod hw {
|
||||
use super::*;
|
||||
use hwcodec::{
|
||||
decode::{DecodeContext, Decoder},
|
||||
encode::{EncodeContext, Encoder},
|
||||
ffmpeg::{ffmpeg_linesize_offset_length, CodecInfo, CodecInfos},
|
||||
AVPixelFormat,
|
||||
Quality::*,
|
||||
RateControl::*,
|
||||
};
|
||||
use hwcodec::ffmpeg::CodecInfo;
|
||||
use scrap::{
|
||||
codec::codec_thread_num,
|
||||
convert::{
|
||||
hw::{hw_bgra_to_i420, hw_bgra_to_nv12},
|
||||
i420_to_bgra,
|
||||
},
|
||||
HW_STRIDE_ALIGN,
|
||||
codec::HwEncoderConfig,
|
||||
hwcodec::{HwDecoder, HwEncoder},
|
||||
};
|
||||
|
||||
pub fn test(
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
use super::*;
|
||||
|
||||
pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) {
|
||||
let best = HwEncoder::best();
|
||||
let mut h264s = Vec::new();
|
||||
let mut h265s = Vec::new();
|
||||
if let Some(info) = best.h264 {
|
||||
test_encoder(width, height, quality, info, c, yuv_count, &mut h264s);
|
||||
}
|
||||
if let Some(info) = best.h265 {
|
||||
test_encoder(width, height, quality, info, c, yuv_count, &mut h265s);
|
||||
}
|
||||
let best = HwDecoder::best();
|
||||
if let Some(info) = best.h264 {
|
||||
test_decoder(info, &h264s);
|
||||
}
|
||||
if let Some(info) = best.h265 {
|
||||
test_decoder(info, &h265s);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_encoder(
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
info: CodecInfo,
|
||||
c: &mut Capturer,
|
||||
yuv_count: usize,
|
||||
pixfmt: AVPixelFormat,
|
||||
h26xs: &mut Vec<Vec<u8>>,
|
||||
) {
|
||||
let bitrate = scrap::hwcodec::HwEncoder::convert_quality(quality);
|
||||
let ctx = EncodeContext {
|
||||
name: String::from(""),
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
pixfmt,
|
||||
align: 0,
|
||||
bitrate: bitrate as i32 * 1000,
|
||||
timebase: [1, 30],
|
||||
gop: 60,
|
||||
quality: Quality_Default,
|
||||
rc: RC_DEFAULT,
|
||||
thread_count: codec_thread_num() as _,
|
||||
};
|
||||
|
||||
let encoders = Encoder::available_encoders(ctx.clone());
|
||||
println!("hw encoders: {}", encoders.len());
|
||||
let best = CodecInfo::score(encoders.clone());
|
||||
for info in encoders {
|
||||
test_encoder(info.clone(), ctx.clone(), yuvs, is_best(&best, &info));
|
||||
}
|
||||
|
||||
let (h264s, h265s) = prepare_h26x(best, ctx.clone(), yuvs);
|
||||
assert!(h264s.is_empty() || h264s.len() == yuv_count);
|
||||
assert!(h265s.is_empty() || h265s.len() == yuv_count);
|
||||
let decoders = Decoder::available_decoders();
|
||||
println!("hw decoders: {}", decoders.len());
|
||||
let best = CodecInfo::score(decoders.clone());
|
||||
for info in decoders {
|
||||
let h26xs = if info.name.contains("h264") {
|
||||
&h264s
|
||||
} else {
|
||||
&h265s
|
||||
};
|
||||
if h26xs.len() == yuvs.len() {
|
||||
test_decoder(info.clone(), h26xs, is_best(&best, &info));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_encoder(info: CodecInfo, ctx: EncodeContext, yuvs: &Vec<Vec<u8>>, best: bool) {
|
||||
let mut ctx = ctx;
|
||||
ctx.name = info.name;
|
||||
let mut encoder = Encoder::new(ctx.clone()).unwrap();
|
||||
let start = Instant::now();
|
||||
let mut encoder = HwEncoder::new(
|
||||
EncoderCfg::HW(HwEncoderConfig {
|
||||
name: info.name.clone(),
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
keyframe_interval: None,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let mut size = 0;
|
||||
for yuv in yuvs {
|
||||
let frames = encoder.encode(yuv).unwrap();
|
||||
for frame in frames {
|
||||
size += frame.data.len();
|
||||
|
||||
let mut yuv = Vec::new();
|
||||
let mut mid_data = Vec::new();
|
||||
let mut counter = 0;
|
||||
let mut time_sum = Duration::ZERO;
|
||||
loop {
|
||||
match c.frame(std::time::Duration::from_millis(30)) {
|
||||
Ok(frame) => {
|
||||
let tmp_timer = Instant::now();
|
||||
convert_to_yuv(&frame, encoder.yuvfmt(), &mut yuv, &mut mid_data);
|
||||
for ref frame in encoder.encode(&yuv).unwrap() {
|
||||
size += frame.data.len();
|
||||
|
||||
h26xs.push(frame.data.to_vec());
|
||||
counter += 1;
|
||||
print!("\r{:?} {}/{}", info.name, counter, yuv_count);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
time_sum += tmp_timer.elapsed();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
if counter >= yuv_count {
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{}{}: {:?}, {} byte",
|
||||
if best { "*" } else { "" },
|
||||
ctx.name,
|
||||
start.elapsed() / yuvs.len() as _,
|
||||
size / yuvs.len(),
|
||||
"{}: {:?}, {} byte",
|
||||
info.name,
|
||||
time_sum / yuv_count as u32,
|
||||
size / yuv_count,
|
||||
);
|
||||
}
|
||||
|
||||
fn test_decoder(info: CodecInfo, h26xs: &Vec<Vec<u8>>, best: bool) {
|
||||
let ctx = DecodeContext {
|
||||
name: info.name,
|
||||
device_type: info.hwdevice,
|
||||
thread_count: codec_thread_num() as _,
|
||||
};
|
||||
|
||||
let mut decoder = Decoder::new(ctx.clone()).unwrap();
|
||||
fn test_decoder(info: CodecInfo, h26xs: &Vec<Vec<u8>>) {
|
||||
let mut decoder = HwDecoder::new(info.clone()).unwrap();
|
||||
let start = Instant::now();
|
||||
let mut cnt = 0;
|
||||
for h26x in h26xs {
|
||||
let _ = decoder.decode(h26x).unwrap();
|
||||
cnt += 1;
|
||||
}
|
||||
let device = format!("{:?}", ctx.device_type).to_lowercase();
|
||||
let device = format!("{:?}", info.hwdevice).to_lowercase();
|
||||
let device = device.split("_").last().unwrap();
|
||||
println!(
|
||||
"{}{} {}: {:?}",
|
||||
if best { "*" } else { "" },
|
||||
ctx.name,
|
||||
device,
|
||||
start.elapsed() / cnt
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_h26x(
|
||||
best: CodecInfos,
|
||||
ctx: EncodeContext,
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
) -> (Vec<Vec<u8>>, Vec<Vec<u8>>) {
|
||||
let f = |info: Option<CodecInfo>| {
|
||||
let mut h26xs = vec![];
|
||||
if let Some(info) = info {
|
||||
let mut ctx = ctx.clone();
|
||||
ctx.name = info.name;
|
||||
let mut encoder = Encoder::new(ctx).unwrap();
|
||||
for yuv in yuvs {
|
||||
let h26x = encoder.encode(yuv).unwrap();
|
||||
for frame in h26x {
|
||||
h26xs.push(frame.data.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
h26xs
|
||||
};
|
||||
(f(best.h264), f(best.h265))
|
||||
}
|
||||
|
||||
fn is_best(best: &CodecInfos, info: &CodecInfo) -> bool {
|
||||
Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265
|
||||
}
|
||||
|
||||
pub fn vpx_yuv_to_hw_yuv(
|
||||
yuvs: Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixfmt: AVPixelFormat,
|
||||
) -> Vec<Vec<u8>> {
|
||||
let yuvs = yuvs;
|
||||
let mut bgra = vec![];
|
||||
let mut v = vec![];
|
||||
let (linesize, offset, length) =
|
||||
ffmpeg_linesize_offset_length(pixfmt, width, height, HW_STRIDE_ALIGN).unwrap();
|
||||
for mut yuv in yuvs {
|
||||
i420_to_bgra(width, height, &yuv, &mut bgra);
|
||||
if pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P {
|
||||
hw_bgra_to_i420(width, height, &linesize, &offset, length, &bgra, &mut yuv);
|
||||
} else {
|
||||
hw_bgra_to_nv12(width, height, &linesize, &offset, length, &bgra, &mut yuv);
|
||||
}
|
||||
v.push(yuv);
|
||||
}
|
||||
v
|
||||
println!("{} {}: {:?}", info.name, device, start.elapsed() / cnt);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ extern crate scrap;
|
||||
|
||||
use scrap::Display;
|
||||
#[cfg(windows)]
|
||||
use scrap::{i420_to_rgb, CapturerMag, TraitCapturer};
|
||||
use scrap::{CapturerMag, TraitCapturer};
|
||||
#[cfg(windows)]
|
||||
use std::fs::File;
|
||||
|
||||
@ -24,6 +24,8 @@ fn get_display(i: usize) -> Display {
|
||||
fn record(i: usize) {
|
||||
use std::time::Duration;
|
||||
|
||||
use scrap::TraitFrame;
|
||||
|
||||
for d in Display::all().unwrap() {
|
||||
println!("{:?} {} {}", d.origin(), d.width(), d.height());
|
||||
}
|
||||
@ -32,9 +34,8 @@ fn record(i: usize) {
|
||||
let (w, h) = (display.width(), display.height());
|
||||
|
||||
{
|
||||
let mut capture_mag =
|
||||
CapturerMag::new(display.origin(), display.width(), display.height(), false)
|
||||
.expect("Couldn't begin capture.");
|
||||
let mut capture_mag = CapturerMag::new(display.origin(), display.width(), display.height())
|
||||
.expect("Couldn't begin capture.");
|
||||
let wnd_cls = "";
|
||||
let wnd_name = "RustDeskPrivacyWindow";
|
||||
if false == capture_mag.exclude(wnd_cls, wnd_name).unwrap() {
|
||||
@ -43,7 +44,8 @@ fn record(i: usize) {
|
||||
println!("Filter window for cls {} name {}", wnd_cls, wnd_name);
|
||||
}
|
||||
|
||||
let frame = capture_mag.frame(Duration::from_millis(0)).unwrap();
|
||||
let captured_frame = capture_mag.frame(Duration::from_millis(0)).unwrap();
|
||||
let frame = captured_frame.data();
|
||||
println!("Capture data len: {}, Saving...", frame.len());
|
||||
|
||||
let mut bitflipped = Vec::with_capacity(w * h * 4);
|
||||
@ -68,9 +70,8 @@ fn record(i: usize) {
|
||||
}
|
||||
|
||||
{
|
||||
let mut capture_mag =
|
||||
CapturerMag::new(display.origin(), display.width(), display.height(), true)
|
||||
.expect("Couldn't begin capture.");
|
||||
let mut capture_mag = CapturerMag::new(display.origin(), display.width(), display.height())
|
||||
.expect("Couldn't begin capture.");
|
||||
let wnd_cls = "";
|
||||
let wnd_title = "RustDeskPrivacyWindow";
|
||||
if false == capture_mag.exclude(wnd_cls, wnd_title).unwrap() {
|
||||
@ -79,19 +80,28 @@ fn record(i: usize) {
|
||||
println!("Filter window for cls {} title {}", wnd_cls, wnd_title);
|
||||
}
|
||||
|
||||
let buffer = capture_mag.frame(Duration::from_millis(0)).unwrap();
|
||||
println!("Capture data len: {}, Saving...", buffer.len());
|
||||
let frame = capture_mag.frame(Duration::from_millis(0)).unwrap();
|
||||
println!("Capture data len: {}, Saving...", frame.data().len());
|
||||
|
||||
let mut frame = Default::default();
|
||||
i420_to_rgb(w, h, &buffer, &mut frame);
|
||||
let mut raw = Vec::new();
|
||||
unsafe {
|
||||
scrap::ARGBToRAW(
|
||||
frame.data().as_ptr(),
|
||||
frame.stride()[0] as _,
|
||||
(&mut raw).as_mut_ptr(),
|
||||
(w * 3) as _,
|
||||
w as _,
|
||||
h as _,
|
||||
)
|
||||
};
|
||||
|
||||
let mut bitflipped = Vec::with_capacity(w * h * 4);
|
||||
let stride = frame.len() / h;
|
||||
let stride = raw.len() / h;
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = stride * y + 3 * x;
|
||||
bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]);
|
||||
bitflipped.extend_from_slice(&[raw[i], raw[i + 1], raw[i + 2], 255]);
|
||||
}
|
||||
}
|
||||
let name = format!("capture_mag_{}_2.png", i);
|
||||
|
@ -1,5 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use scrap::TraitFrame;
|
||||
|
||||
extern crate scrap;
|
||||
|
||||
fn main() {
|
||||
@ -27,16 +29,16 @@ fn main() {
|
||||
.spawn()
|
||||
.expect("This example requires ffplay.");
|
||||
|
||||
let mut capturer = Capturer::new(d, false).unwrap();
|
||||
let mut capturer = Capturer::new(d).unwrap();
|
||||
let mut out = child.stdin.unwrap();
|
||||
|
||||
loop {
|
||||
match capturer.frame(Duration::from_millis(0)) {
|
||||
Ok(frame) => {
|
||||
// Write the frame, removing end-of-row padding.
|
||||
let stride = frame.len() / h;
|
||||
let stride = frame.stride()[0];
|
||||
let rowlen = 4 * w;
|
||||
for row in frame.chunks(stride) {
|
||||
for row in frame.data().chunks(stride) {
|
||||
let row = &row[..rowlen];
|
||||
out.write_all(row).unwrap();
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q};
|
||||
use webm::mux;
|
||||
use webm::mux::Track;
|
||||
|
||||
use scrap::vpxcodec as vpx_encode;
|
||||
use scrap::{convert_to_yuv, vpxcodec as vpx_encode};
|
||||
use scrap::{Capturer, Display, TraitCapturer, STRIDE_ALIGN};
|
||||
|
||||
const USAGE: &'static str = "
|
||||
@ -110,13 +110,16 @@ fn main() -> io::Result<()> {
|
||||
Quality::Balanced => Q::Balanced,
|
||||
Quality::Low => Q::Low,
|
||||
};
|
||||
let mut vpx = vpx_encode::VpxEncoder::new(EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
codec: vpx_codec,
|
||||
keyframe_interval: None,
|
||||
}))
|
||||
let mut vpx = vpx_encode::VpxEncoder::new(
|
||||
EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
codec: vpx_codec,
|
||||
keyframe_interval: None,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Start recording.
|
||||
@ -136,7 +139,9 @@ fn main() -> io::Result<()> {
|
||||
let spf = Duration::from_nanos(1_000_000_000 / args.flag_fps);
|
||||
|
||||
// Capturer object is expensive, avoiding to create it frequently.
|
||||
let mut c = Capturer::new(d, true).unwrap();
|
||||
let mut c = Capturer::new(d).unwrap();
|
||||
let mut yuv = Vec::new();
|
||||
let mut mid_data = Vec::new();
|
||||
while !stop.load(Ordering::Acquire) {
|
||||
let now = Instant::now();
|
||||
let time = now - start;
|
||||
@ -147,8 +152,8 @@ fn main() -> io::Result<()> {
|
||||
|
||||
if let Ok(frame) = c.frame(Duration::from_millis(0)) {
|
||||
let ms = time.as_secs() * 1000 + time.subsec_millis() as u64;
|
||||
|
||||
for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() {
|
||||
convert_to_yuv(&frame, vpx.yuvfmt(), &mut yuv, &mut mid_data);
|
||||
for frame in vpx.encode(ms as i64, &yuv, STRIDE_ALIGN).unwrap() {
|
||||
vt.add_frame(frame.data, frame.pts as u64 * 1_000_000, frame.key);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use std::io::ErrorKind::WouldBlock;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use scrap::{i420_to_rgb, Capturer, Display, TraitCapturer};
|
||||
use scrap::{Capturer, Display, TraitCapturer, TraitFrame};
|
||||
|
||||
fn main() {
|
||||
let n = Display::all().unwrap().len();
|
||||
@ -28,14 +28,14 @@ fn record(i: usize) {
|
||||
}
|
||||
|
||||
let display = get_display(i);
|
||||
let mut capturer = Capturer::new(display, false).expect("Couldn't begin capture.");
|
||||
let mut capturer = Capturer::new(display).expect("Couldn't begin capture.");
|
||||
let (w, h) = (capturer.width(), capturer.height());
|
||||
|
||||
loop {
|
||||
// Wait until there's a frame.
|
||||
|
||||
let buffer = match capturer.frame(Duration::from_millis(0)) {
|
||||
Ok(buffer) => buffer,
|
||||
let frame = match capturer.frame(Duration::from_millis(0)) {
|
||||
Ok(frame) => frame,
|
||||
Err(error) => {
|
||||
if error.kind() == WouldBlock {
|
||||
// Keep spinning.
|
||||
@ -46,6 +46,7 @@ fn record(i: usize) {
|
||||
}
|
||||
}
|
||||
};
|
||||
let buffer = frame.data();
|
||||
println!("Captured data len: {}, Saving...", buffer.len());
|
||||
|
||||
// Flip the BGRA image into a RGBA image.
|
||||
@ -77,14 +78,14 @@ fn record(i: usize) {
|
||||
|
||||
drop(capturer);
|
||||
let display = get_display(i);
|
||||
let mut capturer = Capturer::new(display, true).expect("Couldn't begin capture.");
|
||||
let mut capturer = Capturer::new(display).expect("Couldn't begin capture.");
|
||||
let (w, h) = (capturer.width(), capturer.height());
|
||||
|
||||
loop {
|
||||
// Wait until there's a frame.
|
||||
|
||||
let buffer = match capturer.frame(Duration::from_millis(0)) {
|
||||
Ok(buffer) => buffer,
|
||||
let frame = match capturer.frame(Duration::from_millis(0)) {
|
||||
Ok(frame) => frame,
|
||||
Err(error) => {
|
||||
if error.kind() == WouldBlock {
|
||||
// Keep spinning.
|
||||
@ -95,18 +96,28 @@ fn record(i: usize) {
|
||||
}
|
||||
}
|
||||
};
|
||||
let buffer = frame.data();
|
||||
println!("Captured data len: {}, Saving...", buffer.len());
|
||||
|
||||
let mut frame = Default::default();
|
||||
i420_to_rgb(w, h, &buffer, &mut frame);
|
||||
let mut raw = Vec::new();
|
||||
unsafe {
|
||||
scrap::ARGBToRAW(
|
||||
buffer.as_ptr(),
|
||||
frame.stride()[0] as _,
|
||||
(&mut raw).as_mut_ptr(),
|
||||
(w * 3) as _,
|
||||
w as _,
|
||||
h as _,
|
||||
)
|
||||
};
|
||||
|
||||
let mut bitflipped = Vec::with_capacity(w * h * 4);
|
||||
let stride = frame.len() / h;
|
||||
let stride = raw.len() / h;
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = stride * y + 3 * x;
|
||||
bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]);
|
||||
bitflipped.extend_from_slice(&[raw[i], raw[i + 1], raw[i + 2], 255]);
|
||||
}
|
||||
}
|
||||
let name = format!("screenshot{}_2.png", i);
|
||||
|
6
libs/scrap/src/bindings/yuv_ffi.h
Normal file
6
libs/scrap/src/bindings/yuv_ffi.h
Normal file
@ -0,0 +1,6 @@
|
||||
#include <libyuv/convert.h>
|
||||
#include <libyuv/convert_argb.h>
|
||||
#include <libyuv/convert_from.h>
|
||||
#include <libyuv/convert_from_argb.h>
|
||||
#include <libyuv/rotate.h>
|
||||
#include <libyuv/rotate_argb.h>
|
@ -1,5 +1,5 @@
|
||||
use crate::android::ffi::*;
|
||||
use crate::rgba_to_i420;
|
||||
use crate::Pixfmt;
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
@ -12,15 +12,15 @@ lazy_static! {
|
||||
|
||||
pub struct Capturer {
|
||||
display: Display,
|
||||
bgra: Vec<u8>,
|
||||
rgba: Vec<u8>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, _yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
Ok(Capturer {
|
||||
display,
|
||||
bgra: Vec::new(),
|
||||
rgba: Vec::new(),
|
||||
saved_raw_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
@ -35,22 +35,62 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl crate::TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, _use_yuv: bool) {}
|
||||
|
||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
if let Some(buf) = get_video_raw() {
|
||||
crate::would_block_if_equal(&mut self.saved_raw_data, buf)?;
|
||||
rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra);
|
||||
Ok(Frame::RAW(&self.bgra))
|
||||
// Is it safe to directly return buf without copy?
|
||||
self.rgba.resize(buf.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(buf.as_ptr(), self.rgba.as_mut_ptr(), buf.len())
|
||||
};
|
||||
Ok(Frame::new(&self.rgba, self.width(), self.height()))
|
||||
} else {
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Frame<'a> {
|
||||
RAW(&'a [u8]),
|
||||
Empty,
|
||||
pub struct Frame<'a> {
|
||||
data: &'a [u8],
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Frame<'a> {
|
||||
pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
|
||||
let stride0 = data.len() / height;
|
||||
let mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
Frame {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::TraitFrame for Frame<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::RGBA
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display {
|
||||
|
@ -9,11 +9,12 @@ include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs"));
|
||||
use crate::codec::{base_bitrate, codec_thread_num, Quality};
|
||||
use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN};
|
||||
use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result};
|
||||
use crate::{EncodeYuvFormat, Pixfmt};
|
||||
use hbb_common::{
|
||||
anyhow::{anyhow, Context},
|
||||
bytes::Bytes,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
use std::{ptr, slice};
|
||||
@ -52,6 +53,8 @@ pub struct AomEncoder {
|
||||
ctx: aom_codec_ctx_t,
|
||||
width: usize,
|
||||
height: usize,
|
||||
i444: bool,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
}
|
||||
|
||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/video_coding/codecs/av1/libaom_av1_encoder.cc
|
||||
@ -95,6 +98,7 @@ mod webrtc {
|
||||
pub fn enc_cfg(
|
||||
i: *const aom_codec_iface,
|
||||
cfg: AomEncoderConfig,
|
||||
i444: bool,
|
||||
) -> ResultType<aom_codec_enc_cfg> {
|
||||
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
|
||||
call_aom!(aom_codec_enc_config_default(i, &mut c, kUsageProfile));
|
||||
@ -139,6 +143,9 @@ mod webrtc {
|
||||
c.g_pass = aom_enc_pass::AOM_RC_ONE_PASS; // One-pass rate control
|
||||
c.g_lag_in_frames = kLagInFrames; // No look ahead when lag equals 0.
|
||||
|
||||
// https://aomedia.googlesource.com/aom/+/refs/tags/v3.6.0/av1/common/enums.h#82
|
||||
c.g_profile = if i444 { 1 } else { 0 };
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
@ -210,14 +217,14 @@ mod webrtc {
|
||||
}
|
||||
|
||||
impl EncoderApi for AomEncoder {
|
||||
fn new(cfg: crate::codec::EncoderCfg) -> ResultType<Self>
|
||||
fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match cfg {
|
||||
crate::codec::EncoderCfg::AOM(config) => {
|
||||
let i = call_aom_ptr!(aom_codec_av1_cx());
|
||||
let c = webrtc::enc_cfg(i, config)?;
|
||||
let c = webrtc::enc_cfg(i, config, i444)?;
|
||||
|
||||
let mut ctx = Default::default();
|
||||
// Flag options: AOM_CODEC_USE_PSNR and AOM_CODEC_USE_HIGHBITDEPTH
|
||||
@ -234,6 +241,8 @@ impl EncoderApi for AomEncoder {
|
||||
ctx,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
i444,
|
||||
yuvfmt: Self::get_yuvfmt(config.width, config.height, i444),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
@ -255,8 +264,8 @@ impl EncoderApi for AomEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn use_yuv(&self) -> bool {
|
||||
true
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
self.yuvfmt.clone()
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
|
||||
@ -282,14 +291,20 @@ impl EncoderApi for AomEncoder {
|
||||
|
||||
impl AomEncoder {
|
||||
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
if 2 * data.len() < 3 * self.width * self.height {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
}
|
||||
let fmt = if self.i444 {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444
|
||||
} else {
|
||||
aom_img_fmt::AOM_IMG_FMT_I420
|
||||
};
|
||||
|
||||
let mut image = Default::default();
|
||||
call_aom_ptr!(aom_img_wrap(
|
||||
&mut image,
|
||||
aom_img_fmt::AOM_IMG_FMT_I420,
|
||||
fmt,
|
||||
self.width as _,
|
||||
self.height as _,
|
||||
stride_align as _,
|
||||
@ -359,6 +374,34 @@ impl AomEncoder {
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat {
|
||||
let mut img = Default::default();
|
||||
let fmt = if i444 {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444
|
||||
} else {
|
||||
aom_img_fmt::AOM_IMG_FMT_I420
|
||||
};
|
||||
unsafe {
|
||||
aom_img_wrap(
|
||||
&mut img,
|
||||
fmt,
|
||||
width as _,
|
||||
height as _,
|
||||
crate::STRIDE_ALIGN as _,
|
||||
0x1 as _,
|
||||
);
|
||||
}
|
||||
let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 };
|
||||
EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: img.w as _,
|
||||
h: img.h as _,
|
||||
stride: img.stride.map(|s| s as usize).to_vec(),
|
||||
u: img.planes[1] as usize - img.planes[0] as usize,
|
||||
v: img.planes[2] as usize - img.planes[0] as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AomEncoder {
|
||||
@ -524,6 +567,13 @@ impl GoogleImage for Image {
|
||||
fn planes(&self) -> Vec<*mut u8> {
|
||||
self.inner().planes.iter().map(|p| *p as *mut u8).collect()
|
||||
}
|
||||
|
||||
fn chroma(&self) -> Chroma {
|
||||
match self.inner().fmt {
|
||||
aom_img_fmt::AOM_IMG_FMT_I444 => Chroma::I444,
|
||||
_ => Chroma::I420,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
|
@ -14,7 +14,7 @@ use crate::{
|
||||
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
|
||||
common::GoogleImage,
|
||||
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
|
||||
CodecName, ImageRgb,
|
||||
CodecName, EncodeYuvFormat, ImageRgb,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
@ -23,7 +23,7 @@ use hbb_common::{
|
||||
config::PeerConfig,
|
||||
log,
|
||||
message_proto::{
|
||||
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames,
|
||||
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
|
||||
SupportedDecoding, SupportedEncoding, VideoFrame,
|
||||
},
|
||||
sysinfo::{System, SystemExt},
|
||||
@ -56,13 +56,13 @@ pub enum EncoderCfg {
|
||||
}
|
||||
|
||||
pub trait EncoderApi {
|
||||
fn new(cfg: EncoderCfg) -> ResultType<Self>
|
||||
fn new(cfg: EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<VideoFrame>;
|
||||
|
||||
fn use_yuv(&self) -> bool;
|
||||
fn yuvfmt(&self) -> EncodeYuvFormat;
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()>;
|
||||
|
||||
@ -107,18 +107,18 @@ pub enum EncodingUpdate {
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(config: EncoderCfg) -> ResultType<Encoder> {
|
||||
log::info!("new encoder:{:?}", config);
|
||||
pub fn new(config: EncoderCfg, i444: bool) -> ResultType<Encoder> {
|
||||
log::info!("new encoder:{config:?}, i444:{i444}");
|
||||
match config {
|
||||
EncoderCfg::VPX(_) => Ok(Encoder {
|
||||
codec: Box::new(VpxEncoder::new(config)?),
|
||||
codec: Box::new(VpxEncoder::new(config, i444)?),
|
||||
}),
|
||||
EncoderCfg::AOM(_) => Ok(Encoder {
|
||||
codec: Box::new(AomEncoder::new(config)?),
|
||||
codec: Box::new(AomEncoder::new(config, i444)?),
|
||||
}),
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
EncoderCfg::HW(_) => match HwEncoder::new(config) {
|
||||
EncoderCfg::HW(_) => match HwEncoder::new(config, i444) {
|
||||
Ok(hw) => Ok(Encoder {
|
||||
codec: Box::new(hw),
|
||||
}),
|
||||
@ -230,6 +230,12 @@ impl Encoder {
|
||||
let mut encoding = SupportedEncoding {
|
||||
vp8: true,
|
||||
av1: true,
|
||||
i444: Some(CodecAbility {
|
||||
vp9: true,
|
||||
av1: true,
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -240,18 +246,41 @@ impl Encoder {
|
||||
}
|
||||
encoding
|
||||
}
|
||||
|
||||
pub fn use_i444(config: &EncoderCfg) -> bool {
|
||||
let decodings = PEER_DECODINGS.lock().unwrap().clone();
|
||||
let prefer_i444 = decodings
|
||||
.iter()
|
||||
.all(|d| d.1.prefer_chroma == Chroma::I444.into());
|
||||
let i444_useable = match config {
|
||||
EncoderCfg::VPX(vpx) => match vpx.codec {
|
||||
VpxVideoCodecId::VP8 => false,
|
||||
VpxVideoCodecId::VP9 => decodings.iter().all(|d| d.1.i444.vp9),
|
||||
},
|
||||
EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1),
|
||||
EncoderCfg::HW(_) => false,
|
||||
};
|
||||
prefer_i444 && i444_useable && !decodings.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
pub fn supported_decodings(id_for_perfer: Option<&str>) -> SupportedDecoding {
|
||||
let (prefer, prefer_chroma) = Self::preference(id_for_perfer);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut decoding = SupportedDecoding {
|
||||
ability_vp8: 1,
|
||||
ability_vp9: 1,
|
||||
ability_av1: 1,
|
||||
prefer: id_for_perfer
|
||||
.map_or(PreferCodec::Auto, |id| Self::codec_preference(id))
|
||||
.into(),
|
||||
i444: Some(CodecAbility {
|
||||
vp9: true,
|
||||
av1: true,
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
prefer: prefer.into(),
|
||||
prefer_chroma: prefer_chroma.into(),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -314,31 +343,33 @@ impl Decoder {
|
||||
&mut self,
|
||||
frame: &video_frame::Union,
|
||||
rgb: &mut ImageRgb,
|
||||
chroma: &mut Option<Chroma>,
|
||||
) -> ResultType<bool> {
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
if let Some(vp8) = &mut self.vp8 {
|
||||
Decoder::handle_vpxs_video_frame(vp8, vp8s, rgb)
|
||||
Decoder::handle_vpxs_video_frame(vp8, vp8s, rgb, chroma)
|
||||
} else {
|
||||
bail!("vp8 decoder not available");
|
||||
}
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if let Some(vp9) = &mut self.vp9 {
|
||||
Decoder::handle_vpxs_video_frame(vp9, vp9s, rgb)
|
||||
Decoder::handle_vpxs_video_frame(vp9, vp9s, rgb, chroma)
|
||||
} else {
|
||||
bail!("vp9 decoder not available");
|
||||
}
|
||||
}
|
||||
video_frame::Union::Av1s(av1s) => {
|
||||
if let Some(av1) = &mut self.av1 {
|
||||
Decoder::handle_av1s_video_frame(av1, av1s, rgb)
|
||||
Decoder::handle_av1s_video_frame(av1, av1s, rgb, chroma)
|
||||
} else {
|
||||
bail!("av1 decoder not available");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
*chroma = Some(Chroma::I420);
|
||||
if let Some(decoder) = &mut self.hw.h264 {
|
||||
Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420)
|
||||
} else {
|
||||
@ -347,6 +378,7 @@ impl Decoder {
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
*chroma = Some(Chroma::I420);
|
||||
if let Some(decoder) = &mut self.hw.h265 {
|
||||
Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420)
|
||||
} else {
|
||||
@ -355,6 +387,7 @@ impl Decoder {
|
||||
}
|
||||
#[cfg(feature = "mediacodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
*chroma = Some(Chroma::I420);
|
||||
if let Some(decoder) = &mut self.media_codec.h264 {
|
||||
Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb)
|
||||
} else {
|
||||
@ -363,6 +396,7 @@ impl Decoder {
|
||||
}
|
||||
#[cfg(feature = "mediacodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
*chroma = Some(Chroma::I420);
|
||||
if let Some(decoder) = &mut self.media_codec.h265 {
|
||||
Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb)
|
||||
} else {
|
||||
@ -378,6 +412,7 @@ impl Decoder {
|
||||
decoder: &mut VpxDecoder,
|
||||
vpxs: &EncodedVideoFrames,
|
||||
rgb: &mut ImageRgb,
|
||||
chroma: &mut Option<Chroma>,
|
||||
) -> ResultType<bool> {
|
||||
let mut last_frame = vpxcodec::Image::new();
|
||||
for vpx in vpxs.frames.iter() {
|
||||
@ -393,6 +428,7 @@ impl Decoder {
|
||||
if last_frame.is_null() {
|
||||
Ok(false)
|
||||
} else {
|
||||
*chroma = Some(last_frame.chroma());
|
||||
last_frame.to(rgb);
|
||||
Ok(true)
|
||||
}
|
||||
@ -403,6 +439,7 @@ impl Decoder {
|
||||
decoder: &mut AomDecoder,
|
||||
av1s: &EncodedVideoFrames,
|
||||
rgb: &mut ImageRgb,
|
||||
chroma: &mut Option<Chroma>,
|
||||
) -> ResultType<bool> {
|
||||
let mut last_frame = aom::Image::new();
|
||||
for av1 in av1s.frames.iter() {
|
||||
@ -418,6 +455,7 @@ impl Decoder {
|
||||
if last_frame.is_null() {
|
||||
Ok(false)
|
||||
} else {
|
||||
*chroma = Some(last_frame.chroma());
|
||||
last_frame.to(rgb);
|
||||
Ok(true)
|
||||
}
|
||||
@ -457,12 +495,16 @@ impl Decoder {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
fn codec_preference(id: &str) -> PreferCodec {
|
||||
let codec = PeerConfig::load(id)
|
||||
.options
|
||||
fn preference(id: Option<&str>) -> (PreferCodec, Chroma) {
|
||||
let id = id.unwrap_or_default();
|
||||
if id.is_empty() {
|
||||
return (PreferCodec::Auto, Chroma::I420);
|
||||
}
|
||||
let options = PeerConfig::load(id).options;
|
||||
let codec = options
|
||||
.get("codec-preference")
|
||||
.map_or("".to_owned(), |c| c.to_owned());
|
||||
if codec == "vp8" {
|
||||
let codec = if codec == "vp8" {
|
||||
PreferCodec::VP8
|
||||
} else if codec == "vp9" {
|
||||
PreferCodec::VP9
|
||||
@ -474,7 +516,13 @@ impl Decoder {
|
||||
PreferCodec::H265
|
||||
} else {
|
||||
PreferCodec::Auto
|
||||
}
|
||||
};
|
||||
let chroma = if options.get("i444") == Some(&"Y".to_string()) {
|
||||
Chroma::I444
|
||||
} else {
|
||||
Chroma::I420
|
||||
};
|
||||
(codec, chroma)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,367 +1,25 @@
|
||||
use super::vpx::*;
|
||||
use std::os::raw::c_int;
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
extern "C" {
|
||||
// seems libyuv uses reverse byte order compared with our view
|
||||
include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs"));
|
||||
|
||||
pub fn ARGBRotate(
|
||||
src_argb: *const u8,
|
||||
src_stride_argb: c_int,
|
||||
dst_argb: *mut u8,
|
||||
dst_stride_argb: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
mode: c_int,
|
||||
) -> c_int;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use crate::Frame;
|
||||
use crate::{generate_call_macro, EncodeYuvFormat, TraitFrame};
|
||||
use hbb_common::{bail, log, ResultType};
|
||||
|
||||
pub fn ARGBMirror(
|
||||
src_argb: *const u8,
|
||||
src_stride_argb: c_int,
|
||||
dst_argb: *mut u8,
|
||||
dst_stride_argb: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn ARGBToI420(
|
||||
src_bgra: *const u8,
|
||||
src_stride_bgra: c_int,
|
||||
dst_y: *mut u8,
|
||||
dst_stride_y: c_int,
|
||||
dst_u: *mut u8,
|
||||
dst_stride_u: c_int,
|
||||
dst_v: *mut u8,
|
||||
dst_stride_v: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn ABGRToI420(
|
||||
src_rgba: *const u8,
|
||||
src_stride_rgba: c_int,
|
||||
dst_y: *mut u8,
|
||||
dst_stride_y: c_int,
|
||||
dst_u: *mut u8,
|
||||
dst_stride_u: c_int,
|
||||
dst_v: *mut u8,
|
||||
dst_stride_v: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn ARGBToNV12(
|
||||
src_bgra: *const u8,
|
||||
src_stride_bgra: c_int,
|
||||
dst_y: *mut u8,
|
||||
dst_stride_y: c_int,
|
||||
dst_uv: *mut u8,
|
||||
dst_stride_uv: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn NV12ToI420(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_uv: *const u8,
|
||||
src_stride_uv: c_int,
|
||||
dst_y: *mut u8,
|
||||
dst_stride_y: c_int,
|
||||
dst_u: *mut u8,
|
||||
dst_stride_u: c_int,
|
||||
dst_v: *mut u8,
|
||||
dst_stride_v: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
// I420ToRGB24: RGB little endian (bgr in memory)
|
||||
// I420ToRaw: RGB big endian (rgb in memory) to RGBA.
|
||||
pub fn I420ToRAW(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_u: *const u8,
|
||||
src_stride_u: c_int,
|
||||
src_v: *const u8,
|
||||
src_stride_v: c_int,
|
||||
dst_rgba: *mut u8,
|
||||
dst_stride_raw: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn I420ToARGB(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_u: *const u8,
|
||||
src_stride_u: c_int,
|
||||
src_v: *const u8,
|
||||
src_stride_v: c_int,
|
||||
dst_rgba: *mut u8,
|
||||
dst_stride_rgba: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn I420ToABGR(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_u: *const u8,
|
||||
src_stride_u: c_int,
|
||||
src_v: *const u8,
|
||||
src_stride_v: c_int,
|
||||
dst_rgba: *mut u8,
|
||||
dst_stride_rgba: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn NV12ToARGB(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_uv: *const u8,
|
||||
src_stride_uv: c_int,
|
||||
dst_rgba: *mut u8,
|
||||
dst_stride_rgba: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
|
||||
pub fn NV12ToABGR(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_uv: *const u8,
|
||||
src_stride_uv: c_int,
|
||||
dst_rgba: *mut u8,
|
||||
dst_stride_rgba: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c
|
||||
#[inline]
|
||||
fn get_vpx_i420_stride(
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride_align: usize,
|
||||
) -> (usize, usize, usize, usize, usize, usize) {
|
||||
let mut img = Default::default();
|
||||
unsafe {
|
||||
vpx_img_wrap(
|
||||
&mut img,
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420,
|
||||
width as _,
|
||||
height as _,
|
||||
stride_align as _,
|
||||
0x1 as _,
|
||||
);
|
||||
}
|
||||
(
|
||||
img.w as _,
|
||||
img.h as _,
|
||||
img.stride[0] as _,
|
||||
img.stride[1] as _,
|
||||
img.planes[1] as usize - img.planes[0] as usize,
|
||||
img.planes[2] as usize - img.planes[0] as usize,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, _, src_stride_y, src_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
let src_y = src.as_ptr();
|
||||
let src_u = src[u..].as_ptr();
|
||||
let src_v = src[v..].as_ptr();
|
||||
dst.resize(width * height * 3, 0);
|
||||
unsafe {
|
||||
super::I420ToRAW(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_uv as _,
|
||||
src_v,
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 3) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn i420_to_bgra(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, _, src_stride_y, src_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
let src_y = src.as_ptr();
|
||||
let src_u = src[u..].as_ptr();
|
||||
let src_v = src[v..].as_ptr();
|
||||
dst.resize(width * height * 4, 0);
|
||||
unsafe {
|
||||
super::I420ToARGB(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_uv as _,
|
||||
src_v,
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 3) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, h, dst_stride_y, dst_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
dst.resize(h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[u..].as_mut_ptr();
|
||||
let dst_v = dst[v..].as_mut_ptr();
|
||||
unsafe {
|
||||
ARGBToI420(
|
||||
src.as_ptr(),
|
||||
(src.len() / height) as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rgba_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
|
||||
let (_, h, dst_stride_y, dst_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
dst.resize(h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[u..].as_mut_ptr();
|
||||
let dst_v = dst[v..].as_mut_ptr();
|
||||
unsafe {
|
||||
ABGRToI420(
|
||||
src.as_ptr(),
|
||||
(src.len() / height) as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn nv12_to_i420(
|
||||
src_y: *const u8,
|
||||
src_stride_y: c_int,
|
||||
src_uv: *const u8,
|
||||
src_stride_uv: c_int,
|
||||
width: usize,
|
||||
height: usize,
|
||||
dst: &mut Vec<u8>,
|
||||
) {
|
||||
let (_, h, dst_stride_y, dst_stride_uv, u, v) =
|
||||
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
|
||||
dst.resize(h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[u..].as_mut_ptr();
|
||||
let dst_v = dst[v..].as_mut_ptr();
|
||||
NV12ToI420(
|
||||
src_y,
|
||||
src_stride_y,
|
||||
src_uv,
|
||||
src_stride_uv,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
generate_call_macro!(call_yuv, false);
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
pub mod hw {
|
||||
use super::*;
|
||||
use crate::ImageFormat;
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
#[cfg(target_os = "windows")]
|
||||
use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat};
|
||||
|
||||
pub fn hw_bgra_to_i420(
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: &[i32],
|
||||
offset: &[i32],
|
||||
length: i32,
|
||||
src: &[u8],
|
||||
dst: &mut Vec<u8>,
|
||||
) {
|
||||
let stride_y = stride[0] as usize;
|
||||
let stride_u = stride[1] as usize;
|
||||
let stride_v = stride[2] as usize;
|
||||
let offset_u = offset[0] as usize;
|
||||
let offset_v = offset[1] as usize;
|
||||
|
||||
dst.resize(length as _, 0);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[offset_u..].as_mut_ptr();
|
||||
let dst_v = dst[offset_v..].as_mut_ptr();
|
||||
unsafe {
|
||||
super::ARGBToI420(
|
||||
src.as_ptr(),
|
||||
(src.len() / height) as _,
|
||||
dst_y,
|
||||
stride_y as _,
|
||||
dst_u,
|
||||
stride_u as _,
|
||||
dst_v,
|
||||
stride_v as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hw_bgra_to_nv12(
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: &[i32],
|
||||
offset: &[i32],
|
||||
length: i32,
|
||||
src: &[u8],
|
||||
dst: &mut Vec<u8>,
|
||||
) {
|
||||
let stride_y = stride[0] as usize;
|
||||
let stride_uv = stride[1] as usize;
|
||||
let offset_uv = offset[0] as usize;
|
||||
|
||||
dst.resize(length as _, 0);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_uv = dst[offset_uv..].as_mut_ptr();
|
||||
unsafe {
|
||||
super::ARGBToNV12(
|
||||
src.as_ptr(),
|
||||
(src.len() / height) as _,
|
||||
dst_y,
|
||||
stride_y as _,
|
||||
dst_uv,
|
||||
stride_uv as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn hw_nv12_to(
|
||||
fmt: ImageFormat,
|
||||
@ -386,61 +44,59 @@ pub mod hw {
|
||||
let i420_stride_v = linesize_i420[2];
|
||||
i420.resize(i420_len as _, 0);
|
||||
|
||||
unsafe {
|
||||
let i420_offset_y = i420.as_ptr().add(0) as _;
|
||||
let i420_offset_u = i420.as_ptr().add(offset_i420[0] as _) as _;
|
||||
let i420_offset_v = i420.as_ptr().add(offset_i420[1] as _) as _;
|
||||
super::NV12ToI420(
|
||||
src_y.as_ptr(),
|
||||
nv12_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
nv12_stride_uv as _,
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
super::I420ToARGB(
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
super::I420ToABGR(
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("unsupported image format"));
|
||||
}
|
||||
let i420_offset_y = unsafe { i420.as_ptr().add(0) as _ };
|
||||
let i420_offset_u = unsafe { i420.as_ptr().add(offset_i420[0] as _) as _ };
|
||||
let i420_offset_v = unsafe { i420.as_ptr().add(offset_i420[1] as _) as _ };
|
||||
call_yuv!(NV12ToI420(
|
||||
src_y.as_ptr(),
|
||||
nv12_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
nv12_stride_uv as _,
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
call_yuv!(I420ToARGB(
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
ImageFormat::ABGR => {
|
||||
call_yuv!(I420ToABGR(
|
||||
i420_offset_y,
|
||||
i420_stride_y,
|
||||
i420_offset_u,
|
||||
i420_stride_u,
|
||||
i420_offset_v,
|
||||
i420_stride_v,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
bail!("unsupported image format");
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(anyhow!("get linesize offset failed"));
|
||||
bail!("get linesize offset failed");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@ -457,41 +113,34 @@ pub mod hw {
|
||||
_align: usize,
|
||||
) -> ResultType<()> {
|
||||
dst.resize(width * height * 4, 0);
|
||||
unsafe {
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
match super::NV12ToARGB(
|
||||
src_y.as_ptr(),
|
||||
src_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
) {
|
||||
0 => Ok(()),
|
||||
_ => Err(anyhow!("NV12ToARGB failed")),
|
||||
}
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
match super::NV12ToABGR(
|
||||
src_y.as_ptr(),
|
||||
src_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
) {
|
||||
0 => Ok(()),
|
||||
_ => Err(anyhow!("NV12ToABGR failed")),
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow!("unsupported image format")),
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
call_yuv!(NV12ToARGB(
|
||||
src_y.as_ptr(),
|
||||
src_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
call_yuv!(NV12ToABGR(
|
||||
src_y.as_ptr(),
|
||||
src_stride_y as _,
|
||||
src_uv.as_ptr(),
|
||||
src_stride_uv as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
_ => bail!("unsupported image format"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hw_i420_to(
|
||||
@ -505,43 +154,173 @@ pub mod hw {
|
||||
src_stride_u: usize,
|
||||
src_stride_v: usize,
|
||||
dst: &mut Vec<u8>,
|
||||
) {
|
||||
) -> ResultType<()> {
|
||||
let src_y = src_y.as_ptr();
|
||||
let src_u = src_u.as_ptr();
|
||||
let src_v = src_v.as_ptr();
|
||||
dst.resize(width * height * 4, 0);
|
||||
unsafe {
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
super::I420ToARGB(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_u as _,
|
||||
src_v,
|
||||
src_stride_v as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
super::I420ToABGR(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_u as _,
|
||||
src_v,
|
||||
src_stride_v as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
match fmt {
|
||||
ImageFormat::ARGB => {
|
||||
call_yuv!(I420ToARGB(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_u as _,
|
||||
src_v,
|
||||
src_stride_v as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
call_yuv!(I420ToABGR(
|
||||
src_y,
|
||||
src_stride_y as _,
|
||||
src_u,
|
||||
src_stride_u as _,
|
||||
src_v,
|
||||
src_stride_v as _,
|
||||
dst.as_mut_ptr(),
|
||||
(width * 4) as _,
|
||||
width as _,
|
||||
height as _,
|
||||
));
|
||||
}
|
||||
_ => bail!("unsupported image format"),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn convert_to_yuv(
|
||||
captured: &Frame,
|
||||
dst_fmt: EncodeYuvFormat,
|
||||
dst: &mut Vec<u8>,
|
||||
mid_data: &mut Vec<u8>,
|
||||
) -> ResultType<()> {
|
||||
let src = captured.data();
|
||||
let src_stride = captured.stride();
|
||||
let src_pixfmt = captured.pixfmt();
|
||||
let src_width = captured.width();
|
||||
let src_height = captured.height();
|
||||
if src_width > dst_fmt.w || src_height > dst_fmt.h {
|
||||
bail!(
|
||||
"src rect > dst rect: ({src_width}, {src_height}) > ({},{})",
|
||||
dst_fmt.w,
|
||||
dst_fmt.h
|
||||
);
|
||||
}
|
||||
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA {
|
||||
if src.len() < src_stride[0] * src_height {
|
||||
bail!(
|
||||
"wrong src len, {} < {} * {}",
|
||||
src.len(),
|
||||
src_stride[0],
|
||||
src_height
|
||||
);
|
||||
}
|
||||
}
|
||||
let align = |x:usize| {
|
||||
(x + 63) / 64 * 64
|
||||
};
|
||||
|
||||
match (src_pixfmt, dst_fmt.pixfmt) {
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_uv = dst_fmt.stride[1];
|
||||
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||
let f = if src_pixfmt == crate::Pixfmt::BGRA {
|
||||
ARGBToI420
|
||||
} else {
|
||||
ABGRToI420
|
||||
};
|
||||
call_yuv!(f(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_uv as _,
|
||||
dst_v,
|
||||
dst_stride_uv as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12) | (crate::Pixfmt::RGBA, crate::Pixfmt::NV12) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_uv = dst_fmt.stride[1];
|
||||
dst.resize(
|
||||
align(dst_fmt.h) * (align(dst_stride_y) + align(dst_stride_uv / 2)),
|
||||
0,
|
||||
);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let f = if src_pixfmt == crate::Pixfmt::BGRA {
|
||||
ARGBToNV12
|
||||
} else {
|
||||
ABGRToNV12
|
||||
};
|
||||
call_yuv!(f(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_uv,
|
||||
dst_stride_uv as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I444) | (crate::Pixfmt::RGBA, crate::Pixfmt::I444) => {
|
||||
let dst_stride_y = dst_fmt.stride[0];
|
||||
let dst_stride_u = dst_fmt.stride[1];
|
||||
let dst_stride_v = dst_fmt.stride[2];
|
||||
dst.resize(
|
||||
align(dst_fmt.h) * (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)),
|
||||
0,
|
||||
);
|
||||
let dst_y = dst.as_mut_ptr();
|
||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||
let src = if src_pixfmt == crate::Pixfmt::BGRA {
|
||||
src
|
||||
} else {
|
||||
mid_data.resize(src.len(), 0);
|
||||
call_yuv!(ABGRToARGB(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
mid_data.as_mut_ptr(),
|
||||
src_stride[0] as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
mid_data
|
||||
};
|
||||
call_yuv!(ARGBToI444(
|
||||
src.as_ptr(),
|
||||
src_stride[0] as _,
|
||||
dst_y,
|
||||
dst_stride_y as _,
|
||||
dst_u,
|
||||
dst_stride_u as _,
|
||||
dst_v,
|
||||
dst_stride_v as _,
|
||||
src_width as _,
|
||||
src_height as _,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
bail!(
|
||||
"convert not support, {src_pixfmt:?} -> {:?}",
|
||||
dst_fmt.pixfmt
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
use crate::{common::TraitCapturer, dxgi};
|
||||
use crate::{common::TraitCapturer, dxgi, Pixfmt};
|
||||
use std::{
|
||||
io::{
|
||||
self,
|
||||
ErrorKind::{NotFound, TimedOut, WouldBlock},
|
||||
},
|
||||
ops,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@ -15,10 +14,10 @@ pub struct Capturer {
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let width = display.width();
|
||||
let height = display.height();
|
||||
let inner = dxgi::Capturer::new(display.0, yuv)?;
|
||||
let inner = dxgi::Capturer::new(display.0)?;
|
||||
Ok(Capturer {
|
||||
inner,
|
||||
width,
|
||||
@ -40,13 +39,9 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.inner.set_use_yuv(use_yuv);
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self.inner.frame(timeout.as_millis() as _) {
|
||||
Ok(frame) => Ok(Frame(frame)),
|
||||
Ok(frame) => Ok(Frame::new(frame, self.width, self.height)),
|
||||
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
@ -61,12 +56,46 @@ impl TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frame<'a>(pub &'a [u8]);
|
||||
pub struct Frame<'a> {
|
||||
data: &'a [u8],
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> ops::Deref for Frame<'a> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
self.0
|
||||
impl<'a> Frame<'a> {
|
||||
pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
|
||||
let stride0 = data.len() / height;
|
||||
let mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
Frame {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::TraitFrame for Frame<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::BGRA
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,9 +163,9 @@ impl CapturerMag {
|
||||
dxgi::mag::CapturerMag::is_supported()
|
||||
}
|
||||
|
||||
pub fn new(origin: (i32, i32), width: usize, height: usize, use_yuv: bool) -> io::Result<Self> {
|
||||
pub fn new(origin: (i32, i32), width: usize, height: usize) -> io::Result<Self> {
|
||||
Ok(CapturerMag {
|
||||
inner: dxgi::mag::CapturerMag::new(origin, width, height, use_yuv)?,
|
||||
inner: dxgi::mag::CapturerMag::new(origin, width, height)?,
|
||||
data: Vec::new(),
|
||||
})
|
||||
}
|
||||
@ -151,13 +180,13 @@ impl CapturerMag {
|
||||
}
|
||||
|
||||
impl TraitCapturer for CapturerMag {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.inner.set_use_yuv(use_yuv)
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result<Frame<'a>> {
|
||||
self.inner.frame(&mut self.data)?;
|
||||
Ok(Frame(&self.data))
|
||||
Ok(Frame::new(
|
||||
&self.data,
|
||||
self.inner.get_rect().1,
|
||||
self.inner.get_rect().2,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_gdi(&self) -> bool {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg},
|
||||
hw, ImageFormat, ImageRgb, HW_STRIDE_ALIGN,
|
||||
hw, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
|
||||
};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
@ -31,7 +31,6 @@ const DEFAULT_RC: RateControl = RC_DEFAULT;
|
||||
|
||||
pub struct HwEncoder {
|
||||
encoder: Encoder,
|
||||
yuv: Vec<u8>,
|
||||
pub format: DataFormat,
|
||||
pub pixfmt: AVPixelFormat,
|
||||
width: u32,
|
||||
@ -40,7 +39,7 @@ pub struct HwEncoder {
|
||||
}
|
||||
|
||||
impl EncoderApi for HwEncoder {
|
||||
fn new(cfg: EncoderCfg) -> ResultType<Self>
|
||||
fn new(cfg: EncoderCfg, _i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@ -78,7 +77,6 @@ impl EncoderApi for HwEncoder {
|
||||
match Encoder::new(ctx.clone()) {
|
||||
Ok(encoder) => Ok(HwEncoder {
|
||||
encoder,
|
||||
yuv: vec![],
|
||||
format,
|
||||
pixfmt: ctx.pixfmt,
|
||||
width: ctx.width as _,
|
||||
@ -118,8 +116,31 @@ impl EncoderApi for HwEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn use_yuv(&self) -> bool {
|
||||
false
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
let pixfmt = if self.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 {
|
||||
Pixfmt::NV12
|
||||
} else {
|
||||
Pixfmt::I420
|
||||
};
|
||||
let stride = self
|
||||
.encoder
|
||||
.linesize
|
||||
.clone()
|
||||
.drain(..)
|
||||
.map(|i| i as usize)
|
||||
.collect();
|
||||
crate::EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: self.encoder.ctx.width as _,
|
||||
h: self.encoder.ctx.height as _,
|
||||
stride,
|
||||
u: self.encoder.offset[0] as _,
|
||||
v: if pixfmt == Pixfmt::NV12 {
|
||||
0
|
||||
} else {
|
||||
self.encoder.offset[1] as _
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> {
|
||||
@ -145,29 +166,8 @@ impl HwEncoder {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, bgra: &[u8]) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P => hw::hw_bgra_to_i420(
|
||||
self.encoder.ctx.width as _,
|
||||
self.encoder.ctx.height as _,
|
||||
&self.encoder.linesize,
|
||||
&self.encoder.offset,
|
||||
self.encoder.length,
|
||||
bgra,
|
||||
&mut self.yuv,
|
||||
),
|
||||
AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_bgra_to_nv12(
|
||||
self.encoder.ctx.width as _,
|
||||
self.encoder.ctx.height as _,
|
||||
&self.encoder.linesize,
|
||||
&self.encoder.offset,
|
||||
self.encoder.length,
|
||||
bgra,
|
||||
&mut self.yuv,
|
||||
),
|
||||
}
|
||||
|
||||
match self.encoder.encode(&self.yuv) {
|
||||
pub fn encode(&mut self, yuv: &[u8]) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.encoder.encode(yuv) {
|
||||
Ok(v) => {
|
||||
let mut data = Vec::<EncodeFrame>::new();
|
||||
data.append(v);
|
||||
@ -245,7 +245,7 @@ impl HwDecoder {
|
||||
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<HwDecoderImage>> {
|
||||
match self.decoder.decode(data) {
|
||||
Ok(v) => Ok(v.iter().map(|f| HwDecoderImage { frame: f }).collect()),
|
||||
Err(_) => Ok(vec![]),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -274,7 +274,7 @@ impl HwDecoderImage<'_> {
|
||||
&mut rgb.raw as _,
|
||||
i420,
|
||||
HW_STRIDE_ALIGN,
|
||||
),
|
||||
)?,
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P => {
|
||||
hw::hw_i420_to(
|
||||
rgb.fmt(),
|
||||
@ -287,10 +287,10 @@ impl HwDecoderImage<'_> {
|
||||
frame.linesize[1] as _,
|
||||
frame.linesize[2] as _,
|
||||
&mut rgb.raw as _,
|
||||
);
|
||||
return Ok(());
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bgra(&self, bgra: &mut Vec<u8>, i420: &mut Vec<u8>) -> ResultType<()> {
|
||||
|
@ -11,10 +11,10 @@ pub enum Capturer {
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
Ok(match display {
|
||||
Display::X11(d) => Capturer::X11(x11::Capturer::new(d, yuv)?),
|
||||
Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d, yuv)?),
|
||||
Display::X11(d) => Capturer::X11(x11::Capturer::new(d)?),
|
||||
Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d)?),
|
||||
})
|
||||
}
|
||||
|
||||
@ -34,13 +34,6 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
match self {
|
||||
Capturer::X11(d) => d.set_use_yuv(use_yuv),
|
||||
Capturer::WAYLAND(d) => d.set_use_yuv(use_yuv),
|
||||
}
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self {
|
||||
Capturer::X11(d) => d.frame(timeout),
|
||||
|
@ -1,5 +1,8 @@
|
||||
pub use self::vpxcodec::*;
|
||||
use hbb_common::message_proto::{video_frame, VideoFrame};
|
||||
use hbb_common::{
|
||||
log,
|
||||
message_proto::{video_frame, Chroma, VideoFrame},
|
||||
};
|
||||
use std::slice;
|
||||
|
||||
cfg_if! {
|
||||
@ -96,8 +99,6 @@ pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()>
|
||||
}
|
||||
|
||||
pub trait TraitCapturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool);
|
||||
|
||||
// We doesn't support
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<Frame<'a>>;
|
||||
@ -108,6 +109,36 @@ pub trait TraitCapturer {
|
||||
fn set_gdi(&mut self) -> bool;
|
||||
}
|
||||
|
||||
pub trait TraitFrame {
|
||||
fn data(&self) -> &[u8];
|
||||
|
||||
fn width(&self) -> usize;
|
||||
|
||||
fn height(&self) -> usize;
|
||||
|
||||
fn stride(&self) -> Vec<usize>;
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt;
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Pixfmt {
|
||||
BGRA,
|
||||
RGBA,
|
||||
I420,
|
||||
NV12,
|
||||
I444,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncodeYuvFormat {
|
||||
pub pixfmt: Pixfmt,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
pub stride: Vec<usize>,
|
||||
pub u: usize,
|
||||
pub v: usize,
|
||||
}
|
||||
|
||||
#[cfg(x11)]
|
||||
#[inline]
|
||||
pub fn is_x11() -> bool {
|
||||
@ -260,6 +291,7 @@ pub trait GoogleImage {
|
||||
fn height(&self) -> usize;
|
||||
fn stride(&self) -> Vec<i32>;
|
||||
fn planes(&self) -> Vec<*mut u8>;
|
||||
fn chroma(&self) -> Chroma;
|
||||
fn get_bytes_per_row(w: usize, fmt: ImageFormat, stride: usize) -> usize {
|
||||
let bytes_per_pixel = match fmt {
|
||||
ImageFormat::Raw => 3,
|
||||
@ -278,8 +310,8 @@ pub trait GoogleImage {
|
||||
let stride = self.stride();
|
||||
let planes = self.planes();
|
||||
unsafe {
|
||||
match rgb.fmt() {
|
||||
ImageFormat::Raw => {
|
||||
match (self.chroma(), rgb.fmt()) {
|
||||
(Chroma::I420, ImageFormat::Raw) => {
|
||||
super::I420ToRAW(
|
||||
planes[0],
|
||||
stride[0],
|
||||
@ -293,7 +325,7 @@ pub trait GoogleImage {
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
ImageFormat::ARGB => {
|
||||
(Chroma::I420, ImageFormat::ARGB) => {
|
||||
super::I420ToARGB(
|
||||
planes[0],
|
||||
stride[0],
|
||||
@ -307,7 +339,7 @@ pub trait GoogleImage {
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
ImageFormat::ABGR => {
|
||||
(Chroma::I420, ImageFormat::ABGR) => {
|
||||
super::I420ToABGR(
|
||||
planes[0],
|
||||
stride[0],
|
||||
@ -321,6 +353,36 @@ pub trait GoogleImage {
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I444, ImageFormat::ARGB) => {
|
||||
super::I444ToARGB(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
(Chroma::I444, ImageFormat::ABGR) => {
|
||||
super::I444ToABGR(
|
||||
planes[0],
|
||||
stride[0],
|
||||
planes[1],
|
||||
stride[1],
|
||||
planes[2],
|
||||
stride[2],
|
||||
rgb.raw.as_mut_ptr(),
|
||||
bytes_per_row as _,
|
||||
self.width() as _,
|
||||
self.height() as _,
|
||||
);
|
||||
}
|
||||
// (Chroma::I444, ImageFormat::Raw), new version libyuv have I444ToRAW
|
||||
_ => log::error!("unsupported pixfmt:{:?}", self.chroma()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,16 @@
|
||||
use crate::quartz;
|
||||
use crate::{quartz, Pixfmt};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex, TryLockError};
|
||||
use std::{io, mem, ops};
|
||||
use std::{io, mem};
|
||||
|
||||
pub struct Capturer {
|
||||
inner: quartz::Capturer,
|
||||
frame: Arc<Mutex<Option<quartz::Frame>>>,
|
||||
use_yuv: bool,
|
||||
i420: Vec<u8>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, use_yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let frame = Arc::new(Mutex::new(None));
|
||||
|
||||
let f = frame.clone();
|
||||
@ -20,11 +18,7 @@ impl Capturer {
|
||||
display.0,
|
||||
display.width(),
|
||||
display.height(),
|
||||
if use_yuv {
|
||||
quartz::PixelFormat::YCbCr420Video
|
||||
} else {
|
||||
quartz::PixelFormat::Argb8888
|
||||
},
|
||||
quartz::PixelFormat::Argb8888,
|
||||
Default::default(),
|
||||
move |inner| {
|
||||
if let Ok(mut f) = f.lock() {
|
||||
@ -37,8 +31,6 @@ impl Capturer {
|
||||
Ok(Capturer {
|
||||
inner,
|
||||
frame,
|
||||
use_yuv,
|
||||
i420: Vec::new(),
|
||||
saved_raw_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
@ -53,10 +45,6 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl crate::TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.use_yuv = use_yuv;
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, _timeout_ms: std::time::Duration) -> io::Result<Frame<'a>> {
|
||||
match self.frame.try_lock() {
|
||||
Ok(mut handle) => {
|
||||
@ -66,10 +54,13 @@ impl crate::TraitCapturer for Capturer {
|
||||
match frame {
|
||||
Some(mut frame) => {
|
||||
crate::would_block_if_equal(&mut self.saved_raw_data, frame.inner())?;
|
||||
if self.use_yuv {
|
||||
frame.nv12_to_i420(self.width(), self.height(), &mut self.i420);
|
||||
}
|
||||
Ok(Frame(frame, PhantomData))
|
||||
frame.surface_to_bgra(self.height());
|
||||
Ok(Frame {
|
||||
frame,
|
||||
data: PhantomData,
|
||||
width: self.width(),
|
||||
height: self.height(),
|
||||
})
|
||||
}
|
||||
|
||||
None => Err(io::ErrorKind::WouldBlock.into()),
|
||||
@ -83,12 +74,34 @@ impl crate::TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frame<'a>(pub quartz::Frame, PhantomData<&'a [u8]>);
|
||||
pub struct Frame<'a> {
|
||||
frame: quartz::Frame,
|
||||
data: PhantomData<&'a [u8]>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl<'a> ops::Deref for Frame<'a> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&*self.0
|
||||
impl<'a> crate::TraitFrame for Frame<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
&*self.frame
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
let mut v = Vec::new();
|
||||
v.push(self.frame.stride());
|
||||
v
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::BGRA
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@
|
||||
|
||||
use hbb_common::anyhow::{anyhow, Context};
|
||||
use hbb_common::log;
|
||||
use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame};
|
||||
use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame};
|
||||
use hbb_common::ResultType;
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality};
|
||||
use crate::{GoogleImage, STRIDE_ALIGN};
|
||||
use crate::{EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN};
|
||||
|
||||
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
|
||||
use crate::{generate_call_macro, generate_call_ptr_macro, Error, Result};
|
||||
@ -39,6 +39,8 @@ pub struct VpxEncoder {
|
||||
width: usize,
|
||||
height: usize,
|
||||
id: VpxVideoCodecId,
|
||||
i444: bool,
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
}
|
||||
|
||||
pub struct VpxDecoder {
|
||||
@ -46,7 +48,7 @@ pub struct VpxDecoder {
|
||||
}
|
||||
|
||||
impl EncoderApi for VpxEncoder {
|
||||
fn new(cfg: crate::codec::EncoderCfg) -> ResultType<Self>
|
||||
fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@ -98,6 +100,13 @@ impl EncoderApi for VpxEncoder {
|
||||
} else {
|
||||
c.rc_target_bitrate = base_bitrate;
|
||||
}
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282
|
||||
c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
/*
|
||||
The VPX encoder supports two-pass encoding for rate control purposes.
|
||||
@ -166,6 +175,8 @@ impl EncoderApi for VpxEncoder {
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
id: config.codec,
|
||||
i444,
|
||||
yuvfmt: Self::get_yuvfmt(config.width, config.height, i444),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
@ -192,8 +203,8 @@ impl EncoderApi for VpxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn use_yuv(&self) -> bool {
|
||||
true
|
||||
fn yuvfmt(&self) -> crate::EncodeYuvFormat {
|
||||
self.yuvfmt.clone()
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
|
||||
@ -219,14 +230,20 @@ impl EncoderApi for VpxEncoder {
|
||||
|
||||
impl VpxEncoder {
|
||||
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
|
||||
if 2 * data.len() < 3 * self.width * self.height {
|
||||
let bpp = if self.i444 { 24 } else { 12 };
|
||||
if data.len() < self.width * self.height * bpp / 8 {
|
||||
return Err(Error::FailedCall("len not enough".to_string()));
|
||||
}
|
||||
let fmt = if self.i444 {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444
|
||||
} else {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420
|
||||
};
|
||||
|
||||
let mut image = Default::default();
|
||||
call_vpx_ptr!(vpx_img_wrap(
|
||||
&mut image,
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420,
|
||||
fmt,
|
||||
self.width as _,
|
||||
self.height as _,
|
||||
stride_align as _,
|
||||
@ -319,6 +336,34 @@ impl VpxEncoder {
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat {
|
||||
let mut img = Default::default();
|
||||
let fmt = if i444 {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444
|
||||
} else {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I420
|
||||
};
|
||||
unsafe {
|
||||
vpx_img_wrap(
|
||||
&mut img,
|
||||
fmt,
|
||||
width as _,
|
||||
height as _,
|
||||
crate::STRIDE_ALIGN as _,
|
||||
0x1 as _,
|
||||
);
|
||||
}
|
||||
let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 };
|
||||
EncodeYuvFormat {
|
||||
pixfmt,
|
||||
w: img.w as _,
|
||||
h: img.h as _,
|
||||
stride: img.stride.map(|s| s as usize).to_vec(),
|
||||
u: img.planes[1] as usize - img.planes[0] as usize,
|
||||
v: img.planes[2] as usize - img.planes[0] as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VpxEncoder {
|
||||
@ -533,6 +578,13 @@ impl GoogleImage for Image {
|
||||
fn planes(&self) -> Vec<*mut u8> {
|
||||
self.inner().planes.iter().map(|p| *p as *mut u8).collect()
|
||||
}
|
||||
|
||||
fn chroma(&self) -> Chroma {
|
||||
match self.inner().fmt {
|
||||
vpx_img_fmt::VPX_IMG_FMT_I444 => Chroma::I444,
|
||||
_ => Chroma::I420,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
|
@ -2,7 +2,7 @@ use crate::common::{x11::Frame, TraitCapturer};
|
||||
use crate::wayland::{capturable::*, *};
|
||||
use std::{io, sync::RwLock, time::Duration};
|
||||
|
||||
pub struct Capturer(Display, Box<dyn Recorder>, bool, Vec<u8>);
|
||||
pub struct Capturer(Display, Box<dyn Recorder>, Vec<u8>);
|
||||
|
||||
static mut IS_CURSOR_EMBEDDED: Option<bool> = None;
|
||||
|
||||
@ -45,9 +45,9 @@ fn map_err<E: ToString>(err: E) -> io::Error {
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let r = display.0.recorder(false).map_err(map_err)?;
|
||||
Ok(Capturer(display, r, yuv, Default::default()))
|
||||
Ok(Capturer(display, r, Default::default()))
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
@ -60,24 +60,10 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.2 = use_yuv;
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
match self.1.capture(timeout.as_millis() as _).map_err(map_err)? {
|
||||
PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 {
|
||||
crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3);
|
||||
&self.3[..]
|
||||
} else {
|
||||
x
|
||||
})),
|
||||
PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 {
|
||||
crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3);
|
||||
&self.3[..]
|
||||
} else {
|
||||
x
|
||||
})),
|
||||
PixelProvider::BGR0(w, h, x) => Ok(Frame::new(x, crate::Pixfmt::BGRA, w, h)),
|
||||
PixelProvider::RGB0(w, h, x) => Ok(Frame::new(x, crate::Pixfmt::RGBA, w,h)),
|
||||
PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()),
|
||||
_ => Err(map_err("Invalid data")),
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
use crate::{common::TraitCapturer, x11};
|
||||
use std::{io, ops, time::Duration};
|
||||
use crate::{common::TraitCapturer, x11, Pixfmt, TraitFrame};
|
||||
use std::{io, time::Duration};
|
||||
|
||||
pub struct Capturer(x11::Capturer);
|
||||
|
||||
pub const IS_CURSOR_EMBEDDED: bool = false;
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
|
||||
x11::Capturer::new(display.0, yuv).map(Capturer)
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
x11::Capturer::new(display.0).map(Capturer)
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
@ -20,21 +20,53 @@ impl Capturer {
|
||||
}
|
||||
|
||||
impl TraitCapturer for Capturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.0.set_use_yuv(use_yuv);
|
||||
}
|
||||
|
||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||
Ok(Frame(self.0.frame()?))
|
||||
Ok(self.0.frame()?)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frame<'a>(pub &'a [u8]);
|
||||
pub struct Frame<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub pixfmt: Pixfmt,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub stride: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a> ops::Deref for Frame<'a> {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
self.0
|
||||
impl<'a> Frame<'a> {
|
||||
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);
|
||||
Self {
|
||||
data,
|
||||
pixfmt,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TraitFrame for Frame<'a> {
|
||||
fn data(&self) -> &[u8] {
|
||||
self.data
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn stride(&self) -> Vec<usize> {
|
||||
self.stride.clone()
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> crate::Pixfmt {
|
||||
self.pixfmt
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ impl CapturerGDI {
|
||||
stride,
|
||||
self.width,
|
||||
self.height,
|
||||
180,
|
||||
crate::RotationMode::kRotate180,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -245,9 +245,6 @@ pub struct CapturerMag {
|
||||
rect: RECT,
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
||||
use_yuv: bool,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Drop for CapturerMag {
|
||||
@ -262,12 +259,7 @@ impl CapturerMag {
|
||||
MagInterface::new().is_ok()
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
origin: (i32, i32),
|
||||
width: usize,
|
||||
height: usize,
|
||||
use_yuv: bool,
|
||||
) -> Result<Self> {
|
||||
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
||||
unsafe {
|
||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
@ -311,8 +303,6 @@ impl CapturerMag {
|
||||
},
|
||||
width,
|
||||
height,
|
||||
use_yuv,
|
||||
data: Vec::new(),
|
||||
};
|
||||
|
||||
unsafe {
|
||||
@ -437,10 +427,6 @@ impl CapturerMag {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub(crate) fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.use_yuv = use_yuv;
|
||||
}
|
||||
|
||||
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
||||
let name_c = CString::new(name)?;
|
||||
unsafe {
|
||||
@ -579,22 +565,9 @@ impl CapturerMag {
|
||||
));
|
||||
}
|
||||
|
||||
if self.use_yuv {
|
||||
self.data.resize(lock.1.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut self.data[0], self.data.len());
|
||||
}
|
||||
crate::common::bgra_to_i420(
|
||||
self.width as usize,
|
||||
self.height as usize,
|
||||
&self.data,
|
||||
data,
|
||||
);
|
||||
} else {
|
||||
data.resize(lock.1.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut data[0], data.len());
|
||||
}
|
||||
data.resize(lock.1.len(), 0);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut data[0], data.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -651,7 +624,7 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test() {
|
||||
let mut capture_mag = CapturerMag::new((0, 0), 1920, 1080, false).unwrap();
|
||||
let mut capture_mag = CapturerMag::new((0, 0), 1920, 1080).unwrap();
|
||||
capture_mag.exclude("", "RustDeskPrivacyWindow").unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000 * 10));
|
||||
let mut data = Vec::new();
|
||||
|
@ -20,6 +20,8 @@ use winapi::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::RotationMode::*;
|
||||
|
||||
pub struct ComPtr<T>(*mut T);
|
||||
impl<T> ComPtr<T> {
|
||||
fn is_null(&self) -> bool {
|
||||
@ -45,8 +47,6 @@ pub struct Capturer {
|
||||
surface: ComPtr<IDXGISurface>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
use_yuv: bool,
|
||||
yuv: Vec<u8>,
|
||||
rotated: Vec<u8>,
|
||||
gdi_capturer: Option<CapturerGDI>,
|
||||
gdi_buffer: Vec<u8>,
|
||||
@ -54,7 +54,7 @@ pub struct Capturer {
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, use_yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
let mut device = ptr::null_mut();
|
||||
let mut context = ptr::null_mut();
|
||||
let mut duplication = ptr::null_mut();
|
||||
@ -148,8 +148,6 @@ impl Capturer {
|
||||
width: display.width() as usize,
|
||||
height: display.height() as usize,
|
||||
display,
|
||||
use_yuv,
|
||||
yuv: Vec::new(),
|
||||
rotated: Vec::new(),
|
||||
gdi_capturer,
|
||||
gdi_buffer: Vec::new(),
|
||||
@ -157,10 +155,6 @@ impl Capturer {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.use_yuv = use_yuv;
|
||||
}
|
||||
|
||||
pub fn is_gdi(&self) -> bool {
|
||||
self.gdi_capturer.is_some()
|
||||
}
|
||||
@ -259,10 +253,10 @@ impl Capturer {
|
||||
self.unmap();
|
||||
let r = self.load_frame(timeout)?;
|
||||
let rotate = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => kRotate90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => kRotate180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => kRotate270,
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
@ -270,7 +264,7 @@ impl Capturer {
|
||||
));
|
||||
}
|
||||
};
|
||||
if rotate == 0 {
|
||||
if rotate == kRotate0 {
|
||||
slice::from_raw_parts(r.0, r.1 as usize * self.height)
|
||||
} else {
|
||||
self.rotated.resize(self.width * self.height * 4, 0);
|
||||
@ -279,12 +273,12 @@ impl Capturer {
|
||||
r.1,
|
||||
self.rotated.as_mut_ptr(),
|
||||
4 * self.width as i32,
|
||||
if rotate == 180 {
|
||||
if rotate == kRotate180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
} as _,
|
||||
if rotate != 180 {
|
||||
if rotate != kRotate180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
@ -295,19 +289,7 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok({
|
||||
if self.use_yuv {
|
||||
crate::common::bgra_to_i420(
|
||||
self.width as usize,
|
||||
self.height as usize,
|
||||
&result,
|
||||
&mut self.yuv,
|
||||
);
|
||||
&self.yuv[..]
|
||||
} else {
|
||||
result
|
||||
}
|
||||
})
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,8 @@ use super::ffi::*;
|
||||
pub struct Frame {
|
||||
surface: IOSurfaceRef,
|
||||
inner: &'static [u8],
|
||||
i420: *mut u8,
|
||||
i420_len: usize,
|
||||
bgra: Vec<u8>,
|
||||
bgra_stride: usize,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
@ -24,8 +24,8 @@ impl Frame {
|
||||
Frame {
|
||||
surface,
|
||||
inner,
|
||||
i420: ptr::null_mut(),
|
||||
i420_len: 0,
|
||||
bgra: Vec::new(),
|
||||
bgra_stride: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,23 +34,20 @@ impl Frame {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn nv12_to_i420<'a>(&'a mut self, w: usize, h: usize, i420: &'a mut Vec<u8>) {
|
||||
pub fn stride(&self) -> usize {
|
||||
self.bgra_stride
|
||||
}
|
||||
|
||||
pub fn surface_to_bgra<'a>(&'a mut self, h: usize) {
|
||||
unsafe {
|
||||
let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0);
|
||||
let stride0 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0);
|
||||
let plane1 = IOSurfaceGetBaseAddressOfPlane(self.surface, 1);
|
||||
let stride1 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 1);
|
||||
crate::common::nv12_to_i420(
|
||||
self.bgra_stride = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0);
|
||||
self.bgra.resize(self.bgra_stride * h, 0);
|
||||
std::ptr::copy_nonoverlapping(
|
||||
plane0 as _,
|
||||
stride0 as _,
|
||||
plane1 as _,
|
||||
stride1 as _,
|
||||
w,
|
||||
h,
|
||||
i420,
|
||||
self.bgra.as_mut_ptr(),
|
||||
self.bgra_stride * h,
|
||||
);
|
||||
self.i420 = i420.as_mut_ptr() as _;
|
||||
self.i420_len = i420.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,14 +55,7 @@ impl Frame {
|
||||
impl ops::Deref for Frame {
|
||||
type Target = [u8];
|
||||
fn deref<'a>(&'a self) -> &'a [u8] {
|
||||
if self.i420.is_null() {
|
||||
self.inner
|
||||
} else {
|
||||
unsafe {
|
||||
let inner = slice::from_raw_parts(self.i420 as *const u8, self.i420_len);
|
||||
inner
|
||||
}
|
||||
}
|
||||
&self.bgra
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@ use std::{io, ptr, slice};
|
||||
|
||||
use hbb_common::libc;
|
||||
|
||||
use crate::Frame;
|
||||
|
||||
use super::ffi::*;
|
||||
use super::Display;
|
||||
|
||||
@ -12,13 +14,11 @@ pub struct Capturer {
|
||||
buffer: *const u8,
|
||||
|
||||
size: usize,
|
||||
use_yuv: bool,
|
||||
yuv: Vec<u8>,
|
||||
saved_raw_data: Vec<u8>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, use_yuv: bool) -> io::Result<Capturer> {
|
||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||
// Calculate dimensions.
|
||||
|
||||
let pixel_width = 4;
|
||||
@ -67,17 +67,11 @@ impl Capturer {
|
||||
xcbid,
|
||||
buffer,
|
||||
size,
|
||||
use_yuv,
|
||||
yuv: Vec::new(),
|
||||
saved_raw_data: Vec::new(),
|
||||
};
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
pub fn set_use_yuv(&mut self, use_yuv: bool) {
|
||||
self.use_yuv = use_yuv;
|
||||
}
|
||||
|
||||
pub fn display(&self) -> &Display {
|
||||
&self.display
|
||||
}
|
||||
@ -103,16 +97,13 @@ impl Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame<'b>(&'b mut self) -> std::io::Result<&'b [u8]> {
|
||||
pub fn frame<'b>(&'b mut self) -> std::io::Result<Frame> {
|
||||
self.get_image();
|
||||
let result = unsafe { slice::from_raw_parts(self.buffer, self.size) };
|
||||
crate::would_block_if_equal(&mut self.saved_raw_data, result)?;
|
||||
Ok(if self.use_yuv {
|
||||
crate::common::bgra_to_i420(self.display.w(), self.display.h(), &result, &mut self.yuv);
|
||||
&self.yuv[..]
|
||||
} else {
|
||||
result
|
||||
})
|
||||
Ok(
|
||||
Frame::new(result, crate::Pixfmt::BGRA, self.display.w(), self.display.h())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
102
res/lang.py
102
res/lang.py
@ -5,20 +5,22 @@ import glob
|
||||
import sys
|
||||
import csv
|
||||
|
||||
|
||||
def get_lang(lang):
|
||||
out = {}
|
||||
for ln in open('./src/lang/%s.rs'%lang, encoding='utf8'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('("'):
|
||||
k, v = line_split(ln)
|
||||
out[k] = v
|
||||
return out
|
||||
out = {}
|
||||
for ln in open('./src/lang/%s.rs' % lang, encoding='utf8'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('("'):
|
||||
k, v = line_split(ln)
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def line_split(line):
|
||||
toks = line.split('", "')
|
||||
if len(toks) != 2:
|
||||
print(line)
|
||||
assert(0)
|
||||
assert 0
|
||||
# Replace fixed position.
|
||||
# Because toks[1] may be v") or v"),
|
||||
k = toks[0][toks[0].find('"') + 1:]
|
||||
@ -27,62 +29,62 @@ def line_split(line):
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 1:
|
||||
expand()
|
||||
elif sys.argv[1] == '1':
|
||||
to_csv()
|
||||
else:
|
||||
to_rs(sys.argv[1])
|
||||
if len(sys.argv) == 1:
|
||||
expand()
|
||||
elif sys.argv[1] == '1':
|
||||
to_csv()
|
||||
else:
|
||||
to_rs(sys.argv[1])
|
||||
|
||||
|
||||
def expand():
|
||||
for fn in glob.glob('./src/lang/*.rs'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
if lang in ['en','template']: continue
|
||||
print(lang)
|
||||
dict = get_lang(lang)
|
||||
fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8')
|
||||
for line in open('./src/lang/template.rs', encoding='utf8'):
|
||||
line_strip = line.strip()
|
||||
if line_strip.startswith('("'):
|
||||
k, v = line_split(line_strip)
|
||||
if k in dict:
|
||||
# embraced with " to avoid empty v
|
||||
line = line.replace('"%s"'%v, '"%s"'%dict[k])
|
||||
else:
|
||||
line = line.replace(v, "")
|
||||
fw.write(line)
|
||||
else:
|
||||
fw.write(line)
|
||||
fw.close()
|
||||
for fn in glob.glob('./src/lang/*.rs'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
if lang in ['en', 'template']: continue
|
||||
print(lang)
|
||||
dict = get_lang(lang)
|
||||
fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
|
||||
for line in open('./src/lang/template.rs', encoding='utf8'):
|
||||
line_strip = line.strip()
|
||||
if line_strip.startswith('("'):
|
||||
k, v = line_split(line_strip)
|
||||
if k in dict:
|
||||
# embraced with " to avoid empty v
|
||||
line = line.replace('"%s"' % v, '"%s"' % dict[k])
|
||||
else:
|
||||
line = line.replace(v, "")
|
||||
fw.write(line)
|
||||
else:
|
||||
fw.write(line)
|
||||
fw.close()
|
||||
|
||||
|
||||
def to_csv():
|
||||
for fn in glob.glob('./src/lang/*.rs'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
csvfile = open('./src/lang/%s.csv'%lang, "wt", encoding='utf8')
|
||||
csvwriter = csv.writer(csvfile)
|
||||
for line in open(fn, encoding='utf8'):
|
||||
line_strip = line.strip()
|
||||
if line_strip.startswith('("'):
|
||||
k, v = line_split(line_strip)
|
||||
csvwriter.writerow([k, v])
|
||||
csvfile.close()
|
||||
for fn in glob.glob('./src/lang/*.rs'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
csvfile = open('./src/lang/%s.csv' % lang, "wt", encoding='utf8')
|
||||
csvwriter = csv.writer(csvfile)
|
||||
for line in open(fn, encoding='utf8'):
|
||||
line_strip = line.strip()
|
||||
if line_strip.startswith('("'):
|
||||
k, v = line_split(line_strip)
|
||||
csvwriter.writerow([k, v])
|
||||
csvfile.close()
|
||||
|
||||
|
||||
def to_rs(lang):
|
||||
csvfile = open('%s.csv'%lang, "rt", encoding='utf8')
|
||||
fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8')
|
||||
fw.write('''lazy_static::lazy_static! {
|
||||
csvfile = open('%s.csv' % lang, "rt", encoding='utf8')
|
||||
fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
|
||||
fw.write('''lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
''')
|
||||
for row in csv.reader(csvfile):
|
||||
fw.write(' ("%s", "%s"),\n'%(row[0].replace('"', '\"'), row[1].replace('"', '\"')))
|
||||
fw.write(''' ].iter().cloned().collect();
|
||||
for row in csv.reader(csvfile):
|
||||
fw.write(' ("%s", "%s"),\n' % (row[0].replace('"', '\"'), row[1].replace('"', '\"')))
|
||||
fw.write(''' ].iter().cloned().collect();
|
||||
}
|
||||
''')
|
||||
fw.close()
|
||||
fw.close()
|
||||
|
||||
|
||||
main()
|
||||
|
@ -45,7 +45,7 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}"
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
!define MUI_FINISHPAGE_SHOWREADME ""
|
||||
!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create desktop shortcut"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user