mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-01-23 02:23:01 +08:00
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:
parent
289076aa70
commit
e8187588c1
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
optional = true
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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::*;
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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", "ايقاف تسجيل الجلسة"),
|
||||
|
@ -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", "Спыніць запіс сесіі"),
|
||||
|
@ -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", "Край на запис"),
|
||||
|
@ -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ó"),
|
||||
|
@ -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", "结束录屏"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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", "Διακοπή εγγραφής συνεδρίας"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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"),
|
||||
|
@ -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", "توقف ضبط جلسه"),
|
||||
|
@ -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"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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", "セッションの録画を停止"),
|
||||
|
@ -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", "세션 녹화 중지"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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ą"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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", "Остановить запись сеанса"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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", ""),
|
||||
|
@ -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", "หยุดการบันทึกเซสซัน"),
|
||||
|
@ -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"),
|
||||
|
@ -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", "停止錄影"),
|
||||
|
@ -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", "Закінчити запис сеансу"),
|
||||
|
@ -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"),
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
}
|
@ -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")}: </span><span>{root_dir}</span></div> : ""}
|
||||
<div style="word-wrap:break-word"><span>{translate(show_root_dir ? "Outgoing" : "Directory")}: </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);
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
Loading…
Reference in New Issue
Block a user