fix(streaming): keep feedback available for CQP hosts
- commit
- e2328beae680993a38c0f8a63c413114b77e831b
- parent
- 596140420f0557ae177c13dafc1d2457fbd695b0
- parent
- 4304e7120b8ac87a6366f6b59434713ea9234f7e
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | crates/ | 149 | ++++++++++++++++++++++++++---- |
| M | crates/ | 80 | +++++++++++++++- |
@@ -103,6 +103,14 @@ struct Args {
103103 /// complete left-click. Intended for libinput/Gamescope-backed proof.104104 105105 pointer_smoke: bool,106+ /// Attach the media-feedback stream to the active session, submit one107+ /// complete report, and print the host's bounded decision. Intended for108+ /// validating shadow-only encoder configurations.109+ 110+111+112+ )]113+ feedback_smoke: bool,106114 /// Prove adaptive-resolution liveness across a controlled Gamescope stall.107115 /// GATE_DIR coordinates the external SIGSTOP/SIGCONT harness.108116 @@ -114,7 +122,8 @@ struct Args {
114122 "media_v1",115123 "keyframe_smoke",116124 "slow_consumer_ms",117- "minimum_fps"125+ "minimum_fps",126+ "feedback_smoke"118127 ]119128 )]120129 resolution_stall_smoke: Option<PathBuf>,@@ -1047,7 +1056,7 @@ impl ResolutionStallGate {
10471056 }10481057 }10491058 1050-fn resolution_stall_feedback(1059+fn complete_feedback_report(10511060 report_id: u64,10521061 flags: MediaFeedbackFlags,10531062 last_sequence: u64,@@ -1094,7 +1103,7 @@ fn resolution_stall_reduced_dimensions(native: (u16, u16)) -> Result<(u16, u16)>
10941103 Ok(reduced)10951104 }10961105 1097-async fn exchange_resolution_stall_feedback(1106+async fn exchange_feedback_report(10981107 send: &mut iroh::endpoint::SendStream,10991108 recv: &mut iroh::endpoint::RecvStream,11001109 report: &MediaFeedbackReportV1,@@ -1104,23 +1113,61 @@ async fn exchange_resolution_stall_feedback(
11041113 write_media_feedback_report_v1(send, report),11051114 )11061115 .await1107- .context("timed out writing resolution-stall feedback")??;1116+ .context("timed out writing media feedback")??;11081117 let decision = tokio::time::timeout(11091118 RESOLUTION_STALL_FEEDBACK_TIMEOUT,11101119 read_adaptive_bitrate_decision_v1(recv),11111120 )11121121 .await1113- .context("timed out waiting for resolution-stall feedback decision")??1114- .context("host closed the resolution-stall feedback stream")?;1122+ .context("timed out waiting for media feedback decision")??1123+ .context("host closed the media feedback stream")?;11151124 ensure!(11161125 decision.report_id == report.report_id,1117- "resolution-stall decision report ID {} does not match report {}",1126+ "feedback decision report ID {} does not match report {}",11181127 decision.report_id,11191128 report.report_id11201129 );11211130 Ok(decision)11221131 }11231132 1133+async fn run_feedback_smoke(1134+ endpoint: &Endpoint,1135+ address: EndpointAddr,1136+ nonce: [u8; 16],1137+ session_id: u64,1138+ last_sequence: u64,1139+) -> Result<(AdaptiveBitrateDecisionV1, &'static str, Option<f64>)> {1140+ let connection = endpoint1141+ .connect(address, MEDIA_FEEDBACK_ALPN_V1)1142+ .await1143+ .context("connecting media feedback protocol")?;1144+ let (mut send, mut recv) = connection1145+ .open_bi()1146+ .await1147+ .context("opening media feedback stream")?;1148+ let negotiation = negotiate(1149+ &mut send,1150+ &mut recv,1151+ nonce,1152+ vec![Capability::VideoH264],1153+ Capability::VideoH264,1154+ "media feedback",1155+ None,1156+ )1157+ .await?;1158+ ensure!(1159+ negotiation.session_id == session_id,1160+ "media and feedback session IDs differ"1161+ );1162+1163+ let report = complete_feedback_report(1, MediaFeedbackFlags::NONE, last_sequence);1164+ let decision = exchange_feedback_report(&mut send, &mut recv, &report).await?;1165+ let (path_mode, path_rtt_ms) = selected_path_diagnostics(&connection);1166+ send.finish().context("finishing media feedback stream")?;1167+ connection.close(0_u32.into(), b"feedback probe complete");1168+ Ok((decision, path_mode, path_rtt_ms))1169+}1170+11241171 async fn next_resolution_stall_moq_frame(receiver: &mut MoqProbeReceiver) -> Result<AcceptedMedia> {11251172 loop {11261173 match receiver.next().await? {@@ -1248,18 +1295,18 @@ async fn run_resolution_stall_smoke(
12481295 let reduced_dimensions = resolution_stall_reduced_dimensions(native_dimensions)?;12491296 12501297 let mut report_id = 1_u64;1251- let seed = resolution_stall_feedback(report_id, MediaFeedbackFlags::NONE, initial.sequence);1252- let _ = exchange_resolution_stall_feedback(feedback_send, feedback_recv, &seed).await?;1298+ let seed = complete_feedback_report(report_id, MediaFeedbackFlags::NONE, initial.sequence);1299+ let _ = exchange_feedback_report(feedback_send, feedback_recv, &seed).await?;12531300 tokio::time::sleep(RESOLUTION_STALL_FEEDBACK_INTERVAL).await;12541301 12551302 report_id += 1;1256- let pressure = resolution_stall_feedback(1303+ let pressure = complete_feedback_report(12571304 report_id,12581305 MediaFeedbackFlags::RESYNC_ACTIVE,12591306 initial.sequence,12601307 );12611308 let pressure_decision =1262- exchange_resolution_stall_feedback(feedback_send, feedback_recv, &pressure).await?;1309+ exchange_feedback_report(feedback_send, feedback_recv, &pressure).await?;12631310 ensure!(12641311 pressure_decision12651312 .reasons@@ -1311,8 +1358,8 @@ async fn run_resolution_stall_smoke(
13111358 wait_for_resolution_stall_marker(&gate.stalled, timeout).await?;13121359 13131360 report_id += 1;1314- let reseed = resolution_stall_feedback(report_id, MediaFeedbackFlags::NONE, reduced.sequence);1315- let _ = exchange_resolution_stall_feedback(feedback_send, feedback_recv, &reseed).await?;1361+ let reseed = complete_feedback_report(report_id, MediaFeedbackFlags::NONE, reduced.sequence);1362+ let _ = exchange_feedback_report(feedback_send, feedback_recv, &reseed).await?;13161363 let mut last_feedback_at = Instant::now();13171364 let mut fresh_started = None;13181365 let mut stall_input_ack_micros = None;@@ -1323,11 +1370,10 @@ async fn run_resolution_stall_smoke(
13231370 .await;13241371 report_id += 1;13251372 let report =1326- resolution_stall_feedback(report_id, MediaFeedbackFlags::NONE, reduced.sequence);1373+ complete_feedback_report(report_id, MediaFeedbackFlags::NONE, reduced.sequence);13271374 let sent_at = Instant::now();13281375 fresh_started.get_or_insert(sent_at);1329- let decision =1330- exchange_resolution_stall_feedback(feedback_send, feedback_recv, &report).await?;1376+ let decision = exchange_feedback_report(feedback_send, feedback_recv, &report).await?;13311377 ensure!(13321378 decision.state != AdaptiveBitrateStateV1::Increase,13331379 "no-progress feedback unexpectedly increased adaptive bitrate"@@ -2189,6 +2235,20 @@ async fn main() -> Result<()> {
21892235 "probe sustained only {accepted_fps:.3} accepted fps; required at least {minimum_fps:.3} fps"21902236 );21912237 }2238+ let feedback_evidence = if args.feedback_smoke {2239+ Some(2240+ run_feedback_smoke(2241+ &endpoint,2242+ address.clone(),2243+ nonce,2244+ session_id,2245+ last_sequence.context("feedback smoke requires an accepted media sequence")?,2246+ )2247+ .await?,2248+ )2249+ } else {2250+ None2251+ };21922252 let media_diagnostics_connection = moq_receiver21932253 .as_ref()21942254 .map_or(&media_connection, MoqProbeReceiver::connection);@@ -2201,12 +2261,16 @@ async fn main() -> Result<()> {
22012261 ("not-used", None)22022262 };22032263 let (input_path_mode, input_path_rtt_ms) = selected_path_diagnostics(&input_connection);2264+ let feedback_path_mode = feedback_evidence2265+ .as_ref()2266+ .map_or("not-used", |(_, path_mode, _)| *path_mode);22042267 if args.relay_only {22052268 ensure!(22062269 control_path_mode == "relay"22072270 && media_path_mode == "relay"2208- && input_path_mode == "relay",2209- "relay-only probe did not keep every transport on relay: control={control_path_mode}, media={media_path_mode}, input={input_path_mode}"2271+ && input_path_mode == "relay"2272+ && (feedback_evidence.is_none() || feedback_path_mode == "relay"),2273+ "relay-only probe did not keep every transport on relay: control={control_path_mode}, media={media_path_mode}, input={input_path_mode}, feedback={feedback_path_mode}"22102274 );22112275 }22122276 let (@@ -2397,6 +2461,34 @@ async fn main() -> Result<()> {
23972461 "not-requested"23982462 }23992463 );2464+ if let Some((decision, path_mode, path_rtt_ms)) = &feedback_evidence {2465+ println!("feedback_smoke=ok");2466+ println!("feedback_report_id={}", decision.report_id);2467+ println!("feedback_state={:?}", decision.state);2468+ println!("feedback_target_kbps={}", decision.target_kbps);2469+ println!("feedback_floor_kbps={}", decision.floor_kbps);2470+ println!("feedback_ceiling_kbps={}", decision.ceiling_kbps);2471+ println!("feedback_applied={}", decision.applied);2472+ println!("feedback_path_mode={path_mode}");2473+ match path_rtt_ms {2474+ Some(rtt) => println!("feedback_path_rtt_ms={rtt:.3}"),2475+ None => println!("feedback_path_rtt_ms=unknown"),2476+ }2477+ } else {2478+ for field in [2479+ "feedback_smoke",2480+ "feedback_report_id",2481+ "feedback_state",2482+ "feedback_target_kbps",2483+ "feedback_floor_kbps",2484+ "feedback_ceiling_kbps",2485+ "feedback_applied",2486+ "feedback_path_mode",2487+ "feedback_path_rtt_ms",2488+ ] {2489+ println!("{field}=not-requested");2490+ }2491+ }24002492 println!("path_mode={media_path_mode}");24012493 match media_path_rtt_ms {24022494 Some(rtt) => println!("path_rtt_ms={rtt:.3}"),@@ -2681,8 +2773,8 @@ mod tests {
26812773 use super::*;26822774 26832775 2684- fn resolution_stall_feedback_is_complete_and_keeps_the_observed_sequence() {2685- let report = resolution_stall_feedback(7, MediaFeedbackFlags::NONE, 42);2776+ fn feedback_report_is_complete_and_keeps_the_observed_sequence() {2777+ let report = complete_feedback_report(7, MediaFeedbackFlags::NONE, 42);26862778 report.validate().unwrap();26872779 assert_eq!(report.report_id, 7);26882780 assert_eq!(report.interval_ms, 1_000);@@ -2694,7 +2786,7 @@ mod tests {
26942786 assert_eq!(report.decode_p95_ms, Some(3));26952787 assert_eq!(report.presentation_p95_ms, Some(5));26962788 2697- let later = resolution_stall_feedback(8, MediaFeedbackFlags::NONE, 42);2789+ let later = complete_feedback_report(8, MediaFeedbackFlags::NONE, 42);26982790 assert_eq!(later.last_sequence, report.last_sequence);26992791 }27002792 @@ -3372,6 +3464,7 @@ mod tests {
33723464 assert_eq!(default.identity, None);33733465 assert_eq!(default.invitation, None);33743466 assert_eq!(default.expect_size, None);3467+ assert!(!default.feedback_smoke);33753468 let strict = Args::try_parse_from([33763469 "sigil-probe",33773470 "--node-id",@@ -3381,6 +3474,10 @@ mod tests {
33813474 ])33823475 .unwrap();33833476 assert_eq!(strict.expect_size, Some((2_560, 1_600)));3477+ let feedback =3478+ Args::try_parse_from(["sigil-probe", "--node-id", &node_id, "--feedback-smoke"])3479+ .unwrap();3480+ assert!(feedback.feedback_smoke);33843481 for flags in [33853482 ["--media-v1", "--media-v2"],33863483 ["--media-v1", "--media-v3"],@@ -3391,5 +3488,15 @@ mod tests {
33913488 .is_err()33923489 );33933490 }3491+ assert!(3492+ Args::try_parse_from([3493+ "sigil-probe",3494+ "--node-id",3495+ &node_id,3496+ "--feedback-smoke",3497+ "--media-v3",3498+ ])3499+ .is_err()3500+ );33943501 }33953502 }@@ -72,7 +72,7 @@ const SOURCE_REAP_GRACE_TIMEOUT: Duration = Duration::from_secs(1);
7272 const MOQ_ATTACHMENT_TIMEOUT: Duration = Duration::from_secs(10);7373 const MOQ_REJECT_CODE: u32 = 0x534d;7474 const ADAPTIVE_BITRATE_FLOOR_KBPS: u32 = 1_000;75-const TEST_PATTERN_BITRATE_CEILING_KBPS: u32 = 12_000;75+const SHADOW_BITRATE_CEILING_KBPS: u32 = 12_000;7676 const ADAPTIVE_BITRATE_QUANTUM_KBPS: u32 = 250;7777 const ADAPTIVE_BITRATE_CLEAN_WINDOWS: u8 = 10;7878 const ADAPTIVE_BITRATE_MODERATE_WINDOWS: u8 = 2;@@ -3532,12 +3532,19 @@ where
35323532 35333533 fn adaptive_bitrate_ceiling_kbps(config: &HostConfig) -> Result<u32> {35343534 let configured = match config.source {3535- VideoSource::TestPattern => TEST_PATTERN_BITRATE_CEILING_KBPS,3536- VideoSource::GamescopePipewire => config3537- .gamescope_pipewire3538- .as_ref()3539- .and_then(|gamescope| gamescope.bitrate_kbps)3540- .context("adaptive bitrate shadow mode requires a Gamescope CBR bitrate")?,3535+ VideoSource::TestPattern => SHADOW_BITRATE_CEILING_KBPS,3536+ VideoSource::GamescopePipewire => {3537+ let gamescope = config3538+ .gamescope_pipewire3539+ .as_ref()3540+ .context("Gamescope feedback requires gamescope_pipewire configuration")?;3541+ match gamescope.rate_control {3542+ VaapiRateControl::Cbr => gamescope3543+ .bitrate_kbps3544+ .context("adaptive bitrate shadow mode requires a Gamescope CBR bitrate")?,3545+ VaapiRateControl::Cqp => SHADOW_BITRATE_CEILING_KBPS,3546+ }3547+ }35413548 };35423549 let ceiling = quantize_down(configured);35433550 ensure!(@@ -5927,6 +5934,65 @@ mod tests {
59275934 }59285935 }59295936 5937+ fn gamescope_feedback_test_config(rate_control: VaapiRateControl) -> HostConfig {5938+ let (bitrate_kbps, quantizer) = match rate_control {5939+ VaapiRateControl::Cbr => (Some(12_123), None),5940+ VaapiRateControl::Cqp => (None, Some(24)),5941+ };5942+ let mut config = moq_test_config();5943+ config.source = VideoSource::GamescopePipewire;5944+ config.width = None;5945+ config.height = None;5946+ config.gamescope_pipewire = Some(crate::config::GamescopePipewireConfig {5947+ node_name: "gamescope".into(),5948+ media_class: "Video/Source".into(),5949+ match_properties: std::collections::BTreeMap::new(),5950+ xwayland_display: None,5951+ pw_dump_path: "/usr/bin/pw-dump".into(),5952+ gst_launch_path: "/usr/bin/gst-launch-1.0".into(),5953+ gst_inspect_path: "/usr/bin/gst-inspect-1.0".into(),5954+ encoder_backend: GamescopeEncoderBackend::ExternalGstLaunch,5955+ vaapi_encoder: "vah264enc".into(),5956+ vaapi_render_node: "/dev/dri/renderD128".into(),5957+ rate_control,5958+ bitrate_kbps,5959+ quantizer,5960+ });5961+ config.validate().unwrap();5962+ config5963+ }5964+5965+ 5966+ fn external_cqp_uses_a_fixed_shadow_bitrate_ceiling() {5967+ let config = gamescope_feedback_test_config(VaapiRateControl::Cqp);5968+5969+ assert_eq!(5970+ adaptive_bitrate_ceiling_kbps(&config).unwrap(),5971+ SHADOW_BITRATE_CEILING_KBPS5972+ );5973+ assert!(!adaptive_bitrate_actuation_enabled(&config));5974+ }5975+5976+ 5977+ fn gamescope_cbr_keeps_its_quantized_configured_ceiling() {5978+ let config = gamescope_feedback_test_config(VaapiRateControl::Cbr);5979+5980+ assert_eq!(adaptive_bitrate_ceiling_kbps(&config).unwrap(), 12_000);5981+ }5982+5983+ 5984+ fn malformed_cbr_without_a_bitrate_still_fails_closed() {5985+ let mut config = gamescope_feedback_test_config(VaapiRateControl::Cbr);5986+ config.gamescope_pipewire.as_mut().unwrap().bitrate_kbps = None;5987+5988+ assert_eq!(5989+ adaptive_bitrate_ceiling_kbps(&config)5990+ .unwrap_err()5991+ .to_string(),5992+ "adaptive bitrate shadow mode requires a Gamescope CBR bitrate"5993+ );5994+ }5995+59305996 59315997 async fn upstream_moq_groups_are_sequential_and_cancel_the_superseded_gop() {59325998 let track_info = Track {