Skip to content

fix: advance position and show LIVE for unknown-duration streams#62

Merged
chrisuthe merged 4 commits into
masterfrom
fix/radio-unknown-duration
Jul 20, 2026
Merged

fix: advance position and show LIVE for unknown-duration streams#62
chrisuthe merged 4 commits into
masterfrom
fix/radio-unknown-duration

Conversation

@chrisuthe

Copy link
Copy Markdown
Owner

Summary

Live radio / unknown-duration streams showed 0:00 ──────────── 0:00 with a permanently empty bar and an elapsed time that never moved. Two independent defects stacked — fixing either alone still leaves radio looking broken.

1. Position never advanced (spec violation). Tick() bailed out on DurationSeconds <= 0, a condition with no basis in the spec. ExtrapolateAt was already correct — its unknown-duration branch was simply dead code on the periodic path, so a live stream's position only jumped when a fresh progress message happened to arrive.

2. The display was wrong. ProgressPercent returned 0 and the duration label rendered FormatTime(0)0:00, while the progress row stayed visible because its visibility binds to CurrentTrack.

Spec basis

Validated against Sendspin/spec @ 3632c68:

  • README:1447 — track_duration: "total track length in milliseconds, 0 for unlimited/unknown duration (e.g., live radio streams)"
  • README:1454-1461 — the canonical formula's else branch is max(calculated_progress, 0), so position must still advance when duration is 0; only the upper clamp is skipped.

Cross-checked against the references: aiosendspin advances (_get_current_track_progress), and SendspinDroid's protocol layer advances with an explicit test named unknownDuration_notClampedAbove. Our freeze was the outlier.

Changes

  • Tick() now runs whenever an anchor exists; ExtrapolateAt (unchanged) applies clamp-or-not correctly. The SDK models unknown duration as both 0 and null; both are handled.
  • New MainViewModel.HasKnownDuration is the single source of truth for the UI: DurationFormatted shows LIVE instead of 0:00, and the ProgressBar's Visibility binds through the existing BoolToVisibilityConverter (no new converter). The layout column stays Width="*", so collapsing the bar keeps elapsed left-aligned and LIVE right-aligned.
  • Known-duration playback is unchanged in every respect.
  • Design note: docs/superpowers/specs/2026-07-20-radio-unknown-duration-design.md, which flags the LIVE / hidden-bar treatment as a product choice, not spec-mandated — easy to swap for an indeterminate bar if preferred.

Tests

4 net new cases: unclamped advance on zero duration and on null duration, unbounded growth (+2 h with no clamp), speed scaling at unknown duration, and speed 0 still freezing. Red-first confirmed (Failed: 7, Passed: 40) before implementation.

Tracker suite 47/47. Full suite 144/146 — the 2 failures are the known pre-existing resampler ones. Release rebuild clean at the 553-warning baseline.

Three existing tests encoded the old frozen behavior as if it were intended (one literally commented "Preserved behavior: duration-less streams show a static server position") and were rewritten to the spec-correct expectation.

One knock-on worth noting: NegativeProgressAndDuration_ClampToZero now expects the position to advance, because a negative duration clamped to 0 is indistinguishable from "unknown". Negative durations are non-conformant regardless, and the spec's != 0 predicate has the same property.

Manual verification

  1. Radio — elapsed counts up smoothly and indefinitely; right label reads LIVE; no bar track drawn.
  2. Normal track — visually unchanged: filling bar, real duration, clamps at 100%.
  3. Switch radio → track → radio — bar appears/disappears, label alternates, no stale 0:00. This is the only path exercising the new HasKnownDuration change notification and has no automated guard.
  4. Pause on radio — elapsed stops and resumes in place.

The Sendspin spec (README:1447, 1454-1461) defines track_duration = 0 as
unlimited/unknown (live radio) and its progress formula still advances the
position in that case — it only skips the upper clamp. Three existing tests
encoded the opposite, frozen behavior:

- Tick_WithoutDuration_DoesNotAdvance became
  Tick_ZeroDuration_AdvancesUnclamped: its "preserved behavior" comment
  described the defect, not a requirement.
- NullDurationFromTrackStart_ShowsStaticPosition became
  NullDurationFromTrackStart_AdvancesFromServerPosition: the SDK models
  unknown duration as null as well as 0, so it must advance too.
- FreshProgress_ZeroDuration_SetsDurationToZero keeps its duration
  assertion; only its "no extrapolation without a duration" expectation
  changes.
- NegativeProgressAndDuration_ClampToZero: a non-positive duration is
  indistinguishable from unknown, so the position now advances from zero.

Adds coverage for the null-duration variant, unbounded growth two hours in,
speed scaling, and speed 0 still freezing. These fail against the current
tracker.
Tick() bailed out on DurationSeconds <= 0, so a live radio stream's position
only moved when a fresh progress message arrived and was frozen in between.
The condition has no basis in the spec: README:1454-1461 skips only the upper
clamp when track_duration is 0, and ExtrapolateAt already implements exactly
that. Its zero-duration branch was dead code on the periodic path.

Extrapolate whenever an anchor exists and let ExtrapolateAt decide about
clamping. The SDK's null duration collapses to the same DurationSeconds == 0,
so both representations are covered.
With an unknown duration the progress row rendered "0:00 ---- 0:00" with a
permanently empty bar: DurationFormatted formatted 0 as "0:00" and
ProgressPercent was always 0. The row itself is visible whenever a track is
present, so radio always looked broken.

Add HasKnownDuration (Duration > 0) as the single source of truth: the
duration label reads "LIVE" instead of 0:00, and the ProgressBar collapses
via the existing BoolToVisibilityConverter, leaving the elapsed time to tick
on its own. A percentage of an unknown total is meaningless, so
ProgressPercent stays 0 and is simply unused in that mode. The presentation
is a product choice, not a spec requirement; known-duration tracks are
untouched.
@chrisuthe
chrisuthe merged commit c870793 into master Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant