mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
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:
487
Cargo.lock
generated
487
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,10 @@ import '../consts.dart';
|
||||
import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_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/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 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
@@ -96,6 +98,7 @@ enum DesktopType {
|
||||
main,
|
||||
remote,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
cm,
|
||||
portForward,
|
||||
}
|
||||
@@ -1750,7 +1753,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + type.name, v: pos.toString());
|
||||
|
||||
if (type == WindowType.RemoteDesktop && windowId != null) {
|
||||
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
|
||||
windowId != null) {
|
||||
await _saveSessionWindowPosition(
|
||||
type, windowId, isMaximized, isFullscreen, pos);
|
||||
}
|
||||
@@ -1901,7 +1905,9 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
String? pos;
|
||||
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||
// 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(
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
if (peerPos.isNotEmpty) {
|
||||
@@ -1916,7 +1922,7 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
debugPrint("no window position saved, ignoring position restoration");
|
||||
return false;
|
||||
}
|
||||
if (type == WindowType.RemoteDesktop) {
|
||||
if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
|
||||
if (!isRemotePeerPos && windowId != null) {
|
||||
if (lpos.offsetWidth != null) {
|
||||
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
|
||||
@@ -2085,6 +2091,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||
enum UriLinkType {
|
||||
remoteDesktop,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
portForward,
|
||||
rdp,
|
||||
}
|
||||
@@ -2136,6 +2143,11 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--view-camera':
|
||||
type = UriLinkType.viewCamera;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--port-forward':
|
||||
type = UriLinkType.portForward;
|
||||
id = args[i + 1];
|
||||
@@ -2177,6 +2189,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.viewCamera:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newViewCamera(id!,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.portForward:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newPortForward(id!, false,
|
||||
@@ -2200,7 +2218,14 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
String? command;
|
||||
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 &&
|
||||
uri.path.split('').every((char) => char == '/')) {
|
||||
return [];
|
||||
@@ -2238,6 +2263,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
connect(Get.context!, id);
|
||||
} else if (optionIndex == 2) {
|
||||
connect(Get.context!, id, isFileTransfer: true);
|
||||
} else if (optionIndex == 3) {
|
||||
connect(Get.context!, id, isViewCamera: true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -2290,6 +2317,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
|
||||
connectMainDesktop(String id,
|
||||
{required bool isFileTransfer,
|
||||
required bool isViewCamera,
|
||||
required bool isTcpTunneling,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
@@ -2302,6 +2330,12 @@ connectMainDesktop(String id,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isViewCamera) {
|
||||
await rustDeskWinManager.newViewCamera(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTcpTunneling || isRDP) {
|
||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||
password: password,
|
||||
@@ -2318,10 +2352,12 @@ connectMainDesktop(String id,
|
||||
|
||||
/// Connect to a peer with [id].
|
||||
/// 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 [isRDP], starts a session only for rdp.
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
@@ -2353,6 +2389,7 @@ connect(BuildContext context, String id,
|
||||
await connectMainDesktop(
|
||||
id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
password: password,
|
||||
@@ -2363,6 +2400,7 @@ connect(BuildContext context, String id,
|
||||
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
|
||||
'id': id,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTcpTunneling': isTcpTunneling,
|
||||
'isRDP': isRDP,
|
||||
'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 {
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
@@ -2686,6 +2749,8 @@ String getWindowName({WindowType? overrideType}) {
|
||||
return name;
|
||||
case WindowType.FileTransfer:
|
||||
return "File Transfer - $name";
|
||||
case WindowType.ViewCamera:
|
||||
return "View Camera - $name";
|
||||
case WindowType.PortForward:
|
||||
return "Port Forward - $name";
|
||||
case WindowType.RemoteDesktop:
|
||||
@@ -3051,6 +3116,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
'peer_id': peerId,
|
||||
'display': i,
|
||||
'display_count': pi.displays.length,
|
||||
'window_type': (kWindowType ?? WindowType.RemoteDesktop).index,
|
||||
};
|
||||
if (screenRect != null) {
|
||||
args['screen_rect'] = {
|
||||
@@ -3065,12 +3131,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
}
|
||||
|
||||
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
|
||||
int? display, Rect? screenRect) async {
|
||||
WindowType windowType, int? display, Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
// Do not restore window position to new connection if there's a pre-session.
|
||||
// https://github.com/rustdesk/rustdesk/discussions/8825
|
||||
if (preSessionCount == 0) {
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
await restoreWindowPosition(windowType,
|
||||
windowId: windowId, display: display, peerId: peerId);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -488,6 +488,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
BuildContext context,
|
||||
String title, {
|
||||
bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
}) {
|
||||
@@ -502,6 +503,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
peer,
|
||||
tab,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
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
|
||||
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
@@ -880,6 +891,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -939,6 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -992,6 +1005,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -1045,6 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1177,6 +1192,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1398,6 +1414,7 @@ class TagPainter extends CustomPainter {
|
||||
|
||||
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false}) async {
|
||||
var password = '';
|
||||
@@ -1423,6 +1440,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
class RawTouchGestureDetectorRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
final FFI ffi;
|
||||
|
||||
final bool isCamera;
|
||||
late final InputModel inputModel = ffi.inputModel;
|
||||
late final FfiModel ffiModel = ffi.ffiModel;
|
||||
|
||||
RawTouchGestureDetectorRegion({
|
||||
required this.child,
|
||||
required this.ffi,
|
||||
this.isCamera = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -382,6 +384,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
_scale = d.scale;
|
||||
|
||||
if (scale != 0) {
|
||||
if (widget.isCamera) return;
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
@@ -402,6 +405,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
if (widget.isCamera) return;
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +89,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||
|
||||
List<TTextMenu> v = [];
|
||||
// elevation
|
||||
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
|
||||
if (isDefaultConn &&
|
||||
perms['keyboard'] != false &&
|
||||
ffi.elevationModel.showRequestMenu) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Request Elevation')),
|
||||
@@ -101,7 +104,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// osAccount / osPassword
|
||||
if (perms['keyboard'] != false) {
|
||||
if (isDefaultConn && perms['keyboard'] != false) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Row(children: [
|
||||
@@ -130,7 +133,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||
if (isDefaultConn &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
perms['keyboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Send clipboard keystrokes')),
|
||||
onPressed: () async {
|
||||
@@ -142,43 +147,53 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
}));
|
||||
}
|
||||
// reset canvas
|
||||
if (isMobile) {
|
||||
if (isDefaultConn && isMobile) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
connectWithToken(
|
||||
{required bool isFileTransfer, required bool isTcpTunneling}) {
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDesktop) {
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||
onPressed: () => connectWithToken(isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
// viewCamera
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('View camera')),
|
||||
onPressed: () => connectWithToken(isViewCamera: true)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
if (isDesktop) {
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||
onPressed: () => connectWithToken(isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
// note
|
||||
if (bind
|
||||
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
|
||||
.isNotEmpty) {
|
||||
if (isDefaultConn &&
|
||||
bind
|
||||
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
|
||||
.isNotEmpty) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Note')),
|
||||
@@ -186,11 +201,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// divider
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (isDefaultConn && (isDesktop || isWebDesktop)) {
|
||||
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
|
||||
}
|
||||
// ctrlAltDel
|
||||
if (!ffiModel.viewOnly &&
|
||||
if (isDefaultConn &&
|
||||
!ffiModel.viewOnly &&
|
||||
ffiModel.keyboard &&
|
||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||
v.add(
|
||||
@@ -200,7 +216,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// restart
|
||||
if (perms['restart'] != false &&
|
||||
if (isDefaultConn &&
|
||||
perms['restart'] != false &&
|
||||
(pi.platform == kPeerPlatformLinux ||
|
||||
pi.platform == kPeerPlatformWindows ||
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
@@ -212,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// insertLock
|
||||
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
||||
if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Insert Lock')),
|
||||
@@ -220,7 +237,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// blockUserInput
|
||||
if (ffi.ffiModel.keyboard &&
|
||||
if (isDefaultConn &&
|
||||
ffi.ffiModel.keyboard &&
|
||||
ffi.ffiModel.permissions['block_input'] != false &&
|
||||
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
|
||||
{
|
||||
@@ -236,12 +254,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
}));
|
||||
}
|
||||
// switchSides
|
||||
if (isDesktop &&
|
||||
if (isDefaultConn &&
|
||||
isDesktop &&
|
||||
ffiModel.keyboard &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
pi.platform != kPeerPlatformMacOS &&
|
||||
versionCmp(pi.version, '1.2.0') >= 0 &&
|
||||
bind.peerGetDefaultSessionsCount(id: id) == 1) {
|
||||
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Switch Sides')),
|
||||
onPressed: () =>
|
||||
@@ -523,6 +542,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||
|
||||
// show quality monitor
|
||||
final option = 'show-quality-monitor';
|
||||
@@ -535,7 +555,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
child: Text(translate('Show quality monitor'))));
|
||||
// mute
|
||||
if (perms['audio'] != false) {
|
||||
if (isDefaultConn && perms['audio'] != false) {
|
||||
final option = 'disable-audio';
|
||||
final value =
|
||||
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 &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
||||
if (ffiModel.keyboard &&
|
||||
if (isDefaultConn &&
|
||||
ffiModel.keyboard &&
|
||||
perms['file'] != false &&
|
||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
@@ -574,7 +595,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Enable file copy and paste'))));
|
||||
}
|
||||
// disable clipboard
|
||||
if (ffiModel.keyboard && perms['clipboard'] != false) {
|
||||
if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'disable-clipboard';
|
||||
var value =
|
||||
@@ -591,7 +612,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Disable clipboard'))));
|
||||
}
|
||||
// lock after session end
|
||||
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||
if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'lock-after-session-end';
|
||||
final value =
|
||||
@@ -656,12 +677,12 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('True color (4:4:4)'))));
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
if (isDefaultConn && isMobile) {
|
||||
v.addAll(toolbarKeyboardToggles(ffi));
|
||||
}
|
||||
|
||||
// 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(
|
||||
value: ffiModel.viewOnly,
|
||||
onChanged: (value) async {
|
||||
|
||||
@@ -27,6 +27,7 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
@@ -44,6 +45,7 @@ const String kAppTypeConnectionManager = "cm";
|
||||
|
||||
const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kAppTypeDesktopViewCamera = "view camera";
|
||||
const String kAppTypeDesktopPortForward = "port forward";
|
||||
|
||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||
@@ -58,6 +60,7 @@ const String kWindowConnect = "connect";
|
||||
|
||||
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
const String kWindowEventNewViewCamera = "new_view_camera";
|
||||
const String kWindowEventNewPortForward = "new_port_forward";
|
||||
const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
@@ -97,6 +100,7 @@ const String kOptionEnableKeyboard = "enable-keyboard";
|
||||
const String kOptionEnableClipboard = "enable-clipboard";
|
||||
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||
const String kOptionEnableAudio = "enable-audio";
|
||||
const String kOptionEnableCamera = "enable-camera";
|
||||
const String kOptionEnableTunnel = "enable-tunnel";
|
||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||
const String kOptionEnableBlockInput = "enable-block-input";
|
||||
|
||||
@@ -17,7 +17,6 @@ import '../../common/formatter/id_formatter.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
class OnlineStatusWidget extends StatefulWidget {
|
||||
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
|
||||
@@ -203,6 +202,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
final FocusNode _idFocusNode = FocusNode();
|
||||
final TextEditingController _idEditingController = TextEditingController();
|
||||
|
||||
String selectedConnectionType = 'Connect';
|
||||
|
||||
bool isWindowMinimized = false;
|
||||
|
||||
final AllPeersLoader _allPeersLoader = AllPeersLoader();
|
||||
@@ -321,9 +322,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) {
|
||||
var id = _idController.id;
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
@@ -501,21 +503,64 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 13.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () => onConnect(isFileTransfer: true),
|
||||
text: "Transfer file",
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
SizedBox(
|
||||
height: 28.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
onConnect();
|
||||
},
|
||||
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(
|
||||
call.arguments['id'],
|
||||
isFileTransfer: call.arguments['isFileTransfer'],
|
||||
isViewCamera: call.arguments['isViewCamera'],
|
||||
isTcpTunneling: call.arguments['isTcpTunneling'],
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
@@ -789,9 +790,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
} catch (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(
|
||||
windowId, args[1], args[2]);
|
||||
windowId, args[1], args[2], windowType);
|
||||
}
|
||||
} else if (call.method == kWindowEventOpenMonitorSession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
@@ -799,9 +806,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final peerId = args['peer_id'] as String;
|
||||
final display = args['display'] as int;
|
||||
final displayCount = args['display_count'] as int;
|
||||
final windowType = args['window_type'] as int;
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount, screenRect);
|
||||
windowId, peerId, display, displayCount, screenRect, windowType);
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final windowId = int.tryParse(call.arguments);
|
||||
if (windowId != null) {
|
||||
|
||||
@@ -960,6 +960,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable TCP tunneling', kOptionEnableTunnel,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
|
||||
@@ -269,8 +269,10 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
style: style,
|
||||
),
|
||||
proc: () async {
|
||||
await DesktopMultiWindow.invokeMethod(kMainWindowId,
|
||||
kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId');
|
||||
await DesktopMultiWindow.invokeMethod(
|
||||
kMainWindowId,
|
||||
kWindowEventMoveTabToNewWindow,
|
||||
'${windowId()},$key,$sessionId,RemoteDesktop');
|
||||
cancelFunc();
|
||||
},
|
||||
padding: padding,
|
||||
@@ -417,8 +419,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
}
|
||||
await setNewConnectWindowFrame(
|
||||
windowId(), id!, prePeerCount, display, screenRect);
|
||||
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
|
||||
WindowType.RemoteDesktop, display, screenRect);
|
||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||
await windowOnTop(windowId());
|
||||
});
|
||||
|
||||
@@ -353,7 +353,9 @@ Widget buildConnectionCard(Client client) {
|
||||
key: ValueKey(client.id),
|
||||
children: [
|
||||
_CmHeader(client: client),
|
||||
client.type_() != ClientType.remote || client.disconnected
|
||||
client.type_() == ClientType.file ||
|
||||
client.type_() == ClientType.portForward ||
|
||||
client.disconnected
|
||||
? Offstage()
|
||||
: _PrivilegeBoard(client: client),
|
||||
Expanded(
|
||||
@@ -526,7 +528,8 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
Offstage(
|
||||
offstage: !client.authorized ||
|
||||
(client.type_() != ClientType.remote &&
|
||||
client.type_() != ClientType.file),
|
||||
client.type_() != ClientType.file &&
|
||||
client.type_() != ClientType.camera),
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(client.id, () {
|
||||
if (client.type_() == ClientType.file) {
|
||||
@@ -627,96 +630,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing),
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
children: [
|
||||
buildPermissionIcon(
|
||||
client.keyboard,
|
||||
Icons.keyboard,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id, name: "keyboard", enabled: enabled);
|
||||
setState(() {
|
||||
client.keyboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable keyboard/mouse'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.clipboard,
|
||||
Icons.assignment_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id, name: "clipboard", enabled: enabled);
|
||||
setState(() {
|
||||
client.clipboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable clipboard'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.audio,
|
||||
Icons.volume_up_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id, name: "audio", enabled: enabled);
|
||||
setState(() {
|
||||
client.audio = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable audio'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.file,
|
||||
Icons.upload_file_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id, name: "file", enabled: enabled);
|
||||
setState(() {
|
||||
client.file = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable file copy and paste'),
|
||||
),
|
||||
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'),
|
||||
)
|
||||
],
|
||||
children: client.type_() == ClientType.camera
|
||||
? [
|
||||
buildPermissionIcon(
|
||||
client.audio,
|
||||
Icons.volume_up_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "audio",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.audio = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable audio'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.recording,
|
||||
Icons.videocam_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "recording",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.recording = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable recording session'),
|
||||
),
|
||||
]
|
||||
: [
|
||||
buildPermissionIcon(
|
||||
client.keyboard,
|
||||
Icons.keyboard,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "keyboard",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.keyboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable keyboard/mouse'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.clipboard,
|
||||
Icons.assignment_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "clipboard",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.clipboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable clipboard'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.audio,
|
||||
Icons.volume_up_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "audio",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.audio = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable audio'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.file,
|
||||
Icons.upload_file_rounded,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "file",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.file = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable file copy and paste'),
|
||||
),
|
||||
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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
730
flutter/lib/desktop/pages/view_camera_page.dart
Normal file
730
flutter/lib/desktop/pages/view_camera_page.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
499
flutter/lib/desktop/pages/view_camera_tab_page.dart
Normal file
499
flutter/lib/desktop/pages/view_camera_tab_page.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
35
flutter/lib/desktop/screen/desktop_view_camera_screen.dart
Normal file
35
flutter/lib/desktop/screen/desktop_view_camera_screen.dart
Normal file
@@ -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,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -478,7 +478,10 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
state: widget.state,
|
||||
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));
|
||||
if (!isWeb) {
|
||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||
@@ -1043,23 +1046,26 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
scrollStyle(),
|
||||
imageQuality(),
|
||||
codec(),
|
||||
_ResolutionsMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
if (showVirtualDisplayMenu(ffi))
|
||||
if (ffi.connType == ConnType.defaultConn)
|
||||
_ResolutionsMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn)
|
||||
_SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||
child: Text(translate("Virtual display")),
|
||||
),
|
||||
cursorToggles(),
|
||||
if (ffi.connType == ConnType.defaultConn) cursorToggles(),
|
||||
Divider(),
|
||||
toggles(),
|
||||
];
|
||||
// privacy mode
|
||||
if (ffiModel.keyboard && pi.features.privacyMode) {
|
||||
if (ffi.connType == ConnType.defaultConn &&
|
||||
ffiModel.keyboard &&
|
||||
pi.features.privacyMode) {
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
final privacyModeList =
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme;
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -51,6 +52,7 @@ enum DesktopTabType {
|
||||
cm,
|
||||
remoteScreen,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
portForward,
|
||||
install,
|
||||
}
|
||||
@@ -179,11 +181,13 @@ class DesktopTabController {
|
||||
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
|
||||
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++) {
|
||||
final tab = state.value.tabs[i];
|
||||
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) {
|
||||
return jumpTo(i, callOnSelected: true);
|
||||
}
|
||||
@@ -725,6 +729,7 @@ class WindowActionPanelState extends State<WindowActionPanel> {
|
||||
return widget.tabController.state.value.tabs.length > 1 &&
|
||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
|
||||
widget.tabController.tabType == DesktopTabType.viewCamera ||
|
||||
widget.tabController.tabType == DesktopTabType.portForward ||
|
||||
widget.tabController.tabType == DesktopTabType.cm);
|
||||
}
|
||||
|
||||
@@ -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/server_page.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_remote_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
@@ -76,6 +77,13 @@ Future<void> main(List<String> args) async {
|
||||
kAppTypeDesktopFileTransfer,
|
||||
);
|
||||
break;
|
||||
case WindowType.ViewCamera:
|
||||
desktopType = DesktopType.viewCamera;
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopViewCamera,
|
||||
);
|
||||
break;
|
||||
case WindowType.PortForward:
|
||||
desktopType = DesktopType.portForward;
|
||||
runMultiWindow(
|
||||
@@ -192,6 +200,12 @@ void runMultiWindow(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
case kAppTypeDesktopViewCamera:
|
||||
draggablePositions.load();
|
||||
widget = DesktopViewCameraScreen(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
case kAppTypeDesktopPortForward:
|
||||
widget = DesktopPortForwardScreen(
|
||||
params: argument,
|
||||
@@ -227,6 +241,19 @@ void runMultiWindow(
|
||||
await restoreWindowPosition(WindowType.FileTransfer,
|
||||
windowId: kWindowId!);
|
||||
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:
|
||||
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
|
||||
break;
|
||||
|
||||
@@ -204,6 +204,7 @@ class WebHomePage extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
String? id;
|
||||
String? password;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
@@ -219,6 +220,11 @@ class WebHomePage extends StatelessWidget {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--view-camera':
|
||||
isViewCamera = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
@@ -228,7 +234,7 @@ class WebHomePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
connect(context, id, isFileTransfer: isFileTransfer, password: password);
|
||||
connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
721
flutter/lib/mobile/pages/view_camera_page.dart
Normal file
721
flutter/lib/mobile/pages/view_camera_page.dart
Normal file
@@ -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) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
|
||||
@@ -369,6 +369,7 @@ class InputModel {
|
||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
@@ -471,6 +472,7 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if (isViewCamera) return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
@@ -525,6 +527,7 @@ class InputModel {
|
||||
|
||||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if (isViewCamera) return KeyEventResult.handled;
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
@@ -724,6 +727,7 @@ class InputModel {
|
||||
/// [press] indicates a click event(down and up).
|
||||
void inputKey(String name, {bool? down, bool? press}) {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
bind.sessionInputKey(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
@@ -785,6 +789,7 @@ class InputModel {
|
||||
|
||||
/// Send scroll event with scroll distance [y].
|
||||
Future<void> scroll(int y) async {
|
||||
if (isViewCamera) return;
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json
|
||||
@@ -808,6 +813,7 @@ class InputModel {
|
||||
/// Send mouse press event.
|
||||
Future<void> sendMouse(String type, MouseButtons button) async {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
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].
|
||||
Future<void> moveMouse(double x, double y) async {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
var x2 = x.toInt();
|
||||
var y2 = y.toInt();
|
||||
await bind.sessionSendMouse(
|
||||
@@ -857,6 +864,7 @@ class InputModel {
|
||||
_lastScale = 1.0;
|
||||
_stopFling = true;
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
|
||||
}
|
||||
@@ -865,6 +873,7 @@ class InputModel {
|
||||
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
|
||||
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (peerPlatform != kPeerPlatformAndroid) {
|
||||
final scale = ((e.scale - _lastScale) * 1000).toInt();
|
||||
_lastScale = e.scale;
|
||||
@@ -904,6 +913,7 @@ class InputModel {
|
||||
handlePointerEvent('touch', kMouseEventTypePanUpdate,
|
||||
Offset(x.toDouble(), y.toDouble()));
|
||||
} else {
|
||||
if (isViewCamera) return;
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
|
||||
@@ -912,6 +922,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
void _scheduleFling(double x, double y, int delay) {
|
||||
if (isViewCamera) return;
|
||||
if ((x == 0 && y == 0) || _stopFling) {
|
||||
_fling = false;
|
||||
return;
|
||||
@@ -963,6 +974,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
||||
if (isViewCamera) return;
|
||||
if (peerPlatform == kPeerPlatformAndroid) {
|
||||
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
|
||||
return;
|
||||
@@ -994,6 +1006,7 @@ class InputModel {
|
||||
_remoteWindowCoords = [];
|
||||
_windowRect = null;
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = false;
|
||||
@@ -1007,6 +1020,7 @@ class InputModel {
|
||||
void onPointUpImage(PointerUpEvent e) {
|
||||
if (isDesktop) _queryOtherWindowCoords = false;
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
||||
@@ -1015,6 +1029,7 @@ class InputModel {
|
||||
|
||||
void onPointMoveImage(PointerMoveEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_queryOtherWindowCoords) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
@@ -1049,6 +1064,7 @@ class InputModel {
|
||||
|
||||
void onPointerSignalImage(PointerSignalEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
@@ -1146,6 +1162,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
final evt = PointerEventToRust(kind, type, evtValue).toJson();
|
||||
if (isViewCamera) return;
|
||||
bind.sessionSendPointer(
|
||||
sessionId: sessionId, msg: json.encode(modify(evt)));
|
||||
}
|
||||
@@ -1177,6 +1194,7 @@ class InputModel {
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
}) {
|
||||
if (isViewCamera) return;
|
||||
double x = offset.dx;
|
||||
double y = max(0.0, offset.dy);
|
||||
if (_checkPeerControlProtected(x, y)) {
|
||||
|
||||
@@ -407,7 +407,9 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.sendEmptyDirs(evt);
|
||||
}
|
||||
} 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');
|
||||
}
|
||||
} else {
|
||||
@@ -501,7 +503,9 @@ class FfiModel with ChangeNotifier {
|
||||
final display = int.parse(evt['display']);
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -809,7 +813,9 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.primaryDisplay = currentDisplay;
|
||||
}
|
||||
|
||||
if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) {
|
||||
if (bind.peerGetSessionsCount(
|
||||
id: peerId, connType: parent.target!.connType.index) <=
|
||||
1) {
|
||||
_pi.currentDisplay = currentDisplay;
|
||||
}
|
||||
|
||||
@@ -827,9 +833,11 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||
'';
|
||||
}
|
||||
// FIXME: handle ViewCamera ConnType independently.
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else if (connType == ConnType.defaultConn) {
|
||||
} else if (connType == ConnType.defaultConn ||
|
||||
connType == ConnType.viewCamera) {
|
||||
List<Display> newDisplays = [];
|
||||
List<dynamic> displays = json.decode(evt['displays']);
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
@@ -859,7 +867,7 @@ class FfiModel with ChangeNotifier {
|
||||
bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||
}
|
||||
if (connType == ConnType.defaultConn) {
|
||||
if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
|
||||
final platformAdditions = evt['platform_additions'];
|
||||
if (platformAdditions != null && platformAdditions != '') {
|
||||
try {
|
||||
@@ -2576,7 +2584,8 @@ class ElevationModel with ChangeNotifier {
|
||||
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.
|
||||
class FFI {
|
||||
@@ -2651,10 +2660,11 @@ class FFI {
|
||||
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(
|
||||
String id, {
|
||||
bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isPortForward = false,
|
||||
bool isRdp = false,
|
||||
String? switchUuid,
|
||||
@@ -2669,9 +2679,15 @@ class FFI {
|
||||
closed = false;
|
||||
auditNote = '';
|
||||
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) {
|
||||
connType = ConnType.fileTransfer;
|
||||
} else if (isViewCamera) {
|
||||
connType = ConnType.viewCamera;
|
||||
} else if (isPortForward) {
|
||||
connType = ConnType.portForward;
|
||||
} else {
|
||||
@@ -2691,6 +2707,7 @@ class FFI {
|
||||
sessionId: sessionId,
|
||||
id: id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isPortForward: isPortForward,
|
||||
isRdp: isRdp,
|
||||
switchUuid: switchUuid ?? '',
|
||||
@@ -2706,7 +2723,10 @@ class FFI {
|
||||
return;
|
||||
}
|
||||
final addRes = bind.sessionAddExistedSync(
|
||||
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||
id: id,
|
||||
sessionId: sessionId,
|
||||
displays: Int32List.fromList(displays),
|
||||
isViewCamera: isViewCamera);
|
||||
if (addRes != '') {
|
||||
debugPrint(
|
||||
'Unreachable, failed to add existed session to $id, $addRes');
|
||||
@@ -2717,6 +2737,11 @@ class FFI {
|
||||
if (isDesktop && connType == ConnType.defaultConn) {
|
||||
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.
|
||||
// Though the stream is returned immediately, the stream may not be ready.
|
||||
@@ -2993,6 +3018,9 @@ class PeerInfo with ChangeNotifier {
|
||||
bool get isAmyuniIdd =>
|
||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||
|
||||
bool get isSupportViewCamera =>
|
||||
platformAdditions[kPlatformAdditionsSupportViewCamera] == true;
|
||||
|
||||
Display? tryGetDisplay({int? display}) {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
|
||||
@@ -791,6 +791,7 @@ class ServerModel with ChangeNotifier {
|
||||
enum ClientType {
|
||||
remote,
|
||||
file,
|
||||
camera,
|
||||
portForward,
|
||||
}
|
||||
|
||||
@@ -798,6 +799,7 @@ class Client {
|
||||
int id = 0; // client connections inner count id
|
||||
bool authorized = false;
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
String portForward = "";
|
||||
String name = "";
|
||||
String peerId = ""; // peer user's id,show at app
|
||||
@@ -815,13 +817,15 @@ class Client {
|
||||
|
||||
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);
|
||||
|
||||
Client.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
authorized = json['authorized'];
|
||||
isFileTransfer = json['is_file_transfer'];
|
||||
// TODO: no entry then default.
|
||||
isViewCamera = json['is_view_camera'];
|
||||
portForward = json['port_forward'];
|
||||
name = json['name'];
|
||||
peerId = json['peer_id'];
|
||||
@@ -843,6 +847,7 @@ class Client {
|
||||
data['id'] = id;
|
||||
data['authorized'] = authorized;
|
||||
data['is_file_transfer'] = isFileTransfer;
|
||||
data['is_view_camera'] = isViewCamera;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
data['peer_id'] = peerId;
|
||||
@@ -863,6 +868,8 @@ class Client {
|
||||
ClientType type_() {
|
||||
if (isFileTransfer) {
|
||||
return ClientType.file;
|
||||
} else if (isViewCamera) {
|
||||
return ClientType.camera;
|
||||
} else if (portForward.isNotEmpty) {
|
||||
return ClientType.portForward;
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,14 @@ import 'package:flutter_hbb/models/input_model.dart';
|
||||
|
||||
/// must keep the order
|
||||
// ignore: constant_identifier_names
|
||||
enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown }
|
||||
enum WindowType {
|
||||
Main,
|
||||
RemoteDesktop,
|
||||
FileTransfer,
|
||||
ViewCamera,
|
||||
PortForward,
|
||||
Unknown
|
||||
}
|
||||
|
||||
extension Index on int {
|
||||
WindowType get windowType {
|
||||
@@ -23,6 +30,8 @@ extension Index on int {
|
||||
case 2:
|
||||
return WindowType.FileTransfer;
|
||||
case 3:
|
||||
return WindowType.ViewCamera;
|
||||
case 4:
|
||||
return WindowType.PortForward;
|
||||
default:
|
||||
return WindowType.Unknown;
|
||||
@@ -50,31 +59,46 @@ class RustDeskMultiWindowManager {
|
||||
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
|
||||
final List<int> _remoteDesktopWindows = 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);
|
||||
|
||||
moveTabToNewWindow(int windowId, String peerId, String sessionId) async {
|
||||
moveTabToNewWindow(int windowId, String peerId, String sessionId,
|
||||
WindowType windowType) async {
|
||||
var params = {
|
||||
'type': WindowType.RemoteDesktop.index,
|
||||
'type': windowType.index,
|
||||
'id': peerId,
|
||||
'tab_window_id': windowId,
|
||||
'session_id': sessionId,
|
||||
};
|
||||
await _newSession(
|
||||
false,
|
||||
WindowType.RemoteDesktop,
|
||||
kWindowEventNewRemoteDesktop,
|
||||
peerId,
|
||||
_remoteDesktopWindows,
|
||||
jsonEncode(params),
|
||||
);
|
||||
if (windowType == WindowType.RemoteDesktop) {
|
||||
await _newSession(
|
||||
false,
|
||||
WindowType.RemoteDesktop,
|
||||
kWindowEventNewRemoteDesktop,
|
||||
peerId,
|
||||
_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.
|
||||
// Because the _remoteDesktopWindows is managed in that thread.
|
||||
openMonitorSession(int windowId, String peerId, int display, int displayCount,
|
||||
Rect? screenRect) async {
|
||||
if (_remoteDesktopWindows.length > 1) {
|
||||
for (final windowId in _remoteDesktopWindows) {
|
||||
Rect? screenRect, int windowType) async {
|
||||
final isCamera = windowType == WindowType.ViewCamera.index;
|
||||
final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows;
|
||||
if (windowIDs.length > 1) {
|
||||
for (final windowId in windowIDs) {
|
||||
if (await DesktopMultiWindow.invokeMethod(
|
||||
windowId,
|
||||
kWindowEventActiveDisplaySession,
|
||||
@@ -91,7 +115,7 @@ class RustDeskMultiWindowManager {
|
||||
? List.generate(displayCount, (index) => index)
|
||||
: [display];
|
||||
var params = {
|
||||
'type': WindowType.RemoteDesktop.index,
|
||||
'type': windowType,
|
||||
'id': peerId,
|
||||
'tab_window_id': windowId,
|
||||
'display': display,
|
||||
@@ -107,10 +131,10 @@ class RustDeskMultiWindowManager {
|
||||
}
|
||||
await _newSession(
|
||||
false,
|
||||
WindowType.RemoteDesktop,
|
||||
kWindowEventNewRemoteDesktop,
|
||||
windowType.windowType,
|
||||
isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop,
|
||||
peerId,
|
||||
_remoteDesktopWindows,
|
||||
windowIDs,
|
||||
jsonEncode(params),
|
||||
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(
|
||||
String remoteId,
|
||||
bool isRDP, {
|
||||
@@ -324,6 +369,8 @@ class RustDeskMultiWindowManager {
|
||||
return _remoteDesktopWindows;
|
||||
case WindowType.FileTransfer:
|
||||
return _fileTransferWindows;
|
||||
case WindowType.ViewCamera:
|
||||
return _viewCameraWindows;
|
||||
case WindowType.PortForward:
|
||||
return _portForwardWindows;
|
||||
case WindowType.Unknown:
|
||||
@@ -342,6 +389,9 @@ class RustDeskMultiWindowManager {
|
||||
case WindowType.FileTransfer:
|
||||
_fileTransferWindows.clear();
|
||||
break;
|
||||
case WindowType.ViewCamera:
|
||||
_viewCameraWindows.clear();
|
||||
break;
|
||||
case WindowType.PortForward:
|
||||
_portForwardWindows.clear();
|
||||
break;
|
||||
|
||||
@@ -60,7 +60,8 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("hostStopSystemKeyPropagate");
|
||||
}
|
||||
|
||||
int peerGetDefaultSessionsCount({required String id, dynamic hint}) {
|
||||
int peerGetSessionsCount(
|
||||
{required String id, required int connType, dynamic hint}) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ class RustdeskImpl {
|
||||
{required String id,
|
||||
required UuidValue sessionId,
|
||||
required Int32List displays,
|
||||
required bool isViewCamera,
|
||||
dynamic hint}) {
|
||||
return '';
|
||||
}
|
||||
@@ -76,6 +78,7 @@ class RustdeskImpl {
|
||||
{required UuidValue sessionId,
|
||||
required String id,
|
||||
required bool isFileTransfer,
|
||||
required bool isViewCamera,
|
||||
required bool isPortForward,
|
||||
required bool isRdp,
|
||||
required String switchUuid,
|
||||
@@ -90,7 +93,8 @@ class RustdeskImpl {
|
||||
'id': id,
|
||||
'password': password,
|
||||
'is_shared_password': isSharedPassword,
|
||||
'isFileTransfer': isFileTransfer
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final testClients = [
|
||||
Client(0, false, false, "UserAAAAAA", "123123123", true, false, false),
|
||||
Client(1, false, false, "UserBBBBB", "221123123", true, false, false),
|
||||
Client(2, false, false, "UserC", "331123123", true, false, false),
|
||||
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
|
||||
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false),
|
||||
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false),
|
||||
Client(2, false, false, false, "UserC", "331123123", true, false, 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
|
||||
|
||||
@@ -23,6 +23,7 @@ lazy_static = "1.4"
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
webm = { git = "https://github.com/rustdesk-org/rust-webm" }
|
||||
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]
|
||||
version = "0.3"
|
||||
|
||||
232
libs/scrap/src/common/camera.rs
Normal file
232
libs/scrap/src/common/camera.rs
Normal 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) {}
|
||||
}
|
||||
@@ -70,23 +70,30 @@ impl TraitCapturer for Capturer {
|
||||
|
||||
pub struct PixelBuffer<'a> {
|
||||
data: &'a [u8],
|
||||
pixfmt: Pixfmt,
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: Vec<usize>,
|
||||
}
|
||||
|
||||
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 mut stride = Vec::new();
|
||||
stride.push(stride0);
|
||||
PixelBuffer {
|
||||
data,
|
||||
pixfmt,
|
||||
width,
|
||||
height,
|
||||
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> {
|
||||
@@ -107,7 +114,7 @@ impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> {
|
||||
}
|
||||
|
||||
fn pixfmt(&self) -> Pixfmt {
|
||||
Pixfmt::BGRA
|
||||
self.pixfmt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +239,7 @@ impl CapturerMag {
|
||||
impl TraitCapturer for CapturerMag {
|
||||
fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result<Frame<'a>> {
|
||||
self.inner.frame(&mut self.data)?;
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
&self.data,
|
||||
self.inner.get_rect().1,
|
||||
self.inner.get_rect().2,
|
||||
|
||||
@@ -48,6 +48,7 @@ pub use self::convert::*;
|
||||
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 mod camera;
|
||||
pub mod aom;
|
||||
pub mod record;
|
||||
mod vpx;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
pub id: String,
|
||||
pub dir: String,
|
||||
pub display: usize,
|
||||
pub video_service_name: String,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ impl RecorderContext2 {
|
||||
+ "_"
|
||||
+ &ctx.id.clone()
|
||||
+ &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()
|
||||
+ if self.format == CodecFormat::VP9
|
||||
|| self.format == CodecFormat::VP8
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg},
|
||||
codec::{enable_vram_option, EncoderApi, EncoderCfg},
|
||||
hwcodec::HwCodecConfig,
|
||||
AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt,
|
||||
};
|
||||
@@ -30,8 +30,8 @@ use hwcodec::{
|
||||
// https://cybersided.com/two-monitors-two-gpus/
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENOCDE_NOT_USE: Arc<Mutex<HashMap<usize, bool>>> = Default::default();
|
||||
static ref FALLBACK_GDI_DISPLAYS: Arc<Mutex<HashSet<usize>>> = Default::default();
|
||||
static ref ENOCDE_NOT_USE: Arc<Mutex<HashMap<String, bool>>> = Default::default();
|
||||
static ref FALLBACK_GDI_DISPLAYS: Arc<Mutex<HashSet<String>>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -287,16 +287,25 @@ impl VRamEncoder {
|
||||
crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264)
|
||||
}
|
||||
|
||||
pub fn set_not_use(display: usize, not_use: bool) {
|
||||
log::info!("set display#{display} not use vram encode to {not_use}");
|
||||
ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use);
|
||||
pub fn set_not_use(video_service_name: String, not_use: bool) {
|
||||
log::info!("set {video_service_name} not use vram encode to {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 {
|
||||
FALLBACK_GDI_DISPLAYS.lock().unwrap().insert(display);
|
||||
FALLBACK_GDI_DISPLAYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(video_service_name);
|
||||
} else {
|
||||
FALLBACK_GDI_DISPLAYS.lock().unwrap().remove(&display);
|
||||
FALLBACK_GDI_DISPLAYS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&video_service_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ impl Capturer {
|
||||
} else {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
self.get_pixelbuffer(timeout)?,
|
||||
width,
|
||||
height,
|
||||
|
||||
@@ -1389,14 +1389,14 @@ impl VideoHandler {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
if start {
|
||||
self.recorder = Recorder::new(RecorderContext {
|
||||
server: false,
|
||||
id,
|
||||
dir: crate::ui_interface::video_save_directory(false),
|
||||
display,
|
||||
video_service_name,
|
||||
tx: None,
|
||||
})
|
||||
.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(),
|
||||
..Default::default()
|
||||
}),
|
||||
ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()),
|
||||
ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward {
|
||||
host: self.port_forward.0.clone(),
|
||||
port: self.port_forward.1,
|
||||
@@ -2436,6 +2437,14 @@ pub fn start_video_thread<F, T>(
|
||||
{
|
||||
let mut video_callback = video_callback;
|
||||
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 || {
|
||||
#[cfg(windows)]
|
||||
@@ -2478,7 +2487,7 @@ pub fn start_video_thread<F, T>(
|
||||
let record_permission = session.lc.read().unwrap().record_permission;
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
if record_state && record_permission {
|
||||
handler.record_screen(true, id, display);
|
||||
handler.record_screen(true, id, video_service_name.clone());
|
||||
}
|
||||
video_handler = Some(handler);
|
||||
}
|
||||
@@ -2559,7 +2568,7 @@ pub fn start_video_thread<F, T>(
|
||||
MediaData::RecordScreen(start) => {
|
||||
let id = session.lc.read().unwrap().id.clone();
|
||||
if let Some(handler) = video_handler.as_mut() {
|
||||
handler.record_screen(start, id, display);
|
||||
handler.record_screen(start, id, video_service_name.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -83,6 +83,7 @@ struct ParsedPeerInfo {
|
||||
platform: String,
|
||||
is_installed: bool,
|
||||
idd_impl: String,
|
||||
support_view_camera: bool,
|
||||
}
|
||||
|
||||
impl ParsedPeerInfo {
|
||||
@@ -129,7 +130,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let _file_clip_context_holder = {
|
||||
// `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.
|
||||
ContextSend::enable(true);
|
||||
Some(crate::SimpleCallOnReturn {
|
||||
@@ -152,6 +156,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let mut received = false;
|
||||
let conn_type = if self.handler.is_file_transfer() {
|
||||
ConnType::FILE_TRANSFER
|
||||
} else if self.handler.is_view_camera() {
|
||||
ConnType::VIEW_CAMERA
|
||||
} else {
|
||||
ConnType::default()
|
||||
};
|
||||
@@ -173,7 +179,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.set_connected();
|
||||
self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready
|
||||
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
|
||||
.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()
|
||||
|| self.handler.is_port_forward()
|
||||
|| self.handler.is_rdp();
|
||||
|| self.handler.is_rdp()
|
||||
|| self.handler.is_view_camera();
|
||||
if !is_conn_not_default {
|
||||
(self.client_conn_id, rx_clip_client_holder.0) =
|
||||
clipboard::get_rx_cliprdr_client(&self.handler.get_id());
|
||||
@@ -330,12 +337,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.set_disconnected(round);
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if _set_disconnected_ok {
|
||||
if !self.handler.is_view_camera() && _set_disconnected_ok {
|
||||
Client::try_stop_clipboard();
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&data) {
|
||||
match msg_in.union {
|
||||
@@ -1230,10 +1256,19 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let peer_version = pi.version.clone();
|
||||
let peer_platform = pi.platform.clone();
|
||||
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);
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
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(not(target_os = "ios"))]
|
||||
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) => {
|
||||
self.handler.set_permission("restart", p.enabled);
|
||||
}
|
||||
@@ -1773,6 +1811,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
self.peer_info.support_view_camera = platform_additions
|
||||
.get("support_view_camera")
|
||||
.map(|v| v.as_bool())
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
"--connect",
|
||||
"--play",
|
||||
"--file-transfer",
|
||||
"--view-camera",
|
||||
"--port-forward",
|
||||
"--rdp",
|
||||
]
|
||||
@@ -99,7 +100,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
}
|
||||
}
|
||||
#[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();
|
||||
}
|
||||
#[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![];
|
||||
while let Some(arg) = args.next() {
|
||||
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());
|
||||
id = args.next();
|
||||
}
|
||||
|
||||
@@ -1149,8 +1149,14 @@ pub fn session_add_existed(
|
||||
peer_id: String,
|
||||
session_id: SessionID,
|
||||
displays: Vec<i32>,
|
||||
is_view_camera: bool,
|
||||
) -> 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(())
|
||||
}
|
||||
|
||||
@@ -1160,11 +1166,13 @@ pub fn session_add_existed(
|
||||
///
|
||||
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
|
||||
/// * `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.
|
||||
pub fn session_add(
|
||||
session_id: &SessionID,
|
||||
id: &str,
|
||||
is_file_transfer: bool,
|
||||
is_view_camera: bool,
|
||||
is_port_forward: bool,
|
||||
is_rdp: bool,
|
||||
switch_uuid: &str,
|
||||
@@ -1175,6 +1183,8 @@ pub fn session_add(
|
||||
) -> ResultType<FlutterSession> {
|
||||
let conn_type = if is_file_transfer {
|
||||
ConnType::FILE_TRANSFER
|
||||
} else if is_view_camera {
|
||||
ConnType::VIEW_CAMERA
|
||||
} else if is_port_forward {
|
||||
if is_rdp {
|
||||
ConnType::RDP
|
||||
|
||||
@@ -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.
|
||||
pub fn peer_get_default_sessions_count(id: String) -> SyncReturn<usize> {
|
||||
SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN))
|
||||
pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn<usize> {
|
||||
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(
|
||||
id: String,
|
||||
session_id: SessionID,
|
||||
displays: Vec<i32>,
|
||||
is_view_camera: bool,
|
||||
) -> 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))
|
||||
} else {
|
||||
SyncReturn("".to_owned())
|
||||
@@ -112,6 +124,7 @@ pub fn session_add_sync(
|
||||
session_id: SessionID,
|
||||
id: String,
|
||||
is_file_transfer: bool,
|
||||
is_view_camera: bool,
|
||||
is_port_forward: bool,
|
||||
is_rdp: bool,
|
||||
switch_uuid: String,
|
||||
@@ -124,6 +137,7 @@ pub fn session_add_sync(
|
||||
&session_id,
|
||||
&id,
|
||||
is_file_transfer,
|
||||
is_view_camera,
|
||||
is_port_forward,
|
||||
is_rdp,
|
||||
&switch_uuid,
|
||||
|
||||
@@ -188,6 +188,7 @@ pub enum Data {
|
||||
Login {
|
||||
id: i32,
|
||||
is_file_transfer: bool,
|
||||
is_view_camera: bool,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
authorized: bool,
|
||||
@@ -1280,6 +1281,6 @@ mod test {
|
||||
#[test]
|
||||
fn verify_ffi_enum_data_size() {
|
||||
println!("{}", std::mem::size_of::<Data>());
|
||||
assert!(std::mem::size_of::<Data>() < 96);
|
||||
assert!(std::mem::size_of::<Data>() <= 96);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "عرض الكاميرا"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Прагляд камеры"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Преглед на камерата"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sense etiquetar"),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "无标签"),
|
||||
("new-version-of-{}-tip", "{} 版本更新"),
|
||||
("Accessible devices", "可访问的设备"),
|
||||
("View camera", "查看摄像头"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"),
|
||||
("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"),
|
||||
("Enable camera", "允许查看摄像头"),
|
||||
("No cameras", "没有摄像头"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Unmarkiert"),
|
||||
("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Χωρίς ετικέτα"),
|
||||
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
|
||||
("Accessible devices", "Προσβάσιμες συσκευές"),
|
||||
("View camera", "Προβολή κάμερας"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
("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"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Rigardi kameron"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sin itiquetar"),
|
||||
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Everyone", "Igaüks"),
|
||||
("ab_web_console_tip", "Rohkem leiad veebikonsoolist"),
|
||||
("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 window focus", "Jälgi kaugakna fookust"),
|
||||
("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"),
|
||||
("new-version-of-{}-tip", "Saadaval on {} uus versioon"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "نمایش دوربین"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً مشتری RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sans étiquette"),
|
||||
("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", ""),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Címkézetlen"),
|
||||
("new-version-of-{}-tip", "A(z) {} új verziója"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Senza tag"),
|
||||
("new-version-of-{}-tip", "È disponibile una nuova versione di {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "カメラを表示"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "태그 없음"),
|
||||
("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "카메라 보기"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "원격 측의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Камераны Көру"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Neatzīmēts"),
|
||||
("new-version-of-{}-tip", "Ir pieejama jauna {} versija"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Vis kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Ongemarkeerd"),
|
||||
("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Bez etykiety"),
|
||||
("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Ver câmara"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Vezi camera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Без метки"),
|
||||
("new-version-of-{}-tip", "Доступна новая версия {}"),
|
||||
("Accessible devices", "Доступные устройства"),
|
||||
("View camera", "Просмотр камеры"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удаленной стороне!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Chene tag"),
|
||||
("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Neoznačeno"),
|
||||
("new-version-of-{}-tip", "Na voljo je nova različica {}"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", ""),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Pregled kamere"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Visa kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", ""),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "ดูกล้อง"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "無標籤"),
|
||||
("new-version-of-{}-tip", "有新版本的 {} 可用"),
|
||||
("Accessible devices", "可存取的裝置"),
|
||||
("View camera", "檢視相機"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 用戶端升級到 {} 或更新版本!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Без міток"),
|
||||
("new-version-of-{}-tip", "Доступна нова версія {}"),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Перегляд камери"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Xem camera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ use hbb_common::{
|
||||
sodiumoxide::crypto::{box_, sign},
|
||||
timeout, tokio, ResultType, Stream,
|
||||
};
|
||||
use scrap::camera;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use service::ServiceTmpl;
|
||||
use service::{EmptyExtraFieldService, GenericService, Service, Subscriber};
|
||||
use video_service::VideoSource;
|
||||
|
||||
use crate::ipc::Data;
|
||||
|
||||
@@ -76,7 +78,6 @@ const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
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.)
|
||||
// for all initiative connections.
|
||||
//
|
||||
@@ -279,22 +280,53 @@ async fn create_relay_connection_(
|
||||
|
||||
impl Server {
|
||||
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) {
|
||||
let primary_video_service_name =
|
||||
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX);
|
||||
let primary_video_service_name = video_service::get_service_name(
|
||||
VideoSource::Monitor,
|
||||
*display_service::PRIMARY_DISPLAY_IDX,
|
||||
);
|
||||
if !self.contains(&primary_video_service_name) {
|
||||
self.add_service(Box::new(video_service::new(
|
||||
VideoSource::Monitor,
|
||||
*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>) {
|
||||
let primary_video_service_name =
|
||||
video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX);
|
||||
let primary_video_service_name = video_service::get_service_name(
|
||||
VideoSource::Monitor,
|
||||
*display_service::PRIMARY_DISPLAY_IDX,
|
||||
);
|
||||
for s in self.services.values() {
|
||||
let name = s.name();
|
||||
if Self::is_video_service_name(&name) && name != primary_video_service_name {
|
||||
@@ -307,7 +339,6 @@ impl Server {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.update_enable_retina();
|
||||
self.connections.insert(conn.id(), conn);
|
||||
*CONN_COUNT.lock().unwrap() = self.connections.len();
|
||||
}
|
||||
|
||||
pub fn remove_connection(&mut self, conn: &ConnInner) {
|
||||
@@ -315,7 +346,6 @@ impl Server {
|
||||
s.on_unsubscribe(conn.id());
|
||||
}
|
||||
self.connections.remove(&conn.id());
|
||||
*CONN_COUNT.lock().unwrap() = self.connections.len();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.update_enable_retina();
|
||||
}
|
||||
@@ -361,10 +391,15 @@ impl Server {
|
||||
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() {
|
||||
if let Some(display) = display {
|
||||
if k != &video_service::get_service_name(display) {
|
||||
if let Some((source, display)) = display {
|
||||
if k != &video_service::get_service_name(source, display) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -392,13 +427,14 @@ impl Server {
|
||||
fn capture_displays(
|
||||
&mut self,
|
||||
conn: ConnInner,
|
||||
source: VideoSource,
|
||||
displays: &[usize],
|
||||
include: bool,
|
||||
exclude: bool,
|
||||
) {
|
||||
let displays = displays
|
||||
.iter()
|
||||
.map(|d| video_service::get_service_name(*d))
|
||||
.map(|d| video_service::get_service_name(source, *d))
|
||||
.collect::<Vec<_>>();
|
||||
let keys = self.services.keys().cloned().collect::<Vec<_>>();
|
||||
for name in keys.iter() {
|
||||
|
||||
@@ -44,6 +44,7 @@ use hbb_common::{
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use scrap::android::{call_main_service_key_event, call_main_service_pointer_input};
|
||||
use scrap::camera;
|
||||
use serde_derive::Serialize;
|
||||
use serde_json::{json, value::Value};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -167,6 +168,7 @@ pub enum AuthConnType {
|
||||
Remote,
|
||||
FileTransfer,
|
||||
PortForward,
|
||||
ViewCamera,
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
@@ -179,6 +181,7 @@ pub struct Connection {
|
||||
timer: crate::RustDeskInterval,
|
||||
file_timer: crate::RustDeskInterval,
|
||||
file_transfer: Option<(String, bool)>,
|
||||
view_camera: bool,
|
||||
port_forward_socket: Option<Framed<TcpStream, BytesCodec>>,
|
||||
port_forward_address: String,
|
||||
tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
@@ -222,6 +225,7 @@ pub struct Connection {
|
||||
portable: PortableState,
|
||||
from_switch: bool,
|
||||
voice_call_request_timestamp: Option<NonZeroI64>,
|
||||
voice_calling: bool,
|
||||
options_in_login: Option<OptionMessage>,
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pressed_modifiers: HashSet<rdev::Key>,
|
||||
@@ -331,6 +335,7 @@ impl Connection {
|
||||
timer: crate::rustdesk_interval(time::interval(SEC30)),
|
||||
file_timer: crate::rustdesk_interval(time::interval(SEC30)),
|
||||
file_transfer: None,
|
||||
view_camera: false,
|
||||
port_forward_socket: None,
|
||||
port_forward_address: "".to_owned(),
|
||||
tx_to_cm,
|
||||
@@ -369,6 +374,7 @@ impl Connection {
|
||||
from_switch: false,
|
||||
audio_sender: None,
|
||||
voice_call_request_timestamp: None,
|
||||
voice_calling: false,
|
||||
options_in_login: None,
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pressed_modifiers: Default::default(),
|
||||
@@ -533,9 +539,17 @@ impl Connection {
|
||||
conn.send_permission(Permission::Audio, enabled).await;
|
||||
if conn.authorized {
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::audio_service::NAME,
|
||||
conn.inner.clone(), conn.audio_enabled());
|
||||
if conn.is_authed_view_camera_conn() {
|
||||
if conn.voice_calling || !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" {
|
||||
@@ -774,7 +788,7 @@ impl Connection {
|
||||
});
|
||||
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 {
|
||||
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)
|
||||
} else if self.port_forward_socket.is_some() {
|
||||
(2, AuthConnType::PortForward)
|
||||
} else if self.view_camera {
|
||||
(3, AuthConnType::ViewCamera)
|
||||
} else {
|
||||
(0, AuthConnType::Remote)
|
||||
};
|
||||
@@ -1277,6 +1293,11 @@ impl Connection {
|
||||
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"))]
|
||||
if !platform_additions.is_empty() {
|
||||
pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into());
|
||||
@@ -1290,7 +1311,8 @@ impl Connection {
|
||||
return;
|
||||
}
|
||||
#[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();
|
||||
if crate::platform::linux::is_login_screen_wayland() {
|
||||
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);
|
||||
if self.file_transfer.is_some() {
|
||||
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 {
|
||||
let supported_encoding = scrap::codec::Encoder::supported_encoding();
|
||||
self.last_supported_encoding = Some(supported_encoding.clone());
|
||||
@@ -1414,15 +1459,31 @@ impl Connection {
|
||||
} else {
|
||||
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 {
|
||||
if !wait_session_id_confirm {
|
||||
self.try_sub_services();
|
||||
self.try_sub_monitor_services();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_sub_services(&mut self) {
|
||||
let is_remote = self.file_transfer.is_none() && self.port_forward_socket.is_none();
|
||||
fn try_sub_camera_displays(&mut self) {
|
||||
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 {
|
||||
self.services_subed = true;
|
||||
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 crate::platform::is_installed()
|
||||
&& 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.iter().any(|e| e.sid == current_sid)
|
||||
&& 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 {
|
||||
id: self.inner.id(),
|
||||
is_file_transfer: self.file_transfer.is_some(),
|
||||
is_view_camera: self.view_camera,
|
||||
port_forward: self.port_forward_address.clone(),
|
||||
peer_id,
|
||||
name,
|
||||
@@ -1781,6 +1843,15 @@ impl Connection {
|
||||
}
|
||||
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)) => {
|
||||
if !Connection::permission("enable-tunnel") {
|
||||
self.send_login_error("No permission of IP tunneling").await;
|
||||
@@ -1987,6 +2058,9 @@ impl Connection {
|
||||
match msg.union {
|
||||
#[allow(unused_mut)]
|
||||
Some(message::Union::MouseEvent(mut me)) => {
|
||||
if self.is_authed_view_camera_conn() {
|
||||
return true;
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
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);
|
||||
@@ -2005,6 +2079,9 @@ impl Connection {
|
||||
self.update_auto_disconnect_timer();
|
||||
}
|
||||
Some(message::Union::PointerDeviceEvent(pde)) => {
|
||||
if self.is_authed_view_camera_conn() {
|
||||
return true;
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
if let Err(e) = match pde.union {
|
||||
Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union {
|
||||
@@ -2044,6 +2121,9 @@ impl Connection {
|
||||
Some(message::Union::KeyEvent(..)) => {}
|
||||
#[cfg(any(target_os = "android"))]
|
||||
Some(message::Union::KeyEvent(mut me)) => {
|
||||
if self.is_authed_view_camera_conn() {
|
||||
return true;
|
||||
}
|
||||
let key = match me.mode.enum_value() {
|
||||
Ok(KeyboardMode::Map) => {
|
||||
Some(crate::keyboard::keycode_to_rdev_key(me.chr()))
|
||||
@@ -2096,6 +2176,9 @@ impl Connection {
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Some(message::Union::KeyEvent(me)) => {
|
||||
if self.is_authed_view_camera_conn() {
|
||||
return true;
|
||||
}
|
||||
if self.peer_keyboard_enabled() {
|
||||
if is_enter(&me) {
|
||||
CLICK_TIME.store(get_time(), Ordering::SeqCst);
|
||||
@@ -2592,7 +2675,7 @@ impl Connection {
|
||||
let sessions = crate::platform::get_available_sessions(false);
|
||||
if crate::platform::is_installed()
|
||||
&& crate::platform::is_share_rdp()
|
||||
&& raii::AuthedConnID::remote_and_file_conn_count() == 1
|
||||
&& raii::AuthedConnID::non_port_forward_conn_count() == 1
|
||||
&& sessions.len() > 1
|
||||
&& current_process_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() {
|
||||
self.read_dir(&dir, show_hidden);
|
||||
}
|
||||
} else if self.view_camera {
|
||||
self.try_sub_camera_displays();
|
||||
} else {
|
||||
self.try_sub_services();
|
||||
self.try_sub_monitor_services();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(misc::Union::MessageQuery(mq)) => {
|
||||
if let Some(msg_out) =
|
||||
video_service::make_display_changed_msg(mq.switch_display as _, None)
|
||||
{
|
||||
if let Some(msg_out) = video_service::make_display_changed_msg(
|
||||
mq.switch_display as _,
|
||||
None,
|
||||
self.video_source(),
|
||||
) {
|
||||
self.send(msg_out).await;
|
||||
}
|
||||
}
|
||||
@@ -2713,7 +2800,7 @@ impl Connection {
|
||||
video_service::refresh();
|
||||
self.server.upgrade().map(|s| {
|
||||
s.read().unwrap().set_video_service_opt(
|
||||
display,
|
||||
display.map(|d| (self.video_source(), d)),
|
||||
video_service::OPTION_REFRESH,
|
||||
super::service::SERVICE_OPTION_VALUE_TRUE,
|
||||
);
|
||||
@@ -2743,19 +2830,33 @@ impl Connection {
|
||||
// 1. For compatibility with old versions ( < 1.2.4 ).
|
||||
// 2. Sciter version.
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>>) {
|
||||
let new_service_name = video_service::get_service_name(display_idx);
|
||||
let old_service_name = video_service::get_service_name(self.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.video_source(), self.display_idx);
|
||||
let mut lock = server.write().unwrap();
|
||||
if display_idx != *display_service::PRIMARY_DISPLAY_IDX {
|
||||
if !lock.contains(&new_service_name) {
|
||||
lock.add_service(Box::new(video_service::new(display_idx)));
|
||||
lock.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.
|
||||
@@ -2790,26 +2891,27 @@ impl Connection {
|
||||
}
|
||||
|
||||
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() {
|
||||
let mut lock = sever.write().unwrap();
|
||||
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) {
|
||||
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() {
|
||||
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) {
|
||||
lock.add_service(Box::new(video_service::new(*display)));
|
||||
lock.add_service(Box::new(video_service::new(video_source, *display)));
|
||||
}
|
||||
}
|
||||
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() {
|
||||
lock.capture_displays(self.inner.clone(), sub, false, true);
|
||||
lock.capture_displays(self.inner.clone(), video_source, sub, false, true);
|
||||
} 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;
|
||||
if self.follow_remote_window {
|
||||
@@ -2931,6 +3033,16 @@ impl Connection {
|
||||
self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
|
||||
}
|
||||
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 {
|
||||
log::warn!("Possible a voice call attack.");
|
||||
}
|
||||
@@ -2940,6 +3052,14 @@ impl Connection {
|
||||
crate::audio_service::set_voice_call_input_device(None, true);
|
||||
// Notify the connection manager that the voice call has been closed.
|
||||
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) {
|
||||
@@ -3016,11 +3136,21 @@ impl Connection {
|
||||
if q != BoolOption::NotSet {
|
||||
self.disable_audio = q == BoolOption::Yes;
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::audio_service::NAME,
|
||||
self.inner.clone(),
|
||||
self.audio_enabled(),
|
||||
);
|
||||
if self.is_authed_view_camera_conn() {
|
||||
if self.voice_calling || !self.audio_enabled() {
|
||||
s.write().unwrap().subscribe(
|
||||
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) {
|
||||
if self.portable.is_installed
|
||||
|| self.file_transfer.is_some()
|
||||
|| self.view_camera
|
||||
|| self.port_forward_socket.is_some()
|
||||
|| !self.keyboard
|
||||
{
|
||||
@@ -3463,6 +3594,13 @@ impl Connection {
|
||||
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")]
|
||||
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
|
||||
let is_stopping_allowed = clip.is_stopping_allowed();
|
||||
@@ -3966,7 +4104,6 @@ impl Retina {
|
||||
}
|
||||
|
||||
mod raii {
|
||||
// CONN_COUNT: remote connection count in fact
|
||||
// ALIVE_CONNS: all connections, including unauthorized connections
|
||||
// AUTHED_CONNS: all authorized connections
|
||||
|
||||
@@ -4001,7 +4138,7 @@ mod raii {
|
||||
_ONCE.call_once(|| {
|
||||
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
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -4024,12 +4161,12 @@ mod raii {
|
||||
.send((conn_count, remote_count)));
|
||||
}
|
||||
|
||||
pub fn remote_and_file_conn_count() -> usize {
|
||||
pub fn non_port_forward_conn_count() -> usize {
|
||||
AUTHED_CONNS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer)
|
||||
.filter(|c| c.1 != AuthConnType::PortForward)
|
||||
.count()
|
||||
}
|
||||
|
||||
@@ -4112,7 +4249,7 @@ mod raii {
|
||||
|
||||
impl Drop for AuthedConnID {
|
||||
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));
|
||||
video_service::VIDEO_QOS
|
||||
.lock()
|
||||
|
||||
@@ -404,6 +404,7 @@ fn no_displays(displays: &Vec<Display>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(windows))]
|
||||
pub fn try_get_displays() -> ResultType<Vec<Display>> {
|
||||
|
||||
@@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option<thread::JoinHandle<()>> {
|
||||
}
|
||||
|
||||
pub fn try_stop_record_cursor_pos() {
|
||||
let count_lock = CONN_COUNT.lock().unwrap();
|
||||
if *count_lock > 0 {
|
||||
let remote_count = AUTHED_CONNS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|c| c.1 == AuthConnType::Remote)
|
||||
.count();
|
||||
if remote_count > 0 {
|
||||
return;
|
||||
}
|
||||
RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst);
|
||||
|
||||
@@ -717,7 +717,7 @@ pub mod client {
|
||||
}
|
||||
let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
|
||||
let data = slice::from_raw_parts(frame_ptr, (*frame_info).length);
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
data,
|
||||
self.width,
|
||||
self.height,
|
||||
@@ -808,8 +808,13 @@ pub mod client {
|
||||
},
|
||||
ConnCount(None) => {
|
||||
if !quick_support {
|
||||
let cnt = crate::server::CONN_COUNT.lock().unwrap().clone();
|
||||
stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok();
|
||||
let remote_count = crate::server::AUTHED_CONNS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|c| c.1 == crate::server::AuthConnType::Remote)
|
||||
.count();
|
||||
stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok();
|
||||
}
|
||||
},
|
||||
WillClose => {
|
||||
|
||||
@@ -106,7 +106,7 @@ pub struct VideoQoS {
|
||||
fps: u32,
|
||||
ratio: f32,
|
||||
users: HashMap<i32, UserData>,
|
||||
displays: HashMap<usize, DisplayData>,
|
||||
displays: HashMap<String, DisplayData>,
|
||||
bitrate_store: u32,
|
||||
adjust_ratio_instant: Instant,
|
||||
abr_config: bool,
|
||||
@@ -168,8 +168,8 @@ impl VideoQoS {
|
||||
self.users.iter().any(|u| u.1.record)
|
||||
}
|
||||
|
||||
pub fn set_support_changing_quality(&mut self, display_idx: usize, support: bool) {
|
||||
if let Some(display) = self.displays.get_mut(&display_idx) {
|
||||
pub fn set_support_changing_quality(&mut self, video_service_name: &str, support: bool) {
|
||||
if let Some(display) = self.displays.get_mut(video_service_name) {
|
||||
display.support_changing_quality = support;
|
||||
}
|
||||
}
|
||||
@@ -346,16 +346,17 @@ impl VideoQoS {
|
||||
|
||||
// Common adjust functions
|
||||
impl VideoQoS {
|
||||
pub fn new_display(&mut self, display_idx: usize) {
|
||||
self.displays.insert(display_idx, DisplayData::default());
|
||||
pub fn new_display(&mut self, video_service_name: String) {
|
||||
self.displays
|
||||
.insert(video_service_name, DisplayData::default());
|
||||
}
|
||||
|
||||
pub fn remove_display(&mut self, display_idx: usize) {
|
||||
self.displays.remove(&display_idx);
|
||||
pub fn remove_display(&mut self, video_service_name: &str) {
|
||||
self.displays.remove(video_service_name);
|
||||
}
|
||||
|
||||
pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) {
|
||||
if let Some(display) = self.displays.get_mut(&display_idx) {
|
||||
pub fn update_display_data(&mut self, video_service_name: &str, send_counter: usize) {
|
||||
if let Some(display) = self.displays.get_mut(video_service_name) {
|
||||
display.send_counter += send_counter;
|
||||
}
|
||||
self.adjust_fps();
|
||||
|
||||
@@ -18,12 +18,7 @@
|
||||
// to-do:
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
|
||||
use super::{
|
||||
display_service::{check_display_changed, get_display_info},
|
||||
service::ServiceTmpl,
|
||||
video_qos::VideoQoS,
|
||||
*,
|
||||
};
|
||||
use super::{display_service::check_display_changed, service::ServiceTmpl, video_qos::VideoQoS, *};
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::common::SimpleCallOnReturn;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -65,7 +60,6 @@ use std::{
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
|
||||
pub const NAME: &'static str = "video";
|
||||
pub const OPTION_REFRESH: &'static str = "refresh";
|
||||
|
||||
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)]
|
||||
pub struct VideoService {
|
||||
sp: GenericService,
|
||||
idx: usize,
|
||||
source: VideoSource,
|
||||
}
|
||||
|
||||
impl Deref for VideoService {
|
||||
@@ -153,14 +171,15 @@ impl DerefMut for VideoService {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_service_name(idx: usize) -> String {
|
||||
format!("{}{}", NAME, idx)
|
||||
pub fn get_service_name(source: VideoSource, idx: usize) -> String {
|
||||
format!("{}{}", source.service_name_prefix(), idx)
|
||||
}
|
||||
|
||||
pub fn new(idx: usize) -> GenericService {
|
||||
pub fn new(source: VideoSource, idx: usize) -> GenericService {
|
||||
let vs = VideoService {
|
||||
sp: GenericService::new(get_service_name(idx), true),
|
||||
sp: GenericService::new(get_service_name(source, idx), true),
|
||||
idx,
|
||||
source,
|
||||
};
|
||||
GenericService::run(&vs, run);
|
||||
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")]
|
||||
{
|
||||
if !is_x11() {
|
||||
@@ -309,6 +331,7 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType<Ca
|
||||
ndisplay
|
||||
);
|
||||
}
|
||||
|
||||
let display = displays.remove(current);
|
||||
|
||||
#[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<()> {
|
||||
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.
|
||||
//
|
||||
// ensure_inited() is needed because clear() may be called.
|
||||
@@ -406,7 +480,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
|
||||
let display_idx = vs.idx;
|
||||
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)]
|
||||
if !scrap::codec::enable_directx_capture() && !c.is_gdi() {
|
||||
log::info!("disable dxgi with option, fall back to gdi");
|
||||
@@ -423,11 +497,12 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
drop(video_qos);
|
||||
let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder(
|
||||
&c,
|
||||
display_idx,
|
||||
sp.name(),
|
||||
quality,
|
||||
client_record,
|
||||
record_incoming,
|
||||
last_portable_service_running,
|
||||
vs.source,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
@@ -441,26 +516,29 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
}));
|
||||
setup_encoder(
|
||||
&c,
|
||||
display_idx,
|
||||
sp.name(),
|
||||
quality,
|
||||
client_record,
|
||||
record_incoming,
|
||||
last_portable_service_running,
|
||||
vs.source,
|
||||
)?
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "vram")]
|
||||
c.set_output_texture(encoder.input_texture());
|
||||
#[cfg(target_os = "android")]
|
||||
if let Err(e) = check_change_scale(encoder.is_hardware()) {
|
||||
try_broadcast_display_changed(&sp, display_idx, &c, true).ok();
|
||||
bail!(e);
|
||||
if vs.source.is_monitor() {
|
||||
if let Err(e) = check_change_scale(encoder.is_hardware()) {
|
||||
try_broadcast_display_changed(&sp, display_idx, &c, true).ok();
|
||||
bail!(e);
|
||||
}
|
||||
}
|
||||
VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate());
|
||||
VIDEO_QOS
|
||||
.lock()
|
||||
.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:?}");
|
||||
|
||||
if sp.is_option_true(OPTION_REFRESH) {
|
||||
@@ -500,10 +578,12 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
client_record,
|
||||
&mut send_counter,
|
||||
&mut second_instant,
|
||||
display_idx,
|
||||
&sp.name(),
|
||||
)?;
|
||||
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");
|
||||
bail!("SWITCH");
|
||||
}
|
||||
@@ -527,10 +607,12 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
#[cfg(all(windows, feature = "vram"))]
|
||||
if c.is_gdi() && encoder.input_texture() {
|
||||
log::info!("changed to gdi when using vram");
|
||||
VRamEncoder::set_fallback_gdi(display_idx, true);
|
||||
VRamEncoder::set_fallback_gdi(sp.name(), true);
|
||||
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)]
|
||||
{
|
||||
if crate::platform::windows::desktop_changed()
|
||||
@@ -540,7 +622,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
}
|
||||
}
|
||||
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;
|
||||
// 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.
|
||||
@@ -575,7 +657,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
{
|
||||
#[cfg(feature = "vram")]
|
||||
if try_gdi == 1 && !c.is_gdi() {
|
||||
VRamEncoder::set_fallback_gdi(display_idx, false);
|
||||
VRamEncoder::set_fallback_gdi(sp.name(), false);
|
||||
}
|
||||
try_gdi = 0;
|
||||
}
|
||||
@@ -635,7 +717,9 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
Err(err) => {
|
||||
// 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.
|
||||
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)]
|
||||
if !c.is_gdi() {
|
||||
@@ -657,7 +741,9 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
let timeout_millis = 3_000u64;
|
||||
let wait_begin = Instant::now();
|
||||
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);
|
||||
// break if all connections have received current frame
|
||||
if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() {
|
||||
@@ -676,32 +762,35 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Raii(usize);
|
||||
struct Raii(String);
|
||||
|
||||
impl Raii {
|
||||
fn new(display_idx: usize) -> Self {
|
||||
VIDEO_QOS.lock().unwrap().new_display(display_idx);
|
||||
Raii(display_idx)
|
||||
fn new(name: String) -> Self {
|
||||
log::info!("new video service: {}", name);
|
||||
VIDEO_QOS.lock().unwrap().new_display(name.clone());
|
||||
Raii(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Raii {
|
||||
fn drop(&mut self) {
|
||||
log::info!("stop video service: {}", self.0);
|
||||
#[cfg(feature = "vram")]
|
||||
VRamEncoder::set_not_use(self.0, false);
|
||||
VRamEncoder::set_not_use(self.0.clone(), false);
|
||||
#[cfg(feature = "vram")]
|
||||
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(
|
||||
c: &CapturerInfo,
|
||||
display_idx: usize,
|
||||
name: String,
|
||||
quality: f32,
|
||||
client_record: bool,
|
||||
record_incoming: bool,
|
||||
last_portable_service_running: bool,
|
||||
source: VideoSource,
|
||||
) -> ResultType<(
|
||||
Encoder,
|
||||
EncoderCfg,
|
||||
@@ -711,14 +800,15 @@ fn setup_encoder(
|
||||
)> {
|
||||
let encoder_cfg = get_encoder_config(
|
||||
&c,
|
||||
display_idx,
|
||||
name.to_string(),
|
||||
quality,
|
||||
client_record || record_incoming,
|
||||
last_portable_service_running,
|
||||
source,
|
||||
);
|
||||
Encoder::set_fallback(&encoder_cfg);
|
||||
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 encoder = Encoder::new(encoder_cfg.clone(), use_i444)?;
|
||||
Ok((encoder, encoder_cfg, codec_format, use_i444, recorder))
|
||||
@@ -726,15 +816,16 @@ fn setup_encoder(
|
||||
|
||||
fn get_encoder_config(
|
||||
c: &CapturerInfo,
|
||||
_display_idx: usize,
|
||||
_name: String,
|
||||
quality: f32,
|
||||
record: bool,
|
||||
_portable_service: bool,
|
||||
_source: VideoSource,
|
||||
) -> EncoderCfg {
|
||||
#[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);
|
||||
VRamEncoder::set_not_use(_display_idx, true);
|
||||
VRamEncoder::set_not_use(_name, true);
|
||||
}
|
||||
#[cfg(feature = "vram")]
|
||||
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)]
|
||||
let root = crate::platform::is_root();
|
||||
#[cfg(not(windows))]
|
||||
@@ -819,7 +910,7 @@ fn get_recorder(record_incoming: bool, display: usize) -> Arc<Mutex<Option<Recor
|
||||
server: true,
|
||||
id: Config::get_id(),
|
||||
dir: crate::ui_interface::video_save_directory(root),
|
||||
display,
|
||||
video_service_name,
|
||||
tx,
|
||||
})
|
||||
.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),
|
||||
) {
|
||||
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);
|
||||
sp.send_shared(msg_out.clone());
|
||||
// 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(
|
||||
display_idx: usize,
|
||||
opt_display: Option<DisplayInfo>,
|
||||
source: VideoSource,
|
||||
) -> Option<Message> {
|
||||
let display = match opt_display {
|
||||
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();
|
||||
misc.set_switch_display(SwitchDisplay {
|
||||
@@ -1033,13 +1132,24 @@ pub fn make_display_changed_msg(
|
||||
y: display.y,
|
||||
width: display.width,
|
||||
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"))]
|
||||
resolutions: Some(SupportedResolutions {
|
||||
resolutions: if display.name.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
crate::platform::resolutions(&display.name)
|
||||
resolutions: match source {
|
||||
VideoSource::Monitor => {
|
||||
if display.name.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
crate::platform::resolutions(&display.name)
|
||||
}
|
||||
}
|
||||
VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
..SupportedResolutions::default()
|
||||
})
|
||||
@@ -1059,7 +1169,7 @@ fn check_qos(
|
||||
client_record: bool,
|
||||
send_counter: &mut usize,
|
||||
second_instant: &mut Instant,
|
||||
display_idx: usize,
|
||||
name: &str,
|
||||
) -> ResultType<()> {
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
*spf = video_qos.spf();
|
||||
@@ -1082,7 +1192,7 @@ fn check_qos(
|
||||
}
|
||||
if second_instant.elapsed() > Duration::from_secs(1) {
|
||||
*second_instant = Instant::now();
|
||||
video_qos.update_display_data(display_idx, *send_counter);
|
||||
video_qos.update_display_data(&name, *send_counter);
|
||||
*send_counter = 0;
|
||||
}
|
||||
drop(video_qos);
|
||||
|
||||
@@ -19,6 +19,7 @@ impl InvokeUiCM for SciterHandler {
|
||||
&make_args!(
|
||||
client.id,
|
||||
client.is_file_transfer,
|
||||
client.is_view_camera,
|
||||
client.port_forward.clone(),
|
||||
client.peer_id.clone(),
|
||||
client.name.clone(),
|
||||
|
||||
@@ -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);
|
||||
var conn;
|
||||
connections.map(function(c) {
|
||||
@@ -550,7 +550,7 @@ function adjustHeader() {
|
||||
|
||||
view.on("size", adjustHeader);
|
||||
|
||||
// handler.addConnection(0, false, 0, "", "test1", true, false, false, true, true);
|
||||
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false, false);
|
||||
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false, false);
|
||||
// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true);
|
||||
// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false);
|
||||
// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false);
|
||||
// handler.newMessage(0, 'h');
|
||||
|
||||
@@ -316,6 +316,7 @@ impl InvokeUiSession for SciterHandler {
|
||||
ConnType::RDP => {}
|
||||
ConnType::PORT_FORWARD => {}
|
||||
ConnType::FILE_TRANSFER => {}
|
||||
ConnType::VIEW_CAMERA => {}
|
||||
ConnType::DEFAULT_CONN => {
|
||||
crate::keyboard::client::start_grab_loop();
|
||||
}
|
||||
@@ -557,6 +558,8 @@ impl SciterSession {
|
||||
|
||||
let conn_type = if cmd.eq("--file-transfer") {
|
||||
ConnType::FILE_TRANSFER
|
||||
} else if cmd.eq("--view-camera") {
|
||||
ConnType::VIEW_CAMERA
|
||||
} else if cmd.eq("--port-forward") {
|
||||
ConnType::PORT_FORWARD
|
||||
} else if cmd.eq("--rdp") {
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct Client {
|
||||
pub authorized: bool,
|
||||
pub disconnected: bool,
|
||||
pub is_file_transfer: bool,
|
||||
pub is_view_camera: bool,
|
||||
pub port_forward: String,
|
||||
pub name: String,
|
||||
pub peer_id: String,
|
||||
@@ -128,6 +129,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
&self,
|
||||
id: i32,
|
||||
is_file_transfer: bool,
|
||||
is_view_camera: bool,
|
||||
port_forward: String,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
@@ -147,6 +149,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
authorized,
|
||||
disconnected: false,
|
||||
is_file_transfer,
|
||||
is_view_camera,
|
||||
port_forward,
|
||||
name: name.clone(),
|
||||
peer_id: peer_id.clone(),
|
||||
@@ -402,9 +405,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
}
|
||||
Ok(Some(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);
|
||||
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;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -672,6 +675,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
Some(Data::Login {
|
||||
id,
|
||||
is_file_transfer,
|
||||
is_view_camera,
|
||||
port_forward,
|
||||
peer_id,
|
||||
name,
|
||||
@@ -690,6 +694,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
cm.add_connection(
|
||||
id,
|
||||
is_file_transfer,
|
||||
is_view_camera,
|
||||
port_forward,
|
||||
peer_id,
|
||||
name,
|
||||
|
||||
@@ -190,6 +190,10 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
.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 {
|
||||
let conn_type = self.lc.read().unwrap().conn_type;
|
||||
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() {
|
||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
||||
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;
|
||||
}
|
||||
self.try_change_init_resolution(pi.current_display);
|
||||
|
||||
Reference in New Issue
Block a user