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 _connectingWidget; final String _hostName; final Option _onError; final Option _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 createState() => RemoteFrameBufferWidgetState(); } @visibleForTesting class RemoteFrameBufferWidgetState extends State { late Timer _clipBoardMonitorTimer; Option _frameBuffer = none(); Option _image = none(); Option _isolate = none(); Option _isolateSendPort = none(); final ValueNotifier _sizeValueNotifier = ValueNotifier(Size.zero); Option> _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(), ) .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 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( image: image, remoteFrameBufferWidgetSize: MediaQuery.of(context).size, 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 _handleFrameBufferUpdateMessage({ required final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate update, }) => Task(() 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 _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 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 updateFrameBuffer({ required final ByteData frameBuffer, required final Size frameBufferSize, required final RemoteFrameBufferClientUpdateRectangle rectangle, }) => TaskEither.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 prev = {}; Future _handleMethod(MethodCall call) async { if (call.method == 'onPhysicalKeyEvent') { final args = call.arguments as Map; 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, ), ); }, ); } }