@desklink/host 0.1.1 → 0.3.0
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 +23 -0
- package/README.md +107 -55
- package/bin/desklink-host.mjs +2 -2
- 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.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keeper.d.ts +52 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +101 -0
- package/dist/keeper.js.map +1 -0
- package/dist/protocol.d.ts +26 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +3 -3
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +24 -11
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +20 -8
- package/docs/PROTOCOL.md +347 -24
- package/engine/Cargo.lock +47 -1
- package/engine/Cargo.toml +13 -2
- package/engine/build.rs +28 -2
- 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 +75 -55
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/convert.rs +286 -61
- 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/keeper.rs +622 -0
- package/engine/src/mac.rs +9 -6
- package/engine/src/main.rs +64 -4
- package/engine/src/peer.rs +887 -42
- package/engine/src/protocol.rs +153 -4
- package/engine/src/session.rs +1547 -137
- package/engine/src/x11.rs +210 -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 +3 -2
package/docs/PROTOCOL.md
CHANGED
|
@@ -23,7 +23,10 @@ to `stderr`. One JSON object per line, no length prefix, UTF-8.
|
|
|
23
23
|
Why stdio rather than a socket: the consumer already holds the process, so the
|
|
24
24
|
channel inherits the consumer's own access control (same uid, same session, no
|
|
25
25
|
filesystem permission to get wrong) and no second authorization mechanism has to
|
|
26
|
-
exist. A consumer that wants a socket can wrap this process.
|
|
26
|
+
exist. A consumer that wants a socket can wrap this process. (A screen that
|
|
27
|
+
needs no protocol at all — a private `Xvfb` whose windows should simply be kept
|
|
28
|
+
in order and reported — can run the engine as a display keeper instead; see
|
|
29
|
+
"Display keeper" below.)
|
|
27
30
|
|
|
28
31
|
Cancellation is by closing the consumer's end. The engine treats EOF on `stdin`
|
|
29
32
|
exactly like `shutdown`: it releases held input, stops capture, closes peers, and
|
|
@@ -63,7 +66,7 @@ Request parameters use the engine's own snake_case names (`session_id`,
|
|
|
63
66
|
Result:
|
|
64
67
|
|
|
65
68
|
```jsonc
|
|
66
|
-
{"protocol":3,"engine":"desklink-host/0.
|
|
69
|
+
{"protocol":3,"engine":"desklink-host/0.3.0","platform":"linux", /* the rest of Capabilities */ }
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
The consumer must send `hello` first, and a version the engine does not speak is
|
|
@@ -73,8 +76,12 @@ arrives, every other request is refused the same way; `capabilities` and
|
|
|
73
76
|
|
|
74
77
|
## macOS
|
|
75
78
|
|
|
79
|
+
For engine installation and the macOS resolution opt-out, see the
|
|
80
|
+
[host installation guide](../README.md#macos).
|
|
81
|
+
|
|
76
82
|
macOS speaks protocol 3 and shares the session lifecycle below. It captures the
|
|
77
|
-
selected display with ScreenCaptureKit, encodes VP9
|
|
83
|
+
selected display with ScreenCaptureKit, encodes VP9 (and H.264 through
|
|
84
|
+
VideoToolbox, for a receiver that asks for it), and uses the same WebRTC
|
|
78
85
|
offer, ICE, control-channel, lease, and metrics flow as Linux. Capture currently
|
|
79
86
|
uses one bounded ScreenCaptureKit stream (three-frame queue); the backend may
|
|
80
87
|
supply fewer frames. Audio is not captured.
|
|
@@ -105,7 +112,7 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
105
112
|
```jsonc
|
|
106
113
|
{
|
|
107
114
|
"protocol": 3,
|
|
108
|
-
"engine": "desklink-host/0.
|
|
115
|
+
"engine": "desklink-host/0.3.0",
|
|
109
116
|
"platform": "linux",
|
|
110
117
|
"session": {"kind": "wayland"},
|
|
111
118
|
"x11": {"available": true, "size": [2560, 1440]},
|
|
@@ -116,7 +123,8 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
116
123
|
"cursor": "embedded",
|
|
117
124
|
"audio": false
|
|
118
125
|
},
|
|
119
|
-
"encode": {"codecs": ["vp9"], "hardware": false
|
|
126
|
+
"encode": {"codecs": ["vp9", "h264"], "hardware": false,
|
|
127
|
+
"h264": {"encoder": "vaapi", "hardware": true}},
|
|
120
128
|
"input": {
|
|
121
129
|
"mechanism": "inputtino/uinput",
|
|
122
130
|
"pointer": true, "wheel": true, "keyboard": true,
|
|
@@ -125,10 +133,37 @@ session may require fresh consent. The macOS engine requires a VP9-enabled build
|
|
|
125
133
|
"unavailable_reason": null,
|
|
126
134
|
"grant": "granted"
|
|
127
135
|
},
|
|
128
|
-
"clipboard": {"read": true, "write": true, "mime": ["text/plain;charset=utf-8"], "maxBytes": 262144}
|
|
136
|
+
"clipboard": {"read": true, "write": true, "mime": ["text/plain;charset=utf-8"], "maxBytes": 262144},
|
|
137
|
+
"encoded": {"codecs": ["h264"]}
|
|
129
138
|
}
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
`encode` describes what the engine codes from a captured desktop. VP9 (libvpx,
|
|
142
|
+
software; `hardware` describes it) is always there and is the default. `h264`
|
|
143
|
+
appears in `codecs` when an H.264 encoder starts on this machine right now, and
|
|
144
|
+
`encode.h264.encoder` names it: `videotoolbox` on macOS; on Linux `nvenc` or
|
|
145
|
+
`vaapi` when the GPU has an H.264 encoder, else `openh264`, Cisco's prebuilt
|
|
146
|
+
library. `encoder` is `null` when there is none. The answer to the offer picks
|
|
147
|
+
between them (see *Codecs* below).
|
|
148
|
+
|
|
149
|
+
openh264 is never part of this package: the engine loads only Cisco's own
|
|
150
|
+
binary, checked against its published SHA-256, from
|
|
151
|
+
`$XDG_CACHE_HOME/desklink/openh264/` (else `~/.cache/desklink/openh264/`). When a
|
|
152
|
+
Linux machine has no hardware H.264 encoder and the library is not there yet,
|
|
153
|
+
the first captured `session.open` downloads it from Cisco
|
|
154
|
+
(`ciscobinary.openh264.org`) before making its offer; that session is offered
|
|
155
|
+
VP9 alone if the download fails. `capabilities` itself never downloads.
|
|
156
|
+
`DESKLINK_OPENH264=off` in the engine's environment disables openh264 (no
|
|
157
|
+
download, no load); `DESKLINK_OPENH264=<path>` loads that file instead, still
|
|
158
|
+
only if its hash is Cisco's. `DESKLINK_H264_ENCODER=<name>` restricts the engine
|
|
159
|
+
to one H.264 encoder (`off` for none), for measuring one backend. Cisco's
|
|
160
|
+
licence asks that this be stated: OpenH264 Video Codec provided by Cisco
|
|
161
|
+
Systems, Inc.
|
|
162
|
+
|
|
163
|
+
`encoded` lists the codecs this build will carry when the consumer supplies the
|
|
164
|
+
video itself (see *Encoded sources*): a consumer reads it rather than inferring
|
|
165
|
+
support from the engine's version string.
|
|
166
|
+
|
|
132
167
|
`input.mechanism` names the build's portal input path, `inputtino/uinput`,
|
|
133
168
|
where the engine creates its own virtual devices. A session opened against an X
|
|
134
169
|
display applies input through XTest instead, where the X server applies the
|
|
@@ -156,15 +191,17 @@ trigger a capture request.
|
|
|
156
191
|
{"id":4,"method":"session.open","params":{
|
|
157
192
|
"source": {"kind":"portal"}, // or {"kind":"x11","display":":99"}
|
|
158
193
|
// macOS: {"kind":"display","display_id":123}
|
|
194
|
+
// or {"kind":"encoded","codec":"h264","width":486,"height":1080}
|
|
159
195
|
"permissions": ["view","control","clipboard"],
|
|
160
196
|
"max_width": 3840, "max_height": 2160, // encode box (default); never upscales
|
|
161
197
|
"bitrate_kbps": 0, // 0 (default): sized to the encoded surface
|
|
162
|
-
"max_fps":
|
|
198
|
+
"max_fps": 60,
|
|
163
199
|
"ice_servers": [{"urls":["stun:..."],"username":null,"credential":null}],
|
|
164
200
|
"restore_token": null, // from a previous session.restoreToken
|
|
165
201
|
"ttl_seconds": 3600, // session lease; default 3600
|
|
166
202
|
"loopback_tcp": false, // also offer ICE over TCP on 127.0.0.1; default false
|
|
167
|
-
"agent_indicator": false
|
|
203
|
+
"agent_indicator": false, // local input feedback; default false (phone control)
|
|
204
|
+
"local_frames": true // keep frames for session.frame; default true
|
|
168
205
|
}}
|
|
169
206
|
```
|
|
170
207
|
|
|
@@ -182,8 +219,9 @@ Result:
|
|
|
182
219
|
screen cast, which is the only path that carries a Wayland user's consent;
|
|
183
220
|
`x11` reads a named X display's root window and applies input through XTest;
|
|
184
221
|
`display` selects a macOS display by the ID reported in capabilities (or the
|
|
185
|
-
main display when omitted)
|
|
186
|
-
|
|
222
|
+
main display when omitted); `encoded` carries video the consumer encoded itself.
|
|
223
|
+
The choice is normally the *consumer's* decision, because a client is not in a
|
|
224
|
+
position to know which backend a machine can offer.
|
|
187
225
|
|
|
188
226
|
`permissions` are the engine's authority for this session. `view` is required
|
|
189
227
|
to open a session. The engine enforces the remaining scopes on every input and
|
|
@@ -191,7 +229,7 @@ clipboard action and never infers them from a source, a peer or an SDP.
|
|
|
191
229
|
`control` without a working input backend is refused at `session.open` with
|
|
192
230
|
`error.code = "input-unavailable"` — the consumer must ask for `view` explicitly instead.
|
|
193
231
|
|
|
194
|
-
`max_fps` defaults to
|
|
232
|
+
`max_fps` defaults to 60 and is bounded to 1–60. It caps the encoded frame
|
|
195
233
|
rate and X11 capture loop; portal capture offers it as the preferred PipeWire
|
|
196
234
|
frame rate, and macOS sets its ScreenCaptureKit stream minimum frame interval.
|
|
197
235
|
A source may still supply fewer frames.
|
|
@@ -201,11 +239,19 @@ sets it for control sessions, never for view-only or human phone control. It
|
|
|
201
239
|
adds an eased pointer halo, click ripple, and typing pulse. On Wayland it uses
|
|
202
240
|
a click-through layer-shell surface with an edge glow; its bounded area is
|
|
203
241
|
masked before capture/diff (pixels behind the effect can remain briefly stale).
|
|
204
|
-
On bare X11 it replaces the pointer sprite instead, which
|
|
242
|
+
On bare X11 it replaces the pointer sprite instead, which the X11 grab excludes:
|
|
205
243
|
there is no edge glow on that path. macOS uses a nonactivating, click-through
|
|
206
244
|
window excluded by the ScreenCaptureKit content filter. Closing the session
|
|
207
245
|
fades and removes the indicator.
|
|
208
246
|
|
|
247
|
+
`local_frames` keeps the latest pre-encode frame and its tile hashes for
|
|
248
|
+
`session.frame` and `session.frame.changed` (see "Latest frame over the local
|
|
249
|
+
protocol"). A consumer that only streams sets it to `false`: each captured
|
|
250
|
+
frame is then converted once, for the encoder, no frame is kept, no
|
|
251
|
+
`session.frame.changed` is emitted, and `session.frame` is refused with
|
|
252
|
+
`error.code = "operation"`. The WebSocket bridge sets it to `false` unless its
|
|
253
|
+
host listens to engine events, because neither travels past the socket.
|
|
254
|
+
|
|
209
255
|
`session.open` is where the capture request happens, which is where the user's
|
|
210
256
|
consent appears. It is not implicit and it is not retried silently.
|
|
211
257
|
|
|
@@ -233,6 +279,52 @@ the SDP and candidates to the client and brings back the answer.
|
|
|
233
279
|
|
|
234
280
|
A candidate that arrives before the remote description is buffered, not dropped.
|
|
235
281
|
|
|
282
|
+
#### Codecs
|
|
283
|
+
|
|
284
|
+
For a captured desktop the offer's video section lists VP9 (payload type 98)
|
|
285
|
+
and, when `capabilities.encode.codecs` has `h264` and the encoded surface is
|
|
286
|
+
within H.264 level 5.2 (at most 36864 16x16 macroblocks, e.g. 4096x2304 — a
|
|
287
|
+
5K surface is offered VP9 alone), H.264 after it (payload type
|
|
288
|
+
102, `packetization-mode=1`, Constrained Baseline, `profile-level-id=42e034`
|
|
289
|
+
with `level-asymmetry-allowed=1`). The engine codes whichever of the two the
|
|
290
|
+
answer lists first. A receiver that applies the offer as it comes answers in
|
|
291
|
+
the offer's order and gets VP9, as before; a receiver that wants H.264 puts it
|
|
292
|
+
first — with `setCodecPreferences` before `createAnswer`, or by moving its
|
|
293
|
+
payload type to the front of the offer's `m=video` line before applying it.
|
|
294
|
+
The iOS receiver in `@desklink/client` does the latter, because iOS decodes
|
|
295
|
+
H.264 in hardware and VP9 in software; the web and Android receivers keep VP9.
|
|
296
|
+
The choice is made by the first answer and kept for the session, ICE restarts
|
|
297
|
+
included; the encoder starts after that answer, so the first frame sent is
|
|
298
|
+
already in the chosen codec. H.264 is coded at the full encoded size throughout
|
|
299
|
+
(no half-size motion), with an IDR carrying its SPS and PPS only for the first
|
|
300
|
+
frame and on a receiver's request. An `encoded` source offers H.264 alone, as
|
|
301
|
+
described under *Encoded sources*.
|
|
302
|
+
|
|
303
|
+
The offer negotiates the `playout-delay` RTP header extension
|
|
304
|
+
(`http://www.webrtc.org/experiments/rtp-hdrext/playout-delay`), and every video
|
|
305
|
+
packet asks for a playout delay of 0: a receiver that honours it shows each
|
|
306
|
+
frame when it is decoded instead of buffering frames to smooth playback. A
|
|
307
|
+
receiver may decline the extension in its answer; its packets then carry no
|
|
308
|
+
playout delay and it buffers as usual. The iOS receiver in
|
|
309
|
+
`@desklink/react-native` declines it, because `RTCView` draws a frame only when
|
|
310
|
+
its timestamp is new and zero-delay frames all carry the same one.
|
|
311
|
+
|
|
312
|
+
A client whose ICE connection goes `disconnected` may restart ICE on the same
|
|
313
|
+
peer connection instead of opening a new session: it sends
|
|
314
|
+
`session.restart_ice` with the session's id and generation, and the engine
|
|
315
|
+
re-offers — a fresh offer with a new ICE generation — as a new
|
|
316
|
+
`session.description` event, which the client answers as usual. The session
|
|
317
|
+
and its generation survive; only the ICE generation turns over. Fresh
|
|
318
|
+
candidates from the restart trickle through `session.candidate` on both sides
|
|
319
|
+
exactly as the initial ones do.
|
|
320
|
+
|
|
321
|
+
```jsonc
|
|
322
|
+
{"id":7,"method":"session.restart_ice","params":{"session_id":"…","generation":1}}
|
|
323
|
+
// → {"id":7,"result":{"accepted":true}}
|
|
324
|
+
// …then {"event":"session.description","params":{"sessionId":"…","generation":1,
|
|
325
|
+
// "description":{"type":"offer","sdp":"v=0\r\n…"}}}
|
|
326
|
+
```
|
|
327
|
+
|
|
236
328
|
With `loopback_tcp`, the engine also listens for ICE over TCP on
|
|
237
329
|
`127.0.0.1` and offers that passive candidate. It is for a client whose only
|
|
238
330
|
way to this computer is a forward of its loopback, such as an SSH tunnel: the
|
|
@@ -246,10 +338,22 @@ always does.
|
|
|
246
338
|
```jsonc
|
|
247
339
|
{"event":"session.state","params":{"sessionId":"…",
|
|
248
340
|
"capture":"streaming", // consented|streaming|ended
|
|
249
|
-
"transport":"connected", // new|connecting|connected|failed|closed
|
|
341
|
+
"transport":"connected", // new|connecting|connected|disconnected|failed|closed
|
|
250
342
|
"firstFrame":true}} // false until a frame has been encoded
|
|
251
343
|
```
|
|
252
344
|
|
|
345
|
+
`disconnected` is ICE losing the path: the picture is frozen but the session
|
|
346
|
+
is alive, and a client should show reconnecting and may restart ICE (above)
|
|
347
|
+
rather than open a new session. `failed` is terminal: the engine revokes the
|
|
348
|
+
session with code `transport` and closes it, and only a new `session.open`
|
|
349
|
+
recovers it.
|
|
350
|
+
|
|
351
|
+
While its control channel is open the engine also sends a heartbeat twice a
|
|
352
|
+
second, `{"kind":"ping"}`. It lets a client notice a stalled path within
|
|
353
|
+
about a second and a half, long before ICE consent timers declare it
|
|
354
|
+
`disconnected` — and a resumed heartbeat is the path working again. Clients
|
|
355
|
+
that predate it ignore the unknown kind.
|
|
356
|
+
|
|
253
357
|
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,
|
|
254
358
|
and the one the control channel's `hello` repeats. A session keeps the one
|
|
255
359
|
`generation` it was opened with: a different source or scale is a new
|
|
@@ -273,10 +377,26 @@ picker; restoration is silent only where the backend supports it. Cancellation
|
|
|
273
377
|
is an error, not a reason to retry or bypass consent.
|
|
274
378
|
|
|
275
379
|
```jsonc
|
|
276
|
-
{"event":"session.revoked","params":{"sessionId":"…","reason":"…"
|
|
380
|
+
{"event":"session.revoked","params":{"sessionId":"…","reason":"…",
|
|
381
|
+
"code":"transport"}} // transport|lease|error
|
|
277
382
|
```
|
|
278
383
|
|
|
279
384
|
The session ended on the engine's side; there is nothing further to drain.
|
|
385
|
+
`reason` is prose for people; `code` is what a client acts on:
|
|
386
|
+
|
|
387
|
+
| `code` | Meaning | A client should |
|
|
388
|
+
|---|---|---|
|
|
389
|
+
| `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 |
|
|
390
|
+
| `lease` | The session's `ttl_seconds` ran out | end; the grant is spent |
|
|
391
|
+
| `error` | The engine could not keep serving it, such as an encoder refusing a frame | end and report |
|
|
392
|
+
|
|
393
|
+
A session the consumer closes or replaces (`session.close`, a new
|
|
394
|
+
`session.open`, the consumer leaving) sends no `session.revoked`: the consumer
|
|
395
|
+
asked for it, and signaling sees `session.state` with `capture` `ended`. Only
|
|
396
|
+
its control channel is told, with code `closed` (below).
|
|
397
|
+
|
|
398
|
+
A code a client does not know is terminal. Engines before this field sent no
|
|
399
|
+
`code`; treat its absence as terminal too.
|
|
280
400
|
|
|
281
401
|
Metrics are a request, not a notification:
|
|
282
402
|
|
|
@@ -286,16 +406,56 @@ Metrics are a request, not a notification:
|
|
|
286
406
|
|
|
287
407
|
```jsonc
|
|
288
408
|
{"id":7,"result":{"captured_frames":812,"dropped_frames":3,"encoded_frames":809,
|
|
289
|
-
"encoded_bytes":12345678,"key_frames":1,"refined_frames":37,
|
|
290
|
-
"encode_micros":7390000,"target_kbps":16430,"input_applied":44,"input_rejected":0
|
|
409
|
+
"encoded_bytes":12345678,"key_frames":1,"refined_frames":37,"halved_frames":0,"motion_frames":770,
|
|
410
|
+
"encode_micros":7390000,"target_kbps":16430,"input_applied":44,"input_rejected":0,
|
|
411
|
+
"fed_frames":0,"input_forwarded":0,"codec":"vp9","encoder":"libvpx",
|
|
412
|
+
"capture_micros":14100000,"convert_micros":3200000,
|
|
413
|
+
"queue_micros":410000,"send_micros":180000}}
|
|
291
414
|
```
|
|
292
415
|
|
|
293
416
|
`dropped_frames` counts frames superseded by a newer one before they were coded;
|
|
294
|
-
the newest frame is always coded.
|
|
417
|
+
the newest frame is always coded. That includes frames that arrived while a
|
|
418
|
+
large one (a key frame, a refinement) was still crossing the link: the stream
|
|
419
|
+
runs at most about 60 ms of its rate target ahead of it, so frames never queue
|
|
420
|
+
behind a burst. `refined_frames` counts refinement passes: a
|
|
295
421
|
desktop that stops changing is coded once more at a fine quantizer, so it reads
|
|
296
|
-
sharp.
|
|
297
|
-
|
|
298
|
-
|
|
422
|
+
sharp. `halved_frames` counts motion frames coded at half width and height: when
|
|
423
|
+
full-size frames that change much of the picture take longer to code than a
|
|
424
|
+
frame interval while a newer frame is already waiting, several frames in a row,
|
|
425
|
+
motion is coded at half size until the desktop stops or its changes shrink to
|
|
426
|
+
small ones that code in time; the refinement pass is always full size. Key
|
|
427
|
+
frames are sent only for the first frame, when the receiver asks (RTCP PLI or
|
|
428
|
+
FIR), and when the coded size changes that way, so a receiver never has to scale
|
|
429
|
+
references across a size change. `target_kbps` is the current rate target after
|
|
430
|
+
any back-off: for a standing queue on the path, measured from the receiver's
|
|
431
|
+
transport-wide congestion feedback (RTP `transport-cc`), and for the loss
|
|
432
|
+
its receiver reports carry. `codec` is what frames go out in
|
|
433
|
+
(`vp9` or `h264`) and `encoder` what codes them (`libvpx`, `videotoolbox`,
|
|
434
|
+
`nvenc`, `vaapi` or `openh264`); both are empty for an encoded source.
|
|
435
|
+
|
|
436
|
+
Per-stage timings (the L0 lab harness reads these, and every later lane cites
|
|
437
|
+
them). Each is a cumulative microsecond counter; divide by the frame count
|
|
438
|
+
named beside it for the per-frame mean:
|
|
439
|
+
|
|
440
|
+
- `capture_micros` / `captured_frames`: grabbing a frame and converting it on
|
|
441
|
+
the capture thread (on X11: an MIT-SHM grab, falling back to socket
|
|
442
|
+
`GetImage` where the server lacks MIT-SHM, plus the downscale to I420), over
|
|
443
|
+
forwarded (changed) frames only — unchanged polls are excluded.
|
|
444
|
+
- `convert_micros` / `captured_frames`: tile hashing and damage computation on
|
|
445
|
+
the capture thread, after the grab; the hash branch runs only with
|
|
446
|
+
`local_frames` on.
|
|
447
|
+
- `queue_micros` / `motion_frames`: how long a freshly captured frame waited
|
|
448
|
+
between the capture handoff and the start of its encode. Refinement,
|
|
449
|
+
keepalive and receiver-requested re-encodes re-code a picture that was
|
|
450
|
+
already sent; they queue nothing and stay out of this mean.
|
|
451
|
+
- `send_micros` / `encoded_frames`: packetizing a frame and handing it to the
|
|
452
|
+
transport.
|
|
453
|
+
- `encode_micros` / `encoded_frames`: the encoder alone.
|
|
454
|
+
|
|
455
|
+
`capture + convert` per captured frame plus `queue` per motion-coded frame and
|
|
456
|
+
`encode + send` per encoded frame reconciles against engine-side wall time per
|
|
457
|
+
frame within measurement noise; a stage that will not reconcile is a stage
|
|
458
|
+
whose accounting is wrong.
|
|
299
459
|
|
|
300
460
|
## Input and clipboard: the session's control channel
|
|
301
461
|
|
|
@@ -327,14 +487,15 @@ are JSON, one per message:
|
|
|
327
487
|
| `release_all` | — | explicit safety net; the engine also does this on close and on channel loss |
|
|
328
488
|
| `clipboard_read` / `clipboard_write` | `request` (echoed back), `text` | explicit, on user action; never polled |
|
|
329
489
|
|
|
330
|
-
The engine answers on the same channel:
|
|
490
|
+
The engine answers on the same channel, and sends two things unprompted:
|
|
331
491
|
|
|
332
492
|
```jsonc
|
|
333
493
|
{"kind":"hello","protocol":3,"geometry":{…}} // once, when the channel opens
|
|
494
|
+
{"kind":"ping"} // heartbeat, twice a second; see "State" above
|
|
334
495
|
{"kind":"ack","seq":44}
|
|
335
496
|
{"kind":"rejected","seq":44,"code":"coordinates","message":"(9000,4) is outside the 1280x720 surface"}
|
|
336
497
|
{"kind":"clipboard","request":"…","text":"…","truncated":false} // an empty "text" with "error" when the read failed
|
|
337
|
-
{"kind":"revoked","reason":"…"}
|
|
498
|
+
{"kind":"revoked","reason":"…","code":"closed"} // closed, or a session.revoked code
|
|
338
499
|
```
|
|
339
500
|
|
|
340
501
|
Refusal codes: `permission`, `session`, `input-replay`, `coordinates`,
|
|
@@ -354,6 +515,114 @@ layout can produce. macOS reports Unicode text support and currently maps
|
|
|
354
515
|
layout is diagnostic, not proof that this map matches it. Anything outside the
|
|
355
516
|
map is refused with `text-unsupported`.
|
|
356
517
|
|
|
518
|
+
## Encoded sources
|
|
519
|
+
|
|
520
|
+
An `encoded` source is video the **consumer** produced: a phone mirror from a
|
|
521
|
+
device helper, a simulator, a camera. The engine captures nothing, encodes
|
|
522
|
+
nothing and holds no input device for it; it packetizes what it is handed and
|
|
523
|
+
returns the client's control messages to the consumer, which owns whatever
|
|
524
|
+
device the picture came from. That is the whole point of the source: the helper
|
|
525
|
+
that already speaks H.264 keeps its own encoder, and the engine keeps the
|
|
526
|
+
WebRTC, congestion and teardown logic that is already proven on this path.
|
|
527
|
+
|
|
528
|
+
```jsonc
|
|
529
|
+
{"source": {"kind":"encoded","codec":"h264","width":486,"height":1080}}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`codec` is `h264`, the only one this build carries (`capabilities.encoded`).
|
|
533
|
+
`width`/`height` are the stream's own pixel size, chosen by the consumer because
|
|
534
|
+
it is the only party that knows them (`1..7680` by `1..4320`). Nothing is scaled:
|
|
535
|
+
`geometry.source` and `geometry.encoded` are both that size, the client aims at
|
|
536
|
+
those pixels, and the consumer maps them to its device. `max_width`/
|
|
537
|
+
`max_height`/`bitrate_kbps` are ignored for this source, and `permissions` works
|
|
538
|
+
as it does anywhere else — with one difference: `control` needs no local input
|
|
539
|
+
backend, because nothing is applied locally.
|
|
540
|
+
|
|
541
|
+
`session.open` returns before any offer exists. The offer must name the profile
|
|
542
|
+
the stream actually is (`packetization-mode=1` and the SPS's own
|
|
543
|
+
`profile-level-id`), and the only place that is written down is the stream's own
|
|
544
|
+
SPS, so the engine sends `session.description` **after** the first fed access
|
|
545
|
+
unit that carries one. A consumer must therefore not wait for the offer before
|
|
546
|
+
it starts feeding, and a consumer that never feeds an SPS gets no offer at all.
|
|
547
|
+
|
|
548
|
+
### Feeding access units
|
|
549
|
+
|
|
550
|
+
```jsonc
|
|
551
|
+
{"id":8,"method":"session.feed","params":{
|
|
552
|
+
"session_id":"…",
|
|
553
|
+
"keyframe":true, // this unit is an IDR
|
|
554
|
+
"data_b64":"AAAA…" // one Annex-B access unit, base64
|
|
555
|
+
}}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
```jsonc
|
|
559
|
+
{"id":8,"result":{"accepted":true}}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
One request is one access unit: start codes and all, exactly as the encoder
|
|
563
|
+
emitted it, with SPS/PPS in the unit that carries the IDR. `keyframe` is the
|
|
564
|
+
consumer's own statement about the unit; the engine uses it for `session.metrics`
|
|
565
|
+
and for nothing else, because the RTP payloader reads the NAL types itself. The
|
|
566
|
+
reply acknowledges the hand-off, not the picture: units are packetized behind
|
|
567
|
+
the request, and `session.metrics` reports what was sent and what was dropped.
|
|
568
|
+
|
|
569
|
+
Base64 in the same JSON line keeps one ordered channel and one inherited fd.
|
|
570
|
+
Measured, that is not a close call: one second of a 2 Mbit/s stream is 250 KB,
|
|
571
|
+
and base64 costs about 0.09 ms to encode on the consumer's side and 1.0 ms to
|
|
572
|
+
decode on the engine's — roughly a tenth of one per cent of a core, against the
|
|
573
|
+
5 % that would have justified a second fd. Decoded units are
|
|
574
|
+
limited to 8 MiB (`malformed` beyond that); a unit the buffer cannot hold right
|
|
575
|
+
away is **dropped, not queued** — a live picture is worth more than a backlog of
|
|
576
|
+
stale ones — and is counted in `dropped_frames`.
|
|
577
|
+
|
|
578
|
+
An access unit is dropped until the transport connects, because nothing sent
|
|
579
|
+
before it arrives. The engine therefore asks for a key frame the moment the
|
|
580
|
+
transport connects (below); a consumer that ignores that request shows a black
|
|
581
|
+
picture.
|
|
582
|
+
|
|
583
|
+
### Key frames the engine cannot make
|
|
584
|
+
|
|
585
|
+
```jsonc
|
|
586
|
+
{"event":"session.keyframeRequest","params":{"sessionId":"…","generation":1}}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Raised when the receiver reports a loss it cannot repair (RTCP PLI or FIR), and
|
|
590
|
+
once when the transport connects. The consumer responds by feeding a keyframe
|
|
591
|
+
access unit — for a device helper, "reset the video stream". This is the encoded
|
|
592
|
+
source's version of the reference frame the engine's own encode loop sends when
|
|
593
|
+
the same thing happens on a captured desktop.
|
|
594
|
+
|
|
595
|
+
### Input belongs to the consumer
|
|
596
|
+
|
|
597
|
+
For this source only, the control channel is **forwarded, not applied**:
|
|
598
|
+
`pointer`, `wheel`, `key`, `text` and `release_all` arrive as events, and the
|
|
599
|
+
consumer injects them wherever the picture came from (a device helper's own
|
|
600
|
+
touch injection, for instance). Coordinates are the encoded surface's own
|
|
601
|
+
pixels, exactly as a client sends them.
|
|
602
|
+
|
|
603
|
+
```jsonc
|
|
604
|
+
{"event":"session.input","params":{"sessionId":"…",
|
|
605
|
+
"input":{"kind":"pointer","phase":"down","x":100,"y":200,"button":1,"seq":4}}}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
The event carries the control message as it arrived. The engine still validates
|
|
609
|
+
permission, ordering and replay before forwarding, so the client keeps the same
|
|
610
|
+
`ack`/`rejected` behaviour and a replayed gesture still cannot be delivered
|
|
611
|
+
twice. `ack` means "accepted for this session", not "applied to a desktop" —
|
|
612
|
+
`session.metrics.input_forwarded` counts what was handed on, and `input_applied`
|
|
613
|
+
stays zero. Clipboard messages are answered with an empty `clipboard` reply and
|
|
614
|
+
an error, because this source has no desktop clipboard: the device's clipboard
|
|
615
|
+
is the consumer's to carry.
|
|
616
|
+
|
|
617
|
+
`session.frame` and `session.wait_frame` are refused with `error.code =
|
|
618
|
+
"operation"` — there is no lossless frame to read — and `captured_frames`,
|
|
619
|
+
`refined_frames`, `halved_frames`, `motion_frames`, `target_kbps`, `capture_micros`,
|
|
620
|
+
`convert_micros`, `queue_micros` and `send_micros` stay at zero while
|
|
621
|
+
`fed_frames` counts what the consumer handed over and `encoded_frames`,
|
|
622
|
+
`encoded_bytes`, `key_frames` and `encode_micros` describe what was packetized
|
|
623
|
+
and sent — on this path `encode_micros` covers packetizing and sending too,
|
|
624
|
+
because packetization is the only stage the engine itself runs.
|
|
625
|
+
|
|
357
626
|
## Latest frame over the local protocol
|
|
358
627
|
|
|
359
628
|
`session.frame` requires `view` permission. It writes the latest pre-encode frame as
|
|
@@ -372,7 +641,8 @@ of stillness. When both are supplied, both conditions must hold.
|
|
|
372
641
|
Each changed capture emits a local `session.frame.changed` event with `sessionId`,
|
|
373
642
|
`seq` and `damage`, allowing a co-located consumer to track animation across
|
|
374
643
|
observations. Neither the event nor this file-writing method is forwarded by
|
|
375
|
-
the remote WebSocket bridge.
|
|
644
|
+
the remote WebSocket bridge. A session opened with `local_frames: false` keeps
|
|
645
|
+
no frame: both are refused with `error.code = "operation"` (see `session.open`).
|
|
376
646
|
|
|
377
647
|
```json
|
|
378
648
|
{"id":20,"method":"session.frame","params":{"session_id":"…","since":7,"path":"/run/user/1000/frame.raw"}}
|
|
@@ -416,11 +686,64 @@ session that has already ended is refused with `error.code = "session"`.
|
|
|
416
686
|
|
|
417
687
|
Closes every session and exits.
|
|
418
688
|
|
|
689
|
+
## Display keeper
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
desklink-host keep --display :42
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
A kiosk window manager for one X screen the consumer created itself — typically
|
|
696
|
+
one `Xvfb` per screen, with its own `-auth` cookie file. It is not for the
|
|
697
|
+
user's desktop: it takes SubstructureRedirect on the root window, so it is that
|
|
698
|
+
screen's window manager, and a display that already has one refuses the keeper.
|
|
699
|
+
Authentication is the ambient `XAUTHORITY`; a screen the consumer created should
|
|
700
|
+
have its own cookie file, and the keeper inherits whatever the consumer's
|
|
701
|
+
environment carries. There is no handshake and no stdin: the contract is a
|
|
702
|
+
stdout JSON-lines stream, and closing the consumer's stdout end ends the keeper.
|
|
703
|
+
|
|
704
|
+
The keeper fills the screen with every normal (non-override-redirect,
|
|
705
|
+
input-output) top-level window at `0,0,W,H`, raises the newest one and keeps the
|
|
706
|
+
input focus on the newest content window, so keyboard events an engine session
|
|
707
|
+
injects on that display land where a person looking at the screen would expect. A top-level window
|
|
708
|
+
smaller than 120 px on either side is not content — an emulator draws its side
|
|
709
|
+
toolbar as a tiny second window — and is parked far off-screen instead, so a
|
|
710
|
+
capture of the screen never shows it.
|
|
711
|
+
|
|
712
|
+
On startup, and after every change on the screen, it writes one line:
|
|
713
|
+
|
|
714
|
+
```jsonc
|
|
715
|
+
{"windows":[{"id":4194305,"title":"Pricing — Acme Store - Chromium","class":"Chromium","pid":31671,"width":1280,"height":800}]}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
`windows` lists every visible top-level window, bottom of the stacking order
|
|
719
|
+
first, newest last. `id` is the X window id; `title` comes from `_NET_WM_NAME`
|
|
720
|
+
with `WM_NAME` as the fallback; `class` is the second string of `WM_CLASS` (the
|
|
721
|
+
one consumers filter on, e.g. `Chromium`, `Google-chrome`); `pid` is
|
|
722
|
+
`_NET_WM_PID`. Fields the window does not carry are `null`, never absent. A
|
|
723
|
+
change is reported after 100 ms of quiet, and never later than 300 ms after the
|
|
724
|
+
first unreported change, so a busy screen cannot starve its consumer.
|
|
725
|
+
|
|
726
|
+
A refused startup is one error line and a non-zero exit:
|
|
727
|
+
|
|
728
|
+
```jsonc
|
|
729
|
+
{"error":"another window manager"}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
The same shape reports a display that could not be opened. When the display
|
|
733
|
+
goes away — an `Xvfb` the consumer stopped, most commonly — the keeper exits 0
|
|
734
|
+
on its own; a consumer treats the process ending as the screen ending.
|
|
735
|
+
|
|
736
|
+
The keeper deliberately does not decorate, move, resize on request, or remember
|
|
737
|
+
anything across runs. A screen with no windows produces one `"windows":[]` line
|
|
738
|
+
at startup and then nothing: the absence of a report is the absence of
|
|
739
|
+
anything to show.
|
|
740
|
+
|
|
419
741
|
## What the engine deliberately does not do
|
|
420
742
|
|
|
421
743
|
- It does not decide *who* the user is. It trusts the consumer's local
|
|
422
744
|
permission decision and enforces the resulting scope; there is no second
|
|
423
|
-
account, token or pairing ceremony.
|
|
745
|
+
account, token or pairing ceremony. For consumer integration, see the
|
|
746
|
+
[BYOKit guidance](../../../README.md#why-desklink-exists).
|
|
424
747
|
- It does not open ports, register a service, install udev rules, join groups or
|
|
425
748
|
raise capabilities. Kernel input access is a separate, explicit, guided step
|
|
426
749
|
(`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,13 +536,15 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
|
|
517
536
|
|
|
518
537
|
[[package]]
|
|
519
538
|
name = "desklink-host"
|
|
520
|
-
version = "0.
|
|
539
|
+
version = "0.3.0"
|
|
521
540
|
dependencies = [
|
|
522
541
|
"anyhow",
|
|
523
542
|
"ashpd",
|
|
524
543
|
"async-trait",
|
|
544
|
+
"base64 0.22.1",
|
|
525
545
|
"block2",
|
|
526
546
|
"bytes",
|
|
547
|
+
"bzip2-rs",
|
|
527
548
|
"cc",
|
|
528
549
|
"futures-util",
|
|
529
550
|
"libc",
|
|
@@ -533,12 +554,16 @@ dependencies = [
|
|
|
533
554
|
"objc2-core-media",
|
|
534
555
|
"objc2-foundation",
|
|
535
556
|
"objc2-screen-capture-kit",
|
|
557
|
+
"openh264-sys2",
|
|
536
558
|
"pipewire",
|
|
537
559
|
"rand 0.8.8",
|
|
538
560
|
"rtc",
|
|
539
561
|
"serde",
|
|
540
562
|
"serde_json",
|
|
563
|
+
"sha2",
|
|
541
564
|
"tokio",
|
|
565
|
+
"wayland-client",
|
|
566
|
+
"wayland-protocols",
|
|
542
567
|
"webrtc",
|
|
543
568
|
"wl-clipboard-rs",
|
|
544
569
|
"x11rb",
|
|
@@ -1429,6 +1454,16 @@ version = "1.21.4"
|
|
|
1429
1454
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1430
1455
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
1431
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
|
+
|
|
1432
1467
|
[[package]]
|
|
1433
1468
|
name = "ordered-stream"
|
|
1434
1469
|
version = "0.2.0"
|
|
@@ -2238,6 +2273,17 @@ dependencies = [
|
|
|
2238
2273
|
"digest",
|
|
2239
2274
|
]
|
|
2240
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
|
+
|
|
2241
2287
|
[[package]]
|
|
2242
2288
|
name = "shlex"
|
|
2243
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.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.97.1"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -14,6 +14,7 @@ path = "src/main.rs"
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
serde = { version = "1", features = ["derive"] }
|
|
16
16
|
serde_json = "1"
|
|
17
|
+
base64 = "0.22"
|
|
17
18
|
|
|
18
19
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
19
20
|
anyhow = "1"
|
|
@@ -29,8 +30,18 @@ rand = "0.8"
|
|
|
29
30
|
ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
|
|
30
31
|
pipewire = { version = "0.10", features = ["v0_3_44"] }
|
|
31
32
|
xkbcommon = "0.8"
|
|
32
|
-
x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest"] }
|
|
33
|
+
x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest", "shm", "damage"] }
|
|
33
34
|
wl-clipboard-rs = "0.9"
|
|
35
|
+
# H.264 fallback: Cisco's prebuilt openh264, loaded at runtime (never built
|
|
36
|
+
# from source), fetched once as bzip2 and checked against a pinned sha256.
|
|
37
|
+
openh264-sys2 = { version = "0.9.8", default-features = false, features = ["libloading"] }
|
|
38
|
+
sha2 = "0.10"
|
|
39
|
+
bzip2-rs = "0.1.2"
|
|
40
|
+
|
|
41
|
+
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
|
42
|
+
# The Wayland stamp example only; already in the tree through wl-clipboard-rs.
|
|
43
|
+
wayland-client = "0.31"
|
|
44
|
+
wayland-protocols = { version = "0.32", features = ["client"] }
|
|
34
45
|
|
|
35
46
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
36
47
|
anyhow = "1"
|