From 951ef07415e2e29ed8cb4df5fdb6974ae966c3f3 Mon Sep 17 00:00:00 2001 From: Grayson Hieb Date: Fri, 24 Jul 2026 16:21:50 -0600 Subject: [PATCH 1/3] fix(http): apply headers to GStreamer sources --- subwave_appsink/src/video.rs | 9 +-- subwave_core/src/http.rs | 140 ++++++++++++++++++++++++++++++----- subwave_unified/src/video.rs | 5 +- subwave_wayland/src/video.rs | 6 +- 4 files changed, 129 insertions(+), 31 deletions(-) diff --git a/subwave_appsink/src/video.rs b/subwave_appsink/src/video.rs index 9949480..5363c13 100644 --- a/subwave_appsink/src/video.rs +++ b/subwave_appsink/src/video.rs @@ -38,7 +38,7 @@ impl AppsinkVideo { .downcast::() .map_err(|_| Error::Cast)?; - // Apply http-headers context before any state transitions + // Install HTTP source hooks before any state transitions. if let Some(h) = headers { subwave_core::http::set_http_headers_on_pipeline(&pipeline, h); } @@ -452,9 +452,7 @@ impl AppsinkVideo { self.0.get_mut().expect("lock") } - /// Set HTTP headers for HTTP-based sources via GStreamer "http-headers" context. - /// Applies the context to the underlying pipeline so that HTTP elements (e.g. souphttpsrc, - /// adaptivedemux segment fetchers) can use them for requests. + /// Set request headers on HTTP sources created by the playback pipeline. pub fn set_http_headers(&mut self, headers: &[(impl AsRef, impl AsRef)]) { let pipeline = self.get_mut().source.clone(); subwave_core::http::set_http_headers_on_pipeline(&pipeline, headers); @@ -479,9 +477,6 @@ impl Video for AppsinkVideo { (props.width, props.height) } - /// Set HTTP headers for HTTP-based sources via GStreamer "http-headers" context. - /// Applies the context to the underlying pipeline so that HTTP elements (e.g. souphttpsrc, - /// adaptivedemux segment fetchers) can use them for requests. /// Get the framerate of the video as frames per second. fn framerate(&self) -> f64 { let inner = self.read(); diff --git a/subwave_core/src/http.rs b/subwave_core/src/http.rs index 225232e..ab26215 100644 --- a/subwave_core/src/http.rs +++ b/subwave_core/src/http.rs @@ -1,34 +1,138 @@ +use std::sync::Arc; + use gstreamer as gst; use gstreamer::prelude::*; -/// Build a GStreamer `Context` of type `"http-headers"` from provided headers. -/// Returns `None` if the provided slice is empty. -pub fn build_http_headers_context, U: AsRef>( +/// Build the structure expected by HTTP sources such as `souphttpsrc`. +pub fn build_extra_headers, U: AsRef>( headers: &[(T, U)], -) -> Option { +) -> Option { if headers.is_empty() { return None; } - let mut ctx = gst::Context::new("http-headers", true); - { - let s = ctx.get_mut().unwrap().structure_mut(); - for (k, v) in headers.iter() { - s.set(k.as_ref(), v.as_ref()); - } + + let mut extra_headers = gst::Structure::new_empty("extra-headers"); + for (name, value) in headers { + extra_headers.set(name.as_ref(), value.as_ref()); + } + Some(extra_headers) +} + +fn set_headers_on_source(source: &gst::Element, headers: &[(String, String)]) -> bool { + if !source.has_property("extra-headers") { + return false; } - Some(ctx) + + let Some(extra_headers) = build_extra_headers(headers) else { + return false; + }; + source.set_property("extra-headers", extra_headers); + true } -/// Convenience helper to apply HTTP headers to a pipeline using the `http-headers` context. -/// Returns true if a context was applied. +/// Apply HTTP headers to current and future HTTP sources in a playback pipeline. +/// +/// `souphttpsrc` consumes headers through its `extra-headers` property, not a +/// `GstContext`. The `source-setup` hook configures the primary URI source, +/// while `deep-element-added` covers sources created in nested bins. pub fn set_http_headers_on_pipeline, U: AsRef>( pipeline: &gst::Pipeline, headers: &[(T, U)], ) -> bool { - if let Some(ctx) = build_http_headers_context(headers) { - pipeline.set_context(&ctx); - true - } else { - false + if headers.is_empty() { + return false; + } + + let headers = Arc::new( + headers + .iter() + .map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string())) + .collect::>(), + ); + + if gst::glib::subclass::SignalId::lookup("source-setup", pipeline.type_()).is_some() { + let source_headers = Arc::clone(&headers); + pipeline.connect("source-setup", false, move |values| { + if let Some(source) = values + .get(1) + .and_then(|value| value.get::().ok()) + { + set_headers_on_source(&source, source_headers.as_slice()); + } + None + }); + } + + let nested_headers = Arc::clone(&headers); + pipeline.connect_deep_element_added(move |_pipeline, _bin, element| { + set_headers_on_source(element, nested_headers.as_slice()); + }); + + for element in pipeline.iterate_recurse().into_iter().flatten() { + set_headers_on_source(&element, headers.as_slice()); + } + + true +} + +#[cfg(test)] +mod tests { + use super::*; + + fn source_headers(source: &gst::Element) -> gst::Structure { + source + .property::>("extra-headers") + .expect("HTTP source should have extra headers") + } + + #[test] + fn source_setup_applies_headers_to_primary_source() { + gst::init().unwrap(); + let pipeline = gst::ElementFactory::make("playbin3") + .build() + .unwrap() + .downcast::() + .unwrap(); + let source = gst::ElementFactory::make("souphttpsrc").build().unwrap(); + + assert!(set_http_headers_on_pipeline( + &pipeline, + &[("Authorization", "Bearer playback-ticket")], + )); + pipeline.emit_by_name::<()>("source-setup", &[&source]); + + assert_eq!( + source_headers(&source).get::("Authorization"), + Ok("Bearer playback-ticket".to_string()) + ); + } + + #[test] + fn nested_http_sources_receive_headers() { + gst::init().unwrap(); + let pipeline = gst::Pipeline::new(); + let bin = gst::Bin::new(); + pipeline.add(&bin).unwrap(); + + assert!(set_http_headers_on_pipeline( + &pipeline, + &[("Authorization", "Bearer playback-ticket")], + )); + + let source = gst::ElementFactory::make("souphttpsrc").build().unwrap(); + bin.add(&source).unwrap(); + + assert_eq!( + source_headers(&source).get::("Authorization"), + Ok("Bearer playback-ticket".to_string()) + ); + } + + #[test] + fn empty_headers_are_not_installed() { + gst::init().unwrap(); + let pipeline = gst::Pipeline::new(); + + assert!(!set_http_headers_on_pipeline::<&str, &str>(&pipeline, &[],)); } } diff --git a/subwave_unified/src/video.rs b/subwave_unified/src/video.rs index 91553da..7b48894 100644 --- a/subwave_unified/src/video.rs +++ b/subwave_unified/src/video.rs @@ -304,9 +304,8 @@ impl SubwaveVideo { /// Provide HTTP headers to be used by HTTP-based sources within the pipeline. /// - /// This sets a GStreamer "http-headers" context on the underlying pipeline when available. - /// For the Wayland backend where the pipeline is lazily created, headers are stored and - /// applied once the pipeline is initialized. + /// Headers are applied to HTTP sources created by the underlying playback pipeline. + /// The Wayland backend retains them until its pipeline is initialized. pub fn set_http_headers(&mut self, headers: &[(impl AsRef, impl AsRef)]) { match self { SubwaveVideo::Appsink { inner, .. } => { diff --git a/subwave_wayland/src/video.rs b/subwave_wayland/src/video.rs index bbe822c..5c21029 100644 --- a/subwave_wayland/src/video.rs +++ b/subwave_wayland/src/video.rs @@ -364,8 +364,8 @@ impl SubsurfaceVideo { Ok(SubsurfaceVideo(RwLock::new(inner))) } - /// Set HTTP headers for HTTP-based sources via GStreamer "http-headers" context. - /// If the pipeline is not yet initialized, headers are stored and applied during init. + /// Set request headers on HTTP sources created by the playback pipeline. + /// Headers are retained until the lazy Wayland pipeline is initialized. pub fn set_http_headers(&mut self, headers: &[(impl AsRef, impl AsRef)]) { // Stash a copy for later application { @@ -411,7 +411,7 @@ impl SubsurfaceVideo { subtitle_tx, )?); - // Apply any pending HTTP headers context before starting message processing + // Install pending HTTP source hooks before starting message processing. if let Some(h) = self.0.read().pending_http_headers.clone() { subwave_core::http::set_http_headers_on_pipeline(&pipeline.pipeline, h.as_slice()); } From f798a571b33e4a6caa6eba933aa4601e1fe1d442 Mon Sep 17 00:00:00 2001 From: Grayson Hieb Date: Fri, 24 Jul 2026 16:27:09 -0600 Subject: [PATCH 2/3] fix(deps): update quick-xml to 0.41.0 --- Cargo.lock | 8 ++++---- subwave_wayland/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63b75a2..33f913b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2284,9 +2284,9 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -3376,9 +3376,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", "quick-xml", diff --git a/subwave_wayland/Cargo.toml b/subwave_wayland/Cargo.toml index 905fbe5..b1f58f1 100644 --- a/subwave_wayland/Cargo.toml +++ b/subwave_wayland/Cargo.toml @@ -16,7 +16,7 @@ once_cell = "^1.19" libc = "0.2.175" tempfile = "3.21.0" ab_glyph = "0.2" -quick-xml = { version = "0.39.2", default-features = false, features = ["escape-html"] } +quick-xml = { version = "0.41.0", default-features = false, features = ["escape-html"] } [target.'cfg(target_os = "linux")'.dependencies] # Iced dependencies From c779dce30a563386712716ef4a73e677e69b2b52 Mon Sep 17 00:00:00 2001 From: Grayson Hieb Date: Fri, 24 Jul 2026 16:27:38 -0600 Subject: [PATCH 3/3] fix(ci): allow ttf-parser maintenance advisory --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 5a432d5..a2c6df3 100644 --- a/deny.toml +++ b/deny.toml @@ -75,6 +75,7 @@ ignore = [ #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, { id = "RUSTSEC-2024-0436", reason = "`paste` comes from `wgpu` transitive deps; awaiting ecosystem migration" }, + { id = "RUSTSEC-2026-0192", reason = "`ttf-parser` comes from Iced and `ab_glyph`; no safe upgrade is available while the ecosystem migrates to `skrifa`" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library.