feat: EWMA throughput estimator + measured cellular ceiling (fixes #24) - #37
Merged
Conversation
The ABR was reactive-only and hard-capped any cellular path at 2.5 Mbps regardless of the real 5G uplink. This turns that hard cap into a soft SEED the upward probe can exceed toward the user max once measured throughput and a sustained clean queue justify it, and adds an EWMA throughput estimator. StreamCore/ThroughputEstimator.swift (new): - Pure, Sendable/Equatable EWMA (0.4 factor) fed the 1 Hz currentBytesOutPerSecond the network monitor already surfaces. First positive sample is adopted verbatim; non-positive samples (stalls) are ignored; reset() drops the estimate on handoff. NetworkPathSnapshot: - videoBitRateCeiling no longer hard-caps cellular/expensive at 2.5 Mbps (Low Data Mode / ultra-constrained / minimal-link-quality remain hard caps). - New videoBitRateSeed returns the conservative opening bid: 2.5 Mbps on cellular/expensive, the hard ceiling otherwise. AdaptiveBitRateState: - pathSeed + throughput fields. onPathProfile(ceiling:seed:...) opens cellular at the seed on baseline, seeds a handoff from lastGoodTarget (measurement-derived) or the conservative cold seed, and resets the estimator on a genuine handoff. - Upward probe climbs freely below the seed (recovery) but only ABOVE it when the audio-netted EWMA is within 90% of the target AND the queue has been clean 30 s. - A sustained clean queue clears congestion (restoring full frame rate) even when the target is held at the seed below effectiveMaximum, and signals an apply on the clearing tick so the encoder actually restores fps. Publishers: both RTMP and the shared SRT/WHIP controller pass the seed alongside the ceiling, and feed live audio bitrate into the status path so the probe gate compares video-vs-video. Reduction stays on the raw congestion-tick egress (true capacity during sustained queue growth); the EWMA is used only for the probe gate, because egress conflates encoder demand (healthy ticks) with link capacity (congestion) and smoothing the cut against it regresses congestion response either way. Validated by two adversarial review rounds and CI-run Swift Testing suites (AdaptiveBitRateStateTests, ThroughputEstimatorTests, SharedSupervisionTests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #24 (M9 · EWMA throughput estimator + measured cellular ceiling).
Problem
The ABR was reactive-only and hard-capped any cellular path at 2.5 Mbps regardless of the real 5G uplink. Recovery started from a low seed and could never exceed the cap.
What changed
ThroughputEstimator(new, StreamCore): pureSendable/EquatableEWMA (0.4 factor) fed the 1 HzcurrentBytesOutPerSecondthe network monitor already surfaces. First positive sample verbatim; stalls ignored;reset()on handoff.NetworkPathSnapshot: the 2.5 Mbps cellular value is now a seed, not a hard ceiling.videoBitRateCeilingkeeps the genuine hard caps (Low Data Mode 1.2M, ultra-constrained 0.8M, minimal-link halving); newvideoBitRateSeedreturns the conservative opening bid (2.5M on cellular/expensive, hard ceiling otherwise).AdaptiveBitRateState: opens cellular at the seed, and the upward probe climbs freely below the seed (recovery) but only above it (toward the user max) when the audio-netted EWMA is within 90% of the target and the queue has been clean for 30 s. A sustained clean queue clears congestion (restoring full frame rate) even while the target is held at the seed.Publishers: RTMP and the shared SRT/WHIP controller pass the seed alongside the ceiling and feed live audio bitrate into the status path.
Design note (from review)
The reduction stays on the raw congestion-tick egress (≈ true capacity during sustained queue growth). The EWMA is used only for the probe gate: egress conflates encoder demand (healthy ticks) with link capacity (congestion), and two adversarial review rounds showed that smoothing the cut against it regresses congestion response in both directions. The estimator's sound job is confirming the link is delivering the rate before climbing above the conservative seed.
Testing
CI-run Swift Testing suites:
AdaptiveBitRateStateTests(new cases for reduction-vs-EWMA, cellular seed/probe, frame-rate restoration, audio-netting),ThroughputEstimatorTests, updatedSharedSupervisionTests. All arithmetic independently validated against the compiledStreamCoretypes (25 assertions). Two adversarial multi-agent review rounds; all findings resolved.🤖 Generated with Claude Code