Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* RTT as Duration #756
* Network emulation for tests #774
* Apple Crypto backend #763
* Move all crypto backends out of main crate #768
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "str0m"
version = "0.13.0"
version = "0.14.0"
authors = ["Martin Algesten <martin@algesten.se>", "Hugo Tunius <h@tunius.se>", "Davide Bertola <dade@dadeb.it>"]
description = "WebRTC library in Sans-IO style"
license = "MIT OR Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion crypto/wincrypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ windows = { version = "0.58", features = [
] }

[dev-dependencies]
str0m = { version = "0.12.0", path = "../../" }
str0m = { version = "0.13.0", path = "../../" }
56 changes: 56 additions & 0 deletions docs/SDP.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,61 @@ by str0m or the client. For media str0m creates, it refuses to allow the client
transition `Inactive` -> `RecvOnly`. All other transitions are fine, i.e. a client can change, also a
str0m created media, from `Inactive` to `SendRecv`.

## BUNDLE and Bundle Policies

str0m only supports a single ICE transport, meaning all m-lines must
be bundled together. The `a=group:BUNDLE` attribute in the SDP lists
which m-lines share transport.

### max-bundle vs max-compat

[RFC 8843][rfc8843] defines bundle policies that control how ports are set in SDP offers:

- **max-bundle**: Only the first m-line in the bundle has a real port
(typically 9). Subsequent bundled m-lines use `port=0` to indicate
they share transport with the first m-line.
- **max-compat**: All m-lines use a non-zero port (typically 9), for
backwards compatibility with endpoints that don't understand
BUNDLE.

The bundle policy is a local choice made by the offerer and is **not
explicitly stated** in the SDP. It can only be inferred by looking at
the port values on bundled m-lines.

Example max-bundle offer:

```
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 <- port 9
m=video 0 UDP/TLS/RTP/SAVPF 96 <- port 0 (bundled, NOT rejected)
```

Example max-compat offer:

```
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 <- port 9
m=video 9 UDP/TLS/RTP/SAVPF 96 <- port 9
```

### How str0m handles this

**Generating SDPs**: str0m produces max-compat style SDPs. All active
m-lines use port 9. Only truly rejected m-lines (e.g., no codec
match) use port 0.

**Receiving SDPs**: str0m accepts both styles. An m-line with `port=0`
is only considered rejected if it is NOT listed in the
`a=group:BUNDLE` attribute. If it is in the BUNDLE group, `port=0`
simply indicates the m-line shares transport with the first bundled
m-line (max-bundle style).

| Port | In BUNDLE group | Meaning |
|------|-----------------|-----------------------------------------|
| 0 | Yes | Valid bundled m-line (max-bundle style) |
| 0 | No | Rejected m-line |
| 9 | Yes | Valid bundled m-line (max-compat style) |

[quote]: https://mailarchive.ietf.org/arch/msg/mmusic/2N1_-eUTVrmciX3LpSjkjFH7oCU/
[sdpspec]: https://datatracker.ietf.org/doc/html/rfc3264#section-6.1
[rfc8843]: https://datatracker.ietf.org/doc/html/rfc8843
126 changes: 77 additions & 49 deletions src/change/sdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,9 +796,10 @@ fn apply_offer(session: &mut Session, offer: SdpOffer) -> Result<(), RtcError> {

update_session(session, &offer);

let bundle_mids = offer.bundle_mids();
let new_lines = sync_medias(session, &offer).map_err(RtcError::RemoteSdp)?;

add_new_lines(session, &new_lines, true).map_err(RtcError::RemoteSdp)?;
add_new_lines(session, &new_lines, true, bundle_mids).map_err(RtcError::RemoteSdp)?;

ensure_stream_tx(session);

Expand All @@ -814,14 +815,15 @@ fn apply_answer(

update_session(session, &answer);

let bundle_mids = answer.bundle_mids();
let new_lines = sync_medias(session, &answer).map_err(RtcError::RemoteSdp)?;

// The new_lines from the answer must correspond to what we sent in the offer.
if let Some(err) = pending.ensure_correct_answer(&new_lines) {
return Err(RtcError::RemoteSdp(err));
}

add_new_lines(session, &new_lines, false).map_err(RtcError::RemoteSdp)?;
add_new_lines(session, &new_lines, false, bundle_mids).map_err(RtcError::RemoteSdp)?;

// Add all pending changes (since we pre-allocated SSRC communicated in the Offer).
add_pending_changes(session, pending);
Expand Down Expand Up @@ -934,6 +936,7 @@ fn add_pending_changes(session: &mut Session, pending: Changes) {
/// * New m-lines are returned to the caller.
fn sync_medias<'a>(session: &mut Session, sdp: &'a Sdp) -> Result<Vec<&'a MediaLine>, String> {
let mut new_lines = Vec::with_capacity(sdp.media_lines.len());
let bundle_mids = sdp.bundle_mids();

for (idx, m) in sdp.media_lines.iter().enumerate() {
// First, match existing m-lines.
Expand All @@ -958,6 +961,7 @@ fn sync_medias<'a>(session: &mut Session, sdp: &'a Sdp) -> Result<Vec<&'a MediaL
&session.codec_config,
&session.exts,
&mut session.streams,
bundle_mids,
);

continue;
Expand All @@ -984,6 +988,7 @@ fn add_new_lines(
session: &mut Session,
new_lines: &[&MediaLine],
is_offer: bool,
bundle_mids: Option<&[Mid]>,
) -> Result<(), String> {
for m in new_lines {
let idx = session.line_count();
Expand All @@ -993,7 +998,12 @@ fn add_new_lines(

// For disabled (rejected) m-lines, don't fire open event
// and the direction will be set to Inactive by update_media.
media.need_open_event = is_offer && !m.disabled;
// In max-bundle, port=0 with the MID in BUNDLE group is NOT rejected.
let is_in_bundle = bundle_mids
.map(|mids| mids.contains(&m.mid()))
.unwrap_or(false);
let is_rejected = m.disabled && !is_in_bundle;
media.need_open_event = is_offer && !is_rejected;

// Match/remap remote params.
session
Expand All @@ -1009,6 +1019,7 @@ fn add_new_lines(
&session.codec_config,
&session.exts,
&mut session.streams,
bundle_mids,
);

session.add_media(media);
Expand Down Expand Up @@ -1067,13 +1078,23 @@ fn update_media(
config: &CodecConfig,
exts: &ExtensionMap,
streams: &mut Streams,
bundle_mids: Option<&[Mid]>,
) {
// If the m-line is disabled (port=0), clear remote_pts and set direction to Inactive.
// This handles the case where the remote rejects an m-line.
if m.disabled {
// If the m-line has port=0, it could mean:
// 1. The m-line is rejected (not in BUNDLE group)
// 2. The m-line is bundled with another m-line (max-bundle format, RFC 8843)
//
// In max-bundle, secondary m-lines use port=0 to indicate they share transport
// with the first m-line. This is NOT a rejection.
let is_in_bundle = bundle_mids
.map(|mids| mids.contains(&m.mid()))
.unwrap_or(false);
let is_rejected = m.disabled && !is_in_bundle;

if is_rejected {
if !media.disabled() {
debug!(
"Mid ({}) is rejected (port=0), setting to Inactive",
"Mid ({}) is rejected (port=0, not in BUNDLE), setting to Inactive",
media.mid()
);
}
Expand Down Expand Up @@ -1603,7 +1624,7 @@ mod test {
use sdp::SimulcastLayer as SdpSimulcastLayer;

use crate::format::Codec;
use crate::media::Simulcast;
use crate::media::{Simulcast, SimulcastLayer};
use crate::sdp::RtpMap;

use super::*;
Expand Down Expand Up @@ -1755,11 +1776,11 @@ mod test {

let mut rtc1 = Rtc::new();

let simulcast = Simulcast::builder()
.add_send_layer("h")
.add_send_layer("m")
.add_send_layer("l")
.build();
let mut simulcast = Simulcast::new();

simulcast.add_send_layer(SimulcastLayer::new("h"));
simulcast.add_send_layer(SimulcastLayer::new("m"));
simulcast.add_send_layer(SimulcastLayer::new("l"));

let mut change = rtc1.sdp_api();
change.add_media(
Expand Down Expand Up @@ -1843,42 +1864,49 @@ mod test {

let mut rtc1 = Rtc::new();

let simulcast_builder = Simulcast::builder()
// High layer
.add_send_layer_with_attributes("high")
.max_width(1280)
.max_height(720)
.max_br(1100000)
.max_br(1300000)
.max_br(1500000) // the last one wins
.max_fps(30)
.finish()
// Medium layer
.add_send_layer_with_attributes("medium")
.max_width(640)
.max_height(360)
.max_br(600000)
// No max_fps
.finish()
// Low layer
.add_send_layer_with_attributes("low")
// No max_width
.max_height(180)
.max_br(200000)
.max_fps(15)
.finish()
// Custom attribute
.add_send_layer_with_attributes("custom")
.custom("foo", "bar")
.finish();

// Make sure we can add layers one at a time, e.g. in a loop in a user's application
let simulcast = simulcast_builder
// No attributes
.add_send_layer_with_attributes("no_attrs")
.finish()
// Build the simulcast itself
.build();
let mut simulcast = Simulcast::new();

// High layer
simulcast.add_send_layer(
SimulcastLayer::new_with_attributes("high")
.max_width(1280)
.max_height(720)
.max_br(1100000)
.max_br(1300000)
.max_br(1500000) // the last one wins
.max_fps(30)
.build(),
);

// Medium layer
simulcast.add_send_layer(
SimulcastLayer::new_with_attributes("medium")
.max_width(640)
.max_height(360)
.max_br(600000)
// No max_fps
.build(),
);

// Low layer
simulcast.add_send_layer(
SimulcastLayer::new_with_attributes("low")
// No max_width
.max_height(180)
.max_br(200000)
.max_fps(15)
.build(),
);

// Custom attribute
simulcast.add_send_layer(
SimulcastLayer::new_with_attributes("custom")
.custom("foo", "bar")
.build(),
);

// No attributes
simulcast.add_send_layer(SimulcastLayer::new_with_attributes("no_attrs").build());

let mut change = rtc1.sdp_api();
change.add_media(
Expand Down
Loading
Loading