diff --git a/.changes/priority-control b/.changes/priority-control new file mode 100644 index 000000000..fb17dcef0 --- /dev/null +++ b/.changes/priority-control @@ -0,0 +1 @@ +minor type="added" "Expose separate bitrate/network priorities for media tracks" diff --git a/Sources/LiveKit/Core/RTC.swift b/Sources/LiveKit/Core/RTC.swift index 8efc78f40..6c1de4cbe 100644 --- a/Sources/LiveKit/Core/RTC.swift +++ b/Sources/LiveKit/Core/RTC.swift @@ -189,6 +189,14 @@ actor RTC { result.scalabilityMode = scalabilityMode.rawStringValue } + if let bitratePriority = encoding?.bitratePriority { + result.bitratePriority = bitratePriority.toBitratePriority() + } + + if let networkPriority = encoding?.networkPriority { + result.networkPriority = networkPriority.toRTCPriority() + } + return result } } diff --git a/Sources/LiveKit/Core/Room+Engine.swift b/Sources/LiveKit/Core/Room+Engine.swift index 11162bcdb..e128a3be2 100644 --- a/Sources/LiveKit/Core/Room+Engine.swift +++ b/Sources/LiveKit/Core/Room+Engine.swift @@ -137,6 +137,8 @@ extension Room { rtcConfiguration.iceTransportPolicy = connectOptions.iceTransportPolicy.toRTCType() } + rtcConfiguration.enableDscp = connectOptions.isDscpEnabled + return rtcConfiguration } diff --git a/Sources/LiveKit/Extensions/CustomStringConvertible.swift b/Sources/LiveKit/Extensions/CustomStringConvertible.swift index 271619b2a..6798b4d16 100644 --- a/Sources/LiveKit/Extensions/CustomStringConvertible.swift +++ b/Sources/LiveKit/Extensions/CustomStringConvertible.swift @@ -191,10 +191,12 @@ extension LKRTCRtpEncodingParameters { "RTCRtpEncodingParameters(" + "rid: \(String(describing: rid)), " + "isActive: \(String(describing: isActive)), " + - "minBitrateBps: \(String(describing: minBitrateBps))" + - "maxBitrateBps: \(String(describing: maxBitrateBps))" + - "maxFramerate: \(String(describing: maxFramerate))" + - "scaleResolutionDownBy: \(String(describing: scaleResolutionDownBy))" + + "minBitrateBps: \(String(describing: minBitrateBps)), " + + "maxBitrateBps: \(String(describing: maxBitrateBps)), " + + "maxFramerate: \(String(describing: maxFramerate)), " + + "scaleResolutionDownBy: \(String(describing: scaleResolutionDownBy)), " + + "bitratePriority: \(bitratePriority), " + + "networkPriority: \(networkPriority)" + ")" } } diff --git a/Sources/LiveKit/Protocols/MediaEncoding.swift b/Sources/LiveKit/Protocols/MediaEncoding.swift index 88e24d340..cc8cda2c7 100644 --- a/Sources/LiveKit/Protocols/MediaEncoding.swift +++ b/Sources/LiveKit/Protocols/MediaEncoding.swift @@ -16,8 +16,14 @@ import Foundation -@objc public protocol MediaEncoding { - // + /// Maximum bitrate in bits per second. var maxBitrate: Int { get } + + /// Priority for bandwidth allocation. + var bitratePriority: Priority? { get } + + /// Priority for DSCP marking. + /// Requires `ConnectOptions.isDscpEnabled` to be true. + var networkPriority: Priority? { get } } diff --git a/Sources/LiveKit/Types/AudioEncoding.swift b/Sources/LiveKit/Types/AudioEncoding.swift index 06fdc54b7..f57981557 100644 --- a/Sources/LiveKit/Types/AudioEncoding.swift +++ b/Sources/LiveKit/Types/AudioEncoding.swift @@ -23,21 +23,40 @@ public final class AudioEncoding: NSObject, MediaEncoding, Sendable { @objc public let maxBitrate: Int + /// Priority for bandwidth allocation. + public let bitratePriority: Priority? + + /// Priority for DSCP marking. + /// Requires `ConnectOptions.isDscpEnabled` to be true. + public let networkPriority: Priority? + @objc public init(maxBitrate: Int) { self.maxBitrate = maxBitrate + bitratePriority = nil + networkPriority = nil + } + + public init(maxBitrate: Int, bitratePriority: Priority?, networkPriority: Priority?) { + self.maxBitrate = maxBitrate + self.bitratePriority = bitratePriority + self.networkPriority = networkPriority } // MARK: - Equal override public func isEqual(_ object: Any?) -> Bool { guard let other = object as? Self else { return false } - return maxBitrate == other.maxBitrate + return maxBitrate == other.maxBitrate && + bitratePriority == other.bitratePriority && + networkPriority == other.networkPriority } override public var hash: Int { var hasher = Hasher() hasher.combine(maxBitrate) + hasher.combine(bitratePriority) + hasher.combine(networkPriority) return hasher.finalize() } } diff --git a/Sources/LiveKit/Types/Options/ConnectOptions.swift b/Sources/LiveKit/Types/Options/ConnectOptions.swift index e5269eb0e..a72bad1cf 100644 --- a/Sources/LiveKit/Types/Options/ConnectOptions.swift +++ b/Sources/LiveKit/Types/Options/ConnectOptions.swift @@ -73,6 +73,11 @@ public final class ConnectOptions: NSObject, Sendable { @objc public let iceTransportPolicy: IceTransportPolicy + /// Allows DSCP codes to be set on outgoing packets when network priority is used. + /// Defaults to false. + @objc + public let isDscpEnabled: Bool + /// Enable microphone concurrently while connecting. @objc public let enableMicrophone: Bool @@ -92,6 +97,7 @@ public final class ConnectOptions: NSObject, Sendable { publisherTransportConnectTimeout = .defaultTransportState iceServers = [] iceTransportPolicy = .all + isDscpEnabled = false enableMicrophone = false protocolVersion = .v16 } @@ -106,6 +112,7 @@ public final class ConnectOptions: NSObject, Sendable { publisherTransportConnectTimeout: TimeInterval = .defaultTransportState, iceServers: [IceServer] = [], iceTransportPolicy: IceTransportPolicy = .all, + isDscpEnabled: Bool = false, enableMicrophone: Bool = false, protocolVersion: ProtocolVersion = .v16) { @@ -118,6 +125,7 @@ public final class ConnectOptions: NSObject, Sendable { self.publisherTransportConnectTimeout = publisherTransportConnectTimeout self.iceServers = iceServers self.iceTransportPolicy = iceTransportPolicy + self.isDscpEnabled = isDscpEnabled self.enableMicrophone = enableMicrophone self.protocolVersion = protocolVersion } @@ -135,6 +143,7 @@ public final class ConnectOptions: NSObject, Sendable { publisherTransportConnectTimeout == other.publisherTransportConnectTimeout && iceServers == other.iceServers && iceTransportPolicy == other.iceTransportPolicy && + isDscpEnabled == other.isDscpEnabled && enableMicrophone == other.enableMicrophone && protocolVersion == other.protocolVersion } @@ -150,6 +159,7 @@ public final class ConnectOptions: NSObject, Sendable { hasher.combine(publisherTransportConnectTimeout) hasher.combine(iceServers) hasher.combine(iceTransportPolicy) + hasher.combine(isDscpEnabled) hasher.combine(enableMicrophone) hasher.combine(protocolVersion) return hasher.finalize() diff --git a/Sources/LiveKit/Types/Priority.swift b/Sources/LiveKit/Types/Priority.swift new file mode 100644 index 000000000..eeca7558b --- /dev/null +++ b/Sources/LiveKit/Types/Priority.swift @@ -0,0 +1,58 @@ +/* + * 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. + */ + +internal import LiveKitWebRTC + +/// Priority levels for RTP encoding parameters. +/// +/// `bitratePriority` controls WebRTC internal bandwidth allocation between streams. +/// When not set, WebRTC uses its default value equivalent to `.low` (1.0x). +/// +/// `networkPriority` controls DSCP marking for network-level QoS. +/// Requires `ConnectOptions.isDscpEnabled` to be true. +@objc +public enum Priority: Int, Sendable { + case veryLow + case low + case medium + case high +} + +extension Priority { + /// Converts to the native RTCPriority enum used for networkPriority. + func toRTCPriority() -> LKRTCPriority { + switch self { + case .veryLow: .veryLow + case .low: .low + case .medium: .medium + case .high: .high + } + } + + /// Converts to bitratePriority double value. + /// - veryLow: 0.5x + /// - low: 1.0x (default) + /// - medium: 2.0x + /// - high: 4.0x + func toBitratePriority() -> Double { + switch self { + case .veryLow: 0.5 + case .low: 1.0 + case .medium: 2.0 + case .high: 4.0 + } + } +} diff --git a/Sources/LiveKit/Types/VideoEncoding.swift b/Sources/LiveKit/Types/VideoEncoding.swift index b6aa56151..ead9153b9 100644 --- a/Sources/LiveKit/Types/VideoEncoding.swift +++ b/Sources/LiveKit/Types/VideoEncoding.swift @@ -24,10 +24,26 @@ public final class VideoEncoding: NSObject, MediaEncoding, Sendable { @objc public let maxFps: Int + /// Priority for bandwidth allocation. + public let bitratePriority: Priority? + + /// Priority for DSCP marking. + /// Requires `ConnectOptions.isDscpEnabled` to be true. + public let networkPriority: Priority? + @objc public init(maxBitrate: Int, maxFps: Int) { self.maxBitrate = maxBitrate self.maxFps = maxFps + bitratePriority = nil + networkPriority = nil + } + + public init(maxBitrate: Int, maxFps: Int, bitratePriority: Priority?, networkPriority: Priority?) { + self.maxBitrate = maxBitrate + self.maxFps = maxFps + self.bitratePriority = bitratePriority + self.networkPriority = networkPriority } // MARK: - Equal @@ -35,13 +51,17 @@ public final class VideoEncoding: NSObject, MediaEncoding, Sendable { override public func isEqual(_ object: Any?) -> Bool { guard let other = object as? Self else { return false } return maxBitrate == other.maxBitrate && - maxFps == other.maxFps + maxFps == other.maxFps && + bitratePriority == other.bitratePriority && + networkPriority == other.networkPriority } override public var hash: Int { var hasher = Hasher() hasher.combine(maxBitrate) hasher.combine(maxFps) + hasher.combine(bitratePriority) + hasher.combine(networkPriority) return hasher.finalize() } } diff --git a/Sources/LiveKit/Types/VideoParameters.swift b/Sources/LiveKit/Types/VideoParameters.swift index 02995a7fd..71e20a980 100644 --- a/Sources/LiveKit/Types/VideoParameters.swift +++ b/Sources/LiveKit/Types/VideoParameters.swift @@ -91,9 +91,14 @@ extension VideoParameters { let dimensions = Dimensions(width: Int32((Double(dimensions.width) / $0.scaleDownBy).rounded(.down)), height: Int32((Double(dimensions.height) / $0.scaleDownBy).rounded(.down))) let bitrate2 = Int((Double(encoding.maxBitrate) / (pow(Double($0.scaleDownBy), 2) * (Double(encoding.maxFps) / Double($0.fps)))).rounded(.down)) - let encoding = VideoEncoding(maxBitrate: Swift.max(150_000, bitrate2), maxFps: $0.fps) - - return VideoParameters(dimensions: dimensions, encoding: encoding) + let layerEncoding = VideoEncoding( + maxBitrate: Swift.max(150_000, bitrate2), + maxFps: $0.fps, + bitratePriority: encoding.bitratePriority, + networkPriority: encoding.networkPriority + ) + + return VideoParameters(dimensions: dimensions, encoding: layerEncoding) } }