fix: smooth remote desktop input over webrtc
This commit is contained in:
parent
064e0fdc27
commit
b356714d52
@ -32,6 +32,14 @@ Map<String, Object?> desktopOfferParams({
|
||||
};
|
||||
}
|
||||
|
||||
bool desktopShouldDropInputEvent(
|
||||
Map<String, dynamic> event, {
|
||||
required int bufferedAmount,
|
||||
int bufferedAmountLimit = 64 * 1024,
|
||||
}) {
|
||||
return event['type'] == 'mouse_move' && bufferedAmount > bufferedAmountLimit;
|
||||
}
|
||||
|
||||
String desktopSessionId() {
|
||||
return 'remote-desktop-${randomIdInternal()}';
|
||||
}
|
||||
@ -326,6 +334,10 @@ class DesktopClient {
|
||||
final channel = _dataChannel;
|
||||
if (channel != null &&
|
||||
channel.state == RTCDataChannelState.RTCDataChannelOpen) {
|
||||
final bufferedAmount = channel.bufferedAmount ?? 0;
|
||||
if (desktopShouldDropInputEvent(event, bufferedAmount: bufferedAmount)) {
|
||||
return;
|
||||
}
|
||||
final jsonStr = jsonEncode(event);
|
||||
channel.send(RTCDataChannelMessage(jsonStr));
|
||||
}
|
||||
|
||||
@ -2,11 +2,22 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef DesktopInputClock = int Function();
|
||||
|
||||
class DesktopInputHandler {
|
||||
DesktopInputHandler({required this.onSendInput});
|
||||
DesktopInputHandler({
|
||||
required this.onSendInput,
|
||||
int moveIntervalMs = 16,
|
||||
DesktopInputClock? nowMillis,
|
||||
}) : moveIntervalMs = moveIntervalMs < 0 ? 0 : moveIntervalMs,
|
||||
_nowMillis = nowMillis ?? (() => DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
final void Function(Map<String, dynamic> event) onSendInput;
|
||||
final int moveIntervalMs;
|
||||
final DesktopInputClock _nowMillis;
|
||||
int _lastPressedButton = 1; // Default to left click
|
||||
int? _lastMoveSentAtMs;
|
||||
Offset? _lastMovePosition;
|
||||
|
||||
void handlePointerMove(
|
||||
PointerEvent event,
|
||||
@ -20,7 +31,7 @@ class DesktopInputHandler {
|
||||
);
|
||||
if (position == null) return;
|
||||
|
||||
onSendInput({'type': 'mouse_move', 'x': position.dx, 'y': position.dy});
|
||||
_sendPointerMove(position);
|
||||
}
|
||||
|
||||
void handlePointerDown(
|
||||
@ -36,7 +47,7 @@ class DesktopInputHandler {
|
||||
if (position == null) return;
|
||||
|
||||
// Send move event first to ensure click hits the exact coordinates
|
||||
onSendInput({'type': 'mouse_move', 'x': position.dx, 'y': position.dy});
|
||||
_sendPointerMove(position, force: true);
|
||||
|
||||
_lastPressedButton = _mapPointerButtons(event.buttons);
|
||||
|
||||
@ -79,6 +90,25 @@ class DesktopInputHandler {
|
||||
if (buttons & 2 != 0) return 3; // right click
|
||||
return 1;
|
||||
}
|
||||
|
||||
void _sendPointerMove(Offset position, {bool force = false}) {
|
||||
final lastPosition = _lastMovePosition;
|
||||
if (!force &&
|
||||
lastPosition != null &&
|
||||
(position - lastPosition).distance < 0.001) {
|
||||
return;
|
||||
}
|
||||
|
||||
final now = _nowMillis();
|
||||
final lastSentAt = _lastMoveSentAtMs;
|
||||
if (!force && lastSentAt != null && now - lastSentAt < moveIntervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastMoveSentAtMs = now;
|
||||
_lastMovePosition = position;
|
||||
onSendInput({'type': 'mouse_move', 'x': position.dx, 'y': position.dy});
|
||||
}
|
||||
}
|
||||
|
||||
String? desktopKeyName(LogicalKeyboardKey key) {
|
||||
@ -97,7 +127,7 @@ String? desktopKeyName(LogicalKeyboardKey key) {
|
||||
if (key == LogicalKeyboardKey.end) return 'End';
|
||||
if (key == LogicalKeyboardKey.pageUp) return 'Page_Up';
|
||||
if (key == LogicalKeyboardKey.pageDown) return 'Page_Down';
|
||||
|
||||
|
||||
if (key == LogicalKeyboardKey.shiftLeft) return 'Shift_L';
|
||||
if (key == LogicalKeyboardKey.shiftRight) return 'Shift_R';
|
||||
if (key == LogicalKeyboardKey.controlLeft) return 'Control_L';
|
||||
@ -162,7 +192,9 @@ Offset? desktopContentPosition(
|
||||
}) {
|
||||
if (viewportSize.width <= 0 || viewportSize.height <= 0) return null;
|
||||
|
||||
if (contentSize == null || contentSize.width <= 0 || contentSize.height <= 0) {
|
||||
if (contentSize == null ||
|
||||
contentSize.width <= 0 ||
|
||||
contentSize.height <= 0) {
|
||||
return Offset(
|
||||
(localPosition.dx / viewportSize.width).clamp(0.0, 1.0),
|
||||
(localPosition.dy / viewportSize.height).clamp(0.0, 1.0),
|
||||
|
||||
@ -143,14 +143,38 @@ void main() {
|
||||
expect(params['height'], 720);
|
||||
});
|
||||
|
||||
test('generates distinct desktop session ids for parallel app instances', () {
|
||||
final first = desktopSessionId();
|
||||
final second = desktopSessionId();
|
||||
test(
|
||||
'generates distinct desktop session ids for parallel app instances',
|
||||
() {
|
||||
final first = desktopSessionId();
|
||||
final second = desktopSessionId();
|
||||
|
||||
expect(first, startsWith('remote-desktop-'));
|
||||
expect(second, startsWith('remote-desktop-'));
|
||||
expect(first, isNot(second));
|
||||
expect(first, isNot('remote-desktop-session'));
|
||||
expect(first, startsWith('remote-desktop-'));
|
||||
expect(second, startsWith('remote-desktop-'));
|
||||
expect(first, isNot(second));
|
||||
expect(first, isNot('remote-desktop-session'));
|
||||
},
|
||||
);
|
||||
|
||||
test('drops only stale mouse moves when data channel is backed up', () {
|
||||
expect(
|
||||
desktopShouldDropInputEvent({
|
||||
'type': 'mouse_move',
|
||||
}, bufferedAmount: 80 * 1024),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
desktopShouldDropInputEvent({
|
||||
'type': 'mouse_down',
|
||||
}, bufferedAmount: 80 * 1024),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
desktopShouldDropInputEvent({
|
||||
'type': 'mouse_move',
|
||||
}, bufferedAmount: 1024),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('uses bridge-provided remote stream when present', () async {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xworkmate/features/desktop/desktop_input_handler.dart';
|
||||
|
||||
void main() {
|
||||
@ -73,4 +75,82 @@ void main() {
|
||||
expect(position, const Offset(0.5, 0.5));
|
||||
});
|
||||
});
|
||||
|
||||
group('DesktopInputHandler pointer flow control', () {
|
||||
test('throttles pointer move events before they hit the data channel', () {
|
||||
var now = 0;
|
||||
final events = <Map<String, dynamic>>[];
|
||||
final handler = DesktopInputHandler(
|
||||
onSendInput: events.add,
|
||||
nowMillis: () => now,
|
||||
);
|
||||
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(10, 10)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
now = 5;
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(20, 20)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
now = 16;
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(30, 30)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
|
||||
expect(events, hasLength(2));
|
||||
expect(events.first['x'], 0.1);
|
||||
expect(events.last['x'], 0.3);
|
||||
});
|
||||
|
||||
test('deduplicates unchanged pointer move positions', () {
|
||||
var now = 0;
|
||||
final events = <Map<String, dynamic>>[];
|
||||
final handler = DesktopInputHandler(
|
||||
onSendInput: events.add,
|
||||
nowMillis: () => now,
|
||||
);
|
||||
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(10, 10)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
now = 100;
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(10, 10)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
|
||||
expect(events, hasLength(1));
|
||||
});
|
||||
|
||||
test('forces latest pointer position before mouse down', () {
|
||||
var now = 0;
|
||||
final events = <Map<String, dynamic>>[];
|
||||
final handler = DesktopInputHandler(
|
||||
onSendInput: events.add,
|
||||
nowMillis: () => now,
|
||||
);
|
||||
|
||||
handler.handlePointerMove(
|
||||
const PointerHoverEvent(position: Offset(10, 10)),
|
||||
const Size(100, 100),
|
||||
);
|
||||
now = 5;
|
||||
handler.handlePointerDown(
|
||||
const PointerDownEvent(
|
||||
position: Offset(80, 20),
|
||||
buttons: kPrimaryMouseButton,
|
||||
),
|
||||
const Size(100, 100),
|
||||
);
|
||||
|
||||
expect(events, hasLength(3));
|
||||
expect(events[1], containsPair('type', 'mouse_move'));
|
||||
expect(events[1]['x'], 0.8);
|
||||
expect(events[2], containsPair('type', 'mouse_down'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user