From 9489877c7810329e5e43ed577e3b96f126991ec7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 15 Sep 2022 17:31:28 +0800 Subject: [PATCH] video record Signed-off-by: 21pages --- Cargo.lock | 125 +++- .../desktop/pages/desktop_setting_page.dart | 56 ++ flutter/lib/desktop/pages/remote_page.dart | 1 + .../lib/desktop/widgets/remote_menubar.dart | 20 + flutter/lib/models/model.dart | 38 + flutter/pubspec.lock | 17 +- flutter/pubspec.yaml | 1 + libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 2 + libs/scrap/Cargo.toml | 2 +- libs/scrap/src/common/hwcodec.rs | 2 +- libs/scrap/src/common/mod.rs | 1 + libs/scrap/src/common/record.rs | 297 ++++++++ src/client.rs | 80 ++- src/client/io_loop.rs | 14 +- src/flutter_ffi.rs | 24 +- src/lang/cn.rs | 6 + src/lang/cs.rs | 6 + src/lang/da.rs | 6 + src/lang/de.rs | 6 + src/lang/eo.rs | 6 + src/lang/es.rs | 6 + src/lang/fr.rs | 6 + src/lang/hu.rs | 6 + src/lang/id.rs | 6 + src/lang/it.rs | 6 + src/lang/ja.rs | 6 + src/lang/ko.rs | 6 + src/lang/kz.rs | 654 +++++++++--------- src/lang/pl.rs | 6 + src/lang/pt_PT.rs | 6 + src/lang/ptbr.rs | 6 + src/lang/ru.rs | 6 + src/lang/sk.rs | 6 + src/lang/template.rs | 6 + src/lang/tr.rs | 6 + src/lang/tw.rs | 6 + src/lang/vn.rs | 6 + src/server/video_service.rs | 29 +- src/ui.rs | 33 +- src/ui/common.css | 9 + src/ui/header.tis | 13 + src/ui/index.tis | 18 + src/ui/msgbox.tis | 8 + src/ui/remote.rs | 1 + src/ui/remote.tis | 3 + src/ui_interface.rs | 5 + src/ui_session_interface.rs | 4 + 48 files changed, 1186 insertions(+), 398 deletions(-) create mode 100644 libs/scrap/src/common/record.rs diff --git a/Cargo.lock b/Cargo.lock index 5627a861f..c43bccbbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "atomic", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "alsa" version = "0.6.0" @@ -420,6 +435,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -589,8 +625,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ "iana-time-zone", + "js-sys", "num-integer", "num-traits 0.2.15", + "time 0.1.44", + "wasm-bindgen", "winapi 0.3.9", ] @@ -1359,6 +1398,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "embed-resource" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1548,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.3.3", ] [[package]] @@ -1590,7 +1642,7 @@ dependencies = [ "regex", "rustversion", "thiserror", - "time", + "time 0.3.9", ] [[package]] @@ -1895,7 +1947,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -2283,6 +2335,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "chrono", "confy", "directories-next", "dirs-next", @@ -2391,7 +2444,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#890204e0703a3d361fc7a45f035fe75c0575bb1d" +source = "git+https://github.com/21pages/hwcodec#097a476a0ee249e28d99573899ed4c9c0c01f884" dependencies = [ "bindgen", "cc", @@ -2822,6 +2875,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memalloc" version = "0.1.0" @@ -2919,7 +2978,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.36.1", ] @@ -4230,6 +4289,15 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.13", +] + [[package]] name = "rustdesk" version = "1.2.0" @@ -4305,6 +4373,16 @@ dependencies = [ "wol-rs", ] +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs", + "embed-resource", + "md5", +] + [[package]] name = "rustfft" version = "6.0.1" @@ -5050,6 +5128,17 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.3.9" @@ -5374,6 +5463,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -5401,6 +5510,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5ab3b9a51..231e001a2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; @@ -199,6 +201,7 @@ class _GeneralState extends State<_General> { abr(), hwcodec(), audio(context), + record(context), _Card(title: 'Language', children: [language()]), ], ).marginOnly(bottom: _kListViewBottomMargin)); @@ -290,6 +293,59 @@ class _GeneralState extends State<_General> { }); } + Widget record(BuildContext context) { + return _futureBuilder(future: () async { + String customDirectory = + await bind.mainGetOption(key: 'video-save-directory'); + String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); + String dir; + if (customDirectory.isNotEmpty) { + dir = customDirectory; + } else { + dir = defaultDirectory; + } + final canlaunch = await canLaunchUrl(Uri.file(dir)); + return {'dir': dir, 'canlaunch': canlaunch}; + }(), hasData: (data) { + Map map = data as Map; + String dir = map['dir']!; + bool canlaunch = map['canlaunch']! as bool; + + return _Card(title: 'Recording', children: [ + _OptionCheckBox(context, 'Automatically record incoming sessions', + 'allow-auto-record-incoming'), + Row( + children: [ + Text('${translate('Directory')}:'), + Expanded( + child: GestureDetector( + onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null, + child: Text( + dir, + softWrap: true, + style: + const TextStyle(decoration: TextDecoration.underline), + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(initialDirectory: dir); + if (selectedDirectory != null) { + await bind.mainSetOption( + key: 'video-save-directory', + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), + ]); + }); + } + Widget language() { return _futureBuilder(future: () async { String langs = await bind.mainGetLangs(); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dad286ee7..1225e5a66 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -166,6 +166,7 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.imageModel), ChangeNotifierProvider.value(value: _ffi.cursorModel), ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), ], child: buildBody(context))); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 070ad217b..dd0a0bf05 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; +import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; import '../../common.dart'; @@ -134,6 +135,7 @@ class _RemoteMenubarState extends State { if (!isWeb) { menubarItems.add(_buildChat(context)); } + menubarItems.add(_buildRecording(context)); menubarItems.add(_buildClose(context)); return PopupMenuTheme( data: const PopupMenuThemeData( @@ -351,6 +353,24 @@ class _RemoteMenubarState extends State { ); } + Widget _buildRecording(BuildContext context) { + return Consumer( + builder: (context, value, child) => IconButton( + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () async { + await value.toggle(); + }, + icon: Icon( + value.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: _MenubarTheme.commonColor, + ), + )); + } + Widget _buildClose(BuildContext context) { return IconButton( tooltip: translate('Close'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 237378166..8bc3e8083 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -197,6 +197,7 @@ class FfiModel with ChangeNotifier { _display.height = int.parse(evt['height']); if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + parent.target?.recordingModel.switchDisplay(); } // remote is mobile, and orientation changed @@ -972,6 +973,41 @@ class QualityMonitorModel with ChangeNotifier { } } +class RecordingModel with ChangeNotifier { + WeakReference parent; + RecordingModel(this.parent); + bool _start = false; + get start => _start; + + switchDisplay() { + if (!isDesktop || !_start) return; + var id = parent.target?.id; + int? width = parent.target?.canvasModel.getDisplayWidth(); + int? height = parent.target?.canvasModel.getDisplayWidth(); + if (id == null || width == null || height == null) return; + bind.sessionRecordScreen( + id: id, start: _start, width: width, height: height); + } + + Future toggle() async { + if (!isDesktop) return; + var id = parent.target?.id; + int? width = parent.target?.canvasModel.getDisplayWidth(); + int? height = parent.target?.canvasModel.getDisplayWidth(); + if (id == null || width == null || height == null) return; + + await bind.sessionRecordScreen( + id: id, start: !_start, width: width, height: height); + _start = !_start; + notifyListeners(); + if (_start) { + Future.delayed(const Duration(milliseconds: 100), () { + bind.sessionRefresh(id: id); + }); + } + } +} + /// Mouse button enum. enum MouseButtons { left, right, wheel } @@ -1013,6 +1049,7 @@ class FFI { late final AbModel abModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session + late final RecordingModel recordingModel; // recording FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1025,6 +1062,7 @@ class FFI { abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); + recordingModel = RecordingModel(WeakReference(this)); } /// Send a mouse tap event(down and up). diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 79c25ee5b..a97c9adfd 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -325,6 +325,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" fixnum: dependency: transitive description: @@ -588,7 +595,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -602,7 +609,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -679,7 +686,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 05c711dcf..d50f6f970 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 rxdart: ^0.27.5 + file_picker: ^5.1.0 flutter_improved_scrolling: ^0.0.3 # currently, we use flutter 3.0.5 for windows build, latest for other builds. # diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 8d4f36b2d..e7377608b 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -30,6 +30,7 @@ filetime = "0.2" sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } +chrono = "0.4" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 50fcc07b2..02acfd9ff 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -38,6 +38,8 @@ pub use tokio_socks; pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; pub mod password_security; +pub use chrono; +pub use directories_next; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index c980d9d49..e2eb43177 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -20,6 +20,7 @@ libc = "0.2" num_cpus = "1.13" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } +webm = "1.0" [dependencies.winapi] version = "0.3" @@ -37,7 +38,6 @@ ndk = { version = "0.7", features = ["media"], optional = true} [target.'cfg(not(target_os = "android"))'.dev-dependencies] repng = "0.2" docopt = "1.1" -webm = "1.0" serde = {version="1.0", features=["derive"]} quest = "0.3" diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4fdef5462..32bcbd4a2 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -28,7 +28,7 @@ const CFG_KEY_ENCODER: &str = "bestHwEncoders"; const CFG_KEY_DECODER: &str = "bestHwDecoders"; const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; -const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; +pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = 60; const DEFAULT_HW_QUALITY: Quality = Quality_Default; const DEFAULT_RC: RateContorl = RC_DEFAULT; diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 78ea7c888..fe817c00a 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -39,6 +39,7 @@ pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer +pub mod record; mod vpx; #[inline] diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs new file mode 100644 index 000000000..d757fe8cd --- /dev/null +++ b/libs/scrap/src/common/record.rs @@ -0,0 +1,297 @@ +#[cfg(feature = "hwcodec")] +use hbb_common::anyhow::anyhow; +use hbb_common::{ + bail, chrono, + config::Config, + directories_next, + message_proto::{message, video_frame, EncodedVideoFrame, Message}, + ResultType, +}; +#[cfg(feature = "hwcodec")] +use hwcodec::mux::{MuxContext, Muxer}; +use std::{ + fs::{File, OpenOptions}, + io, + time::Instant, +}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, +}; +use webm::mux::{self, Segment, Track, VideoTrack, Writer}; + +const MIN_SECS: u64 = 1; + +#[derive(Debug, Clone, PartialEq)] +pub enum RecodeCodecID { + VP9, + H264, + H265, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext { + pub id: String, + pub filename: String, + pub width: usize, + pub height: usize, + pub codec_id: RecodeCodecID, +} + +impl RecorderContext { + pub fn set_filename(&mut self) -> ResultType<()> { + let mut dir = Config::get_option("video-save-directory"); + if !dir.is_empty() { + if !PathBuf::from(&dir).exists() { + std::fs::create_dir_all(&dir)?; + } + } else { + dir = Self::default_save_directory(); + if !dir.is_empty() && !PathBuf::from(&dir).exists() { + std::fs::create_dir_all(&dir)?; + } + } + let file = self.id.clone() + + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() + + if self.codec_id == RecodeCodecID::VP9 { + ".webm" + } else { + ".mp4" + }; + self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string(); + Ok(()) + } + + pub fn default_save_directory() -> String { + if let Some(user) = directories_next::UserDirs::new() { + if let Some(video_dir) = user.video_dir() { + return video_dir.join("RustDesk").to_string_lossy().to_string(); + } + } + "".to_owned() + } +} + +unsafe impl Send for Recorder {} +unsafe impl Sync for Recorder {} + +pub trait RecorderApi { + fn new(ctx: RecorderContext) -> ResultType + where + Self: Sized; + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; +} + +pub struct Recorder { + pub inner: Box, + ctx: RecorderContext, +} + +impl Deref for Recorder { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Recorder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl Recorder { + pub fn new(mut ctx: RecorderContext) -> ResultType { + ctx.set_filename()?; + let recorder = match ctx.codec_id { + RecodeCodecID::VP9 => Recorder { + inner: Box::new(WebmRecorder::new(ctx.clone())?), + ctx, + }, + #[cfg(feature = "hwcodec")] + _ => Recorder { + inner: Box::new(HwRecorder::new(ctx.clone())?), + ctx, + }, + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + Ok(recorder) + } + + fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { + ctx.set_filename()?; + self.inner = match ctx.codec_id { + RecodeCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), + #[cfg(feature = "hwcodec")] + _ => Box::new(HwRecorder::new(ctx.clone())?), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + self.ctx = ctx; + Ok(()) + } + + pub fn write_message(&mut self, msg: &Message) { + if let Some(message::Union::VideoFrame(vf)) = &msg.union { + if let Some(frame) = &vf.union { + self.write_frame(frame).ok(); + } + } + } + + pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { + match frame { + video_frame::Union::Vp9s(vp9s) => { + if self.ctx.codec_id != RecodeCodecID::VP9 { + self.change(RecorderContext { + codec_id: RecodeCodecID::VP9, + ..self.ctx.clone() + })?; + } + vp9s.frames.iter().map(|f| self.write_video(f)).count(); + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H264s(h264s) => { + if self.ctx.codec_id != RecodeCodecID::H264 { + self.change(RecorderContext { + codec_id: RecodeCodecID::H264, + ..self.ctx.clone() + })?; + } + if self.ctx.codec_id == RecodeCodecID::H264 { + h264s.frames.last().map(|f| self.write_video(f)); + } + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H265s(h265s) => { + if self.ctx.codec_id != RecodeCodecID::H265 { + self.change(RecorderContext { + codec_id: RecodeCodecID::H265, + ..self.ctx.clone() + })?; + } + if self.ctx.codec_id == RecodeCodecID::H265 { + h265s.frames.last().map(|f| self.write_video(f)); + } + } + _ => bail!("unsupported frame type"), + } + Ok(()) + } +} + +struct WebmRecorder { + vt: VideoTrack, + webm: Option>>, + ctx: RecorderContext, + key: bool, + written: bool, + start: Instant, +} + +impl RecorderApi for WebmRecorder { + fn new(ctx: RecorderContext) -> ResultType { + let out = match { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&ctx.filename) + } { + Ok(file) => file, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?, + Err(e) => return Err(e.into()), + }; + let mut webm = match mux::Segment::new(mux::Writer::new(out)) { + Some(v) => v, + None => bail!("Failed to create webm mux"), + }; + let vt = webm.add_video_track( + ctx.width as _, + ctx.height as _, + None, + mux::VideoCodecId::VP9, + ); + Ok(WebmRecorder { + vt, + webm: Some(webm), + ctx, + key: false, + written: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + if frame.key { + self.key = true; + } + if self.key { + let ok = self + .vt + .add_frame(&frame.data, frame.pts as u64 * 1_000_000, frame.key); + if ok { + self.written = true; + } + ok + } else { + false + } + } +} + +impl Drop for WebmRecorder { + fn drop(&mut self) { + std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + std::fs::remove_file(&self.ctx.filename).ok(); + } + } +} + +#[cfg(feature = "hwcodec")] +struct HwRecorder { + muxer: Muxer, + ctx: RecorderContext, + written: bool, + start: Instant, +} + +#[cfg(feature = "hwcodec")] +impl RecorderApi for HwRecorder { + fn new(ctx: RecorderContext) -> ResultType { + let muxer = Muxer::new(MuxContext { + filename: ctx.filename.clone(), + width: ctx.width, + height: ctx.height, + is265: ctx.codec_id == RecodeCodecID::H265, + framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _, + }) + .map_err(|_| anyhow!("Failed to create hardware muxer"))?; + Ok(HwRecorder { + muxer, + ctx, + written: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + let ok = self.muxer.write_video(&frame.data, frame.pts).is_ok(); + if ok { + self.written = true; + } + ok + } +} + +#[cfg(feature = "hwcodec")] +impl Drop for HwRecorder { + fn drop(&mut self) { + self.muxer.write_tail().ok(); + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + std::fs::remove_file(&self.ctx.filename).ok(); + } + } +} diff --git a/src/client.rs b/src/client.rs index baf06833a..c70956b63 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - net::SocketAddr, - ops::{Deref, Not}, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, -}; -use std::sync::atomic::Ordering; pub use async_trait::async_trait; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ @@ -13,6 +6,13 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; +use std::sync::atomic::Ordering; +use std::{ + collections::HashMap, + net::SocketAddr, + ops::{Deref, Not}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, +}; use uuid::Uuid; pub use file_trait::FileManager; @@ -39,6 +39,7 @@ pub use helper::LatencyController; pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, + record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, }; @@ -154,8 +155,7 @@ impl Client { return Err(err); } } - Ok(x) => { - Ok(x)}, + Ok(x) => Ok(x), } } @@ -798,6 +798,8 @@ pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, pub rgb: Vec, + recorder: Arc>>, + record: bool, } impl VideoHandler { @@ -812,6 +814,8 @@ impl VideoHandler { }), latency_controller, rgb: Default::default(), + recorder: Default::default(), + record: false, } } @@ -825,32 +829,21 @@ impl VideoHandler { .update_video(vf.timestamp); } match &vf.union { - Some(frame) => self.decoder.handle_video_frame(frame, &mut self.rgb), + Some(frame) => { + let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + if self.record { + self.recorder + .lock() + .unwrap() + .as_mut() + .map(|r| r.write_frame(frame)); + } + res + } _ => Ok(false), } } - /// Handle a VP9S frame. - // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { - // let mut last_frame = Image::new(); - // for vp9 in vp9s.frames.iter() { - // for frame in self.decoder.decode(&vp9.data)? { - // drop(last_frame); - // last_frame = frame; - // } - // } - // for frame in self.decoder.flush()? { - // drop(last_frame); - // last_frame = frame; - // } - // if last_frame.is_null() { - // Ok(false) - // } else { - // last_frame.rgb(1, true, &mut self.rgb); - // Ok(true) - // } - // } - /// Reset the decoder. pub fn reset(&mut self) { self.decoder = Decoder::new(DecoderCfg { @@ -860,6 +853,24 @@ impl VideoHandler { }, }); } + + /// Start or stop screen record. + pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) { + self.record = false; + if start { + self.recorder = Recorder::new(RecorderContext { + id, + filename: "".to_owned(), + width: w as _, + height: h as _, + codec_id: scrap::record::RecodeCodecID::VP9, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); + } else { + self.recorder = Default::default(); + } + self.record = start; + } } /// Login config handler for [`Client`]. @@ -1395,6 +1406,7 @@ pub enum MediaData { AudioFrame(AudioFrame), AudioFormat(AudioFormat), Reset, + RecordScreen(bool, i32, i32, String), } pub type MediaSender = mpsc::Sender; @@ -1429,6 +1441,9 @@ where MediaData::Reset => { video_handler.reset(); } + MediaData::RecordScreen(start, w, h, id) => { + video_handler.record_screen(start, w, h, id) + } _ => {} } } else { @@ -1703,6 +1718,7 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), + RecordScreen(bool, i32, i32, String), } /// Keycode for key events. @@ -1892,4 +1908,4 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 pub fn disable_keyboard_listening() { crate::ui_session_interface::KEYBOARD_HOOKED.store(false, Ordering::SeqCst); -} \ No newline at end of file +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index cc2ca1dae..1cf89f173 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -601,6 +601,11 @@ impl Remote { } } } + Data::RecordScreen(start, w, h, id) => { + let _ = self + .video_sender + .send(MediaData::RecordScreen(start, w, h, id)); + } _ => {} } true @@ -794,13 +799,8 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - self.handler.update_folder_files( - fd.id, - &entries, - fd.path, - false, - false, - ); + self.handler + .update_folder_files(fd.id, &entries, fd.path, false, false); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f41108895..1404627f5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,13 +19,13 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::get_sound_inputs; use crate::ui_interface::{ - change_id, check_mouse_time, check_super_user_permission, discover, forget_password, - get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, - get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, - get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, - set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, - update_temporary_password, using_public_server, + change_id, check_mouse_time, check_super_user_permission, default_video_save_directory, + discover, forget_password, get_api_server, get_app_name, get_async_job_status, + get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, + get_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid, + get_version, has_hwcodec, has_rendezvous_service, post_request, send_to_cm, set_local_option, + set_option, set_options, set_peer_option, set_permanent_password, set_socks, store_fav, + test_if_valid_server, update_temporary_password, using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -162,6 +162,12 @@ pub fn session_refresh(id: String) { } } +pub fn session_record_screen(id: String, start: bool, width: usize, height: usize) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.record_screen(start, width as _, height as _); + } +} + pub fn session_reconnect(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.reconnect(); @@ -705,6 +711,10 @@ pub fn main_change_language(lang: String) { send_to_cm(&crate::ipc::Data::Language(lang)); } +pub fn main_default_video_save_directory() -> String { + default_video_save_directory() +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 664d6f05b..65c7b529d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "允许RDP访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), + ("Recording", "录屏"), + ("Directory", "目录"), + ("Automatically record incoming sessions", "自动录制来访会话"), + ("Change", "更改"), + ("Start session recording", "开始录屏"), + ("Stop session recording", "结束录屏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ace56788f..a271e2446 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Připnout panel nabídek"), ("Unpin menubar", "Odepnout panel nabídek"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 27724f7b3..77f585390 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Fastgør menulinjen"), ("Unpin menubar", "Frigør menulinjen"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 8d90be381..27e04717e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin-Menüleiste"), ("Unpin menubar", "Menüleiste lösen"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 6c7bb5aa8..a3add20f9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Alpingla menubreto"), ("Unpin menubar", "Malfiksi menubreton"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c8296ced5..049a8c428 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin barra de menú"), ("Unpin menubar", "Desbloquear barra de menú"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d9a42e934..033e08c4c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Épingler la barre de menus"), ("Unpin menubar", "Détacher la barre de menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b35224c03..4cff53b2d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Menüsor rögzítése"), ("Unpin menubar", "Menüsor rögzítésének feloldása"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 657014141..dd4adf2bc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin menubar"), ("Unpin menubar", "Unpin menubar"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8f6dfb3d9..dd941b30b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -345,5 +345,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Blocca la barra dei menu"), ("Unpin menubar", "Sblocca la barra dei menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d0a2a2f7..2d76c93db 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -343,5 +343,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "メニューバーを固定する"), ("Unpin menubar", "メニューバーのピン留めを外す"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ca939e2b8..5897dc690 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "핀 메뉴 바"), ("Unpin menubar", "메뉴 모음 고정 해제"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 720b7109f..c2f5f2cf0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -1,325 +1,331 @@ lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "Күй"), - ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), - ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), - ("Password", "Құпия сөз"), - ("Ready", "Дайын"), - ("Established", "Қосылды"), - ("connecting_status", "RustDesk желісіне қосылуда..."), - ("Enable Service", "Сербесті қосу"), - ("Start Service", "Сербесті іске қосу"), - ("Service is running", "Сербес істеуде"), - ("Service is not running", "Сербес істемеуде"), - ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), - ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), - ("Transfer File", "Файыл Тасымалдау"), - ("Connect", "Қосылу"), - ("Recent Sessions", "Соңғы Сештер"), - ("Address Book", "Мекенжай Кітабы"), - ("Confirmation", "Мақұлдау"), - ("TCP Tunneling", "TCP тунелдеу"), - ("Remove", "Жою"), - ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), - ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), - ("Enable Keyboard/Mouse", "Пернетақта/Тінтуірді қосу"), - ("Enable Clipboard", "Көшіру-тақтасын қосу"), - ("Enable File Transfer", "Файыл Тасымалдауды қосу"), - ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), - ("IP Whitelisting", "IP Ақ-тізімі"), - ("ID/Relay Server", "ID/Relay сербері"), - ("Stop service", "Сербесті тоқтату"), - ("Change ID", "ID ауыстыру"), - ("Website", "Web-сайт"), - ("About", "Туралы"), - ("Mute", "Дыбыссыздандыру"), - ("Audio Input", "Аудио Еңгізу"), - ("Enhancements", "Жақсартулар"), - ("Hardware Codec", "Hardware Codec"), - ("Adaptive Bitrate", "Adaptive Bitrate"), - ("ID Server", "ID Сербері"), - ("Relay Server", "Relay Сербері"), - ("API Server", "API Сербері"), - ("invalid_http", "http:// немесе https://'пен басталуы қажет"), - ("Invalid IP", "Бұрыс IP-Мекенжай"), - ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), - ("Invalid format", "Бұрыс формат"), - ("server_not_support", "Сербер әзірше қолдамайды"), - ("Not available", "Қолжетімсіз"), - ("Too frequent", "Тым жиі"), - ("Cancel", "Болдырмау"), - ("Skip", "Өткізіп жіберу"), - ("Close", "Жабу"), - ("Retry", "Қайтадан көру"), - ("OK", "OK"), - ("Password Required", "Құпия сөз Қажет"), - ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), - ("Remember password", "Құпия сөзді есте сақтау"), - ("Wrong Password", "Бұрыс Құпия сөз"), - ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), - ("Connection Error", "Қосылым Қатесі"), - ("Error", "Қате"), - ("Reset by the peer", "Пир қалпына келтірді"), - ("Connecting...", "Қосылуда..."), - ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), - ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), - ("Login Error", "Кіру Қатесі"), - ("Successful", "Сәтті"), - ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), - ("Name", "Ат"), - ("Type", "Түр"), - ("Modified", "Өзгертілді"), - ("Size", "Өлшем"), - ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), - ("Receive", "Қабылдау"), - ("Send", "Жіберу"), - ("Refresh File", "Файылды жаңарту"), - ("Local", "Лақал"), - ("Remote", "Қашықтағы"), - ("Remote Computer", "Қашықтағы Қампұтыр"), - ("Local Computer", "Лақал Қампұтыр"), - ("Confirm Delete", "Жоюды Растау"), - ("Delete", "Жою"), - ("Properties", "Қасиеттер"), - ("Multi Select", "Көптік таңдау"), - ("Empty Directory", "Бос Бума"), - ("Not an empty directory", "Бос бума емес"), - ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), - ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), - ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), - ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), - ("This is irreversible!", "Бұл қайтымсыз!"), - ("Deleting", "Жойылу"), - ("files", "файылдар"), - ("Waiting", "Күту"), - ("Finished", "Аяқталды"), - ("Speed", "Жылдамдық"), - ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), - ("Privacy mode", "Құпиялылық Модасы"), - ("Block user input", "Қолданушы еңгізуін бұғаттау"), - ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), - ("Adjust Window", "Терезені Реттеу"), - ("Original", "Түпнұсқа"), - ("Shrink", "Қысу"), - ("Stretch", "Созу"), - ("Scrollbar", "Scrollbar"), - ("ScrollAuto", "ScrollAuto"), - ("Good image quality", "Жақсы сурет сапасы"), - ("Balanced", "Теңдестірілген"), - ("Optimize reaction time", "Реакция уақытын оңтайландыру"), - ("Custom", "Теңшеулі"), - ("Show remote cursor", "Қашықтағы курсорды көрсету"), - ("Show quality monitor", "Сапа мониторын көрсету"), - ("Disable clipboard", "Көшіру-тақтасын өшіру"), - ("Lock after session end", "Сеш аяқталған соң құлыптау"), - ("Insert", "Кірістіру"), - ("Insert Lock", "Кірістіруді Құлыптау"), - ("Refresh", "Жаңарту"), - ("ID does not exist", "ID табылмады"), - ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), - ("Please try later", "Кейінірек қайта көруді өтінеміз"), - ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), - ("Key mismatch", "Кілт сәйкессіздігі"), - ("Timeout", "Үзіліс"), - ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), - ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), - ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), - ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), - ("Set Password", "Құпия сөзді Орнату"), - ("OS Password", "OS Құпия сөзі"), - ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), - ("Click to upgrade", "Жаңғырту үшін басыңыз"), - ("Click to download", "Жүктеу үшін басыңыз"), - ("Click to update", "Жаңарту үшін басыңыз"), - ("Configure", "Қалыптау"), - ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), - ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), - ("Installing ...", "Орнатылу..."), - ("Install", "Орнату"), - ("Installation", "Орнатылу"), - ("Installation Path", "Орнатылу Жолы"), - ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), - ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), - ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), - ("Accept and Install", "Қабылдау және Орнату"), - ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), - ("Generating ...", "Генератталуда..."), - ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), - ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), - ("Listening ...", "Тыңдау ..."), - ("Remote Host", "Қашықтағы Хост"), - ("Remote Port", "Қашықтағы Порт"), - ("Action", "Әрекет"), - ("Add", "Қосу"), - ("Local Port", "Лақал Порт"), - ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), - ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), - ("The confirmation is not identical.", "Растау сәйкес келмейді."), - ("Permissions", "Рұқсаттар"), - ("Accept", "Қабылдау"), - ("Dismiss", "Босату"), - ("Disconnect", "Ажырату"), - ("Allow using keyboard and mouse", "Пернетақта мен тінтуірді қолдануды рұқсат ету"), - ("Allow using clipboard", "Көшіру-тақтасын рұқсат ету"), - ("Allow hearing sound", "Дыбыс естуді рұқсат ету"), - ("Allow file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), - ("Connected", "Қосылды"), - ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), - ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), - ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), - ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), - ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), - ("Enter your password", "Құпия сөзіңізді енгізіңіз"), - ("Logging in...", "Кіруде..."), - ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), - ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), - ("Enable Direct IP Access", "Тікелей IP Қолжетімді іске қосу"), - ("Rename", "Атын өзгерту"), - ("Space", "Орын"), - ("Create Desktop Shortcut", "Жұмыс үстелі Таңбашасын Жасау"), - ("Change Path", "Жолды өзгерту"), - ("Create Folder", "Бума жасау"), - ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), - ("Fix it", "Түзету"), - ("Warning", "Ескерту"), - ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), - ("Reboot required", "Қайта-қосу қажет"), - ("Unsupported display server ", "Қолдаусыз дисплей сербері"), - ("x11 expected", "x11 күтілген"), - ("Port", "Порт"), - ("Settings", "Орнатпалар"), - ("Username", "Қолданушы аты"), - ("Invalid port", "Бұрыс порт"), - ("Closed manually by the peer", "Пир қолымен жабылған"), - ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), - ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), - ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), - ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), - ("Login", "Кіру"), - ("Logout", "Шығу"), - ("Tags", "Тақтар"), - ("Search ID", "ID Іздеу"), - ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), - ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), - ("Add ID", "ID Қосу"), - ("Add Tag", "Тақ Қосу"), - ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), - ("Network error", "Желі қатесі"), - ("Username missed", "Қолданушы аты бос"), - ("Password missed", "Құпия сөз бос"), - ("Wrong credentials", "Бұрыс тіркелгі деректер"), - ("Edit Tag", "Тақты Өндеу"), - ("Unremember Password", "Құпия сөзді Ұмыту"), - ("Favorites", "Таңдаулылар"), - ("Add to Favorites", "Таңдаулыларға Қосу"), - ("Remove from Favorites", "Таңдаулылардан алып тастау"), - ("Empty", "Бос"), - ("Invalid folder name", "Бұрыс бума атауы"), - ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Хост атауы"), - ("Discovered", "Табылды"), - ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), - ("Remote ID", "Қашықтағы ID"), - ("Paste", "Қою"), - ("Paste here?", "Осында қою керек пе?"), - ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), - ("Download new version", "Жаңа нұсқаны жүктеу"), - ("Touch mode", "Жанасатын мода"), - ("Mouse mode", "Тінтуірлі мода"), - ("One-Finger Tap", "Бір-Саусақпен Түрту"), - ("Left Mouse", "Солақ Тінтуір"), - ("One-Long Tap", "Бір-Ұзақ Түрту"), - ("Two-Finger Tap", "Екі-Саусақпен Түрту"), - ("Right Mouse", "Оңақ Тінтуір"), - ("One-Finger Move", "Бір-Саусақпен Жылжыту"), - ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), - ("Mouse Drag", "Тінтуір Тартуы"), - ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), - ("Mouse Wheel", "Тінтуір Дөңгелегі"), - ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), - ("Canvas Move", "Кенеп Жылжуы"), - ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), - ("Canvas Zoom", "Кенеп Зумы"), - ("Reset canvas", "Кенепті қалпына келтіру"), - ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), - ("Note", "Нота"), - ("Connection", "Қосылым"), - ("Share Screen", "Екіренді Бөлісу"), - ("CLOSE", "ЖАБУ"), - ("OPEN", "АШУ"), - ("Chat", "Чат"), - ("Total", "Барлығы"), - ("items", "зат"), - ("Selected", "Таңдалған"), - ("Screen Capture", "Екіренді Түсіру"), - ("Input Control", "Еңгізуді Басқару/Қадағалау"), - ("Audio Capture", "Аудио Түсіру"), - ("File Connection", "Файыл Қосылымы"), - ("Screen Connection", "Екірен Қосылымы"), - ("Do you accept?", "Қабылдайсыз ба?"), - ("Open System Setting", "Жүйе Орнатпаларын Ашу"), - ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), - ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), - ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), - ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), - ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), - ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), - ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), - ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), - ("Account", "Есепкі"), - ("Overwrite", "Үстінен қайта жазу"), - ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), - ("Quit", "Шығу"), - ("doc_mac_permission", ""), - ("Help", "Көмек"), - ("Failed", "Сәтсіз"), - ("Succeeded", "Сәтті"), - ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), - ("Unsupported", "Қолдаусыз"), - ("Peer denied", "Пир қабылдамады"), - ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), - ("Peer exit", "Пирдің шығуы"), - ("Failed to turn off", "Сөндіру сәтсіз болды"), - ("Turned off", "Өшірілген"), - ("In privacy mode", "Құпиялылық модасында"), - ("Out privacy mode", "Құпиялылық модасынан Шығу"), - ("Language", "Тіл"), - ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), - ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), - ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), - ("Connection not allowed", "Қосылу рұқсат етілмеген"), - ("Use temporary password", "Уақытша құпия сөзді қолдану"), - ("Use permanent password", "Тұрақты құпия сөзді қолдану"), - ("Use both passwords", "Қос құпия сөзді қолдану"), - ("Set permanent password", "Тұрақты құпия сөзді орнату"), - ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), - ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), - ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), - ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), - ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), - ("Restarting Remote Device", "Қашықтағы Құрылғыны қайта-қосуда"), - ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), - ("Copied", "Көшірілді"), - ("Exit Fullscreen", "Толық екіреннен Шығу"), - ("Fullscreen", "Толық екірен"), - ("Mobile Actions", "Мабыл Әрекеттері"), - ("Select Monitor", "Мониторды Таңдау"), - ("Control Actions", "Басқару Әрекеттері"), - ("Display Settings", "Дисплей Орнатпалары"), - ("Ratio", "Арақатынас"), - ("Image Quality", "Сурет Сапасы"), - ("Scroll Style", "Scroll Теңшетұрі"), - ("Show Menubar", "Мәзір жолағын көрсету"), - ("Hide Menubar", "Мәзір жолағын жасыру"), - ("Direct Connection", "Тікелей Қосылым"), - ("Relay Connection", "Релай Қосылым"), - ("Secure Connection", "Қауіпсіз Қосылым"), - ("Insecure Connection", "Қатерлі Қосылым"), - ("Scale original", "Scale original"), - ("Scale adaptive", "Scale adaptive"), - ("Pin menubar", "Мәзір жолағын бекіту"), - ("Unpin menubar", "Мәзір жолағын босату"), - ].iter().cloned().collect(); - } +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Күй"), + ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), + ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), + ("Password", "Құпия сөз"), + ("Ready", "Дайын"), + ("Established", "Қосылды"), + ("connecting_status", "RustDesk желісіне қосылуда..."), + ("Enable Service", "Сербесті қосу"), + ("Start Service", "Сербесті іске қосу"), + ("Service is running", "Сербес істеуде"), + ("Service is not running", "Сербес істемеуде"), + ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), + ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), + ("Transfer File", "Файыл Тасымалдау"), + ("Connect", "Қосылу"), + ("Recent Sessions", "Соңғы Сештер"), + ("Address Book", "Мекенжай Кітабы"), + ("Confirmation", "Мақұлдау"), + ("TCP Tunneling", "TCP тунелдеу"), + ("Remove", "Жою"), + ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), + ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), + ("Enable Keyboard/Mouse", "Пернетақта/Тінтуірді қосу"), + ("Enable Clipboard", "Көшіру-тақтасын қосу"), + ("Enable File Transfer", "Файыл Тасымалдауды қосу"), + ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), + ("IP Whitelisting", "IP Ақ-тізімі"), + ("ID/Relay Server", "ID/Relay сербері"), + ("Stop service", "Сербесті тоқтату"), + ("Change ID", "ID ауыстыру"), + ("Website", "Web-сайт"), + ("About", "Туралы"), + ("Mute", "Дыбыссыздандыру"), + ("Audio Input", "Аудио Еңгізу"), + ("Enhancements", "Жақсартулар"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Adaptive Bitrate"), + ("ID Server", "ID Сербері"), + ("Relay Server", "Relay Сербері"), + ("API Server", "API Сербері"), + ("invalid_http", "http:// немесе https://'пен басталуы қажет"), + ("Invalid IP", "Бұрыс IP-Мекенжай"), + ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), + ("Invalid format", "Бұрыс формат"), + ("server_not_support", "Сербер әзірше қолдамайды"), + ("Not available", "Қолжетімсіз"), + ("Too frequent", "Тым жиі"), + ("Cancel", "Болдырмау"), + ("Skip", "Өткізіп жіберу"), + ("Close", "Жабу"), + ("Retry", "Қайтадан көру"), + ("OK", "OK"), + ("Password Required", "Құпия сөз Қажет"), + ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), + ("Remember password", "Құпия сөзді есте сақтау"), + ("Wrong Password", "Бұрыс Құпия сөз"), + ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), + ("Connection Error", "Қосылым Қатесі"), + ("Error", "Қате"), + ("Reset by the peer", "Пир қалпына келтірді"), + ("Connecting...", "Қосылуда..."), + ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), + ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), + ("Login Error", "Кіру Қатесі"), + ("Successful", "Сәтті"), + ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), + ("Name", "Ат"), + ("Type", "Түр"), + ("Modified", "Өзгертілді"), + ("Size", "Өлшем"), + ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), + ("Receive", "Қабылдау"), + ("Send", "Жіберу"), + ("Refresh File", "Файылды жаңарту"), + ("Local", "Лақал"), + ("Remote", "Қашықтағы"), + ("Remote Computer", "Қашықтағы Қампұтыр"), + ("Local Computer", "Лақал Қампұтыр"), + ("Confirm Delete", "Жоюды Растау"), + ("Delete", "Жою"), + ("Properties", "Қасиеттер"), + ("Multi Select", "Көптік таңдау"), + ("Empty Directory", "Бос Бума"), + ("Not an empty directory", "Бос бума емес"), + ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), + ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), + ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), + ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), + ("This is irreversible!", "Бұл қайтымсыз!"), + ("Deleting", "Жойылу"), + ("files", "файылдар"), + ("Waiting", "Күту"), + ("Finished", "Аяқталды"), + ("Speed", "Жылдамдық"), + ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), + ("Privacy mode", "Құпиялылық Модасы"), + ("Block user input", "Қолданушы еңгізуін бұғаттау"), + ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), + ("Adjust Window", "Терезені Реттеу"), + ("Original", "Түпнұсқа"), + ("Shrink", "Қысу"), + ("Stretch", "Созу"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Жақсы сурет сапасы"), + ("Balanced", "Теңдестірілген"), + ("Optimize reaction time", "Реакция уақытын оңтайландыру"), + ("Custom", "Теңшеулі"), + ("Show remote cursor", "Қашықтағы курсорды көрсету"), + ("Show quality monitor", "Сапа мониторын көрсету"), + ("Disable clipboard", "Көшіру-тақтасын өшіру"), + ("Lock after session end", "Сеш аяқталған соң құлыптау"), + ("Insert", "Кірістіру"), + ("Insert Lock", "Кірістіруді Құлыптау"), + ("Refresh", "Жаңарту"), + ("ID does not exist", "ID табылмады"), + ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), + ("Please try later", "Кейінірек қайта көруді өтінеміз"), + ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), + ("Key mismatch", "Кілт сәйкессіздігі"), + ("Timeout", "Үзіліс"), + ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), + ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), + ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), + ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), + ("Set Password", "Құпия сөзді Орнату"), + ("OS Password", "OS Құпия сөзі"), + ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), + ("Click to upgrade", "Жаңғырту үшін басыңыз"), + ("Click to download", "Жүктеу үшін басыңыз"), + ("Click to update", "Жаңарту үшін басыңыз"), + ("Configure", "Қалыптау"), + ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), + ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), + ("Installing ...", "Орнатылу..."), + ("Install", "Орнату"), + ("Installation", "Орнатылу"), + ("Installation Path", "Орнатылу Жолы"), + ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), + ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), + ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), + ("Accept and Install", "Қабылдау және Орнату"), + ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), + ("Generating ...", "Генератталуда..."), + ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), + ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), + ("Listening ...", "Тыңдау ..."), + ("Remote Host", "Қашықтағы Хост"), + ("Remote Port", "Қашықтағы Порт"), + ("Action", "Әрекет"), + ("Add", "Қосу"), + ("Local Port", "Лақал Порт"), + ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), + ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), + ("The confirmation is not identical.", "Растау сәйкес келмейді."), + ("Permissions", "Рұқсаттар"), + ("Accept", "Қабылдау"), + ("Dismiss", "Босату"), + ("Disconnect", "Ажырату"), + ("Allow using keyboard and mouse", "Пернетақта мен тінтуірді қолдануды рұқсат ету"), + ("Allow using clipboard", "Көшіру-тақтасын рұқсат ету"), + ("Allow hearing sound", "Дыбыс естуді рұқсат ету"), + ("Allow file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), + ("Connected", "Қосылды"), + ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), + ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), + ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), + ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), + ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), + ("Enter your password", "Құпия сөзіңізді енгізіңіз"), + ("Logging in...", "Кіруде..."), + ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), + ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), + ("Enable Direct IP Access", "Тікелей IP Қолжетімді іске қосу"), + ("Rename", "Атын өзгерту"), + ("Space", "Орын"), + ("Create Desktop Shortcut", "Жұмыс үстелі Таңбашасын Жасау"), + ("Change Path", "Жолды өзгерту"), + ("Create Folder", "Бума жасау"), + ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), + ("Fix it", "Түзету"), + ("Warning", "Ескерту"), + ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), + ("Reboot required", "Қайта-қосу қажет"), + ("Unsupported display server ", "Қолдаусыз дисплей сербері"), + ("x11 expected", "x11 күтілген"), + ("Port", "Порт"), + ("Settings", "Орнатпалар"), + ("Username", "Қолданушы аты"), + ("Invalid port", "Бұрыс порт"), + ("Closed manually by the peer", "Пир қолымен жабылған"), + ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), + ("Run without install", "Орнатпай-ақ Іске қосу"), + ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), + ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), + ("Login", "Кіру"), + ("Logout", "Шығу"), + ("Tags", "Тақтар"), + ("Search ID", "ID Іздеу"), + ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), + ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), + ("Add ID", "ID Қосу"), + ("Add Tag", "Тақ Қосу"), + ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), + ("Network error", "Желі қатесі"), + ("Username missed", "Қолданушы аты бос"), + ("Password missed", "Құпия сөз бос"), + ("Wrong credentials", "Бұрыс тіркелгі деректер"), + ("Edit Tag", "Тақты Өндеу"), + ("Unremember Password", "Құпия сөзді Ұмыту"), + ("Favorites", "Таңдаулылар"), + ("Add to Favorites", "Таңдаулыларға Қосу"), + ("Remove from Favorites", "Таңдаулылардан алып тастау"), + ("Empty", "Бос"), + ("Invalid folder name", "Бұрыс бума атауы"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Хост атауы"), + ("Discovered", "Табылды"), + ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), + ("Remote ID", "Қашықтағы ID"), + ("Paste", "Қою"), + ("Paste here?", "Осында қою керек пе?"), + ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), + ("Download new version", "Жаңа нұсқаны жүктеу"), + ("Touch mode", "Жанасатын мода"), + ("Mouse mode", "Тінтуірлі мода"), + ("One-Finger Tap", "Бір-Саусақпен Түрту"), + ("Left Mouse", "Солақ Тінтуір"), + ("One-Long Tap", "Бір-Ұзақ Түрту"), + ("Two-Finger Tap", "Екі-Саусақпен Түрту"), + ("Right Mouse", "Оңақ Тінтуір"), + ("One-Finger Move", "Бір-Саусақпен Жылжыту"), + ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), + ("Mouse Drag", "Тінтуір Тартуы"), + ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), + ("Mouse Wheel", "Тінтуір Дөңгелегі"), + ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), + ("Canvas Move", "Кенеп Жылжуы"), + ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), + ("Canvas Zoom", "Кенеп Зумы"), + ("Reset canvas", "Кенепті қалпына келтіру"), + ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), + ("Note", "Нота"), + ("Connection", "Қосылым"), + ("Share Screen", "Екіренді Бөлісу"), + ("CLOSE", "ЖАБУ"), + ("OPEN", "АШУ"), + ("Chat", "Чат"), + ("Total", "Барлығы"), + ("items", "зат"), + ("Selected", "Таңдалған"), + ("Screen Capture", "Екіренді Түсіру"), + ("Input Control", "Еңгізуді Басқару/Қадағалау"), + ("Audio Capture", "Аудио Түсіру"), + ("File Connection", "Файыл Қосылымы"), + ("Screen Connection", "Екірен Қосылымы"), + ("Do you accept?", "Қабылдайсыз ба?"), + ("Open System Setting", "Жүйе Орнатпаларын Ашу"), + ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), + ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), + ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), + ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), + ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), + ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), + ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), + ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), + ("Account", "Есепкі"), + ("Overwrite", "Үстінен қайта жазу"), + ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), + ("Quit", "Шығу"), + ("doc_mac_permission", ""), + ("Help", "Көмек"), + ("Failed", "Сәтсіз"), + ("Succeeded", "Сәтті"), + ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), + ("Unsupported", "Қолдаусыз"), + ("Peer denied", "Пир қабылдамады"), + ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), + ("Peer exit", "Пирдің шығуы"), + ("Failed to turn off", "Сөндіру сәтсіз болды"), + ("Turned off", "Өшірілген"), + ("In privacy mode", "Құпиялылық модасында"), + ("Out privacy mode", "Құпиялылық модасынан Шығу"), + ("Language", "Тіл"), + ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), + ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), + ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Connection not allowed", "Қосылу рұқсат етілмеген"), + ("Use temporary password", "Уақытша құпия сөзді қолдану"), + ("Use permanent password", "Тұрақты құпия сөзді қолдану"), + ("Use both passwords", "Қос құпия сөзді қолдану"), + ("Set permanent password", "Тұрақты құпия сөзді орнату"), + ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), + ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), + ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), + ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), + ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), + ("Restarting Remote Device", "Қашықтағы Құрылғыны қайта-қосуда"), + ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), + ("Copied", "Көшірілді"), + ("Exit Fullscreen", "Толық екіреннен Шығу"), + ("Fullscreen", "Толық екірен"), + ("Mobile Actions", "Мабыл Әрекеттері"), + ("Select Monitor", "Мониторды Таңдау"), + ("Control Actions", "Басқару Әрекеттері"), + ("Display Settings", "Дисплей Орнатпалары"), + ("Ratio", "Арақатынас"), + ("Image Quality", "Сурет Сапасы"), + ("Scroll Style", "Scroll Теңшетұрі"), + ("Show Menubar", "Мәзір жолағын көрсету"), + ("Hide Menubar", "Мәзір жолағын жасыру"), + ("Direct Connection", "Тікелей Қосылым"), + ("Relay Connection", "Релай Қосылым"), + ("Secure Connection", "Қауіпсіз Қосылым"), + ("Insecure Connection", "Қатерлі Қосылым"), + ("Scale original", "Scale original"), + ("Scale adaptive", "Scale adaptive"), + ("Pin menubar", "Мәзір жолағын бекіту"), + ("Unpin menubar", "Мәзір жолағын босату"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fe45ddf3e..b54218d56 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -344,5 +344,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Przypnij pasek menu"), ("Unpin menubar", "Odepnij pasek menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 858afd8a1..7b386c3bf 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Fixar barra de menu"), ("Unpin menubar", "Desenganxa la barra de menús"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af4f0b52e..099ecb2bb 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", ""), ("Unpin menubar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 04cfed485..c001b8770 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Закрепить строку меню"), ("Unpin menubar", "Открепить строку меню"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8ae17b1ad..2a1de16aa 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pripnúť panel s ponukami"), ("Unpin menubar", "Uvoľniť panel s ponukami"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 914b103df..5896d4336 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", ""), ("Unpin menubar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b1b029b39..9e9475ead 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Menü çubuğunu sabitle"), ("Unpin menubar", "Menü çubuğunun sabitlemesini kaldır"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 764f666e7..dc7ab8a59 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "允許RDP訪問"), ("Pin menubar", "固定菜單欄"), ("Unpin menubar", "取消固定菜單欄"), + ("Recording", "錄屏"), + ("Directory", "目錄"), + ("Automatically record incoming sessions", "自動錄製來訪會話"), + ("Change", "變更"), + ("Start session recording", "開始錄屏"), + ("Stop session recording", "結束錄屏"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f177581f9..ebd44d8d5 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Ghim thanh menu"), ("Unpin menubar", "Bỏ ghim thanh menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index eee9e4255..272bcf8d5 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -25,6 +25,7 @@ use hbb_common::tokio::sync::{ }; use scrap::{ codec::{Encoder, EncoderCfg, HwEncoderConfig}, + record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, Capturer, Display, TraitCapturer, }; @@ -435,6 +436,21 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] log::info!("gdi: {}", c.is_gdi()); let codec_name = Encoder::current_hw_encoder_name(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { + Recorder::new(RecorderContext { + id: "local".to_owned(), + filename: "".to_owned(), + width: c.width, + height: c.height, + codec_id: scrap::record::RecodeCodecID::VP9, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) + } else { + Default::default() + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let recorder: Arc>> = Default::default(); while sp.ok() { #[cfg(windows)] @@ -495,7 +511,8 @@ fn run(sp: GenericService) -> ResultType<()> { } scrap::Frame::RAW(data) => { if (data.len() != 0) { - let send_conn_ids = handle_one_frame(&sp, data, ms, &mut encoder)?; + let send_conn_ids = + handle_one_frame(&sp, data, ms, &mut encoder, recorder.clone())?; frame_controller.set_send(now, send_conn_ids); } } @@ -511,7 +528,8 @@ fn run(sp: GenericService) -> ResultType<()> { Ok(frame) => { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; - let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut encoder)?; + let send_conn_ids = + handle_one_frame(&sp, &frame, ms, &mut encoder, recorder.clone())?; frame_controller.set_send(now, send_conn_ids); #[cfg(windows)] { @@ -612,6 +630,7 @@ fn handle_one_frame( frame: &[u8], ms: i64, encoder: &mut Encoder, + recorder: Arc>>, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -623,6 +642,12 @@ fn handle_one_frame( let mut send_conn_ids: HashSet = Default::default(); if let Ok(msg) = encoder.encode_to_message(frame, ms) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + recorder + .lock() + .unwrap() + .as_mut() + .map(|r| r.write_message(&msg)); send_conn_ids = sp.send_video_frame(msg); } Ok(send_conn_ids) diff --git a/src/ui.rs b/src/ui.rs index d1c669848..095559811 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -21,20 +21,20 @@ use hbb_common::{ use crate::common::get_app_name; use crate::ipc; use crate::ui_interface::{ - check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, - forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, - get_icon, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, - get_new_version, get_option, get_options, get_peer, get_peer_option, get_recent_sessions, - get_remote_id, get_size, get_socks, get_software_ext, get_software_store_path, - get_software_update_url, get_uuid, get_version, goto_install, has_hwcodec, - has_rendezvous_service, install_me, install_path, is_can_screen_recording, is_installed, - is_installed_daemon, is_installed_lower_version, is_login_wayland, is_ok_change_id, - is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, - new_remote, open_url, peer_has_password, permanent_password, post_request, - recent_sessions_updated, remove_peer, run_without_install, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, set_socks, - show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, - update_temporary_password, using_public_server, + check_mouse_time, closing, create_shortcut, current_is_wayland, default_video_save_directory, + fix_login_wayland, forget_password, get_api_server, get_async_job_status, get_connect_status, + get_error, get_fav, get_icon, get_lan_peers, get_langs, get_license, get_local_option, + get_mouse_time, get_new_version, get_option, get_options, get_peer, get_peer_option, + get_recent_sessions, get_remote_id, get_size, get_socks, get_software_ext, + get_software_store_path, get_software_update_url, get_uuid, get_version, goto_install, + has_hwcodec, has_rendezvous_service, install_me, install_path, is_can_screen_recording, + is_installed, is_installed_daemon, is_installed_lower_version, is_login_wayland, + is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, + modify_default_login, new_remote, open_url, peer_has_password, permanent_password, + post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, + set_option, set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, + set_socks, show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, + update_me, update_temporary_password, using_public_server, }; mod cm; @@ -579,6 +579,10 @@ impl UI { fn get_langs(&self) -> String { get_langs() } + + fn default_video_save_directory(&self) -> String { + default_video_save_directory() + } } impl sciter::EventHandler for UI { @@ -661,6 +665,7 @@ impl sciter::EventHandler for UI { fn get_uuid(); fn has_hwcodec(); fn get_langs(); + fn default_video_save_directory(); } } diff --git a/src/ui/common.css b/src/ui/common.css index c3f3706ef..1814ad32d 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -70,6 +70,15 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } +button.link { + background: none !important; + border: none; + padding: 0 !important; + color: color(button); + text-decoration: underline; + cursor: pointer; +} + input[type=text], input[type=password], input[type=number] { width: *; font-size: 1.5em; diff --git a/src/ui/header.tis b/src/ui/header.tis index b274b0464..a997ce4b3 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -14,6 +14,8 @@ var svg_secure = var svg_insecure = ; var svg_insecure_relay = ; var svg_secure_relay = ; +var svg_recording_off = ; +var svg_recording_on = ; var cur_window_state = view.windowState; function check_state_change() { @@ -90,6 +92,9 @@ function editOSPassword(login=false) { }); } +var recording = false; +var recording_refresh = false; + class Header: Reactor.Component { this var conn_note = ""; @@ -140,6 +145,7 @@ class Header: Reactor.Component { {svg_action} {svg_display} {svg_keyboard} + {recording ? svg_recording_on : svg_recording_off} {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} @@ -279,6 +285,13 @@ class Header: Reactor.Component { me.popup(menu); } + event click $(span#recording) (_, me) { + handler.record_screen(!recording, display_width, display_height); + recording = !recording; + header.update(); + if (recording) self.timer(100ms, function() { recording_refresh = true; handler.refresh_video(); }); + } + event click $(#screen) (_, me) { if (pi.current_display == me.index) return; handler.switch_display(me.index); diff --git a/src/ui/index.tis b/src/ui/index.tis index d0a9d29a8..dc2f403fc 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -214,6 +214,7 @@ class Enhancements: Reactor.Component { {has_hwcodec ?
  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive Bitrate")} (beta)
  • +
  • {translate("Recording")}
  • ; } @@ -232,6 +233,23 @@ class Enhancements: Reactor.Component { var v = me.id; if (v.indexOf("enable-") == 0) { handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : ''); + } else if (v == 'screen-recording') { + var dir = handler.get_option("video-save-directory"); + if (!dir) dir = handler.default_video_save_directory(); + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Automatically record incoming sessions')}
    +
    +
    {translate("Directory")}:  {dir}
    +
    +
    +
    + , function(res=null) { + if (!res) return; + handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); + handler.set_option("video-save-directory", $(#folderPath).text); + }); } this.toggleMenuState(); } diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index b7df30717..7d1430cb0 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -192,6 +192,14 @@ class MsgboxComponent: Reactor.Component { } } } + + event click $(button#select_directory) { + var folder = view.selectFolder(translate("Change"), $(#folderPath).text); + if (folder) { + if (folder.indexOf("file://") == 0) folder = folder.substring(7); + $(#folderPath).text = folder; + } + } function show_progress(show=1, err="") { if (show == -1) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 97e130d09..b3f443dda 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -394,6 +394,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(); + fn record_screen(bool, i32, i32); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 835136442..393c83384 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -20,6 +20,9 @@ handler.setDisplay = function(x, y, w, h) { display_origin_x = x; display_origin_y = y; adaptDisplay(); + + if (recording && !recording_refresh) handler.record_screen(true, w, h); + recording_refresh = false; } // in case toolbar not shown correclty diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dc3a02c7a..419a89676 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -726,6 +726,11 @@ pub fn get_langs() -> String { crate::lang::LANGS.to_string() } +#[inline] +pub fn default_video_save_directory() -> String { + scrap::record::RecorderContext::default_save_directory() +} + #[inline] pub fn is_xfce() -> bool { crate::platform::is_xfce() diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2feceb8fe..9f7391dac 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -98,6 +98,10 @@ impl Session { self.send(Data::Message(LoginConfigHandler::refresh())); } + pub fn record_screen(&self, start: bool, w: i32, h: i32) { + self.send(Data::RecordScreen(start, w, h, self.id.clone())); + } + pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) { let msg = self .lc