auto record outgoing (#9711)

* Add option auto record outgoing session
* In the same connection, all displays and all windows share the same
  recording state.

todo:

Android check external storage permission

Known issue:

* Sciter old issue, stop the process directly without stop record, the record file can't play.

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages 2024-10-21 14:34:06 +08:00 committed by GitHub
parent 289076aa70
commit e8187588c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 442 additions and 322 deletions

2
Cargo.lock generated
View File

@ -3051,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.7.0"
source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa"
dependencies = [
"bindgen 0.59.2",
"cc",

View File

@ -89,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
const String kOptionEnableHwcodec = "enable-hwcodec";
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";

View File

@ -575,12 +575,17 @@ class _GeneralState extends State<_General> {
bool root_dir_exists = map['root_dir_exists']!;
bool user_dir_exists = map['user_dir_exists']!;
return _Card(title: 'Recording', children: [
_OptionCheckBox(context, 'Automatically record incoming sessions',
kOptionAllowAutoRecordIncoming),
if (showRootDir)
if (!bind.isOutgoingOnly())
_OptionCheckBox(context, 'Automatically record incoming sessions',
kOptionAllowAutoRecordIncoming),
if (!bind.isIncomingOnly())
_OptionCheckBox(context, 'Automatically record outgoing sessions',
kOptionAllowAutoRecordOutgoing),
if (showRootDir && !bind.isOutgoingOnly())
Row(
children: [
Text('${translate("Incoming")}:'),
Text(
'${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
Expanded(
child: GestureDetector(
onTap: root_dir_exists
@ -597,45 +602,49 @@ class _GeneralState extends State<_General> {
),
],
).marginOnly(left: _kContentHMargin),
Row(
children: [
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
Expanded(
child: GestureDetector(
onTap: user_dir_exists
? () => launchUrl(Uri.file(user_dir))
: null,
child: Text(
user_dir,
softWrap: true,
style: user_dir_exists
? const TextStyle(decoration: TextDecoration.underline)
if (!(showRootDir && bind.isIncomingOnly()))
Row(
children: [
Text(
'${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
Expanded(
child: GestureDetector(
onTap: user_dir_exists
? () => launchUrl(Uri.file(user_dir))
: null,
)).marginOnly(left: 10),
),
ElevatedButton(
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
? null
: () async {
String? initialDirectory;
if (await Directory.fromUri(Uri.directory(user_dir))
.exists()) {
initialDirectory = user_dir;
}
String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(
initialDirectory: initialDirectory);
if (selectedDirectory != null) {
await bind.mainSetOption(
key: kOptionVideoSaveDirectory,
value: selectedDirectory);
setState(() {});
}
},
child: Text(translate('Change')))
.marginOnly(left: 5),
],
).marginOnly(left: _kContentHMargin),
child: Text(
user_dir,
softWrap: true,
style: user_dir_exists
? const TextStyle(
decoration: TextDecoration.underline)
: null,
)).marginOnly(left: 10),
),
ElevatedButton(
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
? null
: () async {
String? initialDirectory;
if (await Directory.fromUri(
Uri.directory(user_dir))
.exists()) {
initialDirectory = user_dir;
}
String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(
initialDirectory: initialDirectory);
if (selectedDirectory != null) {
await bind.mainSetOption(
key: kOptionVideoSaveDirectory,
value: selectedDirectory);
setState(() {});
}
},
child: Text(translate('Change')))
.marginOnly(left: 5),
],
).marginOnly(left: _kContentHMargin),
]);
});
}

View File

@ -115,6 +115,8 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.start(
widget.id,
@ -253,7 +255,6 @@ class _RemotePageState extends State<RemotePage>
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();

View File

@ -1924,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
var ffi = Provider.of<FfiModel>(context);
var recordingModel = Provider.of<RecordingModel>(context);
final visible =
(recordingModel.start || ffi.permissions['recording'] != false) &&
ffi.pi.currentDisplay != kAllDisplayValue;
(recordingModel.start || ffi.permissions['recording'] != false);
if (!visible) return Offstage();
return _IconMenuButton(
assetName: 'assets/rec.svg',

View File

@ -92,6 +92,13 @@ class _RemotePageState extends State<RemotePage> {
gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI);
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
gFFI.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
if (gFFI.recordingModel.start) {
showToast(translate('Automatically record outgoing sessions'));
}
});
}
@override
@ -207,7 +214,7 @@ class _RemotePageState extends State<RemotePage> {
}
void _handleNonIOSSoftKeyboardInput(String newValue) {
_composingTimer?.cancel();
_composingTimer?.cancel();
if (_textController.value.isComposingRangeValid) {
_composingTimer = Timer(Duration(milliseconds: 25), () {
_handleNonIOSSoftKeyboardInput(_textController.value.text);

View File

@ -79,6 +79,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _enableRecordSession = false;
var _enableHardwareCodec = false;
var _autoRecordIncomingSession = false;
var _autoRecordOutgoingSession = false;
var _allowAutoDisconnect = false;
var _localIP = "";
var _directAccessPort = "";
@ -104,6 +105,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
bind.mainGetOptionSync(key: kOptionAllowAutoRecordOutgoing));
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
@ -231,6 +234,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final outgoingOnly = bind.isOutgoingOnly();
final incommingOnly = bind.isIncomingOnly();
final customClientSection = CustomSettingsSection(
child: Column(
children: [
@ -674,32 +678,55 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
},
),
]),
if (isAndroid && !outgoingOnly)
if (isAndroid)
SettingsSection(
title: Text(translate("Recording")),
tiles: [
SettingsTile.switchTile(
title:
Text(translate('Automatically record incoming sessions')),
leading: Icon(Icons.videocam),
description: Text(
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
initialValue: _autoRecordIncomingSession,
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionAllowAutoRecordIncoming,
value:
bool2option(kOptionAllowAutoRecordIncoming, v));
final newValue = option2bool(
kOptionAllowAutoRecordIncoming,
await bind.mainGetOption(
key: kOptionAllowAutoRecordIncoming));
setState(() {
_autoRecordIncomingSession = newValue;
});
},
if (!outgoingOnly)
SettingsTile.switchTile(
title:
Text(translate('Automatically record incoming sessions')),
initialValue: _autoRecordIncomingSession,
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionAllowAutoRecordIncoming,
value: bool2option(
kOptionAllowAutoRecordIncoming, v));
final newValue = option2bool(
kOptionAllowAutoRecordIncoming,
await bind.mainGetOption(
key: kOptionAllowAutoRecordIncoming));
setState(() {
_autoRecordIncomingSession = newValue;
});
},
),
if (!incommingOnly)
SettingsTile.switchTile(
title:
Text(translate('Automatically record outgoing sessions')),
initialValue: _autoRecordOutgoingSession,
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
? null
: (v) async {
await bind.mainSetOption(
key: kOptionAllowAutoRecordOutgoing,
value: bool2option(
kOptionAllowAutoRecordOutgoing, v));
final newValue = option2bool(
kOptionAllowAutoRecordOutgoing,
await bind.mainGetOption(
key: kOptionAllowAutoRecordOutgoing));
setState(() {
_autoRecordOutgoingSession = newValue;
});
},
),
SettingsTile(
title: Text(translate("Directory")),
description: Text(bind.mainVideoSaveDirectory(root: false)),
),
],
),

View File

@ -397,6 +397,10 @@ class FfiModel with ChangeNotifier {
if (isWeb) {
parent.target?.fileModel.onSelectedFiles(evt);
}
} else if (name == "record_status") {
if (desktopType == DesktopType.remote || isMobile) {
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
}
} else {
debugPrint('Event is not handled in the fixed branch: $name');
}
@ -527,7 +531,6 @@ class FfiModel with ChangeNotifier {
}
}
parent.target?.recordingModel.onSwitchDisplay();
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
handleResolutions(peerId, evt['resolutions']);
}
@ -1135,8 +1138,6 @@ class FfiModel with ChangeNotifier {
// Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId,
{bool updateCursorPos = false}) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose();
// no need to wait for the response
pi.currentDisplay = display;
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
@ -2342,25 +2343,7 @@ class RecordingModel with ChangeNotifier {
WeakReference<FFI> parent;
RecordingModel(this.parent);
bool _start = false;
get start => _start;
onSwitchDisplay() {
if (isIOS || !_start) return;
final sessionId = parent.target?.sessionId;
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayHeight();
if (sessionId == null || width == null || height == null) return;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
bind.sessionRecordScreen(
sessionId: sessionId,
start: true,
display: currentDisplay,
width: width,
height: height);
}
bool get start => _start;
toggle() async {
if (isIOS) return;
@ -2368,48 +2351,16 @@ class RecordingModel with ChangeNotifier {
if (sessionId == null) return;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
_start = !_start;
notifyListeners();
await _sendStatusMessage(sessionId, pi, _start);
if (_start) {
sessionRefreshVideo(sessionId, pi);
if (versionCmp(pi.version, '1.2.4') >= 0) {
// will not receive SwitchDisplay since 1.2.4
onSwitchDisplay();
}
} else {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay,
width: 0,
height: 0);
bool value = !_start;
if (value) {
await sessionRefreshVideo(sessionId, pi);
}
await bind.sessionRecordScreen(sessionId: sessionId, start: value);
}
onClose() async {
if (isIOS) return;
final sessionId = parent.target?.sessionId;
if (sessionId == null) return;
if (!_start) return;
_start = false;
final pi = parent.target?.ffiModel.pi;
if (pi == null) return;
final currentDisplay = pi.currentDisplay;
if (currentDisplay == kAllDisplayValue) return;
await _sendStatusMessage(sessionId, pi, false);
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay,
width: 0,
height: 0);
}
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
updateStatus(bool status) {
_start = status;
notifyListeners();
}
}

View File

@ -965,6 +965,10 @@ impl Config {
.unwrap_or_default()
}
pub fn get_bool_option(k: &str) -> bool {
option2bool(k, &Self::get_option(k))
}
pub fn set_option(k: String, v: String) {
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
return;
@ -2198,6 +2202,7 @@ pub mod keys {
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing";
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
@ -2342,6 +2347,7 @@ pub mod keys {
OPTION_AUTO_DISCONNECT_TIMEOUT,
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
OPTION_ALLOW_AUTO_RECORD_INCOMING,
OPTION_ALLOW_AUTO_RECORD_OUTGOING,
OPTION_VIDEO_SAVE_DIRECTORY,
OPTION_ENABLE_ABR,
OPTION_ALLOW_REMOVE_WALLPAPER,

View File

@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
git = "https://github.com/rustdesk-org/hwcodec"
optional = true

View File

@ -15,7 +15,7 @@ use crate::{
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
common::GoogleImage,
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture,
};
use hbb_common::{
@ -623,7 +623,7 @@ impl Decoder {
&mut self,
frame: &video_frame::Union,
rgb: &mut ImageRgb,
_texture: &mut *mut c_void,
_texture: &mut ImageTexture,
_pixelbuffer: &mut bool,
chroma: &mut Option<Chroma>,
) -> ResultType<bool> {
@ -777,12 +777,16 @@ impl Decoder {
fn handle_vram_video_frame(
decoder: &mut VRamDecoder,
frames: &EncodedVideoFrames,
texture: &mut *mut c_void,
texture: &mut ImageTexture,
) -> ResultType<bool> {
let mut ret = false;
for h26x in frames.frames.iter() {
for image in decoder.decode(&h26x.data)? {
*texture = image.frame.texture;
*texture = ImageTexture {
texture: image.frame.texture,
w: image.frame.width as _,
h: image.frame.height as _,
};
ret = true;
}
}

View File

@ -96,6 +96,22 @@ impl ImageRgb {
}
}
pub struct ImageTexture {
pub texture: *mut c_void,
pub w: usize,
pub h: usize,
}
impl Default for ImageTexture {
fn default() -> Self {
Self {
texture: std::ptr::null_mut(),
w: 0,
h: 0,
}
}
}
#[inline]
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
// does this really help?
@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat {
}
}
impl From<&video_frame::Union> for CodecFormat {
fn from(it: &video_frame::Union) -> Self {
match it {
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
video_frame::Union::Av1s(_) => CodecFormat::AV1,
video_frame::Union::H264s(_) => CodecFormat::H264,
video_frame::Union::H265s(_) => CodecFormat::H265,
_ => CodecFormat::Unknown,
}
}
}
impl From<&CodecName> for CodecFormat {
fn from(value: &CodecName) -> Self {
match value {

View File

@ -25,22 +25,28 @@ pub struct RecorderContext {
pub server: bool,
pub id: String,
pub dir: String,
pub display: usize,
pub tx: Option<Sender<RecordState>>,
}
#[derive(Debug, Clone)]
pub struct RecorderContext2 {
pub filename: String,
pub width: usize,
pub height: usize,
pub format: CodecFormat,
pub tx: Option<Sender<RecordState>>,
}
impl RecorderContext {
pub fn set_filename(&mut self) -> ResultType<()> {
if !PathBuf::from(&self.dir).exists() {
std::fs::create_dir_all(&self.dir)?;
impl RecorderContext2 {
pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
if !PathBuf::from(&ctx.dir).exists() {
std::fs::create_dir_all(&ctx.dir)?;
}
let file = if self.server { "incoming" } else { "outgoing" }.to_string()
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
+ "_"
+ &self.id.clone()
+ &ctx.id.clone()
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
+ &format!("display{}_", ctx.display)
+ &self.format.to_string().to_lowercase()
+ if self.format == CodecFormat::VP9
|| self.format == CodecFormat::VP8
@ -50,11 +56,10 @@ impl RecorderContext {
} else {
".mp4"
};
self.filename = PathBuf::from(&self.dir)
self.filename = PathBuf::from(&ctx.dir)
.join(file)
.to_string_lossy()
.to_string();
log::info!("video will save to {}", self.filename);
Ok(())
}
}
@ -63,7 +68,7 @@ unsafe impl Send for Recorder {}
unsafe impl Sync for Recorder {}
pub trait RecorderApi {
fn new(ctx: RecorderContext) -> ResultType<Self>
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
where
Self: Sized;
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
@ -78,13 +83,15 @@ pub enum RecordState {
}
pub struct Recorder {
pub inner: Box<dyn RecorderApi>,
pub inner: Option<Box<dyn RecorderApi>>,
ctx: RecorderContext,
ctx2: Option<RecorderContext2>,
pts: Option<i64>,
check_failed: bool,
}
impl Deref for Recorder {
type Target = Box<dyn RecorderApi>;
type Target = Option<Box<dyn RecorderApi>>;
fn deref(&self) -> &Self::Target {
&self.inner
@ -98,114 +105,123 @@ impl DerefMut for Recorder {
}
impl Recorder {
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
ctx.set_filename()?;
let recorder = match ctx.format {
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder {
inner: Box::new(WebmRecorder::new(ctx.clone())?),
ctx,
pts: None,
},
#[cfg(feature = "hwcodec")]
_ => Recorder {
inner: Box::new(HwRecorder::new(ctx.clone())?),
ctx,
pts: None,
},
#[cfg(not(feature = "hwcodec"))]
_ => bail!("unsupported codec type"),
};
recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone()));
Ok(recorder)
pub fn new(ctx: RecorderContext) -> ResultType<Self> {
Ok(Self {
inner: None,
ctx,
ctx2: None,
pts: None,
check_failed: false,
})
}
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
ctx.set_filename()?;
self.inner = match ctx.format {
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => {
Box::new(WebmRecorder::new(ctx.clone())?)
fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
match self.ctx2 {
Some(ref ctx2) => {
if ctx2.width != w || ctx2.height != h || ctx2.format != format {
let mut ctx2 = RecorderContext2 {
width: w,
height: h,
format,
filename: Default::default(),
};
ctx2.set_filename(&self.ctx)?;
self.ctx2 = Some(ctx2);
self.inner = None;
}
}
#[cfg(feature = "hwcodec")]
_ => Box::new(HwRecorder::new(ctx.clone())?),
#[cfg(not(feature = "hwcodec"))]
_ => bail!("unsupported codec type"),
None => {
let mut ctx2 = RecorderContext2 {
width: w,
height: h,
format,
filename: Default::default(),
};
ctx2.set_filename(&self.ctx)?;
self.ctx2 = Some(ctx2);
self.inner = None;
}
}
let Some(ctx2) = &self.ctx2 else {
bail!("ctx2 is None");
};
self.ctx = ctx;
self.pts = None;
self.send_state(RecordState::NewFile(self.ctx.filename.clone()));
if self.inner.is_none() {
self.inner = match format {
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
)),
#[cfg(feature = "hwcodec")]
_ => Some(Box::new(HwRecorder::new(
self.ctx.clone(),
(*ctx2).clone(),
)?)),
#[cfg(not(feature = "hwcodec"))]
_ => bail!("unsupported codec type"),
};
self.pts = None;
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
}
Ok(())
}
pub fn write_message(&mut self, msg: &Message) {
pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
if let Some(frame) = &vf.union {
self.write_frame(frame).ok();
self.write_frame(frame, w, h).ok();
}
}
}
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
pub fn write_frame(
&mut self,
frame: &video_frame::Union,
w: usize,
h: usize,
) -> ResultType<()> {
if self.check_failed {
bail!("check failed");
}
let format = CodecFormat::from(frame);
if format == CodecFormat::Unknown {
bail!("unsupported frame type");
}
let res = self.check(w, h, format);
if res.is_err() {
self.check_failed = true;
log::error!("check failed: {:?}", res);
res?;
}
match frame {
video_frame::Union::Vp8s(vp8s) => {
if self.ctx.format != CodecFormat::VP8 {
self.change(RecorderContext {
format: CodecFormat::VP8,
..self.ctx.clone()
})?;
}
for f in vp8s.frames.iter() {
self.check_pts(f.pts)?;
self.write_video(f);
self.check_pts(f.pts, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
video_frame::Union::Vp9s(vp9s) => {
if self.ctx.format != CodecFormat::VP9 {
self.change(RecorderContext {
format: CodecFormat::VP9,
..self.ctx.clone()
})?;
}
for f in vp9s.frames.iter() {
self.check_pts(f.pts)?;
self.write_video(f);
self.check_pts(f.pts, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
video_frame::Union::Av1s(av1s) => {
if self.ctx.format != CodecFormat::AV1 {
self.change(RecorderContext {
format: CodecFormat::AV1,
..self.ctx.clone()
})?;
}
for f in av1s.frames.iter() {
self.check_pts(f.pts)?;
self.write_video(f);
self.check_pts(f.pts, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H264s(h264s) => {
if self.ctx.format != CodecFormat::H264 {
self.change(RecorderContext {
format: CodecFormat::H264,
..self.ctx.clone()
})?;
}
for f in h264s.frames.iter() {
self.check_pts(f.pts)?;
self.write_video(f);
self.check_pts(f.pts, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
#[cfg(feature = "hwcodec")]
video_frame::Union::H265s(h265s) => {
if self.ctx.format != CodecFormat::H265 {
self.change(RecorderContext {
format: CodecFormat::H265,
..self.ctx.clone()
})?;
}
for f in h265s.frames.iter() {
self.check_pts(f.pts)?;
self.write_video(f);
self.check_pts(f.pts, w, h, format)?;
self.as_mut().map(|x| x.write_video(f));
}
}
_ => bail!("unsupported frame type"),
@ -214,13 +230,21 @@ impl Recorder {
Ok(())
}
fn check_pts(&mut self, pts: i64) -> ResultType<()> {
fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
let old_pts = self.pts;
self.pts = Some(pts);
if old_pts.clone().unwrap_or_default() > pts {
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
self.change(self.ctx.clone())?;
self.inner = None;
self.ctx2 = None;
let res = self.check(w, h, format);
if res.is_err() {
self.check_failed = true;
log::error!("check failed: {:?}", res);
res?;
}
self.pts = Some(pts);
}
Ok(())
}
@ -234,21 +258,22 @@ struct WebmRecorder {
vt: VideoTrack,
webm: Option<Segment<Writer<File>>>,
ctx: RecorderContext,
ctx2: RecorderContext2,
key: bool,
written: bool,
start: Instant,
}
impl RecorderApi for WebmRecorder {
fn new(ctx: RecorderContext) -> ResultType<Self> {
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
let out = match {
OpenOptions::new()
.write(true)
.create_new(true)
.open(&ctx.filename)
.open(&ctx2.filename)
} {
Ok(file) => file,
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?,
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
Err(e) => return Err(e.into()),
};
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder {
None => bail!("Failed to create webm mux"),
};
let vt = webm.add_video_track(
ctx.width as _,
ctx.height as _,
ctx2.width as _,
ctx2.height as _,
None,
if ctx.format == CodecFormat::VP9 {
if ctx2.format == CodecFormat::VP9 {
mux::VideoCodecId::VP9
} else if ctx.format == CodecFormat::VP8 {
} else if ctx2.format == CodecFormat::VP8 {
mux::VideoCodecId::VP8
} else {
mux::VideoCodecId::AV1
},
);
if ctx.format == CodecFormat::AV1 {
if ctx2.format == CodecFormat::AV1 {
// [129, 8, 12, 0] in 3.6.0, but zero works
let codec_private = vec![0, 0, 0, 0];
if !webm.set_codec_private(vt.track_number(), &codec_private) {
@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder {
vt,
webm: Some(webm),
ctx,
ctx2,
key: false,
written: false,
start: Instant::now(),
@ -307,7 +333,7 @@ impl Drop for WebmRecorder {
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
let mut state = RecordState::WriteTail;
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
std::fs::remove_file(&self.ctx.filename).ok();
std::fs::remove_file(&self.ctx2.filename).ok();
state = RecordState::RemoveFile;
}
self.ctx.tx.as_ref().map(|tx| tx.send(state));
@ -318,6 +344,7 @@ impl Drop for WebmRecorder {
struct HwRecorder {
muxer: Muxer,
ctx: RecorderContext,
ctx2: RecorderContext2,
written: bool,
key: bool,
start: Instant,
@ -325,18 +352,19 @@ struct HwRecorder {
#[cfg(feature = "hwcodec")]
impl RecorderApi for HwRecorder {
fn new(ctx: RecorderContext) -> ResultType<Self> {
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
let muxer = Muxer::new(MuxContext {
filename: ctx.filename.clone(),
width: ctx.width,
height: ctx.height,
is265: ctx.format == CodecFormat::H265,
filename: ctx2.filename.clone(),
width: ctx2.width,
height: ctx2.height,
is265: ctx2.format == CodecFormat::H265,
framerate: crate::hwcodec::DEFAULT_FPS as _,
})
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
Ok(HwRecorder {
muxer,
ctx,
ctx2,
written: false,
key: false,
start: Instant::now(),
@ -365,7 +393,7 @@ impl Drop for HwRecorder {
self.muxer.write_tail().ok();
let mut state = RecordState::WriteTail;
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
std::fs::remove_file(&self.ctx.filename).ok();
std::fs::remove_file(&self.ctx2.filename).ok();
state = RecordState::RemoveFile;
}
self.ctx.tx.as_ref().map(|tx| tx.send(state));

View File

@ -30,7 +30,6 @@ pub use file_trait::FileManager;
#[cfg(not(feature = "flutter"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::tokio::sync::mpsc::UnboundedSender;
use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
use hbb_common::{
allow_err,
anyhow::{anyhow, Context},
@ -54,11 +53,15 @@ use hbb_common::{
},
AddrMangle, ResultType, Stream,
};
use hbb_common::{
config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING,
tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver},
};
pub use helper::*;
use scrap::{
codec::Decoder,
record::{Recorder, RecorderContext},
CodecFormat, ImageFormat, ImageRgb,
CodecFormat, ImageFormat, ImageRgb, ImageTexture,
};
use crate::{
@ -1146,7 +1149,7 @@ impl AudioHandler {
pub struct VideoHandler {
decoder: Decoder,
pub rgb: ImageRgb,
pub texture: *mut c_void,
pub texture: ImageTexture,
recorder: Arc<Mutex<Option<Recorder>>>,
record: bool,
_display: usize, // useful for debug
@ -1172,7 +1175,7 @@ impl VideoHandler {
VideoHandler {
decoder: Decoder::new(format, luid),
rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()),
texture: std::ptr::null_mut(),
texture: Default::default(),
recorder: Default::default(),
record: false,
_display,
@ -1220,11 +1223,14 @@ impl VideoHandler {
}
self.first_frame = false;
if self.record {
self.recorder
.lock()
.unwrap()
.as_mut()
.map(|r| r.write_frame(frame));
self.recorder.lock().unwrap().as_mut().map(|r| {
let (w, h) = if *pixelbuffer {
(self.rgb.w, self.rgb.h)
} else {
(self.texture.w, self.texture.h)
};
r.write_frame(frame, w, h).ok();
});
}
res
}
@ -1248,17 +1254,14 @@ impl VideoHandler {
}
/// Start or stop screen record.
pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) {
pub fn record_screen(&mut self, start: bool, id: String, display: usize) {
self.record = false;
if start {
self.recorder = Recorder::new(RecorderContext {
server: false,
id,
dir: crate::ui_interface::video_save_directory(false),
filename: "".to_owned(),
width: w as _,
height: h as _,
format: scrap::CodecFormat::VP9,
display,
tx: None,
})
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))));
@ -1347,6 +1350,7 @@ pub struct LoginConfigHandler {
password_source: PasswordSource, // where the sent password comes from
shared_password: Option<String>, // Store the shared password
pub enable_trusted_devices: bool,
pub record: bool,
}
impl Deref for LoginConfigHandler {
@ -1438,6 +1442,7 @@ impl LoginConfigHandler {
self.adapter_luid = adapter_luid;
self.selected_windows_session_id = None;
self.shared_password = shared_password;
self.record = Config::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING);
}
/// Check if the client should auto login.
@ -2227,7 +2232,7 @@ pub enum MediaData {
AudioFrame(Box<AudioFrame>),
AudioFormat(AudioFormat),
Reset(Option<usize>),
RecordScreen(bool, usize, i32, i32, String),
RecordScreen(bool),
}
pub type MediaSender = mpsc::Sender<MediaData>;
@ -2303,10 +2308,16 @@ where
let start = std::time::Instant::now();
let format = CodecFormat::from(&vf);
if !handler_controller_map.contains_key(&display) {
let mut handler = VideoHandler::new(format, display);
let record = session.lc.read().unwrap().record;
let id = session.lc.read().unwrap().id.clone();
if record {
handler.record_screen(record, id, display);
}
handler_controller_map.insert(
display,
VideoHandlerController {
handler: VideoHandler::new(format, display),
handler,
skip_beginning: 0,
},
);
@ -2325,7 +2336,7 @@ where
video_callback(
display,
&mut handler_controller.handler.rgb,
handler_controller.handler.texture,
handler_controller.handler.texture.texture,
pixelbuffer,
);
@ -2399,18 +2410,19 @@ where
}
}
}
MediaData::RecordScreen(start, display, w, h, id) => {
log::info!("record screen command: start: {start}, display: {display}");
// Compatible with the sciter version(single ui session).
// For the sciter version, there're no multi-ui-sessions for one connection.
// The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler.
if let Some(handler_controler) = handler_controller_map.get_mut(&display) {
handler_controler.handler.record_screen(start, w, h, id);
} else if handler_controller_map.len() == 1 {
if let Some(handler_controler) =
handler_controller_map.values_mut().next()
{
handler_controler.handler.record_screen(start, w, h, id);
MediaData::RecordScreen(start) => {
log::info!("record screen command: start: {start}");
let record = session.lc.read().unwrap().record;
session.update_record_status(start);
if record != start {
session.lc.write().unwrap().record = start;
let id = session.lc.read().unwrap().id.clone();
for (display, handler_controler) in handler_controller_map.iter_mut() {
handler_controler.handler.record_screen(
start,
id.clone(),
*display,
);
}
}
}
@ -3169,7 +3181,7 @@ pub enum Data {
SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
AddJob((i32, String, String, i32, bool, bool)),
ResumeJob((i32, bool)),
RecordScreen(bool, usize, i32, i32, String),
RecordScreen(bool),
ElevateDirect,
ElevateWithLogon(String, String),
NewVoiceCall,

View File

@ -837,10 +837,8 @@ impl<T: InvokeUiSession> Remote<T> {
self.handle_job_status(id, -1, err);
}
}
Data::RecordScreen(start, display, w, h, id) => {
let _ = self
.video_sender
.send(MediaData::RecordScreen(start, display, w, h, id));
Data::RecordScreen(start) => {
let _ = self.video_sender.send(MediaData::RecordScreen(start));
}
Data::ElevateDirect => {
let mut request = ElevationRequest::new();
@ -1218,7 +1216,7 @@ impl<T: InvokeUiSession> Remote<T> {
crate::plugin::handle_listen_event(
crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(),
self.handler.get_id(),
)
);
}
if self.handler.is_file_transfer() {

View File

@ -17,7 +17,7 @@ use serde::Serialize;
use serde_json::json;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
ffi::CString,
os::raw::{c_char, c_int, c_void},
str::FromStr,
@ -1010,6 +1010,10 @@ impl InvokeUiSession for FlutterHandler {
rgba_data.valid = false;
}
}
fn update_record_status(&self, start: bool) {
self.push_event("record_status", &[("start", &start.to_string())], &[]);
}
}
impl FlutterHandler {
@ -1830,7 +1834,6 @@ pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i3
// sessions mod is used to avoid the big lock of sessions' map.
pub mod sessions {
use std::collections::HashSet;
use super::*;

View File

@ -241,21 +241,17 @@ pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn<bool> {
}
}
pub fn session_record_screen(
session_id: SessionID,
start: bool,
display: usize,
width: usize,
height: usize,
) {
pub fn session_record_screen(session_id: SessionID, start: bool) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.record_screen(start, display as _, width as _, height as _);
session.record_screen(start);
}
}
pub fn session_record_status(session_id: SessionID, status: bool) {
pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn<bool> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.record_status(status);
SyncReturn(session.is_recording())
} else {
SyncReturn(false)
}
}

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "التسجيل"),
("Directory", "المسار"),
("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"),
("Automatically record outgoing sessions", ""),
("Change", "تغيير"),
("Start session recording", "بدء تسجيل الجلسة"),
("Stop session recording", "ايقاف تسجيل الجلسة"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Запіс"),
("Directory", "Тэчка"),
("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"),
("Automatically record outgoing sessions", ""),
("Change", "Змяніць"),
("Start session recording", "Пачаць запіс сесіі"),
("Stop session recording", "Спыніць запіс сесіі"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Записване"),
("Directory", "Директория"),
("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"),
("Automatically record outgoing sessions", ""),
("Change", "Промяна"),
("Start session recording", "Започванена запис"),
("Stop session recording", "Край на запис"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Gravació"),
("Directory", "Contactes"),
("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"),
("Automatically record outgoing sessions", ""),
("Change", "Canvia"),
("Start session recording", "Inicia la gravació de la sessió"),
("Stop session recording", "Atura la gravació de la sessió"),

View File

@ -363,7 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unpin Toolbar", "取消固定工具栏"),
("Recording", "录屏"),
("Directory", "目录"),
("Automatically record incoming sessions", "自动录制来访会话"),
("Automatically record incoming sessions", "自动录制传入会话"),
("Automatically record outgoing sessions", "自动录制传出会话"),
("Change", "更改"),
("Start session recording", "开始录屏"),
("Stop session recording", "结束录屏"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Nahrávání"),
("Directory", "Adresář"),
("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"),
("Automatically record outgoing sessions", ""),
("Change", "Změnit"),
("Start session recording", "Spustit záznam relace"),
("Stop session recording", "Zastavit záznam relace"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Optager"),
("Directory", "Mappe"),
("Automatically record incoming sessions", "Optag automatisk indgående sessioner"),
("Automatically record outgoing sessions", ""),
("Change", "Ændr"),
("Start session recording", "Start sessionsoptagelse"),
("Stop session recording", "Stop sessionsoptagelse"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Aufnahme"),
("Directory", "Verzeichnis"),
("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"),
("Automatically record outgoing sessions", ""),
("Change", "Ändern"),
("Start session recording", "Sitzungsaufzeichnung starten"),
("Stop session recording", "Sitzungsaufzeichnung beenden"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Εγγραφή"),
("Directory", "Φάκελος εγγραφών"),
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
("Automatically record outgoing sessions", ""),
("Change", "Αλλαγή"),
("Start session recording", "Έναρξη εγγραφής συνεδρίας"),
("Stop session recording", "Διακοπή εγγραφής συνεδρίας"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Grabando"),
("Directory", "Directorio"),
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
("Automatically record outgoing sessions", ""),
("Change", "Cambiar"),
("Start session recording", "Comenzar grabación de sesión"),
("Stop session recording", "Detener grabación de sesión"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Grabatzen"),
("Directory", "Direktorioa"),
("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"),
("Automatically record outgoing sessions", ""),
("Change", "Aldatu"),
("Start session recording", "Hasi saioaren grabaketa"),
("Stop session recording", "Gelditu saioaren grabaketa"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "در حال ضبط"),
("Directory", "مسیر"),
("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"),
("Automatically record outgoing sessions", ""),
("Change", "تغییر"),
("Start session recording", "شروع ضبط جلسه"),
("Stop session recording", "توقف ضبط جلسه"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Enregistrement"),
("Directory", "Répertoire"),
("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"),
("Automatically record outgoing sessions", ""),
("Change", "Modifier"),
("Start session recording", "Commencer l'enregistrement"),
("Stop session recording", "Stopper l'enregistrement"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Snimanje"),
("Directory", "Mapa"),
("Automatically record incoming sessions", "Automatski snimi dolazne sesije"),
("Automatically record outgoing sessions", ""),
("Change", "Promijeni"),
("Start session recording", "Započni snimanje sesije"),
("Stop session recording", "Zaustavi snimanje sesije"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Felvétel"),
("Directory", "Könyvtár"),
("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"),
("Automatically record outgoing sessions", ""),
("Change", "Változtatás"),
("Start session recording", "Munkamenet rögzítés indítása"),
("Stop session recording", "Munkamenet rögzítés leállítása"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Perekaman"),
("Directory", "Direktori"),
("Automatically record incoming sessions", "Otomatis merekam sesi masuk"),
("Automatically record outgoing sessions", ""),
("Change", "Ubah"),
("Start session recording", "Mulai sesi perekaman"),
("Stop session recording", "Hentikan sesi perekaman"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Registrazione"),
("Directory", "Cartella"),
("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"),
("Automatically record outgoing sessions", ""),
("Change", "Modifica"),
("Start session recording", "Inizia registrazione sessione"),
("Stop session recording", "Ferma registrazione sessione"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "録画"),
("Directory", "ディレクトリ"),
("Automatically record incoming sessions", "受信したセッションを自動で記録する"),
("Automatically record outgoing sessions", ""),
("Change", "変更"),
("Start session recording", "セッションの録画を開始"),
("Stop session recording", "セッションの録画を停止"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "녹화"),
("Directory", "경로"),
("Automatically record incoming sessions", "들어오는 세션을 자동으로 녹화"),
("Automatically record outgoing sessions", ""),
("Change", "변경"),
("Start session recording", "세션 녹화 시작"),
("Stop session recording", "세션 녹화 중지"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Įrašymas"),
("Directory", "Katalogas"),
("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"),
("Automatically record outgoing sessions", ""),
("Change", "Keisti"),
("Start session recording", "Pradėti seanso įrašinėjimą"),
("Stop session recording", "Sustabdyti seanso įrašinėjimą"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Ierakstīšana"),
("Directory", "Direktorija"),
("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"),
("Automatically record outgoing sessions", ""),
("Change", "Mainīt"),
("Start session recording", "Sākt sesijas ierakstīšanu"),
("Stop session recording", "Apturēt sesijas ierakstīšanu"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Opptak"),
("Directory", "Mappe"),
("Automatically record incoming sessions", "Ta opp innkommende sesjoner automatisk"),
("Automatically record outgoing sessions", ""),
("Change", "Rediger"),
("Start session recording", "Start sesjonsopptak"),
("Stop session recording", "Stopp sesjonsopptak"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Opnemen"),
("Directory", "Map"),
("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"),
("Automatically record outgoing sessions", ""),
("Change", "Wissel"),
("Start session recording", "Start de sessieopname"),
("Stop session recording", "Stop de sessieopname"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Nagrywanie"),
("Directory", "Folder"),
("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"),
("Automatically record outgoing sessions", ""),
("Change", "Zmień"),
("Start session recording", "Zacznij nagrywać sesję"),
("Stop session recording", "Zatrzymaj nagrywanie sesji"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Gravando"),
("Directory", "Diretório"),
("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"),
("Automatically record outgoing sessions", ""),
("Change", "Alterar"),
("Start session recording", "Iniciar gravação da sessão"),
("Stop session recording", "Parar gravação da sessão"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Înregistrare"),
("Directory", "Director"),
("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"),
("Automatically record outgoing sessions", ""),
("Change", "Modifică"),
("Start session recording", "Începe înregistrarea"),
("Stop session recording", "Oprește înregistrarea"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Запись"),
("Directory", "Папка"),
("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"),
("Automatically record outgoing sessions", ""),
("Change", "Изменить"),
("Start session recording", "Начать запись сеанса"),
("Stop session recording", "Остановить запись сеанса"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Nahrávanie"),
("Directory", "Adresár"),
("Automatically record incoming sessions", "Automaticky nahrávať prichádzajúce relácie"),
("Automatically record outgoing sessions", ""),
("Change", "Zmeniť"),
("Start session recording", "Spustiť záznam relácie"),
("Stop session recording", "Zastaviť záznam relácie"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Snemanje"),
("Directory", "Imenik"),
("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"),
("Automatically record outgoing sessions", ""),
("Change", "Spremeni"),
("Start session recording", "Začni snemanje seje"),
("Stop session recording", "Ustavi snemanje seje"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Regjistrimi"),
("Directory", "Direktoria"),
("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"),
("Automatically record outgoing sessions", ""),
("Change", "Ndrysho"),
("Start session recording", "Fillo regjistrimin e sesionit"),
("Stop session recording", "Ndalo regjistrimin e sesionit"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Snimanje"),
("Directory", "Direktorijum"),
("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"),
("Automatically record outgoing sessions", ""),
("Change", "Promeni"),
("Start session recording", "Započni snimanje sesije"),
("Stop session recording", "Zaustavi snimanje sesije"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Spelar in"),
("Directory", "Katalog"),
("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"),
("Automatically record outgoing sessions", ""),
("Change", "Byt"),
("Start session recording", "Starta inspelning"),
("Stop session recording", "Avsluta inspelning"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", ""),
("Directory", ""),
("Automatically record incoming sessions", ""),
("Automatically record outgoing sessions", ""),
("Change", ""),
("Start session recording", ""),
("Stop session recording", ""),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "การบันทึก"),
("Directory", "ไดเรกทอรี่"),
("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"),
("Automatically record outgoing sessions", ""),
("Change", "เปลี่ยน"),
("Start session recording", "เริ่มต้นการบันทึกเซสชัน"),
("Stop session recording", "หยุดการบันทึกเซสซัน"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Kayıt Ediliyor"),
("Directory", "Klasör"),
("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"),
("Automatically record outgoing sessions", ""),
("Change", "Değiştir"),
("Start session recording", "Oturum kaydını başlat"),
("Stop session recording", "Oturum kaydını sonlandır"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "錄製"),
("Directory", "路徑"),
("Automatically record incoming sessions", "自動錄製連入的工作階段"),
("Automatically record outgoing sessions", ""),
("Change", "變更"),
("Start session recording", "開始錄影"),
("Stop session recording", "停止錄影"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Запис"),
("Directory", "Директорія"),
("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"),
("Automatically record outgoing sessions", ""),
("Change", "Змінити"),
("Start session recording", "Розпочати запис сеансу"),
("Stop session recording", "Закінчити запис сеансу"),

View File

@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Đang ghi hình"),
("Directory", "Thư mục"),
("Automatically record incoming sessions", "Tự động ghi những phiên kết nối vào"),
("Automatically record outgoing sessions", ""),
("Change", "Thay đổi"),
("Start session recording", "Bắt đầu ghi hình phiên kết nối"),
("Stop session recording", "Dừng ghi hình phiên kết nối"),

View File

@ -487,6 +487,8 @@ fn run(vs: VideoService) -> ResultType<()> {
let repeat_encode_max = 10;
let mut encode_fail_counter = 0;
let mut first_frame = true;
let capture_width = c.width;
let capture_height = c.height;
while sp.ok() {
#[cfg(windows)]
@ -576,6 +578,8 @@ fn run(vs: VideoService) -> ResultType<()> {
recorder.clone(),
&mut encode_fail_counter,
&mut first_frame,
capture_width,
capture_height,
)?;
frame_controller.set_send(now, send_conn_ids);
}
@ -632,6 +636,8 @@ fn run(vs: VideoService) -> ResultType<()> {
recorder.clone(),
&mut encode_fail_counter,
&mut first_frame,
capture_width,
capture_height,
)?;
frame_controller.set_send(now, send_conn_ids);
}
@ -722,7 +728,13 @@ fn setup_encoder(
);
Encoder::set_fallback(&encoder_cfg);
let codec_format = Encoder::negotiated_codec();
let recorder = get_recorder(c.width, c.height, &codec_format, record_incoming);
let recorder = get_recorder(
c.width,
c.height,
&codec_format,
record_incoming,
display_idx,
);
let use_i444 = Encoder::use_i444(&encoder_cfg);
let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?;
Ok((encoder, encoder_cfg, codec_format, use_i444, recorder))
@ -809,6 +821,7 @@ fn get_recorder(
height: usize,
codec_format: &CodecFormat,
record_incoming: bool,
display: usize,
) -> Arc<Mutex<Option<Recorder>>> {
#[cfg(windows)]
let root = crate::platform::is_root();
@ -828,10 +841,7 @@ fn get_recorder(
server: true,
id: Config::get_id(),
dir: crate::ui_interface::video_save_directory(root),
filename: "".to_owned(),
width,
height,
format: codec_format.clone(),
display,
tx,
})
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))))
@ -910,6 +920,8 @@ fn handle_one_frame(
recorder: Arc<Mutex<Option<Recorder>>>,
encode_fail_counter: &mut usize,
first_frame: &mut bool,
width: usize,
height: usize,
) -> ResultType<HashSet<i32>> {
sp.snapshot(|sps| {
// so that new sub and old sub share the same encoder after switch
@ -933,7 +945,7 @@ fn handle_one_frame(
.lock()
.unwrap()
.as_mut()
.map(|r| r.write_message(&msg));
.map(|r| r.write_message(&msg, width, height));
send_conn_ids = sp.send_video_frame(msg);
}
Err(e) => {

View File

@ -301,26 +301,12 @@ class Header: Reactor.Component {
}
event click $(span#recording) (_, me) {
recording = !recording;
header.update();
handler.record_status(recording);
// 0 is just a dummy value. It will be ignored by the handler.
if (recording) {
handler.refresh_video(0);
if (handler.version_cmp(pi.version, '1.2.4') >= 0) handler.record_screen(recording, pi.current_display, display_width, display_height);
}
else {
handler.record_screen(recording, pi.current_display, display_width, display_height);
}
handler.record_screen(!recording)
}
event click $(#screen) (_, me) {
if (pi.current_display == me.index) return;
if (recording) {
recording = false;
handler.record_screen(false, pi.current_display, display_width, display_height);
handler.record_status(false);
}
handler.switch_display(me.index);
}
@ -518,6 +504,7 @@ if (!(is_file_transfer || is_port_forward)) {
handler.updatePi = function(v) {
pi = v;
recording = handler.is_recording();
header.update();
if (is_port_forward) {
view.windowState = View.WINDOW_MINIMIZED;
@ -682,3 +669,8 @@ handler.setConnectionType = function(secured, direct) {
direct_connection: direct,
});
}
handler.updateRecordStatus = function(status) {
recording = status;
header.update();
}

View File

@ -253,10 +253,12 @@ class Enhancements: Reactor.Component {
var root_dir = show_root_dir ? handler.video_save_directory(true) : "";
var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {};
var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {};
var ts2 = handler.get_option("allow-auto-record-outgoing") == '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><button|checkbox(auto_record_outgoing) {ts2}>{translate('Automatically record outgoing sessions')}</button></div>
<div>
{show_root_dir ? <div style="word-wrap:break-word"><span>{translate("Incoming")}:&nbsp;&nbsp;</span><span>{root_dir}</span></div> : ""}
<div style="word-wrap:break-word"><span>{translate(show_root_dir ? "Outgoing" : "Directory")}:&nbsp;&nbsp;</span><span #folderPath>{user_dir}</span></div>
@ -267,6 +269,7 @@ class Enhancements: Reactor.Component {
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("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : '');
handler.set_option("video-save-directory", $(#folderPath).text);
});
}

View File

@ -335,6 +335,10 @@ impl InvokeUiSession for SciterHandler {
}
fn next_rgba(&self, _display: usize) {}
fn update_record_status(&self, start: bool) {
self.call("updateRecordStatus", &make_args!(start));
}
}
pub struct SciterSession(Session<SciterHandler>);
@ -478,8 +482,7 @@ impl sciter::EventHandler for SciterSession {
fn save_image_quality(String);
fn save_custom_image_quality(i32);
fn refresh_video(i32);
fn record_screen(bool, i32, i32, i32);
fn record_status(bool);
fn record_screen(bool);
fn get_toggle_option(String);
fn is_privacy_mode_supported();
fn toggle_option(String);
@ -496,6 +499,7 @@ impl sciter::EventHandler for SciterSession {
fn close_voice_call();
fn version_cmp(String, String);
fn set_selected_windows_session_id(String);
fn is_recording();
}
}

View File

@ -389,22 +389,17 @@ impl<T: InvokeUiSession> Session<T> {
self.send(Data::Message(LoginConfigHandler::refresh()));
}
pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) {
self.send(Data::RecordScreen(
start,
display as usize,
w,
h,
self.get_id(),
));
}
pub fn record_status(&self, status: bool) {
pub fn record_screen(&self, start: bool) {
let mut misc = Misc::new();
misc.set_client_record_status(status);
misc.set_client_record_status(start);
let mut msg = Message::new();
msg.set_misc(misc);
self.send(Data::Message(msg));
self.send(Data::RecordScreen(start));
}
pub fn is_recording(&self) -> bool {
self.lc.read().unwrap().record
}
pub fn save_custom_image_quality(&self, custom_image_quality: i32) {
@ -1557,6 +1552,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn set_current_display(&self, disp_idx: i32);
#[cfg(feature = "flutter")]
fn is_multi_ui_session(&self) -> bool;
fn update_record_status(&self, start: bool);
}
impl<T: InvokeUiSession> Deref for Session<T> {