video record

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2022-09-15 17:31:28 +08:00
parent f5b7c34c81
commit 9489877c78
48 changed files with 1186 additions and 398 deletions

125
Cargo.lock generated
View File

@ -58,6 +58,21 @@ dependencies = [
"atomic",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alsa"
version = "0.6.0"
@ -420,6 +435,27 @@ dependencies = [
"once_cell",
]
[[package]]
name = "brotli"
version = "3.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.11.0"
@ -589,8 +625,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits 0.2.15",
"time 0.1.44",
"wasm-bindgen",
"winapi 0.3.9",
]
@ -1359,6 +1398,19 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "embed-resource"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0"
dependencies = [
"cc",
"rustc_version 0.4.0",
"toml",
"vswhom",
"winreg 0.10.1",
]
[[package]]
name = "encoding_rs"
version = "0.8.31"
@ -1548,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
dependencies = [
"memoffset",
"rustc_version",
"rustc_version 0.3.3",
]
[[package]]
@ -1590,7 +1642,7 @@ dependencies = [
"regex",
"rustversion",
"thiserror",
"time",
"time 0.3.9",
]
[[package]]
@ -1895,7 +1947,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -2283,6 +2335,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"chrono",
"confy",
"directories-next",
"dirs-next",
@ -2391,7 +2444,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.1.0"
source = "git+https://github.com/21pages/hwcodec#890204e0703a3d361fc7a45f035fe75c0575bb1d"
source = "git+https://github.com/21pages/hwcodec#097a476a0ee249e28d99573899ed4c9c0c01f884"
dependencies = [
"bindgen",
"cc",
@ -2822,6 +2875,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memalloc"
version = "0.1.0"
@ -2919,7 +2978,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.36.1",
]
@ -4230,6 +4289,15 @@ dependencies = [
"semver 0.11.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.13",
]
[[package]]
name = "rustdesk"
version = "1.2.0"
@ -4305,6 +4373,16 @@ dependencies = [
"wol-rs",
]
[[package]]
name = "rustdesk-portable-packer"
version = "0.1.0"
dependencies = [
"brotli",
"dirs",
"embed-resource",
"md5",
]
[[package]]
name = "rustfft"
version = "6.0.1"
@ -5050,6 +5128,17 @@ dependencies = [
"weezl",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.3.9"
@ -5374,6 +5463,26 @@ dependencies = [
"thiserror",
]
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
@ -5401,6 +5510,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

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';
@ -9,6 +10,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
@ -199,6 +201,7 @@ class _GeneralState extends State<_General> {
abr(),
hwcodec(),
audio(context),
record(context),
_Card(title: 'Language', children: [language()]),
],
).marginOnly(bottom: _kListViewBottomMargin));
@ -290,6 +293,59 @@ class _GeneralState extends State<_General> {
});
}
Widget record(BuildContext context) {
return _futureBuilder(future: () async {
String customDirectory =
await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
String dir;
if (customDirectory.isNotEmpty) {
dir = customDirectory;
} else {
dir = defaultDirectory;
}
final canlaunch = await canLaunchUrl(Uri.file(dir));
return {'dir': dir, 'canlaunch': canlaunch};
}(), hasData: (data) {
Map<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();

View File

@ -166,6 +166,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

@ -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,24 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
);
}
Widget _buildRecording(BuildContext context) {
return Consumer<RecordingModel>(
builder: (context, value, child) => IconButton(
tooltip: value.start
? translate('Stop session recording')
: translate('Start session recording'),
onPressed: () async {
await value.toggle();
},
icon: Icon(
value.start
? Icons.pause_circle_filled
: Icons.videocam_outlined,
color: _MenubarTheme.commonColor,
),
));
}
Widget _buildClose(BuildContext context) {
return IconButton(
tooltip: translate('Close'),

View File

@ -197,6 +197,7 @@ class FfiModel with ChangeNotifier {
_display.height = int.parse(evt['height']);
if (old != _pi.currentDisplay) {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
parent.target?.recordingModel.switchDisplay();
}
// remote is mobile, and orientation changed
@ -972,6 +973,41 @@ class QualityMonitorModel with ChangeNotifier {
}
}
class RecordingModel with ChangeNotifier {
WeakReference<FFI> parent;
RecordingModel(this.parent);
bool _start = false;
get start => _start;
switchDisplay() {
if (!isDesktop || !_start) return;
var id = parent.target?.id;
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayWidth();
if (id == null || width == null || height == null) return;
bind.sessionRecordScreen(
id: id, start: _start, width: width, height: height);
}
Future<void> toggle() async {
if (!isDesktop) return;
var id = parent.target?.id;
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayWidth();
if (id == null || width == null || height == null) return;
await bind.sessionRecordScreen(
id: id, start: !_start, width: width, height: height);
_start = !_start;
notifyListeners();
if (_start) {
Future.delayed(const Duration(milliseconds: 100), () {
bind.sessionRefresh(id: id);
});
}
}
}
/// Mouse button enum.
enum MouseButtons { left, right, wheel }
@ -1013,6 +1049,7 @@ class FFI {
late final AbModel abModel; // global
late final UserModel userModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
FFI() {
imageModel = ImageModel(WeakReference(this));
@ -1025,6 +1062,7 @@ class FFI {
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this));
}
/// Send a mouse tap event(down and up).

View File

@ -140,7 +140,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
charcode:
dependency: transitive
description:
@ -161,7 +161,7 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
code_builder:
dependency: transitive
description:
@ -325,6 +325,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
fixnum:
dependency: transitive
description:
@ -588,7 +595,7 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
menu_base:
dependency: transitive
description:
@ -602,7 +609,7 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
mime:
dependency: transitive
description:
@ -679,7 +686,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
path_provider:
dependency: "direct main"
description:

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

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

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,297 @@
#[cfg(feature = "hwcodec")]
use hbb_common::anyhow::anyhow;
use hbb_common::{
bail, chrono,
config::Config,
directories_next,
message_proto::{message, video_frame, EncodedVideoFrame, Message},
ResultType,
};
#[cfg(feature = "hwcodec")]
use hwcodec::mux::{MuxContext, Muxer};
use std::{
fs::{File, OpenOptions},
io,
time::Instant,
};
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
};
use webm::mux::{self, Segment, Track, VideoTrack, Writer};
const MIN_SECS: u64 = 1;
#[derive(Debug, Clone, PartialEq)]
pub enum RecodeCodecID {
VP9,
H264,
H265,
}
#[derive(Debug, Clone)]
pub struct RecorderContext {
pub id: String,
pub filename: String,
pub width: usize,
pub height: usize,
pub codec_id: RecodeCodecID,
}
impl RecorderContext {
pub fn set_filename(&mut self) -> ResultType<()> {
let mut dir = Config::get_option("video-save-directory");
if !dir.is_empty() {
if !PathBuf::from(&dir).exists() {
std::fs::create_dir_all(&dir)?;
}
} else {
dir = Self::default_save_directory();
if !dir.is_empty() && !PathBuf::from(&dir).exists() {
std::fs::create_dir_all(&dir)?;
}
}
let file = self.id.clone()
+ &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string()
+ if self.codec_id == RecodeCodecID::VP9 {
".webm"
} else {
".mp4"
};
self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string();
Ok(())
}
pub fn default_save_directory() -> String {
if let Some(user) = directories_next::UserDirs::new() {
if let Some(video_dir) = user.video_dir() {
return video_dir.join("RustDesk").to_string_lossy().to_string();
}
}
"".to_owned()
}
}
unsafe impl Send for Recorder {}
unsafe impl Sync for Recorder {}
pub trait RecorderApi {
fn new(ctx: RecorderContext) -> ResultType<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.last().map(|f| self.write_video(f));
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H265s(h265s) => {
if self.ctx.codec_id != RecodeCodecID::H265 {
self.change(RecorderContext {
codec_id: RecodeCodecID::H265,
..self.ctx.clone()
})?;
}
if self.ctx.codec_id == RecodeCodecID::H265 {
h265s.frames.last().map(|f| self.write_video(f));
}
}
_ => bail!("unsupported frame type"),
}
Ok(())
}
}
struct WebmRecorder {
vt: VideoTrack,
webm: Option<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,
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,
start: Instant::now(),
})
}
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool {
let ok = self.muxer.write_video(&frame.data, frame.pts).is_ok();
if ok {
self.written = true;
}
ok
}
}
#[cfg(feature = "hwcodec")]
impl Drop for HwRecorder {
fn drop(&mut self) {
self.muxer.write_tail().ok();
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
std::fs::remove_file(&self.ctx.filename).ok();
}
}
}

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.

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

View File

@ -19,13 +19,13 @@ use crate::ui_interface;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::ui_interface::get_sound_inputs;
use crate::ui_interface::{
change_id, check_mouse_time, check_super_user_permission, discover, forget_password,
get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id,
get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option,
get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec,
has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options,
set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server,
update_temporary_password, using_public_server,
change_id, check_mouse_time, check_super_user_permission, default_video_save_directory,
discover, forget_password, get_api_server, get_app_name, get_async_job_status,
get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option,
get_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid,
get_version, has_hwcodec, has_rendezvous_service, post_request, send_to_cm, set_local_option,
set_option, set_options, set_peer_option, set_permanent_password, set_socks, store_fav,
test_if_valid_server, update_temporary_password, using_public_server,
};
use crate::{
client::file_trait::FileManager,
@ -162,6 +162,12 @@ pub fn session_refresh(id: String) {
}
}
pub fn session_record_screen(id: String, start: bool, width: usize, height: usize) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.record_screen(start, width as _, height as _);
}
}
pub fn session_reconnect(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.reconnect();
@ -705,6 +711,10 @@ pub fn main_change_language(lang: String) {
send_to_cm(&crate::ipc::Data::Language(lang));
}
pub fn main_default_video_save_directory() -> String {
default_video_save_directory()
}
pub fn session_add_port_forward(
id: String,
local_port: i32,

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", "允许RDP访问"),
("Pin menubar", "固定菜单栏"),
("Unpin menubar", "取消固定菜单栏"),
("Recording", "录屏"),
("Directory", "目录"),
("Automatically record incoming sessions", "自动录制来访会话"),
("Change", "更改"),
("Start session recording", "开始录屏"),
("Stop session recording", "结束录屏"),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Připnout panel nabídek"),
("Unpin menubar", "Odepnout panel nabídek"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Fastgør menulinjen"),
("Unpin menubar", "Frigør menulinjen"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Pin-Menüleiste"),
("Unpin menubar", "Menüleiste lösen"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Alpingla menubreto"),
("Unpin menubar", "Malfiksi menubreton"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Pin barra de menú"),
("Unpin menubar", "Desbloquear barra de menú"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Épingler la barre de menus"),
("Unpin menubar", "Détacher la barre de menu"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Menüsor rögzítése"),
("Unpin menubar", "Menüsor rögzítésének feloldása"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Pin menubar"),
("Unpin menubar", "Unpin menubar"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -345,5 +345,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Blocca la barra dei menu"),
("Unpin menubar", "Sblocca la barra dei menu"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -343,5 +343,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "メニューバーを固定する"),
("Unpin menubar", "メニューバーのピン留めを外す"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "핀 메뉴 바"),
("Unpin menubar", "메뉴 모음 고정 해제"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -321,5 +321,11 @@ lazy_static::lazy_static! {
("Scale adaptive", "Scale adaptive"),
("Pin menubar", "Мәзір жолағын бекіту"),
("Unpin menubar", "Мәзір жолағын босату"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -344,5 +344,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Przypnij pasek menu"),
("Unpin menubar", "Odepnij pasek menu"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Fixar barra de menu"),
("Unpin menubar", "Desenganxa la barra de menús"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", ""),
("Unpin menubar", ""),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Закрепить строку меню"),
("Unpin menubar", "Открепить строку меню"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Pripnúť panel s ponukami"),
("Unpin menubar", "Uvoľniť panel s ponukami"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", ""),
("Unpin menubar", ""),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Menü çubuğunu sabitle"),
("Unpin menubar", "Menü çubuğunun sabitlemesini kaldır"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", "允許RDP訪問"),
("Pin menubar", "固定菜單欄"),
("Unpin menubar", "取消固定菜單欄"),
("Recording", "錄屏"),
("Directory", "目錄"),
("Automatically record incoming sessions", "自動錄製來訪會話"),
("Change", "變更"),
("Start session recording", "開始錄屏"),
("Stop session recording", "結束錄屏"),
].iter().cloned().collect();
}

View File

@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable RDP", ""),
("Pin menubar", "Ghim thanh menu"),
("Unpin menubar", "Bỏ ghim thanh menu"),
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),
].iter().cloned().collect();
}

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

@ -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,9 @@ function editOSPassword(login=false) {
});
}
var recording = false;
var recording_refresh = false;
class Header: Reactor.Component {
this var conn_note = "";
@ -140,6 +145,7 @@ class Header: Reactor.Component {
<span #action>{svg_action}</span>
<span #display>{svg_display}</span>
<span #keyboard>{svg_keyboard}</span>
<span #recording>{recording ? svg_recording_on : svg_recording_off}</span>
{this.renderKeyboardPop()}
{this.renderDisplayPop()}
{this.renderActionPop()}
@ -279,6 +285,13 @@ class Header: Reactor.Component {
me.popup(menu);
}
event click $(span#recording) (_, me) {
handler.record_screen(!recording, display_width, display_height);
recording = !recording;
header.update();
if (recording) self.timer(100ms, function() { recording_refresh = true; handler.refresh_video(); });
}
event click $(#screen) (_, me) {
if (pi.current_display == me.index) return;
handler.switch_display(me.index);

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,23 @@ class Enhancements: Reactor.Component {
var v = me.id;
if (v.indexOf("enable-") == 0) {
handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : '');
} else if (v == 'screen-recording') {
var dir = handler.get_option("video-save-directory");
if (!dir) dir = handler.default_video_save_directory();
var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {};
msgbox("custom-recording", translate('Recording'),
<div .form>
<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("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : '');
handler.set_option("video-save-directory", $(#folderPath).text);
});
}
this.toggleMenuState();
}

View File

@ -193,6 +193,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) {
this.close()

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

@ -20,6 +20,9 @@ handler.setDisplay = function(x, y, w, h) {
display_origin_x = x;
display_origin_y = y;
adaptDisplay();
if (recording && !recording_refresh) handler.record_screen(true, w, h);
recording_refresh = false;
}
// in case toolbar not shown correclty

View File

@ -726,6 +726,11 @@ pub fn get_langs() -> String {
crate::lang::LANGS.to_string()
}
#[inline]
pub fn default_video_save_directory() -> String {
scrap::record::RecorderContext::default_save_directory()
}
#[inline]
pub fn is_xfce() -> bool {
crate::platform::is_xfce()

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