From bc0cb8abc350e711738c610474b3b67cec849154 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Fri, 7 Aug 2026 16:13:37 +0800 Subject: [PATCH] Resolve default degradation preference by source, incl. backup codec `.auto` resolved to `.maintainResolution` for every video track. It now resolves from the track source: camera maintains framerate, screen share maintains resolution, other sources fall back to balanced. Degradation preference is a sender-level property, and a backup codec publishes over its own sender, so it needs the preference applied separately. `publish(additionalVideoCodec:for:)` never set it, leaving the backup encoder to resolve one implicitly and potentially adapt along a different axis than the primary. Apply the resolved preference there too. Extract the sender parameter write into `LKRTCRtpSender.set(degradationPreference:)` so both publish paths share it. Co-Authored-By: Claude Opus 5 (1M context) --- .../default-degradation-preference-by-source | 1 + .../LiveKit/Extensions/LKRTCRtpSender.swift | 13 ++++++ .../Participant/LocalParticipant.swift | 13 +++--- .../LiveKit/Types/DegradationPreference.swift | 20 +++++++++ .../DegradationPreferenceTests.swift | 44 +++++++++++++++++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 .changes/default-degradation-preference-by-source create mode 100644 Tests/LiveKitCoreTests/DegradationPreferenceTests.swift diff --git a/.changes/default-degradation-preference-by-source b/.changes/default-degradation-preference-by-source new file mode 100644 index 000000000..a84844da7 --- /dev/null +++ b/.changes/default-degradation-preference-by-source @@ -0,0 +1 @@ +patch type="changed" "Resolve the default video degradation preference from the track source (camera maintains framerate, screen share maintains resolution, others balanced) and apply it to the backup codec's sender as well" diff --git a/Sources/LiveKit/Extensions/LKRTCRtpSender.swift b/Sources/LiveKit/Extensions/LKRTCRtpSender.swift index e100c608e..ffd851f08 100644 --- a/Sources/LiveKit/Extensions/LKRTCRtpSender.swift +++ b/Sources/LiveKit/Extensions/LKRTCRtpSender.swift @@ -19,6 +19,19 @@ import Foundation internal import LiveKitWebRTC extension LKRTCRtpSender: Loggable { + /// Sets the degradation preference on this sender. + /// + /// Degradation preference is a property of the sender, not of the track, so every sender + /// publishing a track needs it applied separately. A backup codec publishes over its own + /// sender, which would otherwise let WebRTC resolve a preference implicitly from the native + /// source and diverge from the primary encoder. + func set(degradationPreference: LKRTCDegradationPreference) { + // Changing params directly doesn't work so we need to update params and set it back to sender.parameters + let _parameters = parameters + _parameters.degradationPreference = NSNumber(value: degradationPreference.rawValue) + parameters = _parameters + } + // ... func _set(subscribedQualities qualities: [Livekit_SubscribedQuality]) { let _parameters = parameters diff --git a/Sources/LiveKit/Participant/LocalParticipant.swift b/Sources/LiveKit/Participant/LocalParticipant.swift index dea4600fe..f1d4c41e1 100644 --- a/Sources/LiveKit/Participant/LocalParticipant.swift +++ b/Sources/LiveKit/Participant/LocalParticipant.swift @@ -509,6 +509,12 @@ extension LocalParticipant { let sender = transceiver.sender + // The backup codec publishes over its own sender, so it needs the same degradation + // preference the primary sender resolved to. + let degradationPreference = publishOptions.degradationPreference.resolve(for: track.source) + log("[Publish/Backup] set degradationPreference to \(degradationPreference)") + sender.set(degradationPreference: degradationPreference) + // Request a new track to the server let trackInfo = try await room.signalClient.sendAddTrack(cid: sender.senderId, name: track.name, @@ -697,13 +703,10 @@ extension LocalParticipant { if track is LocalVideoTrack { let publishOptions = (options as? VideoPublishOptions) ?? room._state.roomOptions.defaultVideoPublishOptions - let degradationPreference = publishOptions.degradationPreference.toRTCType() ?? .maintainResolution + let degradationPreference = publishOptions.degradationPreference.resolve(for: track.source) self.log("[publish] set degradationPreference to \(degradationPreference)") - let params = transceiver.sender.parameters - params.degradationPreference = NSNumber(value: degradationPreference.rawValue) - // Changing params directly doesn't work so we need to update params and set it back to sender.parameters - transceiver.sender.parameters = params + transceiver.sender.set(degradationPreference: degradationPreference) if let preferredCodec = publishOptions.preferredCodec { try transceiver.set(preferredVideoCodec: preferredCodec) diff --git a/Sources/LiveKit/Types/DegradationPreference.swift b/Sources/LiveKit/Types/DegradationPreference.swift index 20fd271f4..2a13eca54 100644 --- a/Sources/LiveKit/Types/DegradationPreference.swift +++ b/Sources/LiveKit/Types/DegradationPreference.swift @@ -42,4 +42,24 @@ extension DegradationPreference { case .balanced: .balanced } } + + /// Resolves this preference for a track published under `source`. + /// + /// ``DegradationPreference/auto`` picks a default based on the source: + /// - Camera: ``DegradationPreference/maintainFramerate`` (smoother video for real-time communication) + /// - Screen share: ``DegradationPreference/maintainResolution`` (clarity is critical for reading text/UI) + /// - Other/unknown: ``DegradationPreference/balanced`` + /// + /// Any other source means the application declined to declare a motion-vs-detail intent, + /// so this falls back to balanced, the preference the WebRTC spec mandates as the default. + func resolve(for source: Track.Source) -> LKRTCDegradationPreference { + if let explicit = toRTCType() { + return explicit + } + switch source { + case .camera: return .maintainFramerate + case .screenShareVideo: return .maintainResolution + default: return .balanced + } + } } diff --git a/Tests/LiveKitCoreTests/DegradationPreferenceTests.swift b/Tests/LiveKitCoreTests/DegradationPreferenceTests.swift new file mode 100644 index 000000000..364e89727 --- /dev/null +++ b/Tests/LiveKitCoreTests/DegradationPreferenceTests.swift @@ -0,0 +1,44 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@testable import LiveKit +import LiveKitWebRTC +import Testing + +struct DegradationPreferenceTests { + @Test func autoResolvesBySource() { + // smoother video for real-time communication + #expect(DegradationPreference.auto.resolve(for: .camera) == .maintainFramerate) + // clarity is critical for reading text/UI + #expect(DegradationPreference.auto.resolve(for: .screenShareVideo) == .maintainResolution) + } + + @Test func autoFallsBackToBalancedForOtherSources() { + // the application declined to declare a motion-vs-detail intent + #expect(DegradationPreference.auto.resolve(for: .unknown) == .balanced) + } + + @Test func explicitPreferenceWinsOverSourceDefault() { + #expect(DegradationPreference.balanced.resolve(for: .camera) == .balanced) + #expect(DegradationPreference.maintainResolution.resolve(for: .camera) == .maintainResolution) + #expect(DegradationPreference.maintainFramerate.resolve(for: .screenShareVideo) == .maintainFramerate) + #expect(DegradationPreference.maintainFramerateAndResolution.resolve(for: .unknown) == .maintainFramerateAndResolution) + } + + @Test func defaultVideoPublishOptionsUseAuto() { + #expect(VideoPublishOptions().degradationPreference == .auto) + } +}