mirror of
https://git.hmsn.ink/flutter/vnc_viewer.git
synced 2026-03-20 00:02:22 +09:00
first
This commit is contained in:
456
lib/rfb/remote_frame_buffer_widget.dart
Normal file
456
lib/rfb/remote_frame_buffer_widget.dart
Normal file
@@ -0,0 +1,456 @@
|
||||
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;
|
||||
}
|
||||
if(keyEvent.logicalKey == LogicalKeyboardKey.f11) {
|
||||
if(keyEvent is KeyDownEvent) {
|
||||
windowManager.isFullScreen().then((flag) {windowManager.setFullScreen(!flag);});
|
||||
}
|
||||
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'];
|
||||
_sendToVnc(key, vkCode, down);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendToVnc(int key, int vkCode, bool down) {
|
||||
_isolateSendPort.match(
|
||||
() {},
|
||||
(SendPort sendPort) {
|
||||
sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.keyEvent(
|
||||
down: down,
|
||||
key: key,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user