@desklink/host 0.1.0 → 0.2.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 +3 -0
- package/README.md +72 -16
- package/bin/desklink-host.mjs +14 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +5 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +17 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/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 +42 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +3 -2
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +20 -5
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +11 -6
- package/docs/PROTOCOL.md +297 -33
- package/engine/Cargo.lock +196 -1
- package/engine/Cargo.toml +26 -5
- package/engine/build.rs +98 -23
- package/engine/native/agent_overlay_mac.m +133 -0
- package/engine/native/agent_overlay_wayland.c +192 -0
- package/engine/native/agent_overlay_x11.c +103 -0
- package/engine/native/mac_stream.mm +188 -0
- package/engine/src/capture.rs +169 -19
- package/engine/src/capture_mac.rs +145 -0
- package/engine/src/clipboard_mac.rs +182 -0
- package/engine/src/convert.rs +57 -1
- package/engine/src/indicator.rs +162 -0
- package/engine/src/input.rs +12 -0
- package/engine/src/input_mac.rs +312 -0
- package/engine/src/keeper.rs +622 -0
- package/engine/src/keymap.rs +25 -0
- package/engine/src/keymap_mac.rs +209 -0
- package/engine/src/mac.rs +722 -0
- package/engine/src/main.rs +158 -2
- package/engine/src/peer.rs +429 -22
- package/engine/src/portal.rs +4 -0
- package/engine/src/portal_mac.rs +68 -0
- package/engine/src/protocol.rs +186 -3
- package/engine/src/session.rs +1307 -100
- package/engine/src/x11.rs +23 -3
- package/engine/src/x11_mac.rs +40 -0
- package/examples/reference-client.html +33 -4
- package/package.json +2 -2
package/engine/src/protocol.rs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
use serde::{Deserialize, Serialize};
|
|
5
5
|
|
|
6
|
-
pub const PROTOCOL_VERSION: u32 =
|
|
6
|
+
pub const PROTOCOL_VERSION: u32 = 3;
|
|
7
7
|
|
|
8
8
|
/// A request from the consumer to the engine.
|
|
9
9
|
#[derive(Debug, Deserialize)]
|
|
@@ -81,10 +81,34 @@ pub struct IceServerParam {
|
|
|
81
81
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
82
82
|
pub enum SourceRequest {
|
|
83
83
|
Portal,
|
|
84
|
+
Display {
|
|
85
|
+
#[serde(default)]
|
|
86
|
+
display_id: Option<u32>,
|
|
87
|
+
},
|
|
84
88
|
X11 {
|
|
85
89
|
#[serde(default)]
|
|
86
90
|
display: Option<String>,
|
|
87
91
|
},
|
|
92
|
+
/// Video the consumer already encoded, fed access unit by access unit with
|
|
93
|
+
/// `session.feed`. The engine has no capture backend, no encoder and no
|
|
94
|
+
/// input backend for this source: it packetizes what it is handed and
|
|
95
|
+
/// forwards control messages back to the consumer, which owns the device.
|
|
96
|
+
Encoded {
|
|
97
|
+
codec: EncodedCodec,
|
|
98
|
+
/// The stream's own pixel size. The consumer is the only party that
|
|
99
|
+
/// knows it, and nothing here is scaled: the client aims at these
|
|
100
|
+
/// pixels and the consumer maps them to its device.
|
|
101
|
+
width: usize,
|
|
102
|
+
height: usize,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// A codec the consumer may feed. Only H.264 is accepted today, because it is
|
|
107
|
+
/// what the device helpers emit and what the clients can already decode.
|
|
108
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
|
109
|
+
#[serde(rename_all = "lowercase")]
|
|
110
|
+
pub enum EncodedCodec {
|
|
111
|
+
H264,
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
#[derive(Debug, Deserialize)]
|
|
@@ -112,6 +136,9 @@ pub struct OpenParams {
|
|
|
112
136
|
/// whose only way here is a local forward (an SSH tunnel).
|
|
113
137
|
#[serde(default)]
|
|
114
138
|
pub loopback_tcp: bool,
|
|
139
|
+
/// Show local, capture-excluded feedback for an agent controlling this session.
|
|
140
|
+
#[serde(default)]
|
|
141
|
+
pub agent_indicator: bool,
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
/// The encode box when the consumer names none: a desktop's own pixels up to
|
|
@@ -130,6 +157,61 @@ fn default_fps() -> u32 {
|
|
|
130
157
|
30
|
|
131
158
|
}
|
|
132
159
|
|
|
160
|
+
#[derive(Debug, Deserialize)]
|
|
161
|
+
pub struct FrameParams {
|
|
162
|
+
pub session_id: String,
|
|
163
|
+
#[serde(default)]
|
|
164
|
+
pub since: Option<u64>,
|
|
165
|
+
#[serde(default)]
|
|
166
|
+
pub after_seq: Option<u64>,
|
|
167
|
+
#[serde(default)]
|
|
168
|
+
pub still_ms: Option<u64>,
|
|
169
|
+
#[serde(default)]
|
|
170
|
+
pub timeout_ms: Option<u64>,
|
|
171
|
+
#[serde(default)]
|
|
172
|
+
pub path: String,
|
|
173
|
+
#[serde(default)]
|
|
174
|
+
pub region: Option<[usize; 4]>,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// One access unit of a consumer-encoded stream.
|
|
178
|
+
///
|
|
179
|
+
/// Base64 inside the same JSON line keeps one ordered channel and one fd: one
|
|
180
|
+
/// second of a 2 Mbit/s stream costs about 1 ms to decode here and 0.09 ms to
|
|
181
|
+
/// encode on the consumer's side, against the 5 % of a core that would have
|
|
182
|
+
/// justified a second fd (see `docs/PROTOCOL.md`, "Encoded sources").
|
|
183
|
+
#[derive(Debug, Deserialize)]
|
|
184
|
+
pub struct FeedParams {
|
|
185
|
+
pub session_id: String,
|
|
186
|
+
/// True for an IDR access unit. The first one also carries the SPS whose
|
|
187
|
+
/// profile-level-id the offer negotiates.
|
|
188
|
+
pub keyframe: bool,
|
|
189
|
+
pub data_b64: String,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The largest access unit this protocol accepts, once decoded. A 4K key frame
|
|
193
|
+
/// at a quality worth sending is a few hundred kilobytes; this refuses a request
|
|
194
|
+
/// that is not a picture rather than acting as a rate limit.
|
|
195
|
+
pub const MAX_ACCESS_UNIT_BYTES: usize = 8 * 1024 * 1024;
|
|
196
|
+
|
|
197
|
+
impl FeedParams {
|
|
198
|
+
/// The access unit's own bytes, or why this request is not one.
|
|
199
|
+
pub fn access_unit(&self) -> std::result::Result<Vec<u8>, String> {
|
|
200
|
+
let data = base64::Engine::decode(
|
|
201
|
+
&base64::engine::general_purpose::STANDARD,
|
|
202
|
+
self.data_b64.as_bytes(),
|
|
203
|
+
)
|
|
204
|
+
.map_err(|error| format!("data_b64 is not base64: {error}"))?;
|
|
205
|
+
if data.len() > MAX_ACCESS_UNIT_BYTES {
|
|
206
|
+
return Err(format!(
|
|
207
|
+
"an access unit is {} bytes; the limit is {MAX_ACCESS_UNIT_BYTES}",
|
|
208
|
+
data.len()
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
Ok(data)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
133
215
|
#[derive(Debug, Deserialize)]
|
|
134
216
|
pub struct SessionRef {
|
|
135
217
|
pub session_id: String,
|
|
@@ -178,7 +260,7 @@ pub struct ClipboardParams {
|
|
|
178
260
|
/// `seq` is the client's own ordering. The engine refuses a repeat or a lower
|
|
179
261
|
/// value rather than re-applying an action, so a replayed frame cannot move the
|
|
180
262
|
/// pointer twice.
|
|
181
|
-
#[derive(Debug, Deserialize)]
|
|
263
|
+
#[derive(Debug, Deserialize, Serialize)]
|
|
182
264
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
183
265
|
pub enum ControlMessage {
|
|
184
266
|
Pointer {
|
|
@@ -250,7 +332,7 @@ impl ControlMessage {
|
|
|
250
332
|
}
|
|
251
333
|
}
|
|
252
334
|
|
|
253
|
-
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
335
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
254
336
|
#[serde(rename_all = "snake_case")]
|
|
255
337
|
pub enum PointerPhase {
|
|
256
338
|
Move,
|
|
@@ -316,6 +398,99 @@ mod tests {
|
|
|
316
398
|
assert!(serde_json::from_str::<ControlMessage>(r#"{"kind":"teleport"}"#).is_err());
|
|
317
399
|
}
|
|
318
400
|
|
|
401
|
+
#[test]
|
|
402
|
+
fn display_source_round_trips_its_explicit_id() {
|
|
403
|
+
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
404
|
+
"kind": "display",
|
|
405
|
+
"display_id": 42
|
|
406
|
+
}))
|
|
407
|
+
.unwrap();
|
|
408
|
+
assert!(matches!(
|
|
409
|
+
request,
|
|
410
|
+
SourceRequest::Display {
|
|
411
|
+
display_id: Some(42)
|
|
412
|
+
}
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[test]
|
|
417
|
+
fn an_encoded_source_names_its_codec_and_its_own_size() {
|
|
418
|
+
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
419
|
+
"kind": "encoded",
|
|
420
|
+
"codec": "h264",
|
|
421
|
+
"width": 486,
|
|
422
|
+
"height": 1080
|
|
423
|
+
}))
|
|
424
|
+
.unwrap();
|
|
425
|
+
assert!(matches!(
|
|
426
|
+
request,
|
|
427
|
+
SourceRequest::Encoded {
|
|
428
|
+
codec: EncodedCodec::H264,
|
|
429
|
+
width: 486,
|
|
430
|
+
height: 1080
|
|
431
|
+
}
|
|
432
|
+
));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#[test]
|
|
436
|
+
fn an_unknown_encoded_codec_is_refused_rather_than_guessed() {
|
|
437
|
+
assert!(serde_json::from_value::<SourceRequest>(serde_json::json!({
|
|
438
|
+
"kind": "encoded",
|
|
439
|
+
"codec": "av1",
|
|
440
|
+
"width": 486,
|
|
441
|
+
"height": 1080
|
|
442
|
+
}))
|
|
443
|
+
.is_err());
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#[test]
|
|
447
|
+
fn a_feed_request_is_one_access_unit_with_its_own_keyframe_flag() {
|
|
448
|
+
let params: FeedParams =
|
|
449
|
+
serde_json::from_str(r#"{"session_id":"s","keyframe":true,"data_b64":"AAAA"}"#)
|
|
450
|
+
.unwrap();
|
|
451
|
+
assert_eq!(params.session_id, "s");
|
|
452
|
+
assert!(params.keyframe, "a feed says whether its unit is an IDR");
|
|
453
|
+
assert_eq!(params.data_b64, "AAAA");
|
|
454
|
+
assert_eq!(params.access_unit().unwrap(), vec![0x00, 0x00, 0x00]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#[test]
|
|
458
|
+
fn a_feed_request_that_is_not_base64_is_refused() {
|
|
459
|
+
let params = FeedParams {
|
|
460
|
+
session_id: String::from("s"),
|
|
461
|
+
keyframe: false,
|
|
462
|
+
data_b64: String::from("not base64!!"),
|
|
463
|
+
};
|
|
464
|
+
assert!(params.access_unit().is_err());
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// The reason this protocol carries bytes in base64 rather than adding a
|
|
468
|
+
/// second inherited fd: at 2 Mbit/s it costs a fraction of a per cent of a
|
|
469
|
+
/// core, against a whole second channel to keep ordered and bounded. The
|
|
470
|
+
/// bound below is deliberately loose — it is a sanity check on the order of
|
|
471
|
+
/// magnitude, not a benchmark, so it does not flake on a loaded machine.
|
|
472
|
+
#[test]
|
|
473
|
+
fn decoding_a_two_megabit_stream_is_nowhere_near_a_core() {
|
|
474
|
+
// One second of a 2 Mbit/s stream, in access units.
|
|
475
|
+
let unit: Vec<u8> = (0..8_000u32).map(|index| index as u8).collect();
|
|
476
|
+
let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &unit);
|
|
477
|
+
let params = FeedParams {
|
|
478
|
+
session_id: String::from("s"),
|
|
479
|
+
keyframe: false,
|
|
480
|
+
data_b64: encoded,
|
|
481
|
+
};
|
|
482
|
+
let started = std::time::Instant::now();
|
|
483
|
+
for _ in 0..(250_000 / unit.len()) {
|
|
484
|
+
assert_eq!(params.access_unit().unwrap().len(), unit.len());
|
|
485
|
+
}
|
|
486
|
+
let elapsed = started.elapsed();
|
|
487
|
+
assert!(
|
|
488
|
+
elapsed < std::time::Duration::from_millis(500),
|
|
489
|
+
"a second of 2 Mbit/s base64 took {elapsed:?}, which is far past 5 % of a core",
|
|
490
|
+
);
|
|
491
|
+
println!("a second of 2 Mbit/s stream decoded in {elapsed:?}");
|
|
492
|
+
}
|
|
493
|
+
|
|
319
494
|
#[test]
|
|
320
495
|
fn open_defaults_keep_the_desktops_own_pixels_up_to_4k() {
|
|
321
496
|
let params: OpenParams =
|
|
@@ -327,5 +502,13 @@ mod tests {
|
|
|
327
502
|
"the engine sizes the rate to the surface"
|
|
328
503
|
);
|
|
329
504
|
assert_eq!(params.max_fps, 30);
|
|
505
|
+
assert!(
|
|
506
|
+
!params.agent_indicator,
|
|
507
|
+
"human control has no overlay by default"
|
|
508
|
+
);
|
|
509
|
+
let agent: OpenParams =
|
|
510
|
+
serde_json::from_str(r#"{"permissions":["view","control"],"agent_indicator":true}"#)
|
|
511
|
+
.unwrap();
|
|
512
|
+
assert!(agent.agent_indicator);
|
|
330
513
|
}
|
|
331
514
|
}
|