Skip to content

Commit a46d057

Browse files
authored
feat: add stereo to opus format params
1 parent eff683e commit a46d057

4 files changed

Lines changed: 102 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
3+
* Add stereo to opus format params #955
4+
15
# 0.19.0
26

37
* Expose H265 some already referenced profile structs #948

src/format/format_params.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ pub struct FormatParams {
1111
/// The minimum duration of media represented by a packet.
1212
pub min_p_time: Option<u8>,
1313

14+
/// Opus specific parameter.
15+
///
16+
/// Decoder-side preference for stereo audio. Default is mono per
17+
/// RFC 7587 Section 7.1: <https://datatracker.ietf.org/doc/html/rfc7587#section-7.1>
18+
pub stereo: Option<bool>,
19+
20+
/// Opus specific parameter.
21+
///
22+
/// Sender-side stereo hint for Opus. When set, it signals the intent to
23+
/// transmit stereo from the sender, matching the symmetric m-line model.
24+
pub sprop_stereo: Option<bool>,
25+
1426
/// Opus specific parameter.
1527
///
1628
/// Specifies that the decoder can do Opus in-band FEC
@@ -117,6 +129,8 @@ impl FormatParams {
117129
use FormatParam::*;
118130
match param {
119131
MinPTime(v) => self.min_p_time = Some(*v),
132+
Stereo(v) => self.stereo = Some(*v),
133+
SpropStereo(v) => self.sprop_stereo = Some(*v),
120134
UseInbandFec(v) => self.use_inband_fec = Some(*v),
121135
UseDtx(v) => self.use_dtx = Some(*v),
122136
LevelAsymmetryAllowed(v) => self.level_asymmetry_allowed = Some(*v),
@@ -140,6 +154,12 @@ impl FormatParams {
140154
if let Some(v) = self.min_p_time {
141155
r.push(MinPTime(v));
142156
}
157+
if let Some(v) = self.stereo {
158+
r.push(Stereo(v));
159+
}
160+
if let Some(v) = self.sprop_stereo {
161+
r.push(SpropStereo(v));
162+
}
143163
if let Some(v) = self.use_inband_fec {
144164
r.push(UseInbandFec(v));
145165
}
@@ -212,6 +232,20 @@ mod test {
212232
assert_eq!(parsed.sprop_max_don_diff, Some(32));
213233
}
214234

235+
#[test]
236+
fn sprop_stereo_roundtrip() {
237+
let f = FormatParams {
238+
sprop_stereo: Some(true),
239+
..Default::default()
240+
};
241+
242+
let fmtp_str = f.to_string();
243+
assert_eq!(fmtp_str, "sprop-stereo=1");
244+
245+
let parsed = FormatParams::parse_line(&fmtp_str);
246+
assert_eq!(parsed.sprop_stereo, Some(true));
247+
}
248+
215249
#[test]
216250
fn h265_combined_params_roundtrip() {
217251
use crate::packet::H265ProfileTierLevel;

src/format/payload_params.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,20 @@ impl PayloadParams {
341341
score = score.saturating_sub(4);
342342
}
343343

344+
// If neither value is specified both sides should assume mono for stereo
345+
// and the sprop-stereo hint defaults to false.
346+
let c0_stereo = c0.format.stereo.unwrap_or(false);
347+
let c1_stereo = c1.format.stereo.unwrap_or(false);
348+
if c0_stereo != c1_stereo {
349+
score = score.saturating_sub(8);
350+
}
351+
352+
let c0_sprop_stereo = c0.format.sprop_stereo.unwrap_or(false);
353+
let c1_sprop_stereo = c1.format.sprop_stereo.unwrap_or(false);
354+
if c0_sprop_stereo != c1_sprop_stereo {
355+
score = score.saturating_sub(8);
356+
}
357+
344358
score
345359
}
346360

src/sdp/data.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,14 @@ pub enum FormatParam {
907907
/// Default 3. Max 120.
908908
MinPTime(u8),
909909

910+
/// Decoder-side preference for stereo audio. Default is mono per
911+
/// RFC 7587 Section 7.1: <https://datatracker.ietf.org/doc/html/rfc7587#section-7.1>
912+
Stereo(bool),
913+
914+
/// Sender-side stereo hint for Opus. This is the symmetric m-line companion
915+
/// to `stereo` and signals the sender's intention to transmit stereo.
916+
SpropStereo(bool),
917+
910918
/// Specifies that the decoder can do Opus in-band FEC
911919
UseInbandFec(bool),
912920

@@ -965,6 +973,8 @@ impl FormatParam {
965973
"minptime" => v.parse().map(MinPTime).ok(),
966974
"useinbandfec" => Some(UseInbandFec(v == "1")),
967975
"usedtx" => Some(UseDtx(v == "1")),
976+
"stereo" => Some(Stereo(v == "1")),
977+
"sprop-stereo" => Some(SpropStereo(v == "1")),
968978
"level-asymmetry-allowed" => Some(LevelAsymmetryAllowed(v == "1")),
969979
"packetization-mode" => v.parse().map(PacketizationMode).ok(),
970980
"profile-level-id" => u32::from_str_radix(v, 16)
@@ -1031,6 +1041,8 @@ impl fmt::Display for FormatParam {
10311041
MinPTime(v) => write!(f, "minptime={v}"),
10321042
UseInbandFec(v) => write!(f, "useinbandfec={}", i32::from(*v)),
10331043
UseDtx(v) => write!(f, "usedtx={}", i32::from(*v)),
1044+
Stereo(v) => write!(f, "stereo={}", i32::from(*v)),
1045+
SpropStereo(v) => write!(f, "sprop-stereo={}", i32::from(*v)),
10341046
LevelAsymmetryAllowed(v) => {
10351047
write!(f, "level-asymmetry-allowed={}", i32::from(*v))
10361048
}
@@ -1514,10 +1526,11 @@ mod test {
15141526
fn fmtp_param_to_string() {
15151527
let f = FormatParams {
15161528
min_p_time: Some(10),
1529+
stereo: Some(true),
15171530
use_inband_fec: Some(true),
15181531
..Default::default()
15191532
};
1520-
assert_eq!(f.to_string(), "minptime=10;useinbandfec=1");
1533+
assert_eq!(f.to_string(), "minptime=10;stereo=1;useinbandfec=1");
15211534
}
15221535
}
15231536

@@ -1845,6 +1858,42 @@ f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
18451858
}
18461859
}
18471860

1861+
/// Opus codec-specific tests.
1862+
/// These tests are mainly verify SDP format params.
1863+
mod opus_codec {
1864+
use super::*;
1865+
1866+
#[test]
1867+
fn opus_fmtp_round_trip() {
1868+
let f = FormatParams {
1869+
use_dtx: Some(true),
1870+
stereo: Some(true),
1871+
sprop_stereo: Some(true),
1872+
..Default::default()
1873+
};
1874+
1875+
let fmtp_str = f.to_string();
1876+
assert_eq!(fmtp_str, "stereo=1;sprop-stereo=1;usedtx=1");
1877+
1878+
let parsed = FormatParams::parse_line(&fmtp_str);
1879+
assert_eq!(parsed, f);
1880+
}
1881+
1882+
#[test]
1883+
fn opus_default_to_mono() {
1884+
let f = FormatParams {
1885+
..Default::default()
1886+
};
1887+
let fmtp_str = f.to_string();
1888+
assert_eq!(fmtp_str, "");
1889+
1890+
let parsed = FormatParams::parse_line(&fmtp_str);
1891+
assert!(parsed.stereo.is_none());
1892+
assert_eq!(parsed.stereo.unwrap_or(false), false);
1893+
assert_eq!(parsed.sprop_stereo.unwrap_or(false), false);
1894+
}
1895+
}
1896+
18481897
/// Core H.265 (HEVC) codec-specific tests.
18491898
/// Tests basic H.265 parameter handling including profile-tier-level parsing,
18501899
/// serialization, and format parameter combinations.

0 commit comments

Comments
 (0)