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]]
name = "wallpaper"
version = "3.2.0"
source = "git+https://github.com/21pages/wallpaper.rs#2bbb70acd93be179c69cb96cb8c3dda487e6f5fd"
source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
dependencies = [
"dirs 5.0.1",
"enquote",

View File

@ -1054,7 +1054,7 @@ Widget msgboxIcon(String type) {
if (type == 'on-uac' || type == 'on-foreground-elevated') {
iconData = Icons.admin_panel_settings;
}
if (type == "info") {
if (type.contains('info')) {
iconData = Icons.info;
}
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);
}
@ -2589,3 +2589,18 @@ String getDesktopTabLabel(String peerId, String alias) {
}
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;
} catch (_) {}
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(
initQuality: qualityInitValue,

View File

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

View File

@ -90,7 +90,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(
TTextMenu(
child: Row(children: [
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
Offstage(
offstage: isDesktop,
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(
scale: 0.8,
child: InkWell(
onTap: () => pi.is_headless
onTap: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
child: Icon(Icons.edit),
),
),
onPressed: () => pi.is_headless
onPressed: () => pi.isHeadless
? showSetOSAccount(sessionId, ffi.dialogManager)
: handleOsPasswordAction(sessionId, ffi.dialogManager),
),
@ -208,7 +208,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
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(
child: Text(translate('Switch Sides')),
onPressed: () =>
@ -218,7 +219,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
if (pi.version.isNotEmpty) {
v.add(TTextMenu(
child: Text(translate('Refresh')),
onPressed: () => bind.sessionRefresh(sessionId: sessionId)));
onPressed: () => sessionRefreshVideo(sessionId, pi),
));
}
// record
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
@ -377,7 +379,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
// show remote cursor
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.is_wayland) {
!pi.isWayland) {
final state = ShowRemoteCursorState.find(id);
final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor';
@ -488,7 +490,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
value: rxValue.value,
onChanged: (value) {
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',
'Please switch to Display 1 first', '', ffi.dialogManager);
return;
@ -512,5 +515,23 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
},
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;
}

View File

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

View File

@ -263,8 +263,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
? textColor
: Color(0xFFDDDDDD),
size: 22,
))
)),
)))),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
InkWell(
@ -273,8 +272,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
message: translate('Change Password'),
child: Icon(
Icons.edit,
color:
editHover.value ? textColor : Color(0xFFDDDDDD),
color: editHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
)).marginOnly(right: 8, top: 4),
),
@ -604,8 +604,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
debugPrint("Failed to parse window id '${call.arguments}': $e");
}
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();

View File

@ -323,24 +323,7 @@ class _GeneralState extends State<_General> {
'enable-confirm-closing-tabs',
isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
if (Platform.isWindows || Platform.isLinux)
Row(
children: [
Flexible(
child: _OptionCheckBox(
context,
'Remove wallpaper during incoming sessions',
'allow-remove-wallpaper'),
),
_CountDownButton(
text: 'Test',
second: 5,
onPressed: () {
bind.mainTestWallpaper(second: 5);
},
)
],
),
wallpaper(),
_OptionCheckBox(
context,
'Open connection in new tab',
@ -367,6 +350,42 @@ class _GeneralState extends State<_General> {
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() {
return Offstage(
offstage: !bind.mainHasHwcodec(),
@ -1289,7 +1308,7 @@ class _DisplayState extends State<_Display> {
Widget other(BuildContext context) {
return _Card(title: 'Other Default Options', children: [
otherRow('View Mode', 'view_only'),
otherRow('show_monitors_tip', 'show_monitors_toolbar'),
otherRow('show_monitors_tip', kKeyShowMonitorsToolbar),
otherRow('Collapse toolbar', 'collapse_toolbar'),
otherRow('Show remote cursor', 'show_remote_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('Privacy mode', 'privacy_mode'),
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);
// Used to skip session close if "move to new window" is clicked.
final Map<String, bool> closeSessionOnDispose = {};
class RemotePage extends StatefulWidget {
@ -36,6 +37,8 @@ class RemotePage extends StatefulWidget {
required this.id,
required this.sessionId,
required this.tabWindowId,
required this.display,
required this.displays,
required this.password,
required this.toolbarState,
required this.tabController,
@ -46,6 +49,8 @@ class RemotePage extends StatefulWidget {
final String id;
final SessionID? sessionId;
final int? tabWindowId;
final int? display;
final List<int>? displays;
final String? password;
final ToolbarState toolbarState;
final String? switchUuid;
@ -73,7 +78,7 @@ class _RemotePageState extends State<RemotePage>
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
late RenderTexture _renderTexture;
final Map<int, RenderTexture> _renderTextures = {};
final _blockableOverlayState = BlockableOverlayState();
@ -109,6 +114,8 @@ class _RemotePageState extends State<RemotePage>
switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
display: widget.display,
displays: widget.displays,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@ -118,9 +125,6 @@ class _RemotePageState extends State<RemotePage>
if (!Platform.isLinux) {
Wakelock.enable();
}
// Register texture.
_renderTexture = RenderTexture();
_renderTexture.create(sessionId);
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
@ -207,7 +211,9 @@ class _RemotePageState extends State<RemotePage>
// https://github.com/flutter/flutter/issues/64935
super.dispose();
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
_ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this);
@ -245,6 +251,7 @@ class _RemotePageState extends State<RemotePage>
onEnterOrLeaveImageSetter: (func) =>
_onEnterOrLeaveImage4Toolbar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
setRemoteState: setState,
);
return Scaffold(
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) {
var paints = <Widget>[
MouseRegion(onEnter: (evt) {
@ -402,16 +441,20 @@ class _RemotePageState extends State<RemotePage>
Future.delayed(Duration.zero, () {
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
});
return ImagePaint(
final peerDisplay = CurrentDisplayState.find(widget.id);
return Obx(
() => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent)
: Obx(() => ImagePaint(
id: widget.id,
zoomCursor: _zoomCursor,
cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
textureId: _renderTexture.textureId,
useTextureRender: RenderTexture.useTextureRender,
listenerBuilder: (child) =>
_buildRawTouchAndPointerRegion(child, enterView, leaveView),
renderTextures: _updateGetRenderTextures(peerDisplay.value),
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
child, enterView, leaveView),
)),
);
}))
];
@ -447,8 +490,7 @@ class ImagePaint extends StatefulWidget {
final RxBool cursorOverImage;
final RxBool keyboardEnabled;
final RxBool remoteCursorMoved;
final RxInt textureId;
final bool useTextureRender;
final Map<int, RenderTexture> renderTextures;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@ -458,8 +500,7 @@ class ImagePaint extends StatefulWidget {
required this.cursorOverImage,
required this.keyboardEnabled,
required this.remoteCursorMoved,
required this.textureId,
required this.useTextureRender,
required this.renderTextures,
this.listenerBuilder})
: super(key: key);
@ -530,27 +571,13 @@ class _ImagePaintState extends State<ImagePaint> {
});
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight);
late final Widget imageWidget;
if (widget.useTextureRender) {
imageWidget = SizedBox(
width: imageWidth,
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),
);
}
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);
final paintWidget = useTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final percentX = _horizontal.hasClients
@ -570,43 +597,79 @@ class _ImagePaintState extends State<ImagePaint> {
},
child: mouseRegion(
child: Obx(() => _buildCrossScrollbarFromLayout(
context, _buildListener(imageWidget), c.size, imageSize)),
context, _buildListener(paintWidget), c.size, paintSize)),
));
} else {
late final Widget imageWidget;
if (c.size.width > 0 && c.size.height > 0) {
if (widget.useTextureRender) {
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x;
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y;
imageWidget = Stack(
children: [
Positioned(
left: x,
top: y,
width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s,
child: Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
final paintWidget = useTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
Platform.isLinux ? c.x.toInt().toDouble() : c.x,
Platform.isLinux ? c.y.toInt().toDouble() : c.y,
),
)
],
);
} else {
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));
c.size,
isViewOriginal())
: _buildScrollAuthNonTextureRender(m, c, s);
return mouseRegion(child: _buildListener(paintWidget));
} else {
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(
CursorModel cursor, double scale, CursorData? cache) {
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) {
@ -770,9 +837,14 @@ class CursorPaint extends StatelessWidget {
double cy = c.y;
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
c.scrollStyle == ScrollStyle.scrollbar) {
final d = c.parent.target!.ffiModel.display;
final imageWidth = d.width * c.scale;
final imageHeight = d.height * c.scale;
final rect = c.parent.target!.ffiModel.rect;
if (rect == null) {
// 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;
cy = -imageHeight * c.scrollY;
}

View File

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

View File

@ -1,10 +1,12 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/state_model.dart';
import 'package:flutter_hbb/consts.dart';
@ -325,7 +327,8 @@ class RemoteToolbar extends StatefulWidget {
final FFI ffi;
final ToolbarState state;
final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner;
final VoidCallback onEnterOrLeaveImageCleaner;
final Function(VoidCallback) setRemoteState;
RemoteToolbar({
Key? key,
@ -334,6 +337,7 @@ class RemoteToolbar extends StatefulWidget {
required this.state,
required this.onEnterOrLeaveImageSetter,
required this.onEnterOrLeaveImageCleaner,
required this.setRemoteState,
}) : super(key: key);
@override
@ -450,13 +454,17 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
}
if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) {
toolbarItems.add(
bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y'
? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi)
: _MonitorMenu(id: widget.id, ffi: widget.ffi),
);
toolbarItems.add(Obx(() {
if (PrivacyModeState.find(widget.id).isFalse &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,
ffi: widget.ffi,
setRemoteState: widget.setRemoteState);
} else {
return Offstage();
}
}));
toolbarItems
.add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
@ -581,11 +589,22 @@ class _MobileActionMenu extends StatelessWidget {
class _MonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
const _MonitorMenu({Key? key, required this.id, required this.ffi})
: super(key: key);
final Function(VoidCallback) setRemoteState;
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
Widget build(BuildContext context) {
Widget build(BuildContext context) =>
showMonitorsToolbar ? buildMultiMonitorMenu() : buildMonitorMenu();
Widget buildMonitorMenu() {
return _IconSubmenuButton(
tooltip: 'Select Monitor',
icon: icon(),
@ -595,7 +614,107 @@ class _MonitorMenu extends StatelessWidget {
menuStyle: MenuStyle(
padding:
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() {
@ -610,7 +729,7 @@ class _MonitorMenu extends StatelessWidget {
Obx(() {
RxInt display = CurrentDisplayState.find(id);
return Text(
'${display.value + 1}/${pi.displays.length}',
'${display.value == kAllDisplayValue ? 'A' : '${display.value + 1}'}/${pi.displays.length}',
style: const TextStyle(
color: _ToolbarTheme.blueColor,
fontSize: 8,
@ -622,48 +741,44 @@ class _MonitorMenu extends StatelessWidget {
);
}
List<Widget> displays(BuildContext context) {
final List<Widget> rowChildren = [];
final pi = ffi.ffiModel.pi;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(_IconMenuButton(
topLevel: false,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
tooltip: "#${i + 1} monitor",
hMargin: 6,
vMargin: 12,
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),
),
Text(
(i + 1).toString(),
style: TextStyle(
color: _ToolbarTheme.blueColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
onPressed: () {
// Open new tab or window to show this monitor.
// For now just open new window.
openMonitorInNewTabOrWindow(int i, PeerInfo pi) {
if (kWindowId == null) {
// unreachable
debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null');
return;
}
DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventOpenMonitorSession,
jsonEncode({
'window_id': kWindowId!,
'peer_id': ffi.id,
'display': i,
'display_count': pi.displays.length,
}));
}
openMonitorInTheSameTab(int i, PeerInfo pi) {
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
bind.sessionSwitchDisplay(
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id);
}
onPressed(int i, PeerInfo pi) {
_menuDismissCallback(ffi);
RxInt display = CurrentDisplayState.find(id);
if (display.value != i) {
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
openMonitorInNewTabOrWindow(i, pi);
} else {
openMonitorInTheSameTab(i, pi);
}
},
));
}
return rowChildren;
}
}
@ -1044,14 +1159,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Resolution? _localResolution;
late final TextEditingController _customWidth =
TextEditingController(text: display.width.toString());
TextEditingController(text: rect?.width.toInt().toString() ?? '');
late final TextEditingController _customHeight =
TextEditingController(text: display.height.toString());
TextEditingController(text: rect?.height.toInt().toString() ?? '');
FFI get ffi => widget.ffi;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display;
Rect? get rect => ffiModel.rect;
List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland();
@ -1063,12 +1178,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
@override
Widget build(BuildContext context) {
final isVirtualDisplay = display.isVirtualDisplayResolution;
final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage();
final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution;
ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
_setGroupValue();
return _SubmenuButton(
@ -1085,12 +1200,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
_setGroupValue() {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
final lastGroupValue =
stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
if (lastGroupValue == _kCustomResolutionValue) {
_groupValue = _kCustomResolutionValue;
} 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() {
_localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay();
if (currentDisplay.isNotEmpty) {
final String mainDisplay = bind.mainGetMainDisplay();
if (mainDisplay.isNotEmpty) {
try {
final display = json.decode(currentDisplay);
final display = json.decode(mainDisplay);
if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']);
}
} catch (e) {
debugPrint('Failed to decode $currentDisplay, $e');
debugPrint('Failed to decode $mainDisplay, $e');
}
}
}
_onChanged(BuildContext context, String? value) async {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
stateGlobal.setLastResolutionGroupValue(
widget.id, pi.currentDisplay, value);
if (value == null) return;
@ -1150,13 +1271,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
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);
}
}
}
_changeResolution(BuildContext context, int w, int h) async {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
await bind.sessionChangeResolution(
sessionId: ffi.sessionId,
display: pi.currentDisplay,
@ -1164,8 +1288,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
height: h,
);
Future.delayed(Duration(seconds: 3), () async {
final display = ffiModel.display;
if (w == display.width && h == display.height) {
final rect = ffiModel.rect;
if (rect == null) {
return;
}
if (w == rect.width.toInt() && h == rect.height.toInt()) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context);
}
@ -1175,6 +1302,10 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
Widget _OriginalResolutionMenuButton(
BuildContext context, bool showOriginalBtn) {
final display = pi.tryGetDisplayIfNotAllDisplay();
if (display == null) {
return Offstage();
}
return Offstage(
offstage: !showOriginalBtn,
child: MenuButton(
@ -1262,7 +1393,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
return null;
}
if (display.isVirtualDisplayResolution) {
if (ffiModel.isVirtualDisplayResolution) {
return _localResolution!;
}
@ -1284,8 +1415,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
if (bestFitResolution == null) {
return true;
}
return bestFitResolution.width == display.width &&
bestFitResolution.height == display.height;
return bestFitResolution.width == rect?.width.toInt() &&
bestFitResolution.height == rect?.height.toInt();
}
}
@ -1361,7 +1492,7 @@ class _KeyboardMenu extends StatelessWidget {
continue;
}
if (pi.is_wayland && mode.key != _kKeyMapMode) {
if (pi.isWayland && mode.key != _kKeyMapMode) {
continue;
}
@ -1404,7 +1535,7 @@ class _KeyboardMenu extends StatelessWidget {
viewMode() {
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(
value: ffiModel.viewOnly,
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_hbb/common.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/models/platform_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),
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) {
if (!isDesktop) return;
assert(onRemoved != null);

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -755,14 +756,14 @@ void showOptions(
if (image != null) {
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 children = <Widget>[];
for (var i = 0; i < pi.displays.length; ++i) {
children.add(InkWell(
onTap: () {
if (i == cur) return;
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: i);
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i]));
gFFI.dialogManager.dismissAll();
},
child: Ink(

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
@ -86,7 +87,7 @@ class CachedPeerData {
class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo();
Display _display = Display();
Rect? _rect;
var _inputBlocked = false;
final _permissions = <String, bool>{};
@ -103,9 +104,15 @@ class FfiModel with ChangeNotifier {
Timer? waitForImageTimer;
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;
@ -130,6 +137,24 @@ class FfiModel with ChangeNotifier {
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() {
if (!isPeerAndroid) {
_touchMode = !_touchMode;
@ -154,7 +179,6 @@ class FfiModel with ChangeNotifier {
clear() {
_pi = PeerInfo();
_display = Display();
_secure = null;
_direct = null;
_inputBlocked = false;
@ -207,9 +231,11 @@ class FfiModel with ChangeNotifier {
updateLastCursorId(element);
await handleCursorData(element);
}
if (data.lastCursorId.isNotEmpty) {
updateLastCursorId(data.lastCursorId);
handleCursorId(data.lastCursorId);
}
}
// todo: why called by two position
StreamEventHandler startEventListener(SessionID sessionId, String peerId) {
@ -220,11 +246,12 @@ class FfiModel with ChangeNotifier {
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
} else if (name == 'sync_peer_info') {
handleSyncPeerInfo(evt, sessionId);
handleSyncPeerInfo(evt, sessionId, peerId);
} else if (name == 'connection_ready') {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
// switch display is kept for backward compatibility
handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') {
updateLastCursorId(evt);
@ -279,7 +306,7 @@ class FfiModel with ChangeNotifier {
await bind.sessionSwitchSides(sessionId: sessionId);
closeConnection(id: peer_id);
} else if (name == 'portable_service_running') {
parent.target?.elevationModel.onPortableServiceRunning(evt);
_handlePortableServiceRunning(peerId, evt);
} else if (name == 'on_url_scheme_received') {
// currently comes from "_url" ipc of mac and dbus of linux
onUrlSchemeReceived(evt);
@ -354,20 +381,65 @@ class FfiModel with ChangeNotifier {
platformFFI.setEventCallback(startEventListener(sessionId, peerId));
}
_updateCurDisplay(SessionID sessionId, Display newDisplay) {
if (newDisplay != _display) {
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
parent.target?.cursorModel
.updateDisplayOrigin(newDisplay.x, newDisplay.y);
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
final running = evt['running'] == 'true';
parent.target?.elevationModel.onPortableServiceRunning(running);
if (running) {
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);
}
}
handleSwitchDisplay(
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();
newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x;
newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y;
@ -378,11 +450,11 @@ class FfiModel with ChangeNotifier {
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
newDisplay.originalHeight =
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
_pi.displays[curDisplay] = newDisplay;
_updateCurDisplay(sessionId, newDisplay);
updateCurDisplay(sessionId);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
CurrentDisplayState.find(peerId).value = curDisplay;
} catch (e) {
//
}
@ -522,13 +594,30 @@ class FfiModel with ChangeNotifier {
}
_updateSessionWidthHeight(SessionID sessionId) {
parent.target?.canvasModel.updateViewStyle();
if (display.width <= 0 || display.height <= 0) {
if (_rect == null) return;
if (_rect!.width <= 0 || _rect!.height <= 0) {
debugPrintStack(
label: 'invalid display size (${display.width},${display.height})');
label: 'invalid display size (${_rect!.width},${_rect!.height})');
} else {
final displays = _pi.getCurDisplays();
if (displays.length == 1) {
bind.sessionSetSize(
sessionId: sessionId, width: display.width, height: display.height);
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();
_pi.version = evt['version'];
_pi.isSupportMultiUiSession =
bind.isSupportMultiUiSession(version: _pi.version);
_pi.username = evt['username'];
_pi.hostname = evt['hostname'];
_pi.platform = evt['platform'];
_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 {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
@ -569,10 +667,10 @@ class FfiModel with ChangeNotifier {
for (int i = 0; i < displays.length; ++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) {
_display = _pi.displays[_pi.currentDisplay];
_updateSessionWidthHeight(sessionId);
// now replaced to _updateCurDisplay
updateCurDisplay(sessionId);
}
if (displays.isNotEmpty) {
_reconnects = 1;
@ -590,12 +688,12 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: 'view-only'));
}
if (connType == ConnType.defaultConn) {
final platform_additions = evt['platform_additions'];
if (platform_additions != null && platform_additions != '') {
final platformDdditions = evt['platform_additions'];
if (platformDdditions != null && platformDdditions != '') {
try {
_pi.platform_additions = json.decode(platform_additions);
_pi.platformDdditions = json.decode(platformDdditions);
} 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].
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
handleSyncPeerInfo(
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
if (evt['displays'] != null) {
cachedPeerData.peerInfo['displays'] = evt['displays'];
List<dynamic> displays = json.decode(evt['displays']);
@ -679,14 +778,54 @@ class FfiModel with ChangeNotifier {
newDisplays.add(evtToDisplay(displays[i]));
}
_pi.displays = newDisplays;
stateGlobal.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
_updateCurDisplay(sessionId, _pi.displays[_pi.currentDisplay]);
_pi.displaysCount.value = _pi.displays.length;
if (_pi.currentDisplay == kAllDisplayValue) {
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();
}
// 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) {
_inputBlocked = evt['input_state'] == 'on';
notifyListeners();
@ -709,7 +848,7 @@ class FfiModel with ChangeNotifier {
}
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
// 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. !!!!!!!!!!!!!!!!
@ -749,16 +888,16 @@ class ImageModel with ChangeNotifier {
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(Uint8List rgba) {
onRgba(int display, Uint8List rgba) {
final pid = parent.target?.id;
img.decodeImageFromPixels(
rgba,
parent.target?.ffiModel.display.width ?? 0,
parent.target?.ffiModel.display.height ?? 0,
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () {
// Unlock the rgba memory from rust codes.
platformFFI.nextRgba(sessionId);
platformFFI.nextRgba(sessionId, display);
}).then((image) {
if (parent.target?.id != pid) return;
try {
@ -1017,20 +1156,20 @@ class CanvasModel with ChangeNotifier {
}
bool get cursorEmbedded =>
parent.target?.ffiModel.display.cursorEmbedded ?? false;
parent.target?.ffiModel._pi.cursorEmbedded ?? false;
int getDisplayWidth() {
final defaultWidth = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayWidth
: kMobileDefaultDisplayWidth;
return parent.target?.ffiModel.display.width ?? defaultWidth;
return parent.target?.ffiModel.rect?.width.toInt() ?? defaultWidth;
}
int getDisplayHeight() {
final defaultHeight = (isDesktop || isWebDesktop)
? kDesktopDefaultDisplayHeight
: kMobileDefaultDisplayHeight;
return parent.target?.ffiModel.display.height ?? defaultHeight;
return parent.target?.ffiModel.rect?.height.toInt() ?? defaultHeight;
}
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
@ -1619,7 +1758,27 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) {
try {
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['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate'];
@ -1646,8 +1805,15 @@ class RecordingModel with ChangeNotifier {
int? width = parent.target?.canvasModel.getDisplayWidth();
int? height = parent.target?.canvasModel.getDisplayHeight();
if (sessionId == null || width == null || height == null) return;
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId, start: true, width: width, height: height);
sessionId: sessionId,
start: true,
display: currentDisplay!,
width: width,
height: height);
}
}
toggle() async {
@ -1658,10 +1824,20 @@ class RecordingModel with ChangeNotifier {
notifyListeners();
await bind.sessionRecordStatus(sessionId: sessionId, status: _start);
if (_start) {
bind.sessionRefresh(sessionId: sessionId);
final pi = parent.target?.ffiModel.pi;
if (pi != null) {
sessionRefreshVideo(sessionId, pi);
}
} else {
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId, start: false, width: 0, height: 0);
sessionId: sessionId,
start: false,
display: currentDisplay!,
width: 0,
height: 0);
}
}
}
@ -1670,8 +1846,15 @@ class RecordingModel with ChangeNotifier {
final sessionId = parent.target?.sessionId;
if (sessionId == null) return;
_start = false;
final currentDisplay = parent.target?.ffiModel.pi.currentDisplay;
if (currentDisplay != kAllDisplayValue) {
bind.sessionRecordScreen(
sessionId: sessionId, start: false, width: 0, height: 0);
sessionId: sessionId,
start: false,
display: currentDisplay!,
width: 0,
height: 0);
}
}
}
@ -1686,9 +1869,7 @@ class ElevationModel with ChangeNotifier {
_running = false;
}
onPortableServiceRunning(Map<String, dynamic> evt) {
_running = evt['running'] == 'true';
}
onPortableServiceRunning(bool running) => _running = running;
}
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].
void start(String id,
{bool isFileTransfer = false,
void start(
String id, {
bool isFileTransfer = false,
bool isPortForward = false,
bool isRdp = false,
String? switchUuid,
String? password,
bool? forceRelay,
int? tabWindowId}) {
int? tabWindowId,
int? display,
List<int>? displays,
}) {
closed = false;
auditNote = '';
if (isMobile) mobileReset();
@ -1788,11 +1973,34 @@ class FFI {
forceRelay: forceRelay ?? false,
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 cb = ffiModel.startEventListener(sessionId, id);
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);
// Preserved for the rgba data.
stream.listen((message) {
@ -1801,8 +2009,9 @@ class FFI {
// Session is read to be moved to a new window.
// Get the cached data and handle the cached data.
Future.delayed(Duration.zero, () async {
final args = jsonEncode({'id': id, 'close': display == null});
final cachedData = await DesktopMultiWindow.invokeMethod(
tabWindowId, kWindowEventGetCachedSessionData, id);
tabWindowId, kWindowEventGetCachedSessionData, args);
if (cachedData == null) {
// unreachable
debugPrint('Unreachable, the cached data is empty.');
@ -1814,7 +2023,7 @@ class FFI {
return;
}
await ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId);
await sessionRefreshVideo(sessionId, ffiModel.pi);
});
isToNewWindowNotified.value = true;
}
@ -1836,18 +2045,19 @@ class FFI {
await cb(event);
}
} else if (message is EventToUI_Rgba) {
final display = message.field0;
if (useTextureRender) {
onEvent2UIRgba();
} else {
// Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId);
final sz = platformFFI.getRgbaSize(sessionId, display);
if (sz == 0) {
return;
}
final rgba = platformFFI.getRgba(sessionId, sz);
final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) {
onEvent2UIRgba();
imageModel.onRgba(rgba);
imageModel.onRgba(display, rgba);
}
}
}
@ -1979,22 +2189,72 @@ class Features {
bool privacyMode = false;
}
const kInvalidDisplayIndex = -1;
class PeerInfo with ChangeNotifier {
String version = '';
String username = '';
String hostname = '';
String platform = '';
bool sasEnabled = false;
bool isSupportMultiUiSession = false;
int currentDisplay = 0;
int primaryDisplay = kInvalidDisplayIndex;
List<Display> displays = [];
Features features = Features();
List<Resolution> resolutions = [];
Map<String, dynamic> platform_additions = {};
Map<String, dynamic> platformDdditions = {};
RxInt displaysCount = 0.obs;
RxBool isSet = false.obs;
bool get is_wayland => platform_additions['is_wayland'] == true;
bool get is_headless => platform_additions['headless'] == true;
bool get isWayland => platformDdditions['is_wayland'] == 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';
@ -2038,8 +2298,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
currentDisplay = p['currentDisplay'];
}
if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) {
ffi.cursorModel
.updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y);
ffi.cursorModel.updateDisplayOrigin(
ffi.ffiModel.rect?.left ?? 0, ffi.ffiModel.rect?.top ?? 0);
return;
}
double xCursor = p['xCursor'];
@ -2047,8 +2307,8 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
double xCanvas = p['xCanvas'];
double yCanvas = p['yCanvas'];
double scale = p['scale'];
ffi.cursorModel.updateDisplayOriginWithCursor(
ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor);
ffi.cursorModel.updateDisplayOriginWithCursor(ffi.ffiModel.rect?.left ?? 0,
ffi.ffiModel.rect?.top ?? 0, xCursor, yCursor);
ffi.canvasModel.update(xCanvas, yCanvas, scale);
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
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/material.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(
WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
@ -148,6 +187,15 @@ class RustDeskMultiWindowManager {
bool openInTabs = type != WindowType.RemoteDesktop ||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs);
if (kOpenSamePeerInNewWindow) {
// Open in new window if the peer is already connected.
// No need to care about the previous session type.
if (type == WindowType.RemoteDesktop &&
await bind.sessionGetFlutterOptionByPeerId(id: remoteId, k: '') !=
null) {
openInTabs = false;
}
} else {
if (windows.length > 1 || !openInTabs) {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
@ -156,6 +204,7 @@ class RustDeskMultiWindowManager {
}
}
}
}
return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
}

View File

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

View File

@ -27,6 +27,7 @@ message VideoFrame {
EncodedVideoFrames vp8s = 12;
EncodedVideoFrames av1s = 13;
}
int32 display = 14;
}
message IdPk {
@ -491,6 +492,12 @@ message SwitchDisplay {
Resolution original_resolution = 8;
}
message CaptureDisplays {
repeated int32 add = 1;
repeated int32 sub = 2;
repeated int32 set = 3;
}
message PermissionInfo {
enum Permission {
Keyboard = 0;
@ -688,6 +695,8 @@ message Misc {
uint32 full_speed_fps = 27;
uint32 auto_adjust_fps = 28;
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"
)]
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(
default,
@ -328,6 +334,7 @@ impl Default for PeerConfig {
keyboard_mode: Default::default(),
view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
displays_as_individual_windows: Self::default_displays_as_individual_windows(),
custom_resolutions: Default::default(),
options: Self::default_options(),
ui_flutter: Default::default(),
@ -1144,6 +1151,11 @@ impl PeerConfig {
deserialize_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> {
let f: f64 = UserDefaultConfig::read()

View File

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

View File

@ -23,8 +23,8 @@ use hbb_common::{
config::PeerConfig,
log,
message_proto::{
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message,
SupportedDecoding, SupportedEncoding,
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames,
SupportedDecoding, SupportedEncoding, VideoFrame,
},
sysinfo::{System, SystemExt},
tokio::time::Instant,
@ -60,7 +60,7 @@ pub trait EncoderApi {
where
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;

View File

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

View File

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

View File

@ -3,10 +3,7 @@ use std::{
net::SocketAddr,
ops::Deref,
str::FromStr,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc, Mutex, RwLock,
},
sync::{mpsc, Arc, Mutex, RwLock},
};
pub use async_trait::async_trait;
@ -60,6 +57,7 @@ use scrap::{
use crate::{
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
is_keyboard_mode_supported,
ui_session_interface::{InvokeUiSession, Session},
};
#[cfg(not(feature = "flutter"))]
@ -675,9 +673,12 @@ impl Client {
}
#[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")]
if crate::flutter::sessions::other_sessions_running(_self_uuid) {
if crate::flutter::sessions::other_sessions_running(
_self_id.to_string(),
ConnType::DEFAULT_CONN,
) {
return;
}
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
@ -1206,6 +1207,17 @@ impl LoginConfigHandler {
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.
///
/// # Arguments
@ -1523,6 +1535,15 @@ impl LoginConfigHandler {
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.
///
/// # Arguments
@ -1789,89 +1810,158 @@ impl LoginConfigHandler {
/// Media data.
pub enum MediaData {
VideoQueue,
VideoQueue(usize),
VideoFrame(Box<VideoFrame>),
AudioFrame(Box<AudioFrame>),
AudioFormat(AudioFormat),
Reset,
RecordScreen(bool, i32, i32, String),
Reset(usize),
RecordScreen(bool, usize, i32, i32, String),
}
pub type MediaSender = mpsc::Sender<MediaData>;
struct VideoHandlerController {
handler: VideoHandler,
count: u128,
duration: std::time::Duration,
skip_beginning: u32,
}
/// Start video and audio thread.
/// Return two [`MediaSender`], they should be given to the media producer.
///
/// # Arguments
///
/// * `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,
) -> (
MediaSender,
MediaSender,
Arc<ArrayQueue<VideoFrame>>,
Arc<AtomicUsize>,
Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
Arc<RwLock<HashMap<usize, usize>>>,
)
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_queue = Arc::new(ArrayQueue::<VideoFrame>::new(VIDEO_QUEUE_SIZE));
let video_queue_cloned = video_queue.clone();
let video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>> = Default::default();
let video_queue_map_cloned = video_queue_map.clone();
let mut video_callback = video_callback;
let mut duration = std::time::Duration::ZERO;
let mut count = 0;
let fps = Arc::new(AtomicUsize::new(0));
let decode_fps = fps.clone();
let mut skip_beginning = 0;
let fps_map = Arc::new(RwLock::new(HashMap::new()));
let decode_fps_map = fps_map.clone();
std::thread::spawn(move || {
#[cfg(windows)]
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 {
if let Ok(data) = video_receiver.recv() {
match data {
MediaData::VideoFrame(_) | MediaData::VideoQueue => {
let vf = if let MediaData::VideoFrame(vf) = data {
*vf
} else {
MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => {
let vf = match data {
MediaData::VideoFrame(vf) => *vf,
MediaData::VideoQueue(display) => {
if let Some(video_queue) =
video_queue_map.read().unwrap().get(&display)
{
if let Some(vf) = video_queue.pop() {
vf
} else {
continue;
}
};
let start = std::time::Instant::now();
if let Ok(true) = video_handler.handle_frame(vf) {
video_callback(&mut video_handler.rgb);
// fps calculation
// The first frame will be very slow
if skip_beginning < 5 {
skip_beginning += 1;
} else {
continue;
}
duration += start.elapsed();
count += 1;
if count % 10 == 0 {
fps.store(
(count * 1000 / duration.as_millis()) as usize,
Ordering::Relaxed,
}
_ => {
// unreachable!();
continue;
}
};
let display = vf.display as usize;
let start = std::time::Instant::now();
if handler_controller_map.len() <= display {
for _i in handler_controller_map.len()..=display {
handler_controller_map.push(VideoHandlerController {
handler: VideoHandler::new(),
count: 0,
duration: std::time::Duration::ZERO,
skip_beginning: 0,
});
}
}
if let Some(handler_controller) = handler_controller_map.get_mut(display) {
match handler_controller.handler.handle_frame(vf) {
Ok(true) => {
video_callback(display, &mut handler_controller.handler.rgb);
// fps calculation
// The first frame will be very slow
if handler_controller.skip_beginning < 5 {
handler_controller.skip_beginning += 1;
continue;
}
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 count > 150 {
count = 0;
duration = Duration::ZERO;
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 => {
video_handler.reset();
MediaData::Reset(display) => {
if let Some(handler_controler) = handler_controller_map.get_mut(display) {
handler_controler.handler.reset();
}
}
MediaData::RecordScreen(start, display, 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);
}
}
MediaData::RecordScreen(start, w, h, id) => {
video_handler.record_screen(start, w, h, id)
}
_ => {}
}
@ -1882,7 +1972,12 @@ where
log::info!("Video decoder loop exits");
});
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
@ -2500,7 +2595,7 @@ pub enum Data {
SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
AddJob((i32, String, String, i32, bool, bool)),
ResumeJob((i32, bool)),
RecordScreen(bool, i32, i32, String),
RecordScreen(bool, usize, i32, i32, String),
ElevateDirect,
ElevateWithLogon(String, String),
NewVoiceCall,

View File

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

View File

@ -1,33 +1,41 @@
use std::collections::HashMap;
use std::num::NonZeroI64;
use std::sync::{
use std::{
collections::HashMap,
num::NonZeroI64,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
Arc, RwLock,
},
};
#[cfg(windows)]
use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend};
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")))]
use hbb_common::sleep;
#[cfg(not(target_os = "ios"))]
use hbb_common::tokio::sync::mpsc::error::TryRecvError;
#[cfg(windows)]
use hbb_common::tokio::sync::Mutex as TokioMutex;
use hbb_common::tokio::{
use hbb_common::{
allow_err,
config::{PeerConfig, TransferSerde},
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 crate::client::{
@ -43,7 +51,7 @@ use crate::{client::Data, client::Interface};
pub struct Remote<T: InvokeUiSession> {
handler: Session<T>,
video_queue: Arc<ArrayQueue<VideoFrame>>,
video_queue_map: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender,
audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>,
@ -61,27 +69,27 @@ pub struct Remote<T: InvokeUiSession> {
#[cfg(windows)]
client_conn_id: i32, // used for file clipboard
data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>,
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
video_format: CodecFormat,
elevation_requested: bool,
fps_control: FpsControl,
decode_fps: Arc<AtomicUsize>,
fps_control_map: HashMap<usize, FpsControl>,
decode_fps_map: Arc<RwLock<HashMap<usize, usize>>>,
}
impl<T: InvokeUiSession> Remote<T> {
pub fn new(
handler: Session<T>,
video_queue: Arc<ArrayQueue<VideoFrame>>,
video_queue: Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
video_sender: MediaSender,
audio_sender: MediaSender,
receiver: mpsc::UnboundedReceiver<Data>,
sender: mpsc::UnboundedSender<Data>,
frame_count: Arc<AtomicUsize>,
decode_fps: Arc<AtomicUsize>,
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
decode_fps: Arc<RwLock<HashMap<usize, usize>>>,
) -> Self {
Self {
handler,
video_queue,
video_queue_map: video_queue,
video_sender,
audio_sender,
receiver,
@ -96,13 +104,13 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(windows)]
client_conn_id: 0,
data_count: Arc::new(AtomicUsize::new(0)),
frame_count,
frame_count_map,
video_format: CodecFormat::Unknown,
stop_voice_call_sender: None,
voice_call_request_timestamp: None,
elevation_requested: false,
fps_control: Default::default(),
decode_fps,
fps_control_map: Default::default(),
decode_fps_map: decode_fps,
}
}
@ -152,7 +160,7 @@ impl<T: InvokeUiSession> Remote<T> {
|| self.handler.is_rdp();
if !is_conn_not_default {
(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)]
@ -229,12 +237,18 @@ impl<T: InvokeUiSession> Remote<T> {
let mut speed = self.data_count.swap(0, Ordering::Relaxed);
speed = speed * 1000 / elapsed as usize;
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
let mut fps = self.frame_count.swap(0, Ordering::Relaxed) as _;
let mut frame_count_map_write = self.frame_count_map.write().unwrap();
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
fps = fps * 1000 / elapsed as i32;
(k.clone(), (*v as i32) * 1000 / elapsed as i32)
}).collect::<HashMap<usize, i32>>();
self.handler.update_quality_status(QualityStatus {
speed:Some(speed),
fps:Some(fps),
speed: Some(speed),
fps,
..Default::default()
});
}
@ -260,7 +274,7 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if _set_disconnected_ok {
Client::try_stop_clipboard(&self.handler.session_id);
Client::try_stop_clipboard(&self.handler.id);
}
#[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
.video_sender
.send(MediaData::RecordScreen(start, w, h, id));
.send(MediaData::RecordScreen(start, display, w, h, id));
}
Data::ElevateDirect => {
let mut request = ElevationRequest::new();
@ -904,15 +918,25 @@ impl<T: InvokeUiSession> Remote<T> {
None => false,
}
}
#[inline]
fn fps_control(&mut self, direct: bool) {
let len = self.video_queue.len();
let ctl = &mut self.fps_control;
// Current full speed decoding fps
let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed);
if decode_fps == 0 {
return;
let decode_fps_read = self.decode_fps_map.read().unwrap();
for (display, decode_fps) in decode_fps_read.iter() {
let video_queue_map_read = self.video_queue_map.read().unwrap();
let Some(video_queue) = video_queue_map_read.get(display) else {
continue;
};
if !self.fps_control_map.contains_key(display) {
self.fps_control_map.insert(*display, FpsControl::default());
}
let Some(ctl) = self.fps_control_map.get_mut(display) else {
return;
};
let len = video_queue.len();
let decode_fps = *decode_fps;
let limited_fps = if direct {
decode_fps * 9 / 10 // 30 got 27
} else {
@ -979,16 +1003,17 @@ impl<T: InvokeUiSession> Remote<T> {
}
// send refresh
if ctl.refresh_times < 10 // enough
&& (len > self.video_queue.capacity() / 2
&& (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(_) = self.video_queue.pop() {}
self.handler.refresh_video();
while let Some(_) = video_queue.pop() {}
self.handler.refresh_video(*display as _);
ctl.refresh_times += 1;
ctl.last_refresh_instant = Instant::now();
}
}
}
async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
if let Ok(msg_in) = Message::parse_from_bytes(&data) {
@ -1008,14 +1033,27 @@ impl<T: InvokeUiSession> Remote<T> {
..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) {
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
.send(MediaData::VideoFrame(Box::new(vf)))
.ok();
} else {
self.video_queue.force_push(vf);
self.video_sender.send(MediaData::VideoQueue).ok();
if let Some(video_queue) = video_queue_write.get_mut(&display) {
video_queue.force_push(vf);
}
self.video_sender.send(MediaData::VideoQueue(display)).ok();
}
}
Some(message::Union::Hash(hash)) => {
@ -1297,7 +1335,9 @@ impl<T: InvokeUiSession> Remote<T> {
}
Some(misc::Union::SwitchDisplay(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 {
self.handler.set_display(
s.x,
@ -1674,7 +1714,7 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(feature = "flutter")]
if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union {
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;
}

View File

@ -54,6 +54,8 @@ pub const PLATFORM_LINUX: &str = "Linux";
pub const PLATFORM_MACOS: &str = "Mac OS";
pub const PLATFORM_ANDROID: &str = "Android";
const MIN_VER_MULTI_UI_SESSION: &str = "1.2.4";
pub mod input {
pub const MOUSE_TYPE_MOVE: i32 = 0;
pub const MOUSE_TYPE_DOWN: i32 = 1;
@ -120,6 +122,16 @@ pub fn set_server_running(b: bool) {
*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
#[inline]
pub fn is_server() -> bool {
@ -780,7 +792,11 @@ pub fn get_sysinfo() -> serde_json::Value {
}
let hostname = hostname(); // sys.hostname() return localhost on android in my test
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"),
"memory": format!("{memory}GB"),
"os": os,

View File

@ -18,8 +18,6 @@ use hbb_common::{
};
use serde_json::json;
#[cfg(not(feature = "flutter_texture_render"))]
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
collections::HashMap,
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.
}
#[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")]
#[derive(Default, Clone)]
pub struct FlutterHandler {
pub event_stream: Arc<RwLock<Option<StreamSink<EventToUI>>>>,
notify_rendered: Arc<RwLock<bool>>,
renderer: Arc<RwLock<VideoRenderer>>,
// ui session id -> display handler data
session_handlers: Arc<RwLock<HashMap<SessionID, SessionHandler>>>,
peer_info: Arc<RwLock<PeerInfo>>,
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
}
#[cfg(not(feature = "flutter_texture_render"))]
#[derive(Default, Clone)]
pub struct FlutterHandler {
pub event_stream: Arc<RwLock<Option<StreamSink<EventToUI>>>>,
struct RgbaData {
// 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].
pub rgba: Arc<RwLock<Vec<u8>>>,
pub rgba_valid: Arc<AtomicBool>,
data: Vec<u8>,
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>>,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
hooks: Arc<RwLock<HashMap<String, SessionHook>>>,
@ -184,14 +197,22 @@ pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(
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
#[cfg(feature = "flutter_texture_render")]
#[derive(Clone)]
struct VideoRenderer {
// TextureRgba pointer in flutter native.
ptr: Arc<RwLock<usize>>,
width: usize,
height: usize,
is_support_multi_ui_session: bool,
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
}
@ -217,9 +238,8 @@ impl Default for VideoRenderer {
}
};
Self {
ptr: Default::default(),
width: 0,
height: 0,
map_display_sessions: Default::default(),
is_support_multi_ui_session: false,
on_rgba_func,
}
}
@ -228,33 +248,74 @@ impl Default for VideoRenderer {
#[cfg(feature = "flutter_texture_render")]
impl VideoRenderer {
#[inline]
pub fn set_size(&mut self, width: usize, height: usize) {
self.width = width;
self.height = height;
fn set_size(&mut self, display: usize, width: usize, height: usize) {
let mut sessions_lock = self.map_display_sessions.write().unwrap();
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) {
let ptr = self.ptr.read().unwrap();
if *ptr == usize::default() {
fn register_texture(&self, display: usize, ptr: usize) {
let mut sessions_lock = self.map_display_sessions.write().unwrap();
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;
}
// 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!(
"width/height mismatch: ({},{}) != ({},{})",
self.width,
self.height,
info.size.0,
info.size.1,
rgba.w,
rgba.h
);
return;
}
if let Some(func) = &self.on_rgba_func {
unsafe {
func(
*ptr as _,
info.texture_rgba_ptr as _,
rgba.raw.as_ptr() as _,
rgba.raw.len() 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 {
/// Push an event to the event queue.
/// An event is stored as json in the event queue.
/// Push an event to all the event queues.
/// An event is stored as json in the event queues.
///
/// # Arguments
///
/// * `name` - The name of the event.
/// * `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();
debug_assert!(h.get("name").is_none());
h.insert("name", name);
let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned());
Some(
self.event_stream
.read()
.unwrap()
.as_ref()?
.add(EventToUI::Event(out)),
)
for (_, session) in self.session_handlers.read().unwrap().iter() {
if let Some(stream) = &session.event_stream {
stream.add(EventToUI::Event(out.clone()));
}
}
}
pub(crate) fn close_event_stream(&self) {
let mut stream_lock = self.event_stream.write().unwrap();
if let Some(stream) = &*stream_lock {
stream.add(EventToUI::Event("close".to_owned()));
pub(crate) fn close_event_stream(&self, session_id: SessionID) {
// to-do: Make sure the following logic is correct.
// No need to remove the display handler, because it will be removed when the connection is closed.
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 {
@ -314,6 +383,7 @@ impl FlutterHandler {
serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned())
}
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub(crate) fn add_session_hook(&self, key: String, hook: SessionHook) -> bool {
let mut hooks = self.hooks.write().unwrap();
@ -325,6 +395,7 @@ impl FlutterHandler {
true
}
#[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub(crate) fn remove_session_hook(&self, key: &String) -> bool {
let mut hooks = self.hooks.write().unwrap();
@ -335,27 +406,6 @@ impl FlutterHandler {
let _ = hooks.remove(key);
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 {
@ -408,7 +458,10 @@ impl InvokeUiSession for FlutterHandler {
"update_quality_status",
vec![
("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())),
(
"target_bitrate",
@ -529,7 +582,7 @@ impl InvokeUiSession for FlutterHandler {
#[inline]
#[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.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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.
// 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();
if let Some(rgba_data) = rgba_write_lock.get_mut(&display) {
if rgba_data.valid {
return;
} else {
rgba_data.valid = true;
}
self.rgba_valid.store(true, Ordering::Relaxed);
// 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());
if let Some(stream) = &*self.event_stream.read().unwrap() {
stream.add(EventToUI::Rgba);
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);
}
drop(rgba_write_lock);
// Non-texture-render UI does not support multiple displays in the one UI session.
// It's Ok to notify each session for now.
for h in self.session_handlers.read().unwrap().values() {
if let Some(stream) = &h.event_stream {
stream.add(EventToUI::Rgba(display));
}
}
}
#[inline]
#[cfg(feature = "flutter_texture_render")]
fn on_rgba(&self, rgba: &mut scrap::ImageRgb) {
self.renderer.read().unwrap().on_rgba(rgba);
if *self.notify_rendered.read().unwrap() {
return;
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) {
let mut try_notify_sessions = Vec::new();
for (id, session) in self.session_handlers.read().unwrap().iter() {
session.renderer.on_rgba(display, rgba);
if !session.notify_rendered {
try_notify_sessions.push(id.clone());
}
}
if try_notify_sessions.len() > 0 {
let mut write_lock = self.session_handlers.write().unwrap();
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;
}
}
}
if let Some(stream) = &*self.event_stream.read().unwrap() {
stream.add(EventToUI::Rgba);
*self.notify_rendered.write().unwrap() = true;
}
}
@ -578,6 +655,17 @@ impl InvokeUiSession for FlutterHandler {
let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
*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(
"peer_info",
vec![
@ -701,19 +789,29 @@ impl InvokeUiSession for FlutterHandler {
}
#[inline]
fn get_rgba(&self) -> *const u8 {
fn get_rgba(&self, _display: usize) -> *const u8 {
#[cfg(not(feature = "flutter_texture_render"))]
if self.rgba_valid.load(Ordering::Relaxed) {
return self.rgba.read().unwrap().as_ptr();
if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) {
if rgba_data.valid {
return rgba_data.data.as_ptr();
}
}
std::ptr::null_mut()
}
#[inline]
fn next_rgba(&self) {
fn next_rgba(&self, _display: usize) {
#[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.
@ -733,18 +831,6 @@ pub fn session_add(
force_relay: bool,
password: String,
) -> 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 {
ConnType::FILE_TRANSFER
} else if is_port_forward {
@ -757,6 +843,26 @@ pub fn session_add(
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() {
None
} else {
@ -768,12 +874,8 @@ pub fn session_add(
.write()
.unwrap()
.initialize(id.to_owned(), conn_type, switch_uuid, force_relay);
let session = Arc::new(session.clone());
if let Some(same_id_session) = sessions::add_session(session_id.to_owned(), session.clone()) {
log::error!("Should not happen");
same_id_session.close();
}
sessions::insert_session(session_id.to_owned(), conn_type, session.clone());
Ok(session)
}
@ -789,7 +891,31 @@ pub fn session_start_(
id: &str,
event_stream: StreamSink<EventToUI>,
) -> 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:
// 1. "Move tab to new window"
// 2. multi ui session within the same peer connnection.
let mut is_connected = false;
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()
);
}
if let Some(session) = sessions::get_session_by_session_id(session_id) {
let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1;
if !is_connected && is_first_ui_session {
#[cfg(feature = "flutter_texture_render")]
log::info!(
"Session {} start, render by flutter texture rgba plugin",
@ -797,10 +923,7 @@ pub fn session_start_(
);
#[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();
session.close_event_stream();
*session.event_stream.write().unwrap() = Some(event_stream);
if !is_pre_added {
let session = (*session).clone();
std::thread::spawn(move || {
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")))]
pub fn update_text_clipboard_required() {
let is_required = sessions::get_sessions()
.iter()
.any(|session| session.is_text_clipboard_required());
.any(|s| s.is_text_clipboard_required());
Client::set_is_text_clipboard_required(is_required);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn send_text_clipboard_msg(msg: Message) {
for session in sessions::get_sessions() {
if session.is_text_clipboard_required() {
session.send(Data::Message(msg.clone()));
for s in sessions::get_sessions() {
if s.is_text_clipboard_required() {
s.send(Data::Message(msg.clone()));
}
}
}
@ -998,6 +1128,11 @@ pub fn get_cur_session_id() -> SessionID {
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) {
if get_cur_session_id() != 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))
}
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"))]
if let Some(session) = sessions::get_session(&_session_id) {
return session.rgba.read().unwrap().len();
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
return session
.display_rgbas
.read()
.unwrap()
.get(&_display)
.map_or(0, |rgba| rgba.data.len());
}
0
}
#[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 Some(session) = sessions::get_session(&session_id) {
return session.get_rgba();
if let Some(s) = sessions::get_session_by_session_id(&session_id) {
return s.ui_handler.get_rgba(display);
}
}
std::ptr::null()
}
pub fn session_next_rgba(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) {
return session.next_rgba();
pub fn session_next_rgba(session_id: SessionID, display: usize) {
if let Some(s) = sessions::get_session_by_session_id(&session_id) {
return s.ui_handler.next_rgba(display);
}
}
#[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")]
if let Some(session) = sessions::get_session(&_session_id) {
session.register_texture(_ptr);
return;
for s in sessions::get_sessions() {
if let Some(h) = s
.ui_handler
.session_handlers
.write()
.unwrap()
.get_mut(&_session_id)
{
h.notify_rendered = false;
h.renderer.set_size(_display, _width, _height);
break;
}
}
}
#[inline]
pub fn push_session_event(
session_id: &SessionID,
name: &str,
event: Vec<(&str, &str)>,
) -> Option<bool> {
sessions::get_session(session_id)?.push_event(name, event)
pub fn session_register_texture(_session_id: SessionID, _display: usize, _ptr: usize) {
#[cfg(feature = "flutter_texture_render")]
for s in sessions::get_sessions() {
if let Some(h) = s
.ui_handler
.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]
@ -1123,7 +1287,7 @@ fn session_send_touch_scale(
) {
match v.get("v").and_then(|s| s.as_i64()) {
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);
}
}
@ -1147,7 +1311,7 @@ fn session_send_touch_pan(
v.get("y").and_then(|y| y.as_i64()),
) {
(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
.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.
#[derive(Clone)]
pub enum SessionHook {
@ -1199,7 +1372,7 @@ pub enum SessionHook {
#[inline]
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.
@ -1207,22 +1380,118 @@ pub mod sessions {
use super::*;
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]
pub fn add_session(session_id: SessionID, session: FlutterSession) -> Option<FlutterSession> {
SESSIONS.write().unwrap().insert(session_id, session)
pub fn get_session_count(peer_id: String, conn_type: ConnType) -> usize {
SESSIONS
.read()
.unwrap()
.get(&(peer_id, conn_type))
.map(|s| s.ui_handler.session_handlers.read().unwrap().len())
.unwrap_or(0)
}
#[inline]
pub fn remove_session(session_id: &SessionID) -> Option<FlutterSession> {
SESSIONS.write().unwrap().remove(session_id)
pub fn get_peer_id_by_session_id(id: &SessionID, conn_type: ConnType) -> Option<String> {
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]
pub fn get_session(session_id: &SessionID) -> Option<FlutterSession> {
SESSIONS.read().unwrap().get(session_id).cloned()
pub fn get_session_by_session_id(id: &SessionID) -> Option<FlutterSession> {
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]
@ -1230,26 +1499,14 @@ pub mod sessions {
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]
#[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
.read()
.unwrap()
.keys()
.filter(|k| *k != session_id)
.count()
!= 0
.get(&(peer_id, conn_type))
.map(|s| s.session_handlers.read().unwrap().len() != 0)
.unwrap_or(false)
}
}

View File

@ -4,7 +4,7 @@ use crate::{
client::file_trait::FileManager,
common::is_keyboard_mode_supported,
common::make_fd_to_json,
flutter::{self, session_add, session_start_, sessions},
flutter::{self, session_add, session_add_existed, session_start_, sessions},
input::*,
ui_interface::{self, *},
};
@ -16,6 +16,7 @@ use hbb_common::{
config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
fs, lazy_static, log,
message_proto::KeyboardMode,
rendezvous_proto::ConnType,
ResultType,
};
use std::{
@ -67,7 +68,7 @@ pub fn stop_global_event_stream(app_type: String) {
}
pub enum EventToUI {
Event(String),
Rgba,
Rgba(usize),
}
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);
}
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen
// 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
// This function is only used to count the number of control sessions.
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(
session_id: SessionID,
id: String,
@ -112,7 +131,7 @@ pub fn session_start(
}
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())
} else {
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> {
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))
} else {
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> {
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))
} else {
None
@ -147,55 +166,61 @@ pub fn session_login(
password: String,
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);
}
}
pub fn session_close(session_id: SessionID) {
if let Some(session) = sessions::remove_session(&session_id) {
session.close_event_stream();
if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
session.close_event_stream(session_id);
session.close();
}
}
pub fn session_refresh(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) {
session.refresh_video();
pub fn session_refresh(session_id: SessionID, display: usize) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.refresh_video(display as _);
}
}
pub fn session_record_screen(session_id: SessionID, start: bool, width: usize, height: usize) {
if let Some(session) = sessions::get_session(&session_id) {
session.record_screen(start, width as _, height as _);
pub fn session_record_screen(
session_id: SessionID,
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) {
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);
}
}
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);
}
}
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);
session.toggle_option(value.clone());
}
#[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();
}
}
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))
} else {
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) {
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);
}
}
// 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> {
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))
} else {
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> {
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())
} else {
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) {
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);
}
}
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())
} else {
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) {
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);
}
}
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())
} else {
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) {
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);
}
}
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())
} else {
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) {
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());
_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> {
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())
} else {
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) {
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);
}
}
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>> {
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())
} else {
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> {
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[..]) {
SyncReturn(is_keyboard_mode_supported(
&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) {
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);
}
}
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);
}
}
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();
}
}
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();
}
}
pub fn session_switch_display(session_id: SessionID, value: i32) {
if let Some(session) = sessions::get_session(&session_id) {
session.switch_display(value);
pub fn session_switch_display(session_id: SessionID, value: Vec<i32>) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
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,
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();
session.handle_flutter_key_event(
&keyboard_mode,
@ -395,7 +439,7 @@ pub fn session_handle_flutter_key_event(
// This will cause the keyboard input to take no effect.
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
#[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();
if _enter {
set_cur_session_id_(_session_id, &keyboard_mode);
@ -417,14 +461,14 @@ pub fn session_input_key(
shift: 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"))]
session.input_key(&name, down, press, alt, ctrl, shift, command);
}
}
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"))]
session.input_string(&value);
}
@ -432,33 +476,33 @@ pub fn session_input_string(session_id: SessionID, value: String) {
// chat_client_mode
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);
}
}
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);
}
}
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);
}
"".to_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);
}
}
// File Action
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);
}
}
@ -472,7 +516,7 @@ pub fn session_send_files(
include_hidden: 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);
}
}
@ -485,7 +529,7 @@ pub fn session_set_confirm_override_file(
remember: 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);
}
}
@ -497,7 +541,7 @@ pub fn session_remove_file(
file_num: 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.remove_file(act_id, path, file_num, is_remote);
}
}
@ -509,7 +553,7 @@ pub fn session_read_dir_recursive(
is_remote: 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);
}
}
@ -520,19 +564,19 @@ pub fn session_remove_all_empty_dirs(
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.remove_dir(act_id, path, is_remote);
}
}
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);
}
}
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);
}
}
@ -549,14 +593,14 @@ pub fn session_read_local_dir_sync(
}
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);
}
"".to_string()
}
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();
} else {
// a tip for flutter dev
@ -576,46 +620,44 @@ pub fn session_add_job(
include_hidden: 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);
}
}
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);
}
}
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();
}
}
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);
}
}
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();
}
}
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);
}
}
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")]
if let Some(session) = sessions::get_session(&_session_id) {
session.set_size(_width, _height);
}
super::flutter::session_set_size(_session_id, _display, _width, _height)
}
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)
}
pub fn main_get_current_display() -> SyncReturn<String> {
pub fn main_get_main_display() -> SyncReturn<String> {
#[cfg(target_os = "ios")]
let display_info = "".to_owned();
#[cfg(not(target_os = "ios"))]
let display_info = match crate::video_service::get_current_display() {
Ok((_, _, display)) => serde_json::to_string(&HashMap::from([
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(),
Err(..) => "".to_string(),
};
#[cfg(target_os = "ios")]
let display_info = "".to_owned();
.unwrap_or_default();
}
}
SyncReturn(display_info)
}
@ -1029,31 +1075,31 @@ pub fn session_add_port_forward(
remote_host: String,
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);
}
}
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);
}
}
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();
}
}
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();
}
}
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();
}
}
@ -1238,20 +1284,20 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) {
_ => 0,
} << 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);
}
}
}
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();
}
}
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)
} else {
"".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) {
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)
}
}
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 msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]);
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) {
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();
}
}
pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
if let Some(session) = sessions::get_session(&session_id) {
session.ui_handler.on_waiting_for_image_dialog_show();
}
super::flutter::session_on_waiting_for_image_dialog_show(session_id);
}
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))
}
pub fn session_get_rgba_size(session_id: SessionID) -> SyncReturn<usize> {
SyncReturn(super::flutter::session_get_rgba_size(session_id))
pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> SyncReturn<usize> {
SyncReturn(super::flutter::session_get_rgba_size(session_id, display))
}
pub fn session_next_rgba(session_id: SessionID) -> SyncReturn<()> {
SyncReturn(super::flutter::session_next_rgba(session_id))
pub fn session_next_rgba(session_id: SessionID, display: usize) -> SyncReturn<()> {
SyncReturn(super::flutter::session_next_rgba(session_id, display))
}
pub fn session_register_texture(session_id: SessionID, ptr: usize) -> SyncReturn<()> {
SyncReturn(super::flutter::session_register_texture(session_id, ptr))
pub fn session_register_texture(
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>) {
@ -1522,15 +1572,15 @@ pub fn main_update_me() -> SyncReturn<bool> {
}
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())
}
}
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);
#[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> {
@ -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.
///
/// * 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")]
pub mod server_side {
use hbb_common::{config, log};

View File

@ -214,6 +214,7 @@ static mut IS_0X021D_DOWN: bool = false;
#[cfg(target_os = "macos")]
static mut IS_LEFT_OPTION_DOWN: bool = false;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn get_keyboard_mode() -> String {
#[cfg(not(any(feature = "flutter", feature = "cli")))]
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", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "按交集过滤"),
("Remove wallpaper during incoming sessions", "接受会话时移除桌面壁纸"),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

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"),
("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"),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("Filter by intersection", "Nach Schnittpunkt filtern")
("Filter by intersection", "Nach Schnittmenge filtern"),
("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

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!"),
("pull_group_failed_tip", "Failed to refresh group"),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "Filter berdasarkan interseksi"),
("Remove wallpaper during incoming sessions", "Hilangkan latar dinding ketika ada sesi yang masuk"),
("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();
}

View File

@ -559,5 +559,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", "Filtra per incrocio"),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

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"),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

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 до версии {} или новее!"),
("pull_group_failed_tip", "Невозможно обновить группу"),
("Filter by intersection", "Фильтровать по пересечению"),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("Remove wallpaper during incoming sessions", "Удалить обои в сеансе"),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -558,5 +558,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("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();
}

View File

@ -1341,6 +1341,14 @@ impl WallPaperRemover {
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 {

View File

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

View File

@ -2392,6 +2392,10 @@ impl WallPaperRemover {
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> {
// 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/

View File

@ -26,7 +26,7 @@ use hbb_common::{
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use service::ServiceTmpl;
use service::{GenericService, Service, Subscriber};
use service::{EmptyExtraFieldService, GenericService, Service, Subscriber};
use crate::ipc::Data;
@ -53,6 +53,7 @@ pub const NAME_POS: &'static str = "";
}
mod connection;
pub mod display_service;
#[cfg(windows)]
pub mod portable_service;
mod service;
@ -80,7 +81,7 @@ lazy_static::lazy_static! {
pub struct Server {
connections: ConnMap,
services: HashMap<&'static str, Box<dyn Service>>,
services: HashMap<String, Box<dyn Service>>,
id_count: i32,
}
@ -94,11 +95,15 @@ pub fn new() -> ServerPtr {
id_count: hbb_common::rand::random::<i32>() % 1000 + 1000, // ensure positive
};
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")))]
{
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_pos()));
}
@ -253,9 +258,19 @@ async fn create_relay_connection_(
}
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>) {
let primary_video_service_name =
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX);
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());
}
}
@ -287,8 +302,12 @@ impl Server {
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) {
if let Some(s) = self.services.get(&name) {
if let Some(s) = self.services.get(name) {
if s.is_subed(conn.id()) == sub {
return;
}
@ -305,6 +324,47 @@ impl Server {
self.id_count += 1;
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 {

View File

@ -24,16 +24,16 @@ static RESTARTING: AtomicBool = AtomicBool::new(false);
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.repeat::<cpal_impl::State, _>(33, cpal_impl::run);
sp
let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
GenericService::repeat::<cpal_impl::State, _, _>(&svc.clone(), 33, cpal_impl::run);
svc.sp
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run(pa_impl::run);
sp
let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
GenericService::run(&svc.clone(), pa_impl::run);
svc.sp
}
pub fn restart() {
@ -48,7 +48,7 @@ pub fn restart() {
mod pa_impl {
use super::*;
#[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
RESTARTING.store(false, Ordering::SeqCst);
#[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| {
match &state.stream {
None => {

View File

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

View File

@ -15,7 +15,7 @@ use crate::{
new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender,
},
common::{get_default_sound_input, set_sound_input},
video_service,
display_service, video_service,
};
#[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
@ -160,6 +160,7 @@ pub enum AuthConnType {
pub struct Connection {
inner: ConnInner,
display_idx: usize,
stream: super::Stream,
server: super::ServerPtrWeak,
hash: Hash,
@ -303,6 +304,7 @@ impl Connection {
tx: Some(tx),
tx_video: Some(tx_video),
},
display_idx: *display_service::PRIMARY_DISPLAY_IDX,
stream,
server,
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 {
@ -1098,19 +1103,20 @@ impl Connection {
pi.username = username;
pi.sas_enabled = sas_enabled;
pi.features = Some(Features {
privacy_mode: video_service::is_privacy_mode_supported(),
privacy_mode: display_service::is_privacy_mode_supported(),
..Default::default()
})
.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")))]
{
pi.resolutions = Some(SupportedResolutions {
resolutions: video_service::get_current_display()
.map(|(_, _, d)| crate::platform::resolutions(&d.name()))
resolutions: display_service::try_get_displays()
.map(|displays| {
displays
.get(self.display_idx)
.map(|d| crate::platform::resolutions(&d.name()))
.unwrap_or(vec![])
})
.unwrap_or(vec![]),
..Default::default()
})
@ -1126,16 +1132,15 @@ impl Connection {
self.send(msg_out).await;
}
match super::video_service::get_displays().await {
match super::display_service::get_displays().await {
Err(err) => {
res.set_error(format!("{}", err));
}
Ok((current, displays)) => {
Ok(displays) => {
pi.displays = displays.clone();
pi.current_display = current as _;
pi.current_display = self.display_idx as _;
res.set_peer_info(pi);
sub_service = true;
*super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays;
}
}
Self::on_remote_authorized();
@ -1289,6 +1294,8 @@ impl Connection {
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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();
}
@ -1945,15 +1952,13 @@ impl Connection {
},
Some(message::Union::Misc(misc)) => match misc.union {
Some(misc::Union::SwitchDisplay(s)) => {
video_service::switch_display(s.display).await;
#[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()
});
self.handle_switch_display(s).await;
}
Some(misc::Union::CaptureDisplays(displays)) => {
let add = displays.add.iter().map(|d| *d as usize).collect::<Vec<_>>();
let sub = displays.sub.iter().map(|d| *d as usize).collect::<Vec<_>>();
let set = displays.set.iter().map(|d| *d as usize).collect::<Vec<_>>();
self.capture_displays(&add, &sub, &set).await;
}
Some(misc::Union::ChatMessage(c)) => {
self.send_to_cm(ipc::Data::ChatMessage { text: c.text });
@ -1965,10 +1970,16 @@ impl Connection {
}
Some(misc::Union::RefreshVideo(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();
}
Some(misc::Union::RefreshVideoDisplay(display)) => {
self.refresh_video_display(Some(display as usize));
self.update_auto_disconnect_timer();
}
Some(misc::Union::VideoReceived(_)) => {
video_service::notify_video_frame_fetched(
self.inner.id,
@ -2084,6 +2095,75 @@ impl Connection {
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)]
async fn handle_elevation_request(&mut self, para: portable_client::StartPara) {
let mut err;
@ -2106,10 +2186,37 @@ impl Connection {
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")))]
fn change_resolution(&mut self, r: &Resolution) {
if self.keyboard {
if let Ok((_, _, display)) = video_service::get_current_display() {
if let Ok(displays) = display_service::try_get_displays() {
if let Some(display) = displays.get(self.display_idx) {
let name = display.name();
#[cfg(all(windows, feature = "virtual_display_driver"))]
if let Some(_ok) =
@ -2121,7 +2228,7 @@ impl Connection {
{
return;
}
video_service::set_last_changed_resolution(
display_service::set_last_changed_resolution(
&name,
(display.width() as _, display.height() as _),
(r.width, r.height),
@ -2140,6 +2247,7 @@ impl Connection {
}
}
}
}
pub async fn handle_voice_call(&mut self, accepted: bool) {
if let Some(ts) = self.voice_call_request_timestamp.take() {
@ -2281,7 +2389,7 @@ impl Connection {
if self.keyboard {
match q {
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(
back_notification::PrivacyModeState::PrvNotSupported,
"Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(),
@ -2289,8 +2397,11 @@ impl Connection {
} else {
match privacy_mode::turn_on_privacy(self.inner.id) {
Ok(true) => {
let err_msg =
video_service::test_create_capturer(self.inner.id, 5_000);
let err_msg = video_service::test_create_capturer(
self.inner.id,
self.display_idx,
5_000,
);
if err_msg.is_empty() {
video_service::set_privacy_mode_conn_id(self.inner.id);
crate::common::make_privacy_mode_msg(
@ -2326,7 +2437,7 @@ impl Connection {
self.send(msg_out).await;
}
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(
back_notification::PrivacyModeState::PrvNotSupported,
"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);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if active_conns_lock.is_empty() {
video_service::reset_resolutions();
display_service::reset_resolutions();
}
#[cfg(all(windows, feature = "virtual_display_driver"))]
if active_conns_lock.is_empty() {
video_service::try_plug_out_virtual_display();
display_service::try_plug_out_virtual_display();
}
#[cfg(all(windows))]
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 std::{
convert::TryFrom,
ops::Sub,
ops::{Deref, DerefMut, Sub},
sync::atomic::{AtomicBool, Ordering},
thread,
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_POS: &'static str = "mouse_pos";
pub type MouseCursorService = ServiceTmpl<MouseCursorSub>;
#[derive(Clone)]
pub struct MouseCursorService {
pub sp: ServiceTmpl<MouseCursorSub>,
}
pub fn new_cursor() -> MouseCursorService {
let sp = MouseCursorService::new(NAME_CURSOR, true);
sp.repeat::<StateCursor, _>(33, run_cursor);
sp
impl Deref for MouseCursorService {
type Target = ServiceTmpl<MouseCursorSub>;
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 {
let sp = GenericService::new(NAME_POS, false);
sp.repeat::<StatePos, _>(33, run_pos);
sp
let svc = EmptyExtraFieldService::new(NAME_POS.to_owned(), false);
GenericService::repeat::<StatePos, _, _>(&svc.clone(), 33, run_pos);
svc.sp
}
#[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();
if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS {
return Ok(());
@ -988,7 +1013,6 @@ pub async fn lock_screen() {
crate::platform::lock_screen();
}
}
super::video_service::switch_to_primary().await;
}
pub fn handle_key(evt: &KeyEvent) {

View File

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

View File

@ -1,16 +1,19 @@
use super::*;
use std::{
collections::HashSet,
ops::{Deref, DerefMut},
thread::{self, JoinHandle},
time,
};
pub trait Service: Send + Sync {
fn name(&self) -> &'static str;
fn name(&self) -> String;
fn on_subscribe(&self, sub: ConnInner);
fn on_unsubscribe(&self, id: i32);
fn is_subed(&self, id: i32) -> bool;
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 {
@ -20,12 +23,13 @@ pub trait Subscriber: Default + Send + Sync + 'static {
#[derive(Default)]
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
name: &'static str,
name: String,
handle: Option<JoinHandle<()>>,
subscribes: HashMap<i32, T>,
new_subscribes: HashMap<i32, T>,
active: bool,
need_snapshot: bool,
options: HashMap<String, String>,
}
pub trait Reset {
@ -37,6 +41,35 @@ pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
pub type GenericService = ServiceTmpl<ConnInner>;
pub const HIBERNATE_TIMEOUT: u64 = 30;
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> {
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> {
#[inline]
fn name(&self) -> &'static str {
self.0.read().unwrap().name
fn name(&self) -> String {
self.0.read().unwrap().name.clone()
}
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> {
@ -105,7 +150,7 @@ impl<T: Subscriber + From<ConnInner>> Clone for 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> {
name,
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]
pub fn has_subscribes(&self) -> bool {
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
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send,
F: 'static + FnMut(Svc, &mut S) -> ResultType<()> + Send,
S: 'static + Default + Reset,
Svc: 'static + Clone + Send + DerefMut<Target = ServiceTmpl<T>>,
{
let interval = time::Duration::from_millis(interval_ms);
let mut callback = callback;
let sp = self.clone();
let sp = svc.clone();
let thread = thread::spawn(move || {
let mut state = S::default();
let mut may_reset = false;
@ -223,14 +284,15 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
}
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
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 thread = thread::spawn(move || {
let mut error_timeout = HIBERNATE_TIMEOUT;
@ -259,7 +321,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
}
log::info!("Service {} exit", sp.name());
});
self.0.write().unwrap().handle = Some(thread);
svc.0.write().unwrap().handle = Some(thread);
}
#[inline]

View File

@ -18,15 +18,11 @@
// to-do:
// https://slhck.info/video/2017/03/01/rate-control.html
use super::{video_qos::VideoQoS, *};
#[cfg(all(windows, feature = "virtual_display_driver"))]
use crate::virtual_display_manager;
use super::{service::ServiceTmpl, video_qos::VideoQoS, *};
#[cfg(windows)]
use crate::{platform::windows::is_process_consent_running, privacy_win_mag};
#[cfg(windows)]
use hbb_common::get_version_number;
use hbb_common::{
protobuf::MessageField,
anyhow::anyhow,
tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Mutex as TokioMutex,
@ -51,71 +47,18 @@ use std::{
};
pub const NAME: &'static str = "video";
struct ChangedResolution {
original: (i32, i32),
changed: (i32, i32),
}
pub const OPTION_DISPLAY_CHANGED: &'static str = "changed";
pub const OPTION_REFRESH: &'static str = "refresh";
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>)>>>) = {
let (tx, rx) = unbounded_channel();
(tx, Arc::new(TokioMutex::new(rx)))
};
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 IS_UAC_RUNNING: 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]
@ -133,15 +76,6 @@ pub fn get_privacy_mode_conn_id() -> i32 {
*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 {
cur: Instant,
send_conn_ids: HashSet<i32>,
@ -192,10 +126,37 @@ impl VideoFrameController {
}
}
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run(run);
sp
#[derive(Clone)]
pub struct VideoService {
sp: GenericService,
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(
@ -221,19 +182,10 @@ fn check_display_changed(
if n != last_n {
return true;
};
for (i, d) in displays.iter().enumerate() {
if d.is_primary() {
if i != last_current {
return true;
};
if d.width() != last_width || d.height() != last_height {
return true;
};
match displays.get(last_current) {
Some(d) => d.width() != last_width || d.height() != last_height,
None => true,
}
}
return false;
}
// Capturer object is expensive, avoiding to create it frequently.
@ -317,16 +269,29 @@ fn create_capturer(
}
// 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();
loop {
let err = match get_current_display() {
Ok((_, current, display)) => {
match create_capturer(privacy_mode_id, display, true, current, false) {
let err = match try_get_displays() {
Ok(mut displays) => {
if displays.len() <= display_idx {
anyhow!(
"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,
};
if test_begin.elapsed().as_millis() >= timeout_millis as _ {
@ -352,6 +317,7 @@ fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> Resu
}
pub(super) struct CapturerInfo {
pub name: String,
pub origin: (i32, i32),
pub width: 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")]
{
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 name = display.name();
log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}",
ndisplay,
@ -395,7 +375,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
height,
num_cpus::get_physical(),
num_cpus::get(),
display.name(),
&name,
);
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,
)?;
Ok(CapturerInfo {
name,
origin,
width,
height,
@ -440,42 +421,13 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
})
}
fn check_displays_new() -> Option<Vec<Display>> {
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<()> {
fn run(vs: VideoService) -> ResultType<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let _wake_lock = get_wake_lock();
// 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")]
super::wayland::ensure_inited()?;
#[cfg(windows)]
@ -483,7 +435,9 @@ fn run(sp: GenericService) -> ResultType<()> {
#[cfg(not(windows))]
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();
video_qos.refresh(None);
@ -506,37 +460,18 @@ fn run(sp: GenericService) -> ResultType<()> {
c.set_use_yuv(encoder.use_yuv());
VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate());
if *SWITCH.lock().unwrap() {
log::debug!("Broadcasting display switch");
let mut misc = Misc::new();
let display_name = get_current_display()
.map(|(_, _, d)| d.name())
.unwrap_or_default();
let original_resolution = get_original_resolution(&display_name, c.width, c.height);
misc.set_switch_display(SwitchDisplay {
display: c.current as _,
x: c.origin.0 as _,
y: c.origin.1 as _,
width: c.width as _,
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);
if sp.is_option_true(OPTION_DISPLAY_CHANGED) {
log::debug!("Broadcasting display changed");
broadcast_display_changed(
display_idx,
&sp,
Some((c.name.clone(), c.origin.clone(), c.width, c.height)),
);
sp.set_option_bool(OPTION_DISPLAY_CHANGED, false);
}
if sp.is_option_true(OPTION_REFRESH) {
sp.set_option_bool(OPTION_REFRESH, false);
}
let mut frame_controller = VideoFrameController::new();
@ -572,13 +507,7 @@ fn run(sp: GenericService) -> ResultType<()> {
}
drop(video_qos);
if *SWITCH.lock().unwrap() {
bail!("SWITCH");
}
if c.current != *CURRENT_DISPLAY.lock().unwrap() {
#[cfg(target_os = "linux")]
super::wayland::clear();
*SWITCH.lock().unwrap() = true;
if sp.is_option_true(OPTION_DISPLAY_CHANGED) || sp.is_option_true(OPTION_REFRESH) {
bail!("SWITCH");
}
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.
#[cfg(target_os = "macos")]
if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
sp.set_option_bool(OPTION_DISPLAY_CHANGED, true);
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");
}
}
*LAST_ACTIVE.lock().unwrap() = now;
frame_controller.reset();
#[cfg(any(target_os = "android", target_os = "ios"))]
@ -631,8 +549,14 @@ fn run(sp: GenericService) -> ResultType<()> {
match frame {
scrap::Frame::RAW(data) => {
if data.len() != 0 {
let send_conn_ids =
handle_one_frame(&sp, data, ms, &mut encoder, recorder.clone())?;
let send_conn_ids = handle_one_frame(
display_idx,
&sp,
data,
ms,
&mut encoder,
recorder.clone(),
)?;
frame_controller.set_send(now, send_conn_ids);
}
}
@ -649,7 +573,7 @@ fn run(sp: GenericService) -> ResultType<()> {
let time = now - start;
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
let send_conn_ids =
handle_one_frame(&sp, &frame, ms, &mut encoder, recorder.clone())?;
handle_one_frame(display_idx, &sp, &frame, ms, &mut encoder, recorder.clone())?;
frame_controller.set_send(now, send_conn_ids);
#[cfg(windows)]
{
@ -693,7 +617,7 @@ fn run(sp: GenericService) -> ResultType<()> {
log::info!("Displays changed");
#[cfg(target_os = "linux")]
super::wayland::clear();
*SWITCH.lock().unwrap() = true;
sp.set_option_bool(OPTION_DISPLAY_CHANGED, true);
bail!("SWITCH");
}
@ -829,6 +753,7 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu
#[inline]
fn handle_one_frame(
display: usize,
sp: &GenericService,
frame: &[u8],
ms: i64,
@ -844,7 +769,10 @@ fn handle_one_frame(
})?;
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"))]
recorder
.lock()
@ -856,69 +784,6 @@ fn handle_one_frame(
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> {
#[cfg(target_os = "linux")]
if !scrap::is_x11() {
@ -927,65 +792,10 @@ pub fn is_inited_msg() -> Option<Message> {
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]
pub fn refresh() {
#[cfg(target_os = "android")]
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]
@ -1034,30 +844,6 @@ fn try_get_displays() -> ResultType<Vec<Display>> {
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)]
fn start_uac_elevation_check() {
static START: Once = Once::new();
@ -1090,3 +876,63 @@ fn get_wake_lock() -> crate::platform::WakeLock {
};
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 {
let mut lock = CAP_DISPLAY_INFO.write().unwrap();
if *lock == 0 {
let all = Display::all()?;
let mut all = Display::all()?;
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() {
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()));
}
let (ndisplay, current, display) =
super::video_service::get_current_display_2(all)?;
let display = all.remove(current);
let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}",
ndisplay,
num,
current,
&origin,
width,
@ -213,16 +214,14 @@ pub(super) async fn check_init() -> ResultType<()> {
Ok(())
}
pub(super) async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
pub(super) async fn get_displays() -> ResultType<Vec<DisplayInfo>> {
check_init().await?;
let addr = *CAP_DISPLAY_INFO.read().unwrap();
if addr != 0 {
let cap_display_info: *const CapDisplayInfo = addr as _;
unsafe {
let cap_display_info = &*cap_display_info;
let primary = cap_display_info.primary;
let displays = cap_display_info.displays.clone();
Ok((primary, displays))
Ok(cap_display_info.displays.clone())
}
} else {
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 rect = cap_display_info.rects[cap_display_info.current];
Ok(super::video_service::CapturerInfo {
name: cap_display_info.displays[cap_display_info.current]
.name
.clone(),
origin: rect.0,
width: rect.1,
height: rect.2,

View File

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

View File

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

View File

@ -210,6 +210,7 @@ class Enhancements: Reactor.Component {
function render() {
var has_hwcodec = handler.has_hwcodec();
var support_remove_wallpaper = handler.support_remove_wallpaper();
var me = this;
self.timer(1ms, function() { me.toggleMenuState() });
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> : ""}
<li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive bitrate")} (beta)</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>
</li>;
}

View File

@ -122,7 +122,11 @@ impl InvokeUiSession for SciterHandler {
"updateQualityStatus",
&make_args!(
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.target_bitrate.map_or(Value::null(), |it| it.into()),
status
@ -223,7 +227,7 @@ impl InvokeUiSession for SciterHandler {
self.call("adaptSize", &make_args!());
}
fn on_rgba(&self, rgba: &mut scrap::ImageRgb) {
fn on_rgba(&self, _display: usize, rgba: &mut scrap::ImageRgb) {
VIDEO
.lock()
.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.
fn get_rgba(&self) -> *const u8 {
fn get_rgba(&self, _display: usize) -> *const u8 {
std::ptr::null()
}
fn next_rgba(&self) {}
fn next_rgba(&self, _display: usize) {}
}
pub struct SciterSession(Session<SciterHandler>);
@ -449,8 +453,8 @@ impl sciter::EventHandler for SciterSession {
fn save_view_style(String);
fn save_image_quality(String);
fn save_custom_image_quality(i32);
fn refresh_video();
fn record_screen(bool, i32, i32);
fn refresh_video(i32);
fn record_screen(bool, i32, i32, i32);
fn record_status(bool);
fn get_toggle_option(String);
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_cursor_embedded = cursor_embedded;
adaptDisplay();
if (recording) handler.record_screen(true, w, h);
if (recording) handler.record_screen(true, 0, w, h);
}
// in case toolbar not shown correctly
@ -478,7 +478,7 @@ function self.closing() {
var (x, y, w, h) = view.box(#rectw, #border, #screen);
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 (recording) handler.record_screen(false, display_width, display_height);
if (recording) handler.record_screen(false, 0, display_width, display_height);
}
var qualityMonitor;

View File

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

View File

@ -2,14 +2,14 @@
use hbb_common::password_security;
use hbb_common::{
allow_err,
config::{self, Config, LocalConfig, PeerConfig},
directories_next, log, tokio,
};
use hbb_common::{
bytes::Bytes,
config::{self, Config, LocalConfig, PeerConfig},
config::{CONNECT_TIMEOUT, RENDEZVOUS_PORT},
directories_next,
futures::future::join_all,
log,
rendezvous_proto::*,
tokio,
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::{
@ -17,9 +17,10 @@ use hbb_common::{
tokio::{sync::mpsc, time},
};
use serde_derive::Serialize;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::process::Child;
use std::{
collections::HashMap,
process::Child,
sync::{Arc, Mutex},
};
@ -31,6 +32,7 @@ use crate::ipc;
type Message = RendezvousMessage;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub type Children = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
#[derive(Clone, Debug, Serialize)]
@ -594,7 +596,13 @@ pub fn current_is_wayland() -> bool {
#[inline]
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]
@ -1248,3 +1256,10 @@ pub fn handle_relay_id(id: String) -> String {
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 rdev::{Event, EventType::*, KeyCode};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::{collections::HashMap, sync::atomic::AtomicBool};
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
str::FromStr,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex, RwLock,
},
sync::{Arc, Mutex, RwLock},
time::SystemTime,
};
use uuid::Uuid;
@ -28,7 +26,7 @@ use hbb_common::{
sync::mpsc,
time::{Duration as TokioDuration, Instant},
},
SessionID, Stream,
Stream,
};
use crate::client::io_loop::Remote;
@ -49,7 +47,6 @@ const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15;
#[derive(Clone, Default)]
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 password: String,
pub args: Vec<String>,
@ -239,10 +236,18 @@ impl<T: InvokeUiSession> Session<T> {
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) {
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) {
self.lc.write().unwrap().save_view_style(value);
}
@ -286,12 +291,30 @@ impl<T: InvokeUiSession> Session<T> {
&& !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()));
}
pub fn record_screen(&self, start: bool, w: i32, h: i32) {
self.send(Data::RecordScreen(start, w, h, self.id.clone()));
pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) {
self.send(Data::RecordScreen(
start,
display as usize,
w,
h,
self.id.clone(),
));
}
pub fn record_status(&self, status: bool) {
@ -603,6 +626,19 @@ impl<T: InvokeUiSession> Session<T> {
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) {
let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) {
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 job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64);
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);
#[cfg(any(target_os = "android", target_os = "ios"))]
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_waiting(&self);
fn on_voice_call_incoming(&self);
fn get_rgba(&self) -> *const u8;
fn next_rgba(&self);
fn get_rgba(&self, display: usize) -> *const u8;
fn next_rgba(&self, display: usize);
}
impl<T: InvokeUiSession> Deref for Session<T> {
@ -1237,7 +1273,7 @@ impl<T: InvokeUiSession> Interface for Session<T> {
if pi.displays.is_empty() {
self.lc.write().unwrap().handle_peer_info(&pi);
self.update_privacy_mode();
self.msgbox("error", "Remote Error", "No Display", "");
self.msgbox("error", "Remote Error", "No Displays", "");
return;
}
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;
}
let frame_count = Arc::new(AtomicUsize::new(0));
let frame_count_cl = frame_count.clone();
let frame_count_map: Arc<RwLock<HashMap<usize, usize>>> = Default::default();
let frame_count_map_cl = frame_count_map.clone();
let ui_handler = handler.ui_handler.clone();
let (video_sender, audio_sender, video_queue, decode_fps) =
start_video_audio_threads(move |data: &mut scrap::ImageRgb| {
frame_count_cl.fetch_add(1, Ordering::Relaxed);
ui_handler.on_rgba(data);
});
let (video_sender, audio_sender, video_queue_map, decode_fps_map) = start_video_audio_threads(
handler.clone(),
move |display: usize, data: &mut scrap::ImageRgb| {
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(
handler,
video_queue,
video_queue_map,
video_sender,
audio_sender,
receiver,
sender,
frame_count,
decode_fps,
frame_count_map,
decode_fps_map,
);
remote.io_loop(&key, &token, round).await;
remote.sync_jobs_status_to_local().await;