@desklink/host 0.1.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/LICENSE +202 -0
- package/NOTICE +44 -0
- package/README.md +266 -0
- package/bin/desklink-host.mjs +113 -0
- package/dist/bridge.d.ts +57 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +208 -0
- package/dist/bridge.js.map +1 -0
- package/dist/engineProcess.d.ts +82 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +243 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +169 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveEngine.d.ts +34 -0
- package/dist/resolveEngine.d.ts.map +1 -0
- package/dist/resolveEngine.js +103 -0
- package/dist/resolveEngine.js.map +1 -0
- package/docs/BOUNDARY.md +145 -0
- package/docs/PROTOCOL.md +363 -0
- package/engine/Cargo.lock +3077 -0
- package/engine/Cargo.toml +37 -0
- package/engine/build.rs +107 -0
- package/engine/native/inputtino_shim.cpp +11 -0
- package/engine/native/vpx_shim.c +184 -0
- package/engine/native/wrapper.h +7 -0
- package/engine/src/capture.rs +510 -0
- package/engine/src/clipboard.rs +132 -0
- package/engine/src/convert.rs +299 -0
- package/engine/src/encoder.rs +204 -0
- package/engine/src/input.rs +355 -0
- package/engine/src/keymap.rs +338 -0
- package/engine/src/main.rs +614 -0
- package/engine/src/peer.rs +775 -0
- package/engine/src/portal.rs +115 -0
- package/engine/src/protocol.rs +331 -0
- package/engine/src/session.rs +2159 -0
- package/engine/src/x11.rs +244 -0
- package/engine/vendor/inputtino/.clang-format +166 -0
- package/engine/vendor/inputtino/.clang-tidy +30 -0
- package/engine/vendor/inputtino/.dockerignore +11 -0
- package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
- package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
- package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
- package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
- package/engine/vendor/inputtino/CMakeLists.txt +187 -0
- package/engine/vendor/inputtino/LICENSE +21 -0
- package/engine/vendor/inputtino/README.md +110 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
- package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
- package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
- package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
- package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
- package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
- package/engine/vendor/inputtino/bindings/python/README.md +215 -0
- package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
- package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
- package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
- package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
- package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
- package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
- package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
- package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
- package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
- package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
- package/engine/vendor/inputtino/docker/Dockerfile +70 -0
- package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
- package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
- package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
- package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
- package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
- package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
- package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
- package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
- package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
- package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
- package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
- package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
- package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
- package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
- package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
- package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
- package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
- package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
- package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
- package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
- package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
- package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
- package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
- package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
- package/examples/reference-client.html +428 -0
- package/package.json +38 -0
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# desklink local control protocol (v2)
|
|
2
|
+
|
|
3
|
+
The host engine is a per-user, on-demand process. A consumer that already owns
|
|
4
|
+
the user's session starts it and talks to it over an inherited private channel —
|
|
5
|
+
the engine never opens a public listener, never runs as root, and never installs
|
|
6
|
+
a service.
|
|
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.
|
|
10
|
+
|
|
11
|
+
## Starting the engine
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
desklink-host serve
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The consumer spawns this process, keeps its `stdin`/`stdout` and parses them as
|
|
18
|
+
newline-delimited JSON. `stdout` carries protocol messages only; diagnostics go
|
|
19
|
+
to `stderr`. One JSON object per line, no length prefix, UTF-8.
|
|
20
|
+
|
|
21
|
+
Why stdio rather than a socket: the consumer already holds the process, so the
|
|
22
|
+
channel inherits the consumer's own access control (same uid, same session, no
|
|
23
|
+
filesystem permission to get wrong) and no second authorization mechanism has to
|
|
24
|
+
exist. A consumer that wants a socket can wrap this process.
|
|
25
|
+
|
|
26
|
+
Cancellation is by closing the consumer's end. The engine treats EOF on `stdin`
|
|
27
|
+
exactly like `shutdown`: it releases held input, stops capture, closes peers, and
|
|
28
|
+
exits.
|
|
29
|
+
|
|
30
|
+
## Message shape
|
|
31
|
+
|
|
32
|
+
Requests (consumer → engine) carry an `id`; responses repeat it. Engine-initiated
|
|
33
|
+
notifications carry `event` and never an `id`.
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
// request
|
|
37
|
+
{"id": 7, "method": "session.open", "params": { }}
|
|
38
|
+
// success
|
|
39
|
+
{"id": 7, "result": { }}
|
|
40
|
+
// failure — the request was refused or could not run
|
|
41
|
+
{"id": 7, "error": {"code": "unsupported-source", "message": "human readable"}}
|
|
42
|
+
// notification
|
|
43
|
+
{"event": "session.state", "params": { }}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`error.code` is a stable machine token; `message` is for logs, not for display.
|
|
47
|
+
|
|
48
|
+
Every engine notification carries the `sessionId` it belongs to, so a consumer
|
|
49
|
+
that has served more than one session in a process can attribute it to the
|
|
50
|
+
right one.
|
|
51
|
+
|
|
52
|
+
Request parameters use the engine's own snake_case names (`session_id`,
|
|
53
|
+
`max_width`, `sdp_mid`, …); the examples below are the wire names to send.
|
|
54
|
+
|
|
55
|
+
## Handshake
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{"id":1,"method":"hello","params":{"protocol":2}}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Result:
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{"protocol":2,"engine":"desklink-host/0.1.0","platform":"linux", /* the rest of Capabilities */ }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The consumer must send `hello` first, and a version the engine does not speak is
|
|
68
|
+
refused with `error.code = "unsupported-protocol"`. Until a supported `hello`
|
|
69
|
+
arrives, every other request is refused the same way; `capabilities` and
|
|
70
|
+
`shutdown` are the two that answer without a handshake.
|
|
71
|
+
|
|
72
|
+
## Capabilities
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{"id":2,"method":"capabilities"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"protocol": 2,
|
|
81
|
+
"engine": "desklink-host/0.1.0",
|
|
82
|
+
"platform": "linux",
|
|
83
|
+
"session": {"kind": "wayland"},
|
|
84
|
+
"x11": {"available": true, "size": [2560, 1440]},
|
|
85
|
+
"capture": {
|
|
86
|
+
"mechanism": "portal-screencast+pipewire",
|
|
87
|
+
"backends": ["portal-screencast+pipewire", "x11-root"],
|
|
88
|
+
"formats": ["bgrx", "bgra", "rgbx", "rgba"],
|
|
89
|
+
"cursor": "embedded",
|
|
90
|
+
"audio": false
|
|
91
|
+
},
|
|
92
|
+
"encode": {"codecs": ["vp9"], "hardware": false},
|
|
93
|
+
"input": {
|
|
94
|
+
"mechanism": "inputtino/uinput",
|
|
95
|
+
"pointer": true, "wheel": true, "keyboard": true,
|
|
96
|
+
"text": ["layout-reachable"],
|
|
97
|
+
"layout": "us",
|
|
98
|
+
"unavailable_reason": null,
|
|
99
|
+
"grant": "granted"
|
|
100
|
+
},
|
|
101
|
+
"clipboard": {"read": true, "write": true, "mime": ["text/plain;charset=utf-8"], "maxBytes": 262144}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`input.mechanism` names the build's portal input path, `inputtino/uinput`,
|
|
106
|
+
where the engine creates its own virtual devices. A session opened against an X
|
|
107
|
+
display applies input through XTest instead, where the X server applies the
|
|
108
|
+
events and nothing the engine does can reach another session's keyboard.
|
|
109
|
+
`input.layout` is the keymap the engine compiled for typing, taken from the
|
|
110
|
+
session's `XKB_DEFAULT_*` variables and `us` on `evdev`/`pc105` when it exports
|
|
111
|
+
none. It is not yet the compositor's live layout, so a mismatch with the
|
|
112
|
+
desktop is visible here rather than silent. `capture.backends` lists what this
|
|
113
|
+
build has, not what this machine can necessarily use.
|
|
114
|
+
|
|
115
|
+
`input.unavailable_reason` and `input.grant` describe the portal/uinput path:
|
|
116
|
+
without kernel input access a portal session may request `view`, but an open
|
|
117
|
+
requesting `control` is refused. X display capture uses XTest instead and does
|
|
118
|
+
not require that grant. The muxr host and WebSocket bridge report clipboard
|
|
119
|
+
unavailable for an X display, regardless of the engine's Wayland clipboard
|
|
120
|
+
probe. The engine never asks for privileges on its own.
|
|
121
|
+
|
|
122
|
+
`capabilities` is safe to call before any consent has been given and must not
|
|
123
|
+
trigger a capture request.
|
|
124
|
+
|
|
125
|
+
## Opening a session
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
{"id":4,"method":"session.open","params":{
|
|
129
|
+
"source": {"kind":"portal"}, // or {"kind":"x11","display":":99"}
|
|
130
|
+
"permissions": ["view","control","clipboard"],
|
|
131
|
+
"max_width": 3840, "max_height": 2160, // encode box (default); never upscales
|
|
132
|
+
"bitrate_kbps": 0, // 0 (default): sized to the encoded surface
|
|
133
|
+
"max_fps": 30,
|
|
134
|
+
"ice_servers": [{"urls":["stun:..."],"username":null,"credential":null}],
|
|
135
|
+
"restore_token": null, // from a previous session.restoreToken
|
|
136
|
+
"ttl_seconds": 3600, // session lease; default 3600
|
|
137
|
+
"loopback_tcp": false // also offer ICE over TCP on 127.0.0.1; default false
|
|
138
|
+
}}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Result:
|
|
142
|
+
|
|
143
|
+
```jsonc
|
|
144
|
+
{"sessionId":"<opaque>","generation":1,
|
|
145
|
+
"source":{"kind":"monitor","width":2560,"height":1440,"origin":{"x":0,"y":0}},
|
|
146
|
+
"geometry":{"source":{"width":2560,"height":1440},
|
|
147
|
+
"encoded":{"width":2560,"height":1440},
|
|
148
|
+
"origin":{"x":0,"y":0}}}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`source` selects the desktop. `portal` (or absent) asks the compositor for a
|
|
152
|
+
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.
|
|
167
|
+
|
|
168
|
+
`session.open` is where the capture request happens, which is where the user's
|
|
169
|
+
consent appears. It is not implicit and it is not retried silently.
|
|
170
|
+
|
|
171
|
+
### Description and ICE exchange
|
|
172
|
+
|
|
173
|
+
The engine is the media offerer; the consumer's authenticated signaling carries
|
|
174
|
+
the SDP and candidates to the client and brings back the answer.
|
|
175
|
+
|
|
176
|
+
```jsonc
|
|
177
|
+
{"event":"session.description","params":{"sessionId":"…","generation":1,
|
|
178
|
+
"description":{"type":"offer","sdp":"v=0\r\n…"}}}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```jsonc
|
|
182
|
+
{"id":5,"method":"session.description","params":{"session_id":"…","generation":1,
|
|
183
|
+
"description":{"type":"answer","sdp":"v=0\r\n…"}}}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```jsonc
|
|
187
|
+
{"event":"session.candidate","params":{"sessionId":"…","generation":1,
|
|
188
|
+
"candidate":"candidate:…","sdpMid":"0","sdpMLineIndex":0}}
|
|
189
|
+
{"id":6,"method":"session.candidate","params":{"session_id":"…","generation":1,
|
|
190
|
+
"candidate":"candidate:…","sdp_mid":"0","sdp_m_line_index":0}}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A candidate that arrives before the remote description is buffered, not dropped.
|
|
194
|
+
|
|
195
|
+
With `loopback_tcp`, the engine also listens for ICE over TCP on
|
|
196
|
+
`127.0.0.1` and offers that passive candidate. It is for a client whose only
|
|
197
|
+
way to this computer is a forward of its loopback, such as an SSH tunnel: the
|
|
198
|
+
client forwards that port over the connection it already has and connects to
|
|
199
|
+
its own end of the forward. The picture and control channel then ride that
|
|
200
|
+
forward, still inside DTLS. A client with a UDP path prefers it, as ICE
|
|
201
|
+
always does.
|
|
202
|
+
|
|
203
|
+
### State
|
|
204
|
+
|
|
205
|
+
```jsonc
|
|
206
|
+
{"event":"session.state","params":{"sessionId":"…",
|
|
207
|
+
"capture":"streaming", // consented|streaming|ended
|
|
208
|
+
"transport":"connected", // new|connecting|connected|failed|closed
|
|
209
|
+
"firstFrame":true}} // false until a frame has been encoded
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The `geometry` a client maps touch against is the one `session.open` returned,
|
|
213
|
+
and the one the control channel's `hello` repeats. A session keeps the one
|
|
214
|
+
`generation` it was opened with: a different source or scale is a new
|
|
215
|
+
`session.open`, not a generation change within a session.
|
|
216
|
+
|
|
217
|
+
```jsonc
|
|
218
|
+
{"event":"session.restoreToken","params":{"sessionId":"…","token":"…"}}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The portal handed back a restore token for a later `session.open`. The engine
|
|
222
|
+
requests persistence until explicitly revoked (`persist_mode = 2`); the portal
|
|
223
|
+
still decides whether to grant it. Tokens are single-use: consume a saved token
|
|
224
|
+
before sending it, and atomically persist each replacement in private host-local
|
|
225
|
+
storage, even if it arrives before the open response. Never log tokens or forward
|
|
226
|
+
them to a remote controller. A consumer that does not persist one can ignore this
|
|
227
|
+
notification and request consent again next time. The WebSocket bridge delivers
|
|
228
|
+
this event only to its host-local `engineOptions.onEvent`, not to sockets.
|
|
229
|
+
|
|
230
|
+
A revoked grant or unavailable source falls back to the portal's normal consent
|
|
231
|
+
picker; restoration is silent only where the backend supports it. Cancellation
|
|
232
|
+
is an error, not a reason to retry or bypass consent.
|
|
233
|
+
|
|
234
|
+
```jsonc
|
|
235
|
+
{"event":"session.revoked","params":{"sessionId":"…","reason":"…"}}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The session ended on the engine's side; there is nothing further to drain.
|
|
239
|
+
|
|
240
|
+
Metrics are a request, not a notification:
|
|
241
|
+
|
|
242
|
+
```jsonc
|
|
243
|
+
{"id":7,"method":"session.metrics","params":{"session_id":"…"}}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```jsonc
|
|
247
|
+
{"id":7,"result":{"captured_frames":812,"dropped_frames":3,"encoded_frames":809,
|
|
248
|
+
"encoded_bytes":12345678,"key_frames":1,"refined_frames":37,
|
|
249
|
+
"encode_micros":7390000,"target_kbps":16430,"input_applied":44,"input_rejected":0}}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`dropped_frames` counts frames superseded by a newer one before they were coded;
|
|
253
|
+
the newest frame is always coded. `refined_frames` counts refinement passes: a
|
|
254
|
+
desktop that stops changing is coded once more at a fine quantizer, so it reads
|
|
255
|
+
sharp. Key frames are sent only for the first frame and when the receiver asks
|
|
256
|
+
(RTCP PLI or FIR). `target_kbps` is the current rate target after any back-off
|
|
257
|
+
for loss the receiver reported.
|
|
258
|
+
|
|
259
|
+
## Input and clipboard: the session's control channel
|
|
260
|
+
|
|
261
|
+
Input does **not** travel the local protocol. It rides the WebRTC data channel
|
|
262
|
+
the engine creates for the session, which is inside the DTLS/SRTP session that
|
|
263
|
+
was opened for one authorized grant: it inherits that session's identity and dies
|
|
264
|
+
with it, so a client that kept a socket open cannot keep driving a session that
|
|
265
|
+
was revoked, and the consumer's request path never carries a pointer move.
|
|
266
|
+
|
|
267
|
+
The engine is the offerer and creates a data channel labelled `control`. Messages
|
|
268
|
+
are JSON, one per message:
|
|
269
|
+
|
|
270
|
+
```jsonc
|
|
271
|
+
{"kind":"pointer","phase":"down","x":812,"y":433,"button":1,"seq":44}
|
|
272
|
+
{"kind":"wheel","dx":0,"dy":2,"seq":45}
|
|
273
|
+
{"kind":"key","name":"Enter","down":true,"modifiers":["Control"],"seq":46}
|
|
274
|
+
{"kind":"text","text":"hello","seq":47}
|
|
275
|
+
{"kind":"release_all"}
|
|
276
|
+
{"kind":"clipboard_read","request":"<opaque>","seq":48}
|
|
277
|
+
{"kind":"clipboard_write","request":"<opaque>","text":"…","seq":49}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| kind | fields | notes |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `pointer` | `phase`: `move`\|`down`\|`up`\|`cancel`, `x`, `y`, `button` (default 1) | `x`/`y` are integers in the **encoded surface's** own pixels, i.e. `geometry.encoded` |
|
|
283
|
+
| `wheel` | `dx`, `dy` (detents; fractions allowed) | Positive `dx` scrolls right; positive `dy` scrolls down. A fraction is a smooth partial scroll on a desktop with high-resolution wheel support (120 units per detent); an X display receives whole detents as they accumulate. Native adapters normalize platform wheel signs. |
|
|
284
|
+
| `key` | `name` (a named key or modifier) or `character` (one character), `down`, `modifiers` | the engine maps `character` through the layout it compiled (`input.layout` in `capabilities`), and refuses a character that layout cannot produce |
|
|
285
|
+
| `text` | `text` (≤ 4096 bytes) | applied as real key events, not as a clipboard paste |
|
|
286
|
+
| `release_all` | — | explicit safety net; the engine also does this on close and on channel loss |
|
|
287
|
+
| `clipboard_read` / `clipboard_write` | `request` (echoed back), `text` | explicit, on user action; never polled |
|
|
288
|
+
|
|
289
|
+
The engine answers on the same channel:
|
|
290
|
+
|
|
291
|
+
```jsonc
|
|
292
|
+
{"kind":"hello","protocol":2,"geometry":{…}} // once, when the channel opens
|
|
293
|
+
{"kind":"ack","seq":44}
|
|
294
|
+
{"kind":"rejected","seq":44,"code":"coordinates","message":"(9000,4) is outside the 1280x720 surface"}
|
|
295
|
+
{"kind":"clipboard","request":"…","text":"…","truncated":false} // an empty "text" with "error" when the read failed
|
|
296
|
+
{"kind":"revoked","reason":"…"}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Refusal codes: `permission`, `session`, `input-replay`, `coordinates`,
|
|
300
|
+
`text-too-large`, `text-unsupported`, `input-unavailable`, `operation`.
|
|
301
|
+
|
|
302
|
+
`seq` is monotonic per session. A repeat or a lower value is refused with
|
|
303
|
+
`input-replay` and **no side effect**, so a replayed message cannot move the
|
|
304
|
+
pointer twice. Validation, permission and ordering checks all run before any
|
|
305
|
+
physical effect.
|
|
306
|
+
|
|
307
|
+
The engine holds its own pressed-key/button state. `close`, `release_all`,
|
|
308
|
+
control-channel loss and process exit all synthesize releases for exactly the
|
|
309
|
+
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.
|
|
313
|
+
|
|
314
|
+
## Clipboard over the local protocol
|
|
315
|
+
|
|
316
|
+
The control channel is the path a controller uses (above). A co-located consumer
|
|
317
|
+
— a test harness, or a script on the same machine — can use the local protocol
|
|
318
|
+
instead; both reach the same clipboard:
|
|
319
|
+
|
|
320
|
+
```jsonc
|
|
321
|
+
{"id":9,"method":"session.clipboard.read","params":{"session_id":"…"}}
|
|
322
|
+
{"id":9,"result":{"text":"…","truncated":false}}
|
|
323
|
+
{"id":10,"method":"session.clipboard.write","params":{"session_id":"…","text":"…"}}
|
|
324
|
+
{"id":10,"result":{"written":true}}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Refused with `error.code = "clipboard"` when the session lacks `clipboard`, or
|
|
328
|
+
when the desktop's clipboard has no text. A read is bounded by
|
|
329
|
+
`capabilities.clipboard.maxBytes`; when the desktop's text is longer, `truncated`
|
|
330
|
+
is true and the text is cut on a character boundary, so the returned text is
|
|
331
|
+
still valid.
|
|
332
|
+
|
|
333
|
+
## Close
|
|
334
|
+
|
|
335
|
+
```jsonc
|
|
336
|
+
{"id":12,"method":"session.close","params":{"session_id":"…"}}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Closing is terminal for that session: held input is released, capture stops,
|
|
340
|
+
the peer is closed and the consent-bearing PipeWire stream is dropped. A close
|
|
341
|
+
naming a generation that is no longer current is refused with
|
|
342
|
+
`error.code = "generation"` and never closes a newer session; a close for a
|
|
343
|
+
session that has already ended is refused with `error.code = "session"`.
|
|
344
|
+
|
|
345
|
+
```jsonc
|
|
346
|
+
{"id":13,"method":"shutdown"}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Closes every session and exits.
|
|
350
|
+
|
|
351
|
+
## What the engine deliberately does not do
|
|
352
|
+
|
|
353
|
+
- It does not decide *who* the user is. It trusts the consumer's local
|
|
354
|
+
permission decision and enforces the resulting scope; there is no second
|
|
355
|
+
account, token or pairing ceremony.
|
|
356
|
+
- It does not open ports, register a service, install udev rules, join groups or
|
|
357
|
+
raise capabilities. Kernel input access is a separate, explicit, guided step
|
|
358
|
+
(`desklink-host setup-input` reports what is missing; it does not change it).
|
|
359
|
+
- It does not carry audio. A phone watching and driving a desktop needs pixels
|
|
360
|
+
and input; audio is a different product decision.
|
|
361
|
+
- It does not arbitrate between two controllers. One session, one controller.
|
|
362
|
+
- It does not auto-reconnect with authority it no longer holds: after a lease
|
|
363
|
+
expiry or owner disconnect it requires a fresh `session.open`.
|