Repository navigation
Abort a blocked play()'s downloads (5.0.0-alpha.2) - #32
Conversation
Moving a track out of the window now aborts its download, drops a decode in progress and releases its decoded buffer, instead of only cancelling an unfinished fetch. Late fetch/decode results from an unloaded load are ignored.
When the browser refused HTML5 play() (autoplay policy, NotAllowedError), gapless reported onPlayBlocked and the queue paused, but the track stayed in the html5 state with both downloads running: the Web Audio HEAD and GET started alongside play(), and the element, switched to preload=auto, kept buffering. Nothing cancelled them; unload() refuses the current track, and the element is internal. A page whose autoplay was blocked downloaded the whole first track anyway. A blocked play() now sends PLAY_BLOCKED to the track before onPlayBlocked runs. From html5 it returns to idle, as if play() had never been called: isPlaying cleared, progress loop stopped, the element released (source dropped and reattached with preload=none, which aborts its fetch without an error event), and a Web Audio load still in flight aborted, with any decode that finishes later dropped (the same bookkeeping as unload()). The seek target is kept, so the next play() resumes where it would have. The next play() starts both loads fresh. The queue's existing PAUSE then reaches an idle track, which ignores it, so the app sees a stopped queue inside its onPlayBlocked callback.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a24becec58
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.audioBuffer = null; | ||
| this._actor.send({ type: 'UNLOAD' }); |
There was a problem hiding this comment.
Reset playback type before replaying an unloaded track
When a track that previously played through Web Audio leaves the preload window, unloading clears its buffer but leaves context.playbackType as WEBAUDIO. Returning to that track before it is preloaded again starts HTML5 playback, but neither ACTIVATE nor the HTML5 PLAY transition resets this field. Besides reporting the wrong playback type through TrackInfo, this bypasses the bandwidth-contention guard in Queue._preloadAhead, which checks for playbackType === 'HTML5': after jumping away and back, the next track can download concurrently with the current track's fresh fetch, delaying its crossover. Reset the playback type when entering HTML5 playback so replaying an unloaded track follows the same loading policy as its first play.
Useful? React with 👍 / 👎.
Nothing set playbackType back to HTML5. Before tracks could be unloaded that didn't matter: a track that had played on Web Audio kept its buffer, so it replayed on Web Audio. Now unload() can release that buffer, and replaying the track goes through HTML5 with playbackType still WEBAUDIO, which the queue reads in two places: - _preloadAhead's bandwidth guard (defer preloading while the current track is on HTML5 and still loading) didn't apply, so the next track downloaded alongside the current track's fresh fetch, delaying its crossover; - _cancelScheduledGapless skipped cancelling the HTML5 gain mute. The html5 state now sets playbackType to HTML5 on entry: it's HTML5 playback by definition, and both transitions into it get it. Reported by Codex review on #32.
Deploying gapless with
|
| Latest commit: |
0d8f3f1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://95124239.gapless-e1l.pages.dev |
| Branch Preview URL: | https://fix-abort-load-on-play-block.gapless-e1l.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 269f7af4ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this._dropPendingDecode(); | ||
| this.audioBuffer = null; | ||
| this._actor.send({ type: 'UNLOAD' }); |
There was a problem hiding this comment.
Release the finished source node when unloading its buffer
When a Web Audio track ends naturally, WEBAUDIO_ENDED transitions it to idle without clearing sourceNode, and the subsequent idle DEACTIVATE does not clear it either. Consequently, unloading that track only nulls audioBuffer; this.sourceNode.buffer still references the decoded audio. Normal sequential playback therefore retains every completed track's decoded buffer despite moving them outside the preload window, defeating the new memory limit and potentially retaining hundreds of MB per track. Clear or dispose of the finished source node during unloading so the decoded buffer can actually be reclaimed.
Useful? React with 👍 / 👎.
unload() nulled audioBuffer, but a track that ended naturally on Web Audio still held its source node: WEBAUDIO_ENDED goes to idle without stopping it, and DEACTIVATE in idle doesn't either. The node's buffer is the decoded audio, so in ordinary sequential playback every finished track kept its decoded audio (hundreds of MB for a long track) after the queue unloaded it for leaving the preload window. unload() now also releases the source node through _stopSourceNode() (onended cleared, disconnected, dropped). unload() never runs on a playing or scheduled track, so the node it finds is a finished one. Reported by Codex review on #32.
Problem
When the browser refuses HTML5
play()(autoplay policy,NotAllowedError), gapless reportsonPlayBlockedand the queue pauses. The track stays inhtml5, though, with both downloads still running:HEADand GET started alongsideplay().preload = 'auto'and keeps buffering.Nothing cancels either one:
unload()refuses the current track, and the element is internal. So a page whose autoplay is blocked downloads the whole first track anyway. Seen in relisten on Firefox: "Autoplay blocked" prompt shown, track still LOADING, aHEADand two206GETs.Fix
A blocked
play()now sendsPLAY_BLOCKEDto the track beforeonPlayBlockedruns. Fromhtml5the track returns toidle, as ifplay()had never been called:isPlayingcleared, progress loop stoppedpreload = 'none', which aborts its fetch without an error eventunload())play()resumes where it would have and starts both loads freshThe queue's existing
PAUSEthen reaches an idle track, which ignores it, so the app sees a stopped queue insideonPlayBlocked.Tests
tests/unit/play-blocked.test.ts:onPlayBlockedplay()loads againAll three fail without the fix. 427 tests pass; lint, format, types and the build are clean. Not yet tested in a real browser.
Also in this PR
This branch is based on
unload-outside-window, which was published as5.0.0-alpha.1but never merged, so its two commits are included here: unloading tracks outside the preload window, and thealpha.1bump. The last commit bumps to5.0.0-alpha.2. Not published to npm yet.