fix: advance position and show LIVE for unknown-duration streams#62
Merged
Conversation
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.
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.
Summary
Live radio / unknown-duration streams showed
0:00 ──────────── 0:00with 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 onDurationSeconds <= 0, a condition with no basis in the spec.ExtrapolateAtwas 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.
ProgressPercentreturned 0 and the duration label renderedFormatTime(0)→0:00, while the progress row stayed visible because its visibility binds toCurrentTrack.Spec basis
Validated against Sendspin/spec @
3632c68:track_duration: "total track length in milliseconds, 0 for unlimited/unknown duration (e.g., live radio streams)"elsebranch ismax(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 namedunknownDuration_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 both0andnull; both are handled.MainViewModel.HasKnownDurationis the single source of truth for the UI:DurationFormattedshowsLIVEinstead of0:00, and the ProgressBar'sVisibilitybinds through the existingBoolToVisibilityConverter(no new converter). The layout column staysWidth="*", so collapsing the bar keeps elapsed left-aligned andLIVEright-aligned.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_ClampToZeronow 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!= 0predicate has the same property.Manual verification
LIVE; no bar track drawn.0:00. This is the only path exercising the newHasKnownDurationchange notification and has no automated guard.