spotupnp: volume stops working after gettime_ms() wraps (uint32 epoch ms) - #72
Merged
philippe44 merged 1 commit intoSep 6, 2026
Conversation
gettime_ms() is epoch-based milliseconds truncated to uint32_t, so it wraps every 49.7 days. The volume echo gates compared stamps in absolute terms - 'now < VolumeStampRx + 1000' and 'now > VolumeStampTx + 1000' - which inverts once a wrap lands between the stamp and now: every volume command from Spotify is then discarded as an echo, and every local renderer volume change is suppressed as our own echo, both silently, for up to the next 49.7 days or until the device is re-added. Observed live: a renderer added 2026-08-10 stamped 3,973,793,078; epoch milliseconds wrapped on 2026-08-14; by 2026-08-16 now read 163,458,864 and both volume directions were dead while everything else worked. Measure age by unsigned subtraction instead, which stays correct across the wrap, including for the now - 2000 seeding in AddMRDevice().
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.
Symptom
Volume control dies in both directions while everything else keeps working: volume commands from a Spotify controller are silently dropped before reaching the renderer, and local volume changes on the renderer are no longer forwarded to Spotify. Play/pause/next/seek/load are all unaffected. The problem heals by itself after up to ~49 days, or immediately on any restart or renderer re-add, which makes it look random and pretty much impossible to reproduce on demand.
What I observed
My spotupnp instance (Linux x86_64, single Hama DIT2010 renderer) had been running since Aug 10. On Aug 16 the desktop app's volume slider did nothing, although the spirc volume frame demonstrably arrived: cspot accepted it and echoed it back (the slider snaps to the device value), but no
Volume[0..100]log and no SetVolume ever followed. Local volume changes on the renderer also stopped producingUPnP Volume local changelines. Restarting the very same binary fixed both directions instantly.The timeline is what gave it away:
VolumeStampRx/Txwere seeded withgettime_ms() - 2000= 3,973,793,078gettime_ms()returned 163,458,864Root cause (as far as I can tell)
gettime_ms()is epoch-based and truncated touint32_t, so it wraps every 49.7 days. The two volume echo gates compare stamps in absolute terms:Once a wrap lands between the stamp and
now, both comparisons invert: 163,458,864 < 3,973,794,078, so every incoming volume command is treated as an echo of a local change, and every local change is treated as an echo of our own SetVolume. Both paths are silent, and they stay wrong untilnowgrows past the stale stamp (up to another 49 days) or until the device is re-added and the stamps are re-seeded.Fix
Measure age by unsigned subtraction instead of comparing absolute stamps; modular arithmetic keeps
now - stampcorrect across the wrap, including for a genuine echo that itself straddles the wrap, and thenow - 2000seeding inAddMRDevice()also behaves under it:I verified the arithmetic with a small standalone test using the captured values above plus echo-across-wrap and fresh-device cases, and confirmed on the live system that the gate was exactly where the commands died.
Open points
I kept this deliberately minimal, but the same absolute-comparison pattern exists in a couple of other places (the expiration checks in cross_net.c, and the presence check divides
gettime_ms()by 1000, which makes even subtraction jump at the wrap; in the worst case that could briefly make a STOPPED device look mute to discovery). Happy to extend the patch if you think those are worth covering, or leave them alone if you prefer.I think this patch should fix it, but I would appreciate a sanity check of the reasoning - does the analysis look right to you? If it does, this probably explains any historical "volume randomly stopped working for a few weeks, then came back on its own" reports across the UPnP bridges, since the wrap moment is global (epoch-based): every long-running instance whose renderer was added before a wrap goes deaf to volume at the same wall-clock instant.