Persist BGM track index and playback position across page refresh (Album 1) #71
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#71
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Today, refreshing the page resets the Album 1 BGM back to track 1 at 00:00. Players who have been listening for a while lose their place every time they navigate, refresh, or reopen the tab. We want the player to resume from approximately where the listener left off.
This issue covers persisting the current track index and the current playback position (seconds into the track) in the browser, then restoring them when the audio engine starts up.
Related: issue #68 (Album 1 BGM + SFX bus), guardrail #17 in
.cursor/skills/yieldomega-guardrails/SKILL.md.Background / current state
frontend/src/audio/:WebAudioMixer.tsowns the singleHTMLAudioElement+MediaElementAudioSourceNodeand thetrackIndexcounter.AudioEngineProvider.tsxwires the mixer, attempts autoplay on load, and unlocks audio on the first user gesture.audioPreferences.tsalready persists volume / mute prefs tolocalStorageunder the key prefixyieldomega:audio:v1:(prefsslot).new WebAudioMixer(...)starts withtrackIndex = 0andplayBgm()always setsaudioEl.srcto the first track fromALBUM_1_PLAYLISTand starts at offset 0.So the persistence machinery exists for prefs but is not used for playlist position.
Work to be done
Define a v1 playback-state record in
frontend/src/audio/audioPreferences.ts(or a siblingaudioPlaybackState.tsif it keeps the prefs file focused on mix settings). Suggested shape:Tracking in the browser session. Use
window.localStorage(same pattern assaveAudioPrefs) under a new key, e.g.yieldomega:audio:v1:playbackState. Persistence triggers:advanceAfterTrackEnded): write newtrackIndex,positionSec = 0.skipBgmNext): write newtrackIndex,positionSec = 0.pauseBgm): write currentaudioEl.currentTime.timeupdatelistener while playing: write at most once every ~3–5 seconds (throttle / coalesce;timeupdatefires ~4×/sec) to avoid localStorage thrash.visibilitychange→ hidden and onpagehide/beforeunload: flush a final snapshot synchronously.Use
localStorage(notsessionStorage) so a fresh tab on the same origin still resumes; keep a single key so multiple tabs eventually converge (last-writer wins is acceptable for v1).Restore on startup in
WebAudioMixer:hydrateFromStorage()called beforeplayBgm), read the saved state.trackIdstill exists inALBUM_1_PLAYLIST. If yes, setthis.trackIndexto the matching index. If the id is missing (playlist edited), fall back to clamped numeric index, and if that fails fall back to 0 / 0.positionSecsoplayBgm()can apply it afteraudioEl.srcis set. Apply viaaudioEl.currentTime = positionSeconce metadata is loaded (loadedmetadataevent) or right beforeplay()ifreadyState >= 1.trackIndexthrough the existingonTrackChangecallback so the dock title inAlbumPlayerBarreflects the resumed track immediately, even before audio unlock.Autoplay-blocked behavior. When the browser blocks autoplay, BGM does not start until the first user gesture. The dock currently shows "Tap anywhere or ▶ if autoplay was blocked". Restoration must still apply: when the unlock gesture finally fires
playBgm(), the mixer should resume at the saved track + offset (not reset to 0/0).Skip / next semantics. Pressing "next track" should not preserve the previous offset on the new track (always start the new track at 0); this matches today's behavior and the snapshot-on-skip rule above.
Stale state handling. If
savedAtis older than, say, 7 days, treat it as expired and start at the savedtrackIndexbutpositionSec = 0(or fall back to 0/0). Keep this threshold a single named constant.Quota / private mode. Wrap reads/writes in try/catch like
loadAudioPrefs/saveAudioPrefsalready do; any failure must degrade silently to "start at 0/0". No console spam.Tests. Add unit tests next to
audioPreferences.test.ts(audioPlaybackState.test.tsor extend the existing file):positionSecto non-negative finite numberspositionSec = 0Mixer-level behavior (apply
currentTimeonloadedmetadata, persist on pause / skip / ended / visibility change) should be covered by tests where practical; if jsdom can't driveHTMLAudioElementevents, add at minimum a small adapter unit-tested in isolation.Docs. Update:
docs/frontend/sound-effects-recommendations.md§8 (Album 1 / SFX bus) to describe playback-state persistence alongside the existing autoplay + first-gesture behavior.docs/testing/invariants-and-business-logic.mdrow for issue #68 (or add a sibling row for this issue) so the new persistence + TTL is part of the test map..cursor/skills/yieldomega-guardrails/SKILL.mdguardrail #17.How to track in the browser session
window.localStorage, keyyieldomega:audio:v1:playbackState(matches the existingyieldomega:audio:v1:prefix used byaudioPreferences.ts).AudioPlaybackStateV1(trackIndex, trackId, positionSec, savedAt).timeupdate(~3–5s),visibilitychange→ hidden,pagehide/beforeunloadflush.playBgm()call; restored state must apply both for autoplay-success and for autoplay-blocked → first-gesture paths.Acceptance criteria
localStorageis unavailable, throws on write, contains corrupt JSON, references a track id no longer inALBUM_1_PLAYLIST, or is older than the TTL constant, the player falls back gracefully to track 1 / 00:00 with no console errors.AlbumPlayerBarreflects the restored track immediately on mount (not "track 1" briefly flashing before correcting).localStoragewrites faster than ~once every 3 seconds while idle-playing (verify via a short manual log or test).pnpm --filter frontend test(or the equivalent vitest invocation already used infrontend/).docs/frontend/sound-effects-recommendations.mdanddocs/testing/invariants-and-business-logic.mdupdated; guardrail #17 cross-references the new behavior.Out of scope
mentioned in commit
948cbbf704Implemented (merged to
main)BGM track + offset persistence for Album 1 per the issue spec:
yieldomega:audio:v1:playbackState— JSON withtrackId(manifest-stablealbumId/file),trackIndex,positionSec,savedAt.frontend/src/audio/audioPlaybackState.ts— load/save, 7-day TTL (offset → 0, keep track), clamping, 4screateMinIntervalGatefor periodic writes.WebAudioMixerhydrates on construct; seek afterloadedmetadatathenplay()so autoplay-blocked sessions still resume on first gesture; persist on pause, throttled timeupdate, skip, natural ended, visibility hidden, pagehide/beforeunload.AudioEngineProviderinitialtrackIndexfromloadAudioPlaybackStateso the ticker/title matches the restored track immediately.AlbumTrackgainsid+durationSecfrom the manifest for validation/clamping.audioPlaybackState.test.ts+ updatedalbumPlaylist.test.ts.docs/frontend/sound-effects-recommendations.md§8, new invariants section + table row,timecurve-views.md,docs/agents/metadata-and-skills.md, guardrail #17,skills/verify-yo-album-bgm-resume/SKILL.md+skills/README.md.@brouie could you verify on Chromium + Firefox using the checklist below? Leaving the issue open until you sign off.
Checklist for verification
playbackStatesavedAt does not update faster than ~3–5s during steady play unless pause/skip/hide/unload.Play skill for agents:
skills/verify-yo-album-bgm-resume/SKILL.mdCommit:
948cbbfonmain.mentioned in issue #72
mentioned in issue #100
Source-level pre-walkthrough on
948cbbf(carried into243e2a0).src/audio/audioPlaybackState.tsexists withSTORAGE_PREFIX = "yieldomega:audio:v1:"andAUDIO_PLAYBACK_STALE_MS = 7 * 24 * 60 * 60 * 1000(= 604,800,000 ms = 7 days, matches your spec).nowMs - savedAt > AUDIO_PLAYBACK_STALE_MS— clamps offset path on stale entries per acceptance.createMinIntervalGate(line 162) is the 4s throttle gate referenced in your commit notes.audioPlaybackState.test.ts: 7 tests PASS in 4ms vianpx vitest run audioPlaybackState.Note — your 4/26 comment links
skills/verify-yo-album-bgm-resume/SKILL.md, which was removed during #100 cleanup. Migrated home is nowmanual-qa-checklists.md#manual-qa-issue-71. Just flagging in case you want to update the stale path; not blocking.cc @PlasticDigits
Yes update the stale path
mentioned in commit
3806356154mentioned in commit
06a94ad765Doc fix merged (canonical #71 verification path)
Commit:
3806356onmain.Change: Resolved the stale bookmark noted by @brouie — anything that pointed at the removed
skills/verify-yo-album-bgm-resume/SKILL.md(#100) now documents Album 1 BGM resume —manual-qa-checklists.md#manual-qa-issue-71as the only contributor/agent checklist. Cross-linked:skills/README.md,docs/agents/metadata-and-skills.md, invariants summary row + prose,sound-effects-recommendations.md§8,timecurve-views.md, guardrail #17 (Cursor skill).@brouie please re-verify bookmarks and any saved links hit the anchor above (no 404 skill path).
Checklist for you
docs/testing/manual-qa-checklists.md§ Album 1 BGM resume — anchor resolves in browser/GitLab UI.skills/README.mdcontributor paragraph links #71 + notes removal of verify-yo-album skill.skills/verify-yo-album-bgm-resume/— use manual QA TOC row for #71 instead.mentioned in commit
33d8d07331