@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.
Files changed (45) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +50 -16
  3. package/bin/desklink-host.mjs +13 -2
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +5 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts.map +1 -1
  8. package/dist/engineProcess.js +17 -5
  9. package/dist/engineProcess.js.map +1 -1
  10. package/dist/protocol.d.ts +42 -5
  11. package/dist/protocol.d.ts.map +1 -1
  12. package/dist/protocol.js +1 -1
  13. package/dist/protocol.js.map +1 -1
  14. package/dist/resolveEngine.d.ts +1 -0
  15. package/dist/resolveEngine.d.ts.map +1 -1
  16. package/dist/resolveEngine.js +16 -1
  17. package/dist/resolveEngine.js.map +1 -1
  18. package/docs/BOUNDARY.md +4 -3
  19. package/docs/PROTOCOL.md +95 -27
  20. package/engine/Cargo.lock +195 -1
  21. package/engine/Cargo.toml +25 -5
  22. package/engine/build.rs +98 -23
  23. package/engine/native/agent_overlay_mac.m +133 -0
  24. package/engine/native/agent_overlay_wayland.c +192 -0
  25. package/engine/native/agent_overlay_x11.c +103 -0
  26. package/engine/native/mac_stream.mm +188 -0
  27. package/engine/src/capture.rs +169 -19
  28. package/engine/src/capture_mac.rs +145 -0
  29. package/engine/src/clipboard_mac.rs +182 -0
  30. package/engine/src/convert.rs +57 -1
  31. package/engine/src/indicator.rs +162 -0
  32. package/engine/src/input.rs +12 -0
  33. package/engine/src/input_mac.rs +312 -0
  34. package/engine/src/keymap.rs +25 -0
  35. package/engine/src/keymap_mac.rs +209 -0
  36. package/engine/src/mac.rs +722 -0
  37. package/engine/src/main.rs +116 -2
  38. package/engine/src/portal.rs +4 -0
  39. package/engine/src/portal_mac.rs +68 -0
  40. package/engine/src/protocol.rs +48 -1
  41. package/engine/src/session.rs +556 -62
  42. package/engine/src/x11.rs +23 -3
  43. package/engine/src/x11_mac.rs +40 -0
  44. package/examples/reference-client.html +33 -4
  45. package/package.json +2 -2
package/docs/PROTOCOL.md CHANGED
@@ -1,12 +1,14 @@
1
- # desklink local control protocol (v2)
1
+ # desklink local control protocol (v3)
2
2
 
3
3
  The host engine is a per-user, on-demand process. A consumer that already owns
4
4
  the user's session starts it and talks to it over an inherited private channel —
5
5
  the engine never opens a public listener, never runs as root, and never installs
6
6
  a service.
7
7
 
8
- This document is the engine's whole public surface. It contains no application
9
- concepts: no accounts, no chat, no machine ids, no pane ids.
8
+ This document describes the shared Linux and macOS engine protocol. The macOS
9
+ engine uses the same v3 stdio/WebRTC session path, with native display capture,
10
+ input and clipboard adapters. The protocol contains no application concepts:
11
+ no accounts, no chat, no machine ids, no pane ids.
10
12
 
11
13
  ## Starting the engine
12
14
 
@@ -55,13 +57,13 @@ Request parameters use the engine's own snake_case names (`session_id`,
55
57
  ## Handshake
56
58
 
57
59
  ```jsonc
58
- {"id":1,"method":"hello","params":{"protocol":2}}
60
+ {"id":1,"method":"hello","params":{"protocol":3}}
59
61
  ```
60
62
 
61
63
  Result:
62
64
 
63
65
  ```jsonc
64
- {"protocol":2,"engine":"desklink-host/0.1.0","platform":"linux", /* the rest of Capabilities */ }
66
+ {"protocol":3,"engine":"desklink-host/0.1.0","platform":"linux", /* the rest of Capabilities */ }
65
67
  ```
66
68
 
67
69
  The consumer must send `hello` first, and a version the engine does not speak is
@@ -69,6 +71,31 @@ refused with `error.code = "unsupported-protocol"`. Until a supported `hello`
69
71
  arrives, every other request is refused the same way; `capabilities` and
70
72
  `shutdown` are the two that answer without a handshake.
71
73
 
74
+ ## macOS
75
+
76
+ macOS speaks protocol 3 and shares the session lifecycle below. It captures the
77
+ selected display with ScreenCaptureKit, encodes VP9, and uses the same WebRTC
78
+ offer, ICE, control-channel, lease, and metrics flow as Linux. Capture currently
79
+ uses one bounded ScreenCaptureKit stream (three-frame queue); the backend may
80
+ supply fewer frames. Audio is not captured.
81
+
82
+ `capabilities` is non-prompting and reports active displays (pixel and point
83
+ dimensions, origin and scale), the current keyboard layout, console state, and
84
+ Screen Recording/Accessibility grant states. A `display` source accepts an
85
+ optional `display_id` from `capture.displays`; an absent ID selects the main
86
+ display. Screen Recording consent is requested by `session.open` when capture
87
+ is needed. Control may request Accessibility consent; if it remains unavailable,
88
+ open fails with `input-unavailable`. TCC permissions attach to the responsible
89
+ launching app (for example Terminal, iTerm, or the Node.js host), not to a
90
+ separate DesklinkHost.app bundle. Enable that app in **System Settings → Privacy
91
+ & Security → Screen & System Audio Recording** and **Accessibility**, then
92
+ reconnect. No TCC state is pre-seeded or bypassed.
93
+
94
+ macOS clipboard read/write is plain text and requires the session's `clipboard`
95
+ permission. `restore_token` is not supported by this native backend; a new
96
+ session may require fresh consent. The macOS engine requires a VP9-enabled build
97
+ (`DESKLINK_VPX_STATIC_DIR`) for `serve`.
98
+
72
99
  ## Capabilities
73
100
 
74
101
  ```jsonc
@@ -77,7 +104,7 @@ arrives, every other request is refused the same way; `capabilities` and
77
104
 
78
105
  ```jsonc
79
106
  {
80
- "protocol": 2,
107
+ "protocol": 3,
81
108
  "engine": "desklink-host/0.1.0",
82
109
  "platform": "linux",
83
110
  "session": {"kind": "wayland"},
@@ -117,7 +144,8 @@ without kernel input access a portal session may request `view`, but an open
117
144
  requesting `control` is refused. X display capture uses XTest instead and does
118
145
  not require that grant. The muxr host and WebSocket bridge report clipboard
119
146
  unavailable for an X display, regardless of the engine's Wayland clipboard
120
- probe. The engine never asks for privileges on its own.
147
+ probe. On Linux the engine never asks for privileges on its own; macOS may
148
+ request Screen Recording or Accessibility consent at `session.open`.
121
149
 
122
150
  `capabilities` is safe to call before any consent has been given and must not
123
151
  trigger a capture request.
@@ -127,6 +155,7 @@ trigger a capture request.
127
155
  ```jsonc
128
156
  {"id":4,"method":"session.open","params":{
129
157
  "source": {"kind":"portal"}, // or {"kind":"x11","display":":99"}
158
+ // macOS: {"kind":"display","display_id":123}
130
159
  "permissions": ["view","control","clipboard"],
131
160
  "max_width": 3840, "max_height": 2160, // encode box (default); never upscales
132
161
  "bitrate_kbps": 0, // 0 (default): sized to the encoded surface
@@ -134,7 +163,8 @@ trigger a capture request.
134
163
  "ice_servers": [{"urls":["stun:..."],"username":null,"credential":null}],
135
164
  "restore_token": null, // from a previous session.restoreToken
136
165
  "ttl_seconds": 3600, // session lease; default 3600
137
- "loopback_tcp": false // also offer ICE over TCP on 127.0.0.1; default false
166
+ "loopback_tcp": false, // also offer ICE over TCP on 127.0.0.1; default false
167
+ "agent_indicator": false // local input feedback; default false (phone control)
138
168
  }}
139
169
  ```
140
170
 
@@ -150,20 +180,31 @@ Result:
150
180
 
151
181
  `source` selects the desktop. `portal` (or absent) asks the compositor for a
152
182
  screen cast, which is the only path that carries a Wayland user's consent;
153
- `x11` reads a named X display's root window and applies input through XTest. The
154
- choice is capability-based rather than a table of desktops: the portal when there
155
- is one, an X display otherwise. It is normally the *consumer's* decision, because
156
- a client is not in a position to know which of the two a machine can offer.
157
-
158
- `permissions` are the engine's authority for this session. The engine enforces
159
- them on every input and clipboard action and never infers them from a source, a
160
- peer or an SDP. `control` without a working input backend is refused at
161
- `session.open` with `error.code = "input-unavailable"` — the consumer must ask
162
- for `view` explicitly instead.
163
-
164
- `max_fps` defaults to 30. It caps the encoded frame rate and the X11 capture
165
- loop; portal capture offers it as the preferred PipeWire frame rate (clamped
166
- to the offered 0–1000 fps range). The source may still supply fewer frames.
183
+ `x11` reads a named X display's root window and applies input through XTest;
184
+ `display` selects a macOS display by the ID reported in capabilities (or the
185
+ main display when omitted). The choice is normally the *consumer's* decision,
186
+ because a client is not in a position to know which backend a machine can offer.
187
+
188
+ `permissions` are the engine's authority for this session. `view` is required
189
+ to open a session. The engine enforces the remaining scopes on every input and
190
+ clipboard action and never infers them from a source, a peer or an SDP.
191
+ `control` without a working input backend is refused at `session.open` with
192
+ `error.code = "input-unavailable"` — the consumer must ask for `view` explicitly instead.
193
+
194
+ `max_fps` defaults to 30 and is bounded to 1–60. It caps the encoded frame
195
+ rate and X11 capture loop; portal capture offers it as the preferred PipeWire
196
+ frame rate, and macOS sets its ScreenCaptureKit stream minimum frame interval.
197
+ A source may still supply fewer frames.
198
+
199
+ `agent_indicator` is opt-in local feedback for an agent controller; desklink-axi
200
+ sets it for control sessions, never for view-only or human phone control. It
201
+ adds an eased pointer halo, click ripple, and typing pulse. On Wayland it uses
202
+ a click-through layer-shell surface with an edge glow; its bounded area is
203
+ masked before capture/diff (pixels behind the effect can remain briefly stale).
204
+ On bare X11 it replaces the pointer sprite instead, which XGetImage excludes:
205
+ there is no edge glow on that path. macOS uses a nonactivating, click-through
206
+ window excluded by the ScreenCaptureKit content filter. Closing the session
207
+ fades and removes the indicator.
167
208
 
168
209
  `session.open` is where the capture request happens, which is where the user's
169
210
  consent appears. It is not implicit and it is not retried silently.
@@ -209,7 +250,7 @@ always does.
209
250
  "firstFrame":true}} // false until a frame has been encoded
210
251
  ```
211
252
 
212
- The `geometry` a client maps touch against is the one `session.open` returned,
253
+ A stopped capture emits `{"event":"session.capture.stopped","params":{"sessionId":"…","reason":"SCStream stopped (SCStreamErrorDomain -3821): …"}}`. Frame requests then return `stream-stopped` rather than stale pixels. On macOS the engine retries opening the stream up to three times and emits `session.state` with `capture=streaming` on recovery; Linux capture errors also surface instead of serving an old frame.\n\nThe `geometry` a client maps touch against is the one `session.open` returned,
213
254
  and the one the control channel's `hello` repeats. A session keeps the one
214
255
  `generation` it was opened with: a different source or scale is a new
215
256
  `session.open`, not a generation change within a session.
@@ -289,7 +330,7 @@ are JSON, one per message:
289
330
  The engine answers on the same channel:
290
331
 
291
332
  ```jsonc
292
- {"kind":"hello","protocol":2,"geometry":{…}} // once, when the channel opens
333
+ {"kind":"hello","protocol":3,"geometry":{…}} // once, when the channel opens
293
334
  {"kind":"ack","seq":44}
294
335
  {"kind":"rejected","seq":44,"code":"coordinates","message":"(9000,4) is outside the 1280x720 surface"}
295
336
  {"kind":"clipboard","request":"…","text":"…","truncated":false} // an empty "text" with "error" when the read failed
@@ -307,9 +348,36 @@ physical effect.
307
348
  The engine holds its own pressed-key/button state. `close`, `release_all`,
308
349
  control-channel loss and process exit all synthesize releases for exactly the
309
350
  keys and buttons this session pressed, so a dropped connection cannot leave a
310
- modifier stuck down on the desktop. `input.text` in `capabilities` states the
311
- characters the compiled layout can actually produce; anything else is refused
312
- with `text-unsupported` and the honest workaround is the explicit clipboard.
351
+ modifier stuck down on the desktop. On Linux, `input.text` in `capabilities` states the characters the compiled
352
+ layout can produce. macOS reports Unicode text support and currently maps
353
+ `key.character` chords through a US ANSI virtual-key map; the reported keyboard
354
+ layout is diagnostic, not proof that this map matches it. Anything outside the
355
+ map is refused with `text-unsupported`.
356
+
357
+ ## Latest frame over the local protocol
358
+
359
+ `session.frame` requires `view` permission. It writes the latest pre-encode frame as
360
+ packed BGRX bytes to the caller's path, with optional `[x,y,w,h]` `region`.
361
+ The directory must already exist and be private to the caller; pixels never
362
+ travel through JSON. `seq` advances only when capture changes, `still_ms` is
363
+ elapsed time since that capture, and `damage` bounds the changed 32-pixel
364
+ tiles since `since` (full frame when the requested sequence is older than the
365
+ previous capture). A request with the current sequence reports empty damage;
366
+ omitting `path` requests metadata only (`written: false`). `after_seq` waits for
367
+ `seq` to exceed that value; `still_ms` waits for that many milliseconds
368
+ of stillness. When both are supplied, both conditions must hold.
369
+ `timeout_ms` alone waits for 150 ms of stillness. The default deadline is 5 s;
370
+ `timeout_ms` can extend it to at most 120 s. An unmet condition returns
371
+ `frame-timeout`. Damage is grouped by connected 32-pixel tiles.
372
+ Each changed capture emits a local `session.frame.changed` event with `sessionId`,
373
+ `seq` and `damage`, allowing a co-located consumer to track animation across
374
+ observations. Neither the event nor this file-writing method is forwarded by
375
+ the remote WebSocket bridge.
376
+
377
+ ```json
378
+ {"id":20,"method":"session.frame","params":{"session_id":"…","since":7,"path":"/run/user/1000/frame.raw"}}
379
+ {"id":20,"result":{"seq":8,"still_ms":210,"width":1280,"height":720,"format":"bgrx","damage":[[32,32,32,32]],"written":true}}
380
+ ```
313
381
 
314
382
  ## Clipboard over the local protocol
315
383
 
package/engine/Cargo.lock CHANGED
@@ -257,6 +257,15 @@ dependencies = [
257
257
  "generic-array",
258
258
  ]
259
259
 
260
+ [[package]]
261
+ name = "block2"
262
+ version = "0.6.2"
263
+ source = "registry+https://github.com/rust-lang/crates.io-index"
264
+ checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
265
+ dependencies = [
266
+ "objc2",
267
+ ]
268
+
260
269
  [[package]]
261
270
  name = "bumpalo"
262
271
  version = "3.20.3"
@@ -508,16 +517,22 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
508
517
 
509
518
  [[package]]
510
519
  name = "desklink-host"
511
- version = "0.1.0"
520
+ version = "0.1.1"
512
521
  dependencies = [
513
522
  "anyhow",
514
523
  "ashpd",
515
524
  "async-trait",
525
+ "block2",
516
526
  "bytes",
517
527
  "cc",
518
528
  "futures-util",
519
529
  "libc",
520
530
  "log",
531
+ "objc2-core-foundation",
532
+ "objc2-core-graphics",
533
+ "objc2-core-media",
534
+ "objc2-foundation",
535
+ "objc2-screen-capture-kit",
521
536
  "pipewire",
522
537
  "rand 0.8.8",
523
538
  "rtc",
@@ -541,6 +556,16 @@ dependencies = [
541
556
  "subtle",
542
557
  ]
543
558
 
559
+ [[package]]
560
+ name = "dispatch2"
561
+ version = "0.3.1"
562
+ source = "registry+https://github.com/rust-lang/crates.io-index"
563
+ checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
564
+ dependencies = [
565
+ "bitflags 2.13.2",
566
+ "objc2",
567
+ ]
568
+
544
569
  [[package]]
545
570
  name = "displaydoc"
546
571
  version = "0.2.7"
@@ -1211,6 +1236,175 @@ dependencies = [
1211
1236
  "autocfg",
1212
1237
  ]
1213
1238
 
1239
+ [[package]]
1240
+ name = "objc2"
1241
+ version = "0.6.4"
1242
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1243
+ checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
1244
+ dependencies = [
1245
+ "objc2-encode",
1246
+ ]
1247
+
1248
+ [[package]]
1249
+ name = "objc2-av-foundation"
1250
+ version = "0.3.2"
1251
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1252
+ checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0"
1253
+ dependencies = [
1254
+ "objc2",
1255
+ "objc2-foundation",
1256
+ ]
1257
+
1258
+ [[package]]
1259
+ name = "objc2-core-audio"
1260
+ version = "0.3.2"
1261
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1262
+ checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
1263
+ dependencies = [
1264
+ "dispatch2",
1265
+ "objc2",
1266
+ "objc2-core-audio-types",
1267
+ "objc2-core-foundation",
1268
+ ]
1269
+
1270
+ [[package]]
1271
+ name = "objc2-core-audio-types"
1272
+ version = "0.3.2"
1273
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1274
+ checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
1275
+ dependencies = [
1276
+ "bitflags 2.13.2",
1277
+ "objc2",
1278
+ ]
1279
+
1280
+ [[package]]
1281
+ name = "objc2-core-foundation"
1282
+ version = "0.3.2"
1283
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1284
+ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
1285
+ dependencies = [
1286
+ "bitflags 2.13.2",
1287
+ "block2",
1288
+ "dispatch2",
1289
+ "libc",
1290
+ "objc2",
1291
+ ]
1292
+
1293
+ [[package]]
1294
+ name = "objc2-core-graphics"
1295
+ version = "0.3.2"
1296
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1297
+ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
1298
+ dependencies = [
1299
+ "bitflags 2.13.2",
1300
+ "block2",
1301
+ "dispatch2",
1302
+ "libc",
1303
+ "objc2",
1304
+ "objc2-core-foundation",
1305
+ "objc2-io-surface",
1306
+ "objc2-metal",
1307
+ ]
1308
+
1309
+ [[package]]
1310
+ name = "objc2-core-media"
1311
+ version = "0.3.2"
1312
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1313
+ checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"
1314
+ dependencies = [
1315
+ "bitflags 2.13.2",
1316
+ "block2",
1317
+ "dispatch2",
1318
+ "objc2",
1319
+ "objc2-core-audio",
1320
+ "objc2-core-audio-types",
1321
+ "objc2-core-foundation",
1322
+ "objc2-core-video",
1323
+ ]
1324
+
1325
+ [[package]]
1326
+ name = "objc2-core-video"
1327
+ version = "0.3.2"
1328
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1329
+ checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
1330
+ dependencies = [
1331
+ "bitflags 2.13.2",
1332
+ "objc2",
1333
+ "objc2-core-foundation",
1334
+ "objc2-core-graphics",
1335
+ "objc2-io-surface",
1336
+ ]
1337
+
1338
+ [[package]]
1339
+ name = "objc2-encode"
1340
+ version = "4.1.0"
1341
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1342
+ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
1343
+
1344
+ [[package]]
1345
+ name = "objc2-foundation"
1346
+ version = "0.3.2"
1347
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1348
+ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
1349
+ dependencies = [
1350
+ "bitflags 2.13.2",
1351
+ "block2",
1352
+ "libc",
1353
+ "objc2",
1354
+ "objc2-core-foundation",
1355
+ ]
1356
+
1357
+ [[package]]
1358
+ name = "objc2-io-surface"
1359
+ version = "0.3.2"
1360
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1361
+ checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
1362
+ dependencies = [
1363
+ "bitflags 2.13.2",
1364
+ "objc2",
1365
+ "objc2-core-foundation",
1366
+ ]
1367
+
1368
+ [[package]]
1369
+ name = "objc2-metal"
1370
+ version = "0.3.2"
1371
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1372
+ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
1373
+ dependencies = [
1374
+ "bitflags 2.13.2",
1375
+ "objc2",
1376
+ "objc2-foundation",
1377
+ ]
1378
+
1379
+ [[package]]
1380
+ name = "objc2-screen-capture-kit"
1381
+ version = "0.3.2"
1382
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1383
+ checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394"
1384
+ dependencies = [
1385
+ "bitflags 2.13.2",
1386
+ "block2",
1387
+ "dispatch2",
1388
+ "libc",
1389
+ "objc2",
1390
+ "objc2-av-foundation",
1391
+ "objc2-core-foundation",
1392
+ "objc2-core-graphics",
1393
+ "objc2-core-media",
1394
+ "objc2-foundation",
1395
+ "objc2-uniform-type-identifiers",
1396
+ ]
1397
+
1398
+ [[package]]
1399
+ name = "objc2-uniform-type-identifiers"
1400
+ version = "0.3.2"
1401
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1402
+ checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4"
1403
+ dependencies = [
1404
+ "objc2",
1405
+ "objc2-foundation",
1406
+ ]
1407
+
1214
1408
  [[package]]
1215
1409
  name = "oid-registry"
1216
1410
  version = "0.7.1"
package/engine/Cargo.toml CHANGED
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
+ rust-version = "1.97.1"
5
6
  license = "Apache-2.0"
6
7
  description = "Per-user on-demand desktop capture, encode and input engine speaking a versioned local control protocol"
7
8
  publish = false
@@ -11,23 +12,42 @@ name = "desklink-host"
11
12
  path = "src/main.rs"
12
13
 
13
14
  [dependencies]
14
- anyhow = "1"
15
15
  serde = { version = "1", features = ["derive"] }
16
16
  serde_json = "1"
17
+
18
+ [target.'cfg(target_os = "linux")'.dependencies]
19
+ anyhow = "1"
17
20
  tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
18
21
  futures-util = "0.3"
19
- ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
20
- pipewire = { version = "0.10", features = ["v0_3_44"] }
21
22
  libc = "0.2"
22
23
  bytes = "1"
23
24
  webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
24
25
  rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
26
+ log = "0.4"
27
+ async-trait = "0.1"
28
+ rand = "0.8"
29
+ ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
30
+ pipewire = { version = "0.10", features = ["v0_3_44"] }
25
31
  xkbcommon = "0.8"
26
32
  x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest"] }
27
33
  wl-clipboard-rs = "0.9"
28
- log = "0.4"
34
+
35
+ [target.'cfg(target_os = "macos")'.dependencies]
36
+ anyhow = "1"
29
37
  async-trait = "0.1"
38
+ bytes = "1"
39
+ libc = "0.2"
40
+ log = "0.4"
30
41
  rand = "0.8"
42
+ rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
43
+ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
44
+ webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
45
+ objc2-screen-capture-kit = { version = "0.3.2", features = ["SCStream", "SCShareableContent", "SCError", "SCScreenshotManager", "block2", "dispatch2", "objc2-core-media", "objc2-core-graphics"] }
46
+ objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSError", "NSString"] }
47
+ objc2-core-media = "0.3.2"
48
+ objc2-core-foundation = "0.3.2"
49
+ objc2-core-graphics = { version = "0.3.2", features = ["CGImage", "CGDataProvider"] }
50
+ block2 = "0.6"
31
51
 
32
52
  [build-dependencies]
33
53
  cc = "1"
package/engine/build.rs CHANGED
@@ -1,7 +1,8 @@
1
1
  //! Native dependencies of the engine.
2
2
  //!
3
- //! Two permissive C libraries, both built or linked from this machine's own
4
- //! sources — no prebuilt binary is fetched, and nothing is installed:
3
+ //! The Linux build uses two permissive C libraries, both built or linked from
4
+ //! this machine's own sources — no prebuilt binary is fetched, and nothing is
5
+ //! installed:
5
6
  //!
6
7
  //! * libvpx (BSD-3-Clause) — VP9 encode, through `native/vpx_shim.c` so the
7
8
  //! versioned encoder config struct is laid out by a C compiler.
@@ -9,9 +10,9 @@
9
10
  //! over `uinput`/`libevdev`, built by its own CMake project into a static
10
11
  //! library and called through its documented C API.
11
12
  //!
12
- //! Both are required to build the engine; the README lists the system packages
13
- //! they need. Whether input can actually be injected is a separate, runtime
14
- //! question the engine answers through its own `/dev/uinput` probe.
13
+ //! macOS builds only the libvpx shim; ScreenCaptureKit, Quartz input and
14
+ //! AppKit clipboard use native frameworks. Linux runtime input access is still
15
+ //! checked through `/dev/uinput`.
15
16
  //!
16
17
  //! `DESKLINK_VPX_STATIC_DIR` names a libvpx install prefix (`include/`, `lib/`)
17
18
  //! to link statically instead. The prebuilt engine uses it, because libvpx's
@@ -21,40 +22,114 @@
21
22
  use std::path::{Path, PathBuf};
22
23
 
23
24
  fn main() -> Result<(), Box<dyn std::error::Error>> {
25
+ println!("cargo:rustc-check-cfg=cfg(desklink_vpx)");
26
+ println!("cargo:rustc-check-cfg=cfg(desklink_macos_cli)");
24
27
  println!("cargo:rerun-if-changed=native/vpx_shim.c");
25
28
  println!("cargo:rerun-if-changed=native/inputtino_shim.cpp");
29
+ println!("cargo:rerun-if-changed=native/mac_stream.mm");
30
+ println!("cargo:rerun-if-changed=native/agent_overlay_mac.m");
31
+ println!("cargo:rerun-if-changed=native/agent_overlay_x11.c");
32
+ println!("cargo:rerun-if-changed=native/agent_overlay_wayland.c");
33
+ println!("cargo:rerun-if-changed=Info.plist");
26
34
  println!("cargo:rerun-if-changed=vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp");
27
35
  println!("cargo:rerun-if-changed=vendor/inputtino/include/inputtino/input.h");
28
36
  println!("cargo:rerun-if-changed=vendor/inputtino/CMakeLists.txt");
29
37
 
30
38
  println!("cargo:rerun-if-env-changed=DESKLINK_VPX_STATIC_DIR");
39
+ println!("cargo:rerun-if-env-changed=DESKLINK_MACOS_CLI");
31
40
 
32
- let static_vpx = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from);
33
- let mut shim = cc::Build::new();
34
- shim.file("native/vpx_shim.c")
35
- .flag_if_supported("-Wno-unused-parameter");
36
- if let Some(prefix) = &static_vpx {
37
- shim.include(prefix.join("include"));
41
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
42
+ if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
43
+ if !prefix.join("include/vpx/vp8cx.h").exists() || !prefix.join("lib/libvpx.a").exists() {
44
+ return Err(format!("DESKLINK_VPX_STATIC_DIR must be the libvpx install prefix containing include/vpx/vp8cx.h and lib/libvpx.a, not its lib directory: {}", prefix.display()).into());
45
+ }
46
+ }
47
+ if target_os == "macos" && std::env::var_os("DESKLINK_MACOS_CLI").is_some() {
48
+ println!("cargo:rustc-cfg=desklink_macos_cli");
49
+ }
50
+ if target_os == "linux" {
51
+ let static_vpx = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from);
52
+ let mut shim = cc::Build::new();
53
+ shim.file("native/vpx_shim.c")
54
+ .flag_if_supported("-Wno-unused-parameter");
55
+ if let Some(prefix) = &static_vpx {
56
+ shim.include(prefix.join("include"));
57
+ }
58
+ shim.compile("dlvpx");
59
+ match &static_vpx {
60
+ Some(prefix) => {
61
+ println!(
62
+ "cargo:rustc-link-search=native={}",
63
+ prefix.join("lib").display()
64
+ );
65
+ println!("cargo:rustc-link-lib=static=vpx");
66
+ }
67
+ None => println!("cargo:rustc-link-lib=vpx"),
68
+ }
38
69
  }
39
- shim.compile("dlvpx");
40
- match &static_vpx {
41
- Some(prefix) => {
70
+
71
+ if target_os == "linux" {
72
+ cc::Build::new()
73
+ .file("native/agent_overlay_x11.c")
74
+ .file("native/agent_overlay_wayland.c")
75
+ .compile("dlagentoverlay");
76
+ for lib in ["X11", "Xcursor", "wayland-client", "m"] {
77
+ println!("cargo:rustc-link-lib={lib}");
78
+ }
79
+ cc::Build::new()
80
+ .cpp(true)
81
+ .std("c++17")
82
+ .include("vendor/inputtino/src/uinput/include")
83
+ .file("native/inputtino_shim.cpp")
84
+ .compile("dlinputkey");
85
+ build_inputtino(&PathBuf::from(std::env::var("OUT_DIR")?))?;
86
+ } else if target_os == "macos" {
87
+ cc::Build::new()
88
+ .cpp(true)
89
+ .file("native/mac_stream.mm")
90
+ .flag("-fobjc-arc")
91
+ .compile("dlmacstream");
92
+ cc::Build::new()
93
+ .file("native/agent_overlay_mac.m")
94
+ .flag("-fobjc-arc")
95
+ .compile("dlagentoverlay");
96
+ for framework in [
97
+ "ScreenCaptureKit",
98
+ "CoreVideo",
99
+ "CoreMedia",
100
+ "Foundation",
101
+ "AppKit",
102
+ "QuartzCore",
103
+ ] {
104
+ println!("cargo:rustc-link-lib=framework={framework}");
105
+ }
106
+ if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
107
+ cc::Build::new()
108
+ .file("native/vpx_shim.c")
109
+ .include(prefix.join("include"))
110
+ .compile("dlvpx");
42
111
  println!(
43
112
  "cargo:rustc-link-search=native={}",
44
113
  prefix.join("lib").display()
45
114
  );
46
115
  println!("cargo:rustc-link-lib=static=vpx");
116
+ println!("cargo:rustc-cfg=desklink_vpx");
117
+ }
118
+ println!("cargo:rustc-link-lib=framework=CoreGraphics");
119
+ println!("cargo:rustc-link-lib=framework=ApplicationServices");
120
+ println!("cargo:rustc-link-lib=framework=Carbon");
121
+ // The dev harness embeds its bundle identity for local TCC qualification.
122
+ // The published CLI must inherit the responsible app's TCC identity instead.
123
+ if std::env::var_os("DESKLINK_MACOS_CLI").is_none() {
124
+ println!("cargo:rustc-link-arg-bins=-sectcreate");
125
+ println!("cargo:rustc-link-arg-bins=__TEXT");
126
+ println!("cargo:rustc-link-arg-bins=__info_plist");
127
+ println!(
128
+ "cargo:rustc-link-arg-bins={}",
129
+ Path::new("Info.plist").canonicalize()?.display()
130
+ );
47
131
  }
48
- None => println!("cargo:rustc-link-lib=vpx"),
49
132
  }
50
-
51
- cc::Build::new()
52
- .cpp(true)
53
- .std("c++17")
54
- .include("vendor/inputtino/src/uinput/include")
55
- .file("native/inputtino_shim.cpp")
56
- .compile("dlinputkey");
57
- build_inputtino(&PathBuf::from(std::env::var("OUT_DIR")?))?;
58
133
  Ok(())
59
134
  }
60
135