Skip to content

Change default degradation preference by video source - #991

Merged
xianshijing-lk merged 3 commits into
mainfrom
sxian/CLT-3068/change_default_degrade_preference_mode_based_on_video_source
Aug 7, 2026
Merged

Change default degradation preference by video source#991
xianshijing-lk merged 3 commits into
mainfrom
sxian/CLT-3068/change_default_degrade_preference_mode_based_on_video_source

Conversation

@xianshijing-lk

Copy link
Copy Markdown
Contributor

Summary

  • Default video degradation preference by track source when degradationPreference is not explicitly set.
  • Camera tracks now default to MAINTAIN_FRAMERATE.
  • Screen share tracks now default to MAINTAIN_RESOLUTION.
  • Other video sources default to BALANCED.
  • Preserve explicit degradationPreference overrides.
  • Add tests for camera and screen share defaults.

Testing

  • git diff --check
  • Focused Gradle test attempted, but compile failed due existing generated protocol reference issues (AGENT_ERROR, PUBLISH_DATA_TRACK_RESPONSE, clientProtocol, etc.).

@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 85a76ee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
client-sdk-android Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@adrian-niculescu @davidliu , I broke down default degradation preference changes from #973 to keep the discussion more focused.

Please review this PR.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Diffuse output:

OLD: diffuse-source-file
NEW: livekit-android-sdk-release.aar

 AAR      │ old      │ new      │ diff      
──────────┼──────────┼──────────┼───────────
      jar │  2.7 MiB │  2.7 MiB │ +16.7 KiB 
 manifest │  1.5 KiB │  1.5 KiB │       0 B 
 lint-jar │ 12.7 KiB │ 12.7 KiB │       0 B 
    other │  1.9 KiB │  1.9 KiB │       0 B 
──────────┼──────────┼──────────┼───────────
    total │  2.7 MiB │  2.7 MiB │ +16.7 KiB 

 JAR     │ old   │ new   │ diff          
─────────┼───────┼───────┼───────────────
 classes │  1504 │  1512 │  +8 (+10 -2)  
 methods │ 20222 │ 20271 │ +49 (+66 -17) 
  fields │  5166 │  5212 │ +46 (+52 -6)
AAR
 size    │ diff      │ path          
─────────┼───────────┼───────────────
 2.7 MiB │ +16.7 KiB │ ∆ classes.jar 
─────────┼───────────┼───────────────
 2.7 MiB │ +16.7 KiB │ (total)
JAR
CLASSES:

   old  │ new  │ diff        
  ──────┼──────┼─────────────
   1504 │ 1512 │ +8 (+10 -2) 
  
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_1
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1
  + io.livekit.android.room.SenderTransceiverHandle
  + io.livekit.android.room.SignalSessionState
  + io.livekit.android.room.participant.LocalParticipant_publishTrackImpl_negotiate_createdTransceiver_1
  + io.livekit.android.room.participant.LocalParticipantKt_WhenMappings
  + io.livekit.android.token.DevelopmentTokenServerOptions
  + io.livekit.android.token.DevelopmentTokenSource
  
  - io.livekit.android.room.RTCEngine_createSenderTransceiver_2
  - io.livekit.android.token.SandboxTokenSource
  

METHODS:

   old   │ new   │ diff          
  ───────┼───────┼───────────────
   20222 │ 20271 │ +49 (+66 -17) 
  
  + io.livekit.android.room.RTCEngine access_getSignalSessionState_p(RTCEngine) → SignalSessionState
  + io.livekit.android.room.RTCEngine access_setSignalSessionState_p(RTCEngine, SignalSessionState)
  + io.livekit.android.room.RTCEngine endSignalSession()
  + io.livekit.android.room.RTCEngine rollbackSenderTransceiver_livekit_android_sdk_release(SenderTransceiverHandle, boolean) → boolean
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_1 <init>(RTCEngine, Continuation)
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_1 invokeSuspend(Object) → Object
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1 <init>(MediaStreamTrack, RtpTransceiver_RtpTransceiverInit, Continuation)
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1 create(Object, Continuation) → Continuation
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1 invoke(Object, Object) → Object
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1 invoke(PeerConnection, Continuation) → Object
  + io.livekit.android.room.RTCEngine_createSenderTransceiver_transceiver_1 invokeSuspend(Object) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1 <init>(PeerConnectionTransport, RTCEngine, SenderTransceiverHandle, boolean, Continuation)
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1 create(Object, Continuation) → Continuation
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1 invoke(Object, Object) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1 invoke(CoroutineScope, Continuation) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1 invokeSuspend(Object) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1 <init>(RTCEngine, PeerConnectionTransport, SenderTransceiverHandle, boolean, Continuation)
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1 create(Object, Continuation) → Continuation
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1 invoke(Object, Object) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1 invoke(PeerConnection, Continuation) → Object
  + io.livekit.android.room.RTCEngine_rollbackSenderTransceiver_1_1 invokeSuspend(Object) → Object
  + io.livekit.android.room.SenderTransceiverHandle <init>(PeerConnectionTransport, RtpTransceiver, SignalSessionState)
  + io.livekit.android.room.SenderTransceiverHandle getPublisher_livekit_android_sdk_release() → PeerConnectionTransport
  + io.livekit.android.room.SenderTransceiverHandle getSignalSessionState_livekit_android_sdk_release() → SignalSessionState
  + io.livekit.android.room.SenderTransceiverHandle getTransceiver_livekit_android_sdk_release() → RtpTransceiver
  + io.livekit.android.room.SignalSessionState <init>(boolean)
  + io.livekit.android.room.SignalSessionState getEnded() → boolean
  + io.livekit.android.room.participant.LocalParticipant access_publishTrackImpl_negotiate(LocalParticipant, Ref_ObjectRef, Track, Ref_ObjectRef, Ref_ObjectRef, String, Track_Source, Ref_ObjectRef, LocalParticipant_PublishListener, Continuation) → Object
  + io.livekit.android.room.participant.LocalParticipant access_setTrackEnabled_lambda_7_notifyStopped(AtomicBoolean, ScreenCaptureParams)
  + io.livekit.android.room.participant.LocalParticipant publishTrackImpl_negotiate(LocalParticipant, Ref_ObjectRef, Track, Ref_ObjectRef, Ref_Obje
...✂

@adrian-niculescu adrian-niculescu left a comment

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.

The backup codec sender never gets the preference.

publishAdditionalCodecForTrack creates a second transceiver over the same rtcTrack and never touches sender.parameters, so that sender keeps resolving implicitly from the native source's is_screencast. Consequences:

  • Camera and screen share stay media-equivalent across the two encoders as long as options.source agrees with track.options.isScreencast. A screencast-backed track published with source = CAMERA now diverges: primary MAINTAIN_FRAMERATE, backup MAINTAIN_RESOLUTION.
  • An application-supplied degradationPreference reaches the primary encoder only.
  • The new BALANCED fallback reaches the primary encoder only.

The explicit-override half predates this PR and JS has the same gap, but this PR is what makes a resolved preference a value worth carrying, so applying it to the backup sender belongs here.

return when (source) {
Track.Source.CAMERA -> RtpParameters.DegradationPreference.MAINTAIN_FRAMERATE
Track.Source.SCREEN_SHARE -> RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION
else -> RtpParameters.DegradationPreference.BALANCED

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.

A custom feed published with VideoTrackPublishOptions(source = Track.Source.UNKNOWN) lands here, and this overwrites what libwebrtc would have derived from the native source: a screencast-backed track goes MAINTAIN_RESOLUTION to BALANCED, a camera-like one MAINTAIN_FRAMERATE to BALANCED. track.options.isScreencast already carries that information. BALANCED is also the mode libwebrtc keeps behind the WebRTC-Video-BalancedDegradation field trial, with the in-tree note that it "needs to be tuned first".

Proposal:

private fun getDefaultDegradationPreference(source: Track.Source): RtpParameters.DegradationPreference? {
    return when (source) {
        Track.Source.CAMERA -> RtpParameters.DegradationPreference.MAINTAIN_FRAMERATE
        Track.Source.SCREEN_SHARE -> RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION
        else -> null
    }
}

The KDoc bullet above changes with it. Not a blocker: JS made the same BALANCED choice deliberately, so if this is cross-SDK alignment it stands, but the argument applies there too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think BALANCED is fine for the unknown source here.

And balanced mode is set when the source is neither camera nor screen share, which requires an app to explicitly declare something else, source defaults to null and resolves to Camera/ScreenShare from isScreencast. And it only takes effect when the app hasn't set degradationPreference itself, so anyone who wants a specific behavior (including libwebrtc's implicit derivation) can name it directly.

I ran this locally in both good and constrained network conditions and personally found BALANCED to work pretty well, even without further tuning on the WebRTC side. And for a feed the app has declined to label as motion or detail, a balanced tradeoff seems like the more honest default than committing hard to either axis.

Once this PR is landed, I am going to make follow-up on other SDKs to follow what this PR is doing.

@xianshijing-lk

Copy link
Copy Markdown
Contributor Author

The backup codec sender never gets the preference.

publishAdditionalCodecForTrack creates a second transceiver over the same rtcTrack and never touches sender.parameters, so that sender keeps resolving implicitly from the native source's is_screencast. Consequences:

  • Camera and screen share stay media-equivalent across the two encoders as long as options.source agrees with track.options.isScreencast. A screencast-backed track published with source = CAMERA now diverges: primary MAINTAIN_FRAMERATE, backup MAINTAIN_RESOLUTION.
  • An application-supplied degradationPreference reaches the primary encoder only.
  • The new BALANCED fallback reaches the primary encoder only.

The explicit-override half predates this PR and JS has the same gap, but this PR is what makes a resolved preference a value worth carrying, so applying it to the backup sender belongs here.

Good catch. Confirmed: degradation preference is a sender level property (a top level field on RtpParameters, not per encoding), and publishAdditionalCodecForTrack adds a second transceiver over the same rtc track, so the backup codec has its own sender with its own parameters, which we were never setting. Worth noting both senders sink from the same VideoSource, and VideoBroadcaster::UpdateWants takes the MIN of max_pixel_count and max_framerate_fps across sinks, so a diverging backup drags its restriction onto the primary too.

Applied the resolved preference to the backup sender, resolving the source the same way the primary publish does rather than reading it back off the publication, so the two can't disagree. Added tests asserting both senders match for the default camera, default screen share, explicit-override, and screencast-backed-published-as-camera cases.

I will port the same fix to other sdks once landing this PR.

@xianshijing-lk

Copy link
Copy Markdown
Contributor Author

Hi @adrian-niculescu , thanks for the comments, can you take another look ?

@adrian-niculescu adrian-niculescu left a comment

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.

Read the changes and your comments. LGTM, thanks!

@xianshijing-lk

Copy link
Copy Markdown
Contributor Author

Thanks @adrian-niculescu and @davidliu

@xianshijing-lk
xianshijing-lk merged commit cd99c51 into main Aug 7, 2026
5 checks passed
@xianshijing-lk
xianshijing-lk deleted the sxian/CLT-3068/change_default_degrade_preference_mode_based_on_video_source branch August 7, 2026 06:14
@davidliu davidliu mentioned this pull request Aug 7, 2026
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.

3 participants