diff --git a/packages/serve-sim/Sources/StreamingPolicy/WebRTCBitratePolicy.swift b/packages/serve-sim/Sources/StreamingPolicy/WebRTCBitratePolicy.swift index 386db654e..80ca5c0db 100644 --- a/packages/serve-sim/Sources/StreamingPolicy/WebRTCBitratePolicy.swift +++ b/packages/serve-sim/Sources/StreamingPolicy/WebRTCBitratePolicy.swift @@ -1,9 +1,22 @@ public struct WebRTCBitratePolicy: Equatable, Sendable { + /// Below this the stream is not worth keeping alive at any resolution. + public static let absoluteMinimumBitsPerSecond = 250_000 + public let minimumBitsPerSecond: Int public let maximumBitsPerSecond: Int + /// The estimator starts at the target (the owner passes the maximum as the + /// starting estimate), so ramp-up stays instant on healthy paths. The + /// minimum only bounds how far congestion control may back off: at 10% of + /// target it can shrink into what a constrained path actually carries. + /// The previous minimum was 90% of target, which pinned the estimator into + /// a band the path sometimes could not carry — sustained overshoot, loss, + /// recovery keyframes, and freezes instead of a quality dip. public init(targetBitsPerSecond: Int) { - minimumBitsPerSecond = targetBitsPerSecond * 9 / 10 + minimumBitsPerSecond = min( + targetBitsPerSecond, + max(Self.absoluteMinimumBitsPerSecond, targetBitsPerSecond / 10) + ) maximumBitsPerSecond = targetBitsPerSecond } } diff --git a/packages/serve-sim/Tests/StreamingPolicyTests/WebRTCBitratePolicyTests.swift b/packages/serve-sim/Tests/StreamingPolicyTests/WebRTCBitratePolicyTests.swift index a9e012267..83a147ba6 100644 --- a/packages/serve-sim/Tests/StreamingPolicyTests/WebRTCBitratePolicyTests.swift +++ b/packages/serve-sim/Tests/StreamingPolicyTests/WebRTCBitratePolicyTests.swift @@ -2,10 +2,31 @@ import XCTest @testable import StreamingPolicy final class WebRTCBitratePolicyTests: XCTestCase { - func testKeepsAdaptiveBandwidthBetweenNinetyPercentAndConfiguredTarget() { + func testLeavesCongestionControlRoomBelowTheConfiguredTarget() { let policy = WebRTCBitratePolicy(targetBitsPerSecond: 6_000_000) - XCTAssertEqual(policy.minimumBitsPerSecond, 5_400_000) + // 10% of target: under congestion the estimator may back off to a + // survivable rate instead of overrunning a path that cannot carry + // 90% of target — the old floor turned every real dip into loss, + // recovery keyframes, and freezes. + XCTAssertEqual(policy.minimumBitsPerSecond, 600_000) XCTAssertEqual(policy.maximumBitsPerSecond, 6_000_000) } + + func testMinimumNeverDropsBelowASurvivableAbsoluteFloor() { + let policy = WebRTCBitratePolicy(targetBitsPerSecond: 1_000_000) + + // 10% would be 100 kbps — below what keeps the stream alive. + XCTAssertEqual(policy.minimumBitsPerSecond, 250_000) + XCTAssertEqual(policy.maximumBitsPerSecond, 1_000_000) + } + + func testMinimumIsClampedToTinyConfiguredTargets() { + let policy = WebRTCBitratePolicy(targetBitsPerSecond: 100_000) + + // The CLI accepts targets down to 100 kbps; the minimum must never + // exceed the maximum. + XCTAssertEqual(policy.minimumBitsPerSecond, 100_000) + XCTAssertEqual(policy.maximumBitsPerSecond, 100_000) + } }