Skip to content

Dynacast: keep SVC tracks active while any quality is subscribed#1214

Open
MaxHeimbrock wants to merge 3 commits into
mainfrom
max/dynacast-svc-any-enabled
Open

Dynacast: keep SVC tracks active while any quality is subscribed#1214
MaxHeimbrock wants to merge 3 commits into
mainfrom
max/dynacast-svc-any-enabled

Conversation

@MaxHeimbrock

Copy link
Copy Markdown
Contributor

Stacked on #1213 (targets that branch; rebase to main once it merges).

What

Ports the SVC special case from client-sdk-js that #1003 didn't bring over: for SVC codecs (VP9/AV1) all spatial layers ride in a single encoded stream and the SFU selects layers server-side. When a SubscribedQualityUpdate has any quality enabled, the whole encoding must stay active, instead of matching qualities per encoding (which can deactivate the track, e.g. if the SFU enables only a quality that doesn't map to the rid-less encoding).

JS reference: setPublishingLayersForSender in livekit-clientif (isSVC) { if (qualities.some(q => q.enabled)) qualities.forEach(q => q.enabled = true); }, with isSVCCodec = vp9 || av1.

Changes

  • LocalVideoTrack::set_publishing_layers takes is_svc and applies the any-enabled rule.
  • handle_subscribed_quality_update derives is_svc from the publication's codec (VP9/AV1), mirroring isSVCCodec in JS.
  • New e2e test test_dynacast_svc (VP9, L3T3_KEY): the single SVC encoding stays active across LOW/HIGH quality requests, deactivates when the last subscriber unsubscribes, and reactivates on resubscribe.
  • SolidColorTrack::publish_with_options test helper for publishing with custom TrackPublishOptions.

Testing

All dynacast e2e tests pass against a local livekit-server --dev:

test test_dynacast ... ok
test test_dynacast_multiple_subscribers_only_publish_requested_tracks ... ok
test test_dynacast_svc ... ok

Observed SFU updates in the new test confirm the flow: vp9:[Low=true, Medium=false, High=false] on a LOW request keeps the encoding ON; [Low=false, Medium=false, High=false] after unsubscribe turns it off.

🤖 Generated with Claude Code

@MaxHeimbrock MaxHeimbrock requested a review from ladvoc as a code owner July 2, 2026 10:03
@MaxHeimbrock MaxHeimbrock force-pushed the max/dynacast-svc-any-enabled branch from b3509d2 to 00d2c3d Compare July 2, 2026 10:04
MaxHeimbrock and others added 2 commits July 2, 2026 12:05
Port the SVC special case from client-sdk-js: for SVC codecs (VP9/AV1)
all spatial layers ride in a single encoded stream and the SFU selects
layers server-side, so any enabled quality in a SubscribedQualityUpdate
must keep the whole encoding active instead of being matched per-layer.

Adds an e2e test publishing VP9 L3T3_KEY that verifies the encoding
stays active across quality requests, deactivates when the last
subscriber leaves, and reactivates on resubscribe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxHeimbrock MaxHeimbrock force-pushed the max/dynacast-svc-any-enabled branch from 00d2c3d to 114340b Compare July 2, 2026 10:07
Comment on lines +446 to +449
let sub_options = TestRoomOptions::default();

let mut rooms = test_rooms_with_options([pub_options, sub_options]).await?;
let (pub_room, _pub_events) = rooms.remove(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: You can use rooms.pop() here.

Comment thread livekit/src/room/mod.rs

let video_codec = publication.publish_options().video_codec;
// SVC codecs carry all spatial layers in one encoded stream.
let is_svc = matches!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): Consider breaking this out into a const helper function.

Base automatically changed from max/fix-dynacast-non-simulcast to main July 6, 2026 17:42

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Diagnostic log shows wrong quality label for SVC tracks with empty RID

The diagnostic log builds its layer summary using the raw encoding RID (video_quality_for_rid_or_default(&e.rid) at livekit/src/room/track/local_video_track.rs:407), but the actual matching logic maps an empty RID to "q" (Low) at line 389, so the log misleadingly reports "High" while the code matched against "Low".

Impact: Developers debugging SVC dynacast issues see incorrect quality labels in logs, making diagnosis harder.

Mechanism: raw vs corrected RID in log vs match

At livekit/src/room/track/local_video_track.rs:389, empty RIDs are remapped to "q" so the quality resolves to Low. But the log at line 407 passes &e.rid (still empty) to video_quality_for_rid_or_default, which falls through to the default High. The log output therefore shows (High)=ON when the encoding was actually matched as Low.

(Refers to line 407)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Public API inconsistency: publishing_layers() reports different quality than set_publishing_layers() for SVC tracks

The public publishing_layers() method at line 346 uses video_quality_for_rid_or_default(&e.rid) with the raw RID, which maps an empty RID to High (the default). Meanwhile, set_publishing_layers() at line 389 maps empty RID to "q"Low. This means the public API reports the SVC encoding as PublishingLayerQuality::High, while internally it's matched as Low.

The test at livekit/tests/dynacast_test.rs:481 only checks layers[0].active and doesn't verify the quality field, so this inconsistency is not caught. External consumers of publishing_layers() (e.g., the example at examples/local_video/src/publisher.rs:1677) would see High quality for an SVC encoding that the SFU addresses as Low.

(Refers to lines 336-350)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread livekit/src/room/mod.rs
Comment on lines +1933 to +1936
let is_svc = matches!(
video_codec,
crate::options::VideoCodec::VP9 | crate::options::VideoCodec::AV1
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Dynacast layer control is broken for VP9/AV1 simulcast tracks because SVC detection relies on codec type alone

The SVC flag is set based solely on codec type (matches!(video_codec, VP9 | AV1) at livekit/src/room/mod.rs:1933-1936) rather than checking whether a scalability mode was actually configured, so VP9 or AV1 tracks published with regular simulcast are misidentified as SVC.

Impact: For VP9/AV1 simulcast tracks, dynacast can never disable individual quality layers — all layers stay active whenever any subscriber is present.

Mechanism: SVC override forces all qualities enabled for misidentified simulcast tracks

VP9 and AV1 can be published in regular simulcast mode (multiple RTP encodings with RIDs, simulcast=true, no scalability_mode). This is confirmed as a valid configuration at livekit/tests/video_test.rs:46.

When is_svc is incorrectly true, the override at livekit/src/room/track/local_video_track.rs:375-383 forces every quality entry to enabled: true whenever any single quality is enabled. This means the SFU's request to disable specific simulcast layers (e.g., Medium or Low) is ignored.

The scalability_mode field is already available on TrackPublishOptions (livekit/src/room/options.rs:141) and should be used to determine SVC status, e.g.:

let is_svc = publication.publish_options().scalability_mode.is_some();
Suggested change
let is_svc = matches!(
video_codec,
crate::options::VideoCodec::VP9 | crate::options::VideoCodec::AV1
);
let is_svc = publication.publish_options().scalability_mode.is_some();
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +392 to +395
// A quality missing from the update is left untouched.
let Some(subscribed) = qualities.iter().find(|q| q.quality == quality as i32) else {
continue;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Behavioral change: missing qualities now leave encodings untouched instead of disabling them

The old code at livekit/src/room/track/local_video_track.rs:375-379 (LEFT) used .unwrap_or(false) when a quality was not found in the update, meaning any quality not explicitly listed was disabled. The new code at line 393-395 (RIGHT) uses continue to skip encodings whose quality is not in the update, leaving them in their current state.

This is a meaningful semantic change: previously, an update listing only [Low=true] would disable Medium and High; now it leaves them as-is. This matches the comment "A quality missing from the update is left untouched" and appears intentional for SVC where the SFU may send partial updates. However, it also changes behavior for simulcast tracks — if the SFU ever sends a partial quality list for simulcast, layers not mentioned will no longer be disabled. This should be verified against the SFU's actual update behavior to ensure no regression for simulcast tracks.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants