mirror of
https://git.hmsn.ink/flutter/vnc_viewer.git
synced 2026-03-20 00:02:22 +09:00
459 lines
15 KiB
Dart
459 lines
15 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:isolate';
|
|
import 'dart:typed_data';
|
|
import 'dart:ui';
|
|
|
|
import 'package:dart_rfb/dart_rfb.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart' hide Image;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:vnc_viewer/rfb/remote_frame_buffer_Listener_detector.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
import 'child_size_notifier_widget.dart';
|
|
import 'extensions/logical_keyboard_key_extensions.dart';
|
|
import 'remote_frame_buffer_client_isolate.dart';
|
|
import 'remote_frame_buffer_gesture_detector.dart';
|
|
import 'remote_frame_buffer_isolate_messages.dart';
|
|
import 'package:fpdart/fpdart.dart' hide State;
|
|
import 'package:logging/logging.dart';
|
|
|
|
final Logger _logger = Logger('RemoteFrameBufferWidget');
|
|
|
|
/// This widget displays the framebuffer associated with the RFB session.
|
|
/// On creation, it tries to establish a connection with the remote server
|
|
/// in an isolate. On success, it runs the read loop in that isolate.
|
|
class RemoteFrameBufferWidget extends StatefulWidget {
|
|
final Option<Widget> _connectingWidget;
|
|
final String _hostName;
|
|
final Option<void Function(Object error)> _onError;
|
|
final Option<String> _password;
|
|
final int _port;
|
|
final bool _isFullscreen;
|
|
|
|
/// Immediately tries to establish a connection to a remote server at
|
|
/// [hostName]:[port], optionally using [password].
|
|
RemoteFrameBufferWidget({
|
|
super.key,
|
|
final Widget? connectingWidget,
|
|
required final String hostName,
|
|
final void Function(Object error)? onError,
|
|
final String? password,
|
|
final int port = 5900,
|
|
required final bool isFullscreen,
|
|
}) : _connectingWidget = optionOf(connectingWidget),
|
|
_hostName = hostName,
|
|
_onError = optionOf(onError),
|
|
_password = optionOf(password),
|
|
_port = port,
|
|
_isFullscreen = isFullscreen;
|
|
|
|
@override
|
|
State<RemoteFrameBufferWidget> createState() =>
|
|
RemoteFrameBufferWidgetState();
|
|
}
|
|
|
|
@visibleForTesting
|
|
class RemoteFrameBufferWidgetState extends State<RemoteFrameBufferWidget> {
|
|
late Timer _clipBoardMonitorTimer;
|
|
Option<ByteData> _frameBuffer = none();
|
|
Option<Image> _image = none();
|
|
Option<Isolate> _isolate = none();
|
|
Option<SendPort> _isolateSendPort = none();
|
|
final ValueNotifier<Size> _sizeValueNotifier = ValueNotifier<Size>(Size.zero);
|
|
Option<StreamSubscription<Object?>> _streamSubscription = none();
|
|
|
|
@override
|
|
Widget build(final BuildContext context) => _frameBuffer
|
|
.flatMap(
|
|
(final ByteData frameBuffer) => frameBuffer.buffer
|
|
.asUint8List(
|
|
frameBuffer.offsetInBytes,
|
|
frameBuffer.lengthInBytes,
|
|
)
|
|
.where((final int byte) => byte != 0)
|
|
.isNotEmpty
|
|
? _image
|
|
: none<Image>(),
|
|
)
|
|
.match(
|
|
_buildConnecting,
|
|
(final Image image) => _buildImage(image: image),
|
|
);
|
|
|
|
bool _rawKeyEventListener(KeyEvent keyEvent) {
|
|
|
|
if(keyEvent is! KeyDownEvent && keyEvent is! KeyUpEvent ) {
|
|
return false;
|
|
} else {
|
|
_isolateSendPort.match(
|
|
() {},
|
|
(final SendPort sendPort) => sendPort.send(
|
|
RemoteFrameBufferIsolateSendMessage.keyEvent(
|
|
down: keyEvent is KeyDownEvent,
|
|
key: keyEvent.logicalKey.asXWindowSystemKey(),
|
|
),
|
|
),
|
|
);
|
|
|
|
return true;
|
|
}
|
|
return true;
|
|
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_clipBoardMonitorTimer.cancel();
|
|
_streamSubscription.match(
|
|
() {},
|
|
(final StreamSubscription<Object?> subscription) =>
|
|
unawaited(subscription.cancel()),
|
|
);
|
|
_image.match(
|
|
() {},
|
|
(final Image image) => image.dispose(),
|
|
);
|
|
_isolate.match(
|
|
() {},
|
|
(final Isolate isolate) => isolate.kill(),
|
|
);
|
|
HardwareKeyboard.instance.removeHandler(_rawKeyEventListener);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_monitorClipBoard();
|
|
HardwareKeyboard.instance.addHandler(_rawKeyEventListener);
|
|
unawaited(_initAsync());
|
|
VncKeyboardHandler();
|
|
}
|
|
|
|
Widget _buildConnecting() => widget._connectingWidget.getOrElse(
|
|
() => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
|
|
SizeTrackingWidget _buildImage({required final Image image}) =>
|
|
SizeTrackingWidget(
|
|
sizeValueNotifier: _sizeValueNotifier,
|
|
child: RemoteFrameBufferListenerDetector(
|
|
sendPort: _isolateSendPort,
|
|
child: RemoteFrameBufferGestureDetector(
|
|
image: image,
|
|
remoteFrameBufferWidgetSize: MediaQuery.of(context).size,
|
|
sendPort: _isolateSendPort,
|
|
isFullscreen: widget._isFullscreen,
|
|
child: RawImage(image: image, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height,),
|
|
),
|
|
),
|
|
);
|
|
|
|
void _decodeAndUpdateImage({
|
|
required final ByteData frameBuffer,
|
|
required final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
|
message,
|
|
}) =>
|
|
decodeImageFromPixels(
|
|
frameBuffer.buffer.asUint8List(),
|
|
message.frameBufferWidth,
|
|
message.frameBufferHeight,
|
|
PixelFormat.bgra8888,
|
|
(final Image result) {
|
|
if (mounted) {
|
|
setState(
|
|
() {
|
|
_image.match(
|
|
() {},
|
|
(final Image image) => image.dispose(),
|
|
);
|
|
_image = some(result);
|
|
},
|
|
);
|
|
_isolateSendPort.match(
|
|
() {},
|
|
(final SendPort sendPort) => sendPort.send(
|
|
const RemoteFrameBufferIsolateSendMessage
|
|
.frameBufferUpdateRequest(),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
void _decodeAndResizeImage({
|
|
required ByteData frameBuffer,
|
|
required int srcWidth,
|
|
required int srcHeight,
|
|
required int dstWidth,
|
|
required int dstHeight,
|
|
}) {
|
|
decodeImageFromPixels(
|
|
frameBuffer.buffer.asUint8List(),
|
|
srcWidth,
|
|
srcHeight,
|
|
PixelFormat.bgra8888,
|
|
(Image originalImage) {
|
|
final pictureRecorder = PictureRecorder();
|
|
final canvas = Canvas(pictureRecorder);
|
|
final paint = Paint()..filterQuality = FilterQuality.low;
|
|
|
|
// 축소해서 그리기
|
|
canvas.drawImageRect(
|
|
originalImage,
|
|
Rect.fromLTWH(0, 0, srcWidth.toDouble(), srcHeight.toDouble()),
|
|
Rect.fromLTWH(0, 0, dstWidth.toDouble(), dstHeight.toDouble()),
|
|
paint,
|
|
);
|
|
|
|
final picture = pictureRecorder.endRecording();
|
|
picture.toImage(dstWidth, dstHeight).then((resizedImage) {
|
|
originalImage.dispose(); // 메모리 해제
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_image.match(() {}, (img) => img.dispose());
|
|
_image = some(resizedImage);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
Task<void> _handleFrameBufferUpdateMessage({
|
|
required final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
|
update,
|
|
}) =>
|
|
Task<void>(() async {
|
|
_logger.finer(
|
|
'Received new update message with ${update.update.rectangles.length} rectangles',
|
|
);
|
|
_isolateSendPort = some(update.sendPort);
|
|
if (_frameBuffer.isNone()) {
|
|
_frameBuffer = some(
|
|
ByteData(
|
|
update.frameBufferHeight * update.frameBufferWidth * 4,
|
|
),
|
|
);
|
|
}
|
|
unawaited(
|
|
_frameBuffer.match(
|
|
() async {},
|
|
(final ByteData frameBuffer) async {
|
|
for (final RemoteFrameBufferClientUpdateRectangle rectangle
|
|
in update.update.rectangles) {
|
|
await rectangle.encodingType.when(
|
|
copyRect: () async {
|
|
final int sourceX = rectangle.byteData.getUint16(0);
|
|
final int sourceY = rectangle.byteData.getUint16(2);
|
|
final BytesBuilder bytesBuilder = BytesBuilder();
|
|
for (int row = 0; row < rectangle.height; row++) {
|
|
for (int column = 0; column < rectangle.width; column++) {
|
|
bytesBuilder.add(
|
|
frameBuffer.buffer.asUint8List(
|
|
((sourceY + row) * update.frameBufferWidth +
|
|
sourceX +
|
|
column) *
|
|
4,
|
|
4,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return (await updateFrameBuffer(
|
|
frameBuffer: frameBuffer,
|
|
frameBufferSize: Size(
|
|
update.frameBufferWidth.toDouble(),
|
|
update.frameBufferHeight.toDouble(),
|
|
),
|
|
rectangle: rectangle.copyWith(
|
|
encodingType: const RemoteFrameBufferEncodingType.copyRect(),
|
|
byteData: ByteData.sublistView(
|
|
bytesBuilder.toBytes(),
|
|
),
|
|
),
|
|
).run())
|
|
.match(
|
|
(final Object error) =>
|
|
// ignore: avoid_print
|
|
print('Error updating frame buffer: $error'),
|
|
(final _) {},
|
|
);
|
|
},
|
|
raw: () async => (await updateFrameBuffer(
|
|
frameBuffer: frameBuffer,
|
|
frameBufferSize: Size(
|
|
update.frameBufferWidth.toDouble(),
|
|
update.frameBufferHeight.toDouble(),
|
|
),
|
|
rectangle: rectangle,
|
|
).run())
|
|
.match(
|
|
(final Object error) =>
|
|
// ignore: avoid_print
|
|
print('Error updating frame buffer: $error'),
|
|
(final _) {},
|
|
),
|
|
unsupported: (final ByteData bytes) async {},
|
|
);
|
|
}
|
|
_decodeAndUpdateImage(
|
|
frameBuffer: frameBuffer,
|
|
message: update,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
});
|
|
|
|
/// Initializes logic that requires to be run asynchronous.
|
|
Future<void> _initAsync() async {
|
|
final ReceivePort receivePort = ReceivePort();
|
|
_streamSubscription = some(
|
|
receivePort.listen(
|
|
(final Object? message) {
|
|
// Error, first is error, second is stacktrace or null
|
|
if (message is List) {
|
|
widget._onError.match(
|
|
() {},
|
|
(final void Function(Object error) onError) =>
|
|
onError(message.first),
|
|
);
|
|
} else if (message is RemoteFrameBufferIsolateReceiveMessage) {
|
|
message.map(
|
|
clipBoardUpdate: (
|
|
final RemoteFrameBufferIsolateReceiveMessageClipBoardUpdate
|
|
update,
|
|
) =>
|
|
Clipboard.setData(ClipboardData(text: update.text)),
|
|
frameBufferUpdate: (
|
|
final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
|
update,
|
|
) {
|
|
_handleFrameBufferUpdateMessage(update: update).run();
|
|
},
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
_logger.info('Spawning new isolate for RFB client');
|
|
_isolate = some(
|
|
await Isolate.spawn(
|
|
startRemoteFrameBufferClient,
|
|
RemoteFrameBufferIsolateInitMessage(
|
|
hostName: widget._hostName,
|
|
password: widget._password,
|
|
port: widget._port,
|
|
sendPort: receivePort.sendPort,
|
|
),
|
|
onError: receivePort.sendPort,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _monitorClipBoard() {
|
|
Option<String> lastClipBoardContent = none();
|
|
_clipBoardMonitorTimer = Timer.periodic(
|
|
const Duration(seconds: 1),
|
|
(final _) async {
|
|
optionOf(await Clipboard.getData(Clipboard.kTextPlain))
|
|
.flatMap((final ClipboardData data) => optionOf(data.text))
|
|
.filter(
|
|
(final String text) => lastClipBoardContent.match(
|
|
() => true,
|
|
(final String lastClipBoardContent) =>
|
|
lastClipBoardContent != text,
|
|
),
|
|
)
|
|
.match(
|
|
() {},
|
|
(final String text) => _isolateSendPort.match(
|
|
() {},
|
|
(final SendPort sendPort) {
|
|
lastClipBoardContent = some(text);
|
|
sendPort.send(
|
|
RemoteFrameBufferIsolateSendMessage.clipBoardUpdate(
|
|
text: text,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Updates [frameBuffer] with the given [rectangle]s.
|
|
@visibleForTesting
|
|
static TaskEither<Object, void> updateFrameBuffer({
|
|
required final ByteData frameBuffer,
|
|
required final Size frameBufferSize,
|
|
required final RemoteFrameBufferClientUpdateRectangle rectangle,
|
|
}) =>
|
|
TaskEither<Object, void>.tryCatch(
|
|
() async {
|
|
for (int y = 0; y < rectangle.height; y++) {
|
|
for (int x = 0; x < rectangle.width; x++) {
|
|
final int frameBufferX = rectangle.x + x;
|
|
final int frameBufferY = rectangle.y + y;
|
|
final int pixelBytes =
|
|
rectangle.byteData.getUint32((y * rectangle.width + x) * 4);
|
|
frameBuffer.setUint32(
|
|
((frameBufferY * frameBufferSize.width + frameBufferX) * 4)
|
|
.toInt(),
|
|
pixelBytes,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
(final Object error, final _) => error,
|
|
);
|
|
|
|
|
|
final MethodChannel _channel = const MethodChannel('com.example.vnc_viewer/keyboard');
|
|
|
|
void VncKeyboardHandler() {
|
|
_channel.setMethodCallHandler(_handleMethod);
|
|
}
|
|
|
|
Map<dynamic, dynamic> prev = {};
|
|
Future<void> _handleMethod(MethodCall call) async {
|
|
|
|
if (call.method == 'onPhysicalKeyEvent') {
|
|
final args = call.arguments as Map<dynamic, dynamic>;
|
|
final bool down = args['down'];
|
|
final int key = args['key'];
|
|
final int vkCode = args['vkCode'];
|
|
if(vkCode == 122) {
|
|
if (down) {
|
|
windowManager.isFullScreen().then((flag) {
|
|
windowManager.setFullScreen(!flag);
|
|
});
|
|
}
|
|
} else {
|
|
_sendToVnc(key, vkCode, down);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _sendToVnc(int key, int vkCode, bool down) {
|
|
_isolateSendPort.match(
|
|
() {},
|
|
(SendPort sendPort) {
|
|
sendPort.send(
|
|
RemoteFrameBufferIsolateSendMessage.keyEvent(
|
|
down: down,
|
|
key: key,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
}
|