@desklink/host 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +3 -0
- package/README.md +50 -16
- package/bin/desklink-host.mjs +13 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +5 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +17 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +42 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +1 -0
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +16 -1
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +4 -3
- package/docs/PROTOCOL.md +95 -27
- package/engine/Cargo.lock +195 -1
- package/engine/Cargo.toml +25 -5
- package/engine/build.rs +98 -23
- package/engine/native/agent_overlay_mac.m +133 -0
- package/engine/native/agent_overlay_wayland.c +192 -0
- package/engine/native/agent_overlay_x11.c +103 -0
- package/engine/native/mac_stream.mm +188 -0
- package/engine/src/capture.rs +169 -19
- package/engine/src/capture_mac.rs +145 -0
- package/engine/src/clipboard_mac.rs +182 -0
- package/engine/src/convert.rs +57 -1
- package/engine/src/indicator.rs +162 -0
- package/engine/src/input.rs +12 -0
- package/engine/src/input_mac.rs +312 -0
- package/engine/src/keymap.rs +25 -0
- package/engine/src/keymap_mac.rs +209 -0
- package/engine/src/mac.rs +722 -0
- package/engine/src/main.rs +116 -2
- package/engine/src/portal.rs +4 -0
- package/engine/src/portal_mac.rs +68 -0
- package/engine/src/protocol.rs +48 -1
- package/engine/src/session.rs +556 -62
- package/engine/src/x11.rs +23 -3
- package/engine/src/x11_mac.rs +40 -0
- package/examples/reference-client.html +33 -4
- package/package.json +2 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#import <AppKit/AppKit.h>
|
|
2
|
+
#import <QuartzCore/QuartzCore.h>
|
|
3
|
+
#include <stdio.h>
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
#include <unistd.h>
|
|
6
|
+
|
|
7
|
+
@interface AgentOverlayView : NSView {
|
|
8
|
+
NSPoint target, cursor;
|
|
9
|
+
CFTimeInterval clickAt, typeAt, endingAt;
|
|
10
|
+
BOOL positioned;
|
|
11
|
+
}
|
|
12
|
+
- (void)event:(char)kind x:(double)x y:(double)y;
|
|
13
|
+
- (void)end;
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
@implementation AgentOverlayView
|
|
17
|
+
- (BOOL)isOpaque { return NO; }
|
|
18
|
+
- (void)event:(char)kind x:(double)x y:(double)y {
|
|
19
|
+
if (kind == 'S') { [self end]; return; }
|
|
20
|
+
if (x >= 0 && y >= 0) {
|
|
21
|
+
NSPoint point = NSMakePoint(x, self.bounds.size.height - y);
|
|
22
|
+
target = point;
|
|
23
|
+
if (!positioned) { cursor = point; positioned = YES; }
|
|
24
|
+
}
|
|
25
|
+
CFTimeInterval now = CACurrentMediaTime();
|
|
26
|
+
if (kind == 'C') clickAt = now;
|
|
27
|
+
if (kind == 'T') typeAt = now;
|
|
28
|
+
[self setNeedsDisplay:YES];
|
|
29
|
+
}
|
|
30
|
+
- (void)end { endingAt = CACurrentMediaTime(); }
|
|
31
|
+
- (void)tick {
|
|
32
|
+
if (endingAt && CACurrentMediaTime() - endingAt > 0.7) { [NSApp terminate:nil]; return; }
|
|
33
|
+
cursor.x += (target.x - cursor.x) * 0.24;
|
|
34
|
+
cursor.y += (target.y - cursor.y) * 0.24;
|
|
35
|
+
[self setNeedsDisplay:YES];
|
|
36
|
+
}
|
|
37
|
+
- (void)drawRect:(NSRect)rect {
|
|
38
|
+
[[NSColor clearColor] set]; NSRectFillUsingOperation(rect, NSCompositingOperationCopy);
|
|
39
|
+
CFTimeInterval now = CACurrentMediaTime();
|
|
40
|
+
CGFloat edge = endingAt ? fmax(0, 1 - (now-endingAt)/0.7) : 1;
|
|
41
|
+
if (edge > 0) {
|
|
42
|
+
NSRect bounds = self.bounds;
|
|
43
|
+
[[NSColor colorWithCalibratedRed:0.28 green:0.67 blue:0.96 alpha:0.75*edge] set];
|
|
44
|
+
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(bounds, 4, 4) xRadius:12 yRadius:12];
|
|
45
|
+
border.lineWidth = 5; [border stroke];
|
|
46
|
+
}
|
|
47
|
+
if (!positioned) return;
|
|
48
|
+
NSBezierPath *pointer = [NSBezierPath bezierPath];
|
|
49
|
+
[pointer moveToPoint:NSMakePoint(cursor.x, cursor.y)];
|
|
50
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+2, cursor.y-24)];
|
|
51
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+8, cursor.y-18)];
|
|
52
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+15, cursor.y-31)];
|
|
53
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+21, cursor.y-28)];
|
|
54
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+14, cursor.y-15)];
|
|
55
|
+
[pointer lineToPoint:NSMakePoint(cursor.x+24, cursor.y-14)];
|
|
56
|
+
[pointer closePath];
|
|
57
|
+
[[NSColor colorWithCalibratedRed:0.16 green:0.60 blue:1 alpha:1] set]; [pointer fill];
|
|
58
|
+
[[NSColor whiteColor] set]; pointer.lineWidth = 2; [pointer stroke];
|
|
59
|
+
for (int i = 3; i >= 0; i--) {
|
|
60
|
+
CGFloat radius = 12 + i*5;
|
|
61
|
+
CGFloat alpha = 0.13 * (1.0 - i/5.0);
|
|
62
|
+
[[NSColor colorWithCalibratedRed:0.30 green:0.73 blue:1 alpha:alpha] set];
|
|
63
|
+
NSBezierPath *halo = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-radius, cursor.y-radius, radius*2, radius*2)];
|
|
64
|
+
halo.lineWidth = 1.5 + i; [halo stroke];
|
|
65
|
+
}
|
|
66
|
+
if (clickAt && now-clickAt < 0.55) {
|
|
67
|
+
CGFloat progress = (now-clickAt)/0.55;
|
|
68
|
+
CGFloat radius = 11 + 37*progress;
|
|
69
|
+
[[NSColor colorWithCalibratedRed:0.37 green:0.77 blue:1 alpha:0.45*(1-progress)] set];
|
|
70
|
+
NSBezierPath *ripple = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(target.x-radius,target.y-radius,2*radius,2*radius)];
|
|
71
|
+
ripple.lineWidth = 2.5*(1-progress)+0.5; [ripple stroke];
|
|
72
|
+
}
|
|
73
|
+
if (typeAt && now-typeAt < 0.4) {
|
|
74
|
+
CGFloat progress = (now-typeAt)/0.4;
|
|
75
|
+
CGFloat radius = 5 + progress*15;
|
|
76
|
+
[[NSColor colorWithCalibratedRed:0.50 green:0.83 blue:1 alpha:0.32*(1-progress)] set];
|
|
77
|
+
[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(target.x-radius,target.y-radius,2*radius,2*radius)] fill];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
@end
|
|
81
|
+
|
|
82
|
+
// The dev recording path converts ScreenCaptureKit screenshots to packed BGRA.
|
|
83
|
+
unsigned char *desklink_image_bgra(CGImageRef image, size_t *length, size_t *stride) {
|
|
84
|
+
size_t width = CGImageGetWidth(image), height = CGImageGetHeight(image);
|
|
85
|
+
*stride = width * 4; *length = *stride * height;
|
|
86
|
+
unsigned char *pixels = malloc(*length);
|
|
87
|
+
if (!pixels) return NULL;
|
|
88
|
+
CGColorSpaceRef color = CGColorSpaceCreateDeviceRGB();
|
|
89
|
+
CGContextRef context = CGBitmapContextCreate(pixels, width, height, 8, *stride, color,
|
|
90
|
+
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
|
|
91
|
+
CGColorSpaceRelease(color);
|
|
92
|
+
if (!context) { free(pixels); return NULL; }
|
|
93
|
+
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
|
94
|
+
CGContextRelease(context);
|
|
95
|
+
return pixels;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
int desklink_agent_overlay_main(int display_id) {
|
|
99
|
+
@autoreleasepool {
|
|
100
|
+
[NSApplication sharedApplication];
|
|
101
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
|
102
|
+
NSScreen *screen = nil;
|
|
103
|
+
for (NSScreen *candidate in [NSScreen screens]) {
|
|
104
|
+
if ([candidate.deviceDescription[@"NSScreenNumber"] intValue] == display_id) { screen = candidate; break; }
|
|
105
|
+
}
|
|
106
|
+
if (!screen) return 2;
|
|
107
|
+
NSWindow *window = [[NSWindow alloc] initWithContentRect:screen.frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO screen:screen];
|
|
108
|
+
window.opaque = NO; window.backgroundColor = NSColor.clearColor;
|
|
109
|
+
window.ignoresMouseEvents = YES; window.hasShadow = NO;
|
|
110
|
+
window.level = NSScreenSaverWindowLevel - 1;
|
|
111
|
+
window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary;
|
|
112
|
+
AgentOverlayView *view = [[AgentOverlayView alloc] initWithFrame:NSMakeRect(0,0,screen.frame.size.width,screen.frame.size.height)];
|
|
113
|
+
window.contentView = view;
|
|
114
|
+
NSPoint mouse = [NSEvent mouseLocation];
|
|
115
|
+
[view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
|
|
116
|
+
[window orderFrontRegardless];
|
|
117
|
+
puts("READY"); fflush(stdout);
|
|
118
|
+
[NSTimer scheduledTimerWithTimeInterval:1.0/60 target:view selector:@selector(tick) userInfo:nil repeats:YES];
|
|
119
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
120
|
+
char line[128];
|
|
121
|
+
while (fgets(line, sizeof(line), stdin)) {
|
|
122
|
+
char kind; double x = 0, y = 0;
|
|
123
|
+
if (sscanf(line, "%c %lf %lf", &kind, &x, &y) >= 1) {
|
|
124
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [view event:kind x:x y:y]; });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [view end]; });
|
|
128
|
+
});
|
|
129
|
+
[NSApp run];
|
|
130
|
+
[window close];
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Minimal wlroots layer-shell surface: transparent, unfocusable and input-empty.
|
|
2
|
+
// Only the portal path uses it; X11 uses a cursor sprite instead.
|
|
3
|
+
// Protocol interface names/signatures from wlr-layer-shell-unstable-v1:
|
|
4
|
+
// Copyright (c) 2017 Drew DeVault. Permission is hereby granted to use, copy,
|
|
5
|
+
// modify and distribute this protocol for any purpose without fee, provided
|
|
6
|
+
// this notice appears in all copies. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT
|
|
7
|
+
// WARRANTY OF ANY KIND; THE AUTHORS ARE NOT LIABLE FOR ANY CLAIM OR DAMAGES.
|
|
8
|
+
#define _GNU_SOURCE
|
|
9
|
+
#include <wayland-client.h>
|
|
10
|
+
#include <wayland-client-protocol.h>
|
|
11
|
+
#include <sys/mman.h>
|
|
12
|
+
#include <sys/poll.h>
|
|
13
|
+
#include <time.h>
|
|
14
|
+
#include <unistd.h>
|
|
15
|
+
#include <fcntl.h>
|
|
16
|
+
#include <math.h>
|
|
17
|
+
#include <stdio.h>
|
|
18
|
+
#include <stdlib.h>
|
|
19
|
+
#include <string.h>
|
|
20
|
+
|
|
21
|
+
// Layer-shell v1 protocol (Drew DeVault, 2017; MIT license).
|
|
22
|
+
static const struct wl_interface layer_surface_interface;
|
|
23
|
+
static const struct wl_interface *get_surface_types[] = {
|
|
24
|
+
&layer_surface_interface, &wl_surface_interface, &wl_output_interface, NULL
|
|
25
|
+
};
|
|
26
|
+
static const struct wl_message shell_requests[] = {
|
|
27
|
+
{"get_layer_surface", "no?ous", get_surface_types}, {"destroy", "3", NULL}
|
|
28
|
+
};
|
|
29
|
+
static const struct wl_interface shell_interface = {"zwlr_layer_shell_v1", 1, 2, shell_requests, 0, NULL};
|
|
30
|
+
static const struct wl_message surface_requests[] = {
|
|
31
|
+
{"set_size", "uu", NULL}, {"set_anchor", "u", NULL}, {"set_exclusive_zone", "i", NULL},
|
|
32
|
+
{"set_margin", "iiii", NULL}, {"set_keyboard_interactivity", "u", NULL},
|
|
33
|
+
{"get_popup", "o", NULL}, {"ack_configure", "u", NULL}, {"destroy", "", NULL}
|
|
34
|
+
};
|
|
35
|
+
static const struct wl_message surface_events[] = { {"configure", "uuu", NULL}, {"closed", "", NULL} };
|
|
36
|
+
static const struct wl_interface layer_surface_interface = {"zwlr_layer_surface_v1", 1, 8, surface_requests, 2, surface_events};
|
|
37
|
+
|
|
38
|
+
int desklink_wayland_geometry(int source_w, int source_h, int surface_w, int surface_h,
|
|
39
|
+
double px, double py, double *x, double *y) {
|
|
40
|
+
if (source_w<=0||source_h<=0||surface_w<=0||surface_h<=0) return 0;
|
|
41
|
+
*x=px*surface_w/source_w;*y=py*surface_h/source_h;
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
void desklink_wayland_policy(uint32_t *anchor, uint32_t *keyboard_focus, uint32_t *empty_input) {
|
|
45
|
+
*anchor=15; *keyboard_focus=0; *empty_input=1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct Buffer { struct wl_buffer *handle; uint32_t *pixels; size_t length; int busy; };
|
|
49
|
+
struct Overlay {
|
|
50
|
+
struct wl_display *display;
|
|
51
|
+
struct wl_compositor *compositor;
|
|
52
|
+
struct wl_shm *shm;
|
|
53
|
+
struct wl_proxy *shell, *layer;
|
|
54
|
+
struct wl_surface *surface;
|
|
55
|
+
struct Buffer buffers[2];
|
|
56
|
+
int width, height, source_w, source_h, configured, closed;
|
|
57
|
+
};
|
|
58
|
+
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
|
|
59
|
+
static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
|
60
|
+
struct Overlay *o=data;
|
|
61
|
+
if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
|
|
62
|
+
if (!strcmp(interface,"wl_shm")) o->shm=wl_registry_bind(registry,name,&wl_shm_interface,1);
|
|
63
|
+
if (!strcmp(interface,"zwlr_layer_shell_v1")) o->shell=wl_registry_bind(registry,name,&shell_interface,1);
|
|
64
|
+
}
|
|
65
|
+
static void registry_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data;(void)registry;(void)name; }
|
|
66
|
+
static const struct wl_registry_listener registry_listener={registry_global,registry_remove};
|
|
67
|
+
static void configured(void *data, struct wl_proxy *layer, uint32_t serial, uint32_t w, uint32_t h) {
|
|
68
|
+
struct Overlay *o=data; o->width=(int)w; o->height=(int)h; o->configured=1;
|
|
69
|
+
wl_proxy_marshal_flags(layer,6,NULL,1,0,serial);
|
|
70
|
+
}
|
|
71
|
+
static void closed(void *data, struct wl_proxy *layer) { (void)layer; ((struct Overlay *)data)->closed=1; }
|
|
72
|
+
static void (*layer_listener[])(void)={(void (*)(void))configured,(void (*)(void))closed};
|
|
73
|
+
static void buffer_release(void *data, struct wl_buffer *buffer) { (void)buffer; ((struct Buffer *)data)->busy=0; }
|
|
74
|
+
static const struct wl_buffer_listener buffer_listener={buffer_release};
|
|
75
|
+
|
|
76
|
+
static int create_buffer(struct Overlay *o, struct Buffer *b) {
|
|
77
|
+
b->length=(size_t)o->width*o->height*4;
|
|
78
|
+
int fd=memfd_create("desklink-agent-overlay",MFD_CLOEXEC);
|
|
79
|
+
if (fd<0 || ftruncate(fd,b->length)) { if(fd>=0)close(fd); return -1; }
|
|
80
|
+
b->pixels=mmap(NULL,b->length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
|
|
81
|
+
if (b->pixels==MAP_FAILED) {close(fd);return -1;}
|
|
82
|
+
struct wl_shm_pool *pool=wl_shm_create_pool(o->shm,fd,(int)b->length);
|
|
83
|
+
b->handle=wl_shm_pool_create_buffer(pool,0,o->width,o->height,o->width*4,WL_SHM_FORMAT_ARGB8888);
|
|
84
|
+
wl_shm_pool_destroy(pool); close(fd);
|
|
85
|
+
wl_buffer_add_listener(b->handle,&buffer_listener,b);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
static void blend(uint32_t *pixel, unsigned alpha, unsigned rgb) {
|
|
89
|
+
// premultiplied ARGB8888: the compositor blends this over the desktop.
|
|
90
|
+
*pixel=(alpha<<24)|(((rgb>>16)&255)*alpha/255<<16)|(((rgb>>8)&255)*alpha/255<<8)|((rgb&255)*alpha/255);
|
|
91
|
+
}
|
|
92
|
+
static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending, int positioned) {
|
|
93
|
+
double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):1;
|
|
94
|
+
memset(b->pixels,0,b->length);
|
|
95
|
+
int w=o->width,h=o->height;
|
|
96
|
+
for (int edge=0;edge<7;edge++) {
|
|
97
|
+
unsigned alpha=(unsigned)((7-edge)*1.8*opacity);
|
|
98
|
+
for(int px=edge;px<w-edge;px++) {
|
|
99
|
+
blend(&b->pixels[(size_t)edge*w+px],alpha,0x4c9ed0);
|
|
100
|
+
blend(&b->pixels[(size_t)(h-1-edge)*w+px],alpha,0x4c9ed0);
|
|
101
|
+
}
|
|
102
|
+
for(int py=edge;py<h-edge;py++) {
|
|
103
|
+
blend(&b->pixels[(size_t)py*w+edge],alpha,0x4c9ed0);
|
|
104
|
+
blend(&b->pixels[(size_t)py*w+w-1-edge],alpha,0x4c9ed0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
int left=fmax(0,x-55),right=fmin(w,x+55),top=fmax(0,y-55),bottom=fmin(h,y+55);
|
|
108
|
+
if(positioned)for(int py=top;py<bottom;py++)for(int px=left;px<right;px++) {
|
|
109
|
+
double distance=hypot(px-x,py-y);
|
|
110
|
+
double ring=fmax(0,1-fabs(distance-13)/5)*0.18;
|
|
111
|
+
ring+=fmax(0,1-fabs(distance-23)/7)*0.06;
|
|
112
|
+
if(click && t-click<0.55)ring+=fmax(0,1-fabs(distance-(12+37*(t-click)/0.55))/3)*0.42*(1-(t-click)/0.55);
|
|
113
|
+
if(typed && t-typed<0.4)
|
|
114
|
+
ring+=fmax(0,1-fabs(distance-(8+15*(t-typed)/0.4))/3)*0.22*(1-(t-typed)/0.4);
|
|
115
|
+
unsigned alpha=(unsigned)(fmin(1,ring*opacity)*255);
|
|
116
|
+
if(alpha)blend(&b->pixels[(size_t)py*w+px],alpha,0x4c9ed0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
int desklink_agent_overlay_wayland(int source_w, int source_h) {
|
|
121
|
+
struct Overlay o={.source_w=source_w,.source_h=source_h};
|
|
122
|
+
o.display=wl_display_connect(NULL);
|
|
123
|
+
if(!o.display)return 2;
|
|
124
|
+
struct wl_registry *registry=wl_display_get_registry(o.display);
|
|
125
|
+
wl_registry_add_listener(registry,®istry_listener,&o);
|
|
126
|
+
wl_display_roundtrip(o.display);
|
|
127
|
+
if(!o.compositor||!o.shm||!o.shell){wl_display_disconnect(o.display);return 2;}
|
|
128
|
+
o.surface=wl_compositor_create_surface(o.compositor);
|
|
129
|
+
o.layer=wl_proxy_marshal_flags(o.shell,0,&layer_surface_interface,1,0,NULL,o.surface,NULL,3,"desklink-agent");
|
|
130
|
+
wl_proxy_add_listener(o.layer,layer_listener,&o);
|
|
131
|
+
uint32_t anchor,keyboard_focus,empty_input;
|
|
132
|
+
desklink_wayland_policy(&anchor,&keyboard_focus,&empty_input);
|
|
133
|
+
wl_proxy_marshal_flags(o.layer,0,NULL,1,0,0u,0u);
|
|
134
|
+
wl_proxy_marshal_flags(o.layer,1,NULL,1,0,anchor);
|
|
135
|
+
wl_proxy_marshal_flags(o.layer,2,NULL,1,0,-1);
|
|
136
|
+
wl_proxy_marshal_flags(o.layer,4,NULL,1,0,keyboard_focus);
|
|
137
|
+
if(empty_input) {
|
|
138
|
+
struct wl_region *empty=wl_compositor_create_region(o.compositor);
|
|
139
|
+
wl_surface_set_input_region(o.surface,empty);
|
|
140
|
+
wl_region_destroy(empty);
|
|
141
|
+
}
|
|
142
|
+
wl_surface_commit(o.surface);
|
|
143
|
+
while(!o.configured&&!o.closed&&wl_display_dispatch(o.display)>=0) {}
|
|
144
|
+
if(o.closed||o.width<=0||o.height<=0){wl_display_disconnect(o.display);return 2;}
|
|
145
|
+
if(create_buffer(&o,&o.buffers[0])||create_buffer(&o,&o.buffers[1])){wl_display_disconnect(o.display);return 2;}
|
|
146
|
+
int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
|
|
147
|
+
puts("READY");fflush(stdout);
|
|
148
|
+
double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
|
|
149
|
+
int active=0,positioned=0;
|
|
150
|
+
char pending[128];size_t used=0;
|
|
151
|
+
while(!o.closed && (!ending||now()-ending<0.6)) {
|
|
152
|
+
struct pollfd fds[2]={{wl_display_get_fd(o.display),POLLIN,0},{STDIN_FILENO,POLLIN|POLLHUP,0}};
|
|
153
|
+
wl_display_flush(o.display);
|
|
154
|
+
poll(fds,2,16);
|
|
155
|
+
if(fds[0].revents&POLLIN && wl_display_dispatch(o.display)<0)break;
|
|
156
|
+
else wl_display_dispatch_pending(o.display);
|
|
157
|
+
char bytes[128];ssize_t n=read(STDIN_FILENO,bytes,sizeof(bytes));
|
|
158
|
+
if(n==0&&!ending)ending=now();
|
|
159
|
+
for(ssize_t i=0;i<n;i++) {
|
|
160
|
+
if(bytes[i]=='\n') {
|
|
161
|
+
pending[used]=0;char kind=0;double px=0,py=0;
|
|
162
|
+
if(sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
|
|
163
|
+
if(kind=='S')ending=now();
|
|
164
|
+
else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
|
|
165
|
+
if(px>=0&&py>=0) {
|
|
166
|
+
if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
|
|
167
|
+
&& !positioned){x=tx;y=ty;positioned=1;}
|
|
168
|
+
}
|
|
169
|
+
active=1;
|
|
170
|
+
if(kind=='C')click=now();if(kind=='T')typed=now();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
used=0;
|
|
174
|
+
}else if(used<sizeof(pending)-1)pending[used++]=bytes[i];
|
|
175
|
+
}
|
|
176
|
+
if(!active)continue;
|
|
177
|
+
x+=(tx-x)*0.24;y+=(ty-y)*0.24;
|
|
178
|
+
for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
|
|
179
|
+
draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
|
|
180
|
+
o.buffers[i].busy=1;
|
|
181
|
+
wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
|
|
182
|
+
wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
|
|
183
|
+
wl_surface_commit(o.surface);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
wl_surface_attach(o.surface,NULL,0,0);wl_surface_commit(o.surface);wl_display_roundtrip(o.display);
|
|
188
|
+
for(int i=0;i<2;i++){wl_buffer_destroy(o.buffers[i].handle);munmap(o.buffers[i].pixels,o.buffers[i].length);}
|
|
189
|
+
wl_proxy_destroy(o.layer);wl_surface_destroy(o.surface);wl_proxy_destroy(o.shell);
|
|
190
|
+
wl_display_disconnect(o.display);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// The X server draws cursors outside GetImage(root). No overlay window can
|
|
2
|
+
// expose black pixels, intercept input, or poison agent frame diffs.
|
|
3
|
+
#include <X11/Xlib.h>
|
|
4
|
+
#include <X11/Xcursor/Xcursor.h>
|
|
5
|
+
#include <math.h>
|
|
6
|
+
#include <fcntl.h>
|
|
7
|
+
#include <stdio.h>
|
|
8
|
+
#include <string.h>
|
|
9
|
+
#include <time.h>
|
|
10
|
+
#include <unistd.h>
|
|
11
|
+
|
|
12
|
+
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }
|
|
13
|
+
|
|
14
|
+
static unsigned pixel(double distance, double radius, double opacity) {
|
|
15
|
+
double edge = fmax(0, 1 - fabs(distance-radius)/2.5);
|
|
16
|
+
unsigned alpha = (unsigned)(fmin(1, edge * opacity) * 255);
|
|
17
|
+
return (alpha << 24) | 0x4c9ed0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
int desklink_agent_overlay_main(const char *display_name) {
|
|
21
|
+
Display *d = XOpenDisplay(display_name);
|
|
22
|
+
if (!d) return 2;
|
|
23
|
+
Window root = DefaultRootWindow(d);
|
|
24
|
+
unsigned best_w=64,best_h=64;
|
|
25
|
+
XQueryBestCursor(d,root,64,64,&best_w,&best_h);
|
|
26
|
+
unsigned size = best_w < best_h ? best_w : best_h;
|
|
27
|
+
if (size < 32) { XCloseDisplay(d); return 2; }
|
|
28
|
+
if (size > 96) size = 96;
|
|
29
|
+
XcursorImage *image = XcursorImageCreate(size,size);
|
|
30
|
+
if (!image) { XCloseDisplay(d); return 2; }
|
|
31
|
+
image->version = XCURSOR_IMAGE_VERSION;
|
|
32
|
+
Cursor previous = None;
|
|
33
|
+
int stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
|
|
34
|
+
fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK);
|
|
35
|
+
puts("READY"); fflush(stdout);
|
|
36
|
+
double target_x=0,target_y=0,cursor_x=0,cursor_y=0,click=0,typed=0,ending=0;
|
|
37
|
+
int active=0;
|
|
38
|
+
char pending[128]; size_t used=0;
|
|
39
|
+
for (;;) {
|
|
40
|
+
char bytes[128]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
|
|
41
|
+
if (n == 0 && !ending) ending=now();
|
|
42
|
+
for (ssize_t i=0; i<n; i++) {
|
|
43
|
+
if (bytes[i]=='\n') {
|
|
44
|
+
pending[used]=0; char kind=0; double px=0,py=0;
|
|
45
|
+
if (sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
|
|
46
|
+
if (kind=='S') ending=now();
|
|
47
|
+
else if (kind=='M'||kind=='C'||kind=='T') {
|
|
48
|
+
if (px>=0 && py>=0) {
|
|
49
|
+
target_x=px; target_y=py;
|
|
50
|
+
if (!active) { cursor_x=px; cursor_y=py; }
|
|
51
|
+
}
|
|
52
|
+
active=1;
|
|
53
|
+
if (kind=='C') click=now();
|
|
54
|
+
if (kind=='T') typed=now();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
used=0;
|
|
58
|
+
} else if (used < sizeof(pending)-1) pending[used++]=bytes[i];
|
|
59
|
+
}
|
|
60
|
+
double t=now();
|
|
61
|
+
if (ending && t-ending>0.55) break;
|
|
62
|
+
if (active) {
|
|
63
|
+
cursor_x += (target_x-cursor_x)*0.24;
|
|
64
|
+
cursor_y += (target_y-cursor_y)*0.24;
|
|
65
|
+
int radius=(int)size/2-3;
|
|
66
|
+
image->xhot = (unsigned)fmax(0,fmin(size-1,(double)size/2+target_x-cursor_x));
|
|
67
|
+
image->yhot = (unsigned)fmax(0,fmin(size-1,(double)size/2+target_y-cursor_y));
|
|
68
|
+
for (unsigned y=0;y<size;y++) for (unsigned x=0;x<size;x++) {
|
|
69
|
+
double distance=hypot(x-(double)size/2,y-(double)size/2);
|
|
70
|
+
double opacity=ending ? fmax(0,1-(t-ending)/0.55) : 1;
|
|
71
|
+
unsigned color=pixel(distance,12,0.72*opacity);
|
|
72
|
+
if (distance>=16 && distance<=radius) {
|
|
73
|
+
unsigned outer=pixel(distance,20,0.20*opacity);
|
|
74
|
+
if ((outer>>24)>(color>>24)) color=outer;
|
|
75
|
+
}
|
|
76
|
+
if (click && t-click<0.5) {
|
|
77
|
+
double progress=(t-click)/0.5;
|
|
78
|
+
unsigned ripple=pixel(distance,12+(radius-14)*progress,0.6*(1-progress)*opacity);
|
|
79
|
+
if ((ripple>>24)>(color>>24)) color=ripple;
|
|
80
|
+
}
|
|
81
|
+
if (typed && t-typed<0.35) {
|
|
82
|
+
double progress=(t-typed)/0.35;
|
|
83
|
+
if (distance<5+progress*12) {
|
|
84
|
+
unsigned pulse=(unsigned)(0.22*(1-progress)*opacity*255);
|
|
85
|
+
if (pulse>(color>>24)) color=(pulse<<24)|0x5baed8;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
image->pixels[y*size+x]=color;
|
|
89
|
+
}
|
|
90
|
+
Cursor next=XcursorImageLoadCursor(d,image);
|
|
91
|
+
if (next!=None) {
|
|
92
|
+
XDefineCursor(d,root,next); XFlush(d);
|
|
93
|
+
if (previous!=None) XFreeCursor(d,previous);
|
|
94
|
+
previous=next;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
usleep(16000);
|
|
98
|
+
}
|
|
99
|
+
XUndefineCursor(d,root); XFlush(d);
|
|
100
|
+
if (previous!=None) XFreeCursor(d,previous);
|
|
101
|
+
XcursorImageDestroy(image); XCloseDisplay(d);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
+
#import <CoreVideo/CoreVideo.h>
|
|
4
|
+
#import <CoreMedia/CoreMedia.h>
|
|
5
|
+
|
|
6
|
+
using FrameCallback = void (*)(void *, const uint8_t *, size_t, size_t, size_t, size_t);
|
|
7
|
+
using StatusCallback = void (*)(void *, const char *, bool);
|
|
8
|
+
|
|
9
|
+
@interface DLStreamOutput : NSObject <SCStreamOutput>
|
|
10
|
+
@property(nonatomic, assign) void *context;
|
|
11
|
+
@property(nonatomic, assign) FrameCallback callback;
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@implementation DLStreamOutput
|
|
15
|
+
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sample ofType:(SCStreamOutputType)type {
|
|
16
|
+
(void)stream;
|
|
17
|
+
if (type != SCStreamOutputTypeScreen || !CMSampleBufferIsValid(sample)) return;
|
|
18
|
+
@autoreleasepool {
|
|
19
|
+
CVPixelBufferRef pixel = CMSampleBufferGetImageBuffer(sample);
|
|
20
|
+
if (!pixel || CVPixelBufferLockBaseAddress(pixel, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) return;
|
|
21
|
+
const uint8_t *base = (const uint8_t *)CVPixelBufferGetBaseAddress(pixel);
|
|
22
|
+
if (base && CVPixelBufferGetPixelFormatType(pixel) == kCVPixelFormatType_32BGRA)
|
|
23
|
+
self.callback(self.context, base, CVPixelBufferGetDataSize(pixel),
|
|
24
|
+
CVPixelBufferGetWidth(pixel), CVPixelBufferGetHeight(pixel), CVPixelBufferGetBytesPerRow(pixel));
|
|
25
|
+
CVPixelBufferUnlockBaseAddress(pixel, kCVPixelBufferLock_ReadOnly);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
@interface DLStreamSession : NSObject <SCStreamDelegate>
|
|
31
|
+
@property(nonatomic, strong) SCStream *stream;
|
|
32
|
+
@property(nonatomic, strong) SCContentFilter *filter;
|
|
33
|
+
@property(nonatomic, strong) SCStreamConfiguration *config;
|
|
34
|
+
@property(nonatomic, strong) DLStreamOutput *output;
|
|
35
|
+
@property(nonatomic, strong) dispatch_queue_t queue;
|
|
36
|
+
@property(nonatomic, assign) StatusCallback status;
|
|
37
|
+
@property(nonatomic, assign) void *context;
|
|
38
|
+
@property(atomic, assign) BOOL closing;
|
|
39
|
+
@property(atomic, assign) BOOL restarting;
|
|
40
|
+
- (NSError *)startStream;
|
|
41
|
+
@end
|
|
42
|
+
@implementation DLStreamSession
|
|
43
|
+
- (NSError *)startStream {
|
|
44
|
+
__block NSError *failure = nil;
|
|
45
|
+
self.stream = [[SCStream alloc] initWithFilter:self.filter configuration:self.config delegate:self];
|
|
46
|
+
if (![self.stream addStreamOutput:self.output type:SCStreamOutputTypeScreen
|
|
47
|
+
sampleHandlerQueue:self.queue error:&failure]) return failure;
|
|
48
|
+
dispatch_semaphore_t started = dispatch_semaphore_create(0);
|
|
49
|
+
[self.stream startCaptureWithCompletionHandler:^(NSError *err) {
|
|
50
|
+
failure = err;
|
|
51
|
+
dispatch_semaphore_signal(started);
|
|
52
|
+
}];
|
|
53
|
+
if (dispatch_semaphore_wait(started, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)))
|
|
54
|
+
return [NSError errorWithDomain:@"desklink.capture" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Timed out starting stream"}];
|
|
55
|
+
return failure;
|
|
56
|
+
}
|
|
57
|
+
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
|
|
58
|
+
if (self.closing || self.restarting || stream != self.stream) return;
|
|
59
|
+
self.restarting = YES;
|
|
60
|
+
NSString *reason = [NSString stringWithFormat:@"SCStream stopped (%@ %ld): %@", error.domain, (long)error.code, error.localizedDescription];
|
|
61
|
+
self.status(self.context, reason.UTF8String, false);
|
|
62
|
+
NSLog(@"%@; restarting capture", reason);
|
|
63
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
64
|
+
for (int attempt = 1; attempt <= 3 && !self.closing; attempt++) {
|
|
65
|
+
[NSThread sleepForTimeInterval:attempt];
|
|
66
|
+
if (self.closing) break;
|
|
67
|
+
NSError *failure = [self startStream];
|
|
68
|
+
if (!failure) {
|
|
69
|
+
if (!self.closing) self.status(self.context, "SCStream restarted", true);
|
|
70
|
+
self.restarting = NO;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
NSLog(@"SCStream restart %d failed: %@", attempt, failure);
|
|
74
|
+
}
|
|
75
|
+
if (!self.closing) self.status(self.context, "SCStream restart exhausted", false);
|
|
76
|
+
self.restarting = NO;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
@end
|
|
80
|
+
|
|
81
|
+
static void fail(char *error, size_t capacity, NSString *message) {
|
|
82
|
+
if (capacity) snprintf(error, capacity, "%s", message.UTF8String ?: "ScreenCaptureKit failed");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
extern "C" void *dl_mac_stream_start(uint32_t display_id, size_t width, size_t height, uint32_t fps,
|
|
86
|
+
uint32_t indicator_pid, void *context, FrameCallback callback,
|
|
87
|
+
StatusCallback status, char *error, size_t capacity) {
|
|
88
|
+
@autoreleasepool {
|
|
89
|
+
dispatch_semaphore_t found = dispatch_semaphore_create(0);
|
|
90
|
+
__block SCShareableContent *content = nil;
|
|
91
|
+
__block NSError *failure = nil;
|
|
92
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *value, NSError *err) {
|
|
93
|
+
content = value;
|
|
94
|
+
failure = err;
|
|
95
|
+
dispatch_semaphore_signal(found);
|
|
96
|
+
}];
|
|
97
|
+
if (dispatch_semaphore_wait(found, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
|
|
98
|
+
fail(error, capacity, @"Timed out listing displays");
|
|
99
|
+
return nullptr;
|
|
100
|
+
}
|
|
101
|
+
if (failure) { fail(error, capacity, failure.localizedDescription); return nullptr; }
|
|
102
|
+
SCDisplay *display = nil;
|
|
103
|
+
for (SCDisplay *candidate in content.displays)
|
|
104
|
+
if (candidate.displayID == display_id) { display = candidate; break; }
|
|
105
|
+
if (!display) { fail(error, capacity, @"Selected display is unavailable"); return nullptr; }
|
|
106
|
+
NSMutableArray<SCWindow *> *excluded = [NSMutableArray array];
|
|
107
|
+
if (indicator_pid) {
|
|
108
|
+
for (SCWindow *window in content.windows)
|
|
109
|
+
if (window.owningApplication.processID == (pid_t)indicator_pid)
|
|
110
|
+
[excluded addObject:window];
|
|
111
|
+
if (!excluded.count) {
|
|
112
|
+
fail(error, capacity, @"Agent indicator window not found for capture exclusion");
|
|
113
|
+
return nullptr;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:excluded];
|
|
117
|
+
SCStreamConfiguration *config = [SCStreamConfiguration new];
|
|
118
|
+
config.width = width;
|
|
119
|
+
config.height = height;
|
|
120
|
+
config.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
121
|
+
config.minimumFrameInterval = CMTimeMake(1, fps ? fps : 30);
|
|
122
|
+
config.queueDepth = 3;
|
|
123
|
+
config.showsCursor = YES;
|
|
124
|
+
config.capturesAudio = NO;
|
|
125
|
+
DLStreamSession *session = [DLStreamSession new];
|
|
126
|
+
session.output = [DLStreamOutput new];
|
|
127
|
+
session.output.context = context;
|
|
128
|
+
session.output.callback = callback;
|
|
129
|
+
session.queue = dispatch_queue_create("dev.desklink.capture", DISPATCH_QUEUE_SERIAL);
|
|
130
|
+
session.filter = filter;
|
|
131
|
+
session.config = config;
|
|
132
|
+
session.status = status;
|
|
133
|
+
session.context = context;
|
|
134
|
+
failure = [session startStream];
|
|
135
|
+
if (failure) {
|
|
136
|
+
fail(error, capacity, failure.localizedDescription);
|
|
137
|
+
dispatch_semaphore_t stopped = dispatch_semaphore_create(0);
|
|
138
|
+
[session.stream stopCaptureWithCompletionHandler:^(NSError *err) {
|
|
139
|
+
(void)err;
|
|
140
|
+
dispatch_semaphore_signal(stopped);
|
|
141
|
+
}];
|
|
142
|
+
dispatch_semaphore_wait(stopped, DISPATCH_TIME_FOREVER);
|
|
143
|
+
dispatch_sync(session.queue, ^{});
|
|
144
|
+
return nullptr;
|
|
145
|
+
}
|
|
146
|
+
const char *simulate = getenv("DESKLINK_AXI_SIMULATE_STREAM_STOP_MS");
|
|
147
|
+
int delay = simulate ? atoi(simulate) : 0;
|
|
148
|
+
if (delay >= 100 && delay <= 60000) {
|
|
149
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)delay * NSEC_PER_MSEC),
|
|
150
|
+
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
151
|
+
if (session.closing) return;
|
|
152
|
+
SCStream *stream = session.stream;
|
|
153
|
+
[stream stopCaptureWithCompletionHandler:^(NSError *err) {
|
|
154
|
+
(void)err;
|
|
155
|
+
NSError *stopped = [NSError errorWithDomain:@"SCStreamErrorDomain" code:-3821
|
|
156
|
+
userInfo:@{NSLocalizedDescriptionKey: @"simulated systemStoppedStream"}];
|
|
157
|
+
[session stream:stream didStopWithError:stopped];
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return (__bridge_retained void *)session;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
extern "C" bool dl_mac_stream_stop(void *handle) {
|
|
166
|
+
if (!handle) return true;
|
|
167
|
+
@autoreleasepool {
|
|
168
|
+
DLStreamSession *session = (__bridge_transfer DLStreamSession *)handle;
|
|
169
|
+
session.closing = YES;
|
|
170
|
+
for (int i = 0; session.restarting && i < 350; i++) [NSThread sleepForTimeInterval:0.1];
|
|
171
|
+
if (session.restarting) {
|
|
172
|
+
(void)CFBridgingRetain(session);
|
|
173
|
+
return false; // The restart still owns the callback context.
|
|
174
|
+
}
|
|
175
|
+
dispatch_semaphore_t stopped = dispatch_semaphore_create(0);
|
|
176
|
+
[session.stream stopCaptureWithCompletionHandler:^(NSError *err) {
|
|
177
|
+
(void)err;
|
|
178
|
+
dispatch_semaphore_signal(stopped);
|
|
179
|
+
}];
|
|
180
|
+
if (dispatch_semaphore_wait(stopped, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
|
|
181
|
+
// A late frame can still use the callback context; keep both alive rather than free it.
|
|
182
|
+
(void)CFBridgingRetain(session);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
dispatch_sync(session.queue, ^{});
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|