Merge branch 'master' into master

This commit is contained in:
RustDesk 2023-10-14 22:35:07 +08:00 committed by GitHub
commit 45a9e54631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 3071 additions and 1350 deletions

2
Cargo.lock generated
View File

@ -6602,7 +6602,7 @@ dependencies = [
[[package]] [[package]]
name = "wallpaper" name = "wallpaper"
version = "3.2.0" version = "3.2.0"
source = "git+https://github.com/21pages/wallpaper.rs#2bbb70acd93be179c69cb96cb8c3dda487e6f5fd" source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
dependencies = [ dependencies = [
"dirs 5.0.1", "dirs 5.0.1",
"enquote", "enquote",

View File

@ -1054,7 +1054,7 @@ Widget msgboxIcon(String type) {
if (type == 'on-uac' || type == 'on-foreground-elevated') { if (type == 'on-uac' || type == 'on-foreground-elevated') {
iconData = Icons.admin_panel_settings; iconData = Icons.admin_panel_settings;
} }
if (type == "info") { if (type.contains('info')) {
iconData = Icons.info; iconData = Icons.info;
} }
if (iconData != null) { if (iconData != null) {
@ -2317,7 +2317,7 @@ Widget dialogButton(String text,
} }
} }
int version_cmp(String v1, String v2) { int versionCmp(String v1, String v2) {
return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
} }
@ -2589,3 +2589,18 @@ String getDesktopTabLabel(String peerId, String alias) {
} }
return label; return label;
} }
sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
if (pi.currentDisplay == kAllDisplayValue) {
for (int i = 0; i < pi.displays.length; i++) {
await bind.sessionRefresh(sessionId: sessionId, display: i);
}
} else {
await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay);
}
}
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
pi.isSupportMultiDisplay &&
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';

View File

@ -1295,7 +1295,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {} } catch (_) {}
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) || bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0; versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
final content = customImageQualityWidget( final content = customImageQualityWidget(
initQuality: qualityInitValue, initQuality: qualityInitValue,

View File

@ -600,7 +600,7 @@ abstract class BasePeerCard extends StatelessWidget {
await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs); await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
_openInWindowsAction(String id) async => await _openNewConnInAction( _openInWindowsAction(String id) async => await _openNewConnInAction(
id, 'Open in New Window', kOptionOpenInWindows); id, 'Open in new window', kOptionOpenInWindows);
_openNewConnInOptAction(String id) async => _openNewConnInOptAction(String id) async =>
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)

View File

@ -90,7 +90,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Row(children: [ child: Row(children: [
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')), Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
Offstage( Offstage(
offstage: isDesktop, offstage: isDesktop,
child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12), child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12),
@ -99,13 +99,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
trailingIcon: Transform.scale( trailingIcon: Transform.scale(
scale: 0.8, scale: 0.8,
child: InkWell( child: InkWell(
onTap: () => pi.is_headless onTap: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager) ? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordEditIcon(sessionId, ffi.dialogManager), : handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
child: Icon(Icons.edit), child: Icon(Icons.edit),
), ),
), ),
onPressed: () => pi.is_headless onPressed: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager) ? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordAction(sessionId, ffi.dialogManager), : handleOsPasswordAction(sessionId, ffi.dialogManager),
), ),
@ -208,7 +208,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
ffiModel.keyboard && ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS && pi.platform != kPeerPlatformMacOS &&
version_cmp(pi.version, '1.2.0') >= 0) { versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetDefaultSessionsCount(id: id) == 1) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Switch Sides')), child: Text(translate('Switch Sides')),
onPressed: () => onPressed: () =>
@ -217,8 +218,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
// refresh // refresh
if (pi.version.isNotEmpty) { if (pi.version.isNotEmpty) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Refresh')), child: Text(translate('Refresh')),
onPressed: () => bind.sessionRefresh(sessionId: sessionId))); onPressed: () => sessionRefreshVideo(sessionId, pi),
));
} }
// record // record
var codecFormat = ffi.qualityMonitorModel.data.codecFormat; var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
@ -377,7 +379,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
// show remote cursor // show remote cursor
if (pi.platform != kPeerPlatformAndroid && if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded && !ffi.canvasModel.cursorEmbedded &&
!pi.is_wayland) { !pi.isWayland) {
final state = ShowRemoteCursorState.find(id); final state = ShowRemoteCursorState.find(id);
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor'; final option = 'show-remote-cursor';
@ -488,7 +490,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
value: rxValue.value, value: rxValue.value,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
if (ffiModel.pi.currentDisplay != 0) { if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info', msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
'Please switch to Display 1 first', '', ffi.dialogManager); 'Please switch to Display 1 first', '', ffi.dialogManager);
return; return;
@ -512,5 +515,23 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
child: Text(translate('Swap control-command key')))); child: Text(translate('Swap control-command key'))));
} }
if (pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isFalse &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
'Y';
v.add(TToggleMenu(
value: value,
onChanged: (value) {
if (value == null) return;
bind.sessionSetDisplaysAsIndividualWindows(
sessionId: sessionId, value: value ? 'Y' : '');
},
child: Text(translate('Show displays as individual windows'))));
}
return v; return v;
} }

View File

@ -4,10 +4,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
const bool kOpenSamePeerInNewWindow = true;
const double kDesktopRemoteTabBarHeight = 28.0; const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1; const int kInvalidWindowId = -1;
const int kMainWindowId = 0; const int kMainWindowId = 0;
const kAllDisplayValue = -1;
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
@ -37,11 +41,13 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kWindowEventOpenMonitorSession = "open_monitor_session";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
@ -60,6 +66,9 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch"; const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse"; const String kPointerEventKindMouse = "mouse";
const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";

View File

@ -187,12 +187,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
? Theme.of(context).scaffoldBackgroundColor ? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
child: Tooltip( child: Tooltip(
message: translate('Settings'), message: translate('Settings'),
child: Icon( child: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
size: 20, size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5), color: hover.value ? textColor : textColor?.withOpacity(0.5),
)), )),
), ),
), ),
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
@ -256,27 +256,27 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: Obx(() => RotatedBox( child: Obx(() => RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: Tooltip( child: Tooltip(
message: translate('Refresh Password'), message: translate('Refresh Password'),
child: Icon( child: Icon(
Icons.refresh, Icons.refresh,
color: refreshHover.value color: refreshHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)) )))),
)),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
InkWell( InkWell(
child: Obx( child: Obx(
() => Tooltip( () => Tooltip(
message: translate('Change Password'), message: translate('Change Password'),
child: Icon( child: Icon(
Icons.edit, Icons.edit,
color: color: editHover.value
editHover.value ? textColor : Color(0xFFDDDDDD), ? textColor
size: 22, : Color(0xFFDDDDDD),
)).marginOnly(right: 8, top: 4), size: 22,
)).marginOnly(right: 8, top: 4),
), ),
onTap: () => DesktopSettingPage.switch2page(1), onTap: () => DesktopSettingPage.switch2page(1),
onHover: (value) => editHover.value = value, onHover: (value) => editHover.value = value,
@ -604,8 +604,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
debugPrint("Failed to parse window id '${call.arguments}': $e"); debugPrint("Failed to parse window id '${call.arguments}': $e");
} }
if (windowId != null) { if (windowId != null) {
await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]); await rustDeskWinManager.moveTabToNewWindow(
windowId, args[1], args[2]);
} }
} else if (call.method == kWindowEventOpenMonitorSession) {
final args = jsonDecode(call.arguments);
final windowId = args['window_id'] as int;
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount);
} }
}); });
_uniLinksSubscription = listenUniLinks(); _uniLinksSubscription = listenUniLinks();

View File

@ -323,24 +323,7 @@ class _GeneralState extends State<_General> {
'enable-confirm-closing-tabs', 'enable-confirm-closing-tabs',
isServer: false), isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'), _OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
if (Platform.isWindows || Platform.isLinux) wallpaper(),
Row(
children: [
Flexible(
child: _OptionCheckBox(
context,
'Remove wallpaper during incoming sessions',
'allow-remove-wallpaper'),
),
_CountDownButton(
text: 'Test',
second: 5,
onPressed: () {
bind.mainTestWallpaper(second: 5);
},
)
],
),
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@ -367,6 +350,42 @@ class _GeneralState extends State<_General> {
return _Card(title: 'Other', children: children); return _Card(title: 'Other', children: children);
} }
Widget wallpaper() {
return futureBuilder(future: () async {
final support = await bind.mainSupportRemoveWallpaper();
return support;
}(), hasData: (data) {
if (data is bool && data == true) {
final option = 'allow-remove-wallpaper';
bool value = mainGetBoolOptionSync(option);
return Row(
children: [
Flexible(
child: _OptionCheckBox(
context,
'Remove wallpaper during incoming sessions',
option,
update: () {
setState(() {});
},
),
),
if (value)
_CountDownButton(
text: 'Test',
second: 5,
onPressed: () {
bind.mainTestWallpaper(second: 5);
},
)
],
);
}
return Offstage();
});
}
Widget hwcodec() { Widget hwcodec() {
return Offstage( return Offstage(
offstage: !bind.mainHasHwcodec(), offstage: !bind.mainHasHwcodec(),
@ -1289,7 +1308,7 @@ class _DisplayState extends State<_Display> {
Widget other(BuildContext context) { Widget other(BuildContext context) {
return _Card(title: 'Other Default Options', children: [ return _Card(title: 'Other Default Options', children: [
otherRow('View Mode', 'view_only'), otherRow('View Mode', 'view_only'),
otherRow('show_monitors_tip', 'show_monitors_toolbar'), otherRow('show_monitors_tip', kKeyShowMonitorsToolbar),
otherRow('Collapse toolbar', 'collapse_toolbar'), otherRow('Collapse toolbar', 'collapse_toolbar'),
otherRow('Show remote cursor', 'show_remote_cursor'), otherRow('Show remote cursor', 'show_remote_cursor'),
otherRow('Zoom cursor', 'zoom-cursor'), otherRow('Zoom cursor', 'zoom-cursor'),
@ -1300,6 +1319,8 @@ class _DisplayState extends State<_Display> {
otherRow('Lock after session end', 'lock_after_session_end'), otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'), otherRow('Privacy mode', 'privacy_mode'),
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'), otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
otherRow('Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows),
]); ]);
} }
} }

View File

@ -28,6 +28,7 @@ import '../widgets/tabbar_widget.dart';
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false); final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
// Used to skip session close if "move to new window" is clicked.
final Map<String, bool> closeSessionOnDispose = {}; final Map<String, bool> closeSessionOnDispose = {};
class RemotePage extends StatefulWidget { class RemotePage extends StatefulWidget {
@ -36,6 +37,8 @@ class RemotePage extends StatefulWidget {
required this.id, required this.id,
required this.sessionId, required this.sessionId,
required this.tabWindowId, required this.tabWindowId,
required this.display,
required this.displays,
required this.password, required this.password,
required this.toolbarState, required this.toolbarState,
required this.tabController, required this.tabController,
@ -46,6 +49,8 @@ class RemotePage extends StatefulWidget {
final String id; final String id;
final SessionID? sessionId; final SessionID? sessionId;
final int? tabWindowId; final int? tabWindowId;
final int? display;
final List<int>? displays;
final String? password; final String? password;
final ToolbarState toolbarState; final ToolbarState toolbarState;
final String? switchUuid; final String? switchUuid;
@ -73,7 +78,7 @@ class _RemotePageState extends State<RemotePage>
late RxBool _zoomCursor; late RxBool _zoomCursor;
late RxBool _remoteCursorMoved; late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled; late RxBool _keyboardEnabled;
late RenderTexture _renderTexture; final Map<int, RenderTexture> _renderTextures = {};
final _blockableOverlayState = BlockableOverlayState(); final _blockableOverlayState = BlockableOverlayState();
@ -109,6 +114,8 @@ class _RemotePageState extends State<RemotePage>
switchUuid: widget.switchUuid, switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId, tabWindowId: widget.tabWindowId,
display: widget.display,
displays: widget.displays,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@ -118,9 +125,6 @@ class _RemotePageState extends State<RemotePage>
if (!Platform.isLinux) { if (!Platform.isLinux) {
Wakelock.enable(); Wakelock.enable();
} }
// Register texture.
_renderTexture = RenderTexture();
_renderTexture.create(sessionId);
_ffi.ffiModel.updateEventListener(sessionId, widget.id); _ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
@ -207,7 +211,9 @@ class _RemotePageState extends State<RemotePage>
// https://github.com/flutter/flutter/issues/64935 // https://github.com/flutter/flutter/issues/64935
super.dispose(); super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
await _renderTexture.destroy(closeSession); for (final texture in _renderTextures.values) {
await texture.destroy(closeSession);
}
// ensure we leave this session, this is a double check // ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false); _ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
@ -245,6 +251,7 @@ class _RemotePageState extends State<RemotePage>
onEnterOrLeaveImageSetter: (func) => onEnterOrLeaveImageSetter: (func) =>
_onEnterOrLeaveImage4Toolbar = func, _onEnterOrLeaveImage4Toolbar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
setRemoteState: setState,
); );
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
@ -392,6 +399,38 @@ class _RemotePageState extends State<RemotePage>
); );
} }
Map<int, RenderTexture> _updateGetRenderTextures(int curDisplay) {
tryCreateTexture(int idx) {
if (!_renderTextures.containsKey(idx)) {
final renderTexture = RenderTexture();
_renderTextures[idx] = renderTexture;
renderTexture.create(idx, sessionId);
}
}
tryRemoveTexture(int idx) {
if (_renderTextures.containsKey(idx)) {
_renderTextures[idx]!.destroy(true);
_renderTextures.remove(idx);
}
}
if (curDisplay == kAllDisplayValue) {
final displays = _ffi.ffiModel.pi.getCurDisplays();
for (var i = 0; i < displays.length; i++) {
tryCreateTexture(i);
}
} else {
tryCreateTexture(curDisplay);
for (var i = 0; i < _ffi.ffiModel.pi.displays.length; i++) {
if (i != curDisplay) {
tryRemoveTexture(i);
}
}
}
return _renderTextures;
}
Widget getBodyForDesktop(BuildContext context) { Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[ var paints = <Widget>[
MouseRegion(onEnter: (evt) { MouseRegion(onEnter: (evt) {
@ -402,16 +441,20 @@ class _RemotePageState extends State<RemotePage>
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
Provider.of<CanvasModel>(context, listen: false).updateViewStyle(); Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
}); });
return ImagePaint( final peerDisplay = CurrentDisplayState.find(widget.id);
id: widget.id, return Obx(
zoomCursor: _zoomCursor, () => _ffi.ffiModel.pi.isSet.isFalse
cursorOverImage: _cursorOverImage, ? Container(color: Colors.transparent)
keyboardEnabled: _keyboardEnabled, : Obx(() => ImagePaint(
remoteCursorMoved: _remoteCursorMoved, id: widget.id,
textureId: _renderTexture.textureId, zoomCursor: _zoomCursor,
useTextureRender: RenderTexture.useTextureRender, cursorOverImage: _cursorOverImage,
listenerBuilder: (child) => keyboardEnabled: _keyboardEnabled,
_buildRawTouchAndPointerRegion(child, enterView, leaveView), remoteCursorMoved: _remoteCursorMoved,
renderTextures: _updateGetRenderTextures(peerDisplay.value),
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
child, enterView, leaveView),
)),
); );
})) }))
]; ];
@ -447,8 +490,7 @@ class ImagePaint extends StatefulWidget {
final RxBool cursorOverImage; final RxBool cursorOverImage;
final RxBool keyboardEnabled; final RxBool keyboardEnabled;
final RxBool remoteCursorMoved; final RxBool remoteCursorMoved;
final RxInt textureId; final Map<int, RenderTexture> renderTextures;
final bool useTextureRender;
final Widget Function(Widget)? listenerBuilder; final Widget Function(Widget)? listenerBuilder;
ImagePaint( ImagePaint(
@ -458,8 +500,7 @@ class ImagePaint extends StatefulWidget {
required this.cursorOverImage, required this.cursorOverImage,
required this.keyboardEnabled, required this.keyboardEnabled,
required this.remoteCursorMoved, required this.remoteCursorMoved,
required this.textureId, required this.renderTextures,
required this.useTextureRender,
this.listenerBuilder}) this.listenerBuilder})
: super(key: key); : super(key: key);
@ -530,27 +571,13 @@ class _ImagePaintState extends State<ImagePaint> {
}); });
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s; final paintWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s; final paintHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight); final paintSize = Size(paintWidth, paintHeight);
late final Widget imageWidget; final paintWidget = useTextureRender
if (widget.useTextureRender) { ? _BuildPaintTextureRender(
imageWidget = SizedBox( c, s, Offset.zero, paintSize, isViewOriginal())
width: imageWidth, : _buildScrollbarNonTextureRender(m, paintSize, s);
height: imageHeight,
child: Obx(() => Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
)),
);
} else {
imageWidget = CustomPaint(
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
}
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
final percentX = _horizontal.hasClients final percentX = _horizontal.hasClients
@ -570,43 +597,79 @@ class _ImagePaintState extends State<ImagePaint> {
}, },
child: mouseRegion( child: mouseRegion(
child: Obx(() => _buildCrossScrollbarFromLayout( child: Obx(() => _buildCrossScrollbarFromLayout(
context, _buildListener(imageWidget), c.size, imageSize)), context, _buildListener(paintWidget), c.size, paintSize)),
)); ));
} else { } else {
late final Widget imageWidget;
if (c.size.width > 0 && c.size.height > 0) { if (c.size.width > 0 && c.size.height > 0) {
if (widget.useTextureRender) { final paintWidget = useTextureRender
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x; ? _BuildPaintTextureRender(
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y; c,
imageWidget = Stack( s,
children: [ Offset(
Positioned( Platform.isLinux ? c.x.toInt().toDouble() : c.x,
left: x, Platform.isLinux ? c.y.toInt().toDouble() : c.y,
top: y,
width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s,
child: Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
), ),
) c.size,
], isViewOriginal())
); : _buildScrollAuthNonTextureRender(m, c, s);
} else { return mouseRegion(child: _buildListener(paintWidget));
imageWidget = CustomPaint(
size: Size(c.size.width, c.size.height),
painter:
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
);
}
return mouseRegion(child: _buildListener(imageWidget));
} else { } else {
return Container(); return Container();
} }
} }
} }
Widget _buildScrollbarNonTextureRender(
ImageModel m, Size imageSize, double s) {
return CustomPaint(
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
}
Widget _buildScrollAuthNonTextureRender(
ImageModel m, CanvasModel c, double s) {
return CustomPaint(
size: Size(c.size.width, c.size.height),
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
);
}
Widget _BuildPaintTextureRender(
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
final ffiModel = c.parent.target!.ffiModel;
final displays = ffiModel.pi.getCurDisplays();
final children = <Widget>[];
final rect = ffiModel.rect;
if (rect == null) {
return Container();
}
final curDisplay = ffiModel.pi.currentDisplay;
for (var i = 0; i < displays.length; i++) {
final textureId = widget
.renderTextures[curDisplay == kAllDisplayValue ? i : curDisplay]
?.textureId;
if (textureId != null) {
children.add(Positioned(
left: (displays[i].x - rect.left) * s + offset.dx,
top: (displays[i].y - rect.top) * s + offset.dy,
width: displays[i].width * s,
height: displays[i].height * s,
child: Obx(() => Texture(
textureId: textureId.value,
filterQuality:
isViewOriginal ? FilterQuality.none : FilterQuality.low,
)),
));
}
}
return SizedBox(
width: size.width,
height: size.height,
child: Stack(children: children),
);
}
MouseCursor _buildCursorOfCache( MouseCursor _buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) { CursorModel cursor, double scale, CursorData? cache) {
if (cache == null) { if (cache == null) {
@ -731,7 +794,11 @@ class _ImagePaintState extends State<ImagePaint> {
); );
} }
return widget; return Container(
child: widget,
width: layoutSize.width,
height: layoutSize.height,
);
} }
Widget _buildListener(Widget child) { Widget _buildListener(Widget child) {
@ -770,9 +837,14 @@ class CursorPaint extends StatelessWidget {
double cy = c.y; double cy = c.y;
if (c.viewStyle.style == kRemoteViewStyleOriginal && if (c.viewStyle.style == kRemoteViewStyleOriginal &&
c.scrollStyle == ScrollStyle.scrollbar) { c.scrollStyle == ScrollStyle.scrollbar) {
final d = c.parent.target!.ffiModel.display; final rect = c.parent.target!.ffiModel.rect;
final imageWidth = d.width * c.scale; if (rect == null) {
final imageHeight = d.height * c.scale; // unreachable!
debugPrint('unreachable! The displays rect is null.');
return Container();
}
final imageWidth = rect.width * c.scale;
final imageHeight = rect.height * c.scale;
cx = -imageWidth * c.scrollX; cx = -imageWidth * c.scrollX;
cy = -imageHeight * c.scrollY; cy = -imageHeight * c.scrollY;
} }

View File

@ -57,6 +57,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
peerId = params['id']; peerId = params['id'];
final sessionId = params['session_id']; final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id']; final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
if (peerId != null) { if (peerId != null) {
ConnectionTypeState.init(peerId!); ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) { tabController.onSelected = (id) {
@ -80,6 +82,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
id: peerId!, id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId, tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: params['password'], password: params['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@ -109,6 +113,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final switchUuid = args['switch_uuid']; final switchUuid = args['switch_uuid'];
final sessionId = args['session_id']; final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id']; final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
windowOnTop(windowId()); windowOnTop(windowId());
if (tabController.length == 0) { if (tabController.length == 0) {
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) { if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
@ -129,6 +135,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
id: id, id: id,
sessionId: sessionId == null ? null : SessionID(sessionId), sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId, tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'], password: args['password'],
toolbarState: _toolbarState, toolbarState: _toolbarState,
tabController: tabController, tabController: tabController,
@ -148,6 +156,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
windowOnTop(windowId()); windowOnTop(windowId());
} }
return jumpOk; return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) { } else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs return tabController.state.value.tabs
.map((e) => e.key) .map((e) => e.key)
@ -160,18 +177,20 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
.join(';'); .join(';');
} else if (call.method == kWindowEventGetCachedSessionData) { } else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab. // Ready to show new window and close old tab.
final peerId = call.arguments; final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try { try {
final remotePage = tabController.state.value.tabs final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == peerId) .firstWhere((tab) => tab.key == id)
.page as RemotePage; .page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString(); returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) { } catch (e) {
debugPrint('Failed to get cached session data: $e'); debugPrint('Failed to get cached session data: $e');
} }
if (returnValue != null) { if (close && returnValue != null) {
closeSessionOnDispose[peerId] = false; closeSessionOnDispose[id] = false;
tabController.closeBy(peerId); tabController.closeBy(id);
} }
} }
_update_remote_count(); _update_remote_count();

View File

@ -1,10 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@ -325,7 +327,8 @@ class RemoteToolbar extends StatefulWidget {
final FFI ffi; final FFI ffi;
final ToolbarState state; final ToolbarState state;
final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner; final VoidCallback onEnterOrLeaveImageCleaner;
final Function(VoidCallback) setRemoteState;
RemoteToolbar({ RemoteToolbar({
Key? key, Key? key,
@ -334,6 +337,7 @@ class RemoteToolbar extends StatefulWidget {
required this.state, required this.state,
required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageSetter,
required this.onEnterOrLeaveImageCleaner, required this.onEnterOrLeaveImageCleaner,
required this.setRemoteState,
}) : super(key: key); }) : super(key: key);
@override @override
@ -450,13 +454,17 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
} }
if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) { toolbarItems.add(Obx(() {
toolbarItems.add( if (PrivacyModeState.find(widget.id).isFalse &&
bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y' pi.displaysCount.value > 1) {
? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi) return _MonitorMenu(
: _MonitorMenu(id: widget.id, ffi: widget.ffi), id: widget.id,
); ffi: widget.ffi,
} setRemoteState: widget.setRemoteState);
} else {
return Offstage();
}
}));
toolbarItems toolbarItems
.add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
@ -581,11 +589,22 @@ class _MobileActionMenu extends StatelessWidget {
class _MonitorMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
const _MonitorMenu({Key? key, required this.id, required this.ffi}) final Function(VoidCallback) setRemoteState;
: super(key: key); const _MonitorMenu({
Key? key,
required this.id,
required this.ffi,
required this.setRemoteState,
}) : super(key: key);
bool get showMonitorsToolbar =>
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) =>
showMonitorsToolbar ? buildMultiMonitorMenu() : buildMonitorMenu();
Widget buildMonitorMenu() {
return _IconSubmenuButton( return _IconSubmenuButton(
tooltip: 'Select Monitor', tooltip: 'Select Monitor',
icon: icon(), icon: icon(),
@ -595,7 +614,107 @@ class _MonitorMenu extends StatelessWidget {
menuStyle: MenuStyle( menuStyle: MenuStyle(
padding: padding:
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
menuChildren: [Row(children: displays(context))]); menuChildren: [buildMonitorSubmenuWidget()]);
}
Widget buildMultiMonitorMenu() {
return Row(children: buildMonitorList(true));
}
Widget buildMonitorSubmenuWidget() {
final pi = ffi.ffiModel.pi;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: buildMonitorList(false)),
pi.isSupportMultiDisplay ? Divider() : Offstage(),
pi.isSupportMultiDisplay ? chooseDisplayBehavior() : Offstage(),
],
);
}
Widget chooseDisplayBehavior() {
final value =
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
'Y';
return CkbMenuButton(
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionSetDisplaysAsIndividualWindows(
sessionId: ffi.sessionId, value: value ? 'Y' : '');
},
ffi: ffi,
child: Text(translate('Show displays as individual windows')));
}
List<Widget> buildMonitorList(bool isMulti) {
final List<Widget> monitorList = [];
final pi = ffi.ffiModel.pi;
getMonitorText(int i) {
if (i == kAllDisplayValue) {
if (pi.displays.length == 2) {
return '1|2';
} else {
return 'ALL';
}
} else {
return (i + 1).toString();
}
}
buildMonitorButton(int i) => Obx(() {
RxInt display = CurrentDisplayState.find(id);
return _IconMenuButton(
tooltip: isMulti ? '' : '#${i + 1} monitor',
hMargin: isMulti ? null : 6,
vMargin: isMulti ? null : 12,
topLevel: false,
color: i == display.value
? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
hoverColor: i == display.value
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
icon: Container(
alignment: AlignmentDirectional.center,
constraints:
const BoxConstraints(minHeight: _ToolbarTheme.height),
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/screen.svg",
colorFilter:
ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(
() => Text(
getMonitorText(i),
style: TextStyle(
color: i == display.value
? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
onPressed: () => onPressed(i, pi),
);
});
for (int i = 0; i < pi.displays.length; i++) {
monitorList.add(buildMonitorButton(i));
}
if (pi.isSupportMultiUiSession && pi.displays.length > 1) {
monitorList.add(buildMonitorButton(kAllDisplayValue));
}
return monitorList;
} }
icon() { icon() {
@ -610,7 +729,7 @@ class _MonitorMenu extends StatelessWidget {
Obx(() { Obx(() {
RxInt display = CurrentDisplayState.find(id); RxInt display = CurrentDisplayState.find(id);
return Text( return Text(
'${display.value + 1}/${pi.displays.length}', '${display.value == kAllDisplayValue ? 'A' : '${display.value + 1}'}/${pi.displays.length}',
style: const TextStyle( style: const TextStyle(
color: _ToolbarTheme.blueColor, color: _ToolbarTheme.blueColor,
fontSize: 8, fontSize: 8,
@ -622,48 +741,44 @@ class _MonitorMenu extends StatelessWidget {
); );
} }
List<Widget> displays(BuildContext context) { // Open new tab or window to show this monitor.
final List<Widget> rowChildren = []; // For now just open new window.
final pi = ffi.ffiModel.pi; openMonitorInNewTabOrWindow(int i, PeerInfo pi) {
for (int i = 0; i < pi.displays.length; i++) { if (kWindowId == null) {
rowChildren.add(_IconMenuButton( // unreachable
topLevel: false, debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null');
color: _ToolbarTheme.blueColor, return;
hoverColor: _ToolbarTheme.hoverBlueColor, }
tooltip: "#${i + 1} monitor", DesktopMultiWindow.invokeMethod(
hMargin: 6, kMainWindowId,
vMargin: 12, kWindowEventOpenMonitorSession,
icon: Container( jsonEncode({
alignment: AlignmentDirectional.center, 'window_id': kWindowId!,
constraints: const BoxConstraints(minHeight: _ToolbarTheme.height), 'peer_id': ffi.id,
child: Stack( 'display': i,
alignment: Alignment.center, 'display_count': pi.displays.length,
children: [ }));
SvgPicture.asset( }
"assets/screen.svg",
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), openMonitorInTheSameTab(int i, PeerInfo pi) {
), final displays = i == kAllDisplayValue
Text( ? List.generate(pi.displays.length, (index) => index)
(i + 1).toString(), : [i];
style: TextStyle( bind.sessionSwitchDisplay(
color: _ToolbarTheme.blueColor, sessionId: ffi.sessionId, value: Int32List.fromList(displays));
fontSize: 12, ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id);
fontWeight: FontWeight.bold, }
),
), onPressed(int i, PeerInfo pi) {
], _menuDismissCallback(ffi);
), RxInt display = CurrentDisplayState.find(id);
), if (display.value != i) {
onPressed: () { if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
_menuDismissCallback(ffi); openMonitorInNewTabOrWindow(i, pi);
RxInt display = CurrentDisplayState.find(id); } else {
if (display.value != i) { openMonitorInTheSameTab(i, pi);
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i); }
}
},
));
} }
return rowChildren;
} }
} }
@ -1044,14 +1159,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Resolution? _localResolution; Resolution? _localResolution;
late final TextEditingController _customWidth = late final TextEditingController _customWidth =
TextEditingController(text: display.width.toString()); TextEditingController(text: rect?.width.toInt().toString() ?? '');
late final TextEditingController _customHeight = late final TextEditingController _customHeight =
TextEditingController(text: display.height.toString()); TextEditingController(text: rect?.height.toInt().toString() ?? '');
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
PeerInfo get pi => widget.ffi.ffiModel.pi; PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel; FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display; Rect? get rect => ffiModel.rect;
List<Resolution> get resolutions => pi.resolutions; List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland(); bool get isWayland => bind.mainCurrentIsWayland();
@ -1063,12 +1178,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isVirtualDisplay = display.isVirtualDisplayResolution; final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
final visible = final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage(); if (!visible) return Offstage();
final showOriginalBtn = final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution; ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal(); final showFitLocalBtn = !_isRemoteResolutionFitLocal();
_setGroupValue(); _setGroupValue();
return _SubmenuButton( return _SubmenuButton(
@ -1085,12 +1200,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
} }
_setGroupValue() { _setGroupValue() {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
final lastGroupValue = final lastGroupValue =
stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay); stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
if (lastGroupValue == _kCustomResolutionValue) { if (lastGroupValue == _kCustomResolutionValue) {
_groupValue = _kCustomResolutionValue; _groupValue = _kCustomResolutionValue;
} else { } else {
_groupValue = '${display.width}x${display.height}'; _groupValue = '${rect?.width.toInt()}x${rect?.height.toInt()}';
} }
} }
@ -1118,20 +1236,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
_getLocalResolution() { _getLocalResolution() {
_localResolution = null; _localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay(); final String mainDisplay = bind.mainGetMainDisplay();
if (currentDisplay.isNotEmpty) { if (mainDisplay.isNotEmpty) {
try { try {
final display = json.decode(currentDisplay); final display = json.decode(mainDisplay);
if (display['w'] != null && display['h'] != null) { if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']); _localResolution = Resolution(display['w'], display['h']);
} }
} catch (e) { } catch (e) {
debugPrint('Failed to decode $currentDisplay, $e'); debugPrint('Failed to decode $mainDisplay, $e');
} }
} }
} }
_onChanged(BuildContext context, String? value) async { _onChanged(BuildContext context, String? value) async {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
stateGlobal.setLastResolutionGroupValue( stateGlobal.setLastResolutionGroupValue(
widget.id, pi.currentDisplay, value); widget.id, pi.currentDisplay, value);
if (value == null) return; if (value == null) return;
@ -1150,13 +1271,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
} }
if (w != null && h != null) { if (w != null && h != null) {
if (w != display.width || h != display.height) { if (w != rect?.width.toInt() || h != rect?.height.toInt()) {
await _changeResolution(context, w, h); await _changeResolution(context, w, h);
} }
} }
} }
_changeResolution(BuildContext context, int w, int h) async { _changeResolution(BuildContext context, int w, int h) async {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
await bind.sessionChangeResolution( await bind.sessionChangeResolution(
sessionId: ffi.sessionId, sessionId: ffi.sessionId,
display: pi.currentDisplay, display: pi.currentDisplay,
@ -1164,8 +1288,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
height: h, height: h,
); );
Future.delayed(Duration(seconds: 3), () async { Future.delayed(Duration(seconds: 3), () async {
final display = ffiModel.display; final rect = ffiModel.rect;
if (w == display.width && h == display.height) { if (rect == null) {
return;
}
if (w == rect.width.toInt() && h == rect.height.toInt()) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) { if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context); widget.screenAdjustor.doAdjustWindow(context);
} }
@ -1175,6 +1302,10 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Widget _OriginalResolutionMenuButton( Widget _OriginalResolutionMenuButton(
BuildContext context, bool showOriginalBtn) { BuildContext context, bool showOriginalBtn) {
final display = pi.tryGetDisplayIfNotAllDisplay();
if (display == null) {
return Offstage();
}
return Offstage( return Offstage(
offstage: !showOriginalBtn, offstage: !showOriginalBtn,
child: MenuButton( child: MenuButton(
@ -1262,7 +1393,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
return null; return null;
} }
if (display.isVirtualDisplayResolution) { if (ffiModel.isVirtualDisplayResolution) {
return _localResolution!; return _localResolution!;
} }
@ -1284,8 +1415,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
if (bestFitResolution == null) { if (bestFitResolution == null) {
return true; return true;
} }
return bestFitResolution.width == display.width && return bestFitResolution.width == rect?.width.toInt() &&
bestFitResolution.height == display.height; bestFitResolution.height == rect?.height.toInt();
} }
} }
@ -1361,7 +1492,7 @@ class _KeyboardMenu extends StatelessWidget {
continue; continue;
} }
if (pi.is_wayland && mode.key != _kKeyMapMode) { if (pi.isWayland && mode.key != _kKeyMapMode) {
continue; continue;
} }
@ -1404,7 +1535,7 @@ class _KeyboardMenu extends StatelessWidget {
viewMode() { viewMode() {
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; final enabled = versionCmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
return CkbMenuButton( return CkbMenuButton(
value: ffiModel.viewOnly, value: ffiModel.viewOnly,
onChanged: enabled onChanged: enabled
@ -2037,71 +2168,3 @@ Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
), ),
); );
} }
class _MultiMonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
const _MultiMonitorMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final List<Widget> rowChildren = [];
final pi = ffi.ffiModel.pi;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(
Obx(() {
RxInt display = CurrentDisplayState.find(id);
return _IconMenuButton(
tooltip: "",
topLevel: false,
color: i == display.value
? _ToolbarTheme.blueColor
: Colors.grey[800]!,
hoverColor: i == display.value
? _ToolbarTheme.hoverBlueColor
: Colors.grey[850]!,
icon: Container(
alignment: AlignmentDirectional.center,
constraints:
const BoxConstraints(minHeight: _ToolbarTheme.height),
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/screen.svg",
colorFilter:
ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(
() => Text(
(i + 1).toString(),
style: TextStyle(
color: i == display.value
? _ToolbarTheme.blueColor
: Colors.grey[800]!,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
onPressed: () {
if (display.value != i) {
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
}
},
);
}),
);
}
return Row(children: rowChildren);
}
}

View File

@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@ -176,6 +177,19 @@ class DesktopTabController {
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
callOnSelected: callOnSelected); callOnSelected: callOnSelected);
bool jumpToByKeyAndDisplay(String key, int display) {
for (int i = 0; i < state.value.tabs.length; i++) {
final tab = state.value.tabs[i];
if (tab.key == key) {
final ffi = (tab.page as RemotePage).ffi;
if (ffi.ffiModel.pi.currentDisplay == display) {
return jumpTo(i, callOnSelected: true);
}
}
}
return false;
}
void closeBy(String? key) { void closeBy(String? key) {
if (!isDesktop) return; if (!isDesktop) return;
assert(onRemoved != null); assert(onRemoved != null);

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -755,14 +756,14 @@ void showOptions(
if (image != null) { if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
} }
if (pi.displays.length > 1) { if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
final cur = pi.currentDisplay; final cur = pi.currentDisplay;
final children = <Widget>[]; final children = <Widget>[];
for (var i = 0; i < pi.displays.length; ++i) { for (var i = 0; i < pi.displays.length; ++i) {
children.add(InkWell( children.add(InkWell(
onTap: () { onTap: () {
if (i == cur) return; if (i == cur) return;
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: i); bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i]));
gFFI.dialogManager.dismissAll(); gFFI.dialogManager.dismissAll();
}, },
child: Ink( child: Ink(

View File

@ -4,25 +4,30 @@ import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import '../../common.dart'; import '../../common.dart';
import './platform_model.dart'; import './platform_model.dart';
final useTextureRender = bind.mainUseTextureRender();
class RenderTexture { class RenderTexture {
final RxInt textureId = RxInt(-1); final RxInt textureId = RxInt(-1);
int _textureKey = -1; int _textureKey = -1;
int _display = 0;
SessionID? _sessionId; SessionID? _sessionId;
static final useTextureRender = bind.mainUseTextureRender();
final textureRenderer = TextureRgbaRenderer(); final textureRenderer = TextureRgbaRenderer();
RenderTexture(); RenderTexture();
create(SessionID sessionId) { int get display => _display;
create(int d, SessionID sessionId) {
if (useTextureRender) { if (useTextureRender) {
_display = d;
_textureKey = bind.getNextTextureKey(); _textureKey = bind.getNextTextureKey();
_sessionId = sessionId; _sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async { textureRenderer.createTexture(_textureKey).then((id) async {
if (id != -1) { if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey); final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr); platformFFI.registerTexture(sessionId, display, ptr);
textureId.value = id; textureId.value = id;
} }
}); });
@ -32,7 +37,7 @@ class RenderTexture {
destroy(bool unregisterTexture) async { destroy(bool unregisterTexture) async {
if (useTextureRender && _textureKey != -1 && _sessionId != null) { if (useTextureRender && _textureKey != -1 && _sessionId != null) {
if (unregisterTexture) { if (unregisterTexture) {
platformFFI.registerTexture(_sessionId!, 0); platformFFI.registerTexture(_sessionId!, display, 0);
} }
await textureRenderer.closeTexture(_textureKey); await textureRenderer.closeTexture(_textureKey);
_textureKey = -1; _textureKey = -1;

View File

@ -552,22 +552,22 @@ class InputModel {
return v; return v;
} }
Offset setNearestEdge(double x, double y, Display d) { Offset setNearestEdge(double x, double y, Rect rect) {
double left = x - d.x; double left = x - rect.left;
double right = d.x + d.width - 1 - x; double right = rect.right - 1 - x;
double top = y - d.y; double top = y - rect.top;
double bottom = d.y + d.height - 1 - y; double bottom = rect.bottom - 1 - y;
if (left < right && left < top && left < bottom) { if (left < right && left < top && left < bottom) {
x = d.x; x = rect.left;
} }
if (right < left && right < top && right < bottom) { if (right < left && right < top && right < bottom) {
x = d.x + d.width - 1; x = rect.right - 1;
} }
if (top < left && top < right && top < bottom) { if (top < left && top < right && top < bottom) {
y = d.y; y = rect.top;
} }
if (bottom < left && bottom < right && bottom < top) { if (bottom < left && bottom < right && bottom < top) {
y = d.y + d.height - 1; y = rect.bottom - 1;
} }
return Offset(x, y); return Offset(x, y);
} }
@ -711,9 +711,12 @@ class InputModel {
final nearThr = 3; final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr; var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr; var nearBottom = (canvasModel.size.height - y) < nearThr;
final d = ffiModel.display; final rect = ffiModel.rect;
final imageWidth = d.width * canvasModel.scale; if (rect == null) {
final imageHeight = d.height * canvasModel.scale; return null;
}
final imageWidth = rect.width * canvasModel.scale;
final imageHeight = rect.height * canvasModel.scale;
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
x += imageWidth * canvasModel.scrollX; x += imageWidth * canvasModel.scrollX;
y += imageHeight * canvasModel.scrollY; y += imageHeight * canvasModel.scrollY;
@ -741,11 +744,11 @@ class InputModel {
y += step; y += step;
} }
} }
x += d.x; x += rect.left;
y += d.y; y += rect.top;
if (onExit) { if (onExit) {
final pos = setNearestEdge(x, y, d); final pos = setNearestEdge(x, y, rect);
x = pos.dx; x = pos.dx;
y = pos.dy; y = pos.dy;
} }
@ -761,10 +764,10 @@ class InputModel {
return null; return null;
} }
int minX = d.x.toInt(); int minX = rect.left.toInt();
int maxX = (d.x + d.width).toInt() - 1; int maxX = (rect.left + rect.width).toInt() - 1;
int minY = d.y.toInt(); int minY = rect.top.toInt();
int maxY = (d.y + d.height).toInt() - 1; int maxY = (rect.top + rect.height).toInt() - 1;
evtX = trySetNearestRange(evtX, minX, maxX, 5); evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5); evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (kind == kPointerEventKindMouse) { if (kind == kPointerEventKindMouse) {

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
@ -86,7 +87,7 @@ class CachedPeerData {
class FfiModel with ChangeNotifier { class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData(); CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo(); PeerInfo _pi = PeerInfo();
Display _display = Display(); Rect? _rect;
var _inputBlocked = false; var _inputBlocked = false;
final _permissions = <String, bool>{}; final _permissions = <String, bool>{};
@ -103,9 +104,15 @@ class FfiModel with ChangeNotifier {
Timer? waitForImageTimer; Timer? waitForImageTimer;
RxBool waitForFirstImage = true.obs; RxBool waitForFirstImage = true.obs;
Map<String, bool> get permissions => _permissions; Rect? get rect => _rect;
bool get isOriginalResolutionSet =>
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false;
bool get isVirtualDisplayResolution =>
_pi.tryGetDisplayIfNotAllDisplay()?.isVirtualDisplayResolution ?? false;
bool get isOriginalResolution =>
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
Display get display => _display; Map<String, bool> get permissions => _permissions;
bool? get secure => _secure; bool? get secure => _secure;
@ -130,6 +137,24 @@ class FfiModel with ChangeNotifier {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
} }
Rect? displaysRect() {
final displays = _pi.getCurDisplays();
if (displays.isEmpty) {
return null;
}
double l = displays[0].x;
double t = displays[0].y;
double r = displays[0].x + displays[0].width;
double b = displays[0].y + displays[0].height;
for (var display in displays.sublist(1)) {
l = min(l, display.x);
t = min(t, display.y);
r = max(r, display.x + display.width);
b = max(b, display.y + display.height);
}
return Rect.fromLTRB(l, t, r, b);
}
toggleTouchMode() { toggleTouchMode() {
if (!isPeerAndroid) { if (!isPeerAndroid) {
_touchMode = !_touchMode; _touchMode = !_touchMode;
@ -154,7 +179,6 @@ class FfiModel with ChangeNotifier {
clear() { clear() {
_pi = PeerInfo(); _pi = PeerInfo();
_display = Display();
_secure = null; _secure = null;
_direct = null; _direct = null;
_inputBlocked = false; _inputBlocked = false;
@ -207,8 +231,10 @@ class FfiModel with ChangeNotifier {
updateLastCursorId(element); updateLastCursorId(element);
await handleCursorData(element); await handleCursorData(element);
} }
updateLastCursorId(data.lastCursorId); if (data.lastCursorId.isNotEmpty) {
handleCursorId(data.lastCursorId); updateLastCursorId(data.lastCursorId);
handleCursorId(data.lastCursorId);
}
} }
// todo: why called by two position // todo: why called by two position
@ -220,11 +246,12 @@ class FfiModel with ChangeNotifier {
} else if (name == 'peer_info') { } else if (name == 'peer_info') {
handlePeerInfo(evt, peerId); handlePeerInfo(evt, peerId);
} else if (name == 'sync_peer_info') { } else if (name == 'sync_peer_info') {
handleSyncPeerInfo(evt, sessionId); handleSyncPeerInfo(evt, sessionId, peerId);
} else if (name == 'connection_ready') { } else if (name == 'connection_ready') {
setConnectionType( setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true'); peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') { } else if (name == 'switch_display') {
// switch display is kept for backward compatibility
handleSwitchDisplay(evt, sessionId, peerId); handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') { } else if (name == 'cursor_data') {
updateLastCursorId(evt); updateLastCursorId(evt);
@ -279,7 +306,7 @@ class FfiModel with ChangeNotifier {
await bind.sessionSwitchSides(sessionId: sessionId); await bind.sessionSwitchSides(sessionId: sessionId);
closeConnection(id: peer_id); closeConnection(id: peer_id);
} else if (name == 'portable_service_running') { } else if (name == 'portable_service_running') {
parent.target?.elevationModel.onPortableServiceRunning(evt); _handlePortableServiceRunning(peerId, evt);
} else if (name == 'on_url_scheme_received') { } else if (name == 'on_url_scheme_received') {
// currently comes from "_url" ipc of mac and dbus of linux // currently comes from "_url" ipc of mac and dbus of linux
onUrlSchemeReceived(evt); onUrlSchemeReceived(evt);
@ -354,20 +381,65 @@ class FfiModel with ChangeNotifier {
platformFFI.setEventCallback(startEventListener(sessionId, peerId)); platformFFI.setEventCallback(startEventListener(sessionId, peerId));
} }
_updateCurDisplay(SessionID sessionId, Display newDisplay) { _handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
if (newDisplay != _display) { final running = evt['running'] == 'true';
if (newDisplay.x != _display.x || newDisplay.y != _display.y) { parent.target?.elevationModel.onPortableServiceRunning(running);
parent.target?.cursorModel if (running) {
.updateDisplayOrigin(newDisplay.x, newDisplay.y); if (pi.primaryDisplay != kInvalidDisplayIndex) {
if (pi.currentDisplay != pi.primaryDisplay) {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'elevated_switch_display_msg', '', parent.target!.dialogManager);
bind.sessionSwitchDisplay(
sessionId: sessionId,
value: Int32List.fromList([pi.primaryDisplay]));
}
} }
_display = newDisplay; }
}
handleAliasChanged(Map<String, dynamic> evt) {
if (!isDesktop) return;
final String peerId = evt['id'];
final String alias = evt['alias'];
String label = getDesktopTabLabel(peerId, alias);
final rxTabLabel = PeerStringOption.find(evt['id'], 'tabLabel');
if (rxTabLabel.value != label) {
rxTabLabel.value = label;
}
}
updateCurDisplay(SessionID sessionId) {
final newRect = displaysRect();
if (newRect == null) {
return;
}
if (newRect != _rect) {
_rect = newRect;
if (newRect.left != _rect?.left || newRect.top != _rect?.top) {
parent.target?.cursorModel
.updateDisplayOrigin(newRect.left, newRect.top);
}
parent.target?.canvasModel.updateViewStyle();
_updateSessionWidthHeight(sessionId); _updateSessionWidthHeight(sessionId);
} }
} }
handleSwitchDisplay( handleSwitchDisplay(
Map<String, dynamic> evt, SessionID sessionId, String peerId) { Map<String, dynamic> evt, SessionID sessionId, String peerId) {
_pi.currentDisplay = int.parse(evt['display']); final curDisplay = int.parse(evt['display']);
// The message should be handled by the another UI session.
if (isChooseDisplayToOpenInNewWindow(_pi, sessionId)) {
if (curDisplay != _pi.currentDisplay) {
return;
}
}
if (_pi.currentDisplay != kAllDisplayValue) {
_pi.currentDisplay = curDisplay;
}
var newDisplay = Display(); var newDisplay = Display();
newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x; newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x;
newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y; newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y;
@ -378,11 +450,11 @@ class FfiModel with ChangeNotifier {
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
newDisplay.originalHeight = newDisplay.originalHeight =
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
_pi.displays[curDisplay] = newDisplay;
_updateCurDisplay(sessionId, newDisplay); updateCurDisplay(sessionId);
try { try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay; CurrentDisplayState.find(peerId).value = curDisplay;
} catch (e) { } catch (e) {
// //
} }
@ -522,13 +594,30 @@ class FfiModel with ChangeNotifier {
} }
_updateSessionWidthHeight(SessionID sessionId) { _updateSessionWidthHeight(SessionID sessionId) {
parent.target?.canvasModel.updateViewStyle(); if (_rect == null) return;
if (display.width <= 0 || display.height <= 0) { if (_rect!.width <= 0 || _rect!.height <= 0) {
debugPrintStack( debugPrintStack(
label: 'invalid display size (${display.width},${display.height})'); label: 'invalid display size (${_rect!.width},${_rect!.height})');
} else { } else {
bind.sessionSetSize( final displays = _pi.getCurDisplays();
sessionId: sessionId, width: display.width, height: display.height); if (displays.length == 1) {
bind.sessionSetSize(
sessionId: sessionId,
display:
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
width: _rect!.width.toInt(),
height: _rect!.height.toInt(),
);
} else {
for (int i = 0; i < displays.length; ++i) {
bind.sessionSetSize(
sessionId: sessionId,
display: i,
width: displays[i].width.toInt(),
height: displays[i].height.toInt(),
);
}
}
} }
} }
@ -541,11 +630,20 @@ class FfiModel with ChangeNotifier {
parent.target?.dialogManager.dismissAll(); parent.target?.dialogManager.dismissAll();
_pi.version = evt['version']; _pi.version = evt['version'];
_pi.isSupportMultiUiSession =
bind.isSupportMultiUiSession(version: _pi.version);
_pi.username = evt['username']; _pi.username = evt['username'];
_pi.hostname = evt['hostname']; _pi.hostname = evt['hostname'];
_pi.platform = evt['platform']; _pi.platform = evt['platform'];
_pi.sasEnabled = evt['sas_enabled'] == 'true'; _pi.sasEnabled = evt['sas_enabled'] == 'true';
_pi.currentDisplay = int.parse(evt['current_display']); final currentDisplay = int.parse(evt['current_display']);
if (_pi.primaryDisplay == kInvalidDisplayIndex) {
_pi.primaryDisplay = currentDisplay;
}
if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) {
_pi.currentDisplay = currentDisplay;
}
try { try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay; CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
@ -569,10 +667,10 @@ class FfiModel with ChangeNotifier {
for (int i = 0; i < displays.length; ++i) { for (int i = 0; i < displays.length; ++i) {
_pi.displays.add(evtToDisplay(displays[i])); _pi.displays.add(evtToDisplay(displays[i]));
} }
stateGlobal.displaysCount.value = _pi.displays.length; _pi.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay < _pi.displays.length) { if (_pi.currentDisplay < _pi.displays.length) {
_display = _pi.displays[_pi.currentDisplay]; // now replaced to _updateCurDisplay
_updateSessionWidthHeight(sessionId); updateCurDisplay(sessionId);
} }
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
_reconnects = 1; _reconnects = 1;
@ -590,12 +688,12 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: 'view-only')); sessionId: sessionId, arg: 'view-only'));
} }
if (connType == ConnType.defaultConn) { if (connType == ConnType.defaultConn) {
final platform_additions = evt['platform_additions']; final platformDdditions = evt['platform_additions'];
if (platform_additions != null && platform_additions != '') { if (platformDdditions != null && platformDdditions != '') {
try { try {
_pi.platform_additions = json.decode(platform_additions); _pi.platformDdditions = json.decode(platformDdditions);
} catch (e) { } catch (e) {
debugPrint('Failed to decode platform_additions $e'); debugPrint('Failed to decode platformDdditions $e');
} }
} }
} }
@ -670,7 +768,8 @@ class FfiModel with ChangeNotifier {
} }
/// Handle the peer info synchronization event based on [evt]. /// Handle the peer info synchronization event based on [evt].
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async { handleSyncPeerInfo(
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
if (evt['displays'] != null) { if (evt['displays'] != null) {
cachedPeerData.peerInfo['displays'] = evt['displays']; cachedPeerData.peerInfo['displays'] = evt['displays'];
List<dynamic> displays = json.decode(evt['displays']); List<dynamic> displays = json.decode(evt['displays']);
@ -679,14 +778,54 @@ class FfiModel with ChangeNotifier {
newDisplays.add(evtToDisplay(displays[i])); newDisplays.add(evtToDisplay(displays[i]));
} }
_pi.displays = newDisplays; _pi.displays = newDisplays;
stateGlobal.displaysCount.value = _pi.displays.length; _pi.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { if (_pi.currentDisplay == kAllDisplayValue) {
_updateCurDisplay(sessionId, _pi.displays[_pi.currentDisplay]); updateCurDisplay(sessionId);
// to-do: What if the displays are changed?
} else {
if (_pi.currentDisplay >= 0 &&
_pi.currentDisplay < _pi.displays.length) {
updateCurDisplay(sessionId);
} else {
if (_pi.displays.isNotEmpty) {
// Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
? 0
: pi.primaryDisplay;
final displays = newDisplay;
bind.sessionSwitchDisplay(
sessionId: sessionId, value: Int32List.fromList([displays]));
if (_pi.isSupportMultiUiSession) {
// If the peer supports multi-ui-session, no switch display message will be send back.
// We need to update the display manually.
switchToNewDisplay(newDisplay, sessionId, peerId);
}
} else {
msgBox(sessionId, 'nocancel-error', 'Prompt', 'No Displays', '',
parent.target!.dialogManager);
}
}
} }
} }
notifyListeners(); notifyListeners();
} }
// Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId) {
// no need to wait for the response
pi.currentDisplay = display;
updateCurDisplay(sessionId);
try {
CurrentDisplayState.find(peerId).value = display;
} catch (e) {
//
}
parent.target?.recordingModel.onSwitchDisplay();
}
updateBlockInputState(Map<String, dynamic> evt, String peerId) { updateBlockInputState(Map<String, dynamic> evt, String peerId) {
_inputBlocked = evt['input_state'] == 'on'; _inputBlocked = evt['input_state'] == 'on';
notifyListeners(); notifyListeners();
@ -709,7 +848,7 @@ class FfiModel with ChangeNotifier {
} }
void setViewOnly(String id, bool value) { void setViewOnly(String id, bool value) {
if (version_cmp(_pi.version, '1.2.0') < 0) return; if (versionCmp(_pi.version, '1.2.0') < 0) return;
// tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389 // tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389
// because below rx not used in mobile version, so not initialized, below code will cause crash // because below rx not used in mobile version, so not initialized, below code will cause crash
// current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!! // current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!!
@ -749,16 +888,16 @@ class ImageModel with ChangeNotifier {
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(Uint8List rgba) { onRgba(int display, Uint8List rgba) {
final pid = parent.target?.id; final pid = parent.target?.id;
img.decodeImageFromPixels( img.decodeImageFromPixels(
rgba, rgba,
parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.rect?.width.toInt() ?? 0,
parent.target?.ffiModel.display.height ?? 0, parent.target?.ffiModel.rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () { onPixelsCopied: () {
// Unlock the rgba memory from rust codes. // Unlock the rgba memory from rust codes.
platformFFI.nextRgba(sessionId); platformFFI.nextRgba(sessionId, display);
}).then((image) { }).then((image) {
if (parent.target?.id != pid) return; if (parent.target?.id != pid) return;
try { try {
@ -1017,20 +1156,20 @@ class CanvasModel with ChangeNotifier {
} }
bool get cursorEmbedded => bool get cursorEmbedded =>
parent.target?.ffiModel.display.cursorEmbedded ?? false; parent.target?.ffiModel._pi.cursorEmbedded ?? false;
int getDisplayWidth() { int getDisplayWidth() {
final defaultWidth = (isDesktop || isWebDesktop) final defaultWidth = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayWidth ? kDesktopDefaultDisplayWidth
: kMobileDefaultDisplayWidth; : kMobileDefaultDisplayWidth;
return parent.target?.ffiModel.display.width ?? defaultWidth; return parent.target?.ffiModel.rect?.width.toInt() ?? defaultWidth;
} }
int getDisplayHeight() { int getDisplayHeight() {
final defaultHeight = (isDesktop || isWebDesktop) final defaultHeight = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayHeight ? kDesktopDefaultDisplayHeight
: kMobileDefaultDisplayHeight; : kMobileDefaultDisplayHeight;
return parent.target?.ffiModel.display.height ?? defaultHeight; return parent.target?.ffiModel.rect?.height.toInt() ?? defaultHeight;
} }
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
@ -1619,7 +1758,27 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) { updateQualityStatus(Map<String, dynamic> evt) {
try { try {
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps']; if ((evt['fps'] as String).isNotEmpty) {
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
final pi = parent.target?.ffiModel.pi;
if (pi != null) {
final currentDisplay = pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
final fps2 = fps[currentDisplay.toString()];
if (fps2 != null) {
_data.fps = fps2.toString();
}
} else if (fps.isNotEmpty) {
final fpsList = [];
for (var i = 0; i < pi.displays.length; i++) {
fpsList.add((fps[i.toString()] ?? 0).toString());
}
_data.fps = fpsList.join(' ');
}
} else {
_data.fps = null;
}
}
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
if ((evt['target_bitrate'] as String).isNotEmpty) { if ((evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate']; _data.targetBitrate = evt['target_bitrate'];
@ -1646,8 +1805,15 @@ class RecordingModel with ChangeNotifier {
int? width = parent.target?.canvasModel.getDisplayWidth(); int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayHeight(); int? height = parent.target?.canvasModel.getDisplayHeight();
if (sessionId == null || width == null || height == null) return; if (sessionId == null || width == null || height == null) return;
bind.sessionRecordScreen( final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
sessionId: sessionId, start: true, width: width, height: height); if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: true,
display: currentDisplay!,
width: width,
height: height);
}
} }
toggle() async { toggle() async {
@ -1658,10 +1824,20 @@ class RecordingModel with ChangeNotifier {
notifyListeners(); notifyListeners();
await bind.sessionRecordStatus(sessionId: sessionId, status: _start); await bind.sessionRecordStatus(sessionId: sessionId, status: _start);
if (_start) { if (_start) {
bind.sessionRefresh(sessionId: sessionId); final pi = parent.target?.ffiModel.pi;
if (pi != null) {
sessionRefreshVideo(sessionId, pi);
}
} else { } else {
bind.sessionRecordScreen( final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
sessionId: sessionId, start: false, width: 0, height: 0); if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay!,
width: 0,
height: 0);
}
} }
} }
@ -1670,8 +1846,15 @@ class RecordingModel with ChangeNotifier {
final sessionId = parent.target?.sessionId; final sessionId = parent.target?.sessionId;
if (sessionId == null) return; if (sessionId == null) return;
_start = false; _start = false;
bind.sessionRecordScreen( final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
sessionId: sessionId, start: false, width: 0, height: 0); if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId,
start: false,
display: currentDisplay!,
width: 0,
height: 0);
}
} }
} }
@ -1686,9 +1869,7 @@ class ElevationModel with ChangeNotifier {
_running = false; _running = false;
} }
onPortableServiceRunning(Map<String, dynamic> evt) { onPortableServiceRunning(bool running) => _running = running;
_running = evt['running'] == 'true';
}
} }
enum ConnType { defaultConn, fileTransfer, portForward, rdp } enum ConnType { defaultConn, fileTransfer, portForward, rdp }
@ -1751,14 +1932,18 @@ class FFI {
} }
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
void start(String id, void start(
{bool isFileTransfer = false, String id, {
bool isPortForward = false, bool isFileTransfer = false,
bool isRdp = false, bool isPortForward = false,
String? switchUuid, bool isRdp = false,
String? password, String? switchUuid,
bool? forceRelay, String? password,
int? tabWindowId}) { bool? forceRelay,
int? tabWindowId,
int? display,
List<int>? displays,
}) {
closed = false; closed = false;
auditNote = ''; auditNote = '';
if (isMobile) mobileReset(); if (isMobile) mobileReset();
@ -1788,11 +1973,34 @@ class FFI {
forceRelay: forceRelay ?? false, forceRelay: forceRelay ?? false,
password: password ?? '', password: password ?? '',
); );
} else if (display != null) {
if (displays == null) {
debugPrint(
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
return;
}
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
if (addRes != '') {
debugPrint(
'Unreachable, failed to add existed session to $id, $addRes');
return;
}
bind.sessionTryAddDisplay(
sessionId: sessionId, displays: Int32List.fromList(displays));
ffiModel.pi.currentDisplay = display;
} }
final stream = bind.sessionStart(sessionId: sessionId, id: id); final stream = bind.sessionStart(sessionId: sessionId, id: id);
final cb = ffiModel.startEventListener(sessionId, id); final cb = ffiModel.startEventListener(sessionId, id);
final useTextureRender = bind.mainUseTextureRender(); final useTextureRender = bind.mainUseTextureRender();
// Force refresh displays.
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
if (displays != null) {
for (final display in displays) {
bind.sessionRefresh(sessionId: sessionId, display: display);
}
}
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false); final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
// Preserved for the rgba data. // Preserved for the rgba data.
stream.listen((message) { stream.listen((message) {
@ -1801,8 +2009,9 @@ class FFI {
// Session is read to be moved to a new window. // Session is read to be moved to a new window.
// Get the cached data and handle the cached data. // Get the cached data and handle the cached data.
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
final args = jsonEncode({'id': id, 'close': display == null});
final cachedData = await DesktopMultiWindow.invokeMethod( final cachedData = await DesktopMultiWindow.invokeMethod(
tabWindowId, kWindowEventGetCachedSessionData, id); tabWindowId, kWindowEventGetCachedSessionData, args);
if (cachedData == null) { if (cachedData == null) {
// unreachable // unreachable
debugPrint('Unreachable, the cached data is empty.'); debugPrint('Unreachable, the cached data is empty.');
@ -1814,7 +2023,7 @@ class FFI {
return; return;
} }
await ffiModel.handleCachedPeerData(data, id); await ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId); await sessionRefreshVideo(sessionId, ffiModel.pi);
}); });
isToNewWindowNotified.value = true; isToNewWindowNotified.value = true;
} }
@ -1836,18 +2045,19 @@ class FFI {
await cb(event); await cb(event);
} }
} else if (message is EventToUI_Rgba) { } else if (message is EventToUI_Rgba) {
final display = message.field0;
if (useTextureRender) { if (useTextureRender) {
onEvent2UIRgba(); onEvent2UIRgba();
} else { } else {
// Fetch the image buffer from rust codes. // Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId); final sz = platformFFI.getRgbaSize(sessionId, display);
if (sz == 0) { if (sz == 0) {
return; return;
} }
final rgba = platformFFI.getRgba(sessionId, sz); final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) { if (rgba != null) {
onEvent2UIRgba(); onEvent2UIRgba();
imageModel.onRgba(rgba); imageModel.onRgba(display, rgba);
} }
} }
} }
@ -1979,22 +2189,72 @@ class Features {
bool privacyMode = false; bool privacyMode = false;
} }
const kInvalidDisplayIndex = -1;
class PeerInfo with ChangeNotifier { class PeerInfo with ChangeNotifier {
String version = ''; String version = '';
String username = ''; String username = '';
String hostname = ''; String hostname = '';
String platform = ''; String platform = '';
bool sasEnabled = false; bool sasEnabled = false;
bool isSupportMultiUiSession = false;
int currentDisplay = 0; int currentDisplay = 0;
int primaryDisplay = kInvalidDisplayIndex;
List<Display> displays = []; List<Display> displays = [];
Features features = Features(); Features features = Features();
List<Resolution> resolutions = []; List<Resolution> resolutions = [];
Map<String, dynamic> platform_additions = {}; Map<String, dynamic> platformDdditions = {};
RxInt displaysCount = 0.obs;
RxBool isSet = false.obs; RxBool isSet = false.obs;
bool get is_wayland => platform_additions['is_wayland'] == true; bool get isWayland => platformDdditions['is_wayland'] == true;
bool get is_headless => platform_additions['headless'] == true; bool get isHeadless => platformDdditions['headless'] == true;
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
Display? tryGetDisplay() {
if (displays.isEmpty) {
return null;
}
if (currentDisplay == kAllDisplayValue) {
return displays[0];
} else {
if (currentDisplay > 0 && currentDisplay < displays.length) {
return displays[currentDisplay];
} else {
return displays[0];
}
}
}
Display? tryGetDisplayIfNotAllDisplay() {
if (displays.isEmpty) {
return null;
}
if (currentDisplay == kAllDisplayValue) {
return null;
}
if (currentDisplay > 0 && currentDisplay < displays.length) {
return displays[currentDisplay];
} else {
return null;
}
}
List<Display> getCurDisplays() {
if (currentDisplay == kAllDisplayValue) {
return displays;
} else {
if (currentDisplay >= 0 && currentDisplay < displays.length) {
return [displays[currentDisplay]];
} else {
return [];
}
}
}
} }
const canvasKey = 'canvas'; const canvasKey = 'canvas';
@ -2038,8 +2298,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
currentDisplay = p['currentDisplay']; currentDisplay = p['currentDisplay'];
} }
if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) { if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) {
ffi.cursorModel ffi.cursorModel.updateDisplayOrigin(
.updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y); ffi.ffiModel.rect?.left ?? 0, ffi.ffiModel.rect?.top ?? 0);
return; return;
} }
double xCursor = p['xCursor']; double xCursor = p['xCursor'];
@ -2047,8 +2307,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
double xCanvas = p['xCanvas']; double xCanvas = p['xCanvas'];
double yCanvas = p['yCanvas']; double yCanvas = p['yCanvas'];
double scale = p['scale']; double scale = p['scale'];
ffi.cursorModel.updateDisplayOriginWithCursor( ffi.cursorModel.updateDisplayOriginWithCursor(ffi.ffiModel.rect?.left ?? 0,
ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor); ffi.ffiModel.rect?.top ?? 0, xCursor, yCursor);
ffi.canvasModel.update(xCanvas, yCanvas, scale); ffi.canvasModel.update(xCanvas, yCanvas, scale);
} }

View File

@ -21,7 +21,8 @@ class RgbaFrame extends Struct {
external Pointer<Uint8> data; external Pointer<Uint8> data;
} }
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>); typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>, int);
typedef F3Dart = Pointer<Uint8> Function(Pointer<Utf8>, Int32);
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt); typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
/// FFI wrapper around the native Rust core. /// FFI wrapper around the native Rust core.
@ -80,12 +81,12 @@ class PlatformFFI {
String translate(String name, String locale) => String translate(String name, String locale) =>
_ffiBind.translate(name: name, locale: locale); _ffiBind.translate(name: name, locale: locale);
Uint8List? getRgba(SessionID sessionId, int bufSize) { Uint8List? getRgba(SessionID sessionId, int display, int bufSize) {
if (_session_get_rgba == null) return null; if (_session_get_rgba == null) return null;
final sessionIdStr = sessionId.toString(); final sessionIdStr = sessionId.toString();
var a = sessionIdStr.toNativeUtf8(); var a = sessionIdStr.toNativeUtf8();
try { try {
final buffer = _session_get_rgba!(a); final buffer = _session_get_rgba!(a, display);
if (buffer == nullptr) { if (buffer == nullptr) {
return null; return null;
} }
@ -96,12 +97,11 @@ class PlatformFFI {
} }
} }
int getRgbaSize(SessionID sessionId) => int getRgbaSize(SessionID sessionId, int display) =>
_ffiBind.sessionGetRgbaSize(sessionId: sessionId); _ffiBind.sessionGetRgbaSize(sessionId: sessionId, display: display);
void nextRgba(SessionID sessionId) => void nextRgba(SessionID sessionId, int display) => _ffiBind.sessionNextRgba(sessionId: sessionId, display: display);
_ffiBind.sessionNextRgba(sessionId: sessionId); void registerTexture(SessionID sessionId, int display, int ptr) =>
void registerTexture(SessionID sessionId, int ptr) => _ffiBind.sessionRegisterTexture(sessionId: sessionId, display: display, ptr: ptr);
_ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
/// Init the FFI class, loads the native Rust core library. /// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async { Future<void> init(String appType) async {
@ -117,7 +117,7 @@ class PlatformFFI {
: DynamicLibrary.process(); : DynamicLibrary.process();
debugPrint('initializing FFI $_appType'); debugPrint('initializing FFI $_appType');
try { try {
_session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba"); _session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
try { try {
// SYSTEM user failed // SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path; _dir = (await getApplicationDocumentsDirectory()).path;

View File

@ -18,7 +18,6 @@ class StateGlobal {
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteToolBar = false.obs; final RxBool showRemoteToolBar = false.obs;
final RxInt displaysCount = 0.obs;
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
// Only used for macOS // Only used for macOS
bool closeOnFullscreen = false; bool closeOnFullscreen = false;

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -67,6 +68,44 @@ class RustDeskMultiWindowManager {
); );
} }
// This function must be called in the main window thread.
// Because the _remoteDesktopWindows is managed in that thread.
openMonitorSession(
int windowId, String peerId, int display, int displayCount) async {
if (_remoteDesktopWindows.length > 1) {
for (final windowId in _remoteDesktopWindows) {
if (await DesktopMultiWindow.invokeMethod(
windowId,
kWindowEventActiveDisplaySession,
jsonEncode({
'id': peerId,
'display': display,
}))) {
return;
}
}
}
final displays = display == kAllDisplayValue
? List.generate(displayCount, (index) => index)
: [display];
var params = {
'type': WindowType.RemoteDesktop.index,
'id': peerId,
'tab_window_id': windowId,
'display': display,
'displays': displays,
};
await _newSession(
false,
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
peerId,
_remoteDesktopWindows,
jsonEncode(params),
);
}
Future<int> newSessionWindow( Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async { WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg); final windowController = await DesktopMultiWindow.createWindow(msg);
@ -148,11 +187,21 @@ class RustDeskMultiWindowManager {
bool openInTabs = type != WindowType.RemoteDesktop || bool openInTabs = type != WindowType.RemoteDesktop ||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs); mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs);
if (windows.length > 1 || !openInTabs) { if (kOpenSamePeerInNewWindow) {
for (final windowId in windows) { // Open in new window if the peer is already connected.
if (await DesktopMultiWindow.invokeMethod( // No need to care about the previous session type.
windowId, kWindowEventActiveSession, remoteId)) { if (type == WindowType.RemoteDesktop &&
return MultiWindowCallResult(windowId, null); await bind.sessionGetFlutterOptionByPeerId(id: remoteId, k: '') !=
null) {
openInTabs = false;
}
} else {
if (windows.length > 1 || !openInTabs) {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return MultiWindowCallResult(windowId, null);
}
} }
} }
} }

View File

@ -5,7 +5,7 @@ use hbb_common::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex, Mutex as TokioMutex,
}, },
ResultType, SessionID, ResultType,
}; };
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::{ use std::{
@ -61,7 +61,7 @@ pub enum ClipboardFile {
} }
struct MsgChannel { struct MsgChannel {
session_uuid: SessionID, peer_id: String,
conn_id: i32, conn_id: i32,
sender: UnboundedSender<ClipboardFile>, sender: UnboundedSender<ClipboardFile>,
receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>, receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>,
@ -90,12 +90,12 @@ impl ClipboardFile {
} }
} }
pub fn get_client_conn_id(session_uuid: &SessionID) -> Option<i32> { pub fn get_client_conn_id(peer_id: &str) -> Option<i32> {
VEC_MSG_CHANNEL VEC_MSG_CHANNEL
.read() .read()
.unwrap() .unwrap()
.iter() .iter()
.find(|x| x.session_uuid == session_uuid.to_owned()) .find(|x| x.peer_id == peer_id)
.map(|x| x.conn_id) .map(|x| x.conn_id)
} }
@ -106,13 +106,10 @@ fn get_conn_id() -> i32 {
} }
pub fn get_rx_cliprdr_client( pub fn get_rx_cliprdr_client(
session_uuid: &SessionID, peer_id: &str,
) -> (i32, Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>) { ) -> (i32, Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>) {
let mut lock = VEC_MSG_CHANNEL.write().unwrap(); let mut lock = VEC_MSG_CHANNEL.write().unwrap();
match lock match lock.iter().find(|x| x.peer_id == peer_id) {
.iter()
.find(|x| x.session_uuid == session_uuid.to_owned())
{
Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()),
None => { None => {
let (sender, receiver) = unbounded_channel(); let (sender, receiver) = unbounded_channel();
@ -120,7 +117,7 @@ pub fn get_rx_cliprdr_client(
let receiver2 = receiver.clone(); let receiver2 = receiver.clone();
let conn_id = get_conn_id(); let conn_id = get_conn_id();
let msg_channel = MsgChannel { let msg_channel = MsgChannel {
session_uuid: session_uuid.to_owned(), peer_id: peer_id.to_owned(),
conn_id, conn_id,
sender, sender,
receiver, receiver,
@ -140,7 +137,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
let receiver = Arc::new(TokioMutex::new(receiver)); let receiver = Arc::new(TokioMutex::new(receiver));
let receiver2 = receiver.clone(); let receiver2 = receiver.clone();
let msg_channel = MsgChannel { let msg_channel = MsgChannel {
session_uuid: SessionID::nil(), peer_id: "".to_string(),
conn_id, conn_id,
sender, sender,
receiver, receiver,

View File

@ -27,6 +27,7 @@ message VideoFrame {
EncodedVideoFrames vp8s = 12; EncodedVideoFrames vp8s = 12;
EncodedVideoFrames av1s = 13; EncodedVideoFrames av1s = 13;
} }
int32 display = 14;
} }
message IdPk { message IdPk {
@ -491,6 +492,12 @@ message SwitchDisplay {
Resolution original_resolution = 8; Resolution original_resolution = 8;
} }
message CaptureDisplays {
repeated int32 add = 1;
repeated int32 sub = 2;
repeated int32 set = 3;
}
message PermissionInfo { message PermissionInfo {
enum Permission { enum Permission {
Keyboard = 0; Keyboard = 0;
@ -688,6 +695,8 @@ message Misc {
uint32 full_speed_fps = 27; uint32 full_speed_fps = 27;
uint32 auto_adjust_fps = 28; uint32 auto_adjust_fps = 28;
bool client_record_status = 29; bool client_record_status = 29;
CaptureDisplays capture_displays = 30;
int32 refresh_video_display = 31;
} }
} }

View File

@ -284,6 +284,12 @@ pub struct PeerConfig {
skip_serializing_if = "String::is_empty" skip_serializing_if = "String::is_empty"
)] )]
pub reverse_mouse_wheel: String, pub reverse_mouse_wheel: String,
#[serde(
default = "PeerConfig::default_displays_as_individual_windows",
deserialize_with = "PeerConfig::deserialize_displays_as_individual_windows",
skip_serializing_if = "String::is_empty"
)]
pub displays_as_individual_windows: String,
#[serde( #[serde(
default, default,
@ -328,6 +334,7 @@ impl Default for PeerConfig {
keyboard_mode: Default::default(), keyboard_mode: Default::default(),
view_only: Default::default(), view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
displays_as_individual_windows: Self::default_displays_as_individual_windows(),
custom_resolutions: Default::default(), custom_resolutions: Default::default(),
options: Self::default_options(), options: Self::default_options(),
ui_flutter: Default::default(), ui_flutter: Default::default(),
@ -1144,6 +1151,11 @@ impl PeerConfig {
deserialize_reverse_mouse_wheel, deserialize_reverse_mouse_wheel,
UserDefaultConfig::read().get("reverse_mouse_wheel") UserDefaultConfig::read().get("reverse_mouse_wheel")
); );
serde_field_string!(
default_displays_as_individual_windows,
deserialize_displays_as_individual_windows,
UserDefaultConfig::read().get("displays_as_individual_windows")
);
fn default_custom_image_quality() -> Vec<i32> { fn default_custom_image_quality() -> Vec<i32> {
let f: f64 = UserDefaultConfig::read() let f: f64 = UserDefaultConfig::read()

View File

@ -13,7 +13,7 @@ use hbb_common::{
anyhow::{anyhow, Context}, anyhow::{anyhow, Context},
bytes::Bytes, bytes::Bytes,
log, log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}, message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType, ResultType,
}; };
use std::{ptr, slice}; use std::{ptr, slice};
@ -240,7 +240,7 @@ impl EncoderApi for AomEncoder {
} }
} }
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message> { fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<VideoFrame> {
let mut frames = Vec::new(); let mut frames = Vec::new();
for ref frame in self for ref frame in self
.encode(ms, frame, STRIDE_ALIGN) .encode(ms, frame, STRIDE_ALIGN)
@ -249,7 +249,7 @@ impl EncoderApi for AomEncoder {
frames.push(Self::create_frame(frame)); frames.push(Self::create_frame(frame));
} }
if frames.len() > 0 { if frames.len() > 0 {
Ok(Self::create_msg(frames)) Ok(Self::create_video_frame(frames))
} else { } else {
Err(anyhow!("no valid frame")) Err(anyhow!("no valid frame"))
} }
@ -311,16 +311,14 @@ impl AomEncoder {
} }
#[inline] #[inline]
pub fn create_msg(frames: Vec<EncodedVideoFrame>) -> Message { pub fn create_video_frame(frames: Vec<EncodedVideoFrame>) -> VideoFrame {
let mut msg_out = Message::new();
let mut vf = VideoFrame::new(); let mut vf = VideoFrame::new();
let av1s = EncodedVideoFrames { let av1s = EncodedVideoFrames {
frames: frames.into(), frames: frames.into(),
..Default::default() ..Default::default()
}; };
vf.set_av1s(av1s); vf.set_av1s(av1s);
msg_out.set_video_frame(vf); vf
msg_out
} }
#[inline] #[inline]

View File

@ -23,8 +23,8 @@ use hbb_common::{
config::PeerConfig, config::PeerConfig,
log, log,
message_proto::{ message_proto::{
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message, supported_decoding::PreferCodec, video_frame, EncodedVideoFrames,
SupportedDecoding, SupportedEncoding, SupportedDecoding, SupportedEncoding, VideoFrame,
}, },
sysinfo::{System, SystemExt}, sysinfo::{System, SystemExt},
tokio::time::Instant, tokio::time::Instant,
@ -60,7 +60,7 @@ pub trait EncoderApi {
where where
Self: Sized; Self: Sized;
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message>; fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<VideoFrame>;
fn use_yuv(&self) -> bool; fn use_yuv(&self) -> bool;

View File

@ -8,7 +8,7 @@ use hbb_common::{
bytes::Bytes, bytes::Bytes,
config::HwCodecConfig, config::HwCodecConfig,
log, log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}, message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType, ResultType,
}; };
use hwcodec::{ use hwcodec::{
@ -92,12 +92,7 @@ impl EncoderApi for HwEncoder {
} }
} }
fn encode_to_message( fn encode_to_message(&mut self, frame: &[u8], _ms: i64) -> ResultType<VideoFrame> {
&mut self,
frame: &[u8],
_ms: i64,
) -> ResultType<hbb_common::message_proto::Message> {
let mut msg_out = Message::new();
let mut vf = VideoFrame::new(); let mut vf = VideoFrame::new();
let mut frames = Vec::new(); let mut frames = Vec::new();
for frame in self.encode(frame).with_context(|| "Failed to encode")? { for frame in self.encode(frame).with_context(|| "Failed to encode")? {
@ -117,8 +112,7 @@ impl EncoderApi for HwEncoder {
DataFormat::H264 => vf.set_h264s(frames), DataFormat::H264 => vf.set_h264s(frames),
DataFormat::H265 => vf.set_h265s(frames), DataFormat::H265 => vf.set_h265s(frames),
} }
msg_out.set_video_frame(vf); Ok(vf)
Ok(msg_out)
} else { } else {
Err(anyhow!("no valid frame")) Err(anyhow!("no valid frame"))
} }

View File

@ -4,7 +4,7 @@
use hbb_common::anyhow::{anyhow, Context}; use hbb_common::anyhow::{anyhow, Context};
use hbb_common::log; use hbb_common::log;
use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame};
use hbb_common::ResultType; use hbb_common::ResultType;
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality}; use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality};
@ -172,7 +172,7 @@ impl EncoderApi for VpxEncoder {
} }
} }
fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<Message> { fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType<VideoFrame> {
let mut frames = Vec::new(); let mut frames = Vec::new();
for ref frame in self for ref frame in self
.encode(ms, frame, STRIDE_ALIGN) .encode(ms, frame, STRIDE_ALIGN)
@ -186,7 +186,7 @@ impl EncoderApi for VpxEncoder {
// to-do: flush periodically, e.g. 1 second // to-do: flush periodically, e.g. 1 second
if frames.len() > 0 { if frames.len() > 0 {
Ok(VpxEncoder::create_msg(self.id, frames)) Ok(VpxEncoder::create_video_frame(self.id, frames))
} else { } else {
Err(anyhow!("no valid frame")) Err(anyhow!("no valid frame"))
} }
@ -266,8 +266,10 @@ impl VpxEncoder {
} }
#[inline] #[inline]
pub fn create_msg(codec_id: VpxVideoCodecId, frames: Vec<EncodedVideoFrame>) -> Message { pub fn create_video_frame(
let mut msg_out = Message::new(); codec_id: VpxVideoCodecId,
frames: Vec<EncodedVideoFrame>,
) -> VideoFrame {
let mut vf = VideoFrame::new(); let mut vf = VideoFrame::new();
let vpxs = EncodedVideoFrames { let vpxs = EncodedVideoFrames {
frames: frames.into(), frames: frames.into(),
@ -277,8 +279,7 @@ impl VpxEncoder {
VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs), VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs),
VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs), VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs),
} }
msg_out.set_video_frame(vf); vf
msg_out
} }
#[inline] #[inline]

View File

@ -3,10 +3,7 @@ use std::{
net::SocketAddr, net::SocketAddr,
ops::Deref, ops::Deref,
str::FromStr, str::FromStr,
sync::{ sync::{mpsc, Arc, Mutex, RwLock},
atomic::{AtomicUsize, Ordering},
mpsc, Arc, Mutex, RwLock,
},
}; };
pub use async_trait::async_trait; pub use async_trait::async_trait;
@ -60,6 +57,7 @@ use scrap::{
use crate::{ use crate::{
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
is_keyboard_mode_supported, is_keyboard_mode_supported,
ui_session_interface::{InvokeUiSession, Session},
}; };
#[cfg(not(feature = "flutter"))] #[cfg(not(feature = "flutter"))]
@ -675,9 +673,12 @@ impl Client {
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn try_stop_clipboard(_self_uuid: &uuid::Uuid) { fn try_stop_clipboard(_self_id: &str) {
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if crate::flutter::sessions::other_sessions_running(_self_uuid) { if crate::flutter::sessions::other_sessions_running(
_self_id.to_string(),
ConnType::DEFAULT_CONN,
) {
return; return;
} }
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
@ -1206,6 +1207,17 @@ impl LoginConfigHandler {
self.save_config(config); self.save_config(config);
} }
/// Save reverse mouse wheel ("", "Y") to the current config.
///
/// # Arguments
///
/// * `value` - The "displays_as_individual_windows" value ("", "Y").
pub fn save_displays_as_individual_windows(&mut self, value: String) {
let mut config = self.load_config();
config.displays_as_individual_windows = value;
self.save_config(config);
}
/// Save scroll style to the current config. /// Save scroll style to the current config.
/// ///
/// # Arguments /// # Arguments
@ -1523,6 +1535,15 @@ impl LoginConfigHandler {
msg_out msg_out
} }
/// Create a [`Message`] for refreshing video.
pub fn refresh_display(display: usize) -> Message {
let mut misc = Misc::new();
misc.set_refresh_video_display(display as _);
let mut msg_out = Message::new();
msg_out.set_misc(misc);
msg_out
}
/// Create a [`Message`] for saving custom image quality. /// Create a [`Message`] for saving custom image quality.
/// ///
/// # Arguments /// # Arguments
@ -1789,89 +1810,158 @@ impl LoginConfigHandler {
/// Media data. /// Media data.
pub enum MediaData { pub enum MediaData {
VideoQueue, VideoQueue(usize),
VideoFrame(Box<VideoFrame>), VideoFrame(Box<VideoFrame>),
AudioFrame(Box<AudioFrame>), AudioFrame(Box<AudioFrame>),
AudioFormat(AudioFormat), AudioFormat(AudioFormat),
Reset, Reset(usize),
RecordScreen(bool, i32, i32, String), RecordScreen(bool, usize, i32, i32, String),
} }
pub type MediaSender = mpsc::Sender<MediaData>; pub type MediaSender = mpsc::Sender<MediaData>;
struct VideoHandlerController {
handler: VideoHandler,
count: u128,
duration: std::time::Duration,
skip_beginning: u32,
}
/// Start video and audio thread. /// Start video and audio thread.
/// Return two [`MediaSender`], they should be given to the media producer. /// Return two [`MediaSender`], they should be given to the media producer.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `video_callback` - The callback for video frame. Being called when a video frame is ready. /// * `video_callback` - The callback for video frame. Being called when a video frame is ready.
pub fn start_video_audio_threads<F>( pub fn start_video_audio_threads<F, T>(
session: Session<T>,
video_callback: F, video_callback: F,
) -> ( ) -> (
MediaSender, MediaSender,
MediaSender, MediaSender,
Arc<ArrayQueue<VideoFrame>>, Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
Arc<AtomicUsize>, Arc<RwLock<HashMap<usize, usize>>>,
) )
where where
F: 'static + FnMut(&mut scrap::ImageRgb) + Send, F: 'static + FnMut(usize, &mut scrap::ImageRgb) + Send,
T: InvokeUiSession,
{ {
let (video_sender, video_receiver) = mpsc::channel::<MediaData>(); let (video_sender, video_receiver) = mpsc::channel::<MediaData>();
let video_queue = Arc::new(ArrayQueue::<VideoFrame>::new(VIDEO_QUEUE_SIZE)); let video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>> = Default::default();
let video_queue_cloned = video_queue.clone(); let video_queue_map_cloned = video_queue_map.clone();
let mut video_callback = video_callback; let mut video_callback = video_callback;
let mut duration = std::time::Duration::ZERO; let fps_map = Arc::new(RwLock::new(HashMap::new()));
let mut count = 0; let decode_fps_map = fps_map.clone();
let fps = Arc::new(AtomicUsize::new(0));
let decode_fps = fps.clone();
let mut skip_beginning = 0;
std::thread::spawn(move || { std::thread::spawn(move || {
#[cfg(windows)] #[cfg(windows)]
sync_cpu_usage(); sync_cpu_usage();
let mut video_handler = VideoHandler::new(); let mut handler_controller_map = Vec::new();
// let mut count = Vec::new();
// let mut duration = std::time::Duration::ZERO;
// let mut skip_beginning = Vec::new();
loop { loop {
if let Ok(data) = video_receiver.recv() { if let Ok(data) = video_receiver.recv() {
match data { match data {
MediaData::VideoFrame(_) | MediaData::VideoQueue => { MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => {
let vf = if let MediaData::VideoFrame(vf) = data { let vf = match data {
*vf MediaData::VideoFrame(vf) => *vf,
} else { MediaData::VideoQueue(display) => {
if let Some(vf) = video_queue.pop() { if let Some(video_queue) =
vf video_queue_map.read().unwrap().get(&display)
} else { {
if let Some(vf) = video_queue.pop() {
vf
} else {
continue;
}
} else {
continue;
}
}
_ => {
// unreachable!();
continue; continue;
} }
}; };
let display = vf.display as usize;
let start = std::time::Instant::now(); let start = std::time::Instant::now();
if let Ok(true) = video_handler.handle_frame(vf) { if handler_controller_map.len() <= display {
video_callback(&mut video_handler.rgb); for _i in handler_controller_map.len()..=display {
// fps calculation handler_controller_map.push(VideoHandlerController {
// The first frame will be very slow handler: VideoHandler::new(),
if skip_beginning < 5 { count: 0,
skip_beginning += 1; duration: std::time::Duration::ZERO,
continue; skip_beginning: 0,
});
} }
duration += start.elapsed(); }
count += 1; if let Some(handler_controller) = handler_controller_map.get_mut(display) {
if count % 10 == 0 { match handler_controller.handler.handle_frame(vf) {
fps.store( Ok(true) => {
(count * 1000 / duration.as_millis()) as usize, video_callback(display, &mut handler_controller.handler.rgb);
Ordering::Relaxed,
); // fps calculation
} // The first frame will be very slow
// Clear to get real-time fps if handler_controller.skip_beginning < 5 {
if count > 150 { handler_controller.skip_beginning += 1;
count = 0; continue;
duration = Duration::ZERO; }
handler_controller.duration += start.elapsed();
handler_controller.count += 1;
if handler_controller.count % 10 == 0 {
fps_map.write().unwrap().insert(
display,
(handler_controller.count * 1000
/ handler_controller.duration.as_millis())
as usize,
);
}
// Clear to get real-time fps
if handler_controller.count > 150 {
handler_controller.count = 0;
handler_controller.duration = Duration::ZERO;
}
}
Err(e) => {
// This is a simple workaround.
//
// I only see the following error:
// FailedCall("errcode=1 scrap::common::vpxcodec:libs\\scrap\\src\\common\\vpxcodec.rs:433:9")
// When switching from all displays to one display, the error occurs.
// eg:
// 1. Connect to a device with two displays (A and B).
// 2. Switch to display A. The error occurs.
// 3. If the error does not occur. Switch from A to display B. The error occurs.
//
// to-do: fix the error
log::error!("handle video frame error, {}", e);
session.refresh_video(display as _);
}
_ => {}
} }
} }
} }
MediaData::Reset => { MediaData::Reset(display) => {
video_handler.reset(); if let Some(handler_controler) = handler_controller_map.get_mut(display) {
handler_controler.handler.reset();
}
} }
MediaData::RecordScreen(start, w, h, id) => { MediaData::RecordScreen(start, display, w, h, id) => {
video_handler.record_screen(start, w, h, id) if handler_controller_map.len() == 1 {
// 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.
handler_controller_map[0]
.handler
.record_screen(start, w, h, id);
} else {
if let Some(handler_controler) = handler_controller_map.get_mut(display)
{
handler_controler.handler.record_screen(start, w, h, id);
}
}
} }
_ => {} _ => {}
} }
@ -1882,7 +1972,12 @@ where
log::info!("Video decoder loop exits"); log::info!("Video decoder loop exits");
}); });
let audio_sender = start_audio_thread(); let audio_sender = start_audio_thread();
return (video_sender, audio_sender, video_queue_cloned, decode_fps); return (
video_sender,
audio_sender,
video_queue_map_cloned,
decode_fps_map,
);
} }
/// Start an audio thread /// Start an audio thread
@ -2500,7 +2595,7 @@ pub enum Data {
SetConfirmOverrideFile((i32, i32, bool, bool, bool)), SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
AddJob((i32, String, String, i32, bool, bool)), AddJob((i32, String, String, i32, bool, bool)),
ResumeJob((i32, bool)), ResumeJob((i32, bool)),
RecordScreen(bool, i32, i32, String), RecordScreen(bool, usize, i32, i32, String),
ElevateDirect, ElevateDirect,
ElevateWithLogon(String, String), ElevateWithLogon(String, String),
NewVoiceCall, NewVoiceCall,

View File

@ -1,3 +1,4 @@
use std::collections::HashMap;
use hbb_common::{ use hbb_common::{
get_time, get_time,
message_proto::{Message, VoiceCallRequest, VoiceCallResponse}, message_proto::{Message, VoiceCallRequest, VoiceCallResponse},
@ -7,7 +8,7 @@ use scrap::CodecFormat;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct QualityStatus { pub struct QualityStatus {
pub speed: Option<String>, pub speed: Option<String>,
pub fps: Option<i32>, pub fps: HashMap<usize, i32>,
pub delay: Option<i32>, pub delay: Option<i32>,
pub target_bitrate: Option<i32>, pub target_bitrate: Option<i32>,
pub codec_format: Option<CodecFormat>, pub codec_format: Option<CodecFormat>,

View File

@ -1,33 +1,41 @@
use std::collections::HashMap; use std::{
use std::num::NonZeroI64; collections::HashMap,
use std::sync::{ num::NonZeroI64,
atomic::{AtomicUsize, Ordering}, sync::{
Arc, atomic::{AtomicUsize, Ordering},
Arc, RwLock,
},
}; };
#[cfg(windows)] #[cfg(windows)]
use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend}; use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend};
use crossbeam_queue::ArrayQueue; use crossbeam_queue::ArrayQueue;
use hbb_common::config::{PeerConfig, TransferSerde};
use hbb_common::fs::{
can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult,
RemoveJobMeta,
};
use hbb_common::message_proto::permission_info::Permission;
use hbb_common::protobuf::Message as _;
use hbb_common::rendezvous_proto::ConnType;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::sleep; use hbb_common::sleep;
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::tokio::sync::mpsc::error::TryRecvError;
#[cfg(windows)] #[cfg(windows)]
use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::sync::Mutex as TokioMutex;
use hbb_common::tokio::{ use hbb_common::{
self, allow_err,
sync::mpsc, config::{PeerConfig, TransferSerde},
time::{self, Duration, Instant, Interval}, fs,
fs::{
can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult,
RemoveJobMeta,
},
get_time, log,
message_proto::permission_info::Permission,
message_proto::*,
protobuf::Message as _,
rendezvous_proto::ConnType,
tokio::{
self,
sync::mpsc,
time::{self, Duration, Instant, Interval},
},
Stream,
}; };
use hbb_common::{allow_err, fs, get_time, log, message_proto::*, Stream};
use scrap::CodecFormat; use scrap::CodecFormat;
use crate::client::{ use crate::client::{
@ -43,7 +51,7 @@ use crate::{client::Data, client::Interface};
pub struct Remote<T: InvokeUiSession> { pub struct Remote<T: InvokeUiSession> {
handler: Session<T>, handler: Session<T>,
video_queue: Arc<ArrayQueue<VideoFrame>>, video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender, video_sender: MediaSender,
audio_sender: MediaSender, audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>, receiver: mpsc::UnboundedReceiver<Data>,
@ -61,27 +69,27 @@ pub struct Remote<T: InvokeUiSession> {
#[cfg(windows)] #[cfg(windows)]
client_conn_id: i32, // used for file clipboard client_conn_id: i32, // used for file clipboard
data_count: Arc<AtomicUsize>, data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>, frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
video_format: CodecFormat, video_format: CodecFormat,
elevation_requested: bool, elevation_requested: bool,
fps_control: FpsControl, fps_control_map: HashMap<usize, FpsControl>,
decode_fps: Arc<AtomicUsize>, decode_fps_map: Arc<RwLock<HashMap<usize, usize>>>,
} }
impl<T: InvokeUiSession> Remote<T> { impl<T: InvokeUiSession> Remote<T> {
pub fn new( pub fn new(
handler: Session<T>, handler: Session<T>,
video_queue: Arc<ArrayQueue<VideoFrame>>, video_queue: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender, video_sender: MediaSender,
audio_sender: MediaSender, audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>, receiver: mpsc::UnboundedReceiver<Data>,
sender: mpsc::UnboundedSender<Data>, sender: mpsc::UnboundedSender<Data>,
frame_count: Arc<AtomicUsize>, frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
decode_fps: Arc<AtomicUsize>, decode_fps: Arc<RwLock<HashMap<usize, usize>>>,
) -> Self { ) -> Self {
Self { Self {
handler, handler,
video_queue, video_queue_map: video_queue,
video_sender, video_sender,
audio_sender, audio_sender,
receiver, receiver,
@ -96,13 +104,13 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(windows)] #[cfg(windows)]
client_conn_id: 0, client_conn_id: 0,
data_count: Arc::new(AtomicUsize::new(0)), data_count: Arc::new(AtomicUsize::new(0)),
frame_count, frame_count_map,
video_format: CodecFormat::Unknown, video_format: CodecFormat::Unknown,
stop_voice_call_sender: None, stop_voice_call_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
elevation_requested: false, elevation_requested: false,
fps_control: Default::default(), fps_control_map: Default::default(),
decode_fps, decode_fps_map: decode_fps,
} }
} }
@ -152,7 +160,7 @@ impl<T: InvokeUiSession> Remote<T> {
|| self.handler.is_rdp(); || self.handler.is_rdp();
if !is_conn_not_default { if !is_conn_not_default {
(self.client_conn_id, rx_clip_client_lock) = (self.client_conn_id, rx_clip_client_lock) =
clipboard::get_rx_cliprdr_client(&self.handler.session_id); clipboard::get_rx_cliprdr_client(&self.handler.id);
}; };
} }
#[cfg(windows)] #[cfg(windows)]
@ -229,12 +237,18 @@ impl<T: InvokeUiSession> Remote<T> {
let mut speed = self.data_count.swap(0, Ordering::Relaxed); let mut speed = self.data_count.swap(0, Ordering::Relaxed);
speed = speed * 1000 / elapsed as usize; speed = speed * 1000 / elapsed as usize;
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
let mut fps = self.frame_count.swap(0, Ordering::Relaxed) as _;
// Correcting the inaccuracy of status_timer let mut frame_count_map_write = self.frame_count_map.write().unwrap();
fps = fps * 1000 / elapsed as i32; let frame_count_map = frame_count_map_write.clone();
frame_count_map_write.values_mut().for_each(|v| *v = 0);
drop(frame_count_map_write);
let fps = frame_count_map.iter().map(|(k, v)| {
// Correcting the inaccuracy of status_timer
(k.clone(), (*v as i32) * 1000 / elapsed as i32)
}).collect::<HashMap<usize, i32>>();
self.handler.update_quality_status(QualityStatus { self.handler.update_quality_status(QualityStatus {
speed:Some(speed), speed: Some(speed),
fps:Some(fps), fps,
..Default::default() ..Default::default()
}); });
} }
@ -260,7 +274,7 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if _set_disconnected_ok { if _set_disconnected_ok {
Client::try_stop_clipboard(&self.handler.session_id); Client::try_stop_clipboard(&self.handler.id);
} }
#[cfg(windows)] #[cfg(windows)]
@ -760,10 +774,10 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
} }
Data::RecordScreen(start, w, h, id) => { Data::RecordScreen(start, display, w, h, id) => {
let _ = self let _ = self
.video_sender .video_sender
.send(MediaData::RecordScreen(start, w, h, id)); .send(MediaData::RecordScreen(start, display, w, h, id));
} }
Data::ElevateDirect => { Data::ElevateDirect => {
let mut request = ElevationRequest::new(); let mut request = ElevationRequest::new();
@ -904,89 +918,100 @@ impl<T: InvokeUiSession> Remote<T> {
None => false, None => false,
} }
} }
#[inline] #[inline]
fn fps_control(&mut self, direct: bool) { fn fps_control(&mut self, direct: bool) {
let len = self.video_queue.len(); let decode_fps_read = self.decode_fps_map.read().unwrap();
let ctl = &mut self.fps_control; for (display, decode_fps) in decode_fps_read.iter() {
// Current full speed decoding fps let video_queue_map_read = self.video_queue_map.read().unwrap();
let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed); let Some(video_queue) = video_queue_map_read.get(display) else {
if decode_fps == 0 { continue;
return; };
}
let limited_fps = if direct {
decode_fps * 9 / 10 // 30 got 27
} else {
decode_fps * 4 / 5 // 30 got 24
};
// send full speed fps
let version = self.handler.lc.read().unwrap().version;
let max_encode_speed = 144 * 10 / 9;
if version >= hbb_common::get_version_number("1.2.1")
&& (ctl.last_full_speed_fps.is_none() // First time
|| ((ctl.last_full_speed_fps.unwrap_or_default() - decode_fps as i32).abs() >= 5 // diff 5
&& !(decode_fps > max_encode_speed // already exceed max encoding speed
&& ctl.last_full_speed_fps.unwrap_or_default() > max_encode_speed as i32)))
{
let mut misc = Misc::new();
misc.set_full_speed_fps(decode_fps as _);
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
ctl.last_full_speed_fps = Some(decode_fps as _);
}
// decrease judgement
let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; // 500ms
let should_decrease = len >= debounce // exceed debounce
&& len > ctl.last_queue_size + 5 // still caching
&& !ctl.last_custom_fps.unwrap_or(i32::MAX) < limited_fps as i32; // NOT already set a smaller one
// increase judgement if !self.fps_control_map.contains_key(display) {
if len <= 1 { self.fps_control_map.insert(*display, FpsControl::default());
ctl.idle_counter += 1;
} else {
ctl.idle_counter = 0;
}
let mut should_increase = false;
if let Some(last_custom_fps) = ctl.last_custom_fps {
// ever set
if last_custom_fps + 5 < limited_fps as i32 && ctl.idle_counter > 3 {
// limited_fps is 5 larger than last set, and idle time is more than 3 seconds
should_increase = true;
} }
} let Some(ctl) = self.fps_control_map.get_mut(display) else {
if should_decrease || should_increase { return;
// limited_fps to ensure decoding is faster than encoding };
let mut custom_fps = limited_fps as i32;
if custom_fps < 1 { let len = video_queue.len();
custom_fps = 1; let decode_fps = *decode_fps;
} let limited_fps = if direct {
// send custom fps decode_fps * 9 / 10 // 30 got 27
let mut misc = Misc::new();
if version > hbb_common::get_version_number("1.2.1") {
// avoid confusion with custom image quality fps
misc.set_auto_adjust_fps(custom_fps as _);
} else { } else {
misc.set_option(OptionMessage { decode_fps * 4 / 5 // 30 got 24
custom_fps, };
..Default::default() // send full speed fps
}); let version = self.handler.lc.read().unwrap().version;
let max_encode_speed = 144 * 10 / 9;
if version >= hbb_common::get_version_number("1.2.1")
&& (ctl.last_full_speed_fps.is_none() // First time
|| ((ctl.last_full_speed_fps.unwrap_or_default() - decode_fps as i32).abs() >= 5 // diff 5
&& !(decode_fps > max_encode_speed // already exceed max encoding speed
&& ctl.last_full_speed_fps.unwrap_or_default() > max_encode_speed as i32)))
{
let mut misc = Misc::new();
misc.set_full_speed_fps(decode_fps as _);
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
ctl.last_full_speed_fps = Some(decode_fps as _);
}
// decrease judgement
let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; // 500ms
let should_decrease = len >= debounce // exceed debounce
&& len > ctl.last_queue_size + 5 // still caching
&& !ctl.last_custom_fps.unwrap_or(i32::MAX) < limited_fps as i32; // NOT already set a smaller one
// increase judgement
if len <= 1 {
ctl.idle_counter += 1;
} else {
ctl.idle_counter = 0;
}
let mut should_increase = false;
if let Some(last_custom_fps) = ctl.last_custom_fps {
// ever set
if last_custom_fps + 5 < limited_fps as i32 && ctl.idle_counter > 3 {
// limited_fps is 5 larger than last set, and idle time is more than 3 seconds
should_increase = true;
}
}
if should_decrease || should_increase {
// limited_fps to ensure decoding is faster than encoding
let mut custom_fps = limited_fps as i32;
if custom_fps < 1 {
custom_fps = 1;
}
// send custom fps
let mut misc = Misc::new();
if version > hbb_common::get_version_number("1.2.1") {
// avoid confusion with custom image quality fps
misc.set_auto_adjust_fps(custom_fps as _);
} else {
misc.set_option(OptionMessage {
custom_fps,
..Default::default()
});
}
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
ctl.last_queue_size = len;
ctl.last_custom_fps = Some(custom_fps);
}
// send refresh
if ctl.refresh_times < 10 // enough
&& (len > video_queue.capacity() / 2
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30))
{
// Refresh causes client set_display, left frames cause flickering.
while let Some(_) = video_queue.pop() {}
self.handler.refresh_video(*display as _);
ctl.refresh_times += 1;
ctl.last_refresh_instant = Instant::now();
} }
let mut msg = Message::new();
msg.set_misc(misc);
self.sender.send(Data::Message(msg)).ok();
ctl.last_queue_size = len;
ctl.last_custom_fps = Some(custom_fps);
}
// send refresh
if ctl.refresh_times < 10 // enough
&& (len > self.video_queue.capacity() / 2
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30))
{
// Refresh causes client set_display, left frames cause flickering.
while let Some(_) = self.video_queue.pop() {}
self.handler.refresh_video();
ctl.refresh_times += 1;
ctl.last_refresh_instant = Instant::now();
} }
} }
@ -1008,14 +1033,27 @@ impl<T: InvokeUiSession> Remote<T> {
..Default::default() ..Default::default()
}) })
}; };
let display = vf.display as usize;
let mut video_queue_write = self.video_queue_map.write().unwrap();
if !video_queue_write.contains_key(&display) {
video_queue_write.insert(
display,
ArrayQueue::<VideoFrame>::new(crate::client::VIDEO_QUEUE_SIZE),
);
}
if Self::contains_key_frame(&vf) { if Self::contains_key_frame(&vf) {
while let Some(_) = self.video_queue.pop() {} if let Some(video_queue) = video_queue_write.get_mut(&display) {
while let Some(_) = video_queue.pop() {}
}
self.video_sender self.video_sender
.send(MediaData::VideoFrame(Box::new(vf))) .send(MediaData::VideoFrame(Box::new(vf)))
.ok(); .ok();
} else { } else {
self.video_queue.force_push(vf); if let Some(video_queue) = video_queue_write.get_mut(&display) {
self.video_sender.send(MediaData::VideoQueue).ok(); video_queue.force_push(vf);
}
self.video_sender.send(MediaData::VideoQueue(display)).ok();
} }
} }
Some(message::Union::Hash(hash)) => { Some(message::Union::Hash(hash)) => {
@ -1297,7 +1335,9 @@ impl<T: InvokeUiSession> Remote<T> {
} }
Some(misc::Union::SwitchDisplay(s)) => { Some(misc::Union::SwitchDisplay(s)) => {
self.handler.handle_peer_switch_display(&s); self.handler.handle_peer_switch_display(&s);
self.video_sender.send(MediaData::Reset).ok(); self.video_sender
.send(MediaData::Reset(s.display as _))
.ok();
if s.width > 0 && s.height > 0 { if s.width > 0 && s.height > 0 {
self.handler.set_display( self.handler.set_display(
s.x, s.x,
@ -1674,7 +1714,7 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union {
if self.client_conn_id if self.client_conn_id
!= clipboard::get_client_conn_id(&crate::flutter::get_cur_session_id()).unwrap_or(0) != clipboard::get_client_conn_id(&crate::flutter::get_cur_peer_id()).unwrap_or(0)
{ {
return; return;
} }

View File

@ -54,6 +54,8 @@ pub const PLATFORM_LINUX: &str = "Linux";
pub const PLATFORM_MACOS: &str = "Mac OS"; pub const PLATFORM_MACOS: &str = "Mac OS";
pub const PLATFORM_ANDROID: &str = "Android"; pub const PLATFORM_ANDROID: &str = "Android";
const MIN_VER_MULTI_UI_SESSION: &str = "1.2.4";
pub mod input { pub mod input {
pub const MOUSE_TYPE_MOVE: i32 = 0; pub const MOUSE_TYPE_MOVE: i32 = 0;
pub const MOUSE_TYPE_DOWN: i32 = 1; pub const MOUSE_TYPE_DOWN: i32 = 1;
@ -120,6 +122,16 @@ pub fn set_server_running(b: bool) {
*SERVER_RUNNING.write().unwrap() = b; *SERVER_RUNNING.write().unwrap() = b;
} }
#[inline]
pub fn is_support_multi_ui_session(ver: &str) -> bool {
is_support_multi_ui_session_num(hbb_common::get_version_number(ver))
}
#[inline]
pub fn is_support_multi_ui_session_num(ver: i64) -> bool {
ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION)
}
// is server process, with "--server" args // is server process, with "--server" args
#[inline] #[inline]
pub fn is_server() -> bool { pub fn is_server() -> bool {
@ -780,13 +792,17 @@ pub fn get_sysinfo() -> serde_json::Value {
} }
let hostname = hostname(); // sys.hostname() return localhost on android in my test let hostname = hostname(); // sys.hostname() return localhost on android in my test
use serde_json::json; use serde_json::json;
let mut out = json!({ #[cfg(any(target_os = "android", target_os = "ios"))]
let out;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let mut out;
out = json!({
"cpu": format!("{cpu}{num_cpus}/{num_pcpus} cores"), "cpu": format!("{cpu}{num_cpus}/{num_pcpus} cores"),
"memory": format!("{memory}GB"), "memory": format!("{memory}GB"),
"os": os, "os": os,
"hostname": hostname, "hostname": hostname,
}); });
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
out["username"] = json!(crate::platform::get_active_username()); out["username"] = json!(crate::platform::get_active_username());
} }

View File

@ -18,8 +18,6 @@ use hbb_common::{
}; };
use serde_json::json; use serde_json::json;
#[cfg(not(feature = "flutter_texture_render"))]
use std::sync::atomic::{AtomicBool, Ordering};
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::CString, ffi::CString,
@ -150,25 +148,40 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) {
// Afterwards the vector will be dropped and thus freed. // Afterwards the vector will be dropped and thus freed.
} }
#[derive(Default)]
struct SessionHandler {
event_stream: Option<StreamSink<EventToUI>>,
#[cfg(feature = "flutter_texture_render")]
notify_rendered: bool,
#[cfg(feature = "flutter_texture_render")]
renderer: VideoRenderer,
}
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct FlutterHandler { pub struct FlutterHandler {
pub event_stream: Arc<RwLock<Option<StreamSink<EventToUI>>>>, // ui session id -> display handler data
notify_rendered: Arc<RwLock<bool>>, session_handlers: Arc<RwLock<HashMap<SessionID, SessionHandler>>>,
renderer: Arc<RwLock<VideoRenderer>>,
peer_info: Arc<RwLock<PeerInfo>>, peer_info: Arc<RwLock<PeerInfo>>,
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
hooks: Arc<RwLock<HashMap<String, SessionHook>>>, hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
} }
#[cfg(not(feature = "flutter_texture_render"))] #[cfg(not(feature = "flutter_texture_render"))]
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct FlutterHandler { struct RgbaData {
pub event_stream: Arc<RwLock<Option<StreamSink<EventToUI>>>>,
// SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`.
// We must check the `rgba_valid` before reading [rgba]. // We must check the `rgba_valid` before reading [rgba].
pub rgba: Arc<RwLock<Vec<u8>>>, data: Vec<u8>,
pub rgba_valid: Arc<AtomicBool>, valid: bool,
}
#[cfg(not(feature = "flutter_texture_render"))]
#[derive(Default, Clone)]
pub struct FlutterHandler {
session_handlers: Arc<RwLock<HashMap<SessionID, SessionHandler>>>,
display_rgbas: Arc<RwLock<HashMap<usize, RgbaData>>>,
peer_info: Arc<RwLock<PeerInfo>>, peer_info: Arc<RwLock<PeerInfo>>,
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
hooks: Arc<RwLock<HashMap<String, SessionHook>>>, hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
@ -184,14 +197,22 @@ pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(
dst_rgba_stride: c_int, dst_rgba_stride: c_int,
); );
#[cfg(feature = "flutter_texture_render")]
pub(super) type TextureRgbaPtr = usize;
#[cfg(feature = "flutter_texture_render")]
struct DisplaySessionInfo {
// TextureRgba pointer in flutter native.
texture_rgba_ptr: TextureRgbaPtr,
size: (usize, usize),
}
// Video Texture Renderer in Flutter // Video Texture Renderer in Flutter
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
#[derive(Clone)] #[derive(Clone)]
struct VideoRenderer { struct VideoRenderer {
// TextureRgba pointer in flutter native. is_support_multi_ui_session: bool,
ptr: Arc<RwLock<usize>>, map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
width: usize,
height: usize,
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>, on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
} }
@ -217,9 +238,8 @@ impl Default for VideoRenderer {
} }
}; };
Self { Self {
ptr: Default::default(), map_display_sessions: Default::default(),
width: 0, is_support_multi_ui_session: false,
height: 0,
on_rgba_func, on_rgba_func,
} }
} }
@ -228,33 +248,74 @@ impl Default for VideoRenderer {
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
impl VideoRenderer { impl VideoRenderer {
#[inline] #[inline]
pub fn set_size(&mut self, width: usize, height: usize) { fn set_size(&mut self, display: usize, width: usize, height: usize) {
self.width = width; let mut sessions_lock = self.map_display_sessions.write().unwrap();
self.height = height; if let Some(info) = sessions_lock.get_mut(&display) {
info.size = (width, height);
} else {
sessions_lock.insert(
display,
DisplaySessionInfo {
texture_rgba_ptr: usize::default(),
size: (width, height),
},
);
}
} }
pub fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { fn register_texture(&self, display: usize, ptr: usize) {
let ptr = self.ptr.read().unwrap(); let mut sessions_lock = self.map_display_sessions.write().unwrap();
if *ptr == usize::default() { if ptr == 0 {
sessions_lock.remove(&display);
} else {
if let Some(info) = sessions_lock.get_mut(&display) {
if info.texture_rgba_ptr != 0 && info.texture_rgba_ptr != ptr as TextureRgbaPtr {
log::error!("unreachable, texture_rgba_ptr is not null and not equal to ptr");
}
info.texture_rgba_ptr = ptr as _;
} else {
if ptr != 0 {
sessions_lock.insert(
display,
DisplaySessionInfo {
texture_rgba_ptr: ptr as _,
size: (0, 0),
},
);
}
}
}
}
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) {
let read_lock = self.map_display_sessions.read().unwrap();
let opt_info = if !self.is_support_multi_ui_session {
read_lock.values().next()
} else {
read_lock.get(&display)
};
let Some(info) = opt_info else {
return;
};
if info.texture_rgba_ptr == usize::default() {
return; return;
} }
// It is also Ok to skip this check. // It is also Ok to skip this check.
if self.width != rgba.w || self.height != rgba.h { if info.size.0 != rgba.w || info.size.1 != rgba.h {
log::error!( log::error!(
"width/height mismatch: ({},{}) != ({},{})", "width/height mismatch: ({},{}) != ({},{})",
self.width, info.size.0,
self.height, info.size.1,
rgba.w, rgba.w,
rgba.h rgba.h
); );
return; return;
} }
if let Some(func) = &self.on_rgba_func { if let Some(func) = &self.on_rgba_func {
unsafe { unsafe {
func( func(
*ptr as _, info.texture_rgba_ptr as _,
rgba.raw.as_ptr() as _, rgba.raw.as_ptr() as _,
rgba.raw.len() as _, rgba.raw.len() as _,
rgba.w as _, rgba.w as _,
@ -266,34 +327,42 @@ impl VideoRenderer {
} }
} }
impl SessionHandler {
pub fn on_waiting_for_image_dialog_show(&mut self) {
#[cfg(any(feature = "flutter_texture_render"))]
{
self.notify_rendered = false;
}
// rgba array render will notify every frame
}
}
impl FlutterHandler { impl FlutterHandler {
/// Push an event to the event queue. /// Push an event to all the event queues.
/// An event is stored as json in the event queue. /// An event is stored as json in the event queues.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `name` - The name of the event. /// * `name` - The name of the event.
/// * `event` - Fields of the event content. /// * `event` - Fields of the event content.
pub fn push_event(&self, name: &str, event: Vec<(&str, &str)>) -> Option<bool> { pub fn push_event(&self, name: &str, event: Vec<(&str, &str)>) {
let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
debug_assert!(h.get("name").is_none()); debug_assert!(h.get("name").is_none());
h.insert("name", name); h.insert("name", name);
let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned());
Some( for (_, session) in self.session_handlers.read().unwrap().iter() {
self.event_stream if let Some(stream) = &session.event_stream {
.read() stream.add(EventToUI::Event(out.clone()));
.unwrap() }
.as_ref()? }
.add(EventToUI::Event(out)),
)
} }
pub(crate) fn close_event_stream(&self) { pub(crate) fn close_event_stream(&self, session_id: SessionID) {
let mut stream_lock = self.event_stream.write().unwrap(); // to-do: Make sure the following logic is correct.
if let Some(stream) = &*stream_lock { // No need to remove the display handler, because it will be removed when the connection is closed.
stream.add(EventToUI::Event("close".to_owned())); if let Some(session) = self.session_handlers.write().unwrap().get_mut(&session_id) {
try_send_close_event(&session.event_stream);
} }
*stream_lock = None;
} }
fn make_displays_msg(displays: &Vec<DisplayInfo>) -> String { fn make_displays_msg(displays: &Vec<DisplayInfo>) -> String {
@ -314,6 +383,7 @@ impl FlutterHandler {
serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned())
} }
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub(crate) fn add_session_hook(&self, key: String, hook: SessionHook) -> bool { pub(crate) fn add_session_hook(&self, key: String, hook: SessionHook) -> bool {
let mut hooks = self.hooks.write().unwrap(); let mut hooks = self.hooks.write().unwrap();
@ -325,6 +395,7 @@ impl FlutterHandler {
true true
} }
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub(crate) fn remove_session_hook(&self, key: &String) -> bool { pub(crate) fn remove_session_hook(&self, key: &String) -> bool {
let mut hooks = self.hooks.write().unwrap(); let mut hooks = self.hooks.write().unwrap();
@ -335,27 +406,6 @@ impl FlutterHandler {
let _ = hooks.remove(key); let _ = hooks.remove(key);
true true
} }
#[inline]
#[cfg(feature = "flutter_texture_render")]
pub fn register_texture(&self, ptr: usize) {
*self.renderer.read().unwrap().ptr.write().unwrap() = ptr;
}
#[inline]
#[cfg(feature = "flutter_texture_render")]
pub fn set_size(&self, width: usize, height: usize) {
*self.notify_rendered.write().unwrap() = false;
self.renderer.write().unwrap().set_size(width, height);
}
pub fn on_waiting_for_image_dialog_show(&self) {
#[cfg(any(feature = "flutter_texture_render"))]
{
*self.notify_rendered.write().unwrap() = false;
}
// rgba array render will notify every frame
}
} }
impl InvokeUiSession for FlutterHandler { impl InvokeUiSession for FlutterHandler {
@ -408,7 +458,10 @@ impl InvokeUiSession for FlutterHandler {
"update_quality_status", "update_quality_status",
vec![ vec![
("speed", &status.speed.map_or(NULL, |it| it)), ("speed", &status.speed.map_or(NULL, |it| it)),
("fps", &status.fps.map_or(NULL, |it| it.to_string())), (
"fps",
&serde_json::ser::to_string(&status.fps).unwrap_or(NULL.to_owned()),
),
("delay", &status.delay.map_or(NULL, |it| it.to_string())), ("delay", &status.delay.map_or(NULL, |it| it.to_string())),
( (
"target_bitrate", "target_bitrate",
@ -529,7 +582,7 @@ impl InvokeUiSession for FlutterHandler {
#[inline] #[inline]
#[cfg(not(feature = "flutter_texture_render"))] #[cfg(not(feature = "flutter_texture_render"))]
fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
// Give a chance for plugins or etc to hook a rgba data. // Give a chance for plugins or etc to hook a rgba data.
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
for (key, hook) in self.hooks.read().unwrap().iter() { for (key, hook) in self.hooks.read().unwrap().iter() {
@ -541,27 +594,51 @@ impl InvokeUiSession for FlutterHandler {
} }
// If the current rgba is not fetched by flutter, i.e., is valid. // If the current rgba is not fetched by flutter, i.e., is valid.
// We give up sending a new event to flutter. // We give up sending a new event to flutter.
if self.rgba_valid.load(Ordering::Relaxed) { let mut rgba_write_lock = self.display_rgbas.write().unwrap();
return; if let Some(rgba_data) = rgba_write_lock.get_mut(&display) {
if rgba_data.valid {
return;
} else {
rgba_data.valid = true;
}
// Return the rgba buffer to the video handler for reusing allocated rgba buffer.
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
} else {
let mut rgba_data = RgbaData::default();
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut rgba_data.data);
rgba_write_lock.insert(display, rgba_data);
} }
self.rgba_valid.store(true, Ordering::Relaxed); drop(rgba_write_lock);
// Return the rgba buffer to the video handler for reusing allocated rgba buffer.
std::mem::swap::<Vec<u8>>(&mut rgba.raw, &mut *self.rgba.write().unwrap()); // Non-texture-render UI does not support multiple displays in the one UI session.
if let Some(stream) = &*self.event_stream.read().unwrap() { // It's Ok to notify each session for now.
stream.add(EventToUI::Rgba); for h in self.session_handlers.read().unwrap().values() {
if let Some(stream) = &h.event_stream {
stream.add(EventToUI::Rgba(display));
}
} }
} }
#[inline] #[inline]
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
self.renderer.read().unwrap().on_rgba(rgba); let mut try_notify_sessions = Vec::new();
if *self.notify_rendered.read().unwrap() { for (id, session) in self.session_handlers.read().unwrap().iter() {
return; session.renderer.on_rgba(display, rgba);
if !session.notify_rendered {
try_notify_sessions.push(id.clone());
}
} }
if let Some(stream) = &*self.event_stream.read().unwrap() { if try_notify_sessions.len() > 0 {
stream.add(EventToUI::Rgba); let mut write_lock = self.session_handlers.write().unwrap();
*self.notify_rendered.write().unwrap() = true; for id in try_notify_sessions.iter() {
if let Some(session) = write_lock.get_mut(id) {
if let Some(stream) = &session.event_stream {
stream.add(EventToUI::Rgba(display));
session.notify_rendered = true;
}
}
}
} }
} }
@ -578,6 +655,17 @@ impl InvokeUiSession for FlutterHandler {
let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
let resolutions = serialize_resolutions(&pi.resolutions.resolutions); let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
*self.peer_info.write().unwrap() = pi.clone(); *self.peer_info.write().unwrap() = pi.clone();
#[cfg(feature = "flutter_texture_render")]
{
self.session_handlers
.write()
.unwrap()
.values_mut()
.for_each(|h| {
h.renderer.is_support_multi_ui_session =
crate::common::is_support_multi_ui_session(&pi.version);
});
}
self.push_event( self.push_event(
"peer_info", "peer_info",
vec![ vec![
@ -701,21 +789,31 @@ impl InvokeUiSession for FlutterHandler {
} }
#[inline] #[inline]
fn get_rgba(&self) -> *const u8 { fn get_rgba(&self, _display: usize) -> *const u8 {
#[cfg(not(feature = "flutter_texture_render"))] #[cfg(not(feature = "flutter_texture_render"))]
if self.rgba_valid.load(Ordering::Relaxed) { if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) {
return self.rgba.read().unwrap().as_ptr(); if rgba_data.valid {
return rgba_data.data.as_ptr();
}
} }
std::ptr::null_mut() std::ptr::null_mut()
} }
#[inline] #[inline]
fn next_rgba(&self) { fn next_rgba(&self, _display: usize) {
#[cfg(not(feature = "flutter_texture_render"))] #[cfg(not(feature = "flutter_texture_render"))]
self.rgba_valid.store(false, Ordering::Relaxed); if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&_display) {
rgba_data.valid = true;
}
} }
} }
// This function is only used for the default connection session.
pub fn session_add_existed(peer_id: String, session_id: SessionID) -> ResultType<()> {
sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id);
Ok(())
}
/// Create a new remote session with the given id. /// Create a new remote session with the given id.
/// ///
/// # Arguments /// # Arguments
@ -733,18 +831,6 @@ pub fn session_add(
force_relay: bool, force_relay: bool,
password: String, password: String,
) -> ResultType<FlutterSession> { ) -> ResultType<FlutterSession> {
LocalConfig::set_remote_id(&id);
let session: Session<FlutterHandler> = Session {
session_id: session_id.clone(),
id: id.to_owned(),
password,
server_keyboard_enabled: Arc::new(RwLock::new(true)),
server_file_transfer_enabled: Arc::new(RwLock::new(true)),
server_clipboard_enabled: Arc::new(RwLock::new(true)),
..Default::default()
};
let conn_type = if is_file_transfer { let conn_type = if is_file_transfer {
ConnType::FILE_TRANSFER ConnType::FILE_TRANSFER
} else if is_port_forward { } else if is_port_forward {
@ -757,6 +843,26 @@ pub fn session_add(
ConnType::DEFAULT_CONN ConnType::DEFAULT_CONN
}; };
// to-do: check the same id session.
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
if session.lc.read().unwrap().conn_type != conn_type {
bail!("same session id is found with different conn type?");
}
// The same session is added before?
bail!("same session id is found");
}
LocalConfig::set_remote_id(&id);
let session: Session<FlutterHandler> = Session {
id: id.to_owned(),
password,
server_keyboard_enabled: Arc::new(RwLock::new(true)),
server_file_transfer_enabled: Arc::new(RwLock::new(true)),
server_clipboard_enabled: Arc::new(RwLock::new(true)),
..Default::default()
};
let switch_uuid = if switch_uuid.is_empty() { let switch_uuid = if switch_uuid.is_empty() {
None None
} else { } else {
@ -768,12 +874,8 @@ pub fn session_add(
.write() .write()
.unwrap() .unwrap()
.initialize(id.to_owned(), conn_type, switch_uuid, force_relay); .initialize(id.to_owned(), conn_type, switch_uuid, force_relay);
let session = Arc::new(session.clone()); let session = Arc::new(session.clone());
if let Some(same_id_session) = sessions::add_session(session_id.to_owned(), session.clone()) { sessions::insert_session(session_id.to_owned(), conn_type, session.clone());
log::error!("Should not happen");
same_id_session.close();
}
Ok(session) Ok(session)
} }
@ -789,18 +891,39 @@ pub fn session_start_(
id: &str, id: &str,
event_stream: StreamSink<EventToUI>, event_stream: StreamSink<EventToUI>,
) -> ResultType<()> { ) -> ResultType<()> {
if let Some(session) = sessions::get_session(session_id) { // is_connected is used to indicate whether to start a peer connection. For two cases:
#[cfg(feature = "flutter_texture_render")] // 1. "Move tab to new window"
log::info!( // 2. multi ui session within the same peer connnection.
"Session {} start, render by flutter texture rgba plugin", let mut is_connected = false;
id let mut is_found = false;
for s in sessions::get_sessions() {
if let Some(h) = s.session_handlers.write().unwrap().get_mut(session_id) {
is_connected = h.event_stream.is_some();
try_send_close_event(&h.event_stream);
h.event_stream = Some(event_stream);
is_found = true;
break;
}
}
if !is_found {
bail!(
"No session with peer id {}, session id: {}",
id,
session_id.to_string()
); );
#[cfg(not(feature = "flutter_texture_render"))] }
log::info!("Session {} start, render by flutter paint widget", id);
let is_pre_added = session.event_stream.read().unwrap().is_some(); if let Some(session) = sessions::get_session_by_session_id(session_id) {
session.close_event_stream(); let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1;
*session.event_stream.write().unwrap() = Some(event_stream); if !is_connected && is_first_ui_session {
if !is_pre_added { #[cfg(feature = "flutter_texture_render")]
log::info!(
"Session {} start, render by flutter texture rgba plugin",
id
);
#[cfg(not(feature = "flutter_texture_render"))]
log::info!("Session {} start, render by flutter paint widget", id);
let session = (*session).clone(); let session = (*session).clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let round = session.connection_round_state.lock().unwrap().new_round(); let round = session.connection_round_state.lock().unwrap().new_round();
@ -813,19 +936,26 @@ pub fn session_start_(
} }
} }
#[inline]
fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
if let Some(stream) = &event_stream {
stream.add(EventToUI::Event("close".to_owned()));
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn update_text_clipboard_required() { pub fn update_text_clipboard_required() {
let is_required = sessions::get_sessions() let is_required = sessions::get_sessions()
.iter() .iter()
.any(|session| session.is_text_clipboard_required()); .any(|s| s.is_text_clipboard_required());
Client::set_is_text_clipboard_required(is_required); Client::set_is_text_clipboard_required(is_required);
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn send_text_clipboard_msg(msg: Message) { pub fn send_text_clipboard_msg(msg: Message) {
for session in sessions::get_sessions() { for s in sessions::get_sessions() {
if session.is_text_clipboard_required() { if s.is_text_clipboard_required() {
session.send(Data::Message(msg.clone())); s.send(Data::Message(msg.clone()));
} }
} }
} }
@ -998,6 +1128,11 @@ pub fn get_cur_session_id() -> SessionID {
CUR_SESSION_ID.read().unwrap().clone() CUR_SESSION_ID.read().unwrap().clone()
} }
pub fn get_cur_peer_id() -> String {
sessions::get_peer_id_by_session_id(&get_cur_session_id(), ConnType::DEFAULT_CONN)
.unwrap_or("".to_string())
}
pub fn set_cur_session_id(session_id: SessionID) { pub fn set_cur_session_id(session_id: SessionID) {
if get_cur_session_id() != session_id { if get_cur_session_id() != session_id {
*CUR_SESSION_ID.write().unwrap() = session_id; *CUR_SESSION_ID.write().unwrap() = session_id;
@ -1034,47 +1169,76 @@ fn char_to_session_id(c: *const char) -> ResultType<SessionID> {
SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e)) SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e))
} }
pub fn session_get_rgba_size(_session_id: SessionID) -> usize { pub fn session_get_rgba_size(_session_id: SessionID, _display: usize) -> usize {
#[cfg(not(feature = "flutter_texture_render"))] #[cfg(not(feature = "flutter_texture_render"))]
if let Some(session) = sessions::get_session(&_session_id) { if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
return session.rgba.read().unwrap().len(); return session
.display_rgbas
.read()
.unwrap()
.get(&_display)
.map_or(0, |rgba| rgba.data.len());
} }
0 0
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn session_get_rgba(session_uuid_str: *const char) -> *const u8 { pub extern "C" fn session_get_rgba(session_uuid_str: *const char, display: usize) -> *const u8 {
if let Ok(session_id) = char_to_session_id(session_uuid_str) { if let Ok(session_id) = char_to_session_id(session_uuid_str) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(s) = sessions::get_session_by_session_id(&session_id) {
return session.get_rgba(); return s.ui_handler.get_rgba(display);
} }
} }
std::ptr::null() std::ptr::null()
} }
pub fn session_next_rgba(session_id: SessionID) { pub fn session_next_rgba(session_id: SessionID, display: usize) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(s) = sessions::get_session_by_session_id(&session_id) {
return session.next_rgba(); return s.ui_handler.next_rgba(display);
} }
} }
#[inline] #[inline]
pub fn session_register_texture(_session_id: SessionID, _ptr: usize) { pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) {
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
if let Some(session) = sessions::get_session(&_session_id) { for s in sessions::get_sessions() {
session.register_texture(_ptr); if let Some(h) = s
return; .ui_handler
.session_handlers
.write()
.unwrap()
.get_mut(&_session_id)
{
h.notify_rendered = false;
h.renderer.set_size(_display, _width, _height);
break;
}
} }
} }
#[inline] #[inline]
pub fn push_session_event( pub fn session_register_texture(_session_id: SessionID, _display: usize, _ptr: usize) {
session_id: &SessionID, #[cfg(feature = "flutter_texture_render")]
name: &str, for s in sessions::get_sessions() {
event: Vec<(&str, &str)>, if let Some(h) = s
) -> Option<bool> { .ui_handler
sessions::get_session(session_id)?.push_event(name, event) .session_handlers
.read()
.unwrap()
.get(&_session_id)
{
h.renderer.register_texture(_display, _ptr);
break;
}
}
}
#[inline]
pub fn push_session_event(session_id: &SessionID, name: &str, event: Vec<(&str, &str)>) {
if let Some(s) = sessions::get_session_by_session_id(session_id) {
s.push_event(name, event);
}
} }
#[inline] #[inline]
@ -1123,7 +1287,7 @@ fn session_send_touch_scale(
) { ) {
match v.get("v").and_then(|s| s.as_i64()) { match v.get("v").and_then(|s| s.as_i64()) {
Some(scale) => { Some(scale) => {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_touch_scale(scale as _, alt, ctrl, shift, command); session.send_touch_scale(scale as _, alt, ctrl, shift, command);
} }
} }
@ -1147,7 +1311,7 @@ fn session_send_touch_pan(
v.get("y").and_then(|y| y.as_i64()), v.get("y").and_then(|y| y.as_i64()),
) { ) {
(Some(x), Some(y)) => { (Some(x), Some(y)) => {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session session
.send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command); .send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command);
} }
@ -1191,6 +1355,15 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) {
} }
} }
#[inline]
pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
for s in sessions::get_sessions() {
if let Some(h) = s.session_handlers.write().unwrap().get_mut(&session_id) {
h.on_waiting_for_image_dialog_show();
}
}
}
/// Hooks for session. /// Hooks for session.
#[derive(Clone)] #[derive(Clone)]
pub enum SessionHook { pub enum SessionHook {
@ -1199,7 +1372,7 @@ pub enum SessionHook {
#[inline] #[inline]
pub fn get_cur_session() -> Option<FlutterSession> { pub fn get_cur_session() -> Option<FlutterSession> {
sessions::get_session(&*CUR_SESSION_ID.read().unwrap()) sessions::get_session_by_session_id(&*CUR_SESSION_ID.read().unwrap())
} }
// sessions mod is used to avoid the big lock of sessions' map. // sessions mod is used to avoid the big lock of sessions' map.
@ -1207,22 +1380,118 @@ pub mod sessions {
use super::*; use super::*;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref SESSIONS: RwLock<HashMap<SessionID, FlutterSession>> = Default::default(); // peer -> peer session, peer session -> ui sessions
static ref SESSIONS: RwLock<HashMap<(String, ConnType), FlutterSession>> = Default::default();
} }
#[inline] #[inline]
pub fn add_session(session_id: SessionID, session: FlutterSession) -> Option<FlutterSession> { pub fn get_session_count(peer_id: String, conn_type: ConnType) -> usize {
SESSIONS.write().unwrap().insert(session_id, session) SESSIONS
.read()
.unwrap()
.get(&(peer_id, conn_type))
.map(|s| s.ui_handler.session_handlers.read().unwrap().len())
.unwrap_or(0)
} }
#[inline] #[inline]
pub fn remove_session(session_id: &SessionID) -> Option<FlutterSession> { pub fn get_peer_id_by_session_id(id: &SessionID, conn_type: ConnType) -> Option<String> {
SESSIONS.write().unwrap().remove(session_id) SESSIONS
.read()
.unwrap()
.iter()
.find_map(|((peer_id, t), s)| {
if *t == conn_type
&& s.ui_handler
.session_handlers
.read()
.unwrap()
.contains_key(id)
{
Some(peer_id.clone())
} else {
None
}
})
} }
#[inline] #[inline]
pub fn get_session(session_id: &SessionID) -> Option<FlutterSession> { pub fn get_session_by_session_id(id: &SessionID) -> Option<FlutterSession> {
SESSIONS.read().unwrap().get(session_id).cloned() SESSIONS
.read()
.unwrap()
.values()
.find(|s| {
s.ui_handler
.session_handlers
.read()
.unwrap()
.contains_key(id)
})
.cloned()
}
#[inline]
pub fn get_session_by_peer_id(peer_id: String, conn_type: ConnType) -> Option<FlutterSession> {
SESSIONS.read().unwrap().get(&(peer_id, conn_type)).cloned()
}
#[inline]
pub fn remove_session_by_session_id(id: &SessionID) -> Option<FlutterSession> {
let mut remove_peer_key = None;
for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() {
let mut write_lock = s.ui_handler.session_handlers.write().unwrap();
if write_lock.remove(id).is_some() {
if write_lock.is_empty() {
remove_peer_key = Some(peer_key.clone());
}
break;
}
}
SESSIONS.write().unwrap().remove(&remove_peer_key?)
}
#[inline]
pub fn insert_session(session_id: SessionID, conn_type: ConnType, session: FlutterSession) {
SESSIONS
.write()
.unwrap()
.entry((session.id.clone(), conn_type))
.or_insert(session)
.ui_handler
.session_handlers
.write()
.unwrap()
.insert(session_id, Default::default());
}
#[inline]
pub fn insert_peer_session_id(
peer_id: String,
conn_type: ConnType,
session_id: SessionID,
) -> bool {
if let Some(s) = SESSIONS.read().unwrap().get(&(peer_id, conn_type)) {
#[cfg(not(feature = "flutter_texture_render"))]
let h = SessionHandler::default();
#[cfg(feature = "flutter_texture_render")]
let mut h = SessionHandler::default();
#[cfg(feature = "flutter_texture_render")]
{
h.renderer.is_support_multi_ui_session = crate::common::is_support_multi_ui_session(
&s.ui_handler.peer_info.read().unwrap().version,
);
}
let _ = s
.ui_handler
.session_handlers
.write()
.unwrap()
.insert(session_id, h);
true
} else {
false
}
} }
#[inline] #[inline]
@ -1230,26 +1499,14 @@ pub mod sessions {
SESSIONS.read().unwrap().values().cloned().collect() SESSIONS.read().unwrap().values().cloned().collect()
} }
#[inline]
pub fn get_session_by_peer_id(peer_id: &str) -> Option<FlutterSession> {
SESSIONS
.read()
.unwrap()
.values()
.find(|session| session.id == peer_id)
.map(|s| s.clone())
.clone()
}
#[inline] #[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn other_sessions_running(session_id: &SessionID) -> bool { pub fn other_sessions_running(peer_id: String, conn_type: ConnType) -> bool {
SESSIONS SESSIONS
.read() .read()
.unwrap() .unwrap()
.keys() .get(&(peer_id, conn_type))
.filter(|k| *k != session_id) .map(|s| s.session_handlers.read().unwrap().len() != 0)
.count() .unwrap_or(false)
!= 0
} }
} }

View File

@ -4,7 +4,7 @@ use crate::{
client::file_trait::FileManager, client::file_trait::FileManager,
common::is_keyboard_mode_supported, common::is_keyboard_mode_supported,
common::make_fd_to_json, common::make_fd_to_json,
flutter::{self, session_add, session_start_, sessions}, flutter::{self, session_add, session_add_existed, session_start_, sessions},
input::*, input::*,
ui_interface::{self, *}, ui_interface::{self, *},
}; };
@ -16,6 +16,7 @@ use hbb_common::{
config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
fs, lazy_static, log, fs, lazy_static, log,
message_proto::KeyboardMode, message_proto::KeyboardMode,
rendezvous_proto::ConnType,
ResultType, ResultType,
}; };
use std::{ use std::{
@ -67,7 +68,7 @@ pub fn stop_global_event_stream(app_type: String) {
} }
pub enum EventToUI { pub enum EventToUI {
Event(String), Event(String),
Rgba, Rgba(usize),
} }
pub fn host_stop_system_key_propagate(_stopped: bool) { pub fn host_stop_system_key_propagate(_stopped: bool) {
@ -75,8 +76,26 @@ pub fn host_stop_system_key_propagate(_stopped: bool) {
crate::platform::windows::stop_system_key_propagate(_stopped); crate::platform::windows::stop_system_key_propagate(_stopped);
} }
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen // This function is only used to count the number of control sessions.
// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 pub fn peer_get_default_sessions_count(id: String) -> SyncReturn<usize> {
SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN))
}
pub fn session_add_existed_sync(id: String, session_id: SessionID) -> SyncReturn<String> {
if let Err(e) = session_add_existed(id.clone(), session_id) {
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
} else {
SyncReturn("".to_owned())
}
}
pub fn session_try_add_display(session_id: SessionID, displays: Vec<i32>) -> SyncReturn<()> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.capture_displays(displays, vec![], vec![]);
}
SyncReturn(())
}
pub fn session_add_sync( pub fn session_add_sync(
session_id: SessionID, session_id: SessionID,
id: String, id: String,
@ -112,7 +131,7 @@ pub fn session_start(
} }
pub fn session_get_remember(session_id: SessionID) -> Option<bool> { pub fn session_get_remember(session_id: SessionID) -> Option<bool> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_remember()) Some(session.get_remember())
} else { } else {
None None
@ -120,7 +139,7 @@ pub fn session_get_remember(session_id: SessionID) -> Option<bool> {
} }
pub fn session_get_toggle_option(session_id: SessionID, arg: String) -> Option<bool> { pub fn session_get_toggle_option(session_id: SessionID, arg: String) -> Option<bool> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_toggle_option(arg)) Some(session.get_toggle_option(arg))
} else { } else {
None None
@ -133,7 +152,7 @@ pub fn session_get_toggle_option_sync(session_id: SessionID, arg: String) -> Syn
} }
pub fn session_get_option(session_id: SessionID, arg: String) -> Option<String> { pub fn session_get_option(session_id: SessionID, arg: String) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_option(arg)) Some(session.get_option(arg))
} else { } else {
None None
@ -147,55 +166,61 @@ pub fn session_login(
password: String, password: String,
remember: bool, remember: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.login(os_username, os_password, password, remember); session.login(os_username, os_password, password, remember);
} }
} }
pub fn session_close(session_id: SessionID) { pub fn session_close(session_id: SessionID) {
if let Some(session) = sessions::remove_session(&session_id) { if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
session.close_event_stream(); session.close_event_stream(session_id);
session.close(); session.close();
} }
} }
pub fn session_refresh(session_id: SessionID) { pub fn session_refresh(session_id: SessionID, display: usize) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.refresh_video(); session.refresh_video(display as _);
} }
} }
pub fn session_record_screen(session_id: SessionID, start: bool, width: usize, height: usize) { pub fn session_record_screen(
if let Some(session) = sessions::get_session(&session_id) { session_id: SessionID,
session.record_screen(start, width as _, height as _); start: bool,
display: usize,
width: usize,
height: usize,
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.record_screen(start, display as _, width as _, height as _);
} }
} }
pub fn session_record_status(session_id: SessionID, status: bool) { pub fn session_record_status(session_id: SessionID, status: bool) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.record_status(status); session.record_status(status);
} }
} }
pub fn session_reconnect(session_id: SessionID, force_relay: bool) { pub fn session_reconnect(session_id: SessionID, force_relay: bool) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.reconnect(force_relay); session.reconnect(force_relay);
} }
} }
pub fn session_toggle_option(session_id: SessionID, value: String) { pub fn session_toggle_option(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
log::warn!("toggle option {}", &value); log::warn!("toggle option {}", &value);
session.toggle_option(value.clone()); session.toggle_option(value.clone());
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if sessions::get_session(&session_id).is_some() && value == "disable-clipboard" { if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
crate::flutter::update_text_clipboard_required(); crate::flutter::update_text_clipboard_required();
} }
} }
pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option<String> { pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_flutter_option(k)) Some(session.get_flutter_option(k))
} else { } else {
None None
@ -203,13 +228,14 @@ pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option<St
} }
pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) { pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_flutter_option(k, v); session.save_flutter_option(k, v);
} }
} }
// This function is only used for the default connection session.
pub fn session_get_flutter_option_by_peer_id(id: String, k: String) -> Option<String> { pub fn session_get_flutter_option_by_peer_id(id: String, k: String) -> Option<String> {
if let Some(session) = sessions::get_session_by_peer_id(&id) { if let Some(session) = sessions::get_session_by_peer_id(id, ConnType::DEFAULT_CONN) {
Some(session.get_flutter_option(k)) Some(session.get_flutter_option(k))
} else { } else {
None None
@ -238,7 +264,7 @@ pub fn set_local_kb_layout_type(kb_layout_type: String) {
} }
pub fn session_get_view_style(session_id: SessionID) -> Option<String> { pub fn session_get_view_style(session_id: SessionID) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_view_style()) Some(session.get_view_style())
} else { } else {
None None
@ -246,13 +272,13 @@ pub fn session_get_view_style(session_id: SessionID) -> Option<String> {
} }
pub fn session_set_view_style(session_id: SessionID, value: String) { pub fn session_set_view_style(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_view_style(value); session.save_view_style(value);
} }
} }
pub fn session_get_scroll_style(session_id: SessionID) -> Option<String> { pub fn session_get_scroll_style(session_id: SessionID) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_scroll_style()) Some(session.get_scroll_style())
} else { } else {
None None
@ -260,13 +286,13 @@ pub fn session_get_scroll_style(session_id: SessionID) -> Option<String> {
} }
pub fn session_set_scroll_style(session_id: SessionID, value: String) { pub fn session_set_scroll_style(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_scroll_style(value); session.save_scroll_style(value);
} }
} }
pub fn session_get_image_quality(session_id: SessionID) -> Option<String> { pub fn session_get_image_quality(session_id: SessionID) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_image_quality()) Some(session.get_image_quality())
} else { } else {
None None
@ -274,13 +300,13 @@ pub fn session_get_image_quality(session_id: SessionID) -> Option<String> {
} }
pub fn session_set_image_quality(session_id: SessionID, value: String) { pub fn session_set_image_quality(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_image_quality(value); session.save_image_quality(value);
} }
} }
pub fn session_get_keyboard_mode(session_id: SessionID) -> Option<String> { pub fn session_get_keyboard_mode(session_id: SessionID) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_keyboard_mode()) Some(session.get_keyboard_mode())
} else { } else {
None None
@ -289,7 +315,7 @@ pub fn session_get_keyboard_mode(session_id: SessionID) -> Option<String> {
pub fn session_set_keyboard_mode(session_id: SessionID, value: String) { pub fn session_set_keyboard_mode(session_id: SessionID, value: String) {
let mut _mode_updated = false; let mut _mode_updated = false;
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_keyboard_mode(value.clone()); session.save_keyboard_mode(value.clone());
_mode_updated = true; _mode_updated = true;
} }
@ -300,7 +326,7 @@ pub fn session_set_keyboard_mode(session_id: SessionID, value: String) {
} }
pub fn session_get_reverse_mouse_wheel(session_id: SessionID) -> Option<String> { pub fn session_get_reverse_mouse_wheel(session_id: SessionID) -> Option<String> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_reverse_mouse_wheel()) Some(session.get_reverse_mouse_wheel())
} else { } else {
None None
@ -308,13 +334,27 @@ pub fn session_get_reverse_mouse_wheel(session_id: SessionID) -> Option<String>
} }
pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) { pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_reverse_mouse_wheel(value); session.save_reverse_mouse_wheel(value);
} }
} }
pub fn session_get_displays_as_individual_windows(session_id: SessionID) -> SyncReturn<Option<String>> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
SyncReturn(Some(session.get_displays_as_individual_windows()))
} else {
SyncReturn(None)
}
}
pub fn session_set_displays_as_individual_windows(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_displays_as_individual_windows(value);
}
}
pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32>> { pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32>> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
Some(session.get_custom_image_quality()) Some(session.get_custom_image_quality())
} else { } else {
None None
@ -322,7 +362,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32
} }
pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> { pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { if let Ok(mode) = KeyboardMode::from_str(&mode[..]) {
SyncReturn(is_keyboard_mode_supported( SyncReturn(is_keyboard_mode_supported(
&mode, &mode,
@ -337,32 +377,36 @@ pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -
} }
pub fn session_set_custom_image_quality(session_id: SessionID, value: i32) { pub fn session_set_custom_image_quality(session_id: SessionID, value: i32) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.save_custom_image_quality(value); session.save_custom_image_quality(value);
} }
} }
pub fn session_set_custom_fps(session_id: SessionID, fps: i32) { pub fn session_set_custom_fps(session_id: SessionID, fps: i32) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.set_custom_fps(fps); session.set_custom_fps(fps);
} }
} }
pub fn session_lock_screen(session_id: SessionID) { pub fn session_lock_screen(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.lock_screen(); session.lock_screen();
} }
} }
pub fn session_ctrl_alt_del(session_id: SessionID) { pub fn session_ctrl_alt_del(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.ctrl_alt_del(); session.ctrl_alt_del();
} }
} }
pub fn session_switch_display(session_id: SessionID, value: i32) { pub fn session_switch_display(session_id: SessionID, value: Vec<i32>) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.switch_display(value); if value.len() == 1 {
session.switch_display(value[0]);
} else {
session.capture_displays(vec![], vec![], value);
}
} }
} }
@ -374,7 +418,7 @@ pub fn session_handle_flutter_key_event(
lock_modes: i32, lock_modes: i32,
down_or_up: bool, down_or_up: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode(); let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event( session.handle_flutter_key_event(
&keyboard_mode, &keyboard_mode,
@ -395,7 +439,7 @@ pub fn session_handle_flutter_key_event(
// This will cause the keyboard input to take no effect. // This will cause the keyboard input to take no effect.
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> { pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(session) = sessions::get_session(&_session_id) { if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
let keyboard_mode = session.get_keyboard_mode(); let keyboard_mode = session.get_keyboard_mode();
if _enter { if _enter {
set_cur_session_id_(_session_id, &keyboard_mode); set_cur_session_id_(_session_id, &keyboard_mode);
@ -417,14 +461,14 @@ pub fn session_input_key(
shift: bool, shift: bool,
command: bool, command: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
// #[cfg(any(target_os = "android", target_os = "ios"))] // #[cfg(any(target_os = "android", target_os = "ios"))]
session.input_key(&name, down, press, alt, ctrl, shift, command); session.input_key(&name, down, press, alt, ctrl, shift, command);
} }
} }
pub fn session_input_string(session_id: SessionID, value: String) { pub fn session_input_string(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
// #[cfg(any(target_os = "android", target_os = "ios"))] // #[cfg(any(target_os = "android", target_os = "ios"))]
session.input_string(&value); session.input_string(&value);
} }
@ -432,33 +476,33 @@ pub fn session_input_string(session_id: SessionID, value: String) {
// chat_client_mode // chat_client_mode
pub fn session_send_chat(session_id: SessionID, text: String) { pub fn session_send_chat(session_id: SessionID, text: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_chat(text); session.send_chat(text);
} }
} }
pub fn session_peer_option(session_id: SessionID, name: String, value: String) { pub fn session_peer_option(session_id: SessionID, name: String, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.set_option(name, value); session.set_option(name, value);
} }
} }
pub fn session_get_peer_option(session_id: SessionID, name: String) -> String { pub fn session_get_peer_option(session_id: SessionID, name: String) -> String {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
return session.get_option(name); return session.get_option(name);
} }
"".to_string() "".to_string()
} }
pub fn session_input_os_password(session_id: SessionID, value: String) { pub fn session_input_os_password(session_id: SessionID, value: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.input_os_password(value, true); session.input_os_password(value, true);
} }
} }
// File Action // File Action
pub fn session_read_remote_dir(session_id: SessionID, path: String, include_hidden: bool) { pub fn session_read_remote_dir(session_id: SessionID, path: String, include_hidden: bool) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.read_remote_dir(path, include_hidden); session.read_remote_dir(path, include_hidden);
} }
} }
@ -472,7 +516,7 @@ pub fn session_send_files(
include_hidden: bool, include_hidden: bool,
is_remote: bool, is_remote: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_files(act_id, path, to, file_num, include_hidden, is_remote); session.send_files(act_id, path, to, file_num, include_hidden, is_remote);
} }
} }
@ -485,7 +529,7 @@ pub fn session_set_confirm_override_file(
remember: bool, remember: bool,
is_upload: bool, is_upload: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload); session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload);
} }
} }
@ -497,7 +541,7 @@ pub fn session_remove_file(
file_num: i32, file_num: i32,
is_remote: bool, is_remote: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.remove_file(act_id, path, file_num, is_remote); session.remove_file(act_id, path, file_num, is_remote);
} }
} }
@ -509,7 +553,7 @@ pub fn session_read_dir_recursive(
is_remote: bool, is_remote: bool,
show_hidden: bool, show_hidden: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.remove_dir_all(act_id, path, is_remote, show_hidden); session.remove_dir_all(act_id, path, is_remote, show_hidden);
} }
} }
@ -520,19 +564,19 @@ pub fn session_remove_all_empty_dirs(
path: String, path: String,
is_remote: bool, is_remote: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.remove_dir(act_id, path, is_remote); session.remove_dir(act_id, path, is_remote);
} }
} }
pub fn session_cancel_job(session_id: SessionID, act_id: i32) { pub fn session_cancel_job(session_id: SessionID, act_id: i32) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.cancel_job(act_id); session.cancel_job(act_id);
} }
} }
pub fn session_create_dir(session_id: SessionID, act_id: i32, path: String, is_remote: bool) { pub fn session_create_dir(session_id: SessionID, act_id: i32, path: String, is_remote: bool) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.create_dir(act_id, path, is_remote); session.create_dir(act_id, path, is_remote);
} }
} }
@ -549,14 +593,14 @@ pub fn session_read_local_dir_sync(
} }
pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
return session.get_platform(is_remote); return session.get_platform(is_remote);
} }
"".to_string() "".to_string()
} }
pub fn session_load_last_transfer_jobs(session_id: SessionID) { pub fn session_load_last_transfer_jobs(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
return session.load_last_jobs(); return session.load_last_jobs();
} else { } else {
// a tip for flutter dev // a tip for flutter dev
@ -576,46 +620,44 @@ pub fn session_add_job(
include_hidden: bool, include_hidden: bool,
is_remote: bool, is_remote: bool,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.add_job(act_id, path, to, file_num, include_hidden, is_remote); session.add_job(act_id, path, to, file_num, include_hidden, is_remote);
} }
} }
pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.resume_job(act_id, is_remote); session.resume_job(act_id, is_remote);
} }
} }
pub fn session_elevate_direct(session_id: SessionID) { pub fn session_elevate_direct(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.elevate_direct(); session.elevate_direct();
} }
} }
pub fn session_elevate_with_logon(session_id: SessionID, username: String, password: String) { pub fn session_elevate_with_logon(session_id: SessionID, username: String, password: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.elevate_with_logon(username, password); session.elevate_with_logon(username, password);
} }
} }
pub fn session_switch_sides(session_id: SessionID) { pub fn session_switch_sides(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.switch_sides(); session.switch_sides();
} }
} }
pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32, height: i32) { pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32, height: i32) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.change_resolution(display, width, height); session.change_resolution(display, width, height);
} }
} }
pub fn session_set_size(_session_id: SessionID, _width: usize, _height: usize) { pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) {
#[cfg(feature = "flutter_texture_render")] #[cfg(feature = "flutter_texture_render")]
if let Some(session) = sessions::get_session(&_session_id) { super::flutter::session_set_size(_session_id, _display, _width, _height)
session.set_size(_width, _height);
}
} }
pub fn main_get_sound_inputs() -> Vec<String> { pub fn main_get_sound_inputs() -> Vec<String> {
@ -1008,18 +1050,22 @@ pub fn main_handle_relay_id(id: String) -> String {
handle_relay_id(id) handle_relay_id(id)
} }
pub fn main_get_current_display() -> SyncReturn<String> { pub fn main_get_main_display() -> SyncReturn<String> {
#[cfg(not(target_os = "ios"))]
let display_info = match crate::video_service::get_current_display() {
Ok((_, _, display)) => serde_json::to_string(&HashMap::from([
("w", display.width()),
("h", display.height()),
]))
.unwrap_or_default(),
Err(..) => "".to_string(),
};
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
let display_info = "".to_owned(); let display_info = "".to_owned();
#[cfg(not(target_os = "ios"))]
let mut display_info = "".to_owned();
#[cfg(not(target_os = "ios"))]
if let Ok(displays) = crate::display_service::try_get_displays() {
// to-do: Need to detect current display index.
if let Some(display) = displays.iter().next() {
display_info = serde_json::to_string(&HashMap::from([
("w", display.width()),
("h", display.height()),
]))
.unwrap_or_default();
}
}
SyncReturn(display_info) SyncReturn(display_info)
} }
@ -1029,31 +1075,31 @@ pub fn session_add_port_forward(
remote_host: String, remote_host: String,
remote_port: i32, remote_port: i32,
) { ) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.add_port_forward(local_port, remote_host, remote_port); session.add_port_forward(local_port, remote_host, remote_port);
} }
} }
pub fn session_remove_port_forward(session_id: SessionID, local_port: i32) { pub fn session_remove_port_forward(session_id: SessionID, local_port: i32) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.remove_port_forward(local_port); session.remove_port_forward(local_port);
} }
} }
pub fn session_new_rdp(session_id: SessionID) { pub fn session_new_rdp(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.new_rdp(); session.new_rdp();
} }
} }
pub fn session_request_voice_call(session_id: SessionID) { pub fn session_request_voice_call(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.request_voice_call(); session.request_voice_call();
} }
} }
pub fn session_close_voice_call(session_id: SessionID) { pub fn session_close_voice_call(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.close_voice_call(); session.close_voice_call();
} }
} }
@ -1238,20 +1284,20 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) {
_ => 0, _ => 0,
} << 3; } << 3;
} }
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_mouse(mask, x, y, alt, ctrl, shift, command); session.send_mouse(mask, x, y, alt, ctrl, shift, command);
} }
} }
} }
pub fn session_restart_remote_device(session_id: SessionID) { pub fn session_restart_remote_device(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.restart_remote_device(); session.restart_remote_device();
} }
} }
pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> SyncReturn<String> { pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> SyncReturn<String> {
let res = if let Some(session) = sessions::get_session(&session_id) { let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.get_audit_server(typ) session.get_audit_server(typ)
} else { } else {
"".to_owned() "".to_owned()
@ -1260,13 +1306,13 @@ pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> Sync
} }
pub fn session_send_note(session_id: SessionID, note: String) { pub fn session_send_note(session_id: SessionID, note: String) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_note(note) session.send_note(note)
} }
} }
pub fn session_alternative_codecs(session_id: SessionID) -> String { pub fn session_alternative_codecs(session_id: SessionID) -> String {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let (vp8, av1, h264, h265) = session.alternative_codecs(); let (vp8, av1, h264, h265) = session.alternative_codecs();
let msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]); let msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]);
serde_json::ser::to_string(&msg).unwrap_or("".to_owned()) serde_json::ser::to_string(&msg).unwrap_or("".to_owned())
@ -1276,15 +1322,13 @@ pub fn session_alternative_codecs(session_id: SessionID) -> String {
} }
pub fn session_change_prefer_codec(session_id: SessionID) { pub fn session_change_prefer_codec(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.change_prefer_codec(); session.change_prefer_codec();
} }
} }
pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { super::flutter::session_on_waiting_for_image_dialog_show(session_id);
session.ui_handler.on_waiting_for_image_dialog_show();
}
} }
pub fn main_set_home_dir(_home: String) { pub fn main_set_home_dir(_home: String) {
@ -1434,16 +1478,22 @@ pub fn translate(name: String, locale: String) -> SyncReturn<String> {
SyncReturn(crate::client::translate_locale(name, &locale)) SyncReturn(crate::client::translate_locale(name, &locale))
} }
pub fn session_get_rgba_size(session_id: SessionID) -> SyncReturn<usize> { pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> SyncReturn<usize> {
SyncReturn(super::flutter::session_get_rgba_size(session_id)) SyncReturn(super::flutter::session_get_rgba_size(session_id, display))
} }
pub fn session_next_rgba(session_id: SessionID) -> SyncReturn<()> { pub fn session_next_rgba(session_id: SessionID, display: usize) -> SyncReturn<()> {
SyncReturn(super::flutter::session_next_rgba(session_id)) SyncReturn(super::flutter::session_next_rgba(session_id, display))
} }
pub fn session_register_texture(session_id: SessionID, ptr: usize) -> SyncReturn<()> { pub fn session_register_texture(
SyncReturn(super::flutter::session_register_texture(session_id, ptr)) session_id: SessionID,
display: usize,
ptr: usize,
) -> SyncReturn<()> {
SyncReturn(super::flutter::session_register_texture(
session_id, display, ptr,
))
} }
pub fn query_onlines(ids: Vec<String>) { pub fn query_onlines(ids: Vec<String>) {
@ -1522,15 +1572,15 @@ pub fn main_update_me() -> SyncReturn<bool> {
} }
pub fn set_cur_session_id(session_id: SessionID) { pub fn set_cur_session_id(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
set_cur_session_id_(session_id, &session.get_keyboard_mode()) set_cur_session_id_(session_id, &session.get_keyboard_mode())
} }
} }
fn set_cur_session_id_(session_id: SessionID, keyboard_mode: &str) { fn set_cur_session_id_(session_id: SessionID, _keyboard_mode: &str) {
super::flutter::set_cur_session_id(session_id); super::flutter::set_cur_session_id(session_id);
#[cfg(windows)] #[cfg(windows)]
crate::keyboard::update_grab_get_key_name(keyboard_mode); crate::keyboard::update_grab_get_key_name(_keyboard_mode);
} }
pub fn install_show_run_without_install() -> SyncReturn<bool> { pub fn install_show_run_without_install() -> SyncReturn<bool> {
@ -1625,6 +1675,10 @@ pub fn main_test_wallpaper(_second: u64) {
}); });
} }
pub fn main_support_remove_wallpaper() -> bool {
support_remove_wallpaper()
}
/// Send a url scheme throught the ipc. /// Send a url scheme throught the ipc.
/// ///
/// * macOS only /// * macOS only
@ -1808,6 +1862,10 @@ pub fn plugin_install(_id: String, _b: bool) {
} }
} }
pub fn is_support_multi_ui_session(version: String) -> SyncReturn<bool> {
SyncReturn(crate::common::is_support_multi_ui_session(&version))
}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub mod server_side { pub mod server_side {
use hbb_common::{config, log}; use hbb_common::{config, log};

View File

@ -214,6 +214,7 @@ static mut IS_0X021D_DOWN: bool = false;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static mut IS_LEFT_OPTION_DOWN: bool = false; static mut IS_LEFT_OPTION_DOWN: bool = false;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn get_keyboard_mode() -> String { fn get_keyboard_mode() -> String {
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(any(feature = "flutter", feature = "cli")))]
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "按交集过滤"), ("Filter by intersection", "按交集过滤"),
("Remove wallpaper during incoming sessions", "接受会话时移除桌面壁纸"), ("Remove wallpaper during incoming sessions", "接受会话时移除桌面壁纸"),
("Test", "测试"), ("Test", "测试"),
("switch_display_elevated_connections_tip", "提权后,被控有多个连接,不能切换到非主显示器。若要控制多显示器,请安装后再试。"),
("display_is_plugged_out_msg", "显示器被拔出,切换到第一个显示器。"),
("No displays", "没有显示器。"),
("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"),
("Open in new window", "在新的窗口中打开"),
("Show displays as individual windows", "在单个窗口中打开显示器"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -555,8 +555,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"), ("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"),
("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"),
("Remove wallpaper during incoming sessions", ""), ("Filter by intersection", "Nach Schnittmenge filtern"),
("Test", ""), ("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"),
("Filter by intersection", "Nach Schnittpunkt filtern") ("Test", "Test"),
("Filter by intersection", "Nach Schnittpunkt filtern"),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -222,5 +222,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("upgrade_rustdesk_server_pro_to_{}_tip", "Please upgrade RustDesk Server Pro to version {} or newer!"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Please upgrade RustDesk Server Pro to version {} or newer!"),
("pull_group_failed_tip", "Failed to refresh group"), ("pull_group_failed_tip", "Failed to refresh group"),
("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
("switch_display_elevated_connections_tip", "Switching to non-primary display is not supported in the elevated mode when there are multiple connections. Please try again after installation if you want to control multiple displays."),
("display_is_plugged_out_msg", "The diplay is plugged out, switch to the first display."),
("elevated_switch_display_msg", "Switch to the primary display because multiple display is not supported in elevated mode."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "Filter berdasarkan interseksi"), ("Filter by intersection", "Filter berdasarkan interseksi"),
("Remove wallpaper during incoming sessions", "Hilangkan latar dinding ketika ada sesi yang masuk"), ("Remove wallpaper during incoming sessions", "Hilangkan latar dinding ketika ada sesi yang masuk"),
("Test", "Tes"), ("Test", "Tes"),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -559,5 +559,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "Filtra per incrocio"), ("Filter by intersection", "Filtra per incrocio"),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "Filtrēt pēc krustpunkta"), ("Filter by intersection", "Filtrēt pēc krustpunkta"),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -556,7 +556,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("upgrade_rustdesk_server_pro_to_{}_tip", "Обновите RustDesk Server Pro до версии {} или новее!"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Обновите RustDesk Server Pro до версии {} или новее!"),
("pull_group_failed_tip", "Невозможно обновить группу"), ("pull_group_failed_tip", "Невозможно обновить группу"),
("Filter by intersection", "Фильтровать по пересечению"), ("Filter by intersection", "Фильтровать по пересечению"),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", "Удалить обои в сеансе"),
("Test", ""), ("Test", "Тест"),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""), ("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", ""),
("Test", ""), ("Test", ""),
("switch_display_elevated_connections_tip", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("elevated_switch_display_msg", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -1341,6 +1341,14 @@ impl WallPaperRemover {
old_path_dark, old_path_dark,
}) })
} }
pub fn support() -> bool {
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
if wallpaper::gnome::is_compliant(&desktop) || desktop.as_str() == "XFCE" {
return wallpaper::get().is_ok();
}
false
}
} }
impl Drop for WallPaperRemover { impl Drop for WallPaperRemover {

View File

@ -83,6 +83,7 @@ pub const PA_SAMPLE_RATE: u32 = 48000;
pub(crate) struct InstallingService; // please use new pub(crate) struct InstallingService; // please use new
impl InstallingService { impl InstallingService {
#[cfg(any(target_os = "windows", target_os = "linux"))]
pub fn new() -> Self { pub fn new() -> Self {
*INSTALLING_SERVICE.lock().unwrap() = true; *INSTALLING_SERVICE.lock().unwrap() = true;
Self Self

View File

@ -2392,6 +2392,10 @@ impl WallPaperRemover {
Ok(Self { old_path }) Ok(Self { old_path })
} }
pub fn support() -> bool {
wallpaper::get().is_ok() || !Self::get_recent_wallpaper().unwrap_or_default().is_empty()
}
fn get_recent_wallpaper() -> ResultType<String> { fn get_recent_wallpaper() -> ResultType<String> {
// SystemParametersInfoW may return %appdata%\Microsoft\Windows\Themes\TranscodedWallpaper, not real path and may not real cache // SystemParametersInfoW may return %appdata%\Microsoft\Windows\Themes\TranscodedWallpaper, not real path and may not real cache
// https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/ // https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/

View File

@ -26,7 +26,7 @@ use hbb_common::{
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use service::ServiceTmpl; use service::ServiceTmpl;
use service::{GenericService, Service, Subscriber}; use service::{EmptyExtraFieldService, GenericService, Service, Subscriber};
use crate::ipc::Data; use crate::ipc::Data;
@ -53,6 +53,7 @@ pub const NAME_POS: &'static str = "";
} }
mod connection; mod connection;
pub mod display_service;
#[cfg(windows)] #[cfg(windows)]
pub mod portable_service; pub mod portable_service;
mod service; mod service;
@ -80,7 +81,7 @@ lazy_static::lazy_static! {
pub struct Server { pub struct Server {
connections: ConnMap, connections: ConnMap,
services: HashMap<&'static str, Box<dyn Service>>, services: HashMap<String, Box<dyn Service>>,
id_count: i32, id_count: i32,
} }
@ -94,11 +95,15 @@ pub fn new() -> ServerPtr {
id_count: hbb_common::rand::random::<i32>() % 1000 + 1000, // ensure positive id_count: hbb_common::rand::random::<i32>() % 1000 + 1000, // ensure positive
}; };
server.add_service(Box::new(audio_service::new())); server.add_service(Box::new(audio_service::new()));
server.add_service(Box::new(video_service::new())); #[cfg(not(any(target_os = "android", target_os = "ios")))]
server.add_service(Box::new(display_service::new()));
server.add_service(Box::new(video_service::new(
*display_service::PRIMARY_DISPLAY_IDX,
)));
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
server.add_service(Box::new(clipboard_service::new())); server.add_service(Box::new(clipboard_service::new()));
if !video_service::capture_cursor_embedded() { if !display_service::capture_cursor_embedded() {
server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_cursor()));
server.add_service(Box::new(input_service::new_pos())); server.add_service(Box::new(input_service::new_pos()));
} }
@ -253,9 +258,19 @@ async fn create_relay_connection_(
} }
impl Server { impl Server {
fn is_video_service_name(name: &str) -> bool {
name.starts_with(video_service::NAME)
}
pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) {
let primary_video_service_name =
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX);
for s in self.services.values() { for s in self.services.values() {
if !noperms.contains(&s.name()) { let name = s.name();
if Self::is_video_service_name(&name) && name != primary_video_service_name {
continue;
}
if !noperms.contains(&(&name as _)) {
s.on_subscribe(conn.clone()); s.on_subscribe(conn.clone());
} }
} }
@ -287,8 +302,12 @@ impl Server {
self.services.insert(name, service); self.services.insert(name, service);
} }
pub fn contains(&self, name: &str) -> bool {
self.services.contains_key(name)
}
pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) { pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) {
if let Some(s) = self.services.get(&name) { if let Some(s) = self.services.get(name) {
if s.is_subed(conn.id()) == sub { if s.is_subed(conn.id()) == sub {
return; return;
} }
@ -305,6 +324,47 @@ impl Server {
self.id_count += 1; self.id_count += 1;
self.id_count self.id_count
} }
pub fn set_video_service_opt(&self, display: Option<usize>, opt: &str, value: &str) {
for (k, v) in self.services.iter() {
if let Some(display) = display {
if k != &video_service::get_service_name(display) {
continue;
}
}
if Self::is_video_service_name(k) {
v.set_option(opt, value);
}
}
}
fn capture_displays(
&mut self,
conn: ConnInner,
displays: &[usize],
include: bool,
exclude: bool,
) {
let displays = displays
.iter()
.map(|d| video_service::get_service_name(*d))
.collect::<Vec<_>>();
let keys = self.services.keys().cloned().collect::<Vec<_>>();
for name in keys.iter() {
if Self::is_video_service_name(&name) {
if displays.contains(&name) {
if include {
self.subscribe(&name, conn.clone(), true);
}
} else {
if exclude {
self.subscribe(&name, conn.clone(), false);
}
}
}
}
}
} }
impl Drop for Server { impl Drop for Server {

View File

@ -24,16 +24,16 @@ static RESTARTING: AtomicBool = AtomicBool::new(false);
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn new() -> GenericService { pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true); let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
sp.repeat::<cpal_impl::State, _>(33, cpal_impl::run); GenericService::repeat::<cpal_impl::State, _, _>(&svc.clone(), 33, cpal_impl::run);
sp svc.sp
} }
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
pub fn new() -> GenericService { pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true); let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
sp.run(pa_impl::run); GenericService::run(&svc.clone(), pa_impl::run);
sp svc.sp
} }
pub fn restart() { pub fn restart() {
@ -48,7 +48,7 @@ pub fn restart() {
mod pa_impl { mod pa_impl {
use super::*; use super::*;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
pub async fn run(sp: GenericService) -> ResultType<()> { pub async fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc
RESTARTING.store(false, Ordering::SeqCst); RESTARTING.store(false, Ordering::SeqCst);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -125,7 +125,7 @@ mod cpal_impl {
} }
} }
pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> { pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
sp.snapshot(|sps| { sp.snapshot(|sps| {
match &state.stream { match &state.stream {
None => { None => {

View File

@ -28,12 +28,12 @@ impl super::service::Reset for State {
} }
pub fn new() -> GenericService { pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true); let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
sp.repeat::<State, _>(INTERVAL, run); GenericService::repeat::<State, _, _>(&svc.clone(), INTERVAL, run);
sp svc.sp
} }
fn run(sp: GenericService, state: &mut State) -> ResultType<()> { fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
if let Some(ctx) = state.ctx.as_mut() { if let Some(ctx) = state.ctx.as_mut() {
if let Some(msg) = check_clipboard(ctx, None) { if let Some(msg) = check_clipboard(ctx, None) {
sp.send(msg); sp.send(msg);

View File

@ -15,7 +15,7 @@ use crate::{
new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender,
}, },
common::{get_default_sound_input, set_sound_input}, common::{get_default_sound_input, set_sound_input},
video_service, display_service, video_service,
}; };
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
@ -160,6 +160,7 @@ pub enum AuthConnType {
pub struct Connection { pub struct Connection {
inner: ConnInner, inner: ConnInner,
display_idx: usize,
stream: super::Stream, stream: super::Stream,
server: super::ServerPtrWeak, server: super::ServerPtrWeak,
hash: Hash, hash: Hash,
@ -303,6 +304,7 @@ impl Connection {
tx: Some(tx), tx: Some(tx),
tx_video: Some(tx_video), tx_video: Some(tx_video),
}, },
display_idx: *display_service::PRIMARY_DISPLAY_IDX,
stream, stream,
server, server,
hash, hash,
@ -609,6 +611,9 @@ impl Connection {
_ => {}, _ => {},
} }
} }
Some(message::Union::PeerInfo(_)) => {
conn.refresh_video_display(None);
}
_ => {} _ => {}
} }
if let Err(err) = conn.stream.send(msg).await { if let Err(err) = conn.stream.send(msg).await {
@ -1098,19 +1103,20 @@ impl Connection {
pi.username = username; pi.username = username;
pi.sas_enabled = sas_enabled; pi.sas_enabled = sas_enabled;
pi.features = Some(Features { pi.features = Some(Features {
privacy_mode: video_service::is_privacy_mode_supported(), privacy_mode: display_service::is_privacy_mode_supported(),
..Default::default() ..Default::default()
}) })
.into(); .into();
// `try_reset_current_display` is needed because `get_displays` may change the current display,
// which may cause the mismatch of current display and the current display name.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
video_service::try_reset_current_display();
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
pi.resolutions = Some(SupportedResolutions { pi.resolutions = Some(SupportedResolutions {
resolutions: video_service::get_current_display() resolutions: display_service::try_get_displays()
.map(|(_, _, d)| crate::platform::resolutions(&d.name())) .map(|displays| {
displays
.get(self.display_idx)
.map(|d| crate::platform::resolutions(&d.name()))
.unwrap_or(vec![])
})
.unwrap_or(vec![]), .unwrap_or(vec![]),
..Default::default() ..Default::default()
}) })
@ -1126,16 +1132,15 @@ impl Connection {
self.send(msg_out).await; self.send(msg_out).await;
} }
match super::video_service::get_displays().await { match super::display_service::get_displays().await {
Err(err) => { Err(err) => {
res.set_error(format!("{}", err)); res.set_error(format!("{}", err));
} }
Ok((current, displays)) => { Ok(displays) => {
pi.displays = displays.clone(); pi.displays = displays.clone();
pi.current_display = current as _; pi.current_display = self.display_idx as _;
res.set_peer_info(pi); res.set_peer_info(pi);
sub_service = true; sub_service = true;
*super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays;
} }
} }
Self::on_remote_authorized(); Self::on_remote_authorized();
@ -1289,6 +1294,8 @@ impl Connection {
#[inline] #[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn input_key(&self, msg: KeyEvent, press: bool) { fn input_key(&self, msg: KeyEvent, press: bool) {
// to-do: if is the legacy mode, and the key is function key "LockScreen".
// Switch to the primary display.
self.tx_input.send(MessageInput::Key((msg, press))).ok(); self.tx_input.send(MessageInput::Key((msg, press))).ok();
} }
@ -1945,15 +1952,13 @@ impl Connection {
}, },
Some(message::Union::Misc(misc)) => match misc.union { Some(message::Union::Misc(misc)) => match misc.union {
Some(misc::Union::SwitchDisplay(s)) => { Some(misc::Union::SwitchDisplay(s)) => {
video_service::switch_display(s.display).await; self.handle_switch_display(s).await;
#[cfg(not(any(target_os = "android", target_os = "ios")))] }
if s.width != 0 && s.height != 0 { Some(misc::Union::CaptureDisplays(displays)) => {
self.change_resolution(&Resolution { let add = displays.add.iter().map(|d| *d as usize).collect::<Vec<_>>();
width: s.width, let sub = displays.sub.iter().map(|d| *d as usize).collect::<Vec<_>>();
height: s.height, let set = displays.set.iter().map(|d| *d as usize).collect::<Vec<_>>();
..Default::default() self.capture_displays(&add, &sub, &set).await;
});
}
} }
Some(misc::Union::ChatMessage(c)) => { Some(misc::Union::ChatMessage(c)) => {
self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
@ -1965,10 +1970,16 @@ impl Connection {
} }
Some(misc::Union::RefreshVideo(r)) => { Some(misc::Union::RefreshVideo(r)) => {
if r { if r {
super::video_service::refresh(); // Refresh all videos.
// Compatibility with old versions and sciter(remote).
self.refresh_video_display(None);
} }
self.update_auto_disconnect_timer(); self.update_auto_disconnect_timer();
} }
Some(misc::Union::RefreshVideoDisplay(display)) => {
self.refresh_video_display(Some(display as usize));
self.update_auto_disconnect_timer();
}
Some(misc::Union::VideoReceived(_)) => { Some(misc::Union::VideoReceived(_)) => {
video_service::notify_video_frame_fetched( video_service::notify_video_frame_fetched(
self.inner.id, self.inner.id,
@ -2084,6 +2095,75 @@ impl Connection {
true true
} }
fn refresh_video_display(&self, display: Option<usize>) {
video_service::refresh();
self.server.upgrade().map(|s| {
s.read().unwrap().set_video_service_opt(
display,
video_service::OPTION_REFRESH,
super::service::SERVICE_OPTION_VALUE_TRUE,
)
});
}
async fn handle_switch_display(&mut self, s: SwitchDisplay) {
#[cfg(windows)]
if portable_client::running()
&& *CONN_COUNT.lock().unwrap() > 1
&& s.display != (*display_service::PRIMARY_DISPLAY_IDX as i32)
{
log::info!("Switch to non-primary display is not supported in the elevated mode when there are multiple connections.");
let mut msg_out = Message::new();
let res = MessageBox {
msgtype: "nook-nocancel-hasclose".to_owned(),
title: "Prompt".to_owned(),
text: "switch_display_elevated_connections_tip".to_owned(),
link: "".to_owned(),
..Default::default()
};
msg_out.set_message_box(res);
self.send(msg_out).await;
return;
}
let display_idx = s.display as usize;
if self.display_idx != display_idx {
if let Some(server) = self.server.upgrade() {
self.switch_display_to(display_idx, server.clone());
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if s.width != 0 && s.height != 0 {
self.change_resolution(&Resolution {
width: s.width,
height: s.height,
..Default::default()
});
}
// send display changed message
if let Some(msg_out) =
video_service::make_display_changed_msg(self.display_idx, None)
{
self.send(msg_out).await;
}
}
}
}
fn switch_display_to(&mut self, display_idx: usize, server: Arc<RwLock<Server>>) {
let new_service_name = video_service::get_service_name(display_idx);
let old_service_name = video_service::get_service_name(self.display_idx);
let mut lock = server.write().unwrap();
if display_idx != *display_service::PRIMARY_DISPLAY_IDX {
if !lock.contains(&new_service_name) {
lock.add_service(Box::new(video_service::new(display_idx)));
}
}
lock.subscribe(&old_service_name, self.inner.clone(), false);
lock.subscribe(&new_service_name, self.inner.clone(), true);
self.display_idx = display_idx;
}
#[cfg(windows)] #[cfg(windows)]
async fn handle_elevation_request(&mut self, para: portable_client::StartPara) { async fn handle_elevation_request(&mut self, para: portable_client::StartPara) {
let mut err; let mut err;
@ -2106,36 +2186,64 @@ impl Connection {
self.update_auto_disconnect_timer(); self.update_auto_disconnect_timer();
} }
async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) {
if let Some(sever) = self.server.upgrade() {
let mut lock = sever.write().unwrap();
for display in add.iter() {
let service_name = video_service::get_service_name(*display);
if !lock.contains(&service_name) {
lock.add_service(Box::new(video_service::new(*display)));
}
}
for display in set.iter() {
let service_name = video_service::get_service_name(*display);
if !lock.contains(&service_name) {
lock.add_service(Box::new(video_service::new(*display)));
}
}
if !add.is_empty() {
lock.capture_displays(self.inner.clone(), add, true, false);
} else if !sub.is_empty() {
lock.capture_displays(self.inner.clone(), sub, false, true);
} else {
lock.capture_displays(self.inner.clone(), set, true, true);
}
drop(lock);
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
fn change_resolution(&mut self, r: &Resolution) { fn change_resolution(&mut self, r: &Resolution) {
if self.keyboard { if self.keyboard {
if let Ok((_, _, display)) = video_service::get_current_display() { if let Ok(displays) = display_service::try_get_displays() {
let name = display.name(); if let Some(display) = displays.get(self.display_idx) {
#[cfg(all(windows, feature = "virtual_display_driver"))] let name = display.name();
if let Some(_ok) = #[cfg(all(windows, feature = "virtual_display_driver"))]
crate::virtual_display_manager::change_resolution_if_is_virtual_display( if let Some(_ok) =
crate::virtual_display_manager::change_resolution_if_is_virtual_display(
&name,
r.width as _,
r.height as _,
)
{
return;
}
display_service::set_last_changed_resolution(
&name, &name,
r.width as _, (display.width() as _, display.height() as _),
r.height as _, (r.width, r.height),
)
{
return;
}
video_service::set_last_changed_resolution(
&name,
(display.width() as _, display.height() as _),
(r.width, r.height),
);
if let Err(e) =
crate::platform::change_resolution(&name, r.width as _, r.height as _)
{
log::error!(
"Failed to change resolution '{}' to ({},{}):{:?}",
&name,
r.width,
r.height,
e
); );
if let Err(e) =
crate::platform::change_resolution(&name, r.width as _, r.height as _)
{
log::error!(
"Failed to change resolution '{}' to ({},{}):{:?}",
&name,
r.width,
r.height,
e
);
}
} }
} }
} }
@ -2281,7 +2389,7 @@ impl Connection {
if self.keyboard { if self.keyboard {
match q { match q {
BoolOption::Yes => { BoolOption::Yes => {
let msg_out = if !video_service::is_privacy_mode_supported() { let msg_out = if !display_service::is_privacy_mode_supported() {
crate::common::make_privacy_mode_msg_with_details( crate::common::make_privacy_mode_msg_with_details(
back_notification::PrivacyModeState::PrvNotSupported, back_notification::PrivacyModeState::PrvNotSupported,
"Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(),
@ -2289,8 +2397,11 @@ impl Connection {
} else { } else {
match privacy_mode::turn_on_privacy(self.inner.id) { match privacy_mode::turn_on_privacy(self.inner.id) {
Ok(true) => { Ok(true) => {
let err_msg = let err_msg = video_service::test_create_capturer(
video_service::test_create_capturer(self.inner.id, 5_000); self.inner.id,
self.display_idx,
5_000,
);
if err_msg.is_empty() { if err_msg.is_empty() {
video_service::set_privacy_mode_conn_id(self.inner.id); video_service::set_privacy_mode_conn_id(self.inner.id);
crate::common::make_privacy_mode_msg( crate::common::make_privacy_mode_msg(
@ -2326,7 +2437,7 @@ impl Connection {
self.send(msg_out).await; self.send(msg_out).await;
} }
BoolOption::No => { BoolOption::No => {
let msg_out = if !video_service::is_privacy_mode_supported() { let msg_out = if !display_service::is_privacy_mode_supported() {
crate::common::make_privacy_mode_msg_with_details( crate::common::make_privacy_mode_msg_with_details(
back_notification::PrivacyModeState::PrvNotSupported, back_notification::PrivacyModeState::PrvNotSupported,
"Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(),
@ -2801,11 +2912,11 @@ mod raii {
active_conns_lock.retain(|&c| c != self.0); active_conns_lock.retain(|&c| c != self.0);
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if active_conns_lock.is_empty() { if active_conns_lock.is_empty() {
video_service::reset_resolutions(); display_service::reset_resolutions();
} }
#[cfg(all(windows, feature = "virtual_display_driver"))] #[cfg(all(windows, feature = "virtual_display_driver"))]
if active_conns_lock.is_empty() { if active_conns_lock.is_empty() {
video_service::try_plug_out_virtual_display(); display_service::try_plug_out_virtual_display();
} }
#[cfg(all(windows))] #[cfg(all(windows))]
if active_conns_lock.is_empty() { if active_conns_lock.is_empty() {

View File

@ -0,0 +1,262 @@
use super::*;
#[cfg(all(windows, feature = "virtual_display_driver"))]
use crate::virtual_display_manager;
#[cfg(windows)]
use hbb_common::get_version_number;
use hbb_common::protobuf::MessageField;
use scrap::Display;
pub const NAME: &'static str = "display";
struct ChangedResolution {
original: (i32, i32),
changed: (i32, i32),
}
lazy_static::lazy_static! {
static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported();
static ref CHANGED_RESOLUTIONS: Arc<RwLock<HashMap<String, ChangedResolution>>> = Default::default();
// Initial primary display index.
// It should only be updated when the rustdesk server is started, and should not be updated when displays changed.
pub static ref PRIMARY_DISPLAY_IDX: usize = get_primary();
}
#[inline]
pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) {
let mut lock = CHANGED_RESOLUTIONS.write().unwrap();
match lock.get_mut(display_name) {
Some(res) => res.changed = changed,
None => {
lock.insert(
display_name.to_owned(),
ChangedResolution { original, changed },
);
}
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn reset_resolutions() {
for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() {
let (w, h) = res.original;
if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) {
log::error!(
"Failed to reset resolution of display '{}' to ({},{}): {}",
name,
w,
h,
e
);
}
}
}
#[inline]
fn is_capturer_mag_supported() -> bool {
#[cfg(windows)]
return scrap::CapturerMag::is_supported();
#[cfg(not(windows))]
false
}
#[inline]
pub fn capture_cursor_embedded() -> bool {
scrap::is_cursor_embedded()
}
#[inline]
pub fn is_privacy_mode_supported() -> bool {
#[cfg(windows)]
return *IS_CAPTURER_MAGNIFIER_SUPPORTED
&& get_version_number(&crate::VERSION) > get_version_number("1.1.9");
#[cfg(not(windows))]
return false;
}
#[derive(Default)]
struct StateDisplay {
synced_displays: Vec<DisplayInfo>,
}
impl super::service::Reset for StateDisplay {
fn reset(&mut self) {
self.synced_displays.clear();
}
}
pub fn new() -> GenericService {
let svc = EmptyExtraFieldService::new(NAME.to_owned(), false);
GenericService::repeat::<StateDisplay, _, _>(&svc.clone(), 300, run);
svc.sp
}
fn check_get_displays_changed_msg(last_synced_displays: &mut Vec<DisplayInfo>) -> Option<Message> {
let displays = try_get_displays().ok()?;
if displays.len() == last_synced_displays.len() {
return None;
}
// Display to DisplayInfo
let displays = to_display_info(&displays);
if last_synced_displays.len() == 0 {
*last_synced_displays = displays;
None
} else {
let mut pi = PeerInfo {
..Default::default()
};
pi.displays = displays.clone();
pi.current_display = 0;
let mut msg_out = Message::new();
msg_out.set_peer_info(pi);
*last_synced_displays = displays;
Some(msg_out)
}
}
#[cfg(all(windows, feature = "virtual_display_driver"))]
pub fn try_plug_out_virtual_display() {
let _res = virtual_display_manager::plug_out_headless();
}
fn run(sp: EmptyExtraFieldService, state: &mut StateDisplay) -> ResultType<()> {
if let Some(msg_out) = check_get_displays_changed_msg(&mut state.synced_displays) {
sp.send(msg_out);
log::info!("Displays changed");
}
Ok(())
}
#[inline]
pub(super) fn get_original_resolution(
display_name: &str,
w: usize,
h: usize,
) -> MessageField<Resolution> {
#[cfg(all(windows, feature = "virtual_display_driver"))]
let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name);
#[cfg(not(all(windows, feature = "virtual_display_driver")))]
let is_virtual_display = false;
Some(if is_virtual_display {
Resolution {
width: 0,
height: 0,
..Default::default()
}
} else {
let mut changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap();
let (width, height) = match changed_resolutions.get(display_name) {
Some(res) => {
if res.changed.0 != w as i32 || res.changed.1 != h as i32 {
// If the resolution is changed by third process, remove the record in changed_resolutions.
changed_resolutions.remove(display_name);
(w as _, h as _)
} else {
res.original
}
}
None => (w as _, h as _),
};
Resolution {
width,
height,
..Default::default()
}
})
.into()
}
pub fn to_display_info(all: &Vec<Display>) -> Vec<DisplayInfo> {
all.iter()
.map(|d| {
let display_name = d.name();
let original_resolution = get_original_resolution(&display_name, d.width(), d.height());
DisplayInfo {
x: d.origin().0 as _,
y: d.origin().1 as _,
width: d.width() as _,
height: d.height() as _,
name: display_name,
online: d.is_online(),
cursor_embedded: false,
original_resolution,
..Default::default()
}
})
.collect::<Vec<DisplayInfo>>()
}
pub fn is_inited_msg() -> Option<Message> {
#[cfg(target_os = "linux")]
if !scrap::is_x11() {
return super::wayland::is_inited();
}
None
}
pub async fn get_displays() -> ResultType<Vec<DisplayInfo>> {
#[cfg(target_os = "linux")]
{
if !scrap::is_x11() {
return super::wayland::get_displays().await;
}
}
Ok(to_display_info(&try_get_displays()?))
}
#[inline]
pub fn get_primary() -> usize {
#[cfg(target_os = "linux")]
{
if !scrap::is_x11() {
return match super::wayland::get_primary() {
Ok(n) => n,
Err(_) => 0,
};
}
}
try_get_displays().map(|d| get_primary_2(&d)).unwrap_or(0)
}
#[inline]
pub fn get_primary_2(all: &Vec<Display>) -> usize {
all.iter().position(|d| d.is_primary()).unwrap_or(0)
}
#[inline]
#[cfg(all(windows, feature = "virtual_display_driver"))]
fn no_displays(displays: &Vec<Display>) -> bool {
let display_len = displays.len();
if display_len == 0 {
true
} else if display_len == 1 {
let display = &displays[0];
let dummy_display_side_max_size = 800;
display.width() <= dummy_display_side_max_size
&& display.height() <= dummy_display_side_max_size
} else {
false
}
}
#[inline]
#[cfg(not(all(windows, feature = "virtual_display_driver")))]
pub fn try_get_displays() -> ResultType<Vec<Display>> {
Ok(Display::all()?)
}
#[cfg(all(windows, feature = "virtual_display_driver"))]
pub fn try_get_displays() -> ResultType<Vec<Display>> {
let mut displays = Display::all()?;
if no_displays(&displays) {
log::debug!("no displays, create virtual display");
if let Err(e) = virtual_display_manager::plug_in_headless() {
log::error!("plug in headless failed {}", e);
} else {
displays = Display::all()?;
}
}
Ok(displays)
}

View File

@ -17,7 +17,7 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey};
use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput};
use std::{ use std::{
convert::TryFrom, convert::TryFrom,
ops::Sub, ops::{Deref, DerefMut, Sub},
sync::atomic::{AtomicBool, Ordering}, sync::atomic::{AtomicBool, Ordering},
thread, thread,
time::{self, Duration, Instant}, time::{self, Duration, Instant},
@ -236,18 +236,43 @@ fn should_disable_numlock(evt: &KeyEvent) -> bool {
pub const NAME_CURSOR: &'static str = "mouse_cursor"; pub const NAME_CURSOR: &'static str = "mouse_cursor";
pub const NAME_POS: &'static str = "mouse_pos"; pub const NAME_POS: &'static str = "mouse_pos";
pub type MouseCursorService = ServiceTmpl<MouseCursorSub>; #[derive(Clone)]
pub struct MouseCursorService {
pub sp: ServiceTmpl<MouseCursorSub>,
}
pub fn new_cursor() -> MouseCursorService { impl Deref for MouseCursorService {
let sp = MouseCursorService::new(NAME_CURSOR, true); type Target = ServiceTmpl<MouseCursorSub>;
sp.repeat::<StateCursor, _>(33, run_cursor);
sp fn deref(&self) -> &Self::Target {
&self.sp
}
}
impl DerefMut for MouseCursorService {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sp
}
}
impl MouseCursorService {
pub fn new(name: String, need_snapshot: bool) -> Self {
Self {
sp: ServiceTmpl::<MouseCursorSub>::new(name, need_snapshot),
}
}
}
pub fn new_cursor() -> ServiceTmpl<MouseCursorSub> {
let svc = MouseCursorService::new(NAME_CURSOR.to_owned(), true);
ServiceTmpl::<MouseCursorSub>::repeat::<StateCursor, _, _>(&svc.clone(), 33, run_cursor);
svc.sp
} }
pub fn new_pos() -> GenericService { pub fn new_pos() -> GenericService {
let sp = GenericService::new(NAME_POS, false); let svc = EmptyExtraFieldService::new(NAME_POS.to_owned(), false);
sp.repeat::<StatePos, _>(33, run_pos); GenericService::repeat::<StatePos, _, _>(&svc.clone(), 33, run_pos);
sp svc.sp
} }
#[inline] #[inline]
@ -258,7 +283,7 @@ fn update_last_cursor_pos(x: i32, y: i32) {
} }
} }
fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { fn run_pos(sp: EmptyExtraFieldService, state: &mut StatePos) -> ResultType<()> {
let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap(); let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap();
if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS { if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS {
return Ok(()); return Ok(());
@ -988,7 +1013,6 @@ pub async fn lock_screen() {
crate::platform::lock_screen(); crate::platform::lock_screen();
} }
} }
super::video_service::switch_to_primary().await;
} }
pub fn handle_key(evt: &KeyEvent) { pub fn handle_key(evt: &KeyEvent) {

View File

@ -25,7 +25,6 @@ use winapi::{
use crate::{ use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService}, ipc::{self, new_listener, Connection, Data, DataPortableService},
platform::set_path_permission, platform::set_path_permission,
video_service::get_current_display,
}; };
use super::video_qos; use super::video_qos;
@ -224,6 +223,8 @@ mod utils {
pub mod server { pub mod server {
use hbb_common::message_proto::PointerDeviceEvent; use hbb_common::message_proto::PointerDeviceEvent;
use crate::display_service;
use super::*; use super::*;
lazy_static::lazy_static! { lazy_static::lazy_static! {
@ -324,12 +325,17 @@ pub mod server {
continue; continue;
} }
if c.is_none() { if c.is_none() {
*crate::video_service::CURRENT_DISPLAY.lock().unwrap() = current_display; let Ok(mut displays) = display_service::try_get_displays() else {
let Ok((_, _current, display)) = get_current_display() else { log::error!("Failed to get displays");
log::error!("Failed to get current display");
*EXIT.lock().unwrap() = true; *EXIT.lock().unwrap() = true;
return; return;
}; };
if displays.len() <= current_display {
log::error!("Invalid display index:{}", current_display);
*EXIT.lock().unwrap() = true;
return;
}
let display = displays.remove(current_display);
display_width = display.width(); display_width = display.width();
display_height = display.height(); display_height = display.height();
match Capturer::new(display, use_yuv) { match Capturer::new(display, use_yuv) {
@ -522,6 +528,8 @@ pub mod server {
pub mod client { pub mod client {
use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent}; use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent};
use crate::display_service;
use super::*; use super::*;
lazy_static::lazy_static! { lazy_static::lazy_static! {
@ -665,8 +673,8 @@ pub mod client {
shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE));
} }
let (mut width, mut height) = (0, 0); let (mut width, mut height) = (0, 0);
if let Ok((_, current, display)) = get_current_display() { if let Ok(displays) = display_service::try_get_displays() {
if current_display == current { if let Some(display) = displays.get(current_display) {
width = display.width(); width = display.width();
height = display.height(); height = display.height();
} }
@ -910,7 +918,15 @@ pub mod client {
} }
if portable_service_running { if portable_service_running {
log::info!("Create shared memory capturer"); log::info!("Create shared memory capturer");
return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); if current_display == *display_service::PRIMARY_DISPLAY_IDX {
return Ok(Box::new(CapturerPortable::new(current_display, use_yuv)));
} else {
bail!(
"Ignore capture display index: {}, the primary display index is: {}",
current_display,
*display_service::PRIMARY_DISPLAY_IDX
);
}
} else { } else {
log::debug!("Create capturer dxgi|gdi"); log::debug!("Create capturer dxgi|gdi");
return Ok(Box::new( return Ok(Box::new(

View File

@ -1,16 +1,19 @@
use super::*; use super::*;
use std::{ use std::{
collections::HashSet, collections::HashSet,
ops::{Deref, DerefMut},
thread::{self, JoinHandle}, thread::{self, JoinHandle},
time, time,
}; };
pub trait Service: Send + Sync { pub trait Service: Send + Sync {
fn name(&self) -> &'static str; fn name(&self) -> String;
fn on_subscribe(&self, sub: ConnInner); fn on_subscribe(&self, sub: ConnInner);
fn on_unsubscribe(&self, id: i32); fn on_unsubscribe(&self, id: i32);
fn is_subed(&self, id: i32) -> bool; fn is_subed(&self, id: i32) -> bool;
fn join(&self); fn join(&self);
fn get_option(&self, opt: &str) -> Option<String>;
fn set_option(&self, opt: &str, val: &str) -> Option<String>;
} }
pub trait Subscriber: Default + Send + Sync + 'static { pub trait Subscriber: Default + Send + Sync + 'static {
@ -20,12 +23,13 @@ pub trait Subscriber: Default + Send + Sync + 'static {
#[derive(Default)] #[derive(Default)]
pub struct ServiceInner<T: Subscriber + From<ConnInner>> { pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
name: &'static str, name: String,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
subscribes: HashMap<i32, T>, subscribes: HashMap<i32, T>,
new_subscribes: HashMap<i32, T>, new_subscribes: HashMap<i32, T>,
active: bool, active: bool,
need_snapshot: bool, need_snapshot: bool,
options: HashMap<String, String>,
} }
pub trait Reset { pub trait Reset {
@ -37,6 +41,35 @@ pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
pub type GenericService = ServiceTmpl<ConnInner>; pub type GenericService = ServiceTmpl<ConnInner>;
pub const HIBERNATE_TIMEOUT: u64 = 30; pub const HIBERNATE_TIMEOUT: u64 = 30;
pub const MAX_ERROR_TIMEOUT: u64 = 1_000; pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
pub const SERVICE_OPTION_VALUE_TRUE: &str = "1";
pub const SERVICE_OPTION_VALUE_FALSE: &str = "0";
#[derive(Clone)]
pub struct EmptyExtraFieldService {
pub sp: GenericService,
}
impl Deref for EmptyExtraFieldService {
type Target = ServiceTmpl<ConnInner>;
fn deref(&self) -> &Self::Target {
&self.sp
}
}
impl DerefMut for EmptyExtraFieldService {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sp
}
}
impl EmptyExtraFieldService {
pub fn new(name: String, need_snapshot: bool) -> Self {
Self {
sp: GenericService::new(name, need_snapshot),
}
}
}
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> { impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
fn send_new_subscribes(&mut self, msg: Arc<Message>) { fn send_new_subscribes(&mut self, msg: Arc<Message>) {
@ -60,8 +93,8 @@ impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> { impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
#[inline] #[inline]
fn name(&self) -> &'static str { fn name(&self) -> String {
self.0.read().unwrap().name self.0.read().unwrap().name.clone()
} }
fn is_subed(&self, id: i32) -> bool { fn is_subed(&self, id: i32) -> bool {
@ -96,6 +129,18 @@ impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
} }
} }
} }
fn get_option(&self, opt: &str) -> Option<String> {
self.0.read().unwrap().options.get(opt).cloned()
}
fn set_option(&self, opt: &str, val: &str) -> Option<String> {
self.0
.write()
.unwrap()
.options
.insert(opt.to_string(), val.to_string())
}
} }
impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> { impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
@ -105,7 +150,7 @@ impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
} }
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> { impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
pub fn new(name: &'static str, need_snapshot: bool) -> Self { pub fn new(name: String, need_snapshot: bool) -> Self {
Self(Arc::new(RwLock::new(ServiceInner::<T> { Self(Arc::new(RwLock::new(ServiceInner::<T> {
name, name,
active: true, active: true,
@ -114,6 +159,21 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
}))) })))
} }
#[inline]
pub fn is_option_true(&self, opt: &str) -> bool {
self.get_option(opt)
.map_or(false, |v| v == SERVICE_OPTION_VALUE_TRUE)
}
#[inline]
pub fn set_option_bool(&self, opt: &str, val: bool) {
if val {
self.set_option(opt, SERVICE_OPTION_VALUE_TRUE);
} else {
self.set_option(opt, SERVICE_OPTION_VALUE_FALSE);
}
}
#[inline] #[inline]
pub fn has_subscribes(&self) -> bool { pub fn has_subscribes(&self) -> bool {
self.0.read().unwrap().has_subscribes() self.0.read().unwrap().has_subscribes()
@ -189,14 +249,15 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
} }
} }
pub fn repeat<S, F>(&self, interval_ms: u64, callback: F) pub fn repeat<S, F, Svc>(svc: &Svc, interval_ms: u64, callback: F)
where where
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send, F: 'static + FnMut(Svc, &mut S) -> ResultType<()> + Send,
S: 'static + Default + Reset, S: 'static + Default + Reset,
Svc: 'static + Clone + Send + DerefMut<Target = ServiceTmpl<T>>,
{ {
let interval = time::Duration::from_millis(interval_ms); let interval = time::Duration::from_millis(interval_ms);
let mut callback = callback; let mut callback = callback;
let sp = self.clone(); let sp = svc.clone();
let thread = thread::spawn(move || { let thread = thread::spawn(move || {
let mut state = S::default(); let mut state = S::default();
let mut may_reset = false; let mut may_reset = false;
@ -223,14 +284,15 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
} }
log::info!("Service {} exit", sp.name()); log::info!("Service {} exit", sp.name());
}); });
self.0.write().unwrap().handle = Some(thread); svc.0.write().unwrap().handle = Some(thread);
} }
pub fn run<F>(&self, callback: F) pub fn run<F, Svc>(svc: &Svc, callback: F)
where where
F: 'static + FnMut(Self) -> ResultType<()> + Send, F: 'static + FnMut(Svc) -> ResultType<()> + Send,
Svc: 'static + Clone + Send + DerefMut<Target = ServiceTmpl<T>>,
{ {
let sp = self.clone(); let sp = svc.clone();
let mut callback = callback; let mut callback = callback;
let thread = thread::spawn(move || { let thread = thread::spawn(move || {
let mut error_timeout = HIBERNATE_TIMEOUT; let mut error_timeout = HIBERNATE_TIMEOUT;
@ -259,7 +321,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
} }
log::info!("Service {} exit", sp.name()); log::info!("Service {} exit", sp.name());
}); });
self.0.write().unwrap().handle = Some(thread); svc.0.write().unwrap().handle = Some(thread);
} }
#[inline] #[inline]

View File

@ -18,15 +18,11 @@
// to-do: // to-do:
// https://slhck.info/video/2017/03/01/rate-control.html // https://slhck.info/video/2017/03/01/rate-control.html
use super::{video_qos::VideoQoS, *}; use super::{service::ServiceTmpl, video_qos::VideoQoS, *};
#[cfg(all(windows, feature = "virtual_display_driver"))]
use crate::virtual_display_manager;
#[cfg(windows)] #[cfg(windows)]
use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; use crate::{platform::windows::is_process_consent_running, privacy_win_mag};
#[cfg(windows)]
use hbb_common::get_version_number;
use hbb_common::{ use hbb_common::{
protobuf::MessageField, anyhow::anyhow,
tokio::sync::{ tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex, Mutex as TokioMutex,
@ -51,71 +47,18 @@ use std::{
}; };
pub const NAME: &'static str = "video"; pub const NAME: &'static str = "video";
pub const OPTION_DISPLAY_CHANGED: &'static str = "changed";
struct ChangedResolution { pub const OPTION_REFRESH: &'static str = "refresh";
original: (i32, i32),
changed: (i32, i32),
}
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
static ref SWITCH: Arc<Mutex<bool>> = Default::default();
static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option<Instant>)>, Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>) = { static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option<Instant>)>, Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>) = {
let (tx, rx) = unbounded_channel(); let (tx, rx) = unbounded_channel();
(tx, Arc::new(TokioMutex::new(rx))) (tx, Arc::new(TokioMutex::new(rx)))
}; };
static ref PRIVACY_MODE_CONN_ID: Mutex<i32> = Mutex::new(0); static ref PRIVACY_MODE_CONN_ID: Mutex<i32> = Mutex::new(0);
static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported();
pub static ref VIDEO_QOS: Arc<Mutex<VideoQoS>> = Default::default(); pub static ref VIDEO_QOS: Arc<Mutex<VideoQoS>> = Default::default();
pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default(); pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default();
pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default();
pub static ref LAST_SYNC_DISPLAYS: Arc<RwLock<Vec<DisplayInfo>>> = Default::default();
static ref CHANGED_RESOLUTIONS: Arc<RwLock<HashMap<String, ChangedResolution>>> = Default::default();
}
#[inline]
pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) {
let mut lock = CHANGED_RESOLUTIONS.write().unwrap();
match lock.get_mut(display_name) {
Some(res) => res.changed = changed,
None => {
lock.insert(
display_name.to_owned(),
ChangedResolution { original, changed },
);
}
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn reset_resolutions() {
for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() {
let (w, h) = res.original;
if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) {
log::error!(
"Failed to reset resolution of display '{}' to ({},{}): {}",
name,
w,
h,
e
);
}
}
}
#[inline]
fn is_capturer_mag_supported() -> bool {
#[cfg(windows)]
return scrap::CapturerMag::is_supported();
#[cfg(not(windows))]
false
}
#[inline]
pub fn capture_cursor_embedded() -> bool {
scrap::is_cursor_embedded()
} }
#[inline] #[inline]
@ -133,15 +76,6 @@ pub fn get_privacy_mode_conn_id() -> i32 {
*PRIVACY_MODE_CONN_ID.lock().unwrap() *PRIVACY_MODE_CONN_ID.lock().unwrap()
} }
#[inline]
pub fn is_privacy_mode_supported() -> bool {
#[cfg(windows)]
return *IS_CAPTURER_MAGNIFIER_SUPPORTED
&& get_version_number(&crate::VERSION) > get_version_number("1.1.9");
#[cfg(not(windows))]
return false;
}
struct VideoFrameController { struct VideoFrameController {
cur: Instant, cur: Instant,
send_conn_ids: HashSet<i32>, send_conn_ids: HashSet<i32>,
@ -192,10 +126,37 @@ impl VideoFrameController {
} }
} }
pub fn new() -> GenericService { #[derive(Clone)]
let sp = GenericService::new(NAME, true); pub struct VideoService {
sp.run(run); sp: GenericService,
sp idx: usize,
}
impl Deref for VideoService {
type Target = ServiceTmpl<ConnInner>;
fn deref(&self) -> &Self::Target {
&self.sp
}
}
impl DerefMut for VideoService {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sp
}
}
pub fn get_service_name(idx: usize) -> String {
format!("{}{}", NAME, idx)
}
pub fn new(idx: usize) -> GenericService {
let vs = VideoService {
sp: GenericService::new(get_service_name(idx), true),
idx,
};
GenericService::run(&vs, run);
vs.sp
} }
fn check_display_changed( fn check_display_changed(
@ -221,19 +182,10 @@ fn check_display_changed(
if n != last_n { if n != last_n {
return true; return true;
}; };
match displays.get(last_current) {
for (i, d) in displays.iter().enumerate() { Some(d) => d.width() != last_width || d.height() != last_height,
if d.is_primary() { None => true,
if i != last_current {
return true;
};
if d.width() != last_width || d.height() != last_height {
return true;
};
}
} }
return false;
} }
// Capturer object is expensive, avoiding to create it frequently. // Capturer object is expensive, avoiding to create it frequently.
@ -317,14 +269,27 @@ fn create_capturer(
} }
// This function works on privacy mode. Windows only for now. // This function works on privacy mode. Windows only for now.
pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> String { pub fn test_create_capturer(
privacy_mode_id: i32,
display_idx: usize,
timeout_millis: u64,
) -> String {
let test_begin = Instant::now(); let test_begin = Instant::now();
loop { loop {
let err = match get_current_display() { let err = match try_get_displays() {
Ok((_, current, display)) => { Ok(mut displays) => {
match create_capturer(privacy_mode_id, display, true, current, false) { if displays.len() <= display_idx {
Ok(_) => return "".to_owned(), anyhow!(
Err(e) => e, "Failed to get display {}, the displays' count is {}",
display_idx,
displays.len()
)
} else {
let display = displays.remove(display_idx);
match create_capturer(privacy_mode_id, display, true, display_idx, false) {
Ok(_) => return "".to_owned(),
Err(e) => e,
}
} }
} }
Err(e) => e, Err(e) => e,
@ -352,6 +317,7 @@ fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> Resu
} }
pub(super) struct CapturerInfo { pub(super) struct CapturerInfo {
pub name: String,
pub origin: (i32, i32), pub origin: (i32, i32),
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
@ -376,7 +342,11 @@ impl DerefMut for CapturerInfo {
} }
} }
fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<CapturerInfo> { fn get_capturer(
current: usize,
use_yuv: bool,
portable_service_running: bool,
) -> ResultType<CapturerInfo> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if !scrap::is_x11() { if !scrap::is_x11() {
@ -384,8 +354,18 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
} }
} }
let (ndisplay, current, display) = get_current_display()?; let mut displays = try_get_displays()?;
let ndisplay = displays.len();
if ndisplay <= current {
bail!(
"Failed to get display {}, displays len: {}",
current,
ndisplay
);
}
let display = displays.remove(current);
let (origin, width, height) = (display.origin(), display.width(), display.height()); let (origin, width, height) = (display.origin(), display.width(), display.height());
let name = display.name();
log::debug!( log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", "#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}",
ndisplay, ndisplay,
@ -395,7 +375,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
height, height,
num_cpus::get_physical(), num_cpus::get_physical(),
num_cpus::get(), num_cpus::get(),
display.name(), &name,
); );
let privacy_mode_id = *PRIVACY_MODE_CONN_ID.lock().unwrap(); let privacy_mode_id = *PRIVACY_MODE_CONN_ID.lock().unwrap();
@ -429,6 +409,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
portable_service_running, portable_service_running,
)?; )?;
Ok(CapturerInfo { Ok(CapturerInfo {
name,
origin, origin,
width, width,
height, height,
@ -440,42 +421,13 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
}) })
} }
fn check_displays_new() -> Option<Vec<Display>> { fn run(vs: VideoService) -> ResultType<()> {
let displays = try_get_displays().ok()?;
let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap();
if displays.len() != last_sync_displays.len() {
// No need to check if the resolutions are changed by third process.
Some(displays)
} else {
None
}
}
fn check_get_displays_changed_msg() -> Option<Message> {
let displays = check_displays_new()?;
// Display to DisplayInfo
let (current, displays) = get_displays_2(&displays);
let mut pi = PeerInfo {
..Default::default()
};
pi.displays = displays.clone();
pi.current_display = current as _;
let mut msg_out = Message::new();
msg_out.set_peer_info(pi);
*LAST_SYNC_DISPLAYS.write().unwrap() = displays;
Some(msg_out)
}
#[cfg(all(windows, feature = "virtual_display_driver"))]
pub fn try_plug_out_virtual_display() {
let _res = virtual_display_manager::plug_out_headless();
}
fn run(sp: GenericService) -> ResultType<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
let _wake_lock = get_wake_lock(); let _wake_lock = get_wake_lock();
// ensure_inited() is needed because clear() may be called. // ensure_inited() is needed because clear() may be called.
// to-do: wayland ensure_inited should pass current display index.
// But for now, we do not support multi-screen capture on wayland.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
super::wayland::ensure_inited()?; super::wayland::ensure_inited()?;
#[cfg(windows)] #[cfg(windows)]
@ -483,7 +435,9 @@ fn run(sp: GenericService) -> ResultType<()> {
#[cfg(not(windows))] #[cfg(not(windows))]
let last_portable_service_running = false; let last_portable_service_running = false;
let mut c = get_capturer(true, last_portable_service_running)?; let display_idx = vs.idx;
let sp = vs.sp;
let mut c = get_capturer(display_idx, true, last_portable_service_running)?;
let mut video_qos = VIDEO_QOS.lock().unwrap(); let mut video_qos = VIDEO_QOS.lock().unwrap();
video_qos.refresh(None); video_qos.refresh(None);
@ -506,37 +460,18 @@ fn run(sp: GenericService) -> ResultType<()> {
c.set_use_yuv(encoder.use_yuv()); c.set_use_yuv(encoder.use_yuv());
VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate());
if *SWITCH.lock().unwrap() { if sp.is_option_true(OPTION_DISPLAY_CHANGED) {
log::debug!("Broadcasting display switch"); log::debug!("Broadcasting display changed");
let mut misc = Misc::new(); broadcast_display_changed(
let display_name = get_current_display() display_idx,
.map(|(_, _, d)| d.name()) &sp,
.unwrap_or_default(); Some((c.name.clone(), c.origin.clone(), c.width, c.height)),
let original_resolution = get_original_resolution(&display_name, c.width, c.height); );
misc.set_switch_display(SwitchDisplay { sp.set_option_bool(OPTION_DISPLAY_CHANGED, false);
display: c.current as _, }
x: c.origin.0 as _,
y: c.origin.1 as _, if sp.is_option_true(OPTION_REFRESH) {
width: c.width as _, sp.set_option_bool(OPTION_REFRESH, false);
height: c.height as _,
cursor_embedded: capture_cursor_embedded(),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
resolutions: Some(SupportedResolutions {
resolutions: if display_name.is_empty() {
vec![]
} else {
crate::platform::resolutions(&display_name)
},
..SupportedResolutions::default()
})
.into(),
original_resolution,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_misc(misc);
*SWITCH.lock().unwrap() = false;
sp.send(msg_out);
} }
let mut frame_controller = VideoFrameController::new(); let mut frame_controller = VideoFrameController::new();
@ -572,13 +507,7 @@ fn run(sp: GenericService) -> ResultType<()> {
} }
drop(video_qos); drop(video_qos);
if *SWITCH.lock().unwrap() { if sp.is_option_true(OPTION_DISPLAY_CHANGED) || sp.is_option_true(OPTION_REFRESH) {
bail!("SWITCH");
}
if c.current != *CURRENT_DISPLAY.lock().unwrap() {
#[cfg(target_os = "linux")]
super::wayland::clear();
*SWITCH.lock().unwrap() = true;
bail!("SWITCH"); bail!("SWITCH");
} }
if codec_name != Encoder::negotiated_codec() { if codec_name != Encoder::negotiated_codec() {
@ -604,23 +533,12 @@ fn run(sp: GenericService) -> ResultType<()> {
// Capturer on macos does not return Err event the solution is changed. // Capturer on macos does not return Err event the solution is changed.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if check_display_changed(c.ndisplay, c.current, c.width, c.height) { if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
sp.set_option_bool(OPTION_DISPLAY_CHANGED, true);
log::info!("Displays changed"); log::info!("Displays changed");
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
}
if let Some(msg_out) = check_get_displays_changed_msg() {
sp.send(msg_out);
log::info!("Displays changed");
#[cfg(target_os = "linux")]
super::wayland::clear();
*SWITCH.lock().unwrap() = true;
bail!("SWITCH"); bail!("SWITCH");
} }
} }
*LAST_ACTIVE.lock().unwrap() = now;
frame_controller.reset(); frame_controller.reset();
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
@ -631,8 +549,14 @@ fn run(sp: GenericService) -> ResultType<()> {
match frame { match frame {
scrap::Frame::RAW(data) => { scrap::Frame::RAW(data) => {
if data.len() != 0 { if data.len() != 0 {
let send_conn_ids = let send_conn_ids = handle_one_frame(
handle_one_frame(&sp, data, ms, &mut encoder, recorder.clone())?; display_idx,
&sp,
data,
ms,
&mut encoder,
recorder.clone(),
)?;
frame_controller.set_send(now, send_conn_ids); frame_controller.set_send(now, send_conn_ids);
} }
} }
@ -649,7 +573,7 @@ fn run(sp: GenericService) -> ResultType<()> {
let time = now - start; let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
let send_conn_ids = let send_conn_ids =
handle_one_frame(&sp, &frame, ms, &mut encoder, recorder.clone())?; handle_one_frame(display_idx, &sp, &frame, ms, &mut encoder, recorder.clone())?;
frame_controller.set_send(now, send_conn_ids); frame_controller.set_send(now, send_conn_ids);
#[cfg(windows)] #[cfg(windows)]
{ {
@ -693,7 +617,7 @@ fn run(sp: GenericService) -> ResultType<()> {
log::info!("Displays changed"); log::info!("Displays changed");
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
super::wayland::clear(); super::wayland::clear();
*SWITCH.lock().unwrap() = true; sp.set_option_bool(OPTION_DISPLAY_CHANGED, true);
bail!("SWITCH"); bail!("SWITCH");
} }
@ -829,6 +753,7 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu
#[inline] #[inline]
fn handle_one_frame( fn handle_one_frame(
display: usize,
sp: &GenericService, sp: &GenericService,
frame: &[u8], frame: &[u8],
ms: i64, ms: i64,
@ -844,7 +769,10 @@ fn handle_one_frame(
})?; })?;
let mut send_conn_ids: HashSet<i32> = Default::default(); let mut send_conn_ids: HashSet<i32> = Default::default();
if let Ok(msg) = encoder.encode_to_message(frame, ms) { if let Ok(mut vf) = encoder.encode_to_message(frame, ms) {
vf.display = display as _;
let mut msg = Message::new();
msg.set_video_frame(vf);
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
recorder recorder
.lock() .lock()
@ -856,69 +784,6 @@ fn handle_one_frame(
Ok(send_conn_ids) Ok(send_conn_ids)
} }
#[inline]
fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageField<Resolution> {
#[cfg(all(windows, feature = "virtual_display_driver"))]
let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name);
#[cfg(not(all(windows, feature = "virtual_display_driver")))]
let is_virtual_display = false;
Some(if is_virtual_display {
Resolution {
width: 0,
height: 0,
..Default::default()
}
} else {
let mut changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap();
let (width, height) = match changed_resolutions.get(display_name) {
Some(res) => {
if res.changed.0 != w as i32 || res.changed.1 != h as i32 {
// If the resolution is changed by third process, remove the record in changed_resolutions.
changed_resolutions.remove(display_name);
(w as _, h as _)
} else {
res.original
}
}
None => (w as _, h as _),
};
Resolution {
width,
height,
..Default::default()
}
})
.into()
}
pub(super) fn get_displays_2(all: &Vec<Display>) -> (usize, Vec<DisplayInfo>) {
let mut displays = Vec::new();
let mut primary = 0;
for (i, d) in all.iter().enumerate() {
if d.is_primary() {
primary = i;
}
let display_name = d.name();
let original_resolution = get_original_resolution(&display_name, d.width(), d.height());
displays.push(DisplayInfo {
x: d.origin().0 as _,
y: d.origin().1 as _,
width: d.width() as _,
height: d.height() as _,
name: display_name,
online: d.is_online(),
cursor_embedded: false,
original_resolution,
..Default::default()
});
}
let mut lock = CURRENT_DISPLAY.lock().unwrap();
if *lock >= displays.len() {
*lock = primary
}
(*lock, displays)
}
pub fn is_inited_msg() -> Option<Message> { pub fn is_inited_msg() -> Option<Message> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !scrap::is_x11() { if !scrap::is_x11() {
@ -927,65 +792,10 @@ pub fn is_inited_msg() -> Option<Message> {
None None
} }
// switch to primary display if long time (30 seconds) no users
#[inline]
pub fn try_reset_current_display() {
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
}
*LAST_ACTIVE.lock().unwrap() = time::Instant::now();
}
pub async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
#[cfg(target_os = "linux")]
{
if !scrap::is_x11() {
return super::wayland::get_displays().await;
}
}
Ok(get_displays_2(&try_get_displays()?))
}
pub async fn switch_display(i: i32) {
let i = i as usize;
if let Ok((_, displays)) = get_displays().await {
if i < displays.len() {
*CURRENT_DISPLAY.lock().unwrap() = i;
}
}
}
#[inline] #[inline]
pub fn refresh() { pub fn refresh() {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
Display::refresh_size(); Display::refresh_size();
*SWITCH.lock().unwrap() = true;
}
fn get_primary() -> usize {
#[cfg(target_os = "linux")]
{
if !scrap::is_x11() {
return match super::wayland::get_primary() {
Ok(n) => n,
Err(_) => 0,
};
}
}
if let Ok(all) = try_get_displays() {
for (i, d) in all.iter().enumerate() {
if d.is_primary() {
return i;
}
}
}
0
}
#[inline]
pub async fn switch_to_primary() {
switch_display(get_primary() as _).await;
} }
#[inline] #[inline]
@ -1034,30 +844,6 @@ fn try_get_displays() -> ResultType<Vec<Display>> {
Ok(Display::all()?) Ok(Display::all()?)
} }
pub(super) fn get_current_display_2(mut all: Vec<Display>) -> ResultType<(usize, usize, Display)> {
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
if all.len() == 0 {
bail!("No displays");
}
let n = all.len();
if current >= n {
current = 0;
for (i, d) in all.iter().enumerate() {
if d.is_primary() {
current = i;
break;
}
}
*CURRENT_DISPLAY.lock().unwrap() = current;
}
return Ok((n, current, all.remove(current)));
}
#[inline]
pub fn get_current_display() -> ResultType<(usize, usize, Display)> {
get_current_display_2(try_get_displays()?)
}
#[cfg(windows)] #[cfg(windows)]
fn start_uac_elevation_check() { fn start_uac_elevation_check() {
static START: Once = Once::new(); static START: Once = Once::new();
@ -1090,3 +876,63 @@ fn get_wake_lock() -> crate::platform::WakeLock {
}; };
crate::platform::WakeLock::new(display, idle, sleep) crate::platform::WakeLock::new(display, idle, sleep)
} }
#[inline]
fn broadcast_display_changed(
display_idx: usize,
sp: &GenericService,
display_meta: Option<(String, (i32, i32), usize, usize)>,
) {
if let Some(msg_out) = make_display_changed_msg(display_idx, display_meta) {
sp.send(msg_out);
}
}
fn get_display_info_simple_meta(display_idx: usize) -> Option<(String, (i32, i32), usize, usize)> {
let displays = display_service::try_get_displays().ok()?;
if let Some(display) = displays.get(display_idx) {
Some((
display.name(),
display.origin(),
display.width(),
display.height(),
))
} else {
None
}
}
pub fn make_display_changed_msg(
display_idx: usize,
display_meta: Option<(String, (i32, i32), usize, usize)>,
) -> Option<Message> {
let mut misc = Misc::new();
let (name, origin, width, height) = match display_meta {
Some(d) => d,
None => get_display_info_simple_meta(display_idx)?,
};
let original_resolution = display_service::get_original_resolution(&name, width, height);
misc.set_switch_display(SwitchDisplay {
display: display_idx as _,
x: origin.0,
y: origin.1,
width: width as _,
height: height as _,
cursor_embedded: display_service::capture_cursor_embedded(),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
resolutions: Some(SupportedResolutions {
resolutions: if name.is_empty() {
vec![]
} else {
crate::platform::resolutions(&name)
},
..SupportedResolutions::default()
})
.into(),
original_resolution,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_misc(misc);
Some(msg_out)
}

View File

@ -142,9 +142,11 @@ pub(super) async fn check_init() -> ResultType<()> {
if *CAP_DISPLAY_INFO.read().unwrap() == 0 { if *CAP_DISPLAY_INFO.read().unwrap() == 0 {
let mut lock = CAP_DISPLAY_INFO.write().unwrap(); let mut lock = CAP_DISPLAY_INFO.write().unwrap();
if *lock == 0 { if *lock == 0 {
let all = Display::all()?; let mut all = Display::all()?;
let num = all.len(); let num = all.len();
let (primary, mut displays) = super::video_service::get_displays_2(&all); let primary = super::display_service::get_primary_2(&all);
let current = primary;
let mut displays = super::display_service::to_display_info(&all);
for display in displays.iter_mut() { for display in displays.iter_mut() {
display.cursor_embedded = is_cursor_embedded(); display.cursor_embedded = is_cursor_embedded();
} }
@ -154,12 +156,11 @@ pub(super) async fn check_init() -> ResultType<()> {
rects.push((d.origin(), d.width(), d.height())); rects.push((d.origin(), d.width(), d.height()));
} }
let (ndisplay, current, display) = let display = all.remove(current);
super::video_service::get_current_display_2(all)?;
let (origin, width, height) = (display.origin(), display.width(), display.height()); let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!( log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}", "#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}",
ndisplay, num,
current, current,
&origin, &origin,
width, width,
@ -213,16 +214,14 @@ pub(super) async fn check_init() -> ResultType<()> {
Ok(()) Ok(())
} }
pub(super) async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> { pub(super) async fn get_displays() -> ResultType<Vec<DisplayInfo>> {
check_init().await?; check_init().await?;
let addr = *CAP_DISPLAY_INFO.read().unwrap(); let addr = *CAP_DISPLAY_INFO.read().unwrap();
if addr != 0 { if addr != 0 {
let cap_display_info: *const CapDisplayInfo = addr as _; let cap_display_info: *const CapDisplayInfo = addr as _;
unsafe { unsafe {
let cap_display_info = &*cap_display_info; let cap_display_info = &*cap_display_info;
let primary = cap_display_info.primary; Ok(cap_display_info.displays.clone())
let displays = cap_display_info.displays.clone();
Ok((primary, displays))
} }
} else { } else {
bail!("Failed to get capturer display info"); bail!("Failed to get capturer display info");
@ -268,6 +267,9 @@ pub(super) fn get_capturer() -> ResultType<super::video_service::CapturerInfo> {
let cap_display_info = &*cap_display_info; let cap_display_info = &*cap_display_info;
let rect = cap_display_info.rects[cap_display_info.current]; let rect = cap_display_info.rects[cap_display_info.current];
Ok(super::video_service::CapturerInfo { Ok(super::video_service::CapturerInfo {
name: cap_display_info.displays[cap_display_info.current]
.name
.clone(),
origin: rect.0, origin: rect.0,
width: rect.1, width: rect.1,
height: rect.2, height: rect.2,

View File

@ -1,7 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
iter::FromIterator, iter::FromIterator,
process::Child,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -22,7 +21,6 @@ mod cm;
pub mod inline; pub mod inline;
pub mod remote; pub mod remote;
pub type Children = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
#[allow(dead_code)] #[allow(dead_code)]
type Status = (i32, bool, i64, String); type Status = (i32, bool, i64, String);
@ -34,7 +32,6 @@ lazy_static::lazy_static! {
#[cfg(not(any(feature = "flutter", feature = "cli")))] #[cfg(not(any(feature = "flutter", feature = "cli")))]
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref CUR_SESSION: Arc<Mutex<Option<Session<remote::SciterHandler>>>> = Default::default(); pub static ref CUR_SESSION: Arc<Mutex<Option<Session<remote::SciterHandler>>>> = Default::default();
static ref CHILDREN : Children = Default::default();
} }
struct UIHostHandler; struct UIHostHandler;
@ -598,6 +595,10 @@ impl UI {
fn get_login_device_info(&self) -> String { fn get_login_device_info(&self) -> String {
get_login_device_info_json() get_login_device_info_json()
} }
fn support_remove_wallpaper(&self) -> bool {
support_remove_wallpaper()
}
} }
impl sciter::EventHandler for UI { impl sciter::EventHandler for UI {
@ -683,6 +684,7 @@ impl sciter::EventHandler for UI {
fn default_video_save_directory(); fn default_video_save_directory();
fn handle_relay_id(String); fn handle_relay_id(String);
fn get_login_device_info(); fn get_login_device_info();
fn support_remove_wallpaper();
} }
} }

View File

@ -298,10 +298,11 @@ class Header: Reactor.Component {
recording = !recording; recording = !recording;
header.update(); header.update();
handler.record_status(recording); handler.record_status(recording);
// 0 is just a dummy value. It will be ignored by the handler.
if (recording) if (recording)
handler.refresh_video(); handler.refresh_video(0);
else else
handler.record_screen(false, display_width, display_height); handler.record_screen(false, 0, display_width, display_height);
} }
event click $(#screen) (_, me) { event click $(#screen) (_, me) {
@ -370,7 +371,8 @@ class Header: Reactor.Component {
} }
event click $(#refresh) { event click $(#refresh) {
handler.refresh_video(); // 0 is just a dummy value. It will be ignored by the handler.
handler.refresh_video(0);
} }
event click $(#block-input) { event click $(#block-input) {

View File

@ -210,6 +210,7 @@ class Enhancements: Reactor.Component {
function render() { function render() {
var has_hwcodec = handler.has_hwcodec(); var has_hwcodec = handler.has_hwcodec();
var support_remove_wallpaper = handler.support_remove_wallpaper();
var me = this; var me = this;
self.timer(1ms, function() { me.toggleMenuState() }); self.timer(1ms, function() { me.toggleMenuState() });
return <li>{translate('Enhancements')} return <li>{translate('Enhancements')}
@ -217,7 +218,7 @@ class Enhancements: Reactor.Component {
{has_hwcodec ? <li #enable-hwcodec><span>{svg_checkmark}</span>{translate("Hardware Codec")} (beta)</li> : ""} {has_hwcodec ? <li #enable-hwcodec><span>{svg_checkmark}</span>{translate("Hardware Codec")} (beta)</li> : ""}
<li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive bitrate")} (beta)</li> <li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive bitrate")} (beta)</li>
<li #screen-recording>{translate("Recording")}</li> <li #screen-recording>{translate("Recording")}</li>
{is_osx ? "" : <li #allow-remove-wallpaper><span>{svg_checkmark}</span>{translate("Remove wallpaper during incoming sessions")}</li>} {support_remove_wallpaper ? <li #allow-remove-wallpaper><span>{svg_checkmark}</span>{translate("Remove wallpaper during incoming sessions")}</li> : ""}
</menu> </menu>
</li>; </li>;
} }

View File

@ -122,7 +122,11 @@ impl InvokeUiSession for SciterHandler {
"updateQualityStatus", "updateQualityStatus",
&make_args!( &make_args!(
status.speed.map_or(Value::null(), |it| it.into()), status.speed.map_or(Value::null(), |it| it.into()),
status.fps.map_or(Value::null(), |it| it.into()), status
.fps
.iter()
.next()
.map_or(Value::null(), |(_, v)| (*v).into()),
status.delay.map_or(Value::null(), |it| it.into()), status.delay.map_or(Value::null(), |it| it.into()),
status.target_bitrate.map_or(Value::null(), |it| it.into()), status.target_bitrate.map_or(Value::null(), |it| it.into()),
status status
@ -223,7 +227,7 @@ impl InvokeUiSession for SciterHandler {
self.call("adaptSize", &make_args!()); self.call("adaptSize", &make_args!());
} }
fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { fn on_rgba(&self, _display: usize, rgba: &mut scrap::ImageRgb) {
VIDEO VIDEO
.lock() .lock()
.unwrap() .unwrap()
@ -304,11 +308,11 @@ impl InvokeUiSession for SciterHandler {
} }
/// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui.
fn get_rgba(&self) -> *const u8 { fn get_rgba(&self, _display: usize) -> *const u8 {
std::ptr::null() std::ptr::null()
} }
fn next_rgba(&self) {} fn next_rgba(&self, _display: usize) {}
} }
pub struct SciterSession(Session<SciterHandler>); pub struct SciterSession(Session<SciterHandler>);
@ -449,8 +453,8 @@ impl sciter::EventHandler for SciterSession {
fn save_view_style(String); fn save_view_style(String);
fn save_image_quality(String); fn save_image_quality(String);
fn save_custom_image_quality(i32); fn save_custom_image_quality(i32);
fn refresh_video(); fn refresh_video(i32);
fn record_screen(bool, i32, i32); fn record_screen(bool, i32, i32, i32);
fn record_status(bool); fn record_status(bool);
fn get_toggle_option(String); fn get_toggle_option(String);
fn is_privacy_mode_supported(); fn is_privacy_mode_supported();

View File

@ -23,7 +23,7 @@ handler.setDisplay = function(x, y, w, h, cursor_embedded) {
display_origin_y = y; display_origin_y = y;
display_cursor_embedded = cursor_embedded; display_cursor_embedded = cursor_embedded;
adaptDisplay(); adaptDisplay();
if (recording) handler.record_screen(true, w, h); if (recording) handler.record_screen(true, 0, w, h);
} }
// in case toolbar not shown correctly // in case toolbar not shown correctly
@ -478,7 +478,7 @@ function self.closing() {
var (x, y, w, h) = view.box(#rectw, #border, #screen); var (x, y, w, h) = view.box(#rectw, #border, #screen);
if (is_file_transfer) save_file_transfer_close_state(); if (is_file_transfer) save_file_transfer_close_state();
if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h);
if (recording) handler.record_screen(false, display_width, display_height); if (recording) handler.record_screen(false, 0, display_width, display_height);
} }
var qualityMonitor; var qualityMonitor;

View File

@ -164,6 +164,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
} }
#[inline] #[inline]
#[cfg(windows)]
fn is_authorized(&self, id: i32) -> bool { fn is_authorized(&self, id: i32) -> bool {
CLIENTS CLIENTS
.read() .read()

View File

@ -2,14 +2,14 @@
use hbb_common::password_security; use hbb_common::password_security;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
config::{self, Config, LocalConfig, PeerConfig},
directories_next, log, tokio,
};
use hbb_common::{
bytes::Bytes, bytes::Bytes,
config::{self, Config, LocalConfig, PeerConfig},
config::{CONNECT_TIMEOUT, RENDEZVOUS_PORT}, config::{CONNECT_TIMEOUT, RENDEZVOUS_PORT},
directories_next,
futures::future::join_all, futures::future::join_all,
log,
rendezvous_proto::*, rendezvous_proto::*,
tokio,
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::{ use hbb_common::{
@ -17,9 +17,10 @@ use hbb_common::{
tokio::{sync::mpsc, time}, tokio::{sync::mpsc, time},
}; };
use serde_derive::Serialize; use serde_derive::Serialize;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::process::Child;
use std::{ use std::{
collections::HashMap, collections::HashMap,
process::Child,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -31,6 +32,7 @@ use crate::ipc;
type Message = RendezvousMessage; type Message = RendezvousMessage;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub type Children = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>; pub type Children = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -594,7 +596,13 @@ pub fn current_is_wayland() -> bool {
#[inline] #[inline]
pub fn get_new_version() -> String { pub fn get_new_version() -> String {
(*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string() (*SOFTWARE_UPDATE_URL
.lock()
.unwrap()
.rsplit('/')
.next()
.unwrap_or(""))
.to_string()
} }
#[inline] #[inline]
@ -1248,3 +1256,10 @@ pub fn handle_relay_id(id: String) -> String {
id id
} }
} }
pub fn support_remove_wallpaper() -> bool {
#[cfg(any(target_os = "windows", target_os = "linux"))]
return crate::platform::WallPaperRemover::support();
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
return false;
}

View File

@ -3,14 +3,12 @@ use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use rdev::{Event, EventType::*, KeyCode}; use rdev::{Event, EventType::*, KeyCode};
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::{collections::HashMap, sync::atomic::AtomicBool}; use std::sync::atomic::{AtomicBool, Ordering};
use std::{ use std::{
collections::HashMap,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
str::FromStr, str::FromStr,
sync::{ sync::{Arc, Mutex, RwLock},
atomic::{AtomicUsize, Ordering},
Arc, Mutex, RwLock,
},
time::SystemTime, time::SystemTime,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -28,7 +26,7 @@ use hbb_common::{
sync::mpsc, sync::mpsc,
time::{Duration as TokioDuration, Instant}, time::{Duration as TokioDuration, Instant},
}, },
SessionID, Stream, Stream,
}; };
use crate::client::io_loop::Remote; use crate::client::io_loop::Remote;
@ -49,8 +47,7 @@ const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Session<T: InvokeUiSession> { pub struct Session<T: InvokeUiSession> {
pub session_id: SessionID, // different from the one in LoginConfigHandler, used for flutter UI message pass pub id: String, // peer id
pub id: String, // peer id
pub password: String, pub password: String,
pub args: Vec<String>, pub args: Vec<String>,
pub lc: Arc<RwLock<LoginConfigHandler>>, pub lc: Arc<RwLock<LoginConfigHandler>>,
@ -239,10 +236,18 @@ impl<T: InvokeUiSession> Session<T> {
self.lc.read().unwrap().reverse_mouse_wheel.clone() self.lc.read().unwrap().reverse_mouse_wheel.clone()
} }
pub fn get_displays_as_individual_windows(&self) -> String {
self.lc.read().unwrap().displays_as_individual_windows.clone()
}
pub fn save_reverse_mouse_wheel(&self, value: String) { pub fn save_reverse_mouse_wheel(&self, value: String) {
self.lc.write().unwrap().save_reverse_mouse_wheel(value); self.lc.write().unwrap().save_reverse_mouse_wheel(value);
} }
pub fn save_displays_as_individual_windows(&self, value: String) {
self.lc.write().unwrap().save_displays_as_individual_windows(value);
}
pub fn save_view_style(&self, value: String) { pub fn save_view_style(&self, value: String) {
self.lc.write().unwrap().save_view_style(value); self.lc.write().unwrap().save_view_style(value);
} }
@ -286,12 +291,30 @@ impl<T: InvokeUiSession> Session<T> {
&& !self.lc.read().unwrap().disable_clipboard.v && !self.lc.read().unwrap().disable_clipboard.v
} }
pub fn refresh_video(&self) { #[cfg(feature = "flutter")]
pub fn refresh_video(&self, display: i32) {
if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) {
self.send(Data::Message(LoginConfigHandler::refresh_display(
display as _,
)));
} else {
self.send(Data::Message(LoginConfigHandler::refresh()));
}
}
#[cfg(not(feature = "flutter"))]
pub fn refresh_video(&self, _display: i32) {
self.send(Data::Message(LoginConfigHandler::refresh())); self.send(Data::Message(LoginConfigHandler::refresh()));
} }
pub fn record_screen(&self, start: bool, w: i32, h: i32) { pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) {
self.send(Data::RecordScreen(start, w, h, self.id.clone())); self.send(Data::RecordScreen(
start,
display as usize,
w,
h,
self.id.clone(),
));
} }
pub fn record_status(&self, status: bool) { pub fn record_status(&self, status: bool) {
@ -603,6 +626,19 @@ impl<T: InvokeUiSession> Session<T> {
self.send(Data::Message(msg_out)); self.send(Data::Message(msg_out));
} }
pub fn capture_displays(&self, add: Vec<i32>, sub: Vec<i32>, set: Vec<i32>) {
let mut misc = Misc::new();
misc.set_capture_displays(CaptureDisplays {
add,
sub,
set,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_misc(misc);
self.send(Data::Message(msg_out));
}
pub fn switch_display(&self, display: i32) { pub fn switch_display(&self, display: i32) {
let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) { let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) {
Some((w, h)) => (w, h), Some((w, h)) => (w, h),
@ -1164,7 +1200,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn update_block_input_state(&self, on: bool); fn update_block_input_state(&self, on: bool);
fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64);
fn adapt_size(&self); fn adapt_size(&self);
fn on_rgba(&self, rgba: &mut scrap::ImageRgb); fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb);
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool);
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
fn clipboard(&self, content: String); fn clipboard(&self, content: String);
@ -1175,8 +1211,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_closed(&self, reason: &str);
fn on_voice_call_waiting(&self); fn on_voice_call_waiting(&self);
fn on_voice_call_incoming(&self); fn on_voice_call_incoming(&self);
fn get_rgba(&self) -> *const u8; fn get_rgba(&self, display: usize) -> *const u8;
fn next_rgba(&self); fn next_rgba(&self, display: usize);
} }
impl<T: InvokeUiSession> Deref for Session<T> { impl<T: InvokeUiSession> Deref for Session<T> {
@ -1237,7 +1273,7 @@ impl<T: InvokeUiSession> Interface for Session<T> {
if pi.displays.is_empty() { if pi.displays.is_empty() {
self.lc.write().unwrap().handle_peer_info(&pi); self.lc.write().unwrap().handle_peer_info(&pi);
self.update_privacy_mode(); self.update_privacy_mode();
self.msgbox("error", "Remote Error", "No Display", ""); self.msgbox("error", "Remote Error", "No Displays", "");
return; return;
} }
self.try_change_init_resolution(pi.current_display); self.try_change_init_resolution(pi.current_display);
@ -1447,24 +1483,29 @@ pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>, round: u32) {
} }
return; return;
} }
let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_map: Arc<RwLock<HashMap<usize, usize>>> = Default::default();
let frame_count_cl = frame_count.clone(); let frame_count_map_cl = frame_count_map.clone();
let ui_handler = handler.ui_handler.clone(); let ui_handler = handler.ui_handler.clone();
let (video_sender, audio_sender, video_queue, decode_fps) = let (video_sender, audio_sender, video_queue_map, decode_fps_map) = start_video_audio_threads(
start_video_audio_threads(move |data: &mut scrap::ImageRgb| { handler.clone(),
frame_count_cl.fetch_add(1, Ordering::Relaxed); move |display: usize, data: &mut scrap::ImageRgb| {
ui_handler.on_rgba(data); let mut write_lock = frame_count_map_cl.write().unwrap();
}); let count = write_lock.get(&display).unwrap_or(&0) + 1;
write_lock.insert(display, count);
drop(write_lock);
ui_handler.on_rgba(display, data);
},
);
let mut remote = Remote::new( let mut remote = Remote::new(
handler, handler,
video_queue, video_queue_map,
video_sender, video_sender,
audio_sender, audio_sender,
receiver, receiver,
sender, sender,
frame_count, frame_count_map,
decode_fps, decode_fps_map,
); );
remote.io_loop(&key, &token, round).await; remote.io_loop(&key, &token, round).await;
remote.sync_jobs_status_to_local().await; remote.sync_jobs_status_to_local().await;