Browse Source

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>
21pages 1 day ago
parent
commit
e8187588c1
65 changed files with 442 additions and 322 deletions
  1. 1 1
      Cargo.lock
  2. 1 0
      flutter/lib/consts.dart
  3. 51 42
      flutter/lib/desktop/pages/desktop_setting_page.dart
  4. 2 1
      flutter/lib/desktop/pages/remote_page.dart
  5. 1 2
      flutter/lib/desktop/widgets/remote_toolbar.dart
  6. 8 1
      flutter/lib/mobile/pages/remote_page.dart
  7. 50 23
      flutter/lib/mobile/pages/settings_page.dart
  8. 12 61
      flutter/lib/models/model.dart
  9. 6 0
      libs/hbb_common/src/config.rs
  10. 0 1
      libs/scrap/Cargo.toml
  11. 8 4
      libs/scrap/src/common/codec.rs
  12. 29 0
      libs/scrap/src/common/mod.rs
  13. 131 103
      libs/scrap/src/common/record.rs
  14. 42 30
      src/client.rs
  15. 3 5
      src/client/io_loop.rs
  16. 5 2
      src/flutter.rs
  17. 6 10
      src/flutter_ffi.rs
  18. 1 0
      src/lang/ar.rs
  19. 1 0
      src/lang/be.rs
  20. 1 0
      src/lang/bg.rs
  21. 1 0
      src/lang/ca.rs
  22. 2 1
      src/lang/cn.rs
  23. 1 0
      src/lang/cs.rs
  24. 1 0
      src/lang/da.rs
  25. 1 0
      src/lang/de.rs
  26. 1 0
      src/lang/el.rs
  27. 1 0
      src/lang/eo.rs
  28. 1 0
      src/lang/es.rs
  29. 1 0
      src/lang/et.rs
  30. 1 0
      src/lang/eu.rs
  31. 1 0
      src/lang/fa.rs
  32. 1 0
      src/lang/fr.rs
  33. 1 0
      src/lang/he.rs
  34. 1 0
      src/lang/hr.rs
  35. 1 0
      src/lang/hu.rs
  36. 1 0
      src/lang/id.rs
  37. 1 0
      src/lang/it.rs
  38. 1 0
      src/lang/ja.rs
  39. 1 0
      src/lang/ko.rs
  40. 1 0
      src/lang/kz.rs
  41. 1 0
      src/lang/lt.rs
  42. 1 0
      src/lang/lv.rs
  43. 1 0
      src/lang/nb.rs
  44. 1 0
      src/lang/nl.rs
  45. 1 0
      src/lang/pl.rs
  46. 1 0
      src/lang/pt_PT.rs
  47. 1 0
      src/lang/ptbr.rs
  48. 1 0
      src/lang/ro.rs
  49. 1 0
      src/lang/ru.rs
  50. 1 0
      src/lang/sk.rs
  51. 1 0
      src/lang/sl.rs
  52. 1 0
      src/lang/sq.rs
  53. 1 0
      src/lang/sr.rs
  54. 1 0
      src/lang/sv.rs
  55. 1 0
      src/lang/template.rs
  56. 1 0
      src/lang/th.rs
  57. 1 0
      src/lang/tr.rs
  58. 1 0
      src/lang/tw.rs
  59. 1 0
      src/lang/uk.rs
  60. 1 0
      src/lang/vn.rs
  61. 18 6
      src/server/video_service.rs
  62. 7 15
      src/ui/header.tis
  63. 3 0
      src/ui/index.tis
  64. 6 2
      src/ui/remote.rs
  65. 8 12
      src/ui_session_interface.rs

+ 1 - 1
Cargo.lock

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

+ 1 - 0
flutter/lib/consts.dart

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

+ 51 - 42
flutter/lib/desktop/pages/desktop_setting_page.dart

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

+ 2 - 1
flutter/lib/desktop/pages/remote_page.dart

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

+ 1 - 2
flutter/lib/desktop/widgets/remote_toolbar.dart

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

+ 8 - 1
flutter/lib/mobile/pages/remote_page.dart

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

+ 50 - 23
flutter/lib/mobile/pages/settings_page.dart

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

+ 12 - 61
flutter/lib/models/model.dart

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

+ 6 - 0
libs/hbb_common/src/config.rs

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

+ 0 - 1
libs/scrap/Cargo.toml

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

+ 8 - 4
libs/scrap/src/common/codec.rs

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

+ 29 - 0
libs/scrap/src/common/mod.rs

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

+ 131 - 103
libs/scrap/src/common/record.rs

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

+ 42 - 30
src/client.rs

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

+ 3 - 5
src/client/io_loop.rs

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

+ 5 - 2
src/flutter.rs

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

+ 6 - 10
src/flutter_ffi.rs

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

+ 1 - 0
src/lang/ar.rs

@@ -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", "ايقاف تسجيل الجلسة"),

+ 1 - 0
src/lang/be.rs

@@ -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", "Спыніць запіс сесіі"),

+ 1 - 0
src/lang/bg.rs

@@ -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", "Край на запис"),

+ 1 - 0
src/lang/ca.rs

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

+ 2 - 1
src/lang/cn.rs

@@ -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", "结束录屏"),

+ 1 - 0
src/lang/cs.rs

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

+ 1 - 0
src/lang/da.rs

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

+ 1 - 0
src/lang/de.rs

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

+ 1 - 0
src/lang/el.rs

@@ -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", "Διακοπή εγγραφής συνεδρίας"),

+ 1 - 0
src/lang/eo.rs

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

+ 1 - 0
src/lang/es.rs

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

+ 1 - 0
src/lang/et.rs

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

+ 1 - 0
src/lang/eu.rs

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

+ 1 - 0
src/lang/fa.rs

@@ -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", "توقف ضبط جلسه"),

+ 1 - 0
src/lang/fr.rs

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

+ 1 - 0
src/lang/he.rs

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

+ 1 - 0
src/lang/hr.rs

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

+ 1 - 0
src/lang/hu.rs

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

+ 1 - 0
src/lang/id.rs

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

+ 1 - 0
src/lang/it.rs

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

+ 1 - 0
src/lang/ja.rs

@@ -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", "セッションの録画を停止"),

+ 1 - 0
src/lang/ko.rs

@@ -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", "세션 녹화 중지"),

+ 1 - 0
src/lang/kz.rs

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

+ 1 - 0
src/lang/lt.rs

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

+ 1 - 0
src/lang/lv.rs

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

+ 1 - 0
src/lang/nb.rs

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

+ 1 - 0
src/lang/nl.rs

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

+ 1 - 0
src/lang/pl.rs

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

+ 1 - 0
src/lang/pt_PT.rs

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

+ 1 - 0
src/lang/ptbr.rs

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

+ 1 - 0
src/lang/ro.rs

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

+ 1 - 0
src/lang/ru.rs

@@ -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", "Остановить запись сеанса"),

+ 1 - 0
src/lang/sk.rs

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

+ 1 - 0
src/lang/sl.rs

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

+ 1 - 0
src/lang/sq.rs

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

+ 1 - 0
src/lang/sr.rs

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

+ 1 - 0
src/lang/sv.rs

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

+ 1 - 0
src/lang/template.rs

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

+ 1 - 0
src/lang/th.rs

@@ -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", "หยุดการบันทึกเซสซัน"),

+ 1 - 0
src/lang/tr.rs

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

+ 1 - 0
src/lang/tw.rs

@@ -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", "停止錄影"),

+ 1 - 0
src/lang/uk.rs

@@ -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", "Закінчити запис сеансу"),

+ 1 - 0
src/lang/vn.rs

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

+ 18 - 6
src/server/video_service.rs

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

+ 7 - 15
src/ui/header.tis

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

+ 3 - 0
src/ui/index.tis

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

+ 6 - 2
src/ui/remote.rs

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

+ 8 - 12
src/ui_session_interface.rs

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