Merge pull request #1577 from 21pages/record

video record
This commit is contained in:
RustDesk 2022-09-27 15:32:33 +08:00 committed by GitHub
commit dac851ace9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 883 additions and 86 deletions

27
Cargo.lock generated
View File

@ -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"

View File

@ -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,

View File

@ -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<String, dynamic> map = data as Map<String, dynamic>;
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),

View File

@ -116,6 +116,7 @@ class _RemotePageState extends State<RemotePage>
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<RemotePage>
ChangeNotifierProvider.value(value: _ffi.imageModel),
ChangeNotifierProvider.value(value: _ffi.cursorModel),
ChangeNotifierProvider.value(value: _ffi.canvasModel),
ChangeNotifierProvider.value(value: _ffi.recordingModel),
], child: buildBody(context)));
}

View File

@ -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),
],
)),
],

View File

@ -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<RemoteMenubar> {
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<RemoteMenubar> {
);
}
Widget _buildRecording(BuildContext context) {
return Consumer<FfiModel>(builder: ((context, value, child) {
if (value.permissions['recording'] != false) {
return Consumer<RecordingModel>(
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'),

View File

@ -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<FFI> 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<String, dynamic> 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);

View File

@ -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<String, dynamic> toJson() {

View File

@ -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:

View File

@ -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.
#

View File

@ -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"

View File

@ -436,6 +436,7 @@ message PermissionInfo {
Audio = 3;
File = 4;
Restart = 5;
Recording = 6;
}
Permission permission = 1;

View File

@ -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;

View File

@ -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"

View File

@ -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()
});
}

View File

@ -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]

View File

@ -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<Self>
where
Self: Sized;
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
}
pub struct Recorder {
pub inner: Box<dyn RecorderApi>,
ctx: RecorderContext,
}
impl Deref for Recorder {
type Target = Box<dyn RecorderApi>;
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<Self> {
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<Segment<Writer<File>>>,
ctx: RecorderContext,
key: bool,
written: bool,
start: Instant,
}
impl RecorderApi for WebmRecorder {
fn new(ctx: RecorderContext) -> ResultType<Self> {
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<Self> {
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();
}
}
}

View File

@ -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<Mutex<LatencyController>>,
pub rgb: Vec<u8>,
recorder: Arc<Mutex<Option<Recorder>>>,
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<bool> {
// 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<MediaData>;
@ -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);
}
}

View File

@ -601,6 +601,11 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
}
Data::RecordScreen(start, w, h, id) => {
let _ = self
.video_sender
.send(MediaData::RecordScreen(start, w, h, id));
}
_ => {}
}
true
@ -794,13 +799,8 @@ impl<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
Permission::Restart => {
self.handler.set_permission("restart", p.enabled);
}
Permission::Recording => {
self.handler.set_permission("recording", p.enabled);
}
}
}
Some(misc::Union::SwitchDisplay(s)) => {

View File

@ -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<FileEntry>, only_count: bool) -> S
}
m.insert("total_size".into(), json!(n as f64));
serde_json::to_string(&m).unwrap_or("".into())
}
}

View File

@ -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,

View File

@ -146,6 +146,7 @@ pub enum Data {
file: bool,
file_transfer_enabled: bool,
restart: bool,
recording: bool,
},
ChatMessage {
text: String,

View File

@ -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", "输入聊天消息"),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", "Есепкі"),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", ""),

View File

@ -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", "輸入聊天消息"),

View File

@ -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", ""),

View File

@ -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,
});
}

View File

@ -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<Mutex<Option<Recorder>>> = 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<Mutex<Option<Recorder>>>,
) -> ResultType<HashSet<i32>> {
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<i32> = 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)

View File

@ -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();
}
}

View File

@ -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;

View File

@ -32,7 +32,8 @@ impl InvokeUiCM for SciterHandler {
client.clipboard,
client.audio,
client.file,
client.restart
client.restart,
client.recording
),
);
}

View File

@ -41,13 +41,16 @@ class Body: Reactor.Component
</div>
<div />
{c.is_file_transfer || c.port_forward ? "" : <div>{translate('Permissions')}</div>}
{c.is_file_transfer || c.port_forward ? "" : <div .permissions>
{c.is_file_transfer || c.port_forward ? "" : <div> <div .permissions>
<div class={!c.keyboard ? "disabled" : ""} title={translate('Allow using keyboard and mouse')}><icon .keyboard /></div>
<div class={!c.clipboard ? "disabled" : ""} title={translate('Allow using clipboard')}><icon .clipboard /></div>
<div class={!c.audio ? "disabled" : ""} title={translate('Allow hearing sound')}><icon .audio /></div>
<div class={!c.file ? "disabled" : ""} title={translate('Allow file copy and paste')}><icon .file /></div>
<div class={!c.restart ? "disabled" : ""} title={translate('Allow remote restart')}><icon .restart /></div>
</div>}
</div> <div .permissions style="margin-top:8px;" >
<div class={!c.recording ? "disabled" : ""} title={translate('Allow recording session')}><icon .recording /></div>
</div></div>
}
{c.port_forward ? <div>Port Forwarding: {c.port_forward}</div> : ""}
<div style="size:*"/>
<div .buttons>
@ -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();

View File

@ -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;

View File

@ -14,6 +14,8 @@ var svg_secure = <svg viewBox="0 0 347.97 347.97">
var svg_insecure = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M238.802 115.023l-111.573 114.68-8.6-8.367L230.2 106.656z"/><path d="M125.559 108.093l114.68 111.572-8.368 8.601-114.68-111.572z"/></g></svg>;
var svg_insecure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z"/></g></svg>;
var svg_secure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="#3f7d46" stroke="#3f7d46" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z" fill="#fff"/></g></svg>;
var svg_recording_off = <svg t="1663505560063" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5393" width="32" height="32"><path d="M1002.666667 260.266667c-12.8-8.533333-29.866667-4.266667-42.666667 4.266666L725.333333 430.933333V298.666667c0-72.533333-55.466667-128-128-128H128C55.466667 170.666667 0 226.133333 0 298.666667v426.666666c0 72.533333 55.466667 128 128 128h469.333333c72.533333 0 128-55.466667 128-128v-132.266666l230.4 166.4c17.066667 12.8 46.933333 8.533333 59.733334-8.533334 4.266667-8.533333 8.533333-17.066667 8.533333-25.6V298.666667c0-17.066667-8.533333-29.866667-21.333333-38.4zM640 725.333333c0 25.6-17.066667 42.666667-42.666667 42.666667H128c-25.6 0-42.666667-17.066667-42.666667-42.666667V298.666667c0-25.6 17.066667-42.666667 42.666667-42.666667h469.333333c25.6 0 42.666667 17.066667 42.666667 42.666667v426.666666z m298.666667-81.066666L755.2 512 938.666667 379.733333v264.533334z" p-id="5394" fill="#8a8a8a"></path></svg>;
var svg_recording_on = <svg t="1663505598640" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5644" width="32" height="32"><path d="M1002.666667 260.266667c-12.8-8.533333-29.866667-4.266667-42.666667 4.266666L725.333333 430.933333V298.666667c0-72.533333-55.466667-128-128-128H128C55.466667 170.666667 0 226.133333 0 298.666667v426.666666c0 72.533333 55.466667 128 128 128h469.333333c72.533333 0 128-55.466667 128-128v-132.266666l230.4 166.4c17.066667 12.8 46.933333 8.533333 59.733334-8.533334 4.266667-8.533333 8.533333-17.066667 8.533333-25.6V298.666667c0-17.066667-8.533333-29.866667-21.333333-38.4z" p-id="5645" fill="#2C8CFF"></path></svg>;
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 {
<span #action>{svg_action}</span>
<span #display>{svg_display}</span>
<span #keyboard>{svg_keyboard}</span>
{recording_enabled ? <span #recording>{recording ? svg_recording_on : svg_recording_off}</span> : ""}
{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);

View File

@ -214,6 +214,7 @@ class Enhancements: Reactor.Component {
<menu #enhancements-menu>
{has_hwcodec ? <li #enable-hwcodec><span>{svg_checkmark}</span>{translate("Hardware Codec")} (beta)</li> : ""}
<li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive Bitrate")} (beta)</li>
<li #screen-recording>{translate("Recording")}</li>
</menu>
</li>;
}
@ -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'),
<div .form>
<div><button|checkbox(enable_record_session) {ts0}>{translate('Enable Recording Session')}</button></div>
<div><button|checkbox(auto_record_incoming) {ts1}>{translate('Automatically record incoming sessions')}</button></div>
<div>
<div style="word-wrap:break-word"><span>{translate("Directory")}:&nbsp;&nbsp;</span><span #folderPath>{dir}</span></div>
<div> <button #select_directory .link>{translate('Change')}</button> </div>
</div>
</div>
, 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();
}

View File

@ -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) {

View File

@ -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);

View File

@ -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();
});

View File

@ -40,6 +40,7 @@ pub struct Client {
pub audio: bool,
pub file: bool,
pub restart: bool,
pub recording: bool,
#[serde(skip)]
tx: UnboundedSender<Data>,
}
@ -94,6 +95,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
audio: bool,
file: bool,
restart: bool,
recording: bool,
tx: mpsc::UnboundedSender<Data>,
) {
let client = Client {
@ -108,6 +110,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
audio,
file,
restart,
recording,
tx,
};
self.ui_handler.add_connection(&client);
@ -250,11 +253,11 @@ pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
}
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<T: InvokeUiCM>(
audio,
file,
restart,
recording,
..
}) => {
current_id = id;
@ -364,6 +368,7 @@ pub async fn start_listen<T: InvokeUiCM>(
audio,
file,
restart,
recording,
tx.clone(),
);
}

View File

@ -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()

View File

@ -98,6 +98,10 @@ impl<T: InvokeUiSession> Session<T> {
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