@desklink/host 0.2.0 → 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 +78 -48
- package/bin/desklink-host.mjs +1 -1
- 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/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 +1 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +22 -9
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +150 -23
- package/engine/Cargo.lock +46 -1
- package/engine/Cargo.toml +12 -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/mac.rs +6 -3
- package/engine/src/main.rs +22 -4
- package/engine/src/peer.rs +466 -28
- package/engine/src/protocol.rs +15 -2
- package/engine/src/session.rs +790 -93
- 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,16 +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
18
|
use crate::protocol::{
|
|
17
19
|
ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
|
|
18
20
|
};
|
|
19
21
|
use crate::x11::X11Desktop;
|
|
20
|
-
use anyhow::Result;
|
|
22
|
+
use anyhow::{Context, Result};
|
|
21
23
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
22
24
|
use std::sync::{Arc, Condvar, Mutex};
|
|
23
25
|
use std::time::{Duration, Instant};
|
|
@@ -51,14 +53,41 @@ pub struct Metrics {
|
|
|
51
53
|
pub dropped_frames: u64,
|
|
52
54
|
pub encoded_frames: u64,
|
|
53
55
|
pub encoded_bytes: u64,
|
|
54
|
-
/// 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.
|
|
55
58
|
pub key_frames: u64,
|
|
56
59
|
/// Refinement passes: a still desktop re-coded once, sharp.
|
|
57
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,
|
|
58
66
|
/// Time spent in the encoder, in microseconds, over every encoded frame.
|
|
59
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,
|
|
60
84
|
/// The current rate target, after any loss back-off.
|
|
61
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,
|
|
62
91
|
pub input_applied: u64,
|
|
63
92
|
pub input_rejected: u64,
|
|
64
93
|
/// Access units the consumer fed to an encoded source.
|
|
@@ -185,6 +214,7 @@ fn select_x11(
|
|
|
185
214
|
let desktop = Arc::new(Mutex::new(X11Desktop::connect(display)?));
|
|
186
215
|
let (width, height) = {
|
|
187
216
|
let desktop = lock(&desktop);
|
|
217
|
+
eprintln!("x11 capture path: {}", desktop.capture_path());
|
|
188
218
|
desktop.screen_size()
|
|
189
219
|
};
|
|
190
220
|
let source = SelectedSource {
|
|
@@ -207,26 +237,75 @@ fn select_x11(
|
|
|
207
237
|
.spawn(move || {
|
|
208
238
|
let interval = frame_interval(max_fps);
|
|
209
239
|
let mut sequence = 0u64;
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
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();
|
|
213
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;
|
|
214
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
|
+
}
|
|
215
263
|
let started = Instant::now();
|
|
264
|
+
let grabbed = Instant::now();
|
|
216
265
|
let frame = {
|
|
217
266
|
let mut desktop = lock(&desktop);
|
|
218
267
|
desktop.capture_with_pixels(max_width, max_height)
|
|
219
268
|
};
|
|
269
|
+
let grab_micros = grabbed.elapsed().as_micros() as u64;
|
|
220
270
|
match frame {
|
|
221
271
|
Ok((frame, raw)) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
last_hash
|
|
228
|
-
|
|
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
|
+
});
|
|
229
306
|
sequence += 1;
|
|
307
|
+
last_sent = Some(started);
|
|
308
|
+
first = false;
|
|
230
309
|
}
|
|
231
310
|
}
|
|
232
311
|
Err(error) => {
|
|
@@ -239,11 +318,21 @@ fn select_x11(
|
|
|
239
318
|
on_stop(format!("X11 capture stopped: {error}"));
|
|
240
319
|
break;
|
|
241
320
|
}
|
|
321
|
+
if !hashed {
|
|
322
|
+
let elapsed = started.elapsed();
|
|
323
|
+
if elapsed < interval {
|
|
324
|
+
std::thread::sleep(interval - elapsed);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
242
327
|
}
|
|
243
328
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
|
247
336
|
}
|
|
248
337
|
}
|
|
249
338
|
})
|
|
@@ -506,6 +595,9 @@ pub struct OpenRequest {
|
|
|
506
595
|
/// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
|
|
507
596
|
pub loopback_tcp: bool,
|
|
508
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,
|
|
509
601
|
}
|
|
510
602
|
|
|
511
603
|
/// Events a session raises for its consumer.
|
|
@@ -546,8 +638,13 @@ pub enum SessionEvent {
|
|
|
546
638
|
CaptureStopped {
|
|
547
639
|
reason: String,
|
|
548
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.
|
|
549
645
|
Revoked {
|
|
550
646
|
reason: String,
|
|
647
|
+
code: &'static str,
|
|
551
648
|
},
|
|
552
649
|
}
|
|
553
650
|
|
|
@@ -579,7 +676,7 @@ struct Inner {
|
|
|
579
676
|
/// offer has to name the profile the stream's own SPS declares.
|
|
580
677
|
peer: Mutex<Option<Arc<VideoPeer>>>,
|
|
581
678
|
/// Absent for an encoded source: the consumer is the encoder.
|
|
582
|
-
encoder: Mutex<Option<
|
|
679
|
+
encoder: Mutex<Option<VideoEncoder>>,
|
|
583
680
|
input: Mutex<Option<InputTarget>>,
|
|
584
681
|
indicator: Mutex<Option<crate::indicator::Indicator>>,
|
|
585
682
|
capture: Mutex<Option<FrameSource>>,
|
|
@@ -595,6 +692,8 @@ struct Inner {
|
|
|
595
692
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
596
693
|
/// Present only for an encoded source.
|
|
597
694
|
encoded: Mutex<Option<Encoded>>,
|
|
695
|
+
/// `OpenRequest::local_frames`: whether `latest` is kept at all.
|
|
696
|
+
local_frames: bool,
|
|
598
697
|
}
|
|
599
698
|
|
|
600
699
|
/// A source whose video arrives already encoded.
|
|
@@ -675,7 +774,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
675
774
|
"cursor": "embedded",
|
|
676
775
|
"audio": false,
|
|
677
776
|
},
|
|
678
|
-
"encode":
|
|
777
|
+
"encode": encode_capabilities(),
|
|
679
778
|
"input": {
|
|
680
779
|
"mechanism": "inputtino/uinput",
|
|
681
780
|
"pointer": input,
|
|
@@ -733,7 +832,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
733
832
|
"remedy": format!("Allow {} in System Settings › Privacy & Security › Screen & System Audio Recording, then reconnect.", crate::mac::tcc_responsible_app_name())
|
|
734
833
|
}) }
|
|
735
834
|
},
|
|
736
|
-
"encode":
|
|
835
|
+
"encode": encode_capabilities(),
|
|
737
836
|
"input": {
|
|
738
837
|
"mechanism": "quartz-cgevent",
|
|
739
838
|
"pointer": input,
|
|
@@ -750,6 +849,24 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
750
849
|
})
|
|
751
850
|
}
|
|
752
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
|
+
},
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
|
|
753
870
|
impl Session {
|
|
754
871
|
pub async fn open(
|
|
755
872
|
request: OpenRequest,
|
|
@@ -832,9 +949,18 @@ impl Session {
|
|
|
832
949
|
let capture_status = Arc::new(on_capture_status);
|
|
833
950
|
let frame_events = events.clone();
|
|
834
951
|
let frame_session_id = id.clone();
|
|
835
|
-
let
|
|
836
|
-
|
|
837
|
-
|
|
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()) {
|
|
838
964
|
let unchanged =
|
|
839
965
|
held.as_ref()
|
|
840
966
|
.is_some_and(|(_, _, width, height, _, current, _)| {
|
|
@@ -865,10 +991,11 @@ impl Session {
|
|
|
865
991
|
}
|
|
866
992
|
if let Ok(mut m) = captured.lock() {
|
|
867
993
|
m.captured_frames += 1;
|
|
994
|
+
m.convert_micros += captured_at.elapsed().as_micros() as u64;
|
|
868
995
|
}
|
|
869
996
|
// An encoder that is behind gets the newest frame, not a queue: a
|
|
870
997
|
// desktop stream is live, and the last state is the one that counts.
|
|
871
|
-
if frame_tx.put(frame) {
|
|
998
|
+
if frame_tx.put(frame, captured_at) {
|
|
872
999
|
if let Ok(mut m) = captured.lock() {
|
|
873
1000
|
m.dropped_frames += 1;
|
|
874
1001
|
}
|
|
@@ -1046,6 +1173,16 @@ impl Session {
|
|
|
1046
1173
|
SessionError::new("input", format!("no keyboard layout: {error:#}"))
|
|
1047
1174
|
})?;
|
|
1048
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
|
+
};
|
|
1049
1186
|
let (peer_events_tx, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
1050
1187
|
let (peer, offer) = VideoPeer::offer(
|
|
1051
1188
|
TransportOptions {
|
|
@@ -1056,7 +1193,7 @@ impl Session {
|
|
|
1056
1193
|
pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
|
|
1057
1194
|
},
|
|
1058
1195
|
peer_events_tx,
|
|
1059
|
-
|
|
1196
|
+
codec,
|
|
1060
1197
|
)
|
|
1061
1198
|
.await
|
|
1062
1199
|
.map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
|
|
@@ -1081,7 +1218,7 @@ impl Session {
|
|
|
1081
1218
|
capture_error,
|
|
1082
1219
|
frame_changes,
|
|
1083
1220
|
peer: Mutex::new(Some(peer.clone())),
|
|
1084
|
-
encoder: Mutex::new(Some(encoder)),
|
|
1221
|
+
encoder: Mutex::new(Some(VideoEncoder::Vp9(encoder))),
|
|
1085
1222
|
input: Mutex::new(input),
|
|
1086
1223
|
indicator: Mutex::new(indicator),
|
|
1087
1224
|
capture: Mutex::new(Some(capture)),
|
|
@@ -1092,6 +1229,7 @@ impl Session {
|
|
|
1092
1229
|
pipeline: pipeline.clone(),
|
|
1093
1230
|
events: events.clone(),
|
|
1094
1231
|
encoded: Mutex::new(None),
|
|
1232
|
+
local_frames: request.local_frames,
|
|
1095
1233
|
});
|
|
1096
1234
|
|
|
1097
1235
|
inner.notify(SessionEvent::Description {
|
|
@@ -1189,6 +1327,7 @@ impl Session {
|
|
|
1189
1327
|
peer_events,
|
|
1190
1328
|
feed,
|
|
1191
1329
|
})),
|
|
1330
|
+
local_frames: false,
|
|
1192
1331
|
});
|
|
1193
1332
|
inner.notify(SessionEvent::State {
|
|
1194
1333
|
capture: "consented",
|
|
@@ -1266,6 +1405,7 @@ impl Session {
|
|
|
1266
1405
|
"this session forwards an encoded stream and keeps no frame",
|
|
1267
1406
|
));
|
|
1268
1407
|
}
|
|
1408
|
+
self.keeps_frames()?;
|
|
1269
1409
|
let mut changes = self.inner.frame_changes.clone();
|
|
1270
1410
|
let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
|
|
1271
1411
|
loop {
|
|
@@ -1302,6 +1442,16 @@ impl Session {
|
|
|
1302
1442
|
}
|
|
1303
1443
|
}
|
|
1304
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
|
+
|
|
1305
1455
|
/// Write only the requested lossless frame bytes; JSON carries metadata, never pixels.
|
|
1306
1456
|
pub fn frame(
|
|
1307
1457
|
&self,
|
|
@@ -1321,6 +1471,7 @@ impl Session {
|
|
|
1321
1471
|
"this session forwards an encoded stream and keeps no frame",
|
|
1322
1472
|
));
|
|
1323
1473
|
}
|
|
1474
|
+
self.keeps_frames()?;
|
|
1324
1475
|
if let Some(reason) = lock(&self.inner.capture_error).clone() {
|
|
1325
1476
|
return Err(SessionError::new("stream-stopped", reason));
|
|
1326
1477
|
}
|
|
@@ -1449,7 +1600,7 @@ impl Session {
|
|
|
1449
1600
|
}
|
|
1450
1601
|
|
|
1451
1602
|
pub async fn close(&self, reason: &str) {
|
|
1452
|
-
self.inner.close(reason).await;
|
|
1603
|
+
self.inner.close(reason, "closed").await;
|
|
1453
1604
|
}
|
|
1454
1605
|
}
|
|
1455
1606
|
|
|
@@ -1464,13 +1615,18 @@ impl Inner {
|
|
|
1464
1615
|
|
|
1465
1616
|
/// Stop everything and release whatever input this session held. Idempotent,
|
|
1466
1617
|
/// and the one teardown both an explicit close and a lease expiry take.
|
|
1467
|
-
async fn close(&self, reason: &str) {
|
|
1618
|
+
async fn close(&self, reason: &str, code: &str) {
|
|
1468
1619
|
if self.closed.swap(true, Ordering::SeqCst) {
|
|
1469
1620
|
return;
|
|
1470
1621
|
}
|
|
1471
1622
|
self.control_open.store(false, Ordering::SeqCst);
|
|
1472
1623
|
self.pipeline.store(false, Ordering::SeqCst);
|
|
1473
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());
|
|
1474
1630
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1475
1631
|
indicator.take();
|
|
1476
1632
|
}
|
|
@@ -1485,8 +1641,8 @@ impl Inner {
|
|
|
1485
1641
|
if let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) {
|
|
1486
1642
|
let _ = peer
|
|
1487
1643
|
.send_control(
|
|
1488
|
-
&serde_json::to_string(&ControlReply::Revoked { reason }).unwrap_or_else(
|
|
1489
|
-
|_| String::from(r#"{"kind":"revoked","reason":"closed"}"#),
|
|
1644
|
+
&serde_json::to_string(&ControlReply::Revoked { reason, code }).unwrap_or_else(
|
|
1645
|
+
|_| String::from(r#"{"kind":"revoked","reason":"closed","code":"closed"}"#),
|
|
1490
1646
|
),
|
|
1491
1647
|
)
|
|
1492
1648
|
.await;
|
|
@@ -1937,11 +2093,13 @@ fn frame_interval(max_fps: u32) -> Duration {
|
|
|
1937
2093
|
Duration::from_secs_f64(1.0 / max_fps.max(1) as f64)
|
|
1938
2094
|
}
|
|
1939
2095
|
|
|
1940
|
-
/// The newest captured frame, handed from the capture
|
|
1941
|
-
/// newer frame replaces one the encoder has not taken,
|
|
1942
|
-
/// 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.
|
|
1943
2101
|
struct FrameSlot {
|
|
1944
|
-
frame: Mutex<(Option<I420>, bool)>,
|
|
2102
|
+
frame: Mutex<(Option<(I420, Instant)>, bool)>,
|
|
1945
2103
|
ready: Condvar,
|
|
1946
2104
|
}
|
|
1947
2105
|
|
|
@@ -1958,13 +2116,13 @@ fn latest_frame() -> (FrameSender, FrameReceiver) {
|
|
|
1958
2116
|
|
|
1959
2117
|
impl FrameSender {
|
|
1960
2118
|
/// Hand over a frame; true when it replaced one the encoder never took.
|
|
1961
|
-
fn put(&self, frame: I420) -> bool {
|
|
2119
|
+
fn put(&self, frame: I420, captured: Instant) -> bool {
|
|
1962
2120
|
let mut held = self
|
|
1963
2121
|
.0
|
|
1964
2122
|
.frame
|
|
1965
2123
|
.lock()
|
|
1966
2124
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
1967
|
-
let replaced = held.0.replace(frame).is_some();
|
|
2125
|
+
let replaced = held.0.replace((frame, captured)).is_some();
|
|
1968
2126
|
self.0.ready.notify_one();
|
|
1969
2127
|
replaced
|
|
1970
2128
|
}
|
|
@@ -1984,12 +2142,22 @@ impl Drop for FrameSender {
|
|
|
1984
2142
|
}
|
|
1985
2143
|
|
|
1986
2144
|
enum Taken {
|
|
1987
|
-
Frame(I420),
|
|
2145
|
+
Frame(I420, Instant),
|
|
1988
2146
|
Timeout,
|
|
1989
2147
|
Ended,
|
|
1990
2148
|
}
|
|
1991
2149
|
|
|
1992
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
|
+
|
|
1993
2161
|
fn take(&self, wait: Duration) -> Taken {
|
|
1994
2162
|
let held = self
|
|
1995
2163
|
.0
|
|
@@ -2002,7 +2170,7 @@ impl FrameReceiver {
|
|
|
2002
2170
|
.wait_timeout_while(held, wait, |(frame, ended)| frame.is_none() && !*ended)
|
|
2003
2171
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
2004
2172
|
match held.0.take() {
|
|
2005
|
-
Some(frame) => Taken::Frame(frame),
|
|
2173
|
+
Some((frame, captured)) => Taken::Frame(frame, captured),
|
|
2006
2174
|
None if held.1 => Taken::Ended,
|
|
2007
2175
|
None => Taken::Timeout,
|
|
2008
2176
|
}
|
|
@@ -2028,12 +2196,32 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
|
|
|
2028
2196
|
(1500 + (width * height * 18 / 10_000) as u32).min(25_000)
|
|
2029
2197
|
}
|
|
2030
2198
|
|
|
2031
|
-
///
|
|
2032
|
-
///
|
|
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.
|
|
2033
2219
|
struct RateControl {
|
|
2034
2220
|
ceiling: u32,
|
|
2035
2221
|
target: u32,
|
|
2036
2222
|
last_change: Option<Instant>,
|
|
2223
|
+
/// The last queueing delay reported, while the path reports one.
|
|
2224
|
+
queue: Option<Duration>,
|
|
2037
2225
|
}
|
|
2038
2226
|
|
|
2039
2227
|
impl RateControl {
|
|
@@ -2042,26 +2230,40 @@ impl RateControl {
|
|
|
2042
2230
|
ceiling,
|
|
2043
2231
|
target: ceiling,
|
|
2044
2232
|
last_change: None,
|
|
2233
|
+
queue: None,
|
|
2045
2234
|
}
|
|
2046
2235
|
}
|
|
2047
2236
|
|
|
2048
|
-
/// The new target when these reports
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
.
|
|
2054
|
-
{
|
|
2055
|
-
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);
|
|
2056
2243
|
}
|
|
2057
|
-
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);
|
|
2058
2246
|
let floor = (self.ceiling / 8).max(500);
|
|
2059
|
-
let next = if
|
|
2060
|
-
(
|
|
2061
|
-
|
|
2062
|
-
|
|
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)
|
|
2063
2253
|
} else {
|
|
2064
|
-
|
|
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
|
+
}
|
|
2065
2267
|
};
|
|
2066
2268
|
if next == self.target {
|
|
2067
2269
|
return None;
|
|
@@ -2072,9 +2274,127 @@ impl RateControl {
|
|
|
2072
2274
|
}
|
|
2073
2275
|
}
|
|
2074
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
|
+
|
|
2075
2394
|
enum Pass {
|
|
2076
2395
|
Motion {
|
|
2077
2396
|
keyframe: bool,
|
|
2397
|
+
half: bool,
|
|
2078
2398
|
},
|
|
2079
2399
|
Refine,
|
|
2080
2400
|
/// The same picture again, so a still stream never looks stalled.
|
|
@@ -2107,8 +2427,11 @@ fn spawn_pipeline(
|
|
|
2107
2427
|
let mut rate = RateControl::new(bitrate_kbps);
|
|
2108
2428
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
2109
2429
|
m.target_kbps = bitrate_kbps;
|
|
2430
|
+
(m.codec, m.encoder) = ("vp9", "libvpx");
|
|
2110
2431
|
}
|
|
2111
|
-
|
|
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;
|
|
2112
2435
|
// `pending`: the latest frame has not been coded. `refined`: it has
|
|
2113
2436
|
// had its refinement pass, or there is nothing to refine.
|
|
2114
2437
|
let mut pending = false;
|
|
@@ -2116,11 +2439,15 @@ fn spawn_pipeline(
|
|
|
2116
2439
|
let mut keyframe = true;
|
|
2117
2440
|
let mut still_since = Instant::now();
|
|
2118
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;
|
|
2119
2448
|
loop {
|
|
2120
2449
|
let now = Instant::now();
|
|
2121
|
-
let slot =
|
|
2122
|
-
interval.saturating_sub(now.duration_since(at))
|
|
2123
|
-
});
|
|
2450
|
+
let slot = cadence.wait(now).max(budget.wait(now, rate.target));
|
|
2124
2451
|
let connected = peer.is_connected();
|
|
2125
2452
|
let wait = if !connected {
|
|
2126
2453
|
// Nothing sent before the transport connects arrives; the
|
|
@@ -2140,13 +2467,13 @@ fn spawn_pipeline(
|
|
|
2140
2467
|
.clamp(Duration::from_millis(10), Duration::from_millis(100))
|
|
2141
2468
|
};
|
|
2142
2469
|
match frame_rx.take(wait) {
|
|
2143
|
-
Taken::Frame(frame) => {
|
|
2470
|
+
Taken::Frame(frame, captured) => {
|
|
2144
2471
|
if pending {
|
|
2145
2472
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
2146
2473
|
m.dropped_frames += 1;
|
|
2147
2474
|
}
|
|
2148
2475
|
}
|
|
2149
|
-
latest = Some(frame);
|
|
2476
|
+
latest = Some((frame, captured));
|
|
2150
2477
|
pending = true;
|
|
2151
2478
|
refined = false;
|
|
2152
2479
|
still_since = Instant::now();
|
|
@@ -2161,7 +2488,8 @@ fn spawn_pipeline(
|
|
|
2161
2488
|
keyframe = true;
|
|
2162
2489
|
}
|
|
2163
2490
|
let reports = peer.take_loss_reports();
|
|
2164
|
-
|
|
2491
|
+
let path = peer.take_path_report();
|
|
2492
|
+
if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
|
|
2165
2493
|
let applied = inner.encoder.lock().map(|mut encoder| {
|
|
2166
2494
|
encoder
|
|
2167
2495
|
.as_mut()
|
|
@@ -2174,18 +2502,45 @@ fn spawn_pipeline(
|
|
|
2174
2502
|
}
|
|
2175
2503
|
}
|
|
2176
2504
|
}
|
|
2177
|
-
let Some(frame) = latest.as_ref() else {
|
|
2505
|
+
let Some((frame, captured)) = latest.as_ref() else {
|
|
2178
2506
|
continue;
|
|
2179
2507
|
};
|
|
2180
2508
|
if !connected {
|
|
2181
2509
|
continue;
|
|
2182
2510
|
}
|
|
2183
2511
|
let now = Instant::now();
|
|
2184
|
-
if
|
|
2512
|
+
if !cadence.wait(now).is_zero() || !budget.wait(now, rate.target).is_zero() {
|
|
2185
2513
|
continue;
|
|
2186
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
|
+
}
|
|
2187
2539
|
let pass = if pending || keyframe {
|
|
2188
|
-
Pass::Motion {
|
|
2540
|
+
Pass::Motion {
|
|
2541
|
+
keyframe,
|
|
2542
|
+
half: size.half && halves,
|
|
2543
|
+
}
|
|
2189
2544
|
} else if !refined && now.duration_since(still_since) >= REFINE_AFTER {
|
|
2190
2545
|
Pass::Refine
|
|
2191
2546
|
} else if refined
|
|
@@ -2206,9 +2561,9 @@ fn spawn_pipeline(
|
|
|
2206
2561
|
break;
|
|
2207
2562
|
};
|
|
2208
2563
|
match pass {
|
|
2209
|
-
Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
|
|
2564
|
+
Pass::Motion { keyframe, half } => encoder.encode(frame, keyframe, half),
|
|
2210
2565
|
Pass::Refine => encoder.refine(frame),
|
|
2211
|
-
Pass::Keepalive => encoder.encode(frame, false),
|
|
2566
|
+
Pass::Keepalive => encoder.encode(frame, false, false),
|
|
2212
2567
|
}
|
|
2213
2568
|
};
|
|
2214
2569
|
let packet = match packet {
|
|
@@ -2226,22 +2581,29 @@ fn spawn_pipeline(
|
|
|
2226
2581
|
let reason = String::from("the encoder rejected a frame");
|
|
2227
2582
|
inner.notify(SessionEvent::Revoked {
|
|
2228
2583
|
reason: reason.clone(),
|
|
2584
|
+
code: "error",
|
|
2229
2585
|
});
|
|
2230
2586
|
let target = inner.clone();
|
|
2231
2587
|
handle.spawn(async move {
|
|
2232
|
-
target.close(&reason).await;
|
|
2588
|
+
target.close(&reason, "error").await;
|
|
2233
2589
|
});
|
|
2234
2590
|
break;
|
|
2235
2591
|
}
|
|
2236
2592
|
};
|
|
2593
|
+
let took = started.elapsed();
|
|
2237
2594
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
2238
2595
|
m.encoded_frames += 1;
|
|
2239
2596
|
m.encoded_bytes += packet.data.len() as u64;
|
|
2240
|
-
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
|
+
}
|
|
2241
2603
|
match pass {
|
|
2242
|
-
Pass::Motion {
|
|
2604
|
+
Pass::Motion { half: true, .. } => m.halved_frames += 1,
|
|
2243
2605
|
Pass::Refine => m.refined_frames += 1,
|
|
2244
|
-
Pass::Motion {
|
|
2606
|
+
Pass::Motion { half: false, .. } | Pass::Keepalive => {}
|
|
2245
2607
|
}
|
|
2246
2608
|
}
|
|
2247
2609
|
match pass {
|
|
@@ -2250,13 +2612,34 @@ fn spawn_pipeline(
|
|
|
2250
2612
|
keyframe = false;
|
|
2251
2613
|
refined = false;
|
|
2252
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();
|
|
2253
2624
|
}
|
|
2254
|
-
Pass::Refine => refined = true,
|
|
2255
2625
|
Pass::Keepalive => {}
|
|
2256
2626
|
}
|
|
2257
2627
|
last_sent = Some(now);
|
|
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();
|
|
2258
2641
|
if let Err(error) =
|
|
2259
|
-
handle.block_on(peer.send_frame(&packet.data, packet.keyframe,
|
|
2642
|
+
handle.block_on(peer.send_frame(&packet.data, packet.keyframe, stamp))
|
|
2260
2643
|
{
|
|
2261
2644
|
// The transport is gone. End the session rather than leave
|
|
2262
2645
|
// the desktop captured behind a picture that stopped.
|
|
@@ -2264,18 +2647,41 @@ fn spawn_pipeline(
|
|
|
2264
2647
|
let reason = String::from("the connection to the phone was lost");
|
|
2265
2648
|
inner.notify(SessionEvent::Revoked {
|
|
2266
2649
|
reason: reason.clone(),
|
|
2650
|
+
code: "transport",
|
|
2267
2651
|
});
|
|
2268
2652
|
let target = inner.clone();
|
|
2269
2653
|
handle.spawn(async move {
|
|
2270
|
-
target.close(&reason).await;
|
|
2654
|
+
target.close(&reason, "transport").await;
|
|
2271
2655
|
});
|
|
2272
2656
|
break;
|
|
2273
2657
|
}
|
|
2658
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2659
|
+
m.send_micros += sending.elapsed().as_micros() as u64;
|
|
2660
|
+
}
|
|
2274
2661
|
}
|
|
2275
2662
|
})
|
|
2276
2663
|
.ok();
|
|
2277
2664
|
}
|
|
2278
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
|
+
|
|
2279
2685
|
/// Packetize what a consumer feeds an encoded source, building the peer on the
|
|
2280
2686
|
/// first access unit that carries an SPS.
|
|
2281
2687
|
///
|
|
@@ -2338,6 +2744,7 @@ fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUni
|
|
|
2338
2744
|
revoke(
|
|
2339
2745
|
&inner,
|
|
2340
2746
|
format!("the transport refused the encoded stream: {error:#}"),
|
|
2747
|
+
"error",
|
|
2341
2748
|
)
|
|
2342
2749
|
.await;
|
|
2343
2750
|
return;
|
|
@@ -2362,7 +2769,12 @@ fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUni
|
|
|
2362
2769
|
}
|
|
2363
2770
|
Err(error) => {
|
|
2364
2771
|
eprintln!("the video track refused a fed access unit: {error:#}");
|
|
2365
|
-
revoke(
|
|
2772
|
+
revoke(
|
|
2773
|
+
&inner,
|
|
2774
|
+
String::from("the connection to the phone was lost"),
|
|
2775
|
+
"transport",
|
|
2776
|
+
)
|
|
2777
|
+
.await;
|
|
2366
2778
|
return;
|
|
2367
2779
|
}
|
|
2368
2780
|
}
|
|
@@ -2376,11 +2788,12 @@ fn current_peer(inner: &Arc<Inner>) -> Option<Arc<VideoPeer>> {
|
|
|
2376
2788
|
|
|
2377
2789
|
/// End the session with a reason the consumer can show, exactly as the encode
|
|
2378
2790
|
/// pipeline does when its transport goes away.
|
|
2379
|
-
async fn revoke(inner: &Arc<Inner>, reason: String) {
|
|
2791
|
+
async fn revoke(inner: &Arc<Inner>, reason: String, code: &'static str) {
|
|
2380
2792
|
inner.notify(SessionEvent::Revoked {
|
|
2381
2793
|
reason: reason.clone(),
|
|
2794
|
+
code,
|
|
2382
2795
|
});
|
|
2383
|
-
inner.close(&reason).await;
|
|
2796
|
+
inner.close(&reason, code).await;
|
|
2384
2797
|
}
|
|
2385
2798
|
|
|
2386
2799
|
fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
|
|
@@ -2428,8 +2841,9 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
2428
2841
|
let reason = String::from("the connection to the phone was lost");
|
|
2429
2842
|
inner.notify(SessionEvent::Revoked {
|
|
2430
2843
|
reason: reason.clone(),
|
|
2844
|
+
code: "transport",
|
|
2431
2845
|
});
|
|
2432
|
-
inner.close(&reason).await;
|
|
2846
|
+
inner.close(&reason, "transport").await;
|
|
2433
2847
|
continue;
|
|
2434
2848
|
}
|
|
2435
2849
|
inner.notify(SessionEvent::State {
|
|
@@ -2487,10 +2901,11 @@ fn spawn_lease(inner: &Arc<Inner>, ttl: Duration) {
|
|
|
2487
2901
|
let reason = String::from("the session lease expired");
|
|
2488
2902
|
inner.notify(SessionEvent::Revoked {
|
|
2489
2903
|
reason: reason.clone(),
|
|
2904
|
+
code: "lease",
|
|
2490
2905
|
});
|
|
2491
2906
|
// An expiry ends the session exactly as an explicit close does; the
|
|
2492
2907
|
// notification above is not a substitute for stopping the desktop.
|
|
2493
|
-
inner.close(&reason).await;
|
|
2908
|
+
inner.close(&reason, "lease").await;
|
|
2494
2909
|
});
|
|
2495
2910
|
}
|
|
2496
2911
|
|
|
@@ -2575,10 +2990,11 @@ mod tests {
|
|
|
2575
2990
|
peer_events,
|
|
2576
2991
|
feed,
|
|
2577
2992
|
})),
|
|
2993
|
+
local_frames: false,
|
|
2578
2994
|
});
|
|
2579
2995
|
spawn_peer_events(&inner, peer_events_rx);
|
|
2580
2996
|
spawn_encoded(&inner, feed_rx);
|
|
2581
|
-
inner.close("test").await;
|
|
2997
|
+
inner.close("test", "closed").await;
|
|
2582
2998
|
for _ in 0..100 {
|
|
2583
2999
|
if Arc::strong_count(&inner) == 1 {
|
|
2584
3000
|
break;
|
|
@@ -2600,6 +3016,41 @@ mod tests {
|
|
|
2600
3016
|
);
|
|
2601
3017
|
}
|
|
2602
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
|
+
|
|
2603
3054
|
#[test]
|
|
2604
3055
|
fn a_small_source_is_never_upscaled() {
|
|
2605
3056
|
assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
|
|
@@ -2658,6 +3109,15 @@ mod tests {
|
|
|
2658
3109
|
)
|
|
2659
3110
|
.await
|
|
2660
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)>>>) {
|
|
2661
3121
|
// The pipeline codes nothing before the transport connects.
|
|
2662
3122
|
peer.assume_connected();
|
|
2663
3123
|
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
@@ -2684,7 +3144,9 @@ mod tests {
|
|
|
2684
3144
|
capture_error: Arc::new(Mutex::new(None)),
|
|
2685
3145
|
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2686
3146
|
peer: Mutex::new(Some(Arc::new(peer))),
|
|
2687
|
-
encoder: Mutex::new(Some(
|
|
3147
|
+
encoder: Mutex::new(Some(VideoEncoder::Vp9(
|
|
3148
|
+
Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
|
|
3149
|
+
))),
|
|
2688
3150
|
input: Mutex::new(Some(InputTarget {
|
|
2689
3151
|
applier: Applier::Recording(recorded.clone()),
|
|
2690
3152
|
held: HeldState::default(),
|
|
@@ -2702,6 +3164,7 @@ mod tests {
|
|
|
2702
3164
|
pipeline: Arc::new(AtomicBool::new(true)),
|
|
2703
3165
|
events,
|
|
2704
3166
|
encoded: Mutex::new(None),
|
|
3167
|
+
local_frames: true,
|
|
2705
3168
|
});
|
|
2706
3169
|
(inner, recorded)
|
|
2707
3170
|
}
|
|
@@ -2742,11 +3205,11 @@ mod tests {
|
|
|
2742
3205
|
matches!(
|
|
2743
3206
|
received.try_recv(),
|
|
2744
3207
|
Ok(Notice {
|
|
2745
|
-
event: SessionEvent::Revoked { .. },
|
|
3208
|
+
event: SessionEvent::Revoked { code: "lease", .. },
|
|
2746
3209
|
..
|
|
2747
3210
|
})
|
|
2748
3211
|
),
|
|
2749
|
-
"the consumer is still told
|
|
3212
|
+
"the consumer is still told the session's authority ended",
|
|
2750
3213
|
);
|
|
2751
3214
|
|
|
2752
3215
|
let rejected = inner.metrics.lock().unwrap().input_rejected;
|
|
@@ -3020,20 +3483,24 @@ mod tests {
|
|
|
3020
3483
|
|
|
3021
3484
|
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
3022
3485
|
// used to be counted and dropped behind a permanently black picture.
|
|
3023
|
-
frame_tx.put(
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
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
|
+
);
|
|
3028
3494
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
|
3029
3495
|
|
|
3030
3496
|
let mut reason = None;
|
|
3031
3497
|
while let Ok(event) = received.try_recv() {
|
|
3032
3498
|
if let Notice {
|
|
3033
|
-
event: SessionEvent::Revoked { reason: why },
|
|
3499
|
+
event: SessionEvent::Revoked { reason: why, code },
|
|
3034
3500
|
..
|
|
3035
3501
|
} = event
|
|
3036
3502
|
{
|
|
3503
|
+
assert_eq!(code, "error", "a refused frame is a fault, not a lost path");
|
|
3037
3504
|
reason = Some(why);
|
|
3038
3505
|
}
|
|
3039
3506
|
}
|
|
@@ -3074,6 +3541,7 @@ mod tests {
|
|
|
3074
3541
|
ttl: Some(Duration::from_secs(30)),
|
|
3075
3542
|
loopback_tcp: false,
|
|
3076
3543
|
agent_indicator: false,
|
|
3544
|
+
local_frames: true,
|
|
3077
3545
|
}
|
|
3078
3546
|
}
|
|
3079
3547
|
|
|
@@ -3241,6 +3709,7 @@ mod tests {
|
|
|
3241
3709
|
ttl: None,
|
|
3242
3710
|
loopback_tcp: false,
|
|
3243
3711
|
agent_indicator: false,
|
|
3712
|
+
local_frames: true,
|
|
3244
3713
|
},
|
|
3245
3714
|
events,
|
|
3246
3715
|
)
|
|
@@ -3282,8 +3751,17 @@ mod tests {
|
|
|
3282
3751
|
);
|
|
3283
3752
|
|
|
3284
3753
|
peer_tx.send(PeerEvent::State(State::Failed)).unwrap();
|
|
3754
|
+
let mut transport_lost = false;
|
|
3285
3755
|
tokio::time::timeout(Duration::from_secs(2), async {
|
|
3286
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
|
+
);
|
|
3287
3765
|
if matches!(
|
|
3288
3766
|
notice.event,
|
|
3289
3767
|
SessionEvent::State {
|
|
@@ -3298,11 +3776,158 @@ mod tests {
|
|
|
3298
3776
|
})
|
|
3299
3777
|
.await
|
|
3300
3778
|
.unwrap();
|
|
3779
|
+
assert!(
|
|
3780
|
+
transport_lost,
|
|
3781
|
+
"a failed transport must revoke with code transport"
|
|
3782
|
+
);
|
|
3301
3783
|
assert!(inner.revoked_reason().is_some());
|
|
3302
3784
|
assert!(!inner.pipeline.load(Ordering::SeqCst));
|
|
3303
3785
|
assert!(inner.input.lock().unwrap().is_none());
|
|
3304
3786
|
}
|
|
3305
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
|
+
|
|
3306
3931
|
#[tokio::test]
|
|
3307
3932
|
async fn the_pipeline_caps_the_rate_codes_the_last_frame_and_refines_a_still_desktop_once() {
|
|
3308
3933
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
@@ -3319,11 +3944,14 @@ mod tests {
|
|
|
3319
3944
|
|
|
3320
3945
|
// 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
|
|
3321
3946
|
for _ in 0..40 {
|
|
3322
|
-
frame_tx.put(
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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
|
+
);
|
|
3327
3955
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
3328
3956
|
}
|
|
3329
3957
|
let motion = inner.metrics.lock().unwrap().encoded_frames;
|
|
@@ -3362,5 +3990,74 @@ mod tests {
|
|
|
3362
3990
|
"a still stream still sends"
|
|
3363
3991
|
);
|
|
3364
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
|
+
);
|
|
3365
4062
|
}
|
|
3366
4063
|
}
|