view camera (#11040)

* view camera

Signed-off-by: 21pages <sunboeasy@gmail.com>

* `No cameras` prompt if no cameras available,  `peerGetSessionsCount` use
connType as parameter

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix, use video_service_name rather than display_idx as key in qos,etc

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Adwin White <adwinw01@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
21pages
2025-03-10 21:06:53 +08:00
committed by GitHub
parent df4a101316
commit f0f999dc27
96 changed files with 3999 additions and 458 deletions
Generated
+378 -109
View File
File diff suppressed because it is too large Load Diff
+72 -6
View File
@@ -29,8 +29,10 @@ import '../consts.dart';
import 'common/widgets/overlay.dart'; import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'mobile/pages/view_camera_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
@@ -96,6 +98,7 @@ enum DesktopType {
main, main,
remote, remote,
fileTransfer, fileTransfer,
viewCamera,
cm, cm,
portForward, portForward,
} }
@@ -1750,7 +1753,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
await bind.setLocalFlutterOption( await bind.setLocalFlutterOption(
k: windowFramePrefix + type.name, v: pos.toString()); k: windowFramePrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) { if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
windowId != null) {
await _saveSessionWindowPosition( await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos); type, windowId, isMaximized, isFullscreen, pos);
} }
@@ -1901,7 +1905,9 @@ Future<bool> restoreWindowPosition(WindowType type,
String? pos; String? pos;
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
// Though "open in tabs" is true and the new window restore peer position, it's ok. // Though "open in tabs" is true and the new window restore peer position, it's ok.
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) { if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
windowId != null &&
peerId != null) {
final peerPos = bind.mainGetPeerFlutterOptionSync( final peerPos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: windowFramePrefix + type.name); id: peerId, k: windowFramePrefix + type.name);
if (peerPos.isNotEmpty) { if (peerPos.isNotEmpty) {
@@ -1916,7 +1922,7 @@ Future<bool> restoreWindowPosition(WindowType type,
debugPrint("no window position saved, ignoring position restoration"); debugPrint("no window position saved, ignoring position restoration");
return false; return false;
} }
if (type == WindowType.RemoteDesktop) { if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
if (!isRemotePeerPos && windowId != null) { if (!isRemotePeerPos && windowId != null) {
if (lpos.offsetWidth != null) { if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
@@ -2085,6 +2091,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
enum UriLinkType { enum UriLinkType {
remoteDesktop, remoteDesktop,
fileTransfer, fileTransfer,
viewCamera,
portForward, portForward,
rdp, rdp,
} }
@@ -2136,6 +2143,11 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1]; id = args[i + 1];
i++; i++;
break; break;
case '--view-camera':
type = UriLinkType.viewCamera;
id = args[i + 1];
i++;
break;
case '--port-forward': case '--port-forward':
type = UriLinkType.portForward; type = UriLinkType.portForward;
id = args[i + 1]; id = args[i + 1];
@@ -2177,6 +2189,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
password: password, forceRelay: forceRelay); password: password, forceRelay: forceRelay);
}); });
break; break;
case UriLinkType.viewCamera:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newViewCamera(id!,
password: password, forceRelay: forceRelay);
});
break;
case UriLinkType.portForward: case UriLinkType.portForward:
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
rustDeskWinManager.newPortForward(id!, false, rustDeskWinManager.newPortForward(id!, false,
@@ -2200,7 +2218,14 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? urlLinkToCmdArgs(Uri uri) { List<String>? urlLinkToCmdArgs(Uri uri) {
String? command; String? command;
String? id; String? id;
final options = ["connect", "play", "file-transfer", "port-forward", "rdp"]; final options = [
"connect",
"play",
"file-transfer",
"view-camera",
"port-forward",
"rdp"
];
if (uri.authority.isEmpty && if (uri.authority.isEmpty &&
uri.path.split('').every((char) => char == '/')) { uri.path.split('').every((char) => char == '/')) {
return []; return [];
@@ -2238,6 +2263,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
connect(Get.context!, id); connect(Get.context!, id);
} else if (optionIndex == 2) { } else if (optionIndex == 2) {
connect(Get.context!, id, isFileTransfer: true); connect(Get.context!, id, isFileTransfer: true);
} else if (optionIndex == 3) {
connect(Get.context!, id, isViewCamera: true);
} }
return null; return null;
} }
@@ -2290,6 +2317,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
connectMainDesktop(String id, connectMainDesktop(String id,
{required bool isFileTransfer, {required bool isFileTransfer,
required bool isViewCamera,
required bool isTcpTunneling, required bool isTcpTunneling,
required bool isRDP, required bool isRDP,
bool? forceRelay, bool? forceRelay,
@@ -2302,6 +2330,12 @@ connectMainDesktop(String id,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
connToken: connToken, connToken: connToken,
forceRelay: forceRelay); forceRelay: forceRelay);
} else if (isViewCamera) {
await rustDeskWinManager.newViewCamera(id,
password: password,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) { } else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP, await rustDeskWinManager.newPortForward(id, isRDP,
password: password, password: password,
@@ -2318,10 +2352,12 @@ connectMainDesktop(String id,
/// Connect to a peer with [id]. /// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer. /// If [isFileTransfer], starts a session only for file transfer.
/// If [isViewCamera], starts a session only for view camera.
/// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp. /// If [isRDP], starts a session only for rdp.
connect(BuildContext context, String id, connect(BuildContext context, String id,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false, bool isRDP = false,
bool forceRelay = false, bool forceRelay = false,
@@ -2353,6 +2389,7 @@ connect(BuildContext context, String id,
await connectMainDesktop( await connectMainDesktop(
id, id,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP, isRDP: isRDP,
password: password, password: password,
@@ -2363,6 +2400,7 @@ connect(BuildContext context, String id,
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id, 'id': id,
'isFileTransfer': isFileTransfer, 'isFileTransfer': isFileTransfer,
'isViewCamera': isViewCamera,
'isTcpTunneling': isTcpTunneling, 'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP, 'isRDP': isRDP,
'password': password, 'password': password,
@@ -2400,6 +2438,31 @@ connect(BuildContext context, String id,
), ),
); );
} }
} else if (isViewCamera) {
if (isWeb) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
desktop_view_camera.ViewCameraPage(
key: ValueKey(id),
id: id,
toolbarState: ToolbarState(),
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ViewCameraPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} else { } else {
if (isWeb) { if (isWeb) {
Navigator.push( Navigator.push(
@@ -2686,6 +2749,8 @@ String getWindowName({WindowType? overrideType}) {
return name; return name;
case WindowType.FileTransfer: case WindowType.FileTransfer:
return "File Transfer - $name"; return "File Transfer - $name";
case WindowType.ViewCamera:
return "View Camera - $name";
case WindowType.PortForward: case WindowType.PortForward:
return "Port Forward - $name"; return "Port Forward - $name";
case WindowType.RemoteDesktop: case WindowType.RemoteDesktop:
@@ -3051,6 +3116,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
'peer_id': peerId, 'peer_id': peerId,
'display': i, 'display': i,
'display_count': pi.displays.length, 'display_count': pi.displays.length,
'window_type': (kWindowType ?? WindowType.RemoteDesktop).index,
}; };
if (screenRect != null) { if (screenRect != null) {
args['screen_rect'] = { args['screen_rect'] = {
@@ -3065,12 +3131,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
} }
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount, setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
int? display, Rect? screenRect) async { WindowType windowType, int? display, Rect? screenRect) async {
if (screenRect == null) { if (screenRect == null) {
// Do not restore window position to new connection if there's a pre-session. // Do not restore window position to new connection if there's a pre-session.
// https://github.com/rustdesk/rustdesk/discussions/8825 // https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) { if (preSessionCount == 0) {
await restoreWindowPosition(WindowType.RemoteDesktop, await restoreWindowPosition(windowType,
windowId: windowId, display: display, peerId: peerId); windowId: windowId, display: display, peerId: peerId);
} }
} else { } else {
+18
View File
@@ -488,6 +488,7 @@ abstract class BasePeerCard extends StatelessWidget {
BuildContext context, BuildContext context,
String title, { String title, {
bool isFileTransfer = false, bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false, bool isRDP = false,
}) { }) {
@@ -502,6 +503,7 @@ abstract class BasePeerCard extends StatelessWidget {
peer, peer,
tab, tab,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP, isRDP: isRDP,
); );
@@ -530,6 +532,15 @@ abstract class BasePeerCard extends StatelessWidget {
); );
} }
@protected
MenuEntryBase<String> _viewCameraAction(BuildContext context) {
return _connectCommonAction(
context,
translate('View camera'),
isViewCamera: true,
);
}
@protected @protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) { MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction( return _connectCommonAction(
@@ -880,6 +891,7 @@ class RecentPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -939,6 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -992,6 +1005,7 @@ class DiscoveredPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -1045,6 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1177,6 +1192,7 @@ class MyGroupPeerCard extends BasePeerCard {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1398,6 +1414,7 @@ class TagPainter extends CustomPainter {
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false}) async { bool isRDP = false}) async {
var password = ''; var password = '';
@@ -1423,6 +1440,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
password: password, password: password,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP); isRDP: isRDP);
} }
+48 -1
View File
@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget {
class RawTouchGestureDetectorRegion extends StatefulWidget { class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child; final Widget child;
final FFI ffi; final FFI ffi;
final bool isCamera;
late final InputModel inputModel = ffi.inputModel; late final InputModel inputModel = ffi.inputModel;
late final FfiModel ffiModel = ffi.ffiModel; late final FfiModel ffiModel = ffi.ffiModel;
RawTouchGestureDetectorRegion({ RawTouchGestureDetectorRegion({
required this.child, required this.child,
required this.ffi, required this.ffi,
this.isCamera = false,
}); });
@override @override
@@ -382,6 +384,7 @@ class _RawTouchGestureDetectorRegionState
_scale = d.scale; _scale = d.scale;
if (scale != 0) { if (scale != 0) {
if (widget.isCamera) return;
await bind.sessionSendPointer( await bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
@@ -402,6 +405,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if ((isDesktop || isWebDesktop)) { if ((isDesktop || isWebDesktop)) {
if (widget.isCamera) return;
await bind.sessionSendPointer( await bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
@@ -536,3 +540,46 @@ class RawPointerMouseRegion extends StatelessWidget {
); );
} }
} }
class CameraRawPointerMouseRegion extends StatelessWidget {
final InputModel inputModel;
final Widget child;
final PointerEnterEventListener? onEnter;
final PointerExitEventListener? onExit;
final PointerDownEventListener? onPointerDown;
final PointerUpEventListener? onPointerUp;
CameraRawPointerMouseRegion({
this.onEnter,
this.onExit,
this.onPointerDown,
this.onPointerUp,
required this.inputModel,
required this.child,
});
@override
Widget build(BuildContext context) {
return Listener(
onPointerHover: (evt) {
final offset = evt.position;
double x = offset.dx;
double y = max(0.0, offset.dy);
inputModel.handlePointerDevicePos(
kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault);
},
onPointerDown: (evt) {
onPointerDown?.call(evt);
},
onPointerUp: (evt) {
onPointerUp?.call(evt);
},
child: MouseRegion(
cursor: MouseCursor.defer,
onEnter: onEnter,
onExit: onExit,
child: child,
),
);
}
}
+48 -27
View File
@@ -89,10 +89,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final pi = ffiModel.pi; final pi = ffiModel.pi;
final perms = ffiModel.permissions; final perms = ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
List<TTextMenu> v = []; List<TTextMenu> v = [];
// elevation // elevation
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) { if (isDefaultConn &&
perms['keyboard'] != false &&
ffi.elevationModel.showRequestMenu) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Request Elevation')), child: Text(translate('Request Elevation')),
@@ -101,7 +104,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// osAccount / osPassword // osAccount / osPassword
if (perms['keyboard'] != false) { if (isDefaultConn && perms['keyboard'] != false) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Row(children: [ child: Row(children: [
@@ -130,7 +133,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// paste // paste
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) { if (isDefaultConn &&
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')), child: Text(translate('Send clipboard keystrokes')),
onPressed: () async { onPressed: () async {
@@ -142,43 +147,53 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
})); }));
} }
// reset canvas // reset canvas
if (isMobile) { if (isDefaultConn && isMobile) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Reset canvas')), child: Text(translate('Reset canvas')),
onPressed: () => ffi.cursorModel.reset())); onPressed: () => ffi.cursorModel.reset()));
} }
connectWithToken( connectWithToken(
{required bool isFileTransfer, required bool isTcpTunneling}) { {bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false}) {
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
connect(context, id, connect(context, id,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
connToken: connToken); connToken: connToken);
} }
// transferFile // transferFile
if (isDesktop) { if (isDefaultConn && isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Transfer file')), child: Text(translate('Transfer file')),
onPressed: () => onPressed: () => connectWithToken(isFileTransfer: true)),
connectWithToken(isFileTransfer: true, isTcpTunneling: false)), );
}
// viewCamera
if (isDefaultConn && isDesktop) {
v.add(
TTextMenu(
child: Text(translate('View camera')),
onPressed: () => connectWithToken(isViewCamera: true)),
); );
} }
// tcpTunneling // tcpTunneling
if (isDesktop) { if (isDefaultConn && isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('TCP tunneling')), child: Text(translate('TCP tunneling')),
onPressed: () => onPressed: () => connectWithToken(isTcpTunneling: true)),
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
); );
} }
// note // note
if (bind if (isDefaultConn &&
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") bind
.isNotEmpty) { .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
.isNotEmpty) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Note')), child: Text(translate('Note')),
@@ -186,11 +201,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// divider // divider
if (isDesktop || isWebDesktop) { if (isDefaultConn && (isDesktop || isWebDesktop)) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
} }
// ctrlAltDel // ctrlAltDel
if (!ffiModel.viewOnly && if (isDefaultConn &&
!ffiModel.viewOnly &&
ffiModel.keyboard && ffiModel.keyboard &&
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
v.add( v.add(
@@ -200,7 +216,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// restart // restart
if (perms['restart'] != false && if (isDefaultConn &&
perms['restart'] != false &&
(pi.platform == kPeerPlatformLinux || (pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows || pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) { pi.platform == kPeerPlatformMacOS)) {
@@ -212,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// insertLock // insertLock
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) { if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Insert Lock')), child: Text(translate('Insert Lock')),
@@ -220,7 +237,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// blockUserInput // blockUserInput
if (ffi.ffiModel.keyboard && if (isDefaultConn &&
ffi.ffiModel.keyboard &&
ffi.ffiModel.permissions['block_input'] != false && ffi.ffiModel.permissions['block_input'] != false &&
pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
{ {
@@ -236,12 +254,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
})); }));
} }
// switchSides // switchSides
if (isDesktop && if (isDefaultConn &&
isDesktop &&
ffiModel.keyboard && ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS && pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 && versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetDefaultSessionsCount(id: id) == 1) { bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Switch Sides')), child: Text(translate('Switch Sides')),
onPressed: () => onPressed: () =>
@@ -523,6 +542,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final pi = ffiModel.pi; final pi = ffiModel.pi;
final perms = ffiModel.permissions; final perms = ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
// show quality monitor // show quality monitor
final option = 'show-quality-monitor'; final option = 'show-quality-monitor';
@@ -535,7 +555,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
child: Text(translate('Show quality monitor')))); child: Text(translate('Show quality monitor'))));
// mute // mute
if (perms['audio'] != false) { if (isDefaultConn && perms['audio'] != false) {
final option = 'disable-audio'; final option = 'disable-audio';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
@@ -556,7 +576,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() && bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
if (ffiModel.keyboard && if (isDefaultConn &&
ffiModel.keyboard &&
perms['file'] != false && perms['file'] != false &&
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
@@ -574,7 +595,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Enable file copy and paste')))); child: Text(translate('Enable file copy and paste'))));
} }
// disable clipboard // disable clipboard
if (ffiModel.keyboard && perms['clipboard'] != false) { if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'disable-clipboard'; final option = 'disable-clipboard';
var value = var value =
@@ -591,7 +612,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Disable clipboard')))); child: Text(translate('Disable clipboard'))));
} }
// lock after session end // lock after session end
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) { if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end'; final option = 'lock-after-session-end';
final value = final value =
@@ -656,12 +677,12 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('True color (4:4:4)')))); child: Text(translate('True color (4:4:4)'))));
} }
if (isMobile) { if (isDefaultConn && isMobile) {
v.addAll(toolbarKeyboardToggles(ffi)); v.addAll(toolbarKeyboardToggles(ffi));
} }
// view mode (mobile only, desktop is in keyboard menu) // view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) { if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu( v.add(TToggleMenu(
value: ffiModel.viewOnly, value: ffiModel.viewOnly,
onChanged: (value) async { onChanged: (value) async {
+4
View File
@@ -27,6 +27,7 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl"; "supported_privacy_mode_impl";
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
@@ -44,6 +45,7 @@ const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopViewCamera = "view camera";
const String kAppTypeDesktopPortForward = "port forward"; const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowMainWindowOnTop = "main_window_on_top";
@@ -58,6 +60,7 @@ const String kWindowConnect = "connect";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewViewCamera = "new_view_camera";
const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventActiveDisplaySession = "active_display_session";
@@ -97,6 +100,7 @@ const String kOptionEnableKeyboard = "enable-keyboard";
const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio"; const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableCamera = "enable-camera";
const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input"; const String kOptionEnableBlockInput = "enable-block-input";
+61 -16
View File
@@ -17,7 +17,6 @@ import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart'; import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/button.dart';
class OnlineStatusWidget extends StatefulWidget { class OnlineStatusWidget extends StatefulWidget {
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
@@ -203,6 +202,8 @@ class _ConnectionPageState extends State<ConnectionPage>
final FocusNode _idFocusNode = FocusNode(); final FocusNode _idFocusNode = FocusNode();
final TextEditingController _idEditingController = TextEditingController(); final TextEditingController _idEditingController = TextEditingController();
String selectedConnectionType = 'Connect';
bool isWindowMinimized = false; bool isWindowMinimized = false;
final AllPeersLoader _allPeersLoader = AllPeersLoader(); final AllPeersLoader _allPeersLoader = AllPeersLoader();
@@ -321,9 +322,10 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Callback for the connect button. /// Callback for the connect button.
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) { void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) {
var id = _idController.id; var id = _idController.id;
connect(context, id, isFileTransfer: isFileTransfer); connect(context, id,
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera);
} }
/// UI for the remote ID TextField. /// UI for the remote ID TextField.
@@ -501,21 +503,64 @@ class _ConnectionPageState extends State<ConnectionPage>
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 13.0), padding: const EdgeInsets.only(top: 13.0),
child: Row( child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
mainAxisAlignment: MainAxisAlignment.end, SizedBox(
children: [ height: 28.0,
Button( child: ElevatedButton(
isOutline: true, onPressed: () {
onTap: () => onConnect(isFileTransfer: true), onConnect();
text: "Transfer file", },
child: Text(translate("Connect")),
), ),
const SizedBox( ),
width: 17, const SizedBox(width: 3),
Container(
height: 28.0,
width: 28.0,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
), ),
Button(onTap: onConnect, text: "Connect"), child: Center(
], child: MenuAnchor(
), builder: (context, controller, builder) {
) return IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
visualDensity: VisualDensity.compact,
icon: controller.isOpen
? const Icon(Icons.keyboard_arrow_up)
: const Icon(Icons.keyboard_arrow_down),
onPressed: () {
setState(() {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
});
},
);
},
menuChildren: <Widget>[
MenuItemButton(
onPressed: () {
onConnect(isFileTransfer: true);
},
child: Text(translate('Transfer file')),
),
MenuItemButton(
onPressed: () {
onConnect(isViewCamera: true);
},
child: Text(translate('View camera')),
),
],
),
),
),
]),
),
], ],
), ),
), ),
@@ -775,6 +775,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
await connectMainDesktop( await connectMainDesktop(
call.arguments['id'], call.arguments['id'],
isFileTransfer: call.arguments['isFileTransfer'], isFileTransfer: call.arguments['isFileTransfer'],
isViewCamera: call.arguments['isViewCamera'],
isTcpTunneling: call.arguments['isTcpTunneling'], isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'], isRDP: call.arguments['isRDP'],
password: call.arguments['password'], password: call.arguments['password'],
@@ -789,9 +790,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} catch (e) { } catch (e) {
debugPrint("Failed to parse window id '${call.arguments}': $e"); debugPrint("Failed to parse window id '${call.arguments}': $e");
} }
if (windowId != null) { WindowType? windowType;
try {
windowType = WindowType.values.byName(args[3]);
} catch (e) {
debugPrint("Failed to parse window type '${call.arguments}': $e");
}
if (windowId != null && windowType != null) {
await rustDeskWinManager.moveTabToNewWindow( await rustDeskWinManager.moveTabToNewWindow(
windowId, args[1], args[2]); windowId, args[1], args[2], windowType);
} }
} else if (call.method == kWindowEventOpenMonitorSession) { } else if (call.method == kWindowEventOpenMonitorSession) {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
@@ -799,9 +806,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final peerId = args['peer_id'] as String; final peerId = args['peer_id'] as String;
final display = args['display'] as int; final display = args['display'] as int;
final displayCount = args['display_count'] as int; final displayCount = args['display_count'] as int;
final windowType = args['window_type'] as int;
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession( await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect); windowId, peerId, display, displayCount, screenRect, windowType);
} else if (call.method == kWindowEventRemoteWindowCoords) { } else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments); final windowId = int.tryParse(call.arguments);
if (windowId != null) { if (windowId != null) {
@@ -960,6 +960,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio, _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _OptionCheckBox(
context, 'Enable TCP tunneling', kOptionEnableTunnel, context, 'Enable TCP tunneling', kOptionEnableTunnel,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
@@ -269,8 +269,10 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
style: style, style: style,
), ),
proc: () async { proc: () async {
await DesktopMultiWindow.invokeMethod(kMainWindowId, await DesktopMultiWindow.invokeMethod(
kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId'); kMainWindowId,
kWindowEventMoveTabToNewWindow,
'${windowId()},$key,$sessionId,RemoteDesktop');
cancelFunc(); cancelFunc();
}, },
padding: padding, padding: padding,
@@ -417,8 +419,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
await WindowController.fromWindowId(windowId()).setFullscreen(false); await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false); stateGlobal.setFullscreen(false, procWnd: false);
} }
await setNewConnectWindowFrame( await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
windowId(), id!, prePeerCount, display, screenRect); WindowType.RemoteDesktop, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId()); await windowOnTop(windowId());
}); });
+138 -92
View File
@@ -353,7 +353,9 @@ Widget buildConnectionCard(Client client) {
key: ValueKey(client.id), key: ValueKey(client.id),
children: [ children: [
_CmHeader(client: client), _CmHeader(client: client),
client.type_() != ClientType.remote || client.disconnected client.type_() == ClientType.file ||
client.type_() == ClientType.portForward ||
client.disconnected
? Offstage() ? Offstage()
: _PrivilegeBoard(client: client), : _PrivilegeBoard(client: client),
Expanded( Expanded(
@@ -526,7 +528,8 @@ class _CmHeaderState extends State<_CmHeader>
Offstage( Offstage(
offstage: !client.authorized || offstage: !client.authorized ||
(client.type_() != ClientType.remote && (client.type_() != ClientType.remote &&
client.type_() != ClientType.file), client.type_() != ClientType.file &&
client.type_() != ClientType.camera),
child: IconButton( child: IconButton(
onPressed: () => checkClickTime(client.id, () { onPressed: () => checkClickTime(client.id, () {
if (client.type_() == ClientType.file) { if (client.type_() == ClientType.file) {
@@ -627,96 +630,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
padding: EdgeInsets.symmetric(horizontal: spacing), padding: EdgeInsets.symmetric(horizontal: spacing),
mainAxisSpacing: spacing, mainAxisSpacing: spacing,
crossAxisSpacing: spacing, crossAxisSpacing: spacing,
children: [ children: client.type_() == ClientType.camera
buildPermissionIcon( ? [
client.keyboard, buildPermissionIcon(
Icons.keyboard, client.audio,
(enabled) { Icons.volume_up_rounded,
bind.cmSwitchPermission( (enabled) {
connId: client.id, name: "keyboard", enabled: enabled); bind.cmSwitchPermission(
setState(() { connId: client.id,
client.keyboard = enabled; name: "audio",
}); enabled: enabled);
}, setState(() {
translate('Enable keyboard/mouse'), client.audio = enabled;
), });
buildPermissionIcon( },
client.clipboard, translate('Enable audio'),
Icons.assignment_rounded, ),
(enabled) { buildPermissionIcon(
bind.cmSwitchPermission( client.recording,
connId: client.id, name: "clipboard", enabled: enabled); Icons.videocam_rounded,
setState(() { (enabled) {
client.clipboard = enabled; bind.cmSwitchPermission(
}); connId: client.id,
}, name: "recording",
translate('Enable clipboard'), enabled: enabled);
), setState(() {
buildPermissionIcon( client.recording = enabled;
client.audio, });
Icons.volume_up_rounded, },
(enabled) { translate('Enable recording session'),
bind.cmSwitchPermission( ),
connId: client.id, name: "audio", enabled: enabled); ]
setState(() { : [
client.audio = enabled; buildPermissionIcon(
}); client.keyboard,
}, Icons.keyboard,
translate('Enable audio'), (enabled) {
), bind.cmSwitchPermission(
buildPermissionIcon( connId: client.id,
client.file, name: "keyboard",
Icons.upload_file_rounded, enabled: enabled);
(enabled) { setState(() {
bind.cmSwitchPermission( client.keyboard = enabled;
connId: client.id, name: "file", enabled: enabled); });
setState(() { },
client.file = enabled; translate('Enable keyboard/mouse'),
}); ),
}, buildPermissionIcon(
translate('Enable file copy and paste'), client.clipboard,
), Icons.assignment_rounded,
buildPermissionIcon( (enabled) {
client.restart, bind.cmSwitchPermission(
Icons.restart_alt_rounded, connId: client.id,
(enabled) { name: "clipboard",
bind.cmSwitchPermission( enabled: enabled);
connId: client.id, name: "restart", enabled: enabled); setState(() {
setState(() { client.clipboard = enabled;
client.restart = enabled; });
}); },
}, translate('Enable clipboard'),
translate('Enable remote restart'), ),
), buildPermissionIcon(
buildPermissionIcon( client.audio,
client.recording, Icons.volume_up_rounded,
Icons.videocam_rounded, (enabled) {
(enabled) { bind.cmSwitchPermission(
bind.cmSwitchPermission( connId: client.id,
connId: client.id, name: "recording", enabled: enabled); name: "audio",
setState(() { enabled: enabled);
client.recording = enabled; setState(() {
}); client.audio = enabled;
}, });
translate('Enable recording session'), },
), translate('Enable audio'),
// only windows support block input ),
if (isWindows) buildPermissionIcon(
buildPermissionIcon( client.file,
client.blockInput, Icons.upload_file_rounded,
Icons.block, (enabled) {
(enabled) { bind.cmSwitchPermission(
bind.cmSwitchPermission( connId: client.id,
connId: client.id, name: "file",
name: "block_input", enabled: enabled);
enabled: enabled); setState(() {
setState(() { client.file = enabled;
client.blockInput = enabled; });
}); },
}, translate('Enable file copy and paste'),
translate('Enable blocking user input'), ),
) buildPermissionIcon(
], client.restart,
Icons.restart_alt_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "restart",
enabled: enabled);
setState(() {
client.restart = enabled;
});
},
translate('Enable remote restart'),
),
buildPermissionIcon(
client.recording,
Icons.videocam_rounded,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "recording",
enabled: enabled);
setState(() {
client.recording = enabled;
});
},
translate('Enable recording session'),
),
// only windows support block input
if (isWindows)
buildPermissionIcon(
client.blockInput,
Icons.block,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "block_input",
enabled: enabled);
setState(() {
client.blockInput = enabled;
});
},
translate('Enable blocking user input'),
)
],
), ),
), ),
], ],
@@ -0,0 +1,730 @@
import 'dart:async';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart';
import '../../common/widgets/overlay.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/toolbar.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.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 ViewCameraPage extends StatefulWidget {
ViewCameraPage({
Key? key,
required this.id,
required this.toolbarState,
this.sessionId,
this.tabWindowId,
this.password,
this.display,
this.displays,
this.tabController,
this.connToken,
this.forceRelay,
this.isSharedPassword,
}) : super(key: key) {
initSharedStates(id);
}
final String id;
final SessionID? sessionId;
final int? tabWindowId;
final int? display;
final List<int>? displays;
final String? password;
final ToolbarState toolbarState;
final bool? forceRelay;
final bool? isSharedPassword;
final String? connToken;
final SimpleWrapper<State<ViewCameraPage>?> _lastState = SimpleWrapper(null);
final DesktopTabController? tabController;
FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
@override
State<ViewCameraPage> createState() {
final state = _ViewCameraPageState(id);
_lastState.value = state;
return state;
}
}
class _ViewCameraPageState extends State<ViewCameraPage>
with AutomaticKeepAliveClientMixin, MultiWindowListener {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
final _cursorOverImage = false.obs;
var _blockableOverlayState = BlockableOverlayState();
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
// to identify the toolbar instance and its callback function.
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
SessionID get sessionId => _ffi.sessionId;
_ViewCameraPageState(String id) {
_initStates(id);
}
void _initStates(String id) {}
@override
void initState() {
super.initState();
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.start(
widget.id,
isViewCamera: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
display: widget.display,
displays: widget.displays,
connToken: widget.connToken,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!isLinux) {
WakelockPlus.enable();
}
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
// (String? lastKey, String? currentKey) async {
// if (_firstEnterImage.value) {
// _firstEnterImage.value = false;
// return true;
// }
// return lastKey == null || lastKey != currentKey;
// });
// _isCustomCursorInited = true;
// }
_blockableOverlayState.applyFfi(_ffi);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
}
@override
void onWindowBlur() {
super.onWindowBlur();
// On windows, we use `focus` way to handle keyboard better.
// Now on Linux, there's some rdev issues which will break the input.
// We disable the `focus` way for non-Windows temporarily.
if (isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
stateGlobal.isFocused.value = false;
}
@override
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (isWindows) {
_isWindowBlur = false;
}
stateGlobal.isFocused.value = true;
}
@override
void onWindowRestore() {
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (isWindows) {
_isWindowBlur = false;
}
if (!isLinux) {
WakelockPlus.enable();
}
}
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
@override
void onWindowMaximize() {
super.onWindowMaximize();
if (!isLinux) {
WakelockPlus.enable();
}
}
@override
void onWindowMinimize() {
super.onWindowMinimize();
if (!isLinux) {
WakelockPlus.disable();
}
}
@override
void onWindowEnterFullScreen() {
super.onWindowEnterFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(true);
}
}
@override
void onWindowLeaveFullScreen() {
super.onWindowLeaveFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(false);
}
}
@override
Future<void> dispose() async {
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession);
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();
if (closeSession) {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
}
if (!isLinux) {
await WakelockPlus.disable();
}
await Get.delete<FFI>(tag: widget.id);
removeSharedStates(widget.id);
}
Widget emptyOverlay() => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: Colors.transparent,
),
);
Widget buildBody(BuildContext context) {
remoteToolbar(BuildContext context) => RemoteToolbar(
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (id, func) {
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
_onEnterOrLeaveImage4Toolbar = func;
},
onEnterOrLeaveImageCleaner: (id) {
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
_onEnterOrLeaveImage4Toolbar = null;
}
},
setRemoteState: setState,
);
bodyWidget() {
return Stack(
children: [
Container(
color: kColorCanvas,
child: getBodyForDesktop(context),
),
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage();
} else {
return Obx(() => Offstage(
offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse,
child: Overlay(initialEntries: [
makeMobileActionsOverlayEntry(
() => _ffi.dialogManager
.setMobileActionsOverlayVisible(false),
ffi: _ffi,
)
]),
));
}
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(
initialEntries: [OverlayEntry(builder: remoteToolbar)])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
),
],
);
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Obx(() {
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isFalse;
if (imageReady) {
// If the privacy mode(disable physical displays) is switched,
// we should not dismiss the dialog immediately.
if (DateTime.now().difference(togglePrivacyModeTime) >
const Duration(milliseconds: 3000)) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
}
// Block the whole `bodyWidget()` when dialog shows.
return BlockableOverlay(
underlying: bodyWidget(),
state: _blockableOverlayState,
);
} else {
// `_blockableOverlayState` is not recreated here.
// The toolbar's block state won't work properly when reconnecting, but that's okay.
return bodyWidget();
}
}),
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager);
return false;
},
child: MultiProvider(providers: [
ChangeNotifierProvider.value(value: _ffi.ffiModel),
ChangeNotifierProvider.value(value: _ffi.imageModel),
ChangeNotifierProvider.value(value: _ffi.cursorModel),
ChangeNotifierProvider.value(value: _ffi.canvasModel),
ChangeNotifierProvider.value(value: _ffi.recordingModel),
], child: buildBody(context)));
}
void enterView(PointerEnterEvent evt) {
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(true);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
_ffi.inputModel.enterOrLeave(true);
}
}
void leaveView(PointerExitEvent evt) {
if (_ffi.ffiModel.keyboard) {
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
}
_cursorOverImage.value = false;
_firstEnterImage.value = false;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(false);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
_ffi.inputModel.enterOrLeave(false);
}
}
Widget _buildRawTouchAndPointerRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return RawTouchGestureDetectorRegion(
child: _buildRawPointerMouseRegion(child, onEnter, onExit),
ffi: _ffi,
isCamera: true,
);
}
Widget _buildRawPointerMouseRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return CameraRawPointerMouseRegion(
onEnter: onEnter,
onExit: onExit,
onPointerDown: (event) {
// A double check for blur status.
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
// Sometimes the system does not send the necessary focus event to flutter. We should manually
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
// ensure the grab-key thread is running when our users are clicking the remote canvas.
if (_isWindowBlur) {
debugPrint(
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
_isWindowBlur = false;
}
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
},
inputModel: _ffi.inputModel,
child: child,
);
}
Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[
MouseRegion(onEnter: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) {
final c = Provider.of<CanvasModel>(context, listen: false);
Future.delayed(Duration.zero, () => c.updateViewStyle());
final peerDisplay = CurrentDisplayState.find(widget.id);
return Obx(
() => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent)
: Obx(() {
widget.toolbarState.initShow(sessionId);
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
return ImagePaint(
id: widget.id,
cursorOverImage: _cursorOverImage,
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
child, enterView, leaveView),
ffi: _ffi,
);
}),
);
}))
];
paints.add(
Positioned(
top: 10,
right: 10,
child: _buildRawTouchAndPointerRegion(
QualityMonitor(_ffi.qualityMonitorModel), null, null),
),
);
return Stack(
children: paints,
);
}
@override
bool get wantKeepAlive => true;
}
class ImagePaint extends StatefulWidget {
final FFI ffi;
final String id;
final RxBool cursorOverImage;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
{Key? key,
required this.ffi,
required this.id,
required this.cursorOverImage,
this.listenerBuilder})
: super(key: key);
@override
State<StatefulWidget> createState() => _ImagePaintState();
}
class _ImagePaintState extends State<ImagePaint> {
bool _lastRemoteCursorMoved = false;
String get id => widget.id;
RxBool get cursorOverImage => widget.cursorOverImage;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
c.updateScrollPercent();
return false;
},
child: Container(
child: _buildCrossScrollbarFromLayout(
context,
_buildListener(paintWidget),
c.size,
paintSize,
c.scrollHorizontal,
c.scrollVertical,
)),
);
} else {
if (c.size.width > 0 && c.size.height > 0) {
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
: _buildScrollAutoNonTextureRender(m, c, s);
return Container(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 _buildScrollAutoNonTextureRender(
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.ffi.textureModel
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
if (true) {
// both "textureId.value != -1" and "true" seems ok
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 _buildCustomCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = cursor.cache ?? preDefaultCursor.cache;
return buildCursorOfCache(cursor, scale, cache);
}
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = preForbiddenCursor.cache;
return buildCursorOfCache(cursor, scale, cache);
}
Widget _buildCrossScrollbarFromLayout(
BuildContext context,
Widget child,
Size layoutSize,
Size size,
ScrollController horizontal,
ScrollController vertical,
) {
var widget = child;
if (layoutSize.width < size.width) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: horizontal,
scrollDirection: Axis.horizontal,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Row(
children: [
Container(
width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.height < size.height) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: vertical,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Column(
children: [
Container(
height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.width < size.width) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: horizontal,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: layoutSize.height < size.height
? (notification) => notification.depth == 1
: defaultScrollNotificationPredicate,
child: widget,
);
}
if (layoutSize.height < size.height) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: vertical,
thumbVisibility: false,
trackVisibility: false,
child: widget,
);
}
return Container(
child: widget,
width: layoutSize.width,
height: layoutSize.height,
);
}
Widget _buildListener(Widget child) {
if (listenerBuilder != null) {
return listenerBuilder!(child);
} else {
return child;
}
}
}
@@ -0,0 +1,499 @@
import 'dart:convert';
import 'dart:async';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
static const Color blueColor = MyTheme.button;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
}
class ViewCameraTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const ViewCameraTabPage({Key? key, required this.params}) : super(key: key);
@override
State<ViewCameraTabPage> createState() => _ViewCameraTabPageState(params);
}
class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
final tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera));
final contentKey = UniqueKey();
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
String? peerId;
bool _isScreenRectSet = false;
int? _display;
var connectionMap = RxList<Widget>.empty(growable: true);
_ViewCameraTabPageState(Map<String, dynamic> params) {
RemoteCountState.init();
peerId = params['id'];
final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
final screenRect = parseParamScreenRect(params);
_isScreenRectSet = screenRect != null;
_display = display as int?;
tryMoveToScreenAndSetFullscreen(screenRect);
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
final viewCameraPage = tabController.widget(id);
if (viewCameraPage is ViewCameraPage) {
final ffi = viewCameraPage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId);
}
WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0;
};
tabController.add(TabInfo(
key: peerId!,
label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
page: ViewCameraPage(
key: ValueKey(peerId),
id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: params['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: params['connToken'],
forceRelay: params['forceRelay'],
isSharedPassword: params['isSharedPassword'],
),
));
_update_remote_count();
}
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
}
@override
void initState() {
super.initState();
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.ViewCamera,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
display: _display,
);
});
}
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
bool secure =
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
fingerprint = 'N/A';
}
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final tab = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4),
],
);
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
},
target: e.position,
);
}
},
child: tab,
);
}
}),
),
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: workaroundWindowBorder(
context,
Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
key: contentKey,
child: tabWidget,
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
));
}
// Note: Some dup code to ../widgets/remote_toolbar
Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> menu = [];
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final sessionId = ffi.sessionId;
final toolbarState = viewCameraPage.toolbarState;
menu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
style: style,
)),
proc: () {
toolbarState.switchShow(sessionId);
cancelFunc();
},
padding: padding,
),
]);
if (tabController.state.value.tabs.length > 1) {
final splitAction = MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Move tab to new window'),
style: style,
),
proc: () async {
await DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventMoveTabToNewWindow,
'${windowId()},$key,$sessionId,ViewCamera');
cancelFunc();
},
padding: padding,
);
menu.insert(1, splitAction);
}
menu.addAll([
MenuEntryDivider<String>(),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Copy Fingerprint'),
style: style,
),
proc: () => onCopyFingerprint(FingerprintState.find(key).value),
padding: padding,
dismissOnClicked: true,
dismissCallback: cancelFunc,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Close'),
style: style,
),
proc: () {
tabController.closeBy(key);
cancelFunc();
},
padding: padding,
)
]);
return mod_menu.PopupMenu<String>(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);
_update_remote_count();
}
int windowId() {
return widget.params["windowId"];
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength <= 1) {
tabController.clear();
return true;
} else {
final bool res;
if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
}
return res;
}
}
_update_remote_count() =>
RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
debugPrint(
"[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewViewCamera) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
WindowType.ViewCamera, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
});
ConnectionTypeState.init(id);
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: ViewCameraPage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: args['connToken'],
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
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, isCamera: true);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as ViewCameraPage;
returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
} else if (call.method == kWindowEventRemoteWindowCoords) {
final viewCameraPage =
tabController.state.value.selectedTabInfo.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final displayRect = ffi.ffiModel.displaysRect();
if (displayRect != null) {
final wc = WindowController.fromWindowId(windowId());
Rect? frame;
try {
frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
}
if (frame != null) {
ffi.cursorModel.moveLocal(0, 0);
final coords = RemoteWindowCoords(
frame,
CanvasCoords.fromCanvasModel(ffi.canvasModel),
CursorCoords.fromCursorModel(ffi.cursorModel),
displayRect);
returnValue = jsonEncode(coords.toJson());
}
}
} else if (call.method == kWindowEventSetFullscreen) {
stateGlobal.setFullscreen(call.arguments == 'true');
}
_update_remote_count();
return returnValue;
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
class DesktopViewCameraScreen extends StatelessWidget {
final Map<String, dynamic> params;
DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) {
bind.mainInitInputSource();
stateGlobal.getInputSource(force: true);
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
child: Scaffold(
// Set transparent background for padding the resize area out of the flutter view.
// This allows the wallpaper goes through our resize area. (Linux only now).
backgroundColor: isLinux ? Colors.transparent : null,
body: ViewCameraTabPage(
params: params,
),
));
}
}
+18 -10
View File
@@ -478,7 +478,10 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
state: widget.state, state: widget.state,
setFullscreen: _setFullscreen, setFullscreen: _setFullscreen,
)); ));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); // Do not show keyboard for camera connection type.
if (widget.ffi.connType == ConnType.defaultConn) {
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
}
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) { if (!isWeb) {
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
@@ -1043,23 +1046,26 @@ class _DisplayMenuState extends State<_DisplayMenu> {
scrollStyle(), scrollStyle(),
imageQuality(), imageQuality(),
codec(), codec(),
_ResolutionsMenu( if (ffi.connType == ConnType.defaultConn)
id: widget.id, _ResolutionsMenu(
ffi: widget.ffi, id: widget.id,
screenAdjustor: _screenAdjustor, ffi: widget.ffi,
), screenAdjustor: _screenAdjustor,
if (showVirtualDisplayMenu(ffi)) ),
if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn)
_SubmenuButton( _SubmenuButton(
ffi: widget.ffi, ffi: widget.ffi,
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null), menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
child: Text(translate("Virtual display")), child: Text(translate("Virtual display")),
), ),
cursorToggles(), if (ffi.connType == ConnType.defaultConn) cursorToggles(),
Divider(), Divider(),
toggles(), toggles(),
]; ];
// privacy mode // privacy mode
if (ffiModel.keyboard && pi.features.privacyMode) { if (ffi.connType == ConnType.defaultConn &&
ffiModel.keyboard &&
pi.features.privacyMode) {
final privacyModeState = PrivacyModeState.find(id); final privacyModeState = PrivacyModeState.find(id);
final privacyModeList = final privacyModeList =
toolbarPrivacyMode(privacyModeState, context, id, ffi); toolbarPrivacyMode(privacyModeState, context, id, ffi);
@@ -1085,7 +1091,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
]); ]);
} }
} }
menuChildren.add(widget.pluginItem); if (ffi.connType == ConnType.defaultConn) {
menuChildren.add(widget.pluginItem);
}
return menuChildren; return menuChildren;
} }
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -51,6 +52,7 @@ enum DesktopTabType {
cm, cm,
remoteScreen, remoteScreen,
fileTransfer, fileTransfer,
viewCamera,
portForward, portForward,
install, install,
} }
@@ -179,11 +181,13 @@ class DesktopTabController {
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
callOnSelected: callOnSelected); callOnSelected: callOnSelected);
bool jumpToByKeyAndDisplay(String key, int display) { bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) {
for (int i = 0; i < state.value.tabs.length; i++) { for (int i = 0; i < state.value.tabs.length; i++) {
final tab = state.value.tabs[i]; final tab = state.value.tabs[i];
if (tab.key == key) { if (tab.key == key) {
final ffi = (tab.page as RemotePage).ffi; final ffi = isCamera
? (tab.page as ViewCameraPage).ffi
: (tab.page as RemotePage).ffi;
if (ffi.ffiModel.pi.currentDisplay == display) { if (ffi.ffiModel.pi.currentDisplay == display) {
return jumpTo(i, callOnSelected: true); return jumpTo(i, callOnSelected: true);
} }
@@ -725,6 +729,7 @@ class WindowActionPanelState extends State<WindowActionPanel> {
return widget.tabController.state.value.tabs.length > 1 && return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen || (widget.tabController.tabType == DesktopTabType.remoteScreen ||
widget.tabController.tabType == DesktopTabType.fileTransfer || widget.tabController.tabType == DesktopTabType.fileTransfer ||
widget.tabController.tabType == DesktopTabType.viewCamera ||
widget.tabController.tabType == DesktopTabType.portForward || widget.tabController.tabType == DesktopTabType.portForward ||
widget.tabController.tabType == DesktopTabType.cm); widget.tabController.tabType == DesktopTabType.cm);
} }
+27
View File
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
@@ -76,6 +77,13 @@ Future<void> main(List<String> args) async {
kAppTypeDesktopFileTransfer, kAppTypeDesktopFileTransfer,
); );
break; break;
case WindowType.ViewCamera:
desktopType = DesktopType.viewCamera;
runMultiWindow(
argument,
kAppTypeDesktopViewCamera,
);
break;
case WindowType.PortForward: case WindowType.PortForward:
desktopType = DesktopType.portForward; desktopType = DesktopType.portForward;
runMultiWindow( runMultiWindow(
@@ -192,6 +200,12 @@ void runMultiWindow(
params: argument, params: argument,
); );
break; break;
case kAppTypeDesktopViewCamera:
draggablePositions.load();
widget = DesktopViewCameraScreen(
params: argument,
);
break;
case kAppTypeDesktopPortForward: case kAppTypeDesktopPortForward:
widget = DesktopPortForwardScreen( widget = DesktopPortForwardScreen(
params: argument, params: argument,
@@ -227,6 +241,19 @@ void runMultiWindow(
await restoreWindowPosition(WindowType.FileTransfer, await restoreWindowPosition(WindowType.FileTransfer,
windowId: kWindowId!); windowId: kWindowId!);
break; break;
case kAppTypeDesktopViewCamera:
// If screen rect is set, the window will be moved to the target screen and then set fullscreen.
if (argument['screen_rect'] == null) {
// display can be used to control the offset of the window.
await restoreWindowPosition(
WindowType.ViewCamera,
windowId: kWindowId!,
peerId: argument['id'] as String?,
// FIXME: fix display index.
display: argument['display'] as int?,
);
}
break;
case kAppTypeDesktopPortForward: case kAppTypeDesktopPortForward:
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
break; break;
+7 -1
View File
@@ -204,6 +204,7 @@ class WebHomePage extends StatelessWidget {
return; return;
} }
bool isFileTransfer = false; bool isFileTransfer = false;
bool isViewCamera = false;
String? id; String? id;
String? password; String? password;
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
@@ -219,6 +220,11 @@ class WebHomePage extends StatelessWidget {
id = args[i + 1]; id = args[i + 1];
i++; i++;
break; break;
case '--view-camera':
isViewCamera = true;
id = args[i + 1];
i++;
break;
case '--password': case '--password':
password = args[i + 1]; password = args[i + 1];
i++; i++;
@@ -228,7 +234,7 @@ class WebHomePage extends StatelessWidget {
} }
} }
if (id != null) { if (id != null) {
connect(context, id, isFileTransfer: isFileTransfer, password: password); connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password);
} }
} }
} }
@@ -0,0 +1,721 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../../common.dart';
import '../../common/widgets/overlay.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/remote_input.dart';
import '../../models/input_model.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../utils/image.dart';
final initText = '1' * 1024;
// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
// https://github.com/flutter/flutter/issues/159384
// https://github.com/flutter/flutter/issues/159383
void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
if (isAndroid) {
if (isKeyboardVisible != true) {
// `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
gFFI.invokeMethod("enable_soft_keyboard", false);
}
}
}
class ViewCameraPage extends StatefulWidget {
ViewCameraPage(
{Key? key, required this.id, this.password, this.isSharedPassword})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
@override
State<ViewCameraPage> createState() => _ViewCameraPageState(id);
}
class _ViewCameraPageState extends State<ViewCameraPage>
with WidgetsBindingObserver {
Timer? _timer;
bool _showBar = !isWebDesktop;
bool _showGestureHelp = false;
Orientation? _currentOrientation;
double _viewInsetsBottom = 0;
Timer? _timerDidChangeMetrics;
final _blockableOverlayState = BlockableOverlayState();
final keyboardVisibilityController = KeyboardVisibilityController();
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
final TextEditingController _textController =
TextEditingController(text: initText);
_ViewCameraPageState(String id) {
initSharedStates(id);
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override
void initState() {
super.initState();
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
gFFI.start(
widget.id,
isViewCamera: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!isWeb) {
WakelockPlus.enable();
}
_physicalFocusNode.requestFocus();
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI);
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
gFFI.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
if (gFFI.recordingModel.start) {
showToast(translate('Automatically record outgoing sessions'));
}
_disableAndroidSoftKeyboard(
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
WidgetsBinding.instance.addObserver(this);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
gFFI.inputModel.listenToMouse(false);
gFFI.imageModel.disposeImage();
gFFI.cursorModel.disposeImages();
await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
await gFFI.close();
_timer?.cancel();
_timerDidChangeMetrics?.cancel();
gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
if (!isWeb) {
await WakelockPlus.disable();
}
removeSharedStates(widget.id);
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {}
@override
void didChangeMetrics() {
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
// Don't try reset the view style and focus the cursor.
if (gFFI.cursorModel.lastKeyboardIsVisible &&
gFFI.canvasModel.isMobileCanvasChanged) {
return;
}
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
_timerDidChangeMetrics?.cancel();
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
if (newBottom != _viewInsetsBottom) {
gFFI.canvasModel.mobileFocusCanvasCursor();
_viewInsetsBottom = newBottom;
}
});
}
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
// But I don't know why and how to fix it.
Widget emptyOverlay(Color bgColor) => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: bgColor,
),
);
Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar()
: Offstage());
@override
Widget build(BuildContext context) {
final keyboardIsVisible =
keyboardVisibilityController.isVisible && _showEdit;
final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, gFFI.dialogManager);
return false;
},
child: Scaffold(
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
floatingActionButtonLocation: keyboardIsVisible
? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
: null,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
mini: !keyboardIsVisible,
child: Icon(
(keyboardIsVisible || _showGestureHelp)
? Icons.expand_more
: Icons.expand_less,
color: Colors.white,
),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (keyboardIsVisible) {
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
} else if (_showGestureHelp) {
_showGestureHelp = false;
} else {
_showBar = !_showBar;
}
});
}),
bottomNavigationBar: Obx(() => Stack(
alignment: Alignment.bottomCenter,
children: [
gFFI.ffiModel.pi.isSet.isTrue &&
gFFI.ffiModel.waitForFirstImage.isTrue
? emptyOverlay(MyTheme.canvasColor)
: () {
gFFI.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
_bottomWidget(),
gFFI.ffiModel.pi.isSet.isFalse
? emptyOverlay(MyTheme.canvasColor)
: Offstage(),
],
)),
body: Obx(
() => getRawPointerAndKeyBody(Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return Container(
color: kColorCanvas,
child: SafeArea(
child: OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) {
Timer(const Duration(milliseconds: 200), () {
gFFI.dialogManager
.resetMobileActionsOverlay(ffi: gFFI);
_currentOrientation = orientation;
gFFI.canvasModel.updateViewStyle();
});
}
return Container(
color: MyTheme.canvasColor,
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
);
}),
),
);
})
],
)),
)),
);
}
Widget getRawPointerAndKeyBody(Widget child) {
return CameraRawPointerMouseRegion(
inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
child: gFFI.ffiModel.pi.isSet.isTrue
? RawKeyFocusScope(
focusNode: _physicalFocusNode,
inputModel: inputModel,
child: child)
: child,
);
}
Widget getBottomAppBar() {
return BottomAppBar(
elevation: 10,
color: MyTheme.accent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose(sessionId, gFFI.dialogManager);
},
),
IconButton(
color: Colors.white,
icon: Icon(Icons.tv),
onPressed: () {
setState(() => _showEdit = false);
showOptions(context, widget.id, gFFI.dialogManager);
},
)
] +
(isWeb
? []
: <Widget>[
futureBuilder(
future: gFFI.invokeMethod(
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
hasData: (isSupportVoiceCall) => IconButton(
color: Colors.white,
icon: isAndroid && isSupportVoiceCall
? SvgPicture.asset('assets/chat.svg',
colorFilter: ColorFilter.mode(
Colors.white, BlendMode.srcIn))
: Icon(Icons.message),
onPressed: () =>
isAndroid && isSupportVoiceCall
? showChatOptions(widget.id)
: onPressedTextChat(widget.id),
))
]) +
[
IconButton(
color: Colors.white,
icon: Icon(Icons.more_vert),
onPressed: () {
setState(() => _showEdit = false);
showActions(widget.id);
},
),
]),
Obx(() => IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
? null
: () {
setState(() => _showBar = !_showBar);
},
)),
],
),
);
}
Widget getBodyForMobile() {
return Container(
color: MyTheme.canvasColor,
child: Stack(children: () {
final paints = [
ImagePaint(),
Positioned(
top: 10,
right: 10,
child: QualityMonitor(gFFI.qualityMonitorModel),
),
SizedBox(
width: 0,
height: 0,
child: !_showEdit
? Container()
: TextFormField(
textInputAction: TextInputAction.newline,
autocorrect: false,
// Flutter 3.16.9 Android.
// `enableSuggestions` causes secure keyboard to be shown.
// https://github.com/flutter/flutter/issues/139143
// https://github.com/flutter/flutter/issues/146540
// enableSuggestions: false,
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
controller: _textController,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
// `onChanged` may be called depending on the input method if this widget is wrapped in
// `Focus(onKeyEvent: ..., child: ...)`
// For `Backspace` button in the soft keyboard:
// en/fr input method:
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
// 2. The button will trigger `onKeyEvent` if the text field is empty.
// ko/zh/ja input method: the button will trigger `onKeyEvent`
// and the event will not popup if `KeyEventResult.handled` is returned.
onChanged: null,
).workaroundFreezeLinuxMint(),
),
];
return paints;
}()));
}
Widget getBodyForDesktopWithListener() {
var paints = <Widget>[ImagePaint()];
return Container(
color: MyTheme.canvasColor, child: Stack(children: paints));
}
List<TTextMenu> _getMobileActionMenus() {
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
!gFFI.ffiModel.keyboard) {
return [];
}
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
if (!enabled) return [];
return [
TTextMenu(
child: Text(translate('Back')),
onPressed: () => gFFI.inputModel.onMobileBack(),
),
TTextMenu(
child: Text(translate('Home')),
onPressed: () => gFFI.inputModel.onMobileHome(),
),
TTextMenu(
child: Text(translate('Apps')),
onPressed: () => gFFI.inputModel.onMobileApps(),
),
TTextMenu(
child: Text(translate('Volume up')),
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
),
TTextMenu(
child: Text(translate('Volume down')),
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
),
TTextMenu(
child: Text(translate('Power')),
onPressed: () => gFFI.inputModel.onMobilePower(),
),
];
}
void showActions(String id) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
final mobileActionMenus = _getMobileActionMenus();
final menus = toolbarControls(context, id, gFFI);
final List<PopupMenuEntry<int>> more = [
...mobileActionMenus
.asMap()
.entries
.map((e) =>
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList(),
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
...menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(
child: e.value.getChild(),
value: e.key + mobileActionMenus.length))
.toList(),
];
() async {
var index = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: more,
elevation: 8,
);
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
}
}
}();
}
onPressedTextChat(String id) {
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
gFFI.chatModel.toggleChatOverlay();
}
showChatOptions(String id) async {
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
{TextStyle? labelStyle}) =>
TTextMenu(
child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IgnorePointer(
child: IconButton(
onPressed: null,
icon: icon,
),
),
),
onPressed: onPressed,
);
final isInVoice = [
VoiceCallStatus.waitingForResponse,
VoiceCallStatus.connected
].contains(gFFI.chatModel.voiceCallStatus.value);
final menus = [
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
() => onPressedTextChat(widget.id)),
isInVoice
? makeTextMenu(
'End voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter:
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
),
onPressEndVoiceCall,
labelStyle: TextStyle(color: Colors.redAccent))
: makeTextMenu(
'Voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
),
onPressVoiceCall),
];
final menuItems = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
.toList();
Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
var index = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: menuItems,
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
}
});
}
}
class ImagePaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
final c = Provider.of<CanvasModel>(context);
var s = c.scale;
final adjust = c.getAdjustY();
return CustomPaint(
painter: ImagePainter(
image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
);
}
}
void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
var displays = <Widget>[];
final pi = gFFI.ffiModel.pi;
final image = gFFI.ffiModel.getConnectionImage();
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}
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;
openMonitorInTheSameTab(i, gFFI, pi);
gFFI.dialogManager.dismissAll();
},
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(2),
color: i == cur
? Theme.of(context).primaryColor.withOpacity(0.6)
: null),
child: Center(
child: Text((i + 1).toString(),
style: TextStyle(
color: i == cur ? Colors.white : Colors.black87,
fontWeight: FontWeight.bold))))));
}
displays.add(Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: children,
)));
}
if (displays.isNotEmpty) {
displays.add(const Divider(color: MyTheme.border));
}
List<TRadioMenu<String>> viewStyleRadios =
await toolbarViewStyle(context, id, gFFI);
List<TRadioMenu<String>> imageQualityRadios =
await toolbarImageQuality(context, id, gFFI);
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI);
dialogManager.show((setState, close, context) {
var viewStyle =
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
var imageQuality =
(imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
.obs;
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
final radios = [
for (var e in viewStyleRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
viewStyle.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) viewStyle.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in imageQualityRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
imageQuality.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) imageQuality.value = v;
}
: null)),
const Divider(color: MyTheme.border),
for (var e in codecRadios)
Obx(() => getRadio<String>(
e.child,
e.value,
codec.value,
e.onChanged != null
? (v) {
e.onChanged?.call(v);
if (v != null) codec.value = v;
}
: null)),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
];
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
final displayTogglesList = displayToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxToggleValues[e.key].value,
onChanged: e.value.onChanged != null
? (v) {
e.value.onChanged?.call(v);
if (v != null) rxToggleValues[e.key].value = v;
}
: null,
title: e.value.child)))
.toList();
final toggles = [
...displayTogglesList,
];
var popupDialogMenus = List<Widget>.empty(growable: true);
if (popupDialogMenus.isNotEmpty) {
popupDialogMenus.add(const Divider(color: MyTheme.border));
}
return CustomAlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays + radios + popupDialogMenus + toggles),
);
}, clickMaskDismiss: true, backDismiss: true).then((value) {
_disableAndroidSoftKeyboard();
});
}
class FABLocation extends FloatingActionButtonLocation {
FloatingActionButtonLocation location;
double offsetX;
double offsetY;
FABLocation(this.location, this.offsetX, this.offsetY);
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final offset = location.getOffset(scaffoldGeometry);
return Offset(offset.dx + offsetX, offset.dy + offsetY);
}
}
@@ -235,6 +235,17 @@ class TextureModel {
} }
} }
onViewCameraPageDispose(bool closeSession) async {
final ffi = parent.target;
if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi);
}
for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi);
}
}
ensureControl(int display) { ensureControl(int display) {
var ctl = _control[display]; var ctl = _control[display];
if (ctl == null) { if (ctl == null) {
+18
View File
@@ -369,6 +369,7 @@ class InputModel {
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly; bool get isViewOnly => parent.target!.ffiModel.viewOnly;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
@@ -471,6 +472,7 @@ class InputModel {
KeyEventResult handleRawKeyEvent(RawKeyEvent e) { KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled; if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
if (!isInputSourceFlutter) { if (!isInputSourceFlutter) {
if (isDesktop) { if (isDesktop) {
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -525,6 +527,7 @@ class InputModel {
KeyEventResult handleKeyEvent(KeyEvent e) { KeyEventResult handleKeyEvent(KeyEvent e) {
if (isViewOnly) return KeyEventResult.handled; if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
if (!isInputSourceFlutter) { if (!isInputSourceFlutter) {
if (isDesktop) { if (isDesktop) {
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -724,6 +727,7 @@ class InputModel {
/// [press] indicates a click event(down and up). /// [press] indicates a click event(down and up).
void inputKey(String name, {bool? down, bool? press}) { void inputKey(String name, {bool? down, bool? press}) {
if (!keyboardPerm) return; if (!keyboardPerm) return;
if (isViewCamera) return;
bind.sessionInputKey( bind.sessionInputKey(
sessionId: sessionId, sessionId: sessionId,
name: name, name: name,
@@ -785,6 +789,7 @@ class InputModel {
/// Send scroll event with scroll distance [y]. /// Send scroll event with scroll distance [y].
Future<void> scroll(int y) async { Future<void> scroll(int y) async {
if (isViewCamera) return;
await bind.sessionSendMouse( await bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: json msg: json
@@ -808,6 +813,7 @@ class InputModel {
/// Send mouse press event. /// Send mouse press event.
Future<void> sendMouse(String type, MouseButtons button) async { Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return; if (!keyboardPerm) return;
if (isViewCamera) return;
await bind.sessionSendMouse( await bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value}))); msg: json.encode(modify({'type': type, 'buttons': button.value})));
@@ -834,6 +840,7 @@ class InputModel {
/// Send mouse movement event with distance in [x] and [y]. /// Send mouse movement event with distance in [x] and [y].
Future<void> moveMouse(double x, double y) async { Future<void> moveMouse(double x, double y) async {
if (!keyboardPerm) return; if (!keyboardPerm) return;
if (isViewCamera) return;
var x2 = x.toInt(); var x2 = x.toInt();
var y2 = y.toInt(); var y2 = y.toInt();
await bind.sessionSendMouse( await bind.sessionSendMouse(
@@ -857,6 +864,7 @@ class InputModel {
_lastScale = 1.0; _lastScale = 1.0;
_stopFling = true; _stopFling = true;
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', kMouseEventTypePanStart, e.position); handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
} }
@@ -865,6 +873,7 @@ class InputModel {
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (peerPlatform != kPeerPlatformAndroid) { if (peerPlatform != kPeerPlatformAndroid) {
final scale = ((e.scale - _lastScale) * 1000).toInt(); final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale; _lastScale = e.scale;
@@ -904,6 +913,7 @@ class InputModel {
handlePointerEvent('touch', kMouseEventTypePanUpdate, handlePointerEvent('touch', kMouseEventTypePanUpdate,
Offset(x.toDouble(), y.toDouble())); Offset(x.toDouble(), y.toDouble()));
} else { } else {
if (isViewCamera) return;
bind.sessionSendMouse( bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
@@ -912,6 +922,7 @@ class InputModel {
} }
void _scheduleFling(double x, double y, int delay) { void _scheduleFling(double x, double y, int delay) {
if (isViewCamera) return;
if ((x == 0 && y == 0) || _stopFling) { if ((x == 0 && y == 0) || _stopFling) {
_fling = false; _fling = false;
return; return;
@@ -963,6 +974,7 @@ class InputModel {
} }
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (isViewCamera) return;
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
return; return;
@@ -994,6 +1006,7 @@ class InputModel {
_remoteWindowCoords = []; _remoteWindowCoords = [];
_windowRect = null; _windowRect = null;
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
isPhysicalMouse.value = false; isPhysicalMouse.value = false;
@@ -1007,6 +1020,7 @@ class InputModel {
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false; if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
@@ -1015,6 +1029,7 @@ class InputModel {
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) { if (_queryOtherWindowCoords) {
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
@@ -1049,6 +1064,7 @@ class InputModel {
void onPointerSignalImage(PointerSignalEvent e) { void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return;
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt(); var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt(); var dy = e.scrollDelta.dy.toInt();
@@ -1146,6 +1162,7 @@ class InputModel {
} }
final evt = PointerEventToRust(kind, type, evtValue).toJson(); final evt = PointerEventToRust(kind, type, evtValue).toJson();
if (isViewCamera) return;
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, msg: json.encode(modify(evt))); sessionId: sessionId, msg: json.encode(modify(evt)));
} }
@@ -1177,6 +1194,7 @@ class InputModel {
Offset offset, { Offset offset, {
bool onExit = false, bool onExit = false,
}) { }) {
if (isViewCamera) return;
double x = offset.dx; double x = offset.dx;
double y = max(0.0, offset.dy); double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) { if (_checkPeerControlProtected(x, y)) {
+37 -9
View File
@@ -407,7 +407,9 @@ class FfiModel with ChangeNotifier {
parent.target?.fileModel.sendEmptyDirs(evt); parent.target?.fileModel.sendEmptyDirs(evt);
} }
} else if (name == "record_status") { } else if (name == "record_status") {
if (desktopType == DesktopType.remote || isMobile) { if (desktopType == DesktopType.remote ||
desktopType == DesktopType.viewCamera ||
isMobile) {
parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
} }
} else { } else {
@@ -501,7 +503,9 @@ class FfiModel with ChangeNotifier {
final display = int.parse(evt['display']); final display = int.parse(evt['display']);
if (_pi.currentDisplay != kAllDisplayValue) { if (_pi.currentDisplay != kAllDisplayValue) {
if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) { if (bind.peerGetSessionsCount(
id: peerId, connType: parent.target!.connType.index) >
1) {
if (display != _pi.currentDisplay) { if (display != _pi.currentDisplay) {
return; return;
} }
@@ -809,7 +813,9 @@ class FfiModel with ChangeNotifier {
_pi.primaryDisplay = currentDisplay; _pi.primaryDisplay = currentDisplay;
} }
if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) { if (bind.peerGetSessionsCount(
id: peerId, connType: parent.target!.connType.index) <=
1) {
_pi.currentDisplay = currentDisplay; _pi.currentDisplay = currentDisplay;
} }
@@ -827,9 +833,11 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: kOptionTouchMode) != sessionId: sessionId, arg: kOptionTouchMode) !=
''; '';
} }
// FIXME: handle ViewCamera ConnType independently.
if (connType == ConnType.fileTransfer) { if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady(); parent.target?.fileModel.onReady();
} else if (connType == ConnType.defaultConn) { } else if (connType == ConnType.defaultConn ||
connType == ConnType.viewCamera) {
List<Display> newDisplays = []; List<Display> newDisplays = [];
List<dynamic> displays = json.decode(evt['displays']); List<dynamic> displays = json.decode(evt['displays']);
for (int i = 0; i < displays.length; ++i) { for (int i = 0; i < displays.length; ++i) {
@@ -859,7 +867,7 @@ class FfiModel with ChangeNotifier {
bind.sessionGetToggleOptionSync( bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleViewOnly)); sessionId: sessionId, arg: kOptionToggleViewOnly));
} }
if (connType == ConnType.defaultConn) { if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
final platformAdditions = evt['platform_additions']; final platformAdditions = evt['platform_additions'];
if (platformAdditions != null && platformAdditions != '') { if (platformAdditions != null && platformAdditions != '') {
try { try {
@@ -2576,7 +2584,8 @@ class ElevationModel with ChangeNotifier {
onPortableServiceRunning(bool running) => _running = running; onPortableServiceRunning(bool running) => _running = running;
} }
enum ConnType { defaultConn, fileTransfer, portForward, rdp } // The index values of `ConnType` are same as rust protobuf.
enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera }
/// Flutter state manager and data communication with the Rust core. /// Flutter state manager and data communication with the Rust core.
class FFI { class FFI {
@@ -2651,10 +2660,11 @@ class FFI {
ffiModel.waitForImageTimer = null; ffiModel.waitForImageTimer = null;
} }
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. /// Start with the given [id]. Only transfer file if [isFileTransfer], only view camera if [isViewCamera], only port forward if [isPortForward].
void start( void start(
String id, { String id, {
bool isFileTransfer = false, bool isFileTransfer = false,
bool isViewCamera = false,
bool isPortForward = false, bool isPortForward = false,
bool isRdp = false, bool isRdp = false,
String? switchUuid, String? switchUuid,
@@ -2669,9 +2679,15 @@ class FFI {
closed = false; closed = false;
auditNote = ''; auditNote = '';
if (isMobile) mobileReset(); if (isMobile) mobileReset();
assert(!(isFileTransfer && isPortForward), 'more than one connect type'); assert(
(!(isPortForward && isViewCamera)) &&
(!(isViewCamera && isPortForward)) &&
(!(isPortForward && isFileTransfer)),
'more than one connect type');
if (isFileTransfer) { if (isFileTransfer) {
connType = ConnType.fileTransfer; connType = ConnType.fileTransfer;
} else if (isViewCamera) {
connType = ConnType.viewCamera;
} else if (isPortForward) { } else if (isPortForward) {
connType = ConnType.portForward; connType = ConnType.portForward;
} else { } else {
@@ -2691,6 +2707,7 @@ class FFI {
sessionId: sessionId, sessionId: sessionId,
id: id, id: id,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isPortForward: isPortForward, isPortForward: isPortForward,
isRdp: isRdp, isRdp: isRdp,
switchUuid: switchUuid ?? '', switchUuid: switchUuid ?? '',
@@ -2706,7 +2723,10 @@ class FFI {
return; return;
} }
final addRes = bind.sessionAddExistedSync( final addRes = bind.sessionAddExistedSync(
id: id, sessionId: sessionId, displays: Int32List.fromList(displays)); id: id,
sessionId: sessionId,
displays: Int32List.fromList(displays),
isViewCamera: isViewCamera);
if (addRes != '') { if (addRes != '') {
debugPrint( debugPrint(
'Unreachable, failed to add existed session to $id, $addRes'); 'Unreachable, failed to add existed session to $id, $addRes');
@@ -2717,6 +2737,11 @@ class FFI {
if (isDesktop && connType == ConnType.defaultConn) { if (isDesktop && connType == ConnType.defaultConn) {
textureModel.updateCurrentDisplay(display ?? 0); textureModel.updateCurrentDisplay(display ?? 0);
} }
// FIXME: separate cameras displays or shift all indices.
if (isDesktop && connType == ConnType.viewCamera) {
// FIXME: currently the default 0 is not used.
textureModel.updateCurrentDisplay(display ?? 0);
}
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions. // CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
// Though the stream is returned immediately, the stream may not be ready. // Though the stream is returned immediately, the stream may not be ready.
@@ -2993,6 +3018,9 @@ class PeerInfo with ChangeNotifier {
bool get isAmyuniIdd => bool get isAmyuniIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd'; platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
bool get isSupportViewCamera =>
platformAdditions[kPlatformAdditionsSupportViewCamera] == true;
Display? tryGetDisplay({int? display}) { Display? tryGetDisplay({int? display}) {
if (displays.isEmpty) { if (displays.isEmpty) {
return null; return null;
+8 -1
View File
@@ -791,6 +791,7 @@ class ServerModel with ChangeNotifier {
enum ClientType { enum ClientType {
remote, remote,
file, file,
camera,
portForward, portForward,
} }
@@ -798,6 +799,7 @@ class Client {
int id = 0; // client connections inner count id int id = 0; // client connections inner count id
bool authorized = false; bool authorized = false;
bool isFileTransfer = false; bool isFileTransfer = false;
bool isViewCamera = false;
String portForward = ""; String portForward = "";
String name = ""; String name = "";
String peerId = ""; // peer user's id,show at app String peerId = ""; // peer user's id,show at app
@@ -815,13 +817,15 @@ class Client {
RxInt unreadChatMessageCount = 0.obs; RxInt unreadChatMessageCount = 0.obs;
Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera, this.name, this.peerId,
this.keyboard, this.clipboard, this.audio); this.keyboard, this.clipboard, this.audio);
Client.fromJson(Map<String, dynamic> json) { Client.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
authorized = json['authorized']; authorized = json['authorized'];
isFileTransfer = json['is_file_transfer']; isFileTransfer = json['is_file_transfer'];
// TODO: no entry then default.
isViewCamera = json['is_view_camera'];
portForward = json['port_forward']; portForward = json['port_forward'];
name = json['name']; name = json['name'];
peerId = json['peer_id']; peerId = json['peer_id'];
@@ -843,6 +847,7 @@ class Client {
data['id'] = id; data['id'] = id;
data['authorized'] = authorized; data['authorized'] = authorized;
data['is_file_transfer'] = isFileTransfer; data['is_file_transfer'] = isFileTransfer;
data['is_view_camera'] = isViewCamera;
data['port_forward'] = portForward; data['port_forward'] = portForward;
data['name'] = name; data['name'] = name;
data['peer_id'] = peerId; data['peer_id'] = peerId;
@@ -863,6 +868,8 @@ class Client {
ClientType type_() { ClientType type_() {
if (isFileTransfer) { if (isFileTransfer) {
return ClientType.file; return ClientType.file;
} else if (isViewCamera) {
return ClientType.camera;
} else if (portForward.isNotEmpty) { } else if (portForward.isNotEmpty) {
return ClientType.portForward; return ClientType.portForward;
} else { } else {
+68 -18
View File
@@ -11,7 +11,14 @@ import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order /// must keep the order
// ignore: constant_identifier_names // ignore: constant_identifier_names
enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } enum WindowType {
Main,
RemoteDesktop,
FileTransfer,
ViewCamera,
PortForward,
Unknown
}
extension Index on int { extension Index on int {
WindowType get windowType { WindowType get windowType {
@@ -23,6 +30,8 @@ extension Index on int {
case 2: case 2:
return WindowType.FileTransfer; return WindowType.FileTransfer;
case 3: case 3:
return WindowType.ViewCamera;
case 4:
return WindowType.PortForward; return WindowType.PortForward;
default: default:
return WindowType.Unknown; return WindowType.Unknown;
@@ -50,31 +59,46 @@ class RustDeskMultiWindowManager {
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true); final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
final List<int> _remoteDesktopWindows = List.empty(growable: true); final List<int> _remoteDesktopWindows = List.empty(growable: true);
final List<int> _fileTransferWindows = List.empty(growable: true); final List<int> _fileTransferWindows = List.empty(growable: true);
final List<int> _viewCameraWindows = List.empty(growable: true);
final List<int> _portForwardWindows = List.empty(growable: true); final List<int> _portForwardWindows = List.empty(growable: true);
moveTabToNewWindow(int windowId, String peerId, String sessionId) async { moveTabToNewWindow(int windowId, String peerId, String sessionId,
WindowType windowType) async {
var params = { var params = {
'type': WindowType.RemoteDesktop.index, 'type': windowType.index,
'id': peerId, 'id': peerId,
'tab_window_id': windowId, 'tab_window_id': windowId,
'session_id': sessionId, 'session_id': sessionId,
}; };
await _newSession( if (windowType == WindowType.RemoteDesktop) {
false, await _newSession(
WindowType.RemoteDesktop, false,
kWindowEventNewRemoteDesktop, WindowType.RemoteDesktop,
peerId, kWindowEventNewRemoteDesktop,
_remoteDesktopWindows, peerId,
jsonEncode(params), _remoteDesktopWindows,
); jsonEncode(params),
);
} else if (windowType == WindowType.ViewCamera) {
await _newSession(
false,
WindowType.ViewCamera,
kWindowEventNewViewCamera,
peerId,
_viewCameraWindows,
jsonEncode(params),
);
}
} }
// This function must be called in the main window thread. // This function must be called in the main window thread.
// Because the _remoteDesktopWindows is managed in that thread. // Because the _remoteDesktopWindows is managed in that thread.
openMonitorSession(int windowId, String peerId, int display, int displayCount, openMonitorSession(int windowId, String peerId, int display, int displayCount,
Rect? screenRect) async { Rect? screenRect, int windowType) async {
if (_remoteDesktopWindows.length > 1) { final isCamera = windowType == WindowType.ViewCamera.index;
for (final windowId in _remoteDesktopWindows) { final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows;
if (windowIDs.length > 1) {
for (final windowId in windowIDs) {
if (await DesktopMultiWindow.invokeMethod( if (await DesktopMultiWindow.invokeMethod(
windowId, windowId,
kWindowEventActiveDisplaySession, kWindowEventActiveDisplaySession,
@@ -91,7 +115,7 @@ class RustDeskMultiWindowManager {
? List.generate(displayCount, (index) => index) ? List.generate(displayCount, (index) => index)
: [display]; : [display];
var params = { var params = {
'type': WindowType.RemoteDesktop.index, 'type': windowType,
'id': peerId, 'id': peerId,
'tab_window_id': windowId, 'tab_window_id': windowId,
'display': display, 'display': display,
@@ -107,10 +131,10 @@ class RustDeskMultiWindowManager {
} }
await _newSession( await _newSession(
false, false,
WindowType.RemoteDesktop, windowType.windowType,
kWindowEventNewRemoteDesktop, isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop,
peerId, peerId,
_remoteDesktopWindows, windowIDs,
jsonEncode(params), jsonEncode(params),
screenRect: screenRect, screenRect: screenRect,
); );
@@ -277,6 +301,27 @@ class RustDeskMultiWindowManager {
); );
} }
Future<MultiWindowCallResult> newViewCamera(
String remoteId, {
String? password,
bool? isSharedPassword,
String? switchUuid,
bool? forceRelay,
String? connToken,
}) async {
return await newSession(
WindowType.ViewCamera,
kWindowEventNewViewCamera,
remoteId,
_viewCameraWindows,
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
isSharedPassword: isSharedPassword,
connToken: connToken,
);
}
Future<MultiWindowCallResult> newPortForward( Future<MultiWindowCallResult> newPortForward(
String remoteId, String remoteId,
bool isRDP, { bool isRDP, {
@@ -324,6 +369,8 @@ class RustDeskMultiWindowManager {
return _remoteDesktopWindows; return _remoteDesktopWindows;
case WindowType.FileTransfer: case WindowType.FileTransfer:
return _fileTransferWindows; return _fileTransferWindows;
case WindowType.ViewCamera:
return _viewCameraWindows;
case WindowType.PortForward: case WindowType.PortForward:
return _portForwardWindows; return _portForwardWindows;
case WindowType.Unknown: case WindowType.Unknown:
@@ -342,6 +389,9 @@ class RustDeskMultiWindowManager {
case WindowType.FileTransfer: case WindowType.FileTransfer:
_fileTransferWindows.clear(); _fileTransferWindows.clear();
break; break;
case WindowType.ViewCamera:
_viewCameraWindows.clear();
break;
case WindowType.PortForward: case WindowType.PortForward:
_portForwardWindows.clear(); _portForwardWindows.clear();
break; break;
+6 -2
View File
@@ -60,7 +60,8 @@ class RustdeskImpl {
throw UnimplementedError("hostStopSystemKeyPropagate"); throw UnimplementedError("hostStopSystemKeyPropagate");
} }
int peerGetDefaultSessionsCount({required String id, dynamic hint}) { int peerGetSessionsCount(
{required String id, required int connType, dynamic hint}) {
return 0; return 0;
} }
@@ -68,6 +69,7 @@ class RustdeskImpl {
{required String id, {required String id,
required UuidValue sessionId, required UuidValue sessionId,
required Int32List displays, required Int32List displays,
required bool isViewCamera,
dynamic hint}) { dynamic hint}) {
return ''; return '';
} }
@@ -76,6 +78,7 @@ class RustdeskImpl {
{required UuidValue sessionId, {required UuidValue sessionId,
required String id, required String id,
required bool isFileTransfer, required bool isFileTransfer,
required bool isViewCamera,
required bool isPortForward, required bool isPortForward,
required bool isRdp, required bool isRdp,
required String switchUuid, required String switchUuid,
@@ -90,7 +93,8 @@ class RustdeskImpl {
'id': id, 'id': id,
'password': password, 'password': password,
'is_shared_password': isSharedPassword, 'is_shared_password': isSharedPassword,
'isFileTransfer': isFileTransfer 'isFileTransfer': isFileTransfer,
'isViewCamera': isViewCamera
}) })
]); ]);
} }
+4 -4
View File
@@ -10,10 +10,10 @@ import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
final testClients = [ final testClients = [
Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false),
Client(1, false, false, "UserBBBBB", "221123123", true, false, false), Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false),
Client(2, false, false, "UserC", "331123123", true, false, false), Client(2, false, false, false, "UserC", "331123123", true, false, false, false),
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false)
]; ];
/// flutter run -d {platform} -t test/cm_test.dart to test cm /// flutter run -d {platform} -t test/cm_test.dart to test cm
+1
View File
@@ -23,6 +23,7 @@ lazy_static = "1.4"
hbb_common = { path = "../hbb_common" } hbb_common = { path = "../hbb_common" }
webm = { git = "https://github.com/rustdesk-org/rust-webm" } webm = { git = "https://github.com/rustdesk-org/rust-webm" }
serde = {version="1.0", features=["derive"]} serde = {version="1.0", features=["derive"]}
nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] }
[dependencies.winapi] [dependencies.winapi]
version = "0.3" version = "0.3"
+232
View File
@@ -0,0 +1,232 @@
use std::{
io,
sync::{Arc, Mutex},
};
use nokhwa::{
pixel_format::RgbAFormat,
query,
utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType},
Camera,
};
use hbb_common::message_proto::{DisplayInfo, Resolution};
#[cfg(feature = "vram")]
use crate::AdapterDevice;
use crate::common::{bail, ResultType};
use crate::{Frame, PixelBuffer, Pixfmt, TraitCapturer};
pub const PRIMARY_CAMERA_IDX: usize = 0;
lazy_static::lazy_static! {
static ref SYNC_CAMERA_DISPLAYS: Arc<Mutex<Vec<DisplayInfo>>> = Arc::new(Mutex::new(Vec::new()));
}
pub struct Cameras;
// pre-condition
pub fn primary_camera_exists() -> bool {
Cameras::exists(PRIMARY_CAMERA_IDX)
}
impl Cameras {
pub fn all_info() -> ResultType<Vec<DisplayInfo>> {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
return Ok(Vec::new());
match query(ApiBackend::Auto) {
Ok(cameras) => {
let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap();
camera_displays.clear();
// FIXME: nokhwa returns duplicate info for one physical camera on linux for now.
// issue: https://github.com/l1npengtul/nokhwa/issues/171
// Use only one camera as a temporary hack.
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
let Some(info) = cameras.first() else {
bail!("No camera found")
};
let camera = Self::create_camera(info.index())?;
let resolution = camera.resolution();
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
camera_displays.push(DisplayInfo {
x: 0,
y: 0,
name: info.human_name().clone(),
width,
height,
online: true,
cursor_embedded: false,
scale:1.0,
original_resolution: Some(Resolution {
width,
height,
..Default::default()
}).into(),
..Default::default()
});
} else {
let mut x = 0;
for info in &cameras {
let camera = Self::create_camera(info.index())?;
let resolution = camera.resolution();
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
camera_displays.push(DisplayInfo {
x,
y: 0,
name: info.human_name().clone(),
width,
height,
online: true,
cursor_embedded: false,
scale:1.0,
original_resolution: Some(Resolution {
width,
height,
..Default::default()
}).into(),
..Default::default()
});
x += width;
}
}
}
Ok(camera_displays.clone())
}
Err(e) => {
bail!("Query cameras error: {}", e)
}
}
}
pub fn exists(index: usize) -> bool {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
return false;
match query(ApiBackend::Auto) {
Ok(cameras) => index < cameras.len(),
_ => return false,
}
}
fn create_camera(index: &CameraIndex) -> ResultType<Camera> {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
bail!("This platform doesn't support camera yet");
let result = Camera::new(
index.clone(),
RequestedFormat::new::<RgbAFormat>(RequestedFormatType::AbsoluteHighestResolution),
);
match result {
Ok(camera) => Ok(camera),
Err(e) => bail!("create camera{} error: {}", index, e),
}
}
pub fn get_camera_resolution(index: usize) -> ResultType<Resolution> {
let index = CameraIndex::Index(index as u32);
let camera = Self::create_camera(&index)?;
let resolution = camera.resolution();
Ok(Resolution {
width: resolution.width() as i32,
height: resolution.height() as i32,
..Default::default()
})
}
pub fn get_sync_cameras() -> Vec<DisplayInfo> {
SYNC_CAMERA_DISPLAYS.lock().unwrap().clone()
}
pub fn get_capturer(current: usize) -> ResultType<Box<dyn TraitCapturer>> {
Ok(Box::new(CameraCapturer::new(current)?))
}
}
pub struct CameraCapturer {
camera: Camera,
data: Vec<u8>,
last_data: Vec<u8>, // for faster compare and copy
}
impl CameraCapturer {
fn new(current: usize) -> ResultType<Self> {
let index = CameraIndex::Index(current as u32);
let camera = Cameras::create_camera(&index)?;
Ok(CameraCapturer {
camera,
data: Vec::new(),
last_data: Vec::new(),
})
}
}
impl TraitCapturer for CameraCapturer {
fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
// TODO: move this check outside `frame`.
if !self.camera.is_stream_open() {
if let Err(e) = self.camera.open_stream() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera open stream error: {}", e),
));
}
}
match self.camera.frame() {
Ok(buffer) => {
match buffer.decode_image::<RgbAFormat>() {
Ok(decoded) => {
self.data = decoded.as_raw().to_vec();
crate::would_block_if_equal(&mut self.last_data, &self.data)?;
// FIXME: macos's PixelBuffer cannot be directly created from bytes slice.
cfg_if::cfg_if! {
if #[cfg(any(target_os = "linux", target_os = "windows"))] {
Ok(Frame::PixelBuffer(PixelBuffer::new(
&self.data,
Pixfmt::RGBA,
decoded.width() as usize,
decoded.height() as usize,
)))
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera is not supported on this platform yet"),
))
}
}
}
Err(e) => Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera frame decode error: {}", e),
)),
}
}
Err(e) => Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera frame error: {}", e),
)),
}
}
#[cfg(windows)]
fn is_gdi(&self) -> bool {
false
}
#[cfg(windows)]
fn set_gdi(&mut self) -> bool {
false
}
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice {
AdapterDevice::default()
}
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, _texture: bool) {}
}
+10 -3
View File
@@ -70,23 +70,30 @@ impl TraitCapturer for Capturer {
pub struct PixelBuffer<'a> { pub struct PixelBuffer<'a> {
data: &'a [u8], data: &'a [u8],
pixfmt: Pixfmt,
width: usize, width: usize,
height: usize, height: usize,
stride: Vec<usize>, stride: Vec<usize>,
} }
impl<'a> PixelBuffer<'a> { impl<'a> PixelBuffer<'a> {
pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self {
let stride0 = data.len() / height; let stride0 = data.len() / height;
let mut stride = Vec::new(); let mut stride = Vec::new();
stride.push(stride0); stride.push(stride0);
PixelBuffer { PixelBuffer {
data, data,
pixfmt,
width, width,
height, height,
stride, stride,
} }
} }
#[allow(non_snake_case)]
pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self {
Self::new(data, Pixfmt::BGRA, width, height)
}
} }
impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
@@ -107,7 +114,7 @@ impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
} }
fn pixfmt(&self) -> Pixfmt { fn pixfmt(&self) -> Pixfmt {
Pixfmt::BGRA self.pixfmt
} }
} }
@@ -232,7 +239,7 @@ impl CapturerMag {
impl TraitCapturer for CapturerMag { impl TraitCapturer for CapturerMag {
fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result<Frame<'a>> { fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result<Frame<'a>> {
self.inner.frame(&mut self.data)?; self.inner.frame(&mut self.data)?;
Ok(Frame::PixelBuffer(PixelBuffer::new( Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
&self.data, &self.data,
self.inner.get_rect().1, self.inner.get_rect().1,
self.inner.get_rect().2, self.inner.get_rect().2,
+1
View File
@@ -48,6 +48,7 @@ pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
pub mod camera;
pub mod aom; pub mod aom;
pub mod record; pub mod record;
mod vpx; mod vpx;
+2 -2
View File
@@ -25,7 +25,7 @@ pub struct RecorderContext {
pub server: bool, pub server: bool,
pub id: String, pub id: String,
pub dir: String, pub dir: String,
pub display: usize, pub video_service_name: String,
pub tx: Option<Sender<RecordState>>, pub tx: Option<Sender<RecordState>>,
} }
@@ -46,7 +46,7 @@ impl RecorderContext2 {
+ "_" + "_"
+ &ctx.id.clone() + &ctx.id.clone()
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
+ &format!("display{}_", ctx.display) + &format!("{}_", ctx.video_service_name)
+ &self.format.to_string().to_lowercase() + &self.format.to_string().to_lowercase()
+ if self.format == CodecFormat::VP9 + if self.format == CodecFormat::VP9
|| self.format == CodecFormat::VP8 || self.format == CodecFormat::VP8
+18 -9
View File
@@ -5,7 +5,7 @@ use std::{
}; };
use crate::{ use crate::{
codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg}, codec::{enable_vram_option, EncoderApi, EncoderCfg},
hwcodec::HwCodecConfig, hwcodec::HwCodecConfig,
AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt,
}; };
@@ -30,8 +30,8 @@ use hwcodec::{
// https://cybersided.com/two-monitors-two-gpus/ // https://cybersided.com/two-monitors-two-gpus/
// https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks // https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref ENOCDE_NOT_USE: Arc<Mutex<HashMap<usize, bool>>> = Default::default(); static ref ENOCDE_NOT_USE: Arc<Mutex<HashMap<String, bool>>> = Default::default();
static ref FALLBACK_GDI_DISPLAYS: Arc<Mutex<HashSet<usize>>> = Default::default(); static ref FALLBACK_GDI_DISPLAYS: Arc<Mutex<HashSet<String>>> = Default::default();
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -287,16 +287,25 @@ impl VRamEncoder {
crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264) crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264)
} }
pub fn set_not_use(display: usize, not_use: bool) { pub fn set_not_use(video_service_name: String, not_use: bool) {
log::info!("set display#{display} not use vram encode to {not_use}"); log::info!("set {video_service_name} not use vram encode to {not_use}");
ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use); ENOCDE_NOT_USE
.lock()
.unwrap()
.insert(video_service_name, not_use);
} }
pub fn set_fallback_gdi(display: usize, fallback: bool) { pub fn set_fallback_gdi(video_service_name: String, fallback: bool) {
if fallback { if fallback {
FALLBACK_GDI_DISPLAYS.lock().unwrap().insert(display); FALLBACK_GDI_DISPLAYS
.lock()
.unwrap()
.insert(video_service_name);
} else { } else {
FALLBACK_GDI_DISPLAYS.lock().unwrap().remove(&display); FALLBACK_GDI_DISPLAYS
.lock()
.unwrap()
.remove(&video_service_name);
} }
} }
} }
+1 -1
View File
@@ -396,7 +396,7 @@ impl Capturer {
} else { } else {
let width = self.width; let width = self.width;
let height = self.height; let height = self.height;
Ok(Frame::PixelBuffer(PixelBuffer::new( Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
self.get_pixelbuffer(timeout)?, self.get_pixelbuffer(timeout)?,
width, width,
height, height,
+13 -4
View File
@@ -1389,14 +1389,14 @@ impl VideoHandler {
} }
/// Start or stop screen record. /// Start or stop screen record.
pub fn record_screen(&mut self, start: bool, id: String, display: usize) { pub fn record_screen(&mut self, start: bool, id: String, video_service_name: String) {
self.record = false; self.record = false;
if start { if start {
self.recorder = Recorder::new(RecorderContext { self.recorder = Recorder::new(RecorderContext {
server: false, server: false,
id, id,
dir: crate::ui_interface::video_save_directory(false), dir: crate::ui_interface::video_save_directory(false),
display, video_service_name,
tx: None, tx: None,
}) })
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))));
@@ -2349,6 +2349,7 @@ impl LoginConfigHandler {
show_hidden: !self.get_option("remote_show_hidden").is_empty(), show_hidden: !self.get_option("remote_show_hidden").is_empty(),
..Default::default() ..Default::default()
}), }),
ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()),
ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward {
host: self.port_forward.0.clone(), host: self.port_forward.0.clone(),
port: self.port_forward.1, port: self.port_forward.1,
@@ -2436,6 +2437,14 @@ pub fn start_video_thread<F, T>(
{ {
let mut video_callback = video_callback; let mut video_callback = video_callback;
let mut last_chroma = None; let mut last_chroma = None;
let video_service_name = crate::video_service::get_service_name(
if session.is_view_camera() {
crate::video_service::VideoSource::Camera
} else {
crate::video_service::VideoSource::Monitor
},
display,
);
std::thread::spawn(move || { std::thread::spawn(move || {
#[cfg(windows)] #[cfg(windows)]
@@ -2478,7 +2487,7 @@ pub fn start_video_thread<F, T>(
let record_permission = session.lc.read().unwrap().record_permission; let record_permission = session.lc.read().unwrap().record_permission;
let id = session.lc.read().unwrap().id.clone(); let id = session.lc.read().unwrap().id.clone();
if record_state && record_permission { if record_state && record_permission {
handler.record_screen(true, id, display); handler.record_screen(true, id, video_service_name.clone());
} }
video_handler = Some(handler); video_handler = Some(handler);
} }
@@ -2559,7 +2568,7 @@ pub fn start_video_thread<F, T>(
MediaData::RecordScreen(start) => { MediaData::RecordScreen(start) => {
let id = session.lc.read().unwrap().id.clone(); let id = session.lc.read().unwrap().id.clone();
if let Some(handler) = video_handler.as_mut() { if let Some(handler) = video_handler.as_mut() {
handler.record_screen(start, id, display); handler.record_screen(start, id, video_service_name.clone());
} }
} }
_ => {} _ => {}
+49 -6
View File
@@ -83,6 +83,7 @@ struct ParsedPeerInfo {
platform: String, platform: String,
is_installed: bool, is_installed: bool,
idd_impl: String, idd_impl: String,
support_view_camera: bool,
} }
impl ParsedPeerInfo { impl ParsedPeerInfo {
@@ -129,7 +130,10 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let _file_clip_context_holder = { let _file_clip_context_holder = {
// `is_port_forward()` will not reach here, but we still check it for clarity. // `is_port_forward()` will not reach here, but we still check it for clarity.
if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { if !self.handler.is_file_transfer()
&& !self.handler.is_port_forward()
&& !self.handler.is_view_camera()
{
// It is ok to call this function multiple times. // It is ok to call this function multiple times.
ContextSend::enable(true); ContextSend::enable(true);
Some(crate::SimpleCallOnReturn { Some(crate::SimpleCallOnReturn {
@@ -152,6 +156,8 @@ impl<T: InvokeUiSession> Remote<T> {
let mut received = false; let mut received = false;
let conn_type = if self.handler.is_file_transfer() { let conn_type = if self.handler.is_file_transfer() {
ConnType::FILE_TRANSFER ConnType::FILE_TRANSFER
} else if self.handler.is_view_camera() {
ConnType::VIEW_CAMERA
} else { } else {
ConnType::default() ConnType::default()
}; };
@@ -173,7 +179,7 @@ impl<T: InvokeUiSession> Remote<T> {
.set_connected(); .set_connected();
self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready
self.handler.update_direct(Some(direct)); self.handler.update_direct(Some(direct));
if conn_type == ConnType::DEFAULT_CONN { if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA {
self.handler self.handler
.set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default()));
} }
@@ -190,7 +196,8 @@ impl<T: InvokeUiSession> Remote<T> {
{ {
let is_conn_not_default = self.handler.is_file_transfer() let is_conn_not_default = self.handler.is_file_transfer()
|| self.handler.is_port_forward() || self.handler.is_port_forward()
|| self.handler.is_rdp(); || self.handler.is_rdp()
|| self.handler.is_view_camera();
if !is_conn_not_default { if !is_conn_not_default {
(self.client_conn_id, rx_clip_client_holder.0) = (self.client_conn_id, rx_clip_client_holder.0) =
clipboard::get_rx_cliprdr_client(&self.handler.get_id()); clipboard::get_rx_cliprdr_client(&self.handler.get_id());
@@ -330,12 +337,12 @@ impl<T: InvokeUiSession> Remote<T> {
.set_disconnected(round); .set_disconnected(round);
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
if _set_disconnected_ok { if !self.handler.is_view_camera() && _set_disconnected_ok {
Client::try_stop_clipboard(); Client::try_stop_clipboard();
} }
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
if _set_disconnected_ok { if !self.handler.is_view_camera() && _set_disconnected_ok {
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
} }
} }
@@ -1176,6 +1183,25 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
fn check_view_camera_support(&self, peer_version: &str, peer_platform: &str) -> bool {
if self.peer_info.support_view_camera {
return true;
}
if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.3.9")
&& (peer_platform == "Windows" || peer_platform == "Linux")
{
self.handler.msgbox(
"error",
"Download new version",
"upgrade_remote_rustdesk_client_to_{1.3.9}_tip",
"",
);
} else {
self.handler.on_error("view_camera_unsupported_tip");
}
return false;
}
async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
if let Ok(msg_in) = Message::parse_from_bytes(&data) { if let Ok(msg_in) = Message::parse_from_bytes(&data) {
match msg_in.union { match msg_in.union {
@@ -1230,10 +1256,19 @@ impl<T: InvokeUiSession> Remote<T> {
let peer_version = pi.version.clone(); let peer_version = pi.version.clone();
let peer_platform = pi.platform.clone(); let peer_platform = pi.platform.clone();
self.set_peer_info(&pi); self.set_peer_info(&pi);
if self.handler.is_view_camera() {
if !self.check_view_camera_support(&peer_version, &peer_platform) {
self.handler.lc.write().unwrap().handle_peer_info(&pi);
return false;
}
}
self.handler.handle_peer_info(pi); self.handler.handle_peer_info(pi);
#[cfg(all(target_os = "windows", not(feature = "flutter")))] #[cfg(all(target_os = "windows", not(feature = "flutter")))]
self.check_clipboard_file_context(); self.check_clipboard_file_context();
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { if !(self.handler.is_file_transfer()
|| self.handler.is_port_forward()
|| self.handler.is_view_camera())
{
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
let rx = Client::try_start_clipboard(None); let rx = Client::try_start_clipboard(None);
@@ -1532,6 +1567,9 @@ impl<T: InvokeUiSession> Remote<T> {
); );
} }
} }
Ok(Permission::Camera) => {
self.handler.set_permission("camera", p.enabled);
}
Ok(Permission::Restart) => { Ok(Permission::Restart) => {
self.handler.set_permission("restart", p.enabled); self.handler.set_permission("restart", p.enabled);
} }
@@ -1773,6 +1811,11 @@ impl<T: InvokeUiSession> Remote<T> {
.flatten() .flatten()
.unwrap_or_default() .unwrap_or_default()
.to_string(); .to_string();
self.peer_info.support_view_camera = platform_additions
.get("support_view_camera")
.map(|v| v.as_bool())
.flatten()
.unwrap_or(false);
} }
} }
+3 -2
View File
@@ -53,6 +53,7 @@ pub fn core_main() -> Option<Vec<String>> {
"--connect", "--connect",
"--play", "--play",
"--file-transfer", "--file-transfer",
"--view-camera",
"--port-forward", "--port-forward",
"--rdp", "--rdp",
] ]
@@ -99,7 +100,7 @@ pub fn core_main() -> Option<Vec<String>> {
} }
} }
#[cfg(windows)] #[cfg(windows)]
if args.contains(&"--connect".to_string()) { if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) {
hbb_common::platform::windows::start_cpu_performance_monitor(); hbb_common::platform::windows::start_cpu_performance_monitor();
} }
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
@@ -589,7 +590,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<Strin
let mut param_array = vec![]; let mut param_array = vec![];
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
match arg.as_str() { match arg.as_str() {
"--connect" | "--play" | "--file-transfer" | "--port-forward" | "--rdp" => { "--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" | "--rdp" => {
authority = Some((&arg.to_string()[2..]).to_owned()); authority = Some((&arg.to_string()[2..]).to_owned());
id = args.next(); id = args.next();
} }
+11 -1
View File
@@ -1149,8 +1149,14 @@ pub fn session_add_existed(
peer_id: String, peer_id: String,
session_id: SessionID, session_id: SessionID,
displays: Vec<i32>, displays: Vec<i32>,
is_view_camera: bool,
) -> ResultType<()> { ) -> ResultType<()> {
sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id, displays); let conn_type = if is_view_camera {
ConnType::VIEW_CAMERA
} else {
ConnType::DEFAULT_CONN
};
sessions::insert_peer_session_id(peer_id, conn_type, session_id, displays);
Ok(()) Ok(())
} }
@@ -1160,11 +1166,13 @@ pub fn session_add_existed(
/// ///
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
/// * `is_file_transfer` - If the session is used for file transfer. /// * `is_file_transfer` - If the session is used for file transfer.
/// * `is_view_camera` - If the session is used for view camera.
/// * `is_port_forward` - If the session is used for port forward. /// * `is_port_forward` - If the session is used for port forward.
pub fn session_add( pub fn session_add(
session_id: &SessionID, session_id: &SessionID,
id: &str, id: &str,
is_file_transfer: bool, is_file_transfer: bool,
is_view_camera: bool,
is_port_forward: bool, is_port_forward: bool,
is_rdp: bool, is_rdp: bool,
switch_uuid: &str, switch_uuid: &str,
@@ -1175,6 +1183,8 @@ pub fn session_add(
) -> ResultType<FlutterSession> { ) -> ResultType<FlutterSession> {
let conn_type = if is_file_transfer { let conn_type = if is_file_transfer {
ConnType::FILE_TRANSFER ConnType::FILE_TRANSFER
} else if is_view_camera {
ConnType::VIEW_CAMERA
} else if is_port_forward { } else if is_port_forward {
if is_rdp { if is_rdp {
ConnType::RDP ConnType::RDP
+17 -3
View File
@@ -92,16 +92,28 @@ pub fn host_stop_system_key_propagate(_stopped: bool) {
} }
// This function is only used to count the number of control sessions. // This function is only used to count the number of control sessions.
pub fn peer_get_default_sessions_count(id: String) -> SyncReturn<usize> { pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn<usize> {
SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN)) let conn_type = if conn_type == ConnType::VIEW_CAMERA as i32 {
ConnType::VIEW_CAMERA
} else if conn_type == ConnType::FILE_TRANSFER as i32 {
ConnType::FILE_TRANSFER
} else if conn_type == ConnType::PORT_FORWARD as i32 {
ConnType::PORT_FORWARD
} else if conn_type == ConnType::RDP as i32 {
ConnType::RDP
} else {
ConnType::DEFAULT_CONN
};
SyncReturn(sessions::get_session_count(id, conn_type))
} }
pub fn session_add_existed_sync( pub fn session_add_existed_sync(
id: String, id: String,
session_id: SessionID, session_id: SessionID,
displays: Vec<i32>, displays: Vec<i32>,
is_view_camera: bool,
) -> SyncReturn<String> { ) -> SyncReturn<String> {
if let Err(e) = session_add_existed(id.clone(), session_id, displays) { if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) {
SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
} else { } else {
SyncReturn("".to_owned()) SyncReturn("".to_owned())
@@ -112,6 +124,7 @@ pub fn session_add_sync(
session_id: SessionID, session_id: SessionID,
id: String, id: String,
is_file_transfer: bool, is_file_transfer: bool,
is_view_camera: bool,
is_port_forward: bool, is_port_forward: bool,
is_rdp: bool, is_rdp: bool,
switch_uuid: String, switch_uuid: String,
@@ -124,6 +137,7 @@ pub fn session_add_sync(
&session_id, &session_id,
&id, &id,
is_file_transfer, is_file_transfer,
is_view_camera,
is_port_forward, is_port_forward,
is_rdp, is_rdp,
&switch_uuid, &switch_uuid,
+2 -1
View File
@@ -188,6 +188,7 @@ pub enum Data {
Login { Login {
id: i32, id: i32,
is_file_transfer: bool, is_file_transfer: bool,
is_view_camera: bool,
peer_id: String, peer_id: String,
name: String, name: String,
authorized: bool, authorized: bool,
@@ -1280,6 +1281,6 @@ mod test {
#[test] #[test]
fn verify_ffi_enum_data_size() { fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>()); println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() < 96); assert!(std::mem::size_of::<Data>() <= 96);
} }
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "عرض الكاميرا"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Прагляд камеры"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Преглед на камерата"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Sense etiquetar"), ("Untagged", "Sense etiquetar"),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", "Dispositius accessibles"), ("Accessible devices", "Dispositius accessibles"),
("View camera", "Mostra la càmera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "无标签"), ("Untagged", "无标签"),
("new-version-of-{}-tip", "{} 版本更新"), ("new-version-of-{}-tip", "{} 版本更新"),
("Accessible devices", "可访问的设备"), ("Accessible devices", "可访问的设备"),
("View camera", "查看摄像头"),
("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"),
("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"),
("Enable camera", "允许查看摄像头"),
("No cameras", "没有摄像头"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Zobrazit kameru"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Se kamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Unmarkiert"), ("Untagged", "Unmarkiert"),
("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"),
("Accessible devices", "Erreichbare Geräte"), ("Accessible devices", "Erreichbare Geräte"),
("View camera", "Kamera anzeigen"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Χωρίς ετικέτα"), ("Untagged", "Χωρίς ετικέτα"),
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
("Accessible devices", "Προσβάσιμες συσκευές"), ("Accessible devices", "Προσβάσιμες συσκευές"),
("View camera", "Προβολή κάμερας"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+3
View File
@@ -237,5 +237,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."),
("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (<id>@<server_address>?key=<key_value>), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"<id>@public\", the key is not needed for public server."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (<id>@<server_address>?key=<key_value>), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"<id>@public\", the key is not needed for public server."),
("new-version-of-{}-tip", "There is a new version of {} available"), ("new-version-of-{}-tip", "There is a new version of {} available"),
("View camera", "View camera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"),
("view_camera_unsupported_tip", "The remote device does not support viewing the camera."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Rigardi kameron"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Sin itiquetar"), ("Untagged", "Sin itiquetar"),
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Ver cámara"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+6 -1
View File
@@ -600,7 +600,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Everyone", "Igaüks"), ("Everyone", "Igaüks"),
("ab_web_console_tip", "Rohkem leiad veebikonsoolist"), ("ab_web_console_tip", "Rohkem leiad veebikonsoolist"),
("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."), ("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."),
("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."),
("Follow remote cursor", "Jälgi kaugkursorit"), ("Follow remote cursor", "Jälgi kaugkursorit"),
("Follow remote window focus", "Jälgi kaugakna fookust"), ("Follow remote window focus", "Jälgi kaugakna fookust"),
("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."), ("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."),
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Sildistamata"), ("Untagged", "Sildistamata"),
("new-version-of-{}-tip", "Saadaval on {} uus versioon"), ("new-version-of-{}-tip", "Saadaval on {} uus versioon"),
("Accessible devices", "Ligipääsetavad seadmed"), ("Accessible devices", "Ligipääsetavad seadmed"),
("View camera", "Vaata kaamerat"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Ikusi kamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "نمایش دوربین"),
("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً مشتری RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Sans étiquette"), ("Untagged", "Sans étiquette"),
("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"),
("Accessible devices", "Appareils accessibles"), ("Accessible devices", "Appareils accessibles"),
("View camera", "Voir la caméra"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à jour le client RustDesk avec la version {} ou une version plus récente sur l'appareil distant"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Pregled kamere"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Címkézetlen"), ("Untagged", "Címkézetlen"),
("new-version-of-{}-tip", "A(z) {} új verziója"), ("new-version-of-{}-tip", "A(z) {} új verziója"),
("Accessible devices", "Hozzáférhető eszközök"), ("Accessible devices", "Hozzáférhető eszközök"),
("View camera", "Kamera megtekintése"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Kérjük, frissítse a RustDesk kliens {} vagy újabb verziójára a távoli oldalon!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Lihat Kamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Silakan perbarui klien RustDesk ke versi {} atau lebih baru di sisi remote!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Senza tag"), ("Untagged", "Senza tag"),
("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"),
("Accessible devices", "Dispositivi accessibili"), ("Accessible devices", "Dispositivi accessibili"),
("View camera", "Visualizza telecamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "カメラを表示"),
("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "태그 없음"), ("Untagged", "태그 없음"),
("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "카메라 보기"),
("upgrade_remote_rustdesk_client_to_{}_tip", "원격 측의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Камераны Көру"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Peržiūrėti kamerą"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Neatzīmēts"), ("Untagged", "Neatzīmēts"),
("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"),
("Accessible devices", "Pieejamas ierīces"), ("Accessible devices", "Pieejamas ierīces"),
("View camera", "Skatīt kameru"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Vis kamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Ongemarkeerd"), ("Untagged", "Ongemarkeerd"),
("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"),
("Accessible devices", "Toegankelijke apparaten"), ("Accessible devices", "Toegankelijke apparaten"),
("View camera", "Camera bekijken"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Bez etykiety"), ("Untagged", "Bez etykiety"),
("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"),
("Accessible devices", "Dostępne urządzenia"), ("Accessible devices", "Dostępne urządzenia"),
("View camera", "Podgląd kamery"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Ver câmara"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Visualizar Câmera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Vezi camera"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Без метки"), ("Untagged", "Без метки"),
("new-version-of-{}-tip", "Доступна новая версия {}"), ("new-version-of-{}-tip", "Доступна новая версия {}"),
("Accessible devices", "Доступные устройства"), ("Accessible devices", "Доступные устройства"),
("View camera", "Просмотр камеры"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удаленной стороне!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Chene tag"), ("Untagged", "Chene tag"),
("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"),
("Accessible devices", "Dispositivos atzessìbiles"), ("Accessible devices", "Dispositivos atzessìbiles"),
("View camera", "Mustra càmera"),
("upgrade_remote_rustdesk_client_to_{}_tip", "¡Actualice el cliente RustDesk a la versión {} o más reciente en el lado remoto!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Zobraziť kameru"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Neoznačeno"), ("Untagged", "Neoznačeno"),
("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Pogled kamere"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Pregled kamere"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Visa kamera"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "ดูกล้อง"),
("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Kamerayı görüntüle"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "無標籤"), ("Untagged", "無標籤"),
("new-version-of-{}-tip", "有新版本的 {} 可用"), ("new-version-of-{}-tip", "有新版本的 {} 可用"),
("Accessible devices", "可存取的裝置"), ("Accessible devices", "可存取的裝置"),
("View camera", "檢視相機"),
("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 用戶端升級到 {} 或更新版本!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", "Без міток"), ("Untagged", "Без міток"),
("new-version-of-{}-tip", "Доступна нова версія {}"), ("new-version-of-{}-tip", "Доступна нова версія {}"),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Перегляд камери"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+5
View File
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""), ("Accessible devices", ""),
("View camera", "Xem camera"),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("view_camera_unsupported_tip", ""),
("Enable camera", ""),
("No cameras", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }
+48 -12
View File
@@ -24,9 +24,11 @@ use hbb_common::{
sodiumoxide::crypto::{box_, sign}, sodiumoxide::crypto::{box_, sign},
timeout, tokio, ResultType, Stream, timeout, tokio, ResultType, Stream,
}; };
use scrap::camera;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use service::ServiceTmpl; use service::ServiceTmpl;
use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; use service::{EmptyExtraFieldService, GenericService, Service, Subscriber};
use video_service::VideoSource;
use crate::ipc::Data; use crate::ipc::Data;
@@ -76,7 +78,6 @@ const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3;
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CHILD_PROCESS: Childs = Default::default();
pub static ref CONN_COUNT: Arc<Mutex<usize>> = Default::default();
// A client server used to provide local services(audio, video, clipboard, etc.) // A client server used to provide local services(audio, video, clipboard, etc.)
// for all initiative connections. // for all initiative connections.
// //
@@ -279,22 +280,53 @@ async fn create_relay_connection_(
impl Server { impl Server {
fn is_video_service_name(name: &str) -> bool { fn is_video_service_name(name: &str) -> bool {
name.starts_with(video_service::NAME) name.starts_with(VideoSource::Monitor.service_name_prefix())
|| name.starts_with(VideoSource::Camera.service_name_prefix())
}
pub fn try_add_primary_camera_service(&mut self) {
if !camera::primary_camera_exists() {
return;
}
let primary_camera_name =
video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX);
if !self.contains(&primary_camera_name) {
self.add_service(Box::new(video_service::new(
VideoSource::Camera,
camera::PRIMARY_CAMERA_IDX,
)));
}
} }
pub fn try_add_primay_video_service(&mut self) { pub fn try_add_primay_video_service(&mut self) {
let primary_video_service_name = let primary_video_service_name = video_service::get_service_name(
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); VideoSource::Monitor,
*display_service::PRIMARY_DISPLAY_IDX,
);
if !self.contains(&primary_video_service_name) { if !self.contains(&primary_video_service_name) {
self.add_service(Box::new(video_service::new( self.add_service(Box::new(video_service::new(
VideoSource::Monitor,
*display_service::PRIMARY_DISPLAY_IDX, *display_service::PRIMARY_DISPLAY_IDX,
))); )));
} }
} }
pub fn add_camera_connection(&mut self, conn: ConnInner) {
if camera::primary_camera_exists() {
let primary_camera_name =
video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX);
if let Some(s) = self.services.get(&primary_camera_name) {
s.on_subscribe(conn.clone());
}
}
self.connections.insert(conn.id(), conn);
}
pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) {
let primary_video_service_name = let primary_video_service_name = video_service::get_service_name(
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); VideoSource::Monitor,
*display_service::PRIMARY_DISPLAY_IDX,
);
for s in self.services.values() { for s in self.services.values() {
let name = s.name(); let name = s.name();
if Self::is_video_service_name(&name) && name != primary_video_service_name { if Self::is_video_service_name(&name) && name != primary_video_service_name {
@@ -307,7 +339,6 @@ impl Server {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
self.update_enable_retina(); self.update_enable_retina();
self.connections.insert(conn.id(), conn); self.connections.insert(conn.id(), conn);
*CONN_COUNT.lock().unwrap() = self.connections.len();
} }
pub fn remove_connection(&mut self, conn: &ConnInner) { pub fn remove_connection(&mut self, conn: &ConnInner) {
@@ -315,7 +346,6 @@ impl Server {
s.on_unsubscribe(conn.id()); s.on_unsubscribe(conn.id());
} }
self.connections.remove(&conn.id()); self.connections.remove(&conn.id());
*CONN_COUNT.lock().unwrap() = self.connections.len();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
self.update_enable_retina(); self.update_enable_retina();
} }
@@ -361,10 +391,15 @@ impl Server {
self.id_count self.id_count
} }
pub fn set_video_service_opt(&self, display: Option<usize>, opt: &str, value: &str) { pub fn set_video_service_opt(
&self,
display: Option<(VideoSource, usize)>,
opt: &str,
value: &str,
) {
for (k, v) in self.services.iter() { for (k, v) in self.services.iter() {
if let Some(display) = display { if let Some((source, display)) = display {
if k != &video_service::get_service_name(display) { if k != &video_service::get_service_name(source, display) {
continue; continue;
} }
} }
@@ -392,13 +427,14 @@ impl Server {
fn capture_displays( fn capture_displays(
&mut self, &mut self,
conn: ConnInner, conn: ConnInner,
source: VideoSource,
displays: &[usize], displays: &[usize],
include: bool, include: bool,
exclude: bool, exclude: bool,
) { ) {
let displays = displays let displays = displays
.iter() .iter()
.map(|d| video_service::get_service_name(*d)) .map(|d| video_service::get_service_name(source, *d))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let keys = self.services.keys().cloned().collect::<Vec<_>>(); let keys = self.services.keys().cloned().collect::<Vec<_>>();
for name in keys.iter() { for name in keys.iter() {
+173 -36
View File
@@ -44,6 +44,7 @@ use hbb_common::{
}; };
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; use scrap::android::{call_main_service_key_event, call_main_service_pointer_input};
use scrap::camera;
use serde_derive::Serialize; use serde_derive::Serialize;
use serde_json::{json, value::Value}; use serde_json::{json, value::Value};
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -167,6 +168,7 @@ pub enum AuthConnType {
Remote, Remote,
FileTransfer, FileTransfer,
PortForward, PortForward,
ViewCamera,
} }
pub struct Connection { pub struct Connection {
@@ -179,6 +181,7 @@ pub struct Connection {
timer: crate::RustDeskInterval, timer: crate::RustDeskInterval,
file_timer: crate::RustDeskInterval, file_timer: crate::RustDeskInterval,
file_transfer: Option<(String, bool)>, file_transfer: Option<(String, bool)>,
view_camera: bool,
port_forward_socket: Option<Framed<TcpStream, BytesCodec>>, port_forward_socket: Option<Framed<TcpStream, BytesCodec>>,
port_forward_address: String, port_forward_address: String,
tx_to_cm: mpsc::UnboundedSender<ipc::Data>, tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
@@ -222,6 +225,7 @@ pub struct Connection {
portable: PortableState, portable: PortableState,
from_switch: bool, from_switch: bool,
voice_call_request_timestamp: Option<NonZeroI64>, voice_call_request_timestamp: Option<NonZeroI64>,
voice_calling: bool,
options_in_login: Option<OptionMessage>, options_in_login: Option<OptionMessage>,
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
pressed_modifiers: HashSet<rdev::Key>, pressed_modifiers: HashSet<rdev::Key>,
@@ -331,6 +335,7 @@ impl Connection {
timer: crate::rustdesk_interval(time::interval(SEC30)), timer: crate::rustdesk_interval(time::interval(SEC30)),
file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_timer: crate::rustdesk_interval(time::interval(SEC30)),
file_transfer: None, file_transfer: None,
view_camera: false,
port_forward_socket: None, port_forward_socket: None,
port_forward_address: "".to_owned(), port_forward_address: "".to_owned(),
tx_to_cm, tx_to_cm,
@@ -369,6 +374,7 @@ impl Connection {
from_switch: false, from_switch: false,
audio_sender: None, audio_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
voice_calling: false,
options_in_login: None, options_in_login: None,
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
pressed_modifiers: Default::default(), pressed_modifiers: Default::default(),
@@ -533,9 +539,17 @@ impl Connection {
conn.send_permission(Permission::Audio, enabled).await; conn.send_permission(Permission::Audio, enabled).await;
if conn.authorized { if conn.authorized {
if let Some(s) = conn.server.upgrade() { if let Some(s) = conn.server.upgrade() {
s.write().unwrap().subscribe( if conn.is_authed_view_camera_conn() {
super::audio_service::NAME, if conn.voice_calling || !conn.audio_enabled() {
conn.inner.clone(), conn.audio_enabled()); s.write().unwrap().subscribe(
super::audio_service::NAME,
conn.inner.clone(), conn.audio_enabled());
}
} else {
s.write().unwrap().subscribe(
super::audio_service::NAME,
conn.inner.clone(), conn.audio_enabled());
}
} }
} }
} else if &name == "file" { } else if &name == "file" {
@@ -774,7 +788,7 @@ impl Connection {
}); });
conn.send(msg_out.into()).await; conn.send(msg_out.into()).await;
} }
if conn.is_authed_remote_conn() { if conn.is_authed_remote_conn() || conn.view_camera {
if let Some(last_test_delay) = conn.last_test_delay { if let Some(last_test_delay) = conn.last_test_delay {
video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis()); video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis());
} }
@@ -1189,6 +1203,8 @@ impl Connection {
(1, AuthConnType::FileTransfer) (1, AuthConnType::FileTransfer)
} else if self.port_forward_socket.is_some() { } else if self.port_forward_socket.is_some() {
(2, AuthConnType::PortForward) (2, AuthConnType::PortForward)
} else if self.view_camera {
(3, AuthConnType::ViewCamera)
} else { } else {
(0, AuthConnType::Remote) (0, AuthConnType::Remote)
}; };
@@ -1277,6 +1293,11 @@ impl Connection {
platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard));
} }
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
platform_additions.insert("support_view_camera".into(), json!(true));
}
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
if !platform_additions.is_empty() { if !platform_additions.is_empty() {
pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into());
@@ -1290,7 +1311,8 @@ impl Connection {
return; return;
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() && !self.view_camera
{
let mut msg = "".to_string(); let mut msg = "".to_string();
if crate::platform::linux::is_login_screen_wayland() { if crate::platform::linux::is_login_screen_wayland() {
msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned()
@@ -1347,6 +1369,29 @@ impl Connection {
self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm);
if self.file_transfer.is_some() { if self.file_transfer.is_some() {
res.set_peer_info(pi); res.set_peer_info(pi);
} else if self.view_camera {
let supported_encoding = scrap::codec::Encoder::supported_encoding();
self.last_supported_encoding = Some(supported_encoding.clone());
log::info!("peer info supported_encoding: {:?}", supported_encoding);
pi.encoding = Some(supported_encoding).into();
pi.displays = camera::Cameras::all_info().unwrap_or(Vec::new());
pi.current_display = camera::PRIMARY_CAMERA_IDX as _;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
pi.resolutions = Some(SupportedResolutions {
resolutions: camera::Cameras::get_camera_resolution(
pi.current_display as usize,
)
.ok()
.into_iter()
.collect(),
..Default::default()
})
.into();
}
res.set_peer_info(pi);
self.update_codec_on_login();
} else { } else {
let supported_encoding = scrap::codec::Encoder::supported_encoding(); let supported_encoding = scrap::codec::Encoder::supported_encoding();
self.last_supported_encoding = Some(supported_encoding.clone()); self.last_supported_encoding = Some(supported_encoding.clone());
@@ -1414,15 +1459,31 @@ impl Connection {
} else { } else {
self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); self.delayed_read_dir = Some((dir.to_owned(), show_hidden));
} }
} else if self.view_camera {
if !wait_session_id_confirm {
self.try_sub_camera_displays();
}
self.keyboard = false;
self.send_permission(Permission::Keyboard, false).await;
} else if sub_service { } else if sub_service {
if !wait_session_id_confirm { if !wait_session_id_confirm {
self.try_sub_services(); self.try_sub_monitor_services();
} }
} }
} }
fn try_sub_services(&mut self) { fn try_sub_camera_displays(&mut self) {
let is_remote = self.file_transfer.is_none() && self.port_forward_socket.is_none(); if let Some(s) = self.server.upgrade() {
let mut s = s.write().unwrap();
s.try_add_primary_camera_service();
s.add_camera_connection(self.inner.clone());
}
}
fn try_sub_monitor_services(&mut self) {
let is_remote =
self.file_transfer.is_none() && self.port_forward_socket.is_none() && !self.view_camera;
if is_remote && !self.services_subed { if is_remote && !self.services_subed {
self.services_subed = true; self.services_subed = true;
if let Some(s) = self.server.upgrade() { if let Some(s) = self.server.upgrade() {
@@ -1466,7 +1527,7 @@ impl Connection {
if let Some(current_sid) = crate::platform::get_current_process_session_id() { if let Some(current_sid) = crate::platform::get_current_process_session_id() {
if crate::platform::is_installed() if crate::platform::is_installed()
&& crate::platform::is_share_rdp() && crate::platform::is_share_rdp()
&& raii::AuthedConnID::remote_and_file_conn_count() == 1 && raii::AuthedConnID::non_port_forward_conn_count() == 1
&& sessions.len() > 1 && sessions.len() > 1
&& sessions.iter().any(|e| e.sid == current_sid) && sessions.iter().any(|e| e.sid == current_sid)
&& get_version_number(&self.lr.version) >= get_version_number("1.2.4") && get_version_number(&self.lr.version) >= get_version_number("1.2.4")
@@ -1539,6 +1600,7 @@ impl Connection {
self.send_to_cm(ipc::Data::Login { self.send_to_cm(ipc::Data::Login {
id: self.inner.id(), id: self.inner.id(),
is_file_transfer: self.file_transfer.is_some(), is_file_transfer: self.file_transfer.is_some(),
is_view_camera: self.view_camera,
port_forward: self.port_forward_address.clone(), port_forward: self.port_forward_address.clone(),
peer_id, peer_id,
name, name,
@@ -1781,6 +1843,15 @@ impl Connection {
} }
self.file_transfer = Some((ft.dir, ft.show_hidden)); self.file_transfer = Some((ft.dir, ft.show_hidden));
} }
Some(login_request::Union::ViewCamera(_vc)) => {
if !Connection::permission(keys::OPTION_ENABLE_CAMERA) {
self.send_login_error("No permission of viewing camera")
.await;
sleep(1.).await;
return false;
}
self.view_camera = true;
}
Some(login_request::Union::PortForward(mut pf)) => { Some(login_request::Union::PortForward(mut pf)) => {
if !Connection::permission("enable-tunnel") { if !Connection::permission("enable-tunnel") {
self.send_login_error("No permission of IP tunneling").await; self.send_login_error("No permission of IP tunneling").await;
@@ -1987,6 +2058,9 @@ impl Connection {
match msg.union { match msg.union {
#[allow(unused_mut)] #[allow(unused_mut)]
Some(message::Union::MouseEvent(mut me)) => { Some(message::Union::MouseEvent(mut me)) => {
if self.is_authed_view_camera_conn() {
return true;
}
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) {
log::debug!("call_main_service_pointer_input fail:{}", e); log::debug!("call_main_service_pointer_input fail:{}", e);
@@ -2005,6 +2079,9 @@ impl Connection {
self.update_auto_disconnect_timer(); self.update_auto_disconnect_timer();
} }
Some(message::Union::PointerDeviceEvent(pde)) => { Some(message::Union::PointerDeviceEvent(pde)) => {
if self.is_authed_view_camera_conn() {
return true;
}
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = match pde.union { if let Err(e) = match pde.union {
Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union {
@@ -2044,6 +2121,9 @@ impl Connection {
Some(message::Union::KeyEvent(..)) => {} Some(message::Union::KeyEvent(..)) => {}
#[cfg(any(target_os = "android"))] #[cfg(any(target_os = "android"))]
Some(message::Union::KeyEvent(mut me)) => { Some(message::Union::KeyEvent(mut me)) => {
if self.is_authed_view_camera_conn() {
return true;
}
let key = match me.mode.enum_value() { let key = match me.mode.enum_value() {
Ok(KeyboardMode::Map) => { Ok(KeyboardMode::Map) => {
Some(crate::keyboard::keycode_to_rdev_key(me.chr())) Some(crate::keyboard::keycode_to_rdev_key(me.chr()))
@@ -2096,6 +2176,9 @@ impl Connection {
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(message::Union::KeyEvent(me)) => { Some(message::Union::KeyEvent(me)) => {
if self.is_authed_view_camera_conn() {
return true;
}
if self.peer_keyboard_enabled() { if self.peer_keyboard_enabled() {
if is_enter(&me) { if is_enter(&me) {
CLICK_TIME.store(get_time(), Ordering::SeqCst); CLICK_TIME.store(get_time(), Ordering::SeqCst);
@@ -2592,7 +2675,7 @@ impl Connection {
let sessions = crate::platform::get_available_sessions(false); let sessions = crate::platform::get_available_sessions(false);
if crate::platform::is_installed() if crate::platform::is_installed()
&& crate::platform::is_share_rdp() && crate::platform::is_share_rdp()
&& raii::AuthedConnID::remote_and_file_conn_count() == 1 && raii::AuthedConnID::non_port_forward_conn_count() == 1
&& sessions.len() > 1 && sessions.len() > 1
&& current_process_sid != sid && current_process_sid != sid
&& sessions.iter().any(|e| e.sid == sid) && sessions.iter().any(|e| e.sid == sid)
@@ -2606,15 +2689,19 @@ impl Connection {
if let Some((dir, show_hidden)) = self.delayed_read_dir.take() { if let Some((dir, show_hidden)) = self.delayed_read_dir.take() {
self.read_dir(&dir, show_hidden); self.read_dir(&dir, show_hidden);
} }
} else if self.view_camera {
self.try_sub_camera_displays();
} else { } else {
self.try_sub_services(); self.try_sub_monitor_services();
} }
} }
} }
Some(misc::Union::MessageQuery(mq)) => { Some(misc::Union::MessageQuery(mq)) => {
if let Some(msg_out) = if let Some(msg_out) = video_service::make_display_changed_msg(
video_service::make_display_changed_msg(mq.switch_display as _, None) mq.switch_display as _,
{ None,
self.video_source(),
) {
self.send(msg_out).await; self.send(msg_out).await;
} }
} }
@@ -2713,7 +2800,7 @@ impl Connection {
video_service::refresh(); video_service::refresh();
self.server.upgrade().map(|s| { self.server.upgrade().map(|s| {
s.read().unwrap().set_video_service_opt( s.read().unwrap().set_video_service_opt(
display, display.map(|d| (self.video_source(), d)),
video_service::OPTION_REFRESH, video_service::OPTION_REFRESH,
super::service::SERVICE_OPTION_VALUE_TRUE, super::service::SERVICE_OPTION_VALUE_TRUE,
); );
@@ -2743,19 +2830,33 @@ impl Connection {
// 1. For compatibility with old versions ( < 1.2.4 ). // 1. For compatibility with old versions ( < 1.2.4 ).
// 2. Sciter version. // 2. Sciter version.
// 3. Update `SupportedResolutions`. // 3. Update `SupportedResolutions`.
if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { if let Some(msg_out) =
video_service::make_display_changed_msg(self.display_idx, None, self.video_source())
{
self.send(msg_out).await; self.send(msg_out).await;
} }
} }
} }
fn video_source(&self) -> VideoSource {
if self.view_camera {
VideoSource::Camera
} else {
VideoSource::Monitor
}
}
fn switch_display_to(&mut self, display_idx: usize, server: Arc<RwLock<Server>>) { 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 new_service_name = video_service::get_service_name(self.video_source(), display_idx);
let old_service_name = video_service::get_service_name(self.display_idx); let old_service_name =
video_service::get_service_name(self.video_source(), self.display_idx);
let mut lock = server.write().unwrap(); let mut lock = server.write().unwrap();
if display_idx != *display_service::PRIMARY_DISPLAY_IDX { if display_idx != *display_service::PRIMARY_DISPLAY_IDX {
if !lock.contains(&new_service_name) { if !lock.contains(&new_service_name) {
lock.add_service(Box::new(video_service::new(display_idx))); lock.add_service(Box::new(video_service::new(
self.video_source(),
display_idx,
)));
} }
} }
// For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately.
@@ -2790,26 +2891,27 @@ impl Connection {
} }
async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) {
let video_source = self.video_source();
if let Some(sever) = self.server.upgrade() { if let Some(sever) = self.server.upgrade() {
let mut lock = sever.write().unwrap(); let mut lock = sever.write().unwrap();
for display in add.iter() { for display in add.iter() {
let service_name = video_service::get_service_name(*display); let service_name = video_service::get_service_name(video_source, *display);
if !lock.contains(&service_name) { if !lock.contains(&service_name) {
lock.add_service(Box::new(video_service::new(*display))); lock.add_service(Box::new(video_service::new(video_source, *display)));
} }
} }
for display in set.iter() { for display in set.iter() {
let service_name = video_service::get_service_name(*display); let service_name = video_service::get_service_name(video_source, *display);
if !lock.contains(&service_name) { if !lock.contains(&service_name) {
lock.add_service(Box::new(video_service::new(*display))); lock.add_service(Box::new(video_service::new(video_source, *display)));
} }
} }
if !add.is_empty() { if !add.is_empty() {
lock.capture_displays(self.inner.clone(), add, true, false); lock.capture_displays(self.inner.clone(), video_source, add, true, false);
} else if !sub.is_empty() { } else if !sub.is_empty() {
lock.capture_displays(self.inner.clone(), sub, false, true); lock.capture_displays(self.inner.clone(), video_source, sub, false, true);
} else { } else {
lock.capture_displays(self.inner.clone(), set, true, true); lock.capture_displays(self.inner.clone(), video_source, set, true, true);
} }
self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1;
if self.follow_remote_window { if self.follow_remote_window {
@@ -2931,6 +3033,16 @@ impl Connection {
self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
} }
self.send(msg).await; self.send(msg).await;
self.voice_calling = accepted;
if self.is_authed_view_camera_conn() {
if let Some(s) = self.server.upgrade() {
s.write().unwrap().subscribe(
super::audio_service::NAME,
self.inner.clone(),
self.audio_enabled() && accepted,
);
}
}
} else { } else {
log::warn!("Possible a voice call attack."); log::warn!("Possible a voice call attack.");
} }
@@ -2940,6 +3052,14 @@ impl Connection {
crate::audio_service::set_voice_call_input_device(None, true); crate::audio_service::set_voice_call_input_device(None, true);
// Notify the connection manager that the voice call has been closed. // Notify the connection manager that the voice call has been closed.
self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
self.voice_calling = false;
if self.is_authed_view_camera_conn() {
if let Some(s) = self.server.upgrade() {
s.write()
.unwrap()
.subscribe(super::audio_service::NAME, self.inner.clone(), false);
}
}
} }
async fn update_options(&mut self, o: &OptionMessage) { async fn update_options(&mut self, o: &OptionMessage) {
@@ -3016,11 +3136,21 @@ impl Connection {
if q != BoolOption::NotSet { if q != BoolOption::NotSet {
self.disable_audio = q == BoolOption::Yes; self.disable_audio = q == BoolOption::Yes;
if let Some(s) = self.server.upgrade() { if let Some(s) = self.server.upgrade() {
s.write().unwrap().subscribe( if self.is_authed_view_camera_conn() {
super::audio_service::NAME, if self.voice_calling || !self.audio_enabled() {
self.inner.clone(), s.write().unwrap().subscribe(
self.audio_enabled(), super::audio_service::NAME,
); self.inner.clone(),
self.audio_enabled(),
);
}
} else {
s.write().unwrap().subscribe(
super::audio_service::NAME,
self.inner.clone(),
self.audio_enabled(),
);
}
} }
} }
} }
@@ -3316,6 +3446,7 @@ impl Connection {
fn portable_check(&mut self) { fn portable_check(&mut self) {
if self.portable.is_installed if self.portable.is_installed
|| self.file_transfer.is_some() || self.file_transfer.is_some()
|| self.view_camera
|| self.port_forward_socket.is_some() || self.port_forward_socket.is_some()
|| !self.keyboard || !self.keyboard
{ {
@@ -3463,6 +3594,13 @@ impl Connection {
false false
} }
fn is_authed_view_camera_conn(&self) -> bool {
if let Some(id) = self.authed_conn_id.as_ref() {
return id.conn_type() == AuthConnType::ViewCamera;
}
false
}
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
let is_stopping_allowed = clip.is_stopping_allowed(); let is_stopping_allowed = clip.is_stopping_allowed();
@@ -3966,7 +4104,6 @@ impl Retina {
} }
mod raii { mod raii {
// CONN_COUNT: remote connection count in fact
// ALIVE_CONNS: all connections, including unauthorized connections // ALIVE_CONNS: all connections, including unauthorized connections
// AUTHED_CONNS: all authorized connections // AUTHED_CONNS: all authorized connections
@@ -4001,7 +4138,7 @@ mod raii {
_ONCE.call_once(|| { _ONCE.call_once(|| {
shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); shutdown_hooks::add_shutdown_hook(connection_shutdown_hook);
}); });
if conn_type == AuthConnType::Remote { if conn_type == AuthConnType::Remote || conn_type == AuthConnType::ViewCamera {
video_service::VIDEO_QOS video_service::VIDEO_QOS
.lock() .lock()
.unwrap() .unwrap()
@@ -4024,12 +4161,12 @@ mod raii {
.send((conn_count, remote_count))); .send((conn_count, remote_count)));
} }
pub fn remote_and_file_conn_count() -> usize { pub fn non_port_forward_conn_count() -> usize {
AUTHED_CONNS AUTHED_CONNS
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) .filter(|c| c.1 != AuthConnType::PortForward)
.count() .count()
} }
@@ -4112,7 +4249,7 @@ mod raii {
impl Drop for AuthedConnID { impl Drop for AuthedConnID {
fn drop(&mut self) { fn drop(&mut self) {
if self.1 == AuthConnType::Remote { if self.1 == AuthConnType::Remote || self.1 == AuthConnType::ViewCamera {
scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0));
video_service::VIDEO_QOS video_service::VIDEO_QOS
.lock() .lock()
+1
View File
@@ -404,6 +404,7 @@ fn no_displays(displays: &Vec<Display>) -> bool {
} }
} }
#[inline] #[inline]
#[cfg(not(windows))] #[cfg(not(windows))]
pub fn try_get_displays() -> ResultType<Vec<Display>> { pub fn try_get_displays() -> ResultType<Vec<Display>> {
+7 -2
View File
@@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option<thread::JoinHandle<()>> {
} }
pub fn try_stop_record_cursor_pos() { pub fn try_stop_record_cursor_pos() {
let count_lock = CONN_COUNT.lock().unwrap(); let remote_count = AUTHED_CONNS
if *count_lock > 0 { .lock()
.unwrap()
.iter()
.filter(|c| c.1 == AuthConnType::Remote)
.count();
if remote_count > 0 {
return; return;
} }
RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst);
+8 -3
View File
@@ -717,7 +717,7 @@ pub mod client {
} }
let frame_ptr = base.add(ADDR_CAPTURE_FRAME); let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); let data = slice::from_raw_parts(frame_ptr, (*frame_info).length);
Ok(Frame::PixelBuffer(PixelBuffer::new( Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
data, data,
self.width, self.width,
self.height, self.height,
@@ -808,8 +808,13 @@ pub mod client {
}, },
ConnCount(None) => { ConnCount(None) => {
if !quick_support { if !quick_support {
let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); let remote_count = crate::server::AUTHED_CONNS
stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); .lock()
.unwrap()
.iter()
.filter(|c| c.1 == crate::server::AuthConnType::Remote)
.count();
stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok();
} }
}, },
WillClose => { WillClose => {
+10 -9
View File
@@ -106,7 +106,7 @@ pub struct VideoQoS {
fps: u32, fps: u32,
ratio: f32, ratio: f32,
users: HashMap<i32, UserData>, users: HashMap<i32, UserData>,
displays: HashMap<usize, DisplayData>, displays: HashMap<String, DisplayData>,
bitrate_store: u32, bitrate_store: u32,
adjust_ratio_instant: Instant, adjust_ratio_instant: Instant,
abr_config: bool, abr_config: bool,
@@ -168,8 +168,8 @@ impl VideoQoS {
self.users.iter().any(|u| u.1.record) self.users.iter().any(|u| u.1.record)
} }
pub fn set_support_changing_quality(&mut self, display_idx: usize, support: bool) { pub fn set_support_changing_quality(&mut self, video_service_name: &str, support: bool) {
if let Some(display) = self.displays.get_mut(&display_idx) { if let Some(display) = self.displays.get_mut(video_service_name) {
display.support_changing_quality = support; display.support_changing_quality = support;
} }
} }
@@ -346,16 +346,17 @@ impl VideoQoS {
// Common adjust functions // Common adjust functions
impl VideoQoS { impl VideoQoS {
pub fn new_display(&mut self, display_idx: usize) { pub fn new_display(&mut self, video_service_name: String) {
self.displays.insert(display_idx, DisplayData::default()); self.displays
.insert(video_service_name, DisplayData::default());
} }
pub fn remove_display(&mut self, display_idx: usize) { pub fn remove_display(&mut self, video_service_name: &str) {
self.displays.remove(&display_idx); self.displays.remove(video_service_name);
} }
pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) { pub fn update_display_data(&mut self, video_service_name: &str, send_counter: usize) {
if let Some(display) = self.displays.get_mut(&display_idx) { if let Some(display) = self.displays.get_mut(video_service_name) {
display.send_counter += send_counter; display.send_counter += send_counter;
} }
self.adjust_fps(); self.adjust_fps();
+161 -51
View File
@@ -18,12 +18,7 @@
// to-do: // to-do:
// https://slhck.info/video/2017/03/01/rate-control.html // https://slhck.info/video/2017/03/01/rate-control.html
use super::{ use super::{display_service::check_display_changed, service::ServiceTmpl, video_qos::VideoQoS, *};
display_service::{check_display_changed, get_display_info},
service::ServiceTmpl,
video_qos::VideoQoS,
*,
};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::common::SimpleCallOnReturn; use crate::common::SimpleCallOnReturn;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -65,7 +60,6 @@ use std::{
time::{self, Duration, Instant}, time::{self, Duration, Instant},
}; };
pub const NAME: &'static str = "video";
pub const OPTION_REFRESH: &'static str = "refresh"; pub const OPTION_REFRESH: &'static str = "refresh";
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -133,10 +127,34 @@ impl VideoFrameController {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VideoSource {
Monitor,
Camera,
}
impl VideoSource {
pub fn service_name_prefix(&self) -> &'static str {
match self {
VideoSource::Monitor => "monitor",
VideoSource::Camera => "camera",
}
}
pub fn is_monitor(&self) -> bool {
matches!(self, VideoSource::Monitor)
}
pub fn is_camera(&self) -> bool {
matches!(self, VideoSource::Camera)
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct VideoService { pub struct VideoService {
sp: GenericService, sp: GenericService,
idx: usize, idx: usize,
source: VideoSource,
} }
impl Deref for VideoService { impl Deref for VideoService {
@@ -153,14 +171,15 @@ impl DerefMut for VideoService {
} }
} }
pub fn get_service_name(idx: usize) -> String { pub fn get_service_name(source: VideoSource, idx: usize) -> String {
format!("{}{}", NAME, idx) format!("{}{}", source.service_name_prefix(), idx)
} }
pub fn new(idx: usize) -> GenericService { pub fn new(source: VideoSource, idx: usize) -> GenericService {
let vs = VideoService { let vs = VideoService {
sp: GenericService::new(get_service_name(idx), true), sp: GenericService::new(get_service_name(source, idx), true),
idx, idx,
source,
}; };
GenericService::run(&vs, run); GenericService::run(&vs, run);
vs.sp vs.sp
@@ -292,7 +311,10 @@ impl DerefMut for CapturerInfo {
} }
} }
fn get_capturer(current: usize, portable_service_running: bool) -> ResultType<CapturerInfo> { fn get_capturer_monitor(
current: usize,
portable_service_running: bool,
) -> ResultType<CapturerInfo> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if !is_x11() { if !is_x11() {
@@ -309,6 +331,7 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType<Ca
ndisplay ndisplay
); );
} }
let display = displays.remove(current); let display = displays.remove(current);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -382,8 +405,59 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType<Ca
}) })
} }
fn get_capturer_camera(current: usize) -> ResultType<CapturerInfo> {
let cameras = camera::Cameras::get_sync_cameras();
let ncamera = cameras.len();
if ncamera <= current {
bail!("Failed to get camera {}, cameras len: {}", current, ncamera,);
}
let Some(camera) = cameras.get(current) else {
bail!(
"Camera of index {} doesn't exist or platform not supported",
current
);
};
let capturer = camera::Cameras::get_capturer(current)?;
let (width, height) = (camera.width as usize, camera.height as usize);
let origin = (camera.x as i32, camera.y as i32);
let name = &camera.name;
let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID);
let _capturer_privacy_mode_id = privacy_mode_id;
log::debug!(
"#cameras={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}",
ncamera,
current,
&origin,
width,
height,
num_cpus::get_physical(),
num_cpus::get(),
name,
);
return Ok(CapturerInfo {
origin,
width,
height,
ndisplay: ncamera,
current,
privacy_mode_id,
_capturer_privacy_mode_id: privacy_mode_id,
capturer,
});
}
fn get_capturer(
source: VideoSource,
current: usize,
portable_service_running: bool,
) -> ResultType<CapturerInfo> {
match source {
VideoSource::Monitor => get_capturer_monitor(current, portable_service_running),
VideoSource::Camera => get_capturer_camera(current),
}
}
fn run(vs: VideoService) -> ResultType<()> { fn run(vs: VideoService) -> ResultType<()> {
let _raii = Raii::new(vs.idx); let _raii = Raii::new(vs.sp.name());
// Wayland only support one video capturer for now. It is ok to call ensure_inited() here. // Wayland only support one video capturer for now. It is ok to call ensure_inited() here.
// //
// ensure_inited() is needed because clear() may be called. // ensure_inited() is needed because clear() may be called.
@@ -406,7 +480,7 @@ fn run(vs: VideoService) -> ResultType<()> {
let display_idx = vs.idx; let display_idx = vs.idx;
let sp = vs.sp; let sp = vs.sp;
let mut c = get_capturer(display_idx, last_portable_service_running)?; let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?;
#[cfg(windows)] #[cfg(windows)]
if !scrap::codec::enable_directx_capture() && !c.is_gdi() { if !scrap::codec::enable_directx_capture() && !c.is_gdi() {
log::info!("disable dxgi with option, fall back to gdi"); log::info!("disable dxgi with option, fall back to gdi");
@@ -423,11 +497,12 @@ fn run(vs: VideoService) -> ResultType<()> {
drop(video_qos); drop(video_qos);
let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder( let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder(
&c, &c,
display_idx, sp.name(),
quality, quality,
client_record, client_record,
record_incoming, record_incoming,
last_portable_service_running, last_portable_service_running,
vs.source,
) { ) {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(err) => {
@@ -441,26 +516,29 @@ fn run(vs: VideoService) -> ResultType<()> {
})); }));
setup_encoder( setup_encoder(
&c, &c,
display_idx, sp.name(),
quality, quality,
client_record, client_record,
record_incoming, record_incoming,
last_portable_service_running, last_portable_service_running,
vs.source,
)? )?
} }
}; };
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
c.set_output_texture(encoder.input_texture()); c.set_output_texture(encoder.input_texture());
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
if let Err(e) = check_change_scale(encoder.is_hardware()) { if vs.source.is_monitor() {
try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); if let Err(e) = check_change_scale(encoder.is_hardware()) {
bail!(e); try_broadcast_display_changed(&sp, display_idx, &c, true).ok();
bail!(e);
}
} }
VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate());
VIDEO_QOS VIDEO_QOS
.lock() .lock()
.unwrap() .unwrap()
.set_support_changing_quality(display_idx, encoder.support_changing_quality()); .set_support_changing_quality(&sp.name(), encoder.support_changing_quality());
log::info!("initial quality: {quality:?}"); log::info!("initial quality: {quality:?}");
if sp.is_option_true(OPTION_REFRESH) { if sp.is_option_true(OPTION_REFRESH) {
@@ -500,10 +578,12 @@ fn run(vs: VideoService) -> ResultType<()> {
client_record, client_record,
&mut send_counter, &mut send_counter,
&mut second_instant, &mut second_instant,
display_idx, &sp.name(),
)?; )?;
if sp.is_option_true(OPTION_REFRESH) { if sp.is_option_true(OPTION_REFRESH) {
let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); if vs.source.is_monitor() {
let _ = try_broadcast_display_changed(&sp, display_idx, &c, true);
}
log::info!("switch to refresh"); log::info!("switch to refresh");
bail!("SWITCH"); bail!("SWITCH");
} }
@@ -527,10 +607,12 @@ fn run(vs: VideoService) -> ResultType<()> {
#[cfg(all(windows, feature = "vram"))] #[cfg(all(windows, feature = "vram"))]
if c.is_gdi() && encoder.input_texture() { if c.is_gdi() && encoder.input_texture() {
log::info!("changed to gdi when using vram"); log::info!("changed to gdi when using vram");
VRamEncoder::set_fallback_gdi(display_idx, true); VRamEncoder::set_fallback_gdi(sp.name(), true);
bail!("SWITCH"); bail!("SWITCH");
} }
check_privacy_mode_changed(&sp, display_idx, &c)?; if vs.source.is_monitor() {
check_privacy_mode_changed(&sp, display_idx, &c)?;
}
#[cfg(windows)] #[cfg(windows)]
{ {
if crate::platform::windows::desktop_changed() if crate::platform::windows::desktop_changed()
@@ -540,7 +622,7 @@ fn run(vs: VideoService) -> ResultType<()> {
} }
} }
let now = time::Instant::now(); let now = time::Instant::now();
if last_check_displays.elapsed().as_millis() > 1000 { if vs.source.is_monitor() && last_check_displays.elapsed().as_millis() > 1000 {
last_check_displays = now; last_check_displays = now;
// This check may be redundant, but it is better to be safe. // This check may be redundant, but it is better to be safe.
// The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough.
@@ -575,7 +657,7 @@ fn run(vs: VideoService) -> ResultType<()> {
{ {
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
if try_gdi == 1 && !c.is_gdi() { if try_gdi == 1 && !c.is_gdi() {
VRamEncoder::set_fallback_gdi(display_idx, false); VRamEncoder::set_fallback_gdi(sp.name(), false);
} }
try_gdi = 0; try_gdi = 0;
} }
@@ -635,7 +717,9 @@ fn run(vs: VideoService) -> ResultType<()> {
Err(err) => { Err(err) => {
// This check may be redundant, but it is better to be safe. // This check may be redundant, but it is better to be safe.
// The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough.
try_broadcast_display_changed(&sp, display_idx, &c, true)?; if vs.source.is_monitor() {
try_broadcast_display_changed(&sp, display_idx, &c, true)?;
}
#[cfg(windows)] #[cfg(windows)]
if !c.is_gdi() { if !c.is_gdi() {
@@ -657,7 +741,9 @@ fn run(vs: VideoService) -> ResultType<()> {
let timeout_millis = 3_000u64; let timeout_millis = 3_000u64;
let wait_begin = Instant::now(); let wait_begin = Instant::now();
while wait_begin.elapsed().as_millis() < timeout_millis as _ { while wait_begin.elapsed().as_millis() < timeout_millis as _ {
check_privacy_mode_changed(&sp, display_idx, &c)?; if vs.source.is_monitor() {
check_privacy_mode_changed(&sp, display_idx, &c)?;
}
frame_controller.try_wait_next(&mut fetched_conn_ids, 300); frame_controller.try_wait_next(&mut fetched_conn_ids, 300);
// break if all connections have received current frame // break if all connections have received current frame
if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() {
@@ -676,32 +762,35 @@ fn run(vs: VideoService) -> ResultType<()> {
Ok(()) Ok(())
} }
struct Raii(usize); struct Raii(String);
impl Raii { impl Raii {
fn new(display_idx: usize) -> Self { fn new(name: String) -> Self {
VIDEO_QOS.lock().unwrap().new_display(display_idx); log::info!("new video service: {}", name);
Raii(display_idx) VIDEO_QOS.lock().unwrap().new_display(name.clone());
Raii(name)
} }
} }
impl Drop for Raii { impl Drop for Raii {
fn drop(&mut self) { fn drop(&mut self) {
log::info!("stop video service: {}", self.0);
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
VRamEncoder::set_not_use(self.0, false); VRamEncoder::set_not_use(self.0.clone(), false);
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
Encoder::update(scrap::codec::EncodingUpdate::Check); Encoder::update(scrap::codec::EncodingUpdate::Check);
VIDEO_QOS.lock().unwrap().remove_display(self.0); VIDEO_QOS.lock().unwrap().remove_display(&self.0);
} }
} }
fn setup_encoder( fn setup_encoder(
c: &CapturerInfo, c: &CapturerInfo,
display_idx: usize, name: String,
quality: f32, quality: f32,
client_record: bool, client_record: bool,
record_incoming: bool, record_incoming: bool,
last_portable_service_running: bool, last_portable_service_running: bool,
source: VideoSource,
) -> ResultType<( ) -> ResultType<(
Encoder, Encoder,
EncoderCfg, EncoderCfg,
@@ -711,14 +800,15 @@ fn setup_encoder(
)> { )> {
let encoder_cfg = get_encoder_config( let encoder_cfg = get_encoder_config(
&c, &c,
display_idx, name.to_string(),
quality, quality,
client_record || record_incoming, client_record || record_incoming,
last_portable_service_running, last_portable_service_running,
source,
); );
Encoder::set_fallback(&encoder_cfg); Encoder::set_fallback(&encoder_cfg);
let codec_format = Encoder::negotiated_codec(); let codec_format = Encoder::negotiated_codec();
let recorder = get_recorder(record_incoming, display_idx); let recorder = get_recorder(record_incoming, name);
let use_i444 = Encoder::use_i444(&encoder_cfg); let use_i444 = Encoder::use_i444(&encoder_cfg);
let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?;
Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) Ok((encoder, encoder_cfg, codec_format, use_i444, recorder))
@@ -726,15 +816,16 @@ fn setup_encoder(
fn get_encoder_config( fn get_encoder_config(
c: &CapturerInfo, c: &CapturerInfo,
_display_idx: usize, _name: String,
quality: f32, quality: f32,
record: bool, record: bool,
_portable_service: bool, _portable_service: bool,
_source: VideoSource,
) -> EncoderCfg { ) -> EncoderCfg {
#[cfg(all(windows, feature = "vram"))] #[cfg(all(windows, feature = "vram"))]
if _portable_service || c.is_gdi() { if _portable_service || c.is_gdi() || _source == VideoSource::Camera {
log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service); log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service);
VRamEncoder::set_not_use(_display_idx, true); VRamEncoder::set_not_use(_name, true);
} }
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
Encoder::update(scrap::codec::EncodingUpdate::Check); Encoder::update(scrap::codec::EncodingUpdate::Check);
@@ -800,7 +891,7 @@ fn get_encoder_config(
} }
} }
fn get_recorder(record_incoming: bool, display: usize) -> Arc<Mutex<Option<Recorder>>> { fn get_recorder(record_incoming: bool, video_service_name: String) -> Arc<Mutex<Option<Recorder>>> {
#[cfg(windows)] #[cfg(windows)]
let root = crate::platform::is_root(); let root = crate::platform::is_root();
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -819,7 +910,7 @@ fn get_recorder(record_incoming: bool, display: usize) -> Arc<Mutex<Option<Recor
server: true, server: true,
id: Config::get_id(), id: Config::get_id(),
dir: crate::ui_interface::video_save_directory(root), dir: crate::ui_interface::video_save_directory(root),
display, video_service_name,
tx, tx,
}) })
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))))
@@ -1004,7 +1095,9 @@ fn try_broadcast_display_changed(
(cap.origin.0, cap.origin.1, cap.width, cap.height), (cap.origin.0, cap.origin.1, cap.width, cap.height),
) { ) {
log::info!("Display {} changed", display); log::info!("Display {} changed", display);
if let Some(msg_out) = make_display_changed_msg(display_idx, Some(display)) { if let Some(msg_out) =
make_display_changed_msg(display_idx, Some(display), VideoSource::Monitor)
{
let msg_out = Arc::new(msg_out); let msg_out = Arc::new(msg_out);
sp.send_shared(msg_out.clone()); sp.send_shared(msg_out.clone());
// switch display may occur before the first video frame, add snapshot to send to new subscribers // switch display may occur before the first video frame, add snapshot to send to new subscribers
@@ -1021,10 +1114,16 @@ fn try_broadcast_display_changed(
pub fn make_display_changed_msg( pub fn make_display_changed_msg(
display_idx: usize, display_idx: usize,
opt_display: Option<DisplayInfo>, opt_display: Option<DisplayInfo>,
source: VideoSource,
) -> Option<Message> { ) -> Option<Message> {
let display = match opt_display { let display = match opt_display {
Some(d) => d, Some(d) => d,
None => get_display_info(display_idx)?, None => match source {
VideoSource::Monitor => display_service::get_display_info(display_idx)?,
VideoSource::Camera => camera::Cameras::get_sync_cameras()
.get(display_idx)?
.clone(),
},
}; };
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_switch_display(SwitchDisplay { misc.set_switch_display(SwitchDisplay {
@@ -1033,13 +1132,24 @@ pub fn make_display_changed_msg(
y: display.y, y: display.y,
width: display.width, width: display.width,
height: display.height, height: display.height,
cursor_embedded: display_service::capture_cursor_embedded(), cursor_embedded: match source {
VideoSource::Monitor => display_service::capture_cursor_embedded(),
VideoSource::Camera => false,
},
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
resolutions: Some(SupportedResolutions { resolutions: Some(SupportedResolutions {
resolutions: if display.name.is_empty() { resolutions: match source {
vec![] VideoSource::Monitor => {
} else { if display.name.is_empty() {
crate::platform::resolutions(&display.name) vec![]
} else {
crate::platform::resolutions(&display.name)
}
}
VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx)
.ok()
.into_iter()
.collect(),
}, },
..SupportedResolutions::default() ..SupportedResolutions::default()
}) })
@@ -1059,7 +1169,7 @@ fn check_qos(
client_record: bool, client_record: bool,
send_counter: &mut usize, send_counter: &mut usize,
second_instant: &mut Instant, second_instant: &mut Instant,
display_idx: usize, name: &str,
) -> ResultType<()> { ) -> ResultType<()> {
let mut video_qos = VIDEO_QOS.lock().unwrap(); let mut video_qos = VIDEO_QOS.lock().unwrap();
*spf = video_qos.spf(); *spf = video_qos.spf();
@@ -1082,7 +1192,7 @@ fn check_qos(
} }
if second_instant.elapsed() > Duration::from_secs(1) { if second_instant.elapsed() > Duration::from_secs(1) {
*second_instant = Instant::now(); *second_instant = Instant::now();
video_qos.update_display_data(display_idx, *send_counter); video_qos.update_display_data(&name, *send_counter);
*send_counter = 0; *send_counter = 0;
} }
drop(video_qos); drop(video_qos);
+1
View File
@@ -19,6 +19,7 @@ impl InvokeUiCM for SciterHandler {
&make_args!( &make_args!(
client.id, client.id,
client.is_file_transfer, client.is_file_transfer,
client.is_view_camera,
client.port_forward.clone(), client.port_forward.clone(),
client.peer_id.clone(), client.peer_id.clone(),
client.name.clone(), client.name.clone(),
+4 -4
View File
@@ -356,7 +356,7 @@ function bring_to_top(idx=-1) {
} }
} }
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
stdout.println("new connection #" + id + ": " + peer_id); stdout.println("new connection #" + id + ": " + peer_id);
var conn; var conn;
connections.map(function(c) { connections.map(function(c) {
@@ -550,7 +550,7 @@ function adjustHeader() {
view.on("size", adjustHeader); view.on("size", adjustHeader);
// handler.addConnection(0, false, 0, "", "test1", true, false, false, true, true); // handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true);
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false, false); // handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false);
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false, false); // handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false);
// handler.newMessage(0, 'h'); // handler.newMessage(0, 'h');
+3
View File
@@ -316,6 +316,7 @@ impl InvokeUiSession for SciterHandler {
ConnType::RDP => {} ConnType::RDP => {}
ConnType::PORT_FORWARD => {} ConnType::PORT_FORWARD => {}
ConnType::FILE_TRANSFER => {} ConnType::FILE_TRANSFER => {}
ConnType::VIEW_CAMERA => {}
ConnType::DEFAULT_CONN => { ConnType::DEFAULT_CONN => {
crate::keyboard::client::start_grab_loop(); crate::keyboard::client::start_grab_loop();
} }
@@ -557,6 +558,8 @@ impl SciterSession {
let conn_type = if cmd.eq("--file-transfer") { let conn_type = if cmd.eq("--file-transfer") {
ConnType::FILE_TRANSFER ConnType::FILE_TRANSFER
} else if cmd.eq("--view-camera") {
ConnType::VIEW_CAMERA
} else if cmd.eq("--port-forward") { } else if cmd.eq("--port-forward") {
ConnType::PORT_FORWARD ConnType::PORT_FORWARD
} else if cmd.eq("--rdp") { } else if cmd.eq("--rdp") {
+7 -2
View File
@@ -47,6 +47,7 @@ pub struct Client {
pub authorized: bool, pub authorized: bool,
pub disconnected: bool, pub disconnected: bool,
pub is_file_transfer: bool, pub is_file_transfer: bool,
pub is_view_camera: bool,
pub port_forward: String, pub port_forward: String,
pub name: String, pub name: String,
pub peer_id: String, pub peer_id: String,
@@ -128,6 +129,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
&self, &self,
id: i32, id: i32,
is_file_transfer: bool, is_file_transfer: bool,
is_view_camera: bool,
port_forward: String, port_forward: String,
peer_id: String, peer_id: String,
name: String, name: String,
@@ -147,6 +149,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
authorized, authorized,
disconnected: false, disconnected: false,
is_file_transfer, is_file_transfer,
is_view_camera,
port_forward, port_forward,
name: name.clone(), name: name.clone(),
peer_id: peer_id.clone(), peer_id: peer_id.clone(),
@@ -402,9 +405,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
} }
Ok(Some(data)) => { Ok(Some(data)) => {
match data { match data {
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { Data::Login{id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
log::debug!("conn_id: {}", id); log::debug!("conn_id: {}", id);
self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.cm.add_connection(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.conn_id = id; self.conn_id = id;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@@ -672,6 +675,7 @@ pub async fn start_listen<T: InvokeUiCM>(
Some(Data::Login { Some(Data::Login {
id, id,
is_file_transfer, is_file_transfer,
is_view_camera,
port_forward, port_forward,
peer_id, peer_id,
name, name,
@@ -690,6 +694,7 @@ pub async fn start_listen<T: InvokeUiCM>(
cm.add_connection( cm.add_connection(
id, id,
is_file_transfer, is_file_transfer,
is_view_camera,
port_forward, port_forward,
peer_id, peer_id,
name, name,
+10 -1
View File
@@ -190,6 +190,10 @@ impl<T: InvokeUiSession> Session<T> {
.eq(&ConnType::FILE_TRANSFER) .eq(&ConnType::FILE_TRANSFER)
} }
pub fn is_view_camera(&self) -> bool {
self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA)
}
pub fn is_port_forward(&self) -> bool { pub fn is_port_forward(&self) -> bool {
let conn_type = self.lc.read().unwrap().conn_type; let conn_type = self.lc.read().unwrap().conn_type;
conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP
@@ -1630,7 +1634,12 @@ impl<T: InvokeUiSession> Interface for Session<T> {
if pi.displays.is_empty() { if pi.displays.is_empty() {
self.lc.write().unwrap().handle_peer_info(&pi); self.lc.write().unwrap().handle_peer_info(&pi);
self.update_privacy_mode(); self.update_privacy_mode();
self.msgbox("error", "Remote Error", "No Displays", ""); let msg = if self.is_view_camera() {
"No cameras"
} else {
"No displays"
};
self.msgbox("error", "Error", msg, "");
return; return;
} }
self.try_change_init_resolution(pi.current_display); self.try_change_init_resolution(pi.current_display);