diff --git a/Cargo.lock b/Cargo.lock index 9a129977c..60852eba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,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", ] @@ -1639,7 +1642,7 @@ dependencies = [ "regex", "rustversion", "thiserror", - "time", + "time 0.3.9", ] [[package]] @@ -1944,7 +1947,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -2332,6 +2335,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "chrono", "confy", "directories-next", "dirs-next", @@ -2974,7 +2978,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.36.1", ] @@ -5124,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" @@ -5495,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/common.dart b/flutter/lib/common.dart index aee5a73af..a708a0a15 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -50,6 +50,8 @@ late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); +late final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); enum DesktopType { main, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 1f11a6fc5..1c28fdd98 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'; @@ -11,6 +12,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'; @@ -233,6 +235,7 @@ class _GeneralState extends State<_General> { abr(), hwcodec(), audio(context), + record(context), _Card(title: 'Language', children: [language()]), ], ).marginOnly(bottom: _kListViewBottomMargin)); @@ -324,6 +327,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(); @@ -414,6 +470,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled), _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', enabled: enabled), + _OptionCheckBox( + context, 'Enable Recording Session', 'enable-record-session', + enabled: enabled), _OptionCheckBox(context, 'Enable remote configuration modification', 'allow-remote-config-modification', enabled: enabled), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 74ff9b7af..f7366b960 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -116,6 +116,7 @@ class _RemotePageState extends State void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); _ffi.close(); _timer?.cancel(); @@ -164,6 +165,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/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d6f3d7f16..b17fe70f4 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -412,6 +412,13 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { client.restart = enabled; }); }, null), + buildPermissionIcon(client.recording, iconRecording, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "recording", enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, null), ], )), ], diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 67e69a1d9..d524ef279 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,28 @@ class _RemoteMenubarState extends State { ); } + Widget _buildRecording(BuildContext context) { + return Consumer(builder: ((context, value, child) { + if (value.permissions['recording'] != false) { + return Consumer( + builder: (context, value, child) => IconButton( + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () => value.toggle(), + icon: Icon( + value.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: _MenubarTheme.commonColor, + ), + )); + } else { + return Offstage(); + } + })); + } + 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..c90b07daf 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,7 @@ class FfiModel with ChangeNotifier { if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); } + parent.target?.recordingModel.onSwitchDisplay(); notifyListeners(); } @@ -972,6 +973,43 @@ class QualityMonitorModel with ChangeNotifier { } } +class RecordingModel with ChangeNotifier { + WeakReference parent; + RecordingModel(this.parent); + bool _start = false; + get start => _start; + + onSwitchDisplay() { + 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: true, width: width, height: height); + } + + toggle() { + if (!isDesktop) return; + var id = parent.target?.id; + if (id == null) return; + _start = !_start; + notifyListeners(); + if (_start) { + bind.sessionRefresh(id: id); + } else { + bind.sessionRecordScreen(id: id, start: false, width: 0, height: 0); + } + } + + onClose() { + if (!isDesktop) return; + var id = parent.target?.id; + if (id == null) return; + _start = false; + bind.sessionRecordScreen(id: id, start: false, width: 0, height: 0); + } +} + /// Mouse button enum. enum MouseButtons { left, right, wheel } @@ -1013,6 +1051,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 +1064,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). @@ -1136,7 +1176,7 @@ class FFI { Map event = json.decode(message.field0); await cb(event); } catch (e) { - debugPrint('json.decode fail(): $e'); + debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is Rgba) { imageModel.onRgba(message.field0, tabBarHeight); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 5e3fb755e..0e70e40ea 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -544,6 +544,7 @@ class Client { bool audio = false; bool file = false; bool restart = false; + bool recording = false; Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); @@ -559,6 +560,7 @@ class Client { audio = json['audio']; file = json['file']; restart = json['restart']; + recording = json['recording']; } Map toJson() { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index db5b4b535..3975e1e8d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -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: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 019bf7f33..ba0578417 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/protos/message.proto b/libs/hbb_common/protos/message.proto index e711f5826..8fb67e5c1 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -436,6 +436,7 @@ message PermissionInfo { Audio = 3; File = 4; Restart = 5; + Recording = 6; } Permission permission = 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..166f7516c 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; @@ -94,6 +94,7 @@ impl EncoderApi for HwEncoder { frames.push(EncodedVideoFrame { data: Bytes::from(frame.data), pts: frame.pts as _, + key:frame.key == 1, ..Default::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..e8bbacf02 --- /dev/null +++ b/libs/scrap/src/common/record.rs @@ -0,0 +1,306 @@ +#[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.iter().map(|f| self.write_video(f)).count(); + } + } + #[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.iter().map(|f| self.write_video(f)).count(); + } + } + _ => 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, + key: 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, + key: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + if frame.key { + self.key = true; + } + if self.key { + let ok = self.muxer.write_video(&frame.data, frame.key).is_ok(); + if ok { + self.written = true; + } + ok + } else { + false + } + } +} + +#[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..cf6168834 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); @@ -958,6 +958,9 @@ impl Remote { Permission::Restart => { self.handler.set_permission("restart", p.enabled); } + Permission::Recording => { + self.handler.set_permission("recording", p.enabled); + } } } Some(misc::Union::SwitchDisplay(s)) => { diff --git a/src/flutter.rs b/src/flutter.rs index fea412c23..755e245fe 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -257,7 +257,7 @@ impl InvokeUiSession for FlutterHandler { self.push_event( "switch_display", vec![ - ("display", &display.to_string()), + ("display", &display.display.to_string()), ("x", &display.x.to_string()), ("y", &display.y.to_string()), ("width", &display.width.to_string()), @@ -485,4 +485,4 @@ pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> S } m.insert("total_size".into(), json!(n as f64)); serde_json::to_string(&m).unwrap_or("".into()) -} \ No newline at end of file +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2957a62d0..1adee6988 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -15,19 +15,7 @@ use hbb_common::{message_proto::Hash, ResultType}; use crate::flutter::{self, SESSIONS}; use crate::start_server; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_interface::get_sound_inputs; -use crate::ui_interface::{ - self, 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_new_version, - get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, - goto_install, has_hwcodec, has_rendezvous_service, is_can_screen_recording, is_installed, - is_installed_daemon, is_installed_lower_version, is_process_trusted, is_rdp_service_open, - is_share_rdp, peer_has_password, 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_me, update_temporary_password, using_public_server, -}; +use crate::ui_interface::{self, *}; use crate::{ client::file_trait::FileManager, flutter::{make_fd_to_json, session_add, session_start_}, @@ -163,6 +151,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(); @@ -719,6 +713,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/ipc.rs b/src/ipc.rs index 36f6b9c1f..709384bb6 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -146,6 +146,7 @@ pub enum Data { file: bool, file_transfer_enabled: bool, restart: bool, + recording: bool, }, ChatMessage { text: String, diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9b1b06396..c974bf7fe 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -349,6 +349,14 @@ 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", "结束录屏"), + ("Enable Recording Session", "允许录制会话"), + ("Allow recording session", "允许录制会话"), ("Enable LAN Discovery", "允许局域网发现"), ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index b9d0fc16b..def7206a7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e864ecbc..8151ea4e0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 851e8e565..57596f709 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ad5e6c536..cc21e7509 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index af696f6b7..1e57c6913 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 935f5dab0..479c701bf 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index bf1c9c2fa..1847c8cc5 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index ed4e02e70..ebb83c862 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index d9d5ba2fd..2db9f3026 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index bfbc87601..0021f0db9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0d5f0771c..89bc69fa2 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 6d08d398b..6eea0d8be 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -290,9 +290,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), ("Connection not allowed", "Қосылу рұқсат етілмеген"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), ("Use temporary password", "Уақытша құпия сөзді қолдану"), ("Use permanent password", "Тұрақты құпия сөзді қолдану"), ("Use both passwords", "Қос құпия сөзді қолдану"), @@ -322,6 +319,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("General", ""), ("Security", ""), ("Account", "Есепкі"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 0bf270bae..1f101a069 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6ce9369a4..ab49d9f93 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d149a2ac1..640a35d10 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ef1f21f73..f1d9c6f31 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 16f543335..a7b527302 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index d54ce431f..e879b2a21 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index c1ba55c72..50ce2ec2a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 684b07e79..c02c95df1 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -349,6 +349,14 @@ 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", "結束錄屏"), + ("Enable Recording Session", "允許錄製會話"), + ("Allow recording session", "允許錄製會話"), ("Enable LAN Discovery", "允許局域網發現"), ("Deny LAN Discovery", "拒絕局域網發現"), ("Write a message", "輸入聊天消息"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 46beea4fb..dce16bb7b 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -349,6 +349,14 @@ 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", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ("Enable LAN Discovery", ""), ("Deny LAN Discovery", ""), ("Write a message", ""), diff --git a/src/server/connection.rs b/src/server/connection.rs index 161c058f8..15d313fbe 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -81,6 +81,7 @@ pub struct Connection { audio: bool, file: bool, restart: bool, + recording: bool, last_test_delay: i64, lock_after_session_end: bool, show_remote_cursor: bool, // by peer @@ -169,6 +170,7 @@ impl Connection { audio: Config::get_option("enable-audio").is_empty(), file: Config::get_option("enable-file-transfer").is_empty(), restart: Config::get_option("enable-remote-restart").is_empty(), + recording: Config::get_option("enable-record-session").is_empty(), last_test_delay: 0, lock_after_session_end: false, show_remote_cursor: false, @@ -210,6 +212,9 @@ impl Connection { if !conn.restart { conn.send_permission(Permission::Restart, false).await; } + if !conn.recording { + conn.send_permission(Permission::Recording, false).await; + } let mut test_delay_timer = time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT); let mut last_recv_time = Instant::now(); @@ -290,6 +295,9 @@ impl Connection { } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; + } else if &name == "recording" { + conn.recording = enabled; + conn.send_permission(Permission::Recording, enabled).await; } } ipc::Data::RawMessage(bytes) => { @@ -777,6 +785,7 @@ impl Connection { file: self.file, file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, + recording: self.recording, }); } 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/cm.css b/src/ui/cm.css index 0832c6251..fbbd58961 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -108,6 +108,10 @@ icon.restart { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'); } +icon.recording { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'); +} + div.buttons { width: *; border-spacing: 0.5em; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 959141da6..e0fea8bf3 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -32,7 +32,8 @@ impl InvokeUiCM for SciterHandler { client.clipboard, client.audio, client.file, - client.restart + client.restart, + client.recording ), ); } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 4708acea5..c6664b50b 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -41,13 +41,16 @@ class Body: Reactor.Component
{c.is_file_transfer || c.port_forward ? "" :
{translate('Permissions')}
} - {c.is_file_transfer || c.port_forward ? "" :
+ {c.is_file_transfer || c.port_forward ? "" :
-
} +
+
+
+ } {c.port_forward ?
Port Forwarding: {c.port_forward}
: ""}
@@ -118,6 +121,15 @@ class Body: Reactor.Component }); } + event click $(icon.recording) { + var { cid, connection } = this; + checkClickTime(function() { + connection.recording = !connection.recording; + body.update(); + handler.switch_permission(cid, "recording", connection.recording); + }); + } + event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -276,7 +288,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart) { +handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -293,7 +305,7 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na port_forward: port_forward, name: name, authorized: authorized, time: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, - audio: audio, file: file, restart: restart + audio: audio, file: file, restart: restart, recording: recording }); body.cur = connections.length - 1; bring_to_top(); 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..8f9fa8a32 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,8 @@ function editOSPassword(login=false) { }); } +var recording = false; + class Header: Reactor.Component { this var conn_note = ""; @@ -140,6 +144,7 @@ class Header: Reactor.Component { {svg_action} {svg_display} {svg_keyboard} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} @@ -279,6 +284,15 @@ class Header: Reactor.Component { me.popup(menu); } + event click $(span#recording) (_, me) { + recording = !recording; + header.update(); + if (recording) + handler.refresh_video(); + else + handler.record_screen(false, display_width, display_height); + } + 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 056225e00..b889ff010 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,26 @@ 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 ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Enable Recording Session')}
    +
    {translate('Automatically record incoming sessions')}
    +
    +
    {translate("Directory")}:  {dir}
    +
    +
    +
    + , function(res=null) { + if (!res) return; + handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); + 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..02f0de270 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -12,6 +12,7 @@ var clipboard_enabled = true; // server side var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side +var recording_enabled = true; // server side var scroll_body = $(body); handler.setDisplay = function(x, y, w, h) { @@ -20,6 +21,7 @@ handler.setDisplay = function(x, y, w, h) { display_origin_x = x; display_origin_y = y; adaptDisplay(); + if (recording) handler.record_screen(true, w, h); } // in case toolbar not shown correclty @@ -467,6 +469,7 @@ function self.closing() { var (x, y, w, h) = view.box(#rectw, #border, #screen); if (is_file_transfer) save_file_transfer_close_state(); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); + if (recording) handler.record_screen(false, display_width, display_height); } var qualityMonitor; @@ -519,6 +522,7 @@ handler.setPermission = function(name, enabled) { if (name == "file") file_enabled = enabled; if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; + if (name == "recording") recording_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index e4dbf80fb..3813760a0 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -40,6 +40,7 @@ pub struct Client { pub audio: bool, pub file: bool, pub restart: bool, + pub recording: bool, #[serde(skip)] tx: UnboundedSender, } @@ -94,6 +95,7 @@ impl ConnectionManager { audio: bool, file: bool, restart: bool, + recording: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -108,6 +110,7 @@ impl ConnectionManager { audio, file, restart, + recording, tx, }; self.ui_handler.add_connection(&client); @@ -250,11 +253,11 @@ pub async fn start_ipc(cm: ConnectionManager) { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart, recording} => { log::debug!("conn_id: {}", id); conn_id = id; tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, tx.clone()); } Data::Close => { tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); @@ -349,6 +352,7 @@ pub async fn start_listen( audio, file, restart, + recording, .. }) => { current_id = id; @@ -364,6 +368,7 @@ pub async fn start_listen( audio, file, restart, + recording, tx.clone(), ); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3d076255f..31c203fb4 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -729,6 +729,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