view camera (#11040)

* view camera

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

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

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

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

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Adwin White <adwinw01@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
21pages
2025-03-10 21:06:53 +08:00
committed by GitHub
parent df4a101316
commit f0f999dc27
96 changed files with 3999 additions and 458 deletions
Generated
+378 -109
View File
File diff suppressed because it is too large Load Diff
+72 -6
View File
@@ -29,8 +29,10 @@ import '../consts.dart';
import 'common/widgets/overlay.dart';
import '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 {
+18
View File
@@ -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);
}
+48 -1
View File
@@ -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,
),
);
}
}
+48 -27
View File
@@ -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 {
+4
View File
@@ -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";
+61 -16
View File
@@ -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());
});
+138 -92
View File
@@ -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'),
)
],
),
),
],
@@ -0,0 +1,730 @@
import 'dart:async';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart';
import '../../common/widgets/overlay.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/toolbar.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
// Used to skip session close if "move to new window" is clicked.
final Map<String, bool> closeSessionOnDispose = {};
class ViewCameraPage extends StatefulWidget {
ViewCameraPage({
Key? key,
required this.id,
required this.toolbarState,
this.sessionId,
this.tabWindowId,
this.password,
this.display,
this.displays,
this.tabController,
this.connToken,
this.forceRelay,
this.isSharedPassword,
}) : super(key: key) {
initSharedStates(id);
}
final String id;
final SessionID? sessionId;
final int? tabWindowId;
final int? display;
final List<int>? displays;
final String? password;
final ToolbarState toolbarState;
final bool? forceRelay;
final bool? isSharedPassword;
final String? connToken;
final SimpleWrapper<State<ViewCameraPage>?> _lastState = SimpleWrapper(null);
final DesktopTabController? tabController;
FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
@override
State<ViewCameraPage> createState() {
final state = _ViewCameraPageState(id);
_lastState.value = state;
return state;
}
}
class _ViewCameraPageState extends State<ViewCameraPage>
with AutomaticKeepAliveClientMixin, MultiWindowListener {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
final _cursorOverImage = false.obs;
var _blockableOverlayState = BlockableOverlayState();
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
// to identify the toolbar instance and its callback function.
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
SessionID get sessionId => _ffi.sessionId;
_ViewCameraPageState(String id) {
_initStates(id);
}
void _initStates(String id) {}
@override
void initState() {
super.initState();
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.start(
widget.id,
isViewCamera: true,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
display: widget.display,
displays: widget.displays,
connToken: widget.connToken,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
if (!isLinux) {
WakelockPlus.enable();
}
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
// (String? lastKey, String? currentKey) async {
// if (_firstEnterImage.value) {
// _firstEnterImage.value = false;
// return true;
// }
// return lastKey == null || lastKey != currentKey;
// });
// _isCustomCursorInited = true;
// }
_blockableOverlayState.applyFfi(_ffi);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
}
@override
void onWindowBlur() {
super.onWindowBlur();
// On windows, we use `focus` way to handle keyboard better.
// Now on Linux, there's some rdev issues which will break the input.
// We disable the `focus` way for non-Windows temporarily.
if (isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
stateGlobal.isFocused.value = false;
}
@override
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (isWindows) {
_isWindowBlur = false;
}
stateGlobal.isFocused.value = true;
}
@override
void onWindowRestore() {
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (isWindows) {
_isWindowBlur = false;
}
if (!isLinux) {
WakelockPlus.enable();
}
}
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
@override
void onWindowMaximize() {
super.onWindowMaximize();
if (!isLinux) {
WakelockPlus.enable();
}
}
@override
void onWindowMinimize() {
super.onWindowMinimize();
if (!isLinux) {
WakelockPlus.disable();
}
}
@override
void onWindowEnterFullScreen() {
super.onWindowEnterFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(true);
}
}
@override
void onWindowLeaveFullScreen() {
super.onWindowLeaveFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(false);
}
}
@override
Future<void> dispose() async {
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession);
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();
if (closeSession) {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
}
if (!isLinux) {
await WakelockPlus.disable();
}
await Get.delete<FFI>(tag: widget.id);
removeSharedStates(widget.id);
}
Widget emptyOverlay() => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: Colors.transparent,
),
);
Widget buildBody(BuildContext context) {
remoteToolbar(BuildContext context) => RemoteToolbar(
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (id, func) {
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
_onEnterOrLeaveImage4Toolbar = func;
},
onEnterOrLeaveImageCleaner: (id) {
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
_onEnterOrLeaveImage4Toolbar = null;
}
},
setRemoteState: setState,
);
bodyWidget() {
return Stack(
children: [
Container(
color: kColorCanvas,
child: getBodyForDesktop(context),
),
Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage();
} else {
return Obx(() => Offstage(
offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse,
child: Overlay(initialEntries: [
makeMobileActionsOverlayEntry(
() => _ffi.dialogManager
.setMobileActionsOverlayVisible(false),
ffi: _ffi,
)
]),
));
}
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(
initialEntries: [OverlayEntry(builder: remoteToolbar)])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
),
],
);
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Obx(() {
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isFalse;
if (imageReady) {
// If the privacy mode(disable physical displays) is switched,
// we should not dismiss the dialog immediately.
if (DateTime.now().difference(togglePrivacyModeTime) >
const Duration(milliseconds: 3000)) {
// `dismissAll()` is to ensure that the state is clean.
// It's ok to call dismissAll() here.
_ffi.dialogManager.dismissAll();
// Recreate the block state to refresh the state.
_blockableOverlayState = BlockableOverlayState();
_blockableOverlayState.applyFfi(_ffi);
}
// Block the whole `bodyWidget()` when dialog shows.
return BlockableOverlay(
underlying: bodyWidget(),
state: _blockableOverlayState,
);
} else {
// `_blockableOverlayState` is not recreated here.
// The toolbar's block state won't work properly when reconnecting, but that's okay.
return bodyWidget();
}
}),
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return WillPopScope(
onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager);
return false;
},
child: MultiProvider(providers: [
ChangeNotifierProvider.value(value: _ffi.ffiModel),
ChangeNotifierProvider.value(value: _ffi.imageModel),
ChangeNotifierProvider.value(value: _ffi.cursorModel),
ChangeNotifierProvider.value(value: _ffi.canvasModel),
ChangeNotifierProvider.value(value: _ffi.recordingModel),
], child: buildBody(context)));
}
void enterView(PointerEnterEvent evt) {
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(true);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
_ffi.inputModel.enterOrLeave(true);
}
}
void leaveView(PointerExitEvent evt) {
if (_ffi.ffiModel.keyboard) {
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
}
_cursorOverImage.value = false;
_firstEnterImage.value = false;
if (_onEnterOrLeaveImage4Toolbar != null) {
try {
_onEnterOrLeaveImage4Toolbar!(false);
} catch (e) {
//
}
}
// See [onWindowBlur].
if (!isWindows) {
_ffi.inputModel.enterOrLeave(false);
}
}
Widget _buildRawTouchAndPointerRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return RawTouchGestureDetectorRegion(
child: _buildRawPointerMouseRegion(child, onEnter, onExit),
ffi: _ffi,
isCamera: true,
);
}
Widget _buildRawPointerMouseRegion(
Widget child,
PointerEnterEventListener? onEnter,
PointerExitEventListener? onExit,
) {
return CameraRawPointerMouseRegion(
onEnter: onEnter,
onExit: onExit,
onPointerDown: (event) {
// A double check for blur status.
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
// Sometimes the system does not send the necessary focus event to flutter. We should manually
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
// ensure the grab-key thread is running when our users are clicking the remote canvas.
if (_isWindowBlur) {
debugPrint(
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
_isWindowBlur = false;
}
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
},
inputModel: _ffi.inputModel,
child: child,
);
}
Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[
MouseRegion(onEnter: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) {
final c = Provider.of<CanvasModel>(context, listen: false);
Future.delayed(Duration.zero, () => c.updateViewStyle());
final peerDisplay = CurrentDisplayState.find(widget.id);
return Obx(
() => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent)
: Obx(() {
widget.toolbarState.initShow(sessionId);
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
return ImagePaint(
id: widget.id,
cursorOverImage: _cursorOverImage,
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
child, enterView, leaveView),
ffi: _ffi,
);
}),
);
}))
];
paints.add(
Positioned(
top: 10,
right: 10,
child: _buildRawTouchAndPointerRegion(
QualityMonitor(_ffi.qualityMonitorModel), null, null),
),
);
return Stack(
children: paints,
);
}
@override
bool get wantKeepAlive => true;
}
class ImagePaint extends StatefulWidget {
final FFI ffi;
final String id;
final RxBool cursorOverImage;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
{Key? key,
required this.ffi,
required this.id,
required this.cursorOverImage,
this.listenerBuilder})
: super(key: key);
@override
State<StatefulWidget> createState() => _ImagePaintState();
}
class _ImagePaintState extends State<ImagePaint> {
bool _lastRemoteCursorMoved = false;
String get id => widget.id;
RxBool get cursorOverImage => widget.cursorOverImage;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context);
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
c.updateScrollPercent();
return false;
},
child: Container(
child: _buildCrossScrollbarFromLayout(
context,
_buildListener(paintWidget),
c.size,
paintSize,
c.scrollHorizontal,
c.scrollVertical,
)),
);
} else {
if (c.size.width > 0 && c.size.height > 0) {
final paintWidget =
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
? _BuildPaintTextureRender(
c,
s,
Offset(
isLinux ? c.x.toInt().toDouble() : c.x,
isLinux ? c.y.toInt().toDouble() : c.y,
),
c.size,
isViewOriginal())
: _buildScrollAutoNonTextureRender(m, c, s);
return Container(child: _buildListener(paintWidget));
} else {
return Container();
}
}
}
Widget _buildScrollbarNonTextureRender(
ImageModel m, Size imageSize, double s) {
return CustomPaint(
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
}
Widget _buildScrollAutoNonTextureRender(
ImageModel m, CanvasModel c, double s) {
return CustomPaint(
size: Size(c.size.width, c.size.height),
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
);
}
Widget _BuildPaintTextureRender(
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
final ffiModel = c.parent.target!.ffiModel;
final displays = ffiModel.pi.getCurDisplays();
final children = <Widget>[];
final rect = ffiModel.rect;
if (rect == null) {
return Container();
}
final curDisplay = ffiModel.pi.currentDisplay;
for (var i = 0; i < displays.length; i++) {
final textureId = widget.ffi.textureModel
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
if (true) {
// both "textureId.value != -1" and "true" seems ok
children.add(Positioned(
left: (displays[i].x - rect.left) * s + offset.dx,
top: (displays[i].y - rect.top) * s + offset.dy,
width: displays[i].width * s,
height: displays[i].height * s,
child: Obx(() => Texture(
textureId: textureId.value,
filterQuality:
isViewOriginal ? FilterQuality.none : FilterQuality.low,
)),
));
}
}
return SizedBox(
width: size.width,
height: size.height,
child: Stack(children: children),
);
}
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = cursor.cache ?? preDefaultCursor.cache;
return buildCursorOfCache(cursor, scale, cache);
}
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
final cursor = Provider.of<CursorModel>(context);
final cache = preForbiddenCursor.cache;
return buildCursorOfCache(cursor, scale, cache);
}
Widget _buildCrossScrollbarFromLayout(
BuildContext context,
Widget child,
Size layoutSize,
Size size,
ScrollController horizontal,
ScrollController vertical,
) {
var widget = child;
if (layoutSize.width < size.width) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: horizontal,
scrollDirection: Axis.horizontal,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Row(
children: [
Container(
width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.height < size.height) {
widget = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: vertical,
physics: cursorOverImage.isTrue
? const NeverScrollableScrollPhysics()
: null,
child: widget,
),
);
} else {
widget = Column(
children: [
Container(
height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
),
widget,
],
);
}
if (layoutSize.width < size.width) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: horizontal,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: layoutSize.height < size.height
? (notification) => notification.depth == 1
: defaultScrollNotificationPredicate,
child: widget,
);
}
if (layoutSize.height < size.height) {
widget = RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: vertical,
thumbVisibility: false,
trackVisibility: false,
child: widget,
);
}
return Container(
child: widget,
width: layoutSize.width,
height: layoutSize.height,
);
}
Widget _buildListener(Widget child) {
if (listenerBuilder != null) {
return listenerBuilder!(child);
} else {
return child;
}
}
}
@@ -0,0 +1,499 @@
import 'dart:convert';
import 'dart:async';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
static const Color blueColor = MyTheme.button;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
}
class ViewCameraTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const ViewCameraTabPage({Key? key, required this.params}) : super(key: key);
@override
State<ViewCameraTabPage> createState() => _ViewCameraTabPageState(params);
}
class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
final tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera));
final contentKey = UniqueKey();
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
String? peerId;
bool _isScreenRectSet = false;
int? _display;
var connectionMap = RxList<Widget>.empty(growable: true);
_ViewCameraTabPageState(Map<String, dynamic> params) {
RemoteCountState.init();
peerId = params['id'];
final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
final screenRect = parseParamScreenRect(params);
_isScreenRectSet = screenRect != null;
_display = display as int?;
tryMoveToScreenAndSetFullscreen(screenRect);
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
final viewCameraPage = tabController.widget(id);
if (viewCameraPage is ViewCameraPage) {
final ffi = viewCameraPage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId);
}
WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0;
};
tabController.add(TabInfo(
key: peerId!,
label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
page: ViewCameraPage(
key: ValueKey(peerId),
id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: params['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: params['connToken'],
forceRelay: params['forceRelay'],
isSharedPassword: params['isSharedPassword'],
),
));
_update_remote_count();
}
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
}
@override
void initState() {
super.initState();
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.ViewCamera,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
display: _display,
);
});
}
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
bool secure =
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
fingerprint = 'N/A';
}
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final tab = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4),
],
);
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
},
target: e.position,
);
}
},
child: tab,
);
}
}),
),
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: workaroundWindowBorder(
context,
Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
)));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
key: contentKey,
child: tabWidget,
// Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
));
}
// Note: Some dup code to ../widgets/remote_toolbar
Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> menu = [];
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final sessionId = ffi.sessionId;
final toolbarState = viewCameraPage.toolbarState;
menu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
style: style,
)),
proc: () {
toolbarState.switchShow(sessionId);
cancelFunc();
},
padding: padding,
),
]);
if (tabController.state.value.tabs.length > 1) {
final splitAction = MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Move tab to new window'),
style: style,
),
proc: () async {
await DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventMoveTabToNewWindow,
'${windowId()},$key,$sessionId,ViewCamera');
cancelFunc();
},
padding: padding,
);
menu.insert(1, splitAction);
}
menu.addAll([
MenuEntryDivider<String>(),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Copy Fingerprint'),
style: style,
),
proc: () => onCopyFingerprint(FingerprintState.find(key).value),
padding: padding,
dismissOnClicked: true,
dismissCallback: cancelFunc,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Close'),
style: style,
),
proc: () {
tabController.closeBy(key);
cancelFunc();
},
padding: padding,
)
]);
return mod_menu.PopupMenu<String>(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);
_update_remote_count();
}
int windowId() {
return widget.params["windowId"];
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length;
if (connLength <= 1) {
tabController.clear();
return true;
} else {
final bool res;
if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
}
return res;
}
}
_update_remote_count() =>
RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
debugPrint(
"[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewViewCamera) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, prePeerCount,
WindowType.ViewCamera, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
});
ConnectionTypeState.init(id);
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: ViewCameraPage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: ToolbarState(),
tabController: tabController,
connToken: args['connToken'],
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk =
tabController.jumpToByKeyAndDisplay(id, display, isCamera: true);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final viewCameraPage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as ViewCameraPage;
returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
} else if (call.method == kWindowEventRemoteWindowCoords) {
final viewCameraPage =
tabController.state.value.selectedTabInfo.page as ViewCameraPage;
final ffi = viewCameraPage.ffi;
final displayRect = ffi.ffiModel.displaysRect();
if (displayRect != null) {
final wc = WindowController.fromWindowId(windowId());
Rect? frame;
try {
frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
}
if (frame != null) {
ffi.cursorModel.moveLocal(0, 0);
final coords = RemoteWindowCoords(
frame,
CanvasCoords.fromCanvasModel(ffi.canvasModel),
CursorCoords.fromCursorModel(ffi.cursorModel),
displayRect);
returnValue = jsonEncode(coords.toJson());
}
}
} else if (call.method == kWindowEventSetFullscreen) {
stateGlobal.setFullscreen(call.arguments == 'true');
}
_update_remote_count();
return returnValue;
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
class DesktopViewCameraScreen extends StatelessWidget {
final Map<String, dynamic> params;
DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) {
bind.mainInitInputSource();
stateGlobal.getInputSource(force: true);
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
child: Scaffold(
// Set transparent background for padding the resize area out of the flutter view.
// This allows the wallpaper goes through our resize area. (Linux only now).
backgroundColor: isLinux ? Colors.transparent : null,
body: ViewCameraTabPage(
params: params,
),
));
}
}
+18 -10
View File
@@ -478,7 +478,10 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
state: widget.state,
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);
}
+27
View File
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/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;
+7 -1
View File
@@ -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);
}
}
}
@@ -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) {
+18
View File
@@ -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)) {
+37 -9
View File
@@ -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;
+8 -1
View File
@@ -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 {
+68 -18
View File
@@ -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;
+6 -2
View File
@@ -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
})
]);
}
+4 -4
View File
@@ -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
+1
View File
@@ -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
View File
@@ -0,0 +1,232 @@
use std::{
io,
sync::{Arc, Mutex},
};
use nokhwa::{
pixel_format::RgbAFormat,
query,
utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType},
Camera,
};
use hbb_common::message_proto::{DisplayInfo, Resolution};
#[cfg(feature = "vram")]
use crate::AdapterDevice;
use crate::common::{bail, ResultType};
use crate::{Frame, PixelBuffer, Pixfmt, TraitCapturer};
pub const PRIMARY_CAMERA_IDX: usize = 0;
lazy_static::lazy_static! {
static ref SYNC_CAMERA_DISPLAYS: Arc<Mutex<Vec<DisplayInfo>>> = Arc::new(Mutex::new(Vec::new()));
}
pub struct Cameras;
// pre-condition
pub fn primary_camera_exists() -> bool {
Cameras::exists(PRIMARY_CAMERA_IDX)
}
impl Cameras {
pub fn all_info() -> ResultType<Vec<DisplayInfo>> {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
return Ok(Vec::new());
match query(ApiBackend::Auto) {
Ok(cameras) => {
let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap();
camera_displays.clear();
// FIXME: nokhwa returns duplicate info for one physical camera on linux for now.
// issue: https://github.com/l1npengtul/nokhwa/issues/171
// Use only one camera as a temporary hack.
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
let Some(info) = cameras.first() else {
bail!("No camera found")
};
let camera = Self::create_camera(info.index())?;
let resolution = camera.resolution();
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
camera_displays.push(DisplayInfo {
x: 0,
y: 0,
name: info.human_name().clone(),
width,
height,
online: true,
cursor_embedded: false,
scale:1.0,
original_resolution: Some(Resolution {
width,
height,
..Default::default()
}).into(),
..Default::default()
});
} else {
let mut x = 0;
for info in &cameras {
let camera = Self::create_camera(info.index())?;
let resolution = camera.resolution();
let (width, height) = (resolution.width() as i32, resolution.height() as i32);
camera_displays.push(DisplayInfo {
x,
y: 0,
name: info.human_name().clone(),
width,
height,
online: true,
cursor_embedded: false,
scale:1.0,
original_resolution: Some(Resolution {
width,
height,
..Default::default()
}).into(),
..Default::default()
});
x += width;
}
}
}
Ok(camera_displays.clone())
}
Err(e) => {
bail!("Query cameras error: {}", e)
}
}
}
pub fn exists(index: usize) -> bool {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
return false;
match query(ApiBackend::Auto) {
Ok(cameras) => index < cameras.len(),
_ => return false,
}
}
fn create_camera(index: &CameraIndex) -> ResultType<Camera> {
// TODO: support more platforms.
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
bail!("This platform doesn't support camera yet");
let result = Camera::new(
index.clone(),
RequestedFormat::new::<RgbAFormat>(RequestedFormatType::AbsoluteHighestResolution),
);
match result {
Ok(camera) => Ok(camera),
Err(e) => bail!("create camera{} error: {}", index, e),
}
}
pub fn get_camera_resolution(index: usize) -> ResultType<Resolution> {
let index = CameraIndex::Index(index as u32);
let camera = Self::create_camera(&index)?;
let resolution = camera.resolution();
Ok(Resolution {
width: resolution.width() as i32,
height: resolution.height() as i32,
..Default::default()
})
}
pub fn get_sync_cameras() -> Vec<DisplayInfo> {
SYNC_CAMERA_DISPLAYS.lock().unwrap().clone()
}
pub fn get_capturer(current: usize) -> ResultType<Box<dyn TraitCapturer>> {
Ok(Box::new(CameraCapturer::new(current)?))
}
}
pub struct CameraCapturer {
camera: Camera,
data: Vec<u8>,
last_data: Vec<u8>, // for faster compare and copy
}
impl CameraCapturer {
fn new(current: usize) -> ResultType<Self> {
let index = CameraIndex::Index(current as u32);
let camera = Cameras::create_camera(&index)?;
Ok(CameraCapturer {
camera,
data: Vec::new(),
last_data: Vec::new(),
})
}
}
impl TraitCapturer for CameraCapturer {
fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
// TODO: move this check outside `frame`.
if !self.camera.is_stream_open() {
if let Err(e) = self.camera.open_stream() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera open stream error: {}", e),
));
}
}
match self.camera.frame() {
Ok(buffer) => {
match buffer.decode_image::<RgbAFormat>() {
Ok(decoded) => {
self.data = decoded.as_raw().to_vec();
crate::would_block_if_equal(&mut self.last_data, &self.data)?;
// FIXME: macos's PixelBuffer cannot be directly created from bytes slice.
cfg_if::cfg_if! {
if #[cfg(any(target_os = "linux", target_os = "windows"))] {
Ok(Frame::PixelBuffer(PixelBuffer::new(
&self.data,
Pixfmt::RGBA,
decoded.width() as usize,
decoded.height() as usize,
)))
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera is not supported on this platform yet"),
))
}
}
}
Err(e) => Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera frame decode error: {}", e),
)),
}
}
Err(e) => Err(io::Error::new(
io::ErrorKind::Other,
format!("Camera frame error: {}", e),
)),
}
}
#[cfg(windows)]
fn is_gdi(&self) -> bool {
false
}
#[cfg(windows)]
fn set_gdi(&mut self) -> bool {
false
}
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice {
AdapterDevice::default()
}
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, _texture: bool) {}
}
+10 -3
View File
@@ -70,23 +70,30 @@ impl TraitCapturer for Capturer {
pub struct PixelBuffer<'a> {
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,
+1
View File
@@ -48,6 +48,7 @@ pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
pub mod camera;
pub mod aom;
pub mod record;
mod vpx;
+2 -2
View File
@@ -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
+18 -9
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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,
+13 -4
View File
@@ -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());
}
}
_ => {}
+49 -6
View File
@@ -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);
}
}
+3 -2
View File
@@ -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();
}
+11 -1
View File
@@ -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
+17 -3
View File
@@ -92,16 +92,28 @@ pub fn host_stop_system_key_propagate(_stopped: bool) {
}
// This function is only used to count the number of control sessions.
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,
+2 -1
View File
@@ -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);
}
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+3
View File
@@ -237,5 +237,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."),
("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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+5
View File
@@ -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();
}
+48 -12
View File
@@ -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() {
+173 -36
View File
@@ -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()
+1
View File
@@ -404,6 +404,7 @@ fn no_displays(displays: &Vec<Display>) -> bool {
}
}
#[inline]
#[cfg(not(windows))]
pub fn try_get_displays() -> ResultType<Vec<Display>> {
+7 -2
View File
@@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option<thread::JoinHandle<()>> {
}
pub fn try_stop_record_cursor_pos() {
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);
+8 -3
View File
@@ -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 => {
+10 -9
View File
@@ -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();
+161 -51
View File
@@ -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);
+1
View File
@@ -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(),
+4 -4
View File
@@ -356,7 +356,7 @@ function bring_to_top(idx=-1) {
}
}
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
stdout.println("new connection #" + id + ": " + peer_id);
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');
+3
View File
@@ -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") {
+7 -2
View File
@@ -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,
+10 -1
View File
@@ -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);