diff --git a/net/rtp/src/opus/pay/imp.rs b/net/rtp/src/opus/pay/imp.rs index 4b27a05b..58685f77 100644 --- a/net/rtp/src/opus/pay/imp.rs +++ b/net/rtp/src/opus/pay/imp.rs @@ -369,22 +369,28 @@ impl RtpBasePay2Impl for RtpOpusPay { if s.get::("channel-mapping-family") == Ok(0) { let peer_s = peer_caps.structure(0).unwrap(); - let pref_chans = peer_s.get::<&str>("sprop-stereo") + + gst::trace!(CAT, imp: self, "Peer preference structure: {peer_s}"); + + let pref_chans = peer_s.get::<&str>("stereo") .ok() .and_then(|params| params.trim().parse::().ok()) .map(|v| match v { 0 => 1, // mono 1 => 2, // stereo _ => { - gst::warning!(CAT, imp: self, "Unexpected sprop-stereo value {v} in input caps {s}"); + gst::warning!(CAT, imp: self, "Unexpected stereo value {v} in peer caps {s}"); 2 // default is stereo } }); if let Some(pref_chans) = pref_chans { - let mut pref_s = peer_s.to_owned(); - pref_s.set("channels", pref_chans); - let mut pref_caps = gst::Caps::builder_full().structure(pref_s).build(); + gst::trace!(CAT, imp: self, "Peer preference: channels={pref_chans}"); + + let mut pref_caps = gst::Caps::builder("audio/x-opus") + .field("channel-mapping-family", 0i32) + .field("channels", pref_chans) + .build(); pref_caps.merge(ret_caps); ret_caps = pref_caps; } diff --git a/net/rtp/src/opus/tests/tests.rs b/net/rtp/src/opus/tests/tests.rs index 2433d730..d472c982 100644 --- a/net/rtp/src/opus/tests/tests.rs +++ b/net/rtp/src/opus/tests/tests.rs @@ -9,6 +9,7 @@ // SPDX-License-Identifier: MPL-2.0 use crate::tests::{run_test_pipeline, ExpectedBuffer, ExpectedPacket, Source}; +use gst::prelude::*; use gst_check::Harness; fn init() { @@ -276,3 +277,156 @@ fn test_opus_depay_pay() { let _output_buffer = h.pull().expect("Didn't get output buffer"); } + +// test_opus_payloader_get_caps +// +// Check that a caps query on payloader sink pad reflects downstream RTP caps requirements. +// +#[test] +fn test_opus_payloader_get_caps() { + init(); + + fn get_allowed_opus_caps_for_rtp_caps_string(recv_caps_str: &str) -> gst::Caps { + let src = gst::ElementFactory::make("appsrc").build().unwrap(); + let pay = gst::ElementFactory::make("rtpopuspay2").build().unwrap(); + let sink = gst::ElementFactory::make("appsink").build().unwrap(); + + sink.set_property_from_str("caps", recv_caps_str); + + gst::Element::link_many([&src, &pay, &sink]).unwrap(); + + let pad = src.static_pad("src").unwrap(); + + pad.allowed_caps().unwrap() + } + + fn get_allowed_opus_caps_for_rtp_caps(flavour: &str, recv_props: &str) -> gst::Caps { + get_allowed_opus_caps_for_rtp_caps_string(&format!( + "application/x-rtp, encoding-name={flavour}, {recv_props}" + )) + } + + let stereo_caps = gst::Caps::builder("audio/x-opus") + .field("channels", 2i32) + .build(); + + let mono_caps = gst::Caps::builder("audio/x-opus") + .field("channels", 1i32) + .build(); + + // "stereo" is just a hint from receiver that sender can use to avoid unnecessary processing, + // but it doesn't signal a hard requirement. So both mono and stereo should always be allowed + // as input, but the channel preference should be relayed in the first caps structure. + { + let allowed_opus_caps = get_allowed_opus_caps_for_rtp_caps("OPUS", "stereo=(string)0"); + + eprintln!("OPUS stereo=0 => {:?}", allowed_opus_caps); + + assert_eq!(allowed_opus_caps.size(), 2); + + // First caps structure should have channels=1 for receiver preference stereo=0 + let s = allowed_opus_caps.structure(0).unwrap(); + assert_eq!(s.get::("channels"), Ok(1)); + + // ... but stereo should still be allowed + assert!(allowed_opus_caps.can_intersect(&stereo_caps)); + + // Make sure it's channel-mapping-family=0 + for i in 0..2 { + let s = allowed_opus_caps.structure(i).unwrap(); + assert!(s.has_name("audio/x-opus")); + assert_eq!(s.get::("channel-mapping-family"), Ok(0)); + } + } + + { + let allowed_opus_caps = get_allowed_opus_caps_for_rtp_caps("OPUS", "stereo=(string)1"); + + eprintln!("OPUS stereo=1 => {:?}", allowed_opus_caps); + + assert_eq!(allowed_opus_caps.size(), 2); + + // First caps structure should have channels=2 for receiver preference stereo=1 + let s = allowed_opus_caps.structure(0).unwrap(); + assert_eq!(s.get::("channels"), Ok(2)); + + // ... but mono should still be allowed + assert!(allowed_opus_caps.can_intersect(&mono_caps)); + + // Make sure it's channel-mapping-family=0 + for i in 0..2 { + let s = allowed_opus_caps.structure(i).unwrap(); + assert!(s.has_name("audio/x-opus")); + assert_eq!(s.get::("channel-mapping-family"), Ok(0)); + } + } + + // Make sure that with MULTIOPUS flavour the stereo hint is ignored entirely + for stereo_str in &[ + &"stereo=(string)0", + &"stereo=(string)1", + &"description=none", // no stereo attribute present + ] { + let allowed_opus_caps = get_allowed_opus_caps_for_rtp_caps("MULTIOPUS", stereo_str); + + eprintln!("MULTIOPUS {stereo_str} => {allowed_opus_caps:?}"); + + assert_eq!(allowed_opus_caps.size(), 1); + + // Neither mono nor stereo should be allowed for MULTIOPUS + let mono_stereo_caps = gst::Caps::builder("audio/x-opus") + .field("channels", gst::IntRange::new(1, 2)) + .build(); + assert!(!allowed_opus_caps.can_intersect(&mono_stereo_caps)); + + // Make sure it's channel-mapping-family=1 + let s = allowed_opus_caps.structure(0).unwrap(); + assert!(s.has_name("audio/x-opus")); + assert_eq!(s.get::("channel-mapping-family"), Ok(1)); + assert_eq!( + s.get::>("channels"), + Ok(gst::IntRange::new(3, 255)) + ); + } + + // If receiver supports both OPUS and MULTIOPUS .. + { + let allowed_opus_caps = get_allowed_opus_caps_for_rtp_caps_string( + "\ + application/x-rtp, encoding-name=OPUS, stereo=(string)0; \ + application/x-rtp, encoding-name=MULTIOPUS", + ); + + eprintln!("OPUS,stereo=0; MULTIOPUS => {allowed_opus_caps:?}"); + + // Mono should be in there of course + assert!(allowed_opus_caps.can_intersect(&mono_caps)); + + // Make sure mono is the first structure to indicate preference as per receiver caps + let s = allowed_opus_caps.structure(0).unwrap(); + assert_eq!(s.get::("channels"), Ok(1)); + + // Stereo should still be allowed, since stereo=0 is just a hint + assert!(allowed_opus_caps.can_intersect(&stereo_caps)); + + // Multichannel of course + let multich_caps = gst::Caps::builder("audio/x-opus") + .field("channel-mapping-family", 1i32) + .field("channels", gst::IntRange::new(3, 255)) + .build(); + + assert!(allowed_opus_caps.can_intersect(&multich_caps)); + + // Make sure it's channel-mapping-family=0 in the first two structs + for i in 0..3 { + let s = allowed_opus_caps.structure(i).unwrap(); + assert!(s.has_name("audio/x-opus")); + assert_eq!( + s.get::("channel-mapping-family"), + if i < 2 { Ok(0) } else { Ok(1) } + ); + } + } + + // Not testing MULTIOPUS, OPUS preference order, doesn't seem very interesting in practice. +}