Skip to content

Live telemetry averageBitrateMbps divides by wall-clock time, so it decays toward zero while paused and never recovers #514

Description

@classicjazz

What happened?

LiveTelemetrySampler.tick() (Sources/AetherEngine/Diagnostics/LiveTelemetrySampler.swift, ~line 207) computes the session-average bitrate as:

let elapsed = max(0.5, Date().timeIntervalSince(sessionStartTime))
let lifetimeBytes = max(0, demuxerBytes - sessionStartBytes)
averageBitrateMbps = Double(lifetimeBytes) * 8.0 / elapsed / 1_000_000.0

The denominator is pure wall-clock time since the session started. It never subtracts time the session spent paused.

While playback is paused, demuxerBytes stops advancing (once the forward buffer is full) but elapsed keeps growing. So the reported average falls steadily the longer the session sits paused, trending toward zero. When playback resumes it climbs back only slowly and never returns to the true average, because the paused seconds stay in the divisor permanently.

instantBitrateMbps is unaffected — it's windowed over recent ticks. Only the lifetime average is wrong.

The value is wrong at the source, in LiveTelemetry.averageBitrateMbps, so every consumer inherits the error — including the "divide file size by averageBitrateMbps" download-time estimate suggested in the doc-comment on LiveTelemetry itself. It's visible in Sodalite's Stats for Nerds "Bitrate" row (avg N Mbps), but that's just where it shows up, not where it originates.

Steps to reproduce

  1. Start any session that produces LiveTelemetry (native or software path), e.g. a direct-play VOD file.
  2. Let it play ~30 s. Note the average bitrate — it sits near the media's real bitrate.
  3. Pause for 2–3 minutes.
  4. Observe the average has dropped well below the real bitrate, roughly in proportion to playtime ÷ total elapsed.
  5. Resume. The average recovers only gradually and asymptotes below the true value — the paused interval is permanently in the denominator.

Expected: the average is taken over time the session was actually playing/transferring, so a pause leaves it flat rather than dragging it down.

AetherEngine version or commit SHA

6.73.0

Host app

Sodalite

Platform

tvOS

OS version

tvos 27

Device / chip

Apple TV 4K

Playback path

Not sure / not a playback bug

Source media (for playback bugs)

No response

Error codes / log lines


Anything else

Why this belongs in AetherEngine and can't just be fixed in Sodalite: averageBitrateMbps is computed in the sampler and shipped as a field of the public LiveTelemetry struct. Sodalite only reads it. A host-side workaround (freeze the last playing value while state == .paused before displaying it) hides the symptom in one row of one screen, but the field stays wrong for every other consumer — the download-time estimate the struct's own documentation points at, any future HUD, any logging. The divisor is the bug and only the engine owns it.

Suggested fix: accumulate an "active" duration — wall-clock minus time spent in .paused / .seeking — and divide by that instead of Date().timeIntervalSince(sessionStartTime). Alternatively, integrate bytes-over-time across the sampler's own tick windows and only advance the accumulator on ticks where the transport is playing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions