@desklink/host 0.2.0 → 0.3.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 +26 -0
- package/README.md +134 -56
- package/bin/desklink-host.mjs +13 -5
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +12 -4
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +35 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +7 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +66 -14
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +267 -27
- package/engine/Cargo.lock +47 -2
- package/engine/Cargo.toml +18 -13
- package/engine/build.rs +86 -4
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/native/nvenc_shim.c +453 -0
- package/engine/native/vaapi_shim.c +727 -0
- package/engine/native/vpx_shim.c +54 -4
- package/engine/native/vt_shim.c +448 -0
- package/engine/src/capture.rs +167 -58
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/convert.rs +286 -61
- package/engine/src/cursor.rs +118 -0
- package/engine/src/encoder.rs +122 -13
- package/engine/src/h264.rs +188 -0
- package/engine/src/h264_nvenc.rs +308 -0
- package/engine/src/h264_openh264.rs +735 -0
- package/engine/src/h264_vaapi.rs +263 -0
- package/engine/src/h264_vt.rs +368 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +34 -9
- package/engine/src/main.rs +186 -26
- package/engine/src/peer.rs +466 -28
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +64 -2
- package/engine/src/session.rs +1252 -105
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +225 -9
- package/engine/src/x11_mac.rs +12 -0
- package/engine/vendor/libva/COPYING +19 -0
- package/engine/vendor/libva/va/va.h +5450 -0
- package/engine/vendor/libva/va/va_dec_av1.h +695 -0
- package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
- package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
- package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
- package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
- package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
- package/engine/vendor/libva/va/va_drm.h +61 -0
- package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
- package/engine/vendor/libva/va/va_enc_h264.h +659 -0
- package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
- package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
- package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
- package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
- package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
- package/engine/vendor/libva/va/va_fei.h +170 -0
- package/engine/vendor/libva/va/va_fei_h264.h +505 -0
- package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
- package/engine/vendor/libva/va/va_prot.h +525 -0
- package/engine/vendor/libva/va/va_version.h +87 -0
- package/engine/vendor/libva/va/va_vpp.h +1590 -0
- package/engine/vendor/nv-codec-headers/LICENSE +24 -0
- package/engine/vendor/nv-codec-headers/README +7 -0
- package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
- package/examples/reference-client.html +1 -1
- package/package.json +4 -3
package/docs/PROTOCOL.md
CHANGED
|
@@ -5,10 +5,15 @@ 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 describes the
|
|
9
|
-
engine uses the same v3 stdio/WebRTC session path, with native display
|
|
10
|
-
input and clipboard adapters.
|
|
11
|
-
|
|
8
|
+
This document describes the Linux, macOS, and Windows engine protocol. The
|
|
9
|
+
macOS engine uses the same v3 stdio/WebRTC session path, with native display
|
|
10
|
+
capture, input and clipboard adapters. A Windows build with static libvpx
|
|
11
|
+
(`DESKLINK_VPX_STATIC_DIR`) also serves consumer-fed H.264 encoded sources over
|
|
12
|
+
the shared session path. Native desktop capture, input and clipboard are not
|
|
13
|
+
implemented on Windows, and `session.open` refuses `agent_indicator` with
|
|
14
|
+
`indicator-unavailable`. Without static libvpx, the Windows compile seam
|
|
15
|
+
reports encoding unavailable and refuses `serve`. The protocol contains no
|
|
16
|
+
application concepts: no accounts, no chat, no machine ids, no pane ids.
|
|
12
17
|
|
|
13
18
|
## Starting the engine
|
|
14
19
|
|
|
@@ -66,7 +71,7 @@ Request parameters use the engine's own snake_case names (`session_id`,
|
|
|
66
71
|
Result:
|
|
67
72
|
|
|
68
73
|
```jsonc
|
|
69
|
-
{"protocol":3,"engine":"desklink-host/0.
|
|
74
|
+
{"protocol":3,"engine":"desklink-host/0.3.0","platform":"linux", /* the rest of Capabilities */ }
|
|
70
75
|
```
|
|
71
76
|
|
|
72
77
|
The consumer must send `hello` first, and a version the engine does not speak is
|
|
@@ -76,8 +81,12 @@ arrives, every other request is refused the same way; `capabilities` and
|
|
|
76
81
|
|
|
77
82
|
## macOS
|
|
78
83
|
|
|
84
|
+
For engine installation and the macOS resolution opt-out, see the
|
|
85
|
+
[host installation guide](../README.md#macos-preview).
|
|
86
|
+
|
|
79
87
|
macOS speaks protocol 3 and shares the session lifecycle below. It captures the
|
|
80
|
-
selected display with ScreenCaptureKit, encodes VP9
|
|
88
|
+
selected display with ScreenCaptureKit, encodes VP9 (and H.264 through
|
|
89
|
+
VideoToolbox, for a receiver that asks for it), and uses the same WebRTC
|
|
81
90
|
offer, ICE, control-channel, lease, and metrics flow as Linux. Capture currently
|
|
82
91
|
uses one bounded ScreenCaptureKit stream (three-frame queue); the backend may
|
|
83
92
|
supply fewer frames. Audio is not captured.
|
|
@@ -108,7 +117,7 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
108
117
|
```jsonc
|
|
109
118
|
{
|
|
110
119
|
"protocol": 3,
|
|
111
|
-
"engine": "desklink-host/0.
|
|
120
|
+
"engine": "desklink-host/0.3.0",
|
|
112
121
|
"platform": "linux",
|
|
113
122
|
"session": {"kind": "wayland"},
|
|
114
123
|
"x11": {"available": true, "size": [2560, 1440]},
|
|
@@ -117,9 +126,12 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
117
126
|
"backends": ["portal-screencast+pipewire", "x11-root"],
|
|
118
127
|
"formats": ["bgrx", "bgra", "rgbx", "rgba"],
|
|
119
128
|
"cursor": "embedded",
|
|
129
|
+
"cursor_modes": ["embedded", "hidden"],
|
|
130
|
+
"cursor_positions": {"x11": true, "portal": "negotiated"},
|
|
120
131
|
"audio": false
|
|
121
132
|
},
|
|
122
|
-
"encode": {"codecs": ["vp9"], "hardware": false
|
|
133
|
+
"encode": {"codecs": ["vp9", "h264"], "hardware": false,
|
|
134
|
+
"h264": {"encoder": "vaapi", "hardware": true}},
|
|
123
135
|
"input": {
|
|
124
136
|
"mechanism": "inputtino/uinput",
|
|
125
137
|
"pointer": true, "wheel": true, "keyboard": true,
|
|
@@ -133,6 +145,32 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
133
145
|
}
|
|
134
146
|
```
|
|
135
147
|
|
|
148
|
+
`encode` describes what the engine codes from a captured desktop. VP9 (libvpx,
|
|
149
|
+
software; `hardware` describes it) is always there and is the default. `h264`
|
|
150
|
+
appears in `codecs` when an H.264 encoder starts on this machine right now, and
|
|
151
|
+
`encode.h264.encoder` names it: `videotoolbox` on macOS; on Linux `nvenc` or
|
|
152
|
+
`vaapi` when the GPU has an H.264 encoder, else `openh264`, Cisco's prebuilt
|
|
153
|
+
library. `encoder` is `null` when there is none. The answer to the offer picks
|
|
154
|
+
between them (see *Codecs* below). Windows with static libvpx reports VP9 in
|
|
155
|
+
`encode.codecs`, but has no native desktop capture; without static libvpx its
|
|
156
|
+
`encode.codecs` is empty. On Windows, `encoded.codecs` reports H.264 only when
|
|
157
|
+
static libvpx is linked, for consumer-fed sources; it does not mean the engine
|
|
158
|
+
can encode H.264 itself.
|
|
159
|
+
|
|
160
|
+
openh264 is never part of this package: the engine loads only Cisco's own
|
|
161
|
+
binary, checked against its published SHA-256, from
|
|
162
|
+
`$XDG_CACHE_HOME/desklink/openh264/` (else `~/.cache/desklink/openh264/`). When a
|
|
163
|
+
Linux machine has no hardware H.264 encoder and the library is not there yet,
|
|
164
|
+
the first captured `session.open` downloads it from Cisco
|
|
165
|
+
(`ciscobinary.openh264.org`) before making its offer; that session is offered
|
|
166
|
+
VP9 alone if the download fails. `capabilities` itself never downloads.
|
|
167
|
+
`DESKLINK_OPENH264=off` in the engine's environment disables openh264 (no
|
|
168
|
+
download, no load); `DESKLINK_OPENH264=<path>` loads that file instead, still
|
|
169
|
+
only if its hash is Cisco's. `DESKLINK_H264_ENCODER=<name>` restricts the engine
|
|
170
|
+
to one H.264 encoder (`off` for none), for measuring one backend. Cisco's
|
|
171
|
+
licence asks that this be stated: OpenH264 Video Codec provided by Cisco
|
|
172
|
+
Systems, Inc.
|
|
173
|
+
|
|
136
174
|
`encoded` lists the codecs this build will carry when the consumer supplies the
|
|
137
175
|
video itself (see *Encoded sources*): a consumer reads it rather than inferring
|
|
138
176
|
support from the engine's version string.
|
|
@@ -168,12 +206,13 @@ trigger a capture request.
|
|
|
168
206
|
"permissions": ["view","control","clipboard"],
|
|
169
207
|
"max_width": 3840, "max_height": 2160, // encode box (default); never upscales
|
|
170
208
|
"bitrate_kbps": 0, // 0 (default): sized to the encoded surface
|
|
171
|
-
"max_fps":
|
|
209
|
+
"max_fps": 60,
|
|
172
210
|
"ice_servers": [{"urls":["stun:..."],"username":null,"credential":null}],
|
|
173
211
|
"restore_token": null, // from a previous session.restoreToken
|
|
174
212
|
"ttl_seconds": 3600, // session lease; default 3600
|
|
175
213
|
"loopback_tcp": false, // also offer ICE over TCP on 127.0.0.1; default false
|
|
176
|
-
"agent_indicator": false
|
|
214
|
+
"agent_indicator": false, // local input feedback; default false (phone control)
|
|
215
|
+
"local_frames": true // keep frames for session.frame; default true
|
|
177
216
|
}}
|
|
178
217
|
```
|
|
179
218
|
|
|
@@ -201,7 +240,7 @@ clipboard action and never infers them from a source, a peer or an SDP.
|
|
|
201
240
|
`control` without a working input backend is refused at `session.open` with
|
|
202
241
|
`error.code = "input-unavailable"` — the consumer must ask for `view` explicitly instead.
|
|
203
242
|
|
|
204
|
-
`max_fps` defaults to
|
|
243
|
+
`max_fps` defaults to 60 and is bounded to 1–60. It caps the encoded frame
|
|
205
244
|
rate and X11 capture loop; portal capture offers it as the preferred PipeWire
|
|
206
245
|
frame rate, and macOS sets its ScreenCaptureKit stream minimum frame interval.
|
|
207
246
|
A source may still supply fewer frames.
|
|
@@ -211,11 +250,19 @@ sets it for control sessions, never for view-only or human phone control. It
|
|
|
211
250
|
adds an eased pointer halo, click ripple, and typing pulse. On Wayland it uses
|
|
212
251
|
a click-through layer-shell surface with an edge glow; its bounded area is
|
|
213
252
|
masked before capture/diff (pixels behind the effect can remain briefly stale).
|
|
214
|
-
On bare X11 it replaces the pointer sprite instead, which
|
|
253
|
+
On bare X11 it replaces the pointer sprite instead, which the X11 grab excludes:
|
|
215
254
|
there is no edge glow on that path. macOS uses a nonactivating, click-through
|
|
216
255
|
window excluded by the ScreenCaptureKit content filter. Closing the session
|
|
217
256
|
fades and removes the indicator.
|
|
218
257
|
|
|
258
|
+
`local_frames` keeps the latest pre-encode frame and its tile hashes for
|
|
259
|
+
`session.frame` and `session.frame.changed` (see "Latest frame over the local
|
|
260
|
+
protocol"). A consumer that only streams sets it to `false`: each captured
|
|
261
|
+
frame is then converted once, for the encoder, no frame is kept, no
|
|
262
|
+
`session.frame.changed` is emitted, and `session.frame` is refused with
|
|
263
|
+
`error.code = "operation"`. The WebSocket bridge sets it to `false` unless its
|
|
264
|
+
host listens to engine events, because neither travels past the socket.
|
|
265
|
+
|
|
219
266
|
`session.open` is where the capture request happens, which is where the user's
|
|
220
267
|
consent appears. It is not implicit and it is not retried silently.
|
|
221
268
|
|
|
@@ -243,6 +290,36 @@ the SDP and candidates to the client and brings back the answer.
|
|
|
243
290
|
|
|
244
291
|
A candidate that arrives before the remote description is buffered, not dropped.
|
|
245
292
|
|
|
293
|
+
#### Codecs
|
|
294
|
+
|
|
295
|
+
For a captured desktop the offer's video section lists VP9 (payload type 98)
|
|
296
|
+
and, when `capabilities.encode.codecs` has `h264` and the encoded surface is
|
|
297
|
+
within H.264 level 5.2 (at most 36864 16x16 macroblocks, e.g. 4096x2304 — a
|
|
298
|
+
5K surface is offered VP9 alone), H.264 after it (payload type
|
|
299
|
+
102, `packetization-mode=1`, Constrained Baseline, `profile-level-id=42e034`
|
|
300
|
+
with `level-asymmetry-allowed=1`). The engine codes whichever of the two the
|
|
301
|
+
answer lists first. A receiver that applies the offer as it comes answers in
|
|
302
|
+
the offer's order and gets VP9, as before; a receiver that wants H.264 puts it
|
|
303
|
+
first — with `setCodecPreferences` before `createAnswer`, or by moving its
|
|
304
|
+
payload type to the front of the offer's `m=video` line before applying it.
|
|
305
|
+
The iOS receiver in `@desklink/client` does the latter, because iOS decodes
|
|
306
|
+
H.264 in hardware and VP9 in software; the web and Android receivers keep VP9.
|
|
307
|
+
The choice is made by the first answer and kept for the session, ICE restarts
|
|
308
|
+
included; the encoder starts after that answer, so the first frame sent is
|
|
309
|
+
already in the chosen codec. H.264 is coded at the full encoded size throughout
|
|
310
|
+
(no half-size motion), with an IDR carrying its SPS and PPS only for the first
|
|
311
|
+
frame and on a receiver's request. An `encoded` source offers H.264 alone, as
|
|
312
|
+
described under *Encoded sources*.
|
|
313
|
+
|
|
314
|
+
The offer negotiates the `playout-delay` RTP header extension
|
|
315
|
+
(`http://www.webrtc.org/experiments/rtp-hdrext/playout-delay`), and every video
|
|
316
|
+
packet asks for a playout delay of 0: a receiver that honours it shows each
|
|
317
|
+
frame when it is decoded instead of buffering frames to smooth playback. A
|
|
318
|
+
receiver may decline the extension in its answer; its packets then carry no
|
|
319
|
+
playout delay and it buffers as usual. The iOS receiver in
|
|
320
|
+
`@desklink/react-native` declines it, because `RTCView` draws a frame only when
|
|
321
|
+
its timestamp is new and zero-delay frames all carry the same one.
|
|
322
|
+
|
|
246
323
|
A client whose ICE connection goes `disconnected` may restart ICE on the same
|
|
247
324
|
peer connection instead of opening a new session: it sends
|
|
248
325
|
`session.restart_ice` with the session's id and generation, and the engine
|
|
@@ -278,8 +355,9 @@ always does.
|
|
|
278
355
|
|
|
279
356
|
`disconnected` is ICE losing the path: the picture is frozen but the session
|
|
280
357
|
is alive, and a client should show reconnecting and may restart ICE (above)
|
|
281
|
-
rather than open a new session. `failed` is terminal: the engine revokes
|
|
282
|
-
closes
|
|
358
|
+
rather than open a new session. `failed` is terminal: the engine revokes the
|
|
359
|
+
session with code `transport` and closes it, and only a new `session.open`
|
|
360
|
+
recovers it.
|
|
283
361
|
|
|
284
362
|
While its control channel is open the engine also sends a heartbeat twice a
|
|
285
363
|
second, `{"kind":"ping"}`. It lets a client notice a stalled path within
|
|
@@ -310,10 +388,26 @@ picker; restoration is silent only where the backend supports it. Cancellation
|
|
|
310
388
|
is an error, not a reason to retry or bypass consent.
|
|
311
389
|
|
|
312
390
|
```jsonc
|
|
313
|
-
{"event":"session.revoked","params":{"sessionId":"…","reason":"…"
|
|
391
|
+
{"event":"session.revoked","params":{"sessionId":"…","reason":"…",
|
|
392
|
+
"code":"transport"}} // transport|lease|error
|
|
314
393
|
```
|
|
315
394
|
|
|
316
395
|
The session ended on the engine's side; there is nothing further to drain.
|
|
396
|
+
`reason` is prose for people; `code` is what a client acts on:
|
|
397
|
+
|
|
398
|
+
| `code` | Meaning | A client should |
|
|
399
|
+
|---|---|---|
|
|
400
|
+
| `transport` | The path to the client's peer was lost (ICE `failed`, or the track refused a frame) | open a new session with fresh authority; the desktop is still available |
|
|
401
|
+
| `lease` | The session's `ttl_seconds` ran out | end; the grant is spent |
|
|
402
|
+
| `error` | The engine could not keep serving it, such as an encoder refusing a frame | end and report |
|
|
403
|
+
|
|
404
|
+
A session the consumer closes or replaces (`session.close`, a new
|
|
405
|
+
`session.open`, the consumer leaving) sends no `session.revoked`: the consumer
|
|
406
|
+
asked for it, and signaling sees `session.state` with `capture` `ended`. Only
|
|
407
|
+
its control channel is told, with code `closed` (below).
|
|
408
|
+
|
|
409
|
+
A code a client does not know is terminal. Engines before this field sent no
|
|
410
|
+
`code`; treat its absence as terminal too.
|
|
317
411
|
|
|
318
412
|
Metrics are a request, not a notification:
|
|
319
413
|
|
|
@@ -323,17 +417,158 @@ Metrics are a request, not a notification:
|
|
|
323
417
|
|
|
324
418
|
```jsonc
|
|
325
419
|
{"id":7,"result":{"captured_frames":812,"dropped_frames":3,"encoded_frames":809,
|
|
326
|
-
"encoded_bytes":12345678,"key_frames":1,"refined_frames":37,
|
|
420
|
+
"encoded_bytes":12345678,"key_frames":1,"refined_frames":37,"halved_frames":0,"motion_frames":770,
|
|
327
421
|
"encode_micros":7390000,"target_kbps":16430,"input_applied":44,"input_rejected":0,
|
|
328
|
-
"fed_frames":0,"input_forwarded":0
|
|
422
|
+
"fed_frames":0,"input_forwarded":0,"codec":"vp9","encoder":"libvpx",
|
|
423
|
+
"capture_micros":14100000,"convert_micros":3200000,
|
|
424
|
+
"queue_micros":410000,"send_micros":180000}}
|
|
329
425
|
```
|
|
330
426
|
|
|
331
427
|
`dropped_frames` counts frames superseded by a newer one before they were coded;
|
|
332
|
-
the newest frame is always coded.
|
|
428
|
+
the newest frame is always coded. That includes frames that arrived while a
|
|
429
|
+
large one (a key frame, a refinement) was still crossing the link: the stream
|
|
430
|
+
runs at most about 60 ms of its rate target ahead of it, so frames never queue
|
|
431
|
+
behind a burst. `refined_frames` counts refinement passes: a
|
|
333
432
|
desktop that stops changing is coded once more at a fine quantizer, so it reads
|
|
334
|
-
sharp.
|
|
335
|
-
|
|
336
|
-
|
|
433
|
+
sharp. `halved_frames` counts motion frames coded at half width and height: when
|
|
434
|
+
full-size frames that change much of the picture take longer to code than a
|
|
435
|
+
frame interval while a newer frame is already waiting, several frames in a row,
|
|
436
|
+
motion is coded at half size until the desktop stops or its changes shrink to
|
|
437
|
+
small ones that code in time; the refinement pass is always full size. Key
|
|
438
|
+
frames are sent only for the first frame, when the receiver asks (RTCP PLI or
|
|
439
|
+
FIR), and when the coded size changes that way, so a receiver never has to scale
|
|
440
|
+
references across a size change. `target_kbps` is the current rate target after
|
|
441
|
+
any back-off: for a standing queue on the path, measured from the receiver's
|
|
442
|
+
transport-wide congestion feedback (RTP `transport-cc`), and for the loss
|
|
443
|
+
its receiver reports carry. `codec` is what frames go out in
|
|
444
|
+
(`vp9` or `h264`) and `encoder` what codes them (`libvpx`, `videotoolbox`,
|
|
445
|
+
`nvenc`, `vaapi` or `openh264`); both are empty for an encoded source.
|
|
446
|
+
|
|
447
|
+
Per-stage timings (the L0 lab harness reads these, and every later lane cites
|
|
448
|
+
them). Each is a cumulative microsecond counter; divide by the frame count
|
|
449
|
+
named beside it for the per-frame mean:
|
|
450
|
+
|
|
451
|
+
- `capture_micros` / `captured_frames`: grabbing a frame and converting it on
|
|
452
|
+
the capture thread (on X11: an MIT-SHM grab, falling back to socket
|
|
453
|
+
`GetImage` where the server lacks MIT-SHM, plus the downscale to I420), over
|
|
454
|
+
forwarded (changed) frames only — unchanged polls are excluded.
|
|
455
|
+
- `convert_micros` / `captured_frames`: tile hashing and damage computation on
|
|
456
|
+
the capture thread, after the grab; the hash branch runs only with
|
|
457
|
+
`local_frames` on.
|
|
458
|
+
- `queue_micros` / `motion_frames`: how long a freshly captured frame waited
|
|
459
|
+
between the capture handoff and the start of its encode. Refinement,
|
|
460
|
+
keepalive and receiver-requested re-encodes re-code a picture that was
|
|
461
|
+
already sent; they queue nothing and stay out of this mean.
|
|
462
|
+
- `send_micros` / `encoded_frames`: packetizing a frame and handing it to the
|
|
463
|
+
transport.
|
|
464
|
+
- `encode_micros` / `encoded_frames`: the encoder alone.
|
|
465
|
+
|
|
466
|
+
`capture + convert` per captured frame plus `queue` per motion-coded frame and
|
|
467
|
+
`encode + send` per encoded frame reconciles against engine-side wall time per
|
|
468
|
+
frame within measurement noise; a stage that will not reconcile is a stage
|
|
469
|
+
whose accounting is wrong.
|
|
470
|
+
|
|
471
|
+
## Cursor-free capture
|
|
472
|
+
|
|
473
|
+
`session.open` accepts `cursor: "hidden"` (default `"embedded"`). Existing
|
|
474
|
+
callers keep their capture behavior. Linux hidden capture requires portal
|
|
475
|
+
**Metadata** so cursor positions remain available while cursor pixels are
|
|
476
|
+
omitted. A portal without Metadata returns the typed
|
|
477
|
+
`cursor_positions_unavailable` refusal. Successful open returns
|
|
478
|
+
`cursor: {"mode":"metadata"|"hidden"|"embedded"|"unavailable",
|
|
479
|
+
"positions":true|false}`; `mode` names the portal mode accepted by the portal,
|
|
480
|
+
not just the requested option.
|
|
481
|
+
|
|
482
|
+
Linux capabilities advertise `capture.cursor_modes: ["embedded","hidden"]`
|
|
483
|
+
and `capture.cursor_positions: {"x11":true,"portal":"negotiated"}`. Portal
|
|
484
|
+
support is resolved at open: hidden capture requires Metadata and an observed
|
|
485
|
+
SPA cursor metadata buffer. A portal that offers only Hidden refuses open with
|
|
486
|
+
`cursor_positions_unavailable`; accepted hidden sessions report `positions:true`.
|
|
487
|
+
Metadata-mode buffers that contain no new cursor data do not fabricate a move
|
|
488
|
+
or a visibility change.
|
|
489
|
+
|
|
490
|
+
Hidden sessions with positions emit local protocol events independently of
|
|
491
|
+
pixel damage, including on a still X11 desktop:
|
|
492
|
+
|
|
493
|
+
```json
|
|
494
|
+
{"event":"session.cursor","params":{"sessionId":"…","x":400,"y":300,"visible":true,"timestamp_us":123456}}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`x,y` name the pointer hotspot in the selected source's coordinates
|
|
498
|
+
(`geometry.source`), before video scaling, without the desktop origin added.
|
|
499
|
+
Portal buffer coordinates are mapped to that geometry. `visible` is false
|
|
500
|
+
outside the source or when SPA supplies an invisible cursor bitmap.
|
|
501
|
+
`timestamp_us` is a monotonic engine-process clock in microseconds, sampled
|
|
502
|
+
when the capture thread observes the pointer. The same clock stamps
|
|
503
|
+
`session.frame.changed` and `session.frame` metadata, so local recording
|
|
504
|
+
consumers can compare cursor samples to frame handoff times. These are not
|
|
505
|
+
wall-clock or RTP timestamps. SPA cursor updates accompany PipeWire buffers;
|
|
506
|
+
X11 pointer queries run at the session frame cap even with no pixel damage.
|
|
507
|
+
Repeated identical positions/visibility are suppressed.
|
|
508
|
+
|
|
509
|
+
X11 root GetImage/MIT-SHM never contains the server cursor and this engine
|
|
510
|
+
never composites it, including for existing embedded-default callers. Its
|
|
511
|
+
open reply therefore reports `mode:"hidden"`; positions are enabled only by
|
|
512
|
+
an explicit `cursor:"hidden"`. macOS and Windows refuse hidden capture with
|
|
513
|
+
`cursor-unavailable` in this build. Consumer-fed encoded sources refuse it too:
|
|
514
|
+
the engine cannot remove pixels the consumer already encoded. Unknown cursor
|
|
515
|
+
options are `malformed`.
|
|
516
|
+
|
|
517
|
+
`@desklink/host` exposes `openSession({permissions:['view'], cursor:'hidden'})`
|
|
518
|
+
and typed `session.cursor` events through `onEvent`/`drainEvents`. The client
|
|
519
|
+
capture request also accepts `cursor`, and AXI accepts `start --cursor hidden`.
|
|
520
|
+
|
|
521
|
+
## Point on the host desktop
|
|
522
|
+
|
|
523
|
+
A local consumer can guide the person without taking control of their pointer
|
|
524
|
+
or keyboard. `session.point` requires `view`, not `control`, and is independent
|
|
525
|
+
of `agent_indicator`. It draws one blue ring centered at `x,y`, with an optional
|
|
526
|
+
label. A new point replaces the previous one and restarts its timer.
|
|
527
|
+
|
|
528
|
+
```jsonc
|
|
529
|
+
{"id":30,"method":"session.point","params":{"session_id":"…","x":400,"y":300,"label":"Here","timeout_ms":3000}}
|
|
530
|
+
{"id":30,"result":{"shown":true}}
|
|
531
|
+
{"id":31,"method":"session.point","params":{"session_id":"…","clear":true}}
|
|
532
|
+
{"id":31,"result":{"shown":false}}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Coordinates are integers in the **selected desktop's original pixels**, top-left
|
|
536
|
+
origin, before video scaling (`geometry.source`, not `geometry.encoded`). On X11
|
|
537
|
+
this is the root screen; on macOS it is the selected display, with native pixels
|
|
538
|
+
converted to display points. Both coordinates must be within its bounds. The
|
|
539
|
+
ring may clip at an edge; its label stays inside the display. `timeout_ms`
|
|
540
|
+
defaults to 3000 and must be an integer from 1 to 120000. Labels are at most 96
|
|
541
|
+
printable ASCII bytes, including spaces; other labels are refused rather than
|
|
542
|
+
rendered incorrectly by the X server's core font. A clear request has no
|
|
543
|
+
coordinates, label or timeout, and is idempotent. Invalid requests do not alter
|
|
544
|
+
an existing marker. Replies acknowledge submission to the local overlay helper,
|
|
545
|
+
not that a person has seen it. Timeout hides it; clear, session replacement,
|
|
546
|
+
lease expiry, close and consumer disconnection remove and reap the helper.
|
|
547
|
+
|
|
548
|
+
X11 uses an override-redirect, shaped window with an empty input shape. It never
|
|
549
|
+
warps the pointer, changes the cursor, requests focus or injects events. macOS
|
|
550
|
+
reuses the nonactivating, mouse-transparent indicator window in a separate
|
|
551
|
+
helper. **Point cues intentionally appear in local frames and outgoing video**:
|
|
552
|
+
the person and observer can see the same cue. They are separate from the
|
|
553
|
+
capture-excluded input activity indicator. Consumers reading frame differences
|
|
554
|
+
should allow for the point's appearance/disappearance.
|
|
555
|
+
|
|
556
|
+
Wayland portal sessions use a wlr-layer-shell overlay with no keyboard
|
|
557
|
+
interactivity and an empty input region. The selected monitor is matched by its
|
|
558
|
+
portal origin against compositor logical output geometry (xdg-output); with one
|
|
559
|
+
output, no origin is needed. Points map from the selected source to that output.
|
|
560
|
+
An ambiguous or missing selected output returns `indicator-unavailable`.
|
|
561
|
+
Compositors without layer-shell return `point_unsupported` with reason
|
|
562
|
+
`layer_shell_unavailable` (including GNOME Mutter). Consumer-fed encoded sources return `operation`
|
|
563
|
+
because they have no host desktop overlay target. Other refusals are `permission`, `session`,
|
|
564
|
+
`coordinates`, `malformed`, and `indicator-unavailable` (including unavailable
|
|
565
|
+
X Shape support). Unknown fields are refused. No control-channel or remote
|
|
566
|
+
bridge forwarding is added; the consumer chooses when to request a local cue.
|
|
567
|
+
|
|
568
|
+
`@desklink/host` exposes `EngineClient.point(sessionId, {x, y, label?, timeoutMs?})`
|
|
569
|
+
and `EngineClient.point(sessionId, {clear: true})`. `@desklink/axi` exposes
|
|
570
|
+
`desklink-axi point 400,300 --label "Here" --timeout 3000` and
|
|
571
|
+
`desklink-axi point --clear`, including for a session started without `--control`.
|
|
337
572
|
|
|
338
573
|
## Input and clipboard: the session's control channel
|
|
339
574
|
|
|
@@ -373,7 +608,7 @@ The engine answers on the same channel, and sends two things unprompted:
|
|
|
373
608
|
{"kind":"ack","seq":44}
|
|
374
609
|
{"kind":"rejected","seq":44,"code":"coordinates","message":"(9000,4) is outside the 1280x720 surface"}
|
|
375
610
|
{"kind":"clipboard","request":"…","text":"…","truncated":false} // an empty "text" with "error" when the read failed
|
|
376
|
-
{"kind":"revoked","reason":"…"}
|
|
611
|
+
{"kind":"revoked","reason":"…","code":"closed"} // closed, or a session.revoked code
|
|
377
612
|
```
|
|
378
613
|
|
|
379
614
|
Refusal codes: `permission`, `session`, `input-replay`, `coordinates`,
|
|
@@ -494,9 +729,12 @@ is the consumer's to carry.
|
|
|
494
729
|
|
|
495
730
|
`session.frame` and `session.wait_frame` are refused with `error.code =
|
|
496
731
|
"operation"` — there is no lossless frame to read — and `captured_frames`,
|
|
497
|
-
`refined_frames`
|
|
498
|
-
|
|
499
|
-
|
|
732
|
+
`refined_frames`, `halved_frames`, `motion_frames`, `target_kbps`, `capture_micros`,
|
|
733
|
+
`convert_micros`, `queue_micros` and `send_micros` stay at zero while
|
|
734
|
+
`fed_frames` counts what the consumer handed over and `encoded_frames`,
|
|
735
|
+
`encoded_bytes`, `key_frames` and `encode_micros` describe what was packetized
|
|
736
|
+
and sent — on this path `encode_micros` covers packetizing and sending too,
|
|
737
|
+
because packetization is the only stage the engine itself runs.
|
|
500
738
|
|
|
501
739
|
## Latest frame over the local protocol
|
|
502
740
|
|
|
@@ -516,7 +754,8 @@ of stillness. When both are supplied, both conditions must hold.
|
|
|
516
754
|
Each changed capture emits a local `session.frame.changed` event with `sessionId`,
|
|
517
755
|
`seq` and `damage`, allowing a co-located consumer to track animation across
|
|
518
756
|
observations. Neither the event nor this file-writing method is forwarded by
|
|
519
|
-
the remote WebSocket bridge.
|
|
757
|
+
the remote WebSocket bridge. A session opened with `local_frames: false` keeps
|
|
758
|
+
no frame: both are refused with `error.code = "operation"` (see `session.open`).
|
|
520
759
|
|
|
521
760
|
```json
|
|
522
761
|
{"id":20,"method":"session.frame","params":{"session_id":"…","since":7,"path":"/run/user/1000/frame.raw"}}
|
|
@@ -616,7 +855,8 @@ anything to show.
|
|
|
616
855
|
|
|
617
856
|
- It does not decide *who* the user is. It trusts the consumer's local
|
|
618
857
|
permission decision and enforces the resulting scope; there is no second
|
|
619
|
-
account, token or pairing ceremony.
|
|
858
|
+
account, token or pairing ceremony. For consumer integration, see the
|
|
859
|
+
[BYOKit guidance](../../../README.md#why-desklink-exists).
|
|
620
860
|
- It does not open ports, register a service, install udev rules, join groups or
|
|
621
861
|
raise capabilities. Kernel input access is a separate, explicit, guided step
|
|
622
862
|
(`desklink-host setup-input` reports what is missing; it does not change it).
|
package/engine/Cargo.lock
CHANGED
|
@@ -307,6 +307,16 @@ version = "1.12.1"
|
|
|
307
307
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
308
308
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
309
309
|
|
|
310
|
+
[[package]]
|
|
311
|
+
name = "bzip2-rs"
|
|
312
|
+
version = "0.1.2"
|
|
313
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
314
|
+
checksum = "beeb59e7e4c811ab37cc73680c798c7a5da77fc9989c62b09138e31ee740f735"
|
|
315
|
+
dependencies = [
|
|
316
|
+
"crc32fast",
|
|
317
|
+
"tinyvec",
|
|
318
|
+
]
|
|
319
|
+
|
|
310
320
|
[[package]]
|
|
311
321
|
name = "cc"
|
|
312
322
|
version = "1.4.7"
|
|
@@ -449,6 +459,15 @@ dependencies = [
|
|
|
449
459
|
"rustc_version",
|
|
450
460
|
]
|
|
451
461
|
|
|
462
|
+
[[package]]
|
|
463
|
+
name = "crc32fast"
|
|
464
|
+
version = "1.5.2"
|
|
465
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
466
|
+
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
|
467
|
+
dependencies = [
|
|
468
|
+
"cfg-if",
|
|
469
|
+
]
|
|
470
|
+
|
|
452
471
|
[[package]]
|
|
453
472
|
name = "crossbeam-utils"
|
|
454
473
|
version = "0.8.23"
|
|
@@ -517,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
|
|
517
536
|
|
|
518
537
|
[[package]]
|
|
519
538
|
name = "desklink-host"
|
|
520
|
-
version = "0.
|
|
539
|
+
version = "0.3.1"
|
|
521
540
|
dependencies = [
|
|
522
541
|
"anyhow",
|
|
523
542
|
"ashpd",
|
|
@@ -525,8 +544,8 @@ dependencies = [
|
|
|
525
544
|
"base64 0.22.1",
|
|
526
545
|
"block2",
|
|
527
546
|
"bytes",
|
|
547
|
+
"bzip2-rs",
|
|
528
548
|
"cc",
|
|
529
|
-
"futures-util",
|
|
530
549
|
"libc",
|
|
531
550
|
"log",
|
|
532
551
|
"objc2-core-foundation",
|
|
@@ -534,13 +553,18 @@ dependencies = [
|
|
|
534
553
|
"objc2-core-media",
|
|
535
554
|
"objc2-foundation",
|
|
536
555
|
"objc2-screen-capture-kit",
|
|
556
|
+
"openh264-sys2",
|
|
537
557
|
"pipewire",
|
|
538
558
|
"rand 0.8.8",
|
|
539
559
|
"rtc",
|
|
540
560
|
"serde",
|
|
541
561
|
"serde_json",
|
|
562
|
+
"sha2",
|
|
542
563
|
"tokio",
|
|
564
|
+
"wayland-client",
|
|
565
|
+
"wayland-protocols",
|
|
543
566
|
"webrtc",
|
|
567
|
+
"windows-sys 0.61.2",
|
|
544
568
|
"wl-clipboard-rs",
|
|
545
569
|
"x11rb",
|
|
546
570
|
"xkbcommon",
|
|
@@ -1430,6 +1454,16 @@ version = "1.21.4"
|
|
|
1430
1454
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1431
1455
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
1432
1456
|
|
|
1457
|
+
[[package]]
|
|
1458
|
+
name = "openh264-sys2"
|
|
1459
|
+
version = "0.9.8"
|
|
1460
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1461
|
+
checksum = "6ada29a4cadc13d4d326737af87c13e224210d7d99fe47594e9f3f9b0102fd70"
|
|
1462
|
+
dependencies = [
|
|
1463
|
+
"libloading",
|
|
1464
|
+
"sha2",
|
|
1465
|
+
]
|
|
1466
|
+
|
|
1433
1467
|
[[package]]
|
|
1434
1468
|
name = "ordered-stream"
|
|
1435
1469
|
version = "0.2.0"
|
|
@@ -2239,6 +2273,17 @@ dependencies = [
|
|
|
2239
2273
|
"digest",
|
|
2240
2274
|
]
|
|
2241
2275
|
|
|
2276
|
+
[[package]]
|
|
2277
|
+
name = "sha2"
|
|
2278
|
+
version = "0.10.9"
|
|
2279
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2280
|
+
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
|
2281
|
+
dependencies = [
|
|
2282
|
+
"cfg-if",
|
|
2283
|
+
"cpufeatures 0.2.17",
|
|
2284
|
+
"digest",
|
|
2285
|
+
]
|
|
2286
|
+
|
|
2242
2287
|
[[package]]
|
|
2243
2288
|
name = "shlex"
|
|
2244
2289
|
version = "1.3.0"
|
package/engine/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "desklink-host"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.97.1"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -16,10 +16,9 @@ serde = { version = "1", features = ["derive"] }
|
|
|
16
16
|
serde_json = "1"
|
|
17
17
|
base64 = "0.22"
|
|
18
18
|
|
|
19
|
-
[target.'cfg(target_os = "linux")'.dependencies]
|
|
19
|
+
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dependencies]
|
|
20
20
|
anyhow = "1"
|
|
21
21
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
|
|
22
|
-
futures-util = "0.3"
|
|
23
22
|
libc = "0.2"
|
|
24
23
|
bytes = "1"
|
|
25
24
|
webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
|
|
@@ -27,22 +26,28 @@ rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
|
|
|
27
26
|
log = "0.4"
|
|
28
27
|
async-trait = "0.1"
|
|
29
28
|
rand = "0.8"
|
|
29
|
+
|
|
30
|
+
[target.'cfg(target_os = "linux")'.dependencies]
|
|
30
31
|
ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
|
|
31
32
|
pipewire = { version = "0.10", features = ["v0_3_44"] }
|
|
32
33
|
xkbcommon = "0.8"
|
|
33
|
-
x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest"] }
|
|
34
|
+
x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest", "shm", "damage"] }
|
|
34
35
|
wl-clipboard-rs = "0.9"
|
|
36
|
+
# H.264 fallback: Cisco's prebuilt openh264, loaded at runtime (never built
|
|
37
|
+
# from source), fetched once as bzip2 and checked against a pinned sha256.
|
|
38
|
+
openh264-sys2 = { version = "0.9.8", default-features = false, features = ["libloading"] }
|
|
39
|
+
sha2 = "0.10"
|
|
40
|
+
bzip2-rs = "0.1.2"
|
|
41
|
+
|
|
42
|
+
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
|
43
|
+
# The Wayland stamp example only; already in the tree through wl-clipboard-rs.
|
|
44
|
+
wayland-client = "0.31"
|
|
45
|
+
wayland-protocols = { version = "0.32", features = ["client"] }
|
|
46
|
+
|
|
47
|
+
[target.'cfg(target_os = "windows")'.dependencies]
|
|
48
|
+
windows-sys = "0.61.2"
|
|
35
49
|
|
|
36
50
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
37
|
-
anyhow = "1"
|
|
38
|
-
async-trait = "0.1"
|
|
39
|
-
bytes = "1"
|
|
40
|
-
libc = "0.2"
|
|
41
|
-
log = "0.4"
|
|
42
|
-
rand = "0.8"
|
|
43
|
-
rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
|
|
44
|
-
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
|
|
45
|
-
webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
|
|
46
51
|
objc2-screen-capture-kit = { version = "0.3.2", features = ["SCStream", "SCShareableContent", "SCError", "SCScreenshotManager", "block2", "dispatch2", "objc2-core-media", "objc2-core-graphics"] }
|
|
47
52
|
objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSError", "NSString"] }
|
|
48
53
|
objc2-core-media = "0.3.2"
|