@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/engine/src/session.rs
CHANGED
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
use crate::capture::{self, Capture};
|
|
9
9
|
use crate::clipboard;
|
|
10
10
|
use crate::convert::{fit, I420};
|
|
11
|
-
use crate::encoder::Encoder;
|
|
11
|
+
use crate::encoder::{Encoder, VideoEncoder};
|
|
12
12
|
use crate::input::{Button, HeldState, InputDevices};
|
|
13
13
|
use crate::keymap::{self, Layout};
|
|
14
|
-
use crate::peer::{
|
|
14
|
+
use crate::peer::{
|
|
15
|
+
h264_profile_level_id, PathReport, PeerEvent, TransportOptions, VideoCodec, VideoPeer,
|
|
16
|
+
};
|
|
15
17
|
use crate::portal::{self, SelectedSource};
|
|
16
|
-
use crate::protocol::{
|
|
18
|
+
use crate::protocol::{
|
|
19
|
+
ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
|
|
20
|
+
};
|
|
17
21
|
use crate::x11::X11Desktop;
|
|
18
|
-
use anyhow::Result;
|
|
22
|
+
use anyhow::{Context, Result};
|
|
19
23
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
20
24
|
use std::sync::{Arc, Condvar, Mutex};
|
|
21
25
|
use std::time::{Duration, Instant};
|
|
@@ -49,16 +53,48 @@ pub struct Metrics {
|
|
|
49
53
|
pub dropped_frames: u64,
|
|
50
54
|
pub encoded_frames: u64,
|
|
51
55
|
pub encoded_bytes: u64,
|
|
52
|
-
/// Key frames sent: the first,
|
|
56
|
+
/// Key frames sent: the first, one per receiver request, and one each time
|
|
57
|
+
/// motion starts or stops being coded at half size.
|
|
53
58
|
pub key_frames: u64,
|
|
54
59
|
/// Refinement passes: a still desktop re-coded once, sharp.
|
|
55
60
|
pub refined_frames: u64,
|
|
61
|
+
/// Motion frames coded at half size because full size could not keep up.
|
|
62
|
+
pub halved_frames: u64,
|
|
63
|
+
/// Fresh captures coded at the pace of change: a motion pass on a frame
|
|
64
|
+
/// the encoder had not coded yet — the population `queue_micros` is over.
|
|
65
|
+
pub motion_frames: u64,
|
|
56
66
|
/// Time spent in the encoder, in microseconds, over every encoded frame.
|
|
57
67
|
pub encode_micros: u64,
|
|
68
|
+
/// Time spent grabbing and converting a frame on the capture thread, in
|
|
69
|
+
/// microseconds, over every frame forwarded to the encoder. On X11 this
|
|
70
|
+
/// is `GetImage` plus the downscale to I420; unchanged polls are hashed,
|
|
71
|
+
/// recognised, and never forwarded, so their grab cost stays out; the lab
|
|
72
|
+
/// harness reads it as the grab cost L1 owns.
|
|
73
|
+
pub capture_micros: u64,
|
|
74
|
+
/// Time spent hashing a captured frame and working out its damage on the
|
|
75
|
+
/// capture thread, in microseconds, over every forwarded frame; the hash
|
|
76
|
+
/// branch runs only when `local_frames` is on.
|
|
77
|
+
pub convert_micros: u64,
|
|
78
|
+
/// Time a freshly captured frame waited between the capture handoff and
|
|
79
|
+
/// the start of its encode, in microseconds, over motion frames.
|
|
80
|
+
pub queue_micros: u64,
|
|
81
|
+
/// Time spent packetizing a frame and handing it to the transport, in
|
|
82
|
+
/// microseconds, over every sent frame.
|
|
83
|
+
pub send_micros: u64,
|
|
58
84
|
/// The current rate target, after any loss back-off.
|
|
59
85
|
pub target_kbps: u32,
|
|
86
|
+
/// The codec frames go out in, `vp9` or `h264`, and what codes them
|
|
87
|
+
/// (`libvpx`, `nvenc`, `vaapi`, `openh264`, `videotoolbox`); empty for an
|
|
88
|
+
/// encoded source, whose consumer is the encoder.
|
|
89
|
+
pub codec: &'static str,
|
|
90
|
+
pub encoder: &'static str,
|
|
60
91
|
pub input_applied: u64,
|
|
61
92
|
pub input_rejected: u64,
|
|
93
|
+
/// Access units the consumer fed to an encoded source.
|
|
94
|
+
pub fed_frames: u64,
|
|
95
|
+
/// Control messages forwarded back to the consumer that owns the device,
|
|
96
|
+
/// instead of being applied to a desktop this engine holds.
|
|
97
|
+
pub input_forwarded: u64,
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
fn tile_hashes(raw: &[u8], width: usize, height: usize) -> Vec<u64> {
|
|
@@ -178,6 +214,7 @@ fn select_x11(
|
|
|
178
214
|
let desktop = Arc::new(Mutex::new(X11Desktop::connect(display)?));
|
|
179
215
|
let (width, height) = {
|
|
180
216
|
let desktop = lock(&desktop);
|
|
217
|
+
eprintln!("x11 capture path: {}", desktop.capture_path());
|
|
181
218
|
desktop.screen_size()
|
|
182
219
|
};
|
|
183
220
|
let source = SelectedSource {
|
|
@@ -200,26 +237,75 @@ fn select_x11(
|
|
|
200
237
|
.spawn(move || {
|
|
201
238
|
let interval = frame_interval(max_fps);
|
|
202
239
|
let mut sequence = 0u64;
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
240
|
+
let mut last_sent: Option<Instant> = None;
|
|
241
|
+
// Without XDamage there is no change signal, so an unchanged
|
|
242
|
+
// screen is recognised by its pixels exactly as before.
|
|
243
|
+
let hashed = !lock(&desktop).damage_armed();
|
|
206
244
|
let mut last_hash = None;
|
|
245
|
+
// The first frame is always grabbed; afterwards Damage wakes
|
|
246
|
+
// the loop, so a still screen costs no grab, no conversion
|
|
247
|
+
// and no hash. The fps cap stays a minimum spacing.
|
|
248
|
+
let mut first = true;
|
|
207
249
|
while !stop.load(Ordering::SeqCst) {
|
|
250
|
+
if !first {
|
|
251
|
+
let changed = lock(&desktop).take_damage();
|
|
252
|
+
if !changed {
|
|
253
|
+
std::thread::sleep(interval);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if let Some(sent) = last_sent {
|
|
257
|
+
let elapsed = sent.elapsed();
|
|
258
|
+
if elapsed < interval {
|
|
259
|
+
std::thread::sleep(interval - elapsed);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
208
263
|
let started = Instant::now();
|
|
264
|
+
let grabbed = Instant::now();
|
|
209
265
|
let frame = {
|
|
210
266
|
let mut desktop = lock(&desktop);
|
|
211
267
|
desktop.capture_with_pixels(max_width, max_height)
|
|
212
268
|
};
|
|
269
|
+
let grab_micros = grabbed.elapsed().as_micros() as u64;
|
|
213
270
|
match frame {
|
|
214
271
|
Ok((frame, raw)) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
last_hash
|
|
221
|
-
|
|
272
|
+
let changed = if hashed {
|
|
273
|
+
use std::hash::{Hash, Hasher};
|
|
274
|
+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
275
|
+
raw.hash(&mut hasher);
|
|
276
|
+
let hash = hasher.finish();
|
|
277
|
+
if last_hash == Some(hash) {
|
|
278
|
+
false
|
|
279
|
+
} else {
|
|
280
|
+
last_hash = Some(hash);
|
|
281
|
+
true
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
true
|
|
285
|
+
};
|
|
286
|
+
if changed {
|
|
287
|
+
// Handing an unchanged frame on would keep the
|
|
288
|
+
// encoder busy and a still desktop would never
|
|
289
|
+
// be refined.
|
|
290
|
+
if let Ok(mut m) = captured.lock() {
|
|
291
|
+
m.capture_micros += grab_micros;
|
|
292
|
+
}
|
|
293
|
+
let (w, h) = (frame.width, frame.height);
|
|
294
|
+
let (screen_w, screen_h) = (width as usize, height as usize);
|
|
295
|
+
sink(frame, sequence, &mut || {
|
|
296
|
+
crate::convert::to_bgrx(
|
|
297
|
+
&raw,
|
|
298
|
+
screen_w,
|
|
299
|
+
screen_h,
|
|
300
|
+
screen_w * 4,
|
|
301
|
+
crate::convert::PixelFormat::Bgrx,
|
|
302
|
+
w,
|
|
303
|
+
h,
|
|
304
|
+
)
|
|
305
|
+
});
|
|
222
306
|
sequence += 1;
|
|
307
|
+
last_sent = Some(started);
|
|
308
|
+
first = false;
|
|
223
309
|
}
|
|
224
310
|
}
|
|
225
311
|
Err(error) => {
|
|
@@ -232,11 +318,21 @@ fn select_x11(
|
|
|
232
318
|
on_stop(format!("X11 capture stopped: {error}"));
|
|
233
319
|
break;
|
|
234
320
|
}
|
|
321
|
+
if !hashed {
|
|
322
|
+
let elapsed = started.elapsed();
|
|
323
|
+
if elapsed < interval {
|
|
324
|
+
std::thread::sleep(interval - elapsed);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
235
327
|
}
|
|
236
328
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
329
|
+
// With Damage the fps cap paced the grab above; without
|
|
330
|
+
// it the loop keeps its old trailing sleep.
|
|
331
|
+
if hashed {
|
|
332
|
+
let elapsed = started.elapsed();
|
|
333
|
+
if elapsed < interval {
|
|
334
|
+
std::thread::sleep(interval - elapsed);
|
|
335
|
+
}
|
|
240
336
|
}
|
|
241
337
|
}
|
|
242
338
|
})
|
|
@@ -460,7 +556,7 @@ fn x11_button(button: Button) -> u8 {
|
|
|
460
556
|
}
|
|
461
557
|
}
|
|
462
558
|
|
|
463
|
-
fn lock<T>(mutex: &
|
|
559
|
+
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
464
560
|
// A poisoned lock means a previous input call panicked while applying; the
|
|
465
561
|
// desktop is then in unknown state, so the session is closed rather than
|
|
466
562
|
// continuing to drive it.
|
|
@@ -470,6 +566,7 @@ fn lock<T>(mutex: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
|
|
|
470
566
|
}
|
|
471
567
|
|
|
472
568
|
/// Why a session could not do what was asked, in the protocol's own vocabulary.
|
|
569
|
+
#[derive(Debug)]
|
|
473
570
|
pub struct SessionError {
|
|
474
571
|
pub code: &'static str,
|
|
475
572
|
pub message: String,
|
|
@@ -498,6 +595,9 @@ pub struct OpenRequest {
|
|
|
498
595
|
/// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
|
|
499
596
|
pub loopback_tcp: bool,
|
|
500
597
|
pub agent_indicator: bool,
|
|
598
|
+
/// Keep the latest frame and its tile hashes for `session.frame` and
|
|
599
|
+
/// `session.frame.changed`; off, a frame is converted once, for the encoder.
|
|
600
|
+
pub local_frames: bool,
|
|
501
601
|
}
|
|
502
602
|
|
|
503
603
|
/// Events a session raises for its consumer.
|
|
@@ -522,10 +622,29 @@ pub enum SessionEvent {
|
|
|
522
622
|
seq: u64,
|
|
523
623
|
damage: Vec<[usize; 4]>,
|
|
524
624
|
},
|
|
625
|
+
/// One control-channel message from the client, for a source whose device
|
|
626
|
+
/// the consumer drives. The engine applied nothing locally.
|
|
627
|
+
Input {
|
|
628
|
+
input: serde_json::Value,
|
|
629
|
+
},
|
|
630
|
+
/// The far end needs a fresh reference frame and only the consumer can make
|
|
631
|
+
/// one: the encoder for this track is on its side. Raised by an RTCP PLI or
|
|
632
|
+
/// FIR, and once when the transport connects, because everything sent before
|
|
633
|
+
/// that was dropped.
|
|
634
|
+
KeyframeRequest {
|
|
635
|
+
generation: u64,
|
|
636
|
+
},
|
|
525
637
|
RestoreToken(String),
|
|
526
|
-
CaptureStopped {
|
|
638
|
+
CaptureStopped {
|
|
639
|
+
reason: String,
|
|
640
|
+
},
|
|
641
|
+
/// The session ended on the engine's side. `code` says whether a new
|
|
642
|
+
/// session may recover it: `transport` is the path to the consumer's peer
|
|
643
|
+
/// lost, which a fresh `session.open` can heal; `lease` and `error` are
|
|
644
|
+
/// the session's authority ending or a fault, which it cannot.
|
|
527
645
|
Revoked {
|
|
528
646
|
reason: String,
|
|
647
|
+
code: &'static str,
|
|
529
648
|
},
|
|
530
649
|
}
|
|
531
650
|
|
|
@@ -552,12 +671,18 @@ struct Inner {
|
|
|
552
671
|
latest: Arc<Mutex<Option<FrameSnapshot>>>,
|
|
553
672
|
capture_error: Arc<Mutex<Option<String>>>,
|
|
554
673
|
frame_changes: tokio::sync::watch::Receiver<u64>,
|
|
555
|
-
|
|
556
|
-
|
|
674
|
+
/// Built when the codec is known: with `session.open` for a captured
|
|
675
|
+
/// desktop, and with the first fed access unit for an encoded source, whose
|
|
676
|
+
/// offer has to name the profile the stream's own SPS declares.
|
|
677
|
+
peer: Mutex<Option<Arc<VideoPeer>>>,
|
|
678
|
+
/// Absent for an encoded source: the consumer is the encoder.
|
|
679
|
+
encoder: Mutex<Option<VideoEncoder>>,
|
|
557
680
|
input: Mutex<Option<InputTarget>>,
|
|
558
681
|
indicator: Mutex<Option<crate::indicator::Indicator>>,
|
|
559
682
|
capture: Mutex<Option<FrameSource>>,
|
|
560
|
-
|
|
683
|
+
/// Absent for an encoded source: there is no local keyboard to translate a
|
|
684
|
+
/// character through, because the client's keys are forwarded instead.
|
|
685
|
+
layout: Mutex<Option<Layout>>,
|
|
561
686
|
last_seq: Mutex<u64>,
|
|
562
687
|
control_open: AtomicBool,
|
|
563
688
|
closed: AtomicBool,
|
|
@@ -565,8 +690,37 @@ struct Inner {
|
|
|
565
690
|
/// from the lease as well as from the consumer.
|
|
566
691
|
pipeline: Arc<AtomicBool>,
|
|
567
692
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
693
|
+
/// Present only for an encoded source.
|
|
694
|
+
encoded: Mutex<Option<Encoded>>,
|
|
695
|
+
/// `OpenRequest::local_frames`: whether `latest` is kept at all.
|
|
696
|
+
local_frames: bool,
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/// A source whose video arrives already encoded.
|
|
700
|
+
struct Encoded {
|
|
701
|
+
/// What the consumer said it would feed. Only H.264 exists today; the offer
|
|
702
|
+
/// is built from this rather than assumed, so a second codec is one arm here.
|
|
703
|
+
codec: EncodedCodec,
|
|
704
|
+
/// The transport settings the consumer chose in `session.open`, kept until
|
|
705
|
+
/// the peer that uses them can be built.
|
|
706
|
+
transport: TransportOptions,
|
|
707
|
+
/// Where the peer's events go once it exists.
|
|
708
|
+
peer_events: tokio_mpsc::UnboundedSender<PeerEvent>,
|
|
709
|
+
/// Access units waiting to be packetized.
|
|
710
|
+
feed: tokio_mpsc::Sender<FedAccessUnit>,
|
|
568
711
|
}
|
|
569
712
|
|
|
713
|
+
/// One access unit a consumer fed, with the keyframe flag it declared.
|
|
714
|
+
struct FedAccessUnit {
|
|
715
|
+
keyframe: bool,
|
|
716
|
+
data: Vec<u8>,
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/// How many fed access units may wait. A live stream drops the newest rather
|
|
720
|
+
/// than queue: the packetizer is the only consumer, and a backlog of
|
|
721
|
+
/// milliseconds-old pictures is worse than a gap the receiver asks to repair.
|
|
722
|
+
const FEED_BACKLOG: usize = 8;
|
|
723
|
+
|
|
570
724
|
pub struct Session {
|
|
571
725
|
inner: Arc<Inner>,
|
|
572
726
|
}
|
|
@@ -620,7 +774,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
620
774
|
"cursor": "embedded",
|
|
621
775
|
"audio": false,
|
|
622
776
|
},
|
|
623
|
-
"encode":
|
|
777
|
+
"encode": encode_capabilities(),
|
|
624
778
|
"input": {
|
|
625
779
|
"mechanism": "inputtino/uinput",
|
|
626
780
|
"pointer": input,
|
|
@@ -633,6 +787,10 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
633
787
|
},
|
|
634
788
|
"clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
|
|
635
789
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
790
|
+
// Video this engine does not capture or encode but can carry, so a
|
|
791
|
+
// consumer can tell an old engine from one that takes `session.feed`
|
|
792
|
+
// without guessing from a version string.
|
|
793
|
+
"encoded": { "codecs": ["h264"] },
|
|
636
794
|
})
|
|
637
795
|
}
|
|
638
796
|
|
|
@@ -674,7 +832,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
674
832
|
"remedy": format!("Allow {} in System Settings › Privacy & Security › Screen & System Audio Recording, then reconnect.", crate::mac::tcc_responsible_app_name())
|
|
675
833
|
}) }
|
|
676
834
|
},
|
|
677
|
-
"encode":
|
|
835
|
+
"encode": encode_capabilities(),
|
|
678
836
|
"input": {
|
|
679
837
|
"mechanism": "quartz-cgevent",
|
|
680
838
|
"pointer": input,
|
|
@@ -687,6 +845,25 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
687
845
|
},
|
|
688
846
|
"clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
|
|
689
847
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
848
|
+
"encoded": { "codecs": ["h264"] },
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/// `capabilities.encode`: VP9 always, H.264 when an encoder for it starts
|
|
853
|
+
/// here now. `hardware` describes VP9, the default.
|
|
854
|
+
fn encode_capabilities() -> serde_json::Value {
|
|
855
|
+
let h264 = crate::h264::probe();
|
|
856
|
+
let mut codecs = vec!["vp9"];
|
|
857
|
+
if h264.is_some() {
|
|
858
|
+
codecs.push("h264");
|
|
859
|
+
}
|
|
860
|
+
serde_json::json!({
|
|
861
|
+
"codecs": codecs,
|
|
862
|
+
"hardware": false,
|
|
863
|
+
"h264": {
|
|
864
|
+
"encoder": h264.map(|info| info.encoder),
|
|
865
|
+
"hardware": h264.is_some_and(|info| info.hardware),
|
|
866
|
+
},
|
|
690
867
|
})
|
|
691
868
|
}
|
|
692
869
|
|
|
@@ -705,6 +882,17 @@ impl Session {
|
|
|
705
882
|
let wants_control = request.permissions.contains(&Permission::Control);
|
|
706
883
|
let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
|
|
707
884
|
let max_fps = request.max_fps.clamp(1, 60);
|
|
885
|
+
// A source the consumer feeds needs no capture backend, no encoder and
|
|
886
|
+
// no local input device: the device the client drives is the consumer's,
|
|
887
|
+
// and the offer has to wait for the stream's own SPS.
|
|
888
|
+
if let Some(SourceRequest::Encoded {
|
|
889
|
+
codec,
|
|
890
|
+
width,
|
|
891
|
+
height,
|
|
892
|
+
}) = request.source.clone()
|
|
893
|
+
{
|
|
894
|
+
return Self::open_encoded(request, codec, width, height, events).await;
|
|
895
|
+
}
|
|
708
896
|
// Which desktop decides which input path is even available: an X display
|
|
709
897
|
// takes XTest, which cannot reach any other session, while a portal
|
|
710
898
|
// desktop needs kernel input access.
|
|
@@ -738,19 +926,41 @@ impl Session {
|
|
|
738
926
|
let captured = metrics.clone();
|
|
739
927
|
let stop_signal = frame_tx_signal.clone();
|
|
740
928
|
let on_capture_status = move |running: bool, reason: String| {
|
|
741
|
-
if let Ok(mut error) = stopped_error.lock() {
|
|
742
|
-
|
|
929
|
+
if let Ok(mut error) = stopped_error.lock() {
|
|
930
|
+
*error = if running { None } else { Some(reason.clone()) };
|
|
931
|
+
}
|
|
932
|
+
if let Ok(mut frame) = stopped_latest.lock() {
|
|
933
|
+
*frame = None;
|
|
934
|
+
}
|
|
743
935
|
stop_signal.send_replace(0);
|
|
744
|
-
let _ = stopped_events.send(Notice {
|
|
745
|
-
|
|
746
|
-
|
|
936
|
+
let _ = stopped_events.send(Notice {
|
|
937
|
+
session_id: stopped_id.clone(),
|
|
938
|
+
event: if running {
|
|
939
|
+
SessionEvent::State {
|
|
940
|
+
capture: "streaming",
|
|
941
|
+
transport: String::from("connected"),
|
|
942
|
+
first_frame: false,
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
SessionEvent::CaptureStopped { reason }
|
|
946
|
+
},
|
|
947
|
+
});
|
|
747
948
|
};
|
|
748
949
|
let capture_status = Arc::new(on_capture_status);
|
|
749
950
|
let frame_events = events.clone();
|
|
750
951
|
let frame_session_id = id.clone();
|
|
751
|
-
let
|
|
752
|
-
|
|
753
|
-
|
|
952
|
+
let local_frames = request.local_frames;
|
|
953
|
+
let sink = Box::new(move |frame: I420, _seq: u64, pixels: capture::Pixels| {
|
|
954
|
+
// The frame's RTP time: when capture handed it over, not when it
|
|
955
|
+
// was coded, so the receiver never sees encode jitter as cadence.
|
|
956
|
+
// ponytail: stamped after the backend's grab and conversion; a
|
|
957
|
+
// grab-start instant from each backend needs a FrameSink change.
|
|
958
|
+
let captured_at = Instant::now();
|
|
959
|
+
let raw = if local_frames { pixels() } else { None };
|
|
960
|
+
let hashes = raw
|
|
961
|
+
.as_ref()
|
|
962
|
+
.map(|raw| tile_hashes(raw, frame.width, frame.height));
|
|
963
|
+
if let (Some(raw), Some(hashes), Ok(mut held)) = (raw, hashes, observed.lock()) {
|
|
754
964
|
let unchanged =
|
|
755
965
|
held.as_ref()
|
|
756
966
|
.is_some_and(|(_, _, width, height, _, current, _)| {
|
|
@@ -781,10 +991,11 @@ impl Session {
|
|
|
781
991
|
}
|
|
782
992
|
if let Ok(mut m) = captured.lock() {
|
|
783
993
|
m.captured_frames += 1;
|
|
994
|
+
m.convert_micros += captured_at.elapsed().as_micros() as u64;
|
|
784
995
|
}
|
|
785
996
|
// An encoder that is behind gets the newest frame, not a queue: a
|
|
786
997
|
// desktop stream is live, and the last state is the one that counts.
|
|
787
|
-
if frame_tx.put(frame) {
|
|
998
|
+
if frame_tx.put(frame, captured_at) {
|
|
788
999
|
if let Ok(mut m) = captured.lock() {
|
|
789
1000
|
m.dropped_frames += 1;
|
|
790
1001
|
}
|
|
@@ -803,7 +1014,10 @@ impl Session {
|
|
|
803
1014
|
max_fps,
|
|
804
1015
|
metrics.clone(),
|
|
805
1016
|
sink,
|
|
806
|
-
Box::new({
|
|
1017
|
+
Box::new({
|
|
1018
|
+
let status = capture_status.clone();
|
|
1019
|
+
move |reason| status(false, reason)
|
|
1020
|
+
}),
|
|
807
1021
|
)
|
|
808
1022
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?,
|
|
809
1023
|
_ => {
|
|
@@ -852,7 +1066,10 @@ impl Session {
|
|
|
852
1066
|
max_fps,
|
|
853
1067
|
sink,
|
|
854
1068
|
indicator.as_ref().map(crate::indicator::Indicator::pid),
|
|
855
|
-
Box::new({
|
|
1069
|
+
Box::new({
|
|
1070
|
+
let status = capture_status.clone();
|
|
1071
|
+
move |running, reason| status(running, reason)
|
|
1072
|
+
}),
|
|
856
1073
|
)
|
|
857
1074
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
858
1075
|
#[cfg(target_os = "linux")]
|
|
@@ -865,7 +1082,10 @@ impl Session {
|
|
|
865
1082
|
indicator
|
|
866
1083
|
.as_ref()
|
|
867
1084
|
.map(crate::indicator::Indicator::position),
|
|
868
|
-
Box::new({
|
|
1085
|
+
Box::new({
|
|
1086
|
+
let status = capture_status.clone();
|
|
1087
|
+
move |reason| status(false, reason)
|
|
1088
|
+
}),
|
|
869
1089
|
)
|
|
870
1090
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
871
1091
|
#[cfg(target_os = "linux")]
|
|
@@ -953,6 +1173,16 @@ impl Session {
|
|
|
953
1173
|
SessionError::new("input", format!("no keyboard layout: {error:#}"))
|
|
954
1174
|
})?;
|
|
955
1175
|
|
|
1176
|
+
// H.264 is offered beside VP9 only when an encoder for it starts on
|
|
1177
|
+
// this machine; the answer then decides which one is coded.
|
|
1178
|
+
let codec = if !crate::h264::fits(width, height) {
|
|
1179
|
+
VideoCodec::Vp9
|
|
1180
|
+
} else {
|
|
1181
|
+
match tokio::task::spawn_blocking(crate::h264::prepare).await {
|
|
1182
|
+
Ok(Some(_)) => VideoCodec::Vp9OrH264,
|
|
1183
|
+
_ => VideoCodec::Vp9,
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
956
1186
|
let (peer_events_tx, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
957
1187
|
let (peer, offer) = VideoPeer::offer(
|
|
958
1188
|
TransportOptions {
|
|
@@ -963,9 +1193,11 @@ impl Session {
|
|
|
963
1193
|
pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
|
|
964
1194
|
},
|
|
965
1195
|
peer_events_tx,
|
|
1196
|
+
codec,
|
|
966
1197
|
)
|
|
967
1198
|
.await
|
|
968
1199
|
.map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
|
|
1200
|
+
let peer = Arc::new(peer);
|
|
969
1201
|
|
|
970
1202
|
let generation = 1u64;
|
|
971
1203
|
let geometry = serde_json::json!({
|
|
@@ -985,17 +1217,19 @@ impl Session {
|
|
|
985
1217
|
latest,
|
|
986
1218
|
capture_error,
|
|
987
1219
|
frame_changes,
|
|
988
|
-
peer:
|
|
989
|
-
encoder: Mutex::new(encoder),
|
|
1220
|
+
peer: Mutex::new(Some(peer.clone())),
|
|
1221
|
+
encoder: Mutex::new(Some(VideoEncoder::Vp9(encoder))),
|
|
990
1222
|
input: Mutex::new(input),
|
|
991
1223
|
indicator: Mutex::new(indicator),
|
|
992
1224
|
capture: Mutex::new(Some(capture)),
|
|
993
|
-
layout: Mutex::new(layout),
|
|
1225
|
+
layout: Mutex::new(Some(layout)),
|
|
994
1226
|
last_seq: Mutex::new(0),
|
|
995
1227
|
control_open: AtomicBool::new(false),
|
|
996
1228
|
closed: AtomicBool::new(false),
|
|
997
1229
|
pipeline: pipeline.clone(),
|
|
998
1230
|
events: events.clone(),
|
|
1231
|
+
encoded: Mutex::new(None),
|
|
1232
|
+
local_frames: request.local_frames,
|
|
999
1233
|
});
|
|
1000
1234
|
|
|
1001
1235
|
inner.notify(SessionEvent::Description {
|
|
@@ -1008,13 +1242,141 @@ impl Session {
|
|
|
1008
1242
|
first_frame: false,
|
|
1009
1243
|
});
|
|
1010
1244
|
|
|
1011
|
-
spawn_pipeline(&inner, frame_rx, pipeline, max_fps, bitrate_kbps);
|
|
1245
|
+
spawn_pipeline(&inner, peer, frame_rx, pipeline, max_fps, bitrate_kbps);
|
|
1012
1246
|
spawn_peer_events(&inner, peer_events_rx);
|
|
1013
1247
|
spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
|
|
1014
1248
|
|
|
1015
1249
|
Ok(Self { inner })
|
|
1016
1250
|
}
|
|
1017
1251
|
|
|
1252
|
+
/// Open a session over video the consumer already encoded.
|
|
1253
|
+
///
|
|
1254
|
+
/// Nothing is captured, encoded or driven locally: the engine packetizes
|
|
1255
|
+
/// what it is fed and sends the client's control messages back to the
|
|
1256
|
+
/// consumer, which owns the device. The peer is deliberately absent here —
|
|
1257
|
+
/// the offer must name the profile-level-id of the stream's own SPS, and
|
|
1258
|
+
/// that only exists once the first access unit arrives.
|
|
1259
|
+
async fn open_encoded(
|
|
1260
|
+
request: OpenRequest,
|
|
1261
|
+
codec: EncodedCodec,
|
|
1262
|
+
width: usize,
|
|
1263
|
+
height: usize,
|
|
1264
|
+
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
1265
|
+
) -> std::result::Result<Self, SessionError> {
|
|
1266
|
+
if width == 0 || height == 0 || width > 7680 || height > 4320 {
|
|
1267
|
+
return Err(SessionError::new(
|
|
1268
|
+
"source",
|
|
1269
|
+
format!(
|
|
1270
|
+
"an encoded surface must be 1..7680 by 1..4320 pixels, not {width}x{height}"
|
|
1271
|
+
),
|
|
1272
|
+
));
|
|
1273
|
+
}
|
|
1274
|
+
let id = opaque_id();
|
|
1275
|
+
let generation = 1u64;
|
|
1276
|
+
let metrics = Arc::new(Mutex::new(Metrics::default()));
|
|
1277
|
+
// Nothing raises it, and nothing waits for it: an encoded source has no
|
|
1278
|
+
// frame to read, only an encoded stream to forward.
|
|
1279
|
+
let (_frame_signal, frame_changes) = tokio::sync::watch::channel(0u64);
|
|
1280
|
+
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
1281
|
+
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
1282
|
+
let geometry = serde_json::json!({
|
|
1283
|
+
"source": { "width": width, "height": height },
|
|
1284
|
+
// Nothing is scaled: the client aims at the stream's own pixels, and
|
|
1285
|
+
// the consumer maps them to its device.
|
|
1286
|
+
"encoded": { "width": width, "height": height },
|
|
1287
|
+
"origin": { "x": 0, "y": 0 },
|
|
1288
|
+
});
|
|
1289
|
+
let inner = Arc::new(Inner {
|
|
1290
|
+
id,
|
|
1291
|
+
generation,
|
|
1292
|
+
permissions: request.permissions,
|
|
1293
|
+
source: SelectedSource {
|
|
1294
|
+
node_id: 0,
|
|
1295
|
+
width: width as i32,
|
|
1296
|
+
height: height as i32,
|
|
1297
|
+
position: Some((0, 0)),
|
|
1298
|
+
source_type: Some(String::from("encoded")),
|
|
1299
|
+
origin_x: 0,
|
|
1300
|
+
origin_y: 0,
|
|
1301
|
+
},
|
|
1302
|
+
geometry,
|
|
1303
|
+
metrics,
|
|
1304
|
+
latest: Arc::new(Mutex::new(None)),
|
|
1305
|
+
capture_error: Arc::new(Mutex::new(None)),
|
|
1306
|
+
frame_changes,
|
|
1307
|
+
peer: Mutex::new(None),
|
|
1308
|
+
encoder: Mutex::new(None),
|
|
1309
|
+
input: Mutex::new(None),
|
|
1310
|
+
indicator: Mutex::new(None),
|
|
1311
|
+
capture: Mutex::new(None),
|
|
1312
|
+
layout: Mutex::new(None),
|
|
1313
|
+
last_seq: Mutex::new(0),
|
|
1314
|
+
control_open: AtomicBool::new(false),
|
|
1315
|
+
closed: AtomicBool::new(false),
|
|
1316
|
+
pipeline: Arc::new(AtomicBool::new(false)),
|
|
1317
|
+
events: events.clone(),
|
|
1318
|
+
encoded: Mutex::new(Some(Encoded {
|
|
1319
|
+
codec,
|
|
1320
|
+
transport: TransportOptions {
|
|
1321
|
+
ice_servers: request.ice_servers,
|
|
1322
|
+
loopback_tcp: request.loopback_tcp,
|
|
1323
|
+
// A device stream is a few megabits; pacing exists to spread
|
|
1324
|
+
// a large frame, not to shape a rate this engine does not set.
|
|
1325
|
+
pace_bps: 20_000_000.0,
|
|
1326
|
+
},
|
|
1327
|
+
peer_events,
|
|
1328
|
+
feed,
|
|
1329
|
+
})),
|
|
1330
|
+
local_frames: false,
|
|
1331
|
+
});
|
|
1332
|
+
inner.notify(SessionEvent::State {
|
|
1333
|
+
capture: "consented",
|
|
1334
|
+
transport: String::from("new"),
|
|
1335
|
+
first_frame: false,
|
|
1336
|
+
});
|
|
1337
|
+
spawn_peer_events(&inner, peer_events_rx);
|
|
1338
|
+
spawn_encoded(&inner, feed_rx);
|
|
1339
|
+
spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
|
|
1340
|
+
Ok(Self { inner })
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/// Take one access unit of an encoded source.
|
|
1344
|
+
///
|
|
1345
|
+
/// The reply acknowledges the hand-off, not the picture: the unit is
|
|
1346
|
+
/// packetized behind this request, and `session.metrics` reports what was
|
|
1347
|
+
/// sent and what was dropped.
|
|
1348
|
+
pub fn feed(&self, keyframe: bool, data: Vec<u8>) -> std::result::Result<(), SessionError> {
|
|
1349
|
+
if self.inner.closed.load(Ordering::Relaxed) {
|
|
1350
|
+
return Err(SessionError::new("session", "the session has ended"));
|
|
1351
|
+
}
|
|
1352
|
+
let encoded = lock(&self.inner.encoded);
|
|
1353
|
+
let Some(encoded) = encoded.as_ref() else {
|
|
1354
|
+
return Err(SessionError::new(
|
|
1355
|
+
"operation",
|
|
1356
|
+
"this session has no encoded source to feed",
|
|
1357
|
+
));
|
|
1358
|
+
};
|
|
1359
|
+
if data.is_empty() {
|
|
1360
|
+
return Err(SessionError::new(
|
|
1361
|
+
"operation",
|
|
1362
|
+
"an access unit cannot be empty",
|
|
1363
|
+
));
|
|
1364
|
+
}
|
|
1365
|
+
if let Ok(mut m) = self.inner.metrics.lock() {
|
|
1366
|
+
m.fed_frames += 1;
|
|
1367
|
+
}
|
|
1368
|
+
if encoded
|
|
1369
|
+
.feed
|
|
1370
|
+
.try_send(FedAccessUnit { keyframe, data })
|
|
1371
|
+
.is_err()
|
|
1372
|
+
{
|
|
1373
|
+
if let Ok(mut m) = self.inner.metrics.lock() {
|
|
1374
|
+
m.dropped_frames += 1;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
Ok(())
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1018
1380
|
pub fn id(&self) -> &str {
|
|
1019
1381
|
&self.inner.id
|
|
1020
1382
|
}
|
|
@@ -1037,6 +1399,13 @@ impl Session {
|
|
|
1037
1399
|
still_ms: Option<u64>,
|
|
1038
1400
|
timeout_ms: u64,
|
|
1039
1401
|
) -> std::result::Result<(), SessionError> {
|
|
1402
|
+
if lock(&self.inner.encoded).is_some() {
|
|
1403
|
+
return Err(SessionError::new(
|
|
1404
|
+
"operation",
|
|
1405
|
+
"this session forwards an encoded stream and keeps no frame",
|
|
1406
|
+
));
|
|
1407
|
+
}
|
|
1408
|
+
self.keeps_frames()?;
|
|
1040
1409
|
let mut changes = self.inner.frame_changes.clone();
|
|
1041
1410
|
let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
|
|
1042
1411
|
loop {
|
|
@@ -1073,6 +1442,16 @@ impl Session {
|
|
|
1073
1442
|
}
|
|
1074
1443
|
}
|
|
1075
1444
|
|
|
1445
|
+
fn keeps_frames(&self) -> std::result::Result<(), SessionError> {
|
|
1446
|
+
if self.inner.local_frames {
|
|
1447
|
+
return Ok(());
|
|
1448
|
+
}
|
|
1449
|
+
Err(SessionError::new(
|
|
1450
|
+
"operation",
|
|
1451
|
+
"this session was opened with local_frames off and keeps no frame",
|
|
1452
|
+
))
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1076
1455
|
/// Write only the requested lossless frame bytes; JSON carries metadata, never pixels.
|
|
1077
1456
|
pub fn frame(
|
|
1078
1457
|
&self,
|
|
@@ -1086,6 +1465,13 @@ impl Session {
|
|
|
1086
1465
|
"view permission is required",
|
|
1087
1466
|
));
|
|
1088
1467
|
}
|
|
1468
|
+
if lock(&self.inner.encoded).is_some() {
|
|
1469
|
+
return Err(SessionError::new(
|
|
1470
|
+
"operation",
|
|
1471
|
+
"this session forwards an encoded stream and keeps no frame",
|
|
1472
|
+
));
|
|
1473
|
+
}
|
|
1474
|
+
self.keeps_frames()?;
|
|
1089
1475
|
if let Some(reason) = lock(&self.inner.capture_error).clone() {
|
|
1090
1476
|
return Err(SessionError::new("stream-stopped", reason));
|
|
1091
1477
|
}
|
|
@@ -1140,7 +1526,32 @@ impl Session {
|
|
|
1140
1526
|
}
|
|
1141
1527
|
|
|
1142
1528
|
pub async fn accept_answer(&self, sdp: String) -> Result<()> {
|
|
1143
|
-
self.
|
|
1529
|
+
self.current_peer()?
|
|
1530
|
+
.accept_answer(sdp)
|
|
1531
|
+
.await
|
|
1532
|
+
.map(|_applied| ())
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/// The peer for this session, which an encoded source does not have until
|
|
1536
|
+
/// its first access unit has named the codec.
|
|
1537
|
+
fn current_peer(&self) -> Result<Arc<VideoPeer>> {
|
|
1538
|
+
self.inner
|
|
1539
|
+
.peer
|
|
1540
|
+
.lock()
|
|
1541
|
+
.ok()
|
|
1542
|
+
.and_then(|peer| peer.clone())
|
|
1543
|
+
.ok_or_else(|| anyhow::anyhow!("no offer has been made for this session yet"))
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/// Restart ICE: a fresh offer on the same peer, announced as a new
|
|
1547
|
+
/// `session.description` event for the client to answer as usual.
|
|
1548
|
+
pub async fn restart_ice(&self) -> Result<()> {
|
|
1549
|
+
let offer = self.current_peer()?.restart_ice().await?;
|
|
1550
|
+
self.inner.notify(SessionEvent::Description {
|
|
1551
|
+
generation: self.inner.generation,
|
|
1552
|
+
sdp: offer,
|
|
1553
|
+
});
|
|
1554
|
+
Ok(())
|
|
1144
1555
|
}
|
|
1145
1556
|
|
|
1146
1557
|
pub async fn add_candidate(
|
|
@@ -1149,8 +1560,7 @@ impl Session {
|
|
|
1149
1560
|
sdp_mid: Option<String>,
|
|
1150
1561
|
sdp_m_line_index: Option<u16>,
|
|
1151
1562
|
) -> Result<()> {
|
|
1152
|
-
self.
|
|
1153
|
-
.peer
|
|
1563
|
+
self.current_peer()?
|
|
1154
1564
|
.add_candidate(candidate, sdp_mid, sdp_m_line_index)
|
|
1155
1565
|
.await
|
|
1156
1566
|
}
|
|
@@ -1190,7 +1600,7 @@ impl Session {
|
|
|
1190
1600
|
}
|
|
1191
1601
|
|
|
1192
1602
|
pub async fn close(&self, reason: &str) {
|
|
1193
|
-
self.inner.close(reason).await;
|
|
1603
|
+
self.inner.close(reason, "closed").await;
|
|
1194
1604
|
}
|
|
1195
1605
|
}
|
|
1196
1606
|
|
|
@@ -1205,12 +1615,18 @@ impl Inner {
|
|
|
1205
1615
|
|
|
1206
1616
|
/// Stop everything and release whatever input this session held. Idempotent,
|
|
1207
1617
|
/// and the one teardown both an explicit close and a lease expiry take.
|
|
1208
|
-
async fn close(&self, reason: &str) {
|
|
1618
|
+
async fn close(&self, reason: &str, code: &str) {
|
|
1209
1619
|
if self.closed.swap(true, Ordering::SeqCst) {
|
|
1210
1620
|
return;
|
|
1211
1621
|
}
|
|
1212
1622
|
self.control_open.store(false, Ordering::SeqCst);
|
|
1213
1623
|
self.pipeline.store(false, Ordering::SeqCst);
|
|
1624
|
+
drop(lock(&self.encoded).take());
|
|
1625
|
+
// The encoder goes now, not with the last handle: a hardware session
|
|
1626
|
+
// still open when the process exits can hang the driver's exit
|
|
1627
|
+
// handlers, and `shutdown` exits right after this. The encode thread
|
|
1628
|
+
// finds it gone and stops.
|
|
1629
|
+
drop(lock(&self.encoder).take());
|
|
1214
1630
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1215
1631
|
indicator.take();
|
|
1216
1632
|
}
|
|
@@ -1222,14 +1638,16 @@ impl Inner {
|
|
|
1222
1638
|
}
|
|
1223
1639
|
*input = None;
|
|
1224
1640
|
}
|
|
1225
|
-
let
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1641
|
+
if let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) {
|
|
1642
|
+
let _ = peer
|
|
1643
|
+
.send_control(
|
|
1644
|
+
&serde_json::to_string(&ControlReply::Revoked { reason, code }).unwrap_or_else(
|
|
1645
|
+
|_| String::from(r#"{"kind":"revoked","reason":"closed","code":"closed"}"#),
|
|
1646
|
+
),
|
|
1647
|
+
)
|
|
1648
|
+
.await;
|
|
1649
|
+
peer.close().await;
|
|
1650
|
+
}
|
|
1233
1651
|
// Dropping the capture stops the PipeWire stream and joins its thread,
|
|
1234
1652
|
// which is what releases the compositor's consent for this session. It
|
|
1235
1653
|
// has to happen synchronously, not when the last handle happens to fall
|
|
@@ -1256,7 +1674,10 @@ impl Inner {
|
|
|
1256
1674
|
}
|
|
1257
1675
|
|
|
1258
1676
|
fn clipboard_refusal(&self) -> Option<(&'static str, &'static str)> {
|
|
1259
|
-
if
|
|
1677
|
+
if matches!(
|
|
1678
|
+
self.source.source_type.as_deref(),
|
|
1679
|
+
Some("x11-root") | Some("encoded")
|
|
1680
|
+
) || !wayland_clipboard_available()
|
|
1260
1681
|
{
|
|
1261
1682
|
return Some((
|
|
1262
1683
|
"clipboard-unsupported",
|
|
@@ -1279,9 +1700,14 @@ impl Inner {
|
|
|
1279
1700
|
|
|
1280
1701
|
fn reply(&self, payload: &ControlReply<'_>) -> Result<()> {
|
|
1281
1702
|
let text = serde_json::to_string(payload)?;
|
|
1703
|
+
let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) else {
|
|
1704
|
+
// An encoded source has no peer until its first access unit, so a
|
|
1705
|
+
// rejection raised before then has nowhere to go; the request that
|
|
1706
|
+
// caused it is refused in the same breath.
|
|
1707
|
+
return Ok(());
|
|
1708
|
+
};
|
|
1282
1709
|
// The channel is asynchronous; a failure here means the peer is gone,
|
|
1283
1710
|
// which the connection-state handler closes anyway.
|
|
1284
|
-
let peer = self.peer.clone();
|
|
1285
1711
|
tokio::spawn(async move {
|
|
1286
1712
|
let _ = peer.send_control(&text).await;
|
|
1287
1713
|
});
|
|
@@ -1318,7 +1744,7 @@ impl Inner {
|
|
|
1318
1744
|
*last = seq;
|
|
1319
1745
|
}
|
|
1320
1746
|
let feedback = match &message {
|
|
1321
|
-
ControlMessage::Pointer { phase, x, y, .. } => {
|
|
1747
|
+
ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
|
|
1322
1748
|
let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
|
|
1323
1749
|
Some((
|
|
1324
1750
|
if matches!(phase, PointerPhase::Down) {
|
|
@@ -1334,6 +1760,13 @@ impl Inner {
|
|
|
1334
1760
|
ControlMessage::Key { down: true, .. } => Some(('T', -1, -1)),
|
|
1335
1761
|
_ => None,
|
|
1336
1762
|
};
|
|
1763
|
+
// A source the consumer feeds is a device the consumer drives. Nothing is
|
|
1764
|
+
// applied to this machine: the message travels back as an event, and the
|
|
1765
|
+
// consumer injects it where the picture actually came from.
|
|
1766
|
+
if lock(&self.encoded).is_some() {
|
|
1767
|
+
self.forward(message);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1337
1770
|
let outcome = match message {
|
|
1338
1771
|
ControlMessage::Pointer {
|
|
1339
1772
|
phase,
|
|
@@ -1386,6 +1819,41 @@ impl Inner {
|
|
|
1386
1819
|
}
|
|
1387
1820
|
}
|
|
1388
1821
|
|
|
1822
|
+
/// Hand one client action back to the consumer, which owns the device this
|
|
1823
|
+
/// stream came from. The audit trail is the event itself and the count in
|
|
1824
|
+
/// `session.metrics`; nothing was applied here.
|
|
1825
|
+
fn forward(&self, message: ControlMessage) {
|
|
1826
|
+
let seq = message.seq();
|
|
1827
|
+
// The clipboard belongs to the device the consumer drives, and this
|
|
1828
|
+
// engine has none for this source; saying so is better than a request
|
|
1829
|
+
// that never answers.
|
|
1830
|
+
match &message {
|
|
1831
|
+
ControlMessage::ClipboardRead { request, .. }
|
|
1832
|
+
| ControlMessage::ClipboardWrite { request, .. } => {
|
|
1833
|
+
let _ = self.reply(&ControlReply::Clipboard {
|
|
1834
|
+
request,
|
|
1835
|
+
text: String::new(),
|
|
1836
|
+
truncated: false,
|
|
1837
|
+
error: Some("this source's clipboard belongs to the consumer"),
|
|
1838
|
+
});
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
_ => {}
|
|
1842
|
+
}
|
|
1843
|
+
let input = match serde_json::to_value(&message) {
|
|
1844
|
+
Ok(input) => input,
|
|
1845
|
+
Err(_) => {
|
|
1846
|
+
self.reject(seq, "operation", "unrecognised control message");
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
self.notify(SessionEvent::Input { input });
|
|
1851
|
+
if let Ok(mut m) = self.metrics.lock() {
|
|
1852
|
+
m.input_forwarded += 1;
|
|
1853
|
+
}
|
|
1854
|
+
let _ = self.reply(&ControlReply::Ack { seq });
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1389
1857
|
fn with_input<T>(
|
|
1390
1858
|
&self,
|
|
1391
1859
|
action: impl FnOnce(&mut InputTarget) -> Result<T>,
|
|
@@ -1462,10 +1930,13 @@ impl Inner {
|
|
|
1462
1930
|
}
|
|
1463
1931
|
|
|
1464
1932
|
let stroke = {
|
|
1465
|
-
let
|
|
1933
|
+
let held = self
|
|
1466
1934
|
.layout
|
|
1467
1935
|
.lock()
|
|
1468
1936
|
.map_err(|_| ("session", String::from("no keyboard layout")))?;
|
|
1937
|
+
let layout = held
|
|
1938
|
+
.as_ref()
|
|
1939
|
+
.ok_or(("session", String::from("no keyboard layout")))?;
|
|
1469
1940
|
if let Some(name) = name {
|
|
1470
1941
|
keymap::named_key(&name)
|
|
1471
1942
|
.ok_or(("text-unsupported", format!("unknown key {name}")))?
|
|
@@ -1512,12 +1983,15 @@ impl Inner {
|
|
|
1512
1983
|
}
|
|
1513
1984
|
#[cfg(target_os = "linux")]
|
|
1514
1985
|
{
|
|
1515
|
-
let
|
|
1986
|
+
let held = self
|
|
1516
1987
|
.layout
|
|
1517
1988
|
.lock()
|
|
1518
1989
|
.map_err(|_| ("session", String::from("no keyboard layout")))?;
|
|
1990
|
+
let layout = held
|
|
1991
|
+
.as_ref()
|
|
1992
|
+
.ok_or(("session", String::from("no keyboard layout")))?;
|
|
1519
1993
|
let (plan, unreachable) = layout.plan_text(text);
|
|
1520
|
-
drop(
|
|
1994
|
+
drop(held);
|
|
1521
1995
|
if !unreachable.is_empty() {
|
|
1522
1996
|
return Err((
|
|
1523
1997
|
"text-unsupported",
|
|
@@ -1619,11 +2093,13 @@ fn frame_interval(max_fps: u32) -> Duration {
|
|
|
1619
2093
|
Duration::from_secs_f64(1.0 / max_fps.max(1) as f64)
|
|
1620
2094
|
}
|
|
1621
2095
|
|
|
1622
|
-
/// The newest captured frame, handed from the capture
|
|
1623
|
-
/// newer frame replaces one the encoder has not taken,
|
|
1624
|
-
/// it falls, what it codes next is the desktop as it is
|
|
2096
|
+
/// The newest captured frame and when it was captured, handed from the capture
|
|
2097
|
+
/// thread to the encoder. A newer frame replaces one the encoder has not taken,
|
|
2098
|
+
/// so however far behind it falls, what it codes next is the desktop as it is
|
|
2099
|
+
/// now. The instant travels with the picture so the pipeline can report how
|
|
2100
|
+
/// long a coded frame waited for its encode.
|
|
1625
2101
|
struct FrameSlot {
|
|
1626
|
-
frame: Mutex<(Option<I420>, bool)>,
|
|
2102
|
+
frame: Mutex<(Option<(I420, Instant)>, bool)>,
|
|
1627
2103
|
ready: Condvar,
|
|
1628
2104
|
}
|
|
1629
2105
|
|
|
@@ -1640,13 +2116,13 @@ fn latest_frame() -> (FrameSender, FrameReceiver) {
|
|
|
1640
2116
|
|
|
1641
2117
|
impl FrameSender {
|
|
1642
2118
|
/// Hand over a frame; true when it replaced one the encoder never took.
|
|
1643
|
-
fn put(&self, frame: I420) -> bool {
|
|
2119
|
+
fn put(&self, frame: I420, captured: Instant) -> bool {
|
|
1644
2120
|
let mut held = self
|
|
1645
2121
|
.0
|
|
1646
2122
|
.frame
|
|
1647
2123
|
.lock()
|
|
1648
2124
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
1649
|
-
let replaced = held.0.replace(frame).is_some();
|
|
2125
|
+
let replaced = held.0.replace((frame, captured)).is_some();
|
|
1650
2126
|
self.0.ready.notify_one();
|
|
1651
2127
|
replaced
|
|
1652
2128
|
}
|
|
@@ -1666,12 +2142,22 @@ impl Drop for FrameSender {
|
|
|
1666
2142
|
}
|
|
1667
2143
|
|
|
1668
2144
|
enum Taken {
|
|
1669
|
-
Frame(I420),
|
|
2145
|
+
Frame(I420, Instant),
|
|
1670
2146
|
Timeout,
|
|
1671
2147
|
Ended,
|
|
1672
2148
|
}
|
|
1673
2149
|
|
|
1674
2150
|
impl FrameReceiver {
|
|
2151
|
+
/// Whether a frame is waiting to be taken.
|
|
2152
|
+
fn waiting(&self) -> bool {
|
|
2153
|
+
self.0
|
|
2154
|
+
.frame
|
|
2155
|
+
.lock()
|
|
2156
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
2157
|
+
.0
|
|
2158
|
+
.is_some()
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1675
2161
|
fn take(&self, wait: Duration) -> Taken {
|
|
1676
2162
|
let held = self
|
|
1677
2163
|
.0
|
|
@@ -1684,7 +2170,7 @@ impl FrameReceiver {
|
|
|
1684
2170
|
.wait_timeout_while(held, wait, |(frame, ended)| frame.is_none() && !*ended)
|
|
1685
2171
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
1686
2172
|
match held.0.take() {
|
|
1687
|
-
Some(frame) => Taken::Frame(frame),
|
|
2173
|
+
Some((frame, captured)) => Taken::Frame(frame, captured),
|
|
1688
2174
|
None if held.1 => Taken::Ended,
|
|
1689
2175
|
None => Taken::Timeout,
|
|
1690
2176
|
}
|
|
@@ -1710,12 +2196,32 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
|
|
|
1710
2196
|
(1500 + (width * height * 18 / 10_000) as u32).min(25_000)
|
|
1711
2197
|
}
|
|
1712
2198
|
|
|
1713
|
-
///
|
|
1714
|
-
///
|
|
2199
|
+
/// Queueing delay that means the link carries more than it drains: well under
|
|
2200
|
+
/// the latency budget, well over the noise of a busy host's scheduling.
|
|
2201
|
+
const QUEUE_HIGH: Duration = Duration::from_millis(25);
|
|
2202
|
+
/// Below this the path has room again and the target may creep back up.
|
|
2203
|
+
const QUEUE_LOW: Duration = Duration::from_millis(10);
|
|
2204
|
+
/// How often queueing delay may back the target off: long enough for the last
|
|
2205
|
+
/// back-off to reach the far end and show in its feedback, short enough that a
|
|
2206
|
+
/// queue is drained in a few steps rather than a few seconds.
|
|
2207
|
+
const DELAY_BACKOFF_EVERY: Duration = Duration::from_millis(300);
|
|
2208
|
+
|
|
2209
|
+
/// The rate target: back off when the far end reports a standing queue or
|
|
2210
|
+
/// loss, creep back when it reports neither, never above what the session
|
|
2211
|
+
/// asked for.
|
|
2212
|
+
///
|
|
2213
|
+
/// Delay leads. A link with a deep buffer (most Wi-Fi and cellular ones) never
|
|
2214
|
+
/// loses a packet it can queue, so loss alone lets queueing delay grow to
|
|
2215
|
+
/// seconds before anything reacts. A standing queue backs the target off to
|
|
2216
|
+
/// 85% of what the path delivered, which drains it; never more than halving
|
|
2217
|
+
/// per step, so a stream that was idle and sent little is not taken for a
|
|
2218
|
+
/// slow link. Receivers without transport-wide feedback leave it loss-only.
|
|
1715
2219
|
struct RateControl {
|
|
1716
2220
|
ceiling: u32,
|
|
1717
2221
|
target: u32,
|
|
1718
2222
|
last_change: Option<Instant>,
|
|
2223
|
+
/// The last queueing delay reported, while the path reports one.
|
|
2224
|
+
queue: Option<Duration>,
|
|
1719
2225
|
}
|
|
1720
2226
|
|
|
1721
2227
|
impl RateControl {
|
|
@@ -1724,26 +2230,40 @@ impl RateControl {
|
|
|
1724
2230
|
ceiling,
|
|
1725
2231
|
target: ceiling,
|
|
1726
2232
|
last_change: None,
|
|
2233
|
+
queue: None,
|
|
1727
2234
|
}
|
|
1728
2235
|
}
|
|
1729
2236
|
|
|
1730
|
-
/// The new target when these reports
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
.
|
|
1736
|
-
{
|
|
1737
|
-
return None;
|
|
2237
|
+
/// The new target when these loss reports (RFC 3550 fractions, /256) or
|
|
2238
|
+
/// this path report move it: down on a standing queue at most every
|
|
2239
|
+
/// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
|
|
2240
|
+
fn update(&mut self, reports: &[u8], path: Option<PathReport>, now: Instant) -> Option<u32> {
|
|
2241
|
+
if let Some(path) = path {
|
|
2242
|
+
self.queue = Some(path.queue_delay);
|
|
1738
2243
|
}
|
|
1739
|
-
let
|
|
2244
|
+
let since = self.last_change.map(|at| now.duration_since(at));
|
|
2245
|
+
let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
|
|
1740
2246
|
let floor = (self.ceiling / 8).max(500);
|
|
1741
|
-
let next = if
|
|
1742
|
-
(
|
|
1743
|
-
|
|
1744
|
-
|
|
2247
|
+
let next = if let Some(path) = path.filter(|path| path.queue_delay > QUEUE_HIGH) {
|
|
2248
|
+
if !waited(DELAY_BACKOFF_EVERY) {
|
|
2249
|
+
return None;
|
|
2250
|
+
}
|
|
2251
|
+
let delivered = path.delivered_kbps.clamp(self.target / 2, self.target);
|
|
2252
|
+
((delivered as f64 * 0.85) as u32).max(floor)
|
|
1745
2253
|
} else {
|
|
1746
|
-
|
|
2254
|
+
let worst = *reports.iter().max()?;
|
|
2255
|
+
if !waited(Duration::from_secs(1)) {
|
|
2256
|
+
return None;
|
|
2257
|
+
}
|
|
2258
|
+
let loss = worst as f64 / 256.0;
|
|
2259
|
+
let roomy = self.queue.is_none_or(|queue| queue < QUEUE_LOW);
|
|
2260
|
+
if loss > 0.10 {
|
|
2261
|
+
((self.target as f64 * 0.7) as u32).max(floor)
|
|
2262
|
+
} else if loss < 0.02 && roomy {
|
|
2263
|
+
((self.target as f64 * 1.08) as u32 + 64).min(self.ceiling)
|
|
2264
|
+
} else {
|
|
2265
|
+
self.target
|
|
2266
|
+
}
|
|
1747
2267
|
};
|
|
1748
2268
|
if next == self.target {
|
|
1749
2269
|
return None;
|
|
@@ -1754,9 +2274,127 @@ impl RateControl {
|
|
|
1754
2274
|
}
|
|
1755
2275
|
}
|
|
1756
2276
|
|
|
2277
|
+
/// How far the stream may run ahead of its rate target before the next frame
|
|
2278
|
+
/// waits: one large frame (a key frame, a refinement) goes at once, but the
|
|
2279
|
+
/// frames behind it hold until the link has had time to carry it, instead of
|
|
2280
|
+
/// queueing behind it. The frame that goes then is the newest one.
|
|
2281
|
+
const FRAME_BURST: Duration = Duration::from_millis(60);
|
|
2282
|
+
|
|
2283
|
+
/// How much faster than the target the budget drains. A rate-controlled
|
|
2284
|
+
/// encoder averages its target but overshoots it frame to frame; drained at
|
|
2285
|
+
/// the target itself, that ordinary overshoot would add up and hold frames on
|
|
2286
|
+
/// a link with room to spare. The rate control, not this, keeps the average
|
|
2287
|
+
/// under what the path carries.
|
|
2288
|
+
const BUDGET_HEADROOM: f64 = 1.25;
|
|
2289
|
+
|
|
2290
|
+
/// What has been sent beyond the rate target, draining a little faster than
|
|
2291
|
+
/// it: a leaky bucket that bounds a burst to `FRAME_BURST` plus one frame.
|
|
2292
|
+
#[derive(Default)]
|
|
2293
|
+
struct SendBudget {
|
|
2294
|
+
bits: f64,
|
|
2295
|
+
at: Option<Instant>,
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
impl SendBudget {
|
|
2299
|
+
/// The drain rate, in bits per second.
|
|
2300
|
+
fn rate(kbps: u32) -> f64 {
|
|
2301
|
+
kbps.max(1) as f64 * 1000.0 * BUDGET_HEADROOM
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
fn drained(&self, now: Instant, kbps: u32) -> f64 {
|
|
2305
|
+
let elapsed = self
|
|
2306
|
+
.at
|
|
2307
|
+
.map_or(0.0, |at| now.saturating_duration_since(at).as_secs_f64());
|
|
2308
|
+
(self.bits - elapsed * Self::rate(kbps)).max(0.0)
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/// How long until a frame may go; zero when it may go now.
|
|
2312
|
+
fn wait(&self, now: Instant, kbps: u32) -> Duration {
|
|
2313
|
+
let rate = Self::rate(kbps);
|
|
2314
|
+
let over = self.drained(now, kbps) - FRAME_BURST.as_secs_f64() * rate;
|
|
2315
|
+
Duration::from_secs_f64((over / rate).max(0.0))
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
fn sent(&mut self, now: Instant, bytes: usize, kbps: u32) {
|
|
2319
|
+
self.bits = self.drained(now, kbps) + bytes as f64 * 8.0;
|
|
2320
|
+
self.at = Some(now);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/// Whether motion is coded at half size. A full-size frame is slow when it
|
|
2325
|
+
/// took longer to code than a frame interval and the source already had a
|
|
2326
|
+
/// newer frame waiting: the encoder, not the source, set the pace. Content that
|
|
2327
|
+
/// arrives slower than the frame cap (30 fps into a 60 fps session) is never
|
|
2328
|
+
/// slow however long a frame takes, as long as it is done before the next one.
|
|
2329
|
+
/// Several slow frames that each change much of the picture (a packet over a
|
|
2330
|
+
/// quarter of the per-frame rate budget) mean full size cannot reach the frame
|
|
2331
|
+
/// rate, so the rest of the motion is coded at half size, until it stops (the
|
|
2332
|
+
/// refinement pass is always full size) or a run of frames that are both small
|
|
2333
|
+
/// and not slow shows full size would keep up again. Packet size or coding time
|
|
2334
|
+
/// alone never returns it to full size, so heavy motion does not flap between
|
|
2335
|
+
/// sizes; each change costs a key frame. Both directions need a run of frames
|
|
2336
|
+
/// rather than one, so a slow key frame or a busy moment on the host does not
|
|
2337
|
+
/// flip it, and typing never halves.
|
|
2338
|
+
#[derive(Default)]
|
|
2339
|
+
struct MotionSize {
|
|
2340
|
+
half: bool,
|
|
2341
|
+
run: u32,
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
impl MotionSize {
|
|
2345
|
+
const SLOW_RUN: u32 = 5;
|
|
2346
|
+
const SMALL_RUN: u32 = 30;
|
|
2347
|
+
|
|
2348
|
+
/// Account for a motion frame coded at the current size in `took`; `large`
|
|
2349
|
+
/// when its packet was over a quarter of the per-frame rate budget, `behind`
|
|
2350
|
+
/// when a newer frame was already waiting once it was coded.
|
|
2351
|
+
fn coded(&mut self, took: Duration, large: bool, behind: bool, interval: Duration) {
|
|
2352
|
+
let slow = took > interval && behind;
|
|
2353
|
+
let (counts, needed) = if self.half {
|
|
2354
|
+
(!large && !slow, Self::SMALL_RUN)
|
|
2355
|
+
} else {
|
|
2356
|
+
(large && slow, Self::SLOW_RUN)
|
|
2357
|
+
};
|
|
2358
|
+
self.run = if counts { self.run + 1 } else { 0 };
|
|
2359
|
+
if self.run >= needed {
|
|
2360
|
+
self.half = !self.half;
|
|
2361
|
+
self.run = 0;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/// The rate cap as a deadline rather than a minimum gap. `due` is where the
|
|
2367
|
+
/// next frame falls on the `max_fps` cadence the stream has kept so far, and a
|
|
2368
|
+
/// frame may go up to one interval before it, so a late frame's lost time is
|
|
2369
|
+
/// made up by the next. A capture clock that jitters around the cap is then
|
|
2370
|
+
/// coded the moment each frame arrives, instead of beating against a second
|
|
2371
|
+
/// clock that delays a frame or lets the next one supersede it; the sustained
|
|
2372
|
+
/// rate still never exceeds the cap, and no window holds more than one frame
|
|
2373
|
+
/// over it.
|
|
2374
|
+
struct Cadence {
|
|
2375
|
+
interval: Duration,
|
|
2376
|
+
due: Instant,
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
impl Cadence {
|
|
2380
|
+
fn new(interval: Duration, now: Instant) -> Self {
|
|
2381
|
+
Self { interval, due: now }
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/// How long until a frame may go; zero when it may go now.
|
|
2385
|
+
fn wait(&self, now: Instant) -> Duration {
|
|
2386
|
+
self.due.saturating_duration_since(now + self.interval)
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
fn sent(&mut self, now: Instant) {
|
|
2390
|
+
self.due = self.due.max(now) + self.interval;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
1757
2394
|
enum Pass {
|
|
1758
2395
|
Motion {
|
|
1759
2396
|
keyframe: bool,
|
|
2397
|
+
half: bool,
|
|
1760
2398
|
},
|
|
1761
2399
|
Refine,
|
|
1762
2400
|
/// The same picture again, so a still stream never looks stalled.
|
|
@@ -1774,6 +2412,7 @@ enum Pass {
|
|
|
1774
2412
|
/// the desktop goes still the last frame is coded once more, sharp.
|
|
1775
2413
|
fn spawn_pipeline(
|
|
1776
2414
|
inner: &Arc<Inner>,
|
|
2415
|
+
peer: Arc<VideoPeer>,
|
|
1777
2416
|
frame_rx: FrameReceiver,
|
|
1778
2417
|
running: Arc<AtomicBool>,
|
|
1779
2418
|
max_fps: u32,
|
|
@@ -1788,8 +2427,11 @@ fn spawn_pipeline(
|
|
|
1788
2427
|
let mut rate = RateControl::new(bitrate_kbps);
|
|
1789
2428
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1790
2429
|
m.target_kbps = bitrate_kbps;
|
|
2430
|
+
(m.codec, m.encoder) = ("vp9", "libvpx");
|
|
1791
2431
|
}
|
|
1792
|
-
|
|
2432
|
+
// Whether motion may be coded at half size: VP9's alone.
|
|
2433
|
+
let mut halves = true;
|
|
2434
|
+
let mut latest: Option<(I420, Instant)> = None;
|
|
1793
2435
|
// `pending`: the latest frame has not been coded. `refined`: it has
|
|
1794
2436
|
// had its refinement pass, or there is nothing to refine.
|
|
1795
2437
|
let mut pending = false;
|
|
@@ -1797,12 +2439,16 @@ fn spawn_pipeline(
|
|
|
1797
2439
|
let mut keyframe = true;
|
|
1798
2440
|
let mut still_since = Instant::now();
|
|
1799
2441
|
let mut last_sent: Option<Instant> = None;
|
|
2442
|
+
let mut size = MotionSize::default();
|
|
2443
|
+
let mut cadence = Cadence::new(interval, Instant::now());
|
|
2444
|
+
let mut budget = SendBudget::default();
|
|
2445
|
+
// What the last frame was stamped with: the RTP clock never runs
|
|
2446
|
+
// backwards, even for a re-coded picture.
|
|
2447
|
+
let mut last_stamp: Option<Instant> = None;
|
|
1800
2448
|
loop {
|
|
1801
2449
|
let now = Instant::now();
|
|
1802
|
-
let slot =
|
|
1803
|
-
|
|
1804
|
-
});
|
|
1805
|
-
let connected = inner.peer.is_connected();
|
|
2450
|
+
let slot = cadence.wait(now).max(budget.wait(now, rate.target));
|
|
2451
|
+
let connected = peer.is_connected();
|
|
1806
2452
|
let wait = if !connected {
|
|
1807
2453
|
// Nothing sent before the transport connects arrives; the
|
|
1808
2454
|
// first frame after it is a key frame.
|
|
@@ -1821,13 +2467,13 @@ fn spawn_pipeline(
|
|
|
1821
2467
|
.clamp(Duration::from_millis(10), Duration::from_millis(100))
|
|
1822
2468
|
};
|
|
1823
2469
|
match frame_rx.take(wait) {
|
|
1824
|
-
Taken::Frame(frame) => {
|
|
2470
|
+
Taken::Frame(frame, captured) => {
|
|
1825
2471
|
if pending {
|
|
1826
2472
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1827
2473
|
m.dropped_frames += 1;
|
|
1828
2474
|
}
|
|
1829
2475
|
}
|
|
1830
|
-
latest = Some(frame);
|
|
2476
|
+
latest = Some((frame, captured));
|
|
1831
2477
|
pending = true;
|
|
1832
2478
|
refined = false;
|
|
1833
2479
|
still_since = Instant::now();
|
|
@@ -1838,33 +2484,63 @@ fn spawn_pipeline(
|
|
|
1838
2484
|
if !running.load(Ordering::Relaxed) {
|
|
1839
2485
|
break;
|
|
1840
2486
|
}
|
|
1841
|
-
if
|
|
2487
|
+
if peer.take_keyframe_request() || !connected {
|
|
1842
2488
|
keyframe = true;
|
|
1843
2489
|
}
|
|
1844
|
-
let reports =
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2490
|
+
let reports = peer.take_loss_reports();
|
|
2491
|
+
let path = peer.take_path_report();
|
|
2492
|
+
if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
|
|
2493
|
+
let applied = inner.encoder.lock().map(|mut encoder| {
|
|
2494
|
+
encoder
|
|
2495
|
+
.as_mut()
|
|
2496
|
+
.map(|encoder| encoder.set_bitrate(kbps))
|
|
2497
|
+
.unwrap_or(Ok(()))
|
|
2498
|
+
});
|
|
1850
2499
|
if matches!(applied, Ok(Ok(()))) {
|
|
1851
2500
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1852
2501
|
m.target_kbps = kbps;
|
|
1853
2502
|
}
|
|
1854
2503
|
}
|
|
1855
2504
|
}
|
|
1856
|
-
let Some(frame) = latest.as_ref() else {
|
|
2505
|
+
let Some((frame, captured)) = latest.as_ref() else {
|
|
1857
2506
|
continue;
|
|
1858
2507
|
};
|
|
1859
2508
|
if !connected {
|
|
1860
2509
|
continue;
|
|
1861
2510
|
}
|
|
1862
2511
|
let now = Instant::now();
|
|
1863
|
-
if
|
|
2512
|
+
if !cadence.wait(now).is_zero() || !budget.wait(now, rate.target).is_zero() {
|
|
1864
2513
|
continue;
|
|
1865
2514
|
}
|
|
2515
|
+
if halves && peer.sends_h264() {
|
|
2516
|
+
// The answer asked for H.264: nothing has been sent yet,
|
|
2517
|
+
// since frames wait for the transport, which waits for it.
|
|
2518
|
+
match start_h264(&inner, rate.target, max_fps) {
|
|
2519
|
+
Ok(()) => {
|
|
2520
|
+
halves = false;
|
|
2521
|
+
size = MotionSize::default();
|
|
2522
|
+
keyframe = true;
|
|
2523
|
+
}
|
|
2524
|
+
Err(error) => {
|
|
2525
|
+
eprintln!("{error:#}");
|
|
2526
|
+
let reason = String::from("no H.264 encoder could start");
|
|
2527
|
+
inner.notify(SessionEvent::Revoked {
|
|
2528
|
+
reason: reason.clone(),
|
|
2529
|
+
code: "error",
|
|
2530
|
+
});
|
|
2531
|
+
let target = inner.clone();
|
|
2532
|
+
handle.spawn(async move {
|
|
2533
|
+
target.close(&reason, "error").await;
|
|
2534
|
+
});
|
|
2535
|
+
break;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
1866
2539
|
let pass = if pending || keyframe {
|
|
1867
|
-
Pass::Motion {
|
|
2540
|
+
Pass::Motion {
|
|
2541
|
+
keyframe,
|
|
2542
|
+
half: size.half && halves,
|
|
2543
|
+
}
|
|
1868
2544
|
} else if !refined && now.duration_since(still_since) >= REFINE_AFTER {
|
|
1869
2545
|
Pass::Refine
|
|
1870
2546
|
} else if refined
|
|
@@ -1877,14 +2553,17 @@ fn spawn_pipeline(
|
|
|
1877
2553
|
|
|
1878
2554
|
let started = Instant::now();
|
|
1879
2555
|
let packet = {
|
|
1880
|
-
let mut
|
|
1881
|
-
Ok(
|
|
2556
|
+
let mut held = match inner.encoder.lock() {
|
|
2557
|
+
Ok(held) => held,
|
|
1882
2558
|
Err(_) => break,
|
|
1883
2559
|
};
|
|
2560
|
+
let Some(encoder) = held.as_mut() else {
|
|
2561
|
+
break;
|
|
2562
|
+
};
|
|
1884
2563
|
match pass {
|
|
1885
|
-
Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
|
|
2564
|
+
Pass::Motion { keyframe, half } => encoder.encode(frame, keyframe, half),
|
|
1886
2565
|
Pass::Refine => encoder.refine(frame),
|
|
1887
|
-
Pass::Keepalive => encoder.encode(frame, false),
|
|
2566
|
+
Pass::Keepalive => encoder.encode(frame, false, false),
|
|
1888
2567
|
}
|
|
1889
2568
|
};
|
|
1890
2569
|
let packet = match packet {
|
|
@@ -1902,22 +2581,29 @@ fn spawn_pipeline(
|
|
|
1902
2581
|
let reason = String::from("the encoder rejected a frame");
|
|
1903
2582
|
inner.notify(SessionEvent::Revoked {
|
|
1904
2583
|
reason: reason.clone(),
|
|
2584
|
+
code: "error",
|
|
1905
2585
|
});
|
|
1906
2586
|
let target = inner.clone();
|
|
1907
2587
|
handle.spawn(async move {
|
|
1908
|
-
target.close(&reason).await;
|
|
2588
|
+
target.close(&reason, "error").await;
|
|
1909
2589
|
});
|
|
1910
2590
|
break;
|
|
1911
2591
|
}
|
|
1912
2592
|
};
|
|
2593
|
+
let took = started.elapsed();
|
|
1913
2594
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1914
2595
|
m.encoded_frames += 1;
|
|
1915
2596
|
m.encoded_bytes += packet.data.len() as u64;
|
|
1916
|
-
m.encode_micros +=
|
|
2597
|
+
m.encode_micros += took.as_micros() as u64;
|
|
2598
|
+
m.key_frames += packet.keyframe as u64;
|
|
2599
|
+
if matches!(pass, Pass::Motion { .. }) && pending {
|
|
2600
|
+
m.queue_micros += started.duration_since(*captured).as_micros() as u64;
|
|
2601
|
+
m.motion_frames += 1;
|
|
2602
|
+
}
|
|
1917
2603
|
match pass {
|
|
1918
|
-
Pass::Motion {
|
|
2604
|
+
Pass::Motion { half: true, .. } => m.halved_frames += 1,
|
|
1919
2605
|
Pass::Refine => m.refined_frames += 1,
|
|
1920
|
-
Pass::Motion {
|
|
2606
|
+
Pass::Motion { half: false, .. } | Pass::Keepalive => {}
|
|
1921
2607
|
}
|
|
1922
2608
|
}
|
|
1923
2609
|
match pass {
|
|
@@ -1926,14 +2612,34 @@ fn spawn_pipeline(
|
|
|
1926
2612
|
keyframe = false;
|
|
1927
2613
|
refined = false;
|
|
1928
2614
|
still_since = now;
|
|
2615
|
+
let budget = rate.target as u64 * 1000 / 8 / max_fps.max(1) as u64;
|
|
2616
|
+
let large = packet.data.len() as u64 * 4 > budget;
|
|
2617
|
+
if halves {
|
|
2618
|
+
size.coded(took, large, frame_rx.waiting(), interval);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
Pass::Refine => {
|
|
2622
|
+
refined = true;
|
|
2623
|
+
size = MotionSize::default();
|
|
1929
2624
|
}
|
|
1930
|
-
Pass::Refine => refined = true,
|
|
1931
2625
|
Pass::Keepalive => {}
|
|
1932
2626
|
}
|
|
1933
2627
|
last_sent = Some(now);
|
|
1934
|
-
|
|
2628
|
+
cadence.sent(now);
|
|
2629
|
+
budget.sent(now, packet.data.len(), rate.target);
|
|
2630
|
+
// A new picture carries the moment it was captured, so the
|
|
2631
|
+
// receiver paces playout by the desktop's clock rather than by
|
|
2632
|
+
// encode time; a re-coded one (refinement, keepalive) is new
|
|
2633
|
+
// content now.
|
|
2634
|
+
let stamp = match pass {
|
|
2635
|
+
Pass::Motion { .. } => *captured,
|
|
2636
|
+
Pass::Refine | Pass::Keepalive => now,
|
|
2637
|
+
};
|
|
2638
|
+
let stamp = last_stamp.map_or(stamp, |at| stamp.max(at + Duration::from_millis(1)));
|
|
2639
|
+
last_stamp = Some(stamp);
|
|
2640
|
+
let sending = Instant::now();
|
|
1935
2641
|
if let Err(error) =
|
|
1936
|
-
handle.block_on(peer.send_frame(&packet.data, packet.keyframe,
|
|
2642
|
+
handle.block_on(peer.send_frame(&packet.data, packet.keyframe, stamp))
|
|
1937
2643
|
{
|
|
1938
2644
|
// The transport is gone. End the session rather than leave
|
|
1939
2645
|
// the desktop captured behind a picture that stopped.
|
|
@@ -1941,18 +2647,155 @@ fn spawn_pipeline(
|
|
|
1941
2647
|
let reason = String::from("the connection to the phone was lost");
|
|
1942
2648
|
inner.notify(SessionEvent::Revoked {
|
|
1943
2649
|
reason: reason.clone(),
|
|
2650
|
+
code: "transport",
|
|
1944
2651
|
});
|
|
1945
2652
|
let target = inner.clone();
|
|
1946
2653
|
handle.spawn(async move {
|
|
1947
|
-
target.close(&reason).await;
|
|
2654
|
+
target.close(&reason, "transport").await;
|
|
1948
2655
|
});
|
|
1949
2656
|
break;
|
|
1950
2657
|
}
|
|
2658
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2659
|
+
m.send_micros += sending.elapsed().as_micros() as u64;
|
|
2660
|
+
}
|
|
1951
2661
|
}
|
|
1952
2662
|
})
|
|
1953
2663
|
.ok();
|
|
1954
2664
|
}
|
|
1955
2665
|
|
|
2666
|
+
/// Replace the session's VP9 encoder with an H.264 one of the same size and
|
|
2667
|
+
/// rate target, on the encode thread that will drive it.
|
|
2668
|
+
fn start_h264(inner: &Arc<Inner>, bitrate_kbps: u32, fps: u32) -> Result<()> {
|
|
2669
|
+
let mut held = lock(&inner.encoder);
|
|
2670
|
+
let (width, height) = held
|
|
2671
|
+
.as_ref()
|
|
2672
|
+
.map(VideoEncoder::size)
|
|
2673
|
+
.context("the session has no encoder")?;
|
|
2674
|
+
let (backend, info) = crate::h264::open(width, height, bitrate_kbps, fps)?;
|
|
2675
|
+
*held = Some(VideoEncoder::H264 {
|
|
2676
|
+
backend,
|
|
2677
|
+
size: (width, height),
|
|
2678
|
+
});
|
|
2679
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2680
|
+
(m.codec, m.encoder) = ("h264", info.encoder);
|
|
2681
|
+
}
|
|
2682
|
+
Ok(())
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/// Packetize what a consumer feeds an encoded source, building the peer on the
|
|
2686
|
+
/// first access unit that carries an SPS.
|
|
2687
|
+
///
|
|
2688
|
+
/// That order is forced by the contract: the offer must name the profile the
|
|
2689
|
+
/// stream actually is, and the only place that is written down is the stream's
|
|
2690
|
+
/// own SPS. Until one arrives there is nothing that could be offered.
|
|
2691
|
+
fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUnit>) {
|
|
2692
|
+
let inner = inner.clone();
|
|
2693
|
+
tokio::spawn(async move {
|
|
2694
|
+
while let Some(unit) = feed.recv().await {
|
|
2695
|
+
if inner.closed.load(Ordering::SeqCst) {
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if current_peer(&inner).is_none() {
|
|
2699
|
+
let Some(profile) = h264_profile_level_id(&unit.data) else {
|
|
2700
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2701
|
+
m.dropped_frames += 1;
|
|
2702
|
+
}
|
|
2703
|
+
continue;
|
|
2704
|
+
};
|
|
2705
|
+
let (transport, peer_events, codec) = {
|
|
2706
|
+
let encoded = lock(&inner.encoded);
|
|
2707
|
+
let Some(encoded) = encoded.as_ref() else {
|
|
2708
|
+
return;
|
|
2709
|
+
};
|
|
2710
|
+
let codec = match encoded.codec {
|
|
2711
|
+
EncodedCodec::H264 => VideoCodec::H264 {
|
|
2712
|
+
profile_level_id: profile,
|
|
2713
|
+
},
|
|
2714
|
+
};
|
|
2715
|
+
(
|
|
2716
|
+
encoded.transport.clone(),
|
|
2717
|
+
encoded.peer_events.clone(),
|
|
2718
|
+
codec,
|
|
2719
|
+
)
|
|
2720
|
+
};
|
|
2721
|
+
let offered = VideoPeer::offer(transport, peer_events, codec).await;
|
|
2722
|
+
match offered {
|
|
2723
|
+
Ok((peer, offer)) => {
|
|
2724
|
+
let peer = Arc::new(peer);
|
|
2725
|
+
let stored = {
|
|
2726
|
+
let mut held = lock(&inner.peer);
|
|
2727
|
+
if inner.closed.load(Ordering::SeqCst) {
|
|
2728
|
+
false
|
|
2729
|
+
} else {
|
|
2730
|
+
*held = Some(peer.clone());
|
|
2731
|
+
true
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
if !stored {
|
|
2735
|
+
peer.close().await;
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
inner.notify(SessionEvent::Description {
|
|
2739
|
+
generation: inner.generation,
|
|
2740
|
+
sdp: offer,
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
Err(error) => {
|
|
2744
|
+
revoke(
|
|
2745
|
+
&inner,
|
|
2746
|
+
format!("the transport refused the encoded stream: {error:#}"),
|
|
2747
|
+
"error",
|
|
2748
|
+
)
|
|
2749
|
+
.await;
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
let Some(peer) = current_peer(&inner) else {
|
|
2755
|
+
return;
|
|
2756
|
+
};
|
|
2757
|
+
let started = Instant::now();
|
|
2758
|
+
let sent = peer.send_frame(&unit.data, unit.keyframe, started).await;
|
|
2759
|
+
match sent {
|
|
2760
|
+
Ok(()) => {
|
|
2761
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2762
|
+
m.encoded_frames += 1;
|
|
2763
|
+
m.encoded_bytes += unit.data.len() as u64;
|
|
2764
|
+
m.encode_micros += started.elapsed().as_micros() as u64;
|
|
2765
|
+
if unit.keyframe {
|
|
2766
|
+
m.key_frames += 1;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
Err(error) => {
|
|
2771
|
+
eprintln!("the video track refused a fed access unit: {error:#}");
|
|
2772
|
+
revoke(
|
|
2773
|
+
&inner,
|
|
2774
|
+
String::from("the connection to the phone was lost"),
|
|
2775
|
+
"transport",
|
|
2776
|
+
)
|
|
2777
|
+
.await;
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
fn current_peer(inner: &Arc<Inner>) -> Option<Arc<VideoPeer>> {
|
|
2786
|
+
inner.peer.lock().ok().and_then(|peer| peer.clone())
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
/// End the session with a reason the consumer can show, exactly as the encode
|
|
2790
|
+
/// pipeline does when its transport goes away.
|
|
2791
|
+
async fn revoke(inner: &Arc<Inner>, reason: String, code: &'static str) {
|
|
2792
|
+
inner.notify(SessionEvent::Revoked {
|
|
2793
|
+
reason: reason.clone(),
|
|
2794
|
+
code,
|
|
2795
|
+
});
|
|
2796
|
+
inner.close(&reason, code).await;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
1956
2799
|
fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
|
|
1957
2800
|
let inner = inner.clone();
|
|
1958
2801
|
tokio::spawn(async move {
|
|
@@ -1975,6 +2818,16 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
1975
2818
|
}
|
|
1976
2819
|
PeerEvent::State(state) => {
|
|
1977
2820
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
2821
|
+
// The far end needs a reference frame to start from, and only
|
|
2822
|
+
// the consumer can make one for a stream it encodes. A
|
|
2823
|
+
// captured desktop manages this in its encode loop; a fed one
|
|
2824
|
+
// has to ask — and everything sent before the transport
|
|
2825
|
+
// connected was dropped, so this is not optional.
|
|
2826
|
+
if lock(&inner.encoded).is_some() && matches!(state, State::Connected) {
|
|
2827
|
+
inner.notify(SessionEvent::KeyframeRequest {
|
|
2828
|
+
generation: inner.generation,
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
1978
2831
|
// ICE loss need not close SCTP. Never leave a drag or chord
|
|
1979
2832
|
// held while waiting for that independent notification.
|
|
1980
2833
|
if matches!(state, State::Disconnected | State::Failed | State::Closed) {
|
|
@@ -1988,8 +2841,9 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
1988
2841
|
let reason = String::from("the connection to the phone was lost");
|
|
1989
2842
|
inner.notify(SessionEvent::Revoked {
|
|
1990
2843
|
reason: reason.clone(),
|
|
2844
|
+
code: "transport",
|
|
1991
2845
|
});
|
|
1992
|
-
inner.close(&reason).await;
|
|
2846
|
+
inner.close(&reason, "transport").await;
|
|
1993
2847
|
continue;
|
|
1994
2848
|
}
|
|
1995
2849
|
inner.notify(SessionEvent::State {
|
|
@@ -2021,6 +2875,11 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
2021
2875
|
}
|
|
2022
2876
|
}
|
|
2023
2877
|
}
|
|
2878
|
+
PeerEvent::KeyframeRequest => {
|
|
2879
|
+
inner.notify(SessionEvent::KeyframeRequest {
|
|
2880
|
+
generation: inner.generation,
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2024
2883
|
PeerEvent::ControlMessage(text) => {
|
|
2025
2884
|
match serde_json::from_str::<ControlMessage>(&text) {
|
|
2026
2885
|
Ok(message) => inner.apply(message),
|
|
@@ -2042,10 +2901,11 @@ fn spawn_lease(inner: &Arc<Inner>, ttl: Duration) {
|
|
|
2042
2901
|
let reason = String::from("the session lease expired");
|
|
2043
2902
|
inner.notify(SessionEvent::Revoked {
|
|
2044
2903
|
reason: reason.clone(),
|
|
2904
|
+
code: "lease",
|
|
2045
2905
|
});
|
|
2046
2906
|
// An expiry ends the session exactly as an explicit close does; the
|
|
2047
2907
|
// notification above is not a substitute for stopping the desktop.
|
|
2048
|
-
inner.close(&reason).await;
|
|
2908
|
+
inner.close(&reason, "lease").await;
|
|
2049
2909
|
});
|
|
2050
2910
|
}
|
|
2051
2911
|
|
|
@@ -2082,6 +2942,72 @@ fn opaque_id() -> String {
|
|
|
2082
2942
|
mod tests {
|
|
2083
2943
|
use super::*;
|
|
2084
2944
|
|
|
2945
|
+
#[tokio::test]
|
|
2946
|
+
async fn a_closed_encoded_session_ends_the_tasks_its_channels_park() {
|
|
2947
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
2948
|
+
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
2949
|
+
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
2950
|
+
let inner = Arc::new(Inner {
|
|
2951
|
+
id: String::from("encoded-session"),
|
|
2952
|
+
generation: 1,
|
|
2953
|
+
permissions: Vec::new(),
|
|
2954
|
+
source: SelectedSource {
|
|
2955
|
+
node_id: 0,
|
|
2956
|
+
width: 640,
|
|
2957
|
+
height: 480,
|
|
2958
|
+
position: None,
|
|
2959
|
+
source_type: Some(String::from("encoded")),
|
|
2960
|
+
origin_x: 0,
|
|
2961
|
+
origin_y: 0,
|
|
2962
|
+
},
|
|
2963
|
+
geometry: serde_json::json!({
|
|
2964
|
+
"source": { "width": 640, "height": 480 },
|
|
2965
|
+
"encoded": { "width": 640, "height": 480 },
|
|
2966
|
+
"origin": { "x": 0, "y": 0 },
|
|
2967
|
+
}),
|
|
2968
|
+
metrics: Arc::new(Mutex::new(Metrics::default())),
|
|
2969
|
+
latest: Arc::new(Mutex::new(None)),
|
|
2970
|
+
capture_error: Arc::new(Mutex::new(None)),
|
|
2971
|
+
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2972
|
+
peer: Mutex::new(None),
|
|
2973
|
+
encoder: Mutex::new(None),
|
|
2974
|
+
input: Mutex::new(None),
|
|
2975
|
+
indicator: Mutex::new(None),
|
|
2976
|
+
capture: Mutex::new(None),
|
|
2977
|
+
layout: Mutex::new(None),
|
|
2978
|
+
last_seq: Mutex::new(0),
|
|
2979
|
+
control_open: AtomicBool::new(false),
|
|
2980
|
+
closed: AtomicBool::new(false),
|
|
2981
|
+
pipeline: Arc::new(AtomicBool::new(false)),
|
|
2982
|
+
events,
|
|
2983
|
+
encoded: Mutex::new(Some(Encoded {
|
|
2984
|
+
codec: EncodedCodec::H264,
|
|
2985
|
+
transport: TransportOptions {
|
|
2986
|
+
ice_servers: Vec::new(),
|
|
2987
|
+
loopback_tcp: false,
|
|
2988
|
+
pace_bps: 20_000_000.0,
|
|
2989
|
+
},
|
|
2990
|
+
peer_events,
|
|
2991
|
+
feed,
|
|
2992
|
+
})),
|
|
2993
|
+
local_frames: false,
|
|
2994
|
+
});
|
|
2995
|
+
spawn_peer_events(&inner, peer_events_rx);
|
|
2996
|
+
spawn_encoded(&inner, feed_rx);
|
|
2997
|
+
inner.close("test", "closed").await;
|
|
2998
|
+
for _ in 0..100 {
|
|
2999
|
+
if Arc::strong_count(&inner) == 1 {
|
|
3000
|
+
break;
|
|
3001
|
+
}
|
|
3002
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
3003
|
+
}
|
|
3004
|
+
assert_eq!(
|
|
3005
|
+
Arc::strong_count(&inner),
|
|
3006
|
+
1,
|
|
3007
|
+
"closing must end the tasks whose channels the close takes away",
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2085
3011
|
#[test]
|
|
2086
3012
|
fn tile_damage_at_origin_does_not_underflow_or_hide_other_tiles() {
|
|
2087
3013
|
assert_eq!(
|
|
@@ -2090,6 +3016,41 @@ mod tests {
|
|
|
2090
3016
|
);
|
|
2091
3017
|
}
|
|
2092
3018
|
|
|
3019
|
+
#[test]
|
|
3020
|
+
fn frames_that_jitter_around_the_cap_go_at_once_and_a_flood_stays_capped() {
|
|
3021
|
+
let interval = Duration::from_millis(20);
|
|
3022
|
+
let start = Instant::now();
|
|
3023
|
+
let mut cadence = Cadence::new(interval, start);
|
|
3024
|
+
// Captured at the cap, each a few ms early or late: none waits, so
|
|
3025
|
+
// none is delayed or superseded by the next.
|
|
3026
|
+
for (n, jitter) in [0i64, -6, 5, -8, 7, -3, 0, -9, 9, -5].iter().enumerate() {
|
|
3027
|
+
let at = start + interval * n as u32 + Duration::from_millis(9);
|
|
3028
|
+
let at = if *jitter < 0 {
|
|
3029
|
+
at - Duration::from_millis(jitter.unsigned_abs())
|
|
3030
|
+
} else {
|
|
3031
|
+
at + Duration::from_millis(*jitter as u64)
|
|
3032
|
+
};
|
|
3033
|
+
assert_eq!(cadence.wait(at), Duration::ZERO, "frame {n} waited");
|
|
3034
|
+
cadence.sent(at);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// A source far above the cap: one second of 1 ms frames sends at most
|
|
3038
|
+
// one frame over the cap.
|
|
3039
|
+
let start = start + Duration::from_secs(10);
|
|
3040
|
+
let mut cadence = Cadence::new(interval, start);
|
|
3041
|
+
let sent = (0..1000)
|
|
3042
|
+
.map(|ms| start + Duration::from_millis(ms))
|
|
3043
|
+
.filter(|&at| {
|
|
3044
|
+
let go = cadence.wait(at).is_zero();
|
|
3045
|
+
if go {
|
|
3046
|
+
cadence.sent(at);
|
|
3047
|
+
}
|
|
3048
|
+
go
|
|
3049
|
+
})
|
|
3050
|
+
.count();
|
|
3051
|
+
assert!((50..=51).contains(&sent), "sent {sent} at a 50 fps cap");
|
|
3052
|
+
}
|
|
3053
|
+
|
|
2093
3054
|
#[test]
|
|
2094
3055
|
fn a_small_source_is_never_upscaled() {
|
|
2095
3056
|
assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
|
|
@@ -2144,9 +3105,19 @@ mod tests {
|
|
|
2144
3105
|
pace_bps: 20_000_000.0,
|
|
2145
3106
|
},
|
|
2146
3107
|
peer_events,
|
|
3108
|
+
VideoCodec::Vp9,
|
|
2147
3109
|
)
|
|
2148
3110
|
.await
|
|
2149
3111
|
.expect("a peer connection builds without a network");
|
|
3112
|
+
test_inner_with(events, peer, 64, 64)
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
fn test_inner_with(
|
|
3116
|
+
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3117
|
+
peer: VideoPeer,
|
|
3118
|
+
width: usize,
|
|
3119
|
+
height: usize,
|
|
3120
|
+
) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
|
|
2150
3121
|
// The pipeline codes nothing before the transport connects.
|
|
2151
3122
|
peer.assume_connected();
|
|
2152
3123
|
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
@@ -2172,8 +3143,10 @@ mod tests {
|
|
|
2172
3143
|
latest: Arc::new(Mutex::new(None)),
|
|
2173
3144
|
capture_error: Arc::new(Mutex::new(None)),
|
|
2174
3145
|
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2175
|
-
peer: Arc::new(peer),
|
|
2176
|
-
encoder: Mutex::new(
|
|
3146
|
+
peer: Mutex::new(Some(Arc::new(peer))),
|
|
3147
|
+
encoder: Mutex::new(Some(VideoEncoder::Vp9(
|
|
3148
|
+
Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
|
|
3149
|
+
))),
|
|
2177
3150
|
input: Mutex::new(Some(InputTarget {
|
|
2178
3151
|
applier: Applier::Recording(recorded.clone()),
|
|
2179
3152
|
held: HeldState::default(),
|
|
@@ -2184,12 +3157,14 @@ mod tests {
|
|
|
2184
3157
|
})),
|
|
2185
3158
|
capture: Mutex::new(None),
|
|
2186
3159
|
indicator: Mutex::new(None),
|
|
2187
|
-
layout: Mutex::new(Layout::from_environment().expect("a keymap")),
|
|
3160
|
+
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
2188
3161
|
last_seq: Mutex::new(0),
|
|
2189
3162
|
control_open: AtomicBool::new(true),
|
|
2190
3163
|
closed: AtomicBool::new(false),
|
|
2191
3164
|
pipeline: Arc::new(AtomicBool::new(true)),
|
|
2192
3165
|
events,
|
|
3166
|
+
encoded: Mutex::new(None),
|
|
3167
|
+
local_frames: true,
|
|
2193
3168
|
});
|
|
2194
3169
|
(inner, recorded)
|
|
2195
3170
|
}
|
|
@@ -2230,11 +3205,11 @@ mod tests {
|
|
|
2230
3205
|
matches!(
|
|
2231
3206
|
received.try_recv(),
|
|
2232
3207
|
Ok(Notice {
|
|
2233
|
-
event: SessionEvent::Revoked { .. },
|
|
3208
|
+
event: SessionEvent::Revoked { code: "lease", .. },
|
|
2234
3209
|
..
|
|
2235
3210
|
})
|
|
2236
3211
|
),
|
|
2237
|
-
"the consumer is still told
|
|
3212
|
+
"the consumer is still told the session's authority ended",
|
|
2238
3213
|
);
|
|
2239
3214
|
|
|
2240
3215
|
let rejected = inner.metrics.lock().unwrap().input_rejected;
|
|
@@ -2474,12 +3449,22 @@ mod tests {
|
|
|
2474
3449
|
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
2475
3450
|
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
2476
3451
|
let (mut inner, _) = test_inner(events).await;
|
|
2477
|
-
Arc::get_mut(&mut inner)
|
|
3452
|
+
Arc::get_mut(&mut inner)
|
|
3453
|
+
.unwrap()
|
|
3454
|
+
.permissions
|
|
3455
|
+
.push(Permission::View);
|
|
2478
3456
|
let session = Session { inner };
|
|
2479
|
-
*lock(&session.inner.latest) =
|
|
3457
|
+
*lock(&session.inner.latest) =
|
|
3458
|
+
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
2480
3459
|
*lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
|
|
2481
|
-
assert_eq!(
|
|
2482
|
-
|
|
3460
|
+
assert_eq!(
|
|
3461
|
+
session.frame(None, "", None).unwrap_err().code,
|
|
3462
|
+
"stream-stopped"
|
|
3463
|
+
);
|
|
3464
|
+
assert_eq!(
|
|
3465
|
+
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
3466
|
+
"stream-stopped"
|
|
3467
|
+
);
|
|
2483
3468
|
}
|
|
2484
3469
|
|
|
2485
3470
|
#[tokio::test]
|
|
@@ -2487,24 +3472,35 @@ mod tests {
|
|
|
2487
3472
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
2488
3473
|
let (inner, _recorded) = test_inner(events).await;
|
|
2489
3474
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2490
|
-
spawn_pipeline(
|
|
3475
|
+
spawn_pipeline(
|
|
3476
|
+
&inner,
|
|
3477
|
+
current_peer(&inner).expect("the test's peer"),
|
|
3478
|
+
frame_rx,
|
|
3479
|
+
Arc::new(AtomicBool::new(true)),
|
|
3480
|
+
30,
|
|
3481
|
+
1000,
|
|
3482
|
+
);
|
|
2491
3483
|
|
|
2492
3484
|
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
2493
3485
|
// used to be counted and dropped behind a permanently black picture.
|
|
2494
|
-
frame_tx.put(
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
3486
|
+
frame_tx.put(
|
|
3487
|
+
I420 {
|
|
3488
|
+
width: 32,
|
|
3489
|
+
height: 32,
|
|
3490
|
+
data: vec![128u8; 32 * 32 + 2 * 16 * 16],
|
|
3491
|
+
},
|
|
3492
|
+
Instant::now(),
|
|
3493
|
+
);
|
|
2499
3494
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
2500
3495
|
|
|
2501
3496
|
let mut reason = None;
|
|
2502
3497
|
while let Ok(event) = received.try_recv() {
|
|
2503
3498
|
if let Notice {
|
|
2504
|
-
event: SessionEvent::Revoked { reason: why },
|
|
3499
|
+
event: SessionEvent::Revoked { reason: why, code },
|
|
2505
3500
|
..
|
|
2506
3501
|
} = event
|
|
2507
3502
|
{
|
|
3503
|
+
assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
|
|
2508
3504
|
reason = Some(why);
|
|
2509
3505
|
}
|
|
2510
3506
|
}
|
|
@@ -2519,6 +3515,184 @@ mod tests {
|
|
|
2519
3515
|
);
|
|
2520
3516
|
}
|
|
2521
3517
|
|
|
3518
|
+
/// A consumer-fed stream: a keyframe carrying SPS, PPS and an IDR slice.
|
|
3519
|
+
fn fed_keyframe() -> Vec<u8> {
|
|
3520
|
+
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
3521
|
+
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
3522
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
3523
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
3524
|
+
stream
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
fn encoded_open() -> OpenRequest {
|
|
3528
|
+
OpenRequest {
|
|
3529
|
+
source: Some(SourceRequest::Encoded {
|
|
3530
|
+
codec: EncodedCodec::H264,
|
|
3531
|
+
width: 486,
|
|
3532
|
+
height: 1080,
|
|
3533
|
+
}),
|
|
3534
|
+
permissions: vec![Permission::View, Permission::Control],
|
|
3535
|
+
max_width: 3840,
|
|
3536
|
+
max_height: 2160,
|
|
3537
|
+
bitrate_kbps: 0,
|
|
3538
|
+
max_fps: 30,
|
|
3539
|
+
ice_servers: Vec::new(),
|
|
3540
|
+
restore_token: None,
|
|
3541
|
+
ttl: Some(Duration::from_secs(30)),
|
|
3542
|
+
loopback_tcp: false,
|
|
3543
|
+
agent_indicator: false,
|
|
3544
|
+
local_frames: true,
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
async fn next_notice(received: &mut tokio_mpsc::UnboundedReceiver<Notice>) -> Notice {
|
|
3549
|
+
tokio::time::timeout(Duration::from_secs(5), received.recv())
|
|
3550
|
+
.await
|
|
3551
|
+
.expect("a notification arrives")
|
|
3552
|
+
.expect("the channel stays open")
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
#[tokio::test]
|
|
3556
|
+
async fn an_encoded_session_opens_without_capture_encoder_or_input() {
|
|
3557
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3558
|
+
let session = Session::open(encoded_open(), events)
|
|
3559
|
+
.await
|
|
3560
|
+
.expect("an encoded source needs no display and no input device");
|
|
3561
|
+
let inner = &session.inner;
|
|
3562
|
+
assert_eq!(session.restore_source(), "encoded");
|
|
3563
|
+
assert_eq!(session.geometry()["encoded"]["width"], 486);
|
|
3564
|
+
assert_eq!(session.geometry()["encoded"]["height"], 1080);
|
|
3565
|
+
assert!(
|
|
3566
|
+
inner.encoder.lock().unwrap().is_none(),
|
|
3567
|
+
"the consumer is the encoder for this source"
|
|
3568
|
+
);
|
|
3569
|
+
assert!(
|
|
3570
|
+
inner.capture.lock().unwrap().is_none(),
|
|
3571
|
+
"there is nothing to capture"
|
|
3572
|
+
);
|
|
3573
|
+
assert!(
|
|
3574
|
+
inner.input.lock().unwrap().is_none(),
|
|
3575
|
+
"the client's input belongs to the consumer's device"
|
|
3576
|
+
);
|
|
3577
|
+
assert!(
|
|
3578
|
+
inner.peer.lock().unwrap().is_none(),
|
|
3579
|
+
"the offer waits for the stream's own SPS"
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
#[tokio::test]
|
|
3584
|
+
async fn the_offer_for_a_fed_stream_names_that_streams_own_profile() {
|
|
3585
|
+
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3586
|
+
let session = Session::open(encoded_open(), events)
|
|
3587
|
+
.await
|
|
3588
|
+
.expect("an encoded session");
|
|
3589
|
+
let first = next_notice(&mut received).await;
|
|
3590
|
+
assert!(
|
|
3591
|
+
matches!(
|
|
3592
|
+
first.event,
|
|
3593
|
+
SessionEvent::State {
|
|
3594
|
+
capture: "consented",
|
|
3595
|
+
..
|
|
3596
|
+
}
|
|
3597
|
+
),
|
|
3598
|
+
"the session is open before any offer exists: {:?}",
|
|
3599
|
+
first.event,
|
|
3600
|
+
);
|
|
3601
|
+
|
|
3602
|
+
session
|
|
3603
|
+
.feed(true, fed_keyframe())
|
|
3604
|
+
.expect("a keyframe is taken");
|
|
3605
|
+
let offered = loop {
|
|
3606
|
+
let notice = next_notice(&mut received).await;
|
|
3607
|
+
if let SessionEvent::Description { sdp, .. } = notice.event {
|
|
3608
|
+
break sdp;
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
assert!(
|
|
3612
|
+
offered.contains("H264/90000"),
|
|
3613
|
+
"the fed track is offered as H.264: {offered}"
|
|
3614
|
+
);
|
|
3615
|
+
assert!(
|
|
3616
|
+
offered.contains("profile-level-id=42c029"),
|
|
3617
|
+
"the profile-level-id comes from the fed SPS, not a default: {offered}"
|
|
3618
|
+
);
|
|
3619
|
+
assert_eq!(session.metrics().fed_frames, 1);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
#[tokio::test]
|
|
3623
|
+
async fn a_fed_stream_forwards_its_controls_instead_of_applying_them() {
|
|
3624
|
+
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3625
|
+
let session = Session::open(encoded_open(), events)
|
|
3626
|
+
.await
|
|
3627
|
+
.expect("an encoded session");
|
|
3628
|
+
session.inner.control_open.store(true, Ordering::SeqCst);
|
|
3629
|
+
|
|
3630
|
+
session.inner.apply(ControlMessage::Pointer {
|
|
3631
|
+
phase: PointerPhase::Down,
|
|
3632
|
+
x: 10,
|
|
3633
|
+
y: 20,
|
|
3634
|
+
button: 1,
|
|
3635
|
+
seq: 1,
|
|
3636
|
+
});
|
|
3637
|
+
|
|
3638
|
+
let forwarded = loop {
|
|
3639
|
+
let notice = next_notice(&mut received).await;
|
|
3640
|
+
if let SessionEvent::Input { input } = notice.event {
|
|
3641
|
+
break input;
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
assert_eq!(forwarded["kind"], "pointer");
|
|
3645
|
+
assert_eq!(forwarded["phase"], "down");
|
|
3646
|
+
assert_eq!(forwarded["x"], 10);
|
|
3647
|
+
assert_eq!(forwarded["seq"], 1);
|
|
3648
|
+
assert_eq!(session.metrics().input_forwarded, 1);
|
|
3649
|
+
assert_eq!(
|
|
3650
|
+
session.metrics().input_applied,
|
|
3651
|
+
0,
|
|
3652
|
+
"nothing was applied to this machine"
|
|
3653
|
+
);
|
|
3654
|
+
|
|
3655
|
+
// The stream's own coordinates are what travels, so an encoded surface
|
|
3656
|
+
// cannot be driven by a coordinate the far end never rendered.
|
|
3657
|
+
session.inner.apply(ControlMessage::Pointer {
|
|
3658
|
+
phase: PointerPhase::Move,
|
|
3659
|
+
x: 9_000,
|
|
3660
|
+
y: 20,
|
|
3661
|
+
button: 1,
|
|
3662
|
+
seq: 2,
|
|
3663
|
+
});
|
|
3664
|
+
assert_eq!(session.metrics().input_forwarded, 2);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
#[tokio::test]
|
|
3668
|
+
async fn a_capture_session_refuses_a_fed_stream() {
|
|
3669
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3670
|
+
let (inner, _recorded) = test_inner(events).await;
|
|
3671
|
+
let session = Session { inner };
|
|
3672
|
+
assert_eq!(
|
|
3673
|
+
session.feed(true, fed_keyframe()).unwrap_err().code,
|
|
3674
|
+
"operation",
|
|
3675
|
+
"only an encoded source takes access units",
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
#[tokio::test]
|
|
3680
|
+
async fn an_encoded_session_has_no_frame_to_read() {
|
|
3681
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3682
|
+
let session = Session::open(encoded_open(), events)
|
|
3683
|
+
.await
|
|
3684
|
+
.expect("an encoded session");
|
|
3685
|
+
assert_eq!(session.frame(None, "", None).unwrap_err().code, "operation");
|
|
3686
|
+
assert_eq!(
|
|
3687
|
+
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
3688
|
+
"operation"
|
|
3689
|
+
);
|
|
3690
|
+
assert_eq!(
|
|
3691
|
+
session.read_clipboard().await.unwrap_err(),
|
|
3692
|
+
"clipboard is unavailable for this desktop source",
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
2522
3696
|
#[tokio::test]
|
|
2523
3697
|
async fn opening_without_view_permission_is_refused_before_capture() {
|
|
2524
3698
|
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
@@ -2535,6 +3709,7 @@ mod tests {
|
|
|
2535
3709
|
ttl: None,
|
|
2536
3710
|
loopback_tcp: false,
|
|
2537
3711
|
agent_indicator: false,
|
|
3712
|
+
local_frames: true,
|
|
2538
3713
|
},
|
|
2539
3714
|
events,
|
|
2540
3715
|
)
|
|
@@ -2576,8 +3751,17 @@ mod tests {
|
|
|
2576
3751
|
);
|
|
2577
3752
|
|
|
2578
3753
|
peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
|
|
3754
|
+
let mut transport_lost = false;
|
|
2579
3755
|
tokio::time::timeout(Duration::from_secs(2), async {
|
|
2580
3756
|
while let Some(notice) = received.recv().await {
|
|
3757
|
+
// A lost path is the one revocation a new session can heal.
|
|
3758
|
+
transport_lost |= matches!(
|
|
3759
|
+
notice.event,
|
|
3760
|
+
SessionEvent::Revoked {
|
|
3761
|
+
code: "transport",
|
|
3762
|
+
..
|
|
3763
|
+
}
|
|
3764
|
+
);
|
|
2581
3765
|
if matches!(
|
|
2582
3766
|
notice.event,
|
|
2583
3767
|
SessionEvent::State {
|
|
@@ -2592,25 +3776,182 @@ mod tests {
|
|
|
2592
3776
|
})
|
|
2593
3777
|
.await
|
|
2594
3778
|
.unwrap();
|
|
3779
|
+
assert!(
|
|
3780
|
+
transport_lost,
|
|
3781
|
+
"a failed transport must revoke with code transport"
|
|
3782
|
+
);
|
|
2595
3783
|
assert!(inner.revoked_reason().is_some());
|
|
2596
3784
|
assert!(!inner.pipeline.load(Ordering::SeqCst));
|
|
2597
3785
|
assert!(inner.input.lock().unwrap().is_none());
|
|
2598
3786
|
}
|
|
2599
3787
|
|
|
3788
|
+
#[test]
|
|
3789
|
+
fn a_standing_queue_backs_the_rate_off_to_what_the_path_delivers() {
|
|
3790
|
+
let start = Instant::now();
|
|
3791
|
+
let queued = |ms: u64, delivered_kbps: u32| {
|
|
3792
|
+
Some(PathReport {
|
|
3793
|
+
queue_delay: Duration::from_millis(ms),
|
|
3794
|
+
delivered_kbps,
|
|
3795
|
+
})
|
|
3796
|
+
};
|
|
3797
|
+
let mut rate = RateControl::new(8000);
|
|
3798
|
+
// A burst's worth of queue and no loss moves nothing.
|
|
3799
|
+
assert_eq!(rate.update(&[], queued(15, 3000), start), None);
|
|
3800
|
+
// A standing queue on a 4 Mbps link: 85% of what got through, at once,
|
|
3801
|
+
// with no loss reported at all.
|
|
3802
|
+
assert_eq!(rate.update(&[], queued(80, 4000), start), Some(3400));
|
|
3803
|
+
// Not again until the back-off has had time to show in the feedback.
|
|
3804
|
+
assert_eq!(
|
|
3805
|
+
rate.update(&[], queued(80, 3400), start + Duration::from_millis(100)),
|
|
3806
|
+
None
|
|
3807
|
+
);
|
|
3808
|
+
assert_eq!(
|
|
3809
|
+
rate.update(&[], queued(60, 3400), start + Duration::from_millis(300)),
|
|
3810
|
+
Some(2890)
|
|
3811
|
+
);
|
|
3812
|
+
// A stream that sent little is not taken for a slow link: at most half.
|
|
3813
|
+
assert_eq!(
|
|
3814
|
+
rate.update(&[], queued(60, 100), start + Duration::from_millis(600)),
|
|
3815
|
+
Some(1228)
|
|
3816
|
+
);
|
|
3817
|
+
// Clean loss reports do not raise it while the queue has not drained...
|
|
3818
|
+
assert_eq!(
|
|
3819
|
+
rate.update(&[0], queued(15, 1200), start + Duration::from_secs(2)),
|
|
3820
|
+
None
|
|
3821
|
+
);
|
|
3822
|
+
// ...and do once it has.
|
|
3823
|
+
assert_eq!(
|
|
3824
|
+
rate.update(&[0], queued(2, 1200), start + Duration::from_secs(3)),
|
|
3825
|
+
Some(1390)
|
|
3826
|
+
);
|
|
3827
|
+
// Nothing drops below the floor.
|
|
3828
|
+
let mut rate = RateControl::new(8000);
|
|
3829
|
+
for step in 0..20u64 {
|
|
3830
|
+
rate.update(
|
|
3831
|
+
&[],
|
|
3832
|
+
queued(200, 1),
|
|
3833
|
+
start + DELAY_BACKOFF_EVERY * step as u32,
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
assert_eq!(rate.target, 1000);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
#[test]
|
|
3840
|
+
fn without_transport_feedback_the_rate_follows_loss_alone() {
|
|
3841
|
+
let start = Instant::now();
|
|
3842
|
+
let mut rate = RateControl::new(8000);
|
|
3843
|
+
assert_eq!(rate.update(&[], None, start), None);
|
|
3844
|
+
assert_eq!(rate.update(&[64], None, start), Some(5600));
|
|
3845
|
+
assert_eq!(
|
|
3846
|
+
rate.update(&[64], None, start + Duration::from_millis(500)),
|
|
3847
|
+
None
|
|
3848
|
+
);
|
|
3849
|
+
assert_eq!(
|
|
3850
|
+
rate.update(&[10], None, start + Duration::from_secs(1)),
|
|
3851
|
+
None
|
|
3852
|
+
);
|
|
3853
|
+
assert_eq!(
|
|
3854
|
+
rate.update(&[0], None, start + Duration::from_secs(2)),
|
|
3855
|
+
Some(6112)
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
#[test]
|
|
3860
|
+
fn a_large_frame_holds_the_next_until_the_link_has_carried_it() {
|
|
3861
|
+
let start = Instant::now();
|
|
3862
|
+
let mut budget = SendBudget::default();
|
|
3863
|
+
// Frames at the target, one per 20 ms at 4000 kbps, never wait, and
|
|
3864
|
+
// neither does the ordinary overshoot of a frame at twice the budget.
|
|
3865
|
+
for n in 0..50u32 {
|
|
3866
|
+
let at = start + Duration::from_millis(20) * n;
|
|
3867
|
+
assert_eq!(budget.wait(at, 4000), Duration::ZERO, "frame {n} waited");
|
|
3868
|
+
budget.sent(at, if n % 5 == 0 { 20_000 } else { 10_000 }, 4000);
|
|
3869
|
+
}
|
|
3870
|
+
// A 100 kB key frame drains in 160 ms (4000 kbps and a quarter): it
|
|
3871
|
+
// goes at once, and the next waits out all of it past the 60 ms burst
|
|
3872
|
+
// allowance.
|
|
3873
|
+
let at = start + Duration::from_secs(1);
|
|
3874
|
+
assert_eq!(budget.wait(at, 4000), Duration::ZERO);
|
|
3875
|
+
budget.sent(at, 100_000, 4000);
|
|
3876
|
+
let wait = budget.wait(at, 4000);
|
|
3877
|
+
assert!(
|
|
3878
|
+
(Duration::from_millis(99)..=Duration::from_millis(101)).contains(&wait),
|
|
3879
|
+
"waited {wait:?}"
|
|
3880
|
+
);
|
|
3881
|
+
assert_eq!(budget.wait(at + wait, 4000), Duration::ZERO);
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
#[test]
|
|
3885
|
+
fn motion_halves_only_when_the_encoder_sets_the_pace_and_does_not_flap() {
|
|
3886
|
+
let interval = Duration::from_millis(16);
|
|
3887
|
+
let (slow, fast) = (Duration::from_millis(30), Duration::from_millis(2));
|
|
3888
|
+
let mut size = MotionSize::default();
|
|
3889
|
+
for _ in 0..MotionSize::SLOW_RUN * 4 {
|
|
3890
|
+
size.coded(slow, false, true, interval);
|
|
3891
|
+
}
|
|
3892
|
+
assert!(
|
|
3893
|
+
!size.half,
|
|
3894
|
+
"slow small frames (typing on a busy host) keep full size"
|
|
3895
|
+
);
|
|
3896
|
+
for _ in 0..MotionSize::SLOW_RUN * 4 {
|
|
3897
|
+
size.coded(slow, true, false, interval);
|
|
3898
|
+
}
|
|
3899
|
+
assert!(
|
|
3900
|
+
!size.half,
|
|
3901
|
+
"a source slower than the cap (30 fps, 30 ms encode) never halves"
|
|
3902
|
+
);
|
|
3903
|
+
for _ in 0..MotionSize::SLOW_RUN - 1 {
|
|
3904
|
+
size.coded(slow, true, true, interval);
|
|
3905
|
+
}
|
|
3906
|
+
size.coded(fast, true, true, interval);
|
|
3907
|
+
assert!(!size.half, "a broken run of slow frames keeps full size");
|
|
3908
|
+
for _ in 0..MotionSize::SLOW_RUN {
|
|
3909
|
+
size.coded(slow, true, true, interval);
|
|
3910
|
+
}
|
|
3911
|
+
assert!(size.half, "a run of slow large frames halves");
|
|
3912
|
+
// The marginal band: half-size frames that are small but still slow,
|
|
3913
|
+
// between large ones. Neither may send it back to full size.
|
|
3914
|
+
for i in 0..MotionSize::SMALL_RUN * 10 {
|
|
3915
|
+
size.coded(slow, i % 7 == 0, true, interval);
|
|
3916
|
+
assert!(size.half, "marginal half-size motion must not flap back");
|
|
3917
|
+
}
|
|
3918
|
+
for _ in 0..MotionSize::SMALL_RUN * 2 {
|
|
3919
|
+
size.coded(fast, true, true, interval);
|
|
3920
|
+
}
|
|
3921
|
+
assert!(size.half, "large half-size frames stay half, however fast");
|
|
3922
|
+
for _ in 0..MotionSize::SMALL_RUN {
|
|
3923
|
+
size.coded(fast, false, true, interval);
|
|
3924
|
+
}
|
|
3925
|
+
assert!(
|
|
3926
|
+
!size.half,
|
|
3927
|
+
"a run of small, fast half-size frames returns to full size"
|
|
3928
|
+
);
|
|
3929
|
+
}
|
|
3930
|
+
|
|
2600
3931
|
#[tokio::test]
|
|
2601
3932
|
async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
|
|
2602
3933
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
2603
3934
|
let (inner, _recorded) = test_inner(events).await;
|
|
2604
3935
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2605
|
-
spawn_pipeline(
|
|
3936
|
+
spawn_pipeline(
|
|
3937
|
+
&inner,
|
|
3938
|
+
current_peer(&inner).expect("the test's peer"),
|
|
3939
|
+
frame_rx,
|
|
3940
|
+
Arc::new(AtomicBool::new(true)),
|
|
3941
|
+
20,
|
|
3942
|
+
1000,
|
|
3943
|
+
);
|
|
2606
3944
|
|
|
2607
3945
|
// 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
|
|
2608
3946
|
for _ in 0..40 {
|
|
2609
|
-
frame_tx.put(
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
3947
|
+
frame_tx.put(
|
|
3948
|
+
I420 {
|
|
3949
|
+
width: 64,
|
|
3950
|
+
height: 64,
|
|
3951
|
+
data: vec![128u8; 64 * 64 + 2 * 32 * 32],
|
|
3952
|
+
},
|
|
3953
|
+
Instant::now(),
|
|
3954
|
+
);
|
|
2614
3955
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
2615
3956
|
}
|
|
2616
3957
|
let motion = inner.metrics.lock().unwrap().encoded_frames;
|
|
@@ -2649,5 +3990,74 @@ mod tests {
|
|
|
2649
3990
|
"a still stream still sends"
|
|
2650
3991
|
);
|
|
2651
3992
|
assert_eq!((later.key_frames, later.refined_frames), (1, 1));
|
|
3993
|
+
assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
#[tokio::test]
|
|
3997
|
+
async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
|
|
3998
|
+
let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
|
|
3999
|
+
.await
|
|
4000
|
+
.unwrap()
|
|
4001
|
+
else {
|
|
4002
|
+
eprintln!("skipped: no H.264 encoder starts on this machine");
|
|
4003
|
+
return;
|
|
4004
|
+
};
|
|
4005
|
+
let (peer, _answer) =
|
|
4006
|
+
crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
|
|
4007
|
+
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4008
|
+
let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
|
|
4009
|
+
// The answer is real, so ICE runs and, with no candidates, never
|
|
4010
|
+
// connects; each state it reports would take back `assume_connected`.
|
|
4011
|
+
let pinned = current_peer(&inner).expect("the test's peer");
|
|
4012
|
+
let pin = tokio::spawn(async move {
|
|
4013
|
+
loop {
|
|
4014
|
+
pinned.assume_connected();
|
|
4015
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
let (frame_tx, frame_rx) = latest_frame();
|
|
4019
|
+
spawn_pipeline(
|
|
4020
|
+
&inner,
|
|
4021
|
+
current_peer(&inner).expect("the test's peer"),
|
|
4022
|
+
frame_rx,
|
|
4023
|
+
Arc::new(AtomicBool::new(true)),
|
|
4024
|
+
20,
|
|
4025
|
+
1000,
|
|
4026
|
+
);
|
|
4027
|
+
for shade in 0..10u8 {
|
|
4028
|
+
frame_tx.put(
|
|
4029
|
+
I420 {
|
|
4030
|
+
width: 320,
|
|
4031
|
+
height: 240,
|
|
4032
|
+
data: vec![shade * 20; 320 * 240 * 3 / 2],
|
|
4033
|
+
},
|
|
4034
|
+
Instant::now(),
|
|
4035
|
+
);
|
|
4036
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
4037
|
+
}
|
|
4038
|
+
// Opening a hardware session can take a good part of a second, more
|
|
4039
|
+
// with other tests holding the GPU: wait for the still to be refined.
|
|
4040
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
4041
|
+
while inner.metrics.lock().unwrap().refined_frames == 0 && Instant::now() < deadline {
|
|
4042
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
4043
|
+
}
|
|
4044
|
+
let metrics = inner.metrics.lock().unwrap().clone();
|
|
4045
|
+
assert_eq!((metrics.codec, metrics.encoder), ("h264", info.encoder));
|
|
4046
|
+
assert!(metrics.encoded_frames >= 2, "{metrics:?}");
|
|
4047
|
+
assert_eq!(
|
|
4048
|
+
metrics.key_frames, 1,
|
|
4049
|
+
"one IDR, then inter frames: {metrics:?}"
|
|
4050
|
+
);
|
|
4051
|
+
assert_eq!(metrics.refined_frames, 1, "a still desktop is refined once");
|
|
4052
|
+
assert_eq!(
|
|
4053
|
+
metrics.halved_frames, 0,
|
|
4054
|
+
"H.264 is always coded at full size"
|
|
4055
|
+
);
|
|
4056
|
+
pin.abort();
|
|
4057
|
+
inner.close("done", "closed").await;
|
|
4058
|
+
assert!(
|
|
4059
|
+
lock(&inner.encoder).is_none(),
|
|
4060
|
+
"closing releases the encoder"
|
|
4061
|
+
);
|
|
2652
4062
|
}
|
|
2653
4063
|
}
|