Skip to content

kithara abr

Pavel Litvinenko edited this page Sep 10, 2026 · 2 revisions

kithara-abr

Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.

Decision flow

The controller consumes bandwidth and peer progress, records switch intent, and wakes the peer. It does not publish a variant change; publication belongs to the boundary owner described below. The decision implementation lives in the source linked above.

Decision logic

The configured decision policy handles manual selection, throughput, buffer urgency, escape, hysteresis, and the minimum switch interval. Rescue and manual decisions bypass that interval. A deferred decision records a deadline driven by the downloader and re-evaluates live evidence on expiry; it never caches a target or creates a separate ABR timer task.

Pending protocol and publication authority

AbrState separates intent from publication.

  • request_target(target, reason) records intent. Replace-pending, latest wins; a repeat request for the target already queued is a full no-op, ticket and reason included — the ticket names the exact transition a consumer has already built its incoming session and decoder against, so re-minting one would cancel that work on every tick and the switch could never finish. Under Manual(idx) a request for any other target is refused; the mode is read under the slot lock, pairing with the store-mode-then-clear-slot order in set_mode. The write happens regardless of lock state.
  • AbrTicket identifies one accepted request. pending_claim(current) reports Absent, Locked(claim), or Ready(claim); a pending that already equals current reports Absent.
  • commit_pending(claim, now) publishes only when the slot still carries the same ticket and rebuilds the same decision, and never while locked. abort_pending(ticket) drops only the matching request; a stale ticket leaves a newer intent untouched.
  • commit_pending and apply_decision are the only writers of current_variant. Production HLS commits through the publisher at a segment boundary (kithara-hls stream/transition.rs); apply_decision is the direct publish path.
  • AbrPublisher (minted by AbrState::publisher()) is the publication capability the state's owner keeps. Consumers receive AbrHandle, which observes and controls but cannot publish.
  • lock() / unlock() are reentrant and gate publication only — the intent survives so it resumes on unlock. invalidate_pending() is the destructive counterpart, called on a semantic seek boundary: it drops throughput-driven intents (UpSwitch, DownSwitch, UrgentDownSwitch, EscapeStalled) and preserves ManualOverride / Initial, which a position jump does not invalidate.
  • retract_throughput_pending(current) runs from the tick's Stay { AlreadyOptimal } arm — the one verdict that re-affirms current on live evidence. It drops a throughput-driven, non-rescue pending aimed elsewhere, so an urgent down-switch latched on the initial seed cannot outlive the estimate that justified it.
  • set_mode clears the pending slot, except when a manual pin restates the target already queued.
  • selected_variant_for_seek() returns the pending target when one exists, even while locked, so a seek replacement opens the variant the user is switching to.

Escape

mark_escape() flags the active variant as non-delivering; the HLS stall detector sets it when the reader is parked at a clean boundary on a segment whose in-flight fetch crossed the downloader soft timeout. The caller must follow with AbrHandle::reevaluate(): the flag is set under the HLS state lock and the tick re-locks that state through peer.progress(), so the tick must fire outside the lock. Publishing a new variant clears escape before storing it, so a concurrent tick that observes the new current_variant also observes the cleared flag and cannot immediately re-escape off a variant that has not had a chance to deliver.

Throughput estimation

Dual-track EWMA — fast (2 s half-life) and slow (10 s half-life); estimate = min(fast, slow), conservative by construction.

  • Samples below 16 000 bytes are dropped as noise, fetch duration is clamped to ≥ 0.5 ms, and a zero fetch duration drops the sample in record_bandwidth before it reaches the estimator.
  • BandwidthSource::Cache never feeds the EWMAs: it pins the fallback estimate to 100 Mbps so a cache hit is not mistaken for network throughput.
  • With no EWMA weight yet, the estimate falls back to the seed / cache value; None only when there is neither.

Initial seed

AbrSettings::initial_throughput_bps (default Some(2_000_000)) is applied to the estimator at controller construction so the first tick can pick a sensible variant before a real sample lands. ≈2 Mbps covers Wi-Fi and most 4G; constrained networks down-switch after the first real sample. It is a transient prior — real EWMA weight replaces it through the min(fast, slow) consensus. Set it to None for the cold-start path: decide() returns NoEstimate and the peer stays on its initial variant until samples accumulate.

AbrSettingsPatch is the second entry point: a configuration document types into it and apply writes past the builder, so a document can compose a setting the builder would have refused. One thing it cannot express — initial_throughput_bps and max_bandwidth_bps are already Option<u64>, so a document sets a value but cannot blank one: initial_throughput_bps: null reads as "leave it alone", not as the cold-start path above. The cold start stays a builder-only choice.

AbrSettings is the facade configuration for the controller: it carries both algorithm parameters and injected resources such as the optional parent CancelToken. It is #[non_exhaustive] and built with AbrSettings::builder()…build() (Default goes through the builder); initial_throughput_bps(Some(value)) sets the seed and initial_throughput_bps(None) explicitly disables it.

Ownership

  • kithara-abr owns the public ABR event and value vocabulary: AbrEvent, AbrMode, AbrProgressSnapshot, AbrReason, BandwidthSource, BoundsError, VariantDuration, VariantIndex, and VariantInfo.
  • Variants live on the peer (Abr::variants()), never in AbrState; they reach the decision through AbrView. AbrState owns only runtime control: current index, mode, lock count, escape flag, bandwidth cap, last-switch timestamp, pending slot.
  • Abr::cancel() is mandatory and returns the protocol track/source token. The controller observes this token but never cancels it; HLS returns its stream token and File returns its source token.
  • AbrHandle::set_mode validates Manual(idx) against the peer's live variant list and returns AbrError::VariantOutOfBounds; AbrState::set_mode does not validate.
  • AbrHandle is the consumer surface; dropping the last clone unregisters the peer. The track-scoped EventBus lives on the handle (with_bus), so peers stay free of event-bus plumbing.
  • AbrController owns a child scope of the optional parent carried by AbrSettings. Each registration derives one controller-owned child and OR-combines it with Abr::cancel() through CancelGroup; controller/parent cancellation stops every registration, protocol cancellation stops that track, and sibling registrations remain live.
  • Downloader::run is the only async driver for ABR scheduling. AbrController stores coalesced tick requests, deadlines, and the downloader task's current waker; it never spawns a worker of its own.
  • AbrController::register(peer) discovers protocol cancellation from the peer. Dropping the last AbrHandle cancels only the controller-owned registration child, so unregister never cancels the protocol track or the controller scope.
  • AbrHandle::notify_exact_commit publishes AbrEvent::VariantApplied after a promotion and does nothing else. Its caller is the audio worker, which is not a runtime thread, so this path must not schedule async work.
  • kithara-hls reads the variant through AbrHandle / Arc<AbrState>; no cloneable Arc<AtomicUsize> handle is exposed — see redundant_accessors in crates/kithara-devtools/src/arch/checks for the rationale.

Event throttling

Per peer: ThroughputSample at most every throughput_sample_min_interval (default 200 ms); BandwidthEstimate when bandwidth_emit_min_interval has elapsed or the relative change reaches bandwidth_emit_min_delta_ratio; BufferAhead on any Some/None transition, otherwise when both the interval and the absolute-delta thresholds are met.

Benchmarking

Criterion microbenchmarks for the estimator/decision hot paths live in kithara-integration-tests:

cargo bench -p kithara-integration-tests --bench abr_estimator

Clone this wiki locally