Handoff real între sunete: cel vechi cântă până când cel nou chiar se aude#41
Handoff real între sunete: cel vechi cântă până când cel nou chiar se aude#41adyz wants to merge 8 commits into
Conversation
…audible
play() only INITIATES playback (async decode/buffer) — the previous
play-before-stop ordering still left a real silence gap, and on a locked
iPhone that gap kills the audio session and gets the pending play()
denied (the reported "loading is audible but the error sound never
starts on the lock screen, until I fiddle with the buttons").
audioInstance now tracks a pending start ('playing' event settles it)
and stop() defers the actual stop while the partner sound is still
starting. If iOS keeps denying the new sound, the old one keeps playing
(any sound beats silence) and the supervisor keeps retrying. A user
stop/pause still silences both immediately: stopping a pending sound
releases its partner's deferred stop.
The 'offline station change' e2e assertion moves from an instant
loadingNoise.paused check to waiting for the handoff to complete —
deliberate behavior change, the test's intent (only the error sound
ends up audible) is unchanged. 37/37 e2e, 63/63 unit, typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CI Summary
|
Field testing showed backgrounded iOS denies FRESH playback starts outright: with the handoff in place the loading sound now keeps playing (no more silence), but the error element still never starts. iOS does allow an already-playing element to swap sources and continue (playlist-style) — so when a sound has not managed to start by a supervisor tick while its partner is audibly playing, it hijacks the partner element: swaps its src to the right tone and lets it continue. - One takeover attempt per play cycle; reverts to the partner's own sound if even the continuation is denied (never trade audible for silent). - play()/stop() reclaim/reset a borrowed element automatically; if the rightful element later starts, the borrowed one is released by the existing deferred-stop handoff. Typecheck clean, 63/63 unit, 37/37 e2e (untouched this time). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Escaladare după testul lui Adrian pe telefon (handoff-ul a eliminat liniștea — loading-ul continuă să cânte — dar eroarea tot nu pornea): iOS în background refuză orice pornire proaspătă de element audio, permite doar continuarea unuia care deja rulează (așa merg playlisturile în background). Noul commit adaugă exact asta ca ultimă soluție: dacă sunetul de eroare nu a reușit să pornească până la tick-ul supervizorului iar elementul de loading încă se aude, elementul de loading e împrumutat — i se schimbă sursa la tonul de eroare și continuă (continuare, nu pornire nouă). O singură tentativă per ciclu, cu revert dacă și continuarea e refuzată (nu schimbăm niciodată audibil pe mut). Dacă elementul de eroare pornește totuși între timp, împrumutul se eliberează automat prin handoff-ul existent. De retestat pe telefon: play → net tăiat → lock screen → ar trebui să auzi tonul de eroare (chiar dacă tehnic iese din elementul de loading). |
macOS ties the Now Playing widget to the tab's active media element and
drops the metadata when a different one takes over. With the sound
handoff/hijack the element switch now happens AFTER the state
transition (the old sound stops only once the new one is audible), so
the metadata set by updateMediaSession got detached — the widget went
blank in the error state while it still worked during loading.
Keep the last MediaMetadataInit and re-set it wherever we already
re-assert handlers/playbackState on element switches ('play'/'playing'
and 'pause' listeners of both feedback sounds).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setting mediaSession.metadata synchronously inside a media element 'play'/'playing'/'pause' handler gets clobbered by WebKit's internal Now Playing refresh that runs right after the event — which blanked the macOS widget even during loading. Defer the re-assert by 150ms (deduped across bursts of element events) so it lands after WebKit's update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two error-state-specific causes, on top of the deferred re-assert: - The main <audio> element dies completely there (pause + src='') and its events trigger WebKit's own Now Playing refresh after our sfx- driven re-asserts had already run. Re-assert (deferred, deduped) from the main player's 'pause' and 'emptied' events too. - The widget fetches artwork itself, bypassing our SW cache — offline, the failing Cloudinary fetch can take the whole entry with it. When navigator.onLine is false, set the metadata without artwork: a title without an image beats neither. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the watchdog Turning wifi off mid-playback left ~5-6s of silence: the buffer drains fast but the watchdog needs 3 frozen ticks to declare the stream dead. When the browser itself reports the network is gone there is nothing to wait for — enter the audible retry/error pipeline immediately. The watchdog remains the detector for wifi-without-internet, where no 'offline' event ever fires. Two new unit tests (65 total); e2e untouched, 37/37. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three attempts (deferred re-assert, main-player event hooks, offline artwork removal) did not fix the blank widget in the offline error state on macOS — an acceptable edge case (offline on a laptop is rare). Keep the branch focused on what field-testing confirmed: the iOS lock-screen sound handoff/takeover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- One consolidated protocol comment atop the sfx section documenting the two device-verified iOS rules and the three mechanisms (deferred stop, carry, reclaim); hijackWith/borrowedSrc renamed to the calmer carrySound/carriedSrc; per-method docs on SfxInstance. - New e2e (white-box exception, same rationale as the cache-versioning describe): patches the error element's play() to reject like backgrounded iOS, then asserts the audible outcome — the loading element keeps playing, carries the error tone (src switches away from its own sound), the denied element never starts, and recovery still silences everything once the network returns. 65/65 unit, 38/38 e2e, typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Închis la decizia lui Adrian: mecanismul handoff/carry funcționa pe device, dar contrazice filozofia proiectului (state machine = precizie, fără sunete suprapuse, fără coordonare event-driven estimată). Cunoștințele câștigate rămân documentate: (1) iOS în background refuză orice pornire proaspătă de element audio; (2) permite unui element care deja cântă să-și schimbe src și să continue. Direcția agreată pentru o eventuală reluare: UN SINGUR element de feedback («canal») cu setTone dirijat declarativ de state machine — fără handoff, overlap imposibil prin construcție. |
…omes R4b, next after R4 Adrian's explicit requirement: the error sound must be audible on the lock screen FROM THE FIRST TIME, not only after an error cycle with the app open. The play-then-lock repro is a requirement bug, not an accepted limitation. PR #41 proved the mechanism works on device; R4b rebuilds it machine-driven, ordered before R5/R6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix-ul pentru "din prima nu aud eroarea pe lock screen, dar dacă frec butoanele merge" (testat de Adrian pe preview).
Problema
play()pe un element audio doar inițiază pornirea (decodare/buffer, async). Reordonarea play-înainte-de-stop din PR-ul anterior nu garanta suprapunerea: sunetul vechi era oprit imediat după inițierea celui nou, deci rămânea un gol real de liniște. Pe iPhone blocat, în golul ăla moare sesiunea audio a aplicației și iOS refuzăplay()-ul în curs — de-aia loading-ul (pornit lângă gestul userului) se auzea, iar eroarea (pornită la ~9s de ultimul gest) nu.Fix
audioInstanceurmărește acum starea "pornire în curs" (până la evenimentulplaying), iarstop()-ul sunetului vechi se amână până când înlocuitorul chiar produce audio:Teste
offline station change plays the error sound, not the loading one— verifica instantaneu că loading-ul e oprit; acum așteaptă finalizarea handoff-ului. Intenția testului (în final se aude doar eroarea) e neschimbată.🤖 Generated with Claude Code